@krizaka/ui 3.9.0 → 3.11.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/README.md +3 -1
- package/dist/blocks.css +9 -2
- package/dist/chunk-HG65YAV4.js +379 -0
- package/dist/chunk-HG65YAV4.js.map +1 -0
- package/dist/chunk-OT6HX3PN.js +85 -0
- package/dist/chunk-OT6HX3PN.js.map +1 -0
- package/dist/index.js +9 -367
- package/dist/index.js.map +1 -1
- package/dist/link-card.d.ts +7 -1
- package/dist/link-card.js +8 -5
- package/dist/link-card.js.map +1 -1
- package/dist/native.d.ts +48 -1
- package/dist/native.js +374 -257
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.d.ts +86 -3
- package/dist/nav-bar.js +88 -2
- package/dist/nav-bar.js.map +1 -1
- package/dist/showcase.d.ts +69 -0
- package/dist/showcase.js +218 -0
- package/dist/showcase.js.map +1 -0
- package/dist/site-footer.d.ts +172 -0
- package/dist/site-footer.js +105 -0
- package/dist/site-footer.js.map +1 -0
- package/dist/sites-CgJrxejx.d.ts +32 -0
- package/dist/sites.d.ts +64 -0
- package/dist/sites.js +69 -0
- package/dist/sites.js.map +1 -0
- package/package.json +47 -9
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/activity-feed.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +11 -2
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/chat.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +1 -1
- package/registry/decision-list.json +1 -1
- package/registry/description-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/card-grid/aligned.d.ts +4 -0
- package/registry/examples/card-grid/aligned.js +18 -0
- package/registry/examples/card-grid/aligned.tsx +16 -0
- package/registry/examples/nav-bar/family-admin.d.ts +4 -0
- package/registry/examples/nav-bar/family-admin.js +21 -0
- package/registry/examples/nav-bar/family-admin.tsx +20 -0
- package/registry/examples/nav-bar/family-java.d.ts +4 -0
- package/registry/examples/nav-bar/family-java.js +33 -0
- package/registry/examples/nav-bar/family-java.tsx +34 -0
- package/registry/examples/nav-bar/family-product.d.ts +4 -0
- package/registry/examples/nav-bar/family-product.js +35 -0
- package/registry/examples/nav-bar/family-product.tsx +39 -0
- package/registry/examples/nav-bar/family-ui.d.ts +4 -0
- package/registry/examples/nav-bar/family-ui.js +34 -0
- package/registry/examples/nav-bar/family-ui.tsx +35 -0
- package/registry/examples/nav-bar/family-www.d.ts +4 -0
- package/registry/examples/nav-bar/family-www.js +34 -0
- package/registry/examples/nav-bar/family-www.tsx +36 -0
- package/registry/examples/showcase/native/slides.d.ts +4 -0
- package/registry/examples/showcase/native/slides.dark.png +0 -0
- package/registry/examples/showcase/native/slides.js +27 -0
- package/registry/examples/showcase/native/slides.light.png +0 -0
- package/registry/examples/showcase/native/slides.tsx +28 -0
- package/registry/examples/showcase/prompt-image.d.ts +4 -0
- package/registry/examples/showcase/prompt-image.js +29 -0
- package/registry/examples/showcase/prompt-image.tsx +28 -0
- package/registry/examples/showcase/slides.d.ts +4 -0
- package/registry/examples/showcase/slides.js +29 -0
- package/registry/examples/showcase/slides.tsx +24 -0
- package/registry/examples/site-footer/company.d.ts +4 -0
- package/registry/examples/site-footer/company.js +26 -0
- package/registry/examples/site-footer/company.tsx +23 -0
- package/registry/examples/site-footer/open-source-fr.d.ts +4 -0
- package/registry/examples/site-footer/open-source-fr.js +17 -0
- package/registry/examples/site-footer/open-source-fr.tsx +17 -0
- package/registry/examples/site-footer/product.d.ts +4 -0
- package/registry/examples/site-footer/product.js +22 -0
- package/registry/examples/site-footer/product.tsx +19 -0
- package/registry/examples/sites/family.d.ts +4 -0
- package/registry/examples/sites/family.js +20 -0
- package/registry/examples/sites/family.tsx +30 -0
- package/registry/examples/sites/french.d.ts +4 -0
- package/registry/examples/sites/french.js +25 -0
- package/registry/examples/sites/french.tsx +25 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/form.json +1 -1
- package/registry/gallery-page.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +65 -7
- package/registry/iso.json +1 -1
- package/registry/kanban.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +18 -3
- package/registry/list.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/map.json +2 -2
- package/registry/markdown.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/match-dialog.json +1 -1
- package/registry/media-capture.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +257 -13
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/photo-grid.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +1 -1
- package/registry/recipes.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/showcase.json +421 -0
- package/registry/site-footer.json +389 -0
- package/registry/sites.json +93 -0
- package/registry/skeleton.json +1 -1
- package/registry/skip-link.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/step-progress.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/swipe-deck.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +1 -1
- package/registry/tag-selector.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/tree-view.json +1 -1
- package/registry/verified-badge.json +1 -1
- package/registry/vertical-video.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "showcase",
|
|
3
|
+
"type": "primitive",
|
|
4
|
+
"title": "Showcase",
|
|
5
|
+
"summary": "A slideshow that advances by itself and shows it: a segmented progress bar — the current segment fills over the interval, each one a button — with a counter, previous / next and a visible pause button. It waits for the pointer, the keyboard focus, a hidden tab and a scroll away, and never advances under reduced motion.",
|
|
6
|
+
"description": "A slideshow that advances by itself and shows it: a segmented progress bar — the current segment fills over the interval, each one a button — with a counter, previous / next and a visible pause button. It waits for the pointer, the keyboard focus, a hidden tab and a scroll away, and never advances under reduced motion.",
|
|
7
|
+
"status": "beta",
|
|
8
|
+
"deprecated": null,
|
|
9
|
+
"category": "data-display",
|
|
10
|
+
"platforms": "both",
|
|
11
|
+
"whenToUse": [
|
|
12
|
+
"A gallery of results that should move on its own — prompt and image, before and after, three steps of a product — where each slide is a small composition (a picture, words, an action).",
|
|
13
|
+
"A landing page's proof: the reader sees it moving, can stop it and can jump to any slide."
|
|
14
|
+
],
|
|
15
|
+
"whenNotToUse": [
|
|
16
|
+
{
|
|
17
|
+
"when": "For content the reader must compare or read in order: lay the items out."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"when": "For a band of logos or short quotes that slides continuously.",
|
|
21
|
+
"use": "marquee"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"when": "For cards to swipe away and decide on.",
|
|
25
|
+
"use": "swipe-deck"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"bestPractices": [
|
|
29
|
+
"Give every slide the same size (an aspect ratio): only the slide in view and its neighbours are mounted, and the height must not jump.",
|
|
30
|
+
"Pass `labels` translated — the controls, the counter (“Image 1 of 17”) and each slide's “n of N”; `getTitle` names the slide in the counter.",
|
|
31
|
+
"5–8 seconds per slide for a picture and a sentence; the bar shows the time, so the reader can decide to stay.",
|
|
32
|
+
"Type a text with `useTypedText` only when it is short, and keep the full text for assistive technology (`sr-only`).",
|
|
33
|
+
"The bar's fill is a CSS animation and its end moves the show: its rules ship with the component, nothing to import."
|
|
34
|
+
],
|
|
35
|
+
"accessibility": {
|
|
36
|
+
"keyboard": [
|
|
37
|
+
{
|
|
38
|
+
"keys": "← / →",
|
|
39
|
+
"action": "Previous or next slide (the show then reads the new slide aloud)."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"keys": "Home / End",
|
|
43
|
+
"action": "First or last slide."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"keys": "Tab",
|
|
47
|
+
"action": "The current segment, previous, pause / play, next, then the content of the slide in view."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"keys": "Enter / Space",
|
|
51
|
+
"action": "On a segment: that slide. On the pause button: stops or restarts the show."
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"notes": [
|
|
55
|
+
"A region with `aria-roledescription=\"carousel\"` named by `label`; each slide is a `group` (`aria-roledescription=\"slide\"`) named “n of N”; the slides out of view are hidden and inert.",
|
|
56
|
+
"WCAG 2.2.2: a visible pause button; the show also stops while the pointer is over it, while a keyboard focus is inside, while the tab is hidden and while it is off screen.",
|
|
57
|
+
"The live region is `polite` only after the visitor navigates — the automatic changes are never announced.",
|
|
58
|
+
"Under `prefers-reduced-motion` the show never advances by itself: no pause button, the current segment is drawn full, the navigation is manual.",
|
|
59
|
+
"On a phone: swipe the pages; Reduce Motion turns the autoplay off; the app in the background holds it."
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"related": [
|
|
63
|
+
"marquee",
|
|
64
|
+
"swipe-deck",
|
|
65
|
+
"step-progress",
|
|
66
|
+
"tabs"
|
|
67
|
+
],
|
|
68
|
+
"web": {
|
|
69
|
+
"entry": "@krizaka/ui/showcase",
|
|
70
|
+
"import": "import { Showcase, useTypedText, showcaseLabels } from \"@krizaka/ui/showcase\";",
|
|
71
|
+
"props": [
|
|
72
|
+
{
|
|
73
|
+
"component": "Showcase",
|
|
74
|
+
"description": "An auto-advancing slideshow with a visible, clickable progress bar, a counter, prev / next and a pause button.",
|
|
75
|
+
"props": [
|
|
76
|
+
{
|
|
77
|
+
"name": "getId",
|
|
78
|
+
"type": "(item: T) => string",
|
|
79
|
+
"default": null,
|
|
80
|
+
"description": "A stable key for an item.",
|
|
81
|
+
"required": true
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "items",
|
|
85
|
+
"type": "readonly T[]",
|
|
86
|
+
"default": null,
|
|
87
|
+
"description": "The slides, in order.",
|
|
88
|
+
"required": true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "label",
|
|
92
|
+
"type": "string",
|
|
93
|
+
"default": null,
|
|
94
|
+
"description": "The slideshow's accessible name (“Prompt to image”), passed translated.",
|
|
95
|
+
"required": true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "renderItem",
|
|
99
|
+
"type": "(item: T, state: ShowcaseSlideState) => ReactNode",
|
|
100
|
+
"default": null,
|
|
101
|
+
"description": "One slide's content. Only the slide in view and its neighbours are mounted (the rest stay empty): give slides the same size.",
|
|
102
|
+
"required": true
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "autoplay",
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"default": "true",
|
|
108
|
+
"description": "Advances by itself. Never under `prefers-reduced-motion`.",
|
|
109
|
+
"required": false
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "defaultIndex",
|
|
113
|
+
"type": "number",
|
|
114
|
+
"default": "0",
|
|
115
|
+
"description": "The slide shown first.",
|
|
116
|
+
"required": false
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "getTitle",
|
|
120
|
+
"type": "((item: T) => string)",
|
|
121
|
+
"default": null,
|
|
122
|
+
"description": "A slide's title, shown before the counter (“Golden-hour editorial”).",
|
|
123
|
+
"required": false
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "index",
|
|
127
|
+
"type": "number",
|
|
128
|
+
"default": null,
|
|
129
|
+
"description": "The slide in view (controlled).",
|
|
130
|
+
"required": false
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "interval",
|
|
134
|
+
"type": "number",
|
|
135
|
+
"default": "6000",
|
|
136
|
+
"description": "Milliseconds a slide stays (the time the segment takes to fill).",
|
|
137
|
+
"required": false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "labels",
|
|
141
|
+
"type": "ShowcaseLabels",
|
|
142
|
+
"default": "{\n previous: \"Previous slide\",\n next: \"Next slide\",\n pause: \"Pause the slideshow\",\n play: \"Play the slideshow\",\n goTo: (n) => `Go to slide ${n}`,\n counter: (n, total) => `Slide ${n} of ${total}`,\n slide: (n, total) => `${n} of ${total}`,\n}",
|
|
143
|
+
"description": "The words of the controls, translated.",
|
|
144
|
+
"required": false
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "loop",
|
|
148
|
+
"type": "boolean",
|
|
149
|
+
"default": "true",
|
|
150
|
+
"description": "After the last slide, go back to the first.",
|
|
151
|
+
"required": false
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"name": "onIndexChange",
|
|
155
|
+
"type": "((index: number, user: boolean) => void)",
|
|
156
|
+
"default": null,
|
|
157
|
+
"description": "Called when the slide in view changes — `user` is true when the visitor asked for it.",
|
|
158
|
+
"required": false
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "transition",
|
|
162
|
+
"type": "\"fade\" | \"slide\"",
|
|
163
|
+
"default": "fade",
|
|
164
|
+
"description": "`fade` crossfades; `slide` also moves the slides a few pixels.",
|
|
165
|
+
"required": false
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"examples": [
|
|
171
|
+
{
|
|
172
|
+
"name": "prompt-image",
|
|
173
|
+
"title": "Prompt → image",
|
|
174
|
+
"description": "An image on the left, its prompt typed on the right with the action: moves on every 6 s, the bar shows how long is left.",
|
|
175
|
+
"path": "examples/showcase/prompt-image.tsx",
|
|
176
|
+
"module": "@krizaka/ui/registry/examples/showcase/prompt-image",
|
|
177
|
+
"code": "import { Button } from \"@krizaka/ui/button\";\nimport { Showcase, useTypedText } from \"@krizaka/ui/showcase\";\n\nconst CDN = \"https://krizaka-assets.b-cdn.net/site/people/ui\";\nconst SHOTS = [\n { id: \"a\", title: \"Golden-hour editorial\", prompt: \"A woman on a rooftop at golden hour, soft rim light, 85mm.\", src: `${CDN}/camille-512.9573eaa0b8.webp` },\n { id: \"b\", title: \"Sunday market\", prompt: \"A man choosing peaches at a market stall, warm morning haze.\", src: `${CDN}/tomas-256.7eaab80903.webp` },\n { id: \"c\", title: \"Backstage\", prompt: \"A singer in a dressing-room mirror, tungsten bulbs, grain.\", src: `${CDN}/noor-256.c340c41421.webp` },\n];\n\nfunction Slide({ shot, active }: { shot: (typeof SHOTS)[number]; active: boolean }) {\n const typed = useTypedText(shot.prompt, { active });\n return (\n <div className=\"grid items-center gap-6 sm:grid-cols-2\">\n <img src={shot.src} alt={shot.title} className=\"aspect-square w-full rounded-xl object-cover\" />\n <div className=\"flex flex-col items-start gap-3\">\n <p className=\"sr-only\">{shot.prompt}</p>\n <p aria-hidden className=\"m-0 min-h-20 font-mono text-sm text-fg\">{typed.text}</p>\n <Button variant=\"secondary\" size=\"sm\">Reproduce it</Button>\n </div>\n </div>\n );\n}\n\n// Image left, prompt and action right; the bar below fills over 6 s and moves on, every segment is a button.\nexport default function ShowcasePromptImage() {\n return <Showcase label=\"Prompt to image\" className=\"w-full max-w-3xl\" items={SHOTS} getId={(s) => s.id} getTitle={(s) => s.title} renderItem={(shot, { active }) => <Slide shot={shot} active={active} />} />;\n}\n"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "slides",
|
|
181
|
+
"title": "Any content",
|
|
182
|
+
"description": "Text slides, 4 s each, the slide transition, no loop: the last slide stays.",
|
|
183
|
+
"path": "examples/showcase/slides.tsx",
|
|
184
|
+
"module": "@krizaka/ui/registry/examples/showcase/slides",
|
|
185
|
+
"code": "import { Showcase } from \"@krizaka/ui/showcase\";\n\nconst SLIDES = [\n { id: \"1\", title: \"Describe it\", body: \"Write what you want in a sentence.\" },\n { id: \"2\", title: \"Generate it\", body: \"The model runs on your own machine.\" },\n { id: \"3\", title: \"Reproduce it\", body: \"Every result keeps its prompt and its seed.\" },\n];\n\n// Any content in a slide; 4 s each, a slide effect, no loop back to the first. The pointer, a keyboard focus and a hidden tab hold it still.\nexport default function ShowcaseSlides() {\n return (\n <Showcase\n label=\"How it works\"\n className=\"w-full max-w-md\"\n items={SLIDES}\n getId={(s) => s.id}\n getTitle={(s) => s.title}\n interval={4000}\n transition=\"slide\"\n loop={false}\n renderItem={(s) => <p className=\"m-0 rounded-xl border border-border-default bg-surface-1 p-6 text-fg\">{s.body}</p>}\n />\n );\n}\n"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
"native": {
|
|
190
|
+
"entry": "@krizaka/ui/native",
|
|
191
|
+
"import": "import { Showcase, showcaseLabels } from \"@krizaka/ui/native\";",
|
|
192
|
+
"differences": [
|
|
193
|
+
"Swipeable pages (a paging `FlatList`) instead of a crossfade; the bar, the counter and the buttons are the same.",
|
|
194
|
+
"No hover or keyboard: the autoplay holds while the finger drags, while the app is in the background and with Reduce Motion on."
|
|
195
|
+
],
|
|
196
|
+
"props": [
|
|
197
|
+
{
|
|
198
|
+
"component": "Showcase",
|
|
199
|
+
"description": "An auto-advancing slideshow: swipeable pages, a segmented progress bar that fills over the interval (each segment a button), a counter, previous / next and a pause button.",
|
|
200
|
+
"props": [
|
|
201
|
+
{
|
|
202
|
+
"name": "getId",
|
|
203
|
+
"type": "(item: T) => string",
|
|
204
|
+
"default": null,
|
|
205
|
+
"description": "A stable key for an item.",
|
|
206
|
+
"required": true
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "items",
|
|
210
|
+
"type": "readonly T[]",
|
|
211
|
+
"default": null,
|
|
212
|
+
"description": "The slides.",
|
|
213
|
+
"required": true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "label",
|
|
217
|
+
"type": "string",
|
|
218
|
+
"default": null,
|
|
219
|
+
"description": "The slideshow's accessible name, translated.",
|
|
220
|
+
"required": true
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "renderItem",
|
|
224
|
+
"type": "(item: T, state: { index: number; active: boolean; }) => ReactNode",
|
|
225
|
+
"default": null,
|
|
226
|
+
"description": "One slide's content (it fills the width).",
|
|
227
|
+
"required": true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "autoplay",
|
|
231
|
+
"type": "boolean",
|
|
232
|
+
"default": "true",
|
|
233
|
+
"description": "Advances by itself — never with Reduce Motion on.",
|
|
234
|
+
"required": false
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "getTitle",
|
|
238
|
+
"type": "((item: T) => string)",
|
|
239
|
+
"default": null,
|
|
240
|
+
"description": "A slide's title, shown before the counter.",
|
|
241
|
+
"required": false
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "interval",
|
|
245
|
+
"type": "number",
|
|
246
|
+
"default": "6000",
|
|
247
|
+
"description": "Milliseconds a slide stays.",
|
|
248
|
+
"required": false
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "labels",
|
|
252
|
+
"type": "ShowcaseLabels",
|
|
253
|
+
"default": "{\n previous: \"Previous slide\",\n next: \"Next slide\",\n pause: \"Pause the slideshow\",\n play: \"Play the slideshow\",\n goTo: (n) => `Go to slide ${n}`,\n counter: (n, total) => `Slide ${n} of ${total}`,\n slide: (n, total) => `${n} of ${total}`,\n}",
|
|
254
|
+
"description": "The words of the controls, translated.",
|
|
255
|
+
"required": false
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "loop",
|
|
259
|
+
"type": "boolean",
|
|
260
|
+
"default": "true",
|
|
261
|
+
"description": "After the last slide, back to the first.",
|
|
262
|
+
"required": false
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"name": "onIndexChange",
|
|
266
|
+
"type": "((index: number, user: boolean) => void)",
|
|
267
|
+
"default": null,
|
|
268
|
+
"description": "Called when the slide changes; `user` is true when the person asked for it.",
|
|
269
|
+
"required": false
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"name": "style",
|
|
273
|
+
"type": "StyleProp<ViewStyle>",
|
|
274
|
+
"default": null,
|
|
275
|
+
"description": "Styles merged on the root.",
|
|
276
|
+
"required": false
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
"examples": [
|
|
282
|
+
{
|
|
283
|
+
"name": "slides",
|
|
284
|
+
"title": "Any content",
|
|
285
|
+
"description": "Swipe or wait: the bar fills over 5 s and moves on.",
|
|
286
|
+
"path": "examples/showcase/native/slides.tsx",
|
|
287
|
+
"module": "@krizaka/ui/registry/examples/showcase/native/slides",
|
|
288
|
+
"code": "import { Showcase, Txt } from \"@krizaka/ui/native\";\nimport { View } from \"react-native\";\n\nconst SLIDES = [\n { id: \"1\", title: \"Describe it\", body: \"Write what you want in a sentence.\" },\n { id: \"2\", title: \"Generate it\", body: \"The model runs on your own machine.\" },\n { id: \"3\", title: \"Reproduce it\", body: \"Every result keeps its prompt and its seed.\" },\n];\n\n// Swipe or wait: the bar fills over 5 s and moves on; every segment is a button, the pause button stops it.\nexport default function NativeShowcaseSlides() {\n return (\n <View style={{ width: 360 }}>\n <Showcase\n label=\"How it works\"\n items={SLIDES}\n getId={(s) => s.id}\n getTitle={(s) => s.title}\n interval={5000}\n renderItem={(s) => (\n <View style={{ padding: 24 }}>\n <Txt>{s.body}</Txt>\n </View>\n )}\n />\n </View>\n );\n}\n",
|
|
289
|
+
"screenshots": {
|
|
290
|
+
"dark": "examples/showcase/native/slides.dark.png",
|
|
291
|
+
"light": "examples/showcase/native/slides.light.png"
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
"files": [
|
|
297
|
+
{
|
|
298
|
+
"path": "meta.ts",
|
|
299
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"path": "showcase/index.ts",
|
|
303
|
+
"content": "/** @krizaka/ui/showcase — `Showcase`, an auto-advancing slideshow with a visible progress bar, pause and counter; `useTypedText`. Client. */\nexport { Showcase, type ShowcaseLabels, showcaseLabels, type ShowcaseProps, type ShowcaseSlideState, useTypedText } from \"./showcase\";\n"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"path": "showcase/meta.ts",
|
|
307
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Showcase\",\n summary:\n \"A slideshow that advances by itself and shows it: a segmented progress bar — the current segment fills over the interval, each one a button — with a counter, previous / next and a visible pause button. It waits for the pointer, the keyboard focus, a hidden tab and a scroll away, and never advances under reduced motion.\",\n status: \"beta\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\n \"A gallery of results that should move on its own — prompt and image, before and after, three steps of a product — where each slide is a small composition (a picture, words, an action).\",\n \"A landing page's proof: the reader sees it moving, can stop it and can jump to any slide.\",\n ],\n whenNotToUse: [\n { when: \"For content the reader must compare or read in order: lay the items out.\" },\n { when: \"For a band of logos or short quotes that slides continuously.\", use: \"marquee\" },\n { when: \"For cards to swipe away and decide on.\", use: \"swipe-deck\" },\n ],\n bestPractices: [\n \"Give every slide the same size (an aspect ratio): only the slide in view and its neighbours are mounted, and the height must not jump.\",\n \"Pass `labels` translated — the controls, the counter (“Image 1 of 17”) and each slide's “n of N”; `getTitle` names the slide in the counter.\",\n \"5–8 seconds per slide for a picture and a sentence; the bar shows the time, so the reader can decide to stay.\",\n \"Type a text with `useTypedText` only when it is short, and keep the full text for assistive technology (`sr-only`).\",\n \"The bar's fill is a CSS animation and its end moves the show: its rules ship with the component, nothing to import.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"← / →\", action: \"Previous or next slide (the show then reads the new slide aloud).\" },\n { keys: \"Home / End\", action: \"First or last slide.\" },\n { keys: \"Tab\", action: \"The current segment, previous, pause / play, next, then the content of the slide in view.\" },\n { keys: \"Enter / Space\", action: \"On a segment: that slide. On the pause button: stops or restarts the show.\" },\n ],\n notes: [\n \"A region with `aria-roledescription=\\\"carousel\\\"` named by `label`; each slide is a `group` (`aria-roledescription=\\\"slide\\\"`) named “n of N”; the slides out of view are hidden and inert.\",\n \"WCAG 2.2.2: a visible pause button; the show also stops while the pointer is over it, while a keyboard focus is inside, while the tab is hidden and while it is off screen.\",\n \"The live region is `polite` only after the visitor navigates — the automatic changes are never announced.\",\n \"Under `prefers-reduced-motion` the show never advances by itself: no pause button, the current segment is drawn full, the navigation is manual.\",\n \"On a phone: swipe the pages; Reduce Motion turns the autoplay off; the app in the background holds it.\",\n ],\n },\n related: [\"marquee\", \"swipe-deck\", \"step-progress\", \"tabs\"],\n web: {\n imports: [\"Showcase\", \"useTypedText\", \"showcaseLabels\"],\n examples: [\n { name: \"prompt-image\", title: \"Prompt → image\", description: \"An image on the left, its prompt typed on the right with the action: moves on every 6 s, the bar shows how long is left.\" },\n { name: \"slides\", title: \"Any content\", description: \"Text slides, 4 s each, the slide transition, no loop: the last slide stays.\" },\n ],\n },\n native: {\n imports: [\"Showcase\", \"showcaseLabels\"],\n differences: [\n \"Swipeable pages (a paging `FlatList`) instead of a crossfade; the bar, the counter and the buttons are the same.\",\n \"No hover or keyboard: the autoplay holds while the finger drags, while the app is in the background and with Reduce Motion on.\",\n ],\n examples: [{ name: \"slides\", title: \"Any content\", description: \"Swipe or wait: the bar fills over 5 s and moves on.\" }],\n },\n} satisfies ComponentMeta;\n"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"path": "showcase/showcase.tsx",
|
|
311
|
+
"content": "// Client (state, timers' observers, keys). A slideshow that advances by itself — and shows it: a segmented progress bar\n// (one segment per slide, the current one fills over the interval, each one a button) with a counter, previous / next and a\n// visible pause button (WCAG 2.2.2). The time IS the CSS animation of the current segment (`.kz-showcase-fill`):\n// when it ends, the next slide comes — so what you see filling is exactly what decides, and pausing the animation pauses\n// the show. Its two rules travel with the component (a hoisted, deduplicated <style>): no stylesheet to import. It waits while the pointer is over it, while a keyboard focus is inside, while the tab is hidden and while it\n// is off screen; under reduced motion it never advances by itself. Slides are slots: any content.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\n\nconst FILL_CSS = \".kz-showcase-fill{transform-origin:left;animation:kz-showcase-fill 6s linear forwards}@keyframes kz-showcase-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\";\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst subscribeNothing = () => () => undefined;\nconst readReduced = () => Boolean(typeof window !== \"undefined\" && window.matchMedia?.(REDUCED).matches);\nfunction subscribeReduced(notify: () => void) {\n const query = window.matchMedia?.(REDUCED);\n query?.addEventListener?.(\"change\", notify);\n return () => query?.removeEventListener?.(\"change\", notify);\n}\nconst readHidden = () => document.visibilityState === \"hidden\";\nfunction subscribeVisibility(notify: () => void) {\n document.addEventListener(\"visibilitychange\", notify);\n return () => document.removeEventListener(\"visibilitychange\", notify);\n}\n\nexport interface ShowcaseLabels {\n /** The previous button's name. */\n previous: string;\n /** The next button's name. */\n next: string;\n /** The pause button's name while it plays. */\n pause: string;\n /** The same button's name while it is paused. */\n play: string;\n /** A progress segment's name: `(3, 17) => \"Go to image 3\"`. */\n goTo: (n: number, total: number) => string;\n /** The position in words, beside the title: `(1, 17) => \"Image 1 of 17\"`. */\n counter: (n: number, total: number) => string;\n /** The slide's accessible name: `(1, 17) => \"1 of 17\"`. */\n slide: (n: number, total: number) => string;\n}\n\n/** English defaults — pass your own translated `labels`. */\nexport const showcaseLabels: ShowcaseLabels = {\n previous: \"Previous slide\",\n next: \"Next slide\",\n pause: \"Pause the slideshow\",\n play: \"Play the slideshow\",\n goTo: (n) => `Go to slide ${n}`,\n counter: (n, total) => `Slide ${n} of ${total}`,\n slide: (n, total) => `${n} of ${total}`,\n};\n\nexport interface ShowcaseSlideState {\n index: number;\n /** The slide in view. */\n active: boolean;\n}\n\nexport interface ShowcaseProps<T> extends Omit<React.ComponentProps<\"section\">, \"children\" | \"onChange\"> {\n /** The slideshow's accessible name (“Prompt to image”), passed translated. */\n label: string;\n /** The slides, in order. */\n items: readonly T[];\n /** A stable key for an item. */\n getId: (item: T) => string;\n /** One slide's content. Only the slide in view and its neighbours are mounted (the rest stay empty): give slides the same size. */\n renderItem: (item: T, state: ShowcaseSlideState) => React.ReactNode;\n /** A slide's title, shown before the counter (“Golden-hour editorial”). */\n getTitle?: (item: T) => string;\n /** Milliseconds a slide stays (the time the segment takes to fill). @default 6000 */\n interval?: number;\n /** Advances by itself. Never under `prefers-reduced-motion`. @default true */\n autoplay?: boolean;\n /** `fade` crossfades; `slide` also moves the slides a few pixels. @default \"fade\" */\n transition?: \"fade\" | \"slide\";\n /** After the last slide, go back to the first. @default true */\n loop?: boolean;\n /** The slide in view (controlled). */\n index?: number;\n /** The slide shown first. @default 0 */\n defaultIndex?: number;\n /** Called when the slide in view changes — `user` is true when the visitor asked for it. */\n onIndexChange?: (index: number, user: boolean) => void;\n /** The words of the controls, translated. */\n labels?: ShowcaseLabels;\n}\n\n/** An auto-advancing slideshow with a visible, clickable progress bar, a counter, prev / next and a pause button. */\nexport function Showcase<T>({\n label,\n items,\n getId,\n renderItem,\n getTitle,\n interval = 6000,\n autoplay = true,\n transition = \"fade\",\n loop = true,\n index: controlled,\n defaultIndex = 0,\n onIndexChange,\n labels = showcaseLabels,\n className,\n onKeyDown,\n onFocus,\n onBlur,\n onPointerEnter,\n onPointerLeave,\n ...props\n}: ShowcaseProps<T>) {\n const total = items.length;\n const root = React.useRef<HTMLElement>(null);\n const [inner, setInner] = React.useState(defaultIndex);\n const current = Math.min(Math.max(controlled ?? inner, 0), Math.max(total - 1, 0));\n const mounted = React.useSyncExternalStore(subscribeNothing, () => true, () => false);\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n const hidden = React.useSyncExternalStore(subscribeVisibility, readHidden, () => false);\n const [paused, setPaused] = React.useState(false);\n const [hover, setHover] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const [inView, setInView] = React.useState(true);\n const [announce, setAnnounce] = React.useState(false);\n\n React.useEffect(() => {\n const host = root.current;\n if (!host || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver((entries) => setInView(entries.some((e) => e.isIntersecting)), { threshold: 0.2 });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n const move = (to: number, user: boolean) => {\n if (total === 0) return;\n const next = loop ? (to + total) % total : Math.min(Math.max(to, 0), total - 1);\n if (next === current) return;\n setAnnounce(user);\n if (controlled === undefined) setInner(next);\n onIndexChange?.(next, user);\n };\n\n const playing = autoplay && mounted && !reduced && total > 1;\n const running = playing && !paused && !hover && !focus && !hidden && inView;\n const lastStop = !loop && current === total - 1;\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n onKeyDown?.(event);\n if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;\n const target = event.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const to = event.key === \"ArrowRight\" ? current + 1 : event.key === \"ArrowLeft\" ? current - 1 : event.key === \"Home\" ? 0 : event.key === \"End\" ? total - 1 : null;\n if (to === null) return;\n event.preventDefault();\n move(to, true);\n };\n\n const item = items[current];\n const title = item !== undefined ? getTitle?.(item) : undefined;\n const button = \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-default bg-surface-1 text-fg-secondary transition-colors hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40\";\n\n return (\n <section\n ref={root}\n aria-roledescription=\"carousel\"\n aria-label={label}\n data-running={running ? \"\" : undefined}\n className={cn(\"flex flex-col gap-3\", className)}\n onKeyDown={handleKeyDown}\n onPointerEnter={(e) => {\n onPointerEnter?.(e);\n if (e.pointerType === \"mouse\") setHover(true);\n }}\n onPointerLeave={(e) => {\n onPointerLeave?.(e);\n setHover(false);\n }}\n onFocus={(e) => {\n onFocus?.(e);\n // A mouse click on a button is not a reason to stop: only a keyboard focus is.\n let visible = true;\n try {\n visible = e.target.matches(\":focus-visible\");\n } catch {\n /* a stand-in without :focus-visible */\n }\n if (visible) setFocus(true);\n }}\n onBlur={(e) => {\n onBlur?.(e);\n if (!e.currentTarget.contains(e.relatedTarget)) setFocus(false);\n }}\n {...props}\n >\n <style href=\"kz-showcase\" precedence=\"medium\">\n {FILL_CSS}\n </style>\n <div aria-live={announce ? \"polite\" : \"off\"} className=\"grid\">\n {items.map((slide, i) => {\n const active = i === current;\n const gap = Math.abs(i - current);\n const near = gap <= 1 || (loop && total - gap <= 1);\n const side = i < current ? \"-translate-x-4\" : \"translate-x-4\";\n return (\n <div\n key={getId(slide)}\n role=\"group\"\n aria-roledescription=\"slide\"\n aria-label={labels.slide(i + 1, total)}\n aria-hidden={active ? undefined : true}\n inert={active ? undefined : true}\n data-active={active ? \"\" : undefined}\n className={cn(\n \"col-start-1 row-start-1 min-w-0 transition-[opacity,translate,visibility] duration-500 motion-reduce:transition-none\",\n active ? \"visible opacity-100\" : \"invisible opacity-0\",\n transition === \"slide\" && (active ? \"translate-x-0\" : side),\n )}\n >\n {near ? renderItem(slide, { index: i, active }) : null}\n </div>\n );\n })}\n </div>\n\n <div className=\"flex gap-1\">\n {items.map((slide, i) => (\n <button\n key={getId(slide)}\n type=\"button\"\n aria-label={labels.goTo(i + 1, total)}\n aria-current={i === current ? \"true\" : undefined}\n tabIndex={i === current ? 0 : -1}\n onClick={() => move(i, true)}\n className=\"group/seg relative flex h-6 min-w-0 flex-1 cursor-pointer items-center rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <span className=\"relative block h-1 w-full overflow-hidden rounded-full bg-surface-3 transition-[height] group-hover/seg:h-1.5\">\n {i < current || (i === current && (!playing || lastStop)) ? <span className=\"absolute inset-0 bg-accent\" /> : null}\n {i === current && playing && !lastStop ? (\n <span\n key={`${current}`}\n data-segment-fill\n className=\"kz-showcase-fill absolute inset-0 bg-accent\"\n style={{ animationDuration: `${interval}ms`, animationPlayState: running ? \"running\" : \"paused\" }}\n onAnimationEnd={() => running && move(current + 1, false)}\n />\n ) : null}\n </span>\n </button>\n ))}\n </div>\n\n <div className=\"flex items-center gap-3\">\n <p className=\"m-0 min-w-0 flex-1 truncate text-sm text-fg-secondary\">\n {title ? <span className=\"font-medium text-fg\">{title}</span> : null}\n {title ? <span aria-hidden> · </span> : null}\n <span>{labels.counter(current + 1, total)}</span>\n </p>\n <div className=\"flex items-center gap-1.5\">\n <button type=\"button\" aria-label={labels.previous} disabled={!loop && current === 0} onClick={() => move(current - 1, true)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M15 5l-7 7 7 7\" />\n </svg>\n </button>\n {playing ? (\n <button type=\"button\" aria-label={paused ? labels.play : labels.pause} onClick={() => setPaused(!paused)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"currentColor\" aria-hidden>\n {paused ? <path d=\"M8 5.5v13l10-6.5Z\" /> : <path d=\"M7 5h3.5v14H7zM13.5 5H17v14h-3.5z\" />}\n </svg>\n </button>\n ) : null}\n <button type=\"button\" aria-label={labels.next} disabled={!loop && current === total - 1} onClick={() => move(current + 1, true)} className={button}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n </div>\n </div>\n </section>\n );\n}\n\n/**\n * The typed reveal of a text, kept minimal: the visible prefix grows at `speed` characters per second while `active`;\n * under reduced motion (or when not active) it is the whole text at once. Render the full text for assistive technology\n * (`sr-only`) and this one `aria-hidden`.\n */\nexport function useTypedText(text: string, { active = true, speed = 45 }: { active?: boolean; speed?: number } = {}) {\n const [typed, setTyped] = React.useState({ of: \"\", count: 0 });\n const reduced = React.useSyncExternalStore(subscribeReduced, readReduced, () => false);\n React.useEffect(() => {\n if (!active || reduced) return;\n const id = window.setInterval(() => {\n setTyped((state) => {\n const count = state.of === text ? state.count + 1 : 1;\n if (count >= text.length) window.clearInterval(id);\n return { of: text, count };\n });\n }, Math.max(10, 1000 / speed));\n return () => window.clearInterval(id);\n }, [text, active, reduced, speed]);\n const count = typed.of === text ? typed.count : 0;\n const done = !active || reduced || count >= text.length;\n return { text: done ? text : text.slice(0, count), done };\n}\n"
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
"dependencies": [],
|
|
315
|
+
"registryDependencies": [
|
|
316
|
+
"cn"
|
|
317
|
+
],
|
|
318
|
+
"demo": {
|
|
319
|
+
"path": "examples/showcase/prompt-image.tsx",
|
|
320
|
+
"content": "import { Button } from \"@krizaka/ui/button\";\nimport { Showcase, useTypedText } from \"@krizaka/ui/showcase\";\n\nconst CDN = \"https://krizaka-assets.b-cdn.net/site/people/ui\";\nconst SHOTS = [\n { id: \"a\", title: \"Golden-hour editorial\", prompt: \"A woman on a rooftop at golden hour, soft rim light, 85mm.\", src: `${CDN}/camille-512.9573eaa0b8.webp` },\n { id: \"b\", title: \"Sunday market\", prompt: \"A man choosing peaches at a market stall, warm morning haze.\", src: `${CDN}/tomas-256.7eaab80903.webp` },\n { id: \"c\", title: \"Backstage\", prompt: \"A singer in a dressing-room mirror, tungsten bulbs, grain.\", src: `${CDN}/noor-256.c340c41421.webp` },\n];\n\nfunction Slide({ shot, active }: { shot: (typeof SHOTS)[number]; active: boolean }) {\n const typed = useTypedText(shot.prompt, { active });\n return (\n <div className=\"grid items-center gap-6 sm:grid-cols-2\">\n <img src={shot.src} alt={shot.title} className=\"aspect-square w-full rounded-xl object-cover\" />\n <div className=\"flex flex-col items-start gap-3\">\n <p className=\"sr-only\">{shot.prompt}</p>\n <p aria-hidden className=\"m-0 min-h-20 font-mono text-sm text-fg\">{typed.text}</p>\n <Button variant=\"secondary\" size=\"sm\">Reproduce it</Button>\n </div>\n </div>\n );\n}\n\n// Image left, prompt and action right; the bar below fills over 6 s and moves on, every segment is a button.\nexport default function ShowcasePromptImage() {\n return <Showcase label=\"Prompt to image\" className=\"w-full max-w-3xl\" items={SHOTS} getId={(s) => s.id} getTitle={(s) => s.title} renderItem={(shot, { active }) => <Slide shot={shot} active={active} />} />;\n}\n"
|
|
321
|
+
},
|
|
322
|
+
"props": [
|
|
323
|
+
{
|
|
324
|
+
"component": "Showcase",
|
|
325
|
+
"description": "An auto-advancing slideshow with a visible, clickable progress bar, a counter, prev / next and a pause button.",
|
|
326
|
+
"props": [
|
|
327
|
+
{
|
|
328
|
+
"name": "getId",
|
|
329
|
+
"type": "(item: T) => string",
|
|
330
|
+
"default": null,
|
|
331
|
+
"description": "A stable key for an item.",
|
|
332
|
+
"required": true
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"name": "items",
|
|
336
|
+
"type": "readonly T[]",
|
|
337
|
+
"default": null,
|
|
338
|
+
"description": "The slides, in order.",
|
|
339
|
+
"required": true
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"name": "label",
|
|
343
|
+
"type": "string",
|
|
344
|
+
"default": null,
|
|
345
|
+
"description": "The slideshow's accessible name (“Prompt to image”), passed translated.",
|
|
346
|
+
"required": true
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"name": "renderItem",
|
|
350
|
+
"type": "(item: T, state: ShowcaseSlideState) => ReactNode",
|
|
351
|
+
"default": null,
|
|
352
|
+
"description": "One slide's content. Only the slide in view and its neighbours are mounted (the rest stay empty): give slides the same size.",
|
|
353
|
+
"required": true
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "autoplay",
|
|
357
|
+
"type": "boolean",
|
|
358
|
+
"default": "true",
|
|
359
|
+
"description": "Advances by itself. Never under `prefers-reduced-motion`.",
|
|
360
|
+
"required": false
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"name": "defaultIndex",
|
|
364
|
+
"type": "number",
|
|
365
|
+
"default": "0",
|
|
366
|
+
"description": "The slide shown first.",
|
|
367
|
+
"required": false
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"name": "getTitle",
|
|
371
|
+
"type": "((item: T) => string)",
|
|
372
|
+
"default": null,
|
|
373
|
+
"description": "A slide's title, shown before the counter (“Golden-hour editorial”).",
|
|
374
|
+
"required": false
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"name": "index",
|
|
378
|
+
"type": "number",
|
|
379
|
+
"default": null,
|
|
380
|
+
"description": "The slide in view (controlled).",
|
|
381
|
+
"required": false
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"name": "interval",
|
|
385
|
+
"type": "number",
|
|
386
|
+
"default": "6000",
|
|
387
|
+
"description": "Milliseconds a slide stays (the time the segment takes to fill).",
|
|
388
|
+
"required": false
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"name": "labels",
|
|
392
|
+
"type": "ShowcaseLabels",
|
|
393
|
+
"default": "{\n previous: \"Previous slide\",\n next: \"Next slide\",\n pause: \"Pause the slideshow\",\n play: \"Play the slideshow\",\n goTo: (n) => `Go to slide ${n}`,\n counter: (n, total) => `Slide ${n} of ${total}`,\n slide: (n, total) => `${n} of ${total}`,\n}",
|
|
394
|
+
"description": "The words of the controls, translated.",
|
|
395
|
+
"required": false
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"name": "loop",
|
|
399
|
+
"type": "boolean",
|
|
400
|
+
"default": "true",
|
|
401
|
+
"description": "After the last slide, go back to the first.",
|
|
402
|
+
"required": false
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"name": "onIndexChange",
|
|
406
|
+
"type": "((index: number, user: boolean) => void)",
|
|
407
|
+
"default": null,
|
|
408
|
+
"description": "Called when the slide in view changes — `user` is true when the visitor asked for it.",
|
|
409
|
+
"required": false
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"name": "transition",
|
|
413
|
+
"type": "\"fade\" | \"slide\"",
|
|
414
|
+
"default": "fade",
|
|
415
|
+
"description": "`fade` crossfades; `slide` also moves the slides a few pixels.",
|
|
416
|
+
"required": false
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
]
|
|
421
|
+
}
|