srcdev-nuxt-components 9.1.49 → 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 (220) 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-dialog-controls.md +55 -43
  11. package/.claude/skills/index.md +5 -2
  12. package/app/components/01.atoms/card/CONSUMER-STYLING.md +175 -0
  13. package/app/components/01.atoms/card/CardCore.vue +48 -21
  14. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +163 -0
  15. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +277 -0
  16. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +177 -0
  17. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +264 -0
  18. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +18 -0
  19. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +90 -0
  20. package/app/components/01.atoms/page-row/PageRow.vue +115 -0
  21. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +311 -0
  22. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +191 -0
  23. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +145 -0
  24. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +21 -11
  25. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +12 -6
  26. package/app/components/{forms/ui → 05.forms/form-field}/FormField.vue +12 -19
  27. package/app/components/{forms/ui → 05.forms/form-wrapper}/FormWrapper.vue +6 -6
  28. package/app/components/{forms → 05.forms}/input-button/InputButtonCore.vue +2 -0
  29. package/app/components/{forms → 05.forms}/input-button/stories/InputButtonCore.stories.ts +1 -1
  30. package/app/components/{forms → 05.forms}/input-text/InputTextCore.vue +73 -73
  31. package/app/components/{forms → 05.forms}/input-text/variants/InputTextAsNumberWithLabel.vue +29 -29
  32. package/app/components/carousels/CarouselBasic.vue +99 -99
  33. package/app/components/carousels/CarouselInfinite.vue +150 -150
  34. package/app/components/deep-expanding-menu/DeepExpandingMenu.vue +111 -111
  35. package/app/components/responsive-header/ResponsiveHeader.vue +250 -282
  36. package/app/components/test-storybook/TestStorybook.vue +16 -16
  37. package/app/composables/tests/useDialogControls.spec.ts +34 -64
  38. package/app/composables/useDialogControls.ts +19 -33
  39. package/app/composables/useFormControl.ts +0 -2
  40. package/app/layouts/default.vue +9 -7
  41. package/app/pages/auto-grid.vue +37 -20
  42. package/app/pages/banner-video.vue +2 -2
  43. package/app/pages/forms/examples/buttons/index.vue +189 -215
  44. package/app/pages/forms/examples/material/text-fields.vue +2 -2
  45. package/app/pages/grid-stack.vue +10 -10
  46. package/app/pages/index.vue +20 -20
  47. package/app/pages/samaritan.vue +4 -4
  48. package/app/pages/typography/hero-text.vue +6 -6
  49. package/app/pages/typography/index.vue +4 -4
  50. package/app/pages/typography/page-body.vue +4 -4
  51. package/app/pages/typography/page-heading.vue +4 -4
  52. package/app/pages/typography/page-link.vue +4 -4
  53. package/app/pages/ui/accordian.vue +4 -4
  54. package/app/pages/ui/anchor-scroll-tab-navigation.vue +77 -66
  55. package/app/pages/ui/anchor-scroll.vue +94 -70
  56. package/app/pages/ui/animated-svg-text.vue +4 -4
  57. package/app/pages/ui/block-decorators.vue +11 -11
  58. package/app/pages/ui/card-core.vue +9 -9
  59. package/app/pages/ui/carousel-basic.vue +4 -4
  60. package/app/pages/ui/carousel-flip.vue +2 -2
  61. package/app/pages/ui/carousel-infinite.vue +4 -4
  62. package/app/pages/ui/clipped-panels.vue +10 -10
  63. package/app/pages/ui/contact-section.vue +4 -4
  64. package/app/pages/ui/container-glow.vue +2 -3
  65. package/app/pages/ui/deep-expanding-menu.vue +138 -0
  66. package/app/pages/ui/display-avatar.vue +2 -2
  67. package/app/pages/ui/display-banner.vue +2 -2
  68. package/app/pages/ui/display-chip.vue +32 -25
  69. package/app/pages/ui/display-details.vue +6 -10
  70. package/app/pages/ui/display-dialog.vue +96 -131
  71. package/app/pages/ui/display-pill.vue +27 -33
  72. package/app/pages/ui/display-prompt.vue +4 -4
  73. package/app/pages/ui/display-toast.vue +2 -2
  74. package/app/pages/ui/expanding-panel.vue +12 -16
  75. package/app/pages/ui/glowing-border.vue +12 -12
  76. package/app/pages/ui/layout-grid-a.vue +6 -6
  77. package/app/pages/ui/layout-grid-b.vue +4 -4
  78. package/app/pages/ui/magnetic-navigation.vue +6 -6
  79. package/app/pages/ui/marquee-scroller.vue +2 -2
  80. package/app/pages/ui/mask-element.vue +2 -2
  81. package/app/pages/ui/masonry-columns.vue +4 -4
  82. package/app/pages/ui/masonry-grid-ordered-experiment.vue +4 -4
  83. package/app/pages/ui/masonry-grid-ordered.vue +4 -4
  84. package/app/pages/ui/masonry-grid-sorted.vue +4 -4
  85. package/app/pages/ui/masonry-grid.vue +4 -4
  86. package/app/pages/ui/navigation/navigation-horizontal.vue +12 -12
  87. package/app/pages/ui/navigation/tab-navigation.vue +493 -0
  88. package/app/pages/ui/page-row.vue +127 -0
  89. package/app/pages/ui/price-list.vue +4 -4
  90. package/app/pages/ui/profile-section.vue +4 -4
  91. package/app/pages/ui/qr-code/[componentName].vue +6 -6
  92. package/app/pages/ui/rotating-carousel.vue +16 -16
  93. package/app/pages/ui/scroll-reveal-image.vue +20 -20
  94. package/app/pages/ui/section-parallax.vue +7 -7
  95. package/app/pages/ui/services/colour-finder.vue +4 -4
  96. package/app/pages/ui/services/services-cards.vue +4 -4
  97. package/app/pages/ui/services/services-section/[slug].vue +4 -4
  98. package/app/pages/ui/services/services-sections-compact.vue +6 -6
  99. package/app/pages/ui/settings.vue +10 -10
  100. package/app/pages/ui/simple-grid.vue +2 -2
  101. package/app/pages/ui/slider-gallery.vue +5 -5
  102. package/app/pages/ui/tabs-y.vue +2 -3
  103. package/app/pages/ui/tabs.vue +6 -9
  104. package/app/pages/ui/tooltips.vue +9 -9
  105. package/app/pages/ui/wipe-away-vertical.vue +14 -14
  106. package/package.json +1 -1
  107. package/.claude/skills/components/layout-row.md +0 -239
  108. package/app/components/display-dialog/DisplayDialogCore.vue +0 -276
  109. package/app/components/display-dialog/variants/DisplayDialogConfirm.vue +0 -47
  110. package/app/components/display-dialog/variants/DisplayDialogScrollableContent.vue +0 -51
  111. package/app/components/forms/c12/utils.ts +0 -14
  112. package/app/components/layout-row/LayoutRow.vue +0 -171
  113. package/app/components/layout-row/stories/LayoutRow.stories.ts +0 -528
  114. package/app/pages/ui/layout-row.vue +0 -139
  115. /package/app/components/{forms → 05.forms}/form-errors/InputError.vue +0 -0
  116. /package/app/components/{forms → 05.forms}/form-errors/tests/InputError.spec.ts +0 -0
  117. /package/app/components/{forms → 05.forms}/form-fieldset/FormFieldset.vue +0 -0
  118. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts +0 -0
  119. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-chromium-darwin.png +0 -0
  120. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-firefox-darwin.png +0 -0
  121. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-secondary-webkit-darwin.png +0 -0
  122. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-chromium-darwin.png +0 -0
  123. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-firefox-darwin.png +0 -0
  124. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-icons-tertiary-webkit-darwin.png +0 -0
  125. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-chromium-darwin.png +0 -0
  126. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-firefox-darwin.png +0 -0
  127. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-primary-pending-webkit-darwin.png +0 -0
  128. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-chromium-darwin.png +0 -0
  129. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-firefox-darwin.png +0 -0
  130. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-secondary-pill-webkit-darwin.png +0 -0
  131. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-chromium-darwin.png +0 -0
  132. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-firefox-darwin.png +0 -0
  133. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/combo-tertiary-readonly-webkit-darwin.png +0 -0
  134. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
  135. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-firefox-darwin.png +0 -0
  136. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-chromium-darwin.png +0 -0
  137. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-firefox-darwin.png +0 -0
  138. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-default-webkit-darwin.png +0 -0
  139. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-chromium-darwin.png +0 -0
  140. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-firefox-darwin.png +0 -0
  141. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-error-webkit-darwin.png +0 -0
  142. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-chromium-darwin.png +0 -0
  143. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-firefox-darwin.png +0 -0
  144. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-success-webkit-darwin.png +0 -0
  145. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-chromium-darwin.png +0 -0
  146. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-firefox-darwin.png +0 -0
  147. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-theme-warning-webkit-darwin.png +0 -0
  148. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-chromium-darwin.png +0 -0
  149. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-firefox-darwin.png +0 -0
  150. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-primary-webkit-darwin.png +0 -0
  151. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-chromium-darwin.png +0 -0
  152. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-firefox-darwin.png +0 -0
  153. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-secondary-webkit-darwin.png +0 -0
  154. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-chromium-darwin.png +0 -0
  155. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-firefox-darwin.png +0 -0
  156. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-variant-tertiary-webkit-darwin.png +0 -0
  157. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/default-webkit-darwin.png +0 -0
  158. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-chromium-darwin.png +0 -0
  159. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-firefox-darwin.png +0 -0
  160. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/nuxt-icon-only-component-webkit-darwin.png +0 -0
  161. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-chromium-darwin.png +0 -0
  162. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-firefox-darwin.png +0 -0
  163. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pending-webkit-darwin.png +0 -0
  164. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-chromium-darwin.png +0 -0
  165. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-firefox-darwin.png +0 -0
  166. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-pill-webkit-darwin.png +0 -0
  167. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-chromium-darwin.png +0 -0
  168. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-firefox-darwin.png +0 -0
  169. /package/app/components/{forms → 05.forms}/input-button/playwright/input-button-core.playwright.ts-snapshots/state-readonly-webkit-darwin.png +0 -0
  170. /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
  171. /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
  172. /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
  173. /package/app/components/{forms → 05.forms}/input-button/tests/InputButtonCore.spec.ts +0 -0
  174. /package/app/components/{forms → 05.forms}/input-button/tests/__snapshots__/InputButtonCore.spec.ts.snap +0 -0
  175. /package/app/components/{forms → 05.forms}/input-checkbox/MultipleCheckboxes.vue +0 -0
  176. /package/app/components/{forms → 05.forms}/input-checkbox/SingleCheckbox.vue +0 -0
  177. /package/app/components/{forms → 05.forms}/input-checkbox/tests/MultipleCheckboxes.spec.ts +0 -0
  178. /package/app/components/{forms → 05.forms}/input-checkbox/tests/data/tags.json +0 -0
  179. /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioButton.vue +0 -0
  180. /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioCore.vue +0 -0
  181. /package/app/components/{forms → 05.forms}/input-checkbox-radio/InputCheckboxRadioWithLabel.vue +0 -0
  182. /package/app/components/{forms → 05.forms}/input-checkbox-radio/tests/InputCheckboxRadioCore.spec.ts +0 -0
  183. /package/app/components/{forms → 05.forms}/input-copy/InputCopyCore.vue +0 -0
  184. /package/app/components/{forms → 05.forms}/input-copy/stories/InputCopyCore.stories.ts +0 -0
  185. /package/app/components/{forms → 05.forms}/input-copy/tests/InputCopyCore.spec.ts +0 -0
  186. /package/app/components/{forms → 05.forms}/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +0 -0
  187. /package/app/components/{forms → 05.forms}/input-description/InputDescription.vue +0 -0
  188. /package/app/components/{forms → 05.forms}/input-description/tests/InputDescription.spec.ts +0 -0
  189. /package/app/components/{forms → 05.forms}/input-label/InputLabel.vue +0 -0
  190. /package/app/components/{forms → 05.forms}/input-label/tests/InputLabel.spec.ts +0 -0
  191. /package/app/components/{forms → 05.forms}/input-number/InputNumberCore.vue +0 -0
  192. /package/app/components/{forms → 05.forms}/input-number/variants/InputNumberDefault.vue +0 -0
  193. /package/app/components/{forms → 05.forms}/input-radio/MultipleRadiobuttons.vue +0 -0
  194. /package/app/components/{forms → 05.forms}/input-radio/tests/MultipleRadioButtons.spec.ts +0 -0
  195. /package/app/components/{forms → 05.forms}/input-radio/tests/data/tags.json +0 -0
  196. /package/app/components/{forms → 05.forms}/input-range/InputRangeCore.vue +0 -0
  197. /package/app/components/{forms → 05.forms}/input-range/variants/InputRangeDefault.vue +0 -0
  198. /package/app/components/{forms → 05.forms}/input-range-fancy/InputRangeFancyCore.vue +0 -0
  199. /package/app/components/{forms → 05.forms}/input-range-fancy/InputRangeFancyWithLabel.vue +0 -0
  200. /package/app/components/{forms → 05.forms}/input-select/InputSelectCore.vue +0 -0
  201. /package/app/components/{forms → 05.forms}/input-select/variants/InputSelectWithLabel.vue +0 -0
  202. /package/app/components/{forms → 05.forms}/input-text/stories/InputPasswordWithLabel.stories.ts +0 -0
  203. /package/app/components/{forms → 05.forms}/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -0
  204. /package/app/components/{forms → 05.forms}/input-text/stories/InputTextCore.stories.ts +0 -0
  205. /package/app/components/{forms → 05.forms}/input-text/stories/InputTextWithLabel.stories.ts +0 -0
  206. /package/app/components/{forms → 05.forms}/input-text/variants/InputPasswordWithLabel.vue +0 -0
  207. /package/app/components/{forms → 05.forms}/input-text/variants/InputTextWithLabel.vue +0 -0
  208. /package/app/components/{forms → 05.forms}/input-textarea/InputTextareaCore.vue +0 -0
  209. /package/app/components/{forms → 05.forms}/input-textarea/stories/InputTextareaCore.stories.ts +0 -0
  210. /package/app/components/{forms → 05.forms}/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -0
  211. /package/app/components/{forms → 05.forms}/input-textarea/variants/InputTextareaWithLabel.vue +0 -0
  212. /package/app/components/{forms → 05.forms}/pending-effect/PendingEffect.vue +0 -0
  213. /package/app/components/{forms → 05.forms}/toggle-switch/ToggleSwitchCore.vue +0 -0
  214. /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -0
  215. /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -0
  216. /package/app/components/{forms → 05.forms}/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -0
  217. /package/app/components/{forms → 05.forms}/toggle-switch/variants/ToggleSwitchWithLabel.vue +0 -0
  218. /package/app/components/{forms → 05.forms}/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +0 -0
  219. /package/app/components/{forms → 05.forms}/triple-toggle-switch/TripleToggleSwitchCore.vue +0 -0
  220. /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.
@@ -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
@@ -0,0 +1,175 @@
1
+ # CardCore — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ All `--card-core-*` tokens are the stable override surface. Set them at any scope (global, page,
6
+ or instance) without touching the component itself.
7
+
8
+ | Token | Default | Controls |
9
+ |---|---|---|
10
+ | `--card-core-row-gap` | `1rem` | Gap between card rows |
11
+ | `--card-core-inner-padding` | `1rem` | Internal padding (applied per row by the consumer) |
12
+ | `--card-core-border-radius` | `0.5rem` | Corner rounding |
13
+ | `--card-core-border` | `0.2rem solid var(--slate-08)` | Full border shorthand |
14
+ | `--card-core-box-shadow` | `0.1rem 0.1rem 0.4rem oklch(from var(--slate-08) l c h / 0.45)` | Box shadow shorthand |
15
+ | `--card-core-background-color` | `transparent` | Main background colour |
16
+ | `--card-core-background-image` | `none` | Main background image |
17
+ | `--card-core-lower-background-color` | `transparent` | Blurred backdrop layer colour |
18
+ | `--card-core-lower-background-image` | `none` | Blurred backdrop layer image |
19
+ | `--card-core-lower-background-position` | `center` | Backdrop image position |
20
+ | `--card-core-lower-background-blur` | `16px` | Backdrop blur amount |
21
+ | `--card-core-lower-scale` | `1.1` | Backdrop scale (zoom to hide blur edges) |
22
+
23
+ > **Variant note**: The `solid`, `subtle`, `soft`, and `outline` variants directly override the
24
+ > internal `--_card-background-color` and `--_card-border` tokens, bypassing the public tokens
25
+ > for those properties. To override a variant's colours, target the variant class directly (see
26
+ > [Per-variant overrides](#per-variant-overrides) below).
27
+
28
+ ---
29
+
30
+ ## Global theming — app-level CSS file
31
+
32
+ Create `assets/styles/setup/07.components/card-core.css` in the consuming app and set tokens on
33
+ `:root`. These values apply to every `CardCore` instance across the site.
34
+
35
+ ```css
36
+ /* assets/styles/setup/07.components/card-core.css */
37
+ :root {
38
+ --card-core-border-radius: 1rem;
39
+ --card-core-border: 0.1rem solid var(--brand-border);
40
+ --card-core-box-shadow: 0 0.2rem 1.2rem rgb(0 0 0 / 8%);
41
+ --card-core-row-gap: 0;
42
+ }
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Per-variant overrides
48
+
49
+ Variants override internal tokens directly, so target the variant class to change their colours:
50
+
51
+ ```css
52
+ /* assets/styles/setup/07.components/card-core.css */
53
+ :root {
54
+ /* shared geometry */
55
+ --card-core-border-radius: 0.8rem;
56
+ }
57
+
58
+ /* variant colour overrides — set on the variant class, not :root */
59
+ .card-core {
60
+ &.solid {
61
+ --_card-background-color: var(--brand-surface);
62
+ --_card-border: 0.1rem solid var(--brand-border);
63
+ }
64
+
65
+ &.subtle {
66
+ --_card-background-color: color-mix(in oklab, var(--brand-surface) 50%, transparent);
67
+ --_card-border: 0.1rem solid var(--brand-border);
68
+ }
69
+
70
+ &.outline {
71
+ --_card-border: 0.15rem solid var(--brand-primary);
72
+ }
73
+ }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Blurred backdrop pattern
79
+
80
+ Set `--card-core-lower-background-image` and `--card-core-lower-background-color` to create a
81
+ blurred translucent layer behind the card content. The `::before` pseudo-element handles the blur
82
+ and the scale hides the blur-edge artefact.
83
+
84
+ ```css
85
+ .card-core {
86
+ &.hero-card {
87
+ --card-core-lower-background-image: url("/images/hero.jpg");
88
+ --card-core-lower-background-blur: 24px;
89
+ --card-core-lower-scale: 1.15;
90
+ --card-core-lower-background-color: rgb(0 0 0 / 30%); /* tint over image */
91
+ --card-core-background-color: transparent;
92
+ }
93
+ }
94
+ ```
95
+
96
+ Or inline for a single instance:
97
+
98
+ ```vue
99
+ <CardCore
100
+ variant="solid"
101
+ style="
102
+ --card-core-lower-background-image: url('/images/hero.jpg');
103
+ --card-core-lower-background-blur: 20px;
104
+ "
105
+ >
106
+ ...
107
+ </CardCore>
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Page-scoped overrides
113
+
114
+ When a card appears in a specific page context, scope overrides under the page wrapper. No
115
+ `:deep()` is required (component styles are unscoped).
116
+
117
+ ```css
118
+ /* In the consuming page's unscoped <style> block */
119
+ .services-page {
120
+ .card-core {
121
+ --card-core-border-radius: 1.2rem;
122
+ --card-core-row-gap: 0;
123
+
124
+ &.solid {
125
+ --_card-background-color: var(--brand-surface-alt);
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Per-instance overrides via styleClassPassthrough
134
+
135
+ ```vue
136
+ <CardCore :style-class-passthrough="['featured']">
137
+ ...
138
+ </CardCore>
139
+ ```
140
+
141
+ ```css
142
+ .card-core {
143
+ &.featured {
144
+ --card-core-border: 0.2rem solid var(--brand-primary);
145
+ --card-core-box-shadow: 0 0.4rem 2rem rgb(0 0 0 / 15%);
146
+ --card-core-border-radius: 1.6rem;
147
+ }
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Card row targeting
154
+
155
+ Slot content is wrapped in `.card-row.card-row-{name}`. Target rows for padding, borders, or
156
+ backgrounds without modifying the component:
157
+
158
+ ```css
159
+ .card-core {
160
+ .card-row-header {
161
+ padding: 1.6rem;
162
+ background-color: var(--brand-primary);
163
+ color: white;
164
+ }
165
+
166
+ .card-row-body {
167
+ padding: 1.6rem;
168
+ }
169
+
170
+ .card-row-footer {
171
+ padding: 1.2rem 1.6rem;
172
+ border-top: 0.1rem solid var(--brand-border);
173
+ }
174
+ }
175
+ ```
@@ -41,50 +41,77 @@ watch(
41
41
  <style lang="css">
42
42
  @layer components {
43
43
  .card-core {
44
- --_inner-padding: 1rem;
45
- --_background-color: white;
46
- --_border-color: green;
47
- --_border-width: 0.2rem;
48
- --_box-shadow-color: transparent;
44
+ --_card-row-gap: var(--card-core-row-gap, 1rem);
45
+ --_card-padding: var(--card-core-inner-padding, 1rem);
46
+ --_card-lower-background-image: var(--card-core-lower-background-image, none);
47
+ --_card-lower-background-position: var(--card-core-lower-background-position, center);
48
+ --_card-lower-background-color: var(--card-core-lower-background-color, transparent);
49
+ --_card-lower-background-blur: var(--card-core-lower-background-blur, 16px);
50
+ --_card-lower-scale: var(--card-core-lower-scale, 1.1);
51
+ --_card-background-image: var(--card-core-background-image, none);
52
+ --_card-background-color: var(--card-core-background-color, transparent);
53
+ --_card-border: var(--card-core-border, 0.2rem solid var(--slate-08));
54
+ --_card-border-radius: var(--card-core-border-radius, 0.5rem);
55
+ --_card-box-shadow: var(--card-core-box-shadow, 0.1rem 0.1rem 0.4rem oklch(from var(--slate-08) l c h / 0.45));
49
56
 
50
57
  display: grid;
51
58
  grid-auto-flow: row;
52
- /* gap: 1rem; */
53
- border-radius: 0.5rem;
59
+ gap: var(--_card-row-gap);
60
+ border-radius: var(--_card-border-radius);
54
61
  overflow: hidden;
62
+ position: relative;
55
63
 
56
- background-color: var(--_background-color, transparent);
57
- border: var(--_border-width) solid var(--_border-color, transparent);
58
- box-shadow: 0 0 0.4rem var(--_border-width) var(--_box-shadow-color, transparent);
64
+ background-color: var(--_card-background-color, transparent);
65
+ border: var(--_card-border);
66
+ box-shadow: var(--_card-box-shadow);
67
+
68
+ &::before {
69
+ content: "";
70
+ position: absolute;
71
+ inset: 0;
72
+ background-color: var(--_card-lower-background-color);
73
+ background-image: var(--_card-lower-background-image);
74
+ background-position: var(--_card-lower-background-position, center);
75
+ background-size: cover;
76
+ background-repeat: no-repeat;
77
+ filter: blur(var(--_card-lower-background-blur));
78
+ scale: var(--_card-lower-scale);
79
+ z-index: 0;
80
+ }
81
+
82
+ > * {
83
+ position: relative;
84
+ z-index: 1;
85
+ }
59
86
 
60
87
  &.no-outline {
61
- --_border-width: 0;
88
+ --_card-border: 0 solid transparent;
89
+ --_card-box-shadow: none;
62
90
  }
63
91
 
64
92
  &.solid {
65
- --_background-color: light-dark(var(--slate-00), var(--slate-10));
66
- --_border-color: red;
93
+ --_card-background-color: light-dark(var(--slate-00), var(--slate-10));
94
+ --_card-border: 0.2rem solid var(--slate-08);
67
95
  }
68
96
 
69
97
  &.subtle {
70
- --_background-color: color-mix(in oklab, light-dark(var(--slate-01), var(--slate-08)) 50%, transparent);
71
- --_border-color: red;
98
+ --_card-background-color: color-mix(in oklab, light-dark(var(--slate-01), var(--slate-08)) 50%, transparent);
99
+ --_card-border: 0.2rem solid var(--slate-08);
72
100
  }
73
101
 
74
102
  &.soft {
75
- --_background-color: color-mix(in oklab, light-dark(var(--slate-01), var(--slate-08)) 20%, transparent);
76
- --_box-shadow-color: color-mix(in oklab, light-dark(var(--slate-02), var(--slate-08)) 80%, transparent);
103
+ --_card-background-color: color-mix(in oklab, light-dark(var(--slate-01), var(--slate-08)) 20%, transparent);
104
+ --_card-box-shadow-color: color-mix(in oklab, light-dark(var(--slate-02), var(--slate-08)) 80%, transparent);
77
105
  }
78
106
 
79
107
  &.outline {
80
- --_background-color: transparent;
81
- --_border-color: green;
108
+ --_card-background-color: transparent;
109
+ --_card-border: 0.2rem solid var(--slate-08);
82
110
  }
83
111
 
84
112
  &.has-dividers {
85
113
  .card-row + .card-row {
86
- /* border-top: 0.2rem solid var(--_border-color); */
87
- border-top: 0.2rem solid green;
114
+ border-top: 0.2rem solid var(--slate-08);
88
115
  }
89
116
  }
90
117
  }