srcdev-nuxt-components 9.4.4 → 9.4.5

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 (84) hide show
  1. package/.claude/commands/migrate-component.md +68 -0
  2. package/.claude/component-ledger/artifact-url.txt +1 -0
  3. package/.claude/component-ledger/audit.json +1 -0
  4. package/.claude/component-ledger/build.mjs +111 -0
  5. package/.claude/component-ledger/output.html +559 -0
  6. package/.claude/component-ledger/template.html +559 -0
  7. package/.claude/hooks/refresh-component-ledger.sh +32 -0
  8. package/.claude/settings.json +10 -0
  9. package/.claude/skills/components/animated-svg-text.md +78 -0
  10. package/.claude/skills/components/canvas-switcher.md +61 -0
  11. package/.claude/skills/components/clip-element.md +67 -0
  12. package/.claude/skills/components/clipped-panel.md +64 -0
  13. package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
  14. package/.claude/skills/components/deep-expanding-menu.md +92 -0
  15. package/.claude/skills/components/display-banner.md +60 -0
  16. package/.claude/skills/components/display-tooltip-defined.md +136 -0
  17. package/.claude/skills/components/display-tooltip.md +84 -0
  18. package/.claude/skills/composable-tooltips-guide.md +10 -0
  19. package/.claude/skills/index.md +7 -1
  20. package/.claude/skills/storybook-add-story.md +30 -0
  21. package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
  22. package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
  23. package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
  24. package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
  25. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
  26. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
  27. package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
  28. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +14 -0
  29. package/.vscode/srcdev-component-display-tooltip.code-snippets +27 -0
  30. package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
  31. package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
  32. package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
  33. package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
  34. package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
  35. package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
  36. package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
  37. package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
  38. package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
  39. package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
  40. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
  41. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
  42. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
  43. package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
  44. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
  45. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
  46. package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
  47. package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
  48. package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
  49. package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
  50. package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
  51. package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
  52. package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
  53. package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
  54. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
  55. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +185 -0
  56. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
  57. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
  58. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
  59. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
  60. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +103 -0
  61. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
  62. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
  63. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
  64. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
  65. package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
  66. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
  67. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
  68. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
  69. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
  70. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
  71. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
  72. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
  73. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
  74. package/app/types/components/display-tooltip-defined.d.ts +10 -0
  75. package/app/types/components/index.ts +1 -0
  76. package/package.json +1 -1
  77. package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
  78. package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
  79. package/app/components/clipped-panels/ClippedPanel.vue +0 -87
  80. package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
  81. package/app/components/display-details/DisplayDetailsCore.vue +0 -122
  82. package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
  83. package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
  84. package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
@@ -0,0 +1,559 @@
1
+ <title>Component Ledger</title>
2
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
3
+ <style>
4
+ :root{
5
+ --paper:#eef0ea;
6
+ --surface:#ffffff;
7
+ --surface-2:#e4e7de;
8
+ --ink:#1b2420;
9
+ --ink-soft:#51594f;
10
+ --ink-faint:#838a7c;
11
+ --line:#cdd2c4;
12
+ --accent:#2f6f5e;
13
+ --accent-soft:#dce9e2;
14
+ --warn:#9c6a1f;
15
+ --warn-soft:#f3e4c8;
16
+ --bad:#a1423a;
17
+ --bad-soft:#f4dcd8;
18
+ --shadow: 0 1px 2px rgba(27,36,32,0.06), 0 8px 24px -12px rgba(27,36,32,0.18);
19
+ }
20
+ @media (prefers-color-scheme: dark){
21
+ :root:not([data-theme="light"]){
22
+ --paper:#12160f;
23
+ --surface:#191f18;
24
+ --surface-2:#222922;
25
+ --ink:#e7ebe0;
26
+ --ink-soft:#aab2a0;
27
+ --ink-faint:#707868;
28
+ --line:#333c30;
29
+ --accent:#67b79d;
30
+ --accent-soft:#1e3630;
31
+ --warn:#d9a54f;
32
+ --warn-soft:#3a2f18;
33
+ --bad:#e0847b;
34
+ --bad-soft:#3d201d;
35
+ --shadow: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px -12px rgba(0,0,0,0.6);
36
+ }
37
+ }
38
+ :root[data-theme="dark"]{
39
+ --paper:#12160f;
40
+ --surface:#191f18;
41
+ --surface-2:#222922;
42
+ --ink:#e7ebe0;
43
+ --ink-soft:#aab2a0;
44
+ --ink-faint:#707868;
45
+ --line:#333c30;
46
+ --accent:#67b79d;
47
+ --accent-soft:#1e3630;
48
+ --warn:#d9a54f;
49
+ --warn-soft:#3a2f18;
50
+ --bad:#e0847b;
51
+ --bad-soft:#3d201d;
52
+ --shadow: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px -12px rgba(0,0,0,0.6);
53
+ }
54
+
55
+ *{box-sizing:border-box;}
56
+ body{
57
+ background:var(--paper);
58
+ color:var(--ink);
59
+ font-family:"IBM Plex Sans", system-ui, sans-serif;
60
+ font-size:1.4rem;
61
+ line-height:1.5;
62
+ }
63
+ .mono{font-family:"IBM Plex Mono","SFMono-Regular",Menlo,monospace;}
64
+ ::selection{background:var(--accent-soft); color:var(--ink);}
65
+
66
+ .wrap{
67
+ max-width:112rem;
68
+ margin:0 auto;
69
+ padding:4.8rem 3.2rem 8rem;
70
+ }
71
+
72
+ /* ---------- header ---------- */
73
+ header.top{
74
+ display:flex;
75
+ justify-content:space-between;
76
+ align-items:flex-end;
77
+ gap:3.2rem;
78
+ border-bottom:0.1rem solid var(--line);
79
+ padding-bottom:2.4rem;
80
+ margin-bottom:3.2rem;
81
+ flex-wrap:wrap;
82
+ }
83
+ .title-block .eyebrow{
84
+ font-family:"IBM Plex Mono", monospace;
85
+ font-size:1.1rem;
86
+ letter-spacing:0.14em;
87
+ text-transform:uppercase;
88
+ color:var(--accent);
89
+ margin:0 0 0.8rem;
90
+ }
91
+ h1{
92
+ font-size:3.4rem;
93
+ font-weight:700;
94
+ margin:0 0 0.6rem;
95
+ text-wrap:balance;
96
+ letter-spacing:-0.01em;
97
+ }
98
+ .title-block p{
99
+ margin:0;
100
+ max-width:56rem;
101
+ color:var(--ink-soft);
102
+ font-size:1.5rem;
103
+ }
104
+ .meta-block{
105
+ text-align:right;
106
+ font-family:"IBM Plex Mono", monospace;
107
+ font-size:1.2rem;
108
+ color:var(--ink-faint);
109
+ white-space:nowrap;
110
+ }
111
+ .meta-block strong{color:var(--ink-soft); font-weight:500;}
112
+
113
+ /* ---------- stat strip ---------- */
114
+ .stats{
115
+ display:grid;
116
+ grid-template-columns:repeat(6, 1fr);
117
+ gap:1.2rem;
118
+ margin-bottom:3.6rem;
119
+ }
120
+ .stat{
121
+ background:var(--surface);
122
+ border:0.1rem solid var(--line);
123
+ border-radius:0.6rem;
124
+ padding:1.6rem 1.6rem 1.4rem;
125
+ box-shadow:var(--shadow);
126
+ }
127
+ .stat .num{
128
+ font-family:"IBM Plex Mono", monospace;
129
+ font-variant-numeric:tabular-nums;
130
+ font-size:2.6rem;
131
+ font-weight:600;
132
+ line-height:1;
133
+ }
134
+ .stat .label{
135
+ margin-top:0.6rem;
136
+ font-size:1.15rem;
137
+ color:var(--ink-soft);
138
+ text-transform:uppercase;
139
+ letter-spacing:0.05em;
140
+ }
141
+ .stat .bar{
142
+ margin-top:1rem;
143
+ height:0.4rem;
144
+ border-radius:99rem;
145
+ background:var(--surface-2);
146
+ overflow:hidden;
147
+ }
148
+ .stat .bar > span{
149
+ display:block;
150
+ height:100%;
151
+ background:var(--accent);
152
+ border-radius:99rem;
153
+ }
154
+ .stat.low .bar > span{background:var(--bad);}
155
+ .stat.mid .bar > span{background:var(--warn);}
156
+
157
+ /* ---------- narrative ---------- */
158
+ .finding{
159
+ display:grid;
160
+ grid-template-columns:1.4fr 1fr;
161
+ gap:2.4rem;
162
+ margin-bottom:4rem;
163
+ }
164
+ @media (max-width:820px){ .finding{grid-template-columns:1fr;} }
165
+ .finding h2{
166
+ font-size:1.9rem;
167
+ margin:0 0 1rem;
168
+ }
169
+ .finding p{margin:0 0 1rem; color:var(--ink-soft);}
170
+ .finding p:last-child{margin-bottom:0;}
171
+ .finding .card{
172
+ background:var(--surface);
173
+ border:0.1rem solid var(--line);
174
+ border-radius:0.6rem;
175
+ padding:2rem 2.2rem;
176
+ box-shadow:var(--shadow);
177
+ }
178
+ .card ol, .card ul{margin:0; padding-left:1.8rem;}
179
+ .card li{margin-bottom:0.7rem; color:var(--ink-soft);}
180
+ .card li:last-child{margin-bottom:0;}
181
+ .card li strong{color:var(--ink);}
182
+
183
+ /* ---------- controls ---------- */
184
+ .controls{
185
+ display:flex;
186
+ flex-wrap:wrap;
187
+ align-items:center;
188
+ gap:1rem;
189
+ margin-bottom:1.6rem;
190
+ }
191
+ .chip-group{display:flex; flex-wrap:wrap; gap:0.6rem;}
192
+ .chip{
193
+ font-family:"IBM Plex Mono", monospace;
194
+ font-size:1.15rem;
195
+ padding:0.5rem 1.1rem;
196
+ border-radius:99rem;
197
+ border:0.1rem solid var(--line);
198
+ background:var(--surface);
199
+ color:var(--ink-soft);
200
+ cursor:pointer;
201
+ transition:background .12s, color .12s, border-color .12s;
202
+ }
203
+ .chip:hover{border-color:var(--accent);}
204
+ .chip.active{
205
+ background:var(--ink);
206
+ border-color:var(--ink);
207
+ color:var(--paper);
208
+ }
209
+ .search{
210
+ margin-left:auto;
211
+ display:flex;
212
+ align-items:center;
213
+ gap:0.6rem;
214
+ background:var(--surface);
215
+ border:0.1rem solid var(--line);
216
+ border-radius:0.5rem;
217
+ padding:0.5rem 1rem;
218
+ }
219
+ .search input{
220
+ border:none;
221
+ background:transparent;
222
+ outline:none;
223
+ font-family:"IBM Plex Mono", monospace;
224
+ font-size:1.3rem;
225
+ color:var(--ink);
226
+ width:18rem;
227
+ }
228
+ .search input::placeholder{color:var(--ink-faint);}
229
+ .count-note{
230
+ font-family:"IBM Plex Mono", monospace;
231
+ font-size:1.15rem;
232
+ color:var(--ink-faint);
233
+ margin-bottom:1rem;
234
+ }
235
+
236
+ /* ---------- table ---------- */
237
+ .table-scroll{
238
+ overflow-x:auto;
239
+ border:0.1rem solid var(--line);
240
+ border-radius:0.6rem;
241
+ box-shadow:var(--shadow);
242
+ }
243
+ table{
244
+ width:100%;
245
+ border-collapse:collapse;
246
+ background:var(--surface);
247
+ min-width:78rem;
248
+ }
249
+ thead th{
250
+ position:sticky;
251
+ top:0;
252
+ background:var(--surface-2);
253
+ z-index:2;
254
+ text-align:left;
255
+ font-size:1.05rem;
256
+ text-transform:uppercase;
257
+ letter-spacing:0.05em;
258
+ color:var(--ink-soft);
259
+ font-weight:600;
260
+ padding:1rem 1rem;
261
+ border-bottom:0.1rem solid var(--line);
262
+ cursor:pointer;
263
+ white-space:nowrap;
264
+ user-select:none;
265
+ }
266
+ thead th:hover{color:var(--ink);}
267
+ thead th.sorted::after{content:" ▾"; color:var(--accent);}
268
+ thead th.sorted.asc::after{content:" ▴";}
269
+ tbody td{
270
+ padding:0.9rem 1rem;
271
+ border-bottom:0.1rem solid var(--line);
272
+ font-size:1.3rem;
273
+ vertical-align:middle;
274
+ }
275
+ tbody tr:last-child td{border-bottom:none;}
276
+ tbody tr:hover td{background:var(--surface-2);}
277
+ td.path{
278
+ font-family:"IBM Plex Mono", monospace;
279
+ font-size:1.2rem;
280
+ color:var(--ink);
281
+ }
282
+ td.path .n{color:var(--ink-faint); font-size:1.05rem; margin-left:0.4rem;}
283
+ .tier-pill{
284
+ font-family:"IBM Plex Mono", monospace;
285
+ font-size:1.05rem;
286
+ padding:0.15rem 0.6rem;
287
+ border-radius:0.3rem;
288
+ background:var(--surface-2);
289
+ color:var(--ink-soft);
290
+ white-space:nowrap;
291
+ }
292
+ .tier-pill.none{background:var(--bad-soft); color:var(--bad);}
293
+ td.check{text-align:center; font-size:1.5rem; line-height:1;}
294
+ .yes{color:var(--accent);}
295
+ .no{color:var(--ink-faint); opacity:0.5;}
296
+ .priv-count{
297
+ font-family:"IBM Plex Mono", monospace;
298
+ font-variant-numeric:tabular-nums;
299
+ font-size:1.2rem;
300
+ }
301
+ .priv-count.zero{color:var(--ink-faint);}
302
+ .priv-count.some{color:var(--ink-soft);}
303
+ .priv-count.many{color:var(--warn); font-weight:600;}
304
+ .score-bar{
305
+ display:inline-flex;
306
+ gap:0.2rem;
307
+ vertical-align:middle;
308
+ }
309
+ .score-bar span{
310
+ width:0.7rem;
311
+ height:0.7rem;
312
+ border-radius:50%;
313
+ background:var(--surface-2);
314
+ border:0.1rem solid var(--line);
315
+ }
316
+ .score-bar span.on{background:var(--accent); border-color:var(--accent);}
317
+
318
+ footer.note{
319
+ margin-top:2.4rem;
320
+ font-size:1.2rem;
321
+ color:var(--ink-faint);
322
+ max-width:80rem;
323
+ }
324
+ footer.note code{
325
+ font-family:"IBM Plex Mono", monospace;
326
+ background:var(--surface-2);
327
+ padding:0.1rem 0.4rem;
328
+ border-radius:0.3rem;
329
+ }
330
+
331
+ @media (max-width:900px){
332
+ .stats{grid-template-columns:repeat(3,1fr);}
333
+ }
334
+ @media (max-width:560px){
335
+ .stats{grid-template-columns:repeat(2,1fr);}
336
+ .wrap{padding:3.2rem 1.6rem 6rem;}
337
+ h1{font-size:2.6rem;}
338
+ }
339
+ </style>
340
+
341
+ <div class="wrap">
342
+
343
+ <header class="top">
344
+ <div class="title-block">
345
+ <p class="eyebrow">nuxt-components · definition of done</p>
346
+ <h1>Component Ledger</h1>
347
+ <p>A scripted pass over every component folder against the current standard: tier placement, consumer styling docs, tests, Storybook coverage, public skill docs, and VS Code snippets. Regenerated automatically whenever a component change is staged for commit.</p>
348
+ </div>
349
+ <div class="meta-block">
350
+ last rebuilt <strong id="audited-at">—</strong><br>
351
+ <span id="rowcount">—</span> component groups · <span id="vuecount">—</span> .vue files
352
+ </div>
353
+ </header>
354
+
355
+ <section class="stats" id="stats"></section>
356
+
357
+ <section class="finding">
358
+ <div id="narrative"></div>
359
+ <div class="card">
360
+ <h2 style="margin-top:0;">Reference-quality components</h2>
361
+ <p style="margin-bottom:1rem; color:var(--ink-soft);">Pass every doc/test/story/skill/snippet check, sit in a tier, no stray <code class="mono">variants/</code> folder. Point future work at these when in doubt about the shape of "done":</p>
362
+ <ol id="reference-list"></ol>
363
+ </div>
364
+ </section>
365
+
366
+ <div class="controls">
367
+ <div class="chip-group" id="tier-filters"></div>
368
+ <div class="chip-group">
369
+ <button class="chip" data-gap-filter="0">only incomplete</button>
370
+ </div>
371
+ <div class="search">
372
+ <span class="mono" style="color:var(--ink-faint);">⌕</span>
373
+ <input type="text" id="search" placeholder="filter by path…">
374
+ </div>
375
+ </div>
376
+ <p class="count-note"><span id="visible-count">0</span> shown &middot; click a column heading to sort</p>
377
+
378
+ <div class="table-scroll">
379
+ <table>
380
+ <thead>
381
+ <tr>
382
+ <th data-key="compdir" data-type="str">Component</th>
383
+ <th data-key="tier" data-type="str">Tier</th>
384
+ <th data-key="consumer_styling" data-type="bool" title="CONSUMER-STYLING.md present">Styling doc</th>
385
+ <th data-key="priv_tokens" data-type="num" title="Count of --_ prefixed vars — informational, not a defect count; see footer">--_ vars</th>
386
+ <th data-key="tests" data-type="bool">Tests</th>
387
+ <th data-key="stories" data-type="bool">Story</th>
388
+ <th data-key="skill" data-type="bool">Skill doc</th>
389
+ <th data-key="snippet" data-type="bool">Snippet</th>
390
+ <th data-key="variants" data-type="bool" title="Uses a variants/ subfolder">Variants folder</th>
391
+ <th data-key="legacy_props" data-type="bool" title="Still options-style defineProps({...}), not defineProps&lt;Props&gt;()">Legacy props</th>
392
+ <th data-key="score" data-type="num">Score</th>
393
+ </tr>
394
+ </thead>
395
+ <tbody id="tbody"></tbody>
396
+ </table>
397
+ </div>
398
+
399
+ <footer class="note">
400
+ <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>
401
+ <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>
402
+ <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>
403
+ </footer>
404
+
405
+ </div>
406
+
407
+ <script>
408
+ const PAYLOAD = __PAYLOAD__;
409
+ const DATA = PAYLOAD.rows;
410
+
411
+ document.getElementById("audited-at").textContent = PAYLOAD.auditedAt;
412
+ document.getElementById("rowcount").textContent = DATA.length;
413
+ document.getElementById("vuecount").textContent = DATA.reduce((s,d) => s + d.n_vue, 0);
414
+
415
+ // ---- stat strip ----
416
+ const statDefs = [
417
+ { key:"consumer_styling", label:"Styling doc" },
418
+ { key:"tests", label:"Tests" },
419
+ { key:"stories", label:"Story" },
420
+ { key:"skill", label:"Skill doc" },
421
+ { key:"snippet", label:"Snippet" },
422
+ { key:"placed", label:"In a tier", derive:d => d.tier !== "NONE" },
423
+ ];
424
+ const statsEl = document.getElementById("stats");
425
+ statDefs.forEach(sd => {
426
+ const count = DATA.filter(d => sd.derive ? sd.derive(d) : d[sd.key]).length;
427
+ const pct = DATA.length ? Math.round((count / DATA.length) * 100) : 0;
428
+ const cls = pct < 40 ? "low" : pct < 70 ? "mid" : "";
429
+ const el = document.createElement("div");
430
+ el.className = "stat " + cls;
431
+ el.innerHTML = `
432
+ <div class="num">${pct}<span style="font-size:1.5rem;">%</span></div>
433
+ <div class="label">${sd.label} &middot; ${count}/${DATA.length}</div>
434
+ <div class="bar"><span style="width:${pct}%"></span></div>
435
+ `;
436
+ statsEl.appendChild(el);
437
+ });
438
+
439
+ // ---- dynamic narrative (every number computed from DATA, nothing hand-written) ----
440
+ const fullyCompliant = DATA.filter(d => d.score === 5 && d.tier !== "NONE" && !d.variants && !d.legacy_props);
441
+ const unplaced = DATA.filter(d => d.tier === "NONE");
442
+ const forkedVariants = DATA.filter(d => d.variants);
443
+ const legacyProps = DATA.filter(d => d.legacy_props);
444
+ const noSkillAndUnplaced = unplaced.filter(d => !d.skill).length;
445
+ const topPrivate = DATA.filter(d => d.priv_tokens > 0).sort((a,b) => b.priv_tokens - a.priv_tokens).slice(0, 3);
446
+
447
+ const narrativeEl = document.getElementById("narrative");
448
+ narrativeEl.innerHTML = `
449
+ <h2>What's dragging the average down</h2>
450
+ <p>Only <strong>${fullyCompliant.length} of ${DATA.length}</strong> component groups clear every check. The gaps cluster in a predictable order: the older a component, the less likely it ever got a skill doc or CONSUMER-STYLING.md written for it, because both postdate a lot of what's in this tree.</p>
451
+ <p><strong>${unplaced.length} groups</strong> still sit loose at <code class="mono">app/components/</code> top level rather than inside a numbered tier. ${noSkillAndUnplaced} of those ${unplaced.length} also have no skill doc &mdash; an unplaced component tends to also be an undocumented one.</p>
452
+ <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>
453
+ <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>
454
+ <p><strong>${topPrivate.length ? "On --_ vars:" : ""}</strong> ${topPrivate.length
455
+ ? `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.`
456
+ : ""}</p>
457
+ `;
458
+
459
+ // ---- reference list ----
460
+ const refList = document.getElementById("reference-list");
461
+ fullyCompliant.forEach(d => {
462
+ const li = document.createElement("li");
463
+ li.innerHTML = `<strong>${d.compdir.split("/").pop()}</strong>`;
464
+ refList.appendChild(li);
465
+ });
466
+ if (!refList.children.length) {
467
+ refList.innerHTML = "<li>No component currently clears every check.</li>";
468
+ }
469
+
470
+ // ---- tier filter chips ----
471
+ const tiers = [...new Set(DATA.map(d => d.tier))].sort();
472
+ const tierFilters = document.getElementById("tier-filters");
473
+ let activeTier = "all";
474
+ const allChip = mkChip("all components", "all");
475
+ tierFilters.appendChild(allChip);
476
+ tiers.forEach(t => tierFilters.appendChild(mkChip(t === "NONE" ? "unplaced" : t, t)));
477
+
478
+ function mkChip(label, value){
479
+ const b = document.createElement("button");
480
+ b.className = "chip" + (value === "all" ? " active" : "");
481
+ b.textContent = label;
482
+ b.dataset.tier = value;
483
+ b.addEventListener("click", () => {
484
+ activeTier = value;
485
+ [...tierFilters.children].forEach(c => c.classList.toggle("active", c === b));
486
+ render();
487
+ });
488
+ return b;
489
+ }
490
+
491
+ let onlyIncomplete = false;
492
+ document.querySelector("[data-gap-filter]").addEventListener("click", (e) => {
493
+ onlyIncomplete = !onlyIncomplete;
494
+ e.target.classList.toggle("active", onlyIncomplete);
495
+ render();
496
+ });
497
+
498
+ const searchEl = document.getElementById("search");
499
+ searchEl.addEventListener("input", render);
500
+
501
+ let sortKey = "score";
502
+ let sortAsc = true;
503
+ document.querySelectorAll("thead th").forEach(th => {
504
+ th.addEventListener("click", () => {
505
+ const key = th.dataset.key;
506
+ if (sortKey === key) sortAsc = !sortAsc;
507
+ else { sortKey = key; sortAsc = key !== "score"; }
508
+ document.querySelectorAll("thead th").forEach(t => t.classList.remove("sorted","asc"));
509
+ th.classList.add("sorted");
510
+ if (sortAsc) th.classList.add("asc");
511
+ render();
512
+ });
513
+ });
514
+
515
+ function tierLabel(t){ return t === "NONE" ? "unplaced" : t; }
516
+
517
+ function render(){
518
+ let rows = DATA.slice();
519
+ if (activeTier !== "all") rows = rows.filter(d => d.tier === activeTier);
520
+ if (onlyIncomplete) rows = rows.filter(d => d.score < 5 || d.tier === "NONE" || d.variants || d.legacy_props);
521
+ const q = searchEl.value.trim().toLowerCase();
522
+ if (q) rows = rows.filter(d => d.compdir.toLowerCase().includes(q));
523
+
524
+ rows.sort((a,b) => {
525
+ let av = a[sortKey], bv = b[sortKey];
526
+ if (typeof av === "boolean") { av = av ? 1 : 0; bv = bv ? 1 : 0; }
527
+ if (typeof av === "string") return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
528
+ return sortAsc ? av - bv : bv - av;
529
+ });
530
+
531
+ document.getElementById("visible-count").textContent = rows.length;
532
+
533
+ const tbody = document.getElementById("tbody");
534
+ tbody.innerHTML = rows.map(d => {
535
+ const privClass = d.priv_tokens === 0 ? "zero" : d.priv_tokens > 15 ? "many" : "some";
536
+ const dots = [d.consumer_styling, d.tests, d.stories, d.skill, d.snippet]
537
+ .map(v => `<span class="${v ? "on" : ""}"></span>`).join("");
538
+ return `<tr>
539
+ <td class="path">${d.compdir}<span class="n">${d.n_vue > 1 ? "×" + d.n_vue : ""}</span></td>
540
+ <td><span class="tier-pill ${d.tier === "NONE" ? "none" : ""} mono">${tierLabel(d.tier)}</span></td>
541
+ <td class="check">${check(d.consumer_styling)}</td>
542
+ <td class="priv-count ${privClass}">${d.priv_tokens}</td>
543
+ <td class="check">${check(d.tests)}</td>
544
+ <td class="check">${check(d.stories)}</td>
545
+ <td class="check">${check(d.skill)}</td>
546
+ <td class="check">${check(d.snippet)}</td>
547
+ <td class="check">${d.variants ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
548
+ <td class="check">${d.legacy_props ? '<span class="no" style="opacity:1;color:var(--warn);">●</span>' : '<span class="no">–</span>'}</td>
549
+ <td><span class="score-bar">${dots}</span></td>
550
+ </tr>`;
551
+ }).join("");
552
+ }
553
+
554
+ function check(v){
555
+ return v ? '<span class="yes">●</span>' : '<span class="no">–</span>';
556
+ }
557
+
558
+ render();
559
+ </script>
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse hook (Skill) — fires whenever /create-commit-message runs. If any staged file
3
+ # touches app/components/, rebuilds the Component Ledger audit (.claude/component-ledger/) from
4
+ # the current repo state and tells Claude to republish it to the stored artifact URL. See
5
+ # project_component_compliance_standard.md / feedback_private_token_convention_clarified.md in
6
+ # memory for what this ledger tracks and why.
7
+
8
+ root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
9
+ cd "$root" || exit 0
10
+
11
+ skill=$(jq -r '.tool_input.skill // empty')
12
+ [[ "$skill" == "create-commit-message" ]] || exit 0
13
+
14
+ # Only bother if something staged actually touches a component.
15
+ staged_components=$(git diff --cached --name-only -- 'app/components/*' 2>/dev/null)
16
+ [[ -n "$staged_components" ]] || exit 0
17
+
18
+ node "$root/.claude/component-ledger/build.mjs" >/tmp/component-ledger-build.log 2>&1
19
+ build_status=$?
20
+
21
+ url_file="$root/.claude/component-ledger/artifact-url.txt"
22
+ url=$(cat "$url_file" 2>/dev/null)
23
+
24
+ if [[ $build_status -ne 0 ]]; then
25
+ msg="Staged changes touch component(s) (see: $staged_components), but rebuilding the Component Ledger failed — check /tmp/component-ledger-build.log. Fix before republishing."
26
+ elif [[ -z "$url" ]]; then
27
+ msg="Staged changes touch component(s) (see: $staged_components). Component Ledger rebuilt at .claude/component-ledger/output.html, but no stored artifact URL was found at $url_file — ask the user for the ledger's URL, save it there, then republish."
28
+ else
29
+ msg="Staged changes touch component(s): $(echo "$staged_components" | tr '\n' ' '). Component Ledger data rebuilt at .claude/component-ledger/output.html from the current repo state — republish it now via the Artifact tool with url=\"$url\" so the ledger reflects this change before finishing the commit message."
30
+ fi
31
+
32
+ jq -n --arg msg "$msg" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
@@ -118,6 +118,16 @@
118
118
  "timeout": 15
119
119
  }
120
120
  ]
121
+ },
122
+ {
123
+ "matcher": "Skill",
124
+ "hooks": [
125
+ {
126
+ "type": "command",
127
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/refresh-component-ledger.sh\"",
128
+ "timeout": 30
129
+ }
130
+ ]
121
131
  }
122
132
  ]
123
133
  }
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: AnimatedSvgText
3
+ description: AnimatedSvgText inline SVG stroke-draw-then-fill animation — text slot, CSS token API
4
+ type: reference
5
+ ---
6
+
7
+ # AnimatedSvgText
8
+
9
+ ## Overview
10
+
11
+ `AnimatedSvgText` animates SVG content placed in its `text` slot with a stroke-draw-then-fill
12
+ effect: the outline strokes in, then the shape fills. Works with either `<path>` elements (e.g.
13
+ letters converted to path data) or a plain SVG `<text>` element — both support `stroke`,
14
+ `fill`, and `stroke-dasharray` per the SVG spec.
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ | ---- | ---- | ------- | ----------- |
20
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `<div>`. |
21
+
22
+ ## Slots
23
+
24
+ | Slot | Description |
25
+ | ---- | ----------- |
26
+ | `text` | The SVG markup to animate. Any `<path>` elements inside are targeted by the draw animation. |
27
+
28
+ ## Basic usage
29
+
30
+ ```vue
31
+ <AnimatedSvgText>
32
+ <template #text>
33
+ <svg viewBox="0 0 600 150">
34
+ <path d="..." />
35
+ </svg>
36
+ </template>
37
+ </AnimatedSvgText>
38
+ ```
39
+
40
+ Or with a plain SVG `<text>` element (no need to trace letterforms to paths) — just scale
41
+ `--animated-svg-text-stroke-width`/`-stroke-dasharray` up for a larger `viewBox`/`font-size`:
42
+
43
+ ```vue
44
+ <AnimatedSvgText style="--animated-svg-text-stroke-width: 2; --animated-svg-text-stroke-dasharray: 2200;">
45
+ <template #text>
46
+ <svg viewBox="0 0 600 150">
47
+ <text x="0" y="110" font-size="120" font-weight="700">Hello</text>
48
+ </svg>
49
+ </template>
50
+ </AnimatedSvgText>
51
+ ```
52
+
53
+ ## CSS custom properties
54
+
55
+ All public — see `CONSUMER-STYLING.md` in the component folder for the full table.
56
+
57
+ | Property | Default |
58
+ | -------- | ------- |
59
+ | `--animated-svg-text-stroke-colour` | `var(--theme-text)` |
60
+ | `--animated-svg-text-fill-colour` | `var(--theme-text)` |
61
+ | `--animated-svg-text-stroke-width` | `0.3` |
62
+ | `--animated-svg-text-stroke-dasharray` | `1000` |
63
+ | `--animated-svg-text-animation-duration` | `2s` |
64
+
65
+ `--animated-svg-text-stroke-dasharray` is the one worth tuning per SVG — it should roughly match
66
+ the total path length of the animated content; too small finishes the stroke early, too large
67
+ makes the animation appear to stall before it starts.
68
+
69
+ ## Notes
70
+
71
+ - The animation runs once on mount (`forwards`, 1 iteration) — no loop, no scroll trigger, no
72
+ replay on re-entry.
73
+ - Previous versions of this component tried to switch colours via a `[data-color-scheme]`
74
+ attribute on `<html>` that nothing in this app ever set, referencing custom properties
75
+ (`--_animated-svg-text-stroke-light`, etc.) that were never declared anywhere — the animation's
76
+ colour and duration were effectively dead/undefined in production. Fixed by exposing genuine
77
+ public tokens that default to `var(--theme-text)`, matching the rest of the library's `--theme-*`
78
+ light/dark convention instead of reimplementing it per-component.