@krizaka/ui 2.3.0 → 2.4.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 +16 -1
- package/dist/audio-player.d.ts +113 -0
- package/dist/audio-player.js +207 -0
- package/dist/audio-player.js.map +1 -0
- package/dist/chunk-2USC2YT6.js +17 -0
- package/dist/chunk-2USC2YT6.js.map +1 -0
- package/dist/chunk-EUPTXKH7.js +95 -0
- package/dist/chunk-EUPTXKH7.js.map +1 -0
- package/dist/chunk-RUPRYJ45.js +82 -0
- package/dist/chunk-RUPRYJ45.js.map +1 -0
- package/dist/chunk-XUMJA5ZI.js +283 -0
- package/dist/chunk-XUMJA5ZI.js.map +1 -0
- package/dist/chunk-YH4LMWRU.js +92 -0
- package/dist/chunk-YH4LMWRU.js.map +1 -0
- package/dist/chunk-YX6CNVJA.js +94 -0
- package/dist/chunk-YX6CNVJA.js.map +1 -0
- package/dist/chunk-ZIXB33X7.js +95 -0
- package/dist/chunk-ZIXB33X7.js.map +1 -0
- package/dist/combobox.d.ts +188 -0
- package/dist/combobox.js +16 -0
- package/dist/combobox.js.map +1 -0
- package/dist/data-table.d.ts +246 -0
- package/dist/data-table.js +290 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-picker.d.ts +288 -0
- package/dist/date-picker.js +364 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/file-upload.d.ts +238 -0
- package/dist/file-upload.js +437 -0
- package/dist/file-upload.js.map +1 -0
- package/dist/filter-bar.d.ts +139 -0
- package/dist/filter-bar.js +155 -0
- package/dist/filter-bar.js.map +1 -0
- package/dist/image-cropper.d.ts +115 -0
- package/dist/image-cropper.js +166 -0
- package/dist/image-cropper.js.map +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-capture.d.ts +155 -0
- package/dist/media-capture.js +242 -0
- package/dist/media-capture.js.map +1 -0
- package/dist/native/media-capture.d.ts +49 -0
- package/dist/native/media-capture.js +113 -0
- package/dist/native/media-capture.js.map +1 -0
- package/dist/native/video-player.d.ts +50 -0
- package/dist/native/video-player.js +69 -0
- package/dist/native/video-player.js.map +1 -0
- package/dist/native.d.ts +66 -2
- package/dist/native.js +166 -68
- package/dist/native.js.map +1 -1
- package/dist/pagination.d.ts +141 -0
- package/dist/pagination.js +165 -0
- package/dist/pagination.js.map +1 -0
- package/dist/search-field.d.ts +100 -0
- package/dist/search-field.js +167 -0
- package/dist/search-field.js.map +1 -0
- package/dist/tag-input.d.ts +44 -0
- package/dist/tag-input.js +67 -0
- package/dist/tag-input.js.map +1 -0
- package/dist/video-player.d.ts +204 -0
- package/dist/video-player.js +371 -0
- package/dist/video-player.js.map +1 -0
- package/dist/video-trimmer.d.ts +142 -0
- package/dist/video-trimmer.js +193 -0
- package/dist/video-trimmer.js.map +1 -0
- package/package.json +232 -4
- package/registry/alert.json +1 -1
- package/registry/audio-player.json +282 -0
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/combobox.json +475 -0
- package/registry/command.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/data-table.json +500 -0
- package/registry/date-picker.json +978 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/examples/audio-player/podcast.d.ts +4 -0
- package/registry/examples/audio-player/podcast.js +22 -0
- package/registry/examples/audio-player/podcast.tsx +19 -0
- package/registry/examples/audio-player/voice-note.d.ts +4 -0
- package/registry/examples/audio-player/voice-note.js +12 -0
- package/registry/examples/audio-player/voice-note.tsx +12 -0
- package/registry/examples/combobox/async.d.ts +4 -0
- package/registry/examples/combobox/async.js +23 -0
- package/registry/examples/combobox/async.tsx +22 -0
- package/registry/examples/combobox/creatable.d.ts +4 -0
- package/registry/examples/combobox/creatable.js +32 -0
- package/registry/examples/combobox/creatable.tsx +28 -0
- package/registry/examples/combobox/invalid.d.ts +4 -0
- package/registry/examples/combobox/invalid.js +23 -0
- package/registry/examples/combobox/invalid.tsx +17 -0
- package/registry/examples/combobox/multiple.d.ts +4 -0
- package/registry/examples/combobox/multiple.js +29 -0
- package/registry/examples/combobox/multiple.tsx +26 -0
- package/registry/examples/data-table/cards.d.ts +4 -0
- package/registry/examples/data-table/cards.js +21 -0
- package/registry/examples/data-table/cards.tsx +20 -0
- package/registry/examples/data-table/empty.d.ts +4 -0
- package/registry/examples/data-table/empty.js +26 -0
- package/registry/examples/data-table/empty.tsx +21 -0
- package/registry/examples/data-table/error.d.ts +4 -0
- package/registry/examples/data-table/error.js +28 -0
- package/registry/examples/data-table/error.tsx +29 -0
- package/registry/examples/data-table/jobs.d.ts +4 -0
- package/registry/examples/data-table/jobs.js +37 -0
- package/registry/examples/data-table/jobs.tsx +36 -0
- package/registry/examples/data-table/loading.d.ts +4 -0
- package/registry/examples/data-table/loading.js +16 -0
- package/registry/examples/data-table/loading.tsx +12 -0
- package/registry/examples/data-table/virtualized.d.ts +4 -0
- package/registry/examples/data-table/virtualized.js +21 -0
- package/registry/examples/data-table/virtualized.tsx +20 -0
- package/registry/examples/date-picker/calendar.d.ts +4 -0
- package/registry/examples/date-picker/calendar.js +14 -0
- package/registry/examples/date-picker/calendar.tsx +16 -0
- package/registry/examples/date-picker/french.d.ts +4 -0
- package/registry/examples/date-picker/french.js +20 -0
- package/registry/examples/date-picker/french.tsx +14 -0
- package/registry/examples/date-picker/open.d.ts +4 -0
- package/registry/examples/date-picker/open.js +11 -0
- package/registry/examples/date-picker/open.tsx +8 -0
- package/registry/examples/date-picker/range.d.ts +4 -0
- package/registry/examples/date-picker/range.js +29 -0
- package/registry/examples/date-picker/range.tsx +25 -0
- package/registry/examples/date-picker/schedule.d.ts +4 -0
- package/registry/examples/date-picker/schedule.js +26 -0
- package/registry/examples/date-picker/schedule.tsx +22 -0
- package/registry/examples/file-upload/images.d.ts +4 -0
- package/registry/examples/file-upload/images.js +32 -0
- package/registry/examples/file-upload/images.tsx +25 -0
- package/registry/examples/file-upload/states.d.ts +4 -0
- package/registry/examples/file-upload/states.js +28 -0
- package/registry/examples/file-upload/states.tsx +23 -0
- package/registry/examples/file-upload/tus.d.ts +4 -0
- package/registry/examples/file-upload/tus.js +23 -0
- package/registry/examples/file-upload/tus.tsx +26 -0
- package/registry/examples/file-upload/video.d.ts +4 -0
- package/registry/examples/file-upload/video.js +39 -0
- package/registry/examples/file-upload/video.tsx +36 -0
- package/registry/examples/filter-bar/address.d.ts +4 -0
- package/registry/examples/filter-bar/address.js +29 -0
- package/registry/examples/filter-bar/address.tsx +27 -0
- package/registry/examples/filter-bar/jobs.d.ts +4 -0
- package/registry/examples/filter-bar/jobs.js +37 -0
- package/registry/examples/filter-bar/jobs.tsx +37 -0
- package/registry/examples/filter-bar/menu.d.ts +4 -0
- package/registry/examples/filter-bar/menu.js +27 -0
- package/registry/examples/filter-bar/menu.tsx +23 -0
- package/registry/examples/image-cropper/avatar.d.ts +4 -0
- package/registry/examples/image-cropper/avatar.js +13 -0
- package/registry/examples/image-cropper/avatar.tsx +14 -0
- package/registry/examples/image-cropper/banner.d.ts +4 -0
- package/registry/examples/image-cropper/banner.js +11 -0
- package/registry/examples/image-cropper/banner.tsx +10 -0
- package/registry/examples/image-cropper/vertical-video.d.ts +4 -0
- package/registry/examples/image-cropper/vertical-video.js +12 -0
- package/registry/examples/image-cropper/vertical-video.tsx +16 -0
- package/registry/examples/media-capture/clip.d.ts +4 -0
- package/registry/examples/media-capture/clip.js +20 -0
- package/registry/examples/media-capture/clip.tsx +18 -0
- package/registry/examples/media-capture/native/photo.d.ts +4 -0
- package/registry/examples/media-capture/native/photo.dark.png +0 -0
- package/registry/examples/media-capture/native/photo.js +27 -0
- package/registry/examples/media-capture/native/photo.light.png +0 -0
- package/registry/examples/media-capture/native/photo.tsx +20 -0
- package/registry/examples/media-capture/profile-photo.d.ts +4 -0
- package/registry/examples/media-capture/profile-photo.js +24 -0
- package/registry/examples/media-capture/profile-photo.tsx +20 -0
- package/registry/examples/pagination/cursor.d.ts +4 -0
- package/registry/examples/pagination/cursor.js +24 -0
- package/registry/examples/pagination/cursor.tsx +20 -0
- package/registry/examples/pagination/links.d.ts +4 -0
- package/registry/examples/pagination/links.js +11 -0
- package/registry/examples/pagination/links.tsx +6 -0
- package/registry/examples/pagination/native/cursor.d.ts +4 -0
- package/registry/examples/pagination/native/cursor.dark.png +0 -0
- package/registry/examples/pagination/native/cursor.js +19 -0
- package/registry/examples/pagination/native/cursor.light.png +0 -0
- package/registry/examples/pagination/native/cursor.tsx +13 -0
- package/registry/examples/pagination/native/pages.d.ts +4 -0
- package/registry/examples/pagination/native/pages.dark.png +0 -0
- package/registry/examples/pagination/native/pages.js +21 -0
- package/registry/examples/pagination/native/pages.light.png +0 -0
- package/registry/examples/pagination/native/pages.tsx +14 -0
- package/registry/examples/pagination/results.d.ts +4 -0
- package/registry/examples/pagination/results.js +23 -0
- package/registry/examples/pagination/results.tsx +19 -0
- package/registry/examples/pagination/table-footer.d.ts +4 -0
- package/registry/examples/pagination/table-footer.js +31 -0
- package/registry/examples/pagination/table-footer.tsx +27 -0
- package/registry/examples/search-field/global.d.ts +4 -0
- package/registry/examples/search-field/global.js +19 -0
- package/registry/examples/search-field/global.tsx +15 -0
- package/registry/examples/search-field/native/default.d.ts +4 -0
- package/registry/examples/search-field/native/default.dark.png +0 -0
- package/registry/examples/search-field/native/default.js +13 -0
- package/registry/examples/search-field/native/default.light.png +0 -0
- package/registry/examples/search-field/native/default.tsx +7 -0
- package/registry/examples/search-field/suggestions.d.ts +4 -0
- package/registry/examples/search-field/suggestions.js +30 -0
- package/registry/examples/search-field/suggestions.tsx +27 -0
- package/registry/examples/search-field/toolbar.d.ts +4 -0
- package/registry/examples/search-field/toolbar.js +17 -0
- package/registry/examples/search-field/toolbar.tsx +21 -0
- package/registry/examples/tag-input/recipients.d.ts +4 -0
- package/registry/examples/tag-input/recipients.js +23 -0
- package/registry/examples/tag-input/recipients.tsx +20 -0
- package/registry/examples/tag-input/video-tags.d.ts +4 -0
- package/registry/examples/tag-input/video-tags.js +26 -0
- package/registry/examples/tag-input/video-tags.tsx +22 -0
- package/registry/examples/video-player/default.d.ts +4 -0
- package/registry/examples/video-player/default.js +12 -0
- package/registry/examples/video-player/default.tsx +8 -0
- package/registry/examples/video-player/gated.d.ts +4 -0
- package/registry/examples/video-player/gated.js +26 -0
- package/registry/examples/video-player/gated.tsx +23 -0
- package/registry/examples/video-player/native/default.d.ts +4 -0
- package/registry/examples/video-player/native/default.dark.png +0 -0
- package/registry/examples/video-player/native/default.js +18 -0
- package/registry/examples/video-player/native/default.light.png +0 -0
- package/registry/examples/video-player/native/default.tsx +12 -0
- package/registry/examples/video-player/signed-source.d.ts +4 -0
- package/registry/examples/video-player/signed-source.js +16 -0
- package/registry/examples/video-player/signed-source.tsx +15 -0
- package/registry/examples/video-player/vertical.d.ts +4 -0
- package/registry/examples/video-player/vertical.js +12 -0
- package/registry/examples/video-player/vertical.tsx +12 -0
- package/registry/examples/video-trimmer/custom.d.ts +4 -0
- package/registry/examples/video-trimmer/custom.js +24 -0
- package/registry/examples/video-trimmer/custom.tsx +26 -0
- package/registry/examples/video-trimmer/filmstrip.d.ts +4 -0
- package/registry/examples/video-trimmer/filmstrip.js +15 -0
- package/registry/examples/video-trimmer/filmstrip.tsx +13 -0
- package/registry/examples/video-trimmer/playhead.d.ts +4 -0
- package/registry/examples/video-trimmer/playhead.js +15 -0
- package/registry/examples/video-trimmer/playhead.tsx +13 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +471 -0
- package/registry/filter-bar.json +347 -0
- package/registry/glow-card.json +1 -1
- package/registry/image-cropper.json +313 -0
- package/registry/index.json +255 -1
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/media-capture.json +347 -0
- package/registry/meter.json +3 -3
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/pagination.json +475 -0
- package/registry/popover.json +1 -1
- package/registry/progress.json +5 -5
- package/registry/radio-group.json +1 -1
- package/registry/search-field.json +436 -0
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +363 -0
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +598 -0
- package/registry/video-trimmer.json +385 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "media-capture",
|
|
3
|
+
"type": "primitive",
|
|
4
|
+
"title": "Media capture",
|
|
5
|
+
"summary": "The camera in the page: a photo or a clip, front or back, the permission asked only on a gesture, and a way out every time the camera cannot start.",
|
|
6
|
+
"description": "The camera in the page: a photo or a clip, front or back, the permission asked only on a gesture, and a way out every time the camera cannot start.",
|
|
7
|
+
"status": "beta",
|
|
8
|
+
"category": "media",
|
|
9
|
+
"platforms": "both",
|
|
10
|
+
"whenToUse": [
|
|
11
|
+
"To take a profile picture or a selfie for a verification without leaving the page.",
|
|
12
|
+
"To record a short clip (a story, a reply, a proof) with a maximum length."
|
|
13
|
+
],
|
|
14
|
+
"whenNotToUse": [
|
|
15
|
+
{
|
|
16
|
+
"when": "To send files the person already has: a drop zone.",
|
|
17
|
+
"use": "file-upload"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"when": "For a long recording with editing: record, then trim with the video trimmer.",
|
|
21
|
+
"use": "video-trimmer"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"bestPractices": [
|
|
25
|
+
"Say why the camera is needed in `labels.prompt` before asking: the browser's prompt then makes sense.",
|
|
26
|
+
"Never ask on load: `autoStart` starts only when the permission is already granted.",
|
|
27
|
+
"Set `maxDuration` for clips and show the limit; send the blob with the file upload's queue.",
|
|
28
|
+
"In React Native, import it from `@krizaka/ui/native/media-capture` (expo-camera, an optional peer)."
|
|
29
|
+
],
|
|
30
|
+
"accessibility": {
|
|
31
|
+
"keyboard": [
|
|
32
|
+
{
|
|
33
|
+
"keys": "Enter / Space",
|
|
34
|
+
"action": "Turn the camera on, take the photo, start or stop the recording."
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"notes": [
|
|
38
|
+
"A `region` named by `labels.region`; the preview is decorative, the shutter is a button named by its action.",
|
|
39
|
+
"A refusal or a missing camera is announced (`role=\"alert\"`) with the file picker as the way out.",
|
|
40
|
+
"The recording time is a `timer`; under reduced motion the recording dot does not pulse."
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"related": [
|
|
44
|
+
"file-upload",
|
|
45
|
+
"image-cropper",
|
|
46
|
+
"video-player"
|
|
47
|
+
],
|
|
48
|
+
"web": {
|
|
49
|
+
"entry": "@krizaka/ui/media-capture",
|
|
50
|
+
"import": "import { MediaCapture } from \"@krizaka/ui/media-capture\";",
|
|
51
|
+
"props": [
|
|
52
|
+
{
|
|
53
|
+
"component": "MediaCapture",
|
|
54
|
+
"description": "The camera in the page: photo or clip, front or back, with a file-picker way out when the camera cannot start.",
|
|
55
|
+
"props": [
|
|
56
|
+
{
|
|
57
|
+
"name": "onCapture",
|
|
58
|
+
"type": "(result: CaptureResult) => void",
|
|
59
|
+
"default": null,
|
|
60
|
+
"description": "Called with a photo taken or a clip recorded (or a file picked in the fallback).",
|
|
61
|
+
"required": true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "aspectRatio",
|
|
65
|
+
"type": "string",
|
|
66
|
+
"default": "3 / 4",
|
|
67
|
+
"description": "The width / height ratio of the preview. Default `\"3 / 4\"`.",
|
|
68
|
+
"required": false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "audio",
|
|
72
|
+
"type": "boolean",
|
|
73
|
+
"default": "true",
|
|
74
|
+
"description": "Records the microphone with a clip. Default `true`.",
|
|
75
|
+
"required": false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "autoStart",
|
|
79
|
+
"type": "boolean",
|
|
80
|
+
"default": null,
|
|
81
|
+
"description": "Starts the camera at mount (only when the permission is already granted — never a prompt on load).",
|
|
82
|
+
"required": false
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "className",
|
|
86
|
+
"type": "string",
|
|
87
|
+
"default": null,
|
|
88
|
+
"description": "Classes merged on the root.",
|
|
89
|
+
"required": false
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "facing",
|
|
93
|
+
"type": "\"user\" | \"environment\"",
|
|
94
|
+
"default": "user",
|
|
95
|
+
"description": "The camera at first: `user` (front, mirrored, default) or `environment` (back).",
|
|
96
|
+
"required": false
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "labels",
|
|
100
|
+
"type": "Partial<MediaCaptureLabels>",
|
|
101
|
+
"default": null,
|
|
102
|
+
"description": "The words, passed translated; English by default.",
|
|
103
|
+
"required": false
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "maxDuration",
|
|
107
|
+
"type": "number",
|
|
108
|
+
"default": null,
|
|
109
|
+
"description": "The longest clip, in seconds: recording stops by itself.",
|
|
110
|
+
"required": false
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "mode",
|
|
114
|
+
"type": "\"video\" | \"both\" | \"photo\"",
|
|
115
|
+
"default": "photo",
|
|
116
|
+
"description": "What it captures: `photo` (default), `video`, or `both` (a switch above the preview).",
|
|
117
|
+
"required": false
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "onError",
|
|
121
|
+
"type": "((error: unknown) => void)",
|
|
122
|
+
"default": null,
|
|
123
|
+
"description": "Called when the camera cannot start (`NotAllowedError`, `NotFoundError`…).",
|
|
124
|
+
"required": false
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"examples": [
|
|
130
|
+
{
|
|
131
|
+
"name": "profile-photo",
|
|
132
|
+
"title": "Profile photo",
|
|
133
|
+
"description": "The front camera, a photo: the prompt explains before the browser asks.",
|
|
134
|
+
"path": "examples/media-capture/profile-photo.tsx",
|
|
135
|
+
"module": "@krizaka/ui/registry/examples/media-capture/profile-photo",
|
|
136
|
+
"code": "\"use client\";\n\nimport { MediaCapture } from \"@krizaka/ui/media-capture\";\nimport * as React from \"react\";\n\n// The front camera, a photo; the browser asks only when the button is pressed.\nexport default function MediaCaptureProfilePhoto() {\n const [photo, setPhoto] = React.useState<string | null>(null);\n return (\n <div className=\"flex w-72 flex-col gap-3\">\n <MediaCapture\n mode=\"photo\"\n facing=\"user\"\n onCapture={(result) => setPhoto(result.url)}\n labels={{ prompt: \"Take a profile picture with your camera.\", start: \"Turn on the camera\" }}\n />\n {photo && <img src={photo} alt=\"Your new profile picture\" className=\"h-20 w-20 rounded-full object-cover\" />}\n </div>\n );\n}\n"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "clip",
|
|
140
|
+
"title": "Photo or clip",
|
|
141
|
+
"description": "`mode=\"both\"`: a switch above the preview, clips up to 60 seconds.",
|
|
142
|
+
"path": "examples/media-capture/clip.tsx",
|
|
143
|
+
"module": "@krizaka/ui/registry/examples/media-capture/clip",
|
|
144
|
+
"code": "\"use client\";\n\nimport { MediaCapture } from \"@krizaka/ui/media-capture\";\n\n// A photo or a clip of 60 seconds at most, for a story; the blob goes to the upload queue.\nexport default function MediaCaptureClip() {\n return (\n <div className=\"w-72\">\n <MediaCapture\n mode=\"both\"\n maxDuration={60}\n aspectRatio=\"9 / 16\"\n onCapture={(result) => console.info(result.kind, result.blob.size)}\n labels={{ prompt: \"Film a story of up to 60 seconds, or take a photo.\" }}\n />\n </div>\n );\n}\n"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
"native": {
|
|
149
|
+
"entry": "@krizaka/ui/native/media-capture",
|
|
150
|
+
"import": "import { MediaCapture } from \"@krizaka/ui/native/media-capture\";",
|
|
151
|
+
"differences": [
|
|
152
|
+
"Its own entry, `@krizaka/ui/native/media-capture`, on expo-camera (an optional peer).",
|
|
153
|
+
"`facing` is `front` or `back`; `onCapture` receives a file `uri`, not a blob; a refusal for good opens the Settings.",
|
|
154
|
+
"`mode` is `photo` or `video`; `labels` is required."
|
|
155
|
+
],
|
|
156
|
+
"props": [
|
|
157
|
+
{
|
|
158
|
+
"component": "MediaCapture",
|
|
159
|
+
"description": "The camera in a screen, on expo-camera: asks for the permission only when the person presses the button, sends them\nto Settings when it was refused for good, takes a photo or records a clip (and the microphone for a clip), switches\ncameras. The web `MediaCapture` with the same words.",
|
|
160
|
+
"props": [
|
|
161
|
+
{
|
|
162
|
+
"name": "labels",
|
|
163
|
+
"type": "MediaCaptureLabels",
|
|
164
|
+
"default": null,
|
|
165
|
+
"description": "The words, passed translated.",
|
|
166
|
+
"required": true
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"name": "onCapture",
|
|
170
|
+
"type": "(result: NativeCaptureResult) => void",
|
|
171
|
+
"default": null,
|
|
172
|
+
"description": "Called with the photo taken or the clip recorded.",
|
|
173
|
+
"required": true
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "aspectRatio",
|
|
177
|
+
"type": "number",
|
|
178
|
+
"default": "3 / 4",
|
|
179
|
+
"description": "The width / height ratio of the preview. Default 3 / 4.",
|
|
180
|
+
"required": false
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "facing",
|
|
184
|
+
"type": "\"front\" | \"back\"",
|
|
185
|
+
"default": "front",
|
|
186
|
+
"description": "The camera at first: `front` (default) or `back`.",
|
|
187
|
+
"required": false
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "maxDuration",
|
|
191
|
+
"type": "number",
|
|
192
|
+
"default": null,
|
|
193
|
+
"description": "The longest clip, in seconds.",
|
|
194
|
+
"required": false
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"name": "mode",
|
|
198
|
+
"type": "\"video\" | \"photo\"",
|
|
199
|
+
"default": "photo",
|
|
200
|
+
"description": "What it captures: `photo` (default) or `video`.",
|
|
201
|
+
"required": false
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "onError",
|
|
205
|
+
"type": "((error: unknown) => void)",
|
|
206
|
+
"default": null,
|
|
207
|
+
"description": "Called when a capture fails.",
|
|
208
|
+
"required": false
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "style",
|
|
212
|
+
"type": "StyleProp<ViewStyle>",
|
|
213
|
+
"default": null,
|
|
214
|
+
"description": "Styles merged last, on the frame.",
|
|
215
|
+
"required": false
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"examples": [
|
|
221
|
+
{
|
|
222
|
+
"name": "photo",
|
|
223
|
+
"title": "Photo",
|
|
224
|
+
"description": "Before the permission: why, and the button that asks.",
|
|
225
|
+
"path": "examples/media-capture/native/photo.tsx",
|
|
226
|
+
"module": "@krizaka/ui/registry/examples/media-capture/native/photo",
|
|
227
|
+
"code": "import { MediaCapture } from \"@krizaka/ui/native/media-capture\";\n\nexport default function NativeMediaCapturePhoto() {\n return (\n <MediaCapture\n mode=\"photo\"\n onCapture={({ uri }) => console.info(uri)}\n labels={{\n prompt: \"Take a profile picture with your camera.\",\n start: \"Turn on the camera\",\n denied: \"Camera access is off. Turn it on in Settings.\",\n settings: \"Open Settings\",\n takePhoto: \"Take photo\",\n startRecording: \"Start recording\",\n stopRecording: \"Stop recording\",\n switchCamera: \"Switch camera\",\n }}\n />\n );\n}\n",
|
|
228
|
+
"screenshots": {
|
|
229
|
+
"dark": "examples/media-capture/native/photo.dark.png",
|
|
230
|
+
"light": "examples/media-capture/native/photo.light.png"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"files": [
|
|
236
|
+
{
|
|
237
|
+
"path": "glyphs.tsx",
|
|
238
|
+
"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);\n"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"path": "media-capture/index.ts",
|
|
242
|
+
"content": "/** @krizaka/ui/media-capture — `MediaCapture` (camera in the page: photo or clip, front / back, permission and file-picker fallback), `mediaCapture` (the slots). Client (\"use client\"). */\nexport { type CaptureResult, MediaCapture, mediaCapture, type MediaCaptureLabels, mediaCaptureLabels, type MediaCaptureProps } from \"./media-capture\";\n"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"path": "media-capture/media-capture.tsx",
|
|
246
|
+
"content": "// Client. The camera in the page: asks for it only when the person presses the button (never on load), shows the\n// preview (mirrored for the front camera), takes a photo (a canvas → JPEG `Blob`) or records a clip (MediaRecorder,\n// a timer, a maximum length), switches cameras. Every dead end has a way out: permission refused, no camera, no\n// `getUserMedia` (an old browser, an insecure page) → the file picker with `capture`, which opens the phone's own\n// camera. The tracks stop when it unmounts or the page is hidden.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { CameraGlyph, FlipGlyph } from \"../glyphs\";\nimport { formatClock, playSafely, usePrefersReducedMotion } from \"../media\";\n\nexport const mediaCapture = tv({\n slots: {\n root: \"theme-dark relative isolate flex w-full flex-col overflow-hidden rounded-xl bg-media text-fg-on-media\",\n stage: \"relative w-full\",\n video: \"absolute inset-0 h-full w-full object-cover data-[mirrored]:-scale-x-100\",\n panel: \"absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-center text-sm\",\n panelIcon: \"flex h-12 w-12 items-center justify-center rounded-full bg-fg-on-media/15\",\n primary:\n \"inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-accent px-4 text-sm font-semibold text-on-accent hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-media\",\n secondary:\n \"inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-fg-on-media/40 px-4 text-sm font-semibold text-fg-on-media hover:bg-fg-on-media/10 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n bar: \"absolute inset-x-0 bottom-0 flex items-center justify-center gap-6 bg-linear-to-t from-scrim-strong to-transparent px-4 pb-4 pt-10\",\n shutter:\n \"relative inline-flex h-16 w-16 items-center justify-center rounded-full border-4 border-fg-on-media bg-transparent \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-media \" +\n \"after:h-12 after:w-12 after:rounded-full after:bg-fg-on-media after:transition-all motion-reduce:after:transition-none \" +\n \"data-[mode=video]:after:bg-danger data-[recording]:after:h-6 data-[recording]:after:w-6 data-[recording]:after:rounded-md\",\n round:\n \"inline-flex h-11 w-11 items-center justify-center rounded-full bg-scrim-strong text-fg-on-media hover:bg-scrim \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:invisible\",\n modes: \"absolute inset-x-0 top-3 flex justify-center\",\n mode:\n \"rounded-full px-3 py-1 text-xs font-semibold text-fg-on-media/80 aria-pressed:bg-fg-on-media aria-pressed:text-media \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n timer:\n \"absolute left-3 top-3 inline-flex items-center gap-1.5 rounded-full bg-scrim-strong px-2.5 py-1 font-mono text-xs tabular-nums \" +\n \"before:h-2 before:w-2 before:rounded-full before:bg-danger before:animate-pulse motion-reduce:before:animate-none\",\n },\n});\n\nexport type CaptureResult = {\n /** The photo (JPEG) or the clip (WebM / MP4, as the browser records). */\n blob: Blob;\n /** `photo` or `video`. */\n kind: \"photo\" | \"video\";\n /** An object URL of the blob, to preview it (revoke it when done). */\n url: string;\n};\n\nexport type MediaCaptureLabels = {\n /** The accessible name of the camera (\"Camera\"). */\n region: string;\n /** Why the camera is asked for, before the request (\"Use your camera to take a photo\"). */\n prompt: string;\n /** The button that asks (\"Turn on the camera\"). */\n start: string;\n /** Permission refused (\"Camera access is blocked. Allow it in your browser settings, or choose a file.\"). */\n denied: string;\n /** No camera, or no access possible here (\"No camera is available.\"). */\n unavailable: string;\n /** The way out: the file picker (\"Choose a file\"). */\n fallback: string;\n takePhoto: string;\n startRecording: string;\n stopRecording: string;\n switchCamera: string;\n photoMode: string;\n videoMode: string;\n};\n\nexport const mediaCaptureLabels: MediaCaptureLabels = {\n region: \"Camera\",\n prompt: \"Use your camera to take a photo or record a clip.\",\n start: \"Turn on the camera\",\n denied: \"Camera access is blocked. Allow it in your browser settings, or choose a file.\",\n unavailable: \"No camera is available here.\",\n fallback: \"Choose a file\",\n takePhoto: \"Take photo\",\n startRecording: \"Start recording\",\n stopRecording: \"Stop recording\",\n switchCamera: \"Switch camera\",\n photoMode: \"Photo\",\n videoMode: \"Video\",\n};\n\nexport type MediaCaptureProps = {\n /** What it captures: `photo` (default), `video`, or `both` (a switch above the preview). */\n mode?: \"photo\" | \"video\" | \"both\";\n /** The camera at first: `user` (front, mirrored, default) or `environment` (back). */\n facing?: \"user\" | \"environment\";\n /** Records the microphone with a clip. Default `true`. */\n audio?: boolean;\n /** The longest clip, in seconds: recording stops by itself. */\n maxDuration?: number;\n /** Starts the camera at mount (only when the permission is already granted — never a prompt on load). */\n autoStart?: boolean;\n /** Called with a photo taken or a clip recorded (or a file picked in the fallback). */\n onCapture: (result: CaptureResult) => void;\n /** Called when the camera cannot start (`NotAllowedError`, `NotFoundError`…). */\n onError?: (error: unknown) => void;\n /** The width / height ratio of the preview. Default `\"3 / 4\"`. */\n aspectRatio?: string;\n /** The words, passed translated; English by default. */\n labels?: Partial<MediaCaptureLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\ntype State = \"idle\" | \"starting\" | \"live\" | \"denied\" | \"unavailable\";\n\nconst supported = () => typeof navigator !== \"undefined\" && Boolean(navigator.mediaDevices?.getUserMedia);\nconst recorderType = () =>\n typeof MediaRecorder === \"undefined\" ? undefined : [\"video/webm;codecs=vp9,opus\", \"video/webm\", \"video/mp4\"].find((t) => MediaRecorder.isTypeSupported?.(t));\n\n/** The camera in the page: photo or clip, front or back, with a file-picker way out when the camera cannot start. */\nexport function MediaCapture({\n mode = \"photo\",\n facing: initialFacing = \"user\",\n audio = true,\n maxDuration,\n autoStart,\n onCapture,\n onError,\n aspectRatio = \"3 / 4\",\n labels: given,\n className,\n}: MediaCaptureProps) {\n const labels = { ...mediaCaptureLabels, ...given };\n const s = mediaCapture();\n const reduce = usePrefersReducedMotion();\n const video = React.useRef<HTMLVideoElement>(null);\n const stream = React.useRef<MediaStream | null>(null);\n const recorder = React.useRef<MediaRecorder | null>(null);\n const file = React.useRef<HTMLInputElement>(null);\n const [state, setState] = React.useState<State>(\"idle\");\n const [facing, setFacing] = React.useState(initialFacing);\n const [kind, setKind] = React.useState<\"photo\" | \"video\">(mode === \"video\" ? \"video\" : \"photo\");\n const [recording, setRecording] = React.useState(false);\n const [elapsed, setElapsed] = React.useState(0);\n const [cameras, setCameras] = React.useState(0);\n\n const stop = React.useCallback(() => {\n if (recorder.current?.state === \"recording\") recorder.current.stop();\n stream.current?.getTracks().forEach((track) => track.stop());\n stream.current = null;\n }, []);\n\n const start = React.useCallback(\n async (side: \"user\" | \"environment\") => {\n if (!supported()) {\n setState(\"unavailable\");\n return;\n }\n setState(\"starting\");\n stop();\n try {\n const media = await navigator.mediaDevices.getUserMedia({ video: { facingMode: side }, audio: mode !== \"photo\" && audio });\n stream.current = media;\n if (video.current) {\n video.current.srcObject = media;\n playSafely(video.current);\n }\n const devices = await navigator.mediaDevices.enumerateDevices?.().catch(() => []);\n setCameras((devices ?? []).filter((d) => d.kind === \"videoinput\").length);\n setState(\"live\");\n } catch (error) {\n const name = (error as { name?: string })?.name;\n setState(name === \"NotAllowedError\" || name === \"SecurityError\" ? \"denied\" : \"unavailable\");\n onError?.(error);\n }\n },\n [audio, mode, onError, stop],\n );\n\n React.useEffect(() => {\n if (!autoStart || !supported() || !navigator.permissions?.query) return;\n let live = true;\n navigator.permissions\n .query({ name: \"camera\" as PermissionName })\n .then((status) => {\n if (live && status.state === \"granted\") void start(initialFacing);\n })\n .catch(() => undefined);\n return () => {\n live = false;\n };\n }, [autoStart, initialFacing, start]);\n\n React.useEffect(() => {\n const hide = () => {\n if (!document.hidden) return;\n stop();\n setState((current) => (current === \"live\" ? \"idle\" : current));\n };\n document.addEventListener(\"visibilitychange\", hide);\n return () => {\n document.removeEventListener(\"visibilitychange\", hide);\n stop();\n };\n }, [stop]);\n\n React.useEffect(() => {\n if (!recording) return;\n const began = Date.now();\n const timer = setInterval(() => {\n const seconds = (Date.now() - began) / 1000;\n setElapsed(seconds);\n if (maxDuration && seconds >= maxDuration) recorder.current?.stop();\n }, 250);\n return () => clearInterval(timer);\n }, [recording, maxDuration]);\n\n const deliver = (blob: Blob, captured: \"photo\" | \"video\") => onCapture({ blob, kind: captured, url: URL.createObjectURL(blob) });\n\n const takePhoto = () => {\n const element = video.current;\n if (!element?.videoWidth) return;\n const canvas = document.createElement(\"canvas\");\n canvas.width = element.videoWidth;\n canvas.height = element.videoHeight;\n const context = canvas.getContext(\"2d\");\n if (!context) return;\n if (facing === \"user\") {\n context.translate(canvas.width, 0);\n context.scale(-1, 1);\n }\n context.drawImage(element, 0, 0);\n canvas.toBlob((blob) => blob && deliver(blob, \"photo\"), \"image/jpeg\", 0.92);\n };\n\n const toggleRecording = () => {\n if (recording) {\n recorder.current?.stop();\n return;\n }\n if (!stream.current) return;\n const chunks: Blob[] = [];\n const type = recorderType();\n const next = new MediaRecorder(stream.current, type ? { mimeType: type } : undefined);\n next.ondataavailable = (event) => event.data.size && chunks.push(event.data);\n next.onstop = () => {\n setRecording(false);\n setElapsed(0);\n deliver(new Blob(chunks, { type: next.mimeType || type || \"video/webm\" }), \"video\");\n };\n recorder.current = next;\n next.start(1000);\n setRecording(true);\n };\n\n const flip = () => {\n const side = facing === \"user\" ? \"environment\" : \"user\";\n setFacing(side);\n void start(side);\n };\n\n const accept = mode === \"video\" ? \"video/*\" : mode === \"photo\" ? \"image/*\" : \"image/*,video/*\";\n const fallback = (\n <>\n <button type=\"button\" className={s.secondary()} onClick={() => file.current?.click()}>\n {labels.fallback}\n </button>\n <input\n ref={file}\n type=\"file\"\n accept={accept}\n capture={facing}\n tabIndex={-1}\n aria-hidden\n className=\"sr-only\"\n onChange={(event) => {\n const picked = event.target.files?.[0];\n if (picked) deliver(picked, picked.type.startsWith(\"video/\") ? \"video\" : \"photo\");\n event.target.value = \"\";\n }}\n />\n </>\n );\n\n return (\n <section aria-label={labels.region} className={s.root({ className })}>\n <div className={s.stage()} style={{ aspectRatio }}>\n <video ref={video} muted playsInline aria-hidden data-mirrored={facing === \"user\" ? \"\" : undefined} className={s.video()} hidden={state !== \"live\"} />\n {state !== \"live\" && (\n <div className={s.panel()} role={state === \"denied\" || state === \"unavailable\" ? \"alert\" : undefined}>\n <span className={s.panelIcon()} aria-hidden>\n <CameraGlyph className=\"h-6 w-6\" />\n </span>\n <p className=\"max-w-xs\">{state === \"denied\" ? labels.denied : state === \"unavailable\" ? labels.unavailable : labels.prompt}</p>\n <div className=\"flex flex-wrap justify-center gap-2\">\n {(state === \"idle\" || state === \"starting\") && (\n <button type=\"button\" className={s.primary()} disabled={state === \"starting\"} onClick={() => void start(facing)}>\n <CameraGlyph className=\"h-4 w-4\" />\n {labels.start}\n </button>\n )}\n {fallback}\n </div>\n </div>\n )}\n {state === \"live\" && mode === \"both\" && !recording && (\n <div className={s.modes()}>\n <div className=\"flex gap-1 rounded-full bg-scrim-strong p-1\">\n <button type=\"button\" aria-pressed={kind === \"photo\"} className={s.mode()} onClick={() => setKind(\"photo\")}>\n {labels.photoMode}\n </button>\n <button type=\"button\" aria-pressed={kind === \"video\"} className={s.mode()} onClick={() => setKind(\"video\")}>\n {labels.videoMode}\n </button>\n </div>\n </div>\n )}\n {recording && (\n <span className={s.timer()} role=\"timer\" aria-live=\"off\">\n {formatClock(elapsed)}\n {maxDuration ? ` / ${formatClock(maxDuration)}` : \"\"}\n </span>\n )}\n {state === \"live\" && (\n <div className={s.bar()}>\n <span className=\"h-11 w-11\" />\n <button\n type=\"button\"\n className={s.shutter()}\n data-mode={kind}\n data-recording={recording ? \"\" : undefined}\n data-reduce={reduce ? \"\" : undefined}\n aria-label={kind === \"photo\" ? labels.takePhoto : recording ? labels.stopRecording : labels.startRecording}\n onClick={kind === \"photo\" ? takePhoto : toggleRecording}\n />\n <button type=\"button\" className={s.round()} aria-label={labels.switchCamera} disabled={cameras < 2 || recording} onClick={flip}>\n <FlipGlyph className=\"h-5 w-5\" />\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"path": "media-capture/meta.ts",
|
|
250
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Media capture\",\n summary: \"The camera in the page: a photo or a clip, front or back, the permission asked only on a gesture, and a way out every time the camera cannot start.\",\n status: \"beta\",\n category: \"media\",\n platforms: \"both\",\n whenToUse: [\n \"To take a profile picture or a selfie for a verification without leaving the page.\",\n \"To record a short clip (a story, a reply, a proof) with a maximum length.\",\n ],\n whenNotToUse: [\n { when: \"To send files the person already has: a drop zone.\", use: \"file-upload\" },\n { when: \"For a long recording with editing: record, then trim with the video trimmer.\", use: \"video-trimmer\" },\n ],\n bestPractices: [\n \"Say why the camera is needed in `labels.prompt` before asking: the browser's prompt then makes sense.\",\n \"Never ask on load: `autoStart` starts only when the permission is already granted.\",\n \"Set `maxDuration` for clips and show the limit; send the blob with the file upload's queue.\",\n \"In React Native, import it from `@krizaka/ui/native/media-capture` (expo-camera, an optional peer).\",\n ],\n accessibility: {\n keyboard: [{ keys: \"Enter / Space\", action: \"Turn the camera on, take the photo, start or stop the recording.\" }],\n notes: [\n \"A `region` named by `labels.region`; the preview is decorative, the shutter is a button named by its action.\",\n \"A refusal or a missing camera is announced (`role=\\\"alert\\\"`) with the file picker as the way out.\",\n \"The recording time is a `timer`; under reduced motion the recording dot does not pulse.\",\n ],\n },\n related: [\"file-upload\", \"image-cropper\", \"video-player\"],\n web: {\n imports: [\"MediaCapture\"],\n examples: [\n { name: \"profile-photo\", title: \"Profile photo\", description: \"The front camera, a photo: the prompt explains before the browser asks.\" },\n { name: \"clip\", title: \"Photo or clip\", description: \"`mode=\\\"both\\\"`: a switch above the preview, clips up to 60 seconds.\" },\n ],\n },\n native: {\n entry: \"@krizaka/ui/native/media-capture\",\n imports: [\"MediaCapture\"],\n differences: [\n \"Its own entry, `@krizaka/ui/native/media-capture`, on expo-camera (an optional peer).\",\n \"`facing` is `front` or `back`; `onCapture` receives a file `uri`, not a blob; a refusal for good opens the Settings.\",\n \"`mode` is `photo` or `video`; `labels` is required.\",\n ],\n examples: [{ name: \"photo\", title: \"Photo\", description: \"Before the permission: why, and the button that asks.\" }],\n },\n} satisfies ComponentMeta;\n"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"path": "media.ts",
|
|
254
|
+
"content": "// Shared by the data and media primitives (client): the clock format, the reduced-motion preference, the\n// in-view observer, a debounced callback. Never an entry of its own.\nimport * as React from \"react\";\n\n/** Seconds as a clock: `75` → `1:15`, `3725` → `1:02:05`. Negative or unknown values read `0:00`. */\nexport function formatClock(seconds: number): string {\n const s = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const rest = String(s % 60).padStart(2, \"0\");\n return h > 0 ? `${h}:${String(m).padStart(2, \"0\")}:${rest}` : `${m}:${rest}`;\n}\n\n/** Clamps `value` between `min` and `max`. */\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst REDUCE = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduce = (notify: () => void) => {\n if (typeof window === \"undefined\" || !window.matchMedia) return () => undefined;\n const query = window.matchMedia(REDUCE);\n query.addEventListener?.(\"change\", notify);\n return () => query.removeEventListener?.(\"change\", notify);\n};\n\n/** `true` when the person asked for less motion (`prefers-reduced-motion: reduce`); follows a change of the setting. */\nexport function usePrefersReducedMotion(): boolean {\n return React.useSyncExternalStore(\n subscribeReduce,\n () => (typeof window !== \"undefined\" && window.matchMedia ? window.matchMedia(REDUCE).matches : false),\n () => false,\n );\n}\n\n/** The latest value of a callback, for an effect or an event that must not re-subscribe when it changes. */\nexport function useLatest<T>(value: T) {\n const ref = React.useRef(value);\n React.useLayoutEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\n/** Calls `onChange(visible)` when at least `threshold` of the element enters or leaves the viewport. */\nexport function useInView(ref: React.RefObject<Element | null>, onChange: (visible: boolean) => void, enabled = true, threshold = 0.25) {\n const latest = useLatest(onChange);\n React.useEffect(() => {\n const element = ref.current;\n if (!enabled || !element || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => entry && latest.current(entry.isIntersecting), { threshold });\n observer.observe(element);\n return () => observer.disconnect();\n }, [ref, enabled, threshold, latest]);\n}\n\n/** A stable function that runs `callback` once `delay` ms have passed without a new call; `cancel()` drops the pending one. */\nexport function useDebouncedCallback<A extends unknown[]>(callback: (...args: A) => void, delay: number) {\n const latest = useLatest(callback);\n const [debounced] = React.useState(() => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let wait = delay;\n const run = (...args: A) => {\n clearTimeout(timer);\n if (wait <= 0) latest.current(...args);\n else timer = setTimeout(() => latest.current(...args), wait);\n };\n return Object.assign(run, {\n cancel: () => clearTimeout(timer),\n setDelay: (next: number) => {\n wait = next;\n },\n });\n });\n React.useEffect(() => debounced.setDelay(delay), [debounced, delay]);\n React.useEffect(() => () => debounced.cancel(), [debounced]);\n return debounced;\n}\n\n/** A value controlled by a prop when it is given, held inside otherwise; `set` calls `onChange` either way. */\nexport function useControllable<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [inner, setInner] = React.useState(defaultValue);\n const controlled = value !== undefined;\n const current = controlled ? value : inner;\n const set = React.useCallback(\n (next: T) => {\n if (!controlled) setInner(next);\n onChange?.(next);\n },\n [controlled, onChange],\n );\n return [current, set] as const;\n}\n\n/** Starts a media element without an unhandled rejection (autoplay refused, the source replaced meanwhile). */\nexport function playSafely(media: HTMLMediaElement | null | undefined) {\n try {\n const started = media?.play() as Promise<void> | undefined;\n started?.catch?.(() => undefined);\n } catch {\n // Not playable here (no media support): the controls stay as they are.\n }\n}\n"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"path": "meta.ts",
|
|
258
|
+
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"audio-player\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"combobox\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"data-table\"\n | \"date-picker\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"glow-card\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"pagination\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. `media`: video, sound, camera, files, cropping. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 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"
|
|
259
|
+
}
|
|
260
|
+
],
|
|
261
|
+
"dependencies": [
|
|
262
|
+
"tailwind-variants@^3.3.1"
|
|
263
|
+
],
|
|
264
|
+
"registryDependencies": [],
|
|
265
|
+
"demo": {
|
|
266
|
+
"path": "examples/media-capture/profile-photo.tsx",
|
|
267
|
+
"content": "\"use client\";\n\nimport { MediaCapture } from \"@krizaka/ui/media-capture\";\nimport * as React from \"react\";\n\n// The front camera, a photo; the browser asks only when the button is pressed.\nexport default function MediaCaptureProfilePhoto() {\n const [photo, setPhoto] = React.useState<string | null>(null);\n return (\n <div className=\"flex w-72 flex-col gap-3\">\n <MediaCapture\n mode=\"photo\"\n facing=\"user\"\n onCapture={(result) => setPhoto(result.url)}\n labels={{ prompt: \"Take a profile picture with your camera.\", start: \"Turn on the camera\" }}\n />\n {photo && <img src={photo} alt=\"Your new profile picture\" className=\"h-20 w-20 rounded-full object-cover\" />}\n </div>\n );\n}\n"
|
|
268
|
+
},
|
|
269
|
+
"props": [
|
|
270
|
+
{
|
|
271
|
+
"component": "MediaCapture",
|
|
272
|
+
"description": "The camera in the page: photo or clip, front or back, with a file-picker way out when the camera cannot start.",
|
|
273
|
+
"props": [
|
|
274
|
+
{
|
|
275
|
+
"name": "onCapture",
|
|
276
|
+
"type": "(result: CaptureResult) => void",
|
|
277
|
+
"default": null,
|
|
278
|
+
"description": "Called with a photo taken or a clip recorded (or a file picked in the fallback).",
|
|
279
|
+
"required": true
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"name": "aspectRatio",
|
|
283
|
+
"type": "string",
|
|
284
|
+
"default": "3 / 4",
|
|
285
|
+
"description": "The width / height ratio of the preview. Default `\"3 / 4\"`.",
|
|
286
|
+
"required": false
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "audio",
|
|
290
|
+
"type": "boolean",
|
|
291
|
+
"default": "true",
|
|
292
|
+
"description": "Records the microphone with a clip. Default `true`.",
|
|
293
|
+
"required": false
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "autoStart",
|
|
297
|
+
"type": "boolean",
|
|
298
|
+
"default": null,
|
|
299
|
+
"description": "Starts the camera at mount (only when the permission is already granted — never a prompt on load).",
|
|
300
|
+
"required": false
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"name": "className",
|
|
304
|
+
"type": "string",
|
|
305
|
+
"default": null,
|
|
306
|
+
"description": "Classes merged on the root.",
|
|
307
|
+
"required": false
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "facing",
|
|
311
|
+
"type": "\"user\" | \"environment\"",
|
|
312
|
+
"default": "user",
|
|
313
|
+
"description": "The camera at first: `user` (front, mirrored, default) or `environment` (back).",
|
|
314
|
+
"required": false
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"name": "labels",
|
|
318
|
+
"type": "Partial<MediaCaptureLabels>",
|
|
319
|
+
"default": null,
|
|
320
|
+
"description": "The words, passed translated; English by default.",
|
|
321
|
+
"required": false
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"name": "maxDuration",
|
|
325
|
+
"type": "number",
|
|
326
|
+
"default": null,
|
|
327
|
+
"description": "The longest clip, in seconds: recording stops by itself.",
|
|
328
|
+
"required": false
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"name": "mode",
|
|
332
|
+
"type": "\"video\" | \"both\" | \"photo\"",
|
|
333
|
+
"default": "photo",
|
|
334
|
+
"description": "What it captures: `photo` (default), `video`, or `both` (a switch above the preview).",
|
|
335
|
+
"required": false
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "onError",
|
|
339
|
+
"type": "((error: unknown) => void)",
|
|
340
|
+
"default": null,
|
|
341
|
+
"description": "Called when the camera cannot start (`NotAllowedError`, `NotFoundError`…).",
|
|
342
|
+
"required": false
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
package/registry/meter.json
CHANGED
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"name": "variant",
|
|
130
|
-
"type": "\"
|
|
130
|
+
"type": "\"bar\" | \"ring\"",
|
|
131
131
|
"default": "bar",
|
|
132
132
|
"description": "bar · ring.",
|
|
133
133
|
"required": false
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
"files": [
|
|
187
187
|
{
|
|
188
188
|
"path": "meta.ts",
|
|
189
|
-
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"glow-card\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 /** 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 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"
|
|
189
|
+
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"audio-player\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"combobox\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"data-table\"\n | \"date-picker\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"glow-card\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"pagination\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. `media`: video, sound, camera, files, cropping. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 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"
|
|
190
190
|
},
|
|
191
191
|
{
|
|
192
192
|
"path": "meter/index.ts",
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
},
|
|
294
294
|
{
|
|
295
295
|
"name": "variant",
|
|
296
|
-
"type": "\"
|
|
296
|
+
"type": "\"bar\" | \"ring\"",
|
|
297
297
|
"default": "bar",
|
|
298
298
|
"description": "bar · ring.",
|
|
299
299
|
"required": false
|
package/registry/nav-bar.json
CHANGED
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"files": [
|
|
275
275
|
{
|
|
276
276
|
"path": "meta.ts",
|
|
277
|
-
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"glow-card\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 /** 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 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"
|
|
277
|
+
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"audio-player\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"combobox\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"data-table\"\n | \"date-picker\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"glow-card\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"pagination\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. `media`: video, sound, camera, files, cropping. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 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"
|
|
278
278
|
},
|
|
279
279
|
{
|
|
280
280
|
"path": "nav-bar/index.ts",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"path": "meta.ts",
|
|
141
|
-
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"glow-card\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 /** 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 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"
|
|
141
|
+
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"audio-player\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"combobox\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"data-table\"\n | \"date-picker\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"glow-card\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"pagination\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. `media`: video, sound, camera, files, cropping. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 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"
|
|
142
142
|
},
|
|
143
143
|
{
|
|
144
144
|
"path": "number-ticker/index.ts",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"files": [
|
|
116
116
|
{
|
|
117
117
|
"path": "meta.ts",
|
|
118
|
-
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"glow-card\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 /** 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 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"
|
|
118
|
+
"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 krizaka.com renders as\n// /docs/ui/<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 | \"alert\"\n | \"audio-player\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"cn\"\n | \"combobox\"\n | \"command\"\n | \"confirm-button\"\n | \"countdown\"\n | \"data-table\"\n | \"date-picker\"\n | \"dialog\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"glow-card\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"loader\"\n | \"marquee\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page-header\"\n | \"pagination\"\n | \"popover\"\n | \"progress\"\n | \"radio-group\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"stat\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"theme\"\n | \"toast\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\";\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/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/** The group of the catalogue. `media`: video, sound, camera, files, cropping. */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"layout\" | \"foundations\";\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 */\nexport interface Example {\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/** 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 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"
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"path": "page-header/index.ts",
|