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,19 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`RotatingCarouselImage > renders correct HTML structure (all props set) 1`] = `
|
|
4
|
+
"<section class="rotating-carousel hover-pauses" style="--_rotate-x: 10deg; --_perspective: 1200px; --_translateZ: 1200px;" role="region" aria-label="Custom carousel" tabindex="0">
|
|
5
|
+
<div class="sr-only">Custom instructions</div><button class="control-btn" type="button" aria-label="Pause rotation"><span class="iconify i-mdi:pause" aria-hidden="true"></span></button>
|
|
6
|
+
<div class="slider" style="--_quantity: 2;">
|
|
7
|
+
<div class="item" style="--_position: 0;"><img data-nuxt-img="" srcset="/_ipx/_/a.jpg 1x, /_ipx/_/a.jpg 2x" alt="A" src="/_ipx/_/a.jpg"></div>
|
|
8
|
+
<div class="item" style="--_position: 1;"><img data-nuxt-img="" srcset="/_ipx/_/b.jpg 1x, /_ipx/_/b.jpg 2x" alt="B" src="/_ipx/_/b.jpg"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</section>"
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`RotatingCarouselImage > renders correct HTML structure (default props) 1`] = `
|
|
14
|
+
"<div class="rotating-carousel" style="--_rotate-x: 0deg; --_perspective: 1000px; --_translateZ: 1000px;" role="region" aria-label="Rotating image carousel" tabindex="0">
|
|
15
|
+
<div class="sr-only">Use spacebar to pause or play the rotation.</div>
|
|
16
|
+
<!--v-if-->
|
|
17
|
+
<div class="slider" style="--_quantity: 0;"></div>
|
|
18
|
+
</div>"
|
|
19
|
+
`;
|
|
@@ -37,17 +37,18 @@ export default {
|
|
|
37
37
|
},
|
|
38
38
|
} as Meta<typeof CanvasSwitcher>;
|
|
39
39
|
|
|
40
|
+
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
41
|
+
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
42
|
+
// and stop reflecting later Controls-panel changes.
|
|
40
43
|
const Template: StoryFn<CanvasSwitcherStoryArgs> = (args) => ({
|
|
41
44
|
components: { CanvasSwitcher },
|
|
42
45
|
setup() {
|
|
43
|
-
|
|
44
|
-
const selected = ref(canvasName);
|
|
45
|
-
return { selected, args: otherArgs };
|
|
46
|
+
return { args };
|
|
46
47
|
},
|
|
47
48
|
template: `
|
|
48
49
|
<div>
|
|
49
|
-
<CanvasSwitcher v-model:canvas-name="
|
|
50
|
-
<p style="margin-top: 1.6rem;">Selected: {{
|
|
50
|
+
<CanvasSwitcher v-model:canvas-name="args.canvasName" v-bind="args" />
|
|
51
|
+
<p style="margin-top: 1.6rem;">Selected: {{ args.canvasName }}</p>
|
|
51
52
|
</div>
|
|
52
53
|
`,
|
|
53
54
|
});
|
|
@@ -60,14 +61,12 @@ PreviewWidthDemo.args = {};
|
|
|
60
61
|
PreviewWidthDemo.render = (args) => ({
|
|
61
62
|
components: { CanvasSwitcher },
|
|
62
63
|
setup() {
|
|
63
|
-
|
|
64
|
-
const selected = ref(canvasName);
|
|
65
|
-
return { selected };
|
|
64
|
+
return { args };
|
|
66
65
|
},
|
|
67
66
|
template: `
|
|
68
67
|
<div>
|
|
69
|
-
<CanvasSwitcher v-model:canvas-name="
|
|
70
|
-
<div :class="
|
|
68
|
+
<CanvasSwitcher v-model:canvas-name="args.canvasName" />
|
|
69
|
+
<div :class="args.canvasName" style="border: 1px dashed currentColor; margin-top: 1.6rem; padding: 1.6rem;">
|
|
71
70
|
This box is constrained by the selected canvas's utility class.
|
|
72
71
|
</div>
|
|
73
72
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import ContentDocs from "../ContentDocs.vue";
|
|
3
4
|
import type { DocsNavItem } from "~/types/components";
|
|
4
5
|
|
|
@@ -86,19 +87,19 @@ export default {
|
|
|
86
87
|
|
|
87
88
|
// ─── Stories ─────────────────────────────────────────────────────────────────
|
|
88
89
|
|
|
90
|
+
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
91
|
+
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
92
|
+
// and stop reflecting later Controls-panel changes.
|
|
89
93
|
const Template: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
90
94
|
components: { ContentDocs },
|
|
91
95
|
setup() {
|
|
92
|
-
|
|
93
|
-
const activeNav = ref(activeNavItem);
|
|
94
|
-
const activePageNav = ref(activePageNavItem);
|
|
95
|
-
return { args: rest, activeNav, activePageNav };
|
|
96
|
+
return { args };
|
|
96
97
|
},
|
|
97
98
|
template: `
|
|
98
99
|
<ContentDocs
|
|
99
100
|
v-bind="args"
|
|
100
|
-
v-model:active-nav-item="
|
|
101
|
-
v-model:active-page-nav-item="
|
|
101
|
+
v-model:active-nav-item="args.activeNavItem"
|
|
102
|
+
v-model:active-page-nav-item="args.activePageNavItem"
|
|
102
103
|
>
|
|
103
104
|
<template #docsContent>
|
|
104
105
|
<h3 style="margin-top:0">Docs Content</h3>
|
|
@@ -140,10 +141,8 @@ CustomLabels.args = {
|
|
|
140
141
|
export const IconAtEnd: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
141
142
|
components: { ContentDocs },
|
|
142
143
|
setup() {
|
|
143
|
-
const {
|
|
144
|
-
|
|
145
|
-
const activePageNav = ref(activePageNavItem);
|
|
146
|
-
return { args: { ...rest, styleClassPassthrough: ["icon-at-end-demo"] }, activeNav, activePageNav };
|
|
144
|
+
const componentArgs = computed(() => ({ ...args, styleClassPassthrough: ["icon-at-end-demo"] }));
|
|
145
|
+
return { args, componentArgs };
|
|
147
146
|
},
|
|
148
147
|
template: `
|
|
149
148
|
<div>
|
|
@@ -154,9 +153,9 @@ export const IconAtEnd: StoryFn<ContentDocsStoryArgs> = (args) => ({
|
|
|
154
153
|
}
|
|
155
154
|
</style>
|
|
156
155
|
<ContentDocs
|
|
157
|
-
v-bind="
|
|
158
|
-
v-model:active-nav-item="
|
|
159
|
-
v-model:active-page-nav-item="
|
|
156
|
+
v-bind="componentArgs"
|
|
157
|
+
v-model:active-nav-item="args.activeNavItem"
|
|
158
|
+
v-model:active-page-nav-item="args.activePageNavItem"
|
|
160
159
|
>
|
|
161
160
|
<template #docsContent>
|
|
162
161
|
<h3 style="margin-top:0">Docs Content</h3>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ColumnFlowGrid — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Falls back to | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--column-flow-grid-item-border-colour` | `var(--theme-border)` | Outline colour around each item |
|
|
8
|
+
| `--column-flow-grid-item-padding` | `1.2rem` | Inner padding of each item |
|
|
9
|
+
|
|
10
|
+
Column width and gap are controlled via props (`item-min-width`, `gap`, `unit`), not CSS custom
|
|
11
|
+
properties — they drive the CSS column layout directly and need JS-computed values, so they aren't
|
|
12
|
+
part of the public token API. See `.claude/skills/components/column-flow-grid.md`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Global theming — app-level CSS file
|
|
17
|
+
|
|
18
|
+
```css
|
|
19
|
+
:where(html) {
|
|
20
|
+
--column-flow-grid-item-border-colour: var(--rose-05);
|
|
21
|
+
--column-flow-grid-item-padding: 1.6rem;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Per-instance overrides
|
|
28
|
+
|
|
29
|
+
```vue
|
|
30
|
+
<ColumnFlowGrid style="--column-flow-grid-item-border-colour: var(--gold-04);">
|
|
31
|
+
...
|
|
32
|
+
</ColumnFlowGrid>
|
|
33
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
class="column-flow-grid-wrapper"
|
|
5
|
+
:class="[elementClasses]"
|
|
6
|
+
:style="`--_column-flow-grid-gap: ${gap}${unit}; --_item-min-width: ${itemMinWidth}px`"
|
|
7
|
+
>
|
|
8
|
+
<div v-for="(_, name) in $slots" :key="name" class="column-flow-grid-item">
|
|
9
|
+
<slot :name="name"></slot>
|
|
10
|
+
</div>
|
|
11
|
+
</component>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
interface Props {
|
|
16
|
+
tag?: "div" | "section" | "article" | "main";
|
|
17
|
+
itemMinWidth?: number;
|
|
18
|
+
gap?: number;
|
|
19
|
+
unit?: string;
|
|
20
|
+
styleClassPassthrough?: string | string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
24
|
+
tag: "div",
|
|
25
|
+
itemMinWidth: 300,
|
|
26
|
+
gap: 1.2,
|
|
27
|
+
unit: "rem",
|
|
28
|
+
styleClassPassthrough: () => [],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
32
|
+
|
|
33
|
+
watch(
|
|
34
|
+
() => props.styleClassPassthrough,
|
|
35
|
+
() => {
|
|
36
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<style lang="css">
|
|
42
|
+
@layer components {
|
|
43
|
+
.column-flow-grid-wrapper {
|
|
44
|
+
columns: auto var(--_item-min-width);
|
|
45
|
+
column-gap: var(--_column-flow-grid-gap);
|
|
46
|
+
|
|
47
|
+
.column-flow-grid-item {
|
|
48
|
+
break-inside: avoid;
|
|
49
|
+
outline: 0.1rem solid var(--column-flow-grid-item-border-colour, var(--theme-border));
|
|
50
|
+
padding: var(--column-flow-grid-item-padding, 1.2rem);
|
|
51
|
+
margin-block-end: var(--_column-flow-grid-gap);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import ColumnFlowGrid from "../ColumnFlowGrid.vue";
|
|
3
|
+
|
|
4
|
+
interface ColumnFlowGridArgs {
|
|
5
|
+
tag: "div" | "section" | "article" | "main";
|
|
6
|
+
itemMinWidth: number;
|
|
7
|
+
gap: number;
|
|
8
|
+
unit: string;
|
|
9
|
+
styleClassPassthrough: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const meta: Meta<ColumnFlowGridArgs> = {
|
|
13
|
+
title: "Atoms/Grids/ColumnFlowGrid",
|
|
14
|
+
component: ColumnFlowGrid,
|
|
15
|
+
argTypes: {
|
|
16
|
+
tag: {
|
|
17
|
+
control: { type: "select" },
|
|
18
|
+
options: ["div", "section", "article", "main"],
|
|
19
|
+
description: "HTML tag to render as",
|
|
20
|
+
table: { category: "Props" },
|
|
21
|
+
},
|
|
22
|
+
itemMinWidth: {
|
|
23
|
+
control: { type: "select" },
|
|
24
|
+
options: [160, 200, 240, 300, 360, 450],
|
|
25
|
+
description: "Minimum column width in pixels — drives `columns: auto <value>px`",
|
|
26
|
+
table: { category: "Props" },
|
|
27
|
+
},
|
|
28
|
+
gap: {
|
|
29
|
+
control: { type: "select" },
|
|
30
|
+
options: [0, 0.4, 0.8, 1.2, 1.6, 2, 3],
|
|
31
|
+
description: "Gap between columns/items, in `unit`",
|
|
32
|
+
table: { category: "Props" },
|
|
33
|
+
},
|
|
34
|
+
unit: {
|
|
35
|
+
control: { type: "text" },
|
|
36
|
+
description: "CSS unit applied to `gap` (e.g. `rem`, `px`)",
|
|
37
|
+
table: { category: "Props" },
|
|
38
|
+
},
|
|
39
|
+
styleClassPassthrough: {
|
|
40
|
+
table: { disable: true },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
args: {
|
|
44
|
+
tag: "div",
|
|
45
|
+
itemMinWidth: 300,
|
|
46
|
+
gap: 1.2,
|
|
47
|
+
unit: "rem",
|
|
48
|
+
styleClassPassthrough: [],
|
|
49
|
+
},
|
|
50
|
+
parameters: {
|
|
51
|
+
docs: {
|
|
52
|
+
description: {
|
|
53
|
+
component:
|
|
54
|
+
"A CSS `columns` (multi-column text flow) layout. Renders whatever slots the consumer " +
|
|
55
|
+
"passes — no count/data prop needed (named dynamic slots, like `AutoGrid`/`GridStack`). " +
|
|
56
|
+
"Items flow into columns in DOM order, filling one column fully before starting the " +
|
|
57
|
+
"next — this does **not** reorder content, so DOM/reading order and visual order can " +
|
|
58
|
+
"diverge. Use `MasonryGrid` instead when reading order needs to match the visual layout.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default meta;
|
|
65
|
+
|
|
66
|
+
const itemStyle =
|
|
67
|
+
"padding: 1.6rem; background: white; border-radius: 0.6rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08);";
|
|
68
|
+
const badgeStyle =
|
|
69
|
+
"display: inline-flex; align-items: center; justify-content: center; width: 2.4rem; height: 2.4rem; " +
|
|
70
|
+
"margin-bottom: 0.8rem; border-radius: 50%; background: #111827; color: white; font-weight: 700; " +
|
|
71
|
+
"font-size: 1.3rem; font-family: monospace;";
|
|
72
|
+
|
|
73
|
+
// Each item shows its DOM order (slot index) as a numbered badge so you can see how the column-fill
|
|
74
|
+
// algorithm actually orders items — it's not simple row-by-row order, so an item's position in the
|
|
75
|
+
// visual grid doesn't always match its number sequentially top-to-bottom.
|
|
76
|
+
const itemBadge = (n: number) => `<span style="${badgeStyle}">${n}</span><br />`;
|
|
77
|
+
|
|
78
|
+
const paragraphPool = [
|
|
79
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio praesent libero sed cursus ante.",
|
|
80
|
+
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
|
81
|
+
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
|
82
|
+
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
|
83
|
+
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem.",
|
|
84
|
+
"Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit sed quia consequuntur magni dolores.",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
interface DemoItem {
|
|
88
|
+
slotName: string;
|
|
89
|
+
index: number;
|
|
90
|
+
bodyHtml: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generates a random number of paragraphs (1-4) per item so item heights vary unpredictably —
|
|
94
|
+
// closer to real content than a handful of hand-written examples, and enough items (12) to
|
|
95
|
+
// actually show multi-column flow. Built once per mount, not on every render, so a
|
|
96
|
+
// Controls-panel change (itemMinWidth/gap) doesn't reshuffle the demo content underneath it.
|
|
97
|
+
function buildDemoItems(count: number): DemoItem[] {
|
|
98
|
+
return Array.from({ length: count }, (_, i) => {
|
|
99
|
+
const paragraphCount = Math.floor(Math.random() * 4) + 1; // 1-4
|
|
100
|
+
const bodyHtml = Array.from(
|
|
101
|
+
{ length: paragraphCount },
|
|
102
|
+
() => `<p style="margin: 0 0 0.8rem;">${paragraphPool[Math.floor(Math.random() * paragraphPool.length)]}</p>`
|
|
103
|
+
).join("");
|
|
104
|
+
return { slotName: `item-${i + 1}`, index: i + 1, bodyHtml };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const Template: StoryFn<ColumnFlowGridArgs> = (args) => ({
|
|
109
|
+
components: { ColumnFlowGrid },
|
|
110
|
+
setup() {
|
|
111
|
+
const items = buildDemoItems(12);
|
|
112
|
+
return { args, items };
|
|
113
|
+
},
|
|
114
|
+
template: `
|
|
115
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
116
|
+
<ColumnFlowGrid v-bind="args">
|
|
117
|
+
<template v-for="item in items" :key="item.slotName" #[item.slotName]>
|
|
118
|
+
<div style="${itemStyle}">
|
|
119
|
+
<span style="${badgeStyle}">{{ item.index }}</span><br />
|
|
120
|
+
<div v-html="item.bodyHtml"></div>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
</ColumnFlowGrid>
|
|
124
|
+
</div>
|
|
125
|
+
`,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const Default = Template.bind({});
|
|
129
|
+
Default.parameters = {
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story:
|
|
133
|
+
"12 items with a random 1-4 paragraphs each, flowing into auto-sized columns at least " +
|
|
134
|
+
"300px wide. Each item is numbered with its slot/DOM order — resize the preview to change " +
|
|
135
|
+
"the column count and see how the numbering no longer reads strictly top-to-bottom per " +
|
|
136
|
+
"column once items wrap, since column-fill picks whichever column is currently shortest.",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const NarrowColumns = Template.bind({});
|
|
142
|
+
NarrowColumns.args = { itemMinWidth: 160, gap: 0.8 };
|
|
143
|
+
NarrowColumns.parameters = {
|
|
144
|
+
docs: {
|
|
145
|
+
description: { story: "A smaller `itemMinWidth` fits more, narrower columns." },
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const WideColumns = Template.bind({});
|
|
150
|
+
WideColumns.args = { itemMinWidth: 450, gap: 2 };
|
|
151
|
+
WideColumns.parameters = {
|
|
152
|
+
docs: {
|
|
153
|
+
description: { story: "A larger `itemMinWidth` fits fewer, wider columns." },
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const TwoItems: StoryFn<ColumnFlowGridArgs> = (args) => ({
|
|
158
|
+
components: { ColumnFlowGrid },
|
|
159
|
+
setup() {
|
|
160
|
+
return { args };
|
|
161
|
+
},
|
|
162
|
+
template: `
|
|
163
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
164
|
+
<ColumnFlowGrid v-bind="args">
|
|
165
|
+
<template #item-1>
|
|
166
|
+
<div style="${itemStyle}">${itemBadge(1)}Only two items provided</div>
|
|
167
|
+
</template>
|
|
168
|
+
<template #item-2>
|
|
169
|
+
<div style="${itemStyle}">
|
|
170
|
+
${itemBadge(2)}No count prop to keep in sync — the grid just renders whichever
|
|
171
|
+
slots you give it.
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
174
|
+
</ColumnFlowGrid>
|
|
175
|
+
</div>
|
|
176
|
+
`,
|
|
177
|
+
});
|
|
178
|
+
TwoItems.storyName = "Two Items (no count prop needed)";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import ColumnFlowGrid from "../ColumnFlowGrid.vue";
|
|
4
|
+
|
|
5
|
+
describe("ColumnFlowGrid", () => {
|
|
6
|
+
it("mounts without error", async () => {
|
|
7
|
+
const wrapper = await mountSuspended(ColumnFlowGrid);
|
|
8
|
+
expect(wrapper.vm).toBeTruthy();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("renders as div by default", async () => {
|
|
12
|
+
const wrapper = await mountSuspended(ColumnFlowGrid);
|
|
13
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders a different tag when provided", async () => {
|
|
17
|
+
const wrapper = await mountSuspended(ColumnFlowGrid, {
|
|
18
|
+
props: { tag: "section" },
|
|
19
|
+
});
|
|
20
|
+
expect(wrapper.element.tagName).toBe("SECTION");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders slot content", async () => {
|
|
24
|
+
const wrapper = await mountSuspended(ColumnFlowGrid, {
|
|
25
|
+
slots: {
|
|
26
|
+
"item-1": "<div>Item 1</div>",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
expect(wrapper.text()).toContain("Item 1");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders each provided slot as its own column-flow-grid-item", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(ColumnFlowGrid, {
|
|
34
|
+
slots: {
|
|
35
|
+
"item-1": "<div>Item 1</div>",
|
|
36
|
+
"item-2": "<div>Item 2</div>",
|
|
37
|
+
"item-3": "<div>Item 3</div>",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const items = wrapper.findAll(".column-flow-grid-item");
|
|
41
|
+
expect(items).toHaveLength(3);
|
|
42
|
+
expect(items[0]!.text()).toBe("Item 1");
|
|
43
|
+
expect(items[1]!.text()).toBe("Item 2");
|
|
44
|
+
expect(items[2]!.text()).toBe("Item 3");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders no items when no slots are provided", async () => {
|
|
48
|
+
const wrapper = await mountSuspended(ColumnFlowGrid);
|
|
49
|
+
expect(wrapper.findAll(".column-flow-grid-item")).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("applies itemMinWidth and gap as inline CSS custom properties", async () => {
|
|
53
|
+
const wrapper = await mountSuspended(ColumnFlowGrid, {
|
|
54
|
+
props: { itemMinWidth: 250, gap: 2, unit: "em" },
|
|
55
|
+
});
|
|
56
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
57
|
+
expect(style.getPropertyValue("--_item-min-width")).toBe("250px");
|
|
58
|
+
expect(style.getPropertyValue("--_column-flow-grid-gap")).toBe("2em");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("applies styleClassPassthrough", async () => {
|
|
62
|
+
const wrapper = await mountSuspended(ColumnFlowGrid, {
|
|
63
|
+
props: { styleClassPassthrough: "custom-class" },
|
|
64
|
+
});
|
|
65
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("applies multiple styleClassPassthrough classes", async () => {
|
|
69
|
+
const wrapper = await mountSuspended(ColumnFlowGrid, {
|
|
70
|
+
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
71
|
+
});
|
|
72
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
73
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# MasonryGrid — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Falls back to | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--masonry-grid-item-border-colour` | `var(--theme-border)` | Outline colour around each item |
|
|
8
|
+
| `--masonry-grid-item-padding` | `1.2rem` | Inner padding of each item |
|
|
9
|
+
| `--masonry-grid-transition-duration` | `0.3s` | How long an item takes to slide into its new position on resize (respects `prefers-reduced-motion`) |
|
|
10
|
+
|
|
11
|
+
Column width, gap, fixed-width mode, and alignment are controlled via props (`item-min-width`,
|
|
12
|
+
`gap`, `fixed-width`, `justify`), not CSS custom properties — column count and item positions are
|
|
13
|
+
computed in JS from measured pixel values. See `.claude/skills/components/masonry-grid.md`.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Global theming — app-level CSS file
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
:where(html) {
|
|
21
|
+
--masonry-grid-item-border-colour: var(--rose-05);
|
|
22
|
+
--masonry-grid-item-padding: 1.6rem;
|
|
23
|
+
--masonry-grid-transition-duration: 0.5s;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Per-instance overrides
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<MasonryGrid style="--masonry-grid-item-border-colour: var(--gold-04);">
|
|
33
|
+
...
|
|
34
|
+
</MasonryGrid>
|
|
35
|
+
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="masonry-grid" :class="[elementClasses]">
|
|
3
|
+
<div ref="gridWrapper" class="masonry-grid-wrapper" :class="{ 'multiple-cols': !isSingleColumn }">
|
|
4
|
+
<div v-for="name in slotNames" :key="name" ref="gridItemRefs" class="masonry-grid-item">
|
|
5
|
+
<slot :name="name"></slot>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
</component>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { useElementSize } from "@vueuse/core";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
tag?: "div" | "section" | "article" | "main";
|
|
16
|
+
/** Minimum tile width in pixels — also the fixed tile width when `fixedWidth` is set. */
|
|
17
|
+
itemMinWidth?: number;
|
|
18
|
+
/** Gap between tiles in pixels. */
|
|
19
|
+
gap?: number;
|
|
20
|
+
/** Keep every tile at exactly `itemMinWidth` instead of stretching to fill each column. */
|
|
21
|
+
fixedWidth?: boolean;
|
|
22
|
+
/** How the block of tiles aligns within the wrapper — only visible when `fixedWidth` is set
|
|
23
|
+
* (tiles otherwise stretch to fill the full width, so there's nothing to align). */
|
|
24
|
+
justify?: "left" | "center" | "right";
|
|
25
|
+
styleClassPassthrough?: string | string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
tag: "div",
|
|
30
|
+
itemMinWidth: 300,
|
|
31
|
+
gap: 12,
|
|
32
|
+
fixedWidth: false,
|
|
33
|
+
justify: "left",
|
|
34
|
+
styleClassPassthrough: () => [],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
38
|
+
|
|
39
|
+
const slots = useSlots();
|
|
40
|
+
// Consumer-provided slot names, in natural authoring/DOM order — this is both the "data" this
|
|
41
|
+
// component works with (no separate gridData/itemCount prop needed) and the order items are
|
|
42
|
+
// measured and packed in, so reading order is always preserved: unlike a CSS `columns`-based
|
|
43
|
+
// masonry, this never needs to reorder the DOM to keep visual and reading order in sync.
|
|
44
|
+
const slotNames = computed(() => Object.keys(slots));
|
|
45
|
+
|
|
46
|
+
const gridWrapper = ref<HTMLElement | null>(null);
|
|
47
|
+
const gridItemRefs = ref<HTMLElement[]>([]);
|
|
48
|
+
// Resize-reactive — useElementSize wires up its own ResizeObserver internally.
|
|
49
|
+
const { width } = useElementSize(gridWrapper);
|
|
50
|
+
|
|
51
|
+
const columnCount = computed(() => {
|
|
52
|
+
if (width.value === 0) return 1;
|
|
53
|
+
return Math.max(1, Math.floor(width.value / (props.itemMinWidth + props.gap)));
|
|
54
|
+
});
|
|
55
|
+
const isSingleColumn = computed(() => columnCount.value === 1);
|
|
56
|
+
|
|
57
|
+
const gapPx = computed(() => `${props.gap}px`);
|
|
58
|
+
const itemMinWidthPx = computed(() => `${props.itemMinWidth}px`);
|
|
59
|
+
|
|
60
|
+
// Real masonry packing: each item, in natural DOM order, is measured and placed into whichever
|
|
61
|
+
// column is currently shortest (a greedy bin-pack) — not CSS `columns`' column-fill, which packs
|
|
62
|
+
// by accumulated height per column in DOM order and can't be reordered without breaking reading
|
|
63
|
+
// order. Because placement follows DOM order directly, no reordering is ever needed here.
|
|
64
|
+
function updateGrid() {
|
|
65
|
+
if (gridWrapper.value === null || columnCount.value <= 1) {
|
|
66
|
+
gridItemRefs.value.forEach((item) => {
|
|
67
|
+
item?.style.removeProperty("--_position");
|
|
68
|
+
item?.style.removeProperty("--_position-top");
|
|
69
|
+
item?.style.removeProperty("--_position-left");
|
|
70
|
+
item?.style.removeProperty("--_element-width");
|
|
71
|
+
});
|
|
72
|
+
gridWrapper.value?.style.removeProperty("--_wrapper-height");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const wrapperWidth = width.value;
|
|
77
|
+
const itemWidth = props.fixedWidth
|
|
78
|
+
? props.itemMinWidth
|
|
79
|
+
: Math.floor((wrapperWidth - (columnCount.value - 1) * props.gap) / columnCount.value);
|
|
80
|
+
|
|
81
|
+
// Columns are placed at fixed pixel steps (itemWidth + gap) rather than equal percentage slots
|
|
82
|
+
// of the container — a percentage slot only matches the item's actual width when the item
|
|
83
|
+
// stretches to fill it (the non-fixedWidth case); with fixedWidth, the item is narrower than an
|
|
84
|
+
// equal-percentage slot, and the leftover slot space would show up as a much bigger gap than
|
|
85
|
+
// `gap` actually specifies. `justify` shifts this whole fixed-width block left/center/right
|
|
86
|
+
// within the leftover space — done here in JS, not via CSS `justify-content`, since it has no
|
|
87
|
+
// effect on children once they're taken out of grid flow by `position: absolute` below.
|
|
88
|
+
const totalContentWidth = columnCount.value * itemWidth + (columnCount.value - 1) * props.gap;
|
|
89
|
+
const startX = props.fixedWidth
|
|
90
|
+
? { left: 0, center: (wrapperWidth - totalContentWidth) / 2, right: wrapperWidth - totalContentWidth }[props.justify]
|
|
91
|
+
: 0;
|
|
92
|
+
|
|
93
|
+
const colHeights = Array(columnCount.value).fill(0);
|
|
94
|
+
|
|
95
|
+
gridItemRefs.value.forEach((item) => {
|
|
96
|
+
const minHeight = Math.min(...colHeights);
|
|
97
|
+
const minIndex = colHeights.indexOf(minHeight);
|
|
98
|
+
|
|
99
|
+
item?.style.setProperty("--_position", "absolute");
|
|
100
|
+
item?.style.setProperty("--_position-top", `${minHeight}px`);
|
|
101
|
+
item?.style.setProperty("--_position-left", `${startX + minIndex * (itemWidth + props.gap)}px`);
|
|
102
|
+
item?.style.setProperty("--_element-width", `${itemWidth}px`);
|
|
103
|
+
|
|
104
|
+
colHeights[minIndex] += Math.floor((item?.offsetHeight ?? 0) + props.gap);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const maxHeight = Math.max(...colHeights);
|
|
108
|
+
gridWrapper.value.style.setProperty("--_wrapper-height", `${maxHeight}px`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Re-pack whenever the measured width changes (useElementSize already wires up its own
|
|
112
|
+
// ResizeObserver — a second explicit useResizeObserver call here would just duplicate it).
|
|
113
|
+
watch(width, () => nextTick(() => updateGrid()));
|
|
114
|
+
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
nextTick(() => updateGrid());
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
watch(
|
|
120
|
+
() => [props.fixedWidth, props.justify, props.itemMinWidth, props.gap],
|
|
121
|
+
() => updateGrid()
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
watch(
|
|
125
|
+
() => props.styleClassPassthrough,
|
|
126
|
+
() => {
|
|
127
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<style lang="css">
|
|
133
|
+
@layer components {
|
|
134
|
+
.masonry-grid-wrapper {
|
|
135
|
+
container-type: inline-size;
|
|
136
|
+
position: relative;
|
|
137
|
+
|
|
138
|
+
display: grid;
|
|
139
|
+
gap: v-bind(gapPx);
|
|
140
|
+
/* Single column always stretches to fill the full width — with only one column there's no
|
|
141
|
+
"how many columns fit" question for itemMinWidth to answer, so applying its floor here
|
|
142
|
+
(minmax(itemMinWidth, 1fr)) only ever hurt: on a narrow container it fights the container
|
|
143
|
+
for space instead of just filling it. itemMinWidth starts governing column width once
|
|
144
|
+
there's an actual choice to make, from two columns up (below). */
|
|
145
|
+
grid-template-columns: 1fr;
|
|
146
|
+
|
|
147
|
+
height: var(--_wrapper-height, auto);
|
|
148
|
+
|
|
149
|
+
@container (min-width: 768px) {
|
|
150
|
+
grid-template-columns: repeat(2, minmax(v-bind(itemMinWidthPx), 1fr));
|
|
151
|
+
}
|
|
152
|
+
@container (min-width: 1024px) {
|
|
153
|
+
grid-template-columns: repeat(3, minmax(v-bind(itemMinWidthPx), 1fr));
|
|
154
|
+
}
|
|
155
|
+
@container (min-width: 1280px) {
|
|
156
|
+
grid-template-columns: repeat(4, minmax(v-bind(itemMinWidthPx), 1fr));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.masonry-grid-item {
|
|
160
|
+
outline: 0.1rem solid var(--masonry-grid-item-border-colour, var(--theme-border));
|
|
161
|
+
padding: var(--masonry-grid-item-padding, 1.2rem);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&.multiple-cols .masonry-grid-item {
|
|
165
|
+
position: var(--_position, static);
|
|
166
|
+
top: var(--_position-top, auto);
|
|
167
|
+
left: var(--_position-left, auto);
|
|
168
|
+
width: var(--_element-width, auto);
|
|
169
|
+
|
|
170
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
171
|
+
transition:
|
|
172
|
+
top var(--masonry-grid-transition-duration, 0.3s) ease,
|
|
173
|
+
left var(--masonry-grid-transition-duration, 0.3s) ease;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
</style>
|