srcdev-nuxt-components 9.1.48 → 9.1.50
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/.claude/settings.json +2 -4
- package/.claude/settings.local.json +22 -2
- package/.claude/skills/components/card-core.md +205 -0
- package/.claude/skills/components/display-dialog.md +128 -0
- package/.claude/skills/components/page-hero-highlights.md +1 -1
- package/.claude/skills/components/page-row.md +157 -0
- package/.claude/skills/components/services-card-grid.md +4 -4
- package/.claude/skills/components/services-section-grid.md +4 -4
- package/.claude/skills/components/tab-navigation.md +222 -0
- package/.claude/skills/composable-anchor-scroll.md +110 -28
- package/.claude/skills/composable-dialog-controls.md +55 -43
- package/.claude/skills/index.md +5 -2
- package/app/components/01.atoms/card/CONSUMER-STYLING.md +175 -0
- package/app/components/01.atoms/card/CardCore.vue +48 -21
- package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +163 -0
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +277 -0
- package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +177 -0
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +264 -0
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +18 -0
- package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +90 -0
- package/app/components/01.atoms/page-row/PageRow.vue +115 -0
- package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +311 -0
- package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +191 -0
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +145 -0
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +52 -11
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +12 -6
- package/app/components/{forms/ui → 05.forms/form-field}/FormField.vue +12 -19
- package/app/components/{forms/ui → 05.forms/form-wrapper}/FormWrapper.vue +6 -6
- package/app/components/{forms → 05.forms}/input-button/InputButtonCore.vue +2 -0
- package/app/components/{forms → 05.forms}/input-button/stories/InputButtonCore.stories.ts +1 -1
- package/app/components/{forms → 05.forms}/input-text/InputTextCore.vue +73 -73
- package/app/components/{forms → 05.forms}/input-text/variants/InputTextAsNumberWithLabel.vue +29 -29
- package/app/components/carousels/CarouselBasic.vue +99 -99
- package/app/components/carousels/CarouselInfinite.vue +150 -150
- package/app/components/deep-expanding-menu/DeepExpandingMenu.vue +111 -111
- package/app/components/responsive-header/ResponsiveHeader.vue +250 -282
- package/app/components/test-storybook/TestStorybook.vue +16 -16
- package/app/composables/tests/useDialogControls.spec.ts +34 -64
- package/app/composables/useAnchorScroll.ts +16 -3
- package/app/composables/useDialogControls.ts +19 -33
- package/app/composables/useFormControl.ts +0 -2
- package/app/composables/useNavCollapse.ts +5 -1
- package/app/layouts/default.vue +10 -7
- package/app/pages/auto-grid.vue +37 -20
- package/app/pages/banner-video.vue +2 -2
- package/app/pages/forms/examples/buttons/index.vue +189 -215
- package/app/pages/forms/examples/material/text-fields.vue +2 -2
- package/app/pages/grid-stack.vue +10 -10
- package/app/pages/index.vue +20 -20
- package/app/pages/samaritan.vue +4 -4
- package/app/pages/typography/hero-text.vue +6 -6
- package/app/pages/typography/index.vue +4 -4
- package/app/pages/typography/page-body.vue +4 -4
- package/app/pages/typography/page-heading.vue +4 -4
- package/app/pages/typography/page-link.vue +4 -4
- package/app/pages/ui/accordian.vue +4 -4
- package/app/pages/ui/anchor-scroll-tab-navigation.vue +191 -0
- package/app/pages/ui/anchor-scroll.vue +105 -71
- package/app/pages/ui/animated-svg-text.vue +4 -4
- package/app/pages/ui/block-decorators.vue +11 -11
- package/app/pages/ui/card-core.vue +9 -9
- package/app/pages/ui/carousel-basic.vue +4 -4
- package/app/pages/ui/carousel-flip.vue +2 -2
- package/app/pages/ui/carousel-infinite.vue +4 -4
- package/app/pages/ui/clipped-panels.vue +10 -10
- package/app/pages/ui/contact-section.vue +4 -4
- package/app/pages/ui/container-glow.vue +2 -3
- package/app/pages/ui/deep-expanding-menu.vue +138 -0
- package/app/pages/ui/display-avatar.vue +2 -2
- package/app/pages/ui/display-banner.vue +2 -2
- package/app/pages/ui/display-chip.vue +32 -25
- package/app/pages/ui/display-details.vue +6 -10
- package/app/pages/ui/display-dialog.vue +96 -131
- package/app/pages/ui/display-pill.vue +27 -33
- package/app/pages/ui/display-prompt.vue +4 -4
- package/app/pages/ui/display-toast.vue +2 -2
- package/app/pages/ui/expanding-panel.vue +12 -16
- package/app/pages/ui/glowing-border.vue +12 -12
- package/app/pages/ui/layout-grid-a.vue +6 -6
- package/app/pages/ui/layout-grid-b.vue +4 -4
- package/app/pages/ui/magnetic-navigation.vue +6 -6
- package/app/pages/ui/marquee-scroller.vue +2 -2
- package/app/pages/ui/mask-element.vue +2 -2
- package/app/pages/ui/masonry-columns.vue +4 -4
- package/app/pages/ui/masonry-grid-ordered-experiment.vue +4 -4
- package/app/pages/ui/masonry-grid-ordered.vue +4 -4
- package/app/pages/ui/masonry-grid-sorted.vue +4 -4
- package/app/pages/ui/masonry-grid.vue +4 -4
- package/app/pages/ui/navigation/navigation-horizontal.vue +12 -12
- package/app/pages/ui/navigation/tab-navigation.vue +493 -0
- package/app/pages/ui/page-row.vue +127 -0
- package/app/pages/ui/price-list.vue +4 -4
- package/app/pages/ui/profile-section.vue +4 -4
- package/app/pages/ui/qr-code/[componentName].vue +6 -6
- package/app/pages/ui/rotating-carousel.vue +16 -16
- package/app/pages/ui/scroll-reveal-image.vue +20 -20
- package/app/pages/ui/section-parallax.vue +7 -7
- package/app/pages/ui/services/colour-finder.vue +4 -4
- package/app/pages/ui/services/services-cards.vue +4 -4
- package/app/pages/ui/services/services-section/[slug].vue +4 -4
- package/app/pages/ui/services/services-sections-compact.vue +6 -6
- package/app/pages/ui/settings.vue +10 -10
- package/app/pages/ui/simple-grid.vue +2 -2
- package/app/pages/ui/slider-gallery.vue +5 -5
- package/app/pages/ui/tabs-y.vue +2 -3
- package/app/pages/ui/tabs.vue +6 -9
- package/app/pages/ui/tooltips.vue +9 -9
- package/app/pages/ui/wipe-away-vertical.vue +14 -14
- package/package.json +1 -1
- package/.claude/skills/components/layout-row.md +0 -239
- package/app/components/display-dialog/DisplayDialogCore.vue +0 -276
- package/app/components/display-dialog/variants/DisplayDialogConfirm.vue +0 -47
- package/app/components/display-dialog/variants/DisplayDialogScrollableContent.vue +0 -51
- package/app/components/forms/c12/utils.ts +0 -14
- package/app/components/layout-row/LayoutRow.vue +0 -171
- package/app/components/layout-row/stories/LayoutRow.stories.ts +0 -528
- package/app/pages/ui/layout-row.vue +0 -139
- /package/app/components/{forms → 05.forms}/form-errors/InputError.vue +0 -0
- /package/app/components/{forms → 05.forms}/form-errors/tests/InputError.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/form-fieldset/FormFieldset.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-chromium-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-firefox-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/with-both-nuxt-icon-components-webkit-darwin.png +0 -0
- /package/app/components/{forms → 05.forms}/input-button/tests/InputButtonCore.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-button/tests/__snapshots__/InputButtonCore.spec.ts.snap +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox/MultipleCheckboxes.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox/SingleCheckbox.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox/tests/MultipleCheckboxes.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox/tests/data/tags.json +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioButton.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-checkbox-radio/tests/InputCheckboxRadioCore.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-copy/InputCopyCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-copy/stories/InputCopyCore.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-copy/tests/InputCopyCore.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +0 -0
- /package/app/components/{forms → 05.forms}/input-description/InputDescription.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-description/tests/InputDescription.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-label/InputLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-label/tests/InputLabel.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-number/InputNumberCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-number/variants/InputNumberDefault.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-radio/MultipleRadiobuttons.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-radio/tests/MultipleRadioButtons.spec.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-radio/tests/data/tags.json +0 -0
- /package/app/components/{forms → 05.forms}/input-range/InputRangeCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-range/variants/InputRangeDefault.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-range-fancy/InputRangeFancyCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-range-fancy/InputRangeFancyWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-select/InputSelectCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-select/variants/InputSelectWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-text/stories/InputPasswordWithLabel.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-text/stories/InputTextCore.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-text/stories/InputTextWithLabel.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-text/variants/InputPasswordWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-text/variants/InputTextWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-textarea/InputTextareaCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/input-textarea/stories/InputTextareaCore.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/input-textarea/variants/InputTextareaWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/pending-effect/PendingEffect.vue +0 -0
- /package/app/components/{forms → 05.forms}/toggle-switch/ToggleSwitchCore.vue +0 -0
- /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -0
- /package/app/components/{forms → 05.forms}/toggle-switch/variants/ToggleSwitchWithLabel.vue +0 -0
- /package/app/components/{forms → 05.forms}/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +0 -0
- /package/app/components/{forms → 05.forms}/triple-toggle-switch/TripleToggleSwitchCore.vue +0 -0
- /package/app/{components/forms/c12/prop-validators/index.ts → utils/prop-validators.ts} +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# TabNavigation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A responsive horizontal navigation bar with CSS anchor-positioning indicators, smooth anchor
|
|
6
|
+
scroll, and a burger-menu collapse at narrow widths. Accepts both route links (`NuxtLink`) and
|
|
7
|
+
hash anchors (`<a>`). Items are driven by a `NavItemData` object; the component only reads the
|
|
8
|
+
`main` key.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Types
|
|
13
|
+
|
|
14
|
+
Import from the layer package root — do **not** use `~/types/...` in consuming apps:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import type { NavItemData } from "srcdev-nuxt-components"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
interface NavItem {
|
|
22
|
+
text: string;
|
|
23
|
+
href?: string;
|
|
24
|
+
isExternal?: boolean; // adds target="_blank" rel behaviour via NuxtLink
|
|
25
|
+
iconName?: string; // icon set name, rendered via <Icon>
|
|
26
|
+
cssName?: string; // extra CSS class on the <li>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface NavItemData {
|
|
30
|
+
[key: string]: NavItem[]; // component reads navItemData.main
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Props
|
|
37
|
+
|
|
38
|
+
| Prop (template form) | Type | Default | Notes |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| `nav-item-data` | `NavItemData` | required | Navigation link data — only `main` key is read |
|
|
41
|
+
| `nav-align` | `"left" \| "center" \| "right"` | `"left"` | Positions the tab list within the nav container |
|
|
42
|
+
| `anchor-scroll-offset` | `number \| (() => number)` | — | Pixel offset subtracted from scroll target; pass a getter so it is re-evaluated at click time |
|
|
43
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root `<nav>` element |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Basic usage — route links
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<TabNavigation :nav-item-data="navData" nav-align="center" />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import type { NavItemData } from "srcdev-nuxt-components"
|
|
55
|
+
|
|
56
|
+
const navData: NavItemData = {
|
|
57
|
+
main: [
|
|
58
|
+
{ text: "Home", href: "/" },
|
|
59
|
+
{ text: "About", href: "/about" },
|
|
60
|
+
{ text: "Services", href: "/services" },
|
|
61
|
+
{ text: "Contact", href: "/contact" },
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Items without a leading `#` render as `NuxtLink`. The active indicator tracks the current route
|
|
67
|
+
via `router-link-exact-active`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Anchor scroll usage
|
|
72
|
+
|
|
73
|
+
Hash-prefixed hrefs render as plain `<a>` elements to keep Vue Router out of the scroll path.
|
|
74
|
+
Pass a getter for the offset so the sticky bar height is read at click time (not at mount).
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<div ref="stickyRef" class="sticky-nav">
|
|
78
|
+
<TabNavigation
|
|
79
|
+
:nav-item-data="anchorData"
|
|
80
|
+
:anchor-scroll-offset="() => stickyRef?.offsetHeight ?? 0"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const stickyRef = ref<HTMLElement | null>(null)
|
|
87
|
+
|
|
88
|
+
const anchorData: NavItemData = {
|
|
89
|
+
main: [
|
|
90
|
+
{ text: "Overview", href: "#overview" },
|
|
91
|
+
{ text: "Features", href: "#features" },
|
|
92
|
+
{ text: "Pricing", href: "#pricing" },
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Clicks are handled by `useAnchorScroll`. Respects `prefers-reduced-motion` — motion-sensitive
|
|
98
|
+
users get an instant jump rather than smooth scroll. See the `composable-anchor-scroll.md` skill
|
|
99
|
+
for the full composable reference.
|
|
100
|
+
|
|
101
|
+
### Mixed navigation
|
|
102
|
+
|
|
103
|
+
Route links and hash anchors can coexist in the same `navItemData`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const navData: NavItemData = {
|
|
107
|
+
main: [
|
|
108
|
+
{ text: "Home", href: "/" }, // NuxtLink
|
|
109
|
+
{ text: "Overview", href: "#intro" }, // <a> + smooth scroll
|
|
110
|
+
{ text: "Blog", href: "/blog" }, // NuxtLink
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Responsive collapse
|
|
118
|
+
|
|
119
|
+
`useNavCollapse` measures the nav list on every resize. When the list overflows its container it
|
|
120
|
+
is hidden and replaced with a burger button. The button opens a slide-down panel listing all items.
|
|
121
|
+
|
|
122
|
+
The nav hides itself (opacity 0) until the first measurement completes to prevent a flash of the
|
|
123
|
+
wrong state.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Alignment variants
|
|
128
|
+
|
|
129
|
+
```vue
|
|
130
|
+
<TabNavigation :nav-item-data="navData" nav-align="left" /> <!-- default -->
|
|
131
|
+
<TabNavigation :nav-item-data="navData" nav-align="center" />
|
|
132
|
+
<TabNavigation :nav-item-data="navData" nav-align="right" />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`center` and `right` use `margin-inline: auto` / `margin-inline-start: auto` on the `ul`.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## CSS token API
|
|
140
|
+
|
|
141
|
+
Set `--tab-nav-*` tokens on any ancestor element — no prop needed. See `CONSUMER-STYLING.md` in
|
|
142
|
+
the component source folder for the full token reference.
|
|
143
|
+
|
|
144
|
+
### Horizontal nav
|
|
145
|
+
|
|
146
|
+
| Token | Default | Controls |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `--tab-nav-link-color` | `var(--slate-01, currentColor)` | Link text colour |
|
|
149
|
+
| `--tab-nav-link-hover-color` | `var(--slate-04, currentColor)` | Link hover colour |
|
|
150
|
+
| `--tab-nav-link-active-color` | `var(--slate-01, currentColor)` | Active link colour |
|
|
151
|
+
| `--tab-nav-link-size` | `1.6rem` | Font size |
|
|
152
|
+
| `--tab-nav-link-weight` | `400` | Font weight |
|
|
153
|
+
| `--tab-nav-link-tracking` | `0.06em` | Letter spacing |
|
|
154
|
+
| `--tab-nav-gap` | `2.2rem` | Gap between items |
|
|
155
|
+
| `--tab-nav-transition` | `250ms ease` | Colour fade on hover/active |
|
|
156
|
+
|
|
157
|
+
### Indicator decorators
|
|
158
|
+
|
|
159
|
+
| Token | Default | Controls |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `--tab-nav-decorator-indicator-color` | `var(--slate-01, currentColor)` | Active underline bar colour |
|
|
162
|
+
| `--tab-nav-decorator-hovered-bg` | `transparent` | Hover pill background (set non-transparent to enable) |
|
|
163
|
+
|
|
164
|
+
### Mobile panel
|
|
165
|
+
|
|
166
|
+
| Token | Default | Controls |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `--tab-nav-panel-bg` | `var(--page-bg, #1a1614)` | Panel background |
|
|
169
|
+
| `--tab-nav-panel-border-color` | `color-mix(in oklch, var(--slate-01) 35%, transparent)` | Panel top border |
|
|
170
|
+
| `--tab-nav-panel-item-border` | `color-mix(in oklch, var(--slate-01) 8%, transparent)` | Item dividers |
|
|
171
|
+
| `--tab-nav-panel-link-color` | `var(--slate-01, currentColor)` | Panel link colour |
|
|
172
|
+
| `--tab-nav-panel-padding-block` | `1.4rem` | Link vertical padding |
|
|
173
|
+
| `--tab-nav-panel-padding-inline` | `1.5rem` | Link horizontal padding |
|
|
174
|
+
| `--tab-nav-panel-slide-duration` | `350ms` | Panel open/close duration |
|
|
175
|
+
| `--tab-nav-panel-slide-easing` | `cubic-bezier(0.4, 0, 0.2, 1)` | Panel easing |
|
|
176
|
+
|
|
177
|
+
### Burger button
|
|
178
|
+
|
|
179
|
+
| Token | Default | Controls |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `--tab-nav-burger-color` | `var(--slate-01, currentColor)` | Bar colour |
|
|
182
|
+
| `--tab-nav-burger-width` | `22px` | Bar width |
|
|
183
|
+
| `--tab-nav-burger-height` | `1.5px` | Bar height |
|
|
184
|
+
| `--tab-nav-burger-gap` | `5px` | Gap between bars |
|
|
185
|
+
| `--tab-nav-burger-transition` | `300ms ease` | Open/close animation |
|
|
186
|
+
|
|
187
|
+
### Backdrop
|
|
188
|
+
|
|
189
|
+
| Token | Default | Controls |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `--tab-nav-backdrop-bg` | `oklch(0% 0 0 / 55%)` | Overlay colour |
|
|
192
|
+
| `--tab-nav-backdrop-blur` | `3px` | Overlay blur |
|
|
193
|
+
| `--tab-nav-backdrop-duration` | `350ms` | Overlay fade duration |
|
|
194
|
+
|
|
195
|
+
### Page-scoped theme example
|
|
196
|
+
|
|
197
|
+
```css
|
|
198
|
+
/* Unscoped <style> in the consuming page */
|
|
199
|
+
.services-page {
|
|
200
|
+
.tab-navigation {
|
|
201
|
+
--tab-nav-decorator-indicator-color: var(--brand-accent);
|
|
202
|
+
--tab-nav-decorator-hovered-bg: oklch(from var(--brand-accent) l c h / 0.1);
|
|
203
|
+
--tab-nav-link-active-color: var(--brand-accent);
|
|
204
|
+
--tab-nav-panel-bg: var(--page-bg);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Notes
|
|
212
|
+
|
|
213
|
+
- Auto-imported in Nuxt — no manual import needed.
|
|
214
|
+
- The indicator and hover pill use CSS Anchor Positioning and animate via `left`/`right` transitions
|
|
215
|
+
— set `--tab-nav-decorator-hovered-bg` to a semi-transparent colour to enable the pill; it is
|
|
216
|
+
invisible by default.
|
|
217
|
+
- `--tab-nav-transition` controls only the link colour fade; the indicator slide animation is
|
|
218
|
+
internally managed and does not inherit this token.
|
|
219
|
+
- The panel teleports its backdrop to `<body>` so it escapes any parent `overflow: hidden` or
|
|
220
|
+
stacking-context constraint.
|
|
221
|
+
- `--tab-nav-panel-bg` should match `--page-bg` for a seamless panel appearance in the collapsed
|
|
222
|
+
state.
|
|
@@ -20,7 +20,8 @@ Auto-imported by Nuxt — **do not create a local copy**.
|
|
|
20
20
|
|
|
21
21
|
| Option | Type | Default | Description |
|
|
22
22
|
|---|---|---|---|
|
|
23
|
-
| `offset` | `number
|
|
23
|
+
| `offset` | `number | (() => number)` | `0` | Pixels subtracted from the final scroll position. Pass a getter function to read a sticky element's height at scroll time rather than at composable init. |
|
|
24
|
+
| `offsetElement` | `Ref<HTMLElement | null>` | — | Convenience alternative to `offset`. Reads `element.offsetHeight` at scroll time. Takes priority over `offset` when both are supplied. |
|
|
24
25
|
|
|
25
26
|
### Returns
|
|
26
27
|
|
|
@@ -28,17 +29,69 @@ Auto-imported by Nuxt — **do not create a local copy**.
|
|
|
28
29
|
|---|---|---|
|
|
29
30
|
| `handleNavClick` | `(event: MouseEvent, href: string) => void` | Attach to click handlers. No-ops silently for non-`#` hrefs — safe on all links. |
|
|
30
31
|
| `scrollToAnchor` | `(hash: string) => void` | Scroll programmatically. Accepts `"#section"` or `"section"`. Respects motion preference and offset. |
|
|
32
|
+
| `activeHash` | `Ref<string>` | Reactive ref tracking the currently active hash. Starts `""` on server and after hydration is set from `window.location.hash`. Updated on every `handleNavClick` call. Use this to drive `is-active` classes — **do not use `route.hash`**, which is not updated by `history.pushState`. |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Active state
|
|
37
|
+
|
|
38
|
+
`activeHash` is the correct way to drive active-state styling on anchor nav links.
|
|
39
|
+
|
|
40
|
+
**Why not `route.hash`?** `handleNavClick` calls `history.pushState` directly (to avoid triggering Vue Router's scroll behaviour). `pushState` does not update Vue Router's reactive `route.hash`, so `route.hash` stays stale after clicks.
|
|
41
|
+
|
|
42
|
+
**Hydration safety:** `activeHash` is initialised to `""` on both server and client so the SSR-rendered HTML always matches the pre-mount client vdom. The real hash is applied in `onMounted`, after hydration, to avoid mismatches.
|
|
43
|
+
|
|
44
|
+
**Default active item:** `activeHash` is empty until `onMounted` fires. If no hash is in the URL, set a default in the component's own `onMounted` (which runs after the composable's `onMounted`):
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const { handleNavClick, activeHash } = useAnchorScroll({ offset: 64 });
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
// Default to first section when no hash is in the URL
|
|
51
|
+
if (!activeHash.value && sections[0]) activeHash.value = `#${sections[0].id}`;
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Binding active state in a template
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<a
|
|
59
|
+
v-for="section in sections"
|
|
60
|
+
:key="section.id"
|
|
61
|
+
:href="`#${section.id}`"
|
|
62
|
+
:class="{ 'is-active': `#${section.id}` === activeHash }"
|
|
63
|
+
@click="(e) => handleNavClick(e, `#${section.id}`)"
|
|
64
|
+
>{{ section.label }}</a>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Active state in TabNavigation
|
|
68
|
+
|
|
69
|
+
`TabNavigation` handles `activeHash` internally for hash nav items. No extra work needed —
|
|
70
|
+
just pass anchor hrefs in `navItemData` and the active indicator moves automatically on click
|
|
71
|
+
and on initial load (defaulting to the first hash item when the URL has no hash).
|
|
31
72
|
|
|
32
73
|
---
|
|
33
74
|
|
|
34
75
|
## Usage patterns
|
|
35
76
|
|
|
36
|
-
### 1. TabNavigation with
|
|
77
|
+
### 1. TabNavigation with a sticky site header
|
|
37
78
|
|
|
38
|
-
`TabNavigation`
|
|
39
|
-
|
|
79
|
+
`TabNavigation` accepts an `anchorScrollOffset` prop that is passed directly to `useAnchorScroll`.
|
|
80
|
+
Pass a getter so the live header height is read at scroll time — this stays correct if the
|
|
81
|
+
header resizes on different viewports.
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<header ref="headerRef">
|
|
85
|
+
<TabNavigation
|
|
86
|
+
:nav-item-data="navItemData"
|
|
87
|
+
:anchor-scroll-offset="() => headerRef?.offsetHeight ?? 0"
|
|
88
|
+
/>
|
|
89
|
+
</header>
|
|
90
|
+
```
|
|
40
91
|
|
|
41
92
|
```ts
|
|
93
|
+
const headerRef = ref<HTMLElement | null>(null);
|
|
94
|
+
|
|
42
95
|
const navItemData = {
|
|
43
96
|
main: [
|
|
44
97
|
{ text: "About", href: "#about" },
|
|
@@ -49,39 +102,55 @@ const navItemData = {
|
|
|
49
102
|
};
|
|
50
103
|
```
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
105
|
+
**CSS alternative** — if you want all anchor links site-wide to respect the sticky header
|
|
106
|
+
(not just the ones inside `TabNavigation`), add `scroll-padding-top` to `html` instead and
|
|
107
|
+
skip the prop:
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
/* ─ app/assets/styles/setup/01.config/_head.css ─ */
|
|
111
|
+
html {
|
|
112
|
+
scroll-padding-top: var(--sticky-header-height, 64px);
|
|
113
|
+
}
|
|
54
114
|
```
|
|
55
115
|
|
|
56
|
-
|
|
57
|
-
|
|
116
|
+
```css
|
|
117
|
+
/* ─ app/assets/styles/main.css ─ */
|
|
118
|
+
:root {
|
|
119
|
+
--sticky-header-height: 64px; /* adjust to match actual header height */
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`scrollIntoView` respects `scroll-padding-top`, so this works with the no-offset code path.
|
|
58
124
|
|
|
59
125
|
---
|
|
60
126
|
|
|
61
|
-
### 2. Sticky section nav with dynamic offset
|
|
127
|
+
### 2. Sticky section nav with dynamic offset (using offsetElement)
|
|
62
128
|
|
|
63
|
-
The
|
|
64
|
-
|
|
65
|
-
is read at scroll time — this stays correct if the bar resizes (e.g. on viewport changes).
|
|
129
|
+
The `offsetElement` option is the clearest way to derive an offset from an element ref.
|
|
130
|
+
It reads `offsetHeight` at scroll time, so resize changes are always captured.
|
|
66
131
|
|
|
67
132
|
```ts
|
|
68
133
|
const stickyNavRef = ref<HTMLElement | null>(null);
|
|
69
134
|
|
|
70
|
-
const { handleNavClick } = useAnchorScroll({
|
|
71
|
-
|
|
135
|
+
const { handleNavClick, activeHash } = useAnchorScroll({ offsetElement: stickyNavRef });
|
|
136
|
+
|
|
137
|
+
onMounted(() => {
|
|
138
|
+
if (!activeHash.value && sections[0]) activeHash.value = `#${sections[0].id}`;
|
|
72
139
|
});
|
|
73
140
|
```
|
|
74
141
|
|
|
75
142
|
```vue
|
|
76
143
|
<nav ref="stickyNavRef" class="sticky-section-nav">
|
|
77
|
-
<a
|
|
78
|
-
|
|
79
|
-
|
|
144
|
+
<a
|
|
145
|
+
v-for="section in sections"
|
|
146
|
+
:key="section.id"
|
|
147
|
+
:href="`#${section.id}`"
|
|
148
|
+
:class="{ 'is-active': `#${section.id}` === activeHash }"
|
|
149
|
+
@click="(e) => handleNavClick(e, `#${section.id}`)"
|
|
150
|
+
>{{ section.label }}</a>
|
|
80
151
|
</nav>
|
|
81
152
|
|
|
82
|
-
<section id="
|
|
83
|
-
<section id="pricing">…</section>
|
|
84
|
-
<section id="contact">…</section>
|
|
153
|
+
<section v-for="section in sections" :key="section.id" :id="section.id">…</section>
|
|
85
154
|
```
|
|
86
155
|
|
|
87
156
|
CSS to make the nav sticky:
|
|
@@ -102,7 +171,7 @@ Same pattern as above — useful for terms, privacy policy, or documentation pag
|
|
|
102
171
|
sidebar links to in-document sections.
|
|
103
172
|
|
|
104
173
|
```ts
|
|
105
|
-
const { handleNavClick } = useAnchorScroll({ offset: 24 }); // fixed header height
|
|
174
|
+
const { handleNavClick, activeHash } = useAnchorScroll({ offset: 24 }); // fixed header height
|
|
106
175
|
```
|
|
107
176
|
|
|
108
177
|
```vue
|
|
@@ -110,6 +179,7 @@ const { handleNavClick } = useAnchorScroll({ offset: 24 }); // fixed header heig
|
|
|
110
179
|
<nav>
|
|
111
180
|
<a v-for="section in termsSections" :key="section.id"
|
|
112
181
|
:href="`#${section.id}`"
|
|
182
|
+
:class="{ 'is-active': `#${section.id}` === activeHash }"
|
|
113
183
|
@click="(e) => handleNavClick(e, `#${section.id}`)">
|
|
114
184
|
{{ section.title }}
|
|
115
185
|
</a>
|
|
@@ -156,10 +226,10 @@ onMounted(() => {
|
|
|
156
226
|
|
|
157
227
|
## How offset works
|
|
158
228
|
|
|
159
|
-
Without `offset`, the composable calls `el.scrollIntoView({ behavior, block: "start" })` —
|
|
229
|
+
Without `offset` or `offsetElement`, the composable calls `el.scrollIntoView({ behavior, block: "start" })` —
|
|
160
230
|
the element's top edge aligns with the viewport top.
|
|
161
231
|
|
|
162
|
-
With
|
|
232
|
+
With an offset, it uses `window.scrollTo` with a calculated position:
|
|
163
233
|
|
|
164
234
|
```
|
|
165
235
|
top = el.getBoundingClientRect().top + window.scrollY - offset
|
|
@@ -168,16 +238,19 @@ top = el.getBoundingClientRect().top + window.scrollY - offset
|
|
|
168
238
|
This shifts the final resting position downward by `offset` pixels, so a sticky bar of that
|
|
169
239
|
height does not overlap the section heading.
|
|
170
240
|
|
|
171
|
-
|
|
241
|
+
**`offsetElement` vs `offset` getter:**
|
|
172
242
|
|
|
173
243
|
```ts
|
|
174
|
-
// ✅
|
|
175
|
-
const { handleNavClick } = useAnchorScroll({
|
|
244
|
+
// ✅ offsetElement — reads offsetHeight at click time, no boilerplate
|
|
245
|
+
const { handleNavClick, activeHash } = useAnchorScroll({ offsetElement: navRef });
|
|
246
|
+
|
|
247
|
+
// ✅ offset getter — equivalent, useful when the offset is derived from more than one element
|
|
248
|
+
const { handleNavClick, activeHash } = useAnchorScroll({
|
|
176
249
|
offset: () => navRef.value?.offsetHeight ?? 0,
|
|
177
250
|
});
|
|
178
251
|
|
|
179
|
-
// ✗
|
|
180
|
-
const { handleNavClick } = useAnchorScroll({
|
|
252
|
+
// ✗ Static offset — stale if the bar changes height later
|
|
253
|
+
const { handleNavClick, activeHash } = useAnchorScroll({
|
|
181
254
|
offset: navRef.value?.offsetHeight ?? 0,
|
|
182
255
|
});
|
|
183
256
|
```
|
|
@@ -195,10 +268,19 @@ When `window.matchMedia("(prefers-reduced-motion: reduce)").matches` is `true`:
|
|
|
195
268
|
No configuration needed — the check happens at call time, so toggling the OS preference mid-session
|
|
196
269
|
takes effect immediately on the next click.
|
|
197
270
|
|
|
271
|
+
> **Debugging tip:** If clicks produce an instant jump instead of smooth scroll, check whether
|
|
272
|
+
> "Reduce Motion" is enabled in your OS accessibility settings (macOS: System Preferences →
|
|
273
|
+
> Accessibility → Display → Reduce Motion). This is intentional behaviour, not a bug.
|
|
274
|
+
|
|
198
275
|
---
|
|
199
276
|
|
|
200
277
|
## Notes
|
|
201
278
|
|
|
279
|
+
- **`activeHash` vs `route.hash`** — always use `activeHash` for active-state classes. `route.hash`
|
|
280
|
+
is not updated by `history.pushState` and will be stale after clicks.
|
|
281
|
+
- **Hydration mismatches** — `activeHash` is always `""` at render time; the real value is set in
|
|
282
|
+
`onMounted`. Never initialise it from `window.location.hash` in setup — that causes a server/client
|
|
283
|
+
class mismatch.
|
|
202
284
|
- **`history.pushState`** — `handleNavClick` pushes the hash into the URL so the back button and
|
|
203
285
|
deep links work correctly. This runs after `preventDefault` stops Vue Router from navigating,
|
|
204
286
|
so the URL stays in sync without triggering a router scroll.
|
|
@@ -2,77 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
`useDialogControls` manages open/closed state for one or more named dialogs
|
|
5
|
+
`useDialogControls` manages open/closed state for one or more named dialogs with optional
|
|
6
|
+
confirm/cancel callbacks. Pass all dialog IDs and their callbacks in a single config object
|
|
7
|
+
at call time — no separate initialisation or registration step needed.
|
|
6
8
|
|
|
7
|
-
**This composable ships inside the `srcdev-nuxt-components` layer** (`app/composables/useDialogControls.ts`).
|
|
9
|
+
**This composable ships inside the `srcdev-nuxt-components` layer** (`app/composables/useDialogControls.ts`).
|
|
10
|
+
It is auto-imported via the Nuxt layer — **do not create a local copy** in the consuming app.
|
|
8
11
|
|
|
9
12
|
## Usage
|
|
10
13
|
|
|
11
|
-
### 1.
|
|
14
|
+
### 1. Set up dialog controls
|
|
12
15
|
|
|
13
|
-
Call `
|
|
16
|
+
Call `useDialogControls` with a config object whose keys are dialog IDs:
|
|
14
17
|
|
|
15
18
|
```ts
|
|
16
|
-
const { dialogsConfig,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const { dialogsConfig, openDialog, closeDialog } = useDialogControls({
|
|
20
|
+
confirmDelete: {
|
|
21
|
+
onConfirm: () => deleteItem(),
|
|
22
|
+
onCancel: () => console.log("Cancelled"),
|
|
23
|
+
},
|
|
24
|
+
editProfile: {
|
|
25
|
+
onConfirm: () => saveProfile(),
|
|
26
|
+
},
|
|
27
|
+
})
|
|
19
28
|
```
|
|
20
29
|
|
|
21
|
-
Each
|
|
30
|
+
Each key gets a reactive boolean in `dialogsConfig` (initially `false` = closed).
|
|
31
|
+
Callbacks are optional — omit either or both.
|
|
22
32
|
|
|
23
33
|
### 2. Bind to a dialog component
|
|
24
34
|
|
|
25
|
-
Use `
|
|
35
|
+
Use `v-if` + `v-model` with `dialogsConfig[id]`:
|
|
26
36
|
|
|
27
37
|
```vue
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
<DisplayDialog
|
|
39
|
+
v-if="dialogsConfig['confirmDelete']"
|
|
40
|
+
v-model="dialogsConfig['confirmDelete']"
|
|
41
|
+
variant="confirm"
|
|
42
|
+
data-dialog-id="confirmDelete"
|
|
43
|
+
>
|
|
44
|
+
<template #dialogTitle>
|
|
45
|
+
<p>Confirm delete?</p>
|
|
46
|
+
</template>
|
|
47
|
+
<template #dialogContent>
|
|
48
|
+
<p>This action cannot be undone.</p>
|
|
34
49
|
</template>
|
|
35
|
-
|
|
50
|
+
<template #actionButtonLeft>
|
|
51
|
+
<button @click="closeDialog('confirmDelete', 'cancel')">Cancel</button>
|
|
52
|
+
</template>
|
|
53
|
+
<template #actionButtonRight>
|
|
54
|
+
<button @click="closeDialog('confirmDelete', 'confirm')">Delete</button>
|
|
55
|
+
</template>
|
|
56
|
+
</DisplayDialog>
|
|
36
57
|
```
|
|
37
58
|
|
|
38
59
|
### 3. Open and close dialogs
|
|
39
60
|
|
|
40
61
|
```ts
|
|
41
62
|
// Open
|
|
42
|
-
|
|
63
|
+
openDialog("confirmDelete")
|
|
43
64
|
|
|
44
|
-
// Close without
|
|
45
|
-
|
|
65
|
+
// Close without firing a callback
|
|
66
|
+
closeDialog("confirmDelete")
|
|
46
67
|
|
|
47
|
-
// Close and fire
|
|
48
|
-
|
|
49
|
-
controlDialogs("confirmDelete", false, "cancel") // fires onCancel if registered
|
|
50
|
-
```
|
|
68
|
+
// Close and fire onConfirm
|
|
69
|
+
closeDialog("confirmDelete", "confirm")
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Register confirm/cancel callbacks before the dialog is opened:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
registerDialogCallbacks("confirmDelete", {
|
|
58
|
-
onConfirm: () => deleteItem(),
|
|
59
|
-
onCancel: () => console.log("Cancelled"),
|
|
60
|
-
})
|
|
71
|
+
// Close and fire onCancel
|
|
72
|
+
closeDialog("confirmDelete", "cancel")
|
|
61
73
|
```
|
|
62
74
|
|
|
63
|
-
Callbacks fire when `controlDialogs` is called with a matching action string.
|
|
64
|
-
|
|
65
75
|
## API reference
|
|
66
76
|
|
|
67
|
-
| Return value | Description |
|
|
68
|
-
|
|
69
|
-
| `dialogsConfig` | Reactive
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `registerDialogCallbacks(id, { onConfirm?, onCancel? })` | Register lifecycle callbacks for a dialog ID. |
|
|
77
|
+
| Return value | Type | Description |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `dialogsConfig` | `Record<string, boolean>` | Reactive map of dialog ID → open state. Bind to `v-model` and `v-if`. |
|
|
80
|
+
| `openDialog(name)` | `(name: string) => void` | Sets `dialogsConfig[name]` to `true`. |
|
|
81
|
+
| `closeDialog(name, action?)` | `(name: string, action?: 'confirm' \| 'cancel') => void` | Sets `dialogsConfig[name]` to `false`. If `action` is provided, fires the registered callback first. |
|
|
73
82
|
|
|
74
83
|
## Notes
|
|
75
84
|
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
- `
|
|
85
|
+
- State is scoped to the component instance — each component that calls `useDialogControls` gets
|
|
86
|
+
its own `dialogsConfig`. It is not a global store.
|
|
87
|
+
- `v-if="dialogsConfig['id']"` on the dialog component ensures it only mounts when open, which
|
|
88
|
+
triggers `onMounted` body-lock logic inside `DisplayDialog`.
|
|
89
|
+
- Callbacks fire before the dialog closes — useful for async operations that need to run while
|
|
90
|
+
the dialog is still visible.
|
package/.claude/skills/index.md
CHANGED
|
@@ -48,7 +48,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
48
48
|
├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
|
|
49
49
|
├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
|
|
50
50
|
├── composable-colour-scheme.md — useColourScheme: reactive light/dark/auto switching, localStorage persistence, runtime config
|
|
51
|
-
├── composable-dialog-controls.md — useDialogControls:
|
|
51
|
+
├── composable-dialog-controls.md — useDialogControls: single-call setup with config object, openDialog/closeDialog API, confirm/cancel callbacks
|
|
52
52
|
├── composable-anchor-scroll.md — useAnchorScroll: smooth anchor scrolling with reduced-motion support, dynamic offset, and TabNavigation integration
|
|
53
53
|
├── composable-tooltips-guide.md — useTooltipsGuide: sequential popover guide with auto-start, dismiss-to-advance, manual controls
|
|
54
54
|
└── components/
|
|
@@ -56,7 +56,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
56
56
|
├── eyebrow-text.md — EyebrowText props, usage patterns, styling
|
|
57
57
|
├── hero-text.md — HeroText props, usage patterns, styling
|
|
58
58
|
├── layout-grid-by-cols.md — LayoutGridByCols dynamic slots (item-{n}), props, column/gap/breakpoint decisions
|
|
59
|
-
├──
|
|
59
|
+
├── page-row.md — PageRow layout primitive: CSS grid named lines, nesting pattern, align prop, aria-labelledby, CSS token API
|
|
60
60
|
├── link-text.md — LinkText props, slots, usage patterns, styling
|
|
61
61
|
├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
|
|
62
62
|
├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
|
|
@@ -74,12 +74,15 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
74
74
|
├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
|
|
75
75
|
├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
|
|
76
76
|
├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
77
|
+
├── tab-navigation.md — TabNavigation: horizontal tab nav with CSS anchor-positioning indicators, anchor scroll, burger collapse, full CSS token API
|
|
77
78
|
├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
|
|
78
79
|
├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
|
|
79
80
|
├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
|
|
80
81
|
├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
|
|
81
82
|
├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-min-col-size/gap tokens, semantic tag + aria
|
|
82
83
|
├── display-avatar.md — DisplayAvatar: circular avatar with image/initials fallback, size variants, chip badge, icon slot, styleClassPassthrough
|
|
84
|
+
├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
|
|
85
|
+
├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, CSS token API
|
|
83
86
|
├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
|
|
84
87
|
├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, full CSS token API for border/outline/colour
|
|
85
88
|
├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
|