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.
Files changed (178) hide show
  1. package/.claude/commands/migrate-component.md +38 -6
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +26 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/alert-mask-core.md +84 -0
  8. package/.claude/skills/components/canvas-switcher.md +1 -1
  9. package/.claude/skills/components/carousel-flip.md +112 -168
  10. package/.claude/skills/components/container-glow.md +123 -0
  11. package/.claude/skills/components/dashboard-quad-grid.md +98 -0
  12. package/.claude/skills/components/dashboard-stats-grid.md +103 -0
  13. package/.claude/skills/components/display-theme-switch.md +60 -0
  14. package/.claude/skills/components/glowing-border.md +66 -0
  15. package/.claude/skills/components/input-number-core.md +214 -0
  16. package/.claude/skills/components/input-range-core.md +142 -0
  17. package/.claude/skills/components/input-select-core.md +151 -0
  18. package/.claude/skills/components/input-text-core.md +16 -0
  19. package/.claude/skills/components/input-textarea-core.md +154 -0
  20. package/.claude/skills/components/section-parallax.md +32 -10
  21. package/.claude/skills/components/select-menu.md +195 -0
  22. package/.claude/skills/components/toggle-switch-core.md +150 -0
  23. package/.claude/skills/components/ui-block-decorated.md +56 -0
  24. package/.claude/skills/components/wipe-away-vertical.md +98 -0
  25. package/.claude/skills/icon-sets.md +1 -1
  26. package/.claude/skills/index.md +15 -1
  27. package/.vscode/css-custom-data.json +38 -0
  28. package/.vscode/settings.json +3 -0
  29. package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
  30. package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
  31. package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
  32. package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
  33. package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
  34. package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
  35. package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
  36. package/.vscode/srcdev-component-input-number.code-snippets +65 -0
  37. package/.vscode/srcdev-component-input-range.code-snippets +85 -0
  38. package/.vscode/srcdev-component-input-select.code-snippets +42 -0
  39. package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
  40. package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
  41. package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
  42. package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
  43. package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
  44. package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
  45. package/README.md +18 -37
  46. package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
  47. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
  48. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
  49. package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
  50. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
  51. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
  52. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
  53. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
  54. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
  55. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
  56. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
  57. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
  58. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
  59. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
  60. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
  61. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
  62. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
  63. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
  64. package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
  65. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
  66. package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
  67. package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
  68. package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
  69. package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
  70. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
  71. package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
  72. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +3 -3
  73. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
  74. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
  75. package/app/components/01.atoms/card/CardCore.vue +4 -4
  76. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
  77. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
  78. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
  79. package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
  80. package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
  81. package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
  82. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
  83. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
  84. package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
  85. package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
  86. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
  87. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
  88. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
  89. package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
  90. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
  91. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
  92. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
  93. package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
  94. package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
  95. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
  96. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
  97. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
  98. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
  99. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
  100. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
  101. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
  102. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
  103. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
  104. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
  105. package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
  106. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
  107. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
  108. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
  109. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
  110. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
  111. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +114 -29
  112. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
  113. package/app/components/05.forms/form-field/FormField.vue +1 -1
  114. package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
  115. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
  116. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
  117. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
  118. package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
  119. package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
  120. package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
  121. package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
  122. package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
  123. package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
  124. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
  125. package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
  126. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
  127. package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
  128. package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
  129. package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
  130. package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
  131. package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
  132. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
  133. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
  134. package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
  135. package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
  136. package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
  137. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
  138. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
  139. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
  140. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
  141. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
  142. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
  143. package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
  144. package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
  145. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +220 -0
  146. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
  147. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
  148. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
  149. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
  150. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
  151. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
  152. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
  153. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
  154. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
  155. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
  156. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +20 -6
  157. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +12 -10
  158. package/app/types/components/container-glow.d.ts +8 -0
  159. package/app/types/components/index.ts +2 -1
  160. package/app/types/components/select-menu.d.ts +5 -0
  161. package/package.json +3 -2
  162. package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
  163. package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
  164. package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
  165. package/app/components/carousels/CarouselBasic.vue +0 -317
  166. package/app/components/carousels/CarouselInfinite.vue +0 -358
  167. package/app/components/container-glow/ContainerGlowCore.vue +0 -296
  168. package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
  169. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
  170. package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
  171. package/app/components/glowing-border/GlowingBorder.vue +0 -142
  172. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
  173. package/app/components/layout-grids/LayoutGridB.vue +0 -134
  174. package/app/components/test-storybook/TestStorybook.vue +0 -49
  175. package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
  176. package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
  177. package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
  178. 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&lt;Props&gt;()">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> &mdash; 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&lt;Props&gt;()</code> &mdash; 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 &mdash; it does not re-run <code>setup()</code> &mdash; 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 &mdash; a since-removed dead computed &mdash; 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>) &mdash; the fix there is <code>defineModel&lt;T&gt;({'{'} 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 &mdash; 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) &mdash; 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 &mdash; 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 &mdash; 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&lt;Props&gt;()</code> &mdash; 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(", ")}) &mdash; 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(", ")}) &mdash; 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(", ")} &mdash; 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 `LayoutGridA`/`LayoutGridB` stories for the pattern.
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
- A FLIP-animated carousel that reorders items in the DOM using CSS `order` and animates transitions with the FLIP technique (First, Last, Invert, Play). Supports swipe, keyboard navigation, and marker dots. The prev/next buttons and controls bar can be placed in several layouts via a single prop.
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
- ## Implementation guide
17
+ Has live production usage on instepreflexology — check before making a breaking change to its
18
+ default rendered output.
10
19
 
11
- When a dev asks to implement CarouselFlip, work through the following questions before writing any code. Each answer maps directly to a prop or CSS decision. You do not need to ask all questions at once — use context clues where the answer is obvious.
20
+ ## Props
12
21
 
13
- ### Step 1 — Data source
14
-
15
- Ask: **"Where is the carousel data coming from — a static array, an API, or a Nuxt `useFetch`?"**
16
-
17
- - **Static array**: Define `carouselDataIds` directly as a `const`.
18
- - **API / useFetch**: Derive `carouselDataIds` as a `computed` from the response. Gate the component with `v-if="status === 'success'"` to avoid a flash of empty slots.
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
- <!-- API pattern -->
22
- const { data, status } = await useFetch<MyType>("/api/items");
23
- const carouselDataIds = computed(() => data.value?.items.map(i => i.id) ?? []);
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
- ### Step 2 — Button layout
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
- :carousel-data-ids="carouselDataIds"
87
- :allow-carousel-overflow="true"
88
- :transition-speed="600"
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
- ### Step 7 — Style the component
104
-
105
- Always scope overrides using `styleClassPassthrough` + a page/section wrapper class. Required tokens for a usable carousel:
106
-
107
- ```css
108
- .my-carousel.carousel-flip {
109
- --_carousel-item-track-gap: 16px;
110
- --_carousel-container-max-inline-size: 900px;
111
- --_carousel-item-edge-preview-width: 32px; /* 2× track-gap when overflow is on */
112
-
113
- /* Items */
114
- .item {
115
- border-radius: 1.2rem;
116
- overflow: hidden;
117
- }
118
-
119
- /* Markers */
120
- .btn-marker {
121
- width: 10px;
122
- height: 10px;
123
- border-radius: 100vw;
124
- background: oklch(70% 0 0);
125
-
126
- &.active { background: white; }
127
- }
128
-
129
- /* Prev/next buttons */
130
- .btn-action {
131
- padding: 10px;
132
- background: oklch(0% 0 0 / 0.4);
133
- border: none;
134
- border-radius: 100vw;
135
- color: white;
136
- }
137
- }
138
- ```
139
-
140
- ---
141
-
142
- ## Props reference
143
-
144
- | Prop | Type | Default | Description |
145
- |---|---|---|---|
146
- | `carouselDataIds` | `string[]` | `[]` | Ordered list of unique IDs — each becomes a named slot |
147
- | `transitionSpeed` | `number` | `200` | Animation duration in ms |
148
- | `allowCarouselOverflow` | `boolean` | `false` | Allows peeking items outside the container bounds |
149
- | `useFlipAnimation` | `boolean` | `false` | Enables FLIP reorder animation on prev/next |
150
- | `useSpringEffect` | `boolean` | `false` | Uses spring easing (`var(--spring-easing)`) instead of `ease` |
151
- | `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay"` | `"sides"` | Controls placement of prev/next buttons relative to the carousel frame and controls bar |
152
- | `showControls` | `boolean` | `true` | Show or hide the markers/controls bar. When `false` the element is removed from the DOM; `controlsContainerRef` becomes null and its keyboard listener detaches automatically |
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
- - **Opacity fade-in**: The root starts at `opacity: 0` and gets `.mounted` (opacity 1) after `initialSetup()` completes. This prevents a flash of unstyled layout on mount.
185
- - **z-index**: `.btn-prev`/`.btn-next` have `z-index: 1` to sit above `.item-container` which uses `isolation: isolate` (carousel items are translated and can overlap the button columns).
186
- - **ResizeObserver**: `initialSetup()` re-runs on resize to recalculate item widths. CSS `translate` on `.item` is driven by the measured `itemWidth` via `v-bind`.
187
- - **Spring easing**: `useSpringEffect` switches to `var(--spring-easing)`. Make sure this custom property is defined in your theme or global CSS when enabling it.
188
- - **buttonLayout + showControls combo**: `"controls-flanking"` and `"controls-grouped-right"` place buttons in the controls row. If `showControls` is false that row collapses — use `"sides"` or `"overlay"` instead.
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.