@krizaka/ui 3.17.0 → 3.18.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.
Files changed (135) hide show
  1. package/dist/media-grid.d.ts +146 -0
  2. package/dist/media-grid.js +259 -0
  3. package/dist/media-grid.js.map +1 -0
  4. package/package.json +25 -6
  5. package/registry/achievement.json +1 -1
  6. package/registry/action-bar.json +1 -1
  7. package/registry/activity-feed.json +1 -1
  8. package/registry/alert.json +1 -1
  9. package/registry/app-shell.json +1 -1
  10. package/registry/argument-card.json +1 -1
  11. package/registry/audio-player.json +1 -1
  12. package/registry/auth-layout.json +1 -1
  13. package/registry/avatar.json +1 -1
  14. package/registry/badge.json +1 -1
  15. package/registry/button.json +1 -1
  16. package/registry/card-grid.json +1 -1
  17. package/registry/card.json +1 -1
  18. package/registry/catalogue-page.json +1 -1
  19. package/registry/changelog-page.json +1 -1
  20. package/registry/chat.json +1 -1
  21. package/registry/checkbox.json +1 -1
  22. package/registry/chip-link.json +1 -1
  23. package/registry/chip.json +1 -1
  24. package/registry/choice-grid.json +1 -1
  25. package/registry/code-block.json +1 -1
  26. package/registry/combobox.json +1 -1
  27. package/registry/command.json +1 -1
  28. package/registry/component-page.json +1 -1
  29. package/registry/confirm-button.json +1 -1
  30. package/registry/contact-page.json +1 -1
  31. package/registry/continue-prompt.json +1 -1
  32. package/registry/countdown.json +1 -1
  33. package/registry/cta-panel.json +1 -1
  34. package/registry/dashboard-layout.json +1 -1
  35. package/registry/data-table.json +1 -1
  36. package/registry/date-picker.json +1 -1
  37. package/registry/decision-list.json +1 -1
  38. package/registry/description-list.json +1 -1
  39. package/registry/detail-layout.json +1 -1
  40. package/registry/dialog.json +1 -1
  41. package/registry/docs-article-page.json +1 -1
  42. package/registry/docs-hub-page.json +1 -1
  43. package/registry/docs-layout.json +1 -1
  44. package/registry/document-viewer.json +1 -1
  45. package/registry/dropdown-menu.json +1 -1
  46. package/registry/empty-state.json +1 -1
  47. package/registry/error-page.json +1 -1
  48. package/registry/examples/media-grid/feed.d.ts +4 -0
  49. package/registry/examples/media-grid/feed.js +32 -0
  50. package/registry/examples/media-grid/feed.tsx +31 -0
  51. package/registry/examples/media-grid/uniform.d.ts +4 -0
  52. package/registry/examples/media-grid/uniform.js +29 -0
  53. package/registry/examples/media-grid/uniform.tsx +26 -0
  54. package/registry/field.json +1 -1
  55. package/registry/file-upload.json +1 -1
  56. package/registry/filter-bar.json +1 -1
  57. package/registry/form.json +1 -1
  58. package/registry/gallery-page.json +1 -1
  59. package/registry/glow-card.json +1 -1
  60. package/registry/goal-gauge.json +1 -1
  61. package/registry/how-it-works-page.json +1 -1
  62. package/registry/image-cropper.json +1 -1
  63. package/registry/index.json +24 -4
  64. package/registry/iso.json +1 -1
  65. package/registry/kanban.json +1 -1
  66. package/registry/kbd.json +1 -1
  67. package/registry/landing-page.json +1 -1
  68. package/registry/landscape.json +1 -1
  69. package/registry/legal-page.json +1 -1
  70. package/registry/link-card.json +1 -1
  71. package/registry/list.json +1 -1
  72. package/registry/loader.json +1 -1
  73. package/registry/longform-page.json +1 -1
  74. package/registry/map.json +2 -2
  75. package/registry/markdown.json +1 -1
  76. package/registry/marketing-layout.json +1 -1
  77. package/registry/marquee.json +1 -1
  78. package/registry/mascot-quote.json +1 -1
  79. package/registry/mascot-state.json +1 -1
  80. package/registry/mascots.json +1 -1
  81. package/registry/match-dialog.json +1 -1
  82. package/registry/media-capture.json +1 -1
  83. package/registry/media-grid.json +618 -0
  84. package/registry/meter.json +1 -1
  85. package/registry/nav-bar.json +2 -2
  86. package/registry/number-ticker.json +1 -1
  87. package/registry/page-header.json +1 -1
  88. package/registry/page-hero.json +1 -1
  89. package/registry/page.json +1 -1
  90. package/registry/pagination.json +1 -1
  91. package/registry/photo-grid.json +1 -1
  92. package/registry/pointer-light.json +1 -1
  93. package/registry/popover.json +1 -1
  94. package/registry/pricing-page.json +1 -1
  95. package/registry/product-page.json +1 -1
  96. package/registry/profile-card.json +1 -1
  97. package/registry/progress.json +1 -1
  98. package/registry/promise-card.json +1 -1
  99. package/registry/radio-group.json +1 -1
  100. package/registry/recipes.json +1 -1
  101. package/registry/scroll-stage.json +1 -1
  102. package/registry/search-field.json +1 -1
  103. package/registry/section-backdrop.json +1 -1
  104. package/registry/separator.json +1 -1
  105. package/registry/settings-layout.json +1 -1
  106. package/registry/showcase.json +1 -1
  107. package/registry/site-footer.json +2 -2
  108. package/registry/sites.json +1 -1
  109. package/registry/skeleton.json +1 -1
  110. package/registry/skip-link.json +1 -1
  111. package/registry/slider.json +1 -1
  112. package/registry/slot.json +1 -1
  113. package/registry/spinner.json +1 -1
  114. package/registry/split-view.json +1 -1
  115. package/registry/stage-banner.json +1 -1
  116. package/registry/stat.json +1 -1
  117. package/registry/status-page.json +1 -1
  118. package/registry/step-progress.json +1 -1
  119. package/registry/stepper.json +1 -1
  120. package/registry/swipe-deck.json +1 -1
  121. package/registry/switch.json +1 -1
  122. package/registry/tabs.json +1 -1
  123. package/registry/tag-input.json +1 -1
  124. package/registry/tag-selector.json +1 -1
  125. package/registry/terminal-replay.json +1 -1
  126. package/registry/theme.json +1 -1
  127. package/registry/toast.json +1 -1
  128. package/registry/toc.json +1 -1
  129. package/registry/tooltip.json +1 -1
  130. package/registry/tree-view.json +1 -1
  131. package/registry/verified-badge.json +1 -1
  132. package/registry/vertical-video.json +1 -1
  133. package/registry/video-player.json +1 -1
  134. package/registry/video-trimmer.json +1 -1
  135. package/registry/virtual.json +1 -1
@@ -0,0 +1,146 @@
1
+ import * as React from 'react';
2
+
3
+ /** The app's link component (Next's `Link`, React Router's `Link`…): receives `href`, `className` and the children. */
4
+ type LinkComponent = React.ElementType<{
5
+ href: string;
6
+ className?: string;
7
+ children?: React.ReactNode;
8
+ "aria-label"?: string;
9
+ "aria-describedby"?: string;
10
+ }>;
11
+
12
+ interface ShortsShelfProps<T> extends Omit<React.ComponentProps<"section">, "children" | "title"> {
13
+ /** The shelf's accessible name ("Shorts"), passed translated; shown as its heading unless `title` is set. */
14
+ label: string;
15
+ /** A heading different from the name. */
16
+ title?: React.ReactNode;
17
+ /** The clips, in order. */
18
+ items: readonly T[];
19
+ /** A stable key for an item. */
20
+ getId: (item: T) => string;
21
+ /** One card: use `VideoCard aspect="portrait"`. */
22
+ renderItem: (item: T, index: number) => React.ReactNode;
23
+ /** Opens the vertical feed. */
24
+ seeAll?: {
25
+ href: string;
26
+ label: string;
27
+ linkAs?: LinkComponent;
28
+ };
29
+ /** The arrows' names, passed translated. */
30
+ previousLabel: string;
31
+ /** The next arrow's name. */
32
+ nextLabel: string;
33
+ }
34
+ /** A Shorts shelf: equal-height 9:16 cards in a snap-scrolling row. */
35
+ declare function ShortsShelf<T>({ label, title, items, getId, renderItem, seeAll, previousLabel, nextLabel, className, ...props }: ShortsShelfProps<T>): React.JSX.Element | null;
36
+
37
+ type DefaultAvatarBrand = "krizaka" | "orazaka" | "orochia" | "yasakia" | "yororis" | "yomizaka" | "kachizaka" | "akinaza";
38
+ /** `person`: a neutral silhouette. `plain`: the brand's gradient alone, ready for initials. */
39
+ type DefaultAvatarKind = "person" | "plain";
40
+ interface DefaultAvatar {
41
+ /** The brand of the product showing the avatar: its gradient, its hues. */
42
+ brand: DefaultAvatarBrand;
43
+ /** Any stable text or number of the person (their id): the same seed gives the same tone everywhere. */
44
+ seed?: string | number;
45
+ /** Force a tone, 1 to 6, instead of deriving it from `seed`. */
46
+ tone?: number;
47
+ /**
48
+ * `person` (default): a silhouette. `plain`: the gradient alone.
49
+ * @default "person"
50
+ */
51
+ kind?: DefaultAvatarKind;
52
+ }
53
+
54
+ type VideoCardAspect = "landscape" | "portrait";
55
+ interface VideoCardCreator {
56
+ /** The creator's name. */
57
+ name: string;
58
+ /** Their photo. */
59
+ avatarSrc?: string | null;
60
+ /** Their profile. */
61
+ href?: string;
62
+ /** The Krizaka default picture when there is no photo (a brand and a seed). */
63
+ defaultAvatar?: DefaultAvatar;
64
+ }
65
+ interface VideoCardProps extends Omit<React.ComponentProps<"article">, "title"> {
66
+ /** The title; it names the card and is its link. */
67
+ title: string;
68
+ /** Where the card opens. */
69
+ href: string;
70
+ /** The app's router link (Next's `Link`). */
71
+ linkAs?: LinkComponent;
72
+ /**
73
+ * The frame: `landscape` (16:9) or `portrait` (9:16).
74
+ * @default "landscape"
75
+ */
76
+ aspect?: VideoCardAspect;
77
+ /**
78
+ * `stack` (picture over text) or `row` (picture beside the text: the list mode of a section).
79
+ * @default "stack"
80
+ */
81
+ layout?: "stack" | "row";
82
+ /** The picture. */
83
+ poster?: string | null;
84
+ /** An animated picture (WebP) shown while the card is hovered or focused. */
85
+ previewSrc?: string | null;
86
+ /**
87
+ * `cover` fills the frame; `contain` draws the whole picture over a blurred copy of itself (a source of the other
88
+ * orientation: no cropped face, no black bar).
89
+ * @default "cover"
90
+ */
91
+ fit?: "cover" | "contain";
92
+ /** The length, already formatted (`3:42`). */
93
+ duration?: string;
94
+ /** Badges at the top-left of the picture (Free, a price, 18+). Put them on `scrim` so they read in both themes. */
95
+ badges?: React.ReactNode;
96
+ /** Who made it: name, photo, profile. */
97
+ creator?: VideoCardCreator;
98
+ /** What is counted (`12.4K views`), already formatted. */
99
+ meta?: React.ReactNode;
100
+ /** One quick action at the end of the footer (a tip, a save). Its clicks must not follow the card's link. */
101
+ action?: React.ReactNode;
102
+ /** A sensitive picture: blurred and scaled so nothing shows (put the reveal button in `overlay`). */
103
+ blurred?: boolean;
104
+ /** Over the whole picture, above the card's link (a veil, a reveal button). */
105
+ overlay?: React.ReactNode;
106
+ /** Shown instead of the picture while there is none. */
107
+ fallback?: React.ReactNode;
108
+ /** `eager` for the first cards of a page. */
109
+ loading?: "lazy" | "eager";
110
+ }
111
+ /** A video card whose frame, body and footer have one height per aspect, so a grid row or a shelf never staggers. */
112
+ declare function VideoCard({ title, href, linkAs, aspect, layout, poster, previewSrc, fit, duration, badges, creator, meta, action, fallback, blurred, overlay, loading, className, onMouseEnter, onMouseLeave, onFocus, onBlur, ...props }: VideoCardProps): React.JSX.Element;
113
+
114
+ interface MediaGridState {
115
+ /** The frame the card is drawn in (`aspect` of `VideoCard`). */
116
+ aspect: VideoCardAspect;
117
+ /** The card's `layout`. */
118
+ layout: "stack" | "row";
119
+ /** The source's orientation differs from the frame: pass `fit="contain"`. */
120
+ mismatch: boolean;
121
+ }
122
+ interface MediaGridProps<T> extends Omit<React.ComponentProps<"div">, "children"> {
123
+ /** The videos, in order. */
124
+ items: readonly T[];
125
+ /** A stable key for an item. */
126
+ getId: (item: T) => string;
127
+ /** The source's orientation (from its width and height). */
128
+ orientationOf: (item: T) => VideoCardAspect;
129
+ /** One card, drawn in the frame the grid gives (`state`): use `VideoCard`. */
130
+ renderItem: (item: T, state: MediaGridState) => React.ReactNode;
131
+ /**
132
+ * `split`: verticals on the shelf, the others in the grid · `uniform`: all in one frame (`frame`).
133
+ * @default "split"
134
+ */
135
+ mode?: "split" | "uniform";
136
+ /** The frame of `uniform`. @default "landscape" */
137
+ frame?: VideoCardAspect;
138
+ /** `grid` or `list` (one row per video). @default "grid" */
139
+ layout?: "grid" | "list";
140
+ /** The shelf of `split`: its name, "See all" and arrows, passed translated. */
141
+ shelf: Pick<ShortsShelfProps<unknown>, "label" | "title" | "seeAll" | "previousLabel" | "nextLabel">;
142
+ }
143
+ /** A media grid that never mixes heights badly: a shelf for the 9:16, a uniform grid for the rest. */
144
+ declare function MediaGrid<T>({ items, getId, orientationOf, renderItem, mode, frame, layout, shelf, className, ...props }: MediaGridProps<T>): React.JSX.Element;
145
+
146
+ export { MediaGrid, type MediaGridProps, type MediaGridState, ShortsShelf, type ShortsShelfProps, VideoCard, type VideoCardAspect, type VideoCardCreator, type VideoCardProps };
@@ -0,0 +1,259 @@
1
+ "use client";
2
+ import "./chunk-NPF3OHE2.js";
3
+ import {
4
+ Avatar
5
+ } from "./chunk-YIBKJ36L.js";
6
+ import {
7
+ defineRecipe
8
+ } from "./chunk-D3T54RSL.js";
9
+ import {
10
+ cn
11
+ } from "./chunk-2FRWNSRO.js";
12
+
13
+ // src/media-grid/shorts-shelf.tsx
14
+ import * as React from "react";
15
+
16
+ // src/block-link.tsx
17
+ import { jsx } from "react/jsx-runtime";
18
+ var isExternal = (href) => /^[a-z][a-z0-9+.-]*:/i.test(href);
19
+ function BlockLink({
20
+ href,
21
+ linkAs: Link = "a",
22
+ external = isExternal(href),
23
+ rel,
24
+ ...props
25
+ }) {
26
+ if (external) return /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: rel ?? "noopener noreferrer", ...props });
27
+ const Comp = Link;
28
+ return /* @__PURE__ */ jsx(Comp, { href, ...props });
29
+ }
30
+
31
+ // src/media-grid/shorts-shelf.tsx
32
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
33
+ var Chevron = ({ flip }) => /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", width: "18", height: "18", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: flip ? "rotate-180" : void 0, children: /* @__PURE__ */ jsx2("path", { d: "m9 6 6 6-6 6" }) });
34
+ var ARROW = "hidden h-9 w-9 items-center justify-center rounded-full border border-border-default bg-surface-1 text-fg shadow-sm transition-colors hover:bg-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40 [@media(hover:hover)]:inline-flex";
35
+ function ShortsShelf({ label, title, items, getId, renderItem, seeAll, previousLabel, nextLabel, className, ...props }) {
36
+ const row = React.useRef(null);
37
+ const [edge, setEdge] = React.useState({ start: true, end: false });
38
+ const measure = React.useCallback(() => {
39
+ const el = row.current;
40
+ if (!el) return;
41
+ setEdge({ start: el.scrollLeft <= 2, end: el.scrollLeft + el.clientWidth >= el.scrollWidth - 2 });
42
+ }, []);
43
+ React.useEffect(measure, [measure, items.length]);
44
+ const page = (dir) => {
45
+ const el = row.current;
46
+ if (el) el.scrollBy({ left: dir * el.clientWidth * 0.85, behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
47
+ };
48
+ const headingId = React.useId();
49
+ if (items.length === 0) return null;
50
+ return /* @__PURE__ */ jsxs("section", { "aria-labelledby": headingId, "data-shorts-shelf": true, className: cn("min-w-0", className), ...props, children: [
51
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center gap-2", children: [
52
+ /* @__PURE__ */ jsx2("h2", { id: headingId, className: "m-0 mr-auto text-base font-bold text-fg", children: title ?? label }),
53
+ seeAll && /* @__PURE__ */ jsx2(BlockLink, { href: seeAll.href, linkAs: seeAll.linkAs, className: "text-sm font-semibold text-fg-accent hover:underline", children: seeAll.label }),
54
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": previousLabel, disabled: edge.start, onClick: () => page(-1), className: ARROW, children: /* @__PURE__ */ jsx2(Chevron, { flip: true }) }),
55
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": nextLabel, disabled: edge.end, onClick: () => page(1), className: ARROW, children: /* @__PURE__ */ jsx2(Chevron, {}) })
56
+ ] }),
57
+ /* @__PURE__ */ jsx2(
58
+ "ul",
59
+ {
60
+ ref: row,
61
+ onScroll: measure,
62
+ tabIndex: 0,
63
+ "aria-labelledby": headingId,
64
+ className: "m-0 flex list-none snap-x snap-mandatory items-stretch gap-3 overflow-x-auto overscroll-x-contain p-0 pb-2 [scrollbar-width:none] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-scrollbar]:hidden",
65
+ children: items.map((item, i) => /* @__PURE__ */ jsx2("li", { className: "w-[9.5rem] shrink-0 snap-start sm:w-44", children: renderItem(item, i) }, getId(item)))
66
+ }
67
+ )
68
+ ] });
69
+ }
70
+
71
+ // src/media-grid/media-grid.tsx
72
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
73
+ var GRID = {
74
+ landscape: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
75
+ portrait: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6"
76
+ };
77
+ function MediaGrid({ items, getId, orientationOf, renderItem, mode = "split", frame = "landscape", layout = "grid", shelf, className, ...props }) {
78
+ const row = layout === "list";
79
+ const split = mode === "split";
80
+ const verticals = split ? items.filter((i) => orientationOf(i) === "portrait") : [];
81
+ const rest = split ? items.filter((i) => orientationOf(i) !== "portrait") : items;
82
+ const gridFrame = split ? "landscape" : frame;
83
+ const state = (item, aspect) => ({ aspect, layout: row ? "row" : "stack", mismatch: orientationOf(item) !== aspect });
84
+ return /* @__PURE__ */ jsxs2("div", { "data-media-grid": mode, className: cn("flex min-w-0 flex-col gap-8", className), ...props, children: [
85
+ verticals.length > 0 && /* @__PURE__ */ jsx3(ShortsShelf, { ...shelf, items: verticals, getId, renderItem: (item) => renderItem(item, { ...state(item, "portrait"), layout: "stack" }) }),
86
+ rest.length > 0 && /* @__PURE__ */ jsx3("ul", { "data-grid-frame": gridFrame, className: cn("m-0 grid list-none auto-rows-fr gap-4 p-0", row ? "grid-cols-1" : GRID[gridFrame]), children: rest.map((item) => /* @__PURE__ */ jsx3("li", { className: "min-w-0", children: renderItem(item, state(item, gridFrame)) }, getId(item))) })
87
+ ] });
88
+ }
89
+
90
+ // src/media-grid/video-card.tsx
91
+ import * as React2 from "react";
92
+
93
+ // src/card/card.tsx
94
+ import { Slot } from "radix-ui";
95
+ import { tv } from "tailwind-variants";
96
+
97
+ // src/card/recipe.ts
98
+ var cardRecipe = defineRecipe({
99
+ slots: {
100
+ root: "group relative flex flex-col overflow-hidden border border-border-default bg-surface-1 text-fg transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
101
+ media: "relative overflow-hidden bg-media",
102
+ image: "h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-105",
103
+ fallback: "flex h-full w-full items-center justify-center bg-linear-to-br from-accent/15 via-media to-accent-2/15 text-accent/60",
104
+ overlay: "absolute z-10 flex items-center gap-1.5",
105
+ body: "relative flex flex-1 flex-col gap-3",
106
+ title: "line-clamp-1 text-sm font-semibold text-fg transition-colors group-hover:text-fg-accent",
107
+ description: "line-clamp-2 text-xs text-fg-secondary",
108
+ stat: "min-w-0",
109
+ statLabel: "text-[10px] font-semibold uppercase tracking-wider text-fg-secondary",
110
+ statValue: "font-display text-xl font-black tabular-nums text-fg",
111
+ footer: "relative mt-auto flex items-center gap-2 border-t border-border-subtle pt-3 text-xs text-fg-secondary"
112
+ },
113
+ variants: {
114
+ radius: { md: { root: "rounded-md" }, lg: { root: "rounded-lg" }, xl: { root: "rounded-xl" } },
115
+ tone: { default: {}, elevated: { root: "bg-surface-2 shadow-md" }, glass: { root: "bg-surface-1/70 backdrop-blur-md" } },
116
+ interactive: { true: { root: "kz-spotlight kz-lift hover:border-accent/50" } },
117
+ padding: { none: { body: "p-0" }, sm: { body: "p-3" }, md: { body: "p-4" }, lg: { body: "p-6" } }
118
+ },
119
+ defaultVariants: { radius: "xl", tone: "default", padding: "md" }
120
+ });
121
+
122
+ // src/card/card.tsx
123
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
124
+ var card = tv(cardRecipe);
125
+ var slots = card();
126
+ function CardRoot({ asChild, reveal, radius, tone, interactive, className, style, ...props }) {
127
+ const Comp = asChild ? Slot.Root : "div";
128
+ const revealStyle = reveal === void 0 ? style : { ...style, "--kz-delay": `${Math.min(Math.max(reveal, 0), 8) * 50}ms` };
129
+ return /* @__PURE__ */ jsx4(
130
+ Comp,
131
+ {
132
+ "data-reveal": reveal === void 0 ? void 0 : "",
133
+ "data-interactive": interactive ? "" : void 0,
134
+ style: revealStyle,
135
+ className: card({ radius, tone, interactive }).root({ className }),
136
+ ...props
137
+ }
138
+ );
139
+ }
140
+ var ASPECT = { video: "aspect-video", square: "aspect-square", portrait: "aspect-[3/4]", auto: "" };
141
+ function CardMedia({ aspect = "video", className, ...props }) {
142
+ return /* @__PURE__ */ jsx4("div", { "data-aspect": aspect, className: slots.media({ className: cn(ASPECT[aspect], className) }), ...props });
143
+ }
144
+ function CardImage({ src, alt = "", fallback, loading = "lazy", className, ...props }) {
145
+ if (!src) {
146
+ return /* @__PURE__ */ jsx4("div", { "data-fallback": "", "aria-hidden": true, className: slots.fallback({ className }), children: fallback });
147
+ }
148
+ return /* @__PURE__ */ jsx4("img", { src, alt, loading, decoding: "async", className: slots.image({ className }), ...props });
149
+ }
150
+ var CORNER = {
151
+ "top-left": "left-2.5 top-2.5",
152
+ "top-right": "right-2.5 top-2.5",
153
+ "bottom-left": "bottom-2.5 left-2.5",
154
+ "bottom-right": "bottom-2.5 right-2.5"
155
+ };
156
+ function CardOverlay({ corner = "top-left", className, ...props }) {
157
+ return /* @__PURE__ */ jsx4("div", { "data-corner": corner, className: slots.overlay({ className: cn(CORNER[corner], className) }), ...props });
158
+ }
159
+ function CardBody({ padding, className, ...props }) {
160
+ return /* @__PURE__ */ jsx4("div", { className: card({ padding }).body({ className }), ...props });
161
+ }
162
+ function CardTitle({ as: Tag = "h3", className, ...props }) {
163
+ return /* @__PURE__ */ jsx4(Tag, { className: slots.title({ className }), ...props });
164
+ }
165
+ function CardDescription({ className, ...props }) {
166
+ return /* @__PURE__ */ jsx4("p", { className: slots.description({ className }), ...props });
167
+ }
168
+ function CardStat({ label, className, children, ...props }) {
169
+ return /* @__PURE__ */ jsxs3("div", { className: slots.stat({ className }), ...props, children: [
170
+ /* @__PURE__ */ jsx4("p", { className: slots.statLabel(), children: label }),
171
+ /* @__PURE__ */ jsx4("p", { className: slots.statValue(), children })
172
+ ] });
173
+ }
174
+ function CardFooter({ className, ...props }) {
175
+ return /* @__PURE__ */ jsx4("div", { className: slots.footer({ className }), ...props });
176
+ }
177
+ var Card = {
178
+ Root: CardRoot,
179
+ Media: CardMedia,
180
+ Image: CardImage,
181
+ Overlay: CardOverlay,
182
+ Body: CardBody,
183
+ Title: CardTitle,
184
+ Description: CardDescription,
185
+ Stat: CardStat,
186
+ Footer: CardFooter
187
+ };
188
+
189
+ // src/media-grid/video-card.tsx
190
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
191
+ var FRAME = { landscape: "aspect-video", portrait: "aspect-[9/16]" };
192
+ function VideoCard({
193
+ title,
194
+ href,
195
+ linkAs,
196
+ aspect = "landscape",
197
+ layout = "stack",
198
+ poster,
199
+ previewSrc,
200
+ fit = "cover",
201
+ duration,
202
+ badges,
203
+ creator,
204
+ meta,
205
+ action,
206
+ fallback,
207
+ blurred = false,
208
+ overlay,
209
+ loading = "lazy",
210
+ className,
211
+ onMouseEnter,
212
+ onMouseLeave,
213
+ onFocus,
214
+ onBlur,
215
+ ...props
216
+ }) {
217
+ const [live, setLive] = React2.useState(false);
218
+ const picture = live && previewSrc ? previewSrc : poster;
219
+ const row = layout === "row";
220
+ return /* @__PURE__ */ jsx5(
221
+ "article",
222
+ {
223
+ "data-aspect": aspect,
224
+ "data-layout": layout,
225
+ className: cn("group relative h-full min-w-0", className),
226
+ onMouseEnter: (e) => (setLive(true), onMouseEnter?.(e)),
227
+ onMouseLeave: (e) => (setLive(false), onMouseLeave?.(e)),
228
+ onFocus: (e) => (setLive(true), onFocus?.(e)),
229
+ onBlur: (e) => (setLive(false), onBlur?.(e)),
230
+ ...props,
231
+ children: /* @__PURE__ */ jsxs4(Card.Root, { interactive: true, tone: "glass", className: cn("h-full w-full max-w-full", row ? "flex-row" : "flex-col"), children: [
232
+ /* @__PURE__ */ jsxs4(Card.Media, { aspect: "auto", className: cn("shrink-0", FRAME[aspect], row && (aspect === "portrait" ? "w-24 sm:w-28" : "w-40 sm:w-56")), children: [
233
+ fit === "contain" && picture && /* @__PURE__ */ jsx5("img", { src: picture, alt: "", "aria-hidden": true, loading: "lazy", className: "absolute inset-0 h-full w-full scale-125 object-cover opacity-70 blur-2xl" }),
234
+ /* @__PURE__ */ jsx5(Card.Image, { src: picture, loading, fallback, className: cn(fit === "contain" && "relative object-contain", blurred && "scale-110 blur-xl") }),
235
+ overlay && /* @__PURE__ */ jsx5("div", { className: "absolute inset-0 z-20 flex", children: overlay }),
236
+ badges && /* @__PURE__ */ jsx5(Card.Overlay, { corner: "top-left", className: "theme-dark flex-wrap", children: badges }),
237
+ duration && /* @__PURE__ */ jsx5(Card.Overlay, { corner: "bottom-right", children: /* @__PURE__ */ jsx5("span", { className: "rounded-sm bg-scrim px-1.5 py-0.5 font-mono text-[11px] text-fg-on-media", children: duration }) })
238
+ ] }),
239
+ /* @__PURE__ */ jsxs4(Card.Body, { className: "min-w-0 flex-1 justify-between gap-2 p-3", children: [
240
+ /* @__PURE__ */ jsx5("h3", { className: "m-0 min-h-[2.5em] text-sm font-semibold leading-tight text-fg", children: /* @__PURE__ */ jsx5(BlockLink, { href, linkAs, className: "line-clamp-2 outline-none after:absolute after:inset-0 after:z-0 after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-ring", children: title }) }),
241
+ /* @__PURE__ */ jsxs4("div", { className: "relative z-10 flex h-8 items-center gap-2 text-xs text-fg-secondary", children: [
242
+ creator && /* @__PURE__ */ jsxs4(Fragment, { children: [
243
+ /* @__PURE__ */ jsx5(Avatar, { src: creator.avatarSrc, defaultAvatar: creator.defaultAvatar, alt: "", fallback: creator.name.charAt(0), size: "xs", className: "shrink-0" }),
244
+ creator.href ? /* @__PURE__ */ jsx5(BlockLink, { href: creator.href, linkAs, className: "truncate hover:text-fg", children: creator.name }) : /* @__PURE__ */ jsx5("span", { className: "truncate", children: creator.name })
245
+ ] }),
246
+ meta && /* @__PURE__ */ jsx5("span", { className: "ml-auto shrink-0 font-mono text-[11px] text-fg-muted", children: meta }),
247
+ action && /* @__PURE__ */ jsx5("span", { className: "shrink-0", children: action })
248
+ ] })
249
+ ] })
250
+ ] })
251
+ }
252
+ );
253
+ }
254
+ export {
255
+ MediaGrid,
256
+ ShortsShelf,
257
+ VideoCard
258
+ };
259
+ //# sourceMappingURL=media-grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/media-grid/shorts-shelf.tsx","../src/block-link.tsx","../src/media-grid/media-grid.tsx","../src/media-grid/video-card.tsx","../src/card/card.tsx","../src/card/recipe.ts"],"sourcesContent":["// Client (the arrows scroll the row). A horizontal row of 9:16 cards of one height: scroll-snap, arrows with a mouse,\n// a swipe on touch, a \"See all\" link. The row is a labelled list that scrolls with the keyboard (it takes focus).\nimport * as React from \"react\";\n\nimport { BlockLink, type LinkComponent } from \"../block-link\";\nimport { cn } from \"../cn\";\n\nexport interface ShortsShelfProps<T> extends Omit<React.ComponentProps<\"section\">, \"children\" | \"title\"> {\n /** The shelf's accessible name (\"Shorts\"), passed translated; shown as its heading unless `title` is set. */\n label: string;\n /** A heading different from the name. */\n title?: React.ReactNode;\n /** The clips, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** One card: use `VideoCard aspect=\"portrait\"`. */\n renderItem: (item: T, index: number) => React.ReactNode;\n /** Opens the vertical feed. */\n seeAll?: { href: string; label: string; linkAs?: LinkComponent };\n /** The arrows' names, passed translated. */\n previousLabel: string;\n /** The next arrow's name. */\n nextLabel: string;\n}\n\nconst Chevron = ({ flip }: { flip?: boolean }) => (\n <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={flip ? \"rotate-180\" : undefined}>\n <path d=\"m9 6 6 6-6 6\" />\n </svg>\n);\n\nconst ARROW =\n \"hidden h-9 w-9 items-center justify-center rounded-full border border-border-default bg-surface-1 text-fg shadow-sm transition-colors hover:bg-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40 [@media(hover:hover)]:inline-flex\";\n\n/** A Shorts shelf: equal-height 9:16 cards in a snap-scrolling row. */\nexport function ShortsShelf<T>({ label, title, items, getId, renderItem, seeAll, previousLabel, nextLabel, className, ...props }: ShortsShelfProps<T>) {\n const row = React.useRef<HTMLUListElement>(null);\n const [edge, setEdge] = React.useState({ start: true, end: false });\n const measure = React.useCallback(() => {\n const el = row.current;\n if (!el) return;\n setEdge({ start: el.scrollLeft <= 2, end: el.scrollLeft + el.clientWidth >= el.scrollWidth - 2 });\n }, []);\n React.useEffect(measure, [measure, items.length]);\n const page = (dir: 1 | -1) => {\n const el = row.current;\n if (el) el.scrollBy({ left: dir * el.clientWidth * 0.85, behavior: window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ? \"auto\" : \"smooth\" });\n };\n const headingId = React.useId();\n if (items.length === 0) return null;\n return (\n <section aria-labelledby={headingId} data-shorts-shelf className={cn(\"min-w-0\", className)} {...props}>\n <div className=\"mb-3 flex items-center gap-2\">\n <h2 id={headingId} className=\"m-0 mr-auto text-base font-bold text-fg\">\n {title ?? label}\n </h2>\n {seeAll && (\n <BlockLink href={seeAll.href} linkAs={seeAll.linkAs} className=\"text-sm font-semibold text-fg-accent hover:underline\">\n {seeAll.label}\n </BlockLink>\n )}\n <button type=\"button\" aria-label={previousLabel} disabled={edge.start} onClick={() => page(-1)} className={ARROW}>\n <Chevron flip />\n </button>\n <button type=\"button\" aria-label={nextLabel} disabled={edge.end} onClick={() => page(1)} className={ARROW}>\n <Chevron />\n </button>\n </div>\n <ul\n ref={row}\n onScroll={measure}\n // biome-ignore: a scroll container must take the keyboard focus to scroll with the arrow keys\n tabIndex={0}\n aria-labelledby={headingId}\n className=\"m-0 flex list-none snap-x snap-mandatory items-stretch gap-3 overflow-x-auto overscroll-x-contain p-0 pb-2 [scrollbar-width:none] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-scrollbar]:hidden\"\n >\n {items.map((item, i) => (\n <li key={getId(item)} className=\"w-[9.5rem] shrink-0 snap-start sm:w-44\">\n {renderItem(item, i)}\n </li>\n ))}\n </ul>\n </section>\n );\n}\n","// The links of the page blocks (PageHero's back link, LinkCard, ArgumentCard, ActionBar): an internal path renders\n// through the app's router link (`linkAs`, e.g. Next's `Link`), anything else is an `<a>` that opens in a new tab.\n// Shared by the block entries; never exported on its own.\nimport type * as React from \"react\";\n\n/** The app's link component (Next's `Link`, React Router's `Link`…): receives `href`, `className` and the children. */\nexport type LinkComponent = React.ElementType<{ href: string; className?: string; children?: React.ReactNode; \"aria-label\"?: string; \"aria-describedby\"?: string }>;\n\n/** An address that leaves the app: an absolute URL or a protocol (`mailto:`). */\nexport const isExternal = (href: string) => /^[a-z][a-z0-9+.-]*:/i.test(href);\n\nexport function BlockLink({\n href,\n linkAs: Link = \"a\",\n external = isExternal(href),\n rel,\n ...props\n}: Omit<React.ComponentProps<\"a\">, \"href\"> & { href: string; linkAs?: LinkComponent; external?: boolean }) {\n if (external) return <a href={href} target=\"_blank\" rel={rel ?? \"noopener noreferrer\"} {...props} />;\n const Comp = Link as React.ElementType;\n return <Comp href={href} {...props} />;\n}\n","// Client (it renders the shelf). Mixed orientations without a broken grid. `split` (the feed default) sends the 9:16\n// clips to a `ShortsShelf` and lays the rest in a uniform 16:9 grid; `uniform` puts everything in one grid of one frame\n// (a source of the other orientation is told to `contain`, never cropped to a face). `layout=\"list\"` is one column.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { ShortsShelf, type ShortsShelfProps } from \"./shorts-shelf\";\nimport type { VideoCardAspect } from \"./video-card\";\n\nexport interface MediaGridState {\n /** The frame the card is drawn in (`aspect` of `VideoCard`). */\n aspect: VideoCardAspect;\n /** The card's `layout`. */\n layout: \"stack\" | \"row\";\n /** The source's orientation differs from the frame: pass `fit=\"contain\"`. */\n mismatch: boolean;\n}\n\nexport interface MediaGridProps<T> extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** The videos, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** The source's orientation (from its width and height). */\n orientationOf: (item: T) => VideoCardAspect;\n /** One card, drawn in the frame the grid gives (`state`): use `VideoCard`. */\n renderItem: (item: T, state: MediaGridState) => React.ReactNode;\n /**\n * `split`: verticals on the shelf, the others in the grid · `uniform`: all in one frame (`frame`).\n * @default \"split\"\n */\n mode?: \"split\" | \"uniform\";\n /** The frame of `uniform`. @default \"landscape\" */\n frame?: VideoCardAspect;\n /** `grid` or `list` (one row per video). @default \"grid\" */\n layout?: \"grid\" | \"list\";\n /** The shelf of `split`: its name, \"See all\" and arrows, passed translated. */\n shelf: Pick<ShortsShelfProps<unknown>, \"label\" | \"title\" | \"seeAll\" | \"previousLabel\" | \"nextLabel\">;\n}\n\nconst GRID = {\n landscape: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\",\n portrait: \"grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6\",\n} as const;\n\n/** A media grid that never mixes heights badly: a shelf for the 9:16, a uniform grid for the rest. */\nexport function MediaGrid<T>({ items, getId, orientationOf, renderItem, mode = \"split\", frame = \"landscape\", layout = \"grid\", shelf, className, ...props }: MediaGridProps<T>) {\n const row = layout === \"list\";\n const split = mode === \"split\";\n const verticals = split ? items.filter((i) => orientationOf(i) === \"portrait\") : [];\n const rest = split ? items.filter((i) => orientationOf(i) !== \"portrait\") : items;\n const gridFrame: VideoCardAspect = split ? \"landscape\" : frame;\n const state = (item: T, aspect: VideoCardAspect): MediaGridState => ({ aspect, layout: row ? \"row\" : \"stack\", mismatch: orientationOf(item) !== aspect });\n return (\n <div data-media-grid={mode} className={cn(\"flex min-w-0 flex-col gap-8\", className)} {...props}>\n {verticals.length > 0 && <ShortsShelf {...shelf} items={verticals} getId={getId} renderItem={(item) => renderItem(item, { ...state(item, \"portrait\"), layout: \"stack\" })} />}\n {rest.length > 0 && (\n <ul data-grid-frame={gridFrame} className={cn(\"m-0 grid list-none auto-rows-fr gap-4 p-0\", row ? \"grid-cols-1\" : GRID[gridFrame])}>\n {rest.map((item) => (\n <li key={getId(item)} className=\"min-w-0\">\n {renderItem(item, state(item, gridFrame))}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n","// Client (hover / focus swaps the picture for its animated preview). A video in a grid or a shelf: the picture in a 16:9\n// or 9:16 frame (a source of the other shape is shown inside, never stretched), badges, duration, the title on two lines\n// reserved, the creator, a count and one quick action — the same height of body in every card so a row aligns.\nimport * as React from \"react\";\n\nimport { Avatar } from \"../avatar\";\nimport type { DefaultAvatar } from \"../avatar/default-avatar\";\nimport { BlockLink, type LinkComponent } from \"../block-link\";\nimport { Card } from \"../card\";\nimport { cn } from \"../cn\";\n\nexport type VideoCardAspect = \"landscape\" | \"portrait\";\n\nexport interface VideoCardCreator {\n /** The creator's name. */\n name: string;\n /** Their photo. */\n avatarSrc?: string | null;\n /** Their profile. */\n href?: string;\n /** The Krizaka default picture when there is no photo (a brand and a seed). */\n defaultAvatar?: DefaultAvatar;\n}\n\nexport interface VideoCardProps extends Omit<React.ComponentProps<\"article\">, \"title\"> {\n /** The title; it names the card and is its link. */\n title: string;\n /** Where the card opens. */\n href: string;\n /** The app's router link (Next's `Link`). */\n linkAs?: LinkComponent;\n /**\n * The frame: `landscape` (16:9) or `portrait` (9:16).\n * @default \"landscape\"\n */\n aspect?: VideoCardAspect;\n /**\n * `stack` (picture over text) or `row` (picture beside the text: the list mode of a section).\n * @default \"stack\"\n */\n layout?: \"stack\" | \"row\";\n /** The picture. */\n poster?: string | null;\n /** An animated picture (WebP) shown while the card is hovered or focused. */\n previewSrc?: string | null;\n /**\n * `cover` fills the frame; `contain` draws the whole picture over a blurred copy of itself (a source of the other\n * orientation: no cropped face, no black bar).\n * @default \"cover\"\n */\n fit?: \"cover\" | \"contain\";\n /** The length, already formatted (`3:42`). */\n duration?: string;\n /** Badges at the top-left of the picture (Free, a price, 18+). Put them on `scrim` so they read in both themes. */\n badges?: React.ReactNode;\n /** Who made it: name, photo, profile. */\n creator?: VideoCardCreator;\n /** What is counted (`12.4K views`), already formatted. */\n meta?: React.ReactNode;\n /** One quick action at the end of the footer (a tip, a save). Its clicks must not follow the card's link. */\n action?: React.ReactNode;\n /** A sensitive picture: blurred and scaled so nothing shows (put the reveal button in `overlay`). */\n blurred?: boolean;\n /** Over the whole picture, above the card's link (a veil, a reveal button). */\n overlay?: React.ReactNode;\n /** Shown instead of the picture while there is none. */\n fallback?: React.ReactNode;\n /** `eager` for the first cards of a page. */\n loading?: \"lazy\" | \"eager\";\n}\n\nconst FRAME = { landscape: \"aspect-video\", portrait: \"aspect-[9/16]\" } as const;\n\n/** A video card whose frame, body and footer have one height per aspect, so a grid row or a shelf never staggers. */\nexport function VideoCard({\n title,\n href,\n linkAs,\n aspect = \"landscape\",\n layout = \"stack\",\n poster,\n previewSrc,\n fit = \"cover\",\n duration,\n badges,\n creator,\n meta,\n action,\n fallback,\n blurred = false,\n overlay,\n loading = \"lazy\",\n className,\n onMouseEnter,\n onMouseLeave,\n onFocus,\n onBlur,\n ...props\n}: VideoCardProps) {\n const [live, setLive] = React.useState(false);\n const picture = live && previewSrc ? previewSrc : poster;\n const row = layout === \"row\";\n return (\n <article\n data-aspect={aspect}\n data-layout={layout}\n className={cn(\"group relative h-full min-w-0\", className)}\n onMouseEnter={(e) => (setLive(true), onMouseEnter?.(e))}\n onMouseLeave={(e) => (setLive(false), onMouseLeave?.(e))}\n onFocus={(e) => (setLive(true), onFocus?.(e))}\n onBlur={(e) => (setLive(false), onBlur?.(e))}\n {...props}\n >\n <Card.Root interactive tone=\"glass\" className={cn(\"h-full w-full max-w-full\", row ? \"flex-row\" : \"flex-col\")}>\n <Card.Media aspect=\"auto\" className={cn(\"shrink-0\", FRAME[aspect], row && (aspect === \"portrait\" ? \"w-24 sm:w-28\" : \"w-40 sm:w-56\"))}>\n {fit === \"contain\" && picture && <img src={picture} alt=\"\" aria-hidden loading=\"lazy\" className=\"absolute inset-0 h-full w-full scale-125 object-cover opacity-70 blur-2xl\" />}\n <Card.Image src={picture} loading={loading} fallback={fallback} className={cn(fit === \"contain\" && \"relative object-contain\", blurred && \"scale-110 blur-xl\")} />\n {overlay && <div className=\"absolute inset-0 z-20 flex\">{overlay}</div>}\n {badges && (\n <Card.Overlay corner=\"top-left\" className=\"theme-dark flex-wrap\">\n {badges}\n </Card.Overlay>\n )}\n {duration && (\n <Card.Overlay corner=\"bottom-right\">\n <span className=\"rounded-sm bg-scrim px-1.5 py-0.5 font-mono text-[11px] text-fg-on-media\">{duration}</span>\n </Card.Overlay>\n )}\n </Card.Media>\n <Card.Body className=\"min-w-0 flex-1 justify-between gap-2 p-3\">\n <h3 className=\"m-0 min-h-[2.5em] text-sm font-semibold leading-tight text-fg\">\n <BlockLink href={href} linkAs={linkAs} className=\"line-clamp-2 outline-none after:absolute after:inset-0 after:z-0 after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-ring\">\n {title}\n </BlockLink>\n </h3>\n <div className=\"relative z-10 flex h-8 items-center gap-2 text-xs text-fg-secondary\">\n {creator && (\n <>\n <Avatar src={creator.avatarSrc} defaultAvatar={creator.defaultAvatar} alt=\"\" fallback={creator.name.charAt(0)} size=\"xs\" className=\"shrink-0\" />\n {creator.href ? (\n <BlockLink href={creator.href} linkAs={linkAs} className=\"truncate hover:text-fg\">\n {creator.name}\n </BlockLink>\n ) : (\n <span className=\"truncate\">{creator.name}</span>\n )}\n </>\n )}\n {meta && <span className=\"ml-auto shrink-0 font-mono text-[11px] text-fg-muted\">{meta}</span>}\n {action && <span className=\"shrink-0\">{action}</span>}\n </div>\n </Card.Body>\n </Card.Root>\n </article>\n );\n}\n","// Server-safe: no hook, no context. One `tv` definition with slots: each part takes its slot, and a product overrides\n// any part through `className` (merged last by tailwind-merge).\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { cardRecipe } from \"./recipe\";\n\nexport const card = tv(cardRecipe);\n\ntype CardVariants = VariantProps<typeof card>;\n/** The parts without a variant read the default slots: no context, so server-safe. */\nconst slots = card();\n\nexport type CardRootProps = React.ComponentProps<\"div\"> &\n Pick<CardVariants, \"radius\" | \"tone\" | \"interactive\"> & {\n /**\n * md · lg · xl.\n * @default \"xl\"\n */\n radius?: CardVariants[\"radius\"];\n /**\n * default (the base surface) · elevated (raised, a shadow) · glass (translucent, blurred).\n * @default \"default\"\n */\n tone?: CardVariants[\"tone\"];\n /** The spotlight that follows the pointer and the lift on hover: for a card that is a link. */\n interactive?: CardVariants[\"interactive\"];\n /** Renders the child (a link, an article) instead of the <div>, with props, classes and ref merged. */\n asChild?: boolean;\n /** Enters on scroll (MotionObserver): the index in the grid becomes the delay (50 ms a step, 8 steps at most). */\n reveal?: number;\n };\n\nexport function CardRoot({ asChild, reveal, radius, tone, interactive, className, style, ...props }: CardRootProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n const revealStyle =\n reveal === undefined ? style : ({ ...style, \"--kz-delay\": `${Math.min(Math.max(reveal, 0), 8) * 50}ms` } as React.CSSProperties);\n return (\n <Comp\n data-reveal={reveal === undefined ? undefined : \"\"}\n data-interactive={interactive ? \"\" : undefined}\n style={revealStyle}\n className={card({ radius, tone, interactive }).root({ className })}\n {...props}\n />\n );\n}\n\nconst ASPECT = { video: \"aspect-video\", square: \"aspect-square\", portrait: \"aspect-[3/4]\", auto: \"\" } as const;\nexport type CardMediaProps = React.ComponentProps<\"div\"> & {\n /** The ratio of the media: video (16:9) · square · portrait (3:4) · auto (the content's). */\n aspect?: keyof typeof ASPECT;\n};\n\nexport function CardMedia({ aspect = \"video\", className, ...props }: CardMediaProps) {\n return <div data-aspect={aspect} className={slots.media({ className: cn(ASPECT[aspect], className) })} {...props} />;\n}\n\nexport type CardImageProps = Omit<React.ComponentProps<\"img\">, \"src\"> & {\n /** The image's URL; `null` or nothing shows the `fallback`. */\n src?: string | null;\n /** Shown when there is no image: an icon, an illustration (decorative). */\n fallback?: React.ReactNode;\n /**\n * When the browser loads it: `lazy` (default, a card in a list or below the fold) or `eager` for the first cards\n * of a page — the image that is the largest contentful paint must not wait (add `fetchPriority=\"high\"`).\n * @default \"lazy\"\n */\n loading?: \"lazy\" | \"eager\";\n};\n\n/** The image of a media, or its `fallback` when there is none. `alt` is empty by default: the title names the card. */\nexport function CardImage({ src, alt = \"\", fallback, loading = \"lazy\", className, ...props }: CardImageProps) {\n if (!src) {\n return (\n <div data-fallback=\"\" aria-hidden className={slots.fallback({ className })}>\n {fallback}\n </div>\n );\n }\n return <img src={src} alt={alt} loading={loading} decoding=\"async\" className={slots.image({ className })} {...props} />;\n}\n\nconst CORNER = {\n \"top-left\": \"left-2.5 top-2.5\",\n \"top-right\": \"right-2.5 top-2.5\",\n \"bottom-left\": \"bottom-2.5 left-2.5\",\n \"bottom-right\": \"bottom-2.5 right-2.5\",\n} as const;\nexport type CardOverlayProps = React.ComponentProps<\"div\"> & {\n /** The corner of the media it sits in. */\n corner?: keyof typeof CORNER;\n};\n\n/** What sits on the media (badges, a caption). `scrim` and `fg-on-media` are invariant: nothing to do for light. */\nexport function CardOverlay({ corner = \"top-left\", className, ...props }: CardOverlayProps) {\n return <div data-corner={corner} className={slots.overlay({ className: cn(CORNER[corner], className) })} {...props} />;\n}\n\nexport type CardBodyProps = React.ComponentProps<\"div\"> & {\n /**\n * none · sm · md · lg.\n * @default \"md\"\n */\n padding?: CardVariants[\"padding\"];\n};\n\nexport function CardBody({ padding, className, ...props }: CardBodyProps) {\n return <div className={card({ padding }).body({ className })} {...props} />;\n}\n\nexport type CardTitleProps = React.ComponentProps<\"h3\"> & {\n /** The heading level, to fit the page's outline. */\n as?: \"h2\" | \"h3\" | \"h4\";\n};\n\nexport function CardTitle({ as: Tag = \"h3\", className, ...props }: CardTitleProps) {\n return <Tag className={slots.title({ className })} {...props} />;\n}\n\nexport function CardDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return <p className={slots.description({ className })} {...props} />;\n}\n\nexport type CardStatProps = React.ComponentProps<\"div\"> & {\n /** What the figure is, in small capitals above it (the figure is the children). */\n label: React.ReactNode;\n};\n\n/** A figure put forward: the label in capitals, the value in tabular digits. */\nexport function CardStat({ label, className, children, ...props }: CardStatProps) {\n return (\n <div className={slots.stat({ className })} {...props}>\n <p className={slots.statLabel()}>{label}</p>\n <p className={slots.statValue()}>{children}</p>\n </div>\n );\n}\n\nexport function CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={slots.footer({ className })} {...props} />;\n}\n\n/** `Card.Root/Media/Image/Overlay/Body/Title/Description/Stat/Footer`. */\nexport const Card = {\n Root: CardRoot,\n Media: CardMedia,\n Image: CardImage,\n Overlay: CardOverlay,\n Body: CardBody,\n Title: CardTitle,\n Description: CardDescription,\n Stat: CardStat,\n Footer: CardFooter,\n};\n","// The card's parts, radii, tones and paddings, written once as classes of the Krizaka tokens: the web wears them with\n// `tv(cardRecipe)`, React Native reads the same classes into styles (native/card.tsx): a part of the card that exists only\n// on the web (the media's zoom, the spotlight) is simply ignored there.\nimport { defineRecipe } from \"../recipes/define\";\n\nexport const cardRecipe = defineRecipe({\n slots: {\n root:\n \"group relative flex flex-col overflow-hidden border border-border-default bg-surface-1 text-fg transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n media: \"relative overflow-hidden bg-media\",\n image: \"h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-105\",\n fallback: \"flex h-full w-full items-center justify-center bg-linear-to-br from-accent/15 via-media to-accent-2/15 text-accent/60\",\n overlay: \"absolute z-10 flex items-center gap-1.5\",\n body: \"relative flex flex-1 flex-col gap-3\",\n title: \"line-clamp-1 text-sm font-semibold text-fg transition-colors group-hover:text-fg-accent\",\n description: \"line-clamp-2 text-xs text-fg-secondary\",\n stat: \"min-w-0\",\n statLabel: \"text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n statValue: \"font-display text-xl font-black tabular-nums text-fg\",\n footer: \"relative mt-auto flex items-center gap-2 border-t border-border-subtle pt-3 text-xs text-fg-secondary\",\n },\n variants: {\n radius: { md: { root: \"rounded-md\" }, lg: { root: \"rounded-lg\" }, xl: { root: \"rounded-xl\" } },\n tone: { default: {}, elevated: { root: \"bg-surface-2 shadow-md\" }, glass: { root: \"bg-surface-1/70 backdrop-blur-md\" } },\n interactive: { true: { root: \"kz-spotlight kz-lift hover:border-accent/50\" } },\n padding: { none: { body: \"p-0\" }, sm: { body: \"p-3\" }, md: { body: \"p-4\" }, lg: { body: \"p-6\" } },\n },\n defaultVariants: { radius: \"xl\", tone: \"default\", padding: \"md\" },\n});\n"],"mappings":"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","names":["jsx","jsx","jsxs","React","jsx","jsxs","jsx","jsxs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.17.0",
3
+ "version": "3.18.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",
@@ -557,6 +557,10 @@
557
557
  "types": "./dist/verified-badge.d.ts",
558
558
  "import": "./dist/verified-badge.js"
559
559
  },
560
+ "./media-grid": {
561
+ "types": "./dist/media-grid.d.ts",
562
+ "import": "./dist/media-grid.js"
563
+ },
560
564
  "./photo-grid": {
561
565
  "types": "./dist/photo-grid.d.ts",
562
566
  "import": "./dist/photo-grid.js"
@@ -625,7 +629,7 @@
625
629
  ],
626
630
  "peerDependencies": {
627
631
  "@krizaka/query": ">=0.2.1",
628
- "@krizaka/tailwind": ">=3.17.0",
632
+ "@krizaka/tailwind": ">=3.18.0",
629
633
  "@maplibre/maplibre-react-native": ">=11",
630
634
  "@shopify/flash-list": ">=2",
631
635
  "expo-camera": ">=16",
@@ -685,8 +689,8 @@
685
689
  "typescript": "^5.9.0",
686
690
  "vitest": "^4.1.11",
687
691
  "zod": "^4.6.5",
688
- "@krizaka/config": "0.4.1",
689
- "@krizaka/query": "0.2.1"
692
+ "@krizaka/query": "0.2.1",
693
+ "@krizaka/config": "0.4.1"
690
694
  },
691
695
  "engines": {
692
696
  "node": ">=22"
@@ -2038,6 +2042,21 @@
2038
2042
  "@krizaka/ui"
2039
2043
  ]
2040
2044
  },
2045
+ {
2046
+ "name": "./media-grid",
2047
+ "path": "dist/media-grid.js",
2048
+ "limit": "5 kB",
2049
+ "gzip": true,
2050
+ "ignore": [
2051
+ "tailwind-variants",
2052
+ "tailwind-merge",
2053
+ "react",
2054
+ "react-dom",
2055
+ "radix-ui",
2056
+ "clsx",
2057
+ "@krizaka/ui"
2058
+ ]
2059
+ },
2041
2060
  {
2042
2061
  "name": "./photo-grid",
2043
2062
  "path": "dist/photo-grid.js",
@@ -2123,9 +2142,9 @@
2123
2142
  "radix-ui": "^1.7.0",
2124
2143
  "sonner": "^2.0.8",
2125
2144
  "tailwind-variants": "^3.3.1",
2126
- "@krizaka/pacer": "^0.1.0",
2127
2145
  "@krizaka/intl": "^0.2.1",
2128
- "@krizaka/tokens": "^3.17.0"
2146
+ "@krizaka/tokens": "^3.18.0",
2147
+ "@krizaka/pacer": "^0.1.0"
2129
2148
  },
2130
2149
  "scripts": {
2131
2150
  "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",
@@ -194,7 +194,7 @@
194
194
  },
195
195
  {
196
196
  "path": "meta.ts",
197
- "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"document-viewer\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
197
+ "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"document-viewer\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
198
198
  },
199
199
  {
200
200
  "path": "story-variant.ts",