@pithyjs/ui-kit 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/animation/animate-in.d.ts +35 -0
- package/dist/animation/animate-in.d.ts.map +1 -0
- package/dist/animation/animate-in.js +71 -0
- package/dist/animation/animate-in.js.map +1 -0
- package/dist/atoms/actions/_icon-control.scss +53 -0
- package/dist/atoms/actions/button/Button.d.ts +93 -0
- package/dist/atoms/actions/button/Button.d.ts.map +1 -0
- package/dist/atoms/actions/button/Button.js +332 -0
- package/dist/atoms/actions/button/Button.js.map +1 -0
- package/dist/atoms/actions/button/button.scss +342 -0
- package/dist/atoms/actions/icon-button/IconButton.d.ts +47 -0
- package/dist/atoms/actions/icon-button/IconButton.d.ts.map +1 -0
- package/dist/atoms/actions/icon-button/IconButton.js +81 -0
- package/dist/atoms/actions/icon-button/IconButton.js.map +1 -0
- package/dist/atoms/actions/icon-button/icon-button.scss +20 -0
- package/dist/atoms/actions/icon-group/IconGroup.d.ts +60 -0
- package/dist/atoms/actions/icon-group/IconGroup.d.ts.map +1 -0
- package/dist/atoms/actions/icon-group/IconGroup.js +191 -0
- package/dist/atoms/actions/icon-group/IconGroup.js.map +1 -0
- package/dist/atoms/actions/icon-group/icon-group.scss +36 -0
- package/dist/atoms/actions/segmented-control/SegmentedControl.d.ts +49 -0
- package/dist/atoms/actions/segmented-control/SegmentedControl.d.ts.map +1 -0
- package/dist/atoms/actions/segmented-control/SegmentedControl.js +107 -0
- package/dist/atoms/actions/segmented-control/SegmentedControl.js.map +1 -0
- package/dist/atoms/actions/segmented-control/segmented-control.scss +90 -0
- package/dist/atoms/content/_mixins.scss +79 -0
- package/dist/atoms/content/action-tile/ActionTile.d.ts +27 -0
- package/dist/atoms/content/action-tile/ActionTile.d.ts.map +1 -0
- package/dist/atoms/content/action-tile/ActionTile.js +98 -0
- package/dist/atoms/content/action-tile/ActionTile.js.map +1 -0
- package/dist/atoms/content/action-tile/action-tile.scss +150 -0
- package/dist/atoms/content/badge/Badge.d.ts +57 -0
- package/dist/atoms/content/badge/Badge.d.ts.map +1 -0
- package/dist/atoms/content/badge/Badge.js +177 -0
- package/dist/atoms/content/badge/Badge.js.map +1 -0
- package/dist/atoms/content/badge/badge.scss +198 -0
- package/dist/atoms/content/heading/Heading.d.ts +53 -0
- package/dist/atoms/content/heading/Heading.d.ts.map +1 -0
- package/dist/atoms/content/heading/Heading.js +142 -0
- package/dist/atoms/content/heading/Heading.js.map +1 -0
- package/dist/atoms/content/heading/heading.scss +43 -0
- package/dist/atoms/content/icon/Icon.d.ts +68 -0
- package/dist/atoms/content/icon/Icon.d.ts.map +1 -0
- package/dist/atoms/content/icon/Icon.js +137 -0
- package/dist/atoms/content/icon/Icon.js.map +1 -0
- package/dist/atoms/content/icon/icon.scss +66 -0
- package/dist/atoms/content/icon/resolveIconSvg.d.ts +21 -0
- package/dist/atoms/content/icon/resolveIconSvg.d.ts.map +1 -0
- package/dist/atoms/content/icon/resolveIconSvg.js +73 -0
- package/dist/atoms/content/icon/resolveIconSvg.js.map +1 -0
- package/dist/atoms/content/image/Image.d.ts +81 -0
- package/dist/atoms/content/image/Image.d.ts.map +1 -0
- package/dist/atoms/content/image/Image.js +293 -0
- package/dist/atoms/content/image/Image.js.map +1 -0
- package/dist/atoms/content/image/image.scss +77 -0
- package/dist/atoms/content/text/Text.d.ts +58 -0
- package/dist/atoms/content/text/Text.d.ts.map +1 -0
- package/dist/atoms/content/text/Text.js +158 -0
- package/dist/atoms/content/text/Text.js.map +1 -0
- package/dist/atoms/content/text/text.scss +98 -0
- package/dist/atoms/content/tile/Tile.d.ts +66 -0
- package/dist/atoms/content/tile/Tile.d.ts.map +1 -0
- package/dist/atoms/content/tile/Tile.js +125 -0
- package/dist/atoms/content/tile/Tile.js.map +1 -0
- package/dist/atoms/content/tile/tile.scss +122 -0
- package/dist/atoms/feedback/tooltip/Tooltip.d.ts +44 -0
- package/dist/atoms/feedback/tooltip/Tooltip.d.ts.map +1 -0
- package/dist/atoms/feedback/tooltip/Tooltip.js +121 -0
- package/dist/atoms/feedback/tooltip/Tooltip.js.map +1 -0
- package/dist/atoms/feedback/tooltip/tooltip.scss +96 -0
- package/dist/atoms/forms/checkbox/Checkbox.d.ts +49 -0
- package/dist/atoms/forms/checkbox/Checkbox.d.ts.map +1 -0
- package/dist/atoms/forms/checkbox/Checkbox.js +269 -0
- package/dist/atoms/forms/checkbox/Checkbox.js.map +1 -0
- package/dist/atoms/forms/checkbox/CheckboxGroup.d.ts +50 -0
- package/dist/atoms/forms/checkbox/CheckboxGroup.d.ts.map +1 -0
- package/dist/atoms/forms/checkbox/CheckboxGroup.js +132 -0
- package/dist/atoms/forms/checkbox/CheckboxGroup.js.map +1 -0
- package/dist/atoms/forms/checkbox/checkbox.scss +506 -0
- package/dist/atoms/forms/file-picker/FilePicker.d.ts +23 -0
- package/dist/atoms/forms/file-picker/FilePicker.d.ts.map +1 -0
- package/dist/atoms/forms/file-picker/FilePicker.js +103 -0
- package/dist/atoms/forms/file-picker/FilePicker.js.map +1 -0
- package/dist/atoms/forms/file-picker/file-picker.scss +84 -0
- package/dist/atoms/forms/form-field/FormField.d.ts +39 -0
- package/dist/atoms/forms/form-field/FormField.d.ts.map +1 -0
- package/dist/atoms/forms/form-field/FormField.js +139 -0
- package/dist/atoms/forms/form-field/FormField.js.map +1 -0
- package/dist/atoms/forms/form-field/form-field.scss +46 -0
- package/dist/atoms/forms/input/Input.d.ts +81 -0
- package/dist/atoms/forms/input/Input.d.ts.map +1 -0
- package/dist/atoms/forms/input/Input.js +417 -0
- package/dist/atoms/forms/input/Input.js.map +1 -0
- package/dist/atoms/forms/input/input.scss +547 -0
- package/dist/atoms/forms/radio/Radio.d.ts +54 -0
- package/dist/atoms/forms/radio/Radio.d.ts.map +1 -0
- package/dist/atoms/forms/radio/Radio.js +278 -0
- package/dist/atoms/forms/radio/Radio.js.map +1 -0
- package/dist/atoms/forms/radio/RadioGroup.d.ts +54 -0
- package/dist/atoms/forms/radio/RadioGroup.d.ts.map +1 -0
- package/dist/atoms/forms/radio/RadioGroup.js +137 -0
- package/dist/atoms/forms/radio/RadioGroup.js.map +1 -0
- package/dist/atoms/forms/radio/radio.scss +459 -0
- package/dist/atoms/forms/select/Select.d.ts +57 -0
- package/dist/atoms/forms/select/Select.d.ts.map +1 -0
- package/dist/atoms/forms/select/Select.js +457 -0
- package/dist/atoms/forms/select/Select.js.map +1 -0
- package/dist/atoms/forms/select/select-dropdown.d.ts +49 -0
- package/dist/atoms/forms/select/select-dropdown.d.ts.map +1 -0
- package/dist/atoms/forms/select/select-dropdown.js +305 -0
- package/dist/atoms/forms/select/select-dropdown.js.map +1 -0
- package/dist/atoms/forms/select/select-keyboard.d.ts +14 -0
- package/dist/atoms/forms/select/select-keyboard.d.ts.map +1 -0
- package/dist/atoms/forms/select/select-keyboard.js +182 -0
- package/dist/atoms/forms/select/select-keyboard.js.map +1 -0
- package/dist/atoms/forms/select/select.scss +630 -0
- package/dist/atoms/forms/textarea/Textarea.d.ts +69 -0
- package/dist/atoms/forms/textarea/Textarea.d.ts.map +1 -0
- package/dist/atoms/forms/textarea/Textarea.js +394 -0
- package/dist/atoms/forms/textarea/Textarea.js.map +1 -0
- package/dist/atoms/forms/textarea/textarea.scss +486 -0
- package/dist/atoms/forms/toggle/Toggle.d.ts +47 -0
- package/dist/atoms/forms/toggle/Toggle.d.ts.map +1 -0
- package/dist/atoms/forms/toggle/Toggle.js +243 -0
- package/dist/atoms/forms/toggle/Toggle.js.map +1 -0
- package/dist/atoms/forms/toggle/toggle.scss +561 -0
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +18 -0
- package/dist/compiler.js.map +1 -0
- package/dist/icons/index.d.ts +14 -0
- package/dist/icons/index.d.ts.map +1 -0
- package/dist/icons/index.js +14 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/icons/registry.d.ts +65 -0
- package/dist/icons/registry.d.ts.map +1 -0
- package/dist/icons/registry.js +126 -0
- package/dist/icons/registry.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/Grid.d.ts +78 -0
- package/dist/layout/Grid.d.ts.map +1 -0
- package/dist/layout/Grid.js +272 -0
- package/dist/layout/Grid.js.map +1 -0
- package/dist/layout/GridItem.d.ts +52 -0
- package/dist/layout/GridItem.d.ts.map +1 -0
- package/dist/layout/GridItem.js +99 -0
- package/dist/layout/GridItem.js.map +1 -0
- package/dist/layout/PageSection.d.ts +43 -0
- package/dist/layout/PageSection.d.ts.map +1 -0
- package/dist/layout/PageSection.js +75 -0
- package/dist/layout/PageSection.js.map +1 -0
- package/dist/layout/SmartContainer.d.ts +66 -0
- package/dist/layout/SmartContainer.d.ts.map +1 -0
- package/dist/layout/SmartContainer.js +194 -0
- package/dist/layout/SmartContainer.js.map +1 -0
- package/dist/layout/Stack.d.ts +65 -0
- package/dist/layout/Stack.d.ts.map +1 -0
- package/dist/layout/Stack.js +114 -0
- package/dist/layout/Stack.js.map +1 -0
- package/dist/organisms/content/Card.d.ts +93 -0
- package/dist/organisms/content/Card.d.ts.map +1 -0
- package/dist/organisms/content/Card.js +353 -0
- package/dist/organisms/content/Card.js.map +1 -0
- package/dist/organisms/content/_card-horizontal.scss +88 -0
- package/dist/organisms/content/_card-link.scss +49 -0
- package/dist/organisms/content/_card-overlay.scss +91 -0
- package/dist/organisms/content/_card-root.scss +99 -0
- package/dist/organisms/content/_card-slots.scss +143 -0
- package/dist/organisms/content/_card-states.scss +166 -0
- package/dist/organisms/content/_card-tokens.scss +22 -0
- package/dist/organisms/content/_card-variants.scss +31 -0
- package/dist/organisms/content/card.scss +15 -0
- package/dist/organisms/feedback/Modal.d.ts +39 -0
- package/dist/organisms/feedback/Modal.d.ts.map +1 -0
- package/dist/organisms/feedback/Modal.js +120 -0
- package/dist/organisms/feedback/Modal.js.map +1 -0
- package/dist/organisms/feedback/Popover.d.ts +30 -0
- package/dist/organisms/feedback/Popover.d.ts.map +1 -0
- package/dist/organisms/feedback/Popover.js +151 -0
- package/dist/organisms/feedback/Popover.js.map +1 -0
- package/dist/organisms/feedback/modal.scss +66 -0
- package/dist/organisms/feedback/popover.scss +27 -0
- package/dist/organisms/forms/DynamicForm.d.ts +118 -0
- package/dist/organisms/forms/DynamicForm.d.ts.map +1 -0
- package/dist/organisms/forms/DynamicForm.js +546 -0
- package/dist/organisms/forms/DynamicForm.js.map +1 -0
- package/dist/organisms/forms/Form.d.ts +62 -0
- package/dist/organisms/forms/Form.d.ts.map +1 -0
- package/dist/organisms/forms/Form.js +131 -0
- package/dist/organisms/forms/Form.js.map +1 -0
- package/dist/organisms/forms/dynamic-form.scss +91 -0
- package/dist/organisms/forms/form-helpers.d.ts +21 -0
- package/dist/organisms/forms/form-helpers.d.ts.map +1 -0
- package/dist/organisms/forms/form-helpers.js +57 -0
- package/dist/organisms/forms/form-helpers.js.map +1 -0
- package/dist/organisms/forms/form-state.d.ts +66 -0
- package/dist/organisms/forms/form-state.d.ts.map +1 -0
- package/dist/organisms/forms/form-state.js +156 -0
- package/dist/organisms/forms/form-state.js.map +1 -0
- package/dist/organisms/forms/form.scss +23 -0
- package/dist/organisms/forms/validation.d.ts +32 -0
- package/dist/organisms/forms/validation.d.ts.map +1 -0
- package/dist/organisms/forms/validation.js +118 -0
- package/dist/organisms/forms/validation.js.map +1 -0
- package/dist/organisms/navigation/Navbar.d.ts +66 -0
- package/dist/organisms/navigation/Navbar.d.ts.map +1 -0
- package/dist/organisms/navigation/Navbar.js +881 -0
- package/dist/organisms/navigation/Navbar.js.map +1 -0
- package/dist/organisms/navigation/_navbar-announcement.scss +62 -0
- package/dist/organisms/navigation/_navbar-brand.scss +69 -0
- package/dist/organisms/navigation/_navbar-container.scss +37 -0
- package/dist/organisms/navigation/_navbar-desktop.scss +116 -0
- package/dist/organisms/navigation/_navbar-drawer.scss +97 -0
- package/dist/organisms/navigation/_navbar-links.scss +143 -0
- package/dist/organisms/navigation/_navbar-menu.scss +193 -0
- package/dist/organisms/navigation/_navbar-root.scss +107 -0
- package/dist/organisms/navigation/_navbar-search.scss +83 -0
- package/dist/organisms/navigation/_navbar-toggle.scss +91 -0
- package/dist/organisms/navigation/_navbar-tokens.scss +15 -0
- package/dist/organisms/navigation/navbar.scss +17 -0
- package/dist/registry/categories.d.ts +6 -0
- package/dist/registry/categories.d.ts.map +1 -0
- package/dist/registry/categories.js +13 -0
- package/dist/registry/categories.js.map +1 -0
- package/dist/registry/components.d.ts +5 -0
- package/dist/registry/components.d.ts.map +1 -0
- package/dist/registry/components.js +95 -0
- package/dist/registry/components.js.map +1 -0
- package/dist/registry/index.d.ts +59 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +79 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/levels.d.ts +6 -0
- package/dist/registry/levels.d.ts.map +1 -0
- package/dist/registry/levels.js +9 -0
- package/dist/registry/levels.js.map +1 -0
- package/dist/registry/registry-meta-types.d.ts +67 -0
- package/dist/registry/registry-meta-types.d.ts.map +1 -0
- package/dist/registry/registry-meta-types.js +6 -0
- package/dist/registry/registry-meta-types.js.map +1 -0
- package/dist/registry/registry-meta.d.ts +5 -0
- package/dist/registry/registry-meta.d.ts.map +1 -0
- package/dist/registry/registry-meta.js +731 -0
- package/dist/registry/registry-meta.js.map +1 -0
- package/dist/registry/renderer.d.ts +69 -0
- package/dist/registry/renderer.d.ts.map +1 -0
- package/dist/registry/renderer.js +311 -0
- package/dist/registry/renderer.js.map +1 -0
- package/dist/sections/_section-base.scss +204 -0
- package/dist/sections/contact/ContactSection.d.ts +55 -0
- package/dist/sections/contact/ContactSection.d.ts.map +1 -0
- package/dist/sections/contact/ContactSection.js +142 -0
- package/dist/sections/contact/ContactSection.js.map +1 -0
- package/dist/sections/contact/contact-section.scss +142 -0
- package/dist/sections/cta/CTASection.d.ts +52 -0
- package/dist/sections/cta/CTASection.d.ts.map +1 -0
- package/dist/sections/cta/CTASection.js +130 -0
- package/dist/sections/cta/CTASection.js.map +1 -0
- package/dist/sections/cta/cta-section.scss +131 -0
- package/dist/sections/entrance.d.ts +29 -0
- package/dist/sections/entrance.d.ts.map +1 -0
- package/dist/sections/entrance.js +69 -0
- package/dist/sections/entrance.js.map +1 -0
- package/dist/sections/features/FeaturesSection.d.ts +54 -0
- package/dist/sections/features/FeaturesSection.d.ts.map +1 -0
- package/dist/sections/features/FeaturesSection.js +139 -0
- package/dist/sections/features/FeaturesSection.js.map +1 -0
- package/dist/sections/features/features-section.scss +140 -0
- package/dist/sections/footer/FooterSection.d.ts +53 -0
- package/dist/sections/footer/FooterSection.d.ts.map +1 -0
- package/dist/sections/footer/FooterSection.js +147 -0
- package/dist/sections/footer/FooterSection.js.map +1 -0
- package/dist/sections/footer/footer-section.scss +131 -0
- package/dist/sections/hero/HeroSection.d.ts +66 -0
- package/dist/sections/hero/HeroSection.d.ts.map +1 -0
- package/dist/sections/hero/HeroSection.js +156 -0
- package/dist/sections/hero/HeroSection.js.map +1 -0
- package/dist/sections/hero/hero-section.scss +211 -0
- package/dist/shared/_form-animations.scss +283 -0
- package/dist/shared/_media-fit.scss +63 -0
- package/dist/shared/_mixins.scss +69 -0
- package/dist/shared/_tokens.scss +116 -0
- package/dist/shared/badge.d.ts +15 -0
- package/dist/shared/badge.d.ts.map +1 -0
- package/dist/shared/badge.js +15 -0
- package/dist/shared/badge.js.map +1 -0
- package/dist/shared/button.d.ts +22 -0
- package/dist/shared/button.d.ts.map +1 -0
- package/dist/shared/button.js +22 -0
- package/dist/shared/button.js.map +1 -0
- package/dist/shared/card.d.ts +21 -0
- package/dist/shared/card.d.ts.map +1 -0
- package/dist/shared/card.js +38 -0
- package/dist/shared/card.js.map +1 -0
- package/dist/shared/checkbox.d.ts +10 -0
- package/dist/shared/checkbox.d.ts.map +1 -0
- package/dist/shared/checkbox.js +28 -0
- package/dist/shared/checkbox.js.map +1 -0
- package/dist/shared/clickable.d.ts +34 -0
- package/dist/shared/clickable.d.ts.map +1 -0
- package/dist/shared/clickable.js +54 -0
- package/dist/shared/clickable.js.map +1 -0
- package/dist/shared/common.d.ts +31 -0
- package/dist/shared/common.d.ts.map +1 -0
- package/dist/shared/common.js +92 -0
- package/dist/shared/common.js.map +1 -0
- package/dist/shared/entrance-config.d.ts +46 -0
- package/dist/shared/entrance-config.d.ts.map +1 -0
- package/dist/shared/entrance-config.js +54 -0
- package/dist/shared/entrance-config.js.map +1 -0
- package/dist/shared/focusable.d.ts +8 -0
- package/dist/shared/focusable.d.ts.map +1 -0
- package/dist/shared/focusable.js +17 -0
- package/dist/shared/focusable.js.map +1 -0
- package/dist/shared/form-elements.d.ts +57 -0
- package/dist/shared/form-elements.d.ts.map +1 -0
- package/dist/shared/form-elements.js +95 -0
- package/dist/shared/form-elements.js.map +1 -0
- package/dist/shared/form-state.d.ts +52 -0
- package/dist/shared/form-state.d.ts.map +1 -0
- package/dist/shared/form-state.js +151 -0
- package/dist/shared/form-state.js.map +1 -0
- package/dist/shared/grid.d.ts +21 -0
- package/dist/shared/grid.d.ts.map +1 -0
- package/dist/shared/grid.js +187 -0
- package/dist/shared/grid.js.map +1 -0
- package/dist/shared/icon-slot.d.ts +22 -0
- package/dist/shared/icon-slot.d.ts.map +1 -0
- package/dist/shared/icon-slot.js +38 -0
- package/dist/shared/icon-slot.js.map +1 -0
- package/dist/shared/icon.d.ts +17 -0
- package/dist/shared/icon.d.ts.map +1 -0
- package/dist/shared/icon.js +18 -0
- package/dist/shared/icon.js.map +1 -0
- package/dist/shared/icons.d.ts +19 -0
- package/dist/shared/icons.d.ts.map +1 -0
- package/dist/shared/icons.js +22 -0
- package/dist/shared/icons.js.map +1 -0
- package/dist/shared/image.d.ts +35 -0
- package/dist/shared/image.d.ts.map +1 -0
- package/dist/shared/image.js +46 -0
- package/dist/shared/image.js.map +1 -0
- package/dist/shared/input.d.ts +12 -0
- package/dist/shared/input.d.ts.map +1 -0
- package/dist/shared/input.js +29 -0
- package/dist/shared/input.js.map +1 -0
- package/dist/shared/radio.d.ts +10 -0
- package/dist/shared/radio.d.ts.map +1 -0
- package/dist/shared/radio.js +29 -0
- package/dist/shared/radio.js.map +1 -0
- package/dist/shared/reconcile-list.d.ts +37 -0
- package/dist/shared/reconcile-list.d.ts.map +1 -0
- package/dist/shared/reconcile-list.js +95 -0
- package/dist/shared/reconcile-list.js.map +1 -0
- package/dist/shared/section.d.ts +23 -0
- package/dist/shared/section.d.ts.map +1 -0
- package/dist/shared/section.js +65 -0
- package/dist/shared/section.js.map +1 -0
- package/dist/shared/segmented.d.ts +17 -0
- package/dist/shared/segmented.d.ts.map +1 -0
- package/dist/shared/segmented.js +53 -0
- package/dist/shared/segmented.js.map +1 -0
- package/dist/shared/select.d.ts +18 -0
- package/dist/shared/select.d.ts.map +1 -0
- package/dist/shared/select.js +14 -0
- package/dist/shared/select.js.map +1 -0
- package/dist/shared/slots.d.ts +11 -0
- package/dist/shared/slots.d.ts.map +1 -0
- package/dist/shared/slots.js +33 -0
- package/dist/shared/slots.js.map +1 -0
- package/dist/shared/smart-container.d.ts +19 -0
- package/dist/shared/smart-container.d.ts.map +1 -0
- package/dist/shared/smart-container.js +29 -0
- package/dist/shared/smart-container.js.map +1 -0
- package/dist/shared/text.d.ts +29 -0
- package/dist/shared/text.d.ts.map +1 -0
- package/dist/shared/text.js +72 -0
- package/dist/shared/text.js.map +1 -0
- package/dist/shared/textarea.d.ts +6 -0
- package/dist/shared/textarea.d.ts.map +1 -0
- package/dist/shared/textarea.js +18 -0
- package/dist/shared/textarea.js.map +1 -0
- package/dist/shared/toggle.d.ts +6 -0
- package/dist/shared/toggle.d.ts.map +1 -0
- package/dist/shared/toggle.js +19 -0
- package/dist/shared/toggle.js.map +1 -0
- package/dist/shared/types.d.ts +41 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +20 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/structural/ErrorBoundary.d.ts +18 -0
- package/dist/structural/ErrorBoundary.d.ts.map +1 -0
- package/dist/structural/ErrorBoundary.js +119 -0
- package/dist/structural/ErrorBoundary.js.map +1 -0
- package/dist/structural/For.d.ts +37 -0
- package/dist/structural/For.d.ts.map +1 -0
- package/dist/structural/For.js +129 -0
- package/dist/structural/For.js.map +1 -0
- package/dist/structural/Lazy.d.ts +43 -0
- package/dist/structural/Lazy.d.ts.map +1 -0
- package/dist/structural/Lazy.js +47 -0
- package/dist/structural/Lazy.js.map +1 -0
- package/dist/structural/NavLink.d.ts +45 -0
- package/dist/structural/NavLink.d.ts.map +1 -0
- package/dist/structural/NavLink.js +37 -0
- package/dist/structural/NavLink.js.map +1 -0
- package/dist/structural/Show.d.ts +35 -0
- package/dist/structural/Show.d.ts.map +1 -0
- package/dist/structural/Show.js +34 -0
- package/dist/structural/Show.js.map +1 -0
- package/dist/structural/nav-link.scss +54 -0
- package/dist/styles/index.scss +58 -0
- package/package.json +64 -0
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.ui-kit.navbar",
|
|
4
|
+
"title": "<Navbar> Organism",
|
|
5
|
+
"category": "feature",
|
|
6
|
+
"risk": "high"
|
|
7
|
+
}
|
|
8
|
+
*/
|
|
9
|
+
import { captureChildren } from '@pithyjs/core';
|
|
10
|
+
import { isSafeHref } from '@pithyjs/shared';
|
|
11
|
+
import { getGlobalRouter, routerLink, cleanupRouterDirectives, PREFETCH_STRATEGIES, } from '@pithyjs/router';
|
|
12
|
+
import { nextUid } from '../../shared/common';
|
|
13
|
+
import { captureSlot, extractSlot } from '../../shared/slots';
|
|
14
|
+
import { validateEnum } from '../../shared/section';
|
|
15
|
+
import { HAMBURGER_ICON, CLOSE_ICON } from '../../shared/icons';
|
|
16
|
+
function isItemActive(current, target, exact) {
|
|
17
|
+
if (!current || !target)
|
|
18
|
+
return false;
|
|
19
|
+
if (exact)
|
|
20
|
+
return current === target;
|
|
21
|
+
if (target === '/')
|
|
22
|
+
return current === '/';
|
|
23
|
+
return current === target || current.startsWith(target + '/');
|
|
24
|
+
}
|
|
25
|
+
// Ref-counted global state so concurrent drawers don't clobber each other's
|
|
26
|
+
// snapshots. Last releaser restores the body overflow; each element's inert
|
|
27
|
+
// count only drops to zero when every drawer that requested it has released.
|
|
28
|
+
let scrollLockRefs = 0;
|
|
29
|
+
let originalBodyOverflow = '';
|
|
30
|
+
const inertRefs = new WeakMap();
|
|
31
|
+
function acquireScrollLock() {
|
|
32
|
+
if (scrollLockRefs === 0) {
|
|
33
|
+
originalBodyOverflow = document.body.style.overflow;
|
|
34
|
+
document.body.style.overflow = 'hidden';
|
|
35
|
+
}
|
|
36
|
+
scrollLockRefs += 1;
|
|
37
|
+
}
|
|
38
|
+
function releaseScrollLock() {
|
|
39
|
+
if (scrollLockRefs === 0)
|
|
40
|
+
return;
|
|
41
|
+
scrollLockRefs -= 1;
|
|
42
|
+
if (scrollLockRefs === 0) {
|
|
43
|
+
document.body.style.overflow = originalBodyOverflow;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function acquireInert(el) {
|
|
47
|
+
const count = inertRefs.get(el) ?? 0;
|
|
48
|
+
if (count === 0)
|
|
49
|
+
el.setAttribute('inert', '');
|
|
50
|
+
inertRefs.set(el, count + 1);
|
|
51
|
+
}
|
|
52
|
+
function releaseInert(el) {
|
|
53
|
+
const count = inertRefs.get(el) ?? 0;
|
|
54
|
+
if (count <= 1) {
|
|
55
|
+
inertRefs.delete(el);
|
|
56
|
+
el.removeAttribute('inert');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
inertRefs.set(el, count - 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Test-only: reset the module-level ref counters so one test's leaked state
|
|
64
|
+
* (e.g. a nav that opened but never closed) can't skew the next test's
|
|
65
|
+
* acquire logic. Not exported from the public barrel.
|
|
66
|
+
*/
|
|
67
|
+
export function __resetNavbarGlobalLocksForTest() {
|
|
68
|
+
scrollLockRefs = 0;
|
|
69
|
+
originalBodyOverflow = '';
|
|
70
|
+
}
|
|
71
|
+
/** @codexApi {"parent":"pithy.ui-kit.navbar","name":"Navbar","stability":"stable","signature":"(props: NavbarProps) => { el: HTMLElement; destroy: () => void }"} */
|
|
72
|
+
export function Navbar(props) {
|
|
73
|
+
const { label, brand, brandImageSrc, brandImageAlt, brandHref = '/', items, sticky = false, transparent = false, density = 'comfortable', skipToContentTarget = 'main-content', id, } = props;
|
|
74
|
+
if (import.meta.env?.DEV) {
|
|
75
|
+
if (typeof label !== 'string' || label.length === 0) {
|
|
76
|
+
console.warn('[<Navbar>] `label` is required — <nav> needs an accessible name. A page may contain multiple <nav> elements.');
|
|
77
|
+
}
|
|
78
|
+
if (brandImageSrc && !brandImageAlt) {
|
|
79
|
+
console.warn('[<Navbar>] `brandImageAlt` is required when `brandImageSrc` is set — screen readers need an accessible name for the logo image.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const safeDensity = validateEnum('Navbar', 'density', density, ['comfortable', 'compact'], 'comfortable');
|
|
83
|
+
const menuId = nextUid('ui-navbar-menu');
|
|
84
|
+
const disposers = [];
|
|
85
|
+
// Capture projected children BEFORE calling any inner components, per the
|
|
86
|
+
// section-template pattern. We re-home nodes into the correct slot below
|
|
87
|
+
// based on their `data-slot` attribute.
|
|
88
|
+
const { el: childFragment, destroy: destroyChildren } = captureChildren();
|
|
89
|
+
disposers.push(destroyChildren);
|
|
90
|
+
const nav = document.createElement('nav');
|
|
91
|
+
nav.classList.add('ui-navbar');
|
|
92
|
+
const safeLabel = typeof label === 'string' && label.length > 0 ? label : 'Navigation';
|
|
93
|
+
nav.setAttribute('aria-label', safeLabel);
|
|
94
|
+
nav.style.viewTransitionName = `pithy-${menuId}`;
|
|
95
|
+
nav.dataset.density = safeDensity;
|
|
96
|
+
if (sticky)
|
|
97
|
+
nav.dataset.sticky = 'true';
|
|
98
|
+
if (transparent)
|
|
99
|
+
nav.dataset.transparent = 'true';
|
|
100
|
+
if (id)
|
|
101
|
+
nav.id = id;
|
|
102
|
+
// Must stay the first focusable element so Tab lands on it before anything.
|
|
103
|
+
const skip = document.createElement('a');
|
|
104
|
+
skip.classList.add('ui-navbar-skip');
|
|
105
|
+
skip.href = `#${skipToContentTarget}`;
|
|
106
|
+
skip.textContent = 'Skip to content';
|
|
107
|
+
nav.appendChild(skip);
|
|
108
|
+
// Mounted after the skip link so skip stays the first tab stop.
|
|
109
|
+
const projectedAnnouncement = extractSlot(childFragment, 'announcement');
|
|
110
|
+
if (projectedAnnouncement) {
|
|
111
|
+
const bar = document.createElement('div');
|
|
112
|
+
bar.classList.add('ui-navbar-announcement');
|
|
113
|
+
captureSlot(bar, 'announcement');
|
|
114
|
+
bar.appendChild(projectedAnnouncement);
|
|
115
|
+
nav.appendChild(bar);
|
|
116
|
+
}
|
|
117
|
+
const container = document.createElement('div');
|
|
118
|
+
container.classList.add('ui-navbar-container');
|
|
119
|
+
nav.appendChild(container);
|
|
120
|
+
const brandHasImage = typeof brandImageSrc === 'string' && brandImageSrc.length > 0;
|
|
121
|
+
const brandHasText = typeof brand === 'string' && brand.length > 0;
|
|
122
|
+
const safeBrandHref = typeof brandHref === 'string' && isSafeHref(brandHref) ? brandHref : '/';
|
|
123
|
+
// Shared creation logic for the brand mark. Called once for the main brand
|
|
124
|
+
// link and once for the drawer brand so both sites emit identical markup
|
|
125
|
+
// and attributes (the DOM does contain two <img> elements by design).
|
|
126
|
+
const appendBrandContent = (parent) => {
|
|
127
|
+
if (brandHasImage) {
|
|
128
|
+
const img = document.createElement('img');
|
|
129
|
+
img.classList.add('ui-navbar-brand-logo');
|
|
130
|
+
img.src = brandImageSrc;
|
|
131
|
+
// Alt is required when image is set; fall back to empty string so the
|
|
132
|
+
// image is treated as decorative rather than announced as the URL.
|
|
133
|
+
img.alt = typeof brandImageAlt === 'string' ? brandImageAlt : '';
|
|
134
|
+
img.setAttribute('loading', 'eager');
|
|
135
|
+
img.setAttribute('decoding', 'async');
|
|
136
|
+
parent.appendChild(img);
|
|
137
|
+
}
|
|
138
|
+
if (brandHasText) {
|
|
139
|
+
const wordmark = document.createElement('span');
|
|
140
|
+
wordmark.classList.add('ui-navbar-brand-text');
|
|
141
|
+
wordmark.textContent = brand;
|
|
142
|
+
parent.appendChild(wordmark);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const projectedBrand = extractSlot(childFragment, 'brand');
|
|
146
|
+
if (brandHasImage || brandHasText || projectedBrand) {
|
|
147
|
+
const brandLink = document.createElement('a');
|
|
148
|
+
brandLink.classList.add('ui-navbar-brand');
|
|
149
|
+
captureSlot(brandLink, 'brand');
|
|
150
|
+
brandLink.href = safeBrandHref;
|
|
151
|
+
if (projectedBrand) {
|
|
152
|
+
brandLink.appendChild(projectedBrand);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
appendBrandContent(brandLink);
|
|
156
|
+
}
|
|
157
|
+
container.appendChild(brandLink);
|
|
158
|
+
}
|
|
159
|
+
const toggle = document.createElement('button');
|
|
160
|
+
toggle.type = 'button';
|
|
161
|
+
toggle.classList.add('ui-navbar-toggle');
|
|
162
|
+
toggle.setAttribute('aria-label', 'Open menu');
|
|
163
|
+
// Native Invoker Commands: the button opens/closes the menu popover,
|
|
164
|
+
// and the browser syncs aria-expanded/aria-controls for us.
|
|
165
|
+
toggle.setAttribute('commandfor', menuId);
|
|
166
|
+
toggle.setAttribute('command', 'toggle-popover');
|
|
167
|
+
// aria-controls pairs with commandfor but also provides a fallback for
|
|
168
|
+
// AT that don't yet surface Invoker Command relationships.
|
|
169
|
+
toggle.setAttribute('aria-controls', menuId);
|
|
170
|
+
toggle.innerHTML = HAMBURGER_ICON;
|
|
171
|
+
container.appendChild(toggle);
|
|
172
|
+
const menu = document.createElement('div');
|
|
173
|
+
menu.id = menuId;
|
|
174
|
+
menu.classList.add('ui-navbar-menu');
|
|
175
|
+
menu.setAttribute('popover', 'auto');
|
|
176
|
+
container.appendChild(menu);
|
|
177
|
+
// Close affordance mirrored inside the drawer because on mobile the outer
|
|
178
|
+
// hamburger sits below the popover ::backdrop and can't be tapped.
|
|
179
|
+
const drawerHeader = document.createElement('div');
|
|
180
|
+
drawerHeader.classList.add('ui-navbar-drawer-header');
|
|
181
|
+
if (brandHasImage || brandHasText) {
|
|
182
|
+
const drawerBrand = document.createElement('a');
|
|
183
|
+
drawerBrand.classList.add('ui-navbar-drawer-brand');
|
|
184
|
+
drawerBrand.href = safeBrandHref;
|
|
185
|
+
appendBrandContent(drawerBrand);
|
|
186
|
+
drawerHeader.appendChild(drawerBrand);
|
|
187
|
+
}
|
|
188
|
+
const closeInDrawer = document.createElement('button');
|
|
189
|
+
closeInDrawer.type = 'button';
|
|
190
|
+
closeInDrawer.classList.add('ui-navbar-close');
|
|
191
|
+
closeInDrawer.setAttribute('aria-label', 'Close menu');
|
|
192
|
+
closeInDrawer.setAttribute('commandfor', menuId);
|
|
193
|
+
closeInDrawer.setAttribute('command', 'hide-popover');
|
|
194
|
+
closeInDrawer.innerHTML = CLOSE_ICON;
|
|
195
|
+
drawerHeader.appendChild(closeInDrawer);
|
|
196
|
+
menu.appendChild(drawerHeader);
|
|
197
|
+
// popover=auto doesn't auto-close on internal activation, and routerLink
|
|
198
|
+
// calls stopPropagation(), so a bubble-phase listener here never fires on
|
|
199
|
+
// link clicks. Capture-phase runs before the anchor's handler; we defer
|
|
200
|
+
// hidePopover() one microtask so navigation still wins.
|
|
201
|
+
const closeMenuOnClick = (event) => {
|
|
202
|
+
const target = event.target;
|
|
203
|
+
if (!target)
|
|
204
|
+
return;
|
|
205
|
+
if (!target.closest('a, button'))
|
|
206
|
+
return;
|
|
207
|
+
if (target.closest('.ui-navbar-close'))
|
|
208
|
+
return;
|
|
209
|
+
if (!isMenuOpen)
|
|
210
|
+
return;
|
|
211
|
+
queueMicrotask(() => {
|
|
212
|
+
try {
|
|
213
|
+
menu.hidePopover();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* already closed */
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
menu.addEventListener('click', closeMenuOnClick, true);
|
|
221
|
+
disposers.push(() => menu.removeEventListener('click', closeMenuOnClick, true));
|
|
222
|
+
// Long-press on a drawer link in touch mode opens the browser's context
|
|
223
|
+
// menu, which can leave the popover/body-scroll state inconsistent when
|
|
224
|
+
// dismissed. Suppress it for link items only so desktop right-click on
|
|
225
|
+
// chrome (close button, brand) still works normally.
|
|
226
|
+
const suppressLinkContextMenu = (event) => {
|
|
227
|
+
const target = event.target;
|
|
228
|
+
if (target?.closest('.ui-navbar-link'))
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
};
|
|
231
|
+
menu.addEventListener('contextmenu', suppressLinkContextMenu);
|
|
232
|
+
disposers.push(() => menu.removeEventListener('contextmenu', suppressLinkContextMenu));
|
|
233
|
+
const list = document.createElement('ul');
|
|
234
|
+
list.classList.add('ui-navbar-list', 'u-list-reset');
|
|
235
|
+
// Safari/VoiceOver strips list semantics when list-style:none is applied
|
|
236
|
+
// via CSS. Explicit role restores "list, N items" announcement.
|
|
237
|
+
list.setAttribute('role', 'list');
|
|
238
|
+
menu.appendChild(list);
|
|
239
|
+
// Wrap the decorative indicator in an <li> so the <ul> contains only valid
|
|
240
|
+
// children. role="presentation" prevents it from inflating the list count.
|
|
241
|
+
const indicatorLi = document.createElement('li');
|
|
242
|
+
indicatorLi.setAttribute('role', 'presentation');
|
|
243
|
+
indicatorLi.setAttribute('aria-hidden', 'true');
|
|
244
|
+
const indicator = document.createElement('span');
|
|
245
|
+
indicator.classList.add('ui-navbar-indicator');
|
|
246
|
+
indicatorLi.appendChild(indicator);
|
|
247
|
+
list.appendChild(indicatorLi);
|
|
248
|
+
const positionIndicator = () => {
|
|
249
|
+
const active = list.querySelector('a[aria-current="page"], a.router-link-active');
|
|
250
|
+
if (!active || active.offsetParent === null) {
|
|
251
|
+
indicator.style.setProperty('--_indicator-opacity', '0');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const listRect = list.getBoundingClientRect();
|
|
255
|
+
const activeRect = active.getBoundingClientRect();
|
|
256
|
+
const isRtl = resolveDirection(nav) === 'rtl';
|
|
257
|
+
// In LTR inset-inline-start is physical left → positive translate moves right.
|
|
258
|
+
// In RTL inset-inline-start is physical right → negative translate moves left.
|
|
259
|
+
const offset = isRtl
|
|
260
|
+
? activeRect.right - listRect.right
|
|
261
|
+
: activeRect.left - listRect.left;
|
|
262
|
+
indicator.style.setProperty('--_indicator-x', `${offset}px`);
|
|
263
|
+
indicator.style.setProperty('--_indicator-w', `${activeRect.width}px`);
|
|
264
|
+
indicator.style.setProperty('--_indicator-opacity', '1');
|
|
265
|
+
};
|
|
266
|
+
const indicatorObserver = typeof MutationObserver !== 'undefined'
|
|
267
|
+
? new MutationObserver(() => positionIndicator())
|
|
268
|
+
: null;
|
|
269
|
+
indicatorObserver?.observe(list, {
|
|
270
|
+
subtree: true,
|
|
271
|
+
attributes: true,
|
|
272
|
+
attributeFilter: ['aria-current', 'class'],
|
|
273
|
+
});
|
|
274
|
+
if (indicatorObserver)
|
|
275
|
+
disposers.push(() => indicatorObserver.disconnect());
|
|
276
|
+
if (typeof window !== 'undefined') {
|
|
277
|
+
const onResize = () => positionIndicator();
|
|
278
|
+
window.addEventListener('resize', onResize);
|
|
279
|
+
disposers.push(() => window.removeEventListener('resize', onResize));
|
|
280
|
+
// Initial position after layout settles (fonts, container queries).
|
|
281
|
+
const indicatorRafId = requestAnimationFrame(positionIndicator);
|
|
282
|
+
disposers.push(() => cancelAnimationFrame(indicatorRafId));
|
|
283
|
+
}
|
|
284
|
+
const safeItems = Array.isArray(items) ? items : [];
|
|
285
|
+
// Router may not be mounted yet when Navbar constructs (App template renders
|
|
286
|
+
// Navbar before @routerOutlet runs). Re-resolve right before use — and always
|
|
287
|
+
// tag anchors with `data-router-link` so `setGlobalRouter → bindExistingRouterLinks`
|
|
288
|
+
// hooks them up on late router mount.
|
|
289
|
+
const resolveRouter = () => {
|
|
290
|
+
try {
|
|
291
|
+
return getGlobalRouter();
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const currentPath = typeof window !== 'undefined' && window.location
|
|
298
|
+
? window.location.pathname || '/'
|
|
299
|
+
: '/';
|
|
300
|
+
// Populated for the popstate fallback — used only if no router ever mounts.
|
|
301
|
+
const anchorHandles = [];
|
|
302
|
+
for (let i = 0; i < safeItems.length; i++) {
|
|
303
|
+
const item = safeItems[i];
|
|
304
|
+
if (!item ||
|
|
305
|
+
typeof item.label !== 'string' ||
|
|
306
|
+
typeof item.href !== 'string')
|
|
307
|
+
continue;
|
|
308
|
+
if (!isSafeHref(item.href))
|
|
309
|
+
continue;
|
|
310
|
+
const li = document.createElement('li');
|
|
311
|
+
li.classList.add('ui-navbar-item');
|
|
312
|
+
li.style.setProperty('--_i', String(i));
|
|
313
|
+
const anchor = document.createElement('a');
|
|
314
|
+
anchor.classList.add('ui-nav-link', 'ui-navbar-link');
|
|
315
|
+
anchor.href = item.href;
|
|
316
|
+
anchor.textContent = item.label;
|
|
317
|
+
const prefetchStrategy = item.prefetch === undefined
|
|
318
|
+
? 'none'
|
|
319
|
+
: validateEnum('Navbar', `items[${i}].prefetch`, item.prefetch, PREFETCH_STRATEGIES, 'none');
|
|
320
|
+
anchor.setAttribute('data-router-link', '');
|
|
321
|
+
anchor.setAttribute('data-prefetch', prefetchStrategy);
|
|
322
|
+
if (item.exactActive === true)
|
|
323
|
+
anchor.setAttribute('exact-match', '');
|
|
324
|
+
const syncDataCurrent = () => {
|
|
325
|
+
if (anchor.getAttribute('aria-current') === 'page') {
|
|
326
|
+
anchor.dataset.current = 'page';
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
delete anchor.dataset.current;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
333
|
+
const mo = new MutationObserver(syncDataCurrent);
|
|
334
|
+
mo.observe(anchor, {
|
|
335
|
+
attributes: true,
|
|
336
|
+
attributeFilter: ['aria-current'],
|
|
337
|
+
});
|
|
338
|
+
disposers.push(() => mo.disconnect());
|
|
339
|
+
}
|
|
340
|
+
disposers.push(() => cleanupRouterDirectives(anchor));
|
|
341
|
+
const router = resolveRouter();
|
|
342
|
+
if (router) {
|
|
343
|
+
anchor.setAttribute('data-router-bound', 'true');
|
|
344
|
+
routerLink(anchor, router, {
|
|
345
|
+
exactMatch: item.exactActive === true,
|
|
346
|
+
prefetch: prefetchStrategy,
|
|
347
|
+
});
|
|
348
|
+
syncDataCurrent();
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const active = isItemActive(currentPath, item.href, item.exactActive === true);
|
|
352
|
+
if (active) {
|
|
353
|
+
anchor.classList.add('router-link-active');
|
|
354
|
+
anchor.setAttribute('aria-current', 'page');
|
|
355
|
+
}
|
|
356
|
+
syncDataCurrent();
|
|
357
|
+
anchorHandles.push({ anchor, item });
|
|
358
|
+
}
|
|
359
|
+
li.appendChild(anchor);
|
|
360
|
+
list.appendChild(li);
|
|
361
|
+
}
|
|
362
|
+
// Search slot sits between items and actions so a ⌘K trigger reads as a
|
|
363
|
+
// peer of the nav, not a trailing action. Extracted before fall-through.
|
|
364
|
+
const projectedSearch = extractSlot(childFragment, 'search');
|
|
365
|
+
if (projectedSearch) {
|
|
366
|
+
const searchSlot = document.createElement('div');
|
|
367
|
+
searchSlot.classList.add('ui-navbar-search');
|
|
368
|
+
captureSlot(searchSlot, 'search');
|
|
369
|
+
searchSlot.appendChild(projectedSearch);
|
|
370
|
+
menu.appendChild(searchSlot);
|
|
371
|
+
}
|
|
372
|
+
const actionsSlot = document.createElement('div');
|
|
373
|
+
actionsSlot.classList.add('ui-navbar-actions');
|
|
374
|
+
captureSlot(actionsSlot, 'actions');
|
|
375
|
+
const projectedActions = extractSlot(childFragment, 'actions');
|
|
376
|
+
if (projectedActions)
|
|
377
|
+
actionsSlot.appendChild(projectedActions);
|
|
378
|
+
for (const node of Array.from(childFragment.childNodes)) {
|
|
379
|
+
if (node.nodeType === Node.ELEMENT_NODE)
|
|
380
|
+
actionsSlot.appendChild(node);
|
|
381
|
+
}
|
|
382
|
+
menu.appendChild(actionsSlot);
|
|
383
|
+
// A sticky nav keeps any in-nav sentinel pinned, so IntersectionObserver
|
|
384
|
+
// can't tell us we've scrolled — a passive scroll listener is the fallback.
|
|
385
|
+
/** Max time to wait for transitionend before clearing inline menu styles. */
|
|
386
|
+
const CLOSE_TRANSITION_TIMEOUT = 600;
|
|
387
|
+
const SCROLL_THRESHOLD = 8;
|
|
388
|
+
let scrollRafQueued = false;
|
|
389
|
+
let scrollRafId = 0;
|
|
390
|
+
const applyScrollFrame = () => {
|
|
391
|
+
scrollRafQueued = false;
|
|
392
|
+
const y = typeof window !== 'undefined'
|
|
393
|
+
? window.scrollY || window.pageYOffset || 0
|
|
394
|
+
: 0;
|
|
395
|
+
if (y > SCROLL_THRESHOLD) {
|
|
396
|
+
nav.dataset.scrolled = 'true';
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
delete nav.dataset.scrolled;
|
|
400
|
+
}
|
|
401
|
+
if (isMenuOpen)
|
|
402
|
+
positionMenu();
|
|
403
|
+
};
|
|
404
|
+
const onWindowScroll = () => {
|
|
405
|
+
if (scrollRafQueued)
|
|
406
|
+
return;
|
|
407
|
+
scrollRafQueued = true;
|
|
408
|
+
scrollRafId = requestAnimationFrame(applyScrollFrame);
|
|
409
|
+
};
|
|
410
|
+
// Tracked from the `toggle` event so we never call
|
|
411
|
+
// `menu.matches(":popover-open")`, which throws a SyntaxError in browsers
|
|
412
|
+
// that don't recognise the `:popover-open` pseudo-class (Firefox <125, Safari <17).
|
|
413
|
+
let isMenuOpen = false;
|
|
414
|
+
// Sync aria-expanded ourselves in case Invoker Commands haven't shipped yet.
|
|
415
|
+
// Initial focus lands inside the menu only when the keyboard opened it;
|
|
416
|
+
// pointer opens leave focus on the trigger so no unasked-for focus ring appears.
|
|
417
|
+
let toggleLastInput = 'pointer';
|
|
418
|
+
const onTogglePointerDown = () => {
|
|
419
|
+
toggleLastInput = 'pointer';
|
|
420
|
+
};
|
|
421
|
+
const onToggleKeyDown = (ev) => {
|
|
422
|
+
if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') {
|
|
423
|
+
toggleLastInput = 'keyboard';
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
toggle.addEventListener('pointerdown', onTogglePointerDown);
|
|
427
|
+
toggle.addEventListener('keydown', onToggleKeyDown);
|
|
428
|
+
disposers.push(() => {
|
|
429
|
+
toggle.removeEventListener('pointerdown', onTogglePointerDown);
|
|
430
|
+
toggle.removeEventListener('keydown', onToggleKeyDown);
|
|
431
|
+
});
|
|
432
|
+
// All focusables inside the drawer — drives the Tab focus trap so every
|
|
433
|
+
// control (links, buttons, form fields in the actions/search slots,
|
|
434
|
+
// explicit tabindex) is reachable via Tab.
|
|
435
|
+
const MENU_FOCUSABLE_SELECTOR = [
|
|
436
|
+
'a[href]',
|
|
437
|
+
'button:not([disabled])',
|
|
438
|
+
'input:not([disabled])',
|
|
439
|
+
'select:not([disabled])',
|
|
440
|
+
'textarea:not([disabled])',
|
|
441
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
442
|
+
].join(',');
|
|
443
|
+
const getMenuFocusables = () => Array.from(menu.querySelectorAll(MENU_FOCUSABLE_SELECTOR)).filter(el => !el.hasAttribute('inert') && !el.closest('[inert]') && !el.hidden);
|
|
444
|
+
// Arrow/Home/End/initial-focus walk the primary nav links only — chrome
|
|
445
|
+
// (drawer brand, close button) and slot content (actions/search) stay
|
|
446
|
+
// reachable via Tab but shouldn't receive arrow navigation.
|
|
447
|
+
const getMenuNavItems = () => Array.from(menu.querySelectorAll('.ui-navbar-list .ui-navbar-link'));
|
|
448
|
+
const onMenuToggle = (e) => {
|
|
449
|
+
const ev = e;
|
|
450
|
+
const open = ev.newState === 'open';
|
|
451
|
+
isMenuOpen = open;
|
|
452
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
453
|
+
toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
|
|
454
|
+
if (open && toggleLastInput === 'keyboard') {
|
|
455
|
+
const items = getMenuNavItems();
|
|
456
|
+
if (items.length > 0)
|
|
457
|
+
items[0].focus();
|
|
458
|
+
}
|
|
459
|
+
// When the drawer closes via light-dismiss (backdrop click) or
|
|
460
|
+
// programmatic hidePopover(), focus falls to <body>. Restore it to the
|
|
461
|
+
// toggle so keyboard users aren't stranded at the top of the page.
|
|
462
|
+
// Skip if navigation already moved focus to a heading (activeElement
|
|
463
|
+
// won't be body in that case).
|
|
464
|
+
if (!open) {
|
|
465
|
+
toggleLastInput = 'pointer';
|
|
466
|
+
requestAnimationFrame(() => {
|
|
467
|
+
if (document.activeElement === document.body ||
|
|
468
|
+
document.activeElement === document.documentElement) {
|
|
469
|
+
toggle.focus({ preventScroll: true });
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
475
|
+
menu.addEventListener('toggle', onMenuToggle);
|
|
476
|
+
disposers.push(() => menu.removeEventListener('toggle', onMenuToggle));
|
|
477
|
+
// True only when the navbar is in mobile layout (hamburger mode). Driven by
|
|
478
|
+
// the ResizeObserver-based overflow detection below — `data-layout="mobile"`
|
|
479
|
+
// is set when nav items don't fit in a single row.
|
|
480
|
+
const isMobileLayout = () => nav.dataset.layout === 'mobile';
|
|
481
|
+
const onMenuKeyDown = (ev) => {
|
|
482
|
+
if (ev.key !== 'ArrowDown' &&
|
|
483
|
+
ev.key !== 'ArrowUp' &&
|
|
484
|
+
ev.key !== 'Home' &&
|
|
485
|
+
ev.key !== 'End' &&
|
|
486
|
+
ev.key !== 'Tab') {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (ev.key === 'Tab') {
|
|
490
|
+
// Mobile-only focus trap — cycle across every focusable in the drawer
|
|
491
|
+
// (including the in-drawer close button).
|
|
492
|
+
if (!isMobileLayout() || !isMenuOpen)
|
|
493
|
+
return;
|
|
494
|
+
const focusables = getMenuFocusables();
|
|
495
|
+
if (focusables.length === 0)
|
|
496
|
+
return;
|
|
497
|
+
const active = document.activeElement;
|
|
498
|
+
const idx = active ? focusables.indexOf(active) : -1;
|
|
499
|
+
if (ev.shiftKey && idx === 0) {
|
|
500
|
+
ev.preventDefault();
|
|
501
|
+
focusables[focusables.length - 1].focus();
|
|
502
|
+
}
|
|
503
|
+
else if (!ev.shiftKey && idx === focusables.length - 1) {
|
|
504
|
+
ev.preventDefault();
|
|
505
|
+
focusables[0].focus();
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Arrow/Home/End operate on nav items only — close button is chrome.
|
|
510
|
+
const items = getMenuNavItems();
|
|
511
|
+
if (items.length === 0)
|
|
512
|
+
return;
|
|
513
|
+
const active = document.activeElement;
|
|
514
|
+
const idx = active ? items.indexOf(active) : -1;
|
|
515
|
+
let nextIdx = idx;
|
|
516
|
+
if (ev.key === 'ArrowDown')
|
|
517
|
+
nextIdx = idx < 0 ? 0 : (idx + 1) % items.length;
|
|
518
|
+
else if (ev.key === 'ArrowUp')
|
|
519
|
+
nextIdx =
|
|
520
|
+
idx < 0 ? items.length - 1 : (idx - 1 + items.length) % items.length;
|
|
521
|
+
else if (ev.key === 'Home')
|
|
522
|
+
nextIdx = 0;
|
|
523
|
+
else if (ev.key === 'End')
|
|
524
|
+
nextIdx = items.length - 1;
|
|
525
|
+
if (nextIdx !== idx) {
|
|
526
|
+
ev.preventDefault();
|
|
527
|
+
items[nextIdx].focus();
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
menu.addEventListener('keydown', onMenuKeyDown);
|
|
531
|
+
disposers.push(() => menu.removeEventListener('keydown', onMenuKeyDown));
|
|
532
|
+
// Satisfies WCAG 2.4.11 when the menu overflows. Mutates `scrollTop` directly
|
|
533
|
+
// rather than calling `scrollIntoView` so no ancestor can scroll by surprise.
|
|
534
|
+
const FOCUS_EDGE_MARGIN = 8;
|
|
535
|
+
const onMenuFocusIn = (ev) => {
|
|
536
|
+
const target = ev.target;
|
|
537
|
+
if (!target || target === menu || !menu.contains(target))
|
|
538
|
+
return;
|
|
539
|
+
if (menu.scrollHeight <= menu.clientHeight)
|
|
540
|
+
return;
|
|
541
|
+
const menuRect = menu.getBoundingClientRect();
|
|
542
|
+
const targetRect = target.getBoundingClientRect();
|
|
543
|
+
const topDelta = targetRect.top - menuRect.top - FOCUS_EDGE_MARGIN;
|
|
544
|
+
const bottomDelta = targetRect.bottom - menuRect.bottom + FOCUS_EDGE_MARGIN;
|
|
545
|
+
if (topDelta < 0) {
|
|
546
|
+
menu.scrollTop += topDelta;
|
|
547
|
+
}
|
|
548
|
+
else if (bottomDelta > 0) {
|
|
549
|
+
menu.scrollTop += bottomDelta;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
menu.addEventListener('focusin', onMenuFocusIn);
|
|
553
|
+
disposers.push(() => menu.removeEventListener('focusin', onMenuFocusIn));
|
|
554
|
+
// Popovers in the top layer aren't implicitly anchored to their invoker, and
|
|
555
|
+
// CSS Anchor Positioning is Chromium-only — so we pin the menu with JS on
|
|
556
|
+
// open and clear the inline styles on close (so desktop's container query
|
|
557
|
+
// can retake the layout).
|
|
558
|
+
const POPOVER_GAP = 8;
|
|
559
|
+
// Attribute walk first so the resolver is deterministic across runtimes;
|
|
560
|
+
// jsdom/happy-dom return "ltr" from getComputedStyle regardless of inheritance.
|
|
561
|
+
const resolveDirection = (el) => {
|
|
562
|
+
let cur = el;
|
|
563
|
+
while (cur) {
|
|
564
|
+
const dir = cur.getAttribute?.('dir');
|
|
565
|
+
if (dir === 'rtl')
|
|
566
|
+
return 'rtl';
|
|
567
|
+
if (dir === 'ltr')
|
|
568
|
+
return 'ltr';
|
|
569
|
+
cur = cur.parentElement;
|
|
570
|
+
}
|
|
571
|
+
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
572
|
+
const computed = window.getComputedStyle(el).direction;
|
|
573
|
+
if (computed === 'rtl')
|
|
574
|
+
return 'rtl';
|
|
575
|
+
}
|
|
576
|
+
return 'ltr';
|
|
577
|
+
};
|
|
578
|
+
const positionMenu = () => {
|
|
579
|
+
if (isMobileLayout())
|
|
580
|
+
return;
|
|
581
|
+
const r = toggle.getBoundingClientRect();
|
|
582
|
+
const topPx = r.bottom + POPOVER_GAP;
|
|
583
|
+
menu.style.insetBlockStart = `${topPx}px`;
|
|
584
|
+
const isRtl = resolveDirection(nav) === 'rtl';
|
|
585
|
+
const viewportInlineEnd = isRtl ? r.left : window.innerWidth - r.right;
|
|
586
|
+
menu.style.insetInlineEnd = `${Math.max(0, viewportInlineEnd)}px`;
|
|
587
|
+
menu.style.insetInlineStart = 'auto';
|
|
588
|
+
menu.style.insetBlockEnd = 'auto';
|
|
589
|
+
menu.style.setProperty('--_menu-top', `${topPx}px`);
|
|
590
|
+
};
|
|
591
|
+
let heldScrollLock = false;
|
|
592
|
+
// Elements this instance has incremented an inert ref on. Instance-local so
|
|
593
|
+
// destroy only decrements what it acquired; the global ref map ensures other
|
|
594
|
+
// concurrent drawers keep the `inert` attribute while they still need it.
|
|
595
|
+
let inertedSiblings = [];
|
|
596
|
+
// Walk from nav up to <body>, inerting siblings at every ancestor level.
|
|
597
|
+
// This ensures <main> inside a wrapper like <div id="app"> is also inerted,
|
|
598
|
+
// not just direct body children.
|
|
599
|
+
const applyInertSiblings = () => {
|
|
600
|
+
if (inertedSiblings.length > 0 || typeof document === 'undefined')
|
|
601
|
+
return;
|
|
602
|
+
let cur = nav;
|
|
603
|
+
while (cur && cur !== document.body) {
|
|
604
|
+
const parentEl = cur.parentElement;
|
|
605
|
+
if (!parentEl)
|
|
606
|
+
break;
|
|
607
|
+
for (const child of Array.from(parentEl.children)) {
|
|
608
|
+
if (!(child instanceof HTMLElement))
|
|
609
|
+
continue;
|
|
610
|
+
if (child === cur)
|
|
611
|
+
continue;
|
|
612
|
+
// Skip elements that were inert before any drawer touched them — those
|
|
613
|
+
// belong to someone else (a modal, a permanently disabled panel, etc.).
|
|
614
|
+
if (child.hasAttribute('inert') && !inertRefs.has(child))
|
|
615
|
+
continue;
|
|
616
|
+
acquireInert(child);
|
|
617
|
+
inertedSiblings.push(child);
|
|
618
|
+
}
|
|
619
|
+
cur = parentEl;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const releaseInertSiblings = () => {
|
|
623
|
+
for (const el of inertedSiblings)
|
|
624
|
+
releaseInert(el);
|
|
625
|
+
inertedSiblings = [];
|
|
626
|
+
};
|
|
627
|
+
// Clearing inline position before the close transition finishes snaps the
|
|
628
|
+
// still-visible popover to the viewport's top-left, so we defer the clear
|
|
629
|
+
// to the opacity transitionend (with a timeout fallback).
|
|
630
|
+
let pendingCloseCleanup = null;
|
|
631
|
+
const clearMenuPosition = () => {
|
|
632
|
+
menu.style.insetBlockStart = '';
|
|
633
|
+
menu.style.insetInlineEnd = '';
|
|
634
|
+
menu.style.insetInlineStart = '';
|
|
635
|
+
menu.style.insetBlockEnd = '';
|
|
636
|
+
menu.style.removeProperty('--_menu-top');
|
|
637
|
+
};
|
|
638
|
+
const cancelPendingClose = () => {
|
|
639
|
+
if (pendingCloseCleanup) {
|
|
640
|
+
pendingCloseCleanup();
|
|
641
|
+
pendingCloseCleanup = null;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
const onBeforeToggle = (e) => {
|
|
645
|
+
const ev = e;
|
|
646
|
+
if (ev.newState === 'open') {
|
|
647
|
+
cancelPendingClose();
|
|
648
|
+
positionMenu();
|
|
649
|
+
// Only lock scroll when the layout is mobile (hamburger mode); desktop
|
|
650
|
+
// renders the menu inline so no overlay or scroll lock is needed.
|
|
651
|
+
if (isMobileLayout() && !heldScrollLock) {
|
|
652
|
+
acquireScrollLock();
|
|
653
|
+
heldScrollLock = true;
|
|
654
|
+
applyInertSiblings();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
// Defer scroll-lock and inert release until the close transition
|
|
659
|
+
// finishes — releasing immediately would re-enable background scroll
|
|
660
|
+
// and focus while the drawer is still visually present.
|
|
661
|
+
const finalizeClose = () => {
|
|
662
|
+
if (heldScrollLock) {
|
|
663
|
+
releaseScrollLock();
|
|
664
|
+
heldScrollLock = false;
|
|
665
|
+
}
|
|
666
|
+
releaseInertSiblings();
|
|
667
|
+
clearMenuPosition();
|
|
668
|
+
};
|
|
669
|
+
const prefersReducedMotion = typeof window !== 'undefined' &&
|
|
670
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
671
|
+
if (prefersReducedMotion) {
|
|
672
|
+
finalizeClose();
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
const onEnd = (te) => {
|
|
676
|
+
if (te.target !== menu)
|
|
677
|
+
return;
|
|
678
|
+
if (te.propertyName !== 'opacity')
|
|
679
|
+
return;
|
|
680
|
+
cleanup();
|
|
681
|
+
};
|
|
682
|
+
const timeoutId = window.setTimeout(() => cleanup(), CLOSE_TRANSITION_TIMEOUT);
|
|
683
|
+
const cleanup = () => {
|
|
684
|
+
menu.removeEventListener('transitionend', onEnd);
|
|
685
|
+
window.clearTimeout(timeoutId);
|
|
686
|
+
pendingCloseCleanup = null;
|
|
687
|
+
finalizeClose();
|
|
688
|
+
};
|
|
689
|
+
pendingCloseCleanup = cleanup;
|
|
690
|
+
menu.addEventListener('transitionend', onEnd);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
disposers.push(() => {
|
|
695
|
+
if (heldScrollLock) {
|
|
696
|
+
releaseScrollLock();
|
|
697
|
+
heldScrollLock = false;
|
|
698
|
+
}
|
|
699
|
+
releaseInertSiblings();
|
|
700
|
+
cancelPendingClose();
|
|
701
|
+
});
|
|
702
|
+
menu.addEventListener('beforetoggle', onBeforeToggle);
|
|
703
|
+
disposers.push(() => menu.removeEventListener('beforetoggle', onBeforeToggle));
|
|
704
|
+
if (typeof window !== 'undefined') {
|
|
705
|
+
window.addEventListener('scroll', onWindowScroll, { passive: true });
|
|
706
|
+
window.addEventListener('resize', onWindowScroll, { passive: true });
|
|
707
|
+
// Catch hydration mid-page (user already scrolled).
|
|
708
|
+
applyScrollFrame();
|
|
709
|
+
disposers.push(() => {
|
|
710
|
+
window.removeEventListener('scroll', onWindowScroll);
|
|
711
|
+
window.removeEventListener('resize', onWindowScroll);
|
|
712
|
+
if (scrollRafId !== 0)
|
|
713
|
+
cancelAnimationFrame(scrollRafId);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
// Content-aware responsive layout — switch to hamburger only when items
|
|
717
|
+
// don't fit in a single row. ResizeObserver fires before paint so there's
|
|
718
|
+
// no visible flash on initial render.
|
|
719
|
+
let layoutDestroyed = false;
|
|
720
|
+
/** Temporarily set overflow:hidden for accurate list width measurement,
|
|
721
|
+
* then restore the previous inline value. This avoids persistent clipping
|
|
722
|
+
* of focus rings and keeps measurement side-effect free. */
|
|
723
|
+
const measureListWidths = () => {
|
|
724
|
+
const prev = list.style.overflow;
|
|
725
|
+
list.style.overflow = 'hidden';
|
|
726
|
+
const scrollWidth = list.scrollWidth;
|
|
727
|
+
const clientWidth = list.clientWidth;
|
|
728
|
+
list.style.overflow = prev;
|
|
729
|
+
return { scrollWidth, clientWidth };
|
|
730
|
+
};
|
|
731
|
+
// Track the nav width at the moment we switched to mobile. Only attempt
|
|
732
|
+
// switching back to desktop when the nav has actually grown — this prevents
|
|
733
|
+
// oscillation where toggling data-layout changes the list dimensions, which
|
|
734
|
+
// re-triggers the ResizeObserver, which toggles again, ad infinitum.
|
|
735
|
+
let mobileAtNavWidth = 0;
|
|
736
|
+
const goMobile = () => {
|
|
737
|
+
mobileAtNavWidth = nav.clientWidth;
|
|
738
|
+
nav.dataset.layout = 'mobile';
|
|
739
|
+
};
|
|
740
|
+
// Layout flips reuse the popover's display/overlay transitions — without
|
|
741
|
+
// suppression, desktop→mobile plays the hide animation and flashes items.
|
|
742
|
+
let transitionUnlockId = 0;
|
|
743
|
+
const runWithoutTransitions = (fn) => {
|
|
744
|
+
nav.dataset.layoutTransitioning = 'true';
|
|
745
|
+
fn();
|
|
746
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
747
|
+
delete nav.dataset.layoutTransitioning;
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (transitionUnlockId !== 0)
|
|
751
|
+
cancelAnimationFrame(transitionUnlockId);
|
|
752
|
+
transitionUnlockId = requestAnimationFrame(() => {
|
|
753
|
+
transitionUnlockId = requestAnimationFrame(() => {
|
|
754
|
+
transitionUnlockId = 0;
|
|
755
|
+
delete nav.dataset.layoutTransitioning;
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
};
|
|
759
|
+
const checkLayout = () => {
|
|
760
|
+
if (layoutDestroyed)
|
|
761
|
+
return;
|
|
762
|
+
if (nav.dataset.layout !== 'mobile') {
|
|
763
|
+
// Desktop: measure the list directly for overflow
|
|
764
|
+
const { scrollWidth, clientWidth } = measureListWidths();
|
|
765
|
+
if (scrollWidth > clientWidth + 1)
|
|
766
|
+
runWithoutTransitions(goMobile); // +1 subpixel guard
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// Body scroll-lock (set synchronously in beforetoggle) removes the
|
|
770
|
+
// scrollbar, which widens nav.clientWidth past mobileAtNavWidth.
|
|
771
|
+
// Check the module-level ref count so another Navbar's open drawer
|
|
772
|
+
// also pauses this instance's probe.
|
|
773
|
+
if (scrollLockRefs > 0)
|
|
774
|
+
return;
|
|
775
|
+
// Mobile: only attempt desktop when the nav has grown wider than
|
|
776
|
+
// when we last switched. This gates out the self-triggered callbacks
|
|
777
|
+
// from the layout toggle below.
|
|
778
|
+
if (nav.clientWidth <= mobileAtNavWidth)
|
|
779
|
+
return;
|
|
780
|
+
runWithoutTransitions(() => {
|
|
781
|
+
// Temporarily flip to desktop to get real measurements, then decide.
|
|
782
|
+
delete nav.dataset.layout;
|
|
783
|
+
const { scrollWidth, clientWidth } = measureListWidths();
|
|
784
|
+
if (scrollWidth > clientWidth + 1) {
|
|
785
|
+
// Items still overflow — revert and update the cached nav width
|
|
786
|
+
// so we don't try again until the viewport grows further.
|
|
787
|
+
goMobile();
|
|
788
|
+
}
|
|
789
|
+
else if (isMenuOpen) {
|
|
790
|
+
// Genuinely wider viewport — close the popover so stale
|
|
791
|
+
// scroll-lock / inert doesn't linger on the desktop layout.
|
|
792
|
+
try {
|
|
793
|
+
menu.hidePopover();
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
/* already closed */
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
803
|
+
const layoutObserver = new ResizeObserver(checkLayout);
|
|
804
|
+
layoutObserver.observe(nav);
|
|
805
|
+
// Also observe the list — internal content changes (slot content, label
|
|
806
|
+
// text) can change scrollWidth without resizing the nav container itself.
|
|
807
|
+
layoutObserver.observe(list);
|
|
808
|
+
disposers.push(() => {
|
|
809
|
+
layoutDestroyed = true;
|
|
810
|
+
layoutObserver.disconnect();
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
else if (typeof window !== 'undefined') {
|
|
814
|
+
// Fallback for environments without ResizeObserver — listen for window
|
|
815
|
+
// resize events so items don't overflow/clip on small screens.
|
|
816
|
+
checkLayout();
|
|
817
|
+
const onResize = () => {
|
|
818
|
+
if (!layoutDestroyed)
|
|
819
|
+
checkLayout();
|
|
820
|
+
};
|
|
821
|
+
window.addEventListener('resize', onResize);
|
|
822
|
+
disposers.push(() => {
|
|
823
|
+
layoutDestroyed = true;
|
|
824
|
+
window.removeEventListener('resize', onResize);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
// Always push a disposer for the destroyed flag so fonts.ready (and any
|
|
828
|
+
// future async paths) are gated even when neither branch above ran.
|
|
829
|
+
disposers.push(() => {
|
|
830
|
+
layoutDestroyed = true;
|
|
831
|
+
});
|
|
832
|
+
// Re-check after fonts finish loading — web fonts can change text metrics
|
|
833
|
+
// and cause scrollWidth to differ without triggering a resize on nav.
|
|
834
|
+
if (typeof document !== 'undefined' && document.fonts?.ready) {
|
|
835
|
+
document.fonts.ready.then(() => {
|
|
836
|
+
if (!layoutDestroyed)
|
|
837
|
+
checkLayout();
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
// No-router fallback: if no router is mounted, back/forward still fires
|
|
841
|
+
// popstate and we re-sync active-state. Safe if the router binds later via
|
|
842
|
+
// bindExistingRouterLinks — both paths agree on prefix-match semantics.
|
|
843
|
+
if (resolveRouter() === null &&
|
|
844
|
+
typeof window !== 'undefined' &&
|
|
845
|
+
anchorHandles.length > 0) {
|
|
846
|
+
const onPopState = () => {
|
|
847
|
+
const path = window.location.pathname || '/';
|
|
848
|
+
for (const { anchor, item } of anchorHandles) {
|
|
849
|
+
const active = isItemActive(path, item.href, item.exactActive === true);
|
|
850
|
+
anchor.classList.toggle('router-link-active', active);
|
|
851
|
+
if (active) {
|
|
852
|
+
anchor.setAttribute('aria-current', 'page');
|
|
853
|
+
anchor.dataset.current = 'page';
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
anchor.removeAttribute('aria-current');
|
|
857
|
+
delete anchor.dataset.current;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
window.addEventListener('popstate', onPopState);
|
|
862
|
+
disposers.push(() => window.removeEventListener('popstate', onPopState));
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
el: nav,
|
|
866
|
+
destroy: () => {
|
|
867
|
+
for (const dispose of disposers) {
|
|
868
|
+
try {
|
|
869
|
+
dispose();
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
if (import.meta.env?.DEV) {
|
|
873
|
+
console.error('[<Navbar>] disposer threw:', err);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
nav.replaceChildren();
|
|
878
|
+
},
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
//# sourceMappingURL=Navbar.js.map
|