srcdev-nuxt-components 9.4.6 → 9.4.8

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 (109) hide show
  1. package/.claude/commands/migrate-component.md +34 -3
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/column-flow-grid.md +93 -0
  8. package/.claude/skills/components/entry-animation.md +88 -0
  9. package/.claude/skills/components/header-block.md +92 -0
  10. package/.claude/skills/components/input-copy.md +2 -0
  11. package/.claude/skills/components/input-text-core.md +186 -0
  12. package/.claude/skills/components/masonry-grid.md +153 -0
  13. package/.claude/skills/components/pop-over.md +100 -0
  14. package/.claude/skills/components/rotating-carousel-image.md +101 -0
  15. package/.claude/skills/components/skip-links.md +82 -0
  16. package/.claude/skills/components/tabs-core.md +187 -0
  17. package/.claude/skills/index.md +10 -1
  18. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  19. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  20. package/.vscode/srcdev-component-header-block.code-snippets +41 -0
  21. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  22. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  23. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  24. package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
  25. package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
  26. package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
  27. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
  28. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  29. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  30. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  31. package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
  32. package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
  33. package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
  34. package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
  35. package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
  36. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  37. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  38. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  39. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  40. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  41. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  42. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  43. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  44. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  45. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  46. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
  47. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
  48. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
  49. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
  50. package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
  51. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
  52. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
  53. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
  54. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
  55. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  56. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  57. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  58. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  59. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  60. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
  61. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
  62. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
  63. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
  64. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
  65. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  66. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  67. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  68. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  69. package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
  70. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
  71. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
  72. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  73. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  74. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  75. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  76. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  77. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  78. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  79. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  80. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  81. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  82. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  83. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  84. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  85. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  86. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  87. package/app/composables/useTabs.ts +225 -207
  88. package/app/types/components/index.ts +2 -0
  89. package/app/types/components/rotating-carousel-image.d.ts +4 -0
  90. package/app/types/components/skip-links.d.ts +4 -0
  91. package/app/types/forms/types.forms.d.ts +1 -1
  92. package/package.json +1 -1
  93. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
  94. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  95. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  96. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  97. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  98. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  99. package/app/components/pop-over/PopOver.vue +0 -90
  100. package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
  101. package/app/components/skip-links/SkipLinks.vue +0 -60
  102. package/app/components/tabs/TabsCore.vue +0 -306
  103. package/app/components/typography/HeaderBlock.vue +0 -35
  104. package/app/layouts/default.vue +0 -308
  105. package/app/layouts/site-navigation-demo.vue +0 -188
  106. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
  107. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
  108. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
  109. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: PopOver
3
+ description: PopOver generic anchor-positioned disclosure panel with consumer-supplied trigger/content slots, placement prop, CSS token API
4
+ type: reference
5
+ ---
6
+
7
+ # PopOver
8
+
9
+ ## Overview
10
+
11
+ `PopOver` renders a fully consumer-supplied trigger that opens an anchor-positioned popover panel
12
+ with fully consumer-supplied content, built on the native Popover API
13
+ (`popover`/`popovertarget`) and CSS anchor-positioning (`anchor-name`/`position-anchor`/
14
+ `anchor()`). Unlike [`DisplayTooltip`](display-tooltip.md) (a fixed icon trigger for inline help
15
+ text) or `ActionMenu` (menu semantics with arrow-key navigation), `PopOver` makes no assumption
16
+ about what the trigger or content look like, and always renders its own visible close button.
17
+
18
+ **Browser support caveat:** CSS anchor-positioning and the Popover API are both comparatively
19
+ recent — check current browser support before relying on this where broad support is a hard
20
+ requirement, same caution as `DisplayTooltip`/`DeepExpandingMenu`'s equivalent gap.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |------|------|---------|-------------|
26
+ | `placement` | `"top" \| "right" \| "bottom" \| "left"` | `"right"` | Which side of the trigger the popover panel opens on. Each side has a `position-try-fallbacks` so the browser flips it automatically if it would overflow the viewport. |
27
+ | `triggerAriaLabel` | `string` | `""` | aria-label on the trigger button — set this when the `trigger` slot is icon-only. Unset (no attribute) by default, since the trigger slot usually carries its own visible/accessible content. |
28
+ | `popoverAriaLabel` | `string` | `""` | aria-label on the popover content region — set this when the `content` slot has no visible heading. |
29
+ | `closeButtonAriaLabel` | `string` | `"Close"` | aria-label on the close button — override for localisation. |
30
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
31
+
32
+ ## Slots
33
+
34
+ | Slot | Description |
35
+ |------|-------------|
36
+ | `trigger` | Content rendered inside the trigger button. |
37
+ | `content` | Content rendered inside the popover panel, below the close button. |
38
+
39
+ ## Basic usage
40
+
41
+ ```vue
42
+ <PopOver placement="bottom">
43
+ <template #trigger>
44
+ <span>Open popover</span>
45
+ </template>
46
+ <template #content>
47
+ <p>This is some popover content.</p>
48
+ </template>
49
+ </PopOver>
50
+ ```
51
+
52
+ Icon-only trigger:
53
+
54
+ ```vue
55
+ <PopOver trigger-aria-label="Show filters" popover-aria-label="Filter options">
56
+ <template #trigger>
57
+ <Icon name="lucide:filter" aria-hidden="true" />
58
+ </template>
59
+ <template #content>
60
+ <p>Filter controls would go here.</p>
61
+ </template>
62
+ </PopOver>
63
+ ```
64
+
65
+ ## Notes on content
66
+
67
+ The `content` slot's immediate children have their default UA top/bottom margins trimmed (via
68
+ `:first-child`/`:last-child`), so `--pop-over-content-padding`/`-content-padding-block-start` are
69
+ the only source of space above/below the slotted content — a plain `<p>` doesn't stack its own
70
+ `margin-block` on top of the panel's padding. Interior spacing between multiple slotted children
71
+ (e.g. two paragraphs) is untouched.
72
+
73
+ ## Behaviour
74
+
75
+ - Opening/closing is handled natively by the Popover API: clicking the trigger toggles the panel,
76
+ clicking the close button or pressing Escape hides it, and focus returns to the trigger
77
+ automatically on close.
78
+ - On open, focus moves to the close button (`handleToggle`, driven by the popover's native
79
+ `toggle` event) so keyboard users land inside the panel immediately.
80
+
81
+ ## CSS custom properties
82
+
83
+ See `CONSUMER-STYLING.md` for the full `--pop-over-*` token API (gap, trigger outline, panel
84
+ border/background/text/shadow/width, content padding, and close-button tokens).
85
+
86
+ ## Notes
87
+
88
+ - Auto-imported in Nuxt — no manual import needed.
89
+ - 2026-09-13 migration: moved from an unplaced top-level folder (`app/components/pop-over/`) into
90
+ `01.atoms/pop-over/`; converted options-style `defineProps` to `interface Props` +
91
+ `withDefaults`; the slot named `popoverCotent` (typo) was renamed to `content`, and the required
92
+ `popovertarget` prop was replaced with an auto-generated id (`useId()`) so consumers no longer
93
+ need to wire up matching ids themselves; the `role="tooltip"` on the popover panel was removed
94
+ (it described a tooltip, not this component's actual arbitrary-content disclosure semantics);
95
+ added a real, visible close button (the original template referenced one with no styling or
96
+ accessible label — literal `x` text); added `placement` (`top`/`right`/`bottom`/`left`, all with
97
+ `position-try-fallbacks`) — the original only supported opening to the right, with several
98
+ `@position-try` blocks commented out and unused; promoted every hardcoded value to a public CSS
99
+ token; added tests, a Storybook story, this skill doc, and a VS Code snippet (all previously
100
+ missing).
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: RotatingCarouselImage
3
+ description: RotatingCarouselImage 3D rotating image carousel — scroll-parallax tilt, props, accessibility controls, CSS tokens, consumer styling
4
+ type: reference
5
+ ---
6
+
7
+ # RotatingCarouselImage
8
+
9
+ ## Overview
10
+
11
+ `RotatingCarouselImage` renders a set of images arranged in a circle and continuously rotated with
12
+ a CSS animation, tilted in 3D via `perspective`/`rotateX`. By default (`useParallaxEffect: true`)
13
+ the tilt follows scroll position instead of a fixed value — the carousel leans more as it enters
14
+ and leaves the viewport centre. The rotation pauses automatically on keyboard focus and when the
15
+ user has `prefers-reduced-motion` set, and can optionally expose a visible pause/play button.
16
+
17
+ ## Props
18
+
19
+ | Prop | Type | Default | Description |
20
+ |------|------|---------|-------------|
21
+ | `data` | `CarouselImageData[]` (`{ src: string; alt: string }[]`) | `[]` | Images to render around the rotation. |
22
+ | `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
23
+ | `rotateX` | `number` | `0` | Static X-axis tilt in degrees. Only applied while `useParallaxEffect` is `false`. |
24
+ | `perspective` | `number` | `1000` | CSS perspective distance in pixels. |
25
+ | `translateZ` | `number` | `1000` | Radius of the rotation — distance each item sits from the centre, in pixels. |
26
+ | `pauseOnHover` | `boolean` | `false` | Pauses the rotation while the pointer hovers over the carousel. |
27
+ | `useParallaxEffect` | `boolean` | `true` | Drives the tilt from scroll position instead of the static `rotateX` prop. |
28
+ | `showControls` | `boolean` | `false` | Shows a visible pause/play button. Recommended (WCAG 2.2.2) for any auto-rotating content. |
29
+ | `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for `prefers-reduced-motion: reduce`. |
30
+ | `ariaLabel` | `string` | `"Rotating image carousel"` | `aria-label` on the root region — override for localisation. |
31
+ | `ariaDescription` | `string` | `"Use spacebar to pause or play the rotation."` | Screen-reader-only instructions rendered inside the region — override for localisation. |
32
+ | `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the control button while paused. Ignored if the `toggle-icon` slot is used. |
33
+ | `pauseIcon` | `string` | `"mdi:pause"` | Iconify icon name shown on the control button while playing. Ignored if the `toggle-icon` slot is used. |
34
+ | `playLabel` | `string` | `"Play rotation"` | Control button `aria-label` while paused — override for localisation. |
35
+ | `pauseLabel` | `string` | `"Pause rotation"` | Control button `aria-label` while playing — override for localisation. |
36
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
37
+
38
+ Import the data type from the library's type exports when building `data` outside the component:
39
+
40
+ ```ts
41
+ import type { CarouselImageData } from "srcdev-nuxt-components";
42
+ ```
43
+
44
+ ## Slots
45
+
46
+ `toggle-icon` (scoped with `{ isPaused: boolean }`) replaces the control button's icon entirely —
47
+ same pattern as `MarqueeScroller`:
48
+
49
+ ```vue
50
+ <RotatingCarouselImage :data="images" show-controls>
51
+ <template #toggle-icon="{ isPaused }">
52
+ <Icon :name="isPaused ? 'lucide:play' : 'lucide:pause'" />
53
+ </template>
54
+ </RotatingCarouselImage>
55
+ ```
56
+
57
+ ## Accessibility behaviour
58
+
59
+ - The root is `role="region"` with `tabindex="0"` so it's independently focusable.
60
+ - Spacebar toggles pause/play while the region is focused.
61
+ - Focusing the region always pauses the rotation so keyboard users can inspect it; blurring
62
+ resumes it unless the user prefers reduced motion.
63
+ - Hovering the carousel pauses it only when `pauseOnHover` is `true` (default `false`, preserving
64
+ the component's original opt-in behaviour).
65
+ - All user-visible copy (`ariaLabel`, `ariaDescription`, `playLabel`, `pauseLabel`) is a plain
66
+ string prop with an English default — pass translated strings from the consumer's own i18n
67
+ solution.
68
+
69
+ ## Basic usage
70
+
71
+ ```vue
72
+ <RotatingCarouselImage
73
+ :data="[
74
+ { src: '/carousel/1.jpg', alt: 'Product shot 1' },
75
+ { src: '/carousel/2.jpg', alt: 'Product shot 2' },
76
+ ]"
77
+ show-controls
78
+ />
79
+ ```
80
+
81
+ Disable scroll-driven tilt for a static, self-contained rotation:
82
+
83
+ ```vue
84
+ <RotatingCarouselImage :data="images" :use-parallax-effect="false" :rotate-x="10" show-controls />
85
+ ```
86
+
87
+ ## CSS custom properties
88
+
89
+ See `CONSUMER-STYLING.md` in this component's folder for the full public token table (carousel
90
+ height, item size, rotation duration, control button styling). `rotateX`/`perspective`/
91
+ `translateZ` are prop-driven, not tokens, since they also drive the component's own scroll-parallax
92
+ math.
93
+
94
+ ## Migration note (2026-09-15)
95
+
96
+ Previously lived at `app/components/rotating-carousel/RotatingCarouselImage.vue` with
97
+ options-style `defineProps`, no accessibility controls (no focus/keyboard pause, no visible pause
98
+ button), and several hardcoded CSS values (item size, rotation duration, carousel height) with no
99
+ override hook. Moved to `01.atoms/animations/rotating-carousel-image/`, migrated to
100
+ `interface Props` + `withDefaults`, and given the same pause/focus/reduced-motion/control-button
101
+ pattern as `MarqueeScroller`.
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: SkipLinks
3
+ description: SkipLinks accessibility skip-navigation utility — focus-revealed jump links, props, CSS tokens, consumer styling
4
+ type: reference
5
+ ---
6
+
7
+ # SkipLinks
8
+
9
+ ## Overview
10
+
11
+ `SkipLinks` renders a set of visually-hidden links that become visible on keyboard focus, letting
12
+ keyboard and screen-reader users jump past repeated navigation straight to main content, the
13
+ footer, or any other in-page target. Place it first in the page layout, before the header/nav, so
14
+ it's the first focusable element on the page.
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Required |
19
+ |------|------|---------|----------|
20
+ | `links` | `SkipLink[]` (`{ href: string; label: string }[]`) | `[{ href: "#main-content", label: "Skip to main content" }, { href: "#footer-content", label: "Skip to footer" }]` | no |
21
+ | `ariaLabel` | `string` | `"Skip navigation"` | no — override for localisation |
22
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
23
+
24
+ `SkipLink` is exported from `~/types/components/skip-links` (and re-exported from
25
+ `~/types/components`):
26
+
27
+ ```typescript
28
+ interface SkipLink {
29
+ href: string;
30
+ label: string;
31
+ }
32
+ ```
33
+
34
+ ## Slots
35
+
36
+ `homeLink` — optional content (typically a logo/home link) rendered before the skip-nav, wrapped
37
+ in `.skip-links__home`. Only rendered when the slot is actually used.
38
+
39
+ ```vue
40
+ <SkipLinks>
41
+ <template #homeLink>
42
+ <NuxtLink to="/" class="logo">Acme</NuxtLink>
43
+ </template>
44
+ </SkipLinks>
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```vue
50
+ <SkipLinks
51
+ :links="[
52
+ { href: '#site-navigation', label: 'Skip to navigation' },
53
+ { href: '#main-content', label: 'Skip to main content' },
54
+ { href: '#footer-content', label: 'Skip to footer' },
55
+ ]"
56
+ />
57
+ ```
58
+
59
+ Each `href` must match the `id` of a real element on the page (e.g. `<main id="main-content">`) —
60
+ the component does not create those anchors itself.
61
+
62
+ ## Accessibility behaviour
63
+
64
+ - Links are hidden via `opacity`/`transform`, not `display`/`visibility` — they stay in the tab
65
+ order and keyboard-focusable even while visually hidden.
66
+ - The panel reveals on `:focus-within`, so tabbing onto any link shows the whole group.
67
+ - Wrapped in a `<nav :aria-label="ariaLabel">` landmark, defaulting to `"Skip navigation"`.
68
+ - The reveal transition is disabled under `prefers-reduced-motion: reduce`.
69
+ - `ariaLabel` and every link's `label` are plain string props/fields — pass translated strings
70
+ from the consumer's own i18n solution.
71
+
72
+ ## CSS custom properties
73
+
74
+ See `CONSUMER-STYLING.md` in this component's folder for the full `--skip-links-*` token API
75
+ (panel background/border, link text colour, spacing, transition duration, focus outline).
76
+
77
+ ## Notes
78
+
79
+ - Component is auto-imported in Nuxt — no import needed.
80
+ - Migrated 2026-09-15 from the unplaced `skip-links/` folder: the link set was previously
81
+ hardcoded (two fixed `<a>` tags with hardcoded English copy) — now data-driven via `links` so
82
+ consumers can add/remove targets and localise the copy without forking the component.
@@ -0,0 +1,187 @@
1
+ ---
2
+ name: TabsCore
3
+ description: TabsCore tab list/panel widget — indexed dynamic slots, arrow-key navigation, moving hover/active/underline indicators, CSS tokens
4
+ type: reference
5
+ ---
6
+
7
+ # TabsCore
8
+
9
+ ## Overview
10
+
11
+ `TabsCore` renders a WAI-ARIA tablist from indexed slots. The number of tabs is set via
12
+ `itemCount`; each tab's trigger and content come from `tab-{n}-trigger`/`tab-{n}-content` slots.
13
+ Indexed slots (rather than named dynamic slots) are used here deliberately — `itemCount` is needed
14
+ for ARIA linking across the two parallel loops (triggers and panels), not just the slot loop
15
+ itself.
16
+
17
+ Three independent moving-highlight decorators track pointer/keyboard state: a hover highlight, an
18
+ active-tab highlight, and an underline/sideline indicator — each can be turned off individually.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Required |
23
+ |------|------|---------|----------|
24
+ | `itemCount` | `number` | — | **yes** — number of tabs; drives both slot loops |
25
+ | `axis` | `"x" \| "y"` | `"x"` | no — horizontal row or vertical column layout |
26
+ | `transitionDuration` | `number` | `200` | no — ms duration of the moving indicators |
27
+ | `trackHover` | `boolean` | `true` | no — shows the moving hover highlight |
28
+ | `trackActive` | `boolean` | `true` | no — shows the moving active-tab highlight |
29
+ | `trackIndicator` | `boolean` | `true` | no — shows the moving underline/sideline indicator |
30
+ | `ariaLabel` | `string` | `"Tabs"` | no — override for localisation |
31
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | no |
32
+
33
+ ## Slots
34
+
35
+ For each index `0..itemCount-1`:
36
+
37
+ - `tab-{n}-trigger` — content of the tab button.
38
+ - `tab-{n}-content` — content of that tab's panel.
39
+
40
+ ```vue
41
+ <TabsCore :item-count="2">
42
+ <template #tab-0-trigger>Overview</template>
43
+ <template #tab-0-content>Overview content…</template>
44
+ <template #tab-1-trigger>Specs</template>
45
+ <template #tab-1-content>Specs content…</template>
46
+ </TabsCore>
47
+ ```
48
+
49
+ ## Accessibility behaviour
50
+
51
+ - Renders `role="tablist"` (with `aria-label`), `role="tab"` triggers (`type="button"`), and
52
+ `role="tabpanel"` panels (`aria-labelledby` pointing at their trigger, `tabindex="0"` so keyboard
53
+ users can scroll into panel content per the WAI-ARIA Tabs Pattern).
54
+ Fixed 2026-09-15 — panels previously used `role="region"` and were hardcoded
55
+ `aria-hidden="true"` permanently, even the visible one, hiding all tab content from assistive
56
+ tech at all times.
57
+ - Roving tabindex: only the active trigger has `tabindex="0"`; the rest are `-1`.
58
+ - Arrow keys move focus and activate the target tab (automatic activation): `ArrowLeft`/`ArrowRight`
59
+ on `axis="x"`, `ArrowUp`/`ArrowDown` on `axis="y"`, wrapping at the ends; `Home`/`End` jump to the
60
+ first/last tab. Fixed 2026-09-15 — previously there was no keyboard navigation at all beyond
61
+ native Tab-key focus and click/Enter/Space activation on individual buttons.
62
+ - Focus is visible via `:focus-visible` on both triggers and panels.
63
+
64
+ ## CSS custom properties
65
+
66
+ See `CONSUMER-STYLING.md` in this component's folder for the full `--tabs-*` token API (indicator
67
+ colours, list-item spacing/typography, content-panel border/background, axis-y gap). Whether each
68
+ indicator renders at all is controlled by `trackHover`/`trackActive`/`trackIndicator`, not tokens.
69
+
70
+ ## Migration note (2026-09-15)
71
+
72
+ Previously lived at `tabs/TabsCore.vue` with options-style `defineProps`, no tier folder, and
73
+ several defects fixed in this pass:
74
+
75
+ - `querySelectorAll("[data-nav-item")` was missing its closing `]` — an invalid selector that
76
+ some browsers tolerate but that throws in strict DOM implementations (including this repo's test
77
+ environment). Fixed to `"[data-nav-item]"`.
78
+ - `aria-labelledby="channel-name"` was hardcoded on the tablist, pointing at an id that doesn't
79
+ exist anywhere in this library. Replaced with the new `ariaLabel` prop.
80
+ - `border-bottom`/`border-left` on the tab list referenced `--_tabs-border-bottom`, a private CSS
81
+ variable that was never set anywhere (dead reference — the border never rendered). Promoted to
82
+ the public `--tabs-nav-border` token with a real default.
83
+ - `tag`, `trackHover`, `trackActive`, and `trackIndicator` were all declared props with no effect
84
+ anywhere in the component or its composable (`useTabs`). `tag` had no coherent purpose (triggers
85
+ were always hardcoded `<button>`s) and was removed; the three `track*` flags were wired up to
86
+ actually gate their respective decorator elements, since removing them would have been a bigger
87
+ behavioural change than making them work as their names already implied.
88
+ - Almost every colour/spacing/typography value in the `<style>` block was hardcoded with no
89
+ override hook at all (not even a private token) — promoted to public `--tabs-*` tokens.
90
+
91
+ ## Performance notes (2026-09-15)
92
+
93
+ `useTabs` had a few real perf issues, fixed alongside the migration:
94
+
95
+ - **Layout thrashing**: `moveActiveIndicator`/`moveHoveredIndicator`/`setFinalActivePositions`/
96
+ `setFinalHoveredPositions` used to interleave `offsetLeft`/`offsetWidth`/etc. reads with
97
+ `style.setProperty` writes, forcing a synchronous reflow between each pair. All layout reads are
98
+ now batched into local variables before any writes happen.
99
+ - **`@mouseover` → `@mouseenter`**: the hover trigger listener used `mouseover`, which bubbles and
100
+ re-fires as the pointer crosses any child element inside the button. `mouseenter` fires exactly
101
+ once per tab entered.
102
+ - **Debounced settle timers**: `moveActiveIndicator`/`moveHoveredIndicator` schedule a `setTimeout`
103
+ to snap the indicator to its exact final position once the CSS transition ends. Previously each
104
+ call scheduled a new timeout without clearing the last one, so moving the pointer quickly across
105
+ several tabs stacked up redundant pending timeouts. Now the previous timeout is cleared before
106
+ scheduling the next, and both are cleared in `onUnmounted`.
107
+ - `handleTransitioningClass` called `.indexOf()` on the same two tabs repeatedly inside its loop
108
+ condition and body — now computed once per call.
109
+ - `previousActiveTab` used `useState("previousActiveTab", ...)` with a fixed, unscoped key — every
110
+ `TabsCore` instance on a page would have shared that one piece of state. Not actually a
111
+ performance issue, but a correctness bug found while auditing this file; fixed by switching to a
112
+ plain per-instance `ref`, since this state doesn't need SSR/hydration sharing.
113
+
114
+ ## Fixed: hover/active label text could become unreadable against the indicator (2026-09-15)
115
+
116
+ `--tabs-hover-indicator-text-colour` and `--tabs-active-indicator-text-colour` were set as `color`
117
+ on `.nav__hovered`/`.nav__active` — decorative, empty, absolutely-positioned divs with no text
118
+ content, so that `color` was dead CSS with no visible effect. The actual tab label
119
+ (`.tabs-list-item`) used one shared `--tabs-list-item-colour-selected` token for hover, active,
120
+ *and* transitioning alike, regardless of which of the two (potentially differently-coloured)
121
+ indicator backgrounds was actually behind it at that moment. A consumer overriding only the hover
122
+ indicator's background (or the active one) had no working lever to keep the label readable against
123
+ it, and could end up with text the same colour as the background sliding behind it — this was the
124
+ original defect noticed during this component's initial authoring, reported as "the button text
125
+ became the same colour as the track item background and became invisible."
126
+
127
+ Fixed by removing the dead `color` declarations from the two decorator divs and splitting
128
+ `.tabs-list-item`'s combined `:hover, [aria-selected="true"], .transitioning` rule so hover uses
129
+ `--tabs-hover-indicator-text-colour` and active uses `--tabs-list-item-colour-selected` falling
130
+ back to `--tabs-active-indicator-text-colour` — each state's text colour now tracks the background
131
+ actually behind it. See `CONSUMER-STYLING.md`'s "Tab-label text colour tracks the indicator behind
132
+ it" section: overriding an indicator background without its matching text-colour token can still
133
+ produce poor contrast (same tradeoff as any two independently-set colour tokens) — the fix makes
134
+ the tokens functional, it doesn't auto-compute contrast.
135
+
136
+ ## Fixed: intermediate tab labels could go invisible during a multi-tab jump (2026-09-15)
137
+
138
+ The first fix above wasn't the whole story. The exact repro that surfaced the rest of it: with tab
139
+ 1 active, hover then click tab 5 — the tabs in between briefly lost all visible text. Going the
140
+ other direction (tab 5 active, hover then click tab 1) looked fine.
141
+
142
+ Cause: `useTabs` had a `handleTransitioningClass` helper that added a `.transitioning` class to
143
+ every tab spanned by a jump (previously shared by the `[aria-selected="true"]` colour rule fixed
144
+ above), on the assumption the sliding indicator visually covers every spanned tab for the whole
145
+ transition — so it force-applied the active text colour to all of them, for the full
146
+ `transitionDuration`. It doesn't cover them the whole time: the indicator's "grow from anchor" CSS
147
+ transition (see `moveActiveIndicator`/`moveHoveredIndicator`) keeps the box's leading edge pinned
148
+ at the *old* tab's position on a forward move and only grows its width, so it only gradually sweeps
149
+ across later tabs — a tab several positions away isn't actually covered until *late* in the
150
+ transition. Recolouring it to the active text colour from the very start meant its label matched
151
+ neither its own background nor the not-yet-arrived indicator, going invisible. A backward move
152
+ animates position and width toward their final values together, so the box's rendered coverage is
153
+ more consistent throughout — which is why that direction looked fine and made the bug easy to miss
154
+ without a specific repro.
155
+
156
+ Fixed by removing `handleTransitioningClass` and the `.transitioning` class entirely — it had no
157
+ other purpose. Tabs spanned by a jump now keep their normal (inactive) styling throughout; only the
158
+ newly-active tab (`[aria-selected="true"]`) and the currently-hovered tab (`:hover`) get their
159
+ special colours. See `CONSUMER-STYLING.md`'s "Tabs spanned by a jump keep their normal styling".
160
+
161
+ ## Fixed: passed-over tab text still invisible under the active indicator specifically (2026-09-15)
162
+
163
+ Removing `.transitioning` (previous fix) uncovered a second, pre-existing defect that
164
+ `.transitioning`'s forced recolour had been accidentally masking the whole time: a non-active tab's
165
+ normal text colour (`--tabs-list-item-colour`) was **exactly the same value** as the active
166
+ indicator's background (`--tabs-active-indicator-colour`, same default) — this collision existed in
167
+ the very first authored version of this component, not something introduced by any migration
168
+ change. So whenever the active pill visually passes under (or briefly sits under) any tab that
169
+ isn't itself marked active — which happens on every transition, single-step or distant, in either
170
+ direction — that tab's text became literally the same colour as the background sliding beneath it.
171
+ `--tabs-hover-indicator-colour`'s default never had this problem since it doesn't coincide with the
172
+ inactive text default.
173
+
174
+ Fixed by changing `--tabs-active-indicator-colour`'s default to one step in from the extreme,
175
+ still clearly the boldest of the three indicator colours, but no longer numerically identical to
176
+ `--tabs-list-item-colour`'s default. `--tabs-active-indicator-text-colour`/
177
+ `--tabs-list-item-colour-selected` (used by the truly active tab) still contrast fine against the
178
+ new value. See `CONSUMER-STYLING.md` for the note on keeping these two tokens from colliding again
179
+ if you override either one.
180
+
181
+ ## Colour token defaults are flat values, not light-dark() (2026-09-15)
182
+
183
+ Every colour token's default was originally a `light-dark(lightValue, darkValue)` pair (as most of
184
+ this library's other components use). Deliberately changed to a single flat `--slate-*` value per
185
+ token for this component — it doesn't bake in automatic light/dark scheme-switching for its own
186
+ defaults; a consumer who wants a token to adapt to `color-scheme` wraps their own override value in
187
+ `light-dark()`. See `CONSUMER-STYLING.md`'s "Colour tokens are flat values, not light-dark() pairs".
@@ -70,6 +70,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
70
70
  ├── layout-grid-by-cols.md — LayoutGridByCols dynamic slots (item-{n}), props, column/gap/breakpoint decisions
71
71
  ├── page-row.md — PageRow layout primitive: CSS grid named lines, nesting pattern, align prop, aria-labelledby, CSS token API
72
72
  ├── link-text.md — LinkText props, slots, usage patterns, styling
73
+ ├── header-block.md — HeaderBlock: tagLevel/classLevel semantic-vs-visual heading decoupling, page-heading-N utility classes
73
74
  ├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
74
75
  ├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
75
76
  ├── services-card-grid.md — ServicesCardGrid props, config pass-through, CSS tokens, full page boilerplate
@@ -77,6 +78,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
77
78
  ├── service-summary.md — ServiceSummary props, summary-link slot, DisplayPill duration/price (renamed/stripped from ServicesSection — full mode is now ServiceDetail)
78
79
  ├── service-detail.md — ServiceDetail: full service detail page (hero banner, sticky sidebar booking card + related services, closing CTA), headerTag vs subheadingTag split, book-cta/sidebar-note/related-service/final-cta slots
79
80
  ├── breadcrumb.md — Breadcrumb: items (BreadcrumbItem[]) trail, link vs current-page text, CSS token API
81
+ ├── skip-links.md — SkipLinks: focus-revealed accessibility skip-nav, links (SkipLink[]) data-driven, homeLink slot, CSS token API
82
+ ├── tabs-core.md — TabsCore: tablist from indexed slots (itemCount), arrow-key nav, moving hover/active/underline indicators, CSS token API
80
83
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
81
84
  ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
82
85
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
@@ -90,6 +93,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
90
93
  ├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
91
94
  ├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
92
95
  ├── marquee-scroller.md — MarqueeScroller: infinite logo/badge scroller, per-item dynamic slots (marqueeData id), hover/focus/keyboard pause, reduced-motion, CSS tokens
96
+ ├── rotating-carousel-image.md — RotatingCarouselImage: 3D rotating image carousel, scroll-parallax tilt, focus/keyboard/hover pause, reduced-motion, CSS tokens
93
97
  ├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
94
98
  ├── tab-navigation.md — TabNavigation: horizontal tab nav with CSS anchor-positioning indicators, anchor scroll, burger collapse, full CSS token API
95
99
  ├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
@@ -116,7 +120,12 @@ Each skill is a single markdown file named `<area>-<task>.md`.
116
120
  ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
117
121
  ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
118
122
  ├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, CSS token API
119
- └── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
123
+ ├── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
124
+ ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
125
+ ├── input-text-core.md — InputTextCore: native text/date/number input primitive, min/max pass-through (date-picker range), CSS token API; Variants section covers InputTextWithLabel/InputPasswordWithLabel/InputTextAsNumberWithLabel
126
+ ├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
127
+ ├── column-flow-grid.md — ColumnFlowGrid (renamed from MasonryGrid): CSS multi-column text-flow layout, named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API; not a true masonry, see "which one do I want?"
128
+ └── masonry-grid.md — MasonryGrid: real measured-height masonry (greedy shortest-column packing, animated resize) absorbed from the now-retired MasonryGridOrdered; named dynamic slots, fixedWidth/justify, CSS token API
120
129
  ```
121
130
 
122
131
  ## Skill file template
@@ -0,0 +1,39 @@
1
+ {
2
+ "SRCDEV ColumnFlowGrid Basic": {
3
+ "description": "Basic ColumnFlowGrid with a few named item slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ColumnFlowGrid :item-min-width=\"$1300\" :gap=\"$21.2\">",
7
+ " <template #item-1>",
8
+ " $3",
9
+ " </template>",
10
+ " <template #item-2>",
11
+ " $4",
12
+ " </template>",
13
+ "</ColumnFlowGrid>"
14
+ ]
15
+ },
16
+ "SRCDEV ColumnFlowGrid Looped": {
17
+ "description": "ColumnFlowGrid rendering a v-for list via dynamic slot names (no count prop needed)",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<ColumnFlowGrid :item-min-width=\"$1280\" :gap=\"$21.6\">",
21
+ " <template v-for=\"($3item, $4index) in $5items\" :key=\"$3item.id\" #[`item-${$4index}`]>",
22
+ " $6",
23
+ " </template>",
24
+ "</ColumnFlowGrid>"
25
+ ]
26
+ },
27
+ "SRCDEV ColumnFlowGrid CSS Override": {
28
+ "description": "CSS override scaffold for ColumnFlowGrid tokens",
29
+ "scope": "css",
30
+ "body": [
31
+ ".$1my-page {",
32
+ " .column-flow-grid-wrapper {",
33
+ " --column-flow-grid-item-border-colour: $2;",
34
+ " --column-flow-grid-item-padding: $3;",
35
+ " }",
36
+ "}"
37
+ ]
38
+ }
39
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "SRCDEV EntryAnimation Basic": {
3
+ "description": "Basic EntryAnimation wrapper with a default slide-in animation",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<EntryAnimation animation-type=\"$1entry-slide-in\">",
7
+ " <template #default>",
8
+ " $2",
9
+ " </template>",
10
+ "</EntryAnimation>"
11
+ ]
12
+ },
13
+ "SRCDEV EntryAnimation Looped, Skip First": {
14
+ "description": "EntryAnimation in a v-for loop, skipping the first (above-the-fold) item",
15
+ "scope": "vue,html",
16
+ "body": [
17
+ "<EntryAnimation",
18
+ " v-for=\"($1item, $2index) in $3items\"",
19
+ " :key=\"$1item.id\"",
20
+ " :skip-animation=\"$2index === 0\"",
21
+ " animation-type=\"$4entry-slide-in\"",
22
+ ">",
23
+ " <template #default>",
24
+ " $5",
25
+ " </template>",
26
+ "</EntryAnimation>"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "SRCDEV HeaderBlock Basic": {
3
+ "description": "HeaderBlock — semantic heading tag decoupled from visual page-heading-N size",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<HeaderBlock :tag-level=\"${1:1}\" :class-level=\"${2:1}\">",
7
+ " $3",
8
+ "</HeaderBlock>"
9
+ ]
10
+ },
11
+ "SRCDEV HeaderBlock Decoupled Levels": {
12
+ "description": "HeaderBlock with a semantic tag lower than its visual size (e.g. hero heading below the page's real h1)",
13
+ "scope": "vue,html",
14
+ "body": [
15
+ "<HeaderBlock :tag-level=\"${1:2}\" :class-level=\"${2:1}\">",
16
+ " $3",
17
+ "</HeaderBlock>"
18
+ ]
19
+ },
20
+ "SRCDEV HeaderBlock With Passthrough Class": {
21
+ "description": "HeaderBlock with a custom class for one-off styling overrides",
22
+ "scope": "vue,html",
23
+ "body": [
24
+ "<HeaderBlock :tag-level=\"${1:1}\" :class-level=\"${2:1}\" style-class-passthrough=\"${3:hero-title}\">",
25
+ " $4",
26
+ "</HeaderBlock>"
27
+ ]
28
+ },
29
+ "SRCDEV HeaderBlock Paired With Section aria-labelledby": {
30
+ "description": "HeaderBlock with id bound to a wrapping section's aria-labelledby target",
31
+ "scope": "vue,html",
32
+ "body": [
33
+ "<section aria-labelledby=\"${1:section-heading}\">",
34
+ " <HeaderBlock id=\"${1:section-heading}\" :tag-level=\"${2:2}\" :class-level=\"${3:1}\">",
35
+ " $4",
36
+ " </HeaderBlock>",
37
+ " $5",
38
+ "</section>"
39
+ ]
40
+ }
41
+ }