srcdev-nuxt-components 9.6.11 → 9.6.12
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/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- package/.claude/skills/components/marquee-scroller.md +51 -14
- package/.vscode/srcdev-component-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- package/package.json +1 -1
|
@@ -204,6 +204,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
204
204
|
`aria-hidden` (`ToggleSwitch`, 2026-10-16).
|
|
205
205
|
- Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
|
|
206
206
|
way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
|
|
207
|
+
- Pause-on-focus listens for `focusin`/`focusout` on the root (with a `relatedTarget`
|
|
208
|
+
containment check), not `focus`/`blur`, which don't bubble: focusable content inside an
|
|
209
|
+
`aria-hidden` moving track otherwise gets focus while still hidden and moving. A looping
|
|
210
|
+
duplicate copy gets `inert`, and a focus-caused pause must not undo a pause the user chose
|
|
211
|
+
(`MarqueeScroller`, 2026-10-18).
|
|
207
212
|
- `prefers-reduced-motion` is respected for any animation that isn't purely decorative.
|
|
208
213
|
- A mode that hides a native control's own indicator (a range thumb, a checkbox box, a caret)
|
|
209
214
|
draws its own visible state for the current value, and its interactive area isn't shrunk to a
|
|
@@ -57,10 +57,25 @@ Full token table in `app/components/05.forms/form-field/CONSUMER-STYLING.md`. Pu
|
|
|
57
57
|
Target state via `.form-field[data-width="wide"]`, `.form-field[data-has-gutter]`,
|
|
58
58
|
`.form-field[data-invalid]`.
|
|
59
59
|
|
|
60
|
+
FormField doesn't change `overflow-wrap` or `min-inline-size` on its slot content: wrapping long
|
|
61
|
+
copy is the slotted control's job (every `*Field` component handles its own). Plain text slotted
|
|
62
|
+
directly needs its own wrapping rule.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Storybook
|
|
67
|
+
|
|
68
|
+
`Components/Forms/Form Field/FormField`: `Default`, `AllWidths`, `NoGutter`, and `StressTest`
|
|
69
|
+
(each width, with and without gutter and error state, around an `InputTextField` with hostile
|
|
70
|
+
copy). Stories use the `CanvasSwitcher` decorator.
|
|
71
|
+
|
|
60
72
|
---
|
|
61
73
|
|
|
62
74
|
## History
|
|
63
75
|
|
|
76
|
+
**2026-10-17**: added the `StressTest` story, `CanvasSwitcher` decorator and Docs-page
|
|
77
|
+
descriptions; nothing in the component broke under it.
|
|
78
|
+
|
|
64
79
|
**Migrated 2026-09-26** (0/5 → full compliance):
|
|
65
80
|
|
|
66
81
|
- Width and gutter modifiers moved from bare classes (`.narrow`, `.medium`, `.wide`, `.has-gutter`)
|
|
@@ -21,8 +21,8 @@ ARIA semantics.
|
|
|
21
21
|
| `:legend` | `string` | `""` | Legend text. The legend renders when this or the `legend` slot is given. |
|
|
22
22
|
| `group-role` | `"group" \| "radiogroup"` | `"group"` | `"group"` renders no `role` attribute (a fieldset's native role is already `group`). `"radiogroup"` sets `role="radiogroup"`: use it for radio buttons only. |
|
|
23
23
|
| `:required` | `boolean` | `false` | Sets `aria-required="true"` **only** when `group-role="radiogroup"`. `aria-required` isn't allowed on `role="group"`, so checkbox groups get nothing; put "(required)" in the legend text instead. |
|
|
24
|
-
| `:field-has-error` | `boolean` | `false` | Sets `aria-invalid` on the fieldset
|
|
25
|
-
| `:data-testid` | `string` | `""` | Passed to the fieldset. |
|
|
24
|
+
| `:field-has-error` | `boolean` | `false` | Sets `aria-invalid="true"` on the fieldset; the attribute is absent when `false`. |
|
|
25
|
+
| `:data-testid` | `string` | `""` | Passed to the fieldset; omitted when empty. |
|
|
26
26
|
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the fieldset, via `useStyleClassPassthrough` (reacts to prop changes). |
|
|
27
27
|
|
|
28
28
|
## Slots
|
|
@@ -54,10 +54,26 @@ content `--form-fieldset-content-margin-block|margin-inline`, legend
|
|
|
54
54
|
`--form-fieldset-legend-color|font-size|font-weight|line-height|margin-block|margin-inline`. Error state hook:
|
|
55
55
|
`.form-fieldset[aria-invalid="true"]`.
|
|
56
56
|
|
|
57
|
+
The fieldset sets `min-inline-size: 0` and the legend `overflow-wrap: anywhere`, so neither
|
|
58
|
+
overflows a narrow column. Slotted controls handle their own wrapping.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Storybook
|
|
63
|
+
|
|
64
|
+
`Components/Forms/Form Fieldset/FormFieldset`: `Default`, `CheckboxGroup`, `WithError`, and
|
|
65
|
+
`StressTest` (hostile legend copy with the error state on). Stories use the `CanvasSwitcher`
|
|
66
|
+
decorator and link to the three components that render FormFieldset.
|
|
67
|
+
|
|
57
68
|
---
|
|
58
69
|
|
|
59
70
|
## History
|
|
60
71
|
|
|
72
|
+
**2026-10-17**: StressTest story, CanvasSwitcher and Docs-page descriptions added. Fixed: a
|
|
73
|
+
valid fieldset rendered `aria-invalid="false"`; an empty `dataTestid` rendered `data-testid=""`;
|
|
74
|
+
the fieldset had no `min-inline-size: 0` (each wrapping component set it on its own class), and
|
|
75
|
+
the legend didn't wrap unbroken copy.
|
|
76
|
+
|
|
61
77
|
**Migrated 2026-09-26** (0/5 → full compliance):
|
|
62
78
|
|
|
63
79
|
- **Role bug fixed:** `role="radiogroup"` used to be hardcoded, so screen readers announced
|
|
@@ -10,8 +10,9 @@ type: reference
|
|
|
10
10
|
|
|
11
11
|
`MarqueeScroller` renders an infinite horizontal scroller for small repeating items — client
|
|
12
12
|
logos, badges, testimonial avatars. The track is duplicated once so the loop is seamless
|
|
13
|
-
(
|
|
14
|
-
automatically on hover,
|
|
13
|
+
(`.marquee-scroller-group` renders twice: once real, once `aria-hidden="true"` and `inert`), and the
|
|
14
|
+
whole thing pauses automatically on hover, whenever focus is inside it, and when the user has
|
|
15
|
+
`prefers-reduced-motion` set.
|
|
15
16
|
|
|
16
17
|
## Props
|
|
17
18
|
|
|
@@ -24,11 +25,12 @@ automatically on hover, on keyboard focus, and when the user has `prefers-reduce
|
|
|
24
25
|
| `ariaLabel` | `string` | `"Scrolling content"` | `aria-label` on the root region — override for localisation. |
|
|
25
26
|
| `ariaDescription` | `string` | `"Use spacebar to pause or play the animation."` | Screen-reader-only instructions rendered inside the region — override for localisation. |
|
|
26
27
|
| `showControls` | `boolean` | `false` | Shows a visible pause/play button. Recommended (WCAG 2.2.2, Pause/Stop/Hide) whenever content scrolls automatically and indefinitely. |
|
|
27
|
-
| `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for users with `prefers-reduced-motion: reduce
|
|
28
|
+
| `respectReducedMotion` | `boolean` | `true` | Auto-pauses and disables the animation for users with `prefers-reduced-motion: reduce` (and keeps it paused after focus leaves). Focus-pausing no longer depends on it. |
|
|
28
29
|
| `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the control button while paused. Ignored if the `toggle-icon` slot is used. |
|
|
29
30
|
| `pauseIcon` | `string` | `"mdi:pause"` | Iconify icon name shown on the control button while playing. Ignored if the `toggle-icon` slot is used. |
|
|
30
|
-
| `playLabel` | `string` | `"Play animation"` | Control button
|
|
31
|
-
| `pauseLabel` | `string` | `"Pause animation"` | Control button
|
|
31
|
+
| `playLabel` | `string` | `"Play animation"` | Control button text (visually hidden, its accessible name) while paused — override for localisation. |
|
|
32
|
+
| `pauseLabel` | `string` | `"Pause animation"` | Control button text (visually hidden, its accessible name) while playing — override for localisation. |
|
|
33
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `.marquee-scroller`, via `useStyleClassPassthrough` (reacts to prop changes). |
|
|
32
34
|
|
|
33
35
|
Import the item types from the library's type exports when building `marqueeData`/`itemConfig`
|
|
34
36
|
outside the component:
|
|
@@ -52,8 +54,10 @@ One dynamically-named slot per `marqueeData` entry, named after that item's `id`
|
|
|
52
54
|
</MarqueeScroller>
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
Each slot is rendered twice (once in the visible group, once in the `aria-hidden`
|
|
56
|
-
so the loop wraps seamlessly
|
|
57
|
+
Each slot is rendered twice (once in the visible group, once in the `aria-hidden`, `inert`
|
|
58
|
+
duplicate group) so the loop wraps seamlessly. Don't put anything with side effects or unique ids
|
|
59
|
+
inside a slot. Links are fine: only the real copy is focusable. Items are fixed-size boxes; text
|
|
60
|
+
wraps inside them (`overflow-wrap: anywhere`) but content taller than `itemConfig.height` overflows.
|
|
57
61
|
|
|
58
62
|
`toggle-icon` (scoped with `{ isPaused: boolean }`) replaces the control button's icon entirely —
|
|
59
63
|
use this instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, a
|
|
@@ -70,16 +74,25 @@ different icon set):
|
|
|
70
74
|
## Accessibility behaviour
|
|
71
75
|
|
|
72
76
|
- The root is `role="region"` with `tabindex="0"` so it's independently focusable.
|
|
73
|
-
- Spacebar toggles pause/play while the region is focused
|
|
77
|
+
- Spacebar toggles pause/play while the region itself is focused (keydown from inside, e.g. on the
|
|
78
|
+
control button or a link, is left to that element). Arrow keys do nothing — there is no
|
|
74
79
|
manual-stepping behaviour, so the default `ariaDescription` doesn't claim one (fixed 2026-09-08;
|
|
75
80
|
it previously announced "arrow keys ... for manual control" with no implementation behind it).
|
|
76
|
-
-
|
|
77
|
-
|
|
81
|
+
- Focus anywhere inside the region (the region itself, or a link in an item) pauses the animation
|
|
82
|
+
and removes `aria-hidden` from the track, so the focused content is announced. Focus moving between
|
|
83
|
+
elements inside keeps it paused. When focus leaves, it resumes only if focus caused the pause and
|
|
84
|
+
the user doesn't prefer reduced motion: a pause chosen with the button or spacebar sticks.
|
|
85
|
+
Focus landing on the control button doesn't auto-pause, so one click pauses. (Changed 2026-10-17:
|
|
86
|
+
this used to listen for `focus`/`blur` on the region only, gated on `respectReducedMotion`, so
|
|
87
|
+
tabbing to a link in an item left it moving and `aria-hidden`.)
|
|
78
88
|
- Hovering anywhere over the scroller pauses the track and applies
|
|
79
89
|
`--marquee-scroller-group-hover-filter` (default `grayscale(1)`) to every item; hovering an
|
|
80
90
|
individual item restores it via `--marquee-scroller-item-hover-filter` (default `grayscale(0)`).
|
|
81
91
|
- `aria-live` switches to `"polite"` while paused so screen readers announce the paused content.
|
|
82
|
-
- The
|
|
92
|
+
- The track is `aria-hidden="true"` while playing (attribute removed when paused). The duplicate
|
|
93
|
+
group is always `aria-hidden="true"` and `inert`, so its copies of any links can't be focused.
|
|
94
|
+
- The control button is an `InputButton` (`variant="inline"`, icon-only): `playLabel`/`pauseLabel`
|
|
95
|
+
is its visually hidden text rather than an `aria-label`.
|
|
83
96
|
- All user-visible copy (`ariaLabel`, `ariaDescription`, `playLabel`, `pauseLabel`) is a plain
|
|
84
97
|
string prop with an English default — there's no i18n framework dependency in this library, so
|
|
85
98
|
pass translated strings from the consumer's own i18n solution.
|
|
@@ -110,11 +123,11 @@ theme); only its border width is (`--marquee-scroller-control-border-width`).
|
|
|
110
123
|
|
|
111
124
|
## Seamless loop math
|
|
112
125
|
|
|
113
|
-
The track duplicates `marqueeData` into two `.marquee-group` children so the loop can wrap without
|
|
126
|
+
The track duplicates `marqueeData` into two `.marquee-scroller-group` children so the loop can wrap without
|
|
114
127
|
a visible cut. The animation must shift the track by exactly one group's width plus its connecting
|
|
115
128
|
gap — not `translateX(-50%)`, which overshoots short of the seam once the track's own `gap`
|
|
116
129
|
between the two groups is added to its total width. This is computed as a private
|
|
117
|
-
`--_track-shift: calc(itemCount * (itemWidth + gap))` token and used in the `
|
|
130
|
+
`--_track-shift: calc(itemCount * (itemWidth + gap))` token and used in the `marquee-scroller-move`
|
|
118
131
|
keyframe. Fixed 2026-09-08 — the original `-50%` version produced a small but visible gap/overlap
|
|
119
132
|
at the loop point once more than a couple of items were in `marqueeData`.
|
|
120
133
|
|
|
@@ -125,6 +138,30 @@ section on a wide viewport), the loop briefly shows empty space before snapping
|
|
|
125
138
|
the visible track only spans as wide as the content, not the container. On mount (and on
|
|
126
139
|
container resize, via `ResizeObserver`), the component measures one rendered group's `scrollWidth`
|
|
127
140
|
against the root element's `offsetWidth` and repeats `marqueeData` internally (`repeatCount`)
|
|
128
|
-
enough times per `.marquee-group` so a single group's width always covers the container. This is
|
|
141
|
+
enough times per `.marquee-scroller-group` so a single group's width always covers the container. This is
|
|
129
142
|
transparent to consumers — it doesn't change `marqueeData` or any prop, only how many times each
|
|
130
143
|
item's slot is rendered internally. Fixed 2026-09-08.
|
|
144
|
+
|
|
145
|
+
The repeat count is capped at 50 copies per group, so tiny items in a huge container can't flood
|
|
146
|
+
the DOM (added 2026-10-17).
|
|
147
|
+
|
|
148
|
+
## Storybook
|
|
149
|
+
|
|
150
|
+
`Atoms/Effects/MarqueeScroller`: `WithControls`, `FocusableItems` (links in items, for the focus
|
|
151
|
+
behaviour), and `StressTest` (24 items of hostile copy, a broken image, long localised labels, a
|
|
152
|
+
single item, tiny items hitting the repeat cap, and empty data). Stories use the `CanvasSwitcher`
|
|
153
|
+
decorator.
|
|
154
|
+
|
|
155
|
+
## History
|
|
156
|
+
|
|
157
|
+
**2026-10-17** (stress-test pass, breaking class renames):
|
|
158
|
+
|
|
159
|
+
- Structural classes prefixed (`.marquee-scroller-item`, `-control`, `-track`, `-group`,
|
|
160
|
+
`-description`); bare `.reverse`/`.paused`/`.reduced-motion` became `data-*` attributes; keyframes
|
|
161
|
+
renamed `marquee-scroller-move`. See CONSUMER-STYLING.md for the full map.
|
|
162
|
+
- Raw `<button>` replaced with `InputButton variant="inline"` (icon-only slot).
|
|
163
|
+
- Focus-pausing reworked (focusin/focusout, see Accessibility behaviour); duplicate group `inert`.
|
|
164
|
+
- `prefers-reduced-motion` change listener is now removed on unmount.
|
|
165
|
+
- New `--marquee-scroller-focus-outline-colour` token (was bare `--theme-ring`).
|
|
166
|
+
- Added `styleClassPassthrough`. Removed the redundant `aspect-ratio: 1 / 1` on items (width and
|
|
167
|
+
height are both set, and it mis-centres on old Safari). Items wrap unbroken text.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "FormField layout wrapper around a single form control",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
|
-
"<FormField width=\"${1|wide,medium,narrow|}\" :has-gutter=\"${2|false,true|}\" :field-has-error=\"!!$
|
|
6
|
+
"<FormField width=\"${1|wide,medium,narrow|}\" :has-gutter=\"${2|false,true|}\" :field-has-error=\"!!${3:errors.fieldName}\">",
|
|
7
7
|
" $0",
|
|
8
8
|
"</FormField>"
|
|
9
9
|
]
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
"description": "CSS override scaffold for FormField tokens",
|
|
13
13
|
"scope": "css",
|
|
14
14
|
"body": [
|
|
15
|
-
".$
|
|
15
|
+
".${1:my-page} {",
|
|
16
16
|
" .form-field {",
|
|
17
|
-
" --form-field-max-width-narrow: $2;",
|
|
18
|
-
" --form-field-gutter-width: $3;",
|
|
19
|
-
" --form-field-margin-block: $4;",
|
|
20
|
-
" --form-field-background-color: $5;",
|
|
17
|
+
" --form-field-max-width-narrow: ${2:400px};",
|
|
18
|
+
" --form-field-gutter-width: ${3:1.6rem};",
|
|
19
|
+
" --form-field-margin-block: ${4:0 1.6rem};",
|
|
20
|
+
" --form-field-background-color: ${5:transparent};",
|
|
21
|
+
" --form-field-border-radius: ${6:0.4rem};",
|
|
21
22
|
" }",
|
|
22
23
|
"}"
|
|
23
24
|
]
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
6
|
"<FormFieldset",
|
|
7
|
-
" id=\"$
|
|
8
|
-
" name=\"$
|
|
9
|
-
" legend=\"$
|
|
7
|
+
" id=\"${1:fieldId}\"",
|
|
8
|
+
" name=\"${2:fieldName}\"",
|
|
9
|
+
" legend=\"${3:Legend}\"",
|
|
10
10
|
" group-role=\"${4|group,radiogroup|}\"",
|
|
11
|
-
" :field-has-error=\"!!$
|
|
11
|
+
" :field-has-error=\"!!${5:errors.fieldName}\"",
|
|
12
12
|
">",
|
|
13
13
|
" <template #content>",
|
|
14
14
|
" $0",
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"description": "CSS override scaffold for FormFieldset tokens",
|
|
21
21
|
"scope": "css",
|
|
22
22
|
"body": [
|
|
23
|
-
".$
|
|
23
|
+
".${1:my-page} {",
|
|
24
24
|
" .form-fieldset {",
|
|
25
|
-
" --form-fieldset-padding-block: $
|
|
26
|
-
" --form-fieldset-padding-inline: $
|
|
27
|
-
" --form-fieldset-border: $
|
|
28
|
-
" --form-fieldset-legend-color: $
|
|
29
|
-
" --form-fieldset-legend-font-size: $
|
|
30
|
-
" --form-fieldset-legend-font-weight: $
|
|
31
|
-
" --form-fieldset-legend-margin-block: $
|
|
25
|
+
" --form-fieldset-padding-block: ${2:0};",
|
|
26
|
+
" --form-fieldset-padding-inline: ${3:0};",
|
|
27
|
+
" --form-fieldset-border: ${4:0};",
|
|
28
|
+
" --form-fieldset-legend-color: ${5:inherit};",
|
|
29
|
+
" --form-fieldset-legend-font-size: ${6:var(--step-5)};",
|
|
30
|
+
" --form-fieldset-legend-font-weight: ${7:normal};",
|
|
31
|
+
" --form-fieldset-legend-margin-block: ${8:0.8rem};",
|
|
32
32
|
" }",
|
|
33
33
|
"}"
|
|
34
34
|
]
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
"body": [
|
|
6
6
|
"<MarqueeScroller",
|
|
7
7
|
" :marquee-data=\"[",
|
|
8
|
-
" { id: 1, content: '$
|
|
9
|
-
" { id: 2, content: '$
|
|
8
|
+
" { id: 1, content: '${1:logo-a}' },",
|
|
9
|
+
" { id: 2, content: '${2:logo-b}' },",
|
|
10
10
|
" ]\"",
|
|
11
11
|
" :item-config=\"{ width: '${3:120px}', height: '${4:60px}', gap: '${5:24px}' }\"",
|
|
12
|
+
" :style-class-passthrough=\"[${6:}]\"",
|
|
12
13
|
">",
|
|
13
14
|
" <template #1>",
|
|
14
|
-
" $
|
|
15
|
+
" $7",
|
|
15
16
|
" </template>",
|
|
16
17
|
" <template #2>",
|
|
17
|
-
" $
|
|
18
|
+
" $8",
|
|
18
19
|
" </template>",
|
|
19
20
|
"</MarqueeScroller>"
|
|
20
21
|
]
|
|
@@ -24,11 +25,11 @@
|
|
|
24
25
|
"scope": "vue,html",
|
|
25
26
|
"body": [
|
|
26
27
|
"<MarqueeScroller",
|
|
27
|
-
" :marquee-data=\"$
|
|
28
|
+
" :marquee-data=\"${1:items}\"",
|
|
28
29
|
" show-controls",
|
|
29
|
-
" aria-label=\"$
|
|
30
|
+
" aria-label=\"${2:Scrolling client logos}\"",
|
|
30
31
|
">",
|
|
31
|
-
" <template v-for=\"item in $
|
|
32
|
+
" <template v-for=\"item in ${1:items}\" #[item.id] :key=\"item.id\">",
|
|
32
33
|
" $3",
|
|
33
34
|
" </template>",
|
|
34
35
|
"</MarqueeScroller>"
|
|
@@ -39,14 +40,14 @@
|
|
|
39
40
|
"scope": "vue,html",
|
|
40
41
|
"body": [
|
|
41
42
|
"<MarqueeScroller",
|
|
42
|
-
" :marquee-data=\"$
|
|
43
|
+
" :marquee-data=\"${1:items}\"",
|
|
43
44
|
" show-controls",
|
|
44
45
|
" play-icon=\"${2:mdi:play-circle}\"",
|
|
45
46
|
" pause-icon=\"${3:mdi:pause-circle}\"",
|
|
46
47
|
" play-label=\"${4:Play}\"",
|
|
47
48
|
" pause-label=\"${5:Pause}\"",
|
|
48
49
|
">",
|
|
49
|
-
" <template v-for=\"item in $
|
|
50
|
+
" <template v-for=\"item in ${1:items}\" #[item.id] :key=\"item.id\">",
|
|
50
51
|
" $6",
|
|
51
52
|
" </template>",
|
|
52
53
|
"</MarqueeScroller>"
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"description": "MarqueeScroller with the toggle-icon slot fully replacing the control button's icon",
|
|
57
58
|
"scope": "vue,html",
|
|
58
59
|
"body": [
|
|
59
|
-
"<MarqueeScroller :marquee-data=\"$
|
|
60
|
+
"<MarqueeScroller :marquee-data=\"${1:items}\" show-controls>",
|
|
60
61
|
" <template #toggle-icon=\"{ isPaused }\">",
|
|
61
62
|
" $2",
|
|
62
63
|
" </template>",
|
|
@@ -67,25 +68,27 @@
|
|
|
67
68
|
"description": "CSS override scaffold for MarqueeScroller — scope to a page or section class",
|
|
68
69
|
"scope": "css",
|
|
69
70
|
"body": [
|
|
70
|
-
".$
|
|
71
|
+
".${1:my-page} {",
|
|
71
72
|
" .marquee-scroller {",
|
|
72
|
-
" --marquee-scroller-fade-width: ;",
|
|
73
|
-
" --marquee-scroller-focus-outline-width: ;",
|
|
74
|
-
" --marquee-scroller-focus-outline-
|
|
75
|
-
" --marquee-scroller-
|
|
76
|
-
" --marquee-scroller-item-border-
|
|
77
|
-
" --marquee-scroller-item-border-
|
|
78
|
-
" --marquee-scroller-item-
|
|
79
|
-
" --marquee-scroller-
|
|
80
|
-
" --marquee-scroller-
|
|
81
|
-
" --marquee-scroller-
|
|
82
|
-
" --marquee-scroller-control-
|
|
83
|
-
" --marquee-scroller-control-background-colour
|
|
84
|
-
" --marquee-scroller-control-
|
|
85
|
-
" --marquee-scroller-control-
|
|
86
|
-
" --marquee-scroller-control-border-
|
|
87
|
-
" --marquee-scroller-control-
|
|
88
|
-
" --marquee-scroller-control-
|
|
73
|
+
" --marquee-scroller-fade-width: ${2:10%};",
|
|
74
|
+
" --marquee-scroller-focus-outline-width: ${3:2px};",
|
|
75
|
+
" --marquee-scroller-focus-outline-colour: ${4:var(--theme-ring)};",
|
|
76
|
+
" --marquee-scroller-focus-outline-offset: ${5:2px};",
|
|
77
|
+
" --marquee-scroller-item-border-colour: ${6:var(--slate-10)};",
|
|
78
|
+
" --marquee-scroller-item-border-width: ${7:1px};",
|
|
79
|
+
" --marquee-scroller-item-border-radius: ${8:4px};",
|
|
80
|
+
" --marquee-scroller-item-transition-duration: ${9:0.5s};",
|
|
81
|
+
" --marquee-scroller-group-hover-filter: ${10:grayscale(1)};",
|
|
82
|
+
" --marquee-scroller-item-hover-filter: ${11:grayscale(0)};",
|
|
83
|
+
" --marquee-scroller-control-offset: ${12:8px};",
|
|
84
|
+
" --marquee-scroller-control-background-colour: ${13:rgba(0, 0, 0, 0.7)};",
|
|
85
|
+
" --marquee-scroller-control-background-colour-hover: ${14:rgba(0, 0, 0, 0.9)};",
|
|
86
|
+
" --marquee-scroller-control-text-colour: ${15:white};",
|
|
87
|
+
" --marquee-scroller-control-border-radius: ${16:4px};",
|
|
88
|
+
" --marquee-scroller-control-border-width: ${17:1px};",
|
|
89
|
+
" --marquee-scroller-control-padding: ${18:8px};",
|
|
90
|
+
" --marquee-scroller-control-font-size: ${19:14px};",
|
|
91
|
+
" --marquee-scroller-control-transition-duration: ${20:0.2s};",
|
|
89
92
|
" }",
|
|
90
93
|
"}"
|
|
91
94
|
]
|
|
@@ -2,17 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Public token API
|
|
4
4
|
|
|
5
|
+
### Region
|
|
6
|
+
|
|
5
7
|
| Token | Default | Controls |
|
|
6
8
|
|---|---|---|
|
|
7
9
|
| `--marquee-scroller-fade-width` | `10%` | Width of the transparent fade at each edge of the track (`mask-image`) |
|
|
8
|
-
| `--marquee-scroller-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused
|
|
10
|
+
| `--marquee-scroller-focus-outline-width` | `2px` | Outline width on the root region and the control button when keyboard-focused |
|
|
11
|
+
| `--marquee-scroller-focus-outline-colour` | `var(--theme-ring)` | Outline colour on the same two focus states |
|
|
9
12
|
| `--marquee-scroller-focus-outline-offset` | `2px` | Outline offset on the same two focus states |
|
|
13
|
+
|
|
14
|
+
### Items
|
|
15
|
+
|
|
16
|
+
| Token | Default | Controls |
|
|
17
|
+
|---|---|---|
|
|
10
18
|
| `--marquee-scroller-group-hover-filter` | `grayscale(1)` | Filter applied to every item when the pointer hovers anywhere over the scroller |
|
|
11
19
|
| `--marquee-scroller-item-hover-filter` | `grayscale(0)` | Filter applied to a single item when it is individually hovered (overrides the group filter for that item) |
|
|
12
20
|
| `--marquee-scroller-item-transition-duration` | `0.5s` | Transition duration for the item hover filter change |
|
|
13
21
|
| `--marquee-scroller-item-border-width` | `1px` | Item border width |
|
|
14
22
|
| `--marquee-scroller-item-border-colour` | `var(--slate-10)` | Item border colour |
|
|
15
23
|
| `--marquee-scroller-item-border-radius` | `4px` | Item border radius |
|
|
24
|
+
|
|
25
|
+
### Control button
|
|
26
|
+
|
|
27
|
+
| Token | Default | Controls |
|
|
28
|
+
|---|---|---|
|
|
16
29
|
| `--marquee-scroller-control-offset` | `8px` | Distance of the pause/play button from the top and end edges |
|
|
17
30
|
| `--marquee-scroller-control-background-colour` | `rgba(0, 0, 0, 0.7)` | Pause/play button background |
|
|
18
31
|
| `--marquee-scroller-control-background-colour-hover` | `rgba(0, 0, 0, 0.9)` | Pause/play button background on hover |
|
|
@@ -21,15 +34,35 @@
|
|
|
21
34
|
| `--marquee-scroller-control-padding` | `8px` | Pause/play button padding |
|
|
22
35
|
| `--marquee-scroller-control-font-size` | `14px` | Pause/play button icon font size |
|
|
23
36
|
| `--marquee-scroller-control-transition-duration` | `0.2s` | Pause/play button background-colour transition duration |
|
|
24
|
-
| `--marquee-scroller-control-border-width` | `1px` | Pause/play button border width
|
|
37
|
+
| `--marquee-scroller-control-border-width` | `1px` | Pause/play button border width under `prefers-contrast: high`. The border colour itself stays the system `ButtonText` keyword, see below. |
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
Private tokens (not public API): `--_fade-width`, `--_focus-outline` (width + colour, shared by
|
|
40
|
+
both focus states) and `--_track-shift` (the loop distance, computed from `itemConfig`).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## State hooks
|
|
45
|
+
|
|
46
|
+
The root `.marquee-scroller` carries:
|
|
47
|
+
|
|
48
|
+
| Attribute | When |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `data-paused` | The track is paused (control button, spacebar, focus inside, or reduced motion) |
|
|
51
|
+
| `data-reverse` | `reverse` is `true` |
|
|
52
|
+
| `data-reduced-motion` | The user prefers reduced motion and `respect-reduced-motion` is on |
|
|
53
|
+
|
|
54
|
+
Inner elements: `.marquee-scroller-track` (the animated row), `.marquee-scroller-group` (one copy
|
|
55
|
+
of the items; the second is `aria-hidden` and `inert`), `.marquee-scroller-item` (one fixed-size
|
|
56
|
+
item box), `.marquee-scroller-control` (the pause/play `InputButton`) and
|
|
57
|
+
`.marquee-scroller-description` (the visually hidden instructions).
|
|
58
|
+
|
|
59
|
+
> **Changed 2026-10-17**: the generic class names were renamed so they can't collide with consumer
|
|
60
|
+
> CSS. `.item` → `.marquee-scroller-item`, `.control-btn` → `.marquee-scroller-control` (now an
|
|
61
|
+
> `InputButton`, so it also has `.input-button`), `.marquee-track` → `.marquee-scroller-track`,
|
|
62
|
+
> `.marquee-group` → `.marquee-scroller-group`, `.sr-only` → `.marquee-scroller-description`. The
|
|
63
|
+
> bare state classes became attributes: `.reverse` → `[data-reverse]`, `.paused` →
|
|
64
|
+
> `[data-paused]`, `.reduced-motion` → `[data-reduced-motion]`. The keyframes are now
|
|
65
|
+
> `marquee-scroller-move` (was `marqueeMove`).
|
|
33
66
|
|
|
34
67
|
---
|
|
35
68
|
|
|
@@ -37,10 +70,9 @@
|
|
|
37
70
|
|
|
38
71
|
Under `prefers-contrast: high`, the control button switches to the system colour keywords
|
|
39
72
|
`ButtonFace`/`ButtonText` rather than the component's own background/text tokens. These aren't
|
|
40
|
-
exposed as overridable tokens on purpose
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
constraint.
|
|
73
|
+
exposed as overridable tokens on purpose: they track the user's OS-level high-contrast theme, which
|
|
74
|
+
a component-level override would defeat. Only the border *width* in that mode is a token
|
|
75
|
+
(`--marquee-scroller-control-border-width`), since it's a plain dimension with no such constraint.
|
|
44
76
|
|
|
45
77
|
---
|
|
46
78
|
|
|
@@ -48,7 +80,7 @@ constraint.
|
|
|
48
80
|
|
|
49
81
|
Item `width`/`height`/`gap` and the animation duration are set via the `itemConfig` and
|
|
50
82
|
`animationRuntime` props (not CSS custom properties), since they also drive the component's own
|
|
51
|
-
|
|
83
|
+
loop math (`--_track-shift`) and the repeat measurement. Override those through props:
|
|
52
84
|
|
|
53
85
|
```vue
|
|
54
86
|
<MarqueeScroller
|
|
@@ -57,58 +89,49 @@ layout math (`aspect-ratio`, track width). Override those through props:
|
|
|
57
89
|
/>
|
|
58
90
|
```
|
|
59
91
|
|
|
60
|
-
|
|
92
|
+
Item boxes are a fixed size. Text in an item wraps (`overflow-wrap: anywhere`), but content taller
|
|
93
|
+
than `itemConfig.height` overflows, so size slot content to the box.
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
---
|
|
63
96
|
|
|
64
|
-
|
|
65
|
-
markup for each item via that dynamically-named slot:
|
|
97
|
+
## Global theming
|
|
66
98
|
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<template #2>
|
|
73
|
-
<img src="/logos/b.svg" alt="Logo B" />
|
|
74
|
-
</template>
|
|
75
|
-
</MarqueeScroller>
|
|
99
|
+
```css
|
|
100
|
+
:where(html) {
|
|
101
|
+
--marquee-scroller-fade-width: 15%;
|
|
102
|
+
--marquee-scroller-item-border-colour: var(--slate-04);
|
|
103
|
+
}
|
|
76
104
|
```
|
|
77
105
|
|
|
78
106
|
---
|
|
79
107
|
|
|
80
|
-
##
|
|
81
|
-
|
|
82
|
-
The pause/play button's icon and its `aria-label` copy are all overridable — see `playIcon`/
|
|
83
|
-
`pauseIcon`/`playLabel`/`pauseLabel` props, or the `toggle-icon` slot for a fully custom icon
|
|
84
|
-
(scoped with `isPaused`):
|
|
108
|
+
## Local overrides
|
|
85
109
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
play-label="Reproduire"
|
|
92
|
-
pause-label="Suspendre"
|
|
93
|
-
/>
|
|
94
|
-
```
|
|
110
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
111
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
112
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
113
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the
|
|
114
|
+
component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
95
115
|
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
```css
|
|
117
|
+
.partners-section {
|
|
118
|
+
--marquee-scroller-item-border-colour: #b5651d;
|
|
119
|
+
--marquee-scroller-control-background-colour: #1a1a1a;
|
|
120
|
+
--marquee-scroller-fade-width: 20%;
|
|
121
|
+
}
|
|
102
122
|
```
|
|
103
123
|
|
|
104
124
|
---
|
|
105
125
|
|
|
106
|
-
##
|
|
126
|
+
## Class passthrough
|
|
127
|
+
|
|
128
|
+
`:style-class-passthrough` (string or string array) adds classes to the root `.marquee-scroller`
|
|
129
|
+
region.
|
|
130
|
+
|
|
131
|
+
---
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
component (it falls through to the root element)): they inherit down into the component. Keep the
|
|
110
|
-
block **unlayered** (no `@layer` wrapper) so it beats the library's `@layer components`. If your own
|
|
111
|
-
file uses `<style scoped>`, tokens set on your element still work, but selectors that reach inside
|
|
112
|
-
the component need `:deep()`. Patterns and examples:
|
|
113
|
-
`.claude/skills/component-local-style-override.md`.
|
|
133
|
+
## Notes
|
|
114
134
|
|
|
135
|
+
- Item content and the control button's icon and copy are covered in the skill doc
|
|
136
|
+
(`marquee-scroller.md`): one slot per `marqueeData` id, `toggle-icon` slot, and the
|
|
137
|
+
`playIcon`/`pauseIcon`/`playLabel`/`pauseLabel` props.
|