@krizaka/ui 3.35.0 → 3.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account-menu.js +1 -1
- package/dist/account-menu.js.map +1 -1
- package/dist/activity-feed.js +2 -2
- package/dist/activity-feed.js.map +1 -1
- package/dist/anatomy-plate.d.ts +26 -0
- package/dist/anatomy-plate.js +32 -0
- package/dist/anatomy-plate.js.map +1 -0
- package/dist/app-header.js +1 -1
- package/dist/app-header.js.map +1 -1
- package/dist/app-shell.js +6 -6
- package/dist/app-shell.js.map +1 -1
- package/dist/argument-card.js +1 -1
- package/dist/badge.d.ts +1 -1
- package/dist/badge.js +1 -1
- package/dist/badge.js.map +1 -1
- package/dist/bird-plate.d.ts +38 -0
- package/dist/bird-plate.js +39 -0
- package/dist/bird-plate.js.map +1 -0
- package/dist/blocks.css +243 -46
- package/dist/card.d.ts +6 -6
- package/dist/card.js +2 -2
- package/dist/card.js.map +1 -1
- package/dist/checkbox.js +1 -1
- package/dist/checkbox.js.map +1 -1
- package/dist/chip-row.js +1 -1
- package/dist/chip-row.js.map +1 -1
- package/dist/chip.js +1 -1
- package/dist/chip.js.map +1 -1
- package/dist/{chunk-QDXV544C.js → chunk-4QH6CP2Y.js} +2 -2
- package/dist/chunk-4QH6CP2Y.js.map +1 -0
- package/dist/{chunk-NJFCSVPT.js → chunk-6EAHUURY.js} +2 -2
- package/dist/chunk-6EAHUURY.js.map +1 -0
- package/dist/{chunk-REPTKUJ6.js → chunk-7NRSPHAF.js} +2 -2
- package/dist/chunk-7NRSPHAF.js.map +1 -0
- package/dist/{chunk-CKSHLSDH.js → chunk-AAP3U66V.js} +2 -2
- package/dist/{chunk-7UCPGXKN.js → chunk-C3NF7V5Y.js} +2 -2
- package/dist/{chunk-7UCPGXKN.js.map → chunk-C3NF7V5Y.js.map} +1 -1
- package/dist/chunk-JFJECOZ2.js +338 -0
- package/dist/chunk-JFJECOZ2.js.map +1 -0
- package/dist/{chunk-KIBICUZK.js → chunk-S5LP7KKW.js} +4 -4
- package/dist/{chunk-3S4GHSPZ.js → chunk-VPO27AFS.js} +5 -5
- package/dist/chunk-VPO27AFS.js.map +1 -0
- package/dist/{chunk-2PNLHZLT.js → chunk-WYQ277F2.js} +3 -3
- package/dist/chunk-WYQ277F2.js.map +1 -0
- package/dist/{chunk-7Z4KHPFC.js → chunk-YGIN2YIY.js} +2 -2
- package/dist/{chunk-7Z4KHPFC.js.map → chunk-YGIN2YIY.js.map} +1 -1
- package/dist/{chunk-LZNZLCOE.js → chunk-YKEIOYJ5.js} +20 -6
- package/dist/chunk-YKEIOYJ5.js.map +1 -0
- package/dist/{chunk-2DGLWTPS.js → chunk-YKOLME7R.js} +3 -3
- package/dist/{chunk-2DGLWTPS.js.map → chunk-YKOLME7R.js.map} +1 -1
- package/dist/code-block.js +4 -4
- package/dist/code-block.js.map +1 -1
- package/dist/combobox.js +2 -2
- package/dist/command.js +3 -3
- package/dist/command.js.map +1 -1
- package/dist/comparison-table.js +2 -2
- package/dist/comparison-table.js.map +1 -1
- package/dist/countdown.js +1 -1
- package/dist/countdown.js.map +1 -1
- package/dist/cover-hero.js +15 -6
- package/dist/cover-hero.js.map +1 -1
- package/dist/dashboard-layout.js +1 -1
- package/dist/dashboard-layout.js.map +1 -1
- package/dist/data-table.js +5 -5
- package/dist/date-picker.js +3 -3
- package/dist/date-picker.js.map +1 -1
- package/dist/description-list.js +1 -1
- package/dist/description-list.js.map +1 -1
- package/dist/detail-layout.js +1 -1
- package/dist/detail-layout.js.map +1 -1
- package/dist/dialog.js +1 -1
- package/dist/docs-nav.js +1 -1
- package/dist/docs-nav.js.map +1 -1
- package/dist/dropdown-menu.js +1 -1
- package/dist/dropdown-menu.js.map +1 -1
- package/dist/file-upload.js +3 -3
- package/dist/file-upload.js.map +1 -1
- package/dist/filter-bar.js +2 -2
- package/dist/footnote.d.ts +32 -0
- package/dist/footnote.js +29 -0
- package/dist/footnote.js.map +1 -0
- package/dist/genui.js +6 -6
- package/dist/geometry-DncHaYP8.d.ts +22 -0
- package/dist/language-switch.js +1 -1
- package/dist/list.js +1 -1
- package/dist/loader.js +2 -2
- package/dist/loader.js.map +1 -1
- package/dist/map.js +95 -83
- package/dist/map.js.map +1 -1
- package/dist/marketing-layout.js +1 -1
- package/dist/marketing-layout.js.map +1 -1
- package/dist/mascot-quote.d.ts +2 -21
- package/dist/mascot-quote.js +9 -338
- package/dist/mascot-quote.js.map +1 -1
- package/dist/match-dialog.js +1 -1
- package/dist/match-dialog.js.map +1 -1
- package/dist/media-grid.js +1 -1
- package/dist/meter.js +3 -3
- package/dist/meter.js.map +1 -1
- package/dist/motion.css +28 -41
- package/dist/native/map.js +94 -82
- package/dist/native/map.js.map +1 -1
- package/dist/native.d.ts +8 -1
- package/dist/native.js +57 -45
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.js +12 -12
- package/dist/nav-bar.js.map +1 -1
- package/dist/page-hero.js +1 -1
- package/dist/page-index.d.ts +30 -0
- package/dist/page-index.js +23 -0
- package/dist/page-index.js.map +1 -0
- package/dist/page.d.ts +19 -7
- package/dist/page.js +1 -1
- package/dist/photo-grid.js +2 -2
- package/dist/photo-grid.js.map +1 -1
- package/dist/progress.js +3 -3
- package/dist/progress.js.map +1 -1
- package/dist/radio-group.js +1 -1
- package/dist/radio-group.js.map +1 -1
- package/dist/rail-nav.js +2 -2
- package/dist/rail-nav.js.map +1 -1
- package/dist/search-field.js +1 -1
- package/dist/service-card.js +3 -3
- package/dist/service-card.js.map +1 -1
- package/dist/settings-layout.js +1 -1
- package/dist/settings-layout.js.map +1 -1
- package/dist/site-footer.js +3 -3
- package/dist/site-footer.js.map +1 -1
- package/dist/skip-link.js +1 -1
- package/dist/slot-picker.js +1 -1
- package/dist/slot-picker.js.map +1 -1
- package/dist/specimen-tag.d.ts +52 -0
- package/dist/specimen-tag.js +33 -0
- package/dist/specimen-tag.js.map +1 -0
- package/dist/stat.js +1 -1
- package/dist/stat.js.map +1 -1
- package/dist/step-progress.js +1 -1
- package/dist/step-progress.js.map +1 -1
- package/dist/sticky-action.d.ts +1 -1
- package/dist/sticky-action.js +1 -1
- package/dist/sticky-action.js.map +1 -1
- package/dist/swipe-deck.js +1 -1
- package/dist/swipe-deck.js.map +1 -1
- package/dist/tabs.js +4 -4
- package/dist/tabs.js.map +1 -1
- package/dist/tag-input.js +2 -2
- package/dist/tag-selector.js +1 -1
- package/dist/toast.js +1 -1
- package/dist/toast.js.map +1 -1
- package/package.json +74 -4
- package/registry/account-menu.json +1 -1
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/activity-feed.json +2 -2
- package/registry/alert.json +1 -1
- package/registry/anatomy-plate.json +168 -0
- package/registry/app-header.json +2 -2
- package/registry/app-shell.json +2 -2
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +2 -2
- package/registry/bird-plate.json +221 -0
- package/registry/button.json +1 -1
- package/registry/captcha.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +2 -2
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/chat.json +3 -3
- package/registry/checkbox.json +2 -2
- package/registry/chip-link.json +1 -1
- package/registry/chip-row.json +2 -2
- package/registry/chip.json +2 -2
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +2 -2
- package/registry/comparison-table.json +2 -2
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +2 -2
- package/registry/cover-hero.json +2 -2
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +2 -2
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +4 -4
- package/registry/decision-list.json +1 -1
- package/registry/description-list.json +2 -2
- package/registry/detail-layout.json +2 -2
- package/registry/dialog.json +2 -2
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/docs-nav.json +2 -2
- package/registry/document-viewer.json +1 -1
- package/registry/dropdown-menu.json +2 -2
- package/registry/empty-state.json +1 -1
- package/registry/engine-flow.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/action-sheet/native/video-actions.dark.png +0 -0
- package/registry/examples/action-sheet/native/video-actions.light.png +0 -0
- package/registry/examples/activity-feed/native/notifications.dark.png +0 -0
- package/registry/examples/activity-feed/native/notifications.light.png +0 -0
- package/registry/examples/anatomy-plate/button.d.ts +4 -0
- package/registry/examples/anatomy-plate/button.js +25 -0
- package/registry/examples/anatomy-plate/button.tsx +20 -0
- package/registry/examples/avatar/native/group.dark.png +0 -0
- package/registry/examples/avatar/native/group.light.png +0 -0
- package/registry/examples/avatar/native/presence.dark.png +0 -0
- package/registry/examples/avatar/native/presence.light.png +0 -0
- package/registry/examples/avatar/native/sizes.dark.png +0 -0
- package/registry/examples/avatar/native/sizes.light.png +0 -0
- package/registry/examples/badge/native/dot.dark.png +0 -0
- package/registry/examples/badge/native/dot.light.png +0 -0
- package/registry/examples/badge/native/scrim.dark.png +0 -0
- package/registry/examples/badge/native/scrim.light.png +0 -0
- package/registry/examples/badge/native/tones.dark.png +0 -0
- package/registry/examples/badge/native/tones.light.png +0 -0
- package/registry/examples/bird-plate/flock.d.ts +4 -0
- package/registry/examples/bird-plate/flock.js +24 -0
- package/registry/examples/bird-plate/flock.tsx +18 -0
- package/registry/examples/bird-plate/single.d.ts +4 -0
- package/registry/examples/bird-plate/single.js +19 -0
- package/registry/examples/bird-plate/single.tsx +13 -0
- package/registry/examples/button/native/danger.dark.png +0 -0
- package/registry/examples/button/native/danger.light.png +0 -0
- package/registry/examples/button/native/disabled.dark.png +0 -0
- package/registry/examples/button/native/disabled.light.png +0 -0
- package/registry/examples/button/native/ghost.dark.png +0 -0
- package/registry/examples/button/native/ghost.light.png +0 -0
- package/registry/examples/button/native/gradient.dark.png +0 -0
- package/registry/examples/button/native/gradient.light.png +0 -0
- package/registry/examples/button/native/icon-only.dark.png +0 -0
- package/registry/examples/button/native/icon-only.light.png +0 -0
- package/registry/examples/button/native/icon.dark.png +0 -0
- package/registry/examples/button/native/icon.light.png +0 -0
- package/registry/examples/button/native/loading.dark.png +0 -0
- package/registry/examples/button/native/loading.light.png +0 -0
- package/registry/examples/button/native/outline.dark.png +0 -0
- package/registry/examples/button/native/outline.light.png +0 -0
- package/registry/examples/button/native/primary.dark.png +0 -0
- package/registry/examples/button/native/primary.light.png +0 -0
- package/registry/examples/button/native/secondary.dark.png +0 -0
- package/registry/examples/button/native/secondary.light.png +0 -0
- package/registry/examples/button/native/sizes.dark.png +0 -0
- package/registry/examples/button/native/sizes.light.png +0 -0
- package/registry/examples/card/native/body.dark.png +0 -0
- package/registry/examples/card/native/body.light.png +0 -0
- package/registry/examples/card/native/default.dark.png +0 -0
- package/registry/examples/card/native/default.light.png +0 -0
- package/registry/examples/card/native/elevated.dark.png +0 -0
- package/registry/examples/card/native/elevated.light.png +0 -0
- package/registry/examples/card/native/pressable.dark.png +0 -0
- package/registry/examples/card/native/pressable.light.png +0 -0
- package/registry/examples/chat/native/assistant.dark.png +0 -0
- package/registry/examples/chat/native/assistant.light.png +0 -0
- package/registry/examples/chat/native/conversation.dark.png +0 -0
- package/registry/examples/chat/native/conversation.light.png +0 -0
- package/registry/examples/chat/native/inbox.dark.png +0 -0
- package/registry/examples/chat/native/inbox.light.png +0 -0
- package/registry/examples/checkbox/native/terms.dark.png +0 -0
- package/registry/examples/checkbox/native/terms.light.png +0 -0
- package/registry/examples/chip/native/multiple.dark.png +0 -0
- package/registry/examples/chip/native/multiple.light.png +0 -0
- package/registry/examples/chip/native/removable.dark.png +0 -0
- package/registry/examples/chip/native/removable.light.png +0 -0
- package/registry/examples/chip/native/single.dark.png +0 -0
- package/registry/examples/chip/native/single.light.png +0 -0
- package/registry/examples/chip/native/toggle.dark.png +0 -0
- package/registry/examples/chip/native/toggle.light.png +0 -0
- package/registry/examples/combobox/native/multiple.dark.png +0 -0
- package/registry/examples/combobox/native/multiple.light.png +0 -0
- package/registry/examples/combobox/native/search.dark.png +0 -0
- package/registry/examples/combobox/native/search.light.png +0 -0
- package/registry/examples/countdown/native/days.dark.png +0 -0
- package/registry/examples/countdown/native/days.light.png +0 -0
- package/registry/examples/countdown/native/hours.dark.png +0 -0
- package/registry/examples/countdown/native/hours.light.png +0 -0
- package/registry/examples/countdown/native/sizes.dark.png +0 -0
- package/registry/examples/countdown/native/sizes.light.png +0 -0
- package/registry/examples/countdown/native/urgent.dark.png +0 -0
- package/registry/examples/countdown/native/urgent.light.png +0 -0
- package/registry/examples/countdown/native/with-label.dark.png +0 -0
- package/registry/examples/countdown/native/with-label.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date-wheel.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date-wheel.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date.light.png +0 -0
- package/registry/examples/date-picker/native/calendar.dark.png +0 -0
- package/registry/examples/date-picker/native/calendar.light.png +0 -0
- package/registry/examples/date-picker/native/day-strip.dark.png +0 -0
- package/registry/examples/date-picker/native/day-strip.light.png +0 -0
- package/registry/examples/date-picker/native/month-year.dark.png +0 -0
- package/registry/examples/date-picker/native/month-year.light.png +0 -0
- package/registry/examples/date-picker/native/time-slots.dark.png +0 -0
- package/registry/examples/date-picker/native/time-slots.light.png +0 -0
- package/registry/examples/date-picker/native/time-wheel.dark.png +0 -0
- package/registry/examples/date-picker/native/time-wheel.light.png +0 -0
- package/registry/examples/description-list/native/job-details.dark.png +0 -0
- package/registry/examples/description-list/native/job-details.light.png +0 -0
- package/registry/examples/dialog/native/sheet.dark.png +0 -0
- package/registry/examples/dialog/native/sheet.light.png +0 -0
- package/registry/examples/empty-state/native/default.dark.png +0 -0
- package/registry/examples/empty-state/native/default.light.png +0 -0
- package/registry/examples/fab/native/extended.dark.png +0 -0
- package/registry/examples/fab/native/extended.light.png +0 -0
- package/registry/examples/field/native/default.dark.png +0 -0
- package/registry/examples/field/native/default.light.png +0 -0
- package/registry/examples/field/native/select.dark.png +0 -0
- package/registry/examples/field/native/select.light.png +0 -0
- package/registry/examples/footnote/notes.d.ts +4 -0
- package/registry/examples/footnote/notes.js +24 -0
- package/registry/examples/footnote/notes.tsx +19 -0
- package/registry/examples/force-update-screen/native/default.dark.png +0 -0
- package/registry/examples/force-update-screen/native/default.light.png +0 -0
- package/registry/examples/form/native/amount.dark.png +0 -0
- package/registry/examples/form/native/amount.light.png +0 -0
- package/registry/examples/form/native/audience.dark.png +0 -0
- package/registry/examples/form/native/audience.light.png +0 -0
- package/registry/examples/form/native/profile.dark.png +0 -0
- package/registry/examples/form/native/profile.light.png +0 -0
- package/registry/examples/form/native/settings.dark.png +0 -0
- package/registry/examples/form/native/settings.light.png +0 -0
- package/registry/examples/form/native/sign-up.dark.png +0 -0
- package/registry/examples/form/native/sign-up.light.png +0 -0
- package/registry/examples/form/native/upload.dark.png +0 -0
- package/registry/examples/form/native/upload.light.png +0 -0
- package/registry/examples/goal-gauge/native/default.dark.png +0 -0
- package/registry/examples/goal-gauge/native/default.light.png +0 -0
- package/registry/examples/header/native/large-title.dark.png +0 -0
- package/registry/examples/header/native/large-title.light.png +0 -0
- package/registry/examples/image-viewer/native/gallery.dark.png +0 -0
- package/registry/examples/image-viewer/native/gallery.light.png +0 -0
- package/registry/examples/list/native/default.dark.png +0 -0
- package/registry/examples/list/native/default.light.png +0 -0
- package/registry/examples/list/native/infinite.dark.png +0 -0
- package/registry/examples/list/native/infinite.light.png +0 -0
- package/registry/examples/list/native/states.dark.png +0 -0
- package/registry/examples/list/native/states.light.png +0 -0
- package/registry/examples/loader/native/block.dark.png +0 -0
- package/registry/examples/loader/native/block.light.png +0 -0
- package/registry/examples/loader/native/branded.dark.png +0 -0
- package/registry/examples/loader/native/branded.light.png +0 -0
- package/registry/examples/loader/native/job-wait.dark.png +0 -0
- package/registry/examples/loader/native/job-wait.light.png +0 -0
- package/registry/examples/maintenance-screen/native/default.dark.png +0 -0
- package/registry/examples/maintenance-screen/native/default.light.png +0 -0
- package/registry/examples/map/native/location-picker.dark.png +0 -0
- package/registry/examples/map/native/location-picker.light.png +0 -0
- package/registry/examples/map/native/places.dark.png +0 -0
- package/registry/examples/map/native/places.light.png +0 -0
- package/registry/examples/markdown/native/ai-stream.dark.png +0 -0
- package/registry/examples/markdown/native/ai-stream.light.png +0 -0
- package/registry/examples/markdown/native/basic.dark.png +0 -0
- package/registry/examples/markdown/native/basic.light.png +0 -0
- package/registry/examples/mascot-state/native/default.dark.png +0 -0
- package/registry/examples/mascot-state/native/default.light.png +0 -0
- package/registry/examples/mascots/native/crane.dark.png +0 -0
- package/registry/examples/mascots/native/crane.light.png +0 -0
- package/registry/examples/mascots/native/default.dark.png +0 -0
- package/registry/examples/mascots/native/default.light.png +0 -0
- package/registry/examples/mascots/native/lovebird.dark.png +0 -0
- package/registry/examples/mascots/native/lovebird.light.png +0 -0
- package/registry/examples/mascots/native/phoenix.dark.png +0 -0
- package/registry/examples/mascots/native/phoenix.light.png +0 -0
- package/registry/examples/match-dialog/native/its-a-match.dark.png +0 -0
- package/registry/examples/match-dialog/native/its-a-match.light.png +0 -0
- package/registry/examples/media-capture/native/photo.dark.png +0 -0
- package/registry/examples/media-capture/native/photo.light.png +0 -0
- package/registry/examples/media-picker/native/story.dark.png +0 -0
- package/registry/examples/media-picker/native/story.light.png +0 -0
- package/registry/examples/meta-row/native/video.dark.png +0 -0
- package/registry/examples/meta-row/native/video.light.png +0 -0
- package/registry/examples/meter/native/credits.dark.png +0 -0
- package/registry/examples/meter/native/credits.light.png +0 -0
- package/registry/examples/meter/native/split-ring.dark.png +0 -0
- package/registry/examples/meter/native/split-ring.light.png +0 -0
- package/registry/examples/number-ticker/native/balance.dark.png +0 -0
- package/registry/examples/number-ticker/native/balance.light.png +0 -0
- package/registry/examples/onboarding/native/first-launch.dark.png +0 -0
- package/registry/examples/onboarding/native/first-launch.light.png +0 -0
- package/registry/examples/otp-input/native/invalid.dark.png +0 -0
- package/registry/examples/otp-input/native/invalid.light.png +0 -0
- package/registry/examples/otp-input/native/verify.dark.png +0 -0
- package/registry/examples/otp-input/native/verify.light.png +0 -0
- package/registry/examples/page-index/docs.d.ts +4 -0
- package/registry/examples/page-index/docs.js +33 -0
- package/registry/examples/page-index/docs.tsx +27 -0
- package/registry/examples/pagination/native/cursor.dark.png +0 -0
- package/registry/examples/pagination/native/cursor.light.png +0 -0
- package/registry/examples/pagination/native/pages.dark.png +0 -0
- package/registry/examples/pagination/native/pages.light.png +0 -0
- package/registry/examples/permission-prompt/native/blocked.dark.png +0 -0
- package/registry/examples/permission-prompt/native/blocked.light.png +0 -0
- package/registry/examples/permission-prompt/native/camera.dark.png +0 -0
- package/registry/examples/permission-prompt/native/camera.light.png +0 -0
- package/registry/examples/photo-grid/native/profile-photos.dark.png +0 -0
- package/registry/examples/photo-grid/native/profile-photos.light.png +0 -0
- package/registry/examples/profile-card/native/default.dark.png +0 -0
- package/registry/examples/profile-card/native/default.light.png +0 -0
- package/registry/examples/progress/native/bar.dark.png +0 -0
- package/registry/examples/progress/native/bar.light.png +0 -0
- package/registry/examples/progress/native/indeterminate.dark.png +0 -0
- package/registry/examples/progress/native/indeterminate.light.png +0 -0
- package/registry/examples/progress/native/ring.dark.png +0 -0
- package/registry/examples/progress/native/ring.light.png +0 -0
- package/registry/examples/pull-to-refresh/native/branded.dark.png +0 -0
- package/registry/examples/pull-to-refresh/native/branded.light.png +0 -0
- package/registry/examples/pull-to-refresh/native/feed.dark.png +0 -0
- package/registry/examples/pull-to-refresh/native/feed.light.png +0 -0
- package/registry/examples/radio-group/native/buttons.dark.png +0 -0
- package/registry/examples/radio-group/native/buttons.light.png +0 -0
- package/registry/examples/radio-group/native/cards.dark.png +0 -0
- package/registry/examples/radio-group/native/cards.light.png +0 -0
- package/registry/examples/radio-group/native/list.dark.png +0 -0
- package/registry/examples/radio-group/native/list.light.png +0 -0
- package/registry/examples/radio-group/native/segmented.dark.png +0 -0
- package/registry/examples/radio-group/native/segmented.light.png +0 -0
- package/registry/examples/screen/native/app-shell.dark.png +0 -0
- package/registry/examples/screen/native/app-shell.light.png +0 -0
- package/registry/examples/screen/native/auth.dark.png +0 -0
- package/registry/examples/screen/native/auth.light.png +0 -0
- package/registry/examples/screen/native/chat.dark.png +0 -0
- package/registry/examples/screen/native/chat.light.png +0 -0
- package/registry/examples/screen/native/feed.dark.png +0 -0
- package/registry/examples/screen/native/feed.light.png +0 -0
- package/registry/examples/screen/native/map.dark.png +0 -0
- package/registry/examples/screen/native/map.light.png +0 -0
- package/registry/examples/screen/native/notifications.dark.png +0 -0
- package/registry/examples/screen/native/notifications.light.png +0 -0
- package/registry/examples/screen/native/profile.dark.png +0 -0
- package/registry/examples/screen/native/profile.light.png +0 -0
- package/registry/examples/screen/native/settings.dark.png +0 -0
- package/registry/examples/screen/native/settings.light.png +0 -0
- package/registry/examples/search-field/native/cancel.dark.png +0 -0
- package/registry/examples/search-field/native/cancel.light.png +0 -0
- package/registry/examples/search-field/native/default.dark.png +0 -0
- package/registry/examples/search-field/native/default.light.png +0 -0
- package/registry/examples/segmented/native/default.dark.png +0 -0
- package/registry/examples/segmented/native/default.light.png +0 -0
- package/registry/examples/segmented/native/small.dark.png +0 -0
- package/registry/examples/segmented/native/small.light.png +0 -0
- package/registry/examples/settings-layout/native/account.dark.png +0 -0
- package/registry/examples/settings-layout/native/account.light.png +0 -0
- package/registry/examples/showcase/native/slides.dark.png +0 -0
- package/registry/examples/showcase/native/slides.light.png +0 -0
- package/registry/examples/skeleton/native/shapes.dark.png +0 -0
- package/registry/examples/skeleton/native/shapes.light.png +0 -0
- package/registry/examples/slider/native/default.dark.png +0 -0
- package/registry/examples/slider/native/default.light.png +0 -0
- package/registry/examples/slider/native/range.dark.png +0 -0
- package/registry/examples/slider/native/range.light.png +0 -0
- package/registry/examples/slider/native/stepped.dark.png +0 -0
- package/registry/examples/slider/native/stepped.light.png +0 -0
- package/registry/examples/slider/native/vertical.dark.png +0 -0
- package/registry/examples/slider/native/vertical.light.png +0 -0
- package/registry/examples/specimen-tag/product.d.ts +4 -0
- package/registry/examples/specimen-tag/product.js +28 -0
- package/registry/examples/specimen-tag/product.tsx +25 -0
- package/registry/examples/specimen-tag/stamps.d.ts +4 -0
- package/registry/examples/specimen-tag/stamps.js +16 -0
- package/registry/examples/specimen-tag/stamps.tsx +13 -0
- package/registry/examples/specimen-tag/wire.d.ts +4 -0
- package/registry/examples/specimen-tag/wire.js +16 -0
- package/registry/examples/specimen-tag/wire.tsx +20 -0
- package/registry/examples/spinner/native/sizes.dark.png +0 -0
- package/registry/examples/spinner/native/sizes.light.png +0 -0
- package/registry/examples/splash-handoff/native/default.dark.png +0 -0
- package/registry/examples/splash-handoff/native/default.light.png +0 -0
- package/registry/examples/step-progress/native/sign-up.dark.png +0 -0
- package/registry/examples/step-progress/native/sign-up.light.png +0 -0
- package/registry/examples/stepper/native/run.dark.png +0 -0
- package/registry/examples/stepper/native/run.light.png +0 -0
- package/registry/examples/swipe-deck/native/discover.dark.png +0 -0
- package/registry/examples/swipe-deck/native/discover.light.png +0 -0
- package/registry/examples/swipeable-row/native/conversation.dark.png +0 -0
- package/registry/examples/swipeable-row/native/conversation.light.png +0 -0
- package/registry/examples/switch/native/default.dark.png +0 -0
- package/registry/examples/switch/native/default.light.png +0 -0
- package/registry/examples/switch/native/settings.dark.png +0 -0
- package/registry/examples/switch/native/settings.light.png +0 -0
- package/registry/examples/tab-bar/native/main-sections.dark.png +0 -0
- package/registry/examples/tab-bar/native/main-sections.light.png +0 -0
- package/registry/examples/tabs/native/sections.dark.png +0 -0
- package/registry/examples/tabs/native/sections.light.png +0 -0
- package/registry/examples/tabs/native/segmented.dark.png +0 -0
- package/registry/examples/tabs/native/segmented.light.png +0 -0
- package/registry/examples/tag-selector/native/interests.dark.png +0 -0
- package/registry/examples/tag-selector/native/interests.light.png +0 -0
- package/registry/examples/toast/native/notification.dark.png +0 -0
- package/registry/examples/toast/native/notification.light.png +0 -0
- package/registry/examples/toast/native/snackbar.dark.png +0 -0
- package/registry/examples/toast/native/snackbar.light.png +0 -0
- package/registry/examples/toast/native/tones.dark.png +0 -0
- package/registry/examples/toast/native/tones.light.png +0 -0
- package/registry/examples/txt/native/tones.dark.png +0 -0
- package/registry/examples/txt/native/tones.light.png +0 -0
- package/registry/examples/txt/native/variants.dark.png +0 -0
- package/registry/examples/txt/native/variants.light.png +0 -0
- package/registry/examples/update-prompt/native/default.dark.png +0 -0
- package/registry/examples/update-prompt/native/default.light.png +0 -0
- package/registry/examples/verified-badge/native/verified.dark.png +0 -0
- package/registry/examples/verified-badge/native/verified.light.png +0 -0
- package/registry/examples/vertical-video/native/rail.dark.png +0 -0
- package/registry/examples/vertical-video/native/rail.light.png +0 -0
- package/registry/examples/video-player/native/default.dark.png +0 -0
- package/registry/examples/video-player/native/default.light.png +0 -0
- package/registry/examples/waveform/native/card.dark.png +0 -0
- package/registry/examples/waveform/native/card.light.png +0 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/footnote.json +220 -0
- package/registry/force-update-screen.json +1 -1
- package/registry/form.json +3 -3
- package/registry/gallery-page.json +1 -1
- package/registry/genui.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +160 -5
- package/registry/iso.json +1 -1
- package/registry/kanban.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/language-switch.json +2 -2
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/list.json +1 -1
- package/registry/loader.json +2 -2
- package/registry/longform-page.json +1 -1
- package/registry/maintenance-screen.json +1 -1
- package/registry/map.json +3 -3
- package/registry/markdown.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/match-dialog.json +1 -1
- package/registry/media-capture.json +1 -1
- package/registry/media-gallery.json +1 -1
- package/registry/media-grid.json +1 -1
- package/registry/meta-row.json +1 -1
- package/registry/meter.json +2 -2
- package/registry/nav-bar.json +3 -3
- package/registry/number-ticker.json +1 -1
- package/registry/opening-hours.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page-index.json +168 -0
- package/registry/page.json +89 -5
- package/registry/pagination.json +1 -1
- package/registry/photo-grid.json +2 -2
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +2 -2
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +2 -2
- package/registry/rail-nav.json +3 -3
- package/registry/recipes.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +2 -2
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/service-card.json +2 -2
- package/registry/settings-layout.json +2 -2
- package/registry/showcase.json +1 -1
- package/registry/site-footer.json +3 -3
- package/registry/sites.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/skip-link.json +2 -2
- package/registry/slider.json +1 -1
- package/registry/slot-picker.json +2 -2
- package/registry/slot.json +1 -1
- package/registry/specimen-tag.json +269 -0
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +2 -2
- package/registry/status-page.json +1 -1
- package/registry/step-progress.json +2 -2
- package/registry/step-showcase.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/sticky-action.json +2 -2
- package/registry/swipe-deck.json +2 -2
- package/registry/switch.json +1 -1
- package/registry/tabs.json +2 -2
- package/registry/tag-input.json +1 -1
- package/registry/tag-selector.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +2 -2
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/tree-view.json +1 -1
- package/registry/update-prompt.json +1 -1
- package/registry/verified-badge.json +1 -1
- package/registry/vertical-video.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
- package/registry/waveform.json +1 -1
- package/dist/chunk-2PNLHZLT.js.map +0 -1
- package/dist/chunk-3S4GHSPZ.js.map +0 -1
- package/dist/chunk-LZNZLCOE.js.map +0 -1
- package/dist/chunk-NJFCSVPT.js.map +0 -1
- package/dist/chunk-QDXV544C.js.map +0 -1
- package/dist/chunk-REPTKUJ6.js.map +0 -1
- /package/dist/{chunk-CKSHLSDH.js.map → chunk-AAP3U66V.js.map} +0 -0
- /package/dist/{chunk-KIBICUZK.js.map → chunk-S5LP7KKW.js.map} +0 -0
package/registry/loader.json
CHANGED
|
@@ -443,7 +443,7 @@
|
|
|
443
443
|
},
|
|
444
444
|
{
|
|
445
445
|
"path": "loader/loader.tsx",
|
|
446
|
-
"content": "// Client (timers). The whole life of a wait, done right once: nothing for the first instants (a fast answer never\n// flashes a spinner), then the Krizaka glyph with its words and an ETA, held long enough to be read, ending on a\n// check or a cross that is drawn and announced. `LoadingRegion` hands a region over from its content to a skeleton of\n// the same shape, then to words when the wait grows long, then back to the content.\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { LoaderGlyph } from \"../spinner/glyph\";\n\nexport const loader = tv({\n slots: {\n root: \"text-fg-secondary\",\n box: \"kz-fade flex items-center gap-3\",\n glyph: \"shrink-0 text-accent\",\n mark: \"block shrink-0 [&>svg]:block [&>svg]:h-full [&>svg]:w-full\",\n text: \"flex min-w-0 flex-col\",\n label: \"font-medium text-fg\",\n detail: \"text-xs text-fg-secondary tabular-nums\",\n },\n variants: {\n variant: {\n inline: { box: \"text-sm\", glyph: \"h-5 w-5\", mark: \"h-5 w-5\" },\n block: { box: \"flex-col justify-center py-10 text-center text-sm\", glyph: \"h-9 w-9\", mark: \"h-9 w-9\", text: \"items-center\" },\n overlay: {\n root: \"absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] empty:hidden\",\n box: \"flex-col rounded-xl border border-border-default bg-surface-1/90 px-6 py-5 text-center text-sm shadow-lg backdrop-blur-sm\",\n glyph: \"h-9 w-9\", mark: \"h-9 w-9\",\n text: \"items-center\",\n },\n page: {\n root: \"fixed inset-0 z-50 flex items-center justify-center bg-surface-0/80 backdrop-blur-sm empty:hidden\",\n box: \"flex-col text-center\",\n glyph: \"h-14 w-14\", mark: \"h-14 w-14\",\n text: \"items-center\",\n label: \"font-display text-lg\",\n },\n },\n ended: {\n success: { glyph: \"text-fg-success\" },\n error: { glyph: \"text-fg-danger\" },\n },\n },\n defaultVariants: { variant: \"inline\" },\n});\n\nexport type LoaderVariants = VariantProps<typeof loader>;\n\n/**\n * `true` once `active` has lasted `delay` ms; then stays `true` at least `minDuration` ms — a wait that ends early never\n * shows, one that shows never blinks.\n */\nexport function useDelayedWait(active: boolean, delay = 250, minDuration = 600): boolean {\n const [shown, setShown] = React.useState(false);\n const since = React.useRef(0);\n React.useEffect(() => {\n if (active === shown) return;\n const wait = active ? delay : Math.max(0, minDuration - (Date.now() - since.current));\n const timer = setTimeout(() => {\n since.current = Date.now();\n setShown(active);\n }, wait);\n return () => clearTimeout(timer);\n }, [active, shown, delay, minDuration]);\n return shown;\n}\n\nexport interface LoaderProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** What is loading, in words (“Loading your invoices”) — passed translated; shown and announced. */\n label: string;\n /**\n * loading · success · error. When the wait ends, the loader shows how it ended for `endDuration` ms, then disappears.\n * @default \"loading\"\n */\n status?: \"loading\" | \"success\" | \"error\";\n /** The words of a success (“Invoices loaded”) — passed translated. Default: nothing more than the check. */\n successLabel?: string;\n /** The words of a failure (“Could not load the invoices”) — passed translated. */\n errorLabel?: string;\n /** Between 0 and 1: a determinate wait — the trace grows to it and the percentage shows. */\n value?: number | null;\n /** The time left, in words (“About 20 s left”) — passed translated (`@krizaka/intl` formats durations). */\n eta?: string;\n /**\n * Milliseconds before anything shows: a wait shorter than this never flashes.\n * @default 250\n */\n delay?: number;\n /**\n * Once shown, the least milliseconds it stays: it never blinks.\n * @default 600\n */\n minDuration?: number;\n /**\n * Milliseconds the ending (check or cross) stays before the loader disappears; `0` never shows it, `Infinity` keeps it.\n * @default 1600\n */\n endDuration?: number;\n /**\n * inline (beside the content) · block (a region's centre) · overlay (over a `relative` parent, frosted) · page (the\n * whole screen).\n * @default \"inline\"\n */\n variant?: LoaderVariants[\"variant\"];\n /**\n * The product's mark in place of the Krizaka glyph while it waits — `<OrochiaLogo animated />` (or `ProductLogo`) of\n * `@krizaka/ui`; it takes the loader's size. The ending (check or cross) stays the glyph's. Decorative: leave `title` off.\n */\n mark?: React.ReactNode;\n}\n\n/**\n * A wait, from start to end. `role=\"status\"` (polite): its words are announced when it shows and when it ends. Under\n * prefers-reduced-motion the glyph rests on a still frame; the delay and the ending stay.\n */\nexport function Loader({\n label,\n status = \"loading\",\n successLabel,\n errorLabel,\n value,\n eta,\n delay = 250,\n minDuration = 600,\n endDuration = 1600,\n variant,\n mark,\n className,\n ...props\n}: LoaderProps) {\n const waiting = useDelayedWait(status === \"loading\", delay, minDuration);\n // Rendered already ended (a result to show), it shows its ending; otherwise only a wait that was seen ends visibly.\n const [ending, setEnding] = React.useState<\"success\" | \"error\" | null>(() => (status !== \"loading\" && endDuration > 0 ? status : null));\n const wasWaiting = React.useRef(false);\n React.useEffect(() => {\n if (!Number.isFinite(endDuration)) return;\n const hide = setTimeout(() => setEnding((e) => (wasWaiting.current ? e : null)), endDuration);\n return () => clearTimeout(hide);\n // Only the ending a loader was mounted with: later endings have their own timer below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n React.useEffect(() => {\n if (waiting) {\n wasWaiting.current = true;\n return;\n }\n if (!wasWaiting.current || status === \"loading\") return;\n wasWaiting.current = false;\n const end = status;\n const show = setTimeout(() => setEnding(endDuration > 0 ? end : null), 0);\n const hide = Number.isFinite(endDuration) ? setTimeout(() => setEnding(null), endDuration) : undefined;\n return () => {\n clearTimeout(show);\n clearTimeout(hide);\n };\n }, [waiting, status, endDuration]);\n\n const phase = waiting ? \"loading\" : ending;\n const s = loader({ variant, ended: phase === \"success\" || phase === \"error\" ? phase : undefined });\n const known = phase === \"loading\" && typeof value === \"number\" && Number.isFinite(value);\n const words = phase === \"success\" ? (successLabel ?? label) : phase === \"error\" ? (errorLabel ?? label) : label;\n const detail = phase === \"loading\" ? [known ? `${Math.round(Math.min(1, Math.max(0, value)) * 100)} %` : null, eta].filter(Boolean).join(\" · \") : \"\";\n return (\n <div role=\"status\" aria-live=\"polite\" data-phase={phase ?? \"idle\"} className={s.root({ className })} {...props}>\n {phase && (\n <div className={s.box()}>\n {mark && phase === \"loading\" ? (\n <span aria-hidden className={s.mark()} data-mark=\"\">\n {mark}\n </span>\n ) : (\n <LoaderGlyph state={phase} value={known ? value : null} className={s.glyph()} />\n )}\n <span className={s.text()}>\n <span className={s.label()}>{words}</span>\n {detail && <span className={s.detail()}>{detail}</span>}\n </span>\n </div>\n )}\n </div>\n );\n}\n\nexport interface LoadingRegionProps extends React.ComponentProps<\"div\"> {\n /** Whether the content is on its way. */\n loading: boolean;\n /** What stands in while it loads: skeletons the shape of the content (no layout shift when it arrives). */\n skeleton: React.ReactNode;\n /** What is loading, in words — passed translated; announced when the wait shows. */\n label: string;\n /** The words when the wait grows long (“Still loading — the server is busy”) — passed translated. */\n slowLabel?: string;\n /**\n * Milliseconds of skeleton before the words of a long wait appear over it.\n * @default 4000\n */\n slowAfter?: number;\n /**\n * Milliseconds before the skeleton shows: a fast answer goes straight to the content.\n * @default 200\n */\n delay?: number;\n /**\n * Once shown, the least milliseconds the skeleton stays.\n * @default 500\n */\n minDuration?: number;\n}\n\n/**\n * A region that hands over: its content → (after `delay`) a skeleton of the same shape → (after `slowAfter`) words over\n * the skeleton → its content again, fading in. `aria-busy` while it waits; the wait is announced once.\n */\nexport function LoadingRegion({\n loading,\n skeleton,\n label,\n slowLabel,\n slowAfter = 4000,\n delay = 200,\n minDuration = 500,\n className,\n children,\n ...props\n}: LoadingRegionProps) {\n const waiting = useDelayedWait(loading, delay, minDuration);\n const slow = useDelayedWait(waiting && loading, slowAfter, 0);\n const busy = loading || waiting;\n return (\n <div aria-busy={busy} data-phase={waiting ? (slow ? \"slow\" : \"skeleton\") : busy ? \"pending\" : \"ready\"} className={[\"relative\", className].filter(Boolean).join(\" \")} {...props}>\n <span role=\"status\" className=\"sr-only\">\n {waiting ? (slow ? (slowLabel ?? label) : label) : \"\"}\n </span>\n {waiting ? (\n <div aria-hidden className=\"kz-fade\">\n {skeleton}\n </div>\n ) : busy ? (\n // Within the delay: the skeleton holds the place, invisible — nothing flashes, nothing jumps.\n <div aria-hidden className=\"invisible\">\n {skeleton}\n </div>\n ) : (\n <div className=\"kz-fade\">{children}</div>\n )}\n {waiting && slow && <Loader label={slowLabel ?? label} variant=\"overlay\" delay={0} minDuration={0} endDuration={0} aria-live=\"off\" role=\"presentation\" />}\n </div>\n );\n}\n"
|
|
446
|
+
"content": "// Client (timers). The whole life of a wait, done right once: nothing for the first instants (a fast answer never\n// flashes a spinner), then the Krizaka glyph with its words and an ETA, held long enough to be read, ending on a\n// check or a cross that is drawn and announced. `LoadingRegion` hands a region over from its content to a skeleton of\n// the same shape, then to words when the wait grows long, then back to the content.\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { LoaderGlyph } from \"../spinner/glyph\";\n\nexport const loader = tv({\n slots: {\n root: \"text-fg-secondary\",\n box: \"kz-fade flex items-center gap-3\",\n glyph: \"shrink-0 text-accent\",\n mark: \"block shrink-0 [&>svg]:block [&>svg]:h-full [&>svg]:w-full\",\n text: \"flex min-w-0 flex-col\",\n label: \"font-medium text-fg\",\n detail: \"text-xs text-fg-secondary tabular-nums\",\n },\n variants: {\n variant: {\n inline: { box: \"text-sm\", glyph: \"h-5 w-5\", mark: \"h-5 w-5\" },\n block: { box: \"flex-col justify-center py-10 text-center text-sm\", glyph: \"h-9 w-9\", mark: \"h-9 w-9\", text: \"items-center\" },\n overlay: {\n root: \"absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] empty:hidden\",\n box: \"flex-col rounded-xl border border-border-default bg-surface-1 px-6 py-5 text-center text-sm shadow-lg\",\n glyph: \"h-9 w-9\", mark: \"h-9 w-9\",\n text: \"items-center\",\n },\n page: {\n root: \"fixed inset-0 z-50 flex items-center justify-center bg-surface-0 empty:hidden\",\n box: \"flex-col text-center\",\n glyph: \"h-14 w-14\", mark: \"h-14 w-14\",\n text: \"items-center\",\n label: \"font-display text-lg\",\n },\n },\n ended: {\n success: { glyph: \"text-fg-success\" },\n error: { glyph: \"text-fg-danger\" },\n },\n },\n defaultVariants: { variant: \"inline\" },\n});\n\nexport type LoaderVariants = VariantProps<typeof loader>;\n\n/**\n * `true` once `active` has lasted `delay` ms; then stays `true` at least `minDuration` ms — a wait that ends early never\n * shows, one that shows never blinks.\n */\nexport function useDelayedWait(active: boolean, delay = 250, minDuration = 600): boolean {\n const [shown, setShown] = React.useState(false);\n const since = React.useRef(0);\n React.useEffect(() => {\n if (active === shown) return;\n const wait = active ? delay : Math.max(0, minDuration - (Date.now() - since.current));\n const timer = setTimeout(() => {\n since.current = Date.now();\n setShown(active);\n }, wait);\n return () => clearTimeout(timer);\n }, [active, shown, delay, minDuration]);\n return shown;\n}\n\nexport interface LoaderProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** What is loading, in words (“Loading your invoices”) — passed translated; shown and announced. */\n label: string;\n /**\n * loading · success · error. When the wait ends, the loader shows how it ended for `endDuration` ms, then disappears.\n * @default \"loading\"\n */\n status?: \"loading\" | \"success\" | \"error\";\n /** The words of a success (“Invoices loaded”) — passed translated. Default: nothing more than the check. */\n successLabel?: string;\n /** The words of a failure (“Could not load the invoices”) — passed translated. */\n errorLabel?: string;\n /** Between 0 and 1: a determinate wait — the trace grows to it and the percentage shows. */\n value?: number | null;\n /** The time left, in words (“About 20 s left”) — passed translated (`@krizaka/intl` formats durations). */\n eta?: string;\n /**\n * Milliseconds before anything shows: a wait shorter than this never flashes.\n * @default 250\n */\n delay?: number;\n /**\n * Once shown, the least milliseconds it stays: it never blinks.\n * @default 600\n */\n minDuration?: number;\n /**\n * Milliseconds the ending (check or cross) stays before the loader disappears; `0` never shows it, `Infinity` keeps it.\n * @default 1600\n */\n endDuration?: number;\n /**\n * inline (beside the content) · block (a region's centre) · overlay (over a `relative` parent, frosted) · page (the\n * whole screen).\n * @default \"inline\"\n */\n variant?: LoaderVariants[\"variant\"];\n /**\n * The product's mark in place of the Krizaka glyph while it waits — `<OrochiaLogo animated />` (or `ProductLogo`) of\n * `@krizaka/ui`; it takes the loader's size. The ending (check or cross) stays the glyph's. Decorative: leave `title` off.\n */\n mark?: React.ReactNode;\n}\n\n/**\n * A wait, from start to end. `role=\"status\"` (polite): its words are announced when it shows and when it ends. Under\n * prefers-reduced-motion the glyph rests on a still frame; the delay and the ending stay.\n */\nexport function Loader({\n label,\n status = \"loading\",\n successLabel,\n errorLabel,\n value,\n eta,\n delay = 250,\n minDuration = 600,\n endDuration = 1600,\n variant,\n mark,\n className,\n ...props\n}: LoaderProps) {\n const waiting = useDelayedWait(status === \"loading\", delay, minDuration);\n // Rendered already ended (a result to show), it shows its ending; otherwise only a wait that was seen ends visibly.\n const [ending, setEnding] = React.useState<\"success\" | \"error\" | null>(() => (status !== \"loading\" && endDuration > 0 ? status : null));\n const wasWaiting = React.useRef(false);\n React.useEffect(() => {\n if (!Number.isFinite(endDuration)) return;\n const hide = setTimeout(() => setEnding((e) => (wasWaiting.current ? e : null)), endDuration);\n return () => clearTimeout(hide);\n // Only the ending a loader was mounted with: later endings have their own timer below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n React.useEffect(() => {\n if (waiting) {\n wasWaiting.current = true;\n return;\n }\n if (!wasWaiting.current || status === \"loading\") return;\n wasWaiting.current = false;\n const end = status;\n const show = setTimeout(() => setEnding(endDuration > 0 ? end : null), 0);\n const hide = Number.isFinite(endDuration) ? setTimeout(() => setEnding(null), endDuration) : undefined;\n return () => {\n clearTimeout(show);\n clearTimeout(hide);\n };\n }, [waiting, status, endDuration]);\n\n const phase = waiting ? \"loading\" : ending;\n const s = loader({ variant, ended: phase === \"success\" || phase === \"error\" ? phase : undefined });\n const known = phase === \"loading\" && typeof value === \"number\" && Number.isFinite(value);\n const words = phase === \"success\" ? (successLabel ?? label) : phase === \"error\" ? (errorLabel ?? label) : label;\n const detail = phase === \"loading\" ? [known ? `${Math.round(Math.min(1, Math.max(0, value)) * 100)} %` : null, eta].filter(Boolean).join(\" · \") : \"\";\n return (\n <div role=\"status\" aria-live=\"polite\" data-phase={phase ?? \"idle\"} className={s.root({ className })} {...props}>\n {phase && (\n <div className={s.box()}>\n {mark && phase === \"loading\" ? (\n <span aria-hidden className={s.mark()} data-mark=\"\">\n {mark}\n </span>\n ) : (\n <LoaderGlyph state={phase} value={known ? value : null} className={s.glyph()} />\n )}\n <span className={s.text()}>\n <span className={s.label()}>{words}</span>\n {detail && <span className={s.detail()}>{detail}</span>}\n </span>\n </div>\n )}\n </div>\n );\n}\n\nexport interface LoadingRegionProps extends React.ComponentProps<\"div\"> {\n /** Whether the content is on its way. */\n loading: boolean;\n /** What stands in while it loads: skeletons the shape of the content (no layout shift when it arrives). */\n skeleton: React.ReactNode;\n /** What is loading, in words — passed translated; announced when the wait shows. */\n label: string;\n /** The words when the wait grows long (“Still loading — the server is busy”) — passed translated. */\n slowLabel?: string;\n /**\n * Milliseconds of skeleton before the words of a long wait appear over it.\n * @default 4000\n */\n slowAfter?: number;\n /**\n * Milliseconds before the skeleton shows: a fast answer goes straight to the content.\n * @default 200\n */\n delay?: number;\n /**\n * Once shown, the least milliseconds the skeleton stays.\n * @default 500\n */\n minDuration?: number;\n}\n\n/**\n * A region that hands over: its content → (after `delay`) a skeleton of the same shape → (after `slowAfter`) words over\n * the skeleton → its content again, fading in. `aria-busy` while it waits; the wait is announced once.\n */\nexport function LoadingRegion({\n loading,\n skeleton,\n label,\n slowLabel,\n slowAfter = 4000,\n delay = 200,\n minDuration = 500,\n className,\n children,\n ...props\n}: LoadingRegionProps) {\n const waiting = useDelayedWait(loading, delay, minDuration);\n const slow = useDelayedWait(waiting && loading, slowAfter, 0);\n const busy = loading || waiting;\n return (\n <div aria-busy={busy} data-phase={waiting ? (slow ? \"slow\" : \"skeleton\") : busy ? \"pending\" : \"ready\"} className={[\"relative\", className].filter(Boolean).join(\" \")} {...props}>\n <span role=\"status\" className=\"sr-only\">\n {waiting ? (slow ? (slowLabel ?? label) : label) : \"\"}\n </span>\n {waiting ? (\n <div aria-hidden className=\"kz-fade\">\n {skeleton}\n </div>\n ) : busy ? (\n // Within the delay: the skeleton holds the place, invisible — nothing flashes, nothing jumps.\n <div aria-hidden className=\"invisible\">\n {skeleton}\n </div>\n ) : (\n <div className=\"kz-fade\">{children}</div>\n )}\n {waiting && slow && <Loader label={slowLabel ?? label} variant=\"overlay\" delay={0} minDuration={0} endDuration={0} aria-live=\"off\" role=\"presentation\" />}\n </div>\n );\n}\n"
|
|
447
447
|
},
|
|
448
448
|
{
|
|
449
449
|
"path": "loader/meta.ts",
|
|
@@ -451,7 +451,7 @@
|
|
|
451
451
|
},
|
|
452
452
|
{
|
|
453
453
|
"path": "meta.ts",
|
|
454
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
454
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
455
455
|
}
|
|
456
456
|
],
|
|
457
457
|
"dependencies": [
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"path": "meta.ts",
|
|
163
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
163
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
164
164
|
}
|
|
165
165
|
],
|
|
166
166
|
"dependencies": [],
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
},
|
|
201
201
|
{
|
|
202
202
|
"path": "meta.ts",
|
|
203
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
203
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
204
204
|
}
|
|
205
205
|
],
|
|
206
206
|
"dependencies": [
|
package/registry/map.json
CHANGED
|
@@ -1046,7 +1046,7 @@
|
|
|
1046
1046
|
},
|
|
1047
1047
|
{
|
|
1048
1048
|
"path": "map/map.tsx",
|
|
1049
|
-
"content": "// Client. A map of the Krizaka family on MapLibre GL JS (open source) over Protomaps tiles on our CDN — no API key, no\n// metered service. MapLibre and PMTiles are optional peers, loaded when a map is shown (a dynamic import: a page without\n// a map never pays for them). The style comes from the tokens (`mapStyle`), in the page's scheme, and follows it.\n//\n// What every map does: the canvas is decorative for assistive technologies — the map is a region named by `label`, its\n// places a list read in order (the text alternative, `MapPlaces`), each marker a button when it does something; the\n// attribution of OpenStreetMap (ODbL) and Protomaps is always shown; under reduced motion the camera jumps, never flies;\n// without WebGL (or while the engine loads) `fallback` stands in its place.\n/// <reference types=\"geojson\" />\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\nimport { MAP_ATTRIBUTION, setMapAssetsBase } from \"./assets\";\nimport { circlePolygon, type LngLat, pointAlong, sliceLine } from \"./geo\";\nimport { type MapBrand, mapPalette, type MapRegion, mapStyle } from \"./style\";\n\ntype MapLibre = typeof import(\"maplibre-gl\");\ntype MapInstance = import(\"maplibre-gl\").Map;\n\nexport const map = tv({\n slots: {\n root: \"relative isolate overflow-hidden rounded-2xl border border-border-default bg-surface-2 shadow-raised\",\n canvas: \"absolute inset-0 [&_.maplibregl-canvas]:outline-hidden\",\n fallback: \"absolute inset-0\",\n controls: \"absolute right-3 top-3 z-10 flex flex-col overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-floating\",\n control:\n \"inline-flex h-10 w-10 items-center justify-center text-lg font-semibold text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:focus-ring-inset disabled:opacity-40 [&+&]:border-t [&+&]:border-border-subtle\",\n attribution:\n \"absolute bottom-1.5 right-1.5 z-10 rounded-md bg-surface-1/85 px-1.5 py-0.5 text-[10px] text-fg-secondary backdrop-blur-sm [&_a]:underline-offset-2 hover:[&_a]:underline\",\n places: \"sr-only\",\n marker:\n \"group relative flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-2 border-on-accent bg-accent shadow-floating outline-hidden \" +\n \"transition-transform hover:scale-110 focus-visible:focus-ring motion-reduce:transition-none motion-reduce:hover:scale-100 data-[selected]:scale-125\",\n markerLabel:\n \"pointer-events-none absolute bottom-full mb-2 whitespace-nowrap rounded-lg border border-border-default bg-surface-1 px-2 py-1 text-xs font-semibold text-fg shadow-floating\",\n },\n});\n\ntype Registry = { add: (id: string, text: string) => void; remove: (id: string) => void };\ntype Ctx = { map: MapInstance | null; lib: MapLibre | null; ready: boolean; reduce: boolean; places: Registry };\nconst MapContext = React.createContext<Ctx | null>(null);\n\n/** The map a child is drawn on (`Marker`, `RadiusCircle`, `Cluster`…): the MapLibre instance once it is ready. */\nexport function useMap(): Ctx {\n const ctx = React.useContext(MapContext);\n if (!ctx) throw new Error(\"useMap: inside a <Map> only\");\n return ctx;\n}\n\n/** The scheme of the page (`html.light`), followed as it changes. */\nfunction usePageScheme(): \"dark\" | \"light\" {\n const read = () => (typeof document !== \"undefined\" && document.documentElement.classList.contains(\"light\") ? \"light\" : \"dark\");\n const [scheme, setScheme] = React.useState<\"dark\" | \"light\">(read);\n React.useEffect(() => {\n const observer = new MutationObserver(() => setScheme(read()));\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n return () => observer.disconnect();\n }, []);\n return scheme;\n}\n\nlet protocolAdded = false;\nlet workerUrl: string | null = null;\n\n/**\n * Where MapLibre's worker comes from. A bundler (Next.js, Vite) renames MapLibre's module and leaves its worker behind:\n * by default the worker of the very version the app installed is imported from jsDelivr (free, open source, no key)\n * through a module worker of the page's own origin. An app with a strict CSP serves it itself —\n * `configureMaps({ workerUrl: \"/maplibre-gl-worker.mjs\" })` (with `maplibre-gl-shared.mjs` beside it), once, before a map.\n * `assetsBase` reads the tiles and glyphs through a same-origin proxy of the CDN (`setMapAssetsBase`).\n */\nexport function configureMaps(options: { workerUrl?: string; assetsBase?: string }) {\n if (options.workerUrl) workerUrl = options.workerUrl;\n if (options.assetsBase) setMapAssetsBase(options.assetsBase);\n}\n\n/** MapLibre, its worker and the PMTiles protocol, loaded once for the page. */\nasync function loadEngine(): Promise<MapLibre> {\n const [lib, pmtiles] = await Promise.all([import(\"maplibre-gl\"), import(\"pmtiles\")]);\n const ml = ((lib as unknown as { default?: MapLibre }).default ?? lib) as MapLibre;\n if (!protocolAdded) {\n protocolAdded = true;\n const remote = `https://cdn.jsdelivr.net/npm/maplibre-gl@${ml.getVersion()}/dist/maplibre-gl-worker.mjs`;\n ml.setWorkerUrl(workerUrl ?? URL.createObjectURL(new Blob([`import ${JSON.stringify(remote)};`], { type: \"text/javascript\" })));\n ml.addProtocol(\"pmtiles\", new pmtiles.Protocol().tile);\n }\n return ml;\n}\n\nconst hasWebGL = () => {\n try {\n const canvas = document.createElement(\"canvas\");\n return Boolean(canvas.getContext(\"webgl2\") ?? canvas.getContext(\"webgl\"));\n } catch {\n return false;\n }\n};\n\nexport type MapLabels = {\n /** The zoom-in button (\"Zoom in\"). */\n zoomIn: string;\n /** The zoom-out button (\"Zoom out\"). */\n zoomOut: string;\n /** Before the list of places read in place of the map (\"Places on the map\"). */\n places: string;\n};\n\nexport const mapLabels: MapLabels = { zoomIn: \"Zoom in\", zoomOut: \"Zoom out\", places: \"Places on the map\" };\n\nexport type MapProps = {\n /** The map's accessible name (\"Pick-up points near you\"), passed translated. */\n label: string;\n /** Where the map looks at first: longitude, latitude. */\n center: LngLat;\n /** The zoom at first (0 the world, 12 a city, 16 a street). Default 12. */\n zoom?: number;\n /** The camera follows `center` and `zoom` when they change (flies, or jumps under reduced motion). */\n controlled?: boolean;\n /** The scheme. Default: the page's (`html.light`), followed. */\n scheme?: \"dark\" | \"light\";\n /** The product whose accent the map carries. Default `krizaka`. */\n brand?: MapBrand;\n /** The language of the labels (`fr`, `en`). Default `en`. */\n lang?: string;\n /** The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14). */\n region?: MapRegion | null;\n /** The world's low zooms (PMTiles). Default: our world extract (to zoom 6). */\n world?: MapRegion | null;\n /** The zoom buttons (default) — the keyboard also zooms with + and − on the focused map. */\n controls?: boolean;\n /** Moves with the hand (default); `false` makes a still picture of a place. */\n interactive?: boolean;\n /** Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture. */\n fallback?: React.ReactNode;\n /** Called when the camera rests: its centre and zoom. */\n onMoveEnd?: (view: { center: LngLat; zoom: number }) => void;\n /** Called with the position pressed on the map (not on a marker). */\n onPress?: (position: LngLat) => void;\n /** Markers, circles, clusters, the user's position — and anything drawn over the map. */\n children?: React.ReactNode;\n /** The words, passed translated; English by default. */\n labels?: Partial<MapLabels>;\n /** Classes merged on the root (its height: `h-80`, `aspect-video`). */\n className?: string;\n};\n\n/** A map: MapLibre over our Protomaps tiles, styled from the tokens, with its places as a list for screen readers. */\nexport function Map({\n label,\n center,\n zoom = 12,\n controlled,\n scheme: schemeProp,\n brand,\n lang,\n region,\n world,\n controls = true,\n interactive = true,\n fallback,\n onMoveEnd,\n onPress,\n children,\n labels: given,\n className,\n}: MapProps) {\n const labels = { ...mapLabels, ...given };\n const s = map();\n const pageScheme = usePageScheme();\n const scheme = schemeProp ?? pageScheme;\n const reduce = usePrefersReducedMotion();\n const host = React.useRef<HTMLDivElement>(null);\n const [instance, setInstance] = React.useState<MapInstance | null>(null);\n const [lib, setLib] = React.useState<MapLibre | null>(null);\n const [ready, setReady] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const [places, setPlaces] = React.useState<Record<string, string>>({});\n const registry = React.useMemo<Registry>(\n () => ({\n add: (id, text) => setPlaces((p) => (p[id] === text ? p : { ...p, [id]: text })),\n remove: (id) =>\n setPlaces((p) => {\n const { [id]: _gone, ...rest } = p;\n return rest;\n }),\n }),\n [],\n );\n const latest = React.useRef({ onMoveEnd, onPress });\n React.useEffect(() => {\n latest.current = { onMoveEnd, onPress };\n });\n const styleOf = React.useCallback((sc: \"dark\" | \"light\") => mapStyle({ scheme: sc, brand, lang, ...(region !== undefined ? { region } : {}), ...(world !== undefined ? { world } : {}) }), [brand, lang, region, world]);\n\n // The style the map wears now: the engine loads asynchronously, so the scheme it is created with is read when it is ready\n // (the page's theme may have flipped since the first render), and the effect below applies whatever differs.\n const wanted = React.useRef({ scheme, styleOf });\n React.useEffect(() => {\n wanted.current = { scheme, styleOf };\n });\n const applied = React.useRef<{ scheme: \"dark\" | \"light\"; styleOf: typeof styleOf } | null>(null);\n\n // The engine and the map, once (the style follows the scheme below).\n React.useEffect(() => {\n if (!host.current) return;\n let alive = true;\n let created: MapInstance | null = null;\n (hasWebGL() ? loadEngine() : Promise.reject(new Error(\"no WebGL\")))\n .then((ml) => {\n if (!alive || !host.current) return;\n applied.current = wanted.current;\n created = new ml.Map({\n container: host.current,\n style: wanted.current.styleOf(wanted.current.scheme) as never,\n center: [...center] as [number, number],\n zoom,\n interactive,\n attributionControl: false,\n fadeDuration: reduce ? 0 : 300,\n cooperativeGestures: false,\n });\n created.getCanvas().setAttribute(\"aria-hidden\", \"true\");\n created.getCanvas().setAttribute(\"tabindex\", \"-1\");\n created.on(\"load\", () => alive && setReady(true));\n // The basemap cannot be read (offline, a blocked request): the fallback comes back rather than an empty map.\n created.on(\"error\", (e) => {\n const source = (e as { sourceId?: string }).sourceId;\n if (alive && (source === \"world\" || source === \"region\")) setFailed(true);\n });\n created.on(\"moveend\", () => {\n const c = created!.getCenter();\n latest.current.onMoveEnd?.({ center: [c.lng, c.lat], zoom: created!.getZoom() });\n });\n created.on(\"click\", (e) => {\n if ((e.originalEvent.target as Element | null)?.closest?.(\"[data-kz-marker]\")) return;\n latest.current.onPress?.([e.lngLat.lng, e.lngLat.lat]);\n });\n setLib(ml);\n setInstance(created);\n })\n .catch(() => alive && setFailed(true));\n return () => {\n alive = false;\n created?.remove();\n };\n // Created once: the props that change afterwards are followed by the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // The scheme (and the brand, the language) change the style, not the map.\n React.useEffect(() => {\n if (!instance) return;\n if (applied.current?.scheme === scheme && applied.current.styleOf === styleOf) return;\n applied.current = { scheme, styleOf };\n instance.setStyle(styleOf(scheme) as never);\n }, [instance, scheme, styleOf]);\n\n // A controlled camera: it flies (or jumps) to the new view.\n React.useEffect(() => {\n if (!instance || !controlled) return;\n const view = { center: [...center] as [number, number], zoom };\n if (reduce) instance.jumpTo(view);\n else instance.flyTo({ ...view, speed: 1.4 });\n }, [instance, controlled, center[0], center[1], zoom, reduce]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const zoomBy = (by: number) => instance && (reduce ? instance.setZoom(instance.getZoom() + by) : instance.zoomTo(instance.getZoom() + by, { duration: 250 }));\n const ctx = React.useMemo<Ctx>(() => ({ map: instance, lib, ready, reduce, places: registry }), [instance, lib, ready, reduce, registry]);\n const listed = Object.entries(places);\n return (\n <MapContext.Provider value={ctx}>\n <section\n aria-label={label}\n className={s.root({ className })}\n data-ready={ready ? \"\" : undefined}\n tabIndex={interactive ? 0 : undefined}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget || !instance) return;\n if (e.key === \"+\" || e.key === \"=\") zoomBy(1);\n else if (e.key === \"-\") zoomBy(-1);\n else if (e.key.startsWith(\"Arrow\")) {\n e.preventDefault();\n const step = 80;\n const by: Record<string, [number, number]> = { ArrowUp: [0, -step], ArrowDown: [0, step], ArrowLeft: [-step, 0], ArrowRight: [step, 0] };\n instance.panBy(by[e.key]!, { duration: reduce ? 0 : 200 });\n }\n }}\n >\n {!ready || failed ? <div className={s.fallback()}>{fallback}</div> : null}\n <div ref={host} className={s.canvas()} style={{ position: \"absolute\", inset: 0, visibility: failed ? \"hidden\" : undefined }} />\n {controls && interactive && !failed ? (\n <div className={s.controls()}>\n <button type=\"button\" className={s.control()} aria-label={labels.zoomIn} disabled={!instance} onClick={() => zoomBy(1)}>\n +\n </button>\n <button type=\"button\" className={s.control()} aria-label={labels.zoomOut} disabled={!instance} onClick={() => zoomBy(-1)}>\n −\n </button>\n </div>\n ) : null}\n <p className={s.attribution()}>\n <a href={MAP_ATTRIBUTION.protomapsUrl} target=\"_blank\" rel=\"noreferrer\">\n {MAP_ATTRIBUTION.protomaps}\n </a>\n {\" · \"}\n <a href={MAP_ATTRIBUTION.osmUrl} target=\"_blank\" rel=\"noreferrer\">\n {MAP_ATTRIBUTION.osm}\n </a>\n </p>\n {listed.length ? (\n <div className={s.places()}>\n <p>{labels.places}</p>\n <ul>\n {listed.map(([id, text]) => (\n <li key={id}>{text}</li>\n ))}\n </ul>\n </div>\n ) : null}\n {children}\n </section>\n </MapContext.Provider>\n );\n}\n\nexport type MarkerProps = {\n /** Where it stands. */\n position: LngLat;\n /** Its words: the place's name, read in the list of places and as the button's name (\"Café Olimpico, 350 m\"). */\n label: string;\n /** Pressing it (details in a panel, a sheet): the marker becomes a button. */\n onPress?: () => void;\n /** The chosen one: larger, its label shown above it. */\n selected?: boolean;\n /** Its size in pixels. Default 18. */\n size?: number;\n /** What it draws instead of the accent dot (an avatar, an icon). */\n children?: React.ReactNode;\n /** Drops onto its place once, after this many milliseconds (stagger several: `index * 90`). Still under reduced motion. */\n drop?: number;\n};\n\nconst noSubscription = () => () => undefined;\n\n/** A place on the map: an accent dot (or your content) that is a button when it does something. */\nexport function Marker({ position, label, onPress, selected, size = 18, children, drop }: MarkerProps) {\n const { map: m, lib, places } = useMap();\n const s = map();\n const id = React.useId();\n // The marker's host element exists only in a browser: the server and the hydration pass render nothing for it, then the\n // client swaps in the element — `useSyncExternalStore`'s server snapshot, so hydration never sees a difference.\n const host = React.useRef<HTMLElement | null>(null);\n const el = React.useSyncExternalStore(\n noSubscription,\n () => (host.current ??= document.createElement(\"div\")),\n () => null,\n );\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib || !el) return;\n el.setAttribute(\"data-kz-marker\", \"\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, el, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n if (!el) return null;\n const Tag = onPress ? \"button\" : \"span\";\n return createPortal(\n <Tag\n type={onPress ? \"button\" : undefined}\n aria-label={onPress ? label : undefined}\n aria-hidden={onPress ? undefined : true}\n aria-pressed={onPress && selected !== undefined ? selected : undefined}\n data-selected={selected ? \"\" : undefined}\n data-drop={drop === undefined ? undefined : \"\"}\n className={s.marker()}\n style={{ ...(children ? null : { width: size, height: size }), ...(drop === undefined ? null : ({ \"--kz-map-drop-delay\": `${drop}ms` } as React.CSSProperties)) }}\n onClick={onPress}\n >\n {children}\n {selected ? <span className={s.markerLabel()}>{label}</span> : null}\n </Tag>,\n el,\n );\n}\n\nexport type UserPositionProps = {\n /** Where the person is. */\n position: LngLat;\n /** Its words (\"You are here\"), read in the list of places. */\n label: string;\n /** The accuracy in metres: a soft disc around the dot. */\n accuracy?: number;\n};\n\n/** The person's position: a dot that pulses (still under reduced motion), its accuracy as a soft disc. */\nexport function UserPosition({ position, label, accuracy }: UserPositionProps) {\n const { map: m, lib, places } = useMap();\n const id = React.useId();\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-user\";\n el.setAttribute(\"aria-hidden\", \"true\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n return accuracy ? <RadiusCircle center={position} radius={accuracy} tone=\"soft\" /> : null;\n}\n\nexport type PulseProps = {\n /** Where it beats. */\n position: LngLat;\n /** Its words (\"High demand in Mile End\"), read in the list of places. */\n label: string;\n /** The rings' reach in pixels. Default 56. */\n size?: number;\n /** `accent` (a zone that matters) or `soft`. Default `accent`. */\n tone?: \"accent\" | \"soft\";\n};\n\n/** A heat pulse: rings that swell and fade around a place (a hot spot, a demand peak). Still rings under reduced motion. */\nexport function Pulse({ position, label, size = 56, tone = \"accent\" }: PulseProps) {\n const { map: m, lib, places } = useMap();\n const id = React.useId();\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-pulse\";\n el.setAttribute(\"data-tone\", tone);\n el.setAttribute(\"aria-hidden\", \"true\");\n el.style.setProperty(\"--kz-map-pulse-size\", `${size}px`);\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, tone, size, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n return null;\n}\n\nexport type RouteProps = {\n /** The path, as positions in order (a polyline: a delivery, a walk, a flight). */\n path: readonly LngLat[];\n /** Its words (\"Delivery from the studio to Mile End, 2.4 km\"), read in the list of places. */\n label: string;\n /** The line draws itself once, from its start, when the map is on screen; `false` shows it whole. Default `true`. */\n draw?: boolean;\n /** How long the drawing lasts, in milliseconds. Default 1400. */\n duration?: number;\n /** A dot that travels the route once drawn (`true`, or your own content: an avatar, an icon). */\n courier?: boolean | React.ReactNode;\n /** How long a trip takes, in milliseconds. Default 3200. */\n tripDuration?: number;\n /** Start the trips again after a pause (milliseconds), only while the map is on screen. Without it the courier makes one trip. */\n repeatAfter?: number;\n /** Changing it replays the drawing and the trip. */\n replayKey?: string | number;\n /** `accent` or `soft`. Default `accent`. */\n tone?: \"accent\" | \"soft\";\n /** The timing function of the drawing and of the trips: `ease-out`, `linear`, `ease-in-out`. Default: out for the drawing, in-out for a trip. */\n easing?: \"ease-out\" | \"linear\" | \"ease-in-out\";\n /** Holds the drawing and the trips where they are (a controlled pause, on top of the off-screen one). */\n paused?: boolean;\n};\n\nconst ROUTE_CURVES = {\n linear: (t: number) => t,\n \"ease-out\": (t: number) => 1 - (1 - t) ** 3,\n \"ease-in-out\": (t: number) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),\n} as const;\n\ntype RouteSource = { setData?: (d: GeoJSON.GeoJSON) => void };\n\n/** A route on the map: a line that draws itself, and a courier that travels it. Under reduced motion the line is whole and the courier rests at the end. */\nexport function Route({ path, label, draw = true, duration = 1400, courier, tripDuration = 3200, repeatAfter, replayKey, tone = \"accent\", easing, paused = false }: RouteProps) {\n const { map: m, lib, ready, reduce, places } = useMap();\n const id = `kz-route-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const animated = draw && !reduce;\n // The drawing is done for this `replayKey` (a new key draws again); without animation the line is whole at once.\n const token = replayKey ?? 0;\n const [doneFor, setDoneFor] = React.useState<string | number | null>(null);\n const drawn = !animated || doneFor === token;\n const whole = React.useMemo(() => sliceLine(path, 1), [path]);\n const start = React.useMemo(() => sliceLine(path, 0), [path]);\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n useGeoLayer(id, drawn ? whole : start, (c) => [\n { id: `${id}-casing`, type: \"line\", source: id, layout: { \"line-cap\": \"round\", \"line-join\": \"round\" }, paint: { \"line-color\": c.accent, \"line-width\": 9, \"line-opacity\": tone === \"soft\" ? 0.08 : 0.16, \"line-blur\": 3 } },\n { id: `${id}-line`, type: \"line\", source: id, layout: { \"line-cap\": \"round\", \"line-join\": \"round\" }, paint: { \"line-color\": c.accent, \"line-width\": tone === \"soft\" ? 2 : 3.5, \"line-opacity\": tone === \"soft\" ? 0.6 : 1 } },\n ]);\n // Is the map on screen? The drawing waits for it, and a repeating trip does not start while it is not.\n const [seen, setSeen] = React.useState(() => typeof IntersectionObserver === \"undefined\");\n const visible = seen && !paused;\n React.useEffect(() => {\n const el = m?.getContainer?.();\n if (!el || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver(([entry]) => setSeen(!!entry?.isIntersecting), { threshold: 0.25 });\n io.observe(el);\n return () => io.disconnect();\n }, [m]);\n // The drawing: a finite animation, one frame per paint for `duration` ms — no loop, nothing runs afterwards.\n React.useEffect(() => {\n if (!m || !ready || !animated || drawn || !visible) return;\n const source = m.getSource(id) as RouteSource | undefined;\n let frame = 0;\n const began = performance.now();\n const step = (now: number) => {\n const t = Math.min(1, (now - began) / Math.max(1, duration));\n source?.setData?.(sliceLine(path, ROUTE_CURVES[easing ?? \"ease-out\"](t)));\n if (t < 1) frame = requestAnimationFrame(step);\n else setDoneFor(token);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [m, ready, animated, drawn, visible, id, path, duration, token, easing]);\n // The courier: one trip once the line is drawn (finite frames), again after `repeatAfter` while on screen.\n const [trip, setTrip] = React.useState(0);\n React.useEffect(() => {\n if (!courier || !m || !lib || !ready || !drawn) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-courier\";\n el.setAttribute(\"aria-hidden\", \"true\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...(path[reduce ? path.length - 1 : 0] ?? [0, 0])] as [number, number]).addTo(m);\n let frame = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n if (!reduce && visible) {\n const began = performance.now();\n const step = (now: number) => {\n const t = Math.min(1, (now - began) / Math.max(1, tripDuration));\n const eased = ROUTE_CURVES[easing ?? \"ease-in-out\"](t);\n marker.setLngLat([...pointAlong(path, eased).position] as [number, number]);\n if (t < 1) frame = requestAnimationFrame(step);\n else if (repeatAfter !== undefined) timer = setTimeout(() => setTrip((n) => n + 1), repeatAfter);\n };\n frame = requestAnimationFrame(step);\n } else if (!reduce) marker.setLngLat([...(path[path.length - 1] ?? [0, 0])] as [number, number]);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(timer);\n marker.remove();\n };\n }, [courier, m, lib, ready, drawn, visible, reduce, path, tripDuration, repeatAfter, trip, token, easing]);\n return null;\n}\n\n/** Adds a GeoJSON source and its layers while the map has a style (again after a change of scheme). */\nfunction useGeoLayer(id: string, data: GeoJSON.GeoJSON, layers: (palette: ReturnType<typeof mapPalette>) => object[], source: object = {}) {\n const { map: m, ready } = useMap();\n const dataRef = React.useRef(data);\n React.useEffect(() => {\n dataRef.current = data;\n const src = m?.getSource(id) as { setData?: (d: GeoJSON.GeoJSON) => void } | undefined;\n src?.setData?.(data);\n }, [m, id, data]);\n React.useEffect(() => {\n if (!m || !ready) return;\n const add = () => {\n if (m.getSource(id)) return;\n const scheme = document.documentElement.classList.contains(\"light\") ? \"light\" : \"dark\";\n m.addSource(id, { type: \"geojson\", data: dataRef.current, ...source } as never);\n for (const layer of layers(mapPalette(scheme))) m.addLayer(layer as never);\n };\n add();\n m.on(\"styledata\", add);\n return () => {\n m.off(\"styledata\", add);\n if (!m.getStyle()) return;\n for (const layer of m.getStyle().layers.filter((l) => \"source\" in l && l.source === id)) m.removeLayer(layer.id);\n if (m.getSource(id)) m.removeSource(id);\n };\n }, [m, ready, id]); // eslint-disable-line react-hooks/exhaustive-deps\n}\n\nexport type RadiusCircleProps = {\n /** Its centre. */\n center: LngLat;\n /** Its radius in metres. */\n radius: number;\n /** `accent` (a zone that matters: a delivery area, a search radius) or `soft` (an accuracy). */\n tone?: \"accent\" | \"soft\";\n};\n\n/** A circle on the ground: a delivery zone, a search radius — filled with the accent, its edge drawn. */\nexport function RadiusCircle({ center, radius, tone = \"accent\" }: RadiusCircleProps) {\n const id = `kz-circle-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const data = React.useMemo(() => circlePolygon(center, radius), [center[0], center[1], radius]); // eslint-disable-line react-hooks/exhaustive-deps\n useGeoLayer(id, data, (c) => [\n { id: `${id}-fill`, type: \"fill\", source: id, paint: { \"fill-color\": c.accent, \"fill-opacity\": tone === \"soft\" ? 0.1 : 0.14 } },\n { id: `${id}-line`, type: \"line\", source: id, paint: { \"line-color\": c.accent, \"line-width\": tone === \"soft\" ? 1 : 2, \"line-opacity\": tone === \"soft\" ? 0.4 : 0.9, ...(tone === \"accent\" ? { \"line-dasharray\": [2, 1.5] } : {}) } },\n ]);\n return null;\n}\n\nexport type ClusterPoint = { id: string; position: LngLat; label: string };\n\nexport type ClusterProps = {\n /** The points: many of them (creators, pick-up points, events). */\n points: readonly ClusterPoint[];\n /** Pressing a single point. A group zooms in to open itself. */\n onPointPress?: (point: ClusterPoint) => void;\n /** The radius, in pixels, within which points group. Default 48. */\n radius?: number;\n};\n\n/** Many points grouped while they are close: a disc with the count, opened by zooming in; each point is listed for screen readers. */\nexport function Cluster({ points, onPointPress, radius = 48 }: ClusterProps) {\n const { map: m, ready, reduce, places } = useMap();\n const id = `kz-cluster-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const data = React.useMemo<GeoJSON.FeatureCollection>(\n () => ({ type: \"FeatureCollection\", features: points.map((p) => ({ type: \"Feature\", properties: { id: p.id, label: p.label }, geometry: { type: \"Point\", coordinates: [...p.position] } })) }),\n [points],\n );\n React.useEffect(() => {\n for (const p of points) places.add(`${id}-${p.id}`, p.label);\n return () => points.forEach((p) => places.remove(`${id}-${p.id}`));\n }, [points, places, id]);\n useGeoLayer(\n id,\n data,\n (c) => [\n { id: `${id}-groups`, type: \"circle\", source: id, filter: [\"has\", \"point_count\"], paint: { \"circle-color\": c.accent, \"circle-opacity\": 0.92, \"circle-radius\": [\"step\", [\"get\", \"point_count\"], 16, 10, 20, 50, 26], \"circle-stroke-width\": 3, \"circle-stroke-color\": c.halo } },\n { id: `${id}-count`, type: \"symbol\", source: id, filter: [\"has\", \"point_count\"], layout: { \"text-field\": [\"get\", \"point_count_abbreviated\"], \"text-font\": [\"Noto Sans Medium\"], \"text-size\": 13 }, paint: { \"text-color\": c.onAccent } },\n { id: `${id}-points`, type: \"circle\", source: id, filter: [\"!\", [\"has\", \"point_count\"]], paint: { \"circle-color\": c.accent, \"circle-radius\": 7, \"circle-stroke-width\": 2.5, \"circle-stroke-color\": c.halo } },\n ],\n { cluster: true, clusterRadius: radius, clusterMaxZoom: 15 },\n );\n React.useEffect(() => {\n if (!m || !ready) return;\n const openGroup = async (e: { features?: { properties: Record<string, unknown>; geometry: GeoJSON.Geometry }[] }) => {\n const f = e.features?.[0];\n if (!f) return;\n const src = m.getSource(id) as unknown as { getClusterExpansionZoom: (id: number) => Promise<number> };\n const z = await src.getClusterExpansionZoom(f.properties.cluster_id as number);\n const center = (f.geometry as GeoJSON.Point).coordinates as [number, number];\n if (reduce) m.jumpTo({ center, zoom: z });\n else m.easeTo({ center, zoom: z });\n };\n const pressPoint = (e: { features?: { properties: Record<string, unknown> }[] }) => {\n const pid = e.features?.[0]?.properties.id;\n const point = points.find((p) => p.id === pid);\n if (point) onPointPress?.(point);\n };\n m.on(\"click\", `${id}-groups`, openGroup as never);\n m.on(\"click\", `${id}-points`, pressPoint as never);\n return () => {\n m.off(\"click\", `${id}-groups`, openGroup as never);\n m.off(\"click\", `${id}-points`, pressPoint as never);\n };\n }, [m, ready, id, points, onPointPress, reduce]);\n return null;\n}\n"
|
|
1049
|
+
"content": "// Client. A map of the Krizaka family on MapLibre GL JS (open source) over Protomaps tiles on our CDN — no API key, no\n// metered service. MapLibre and PMTiles are optional peers, loaded when a map is shown (a dynamic import: a page without\n// a map never pays for them). The style comes from the tokens (`mapStyle`), in the page's scheme, and follows it.\n//\n// What every map does: the canvas is decorative for assistive technologies — the map is a region named by `label`, its\n// places a list read in order (the text alternative, `MapPlaces`), each marker a button when it does something; the\n// attribution of OpenStreetMap (ODbL) and Protomaps is always shown; under reduced motion the camera jumps, never flies;\n// without WebGL (or while the engine loads) `fallback` stands in its place.\n/// <reference types=\"geojson\" />\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\nimport { MAP_ATTRIBUTION, setMapAssetsBase } from \"./assets\";\nimport { circlePolygon, type LngLat, pointAlong, sliceLine } from \"./geo\";\nimport { type MapBrand, mapPalette, type MapRegion, mapStyle } from \"./style\";\n\ntype MapLibre = typeof import(\"maplibre-gl\");\ntype MapInstance = import(\"maplibre-gl\").Map;\n\nexport const map = tv({\n slots: {\n root: \"relative isolate overflow-hidden rounded-2xl border border-border-default bg-surface-2 shadow-raised\",\n canvas: \"absolute inset-0 [&_.maplibregl-canvas]:outline-hidden\",\n fallback: \"absolute inset-0\",\n controls: \"absolute right-3 top-3 z-10 flex flex-col overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-floating\",\n control:\n \"inline-flex h-10 w-10 items-center justify-center text-lg font-semibold text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:focus-ring-inset disabled:opacity-40 [&+&]:border-t [&+&]:border-border-subtle\",\n attribution:\n \"absolute bottom-1.5 right-1.5 z-10 rounded-md bg-surface-1 px-1.5 py-0.5 text-[10px] text-fg-secondary [&_a]:underline-offset-2 hover:[&_a]:underline\",\n places: \"sr-only\",\n marker:\n \"group relative flex -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-2 border-on-accent bg-accent shadow-floating outline-hidden \" +\n \"transition-transform hover:scale-110 focus-visible:focus-ring motion-reduce:transition-none motion-reduce:hover:scale-100 data-[selected]:scale-125\",\n markerLabel:\n \"pointer-events-none absolute bottom-full mb-2 whitespace-nowrap rounded-lg border border-border-default bg-surface-1 px-2 py-1 text-xs font-semibold text-fg shadow-floating\",\n },\n});\n\ntype Registry = { add: (id: string, text: string) => void; remove: (id: string) => void };\ntype Ctx = { map: MapInstance | null; lib: MapLibre | null; ready: boolean; reduce: boolean; places: Registry };\nconst MapContext = React.createContext<Ctx | null>(null);\n\n/** The map a child is drawn on (`Marker`, `RadiusCircle`, `Cluster`…): the MapLibre instance once it is ready. */\nexport function useMap(): Ctx {\n const ctx = React.useContext(MapContext);\n if (!ctx) throw new Error(\"useMap: inside a <Map> only\");\n return ctx;\n}\n\n/** The scheme of the page (`html.light`), followed as it changes. */\nfunction usePageScheme(): \"dark\" | \"light\" {\n const read = () => (typeof document !== \"undefined\" && document.documentElement.classList.contains(\"light\") ? \"light\" : \"dark\");\n const [scheme, setScheme] = React.useState<\"dark\" | \"light\">(read);\n React.useEffect(() => {\n const observer = new MutationObserver(() => setScheme(read()));\n observer.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n return () => observer.disconnect();\n }, []);\n return scheme;\n}\n\nlet protocolAdded = false;\nlet workerUrl: string | null = null;\n\n/**\n * Where MapLibre's worker comes from. A bundler (Next.js, Vite) renames MapLibre's module and leaves its worker behind:\n * by default the worker of the very version the app installed is imported from jsDelivr (free, open source, no key)\n * through a module worker of the page's own origin. An app with a strict CSP serves it itself —\n * `configureMaps({ workerUrl: \"/maplibre-gl-worker.mjs\" })` (with `maplibre-gl-shared.mjs` beside it), once, before a map.\n * `assetsBase` reads the tiles and glyphs through a same-origin proxy of the CDN (`setMapAssetsBase`).\n */\nexport function configureMaps(options: { workerUrl?: string; assetsBase?: string }) {\n if (options.workerUrl) workerUrl = options.workerUrl;\n if (options.assetsBase) setMapAssetsBase(options.assetsBase);\n}\n\n/** MapLibre, its worker and the PMTiles protocol, loaded once for the page. */\nasync function loadEngine(): Promise<MapLibre> {\n const [lib, pmtiles] = await Promise.all([import(\"maplibre-gl\"), import(\"pmtiles\")]);\n const ml = ((lib as unknown as { default?: MapLibre }).default ?? lib) as MapLibre;\n if (!protocolAdded) {\n protocolAdded = true;\n const remote = `https://cdn.jsdelivr.net/npm/maplibre-gl@${ml.getVersion()}/dist/maplibre-gl-worker.mjs`;\n ml.setWorkerUrl(workerUrl ?? URL.createObjectURL(new Blob([`import ${JSON.stringify(remote)};`], { type: \"text/javascript\" })));\n ml.addProtocol(\"pmtiles\", new pmtiles.Protocol().tile);\n }\n return ml;\n}\n\nconst hasWebGL = () => {\n try {\n const canvas = document.createElement(\"canvas\");\n return Boolean(canvas.getContext(\"webgl2\") ?? canvas.getContext(\"webgl\"));\n } catch {\n return false;\n }\n};\n\nexport type MapLabels = {\n /** The zoom-in button (\"Zoom in\"). */\n zoomIn: string;\n /** The zoom-out button (\"Zoom out\"). */\n zoomOut: string;\n /** Before the list of places read in place of the map (\"Places on the map\"). */\n places: string;\n};\n\nexport const mapLabels: MapLabels = { zoomIn: \"Zoom in\", zoomOut: \"Zoom out\", places: \"Places on the map\" };\n\nexport type MapProps = {\n /** The map's accessible name (\"Pick-up points near you\"), passed translated. */\n label: string;\n /** Where the map looks at first: longitude, latitude. */\n center: LngLat;\n /** The zoom at first (0 the world, 12 a city, 16 a street). Default 12. */\n zoom?: number;\n /** The camera follows `center` and `zoom` when they change (flies, or jumps under reduced motion). */\n controlled?: boolean;\n /** The scheme. Default: the page's (`html.light`), followed. */\n scheme?: \"dark\" | \"light\";\n /** The product whose accent the map carries. Default `krizaka`. */\n brand?: MapBrand;\n /** The language of the labels (`fr`, `en`). Default `en`. */\n lang?: string;\n /** The detailed region (PMTiles) drawn from zoom 6.5. Default: our Montréal–Québec extract (to zoom 14). */\n region?: MapRegion | null;\n /** The world's low zooms (PMTiles). Default: our world extract (to zoom 6). */\n world?: MapRegion | null;\n /** The zoom buttons (default) — the keyboard also zooms with + and − on the focused map. */\n controls?: boolean;\n /** Moves with the hand (default); `false` makes a still picture of a place. */\n interactive?: boolean;\n /** Shown in place of the map while its engine loads, and for good without WebGL: a `StaticMap`, a picture. */\n fallback?: React.ReactNode;\n /** Called when the camera rests: its centre and zoom. */\n onMoveEnd?: (view: { center: LngLat; zoom: number }) => void;\n /** Called with the position pressed on the map (not on a marker). */\n onPress?: (position: LngLat) => void;\n /** Markers, circles, clusters, the user's position — and anything drawn over the map. */\n children?: React.ReactNode;\n /** The words, passed translated; English by default. */\n labels?: Partial<MapLabels>;\n /** Classes merged on the root (its height: `h-80`, `aspect-video`). */\n className?: string;\n};\n\n/** A map: MapLibre over our Protomaps tiles, styled from the tokens, with its places as a list for screen readers. */\nexport function Map({\n label,\n center,\n zoom = 12,\n controlled,\n scheme: schemeProp,\n brand,\n lang,\n region,\n world,\n controls = true,\n interactive = true,\n fallback,\n onMoveEnd,\n onPress,\n children,\n labels: given,\n className,\n}: MapProps) {\n const labels = { ...mapLabels, ...given };\n const s = map();\n const pageScheme = usePageScheme();\n const scheme = schemeProp ?? pageScheme;\n const reduce = usePrefersReducedMotion();\n const host = React.useRef<HTMLDivElement>(null);\n const [instance, setInstance] = React.useState<MapInstance | null>(null);\n const [lib, setLib] = React.useState<MapLibre | null>(null);\n const [ready, setReady] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const [places, setPlaces] = React.useState<Record<string, string>>({});\n const registry = React.useMemo<Registry>(\n () => ({\n add: (id, text) => setPlaces((p) => (p[id] === text ? p : { ...p, [id]: text })),\n remove: (id) =>\n setPlaces((p) => {\n const { [id]: _gone, ...rest } = p;\n return rest;\n }),\n }),\n [],\n );\n const latest = React.useRef({ onMoveEnd, onPress });\n React.useEffect(() => {\n latest.current = { onMoveEnd, onPress };\n });\n const styleOf = React.useCallback((sc: \"dark\" | \"light\") => mapStyle({ scheme: sc, brand, lang, ...(region !== undefined ? { region } : {}), ...(world !== undefined ? { world } : {}) }), [brand, lang, region, world]);\n\n // The style the map wears now: the engine loads asynchronously, so the scheme it is created with is read when it is ready\n // (the page's theme may have flipped since the first render), and the effect below applies whatever differs.\n const wanted = React.useRef({ scheme, styleOf });\n React.useEffect(() => {\n wanted.current = { scheme, styleOf };\n });\n const applied = React.useRef<{ scheme: \"dark\" | \"light\"; styleOf: typeof styleOf } | null>(null);\n\n // The engine and the map, once (the style follows the scheme below).\n React.useEffect(() => {\n if (!host.current) return;\n let alive = true;\n let created: MapInstance | null = null;\n (hasWebGL() ? loadEngine() : Promise.reject(new Error(\"no WebGL\")))\n .then((ml) => {\n if (!alive || !host.current) return;\n applied.current = wanted.current;\n created = new ml.Map({\n container: host.current,\n style: wanted.current.styleOf(wanted.current.scheme) as never,\n center: [...center] as [number, number],\n zoom,\n interactive,\n attributionControl: false,\n fadeDuration: reduce ? 0 : 300,\n cooperativeGestures: false,\n });\n created.getCanvas().setAttribute(\"aria-hidden\", \"true\");\n created.getCanvas().setAttribute(\"tabindex\", \"-1\");\n created.on(\"load\", () => alive && setReady(true));\n // The basemap cannot be read (offline, a blocked request): the fallback comes back rather than an empty map.\n created.on(\"error\", (e) => {\n const source = (e as { sourceId?: string }).sourceId;\n if (alive && (source === \"world\" || source === \"region\")) setFailed(true);\n });\n created.on(\"moveend\", () => {\n const c = created!.getCenter();\n latest.current.onMoveEnd?.({ center: [c.lng, c.lat], zoom: created!.getZoom() });\n });\n created.on(\"click\", (e) => {\n if ((e.originalEvent.target as Element | null)?.closest?.(\"[data-kz-marker]\")) return;\n latest.current.onPress?.([e.lngLat.lng, e.lngLat.lat]);\n });\n setLib(ml);\n setInstance(created);\n })\n .catch(() => alive && setFailed(true));\n return () => {\n alive = false;\n created?.remove();\n };\n // Created once: the props that change afterwards are followed by the effects below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // The scheme (and the brand, the language) change the style, not the map.\n React.useEffect(() => {\n if (!instance) return;\n if (applied.current?.scheme === scheme && applied.current.styleOf === styleOf) return;\n applied.current = { scheme, styleOf };\n instance.setStyle(styleOf(scheme) as never);\n }, [instance, scheme, styleOf]);\n\n // A controlled camera: it flies (or jumps) to the new view.\n React.useEffect(() => {\n if (!instance || !controlled) return;\n const view = { center: [...center] as [number, number], zoom };\n if (reduce) instance.jumpTo(view);\n else instance.flyTo({ ...view, speed: 1.4 });\n }, [instance, controlled, center[0], center[1], zoom, reduce]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const zoomBy = (by: number) => instance && (reduce ? instance.setZoom(instance.getZoom() + by) : instance.zoomTo(instance.getZoom() + by, { duration: 250 }));\n const ctx = React.useMemo<Ctx>(() => ({ map: instance, lib, ready, reduce, places: registry }), [instance, lib, ready, reduce, registry]);\n const listed = Object.entries(places);\n return (\n <MapContext.Provider value={ctx}>\n <section\n aria-label={label}\n className={s.root({ className })}\n data-ready={ready ? \"\" : undefined}\n tabIndex={interactive ? 0 : undefined}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget || !instance) return;\n if (e.key === \"+\" || e.key === \"=\") zoomBy(1);\n else if (e.key === \"-\") zoomBy(-1);\n else if (e.key.startsWith(\"Arrow\")) {\n e.preventDefault();\n const step = 80;\n const by: Record<string, [number, number]> = { ArrowUp: [0, -step], ArrowDown: [0, step], ArrowLeft: [-step, 0], ArrowRight: [step, 0] };\n instance.panBy(by[e.key]!, { duration: reduce ? 0 : 200 });\n }\n }}\n >\n {!ready || failed ? <div className={s.fallback()}>{fallback}</div> : null}\n <div ref={host} className={s.canvas()} style={{ position: \"absolute\", inset: 0, visibility: failed ? \"hidden\" : undefined }} />\n {controls && interactive && !failed ? (\n <div className={s.controls()}>\n <button type=\"button\" className={s.control()} aria-label={labels.zoomIn} disabled={!instance} onClick={() => zoomBy(1)}>\n +\n </button>\n <button type=\"button\" className={s.control()} aria-label={labels.zoomOut} disabled={!instance} onClick={() => zoomBy(-1)}>\n −\n </button>\n </div>\n ) : null}\n <p className={s.attribution()}>\n <a href={MAP_ATTRIBUTION.protomapsUrl} target=\"_blank\" rel=\"noreferrer\">\n {MAP_ATTRIBUTION.protomaps}\n </a>\n {\" · \"}\n <a href={MAP_ATTRIBUTION.osmUrl} target=\"_blank\" rel=\"noreferrer\">\n {MAP_ATTRIBUTION.osm}\n </a>\n </p>\n {listed.length ? (\n <div className={s.places()}>\n <p>{labels.places}</p>\n <ul>\n {listed.map(([id, text]) => (\n <li key={id}>{text}</li>\n ))}\n </ul>\n </div>\n ) : null}\n {children}\n </section>\n </MapContext.Provider>\n );\n}\n\nexport type MarkerProps = {\n /** Where it stands. */\n position: LngLat;\n /** Its words: the place's name, read in the list of places and as the button's name (\"Café Olimpico, 350 m\"). */\n label: string;\n /** Pressing it (details in a panel, a sheet): the marker becomes a button. */\n onPress?: () => void;\n /** The chosen one: larger, its label shown above it. */\n selected?: boolean;\n /** Its size in pixels. Default 18. */\n size?: number;\n /** What it draws instead of the accent dot (an avatar, an icon). */\n children?: React.ReactNode;\n /** Drops onto its place once, after this many milliseconds (stagger several: `index * 90`). Still under reduced motion. */\n drop?: number;\n};\n\nconst noSubscription = () => () => undefined;\n\n/** A place on the map: an accent dot (or your content) that is a button when it does something. */\nexport function Marker({ position, label, onPress, selected, size = 18, children, drop }: MarkerProps) {\n const { map: m, lib, places } = useMap();\n const s = map();\n const id = React.useId();\n // The marker's host element exists only in a browser: the server and the hydration pass render nothing for it, then the\n // client swaps in the element — `useSyncExternalStore`'s server snapshot, so hydration never sees a difference.\n const host = React.useRef<HTMLElement | null>(null);\n const el = React.useSyncExternalStore(\n noSubscription,\n () => (host.current ??= document.createElement(\"div\")),\n () => null,\n );\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib || !el) return;\n el.setAttribute(\"data-kz-marker\", \"\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, el, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n if (!el) return null;\n const Tag = onPress ? \"button\" : \"span\";\n return createPortal(\n <Tag\n type={onPress ? \"button\" : undefined}\n aria-label={onPress ? label : undefined}\n aria-hidden={onPress ? undefined : true}\n aria-pressed={onPress && selected !== undefined ? selected : undefined}\n data-selected={selected ? \"\" : undefined}\n data-drop={drop === undefined ? undefined : \"\"}\n className={s.marker()}\n style={{ ...(children ? null : { width: size, height: size }), ...(drop === undefined ? null : ({ \"--kz-map-drop-delay\": `${drop}ms` } as React.CSSProperties)) }}\n onClick={onPress}\n >\n {children}\n {selected ? <span className={s.markerLabel()}>{label}</span> : null}\n </Tag>,\n el,\n );\n}\n\nexport type UserPositionProps = {\n /** Where the person is. */\n position: LngLat;\n /** Its words (\"You are here\"), read in the list of places. */\n label: string;\n /** The accuracy in metres: a soft disc around the dot. */\n accuracy?: number;\n};\n\n/** The person's position: a dot that pulses (still under reduced motion), its accuracy as a soft disc. */\nexport function UserPosition({ position, label, accuracy }: UserPositionProps) {\n const { map: m, lib, places } = useMap();\n const id = React.useId();\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-user\";\n el.setAttribute(\"aria-hidden\", \"true\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n return accuracy ? <RadiusCircle center={position} radius={accuracy} tone=\"soft\" /> : null;\n}\n\nexport type PulseProps = {\n /** Where it beats. */\n position: LngLat;\n /** Its words (\"High demand in Mile End\"), read in the list of places. */\n label: string;\n /** The rings' reach in pixels. Default 56. */\n size?: number;\n /** `accent` (a zone that matters) or `soft`. Default `accent`. */\n tone?: \"accent\" | \"soft\";\n};\n\n/** A heat pulse: rings that swell and fade around a place (a hot spot, a demand peak). Still rings under reduced motion. */\nexport function Pulse({ position, label, size = 56, tone = \"accent\" }: PulseProps) {\n const { map: m, lib, places } = useMap();\n const id = React.useId();\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n React.useEffect(() => {\n if (!m || !lib) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-pulse\";\n el.setAttribute(\"data-tone\", tone);\n el.setAttribute(\"aria-hidden\", \"true\");\n el.style.setProperty(\"--kz-map-pulse-size\", `${size}px`);\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...position] as [number, number]).addTo(m);\n return () => void marker.remove();\n }, [m, lib, tone, size, position[0], position[1]]); // eslint-disable-line react-hooks/exhaustive-deps\n return null;\n}\n\nexport type RouteProps = {\n /** The path, as positions in order (a polyline: a delivery, a walk, a flight). */\n path: readonly LngLat[];\n /** Its words (\"Delivery from the studio to Mile End, 2.4 km\"), read in the list of places. */\n label: string;\n /** The line draws itself once, from its start, when the map is on screen; `false` shows it whole. Default `true`. */\n draw?: boolean;\n /** How long the drawing lasts, in milliseconds. Default 1400. */\n duration?: number;\n /** A dot that travels the route once drawn (`true`, or your own content: an avatar, an icon). */\n courier?: boolean | React.ReactNode;\n /** How long a trip takes, in milliseconds. Default 3200. */\n tripDuration?: number;\n /** Start the trips again after a pause (milliseconds), only while the map is on screen. Without it the courier makes one trip. */\n repeatAfter?: number;\n /** Changing it replays the drawing and the trip. */\n replayKey?: string | number;\n /** `accent` or `soft`. Default `accent`. */\n tone?: \"accent\" | \"soft\";\n /** The timing function of the drawing and of the trips: `ease-out`, `linear`, `ease-in-out`. Default: out for the drawing, in-out for a trip. */\n easing?: \"ease-out\" | \"linear\" | \"ease-in-out\";\n /** Holds the drawing and the trips where they are (a controlled pause, on top of the off-screen one). */\n paused?: boolean;\n};\n\nconst ROUTE_CURVES = {\n linear: (t: number) => t,\n \"ease-out\": (t: number) => 1 - (1 - t) ** 3,\n \"ease-in-out\": (t: number) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),\n} as const;\n\ntype RouteSource = { setData?: (d: GeoJSON.GeoJSON) => void };\n\n/** A route on the map: a line that draws itself, and a courier that travels it. Under reduced motion the line is whole and the courier rests at the end. */\nexport function Route({ path, label, draw = true, duration = 1400, courier, tripDuration = 3200, repeatAfter, replayKey, tone = \"accent\", easing, paused = false }: RouteProps) {\n const { map: m, lib, ready, reduce, places } = useMap();\n const id = `kz-route-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const animated = draw && !reduce;\n // The drawing is done for this `replayKey` (a new key draws again); without animation the line is whole at once.\n const token = replayKey ?? 0;\n const [doneFor, setDoneFor] = React.useState<string | number | null>(null);\n const drawn = !animated || doneFor === token;\n const whole = React.useMemo(() => sliceLine(path, 1), [path]);\n const start = React.useMemo(() => sliceLine(path, 0), [path]);\n React.useEffect(() => {\n places.add(id, label);\n return () => places.remove(id);\n }, [id, label, places]);\n useGeoLayer(id, drawn ? whole : start, (c) => [\n { id: `${id}-casing`, type: \"line\", source: id, layout: { \"line-cap\": \"round\", \"line-join\": \"round\" }, paint: { \"line-color\": c.accent, \"line-width\": 9, \"line-opacity\": tone === \"soft\" ? 0.08 : 0.16, \"line-blur\": 3 } },\n { id: `${id}-line`, type: \"line\", source: id, layout: { \"line-cap\": \"round\", \"line-join\": \"round\" }, paint: { \"line-color\": c.accent, \"line-width\": tone === \"soft\" ? 2 : 3.5, \"line-opacity\": tone === \"soft\" ? 0.6 : 1 } },\n ]);\n // Is the map on screen? The drawing waits for it, and a repeating trip does not start while it is not.\n const [seen, setSeen] = React.useState(() => typeof IntersectionObserver === \"undefined\");\n const visible = seen && !paused;\n React.useEffect(() => {\n const el = m?.getContainer?.();\n if (!el || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver(([entry]) => setSeen(!!entry?.isIntersecting), { threshold: 0.25 });\n io.observe(el);\n return () => io.disconnect();\n }, [m]);\n // The drawing: a finite animation, one frame per paint for `duration` ms — no loop, nothing runs afterwards.\n React.useEffect(() => {\n if (!m || !ready || !animated || drawn || !visible) return;\n const source = m.getSource(id) as RouteSource | undefined;\n let frame = 0;\n const began = performance.now();\n const step = (now: number) => {\n const t = Math.min(1, (now - began) / Math.max(1, duration));\n source?.setData?.(sliceLine(path, ROUTE_CURVES[easing ?? \"ease-out\"](t)));\n if (t < 1) frame = requestAnimationFrame(step);\n else setDoneFor(token);\n };\n frame = requestAnimationFrame(step);\n return () => cancelAnimationFrame(frame);\n }, [m, ready, animated, drawn, visible, id, path, duration, token, easing]);\n // The courier: one trip once the line is drawn (finite frames), again after `repeatAfter` while on screen.\n const [trip, setTrip] = React.useState(0);\n React.useEffect(() => {\n if (!courier || !m || !lib || !ready || !drawn) return;\n const el = document.createElement(\"div\");\n el.className = \"kz-map-courier\";\n el.setAttribute(\"aria-hidden\", \"true\");\n const marker = new lib.Marker({ element: el, anchor: \"center\" }).setLngLat([...(path[reduce ? path.length - 1 : 0] ?? [0, 0])] as [number, number]).addTo(m);\n let frame = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n if (!reduce && visible) {\n const began = performance.now();\n const step = (now: number) => {\n const t = Math.min(1, (now - began) / Math.max(1, tripDuration));\n const eased = ROUTE_CURVES[easing ?? \"ease-in-out\"](t);\n marker.setLngLat([...pointAlong(path, eased).position] as [number, number]);\n if (t < 1) frame = requestAnimationFrame(step);\n else if (repeatAfter !== undefined) timer = setTimeout(() => setTrip((n) => n + 1), repeatAfter);\n };\n frame = requestAnimationFrame(step);\n } else if (!reduce) marker.setLngLat([...(path[path.length - 1] ?? [0, 0])] as [number, number]);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(timer);\n marker.remove();\n };\n }, [courier, m, lib, ready, drawn, visible, reduce, path, tripDuration, repeatAfter, trip, token, easing]);\n return null;\n}\n\n/** Adds a GeoJSON source and its layers while the map has a style (again after a change of scheme). */\nfunction useGeoLayer(id: string, data: GeoJSON.GeoJSON, layers: (palette: ReturnType<typeof mapPalette>) => object[], source: object = {}) {\n const { map: m, ready } = useMap();\n const dataRef = React.useRef(data);\n React.useEffect(() => {\n dataRef.current = data;\n const src = m?.getSource(id) as { setData?: (d: GeoJSON.GeoJSON) => void } | undefined;\n src?.setData?.(data);\n }, [m, id, data]);\n React.useEffect(() => {\n if (!m || !ready) return;\n const add = () => {\n if (m.getSource(id)) return;\n const scheme = document.documentElement.classList.contains(\"light\") ? \"light\" : \"dark\";\n m.addSource(id, { type: \"geojson\", data: dataRef.current, ...source } as never);\n for (const layer of layers(mapPalette(scheme))) m.addLayer(layer as never);\n };\n add();\n m.on(\"styledata\", add);\n return () => {\n m.off(\"styledata\", add);\n if (!m.getStyle()) return;\n for (const layer of m.getStyle().layers.filter((l) => \"source\" in l && l.source === id)) m.removeLayer(layer.id);\n if (m.getSource(id)) m.removeSource(id);\n };\n }, [m, ready, id]); // eslint-disable-line react-hooks/exhaustive-deps\n}\n\nexport type RadiusCircleProps = {\n /** Its centre. */\n center: LngLat;\n /** Its radius in metres. */\n radius: number;\n /** `accent` (a zone that matters: a delivery area, a search radius) or `soft` (an accuracy). */\n tone?: \"accent\" | \"soft\";\n};\n\n/** A circle on the ground: a delivery zone, a search radius — filled with the accent, its edge drawn. */\nexport function RadiusCircle({ center, radius, tone = \"accent\" }: RadiusCircleProps) {\n const id = `kz-circle-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const data = React.useMemo(() => circlePolygon(center, radius), [center[0], center[1], radius]); // eslint-disable-line react-hooks/exhaustive-deps\n useGeoLayer(id, data, (c) => [\n { id: `${id}-fill`, type: \"fill\", source: id, paint: { \"fill-color\": c.accent, \"fill-opacity\": tone === \"soft\" ? 0.1 : 0.14 } },\n { id: `${id}-line`, type: \"line\", source: id, paint: { \"line-color\": c.accent, \"line-width\": tone === \"soft\" ? 1 : 2, \"line-opacity\": tone === \"soft\" ? 0.4 : 0.9, ...(tone === \"accent\" ? { \"line-dasharray\": [2, 1.5] } : {}) } },\n ]);\n return null;\n}\n\nexport type ClusterPoint = { id: string; position: LngLat; label: string };\n\nexport type ClusterProps = {\n /** The points: many of them (creators, pick-up points, events). */\n points: readonly ClusterPoint[];\n /** Pressing a single point. A group zooms in to open itself. */\n onPointPress?: (point: ClusterPoint) => void;\n /** The radius, in pixels, within which points group. Default 48. */\n radius?: number;\n};\n\n/** Many points grouped while they are close: a disc with the count, opened by zooming in; each point is listed for screen readers. */\nexport function Cluster({ points, onPointPress, radius = 48 }: ClusterProps) {\n const { map: m, ready, reduce, places } = useMap();\n const id = `kz-cluster-${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n const data = React.useMemo<GeoJSON.FeatureCollection>(\n () => ({ type: \"FeatureCollection\", features: points.map((p) => ({ type: \"Feature\", properties: { id: p.id, label: p.label }, geometry: { type: \"Point\", coordinates: [...p.position] } })) }),\n [points],\n );\n React.useEffect(() => {\n for (const p of points) places.add(`${id}-${p.id}`, p.label);\n return () => points.forEach((p) => places.remove(`${id}-${p.id}`));\n }, [points, places, id]);\n useGeoLayer(\n id,\n data,\n (c) => [\n { id: `${id}-groups`, type: \"circle\", source: id, filter: [\"has\", \"point_count\"], paint: { \"circle-color\": c.accent, \"circle-opacity\": 0.92, \"circle-radius\": [\"step\", [\"get\", \"point_count\"], 16, 10, 20, 50, 26], \"circle-stroke-width\": 3, \"circle-stroke-color\": c.halo } },\n { id: `${id}-count`, type: \"symbol\", source: id, filter: [\"has\", \"point_count\"], layout: { \"text-field\": [\"get\", \"point_count_abbreviated\"], \"text-font\": [\"Noto Sans Medium\"], \"text-size\": 13 }, paint: { \"text-color\": c.onAccent } },\n { id: `${id}-points`, type: \"circle\", source: id, filter: [\"!\", [\"has\", \"point_count\"]], paint: { \"circle-color\": c.accent, \"circle-radius\": 7, \"circle-stroke-width\": 2.5, \"circle-stroke-color\": c.halo } },\n ],\n { cluster: true, clusterRadius: radius, clusterMaxZoom: 15 },\n );\n React.useEffect(() => {\n if (!m || !ready) return;\n const openGroup = async (e: { features?: { properties: Record<string, unknown>; geometry: GeoJSON.Geometry }[] }) => {\n const f = e.features?.[0];\n if (!f) return;\n const src = m.getSource(id) as unknown as { getClusterExpansionZoom: (id: number) => Promise<number> };\n const z = await src.getClusterExpansionZoom(f.properties.cluster_id as number);\n const center = (f.geometry as GeoJSON.Point).coordinates as [number, number];\n if (reduce) m.jumpTo({ center, zoom: z });\n else m.easeTo({ center, zoom: z });\n };\n const pressPoint = (e: { features?: { properties: Record<string, unknown> }[] }) => {\n const pid = e.features?.[0]?.properties.id;\n const point = points.find((p) => p.id === pid);\n if (point) onPointPress?.(point);\n };\n m.on(\"click\", `${id}-groups`, openGroup as never);\n m.on(\"click\", `${id}-points`, pressPoint as never);\n return () => {\n m.off(\"click\", `${id}-groups`, openGroup as never);\n m.off(\"click\", `${id}-points`, pressPoint as never);\n };\n }, [m, ready, id, points, onPointPress, reduce]);\n return null;\n}\n"
|
|
1050
1050
|
},
|
|
1051
1051
|
{
|
|
1052
1052
|
"path": "map/meta.ts",
|
|
@@ -1066,7 +1066,7 @@
|
|
|
1066
1066
|
},
|
|
1067
1067
|
{
|
|
1068
1068
|
"path": "meta.ts",
|
|
1069
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
1069
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
1070
1070
|
},
|
|
1071
1071
|
{
|
|
1072
1072
|
"path": "tv.ts",
|
|
@@ -1074,7 +1074,7 @@
|
|
|
1074
1074
|
}
|
|
1075
1075
|
],
|
|
1076
1076
|
"dependencies": [
|
|
1077
|
-
"@krizaka/tokens@^3.
|
|
1077
|
+
"@krizaka/tokens@^3.37.0",
|
|
1078
1078
|
"@protomaps/basemaps@5.7.2",
|
|
1079
1079
|
"tailwind-variants@^3.3.1"
|
|
1080
1080
|
],
|