srcdev-nuxt-components 9.4.5 → 9.4.7
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/commands/migrate-component.md +74 -5
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +27 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/column-flow-grid.md +93 -0
- package/.claude/skills/components/content-docs.md +2 -0
- package/.claude/skills/components/display-tooltip-defined.md +3 -0
- package/.claude/skills/components/display-tooltip.md +1 -0
- package/.claude/skills/components/entry-animation.md +88 -0
- package/.claude/skills/components/input-copy.md +2 -0
- package/.claude/skills/components/input-text-core.md +186 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/masonry-grid.md +153 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/pop-over.md +100 -0
- package/.claude/skills/components/responsive-header.md +3 -0
- package/.claude/skills/components/site-header.md +4 -0
- package/.claude/skills/components/site-navigation.md +1 -0
- package/.claude/skills/components/tab-navigation.md +1 -0
- package/.claude/skills/index.md +7 -1
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +15 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +11 -0
- package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
- package/.vscode/srcdev-component-input-text.code-snippets +107 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
- package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
- package/.vscode/srcdev-component-site-header.code-snippets +17 -0
- package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
- package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
- package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
- package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
- package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
- package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +4 -1
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +16 -3
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
- package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
- package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
- package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
- package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
- package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
- package/app/types/components/index.ts +1 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/app/types/forms/types.forms.d.ts +1 -1
- package/package.json +1 -1
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
- package/app/components/masonry-grid/MasonryGrid.vue +0 -68
- package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
- package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
- package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
- package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
- package/app/components/pop-over/PopOver.vue +0 -90
- package/app/layouts/default.vue +0 -308
- package/app/layouts/site-navigation-demo.vue +0 -188
|
@@ -389,6 +389,7 @@ footer.note code{
|
|
|
389
389
|
<th data-key="snippet" data-type="bool">Snippet</th>
|
|
390
390
|
<th data-key="variants" data-type="bool" title="Uses a variants/ subfolder">Variants folder</th>
|
|
391
391
|
<th data-key="legacy_props" data-type="bool" title="Still options-style defineProps({...}), not defineProps<Props>()">Legacy props</th>
|
|
392
|
+
<th data-key="story_args_bug" data-type="bool" title="A story destructures/refs Storybook's reactive args at setup-time, so most Controls-panel changes silently stop updating the rendered story after first render">Story args bug</th>
|
|
392
393
|
<th data-key="score" data-type="num">Score</th>
|
|
393
394
|
</tr>
|
|
394
395
|
</thead>
|
|
@@ -398,6 +399,7 @@ footer.note code{
|
|
|
398
399
|
|
|
399
400
|
<footer class="note">
|
|
400
401
|
<p><strong>Methodology:</strong> "Styling doc", "Tests", "Story" and "Variants folder" are filesystem checks (<code>CONSUMER-STYLING.md</code>, a <code>tests/</code> dir, a <code>stories/</code> dir or <code>*.stories.ts</code> file, a <code>variants/</code> subfolder). "Skill doc" and "Snippet" match the component's kebab-case name against <code>.claude/skills/components/*.md</code> and <code>.vscode/*.code-snippets</code> — a small number of near-miss names may read as false negatives and are worth a manual glance before acting. "Score" is the count of the five doc/test/story/skill/snippet checks passed, out of 5, and is what "reference-quality" and "only incomplete" are based on. "Legacy props" is a source check for options-style <code>defineProps({...})</code> rather than <code>defineProps<Props>()</code> — like "Variants folder" it is <em>not</em> counted in the 5-point score (deliberately, so it stays visible instead of vanishing into an already-high score), but it does factor into "reference-quality" and "only incomplete", same as "Variants folder".</p>
|
|
402
|
+
<p style="margin-top:0.8rem;"><strong>"Story args bug"</strong> flags a story whose <code>setup()</code> destructures or spreads Storybook's <code>args</code> into local variables/refs at mount time (e.g. <code>const { modelValue, ...otherArgs } = args;</code> then <code>ref(modelValue)</code>). @storybook/vue3 mounts the story component once and mutates the same <code>args</code> object in place on every Controls change — it does not re-run <code>setup()</code> — so a destructured copy freezes at its initial value and most Controls silently stop updating the rendered story after first render. The fix is to bind the template straight to <code>args.x</code> (optionally through a <code>computed()</code> that strips non-prop extra args), never to a value copied out in <code>setup()</code>. Also not counted in the 5-point score, for the same reason as "Legacy props".</p>
|
|
401
403
|
<p style="margin-top:0.8rem;"><strong>On <code>--_</code> vars:</strong> the public/private split is decided by consumer relevance, not reuse count — a private var is correct even used once, as long as the value has no plausible consumer override. This column is a raw count of <code>--_</code>-prefixed CSS custom properties per group; it is <em>not</em> scored and doesn't factor into "reference-quality" or "score." A high count is only a prompt to check each one by hand for a value that should have been public (a colour, size, or per-state variant with no override token) — the bug class the <code>ServicesCard</code> fix (CLAUDE.md pitfall #20) caught, not the presence of the prefix itself.</p>
|
|
402
404
|
<p style="margin-top:0.8rem;"><strong>This page regenerates from <code>.claude/component-ledger/build.mjs</code></strong>, triggered whenever a staged commit touches <code>app/components/</code> (see the <code>create-commit-message</code> skill's hook). All numbers on this page, including the narrative above, are computed live from the latest scan — nothing here is hand-written prose that can drift out of date.</p>
|
|
403
405
|
</footer>
|
|
@@ -437,10 +439,11 @@ statDefs.forEach(sd => {
|
|
|
437
439
|
});
|
|
438
440
|
|
|
439
441
|
// ---- dynamic narrative (every number computed from DATA, nothing hand-written) ----
|
|
440
|
-
const fullyCompliant = DATA.filter(d => d.score === 5 && d.tier !== "NONE" && !d.variants && !d.legacy_props);
|
|
442
|
+
const fullyCompliant = DATA.filter(d => d.score === 5 && d.tier !== "NONE" && !d.variants && !d.legacy_props && !d.story_args_bug);
|
|
441
443
|
const unplaced = DATA.filter(d => d.tier === "NONE");
|
|
442
444
|
const forkedVariants = DATA.filter(d => d.variants);
|
|
443
445
|
const legacyProps = DATA.filter(d => d.legacy_props);
|
|
446
|
+
const storyArgsBug = DATA.filter(d => d.story_args_bug);
|
|
444
447
|
const noSkillAndUnplaced = unplaced.filter(d => !d.skill).length;
|
|
445
448
|
const topPrivate = DATA.filter(d => d.priv_tokens > 0).sort((a,b) => b.priv_tokens - a.priv_tokens).slice(0, 3);
|
|
446
449
|
|
|
@@ -451,6 +454,7 @@ narrativeEl.innerHTML = `
|
|
|
451
454
|
<p><strong>${unplaced.length} groups</strong> still sit loose at <code class="mono">app/components/</code> top level rather than inside a numbered tier. ${noSkillAndUnplaced} of those ${unplaced.length} also have no skill doc — an unplaced component tends to also be an undocumented one.</p>
|
|
452
455
|
<p>${forkedVariants.length ? `<strong>${forkedVariants.length} group${forkedVariants.length===1?"":"s"}</strong> still fork${forkedVariants.length===1?"s":""} variants into a <code class="mono">variants/</code> subfolder — deprecated in favour of documenting the variation in the parent skill.md.` : `No component groups currently fork variants into a <code class="mono">variants/</code> subfolder.`}</p>
|
|
453
456
|
<p>${legacyProps.length ? `<strong>${legacyProps.length} group${legacyProps.length===1?"":"s"}</strong> still use${legacyProps.length===1?"s":""} options-style <code class="mono">defineProps({...})</code> instead of <code class="mono">defineProps<Props>()</code> — this doesn't move the 5-point score, so it can hide behind an otherwise-complete-looking group.` : `No component groups currently use options-style defineProps.`}</p>
|
|
457
|
+
<p>${storyArgsBug.length ? `<strong>${storyArgsBug.length} group${storyArgsBug.length===1?"":"s"}</strong> ha${storyArgsBug.length===1?"s":"ve"} a story that destructures/refs Storybook's <code class="mono">args</code> at setup-time (${storyArgsBug.map(d => `<code class="mono">${d.compdir.split("/").pop()}</code>`).join(", ")}) — the Controls panel silently stops updating most props on that story after first render. This doesn't move the 5-point score either, for the same reason as "Legacy props".` : `No component groups currently have this Storybook Controls reactivity bug.`}</p>
|
|
454
458
|
<p><strong>${topPrivate.length ? "On --_ vars:" : ""}</strong> ${topPrivate.length
|
|
455
459
|
? `the largest private-token surfaces right now are ${topPrivate.map(d => `<code class="mono">${d.compdir.split("/").pop()}</code> (${d.priv_tokens})`).join(", ")} — worth a manual look for a value that should have been public, per the corrected rule in the footer below. A high count on its own isn't a defect.`
|
|
456
460
|
: ""}</p>
|
|
@@ -517,7 +521,7 @@ function tierLabel(t){ return t === "NONE" ? "unplaced" : t; }
|
|
|
517
521
|
function render(){
|
|
518
522
|
let rows = DATA.slice();
|
|
519
523
|
if (activeTier !== "all") rows = rows.filter(d => d.tier === activeTier);
|
|
520
|
-
if (onlyIncomplete) rows = rows.filter(d => d.score < 5 || d.tier === "NONE" || d.variants || d.legacy_props);
|
|
524
|
+
if (onlyIncomplete) rows = rows.filter(d => d.score < 5 || d.tier === "NONE" || d.variants || d.legacy_props || d.story_args_bug);
|
|
521
525
|
const q = searchEl.value.trim().toLowerCase();
|
|
522
526
|
if (q) rows = rows.filter(d => d.compdir.toLowerCase().includes(q));
|
|
523
527
|
|
|
@@ -546,6 +550,7 @@ function render(){
|
|
|
546
550
|
<td class="check">${check(d.snippet)}</td>
|
|
547
551
|
<td class="check">${d.variants ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
548
552
|
<td class="check">${d.legacy_props ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
553
|
+
<td class="check">${d.story_args_bug ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
549
554
|
<td><span class="score-bar">${dots}</span></td>
|
|
550
555
|
</tr>`;
|
|
551
556
|
}).join("");
|
|
@@ -13,6 +13,7 @@ current page).
|
|
|
13
13
|
|------|------|---------|----------|
|
|
14
14
|
| `items` | `BreadcrumbItem[]` | — | **yes** |
|
|
15
15
|
| `separator` | `string` | `"/"` | no |
|
|
16
|
+
| `ariaLabel` | `string` | `"Breadcrumb"` | no — override for localisation |
|
|
16
17
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | no |
|
|
17
18
|
|
|
18
19
|
`BreadcrumbItem` is exported from `~/types/components/breadcrumb` (and re-exported from
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ColumnFlowGrid
|
|
3
|
+
description: ColumnFlowGrid CSS multi-column (columns) text-flow layout — named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API. Not a true masonry — see MasonryGrid for that.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ColumnFlowGrid
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`ColumnFlowGrid` lays out slot content using CSS `columns` (the "multi-column" text-flow module —
|
|
12
|
+
the same mechanism newspaper-style text layouts use). Every slot is wrapped in a
|
|
13
|
+
`.column-flow-grid-item` div with `break-inside: avoid` so an item never splits across columns.
|
|
14
|
+
|
|
15
|
+
**Renamed from `MasonryGrid`** during this compliance pass — despite the old name, this component
|
|
16
|
+
doesn't do anything masonry-specific: it's CSS `columns` and nothing else, with items simply
|
|
17
|
+
flowing into whichever column is currently shortest **in DOM order**, not reordered for reading.
|
|
18
|
+
See **Which one do I want?** below.
|
|
19
|
+
|
|
20
|
+
## Which one do I want? (`ColumnFlowGrid` vs `MasonryGrid`)
|
|
21
|
+
|
|
22
|
+
| | Technique | DOM/reading order | Column heights | Resize behaviour |
|
|
23
|
+
|---|---|---|---|---|
|
|
24
|
+
| `ColumnFlowGrid` | CSS `columns` | Column-major (as authored) | Uneven, CSS-native column-fill | Instant, native — no JS |
|
|
25
|
+
| `MasonryGrid` | JS-measured, greedy shortest-column packing | Natural (as authored — no reorder needed) | Uneven, genuinely bin-packed | Animated (items slide into new positions) |
|
|
26
|
+
|
|
27
|
+
Use **`ColumnFlowGrid`** when you just want cheap CSS-only column flow and don't need real
|
|
28
|
+
masonry packing or a resize animation — nothing here is measured or JS-driven at all, so it's the
|
|
29
|
+
lighter option. Use **`MasonryGrid`** (`.claude/skills/components/masonry-grid.md`) for genuine
|
|
30
|
+
Pinterest-style packing (each item placed into whichever column is currently shortest) with
|
|
31
|
+
animated reflow on resize.
|
|
32
|
+
|
|
33
|
+
`MasonryGridOrdered` (a third, JS-measured sibling that predated this pair) has been retired — its
|
|
34
|
+
implementation was absorbed into `MasonryGrid` directly. See `masonry-grid.md`'s migration history
|
|
35
|
+
note for details.
|
|
36
|
+
|
|
37
|
+
## Props
|
|
38
|
+
|
|
39
|
+
| Prop | Type | Default | Description |
|
|
40
|
+
|------|------|---------|-------------|
|
|
41
|
+
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
|
|
42
|
+
| `itemMinWidth` | `number` | `300` | Minimum column width in pixels — drives `columns: auto <value>px`. |
|
|
43
|
+
| `gap` | `number` | `1.2` | Gap between columns/items, in `unit`. |
|
|
44
|
+
| `unit` | `string` | `"rem"` | CSS unit applied to `gap`. |
|
|
45
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
46
|
+
|
|
47
|
+
## Slot API
|
|
48
|
+
|
|
49
|
+
Any named slot is accepted — there are no declared slot names, and **no count/data prop to keep
|
|
50
|
+
in sync**. The component iterates `$slots` and wraps each in a `.column-flow-grid-item`:
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<ColumnFlowGrid>
|
|
54
|
+
<template #item-1>...</template>
|
|
55
|
+
<template #item-2>...</template>
|
|
56
|
+
<template #item-3>...</template>
|
|
57
|
+
</ColumnFlowGrid>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Slot names can be anything (`item-1`/`item-2` is just a convention, matching `AutoGrid`).
|
|
61
|
+
|
|
62
|
+
## Basic usage
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<ColumnFlowGrid :item-min-width="280" :gap="1.6">
|
|
66
|
+
<template v-for="(photo, i) in photos" :key="photo.id" #[`item-${i}`]>
|
|
67
|
+
<img :src="photo.src" :alt="photo.alt" />
|
|
68
|
+
</template>
|
|
69
|
+
</ColumnFlowGrid>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## CSS Token Customization
|
|
73
|
+
|
|
74
|
+
| Token | Falls back to | Controls |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `--column-flow-grid-item-border-colour` | `var(--theme-border)` | Outline colour around each item |
|
|
77
|
+
| `--column-flow-grid-item-padding` | `1.2rem` | Inner padding of each item |
|
|
78
|
+
|
|
79
|
+
Column width and gap are controlled via the `itemMinWidth`/`gap`/`unit` props, not CSS custom
|
|
80
|
+
properties, since they need JS-computed values rather than being purely CSS-driven. See
|
|
81
|
+
`CONSUMER-STYLING.md` in the component folder.
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- **Renamed from `MasonryGrid`** (2026-09-13) — see **Which one do I want?** above for why. If you
|
|
86
|
+
have consumer code importing this as `MasonryGrid`, it needs updating to `ColumnFlowGrid`; the
|
|
87
|
+
props and behaviour are otherwise unchanged. CSS classes/tokens were renamed to match
|
|
88
|
+
(`masonry-grid-*` → `column-flow-grid-*`).
|
|
89
|
+
- **Column count is not fixed**: `columns: auto <itemMinWidth>` lets the browser decide how many
|
|
90
|
+
columns fit — there's no `columnCount` prop. Use `itemMinWidth` to influence it.
|
|
91
|
+
- Item ordering is left-to-right by column-fill in `columns` layout (an item goes into the current
|
|
92
|
+
shortest column), not top-to-bottom reading order — be aware of this if visual order matters for
|
|
93
|
+
the specific content being laid out. This is the exact distinction `MasonryGrid` exists to solve.
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
| `docsNavLabel` | `string` | `"Navigation"` | Heading text for the `docsNav` panel's `#summary`. |
|
|
19
19
|
| `docsPageNavLabel` | `string` | `"On this page"` | Heading text for the `docsPageNav` panel's `#summary`. |
|
|
20
20
|
| `panelVariant` | `"modern" \| "classic"` | `"classic"` | Which panel component renders `docsNav`/`docsPageNav`: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit — the mobile nav (which relies on `contentIsOnTop`) becomes unreachable there. See CLAUDE.md pitfall #19. |
|
|
21
|
+
| `docsNavAriaLabel` | `string` | `"Docs navigation"` | aria-label on the `docsNav` `<nav>` landmark — override for localisation. |
|
|
22
|
+
| `docsPageNavAriaLabel` | `string` | `"On this page"` | aria-label on the `docsPageNav` `<nav>` landmark — override for localisation. |
|
|
21
23
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.content-docs` element. |
|
|
22
24
|
|
|
23
25
|
`DocsNavItem` (from `~/types/components`):
|
|
@@ -20,6 +20,9 @@ directly when you need full control over the popover's content.
|
|
|
20
20
|
|------|------|---------|-------------|
|
|
21
21
|
| `tooltipId` | `string` | `""` | Base id for the popover. Prefixed with `nuxt-tooltip-` when provided; auto-generated via `useId()` (also prefixed) when omitted. |
|
|
22
22
|
| `contentText` | `TooltipContentText` | `{}` | Title/body/action content. Each field is optional — omitted fields render nothing. |
|
|
23
|
+
| `triggerAriaLabel` | `string` | `"Toggle the popover"` | Forwarded to the underlying `DisplayTooltip`'s `triggerAriaLabel` — override for localisation. |
|
|
24
|
+
| `closeButtonText` | `string` | `"Close"` | Visible text on the close button — override for localisation. |
|
|
25
|
+
| `closeButtonAriaLabel` | `string` | `"Close tool tip"` | aria-label on the close button — override for localisation. |
|
|
23
26
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the underlying `DisplayTooltip` root element. |
|
|
24
27
|
|
|
25
28
|
`TooltipContentText` is exported from `~/types/components`:
|
|
@@ -24,6 +24,7 @@ requirement, same caution as `DeepExpandingMenu`'s equivalent gap.
|
|
|
24
24
|
|------|------|---------|-------------|
|
|
25
25
|
| `tooltipId` | `string` | `""` | Id linking the trigger button (`popovertarget`) to the popover panel (`id`). Auto-generated via `useId()` when omitted. |
|
|
26
26
|
| `hideTrigger` | `boolean` | `false` | Visually hides the trigger button (kept in the DOM, not removed) — e.g. when an equivalent trigger is supplied via the `triggerContent` slot. |
|
|
27
|
+
| `triggerAriaLabel` | `string` | `"Toggle the popover"` | aria-label on the trigger button — override for localisation. |
|
|
27
28
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
28
29
|
|
|
29
30
|
## Slots
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# EntryAnimation Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`EntryAnimation` wraps slot content in a configurable tag and applies one of three CSS
|
|
6
|
+
scroll-driven entry-animation utility classes (`entry-slide-in`, `entry-zoom-reveal`,
|
|
7
|
+
`entry-exit-blur` — see `.claude/skills/css-animation-utilities.md`). It has no styling of its
|
|
8
|
+
own beyond applying the class; the animation keyframes/timeline live in the shared
|
|
9
|
+
`06.utility-classes/animations` stylesheet.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Props reference
|
|
14
|
+
|
|
15
|
+
> **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:animation-type`, `:skip-animation`.
|
|
16
|
+
|
|
17
|
+
| Prop (template form) | Type | Default | Notes |
|
|
18
|
+
| --------------------------- | -------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------- |
|
|
19
|
+
| `:tag` | `"div" \| "section" \| "article" \| "aside"` | `"div"` | Root element tag. |
|
|
20
|
+
| `:animation-type` | `"entry-slide-in" \| "entry-zoom-reveal" \| "entry-exit-blur"` | `"entry-slide-in"` | Which utility animation class to apply. |
|
|
21
|
+
| `:skip-animation` | `boolean` | `false` | Renders with no animation class at all — see **Skipping the animation** below. |
|
|
22
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element; always applied regardless of `skipAnimation`. |
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Slots
|
|
27
|
+
|
|
28
|
+
| Slot | Notes |
|
|
29
|
+
| --------- | ---------------------------------------- |
|
|
30
|
+
| `default` | The content to animate in. |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Skipping the animation
|
|
35
|
+
|
|
36
|
+
The common case in a `v-for` loop over a list of cards/sections is that the **first** item is
|
|
37
|
+
already above the fold — animating it in only delays content the visitor can already see when the
|
|
38
|
+
page loads. Before `skipAnimation` existed, the only way to handle this was swapping the whole
|
|
39
|
+
component out for a plain tag per-item:
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<!-- ❌ Old workaround — loses tag/styleClassPassthrough consistency for item 0 -->
|
|
43
|
+
<component :is="index === 0 ? 'div' : EntryAnimation">
|
|
44
|
+
...
|
|
45
|
+
</component>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `skip-animation` instead — it keeps the same component, tag, and `styleClassPassthrough` for
|
|
49
|
+
every item, it just omits the animation class for the ones that don't need it:
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<EntryAnimation
|
|
53
|
+
v-for="(item, index) in items"
|
|
54
|
+
:key="item.id"
|
|
55
|
+
:skip-animation="index === 0"
|
|
56
|
+
animation-type="entry-slide-in"
|
|
57
|
+
>
|
|
58
|
+
<template #default>
|
|
59
|
+
<!-- item content -->
|
|
60
|
+
</template>
|
|
61
|
+
</EntryAnimation>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## CSS Token Customization
|
|
67
|
+
|
|
68
|
+
None — this component has no `--entry-animation-*` tokens or CSS of its own. The animation
|
|
69
|
+
keyframes, timing, and `prefers-reduced-motion` guard live in the shared utility class
|
|
70
|
+
(`entry-slide-in`/`entry-zoom-reveal`/`entry-exit-blur`), not in this component.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Accessibility
|
|
75
|
+
|
|
76
|
+
- All three animation utility classes are already scoped inside `@media (prefers-reduced-motion:
|
|
77
|
+
no-preference)` at the CSS level — a visitor with reduced motion enabled sees the content with no
|
|
78
|
+
animation, same as `skip-animation`. `skip-animation` is for a layout/UX decision (already
|
|
79
|
+
visible above the fold), not a substitute for the reduced-motion guard.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- **No animation reactivity**: `animationType`/`skipAnimation` are read once at mount to toggle the
|
|
86
|
+
utility class — changing either prop after mount does not currently re-toggle the class. This
|
|
87
|
+
matches the component's existing usage pattern (animation type is always set once per instance,
|
|
88
|
+
never swapped at runtime); flag it if a future consumer needs runtime toggling.
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
| Prop (template form) | Type | Default | Notes |
|
|
14
14
|
| -------------------------- | ---------------------- | ---------------- | ------------------------------------------------------------ |
|
|
15
15
|
| `:value` | `string` | (required) | The text to copy to clipboard (e.g., license key, API token).|
|
|
16
|
+
| `:id` | `string` | auto-generated | Id applied to the input. Only pass one if something needs to target this input directly. |
|
|
17
|
+
| `:name` | `string` | falls back to `id` | Name applied to the input. Falls back to the resolved `id` (auto-generated or explicit) when not provided. |
|
|
16
18
|
| `:label` | `string` | (optional) | Short label for the field; used in aria-label if ariaLabel not set. |
|
|
17
19
|
| `:aria-label` | `string` | (optional) | Custom aria-label for the input; overrides label. |
|
|
18
20
|
| `:description` | `string` | (optional) | Helper text displayed below the input. |
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# InputTextCore Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`InputTextCore` is the low-level native `<input>` primitive for the `05.forms` text-input family.
|
|
6
|
+
It renders a single styled text-like input with left/right icon slots, focus/dirty state models,
|
|
7
|
+
and numeric-pattern validation for `inputmode="numeric"`. It has no label, description, or error
|
|
8
|
+
message rendering of its own — those are composed by the wrapper variants below.
|
|
9
|
+
|
|
10
|
+
Most consumers should reach for one of the **Variants** rather than `InputTextCore` directly.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Props reference
|
|
15
|
+
|
|
16
|
+
> **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:input-variant`, `:aria-describedby`.
|
|
17
|
+
|
|
18
|
+
| Prop (template form) | Type | Default | Notes |
|
|
19
|
+
| --------------------------- | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------- |
|
|
20
|
+
| `:type` | `"text" \| "email" \| "password" \| "number" \| "tel" \| "url" \| "date"` | `"text"` | Native input type. |
|
|
21
|
+
| `:inputmode` | `"text" \| "email" \| "tel" \| "url" \| "search" \| "numeric" \| "none" \| "decimal"` | `"text"` | Sets virtual keyboard hint; `"numeric"` also enables pattern validation. |
|
|
22
|
+
| `:maxlength` | `number` | `255` | Native `maxlength`. |
|
|
23
|
+
| `:min` | `string \| number` | (optional) | Passed straight through to the native input — e.g. earliest allowed date for `type="date"`, or lowest number for `type="number"`. Use an ISO date string (`"2026-01-01"`) for `type="date"`. |
|
|
24
|
+
| `:max` | `string \| number` | (optional) | Same as `min`, for the upper bound. |
|
|
25
|
+
| `:id` | `string` | (required) | Applied to the native input. |
|
|
26
|
+
| `:name` | `string` | (required) | Applied to the native input. |
|
|
27
|
+
| `:required` | `boolean` | `false` | Native `required`. |
|
|
28
|
+
| `:placeholder` | `string` | `""` | Native placeholder. |
|
|
29
|
+
| `:field-has-error` | `boolean` | `false` | Drives `.error` class, `aria-invalid`, and forces `theme` to `"error"`. |
|
|
30
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the wrapper. |
|
|
31
|
+
| `:theme` | `FormUiTheme` | `"default"` | Overridden to `"error"` internally when `fieldHasError` is true. |
|
|
32
|
+
| `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
|
|
33
|
+
| `:input-variant` | `"normal" \| "outlined" \| "underlined"` | `"normal"` | Visual style — border box vs. underline. |
|
|
34
|
+
|
|
35
|
+
### v-models
|
|
36
|
+
|
|
37
|
+
| Model | Type | Notes |
|
|
38
|
+
| ----------------- | --------- | ---------------------------------------------------------- |
|
|
39
|
+
| `v-model` | `string` | The input value. |
|
|
40
|
+
| `v-model:is-dirty` | `boolean` | Set to `true` once the wrapper variant detects a non-empty value at mount or on change. |
|
|
41
|
+
| `v-model:is-active`| `boolean` | Tracks focus state (`focusin`/`focusout`). |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Slots
|
|
46
|
+
|
|
47
|
+
| Slot | Notes |
|
|
48
|
+
| ------- | ---------------------------------------------------------------------- |
|
|
49
|
+
| `left` | Rendered before the input, inside the bordered wrapper (e.g. an icon or `InputButtonCore variant="inline"`). |
|
|
50
|
+
| `right` | Rendered after the input, inside the bordered wrapper. |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## CSS Token Customization
|
|
55
|
+
|
|
56
|
+
All `--input-text-*` tokens can be overridden at global, page, or instance scope, each falling back
|
|
57
|
+
to the shared `--theme-*`/`--theme-input-*` tokens every other themed form component also uses. See
|
|
58
|
+
`CONSUMER-STYLING.md` in the component directory for the full token table.
|
|
59
|
+
|
|
60
|
+
**Common tokens:**
|
|
61
|
+
- `--input-text-surface` — wrapper background
|
|
62
|
+
- `--input-text-border` / `--input-text-border-hover` / `--input-text-border-focus` — border/outline colour
|
|
63
|
+
- `--input-text-color` — input text colour
|
|
64
|
+
- `--input-text-placeholder-color` — placeholder colour
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Using `min`/`max` for a native date picker
|
|
69
|
+
|
|
70
|
+
`type="date"` plus `min`/`max` is the standard way to stop the browser's native date picker from
|
|
71
|
+
offering dates outside an allowed range — e.g. preventing a booking date in the past:
|
|
72
|
+
|
|
73
|
+
```vue
|
|
74
|
+
<InputTextCore
|
|
75
|
+
id="appointment-date"
|
|
76
|
+
name="appointmentDate"
|
|
77
|
+
type="date"
|
|
78
|
+
v-model="appointmentDate"
|
|
79
|
+
:min="new Date().toISOString().split('T')[0]"
|
|
80
|
+
max="2027-12-31"
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`min`/`max` are passed straight through with no validation of their own — the browser enforces the
|
|
85
|
+
range in its native picker/typed-entry UI. Pair with server-side validation; a native `min`/`max`
|
|
86
|
+
constraint is a UX affordance, not a substitute for validating the submitted value.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Variants
|
|
91
|
+
|
|
92
|
+
The `variants/` folder composes `InputTextCore` with `InputLabel`, `InputDescription`, and
|
|
93
|
+
`InputError` for the common "labelled field" case. Each forwards `min`/`max` straight through if
|
|
94
|
+
you pass them.
|
|
95
|
+
|
|
96
|
+
### InputTextWithLabel
|
|
97
|
+
|
|
98
|
+
The general-purpose labelled text field — supports every `InputTypesText` value including
|
|
99
|
+
`"date"`.
|
|
100
|
+
|
|
101
|
+
```vue
|
|
102
|
+
<InputTextWithLabel
|
|
103
|
+
v-model="appointmentDate"
|
|
104
|
+
id="appointment-date"
|
|
105
|
+
name="appointmentDate"
|
|
106
|
+
type="date"
|
|
107
|
+
label="Appointment date"
|
|
108
|
+
:min="new Date().toISOString().split('T')[0]"
|
|
109
|
+
max="2027-12-31"
|
|
110
|
+
:error-message="errors.appointmentDate"
|
|
111
|
+
:field-has-error="!!errors.appointmentDate"
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Props: same `type`/`inputmode`/`maxlength`/`min`/`max`/`name`/`placeholder`/`label`/`errorMessage`/
|
|
116
|
+
`fieldHasError`/`required`/`styleClassPassthrough`/`theme`/`inputVariant` as `InputTextCore`, plus
|
|
117
|
+
`label: string` (required) and `errorMessage: object | string` (required — pass `""` if unused).
|
|
118
|
+
Slots: `left`, `right`, `descriptionHtml`, `descriptionText`.
|
|
119
|
+
|
|
120
|
+
### InputPasswordWithLabel
|
|
121
|
+
|
|
122
|
+
Adds a show/hide toggle button (`InputButtonCore variant="inline"` in the `right` slot) that swaps
|
|
123
|
+
`type` between `"password"` and `"text"`.
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<InputPasswordWithLabel
|
|
127
|
+
v-model="password"
|
|
128
|
+
name="password"
|
|
129
|
+
label="Password"
|
|
130
|
+
:error-message="errors.password"
|
|
131
|
+
:field-has-error="!!errors.password"
|
|
132
|
+
show-password-text="Show password"
|
|
133
|
+
hide-password-text="Hide password"
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Extra props: `showPasswordText` (default `"Show password"`) and `hidePasswordText` (default
|
|
138
|
+
`"Hide password"`) — the toggle button's accessible name in each state. Pass translated strings
|
|
139
|
+
here for a non-English consumer app; there is no built-in i18n framework dependency.
|
|
140
|
+
|
|
141
|
+
### InputTextAsNumberWithLabel
|
|
142
|
+
|
|
143
|
+
A numeric stepper: left/right `InputButtonCore variant="inline"` buttons decrement/increment the
|
|
144
|
+
value by `step`, disabled once `min`/`max` is reached.
|
|
145
|
+
|
|
146
|
+
```vue
|
|
147
|
+
<InputTextAsNumberWithLabel
|
|
148
|
+
v-model="quantity"
|
|
149
|
+
name="quantity"
|
|
150
|
+
label="Quantity"
|
|
151
|
+
:min="1"
|
|
152
|
+
:max="99"
|
|
153
|
+
:step="1"
|
|
154
|
+
step-down-text="Decrease quantity"
|
|
155
|
+
step-up-text="Increase quantity"
|
|
156
|
+
>
|
|
157
|
+
<template #left>−</template>
|
|
158
|
+
<template #right>+</template>
|
|
159
|
+
</InputTextAsNumberWithLabel>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Extra props: `min: number` (required), `max: number` (required), `step?: number` (default `1`),
|
|
163
|
+
`stepDownText`/`stepUpText` (default `"Step down"`/`"Step up"`) — the decrement/increment buttons'
|
|
164
|
+
accessible names. Requires `left`/`right` slot content for the step icons (no default icon).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Accessibility
|
|
169
|
+
|
|
170
|
+
- `field-has-error` sets `aria-invalid="true"` and forces the error theme, independent of what
|
|
171
|
+
`theme` was passed.
|
|
172
|
+
- `inputmode="numeric"` adds a `pattern="[0-9]+"` and blocks non-numeric `beforeinput` edits that
|
|
173
|
+
would fail it.
|
|
174
|
+
- Focus is tracked via native `focusin`/`focusout`, not a custom keyboard handler — `:focus-visible`
|
|
175
|
+
styling is preserved.
|
|
176
|
+
- The password/number variants give their icon-only toggle/step buttons a real accessible name via
|
|
177
|
+
`InputButtonCore`'s `buttonText` (visually hidden via `.sr-only` when an icon-only slot is used).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
- **No built-in label/error rendering**: `InputTextCore` alone renders no `<label>` or error text —
|
|
184
|
+
use a variant, or compose your own wrapper the same way they do.
|
|
185
|
+
- **`min`/`max` need matching `type`**: they're inert on `type="text"`/`"email"`/etc. — only
|
|
186
|
+
`"date"` and `"number"` (native range-constrained types) act on them.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: MarqueeScroller
|
|
3
|
+
description: MarqueeScroller infinite horizontal logo/badge scroller — props, dynamic per-item slots, accessibility controls, CSS tokens, consumer styling
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MarqueeScroller
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`MarqueeScroller` renders an infinite horizontal scroller for small repeating items — client
|
|
12
|
+
logos, badges, testimonial avatars. The track is duplicated once so the loop is seamless
|
|
13
|
+
(`marquee-group` renders twice: once real, once `aria-hidden="true"`), and the whole thing pauses
|
|
14
|
+
automatically on hover, on keyboard focus, and when the user has `prefers-reduced-motion` set.
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|------|------|---------|-------------|
|
|
20
|
+
| `animationRuntime` | `string` | `"40s"` | CSS `animation-duration` for one full loop of the track. |
|
|
21
|
+
| `reverse` | `boolean` | `false` | Reverses the scroll direction. |
|
|
22
|
+
| `marqueeData` | `MarqueeItem[]` (`{ id: number; content: string }[]`) | `[]` | The items to render. Each item's `id` names the slot that renders its markup. `content` is not rendered by the component itself — it's a convenience field for the consumer to key off inside the slot. |
|
|
23
|
+
| `itemConfig` | `MarqueeItemConfig` (`{ width?, height?, gap? }`) | `{ width: "50px", height: "50px", gap: "16px" }` | Item box size and the gap between items. Also sets the track height. |
|
|
24
|
+
| `ariaLabel` | `string` | `"Scrolling content"` | `aria-label` on the root region — override for localisation. |
|
|
25
|
+
| `ariaDescription` | `string` | `"Use spacebar to pause or play the animation."` | Screen-reader-only instructions rendered inside the region — override for localisation. |
|
|
26
|
+
| `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`, and pauses on keyboard focus. |
|
|
28
|
+
| `playIcon` | `string` | `"mdi:play"` | Iconify icon name shown on the control button while paused. Ignored if the `toggle-icon` slot is used. |
|
|
29
|
+
| `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 `aria-label` while paused — override for localisation. |
|
|
31
|
+
| `pauseLabel` | `string` | `"Pause animation"` | Control button `aria-label` while playing — override for localisation. |
|
|
32
|
+
|
|
33
|
+
Import the item types from the library's type exports when building `marqueeData`/`itemConfig`
|
|
34
|
+
outside the component:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import type { MarqueeItem, MarqueeItemConfig } from "srcdev-nuxt-components";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Slots
|
|
41
|
+
|
|
42
|
+
One dynamically-named slot per `marqueeData` entry, named after that item's `id`:
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<MarqueeScroller :marquee-data="[{ id: 1, content: 'logo-a' }, { id: 2, content: 'logo-b' }]">
|
|
46
|
+
<template #1>
|
|
47
|
+
<img src="/logos/a.svg" alt="Logo A" />
|
|
48
|
+
</template>
|
|
49
|
+
<template #2>
|
|
50
|
+
<img src="/logos/b.svg" alt="Logo B" />
|
|
51
|
+
</template>
|
|
52
|
+
</MarqueeScroller>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Each slot is rendered twice (once in the visible group, once in the `aria-hidden` duplicate group)
|
|
56
|
+
so the loop wraps seamlessly — don't put anything with side effects or unique ids inside a slot.
|
|
57
|
+
|
|
58
|
+
`toggle-icon` (scoped with `{ isPaused: boolean }`) replaces the control button's icon entirely —
|
|
59
|
+
use this instead of `playIcon`/`pauseIcon` when an Iconify name isn't enough (custom SVG, a
|
|
60
|
+
different icon set):
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<MarqueeScroller show-controls>
|
|
64
|
+
<template #toggle-icon="{ isPaused }">
|
|
65
|
+
<Icon :name="isPaused ? 'lucide:play' : 'lucide:pause'" />
|
|
66
|
+
</template>
|
|
67
|
+
</MarqueeScroller>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Accessibility behaviour
|
|
71
|
+
|
|
72
|
+
- The root is `role="region"` with `tabindex="0"` so it's independently focusable.
|
|
73
|
+
- Spacebar toggles pause/play while the region is focused. Arrow keys do nothing — there is no
|
|
74
|
+
manual-stepping behaviour, so the default `ariaDescription` doesn't claim one (fixed 2026-09-08;
|
|
75
|
+
it previously announced "arrow keys ... for manual control" with no implementation behind it).
|
|
76
|
+
- Focusing the region pauses the animation when `respectReducedMotion` is `true` (the default);
|
|
77
|
+
blurring resumes it unless the user prefers reduced motion.
|
|
78
|
+
- Hovering anywhere over the scroller pauses the track and applies
|
|
79
|
+
`--marquee-scroller-group-hover-filter` (default `grayscale(1)`) to every item; hovering an
|
|
80
|
+
individual item restores it via `--marquee-scroller-item-hover-filter` (default `grayscale(0)`).
|
|
81
|
+
- `aria-live` switches to `"polite"` while paused so screen readers announce the paused content.
|
|
82
|
+
- The duplicate track group is always `aria-hidden="true"`.
|
|
83
|
+
- All user-visible copy (`ariaLabel`, `ariaDescription`, `playLabel`, `pauseLabel`) is a plain
|
|
84
|
+
string prop with an English default — there's no i18n framework dependency in this library, so
|
|
85
|
+
pass translated strings from the consumer's own i18n solution.
|
|
86
|
+
|
|
87
|
+
## Basic usage
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<MarqueeScroller
|
|
91
|
+
:marquee-data="[
|
|
92
|
+
{ id: 1, content: 'acme' },
|
|
93
|
+
{ id: 2, content: 'globex' },
|
|
94
|
+
]"
|
|
95
|
+
:item-config="{ width: '120px', height: '60px', gap: '24px' }"
|
|
96
|
+
show-controls
|
|
97
|
+
>
|
|
98
|
+
<template #1><img src="/logos/acme.svg" alt="Acme" /></template>
|
|
99
|
+
<template #2><img src="/logos/globex.svg" alt="Globex" /></template>
|
|
100
|
+
</MarqueeScroller>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## CSS custom properties
|
|
104
|
+
|
|
105
|
+
See `CONSUMER-STYLING.md` in this component's folder for the full public token table. Item size,
|
|
106
|
+
gap, and animation duration are prop-driven (`itemConfig`, `animationRuntime`), not tokens, since
|
|
107
|
+
they also drive the component's own layout math. The `prefers-contrast: high` override's
|
|
108
|
+
`ButtonFace`/`ButtonText` colours are deliberately not tokens (they track the OS high-contrast
|
|
109
|
+
theme); only its border width is (`--marquee-scroller-control-border-width`).
|
|
110
|
+
|
|
111
|
+
## Seamless loop math
|
|
112
|
+
|
|
113
|
+
The track duplicates `marqueeData` into two `.marquee-group` children so the loop can wrap without
|
|
114
|
+
a visible cut. The animation must shift the track by exactly one group's width plus its connecting
|
|
115
|
+
gap — not `translateX(-50%)`, which overshoots short of the seam once the track's own `gap`
|
|
116
|
+
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 `marqueeMove`
|
|
118
|
+
keyframe. Fixed 2026-09-08 — the original `-50%` version produced a small but visible gap/overlap
|
|
119
|
+
at the loop point once more than a couple of items were in `marqueeData`.
|
|
120
|
+
|
|
121
|
+
## Auto-repeating marqueeData in a wide container
|
|
122
|
+
|
|
123
|
+
If a single pass of `marqueeData` is narrower than the container (few items, or a full-bleed
|
|
124
|
+
section on a wide viewport), the loop briefly shows empty space before snapping back into view —
|
|
125
|
+
the visible track only spans as wide as the content, not the container. On mount (and on
|
|
126
|
+
container resize, via `ResizeObserver`), the component measures one rendered group's `scrollWidth`
|
|
127
|
+
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
|
|
129
|
+
transparent to consumers — it doesn't change `marqueeData` or any prop, only how many times each
|
|
130
|
+
item's slot is rendered internally. Fixed 2026-09-08.
|