@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,263 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ICON_SIZE_TOKENS } from "../../styles/tokens";
|
|
3
|
+
import { LIST_GROUP_SIZES, listGroupVariants } from "../list-group/list-group.variants";
|
|
4
|
+
import {
|
|
5
|
+
ITEM_MEDIA_VARIANTS,
|
|
6
|
+
ITEM_ORIENTATIONS,
|
|
7
|
+
ITEM_SIZES,
|
|
8
|
+
ITEM_VARIANTS,
|
|
9
|
+
itemVariants,
|
|
10
|
+
resolveItemFeedback,
|
|
11
|
+
resolveItemRender,
|
|
12
|
+
resolveItemSize,
|
|
13
|
+
resolveItemSurface,
|
|
14
|
+
} from "./item.variants";
|
|
15
|
+
|
|
16
|
+
/** Pulls the horizontal padding step out of a class string — `px-4` yields 4. */
|
|
17
|
+
function paddingStep(cls: string): string | undefined {
|
|
18
|
+
return cls.match(/\bpx-(\d+(?:\.\d+)?)\b/)?.[1];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Position of a class string's `size-icon-*` token on the shared icon scale. */
|
|
22
|
+
function iconStep(cls: string): number {
|
|
23
|
+
const token = cls.match(/\bsize-(icon-[\w-]+)\b/)?.[1];
|
|
24
|
+
return ICON_SIZE_TOKENS.indexOf(token as (typeof ICON_SIZE_TOKENS)[number]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("resolveItemSurface", () => {
|
|
28
|
+
test("a standalone item keeps its own variant", () => {
|
|
29
|
+
for (const variant of ITEM_VARIANTS) {
|
|
30
|
+
expect(resolveItemSurface(variant, false)).toBe(variant);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// The group owns the surface: a border or a fill on each row would draw a
|
|
35
|
+
// card inside a card.
|
|
36
|
+
test("an item inside a ListGroup is always grouped, whatever its variant", () => {
|
|
37
|
+
for (const variant of ITEM_VARIANTS) {
|
|
38
|
+
expect(resolveItemSurface(variant, true)).toBe("grouped");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("resolveItemSize", () => {
|
|
44
|
+
test("an explicit size wins over the group's", () => {
|
|
45
|
+
expect(resolveItemSize("lg", "sm")).toBe("lg");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("inherits the enclosing group's size when unset", () => {
|
|
49
|
+
for (const size of LIST_GROUP_SIZES) {
|
|
50
|
+
expect(resolveItemSize(undefined, size)).toBe(size);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("falls back to md outside a group", () => {
|
|
55
|
+
expect(resolveItemSize(undefined, undefined)).toBe("md");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("resolveItemFeedback", () => {
|
|
60
|
+
test("a caller's feedback always wins", () => {
|
|
61
|
+
expect(resolveItemFeedback("none", true)).toBe("none");
|
|
62
|
+
expect(resolveItemFeedback("scale-fade", false)).toBe("scale-fade");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// A full-bleed row that scales reads as the whole card flexing.
|
|
66
|
+
test("fades inside a group and scales standalone", () => {
|
|
67
|
+
expect(resolveItemFeedback(undefined, true)).toBe("fade");
|
|
68
|
+
expect(resolveItemFeedback(undefined, false)).toBe("scale");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("resolveItemRender", () => {
|
|
73
|
+
const noop = () => {};
|
|
74
|
+
|
|
75
|
+
test("is static without a press handler, disabled or not", () => {
|
|
76
|
+
expect(resolveItemRender({ isDisabled: false })).toBe("static");
|
|
77
|
+
expect(resolveItemRender({ isDisabled: true })).toBe("static");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("is a pressable with a handler", () => {
|
|
81
|
+
expect(resolveItemRender({ isDisabled: false, onPress: noop })).toBe("pressable");
|
|
82
|
+
expect(resolveItemRender({ isDisabled: false, onLongPress: noop })).toBe("pressable");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Pressable composes its resting opacity with the press animation, so a
|
|
86
|
+
// disabled Pressable dims through `opacity-50` like any other row.
|
|
87
|
+
test("stays a pressable when disabled", () => {
|
|
88
|
+
expect(resolveItemRender({ isDisabled: true, onPress: noop })).toBe("pressable");
|
|
89
|
+
expect(resolveItemRender({ isDisabled: true, onLongPress: noop })).toBe("pressable");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("itemVariants root slot", () => {
|
|
94
|
+
test("gives every surface a distinct class string", () => {
|
|
95
|
+
const surfaces = [...ITEM_VARIANTS, "grouped"] as const;
|
|
96
|
+
const seen = new Set(surfaces.map((surface) => itemVariants({ surface }).root()));
|
|
97
|
+
expect(seen.size).toBe(surfaces.length);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("maps each surface", () => {
|
|
101
|
+
expect(itemVariants({ surface: "default" }).root()).toContain("bg-transparent");
|
|
102
|
+
expect(itemVariants({ surface: "outline" }).root()).toContain("border-border");
|
|
103
|
+
expect(itemVariants({ surface: "muted" }).root()).toContain("bg-muted");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a standalone surface is rounded; a grouped one is not", () => {
|
|
107
|
+
for (const surface of ITEM_VARIANTS) {
|
|
108
|
+
expect(itemVariants({ surface }).root()).toMatch(/\brounded-/);
|
|
109
|
+
}
|
|
110
|
+
expect(itemVariants({ surface: "grouped" }).root()).not.toMatch(/\brounded-/);
|
|
111
|
+
expect(itemVariants({ surface: "grouped" }).root()).not.toContain("border-border");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Without it a short row leaves the press feedback ending mid-card.
|
|
115
|
+
test("a grouped row spans the group", () => {
|
|
116
|
+
expect(itemVariants({ surface: "grouped" }).root()).toContain("w-full");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("holds no text colour — a View cannot cascade one", () => {
|
|
120
|
+
for (const surface of [...ITEM_VARIANTS, "grouped"] as const) {
|
|
121
|
+
expect(itemVariants({ surface }).root()).not.toMatch(/\btext-/);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("orientation picks the main axis", () => {
|
|
126
|
+
expect(itemVariants({ orientation: "horizontal" }).root()).toContain("flex-row");
|
|
127
|
+
expect(itemVariants({ orientation: "vertical" }).root()).toContain("flex-col");
|
|
128
|
+
expect(itemVariants({ orientation: "vertical" }).root()).not.toContain("flex-row");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("disabled dims the row", () => {
|
|
132
|
+
expect(itemVariants({ isDisabled: true }).root()).toContain("opacity-50");
|
|
133
|
+
expect(itemVariants({ isDisabled: false }).root()).not.toContain("opacity-50");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("selected lays the accent over every surface, the muted fill included", () => {
|
|
137
|
+
for (const surface of [...ITEM_VARIANTS, "grouped"] as const) {
|
|
138
|
+
const cls = itemVariants({ isSelected: true, surface }).root();
|
|
139
|
+
expect(cls).toContain("bg-accent");
|
|
140
|
+
expect(cls).not.toContain("bg-muted");
|
|
141
|
+
expect(cls).not.toContain("bg-transparent");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("itemVariants sizes", () => {
|
|
147
|
+
// This is what makes an Item drop into a ListGroup: the group insets its
|
|
148
|
+
// dividers by its own row padding, so the item's padding has to match it.
|
|
149
|
+
test("row padding matches ListGroup's divider inset at every size", () => {
|
|
150
|
+
for (const size of ITEM_SIZES) {
|
|
151
|
+
const itemPadding = paddingStep(itemVariants({ size }).root());
|
|
152
|
+
const dividerInset = listGroupVariants({ size })
|
|
153
|
+
.divider()
|
|
154
|
+
.match(/\bmx-(\d+(?:\.\d+)?)\b/)?.[1];
|
|
155
|
+
expect(itemPadding).toBeDefined();
|
|
156
|
+
expect(itemPadding).toBe(dividerInset);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("row metrics match a ListGroup row at every size", () => {
|
|
161
|
+
for (const size of ITEM_SIZES) {
|
|
162
|
+
const item = itemVariants({ size }).root();
|
|
163
|
+
for (const cls of listGroupVariants({ size }).item().split(" ")) {
|
|
164
|
+
if (/^(min-h|gap|px|py)-/.test(cls)) expect(item).toContain(cls);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("title and description step up with size", () => {
|
|
170
|
+
expect(itemVariants({ size: "sm" }).title()).toContain("text-sm");
|
|
171
|
+
expect(itemVariants({ size: "md" }).title()).toContain("text-base");
|
|
172
|
+
expect(itemVariants({ size: "lg" }).title()).toContain("text-lg");
|
|
173
|
+
expect(itemVariants({ size: "sm" }).description()).toContain("text-xs");
|
|
174
|
+
expect(itemVariants({ size: "md" }).description()).toContain("text-sm");
|
|
175
|
+
expect(itemVariants({ size: "lg" }).description()).toContain("text-base");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("text carries its own colour tokens", () => {
|
|
179
|
+
expect(itemVariants().title()).toContain("text-foreground");
|
|
180
|
+
expect(itemVariants().description()).toContain("text-muted-foreground");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("every media icon names an ascending step on the shared icon scale", () => {
|
|
184
|
+
for (const mediaVariant of ITEM_MEDIA_VARIANTS) {
|
|
185
|
+
if (mediaVariant === "image") continue;
|
|
186
|
+
const steps = ITEM_SIZES.map((size) => iconStep(itemVariants({ mediaVariant, size }).mediaIcon()));
|
|
187
|
+
for (const step of steps) expect(step).toBeGreaterThanOrEqual(0);
|
|
188
|
+
expect([...steps].sort((a, b) => a - b)).toEqual(steps);
|
|
189
|
+
expect(new Set(steps).size).toBe(steps.length);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a bare media icon matches ListGroup's prefix icon", () => {
|
|
194
|
+
for (const size of ITEM_SIZES) {
|
|
195
|
+
expect(itemVariants({ mediaVariant: "default", size }).mediaIcon()).toBe(
|
|
196
|
+
listGroupVariants({ size }).prefixIcon()
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("itemVariants media slot", () => {
|
|
203
|
+
test("the icon tile has a fill and a corner", () => {
|
|
204
|
+
const cls = itemVariants({ mediaVariant: "icon" }).media();
|
|
205
|
+
expect(cls).toContain("bg-muted");
|
|
206
|
+
expect(cls).toMatch(/\brounded-/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("an image clips its child to the corner", () => {
|
|
210
|
+
expect(itemVariants({ mediaVariant: "image" }).media()).toContain("overflow-hidden");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("the icon tile and image are fixed squares that grow with size", () => {
|
|
214
|
+
for (const mediaVariant of ["icon", "image"] as const) {
|
|
215
|
+
const edges = ITEM_SIZES.map((size) => {
|
|
216
|
+
const edge = itemVariants({ mediaVariant, size })
|
|
217
|
+
.media()
|
|
218
|
+
.match(/\bsize-(\d+)\b/)?.[1];
|
|
219
|
+
return Number(edge);
|
|
220
|
+
});
|
|
221
|
+
for (const edge of edges) expect(edge).toBeGreaterThan(0);
|
|
222
|
+
expect([...edges].sort((a, b) => a - b)).toEqual(edges);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a tile stays visible on the muted and the selected surface", () => {
|
|
227
|
+
for (const mediaVariant of ["icon", "image"] as const) {
|
|
228
|
+
for (const cls of [
|
|
229
|
+
itemVariants({ mediaVariant, surface: "muted" }).media(),
|
|
230
|
+
itemVariants({ isSelected: true, mediaVariant }).media(),
|
|
231
|
+
]) {
|
|
232
|
+
expect(cls).toContain("bg-background");
|
|
233
|
+
expect(cls).not.toContain("bg-muted");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("a bare media slot draws nothing of its own", () => {
|
|
239
|
+
const cls = itemVariants({ mediaVariant: "default" }).media();
|
|
240
|
+
expect(cls).not.toMatch(/\bbg-/);
|
|
241
|
+
expect(cls).not.toMatch(/\bsize-\d/);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("itemVariants orientation", () => {
|
|
246
|
+
// A `flex-1` column in an auto-height parent collapses to nothing in Yoga.
|
|
247
|
+
test("content only flexes along a row", () => {
|
|
248
|
+
expect(itemVariants({ orientation: "horizontal" }).content()).toContain("flex-1");
|
|
249
|
+
expect(itemVariants({ orientation: "vertical" }).content()).not.toContain("flex-1");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("header and footer are full-width strips on both axes", () => {
|
|
253
|
+
for (const orientation of ITEM_ORIENTATIONS) {
|
|
254
|
+
expect(itemVariants({ orientation }).header()).toContain("w-full");
|
|
255
|
+
expect(itemVariants({ orientation }).footer()).toContain("w-full");
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("a group runs down by default and across when horizontal", () => {
|
|
260
|
+
expect(itemVariants({ groupOrientation: "vertical" }).group()).toContain("flex-col");
|
|
261
|
+
expect(itemVariants({ groupOrientation: "horizontal" }).group()).toContain("flex-row");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { PressableFeedback } from "../pressable/pressable.variants";
|
|
4
|
+
|
|
5
|
+
export const ITEM_VARIANTS = ["default", "outline", "muted"] as const;
|
|
6
|
+
|
|
7
|
+
export const ITEM_SIZES = ["sm", "md", "lg"] as const;
|
|
8
|
+
|
|
9
|
+
export const ITEM_ORIENTATIONS = ["horizontal", "vertical"] as const;
|
|
10
|
+
|
|
11
|
+
export const ITEM_MEDIA_VARIANTS = ["default", "icon", "image"] as const;
|
|
12
|
+
|
|
13
|
+
export type ItemVariant = (typeof ITEM_VARIANTS)[number];
|
|
14
|
+
export type ItemSize = (typeof ITEM_SIZES)[number];
|
|
15
|
+
export type ItemOrientation = (typeof ITEM_ORIENTATIONS)[number];
|
|
16
|
+
export type ItemMediaVariant = (typeof ITEM_MEDIA_VARIANTS)[number];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The surface a row actually draws.
|
|
20
|
+
*
|
|
21
|
+
* One of the three variants a caller picks, or `grouped` — the surface an item
|
|
22
|
+
* takes inside a `ListGroup`, where the group already draws the card and the
|
|
23
|
+
* row must draw nothing but itself.
|
|
24
|
+
*/
|
|
25
|
+
export type ItemSurface = ItemVariant | "grouped";
|
|
26
|
+
|
|
27
|
+
/** Theme token an icon in `Item.Media` inherits. */
|
|
28
|
+
export const ITEM_MEDIA_ICON_TOKEN = "foreground";
|
|
29
|
+
|
|
30
|
+
/** Theme token an icon in `Item.Actions` inherits — a trailing hint, not the row's subject. */
|
|
31
|
+
export const ITEM_ACTIONS_ICON_TOKEN = "muted-foreground";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The surface an item draws, given its variant and whether a `ListGroup`
|
|
35
|
+
* encloses it.
|
|
36
|
+
*
|
|
37
|
+
* Inside a group the variant is ignored: the group owns the border, the fill
|
|
38
|
+
* and the corner, and a row repeating any of them would draw a card inside a
|
|
39
|
+
* card.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveItemSurface(variant: ItemVariant, isInGroup: boolean): ItemSurface {
|
|
42
|
+
return isInGroup ? "grouped" : variant;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An item's size: its own when set, else the enclosing `ListGroup`'s, else
|
|
47
|
+
* `md`.
|
|
48
|
+
*
|
|
49
|
+
* The two share one scale on purpose, so an item dropped into a `sm` group
|
|
50
|
+
* comes out `sm` with nothing said at the call site.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveItemSize(size: ItemSize | undefined, groupSize: ItemSize | undefined): ItemSize {
|
|
53
|
+
return size ?? groupSize ?? "md";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The press feedback for an item, when the caller has not named one.
|
|
58
|
+
*
|
|
59
|
+
* A full-bleed row inside a group fades, because a row that scales reads as
|
|
60
|
+
* the whole card flexing. A standalone item is its own card, so it scales the
|
|
61
|
+
* way a card does.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveItemFeedback(feedback: PressableFeedback | undefined, isInGroup: boolean): PressableFeedback {
|
|
64
|
+
return feedback ?? (isInGroup ? "fade" : "scale");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What an item renders as.
|
|
69
|
+
*
|
|
70
|
+
* - `pressable` — it has a handler: a `Pressable`, announced as a button. A
|
|
71
|
+
* disabled one keeps the role, reports `disabled`, and dims through
|
|
72
|
+
* `opacity-50`, which `Pressable` composes with its press animation.
|
|
73
|
+
* - `static` — no handler: a plain view with no role. A static row announcing
|
|
74
|
+
* itself as a button is a lie VoiceOver tells on every swipe.
|
|
75
|
+
*/
|
|
76
|
+
export type ItemRender = "pressable" | "static";
|
|
77
|
+
|
|
78
|
+
export function resolveItemRender(options: {
|
|
79
|
+
onPress?: () => void;
|
|
80
|
+
onLongPress?: () => void;
|
|
81
|
+
isDisabled: boolean;
|
|
82
|
+
}): ItemRender {
|
|
83
|
+
const hasHandler = options.onPress !== undefined || options.onLongPress !== undefined;
|
|
84
|
+
return hasHandler ? "pressable" : "static";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Styling for every part of an item.
|
|
89
|
+
*
|
|
90
|
+
* Row metrics are the same numbers a `ListGroup` row uses, per size — asserted
|
|
91
|
+
* as a pair in the tests — because a group insets its dividers by its own row
|
|
92
|
+
* padding. An item whose padding drifted from that would sit in a group with
|
|
93
|
+
* its text and its divider out of line.
|
|
94
|
+
*
|
|
95
|
+
* `surface` rather than `variant` is the axis here, because inside a group the
|
|
96
|
+
* variant a caller passed does not decide what is drawn. `resolveItemSurface`
|
|
97
|
+
* is where that decision lives.
|
|
98
|
+
*
|
|
99
|
+
* The horizontal root wraps, and `header` and `footer` are `w-full`: a strip
|
|
100
|
+
* then takes a line of its own above or below the media, text and actions, on
|
|
101
|
+
* either orientation, with no extra prop. `content` only flexes along a row — a
|
|
102
|
+
* `flex-1` column in an auto-height parent collapses to nothing in Yoga.
|
|
103
|
+
*
|
|
104
|
+
* No slot but the two text slots holds a `text-*` colour — a React Native
|
|
105
|
+
* `View` does not cascade one to a `Text`.
|
|
106
|
+
*
|
|
107
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
108
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
109
|
+
*/
|
|
110
|
+
export const itemVariants = tv({
|
|
111
|
+
slots: {
|
|
112
|
+
root: "",
|
|
113
|
+
media: "items-center justify-center",
|
|
114
|
+
/** Edge length an `Icon` in the media slot inherits. */
|
|
115
|
+
mediaIcon: "",
|
|
116
|
+
content: "justify-center gap-0.5",
|
|
117
|
+
title: "font-medium text-foreground",
|
|
118
|
+
description: "text-muted-foreground",
|
|
119
|
+
actions: "flex-row items-center gap-2",
|
|
120
|
+
/** Edge length an `Icon` in the actions slot inherits. */
|
|
121
|
+
actionsIcon: "",
|
|
122
|
+
header: "w-full flex-row items-center justify-between gap-2",
|
|
123
|
+
footer: "w-full flex-row items-center justify-between gap-2",
|
|
124
|
+
group: "",
|
|
125
|
+
},
|
|
126
|
+
variants: {
|
|
127
|
+
surface: {
|
|
128
|
+
default: { root: "rounded-lg border border-transparent bg-transparent" },
|
|
129
|
+
outline: { root: "rounded-lg border border-border bg-transparent" },
|
|
130
|
+
muted: { root: "rounded-lg border border-transparent bg-muted" },
|
|
131
|
+
grouped: { root: "w-full bg-transparent" },
|
|
132
|
+
},
|
|
133
|
+
size: {
|
|
134
|
+
sm: {
|
|
135
|
+
root: "min-h-12 gap-2.5 px-3 py-2",
|
|
136
|
+
title: "text-sm",
|
|
137
|
+
description: "text-xs",
|
|
138
|
+
actionsIcon: "size-icon-xs",
|
|
139
|
+
},
|
|
140
|
+
md: {
|
|
141
|
+
root: "min-h-14 gap-3 px-4 py-3",
|
|
142
|
+
title: "text-base",
|
|
143
|
+
description: "text-sm",
|
|
144
|
+
actionsIcon: "size-icon-sm",
|
|
145
|
+
},
|
|
146
|
+
lg: {
|
|
147
|
+
root: "min-h-16 gap-3.5 px-5 py-4",
|
|
148
|
+
title: "text-lg",
|
|
149
|
+
description: "text-base",
|
|
150
|
+
actionsIcon: "size-icon-md",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
orientation: {
|
|
154
|
+
horizontal: { root: "flex-row flex-wrap items-center", content: "flex-1" },
|
|
155
|
+
vertical: { root: "flex-col items-start", content: "self-stretch" },
|
|
156
|
+
},
|
|
157
|
+
mediaVariant: {
|
|
158
|
+
default: {},
|
|
159
|
+
icon: { media: "rounded-md bg-muted" },
|
|
160
|
+
image: { media: "overflow-hidden rounded-md bg-muted" },
|
|
161
|
+
},
|
|
162
|
+
groupOrientation: {
|
|
163
|
+
vertical: { group: "flex-col gap-2" },
|
|
164
|
+
horizontal: { group: "flex-row gap-3" },
|
|
165
|
+
},
|
|
166
|
+
// The empty `false` branches type the props as `boolean` rather than
|
|
167
|
+
// `true`. See the note in button.variants.ts.
|
|
168
|
+
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
169
|
+
isSelected: { true: { root: "bg-accent" }, false: {} },
|
|
170
|
+
},
|
|
171
|
+
compoundVariants: [
|
|
172
|
+
// A card-shaped surface is `rounded-lg`, stepping down to `rounded-md` at
|
|
173
|
+
// `sm` — the same one step `ListGroup` and `Accordion` take.
|
|
174
|
+
{ size: "sm", surface: ["default", "outline", "muted"], class: { root: "rounded-md" } },
|
|
175
|
+
{ mediaVariant: "default", size: "sm", class: { mediaIcon: "size-icon-md" } },
|
|
176
|
+
{ mediaVariant: "default", size: "md", class: { mediaIcon: "size-icon-lg" } },
|
|
177
|
+
{ mediaVariant: "default", size: "lg", class: { mediaIcon: "size-icon-xl" } },
|
|
178
|
+
{ mediaVariant: "icon", size: "sm", class: { media: "size-8", mediaIcon: "size-icon-sm" } },
|
|
179
|
+
{ mediaVariant: "icon", size: "md", class: { media: "size-10", mediaIcon: "size-icon-md" } },
|
|
180
|
+
{ mediaVariant: "icon", size: "lg", class: { media: "size-12", mediaIcon: "size-icon-lg" } },
|
|
181
|
+
{ mediaVariant: "image", size: "sm", class: { media: "size-10" } },
|
|
182
|
+
{ mediaVariant: "image", size: "md", class: { media: "size-12" } },
|
|
183
|
+
{ mediaVariant: "image", size: "lg", class: { media: "size-14" } },
|
|
184
|
+
// A tile is `bg-muted`, so on a muted or selected surface it would vanish
|
|
185
|
+
// into the row.
|
|
186
|
+
{ mediaVariant: ["icon", "image"], surface: "muted", class: { media: "bg-background" } },
|
|
187
|
+
{ mediaVariant: ["icon", "image"], isSelected: true, class: { media: "bg-background" } },
|
|
188
|
+
],
|
|
189
|
+
defaultVariants: {
|
|
190
|
+
surface: "default",
|
|
191
|
+
size: "md",
|
|
192
|
+
orientation: "horizontal",
|
|
193
|
+
mediaVariant: "default",
|
|
194
|
+
groupOrientation: "vertical",
|
|
195
|
+
isDisabled: false,
|
|
196
|
+
isSelected: false,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export type ItemVariantProps = VariantProps<typeof itemVariants>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Kpi
|
|
2
|
+
|
|
3
|
+
One number, what it is doing, and the shape it made getting there — a
|
|
4
|
+
[`Card`](../card/AGENTS.md) with a vocabulary for a metric, whose sparkline is a
|
|
5
|
+
[`Chart`](../chart/AGENTS.md) line. Compound root plus eleven parts: `Header`,
|
|
6
|
+
`Icon`, `Title`, `Action`, `Content`, `Stat`, `Value`, `Trend`, `Sparkline`,
|
|
7
|
+
`Footer`, `Group`.
|
|
8
|
+
|
|
9
|
+
`import { Kpi } from "@delacour/react-native-ui/kpi";`
|
|
10
|
+
|
|
11
|
+
## Files
|
|
12
|
+
|
|
13
|
+
| File | What it holds |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `index.ts` | → `@delacour/react-native-ui/kpi` |
|
|
16
|
+
| `kpi.tsx` | Root + the `Object.assign` compound surface |
|
|
17
|
+
| `kpi-header.tsx` | `Kpi.Header`, on the card header's inset |
|
|
18
|
+
| `kpi-icon.tsx` | `Kpi.Icon`, and the icon defaults it publishes |
|
|
19
|
+
| `kpi-title.tsx` | `Kpi.Title` |
|
|
20
|
+
| `kpi-action.tsx` | `Kpi.Action` |
|
|
21
|
+
| `kpi-content.tsx` | `Kpi.Content`, and the layout it publishes |
|
|
22
|
+
| `kpi-stat.tsx` | `Kpi.Stat` |
|
|
23
|
+
| `kpi-value.tsx` | `Kpi.Value`, and its loading placeholder |
|
|
24
|
+
| `kpi-trend.tsx` | `Kpi.Trend` — text or badge |
|
|
25
|
+
| `kpi-sparkline.tsx` | `Kpi.Sparkline`, and the internal scrub reader |
|
|
26
|
+
| `kpi-footer.tsx` | `Kpi.Footer`, over `Card.Footer` |
|
|
27
|
+
| `kpi-group.tsx` | `Kpi.Group` |
|
|
28
|
+
| `kpi.context.tsx` | `KpiProvider`, `useKpi()`, `useKpiContext()`, `useKpiPart()`, the layout and group contexts |
|
|
29
|
+
| `kpi.types.ts` | Prop types shared by two or more parts |
|
|
30
|
+
| `kpi.variants.ts` | The slotted `tv()`, the axes, and every pure resolver — no RN imports |
|
|
31
|
+
| `kpi.variants.test.ts` | |
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
- **A KPI is a card, not a second definition of one.** The root renders `Card`
|
|
36
|
+
with the caller's `variant` and `size`, so it takes the four fills, steps
|
|
37
|
+
when nested, and has no scale of its own — `KPI_SIZES` *is* `CARD_SIZES`.
|
|
38
|
+
`Kpi.Header` and `Kpi.Content` compose their classes onto `cardVariants`'
|
|
39
|
+
own `header` and `content` slots, and the test asserts the card's inset
|
|
40
|
+
survives the merge, so the number, the title and the footer sit on one line
|
|
41
|
+
and a retune of the card moves all three.
|
|
42
|
+
- **The trend is coloured by what the movement means, never by its sign.**
|
|
43
|
+
`resolveKpiTrend` turns a signed percentage and a `goodDirection` into a
|
|
44
|
+
`direction` and a `tone`. A fall in churn is `down` and `good`, drawn in
|
|
45
|
+
`success-soft-foreground`. `goodDirection` is said once on the card and every
|
|
46
|
+
trend follows it unless it overrides it; `none` keeps the direction and drops
|
|
47
|
+
the judgement. A value that is not finite — a change against a zero baseline
|
|
48
|
+
— is flat and neutral rather than news of either kind.
|
|
49
|
+
- **There is no direction prop.** The sign is the direction. A separate prop
|
|
50
|
+
would be a second statement of one fact that the call site has to keep in
|
|
51
|
+
step, and the first time it did not the card would paint a fall green.
|
|
52
|
+
- **The trend is printed with a typographic minus.** `−4.2%` rather than
|
|
53
|
+
`-4.2%`: a hyphen is narrower than a plus, so a column of changes would not
|
|
54
|
+
line up on the sign. A value that rounds to zero is unsigned — `−0.0%` would
|
|
55
|
+
claim a fall nothing shows.
|
|
56
|
+
- **Colour is never the only signal.** The sign carries the direction in text,
|
|
57
|
+
the badge adds an arrow, and the trend is one accessible string — "Up 7.8
|
|
58
|
+
percent, vs last month" — rather than a number, an arrow and a caption read
|
|
59
|
+
as three stops.
|
|
60
|
+
- **The badge is `Badge`, soft.** `KPI_TONE_BADGE_COLOR` maps the tone onto
|
|
61
|
+
`success`, `destructive` and `default`, so a trend badge is the same object as
|
|
62
|
+
any other status badge on the screen.
|
|
63
|
+
- **The value is formatted by the caller.** Separators, currency and units are
|
|
64
|
+
locale decisions a component would get wrong in a way that is hard to notice
|
|
65
|
+
and impossible to override. It is one line, shrinking to fit — a number that
|
|
66
|
+
wraps is no longer read as one — and it takes the foreground token of the
|
|
67
|
+
plane the card landed on, the way `Card.Title` does.
|
|
68
|
+
- **The title never grows.** A growing child of a column absorbs the column's
|
|
69
|
+
spare height, which in a row of cards lands every number at a different
|
|
70
|
+
height. The header pushes `Kpi.Action` to the end with `ml-auto` instead, and
|
|
71
|
+
a test forbids `flex-1` on the title.
|
|
72
|
+
- **`Kpi.Stat` exists because the value and its change are one fact.** The
|
|
73
|
+
card's gap is for the space between facts; written straight into the content
|
|
74
|
+
the two drift apart and stop reading as a unit. Beside an `inline` sparkline
|
|
75
|
+
the stat takes the row's width.
|
|
76
|
+
- **The sparkline is `Chart`, with nothing but the line.** No grid, no axes, no
|
|
77
|
+
tooltip readout — `Chart` reserves an axis gutter only for an axis that is
|
|
78
|
+
placed, so the plot is the whole frame. The frame is sized through `Chart`'s
|
|
79
|
+
`frameClassName`, which exists for this: the chart's three `size` heights are
|
|
80
|
+
for a chart someone reads with axes. Rows or a bare list of numbers both work;
|
|
81
|
+
`resolveSparklineSeries` indexes a list.
|
|
82
|
+
- **The y bounds are the data's own, padded a tenth.** With no padding a line
|
|
83
|
+
drawn to its exact extent puts the peak and trough on the frame's edge, half
|
|
84
|
+
the stroke off the canvas. `resolveSparklineDomain` pads the span and gives a
|
|
85
|
+
flat series a unit either side so it sits centred.
|
|
86
|
+
- **Inline is a fixed 128pt column; below is the full width.** A stack of cards
|
|
87
|
+
has labels of every length, and a chart taking whatever the text left would
|
|
88
|
+
be a different width on every card. Fixed, the shapes line up down the
|
|
89
|
+
right-hand edge, which is the reason to put them there. Below, the chart is
|
|
90
|
+
filled; inline it is not, where a fill would make it a second block competing
|
|
91
|
+
with the number — `resolveSparklineFilled`, overridable.
|
|
92
|
+
- **`colorIndex` is on the card, not the chart.** The icon and the sparkline
|
|
93
|
+
share it, and a row of cards gets five series colours by setting one prop on
|
|
94
|
+
each. The trend ignores it: a series colour has nothing to say about whether
|
|
95
|
+
the number went the right way. The icon square is the series colour at 15%,
|
|
96
|
+
written out per index because Tailwind's scanner cannot see a class built at
|
|
97
|
+
runtime.
|
|
98
|
+
- **Holding the sparkline scrubs it, and the scrub is state.** A
|
|
99
|
+
`Chart.Tooltip.Dot` rides the line, and an internal `KpiSparklineScrub` —
|
|
100
|
+
a child of `<Chart>` that is not a mark, so the chart mounts it beside the
|
|
101
|
+
canvas with the scrub's shared values in reach — reports the index to the
|
|
102
|
+
root through `scheduleOnRN` only when it changes, and `null` when the finger
|
|
103
|
+
lifts. The root holds it with `useControllableState`: `activeIndex` and
|
|
104
|
+
`onActiveIndexChange` control it, `defaultActiveIndex` seeds it, and
|
|
105
|
+
`useKpi()` reads it, so a part of the caller's own prints the scrubbed
|
|
106
|
+
point's value in place of the latest. A controlled index moves what the
|
|
107
|
+
caller prints; it draws no dot, because the dot is the finger's.
|
|
108
|
+
- **Loading is a placeholder the size of what it stands for.** `isLoading`
|
|
109
|
+
swaps the value, the trend and the sparkline for muted blocks — the value's
|
|
110
|
+
is the height of its line at each size, which a test pins — and marks the
|
|
111
|
+
card busy, so nothing jumps when the data lands. They are
|
|
112
|
+
`muted-foreground` at 15%, not `bg-muted`: `muted` is the secondary fill, and
|
|
113
|
+
the first build's placeholders vanished on a secondary card. A translucent
|
|
114
|
+
foreground reads on every plane, and a test forbids a fill token there.
|
|
115
|
+
- **No interaction on the card.** Like `Card`, a KPI that selects or navigates
|
|
116
|
+
is a `Pressable` around it, which keeps the role, the haptic and the pressed
|
|
117
|
+
state at the call site that knows what the press means. The playground's
|
|
118
|
+
metric picker is the example.
|
|
119
|
+
- **`Kpi.Group` arranges; it does not restyle.** A row gives each metric an
|
|
120
|
+
equal share of the width. `separated` sets them on one `Surface` with a
|
|
121
|
+
`Separator` between each — hidden from assistive technology, so a rule is not
|
|
122
|
+
an unlabelled stop between two numbers — and each metric drops its own fill
|
|
123
|
+
to `transparent` unless it names one.
|
|
124
|
+
- **No text treatment on a view slot** (rule 1). The tests assert it across
|
|
125
|
+
every combination.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { Kpi, type KpiProps } from "./kpi";
|
|
2
|
+
export {
|
|
3
|
+
type KpiContextValue,
|
|
4
|
+
type KpiGroupContextValue,
|
|
5
|
+
KpiProvider,
|
|
6
|
+
useKpi,
|
|
7
|
+
useKpiContext,
|
|
8
|
+
useKpiLayout,
|
|
9
|
+
} from "./kpi.context";
|
|
10
|
+
export type { KpiSlotProps, KpiTextProps } from "./kpi.types";
|
|
11
|
+
export {
|
|
12
|
+
formatKpiTrend,
|
|
13
|
+
KPI_COLOR_INDEXES,
|
|
14
|
+
KPI_DIRECTIONS,
|
|
15
|
+
KPI_GOOD_DIRECTIONS,
|
|
16
|
+
KPI_GROUP_ORIENTATIONS,
|
|
17
|
+
KPI_LAYOUTS,
|
|
18
|
+
KPI_SIZES,
|
|
19
|
+
KPI_TONE_BADGE_COLOR,
|
|
20
|
+
KPI_TONES,
|
|
21
|
+
KPI_TREND_VARIANTS,
|
|
22
|
+
type KpiColorIndex,
|
|
23
|
+
type KpiDirection,
|
|
24
|
+
type KpiGoodDirection,
|
|
25
|
+
type KpiGroupOrientation,
|
|
26
|
+
type KpiLayout,
|
|
27
|
+
type KpiSize,
|
|
28
|
+
type KpiSparklineData,
|
|
29
|
+
type KpiSparklineSeries,
|
|
30
|
+
type KpiTone,
|
|
31
|
+
type KpiTrendVariant,
|
|
32
|
+
type KpiVariantProps,
|
|
33
|
+
kpiSparklineAccessibilityLabel,
|
|
34
|
+
kpiTrendAccessibilityLabel,
|
|
35
|
+
kpiVariants,
|
|
36
|
+
resolveKpiTrend,
|
|
37
|
+
resolveSparklineDomain,
|
|
38
|
+
resolveSparklineFilled,
|
|
39
|
+
resolveSparklineSeries,
|
|
40
|
+
} from "./kpi.variants";
|
|
41
|
+
export type { KpiContentProps } from "./kpi-content";
|
|
42
|
+
export type { KpiFooterProps } from "./kpi-footer";
|
|
43
|
+
export type { KpiGroupProps } from "./kpi-group";
|
|
44
|
+
export type { KpiSparklineProps } from "./kpi-sparkline";
|
|
45
|
+
export type { KpiTrendProps } from "./kpi-trend";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useKpiPart } from "./kpi.context";
|
|
4
|
+
import type { KpiSlotProps } from "./kpi.types";
|
|
5
|
+
import { kpiVariants } from "./kpi.variants";
|
|
6
|
+
|
|
7
|
+
/** The header's trailing end — a menu button, a period filter, a link — pushed to the right edge. */
|
|
8
|
+
export function KpiAction({ className, ...props }: KpiSlotProps): ReactElement {
|
|
9
|
+
const { size } = useKpiPart("Kpi.Action");
|
|
10
|
+
return <View className={kpiVariants({ size }).action({ className })} {...props} />;
|
|
11
|
+
}
|
|
12
|
+
KpiAction.displayName = "DelacourUI.Kpi.Action";
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { KpiLayoutProvider, useKpiPart } from "./kpi.context";
|
|
6
|
+
import type { KpiSlotProps } from "./kpi.types";
|
|
7
|
+
import { type KpiLayout, kpiVariants } from "./kpi.variants";
|
|
8
|
+
|
|
9
|
+
export type KpiContentProps = KpiSlotProps & {
|
|
10
|
+
/**
|
|
11
|
+
* `below` stacks the stat over a full-width sparkline. `inline` sets the
|
|
12
|
+
* sparkline in a fixed column beside the number, for a stack of cards whose
|
|
13
|
+
* shapes should line up down the right-hand edge.
|
|
14
|
+
*/
|
|
15
|
+
layout?: KpiLayout;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The body: the stat and the sparkline. Sits on the card content's own inset,
|
|
20
|
+
* and tells the stat and the sparkline inside it which layout they are in.
|
|
21
|
+
*/
|
|
22
|
+
export function KpiContent({ layout = "below", className, ...props }: KpiContentProps): ReactElement {
|
|
23
|
+
const { size } = useKpiPart("Kpi.Content");
|
|
24
|
+
return (
|
|
25
|
+
<KpiLayoutProvider value={layout}>
|
|
26
|
+
<View
|
|
27
|
+
className={cardVariants({ size }).content({
|
|
28
|
+
className: cn(kpiVariants({ layout, size }).content(), className),
|
|
29
|
+
})}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
</KpiLayoutProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
KpiContent.displayName = "DelacourUI.Kpi.Content";
|