@delacour/react-native-ui 0.1.0-alpha.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +49 -5
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -221
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -207
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -120
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -52
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -70
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +9 -1
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/screen/screen-navbar-back-button.tsx +1 -1
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { BadgeColor } from "../badge/badge.variants";
|
|
4
|
+
import { CARD_SIZES } from "../card/card.variants";
|
|
5
|
+
import type { ChartDatum } from "../chart/chart.types";
|
|
6
|
+
|
|
7
|
+
/** The card's own three sizes — a KPI is a card, so it has no scale of its own. */
|
|
8
|
+
export const KPI_SIZES = CARD_SIZES;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Which way is the good news. `up` for revenue and sign-ups, `down` for churn
|
|
12
|
+
* and latency, `none` for a number that is neither — a headcount, a version.
|
|
13
|
+
*/
|
|
14
|
+
export const KPI_GOOD_DIRECTIONS = ["up", "down", "none"] as const;
|
|
15
|
+
|
|
16
|
+
/** Which way the number moved. `flat` is a movement inside the threshold. */
|
|
17
|
+
export const KPI_DIRECTIONS = ["up", "down", "flat"] as const;
|
|
18
|
+
|
|
19
|
+
/** What the movement means — the colour it is painted, whatever its sign. */
|
|
20
|
+
export const KPI_TONES = ["good", "bad", "neutral"] as const;
|
|
21
|
+
|
|
22
|
+
/** `text` is a line of colour under the number; `badge` puts a pill round it, with an arrow. */
|
|
23
|
+
export const KPI_TREND_VARIANTS = ["text", "badge"] as const;
|
|
24
|
+
|
|
25
|
+
/** `below` puts the sparkline under everything; `inline` sets it in a column beside the number. */
|
|
26
|
+
export const KPI_LAYOUTS = ["below", "inline"] as const;
|
|
27
|
+
|
|
28
|
+
/** The five slots of the theme's series ramp, `--chart-1` … `--chart-5`. */
|
|
29
|
+
export const KPI_COLOR_INDEXES = [1, 2, 3, 4, 5] as const;
|
|
30
|
+
|
|
31
|
+
/** `horizontal` splits a row between the metrics; `vertical` stacks them. */
|
|
32
|
+
export const KPI_GROUP_ORIENTATIONS = ["horizontal", "vertical"] as const;
|
|
33
|
+
|
|
34
|
+
export type KpiSize = (typeof KPI_SIZES)[number];
|
|
35
|
+
export type KpiGoodDirection = (typeof KPI_GOOD_DIRECTIONS)[number];
|
|
36
|
+
export type KpiDirection = (typeof KPI_DIRECTIONS)[number];
|
|
37
|
+
export type KpiTone = (typeof KPI_TONES)[number];
|
|
38
|
+
export type KpiTrendVariant = (typeof KPI_TREND_VARIANTS)[number];
|
|
39
|
+
export type KpiLayout = (typeof KPI_LAYOUTS)[number];
|
|
40
|
+
export type KpiColorIndex = (typeof KPI_COLOR_INDEXES)[number];
|
|
41
|
+
export type KpiGroupOrientation = (typeof KPI_GROUP_ORIENTATIONS)[number];
|
|
42
|
+
|
|
43
|
+
/** A badge trend's colour, by what the movement means. */
|
|
44
|
+
export const KPI_TONE_BADGE_COLOR: Record<KpiTone, BadgeColor> = {
|
|
45
|
+
good: "success",
|
|
46
|
+
bad: "destructive",
|
|
47
|
+
neutral: "default",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Styling for every part of a KPI.
|
|
52
|
+
*
|
|
53
|
+
* The card itself — fill, hairline, corner, the vertical rhythm and the inset
|
|
54
|
+
* — is `Card`'s. The header and content slots here carry **no inset**: the
|
|
55
|
+
* parts compose them onto `cardVariants`' own `header` and `content`, so a
|
|
56
|
+
* KPI's number sits on exactly the line its card's footer does and a retune of
|
|
57
|
+
* the card moves both.
|
|
58
|
+
*
|
|
59
|
+
* The value is keyed on `plane` the same way `Card.Title` is, so a KPI nested
|
|
60
|
+
* in a secondary surface reads `secondary-foreground` rather than the card's
|
|
61
|
+
* token on something else. The title is `muted-foreground` everywhere: it is
|
|
62
|
+
* a label, and the value is the thing being read.
|
|
63
|
+
*
|
|
64
|
+
* The title never grows. A growing child of a column absorbs that column's
|
|
65
|
+
* spare height, which in a row of cards lands each number at a different
|
|
66
|
+
* height; the header pushes its action to the end with `ml-auto` instead.
|
|
67
|
+
*
|
|
68
|
+
* The trend is coloured by `tone` — what the movement means — and never by its
|
|
69
|
+
* sign: a fall in churn is good news, drawn as good news. The two colours are
|
|
70
|
+
* the `-soft-foreground` state tokens, which are built to be read as text on a
|
|
71
|
+
* card in both themes.
|
|
72
|
+
*
|
|
73
|
+
* The icon square is the series colour at 15%, one token per slot written out,
|
|
74
|
+
* because Tailwind's scanner cannot see a `bg-chart-${n}` built at runtime.
|
|
75
|
+
*
|
|
76
|
+
* View slots carry no `text-*` (rule 1). Free of React Native imports so it
|
|
77
|
+
* stays unit-testable.
|
|
78
|
+
*/
|
|
79
|
+
export const kpiVariants = tv({
|
|
80
|
+
slots: {
|
|
81
|
+
header: "flex-row items-center gap-2",
|
|
82
|
+
icon: "shrink-0 items-center justify-center rounded-md",
|
|
83
|
+
/** Not a view — the class an unstyled `Icon` inside `Kpi.Icon` adopts. */
|
|
84
|
+
iconGlyph: "",
|
|
85
|
+
title: "font-medium text-muted-foreground",
|
|
86
|
+
action: "ml-auto shrink-0 flex-row items-center gap-2",
|
|
87
|
+
content: "self-stretch",
|
|
88
|
+
stat: "gap-1",
|
|
89
|
+
value: "font-semibold",
|
|
90
|
+
valuePlaceholder: "rounded-md bg-muted-foreground/15",
|
|
91
|
+
trend: "flex-row flex-wrap items-center gap-x-1.5 gap-y-1",
|
|
92
|
+
trendText: "font-medium",
|
|
93
|
+
trendCaption: "text-muted-foreground",
|
|
94
|
+
trendPlaceholder: "rounded-sm bg-muted-foreground/15",
|
|
95
|
+
sparkline: "",
|
|
96
|
+
sparklineFrame: "",
|
|
97
|
+
sparklinePlaceholder: "w-full rounded-md bg-muted-foreground/15",
|
|
98
|
+
group: "gap-3",
|
|
99
|
+
groupItem: "",
|
|
100
|
+
},
|
|
101
|
+
variants: {
|
|
102
|
+
size: {
|
|
103
|
+
sm: {
|
|
104
|
+
icon: "size-6",
|
|
105
|
+
iconGlyph: "size-icon-xs",
|
|
106
|
+
title: "text-xs",
|
|
107
|
+
value: "text-2xl",
|
|
108
|
+
valuePlaceholder: "h-8 w-24",
|
|
109
|
+
trendText: "text-xs",
|
|
110
|
+
trendCaption: "text-xs",
|
|
111
|
+
trendPlaceholder: "h-4 w-20",
|
|
112
|
+
},
|
|
113
|
+
md: {
|
|
114
|
+
icon: "size-8",
|
|
115
|
+
iconGlyph: "size-icon-sm",
|
|
116
|
+
title: "text-sm",
|
|
117
|
+
value: "text-3xl",
|
|
118
|
+
valuePlaceholder: "h-9 w-28",
|
|
119
|
+
trendText: "text-sm",
|
|
120
|
+
trendCaption: "text-sm",
|
|
121
|
+
trendPlaceholder: "h-5 w-24",
|
|
122
|
+
},
|
|
123
|
+
lg: {
|
|
124
|
+
icon: "size-10",
|
|
125
|
+
iconGlyph: "size-icon-lg",
|
|
126
|
+
title: "text-base",
|
|
127
|
+
value: "text-4xl",
|
|
128
|
+
valuePlaceholder: "h-10 w-36",
|
|
129
|
+
trendText: "text-base",
|
|
130
|
+
trendCaption: "text-base",
|
|
131
|
+
trendPlaceholder: "h-6 w-28",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
plane: {
|
|
135
|
+
default: { value: "text-card-foreground" },
|
|
136
|
+
secondary: { value: "text-secondary-foreground" },
|
|
137
|
+
tertiary: { value: "text-tertiary-foreground" },
|
|
138
|
+
none: { value: "text-foreground" },
|
|
139
|
+
},
|
|
140
|
+
tone: {
|
|
141
|
+
good: { trendText: "text-success-soft-foreground" },
|
|
142
|
+
bad: { trendText: "text-destructive-soft-foreground" },
|
|
143
|
+
neutral: { trendText: "text-muted-foreground" },
|
|
144
|
+
},
|
|
145
|
+
colorIndex: {
|
|
146
|
+
1: { icon: "bg-chart-1/15" },
|
|
147
|
+
2: { icon: "bg-chart-2/15" },
|
|
148
|
+
3: { icon: "bg-chart-3/15" },
|
|
149
|
+
4: { icon: "bg-chart-4/15" },
|
|
150
|
+
5: { icon: "bg-chart-5/15" },
|
|
151
|
+
},
|
|
152
|
+
layout: {
|
|
153
|
+
below: { sparkline: "w-full" },
|
|
154
|
+
inline: {
|
|
155
|
+
content: "flex-row items-end gap-4",
|
|
156
|
+
stat: "min-w-0 flex-1",
|
|
157
|
+
sparkline: "w-32 shrink-0",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
orientation: {
|
|
161
|
+
horizontal: { group: "flex-row", groupItem: "min-w-0 flex-1" },
|
|
162
|
+
vertical: {},
|
|
163
|
+
},
|
|
164
|
+
// A separated group is one surface with rules between its metrics; the
|
|
165
|
+
// rule is the spacing, so there is no gap to add to it.
|
|
166
|
+
separated: {
|
|
167
|
+
true: { group: "gap-0" },
|
|
168
|
+
false: {},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
compoundVariants: [
|
|
172
|
+
{ layout: "below", size: "sm", class: { sparklineFrame: "h-12", sparklinePlaceholder: "h-12" } },
|
|
173
|
+
{ layout: "below", size: "md", class: { sparklineFrame: "h-16", sparklinePlaceholder: "h-16" } },
|
|
174
|
+
{ layout: "below", size: "lg", class: { sparklineFrame: "h-20", sparklinePlaceholder: "h-20" } },
|
|
175
|
+
{ layout: "inline", size: "sm", class: { sparklineFrame: "h-8", sparklinePlaceholder: "h-8" } },
|
|
176
|
+
{ layout: "inline", size: "md", class: { sparklineFrame: "h-10", sparklinePlaceholder: "h-10" } },
|
|
177
|
+
{ layout: "inline", size: "lg", class: { sparklineFrame: "h-12", sparklinePlaceholder: "h-12" } },
|
|
178
|
+
],
|
|
179
|
+
defaultVariants: {
|
|
180
|
+
size: "md",
|
|
181
|
+
plane: "default",
|
|
182
|
+
tone: "neutral",
|
|
183
|
+
colorIndex: 1,
|
|
184
|
+
layout: "below",
|
|
185
|
+
orientation: "vertical",
|
|
186
|
+
separated: false,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export type KpiVariantProps = VariantProps<typeof kpiVariants>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Which way a number moved and what that means.
|
|
194
|
+
*
|
|
195
|
+
* The sign carries the direction, so there is no separate direction prop to
|
|
196
|
+
* keep in step with the value. A movement no larger than `threshold` counts as
|
|
197
|
+
* none, and a value that is not finite — a change against a zero baseline —
|
|
198
|
+
* is flat rather than painted as news of either kind.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveKpiTrend({
|
|
201
|
+
value,
|
|
202
|
+
goodDirection = "up",
|
|
203
|
+
threshold = 0,
|
|
204
|
+
}: {
|
|
205
|
+
value: number;
|
|
206
|
+
goodDirection?: KpiGoodDirection;
|
|
207
|
+
threshold?: number;
|
|
208
|
+
}): { direction: KpiDirection; tone: KpiTone } {
|
|
209
|
+
const floor = Math.max(0, threshold);
|
|
210
|
+
if (!Number.isFinite(value) || Math.abs(value) <= floor) {
|
|
211
|
+
return { direction: "flat", tone: "neutral" };
|
|
212
|
+
}
|
|
213
|
+
const direction = value > 0 ? "up" : "down";
|
|
214
|
+
if (goodDirection === "none") return { direction, tone: "neutral" };
|
|
215
|
+
return { direction, tone: direction === goodDirection ? "good" : "bad" };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** U+2212, the width of a plus sign. A hyphen-minus is narrower and misaligns a column. */
|
|
219
|
+
const MINUS = "−";
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A signed percentage to one decimal place: `+7.8%`, `−4.2%`, `0.0%`.
|
|
223
|
+
*
|
|
224
|
+
* Signed explicitly both ways, with the typographic minus. A value that rounds
|
|
225
|
+
* to zero is printed unsigned — `−0.0%` would claim a fall nothing shows. A
|
|
226
|
+
* value that is not finite prints as an em dash.
|
|
227
|
+
*/
|
|
228
|
+
export function formatKpiTrend(value: number): string {
|
|
229
|
+
if (!Number.isFinite(value)) return "—";
|
|
230
|
+
const magnitude = Math.abs(value).toFixed(1);
|
|
231
|
+
if (Number(magnitude) === 0) return `${magnitude}%`;
|
|
232
|
+
return `${value > 0 ? "+" : MINUS}${magnitude}%`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const DIRECTION_WORD: Record<KpiDirection, string> = { up: "Up", down: "Down", flat: "No change" };
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* What a screen reader says for a trend, as one string.
|
|
239
|
+
*
|
|
240
|
+
* "Up 7.8 percent, vs last month" rather than an arrow, a number and a caption
|
|
241
|
+
* read as three unrelated stops. A caller's own `formatted` text may not be a
|
|
242
|
+
* percentage at all, so it is read as written after the direction.
|
|
243
|
+
*/
|
|
244
|
+
export function kpiTrendAccessibilityLabel({
|
|
245
|
+
value,
|
|
246
|
+
direction,
|
|
247
|
+
caption,
|
|
248
|
+
formatted,
|
|
249
|
+
}: {
|
|
250
|
+
value: number;
|
|
251
|
+
direction: KpiDirection;
|
|
252
|
+
caption?: string;
|
|
253
|
+
formatted?: string;
|
|
254
|
+
}): string {
|
|
255
|
+
const head =
|
|
256
|
+
direction === "flat"
|
|
257
|
+
? DIRECTION_WORD.flat
|
|
258
|
+
: formatted === undefined
|
|
259
|
+
? `${DIRECTION_WORD[direction]} ${Math.abs(value).toFixed(1)} percent`
|
|
260
|
+
: `${DIRECTION_WORD[direction]}, ${formatted}`;
|
|
261
|
+
return caption === undefined || caption === "" ? head : `${head}, ${caption}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A sparkline's data: a bare list of numbers, or rows with the keys to read.
|
|
266
|
+
*
|
|
267
|
+
* The two are told apart by `xKey`, which rows need and numbers do not have.
|
|
268
|
+
*/
|
|
269
|
+
export type KpiSparklineData =
|
|
270
|
+
| { readonly data: readonly number[]; readonly xKey?: undefined; readonly yKey?: undefined }
|
|
271
|
+
| { readonly data: readonly ChartDatum[]; readonly xKey: string; readonly yKey: string };
|
|
272
|
+
|
|
273
|
+
/** What the sparkline hands `Chart`, plus the plain values for everything else to read. */
|
|
274
|
+
export type KpiSparklineSeries = {
|
|
275
|
+
readonly rows: readonly ChartDatum[];
|
|
276
|
+
readonly xKey: string;
|
|
277
|
+
readonly yKey: string;
|
|
278
|
+
/** One per row, `NaN` where a row has no number. */
|
|
279
|
+
readonly values: readonly number[];
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
function numberOf(value: unknown): number {
|
|
283
|
+
return typeof value === "number" ? value : Number.NaN;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Turns either data shape into rows `Chart` can plot.
|
|
288
|
+
*
|
|
289
|
+
* A bare list of numbers is indexed — `{ index, value }` per point — because a
|
|
290
|
+
* sparkline's x is only ever its order. Rows pass through untouched, keyed by
|
|
291
|
+
* the caller.
|
|
292
|
+
*/
|
|
293
|
+
export function resolveSparklineSeries(input: KpiSparklineData): KpiSparklineSeries {
|
|
294
|
+
if (input.xKey === undefined) {
|
|
295
|
+
const rows = input.data.map((value, index) => ({ index, value }));
|
|
296
|
+
return { rows, xKey: "index", yKey: "value", values: input.data };
|
|
297
|
+
}
|
|
298
|
+
const { data, xKey, yKey } = input;
|
|
299
|
+
return { rows: data, xKey, yKey, values: data.map((row) => numberOf(row[yKey])) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The fraction of the data's span added above its peak and below its trough. */
|
|
303
|
+
const SPARKLINE_HEADROOM = 0.1;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The y bounds a sparkline plots against: the data's own extent, padded.
|
|
307
|
+
*
|
|
308
|
+
* A sparkline has no axis and no padding, so a line drawn to the data's exact
|
|
309
|
+
* extent puts its peak and its trough on the frame's edge with half the stroke
|
|
310
|
+
* off the canvas. A tenth of the span each side keeps both inside. A flat
|
|
311
|
+
* series has no span, so it gets a unit either side and sits centred.
|
|
312
|
+
*/
|
|
313
|
+
export function resolveSparklineDomain(values: readonly number[]): readonly [number, number] | undefined {
|
|
314
|
+
const finite = values.filter(Number.isFinite);
|
|
315
|
+
if (finite.length === 0) return undefined;
|
|
316
|
+
const min = Math.min(...finite);
|
|
317
|
+
const max = Math.max(...finite);
|
|
318
|
+
const pad = max === min ? 1 : (max - min) * SPARKLINE_HEADROOM;
|
|
319
|
+
return [min - pad, max + pad];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Whether the region under the line is filled.
|
|
324
|
+
*
|
|
325
|
+
* On under the card, where the chart has the full width and is looked at
|
|
326
|
+
* properly; off beside the number, where the chart is a gesture and a fill
|
|
327
|
+
* would make it a second block competing with the value. An explicit choice
|
|
328
|
+
* wins either way.
|
|
329
|
+
*/
|
|
330
|
+
export function resolveSparklineFilled({ layout, filled }: { layout: KpiLayout; filled?: boolean }): boolean {
|
|
331
|
+
return filled ?? layout === "below";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* What a screen reader says for a sparkline: how many points, and where it
|
|
336
|
+
* started and ended — the two numbers a glance at the shape is reading.
|
|
337
|
+
*/
|
|
338
|
+
export function kpiSparklineAccessibilityLabel(
|
|
339
|
+
values: readonly number[],
|
|
340
|
+
format: (value: number) => string = String
|
|
341
|
+
): string {
|
|
342
|
+
const finite = values.filter(Number.isFinite);
|
|
343
|
+
const first = finite[0];
|
|
344
|
+
const last = finite[finite.length - 1];
|
|
345
|
+
if (first === undefined || last === undefined) return "No trend data";
|
|
346
|
+
return `Trend over ${values.length} points, from ${format(first)} to ${format(last)}`;
|
|
347
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Label
|
|
2
|
+
|
|
3
|
+
The name of a form control, with required, invalid and disabled states. One
|
|
4
|
+
component, no parts.
|
|
5
|
+
|
|
6
|
+
`import { Label } from "@delacour/react-native-ui/label";`
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.ts` | → `@delacour/react-native-ui/label` |
|
|
13
|
+
| `label.tsx` | `Label` |
|
|
14
|
+
| `label.variants.ts` | Pure `tv()` slots + resolvers, no RN imports |
|
|
15
|
+
| `label.variants.test.ts` | |
|
|
16
|
+
|
|
17
|
+
## Design
|
|
18
|
+
|
|
19
|
+
- **States**: `isRequired`, `isInvalid`, `isDisabled`. All default `false`, all
|
|
20
|
+
independent — a required field that is also wrong shows both.
|
|
21
|
+
- **It *is* `Text.Label`, and passes a colour, never a scale.** The same move
|
|
22
|
+
[`Field`](../field/AGENTS.md)'s text parts make: a `text-sm font-medium` in a
|
|
23
|
+
slot here would be a second definition of [`Text.Label`](../text/AGENTS.md)
|
|
24
|
+
that could drift from it. `resolveLabelColor` returns `destructive` when
|
|
25
|
+
invalid and `undefined` otherwise, and `undefined` means "leave the preset's
|
|
26
|
+
own colour alone". A caller's `color` beats it; `size` and `weight` are
|
|
27
|
+
`Text`'s own axes and work unchanged. A test asserts neither slot carries a
|
|
28
|
+
size, weight or colour.
|
|
29
|
+
- **The required mark is a nested `Text`, not a sibling.** A row of two `Text`s
|
|
30
|
+
would put the asterisk beside the label's **box**: a label long enough to wrap
|
|
31
|
+
would carry it in a column of its own at the right edge of the first line. As a
|
|
32
|
+
nested run it flows with the text and lands after the last word — and a
|
|
33
|
+
no-break space in front of it (` *`) keeps it from wrapping onto a line by
|
|
34
|
+
itself. It inherits the label's size and weight through `Text`'s own nesting,
|
|
35
|
+
so there is nothing to keep in step.
|
|
36
|
+
- **The mark is destructive in every state.** Required is a warning about what
|
|
37
|
+
the form will refuse, so it wears the colour of that refusal whether or not the
|
|
38
|
+
value is wrong yet. `LABEL_REQUIRED_MARK_COLOR` is the constant;
|
|
39
|
+
`requiredMarkClassName` restyles it.
|
|
40
|
+
- **Only the root fades when disabled.** The mark is a run inside the root's own
|
|
41
|
+
text, so the root's `opacity-50` already reaches it. Fading it again would
|
|
42
|
+
square the fade. A disabled label also reports `accessibilityState.disabled`,
|
|
43
|
+
so a screen reader says "dimmed" rather than leaving the fade visual-only.
|
|
44
|
+
- **A required label is announced as "Email, required", not "Email, star".**
|
|
45
|
+
React Native reads a `Text` and its nested runs as one string, so without an
|
|
46
|
+
`accessibilityLabel` VoiceOver speaks the punctuation. `resolveLabelAccessibilityLabel`
|
|
47
|
+
builds one when the children are plain text — `labelText` reads strings,
|
|
48
|
+
numbers and arrays of them, and gives up on anything else rather than guessing
|
|
49
|
+
at an element's words. A caller's own `accessibilityLabel` always wins, and is
|
|
50
|
+
the answer when the children are not text.
|
|
51
|
+
- **`children` and `accessibilityLabel` are narrowed from `Text`'s.** `Text`
|
|
52
|
+
renders `Animated.Text`, whose props also accept Reanimated `SharedValue`s. A
|
|
53
|
+
label has to append its mark to its children and read them as text, and a
|
|
54
|
+
shared value supports neither, so the narrower type is the honest one — the
|
|
55
|
+
reason [`Field.Error`](../field/AGENTS.md) narrows its own children.
|
|
56
|
+
- **It reads no context.** A standalone primitive is whatever it is told, which
|
|
57
|
+
is what makes it usable outside a `Field` — above a `Slider`, beside a
|
|
58
|
+
`Switch`, in a settings row — without a layout it does not need.
|
|
59
|
+
`Field.Label` stays the field-aware counterpart: it reads the enclosing
|
|
60
|
+
field's state, and it is **not** built on `Label`, because doing so would
|
|
61
|
+
change its public surface — `Label` narrows `children`, and a disabled `Label`
|
|
62
|
+
reports `accessibilityState.disabled`, which `Field.Label` does not. The two
|
|
63
|
+
must still read as one component, so `label.variants.test.ts` pins
|
|
64
|
+
`resolveLabelColor` and the disabled fade to `fieldVariants`' label slot and
|
|
65
|
+
`resolveFieldTextColor("label", …)`. Change one and the test names the other.
|
|
66
|
+
- **Inside a `Field`, `Label` is the way to say "required".** `Field.Label` has
|
|
67
|
+
no `isRequired`. Pass the field's state yourself —
|
|
68
|
+
`<Label isInvalid={isInvalid} isRequired>Email</Label>` — or read it in a
|
|
69
|
+
custom part with `useField()`.
|
|
70
|
+
- **There is no `htmlFor`.** React Native has no `<label>` element and no
|
|
71
|
+
label-for-control association. On Android, give the label a `nativeID` and the
|
|
72
|
+
control `accessibilityLabelledBy` with the same string; iOS has no equivalent,
|
|
73
|
+
which is why a control's own `accessibilityLabel` matters more than the label
|
|
74
|
+
beside it.
|
|
75
|
+
- **There is no `Label.Text` part.** The label is already text; a `View` root
|
|
76
|
+
holding a text part would be the row of two boxes the nested mark exists to
|
|
77
|
+
avoid, and would take `ViewProps` where every caller wants `Text`'s `size`,
|
|
78
|
+
`weight` and `numberOfLines`.
|
|
79
|
+
- **No `onPress` wiring.** `Text` already takes `onPress`, so a label that should
|
|
80
|
+
focus its control passes one — the same thing `Field`'s whole-row press does
|
|
81
|
+
for a checkbox. Mounting a gesture detector under every label in a form would
|
|
82
|
+
announce static text as something to activate, which
|
|
83
|
+
[`Badge`](../badge/AGENTS.md) refuses for the same reason. A `Text` with `onPress` is exposed to VoiceOver as a **link**, and
|
|
84
|
+
`accessible={false}` does not change that on iOS — checked on a simulator,
|
|
85
|
+
where it still read as `AXLink`. A caller wiring one should pass
|
|
86
|
+
`accessibilityElementsHidden` and `importantForAccessibility="no-hide-descendants"`,
|
|
87
|
+
and let the control, which carries its own `accessibilityLabel`, be the element
|
|
88
|
+
a screen reader lands on — the move `Field`'s row makes. The playground's
|
|
89
|
+
`/label` "Beside a switch" demo shows it.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { Text, type TextProps } from "../text";
|
|
3
|
+
import {
|
|
4
|
+
LABEL_REQUIRED_MARK,
|
|
5
|
+
LABEL_REQUIRED_MARK_COLOR,
|
|
6
|
+
labelVariants,
|
|
7
|
+
resolveLabelAccessibilityLabel,
|
|
8
|
+
resolveLabelColor,
|
|
9
|
+
} from "./label.variants";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `Text`'s props minus the role it already is, with two narrowed.
|
|
13
|
+
*
|
|
14
|
+
* `Text` renders `Animated.Text`, so its `children` and `accessibilityLabel` may
|
|
15
|
+
* also be Reanimated `SharedValue`s. A label has to append its required mark to
|
|
16
|
+
* its children and read them as text for a screen reader, and neither can be
|
|
17
|
+
* done to a shared value, so the narrower type is the honest one — the move
|
|
18
|
+
* `Field.Error` makes for the same reason.
|
|
19
|
+
*/
|
|
20
|
+
export type LabelProps = Omit<TextProps, "variant" | "children" | "accessibilityLabel"> & {
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
/** What a screen reader announces. Defaults to "Email, required" for a required label of plain text. */
|
|
23
|
+
accessibilityLabel?: string;
|
|
24
|
+
/** Appends a destructive asterisk, and announces the label as required. */
|
|
25
|
+
isRequired?: boolean;
|
|
26
|
+
/** Turns the label destructive, to match a control whose value is wrong. */
|
|
27
|
+
isInvalid?: boolean;
|
|
28
|
+
/** Fades the label, to match a control that is unavailable. */
|
|
29
|
+
isDisabled?: boolean;
|
|
30
|
+
/** Merged into the required mark's class. */
|
|
31
|
+
requiredMarkClassName?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The name of a form control.
|
|
36
|
+
*
|
|
37
|
+
* Renders `Text.Label` and passes it a colour, never a size or a weight, so the
|
|
38
|
+
* type scale stays the preset's. `isInvalid` turns it destructive and
|
|
39
|
+
* `isDisabled` fades it, to read as one state with the control beside it.
|
|
40
|
+
*
|
|
41
|
+
* `isRequired` appends an asterisk as a **nested** `Text` rather than a sibling,
|
|
42
|
+
* with a no-break space in front of it, so a label long enough to wrap carries
|
|
43
|
+
* the mark on its last line, glued to its last word, instead of in a column of
|
|
44
|
+
* its own. A nested run is read as part of the label's text, so a required label
|
|
45
|
+
* whose children are text is announced as "Email, required" rather than
|
|
46
|
+
* "Email, star". Pass `accessibilityLabel` when the children are not text.
|
|
47
|
+
*
|
|
48
|
+
* A standalone primitive: it reads no context, so its state is whatever it is
|
|
49
|
+
* told. `Field.Label` is the field-aware counterpart — it reads a `Field`'s
|
|
50
|
+
* state for itself, and colours and fades exactly as this does, which a test
|
|
51
|
+
* holds. Reach for this one outside a `Field`, or inside one when the label has
|
|
52
|
+
* to say "required", which `Field.Label` does not.
|
|
53
|
+
*
|
|
54
|
+
* There is no `htmlFor`: React Native has no label-for-control association. On
|
|
55
|
+
* Android, give the label a `nativeID` and the control
|
|
56
|
+
* `accessibilityLabelledBy` with the same string.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* <Label isRequired>Email</Label>
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* <Label isInvalid={!isValid} isRequired>
|
|
63
|
+
* Email
|
|
64
|
+
* </Label>
|
|
65
|
+
*/
|
|
66
|
+
export function Label({
|
|
67
|
+
accessibilityLabel,
|
|
68
|
+
accessibilityState,
|
|
69
|
+
children,
|
|
70
|
+
className,
|
|
71
|
+
color,
|
|
72
|
+
isDisabled = false,
|
|
73
|
+
isInvalid = false,
|
|
74
|
+
isRequired = false,
|
|
75
|
+
requiredMarkClassName,
|
|
76
|
+
...props
|
|
77
|
+
}: LabelProps): ReactElement {
|
|
78
|
+
const slots = labelVariants({ isDisabled, isInvalid });
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Text.Label
|
|
82
|
+
accessibilityLabel={resolveLabelAccessibilityLabel({ accessibilityLabel, children, isRequired })}
|
|
83
|
+
accessibilityState={isDisabled ? { ...accessibilityState, disabled: true } : accessibilityState}
|
|
84
|
+
className={slots.root({ className })}
|
|
85
|
+
color={color ?? resolveLabelColor(isInvalid)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{children}
|
|
89
|
+
{isRequired ? (
|
|
90
|
+
<Text className={slots.requiredMark({ className: requiredMarkClassName })} color={LABEL_REQUIRED_MARK_COLOR}>
|
|
91
|
+
{` ${LABEL_REQUIRED_MARK}`}
|
|
92
|
+
</Text>
|
|
93
|
+
) : null}
|
|
94
|
+
</Text.Label>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
Label.displayName = "DelacourUI.Label";
|