@delacour/react-native-ui 0.1.0-alpha.4 → 0.2.0-alpha.20261010061731
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 +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 +10 -0
- package/src/components/badge/badge-close-button.tsx +2 -2
- package/src/components/badge/badge.context.tsx +2 -1
- package/src/components/badge/badge.tsx +13 -4
- package/src/components/badge/badge.variants.test.ts +26 -0
- package/src/components/badge/badge.variants.ts +21 -0
- 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 +51 -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 +7 -1
- package/src/components/button/button.variants.test.ts +190 -0
- package/src/components/button/button.variants.ts +87 -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 +92 -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 +100 -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/input/AGENTS.md +5 -0
- package/src/components/input/input.variants.test.ts +32 -0
- package/src/components/input/input.variants.ts +13 -1
- 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 +82 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +56 -0
- package/src/components/surface/surface.tsx +87 -0
- package/src/components/surface/surface.variants.test.ts +227 -0
- package/src/components/surface/surface.variants.ts +165 -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/text/AGENTS.md +8 -2
- package/src/components/text/text.tsx +7 -0
- package/src/components/text/text.variants.test.ts +24 -1
- package/src/components/text/text.variants.ts +5 -0
- package/src/components/textarea/AGENTS.md +89 -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.parity.test.ts +62 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +11 -6
- 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 +44 -0
- package/src/styles/tokens.test.ts +56 -0
- package/src/styles/tokens.ts +29 -1
- 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,91 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use, useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useMenuPart } from "../menu/menu.context";
|
|
3
|
+
import type { MenuAnchorRect } from "../menu/menu.variants";
|
|
4
|
+
import {
|
|
5
|
+
type ContextMenuAnchor,
|
|
6
|
+
type ContextMenuInvoker,
|
|
7
|
+
type ContextMenuPoint,
|
|
8
|
+
resolveContextAnchor,
|
|
9
|
+
} from "./context-menu.variants";
|
|
10
|
+
|
|
11
|
+
/** One hold (or screen-reader action) that opened the menu, as the trigger saw it. */
|
|
12
|
+
export type ContextMenuInvocation = {
|
|
13
|
+
mode: ContextMenuAnchor;
|
|
14
|
+
point: ContextMenuPoint | null;
|
|
15
|
+
targetRect: MenuAnchorRect;
|
|
16
|
+
invokedBy: ContextMenuInvoker;
|
|
17
|
+
/**
|
|
18
|
+
* The very rect handed to Menu's `open()`. Menu keeps it by identity, so a
|
|
19
|
+
* mismatch means the menu was opened some other way — a parent flipping
|
|
20
|
+
* `isOpen` — and this invocation is stale.
|
|
21
|
+
*/
|
|
22
|
+
opened: MenuAnchorRect;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ContextMenuContextValue = {
|
|
26
|
+
invocation: ContextMenuInvocation | null;
|
|
27
|
+
invoke: (invocation: ContextMenuInvocation) => void;
|
|
28
|
+
/** The trigger's children, which a bare `ContextMenu.Preview` lifts. */
|
|
29
|
+
content: ReactNode;
|
|
30
|
+
registerContent: (content: ReactNode) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ContextMenuContext = createContext<ContextMenuContextValue | null>(null);
|
|
34
|
+
|
|
35
|
+
export function ContextMenuProvider({
|
|
36
|
+
value,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
value: ContextMenuContextValue;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): ReactElement {
|
|
42
|
+
return <ContextMenuContext value={value}>{children}</ContextMenuContext>;
|
|
43
|
+
}
|
|
44
|
+
ContextMenuProvider.displayName = "DelacourUI.ContextMenu.Provider";
|
|
45
|
+
|
|
46
|
+
/** The context-menu context, for its own parts. Throws with the part's name outside a `ContextMenu`. */
|
|
47
|
+
export function useContextMenuPart(part: string): ContextMenuContextValue {
|
|
48
|
+
const context = use(ContextMenuContext);
|
|
49
|
+
if (!context) throw new Error(`${part} must be used inside a <ContextMenu>.`);
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The root's state: the last invocation and the trigger's registered children. */
|
|
54
|
+
export function useContextMenuRootValue(): ContextMenuContextValue {
|
|
55
|
+
const [invocation, setInvocation] = useState<ContextMenuInvocation | null>(null);
|
|
56
|
+
const [content, setContent] = useState<ReactNode>(null);
|
|
57
|
+
const invoke = useCallback((next: ContextMenuInvocation) => setInvocation(next), []);
|
|
58
|
+
const registerContent = useCallback((next: ReactNode) => setContent(next), []);
|
|
59
|
+
return useMemo(
|
|
60
|
+
() => ({ content, invocation, invoke, registerContent }),
|
|
61
|
+
[content, invocation, invoke, registerContent]
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Where the panel anchors and where the lifted preview sits, for this visit.
|
|
67
|
+
*
|
|
68
|
+
* Read by both `ContextMenu.Content` and `ContextMenu.Preview`, so the two can
|
|
69
|
+
* never disagree. A current invocation runs through `resolveContextAnchor`;
|
|
70
|
+
* without one (a controlled open) both fall back to the trigger rect Menu
|
|
71
|
+
* measured itself.
|
|
72
|
+
*/
|
|
73
|
+
export function useContextMenuTarget(
|
|
74
|
+
part: string,
|
|
75
|
+
hasPreview: boolean
|
|
76
|
+
): { anchor: MenuAnchorRect | null; targetRect: MenuAnchorRect | null } {
|
|
77
|
+
const { anchor: menuAnchor } = useMenuPart(part);
|
|
78
|
+
const { invocation } = useContextMenuPart(part);
|
|
79
|
+
const current = invocation !== null && invocation.opened === menuAnchor ? invocation : null;
|
|
80
|
+
if (!current) return { anchor: menuAnchor, targetRect: menuAnchor };
|
|
81
|
+
return {
|
|
82
|
+
anchor: resolveContextAnchor({
|
|
83
|
+
hasPreview,
|
|
84
|
+
invokedBy: current.invokedBy,
|
|
85
|
+
mode: current.mode,
|
|
86
|
+
point: current.point,
|
|
87
|
+
targetRect: current.targetRect,
|
|
88
|
+
}),
|
|
89
|
+
targetRect: current.targetRect,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { MenuProps } from "../menu/menu";
|
|
3
|
+
import { MenuRootProvider, useMenuRootValue } from "../menu/menu.context";
|
|
4
|
+
import { MenuBackground } from "../menu/menu-background";
|
|
5
|
+
import { MenuCheckboxItem } from "../menu/menu-checkbox-item";
|
|
6
|
+
import { MenuLabel } from "../menu/menu-label";
|
|
7
|
+
import { MenuRadioGroup } from "../menu/menu-radio-group";
|
|
8
|
+
import { MenuRadioItem } from "../menu/menu-radio-item";
|
|
9
|
+
import { MenuSeparator } from "../menu/menu-separator";
|
|
10
|
+
import { MenuSub } from "../menu/menu-sub";
|
|
11
|
+
import { MenuSubContent } from "../menu/menu-sub-content";
|
|
12
|
+
import { MenuSubTrigger } from "../menu/menu-sub-trigger";
|
|
13
|
+
import { ContextMenuProvider, useContextMenuRootValue } from "./context-menu.context";
|
|
14
|
+
import { ContextMenuContent } from "./context-menu-content";
|
|
15
|
+
import { ContextMenuItem } from "./context-menu-item";
|
|
16
|
+
import { ContextMenuPreview } from "./context-menu-preview";
|
|
17
|
+
import { ContextMenuTrigger } from "./context-menu-trigger";
|
|
18
|
+
|
|
19
|
+
/** Menu's own root props: the open state, and the haptic played when a row is chosen. */
|
|
20
|
+
export type ContextMenuProps = MenuProps;
|
|
21
|
+
|
|
22
|
+
function ContextMenuRoot({ children, isOpen, defaultOpen, onOpenChange, haptic }: ContextMenuProps): ReactElement {
|
|
23
|
+
const menu = useMenuRootValue({ defaultOpen, haptic, isOpen, onOpenChange });
|
|
24
|
+
const context = useContextMenuRootValue();
|
|
25
|
+
return (
|
|
26
|
+
<MenuRootProvider value={menu}>
|
|
27
|
+
<ContextMenuProvider value={context}>{children}</ContextMenuProvider>
|
|
28
|
+
</MenuRootProvider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The actions that belong to a piece of content — a message, a card, a row —
|
|
34
|
+
* reached by holding the content itself.
|
|
35
|
+
*
|
|
36
|
+
* A `Menu` hangs off a control that exists to open it; a context menu has no
|
|
37
|
+
* such control, because the target is the content. Hold it for 350ms and the
|
|
38
|
+
* panel opens at the finger (or against the whole trigger with
|
|
39
|
+
* `anchor="target"`); a short press runs the trigger's `onPress`, and never both.
|
|
40
|
+
*
|
|
41
|
+
* **The rows and the panel are Menu's.** `ContextMenu.Content` is
|
|
42
|
+
* `Menu.Content` with a scrim and wider defaults, `ContextMenu.Item` is
|
|
43
|
+
* `Menu.Item` with a trailing icon and a taller row, and every other row kind is
|
|
44
|
+
* Menu's own component, so the two entry points cannot drift.
|
|
45
|
+
*
|
|
46
|
+
* `ContextMenu.Preview`, among the rows, lifts a copy of the held content over
|
|
47
|
+
* the scrim, and the panel opens beside it.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* <ContextMenu>
|
|
51
|
+
* <ContextMenu.Trigger haptic="medium" onPress={openThread}>
|
|
52
|
+
* <Card>…</Card>
|
|
53
|
+
* </ContextMenu.Trigger>
|
|
54
|
+
* <ContextMenu.Content>
|
|
55
|
+
* <ContextMenu.Preview />
|
|
56
|
+
* <ContextMenu.Item icon={IconShare} onSelect={share}>Share</ContextMenu.Item>
|
|
57
|
+
* <ContextMenu.Separator />
|
|
58
|
+
* <ContextMenu.Item icon={IconTrashCan} variant="destructive" onSelect={remove}>Delete</ContextMenu.Item>
|
|
59
|
+
* </ContextMenu.Content>
|
|
60
|
+
* </ContextMenu>
|
|
61
|
+
*/
|
|
62
|
+
export const ContextMenu = Object.assign(ContextMenuRoot, {
|
|
63
|
+
/** The content that is held. `onPress` is its short press, arbitrated against the hold. */
|
|
64
|
+
Trigger: ContextMenuTrigger,
|
|
65
|
+
/** The panel — `Menu.Content` over a scrim. */
|
|
66
|
+
Content: ContextMenuContent,
|
|
67
|
+
/** The held content, lifted over the scrim. Place it among the rows. */
|
|
68
|
+
Preview: ContextMenuPreview,
|
|
69
|
+
/** A verb: runs `onSelect` and closes. Icon trailing, row taller. */
|
|
70
|
+
Item: ContextMenuItem,
|
|
71
|
+
/** The panel's surface. Pass one as a `ContextMenu.Content` child to repaint it. */
|
|
72
|
+
Background: MenuBackground,
|
|
73
|
+
/** A section heading. */
|
|
74
|
+
Label: MenuLabel,
|
|
75
|
+
/** A row that toggles a setting. Stays open. */
|
|
76
|
+
CheckboxItem: MenuCheckboxItem,
|
|
77
|
+
/** Holds the value for a set of radio rows. */
|
|
78
|
+
RadioGroup: MenuRadioGroup,
|
|
79
|
+
/** One option in a radio group. Closes. */
|
|
80
|
+
RadioItem: MenuRadioItem,
|
|
81
|
+
/** A rule between groups of rows. */
|
|
82
|
+
Separator: MenuSeparator,
|
|
83
|
+
/** Nested rows that expand in place. */
|
|
84
|
+
Sub: MenuSub,
|
|
85
|
+
/** The row that expands a `ContextMenu.Sub`. */
|
|
86
|
+
SubTrigger: MenuSubTrigger,
|
|
87
|
+
/** The rows a `ContextMenu.Sub` discloses. */
|
|
88
|
+
SubContent: MenuSubContent,
|
|
89
|
+
displayName: "DelacourUI.ContextMenu",
|
|
90
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { MENU_DEFAULT_MIN_WIDTH, resolveMenuPlacement, resolveMenuWidth } from "../menu/menu.variants";
|
|
4
|
+
import {
|
|
5
|
+
CONTEXT_MENU_ACCESSIBILITY_ACTIONS,
|
|
6
|
+
CONTEXT_MENU_CONTENT_DEFAULTS,
|
|
7
|
+
CONTEXT_MENU_HOLD,
|
|
8
|
+
CONTEXT_MENU_PREVIEW_SCALE,
|
|
9
|
+
contextMenuVariants,
|
|
10
|
+
resolveContextAnchor,
|
|
11
|
+
resolveContextMenuAccessibility,
|
|
12
|
+
resolveHoldConfig,
|
|
13
|
+
} from "./context-menu.variants";
|
|
14
|
+
|
|
15
|
+
const TARGET = { height: 120, width: 300, x: 24, y: 200 };
|
|
16
|
+
const POINT = { x: 140, y: 260 };
|
|
17
|
+
|
|
18
|
+
describe("resolveContextAnchor", () => {
|
|
19
|
+
test("point mode, by the finger, is a zero-size rect at the press", () => {
|
|
20
|
+
expect(
|
|
21
|
+
resolveContextAnchor({
|
|
22
|
+
hasPreview: false,
|
|
23
|
+
invokedBy: "pointer",
|
|
24
|
+
mode: "point",
|
|
25
|
+
point: POINT,
|
|
26
|
+
targetRect: TARGET,
|
|
27
|
+
})
|
|
28
|
+
).toEqual({ height: 0, width: 0, x: 140, y: 260 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("target mode anchors to the measured wrapper", () => {
|
|
32
|
+
expect(
|
|
33
|
+
resolveContextAnchor({
|
|
34
|
+
hasPreview: false,
|
|
35
|
+
invokedBy: "pointer",
|
|
36
|
+
mode: "target",
|
|
37
|
+
point: POINT,
|
|
38
|
+
targetRect: TARGET,
|
|
39
|
+
})
|
|
40
|
+
).toEqual(TARGET);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a preview forces the target, so the panel opens outside the lifted copy", () => {
|
|
44
|
+
expect(
|
|
45
|
+
resolveContextAnchor({
|
|
46
|
+
hasPreview: true,
|
|
47
|
+
invokedBy: "pointer",
|
|
48
|
+
mode: "point",
|
|
49
|
+
point: POINT,
|
|
50
|
+
targetRect: TARGET,
|
|
51
|
+
})
|
|
52
|
+
).toEqual(TARGET);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a screen-reader open forces the target — there is no pointer coordinate", () => {
|
|
56
|
+
expect(
|
|
57
|
+
resolveContextAnchor({
|
|
58
|
+
hasPreview: false,
|
|
59
|
+
invokedBy: "accessibility",
|
|
60
|
+
mode: "point",
|
|
61
|
+
point: null,
|
|
62
|
+
targetRect: TARGET,
|
|
63
|
+
})
|
|
64
|
+
).toEqual(TARGET);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("point mode without a point falls back to the target", () => {
|
|
68
|
+
expect(
|
|
69
|
+
resolveContextAnchor({
|
|
70
|
+
hasPreview: false,
|
|
71
|
+
invokedBy: "pointer",
|
|
72
|
+
mode: "point",
|
|
73
|
+
point: null,
|
|
74
|
+
targetRect: TARGET,
|
|
75
|
+
})
|
|
76
|
+
).toEqual(TARGET);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("returns a fresh rect, never the caller's object", () => {
|
|
80
|
+
const result = resolveContextAnchor({
|
|
81
|
+
hasPreview: false,
|
|
82
|
+
invokedBy: "pointer",
|
|
83
|
+
mode: "target",
|
|
84
|
+
point: null,
|
|
85
|
+
targetRect: TARGET,
|
|
86
|
+
});
|
|
87
|
+
expect(result).not.toBe(TARGET);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("resolveHoldConfig", () => {
|
|
92
|
+
test("defaults to a 350ms hold and 12pt of drift", () => {
|
|
93
|
+
expect(resolveHoldConfig({})).toEqual({ delay: 350, slop: 12 });
|
|
94
|
+
expect(CONTEXT_MENU_HOLD).toEqual({ delay: 350, minDelay: 150, slop: 12 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("keeps values above the floors", () => {
|
|
98
|
+
expect(resolveHoldConfig({ delay: 600, slop: 20 })).toEqual({ delay: 600, slop: 20 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("floors the delay at 150ms, so a tap cannot open the menu", () => {
|
|
102
|
+
expect(resolveHoldConfig({ delay: 40 }).delay).toBe(150);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("floors the slop at zero", () => {
|
|
106
|
+
expect(resolveHoldConfig({ slop: -5 }).slop).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a non-finite value takes the default", () => {
|
|
110
|
+
expect(resolveHoldConfig({ delay: Number.NaN, slop: Number.POSITIVE_INFINITY })).toEqual({
|
|
111
|
+
delay: 350,
|
|
112
|
+
slop: 12,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("resolveContextMenuAccessibility", () => {
|
|
118
|
+
test("a trigger with onPress is a button", () => {
|
|
119
|
+
expect(resolveContextMenuAccessibility({ hasPress: true, isDisabled: false, isOpen: false }).role).toBe("button");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a trigger without onPress claims no role", () => {
|
|
123
|
+
expect(resolveContextMenuAccessibility({ hasPress: false, isDisabled: false, isOpen: false }).role).toBe("none");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("offers activate and a labelled long-press", () => {
|
|
127
|
+
expect(CONTEXT_MENU_ACCESSIBILITY_ACTIONS).toEqual([
|
|
128
|
+
{ name: "activate" },
|
|
129
|
+
{ label: "Show menu", name: "longpress" },
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("announces disabled and expanded", () => {
|
|
134
|
+
expect(resolveContextMenuAccessibility({ hasPress: true, isDisabled: true, isOpen: true }).state).toEqual({
|
|
135
|
+
disabled: true,
|
|
136
|
+
expanded: true,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("activate runs onPress when there is one, else opens", () => {
|
|
141
|
+
const withPress = resolveContextMenuAccessibility({ hasPress: true, isDisabled: false, isOpen: false });
|
|
142
|
+
const without = resolveContextMenuAccessibility({ hasPress: false, isDisabled: false, isOpen: false });
|
|
143
|
+
expect(withPress.onAction("activate")).toBe("press");
|
|
144
|
+
expect(without.onAction("activate")).toBe("open");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("longpress opens", () => {
|
|
148
|
+
const a11y = resolveContextMenuAccessibility({ hasPress: true, isDisabled: false, isOpen: false });
|
|
149
|
+
expect(a11y.onAction("longpress")).toBe("open");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("disabled does nothing, for either action", () => {
|
|
153
|
+
const a11y = resolveContextMenuAccessibility({ hasPress: true, isDisabled: true, isOpen: false });
|
|
154
|
+
expect(a11y.onAction("activate")).toBe("none");
|
|
155
|
+
expect(a11y.onAction("longpress")).toBe("none");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("an unknown action does nothing", () => {
|
|
159
|
+
const a11y = resolveContextMenuAccessibility({ hasPress: true, isDisabled: false, isOpen: false });
|
|
160
|
+
expect(a11y.onAction("magicTap")).toBe("none");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("content defaults", () => {
|
|
165
|
+
test("differ from Menu's: bottom, start, 8pt, at least 280 wide, with a scrim", () => {
|
|
166
|
+
expect(CONTEXT_MENU_CONTENT_DEFAULTS).toEqual({
|
|
167
|
+
align: "start",
|
|
168
|
+
hasScrim: true,
|
|
169
|
+
minWidth: 280,
|
|
170
|
+
offset: 8,
|
|
171
|
+
placement: "bottom",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a point anchor resolves to the 280 floor, not Menu's 200", () => {
|
|
176
|
+
const width = resolveMenuWidth({ minWidth: CONTEXT_MENU_CONTENT_DEFAULTS.minWidth, triggerWidth: 0 });
|
|
177
|
+
expect(width).toBe(280);
|
|
178
|
+
expect(width).toBeGreaterThan(MENU_DEFAULT_MIN_WIDTH);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a wide target keeps its own width", () => {
|
|
182
|
+
expect(resolveMenuWidth({ minWidth: CONTEXT_MENU_CONTENT_DEFAULTS.minWidth, triggerWidth: 340 })).toBe(340);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("the panel sits 8pt below a point", () => {
|
|
186
|
+
const result = resolveMenuPlacement({
|
|
187
|
+
align: CONTEXT_MENU_CONTENT_DEFAULTS.align,
|
|
188
|
+
anchor: { height: 0, width: 0, x: 140, y: 260 },
|
|
189
|
+
content: { height: 200, width: 280 },
|
|
190
|
+
insets: { bottom: 34, left: 0, right: 0, top: 47 },
|
|
191
|
+
isRTL: false,
|
|
192
|
+
offset: CONTEXT_MENU_CONTENT_DEFAULTS.offset,
|
|
193
|
+
placement: CONTEXT_MENU_CONTENT_DEFAULTS.placement,
|
|
194
|
+
window: { height: 800, width: 400 },
|
|
195
|
+
});
|
|
196
|
+
expect(result.placement).toBe("bottom");
|
|
197
|
+
expect(result.top).toBe(268);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a panel under a tall target flips above it, clear of the target", () => {
|
|
201
|
+
const target = { height: 300, width: 360, x: 20, y: 420 };
|
|
202
|
+
const result = resolveMenuPlacement({
|
|
203
|
+
align: CONTEXT_MENU_CONTENT_DEFAULTS.align,
|
|
204
|
+
anchor: target,
|
|
205
|
+
content: { height: 200, width: 360 },
|
|
206
|
+
insets: { bottom: 34, left: 0, right: 0, top: 47 },
|
|
207
|
+
isRTL: false,
|
|
208
|
+
offset: CONTEXT_MENU_CONTENT_DEFAULTS.offset,
|
|
209
|
+
placement: CONTEXT_MENU_CONTENT_DEFAULTS.placement,
|
|
210
|
+
window: { height: 800, width: 400 },
|
|
211
|
+
});
|
|
212
|
+
expect(result.placement).toBe("top");
|
|
213
|
+
expect(result.top + 200).toBeLessThanOrEqual(target.y - CONTEXT_MENU_CONTENT_DEFAULTS.offset);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("preview", () => {
|
|
218
|
+
test("lifts by 3%, which the 8pt offset clears for any target under ~530pt tall", () => {
|
|
219
|
+
expect(CONTEXT_MENU_PREVIEW_SCALE).toBe(1.03);
|
|
220
|
+
const grow = ((CONTEXT_MENU_PREVIEW_SCALE - 1) * 530) / 2;
|
|
221
|
+
expect(grow).toBeLessThan(CONTEXT_MENU_CONTENT_DEFAULTS.offset);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("contextMenuVariants", () => {
|
|
226
|
+
test("rows are taller than Menu's", () => {
|
|
227
|
+
expect(contextMenuVariants().item()).toContain("min-h-12");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("the trigger does not shrink to its child", () => {
|
|
231
|
+
expect(contextMenuVariants().trigger()).not.toMatch(/self-start|items-start/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("the preview is absolute and casts a shadow", () => {
|
|
235
|
+
const preview = contextMenuVariants().preview();
|
|
236
|
+
expect(preview).toContain("absolute");
|
|
237
|
+
expect(preview).toMatch(/shadow-/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("merges a caller's class", () => {
|
|
241
|
+
expect(contextMenuVariants().trigger({ className: "rounded-lg" })).toContain("rounded-lg");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("no slot carries a text colour", () => {
|
|
245
|
+
const slots = contextMenuVariants();
|
|
246
|
+
for (const name of ["trigger", "preview", "item"] as const) {
|
|
247
|
+
expect(slots[name]()).not.toMatch(/\btext-/);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("context menu colour tokens", () => {
|
|
253
|
+
const LIGHT = declaredTokens("light");
|
|
254
|
+
const DARK = declaredTokens("dark");
|
|
255
|
+
|
|
256
|
+
test("every token is declared in both themes", () => {
|
|
257
|
+
const slots = contextMenuVariants();
|
|
258
|
+
for (const name of ["trigger", "preview", "item"] as const) {
|
|
259
|
+
for (const [, token] of slots[name]().matchAll(/\b(?:bg|border)-([a-z][\w-]*)/g)) {
|
|
260
|
+
if (token === undefined) continue;
|
|
261
|
+
expect({ inLight: LIGHT.has(token), token }).toEqual({ inLight: true, token });
|
|
262
|
+
expect({ inDark: DARK.has(token), token }).toEqual({ inDark: true, token });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { MenuAlign, MenuAnchorRect, MenuPlacement } from "../menu/menu.variants";
|
|
4
|
+
|
|
5
|
+
export const CONTEXT_MENU_ANCHORS = ["point", "target"] as const;
|
|
6
|
+
|
|
7
|
+
/** What the panel hangs off: the finger's press point, or the held content's rect. */
|
|
8
|
+
export type ContextMenuAnchor = (typeof CONTEXT_MENU_ANCHORS)[number];
|
|
9
|
+
|
|
10
|
+
/** How the menu was asked for. A screen reader has no pointer coordinate. */
|
|
11
|
+
export type ContextMenuInvoker = "pointer" | "accessibility";
|
|
12
|
+
|
|
13
|
+
/** A press point in window coordinates. */
|
|
14
|
+
export type ContextMenuPoint = { x: number; y: number };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The hold: `delay` ms before it is accepted, `slop` pt the finger may drift
|
|
18
|
+
* meanwhile. `minDelay` is the floor — anything shorter is a tap.
|
|
19
|
+
*/
|
|
20
|
+
export const CONTEXT_MENU_HOLD = { delay: 350, minDelay: 150, slop: 12 } as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `ContextMenu.Content`'s defaults, where they differ from `Menu.Content`'s: a
|
|
24
|
+
* wider floor (a point anchor has no width to inherit), a little more gap from
|
|
25
|
+
* the finger, and a scrim — the content behind is not the menu's control.
|
|
26
|
+
*/
|
|
27
|
+
export const CONTEXT_MENU_CONTENT_DEFAULTS: {
|
|
28
|
+
placement: MenuPlacement;
|
|
29
|
+
align: MenuAlign;
|
|
30
|
+
offset: number;
|
|
31
|
+
minWidth: number;
|
|
32
|
+
hasScrim: boolean;
|
|
33
|
+
} = { align: "start", hasScrim: true, minWidth: 280, offset: 8, placement: "bottom" };
|
|
34
|
+
|
|
35
|
+
/** How far the lifted preview grows, 1 → this. */
|
|
36
|
+
export const CONTEXT_MENU_PREVIEW_SCALE = 1.03;
|
|
37
|
+
|
|
38
|
+
/** How long the preview takes to lift, in ms — Menu's enter, so the two arrive together. */
|
|
39
|
+
export const CONTEXT_MENU_PREVIEW_MS = 160;
|
|
40
|
+
|
|
41
|
+
/** The label a screen reader reads for the long-press action. */
|
|
42
|
+
export const CONTEXT_MENU_SHOW_LABEL = "Show menu";
|
|
43
|
+
|
|
44
|
+
export const CONTEXT_MENU_ACCESSIBILITY_ACTIONS: ReadonlyArray<{ name: string; label?: string }> = [
|
|
45
|
+
{ name: "activate" },
|
|
46
|
+
{ label: CONTEXT_MENU_SHOW_LABEL, name: "longpress" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export type ContextAnchorInput = {
|
|
50
|
+
mode: ContextMenuAnchor;
|
|
51
|
+
/** The press point, or `null` when there was none — a screen-reader open. */
|
|
52
|
+
point: ContextMenuPoint | null;
|
|
53
|
+
/** The trigger's measured window rect. */
|
|
54
|
+
targetRect: MenuAnchorRect;
|
|
55
|
+
hasPreview: boolean;
|
|
56
|
+
invokedBy: ContextMenuInvoker;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The rect the panel is placed against.
|
|
61
|
+
*
|
|
62
|
+
* `point` mode gives a zero-size rect at the press, which Menu's placement
|
|
63
|
+
* resolver handles with no special case. Three things force the target instead:
|
|
64
|
+
* `target` mode; a `ContextMenu.Preview`, so the panel opens outside the lifted
|
|
65
|
+
* copy rather than across it; and a screen-reader open, which has no point.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveContextAnchor({
|
|
68
|
+
mode,
|
|
69
|
+
point,
|
|
70
|
+
targetRect,
|
|
71
|
+
hasPreview,
|
|
72
|
+
invokedBy,
|
|
73
|
+
}: ContextAnchorInput): MenuAnchorRect {
|
|
74
|
+
const isTarget = mode === "target" || hasPreview || invokedBy === "accessibility" || point === null;
|
|
75
|
+
if (isTarget) return { height: targetRect.height, width: targetRect.width, x: targetRect.x, y: targetRect.y };
|
|
76
|
+
return { height: 0, width: 0, x: point.x, y: point.y };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The hold's timing: defaults of 350ms and 12pt, a 150ms floor on the delay so
|
|
81
|
+
* a tap can never open the menu, and a zero floor on the slop. A non-finite
|
|
82
|
+
* value takes the default.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveHoldConfig({ delay, slop }: { delay?: number; slop?: number }): {
|
|
85
|
+
delay: number;
|
|
86
|
+
slop: number;
|
|
87
|
+
} {
|
|
88
|
+
const d = delay !== undefined && Number.isFinite(delay) ? delay : CONTEXT_MENU_HOLD.delay;
|
|
89
|
+
const s = slop !== undefined && Number.isFinite(slop) ? slop : CONTEXT_MENU_HOLD.slop;
|
|
90
|
+
return { delay: Math.max(CONTEXT_MENU_HOLD.minDelay, d), slop: Math.max(0, s) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What an accessibility action resolves to. */
|
|
94
|
+
export type ContextMenuActionOutcome = "press" | "open" | "none";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The trigger's accessibility: a `button` when it has `onPress` (activating it
|
|
98
|
+
* does something of its own), otherwise no role — the content inside speaks for
|
|
99
|
+
* itself. `activate` runs `onPress`, or opens when there is none; `longpress`
|
|
100
|
+
* ("Show menu") opens. Disabled, neither does anything.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveContextMenuAccessibility({
|
|
103
|
+
hasPress,
|
|
104
|
+
isDisabled,
|
|
105
|
+
isOpen,
|
|
106
|
+
}: {
|
|
107
|
+
hasPress: boolean;
|
|
108
|
+
isDisabled: boolean;
|
|
109
|
+
isOpen: boolean;
|
|
110
|
+
}): {
|
|
111
|
+
role: "button" | "none";
|
|
112
|
+
state: { disabled: boolean; expanded: boolean };
|
|
113
|
+
onAction: (name: string) => ContextMenuActionOutcome;
|
|
114
|
+
} {
|
|
115
|
+
return {
|
|
116
|
+
onAction: (name) => {
|
|
117
|
+
if (isDisabled) return "none";
|
|
118
|
+
if (name === "activate") return hasPress ? "press" : "open";
|
|
119
|
+
if (name === "longpress") return "open";
|
|
120
|
+
return "none";
|
|
121
|
+
},
|
|
122
|
+
role: hasPress ? "button" : "none",
|
|
123
|
+
state: { disabled: isDisabled, expanded: isOpen },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Styling for ContextMenu's own parts. The rows and the panel are Menu's, and
|
|
129
|
+
* wear `menuVariants`; this holds only what differs.
|
|
130
|
+
*
|
|
131
|
+
* - `trigger` lays out like a plain `View` — it neither shrinks to its child nor
|
|
132
|
+
* adds anything to it.
|
|
133
|
+
* - `preview` is the lifted copy, absolutely placed at the trigger's window rect.
|
|
134
|
+
* - `item` makes a row taller: content menus are reached by a held finger, and a
|
|
135
|
+
* 48pt row is easier to land after the lift.
|
|
136
|
+
*
|
|
137
|
+
* Free of React Native imports so it stays reachable from `bun test`.
|
|
138
|
+
*/
|
|
139
|
+
export const contextMenuVariants = tv({
|
|
140
|
+
slots: {
|
|
141
|
+
trigger: "relative",
|
|
142
|
+
preview: "absolute shadow-lg",
|
|
143
|
+
item: "min-h-12",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export type ContextMenuVariantProps = VariantProps<typeof contextMenuVariants>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { ContextMenu, type ContextMenuProps } from "./context-menu";
|
|
2
|
+
export {
|
|
3
|
+
CONTEXT_MENU_ANCHORS,
|
|
4
|
+
CONTEXT_MENU_CONTENT_DEFAULTS,
|
|
5
|
+
CONTEXT_MENU_HOLD,
|
|
6
|
+
type ContextAnchorInput,
|
|
7
|
+
type ContextMenuAnchor,
|
|
8
|
+
type ContextMenuInvoker,
|
|
9
|
+
type ContextMenuPoint,
|
|
10
|
+
type ContextMenuVariantProps,
|
|
11
|
+
contextMenuVariants,
|
|
12
|
+
resolveContextAnchor,
|
|
13
|
+
resolveHoldConfig,
|
|
14
|
+
} from "./context-menu.variants";
|
|
15
|
+
export type { ContextMenuContentProps } from "./context-menu-content";
|
|
16
|
+
export type { ContextMenuItemProps } from "./context-menu-item";
|
|
17
|
+
export type { ContextMenuPreviewProps } from "./context-menu-preview";
|
|
18
|
+
export type { ContextMenuTriggerProps } from "./context-menu-trigger";
|