srcdev-nuxt-components 9.4.7 → 9.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/output.html +1 -1
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/header-block.md +92 -0
- package/.claude/skills/components/rotating-carousel-image.md +101 -0
- package/.claude/skills/components/skip-links.md +82 -0
- package/.claude/skills/components/tabs-core.md +187 -0
- package/.claude/skills/index.md +4 -0
- package/.vscode/srcdev-component-header-block.code-snippets +41 -0
- package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
- package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
- package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
- package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +98 -0
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
- package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
- package/app/composables/useTabs.ts +225 -207
- package/app/types/components/index.ts +2 -0
- package/app/types/components/rotating-carousel-image.d.ts +4 -0
- package/app/types/components/skip-links.d.ts +4 -0
- package/package.json +1 -1
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
- package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
- package/app/components/skip-links/SkipLinks.vue +0 -60
- package/app/components/tabs/TabsCore.vue +0 -306
- package/app/components/typography/HeaderBlock.vue +0 -35
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
|
@@ -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".
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV RotatingCarouselImage Basic": {
|
|
3
|
+
"description": "RotatingCarouselImage — 3D rotating image carousel with scroll-parallax tilt",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<RotatingCarouselImage",
|
|
7
|
+
" :data=\"[",
|
|
8
|
+
" { src: '${1:/carousel/1.jpg}', alt: '${2:Image 1}' },",
|
|
9
|
+
" { src: '${3:/carousel/2.jpg}', alt: '${4:Image 2}' },",
|
|
10
|
+
" ]\"",
|
|
11
|
+
">$5</RotatingCarouselImage>"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"SRCDEV RotatingCarouselImage With Controls": {
|
|
15
|
+
"description": "RotatingCarouselImage with the visible pause/play control button (WCAG 2.2.2)",
|
|
16
|
+
"scope": "vue,html",
|
|
17
|
+
"body": [
|
|
18
|
+
"<RotatingCarouselImage",
|
|
19
|
+
" :data=\"$1images\"",
|
|
20
|
+
" show-controls",
|
|
21
|
+
" aria-label=\"$2Rotating product photos\"",
|
|
22
|
+
"/>"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"SRCDEV RotatingCarouselImage Static Tilt": {
|
|
26
|
+
"description": "RotatingCarouselImage with scroll-parallax disabled, using a fixed rotateX instead",
|
|
27
|
+
"scope": "vue,html",
|
|
28
|
+
"body": [
|
|
29
|
+
"<RotatingCarouselImage",
|
|
30
|
+
" :data=\"$1images\"",
|
|
31
|
+
" :use-parallax-effect=\"false\"",
|
|
32
|
+
" :rotate-x=\"${2:10}\"",
|
|
33
|
+
" show-controls",
|
|
34
|
+
"/>"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"SRCDEV RotatingCarouselImage Custom Toggle Icon Slot": {
|
|
38
|
+
"description": "RotatingCarouselImage with the toggle-icon slot fully replacing the control button's icon",
|
|
39
|
+
"scope": "vue,html",
|
|
40
|
+
"body": [
|
|
41
|
+
"<RotatingCarouselImage :data=\"$1images\" show-controls>",
|
|
42
|
+
" <template #toggle-icon=\"{ isPaused }\">",
|
|
43
|
+
" $2",
|
|
44
|
+
" </template>",
|
|
45
|
+
"</RotatingCarouselImage>"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"SRCDEV RotatingCarouselImage CSS Override": {
|
|
49
|
+
"description": "CSS override scaffold for RotatingCarouselImage — scope to a page or section class",
|
|
50
|
+
"scope": "css",
|
|
51
|
+
"body": [
|
|
52
|
+
".$1my-page {",
|
|
53
|
+
" .rotating-carousel {",
|
|
54
|
+
" --rotating-carousel-height: ;",
|
|
55
|
+
" --rotating-carousel-item-width: ;",
|
|
56
|
+
" --rotating-carousel-item-height: ;",
|
|
57
|
+
" --rotating-carousel-offset-bottom: ;",
|
|
58
|
+
" --rotating-carousel-rotation-duration: ;",
|
|
59
|
+
" --rotating-carousel-z-index: ;",
|
|
60
|
+
" --rotating-carousel-focus-outline-width: ;",
|
|
61
|
+
" --rotating-carousel-focus-outline-offset: ;",
|
|
62
|
+
" --rotating-carousel-control-offset: ;",
|
|
63
|
+
" --rotating-carousel-control-background-colour: ;",
|
|
64
|
+
" --rotating-carousel-control-background-colour-hover: ;",
|
|
65
|
+
" --rotating-carousel-control-text-colour: ;",
|
|
66
|
+
" --rotating-carousel-control-border-radius: ;",
|
|
67
|
+
" --rotating-carousel-control-border-width: ;",
|
|
68
|
+
" --rotating-carousel-control-padding: ;",
|
|
69
|
+
" --rotating-carousel-control-font-size: ;",
|
|
70
|
+
" }",
|
|
71
|
+
"}"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV SkipLinks Basic": {
|
|
3
|
+
"description": "SkipLinks — accessibility skip-navigation utility, default main-content/footer links",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<SkipLinks />"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"SRCDEV SkipLinks Custom Links": {
|
|
10
|
+
"description": "SkipLinks with a custom list of skip targets",
|
|
11
|
+
"scope": "vue,html",
|
|
12
|
+
"body": [
|
|
13
|
+
"<SkipLinks",
|
|
14
|
+
" :links=\"[",
|
|
15
|
+
" { href: '${1:#site-navigation}', label: '${2:Skip to navigation}' },",
|
|
16
|
+
" { href: '${3:#main-content}', label: '${4:Skip to main content}' },",
|
|
17
|
+
" { href: '${5:#footer-content}', label: '${6:Skip to footer}' },",
|
|
18
|
+
" ]\"",
|
|
19
|
+
"/>"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"SRCDEV SkipLinks With Home Link Slot": {
|
|
23
|
+
"description": "SkipLinks with the homeLink slot for a logo/home link before the skip-nav",
|
|
24
|
+
"scope": "vue,html",
|
|
25
|
+
"body": [
|
|
26
|
+
"<SkipLinks>",
|
|
27
|
+
" <template #homeLink>",
|
|
28
|
+
" $1",
|
|
29
|
+
" </template>",
|
|
30
|
+
"</SkipLinks>"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"SRCDEV SkipLinks CSS Override": {
|
|
34
|
+
"description": "CSS override scaffold for SkipLinks — scope to a page or section class",
|
|
35
|
+
"scope": "css",
|
|
36
|
+
"body": [
|
|
37
|
+
".$1my-page {",
|
|
38
|
+
" .skip-links {",
|
|
39
|
+
" --skip-links-z-index: ;",
|
|
40
|
+
" --skip-links-gap: ;",
|
|
41
|
+
" --skip-links-background-colour: ;",
|
|
42
|
+
" --skip-links-border-colour: ;",
|
|
43
|
+
" --skip-links-border-width: ;",
|
|
44
|
+
" --skip-links-padding: ;",
|
|
45
|
+
" --skip-links-text-colour: ;",
|
|
46
|
+
" --skip-links-link-padding-block: ;",
|
|
47
|
+
" --skip-links-link-padding-inline: ;",
|
|
48
|
+
" --skip-links-transition-duration: ;",
|
|
49
|
+
" --skip-links-focus-outline-width: ;",
|
|
50
|
+
" --skip-links-focus-outline-colour: ;",
|
|
51
|
+
" }",
|
|
52
|
+
"}"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV TabsCore Basic": {
|
|
3
|
+
"description": "TabsCore — horizontal tablist with indexed trigger/content slots",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<TabsCore :item-count=\"${1:3}\">",
|
|
7
|
+
" <template #tab-0-trigger>${2:Overview}</template>",
|
|
8
|
+
" <template #tab-0-content>",
|
|
9
|
+
" $3",
|
|
10
|
+
" </template>",
|
|
11
|
+
" <template #tab-1-trigger>${4:Specs}</template>",
|
|
12
|
+
" <template #tab-1-content>",
|
|
13
|
+
" $5",
|
|
14
|
+
" </template>",
|
|
15
|
+
" <template #tab-2-trigger>${6:Reviews}</template>",
|
|
16
|
+
" <template #tab-2-content>",
|
|
17
|
+
" $7",
|
|
18
|
+
" </template>",
|
|
19
|
+
"</TabsCore>"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"SRCDEV TabsCore Vertical": {
|
|
23
|
+
"description": "TabsCore with axis=\"y\" for a vertical tab column",
|
|
24
|
+
"scope": "vue,html",
|
|
25
|
+
"body": [
|
|
26
|
+
"<TabsCore axis=\"y\" :item-count=\"${1:3}\" aria-label=\"${2:Settings}\">",
|
|
27
|
+
" <template #tab-0-trigger>$3</template>",
|
|
28
|
+
" <template #tab-0-content>$4</template>",
|
|
29
|
+
"</TabsCore>"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"SRCDEV TabsCore Indicators Off": {
|
|
33
|
+
"description": "TabsCore with the moving hover/active/underline decorators all disabled",
|
|
34
|
+
"scope": "vue,html",
|
|
35
|
+
"body": [
|
|
36
|
+
"<TabsCore",
|
|
37
|
+
" :item-count=\"${1:3}\"",
|
|
38
|
+
" :track-hover=\"false\"",
|
|
39
|
+
" :track-active=\"false\"",
|
|
40
|
+
" :track-indicator=\"false\"",
|
|
41
|
+
">",
|
|
42
|
+
" $2",
|
|
43
|
+
"</TabsCore>"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"SRCDEV TabsCore CSS Override": {
|
|
47
|
+
"description": "CSS override scaffold for TabsCore — scope to a page or section class",
|
|
48
|
+
"scope": "css",
|
|
49
|
+
"body": [
|
|
50
|
+
".$1my-page {",
|
|
51
|
+
" .tabs-core {",
|
|
52
|
+
" --tabs-nav-border: ;",
|
|
53
|
+
" --tabs-hover-indicator-colour: ;",
|
|
54
|
+
" --tabs-hover-indicator-text-colour: ;",
|
|
55
|
+
" --tabs-active-indicator-colour: ;",
|
|
56
|
+
" --tabs-active-indicator-text-colour: ;",
|
|
57
|
+
" --tabs-underline-indicator-colour: ;",
|
|
58
|
+
" --tabs-underline-indicator-height: ;",
|
|
59
|
+
" --tabs-list-item-opacity: ;",
|
|
60
|
+
" --tabs-list-item-colour: ;",
|
|
61
|
+
" --tabs-list-item-colour-selected: ;",
|
|
62
|
+
" --tabs-list-item-text-transform: ;",
|
|
63
|
+
" --tabs-list-item-font-weight: ;",
|
|
64
|
+
" --tabs-list-item-padding-block: ;",
|
|
65
|
+
" --tabs-list-item-padding-inline: ;",
|
|
66
|
+
" --tabs-list-item-colour-transition-duration: ;",
|
|
67
|
+
" --tabs-list-item-focus-outline-width: ;",
|
|
68
|
+
" --tabs-list-item-focus-outline-offset: ;",
|
|
69
|
+
" --tabs-content-background-colour: ;",
|
|
70
|
+
" --tabs-content-border-width: ;",
|
|
71
|
+
" --tabs-content-border-colour: ;",
|
|
72
|
+
" --tabs-content-border-radius: ;",
|
|
73
|
+
" --tabs-axis-y-gap: ;",
|
|
74
|
+
" }",
|
|
75
|
+
"}"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# RotatingCarouselImage — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--rotating-carousel-height` | `70svh` | Height of the carousel's root element |
|
|
8
|
+
| `--rotating-carousel-item-width` | `200px` | Width of each rotating item (and the slider box itself) |
|
|
9
|
+
| `--rotating-carousel-item-height` | `250px` | Height of each rotating item (and the slider box itself) |
|
|
10
|
+
| `--rotating-carousel-offset-bottom` | `35%` | Vertical position of the slider box within the root element |
|
|
11
|
+
| `--rotating-carousel-rotation-duration` | `30s` | Duration of one full 360° rotation |
|
|
12
|
+
| `--rotating-carousel-z-index` | `2` | Stacking context of the slider box |
|
|
13
|
+
| `--rotating-carousel-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused (colour is `--theme-ring`) |
|
|
14
|
+
| `--rotating-carousel-focus-outline-offset` | `2px` | Outline offset on the same two focus states |
|
|
15
|
+
| `--rotating-carousel-control-offset` | `8px` | Distance of the pause/play button from the top and end edges |
|
|
16
|
+
| `--rotating-carousel-control-background-colour` | `rgba(0, 0, 0, 0.7)` | Pause/play button background |
|
|
17
|
+
| `--rotating-carousel-control-background-colour-hover` | `rgba(0, 0, 0, 0.9)` | Pause/play button background on hover |
|
|
18
|
+
| `--rotating-carousel-control-text-colour` | `white` | Pause/play button icon colour |
|
|
19
|
+
| `--rotating-carousel-control-border-radius` | `4px` | Pause/play button border radius |
|
|
20
|
+
| `--rotating-carousel-control-padding` | `8px` | Pause/play button padding |
|
|
21
|
+
| `--rotating-carousel-control-font-size` | `14px` | Pause/play button icon font size |
|
|
22
|
+
| `--rotating-carousel-control-transition-duration` | `0.2s` | Pause/play button background-colour transition duration |
|
|
23
|
+
| `--rotating-carousel-control-border-width` | `1px` | Pause/play button border width in forced-colors/high-contrast mode (`prefers-contrast: high`) |
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
.my-page {
|
|
27
|
+
--rotating-carousel-item-width: 260px;
|
|
28
|
+
--rotating-carousel-item-height: 320px;
|
|
29
|
+
--rotating-carousel-rotation-duration: 45s;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## High-contrast mode colours are not tokenised
|
|
34
|
+
|
|
35
|
+
Under `prefers-contrast: high`, the control button switches to the system colour keywords
|
|
36
|
+
`ButtonFace`/`ButtonText` rather than the component's own background/text tokens, so it tracks the
|
|
37
|
+
user's OS-level high-contrast theme. Only the border *width* in that mode is a token
|
|
38
|
+
(`--rotating-carousel-control-border-width`).
|
|
39
|
+
|
|
40
|
+
## Rotation geometry is prop-driven, not tokens
|
|
41
|
+
|
|
42
|
+
`rotateX`, `perspective`, and `translateZ` control the 3D tilt and radius of the rotation and are
|
|
43
|
+
set via props (not CSS custom properties), since they also drive the component's own scroll-based
|
|
44
|
+
parallax math:
|
|
45
|
+
|
|
46
|
+
```vue
|
|
47
|
+
<RotatingCarouselImage :data="images" :perspective="1200" :translate-z="1200" />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Control button icon and copy
|
|
51
|
+
|
|
52
|
+
The pause/play button's icon and its `aria-label` copy are overridable via `playIcon`/`pauseIcon`/
|
|
53
|
+
`playLabel`/`pauseLabel` props, or the `toggle-icon` slot for a fully custom icon (scoped with
|
|
54
|
+
`isPaused`) — same pattern as `MarqueeScroller`.
|