srcdev-nuxt-components 9.4.6 → 9.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/migrate-component.md +34 -3
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +27 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/column-flow-grid.md +93 -0
- package/.claude/skills/components/entry-animation.md +88 -0
- package/.claude/skills/components/header-block.md +92 -0
- package/.claude/skills/components/input-copy.md +2 -0
- package/.claude/skills/components/input-text-core.md +186 -0
- package/.claude/skills/components/masonry-grid.md +153 -0
- package/.claude/skills/components/pop-over.md +100 -0
- package/.claude/skills/components/rotating-carousel-image.md +101 -0
- package/.claude/skills/components/skip-links.md +82 -0
- package/.claude/skills/components/tabs-core.md +187 -0
- package/.claude/skills/index.md +10 -1
- package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
- package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
- package/.vscode/srcdev-component-header-block.code-snippets +41 -0
- package/.vscode/srcdev-component-input-text.code-snippets +107 -0
- package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
- package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
- package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
- package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
- package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
- package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
- package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
- package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
- package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
- package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
- package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
- package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
- package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
- package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
- package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
- package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
- package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
- package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
- package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
- package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
- package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
- package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
- package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
- package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
- package/app/composables/useTabs.ts +225 -207
- package/app/types/components/index.ts +2 -0
- package/app/types/components/rotating-carousel-image.d.ts +4 -0
- package/app/types/components/skip-links.d.ts +4 -0
- package/app/types/forms/types.forms.d.ts +1 -1
- package/package.json +1 -1
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
- package/app/components/masonry-grid/MasonryGrid.vue +0 -68
- package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
- package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
- package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
- package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
- package/app/components/pop-over/PopOver.vue +0 -90
- package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
- package/app/components/skip-links/SkipLinks.vue +0 -60
- package/app/components/tabs/TabsCore.vue +0 -306
- package/app/components/typography/HeaderBlock.vue +0 -35
- package/app/layouts/default.vue +0 -308
- package/app/layouts/site-navigation-demo.vue +0 -188
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
|
@@ -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.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: HeaderBlock
|
|
3
|
+
description: HeaderBlock semantic-vs-visual heading wrapper — tagLevel/classLevel decoupling, page-heading-N global utility classes, id for aria-labelledby pairing, styleClassPassthrough
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# HeaderBlock
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`HeaderBlock` decouples a heading's **semantic level** (which `<h1>`-`<h6>` tag is rendered, for
|
|
12
|
+
document structure and accessibility) from its **visual size** (which `.page-heading-1`–
|
|
13
|
+
`.page-heading-6` global utility class is applied, for how big it reads on the page). These are
|
|
14
|
+
controlled by two independent props, `tagLevel` and `classLevel` — a component can be a semantic
|
|
15
|
+
`<h2>` (correct in the document outline) styled at the `page-heading-1` size (the biggest visual
|
|
16
|
+
size), or vice versa.
|
|
17
|
+
|
|
18
|
+
Use this whenever a page's visual hierarchy doesn't match its logical structure — e.g. a hero
|
|
19
|
+
section's heading needs to be the largest thing on the page but the page already has an `<h1>`
|
|
20
|
+
elsewhere, so the hero heading should be an `<h2>` styled at `page-heading-1` size.
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
|------|------|---------|-------------|
|
|
26
|
+
| `tagLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` (or the numeric string equivalent) | `1` | Which `<h{n}>` tag is rendered. Out-of-range values fall back to `1`. |
|
|
27
|
+
| `classLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` (or the numeric string equivalent) | `1` | Which `.page-heading-{n}` utility class is applied — independent of `tagLevel`. Out-of-range values fall back to `1`. |
|
|
28
|
+
| `id` | `string \| null` | `null` | Rendered as the `id` attribute — bind a wrapping landmark's `aria-labelledby` target here (see "Pairing with a section's aria-labelledby" below). |
|
|
29
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
30
|
+
|
|
31
|
+
## Slots
|
|
32
|
+
|
|
33
|
+
Default slot — the heading content. Can contain nested markup (e.g. a `<span>` for partial
|
|
34
|
+
highlighting), not just plain text.
|
|
35
|
+
|
|
36
|
+
## Basic usage
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<HeaderBlock :tag-level="1" :class-level="1">Page title</HeaderBlock>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Decoupled semantic/visual example
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<!-- Hero heading: visually the biggest thing on the page, but semantically an h2 because
|
|
46
|
+
the page's real h1 lives elsewhere (e.g. in SiteHeader's branding). -->
|
|
47
|
+
<HeaderBlock :tag-level="2" :class-level="1">Welcome</HeaderBlock>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Pairing with a section's aria-labelledby
|
|
51
|
+
|
|
52
|
+
A `<section>`/`<article>`/`<aside>` landmark needs `aria-labelledby` pointing at the `id` of a real
|
|
53
|
+
heading to have an accessible name. Bind the same id to both:
|
|
54
|
+
|
|
55
|
+
```vue
|
|
56
|
+
<section aria-labelledby="pricing-heading">
|
|
57
|
+
<HeaderBlock id="pricing-heading" :tag-level="2" :class-level="1">Pricing</HeaderBlock>
|
|
58
|
+
</section>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Several components in this library (`PageRow`, `LayoutGridByCols`/`LayoutGridByWidth`,
|
|
62
|
+
`ProfileSection`, `ServiceSummary`, `ServiceDetail`, `PageHeroHighlights`) already generate this id
|
|
63
|
+
automatically via the `useAriaLabelledById` composable and expose it as a `heading-id` slot prop —
|
|
64
|
+
when composing inside one of those, bind `HeaderBlock`'s `id` to that slot prop instead of writing
|
|
65
|
+
your own id:
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<PageRow tag="section" v-slot="{ headingId }">
|
|
69
|
+
<HeaderBlock :id="headingId" :tag-level="2" :class-level="1">Pricing</HeaderBlock>
|
|
70
|
+
</PageRow>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Styling
|
|
74
|
+
|
|
75
|
+
`HeaderBlock` has no CSS of its own — `classLevel` applies one of the global
|
|
76
|
+
`.page-heading-1`–`.page-heading-6` utility classes defined in
|
|
77
|
+
`app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css`, each
|
|
78
|
+
setting `font-size` from a shared fluid type-scale token (`--step-3` through `--step-8`). All six
|
|
79
|
+
levels also reset `margin-block`/`padding-block` to `0rem` (deliberately, so a consumer adds
|
|
80
|
+
spacing themselves rather than fighting the browser's own varying per-level heading margins). See
|
|
81
|
+
`CONSUMER-STYLING.md` in this component's folder — overriding those tokens/values affects every
|
|
82
|
+
`page-heading-*` element site-wide, not just one `HeaderBlock` instance; use
|
|
83
|
+
`styleClassPassthrough` to customise a single instance.
|
|
84
|
+
|
|
85
|
+
## Migration note (2026-09-15)
|
|
86
|
+
|
|
87
|
+
Previously lived at `app/components/typography/HeaderBlock.vue` with an inline `defineProps<{...}>()`
|
|
88
|
+
type literal (no `withDefaults`) instead of the standard `interface Props` + `withDefaults` pattern.
|
|
89
|
+
Moved to `01.atoms/text-blocks/header-block/`, alongside `EyebrowText`/`HeroText`/`LinkText`. Also
|
|
90
|
+
added the `id` prop in the same pass — several sections need their heading to carry a specific id
|
|
91
|
+
for `aria-labelledby`, and `HeaderBlock` had no explicit way to receive one (matching `PageRow`'s
|
|
92
|
+
existing `id?: string | null` + `:id` convention).
|
|
@@ -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.
|