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.
Files changed (223) hide show
  1. package/.claude/settings.json +2 -4
  2. package/.claude/settings.local.json +22 -2
  3. package/.claude/skills/components/card-core.md +205 -0
  4. package/.claude/skills/components/display-dialog.md +128 -0
  5. package/.claude/skills/components/page-hero-highlights.md +1 -1
  6. package/.claude/skills/components/page-row.md +157 -0
  7. package/.claude/skills/components/services-card-grid.md +4 -4
  8. package/.claude/skills/components/services-section-grid.md +4 -4
  9. package/.claude/skills/components/tab-navigation.md +222 -0
  10. package/.claude/skills/composable-anchor-scroll.md +110 -28
  11. package/.claude/skills/composable-dialog-controls.md +55 -43
  12. package/.claude/skills/index.md +5 -2
  13. package/app/components/01.atoms/card/CONSUMER-STYLING.md +175 -0
  14. package/app/components/01.atoms/card/CardCore.vue +48 -21
  15. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +163 -0
  16. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +277 -0
  17. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +177 -0
  18. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +264 -0
  19. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +18 -0
  20. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +90 -0
  21. package/app/components/01.atoms/page-row/PageRow.vue +115 -0
  22. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +311 -0
  23. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +191 -0
  24. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +145 -0
  25. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +52 -11
  26. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +12 -6
  27. package/app/components/{forms/ui → 05.forms/form-field}/FormField.vue +12 -19
  28. package/app/components/{forms/ui → 05.forms/form-wrapper}/FormWrapper.vue +6 -6
  29. package/app/components/{forms → 05.forms}/input-button/InputButtonCore.vue +2 -0
  30. package/app/components/{forms → 05.forms}/input-button/stories/InputButtonCore.stories.ts +1 -1
  31. package/app/components/{forms → 05.forms}/input-text/InputTextCore.vue +73 -73
  32. package/app/components/{forms → 05.forms}/input-text/variants/InputTextAsNumberWithLabel.vue +29 -29
  33. package/app/components/carousels/CarouselBasic.vue +99 -99
  34. package/app/components/carousels/CarouselInfinite.vue +150 -150
  35. package/app/components/deep-expanding-menu/DeepExpandingMenu.vue +111 -111
  36. package/app/components/responsive-header/ResponsiveHeader.vue +250 -282
  37. package/app/components/test-storybook/TestStorybook.vue +16 -16
  38. package/app/composables/tests/useDialogControls.spec.ts +34 -64
  39. package/app/composables/useAnchorScroll.ts +16 -3
  40. package/app/composables/useDialogControls.ts +19 -33
  41. package/app/composables/useFormControl.ts +0 -2
  42. package/app/composables/useNavCollapse.ts +5 -1
  43. package/app/layouts/default.vue +10 -7
  44. package/app/pages/auto-grid.vue +37 -20
  45. package/app/pages/banner-video.vue +2 -2
  46. package/app/pages/forms/examples/buttons/index.vue +189 -215
  47. package/app/pages/forms/examples/material/text-fields.vue +2 -2
  48. package/app/pages/grid-stack.vue +10 -10
  49. package/app/pages/index.vue +20 -20
  50. package/app/pages/samaritan.vue +4 -4
  51. package/app/pages/typography/hero-text.vue +6 -6
  52. package/app/pages/typography/index.vue +4 -4
  53. package/app/pages/typography/page-body.vue +4 -4
  54. package/app/pages/typography/page-heading.vue +4 -4
  55. package/app/pages/typography/page-link.vue +4 -4
  56. package/app/pages/ui/accordian.vue +4 -4
  57. package/app/pages/ui/anchor-scroll-tab-navigation.vue +191 -0
  58. package/app/pages/ui/anchor-scroll.vue +105 -71
  59. package/app/pages/ui/animated-svg-text.vue +4 -4
  60. package/app/pages/ui/block-decorators.vue +11 -11
  61. package/app/pages/ui/card-core.vue +9 -9
  62. package/app/pages/ui/carousel-basic.vue +4 -4
  63. package/app/pages/ui/carousel-flip.vue +2 -2
  64. package/app/pages/ui/carousel-infinite.vue +4 -4
  65. package/app/pages/ui/clipped-panels.vue +10 -10
  66. package/app/pages/ui/contact-section.vue +4 -4
  67. package/app/pages/ui/container-glow.vue +2 -3
  68. package/app/pages/ui/deep-expanding-menu.vue +138 -0
  69. package/app/pages/ui/display-avatar.vue +2 -2
  70. package/app/pages/ui/display-banner.vue +2 -2
  71. package/app/pages/ui/display-chip.vue +32 -25
  72. package/app/pages/ui/display-details.vue +6 -10
  73. package/app/pages/ui/display-dialog.vue +96 -131
  74. package/app/pages/ui/display-pill.vue +27 -33
  75. package/app/pages/ui/display-prompt.vue +4 -4
  76. package/app/pages/ui/display-toast.vue +2 -2
  77. package/app/pages/ui/expanding-panel.vue +12 -16
  78. package/app/pages/ui/glowing-border.vue +12 -12
  79. package/app/pages/ui/layout-grid-a.vue +6 -6
  80. package/app/pages/ui/layout-grid-b.vue +4 -4
  81. package/app/pages/ui/magnetic-navigation.vue +6 -6
  82. package/app/pages/ui/marquee-scroller.vue +2 -2
  83. package/app/pages/ui/mask-element.vue +2 -2
  84. package/app/pages/ui/masonry-columns.vue +4 -4
  85. package/app/pages/ui/masonry-grid-ordered-experiment.vue +4 -4
  86. package/app/pages/ui/masonry-grid-ordered.vue +4 -4
  87. package/app/pages/ui/masonry-grid-sorted.vue +4 -4
  88. package/app/pages/ui/masonry-grid.vue +4 -4
  89. package/app/pages/ui/navigation/navigation-horizontal.vue +12 -12
  90. package/app/pages/ui/navigation/tab-navigation.vue +493 -0
  91. package/app/pages/ui/page-row.vue +127 -0
  92. package/app/pages/ui/price-list.vue +4 -4
  93. package/app/pages/ui/profile-section.vue +4 -4
  94. package/app/pages/ui/qr-code/[componentName].vue +6 -6
  95. package/app/pages/ui/rotating-carousel.vue +16 -16
  96. package/app/pages/ui/scroll-reveal-image.vue +20 -20
  97. package/app/pages/ui/section-parallax.vue +7 -7
  98. package/app/pages/ui/services/colour-finder.vue +4 -4
  99. package/app/pages/ui/services/services-cards.vue +4 -4
  100. package/app/pages/ui/services/services-section/[slug].vue +4 -4
  101. package/app/pages/ui/services/services-sections-compact.vue +6 -6
  102. package/app/pages/ui/settings.vue +10 -10
  103. package/app/pages/ui/simple-grid.vue +2 -2
  104. package/app/pages/ui/slider-gallery.vue +5 -5
  105. package/app/pages/ui/tabs-y.vue +2 -3
  106. package/app/pages/ui/tabs.vue +6 -9
  107. package/app/pages/ui/tooltips.vue +9 -9
  108. package/app/pages/ui/wipe-away-vertical.vue +14 -14
  109. package/package.json +1 -1
  110. package/.claude/skills/components/layout-row.md +0 -239
  111. package/app/components/display-dialog/DisplayDialogCore.vue +0 -276
  112. package/app/components/display-dialog/variants/DisplayDialogConfirm.vue +0 -47
  113. package/app/components/display-dialog/variants/DisplayDialogScrollableContent.vue +0 -51
  114. package/app/components/forms/c12/utils.ts +0 -14
  115. package/app/components/layout-row/LayoutRow.vue +0 -171
  116. package/app/components/layout-row/stories/LayoutRow.stories.ts +0 -528
  117. package/app/pages/ui/layout-row.vue +0 -139
  118. /package/app/components/{forms → 05.forms}/form-errors/InputError.vue +0 -0
  119. /package/app/components/{forms → 05.forms}/form-errors/tests/InputError.spec.ts +0 -0
  120. /package/app/components/{forms → 05.forms}/form-fieldset/FormFieldset.vue +0 -0
  121. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts +0 -0
  122. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
  123. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-firefox-darwin.png +0 -0
  124. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-webkit-darwin.png +0 -0
  125. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
  126. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-firefox-darwin.png +0 -0
  127. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-webkit-darwin.png +0 -0
  128. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
  129. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-firefox-darwin.png +0 -0
  130. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-webkit-darwin.png +0 -0
  131. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
  132. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-firefox-darwin.png +0 -0
  133. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-webkit-darwin.png +0 -0
  134. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
  135. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-firefox-darwin.png +0 -0
  136. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-webkit-darwin.png +0 -0
  137. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
  138. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-firefox-darwin.png +0 -0
  139. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
  140. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-firefox-darwin.png +0 -0
  141. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-webkit-darwin.png +0 -0
  142. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
  143. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-firefox-darwin.png +0 -0
  144. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-webkit-darwin.png +0 -0
  145. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
  146. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-firefox-darwin.png +0 -0
  147. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-webkit-darwin.png +0 -0
  148. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
  149. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-firefox-darwin.png +0 -0
  150. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-webkit-darwin.png +0 -0
  151. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
  152. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-firefox-darwin.png +0 -0
  153. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-webkit-darwin.png +0 -0
  154. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
  155. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-firefox-darwin.png +0 -0
  156. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-webkit-darwin.png +0 -0
  157. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
  158. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-firefox-darwin.png +0 -0
  159. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-webkit-darwin.png +0 -0
  160. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-webkit-darwin.png +0 -0
  161. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
  162. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-firefox-darwin.png +0 -0
  163. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-webkit-darwin.png +0 -0
  164. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
  165. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-firefox-darwin.png +0 -0
  166. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-webkit-darwin.png +0 -0
  167. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
  168. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-firefox-darwin.png +0 -0
  169. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-webkit-darwin.png +0 -0
  170. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
  171. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-firefox-darwin.png +0 -0
  172. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-webkit-darwin.png +0 -0
  173. /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
  174. /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
  175. /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
  176. /package/app/components/{forms → 05.forms}/input-button/tests/InputButtonCore.spec.ts +0 -0
  177. /package/app/components/{forms → 05.forms}/input-button/tests/__snapshots__/InputButtonCore.spec.ts.snap +0 -0
  178. /package/app/components/{forms → 05.forms}/input-checkbox/MultipleCheckboxes.vue +0 -0
  179. /package/app/components/{forms → 05.forms}/input-checkbox/SingleCheckbox.vue +0 -0
  180. /package/app/components/{forms → 05.forms}/input-checkbox/tests/MultipleCheckboxes.spec.ts +0 -0
  181. /package/app/components/{forms → 05.forms}/input-checkbox/tests/data/tags.json +0 -0
  182. /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioButton.vue +0 -0
  183. /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioCore.vue +0 -0
  184. /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioWithLabel.vue +0 -0
  185. /package/app/components/{forms → 05.forms}/input-checkbox-radio/tests/InputCheckboxRadioCore.spec.ts +0 -0
  186. /package/app/components/{forms → 05.forms}/input-copy/InputCopyCore.vue +0 -0
  187. /package/app/components/{forms → 05.forms}/input-copy/stories/InputCopyCore.stories.ts +0 -0
  188. /package/app/components/{forms → 05.forms}/input-copy/tests/InputCopyCore.spec.ts +0 -0
  189. /package/app/components/{forms → 05.forms}/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +0 -0
  190. /package/app/components/{forms → 05.forms}/input-description/InputDescription.vue +0 -0
  191. /package/app/components/{forms → 05.forms}/input-description/tests/InputDescription.spec.ts +0 -0
  192. /package/app/components/{forms → 05.forms}/input-label/InputLabel.vue +0 -0
  193. /package/app/components/{forms → 05.forms}/input-label/tests/InputLabel.spec.ts +0 -0
  194. /package/app/components/{forms → 05.forms}/input-number/InputNumberCore.vue +0 -0
  195. /package/app/components/{forms → 05.forms}/input-number/variants/InputNumberDefault.vue +0 -0
  196. /package/app/components/{forms → 05.forms}/input-radio/MultipleRadiobuttons.vue +0 -0
  197. /package/app/components/{forms → 05.forms}/input-radio/tests/MultipleRadioButtons.spec.ts +0 -0
  198. /package/app/components/{forms → 05.forms}/input-radio/tests/data/tags.json +0 -0
  199. /package/app/components/{forms → 05.forms}/input-range/InputRangeCore.vue +0 -0
  200. /package/app/components/{forms → 05.forms}/input-range/variants/InputRangeDefault.vue +0 -0
  201. /package/app/components/{forms → 05.forms}/input-range-fancy/InputRangeFancyCore.vue +0 -0
  202. /package/app/components/{forms → 05.forms}/input-range-fancy/InputRangeFancyWithLabel.vue +0 -0
  203. /package/app/components/{forms → 05.forms}/input-select/InputSelectCore.vue +0 -0
  204. /package/app/components/{forms → 05.forms}/input-select/variants/InputSelectWithLabel.vue +0 -0
  205. /package/app/components/{forms → 05.forms}/input-text/stories/InputPasswordWithLabel.stories.ts +0 -0
  206. /package/app/components/{forms → 05.forms}/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -0
  207. /package/app/components/{forms → 05.forms}/input-text/stories/InputTextCore.stories.ts +0 -0
  208. /package/app/components/{forms → 05.forms}/input-text/stories/InputTextWithLabel.stories.ts +0 -0
  209. /package/app/components/{forms → 05.forms}/input-text/variants/InputPasswordWithLabel.vue +0 -0
  210. /package/app/components/{forms → 05.forms}/input-text/variants/InputTextWithLabel.vue +0 -0
  211. /package/app/components/{forms → 05.forms}/input-textarea/InputTextareaCore.vue +0 -0
  212. /package/app/components/{forms → 05.forms}/input-textarea/stories/InputTextareaCore.stories.ts +0 -0
  213. /package/app/components/{forms → 05.forms}/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -0
  214. /package/app/components/{forms → 05.forms}/input-textarea/variants/InputTextareaWithLabel.vue +0 -0
  215. /package/app/components/{forms → 05.forms}/pending-effect/PendingEffect.vue +0 -0
  216. /package/app/components/{forms → 05.forms}/toggle-switch/ToggleSwitchCore.vue +0 -0
  217. /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -0
  218. /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -0
  219. /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -0
  220. /package/app/components/{forms → 05.forms}/toggle-switch/variants/ToggleSwitchWithLabel.vue +0 -0
  221. /package/app/components/{forms → 05.forms}/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +0 -0
  222. /package/app/components/{forms → 05.forms}/triple-toggle-switch/TripleToggleSwitchCore.vue +0 -0
  223. /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 \| (() => 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. |
23
+ | `offset` | `number &#124; (() => 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 &#124; 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 anchor links (no extra code needed)
77
+ ### 1. TabNavigation with a sticky site header
37
78
 
38
- `TabNavigation` has `useAnchorScroll` wired up internally. Pass anchor hrefs in
39
- `navItemData.main` and smooth scrolling works automatically.
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
- ```vue
53
- <TabNavigation :nav-item-data="navItemData" />
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
- No offset is applied here. If your site header is fixed/sticky and obscures section headings,
57
- use pattern 2 below to build a custom nav with an explicit offset.
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 most common single-page pattern: a sticky bar at the top of the content area, where
64
- each link scrolls to a section below it. Pass a getter for `offset` so the bar's live height
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
- offset: () => stickyNavRef.value?.offsetHeight ?? 0,
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 href="#overview" @click="(e) => handleNavClick(e, '#overview')">Overview</a>
78
- <a href="#pricing" @click="(e) => handleNavClick(e, '#pricing')">Pricing</a>
79
- <a href="#contact" @click="(e) => handleNavClick(e, '#contact')">Contact</a>
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="overview">…</section>
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 `offset`, it uses `window.scrollTo` with a calculated position:
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
- **Use a getter when the bar can resize:**
241
+ **`offsetElement` vs `offset` getter:**
172
242
 
173
243
  ```ts
174
- // ✅ Read height at click time
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
- // ✗ Captured at init — stale if the bar changes height later
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 (modals, confirmation panels, etc.) with optional confirm/cancel callbacks. It is the standard pattern for dialog orchestration in consuming apps.
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`). It is auto-imported via the Nuxt layer — **do not create a local copy** in the consuming app.
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. Initialise named dialogs
14
+ ### 1. Set up dialog controls
12
15
 
13
- Call `initialiseDialogs` with an array of string IDs one per dialog you need to control:
16
+ Call `useDialogControls` with a config object whose keys are dialog IDs:
14
17
 
15
18
  ```ts
16
- const { dialogsConfig, controlDialogs, initialiseDialogs, registerDialogCallbacks } = useDialogControls()
17
-
18
- initialiseDialogs(["confirmDelete", "editProfile"])
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 ID gets a reactive boolean in `dialogsConfig` (initially `false` = closed).
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 `dialogsConfig[id]` as the `v-model` or `:open` prop on your dialog:
35
+ Use `v-if` + `v-model` with `dialogsConfig[id]`:
26
36
 
27
37
  ```vue
28
- <ExpandingPanel v-model="dialogsConfig.confirmDelete">
29
- <template #summary>Confirm delete</template>
30
- <template #content>
31
- <p>Are you sure?</p>
32
- <button @click="controlDialogs('confirmDelete', false, 'confirm')">Yes, delete</button>
33
- <button @click="controlDialogs('confirmDelete', false, 'cancel')">Cancel</button>
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
- </ExpandingPanel>
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
- controlDialogs("confirmDelete", true)
63
+ openDialog("confirmDelete")
43
64
 
44
- // Close without action
45
- controlDialogs("confirmDelete", false)
65
+ // Close without firing a callback
66
+ closeDialog("confirmDelete")
46
67
 
47
- // Close and fire a callback
48
- controlDialogs("confirmDelete", false, "confirm") // fires onConfirm if registered
49
- controlDialogs("confirmDelete", false, "cancel") // fires onCancel if registered
50
- ```
68
+ // Close and fire onConfirm
69
+ closeDialog("confirmDelete", "confirm")
51
70
 
52
- ### 4. Register callbacks (optional)
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 object `{ [id]: boolean }`. Bind to dialog `v-model` or `:open`. |
70
- | `initialiseDialogs(ids)` | Seeds `dialogsConfig` with `false` for each ID. Call once in `<script setup>`. |
71
- | `controlDialogs(name, state, action?)` | Set `dialogsConfig[name]` to `state`. If `state` is `false` and `action` is provided, fires the registered callback before closing. |
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
- - `useDialogControls` is scoped to the component instance — each component that calls it gets its own `dialogsConfig`. It is not a global store; do not expect state to persist across components.
77
- - For a single dialog, `initialiseDialogs(["myDialog"])` is still required — omitting it means `dialogsConfig.myDialog` is `undefined` and reactivity won't work.
78
- - `controlDialogs` checks for the callback before setting state, so the callback always runs before the dialog closes.
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.
@@ -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: named dialog open/close state with confirm/cancel callbacks
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
- ├── layout-row.md LayoutRow variant guide, width/margin decisions, usage patterns
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