@krizaka/ui 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avatar.d.ts +51 -2
- package/dist/avatar.js +90 -4
- package/dist/avatar.js.map +1 -1
- package/dist/blocks.css +9 -2
- package/dist/chat.js +1 -1
- package/dist/chunk-R6ZS4FMV.js +83 -0
- package/dist/chunk-R6ZS4FMV.js.map +1 -0
- package/dist/link-card.d.ts +7 -1
- package/dist/link-card.js +8 -5
- package/dist/link-card.js.map +1 -1
- package/dist/match-dialog.js +1 -1
- package/dist/native.d.ts +33 -2
- package/dist/native.js +28 -4
- package/dist/native.js.map +1 -1
- package/dist/showcase.js +1 -1
- package/dist/showcase.js.map +1 -1
- package/dist/site-footer.js +5 -5
- package/dist/site-footer.js.map +1 -1
- package/dist/vertical-video.js +1 -1
- package/package.json +3 -3
- package/registry/avatar.json +122 -3
- package/registry/card-grid.json +10 -1
- package/registry/examples/avatar/default.d.ts +4 -0
- package/registry/examples/avatar/default.js +12 -0
- package/registry/examples/avatar/default.tsx +13 -0
- package/registry/examples/card-grid/aligned.d.ts +4 -0
- package/registry/examples/card-grid/aligned.js +18 -0
- package/registry/examples/card-grid/aligned.tsx +16 -0
- package/registry/index.json +6 -6
- package/registry/link-card.json +17 -2
- package/registry/map.json +1 -1
- package/registry/nav-bar.json +1 -1
- package/registry/showcase.json +1 -1
- package/registry/site-footer.json +2 -2
- package/dist/chunk-CURW36FI.js +0 -47
- package/dist/chunk-CURW36FI.js.map +0 -1
package/dist/showcase.js
CHANGED
|
@@ -127,7 +127,7 @@ function Showcase({
|
|
|
127
127
|
...props,
|
|
128
128
|
children: [
|
|
129
129
|
/* @__PURE__ */ jsx("style", { href: "kz-showcase", precedence: "medium", children: FILL_CSS }),
|
|
130
|
-
/* @__PURE__ */ jsx("div", { "aria-live": announce ? "polite" : "off", className: "grid", children: items.map((slide, i) => {
|
|
130
|
+
/* @__PURE__ */ jsx("div", { "aria-live": announce ? "polite" : "off", className: "grid grid-cols-[minmax(0,1fr)]", children: items.map((slide, i) => {
|
|
131
131
|
const active = i === current;
|
|
132
132
|
const gap = Math.abs(i - current);
|
|
133
133
|
const near = gap <= 1 || loop && total - gap <= 1;
|
package/dist/showcase.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/showcase/showcase.tsx"],"sourcesContent":["// Client (state, timers' observers, keys). A slideshow that advances by itself — and shows it: a segmented progress bar\n// (one segment per slide, the current one fills over the interval, each one a button) with a counter, previous / next and a\n// visible pause button (WCAG 2.2.2). The time IS the CSS animation of the current segment (`.kz-showcase-fill`):\n// when it ends, the next slide comes — so what you see filling is exactly what decides, and pausing the animation pauses\n// the show. Its two rules travel with the component (a hoisted, deduplicated <style>): no stylesheet to import. It waits while the pointer is over it, while a keyboard focus is inside, while the tab is hidden and while it\n// is off screen; under reduced motion it never advances by itself. Slides are slots: any content.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\n\nconst FILL_CSS = \".kz-showcase-fill{transform-origin:left;animation:kz-showcase-fill 6s linear forwards}@keyframes kz-showcase-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\";\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst subscribeNothing = () => () => undefined;\nconst readReduced = () => Boolean(typeof window !== \"undefined\" && window.matchMedia?.(REDUCED).matches);\nfunction subscribeReduced(notify: () => void) {\n const query = window.matchMedia?.(REDUCED);\n query?.addEventListener?.(\"change\", notify);\n return () => query?.removeEventListener?.(\"change\", notify);\n}\nconst readHidden = () => document.visibilityState === \"hidden\";\nfunction subscribeVisibility(notify: () => void) {\n document.addEventListener(\"visibilitychange\", notify);\n return () => document.removeEventListener(\"visibilitychange\", notify);\n}\n\nexport interface ShowcaseLabels {\n /** The previous button's name. */\n previous: string;\n /** The next button's name. */\n next: string;\n /** The pause button's name while it plays. */\n pause: string;\n /** The same button's name while it is paused. */\n play: string;\n /** A progress segment's name: `(3, 17) => \"Go to image 3\"`. */\n goTo: (n: number, total: number) => string;\n /** The position in words, beside the title: `(1, 17) => \"Image 1 of 17\"`. */\n counter: (n: number, total: number) => string;\n /** The slide's accessible name: `(1, 17) => \"1 of 17\"`. */\n slide: (n: number, total: number) => string;\n}\n\n/** English defaults — pass your own translated `labels`. */\nexport const showcaseLabels: ShowcaseLabels = {\n previous: \"Previous slide\",\n next: \"Next slide\",\n pause: \"Pause the slideshow\",\n play: \"Play the slideshow\",\n goTo: (n) => `Go to slide ${n}`,\n counter: (n, total) => `Slide ${n} of ${total}`,\n slide: (n, total) => `${n} of ${total}`,\n};\n\nexport interface ShowcaseSlideState {\n index: number;\n /** The slide in view. */\n active: boolean;\n}\n\nexport interface ShowcaseProps<T> extends Omit<React.ComponentProps<\"section\">, \"children\" | \"onChange\"> {\n /** The slideshow's accessible name (“Prompt to image”), passed translated. */\n label: string;\n /** The slides, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** One slide's content. Only the slide in view and its neighbours are mounted (the rest stay empty): give slides the same size. */\n renderItem: (item: T, state: ShowcaseSlideState) => React.ReactNode;\n /** A slide's title, shown before the counter (“Golden-hour editorial”). */\n getTitle?: (item: T) => string;\n /** Milliseconds a slide stays (the time the segment takes to fill). @default 6000 */\n interval?: number;\n /** Advances by itself. Never under `prefers-reduced-motion`. @default true */\n autoplay?: boolean;\n /** `fade` crossfades; `slide` also moves the slides a few pixels. @default \"fade\" */\n transition?: \"fade\" | \"slide\";\n /** After the last slide, go back to the first. @default true */\n loop?: boolean;\n /** The slide in view (controlled). */\n index?: number;\n /** The slide shown first. @default 0 */\n defaultIndex?: number;\n /** Called when the slide in view changes — `user` is true when the visitor asked for it. */\n onIndexChange?: (index: number, user: boolean) => void;\n /** The words of the controls, translated. */\n labels?: ShowcaseLabels;\n}\n\n/** An auto-advancing slideshow with a visible, clickable progress bar, a counter, prev / next and a pause button. */\nexport function Showcase<T>({\n label,\n items,\n getId,\n renderItem,\n getTitle,\n interval = 6000,\n autoplay = true,\n transition = \"fade\",\n loop = true,\n index: controlled,\n defaultIndex = 0,\n onIndexChange,\n labels = showcaseLabels,\n className,\n onKeyDown,\n onFocus,\n onBlur,\n onPointerEnter,\n onPointerLeave,\n ...props\n}: ShowcaseProps<T>) {\n const total = items.length;\n const root = React.useRef<HTMLElement>(null);\n const [inner, setInner] = React.useState(defaultIndex);\n const current = Math.min(Math.max(controlled ?? inner, 0), Math.max(total - 1, 0));\n const mounted = React.useSyncExternalStore(subscribeNothing, () => true, () => false);\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n const hidden = React.useSyncExternalStore(subscribeVisibility, readHidden, () => false);\n const [paused, setPaused] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const [inView, setInView] = React.useState(true);\n const [announce, setAnnounce] = React.useState(false);\n\n React.useEffect(() => {\n const host = root.current;\n if (!host || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver((entries) => setInView(entries.some((e) => e.isIntersecting)), { threshold: 0.2 });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n const move = (to: number, user: boolean) => {\n if (total === 0) return;\n const next = loop ? (to + total) % total : Math.min(Math.max(to, 0), total - 1);\n if (next === current) return;\n setAnnounce(user);\n if (controlled === undefined) setInner(next);\n onIndexChange?.(next, user);\n };\n\n const playing = autoplay && mounted && !reduced && total > 1;\n const running = playing && !paused && !hover && !focus && !hidden && inView;\n const lastStop = !loop && current === total - 1;\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n onKeyDown?.(event);\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const target = event.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const to = event.key === \"ArrowRight\" ? current + 1 : event.key === \"ArrowLeft\" ? current - 1 : event.key === \"Home\" ? 0 : event.key === \"End\" ? total - 1 : null;\n if (to === null) return;\n event.preventDefault();\n move(to, true);\n };\n\n const item = items[current];\n const title = item !== undefined ? getTitle?.(item) : undefined;\n const button = \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-default bg-surface-1 text-fg-secondary transition-colors hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40\";\n\n return (\n <section\n ref={root}\n aria-roledescription=\"carousel\"\n aria-label={label}\n data-running={running ? \"\" : undefined}\n className={cn(\"flex flex-col gap-3\", className)}\n onKeyDown={handleKeyDown}\n onPointerEnter={(e) => {\n onPointerEnter?.(e);\n if (e.pointerType === \"mouse\") setHover(true);\n }}\n onPointerLeave={(e) => {\n onPointerLeave?.(e);\n setHover(false);\n }}\n onFocus={(e) => {\n onFocus?.(e);\n // A mouse click on a button is not a reason to stop: only a keyboard focus is.\n let visible = true;\n try {\n visible = e.target.matches(\":focus-visible\");\n } catch {\n /* a stand-in without :focus-visible */\n }\n if (visible) setFocus(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n if (!e.currentTarget.contains(e.relatedTarget)) setFocus(false);\n }}\n {...props}\n >\n <style href=\"kz-showcase\" precedence=\"medium\">\n {FILL_CSS}\n </style>\n <div aria-live={announce ? \"polite\" : \"off\"} className=\"grid\">\n {items.map((slide, i) => {\n const active = i === current;\n const gap = Math.abs(i - current);\n const near = gap <= 1 || (loop && total - gap <= 1);\n const side = i < current ? \"-translate-x-4\" : \"translate-x-4\";\n return (\n <div\n key={getId(slide)}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={labels.slide(i + 1, total)}\n aria-hidden={active ? undefined : true}\n inert={active ? undefined : true}\n data-active={active ? \"\" : undefined}\n className={cn(\n \"col-start-1 row-start-1 min-w-0 transition-[opacity,translate,visibility] duration-500 motion-reduce:transition-none\",\n active ? \"visible opacity-100\" : \"invisible opacity-0\",\n transition === \"slide\" && (active ? \"translate-x-0\" : side),\n )}\n >\n {near ? renderItem(slide, { index: i, active }) : null}\n </div>\n );\n })}\n </div>\n\n <div className=\"flex gap-1\">\n {items.map((slide, i) => (\n <button\n key={getId(slide)}\n type=\"button\"\n aria-label={labels.goTo(i + 1, total)}\n aria-current={i === current ? \"true\" : undefined}\n tabIndex={i === current ? 0 : -1}\n onClick={() => move(i, true)}\n className=\"group/seg relative flex h-6 min-w-0 flex-1 cursor-pointer items-center rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <span className=\"relative block h-1 w-full overflow-hidden rounded-full bg-surface-3 transition-[height] group-hover/seg:h-1.5\">\n {i < current || (i === current && (!playing || lastStop)) ? <span className=\"absolute inset-0 bg-accent\" /> : null}\n {i === current && playing && !lastStop ? (\n <span\n key={`${current}`}\n data-segment-fill\n className=\"kz-showcase-fill absolute inset-0 bg-accent\"\n style={{ animationDuration: `${interval}ms`, animationPlayState: running ? \"running\" : \"paused\" }}\n onAnimationEnd={() => running && move(current + 1, false)}\n />\n ) : null}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"flex items-center gap-3\">\n <p className=\"m-0 min-w-0 flex-1 truncate text-sm text-fg-secondary\">\n {title ? <span className=\"font-medium text-fg\">{title}</span> : null}\n {title ? <span aria-hidden> · </span> : null}\n <span>{labels.counter(current + 1, total)}</span>\n </p>\n <div className=\"flex items-center gap-1.5\">\n <button type=\"button\" aria-label={labels.previous} disabled={!loop && current === 0} onClick={() => move(current - 1, true)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M15 5l-7 7 7 7\" />\n </svg>\n </button>\n {playing ? (\n <button type=\"button\" aria-label={paused ? labels.play : labels.pause} onClick={() => setPaused(!paused)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"currentColor\" aria-hidden>\n {paused ? <path d=\"M8 5.5v13l10-6.5Z\" /> : <path d=\"M7 5h3.5v14H7zM13.5 5H17v14h-3.5z\" />}\n </svg>\n </button>\n ) : null}\n <button type=\"button\" aria-label={labels.next} disabled={!loop && current === total - 1} onClick={() => move(current + 1, true)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </div>\n </div>\n </section>\n );\n}\n\n/**\n * The typed reveal of a text, kept minimal: the visible prefix grows at `speed` characters per second while `active`;\n * under reduced motion (or when not active) it is the whole text at once. Render the full text for assistive technology\n * (`sr-only`) and this one `aria-hidden`.\n */\nexport function useTypedText(text: string, { active = true, speed = 45 }: { active?: boolean; speed?: number } = {}) {\n const [typed, setTyped] = React.useState({ of: \"\", count: 0 });\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n React.useEffect(() => {\n if (!active || reduced) return;\n const id = window.setInterval(() => {\n setTyped((state) => {\n const count = state.of === text ? state.count + 1 : 1;\n if (count >= text.length) window.clearInterval(id);\n return { of: text, count };\n });\n }, Math.max(10, 1000 / speed));\n return () => window.clearInterval(id);\n }, [text, active, reduced, speed]);\n const count = typed.of === text ? typed.count : 0;\n const done = !active || reduced || count >= text.length;\n return { text: done ? text : text.slice(0, count), done };\n}\n"],"mappings":"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","names":["count"]}
|
|
1
|
+
{"version":3,"sources":["../src/showcase/showcase.tsx"],"sourcesContent":["// Client (state, timers' observers, keys). A slideshow that advances by itself — and shows it: a segmented progress bar\n// (one segment per slide, the current one fills over the interval, each one a button) with a counter, previous / next and a\n// visible pause button (WCAG 2.2.2). The time IS the CSS animation of the current segment (`.kz-showcase-fill`):\n// when it ends, the next slide comes — so what you see filling is exactly what decides, and pausing the animation pauses\n// the show. Its two rules travel with the component (a hoisted, deduplicated <style>): no stylesheet to import. It waits while the pointer is over it, while a keyboard focus is inside, while the tab is hidden and while it\n// is off screen; under reduced motion it never advances by itself. Slides are slots: any content.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\n\nconst FILL_CSS = \".kz-showcase-fill{transform-origin:left;animation:kz-showcase-fill 6s linear forwards}@keyframes kz-showcase-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\";\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst subscribeNothing = () => () => undefined;\nconst readReduced = () => Boolean(typeof window !== \"undefined\" && window.matchMedia?.(REDUCED).matches);\nfunction subscribeReduced(notify: () => void) {\n const query = window.matchMedia?.(REDUCED);\n query?.addEventListener?.(\"change\", notify);\n return () => query?.removeEventListener?.(\"change\", notify);\n}\nconst readHidden = () => document.visibilityState === \"hidden\";\nfunction subscribeVisibility(notify: () => void) {\n document.addEventListener(\"visibilitychange\", notify);\n return () => document.removeEventListener(\"visibilitychange\", notify);\n}\n\nexport interface ShowcaseLabels {\n /** The previous button's name. */\n previous: string;\n /** The next button's name. */\n next: string;\n /** The pause button's name while it plays. */\n pause: string;\n /** The same button's name while it is paused. */\n play: string;\n /** A progress segment's name: `(3, 17) => \"Go to image 3\"`. */\n goTo: (n: number, total: number) => string;\n /** The position in words, beside the title: `(1, 17) => \"Image 1 of 17\"`. */\n counter: (n: number, total: number) => string;\n /** The slide's accessible name: `(1, 17) => \"1 of 17\"`. */\n slide: (n: number, total: number) => string;\n}\n\n/** English defaults — pass your own translated `labels`. */\nexport const showcaseLabels: ShowcaseLabels = {\n previous: \"Previous slide\",\n next: \"Next slide\",\n pause: \"Pause the slideshow\",\n play: \"Play the slideshow\",\n goTo: (n) => `Go to slide ${n}`,\n counter: (n, total) => `Slide ${n} of ${total}`,\n slide: (n, total) => `${n} of ${total}`,\n};\n\nexport interface ShowcaseSlideState {\n index: number;\n /** The slide in view. */\n active: boolean;\n}\n\nexport interface ShowcaseProps<T> extends Omit<React.ComponentProps<\"section\">, \"children\" | \"onChange\"> {\n /** The slideshow's accessible name (“Prompt to image”), passed translated. */\n label: string;\n /** The slides, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** One slide's content. Only the slide in view and its neighbours are mounted (the rest stay empty): give slides the same size. */\n renderItem: (item: T, state: ShowcaseSlideState) => React.ReactNode;\n /** A slide's title, shown before the counter (“Golden-hour editorial”). */\n getTitle?: (item: T) => string;\n /** Milliseconds a slide stays (the time the segment takes to fill). @default 6000 */\n interval?: number;\n /** Advances by itself. Never under `prefers-reduced-motion`. @default true */\n autoplay?: boolean;\n /** `fade` crossfades; `slide` also moves the slides a few pixels. @default \"fade\" */\n transition?: \"fade\" | \"slide\";\n /** After the last slide, go back to the first. @default true */\n loop?: boolean;\n /** The slide in view (controlled). */\n index?: number;\n /** The slide shown first. @default 0 */\n defaultIndex?: number;\n /** Called when the slide in view changes — `user` is true when the visitor asked for it. */\n onIndexChange?: (index: number, user: boolean) => void;\n /** The words of the controls, translated. */\n labels?: ShowcaseLabels;\n}\n\n/** An auto-advancing slideshow with a visible, clickable progress bar, a counter, prev / next and a pause button. */\nexport function Showcase<T>({\n label,\n items,\n getId,\n renderItem,\n getTitle,\n interval = 6000,\n autoplay = true,\n transition = \"fade\",\n loop = true,\n index: controlled,\n defaultIndex = 0,\n onIndexChange,\n labels = showcaseLabels,\n className,\n onKeyDown,\n onFocus,\n onBlur,\n onPointerEnter,\n onPointerLeave,\n ...props\n}: ShowcaseProps<T>) {\n const total = items.length;\n const root = React.useRef<HTMLElement>(null);\n const [inner, setInner] = React.useState(defaultIndex);\n const current = Math.min(Math.max(controlled ?? inner, 0), Math.max(total - 1, 0));\n const mounted = React.useSyncExternalStore(subscribeNothing, () => true, () => false);\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n const hidden = React.useSyncExternalStore(subscribeVisibility, readHidden, () => false);\n const [paused, setPaused] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const [inView, setInView] = React.useState(true);\n const [announce, setAnnounce] = React.useState(false);\n\n React.useEffect(() => {\n const host = root.current;\n if (!host || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver((entries) => setInView(entries.some((e) => e.isIntersecting)), { threshold: 0.2 });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n const move = (to: number, user: boolean) => {\n if (total === 0) return;\n const next = loop ? (to + total) % total : Math.min(Math.max(to, 0), total - 1);\n if (next === current) return;\n setAnnounce(user);\n if (controlled === undefined) setInner(next);\n onIndexChange?.(next, user);\n };\n\n const playing = autoplay && mounted && !reduced && total > 1;\n const running = playing && !paused && !hover && !focus && !hidden && inView;\n const lastStop = !loop && current === total - 1;\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n onKeyDown?.(event);\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const target = event.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const to = event.key === \"ArrowRight\" ? current + 1 : event.key === \"ArrowLeft\" ? current - 1 : event.key === \"Home\" ? 0 : event.key === \"End\" ? total - 1 : null;\n if (to === null) return;\n event.preventDefault();\n move(to, true);\n };\n\n const item = items[current];\n const title = item !== undefined ? getTitle?.(item) : undefined;\n const button = \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-default bg-surface-1 text-fg-secondary transition-colors hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40\";\n\n return (\n <section\n ref={root}\n aria-roledescription=\"carousel\"\n aria-label={label}\n data-running={running ? \"\" : undefined}\n className={cn(\"flex flex-col gap-3\", className)}\n onKeyDown={handleKeyDown}\n onPointerEnter={(e) => {\n onPointerEnter?.(e);\n if (e.pointerType === \"mouse\") setHover(true);\n }}\n onPointerLeave={(e) => {\n onPointerLeave?.(e);\n setHover(false);\n }}\n onFocus={(e) => {\n onFocus?.(e);\n // A mouse click on a button is not a reason to stop: only a keyboard focus is.\n let visible = true;\n try {\n visible = e.target.matches(\":focus-visible\");\n } catch {\n /* a stand-in without :focus-visible */\n }\n if (visible) setFocus(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n if (!e.currentTarget.contains(e.relatedTarget)) setFocus(false);\n }}\n {...props}\n >\n <style href=\"kz-showcase\" precedence=\"medium\">\n {FILL_CSS}\n </style>\n <div aria-live={announce ? \"polite\" : \"off\"} className=\"grid grid-cols-[minmax(0,1fr)]\">\n {items.map((slide, i) => {\n const active = i === current;\n const gap = Math.abs(i - current);\n const near = gap <= 1 || (loop && total - gap <= 1);\n const side = i < current ? \"-translate-x-4\" : \"translate-x-4\";\n return (\n <div\n key={getId(slide)}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={labels.slide(i + 1, total)}\n aria-hidden={active ? undefined : true}\n inert={active ? undefined : true}\n data-active={active ? \"\" : undefined}\n className={cn(\n \"col-start-1 row-start-1 min-w-0 transition-[opacity,translate,visibility] duration-500 motion-reduce:transition-none\",\n active ? \"visible opacity-100\" : \"invisible opacity-0\",\n transition === \"slide\" && (active ? \"translate-x-0\" : side),\n )}\n >\n {near ? renderItem(slide, { index: i, active }) : null}\n </div>\n );\n })}\n </div>\n\n <div className=\"flex gap-1\">\n {items.map((slide, i) => (\n <button\n key={getId(slide)}\n type=\"button\"\n aria-label={labels.goTo(i + 1, total)}\n aria-current={i === current ? \"true\" : undefined}\n tabIndex={i === current ? 0 : -1}\n onClick={() => move(i, true)}\n className=\"group/seg relative flex h-6 min-w-0 flex-1 cursor-pointer items-center rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <span className=\"relative block h-1 w-full overflow-hidden rounded-full bg-surface-3 transition-[height] group-hover/seg:h-1.5\">\n {i < current || (i === current && (!playing || lastStop)) ? <span className=\"absolute inset-0 bg-accent\" /> : null}\n {i === current && playing && !lastStop ? (\n <span\n key={`${current}`}\n data-segment-fill\n className=\"kz-showcase-fill absolute inset-0 bg-accent\"\n style={{ animationDuration: `${interval}ms`, animationPlayState: running ? \"running\" : \"paused\" }}\n onAnimationEnd={() => running && move(current + 1, false)}\n />\n ) : null}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"flex items-center gap-3\">\n <p className=\"m-0 min-w-0 flex-1 truncate text-sm text-fg-secondary\">\n {title ? <span className=\"font-medium text-fg\">{title}</span> : null}\n {title ? <span aria-hidden> · </span> : null}\n <span>{labels.counter(current + 1, total)}</span>\n </p>\n <div className=\"flex items-center gap-1.5\">\n <button type=\"button\" aria-label={labels.previous} disabled={!loop && current === 0} onClick={() => move(current - 1, true)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M15 5l-7 7 7 7\" />\n </svg>\n </button>\n {playing ? (\n <button type=\"button\" aria-label={paused ? labels.play : labels.pause} onClick={() => setPaused(!paused)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"currentColor\" aria-hidden>\n {paused ? <path d=\"M8 5.5v13l10-6.5Z\" /> : <path d=\"M7 5h3.5v14H7zM13.5 5H17v14h-3.5z\" />}\n </svg>\n </button>\n ) : null}\n <button type=\"button\" aria-label={labels.next} disabled={!loop && current === total - 1} onClick={() => move(current + 1, true)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </div>\n </div>\n </section>\n );\n}\n\n/**\n * The typed reveal of a text, kept minimal: the visible prefix grows at `speed` characters per second while `active`;\n * under reduced motion (or when not active) it is the whole text at once. Render the full text for assistive technology\n * (`sr-only`) and this one `aria-hidden`.\n */\nexport function useTypedText(text: string, { active = true, speed = 45 }: { active?: boolean; speed?: number } = {}) {\n const [typed, setTyped] = React.useState({ of: \"\", count: 0 });\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n React.useEffect(() => {\n if (!active || reduced) return;\n const id = window.setInterval(() => {\n setTyped((state) => {\n const count = state.of === text ? state.count + 1 : 1;\n if (count >= text.length) window.clearInterval(id);\n return { of: text, count };\n });\n }, Math.max(10, 1000 / speed));\n return () => window.clearInterval(id);\n }, [text, active, reduced, speed]);\n const count = typed.of === text ? typed.count : 0;\n const done = !active || reduced || count >= text.length;\n return { text: done ? text : text.slice(0, count), done };\n}\n"],"mappings":"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","names":["count"]}
|
package/dist/site-footer.js
CHANGED
|
@@ -17,17 +17,17 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
var siteFooter = tv({
|
|
18
18
|
slots: {
|
|
19
19
|
root: "w-full border-t border-border-subtle bg-surface-0 text-fg",
|
|
20
|
-
inner: "mx-auto grid w-full max-w-(--kz-page-max,72rem) gap-10 px-(--kz-page-gutter,16px) py-12",
|
|
21
|
-
top: "grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]",
|
|
20
|
+
inner: "mx-auto grid w-full max-w-(--kz-page-max,72rem) grid-cols-[minmax(0,1fr)] gap-10 px-(--kz-page-gutter,16px) py-12",
|
|
21
|
+
top: "grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]",
|
|
22
22
|
site: "grid content-start gap-3",
|
|
23
23
|
siteName: "inline-flex items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg",
|
|
24
24
|
siteText: "max-w-xs text-sm leading-relaxed text-fg-secondary",
|
|
25
25
|
own: "grid grid-cols-2 gap-8 sm:grid-cols-3",
|
|
26
26
|
family: "grid grid-cols-2 gap-8 border-t border-border-subtle pt-8 md:grid-cols-4",
|
|
27
|
-
column: "grid content-start gap-3",
|
|
27
|
+
column: "grid min-w-0 content-start gap-3",
|
|
28
28
|
title: "text-[11px] font-semibold uppercase tracking-wider text-fg-secondary",
|
|
29
|
-
list: "grid gap-2",
|
|
30
|
-
link: "inline-flex min-h-6 items-center gap-2 rounded-md text-sm text-fg-secondary no-underline transition-colors hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold",
|
|
29
|
+
list: "grid min-w-0 gap-2",
|
|
30
|
+
link: "inline-flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 rounded-md text-sm [overflow-wrap:anywhere] text-fg-secondary no-underline transition-colors hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold",
|
|
31
31
|
mark: "flex h-4 w-4 shrink-0 items-center justify-center text-fg",
|
|
32
32
|
tag: "rounded-full bg-surface-3 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-fg-secondary",
|
|
33
33
|
bottom: "flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-border-subtle pt-6 text-xs text-fg-secondary",
|
package/dist/site-footer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/site-footer/site-footer.tsx"],"sourcesContent":["// Client (the marks hold an id). The footer shared by every site of the Krizaka family: the site's own columns, then the\n// family in columns from `@krizaka/ui/sites` (the same groups as the bar's switcher), then the signature line — “A\n// Krizaka product” by the kind of the current site — the version and the place.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { FamilyMark } from \"../family-mark\";\nimport { KrizakaLogo } from \"../marks/KrizakaLogo\";\nimport { type FamilyLabels, familyLabels, siteById, type SiteId, sitesByGroup } from \"../sites/sites\";\n\nexport const siteFooter = tv({\n slots: {\n root: \"w-full border-t border-border-subtle bg-surface-0 text-fg\",\n inner: \"mx-auto grid w-full max-w-(--kz-page-max,72rem) gap-10 px-(--kz-page-gutter,16px) py-12\",\n top: \"grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]\",\n site: \"grid content-start gap-3\",\n siteName: \"inline-flex items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg\",\n siteText: \"max-w-xs text-sm leading-relaxed text-fg-secondary\",\n own: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n family: \"grid grid-cols-2 gap-8 border-t border-border-subtle pt-8 md:grid-cols-4\",\n column: \"grid content-start gap-3\",\n title: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n list: \"grid gap-2\",\n link:\n \"inline-flex min-h-6 items-center gap-2 rounded-md text-sm text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold\",\n mark: \"flex h-4 w-4 shrink-0 items-center justify-center text-fg\",\n tag: \"rounded-full bg-surface-3 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-border-subtle pt-6 text-xs text-fg-secondary\",\n signature:\n \"inline-flex items-center gap-2 rounded-md font-medium text-fg no-underline hover:text-fg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n version: \"rounded-full border border-border-subtle bg-surface-2 px-2 py-0.5 font-mono text-[11px] text-fg-secondary\",\n legal: \"flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto\",\n },\n});\nconst s = siteFooter();\n\nexport type SiteFooterProps = Omit<React.ComponentProps<\"footer\">, \"children\"> & {\n /** The site this footer belongs to (`@krizaka/ui/sites`): its mark and line, its signature, marked in the family. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** The site's own columns (`SiteFooter.Column`), before the family. */\n children?: React.ReactNode;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */\n legal?: React.ReactNode;\n};\n\n/** The footer of a Krizaka site: its own columns, the family in columns, the signature, the version, Montréal. */\nexport function SiteFooter({ current, labels = familyLabels.en, children, version, env, legal, className, ...props }: SiteFooterProps) {\n const id = React.useId();\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n <div className={s.top()}>\n <div className={s.site()}>\n <a href={site.url} className={s.siteName()}>\n <FamilyMark mark={site.mark} size={24} />\n {site.name}\n </a>\n <p className={s.siteText()}>{labels.descriptions[site.id]}</p>\n </div>\n {children && <div className={s.own()}>{children}</div>}\n </div>\n <nav aria-label={labels.family} className={s.family()}>\n {sitesByGroup().map(({ group, sites }) => (\n <div key={group} className={s.column()}>\n <p id={`${id}-${group}`} className={s.title()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className={s.list()}>\n {sites.map((item) => (\n <li key={item.id}>\n <a href={item.url} aria-current={item.id === current ? \"true\" : undefined} className={s.link()}>\n <span className={s.mark()}>\n <FamilyMark mark={item.mark} size={16} />\n </span>\n {item.name}\n {item.status !== \"released\" && <span className={s.tag()}>{labels.status[item.status]}</span>}\n {item.id === current && <span className=\"sr-only\">({labels.current})</span>}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </nav>\n <div className={s.bottom()}>\n <a href={krizaka.url} className={s.signature()}>\n <KrizakaLogo size={18} animated={false} />\n {labels.signature[site.kind]}\n </a>\n {tag && (\n <span className={s.version()}>\n <span className=\"sr-only\">{labels.version} </span>\n {tag}\n </span>\n )}\n <span>{labels.place}</span>\n {legal && <div className={s.legal()}>{legal}</div>}\n </div>\n </div>\n </footer>\n );\n}\n\nexport type SiteFooterColumnProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The column's heading (“Docs”) — passed translated. */\n title: string;\n /** Its links (`SiteFooter.Link`). */\n children: React.ReactNode;\n};\n\n/** One of the site's own columns: a heading and its links. */\nexport function SiteFooterColumn({ title, children, className, ...props }: SiteFooterColumnProps) {\n const id = React.useId();\n return (\n <div className={s.column({ className })} {...props}>\n <p id={id} className={s.title()}>\n {title}\n </p>\n <ul aria-labelledby={id} className={s.list()}>\n {React.Children.map(children, (child) => (child ? <li>{child}</li> : null))}\n </ul>\n </div>\n );\n}\n\nexport type SiteFooterLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page: `aria-current=\"page\"`. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the footer. */\nexport function SiteFooterLink({ active, asChild, className, ...props }: SiteFooterLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.link({ className })} {...props} />;\n}\n\nSiteFooter.Column = SiteFooterColumn;\nSiteFooter.Link = SiteFooterLink;\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/site-footer/site-footer.tsx"],"sourcesContent":["// Client (the marks hold an id). The footer shared by every site of the Krizaka family: the site's own columns, then the\n// family in columns from `@krizaka/ui/sites` (the same groups as the bar's switcher), then the signature line — “A\n// Krizaka product” by the kind of the current site — the version and the place.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { FamilyMark } from \"../family-mark\";\nimport { KrizakaLogo } from \"../marks/KrizakaLogo\";\nimport { type FamilyLabels, familyLabels, siteById, type SiteId, sitesByGroup } from \"../sites/sites\";\n\nexport const siteFooter = tv({\n slots: {\n root: \"w-full border-t border-border-subtle bg-surface-0 text-fg\",\n inner: \"mx-auto grid w-full max-w-(--kz-page-max,72rem) grid-cols-[minmax(0,1fr)] gap-10 px-(--kz-page-gutter,16px) py-12\",\n top: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]\",\n site: \"grid content-start gap-3\",\n siteName: \"inline-flex items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg\",\n siteText: \"max-w-xs text-sm leading-relaxed text-fg-secondary\",\n own: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n family: \"grid grid-cols-2 gap-8 border-t border-border-subtle pt-8 md:grid-cols-4\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n list: \"grid min-w-0 gap-2\",\n link:\n \"inline-flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 rounded-md text-sm [overflow-wrap:anywhere] text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold\",\n mark: \"flex h-4 w-4 shrink-0 items-center justify-center text-fg\",\n tag: \"rounded-full bg-surface-3 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-border-subtle pt-6 text-xs text-fg-secondary\",\n signature:\n \"inline-flex items-center gap-2 rounded-md font-medium text-fg no-underline hover:text-fg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n version: \"rounded-full border border-border-subtle bg-surface-2 px-2 py-0.5 font-mono text-[11px] text-fg-secondary\",\n legal: \"flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto\",\n },\n});\nconst s = siteFooter();\n\nexport type SiteFooterProps = Omit<React.ComponentProps<\"footer\">, \"children\"> & {\n /** The site this footer belongs to (`@krizaka/ui/sites`): its mark and line, its signature, marked in the family. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** The site's own columns (`SiteFooter.Column`), before the family. */\n children?: React.ReactNode;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */\n legal?: React.ReactNode;\n};\n\n/** The footer of a Krizaka site: its own columns, the family in columns, the signature, the version, Montréal. */\nexport function SiteFooter({ current, labels = familyLabels.en, children, version, env, legal, className, ...props }: SiteFooterProps) {\n const id = React.useId();\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n <div className={s.top()}>\n <div className={s.site()}>\n <a href={site.url} className={s.siteName()}>\n <FamilyMark mark={site.mark} size={24} />\n {site.name}\n </a>\n <p className={s.siteText()}>{labels.descriptions[site.id]}</p>\n </div>\n {children && <div className={s.own()}>{children}</div>}\n </div>\n <nav aria-label={labels.family} className={s.family()}>\n {sitesByGroup().map(({ group, sites }) => (\n <div key={group} className={s.column()}>\n <p id={`${id}-${group}`} className={s.title()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className={s.list()}>\n {sites.map((item) => (\n <li key={item.id}>\n <a href={item.url} aria-current={item.id === current ? \"true\" : undefined} className={s.link()}>\n <span className={s.mark()}>\n <FamilyMark mark={item.mark} size={16} />\n </span>\n {item.name}\n {item.status !== \"released\" && <span className={s.tag()}>{labels.status[item.status]}</span>}\n {item.id === current && <span className=\"sr-only\">({labels.current})</span>}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </nav>\n <div className={s.bottom()}>\n <a href={krizaka.url} className={s.signature()}>\n <KrizakaLogo size={18} animated={false} />\n {labels.signature[site.kind]}\n </a>\n {tag && (\n <span className={s.version()}>\n <span className=\"sr-only\">{labels.version} </span>\n {tag}\n </span>\n )}\n <span>{labels.place}</span>\n {legal && <div className={s.legal()}>{legal}</div>}\n </div>\n </div>\n </footer>\n );\n}\n\nexport type SiteFooterColumnProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The column's heading (“Docs”) — passed translated. */\n title: string;\n /** Its links (`SiteFooter.Link`). */\n children: React.ReactNode;\n};\n\n/** One of the site's own columns: a heading and its links. */\nexport function SiteFooterColumn({ title, children, className, ...props }: SiteFooterColumnProps) {\n const id = React.useId();\n return (\n <div className={s.column({ className })} {...props}>\n <p id={id} className={s.title()}>\n {title}\n </p>\n <ul aria-labelledby={id} className={s.list()}>\n {React.Children.map(children, (child) => (child ? <li>{child}</li> : null))}\n </ul>\n </div>\n );\n}\n\nexport type SiteFooterLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page: `aria-current=\"page\"`. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the footer. */\nexport function SiteFooterLink({ active, asChild, className, ...props }: SiteFooterLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.link({ className })} {...props} />;\n}\n\nSiteFooter.Column = SiteFooterColumn;\nSiteFooter.Link = SiteFooterLink;\n"],"mappings":"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","names":[]}
|
package/dist/vertical-video.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "The Krizaka front-end platform's components: the primitives (button, card, dialog, toast, menu, field, tabs, chip, switch, slider, command palette, theme, countdown…) on the semantic --kz-* tokens, the animated marks (Krizaka, Orazaka, Orochia, Yasakia, Yororis) and the motion signature — web and /native.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"krizaka",
|
|
@@ -614,7 +614,7 @@
|
|
|
614
614
|
],
|
|
615
615
|
"peerDependencies": {
|
|
616
616
|
"@krizaka/query": ">=0.2.1",
|
|
617
|
-
"@krizaka/tailwind": ">=3.
|
|
617
|
+
"@krizaka/tailwind": ">=3.12.0",
|
|
618
618
|
"@maplibre/maplibre-react-native": ">=11",
|
|
619
619
|
"@shopify/flash-list": ">=2",
|
|
620
620
|
"expo-camera": ">=16",
|
|
@@ -2099,7 +2099,7 @@
|
|
|
2099
2099
|
"tailwind-variants": "^3.3.1",
|
|
2100
2100
|
"@krizaka/intl": "^0.2.1",
|
|
2101
2101
|
"@krizaka/pacer": "^0.1.0",
|
|
2102
|
-
"@krizaka/tokens": "^3.
|
|
2102
|
+
"@krizaka/tokens": "^3.12.0"
|
|
2103
2103
|
},
|
|
2104
2104
|
"scripts": {
|
|
2105
2105
|
"build": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});for(const f of fs.readdirSync('registry/examples',{recursive:true}))if(/\\.(js|d\\.ts)$/.test(f))fs.rmSync('registry/examples/'+f)\" && tsup && node -e \"require('node:fs').copyFileSync('src/motion.css','dist/motion.css');require('node:fs').copyFileSync('src/blocks.css','dist/blocks.css');require('node:fs').copyFileSync('src/story.css','dist/story.css')\" && node scripts/build-registry.mjs",
|
package/registry/avatar.json
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
],
|
|
24
24
|
"bestPractices": [
|
|
25
25
|
"Always pass a `fallback` (initials) on the web: images fail, and the layout should not jump.",
|
|
26
|
+
"A profile can lack a photo: pass `defaultAvatar={{ brand, seed: user.id }}` and the person gets the product's own default picture (a silhouette on the brand's gradient, one of six tones, always the same for the same id) instead of an empty circle. It also replaces a photo that fails to load. `fallbackSrc` is the same for your own image; `kind: \"plain\"` is the gradient alone, for initials.",
|
|
27
|
+
"The default pictures are generated from the code (`defaultAvatarSvg`) and served by the Krizaka CDN (`defaultAvatarUrl`, 256 and 512 px WebP, SVG): a product never draws its own.",
|
|
26
28
|
"Use one size per list: `sm` in dense lists, `md` by default, `lg`/`xl` on a profile.",
|
|
27
29
|
"In a group, set `max` so a long list ends with `+n` instead of overflowing."
|
|
28
30
|
],
|
|
@@ -53,6 +55,13 @@
|
|
|
53
55
|
"description": "The image's text alternative (the person's name); empty when a visible name sits next to the avatar.",
|
|
54
56
|
"required": false
|
|
55
57
|
},
|
|
58
|
+
{
|
|
59
|
+
"name": "defaultAvatar",
|
|
60
|
+
"type": "DefaultAvatar",
|
|
61
|
+
"default": null,
|
|
62
|
+
"description": "The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\none of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.",
|
|
63
|
+
"required": false
|
|
64
|
+
},
|
|
56
65
|
{
|
|
57
66
|
"name": "fallback",
|
|
58
67
|
"type": "ReactNode",
|
|
@@ -60,6 +69,13 @@
|
|
|
60
69
|
"description": "Shown while the image loads, when it fails or when there is none: initials, an icon.",
|
|
61
70
|
"required": false
|
|
62
71
|
},
|
|
72
|
+
{
|
|
73
|
+
"name": "fallbackSrc",
|
|
74
|
+
"type": "string",
|
|
75
|
+
"default": null,
|
|
76
|
+
"description": "The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading.",
|
|
77
|
+
"required": false
|
|
78
|
+
},
|
|
63
79
|
{
|
|
64
80
|
"name": "size",
|
|
65
81
|
"type": "AvatarSize",
|
|
@@ -107,6 +123,13 @@
|
|
|
107
123
|
"description": "The image's text alternative (the person's name); empty when a visible name sits next to the avatar.",
|
|
108
124
|
"required": false
|
|
109
125
|
},
|
|
126
|
+
{
|
|
127
|
+
"name": "defaultAvatar",
|
|
128
|
+
"type": "DefaultAvatar",
|
|
129
|
+
"default": null,
|
|
130
|
+
"description": "The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\none of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.",
|
|
131
|
+
"required": false
|
|
132
|
+
},
|
|
110
133
|
{
|
|
111
134
|
"name": "fallback",
|
|
112
135
|
"type": "ReactNode",
|
|
@@ -114,6 +137,13 @@
|
|
|
114
137
|
"description": "Shown while the image loads, when it fails or when there is none: initials, an icon.",
|
|
115
138
|
"required": false
|
|
116
139
|
},
|
|
140
|
+
{
|
|
141
|
+
"name": "fallbackSrc",
|
|
142
|
+
"type": "string",
|
|
143
|
+
"default": null,
|
|
144
|
+
"description": "The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading.",
|
|
145
|
+
"required": false
|
|
146
|
+
},
|
|
117
147
|
{
|
|
118
148
|
"name": "size",
|
|
119
149
|
"type": "AvatarSize",
|
|
@@ -129,6 +159,16 @@
|
|
|
129
159
|
"required": false
|
|
130
160
|
}
|
|
131
161
|
]
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"component": "DEFAULT_AVATAR_BASE",
|
|
165
|
+
"description": "Where the set lives. `v1` is the set's version: a changed picture is a new version folder, never an overwrite.",
|
|
166
|
+
"props": []
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"component": "DEFAULT_AVATAR_TONES",
|
|
170
|
+
"description": "How many tones each brand has: a person always gets the same one from their seed, so a list is not one colour.",
|
|
171
|
+
"props": []
|
|
132
172
|
}
|
|
133
173
|
],
|
|
134
174
|
"examples": [
|
|
@@ -140,6 +180,14 @@
|
|
|
140
180
|
"module": "@krizaka/ui/registry/examples/avatar/with-image",
|
|
141
181
|
"code": "import { Avatar } from \"@krizaka/ui/avatar\";\n\n// A portrait: the image when it loads, the initials while it loads or when it fails.\nexport default function AvatarWithImage() {\n return <Avatar src=\"https://krizaka-assets.b-cdn.net/site/people/ui/ada-256.31a5ce640c.webp\" alt=\"Ada Okafor\" fallback=\"AO\" />;\n}\n"
|
|
142
182
|
},
|
|
183
|
+
{
|
|
184
|
+
"name": "default",
|
|
185
|
+
"title": "Default pictures",
|
|
186
|
+
"description": "No photo: `defaultAvatar={{ brand, seed }}` — the brand's gradient and a silhouette, one of six tones per person.",
|
|
187
|
+
"path": "examples/avatar/default.tsx",
|
|
188
|
+
"module": "@krizaka/ui/registry/examples/avatar/default",
|
|
189
|
+
"code": "import { Avatar } from \"@krizaka/ui/avatar\";\n\n// No photo: the brand's default picture, one of six tones from the person's id — the same one on every screen.\nexport default function AvatarDefault() {\n const people = [\"ada\", \"camille\", \"noor\", \"tomas\", \"ines\", \"kofi\"];\n return (\n <div className=\"flex items-center gap-3\">\n {people.map((id) => (\n <Avatar key={id} defaultAvatar={{ brand: \"orochia\", seed: id }} alt={id} fallback={id.slice(0, 2)} size=\"lg\" />\n ))}\n </div>\n );\n}\n"
|
|
190
|
+
},
|
|
143
191
|
{
|
|
144
192
|
"name": "fallback",
|
|
145
193
|
"title": "Fallback",
|
|
@@ -172,6 +220,7 @@
|
|
|
172
220
|
"differences": [
|
|
173
221
|
"The fallback is the initial of `alt` when `fallback` is not given.",
|
|
174
222
|
"`size` also takes a number of points.",
|
|
223
|
+
"`defaultAvatar` and `fallbackSrc` work as on the web (the default pictures are WebP, drawn by `Image`); `defaultAvatarUrl` is exported from `@krizaka/ui/native`.",
|
|
175
224
|
"`src` is a URI string (an `Image` source), not a URL object.",
|
|
176
225
|
"An SVG `src` (`….svg`, `data:image/svg+xml,…`, or any URI with `svg`) is drawn by react-native-svg — `Image` cannot.",
|
|
177
226
|
"`presence` adds a dot (online, away, busy, offline) and `ring` a story ring (new, seen, live): mobile only for now."
|
|
@@ -188,6 +237,13 @@
|
|
|
188
237
|
"description": "The person's name: the image's accessible name and the source of the default initial.",
|
|
189
238
|
"required": false
|
|
190
239
|
},
|
|
240
|
+
{
|
|
241
|
+
"name": "defaultAvatar",
|
|
242
|
+
"type": "DefaultAvatar",
|
|
243
|
+
"default": null,
|
|
244
|
+
"description": "The Krizaka default picture (the brand's gradient and a silhouette, one of six tones from `seed`) when there is no photo; `fallbackSrc` wins over it.",
|
|
245
|
+
"required": false
|
|
246
|
+
},
|
|
191
247
|
{
|
|
192
248
|
"name": "fallback",
|
|
193
249
|
"type": "ReactNode",
|
|
@@ -195,6 +251,13 @@
|
|
|
195
251
|
"description": "Shown while there is no image or when it fails: default, the first letter of `alt`.",
|
|
196
252
|
"required": false
|
|
197
253
|
},
|
|
254
|
+
{
|
|
255
|
+
"name": "fallbackSrc",
|
|
256
|
+
"type": "string",
|
|
257
|
+
"default": null,
|
|
258
|
+
"description": "The picture to show when `src` is missing or fails to load (an image URI); `fallback` still covers the case of no picture at all.",
|
|
259
|
+
"required": false
|
|
260
|
+
},
|
|
198
261
|
{
|
|
199
262
|
"name": "presence",
|
|
200
263
|
"type": "\"online\" | \"away\" | \"busy\" | \"offline\"",
|
|
@@ -270,6 +333,13 @@
|
|
|
270
333
|
"description": "The person's name: the image's accessible name and the source of the default initial.",
|
|
271
334
|
"required": false
|
|
272
335
|
},
|
|
336
|
+
{
|
|
337
|
+
"name": "defaultAvatar",
|
|
338
|
+
"type": "DefaultAvatar",
|
|
339
|
+
"default": null,
|
|
340
|
+
"description": "The Krizaka default picture (the brand's gradient and a silhouette, one of six tones from `seed`) when there is no photo; `fallbackSrc` wins over it.",
|
|
341
|
+
"required": false
|
|
342
|
+
},
|
|
273
343
|
{
|
|
274
344
|
"name": "fallback",
|
|
275
345
|
"type": "ReactNode",
|
|
@@ -277,6 +347,13 @@
|
|
|
277
347
|
"description": "Shown while there is no image or when it fails: default, the first letter of `alt`.",
|
|
278
348
|
"required": false
|
|
279
349
|
},
|
|
350
|
+
{
|
|
351
|
+
"name": "fallbackSrc",
|
|
352
|
+
"type": "string",
|
|
353
|
+
"default": null,
|
|
354
|
+
"description": "The picture to show when `src` is missing or fails to load (an image URI); `fallback` still covers the case of no picture at all.",
|
|
355
|
+
"required": false
|
|
356
|
+
},
|
|
280
357
|
{
|
|
281
358
|
"name": "presence",
|
|
282
359
|
"type": "\"online\" | \"away\" | \"busy\" | \"offline\"",
|
|
@@ -364,15 +441,19 @@
|
|
|
364
441
|
"files": [
|
|
365
442
|
{
|
|
366
443
|
"path": "avatar/avatar.tsx",
|
|
367
|
-
"content": "// Client: Radix tracks the image's loading state and shows the fallback until it has loaded (or when it fails).\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nconst avatar = tv({\n slots: {\n root: \"relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary\",\n group: \"flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0\",\n },\n variants: {\n size: {\n xs: { root: \"h-5 w-5 text-[9px]\" },\n sm: { root: \"h-8 w-8 text-xs\" },\n md: { root: \"h-10 w-10 text-sm\" },\n lg: { root: \"h-14 w-14 text-base\" },\n xl: { root: \"h-20 w-20 text-lg\" },\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport type AvatarSize = NonNullable<VariantProps<typeof avatar>[\"size\"]>;\n\nexport type AvatarProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */\n alt?: string;\n /** Shown while the image loads, when it fails or when there is none: initials, an icon. */\n fallback?: React.ReactNode;\n /**\n * xs · sm · md · lg · xl.\n * @default \"md\"\n */\n size?: AvatarSize;\n};\n\nexport function AvatarRoot({ src, alt = \"\", fallback, size, className, ...props }: AvatarProps) {\n const s = avatar({ size });\n return (\n <AvatarPrimitive.Root data-size={size ?? \"md\"} className={s.root({ className })} {...props}>\n {
|
|
444
|
+
"content": "// Client: Radix tracks the image's loading state and shows the fallback until it has loaded (or when it fails).\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { type DefaultAvatar, defaultAvatarUrl } from \"./default-avatar\";\n\nconst avatar = tv({\n slots: {\n root: \"relative inline-flex shrink-0 overflow-hidden rounded-full bg-surface-3 align-middle\",\n image: \"h-full w-full object-cover\",\n fallback: \"flex h-full w-full items-center justify-center font-semibold uppercase text-fg-secondary\",\n group: \"flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-surface-0\",\n },\n variants: {\n size: {\n xs: { root: \"h-5 w-5 text-[9px]\" },\n sm: { root: \"h-8 w-8 text-xs\" },\n md: { root: \"h-10 w-10 text-sm\" },\n lg: { root: \"h-14 w-14 text-base\" },\n xl: { root: \"h-20 w-20 text-lg\" },\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport type AvatarSize = NonNullable<VariantProps<typeof avatar>[\"size\"]>;\n\nexport type AvatarProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** The image's text alternative (the person's name); empty when a visible name sits next to the avatar. */\n alt?: string;\n /** Shown while the image loads, when it fails or when there is none: initials, an icon. */\n fallback?: React.ReactNode;\n /** The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading. */\n fallbackSrc?: string;\n /**\n * The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\n * one of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.\n */\n defaultAvatar?: DefaultAvatar;\n /**\n * xs · sm · md · lg · xl.\n * @default \"md\"\n */\n size?: AvatarSize;\n};\n\nexport function AvatarRoot({ src, alt = \"\", fallback, fallbackSrc, defaultAvatar, size, className, ...props }: AvatarProps) {\n const s = avatar({ size });\n const [failed, setFailed] = React.useState<string | null>(null);\n // 512 px for the big sizes, 256 for the others: sharp on a 2x screen either way.\n const backup = fallbackSrc ?? (defaultAvatar ? defaultAvatarUrl(defaultAvatar, { size: size === \"xl\" || size === \"lg\" ? 512 : 256 }) : undefined);\n const shown = src && failed !== src ? src : backup;\n return (\n <AvatarPrimitive.Root data-size={size ?? \"md\"} className={s.root({ className })} {...props}>\n {shown && (\n <AvatarPrimitive.Image\n src={shown}\n alt={alt}\n className={s.image()}\n onLoadingStatusChange={(status) => {\n if (status === \"error\" && shown === src) setFailed(src);\n }}\n />\n )}\n <AvatarPrimitive.Fallback delayMs={shown ? 300 : 0} className={s.fallback()}>\n {fallback}\n </AvatarPrimitive.Fallback>\n </AvatarPrimitive.Root>\n );\n}\n\nexport type AvatarGroupProps = React.ComponentProps<\"div\"> & {\n /** How many avatars are shown; the rest becomes a `+n` avatar. */\n max?: number;\n /** The size of the `+n` avatar (match the avatars of the group). */\n size?: AvatarSize;\n};\n\n/** Overlapping avatars; past `max`, a `+n` avatar counts the rest. */\nexport function AvatarGroup({ max, size, className, children, ...props }: AvatarGroupProps) {\n const items = React.Children.toArray(children);\n const shown = max === undefined ? items : items.slice(0, max);\n const rest = items.length - shown.length;\n return (\n <div className={avatar().group({ className })} {...props}>\n {shown}\n {rest > 0 && <AvatarRoot size={size} fallback={`+${rest}`} />}\n </div>\n );\n}\n\n/** `Avatar` (an image with a fallback) and `Avatar.Group`. */\nexport const Avatar = Object.assign(AvatarRoot, { Group: AvatarGroup });\n"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"path": "avatar/default-avatar.ts",
|
|
448
|
+
"content": "// The default profile pictures of the Krizaka family: what a product shows when a person has no photo.\n// They are generated from this file (tools/export-brand.mjs renders them to SVG and WebP and uploads them to Bunny,\n// `people/defaults/v1/<brand>/…`), so the pictures and the URLs that name them cannot drift apart.\n\nexport type DefaultAvatarBrand = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\";\n/** `person`: a neutral silhouette. `plain`: the brand's gradient alone, ready for initials. */\nexport type DefaultAvatarKind = \"person\" | \"plain\";\nexport type DefaultAvatarSize = 256 | 512;\n\nexport const DEFAULT_AVATAR_BRANDS: readonly DefaultAvatarBrand[] = [\"krizaka\", \"orazaka\", \"orochia\", \"yasakia\", \"yororis\"];\nexport const DEFAULT_AVATAR_KINDS: readonly DefaultAvatarKind[] = [\"person\", \"plain\"];\nexport const DEFAULT_AVATAR_SIZES: readonly DefaultAvatarSize[] = [256, 512];\n/** How many tones each brand has: a person always gets the same one from their seed, so a list is not one colour. */\nexport const DEFAULT_AVATAR_TONES = 6;\n\n/** Where the set lives. `v1` is the set's version: a changed picture is a new version folder, never an overwrite. */\nexport const DEFAULT_AVATAR_BASE = \"https://krizaka-assets.b-cdn.net/people/defaults/v1\";\n\nexport interface DefaultAvatar {\n /** The brand of the product showing the avatar: its gradient, its hues. */\n brand: DefaultAvatarBrand;\n /** Any stable text or number of the person (their id): the same seed gives the same tone everywhere. */\n seed?: string | number;\n /** Force a tone, 1 to 6, instead of deriving it from `seed`. */\n tone?: number;\n /**\n * `person` (default): a silhouette. `plain`: the gradient alone.\n * @default \"person\"\n */\n kind?: DefaultAvatarKind;\n}\n\n/** The mark gradient of each brand (BRAND.md, `$extensions[\"com.krizaka\"].mark`) as [hue, saturation %, lightness %] and the hue spread of its tones. */\nconst BRANDS: Record<DefaultAvatarBrand, { from: [number, number, number]; to: [number, number, number]; spread: number }> = {\n krizaka: { from: [217, 92, 53], to: [217, 92, 68], spread: 14 },\n orazaka: { from: [26, 90.5, 37.1], to: [26, 92, 45], spread: 7 },\n orochia: { from: [262.1, 83.3, 57.8], to: [328.6, 85.5, 70.2], spread: 12 },\n yasakia: { from: [156, 92, 32], to: [178, 90, 32], spread: 12 },\n yororis: { from: [34, 46, 44], to: [104, 28, 42], spread: 10 },\n};\n\n/** White on top of a stop must stay visible: a stop is darkened until the contrast reaches this (WCAG 1.4.11, non-text). */\nconst MIN_CONTRAST = 3.2;\nconst OFFSETS = [0, -1, 1, -2, 2, 3] as const;\n\nfunction hslToHex([h, s, l]: [number, number, number]): string {\n const sat = s / 100;\n const light = l / 100;\n const k = (n: number) => (n + (((h % 360) + 360) % 360) / 30) % 12;\n const a = sat * Math.min(light, 1 - light);\n const channel = (n: number) => Math.round(255 * (light - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))));\n return `#${[0, 8, 4].map((n) => channel(n).toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nconst channelLuminance = (v: number) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);\nconst whiteContrast = (hex: string) => {\n const [r, g, b] = [1, 3, 5].map((i) => channelLuminance(Number.parseInt(hex.slice(i, i + 2), 16) / 255));\n return 1.05 / (0.2126 * r! + 0.7152 * g! + 0.0722 * b! + 0.05);\n};\n/** The stop, darkened one point of lightness at a time until white reads on it. */\nfunction readable([h, s, l]: [number, number, number]): string {\n let hex = hslToHex([h, s, l]);\n for (let light = l; whiteContrast(hex) < MIN_CONTRAST && light > 10; light -= 1) hex = hslToHex([h, s, light]);\n return hex;\n}\n\n/** The two stops (hex) of tone `tone` (1 to 6) of a brand. */\nexport function defaultAvatarTone(brand: DefaultAvatarBrand, tone: number): readonly [string, string] {\n const b = BRANDS[brand];\n const o = OFFSETS[(clampTone(tone) - 1) % OFFSETS.length] * b.spread;\n return [readable([b.from[0] + o, b.from[1], b.from[2]]), readable([b.to[0] + o, b.to[1], b.to[2]])];\n}\n\nconst clampTone = (tone: number) => Math.min(DEFAULT_AVATAR_TONES, Math.max(1, Math.round(tone)));\n\n/** 1 to 6 from a seed (FNV-1a): stable across renders, devices and products. */\nexport function defaultAvatarToneOf(seed: string | number | undefined): number {\n if (seed === undefined) return 1;\n let h = 0x811c9dc5;\n for (const ch of String(seed)) {\n h ^= ch.codePointAt(0) ?? 0;\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return (h % DEFAULT_AVATAR_TONES) + 1;\n}\n\nconst resolveTone = (a: DefaultAvatar) => (a.tone === undefined ? defaultAvatarToneOf(a.seed) : clampTone(a.tone));\n\n/** The file name of a picture in the set: `person-3` + 256 → `person-3-256.webp`; `svg` has no size. */\nexport function defaultAvatarFile(kind: DefaultAvatarKind, tone: number, size: DefaultAvatarSize | \"svg\"): string {\n return size === \"svg\" ? `${kind}-${tone}.svg` : `${kind}-${tone}-${size}.webp`;\n}\n\n/**\n * The URL of a default avatar: `defaultAvatarUrl({ brand: \"orochia\", seed: user.id })`.\n * @param size 256 for lists, 512 for a profile; `\"svg\"` for the vector.\n * @param base Another host or version of the set (a self-hosted copy); the default is the Krizaka CDN.\n */\nexport function defaultAvatarUrl(avatar: DefaultAvatar, { size = 256, base = DEFAULT_AVATAR_BASE }: { size?: DefaultAvatarSize | \"svg\"; base?: string } = {}): string {\n return `${base.replace(/\\/+$/, \"\")}/${avatar.brand}/${defaultAvatarFile(avatar.kind ?? \"person\", resolveTone(avatar), size)}`;\n}\n\n/** The SVG source of a default avatar, 512 × 512 and square (the Avatar clips it round). */\nexport function defaultAvatarSvg(avatar: DefaultAvatar): string {\n const [from, to] = defaultAvatarTone(avatar.brand, resolveTone(avatar));\n const person =\n (avatar.kind ?? \"person\") === \"person\"\n ? `<circle cx=\"256\" cy=\"200\" r=\"86\" fill=\"#fff\" fill-opacity=\".92\"/><path d=\"M84 512c0-108 78-182 172-182s172 74 172 182z\" fill=\"#fff\" fill-opacity=\".92\"/>`\n : \"\";\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\" width=\"512\" height=\"512\"><defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"${from}\"/><stop offset=\"1\" stop-color=\"${to}\"/></linearGradient><radialGradient id=\"l\" cx=\".28\" cy=\".18\" r=\".85\"><stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".24\"/><stop offset=\"1\" stop-color=\"#fff\" stop-opacity=\"0\"/></radialGradient></defs><rect width=\"512\" height=\"512\" fill=\"url(#g)\"/><rect width=\"512\" height=\"512\" fill=\"url(#l)\"/>${person}</svg>`;\n}\n"
|
|
368
449
|
},
|
|
369
450
|
{
|
|
370
451
|
"path": "avatar/index.ts",
|
|
371
|
-
"content": "/** @krizaka/ui/avatar — `Avatar`, `Avatar.Group
|
|
452
|
+
"content": "/** @krizaka/ui/avatar — `Avatar`, `Avatar.Group`, and the default pictures (`defaultAvatarUrl`…). Client (\"use client\"). */\nexport { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, AvatarRoot, type AvatarSize } from \"./avatar\";\nexport {\n DEFAULT_AVATAR_BASE,\n DEFAULT_AVATAR_BRANDS,\n DEFAULT_AVATAR_KINDS,\n DEFAULT_AVATAR_SIZES,\n DEFAULT_AVATAR_TONES,\n type DefaultAvatar,\n type DefaultAvatarBrand,\n defaultAvatarFile,\n type DefaultAvatarKind,\n type DefaultAvatarSize,\n defaultAvatarSvg,\n defaultAvatarTone,\n defaultAvatarToneOf,\n defaultAvatarUrl,\n} from \"./default-avatar\";\n"
|
|
372
453
|
},
|
|
373
454
|
{
|
|
374
455
|
"path": "avatar/meta.ts",
|
|
375
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Avatar\",\n summary: \"A person or an account: their picture, or initials while it loads, when it fails or when there is none.\",\n status: \"stable\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\n \"Next to a name: comments, creators, members, the signed-in account in a header.\",\n \"`Avatar.Group` to show who takes part (followers, collaborators) in little space.\",\n ],\n whenNotToUse: [\n { when: \"For a product, a video or a place: use an image in a card.\", use: \"card\" },\n { when: \"As the only way to say who someone is: show their name too, or give the avatar an `alt`.\" },\n ],\n bestPractices: [\n \"Always pass a `fallback` (initials) on the web: images fail, and the layout should not jump.\",\n \"Use one size per list: `sm` in dense lists, `md` by default, `lg`/`xl` on a profile.\",\n \"In a group, set `max` so a long list ends with `+n` instead of overflowing.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"With `alt`, the picture is an image named by it; without `alt`, it is decorative — put the name in the text next to it.\",\n \"The fallback initials are shown, not announced, when an `alt` names the person.\",\n ],\n },\n related: [\"card\", \"badge\", \"skeleton\"],\n web: {\n imports: [\"Avatar\"],\n examples: [\n { name: \"with-image\", title: \"With an image\", description: \"A picture named by `alt`, with initials as `fallback`.\" },\n { name: \"fallback\", title: \"Fallback\", description: \"No image: the initials.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`xs`, `sm`, `md` (default), `lg`, `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group` with `max`: the others become `+n`.\" },\n ],\n },\n native: {\n imports: [\"Avatar\"],\n differences: [\n \"The fallback is the initial of `alt` when `fallback` is not given.\",\n \"`size` also takes a number of points.\",\n \"`src` is a URI string (an `Image` source), not a URL object.\",\n \"An SVG `src` (`….svg`, `data:image/svg+xml,…`, or any URI with `svg`) is drawn by react-native-svg — `Image` cannot.\",\n \"`presence` adds a dot (online, away, busy, offline) and `ring` a story ring (new, seen, live): mobile only for now.\",\n ],\n examples: [\n { name: \"sizes\", title: \"Sizes\", description: \"Without an image: the initial of `alt`, from `xs` to `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group max={3}`: the rest becomes `+n`.\" },\n { name: \"presence\", title: \"Presence and story rings\", description: \"A presence dot, a story ring (new or seen), said with the name.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
456
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Avatar\",\n summary: \"A person or an account: their picture, or initials while it loads, when it fails or when there is none.\",\n status: \"stable\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\n \"Next to a name: comments, creators, members, the signed-in account in a header.\",\n \"`Avatar.Group` to show who takes part (followers, collaborators) in little space.\",\n ],\n whenNotToUse: [\n { when: \"For a product, a video or a place: use an image in a card.\", use: \"card\" },\n { when: \"As the only way to say who someone is: show their name too, or give the avatar an `alt`.\" },\n ],\n bestPractices: [\n \"Always pass a `fallback` (initials) on the web: images fail, and the layout should not jump.\",\n \"A profile can lack a photo: pass `defaultAvatar={{ brand, seed: user.id }}` and the person gets the product's own default picture (a silhouette on the brand's gradient, one of six tones, always the same for the same id) instead of an empty circle. It also replaces a photo that fails to load. `fallbackSrc` is the same for your own image; `kind: \\\"plain\\\"` is the gradient alone, for initials.\",\n \"The default pictures are generated from the code (`defaultAvatarSvg`) and served by the Krizaka CDN (`defaultAvatarUrl`, 256 and 512 px WebP, SVG): a product never draws its own.\",\n \"Use one size per list: `sm` in dense lists, `md` by default, `lg`/`xl` on a profile.\",\n \"In a group, set `max` so a long list ends with `+n` instead of overflowing.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"With `alt`, the picture is an image named by it; without `alt`, it is decorative — put the name in the text next to it.\",\n \"The fallback initials are shown, not announced, when an `alt` names the person.\",\n ],\n },\n related: [\"card\", \"badge\", \"skeleton\"],\n web: {\n imports: [\"Avatar\"],\n examples: [\n { name: \"with-image\", title: \"With an image\", description: \"A picture named by `alt`, with initials as `fallback`.\" },\n { name: \"default\", title: \"Default pictures\", description: \"No photo: `defaultAvatar={{ brand, seed }}` — the brand's gradient and a silhouette, one of six tones per person.\" },\n { name: \"fallback\", title: \"Fallback\", description: \"No image: the initials.\" },\n { name: \"sizes\", title: \"Sizes\", description: \"`xs`, `sm`, `md` (default), `lg`, `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group` with `max`: the others become `+n`.\" },\n ],\n },\n native: {\n imports: [\"Avatar\"],\n differences: [\n \"The fallback is the initial of `alt` when `fallback` is not given.\",\n \"`size` also takes a number of points.\",\n \"`defaultAvatar` and `fallbackSrc` work as on the web (the default pictures are WebP, drawn by `Image`); `defaultAvatarUrl` is exported from `@krizaka/ui/native`.\",\n \"`src` is a URI string (an `Image` source), not a URL object.\",\n \"An SVG `src` (`….svg`, `data:image/svg+xml,…`, or any URI with `svg`) is drawn by react-native-svg — `Image` cannot.\",\n \"`presence` adds a dot (online, away, busy, offline) and `ring` a story ring (new, seen, live): mobile only for now.\",\n ],\n examples: [\n { name: \"sizes\", title: \"Sizes\", description: \"Without an image: the initial of `alt`, from `xs` to `xl`.\" },\n { name: \"group\", title: \"Group\", description: \"`Avatar.Group max={3}`: the rest becomes `+n`.\" },\n { name: \"presence\", title: \"Presence and story rings\", description: \"A presence dot, a story ring (new or seen), said with the name.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
376
457
|
},
|
|
377
458
|
{
|
|
378
459
|
"path": "meta.ts",
|
|
@@ -400,6 +481,13 @@
|
|
|
400
481
|
"description": "The image's text alternative (the person's name); empty when a visible name sits next to the avatar.",
|
|
401
482
|
"required": false
|
|
402
483
|
},
|
|
484
|
+
{
|
|
485
|
+
"name": "defaultAvatar",
|
|
486
|
+
"type": "DefaultAvatar",
|
|
487
|
+
"default": null,
|
|
488
|
+
"description": "The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\none of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.",
|
|
489
|
+
"required": false
|
|
490
|
+
},
|
|
403
491
|
{
|
|
404
492
|
"name": "fallback",
|
|
405
493
|
"type": "ReactNode",
|
|
@@ -407,6 +495,13 @@
|
|
|
407
495
|
"description": "Shown while the image loads, when it fails or when there is none: initials, an icon.",
|
|
408
496
|
"required": false
|
|
409
497
|
},
|
|
498
|
+
{
|
|
499
|
+
"name": "fallbackSrc",
|
|
500
|
+
"type": "string",
|
|
501
|
+
"default": null,
|
|
502
|
+
"description": "The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading.",
|
|
503
|
+
"required": false
|
|
504
|
+
},
|
|
410
505
|
{
|
|
411
506
|
"name": "size",
|
|
412
507
|
"type": "AvatarSize",
|
|
@@ -454,6 +549,13 @@
|
|
|
454
549
|
"description": "The image's text alternative (the person's name); empty when a visible name sits next to the avatar.",
|
|
455
550
|
"required": false
|
|
456
551
|
},
|
|
552
|
+
{
|
|
553
|
+
"name": "defaultAvatar",
|
|
554
|
+
"type": "DefaultAvatar",
|
|
555
|
+
"default": null,
|
|
556
|
+
"description": "The Krizaka default picture when the person has no photo (or it fails): their brand's gradient and a silhouette,\none of six tones from `seed` — `defaultAvatar={{ brand: \"orochia\", seed: user.id }}`. `fallbackSrc` wins over it.",
|
|
557
|
+
"required": false
|
|
558
|
+
},
|
|
457
559
|
{
|
|
458
560
|
"name": "fallback",
|
|
459
561
|
"type": "ReactNode",
|
|
@@ -461,6 +563,13 @@
|
|
|
461
563
|
"description": "Shown while the image loads, when it fails or when there is none: initials, an icon.",
|
|
462
564
|
"required": false
|
|
463
565
|
},
|
|
566
|
+
{
|
|
567
|
+
"name": "fallbackSrc",
|
|
568
|
+
"type": "string",
|
|
569
|
+
"default": null,
|
|
570
|
+
"description": "The picture to show instead when `src` is missing or fails to load (an image URL). The `fallback` still covers its own loading.",
|
|
571
|
+
"required": false
|
|
572
|
+
},
|
|
464
573
|
{
|
|
465
574
|
"name": "size",
|
|
466
575
|
"type": "AvatarSize",
|
|
@@ -476,6 +585,16 @@
|
|
|
476
585
|
"required": false
|
|
477
586
|
}
|
|
478
587
|
]
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"component": "DEFAULT_AVATAR_BASE",
|
|
591
|
+
"description": "Where the set lives. `v1` is the set's version: a changed picture is a new version folder, never an overwrite.",
|
|
592
|
+
"props": []
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
"component": "DEFAULT_AVATAR_TONES",
|
|
596
|
+
"description": "How many tones each brand has: a person always gets the same one from their seed, so a list is not one colour.",
|
|
597
|
+
"props": []
|
|
479
598
|
}
|
|
480
599
|
]
|
|
481
600
|
}
|
package/registry/card-grid.json
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"Set `min` to the narrowest a card still reads well; the grid does the rest at every width.",
|
|
27
27
|
"`fit=\"fit\"` for two or three cards that should share the whole width; `fill` keeps a lone card at its size.",
|
|
28
28
|
"Keep `reveal` on below the first screen, off on it; `columns` makes the stagger restart on each row.",
|
|
29
|
+
"The default `align=\"stretch\"` gives a row one height; the cards pin their foot to the bottom (`LinkCard` facts + way in, `ArgumentCard` actions) so the footers line up. `align=\"start\"` for cards that unfold.",
|
|
29
30
|
"`as=\"ul\"` when the cards are a list of links."
|
|
30
31
|
],
|
|
31
32
|
"accessibility": {
|
|
@@ -117,6 +118,14 @@
|
|
|
117
118
|
"module": "@krizaka/ui/registry/examples/card-grid/products",
|
|
118
119
|
"code": "import { ProductLogo } from \"@krizaka/ui\";\nimport { CardGrid } from \"@krizaka/ui/card-grid\";\nimport { LinkCard } from \"@krizaka/ui/link-card\";\n\n// `fit=\"fit\"`: the cards of a short row share the whole width — two products, two halves. One column on a phone.\nexport default function CardGridProducts() {\n return (\n <CardGrid className=\"w-full max-w-3xl\" fit=\"fit\" reveal={false}>\n <LinkCard href=\"#orazaka\" icon={<ProductLogo id=\"orazaka\" size={24} animated={false} />} title=\"Orazaka\" description=\"Sovereign AI on your own servers.\" cta=\"Overview\" />\n <LinkCard href=\"#orochia\" icon={<ProductLogo id=\"orochia\" size={24} animated={false} />} title=\"Orochia\" description=\"Creators paid for what they make.\" cta=\"Overview\" />\n </CardGrid>\n );\n}\n"
|
|
119
120
|
},
|
|
121
|
+
{
|
|
122
|
+
"name": "aligned",
|
|
123
|
+
"title": "An aligned row",
|
|
124
|
+
"description": "Cards with sentences of different lengths: one height, and the facts and the way in of every card on the same line.",
|
|
125
|
+
"path": "examples/card-grid/aligned.tsx",
|
|
126
|
+
"module": "@krizaka/ui/registry/examples/card-grid/aligned",
|
|
127
|
+
"code": "import { ProductLogo } from \"@krizaka/ui\";\nimport { CardGrid } from \"@krizaka/ui/card-grid\";\nimport { LinkCard } from \"@krizaka/ui/link-card\";\nimport { Facts } from \"@krizaka/ui/page\";\n\n// A row of cards whose sentences have different lengths: the cells share one height and each card pins its facts\n// and its way in to the bottom, so the status and the link of every card sit on the same line.\nexport default function CardGridAligned() {\n return (\n <CardGrid className=\"w-full max-w-4xl\" min={220} columns={3} reveal={false}>\n <LinkCard layout=\"stack\" href=\"#orazaka\" icon={<ProductLogo id=\"orazaka\" size={28} animated={false} />} title=\"Orazaka\" description=\"Sovereign AI on your own servers: chat, image, video and voice.\" extra={<Facts items={[{ label: \"Dev preview\", tone: \"ok\" }]} />} cta=\"Open orazaka\" />\n <LinkCard layout=\"stack\" href=\"#orochia\" icon={<ProductLogo id=\"orochia\" size={28} animated={false} />} title=\"Orochia\" description=\"Creators, paid.\" extra={<Facts items={[{ label: \"Coming soon\", tone: \"wip\" }]} />} cta=\"Open orochia\" />\n <LinkCard layout=\"stack\" href=\"#yororis\" icon={<ProductLogo id=\"yororis\" size={28} animated={false} />} title=\"Yororis\" description=\"A quiet place to keep what matters to you, built around the moments of a day and the threads between them.\" extra={<Facts items={[{ label: \"Under construction\", tone: \"wip\" }]} />} cta=\"Open yororis\" />\n </CardGrid>\n );\n}\n"
|
|
128
|
+
},
|
|
120
129
|
{
|
|
121
130
|
"name": "reveal",
|
|
122
131
|
"title": "A list of sections",
|
|
@@ -139,7 +148,7 @@
|
|
|
139
148
|
},
|
|
140
149
|
{
|
|
141
150
|
"path": "card-grid/meta.ts",
|
|
142
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Card grid\",\n summary:\n \"A responsive grid of cards: as many columns as the width allows (one on a phone), the cells of a row at one height, a staggered reveal and the light that follows the pointer.\",\n status: \"beta\",\n category: \"blocks\",\n platforms: \"web\",\n whenToUse: [\n \"Entry points of a hub (`LinkCard`), the reasons of a product page (`ArgumentCard`), plans, products.\",\n \"Any set of peer cards that should read as one group with one rhythm.\",\n ],\n whenNotToUse: [\n { when: \"For a sequence the reader follows in order.\", use: \"stepper\" },\n { when: \"For rows of data to compare or sort.\", use: \"data-table\" },\n ],\n bestPractices: [\n \"Set `min` to the narrowest a card still reads well; the grid does the rest at every width.\",\n \"`fit=\\\"fit\\\"` for two or three cards that should share the whole width; `fill` keeps a lone card at its size.\",\n \"Keep `reveal` on below the first screen, off on it; `columns` makes the stagger restart on each row.\",\n \"`as=\\\"ul\\\"` when the cards are a list of links.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"The grid adds no role; `as=\\\"ul\\\"` makes it a list (each cell an `li`).\",\n \"The pointer's light is decorative and never runs on touch screens or under `prefers-reduced-motion`.\",\n \"Revealed cells are only hidden visually until they enter the viewport; under reduced motion they never move.\",\n ],\n },\n related: [\"link-card\", \"argument-card\", \"pointer-light\"],\n web: {\n imports: [\"CardGrid\"],\n examples: [\n { name: \"products\", title: \"Two halves\", description: \"`fit=\\\"fit\\\"`: two cards share the whole width.\" },\n { name: \"reveal\", title: \"A list of sections\", description: \"`as=\\\"ul\\\"`, six links that rise staggered by column on a real page.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
151
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Card grid\",\n summary:\n \"A responsive grid of cards: as many columns as the width allows (one on a phone), the cells of a row at one height, a staggered reveal and the light that follows the pointer.\",\n status: \"beta\",\n category: \"blocks\",\n platforms: \"web\",\n whenToUse: [\n \"Entry points of a hub (`LinkCard`), the reasons of a product page (`ArgumentCard`), plans, products.\",\n \"Any set of peer cards that should read as one group with one rhythm.\",\n ],\n whenNotToUse: [\n { when: \"For a sequence the reader follows in order.\", use: \"stepper\" },\n { when: \"For rows of data to compare or sort.\", use: \"data-table\" },\n ],\n bestPractices: [\n \"Set `min` to the narrowest a card still reads well; the grid does the rest at every width.\",\n \"`fit=\\\"fit\\\"` for two or three cards that should share the whole width; `fill` keeps a lone card at its size.\",\n \"Keep `reveal` on below the first screen, off on it; `columns` makes the stagger restart on each row.\",\n \"The default `align=\\\"stretch\\\"` gives a row one height; the cards pin their foot to the bottom (`LinkCard` facts + way in, `ArgumentCard` actions) so the footers line up. `align=\\\"start\\\"` for cards that unfold.\",\n \"`as=\\\"ul\\\"` when the cards are a list of links.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"The grid adds no role; `as=\\\"ul\\\"` makes it a list (each cell an `li`).\",\n \"The pointer's light is decorative and never runs on touch screens or under `prefers-reduced-motion`.\",\n \"Revealed cells are only hidden visually until they enter the viewport; under reduced motion they never move.\",\n ],\n },\n related: [\"link-card\", \"argument-card\", \"pointer-light\"],\n web: {\n imports: [\"CardGrid\"],\n examples: [\n { name: \"products\", title: \"Two halves\", description: \"`fit=\\\"fit\\\"`: two cards share the whole width.\" },\n { name: \"aligned\", title: \"An aligned row\", description: \"Cards with sentences of different lengths: one height, and the facts and the way in of every card on the same line.\" },\n { name: \"reveal\", title: \"A list of sections\", description: \"`as=\\\"ul\\\"`, six links that rise staggered by column on a real page.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
143
152
|
},
|
|
144
153
|
{
|
|
145
154
|
"path": "meta.ts",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/avatar/default.tsx
|
|
4
|
+
import { Avatar } from "@krizaka/ui/avatar";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
function AvatarDefault() {
|
|
7
|
+
const people = ["ada", "camille", "noor", "tomas", "ines", "kofi"];
|
|
8
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: people.map((id) => /* @__PURE__ */ jsx(Avatar, { defaultAvatar: { brand: "orochia", seed: id }, alt: id, fallback: id.slice(0, 2), size: "lg" }, id)) });
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
AvatarDefault as default
|
|
12
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Avatar } from "@krizaka/ui/avatar";
|
|
2
|
+
|
|
3
|
+
// No photo: the brand's default picture, one of six tones from the person's id — the same one on every screen.
|
|
4
|
+
export default function AvatarDefault() {
|
|
5
|
+
const people = ["ada", "camille", "noor", "tomas", "ines", "kofi"];
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex items-center gap-3">
|
|
8
|
+
{people.map((id) => (
|
|
9
|
+
<Avatar key={id} defaultAvatar={{ brand: "orochia", seed: id }} alt={id} fallback={id.slice(0, 2)} size="lg" />
|
|
10
|
+
))}
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|