srcdev-nuxt-components 9.4.6 → 9.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.claude/commands/migrate-component.md +34 -3
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/components/column-flow-grid.md +93 -0
  7. package/.claude/skills/components/entry-animation.md +88 -0
  8. package/.claude/skills/components/input-copy.md +2 -0
  9. package/.claude/skills/components/input-text-core.md +186 -0
  10. package/.claude/skills/components/masonry-grid.md +153 -0
  11. package/.claude/skills/components/pop-over.md +100 -0
  12. package/.claude/skills/index.md +6 -1
  13. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  14. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  15. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  16. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  17. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  18. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  19. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  20. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  21. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  22. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  23. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  24. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  25. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  26. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  27. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  28. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  29. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  30. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  31. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  32. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  33. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  34. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  35. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  36. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  37. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  38. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  39. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  40. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  41. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  42. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  43. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  44. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  45. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  46. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  47. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  48. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  49. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  50. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  51. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  52. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  53. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  54. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  55. package/app/types/forms/types.forms.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  58. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  59. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  60. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  61. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  62. package/app/components/pop-over/PopOver.vue +0 -90
  63. package/app/layouts/default.vue +0 -308
  64. package/app/layouts/site-navigation-demo.vue +0 -188
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: ColumnFlowGrid
3
+ description: ColumnFlowGrid CSS multi-column (columns) text-flow layout — named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API. Not a true masonry — see MasonryGrid for that.
4
+ type: reference
5
+ ---
6
+
7
+ # ColumnFlowGrid
8
+
9
+ ## Overview
10
+
11
+ `ColumnFlowGrid` lays out slot content using CSS `columns` (the "multi-column" text-flow module —
12
+ the same mechanism newspaper-style text layouts use). Every slot is wrapped in a
13
+ `.column-flow-grid-item` div with `break-inside: avoid` so an item never splits across columns.
14
+
15
+ **Renamed from `MasonryGrid`** during this compliance pass — despite the old name, this component
16
+ doesn't do anything masonry-specific: it's CSS `columns` and nothing else, with items simply
17
+ flowing into whichever column is currently shortest **in DOM order**, not reordered for reading.
18
+ See **Which one do I want?** below.
19
+
20
+ ## Which one do I want? (`ColumnFlowGrid` vs `MasonryGrid`)
21
+
22
+ | | Technique | DOM/reading order | Column heights | Resize behaviour |
23
+ |---|---|---|---|---|
24
+ | `ColumnFlowGrid` | CSS `columns` | Column-major (as authored) | Uneven, CSS-native column-fill | Instant, native — no JS |
25
+ | `MasonryGrid` | JS-measured, greedy shortest-column packing | Natural (as authored — no reorder needed) | Uneven, genuinely bin-packed | Animated (items slide into new positions) |
26
+
27
+ Use **`ColumnFlowGrid`** when you just want cheap CSS-only column flow and don't need real
28
+ masonry packing or a resize animation — nothing here is measured or JS-driven at all, so it's the
29
+ lighter option. Use **`MasonryGrid`** (`.claude/skills/components/masonry-grid.md`) for genuine
30
+ Pinterest-style packing (each item placed into whichever column is currently shortest) with
31
+ animated reflow on resize.
32
+
33
+ `MasonryGridOrdered` (a third, JS-measured sibling that predated this pair) has been retired — its
34
+ implementation was absorbed into `MasonryGrid` directly. See `masonry-grid.md`'s migration history
35
+ note for details.
36
+
37
+ ## Props
38
+
39
+ | Prop | Type | Default | Description |
40
+ |------|------|---------|-------------|
41
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
42
+ | `itemMinWidth` | `number` | `300` | Minimum column width in pixels — drives `columns: auto <value>px`. |
43
+ | `gap` | `number` | `1.2` | Gap between columns/items, in `unit`. |
44
+ | `unit` | `string` | `"rem"` | CSS unit applied to `gap`. |
45
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
46
+
47
+ ## Slot API
48
+
49
+ Any named slot is accepted — there are no declared slot names, and **no count/data prop to keep
50
+ in sync**. The component iterates `$slots` and wraps each in a `.column-flow-grid-item`:
51
+
52
+ ```vue
53
+ <ColumnFlowGrid>
54
+ <template #item-1>...</template>
55
+ <template #item-2>...</template>
56
+ <template #item-3>...</template>
57
+ </ColumnFlowGrid>
58
+ ```
59
+
60
+ Slot names can be anything (`item-1`/`item-2` is just a convention, matching `AutoGrid`).
61
+
62
+ ## Basic usage
63
+
64
+ ```vue
65
+ <ColumnFlowGrid :item-min-width="280" :gap="1.6">
66
+ <template v-for="(photo, i) in photos" :key="photo.id" #[`item-${i}`]>
67
+ <img :src="photo.src" :alt="photo.alt" />
68
+ </template>
69
+ </ColumnFlowGrid>
70
+ ```
71
+
72
+ ## CSS Token Customization
73
+
74
+ | Token | Falls back to | Controls |
75
+ |---|---|---|
76
+ | `--column-flow-grid-item-border-colour` | `var(--theme-border)` | Outline colour around each item |
77
+ | `--column-flow-grid-item-padding` | `1.2rem` | Inner padding of each item |
78
+
79
+ Column width and gap are controlled via the `itemMinWidth`/`gap`/`unit` props, not CSS custom
80
+ properties, since they need JS-computed values rather than being purely CSS-driven. See
81
+ `CONSUMER-STYLING.md` in the component folder.
82
+
83
+ ## Notes
84
+
85
+ - **Renamed from `MasonryGrid`** (2026-09-13) — see **Which one do I want?** above for why. If you
86
+ have consumer code importing this as `MasonryGrid`, it needs updating to `ColumnFlowGrid`; the
87
+ props and behaviour are otherwise unchanged. CSS classes/tokens were renamed to match
88
+ (`masonry-grid-*` → `column-flow-grid-*`).
89
+ - **Column count is not fixed**: `columns: auto <itemMinWidth>` lets the browser decide how many
90
+ columns fit — there's no `columnCount` prop. Use `itemMinWidth` to influence it.
91
+ - Item ordering is left-to-right by column-fill in `columns` layout (an item goes into the current
92
+ shortest column), not top-to-bottom reading order — be aware of this if visual order matters for
93
+ the specific content being laid out. This is the exact distinction `MasonryGrid` exists to solve.
@@ -0,0 +1,88 @@
1
+ # EntryAnimation Component
2
+
3
+ ## Overview
4
+
5
+ `EntryAnimation` wraps slot content in a configurable tag and applies one of three CSS
6
+ scroll-driven entry-animation utility classes (`entry-slide-in`, `entry-zoom-reveal`,
7
+ `entry-exit-blur` — see `.claude/skills/css-animation-utilities.md`). It has no styling of its
8
+ own beyond applying the class; the animation keyframes/timeline live in the shared
9
+ `06.utility-classes/animations` stylesheet.
10
+
11
+ ---
12
+
13
+ ## Props reference
14
+
15
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:animation-type`, `:skip-animation`.
16
+
17
+ | Prop (template form) | Type | Default | Notes |
18
+ | --------------------------- | -------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------- |
19
+ | `:tag` | `"div" \| "section" \| "article" \| "aside"` | `"div"` | Root element tag. |
20
+ | `:animation-type` | `"entry-slide-in" \| "entry-zoom-reveal" \| "entry-exit-blur"` | `"entry-slide-in"` | Which utility animation class to apply. |
21
+ | `:skip-animation` | `boolean` | `false` | Renders with no animation class at all — see **Skipping the animation** below. |
22
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element; always applied regardless of `skipAnimation`. |
23
+
24
+ ---
25
+
26
+ ## Slots
27
+
28
+ | Slot | Notes |
29
+ | --------- | ---------------------------------------- |
30
+ | `default` | The content to animate in. |
31
+
32
+ ---
33
+
34
+ ## Skipping the animation
35
+
36
+ The common case in a `v-for` loop over a list of cards/sections is that the **first** item is
37
+ already above the fold — animating it in only delays content the visitor can already see when the
38
+ page loads. Before `skipAnimation` existed, the only way to handle this was swapping the whole
39
+ component out for a plain tag per-item:
40
+
41
+ ```vue
42
+ <!-- ❌ Old workaround — loses tag/styleClassPassthrough consistency for item 0 -->
43
+ <component :is="index === 0 ? 'div' : EntryAnimation">
44
+ ...
45
+ </component>
46
+ ```
47
+
48
+ Use `skip-animation` instead — it keeps the same component, tag, and `styleClassPassthrough` for
49
+ every item, it just omits the animation class for the ones that don't need it:
50
+
51
+ ```vue
52
+ <EntryAnimation
53
+ v-for="(item, index) in items"
54
+ :key="item.id"
55
+ :skip-animation="index === 0"
56
+ animation-type="entry-slide-in"
57
+ >
58
+ <template #default>
59
+ <!-- item content -->
60
+ </template>
61
+ </EntryAnimation>
62
+ ```
63
+
64
+ ---
65
+
66
+ ## CSS Token Customization
67
+
68
+ None — this component has no `--entry-animation-*` tokens or CSS of its own. The animation
69
+ keyframes, timing, and `prefers-reduced-motion` guard live in the shared utility class
70
+ (`entry-slide-in`/`entry-zoom-reveal`/`entry-exit-blur`), not in this component.
71
+
72
+ ---
73
+
74
+ ## Accessibility
75
+
76
+ - All three animation utility classes are already scoped inside `@media (prefers-reduced-motion:
77
+ no-preference)` at the CSS level — a visitor with reduced motion enabled sees the content with no
78
+ animation, same as `skip-animation`. `skip-animation` is for a layout/UX decision (already
79
+ visible above the fold), not a substitute for the reduced-motion guard.
80
+
81
+ ---
82
+
83
+ ## Notes
84
+
85
+ - **No animation reactivity**: `animationType`/`skipAnimation` are read once at mount to toggle the
86
+ utility class — changing either prop after mount does not currently re-toggle the class. This
87
+ matches the component's existing usage pattern (animation type is always set once per instance,
88
+ never swapped at runtime); flag it if a future consumer needs runtime toggling.
@@ -13,6 +13,8 @@
13
13
  | Prop (template form) | Type | Default | Notes |
14
14
  | -------------------------- | ---------------------- | ---------------- | ------------------------------------------------------------ |
15
15
  | `:value` | `string` | (required) | The text to copy to clipboard (e.g., license key, API token).|
16
+ | `:id` | `string` | auto-generated | Id applied to the input. Only pass one if something needs to target this input directly. |
17
+ | `:name` | `string` | falls back to `id` | Name applied to the input. Falls back to the resolved `id` (auto-generated or explicit) when not provided. |
16
18
  | `:label` | `string` | (optional) | Short label for the field; used in aria-label if ariaLabel not set. |
17
19
  | `:aria-label` | `string` | (optional) | Custom aria-label for the input; overrides label. |
18
20
  | `:description` | `string` | (optional) | Helper text displayed below the input. |
@@ -0,0 +1,186 @@
1
+ # InputTextCore Component
2
+
3
+ ## Overview
4
+
5
+ `InputTextCore` is the low-level native `<input>` primitive for the `05.forms` text-input family.
6
+ It renders a single styled text-like input with left/right icon slots, focus/dirty state models,
7
+ and numeric-pattern validation for `inputmode="numeric"`. It has no label, description, or error
8
+ message rendering of its own — those are composed by the wrapper variants below.
9
+
10
+ Most consumers should reach for one of the **Variants** rather than `InputTextCore` directly.
11
+
12
+ ---
13
+
14
+ ## Props reference
15
+
16
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:input-variant`, `:aria-describedby`.
17
+
18
+ | Prop (template form) | Type | Default | Notes |
19
+ | --------------------------- | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------- |
20
+ | `:type` | `"text" \| "email" \| "password" \| "number" \| "tel" \| "url" \| "date"` | `"text"` | Native input type. |
21
+ | `:inputmode` | `"text" \| "email" \| "tel" \| "url" \| "search" \| "numeric" \| "none" \| "decimal"` | `"text"` | Sets virtual keyboard hint; `"numeric"` also enables pattern validation. |
22
+ | `:maxlength` | `number` | `255` | Native `maxlength`. |
23
+ | `:min` | `string \| number` | (optional) | Passed straight through to the native input — e.g. earliest allowed date for `type="date"`, or lowest number for `type="number"`. Use an ISO date string (`"2026-01-01"`) for `type="date"`. |
24
+ | `:max` | `string \| number` | (optional) | Same as `min`, for the upper bound. |
25
+ | `:id` | `string` | (required) | Applied to the native input. |
26
+ | `:name` | `string` | (required) | Applied to the native input. |
27
+ | `:required` | `boolean` | `false` | Native `required`. |
28
+ | `:placeholder` | `string` | `""` | Native placeholder. |
29
+ | `:field-has-error` | `boolean` | `false` | Drives `.error` class, `aria-invalid`, and forces `theme` to `"error"`. |
30
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the wrapper. |
31
+ | `:theme` | `FormUiTheme` | `"default"` | Overridden to `"error"` internally when `fieldHasError` is true. |
32
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
33
+ | `:input-variant` | `"normal" \| "outlined" \| "underlined"` | `"normal"` | Visual style — border box vs. underline. |
34
+
35
+ ### v-models
36
+
37
+ | Model | Type | Notes |
38
+ | ----------------- | --------- | ---------------------------------------------------------- |
39
+ | `v-model` | `string` | The input value. |
40
+ | `v-model:is-dirty` | `boolean` | Set to `true` once the wrapper variant detects a non-empty value at mount or on change. |
41
+ | `v-model:is-active`| `boolean` | Tracks focus state (`focusin`/`focusout`). |
42
+
43
+ ---
44
+
45
+ ## Slots
46
+
47
+ | Slot | Notes |
48
+ | ------- | ---------------------------------------------------------------------- |
49
+ | `left` | Rendered before the input, inside the bordered wrapper (e.g. an icon or `InputButtonCore variant="inline"`). |
50
+ | `right` | Rendered after the input, inside the bordered wrapper. |
51
+
52
+ ---
53
+
54
+ ## CSS Token Customization
55
+
56
+ All `--input-text-*` tokens can be overridden at global, page, or instance scope, each falling back
57
+ to the shared `--theme-*`/`--theme-input-*` tokens every other themed form component also uses. See
58
+ `CONSUMER-STYLING.md` in the component directory for the full token table.
59
+
60
+ **Common tokens:**
61
+ - `--input-text-surface` — wrapper background
62
+ - `--input-text-border` / `--input-text-border-hover` / `--input-text-border-focus` — border/outline colour
63
+ - `--input-text-color` — input text colour
64
+ - `--input-text-placeholder-color` — placeholder colour
65
+
66
+ ---
67
+
68
+ ## Using `min`/`max` for a native date picker
69
+
70
+ `type="date"` plus `min`/`max` is the standard way to stop the browser's native date picker from
71
+ offering dates outside an allowed range — e.g. preventing a booking date in the past:
72
+
73
+ ```vue
74
+ <InputTextCore
75
+ id="appointment-date"
76
+ name="appointmentDate"
77
+ type="date"
78
+ v-model="appointmentDate"
79
+ :min="new Date().toISOString().split('T')[0]"
80
+ max="2027-12-31"
81
+ />
82
+ ```
83
+
84
+ `min`/`max` are passed straight through with no validation of their own — the browser enforces the
85
+ range in its native picker/typed-entry UI. Pair with server-side validation; a native `min`/`max`
86
+ constraint is a UX affordance, not a substitute for validating the submitted value.
87
+
88
+ ---
89
+
90
+ ## Variants
91
+
92
+ The `variants/` folder composes `InputTextCore` with `InputLabel`, `InputDescription`, and
93
+ `InputError` for the common "labelled field" case. Each forwards `min`/`max` straight through if
94
+ you pass them.
95
+
96
+ ### InputTextWithLabel
97
+
98
+ The general-purpose labelled text field — supports every `InputTypesText` value including
99
+ `"date"`.
100
+
101
+ ```vue
102
+ <InputTextWithLabel
103
+ v-model="appointmentDate"
104
+ id="appointment-date"
105
+ name="appointmentDate"
106
+ type="date"
107
+ label="Appointment date"
108
+ :min="new Date().toISOString().split('T')[0]"
109
+ max="2027-12-31"
110
+ :error-message="errors.appointmentDate"
111
+ :field-has-error="!!errors.appointmentDate"
112
+ />
113
+ ```
114
+
115
+ Props: same `type`/`inputmode`/`maxlength`/`min`/`max`/`name`/`placeholder`/`label`/`errorMessage`/
116
+ `fieldHasError`/`required`/`styleClassPassthrough`/`theme`/`inputVariant` as `InputTextCore`, plus
117
+ `label: string` (required) and `errorMessage: object | string` (required — pass `""` if unused).
118
+ Slots: `left`, `right`, `descriptionHtml`, `descriptionText`.
119
+
120
+ ### InputPasswordWithLabel
121
+
122
+ Adds a show/hide toggle button (`InputButtonCore variant="inline"` in the `right` slot) that swaps
123
+ `type` between `"password"` and `"text"`.
124
+
125
+ ```vue
126
+ <InputPasswordWithLabel
127
+ v-model="password"
128
+ name="password"
129
+ label="Password"
130
+ :error-message="errors.password"
131
+ :field-has-error="!!errors.password"
132
+ show-password-text="Show password"
133
+ hide-password-text="Hide password"
134
+ />
135
+ ```
136
+
137
+ Extra props: `showPasswordText` (default `"Show password"`) and `hidePasswordText` (default
138
+ `"Hide password"`) — the toggle button's accessible name in each state. Pass translated strings
139
+ here for a non-English consumer app; there is no built-in i18n framework dependency.
140
+
141
+ ### InputTextAsNumberWithLabel
142
+
143
+ A numeric stepper: left/right `InputButtonCore variant="inline"` buttons decrement/increment the
144
+ value by `step`, disabled once `min`/`max` is reached.
145
+
146
+ ```vue
147
+ <InputTextAsNumberWithLabel
148
+ v-model="quantity"
149
+ name="quantity"
150
+ label="Quantity"
151
+ :min="1"
152
+ :max="99"
153
+ :step="1"
154
+ step-down-text="Decrease quantity"
155
+ step-up-text="Increase quantity"
156
+ >
157
+ <template #left>−</template>
158
+ <template #right>+</template>
159
+ </InputTextAsNumberWithLabel>
160
+ ```
161
+
162
+ Extra props: `min: number` (required), `max: number` (required), `step?: number` (default `1`),
163
+ `stepDownText`/`stepUpText` (default `"Step down"`/`"Step up"`) — the decrement/increment buttons'
164
+ accessible names. Requires `left`/`right` slot content for the step icons (no default icon).
165
+
166
+ ---
167
+
168
+ ## Accessibility
169
+
170
+ - `field-has-error` sets `aria-invalid="true"` and forces the error theme, independent of what
171
+ `theme` was passed.
172
+ - `inputmode="numeric"` adds a `pattern="[0-9]+"` and blocks non-numeric `beforeinput` edits that
173
+ would fail it.
174
+ - Focus is tracked via native `focusin`/`focusout`, not a custom keyboard handler — `:focus-visible`
175
+ styling is preserved.
176
+ - The password/number variants give their icon-only toggle/step buttons a real accessible name via
177
+ `InputButtonCore`'s `buttonText` (visually hidden via `.sr-only` when an icon-only slot is used).
178
+
179
+ ---
180
+
181
+ ## Notes
182
+
183
+ - **No built-in label/error rendering**: `InputTextCore` alone renders no `<label>` or error text —
184
+ use a variant, or compose your own wrapper the same way they do.
185
+ - **`min`/`max` need matching `type`**: they're inert on `type="text"`/`"email"`/etc. — only
186
+ `"date"` and `"number"` (native range-constrained types) act on them.
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: MasonryGrid
3
+ description: MasonryGrid — real measured-height masonry layout (greedy shortest-column packing, animated resize), named dynamic slots, itemMinWidth/gap/fixedWidth/justify, CSS token API
4
+ type: reference
5
+ ---
6
+
7
+ # MasonryGrid
8
+
9
+ ## Overview
10
+
11
+ `MasonryGrid` is a genuine masonry layout: each item is measured (its real rendered height) and
12
+ placed into whichever column is currently shortest — a greedy bin-pack, the same technique
13
+ Pinterest-style layouts use. This is different from (and better than) laying items out with CSS
14
+ `columns`, which just fills one column's accumulated height before starting the next and has no
15
+ awareness of "shortest column."
16
+
17
+ Because placement follows natural DOM order directly (item 1 gets measured and placed first, then
18
+ item 2, etc.), there's no reordering trick needed to keep visual and reading order in sync — DOM
19
+ order *is* the packing order, so a screen reader or `Tab` key always encounters items 1, 2, 3...
20
+ in the order they were authored, regardless of which column each one's height ends up landing it
21
+ in visually.
22
+
23
+ **History**: this component absorbed the implementation from `MasonryGridOrdered` (now retired —
24
+ see **Migration history** below), replacing a CSS-`columns`-based reorder trick this component
25
+ used previously (see `git log` / `ColumnFlowGrid`'s history for that era).
26
+
27
+ ## When to use this vs. `ColumnFlowGrid`
28
+
29
+ - Use **`ColumnFlowGrid`** (`.claude/skills/components/column-flow-grid.md`) when you don't need
30
+ true masonry packing or animated reflow — a lighter CSS `columns`-only layout, no JS
31
+ measurement, no resize animation.
32
+ - Use **`MasonryGrid`** when you want genuine Pinterest-style packing with items animating into
33
+ new positions on resize.
34
+
35
+ ## Props
36
+
37
+ | Prop | Type | Default | Description |
38
+ |------|------|---------|-------------|
39
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
40
+ | `itemMinWidth` | `number` | `300` | Minimum tile width in pixels — also the fixed tile width when `fixedWidth` is set. |
41
+ | `gap` | `number` | `12` | Gap between tiles in pixels. |
42
+ | `fixedWidth` | `boolean` | `false` | Keep every tile at exactly `itemMinWidth` instead of stretching to fill each column. |
43
+ | `justify` | `"left" \| "center" \| "right"` | `"left"` | How the block of tiles aligns within the wrapper — only visible when `fixedWidth` is set (tiles otherwise stretch to fill the full width, so there's nothing to align). |
44
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
45
+
46
+ ## Slot API
47
+
48
+ Any named slot is accepted — there are no declared slot names, and **no count/data prop to keep
49
+ in sync**. Give slots in whatever order you want them read/tabbed through:
50
+
51
+ ```vue
52
+ <MasonryGrid>
53
+ <template #item-1>...</template>
54
+ <template #item-2>...</template>
55
+ <template #item-3>...</template>
56
+ </MasonryGrid>
57
+ ```
58
+
59
+ ## Basic usage
60
+
61
+ ```vue
62
+ <MasonryGrid :item-min-width="280" :gap="16">
63
+ <template v-for="(photo, i) in photos" :key="photo.id" #[`item-${i}`]>
64
+ <img :src="photo.src" :alt="photo.alt" />
65
+ </template>
66
+ </MasonryGrid>
67
+ ```
68
+
69
+ ### Fixed-width, centered
70
+
71
+ ```vue
72
+ <MasonryGrid :fixed-width="true" justify="center" :item-min-width="240">
73
+ <template v-for="(card, i) in cards" :key="card.id" #[`item-${i}`]>
74
+ <Card v-bind="card" />
75
+ </template>
76
+ </MasonryGrid>
77
+ ```
78
+
79
+ ## CSS Token Customization
80
+
81
+ | Token | Falls back to | Controls |
82
+ |---|---|---|
83
+ | `--masonry-grid-item-border-colour` | `var(--theme-border)` | Outline colour around each item |
84
+ | `--masonry-grid-item-padding` | `1.2rem` | Inner padding of each item |
85
+ | `--masonry-grid-transition-duration` | `0.3s` | How long an item takes to slide into its new position on resize |
86
+
87
+ Column width, gap, fixed-width mode, and alignment are controlled via props, not CSS custom
88
+ properties, since column count and item positions are computed in JS from measured pixel values.
89
+ See `CONSUMER-STYLING.md` in the component folder.
90
+
91
+ ## Accessibility
92
+
93
+ - The resize/reflow animation (`--masonry-grid-transition-duration`) is wrapped in
94
+ `@media (prefers-reduced-motion: no-preference)` — a visitor with reduced motion enabled sees
95
+ items snap to their new position with no transition.
96
+ - DOM/tab order always matches slot authoring order (see Overview) — no additional ARIA is needed
97
+ to communicate item order.
98
+
99
+ ## Notes
100
+
101
+ - **How positioning works**: on mount and on every measured resize, each item is read (`item.
102
+ offsetHeight`) in DOM order and placed at the top of whichever column currently has the least
103
+ accumulated height, via inline `--_position-top`/`--_position-left`/`--_element-width` custom
104
+ properties the component sets directly (not part of the public token API — these are internal
105
+ positioning state, not a styling surface). Column steps are computed in real pixels
106
+ (`itemWidth + gap`) rather than equal percentage slots of the container — a percentage slot only
107
+ matches the item's actual width when the item stretches to fill it (the default, non-`fixedWidth`
108
+ case); with `fixedWidth`, `justify`'s left/center/right offset is also computed here in JS
109
+ (`(wrapperWidth - totalContentWidth) / 2` for `center`, etc.), not via CSS `justify-content` —
110
+ that property has no effect on children once `position: absolute` takes them out of grid flow,
111
+ so it doesn't work here regardless of how it's set.
112
+ - **Single-column fallback**: below the width needed for a second `itemMinWidth` column plus gap,
113
+ items render in normal CSS Grid flow (no absolute positioning) at a single, always-full-width
114
+ column (`grid-template-columns: 1fr`, ignoring `itemMinWidth` entirely) — there's nothing to
115
+ pack with only one column, so it always just fills the container rather than applying
116
+ `itemMinWidth`'s floor. `itemMinWidth` starts governing column width only once there's an actual
117
+ multi-column choice to make (two columns up).
118
+ - **Column count is a JS estimate**: `Math.floor(measuredWidth / (itemMinWidth + gap))`. This
119
+ drives an underlying CSS Grid (`grid-template-columns`, with `768px`/`1024px`/`1280px` container
120
+ query breakpoints) as a no-JS-yet initial layout, which JS then overrides with absolute
121
+ positioning once it has real measurements — so there's no flash of unstyled/overlapping content
122
+ before the first `updateGrid()` run.
123
+ - **Images without explicit dimensions** will change height after they load, but nothing currently
124
+ re-triggers a re-pack when that happens (only container resize does) — a known limitation
125
+ inherited from the original implementation. Set explicit `width`/`height` on images inside slots
126
+ to avoid this, or file a follow-up if it becomes a real problem.
127
+ - **Migration history**: this component's algorithm was ported from `MasonryGridOrdered`
128
+ (`app/components/masonry-grid-ordered/MasonryGridOrdered.vue`), which is now retired along with
129
+ its `MasonryGridOrderedGridExperiment` sibling (an abandoned, more fragile alternative that
130
+ inferred CSS Grid's own auto-placement via `index % columnCount` rather than genuine shortest-
131
+ column packing — never used it). The port modernized typed props (dropped the unused
132
+ `mobilePreferredColCount` prop, and the CSS-string-with-validator `justify` prop became a real
133
+ union type), promoted CSS custom properties to a proper public token API, added
134
+ `prefers-reduced-motion` handling (missing before), and removed a redundant second
135
+ `useResizeObserver` call that duplicated the one `useElementSize` already sets up internally. It
136
+ also replaced `MasonryGrid`'s own previous CSS-`columns`-based reorder-trick implementation,
137
+ which is now `ColumnFlowGrid` (`.claude/skills/components/column-flow-grid.md`). No known
138
+ consumer app was using any of the old APIs/names.
139
+ - **Found after the port, via manual testing**: the original's `--_position-left` used equal
140
+ percentage slots (`minIndex * (100 / columnCount)%`) for every mode. That's fine when items
141
+ stretch to fill their slot, but with `fixedWidth` the item is narrower than its slot, so the
142
+ unused slot space showed up as a much bigger gap than the `gap` prop actually specified — it
143
+ visually looked like `justify-content: space-between`. Also, the CSS `justify-content` the port
144
+ initially added (see above) turned out to be entirely dead: it has no effect on children once
145
+ `position: absolute` removes them from grid flow, so `justify` never did anything either.
146
+ Fixed by computing both column steps and the `justify` offset in real pixels in JS (see **How
147
+ positioning works** above) instead of relying on CSS for either.
148
+ - **Found after that, testing narrow/mobile viewports**: the single-column CSS fallback template
149
+ was `minmax(itemMinWidth, 1fr)`, same as every other breakpoint — on a container narrower than
150
+ `itemMinWidth`, that floor fought the container instead of just filling it, producing a
151
+ non-full-width, misaligned single column. Fixed by making the single-column template a plain
152
+ `1fr` unconditionally (see **Single-column fallback** above) — `itemMinWidth` never had a
153
+ multi-column decision to inform in that case anyway.
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: PopOver
3
+ description: PopOver generic anchor-positioned disclosure panel with consumer-supplied trigger/content slots, placement prop, CSS token API
4
+ type: reference
5
+ ---
6
+
7
+ # PopOver
8
+
9
+ ## Overview
10
+
11
+ `PopOver` renders a fully consumer-supplied trigger that opens an anchor-positioned popover panel
12
+ with fully consumer-supplied content, built on the native Popover API
13
+ (`popover`/`popovertarget`) and CSS anchor-positioning (`anchor-name`/`position-anchor`/
14
+ `anchor()`). Unlike [`DisplayTooltip`](display-tooltip.md) (a fixed icon trigger for inline help
15
+ text) or `ActionMenu` (menu semantics with arrow-key navigation), `PopOver` makes no assumption
16
+ about what the trigger or content look like, and always renders its own visible close button.
17
+
18
+ **Browser support caveat:** CSS anchor-positioning and the Popover API are both comparatively
19
+ recent — check current browser support before relying on this where broad support is a hard
20
+ requirement, same caution as `DisplayTooltip`/`DeepExpandingMenu`'s equivalent gap.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |------|------|---------|-------------|
26
+ | `placement` | `"top" \| "right" \| "bottom" \| "left"` | `"right"` | Which side of the trigger the popover panel opens on. Each side has a `position-try-fallbacks` so the browser flips it automatically if it would overflow the viewport. |
27
+ | `triggerAriaLabel` | `string` | `""` | aria-label on the trigger button — set this when the `trigger` slot is icon-only. Unset (no attribute) by default, since the trigger slot usually carries its own visible/accessible content. |
28
+ | `popoverAriaLabel` | `string` | `""` | aria-label on the popover content region — set this when the `content` slot has no visible heading. |
29
+ | `closeButtonAriaLabel` | `string` | `"Close"` | aria-label on the close button — override for localisation. |
30
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
31
+
32
+ ## Slots
33
+
34
+ | Slot | Description |
35
+ |------|-------------|
36
+ | `trigger` | Content rendered inside the trigger button. |
37
+ | `content` | Content rendered inside the popover panel, below the close button. |
38
+
39
+ ## Basic usage
40
+
41
+ ```vue
42
+ <PopOver placement="bottom">
43
+ <template #trigger>
44
+ <span>Open popover</span>
45
+ </template>
46
+ <template #content>
47
+ <p>This is some popover content.</p>
48
+ </template>
49
+ </PopOver>
50
+ ```
51
+
52
+ Icon-only trigger:
53
+
54
+ ```vue
55
+ <PopOver trigger-aria-label="Show filters" popover-aria-label="Filter options">
56
+ <template #trigger>
57
+ <Icon name="lucide:filter" aria-hidden="true" />
58
+ </template>
59
+ <template #content>
60
+ <p>Filter controls would go here.</p>
61
+ </template>
62
+ </PopOver>
63
+ ```
64
+
65
+ ## Notes on content
66
+
67
+ The `content` slot's immediate children have their default UA top/bottom margins trimmed (via
68
+ `:first-child`/`:last-child`), so `--pop-over-content-padding`/`-content-padding-block-start` are
69
+ the only source of space above/below the slotted content — a plain `<p>` doesn't stack its own
70
+ `margin-block` on top of the panel's padding. Interior spacing between multiple slotted children
71
+ (e.g. two paragraphs) is untouched.
72
+
73
+ ## Behaviour
74
+
75
+ - Opening/closing is handled natively by the Popover API: clicking the trigger toggles the panel,
76
+ clicking the close button or pressing Escape hides it, and focus returns to the trigger
77
+ automatically on close.
78
+ - On open, focus moves to the close button (`handleToggle`, driven by the popover's native
79
+ `toggle` event) so keyboard users land inside the panel immediately.
80
+
81
+ ## CSS custom properties
82
+
83
+ See `CONSUMER-STYLING.md` for the full `--pop-over-*` token API (gap, trigger outline, panel
84
+ border/background/text/shadow/width, content padding, and close-button tokens).
85
+
86
+ ## Notes
87
+
88
+ - Auto-imported in Nuxt — no manual import needed.
89
+ - 2026-09-13 migration: moved from an unplaced top-level folder (`app/components/pop-over/`) into
90
+ `01.atoms/pop-over/`; converted options-style `defineProps` to `interface Props` +
91
+ `withDefaults`; the slot named `popoverCotent` (typo) was renamed to `content`, and the required
92
+ `popovertarget` prop was replaced with an auto-generated id (`useId()`) so consumers no longer
93
+ need to wire up matching ids themselves; the `role="tooltip"` on the popover panel was removed
94
+ (it described a tooltip, not this component's actual arbitrary-content disclosure semantics);
95
+ added a real, visible close button (the original template referenced one with no styling or
96
+ accessible label — literal `x` text); added `placement` (`top`/`right`/`bottom`/`left`, all with
97
+ `position-try-fallbacks`) — the original only supported opening to the right, with several
98
+ `@position-try` blocks commented out and unused; promoted every hardcoded value to a public CSS
99
+ token; added tests, a Storybook story, this skill doc, and a VS Code snippet (all previously
100
+ missing).
@@ -116,7 +116,12 @@ Each skill is a single markdown file named `<area>-<task>.md`.
116
116
  ├── deep-expanding-menu.md — DeepExpandingMenu: anchor-positioned popover nav panels, browser support caveat, CSS token API
117
117
  ├── deep-expanding-menu-classic.md — DeepExpandingMenuClassic: <details>-based fallback nav, click-outside close, CSS token API
118
118
  ├── display-tooltip.md — DisplayTooltip: anchor-positioned popover trigger, browser support caveat, CSS token API
119
- └── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
119
+ ├── display-tooltip-defined.md — DisplayTooltipDefined: structured title/body/action tooltip content with close button, composes DisplayTooltip
120
+ ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
121
+ ├── input-text-core.md — InputTextCore: native text/date/number input primitive, min/max pass-through (date-picker range), CSS token API; Variants section covers InputTextWithLabel/InputPasswordWithLabel/InputTextAsNumberWithLabel
122
+ ├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
123
+ ├── column-flow-grid.md — ColumnFlowGrid (renamed from MasonryGrid): CSS multi-column text-flow layout, named dynamic slots (no count/data prop), itemMinWidth/gap/unit sizing, CSS token API; not a true masonry, see "which one do I want?"
124
+ └── masonry-grid.md — MasonryGrid: real measured-height masonry (greedy shortest-column packing, animated resize) absorbed from the now-retired MasonryGridOrdered; named dynamic slots, fixedWidth/justify, CSS token API
120
125
  ```
121
126
 
122
127
  ## Skill file template