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,99 @@
1
+ # TabsCore — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--tabs-nav-border` | `0.1rem solid var(--slate-06)` | Border between the tab list and its content (bottom border on `axis="x"`, left border on `axis="y"`) |
8
+ | `--tabs-hover-indicator-colour` | `var(--slate-07)` | Background of the moving hover highlight (`trackHover`) |
9
+ | `--tabs-hover-indicator-text-colour` | `var(--slate-00)` | Text colour of a hovered tab trigger's label — keep this in contrast with `--tabs-hover-indicator-colour` |
10
+ | `--tabs-active-indicator-colour` | `var(--slate-09)` | Background of the moving active-tab highlight (`trackActive`) |
11
+ | `--tabs-active-indicator-text-colour` | `var(--slate-00)` | Fallback text colour of the active tab trigger's label — keep this in contrast with `--tabs-active-indicator-colour` |
12
+ | `--tabs-underline-indicator-colour` | `var(--slate-10)` | Colour of the underline/sideline indicator (`trackIndicator`) |
13
+ | `--tabs-underline-indicator-height` | `0.4rem` | Thickness of the underline indicator (`axis="x"`) — also used as its width on `axis="y"` |
14
+ | `--tabs-list-item-opacity` | `0.7` | Opacity of an inactive tab trigger |
15
+ | `--tabs-list-item-colour` | `var(--slate-10)` | Text colour of an inactive tab trigger |
16
+ | `--tabs-list-item-colour-selected` | falls back to `--tabs-active-indicator-text-colour` | Text colour of the active tab trigger — set this directly to override without touching `--tabs-active-indicator-text-colour` |
17
+ | `--tabs-list-item-text-transform` | `uppercase` | `text-transform` of tab trigger labels |
18
+ | `--tabs-list-item-font-weight` | `500` | Font weight of tab trigger labels |
19
+ | `--tabs-list-item-padding-block` | `1em` | Vertical padding of a tab trigger |
20
+ | `--tabs-list-item-padding-inline` | `2em` | Horizontal padding of a tab trigger |
21
+ | `--tabs-list-item-colour-transition-duration` | `100ms` | Transition duration of the trigger text-colour change |
22
+ | `--tabs-list-item-focus-outline-width` | `2px` | Focus outline width on a tab trigger/panel (colour is `--theme-ring`) |
23
+ | `--tabs-list-item-focus-outline-offset` | `-2px` | Focus outline offset on a tab trigger |
24
+ | `--tabs-content-background-colour` | `var(--slate-09)` | Background of the content panel wrapper |
25
+ | `--tabs-content-border-width` | `0.1rem` | Border/outline width of the content panel wrapper |
26
+ | `--tabs-content-border-colour` | `var(--slate-06)` | Border/outline colour of the content panel wrapper |
27
+ | `--tabs-content-border-radius` | `0` | Corner radius of the content panel wrapper |
28
+ | `--tabs-axis-y-gap` | `2em` | Gap between the tab list and content when `axis="y"` |
29
+
30
+ ```css
31
+ .my-page {
32
+ --tabs-active-indicator-colour: #1a1a1a;
33
+ --tabs-underline-indicator-colour: #b5651d;
34
+ --tabs-content-border-radius: 0.8rem;
35
+ }
36
+ ```
37
+
38
+ ## Colour tokens are flat values, not light-dark() pairs
39
+
40
+ Every colour token above defaults to a single flat `--slate-*` value rather than a
41
+ `light-dark(lightValue, darkValue)` pair — this component doesn't bake in automatic light/dark
42
+ scheme-switching for its own defaults. If you want a token to adapt to `color-scheme`, wrap your
43
+ own override in `light-dark()`:
44
+
45
+ ```css
46
+ .my-page {
47
+ --tabs-active-indicator-colour: light-dark(#1a1a1a, #f5f5f5);
48
+ }
49
+ ```
50
+
51
+ ## Tab-label text colour tracks the indicator behind it
52
+
53
+ Hover and active can have different background colours (`--tabs-hover-indicator-colour` vs.
54
+ `--tabs-active-indicator-colour`), so the trigger label uses two separate text-colour tokens
55
+ rather than one shared "selected" colour — otherwise overriding just one indicator's background
56
+ risks making the label unreadable against it while leaving the other state fine. If you override
57
+ either indicator background, override its matching text-colour token to keep contrast:
58
+
59
+ ```css
60
+ .my-page {
61
+ --tabs-hover-indicator-colour: gold;
62
+ --tabs-hover-indicator-text-colour: black;
63
+ --tabs-active-indicator-colour: navy;
64
+ --tabs-active-indicator-text-colour: white;
65
+ }
66
+ ```
67
+
68
+ ## Tabs spanned by a jump keep their normal styling
69
+
70
+ When the active tab (or hover) jumps across several tabs at once (e.g. clicking tab 5 while tab 1
71
+ is active), the sliding indicator animates across the tabs in between, but their own labels are
72
+ **not** recoloured to the active/hover text colour during that transition — they keep their normal
73
+ inactive styling throughout. An earlier version force-applied the active text colour to every
74
+ spanned tab for the whole transition, on the assumption the sliding indicator visually covered them
75
+ the entire time — it doesn't (a forward jump grows the indicator gradually from the old tab's
76
+ position, so it only reaches later tabs partway through), so intermediate labels could end up a
77
+ colour that matched neither their own background nor the indicator yet, making them briefly
78
+ invisible. Fixed 2026-09-15.
79
+
80
+ Since a non-active tab keeps its normal inactive text colour throughout, and the active indicator
81
+ still visually passes under it while sliding, `--tabs-active-indicator-colour`'s default
82
+ (`var(--slate-09)`) is deliberately *not* the same value as `--tabs-list-item-colour`'s default
83
+ (`var(--slate-10)`) — they used to be identical, which meant a passed-over tab's text became
84
+ literally the same colour as the indicator sliding beneath it (invisible). If you override
85
+ `--tabs-active-indicator-colour`, check it still differs enough from `--tabs-list-item-colour` (or
86
+ override that too) to avoid the same collision recurring.
87
+
88
+ ## Indicator visibility is prop-driven, not tokens
89
+
90
+ Whether the hover highlight, active highlight, and underline indicator render at all is controlled
91
+ by the `trackHover`/`trackActive`/`trackIndicator` boolean props (all default `true`), since they
92
+ also gate whether the underlying DOM nodes are created — not just their styling.
93
+
94
+ ## Movement/position values are private
95
+
96
+ `--_x-active`, `--_x-hovered`, `--_width-active`, `--_width-hovered`, `--_y-active`, `--_y-hovered`,
97
+ `--_y-height`, `--_y-width`, and `--_transition-duration` are computed at runtime from the tab
98
+ elements' measured layout (`offsetLeft`/`offsetWidth`/etc. in `useTabs`) and aren't meaningful to
99
+ override directly — use `transitionDuration` (a prop, in ms) to control indicator movement speed.
@@ -0,0 +1,272 @@
1
+ <template>
2
+ <div class="tabs-core" :class="[`axis-${axis}`]">
3
+ <ul
4
+ ref="tabsNavRef"
5
+ role="tablist"
6
+ :aria-label="ariaLabel"
7
+ class="tabs-list"
8
+ :class="[elementClasses]"
9
+ @mouseleave="resetHoverToActivePosition()"
10
+ >
11
+ <li v-for="(index, key) in itemCount" :key="key">
12
+ <button
13
+ :id="`tab-${key}-trigger`"
14
+ :data-tab-index="key"
15
+ data-nav-item
16
+ type="button"
17
+ role="tab"
18
+ aria-selected="false"
19
+ class="tabs-list-item"
20
+ @click.prevent="navItemClicked($event)"
21
+ @mouseenter="navItemHovered($event)"
22
+ @keydown="navItemKeydown($event)"
23
+ >
24
+ <slot :name="`tab-${key}-trigger`"></slot>
25
+ </button>
26
+ </li>
27
+ </ul>
28
+ <div class="tab-content-wrapper">
29
+ <div
30
+ v-for="(item, key) in itemCount"
31
+ :id="`tab-${key}-content`"
32
+ :key="key"
33
+ ref="tabsContentRefs"
34
+ class="tab-content"
35
+ :aria-labelledby="`tab-${key}-trigger`"
36
+ role="tabpanel"
37
+ tabindex="0"
38
+ aria-hidden="true"
39
+ >
40
+ <slot :name="`tab-${key}-content`"></slot>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </template>
45
+
46
+ <script setup lang="ts">
47
+ interface Props {
48
+ axis?: "x" | "y";
49
+ transitionDuration?: number;
50
+ /** Number of tabs to render — drives both the trigger and content indexed slots. */
51
+ itemCount: number;
52
+ /** Shows a moving highlight behind the hovered tab. */
53
+ trackHover?: boolean;
54
+ /** Shows a moving highlight behind the active tab. */
55
+ trackActive?: boolean;
56
+ /** Shows a moving underline/sideline indicator beneath the active tab. */
57
+ trackIndicator?: boolean;
58
+ /** aria-label on the tablist — override for localisation. */
59
+ ariaLabel?: string;
60
+ styleClassPassthrough?: string | string[];
61
+ }
62
+
63
+ const props = withDefaults(defineProps<Props>(), {
64
+ axis: "x",
65
+ transitionDuration: 200,
66
+ trackHover: true,
67
+ trackActive: true,
68
+ trackIndicator: true,
69
+ ariaLabel: "Tabs",
70
+ styleClassPassthrough: () => [],
71
+ });
72
+
73
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
74
+
75
+ const tabsNavRef = ref<HTMLElement | null>(null);
76
+ const tabsContentRefs = ref<HTMLElement[] | null>(null);
77
+
78
+ const { initNavDecorators, navItemClicked, navItemHovered, navItemKeydown, resetHoverToActivePosition } = useTabs(
79
+ props.axis,
80
+ tabsNavRef,
81
+ tabsContentRefs,
82
+ props.transitionDuration,
83
+ { trackHover: props.trackHover, trackActive: props.trackActive, trackIndicator: props.trackIndicator }
84
+ );
85
+
86
+ onMounted(() => {
87
+ initNavDecorators();
88
+ });
89
+ </script>
90
+
91
+ <style lang="css">
92
+ @layer components {
93
+ .tabs-core {
94
+ .tabs-list {
95
+ position: relative;
96
+ display: flex;
97
+ width: fit-content;
98
+ z-index: 1;
99
+
100
+ list-style-type: none;
101
+ margin: 0;
102
+ padding: 0;
103
+
104
+ border-bottom: var(--tabs-nav-border, 0.1rem solid var(--slate-06));
105
+
106
+ .nav__hovered {
107
+ content: "";
108
+ position: absolute;
109
+ left: 0;
110
+ right: 0;
111
+ bottom: 0;
112
+ top: 0;
113
+ scale: var(--_width-hovered, 0.125) 1;
114
+ translate: var(--_x-hovered, 0) 0;
115
+ transform-origin: left;
116
+ transition:
117
+ scale var(--_transition-duration),
118
+ translate var(--_transition-duration);
119
+ z-index: 1;
120
+ background: var(--tabs-hover-indicator-colour, var(--slate-07));
121
+ }
122
+
123
+ .nav__active {
124
+ content: "";
125
+ position: absolute;
126
+ left: 0;
127
+ right: 0;
128
+ bottom: 0;
129
+ top: 0;
130
+ scale: var(--_width-active, 0.125) 1;
131
+ translate: var(--_x-active, 0) 0;
132
+ transform-origin: left;
133
+ transition:
134
+ scale var(--_transition-duration),
135
+ translate var(--_transition-duration);
136
+ z-index: 2;
137
+ background: var(--tabs-active-indicator-colour, var(--slate-09));
138
+ }
139
+
140
+ .nav__active-indicator {
141
+ content: "";
142
+ position: absolute;
143
+ left: 0;
144
+ right: 0;
145
+ bottom: 0;
146
+ scale: var(--_width-active, 0.125) 1;
147
+ translate: var(--_x-active, 0) 0;
148
+ transform-origin: left;
149
+ transition:
150
+ scale var(--_transition-duration),
151
+ translate var(--_transition-duration);
152
+ z-index: 3;
153
+ background: var(--tabs-underline-indicator-colour, var(--slate-10));
154
+ height: var(--tabs-underline-indicator-height, 0.4rem);
155
+ }
156
+
157
+ .tabs-list-item {
158
+ opacity: var(--tabs-list-item-opacity, 0.7);
159
+ position: relative;
160
+ transition: color var(--tabs-list-item-colour-transition-duration, 100ms);
161
+ z-index: 4;
162
+ background: transparent;
163
+ border: 0;
164
+ color: var(--tabs-list-item-colour, var(--slate-10));
165
+ cursor: pointer;
166
+ font: inherit;
167
+ text-transform: var(--tabs-list-item-text-transform, uppercase);
168
+ font-weight: var(--tabs-list-item-font-weight, 500);
169
+ margin: 0;
170
+ padding: var(--tabs-list-item-padding-block, 1em) var(--tabs-list-item-padding-inline, 2em);
171
+
172
+ /* Text colour tracks the indicator actually behind it — hover and active can differ. */
173
+ &:hover {
174
+ opacity: 1;
175
+ color: var(--tabs-hover-indicator-text-colour, var(--slate-00));
176
+ }
177
+
178
+ &[aria-selected="true"] {
179
+ opacity: 1;
180
+ color: var(
181
+ --tabs-list-item-colour-selected,
182
+ var(--tabs-active-indicator-text-colour, var(--slate-00))
183
+ );
184
+ }
185
+
186
+ &:focus-visible {
187
+ outline: var(--tabs-list-item-focus-outline-width, 2px) solid var(--theme-ring);
188
+ outline-offset: var(--tabs-list-item-focus-outline-offset, -2px);
189
+ }
190
+ }
191
+ }
192
+
193
+ .tab-content-wrapper {
194
+ display: grid;
195
+ grid-template-areas: "element-stack";
196
+
197
+ background-color: var(--tabs-content-background-colour, var(--slate-09));
198
+ border: var(--tabs-content-border-width, 0.1rem) solid var(--tabs-content-border-colour, var(--slate-06));
199
+ border-radius: var(--tabs-content-border-radius, 0);
200
+ outline: var(--tabs-content-border-width, 0.1rem) solid var(--tabs-content-border-colour, var(--slate-06));
201
+
202
+ .tab-content {
203
+ grid-area: element-stack;
204
+ display: none;
205
+
206
+ &:focus-visible {
207
+ outline: var(--tabs-list-item-focus-outline-width, 2px) solid var(--theme-ring);
208
+ outline-offset: calc(-1 * var(--tabs-list-item-focus-outline-width, 2px));
209
+ }
210
+ }
211
+ }
212
+
213
+ /*
214
+ * Deal with axis-y
215
+ **/
216
+
217
+ &.axis-y {
218
+ display: flex;
219
+ flex-direction: row;
220
+ gap: var(--tabs-axis-y-gap, 2em);
221
+
222
+ .tabs-list {
223
+ flex-direction: column;
224
+
225
+ border-bottom: initial;
226
+ border-left: var(--tabs-nav-border, 0.1rem solid var(--slate-06));
227
+ position: relative;
228
+
229
+ .tabs-list-item {
230
+ text-align: left;
231
+ width: 100%;
232
+ }
233
+
234
+ .nav__hovered,
235
+ .nav__active {
236
+ left: 0;
237
+ right: initial;
238
+ bottom: initial;
239
+ top: 0;
240
+ height: var(--_y-height);
241
+ transform-origin: top;
242
+ width: var(--_y-width);
243
+ }
244
+
245
+ .nav__hovered {
246
+ translate: 0 var(--_y-hovered, 0);
247
+ }
248
+
249
+ .nav__active {
250
+ translate: 0 var(--_y-active, 0);
251
+ }
252
+
253
+ .nav__active-indicator {
254
+ left: 0;
255
+ right: initial;
256
+ bottom: initial;
257
+ top: 0;
258
+ height: var(--_y-height);
259
+ scale: var(--_width-active, 0.125) 1;
260
+ translate: 0 var(--_y-active, 0);
261
+ transform-origin: top;
262
+ width: var(--tabs-underline-indicator-height, 0.4em);
263
+ }
264
+ }
265
+
266
+ .tab-content-wrapper {
267
+ flex-grow: 1;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ </style>
@@ -0,0 +1,199 @@
1
+ import TabsCore from "../TabsCore.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+
4
+ const meta: Meta<typeof TabsCore> = {
5
+ title: "Atoms/Navigation/TabsCore",
6
+ component: TabsCore,
7
+ argTypes: {
8
+ axis: {
9
+ control: "radio",
10
+ options: ["x", "y"],
11
+ description: "Layout axis — horizontal tab row or vertical tab column",
12
+ },
13
+ itemCount: {
14
+ control: { type: "number", min: 1, max: 6, step: 1 },
15
+ description: "Number of tabs — drives the tab-{n}-trigger / tab-{n}-content indexed slots",
16
+ },
17
+ transitionDuration: {
18
+ control: { type: "range", min: 0, max: 600, step: 10 },
19
+ description: "Duration (ms) of the moving indicator's transition",
20
+ },
21
+ trackHover: {
22
+ control: "boolean",
23
+ description: "Shows a moving highlight behind the hovered tab",
24
+ },
25
+ trackActive: {
26
+ control: "boolean",
27
+ description: "Shows a moving highlight behind the active tab",
28
+ },
29
+ trackIndicator: {
30
+ control: "boolean",
31
+ description: "Shows a moving underline/sideline indicator beneath the active tab",
32
+ },
33
+ ariaLabel: {
34
+ control: "text",
35
+ description: "aria-label on the tablist — override for localisation",
36
+ },
37
+ },
38
+ args: {
39
+ axis: "x",
40
+ itemCount: 3,
41
+ transitionDuration: 200,
42
+ trackHover: true,
43
+ trackActive: true,
44
+ trackIndicator: true,
45
+ ariaLabel: "Tabs",
46
+ },
47
+ parameters: {
48
+ docs: {
49
+ description: {
50
+ component:
51
+ "A tablist built from indexed slots (tab-{n}-trigger / tab-{n}-content, driven by itemCount) with moving hover/active/underline indicators. Supports arrow-key navigation (Left/Right or Up/Down depending on axis, plus Home/End) per the WAI-ARIA Tabs pattern.",
52
+ },
53
+ },
54
+ },
55
+ };
56
+
57
+ export default meta;
58
+ type Story = StoryObj<typeof TabsCore>;
59
+
60
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
61
+
62
+ const panels = [
63
+ { label: "Overview", content: "A short summary of the product goes here." },
64
+ { label: "Specs", content: "Technical specifications and dimensions." },
65
+ { label: "Reviews", content: "Customer reviews and ratings." },
66
+ ];
67
+
68
+ const tabSlots = panels
69
+ .map(
70
+ (panel, index) => `
71
+ <template #tab-${index}-trigger>${panel.label}</template>
72
+ <template #tab-${index}-content>
73
+ <div style="padding: 1.6rem;">${panel.content}</div>
74
+ </template>`
75
+ )
76
+ .join("\n");
77
+
78
+ const fivePanels = [
79
+ { label: "Overview", content: "A short summary of the product goes here." },
80
+ { label: "Specs", content: "Technical specifications and dimensions." },
81
+ { label: "Reviews", content: "Customer reviews and ratings." },
82
+ { label: "Shipping", content: "Delivery estimates and carrier options." },
83
+ { label: "Returns", content: "Return window and refund policy." },
84
+ ];
85
+
86
+ const fiveTabSlots = fivePanels
87
+ .map(
88
+ (panel, index) => `
89
+ <template #tab-${index}-trigger>${panel.label}</template>
90
+ <template #tab-${index}-content>
91
+ <div style="padding: 1.6rem;">${panel.content}</div>
92
+ </template>`
93
+ )
94
+ .join("\n");
95
+
96
+ // ─── Stories ──────────────────────────────────────────────────────────────────
97
+
98
+ export const Horizontal: Story = {
99
+ args: { itemCount: panels.length },
100
+ render: (args) => ({
101
+ components: { TabsCore },
102
+ setup() {
103
+ return { args };
104
+ },
105
+ template: `
106
+ <TabsCore v-bind="args">
107
+ ${tabSlots}
108
+ </TabsCore>
109
+ `,
110
+ }),
111
+ };
112
+
113
+ export const Vertical: Story = {
114
+ args: { itemCount: panels.length, axis: "y" },
115
+ render: (args) => ({
116
+ components: { TabsCore },
117
+ setup() {
118
+ return { args };
119
+ },
120
+ template: `
121
+ <TabsCore v-bind="args">
122
+ ${tabSlots}
123
+ </TabsCore>
124
+ `,
125
+ }),
126
+ };
127
+
128
+ export const IndicatorsOff: Story = {
129
+ name: "Indicators Off",
130
+ args: { itemCount: panels.length, trackHover: false, trackActive: false, trackIndicator: false },
131
+ render: (args) => ({
132
+ components: { TabsCore },
133
+ setup() {
134
+ return { args };
135
+ },
136
+ template: `
137
+ <TabsCore v-bind="args">
138
+ ${tabSlots}
139
+ </TabsCore>
140
+ `,
141
+ }),
142
+ };
143
+
144
+ export const FiveTabs: Story = {
145
+ name: "Five Tabs (regression check)",
146
+ args: { itemCount: fivePanels.length },
147
+ parameters: {
148
+ docs: {
149
+ description: {
150
+ story:
151
+ "itemCount raised to 5 as a regression check — the original hand-rolled version of this component had an indicator-positioning issue that only showed up with more than 3 tabs. Try hovering quickly across several tabs in a row, then clicking a tab several positions away from the active one (e.g. tab 1 → tab 5), to confirm the hover/active/underline indicators land in the right place without jumping or lagging behind.",
152
+ },
153
+ },
154
+ },
155
+ render: (args) => ({
156
+ components: { TabsCore },
157
+ setup() {
158
+ return { args };
159
+ },
160
+ template: `
161
+ <TabsCore v-bind="args">
162
+ ${fiveTabSlots}
163
+ </TabsCore>
164
+ `,
165
+ }),
166
+ };
167
+
168
+ export const IndicatorColours: Story = {
169
+ name: "Indicator Colours (demo)",
170
+ args: { itemCount: panels.length },
171
+ parameters: {
172
+ docs: {
173
+ description: {
174
+ story:
175
+ "Each moving indicator given a distinct colour via its public CSS token, purely to make the three separate states (hover / active / underline) easy to tell apart. Hover a tab to see the green highlight move independently of the blue active highlight — the label text stays white against both backgrounds via the matching --tabs-hover-indicator-text-colour / --tabs-active-indicator-text-colour tokens (see CONSUMER-STYLING.md), which previously had no effect since they were wired to the wrong element.",
176
+ },
177
+ },
178
+ },
179
+ render: (args) => ({
180
+ components: { TabsCore },
181
+ setup() {
182
+ const indicatorColourVars = {
183
+ "--tabs-hover-indicator-colour": "seagreen",
184
+ "--tabs-hover-indicator-text-colour": "white",
185
+ "--tabs-active-indicator-colour": "steelblue",
186
+ "--tabs-active-indicator-text-colour": "white",
187
+ "--tabs-underline-indicator-colour": "darkorange",
188
+ };
189
+ return { args, indicatorColourVars };
190
+ },
191
+ template: `
192
+ <div :style="indicatorColourVars">
193
+ <TabsCore v-bind="args">
194
+ ${tabSlots}
195
+ </TabsCore>
196
+ </div>
197
+ `,
198
+ }),
199
+ };