srcdev-nuxt-components 9.4.9 → 9.4.11
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 +38 -6
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +26 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/alert-mask-core.md +84 -0
- package/.claude/skills/components/canvas-switcher.md +1 -1
- package/.claude/skills/components/carousel-flip.md +112 -168
- package/.claude/skills/components/container-glow.md +123 -0
- package/.claude/skills/components/dashboard-quad-grid.md +98 -0
- package/.claude/skills/components/dashboard-stats-grid.md +103 -0
- package/.claude/skills/components/display-theme-switch.md +60 -0
- package/.claude/skills/components/glowing-border.md +66 -0
- package/.claude/skills/components/input-number-core.md +214 -0
- package/.claude/skills/components/input-range-core.md +142 -0
- package/.claude/skills/components/input-select-core.md +151 -0
- package/.claude/skills/components/input-text-core.md +16 -0
- package/.claude/skills/components/input-textarea-core.md +154 -0
- package/.claude/skills/components/section-parallax.md +32 -10
- package/.claude/skills/components/select-menu.md +195 -0
- package/.claude/skills/components/toggle-switch-core.md +150 -0
- package/.claude/skills/components/ui-block-decorated.md +56 -0
- package/.claude/skills/components/wipe-away-vertical.md +98 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +15 -1
- package/.vscode/css-custom-data.json +38 -0
- package/.vscode/settings.json +3 -0
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
- package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
- package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
- package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
- package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
- package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
- package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
- package/.vscode/srcdev-component-input-number.code-snippets +65 -0
- package/.vscode/srcdev-component-input-range.code-snippets +85 -0
- package/.vscode/srcdev-component-input-select.code-snippets +42 -0
- package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
- package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
- package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
- package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
- package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
- package/README.md +18 -37
- package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
- package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
- package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
- package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
- package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
- package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
- package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
- package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
- package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +3 -3
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
- package/app/components/01.atoms/card/CardCore.vue +4 -4
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
- package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
- package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
- package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
- package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
- package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
- package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
- package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
- package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +114 -29
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
- package/app/components/05.forms/form-field/FormField.vue +1 -1
- package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
- package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
- package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
- package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
- package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
- package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
- package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
- package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
- package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
- package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
- package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
- package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
- package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
- package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
- package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
- package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
- package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
- package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +220 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +20 -6
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +12 -10
- package/app/types/components/container-glow.d.ts +8 -0
- package/app/types/components/index.ts +2 -1
- package/app/types/components/select-menu.d.ts +5 -0
- package/package.json +3 -2
- package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
- package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
- package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
- package/app/components/carousels/CarouselBasic.vue +0 -317
- package/app/components/carousels/CarouselInfinite.vue +0 -358
- package/app/components/container-glow/ContainerGlowCore.vue +0 -296
- package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
- package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
- package/app/components/glowing-border/GlowingBorder.vue +0 -142
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
- package/app/components/layout-grids/LayoutGridB.vue +0 -134
- package/app/components/test-storybook/TestStorybook.vue +0 -49
- package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
- package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
- package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
- package/app/types/components/carousel-basic.d.ts +0 -19
|
@@ -390,6 +390,7 @@ footer.note code{
|
|
|
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
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>
|
|
393
|
+
<th data-key="eslint_issues" data-type="bool" title="npx eslint reports at least one error or warning on a .vue file in this group">ESLint issues</th>
|
|
393
394
|
<th data-key="score" data-type="num">Score</th>
|
|
394
395
|
</tr>
|
|
395
396
|
</thead>
|
|
@@ -400,6 +401,7 @@ footer.note code{
|
|
|
400
401
|
<footer class="note">
|
|
401
402
|
<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
403
|
<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>
|
|
404
|
+
<p style="margin-top:0.8rem;"><strong>"ESLint issues"</strong> runs <code>npx eslint</code> across every <code>.vue</code> file under <code>app/components/</code> once per build and flags a group if any of its files reported an error or a warning. Added 2026-09-21 after fixing a dead <code>const props = withDefaults(...)</code> in <code>InputRangeCore</code> (the assignment's value was never read once its only reference — a since-removed dead computed — was cleaned up) and finding the same pattern live in <code>FormField</code>/<code>FormWrapper</code>. It also currently flags 13 groups for <code>vue/require-default-prop</code> firing on <code>defineModel()</code>-declared props, an eslint-plugin-vue rule with no options to exempt models (it predates <code>defineModel</code>) — the fix there is <code>defineModel<T>({'{'} required: true {'}'})</code> where the native element genuinely can't be meaningfully "empty" (matching what <code>InputRangeCore</code>/<code>InputRangeDefault</code> now do), not a blanket rule disable or an artificial default value. Not counted in the 5-point score, same reasoning as "Legacy props"/"Story args bug".</p>
|
|
403
405
|
<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>
|
|
404
406
|
<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>
|
|
405
407
|
</footer>
|
|
@@ -439,11 +441,12 @@ statDefs.forEach(sd => {
|
|
|
439
441
|
});
|
|
440
442
|
|
|
441
443
|
// ---- dynamic narrative (every number computed from DATA, nothing hand-written) ----
|
|
442
|
-
const fullyCompliant = DATA.filter(d => d.score === 5 && d.tier !== "NONE" && !d.variants && !d.legacy_props && !d.story_args_bug);
|
|
444
|
+
const fullyCompliant = DATA.filter(d => d.score === 5 && d.tier !== "NONE" && !d.variants && !d.legacy_props && !d.story_args_bug && !d.eslint_issues);
|
|
443
445
|
const unplaced = DATA.filter(d => d.tier === "NONE");
|
|
444
446
|
const forkedVariants = DATA.filter(d => d.variants);
|
|
445
447
|
const legacyProps = DATA.filter(d => d.legacy_props);
|
|
446
448
|
const storyArgsBug = DATA.filter(d => d.story_args_bug);
|
|
449
|
+
const eslintIssues = DATA.filter(d => d.eslint_issues);
|
|
447
450
|
const noSkillAndUnplaced = unplaced.filter(d => !d.skill).length;
|
|
448
451
|
const topPrivate = DATA.filter(d => d.priv_tokens > 0).sort((a,b) => b.priv_tokens - a.priv_tokens).slice(0, 3);
|
|
449
452
|
|
|
@@ -455,6 +458,7 @@ narrativeEl.innerHTML = `
|
|
|
455
458
|
<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>
|
|
456
459
|
<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
460
|
<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>
|
|
461
|
+
<p>${eslintIssues.length ? `<strong>${eslintIssues.length} group${eslintIssues.length===1?"":"s"}</strong> ha${eslintIssues.length===1?"s":"ve"} at least one live <code class="mono">npx eslint</code> finding (${eslintIssues.map(d => `<code class="mono">${d.compdir.split("/").pop()}</code>`).join(", ")}) — a component can score 5/5 on docs/tests/story/skill/snippet and still carry these. Not counted in the 5-point score, same reasoning as "Legacy props"/"Story args bug".` : `No component groups currently have a live ESLint finding.`}</p>
|
|
458
462
|
<p><strong>${topPrivate.length ? "On --_ vars:" : ""}</strong> ${topPrivate.length
|
|
459
463
|
? `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.`
|
|
460
464
|
: ""}</p>
|
|
@@ -521,7 +525,7 @@ function tierLabel(t){ return t === "NONE" ? "unplaced" : t; }
|
|
|
521
525
|
function render(){
|
|
522
526
|
let rows = DATA.slice();
|
|
523
527
|
if (activeTier !== "all") rows = rows.filter(d => d.tier === activeTier);
|
|
524
|
-
if (onlyIncomplete) rows = rows.filter(d => d.score < 5 || d.tier === "NONE" || d.variants || d.legacy_props || d.story_args_bug);
|
|
528
|
+
if (onlyIncomplete) rows = rows.filter(d => d.score < 5 || d.tier === "NONE" || d.variants || d.legacy_props || d.story_args_bug || d.eslint_issues);
|
|
525
529
|
const q = searchEl.value.trim().toLowerCase();
|
|
526
530
|
if (q) rows = rows.filter(d => d.compdir.toLowerCase().includes(q));
|
|
527
531
|
|
|
@@ -551,6 +555,7 @@ function render(){
|
|
|
551
555
|
<td class="check">${d.variants ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
552
556
|
<td class="check">${d.legacy_props ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
553
557
|
<td class="check">${d.story_args_bug ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
558
|
+
<td class="check">${d.eslint_issues ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
|
|
554
559
|
<td><span class="score-bar">${dots}</span></td>
|
|
555
560
|
</tr>`;
|
|
556
561
|
}).join("");
|
|
@@ -134,7 +134,7 @@ Some components pair multiple slot types per index (e.g. TabsCore):
|
|
|
134
134
|
- `app/components/01.atoms/navigation/tabs/TabsCore.vue`
|
|
135
135
|
- `app/components/accordian/AccordianCore.vue`
|
|
136
136
|
- `app/components/02.molecules/stepper-list/StepperList.vue`
|
|
137
|
-
- `app/components/view-timeline/WipeAwayVertical.vue`
|
|
137
|
+
- `app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue`
|
|
138
138
|
|
|
139
139
|
---
|
|
140
140
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AlertMaskCore
|
|
3
|
+
description: AlertMaskCore SVG border/background mask sized to slotted content; config-prop-driven geometry/colour, no CSS token API
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# AlertMaskCore
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`AlertMaskCore` draws a border + background around its default slot content using an SVG `<mask>`,
|
|
12
|
+
sized to the content's measured dimensions via `ResizeObserver`. It's the low-level primitive
|
|
13
|
+
`AlertMaskedContent` (`app/components/02.molecules/alert-masked-content/`) builds on for its
|
|
14
|
+
accent-bordered alert shape.
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|------|------|---------|-------------|
|
|
20
|
+
| `config` | `AlertMaskConfig` | `{}` | Border/background colour and geometry (see below). All fields optional. |
|
|
21
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
22
|
+
|
|
23
|
+
### `AlertMaskConfig` (`app/types/components/alert-mask-core.d.ts`)
|
|
24
|
+
|
|
25
|
+
| Field | Type | Default | Description |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `backgroundColour` | `string` | `"rgba(0,0,0,0.25)"` | Inner fill colour. |
|
|
28
|
+
| `borderColour` | `string` | `"var(--orange-08)"` | Border fill colour. |
|
|
29
|
+
| `radiusLeft` | `number` | `12` | Left-side corner radius (px). |
|
|
30
|
+
| `radiusRight` | `number` | `12` | Right-side corner radius (px). |
|
|
31
|
+
| `borderLeft` | `number` | `8` | Left border thickness (px). |
|
|
32
|
+
| `borderTop` | `number` | `8` | Top border thickness (px). |
|
|
33
|
+
| `borderRight` | `number` | `8` | Right border thickness (px). |
|
|
34
|
+
| `borderBottom` | `number` | `8` | Bottom border thickness (px). |
|
|
35
|
+
|
|
36
|
+
## Basic usage
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<AlertMaskCore>
|
|
40
|
+
<p>Alert content</p>
|
|
41
|
+
</AlertMaskCore>
|
|
42
|
+
|
|
43
|
+
<AlertMaskCore
|
|
44
|
+
:config="{
|
|
45
|
+
borderColour: 'var(--theme-accent)',
|
|
46
|
+
backgroundColour: 'rgba(0, 0, 0, 0.3)',
|
|
47
|
+
radiusLeft: 8,
|
|
48
|
+
radiusRight: 4,
|
|
49
|
+
borderLeft: 6,
|
|
50
|
+
borderTop: 1,
|
|
51
|
+
borderRight: 1,
|
|
52
|
+
borderBottom: 1,
|
|
53
|
+
}"
|
|
54
|
+
>
|
|
55
|
+
<p>Accent-bordered alert content</p>
|
|
56
|
+
</AlertMaskCore>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
- The default-slot content is measured via `ResizeObserver` (initial measurement runs after
|
|
62
|
+
`nextTick` on mount).
|
|
63
|
+
- An SVG outer path (rounded rect using `radiusLeft`/`radiusRight`) and inner path (the same rect
|
|
64
|
+
inset by the border thicknesses) are computed from that measurement, and the region between them
|
|
65
|
+
is masked to `borderColour`; the inner region is filled with `backgroundColour`.
|
|
66
|
+
- The slot content itself sits in a separate `.alert-mask-content` div, inset by the same border
|
|
67
|
+
thicknesses via internal `--_inset-*` custom properties, so it visually sits inside the drawn
|
|
68
|
+
border without overlapping it.
|
|
69
|
+
|
|
70
|
+
## No CSS token API
|
|
71
|
+
|
|
72
|
+
Unlike most components in this library, `AlertMaskCore` has no public `--alert-mask-core-*` CSS
|
|
73
|
+
custom properties — the SVG path geometry can't be driven by `var()`, so `config` is the entire
|
|
74
|
+
override surface (colour included). See CONSUMER-STYLING.md.
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- 2026-09-20 migration: moved from `app/components/alert-mask/` (unplaced) into
|
|
79
|
+
`01.atoms/alert-mask/`; converted from options-style `defineProps({...})` to
|
|
80
|
+
`interface Props` + `withDefaults`; renamed the internal `--alertHeight`/`--insetInlineStart`/
|
|
81
|
+
`--insetInlineEnd`/`--insetBlockStart`/`--insetBlockEnd` custom properties to private
|
|
82
|
+
`--_height`/`--_inset-inline-start`/`--_inset-inline-end`/`--_inset-block-start`/
|
|
83
|
+
`--_inset-block-end` (they're JS-computed plumbing, not a consumer override surface — `config`
|
|
84
|
+
already covers that). No behaviour change.
|
|
@@ -47,7 +47,7 @@ const canvasName = ref<MediaCanvas>("desktopCanvas");
|
|
|
47
47
|
The selected value doubles as a utility class name: `CanvasSwitcher`'s stylesheet also declares
|
|
48
48
|
`.mobileCanvas`/`.tabletCanvas`/`.laptopCanvas`/`.desktopCanvas`/`.fullWidthCanvas` (max-width
|
|
49
49
|
utilities, not scoped to `.canvas-switcher`), so binding `:class="canvasName"` directly onto a
|
|
50
|
-
preview wrapper constrains it to match — see `
|
|
50
|
+
preview wrapper constrains it to match — see `DashboardQuadGrid`/`DashboardStatsGrid` stories for the pattern.
|
|
51
51
|
These classes only ship in the bundle once `CanvasSwitcher` (or another importer of them) is
|
|
52
52
|
actually imported somewhere, since Nuxt only bundles a component's SFC styles when it's used.
|
|
53
53
|
|
|
@@ -1,188 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: CarouselFlip
|
|
3
|
+
description: CarouselFlip FLIP-animated multi-item carousel — named dynamic slots (per data id), swipe/keyboard/marker navigation, button layout variants, CSS tokens, consumer styling
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
1
7
|
# CarouselFlip
|
|
2
8
|
|
|
3
9
|
## Overview
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
`CarouselFlip` is a multi-item carousel that reorders its items via the FLIP animation technique
|
|
12
|
+
(First-Last-Invert-Play) on navigation, rather than a simple track-translate. Items are named
|
|
13
|
+
dynamic slots keyed by `carouselDataIds`. It supports swipe, arrow-key, marker-click, and
|
|
14
|
+
prev/next-button navigation, and four `buttonLayout` variants for positioning the prev/next
|
|
15
|
+
buttons relative to the marker controls.
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
Has live production usage on instepreflexology — check before making a breaking change to its
|
|
18
|
+
default rendered output.
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## Props
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|------|------|---------|-------------|
|
|
24
|
+
| `carouselDataIds` | `string[]` | `[]` | Slot names, in display order. One item renders per id. |
|
|
25
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
|
|
26
|
+
| `transitionSpeed` | `number` | `200` | Transition duration in ms. |
|
|
27
|
+
| `allowCarouselOverflow` | `boolean` | `false` | Allows items to overflow the item track horizontally instead of clipping. |
|
|
28
|
+
| `useFlipAnimation` | `boolean` | `false` | Changes the initial item offset used in the FLIP reorder calculation. |
|
|
29
|
+
| `useSpringEffect` | `boolean` | `false` | Uses `var(--spring-easing)` as the transition timing function instead of `ease`. |
|
|
30
|
+
| `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay"` | `"sides"` | Where the prev/next buttons sit relative to the carousel/controls. |
|
|
31
|
+
| `showControls` | `boolean` | `true` | Shows/hides the marker (dot) controls bar. Prev/next buttons remain regardless. |
|
|
32
|
+
| `ariaLabel` | `string` | `"Image carousel"` | `aria-label` on the root region — override for localisation. |
|
|
33
|
+
| `itemsAriaLabel` | `string` | `"Carousel items"` | `aria-label` on the item group — override for localisation. |
|
|
34
|
+
| `previousLabel` | `string` | `"Go to previous item"` | Prev button `aria-label` — override for localisation. |
|
|
35
|
+
| `nextLabel` | `string` | `"Go to next item"` | Next button `aria-label` — override for localisation. |
|
|
36
|
+
| `jumpToItemLabel` | `string` | `"Jump to item"` | Marker button `aria-label` prefix — the 1-based index is appended (`"Jump to item 3"`). |
|
|
37
|
+
| `itemPositionAnnouncement` | `(current: number, total: number) => string` | `` (current, total) => `Item ${current} of ${total}` `` | Builds the screen-reader live-region text on navigation — override for localisation or a different phrasing. |
|
|
38
|
+
| `prevIcon` | `string` | `"ic:outline-keyboard-arrow-left"` | Iconify name for the previous button icon. Ignored if the `prev-icon` slot is used. |
|
|
39
|
+
| `nextIcon` | `string` | `"ic:outline-keyboard-arrow-right"` | Iconify name for the next button icon. Ignored if the `next-icon` slot is used. |
|
|
40
|
+
|
|
41
|
+
## Slots
|
|
42
|
+
|
|
43
|
+
One dynamically-named slot per `carouselDataIds` entry, named after that id:
|
|
19
44
|
|
|
20
45
|
```vue
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<CarouselFlip v-if="status === 'success'" :carousel-data-ids="carouselDataIds">
|
|
26
|
-
<template v-for="item in data.items" :key="item.id" #[item.id]>
|
|
27
|
-
<!-- slot content -->
|
|
28
|
-
</template>
|
|
46
|
+
<CarouselFlip :carousel-data-ids="['slide-1', 'slide-2', 'slide-3']">
|
|
47
|
+
<template #slide-1><img src="/a.jpg" alt="A" /></template>
|
|
48
|
+
<template #slide-2><img src="/b.jpg" alt="B" /></template>
|
|
49
|
+
<template #slide-3><img src="/c.jpg" alt="C" /></template>
|
|
29
50
|
</CarouselFlip>
|
|
30
51
|
```
|
|
31
52
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Ask: **"Where should the prev/next buttons sit?"** Show these options:
|
|
35
|
-
|
|
36
|
-
| Value | Visual description |
|
|
37
|
-
|---|---|
|
|
38
|
-
| `"sides"` | Buttons float on the left and right edges of the carousel frame, centred vertically — the classic look |
|
|
39
|
-
| `"controls-flanking"` | Buttons move down into the controls row: prev · markers · next |
|
|
40
|
-
| `"controls-grouped-right"` | Markers stretch left, both buttons grouped together at the far right of the controls row |
|
|
41
|
-
| `"overlay"` | Buttons stay on the sides; the markers bar overlays the bottom edge of the carousel frame |
|
|
42
|
-
|
|
43
|
-
Set via `:button-layout="..."`. Default is `"sides"`.
|
|
44
|
-
|
|
45
|
-
### Step 3 — Show or hide the markers bar
|
|
46
|
-
|
|
47
|
-
Ask: **"Do you want the dot/marker navigation bar visible?"**
|
|
48
|
-
|
|
49
|
-
- **Yes** (default): omit the prop or pass `:show-controls="true"`.
|
|
50
|
-
- **No**: pass `:show-controls="false"`. The element is fully removed from the DOM — no layout space, no keyboard listener.
|
|
51
|
-
|
|
52
|
-
> If `showControls` is false and `buttonLayout` is `"controls-flanking"` or `"controls-grouped-right"`, the controls row collapses entirely — only the buttons remain. Recommend switching `buttonLayout` to `"sides"` or `"overlay"` in that case.
|
|
53
|
-
|
|
54
|
-
### Step 4 — Edge peek (overflow)
|
|
55
|
-
|
|
56
|
-
Ask: **"Should adjacent carousel items peek in from the sides?"**
|
|
57
|
-
|
|
58
|
-
- **No peek** (default): `:allow-carousel-overflow="false"` — clean, contained look.
|
|
59
|
-
- **Peek visible**: `:allow-carousel-overflow="true"` — items bleed slightly out of the container. Requires CSS custom properties to control how much:
|
|
60
|
-
|
|
61
|
-
```css
|
|
62
|
-
.my-carousel.carousel-flip {
|
|
63
|
-
--_carousel-item-track-gap: 16px;
|
|
64
|
-
--_carousel-item-edge-preview-width: 32px; /* keep at 2× track-gap */
|
|
65
|
-
--_carousel-container-max-inline-size: 900px;
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Step 5 — Animation style
|
|
70
|
-
|
|
71
|
-
Ask: **"What animation feel are you going for?"**
|
|
72
|
-
|
|
73
|
-
| Prop | Effect |
|
|
74
|
-
|---|---|
|
|
75
|
-
| `:use-flip-animation="false"` (default) | Slides — items translate horizontally |
|
|
76
|
-
| `:use-flip-animation="true"` | FLIP reorder — items swap position with a physics-aware delta animation |
|
|
77
|
-
| `:use-spring-effect="true"` | Adds spring easing to FLIP transitions. Requires `var(--spring-easing)` in the theme |
|
|
78
|
-
| `:transition-speed="400"` | Duration in ms. Default `200`. Recommended range: `200`–`1200` |
|
|
79
|
-
|
|
80
|
-
### Step 6 — Minimal working implementation
|
|
81
|
-
|
|
82
|
-
Once the above decisions are made, assemble the component:
|
|
53
|
+
`prev-icon`/`next-icon` replace the prev/next button icon entirely — use these instead of
|
|
54
|
+
`prevIcon`/`nextIcon` when an Iconify name isn't enough (custom SVG, a different icon set):
|
|
83
55
|
|
|
84
56
|
```vue
|
|
85
|
-
<CarouselFlip
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:use-flip-animation="true"
|
|
90
|
-
:use-spring-effect="false"
|
|
91
|
-
button-layout="sides"
|
|
92
|
-
:show-controls="true"
|
|
93
|
-
:style-class-passthrough="['my-carousel']"
|
|
94
|
-
>
|
|
95
|
-
<template v-for="item in items" :key="item.id" #[item.id]>
|
|
96
|
-
<div class="my-carousel__item">
|
|
97
|
-
<!-- item content -->
|
|
98
|
-
</div>
|
|
99
|
-
</template>
|
|
57
|
+
<CarouselFlip :carousel-data-ids="ids">
|
|
58
|
+
<template #prev-icon><MyCustomArrowLeft /></template>
|
|
59
|
+
<template #next-icon><MyCustomArrowRight /></template>
|
|
60
|
+
...
|
|
100
61
|
</CarouselFlip>
|
|
101
62
|
```
|
|
102
63
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
| `styleClassPassthrough` | `string \| string[]` | `[]` | Classes applied to the root element |
|
|
154
|
-
|
|
155
|
-
## CSS custom properties
|
|
156
|
-
|
|
157
|
-
| Property | Purpose |
|
|
158
|
-
|---|---|
|
|
159
|
-
| `--_carousel-item-track-gap` | Gap between carousel items (default `10px`) |
|
|
160
|
-
| `--_carousel-container-max-inline-size` | Max width of the visible carousel window |
|
|
161
|
-
| `--_carousel-item-edge-preview-width` | How much of adjacent items to reveal (edge peek). Keep at `2× --_carousel-item-track-gap` |
|
|
162
|
-
| `--_carousel-display-max-width` | Max width of the whole component inc. controls |
|
|
163
|
-
|
|
164
|
-
## HTML structure
|
|
165
|
-
|
|
166
|
-
```text
|
|
167
|
-
section.carousel-flip ← grid root, data-button-layout="..."
|
|
168
|
-
div.item-container ← grid-area: carousel — flex row of items
|
|
169
|
-
div.item[data-id] ← one per carouselDataIds entry
|
|
170
|
-
div.controls-container ← grid-area: controls — markers bar (v-if="showControls")
|
|
171
|
-
div.markers-container
|
|
172
|
-
ul.markers-list
|
|
173
|
-
li.markers-item
|
|
174
|
-
button.btn-marker
|
|
175
|
-
div.buttons-container ← display: contents by default (transparent to grid)
|
|
176
|
-
button.btn-action.btn-prev ← grid-area: prev (row 1, col 1)
|
|
177
|
-
button.btn-action.btn-next ← grid-area: next (row 1, col 3)
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
`buttons-container` uses `display: contents` so `.btn-prev`/`.btn-next` participate directly in the parent grid. In the `controls-grouped-right` variant the component sets `display: flex` on it, making it the grid child instead.
|
|
64
|
+
## Navigation
|
|
65
|
+
|
|
66
|
+
- **Swipe** (touch): left swipe → next, right swipe → previous.
|
|
67
|
+
- **Keyboard**: `ArrowLeft`/`ArrowRight` on either the item track or the controls bar (both are
|
|
68
|
+
`tabindex="0"`).
|
|
69
|
+
- **Marker click**: jumps directly to that item.
|
|
70
|
+
- **Prev/next buttons**: always rendered, regardless of `showControls`.
|
|
71
|
+
- All navigation wraps around at the ends.
|
|
72
|
+
|
|
73
|
+
## `buttonLayout` variants
|
|
74
|
+
|
|
75
|
+
- `"sides"` (default): prev/next buttons flank the item track (CSS grid areas `prev`/`next`).
|
|
76
|
+
- `"controls-flanking"`: prev/next buttons sit in the row below, flanking the marker controls.
|
|
77
|
+
- `"controls-grouped-right"`: markers centred, prev/next buttons grouped to the right of them.
|
|
78
|
+
- `"overlay"`: markers float over the bottom edge of the item track instead of occupying their
|
|
79
|
+
own grid row.
|
|
80
|
+
|
|
81
|
+
## CSS token API
|
|
82
|
+
|
|
83
|
+
See [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md).
|
|
84
|
+
|
|
85
|
+
| Token | Default | Controls |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `--carousel-flip-gap` | `1rem` | Gap between items inside the item track |
|
|
88
|
+
| `--carousel-flip-layout-gap` | `1rem` | Grid gap between the prev/carousel/next/controls areas |
|
|
89
|
+
| `--carousel-flip-display-max-width` | `80rem` | Max width of the item track and the controls bar |
|
|
90
|
+
| `--carousel-flip-item-max-width` | `80rem` | Max width of a single item, before subtracting the edge-preview width |
|
|
91
|
+
| `--carousel-flip-edge-preview-width` | `4rem` | How much of the neighbouring items peek in at each edge |
|
|
92
|
+
| `--carousel-flip-marker-gap` | `1rem` | Gap between marker (dot) buttons |
|
|
93
|
+
| `--carousel-flip-marker-active-colour` | `light-dark(var(--slate-10), var(--slate-00))` | Active marker background colour |
|
|
94
|
+
| `--carousel-flip-button-background-colour` | `light-dark(white, var(--slate-08))` | Prev/next button background colour |
|
|
95
|
+
| `--carousel-flip-button-border-width` | `0.1rem` | Prev/next button border width |
|
|
96
|
+
| `--carousel-flip-button-border-colour` | `light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%))` | Prev/next button border colour |
|
|
97
|
+
| `--carousel-flip-button-border-radius` | `100vw` | Prev/next button corner radius |
|
|
98
|
+
| `--carousel-flip-button-padding` | `0.8rem` | Prev/next button padding |
|
|
99
|
+
| `--carousel-flip-button-icon-size` | `2.4rem` | Prev/next button icon glyph size |
|
|
100
|
+
| `--carousel-flip-buttons-gap` | `1rem` | Gap between prev/next buttons in the `controls-grouped-right` layout |
|
|
101
|
+
| `--carousel-flip-overlay-controls-offset` | `1rem` | Distance of the controls bar from the bottom edge in the `overlay` layout |
|
|
102
|
+
| `--carousel-flip-focus-outline-width` | `0.2rem` | Marker button focus-visible outline width |
|
|
103
|
+
| `--carousel-flip-focus-outline-colour` | `var(--theme-ring)` | Marker button focus-visible outline colour |
|
|
104
|
+
| `--carousel-flip-focus-outline-offset` | `0.2rem` | Marker button focus-visible outline offset |
|
|
105
|
+
|
|
106
|
+
## Accessibility
|
|
107
|
+
|
|
108
|
+
- Reduced motion: both the CSS transform transition and the JS-driven FLIP reorder transition
|
|
109
|
+
are skipped when `prefers-reduced-motion: reduce` is set — items still reorder, just instantly.
|
|
110
|
+
- Marker buttons previously had `outline: 1px solid transparent`, which suppressed the focus ring
|
|
111
|
+
with no replacement. Fixed 2026-09-21 — they now get a visible `:focus-visible` outline via
|
|
112
|
+
`--carousel-flip-focus-outline-*`.
|
|
113
|
+
- All interactive controls have accessible names via the label props above.
|
|
181
114
|
|
|
182
115
|
## Notes
|
|
183
116
|
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
|
|
188
|
-
|
|
117
|
+
- Migrated from `app/components/carousels/CarouselFlip.vue` (unplaced, legacy location) to
|
|
118
|
+
`03.organisms/image-galleries/carousel-flip/` on 2026-09-21, alongside `SliderGallery`.
|
|
119
|
+
- Sibling components `CarouselBasic` and `CarouselInfinite` were deleted in the same migration —
|
|
120
|
+
they had no consumer usage anywhere and were never fully built out (no tests/story/skill for
|
|
121
|
+
`CarouselInfinite`; `CarouselBasic`'s active-marker colour was hardcoded to plain `red`).
|
|
122
|
+
- **Bug fixed 2026-09-21**: `--carousel-flip-display-max-width`/`--carousel-flip-item-max-width`/
|
|
123
|
+
`--carousel-flip-edge-preview-width` were previously referenced as `--_carousel-display-max-width`
|
|
124
|
+
/`--_carousel-container-max-inline-size`/`--_carousel-item-edge-preview-width` — private custom
|
|
125
|
+
properties that were never declared anywhere, with no fallback. Per CSS spec this makes the
|
|
126
|
+
whole declaration invalid at computed-value time, so the item edge-preview offset and the
|
|
127
|
+
width caps were silently never applying in production. Now declared as real public tokens with
|
|
128
|
+
real defaults (`80rem`/`80rem`/`4rem`) — this is a visible change to the live carousel on
|
|
129
|
+
instepreflexology, done deliberately after confirming with the maintainer.
|
|
130
|
+
- Removed dead commented-out code in `itemWidthOffsetStr`'s computed (an unreachable
|
|
131
|
+
`if (props.allowCarouselOverflow)` branch, entirely commented out, that duplicated the live
|
|
132
|
+
branch's logic).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ContainerGlow
|
|
3
|
+
description: ContainerGlow pointer-proximity glow-border cards, named dynamic slots (one card per slot), config-prop-driven layout/interaction, full CSS token API for static visuals
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ContainerGlow
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`ContainerGlow` renders one "glow card" per named default slot the consumer provides — each card
|
|
12
|
+
tracks the pointer and reveals an animated conic-gradient glow border when the pointer is near it.
|
|
13
|
+
It uses the named dynamic slots pattern (`v-for="(_, name) in $slots"`), so there's no
|
|
14
|
+
`itemCount`-style prop; the consumer's slot names are the source of truth.
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|------|------|---------|-------------|
|
|
20
|
+
| `tag` | `"div" \| "li" \| "article" \| "section"` | `"div"` | HTML tag rendered for each card. |
|
|
21
|
+
| `config` | `ContainerGlowConfig` | `{}` | Proximity/spread/blur/gap/direction/inactive-opacity — see below. All fields optional. |
|
|
22
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root wrapper element. |
|
|
23
|
+
|
|
24
|
+
### `ContainerGlowConfig` (`app/types/components/container-glow.d.ts`)
|
|
25
|
+
|
|
26
|
+
| Field | Type | Default | Description |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| `proximity` | `number` | `40` | Pointer distance (px) at which a card starts glowing. |
|
|
29
|
+
| `spread` | `number` | `80` | Angular spread of the glow (degrees). |
|
|
30
|
+
| `blur` | `number` | `20` | Blur applied to the glow layer (px). |
|
|
31
|
+
| `gap` | `number` | `32` | Gap between cards in the wrapper (px). |
|
|
32
|
+
| `vertical` | `boolean` | `false` | Stack cards vertically instead of horizontally. |
|
|
33
|
+
| `inactiveOpacity` | `number` | `0` | Glow opacity when the pointer isn't nearby. |
|
|
34
|
+
|
|
35
|
+
## Slots
|
|
36
|
+
|
|
37
|
+
Named dynamic — every slot the consumer provides renders as one glow card. There is no fixed slot
|
|
38
|
+
name list or count prop.
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<ContainerGlow>
|
|
42
|
+
<template #one><p>Card one</p></template>
|
|
43
|
+
<template #two><p>Card two</p></template>
|
|
44
|
+
</ContainerGlow>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Basic usage
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<ContainerGlow :config="{ blur: 40, vertical: true }">
|
|
51
|
+
<template #pricing><p>Starter plan</p></template>
|
|
52
|
+
<template #featured><p>Pro plan</p></template>
|
|
53
|
+
</ContainerGlow>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
- A single `pointermove` listener on `document.body` (added on mount, removed via `AbortController`
|
|
59
|
+
on unmount) drives every card at once, throttled to one `requestAnimationFrame` per frame.
|
|
60
|
+
- For each card, the listener checks whether the pointer is within `config.proximity` px of the
|
|
61
|
+
card's bounding box. If so, it computes the pointer's angle relative to the card centre and sets
|
|
62
|
+
that on a private `--_start` custom property (registered via `@property` so it participates in
|
|
63
|
+
the conic-gradient mask math); the card's `--_opacity-active` is set to `1` (or
|
|
64
|
+
`config.inactiveOpacity` otherwise).
|
|
65
|
+
- Two pseudo-elements (`::before`/`::after`) and a `.glows` layer combine a thin proximity ring, a
|
|
66
|
+
coloured gradient glow, and a blurred backdrop glow, all masked by conic gradients driven from
|
|
67
|
+
`--_start` and `--_spread`.
|
|
68
|
+
- `config.gap`/`config.blur`/`config.spread`/`config.vertical` are applied once (and on every
|
|
69
|
+
`config` change, via a deep `watch`) as private custom properties on the wrapper, which cards
|
|
70
|
+
inherit.
|
|
71
|
+
|
|
72
|
+
## Styling
|
|
73
|
+
|
|
74
|
+
Card background/padding/sizing/border-radius/highlight-colour/brightness/transition-duration and
|
|
75
|
+
the glow gradient itself are public CSS custom properties — see `CONSUMER-STYLING.md` for the full
|
|
76
|
+
token table. Layout and interaction behaviour (proximity, spread, blur, gap, direction, inactive
|
|
77
|
+
opacity) go through the `config` prop instead, since they're computed in JavaScript per pointer
|
|
78
|
+
event rather than static CSS.
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<ContainerGlow style="--container-glow-border-radius: 4px; --container-glow-brightness: 2;">
|
|
82
|
+
<template #cta><p>Sharper, brighter glow</p></template>
|
|
83
|
+
</ContainerGlow>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Motion
|
|
87
|
+
|
|
88
|
+
The opacity transition is disabled under `prefers-reduced-motion: reduce` (proximity state changes
|
|
89
|
+
apply instantly instead of fading).
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
- 2026-09-20 migration: renamed from `ContainerGlowCore` to `ContainerGlow` — the `Core` suffix in
|
|
94
|
+
this library denotes a primitive with a non-Core wrapper built on top of it (`InputTextCore` →
|
|
95
|
+
`InputTextWithLabel`, `AlertMaskCore` → `AlertMaskedContent`), and this component has no such
|
|
96
|
+
sibling. Checked all consumer repos first; found one real usage in the defunct
|
|
97
|
+
`nuxt-extend-nuxt-forms` demo app, left as a follow-up for that repo rather than edited here.
|
|
98
|
+
- Moved from `app/components/container-glow/` (unplaced) into
|
|
99
|
+
`01.atoms/animations/container-glow/`.
|
|
100
|
+
- The `Config` type was previously an inline, non-exported interface with **all fields required**,
|
|
101
|
+
so passing a partial `:config="{ blur: 40 }"` silently dropped every other field to `undefined`
|
|
102
|
+
(breaking the proximity/spread/gap/direction maths). Fixed: all `ContainerGlowConfig` fields are
|
|
103
|
+
now optional, merged against defaults internally, and the type is exported from
|
|
104
|
+
`app/types/components/container-glow.d.ts` for consumers to import.
|
|
105
|
+
- `config.gap` and `config.vertical` were computed in JS (`--gap`/`--direction` custom properties
|
|
106
|
+
set on the wrapper) but never actually consumed anywhere in the CSS — the wrapper's flex layout
|
|
107
|
+
hardcoded `gap: 3.2rem` with no `flex-direction` at all, so both fields were silently inert.
|
|
108
|
+
Fixed by wiring the wrapper's `gap`/`flex-direction` to the (now privately-prefixed) computed
|
|
109
|
+
values.
|
|
110
|
+
- `@property --start` was nested inside the `.container-glow-wrapper` style rule, which is invalid
|
|
111
|
+
CSS (`@property` must be a top-level or `@layer`-nested rule, not nested inside a normal
|
|
112
|
+
selector) — browsers silently drop invalid nested at-rules, so the custom property was never
|
|
113
|
+
actually type-registered. Moved to a top-level declaration inside `@layer components`, renamed
|
|
114
|
+
`--start` → `--_start` to match the private-token convention.
|
|
115
|
+
- Removed a dead, never-referenced `--alpha: 0` custom property that appeared on two pseudo-element
|
|
116
|
+
rules.
|
|
117
|
+
- Promoted the previously hardcoded background/padding/aspect-ratio/border-radius/min-max-width/
|
|
118
|
+
content-gap/highlight-colour/brightness/transition-duration and gradient colours to public
|
|
119
|
+
`--container-glow-*` tokens; the JS/config-driven proximity/spread/blur/gap/direction/opacity
|
|
120
|
+
custom properties were renamed to `--_start`/`--_opacity-active`/`--_gap`/`--_blur`/`--_spread`/
|
|
121
|
+
`--_direction`/`--_gradient` to match the private-token convention (they're config-driven, not a
|
|
122
|
+
direct CSS override surface).
|
|
123
|
+
- Added a `prefers-reduced-motion: reduce` guard disabling the opacity transition.
|