@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,52 @@
|
|
|
1
|
+
import { Children, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { CardFooterProps } from "../card";
|
|
3
|
+
import { Card } from "../card";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { useKpiPart } from "./kpi.context";
|
|
6
|
+
import { kpiVariants } from "./kpi.variants";
|
|
7
|
+
|
|
8
|
+
export type KpiFooterProps = CardFooterProps;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wraps each run of bare strings and numbers in one muted caption. A raw string
|
|
12
|
+
* inside a `View` is a red box in React Native, and the run is joined rather
|
|
13
|
+
* than wrapped piece by piece: `last {days} days` is three children, and three
|
|
14
|
+
* `Text`s in the footer's row would sit apart by its gap.
|
|
15
|
+
*/
|
|
16
|
+
function wrapBareText(children: ReactNode, className: string): ReactNode[] {
|
|
17
|
+
const output: ReactNode[] = [];
|
|
18
|
+
let run: string[] = [];
|
|
19
|
+
const flush = (): void => {
|
|
20
|
+
if (run.length === 0) return;
|
|
21
|
+
output.push(
|
|
22
|
+
<Text className={className} key={`text-${output.length}`}>
|
|
23
|
+
{run.join("")}
|
|
24
|
+
</Text>
|
|
25
|
+
);
|
|
26
|
+
run = [];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (const child of Children.toArray(children)) {
|
|
30
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
31
|
+
run.push(String(child));
|
|
32
|
+
} else {
|
|
33
|
+
flush();
|
|
34
|
+
output.push(child);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
flush();
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The bottom strip — a comparison period, a caveat, a link.
|
|
43
|
+
*
|
|
44
|
+
* `Card.Footer`, so `variant="band"` sets it into the card on the next fill
|
|
45
|
+
* down, and it must be the last child for the same reason. Bare text becomes a
|
|
46
|
+
* muted caption at the card's scale.
|
|
47
|
+
*/
|
|
48
|
+
export function KpiFooter({ children, ...props }: KpiFooterProps): ReactElement {
|
|
49
|
+
const { size } = useKpiPart("Kpi.Footer");
|
|
50
|
+
return <Card.Footer {...props}>{wrapBareText(children, kpiVariants({ size }).trendCaption())}</Card.Footer>;
|
|
51
|
+
}
|
|
52
|
+
KpiFooter.displayName = "DelacourUI.Kpi.Footer";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Children, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { Separator } from "../separator";
|
|
4
|
+
import { Surface } from "../surface";
|
|
5
|
+
import { type KpiGroupContextValue, KpiGroupProvider } from "./kpi.context";
|
|
6
|
+
import type { KpiSlotProps } from "./kpi.types";
|
|
7
|
+
import { type KpiGroupOrientation, kpiVariants } from "./kpi.variants";
|
|
8
|
+
|
|
9
|
+
export type KpiGroupProps = KpiSlotProps & {
|
|
10
|
+
/** `horizontal` splits the row between the metrics; `vertical` stacks them. */
|
|
11
|
+
orientation?: KpiGroupOrientation;
|
|
12
|
+
/**
|
|
13
|
+
* Set the metrics on one surface with a rule between them, rather than as
|
|
14
|
+
* separate cards spaced apart. Several metrics divided by a rule read as one
|
|
15
|
+
* panel; several spaced apart read as several panels that happen to be
|
|
16
|
+
* adjacent.
|
|
17
|
+
*/
|
|
18
|
+
separated?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Puts a rule between adjacent children. `Separator` is hidden from assistive technology. */
|
|
22
|
+
function withRules(children: ReactNode, orientation: KpiGroupOrientation): ReactNode[] {
|
|
23
|
+
const output: ReactNode[] = [];
|
|
24
|
+
for (const [index, child] of Children.toArray(children).entries()) {
|
|
25
|
+
if (index > 0) {
|
|
26
|
+
output.push(
|
|
27
|
+
<Separator key={`rule-${index}`} orientation={orientation === "horizontal" ? "vertical" : "horizontal"} />
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
output.push(child);
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Several metrics as one arrangement, in a row or a column.
|
|
37
|
+
*
|
|
38
|
+
* Each `Kpi` inside reads the group: in a row it takes an equal share of the
|
|
39
|
+
* width, and in a `separated` group it drops its own fill and hairline so the
|
|
40
|
+
* group's one surface holds them all, a rule between each.
|
|
41
|
+
*/
|
|
42
|
+
export function KpiGroup({
|
|
43
|
+
orientation = "vertical",
|
|
44
|
+
separated = false,
|
|
45
|
+
className,
|
|
46
|
+
children,
|
|
47
|
+
...props
|
|
48
|
+
}: KpiGroupProps): ReactElement {
|
|
49
|
+
const context = useMemo<KpiGroupContextValue>(() => ({ orientation, separated }), [orientation, separated]);
|
|
50
|
+
const slots = kpiVariants({ orientation, separated });
|
|
51
|
+
|
|
52
|
+
if (separated) {
|
|
53
|
+
return (
|
|
54
|
+
<KpiGroupProvider value={context}>
|
|
55
|
+
<Surface className={slots.group({ className })} padding="none" {...props}>
|
|
56
|
+
{withRules(children, orientation)}
|
|
57
|
+
</Surface>
|
|
58
|
+
</KpiGroupProvider>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<KpiGroupProvider value={context}>
|
|
64
|
+
<View className={slots.group({ className })} {...props}>
|
|
65
|
+
{children}
|
|
66
|
+
</View>
|
|
67
|
+
</KpiGroupProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
KpiGroup.displayName = "DelacourUI.Kpi.Group";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { cardVariants } from "../card/card.variants";
|
|
5
|
+
import { useKpiPart } from "./kpi.context";
|
|
6
|
+
import type { KpiSlotProps } from "./kpi.types";
|
|
7
|
+
import { kpiVariants } from "./kpi.variants";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The top row: a tinted icon, the metric's name, and anything acting on it.
|
|
11
|
+
*
|
|
12
|
+
* Sits on the card header's own inset — the classes are the card's, with the
|
|
13
|
+
* row centred rather than top-aligned — so the title lines up with the number
|
|
14
|
+
* and the footer below it at every size.
|
|
15
|
+
*/
|
|
16
|
+
export function KpiHeader({ className, ...props }: KpiSlotProps): ReactElement {
|
|
17
|
+
const { size } = useKpiPart("Kpi.Header");
|
|
18
|
+
return (
|
|
19
|
+
<View
|
|
20
|
+
className={cardVariants({ size }).header({ className: cn(kpiVariants({ size }).header(), className) })}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
KpiHeader.displayName = "DelacourUI.Kpi.Header";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactElement, useMemo } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { IconDefaultsProvider } from "../icon";
|
|
4
|
+
import { useKpiPart } from "./kpi.context";
|
|
5
|
+
import type { KpiSlotProps } from "./kpi.types";
|
|
6
|
+
import { kpiVariants } from "./kpi.variants";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A tinted square for a glyph.
|
|
10
|
+
*
|
|
11
|
+
* It takes the element rather than drawing one — a metric's icon is the app's
|
|
12
|
+
* choice — and hands an unstyled `Icon` inside it the card's series colour and
|
|
13
|
+
* a step of the icon scale sized to the square. The square is that colour at
|
|
14
|
+
* 15%, so a row of cards given five colour indexes reads as five series.
|
|
15
|
+
*/
|
|
16
|
+
export function KpiIcon({ className, children, ...props }: KpiSlotProps): ReactElement {
|
|
17
|
+
const { size, colorIndex } = useKpiPart("Kpi.Icon");
|
|
18
|
+
const slots = kpiVariants({ colorIndex, size });
|
|
19
|
+
const glyph = slots.iconGlyph();
|
|
20
|
+
const defaults = useMemo(() => ({ className: glyph, color: `chart-${colorIndex}` }), [glyph, colorIndex]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View className={slots.icon({ className })} {...props}>
|
|
24
|
+
<IconDefaultsProvider value={defaults}>{children}</IconDefaultsProvider>
|
|
25
|
+
</View>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
KpiIcon.displayName = "DelacourUI.Kpi.Icon";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { CurveType } from "@delacour/react-native-charts/core";
|
|
2
|
+
import { type ReactElement, useMemo } from "react";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { useAnimatedReaction } from "react-native-reanimated";
|
|
5
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
6
|
+
import { Chart, type ChartConfig } from "../chart";
|
|
7
|
+
import { useChart } from "../chart/chart.context";
|
|
8
|
+
import { useKpiLayout, useKpiPart } from "./kpi.context";
|
|
9
|
+
import {
|
|
10
|
+
type KpiColorIndex,
|
|
11
|
+
type KpiSparklineData,
|
|
12
|
+
kpiSparklineAccessibilityLabel,
|
|
13
|
+
kpiVariants,
|
|
14
|
+
resolveSparklineDomain,
|
|
15
|
+
resolveSparklineFilled,
|
|
16
|
+
resolveSparklineSeries,
|
|
17
|
+
} from "./kpi.variants";
|
|
18
|
+
|
|
19
|
+
export type KpiSparklineProps = KpiSparklineData & {
|
|
20
|
+
/** Overrides the KPI's `colorIndex` for this chart. */
|
|
21
|
+
colorIndex?: KpiColorIndex;
|
|
22
|
+
/** A theme token or a literal, over the series colour entirely. */
|
|
23
|
+
color?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Fill under the line. Defaults on under the card and off beside the
|
|
26
|
+
* number, where a fill would make the chart a second block competing with
|
|
27
|
+
* the value.
|
|
28
|
+
*/
|
|
29
|
+
filled?: boolean;
|
|
30
|
+
strokeWidth?: number;
|
|
31
|
+
curve?: CurveType;
|
|
32
|
+
/**
|
|
33
|
+
* Hold and drag to scrub a point: a dot rides the line and the KPI's
|
|
34
|
+
* `activeIndex` follows it, so a custom part can print that point's value.
|
|
35
|
+
* On by default. The scrub starts on a hold, so a sparkline in a scrolling
|
|
36
|
+
* list does not steal the scroll.
|
|
37
|
+
*/
|
|
38
|
+
interactive?: boolean;
|
|
39
|
+
/** Formats the first and last point for the screen-reader summary. */
|
|
40
|
+
formatValue?: (value: number) => string;
|
|
41
|
+
/** Replaces the screen-reader summary — "Trend over 30 points, from 120 to 184". */
|
|
42
|
+
accessibilityLabel?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
testID?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reports the scrubbed index to the KPI, as `null` once the finger lifts.
|
|
49
|
+
*
|
|
50
|
+
* A child of `<Chart>` that is not a mark, so the chart mounts it beside the
|
|
51
|
+
* canvas where its context — and the scrub's shared values — are in reach. It
|
|
52
|
+
* crosses to the JS thread only when the index changes, which is a few times
|
|
53
|
+
* per drag rather than once a frame.
|
|
54
|
+
*/
|
|
55
|
+
function KpiSparklineScrub({ onChange }: { onChange: (index: number | null) => void }): null {
|
|
56
|
+
const { scrub } = useChart();
|
|
57
|
+
|
|
58
|
+
useAnimatedReaction(
|
|
59
|
+
() => (scrub.isActive.value ? scrub.index.value : -1),
|
|
60
|
+
(next, previous) => {
|
|
61
|
+
if (next === previous) return;
|
|
62
|
+
scheduleOnRN(onChange, next < 0 ? null : next);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
KpiSparklineScrub.displayName = "DelacourUI.Kpi.Sparkline.Scrub";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The shape the number made getting there — a `Chart` line with no grid, no
|
|
72
|
+
* axes and no padding, in the KPI's series colour.
|
|
73
|
+
*
|
|
74
|
+
* Takes a bare list of numbers, or rows with an `xKey` and a `yKey`. Under the
|
|
75
|
+
* card it is full width and filled; beside the number (`Kpi.Content
|
|
76
|
+
* layout="inline"`) it is a fixed 128pt column, unfilled, so a stack of cards
|
|
77
|
+
* lines its shapes up down the right-hand edge whatever their labels. The y
|
|
78
|
+
* bounds are the data's own, padded a tenth each way, so the peak and trough
|
|
79
|
+
* are not drawn half off the canvas.
|
|
80
|
+
*
|
|
81
|
+
* Announced as one image — how many points and where they started and ended.
|
|
82
|
+
* While the KPI loads it is a muted block of its own size.
|
|
83
|
+
*/
|
|
84
|
+
export function KpiSparkline(props: KpiSparklineProps): ReactElement {
|
|
85
|
+
const {
|
|
86
|
+
data,
|
|
87
|
+
xKey,
|
|
88
|
+
yKey,
|
|
89
|
+
colorIndex: colorIndexProp,
|
|
90
|
+
color,
|
|
91
|
+
filled: filledProp,
|
|
92
|
+
strokeWidth = 2,
|
|
93
|
+
curve,
|
|
94
|
+
interactive = true,
|
|
95
|
+
formatValue,
|
|
96
|
+
accessibilityLabel,
|
|
97
|
+
className,
|
|
98
|
+
testID,
|
|
99
|
+
} = props;
|
|
100
|
+
const context = useKpiPart("Kpi.Sparkline");
|
|
101
|
+
const layout = useKpiLayout();
|
|
102
|
+
const slots = kpiVariants({ layout, size: context.size });
|
|
103
|
+
const colorIndex = colorIndexProp ?? context.colorIndex;
|
|
104
|
+
|
|
105
|
+
// Keyed on the three fields the series reads rather than on the props
|
|
106
|
+
// object, which is new every render and would re-plot the chart with it.
|
|
107
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: see above
|
|
108
|
+
const series = useMemo(() => resolveSparklineSeries(props), [data, xKey, yKey]);
|
|
109
|
+
const domain = useMemo(() => ({ y: resolveSparklineDomain(series.values) }), [series]);
|
|
110
|
+
const config = useMemo<ChartConfig>(
|
|
111
|
+
() => ({ [series.yKey]: { label: series.yKey, color: color ?? `chart-${colorIndex}` } }),
|
|
112
|
+
[series.yKey, color, colorIndex]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (context.isLoading) {
|
|
116
|
+
return <View className={slots.sparklinePlaceholder({ className: slots.sparkline({ className }) })} />;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const filled = resolveSparklineFilled({ filled: filledProp, layout });
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View
|
|
123
|
+
accessibilityLabel={accessibilityLabel ?? kpiSparklineAccessibilityLabel(series.values, formatValue)}
|
|
124
|
+
accessibilityRole="image"
|
|
125
|
+
accessible
|
|
126
|
+
className={slots.sparkline({ className })}
|
|
127
|
+
>
|
|
128
|
+
<Chart
|
|
129
|
+
config={config}
|
|
130
|
+
curve={curve}
|
|
131
|
+
data={series.rows}
|
|
132
|
+
domain={domain}
|
|
133
|
+
frameClassName={slots.sparklineFrame()}
|
|
134
|
+
testID={testID}
|
|
135
|
+
xKey={series.xKey}
|
|
136
|
+
>
|
|
137
|
+
{filled ? <Chart.Area yKey={series.yKey} /> : null}
|
|
138
|
+
<Chart.Line strokeWidth={strokeWidth} yKey={series.yKey} />
|
|
139
|
+
{interactive ? <Chart.Tooltip.Dot yKey={series.yKey} /> : null}
|
|
140
|
+
{interactive ? <KpiSparklineScrub onChange={context.setActiveIndex} /> : null}
|
|
141
|
+
</Chart>
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
KpiSparkline.displayName = "DelacourUI.Kpi.Sparkline";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useKpiLayout, useKpiPart } from "./kpi.context";
|
|
4
|
+
import type { KpiSlotProps } from "./kpi.types";
|
|
5
|
+
import { kpiVariants } from "./kpi.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The value and its change, stacked tight.
|
|
9
|
+
*
|
|
10
|
+
* Its own container rather than loose children of the content, because the
|
|
11
|
+
* number and its change are one fact in two lines and the card's spacing is
|
|
12
|
+
* for the gaps *between* facts. Beside an `inline` sparkline it takes the
|
|
13
|
+
* row's width, leaving the chart its column on the end.
|
|
14
|
+
*/
|
|
15
|
+
export function KpiStat({ className, ...props }: KpiSlotProps): ReactElement {
|
|
16
|
+
const { size } = useKpiPart("Kpi.Stat");
|
|
17
|
+
const layout = useKpiLayout();
|
|
18
|
+
return <View className={kpiVariants({ layout, size }).stat({ className })} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
KpiStat.displayName = "DelacourUI.Kpi.Stat";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text } from "../text";
|
|
3
|
+
import { useKpiPart } from "./kpi.context";
|
|
4
|
+
import type { KpiTextProps } from "./kpi.types";
|
|
5
|
+
import { kpiVariants } from "./kpi.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The metric's name. Quiet on purpose — `muted-foreground`, a step down the
|
|
9
|
+
* type scale — because the value is the thing being read.
|
|
10
|
+
*
|
|
11
|
+
* It never grows to fill its row: in a column that would absorb the spare
|
|
12
|
+
* height and land a row of cards' numbers at different heights. The header
|
|
13
|
+
* pushes its action to the end instead.
|
|
14
|
+
*/
|
|
15
|
+
export function KpiTitle({ className, ...props }: KpiTextProps): ReactElement {
|
|
16
|
+
const { size } = useKpiPart("Kpi.Title");
|
|
17
|
+
return <Text className={kpiVariants({ size }).title({ className })} numberOfLines={1} {...props} />;
|
|
18
|
+
}
|
|
19
|
+
KpiTitle.displayName = "DelacourUI.Kpi.Title";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { IconArrowDownRight, IconArrowRight, IconArrowUpRight } from "../../icons/central";
|
|
4
|
+
import { Badge } from "../badge";
|
|
5
|
+
import { Icon, type IconComponent } from "../icon";
|
|
6
|
+
import { Text } from "../text";
|
|
7
|
+
import { useKpiPart } from "./kpi.context";
|
|
8
|
+
import {
|
|
9
|
+
formatKpiTrend,
|
|
10
|
+
KPI_TONE_BADGE_COLOR,
|
|
11
|
+
type KpiDirection,
|
|
12
|
+
type KpiGoodDirection,
|
|
13
|
+
type KpiSize,
|
|
14
|
+
type KpiTrendVariant,
|
|
15
|
+
kpiTrendAccessibilityLabel,
|
|
16
|
+
kpiVariants,
|
|
17
|
+
resolveKpiTrend,
|
|
18
|
+
} from "./kpi.variants";
|
|
19
|
+
|
|
20
|
+
export type KpiTrendProps = Omit<ViewProps, "children"> & {
|
|
21
|
+
/**
|
|
22
|
+
* How much it moved, as a percentage. The sign carries the direction, so
|
|
23
|
+
* `-4.2` is a fall of 4.2%; there is no direction prop to keep in step.
|
|
24
|
+
*/
|
|
25
|
+
value: number;
|
|
26
|
+
/** Writes the number yourself. Receives the raw value, sign and all. */
|
|
27
|
+
format?: (value: number) => string;
|
|
28
|
+
/** Overrides the KPI's own `goodDirection` for this one figure. */
|
|
29
|
+
goodDirection?: KpiGoodDirection;
|
|
30
|
+
/**
|
|
31
|
+
* `text` is a line of colour under the number — what a stat card usually
|
|
32
|
+
* wants. `badge` puts a pill round it with an arrow, for a card busy enough
|
|
33
|
+
* that a bare line of colour is lost in it.
|
|
34
|
+
*/
|
|
35
|
+
variant?: KpiTrendVariant;
|
|
36
|
+
/** What it is compared against — "vs last month", "last 30d". Read after the change. */
|
|
37
|
+
caption?: string;
|
|
38
|
+
/** A movement no larger than this counts as none. Defaults to `0`. */
|
|
39
|
+
threshold?: number;
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Anything after the caption, when a string is not enough. */
|
|
42
|
+
children?: ReactNode;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const ARROW: Record<KpiDirection, IconComponent> = {
|
|
46
|
+
up: IconArrowUpRight,
|
|
47
|
+
down: IconArrowDownRight,
|
|
48
|
+
flat: IconArrowRight,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const BADGE_SIZE: Record<KpiSize, "sm" | "md"> = { sm: "sm", md: "sm", lg: "md" };
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The change: a signed percentage, coloured by what it means.
|
|
55
|
+
*
|
|
56
|
+
* It derives its own direction from the sign and its own tone from the KPI's
|
|
57
|
+
* `goodDirection`, so a fall in churn is drawn as good news without the call
|
|
58
|
+
* site naming a colour. The arrow on a badge says the direction again in a
|
|
59
|
+
* shape, so the meaning never rests on colour alone.
|
|
60
|
+
*
|
|
61
|
+
* Announced as one string — "Up 7.8 percent, vs last month" — rather than a
|
|
62
|
+
* number, an arrow and a caption read as three stops. While the KPI loads it
|
|
63
|
+
* is a short muted bar.
|
|
64
|
+
*/
|
|
65
|
+
export function KpiTrend({
|
|
66
|
+
value,
|
|
67
|
+
format,
|
|
68
|
+
goodDirection: goodDirectionProp,
|
|
69
|
+
variant = "text",
|
|
70
|
+
caption,
|
|
71
|
+
threshold,
|
|
72
|
+
className,
|
|
73
|
+
children,
|
|
74
|
+
...props
|
|
75
|
+
}: KpiTrendProps): ReactElement {
|
|
76
|
+
const context = useKpiPart("Kpi.Trend");
|
|
77
|
+
const { size, isLoading } = context;
|
|
78
|
+
const goodDirection = goodDirectionProp ?? context.goodDirection;
|
|
79
|
+
const { direction, tone } = resolveKpiTrend({ goodDirection, threshold, value });
|
|
80
|
+
const slots = kpiVariants({ size, tone });
|
|
81
|
+
|
|
82
|
+
if (isLoading) {
|
|
83
|
+
return <View className={slots.trendPlaceholder()} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const formatted = format?.(value);
|
|
87
|
+
const text = formatted ?? formatKpiTrend(value);
|
|
88
|
+
const accessibilityLabel = kpiTrendAccessibilityLabel({ caption, direction, formatted, value });
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View accessibilityLabel={accessibilityLabel} accessible className={slots.trend({ className })} {...props}>
|
|
92
|
+
{variant === "badge" ? (
|
|
93
|
+
<Badge color={KPI_TONE_BADGE_COLOR[tone]} size={BADGE_SIZE[size]} variant="soft">
|
|
94
|
+
<Icon icon={ARROW[direction]} />
|
|
95
|
+
<Badge.Label>{text}</Badge.Label>
|
|
96
|
+
</Badge>
|
|
97
|
+
) : (
|
|
98
|
+
<Text className={slots.trendText()}>{text}</Text>
|
|
99
|
+
)}
|
|
100
|
+
{caption === undefined ? null : <Text className={slots.trendCaption()}>{caption}</Text>}
|
|
101
|
+
{children}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
KpiTrend.displayName = "DelacourUI.Kpi.Trend";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useCardContext } from "../card/card.context";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { useKpiPart } from "./kpi.context";
|
|
6
|
+
import type { KpiTextProps } from "./kpi.types";
|
|
7
|
+
import { kpiVariants } from "./kpi.variants";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The number.
|
|
11
|
+
*
|
|
12
|
+
* Formatted by the caller, not here — separators, currency and units are
|
|
13
|
+
* locale decisions a component would get wrong in a way that is hard to
|
|
14
|
+
* notice and impossible to override. It takes the foreground token of the
|
|
15
|
+
* plane the card landed on, like `Card.Title`, and one line: a number that
|
|
16
|
+
* wraps is no longer read as one.
|
|
17
|
+
*
|
|
18
|
+
* While the KPI is loading it is a muted block the height of the number's
|
|
19
|
+
* line, announced as loading, so the card does not jump when the data lands.
|
|
20
|
+
*/
|
|
21
|
+
export function KpiValue({ className, ...props }: KpiTextProps): ReactElement {
|
|
22
|
+
const { size, isLoading } = useKpiPart("Kpi.Value");
|
|
23
|
+
const plane = useCardContext()?.plane ?? null;
|
|
24
|
+
const slots = kpiVariants({ plane: plane ?? "none", size });
|
|
25
|
+
|
|
26
|
+
if (isLoading) {
|
|
27
|
+
return <View accessibilityLabel="Loading" accessible className={slots.valuePlaceholder()} />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return <Text adjustsFontSizeToFit className={slots.value({ className })} numberOfLines={1} {...props} />;
|
|
31
|
+
}
|
|
32
|
+
KpiValue.displayName = "DelacourUI.Kpi.Value";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { KpiColorIndex, KpiGoodDirection, KpiGroupOrientation, KpiLayout, KpiSize } from "./kpi.variants";
|
|
3
|
+
|
|
4
|
+
export type KpiContextValue = {
|
|
5
|
+
/** The card's size — the inset, the value's scale and the sparkline's height. */
|
|
6
|
+
size: KpiSize;
|
|
7
|
+
/** Which `--chart-*` token the sparkline and the icon take. */
|
|
8
|
+
colorIndex: KpiColorIndex;
|
|
9
|
+
/** Which way is the good news, for every trend inside that does not override it. */
|
|
10
|
+
goodDirection: KpiGoodDirection;
|
|
11
|
+
/** Whether the numbers are still on their way. The value, trend and sparkline hold placeholders. */
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The sparkline point being scrubbed, or `null` when nothing is. A custom
|
|
15
|
+
* part reads it to print that point's value in place of the latest one.
|
|
16
|
+
*/
|
|
17
|
+
activeIndex: number | null;
|
|
18
|
+
/** Moves the scrubbed point. The sparkline calls it; a caller holding the state may too. */
|
|
19
|
+
setActiveIndex: (index: number | null) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const KpiContext = createContext<KpiContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Supplies the enclosing KPI's size, colour, direction and scrub to its subtree.
|
|
26
|
+
*
|
|
27
|
+
* Lives in its own module, importing nothing but types, so a part the KPI
|
|
28
|
+
* renders can read it without importing `./kpi` and closing a cycle through
|
|
29
|
+
* `kpi.tsx`. See AGENTS.md rule 3.
|
|
30
|
+
*/
|
|
31
|
+
export function KpiProvider({ value, children }: { value: KpiContextValue; children: ReactNode }): ReactElement {
|
|
32
|
+
return <KpiContext value={value}>{children}</KpiContext>;
|
|
33
|
+
}
|
|
34
|
+
KpiProvider.displayName = "DelacourUI.Kpi.Provider";
|
|
35
|
+
|
|
36
|
+
/** The enclosing KPI's context, or null outside a `<Kpi>`. */
|
|
37
|
+
export function useKpiContext(): KpiContextValue | null {
|
|
38
|
+
return use(KpiContext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads the enclosing KPI's size, colour, direction, loading state and scrub.
|
|
43
|
+
*
|
|
44
|
+
* What a custom part uses to print the scrubbed point's value, match the
|
|
45
|
+
* card's series colour, or hold a placeholder while it loads. Throws outside a
|
|
46
|
+
* `<Kpi>`; use {@link useKpiContext} where the enclosing KPI is optional.
|
|
47
|
+
*/
|
|
48
|
+
export function useKpi(): KpiContextValue {
|
|
49
|
+
const context = useKpiContext();
|
|
50
|
+
if (!context) {
|
|
51
|
+
throw new Error("useKpi must be called inside a <Kpi>.");
|
|
52
|
+
}
|
|
53
|
+
return context;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The enclosing KPI's context, for a compound part that cannot work without one.
|
|
58
|
+
*
|
|
59
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
60
|
+
* library wants {@link useKpi}, whose error message names the hook rather than
|
|
61
|
+
* a part.
|
|
62
|
+
*/
|
|
63
|
+
export function useKpiPart(component: string): KpiContextValue {
|
|
64
|
+
const context = useKpiContext();
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error(`${component} must be rendered inside a <Kpi>.`);
|
|
67
|
+
}
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const KpiLayoutContext = createContext<KpiLayout>("below");
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Supplies `Kpi.Content`'s layout to the stat and the sparkline inside it —
|
|
75
|
+
* the stat takes the row's width and the sparkline a fixed column when it is
|
|
76
|
+
* `inline`.
|
|
77
|
+
*/
|
|
78
|
+
export function KpiLayoutProvider({ value, children }: { value: KpiLayout; children: ReactNode }): ReactElement {
|
|
79
|
+
return <KpiLayoutContext value={value}>{children}</KpiLayoutContext>;
|
|
80
|
+
}
|
|
81
|
+
KpiLayoutProvider.displayName = "DelacourUI.Kpi.Content.Provider";
|
|
82
|
+
|
|
83
|
+
/** The enclosing `Kpi.Content`'s layout — `below` outside one. */
|
|
84
|
+
export function useKpiLayout(): KpiLayout {
|
|
85
|
+
return use(KpiLayoutContext);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type KpiGroupContextValue = {
|
|
89
|
+
orientation: KpiGroupOrientation;
|
|
90
|
+
/** Whether the metrics share one surface with a rule between them. */
|
|
91
|
+
separated: boolean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const KpiGroupContext = createContext<KpiGroupContextValue | null>(null);
|
|
95
|
+
|
|
96
|
+
/** Supplies a `Kpi.Group`'s arrangement to the metrics inside it. */
|
|
97
|
+
export function KpiGroupProvider({
|
|
98
|
+
value,
|
|
99
|
+
children,
|
|
100
|
+
}: {
|
|
101
|
+
value: KpiGroupContextValue;
|
|
102
|
+
children: ReactNode;
|
|
103
|
+
}): ReactElement {
|
|
104
|
+
return <KpiGroupContext value={value}>{children}</KpiGroupContext>;
|
|
105
|
+
}
|
|
106
|
+
KpiGroupProvider.displayName = "DelacourUI.Kpi.Group.Provider";
|
|
107
|
+
|
|
108
|
+
/** The enclosing `Kpi.Group`, or null outside one. */
|
|
109
|
+
export function useKpiGroupContext(): KpiGroupContextValue | null {
|
|
110
|
+
return use(KpiGroupContext);
|
|
111
|
+
}
|