@delacour/react-native-ui 0.1.0-alpha.4 → 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 +10 -6
- package/package.json +48 -4
- 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 -231
- 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 -208
- 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 -122
- 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 -62
- 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 -87
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
- 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 +8 -0
- 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/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,154 @@
|
|
|
1
|
+
import { type ReactElement, useMemo } from "react";
|
|
2
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Card, type CardProps } from "../card";
|
|
5
|
+
import { type KpiContextValue, KpiProvider, useKpiGroupContext } from "./kpi.context";
|
|
6
|
+
import { type KpiColorIndex, type KpiGoodDirection, kpiVariants } from "./kpi.variants";
|
|
7
|
+
import { KpiAction } from "./kpi-action";
|
|
8
|
+
import { KpiContent } from "./kpi-content";
|
|
9
|
+
import { KpiFooter } from "./kpi-footer";
|
|
10
|
+
import { KpiGroup } from "./kpi-group";
|
|
11
|
+
import { KpiHeader } from "./kpi-header";
|
|
12
|
+
import { KpiIcon } from "./kpi-icon";
|
|
13
|
+
import { KpiSparkline } from "./kpi-sparkline";
|
|
14
|
+
import { KpiStat } from "./kpi-stat";
|
|
15
|
+
import { KpiTitle } from "./kpi-title";
|
|
16
|
+
import { KpiTrend } from "./kpi-trend";
|
|
17
|
+
import { KpiValue } from "./kpi-value";
|
|
18
|
+
|
|
19
|
+
export type KpiProps = CardProps & {
|
|
20
|
+
/**
|
|
21
|
+
* Which `--chart-*` token the sparkline and the icon take. Set on the card
|
|
22
|
+
* rather than the chart, so a row of cards can be given five series colours
|
|
23
|
+
* without repeating the choice on every part. Defaults to `1`.
|
|
24
|
+
*/
|
|
25
|
+
colorIndex?: KpiColorIndex;
|
|
26
|
+
/**
|
|
27
|
+
* Which way is the good news. `up` for revenue, `down` for churn and
|
|
28
|
+
* latency, `none` for a number that is neither. Every trend inside follows
|
|
29
|
+
* it unless it overrides it. Defaults to `up`.
|
|
30
|
+
*/
|
|
31
|
+
goodDirection?: KpiGoodDirection;
|
|
32
|
+
/** The numbers are on their way: the value, trend and sparkline hold placeholders of their own size. */
|
|
33
|
+
isLoading?: boolean;
|
|
34
|
+
/** The scrubbed sparkline point, controlled. `null` when nothing is scrubbed. */
|
|
35
|
+
activeIndex?: number | null;
|
|
36
|
+
/** The scrubbed point to start from, uncontrolled. */
|
|
37
|
+
defaultActiveIndex?: number | null;
|
|
38
|
+
/** Called as the scrub moves between points, and with `null` when it lets go. */
|
|
39
|
+
onActiveIndexChange?: (index: number | null) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function KpiRoot({
|
|
43
|
+
colorIndex = 1,
|
|
44
|
+
goodDirection = "up",
|
|
45
|
+
isLoading = false,
|
|
46
|
+
activeIndex: activeIndexProp,
|
|
47
|
+
defaultActiveIndex = null,
|
|
48
|
+
onActiveIndexChange,
|
|
49
|
+
size = "md",
|
|
50
|
+
variant,
|
|
51
|
+
className,
|
|
52
|
+
accessibilityState,
|
|
53
|
+
...props
|
|
54
|
+
}: KpiProps): ReactElement {
|
|
55
|
+
const group = useKpiGroupContext();
|
|
56
|
+
const [activeIndex, setActiveIndex] = useControllableState<number | null>({
|
|
57
|
+
value: activeIndexProp,
|
|
58
|
+
defaultValue: defaultActiveIndex,
|
|
59
|
+
onChange: onActiveIndexChange,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const context = useMemo<KpiContextValue>(
|
|
63
|
+
() => ({ activeIndex, colorIndex, goodDirection, isLoading, setActiveIndex, size }),
|
|
64
|
+
[activeIndex, colorIndex, goodDirection, isLoading, setActiveIndex, size]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Inside a separated group the group's surface is the card; each metric
|
|
68
|
+
// drops its own fill and hairline unless it names one.
|
|
69
|
+
const resolvedVariant = variant ?? (group?.separated ? "transparent" : undefined);
|
|
70
|
+
const itemClassName = group === null ? undefined : kpiVariants({ orientation: group.orientation }).groupItem();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<KpiProvider value={context}>
|
|
74
|
+
<Card
|
|
75
|
+
accessibilityState={{ ...accessibilityState, busy: isLoading }}
|
|
76
|
+
className={cn(itemClassName, group?.separated ? "rounded-none border-0" : undefined, className)}
|
|
77
|
+
size={size}
|
|
78
|
+
variant={resolvedVariant}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
</KpiProvider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One number, what it is doing, and the shape it made getting there.
|
|
87
|
+
*
|
|
88
|
+
* A `Card` with a vocabulary for a metric: a quiet title, a large value, a
|
|
89
|
+
* change coloured by what it means rather than by its sign, and a sparkline in
|
|
90
|
+
* the card's series colour. It takes the card's four fills and three sizes, and
|
|
91
|
+
* nests and steps the same way.
|
|
92
|
+
*
|
|
93
|
+
* `goodDirection` is said once, on the card, and every `Kpi.Trend` inside
|
|
94
|
+
* follows it — a fall in churn is drawn as good news. `colorIndex` is said once
|
|
95
|
+
* too, and the icon and the sparkline share it.
|
|
96
|
+
*
|
|
97
|
+
* Holding the sparkline scrubs it: a dot rides the line and `activeIndex`
|
|
98
|
+
* follows the finger, controlled or not. `useKpi()` reads it, so a part of
|
|
99
|
+
* your own can print the scrubbed point in place of the latest one.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* <Kpi colorIndex={2}>
|
|
103
|
+
* <Kpi.Header>
|
|
104
|
+
* <Kpi.Icon>
|
|
105
|
+
* <Icon icon={IconDollar} />
|
|
106
|
+
* </Kpi.Icon>
|
|
107
|
+
* <Kpi.Title>Revenue</Kpi.Title>
|
|
108
|
+
* </Kpi.Header>
|
|
109
|
+
* <Kpi.Content>
|
|
110
|
+
* <Kpi.Stat>
|
|
111
|
+
* <Kpi.Value>$48,120</Kpi.Value>
|
|
112
|
+
* <Kpi.Trend caption="vs last month" value={7.8} />
|
|
113
|
+
* </Kpi.Stat>
|
|
114
|
+
* <Kpi.Sparkline data={[31, 34, 33, 38, 41, 40, 48]} />
|
|
115
|
+
* </Kpi.Content>
|
|
116
|
+
* </Kpi>
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* <Kpi goodDirection="down" size="sm">
|
|
120
|
+
* <Kpi.Content layout="inline">
|
|
121
|
+
* <Kpi.Stat>
|
|
122
|
+
* <Kpi.Title>Churn</Kpi.Title>
|
|
123
|
+
* <Kpi.Value>2.1%</Kpi.Value>
|
|
124
|
+
* <Kpi.Trend value={-8.4} variant="badge" />
|
|
125
|
+
* </Kpi.Stat>
|
|
126
|
+
* <Kpi.Sparkline data={churn} />
|
|
127
|
+
* </Kpi.Content>
|
|
128
|
+
* </Kpi>
|
|
129
|
+
*/
|
|
130
|
+
export const Kpi = Object.assign(KpiRoot, {
|
|
131
|
+
/** The top row — a tinted icon, the metric's name, and an action pushed to the end. */
|
|
132
|
+
Header: KpiHeader,
|
|
133
|
+
/** A square tinted in the series colour; an unstyled `Icon` inside takes that colour. */
|
|
134
|
+
Icon: KpiIcon,
|
|
135
|
+
/** The metric's name, quiet, on one line. */
|
|
136
|
+
Title: KpiTitle,
|
|
137
|
+
/** The header's trailing end — a menu, a filter, a link. */
|
|
138
|
+
Action: KpiAction,
|
|
139
|
+
/** The body. `layout="inline"` sets the sparkline beside the number instead of under it. */
|
|
140
|
+
Content: KpiContent,
|
|
141
|
+
/** The value and its change, stacked tight as one fact. */
|
|
142
|
+
Stat: KpiStat,
|
|
143
|
+
/** The number, formatted by you, on the card's foreground token. */
|
|
144
|
+
Value: KpiValue,
|
|
145
|
+
/** The change as a signed percentage, coloured by what it means. */
|
|
146
|
+
Trend: KpiTrend,
|
|
147
|
+
/** A line of the series, with no axes, in the card's series colour. Hold to scrub. */
|
|
148
|
+
Sparkline: KpiSparkline,
|
|
149
|
+
/** The last row — a comparison period or a caveat, or `variant="band"` for a strip. */
|
|
150
|
+
Footer: KpiFooter,
|
|
151
|
+
/** Several metrics in a row or a column, optionally on one surface with rules between. */
|
|
152
|
+
Group: KpiGroup,
|
|
153
|
+
displayName: "DelacourUI.Kpi",
|
|
154
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
import type { TextPresetProps } from "../text";
|
|
4
|
+
|
|
5
|
+
/** The shape of a KPI's layout parts — `Header`, `Icon`, `Action`, `Content` and `Stat`. */
|
|
6
|
+
export type KpiSlotProps = ViewProps & { className?: string; children?: ReactNode };
|
|
7
|
+
|
|
8
|
+
/** The shape of a KPI's text parts, `Title` and `Value`. */
|
|
9
|
+
export type KpiTextProps = TextPresetProps;
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CARD_PLANES, CARD_SIZES, cardVariants } from "../card/card.variants";
|
|
3
|
+
import {
|
|
4
|
+
formatKpiTrend,
|
|
5
|
+
KPI_COLOR_INDEXES,
|
|
6
|
+
KPI_GOOD_DIRECTIONS,
|
|
7
|
+
KPI_GROUP_ORIENTATIONS,
|
|
8
|
+
KPI_LAYOUTS,
|
|
9
|
+
KPI_SIZES,
|
|
10
|
+
KPI_TONE_BADGE_COLOR,
|
|
11
|
+
KPI_TONES,
|
|
12
|
+
KPI_TREND_VARIANTS,
|
|
13
|
+
kpiSparklineAccessibilityLabel,
|
|
14
|
+
kpiTrendAccessibilityLabel,
|
|
15
|
+
kpiVariants,
|
|
16
|
+
resolveKpiTrend,
|
|
17
|
+
resolveSparklineDomain,
|
|
18
|
+
resolveSparklineFilled,
|
|
19
|
+
resolveSparklineSeries,
|
|
20
|
+
} from "./kpi.variants";
|
|
21
|
+
|
|
22
|
+
/** Every slot the variant function can produce, one call per combination. */
|
|
23
|
+
function everyCombination(): ReturnType<typeof kpiVariants>[] {
|
|
24
|
+
return KPI_SIZES.flatMap((size) =>
|
|
25
|
+
KPI_LAYOUTS.flatMap((layout) =>
|
|
26
|
+
KPI_TONES.flatMap((tone) =>
|
|
27
|
+
KPI_COLOR_INDEXES.flatMap((colorIndex) =>
|
|
28
|
+
CARD_PLANES.map((plane) => kpiVariants({ colorIndex, layout, plane, size, tone }))
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The numeric step of the first `${prefix}-N` utility in a class string. */
|
|
36
|
+
function step(cls: string, prefix: string): string | undefined {
|
|
37
|
+
return cls.match(new RegExp(`(?:^|\\s)${prefix}-(\\d+(?:\\.5)?)(?:\\s|$)`))?.[1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("resolveKpiTrend", () => {
|
|
41
|
+
test("a rise is good news by default", () => {
|
|
42
|
+
expect(resolveKpiTrend({ value: 7.8 })).toEqual({ direction: "up", tone: "good" });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a fall is bad news by default", () => {
|
|
46
|
+
expect(resolveKpiTrend({ value: -4.2 })).toEqual({ direction: "down", tone: "bad" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Churn, refunds, latency: the number going down is the good news, and the
|
|
50
|
+
// colour has to say so rather than follow the sign.
|
|
51
|
+
test("goodDirection down turns a fall into good news and a rise into bad", () => {
|
|
52
|
+
expect(resolveKpiTrend({ goodDirection: "down", value: -8.4 })).toEqual({ direction: "down", tone: "good" });
|
|
53
|
+
expect(resolveKpiTrend({ goodDirection: "down", value: 3 })).toEqual({ direction: "up", tone: "bad" });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("goodDirection none keeps the direction and drops the judgement", () => {
|
|
57
|
+
expect(resolveKpiTrend({ goodDirection: "none", value: 2.2 })).toEqual({ direction: "up", tone: "neutral" });
|
|
58
|
+
expect(resolveKpiTrend({ goodDirection: "none", value: -2.2 })).toEqual({ direction: "down", tone: "neutral" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("zero is flat and neutral whichever way is good", () => {
|
|
62
|
+
for (const goodDirection of KPI_GOOD_DIRECTIONS) {
|
|
63
|
+
expect(resolveKpiTrend({ goodDirection, value: 0 })).toEqual({ direction: "flat", tone: "neutral" });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a movement inside the threshold counts as no movement", () => {
|
|
68
|
+
expect(resolveKpiTrend({ threshold: 0.5, value: 0.4 })).toEqual({ direction: "flat", tone: "neutral" });
|
|
69
|
+
expect(resolveKpiTrend({ threshold: 0.5, value: -0.5 })).toEqual({ direction: "flat", tone: "neutral" });
|
|
70
|
+
expect(resolveKpiTrend({ threshold: 0.5, value: 0.6 })).toEqual({ direction: "up", tone: "good" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// A value the caller could not compute — a division by a zero baseline —
|
|
74
|
+
// must not be painted as good or bad news.
|
|
75
|
+
test("a non-finite value is flat and neutral", () => {
|
|
76
|
+
expect(resolveKpiTrend({ value: Number.NaN })).toEqual({ direction: "flat", tone: "neutral" });
|
|
77
|
+
expect(resolveKpiTrend({ value: Number.POSITIVE_INFINITY })).toEqual({ direction: "flat", tone: "neutral" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a negative threshold is treated as zero", () => {
|
|
81
|
+
expect(resolveKpiTrend({ threshold: -1, value: 0.1 })).toEqual({ direction: "up", tone: "good" });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("formatKpiTrend", () => {
|
|
86
|
+
test("prints one decimal place with an explicit sign", () => {
|
|
87
|
+
expect(formatKpiTrend(7.8)).toBe("+7.8%");
|
|
88
|
+
expect(formatKpiTrend(12)).toBe("+12.0%");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// A hyphen-minus is narrower than a plus, so a column of changes would not
|
|
92
|
+
// line up on the sign. The typographic minus is the width of the plus.
|
|
93
|
+
test("uses the minus sign, not a hyphen, for a fall", () => {
|
|
94
|
+
expect(formatKpiTrend(-4.25)).toBe("−4.3%");
|
|
95
|
+
expect(formatKpiTrend(-4.25)).not.toContain("-");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("prints zero unsigned", () => {
|
|
99
|
+
expect(formatKpiTrend(0)).toBe("0.0%");
|
|
100
|
+
expect(formatKpiTrend(-0)).toBe("0.0%");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// -0.04 rounds to zero at one place; "−0.0%" would claim a fall nothing shows.
|
|
104
|
+
test("prints a value that rounds to zero unsigned", () => {
|
|
105
|
+
expect(formatKpiTrend(-0.04)).toBe("0.0%");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("prints a dash for a non-finite value", () => {
|
|
109
|
+
expect(formatKpiTrend(Number.NaN)).toBe("—");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("kpiTrendAccessibilityLabel", () => {
|
|
114
|
+
test("announces the direction and the size in words, then the caption", () => {
|
|
115
|
+
expect(kpiTrendAccessibilityLabel({ caption: "vs last month", direction: "up", value: 7.8 })).toBe(
|
|
116
|
+
"Up 7.8 percent, vs last month"
|
|
117
|
+
);
|
|
118
|
+
expect(kpiTrendAccessibilityLabel({ direction: "down", value: -4.2 })).toBe("Down 4.2 percent");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("announces a flat change as no change", () => {
|
|
122
|
+
expect(kpiTrendAccessibilityLabel({ direction: "flat", value: 0 })).toBe("No change");
|
|
123
|
+
expect(kpiTrendAccessibilityLabel({ caption: "vs last week", direction: "flat", value: 0.1 })).toBe(
|
|
124
|
+
"No change, vs last week"
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// A caller's own format may not be a percentage at all ("+120 users"), so
|
|
129
|
+
// it is read as written after the direction rather than rewritten.
|
|
130
|
+
test("reads a caller's own formatted text after the direction", () => {
|
|
131
|
+
expect(kpiTrendAccessibilityLabel({ direction: "up", formatted: "+120 users", value: 120 })).toBe("Up, +120 users");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("KPI_TONE_BADGE_COLOR", () => {
|
|
136
|
+
test("maps good news to success, bad news to destructive and neutral to default", () => {
|
|
137
|
+
expect(KPI_TONE_BADGE_COLOR).toEqual({ bad: "destructive", good: "success", neutral: "default" });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("resolveSparklineSeries", () => {
|
|
142
|
+
test("indexes a bare list of numbers", () => {
|
|
143
|
+
const series = resolveSparklineSeries({ data: [3, 5, 4] });
|
|
144
|
+
expect(series.xKey).toBe("index");
|
|
145
|
+
expect(series.yKey).toBe("value");
|
|
146
|
+
expect(series.rows).toEqual([
|
|
147
|
+
{ index: 0, value: 3 },
|
|
148
|
+
{ index: 1, value: 5 },
|
|
149
|
+
{ index: 2, value: 4 },
|
|
150
|
+
]);
|
|
151
|
+
expect(series.values).toEqual([3, 5, 4]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("passes rows through with the caller's keys", () => {
|
|
155
|
+
const rows = [
|
|
156
|
+
{ day: "Mon", visits: 10 },
|
|
157
|
+
{ day: "Tue", visits: 14 },
|
|
158
|
+
];
|
|
159
|
+
const series = resolveSparklineSeries({ data: rows, xKey: "day", yKey: "visits" });
|
|
160
|
+
expect(series.rows).toBe(rows);
|
|
161
|
+
expect(series.xKey).toBe("day");
|
|
162
|
+
expect(series.yKey).toBe("visits");
|
|
163
|
+
expect(series.values).toEqual([10, 14]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("reads a missing or non-numeric row value as NaN", () => {
|
|
167
|
+
const series = resolveSparklineSeries({
|
|
168
|
+
data: [{ at: 1, v: 2 }, { at: 2 }, { at: 3, v: "x" }],
|
|
169
|
+
xKey: "at",
|
|
170
|
+
yKey: "v",
|
|
171
|
+
});
|
|
172
|
+
expect(series.values[0]).toBe(2);
|
|
173
|
+
expect(series.values[1]).toBeNaN();
|
|
174
|
+
expect(series.values[2]).toBeNaN();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("resolveSparklineDomain", () => {
|
|
179
|
+
// With no axis and no padding, a line through the data's own extent draws
|
|
180
|
+
// its peak and its trough on the frame's edge, half a stroke off the canvas.
|
|
181
|
+
test("pads the data's extent by a fraction of its span at each end", () => {
|
|
182
|
+
expect(resolveSparklineDomain([10, 20])).toEqual([9, 21]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("ignores values that are not finite", () => {
|
|
186
|
+
expect(resolveSparklineDomain([Number.NaN, 10, 20, Number.POSITIVE_INFINITY])).toEqual([9, 21]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// A flat series has no span to take a fraction of; the line sits centred.
|
|
190
|
+
test("centres a flat series in a unit either side", () => {
|
|
191
|
+
expect(resolveSparklineDomain([5, 5, 5])).toEqual([4, 6]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("returns undefined when there is nothing to plot", () => {
|
|
195
|
+
expect(resolveSparklineDomain([])).toBeUndefined();
|
|
196
|
+
expect(resolveSparklineDomain([Number.NaN])).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("resolveSparklineFilled", () => {
|
|
201
|
+
test("fills under the full-width chart and not beside the number", () => {
|
|
202
|
+
expect(resolveSparklineFilled({ layout: "below" })).toBe(true);
|
|
203
|
+
expect(resolveSparklineFilled({ layout: "inline" })).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("an explicit choice wins either way", () => {
|
|
207
|
+
expect(resolveSparklineFilled({ filled: false, layout: "below" })).toBe(false);
|
|
208
|
+
expect(resolveSparklineFilled({ filled: true, layout: "inline" })).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("kpiSparklineAccessibilityLabel", () => {
|
|
213
|
+
test("names the span and the first and last point", () => {
|
|
214
|
+
expect(kpiSparklineAccessibilityLabel([120, 150, 184])).toBe("Trend over 3 points, from 120 to 184");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("reads the ends through a caller's formatter", () => {
|
|
218
|
+
expect(kpiSparklineAccessibilityLabel([1200, 1840], (value) => `$${value}`)).toBe(
|
|
219
|
+
"Trend over 2 points, from $1200 to $1840"
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("skips gaps when finding the ends", () => {
|
|
224
|
+
expect(kpiSparklineAccessibilityLabel([Number.NaN, 4, 6, Number.NaN])).toBe("Trend over 4 points, from 4 to 6");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("says there is no data when there is none", () => {
|
|
228
|
+
expect(kpiSparklineAccessibilityLabel([])).toBe("No trend data");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("kpiVariants insets", () => {
|
|
233
|
+
// A KPI is a card, and its header and content have to sit on the card's own
|
|
234
|
+
// inset or the number drifts off the footer beneath it.
|
|
235
|
+
test("the header and content carry no inset of their own — the card's parts supply it", () => {
|
|
236
|
+
for (const slots of everyCombination()) {
|
|
237
|
+
expect(slots.header()).not.toMatch(/(^|\s)(p|px|pl|pr)-/);
|
|
238
|
+
expect(slots.content()).not.toMatch(/(^|\s)(p|px|pl|pr)-/);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("composes onto the card's header and content without losing the card's inset", () => {
|
|
243
|
+
for (const size of CARD_SIZES) {
|
|
244
|
+
const card = cardVariants({ size });
|
|
245
|
+
const kpi = kpiVariants({ size });
|
|
246
|
+
const header = card.header({ className: kpi.header() });
|
|
247
|
+
const content = card.content({ className: kpi.content() });
|
|
248
|
+
expect(step(header, "px")).toBe(step(card.header(), "px"));
|
|
249
|
+
expect(step(content, "px")).toBe(step(card.content(), "px"));
|
|
250
|
+
expect(header).toContain("items-center");
|
|
251
|
+
expect(header).not.toContain("items-start");
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("kpiVariants value", () => {
|
|
257
|
+
test("steps the value's scale with the size", () => {
|
|
258
|
+
expect(kpiVariants({ size: "sm" }).value()).toContain("text-2xl");
|
|
259
|
+
expect(kpiVariants({ size: "md" }).value()).toContain("text-3xl");
|
|
260
|
+
expect(kpiVariants({ size: "lg" }).value()).toContain("text-4xl");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("the value is the X-foreground token of the plane it sits on", () => {
|
|
264
|
+
expect(kpiVariants({ plane: "default" }).value()).toContain("text-card-foreground");
|
|
265
|
+
expect(kpiVariants({ plane: "secondary" }).value()).toContain("text-secondary-foreground");
|
|
266
|
+
expect(kpiVariants({ plane: "tertiary" }).value()).toContain("text-tertiary-foreground");
|
|
267
|
+
expect(kpiVariants({ plane: "none" }).value()).toContain("text-foreground");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("the value carries exactly one text colour", () => {
|
|
271
|
+
for (const slots of everyCombination()) {
|
|
272
|
+
const colours = slots
|
|
273
|
+
.value()
|
|
274
|
+
.split(/\s+/)
|
|
275
|
+
.filter((name) => /^text-(card|secondary|tertiary|foreground|muted)/.test(name));
|
|
276
|
+
expect(colours).toHaveLength(1);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// The loading placeholder stands where the number will be, so it has to be
|
|
281
|
+
// the number's height or the card jumps when the data lands.
|
|
282
|
+
test("the value placeholder is as tall as the value's line at every size", () => {
|
|
283
|
+
const lineHeight: Record<string, string> = { "text-2xl": "8", "text-3xl": "9", "text-4xl": "10" };
|
|
284
|
+
for (const size of KPI_SIZES) {
|
|
285
|
+
const slots = kpiVariants({ size });
|
|
286
|
+
const scale = slots.value().match(/text-(2xl|3xl|4xl)/)?.[0] ?? "";
|
|
287
|
+
expect(step(slots.valuePlaceholder(), "h")).toBe(lineHeight[scale]);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("kpiVariants placeholders", () => {
|
|
293
|
+
// `bg-muted` is the secondary fill in both themes, so a placeholder painted
|
|
294
|
+
// with it vanished on a secondary card. A translucent foreground reads on
|
|
295
|
+
// every plane the card can land on.
|
|
296
|
+
test("every placeholder is a translucent foreground, never a fill token", () => {
|
|
297
|
+
for (const slots of everyCombination()) {
|
|
298
|
+
for (const cls of [slots.valuePlaceholder(), slots.trendPlaceholder(), slots.sparklinePlaceholder()]) {
|
|
299
|
+
expect(cls).toContain("bg-muted-foreground/15");
|
|
300
|
+
expect(cls).not.toMatch(/(^|\s)bg-(muted|secondary|tertiary|card)(\s|$)/);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("kpiVariants title", () => {
|
|
307
|
+
test("the title is quiet on every plane — the value is the thing being read", () => {
|
|
308
|
+
for (const slots of everyCombination()) {
|
|
309
|
+
expect(slots.title()).toContain("text-muted-foreground");
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// A growing child of a column absorbs the column's spare height, which lands
|
|
314
|
+
// the numbers of a row of cards at different heights. The title never grows.
|
|
315
|
+
test("the title never grows", () => {
|
|
316
|
+
for (const slots of everyCombination()) {
|
|
317
|
+
expect(slots.title()).not.toMatch(/(^|\s)(flex-1|grow)(\s|$)/);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("the header's trailing action is pushed to the end rather than the title stretched to reach it", () => {
|
|
322
|
+
expect(kpiVariants().action()).toContain("ml-auto");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("kpiVariants trend", () => {
|
|
327
|
+
test("colours the text by tone, never by sign", () => {
|
|
328
|
+
expect(kpiVariants({ tone: "good" }).trendText()).toContain("text-success-soft-foreground");
|
|
329
|
+
expect(kpiVariants({ tone: "bad" }).trendText()).toContain("text-destructive-soft-foreground");
|
|
330
|
+
expect(kpiVariants({ tone: "neutral" }).trendText()).toContain("text-muted-foreground");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("the caption is muted at every tone", () => {
|
|
334
|
+
for (const tone of KPI_TONES) {
|
|
335
|
+
expect(kpiVariants({ tone }).trendCaption()).toContain("text-muted-foreground");
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("every trend variant is covered by a badge colour or a text tone", () => {
|
|
340
|
+
expect(KPI_TREND_VARIANTS).toEqual(["text", "badge"]);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("kpiVariants icon", () => {
|
|
345
|
+
test("tints the icon square from the series ramp, one token per colour index", () => {
|
|
346
|
+
for (const colorIndex of KPI_COLOR_INDEXES) {
|
|
347
|
+
expect(kpiVariants({ colorIndex }).icon()).toContain(`bg-chart-${colorIndex}/15`);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("the glyph is a step on the icon scale, growing with the square", () => {
|
|
352
|
+
expect(kpiVariants({ size: "sm" }).iconGlyph()).toBe("size-icon-xs");
|
|
353
|
+
expect(kpiVariants({ size: "md" }).iconGlyph()).toBe("size-icon-sm");
|
|
354
|
+
expect(kpiVariants({ size: "lg" }).iconGlyph()).toBe("size-icon-lg");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("steps the square with the size", () => {
|
|
358
|
+
const sizes = new Set(KPI_SIZES.map((size) => kpiVariants({ size }).icon()));
|
|
359
|
+
expect(sizes.size).toBe(KPI_SIZES.length);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("kpiVariants layout", () => {
|
|
364
|
+
test("below stacks the stat over the chart; inline sets them in a row", () => {
|
|
365
|
+
expect(kpiVariants({ layout: "below" }).content()).not.toContain("flex-row");
|
|
366
|
+
expect(kpiVariants({ layout: "inline" }).content()).toContain("flex-row");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// A stack of cards has labels of every length. A chart that took whatever
|
|
370
|
+
// the text left would be a different width on every card, and the shapes
|
|
371
|
+
// would stop lining up down the right-hand edge.
|
|
372
|
+
test("an inline sparkline takes a fixed column; a full-width one takes the row", () => {
|
|
373
|
+
expect(kpiVariants({ layout: "inline" }).sparkline()).toContain("w-32");
|
|
374
|
+
expect(kpiVariants({ layout: "inline" }).sparkline()).toContain("shrink-0");
|
|
375
|
+
expect(kpiVariants({ layout: "below" }).sparkline()).toContain("w-full");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("the stat takes the width beside an inline chart", () => {
|
|
379
|
+
expect(kpiVariants({ layout: "inline" }).stat()).toContain("flex-1");
|
|
380
|
+
expect(kpiVariants({ layout: "below" }).stat()).not.toContain("flex-1");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("an inline sparkline is shorter than a full-width one at every size", () => {
|
|
384
|
+
for (const size of KPI_SIZES) {
|
|
385
|
+
const below = Number(step(kpiVariants({ layout: "below", size }).sparklineFrame(), "h"));
|
|
386
|
+
const inline = Number(step(kpiVariants({ layout: "inline", size }).sparklineFrame(), "h"));
|
|
387
|
+
expect(inline).toBeLessThan(below);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("kpiVariants group", () => {
|
|
393
|
+
test("horizontal lays the metrics out in a row; vertical stacks them", () => {
|
|
394
|
+
expect(kpiVariants({ orientation: "horizontal" }).group()).toContain("flex-row");
|
|
395
|
+
expect(kpiVariants({ orientation: "vertical" }).group()).not.toContain("flex-row");
|
|
396
|
+
expect(KPI_GROUP_ORIENTATIONS).toEqual(["horizontal", "vertical"]);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("a separated group lets its rules be the spacing", () => {
|
|
400
|
+
expect(kpiVariants({ separated: true }).group()).toContain("gap-0");
|
|
401
|
+
expect(kpiVariants({ separated: false }).group()).toContain("gap-3");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("a metric in a row takes an equal share of it", () => {
|
|
405
|
+
expect(kpiVariants({ orientation: "horizontal" }).groupItem()).toContain("flex-1");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("kpiVariants rule 1", () => {
|
|
410
|
+
// A React Native View does not cascade colour to a Text descendant.
|
|
411
|
+
test("carries no text treatment on a view slot", () => {
|
|
412
|
+
for (const slots of everyCombination()) {
|
|
413
|
+
for (const cls of [
|
|
414
|
+
slots.header(),
|
|
415
|
+
slots.icon(),
|
|
416
|
+
slots.action(),
|
|
417
|
+
slots.content(),
|
|
418
|
+
slots.stat(),
|
|
419
|
+
slots.trend(),
|
|
420
|
+
slots.sparkline(),
|
|
421
|
+
slots.sparklineFrame(),
|
|
422
|
+
slots.group(),
|
|
423
|
+
]) {
|
|
424
|
+
expect(cls).not.toMatch(/(^|\s)text-/);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
});
|