@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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an anchored panel goes — pure, so the whole matrix is reachable from
|
|
3
|
+
* `bun test`.
|
|
4
|
+
*
|
|
5
|
+
* **A leaf.** Popover, Tooltip and, later, Menu and Select all import this
|
|
6
|
+
* file, so it imports nothing at all: never `./popover`, never `./index`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The side of the anchor the panel prefers. A preference: it flips when that side lacks room. */
|
|
10
|
+
export type PopoverPlacement = "top" | "bottom" | "left" | "right";
|
|
11
|
+
|
|
12
|
+
/** Which edges line up along the cross axis. Logical on `top` / `bottom`: mirrored under RTL. */
|
|
13
|
+
export type PopoverAlign = "start" | "center" | "end";
|
|
14
|
+
|
|
15
|
+
/** A fixed width, the anchor's width, the content's own, or the safe span. */
|
|
16
|
+
export type PopoverWidth = number | "trigger" | "content-fit" | "full";
|
|
17
|
+
|
|
18
|
+
/** A view's frame in window coordinates — what `measureInWindow` reports. */
|
|
19
|
+
export type AnchorRect = { x: number; y: number; width: number; height: number };
|
|
20
|
+
|
|
21
|
+
export type AnchoredSize = { width: number; height: number };
|
|
22
|
+
|
|
23
|
+
export type AnchoredInsets = { top: number; right: number; bottom: number; left: number };
|
|
24
|
+
|
|
25
|
+
export type AnchoredBounds = { width: number; height: number; insets: AnchoredInsets };
|
|
26
|
+
|
|
27
|
+
export type AnchoredInput = {
|
|
28
|
+
anchor: AnchorRect;
|
|
29
|
+
content: AnchoredSize;
|
|
30
|
+
bounds: AnchoredBounds;
|
|
31
|
+
placement: PopoverPlacement;
|
|
32
|
+
align: PopoverAlign;
|
|
33
|
+
/** The gap between anchor and panel along the main axis. */
|
|
34
|
+
offset: number;
|
|
35
|
+
/** A nudge along the cross axis, inward from the aligned edge. */
|
|
36
|
+
alignOffset: number;
|
|
37
|
+
/** Keep this far from the bounds' safe edges. */
|
|
38
|
+
collisionPadding: number;
|
|
39
|
+
/** The panel's corner radius plus the arrow's half-diagonal; the arrow never sits inside it. */
|
|
40
|
+
arrowInset: number;
|
|
41
|
+
/** The caller's cap on the panel's height, before the room on the resolved side clamps it. */
|
|
42
|
+
maxHeight?: number;
|
|
43
|
+
/** Align start / end are logical on top / bottom placements. */
|
|
44
|
+
isRTL: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AnchoredPosition = {
|
|
48
|
+
/** The panel's left edge, in window coordinates. */
|
|
49
|
+
x: number;
|
|
50
|
+
/** The panel's top edge, in window coordinates. */
|
|
51
|
+
y: number;
|
|
52
|
+
/** The side it ended up on, after any flip. */
|
|
53
|
+
placement: PopoverPlacement;
|
|
54
|
+
/** `min(maxHeight, room)` — the room on the resolved side for top / bottom, the safe height for left / right. */
|
|
55
|
+
maxHeight: number;
|
|
56
|
+
/** Distance along the panel's facing edge to the arrow's centre — toward the anchor's centre, clamped clear of the corners. */
|
|
57
|
+
arrowOffset: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type SafeRect = { left: number; top: number; right: number; bottom: number };
|
|
61
|
+
|
|
62
|
+
const OPPOSITE: Record<PopoverPlacement, PopoverPlacement> = {
|
|
63
|
+
top: "bottom",
|
|
64
|
+
bottom: "top",
|
|
65
|
+
left: "right",
|
|
66
|
+
right: "left",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function isVertical(placement: PopoverPlacement): boolean {
|
|
70
|
+
return placement === "top" || placement === "bottom";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function safeRect(bounds: AnchoredBounds, padding: number): SafeRect {
|
|
74
|
+
return {
|
|
75
|
+
left: bounds.insets.left + padding,
|
|
76
|
+
top: bounds.insets.top + padding,
|
|
77
|
+
right: bounds.width - bounds.insets.right - padding,
|
|
78
|
+
bottom: bounds.height - bounds.insets.bottom - padding,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The space between the anchor (plus offset) and the safe edge on `placement`'s side. */
|
|
83
|
+
function roomOn(placement: PopoverPlacement, anchor: AnchorRect, offset: number, safe: SafeRect): number {
|
|
84
|
+
switch (placement) {
|
|
85
|
+
case "bottom":
|
|
86
|
+
return safe.bottom - (anchor.y + anchor.height + offset);
|
|
87
|
+
case "top":
|
|
88
|
+
return anchor.y - offset - safe.top;
|
|
89
|
+
case "right":
|
|
90
|
+
return safe.right - (anchor.x + anchor.width + offset);
|
|
91
|
+
case "left":
|
|
92
|
+
return anchor.x - offset - safe.left;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clamp(value: number, min: number, max: number): number {
|
|
97
|
+
return Math.min(Math.max(value, min), Math.max(min, max));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The cross-axis start of a panel of `size` along an anchor spanning
|
|
102
|
+
* `[start, start + length]`, before any shift.
|
|
103
|
+
*
|
|
104
|
+
* `start` lines up leading edges, `end` trailing edges. `alignOffset` moves the
|
|
105
|
+
* panel inward from whichever edge it is aligned to, and `direction` is −1 on
|
|
106
|
+
* the horizontal axis under RTL, which is what makes start / end logical.
|
|
107
|
+
*/
|
|
108
|
+
function alignAlong(
|
|
109
|
+
start: number,
|
|
110
|
+
length: number,
|
|
111
|
+
size: number,
|
|
112
|
+
align: PopoverAlign,
|
|
113
|
+
alignOffset: number,
|
|
114
|
+
direction: 1 | -1
|
|
115
|
+
): number {
|
|
116
|
+
const leading = direction === 1 ? start : start + length - size;
|
|
117
|
+
const trailing = direction === 1 ? start + length - size : start;
|
|
118
|
+
switch (align) {
|
|
119
|
+
case "start":
|
|
120
|
+
return leading + alignOffset * direction;
|
|
121
|
+
case "end":
|
|
122
|
+
return trailing - alignOffset * direction;
|
|
123
|
+
case "center":
|
|
124
|
+
return start + (length - size) / 2 + alignOffset * direction;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Places a panel of `content` size against `anchor`.
|
|
130
|
+
*
|
|
131
|
+
* 1. Prefer `placement`. Flip to the opposite side **only** when the preferred
|
|
132
|
+
* side cannot hold the panel and the opposite has more room — a panel that
|
|
133
|
+
* fits nowhere stays where it was asked to be unless moving buys room.
|
|
134
|
+
* 2. `maxHeight` is `min(maxHeight, room)`: the room on the resolved side for
|
|
135
|
+
* a top / bottom panel, the safe height for a side one. A panel taller than
|
|
136
|
+
* that is clamped, which is what keeps it on screen.
|
|
137
|
+
* 3. Align along the cross axis, then shift to stay inside
|
|
138
|
+
* `bounds − insets − collisionPadding`. Finally clamp both axes, so the
|
|
139
|
+
* panel is never off-screen whatever the anchor did.
|
|
140
|
+
* 4. The arrow points at the anchor's centre, clamped to
|
|
141
|
+
* `[arrowInset, size − arrowInset]` — the middle when the panel is too small
|
|
142
|
+
* to clear both corners.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveAnchoredPosition(input: AnchoredInput): AnchoredPosition {
|
|
145
|
+
const { anchor, content, bounds, align, offset, alignOffset, collisionPadding, arrowInset, isRTL } = input;
|
|
146
|
+
const safe = safeRect(bounds, collisionPadding);
|
|
147
|
+
const cap = input.maxHeight ?? Number.POSITIVE_INFINITY;
|
|
148
|
+
|
|
149
|
+
const preferred = input.placement;
|
|
150
|
+
const opposite = OPPOSITE[preferred];
|
|
151
|
+
const needed = (placement: PopoverPlacement): number =>
|
|
152
|
+
isVertical(placement) ? Math.min(content.height, cap) : content.width;
|
|
153
|
+
const preferredRoom = roomOn(preferred, anchor, offset, safe);
|
|
154
|
+
const oppositeRoom = roomOn(opposite, anchor, offset, safe);
|
|
155
|
+
const placement = preferredRoom < needed(preferred) && oppositeRoom > preferredRoom ? opposite : preferred;
|
|
156
|
+
|
|
157
|
+
const room = isVertical(placement) ? roomOn(placement, anchor, offset, safe) : safe.bottom - safe.top;
|
|
158
|
+
const maxHeight = Math.max(0, Math.min(cap, room));
|
|
159
|
+
const width = content.width;
|
|
160
|
+
const height = Math.min(content.height, maxHeight);
|
|
161
|
+
|
|
162
|
+
let x: number;
|
|
163
|
+
let y: number;
|
|
164
|
+
if (isVertical(placement)) {
|
|
165
|
+
y = placement === "bottom" ? anchor.y + anchor.height + offset : anchor.y - offset - height;
|
|
166
|
+
x = alignAlong(anchor.x, anchor.width, width, align, alignOffset, isRTL ? -1 : 1);
|
|
167
|
+
} else {
|
|
168
|
+
x = placement === "right" ? anchor.x + anchor.width + offset : anchor.x - offset - width;
|
|
169
|
+
y = alignAlong(anchor.y, anchor.height, height, align, alignOffset, 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
x = clamp(x, safe.left, safe.right - width);
|
|
173
|
+
y = clamp(y, safe.top, safe.bottom - height);
|
|
174
|
+
|
|
175
|
+
const edge = isVertical(placement) ? width : height;
|
|
176
|
+
const toCentre = isVertical(placement) ? anchor.x + anchor.width / 2 - x : anchor.y + anchor.height / 2 - y;
|
|
177
|
+
const arrowOffset = edge < arrowInset * 2 ? edge / 2 : clamp(toCentre, arrowInset, edge - arrowInset);
|
|
178
|
+
|
|
179
|
+
return { x, y, placement, maxHeight, arrowOffset };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type PopoverWidthInput = {
|
|
183
|
+
width: PopoverWidth;
|
|
184
|
+
anchorWidth: number;
|
|
185
|
+
bounds: AnchoredBounds;
|
|
186
|
+
collisionPadding: number;
|
|
187
|
+
minWidth?: number;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type ResolvedPopoverWidth = {
|
|
191
|
+
/** A fixed width, or `undefined` to let the content size itself. */
|
|
192
|
+
width?: number;
|
|
193
|
+
/** Only for a content-fit panel; a fixed width has already been raised to it. */
|
|
194
|
+
minWidth?: number;
|
|
195
|
+
/** The safe span — no panel is ever wider than the screen it is on. */
|
|
196
|
+
maxWidth: number;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolves `width` to numbers before the panel is measured.
|
|
201
|
+
*
|
|
202
|
+
* `"trigger"` is the anchor's width, which is what a form field under its
|
|
203
|
+
* trigger wants; `"full"` is the safe span; a number is taken as given. Each is
|
|
204
|
+
* raised to `minWidth` and capped at the safe span. `"content-fit"` leaves the
|
|
205
|
+
* width to the content and passes `minWidth` through as a floor.
|
|
206
|
+
*/
|
|
207
|
+
export function resolvePopoverWidth({
|
|
208
|
+
width,
|
|
209
|
+
anchorWidth,
|
|
210
|
+
bounds,
|
|
211
|
+
collisionPadding,
|
|
212
|
+
minWidth,
|
|
213
|
+
}: PopoverWidthInput): ResolvedPopoverWidth {
|
|
214
|
+
const maxWidth = Math.max(0, bounds.width - bounds.insets.left - bounds.insets.right - collisionPadding * 2);
|
|
215
|
+
if (width === "content-fit") {
|
|
216
|
+
return minWidth === undefined ? { maxWidth } : { minWidth: Math.min(minWidth, maxWidth), maxWidth };
|
|
217
|
+
}
|
|
218
|
+
const base = width === "trigger" ? anchorWidth : width === "full" ? maxWidth : width;
|
|
219
|
+
return { width: Math.min(Math.max(base, minWidth ?? 0), maxWidth), maxWidth };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Where the panel starts its entrance, relative to where it settles: `distance`
|
|
224
|
+
* back toward the anchor, so a panel below the trigger enters downward from it.
|
|
225
|
+
*/
|
|
226
|
+
export function resolveEnterTranslate(placement: PopoverPlacement, distance: number): { x: number; y: number } {
|
|
227
|
+
switch (placement) {
|
|
228
|
+
case "bottom":
|
|
229
|
+
return { x: 0, y: -distance };
|
|
230
|
+
case "top":
|
|
231
|
+
return { x: 0, y: distance };
|
|
232
|
+
case "right":
|
|
233
|
+
return { x: -distance, y: 0 };
|
|
234
|
+
case "left":
|
|
235
|
+
return { x: distance, y: 0 };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The scale's origin, in the panel's own coordinates: the arrow, on the edge
|
|
241
|
+
* facing the anchor — so the panel grows out of the thing that opened it.
|
|
242
|
+
*/
|
|
243
|
+
export function resolveTransformOrigin(
|
|
244
|
+
placement: PopoverPlacement,
|
|
245
|
+
arrowOffset: number,
|
|
246
|
+
size: AnchoredSize
|
|
247
|
+
): { x: number; y: number } {
|
|
248
|
+
switch (placement) {
|
|
249
|
+
case "bottom":
|
|
250
|
+
return { x: arrowOffset, y: 0 };
|
|
251
|
+
case "top":
|
|
252
|
+
return { x: arrowOffset, y: size.height };
|
|
253
|
+
case "right":
|
|
254
|
+
return { x: 0, y: arrowOffset };
|
|
255
|
+
case "left":
|
|
256
|
+
return { x: size.width, y: arrowOffset };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Where the arrow's square sits inside the panel, and how far it is turned.
|
|
262
|
+
*
|
|
263
|
+
* Centred on the facing edge at `arrowOffset`, so half of it shows past the
|
|
264
|
+
* panel. The square carries its border on the bottom and right edges only, and
|
|
265
|
+
* at 45° the corner between those two points straight down: each placement
|
|
266
|
+
* turns that corner toward the anchor, so the bordered edges are the outer two
|
|
267
|
+
* and the bare two are the ones that lie over the panel.
|
|
268
|
+
*/
|
|
269
|
+
export function resolveArrowFrame(
|
|
270
|
+
placement: PopoverPlacement,
|
|
271
|
+
arrowOffset: number,
|
|
272
|
+
size: AnchoredSize,
|
|
273
|
+
arrowSize: number
|
|
274
|
+
): { left: number; top: number; rotate: number } {
|
|
275
|
+
const half = arrowSize / 2;
|
|
276
|
+
switch (placement) {
|
|
277
|
+
case "bottom":
|
|
278
|
+
return { left: arrowOffset - half, top: -half, rotate: 225 };
|
|
279
|
+
case "top":
|
|
280
|
+
return { left: arrowOffset - half, top: size.height - half, rotate: 45 };
|
|
281
|
+
case "right":
|
|
282
|
+
return { left: -half, top: arrowOffset - half, rotate: 135 };
|
|
283
|
+
case "left":
|
|
284
|
+
return { left: size.width - half, top: arrowOffset - half, rotate: 315 };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
3
|
+
import { PopoverContext, type PopoverContextValue } from "./popover.context";
|
|
4
|
+
import type { PopoverPlacement } from "./popover.position";
|
|
5
|
+
import { POPOVER_DEFAULTS } from "./popover.variants";
|
|
6
|
+
import { PopoverAnchor } from "./popover-anchor";
|
|
7
|
+
import { PopoverArrow } from "./popover-arrow";
|
|
8
|
+
import { PopoverClose } from "./popover-close";
|
|
9
|
+
import { PopoverContent } from "./popover-content";
|
|
10
|
+
import { PopoverDescription } from "./popover-description";
|
|
11
|
+
import { PopoverTitle } from "./popover-title";
|
|
12
|
+
import { PopoverTrigger } from "./popover-trigger";
|
|
13
|
+
import { useAnchorMeasure } from "./use-anchor-measure";
|
|
14
|
+
|
|
15
|
+
export type PopoverProps = {
|
|
16
|
+
isOpen?: boolean;
|
|
17
|
+
/** Default false. */
|
|
18
|
+
defaultOpen?: boolean;
|
|
19
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
20
|
+
/** An outside tap, Android back and the escape gesture close it. Default true. */
|
|
21
|
+
isDismissible?: boolean;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function PopoverRoot({
|
|
26
|
+
isOpen: isOpenProp,
|
|
27
|
+
defaultOpen = false,
|
|
28
|
+
onOpenChange,
|
|
29
|
+
isDismissible = true,
|
|
30
|
+
children,
|
|
31
|
+
}: PopoverProps): ReactElement {
|
|
32
|
+
const [isOpen, setOpen] = useControllableState({
|
|
33
|
+
value: isOpenProp,
|
|
34
|
+
defaultValue: defaultOpen,
|
|
35
|
+
onChange: onOpenChange,
|
|
36
|
+
});
|
|
37
|
+
const [anchorCount, setAnchorCount] = useState(0);
|
|
38
|
+
const [placement, setPlacement] = useState<PopoverPlacement>(POPOVER_DEFAULTS.placement);
|
|
39
|
+
const hasAnchor = anchorCount > 0;
|
|
40
|
+
|
|
41
|
+
const trigger = useAnchorMeasure({ isEnabled: isOpen && !hasAnchor });
|
|
42
|
+
const anchor = useAnchorMeasure({ isEnabled: isOpen && hasAnchor });
|
|
43
|
+
|
|
44
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
45
|
+
|
|
46
|
+
const registerAnchor = useCallback(() => {
|
|
47
|
+
setAnchorCount((count) => count + 1);
|
|
48
|
+
return () => setAnchorCount((count) => count - 1);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const value = useMemo<PopoverContextValue>(
|
|
52
|
+
() => ({
|
|
53
|
+
isOpen,
|
|
54
|
+
setOpen,
|
|
55
|
+
close,
|
|
56
|
+
isDismissible,
|
|
57
|
+
placement,
|
|
58
|
+
onPlaced: setPlacement,
|
|
59
|
+
anchorRect: hasAnchor ? anchor.rect : trigger.rect,
|
|
60
|
+
triggerRef: trigger.ref,
|
|
61
|
+
triggerNode: trigger.node,
|
|
62
|
+
anchorRef: anchor.ref,
|
|
63
|
+
registerAnchor,
|
|
64
|
+
}),
|
|
65
|
+
[
|
|
66
|
+
isOpen,
|
|
67
|
+
setOpen,
|
|
68
|
+
close,
|
|
69
|
+
isDismissible,
|
|
70
|
+
placement,
|
|
71
|
+
hasAnchor,
|
|
72
|
+
anchor.rect,
|
|
73
|
+
anchor.ref,
|
|
74
|
+
trigger.rect,
|
|
75
|
+
trigger.ref,
|
|
76
|
+
trigger.node,
|
|
77
|
+
registerAnchor,
|
|
78
|
+
]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return <PopoverContext.Provider value={value}>{children}</PopoverContext.Provider>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A small panel anchored to the control that opened it, with the screen around
|
|
86
|
+
* it still visible — a rename field beside a title, a note on a badge, a short
|
|
87
|
+
* list of options.
|
|
88
|
+
*
|
|
89
|
+
* The placement is a preference: the panel flips across its anchor and slides
|
|
90
|
+
* along it to stay inside the safe area and above the keyboard. It draws in the
|
|
91
|
+
* overlay layer — over sheets, dialogs and the navigator header — so it needs
|
|
92
|
+
* `OverlayProvider` at the app root. An outside tap, Android back and the
|
|
93
|
+
* escape gesture close it unless `isDismissible` is false.
|
|
94
|
+
*
|
|
95
|
+
* Open state is `isOpen` / `defaultOpen` / `onOpenChange`, controlled or not.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* <Popover>
|
|
99
|
+
* <Popover.Trigger asChild><Button variant="secondary">Rename</Button></Popover.Trigger>
|
|
100
|
+
* <Popover.Content align="start" width="trigger" minWidth={260}>
|
|
101
|
+
* <Popover.Arrow />
|
|
102
|
+
* <Popover.Title>Rename</Popover.Title>
|
|
103
|
+
* <Input value={name} onChangeText={setName} />
|
|
104
|
+
* </Popover.Content>
|
|
105
|
+
* </Popover>
|
|
106
|
+
*/
|
|
107
|
+
export const Popover = Object.assign(PopoverRoot, {
|
|
108
|
+
/** Opens the popover; the panel anchors to it unless a `Popover.Anchor` is written. */
|
|
109
|
+
Trigger: PopoverTrigger,
|
|
110
|
+
/** Anchors the panel to a different view than the trigger. */
|
|
111
|
+
Anchor: PopoverAnchor,
|
|
112
|
+
/** The panel — teleported, measured, placed and animated. */
|
|
113
|
+
Content: PopoverContent,
|
|
114
|
+
/** The arrow pointing from the panel at the anchor. */
|
|
115
|
+
Arrow: PopoverArrow,
|
|
116
|
+
/** The panel's heading, which labels it. */
|
|
117
|
+
Title: PopoverTitle,
|
|
118
|
+
/** Muted supporting copy under the title. */
|
|
119
|
+
Description: PopoverDescription,
|
|
120
|
+
/** Closes the popover — the corner ✕, or `asChild` around a button. */
|
|
121
|
+
Close: PopoverClose,
|
|
122
|
+
displayName: "DelacourUI.Popover",
|
|
123
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens, RADIUS_BASE_PX } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { OVERLAY_SCRIM_TOKEN } from "../overlay/overlay.variants";
|
|
4
|
+
import {
|
|
5
|
+
POPOVER_ARROW_INSET,
|
|
6
|
+
POPOVER_ARROW_SIZE,
|
|
7
|
+
POPOVER_CLOSE_HIT_SLOP,
|
|
8
|
+
POPOVER_COLLISION_PADDING,
|
|
9
|
+
POPOVER_DEFAULTS,
|
|
10
|
+
POPOVER_ENTER_DISTANCE,
|
|
11
|
+
POPOVER_ENTER_SCALE,
|
|
12
|
+
popoverVariants,
|
|
13
|
+
} from "./popover.variants";
|
|
14
|
+
|
|
15
|
+
const LIGHT = declaredTokens("light");
|
|
16
|
+
const DARK = declaredTokens("dark");
|
|
17
|
+
|
|
18
|
+
/** `border-t` and friends set a width, not a colour, and name no token. */
|
|
19
|
+
const STRUCTURAL_BORDER_SUFFIXES = new Set(["t", "b", "l", "r", "x", "y", "s", "e"]);
|
|
20
|
+
|
|
21
|
+
/** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
|
|
22
|
+
function colorTokens(cls: string): string[] {
|
|
23
|
+
const tokens: string[] = [];
|
|
24
|
+
for (const [, utility, token] of cls.matchAll(/\b(bg|border|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
|
|
25
|
+
if (utility === "border" && STRUCTURAL_BORDER_SUFFIXES.has(token)) continue;
|
|
26
|
+
tokens.push(token);
|
|
27
|
+
}
|
|
28
|
+
return tokens;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The slots this component declares, pinned so a new one has to be added before the sweeps can miss it. */
|
|
32
|
+
const SLOT_NAMES = ["scrim", "dismissLayer", "content", "arrow", "title", "description", "close"] as const;
|
|
33
|
+
|
|
34
|
+
const SLOTS = popoverVariants();
|
|
35
|
+
const UNSTYLED = popoverVariants({ isUnstyled: true });
|
|
36
|
+
|
|
37
|
+
describe("popoverVariants — slots", () => {
|
|
38
|
+
test("declares every slot", () => {
|
|
39
|
+
for (const name of SLOT_NAMES) expect(typeof SLOTS[name]).toBe("function");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("every token a slot paints with exists in both themes", () => {
|
|
43
|
+
for (const name of SLOT_NAMES) {
|
|
44
|
+
for (const token of colorTokens(SLOTS[name]() ?? "")) {
|
|
45
|
+
expect(LIGHT.has(token)).toBe(true);
|
|
46
|
+
expect(DARK.has(token)).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("the scrim is the foundation's token", () => {
|
|
52
|
+
expect(SLOTS.scrim()).toContain(`bg-${OVERLAY_SCRIM_TOKEN}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the dismiss layer fills the screen and paints nothing", () => {
|
|
56
|
+
const layer = SLOTS.dismissLayer();
|
|
57
|
+
expect(layer).toContain("absolute");
|
|
58
|
+
expect(layer).toContain("inset-0");
|
|
59
|
+
expect(colorTokens(layer)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the panel is a popover surface with a hairline, a card corner, padding and a gap", () => {
|
|
63
|
+
const content = SLOTS.content();
|
|
64
|
+
expect(content).toContain("bg-popover");
|
|
65
|
+
expect(content).toContain("border");
|
|
66
|
+
expect(content).toContain("border-border");
|
|
67
|
+
expect(content).toContain("rounded-lg");
|
|
68
|
+
expect(content).toMatch(/\bp-\d/);
|
|
69
|
+
expect(content).toMatch(/\bgap-\d/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("the arrow is the panel's fill with the panel's border on two edges", () => {
|
|
73
|
+
const arrow = SLOTS.arrow();
|
|
74
|
+
expect(arrow).toContain("bg-popover");
|
|
75
|
+
expect(arrow).toContain("border-border");
|
|
76
|
+
expect(arrow).toContain("border-b");
|
|
77
|
+
expect(arrow).toContain("border-r");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("the title reserves room for the close control and sets no type of its own", () => {
|
|
81
|
+
const title = SLOTS.title();
|
|
82
|
+
expect(title).toContain("pr-6");
|
|
83
|
+
expect(title).not.toMatch(/\btext-(xs|sm|base|lg|xl)\b/);
|
|
84
|
+
expect(title).not.toMatch(/\bfont-/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the close control sits in the corner, out of the flow", () => {
|
|
88
|
+
expect(SLOTS.close()).toContain("absolute");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a caller's className wins on the panel", () => {
|
|
92
|
+
expect(SLOTS.content({ className: "p-6" })).toContain("p-6");
|
|
93
|
+
expect(SLOTS.content({ className: "p-6" })).not.toMatch(/\bp-3\b/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("popoverVariants — isUnstyled", () => {
|
|
98
|
+
test("strips the surface, the border, the corner and the padding", () => {
|
|
99
|
+
const content = UNSTYLED.content();
|
|
100
|
+
expect(content).not.toContain("bg-popover");
|
|
101
|
+
expect(content).not.toMatch(/\bborder\b/);
|
|
102
|
+
expect(content).not.toContain("rounded-lg");
|
|
103
|
+
expect(content).not.toMatch(/\bp-\d/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("strips the arrow's paint too", () => {
|
|
107
|
+
expect(UNSTYLED.arrow()).not.toContain("bg-popover");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("constants", () => {
|
|
112
|
+
test("the arrow never sits inside the panel's rounded corner", () => {
|
|
113
|
+
expect(POPOVER_ARROW_INSET).toBeGreaterThanOrEqual(RADIUS_BASE_PX + (POPOVER_ARROW_SIZE * Math.SQRT2) / 2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("defaults match the documented API", () => {
|
|
117
|
+
expect(POPOVER_DEFAULTS).toEqual({
|
|
118
|
+
placement: "bottom",
|
|
119
|
+
align: "center",
|
|
120
|
+
offset: 8,
|
|
121
|
+
alignOffset: 0,
|
|
122
|
+
width: "content-fit",
|
|
123
|
+
});
|
|
124
|
+
expect(POPOVER_COLLISION_PADDING).toBe(8);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("the entrance is a short slide and a slight scale", () => {
|
|
128
|
+
expect(POPOVER_ENTER_DISTANCE).toBe(6);
|
|
129
|
+
expect(POPOVER_ENTER_SCALE).toBe(0.96);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("the close glyph has slop to reach a 44pt target", () => {
|
|
133
|
+
expect(POPOVER_CLOSE_HIT_SLOP).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { PopoverAlign, PopoverPlacement, PopoverWidth } from "./popover.position";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What `Popover.Content` does when a prop is left out.
|
|
7
|
+
*
|
|
8
|
+
* Below the trigger and centred on it, 8pt clear of it, as wide as its content.
|
|
9
|
+
*/
|
|
10
|
+
export const POPOVER_DEFAULTS = {
|
|
11
|
+
placement: "bottom",
|
|
12
|
+
align: "center",
|
|
13
|
+
offset: 8,
|
|
14
|
+
alignOffset: 0,
|
|
15
|
+
width: "content-fit",
|
|
16
|
+
} as const satisfies {
|
|
17
|
+
placement: PopoverPlacement;
|
|
18
|
+
align: PopoverAlign;
|
|
19
|
+
offset: number;
|
|
20
|
+
alignOffset: number;
|
|
21
|
+
width: PopoverWidth;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** How far a panel keeps from the safe area's edges, in points. */
|
|
25
|
+
export const POPOVER_COLLISION_PADDING = 8;
|
|
26
|
+
|
|
27
|
+
/** The arrow's square, in points, before it is rotated 45°. Half of it shows past the panel's edge. */
|
|
28
|
+
export const POPOVER_ARROW_SIZE = 10;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How close the arrow's centre may come to a panel corner, in points.
|
|
32
|
+
*
|
|
33
|
+
* The panel's corner is `rounded-lg`, which is `--radius` — 10pt by default —
|
|
34
|
+
* and a rotated square reaches half its diagonal either side of its centre. Any
|
|
35
|
+
* less and the arrow would hang off the curve of the corner instead of the
|
|
36
|
+
* straight edge. The test pins it against the radius `tokens.css` declares.
|
|
37
|
+
*/
|
|
38
|
+
export const POPOVER_ARROW_INSET = 18;
|
|
39
|
+
|
|
40
|
+
/** How far the panel travels on its way in, toward its resolved side. */
|
|
41
|
+
export const POPOVER_ENTER_DISTANCE = 6;
|
|
42
|
+
|
|
43
|
+
/** The scale the panel grows from, at the arrow. */
|
|
44
|
+
export const POPOVER_ENTER_SCALE = 0.96;
|
|
45
|
+
|
|
46
|
+
/** A bare glyph in a corner has no capsule to bring it to 44pt — the slop does. */
|
|
47
|
+
export const POPOVER_CLOSE_HIT_SLOP = 8;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Styling for every part of a popover.
|
|
51
|
+
*
|
|
52
|
+
* `content` is the panel: the popover token, the hairline, the card corner and
|
|
53
|
+
* the padding — and `isUnstyled` strips all four, keeping only the gap, for a caller drawing their
|
|
54
|
+
* own surface with `background`. The arrow is the same fill with the border on
|
|
55
|
+
* its two outer edges; rotated 45° those are the two that show past the
|
|
56
|
+
* panel's edge, and the two that would draw a seam across the panel stay bare.
|
|
57
|
+
*
|
|
58
|
+
* `dismissLayer` is the invisible full-screen catcher under a scrim-less
|
|
59
|
+
* panel. It paints nothing, and the test holds it to that.
|
|
60
|
+
*/
|
|
61
|
+
export const popoverVariants = tv({
|
|
62
|
+
slots: {
|
|
63
|
+
scrim: "absolute inset-0 bg-overlay",
|
|
64
|
+
dismissLayer: "absolute inset-0",
|
|
65
|
+
content: "gap-2",
|
|
66
|
+
arrow: "absolute",
|
|
67
|
+
title: "pr-6",
|
|
68
|
+
description: "",
|
|
69
|
+
close: "absolute top-2 right-2 z-10",
|
|
70
|
+
},
|
|
71
|
+
variants: {
|
|
72
|
+
isUnstyled: {
|
|
73
|
+
true: {},
|
|
74
|
+
false: {
|
|
75
|
+
content: "rounded-lg border border-border bg-popover p-3",
|
|
76
|
+
arrow: "border-b border-r border-border bg-popover",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
defaultVariants: {
|
|
81
|
+
isUnstyled: false,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export type PopoverVariantProps = VariantProps<typeof popoverVariants>;
|