@krizaka/ui 3.37.0 → 3.39.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/blocks.css +36 -36
- package/dist/chip-link.js +1 -1
- package/dist/chip-row.js +1 -1
- package/dist/{chunk-IANSFIVC.js → chunk-7JFHTPI3.js} +13 -3
- package/dist/chunk-7JFHTPI3.js.map +1 -0
- package/dist/{chunk-RE5WJ4QP.js → chunk-ADRUEPYC.js} +4 -4
- package/dist/chunk-ADRUEPYC.js.map +1 -0
- package/dist/{chunk-BTTDIVVJ.js → chunk-F2UZ2WWL.js} +2 -2
- package/dist/chunk-F2UZ2WWL.js.map +1 -0
- package/dist/image-cropper.js +1 -1
- package/dist/kanban.js +1 -1
- package/dist/kanban.js.map +1 -1
- package/dist/landing-page.js +1 -6
- package/dist/landing-page.js.map +1 -1
- package/dist/map.js +2 -2
- package/dist/map.js.map +1 -1
- package/dist/motion.css +3 -3
- package/dist/native/vertical-video.js +1 -1
- package/dist/native/vertical-video.js.map +1 -1
- package/dist/native.d.ts +1 -1
- package/dist/native.js +11 -11
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.d.ts +1 -1
- package/dist/nav-bar.js +1 -1
- package/dist/photo-grid.js +1 -1
- package/dist/photo-grid.js.map +1 -1
- package/dist/product-page.js +1 -6
- package/dist/product-page.js.map +1 -1
- package/dist/site-footer.d.ts +5 -17
- package/dist/site-footer.js +33 -29
- package/dist/site-footer.js.map +1 -1
- package/dist/{sites-B4sYlfPQ.d.ts → sites-inztN_Bv.d.ts} +8 -0
- package/dist/sites.d.ts +11 -1
- package/dist/sites.js +12 -2
- package/dist/sites.js.map +1 -1
- package/dist/slider.js +3 -3
- package/dist/slider.js.map +1 -1
- package/dist/step-showcase.js +1 -1
- package/dist/step-showcase.js.map +1 -1
- package/dist/story.css +3 -3
- package/dist/video-player.js +1 -1
- package/dist/video-trimmer.js +1 -1
- package/dist/video-trimmer.js.map +1 -1
- package/package.json +3 -3
- package/registry/chip-link.json +1 -1
- package/registry/examples/site-footer/company.js +0 -4
- package/registry/examples/site-footer/company.tsx +3 -7
- package/registry/index.json +7 -7
- package/registry/kanban.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/map.json +2 -2
- package/registry/nav-bar.json +1 -1
- package/registry/photo-grid.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/rail-nav.json +1 -1
- package/registry/site-footer.json +10 -10
- package/registry/sites.json +2 -2
- package/registry/slider.json +1 -1
- package/registry/step-showcase.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/dist/chunk-BTTDIVVJ.js.map +0 -1
- package/dist/chunk-IANSFIVC.js.map +0 -1
- package/dist/chunk-RE5WJ4QP.js.map +0 -1
package/dist/slider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/slider/slider.tsx"],"sourcesContent":["// Client. Radix Slider: role=\"slider\" on each thumb, arrows / Page Up / Page Down / Home / End, a range with two\n// thumbs, horizontal or vertical. The words arrive as props: `label` names the thumb(s), `formatValue` gives the\n// aria-valuetext and the value shown (above the track, or floating over the thumb while it is touched). `ticks` draws the\n// graduations of the steps (and `marks` their words under the track); `tone` fills with the brand's gradient or its\n// accent alone.\nimport { Slider as SliderPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { tv } from \"../tv\";\n\nexport const slider = tv({\n slots: {\n root: \"flex flex-col gap-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:items-center\",\n header: \"flex items-baseline justify-between gap-3 text-xs font-semibold text-fg-secondary\",\n value: \"font-mono text-[11px] tabular-nums text-fg-secondary\",\n control:\n \"relative flex touch-none select-none items-center data-disabled:cursor-not-allowed data-disabled:opacity-40 \" +\n \"data-[orientation=horizontal]:h-6 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-6 data-[orientation=vertical]:flex-col\",\n track:\n \"relative grow overflow-hidden rounded-full bg-surface-3 inset-shadow-well \" +\n \"data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\",\n range: \"absolute rounded-full data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n thumb:\n \"group/thumb relative block h-5 w-5 cursor-grab rounded-full border-2 border-accent bg-fg-on-media shadow-raised transition-[box-shadow,scale] duration-150 \" +\n \"hover:ring-4 hover:ring-accent/15 focus-visible:outline-hidden focus-visible:focus-ring active:scale-110 active:cursor-grabbing motion-reduce:transition-none motion-reduce:active:scale-100\",\n // The value over the thumb: shown while the thumb is hovered, focused or held.\n bubble:\n \"pointer-events-none absolute bottom-full left-1/2 mb-2.5 -translate-x-1/2 whitespace-nowrap rounded-lg bg-fg px-2 py-1 font-mono text-[11px] font-semibold tabular-nums text-surface-0 opacity-0 shadow-floating \" +\n \"transition-opacity duration-150 after:absolute after:left-1/2 after:top-full after:-translate-x-1/2 after:border-4 after:border-transparent after:border-t-fg \" +\n \"group-hover/thumb:opacity-100 group-focus-visible/thumb:opacity-100 group-active/thumb:opacity-100 data-[always]:opacity-100 motion-reduce:transition-none\",\n bubbleVertical: \"bottom-auto left-full top-1/2 mb-0 ml-3 -translate-x-0 -translate-y-1/2 after:hidden\",\n ticks: \"pointer-events-none absolute inset-0\",\n tick: \"absolute rounded-full bg-border-strong data-[on]:bg-on-accent/70 data-[orientation=horizontal]:top-1/2 data-[orientation=horizontal]:h-1 data-[orientation=horizontal]:w-px data-[orientation=horizontal]:-translate-x-1/2 data-[orientation=horizontal]:-translate-y-1/2 data-[orientation=vertical]:left-1/2 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-1 data-[orientation=vertical]:-translate-x-1/2 data-[orientation=vertical]:translate-y-1/2\",\n marks: \"relative h-4 w-full text-[11px] font-medium tabular-nums text-fg-secondary\",\n mark: \"absolute top-0 -translate-x-1/2 whitespace-nowrap first:translate-x-0 last:-translate-x-full\",\n },\n variants: {\n tone: {\n /** The brand's signature: accent → accent-2, a glow under it. */\n gradient: { range: \"bg-linear-to-r from-accent to-accent-2 shadow-glow data-[orientation=vertical]:bg-linear-to-t\" },\n /** The accent alone. */\n solid: { range: \"bg-accent\" },\n },\n size: {\n md: {},\n lg: {\n track: \"data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5\",\n thumb: \"h-6 w-6\",\n control: \"data-[orientation=horizontal]:h-8 data-[orientation=vertical]:w-8\",\n },\n },\n },\n defaultVariants: { tone: \"gradient\", size: \"md\" },\n});\n\nexport type SliderValue = number | [number, number];\n\nexport type SliderMark = {\n /** Where the mark stands. */\n value: number;\n /** Its words under the track (\"0\", \"50 %\", \"Max\"), passed translated. */\n label: string;\n};\n\nexport type SliderProps<V extends SliderValue = number> = Omit<\n React.ComponentProps<typeof SliderPrimitive.Root>,\n \"value\" | \"defaultValue\" | \"onValueChange\" | \"onValueCommit\" | \"children\"\n> & {\n /** One number (a thumb) or two (a range). Controlled with `onValueChange`. */\n value?: V;\n /** The value at first, uncontrolled: one number, or two for a range. */\n defaultValue?: V;\n /** Called on every move, with one number or two. */\n onValueChange?: (value: V) => void;\n /** Called once the drag or the key press ends. */\n onValueCommit?: (value: V) => void;\n /** The accessible name — of the thumb, or of both thumbs of a range — passed translated. */\n label: string;\n /** For a range: each thumb's own name ([\"Minimum\", \"Maximum\"]), passed translated. */\n thumbLabels?: [string, string];\n /** The value as words (aria-valuetext) and as shown: \"1.5×\", \"−12 %\", \"3 min\". */\n formatValue?: (value: number) => string;\n /** Shows the label and the value above the track. Without it, `label` only names the thumb. */\n showLabel?: boolean;\n /** Shows the value floating over the thumb while it is hovered, focused or held (`\"always\"`: at all times). */\n floatingValue?: boolean | \"always\";\n /** Fills from this value instead of the minimum (a ±range from 0: brightness, balance). One horizontal thumb only. */\n origin?: number;\n /** Draws a graduation at every step (`true`, up to 40 of them) or at the values given. */\n ticks?: boolean | readonly number[];\n /** Words under the track at some values (`[{ value: 0, label: \"0\" }, { value: 100, label: \"100\" }]`). Horizontal only. */\n marks?: readonly SliderMark[];\n /**\n * `gradient` (the brand's accent → accent-2, a glow) or `solid` (the accent alone).\n * @default \"gradient\"\n */\n tone?: \"gradient\" | \"solid\";\n /**\n * md · lg (a thicker track and thumb: a hero control, a touch screen).\n * @default \"md\"\n */\n size?: \"md\" | \"lg\";\n};\n\nconst toArray = (v: SliderValue | undefined) => (v === undefined ? undefined : typeof v === \"number\" ? [v] : v);\n\n/** A slider on one value or a range: `value`/`defaultValue` + `onValueChange`, `min`, `max`, `step`, `orientation`. */\nexport function Slider<V extends SliderValue = number>({\n value,\n defaultValue,\n onValueChange,\n onValueCommit,\n label,\n thumbLabels,\n formatValue = String,\n showLabel,\n floatingValue,\n origin,\n ticks,\n marks,\n tone,\n size,\n min = 0,\n max = 100,\n step = 1,\n orientation = \"horizontal\",\n className,\n ...props\n}: SliderProps<V>) {\n const s = slider({ tone, size });\n const range = Array.isArray(value ?? defaultValue);\n const vertical = orientation === \"vertical\";\n const [inner, setInner] = React.useState(() => toArray(defaultValue) ?? [min]);\n const current = toArray(value) ?? inner;\n const out = (next: number[]) => (range ? next : next[0]) as V;\n const pct = (v: number) => ((v - min) / (max - min || 1)) * 100;\n const fill =\n origin !== undefined && !range && !vertical\n ? { left: `${Math.min(pct(origin), pct(current[0]!))}%`, right: `${100 - Math.max(pct(origin), pct(current[0]!))}%` }\n : undefined;\n const lo = range ? Math.min(...current) : (origin ?? min);\n const hi = range ? Math.max(...current) : current[0]!;\n const tickValues = ticks === true ? (max - min) / step <= 40 ? Array.from({ length: Math.round((max - min) / step) + 1 }, (_, i) => min + i * step) : [] : ticks ? [...ticks] : [];\n const id = React.useId();\n return (\n <div className={s.root({ className })} data-range={range ? \"\" : undefined} data-orientation={orientation}>\n {showLabel && (\n <div className={s.header()}>\n <span id={`${id}-label`}>{label}</span>\n <span className={s.value()} aria-hidden>\n {current.map(formatValue).join(\" – \")}\n </span>\n </div>\n )}\n <SliderPrimitive.Root\n min={min}\n max={max}\n step={step}\n orientation={orientation}\n value={current}\n onValueChange={(next) => {\n if (value === undefined) setInner(next);\n onValueChange?.(out(next));\n }}\n onValueCommit={onValueCommit && ((next) => onValueCommit(out(next)))}\n className={s.control()}\n {...props}\n >\n <SliderPrimitive.Track className={s.track()}>\n {fill ? <span data-origin=\"\" data-orientation={orientation} className={s.range()} style={fill} /> : <SliderPrimitive.Range className={s.range()} />}\n {tickValues.length ? (\n <span aria-hidden className={s.ticks()}>\n {tickValues.map((t) => (\n <span\n key={t}\n data-orientation={orientation}\n data-on={t > lo && t < hi ? \"\" : undefined}\n className={s.tick()}\n style={vertical ? { bottom: `${pct(t)}%` } : { left: `${pct(t)}%` }}\n />\n ))}\n </span>\n ) : null}\n </SliderPrimitive.Track>\n {current.map((v, i) => (\n <SliderPrimitive.Thumb\n key={i}\n aria-label={range ? (thumbLabels?.[i] ?? label) : showLabel ? undefined : label}\n aria-labelledby={!range && showLabel ? `${id}-label` : undefined}\n aria-valuetext={formatValue(v)}\n className={s.thumb()}\n >\n {floatingValue ? (\n <span aria-hidden data-always={floatingValue === \"always\" ? \"\" : undefined} className={s.bubble({ className: vertical ? s.bubbleVertical() : undefined })}>\n {formatValue(v)}\n </span>\n ) : null}\n </SliderPrimitive.Thumb>\n ))}\n </SliderPrimitive.Root>\n {marks?.length && !vertical ? (\n <div aria-hidden className={s.marks()}>\n {marks.map((m) => (\n <span key={m.value} className={s.mark()} style={{ left: `${pct(m.value)}%` }}>\n {m.label}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/slider/slider.tsx"],"sourcesContent":["// Client. Radix Slider: role=\"slider\" on each thumb, arrows / Page Up / Page Down / Home / End, a range with two\n// thumbs, horizontal or vertical. The words arrive as props: `label` names the thumb(s), `formatValue` gives the\n// aria-valuetext and the value shown (above the track, or floating over the thumb while it is touched). `ticks` draws the\n// graduations of the steps (and `marks` their words under the track); `tone` fills with the brand's gradient or its\n// accent alone.\nimport { Slider as SliderPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { tv } from \"../tv\";\n\nexport const slider = tv({\n slots: {\n root: \"flex flex-col gap-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:items-center\",\n header: \"flex items-baseline justify-between gap-3 text-xs font-semibold text-fg-secondary\",\n value: \"font-mono text-[11px] tabular-nums text-fg-secondary\",\n control:\n \"relative flex touch-none select-none items-center data-disabled:cursor-not-allowed data-disabled:opacity-40 \" +\n \"data-[orientation=horizontal]:h-6 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-6 data-[orientation=vertical]:flex-col\",\n track:\n \"relative grow overflow-hidden rounded-sm bg-surface-3 inset-shadow-well \" +\n \"data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\",\n range: \"absolute rounded-sm data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n thumb:\n \"group/thumb relative block h-5 w-5 cursor-grab rounded-full border-2 border-accent bg-fg-on-media shadow-raised transition-[box-shadow,scale] duration-150 \" +\n \"hover:ring-4 hover:ring-accent/15 focus-visible:outline-hidden focus-visible:focus-ring active:scale-110 active:cursor-grabbing motion-reduce:transition-none motion-reduce:active:scale-100\",\n // The value over the thumb: shown while the thumb is hovered, focused or held.\n bubble:\n \"pointer-events-none absolute bottom-full left-1/2 mb-2.5 -translate-x-1/2 whitespace-nowrap rounded-lg bg-fg px-2 py-1 font-mono text-[11px] font-semibold tabular-nums text-surface-0 opacity-0 shadow-floating \" +\n \"transition-opacity duration-150 after:absolute after:left-1/2 after:top-full after:-translate-x-1/2 after:border-4 after:border-transparent after:border-t-fg \" +\n \"group-hover/thumb:opacity-100 group-focus-visible/thumb:opacity-100 group-active/thumb:opacity-100 data-[always]:opacity-100 motion-reduce:transition-none\",\n bubbleVertical: \"bottom-auto left-full top-1/2 mb-0 ml-3 -translate-x-0 -translate-y-1/2 after:hidden\",\n ticks: \"pointer-events-none absolute inset-0\",\n tick: \"absolute rounded-sm bg-border-strong data-[on]:bg-on-accent/70 data-[orientation=horizontal]:top-1/2 data-[orientation=horizontal]:h-1 data-[orientation=horizontal]:w-px data-[orientation=horizontal]:-translate-x-1/2 data-[orientation=horizontal]:-translate-y-1/2 data-[orientation=vertical]:left-1/2 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-1 data-[orientation=vertical]:-translate-x-1/2 data-[orientation=vertical]:translate-y-1/2\",\n marks: \"relative h-4 w-full text-[11px] font-medium tabular-nums text-fg-secondary\",\n mark: \"absolute top-0 -translate-x-1/2 whitespace-nowrap first:translate-x-0 last:-translate-x-full\",\n },\n variants: {\n tone: {\n /** The brand's signature: accent → accent-2, a glow under it. */\n gradient: { range: \"bg-linear-to-r from-accent to-accent-2 shadow-glow data-[orientation=vertical]:bg-linear-to-t\" },\n /** The accent alone. */\n solid: { range: \"bg-accent\" },\n },\n size: {\n md: {},\n lg: {\n track: \"data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5\",\n thumb: \"h-6 w-6\",\n control: \"data-[orientation=horizontal]:h-8 data-[orientation=vertical]:w-8\",\n },\n },\n },\n defaultVariants: { tone: \"gradient\", size: \"md\" },\n});\n\nexport type SliderValue = number | [number, number];\n\nexport type SliderMark = {\n /** Where the mark stands. */\n value: number;\n /** Its words under the track (\"0\", \"50 %\", \"Max\"), passed translated. */\n label: string;\n};\n\nexport type SliderProps<V extends SliderValue = number> = Omit<\n React.ComponentProps<typeof SliderPrimitive.Root>,\n \"value\" | \"defaultValue\" | \"onValueChange\" | \"onValueCommit\" | \"children\"\n> & {\n /** One number (a thumb) or two (a range). Controlled with `onValueChange`. */\n value?: V;\n /** The value at first, uncontrolled: one number, or two for a range. */\n defaultValue?: V;\n /** Called on every move, with one number or two. */\n onValueChange?: (value: V) => void;\n /** Called once the drag or the key press ends. */\n onValueCommit?: (value: V) => void;\n /** The accessible name — of the thumb, or of both thumbs of a range — passed translated. */\n label: string;\n /** For a range: each thumb's own name ([\"Minimum\", \"Maximum\"]), passed translated. */\n thumbLabels?: [string, string];\n /** The value as words (aria-valuetext) and as shown: \"1.5×\", \"−12 %\", \"3 min\". */\n formatValue?: (value: number) => string;\n /** Shows the label and the value above the track. Without it, `label` only names the thumb. */\n showLabel?: boolean;\n /** Shows the value floating over the thumb while it is hovered, focused or held (`\"always\"`: at all times). */\n floatingValue?: boolean | \"always\";\n /** Fills from this value instead of the minimum (a ±range from 0: brightness, balance). One horizontal thumb only. */\n origin?: number;\n /** Draws a graduation at every step (`true`, up to 40 of them) or at the values given. */\n ticks?: boolean | readonly number[];\n /** Words under the track at some values (`[{ value: 0, label: \"0\" }, { value: 100, label: \"100\" }]`). Horizontal only. */\n marks?: readonly SliderMark[];\n /**\n * `gradient` (the brand's accent → accent-2, a glow) or `solid` (the accent alone).\n * @default \"gradient\"\n */\n tone?: \"gradient\" | \"solid\";\n /**\n * md · lg (a thicker track and thumb: a hero control, a touch screen).\n * @default \"md\"\n */\n size?: \"md\" | \"lg\";\n};\n\nconst toArray = (v: SliderValue | undefined) => (v === undefined ? undefined : typeof v === \"number\" ? [v] : v);\n\n/** A slider on one value or a range: `value`/`defaultValue` + `onValueChange`, `min`, `max`, `step`, `orientation`. */\nexport function Slider<V extends SliderValue = number>({\n value,\n defaultValue,\n onValueChange,\n onValueCommit,\n label,\n thumbLabels,\n formatValue = String,\n showLabel,\n floatingValue,\n origin,\n ticks,\n marks,\n tone,\n size,\n min = 0,\n max = 100,\n step = 1,\n orientation = \"horizontal\",\n className,\n ...props\n}: SliderProps<V>) {\n const s = slider({ tone, size });\n const range = Array.isArray(value ?? defaultValue);\n const vertical = orientation === \"vertical\";\n const [inner, setInner] = React.useState(() => toArray(defaultValue) ?? [min]);\n const current = toArray(value) ?? inner;\n const out = (next: number[]) => (range ? next : next[0]) as V;\n const pct = (v: number) => ((v - min) / (max - min || 1)) * 100;\n const fill =\n origin !== undefined && !range && !vertical\n ? { left: `${Math.min(pct(origin), pct(current[0]!))}%`, right: `${100 - Math.max(pct(origin), pct(current[0]!))}%` }\n : undefined;\n const lo = range ? Math.min(...current) : (origin ?? min);\n const hi = range ? Math.max(...current) : current[0]!;\n const tickValues = ticks === true ? (max - min) / step <= 40 ? Array.from({ length: Math.round((max - min) / step) + 1 }, (_, i) => min + i * step) : [] : ticks ? [...ticks] : [];\n const id = React.useId();\n return (\n <div className={s.root({ className })} data-range={range ? \"\" : undefined} data-orientation={orientation}>\n {showLabel && (\n <div className={s.header()}>\n <span id={`${id}-label`}>{label}</span>\n <span className={s.value()} aria-hidden>\n {current.map(formatValue).join(\" – \")}\n </span>\n </div>\n )}\n <SliderPrimitive.Root\n min={min}\n max={max}\n step={step}\n orientation={orientation}\n value={current}\n onValueChange={(next) => {\n if (value === undefined) setInner(next);\n onValueChange?.(out(next));\n }}\n onValueCommit={onValueCommit && ((next) => onValueCommit(out(next)))}\n className={s.control()}\n {...props}\n >\n <SliderPrimitive.Track className={s.track()}>\n {fill ? <span data-origin=\"\" data-orientation={orientation} className={s.range()} style={fill} /> : <SliderPrimitive.Range className={s.range()} />}\n {tickValues.length ? (\n <span aria-hidden className={s.ticks()}>\n {tickValues.map((t) => (\n <span\n key={t}\n data-orientation={orientation}\n data-on={t > lo && t < hi ? \"\" : undefined}\n className={s.tick()}\n style={vertical ? { bottom: `${pct(t)}%` } : { left: `${pct(t)}%` }}\n />\n ))}\n </span>\n ) : null}\n </SliderPrimitive.Track>\n {current.map((v, i) => (\n <SliderPrimitive.Thumb\n key={i}\n aria-label={range ? (thumbLabels?.[i] ?? label) : showLabel ? undefined : label}\n aria-labelledby={!range && showLabel ? `${id}-label` : undefined}\n aria-valuetext={formatValue(v)}\n className={s.thumb()}\n >\n {floatingValue ? (\n <span aria-hidden data-always={floatingValue === \"always\" ? \"\" : undefined} className={s.bubble({ className: vertical ? s.bubbleVertical() : undefined })}>\n {formatValue(v)}\n </span>\n ) : null}\n </SliderPrimitive.Thumb>\n ))}\n </SliderPrimitive.Root>\n {marks?.length && !vertical ? (\n <div aria-hidden className={s.marks()}>\n {marks.map((m) => (\n <span key={m.value} className={s.mark()} style={{ left: `${pct(m.value)}%` }}>\n {m.label}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/step-showcase.js
CHANGED
|
@@ -162,7 +162,7 @@ function StepShowcase({ label, steps, device, deviceTitle, autoplay = true, labe
|
|
|
162
162
|
" ",
|
|
163
163
|
/* @__PURE__ */ jsx2("span", { className: "text-fg-secondary", children: current?.description })
|
|
164
164
|
] }),
|
|
165
|
-
auto && /* @__PURE__ */ jsxs2("button", { type: "button", onClick: () => setPaused((p) => !p), "aria-pressed": paused, className: "mx-auto inline-flex h-8 items-center gap-2 rounded-
|
|
165
|
+
auto && /* @__PURE__ */ jsxs2("button", { type: "button", onClick: () => setPaused((p) => !p), "aria-pressed": paused, className: "mx-auto inline-flex h-8 items-center gap-2 rounded-sm border border-border-subtle px-3 text-xs font-medium text-fg-secondary hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring md:mx-0 md:w-fit", children: [
|
|
166
166
|
/* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", className: "h-3.5 w-3.5", fill: "currentColor", "aria-hidden": true, children: paused ? /* @__PURE__ */ jsx2("path", { d: "M8 5v14l11-7z" }) : /* @__PURE__ */ jsx2("path", { d: "M7 5h4v14H7zM13 5h4v14h-4z" }) }),
|
|
167
167
|
paused ? labels.play : labels.pause
|
|
168
168
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/step-showcase/device-frame.tsx","../src/step-showcase/step-showcase.tsx"],"sourcesContent":["// Server-safe. The frame a product's recording plays in: a phone, a browser window or a macOS window — drawn with the\n// tokens (no image), so it follows dark and light. The content fills the screen area (`object-cover` media).\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\n\nexport type DeviceKind = \"phone\" | \"browser\" | \"desktop\";\n\nexport interface DeviceFrameProps extends React.ComponentProps<\"div\"> {\n /** The device: a phone, a browser window or a macOS window. */\n kind: DeviceKind;\n /** The browser's address or the window's title (“dev.orazaka.com”, “Orazaka”). */\n title?: string;\n}\n\nconst screen: Record<DeviceKind, string> = {\n phone: \"aspect-[9/19.5]\",\n browser: \"aspect-[16/10]\",\n desktop: \"aspect-[16/10]\",\n};\n\n/** A phone, a browser window or a macOS window around a screen of content. */\nexport function DeviceFrame({ kind, title, className, children, ...props }: DeviceFrameProps) {\n if (kind === \"phone\") {\n return (\n <div data-device=\"phone\" className={cn(\"relative mx-auto w-full max-w-[18rem] rounded-[2.4rem] border border-border-strong bg-surface-3 p-2 shadow-2xl\", className)} {...props}>\n <div className={cn(\"relative overflow-hidden rounded-[1.9rem] bg-surface-0\", screen.phone)}>\n {children}\n <span aria-hidden className=\"absolute left-1/2 top-2 h-5 w-20 -translate-x-1/2 rounded-full bg-surface-3\" />\n </div>\n </div>\n );\n }\n return (\n <div data-device={kind} className={cn(\"relative w-full overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-2xl\", className)} {...props}>\n <div className=\"flex h-8 items-center gap-1.5 border-b border-border-subtle bg-surface-2 px-3\">\n <span aria-hidden className=\"h-2.5 w-2.5 rounded-full bg-danger/80\" />\n <span aria-hidden className=\"h-2.5 w-2.5 rounded-full bg-warning/80\" />\n <span aria-hidden className=\"h-2.5 w-2.5 rounded-full bg-success/80\" />\n {title &&\n (kind === \"browser\" ? (\n <span className=\"mx-auto max-w-[60%] truncate rounded-md bg-surface-1 px-3 py-0.5 font-mono text-[11px] text-fg-secondary\">{title}</span>\n ) : (\n <span className=\"mx-auto truncate text-xs font-semibold text-fg-secondary\">{title}</span>\n ))}\n </div>\n <div className={cn(\"relative overflow-hidden bg-surface-0\", screen[kind])}>{children}</div>\n </div>\n );\n}\n","// Client (timers through the video, observers, swipe). A guided sequence in one device frame: the steps listed beside\n// it (a vertical tab list: number · title · one line, a bar filling under the current one), the frame playing the\n// step's recording muted, then moving on to the next; the recordings crossfade. It plays only while in view, holds on\n// hover and focus, keeps a visible play / pause button, and never shows a video's native controls. Recordings load\n// lazily (posters first, the current step's video once the block has been seen). Reduced motion: posters, manual steps.\n// Below `md` the list becomes dots under the frame, the frame swipes, the current step's words show under it.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { DeviceFrame, type DeviceKind } from \"./device-frame\";\n\nconst FILL_CSS = \".kz-step-fill{transform-origin:left;animation:kz-step-fill var(--kz-step-ms,6000ms) linear forwards}@keyframes kz-step-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\";\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst noop = () => () => undefined;\nconst readReduced = () => Boolean(typeof window !== \"undefined\" && window.matchMedia?.(REDUCED).matches);\nconst subscribeReduced = (notify: () => void) => {\n const q = window.matchMedia?.(REDUCED);\n q?.addEventListener?.(\"change\", notify);\n return () => q?.removeEventListener?.(\"change\", notify);\n};\n\nexport interface ShowcaseStep {\n /** A stable key. */\n id: string;\n /** The step's name (“Make an image”), passed translated. */\n title: string;\n /** One line under it. */\n description: string;\n /** The still shown first, and the only picture under reduced motion. */\n poster: string;\n /** What the poster shows, for assistive technology. */\n alt: string;\n /** The recording: WebM and / or MP4. Without one, the poster stays `duration` ms. */\n video?: { webm?: string; mp4?: string };\n /** Milliseconds a step without a video stays. @default 6000 */\n duration?: number;\n}\n\nexport interface StepShowcaseLabels {\n /** The button's name while paused. */\n play: string;\n /** The button's name while it plays. */\n pause: string;\n /** A step's position in words: `(2, 4) => \"Step 2 of 4\"`. */\n step: (n: number, total: number) => string;\n}\n\nexport const stepShowcaseLabels: StepShowcaseLabels = {\n play: \"Play the steps\",\n pause: \"Pause the steps\",\n step: (n, total) => `Step ${n} of ${total}`,\n};\n\nexport interface StepShowcaseProps extends Omit<React.ComponentProps<\"section\">, \"children\"> {\n /** The sequence's accessible name (“Orazaka in your pocket”), passed translated. */\n label: string;\n /** The steps, in the order of the journey. */\n steps: readonly ShowcaseStep[];\n /** The device the recordings were made on. */\n device: DeviceKind;\n /** The browser's address or the window's title. */\n deviceTitle?: string;\n /** Moves on by itself (never under reduced motion). @default true */\n autoplay?: boolean;\n /** The controls' words, translated. */\n labels?: StepShowcaseLabels;\n /** The step shown first. @default 0 */\n defaultIndex?: number;\n /** Called when the step changes — `user` is true when the visitor asked for it. */\n onIndexChange?: (index: number, user: boolean) => void;\n}\n\n/** One device frame, its steps beside it, playing each recording then the next — a product's guided tour. */\nexport function StepShowcase({ label, steps, device, deviceTitle, autoplay = true, labels = stepShowcaseLabels, defaultIndex = 0, onIndexChange, className, ...props }: StepShowcaseProps) {\n const total = steps.length;\n const id = React.useId();\n const root = React.useRef<HTMLElement>(null);\n const video = React.useRef<HTMLVideoElement>(null);\n const bar = React.useRef<HTMLSpanElement>(null);\n const swipe = React.useRef<number | null>(null);\n const [index, setIndex] = React.useState(defaultIndex);\n const [paused, setPaused] = React.useState(false);\n const [hold, setHold] = React.useState({ hover: false, focus: false });\n const [inView, setInView] = React.useState(false);\n const [seen, setSeen] = React.useState(false);\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n const mounted = React.useSyncExternalStore(noop, () => true, () => false);\n const current = steps[Math.min(index, total - 1)];\n const auto = autoplay && !reduced && mounted;\n const running = auto && !paused && !hold.hover && !hold.focus && inView;\n\n React.useEffect(() => {\n const host = root.current;\n if (!host || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver((e) => {\n const visible = e.some((x) => x.isIntersecting);\n setInView(visible);\n if (visible) setSeen(true);\n }, { threshold: 0.35 });\n io.observe(host);\n return () => io.disconnect();\n }, []);\n\n const go = React.useCallback(\n (to: number, user: boolean) => {\n const next = (to + total) % total;\n setIndex(next);\n if (bar.current) bar.current.style.transform = \"scaleX(0)\";\n onIndexChange?.(next, user);\n },\n [total, onIndexChange],\n );\n\n // The recording runs only while the sequence does; its time is the bar.\n React.useEffect(() => {\n const v = video.current;\n if (!v) return;\n if (running) v.play()?.catch?.(() => undefined);\n else v.pause();\n }, [running, index, seen]);\n\n const playable = Boolean(current?.video) && seen && !reduced;\n const onTime = (e: React.SyntheticEvent<HTMLVideoElement>) => {\n const v = e.currentTarget;\n if (bar.current && v.duration) bar.current.style.transform = `scaleX(${v.currentTime / v.duration})`;\n };\n\n return (\n <section\n ref={root}\n aria-roledescription=\"carousel\"\n aria-label={label}\n className={cn(\"grid items-center gap-6 md:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] md:gap-10\", className)}\n onPointerEnter={() => setHold((h) => ({ ...h, hover: true }))}\n onPointerLeave={() => setHold((h) => ({ ...h, hover: false }))}\n onFocus={() => setHold((h) => ({ ...h, focus: true }))}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setHold((h) => ({ ...h, focus: false }));\n }}\n {...props}\n >\n <style href=\"kz-step-showcase\" precedence=\"medium\">{FILL_CSS}</style>\n <div className=\"order-2 grid gap-3 md:order-1\">\n <div role=\"tablist\" aria-orientation=\"vertical\" aria-label={label} className=\"flex justify-center gap-2 md:grid md:gap-1\">\n {steps.map((s, i) => {\n const active = i === index;\n return (\n <button\n key={s.id}\n type=\"button\"\n role=\"tab\"\n id={`${id}-tab-${i}`}\n aria-selected={active}\n aria-controls={`${id}-panel`}\n tabIndex={active ? 0 : -1}\n onClick={() => go(i, true)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") go(i + 1, true);\n else if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") go(i - 1, true);\n else return;\n e.preventDefault();\n }}\n className={cn(\n \"group relative rounded-xl text-left outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n \"max-md:h-2.5 max-md:w-2.5 max-md:rounded-full max-md:bg-border-strong max-md:aria-selected:w-6 max-md:aria-selected:bg-accent\",\n \"md:overflow-hidden md:p-4 md:hover:bg-surface-2 md:aria-selected:bg-surface-2\",\n )}\n >\n <span className=\"max-md:sr-only\">\n <span className=\"flex items-baseline gap-3\">\n <span className={cn(\"font-mono text-xs\", active ? \"text-fg-accent\" : \"text-fg-secondary\")}>{String(i + 1).padStart(2, \"0\")}</span>\n <span className={cn(\"text-sm font-semibold\", active ? \"text-fg\" : \"text-fg-secondary\")}>{s.title}</span>\n </span>\n <span className=\"mt-1 block pl-8 text-xs leading-relaxed text-fg-secondary\">{s.description}</span>\n </span>\n {active && auto && (\n <span aria-hidden className=\"absolute inset-x-4 bottom-0 hidden h-0.5 overflow-hidden rounded-full bg-border-subtle md:block\">\n <span\n key={`${s.id}-${playable}`}\n ref={bar}\n data-fill\n className={cn(\"block h-full bg-accent\", !playable && \"kz-step-fill\")}\n style={{ transform: playable ? \"scaleX(0)\" : undefined, animationPlayState: running ? \"running\" : \"paused\", [\"--kz-step-ms\" as string]: `${s.duration ?? 6000}ms` }}\n onAnimationEnd={() => go(index + 1, false)}\n />\n </span>\n )}\n </button>\n );\n })}\n </div>\n <p aria-hidden className=\"text-center text-sm md:hidden\">\n <span className=\"font-semibold text-fg\">{current?.title}</span> <span className=\"text-fg-secondary\">{current?.description}</span>\n </p>\n {auto && (\n <button type=\"button\" onClick={() => setPaused((p) => !p)} aria-pressed={paused} className=\"mx-auto inline-flex h-8 items-center gap-2 rounded-full border border-border-subtle px-3 text-xs font-medium text-fg-secondary hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring md:mx-0 md:w-fit\">\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"currentColor\" aria-hidden>\n {paused ? <path d=\"M8 5v14l11-7z\" /> : <path d=\"M7 5h4v14H7zM13 5h4v14h-4z\" />}\n </svg>\n {paused ? labels.play : labels.pause}\n </button>\n )}\n </div>\n\n <div\n id={`${id}-panel`}\n role=\"tabpanel\"\n aria-roledescription=\"slide\"\n aria-labelledby={`${id}-tab-${index}`}\n className=\"order-1 touch-pan-y md:order-2\"\n onPointerDown={(e) => {\n swipe.current = e.clientX;\n }}\n onPointerUp={(e) => {\n if (swipe.current === null) return;\n const dx = e.clientX - swipe.current;\n swipe.current = null;\n if (Math.abs(dx) > 40) go(index + (dx < 0 ? 1 : -1), true);\n }}\n >\n <DeviceFrame kind={device} title={deviceTitle}>\n {steps.map((s, i) => {\n const active = i === index;\n return (\n <div key={s.id} data-step={s.id} aria-hidden={!active} className={cn(\"absolute inset-0 transition-opacity duration-500 motion-reduce:transition-none\", active ? \"opacity-100\" : \"opacity-0\")}>\n <img src={s.poster} alt={active ? s.alt : \"\"} loading={i === 0 ? \"eager\" : \"lazy\"} decoding=\"async\" className=\"h-full w-full object-cover\" />\n {active && playable && s.video && (\n <video\n ref={video}\n key={s.id}\n muted\n playsInline\n preload=\"metadata\"\n poster={s.poster}\n aria-hidden\n tabIndex={-1}\n onTimeUpdate={onTime}\n onEnded={() => go(index + 1, false)}\n className=\"absolute inset-0 h-full w-full object-cover\"\n >\n {s.video.webm && <source src={s.video.webm} type=\"video/webm\" />}\n {s.video.mp4 && <source src={s.video.mp4} type=\"video/mp4\" />}\n </video>\n )}\n </div>\n );\n })}\n </DeviceFrame>\n <span className=\"sr-only\" aria-live=\"off\">\n {labels.step(index + 1, total)}\n </span>\n </div>\n </section>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/step-showcase/device-frame.tsx","../src/step-showcase/step-showcase.tsx"],"sourcesContent":["// Server-safe. The frame a product's recording plays in: a phone, a browser window or a macOS window — drawn with the\n// tokens (no image), so it follows dark and light. The content fills the screen area (`object-cover` media).\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\n\nexport type DeviceKind = \"phone\" | \"browser\" | \"desktop\";\n\nexport interface DeviceFrameProps extends React.ComponentProps<\"div\"> {\n /** The device: a phone, a browser window or a macOS window. */\n kind: DeviceKind;\n /** The browser's address or the window's title (“dev.orazaka.com”, “Orazaka”). */\n title?: string;\n}\n\nconst screen: Record<DeviceKind, string> = {\n phone: \"aspect-[9/19.5]\",\n browser: \"aspect-[16/10]\",\n desktop: \"aspect-[16/10]\",\n};\n\n/** A phone, a browser window or a macOS window around a screen of content. */\nexport function DeviceFrame({ kind, title, className, children, ...props }: DeviceFrameProps) {\n if (kind === \"phone\") {\n return (\n <div data-device=\"phone\" className={cn(\"relative mx-auto w-full max-w-[18rem] rounded-[2.4rem] border border-border-strong bg-surface-3 p-2 shadow-2xl\", className)} {...props}>\n <div className={cn(\"relative overflow-hidden rounded-[1.9rem] bg-surface-0\", screen.phone)}>\n {children}\n <span aria-hidden className=\"absolute left-1/2 top-2 h-5 w-20 -translate-x-1/2 rounded-full bg-surface-3\" />\n </div>\n </div>\n );\n }\n return (\n <div data-device={kind} className={cn(\"relative w-full overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-2xl\", className)} {...props}>\n <div className=\"flex h-8 items-center gap-1.5 border-b border-border-subtle bg-surface-2 px-3\">\n <span aria-hidden className=\"h-2.5 w-2.5 rounded-full bg-danger/80\" />\n <span aria-hidden className=\"h-2.5 w-2.5 rounded-full bg-warning/80\" />\n <span aria-hidden className=\"h-2.5 w-2.5 rounded-full bg-success/80\" />\n {title &&\n (kind === \"browser\" ? (\n <span className=\"mx-auto max-w-[60%] truncate rounded-md bg-surface-1 px-3 py-0.5 font-mono text-[11px] text-fg-secondary\">{title}</span>\n ) : (\n <span className=\"mx-auto truncate text-xs font-semibold text-fg-secondary\">{title}</span>\n ))}\n </div>\n <div className={cn(\"relative overflow-hidden bg-surface-0\", screen[kind])}>{children}</div>\n </div>\n );\n}\n","// Client (timers through the video, observers, swipe). A guided sequence in one device frame: the steps listed beside\n// it (a vertical tab list: number · title · one line, a bar filling under the current one), the frame playing the\n// step's recording muted, then moving on to the next; the recordings crossfade. It plays only while in view, holds on\n// hover and focus, keeps a visible play / pause button, and never shows a video's native controls. Recordings load\n// lazily (posters first, the current step's video once the block has been seen). Reduced motion: posters, manual steps.\n// Below `md` the list becomes dots under the frame, the frame swipes, the current step's words show under it.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { DeviceFrame, type DeviceKind } from \"./device-frame\";\n\nconst FILL_CSS = \".kz-step-fill{transform-origin:left;animation:kz-step-fill var(--kz-step-ms,6000ms) linear forwards}@keyframes kz-step-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\";\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst noop = () => () => undefined;\nconst readReduced = () => Boolean(typeof window !== \"undefined\" && window.matchMedia?.(REDUCED).matches);\nconst subscribeReduced = (notify: () => void) => {\n const q = window.matchMedia?.(REDUCED);\n q?.addEventListener?.(\"change\", notify);\n return () => q?.removeEventListener?.(\"change\", notify);\n};\n\nexport interface ShowcaseStep {\n /** A stable key. */\n id: string;\n /** The step's name (“Make an image”), passed translated. */\n title: string;\n /** One line under it. */\n description: string;\n /** The still shown first, and the only picture under reduced motion. */\n poster: string;\n /** What the poster shows, for assistive technology. */\n alt: string;\n /** The recording: WebM and / or MP4. Without one, the poster stays `duration` ms. */\n video?: { webm?: string; mp4?: string };\n /** Milliseconds a step without a video stays. @default 6000 */\n duration?: number;\n}\n\nexport interface StepShowcaseLabels {\n /** The button's name while paused. */\n play: string;\n /** The button's name while it plays. */\n pause: string;\n /** A step's position in words: `(2, 4) => \"Step 2 of 4\"`. */\n step: (n: number, total: number) => string;\n}\n\nexport const stepShowcaseLabels: StepShowcaseLabels = {\n play: \"Play the steps\",\n pause: \"Pause the steps\",\n step: (n, total) => `Step ${n} of ${total}`,\n};\n\nexport interface StepShowcaseProps extends Omit<React.ComponentProps<\"section\">, \"children\"> {\n /** The sequence's accessible name (“Orazaka in your pocket”), passed translated. */\n label: string;\n /** The steps, in the order of the journey. */\n steps: readonly ShowcaseStep[];\n /** The device the recordings were made on. */\n device: DeviceKind;\n /** The browser's address or the window's title. */\n deviceTitle?: string;\n /** Moves on by itself (never under reduced motion). @default true */\n autoplay?: boolean;\n /** The controls' words, translated. */\n labels?: StepShowcaseLabels;\n /** The step shown first. @default 0 */\n defaultIndex?: number;\n /** Called when the step changes — `user` is true when the visitor asked for it. */\n onIndexChange?: (index: number, user: boolean) => void;\n}\n\n/** One device frame, its steps beside it, playing each recording then the next — a product's guided tour. */\nexport function StepShowcase({ label, steps, device, deviceTitle, autoplay = true, labels = stepShowcaseLabels, defaultIndex = 0, onIndexChange, className, ...props }: StepShowcaseProps) {\n const total = steps.length;\n const id = React.useId();\n const root = React.useRef<HTMLElement>(null);\n const video = React.useRef<HTMLVideoElement>(null);\n const bar = React.useRef<HTMLSpanElement>(null);\n const swipe = React.useRef<number | null>(null);\n const [index, setIndex] = React.useState(defaultIndex);\n const [paused, setPaused] = React.useState(false);\n const [hold, setHold] = React.useState({ hover: false, focus: false });\n const [inView, setInView] = React.useState(false);\n const [seen, setSeen] = React.useState(false);\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n const mounted = React.useSyncExternalStore(noop, () => true, () => false);\n const current = steps[Math.min(index, total - 1)];\n const auto = autoplay && !reduced && mounted;\n const running = auto && !paused && !hold.hover && !hold.focus && inView;\n\n React.useEffect(() => {\n const host = root.current;\n if (!host || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver((e) => {\n const visible = e.some((x) => x.isIntersecting);\n setInView(visible);\n if (visible) setSeen(true);\n }, { threshold: 0.35 });\n io.observe(host);\n return () => io.disconnect();\n }, []);\n\n const go = React.useCallback(\n (to: number, user: boolean) => {\n const next = (to + total) % total;\n setIndex(next);\n if (bar.current) bar.current.style.transform = \"scaleX(0)\";\n onIndexChange?.(next, user);\n },\n [total, onIndexChange],\n );\n\n // The recording runs only while the sequence does; its time is the bar.\n React.useEffect(() => {\n const v = video.current;\n if (!v) return;\n if (running) v.play()?.catch?.(() => undefined);\n else v.pause();\n }, [running, index, seen]);\n\n const playable = Boolean(current?.video) && seen && !reduced;\n const onTime = (e: React.SyntheticEvent<HTMLVideoElement>) => {\n const v = e.currentTarget;\n if (bar.current && v.duration) bar.current.style.transform = `scaleX(${v.currentTime / v.duration})`;\n };\n\n return (\n <section\n ref={root}\n aria-roledescription=\"carousel\"\n aria-label={label}\n className={cn(\"grid items-center gap-6 md:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] md:gap-10\", className)}\n onPointerEnter={() => setHold((h) => ({ ...h, hover: true }))}\n onPointerLeave={() => setHold((h) => ({ ...h, hover: false }))}\n onFocus={() => setHold((h) => ({ ...h, focus: true }))}\n onBlur={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setHold((h) => ({ ...h, focus: false }));\n }}\n {...props}\n >\n <style href=\"kz-step-showcase\" precedence=\"medium\">{FILL_CSS}</style>\n <div className=\"order-2 grid gap-3 md:order-1\">\n <div role=\"tablist\" aria-orientation=\"vertical\" aria-label={label} className=\"flex justify-center gap-2 md:grid md:gap-1\">\n {steps.map((s, i) => {\n const active = i === index;\n return (\n <button\n key={s.id}\n type=\"button\"\n role=\"tab\"\n id={`${id}-tab-${i}`}\n aria-selected={active}\n aria-controls={`${id}-panel`}\n tabIndex={active ? 0 : -1}\n onClick={() => go(i, true)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") go(i + 1, true);\n else if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") go(i - 1, true);\n else return;\n e.preventDefault();\n }}\n className={cn(\n \"group relative rounded-xl text-left outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n \"max-md:h-2.5 max-md:w-2.5 max-md:rounded-full max-md:bg-border-strong max-md:aria-selected:w-6 max-md:aria-selected:bg-accent\",\n \"md:overflow-hidden md:p-4 md:hover:bg-surface-2 md:aria-selected:bg-surface-2\",\n )}\n >\n <span className=\"max-md:sr-only\">\n <span className=\"flex items-baseline gap-3\">\n <span className={cn(\"font-mono text-xs\", active ? \"text-fg-accent\" : \"text-fg-secondary\")}>{String(i + 1).padStart(2, \"0\")}</span>\n <span className={cn(\"text-sm font-semibold\", active ? \"text-fg\" : \"text-fg-secondary\")}>{s.title}</span>\n </span>\n <span className=\"mt-1 block pl-8 text-xs leading-relaxed text-fg-secondary\">{s.description}</span>\n </span>\n {active && auto && (\n <span aria-hidden className=\"absolute inset-x-4 bottom-0 hidden h-0.5 overflow-hidden rounded-full bg-border-subtle md:block\">\n <span\n key={`${s.id}-${playable}`}\n ref={bar}\n data-fill\n className={cn(\"block h-full bg-accent\", !playable && \"kz-step-fill\")}\n style={{ transform: playable ? \"scaleX(0)\" : undefined, animationPlayState: running ? \"running\" : \"paused\", [\"--kz-step-ms\" as string]: `${s.duration ?? 6000}ms` }}\n onAnimationEnd={() => go(index + 1, false)}\n />\n </span>\n )}\n </button>\n );\n })}\n </div>\n <p aria-hidden className=\"text-center text-sm md:hidden\">\n <span className=\"font-semibold text-fg\">{current?.title}</span> <span className=\"text-fg-secondary\">{current?.description}</span>\n </p>\n {auto && (\n <button type=\"button\" onClick={() => setPaused((p) => !p)} aria-pressed={paused} className=\"mx-auto inline-flex h-8 items-center gap-2 rounded-sm border border-border-subtle px-3 text-xs font-medium text-fg-secondary hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring md:mx-0 md:w-fit\">\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"currentColor\" aria-hidden>\n {paused ? <path d=\"M8 5v14l11-7z\" /> : <path d=\"M7 5h4v14H7zM13 5h4v14h-4z\" />}\n </svg>\n {paused ? labels.play : labels.pause}\n </button>\n )}\n </div>\n\n <div\n id={`${id}-panel`}\n role=\"tabpanel\"\n aria-roledescription=\"slide\"\n aria-labelledby={`${id}-tab-${index}`}\n className=\"order-1 touch-pan-y md:order-2\"\n onPointerDown={(e) => {\n swipe.current = e.clientX;\n }}\n onPointerUp={(e) => {\n if (swipe.current === null) return;\n const dx = e.clientX - swipe.current;\n swipe.current = null;\n if (Math.abs(dx) > 40) go(index + (dx < 0 ? 1 : -1), true);\n }}\n >\n <DeviceFrame kind={device} title={deviceTitle}>\n {steps.map((s, i) => {\n const active = i === index;\n return (\n <div key={s.id} data-step={s.id} aria-hidden={!active} className={cn(\"absolute inset-0 transition-opacity duration-500 motion-reduce:transition-none\", active ? \"opacity-100\" : \"opacity-0\")}>\n <img src={s.poster} alt={active ? s.alt : \"\"} loading={i === 0 ? \"eager\" : \"lazy\"} decoding=\"async\" className=\"h-full w-full object-cover\" />\n {active && playable && s.video && (\n <video\n ref={video}\n key={s.id}\n muted\n playsInline\n preload=\"metadata\"\n poster={s.poster}\n aria-hidden\n tabIndex={-1}\n onTimeUpdate={onTime}\n onEnded={() => go(index + 1, false)}\n className=\"absolute inset-0 h-full w-full object-cover\"\n >\n {s.video.webm && <source src={s.video.webm} type=\"video/webm\" />}\n {s.video.mp4 && <source src={s.video.mp4} type=\"video/mp4\" />}\n </video>\n )}\n </div>\n );\n })}\n </DeviceFrame>\n <span className=\"sr-only\" aria-live=\"off\">\n {labels.step(index + 1, total)}\n </span>\n </div>\n </section>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs"]}
|
package/dist/story.css
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
.kzs-stat { display: grid; gap: 6px; width: 100%; text-align: left; }
|
|
32
32
|
.kzs-stat-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 12.5px; }
|
|
33
33
|
.kzs-stat-label { color: var(--kz-text-secondary); }
|
|
34
|
-
.kzs-stat-value { color: var(--kz-text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
34
|
+
.kzs-stat-value { color: var(--kz-text-primary); font-weight: 600; font-variant-numeric: var(--kz-figures, lining-nums tabular-nums); }
|
|
35
35
|
.kzs-bar { position: relative; display: block; width: 100%; height: 6px; border-radius: 999px; overflow: hidden; background: var(--kz-surface-3); }
|
|
36
36
|
.kzs-bar-fill { position: absolute; inset: 0; border-radius: inherit; transform-origin: left center; transform: scaleX(var(--kzs-ratio, 1)); background: var(--kz-accent); }
|
|
37
37
|
|
|
@@ -208,7 +208,7 @@ html.light :is(.kzs-profile.kzs-arcade, .kzs-arcade .kzs-profile) { background:
|
|
|
208
208
|
.kzs-gauge { display: grid; gap: 8px; width: 100%; max-width: 560px; }
|
|
209
209
|
.kzs-gauge-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 12.5px; }
|
|
210
210
|
.kzs-gauge-label { color: var(--kz-text-secondary); }
|
|
211
|
-
.kzs-gauge-value { color: var(--kz-text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
211
|
+
.kzs-gauge-value { color: var(--kz-text-primary); font-weight: 600; font-variant-numeric: var(--kz-figures, lining-nums tabular-nums); }
|
|
212
212
|
.kzs-gauge-cells { display: grid; grid-template-columns: repeat(var(--kzs-segments, 8), minmax(0, 1fr)); gap: 4px; }
|
|
213
213
|
.kzs-gauge-cell { height: 8px; border-radius: 999px; background: var(--kz-surface-3); }
|
|
214
214
|
.kzs-gauge-cell[data-lit] { background: var(--kz-accent); }
|
|
@@ -261,7 +261,7 @@ html.light :is(.kzs-promise.kzs-arcade, .kzs-arcade .kzs-promise) { background:
|
|
|
261
261
|
.kzs-prompt-head { display: inline-flex; align-items: center; gap: 14px; }
|
|
262
262
|
.kzs-prompt-title { font-family: var(--kz-font-display); font-size: clamp(1.2rem, 3vw, 1.6rem); font-weight: 700; letter-spacing: -.015em; }
|
|
263
263
|
.kzs-prompt-count { position: relative; display: inline-grid; place-items: center; min-width: 1.8em; height: 1.8em; padding: 0 6px; border-radius: 10px;
|
|
264
|
-
font-family: var(--kz-font-display); font-size: clamp(1.2rem, 3vw, 1.5rem); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--kz-accent-text); background: var(--kz-accent-soft); }
|
|
264
|
+
font-family: var(--kz-font-display); font-size: clamp(1.2rem, 3vw, 1.5rem); font-weight: 700; font-variant-numeric: var(--kz-figures, lining-nums tabular-nums); color: var(--kz-accent-text); background: var(--kz-accent-soft); }
|
|
265
265
|
.kzs-prompt-count::after { position: absolute; inset: 0; display: grid; place-items: center; color: var(--kz-accent-text); }
|
|
266
266
|
.kzs-prompt-deck { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-top: 14px; }
|
|
267
267
|
.kzs-prompt-coin { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; font-size: 12.5px; color: var(--kz-text-secondary); }
|
package/dist/video-player.js
CHANGED
package/dist/video-trimmer.js
CHANGED
|
@@ -12,7 +12,7 @@ var videoTrimmer = tv({
|
|
|
12
12
|
slots: {
|
|
13
13
|
root: "w-full select-none",
|
|
14
14
|
header: "mb-2 flex items-center justify-between font-mono text-[11px] tabular-nums text-fg-secondary",
|
|
15
|
-
kept: "rounded-
|
|
15
|
+
kept: "rounded-sm bg-accent-soft px-2.5 py-0.5 font-sans text-xs font-semibold text-fg",
|
|
16
16
|
track: "relative h-16 touch-none rounded-xl bg-surface-2 data-[disabled]:opacity-50",
|
|
17
17
|
strip: "pointer-events-none absolute inset-0 flex overflow-hidden rounded-xl",
|
|
18
18
|
frame: "h-full min-w-0 flex-1 object-cover",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/video-trimmer/video-trimmer.tsx"],"sourcesContent":["// Client. The trim timeline of a video editor: the clip as a filmstrip (or any track content), the part kept framed\n// between two edge handles — each a `slider` named by a prop, its value read as a time, never past the other one —\n// the outside dimmed, and a playhead to scrub. The handles sit on their value by their outer edge (the start handle\n// to the right of its value, the end handle to its left), so the frame encloses exactly what is kept. Every part\n// takes its own classes (`classNames`) and the handles their own content (`renderHandle`).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { clamp, formatClock } from \"../media\";\n\nexport const videoTrimmer = tv({\n slots: {\n root: \"w-full select-none\",\n header: \"mb-2 flex items-center justify-between font-mono text-[11px] tabular-nums text-fg-secondary\",\n kept: \"rounded-full bg-accent-soft px-2.5 py-0.5 font-sans text-xs font-semibold text-fg\",\n track: \"relative h-16 touch-none rounded-xl bg-surface-2 data-[disabled]:opacity-50\",\n strip: \"pointer-events-none absolute inset-0 flex overflow-hidden rounded-xl\",\n frame: \"h-full min-w-0 flex-1 object-cover\",\n shade: \"pointer-events-none absolute inset-y-0 bg-scrim-strong\",\n range: \"pointer-events-none absolute inset-y-0 border-y-[3px] border-accent\",\n handle:\n \"absolute inset-y-0 z-10 flex w-6 cursor-ew-resize items-center justify-center bg-accent text-on-accent shadow-md \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 \" +\n \"data-[edge=start]:rounded-l-xl data-[edge=end]:-translate-x-full data-[edge=end]:rounded-r-xl\",\n grip: \"flex gap-[3px] [&>span]:h-5 [&>span]:w-[2px] [&>span]:rounded-sm [&>span]:bg-on-accent/70\",\n playhead:\n \"absolute -inset-y-1.5 z-20 w-[3px] -translate-x-1/2 cursor-ew-resize rounded-full bg-fg shadow-md \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n});\n\ntype Part = keyof ReturnType<typeof videoTrimmer>;\n\nexport type VideoTrimmerLabels = {\n /** The start handle (\"Start\"). */\n start: string;\n /** The end handle (\"End\"). */\n end: string;\n /** The playhead (\"Playhead\"). */\n playhead: string;\n /** The length kept, above the track: `(length) => length + \" kept\"` (the length already formatted). */\n kept: (length: string) => string;\n};\n\nexport const videoTrimmerLabels: VideoTrimmerLabels = {\n start: \"Start\",\n end: \"End\",\n playhead: \"Playhead\",\n kept: (length) => `${length} kept`,\n};\n\nexport type VideoTrimmerProps = {\n /** The length of the clip, in seconds. */\n duration: number;\n /** The part kept, `[start, end]` in seconds (controlled). */\n value?: readonly [number, number];\n /** The part kept at first, uncontrolled. Default: the whole clip. */\n defaultValue?: readonly [number, number];\n /** Called on every move of a handle. */\n onValueChange?: (value: [number, number]) => void;\n /** Called once a drag or a key press ends: render a preview, save. */\n onValueCommit?: (value: [number, number]) => void;\n /** The time played, in seconds: shows the playhead. */\n time?: number;\n /** Called when the playhead is moved (a drag on the track, the arrows on the playhead). */\n onSeek?: (time: number) => void;\n /** The shortest part that can be kept, in seconds. Default 0.5. */\n minLength?: number;\n /** One arrow press, in seconds. Default 0.1. */\n step?: number;\n /** Shift + arrow, Page Up / Page Down, in seconds. Default 1. */\n largeStep?: number;\n /** Pictures of the clip, in order: the filmstrip. */\n frames?: readonly string[];\n /** Track content instead of the filmstrip (a waveform, a coloured band). */\n children?: React.ReactNode;\n /** A time as read and shown. Default `m:ss` with tenths. */\n formatTime?: (seconds: number) => string;\n /** Shows the times and the length kept above the track. Default `true`. */\n showHeader?: boolean;\n /** Not available (a render is running). */\n disabled?: boolean;\n /** Classes per part: `root`, `header`, `kept`, `track`, `strip`, `frame`, `shade`, `range`, `handle`, `grip`, `playhead`. */\n classNames?: Partial<Record<Part, string>>;\n /** The content of a handle (default: two grip lines). */\n renderHandle?: (edge: \"start\" | \"end\") => React.ReactNode;\n /** The words, passed translated; English by default. */\n labels?: Partial<VideoTrimmerLabels>;\n};\n\nconst tenths = (seconds: number) => `${formatClock(seconds)}.${Math.floor((Math.max(0, seconds) % 1) * 10)}`;\n\ntype Grip = \"start\" | \"end\" | \"playhead\";\n\n/** Keeps a part of a clip between two handles over its filmstrip, with a playhead to scrub — the keyboard included. */\nexport function VideoTrimmer({\n duration,\n value: valueProp,\n defaultValue,\n onValueChange,\n onValueCommit,\n time,\n onSeek,\n minLength = 0.5,\n step = 0.1,\n largeStep = 1,\n frames,\n children,\n formatTime = tenths,\n showHeader = true,\n disabled,\n classNames = {},\n renderHandle,\n labels: given,\n}: VideoTrimmerProps) {\n const labels = { ...videoTrimmerLabels, ...given };\n const s = videoTrimmer();\n const track = React.useRef<HTMLDivElement>(null);\n const grip = React.useRef<Grip | null>(null);\n const [inner, setInner] = React.useState<[number, number]>(() => [defaultValue?.[0] ?? 0, defaultValue?.[1] ?? duration]);\n const [start, end] = valueProp ?? inner;\n const total = duration > 0 ? duration : 1;\n const pct = (seconds: number) => (clamp(seconds, 0, total) / total) * 100;\n\n const set = (next: [number, number], commit = false) => {\n if (!valueProp) setInner(next);\n onValueChange?.(next);\n if (commit) onValueCommit?.(next);\n };\n const move = (edge: \"start\" | \"end\", to: number, commit = false) => {\n const next: [number, number] =\n edge === \"start\" ? [clamp(to, 0, end - minLength), end] : [start, clamp(to, start + minLength, duration)];\n set([Math.round(next[0] * 1000) / 1000, Math.round(next[1] * 1000) / 1000], commit);\n };\n const seek = (to: number) => onSeek?.(clamp(to, start, Math.max(start, end - 0.05)));\n\n const at = (clientX: number) => {\n const box = track.current?.getBoundingClientRect();\n if (!box || box.width === 0) return 0;\n return clamp(((clientX - box.left) / box.width) * duration, 0, duration);\n };\n\n const keys = (which: Grip) => (event: React.KeyboardEvent) => {\n const current = which === \"start\" ? start : which === \"end\" ? end : (time ?? start);\n const big = largeStep;\n const small = event.shiftKey ? largeStep : step;\n const target: Record<string, number> = {\n ArrowLeft: current - small,\n ArrowDown: current - small,\n ArrowRight: current + small,\n ArrowUp: current + small,\n PageDown: current - big,\n PageUp: current + big,\n Home: 0,\n End: duration,\n };\n const to = target[event.key];\n if (to === undefined || disabled) return;\n event.preventDefault();\n if (which === \"playhead\") seek(to);\n else move(which, to, true);\n };\n\n const handle = (edge: \"start\" | \"end\") => {\n const own = edge === \"start\" ? start : end;\n return (\n <div\n key={edge}\n role=\"slider\"\n tabIndex={disabled ? -1 : 0}\n aria-label={edge === \"start\" ? labels.start : labels.end}\n aria-valuemin={edge === \"start\" ? 0 : Math.round((start + minLength) * 10) / 10}\n aria-valuemax={edge === \"start\" ? Math.round((end - minLength) * 10) / 10 : Math.round(duration * 10) / 10}\n aria-valuenow={Math.round(own * 10) / 10}\n aria-valuetext={formatTime(own)}\n aria-disabled={disabled || undefined}\n data-edge={edge}\n className={s.handle({ className: classNames.handle })}\n style={{ left: `${pct(own)}%` }}\n onPointerDown={(event) => {\n if (disabled) return;\n event.stopPropagation();\n track.current?.setPointerCapture?.(event.pointerId);\n grip.current = edge;\n }}\n onKeyDown={keys(edge)}\n >\n {renderHandle ? (\n renderHandle(edge)\n ) : (\n <span className={s.grip({ className: classNames.grip })}>\n <span />\n <span />\n </span>\n )}\n </div>\n );\n };\n\n return (\n <div className={s.root({ className: classNames.root })}>\n {showHeader && (\n <div className={s.header({ className: classNames.header })}>\n <span>{formatTime(start)}</span>\n <span className={s.kept({ className: classNames.kept })}>{labels.kept(formatTime(end - start))}</span>\n <span>{formatTime(end)}</span>\n </div>\n )}\n <div\n ref={track}\n className={s.track({ className: classNames.track })}\n data-disabled={disabled ? \"\" : undefined}\n onPointerDown={(event) => {\n if (disabled) return;\n track.current?.setPointerCapture?.(event.pointerId);\n const t = at(event.clientX);\n grip.current = onSeek ? \"playhead\" : Math.abs(t - start) < Math.abs(t - end) ? \"start\" : \"end\";\n if (grip.current === \"playhead\") seek(t);\n else move(grip.current, t);\n }}\n onPointerMove={(event) => {\n if (!grip.current) return;\n const t = at(event.clientX);\n if (grip.current === \"playhead\") seek(t);\n else move(grip.current, t);\n }}\n onPointerUp={() => {\n if (grip.current && grip.current !== \"playhead\") onValueCommit?.([start, end]);\n grip.current = null;\n }}\n onPointerCancel={() => {\n grip.current = null;\n }}\n >\n <div className={s.strip({ className: classNames.strip })}>\n {children ?? frames?.map((src, i) => <img key={`${i}-${src}`} src={src} alt=\"\" draggable={false} className={s.frame({ className: classNames.frame })} />)}\n </div>\n <div className={s.shade({ className: classNames.shade })} style={{ left: 0, width: `${pct(start)}%`, borderRadius: \"0.75rem 0 0 0.75rem\" }} />\n <div className={s.shade({ className: classNames.shade })} style={{ right: 0, width: `${100 - pct(end)}%`, borderRadius: \"0 0.75rem 0.75rem 0\" }} />\n <div className={s.range({ className: classNames.range })} style={{ left: `${pct(start)}%`, right: `${100 - pct(end)}%` }} />\n {time !== undefined && (\n <div\n role=\"slider\"\n tabIndex={disabled || !onSeek ? -1 : 0}\n aria-label={labels.playhead}\n aria-valuemin={Math.round(start * 10) / 10}\n aria-valuemax={Math.round(end * 10) / 10}\n aria-valuenow={Math.round(time * 10) / 10}\n aria-valuetext={formatTime(time)}\n aria-readonly={!onSeek || undefined}\n className={s.playhead({ className: classNames.playhead })}\n style={{ left: `${pct(time)}%` }}\n onKeyDown={onSeek ? keys(\"playhead\") : undefined}\n />\n )}\n {handle(\"start\")}\n {handle(\"end\")}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/video-trimmer/video-trimmer.tsx"],"sourcesContent":["// Client. The trim timeline of a video editor: the clip as a filmstrip (or any track content), the part kept framed\n// between two edge handles — each a `slider` named by a prop, its value read as a time, never past the other one —\n// the outside dimmed, and a playhead to scrub. The handles sit on their value by their outer edge (the start handle\n// to the right of its value, the end handle to its left), so the frame encloses exactly what is kept. Every part\n// takes its own classes (`classNames`) and the handles their own content (`renderHandle`).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { clamp, formatClock } from \"../media\";\n\nexport const videoTrimmer = tv({\n slots: {\n root: \"w-full select-none\",\n header: \"mb-2 flex items-center justify-between font-mono text-[11px] tabular-nums text-fg-secondary\",\n kept: \"rounded-sm bg-accent-soft px-2.5 py-0.5 font-sans text-xs font-semibold text-fg\",\n track: \"relative h-16 touch-none rounded-xl bg-surface-2 data-[disabled]:opacity-50\",\n strip: \"pointer-events-none absolute inset-0 flex overflow-hidden rounded-xl\",\n frame: \"h-full min-w-0 flex-1 object-cover\",\n shade: \"pointer-events-none absolute inset-y-0 bg-scrim-strong\",\n range: \"pointer-events-none absolute inset-y-0 border-y-[3px] border-accent\",\n handle:\n \"absolute inset-y-0 z-10 flex w-6 cursor-ew-resize items-center justify-center bg-accent text-on-accent shadow-md \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 \" +\n \"data-[edge=start]:rounded-l-xl data-[edge=end]:-translate-x-full data-[edge=end]:rounded-r-xl\",\n grip: \"flex gap-[3px] [&>span]:h-5 [&>span]:w-[2px] [&>span]:rounded-sm [&>span]:bg-on-accent/70\",\n playhead:\n \"absolute -inset-y-1.5 z-20 w-[3px] -translate-x-1/2 cursor-ew-resize rounded-full bg-fg shadow-md \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n});\n\ntype Part = keyof ReturnType<typeof videoTrimmer>;\n\nexport type VideoTrimmerLabels = {\n /** The start handle (\"Start\"). */\n start: string;\n /** The end handle (\"End\"). */\n end: string;\n /** The playhead (\"Playhead\"). */\n playhead: string;\n /** The length kept, above the track: `(length) => length + \" kept\"` (the length already formatted). */\n kept: (length: string) => string;\n};\n\nexport const videoTrimmerLabels: VideoTrimmerLabels = {\n start: \"Start\",\n end: \"End\",\n playhead: \"Playhead\",\n kept: (length) => `${length} kept`,\n};\n\nexport type VideoTrimmerProps = {\n /** The length of the clip, in seconds. */\n duration: number;\n /** The part kept, `[start, end]` in seconds (controlled). */\n value?: readonly [number, number];\n /** The part kept at first, uncontrolled. Default: the whole clip. */\n defaultValue?: readonly [number, number];\n /** Called on every move of a handle. */\n onValueChange?: (value: [number, number]) => void;\n /** Called once a drag or a key press ends: render a preview, save. */\n onValueCommit?: (value: [number, number]) => void;\n /** The time played, in seconds: shows the playhead. */\n time?: number;\n /** Called when the playhead is moved (a drag on the track, the arrows on the playhead). */\n onSeek?: (time: number) => void;\n /** The shortest part that can be kept, in seconds. Default 0.5. */\n minLength?: number;\n /** One arrow press, in seconds. Default 0.1. */\n step?: number;\n /** Shift + arrow, Page Up / Page Down, in seconds. Default 1. */\n largeStep?: number;\n /** Pictures of the clip, in order: the filmstrip. */\n frames?: readonly string[];\n /** Track content instead of the filmstrip (a waveform, a coloured band). */\n children?: React.ReactNode;\n /** A time as read and shown. Default `m:ss` with tenths. */\n formatTime?: (seconds: number) => string;\n /** Shows the times and the length kept above the track. Default `true`. */\n showHeader?: boolean;\n /** Not available (a render is running). */\n disabled?: boolean;\n /** Classes per part: `root`, `header`, `kept`, `track`, `strip`, `frame`, `shade`, `range`, `handle`, `grip`, `playhead`. */\n classNames?: Partial<Record<Part, string>>;\n /** The content of a handle (default: two grip lines). */\n renderHandle?: (edge: \"start\" | \"end\") => React.ReactNode;\n /** The words, passed translated; English by default. */\n labels?: Partial<VideoTrimmerLabels>;\n};\n\nconst tenths = (seconds: number) => `${formatClock(seconds)}.${Math.floor((Math.max(0, seconds) % 1) * 10)}`;\n\ntype Grip = \"start\" | \"end\" | \"playhead\";\n\n/** Keeps a part of a clip between two handles over its filmstrip, with a playhead to scrub — the keyboard included. */\nexport function VideoTrimmer({\n duration,\n value: valueProp,\n defaultValue,\n onValueChange,\n onValueCommit,\n time,\n onSeek,\n minLength = 0.5,\n step = 0.1,\n largeStep = 1,\n frames,\n children,\n formatTime = tenths,\n showHeader = true,\n disabled,\n classNames = {},\n renderHandle,\n labels: given,\n}: VideoTrimmerProps) {\n const labels = { ...videoTrimmerLabels, ...given };\n const s = videoTrimmer();\n const track = React.useRef<HTMLDivElement>(null);\n const grip = React.useRef<Grip | null>(null);\n const [inner, setInner] = React.useState<[number, number]>(() => [defaultValue?.[0] ?? 0, defaultValue?.[1] ?? duration]);\n const [start, end] = valueProp ?? inner;\n const total = duration > 0 ? duration : 1;\n const pct = (seconds: number) => (clamp(seconds, 0, total) / total) * 100;\n\n const set = (next: [number, number], commit = false) => {\n if (!valueProp) setInner(next);\n onValueChange?.(next);\n if (commit) onValueCommit?.(next);\n };\n const move = (edge: \"start\" | \"end\", to: number, commit = false) => {\n const next: [number, number] =\n edge === \"start\" ? [clamp(to, 0, end - minLength), end] : [start, clamp(to, start + minLength, duration)];\n set([Math.round(next[0] * 1000) / 1000, Math.round(next[1] * 1000) / 1000], commit);\n };\n const seek = (to: number) => onSeek?.(clamp(to, start, Math.max(start, end - 0.05)));\n\n const at = (clientX: number) => {\n const box = track.current?.getBoundingClientRect();\n if (!box || box.width === 0) return 0;\n return clamp(((clientX - box.left) / box.width) * duration, 0, duration);\n };\n\n const keys = (which: Grip) => (event: React.KeyboardEvent) => {\n const current = which === \"start\" ? start : which === \"end\" ? end : (time ?? start);\n const big = largeStep;\n const small = event.shiftKey ? largeStep : step;\n const target: Record<string, number> = {\n ArrowLeft: current - small,\n ArrowDown: current - small,\n ArrowRight: current + small,\n ArrowUp: current + small,\n PageDown: current - big,\n PageUp: current + big,\n Home: 0,\n End: duration,\n };\n const to = target[event.key];\n if (to === undefined || disabled) return;\n event.preventDefault();\n if (which === \"playhead\") seek(to);\n else move(which, to, true);\n };\n\n const handle = (edge: \"start\" | \"end\") => {\n const own = edge === \"start\" ? start : end;\n return (\n <div\n key={edge}\n role=\"slider\"\n tabIndex={disabled ? -1 : 0}\n aria-label={edge === \"start\" ? labels.start : labels.end}\n aria-valuemin={edge === \"start\" ? 0 : Math.round((start + minLength) * 10) / 10}\n aria-valuemax={edge === \"start\" ? Math.round((end - minLength) * 10) / 10 : Math.round(duration * 10) / 10}\n aria-valuenow={Math.round(own * 10) / 10}\n aria-valuetext={formatTime(own)}\n aria-disabled={disabled || undefined}\n data-edge={edge}\n className={s.handle({ className: classNames.handle })}\n style={{ left: `${pct(own)}%` }}\n onPointerDown={(event) => {\n if (disabled) return;\n event.stopPropagation();\n track.current?.setPointerCapture?.(event.pointerId);\n grip.current = edge;\n }}\n onKeyDown={keys(edge)}\n >\n {renderHandle ? (\n renderHandle(edge)\n ) : (\n <span className={s.grip({ className: classNames.grip })}>\n <span />\n <span />\n </span>\n )}\n </div>\n );\n };\n\n return (\n <div className={s.root({ className: classNames.root })}>\n {showHeader && (\n <div className={s.header({ className: classNames.header })}>\n <span>{formatTime(start)}</span>\n <span className={s.kept({ className: classNames.kept })}>{labels.kept(formatTime(end - start))}</span>\n <span>{formatTime(end)}</span>\n </div>\n )}\n <div\n ref={track}\n className={s.track({ className: classNames.track })}\n data-disabled={disabled ? \"\" : undefined}\n onPointerDown={(event) => {\n if (disabled) return;\n track.current?.setPointerCapture?.(event.pointerId);\n const t = at(event.clientX);\n grip.current = onSeek ? \"playhead\" : Math.abs(t - start) < Math.abs(t - end) ? \"start\" : \"end\";\n if (grip.current === \"playhead\") seek(t);\n else move(grip.current, t);\n }}\n onPointerMove={(event) => {\n if (!grip.current) return;\n const t = at(event.clientX);\n if (grip.current === \"playhead\") seek(t);\n else move(grip.current, t);\n }}\n onPointerUp={() => {\n if (grip.current && grip.current !== \"playhead\") onValueCommit?.([start, end]);\n grip.current = null;\n }}\n onPointerCancel={() => {\n grip.current = null;\n }}\n >\n <div className={s.strip({ className: classNames.strip })}>\n {children ?? frames?.map((src, i) => <img key={`${i}-${src}`} src={src} alt=\"\" draggable={false} className={s.frame({ className: classNames.frame })} />)}\n </div>\n <div className={s.shade({ className: classNames.shade })} style={{ left: 0, width: `${pct(start)}%`, borderRadius: \"0.75rem 0 0 0.75rem\" }} />\n <div className={s.shade({ className: classNames.shade })} style={{ right: 0, width: `${100 - pct(end)}%`, borderRadius: \"0 0.75rem 0.75rem 0\" }} />\n <div className={s.range({ className: classNames.range })} style={{ left: `${pct(start)}%`, right: `${100 - pct(end)}%` }} />\n {time !== undefined && (\n <div\n role=\"slider\"\n tabIndex={disabled || !onSeek ? -1 : 0}\n aria-label={labels.playhead}\n aria-valuemin={Math.round(start * 10) / 10}\n aria-valuemax={Math.round(end * 10) / 10}\n aria-valuenow={Math.round(time * 10) / 10}\n aria-valuetext={formatTime(time)}\n aria-readonly={!onSeek || undefined}\n className={s.playhead({ className: classNames.playhead })}\n style={{ left: `${pct(time)}%` }}\n onKeyDown={onSeek ? keys(\"playhead\") : undefined}\n />\n )}\n {handle(\"start\")}\n {handle(\"end\")}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.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",
|
|
@@ -741,7 +741,7 @@
|
|
|
741
741
|
],
|
|
742
742
|
"peerDependencies": {
|
|
743
743
|
"@krizaka/query": ">=0.2.1",
|
|
744
|
-
"@krizaka/tailwind": ">=3.
|
|
744
|
+
"@krizaka/tailwind": ">=3.39.0",
|
|
745
745
|
"@maplibre/maplibre-react-native": ">=11",
|
|
746
746
|
"@shopify/flash-list": ">=2",
|
|
747
747
|
"expo-camera": ">=16",
|
|
@@ -2536,7 +2536,7 @@
|
|
|
2536
2536
|
"tailwind-variants": "^3.3.1",
|
|
2537
2537
|
"@krizaka/intl": "^0.2.1",
|
|
2538
2538
|
"@krizaka/pacer": "^0.1.0",
|
|
2539
|
-
"@krizaka/tokens": "^3.
|
|
2539
|
+
"@krizaka/tokens": "^3.39.0"
|
|
2540
2540
|
},
|
|
2541
2541
|
"scripts": {
|
|
2542
2542
|
"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/chip-link.json
CHANGED
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"files": [
|
|
110
110
|
{
|
|
111
111
|
"path": "chip-link/chip-link.tsx",
|
|
112
|
-
"content": "// Server-safe: no hook, no context. A chip that is a link — a filter that is an address (`/explore?tag=night`): the look\n// of `Chip`, the current filter marked by `selected` (`aria-current`, the accent), usable in a Server Component.\n// `chipVariants` styles anything else (a router link you render yourself).\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const chipVariants = tv({\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-
|
|
112
|
+
"content": "// Server-safe: no hook, no context. A chip that is a link — a filter that is an address (`/explore?tag=night`): the look\n// of `Chip`, the current filter marked by `selected` (`aria-current`, the accent), usable in a Server Component.\n// `chipVariants` styles anything else (a router link you render yourself).\nimport { Slot } from \"radix-ui\";\nimport type * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nexport const chipVariants = tv({\n base:\n \"inline-flex shrink-0 select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-sm border font-semibold no-underline transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n variants: {\n size: { sm: \"h-7 px-3 text-xs\", md: \"h-9 px-3.5 text-xs\" },\n selected: {\n false: \"border-border-default bg-surface-1 text-fg-secondary hover:border-border-strong hover:text-fg\",\n true: \"border-accent bg-accent-soft text-fg\",\n },\n },\n defaultVariants: { size: \"md\", selected: false },\n});\n\nexport type ChipVariantsProps = VariantProps<typeof chipVariants>;\n\nexport type ChipLinkProps = React.ComponentProps<\"a\"> & {\n /**\n * sm · md.\n * @default \"md\"\n */\n size?: ChipVariantsProps[\"size\"];\n /** The current filter: the accent, `aria-current` (`page` by default) and `data-state=\"on\"`. */\n selected?: boolean;\n /**\n * What `aria-current` says when selected: `page` (a filter that is a page) or `true`.\n * @default \"page\"\n */\n current?: \"page\" | \"true\";\n /** Renders your router's `<Link>` with the chip's look. */\n asChild?: boolean;\n};\n\n/** A filter that is a link, with the look of `Chip`. */\nexport function ChipLink({ size, selected = false, current = \"page\", asChild, className, ...props }: ChipLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <Comp\n data-state={selected ? \"on\" : \"off\"}\n aria-current={selected ? current : undefined}\n className={chipVariants({ size, selected, className })}\n {...props}\n />\n );\n}\n"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"path": "chip-link/index.ts",
|
|
@@ -13,10 +13,6 @@ function SiteFooterCompany() {
|
|
|
13
13
|
legal: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
14
14
|
/* @__PURE__ */ jsx(SiteFooter.Link, { href: "#privacy", children: "Privacy" }),
|
|
15
15
|
/* @__PURE__ */ jsx(SiteFooter.Link, { href: "#terms", children: "Terms" })
|
|
16
|
-
] }),
|
|
17
|
-
children: /* @__PURE__ */ jsxs(SiteFooter.Column, { title: "Company", children: [
|
|
18
|
-
/* @__PURE__ */ jsx(SiteFooter.Link, { href: "#story", active: true, children: "Our story" }),
|
|
19
|
-
/* @__PURE__ */ jsx(SiteFooter.Link, { href: "#contact", children: "Contact" })
|
|
20
16
|
] })
|
|
21
17
|
}
|
|
22
18
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { SiteFooter } from "@krizaka/ui/site-footer";
|
|
2
2
|
import { familyLabels } from "@krizaka/ui/sites";
|
|
3
3
|
|
|
4
|
-
// www.krizaka.com: “A Krizaka company
|
|
4
|
+
// www.krizaka.com: “A Krizaka company” — the family's columns already hold the company's pages (story, blog, contact),
|
|
5
|
+
// so www adds no column of its own; the legal links end the signature line.
|
|
5
6
|
export default function SiteFooterCompany() {
|
|
6
7
|
return (
|
|
7
8
|
<SiteFooter
|
|
@@ -13,11 +14,6 @@ export default function SiteFooterCompany() {
|
|
|
13
14
|
<SiteFooter.Link href="#terms">Terms</SiteFooter.Link>
|
|
14
15
|
</>
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
<SiteFooter.Column title="Company">
|
|
18
|
-
<SiteFooter.Link href="#story" active>Our story</SiteFooter.Link>
|
|
19
|
-
<SiteFooter.Link href="#contact">Contact</SiteFooter.Link>
|
|
20
|
-
</SiteFooter.Column>
|
|
21
|
-
</SiteFooter>
|
|
17
|
+
/>
|
|
22
18
|
);
|
|
23
19
|
}
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.0",
|
|
4
4
|
"templatePurposes": {
|
|
5
5
|
"convince": {
|
|
6
6
|
"title": "Convince",
|
|
@@ -3110,7 +3110,7 @@
|
|
|
3110
3110
|
"native": 2
|
|
3111
3111
|
},
|
|
3112
3112
|
"dependencies": [
|
|
3113
|
-
"@krizaka/tokens@^3.
|
|
3113
|
+
"@krizaka/tokens@^3.39.0",
|
|
3114
3114
|
"@protomaps/basemaps@5.7.2",
|
|
3115
3115
|
"tailwind-variants@^3.3.1"
|
|
3116
3116
|
],
|
|
@@ -3549,7 +3549,7 @@
|
|
|
3549
3549
|
"native": 0
|
|
3550
3550
|
},
|
|
3551
3551
|
"dependencies": [
|
|
3552
|
-
"@krizaka/tokens@^3.
|
|
3552
|
+
"@krizaka/tokens@^3.39.0",
|
|
3553
3553
|
"radix-ui@^1.7.0",
|
|
3554
3554
|
"tailwind-variants@^3.3.1"
|
|
3555
3555
|
],
|
|
@@ -4212,7 +4212,7 @@
|
|
|
4212
4212
|
"native": 0
|
|
4213
4213
|
},
|
|
4214
4214
|
"dependencies": [
|
|
4215
|
-
"@krizaka/tokens@^3.
|
|
4215
|
+
"@krizaka/tokens@^3.39.0",
|
|
4216
4216
|
"radix-ui@^1.7.0",
|
|
4217
4217
|
"tailwind-variants@^3.3.1"
|
|
4218
4218
|
],
|
|
@@ -4532,8 +4532,8 @@
|
|
|
4532
4532
|
"name": "site-footer",
|
|
4533
4533
|
"type": "primitive",
|
|
4534
4534
|
"title": "Site footer",
|
|
4535
|
-
"summary": "The footer of a Krizaka site: the
|
|
4536
|
-
"description": "The footer of a Krizaka site: the
|
|
4535
|
+
"summary": "The footer of a Krizaka site, one calm band: its own columns beside the family's — Products · Open source (GitHub with them) · Krizaka (our story, the blog, contact) — on the Krizaka sites (`family={false}` for an independent product), then one line: “A Krizaka company / product / open-source project”, the version, Montréal and the legal links. No site block, no link twice.",
|
|
4536
|
+
"description": "The footer of a Krizaka site, one calm band: its own columns beside the family's — Products · Open source (GitHub with them) · Krizaka (our story, the blog, contact) — on the Krizaka sites (`family={false}` for an independent product), then one line: “A Krizaka company / product / open-source project”, the version, Montréal and the legal links. No site block, no link twice.",
|
|
4537
4537
|
"status": "beta",
|
|
4538
4538
|
"category": "navigation",
|
|
4539
4539
|
"platforms": "web",
|
|
@@ -4555,7 +4555,7 @@
|
|
|
4555
4555
|
"native": 0
|
|
4556
4556
|
},
|
|
4557
4557
|
"dependencies": [
|
|
4558
|
-
"@krizaka/tokens@^3.
|
|
4558
|
+
"@krizaka/tokens@^3.39.0",
|
|
4559
4559
|
"radix-ui@^1.7.0",
|
|
4560
4560
|
"tailwind-variants@^3.3.1"
|
|
4561
4561
|
],
|
package/registry/kanban.json
CHANGED
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "kanban/kanban.tsx",
|
|
170
|
-
"content": "// Client (the drag, the keyboard move, the announcements). Cards in columns that move by drag and drop — and by the\n// keyboard: Space picks a card up, the arrows carry it (← → between columns, ↑ ↓ within one), Space drops it, Esc puts it\n// back. Every move is announced in words. The app owns the data: `onMove` says where a card went.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const kanban = tv({\n slots: {\n root: \"flex w-full gap-3 overflow-x-auto pb-2\",\n column: \"flex w-72 shrink-0 flex-col rounded-xl border border-border-default bg-surface-1 data-[over]:border-accent data-[over]:bg-accent-soft/40\",\n header: \"flex items-center gap-2 px-3 pb-2 pt-3\",\n title: \"min-w-0 flex-1 truncate text-sm font-semibold text-fg\",\n count: \"rounded-full bg-surface-3 px-2 text-xs font-semibold tabular-nums text-fg-secondary data-[over-limit]:bg-danger data-[over-limit]:text-on-accent\",\n list: \"m-0 flex min-h-16 list-none flex-col gap-2 p-2 pt-0\",\n card:\n \"cursor-grab rounded-lg border border-border-default bg-surface-0 p-3 text-sm text-fg shadow-xs outline-hidden transition-shadow \" +\n \"focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing data-[grabbed]:ring-2 data-[grabbed]:ring-accent data-[grabbed]:shadow-lg data-[dragging]:opacity-40\",\n empty: \"rounded-lg border border-dashed border-border-default px-3 py-4 text-center text-xs text-fg-secondary\",\n },\n});\n\nexport type KanbanColumn = {\n /** A stable id. */\n id: string;\n /** Its name — passed translated (“To review”). */\n title: string;\n /** The most cards it should hold: the count turns to danger past it. */\n limit?: number;\n};\n\nexport type KanbanMove = {\n /** The card moved. */\n cardId: string;\n /** Where it came from. */\n from: string;\n /** The column it went to. */\n to: string;\n /** Its position in that column (0 = top). */\n index: number;\n};\n\nexport type KanbanLabels = {\n /** The hint read on a card: how to move it with the keyboard. */\n instructions: string;\n /** Announced when a card is picked up: `(card, column) => …`. */\n grabbed: (card: string, column: string) => string;\n /** Announced as it moves: `(card, column, position, total) => …`. */\n moved: (card: string, column: string, position: number, total: number) => string;\n /** Announced when it is dropped. */\n dropped: (card: string, column: string, position: number) => string;\n /** Announced when the move is cancelled. */\n cancelled: (card: string) => string;\n /** A column without card (“No card”). */\n empty: string;\n};\n\nexport const kanbanLabels: KanbanLabels = {\n instructions: \"Press Space to pick it up, the arrow keys to move it, Space to drop it, Escape to cancel.\",\n grabbed: (card, column) => `${card} picked up, in ${column}.`,\n moved: (card, column, position, total) => `${card}: ${column}, position ${position} of ${total}.`,\n dropped: (card, column, position) => `${card} dropped in ${column}, position ${position}.`,\n cancelled: (card) => `Move of ${card} cancelled.`,\n empty: \"No card\",\n};\n\nexport type KanbanProps<T> = {\n /** The columns, in order. */\n columns: readonly KanbanColumn[];\n /** The cards, in their order within each column. */\n cards: readonly T[];\n /** A card's id. */\n getId: (card: T) => string;\n /** A card's column. */\n getColumn: (card: T) => string;\n /** A card's name, for the announcements. */\n getTitle: (card: T) => string;\n /** What a card shows. */\n renderCard: (card: T) => React.ReactNode;\n /** Called when a card is dropped somewhere new: update the data here. */\n onMove: (move: KanbanMove) => void;\n /** The board's accessible name (“Moderation queue”). */\n \"aria-label\": string;\n /** The words, passed translated; English by default. */\n labels?: Partial<KanbanLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\ntype Placement = { column: string; index: number };\n\n/**\n * A board of cards in columns — a moderation queue, a release plan: drag and drop with the pointer, or Space and the\n * arrow keys, every step announced. Column limits show in the counts.\n */\nexport function Kanban<T>({ columns, cards, getId, getColumn, getTitle, renderCard, onMove, \"aria-label\": label, labels: words, className }: KanbanProps<T>) {\n const labels = { ...kanbanLabels, ...words };\n const s = kanban();\n const instructionsId = React.useId();\n const [grab, setGrab] = React.useState<{ id: string; origin: Placement; at: Placement } | null>(null);\n const [dragging, setDragging] = React.useState<string | null>(null);\n const [over, setOver] = React.useState<Placement | null>(null);\n const [message, setMessage] = React.useState(\"\");\n const refs = React.useRef(new Map<string, HTMLLIElement>());\n const byColumn = (column: string, without?: string) => cards.filter((c) => getColumn(c) === column && getId(c) !== without);\n const titleOf = (id: string) => {\n const card = cards.find((c) => getId(c) === id);\n return card ? getTitle(card) : id;\n };\n const columnTitle = (id: string) => columns.find((c) => c.id === id)?.title ?? id;\n const placementOf = (id: string): Placement => {\n const card = cards.find((c) => getId(c) === id)!;\n const column = getColumn(card);\n return { column, index: byColumn(column).findIndex((c) => getId(c) === id) };\n };\n const commit = (id: string, from: Placement, to: Placement) => {\n if (from.column !== to.column || from.index !== to.index) onMove({ cardId: id, from: from.column, to: to.column, index: to.index });\n };\n // The cards of a column as shown, with the grabbed card at its keyboard position.\n const shown = (column: string) => {\n const list = byColumn(column, grab?.id);\n if (grab && grab.at.column === column) {\n const card = cards.find((c) => getId(c) === grab.id);\n if (card) list.splice(grab.at.index, 0, card);\n }\n return list;\n };\n React.useEffect(() => {\n if (grab) refs.current.get(grab.id)?.focus();\n }, [grab]);\n\n const onKeyDown = (event: React.KeyboardEvent, id: string) => {\n const title = titleOf(id);\n if (!grab) {\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n const origin = placementOf(id);\n setGrab({ id, origin, at: origin });\n setMessage(labels.grabbed(title, columnTitle(origin.column)));\n }\n return;\n }\n const { at, origin } = grab;\n const columnIndex = columns.findIndex((c) => c.id === at.column);\n let next: Placement | null = null;\n if (event.key === \"ArrowLeft\" && columnIndex > 0) next = { column: columns[columnIndex - 1]!.id, index: Math.min(at.index, byColumn(columns[columnIndex - 1]!.id, id).length) };\n if (event.key === \"ArrowRight\" && columnIndex < columns.length - 1) next = { column: columns[columnIndex + 1]!.id, index: Math.min(at.index, byColumn(columns[columnIndex + 1]!.id, id).length) };\n if (event.key === \"ArrowUp\" && at.index > 0) next = { ...at, index: at.index - 1 };\n if (event.key === \"ArrowDown\" && at.index < byColumn(at.column, id).length) next = { ...at, index: at.index + 1 };\n if (next) {\n event.preventDefault();\n setGrab({ ...grab, at: next });\n setMessage(labels.moved(title, columnTitle(next.column), next.index + 1, byColumn(next.column, id).length + 1));\n return;\n }\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n setGrab(null);\n commit(id, origin, at);\n setMessage(labels.dropped(title, columnTitle(at.column), at.index + 1));\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setGrab(null);\n setMessage(labels.cancelled(title));\n }\n };\n\n const indexAt = (column: string, y: number) => {\n const list = byColumn(column, dragging ?? undefined);\n const i = list.findIndex((c) => {\n const box = refs.current.get(getId(c))?.getBoundingClientRect();\n return box ? y < box.top + box.height / 2 : false;\n });\n return i === -1 ? list.length : i;\n };\n\n return (\n <div role=\"group\" aria-label={label} className={s.root({ className })}>\n <p id={instructionsId} className=\"sr-only\">\n {labels.instructions}\n </p>\n <p className=\"sr-only\" aria-live=\"assertive\">\n {message}\n </p>\n {columns.map((column) => {\n const list = shown(column.id);\n const overLimit = column.limit !== undefined && list.length > column.limit;\n return (\n <section\n key={column.id}\n aria-label={column.title}\n data-over={over?.column === column.id || undefined}\n className={s.column()}\n onDragOver={(e) => {\n if (!dragging) return;\n e.preventDefault();\n setOver({ column: column.id, index: indexAt(column.id, e.clientY) });\n }}\n onDragLeave={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setOver(null);\n }}\n onDrop={(e) => {\n e.preventDefault();\n if (dragging) commit(dragging, placementOf(dragging), { column: column.id, index: indexAt(column.id, e.clientY) });\n setDragging(null);\n setOver(null);\n }}\n >\n <header className={s.header()}>\n <h3 className={s.title()}>{column.title}</h3>\n <span className={s.count()} data-over-limit={overLimit || undefined}>\n {column.limit !== undefined ? `${list.length}/${column.limit}` : list.length}\n </span>\n </header>\n <ul className={s.list()}>\n {list.length === 0 ? <li className={s.empty()}>{labels.empty}</li> : null}\n {list.map((card) => {\n const id = getId(card);\n return (\n <li\n key={id}\n ref={(el) => {\n if (el) refs.current.set(id, el);\n else refs.current.delete(id);\n }}\n tabIndex={0}\n draggable\n aria-describedby={instructionsId}\n aria-roledescription=\"card\"\n data-grabbed={grab?.id === id || undefined}\n data-dragging={dragging === id || undefined}\n className={s.card()}\n onKeyDown={(e) => onKeyDown(e, id)}\n onFocus={() => {\n // Tab to another card while one is carried: the carried one goes back.\n if (grab && grab.id !== id) {\n setMessage(labels.cancelled(titleOf(grab.id)));\n setGrab(null);\n }\n }}\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = \"move\";\n e.dataTransfer.setData(\"text/plain\", id);\n setDragging(id);\n }}\n onDragEnd={() => {\n setDragging(null);\n setOver(null);\n }}\n >\n {renderCard(card)}\n </li>\n );\n })}\n </ul>\n </section>\n );\n })}\n </div>\n );\n}\n"
|
|
170
|
+
"content": "// Client (the drag, the keyboard move, the announcements). Cards in columns that move by drag and drop — and by the\n// keyboard: Space picks a card up, the arrows carry it (← → between columns, ↑ ↓ within one), Space drops it, Esc puts it\n// back. Every move is announced in words. The app owns the data: `onMove` says where a card went.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const kanban = tv({\n slots: {\n root: \"flex w-full gap-3 overflow-x-auto pb-2\",\n column: \"flex w-72 shrink-0 flex-col rounded-xl border border-border-default bg-surface-1 data-[over]:border-accent data-[over]:bg-accent-soft/40\",\n header: \"flex items-center gap-2 px-3 pb-2 pt-3\",\n title: \"min-w-0 flex-1 truncate text-sm font-semibold text-fg\",\n count: \"rounded-sm bg-surface-3 px-2 text-xs font-semibold tabular-nums text-fg-secondary data-[over-limit]:bg-danger data-[over-limit]:text-on-accent\",\n list: \"m-0 flex min-h-16 list-none flex-col gap-2 p-2 pt-0\",\n card:\n \"cursor-grab rounded-lg border border-border-default bg-surface-0 p-3 text-sm text-fg shadow-xs outline-hidden transition-shadow \" +\n \"focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing data-[grabbed]:ring-2 data-[grabbed]:ring-accent data-[grabbed]:shadow-lg data-[dragging]:opacity-40\",\n empty: \"rounded-lg border border-dashed border-border-default px-3 py-4 text-center text-xs text-fg-secondary\",\n },\n});\n\nexport type KanbanColumn = {\n /** A stable id. */\n id: string;\n /** Its name — passed translated (“To review”). */\n title: string;\n /** The most cards it should hold: the count turns to danger past it. */\n limit?: number;\n};\n\nexport type KanbanMove = {\n /** The card moved. */\n cardId: string;\n /** Where it came from. */\n from: string;\n /** The column it went to. */\n to: string;\n /** Its position in that column (0 = top). */\n index: number;\n};\n\nexport type KanbanLabels = {\n /** The hint read on a card: how to move it with the keyboard. */\n instructions: string;\n /** Announced when a card is picked up: `(card, column) => …`. */\n grabbed: (card: string, column: string) => string;\n /** Announced as it moves: `(card, column, position, total) => …`. */\n moved: (card: string, column: string, position: number, total: number) => string;\n /** Announced when it is dropped. */\n dropped: (card: string, column: string, position: number) => string;\n /** Announced when the move is cancelled. */\n cancelled: (card: string) => string;\n /** A column without card (“No card”). */\n empty: string;\n};\n\nexport const kanbanLabels: KanbanLabels = {\n instructions: \"Press Space to pick it up, the arrow keys to move it, Space to drop it, Escape to cancel.\",\n grabbed: (card, column) => `${card} picked up, in ${column}.`,\n moved: (card, column, position, total) => `${card}: ${column}, position ${position} of ${total}.`,\n dropped: (card, column, position) => `${card} dropped in ${column}, position ${position}.`,\n cancelled: (card) => `Move of ${card} cancelled.`,\n empty: \"No card\",\n};\n\nexport type KanbanProps<T> = {\n /** The columns, in order. */\n columns: readonly KanbanColumn[];\n /** The cards, in their order within each column. */\n cards: readonly T[];\n /** A card's id. */\n getId: (card: T) => string;\n /** A card's column. */\n getColumn: (card: T) => string;\n /** A card's name, for the announcements. */\n getTitle: (card: T) => string;\n /** What a card shows. */\n renderCard: (card: T) => React.ReactNode;\n /** Called when a card is dropped somewhere new: update the data here. */\n onMove: (move: KanbanMove) => void;\n /** The board's accessible name (“Moderation queue”). */\n \"aria-label\": string;\n /** The words, passed translated; English by default. */\n labels?: Partial<KanbanLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\ntype Placement = { column: string; index: number };\n\n/**\n * A board of cards in columns — a moderation queue, a release plan: drag and drop with the pointer, or Space and the\n * arrow keys, every step announced. Column limits show in the counts.\n */\nexport function Kanban<T>({ columns, cards, getId, getColumn, getTitle, renderCard, onMove, \"aria-label\": label, labels: words, className }: KanbanProps<T>) {\n const labels = { ...kanbanLabels, ...words };\n const s = kanban();\n const instructionsId = React.useId();\n const [grab, setGrab] = React.useState<{ id: string; origin: Placement; at: Placement } | null>(null);\n const [dragging, setDragging] = React.useState<string | null>(null);\n const [over, setOver] = React.useState<Placement | null>(null);\n const [message, setMessage] = React.useState(\"\");\n const refs = React.useRef(new Map<string, HTMLLIElement>());\n const byColumn = (column: string, without?: string) => cards.filter((c) => getColumn(c) === column && getId(c) !== without);\n const titleOf = (id: string) => {\n const card = cards.find((c) => getId(c) === id);\n return card ? getTitle(card) : id;\n };\n const columnTitle = (id: string) => columns.find((c) => c.id === id)?.title ?? id;\n const placementOf = (id: string): Placement => {\n const card = cards.find((c) => getId(c) === id)!;\n const column = getColumn(card);\n return { column, index: byColumn(column).findIndex((c) => getId(c) === id) };\n };\n const commit = (id: string, from: Placement, to: Placement) => {\n if (from.column !== to.column || from.index !== to.index) onMove({ cardId: id, from: from.column, to: to.column, index: to.index });\n };\n // The cards of a column as shown, with the grabbed card at its keyboard position.\n const shown = (column: string) => {\n const list = byColumn(column, grab?.id);\n if (grab && grab.at.column === column) {\n const card = cards.find((c) => getId(c) === grab.id);\n if (card) list.splice(grab.at.index, 0, card);\n }\n return list;\n };\n React.useEffect(() => {\n if (grab) refs.current.get(grab.id)?.focus();\n }, [grab]);\n\n const onKeyDown = (event: React.KeyboardEvent, id: string) => {\n const title = titleOf(id);\n if (!grab) {\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n const origin = placementOf(id);\n setGrab({ id, origin, at: origin });\n setMessage(labels.grabbed(title, columnTitle(origin.column)));\n }\n return;\n }\n const { at, origin } = grab;\n const columnIndex = columns.findIndex((c) => c.id === at.column);\n let next: Placement | null = null;\n if (event.key === \"ArrowLeft\" && columnIndex > 0) next = { column: columns[columnIndex - 1]!.id, index: Math.min(at.index, byColumn(columns[columnIndex - 1]!.id, id).length) };\n if (event.key === \"ArrowRight\" && columnIndex < columns.length - 1) next = { column: columns[columnIndex + 1]!.id, index: Math.min(at.index, byColumn(columns[columnIndex + 1]!.id, id).length) };\n if (event.key === \"ArrowUp\" && at.index > 0) next = { ...at, index: at.index - 1 };\n if (event.key === \"ArrowDown\" && at.index < byColumn(at.column, id).length) next = { ...at, index: at.index + 1 };\n if (next) {\n event.preventDefault();\n setGrab({ ...grab, at: next });\n setMessage(labels.moved(title, columnTitle(next.column), next.index + 1, byColumn(next.column, id).length + 1));\n return;\n }\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault();\n setGrab(null);\n commit(id, origin, at);\n setMessage(labels.dropped(title, columnTitle(at.column), at.index + 1));\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setGrab(null);\n setMessage(labels.cancelled(title));\n }\n };\n\n const indexAt = (column: string, y: number) => {\n const list = byColumn(column, dragging ?? undefined);\n const i = list.findIndex((c) => {\n const box = refs.current.get(getId(c))?.getBoundingClientRect();\n return box ? y < box.top + box.height / 2 : false;\n });\n return i === -1 ? list.length : i;\n };\n\n return (\n <div role=\"group\" aria-label={label} className={s.root({ className })}>\n <p id={instructionsId} className=\"sr-only\">\n {labels.instructions}\n </p>\n <p className=\"sr-only\" aria-live=\"assertive\">\n {message}\n </p>\n {columns.map((column) => {\n const list = shown(column.id);\n const overLimit = column.limit !== undefined && list.length > column.limit;\n return (\n <section\n key={column.id}\n aria-label={column.title}\n data-over={over?.column === column.id || undefined}\n className={s.column()}\n onDragOver={(e) => {\n if (!dragging) return;\n e.preventDefault();\n setOver({ column: column.id, index: indexAt(column.id, e.clientY) });\n }}\n onDragLeave={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setOver(null);\n }}\n onDrop={(e) => {\n e.preventDefault();\n if (dragging) commit(dragging, placementOf(dragging), { column: column.id, index: indexAt(column.id, e.clientY) });\n setDragging(null);\n setOver(null);\n }}\n >\n <header className={s.header()}>\n <h3 className={s.title()}>{column.title}</h3>\n <span className={s.count()} data-over-limit={overLimit || undefined}>\n {column.limit !== undefined ? `${list.length}/${column.limit}` : list.length}\n </span>\n </header>\n <ul className={s.list()}>\n {list.length === 0 ? <li className={s.empty()}>{labels.empty}</li> : null}\n {list.map((card) => {\n const id = getId(card);\n return (\n <li\n key={id}\n ref={(el) => {\n if (el) refs.current.set(id, el);\n else refs.current.delete(id);\n }}\n tabIndex={0}\n draggable\n aria-describedby={instructionsId}\n aria-roledescription=\"card\"\n data-grabbed={grab?.id === id || undefined}\n data-dragging={dragging === id || undefined}\n className={s.card()}\n onKeyDown={(e) => onKeyDown(e, id)}\n onFocus={() => {\n // Tab to another card while one is carried: the carried one goes back.\n if (grab && grab.id !== id) {\n setMessage(labels.cancelled(titleOf(grab.id)));\n setGrab(null);\n }\n }}\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = \"move\";\n e.dataTransfer.setData(\"text/plain\", id);\n setDragging(id);\n }}\n onDragEnd={() => {\n setDragging(null);\n setOver(null);\n }}\n >\n {renderCard(card)}\n </li>\n );\n })}\n </ul>\n </section>\n );\n })}\n </div>\n );\n}\n"
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"path": "kanban/meta.ts",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
"path": "landing-page/landing-page.tsx",
|
|
159
|
-
"content": "// Server-safe. A landing page (blocks.css `.kz-tpl-*`): a split hero — the promise and its actions beside the product\n// alive (a recording, a demo) — a band of proofs, the features as a bento where the main one takes the room, then the\n// closing call. The blocks come through their package entries, so their client parts keep their boundary.\nimport { CtaPanel } from \"@krizaka/ui/cta-panel\";\nimport { Page, Section } from \"@krizaka/ui/page\";\nimport { SectionBackdrop } from \"@krizaka/ui/section-backdrop\";\nimport type * as React from \"react\";\n\nexport interface LandingFeature {\n /** A stable key. */\n id: string;\n /** Its name. */\n title: React.ReactNode;\n /** One or two sentences. */\n text: React.ReactNode;\n /** A signature icon above the title. */\n icon?: React.ReactNode;\n /** A visual at the bottom of the tile (a chart, a screenshot, a code line). */\n visual?: React.ReactNode;\n /**\n * Its weight in the bento: `lg` (four columns, two rows — the main one), `md` (three), `sm` (two).\n * @default \"sm\"\n */\n size?: \"lg\" | \"md\" | \"sm\";\n}\n\nexport type LandingPageProps = Omit<React.HTMLAttributes<HTMLElement>, \"title\"> & {\n /** A mono line above the title. */\n eyebrow?: React.ReactNode;\n /** The promise: the page's `h1`. */\n title: React.ReactNode;\n /** One or two sentences under it. */\n lead?: React.ReactNode;\n /** The hero's actions (the main one cut, with shimmer). */\n actions?: React.ReactNode;\n /** The product, alive, beside the words: a recording, a live demo (framed as a window). */\n visual: React.ReactNode;\n /** The window's caption (`orochia.app — studio`). */\n visualLabel?: React.ReactNode;\n /** Figures or facts in a band under the hero: `{ value, label }`. */\n proofs?: readonly { value: React.ReactNode; label: React.ReactNode }[];\n /** The features' section: its eyebrow, title and lead. */\n featuresHeading?: { eyebrow?: React.ReactNode; title: React.ReactNode; lead?: React.ReactNode };\n /** The features, as a bento. */\n features: readonly LandingFeature[];\n /** Between the features and the closing call: a story, a quote, testimonials. */\n children?: React.ReactNode;\n /** The closing call: its title, body and actions. */\n closing?: { title: React.ReactNode; body?: React.ReactNode; actions: React.ReactNode };\n};\n\n/** A landing page: split hero with the product alive, proofs, a bento of features, the closing call. */\nexport function LandingPage({ eyebrow, title, lead, actions, visual, visualLabel, proofs, featuresHeading, features, closing, children, ...props }: LandingPageProps) {\n return (\n <div {...props}>\n <SectionBackdrop as=\"header\" grid className=\"kz-hero\">\n <Page className=\"kz-tpl-hero-frame\">\n <div className=\"kz-tpl-split\">\n <div className=\"kz-hero-text\">\n {eyebrow && <p className=\"kz-eyebrow\">{eyebrow}</p>}\n <h1 className=\"kz-hero-title\">{title}</h1>\n {lead && <p className=\"kz-hero-lead\">{lead}</p>}\n {actions && <div className=\"kz-tpl-actions kz-hero-extra\">{actions}</div>}\n </div>\n <figure className=\"kz-tpl-stage\">\n <div className=\"kz-tpl-stage-bar\" aria-hidden>\n
|
|
159
|
+
"content": "// Server-safe. A landing page (blocks.css `.kz-tpl-*`): a split hero — the promise and its actions beside the product\n// alive (a recording, a demo) — a band of proofs, the features as a bento where the main one takes the room, then the\n// closing call. The blocks come through their package entries, so their client parts keep their boundary.\nimport { CtaPanel } from \"@krizaka/ui/cta-panel\";\nimport { Page, Section } from \"@krizaka/ui/page\";\nimport { SectionBackdrop } from \"@krizaka/ui/section-backdrop\";\nimport type * as React from \"react\";\n\nexport interface LandingFeature {\n /** A stable key. */\n id: string;\n /** Its name. */\n title: React.ReactNode;\n /** One or two sentences. */\n text: React.ReactNode;\n /** A signature icon above the title. */\n icon?: React.ReactNode;\n /** A visual at the bottom of the tile (a chart, a screenshot, a code line). */\n visual?: React.ReactNode;\n /**\n * Its weight in the bento: `lg` (four columns, two rows — the main one), `md` (three), `sm` (two).\n * @default \"sm\"\n */\n size?: \"lg\" | \"md\" | \"sm\";\n}\n\nexport type LandingPageProps = Omit<React.HTMLAttributes<HTMLElement>, \"title\"> & {\n /** A mono line above the title. */\n eyebrow?: React.ReactNode;\n /** The promise: the page's `h1`. */\n title: React.ReactNode;\n /** One or two sentences under it. */\n lead?: React.ReactNode;\n /** The hero's actions (the main one cut, with shimmer). */\n actions?: React.ReactNode;\n /** The product, alive, beside the words: a recording, a live demo (framed as a window). */\n visual: React.ReactNode;\n /** The window's caption (`orochia.app — studio`). */\n visualLabel?: React.ReactNode;\n /** Figures or facts in a band under the hero: `{ value, label }`. */\n proofs?: readonly { value: React.ReactNode; label: React.ReactNode }[];\n /** The features' section: its eyebrow, title and lead. */\n featuresHeading?: { eyebrow?: React.ReactNode; title: React.ReactNode; lead?: React.ReactNode };\n /** The features, as a bento. */\n features: readonly LandingFeature[];\n /** Between the features and the closing call: a story, a quote, testimonials. */\n children?: React.ReactNode;\n /** The closing call: its title, body and actions. */\n closing?: { title: React.ReactNode; body?: React.ReactNode; actions: React.ReactNode };\n};\n\n/** A landing page: split hero with the product alive, proofs, a bento of features, the closing call. */\nexport function LandingPage({ eyebrow, title, lead, actions, visual, visualLabel, proofs, featuresHeading, features, closing, children, ...props }: LandingPageProps) {\n return (\n <div {...props}>\n <SectionBackdrop as=\"header\" grid className=\"kz-hero\">\n <Page className=\"kz-tpl-hero-frame\">\n <div className=\"kz-tpl-split\">\n <div className=\"kz-hero-text\">\n {eyebrow && <p className=\"kz-eyebrow\">{eyebrow}</p>}\n <h1 className=\"kz-hero-title\">{title}</h1>\n {lead && <p className=\"kz-hero-lead\">{lead}</p>}\n {actions && <div className=\"kz-tpl-actions kz-hero-extra\">{actions}</div>}\n </div>\n <figure className=\"kz-tpl-stage\">\n <div className=\"kz-tpl-stage-bar\" aria-hidden>\n {visualLabel && <span>{visualLabel}</span>}\n </div>\n {visual}\n </figure>\n </div>\n </Page>\n </SectionBackdrop>\n {proofs && proofs.length > 0 && (\n <Page>\n <dl className=\"kz-tpl-band\">\n {proofs.map((p, i) => (\n <div key={i}>\n <dt>{p.value}</dt>\n <dd>{p.label}</dd>\n </div>\n ))}\n </dl>\n </Page>\n )}\n <Section id=\"features\" eyebrow={featuresHeading?.eyebrow} title={featuresHeading?.title} lead={featuresHeading?.lead} reveal aria-label={featuresHeading ? undefined : \"Features\"}>\n <ul className=\"kz-tpl-bento\">\n {features.map((f) => (\n <li key={f.id} data-size={f.size === \"sm\" ? undefined : f.size} data-reveal=\"\">\n {f.icon && <span className=\"kz-block-icon\" aria-hidden>{f.icon}</span>}\n <h3>{f.title}</h3>\n <p>{f.text}</p>\n {f.visual && <div className=\"kz-tpl-bento-visual\">{f.visual}</div>}\n </li>\n ))}\n </ul>\n </Section>\n {children}\n {closing && (\n <Section spacing=\"sm\" aria-labelledby=\"landing-closing\">\n <CtaPanel id=\"landing-closing\" title={closing.title} body={closing.body} reveal>\n {closing.actions}\n </CtaPanel>\n </Section>\n )}\n </div>\n );\n}\n"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"path": "landing-page/meta.ts",
|