@krizaka/ui 3.45.0 → 3.48.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/account-menu.d.ts +2 -111
- package/dist/account-menu.js +39 -118
- package/dist/account-menu.js.map +1 -1
- package/dist/account-settings.d.ts +163 -0
- package/dist/account-settings.js +354 -0
- package/dist/account-settings.js.map +1 -0
- package/dist/agenda.d.ts +136 -0
- package/dist/agenda.js +90 -0
- package/dist/agenda.js.map +1 -0
- package/dist/avatar-iajODANU.d.ts +114 -0
- package/dist/chunk-3D2YI63L.js +27 -0
- package/dist/chunk-3D2YI63L.js.map +1 -0
- package/dist/chunk-3RVFD3UX.js +85 -0
- package/dist/chunk-3RVFD3UX.js.map +1 -0
- package/dist/chunk-S6NYGOJT.js +22 -0
- package/dist/chunk-S6NYGOJT.js.map +1 -0
- package/dist/docs-layout.js +13 -32
- package/dist/docs-layout.js.map +1 -1
- package/dist/docs-nav.js +1 -1
- package/dist/drop-overlay.d.ts +31 -0
- package/dist/drop-overlay.js +86 -0
- package/dist/drop-overlay.js.map +1 -0
- package/dist/motion.css +27 -0
- package/dist/nav-bar.d.ts +41 -1
- package/dist/nav-bar.js +26 -1
- package/dist/nav-bar.js.map +1 -1
- package/dist/settings-layout.d.ts +59 -4
- package/dist/settings-layout.js +229 -24
- package/dist/settings-layout.js.map +1 -1
- package/dist/upload-queue.d.ts +67 -0
- package/dist/upload-queue.js +76 -0
- package/dist/upload-queue.js.map +1 -0
- package/package.json +31 -5
- package/registry/account-menu.json +2 -2
- package/registry/account-settings.json +721 -0
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/activity-feed.json +1 -1
- package/registry/agenda.json +293 -0
- package/registry/alert.json +1 -1
- package/registry/anatomy-plate.json +1 -1
- package/registry/app-header.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/bird-plate.json +1 -1
- package/registry/button.json +1 -1
- package/registry/captcha.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/chat.json +3 -3
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip-row.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/comparison-table.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/cover-hero.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +3 -3
- 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/docs-nav.json +1 -1
- package/registry/document-viewer.json +1 -1
- package/registry/drop-overlay.json +182 -0
- package/registry/dropdown-menu.json +1 -1
- package/registry/elephant.json +2 -2
- package/registry/empty-state.json +1 -1
- package/registry/engine-flow.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/account-settings/avatar-picker.d.ts +4 -0
- package/registry/examples/account-settings/avatar-picker.js +15 -0
- package/registry/examples/account-settings/avatar-picker.tsx +15 -0
- package/registry/examples/account-settings/choice-cards.d.ts +4 -0
- package/registry/examples/account-settings/choice-cards.js +24 -0
- package/registry/examples/account-settings/choice-cards.tsx +20 -0
- package/registry/examples/account-settings/language-picker.d.ts +4 -0
- package/registry/examples/account-settings/language-picker.js +25 -0
- package/registry/examples/account-settings/language-picker.tsx +21 -0
- package/registry/examples/account-settings/patterns.d.ts +4 -0
- package/registry/examples/account-settings/patterns.js +29 -0
- package/registry/examples/account-settings/patterns.tsx +39 -0
- package/registry/examples/account-settings/profile.d.ts +4 -0
- package/registry/examples/account-settings/profile.js +70 -0
- package/registry/examples/account-settings/profile.tsx +80 -0
- package/registry/examples/agenda/day.d.ts +4 -0
- package/registry/examples/agenda/day.js +43 -0
- package/registry/examples/agenda/day.tsx +35 -0
- package/registry/examples/drop-overlay/default.d.ts +4 -0
- package/registry/examples/drop-overlay/default.js +20 -0
- package/registry/examples/drop-overlay/default.tsx +18 -0
- package/registry/examples/upload-queue/default.d.ts +4 -0
- package/registry/examples/upload-queue/default.js +22 -0
- package/registry/examples/upload-queue/default.tsx +20 -0
- package/registry/examples/upload-queue/tray.d.ts +4 -0
- package/registry/examples/upload-queue/tray.js +21 -0
- package/registry/examples/upload-queue/tray.tsx +19 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/flock.json +1 -1
- package/registry/footnote.json +1 -1
- package/registry/force-update-screen.json +1 -1
- package/registry/form.json +3 -3
- package/registry/gallery-page.json +1 -1
- package/registry/genui.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 +148 -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/language-switch.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/list.json +3 -3
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/maintenance-screen.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 +3 -3
- package/registry/mascot-state.json +3 -3
- package/registry/mascots.json +3 -3
- package/registry/match-dialog.json +1 -1
- package/registry/media-capture.json +1 -1
- package/registry/media-gallery.json +1 -1
- package/registry/media-grid.json +1 -1
- package/registry/meta-row.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +93 -5
- package/registry/number-ticker.json +1 -1
- package/registry/opening-hours.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page-index.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 +93 -5
- package/registry/rail-nav.json +2 -2
- package/registry/recipes.json +1 -1
- package/registry/router.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/service-card.json +1 -1
- package/registry/settings-layout.json +34 -4
- package/registry/showcase.json +1 -1
- package/registry/site-footer.json +2 -2
- package/registry/sites.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/skip-link.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot-picker.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/specimen-tag.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/step-showcase.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/sticky-action.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 +3 -3
- 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/update-prompt.json +1 -1
- package/registry/upload-queue.json +243 -0
- 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
- package/registry/waveform.json +1 -1
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "account-settings",
|
|
3
|
+
"type": "primitive",
|
|
4
|
+
"title": "Account settings",
|
|
5
|
+
"summary": "The blocks of an account page, so every app has the same polished one: the identity card (picture, name, verified e-mail, member since), a picture grid to choose from, a labelled language control, selectable cards for a single choice — beside `SettingsSection`'s one inline save.",
|
|
6
|
+
"description": "The blocks of an account page, so every app has the same polished one: the identity card (picture, name, verified e-mail, member since), a picture grid to choose from, a labelled language control, selectable cards for a single choice — beside `SettingsSection`'s one inline save.",
|
|
7
|
+
"status": "beta",
|
|
8
|
+
"deprecated": null,
|
|
9
|
+
"category": "forms",
|
|
10
|
+
"platforms": "web",
|
|
11
|
+
"parity": {
|
|
12
|
+
"web": {
|
|
13
|
+
"state": "yes"
|
|
14
|
+
},
|
|
15
|
+
"native": {
|
|
16
|
+
"state": "gap",
|
|
17
|
+
"reason": "The Expo apps get the same blocks on @krizaka/ui/native.",
|
|
18
|
+
"issue": "krizaka/krizaka-ui#503"
|
|
19
|
+
},
|
|
20
|
+
"desktop": {
|
|
21
|
+
"state": "yes"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"whenToUse": [
|
|
25
|
+
"The profile page of any Krizaka app (a product's `/settings/profile`, the future `@krizaka/auth`).",
|
|
26
|
+
"`ChoiceCards` for any single choice that deserves a sentence each: a side, a plan, a way to be answered."
|
|
27
|
+
],
|
|
28
|
+
"whenNotToUse": [
|
|
29
|
+
{
|
|
30
|
+
"when": "A choice between a few short words (a view, a size).",
|
|
31
|
+
"use": "radio-group"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"when": "A set of tiles that lead somewhere or show a line-up.",
|
|
35
|
+
"use": "choice-grid"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"when": "The language of a public page: it is a link to the same page in the other language.",
|
|
39
|
+
"use": "language-switch"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"bestPractices": [
|
|
43
|
+
"One save per section (`SettingsSection` `save`): enabled while the section is dirty, « Saved » and Undo after it, the failure as an alert. Never a save above the fields.",
|
|
44
|
+
"`AvatarPicker` is one radiogroup — never a row of circles with loose radios; keep the upload inside its grid, after the pictures.",
|
|
45
|
+
"Name every language in its own language and label the control: « Langue », never an unlabelled switch.",
|
|
46
|
+
"Pass every word translated; the member-since date is formatted by the app in the viewer's language.",
|
|
47
|
+
"Section patterns (security, subscription, storage, my data) are flat sections of `SettingsRow`s: the billing portal is one link, the export one button, the delete a `ConfirmButton`."
|
|
48
|
+
],
|
|
49
|
+
"accessibility": {
|
|
50
|
+
"keyboard": [
|
|
51
|
+
{
|
|
52
|
+
"keys": "← → ↑ ↓",
|
|
53
|
+
"action": "Moves to the next picture, card or language and chooses it (one tab stop per group)."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"keys": "Space",
|
|
57
|
+
"action": "Chooses the focused one."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"keys": "Enter (in a field)",
|
|
61
|
+
"action": "Submits the section's form: saves when it is dirty."
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"notes": [
|
|
65
|
+
"Each group is a named `radiogroup`; the chosen item says `aria-checked`, shows a ring and a check (not colour alone).",
|
|
66
|
+
"A card's description is its `aria-describedby`; a picture is named by its `label`, not its image.",
|
|
67
|
+
"The save result is a polite live region that stays mounted; a failure is `role=\"alert\"`."
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"related": [
|
|
71
|
+
"settings-layout",
|
|
72
|
+
"radio-group",
|
|
73
|
+
"avatar",
|
|
74
|
+
"choice-grid",
|
|
75
|
+
"confirm-button",
|
|
76
|
+
"meter"
|
|
77
|
+
],
|
|
78
|
+
"web": {
|
|
79
|
+
"entry": "@krizaka/ui/account-settings",
|
|
80
|
+
"import": "import { AccountProfileCard, AvatarPicker, ChoiceCards, LanguagePicker } from \"@krizaka/ui/account-settings\";",
|
|
81
|
+
"props": [
|
|
82
|
+
{
|
|
83
|
+
"component": "AccountProfileCard",
|
|
84
|
+
"description": "The account's identity card: picture, name, verified e-mail, member since.",
|
|
85
|
+
"props": [
|
|
86
|
+
{
|
|
87
|
+
"name": "email",
|
|
88
|
+
"type": "string",
|
|
89
|
+
"default": null,
|
|
90
|
+
"description": "The e-mail address.",
|
|
91
|
+
"required": true
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "name",
|
|
95
|
+
"type": "string",
|
|
96
|
+
"default": null,
|
|
97
|
+
"description": "The name (the card's heading).",
|
|
98
|
+
"required": true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "unverifiedLabel",
|
|
102
|
+
"type": "string",
|
|
103
|
+
"default": null,
|
|
104
|
+
"description": "“Not verified” — passed translated.",
|
|
105
|
+
"required": true
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "verified",
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"default": null,
|
|
111
|
+
"description": "Whether the address is verified.",
|
|
112
|
+
"required": true
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "verifiedLabel",
|
|
116
|
+
"type": "string",
|
|
117
|
+
"default": null,
|
|
118
|
+
"description": "“Verified” — passed translated.",
|
|
119
|
+
"required": true
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "action",
|
|
123
|
+
"type": "ReactNode",
|
|
124
|
+
"default": null,
|
|
125
|
+
"description": "Beside the picture: the change action (a `Button`).",
|
|
126
|
+
"required": false
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "avatar",
|
|
130
|
+
"type": "Pick<AvatarProps, \"src\" | \"fallback\" | \"defaultAvatar\">",
|
|
131
|
+
"default": null,
|
|
132
|
+
"description": "The picture: the person's photo, or the default avatar of the brand.",
|
|
133
|
+
"required": false
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "compact",
|
|
137
|
+
"type": "boolean",
|
|
138
|
+
"default": "false",
|
|
139
|
+
"description": "One calm line for a settings row: a medium picture beside the name and the e-mail, no wrapping.",
|
|
140
|
+
"required": false
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "memberSince",
|
|
144
|
+
"type": "string",
|
|
145
|
+
"default": null,
|
|
146
|
+
"description": "“Member since October 2026” — formatted by the app, in the viewer's language.",
|
|
147
|
+
"required": false
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"component": "AvatarPicker",
|
|
153
|
+
"description": "A grid of pictures to choose from, with an upload beside them: a radio group, never a row of loose radios.",
|
|
154
|
+
"props": [
|
|
155
|
+
{
|
|
156
|
+
"name": "label",
|
|
157
|
+
"type": "string",
|
|
158
|
+
"default": null,
|
|
159
|
+
"description": "The group's accessible name (“Profile picture”) — passed translated.",
|
|
160
|
+
"required": true
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "options",
|
|
164
|
+
"type": "readonly AvatarOption[]",
|
|
165
|
+
"default": null,
|
|
166
|
+
"description": "The pictures.",
|
|
167
|
+
"required": true
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "dense",
|
|
171
|
+
"type": "boolean",
|
|
172
|
+
"default": "false",
|
|
173
|
+
"description": "Smaller tiles, for a settings row.",
|
|
174
|
+
"required": false
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "description",
|
|
178
|
+
"type": "ReactNode",
|
|
179
|
+
"default": null,
|
|
180
|
+
"description": "One line under the visible label: what it changes.",
|
|
181
|
+
"required": false
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "onValueChange",
|
|
185
|
+
"type": "((value: string) => void) | ((id: string) => void)",
|
|
186
|
+
"default": null,
|
|
187
|
+
"description": "The viewer chooses another picture.",
|
|
188
|
+
"required": false
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "showLabel",
|
|
192
|
+
"type": "boolean",
|
|
193
|
+
"default": null,
|
|
194
|
+
"description": "Shows the label above the grid, with `description` under it.",
|
|
195
|
+
"required": false
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "upload",
|
|
199
|
+
"type": "ReactNode",
|
|
200
|
+
"default": null,
|
|
201
|
+
"description": "After the pictures, in the grid: the upload action (a `Button`, a file input's label). Not a radio.",
|
|
202
|
+
"required": false
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "value",
|
|
206
|
+
"type": "string | null",
|
|
207
|
+
"default": null,
|
|
208
|
+
"description": "The chosen id (controlled).",
|
|
209
|
+
"required": false
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"component": "ChoiceCards",
|
|
215
|
+
"description": "Cards to choose one from: a radio group where each card has an icon, a title and a description.",
|
|
216
|
+
"props": [
|
|
217
|
+
{
|
|
218
|
+
"name": "label",
|
|
219
|
+
"type": "string",
|
|
220
|
+
"default": null,
|
|
221
|
+
"description": "The question (“Which side of the lease are you?”) — the group's accessible name, passed translated.",
|
|
222
|
+
"required": true
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "options",
|
|
226
|
+
"type": "readonly ChoiceCardOption[]",
|
|
227
|
+
"default": null,
|
|
228
|
+
"description": "The choices.",
|
|
229
|
+
"required": true
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"name": "description",
|
|
233
|
+
"type": "ReactNode",
|
|
234
|
+
"default": null,
|
|
235
|
+
"description": "One line under the visible question.",
|
|
236
|
+
"required": false
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "invalid",
|
|
240
|
+
"type": "boolean",
|
|
241
|
+
"default": null,
|
|
242
|
+
"description": "Marks every radio invalid: aria-invalid (pair it with a `Field.Error`).",
|
|
243
|
+
"required": false
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "onValueChange",
|
|
247
|
+
"type": "((value: string) => void) | ((id: string) => void)",
|
|
248
|
+
"default": null,
|
|
249
|
+
"description": "The viewer chooses another picture.",
|
|
250
|
+
"required": false
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "routeKey",
|
|
254
|
+
"type": "string",
|
|
255
|
+
"default": null,
|
|
256
|
+
"description": "Binds the choice to this search parameter of the URL (`?kind=…`): the URL is the state — for a view's selection (a segmented switch), not a form field.",
|
|
257
|
+
"required": false
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"name": "routeValues",
|
|
261
|
+
"type": "readonly string[]",
|
|
262
|
+
"default": null,
|
|
263
|
+
"description": "The values the parameter may hold (the radios' `value`s); the first is the default unless `defaultValue` says otherwise.",
|
|
264
|
+
"required": false
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "showLabel",
|
|
268
|
+
"type": "boolean",
|
|
269
|
+
"default": null,
|
|
270
|
+
"description": "Shows the question above the cards, with `description` under it.",
|
|
271
|
+
"required": false
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"name": "value",
|
|
275
|
+
"type": "string | null",
|
|
276
|
+
"default": null,
|
|
277
|
+
"description": "The chosen id (controlled).",
|
|
278
|
+
"required": false
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"component": "LanguagePicker",
|
|
284
|
+
"description": "The language control of an account page: a labelled segmented choice, native names.",
|
|
285
|
+
"props": [
|
|
286
|
+
{
|
|
287
|
+
"name": "label",
|
|
288
|
+
"type": "ReactNode",
|
|
289
|
+
"default": null,
|
|
290
|
+
"description": "The visible label (“Language”) — passed translated.",
|
|
291
|
+
"required": true
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"name": "languages",
|
|
295
|
+
"type": "readonly LanguageOption[]",
|
|
296
|
+
"default": null,
|
|
297
|
+
"description": "The languages offered.",
|
|
298
|
+
"required": true
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "description",
|
|
302
|
+
"type": "ReactNode",
|
|
303
|
+
"default": null,
|
|
304
|
+
"description": "One line under it: what it changes.",
|
|
305
|
+
"required": false
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "invalid",
|
|
309
|
+
"type": "boolean",
|
|
310
|
+
"default": null,
|
|
311
|
+
"description": "Marks every radio invalid: aria-invalid (pair it with a `Field.Error`).",
|
|
312
|
+
"required": false
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "onValueChange",
|
|
316
|
+
"type": "((value: string) => void) | ((id: string) => void)",
|
|
317
|
+
"default": null,
|
|
318
|
+
"description": "The viewer chooses another picture.",
|
|
319
|
+
"required": false
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "routeKey",
|
|
323
|
+
"type": "string",
|
|
324
|
+
"default": null,
|
|
325
|
+
"description": "Binds the choice to this search parameter of the URL (`?kind=…`): the URL is the state — for a view's selection (a segmented switch), not a form field.",
|
|
326
|
+
"required": false
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"name": "routeValues",
|
|
330
|
+
"type": "readonly string[]",
|
|
331
|
+
"default": null,
|
|
332
|
+
"description": "The values the parameter may hold (the radios' `value`s); the first is the default unless `defaultValue` says otherwise.",
|
|
333
|
+
"required": false
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "showLabel",
|
|
337
|
+
"type": "boolean",
|
|
338
|
+
"default": "true",
|
|
339
|
+
"description": "Shows the label above the control; `false` when a `SettingsRow` already says it (the group keeps its accessible name).",
|
|
340
|
+
"required": false
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"name": "value",
|
|
344
|
+
"type": "string | null",
|
|
345
|
+
"default": null,
|
|
346
|
+
"description": "The chosen id (controlled).",
|
|
347
|
+
"required": false
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
],
|
|
352
|
+
"examples": [
|
|
353
|
+
{
|
|
354
|
+
"name": "profile",
|
|
355
|
+
"title": "Profile page",
|
|
356
|
+
"description": "A whole profile section: identity, name, pictures, language, side — one save, dirty then saved with Undo.",
|
|
357
|
+
"template": true,
|
|
358
|
+
"purpose": "operate",
|
|
359
|
+
"why": "A person changes who they are in the app in one place, and sees at once that it was kept.",
|
|
360
|
+
"path": "examples/account-settings/profile.tsx",
|
|
361
|
+
"module": "@krizaka/ui/registry/examples/account-settings/profile",
|
|
362
|
+
"code": "import { AccountProfileCard, AvatarPicker, ChoiceCards, LanguagePicker } from \"@krizaka/ui/account-settings\";\nimport { Field, Input } from \"@krizaka/ui/field\";\nimport { SettingsLayout, SettingsRow, SettingsSection, useSaveStatus } from \"@krizaka/ui/settings-layout\";\nimport * as React from \"react\";\n\nconst NAV = [\n { id: \"profile\", label: \"Profil\", href: \"/app/settings/profil\", current: true },\n { id: \"security\", label: \"Sécurité\", href: \"/app/settings/securite\" },\n { id: \"billing\", label: \"Abonnement\", href: \"/app/settings/facturation\" },\n { id: \"storage\", label: \"Stockage\", href: \"/app/settings/stockage\" },\n { id: \"data\", label: \"Mes données\", href: \"/app/settings/donnees\" },\n];\nconst AVATARS = [1, 2, 3, 4, 5, 6].map((tone) => ({ id: `tone-${tone}`, label: `Teinte ${tone}`, defaultAvatar: { brand: \"yomizaka\", tone } as const }));\nconst LABELS = { save: \"Enregistrer\", saving: \"Enregistrement…\", saved: \"Enregistré\", error: \"Impossible d'enregistrer. Réessayez.\", undo: \"Annuler\", discard: \"Rétablir\" };\n\n/** One section's draft: dirty against what is saved, saved with one call, undone to what it was. */\nfunction useDraft<T>(saved: T) {\n const [draft, setDraft] = React.useState(saved);\n const [kept, setKept] = React.useState(saved);\n const [before, setBefore] = React.useState(saved);\n const status = useSaveStatus();\n return {\n draft,\n set: (patch: Partial<T>) => setDraft({ ...draft, ...patch }),\n save: {\n dirty: JSON.stringify(draft) !== JSON.stringify(kept),\n status: status.status,\n labels: LABELS,\n onDiscard: () => setDraft(kept),\n onSave: () => {\n setBefore(kept);\n setKept(draft);\n void status.track(async () => undefined);\n },\n onUndo: () => {\n setDraft(before);\n setKept(before);\n status.reset();\n },\n },\n };\n}\n\n// TEMPLATE — an account's profile page, the Orazaka way: one calm column of rows (label and help left, control right), a\n// section per subject with its own save (dirty → enabled, saved → Undo), the role question as cards in a section of its own.\nexport default function AccountProfile() {\n const profile = useDraft({ name: \"Éric Tremblay\", avatar: \"tone-2\", language: \"fr\" });\n const role = useDraft({ side: \"tenant\" });\n return (\n <SettingsLayout variant=\"flat\" className=\"w-full\" title=\"Réglages\" description=\"Votre profil et la façon dont l'application vous répond.\" nav={NAV} navLabel=\"Sections des réglages\">\n <SettingsSection variant=\"flat\" title=\"Profil\" description=\"Ce que l'application sait de vous.\" save={profile.save}>\n <SettingsRow label={<AccountProfileCard compact name={profile.draft.name} email=\"eric@example.com\" verified verifiedLabel=\"Adresse vérifiée\" unverifiedLabel=\"Adresse non vérifiée\" avatar={{ defaultAvatar: { brand: \"yomizaka\", tone: Number(profile.draft.avatar.slice(5)) } }} />} />\n <SettingsRow label=\"Nom\" description=\"Affiché dans le menu du compte.\" htmlFor=\"profile-name\">\n <Field.Root>\n <Input id=\"profile-name\" value={profile.draft.name} onChange={(event) => profile.set({ name: event.target.value })} />\n </Field.Root>\n </SettingsRow>\n <SettingsRow label=\"Avatar\" description=\"Une des images par défaut.\">\n <AvatarPicker dense label=\"Avatar\" options={AVATARS} value={profile.draft.avatar} onValueChange={(avatar) => profile.set({ avatar })} />\n </SettingsRow>\n <SettingsRow label=\"Langue\" description=\"La langue de l'application et des courriels.\">\n <LanguagePicker showLabel={false} label=\"Langue\" value={profile.draft.language} onValueChange={(language) => profile.set({ language })} languages={[{ code: \"fr\", name: \"Français\" }, { code: \"en\", name: \"English\" }]} />\n </SettingsRow>\n </SettingsSection>\n <SettingsSection variant=\"flat\" title=\"De quel côté du bail êtes-vous ?\" description=\"Cela change les vérifications proposées en premier, pas les règles.\" save={role.save}>\n <div className=\"py-4\">\n <ChoiceCards\n label=\"De quel côté du bail êtes-vous ?\"\n value={role.draft.side}\n onValueChange={(side) => role.set({ side })}\n options={[\n { id: \"tenant\", title: \"Locataire\", description: \"Je vérifie mon bail avant de signer.\" },\n { id: \"landlord\", title: \"Propriétaire\", description: \"Je vérifie mon modèle de bail.\" },\n ]}\n />\n </div>\n </SettingsSection>\n </SettingsLayout>\n );\n}\n"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"name": "avatar-picker",
|
|
366
|
+
"title": "Avatar picker",
|
|
367
|
+
"description": "The family's default pictures as one radio group, the upload beside them.",
|
|
368
|
+
"path": "examples/account-settings/avatar-picker.tsx",
|
|
369
|
+
"module": "@krizaka/ui/registry/examples/account-settings/avatar-picker",
|
|
370
|
+
"code": "import { AvatarPicker } from \"@krizaka/ui/account-settings\";\nimport { Button } from \"@krizaka/ui/button\";\nimport * as React from \"react\";\n\nconst OPTIONS = [1, 2, 3, 4, 5, 6].map((tone) => ({ id: `tone-${tone}`, label: `Teinte ${tone}`, defaultAvatar: { brand: \"yomizaka\", tone } as const }));\n\n// The family's default pictures as one radio group, and the upload beside them: no loose radios.\nexport default function AvatarPickerExample() {\n const [value, setValue] = React.useState(\"tone-2\");\n return (\n <div className=\"w-full max-w-md\">\n <AvatarPicker label=\"Photo de profil\" showLabel options={OPTIONS} value={value} onValueChange={setValue} upload={<Button variant=\"secondary\" size=\"sm\">Téléverser</Button>} />\n </div>\n );\n}\n"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"name": "choice-cards",
|
|
374
|
+
"title": "Choice cards",
|
|
375
|
+
"description": "A single choice as two cards with an icon, a title and a line.",
|
|
376
|
+
"path": "examples/account-settings/choice-cards.tsx",
|
|
377
|
+
"module": "@krizaka/ui/registry/examples/account-settings/choice-cards",
|
|
378
|
+
"code": "import { ChoiceCards } from \"@krizaka/ui/account-settings\";\nimport * as React from \"react\";\n\n// One question, two selectable cards: an icon, a title and a line each, instead of two radios under a heading.\nexport default function ChoiceCardsExample() {\n const [value, setValue] = React.useState(\"tenant\");\n return (\n <div className=\"w-full max-w-xl\">\n <ChoiceCards\n label=\"De quel côté du bail êtes-vous ?\"\n value={value}\n onValueChange={setValue}\n options={[\n { id: \"tenant\", title: \"Locataire\", description: \"Je loue mon logement : je veux savoir ce que je signe.\" },\n { id: \"landlord\", title: \"Propriétaire\", description: \"Je loue à quelqu'un : je veux un bail en ordre.\" },\n ]}\n />\n </div>\n );\n}\n"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"name": "language-picker",
|
|
382
|
+
"title": "Language picker",
|
|
383
|
+
"description": "Labelled, native names.",
|
|
384
|
+
"path": "examples/account-settings/language-picker.tsx",
|
|
385
|
+
"module": "@krizaka/ui/registry/examples/account-settings/language-picker",
|
|
386
|
+
"code": "import { LanguagePicker } from \"@krizaka/ui/account-settings\";\nimport * as React from \"react\";\n\n// The account's language, labelled, each language in its own language.\nexport default function LanguagePickerExample() {\n const [value, setValue] = React.useState(\"fr\");\n return (\n <div className=\"w-full max-w-sm\">\n <LanguagePicker\n label=\"Langue\"\n description=\"La langue de l'application et des courriels.\"\n value={value}\n onValueChange={setValue}\n languages={[\n { code: \"fr\", name: \"Français\" },\n { code: \"en\", name: \"English\" },\n ]}\n />\n </div>\n );\n}\n"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"name": "patterns",
|
|
390
|
+
"title": "Section patterns",
|
|
391
|
+
"description": "Security, subscription, storage and my data as flat sections of rows.",
|
|
392
|
+
"path": "examples/account-settings/patterns.tsx",
|
|
393
|
+
"module": "@krizaka/ui/registry/examples/account-settings/patterns",
|
|
394
|
+
"code": "import { Button } from \"@krizaka/ui/button\";\nimport { ConfirmButton } from \"@krizaka/ui/confirm-button\";\nimport { Meter } from \"@krizaka/ui/meter\";\nimport { SettingsRow, SettingsSection } from \"@krizaka/ui/settings-layout\";\n\n// The four other sections of an account page, each a flat SettingsSection of rows: security, subscription (the billing\n// portal is one link), storage (a Meter), « my data » (export, and a delete that asks twice).\nexport default function AccountSectionPatterns() {\n return (\n <div className=\"flex w-full max-w-2xl flex-col gap-10\">\n <SettingsSection variant=\"flat\" title=\"Sécurité\" description=\"Comment vous vous connectez.\">\n <SettingsRow label=\"Courriel\" description=\"Un lien de confirmation est envoyé à la nouvelle adresse.\">\n eric@example.com <Button variant=\"secondary\" size=\"sm\">Changer</Button>\n </SettingsRow>\n <SettingsRow label=\"Mot de passe\" description=\"Modifié il y a 3 mois.\">\n <Button variant=\"secondary\" size=\"sm\">Changer</Button>\n </SettingsRow>\n </SettingsSection>\n <SettingsSection variant=\"flat\" title=\"Abonnement\" description=\"Votre forfait et vos factures.\">\n <SettingsRow label=\"Forfait Solo\" description=\"Renouvelé le 1er novembre.\">\n <Button variant=\"secondary\" size=\"sm\" asChild><a href=\"https://billing.example.com/portal\">Ouvrir le portail de facturation</a></Button>\n </SettingsRow>\n </SettingsSection>\n <SettingsSection variant=\"flat\" title=\"Stockage\" description=\"L'espace utilisé par vos documents.\">\n <SettingsRow label=\"Documents\">\n <Meter className=\"w-56\" label=\"Stockage\" value={18} max={50} valueText=\"18 Go sur 50 Go\" showLabel />\n </SettingsRow>\n </SettingsSection>\n <SettingsSection variant=\"flat\" title=\"Mes données\" description=\"Ce que vous pouvez emporter ou effacer.\">\n <SettingsRow label=\"Exporter\" description=\"Une archive de tout ce que vous avez téléversé.\">\n <Button variant=\"secondary\" size=\"sm\">Demander l'export</Button>\n </SettingsRow>\n <SettingsRow label=\"Supprimer mon compte\" description=\"Vos documents et votre compte disparaissent, sans retour.\">\n <ConfirmButton variant=\"danger\" size=\"sm\" confirmLabel=\"Supprimer pour de bon ?\" onConfirm={() => {}}>Supprimer</ConfirmButton>\n </SettingsRow>\n </SettingsSection>\n </div>\n );\n}\n"
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
"native": null,
|
|
399
|
+
"files": [
|
|
400
|
+
{
|
|
401
|
+
"path": "account-settings/account-profile-card.tsx",
|
|
402
|
+
"content": "// Server-safe. Who the account is, at a glance: the picture with its change action, the name, the e-mail and whether it is\n// verified, since when. The head of a profile section.\nimport type * as React from \"react\";\n\nimport { Avatar, type AvatarProps } from \"../avatar/avatar\";\nimport { Badge } from \"../badge/badge\";\nimport { cn } from \"../cn\";\n\nexport type AccountProfileCardProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n /** The name (the card's heading). */\n name: string;\n /** The e-mail address. */\n email: string;\n /** Whether the address is verified. */\n verified: boolean;\n /** “Verified” — passed translated. */\n verifiedLabel: string;\n /** “Not verified” — passed translated. */\n unverifiedLabel: string;\n /** “Member since October 2026” — formatted by the app, in the viewer's language. */\n memberSince?: string;\n /** The picture: the person's photo, or the default avatar of the brand. */\n avatar?: Pick<AvatarProps, \"src\" | \"defaultAvatar\" | \"fallback\">;\n /** Beside the picture: the change action (a `Button`). */\n action?: React.ReactNode;\n /**\n * One calm line for a settings row: a medium picture beside the name and the e-mail, no wrapping.\n * @default false\n */\n compact?: boolean;\n};\n\n/** The account's identity card: picture, name, verified e-mail, member since. */\nexport function AccountProfileCard({ name, email, verified, verifiedLabel, unverifiedLabel, memberSince, avatar, action, compact, className, ...props }: AccountProfileCardProps) {\n return (\n <section aria-label={name} className={cn(\"flex items-center gap-3\", !compact && \"flex-col items-start gap-4 sm:flex-row sm:gap-5\", className)} {...props}>\n <div className=\"flex shrink-0 flex-col items-center gap-2\">\n <Avatar size={compact ? \"md\" : \"xl\"} alt=\"\" fallback={name.slice(0, 1)} {...avatar} />\n {action}\n </div>\n <div className=\"flex min-w-0 flex-col gap-1\">\n <h2 className={cn(\"m-0 truncate font-bold text-fg\", compact ? \"text-sm font-medium\" : \"text-lg\")}>{name}</h2>\n <p className=\"m-0 flex flex-wrap items-center gap-2 text-sm text-fg-secondary\">\n <span className=\"min-w-0 truncate\">{email}</span>\n <Badge tone={verified ? \"success\" : \"warning\"} dot>\n {verified ? verifiedLabel : unverifiedLabel}\n </Badge>\n </p>\n {memberSince ? <p className=\"text-sm text-fg-secondary\">{memberSince}</p> : null}\n </div>\n </section>\n );\n}\n"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"path": "account-settings/avatar-picker.tsx",
|
|
406
|
+
"content": "// Client (a radio group). The pictures of a person to choose from — the family's default avatars, a few photos — as a grid of\n// round tiles: one named radiogroup, one tab stop, the arrows choose, a ring and a check on the chosen one.\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { type DefaultAvatar, defaultAvatarUrl } from \"../avatar/default-avatar\";\nimport { cn } from \"../cn\";\nimport { CheckGlyph } from \"../glyphs\";\nimport { GroupHead } from \"./group-head\";\n\nexport interface AvatarOption {\n /** The value of the choice. */\n id: string;\n /** Its name for assistive technology (“Teinte 3”) — passed translated. */\n label: string;\n /** A picture's address; otherwise `defaultAvatar` draws one of the family's pictures. */\n src?: string;\n /** One of Krizaka's default pictures (`{ brand, tone }`). */\n defaultAvatar?: DefaultAvatar;\n}\n\nexport type AvatarPickerProps = Omit<React.ComponentProps<typeof RadioGroupPrimitive.Root>, \"children\" | \"onChange\" | \"dir\"> & {\n /** The group's accessible name (“Profile picture”) — passed translated. */\n label: string;\n /**\n * Smaller tiles, for a settings row.\n * @default false\n */\n dense?: boolean;\n /** Shows the label above the grid, with `description` under it. */\n showLabel?: boolean;\n /** One line under the visible label: what it changes. */\n description?: React.ReactNode;\n /** The pictures. */\n options: readonly AvatarOption[];\n /** The chosen id (controlled). */\n value?: string;\n /** The viewer chooses another picture. */\n onValueChange?: (id: string) => void;\n /** After the pictures, in the grid: the upload action (a `Button`, a file input's label). Not a radio. */\n upload?: React.ReactNode;\n};\n\nconst pictureOf = (option: AvatarOption) => option.src ?? (option.defaultAvatar ? defaultAvatarUrl(option.defaultAvatar, { size: 256 }) : undefined);\n\n/** A grid of pictures to choose from, with an upload beside them: a radio group, never a row of loose radios. */\nexport function AvatarPicker({ label, showLabel, description, dense, options, upload, className, ...props }: AvatarPickerProps) {\n const id = React.useId();\n const group = (\n <RadioGroupPrimitive.Root aria-label={label} aria-labelledby={showLabel ? id : undefined} className={cn(dense ? \"flex flex-wrap gap-2.5\" : \"grid grid-cols-[repeat(auto-fill,minmax(4.25rem,1fr))] gap-3\", !showLabel && className)} {...props}>\n {options.map((option) => (\n <RadioGroupPrimitive.Item\n key={option.id}\n value={option.id}\n aria-label={option.label}\n className={cn(\n \"group/avatar relative shrink-0 rounded-full\", dense ? \"h-12 w-12\" : \"aspect-square w-full\",\n \" outline-none transition-shadow duration-200 motion-reduce:transition-none\",\n \"ring-1 ring-border-default hover:ring-border-strong focus-visible:focus-ring data-[state=checked]:ring-2 data-[state=checked]:ring-accent\",\n )}\n >\n <img src={pictureOf(option)} alt=\"\" draggable={false} className=\"h-full w-full rounded-full bg-surface-3 object-cover\" />\n <span\n aria-hidden\n className=\"absolute -bottom-0.5 -right-0.5 hidden h-5 w-5 items-center justify-center rounded-full border-2 border-surface-1 bg-accent text-on-accent group-data-[state=checked]/avatar:inline-flex\"\n >\n <CheckGlyph className=\"h-3 w-3\" />\n </span>\n </RadioGroupPrimitive.Item>\n ))}\n {upload ? <div className=\"flex aspect-square w-full items-center justify-center\">{upload}</div> : null}\n </RadioGroupPrimitive.Root>\n );\n if (!showLabel) return group;\n return (\n <div className={cn(\"flex flex-col gap-3\", className)}>\n <GroupHead id={id} label={label} description={description} />\n {group}\n </div>\n );\n}\n"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"path": "account-settings/choice-cards.tsx",
|
|
410
|
+
"content": "// Client (a radio group). A single choice as selectable cards — an icon, a title, one line: the side of a lease, a plan, a\n// way to get the answer. Built on `RadioGroup.Card`, so the arrows choose, the card is the radio, the check lights up.\nimport * as React from \"react\";\n\nimport { RadioGroup } from \"../radio-group\";\nimport { GroupHead } from \"./group-head\";\n\nexport interface ChoiceCardOption {\n /** The value of the choice. */\n id: string;\n /** Its name, short. */\n title: React.ReactNode;\n /** One line under the title: what it means. It describes the radio. */\n description?: React.ReactNode;\n /** A glyph before the title (decorative). */\n icon?: React.ReactNode;\n disabled?: boolean;\n}\n\nexport type ChoiceCardsProps = Omit<React.ComponentProps<typeof RadioGroup.Root>, \"children\" | \"variant\" | \"label\"> & {\n /** The question (“Which side of the lease are you?”) — the group's accessible name, passed translated. */\n label: string;\n /** Shows the question above the cards, with `description` under it. */\n showLabel?: boolean;\n /** One line under the visible question. */\n description?: React.ReactNode;\n /** The choices. */\n options: readonly ChoiceCardOption[];\n /** The chosen id (controlled). */\n value?: string;\n /** The viewer chooses another card. */\n onValueChange?: (id: string) => void;\n};\n\n/** Cards to choose one from: a radio group where each card has an icon, a title and a description. */\nexport function ChoiceCards({ label, showLabel, description, options, className, ...props }: ChoiceCardsProps) {\n const base = React.useId();\n const group = (\n <RadioGroup.Root variant=\"cards\" label={label} aria-labelledby={showLabel ? `${base}-label` : undefined} className={[\"grid grid-cols-1 gap-3 sm:grid-cols-2\", !showLabel && className].filter(Boolean).join(\" \")} {...props}>\n {options.map((option) => {\n const describedBy = option.description ? `${base}-${option.id}` : undefined;\n return (\n <RadioGroup.Card key={option.id} value={option.id} disabled={option.disabled} aria-describedby={describedBy}>\n <span className=\"flex items-center gap-2.5 font-semibold text-fg [&>svg]:h-5 [&>svg]:w-5 [&>svg]:text-accent\">\n {option.icon}\n {option.title}\n </span>\n {option.description ? (\n <span id={describedBy} className=\"text-sm font-normal text-fg-secondary\">\n {option.description}\n </span>\n ) : null}\n </RadioGroup.Card>\n );\n })}\n </RadioGroup.Root>\n );\n if (!showLabel) return group;\n return (\n <div className={[\"flex flex-col gap-3\", className].filter(Boolean).join(\" \")}>\n <GroupHead id={`${base}-label`} label={label} description={description} />\n {group}\n </div>\n );\n}\n"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"path": "account-settings/group-head.tsx",
|
|
414
|
+
"content": "import type * as React from \"react\";\n\n/** The visible label (and one line of help) of a radio group: the group's `aria-labelledby` points at it. */\nexport function GroupHead({ id, label, description }: { id: string; label: React.ReactNode; description?: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n <span id={id} className=\"text-sm font-medium text-fg\">\n {label}\n </span>\n {description ? <span className=\"text-sm text-fg-secondary\">{description}</span> : null}\n </div>\n );\n}\n"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"path": "account-settings/index.ts",
|
|
418
|
+
"content": "/** @krizaka/ui/account-settings — the blocks of an account page: `AccountProfileCard`, `AvatarPicker`, `ChoiceCards`, `LanguagePicker`. Client. The inline save of a section is `SettingsSection`'s `save` with `useSaveStatus` (`@krizaka/ui/settings-layout`). */\nexport { AccountProfileCard, type AccountProfileCardProps } from \"./account-profile-card\";\nexport { type AvatarOption, AvatarPicker, type AvatarPickerProps } from \"./avatar-picker\";\nexport { type ChoiceCardOption, ChoiceCards, type ChoiceCardsProps } from \"./choice-cards\";\nexport { type LanguageOption, LanguagePicker, type LanguagePickerProps } from \"./language-picker\";\n"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"path": "account-settings/language-picker.tsx",
|
|
422
|
+
"content": "// Client (a radio group). The language of the account, labelled, each language named in its own language, saved per user\n// (the caller saves on change or with its section). Segmented: one light gliding to the choice.\nimport * as React from \"react\";\n\nimport { RadioGroup } from \"../radio-group\";\nimport { GroupHead } from \"./group-head\";\n\nexport interface LanguageOption {\n /** The BCP 47 code (`fr`, `en`): the value, and the `lang` of the name. */\n code: string;\n /** Its native name (`Français`, `English`) — never the current language's name for it. */\n name: string;\n}\n\nexport type LanguagePickerProps = Omit<React.ComponentProps<typeof RadioGroup.Root>, \"children\" | \"variant\" | \"label\" | \"aria-labelledby\"> & {\n /** The visible label (“Language”) — passed translated. */\n label: React.ReactNode;\n /**\n * Shows the label above the control; `false` when a `SettingsRow` already says it (the group keeps its accessible name).\n * @default true\n */\n showLabel?: boolean;\n /** One line under it: what it changes. */\n description?: React.ReactNode;\n /** The languages offered. */\n languages: readonly LanguageOption[];\n /** The chosen code (controlled). */\n value?: string;\n /** The viewer chooses another language. */\n onValueChange?: (code: string) => void;\n};\n\n/** The language control of an account page: a labelled segmented choice, native names. */\nexport function LanguagePicker({ label, showLabel = true, description, languages, className, ...props }: LanguagePickerProps) {\n const id = React.useId();\n return (\n <div className={[\"flex flex-col gap-2\", className].filter(Boolean).join(\" \")}>\n {showLabel ? <GroupHead id={`${id}-label`} label={label} description={description} /> : null}\n <RadioGroup.Root variant=\"segmented\" aria-label={showLabel ? undefined : String(label)} aria-labelledby={showLabel ? `${id}-label` : undefined} {...props}>\n {languages.map((language) => (\n <RadioGroup.Segment key={language.code} value={language.code} lang={language.code}>\n {language.name}\n </RadioGroup.Segment>\n ))}\n </RadioGroup.Root>\n </div>\n );\n}\n"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"path": "account-settings/meta.ts",
|
|
426
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Account settings\",\n summary:\n \"The blocks of an account page, so every app has the same polished one: the identity card (picture, name, verified e-mail, member since), a picture grid to choose from, a labelled language control, selectable cards for a single choice — beside `SettingsSection`'s one inline save.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"web\",\n parity: {\n native: { status: \"gap\", issue: \"krizaka/krizaka-ui#503\", reason: \"The Expo apps get the same blocks on @krizaka/ui/native.\" },\n desktop: \"web\",\n },\n whenToUse: [\n \"The profile page of any Krizaka app (a product's `/settings/profile`, the future `@krizaka/auth`).\",\n \"`ChoiceCards` for any single choice that deserves a sentence each: a side, a plan, a way to be answered.\",\n ],\n whenNotToUse: [\n { when: \"A choice between a few short words (a view, a size).\", use: \"radio-group\" },\n { when: \"A set of tiles that lead somewhere or show a line-up.\", use: \"choice-grid\" },\n { when: \"The language of a public page: it is a link to the same page in the other language.\", use: \"language-switch\" },\n ],\n bestPractices: [\n \"One save per section (`SettingsSection` `save`): enabled while the section is dirty, « Saved » and Undo after it, the failure as an alert. Never a save above the fields.\",\n \"`AvatarPicker` is one radiogroup — never a row of circles with loose radios; keep the upload inside its grid, after the pictures.\",\n \"Name every language in its own language and label the control: « Langue », never an unlabelled switch.\",\n \"Pass every word translated; the member-since date is formatted by the app in the viewer's language.\",\n \"Section patterns (security, subscription, storage, my data) are flat sections of `SettingsRow`s: the billing portal is one link, the export one button, the delete a `ConfirmButton`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"← → ↑ ↓\", action: \"Moves to the next picture, card or language and chooses it (one tab stop per group).\" },\n { keys: \"Space\", action: \"Chooses the focused one.\" },\n { keys: \"Enter (in a field)\", action: \"Submits the section's form: saves when it is dirty.\" },\n ],\n notes: [\n \"Each group is a named `radiogroup`; the chosen item says `aria-checked`, shows a ring and a check (not colour alone).\",\n \"A card's description is its `aria-describedby`; a picture is named by its `label`, not its image.\",\n \"The save result is a polite live region that stays mounted; a failure is `role=\\\"alert\\\"`.\",\n ],\n },\n related: [\"settings-layout\", \"radio-group\", \"avatar\", \"choice-grid\", \"confirm-button\", \"meter\"],\n web: {\n imports: [\"AccountProfileCard\", \"AvatarPicker\", \"ChoiceCards\", \"LanguagePicker\"],\n examples: [\n { name: \"profile\", title: \"Profile page\", description: \"A whole profile section: identity, name, pictures, language, side — one save, dirty then saved with Undo.\", template: true, purpose: \"operate\", why: \"A person changes who they are in the app in one place, and sees at once that it was kept.\" },\n { name: \"avatar-picker\", title: \"Avatar picker\", description: \"The family's default pictures as one radio group, the upload beside them.\" },\n { name: \"choice-cards\", title: \"Choice cards\", description: \"A single choice as two cards with an icon, a title and a line.\" },\n { name: \"language-picker\", title: \"Language picker\", description: \"Labelled, native names.\" },\n { name: \"patterns\", title: \"Section patterns\", description: \"Security, subscription, storage and my data as flat sections of rows.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"path": "glyphs.tsx",
|
|
430
|
+
"content": "// The small line glyphs the data and media primitives draw (play, volume, search, chevrons…): decorative, 24-unit\n// box, `currentColor`, sized by the caller's class. Shared by the entries that need them; never exported on its own —\n// a product draws its own icons with @krizaka/icons.\nimport type * as React from \"react\";\n\ntype GlyphProps = { className?: string };\n\nfunction Glyph({ className = \"h-4 w-4\", fill, children }: GlyphProps & { fill?: boolean; children: React.ReactNode }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n className={className}\n fill={fill ? \"currentColor\" : \"none\"}\n stroke={fill ? \"none\" : \"currentColor\"}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n focusable=\"false\"\n >\n {children}\n </svg>\n );\n}\n\nexport const PlayGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <path d=\"M8 5.5v13a1 1 0 0 0 1.5.86l10.4-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z\" />\n </Glyph>\n);\nexport const PauseGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </Glyph>\n);\nexport const VolumeGlyph = ({ muted, ...p }: GlyphProps & { muted?: boolean }) => (\n <Glyph {...p}>\n <path d=\"M11 5 6 9H3v6h3l5 4V5Z\" />\n {muted ? <path d=\"m22 9-6 6m0-6 6 6\" /> : <path d=\"M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13\" />}\n </Glyph>\n);\nexport const FullscreenGlyph = ({ exit, ...p }: GlyphProps & { exit?: boolean }) => (\n <Glyph {...p}>\n {exit ? <path d=\"M9 4v5H4m11-5v5h5M9 20v-5H4m11 5v-5h5\" /> : <path d=\"M4 9V4h5m6 0h5v5M4 15v5h5m11-5v5h-5\" />}\n </Glyph>\n);\nexport const CaptionsGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <path d=\"M10 10.5a2 2 0 1 0 0 3M17 10.5a2 2 0 1 0 0 3\" />\n </Glyph>\n);\nexport const PipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <rect x=\"12\" y=\"12\" width=\"6\" height=\"4\" rx=\"1\" />\n </Glyph>\n);\nexport const SearchGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </Glyph>\n);\nexport const CloseGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </Glyph>\n);\nexport const ChevronGlyph = ({ direction = \"down\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" | \"up\" | \"down\" }) => (\n <Glyph {...p}>\n <path d={{ left: \"m15 6-6 6 6 6\", right: \"m9 6 6 6-6 6\", up: \"m6 15 6-6 6 6\", down: \"m6 9 6 6 6-6\" }[direction]} />\n </Glyph>\n);\nexport const CheckGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M20 6 9 17l-5-5\" />\n </Glyph>\n);\nexport const PlusGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 5v14M5 12h14\" />\n </Glyph>\n);\nexport const CalendarGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"16\" rx=\"2\" />\n <path d=\"M3 10h18M8 3v4M16 3v4\" />\n </Glyph>\n);\nexport const UploadGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 16V4m0 0L7 9m5-5 5 5\" />\n <path d=\"M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\" />\n </Glyph>\n);\nexport const FileGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z\" />\n <path d=\"M14 3v5h5\" />\n </Glyph>\n);\nexport const CameraGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z\" />\n <circle cx=\"12\" cy=\"13.5\" r=\"3.5\" />\n </Glyph>\n);\nexport const FlipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3\" />\n <path d=\"M18 3v4h-4M6 21v-4h4\" />\n </Glyph>\n);\nexport const RetryGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 1 0 2.3-5.7L4 8.5\" />\n <path d=\"M4 4v4.5h4.5\" />\n </Glyph>\n);\nexport const SortGlyph = ({ direction, ...p }: GlyphProps & { direction?: \"ascending\" | \"descending\" }) => (\n <Glyph {...p}>\n {direction === \"ascending\" ? <path d=\"m7 14 5-5 5 5\" /> : direction === \"descending\" ? <path d=\"m7 10 5 5 5-5\" /> : <path d=\"m8 9 4-4 4 4M8 15l4 4 4-4\" />}\n </Glyph>\n);\nexport const ArrowGlyph = ({ direction = \"right\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" }) => (\n <Glyph {...p}>\n <path d={direction === \"left\" ? \"M19 12H5m0 0 6-6m-6 6 6 6\" : \"M5 12h14m0 0-6-6m6 6-6 6\"} />\n </Glyph>\n);\nexport const ExternalGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 4h6v6M20 4l-9 9\" />\n <path d=\"M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\" />\n </Glyph>\n);\nexport const WarningGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 4 2.5 20h19L12 4Z\" />\n <path d=\"M12 10v4M12 17.2v.1\" />\n </Glyph>\n);\nexport const CopyGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" />\n <path d=\"M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3\" />\n </Glyph>\n);\n"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"path": "meta.ts",
|
|
434
|
+
"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 | \"agenda\"\n | \"account-settings\"\n | \"drop-overlay\"\n | \"upload-queue\"\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"flock\"\n | \"elephant\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\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 | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\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 | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\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 | \"router\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\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 | \"update-prompt\"\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 * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\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 /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\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"
|
|
435
|
+
}
|
|
436
|
+
],
|
|
437
|
+
"dependencies": [
|
|
438
|
+
"radix-ui@^1.7.0"
|
|
439
|
+
],
|
|
440
|
+
"registryDependencies": [
|
|
441
|
+
"avatar",
|
|
442
|
+
"badge",
|
|
443
|
+
"cn",
|
|
444
|
+
"radio-group"
|
|
445
|
+
],
|
|
446
|
+
"demo": {
|
|
447
|
+
"path": "examples/account-settings/profile.tsx",
|
|
448
|
+
"content": "import { AccountProfileCard, AvatarPicker, ChoiceCards, LanguagePicker } from \"@krizaka/ui/account-settings\";\nimport { Field, Input } from \"@krizaka/ui/field\";\nimport { SettingsLayout, SettingsRow, SettingsSection, useSaveStatus } from \"@krizaka/ui/settings-layout\";\nimport * as React from \"react\";\n\nconst NAV = [\n { id: \"profile\", label: \"Profil\", href: \"/app/settings/profil\", current: true },\n { id: \"security\", label: \"Sécurité\", href: \"/app/settings/securite\" },\n { id: \"billing\", label: \"Abonnement\", href: \"/app/settings/facturation\" },\n { id: \"storage\", label: \"Stockage\", href: \"/app/settings/stockage\" },\n { id: \"data\", label: \"Mes données\", href: \"/app/settings/donnees\" },\n];\nconst AVATARS = [1, 2, 3, 4, 5, 6].map((tone) => ({ id: `tone-${tone}`, label: `Teinte ${tone}`, defaultAvatar: { brand: \"yomizaka\", tone } as const }));\nconst LABELS = { save: \"Enregistrer\", saving: \"Enregistrement…\", saved: \"Enregistré\", error: \"Impossible d'enregistrer. Réessayez.\", undo: \"Annuler\", discard: \"Rétablir\" };\n\n/** One section's draft: dirty against what is saved, saved with one call, undone to what it was. */\nfunction useDraft<T>(saved: T) {\n const [draft, setDraft] = React.useState(saved);\n const [kept, setKept] = React.useState(saved);\n const [before, setBefore] = React.useState(saved);\n const status = useSaveStatus();\n return {\n draft,\n set: (patch: Partial<T>) => setDraft({ ...draft, ...patch }),\n save: {\n dirty: JSON.stringify(draft) !== JSON.stringify(kept),\n status: status.status,\n labels: LABELS,\n onDiscard: () => setDraft(kept),\n onSave: () => {\n setBefore(kept);\n setKept(draft);\n void status.track(async () => undefined);\n },\n onUndo: () => {\n setDraft(before);\n setKept(before);\n status.reset();\n },\n },\n };\n}\n\n// TEMPLATE — an account's profile page, the Orazaka way: one calm column of rows (label and help left, control right), a\n// section per subject with its own save (dirty → enabled, saved → Undo), the role question as cards in a section of its own.\nexport default function AccountProfile() {\n const profile = useDraft({ name: \"Éric Tremblay\", avatar: \"tone-2\", language: \"fr\" });\n const role = useDraft({ side: \"tenant\" });\n return (\n <SettingsLayout variant=\"flat\" className=\"w-full\" title=\"Réglages\" description=\"Votre profil et la façon dont l'application vous répond.\" nav={NAV} navLabel=\"Sections des réglages\">\n <SettingsSection variant=\"flat\" title=\"Profil\" description=\"Ce que l'application sait de vous.\" save={profile.save}>\n <SettingsRow label={<AccountProfileCard compact name={profile.draft.name} email=\"eric@example.com\" verified verifiedLabel=\"Adresse vérifiée\" unverifiedLabel=\"Adresse non vérifiée\" avatar={{ defaultAvatar: { brand: \"yomizaka\", tone: Number(profile.draft.avatar.slice(5)) } }} />} />\n <SettingsRow label=\"Nom\" description=\"Affiché dans le menu du compte.\" htmlFor=\"profile-name\">\n <Field.Root>\n <Input id=\"profile-name\" value={profile.draft.name} onChange={(event) => profile.set({ name: event.target.value })} />\n </Field.Root>\n </SettingsRow>\n <SettingsRow label=\"Avatar\" description=\"Une des images par défaut.\">\n <AvatarPicker dense label=\"Avatar\" options={AVATARS} value={profile.draft.avatar} onValueChange={(avatar) => profile.set({ avatar })} />\n </SettingsRow>\n <SettingsRow label=\"Langue\" description=\"La langue de l'application et des courriels.\">\n <LanguagePicker showLabel={false} label=\"Langue\" value={profile.draft.language} onValueChange={(language) => profile.set({ language })} languages={[{ code: \"fr\", name: \"Français\" }, { code: \"en\", name: \"English\" }]} />\n </SettingsRow>\n </SettingsSection>\n <SettingsSection variant=\"flat\" title=\"De quel côté du bail êtes-vous ?\" description=\"Cela change les vérifications proposées en premier, pas les règles.\" save={role.save}>\n <div className=\"py-4\">\n <ChoiceCards\n label=\"De quel côté du bail êtes-vous ?\"\n value={role.draft.side}\n onValueChange={(side) => role.set({ side })}\n options={[\n { id: \"tenant\", title: \"Locataire\", description: \"Je vérifie mon bail avant de signer.\" },\n { id: \"landlord\", title: \"Propriétaire\", description: \"Je vérifie mon modèle de bail.\" },\n ]}\n />\n </div>\n </SettingsSection>\n </SettingsLayout>\n );\n}\n"
|
|
449
|
+
},
|
|
450
|
+
"props": [
|
|
451
|
+
{
|
|
452
|
+
"component": "AccountProfileCard",
|
|
453
|
+
"description": "The account's identity card: picture, name, verified e-mail, member since.",
|
|
454
|
+
"props": [
|
|
455
|
+
{
|
|
456
|
+
"name": "email",
|
|
457
|
+
"type": "string",
|
|
458
|
+
"default": null,
|
|
459
|
+
"description": "The e-mail address.",
|
|
460
|
+
"required": true
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"name": "name",
|
|
464
|
+
"type": "string",
|
|
465
|
+
"default": null,
|
|
466
|
+
"description": "The name (the card's heading).",
|
|
467
|
+
"required": true
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"name": "unverifiedLabel",
|
|
471
|
+
"type": "string",
|
|
472
|
+
"default": null,
|
|
473
|
+
"description": "“Not verified” — passed translated.",
|
|
474
|
+
"required": true
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"name": "verified",
|
|
478
|
+
"type": "boolean",
|
|
479
|
+
"default": null,
|
|
480
|
+
"description": "Whether the address is verified.",
|
|
481
|
+
"required": true
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"name": "verifiedLabel",
|
|
485
|
+
"type": "string",
|
|
486
|
+
"default": null,
|
|
487
|
+
"description": "“Verified” — passed translated.",
|
|
488
|
+
"required": true
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
"name": "action",
|
|
492
|
+
"type": "ReactNode",
|
|
493
|
+
"default": null,
|
|
494
|
+
"description": "Beside the picture: the change action (a `Button`).",
|
|
495
|
+
"required": false
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"name": "avatar",
|
|
499
|
+
"type": "Pick<AvatarProps, \"src\" | \"fallback\" | \"defaultAvatar\">",
|
|
500
|
+
"default": null,
|
|
501
|
+
"description": "The picture: the person's photo, or the default avatar of the brand.",
|
|
502
|
+
"required": false
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"name": "compact",
|
|
506
|
+
"type": "boolean",
|
|
507
|
+
"default": "false",
|
|
508
|
+
"description": "One calm line for a settings row: a medium picture beside the name and the e-mail, no wrapping.",
|
|
509
|
+
"required": false
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"name": "memberSince",
|
|
513
|
+
"type": "string",
|
|
514
|
+
"default": null,
|
|
515
|
+
"description": "“Member since October 2026” — formatted by the app, in the viewer's language.",
|
|
516
|
+
"required": false
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"component": "AvatarPicker",
|
|
522
|
+
"description": "A grid of pictures to choose from, with an upload beside them: a radio group, never a row of loose radios.",
|
|
523
|
+
"props": [
|
|
524
|
+
{
|
|
525
|
+
"name": "label",
|
|
526
|
+
"type": "string",
|
|
527
|
+
"default": null,
|
|
528
|
+
"description": "The group's accessible name (“Profile picture”) — passed translated.",
|
|
529
|
+
"required": true
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"name": "options",
|
|
533
|
+
"type": "readonly AvatarOption[]",
|
|
534
|
+
"default": null,
|
|
535
|
+
"description": "The pictures.",
|
|
536
|
+
"required": true
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"name": "dense",
|
|
540
|
+
"type": "boolean",
|
|
541
|
+
"default": "false",
|
|
542
|
+
"description": "Smaller tiles, for a settings row.",
|
|
543
|
+
"required": false
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"name": "description",
|
|
547
|
+
"type": "ReactNode",
|
|
548
|
+
"default": null,
|
|
549
|
+
"description": "One line under the visible label: what it changes.",
|
|
550
|
+
"required": false
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"name": "onValueChange",
|
|
554
|
+
"type": "((value: string) => void) | ((id: string) => void)",
|
|
555
|
+
"default": null,
|
|
556
|
+
"description": "The viewer chooses another picture.",
|
|
557
|
+
"required": false
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"name": "showLabel",
|
|
561
|
+
"type": "boolean",
|
|
562
|
+
"default": null,
|
|
563
|
+
"description": "Shows the label above the grid, with `description` under it.",
|
|
564
|
+
"required": false
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
"name": "upload",
|
|
568
|
+
"type": "ReactNode",
|
|
569
|
+
"default": null,
|
|
570
|
+
"description": "After the pictures, in the grid: the upload action (a `Button`, a file input's label). Not a radio.",
|
|
571
|
+
"required": false
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"name": "value",
|
|
575
|
+
"type": "string | null",
|
|
576
|
+
"default": null,
|
|
577
|
+
"description": "The chosen id (controlled).",
|
|
578
|
+
"required": false
|
|
579
|
+
}
|
|
580
|
+
]
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"component": "ChoiceCards",
|
|
584
|
+
"description": "Cards to choose one from: a radio group where each card has an icon, a title and a description.",
|
|
585
|
+
"props": [
|
|
586
|
+
{
|
|
587
|
+
"name": "label",
|
|
588
|
+
"type": "string",
|
|
589
|
+
"default": null,
|
|
590
|
+
"description": "The question (“Which side of the lease are you?”) — the group's accessible name, passed translated.",
|
|
591
|
+
"required": true
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
"name": "options",
|
|
595
|
+
"type": "readonly ChoiceCardOption[]",
|
|
596
|
+
"default": null,
|
|
597
|
+
"description": "The choices.",
|
|
598
|
+
"required": true
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"name": "description",
|
|
602
|
+
"type": "ReactNode",
|
|
603
|
+
"default": null,
|
|
604
|
+
"description": "One line under the visible question.",
|
|
605
|
+
"required": false
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
"name": "invalid",
|
|
609
|
+
"type": "boolean",
|
|
610
|
+
"default": null,
|
|
611
|
+
"description": "Marks every radio invalid: aria-invalid (pair it with a `Field.Error`).",
|
|
612
|
+
"required": false
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"name": "onValueChange",
|
|
616
|
+
"type": "((value: string) => void) | ((id: string) => void)",
|
|
617
|
+
"default": null,
|
|
618
|
+
"description": "The viewer chooses another picture.",
|
|
619
|
+
"required": false
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
"name": "routeKey",
|
|
623
|
+
"type": "string",
|
|
624
|
+
"default": null,
|
|
625
|
+
"description": "Binds the choice to this search parameter of the URL (`?kind=…`): the URL is the state — for a view's selection (a segmented switch), not a form field.",
|
|
626
|
+
"required": false
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
"name": "routeValues",
|
|
630
|
+
"type": "readonly string[]",
|
|
631
|
+
"default": null,
|
|
632
|
+
"description": "The values the parameter may hold (the radios' `value`s); the first is the default unless `defaultValue` says otherwise.",
|
|
633
|
+
"required": false
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"name": "showLabel",
|
|
637
|
+
"type": "boolean",
|
|
638
|
+
"default": null,
|
|
639
|
+
"description": "Shows the question above the cards, with `description` under it.",
|
|
640
|
+
"required": false
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
"name": "value",
|
|
644
|
+
"type": "string | null",
|
|
645
|
+
"default": null,
|
|
646
|
+
"description": "The chosen id (controlled).",
|
|
647
|
+
"required": false
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
"component": "LanguagePicker",
|
|
653
|
+
"description": "The language control of an account page: a labelled segmented choice, native names.",
|
|
654
|
+
"props": [
|
|
655
|
+
{
|
|
656
|
+
"name": "label",
|
|
657
|
+
"type": "ReactNode",
|
|
658
|
+
"default": null,
|
|
659
|
+
"description": "The visible label (“Language”) — passed translated.",
|
|
660
|
+
"required": true
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"name": "languages",
|
|
664
|
+
"type": "readonly LanguageOption[]",
|
|
665
|
+
"default": null,
|
|
666
|
+
"description": "The languages offered.",
|
|
667
|
+
"required": true
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
"name": "description",
|
|
671
|
+
"type": "ReactNode",
|
|
672
|
+
"default": null,
|
|
673
|
+
"description": "One line under it: what it changes.",
|
|
674
|
+
"required": false
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
"name": "invalid",
|
|
678
|
+
"type": "boolean",
|
|
679
|
+
"default": null,
|
|
680
|
+
"description": "Marks every radio invalid: aria-invalid (pair it with a `Field.Error`).",
|
|
681
|
+
"required": false
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
"name": "onValueChange",
|
|
685
|
+
"type": "((value: string) => void) | ((id: string) => void)",
|
|
686
|
+
"default": null,
|
|
687
|
+
"description": "The viewer chooses another picture.",
|
|
688
|
+
"required": false
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"name": "routeKey",
|
|
692
|
+
"type": "string",
|
|
693
|
+
"default": null,
|
|
694
|
+
"description": "Binds the choice to this search parameter of the URL (`?kind=…`): the URL is the state — for a view's selection (a segmented switch), not a form field.",
|
|
695
|
+
"required": false
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
"name": "routeValues",
|
|
699
|
+
"type": "readonly string[]",
|
|
700
|
+
"default": null,
|
|
701
|
+
"description": "The values the parameter may hold (the radios' `value`s); the first is the default unless `defaultValue` says otherwise.",
|
|
702
|
+
"required": false
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"name": "showLabel",
|
|
706
|
+
"type": "boolean",
|
|
707
|
+
"default": "true",
|
|
708
|
+
"description": "Shows the label above the control; `false` when a `SettingsRow` already says it (the group keeps its accessible name).",
|
|
709
|
+
"required": false
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
"name": "value",
|
|
713
|
+
"type": "string | null",
|
|
714
|
+
"default": null,
|
|
715
|
+
"description": "The chosen id (controlled).",
|
|
716
|
+
"required": false
|
|
717
|
+
}
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
]
|
|
721
|
+
}
|