@fairgarden/design 0.1.0-alpha.1 → 0.1.0-alpha.2
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/LICENSE +21 -0
- package/Readme.md +51 -27
- package/actions/button/Button.d.ts.map +1 -1
- package/actions/button/Button.js +13 -1
- package/actions/button/Button.js.map +1 -1
- package/actions/button/button.module.css +85 -25
- package/actions/chip/Chip.d.ts +2 -2
- package/actions/chip/Chip.js +2 -2
- package/actions/chip/chip.module.css +12 -18
- package/actions/link/Link.d.ts +1 -1
- package/actions/link/link.module.css +22 -19
- package/actions/toggle/Toggle.js +1 -1
- package/actions/toggle/toggle.module.css +27 -23
- package/content/card/card.module.css +27 -27
- package/content/card-grid/CardGrid.js +1 -1
- package/content/card-grid/card-grid.module.css +3 -3
- package/content/carousel/Carousel.d.ts.map +1 -1
- package/content/carousel/Carousel.js +1 -4
- package/content/carousel/Carousel.js.map +1 -1
- package/content/carousel/carousel.module.css +19 -9
- package/content/code-block/CodeActionsMenu.d.ts +34 -0
- package/content/code-block/CodeActionsMenu.d.ts.map +1 -0
- package/content/code-block/CodeActionsMenu.js +102 -0
- package/content/code-block/CodeActionsMenu.js.map +1 -0
- package/content/code-block/CodeBlock.d.ts +37 -0
- package/content/code-block/CodeBlock.d.ts.map +1 -0
- package/content/code-block/CodeBlock.js +30 -0
- package/content/code-block/CodeBlock.js.map +1 -0
- package/content/code-block/CodeBlockFrame.d.ts +54 -0
- package/content/code-block/CodeBlockFrame.d.ts.map +1 -0
- package/content/code-block/CodeBlockFrame.js +80 -0
- package/content/code-block/CodeBlockFrame.js.map +1 -0
- package/content/code-block/CodeBlockHeader.d.ts +26 -0
- package/content/code-block/CodeBlockHeader.d.ts.map +1 -0
- package/content/code-block/CodeBlockHeader.js +16 -0
- package/content/code-block/CodeBlockHeader.js.map +1 -0
- package/content/code-block/CodeBlockLazy.d.ts +11 -0
- package/content/code-block/CodeBlockLazy.d.ts.map +1 -0
- package/content/code-block/CodeBlockLazy.js +14 -0
- package/content/code-block/CodeBlockLazy.js.map +1 -0
- package/content/code-block/CodeBlockLoading.d.ts +31 -0
- package/content/code-block/CodeBlockLoading.d.ts.map +1 -0
- package/content/code-block/CodeBlockLoading.js +90 -0
- package/content/code-block/CodeBlockLoading.js.map +1 -0
- package/content/code-block/CodeBlockSection.d.ts +40 -0
- package/content/code-block/CodeBlockSection.d.ts.map +1 -0
- package/content/code-block/CodeBlockSection.js +105 -0
- package/content/code-block/CodeBlockSection.js.map +1 -0
- package/content/code-block/CodeBlockToasts.d.ts +26 -0
- package/content/code-block/CodeBlockToasts.d.ts.map +1 -0
- package/content/code-block/CodeBlockToasts.js +88 -0
- package/content/code-block/CodeBlockToasts.js.map +1 -0
- package/content/code-block/code-block.module.css +949 -0
- package/content/code-block/index.d.ts +4 -0
- package/content/code-block/index.d.ts.map +1 -0
- package/content/code-block/index.js +4 -0
- package/content/code-block/index.js.map +1 -0
- package/content/demo/DemoContent.d.ts +32 -0
- package/content/demo/DemoContent.d.ts.map +1 -0
- package/content/demo/DemoContent.js +87 -0
- package/content/demo/DemoContent.js.map +1 -0
- package/content/demo/DemoFrame.d.ts +6 -0
- package/content/demo/DemoFrame.d.ts.map +1 -0
- package/content/demo/DemoFrame.js +17 -0
- package/content/demo/DemoFrame.js.map +1 -0
- package/content/demo/DemoLazy.d.ts +11 -0
- package/content/demo/DemoLazy.d.ts.map +1 -0
- package/content/demo/DemoLazy.js +14 -0
- package/content/demo/DemoLazy.js.map +1 -0
- package/content/demo/DemoLoading.d.ts +23 -0
- package/content/demo/DemoLoading.d.ts.map +1 -0
- package/content/demo/DemoLoading.js +26 -0
- package/content/demo/DemoLoading.js.map +1 -0
- package/content/demo/demo.module.css +163 -0
- package/content/demo/index.d.ts +4 -0
- package/content/demo/index.d.ts.map +1 -0
- package/content/demo/index.js +4 -0
- package/content/demo/index.js.map +1 -0
- package/content/empty-state/EmptyState.d.ts +1 -1
- package/content/empty-state/EmptyState.js +1 -1
- package/content/empty-state/empty-state.module.css +11 -11
- package/content/feature-grid/feature-grid.module.css +22 -22
- package/content/index-rows/IndexRows.d.ts +2 -2
- package/content/index-rows/IndexRows.d.ts.map +1 -1
- package/content/index-rows/IndexRows.js +1 -1
- package/content/index-rows/index-rows.module.css +29 -29
- package/content/pricing/Pricing.d.ts +4 -4
- package/content/pricing/Pricing.d.ts.map +1 -1
- package/content/pricing/Pricing.js +4 -4
- package/content/pricing/Pricing.js.map +1 -1
- package/content/pricing/pricing.module.css +22 -22
- package/content/profile/Profile.d.ts +1 -1
- package/content/profile/Profile.js +2 -2
- package/content/profile/profile.module.css +13 -13
- package/content/quote/Quote.d.ts +1 -1
- package/content/quote/quote.module.css +9 -9
- package/content/timeline/Timeline.d.ts +1 -1
- package/content/timeline/Timeline.d.ts.map +1 -1
- package/content/timeline/Timeline.js +5 -5
- package/content/timeline/Timeline.js.map +1 -1
- package/content/timeline/timeline.module.css +12 -12
- package/content/types-table/TypePre.d.ts +11 -0
- package/content/types-table/TypePre.d.ts.map +1 -0
- package/content/types-table/TypePre.js +13 -0
- package/content/types-table/TypePre.js.map +1 -0
- package/content/types-table/TypesTable.d.ts +22 -0
- package/content/types-table/TypesTable.d.ts.map +1 -0
- package/content/types-table/TypesTable.js +124 -0
- package/content/types-table/TypesTable.js.map +1 -0
- package/content/types-table/index.d.ts +3 -0
- package/content/types-table/index.d.ts.map +1 -0
- package/content/types-table/index.js +3 -0
- package/content/types-table/index.js.map +1 -0
- package/content/types-table/types-table.module.css +160 -0
- package/data/chart/Chart.d.ts +8 -1
- package/data/chart/Chart.d.ts.map +1 -1
- package/data/chart/Chart.js +28 -15
- package/data/chart/Chart.js.map +1 -1
- package/data/chart/chart.module.css +20 -6
- package/data/figure/Figure.d.ts +1 -1
- package/data/figure/Figure.js +1 -1
- package/data/figure/figure.module.css +7 -7
- package/data/map/Map.d.ts +3 -3
- package/data/map/Map.d.ts.map +1 -1
- package/data/map/Map.js +10 -8
- package/data/map/Map.js.map +1 -1
- package/data/map/MapCanvas.d.ts.map +1 -1
- package/data/map/MapCanvas.js +5 -9
- package/data/map/MapCanvas.js.map +1 -1
- package/data/map/map.module.css +39 -14
- package/data/map/mapStyle.d.ts +2 -2
- package/data/map/mapStyle.d.ts.map +1 -1
- package/data/map/mapStyle.js +5 -5
- package/data/map/mapStyle.js.map +1 -1
- package/data/scroll-area/ScrollArea.d.ts.map +1 -1
- package/data/scroll-area/ScrollArea.js +8 -4
- package/data/scroll-area/ScrollArea.js.map +1 -1
- package/data/scroll-area/scroll-area.module.css +28 -9
- package/data/spec-grid/spec-grid.module.css +8 -8
- package/data/spec-list/spec-list.module.css +3 -3
- package/data/spec-sheet/SpecSheet.d.ts +2 -2
- package/data/spec-sheet/SpecSheet.js +1 -1
- package/data/spec-sheet/spec-sheet.module.css +9 -9
- package/data/stat/Stat.d.ts +1 -1
- package/data/stat/Stat.d.ts.map +1 -1
- package/data/stat/stat.module.css +4 -4
- package/data/table/Table.d.ts +6 -5
- package/data/table/Table.d.ts.map +1 -1
- package/data/table/Table.js.map +1 -1
- package/data/table/table.module.css +43 -46
- package/disclosure/accordion/Accordion.d.ts +1 -1
- package/disclosure/accordion/Accordion.js +1 -1
- package/disclosure/accordion/accordion.module.css +12 -12
- package/disclosure/collapsible/Collapsible.d.ts +4 -4
- package/disclosure/collapsible/Collapsible.js +4 -4
- package/disclosure/collapsible/collapsible.module.css +10 -10
- package/disclosure/faq/FAQ.d.ts +3 -3
- package/disclosure/faq/FAQ.d.ts.map +1 -1
- package/disclosure/faq/FAQ.js +3 -3
- package/disclosure/faq/FAQ.js.map +1 -1
- package/disclosure/faq/faq.module.css +20 -20
- package/feedback/alert/alert.module.css +4 -4
- package/feedback/avatar/avatar-group.module.css +2 -2
- package/feedback/avatar/avatar.module.css +9 -9
- package/feedback/badge/Badge.d.ts +1 -1
- package/feedback/badge/badge.module.css +7 -7
- package/feedback/meter/Meter.d.ts +2 -2
- package/feedback/meter/meter.module.css +14 -14
- package/feedback/progress/Progress.d.ts +1 -1
- package/feedback/progress/progress.module.css +7 -7
- package/feedback/tag/tag.module.css +7 -7
- package/feedback/toast/Toast.d.ts +3 -3
- package/feedback/toast/Toast.d.ts.map +1 -1
- package/feedback/toast/Toast.js +17 -7
- package/feedback/toast/Toast.js.map +1 -1
- package/feedback/toast/toast.module.css +31 -20
- package/forms/autocomplete/Autocomplete.d.ts +11 -0
- package/forms/autocomplete/Autocomplete.d.ts.map +1 -1
- package/forms/autocomplete/Autocomplete.js +27 -6
- package/forms/autocomplete/Autocomplete.js.map +1 -1
- package/forms/autocomplete/autocomplete.module.css +35 -41
- package/forms/checkbox/Checkbox.js +1 -1
- package/forms/checkbox/checkbox.module.css +29 -25
- package/forms/checkbox-group/checkbox-group.module.css +4 -4
- package/forms/combobox/Combobox.d.ts +10 -0
- package/forms/combobox/Combobox.d.ts.map +1 -1
- package/forms/combobox/Combobox.js +28 -7
- package/forms/combobox/Combobox.js.map +1 -1
- package/forms/combobox/combobox.module.css +34 -41
- package/forms/field/field.module.css +1 -1
- package/forms/fieldset/Fieldset.d.ts +1 -1
- package/forms/fieldset/Fieldset.js +2 -2
- package/forms/fieldset/fieldset.module.css +14 -14
- package/forms/form/form.module.css +2 -2
- package/forms/input/Input.d.ts +10 -3
- package/forms/input/Input.d.ts.map +1 -1
- package/forms/input/Input.js +10 -4
- package/forms/input/Input.js.map +1 -1
- package/forms/input/input.module.css +38 -21
- package/forms/number-field/number-field.module.css +24 -23
- package/forms/radio/Radio.d.ts +1 -1
- package/forms/radio/Radio.d.ts.map +1 -1
- package/forms/radio/Radio.js +5 -3
- package/forms/radio/Radio.js.map +1 -1
- package/forms/radio/radio.module.css +38 -30
- package/forms/radio-group/RadioGroup.d.ts +1 -1
- package/forms/radio-group/radio-group.module.css +3 -3
- package/forms/search/Search.d.ts +10 -0
- package/forms/search/Search.d.ts.map +1 -1
- package/forms/search/Search.js +25 -6
- package/forms/search/Search.js.map +1 -1
- package/forms/search/search.module.css +25 -33
- package/forms/select/Select.d.ts +10 -2
- package/forms/select/Select.d.ts.map +1 -1
- package/forms/select/Select.js +27 -12
- package/forms/select/Select.js.map +1 -1
- package/forms/select/select.module.css +42 -48
- package/forms/slider/Slider.js +2 -2
- package/forms/slider/slider.module.css +23 -23
- package/forms/switch/Switch.js +1 -1
- package/forms/switch/switch.module.css +23 -19
- package/foundations/expanding-box/ExpandingBox.d.ts +86 -0
- package/foundations/expanding-box/ExpandingBox.d.ts.map +1 -0
- package/foundations/expanding-box/ExpandingBox.js +179 -0
- package/foundations/expanding-box/ExpandingBox.js.map +1 -0
- package/foundations/expanding-box/expanding-box.module.css +415 -0
- package/foundations/expanding-box/index.d.ts +3 -0
- package/foundations/expanding-box/index.d.ts.map +1 -0
- package/foundations/expanding-box/index.js +3 -0
- package/foundations/expanding-box/index.js.map +1 -0
- package/foundations/expanding-box/startExpandingTransition.d.ts +33 -0
- package/foundations/expanding-box/startExpandingTransition.d.ts.map +1 -0
- package/foundations/expanding-box/startExpandingTransition.js +82 -0
- package/foundations/expanding-box/startExpandingTransition.js.map +1 -0
- package/foundations/ground/Ground.d.ts +1 -1
- package/foundations/ground/ground.module.css +4 -4
- package/foundations/icon/Icon.d.ts +1 -1
- package/foundations/icon/Icon.js +1 -1
- package/foundations/icon/icon.module.css +1 -1
- package/foundations/outline-morph/OutlineMorph.d.ts +112 -0
- package/foundations/outline-morph/OutlineMorph.d.ts.map +1 -0
- package/foundations/outline-morph/OutlineMorph.js +83 -0
- package/foundations/outline-morph/OutlineMorph.js.map +1 -0
- package/foundations/outline-morph/index.d.ts +3 -0
- package/foundations/outline-morph/index.d.ts.map +1 -0
- package/foundations/outline-morph/index.js +3 -0
- package/foundations/outline-morph/index.js.map +1 -0
- package/foundations/outline-morph/morphEngine.d.ts +219 -0
- package/foundations/outline-morph/morphEngine.d.ts.map +1 -0
- package/foundations/outline-morph/morphEngine.js +1331 -0
- package/foundations/outline-morph/morphEngine.js.map +1 -0
- package/foundations/outline-morph/outline-morph.module.css +113 -0
- package/foundations/outline-morph/useOutlineMorphRoot.d.ts +50 -0
- package/foundations/outline-morph/useOutlineMorphRoot.d.ts.map +1 -0
- package/foundations/outline-morph/useOutlineMorphRoot.js +55 -0
- package/foundations/outline-morph/useOutlineMorphRoot.js.map +1 -0
- package/foundations/separator/separator.module.css +10 -10
- package/foundations/sticker/Sticker.d.ts +4 -4
- package/foundations/sticker/Sticker.d.ts.map +1 -1
- package/foundations/sticker/Sticker.js +3 -3
- package/foundations/sticker/sticker.module.css +10 -10
- package/icons/paths.d.ts +1 -1
- package/icons/paths.d.ts.map +1 -1
- package/icons/paths.js +50 -0
- package/icons/paths.js.map +1 -1
- package/navigation/breadcrumb/Breadcrumb.d.ts +7 -1
- package/navigation/breadcrumb/Breadcrumb.d.ts.map +1 -1
- package/navigation/breadcrumb/Breadcrumb.js +8 -8
- package/navigation/breadcrumb/Breadcrumb.js.map +1 -1
- package/navigation/breadcrumb/breadcrumb.module.css +14 -14
- package/navigation/file-tabs/FileTabs.d.ts +169 -0
- package/navigation/file-tabs/FileTabs.d.ts.map +1 -0
- package/navigation/file-tabs/FileTabs.js +253 -0
- package/navigation/file-tabs/FileTabs.js.map +1 -0
- package/navigation/file-tabs/file-tabs.module.css +868 -0
- package/navigation/file-tabs/index.d.ts +2 -0
- package/navigation/file-tabs/index.d.ts.map +1 -0
- package/navigation/file-tabs/index.js +2 -0
- package/navigation/file-tabs/index.js.map +1 -0
- package/navigation/nav-drawer/NavDrawer.d.ts +32 -6
- package/navigation/nav-drawer/NavDrawer.d.ts.map +1 -1
- package/navigation/nav-drawer/NavDrawer.js +94 -28
- package/navigation/nav-drawer/NavDrawer.js.map +1 -1
- package/navigation/nav-drawer/nav-drawer.module.css +78 -55
- package/navigation/navigation-bar/NavigationBar.d.ts +31 -11
- package/navigation/navigation-bar/NavigationBar.d.ts.map +1 -1
- package/navigation/navigation-bar/NavigationBar.js +40 -17
- package/navigation/navigation-bar/NavigationBar.js.map +1 -1
- package/navigation/navigation-bar/navigation-bar.module.css +58 -27
- package/navigation/navigation-menu/NavigationMenu.d.ts +9 -9
- package/navigation/navigation-menu/NavigationMenu.d.ts.map +1 -1
- package/navigation/navigation-menu/NavigationMenu.js +15 -15
- package/navigation/navigation-menu/navigation-menu.module.css +39 -53
- package/navigation/pagination/Pagination.js +1 -1
- package/navigation/pagination/pagination.module.css +24 -24
- package/navigation/sidebar-nav/SidebarNav.d.ts +71 -0
- package/navigation/sidebar-nav/SidebarNav.d.ts.map +1 -0
- package/navigation/sidebar-nav/SidebarNav.js +169 -0
- package/navigation/sidebar-nav/SidebarNav.js.map +1 -0
- package/navigation/sidebar-nav/index.d.ts +2 -0
- package/navigation/sidebar-nav/index.d.ts.map +1 -0
- package/navigation/sidebar-nav/index.js +2 -0
- package/navigation/sidebar-nav/index.js.map +1 -0
- package/navigation/sidebar-nav/sidebar-nav.module.css +270 -0
- package/navigation/table-of-contents/TableOfContents.d.ts +92 -0
- package/navigation/table-of-contents/TableOfContents.d.ts.map +1 -0
- package/navigation/table-of-contents/TableOfContents.js +211 -0
- package/navigation/table-of-contents/TableOfContents.js.map +1 -0
- package/navigation/table-of-contents/index.d.ts +3 -0
- package/navigation/table-of-contents/index.d.ts.map +1 -0
- package/navigation/table-of-contents/index.js +3 -0
- package/navigation/table-of-contents/index.js.map +1 -0
- package/navigation/table-of-contents/table-of-contents.module.css +322 -0
- package/navigation/table-of-contents/useActiveHeading.d.ts +37 -0
- package/navigation/table-of-contents/useActiveHeading.d.ts.map +1 -0
- package/navigation/table-of-contents/useActiveHeading.js +111 -0
- package/navigation/table-of-contents/useActiveHeading.js.map +1 -0
- package/navigation/tabs/Tabs.d.ts +2 -2
- package/navigation/tabs/Tabs.js +3 -3
- package/navigation/tabs/tabs.module.css +22 -19
- package/navigation/toolbar/Toolbar.d.ts +7 -1
- package/navigation/toolbar/Toolbar.d.ts.map +1 -1
- package/navigation/toolbar/Toolbar.js +3 -3
- package/navigation/toolbar/Toolbar.js.map +1 -1
- package/navigation/toolbar/toolbar.module.css +6 -6
- package/overlays/alert-dialog/AlertDialog.d.ts +14 -4
- package/overlays/alert-dialog/AlertDialog.d.ts.map +1 -1
- package/overlays/alert-dialog/AlertDialog.js +15 -5
- package/overlays/alert-dialog/AlertDialog.js.map +1 -1
- package/overlays/alert-dialog/alert-dialog.module.css +4 -4
- package/overlays/context-menu/ContextMenu.d.ts +1 -1
- package/overlays/context-menu/ContextMenu.js +1 -1
- package/overlays/dialog/Dialog.d.ts +26 -11
- package/overlays/dialog/Dialog.d.ts.map +1 -1
- package/overlays/dialog/Dialog.js +36 -17
- package/overlays/dialog/Dialog.js.map +1 -1
- package/overlays/dialog/dialog.module.css +32 -31
- package/overlays/dialog/morph.d.ts +17 -0
- package/overlays/dialog/morph.d.ts.map +1 -0
- package/overlays/dialog/morph.js +44 -0
- package/overlays/dialog/morph.js.map +1 -0
- package/overlays/menu/Menu.d.ts +18 -7
- package/overlays/menu/Menu.d.ts.map +1 -1
- package/overlays/menu/Menu.js +35 -12
- package/overlays/menu/Menu.js.map +1 -1
- package/overlays/menu/menu.module.css +26 -50
- package/overlays/menu/morph.d.ts +16 -0
- package/overlays/menu/morph.d.ts.map +1 -0
- package/overlays/menu/morph.js +36 -0
- package/overlays/menu/morph.js.map +1 -0
- package/overlays/menu/popup.d.ts +2 -0
- package/overlays/menu/popup.d.ts.map +1 -1
- package/overlays/menu/popup.js +11 -3
- package/overlays/menu/popup.js.map +1 -1
- package/overlays/menubar/Menubar.d.ts +10 -1
- package/overlays/menubar/Menubar.d.ts.map +1 -1
- package/overlays/menubar/Menubar.js +22 -12
- package/overlays/menubar/Menubar.js.map +1 -1
- package/overlays/menubar/menubar.module.css +13 -13
- package/overlays/popover/Popover.d.ts +22 -8
- package/overlays/popover/Popover.d.ts.map +1 -1
- package/overlays/popover/Popover.js +51 -16
- package/overlays/popover/Popover.js.map +1 -1
- package/overlays/popover/popover.module.css +73 -47
- package/overlays/preview-card/PreviewCard.d.ts +3 -3
- package/overlays/preview-card/PreviewCard.d.ts.map +1 -1
- package/overlays/preview-card/PreviewCard.js +5 -6
- package/overlays/preview-card/PreviewCard.js.map +1 -1
- package/overlays/preview-card/preview-card.module.css +19 -54
- package/overlays/search-dialog/SearchDialog.d.ts +89 -0
- package/overlays/search-dialog/SearchDialog.d.ts.map +1 -0
- package/overlays/search-dialog/SearchDialog.js +220 -0
- package/overlays/search-dialog/SearchDialog.js.map +1 -0
- package/overlays/search-dialog/SearchDialogPopup.d.ts +39 -0
- package/overlays/search-dialog/SearchDialogPopup.d.ts.map +1 -0
- package/overlays/search-dialog/SearchDialogPopup.js +80 -0
- package/overlays/search-dialog/SearchDialogPopup.js.map +1 -0
- package/overlays/search-dialog/SearchDialogTrigger.d.ts +24 -0
- package/overlays/search-dialog/SearchDialogTrigger.d.ts.map +1 -0
- package/overlays/search-dialog/SearchDialogTrigger.js +32 -0
- package/overlays/search-dialog/SearchDialogTrigger.js.map +1 -0
- package/overlays/search-dialog/index.d.ts +3 -0
- package/overlays/search-dialog/index.d.ts.map +1 -0
- package/overlays/search-dialog/index.js +3 -0
- package/overlays/search-dialog/index.js.map +1 -0
- package/overlays/search-dialog/search-dialog.module.css +492 -0
- package/overlays/search-dialog/useShortcutLabel.d.ts +13 -0
- package/overlays/search-dialog/useShortcutLabel.d.ts.map +1 -0
- package/overlays/search-dialog/useShortcutLabel.js +19 -0
- package/overlays/search-dialog/useShortcutLabel.js.map +1 -0
- package/overlays/search-dialog/variants.d.ts +25 -0
- package/overlays/search-dialog/variants.d.ts.map +1 -0
- package/overlays/search-dialog/variants.js +31 -0
- package/overlays/search-dialog/variants.js.map +1 -0
- package/overlays/tooltip/Tooltip.d.ts +3 -3
- package/overlays/tooltip/Tooltip.d.ts.map +1 -1
- package/overlays/tooltip/Tooltip.js +3 -3
- package/overlays/tooltip/Tooltip.js.map +1 -1
- package/overlays/tooltip/tooltip.module.css +2 -2
- package/package.json +27 -19
- package/page/announcement-bar/AnnouncementBar.d.ts +1 -1
- package/page/announcement-bar/AnnouncementBar.js +4 -4
- package/page/announcement-bar/AnnouncementBar.js.map +1 -1
- package/page/announcement-bar/announcement-bar.module.css +11 -17
- package/page/cta-block/CTABlock.d.ts +5 -5
- package/page/cta-block/CTABlock.d.ts.map +1 -1
- package/page/cta-block/CTABlock.js +2 -2
- package/page/cta-block/cta-block.module.css +26 -26
- package/page/docs-layout/DocsLayout.d.ts +83 -0
- package/page/docs-layout/DocsLayout.d.ts.map +1 -0
- package/page/docs-layout/DocsLayout.js +108 -0
- package/page/docs-layout/DocsLayout.js.map +1 -0
- package/page/docs-layout/docs-layout.module.css +266 -0
- package/page/docs-layout/index.d.ts +2 -0
- package/page/docs-layout/index.d.ts.map +1 -0
- package/page/docs-layout/index.js +2 -0
- package/page/docs-layout/index.js.map +1 -0
- package/page/footer/Footer.d.ts +1 -1
- package/page/footer/Footer.js +1 -1
- package/page/footer/footer.module.css +31 -31
- package/page/hero/Hero.d.ts +3 -3
- package/page/hero/Hero.d.ts.map +1 -1
- package/page/hero/Hero.js +1 -1
- package/page/hero/hero.module.css +36 -36
- package/page/marquee/Marquee.d.ts +2 -2
- package/page/marquee/Marquee.js +1 -1
- package/page/marquee/marquee.module.css +11 -11
- package/page/newsletter/Newsletter.d.ts.map +1 -1
- package/page/newsletter/Newsletter.js +4 -59
- package/page/newsletter/Newsletter.js.map +1 -1
- package/page/newsletter/newsletter.module.css +44 -22
- package/page/section-bar/SectionBar.d.ts +10 -3
- package/page/section-bar/SectionBar.d.ts.map +1 -1
- package/page/section-bar/SectionBar.js +21 -11
- package/page/section-bar/SectionBar.js.map +1 -1
- package/page/section-bar/section-bar.module.css +43 -46
- package/page/section-divider/SectionDivider.d.ts +2 -2
- package/page/section-divider/SectionDivider.js +4 -4
- package/page/section-divider/SectionDivider.js.map +1 -1
- package/page/section-divider/section-divider.module.css +18 -18
- package/page/section-header/SectionHeader.d.ts +7 -7
- package/page/section-header/SectionHeader.d.ts.map +1 -1
- package/page/section-header/SectionHeader.js +3 -3
- package/page/section-header/SectionHeader.js.map +1 -1
- package/page/section-header/section-header.module.css +16 -16
- package/utils/Ornament.d.ts +1 -1
- package/utils/Ornament.js +2 -2
- package/utils/assignRef.d.ts +7 -1
- package/utils/assignRef.d.ts.map +1 -1
- package/utils/assignRef.js +18 -0
- package/utils/assignRef.js.map +1 -1
- package/utils/docs/DemoTitle.d.ts +12 -0
- package/utils/docs/DemoTitle.d.ts.map +1 -0
- package/utils/docs/DemoTitle.js +11 -0
- package/utils/docs/DemoTitle.js.map +1 -0
- package/utils/docs/Pre.d.ts +39 -0
- package/utils/docs/Pre.d.ts.map +1 -0
- package/utils/docs/Pre.js +26 -0
- package/utils/docs/Pre.js.map +1 -0
- package/utils/docs/createDemo.d.ts +89 -0
- package/utils/docs/createDemo.d.ts.map +1 -0
- package/utils/docs/createDemo.js +51 -0
- package/utils/docs/createDemo.js.map +1 -0
- package/utils/docs/createMdxComponents.d.ts +52 -0
- package/utils/docs/createMdxComponents.d.ts.map +1 -0
- package/utils/docs/createMdxComponents.js +59 -0
- package/utils/docs/createMdxComponents.js.map +1 -0
- package/utils/docs/createTypes.d.ts +23 -0
- package/utils/docs/createTypes.d.ts.map +1 -0
- package/utils/docs/createTypes.js +27 -0
- package/utils/docs/createTypes.js.map +1 -0
- package/utils/docs/index.d.ts +8 -0
- package/utils/docs/index.d.ts.map +1 -0
- package/utils/docs/index.js +16 -0
- package/utils/docs/index.js.map +1 -0
- package/utils/docs/mdx.module.css +201 -0
- package/utils/docs/serverSourceEnhancers.d.ts +26 -0
- package/utils/docs/serverSourceEnhancers.d.ts.map +1 -0
- package/utils/docs/serverSourceEnhancers.js +32 -0
- package/utils/docs/serverSourceEnhancers.js.map +1 -0
- package/utils/docs/toSidebarItems.d.ts +23 -0
- package/utils/docs/toSidebarItems.d.ts.map +1 -0
- package/utils/docs/toSidebarItems.js +36 -0
- package/utils/docs/toSidebarItems.js.map +1 -0
- package/utils/font-fallbacks.css +176 -0
- package/utils/fonts.d.ts +1 -0
- package/utils/fonts.d.ts.map +1 -1
- package/utils/fonts.js +12 -0
- package/utils/fonts.js.map +1 -1
- package/utils/frame.d.ts +4 -0
- package/utils/frame.d.ts.map +1 -0
- package/utils/frame.js +11 -0
- package/utils/frame.js.map +1 -0
- package/utils/global.css +480 -266
- package/utils/line.module.css +17 -17
- package/utils/media.css +1 -1
- package/utils/ornament.module.css +19 -19
- package/utils/overlay.d.ts +4 -4
- package/utils/overlay.js +4 -4
- package/utils/pattern.module.css +45 -45
- package/utils/print.css +48 -34
- package/utils/reset.css +9 -2
- package/utils/roles.css +29 -29
- package/utils/seriesPatterns.js +3 -3
- package/utils/syntax.module.css +92 -0
- package/utils/theme.css +6 -6
- package/utils/tokens.css +262 -192
- package/utils/tokens.d.ts +51 -8
- package/utils/tokens.d.ts.map +1 -1
- package/utils/tokens.js +46 -9
- package/utils/tokens.js.map +1 -1
- package/utils/type.module.css +87 -87
- package/utils/useReleaseMark.d.ts +13 -0
- package/utils/useReleaseMark.d.ts.map +1 -0
- package/utils/useReleaseMark.js +44 -0
- package/utils/useReleaseMark.js.map +1 -0
- package/utils/view-transitions.css +122 -0
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
import { EXPAND_DURATION_MS, EXPAND_EASE, SURFACE_DURATION_MS } from '../../utils/tokens';
|
|
2
|
+
/*
|
|
3
|
+
* The outline morph's engine: plain DOM and the Web Animations API, no
|
|
4
|
+
* React. `useOutlineMorph` owns one controller per trigger and popup pair.
|
|
5
|
+
*
|
|
6
|
+
* Geometry. Every box is the outer edge of a frame: the focus ring (the
|
|
7
|
+
* trigger's border box grown by the outline's offset and width, each outer
|
|
8
|
+
* radius r + offset + width, as the browser draws a rounded outline), the
|
|
9
|
+
* trigger's edge (its border box and radii), or the popup's frame (its
|
|
10
|
+
* border box and radii). Radii are per corner and clamped as CSS clamps
|
|
11
|
+
* them, so a pill trigger starts as the pill it is and an ear with two
|
|
12
|
+
* square corners keeps them. The overlay frame is absolutely positioned
|
|
13
|
+
* in the popup's positioned container, so boxes are kept in that
|
|
14
|
+
* container's coordinates: the frame moves with the popup and never takes
|
|
15
|
+
* part in layout (a modal surface, which has no positioner, puts it in a
|
|
16
|
+
* fixed full-viewport layer). Its box, radii and edge color animate; it
|
|
17
|
+
* moves by a pure translation and resizes by width and height, never a
|
|
18
|
+
* scale, so the edge stays crisp. Its edge weight never tweens: where the
|
|
19
|
+
* start's weight and the popup's differ (a 2 px ring into a 3 px panel), it
|
|
20
|
+
* steps once, at WEIGHT_STEP of the eased travel, while the frame moves
|
|
21
|
+
* fastest. A popup side with no edge of its own that lies on the viewport's
|
|
22
|
+
* edge (a sheet) is landed on one weight past it, so that edge leaves the
|
|
23
|
+
* screen rather than popping at the hand-off. The popup is
|
|
24
|
+
* clipped to the same box (an `inset() round` clip on the same timing), so
|
|
25
|
+
* its contents appear inside the growing frame; nothing animates opacity.
|
|
26
|
+
* While a morph runs, the end it travels to is re-measured every frame, so
|
|
27
|
+
* a scroll, resize or late popup size change bends the path instead of
|
|
28
|
+
* leaving the frame behind; a close also re-reads which ring it lands on,
|
|
29
|
+
* so focus leaving the field mid-close lands on the field's edge.
|
|
30
|
+
*/
|
|
31
|
+
/** The attribute the popup carries while the overlay draws its frame. */
|
|
32
|
+
export const OUTLINE_MORPH_ATTRIBUTE = 'data-outline-morph';
|
|
33
|
+
/**
|
|
34
|
+
* The app-wide (and per-subtree) switch: `none` on the trigger or the popup
|
|
35
|
+
* (inherited, so `:root` turns every morph off) leaves every popup its own
|
|
36
|
+
* open and close. Read at each open and close.
|
|
37
|
+
*/
|
|
38
|
+
export const OUTLINE_MORPH_PROPERTY = '--fgd-outline-morph';
|
|
39
|
+
/**
|
|
40
|
+
* The popup's attribute while an open waits for its placement (a frame or
|
|
41
|
+
* two, at most PLACEMENT_FRAMES): the foundation's CSS holds the popup
|
|
42
|
+
* clipped away meanwhile (still focusable, as Base UI moves focus in during
|
|
43
|
+
* the wait), a discrete hold, so it never paints unclipped before the morph
|
|
44
|
+
* starts [D196]. A timeout removes it and the popup shows at once.
|
|
45
|
+
*/
|
|
46
|
+
export const OUTLINE_MORPH_PENDING_ATTRIBUTE = 'data-outline-morph-pending';
|
|
47
|
+
/** The overlay frame's attribute while it shows. */
|
|
48
|
+
const RUNNING_ATTRIBUTE = 'data-running';
|
|
49
|
+
/** The tail cap's placement and face, custom properties the engine sets on it (never real properties). */
|
|
50
|
+
const TAIL_X = '--fgd-outline-morph-tail-x';
|
|
51
|
+
const TAIL_Y = '--fgd-outline-morph-tail-y';
|
|
52
|
+
const TAIL_FILL = '--fgd-outline-morph-tail-fill';
|
|
53
|
+
/**
|
|
54
|
+
* The frame's offset in its container and its size, as registered
|
|
55
|
+
* `<length>` properties so the Web Animations API interpolates them; the
|
|
56
|
+
* frame's CSS rounds them to whole pixels for its `translate`, `width` and
|
|
57
|
+
* `height`, so the edge is never painted at a fractional position (which
|
|
58
|
+
* would soften it across three pixels) and the last frame paints the
|
|
59
|
+
* popup's own pixel-snapped edges, whatever its fractional size.
|
|
60
|
+
*/
|
|
61
|
+
const OFFSET_X = '--fgd-outline-morph-x';
|
|
62
|
+
const OFFSET_Y = '--fgd-outline-morph-y';
|
|
63
|
+
const SIZE_W = '--fgd-outline-morph-w';
|
|
64
|
+
const SIZE_H = '--fgd-outline-morph-h';
|
|
65
|
+
/**
|
|
66
|
+
* The frame's paint, also as registered properties (radii, edge color and
|
|
67
|
+
* weight), which the frame's CSS maps to `border-radius`, `border-color` and
|
|
68
|
+
* `border-width`, so the animation writes custom properties only.
|
|
69
|
+
*/
|
|
70
|
+
const RADII = ['--fgd-outline-morph-r1', '--fgd-outline-morph-r2', '--fgd-outline-morph-r3', '--fgd-outline-morph-r4'];
|
|
71
|
+
const EDGE_COLOR = '--fgd-outline-morph-color';
|
|
72
|
+
const EDGE_WEIGHT = '--fgd-outline-morph-weight';
|
|
73
|
+
let offsetMode;
|
|
74
|
+
/**
|
|
75
|
+
* Registers the offset and size properties once. Where registration or CSS
|
|
76
|
+
* `round()` is missing, the frame animates `translate`, `width` and
|
|
77
|
+
* `height` itself (fractional).
|
|
78
|
+
*/
|
|
79
|
+
function frameOffsetMode() {
|
|
80
|
+
if (offsetMode)
|
|
81
|
+
return offsetMode;
|
|
82
|
+
offsetMode = 'plain';
|
|
83
|
+
if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function')
|
|
84
|
+
return offsetMode;
|
|
85
|
+
if (!CSS.supports('translate', 'round(1.5px, 1px) round(1.5px, 1px)'))
|
|
86
|
+
return offsetMode;
|
|
87
|
+
const properties = [
|
|
88
|
+
...[OFFSET_X, OFFSET_Y, SIZE_W, SIZE_H, ...RADII, EDGE_WEIGHT].map((name) => [name, '<length>', '0px']),
|
|
89
|
+
[EDGE_COLOR, '<color>', 'transparent'],
|
|
90
|
+
];
|
|
91
|
+
for (const [name, syntax, initialValue] of properties) {
|
|
92
|
+
try {
|
|
93
|
+
CSS.registerProperty({ name, syntax, inherits: false, initialValue });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Already registered (another copy of the module, or a hot reload).
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
offsetMode = 'rounded';
|
|
100
|
+
return offsetMode;
|
|
101
|
+
}
|
|
102
|
+
/** A morph resumed from its current state never runs shorter than this share of its duration. */
|
|
103
|
+
const MIN_RESUME_SHARE = 0.35;
|
|
104
|
+
/** Frames to wait for the popup to be placed before letting it open its own way. */
|
|
105
|
+
const PLACEMENT_FRAMES = 8;
|
|
106
|
+
/** How long a landed close waits for the popup to hide before clearing the frame itself. */
|
|
107
|
+
const SETTLE_MS = 120;
|
|
108
|
+
/** Re-measured ends closer than this (px) leave the keyframes alone. */
|
|
109
|
+
const DRIFT_PX = 0.5;
|
|
110
|
+
/** How far inside the frame's outer edge the popup's clip runs (px); less than the edge weight. */
|
|
111
|
+
const CLIP_INSET_PX = 1;
|
|
112
|
+
/** An open's start may still move onto a ring drawn late while its progress is under this share. */
|
|
113
|
+
const EARLY_PROGRESS = 0.25;
|
|
114
|
+
/** Where on the eased travel (0–1) the frame's edge weight steps, when its two ends differ. */
|
|
115
|
+
const WEIGHT_STEP = 0.15;
|
|
116
|
+
function px(value) {
|
|
117
|
+
const number = parseFloat(value);
|
|
118
|
+
return Number.isFinite(number) ? number : 0;
|
|
119
|
+
}
|
|
120
|
+
function camel(property) {
|
|
121
|
+
return property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
122
|
+
}
|
|
123
|
+
function toRect(rect) {
|
|
124
|
+
return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
|
|
125
|
+
}
|
|
126
|
+
function drifted(a, b) {
|
|
127
|
+
return (Math.abs(a.x - b.x) > DRIFT_PX ||
|
|
128
|
+
Math.abs(a.y - b.y) > DRIFT_PX ||
|
|
129
|
+
Math.abs(a.w - b.w) > DRIFT_PX ||
|
|
130
|
+
Math.abs(a.h - b.h) > DRIFT_PX);
|
|
131
|
+
}
|
|
132
|
+
/** A computed color's alpha: 0 for `transparent`, else its `/ a` or `rgba()` alpha, else 1. */
|
|
133
|
+
function alphaOf(color) {
|
|
134
|
+
const value = color.trim();
|
|
135
|
+
if (value === '' || value === 'transparent')
|
|
136
|
+
return 0;
|
|
137
|
+
const slash = value.match(/\/\s*([\d.]+)(%?)\s*\)$/);
|
|
138
|
+
if (slash)
|
|
139
|
+
return slash[2] ? parseFloat(slash[1]) / 100 : parseFloat(slash[1]);
|
|
140
|
+
const rgba = value.match(/^rgba\((?:[^,]+,){3}\s*([\d.]+)\s*\)$/);
|
|
141
|
+
if (rgba)
|
|
142
|
+
return parseFloat(rgba[1]);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
/** One corner's horizontal radius from its computed value ("12px", "12px 6px" or "50%"). */
|
|
146
|
+
function cornerRadius(value, basis) {
|
|
147
|
+
const first = value.trim().split(/\s+/)[0] ?? '0';
|
|
148
|
+
return first.endsWith('%') ? (parseFloat(first) / 100) * basis : px(first);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Scales radii down as CSS does when adjacent corners overflow a side
|
|
152
|
+
* (CSS Backgrounds 3, corner overlap), so `--radius-round` reads as the
|
|
153
|
+
* pill's half-height, never 100000 px.
|
|
154
|
+
*/
|
|
155
|
+
function clampRadii(r, w, h) {
|
|
156
|
+
let factor = 1;
|
|
157
|
+
const sides = [
|
|
158
|
+
[r[0] + r[1], w],
|
|
159
|
+
[r[1] + r[2], h],
|
|
160
|
+
[r[2] + r[3], w],
|
|
161
|
+
[r[3] + r[0], h],
|
|
162
|
+
];
|
|
163
|
+
for (const [sum, length] of sides) {
|
|
164
|
+
if (sum > length && sum > 0)
|
|
165
|
+
factor = Math.min(factor, Math.max(0, length) / sum);
|
|
166
|
+
}
|
|
167
|
+
return factor < 1 ? r.map((value) => value * factor) : r;
|
|
168
|
+
}
|
|
169
|
+
/** An element's used corner radii for a box of w × h. */
|
|
170
|
+
function usedRadii(style, w, h) {
|
|
171
|
+
return clampRadii([
|
|
172
|
+
cornerRadius(style.borderTopLeftRadius, w),
|
|
173
|
+
cornerRadius(style.borderTopRightRadius, w),
|
|
174
|
+
cornerRadius(style.borderBottomRightRadius, w),
|
|
175
|
+
cornerRadius(style.borderBottomLeftRadius, w),
|
|
176
|
+
], w, h);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A property's value once its running CSS transition lands: the transition's
|
|
180
|
+
* end keyframe, else the computed value. The trigger's edge steps color on
|
|
181
|
+
* open and close, and the frame must meet the step's end, not its start.
|
|
182
|
+
*/
|
|
183
|
+
function settledValue(element, property) {
|
|
184
|
+
const current = getComputedStyle(element).getPropertyValue(property);
|
|
185
|
+
if (typeof CSSTransition === 'undefined')
|
|
186
|
+
return current;
|
|
187
|
+
for (const animation of element.getAnimations()) {
|
|
188
|
+
if (!(animation instanceof CSSTransition) || animation.transitionProperty !== property)
|
|
189
|
+
continue;
|
|
190
|
+
const frames = animation.effect?.getKeyframes() ?? [];
|
|
191
|
+
const end = frames[frames.length - 1]?.[camel(property)];
|
|
192
|
+
if (typeof end === 'string')
|
|
193
|
+
return end;
|
|
194
|
+
}
|
|
195
|
+
return current;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether the morph may run now: Web Animations, motion allowed, no forced
|
|
199
|
+
* colors, not printing. Read at each open, so a preference change applies
|
|
200
|
+
* to the next one.
|
|
201
|
+
*/
|
|
202
|
+
export function canOutlineMorph() {
|
|
203
|
+
if (typeof window === 'undefined' || typeof Element.prototype.animate !== 'function') {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if (typeof window.matchMedia !== 'function')
|
|
207
|
+
return true;
|
|
208
|
+
return !(window.matchMedia('(prefers-reduced-motion: reduce)').matches ||
|
|
209
|
+
window.matchMedia('(forced-colors: active)').matches ||
|
|
210
|
+
window.matchMedia('print').matches);
|
|
211
|
+
}
|
|
212
|
+
/** A `<time>` custom property's computed value in ms ("333ms", "0.2s", "0ms"), or null. */
|
|
213
|
+
function timeMs(value) {
|
|
214
|
+
const match = value.trim().match(/^(-?[\d.]+)(ms|s)$/);
|
|
215
|
+
if (!match)
|
|
216
|
+
return null;
|
|
217
|
+
const number = parseFloat(match[1]) * (match[2] === 's' ? 1000 : 1);
|
|
218
|
+
return Number.isFinite(number) ? Math.max(0, number) : null;
|
|
219
|
+
}
|
|
220
|
+
const easeChecked = new Map();
|
|
221
|
+
/** Whether the Web Animations API accepts the easing (a `linear()` token needs a recent engine). */
|
|
222
|
+
function easeValid(ease) {
|
|
223
|
+
let valid = easeChecked.get(ease);
|
|
224
|
+
if (valid === undefined) {
|
|
225
|
+
try {
|
|
226
|
+
document.createElement('div').animate([], { duration: 1, easing: ease }).cancel();
|
|
227
|
+
valid = true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
valid = false;
|
|
231
|
+
}
|
|
232
|
+
easeChecked.set(ease, valid);
|
|
233
|
+
}
|
|
234
|
+
return valid;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The morph's timing from the tokens as the trigger resolves them, read at
|
|
238
|
+
* each open and close: `--fgd-duration-expand`, `--fgd-duration-collapse`
|
|
239
|
+
* (else `--fgd-duration-disclosure`) and `--fgd-ease-expand`, so an app's
|
|
240
|
+
* override reaches the morph; a modal surface (`surface`) reads
|
|
241
|
+
* `--fgd-duration-expand-surface` and `--fgd-duration-collapse-surface`. The JS mirrors (utils/tokens.ts) are only the
|
|
242
|
+
* fallback for a value that can't be read; `override` (the hook's
|
|
243
|
+
* `duration` option) wins over the duration tokens.
|
|
244
|
+
*/
|
|
245
|
+
function readTiming(source, override, surface) {
|
|
246
|
+
const style = getComputedStyle(source);
|
|
247
|
+
const token = (name) => style.getPropertyValue(name);
|
|
248
|
+
const open = override?.open ??
|
|
249
|
+
(surface
|
|
250
|
+
? (timeMs(token('--fgd-duration-expand-surface')) ?? SURFACE_DURATION_MS.open)
|
|
251
|
+
: (timeMs(token('--fgd-duration-expand')) ?? EXPAND_DURATION_MS.open));
|
|
252
|
+
const close = override?.close ??
|
|
253
|
+
(surface
|
|
254
|
+
? (timeMs(token('--fgd-duration-collapse-surface')) ?? SURFACE_DURATION_MS.close)
|
|
255
|
+
: (timeMs(token('--fgd-duration-collapse')) ??
|
|
256
|
+
timeMs(token('--fgd-duration-disclosure')) ??
|
|
257
|
+
EXPAND_DURATION_MS.close));
|
|
258
|
+
const ease = token('--fgd-ease-expand').trim().replace(/\s+/g, ' ');
|
|
259
|
+
return { open, close, ease: ease && easeValid(ease) ? ease : EXPAND_EASE };
|
|
260
|
+
}
|
|
261
|
+
/** Whether `--fgd-outline-morph: none` applies to the element (inherited from any ancestor). */
|
|
262
|
+
function switchedOff(element) {
|
|
263
|
+
if (!element)
|
|
264
|
+
return false;
|
|
265
|
+
return getComputedStyle(element).getPropertyValue(OUTLINE_MORPH_PROPERTY).trim() === 'none';
|
|
266
|
+
}
|
|
267
|
+
/** Whether the trigger is rendered (a trigger hidden by `display: none` has no box to morph from). */
|
|
268
|
+
function rendered(element) {
|
|
269
|
+
return element.isConnected && element.getClientRects().length > 0;
|
|
270
|
+
}
|
|
271
|
+
/** Whether the element paints an outline now. */
|
|
272
|
+
function outlineShown(style) {
|
|
273
|
+
return style.outlineStyle !== 'none' && px(style.outlineWidth) > 0;
|
|
274
|
+
}
|
|
275
|
+
/** Whether the trigger draws its focus ring now, as its ring mode draws it. */
|
|
276
|
+
function ringDrawn(source, mode) {
|
|
277
|
+
const focused = mode === 'focus-within' ? source.matches(':focus-within') : source.matches(':focus-visible');
|
|
278
|
+
return focused && outlineShown(getComputedStyle(source));
|
|
279
|
+
}
|
|
280
|
+
/** Whether focus sits in the trigger or the popup's container (so it stays with, or comes back to, the trigger). */
|
|
281
|
+
function focusHeld(source, popup) {
|
|
282
|
+
const active = document.activeElement;
|
|
283
|
+
if (!active || active === document.body)
|
|
284
|
+
return false;
|
|
285
|
+
return source.contains(active) || popup.some((element) => element?.contains(active) ?? false);
|
|
286
|
+
}
|
|
287
|
+
/** A registered `<length>` the engine sets on a trigger for a moment to resolve its ring tokens there. */
|
|
288
|
+
const PROBE = '--fgd-outline-morph-probe';
|
|
289
|
+
let probeRegistered;
|
|
290
|
+
function probeReady() {
|
|
291
|
+
if (probeRegistered !== undefined)
|
|
292
|
+
return probeRegistered;
|
|
293
|
+
probeRegistered = false;
|
|
294
|
+
if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function')
|
|
295
|
+
return false;
|
|
296
|
+
try {
|
|
297
|
+
CSS.registerProperty({ name: PROBE, syntax: '<length>', inherits: false, initialValue: '0px' });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Already registered (another copy of the module, or a hot reload).
|
|
301
|
+
}
|
|
302
|
+
probeRegistered = true;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A length token as the element itself resolves it (so `1lh` and `em` are
|
|
307
|
+
* its own): set for a moment as a registered `<length>` on the element,
|
|
308
|
+
* whose computed value is absolute, then removed again (never a rewrite of
|
|
309
|
+
* the element's style attribute). Null where registration is missing.
|
|
310
|
+
*/
|
|
311
|
+
function resolveOn(element, value) {
|
|
312
|
+
if (!probeReady())
|
|
313
|
+
return null;
|
|
314
|
+
const hadStyle = element.hasAttribute('style');
|
|
315
|
+
element.style.setProperty(PROBE, value);
|
|
316
|
+
const resolved = getComputedStyle(element).getPropertyValue(PROBE);
|
|
317
|
+
// Only the engine's own property goes; an attribute the probe created goes with it.
|
|
318
|
+
element.style.removeProperty(PROBE);
|
|
319
|
+
if (!hadStyle && element.getAttribute('style') === '')
|
|
320
|
+
element.removeAttribute('style');
|
|
321
|
+
return resolved ? px(resolved) : null;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The focus ring's outer box, in viewport coordinates. Drawn: the
|
|
325
|
+
* trigger's computed outline (its color as its transition lands).
|
|
326
|
+
* Predicted (focus is elsewhere and returns with a ring): the system's ring
|
|
327
|
+
* tokens as the trigger resolves them, `--fgd-focus-width` solid
|
|
328
|
+
* `--primary12` at `--fgd-focus-offset` [D45]; a trigger whose ring sits
|
|
329
|
+
* elsewhere (inset) sets those tokens on itself.
|
|
330
|
+
*/
|
|
331
|
+
function ringBox(source, drawn, resolve) {
|
|
332
|
+
const rect = source.getBoundingClientRect();
|
|
333
|
+
const style = getComputedStyle(source);
|
|
334
|
+
const width = drawn
|
|
335
|
+
? px(style.outlineWidth)
|
|
336
|
+
: resolve(style.getPropertyValue('--fgd-focus-width'), source);
|
|
337
|
+
const offset = drawn
|
|
338
|
+
? px(style.outlineOffset)
|
|
339
|
+
: resolve(style.getPropertyValue('--fgd-focus-offset'), source);
|
|
340
|
+
const color = drawn
|
|
341
|
+
? settledValue(source, 'outline-color')
|
|
342
|
+
: style.getPropertyValue('--primary12').trim() || style.color;
|
|
343
|
+
const out = width + offset;
|
|
344
|
+
const w = rect.width + 2 * out;
|
|
345
|
+
const h = rect.height + 2 * out;
|
|
346
|
+
// A rounded corner's outline is rounded by offset + width more; a square one stays square.
|
|
347
|
+
const inner = usedRadii(style, rect.width, rect.height);
|
|
348
|
+
const r = inner.map((radius) => (radius > 0 ? Math.max(0, radius + out) : 0));
|
|
349
|
+
return { x: rect.left - out, y: rect.top - out, w, h, r: clampRadii(r, w, h), color, k: width };
|
|
350
|
+
}
|
|
351
|
+
/** Whether the trigger has a visible boxed edge (all four sides), rather than an underline, a fill or none. */
|
|
352
|
+
function hasBoxEdge(style) {
|
|
353
|
+
return (style.borderTopStyle !== 'none' &&
|
|
354
|
+
px(style.borderTopWidth) > 0 &&
|
|
355
|
+
px(style.borderRightWidth) > 0 &&
|
|
356
|
+
px(style.borderBottomWidth) > 0 &&
|
|
357
|
+
px(style.borderLeftWidth) > 0 &&
|
|
358
|
+
alphaOf(style.borderTopColor) > 0);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The trigger's own edge, in viewport coordinates, where the morph starts
|
|
362
|
+
* and lands without a ring: its border box and radii in the color of its
|
|
363
|
+
* boxed edge (as that edge's transition lands), else its border box in its
|
|
364
|
+
* opaque fill (a solid button); a trigger with neither (a text button, an
|
|
365
|
+
* underline, a bare glyph, a menubar item) has no edge of its own, so its
|
|
366
|
+
* ring's box stands in, in its ink. The color is always opaque, so the
|
|
367
|
+
* frame never fades in.
|
|
368
|
+
*/
|
|
369
|
+
function edgeBox(source, resolve) {
|
|
370
|
+
const rect = source.getBoundingClientRect();
|
|
371
|
+
const style = getComputedStyle(source);
|
|
372
|
+
const box = (color, k) => ({
|
|
373
|
+
...toRect(rect),
|
|
374
|
+
r: usedRadii(style, rect.width, rect.height),
|
|
375
|
+
color,
|
|
376
|
+
k,
|
|
377
|
+
});
|
|
378
|
+
if (hasBoxEdge(style))
|
|
379
|
+
return box(settledValue(source, 'border-top-color'), px(style.borderTopWidth));
|
|
380
|
+
const fill = settledValue(source, 'background-color');
|
|
381
|
+
// A fill has no edge weight of its own: the frame keeps the popup's, drawn inside it in its color.
|
|
382
|
+
if (alphaOf(fill) >= 1)
|
|
383
|
+
return box(fill, null);
|
|
384
|
+
return { ...ringBox(source, false, resolve), color: style.color };
|
|
385
|
+
}
|
|
386
|
+
/** Where the morph starts or lands on the trigger: the ring while one is (or will be) drawn, else its edge. */
|
|
387
|
+
function sourceBox(source, ring, resolve) {
|
|
388
|
+
if (ring === 'drawn')
|
|
389
|
+
return ringBox(source, true, resolve);
|
|
390
|
+
if (ring === 'predicted')
|
|
391
|
+
return ringBox(source, false, resolve);
|
|
392
|
+
return edgeBox(source, resolve);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The controller behind `useOutlineMorph`. Elements arrive through the ref
|
|
396
|
+
* callbacks; `update` receives the open state in a layout effect, so both
|
|
397
|
+
* directions start in the commit that changes the popup, before it paints.
|
|
398
|
+
*/
|
|
399
|
+
export class OutlineMorphController {
|
|
400
|
+
/** Explicit durations (the hook's `duration` option); otherwise the tokens are read at each open and close. */
|
|
401
|
+
duration = undefined;
|
|
402
|
+
ring = 'focus-visible';
|
|
403
|
+
/** A modal surface (a dialog, a sheet): the `-surface` durations. */
|
|
404
|
+
surface = false;
|
|
405
|
+
/** The timing of the running morph, read from the trigger when it started. */
|
|
406
|
+
timing = { ...EXPAND_DURATION_MS, ease: EXPAND_EASE };
|
|
407
|
+
/** Every trigger attached (a popup may have several); `source` is the one this open came from. */
|
|
408
|
+
sources = new Set();
|
|
409
|
+
source = null;
|
|
410
|
+
target = null;
|
|
411
|
+
frame = null;
|
|
412
|
+
/** The popup's own tail (an adopter's arrow) and the cap that stands in for it while the morph runs. */
|
|
413
|
+
tailSource = null;
|
|
414
|
+
tail = null;
|
|
415
|
+
tailAnimation = null;
|
|
416
|
+
tailFlight = null;
|
|
417
|
+
phase = 'idle';
|
|
418
|
+
open = false;
|
|
419
|
+
/** Whether the morph was on at the last update (a popup mounting before its open reaches us is held if so). */
|
|
420
|
+
enabled = true;
|
|
421
|
+
anticipateFrame = 0;
|
|
422
|
+
/**
|
|
423
|
+
* Where a pending open starts (viewport coordinates), the share of the
|
|
424
|
+
* open duration it runs, and, for a fresh open, the ring it started from.
|
|
425
|
+
*/
|
|
426
|
+
pending = null;
|
|
427
|
+
flight = null;
|
|
428
|
+
frameAnimation = null;
|
|
429
|
+
targetAnimation = null;
|
|
430
|
+
/** The popup's own frame, read while it shows (its border is the face while the morph runs). */
|
|
431
|
+
targetFrame = null;
|
|
432
|
+
placementObserver = null;
|
|
433
|
+
placementFrame = 0;
|
|
434
|
+
placementTries = 0;
|
|
435
|
+
trackFrame = 0;
|
|
436
|
+
settleObserver = null;
|
|
437
|
+
settleTimer;
|
|
438
|
+
/** A trigger: its focus ring (or edge) is where the morph starts and lands. */
|
|
439
|
+
sourceRef = (element) => {
|
|
440
|
+
if (element)
|
|
441
|
+
this.sources.add(element);
|
|
442
|
+
};
|
|
443
|
+
/** The popup: its border box and radius are the frame the morph grows into. */
|
|
444
|
+
targetRef = (element) => {
|
|
445
|
+
if (element === this.target)
|
|
446
|
+
return;
|
|
447
|
+
// Another popup element: the old one's morph ends with it.
|
|
448
|
+
if (element && this.target && this.phase !== 'pending')
|
|
449
|
+
this.stop();
|
|
450
|
+
this.target = element;
|
|
451
|
+
// A popup (re)attached while a fresh open waits is held too.
|
|
452
|
+
if (element && this.phase === 'pending' && this.pending?.ring !== undefined) {
|
|
453
|
+
element.setAttribute(OUTLINE_MORPH_PENDING_ATTRIBUTE, '');
|
|
454
|
+
}
|
|
455
|
+
else if (element && this.phase === 'idle' && !this.open && this.enabled) {
|
|
456
|
+
this.anticipate(element);
|
|
457
|
+
}
|
|
458
|
+
if (element)
|
|
459
|
+
this.watchPlacement();
|
|
460
|
+
else
|
|
461
|
+
this.detachSoon();
|
|
462
|
+
};
|
|
463
|
+
/** The popup's own tail, if it has one: the cap draws its copy while the morph runs [§9.17]. */
|
|
464
|
+
tailSourceRef = (element) => {
|
|
465
|
+
this.tailSource = element;
|
|
466
|
+
};
|
|
467
|
+
/** The tail cap, the overlay's tail, rendered after the frame in the same container. */
|
|
468
|
+
tailRef = (element) => {
|
|
469
|
+
if (element === this.tail)
|
|
470
|
+
return;
|
|
471
|
+
this.tail?.removeAttribute(RUNNING_ATTRIBUTE);
|
|
472
|
+
this.tail = element;
|
|
473
|
+
};
|
|
474
|
+
/** The overlay frame, a sibling of the popup inside its positioned container. */
|
|
475
|
+
frameRef = (element) => {
|
|
476
|
+
if (element === this.frame)
|
|
477
|
+
return;
|
|
478
|
+
if (element && this.frame && this.phase !== 'pending')
|
|
479
|
+
this.stop();
|
|
480
|
+
this.frame = element;
|
|
481
|
+
if (element)
|
|
482
|
+
this.watchPlacement();
|
|
483
|
+
else
|
|
484
|
+
this.detachSoon();
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* A popup mounted before its open reaches the controller (its owner's
|
|
488
|
+
* open state can land a commit after Base UI's): held as a pending open
|
|
489
|
+
* would be, if the morph would run, so it never paints unclipped first.
|
|
490
|
+
* The open claims the hold; if none comes within two frames, it goes.
|
|
491
|
+
*/
|
|
492
|
+
anticipate(element) {
|
|
493
|
+
const source = this.pickSource();
|
|
494
|
+
if (!source || !canOutlineMorph() || switchedOff(source))
|
|
495
|
+
return;
|
|
496
|
+
if (!rendered(source) || readTiming(source, this.duration, this.surface).open <= 0)
|
|
497
|
+
return;
|
|
498
|
+
element.setAttribute(OUTLINE_MORPH_PENDING_ATTRIBUTE, '');
|
|
499
|
+
if (this.anticipateFrame)
|
|
500
|
+
cancelAnimationFrame(this.anticipateFrame);
|
|
501
|
+
this.anticipateFrame = requestAnimationFrame(() => {
|
|
502
|
+
this.anticipateFrame = requestAnimationFrame(() => {
|
|
503
|
+
this.anticipateFrame = 0;
|
|
504
|
+
if (this.phase !== 'pending')
|
|
505
|
+
element.removeAttribute(OUTLINE_MORPH_PENDING_ATTRIBUTE);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The trigger this open belongs to: the only one attached, else the one
|
|
511
|
+
* marked open (Base UI's `aria-expanded` / `data-popup-open`), else the
|
|
512
|
+
* one holding focus, else the last attached. Detached ones are dropped.
|
|
513
|
+
*/
|
|
514
|
+
pickSource() {
|
|
515
|
+
for (const element of this.sources) {
|
|
516
|
+
if (!element.isConnected)
|
|
517
|
+
this.sources.delete(element);
|
|
518
|
+
}
|
|
519
|
+
const list = [...this.sources];
|
|
520
|
+
if (list.length <= 1)
|
|
521
|
+
return list[0] ?? null;
|
|
522
|
+
const active = document.activeElement;
|
|
523
|
+
return (list.find((element) => element.getAttribute('aria-expanded') === 'true' || element.hasAttribute('data-popup-open')) ??
|
|
524
|
+
list.find((element) => active != null && element.contains(active)) ??
|
|
525
|
+
list[list.length - 1]);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The popup or the frame detached: once the commit is over, a running
|
|
529
|
+
* morph stops if it is still gone. A ref that is only reattached (a
|
|
530
|
+
* merged ref's new identity, React's development double mount) comes
|
|
531
|
+
* back within the same commit, and a pending open keeps waiting for it.
|
|
532
|
+
*/
|
|
533
|
+
detachSoon() {
|
|
534
|
+
queueMicrotask(() => {
|
|
535
|
+
if ((this.target && this.frame) || this.phase === 'pending')
|
|
536
|
+
return;
|
|
537
|
+
this.stop();
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/** The popup's positioned container, which also holds the frame. */
|
|
541
|
+
get container() {
|
|
542
|
+
return this.frame?.parentElement ?? null;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* A length token's px value: a plain length as is, else resolved on the
|
|
546
|
+
* trigger itself (`resolveOn`), else through the frame's own
|
|
547
|
+
* `outline-offset` (the frame is ours, hidden while idle, and its CSS
|
|
548
|
+
* never sets that property), since a custom property's computed value
|
|
549
|
+
* keeps `calc()` and font-relative units unresolved.
|
|
550
|
+
*/
|
|
551
|
+
resolveLength = (value, context) => {
|
|
552
|
+
const raw = value.trim();
|
|
553
|
+
if (raw === '' || /^-?[\d.]+px$/.test(raw))
|
|
554
|
+
return px(raw);
|
|
555
|
+
const own = resolveOn(context, raw);
|
|
556
|
+
if (own !== null)
|
|
557
|
+
return own;
|
|
558
|
+
const probe = this.frame;
|
|
559
|
+
if (!probe)
|
|
560
|
+
return px(raw);
|
|
561
|
+
probe.style.setProperty('outline-offset', raw);
|
|
562
|
+
const resolved = px(getComputedStyle(probe).outlineOffset);
|
|
563
|
+
probe.style.removeProperty('outline-offset');
|
|
564
|
+
return resolved;
|
|
565
|
+
};
|
|
566
|
+
/**
|
|
567
|
+
* Receives the open state, and whether the morph is on, after each commit.
|
|
568
|
+
* Off (`enabled` false) stops a running morph, so the change the commit
|
|
569
|
+
* carries happens the popup's own way.
|
|
570
|
+
*/
|
|
571
|
+
update(open, enabled) {
|
|
572
|
+
const wasOpen = this.open;
|
|
573
|
+
this.open = open;
|
|
574
|
+
this.enabled = enabled;
|
|
575
|
+
if (!enabled) {
|
|
576
|
+
// Also records a popup opened this way as open, so a later close can morph from it.
|
|
577
|
+
this.stop();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (open === wasOpen)
|
|
581
|
+
return;
|
|
582
|
+
if (open)
|
|
583
|
+
this.startOpen();
|
|
584
|
+
else
|
|
585
|
+
this.startClose();
|
|
586
|
+
}
|
|
587
|
+
/** Clears everything: animations, observers, the frame and the popup's attribute. */
|
|
588
|
+
stop() {
|
|
589
|
+
this.cancelPlacement();
|
|
590
|
+
this.cancelSettle();
|
|
591
|
+
this.cancelAnimations();
|
|
592
|
+
this.pending = null;
|
|
593
|
+
this.frame?.removeAttribute(RUNNING_ATTRIBUTE);
|
|
594
|
+
this.tail?.removeAttribute(RUNNING_ATTRIBUTE);
|
|
595
|
+
this.target?.removeAttribute(OUTLINE_MORPH_ATTRIBUTE);
|
|
596
|
+
this.target?.removeAttribute(OUTLINE_MORPH_PENDING_ATTRIBUTE);
|
|
597
|
+
this.phase = this.open ? 'open' : 'idle';
|
|
598
|
+
}
|
|
599
|
+
startOpen() {
|
|
600
|
+
const reversing = (this.phase === 'closing' || this.phase === 'closed') &&
|
|
601
|
+
(this.frame?.hasAttribute(RUNNING_ATTRIBUTE) ?? false);
|
|
602
|
+
const source = reversing ? (this.source ?? this.pickSource()) : this.pickSource();
|
|
603
|
+
const timing = source ? readTiming(source, this.duration, this.surface) : null;
|
|
604
|
+
// A zero open duration (the token set to 0ms, as under reduced motion) is the morph off; so is
|
|
605
|
+
// no trigger to start from (a programmatic open with none attached, or one not rendered).
|
|
606
|
+
if (!source ||
|
|
607
|
+
!rendered(source) ||
|
|
608
|
+
!timing ||
|
|
609
|
+
timing.open <= 0 ||
|
|
610
|
+
!canOutlineMorph() ||
|
|
611
|
+
switchedOff(source)) {
|
|
612
|
+
this.stop();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.source = source;
|
|
616
|
+
this.timing = timing;
|
|
617
|
+
const current = reversing ? this.currentBox() : null;
|
|
618
|
+
if (current) {
|
|
619
|
+
// Reopened while closing: grow again from where the frame is, for the share left.
|
|
620
|
+
const share = Math.max(this.progress(), MIN_RESUME_SHARE);
|
|
621
|
+
this.cancelSettle();
|
|
622
|
+
this.cancelAnimations();
|
|
623
|
+
this.pending = { from: current, share };
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
this.stop();
|
|
627
|
+
// The trigger still holds focus here; a popup that takes it does so on a later frame.
|
|
628
|
+
const ring = ringDrawn(source, this.ring) ? 'drawn' : 'none';
|
|
629
|
+
this.pending = { from: sourceBox(source, ring, this.resolveLength), share: 1, ring };
|
|
630
|
+
// Held unpainted until it is placed and the morph starts (a reversal is already on screen).
|
|
631
|
+
this.target?.setAttribute(OUTLINE_MORPH_PENDING_ATTRIBUTE, '');
|
|
632
|
+
}
|
|
633
|
+
this.phase = 'pending';
|
|
634
|
+
this.placementTries = 0;
|
|
635
|
+
this.watchPlacement();
|
|
636
|
+
}
|
|
637
|
+
startClose() {
|
|
638
|
+
const source = this.source ?? this.pickSource();
|
|
639
|
+
const target = this.target;
|
|
640
|
+
const container = this.container;
|
|
641
|
+
const running = this.phase === 'opening' || this.phase === 'open';
|
|
642
|
+
const timing = source ? readTiming(source, this.duration, this.surface) : null;
|
|
643
|
+
if (!running ||
|
|
644
|
+
!source ||
|
|
645
|
+
!rendered(source) ||
|
|
646
|
+
!target ||
|
|
647
|
+
!container ||
|
|
648
|
+
!timing ||
|
|
649
|
+
timing.close <= 0 ||
|
|
650
|
+
!canOutlineMorph() ||
|
|
651
|
+
switchedOff(source) ||
|
|
652
|
+
switchedOff(target)) {
|
|
653
|
+
// Never shown by the morph (or motion is now off): the popup closes its own way.
|
|
654
|
+
this.stop();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
this.source = source;
|
|
658
|
+
this.timing = timing;
|
|
659
|
+
const ring = this.closingRing(source, container);
|
|
660
|
+
const to = sourceBox(source, ring, this.resolveLength);
|
|
661
|
+
let from;
|
|
662
|
+
let share = 1;
|
|
663
|
+
if (this.phase === 'opening') {
|
|
664
|
+
share = Math.max(this.progress(), MIN_RESUME_SHARE);
|
|
665
|
+
from = this.currentBox();
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const frame = this.readTargetFrame(target);
|
|
669
|
+
this.targetFrame = frame;
|
|
670
|
+
from = landing(toRect(target.getBoundingClientRect()), frame, frame.width ?? to.k ?? this.defaultWeight(target), frame.color ?? to.color, this.viewport());
|
|
671
|
+
}
|
|
672
|
+
if (!from) {
|
|
673
|
+
// Gone, or a side with no edge that isn't on the viewport's edge: it closes at once.
|
|
674
|
+
this.stop();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this.phase = 'closing';
|
|
678
|
+
this.run(from, to, { kind: 'source', ring }, this.timing.close * share);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Which ring a close lands on. A field that draws its ring on any focus
|
|
682
|
+
* (`focus-within`) keeps it while focus stays in it. Otherwise focus comes
|
|
683
|
+
* back to the trigger when the popup hides, with a ring if the focus inside
|
|
684
|
+
* the popup shows one (the browser carries it over) or if the popup was
|
|
685
|
+
* closed from the keyboard.
|
|
686
|
+
*/
|
|
687
|
+
closingRing(source, container) {
|
|
688
|
+
if (ringDrawn(source, this.ring))
|
|
689
|
+
return 'drawn';
|
|
690
|
+
const active = document.activeElement;
|
|
691
|
+
// The popup's container holds it (a positioner), or it is the popup's sibling (a modal layer).
|
|
692
|
+
const inPopup = active instanceof HTMLElement && (container.contains(active) || (this.target?.contains(active) ?? false));
|
|
693
|
+
if (this.ring === 'focus-within')
|
|
694
|
+
return inPopup ? 'predicted' : 'none';
|
|
695
|
+
const ringReturns = source.matches(':focus-visible') || (inPopup && active.matches(':focus-visible'));
|
|
696
|
+
return ringReturns ? 'predicted' : 'none';
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The ring a running close lands on now: drawn once the trigger draws it;
|
|
700
|
+
* the planned one while focus stays in the trigger or the popup (on its
|
|
701
|
+
* way back); none once focus has gone elsewhere (an outside press).
|
|
702
|
+
*/
|
|
703
|
+
liveRing(planned, source) {
|
|
704
|
+
if (ringDrawn(source, this.ring))
|
|
705
|
+
return 'drawn';
|
|
706
|
+
if (planned === 'none')
|
|
707
|
+
return 'none';
|
|
708
|
+
if (focusHeld(source, [this.container, this.target]))
|
|
709
|
+
return 'predicted';
|
|
710
|
+
// Focus has left. A field's ring went with it; a trigger whose popup
|
|
711
|
+
// hands focus back may pass through the body on the way, so only focus
|
|
712
|
+
// landing somewhere else cancels its ring.
|
|
713
|
+
const active = document.activeElement;
|
|
714
|
+
if (this.ring === 'focus-within' || (active != null && active !== document.body))
|
|
715
|
+
return 'none';
|
|
716
|
+
return planned;
|
|
717
|
+
}
|
|
718
|
+
/** Starts a pending open once the popup is placed and laid out. Returns whether it started. */
|
|
719
|
+
tryStart() {
|
|
720
|
+
const pending = this.pending;
|
|
721
|
+
const target = this.target;
|
|
722
|
+
const container = this.container;
|
|
723
|
+
if (this.phase !== 'pending' || !pending || !target || !container)
|
|
724
|
+
return false;
|
|
725
|
+
if (container.hidden)
|
|
726
|
+
return false;
|
|
727
|
+
const rect = target.getBoundingClientRect();
|
|
728
|
+
if (rect.width === 0 || rect.height === 0)
|
|
729
|
+
return false;
|
|
730
|
+
// A popup not yet placed is held invisible (Base UI's positioner) at the viewport origin.
|
|
731
|
+
if (getComputedStyle(container).opacity === '0')
|
|
732
|
+
return false;
|
|
733
|
+
this.cancelPlacement();
|
|
734
|
+
target.removeAttribute(OUTLINE_MORPH_PENDING_ATTRIBUTE);
|
|
735
|
+
if (switchedOff(target)) {
|
|
736
|
+
// The popup opts out (its own form, such as a tail, isn't a plain frame): it opens its own way.
|
|
737
|
+
this.stop();
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
// A reversed close keeps the frame read before the popup's border took its face.
|
|
741
|
+
const frame = this.targetFrame && target.hasAttribute(OUTLINE_MORPH_ATTRIBUTE)
|
|
742
|
+
? this.targetFrame
|
|
743
|
+
: this.readTargetFrame(target);
|
|
744
|
+
this.targetFrame = frame;
|
|
745
|
+
this.pending = null;
|
|
746
|
+
this.phase = 'opening';
|
|
747
|
+
// A press into a field focuses it after the open began: start on the ring it now draws.
|
|
748
|
+
const upgrade = pending.ring === 'none' && this.source != null && ringDrawn(this.source, this.ring);
|
|
749
|
+
const from = upgrade && this.source ? sourceBox(this.source, 'drawn', this.resolveLength) : pending.from;
|
|
750
|
+
const to = landing(toRect(rect), frame, frame.width ?? from.k ?? this.defaultWeight(target), frame.color ?? from.color, this.viewport());
|
|
751
|
+
if (!to) {
|
|
752
|
+
// A side with no edge of its own, not on the viewport's edge: no frame to land on, so it opens at once.
|
|
753
|
+
this.stop();
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
this.run(from, to, { kind: 'target' }, this.timing.open * pending.share, upgrade ? 'drawn' : pending.ring);
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* The box fixed elements are laid out in: a modal layer's own box (fixed,
|
|
761
|
+
* inset 0: the viewport less a reserved scrollbar gutter), else the
|
|
762
|
+
* root's client box.
|
|
763
|
+
*/
|
|
764
|
+
viewport() {
|
|
765
|
+
const container = this.container;
|
|
766
|
+
if (container && getComputedStyle(container).position === 'fixed') {
|
|
767
|
+
return toRect(container.getBoundingClientRect());
|
|
768
|
+
}
|
|
769
|
+
const root = document.documentElement;
|
|
770
|
+
return { x: 0, y: 0, w: root.clientWidth, h: root.clientHeight };
|
|
771
|
+
}
|
|
772
|
+
/** The system ring's weight, `--fgd-focus-width` (2 px): a frame's weight where neither end has one. */
|
|
773
|
+
defaultWeight(context) {
|
|
774
|
+
return this.resolveLength(getComputedStyle(context).getPropertyValue('--fgd-focus-width'), context) || 2;
|
|
775
|
+
}
|
|
776
|
+
readTargetFrame(target) {
|
|
777
|
+
const style = getComputedStyle(target);
|
|
778
|
+
const rect = target.getBoundingClientRect();
|
|
779
|
+
// A side counts as an edge only if it paints: a border in the popup's
|
|
780
|
+
// own face (a band's seam) is invisible [D12], so it is no edge.
|
|
781
|
+
const face = style.backgroundColor;
|
|
782
|
+
const shown = (side) => {
|
|
783
|
+
const color = style[`border${side}Color`];
|
|
784
|
+
if (style[`border${side}Style`] === 'none' || alphaOf(color) === 0 || color === face)
|
|
785
|
+
return 0;
|
|
786
|
+
return px(style[`border${side}Width`]);
|
|
787
|
+
};
|
|
788
|
+
const sides = [shown('Top'), shown('Right'), shown('Bottom'), shown('Left')];
|
|
789
|
+
const widest = sides.indexOf(Math.max(...sides));
|
|
790
|
+
const colors = [style.borderTopColor, style.borderRightColor, style.borderBottomColor, style.borderLeftColor];
|
|
791
|
+
const edged = sides[widest] > 0;
|
|
792
|
+
return {
|
|
793
|
+
r: usedRadii(style, rect.width, rect.height),
|
|
794
|
+
color: edged ? colors[widest] : null,
|
|
795
|
+
width: edged ? sides[widest] : null,
|
|
796
|
+
sides,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
/** The frame's box now, mid-animation, in viewport coordinates. */
|
|
800
|
+
currentBox() {
|
|
801
|
+
const frame = this.frame;
|
|
802
|
+
const container = this.container;
|
|
803
|
+
if (!frame || !container || !frame.hasAttribute(RUNNING_ATTRIBUTE))
|
|
804
|
+
return null;
|
|
805
|
+
const origin = container.getBoundingClientRect();
|
|
806
|
+
const style = getComputedStyle(frame);
|
|
807
|
+
let tx;
|
|
808
|
+
let ty;
|
|
809
|
+
if (frameOffsetMode() === 'rounded') {
|
|
810
|
+
tx = style.getPropertyValue(OFFSET_X);
|
|
811
|
+
ty = style.getPropertyValue(OFFSET_Y);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
// `translate` serializes as "x y", or "x" when y is 0.
|
|
815
|
+
;
|
|
816
|
+
[tx = '0', ty = '0'] = style.translate === 'none' ? [] : style.translate.split(' ');
|
|
817
|
+
}
|
|
818
|
+
const w = px(style.width);
|
|
819
|
+
const h = px(style.height);
|
|
820
|
+
return {
|
|
821
|
+
x: origin.left + px(tx),
|
|
822
|
+
y: origin.top + px(ty),
|
|
823
|
+
w,
|
|
824
|
+
h,
|
|
825
|
+
r: usedRadii(style, w, h),
|
|
826
|
+
color: style.borderTopColor,
|
|
827
|
+
k: px(style.borderTopWidth),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/** How far the running animation has come (eased, 0–1); 1 when none runs. */
|
|
831
|
+
progress() {
|
|
832
|
+
const progress = this.frameAnimation?.effect?.getComputedTiming().progress;
|
|
833
|
+
return typeof progress === 'number' ? progress : 1;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Animates the frame and the popup's clip from one box to the other
|
|
837
|
+
* (viewport coordinates, converted to the container's) on one timing.
|
|
838
|
+
* The popup's border takes its face (its attribute) while the frame draws
|
|
839
|
+
* the edge.
|
|
840
|
+
*/
|
|
841
|
+
run(from, to, toward, duration, fromRing) {
|
|
842
|
+
const frame = this.frame;
|
|
843
|
+
const target = this.target;
|
|
844
|
+
const container = this.container;
|
|
845
|
+
if (!frame || !target || !container) {
|
|
846
|
+
this.stop();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
this.cancelAnimations();
|
|
850
|
+
const origin = container.getBoundingClientRect();
|
|
851
|
+
const local = (box) => ({
|
|
852
|
+
...box,
|
|
853
|
+
x: box.x - origin.left,
|
|
854
|
+
y: box.y - origin.top,
|
|
855
|
+
});
|
|
856
|
+
// Each end's weight; an end with none of its own (a fill, a sheet with no edge) takes the other's.
|
|
857
|
+
const fallback = this.targetFrame?.width ?? this.defaultWeight(target);
|
|
858
|
+
const fromK = from.k ?? to.k ?? fallback;
|
|
859
|
+
const toK = to.k ?? from.k ?? fallback;
|
|
860
|
+
const flight = {
|
|
861
|
+
from: local(from),
|
|
862
|
+
to: local(to),
|
|
863
|
+
toward,
|
|
864
|
+
target: local(toRect(target.getBoundingClientRect())),
|
|
865
|
+
fromK,
|
|
866
|
+
toK,
|
|
867
|
+
fromRing,
|
|
868
|
+
sourceAt: fromRing !== undefined && this.source ? local(toRect(this.source.getBoundingClientRect())) : undefined,
|
|
869
|
+
};
|
|
870
|
+
this.flight = flight;
|
|
871
|
+
const timing = {
|
|
872
|
+
duration: Math.max(0, Math.round(duration)),
|
|
873
|
+
easing: this.timing.ease,
|
|
874
|
+
fill: 'both',
|
|
875
|
+
};
|
|
876
|
+
target.setAttribute(OUTLINE_MORPH_ATTRIBUTE, '');
|
|
877
|
+
frame.setAttribute(RUNNING_ATTRIBUTE, '');
|
|
878
|
+
const frameAnimation = frame.animate(frameKeyframes(flight), timing);
|
|
879
|
+
const targetAnimation = target.animate(clipKeyframes(flight), timing);
|
|
880
|
+
// One start time for both: a clip the compositor runs can start a frame
|
|
881
|
+
// after the frame's box and trail it by that frame. Both paint their
|
|
882
|
+
// first keyframe first (the ring, the popup's frame, or where a reversed
|
|
883
|
+
// morph turns, which is what is already on screen), then the clip takes
|
|
884
|
+
// the box's start time.
|
|
885
|
+
frameAnimation.ready.then(() => {
|
|
886
|
+
if (this.targetAnimation === targetAnimation && frameAnimation.startTime != null) {
|
|
887
|
+
targetAnimation.startTime = frameAnimation.startTime;
|
|
888
|
+
}
|
|
889
|
+
}, () => { });
|
|
890
|
+
this.frameAnimation = frameAnimation;
|
|
891
|
+
this.targetAnimation = targetAnimation;
|
|
892
|
+
this.startTail(flight, timing, frameAnimation);
|
|
893
|
+
this.track();
|
|
894
|
+
frameAnimation.finished.then(() => {
|
|
895
|
+
if (this.frameAnimation !== frameAnimation)
|
|
896
|
+
return;
|
|
897
|
+
this.cancelTrack();
|
|
898
|
+
if (this.phase === 'opening')
|
|
899
|
+
this.handOff();
|
|
900
|
+
else if (this.phase === 'closing')
|
|
901
|
+
this.landClose();
|
|
902
|
+
}, () => { });
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Re-measures the end the morph travels to every frame while it runs; if
|
|
906
|
+
* it moved (a scroll, a resize, the popup settling its size) or a close's
|
|
907
|
+
* ring changed (focus came back, or left), both animations take new
|
|
908
|
+
* keyframes at their current time.
|
|
909
|
+
*/
|
|
910
|
+
track() {
|
|
911
|
+
this.cancelTrack();
|
|
912
|
+
const step = () => {
|
|
913
|
+
this.trackFrame = 0;
|
|
914
|
+
const flight = this.flight;
|
|
915
|
+
const source = this.source;
|
|
916
|
+
const target = this.target;
|
|
917
|
+
const container = this.container;
|
|
918
|
+
if (!flight || !source || !target || !container || !this.frameAnimation)
|
|
919
|
+
return;
|
|
920
|
+
const origin = container.getBoundingClientRect();
|
|
921
|
+
const targetRect = toRect(target.getBoundingClientRect());
|
|
922
|
+
const localTarget = {
|
|
923
|
+
...targetRect,
|
|
924
|
+
x: targetRect.x - origin.left,
|
|
925
|
+
y: targetRect.y - origin.top,
|
|
926
|
+
};
|
|
927
|
+
let changed = drifted(localTarget, flight.target);
|
|
928
|
+
if (flight.toward.kind === 'target') {
|
|
929
|
+
if (changed && this.targetFrame) {
|
|
930
|
+
// The popup moved or resized (a sheet's bleed follows its box).
|
|
931
|
+
const end = landing(targetRect, this.targetFrame, flight.toK, flight.to.color, this.viewport());
|
|
932
|
+
if (end)
|
|
933
|
+
flight.to = { ...flight.to, ...end, x: end.x - origin.left, y: end.y - origin.top };
|
|
934
|
+
}
|
|
935
|
+
// Early re-measure: a trigger that moved against the container in
|
|
936
|
+
// the first frames (the page shifting under a scroll lock) moves the
|
|
937
|
+
// start with it, before much of the path is painted.
|
|
938
|
+
if (flight.sourceAt && this.progress() < EARLY_PROGRESS) {
|
|
939
|
+
const now = toRect(source.getBoundingClientRect());
|
|
940
|
+
const at = { ...now, x: now.x - origin.left, y: now.y - origin.top };
|
|
941
|
+
if (drifted(at, flight.sourceAt)) {
|
|
942
|
+
const was = flight.sourceAt;
|
|
943
|
+
flight.from = {
|
|
944
|
+
...flight.from,
|
|
945
|
+
x: flight.from.x + at.x - was.x,
|
|
946
|
+
y: flight.from.y + at.y - was.y,
|
|
947
|
+
w: flight.from.w + at.w - was.w,
|
|
948
|
+
h: flight.from.h + at.h - was.h,
|
|
949
|
+
};
|
|
950
|
+
flight.sourceAt = at;
|
|
951
|
+
changed = true;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// A field pressed open takes focus just after the open began (the
|
|
955
|
+
// press's default action): in the first frames, move the start
|
|
956
|
+
// onto the ring it now draws, before much of the path is painted.
|
|
957
|
+
if (flight.fromRing === 'none' && this.progress() < EARLY_PROGRESS && ringDrawn(source, this.ring)) {
|
|
958
|
+
const start = sourceBox(source, 'drawn', this.resolveLength);
|
|
959
|
+
flight.from = { ...start, x: start.x - origin.left, y: start.y - origin.top };
|
|
960
|
+
flight.fromK = start.k ?? flight.fromK;
|
|
961
|
+
flight.fromRing = 'drawn';
|
|
962
|
+
changed = true;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
const ring = this.liveRing(flight.toward.ring, source);
|
|
967
|
+
const end = sourceBox(source, ring, this.resolveLength);
|
|
968
|
+
const localEnd = { ...end, x: end.x - origin.left, y: end.y - origin.top };
|
|
969
|
+
if (ring !== flight.toward.ring) {
|
|
970
|
+
// Focus came back with its ring, or left: land on what the trigger shows now.
|
|
971
|
+
flight.toward = { kind: 'source', ring };
|
|
972
|
+
flight.to = localEnd;
|
|
973
|
+
flight.toK = localEnd.k ?? flight.toK;
|
|
974
|
+
changed = true;
|
|
975
|
+
}
|
|
976
|
+
else if (drifted(localEnd, flight.to)) {
|
|
977
|
+
flight.to = { ...flight.to, x: localEnd.x, y: localEnd.y, w: localEnd.w, h: localEnd.h };
|
|
978
|
+
changed = true;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// The tail moves with its popup (a flip, a shift): re-read it too.
|
|
982
|
+
const tailFlight = this.tailAnimation ? this.readTail(origin) : null;
|
|
983
|
+
const tailMoved = tailFlight != null &&
|
|
984
|
+
this.tailFlight != null &&
|
|
985
|
+
(Math.abs(tailFlight.cx - this.tailFlight.cx) > DRIFT_PX ||
|
|
986
|
+
Math.abs(tailFlight.cy - this.tailFlight.cy) > DRIFT_PX ||
|
|
987
|
+
tailFlight.side !== this.tailFlight.side);
|
|
988
|
+
if (changed) {
|
|
989
|
+
flight.target = localTarget;
|
|
990
|
+
this.frameAnimation.effect?.setKeyframes(frameKeyframes(flight));
|
|
991
|
+
this.targetAnimation?.effect?.setKeyframes(clipKeyframes(flight));
|
|
992
|
+
}
|
|
993
|
+
if (tailFlight && (changed || tailMoved)) {
|
|
994
|
+
this.placeTail(tailFlight);
|
|
995
|
+
this.tailAnimation?.effect?.setKeyframes(tailKeyframes(tailFlight, flight));
|
|
996
|
+
}
|
|
997
|
+
this.trackFrame = requestAnimationFrame(step);
|
|
998
|
+
};
|
|
999
|
+
this.trackFrame = requestAnimationFrame(step);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* The open lands: in one task the popup's clip and attribute go (its own
|
|
1003
|
+
* border shows) and the frame hides, so nothing changes on screen.
|
|
1004
|
+
*/
|
|
1005
|
+
handOff() {
|
|
1006
|
+
this.cancelAnimations();
|
|
1007
|
+
this.target?.removeAttribute(OUTLINE_MORPH_ATTRIBUTE);
|
|
1008
|
+
this.frame?.removeAttribute(RUNNING_ATTRIBUTE);
|
|
1009
|
+
// The popup's own tail shows again on the pixels the cap held.
|
|
1010
|
+
this.tail?.removeAttribute(RUNNING_ATTRIBUTE);
|
|
1011
|
+
this.phase = 'open';
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* The popup's own tail as the cap copies it, in the container's
|
|
1015
|
+
* coordinates, or null (no tail, or not laid out). Its box is exact for
|
|
1016
|
+
* the quarter turns a tail takes. The edge it sits on is read from where
|
|
1017
|
+
* it lies against the popup (so logical sides need no direction rule).
|
|
1018
|
+
*/
|
|
1019
|
+
readTail(origin) {
|
|
1020
|
+
const source = this.tailSource;
|
|
1021
|
+
const target = this.target;
|
|
1022
|
+
if (!source || !target || !source.isConnected || !target.contains(source))
|
|
1023
|
+
return null;
|
|
1024
|
+
const w = source.offsetWidth;
|
|
1025
|
+
const h = source.offsetHeight;
|
|
1026
|
+
if (w === 0 || h === 0)
|
|
1027
|
+
return null;
|
|
1028
|
+
const rect = source.getBoundingClientRect();
|
|
1029
|
+
const popup = target.getBoundingClientRect();
|
|
1030
|
+
const side = source.getAttribute('data-side') ?? 'bottom';
|
|
1031
|
+
const cx = rect.left + rect.width / 2;
|
|
1032
|
+
const cy = rect.top + rect.height / 2;
|
|
1033
|
+
const normal = cy <= popup.top ? 'up' : cy >= popup.bottom ? 'down' : cx <= popup.left ? 'left' : 'right';
|
|
1034
|
+
return {
|
|
1035
|
+
cx: cx - origin.left,
|
|
1036
|
+
cy: cy - origin.top,
|
|
1037
|
+
w,
|
|
1038
|
+
h,
|
|
1039
|
+
d: h,
|
|
1040
|
+
normal,
|
|
1041
|
+
side,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Places the cap on the tail (custom properties only), in whole pixels
|
|
1046
|
+
* from the container's corner: the tail's box is laid out, so it paints
|
|
1047
|
+
* snapped to the pixel grid, and a fractional translation would paint the
|
|
1048
|
+
* cap's stroke softer than the tail it hands off to.
|
|
1049
|
+
*/
|
|
1050
|
+
placeTail(tail) {
|
|
1051
|
+
const cap = this.tail;
|
|
1052
|
+
if (!cap)
|
|
1053
|
+
return;
|
|
1054
|
+
cap.style.setProperty(TAIL_X, `${Math.round(tail.cx - tail.w / 2)}px`);
|
|
1055
|
+
cap.style.setProperty(TAIL_Y, `${Math.round(tail.cy - tail.h / 2)}px`);
|
|
1056
|
+
cap.setAttribute('data-side', tail.side);
|
|
1057
|
+
this.tailFlight = tail;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* The tail cap: a copy of the popup's tail above the frame, shown only on
|
|
1061
|
+
* the trigger side of the frame's trigger-facing inner edge, so it grows
|
|
1062
|
+
* out of that edge as the frame arrives, tip first and always attached,
|
|
1063
|
+
* and is whole exactly as the frame lands; a close retracts it into the
|
|
1064
|
+
* edge. Its stroke takes the frame's color on the same timing, its face
|
|
1065
|
+
* the popup's. Nothing fades.
|
|
1066
|
+
*/
|
|
1067
|
+
startTail(flight, timing, frameAnimation) {
|
|
1068
|
+
const cap = this.tail;
|
|
1069
|
+
const target = this.target;
|
|
1070
|
+
const container = this.container;
|
|
1071
|
+
if (!cap || !target || !container)
|
|
1072
|
+
return;
|
|
1073
|
+
const tail = this.readTail(container.getBoundingClientRect());
|
|
1074
|
+
if (!tail) {
|
|
1075
|
+
cap.removeAttribute(RUNNING_ATTRIBUTE);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
this.placeTail(tail);
|
|
1079
|
+
cap.style.setProperty(TAIL_FILL, getComputedStyle(target).backgroundColor);
|
|
1080
|
+
cap.setAttribute(RUNNING_ATTRIBUTE, '');
|
|
1081
|
+
const tailAnimation = cap.animate(tailKeyframes(tail, flight), timing);
|
|
1082
|
+
frameAnimation.ready.then(() => {
|
|
1083
|
+
if (this.tailAnimation === tailAnimation && frameAnimation.startTime != null) {
|
|
1084
|
+
tailAnimation.startTime = frameAnimation.startTime;
|
|
1085
|
+
}
|
|
1086
|
+
}, () => { });
|
|
1087
|
+
this.tailAnimation = tailAnimation;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* The close lands on the trigger. The frame and the popup's clip hold
|
|
1091
|
+
* their end state until the popup's container hides, which takes the
|
|
1092
|
+
* frame with it in the frame focus returns (so the ring shows at once);
|
|
1093
|
+
* then everything clears. A container that never hides clears after
|
|
1094
|
+
* SETTLE_MS.
|
|
1095
|
+
*/
|
|
1096
|
+
landClose() {
|
|
1097
|
+
this.phase = 'closed';
|
|
1098
|
+
const container = this.container;
|
|
1099
|
+
const settle = () => {
|
|
1100
|
+
if (this.phase === 'closed')
|
|
1101
|
+
this.stop();
|
|
1102
|
+
};
|
|
1103
|
+
if (!container || container.hidden || !container.isConnected) {
|
|
1104
|
+
settle();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
this.cancelSettle();
|
|
1108
|
+
this.settleObserver = new MutationObserver(() => {
|
|
1109
|
+
if (container.hidden || !container.isConnected)
|
|
1110
|
+
settle();
|
|
1111
|
+
});
|
|
1112
|
+
this.settleObserver.observe(container, { attributes: true, attributeFilter: ['hidden', 'style'] });
|
|
1113
|
+
this.settleTimer = setTimeout(settle, SETTLE_MS);
|
|
1114
|
+
}
|
|
1115
|
+
/** While an open waits for the popup's placement: try now, then watch its container and the popup, and poll a few frames. */
|
|
1116
|
+
watchPlacement() {
|
|
1117
|
+
if (this.phase !== 'pending' || this.tryStart())
|
|
1118
|
+
return;
|
|
1119
|
+
const container = this.container;
|
|
1120
|
+
const target = this.target;
|
|
1121
|
+
this.placementObserver?.disconnect();
|
|
1122
|
+
this.placementObserver = null;
|
|
1123
|
+
if (container && target) {
|
|
1124
|
+
this.placementObserver = new MutationObserver(() => {
|
|
1125
|
+
this.tryStart();
|
|
1126
|
+
});
|
|
1127
|
+
this.placementObserver.observe(container, { attributes: true, attributeFilter: ['style', 'hidden'] });
|
|
1128
|
+
this.placementObserver.observe(target, { attributes: true });
|
|
1129
|
+
}
|
|
1130
|
+
if (this.placementFrame)
|
|
1131
|
+
return;
|
|
1132
|
+
const poll = () => {
|
|
1133
|
+
this.placementFrame = 0;
|
|
1134
|
+
if (this.phase !== 'pending' || this.tryStart())
|
|
1135
|
+
return;
|
|
1136
|
+
this.placementTries += 1;
|
|
1137
|
+
if (this.placementTries >= PLACEMENT_FRAMES) {
|
|
1138
|
+
// Never placed in time: the popup opens its own way.
|
|
1139
|
+
this.stop();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
this.placementFrame = requestAnimationFrame(poll);
|
|
1143
|
+
};
|
|
1144
|
+
this.placementFrame = requestAnimationFrame(poll);
|
|
1145
|
+
}
|
|
1146
|
+
cancelPlacement() {
|
|
1147
|
+
this.placementObserver?.disconnect();
|
|
1148
|
+
this.placementObserver = null;
|
|
1149
|
+
if (this.placementFrame)
|
|
1150
|
+
cancelAnimationFrame(this.placementFrame);
|
|
1151
|
+
this.placementFrame = 0;
|
|
1152
|
+
}
|
|
1153
|
+
cancelTrack() {
|
|
1154
|
+
if (this.trackFrame)
|
|
1155
|
+
cancelAnimationFrame(this.trackFrame);
|
|
1156
|
+
this.trackFrame = 0;
|
|
1157
|
+
}
|
|
1158
|
+
cancelSettle() {
|
|
1159
|
+
this.settleObserver?.disconnect();
|
|
1160
|
+
this.settleObserver = null;
|
|
1161
|
+
if (this.settleTimer !== undefined)
|
|
1162
|
+
clearTimeout(this.settleTimer);
|
|
1163
|
+
this.settleTimer = undefined;
|
|
1164
|
+
}
|
|
1165
|
+
cancelAnimations() {
|
|
1166
|
+
this.cancelTrack();
|
|
1167
|
+
this.frameAnimation?.cancel();
|
|
1168
|
+
this.targetAnimation?.cancel();
|
|
1169
|
+
this.tailAnimation?.cancel();
|
|
1170
|
+
this.frameAnimation = null;
|
|
1171
|
+
this.targetAnimation = null;
|
|
1172
|
+
this.tailAnimation = null;
|
|
1173
|
+
this.tailFlight = null;
|
|
1174
|
+
this.flight = null;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/** Radii as a `border-radius` (or `round`) value, drawn in by `inset`. */
|
|
1178
|
+
function radiiValue(r, inset = 0) {
|
|
1179
|
+
return r.map((radius) => `${Math.max(0, radius - inset)}px`).join(' ');
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* The frame's two keyframes: box, radii, color, one edge weight. The box
|
|
1183
|
+
* moves by a translation (never a scale, so the edge keeps its weight),
|
|
1184
|
+
* through the rounded offset properties where supported, and resizes by
|
|
1185
|
+
* width and height from a fixed start corner, so the frame's travel is
|
|
1186
|
+
* never reported as a layout shift.
|
|
1187
|
+
*/
|
|
1188
|
+
function frameKeyframes(flight) {
|
|
1189
|
+
const rounded = frameOffsetMode() === 'rounded';
|
|
1190
|
+
const weight = (k) => (rounded ? { [EDGE_WEIGHT]: `${k}px` } : { borderWidth: `${k}px` });
|
|
1191
|
+
const place = (box, k) => {
|
|
1192
|
+
// Each end at the edges the browser paints it with: snapped to whole
|
|
1193
|
+
// pixels, the size from the snapped far edges (so a popup 297.5 px tall
|
|
1194
|
+
// ends on its own painted frame, not a pixel off).
|
|
1195
|
+
const w = Math.round(box.x + box.w) - Math.round(box.x);
|
|
1196
|
+
const h = Math.round(box.y + box.h) - Math.round(box.y);
|
|
1197
|
+
if (!rounded) {
|
|
1198
|
+
return {
|
|
1199
|
+
translate: `${box.x}px ${box.y}px`,
|
|
1200
|
+
width: `${w}px`,
|
|
1201
|
+
height: `${h}px`,
|
|
1202
|
+
borderRadius: radiiValue(box.r),
|
|
1203
|
+
borderColor: box.color,
|
|
1204
|
+
...weight(k),
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
[OFFSET_X]: `${box.x}px`,
|
|
1209
|
+
[OFFSET_Y]: `${box.y}px`,
|
|
1210
|
+
[SIZE_W]: `${w}px`,
|
|
1211
|
+
[SIZE_H]: `${h}px`,
|
|
1212
|
+
[RADII[0]]: `${Math.max(0, box.r[0])}px`,
|
|
1213
|
+
[RADII[1]]: `${Math.max(0, box.r[1])}px`,
|
|
1214
|
+
[RADII[2]]: `${Math.max(0, box.r[2])}px`,
|
|
1215
|
+
[RADII[3]]: `${Math.max(0, box.r[3])}px`,
|
|
1216
|
+
[EDGE_COLOR]: box.color,
|
|
1217
|
+
...weight(k),
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
1220
|
+
const from = place(flight.from, flight.fromK);
|
|
1221
|
+
const to = place(flight.to, flight.toK);
|
|
1222
|
+
if (flight.fromK === flight.toK)
|
|
1223
|
+
return [from, to];
|
|
1224
|
+
// The weight steps once, never tweens (a fractional edge paints soft):
|
|
1225
|
+
// two keyframes for the weight alone, at the same offset, on the eased
|
|
1226
|
+
// progress the effect's easing produces.
|
|
1227
|
+
return [
|
|
1228
|
+
{ ...from, offset: 0 },
|
|
1229
|
+
{ ...weight(flight.fromK), offset: WEIGHT_STEP },
|
|
1230
|
+
{ ...weight(flight.toK), offset: WEIGHT_STEP },
|
|
1231
|
+
{ ...to, offset: 1 },
|
|
1232
|
+
];
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* The box the frame lands on (or leaves from) on the popup: its border box
|
|
1236
|
+
* and radii, at edge weight `k` in `color` (the popup's own edge, or, for
|
|
1237
|
+
* a popup with no edge at all, the other end's). A side with no edge of its
|
|
1238
|
+
* own (a sheet's) that lies on the viewport's edge is met one weight past
|
|
1239
|
+
* it, so the frame's edge there is off screen at the hand-off; such a side
|
|
1240
|
+
* anywhere else leaves no frame to land on (null: the popup opens and
|
|
1241
|
+
* closes at once). `viewport` is the box fixed elements are laid out in.
|
|
1242
|
+
*/
|
|
1243
|
+
function landing(rect, frame, k, color, viewport) {
|
|
1244
|
+
const box = { ...rect, r: frame.r, color, k };
|
|
1245
|
+
if (frame.sides.every((side) => side >= k - 0.01))
|
|
1246
|
+
return box;
|
|
1247
|
+
const flush = [
|
|
1248
|
+
rect.y <= viewport.y + 0.5,
|
|
1249
|
+
rect.x + rect.w >= viewport.x + viewport.w - 0.5,
|
|
1250
|
+
rect.y + rect.h >= viewport.y + viewport.h - 0.5,
|
|
1251
|
+
rect.x <= viewport.x + 0.5,
|
|
1252
|
+
];
|
|
1253
|
+
const out = [0, 0, 0, 0];
|
|
1254
|
+
for (let side = 0; side < 4; side += 1) {
|
|
1255
|
+
if (frame.sides[side] >= k - 0.01)
|
|
1256
|
+
continue;
|
|
1257
|
+
if (!flush[side])
|
|
1258
|
+
return null;
|
|
1259
|
+
out[side] = k;
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
...box,
|
|
1263
|
+
x: rect.x - out[3],
|
|
1264
|
+
y: rect.y - out[0],
|
|
1265
|
+
w: rect.w + out[1] + out[3],
|
|
1266
|
+
h: rect.h + out[0] + out[2],
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* The popup's two clip keyframes: the frame's box relative to the popup,
|
|
1271
|
+
* drawn in by CLIP_INSET_PX (still under the frame's edge), so the frame's
|
|
1272
|
+
* whole-pixel rounding never lets the popup show past its outer edge.
|
|
1273
|
+
*/
|
|
1274
|
+
function clipKeyframes(flight) {
|
|
1275
|
+
const t = flight.target;
|
|
1276
|
+
const d = CLIP_INSET_PX;
|
|
1277
|
+
const clip = (box) => ({
|
|
1278
|
+
clipPath: `inset(${box.y - t.y + d}px ${t.x + t.w - (box.x + box.w) + d}px ${t.y + t.h - (box.y + box.h) + d}px ${box.x - t.x + d}px round ${radiiValue(box.r, d)})`,
|
|
1279
|
+
});
|
|
1280
|
+
return [clip(flight.from), clip(flight.to)];
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* How much of the tail shows (px, from its tip): the distance from its
|
|
1284
|
+
* tip-side edge to the frame's trigger-facing inner edge, for the frame box
|
|
1285
|
+
* `box` at edge weight `weight`. At or under 0 nothing shows; at `d` (the
|
|
1286
|
+
* frame on the popup's edge) all of it.
|
|
1287
|
+
*/
|
|
1288
|
+
function tailDepth(tail, box, weight) {
|
|
1289
|
+
switch (tail.normal) {
|
|
1290
|
+
case 'up':
|
|
1291
|
+
return box.y + weight - (tail.cy - tail.d / 2);
|
|
1292
|
+
case 'down':
|
|
1293
|
+
return tail.cy + tail.d / 2 - (box.y + box.h - weight);
|
|
1294
|
+
case 'left':
|
|
1295
|
+
return box.x + weight - (tail.cx - tail.d / 2);
|
|
1296
|
+
case 'right':
|
|
1297
|
+
return tail.cx + tail.d / 2 - (box.x + box.w - weight);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* The cap's clip, in its own unrotated box (the tip at the local top, the
|
|
1302
|
+
* base at the local bottom whatever the side, since the rotation applies
|
|
1303
|
+
* after the clip): the moving local bottom at the visible depth; the three
|
|
1304
|
+
* fixed sides one stroke width outside the box, so the tip's miter is never
|
|
1305
|
+
* cut.
|
|
1306
|
+
*/
|
|
1307
|
+
function tailClip(tail, depth, stroke) {
|
|
1308
|
+
// Unclamped above: an inset past the box is an empty shape, and keeping
|
|
1309
|
+
// the true value keeps the clip linear with the edge, so it opens exactly
|
|
1310
|
+
// when the edge reaches the tip.
|
|
1311
|
+
const bottom = Math.max(-stroke, tail.d - depth);
|
|
1312
|
+
return `inset(${-stroke}px ${-stroke}px ${bottom}px ${-stroke}px)`;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* The cap's two keyframes, on the frame's timing: its clip from the depth
|
|
1316
|
+
* at the frame's start box to the depth at its end box (both linear in the
|
|
1317
|
+
* eased progress, as the frame's box is, so the clip tracks the edge), and
|
|
1318
|
+
* its stroke color (`currentColor`) from the frame's start color to its end
|
|
1319
|
+
* color.
|
|
1320
|
+
*/
|
|
1321
|
+
function tailKeyframes(tail, flight) {
|
|
1322
|
+
const stroke = Math.max(flight.fromK, flight.toK);
|
|
1323
|
+
return [
|
|
1324
|
+
{
|
|
1325
|
+
clipPath: tailClip(tail, tailDepth(tail, flight.from, flight.fromK), stroke),
|
|
1326
|
+
color: flight.from.color,
|
|
1327
|
+
},
|
|
1328
|
+
{ clipPath: tailClip(tail, tailDepth(tail, flight.to, flight.toK), stroke), color: flight.to.color },
|
|
1329
|
+
];
|
|
1330
|
+
}
|
|
1331
|
+
//# sourceMappingURL=morphEngine.js.map
|