diff --git a/packages/kami-design/components/Banner/index.tsx b/packages/kami-design/components/Banner/index.tsx
new file mode 100644
index 000000000..889402d37
--- /dev/null
+++ b/packages/kami-design/components/Banner/index.tsx
@@ -0,0 +1,75 @@
+import { clsx } from 'clsx'
+import type { FC } from 'react'
+
+import {
+ ClaritySuccessLine,
+ FluentShieldError20Regular,
+ FluentWarning28Regular,
+ IonInformation,
+} from '../Icons/status'
+
+const IconMap = {
+ warning: FluentWarning28Regular,
+ info: IonInformation,
+ error: FluentShieldError20Regular,
+ success: ClaritySuccessLine,
+}
+
+const bgColorMap = {
+ warning: 'bg-amber-50',
+ info: 'bg-default-blue-50',
+ success: 'bg-default-green-50',
+ error: 'bg-default-red-50',
+}
+
+const borderColorMap = {
+ warning: 'border-amber-300',
+ info: 'border-default-blue-300',
+
+ success: 'border-default-green-300',
+ error: 'border-default-red-300',
+}
+
+const iconColorMap = {
+ warning: 'text-amber-500',
+ info: 'text-default-blue-500',
+ success: 'text-default-green-500',
+ error: 'text-default-red-500',
+}
+
+export const Banner: FC<{
+ type: 'warning' | 'error' | 'success' | 'info'
+ message?: string | React.ReactNode
+ className?: string
+ children?: React.ReactNode
+ placement?: 'center' | 'left'
+ showIcon?: boolean
+}> = (props) => {
+ const Icon = IconMap[props.type] || IconMap.info
+ const { placement = 'center', showIcon = true } = props
+ return (
+
+ {showIcon && (
+
+ )}
+ {props.message ? (
+ {props.message}
+ ) : (
+ props.children
+ )}
+
+ )
+}
diff --git a/packages/kami-design/components/CodeBlock/index.tsx b/packages/kami-design/components/CodeBlock/index.tsx
new file mode 100644
index 000000000..2cc0af3c0
--- /dev/null
+++ b/packages/kami-design/components/CodeBlock/index.tsx
@@ -0,0 +1,18 @@
+import { Mermaid } from '../Mermaid'
+
+export const CodeBlock = (props: {
+ lang: string | undefined
+ content: string
+
+ HighLighter: React.ComponentType<{
+ content: string
+ lang: string | undefined
+ }>
+}) => {
+ const { HighLighter } = props
+ if (props.lang === 'mermaid') {
+ return
+ } else {
+ return
+ }
+}
diff --git a/packages/kami-design/components/Divider/index.tsx b/packages/kami-design/components/Divider/index.tsx
new file mode 100644
index 000000000..eb5bbf451
--- /dev/null
+++ b/packages/kami-design/components/Divider/index.tsx
@@ -0,0 +1,34 @@
+import { clsx } from 'clsx'
+import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'
+
+export const Divider: FC<
+ DetailedHTMLProps, HTMLHRElement>
+> = (props) => {
+ const { className, ...rest } = props
+ return (
+
+ )
+}
+
+export const DividerVertical: FC<
+ DetailedHTMLProps, HTMLSpanElement>
+> = (props) => {
+ const { className, ...rest } = props
+ return (
+
+ w
+
+ )
+}
diff --git a/packages/kami-design/components/FloatPopover/index.module.css b/packages/kami-design/components/FloatPopover/index.module.css
new file mode 100644
index 000000000..dea25d6c5
--- /dev/null
+++ b/packages/kami-design/components/FloatPopover/index.module.css
@@ -0,0 +1,21 @@
+.popover-root {
+ @apply p-4 relative bg-light-bg rounded-lg overflow-hidden z-2 shadow-out-sm;
+}
+
+////
+.popover-root,
+.headless {
+ opacity: 0;
+ transform: translateY(-10px);
+ transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
+
+ @apply delay-100;
+}
+
+.show,
+.headless.show {
+ opacity: 1;
+ transform: translateY(0);
+
+ @apply delay-0;
+}
diff --git a/packages/kami-design/components/FloatPopover/index.tsx b/packages/kami-design/components/FloatPopover/index.tsx
new file mode 100644
index 000000000..86d2f06dc
--- /dev/null
+++ b/packages/kami-design/components/FloatPopover/index.tsx
@@ -0,0 +1,221 @@
+import { clsx } from 'clsx'
+import type { FC } from 'react'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useClickAway } from 'react-use'
+
+import type { UseFloatingProps } from '@floating-ui/react-dom'
+import { flip, offset, shift, useFloating } from '@floating-ui/react-dom'
+
+import { NoSSRWrapper } from '~/utils/no-ssr'
+
+import { RootPortal } from '../Portal'
+import styles from './index.module.css'
+
+export const FloatPopover: FC<
+ {
+ triggerComponent: FC
+ headless?: boolean
+ wrapperClassNames?: string
+ trigger?: 'click' | 'hover' | 'both'
+ padding?: number
+ offset?: number
+ popoverWrapperClassNames?: string
+
+ /**
+ * 不消失
+ */
+ debug?: boolean
+ } & UseFloatingProps
+> = NoSSRWrapper(
+ memo((props) => {
+ const {
+ headless = false,
+ wrapperClassNames,
+ triggerComponent: TriggerComponent,
+ trigger = 'hover',
+ padding,
+ offset: offsetValue,
+ popoverWrapperClassNames,
+ debug,
+ ...rest
+ } = props
+ const [mounted, setMounted] = useState(false)
+ const { x, y, reference, floating, strategy, update } = useFloating({
+ middleware: rest.middleware ?? [
+ flip({ padding: padding ?? 20 }),
+ offset(offsetValue ?? 10),
+ shift(),
+ ],
+ strategy: rest.strategy,
+ placement: rest.placement ?? 'bottom-start',
+ whileElementsMounted: rest.whileElementsMounted,
+ })
+ const [currentStatus, setCurrentStatus] = useState(false)
+ const [open, setOpen] = useState(false)
+ const updateOnce = useRef(false)
+ const doPopoverShow = useCallback(() => {
+ setCurrentStatus(true)
+ setMounted(true)
+
+ if (!updateOnce.current) {
+ requestAnimationFrame(() => {
+ update()
+ updateOnce.current = true
+ })
+ }
+ }, [])
+
+ const [containerAnchorRef, setContainerAnchorRef] =
+ useState()
+ const containerRef = useRef(null)
+
+ const handleTransition = useCallback(
+ (status: 'in' | 'out') => {
+ const nextElementSibling =
+ containerAnchorRef?.nextElementSibling as HTMLDivElement
+
+ if (!nextElementSibling) {
+ return
+ }
+
+ if (status === 'in') {
+ nextElementSibling.ontransitionend = null
+ nextElementSibling?.classList.add(styles.show)
+ } else {
+ nextElementSibling?.classList.remove(styles.show)
+ nextElementSibling!.ontransitionend = () => {
+ setOpen(false)
+ setMounted(false)
+ }
+ }
+ },
+ [containerAnchorRef?.nextElementSibling],
+ )
+
+ useEffect(() => {
+ if (!containerAnchorRef) {
+ return
+ }
+
+ if (currentStatus) {
+ setOpen(true)
+ requestAnimationFrame(() => {
+ handleTransition('in')
+ })
+ } else {
+ requestAnimationFrame(() => {
+ handleTransition('out')
+ })
+ }
+ }, [currentStatus, containerAnchorRef, handleTransition])
+
+ useClickAway(containerRef, () => {
+ if (trigger == 'click' || trigger == 'both') {
+ doPopoverDisappear()
+ clickTriggerFlag.current = false
+ }
+ })
+
+ const doPopoverDisappear = useCallback(() => {
+ if (debug) {
+ return
+ }
+ setCurrentStatus(false)
+ }, [debug])
+
+ const clickTriggerFlag = useRef(false)
+ const handleMouseOut = useCallback(() => {
+ if (clickTriggerFlag.current === true) {
+ return
+ }
+ doPopoverDisappear()
+ }, [])
+ const handleClickTrigger = useCallback(() => {
+ clickTriggerFlag.current = true
+ doPopoverShow()
+ }, [])
+
+ const listener = useMemo(() => {
+ const baseListener = {
+ onFocus: doPopoverShow,
+ onBlur: doPopoverDisappear,
+ }
+ switch (trigger) {
+ case 'click':
+ return {
+ ...baseListener,
+ onClick: doPopoverShow,
+ }
+ case 'hover':
+ return {
+ ...baseListener,
+ onMouseOver: doPopoverShow,
+ onMouseOut: doPopoverDisappear,
+ }
+ case 'both':
+ return {
+ ...baseListener,
+ onClick: handleClickTrigger,
+ onMouseOver: doPopoverShow,
+ onMouseOut: handleMouseOut,
+ }
+ }
+ }, [
+ doPopoverDisappear,
+ doPopoverShow,
+ handleClickTrigger,
+ handleMouseOut,
+ trigger,
+ ])
+
+ const TriggerWrapper = (
+
+
+
+ )
+
+ if (!props.children) {
+ return TriggerWrapper
+ }
+
+ return (
+ <>
+ {TriggerWrapper}
+
+ {mounted && (
+
+
+
+ {open && (
+
+ {props.children}
+
+ )}
+
+
+ )}
+ >
+ )
+ }),
+)
diff --git a/packages/kami-design/components/Icons/arrow.tsx b/packages/kami-design/components/Icons/arrow.tsx
new file mode 100644
index 000000000..a9251d6f9
--- /dev/null
+++ b/packages/kami-design/components/Icons/arrow.tsx
@@ -0,0 +1,45 @@
+import type { SVGProps } from 'react'
+
+export function IcRoundKeyboardDoubleArrowLeft(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IcRoundKeyboardDoubleArrowRight(
+ props: SVGProps,
+) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/emoji.tsx b/packages/kami-design/components/Icons/emoji.tsx
new file mode 100644
index 000000000..0b182b45c
--- /dev/null
+++ b/packages/kami-design/components/Icons/emoji.tsx
@@ -0,0 +1,111 @@
+import type { SVGProps } from 'react'
+
+export function FaSolidSmile(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidSadCry(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidSadTear(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidAngry(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidTired(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidMeh(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidGrinSquintTears(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidFrownOpen(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidGrimace(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidFlushed(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-comment.tsx b/packages/kami-design/components/Icons/for-comment.tsx
new file mode 100644
index 000000000..59148475a
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-comment.tsx
@@ -0,0 +1,118 @@
+import type { FC, SVGProps } from 'react'
+import { memo } from 'react'
+
+export function SiGlyphGlobal(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PhUser(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiEmailFastOutline(props: SVGProps) {
+ return (
+
+ )
+}
+
+export const EmptyIcon: FC = memo(() => {
+ return (
+
+ )
+})
+
+export function GridiconsNoticeOutline(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function LaUserSecret(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function RadixIconsAvatar(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-fav.tsx b/packages/kami-design/components/Icons/for-fav.tsx
new file mode 100644
index 000000000..99d348dc7
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-fav.tsx
@@ -0,0 +1,43 @@
+import type { SVGProps } from 'react'
+
+export function MusicIcon(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PauseIcon(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function RiNeteaseCloudMusicFill(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-footer.tsx b/packages/kami-design/components/Icons/for-footer.tsx
new file mode 100644
index 000000000..876fe5153
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-footer.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react'
+
+export function BxBxsArrowToTop(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidHeadphonesAlt(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-home.tsx b/packages/kami-design/components/Icons/for-home.tsx
new file mode 100644
index 000000000..76a011805
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-home.tsx
@@ -0,0 +1,50 @@
+import type { SVGProps } from 'react'
+
+export function MdiDrawPen(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PhUsersDuotone(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FaSolidKissWinkHeart(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IcBaselineArrowForwardIos(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-login.tsx b/packages/kami-design/components/Icons/for-login.tsx
new file mode 100644
index 000000000..f7196db0b
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-login.tsx
@@ -0,0 +1,13 @@
+import type { SVGProps } from 'react'
+
+export function CarbonPassword(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-note.tsx b/packages/kami-design/components/Icons/for-note.tsx
new file mode 100644
index 000000000..f7ee951e9
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-note.tsx
@@ -0,0 +1,102 @@
+import type { SVGProps } from 'react'
+
+export function PhBookOpen(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function SolidBookmark(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiTagHeartOutline(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function GgCoffee(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MaterialSymbolsArrowCircleRightOutlineRounded(
+ props: SVGProps,
+) {
+ return (
+
+ )
+}
+
+export function MdiClockOutline(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiClockTimeThreeOutline(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiFountainPenTip(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-post.tsx b/packages/kami-design/components/Icons/for-post.tsx
new file mode 100644
index 000000000..0ceef86f3
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-post.tsx
@@ -0,0 +1,80 @@
+import type { SVGProps } from 'react'
+
+export function EntypoCreativeCommons(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IonThumbsup(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FeHash(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiCalendar(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PhPushPinFill(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PhPushPin(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/for-recently.tsx b/packages/kami-design/components/Icons/for-recently.tsx
new file mode 100644
index 000000000..ff99a9628
--- /dev/null
+++ b/packages/kami-design/components/Icons/for-recently.tsx
@@ -0,0 +1,29 @@
+import type { SVGProps } from 'react'
+
+export function JamTrash(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PhLinkFill(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/layout.tsx b/packages/kami-design/components/Icons/layout.tsx
new file mode 100644
index 000000000..240e11e57
--- /dev/null
+++ b/packages/kami-design/components/Icons/layout.tsx
@@ -0,0 +1,113 @@
+import type { SVGProps } from 'react'
+
+export function BiMoonStarsFill(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function PhSunBold(props: SVGProps) {
+ return (
+
+ )
+}
+
+export const CloseIcon = () => (
+
+)
+
+export function JamTags(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IonSearch(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function RegularBookmark(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IcBaselineMenuOpen(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FluentEyeHide20Regular(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiShare(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/menu-icon.tsx b/packages/kami-design/components/Icons/menu-icon.tsx
new file mode 100644
index 000000000..414b9f67b
--- /dev/null
+++ b/packages/kami-design/components/Icons/menu-icon.tsx
@@ -0,0 +1,250 @@
+import type { SVGProps } from 'react'
+
+export function FaSolidCircleNotch(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidComment(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidComments(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidFeatherAlt(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidHistory(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidSubway(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidUserFriends(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidCircle(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidDotCircle(props: SVGProps) {
+ return (
+
+ )
+}
+export function FaSolidTv(props: SVGProps) {
+ return (
+
+ )
+}
+export function IconParkOutlineTencentQq(props: SVGProps) {
+ return (
+
+ )
+}
+export function MdiFlask(props: SVGProps) {
+ return (
+
+ )
+}
+export function MdiTwitter(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IcTwotoneSignpost(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IonBook(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function RiNeteaseCloudMusicLine(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IcBaselineLiveTv(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function JamRssFeed(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function AkarIconsMention(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function CodiconGithubInverted(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IcBaselineTelegram(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/shared.tsx b/packages/kami-design/components/Icons/shared.tsx
new file mode 100644
index 000000000..2332ce424
--- /dev/null
+++ b/packages/kami-design/components/Icons/shared.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react'
+
+export function FluentList16Filled(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/status.tsx b/packages/kami-design/components/Icons/status.tsx
new file mode 100644
index 000000000..1da9c768b
--- /dev/null
+++ b/packages/kami-design/components/Icons/status.tsx
@@ -0,0 +1,69 @@
+import type { SVGProps } from 'react'
+
+export function FluentWarning28Regular(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function FluentShieldError20Regular(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function ClaritySuccessLine(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function IonInformation(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Icons/weather.tsx b/packages/kami-design/components/Icons/weather.tsx
new file mode 100644
index 000000000..1d089f979
--- /dev/null
+++ b/packages/kami-design/components/Icons/weather.tsx
@@ -0,0 +1,56 @@
+import type { SVGProps } from 'react'
+
+export function RiSunCloudyFill(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiCloud(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function MdiSnowflake(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function BiCloudRainFill(props: SVGProps) {
+ return (
+
+ )
+}
+
+export function BiCloudLightningRainFill(props: SVGProps) {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Markdown/components/gallery/index.module.css b/packages/kami-design/components/Markdown/components/gallery/index.module.css
new file mode 100644
index 000000000..23aa6d1ba
--- /dev/null
+++ b/packages/kami-design/components/Markdown/components/gallery/index.module.css
@@ -0,0 +1,34 @@
+.root {
+ &:hover .indicator {
+ opacity: 1;
+ }
+}
+
+.container {
+ scroll-snap-type: x mandatory;
+ display: flex;
+ align-items: flex-start;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+.child {
+ scroll-snap-align: center;
+ flex-shrink: 0;
+
+ text-align: center;
+}
+
+.child:last-child {
+ margin-right: 0 !important;
+}
+
+.indicator {
+ @apply absolute bottom-[24px] left-[50%] flex bg-bg-opacity rounded-[24px] z-1 px-6 py-4 opacity-0;
+ @apply transition-opacity duration-300;
+
+ transform: translateX(-50%);
+ backdrop-filter: blur(20px) saturate(180%);
+}
diff --git a/packages/kami-design/components/Markdown/components/gallery/index.tsx b/packages/kami-design/components/Markdown/components/gallery/index.tsx
new file mode 100644
index 000000000..170d0cddd
--- /dev/null
+++ b/packages/kami-design/components/Markdown/components/gallery/index.tsx
@@ -0,0 +1,223 @@
+import clsx from 'clsx'
+import { throttle } from 'lodash-es'
+import type { FC, UIEventHandler } from 'react'
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import { useInView } from 'react-intersection-observer'
+
+import { ImageLazy } from '~/components/universal/Image'
+import { ImageSizeMetaContext } from '~/context'
+import { useStateRef } from '~/hooks/use-state-ref'
+import { calculateDimensions } from '~/utils/images'
+
+import type { MImageType } from '../../utils/image'
+import styles from './index.module.css'
+
+const IMAGE_CONTAINER_MARGIN_INSET = 60
+const CHILD_GAP = 15
+const AUTOPLAY_DURATION = 5000
+
+interface GalleryProps {
+ images: MImageType[]
+}
+
+export const Gallery: FC = (props) => {
+ const { images } = props
+ const imageMeta = useContext(ImageSizeMetaContext)
+ const [containerRef, setContainerRef] = useState(null)
+ const containerWidth = useMemo(
+ () => containerRef?.clientWidth || 0,
+ [containerRef?.clientWidth],
+ )
+
+ const [, setUpdated] = useState({})
+ const memoedChildContainerWidthRef = useRef(0)
+
+ useEffect(() => {
+ if (!containerRef) {
+ return
+ }
+
+ const ob = new ResizeObserver(() => {
+ setUpdated({})
+ calChild(containerRef)
+ })
+ function calChild(containerRef: HTMLDivElement) {
+ const $child = containerRef.children.item(0)
+ if ($child) {
+ memoedChildContainerWidthRef.current = $child.clientWidth
+ }
+ }
+
+ calChild(containerRef)
+
+ ob.observe(containerRef)
+ return () => {
+ ob.disconnect()
+ }
+ }, [containerRef])
+
+ const childStyle = useRef({
+ width: `calc(100% - ${IMAGE_CONTAINER_MARGIN_INSET}px)`,
+ marginRight: `${CHILD_GAP}px`,
+ }).current
+
+ const [currentIndex, setCurrentIndex] = useState(0)
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const handleOnScroll: UIEventHandler = useCallback(
+ throttle>((e) => {
+ const $ = e.target as HTMLDivElement
+
+ const index = Math.floor(
+ ($.scrollLeft + IMAGE_CONTAINER_MARGIN_INSET + 15) /
+ memoedChildContainerWidthRef.current,
+ )
+ setCurrentIndex(index)
+ }, 60),
+ [],
+ )
+ const handleScrollTo = useCallback(
+ (i: number) => {
+ if (!containerRef) {
+ return
+ }
+
+ containerRef.scrollTo({
+ left: memoedChildContainerWidthRef.current * i,
+ behavior: 'smooth',
+ })
+ },
+ [containerRef],
+ )
+
+ const autoplayTimerRef = useRef(null as any)
+
+ const currentIndexRef = useStateRef(currentIndex)
+ const totalImageLengthRef = useStateRef(images.length)
+
+ // 向后翻页状态
+ const isForward = useRef(true)
+
+ const autoplayRef = useRef(true)
+ const handleCancelAutoplay = useCallback(() => {
+ if (!autoplayRef.current) {
+ return
+ }
+
+ autoplayRef.current = false
+ clearInterval(autoplayTimerRef.current)
+ }, [])
+
+ const { ref } = useInView({
+ initialInView: false,
+ triggerOnce: images.length < 2,
+ onChange(inView) {
+ if (totalImageLengthRef.current < 2 || !autoplayRef.current) {
+ return
+ }
+ if (inView) {
+ autoplayTimerRef.current = setInterval(() => {
+ if (
+ currentIndexRef.current + 1 > totalImageLengthRef.current - 1 &&
+ isForward.current
+ ) {
+ isForward.current = false
+ }
+ if (currentIndexRef.current - 1 < 0 && !isForward.current) {
+ isForward.current = true
+ }
+
+ const index = currentIndexRef.current + (isForward.current ? 1 : -1)
+ handleScrollTo(index)
+ }, AUTOPLAY_DURATION)
+ } else {
+ autoplayTimerRef.current = clearInterval(autoplayTimerRef.current)
+ }
+ },
+ })
+
+ useEffect(() => {
+ return () => {
+ clearInterval(autoplayTimerRef.current)
+ }
+ }, [])
+
+ return (
+
+
+ {images.map((image) => {
+ const info = imageMeta.get(image.url)
+ const maxWidth = containerWidth - IMAGE_CONTAINER_MARGIN_INSET
+ const { height, width } = calculateDimensions(
+ info?.width || 0,
+ info?.height || 0,
+ {
+ width: maxWidth,
+
+ height: 600,
+ },
+ )
+ const alt = image.name
+ const title = image.footnote
+ const imageCaption =
+ title ||
+ (['!', '¡'].some((ch) => ch == alt?.[0]) ? alt?.slice(1) : '') ||
+ ''
+ return (
+
+
+
+ )
+ })}
+
+
+
+ {Array.from({
+ length: images.length,
+ }).map((_, i) => {
+ return (
+
+ )
+ })}
+
+
+ )
+}
diff --git a/packages/kami-design/components/Markdown/index.module.css b/packages/kami-design/components/Markdown/index.module.css
new file mode 100644
index 000000000..57f65dc6d
--- /dev/null
+++ b/packages/kami-design/components/Markdown/index.module.css
@@ -0,0 +1,47 @@
+.md {
+ &.code-fully pre > code {
+ max-height: unset !important;
+ }
+
+ summary {
+ list-style: none;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+
+ summary::marker {
+ display: none;
+ }
+
+ details summary::before {
+ content: '+ ';
+ }
+
+ details[open] summary::before {
+ content: '- ';
+ }
+
+ details[open] summary::before,
+ details summary::before {
+ font-weight: 800;
+ font-family: var(--mono-font);
+ }
+
+ sub span,
+ sup span {
+ border: 0 !important;
+ }
+
+ sub,
+ sup {
+ & > a {
+ @apply inline-block;
+ }
+
+ & > a::first-letter {
+ @apply hidden;
+ }
+ }
+}
diff --git a/packages/kami-design/components/Markdown/index.tsx b/packages/kami-design/components/Markdown/index.tsx
new file mode 100644
index 000000000..8b9b40b13
--- /dev/null
+++ b/packages/kami-design/components/Markdown/index.tsx
@@ -0,0 +1,264 @@
+/* eslint-disable react-hooks/rules-of-hooks */
+import { clsx } from 'clsx'
+import range from 'lodash-es/range'
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import { compiler, sanitizeUrl } from 'markdown-to-jsx'
+import type { FC } from 'react'
+import React, {
+ Fragment,
+ memo,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+
+import { isDev } from '~/utils/env'
+import { springScrollToElement } from '~/utils/spring'
+
+import { CodeBlock } from '../CodeBlock'
+import styles from './index.module.css'
+import { CommentAtRule } from './parsers/comment-at'
+import { ContainerRule } from './parsers/container'
+import { InsertRule } from './parsers/ins'
+import { KateXRule } from './parsers/katex'
+import { MarkRule } from './parsers/mark'
+import { MentionRule } from './parsers/mention'
+import { SpoilderRule } from './parsers/spoiler'
+import {
+ MHeading,
+ MImage,
+ MParagraph,
+ MTableBody,
+ MTableHead,
+ MTableRow,
+} from './renderers'
+import { MDetails } from './renderers/collapse'
+import { MFootNote } from './renderers/footnotes'
+
+interface MdProps {
+ value?: string
+ toc?: boolean
+
+ style?: React.CSSProperties
+ readonly renderers?: { [key: string]: Partial }
+ wrapperProps?: React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ HTMLDivElement
+ >
+ codeBlockFully?: boolean
+ className?: string
+ HighLighter: React.ComponentType<{
+ content: string
+ lang: string | undefined
+ }>
+}
+
+export const Markdown: FC = memo((props) => {
+ const {
+ value,
+ renderers,
+ style,
+ wrapperProps = {},
+ codeBlockFully = false,
+ className,
+ overrides,
+ extendsRules,
+ additionalParserRules,
+
+ ...rest
+ } = props
+
+ const [headings, setHeadings] = useState([])
+
+ const ref = useRef(null)
+ useEffect(() => {
+ if (!ref.current) {
+ return
+ }
+
+ const $headings = ref.current.querySelectorAll(
+ range(1, 6)
+ .map((i) => `h${i}`)
+ .join(','),
+ ) as NodeListOf
+
+ setHeadings(Array.from($headings))
+
+ return () => {
+ setHeadings([])
+ }
+ }, [value, props.children])
+
+ const node = useMemo(() => {
+ if (!value && typeof props.children != 'string') return null
+
+ const Heading = MHeading()
+
+ const mdElement = compiler(`${value || props.children}`, {
+ wrapper: null,
+ // @ts-ignore
+ overrides: {
+ p: MParagraph,
+ img: MImage,
+ thead: MTableHead,
+ tr: MTableRow,
+ tbody: MTableBody,
+ // FIXME: footer tag in raw html will renders not as expected, but footer tag in this markdown lib will wrapper as linkReferer footnotes
+ footer: MFootNote,
+ details: MDetails,
+
+ // for custom react component
+ // LinkCard,
+ ...overrides,
+ },
+
+ extendsRules: {
+ link: {
+ // @ts-ignore
+ react(node, output, state) {
+ const { target, title } = node
+ return null
+ //
+ // {output(node.content, state!)}
+ //
+ },
+ },
+ heading: {
+ react(node, output, state) {
+ return (
+
+ {output(node.content, state!)}
+
+ )
+ },
+ },
+
+ footnoteReference: {
+ react(node, output, state) {
+ const { footnoteMap, target, content } = node
+ const footnote = footnoteMap.get(content)
+ const linkCardId = (() => {
+ try {
+ const thisUrl = new URL(footnote?.footnote?.replace(': ', ''))
+ const isCurrentHost =
+ thisUrl.hostname === window.location.hostname
+
+ if (!isCurrentHost && !isDev) {
+ return undefined
+ }
+ const pathname = thisUrl.pathname
+ return pathname.slice(1)
+ } catch {
+ return undefined
+ }
+ })()
+
+ return (
+
+ {
+ e.preventDefault()
+
+ springScrollToElement(
+ document.getElementById(content)!,
+ undefined,
+ -window.innerHeight / 2,
+ )
+ }}
+ >
+ ^{content}
+
+
+ )
+ },
+ },
+ codeBlock: {
+ react(node, output, state) {
+ return (
+
+ )
+ },
+ },
+ gfmTask: {
+ react(node, _, state) {
+ return (
+
+ )
+ },
+ },
+
+ list: {
+ react(node, output, state) {
+ const Tag = node.ordered ? 'ol' : 'ul'
+
+ return (
+
+ {node.items.map((item, i) => {
+ let className = ''
+ if (item[0]?.type == 'gfmTask') {
+ className = 'list-none flex items-center'
+ }
+
+ return (
+
+ {output(item, state!)}
+
+ )
+ })}
+
+ )
+ },
+ },
+
+ ...extendsRules,
+ ...renderers,
+ },
+ additionalParserRules: {
+ spoilder: SpoilderRule,
+ mention: MentionRule,
+ commentAt: CommentAtRule,
+ mark: MarkRule,
+ ins: InsertRule,
+ kateX: KateXRule,
+ container: ContainerRule,
+ ...additionalParserRules,
+ },
+ ...rest,
+ })
+
+ return mdElement
+ }, [
+ value,
+ props.children,
+ overrides,
+ extendsRules,
+ renderers,
+ additionalParserRules,
+ rest,
+ ])
+
+ return (
+
+ {className ?
{node}
: node}
+
+ )
+})
diff --git a/packages/kami-design/components/Markdown/parsers/comment-at.tsx b/packages/kami-design/components/Markdown/parsers/comment-at.tsx
new file mode 100644
index 000000000..23b179b6f
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/comment-at.tsx
@@ -0,0 +1,23 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import {
+ Priority,
+ parseCaptureInline,
+ simpleInlineRegex,
+} from 'markdown-to-jsx'
+import { Fragment } from 'react'
+
+// @
+export const CommentAtRule: MarkdownToJSX.Rule = {
+ match: simpleInlineRegex(/^@(\w+)\s/),
+ order: Priority.LOW,
+ parse: parseCaptureInline,
+ react(node, _, state) {
+ const { content } = node
+
+ if (!content || !content[0]?.content) {
+ return
+ }
+
+ return @{content[0]?.content}
+ },
+}
diff --git a/packages/kami-design/components/Markdown/parsers/container.tsx b/packages/kami-design/components/Markdown/parsers/container.tsx
new file mode 100644
index 000000000..db7f83915
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/container.tsx
@@ -0,0 +1,65 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import { Priority, blockRegex } from 'markdown-to-jsx'
+
+import { Banner } from '../../Banner'
+import { Gallery } from '../components/gallery'
+import { pickImagesFromMarkdown } from '../utils/image'
+
+const shouldCatchContainerName = ['gallery', 'banner', 'carousel'].join('|')
+export const ContainerRule: MarkdownToJSX.Rule = {
+ match: blockRegex(
+ new RegExp(
+ `^\\s*::: *(?(${shouldCatchContainerName})) *({(?(.*?))})? *\n(?[\\s\\S]+?)\\s*::: *(?:\n *)+\n?`,
+ ),
+ ),
+ order: Priority.MED,
+ parse(capture) {
+ const { groups } = capture
+ return {
+ ...groups,
+ }
+ },
+ // @ts-ignore
+ react(node, _, state) {
+ const { name, content, params } = node
+
+ switch (name) {
+ case 'carousel':
+ case 'gallery': {
+ return (
+
+ )
+ }
+ case 'banner': {
+ if (!params) {
+ break
+ }
+
+ return (
+
+ )
+ }
+ }
+
+ return (
+
+ )
+ },
+}
+
+/**
+ * gallery container
+ *
+ * ::: gallery
+ * ![name](url)
+ * ![name](url)
+ * ![name](url)
+ * :::
+ */
diff --git a/packages/kami-design/components/Markdown/parsers/ins.tsx b/packages/kami-design/components/Markdown/parsers/ins.tsx
new file mode 100644
index 000000000..22c0d1bd2
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/ins.tsx
@@ -0,0 +1,18 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import {
+ Priority,
+ parseCaptureInline,
+ simpleInlineRegex,
+} from 'markdown-to-jsx'
+
+// ++Insert++
+export const InsertRule: MarkdownToJSX.Rule = {
+ match: simpleInlineRegex(
+ /^\+\+((?:\[.*?\]|<.*?>(?:.*?<.*?>)?|`.*?`|.)*?)\+\+/,
+ ),
+ order: Priority.LOW,
+ parse: parseCaptureInline,
+ react(node, output, state?) {
+ return {output(node.content, state!)}
+ },
+}
diff --git a/packages/kami-design/components/Markdown/parsers/katex.tsx b/packages/kami-design/components/Markdown/parsers/katex.tsx
new file mode 100644
index 000000000..327bc14e0
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/katex.tsx
@@ -0,0 +1,48 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import {
+ Priority,
+ parseCaptureInline,
+ simpleInlineRegex,
+} from 'markdown-to-jsx'
+import type { FC } from 'react'
+import { useInsertionEffect, useState } from 'react'
+
+import { loadScript, loadStyleSheet } from '~/utils/load-script'
+
+// $ c = \pm\sqrt{a^2 + b^2} $
+export const KateXRule: MarkdownToJSX.Rule = {
+ match: simpleInlineRegex(
+ /^\$\s{1,}((?:\[.*?\]|<.*?>(?:.*?<.*?>)?|`.*?`|.)*?)\s{1,}\$/,
+ ),
+ order: Priority.LOW,
+ parse: parseCaptureInline,
+ react(node, _, state?) {
+ try {
+ const str = node.content.map((item) => item.content).join('')
+
+ return {str}
+ } catch {
+ return null as any
+ }
+ },
+}
+
+const LateX: FC<{ children: string }> = (props) => {
+ const { children } = props
+
+ const [html, setHtml] = useState('')
+
+ useInsertionEffect(() => {
+ loadStyleSheet(
+ 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/KaTeX/0.15.2/katex.min.css',
+ )
+ loadScript(
+ 'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/KaTeX/0.15.2/katex.min.js',
+ ).then(() => {
+ const html = window.katex.renderToString(children)
+ setHtml(html)
+ })
+ }, [])
+
+ return
+}
diff --git a/packages/kami-design/components/Markdown/parsers/mark.tsx b/packages/kami-design/components/Markdown/parsers/mark.tsx
new file mode 100644
index 000000000..8698e183f
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/mark.tsx
@@ -0,0 +1,23 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import {
+ Priority,
+ parseCaptureInline,
+ simpleInlineRegex,
+} from 'markdown-to-jsx'
+
+// ==Mark==
+export const MarkRule: MarkdownToJSX.Rule = {
+ match: simpleInlineRegex(/^==((?:\[.*?\]|<.*?>(?:.*?<.*?>)?|`.*?`|.)*?)==/),
+ order: Priority.LOW,
+ parse: parseCaptureInline,
+ react(node, output, state?) {
+ return (
+
+ {output(node.content, state!)}
+
+ )
+ },
+}
diff --git a/packages/kami-design/components/Markdown/parsers/mention.tsx b/packages/kami-design/components/Markdown/parsers/mention.tsx
new file mode 100644
index 000000000..584e1b294
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/mention.tsx
@@ -0,0 +1,69 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import { Priority, simpleInlineRegex } from 'markdown-to-jsx'
+
+import {
+ CodiconGithubInverted,
+ IcBaselineTelegram,
+ MdiTwitter,
+} from '../../Icons/menu-icon'
+
+const prefixToIconMap = {
+ GH: ,
+ TW: ,
+ TG: ,
+}
+
+const prefixToUrlMap = {
+ GH: 'https://github.com/',
+ TW: 'https://twitter.com/',
+ TG: 'https://t.me/',
+}
+
+// {GH@Innei} {TW@Innei} {TG@Innei}
+export const MentionRule: MarkdownToJSX.Rule = {
+ match: simpleInlineRegex(
+ /^\{((?(GH)|(TW)|(TG))@(?\w+\b))\}\s?(?!\[.*?\])/,
+ ),
+ order: Priority.MIN,
+ parse(capture) {
+ const { groups } = capture
+
+ if (!groups) {
+ return {}
+ }
+ return {
+ content: { prefix: groups.prefix, name: groups.name },
+ type: 'mention',
+ }
+ },
+ react(result, _, state) {
+ const { content } = result
+ if (!content) {
+ return null as any
+ }
+
+ const { prefix, name } = content
+ if (!name) {
+ return null as any
+ }
+
+ const Icon = prefixToIconMap[prefix]
+ const urlPrefix = prefixToUrlMap[prefix]
+
+ return (
+
+ )
+ },
+}
diff --git a/packages/kami-design/components/Markdown/parsers/spoiler.tsx b/packages/kami-design/components/Markdown/parsers/spoiler.tsx
new file mode 100644
index 000000000..5f122a519
--- /dev/null
+++ b/packages/kami-design/components/Markdown/parsers/spoiler.tsx
@@ -0,0 +1,22 @@
+import type { MarkdownToJSX } from 'markdown-to-jsx'
+import {
+ Priority,
+ parseCaptureInline,
+ simpleInlineRegex,
+} from 'markdown-to-jsx'
+
+// ||Spoilder||
+export const SpoilderRule: MarkdownToJSX.Rule = {
+ match: simpleInlineRegex(
+ /^\|\|((?:\[.*?\]|<.*?>(?:.*?<.*?>)?|`.*?`|.)*?)\|\|/,
+ ),
+ order: Priority.LOW,
+ parse: parseCaptureInline,
+ react(node, output, state?) {
+ return (
+
+ {output(node.content, state!)}
+
+ )
+ },
+}
diff --git a/packages/kami-design/components/Markdown/renderers/collapse.module.css b/packages/kami-design/components/Markdown/renderers/collapse.module.css
new file mode 100644
index 000000000..62341f4a4
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/collapse.module.css
@@ -0,0 +1,15 @@
+.collapse {
+ @apply my-2;
+
+ :global(.ReactCollapse--collapse) {
+ transition: height 200ms;
+ }
+
+ .title {
+ @apply mb-2 pl-2 flex items-center;
+ }
+
+ p {
+ @apply !m-0;
+ }
+}
diff --git a/packages/kami-design/components/Markdown/renderers/collapse.tsx b/packages/kami-design/components/Markdown/renderers/collapse.tsx
new file mode 100644
index 000000000..290655bd8
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/collapse.tsx
@@ -0,0 +1,44 @@
+import clsx from 'clsx'
+import type { FC, ReactNode } from 'react'
+import React, { useState } from 'react'
+import { Collapse } from 'react-collapse'
+
+import { IcRoundKeyboardDoubleArrowRight } from '../../Icons/arrow'
+import styles from './collapse.module.css'
+
+export const MDetails: FC<{ children: ReactNode[] }> = (props) => {
+ const [open, setOpen] = useState(false)
+
+ const $head = props.children[0]
+
+ return (
+
+
{
+ setOpen((o) => !o)
+ }}
+ >
+
+
+
+ {$head}
+
+
+
+ {props.children.slice(1)}
+
+
+
+ )
+}
diff --git a/packages/kami-design/components/Markdown/renderers/footnotes.tsx b/packages/kami-design/components/Markdown/renderers/footnotes.tsx
new file mode 100644
index 000000000..ffdd00c49
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/footnotes.tsx
@@ -0,0 +1,12 @@
+import type { FC } from 'react'
+
+import { Divider } from '../../Divider'
+
+export const MFootNote: FC = (props) => {
+ return (
+
+ )
+}
diff --git a/packages/kami-design/components/Markdown/renderers/heading.tsx b/packages/kami-design/components/Markdown/renderers/heading.tsx
new file mode 100644
index 000000000..111aa7c3a
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/heading.tsx
@@ -0,0 +1,70 @@
+import type { DOMAttributes, FC } from 'react'
+import React, {
+ Fragment,
+ createElement,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+import { useInView } from 'react-intersection-observer'
+
+import { CustomEventTypes } from '~/types/events'
+import { eventBus } from '~/utils/event-emitter'
+
+interface HeadingProps {
+ id: string
+ className?: string
+ children: React.ReactNode
+ level: number
+}
+export const MHeading: () => FC = () => {
+ let index = 0
+
+ const RenderHeading = (props: HeadingProps) => {
+ const currentIndex = useMemo(() => index++, [])
+ // TODO nested children heading
+
+ const [id, setId] = useState('')
+
+ useEffect(() => {
+ if (!$titleRef.current) {
+ return
+ }
+
+ setId($titleRef.current.textContent || '')
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const { ref } = useInView({
+ rootMargin: '-33% 0% -33% 0%',
+ onChange(inView) {
+ if (inView) {
+ eventBus.emit(CustomEventTypes.TOC, currentIndex)
+ }
+ },
+ })
+
+ const $titleRef = useRef(null)
+
+ return (
+
+ {createElement, HTMLHeadingElement>(
+ `h${props.level}`,
+ {
+ id,
+ ref: $titleRef,
+ 'data-index': currentIndex,
+ } as any,
+
+ {props.children}
+
+ ,
+ )}
+
+ )
+ }
+
+ return RenderHeading
+}
diff --git a/packages/kami-design/components/Markdown/renderers/image.tsx b/packages/kami-design/components/Markdown/renderers/image.tsx
new file mode 100644
index 000000000..6ec2bf4cb
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/image.tsx
@@ -0,0 +1,139 @@
+import { sanitizeUrl } from 'markdown-to-jsx'
+import { reaction } from 'mobx'
+import { observer } from 'mobx-react-lite'
+import type { FC } from 'react'
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react'
+
+import type { ImageLazyRef } from '~/components/universal/Image'
+import { ImageLazy } from '~/components/universal/Image'
+import { useIsClient } from '~/hooks/use-is-client'
+import { useStore } from '~/store'
+import { calculateDimensions } from '~/utils/images'
+
+export interface RenderImageProps {
+ height: number
+ width: number
+ type: string
+ accent?: string
+ src: string
+}
+
+export const ImageSizeMetaContext = createContext(
+ new Map() as Map,
+)
+
+const getContainerSize = () => {
+ const $wrap = document.getElementById('write')
+ if (!$wrap) {
+ return
+ }
+
+ return $wrap.getBoundingClientRect().width
+}
+
+/**
+ * This Component only can render in browser.
+ */
+const _Image: FC<{ src: string; alt?: string }> = observer(({ src, alt }) => {
+ const { appUIStore } = useStore()
+ const imageRef = useRef(null)
+ useEffect(() => {
+ const disposer = reaction(
+ () => appUIStore.viewport.w | appUIStore.viewport.h,
+ () => {
+ if (imageRef.current?.status === 'loaded') {
+ disposer()
+
+ return
+ }
+ setMaxWidth(getContainerSize())
+ },
+ )
+
+ return () => {
+ disposer()
+ }
+ }, [])
+ const images = useContext(ImageSizeMetaContext)
+
+ const isPrintMode = appUIStore.mediaType === 'print'
+
+ const [maxWidth, setMaxWidth] = useState(getContainerSize())
+
+ // 因为有动画开始不能获取到大小 , 直到获取到 container 的大小
+ useEffect(() => {
+ let raf = requestAnimationFrame(function a() {
+ const size = getContainerSize()
+ if (!size) {
+ requestAnimationFrame(a)
+ } else {
+ setMaxWidth(size)
+ }
+ }) as any
+ return () => {
+ raf = cancelAnimationFrame(raf)
+ }
+ }, [])
+
+ if (isPrintMode) {
+ return
+ }
+
+ const { accent, height, width } = images.get(src) || {
+ height: undefined,
+ width: undefined,
+ }
+
+ const max = {
+ width: maxWidth ?? 644,
+ height: Infinity,
+ }
+
+ let cal = {} as any
+ if (width && height) {
+ cal = calculateDimensions(width, height, max)
+ }
+
+ return (
+
+ )
+})
+const style = { padding: '1rem 0' }
+export const MImage: FC<
+ React.DetailedHTMLProps<
+ React.ImgHTMLAttributes,
+ HTMLImageElement
+ >
+> = (props) => {
+ const { src, alt, title } = props
+ const sanitizedUrl = sanitizeUrl(src!)
+ const isClient = useIsClient()
+ const imageCaption =
+ title ||
+ (['!', '¡'].some((ch) => ch == alt?.[0]) ? alt?.slice(1) : '') ||
+ ''
+
+ return !isClient ? (
+
+ ) : (
+ <_Image src={sanitizedUrl!} alt={imageCaption} />
+ )
+}
diff --git a/packages/kami-design/components/Markdown/renderers/index.module.css b/packages/kami-design/components/Markdown/renderers/index.module.css
new file mode 100644
index 000000000..a423420a4
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/index.module.css
@@ -0,0 +1,28 @@
+.link {
+ display: inline-block;
+ position: relative;
+
+ a {
+ cursor: alias;
+ overflow: hidden;
+ position: relative;
+ color: var(--primary);
+ }
+
+ a::after {
+ content: '';
+ position: absolute;
+ bottom: -1.9px;
+ height: 1px;
+ background-color: currentColor;
+ width: 0;
+ transform: translateX(-50%);
+ left: 50%;
+ text-align: center;
+ transition: width 0.5s ease-in-out;
+ }
+
+ a:hover::after {
+ width: 100%;
+ }
+}
diff --git a/packages/kami-design/components/Markdown/renderers/index.ts b/packages/kami-design/components/Markdown/renderers/index.ts
new file mode 100644
index 000000000..ae747f8f3
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/index.ts
@@ -0,0 +1,4 @@
+export * from './heading'
+export * from './image'
+export * from './table'
+export * from './paragraph'
diff --git a/packages/kami-design/components/Markdown/renderers/paragraph.tsx b/packages/kami-design/components/Markdown/renderers/paragraph.tsx
new file mode 100644
index 000000000..8bc69bf04
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/paragraph.tsx
@@ -0,0 +1,14 @@
+import clsx from 'clsx'
+import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'
+
+export const MParagraph: FC<
+ DetailedHTMLProps, HTMLParagraphElement>
+> = (props) => {
+ const { children, ...other } = props
+ const { className, ...rest } = other
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/kami-design/components/Markdown/renderers/table.tsx b/packages/kami-design/components/Markdown/renderers/table.tsx
new file mode 100644
index 000000000..9c3b2947d
--- /dev/null
+++ b/packages/kami-design/components/Markdown/renderers/table.tsx
@@ -0,0 +1,16 @@
+import type { FC } from 'react'
+
+export const MTableHead: FC = (props) => {
+ const { children, ...rest } = props
+ return {children}
+}
+
+export const MTableRow: FC = (props) => {
+ const { children, ...rest } = props
+ return {children}
+}
+
+export const MTableBody: FC = (props) => {
+ const { children, ...rest } = props
+ return {children}
+}
diff --git a/packages/kami-design/components/Markdown/test-text.md b/packages/kami-design/components/Markdown/test-text.md
new file mode 100644
index 000000000..92356c8c6
--- /dev/null
+++ b/packages/kami-design/components/Markdown/test-text.md
@@ -0,0 +1,354 @@
+> 影响大众想象力的,并不是事实本身,而是它扩散和传播的方式。
+
+↑ 引用
+
+# Test 文本
+
+> 影响大众想象力的,并不是事实本身,而是它扩散和传播的方式。
+
+# 一级
+
+我与父亲不相见已二年余了,我最不能忘记的是他的背影。
+
+那年冬天,祖母死了,父亲的差使也交卸了,正是祸不单行的日子。我从北京到徐州,打算跟着父亲奔丧回家。到徐州见着父亲,看见满院狼藉的东西,又想起祖母,不禁簌簌地流下眼泪。父亲说:“事已如此,不必难过,好在天无绝人之路!”
+
+## 二级
+
+回家变卖典质,父亲还了亏空;又借钱办了丧事。这些日子,家中光景很是惨澹,一半为了丧事,一半为了父亲赋闲。丧事完毕,父亲要到南京谋事,我也要回北京念书,我们便同行。
+
+到南京时,有朋友约去游逛,勾留了一日;第二日上午便须渡江到浦口,下午上车北去。父亲因为事忙,本已说定不送我,叫旅馆里一个熟识的茶房陪我同去。他再三嘱咐茶房,甚是仔细。但他终于不放心,怕茶房不妥帖;颇踌躇了一会。其实我那年已二十岁,北京已来往过两三次,是没有什么要紧的了。他踌躇了一会,终于决定还是自己送我去。我再三劝他不必去;他只说:“不要紧,他们去不好!”
+
+### 三级
+
+> 影响大众想象力的,并不是事实本身,而是它扩散和传播的方式。
+
+#### 四级
+
+\`code: \`
+
+```tsx
+
+```
+
+|| 你知道的太多了 || spoiler || 你知道的太多了 ||
+
+[链接](https://baidu.com)
+
+![!图片描述](https://gitee.com/xun7788/my-imagination/raw/master/uPic/1615516941397.jpg)
+
+↑ 图片描述
+
+**加粗: 歌词**
+
+~~dele~~ 删除
+
+```mermaid
+flowchart TD
+ 1([手动打 tag 发布一个 release]) -->
+ 2([CI 监视 release 的发布 开始构建和发布]) -->
+ 3([云构建打包产物 zip 发布到 GitHub Release]) -- SSH 连接到服务器-->
+ 4([执行部署脚本]) -->
+ 5([下载构建产物解压]) -->
+ 6([直接运行或使用 PM2 托管])
+```
+
+GH Mention: (GH@Innei)
+
+> _夕暮れ_
+>
+> 作詞:甲本ヒロト
+> 作曲:甲本ヒロト
+>
+> はっきりさせなくてもいい
+> あやふやなまんまでいい
+> 僕達はなんなとなく幸せになるんだ
+>
+> 何年たってもいい 遠く離れてもいい
+> 独りぼっちじゃないぜウィンクするぜ
+>
+> 夕暮れが僕のドアをノックする頃に
+> あなたを「ギュッ」と抱きたくなってる
+> 幻なんかじゃない 人生は夢じゃない
+> 僕達ははっきりと生きてるんだ
+> 夕焼け空は赤い 炎のように赤い
+> この星の半分を真っ赤に染めた
+> それよりももっと赤い血が
+> 体中を流れてるんだぜ
+> 夕暮れが僕のドアをノックする頃に
+>
+> あなたを「ギュッ」と抱きたくなってる
+> 幻なんかじゃない 人生は夢じゃない
+> 僕達ははっきりと生きてるんだ
+>
+> 夕焼け空は赤い 炎のように赤い
+> この星の半分を真っ赤に染めた
+>
+> それよりももっと赤い血が
+> 体中を流れてるんだぜ
+> 体中を流れてるんだぜ
+> 体中を流れてるんだぜ
+
+
+
+---
+__Advertisement :)__
+
+- __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image
+ resize in browser.
+- __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly
+ i18n with plurals support and easy syntax.
+
+You will like those projects!
+
+---
+
+# h1 Heading 8-)
+## h2 Heading
+### h3 Heading
+#### h4 Heading
+##### h5 Heading
+###### h6 Heading
+
+
+## Horizontal Rules
+
+___
+
+---
+
+***
+
+
+## Typographic replacements
+
+Enable typographer option to see result.
+
+(c) (C) (r) (R) (tm) (TM) (p) (P) +-
+
+test.. test... test..... test?..... test!....
+
+!!!!!! ???? ,, -- ---
+
+"Smartypants, double quotes" and 'single quotes'
+
+
+## Emphasis
+
+**This is bold text**
+
+__This is bold text__
+
+*This is italic text*
+
+_This is italic text_
+
+~~Strikethrough~~
+
+
+## Blockquotes
+
+
+> Blockquotes can also be nested...
+>> ...by using additional greater-than signs right next to each other...
+> > > ...or with spaces between arrows.
+
+
+## Lists
+
+Unordered
+
++ Create a list by starting a line with `+`, `-`, or `*`
++ Sub-lists are made by indenting 2 spaces:
+ - Marker character change forces new list start:
+ * Ac tristique libero volutpat at
+ + Facilisis in pretium nisl aliquet
+ - Nulla volutpat aliquam velit
++ Very easy!
+
+Ordered
+
+1. Lorem ipsum dolor sit amet
+2. Consectetur adipiscing elit
+3. Integer molestie lorem at massa
+
+
+1. You can use sequential numbers...
+1. ...or keep all the numbers as `1.`
+
+Start numbering with offset:
+
+57. foo
+1. bar
+
+
+## Code
+
+Inline `code`
+
+Indented code
+
+ // Some comments
+ line 1 of code
+ line 2 of code
+ line 3 of code
+
+
+Block code "fences"
+
+```
+Sample text here...
+```
+
+Syntax highlighting
+
+``` js
+var foo = function (bar) {
+ return bar++;
+};
+
+console.log(foo(5));
+```
+
+## Tables
+
+| Option | Description |
+| ------ | ----------- |
+| data | path to data files to supply the data that will be passed into templates. |
+| engine | engine to be used for processing templates. Handlebars is the default. |
+| ext | extension to be used for dest files. |
+
+Right aligned columns
+
+| Option | Description |
+| ------:| -----------:|
+| data | path to data files to supply the data that will be passed into templates. |
+| engine | engine to be used for processing templates. Handlebars is the default. |
+| ext | extension to be used for dest files. |
+
+
+## Links
+
+[link text](http://dev.nodeca.com)
+
+[link with title](http://nodeca.github.io/pica/demo/ "title text!")
+
+Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
+
+
+## Images
+
+![Minion](https://octodex.github.com/images/minion.png)
+![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
+
+Like links, Images also have a footnote style syntax
+
+![Alt text][id]
+
+With a reference later in the document defining the URL location:
+
+[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
+
+
+## Plugins
+
+The killer feature of `markdown-it` is very effective support of
+[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin).
+
+
+### [Emojies](https://github.com/markdown-it/markdown-it-emoji)
+
+> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum:
+>
+> Shortcuts (emoticons): :-) :-( 8-) ;)
+
+see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji.
+
+
+### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)
+
+- 19^th^
+- H~2~O
+
+
+### [\](https://github.com/markdown-it/markdown-it-ins)
+
+++Inserted text++
+
+
+### [\](https://github.com/markdown-it/markdown-it-mark)
+
+==Marked text==
+
+
+### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)
+
+Footnote 1 link[^first].
+
+Footnote 2 link[^second].
+
+Inline footnote^[Text of inline footnote] definition.
+
+Duplicated footnote reference[^second].
+
+[^first]: Footnote **can have markup**
+
+ and multiple paragraphs.
+
+[^second]: Footnote text.
+
+
+### [Definition lists](https://github.com/markdown-it/markdown-it-deflist)
+
+Term 1
+
+: Definition 1
+with lazy continuation.
+
+Term 2 with *inline markup*
+
+: Definition 2
+
+ { some code, part of Definition 2 }
+
+ Third paragraph of definition 2.
+
+_Compact style:_
+
+Term 1
+ ~ Definition 1
+
+Term 2
+ ~ Definition 2a
+ ~ Definition 2b
+
+
+### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr)
+
+This is HTML abbreviation example.
+
+It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on.
+
+*[HTML]: Hyper Text Markup Language
+
+### [Custom containers](https://github.com/markdown-it/markdown-it-container)
+
+::: warning
+*here be dragons*
+:::
diff --git a/packages/kami-design/components/Markdown/utils/image.ts b/packages/kami-design/components/Markdown/utils/image.ts
new file mode 100644
index 000000000..6c85c032a
--- /dev/null
+++ b/packages/kami-design/components/Markdown/utils/image.ts
@@ -0,0 +1,25 @@
+export interface MImageType {
+ name?: string
+ url: string
+ footnote?: string
+}
+export const pickImagesFromMarkdown = (md: string) => {
+ const regexp =
+ /^!\[((?:\[[^\]]*\]|[^[\]]|\](?=[^[]*\]))*)\]\(\s*((?:[^\s\\]|\\.)*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*\)/
+
+ const lines = md.split('\n')
+
+ const res: MImageType[] = []
+
+ for (const line of lines) {
+ const match = regexp.exec(line)
+ if (!match) {
+ continue
+ }
+
+ const [, name, url, footnote] = match
+ res.push({ name, url, footnote })
+ }
+
+ return res
+}
diff --git a/packages/kami-design/components/Mermaid/index.tsx b/packages/kami-design/components/Mermaid/index.tsx
new file mode 100644
index 000000000..6bbd9ed10
--- /dev/null
+++ b/packages/kami-design/components/Mermaid/index.tsx
@@ -0,0 +1,21 @@
+import type { FC } from 'react'
+import { useInsertionEffect } from 'react'
+
+import { loadScript } from '~/utils/load-script'
+
+export const Mermaid: FC<{ content: string }> = (props) => {
+ useInsertionEffect(() => {
+ loadScript(
+ 'https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/mermaid/8.9.0/mermaid.min.js',
+ ).then(() => {
+ if (window.mermaid) {
+ window.mermaid.initialize({
+ theme: 'default',
+ startOnLoad: false,
+ })
+ window.mermaid.init(undefined, '.mermaid')
+ }
+ })
+ }, [])
+ return {props.content}
+}
diff --git a/packages/kami-design/components/Portal/index.tsx b/packages/kami-design/components/Portal/index.tsx
new file mode 100644
index 000000000..c8fb0d50d
--- /dev/null
+++ b/packages/kami-design/components/Portal/index.tsx
@@ -0,0 +1,14 @@
+import type { FC } from 'react'
+import { memo } from 'react'
+import { createPortal } from 'react-dom'
+
+import { useIsClient } from '~/hooks/use-is-client'
+
+export const RootPortal: FC = memo((props) => {
+ const isClient = useIsClient()
+ if (!isClient) {
+ return null
+ }
+
+ return createPortal(props.children, document.body)
+})
diff --git a/packages/kami-design/env.d.ts b/packages/kami-design/env.d.ts
new file mode 100644
index 000000000..4f11a03dc
--- /dev/null
+++ b/packages/kami-design/env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/packages/kami-design/index.ts b/packages/kami-design/index.ts
new file mode 100644
index 000000000..c9359e93d
--- /dev/null
+++ b/packages/kami-design/index.ts
@@ -0,0 +1,3 @@
+import 'virtual:windi.css'
+
+export { Markdown } from './components/Markdown'
diff --git a/packages/kami-design/package.json b/packages/kami-design/package.json
new file mode 100644
index 000000000..250a8c726
--- /dev/null
+++ b/packages/kami-design/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "@mx-space/kami-design",
+ "version": "0.0.0",
+ "main": "dist/index.cjs",
+ "module": "dist/index.js",
+ "types": "types/index.d.ts",
+ "type": "module",
+ "exports": {
+ ".": {
+ "type": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "./src": {
+ "import": "./src"
+ },
+ "./src/*": {
+ "import": "./src/*"
+ }
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "dependencies": {
+ "markdown-to-jsx": "npm:@innei/markdown-to-jsx@7.1.3-15",
+ "@floating-ui/react-dom": "1.0.1",
+ "@formkit/auto-animate": "1.0.0-beta.5",
+ "@mx-space/api-client": "1.0.0-beta.3",
+ "axios": "0.27.2",
+ "clsx": "1.2.1",
+ "css-spring": "4.1.0",
+ "dayjs": "1.11.6",
+ "devtools-detector": "2.0.14",
+ "js-cookie": "3.0.1",
+ "lodash-es": "4.17.21",
+ "markdown-escape": "2.0.0",
+ "medium-zoom": "1.0.8",
+ "mobx": "6.7.0",
+ "mobx-react-lite": "3.4.0",
+ "next": "13.0.6",
+ "next-seo": "5.14.1",
+ "next-suspense": "0.1.3",
+ "randomcolor": "0.6.2",
+ "react": "18.2.0",
+ "react-collapse": "5.1.1",
+ "react-countup": "6.4.0",
+ "react-dom": "18.2.0",
+ "react-intersection-observer": "9.4.1",
+ "react-masonry-css": "1.0.16",
+ "react-message-popup": "1.0.0",
+ "react-shortcut-guide": "1.0.0-alpha.0",
+ "react-smooth-number-counter": "https://github.com/Innei/react-smooth-number-counter.git",
+ "react-toastify": "9.1.1",
+ "react-transition-group": "4.4.5",
+ "react-use": "17.4.0",
+ "remove-markdown": "0.5.0",
+ "socket.io-client": "4.5.4",
+ "validator": "13.7.0"
+ },
+ "devDependencies": {
+ "@babel/plugin-transform-react-jsx": "7.19.0",
+ "@babel/plugin-transform-typescript": "7.20.2",
+ "@babel/preset-react": "7.18.6",
+ "@rollup/plugin-babel": "6.0.3",
+ "@rollup/plugin-commonjs": "22.0.2",
+ "@rollup/plugin-node-resolve": "14.0.1",
+ "@rollup/plugin-typescript": "8.5.0",
+ "@types/react": "17.0.52",
+ "esbuild": "0.15.17",
+ "rollup": "2.79.0",
+ "rollup-plugin-esbuild": "5.0.0",
+ "rollup-plugin-peer-deps-external": "2.2.4",
+ "rollup-plugin-postcss": "4.0.2",
+ "rollup-plugin-terser": "7.0.2",
+ "rollup-plugin-windicss": "1.8.8",
+ "typescript": "4.9.3"
+ }
+}
diff --git a/packages/kami-design/react-next.d.ts b/packages/kami-design/react-next.d.ts
new file mode 100644
index 000000000..634d3ad5c
--- /dev/null
+++ b/packages/kami-design/react-next.d.ts
@@ -0,0 +1,85 @@
+// copy from @types/react@18
+// why not use @types/react@18 directly, buz it is fucking shit break all change.
+import 'react'
+
+declare module 'react' {
+ // must be synchronous
+ export type TransitionFunction = () => VoidOrUndefinedOnly
+ // strange definition to allow vscode to show documentation on the invocation
+ export interface TransitionStartFunction {
+ /**
+ * State updates caused inside the callback are allowed to be deferred.
+ *
+ * **If some state update causes a component to suspend, that state update should be wrapped in a transition.**
+ *
+ * @param callback A _synchronous_ function which causes state updates that can be deferred.
+ */
+ (callback: TransitionFunction): void
+ }
+
+ /**
+ * Returns a deferred version of the value that may “lag behind” it for at most `timeoutMs`.
+ *
+ * This is commonly used to keep the interface responsive when you have something that renders immediately
+ * based on user input and something that needs to wait for a data fetch.
+ *
+ * A good example of this is a text input.
+ *
+ * @param value The value that is going to be deferred
+ *
+ * @see https://reactjs.org/docs/concurrent-mode-reference.html#usedeferredvalue
+ */
+ export function useDeferredValue(value: T): T
+
+ /**
+ * Allows components to avoid undesirable loading states by waiting for content to load
+ * before transitioning to the next screen. It also allows components to defer slower,
+ * data fetching updates until subsequent renders so that more crucial updates can be
+ * rendered immediately.
+ *
+ * The `useTransition` hook returns two values in an array.
+ *
+ * The first is a boolean, React’s way of informing us whether we’re waiting for the transition to finish.
+ * The second is a function that takes a callback. We can use it to tell React which state we want to defer.
+ *
+ * **If some state update causes a component to suspend, that state update should be wrapped in a transition.**
+ *
+ * @param config An optional object with `timeoutMs`
+ *
+ * @see https://reactjs.org/docs/concurrent-mode-reference.html#usetransition
+ */
+ export function useTransition(): [boolean, TransitionStartFunction]
+
+ /**
+ * Similar to `useTransition` but allows uses where hooks are not available.
+ *
+ * @param callback A _synchronous_ function which causes state updates that can be deferred.
+ */
+ export function startTransition(scope: TransitionFunction): void
+
+ export function useId(): string
+
+ /**
+ * @param effect Imperative function that can return a cleanup function
+ * @param deps If present, effect will only activate if the values in the list change.
+ *
+ * @see https://github.com/facebook/react/pull/21913
+ */
+ export function useInsertionEffect(
+ effect: EffectCallback,
+ deps?: DependencyList,
+ ): void
+
+ /**
+ * @param subscribe
+ * @param getSnapshot
+ *
+ * @see https://github.com/reactwg/react-18/discussions/86
+ */
+ // keep in sync with `useSyncExternalStore` from `use-sync-external-store`
+ export function useSyncExternalStore(
+ subscribe: (onStoreChange: () => void) => () => void,
+ getSnapshot: () => Snapshot,
+ getServerSnapshot?: () => Snapshot,
+ ): Snapshot
+}
diff --git a/packages/kami-design/rollup.config.js b/packages/kami-design/rollup.config.js
new file mode 100644
index 000000000..727e55a4b
--- /dev/null
+++ b/packages/kami-design/rollup.config.js
@@ -0,0 +1,112 @@
+// @ts-check
+
+import esbuild from 'rollup-plugin-esbuild'
+import peerDepsExternal from 'rollup-plugin-peer-deps-external'
+import css from 'rollup-plugin-postcss'
+import { terser } from 'rollup-plugin-terser'
+import WindiCSS from 'rollup-plugin-windicss'
+
+import commonjs from '@rollup/plugin-commonjs'
+import { nodeResolve } from '@rollup/plugin-node-resolve'
+import typescript from '@rollup/plugin-typescript'
+
+const packageJson = require('./package.json')
+
+const globals = {
+ // @ts-ignore
+ ...(packageJson?.dependencies || {}),
+}
+
+const dir = 'dist'
+
+/**
+ * @type {import('rollup').RollupOptions[]}
+ */
+const config = [
+ {
+ input: './index.ts',
+ // ignore lib
+ external: [
+ 'react',
+ 'react-dom',
+ 'lodash',
+ 'lodash-es',
+ ...Object.keys(globals),
+ ],
+
+ output: [
+ {
+ file: `${dir}/index.cjs`,
+ format: 'cjs',
+ sourcemap: true,
+ },
+ {
+ file: `${dir}/index.min.cjs`,
+ format: 'cjs',
+ sourcemap: true,
+ plugins: [terser()],
+ },
+ {
+ file: `${dir}/index.js`,
+ format: 'esm',
+ sourcemap: true,
+ },
+ {
+ file: `${dir}/index.min.js`,
+ format: 'esm',
+ sourcemap: true,
+ plugins: [terser()],
+ },
+ ],
+ plugins: [
+ ...WindiCSS(),
+ nodeResolve(),
+ commonjs({ include: 'node_modules/**' }),
+ typescript({
+ tsconfig: './tsconfig.json',
+ declaration: false,
+ }),
+ css({
+ // extract: true,
+ }),
+
+ // @ts-ignore
+ peerDepsExternal(),
+ // babel({
+ // babelHelpers: 'bundled',
+ // extensions: ['.ts', '.tsx', '.jsx', '.js'],
+ // presets: ['@babel/preset-react'],
+ // plugins: [
+ // '@babel/plugin-transform-typescript',
+ // '@babel/plugin-transform-react-jsx',
+ // ],
+ // }),
+ esbuild({
+ // All options are optional
+ include: /\.[jt]sx?$/, // default, inferred from `loaders` option
+ exclude: /node_modules/, // default
+ sourceMap: true, // default
+ minify: process.env.NODE_ENV === 'production',
+ target: 'es2017', // default, or 'es20XX', 'esnext'
+ jsx: 'transform', // default, or 'preserve'
+ jsxFactory: 'React.createElement',
+ jsxFragment: 'React.Fragment',
+ // Like @rollup/plugin-replace
+
+ tsconfig: 'tsconfig.json', // default
+ // Add extra loaders
+ loaders: {
+ // Add .json files support
+ // require @rollup/plugin-commonjs
+ '.json': 'json',
+ // Enable JSX in .js files too
+ '.js': 'jsx',
+ },
+ }),
+ ],
+
+ treeshake: true,
+ },
+]
+
+export default config
diff --git a/packages/kami-design/tsconfig.json b/packages/kami-design/tsconfig.json
new file mode 100644
index 000000000..34d53c93f
--- /dev/null
+++ b/packages/kami-design/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "strict": true,
+ "target": "ES2020",
+ "module": "ES2020",
+ "jsx": "preserve",
+ "allowJs": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "removeComments": false,
+ "preserveConstEnums": true,
+ "sourceMap": true,
+ "skipLibCheck": true,
+ "noImplicitAny": false,
+ "useDefineForClassFields": true,
+ "lib": [
+ "dom"
+ ],
+ "outDir": "dist",
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "incremental": true,
+ "baseUrl": ".",
+ "paths": {
+ "~": [
+ "../../src"
+ ],
+ "~/*": [
+ "../../src/*"
+ ]
+ }
+ },
+ "exclude": [
+ ".next",
+ "server/**/*.*"
+ ],
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ]
+}
\ No newline at end of file
diff --git a/packages/kami-design/types.d.ts b/packages/kami-design/types.d.ts
new file mode 100644
index 000000000..03b78a953
--- /dev/null
+++ b/packages/kami-design/types.d.ts
@@ -0,0 +1,7 @@
+declare global {
+ export interface Window {
+ [key: string]: any
+ }
+}
+
+export {}