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.
- 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/components/column-flow-grid.md +93 -0
- package/.claude/skills/components/entry-animation.md +88 -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/index.md +6 -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-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/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/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/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/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/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/types/forms/types.forms.d.ts +1 -1
- package/package.json +1 -1
- 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/layouts/default.vue +0 -308
- package/app/layouts/site-navigation-demo.vue +0 -188
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import StorybookComponent from "../variants/InputTextareaWithLabel.vue";
|
|
3
4
|
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
4
5
|
|
|
@@ -12,7 +13,7 @@ interface InputTextareaWithLabelStoryArgs {
|
|
|
12
13
|
fieldHasError: boolean;
|
|
13
14
|
required: boolean;
|
|
14
15
|
theme: FormUiTheme;
|
|
15
|
-
|
|
16
|
+
inputVariant: InputUiVariant;
|
|
16
17
|
styleClassPassthrough: string[];
|
|
17
18
|
useDescriptionTextSlot: boolean;
|
|
18
19
|
useDescriptionHtmlSlot: boolean;
|
|
@@ -99,7 +100,7 @@ export default {
|
|
|
99
100
|
category: "Styling",
|
|
100
101
|
},
|
|
101
102
|
},
|
|
102
|
-
|
|
103
|
+
inputVariant: {
|
|
103
104
|
control: { type: "select" },
|
|
104
105
|
options: ["normal", "outlined", "underlined"],
|
|
105
106
|
description: "Textarea variant style",
|
|
@@ -196,37 +197,42 @@ export default {
|
|
|
196
197
|
},
|
|
197
198
|
} as Meta<typeof StorybookComponent>;
|
|
198
199
|
|
|
200
|
+
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
201
|
+
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
202
|
+
// and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
|
|
203
|
+
// slot-toggle args so they don't leak onto the component as unknown attributes.
|
|
199
204
|
const Template: StoryFn<InputTextareaWithLabelStoryArgs> = (args) => ({
|
|
200
205
|
components: { StorybookComponent },
|
|
201
206
|
setup() {
|
|
202
|
-
const
|
|
203
|
-
|
|
207
|
+
const componentArgs = computed(() => {
|
|
208
|
+
const {
|
|
209
|
+
useDescriptionTextSlot,
|
|
210
|
+
useDescriptionHtmlSlot,
|
|
211
|
+
descriptionTextContent,
|
|
212
|
+
descriptionHtmlContent,
|
|
213
|
+
useLeftSlot,
|
|
214
|
+
useRightSlot,
|
|
215
|
+
leftSlotContent,
|
|
216
|
+
rightSlotContent,
|
|
217
|
+
...rest
|
|
218
|
+
} = args;
|
|
219
|
+
return rest;
|
|
220
|
+
});
|
|
204
221
|
|
|
205
|
-
return {
|
|
206
|
-
textareaValue,
|
|
207
|
-
args: otherArgs,
|
|
208
|
-
descriptionTextContent: args.descriptionTextContent,
|
|
209
|
-
descriptionHtmlContent: args.descriptionHtmlContent,
|
|
210
|
-
useDescriptionTextSlot: args.useDescriptionTextSlot,
|
|
211
|
-
useDescriptionHtmlSlot: args.useDescriptionHtmlSlot,
|
|
212
|
-
leftSlotContent: args.leftSlotContent,
|
|
213
|
-
rightSlotContent: args.rightSlotContent,
|
|
214
|
-
useLeftSlot: args.useLeftSlot,
|
|
215
|
-
useRightSlot: args.useRightSlot,
|
|
216
|
-
};
|
|
222
|
+
return { args, componentArgs };
|
|
217
223
|
},
|
|
218
224
|
template: `
|
|
219
225
|
<StorybookComponent
|
|
220
|
-
v-model="
|
|
221
|
-
v-bind="
|
|
226
|
+
v-model="args.modelValue"
|
|
227
|
+
v-bind="componentArgs"
|
|
222
228
|
>
|
|
223
|
-
<template v-if="useDescriptionTextSlot" #descriptionText>{{ descriptionTextContent }}</template>
|
|
224
|
-
<template v-if="useDescriptionHtmlSlot" #descriptionHtml v-html="descriptionHtmlContent"></template>
|
|
225
|
-
<template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
|
|
226
|
-
<template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
|
|
229
|
+
<template v-if="args.useDescriptionTextSlot" #descriptionText>{{ args.descriptionTextContent }}</template>
|
|
230
|
+
<template v-if="args.useDescriptionHtmlSlot" #descriptionHtml v-html="args.descriptionHtmlContent"></template>
|
|
231
|
+
<template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
|
|
232
|
+
<template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
|
|
227
233
|
</StorybookComponent>
|
|
228
234
|
<div class="mt-4 text-sm text-gray-600">
|
|
229
|
-
Character count: {{
|
|
235
|
+
Character count: {{ args.modelValue.length }} / {{ args.maxlength }}
|
|
230
236
|
</div>
|
|
231
237
|
`,
|
|
232
238
|
});
|
|
@@ -305,7 +311,7 @@ Required.args = {
|
|
|
305
311
|
|
|
306
312
|
export const Outlined = Template.bind({});
|
|
307
313
|
Outlined.args = {
|
|
308
|
-
|
|
314
|
+
inputVariant: "outlined",
|
|
309
315
|
label: "Outlined Textarea",
|
|
310
316
|
placeholder: "This is an outlined textarea",
|
|
311
317
|
useDescriptionTextSlot: true,
|
|
@@ -314,7 +320,7 @@ Outlined.args = {
|
|
|
314
320
|
|
|
315
321
|
export const Underlined = Template.bind({});
|
|
316
322
|
Underlined.args = {
|
|
317
|
-
|
|
323
|
+
inputVariant: "underlined",
|
|
318
324
|
label: "Underlined Textarea",
|
|
319
325
|
placeholder: "This is an underlined textarea",
|
|
320
326
|
};
|
|
@@ -359,7 +365,7 @@ AllVariants.render = (args) => ({
|
|
|
359
365
|
<h3 class="text-lg font-semibold capitalize">{{ variant }} Variant</h3>
|
|
360
366
|
<StorybookComponent
|
|
361
367
|
v-model="textareaValues[variant]"
|
|
362
|
-
v-bind="{ ...args,
|
|
368
|
+
v-bind="{ ...args, inputVariant: variant, label: variant + ' Textarea', name: variant + '-textarea' }"
|
|
363
369
|
/>
|
|
364
370
|
</div>
|
|
365
371
|
</div>
|
|
@@ -16,7 +16,7 @@ export type CheckboxStyle = "check" | "cross";
|
|
|
16
16
|
export type RadioAppearance = null | "with-decorator";
|
|
17
17
|
export type OptionsLayout = "block" | "inline" | "equal-widths";
|
|
18
18
|
export type LabelWeight = "normal" | "semi-bold" | "bold";
|
|
19
|
-
export type InputTypesText = "text" | "email" | "password" | "number" | "tel" | "url";
|
|
19
|
+
export type InputTypesText = "text" | "email" | "password" | "number" | "tel" | "url" | "date";
|
|
20
20
|
export type InputMode = "text" | "email" | "tel" | "url" | "search" | "numeric" | "none" | "decimal";
|
|
21
21
|
|
|
22
22
|
// Weight types for form components
|
package/package.json
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="masonry-grid-wrapper"
|
|
4
|
-
:class="[elementClasses]"
|
|
5
|
-
:style="`--_masonry-grid-gap: ${gap}${unit}; --_item-min-width: ${itemMinWidth}px`"
|
|
6
|
-
>
|
|
7
|
-
<template v-for="item in gridData" :key="item.id">
|
|
8
|
-
<div class="masonry-grid-item">
|
|
9
|
-
<slot :name="item.id"></slot>
|
|
10
|
-
</div>
|
|
11
|
-
</template>
|
|
12
|
-
</div>
|
|
13
|
-
</template>
|
|
14
|
-
|
|
15
|
-
<script setup lang="ts">
|
|
16
|
-
const props = defineProps({
|
|
17
|
-
gridData: {
|
|
18
|
-
type: Object,
|
|
19
|
-
default: () => ({}),
|
|
20
|
-
},
|
|
21
|
-
itemMinWidth: {
|
|
22
|
-
type: Number,
|
|
23
|
-
default: 300,
|
|
24
|
-
},
|
|
25
|
-
gap: {
|
|
26
|
-
type: Number,
|
|
27
|
-
default: 1.2,
|
|
28
|
-
},
|
|
29
|
-
unit: {
|
|
30
|
-
type: String,
|
|
31
|
-
default: "rem",
|
|
32
|
-
},
|
|
33
|
-
styleClassPassthrough: {
|
|
34
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
35
|
-
default: () => [],
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
40
|
-
|
|
41
|
-
const gridData = toRef(() => props.gridData);
|
|
42
|
-
|
|
43
|
-
watch(
|
|
44
|
-
() => props.styleClassPassthrough,
|
|
45
|
-
() => {
|
|
46
|
-
resetElementClasses(props.styleClassPassthrough);
|
|
47
|
-
}
|
|
48
|
-
);
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<style lang="css">
|
|
52
|
-
@layer components {
|
|
53
|
-
.masonry-grid-wrapper {
|
|
54
|
-
--_border-color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
|
|
55
|
-
--_color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
|
|
56
|
-
|
|
57
|
-
columns: auto var(--_item-min-width);
|
|
58
|
-
column-gap: var(--_masonry-grid-gap);
|
|
59
|
-
|
|
60
|
-
.masonry-grid-item {
|
|
61
|
-
break-inside: avoid;
|
|
62
|
-
outline: 0.1rem solid var(--_border-color);
|
|
63
|
-
padding: 1.2rem;
|
|
64
|
-
margin-block-end: var(--_masonry-grid-gap);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
</style>
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="masonry-grid-ordered" :class="[elementClasses]">
|
|
3
|
-
<div class="masonry-grid-ordered-wrapper" ref="gridWrapper">
|
|
4
|
-
<div v-for="item in gridData" :key="item.id" class="masonry-grid-ordered-item" ref="gridItemsRefs">
|
|
5
|
-
<slot :name="item.id"></slot>
|
|
6
|
-
</div>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
</template>
|
|
10
|
-
|
|
11
|
-
<script setup lang="ts">
|
|
12
|
-
import { useElementSize, useResizeObserver } from "@vueuse/core"
|
|
13
|
-
|
|
14
|
-
const props = defineProps({
|
|
15
|
-
gridData: {
|
|
16
|
-
type: Object,
|
|
17
|
-
default: () => ({}),
|
|
18
|
-
},
|
|
19
|
-
minTileWidth: {
|
|
20
|
-
type: Number,
|
|
21
|
-
default: 312,
|
|
22
|
-
},
|
|
23
|
-
gap: {
|
|
24
|
-
type: Number,
|
|
25
|
-
default: 12,
|
|
26
|
-
},
|
|
27
|
-
styleClassPassthrough: {
|
|
28
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
29
|
-
default: () => [],
|
|
30
|
-
},
|
|
31
|
-
mobilePreferredColCount: {
|
|
32
|
-
type: Number,
|
|
33
|
-
default: 1,
|
|
34
|
-
},
|
|
35
|
-
fixedWidth: {
|
|
36
|
-
type: Boolean,
|
|
37
|
-
default: false,
|
|
38
|
-
},
|
|
39
|
-
justify: {
|
|
40
|
-
type: String as PropType<String>,
|
|
41
|
-
default: "left",
|
|
42
|
-
validator: (val: string) => ["left", "center", "right"].includes(val),
|
|
43
|
-
},
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
47
|
-
|
|
48
|
-
const gridData = toRef(() => props.gridData)
|
|
49
|
-
|
|
50
|
-
const minTileWidth = toRef(() => props.minTileWidth)
|
|
51
|
-
const gridWrapper = ref<null | HTMLDivElement>(null)
|
|
52
|
-
const gridItemsRefs = ref<HTMLDivElement[]>([])
|
|
53
|
-
const { width } = useElementSize(gridWrapper)
|
|
54
|
-
const columnCount = computed(() => {
|
|
55
|
-
return Math.floor(width.value / minTileWidth.value)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const gapNum = toRef(props.gap)
|
|
59
|
-
const gapStr = toRef(props.gap + "px")
|
|
60
|
-
|
|
61
|
-
const fixedWidth = toRef(() => props.fixedWidth)
|
|
62
|
-
const minTileWidthStr = toRef(props.minTileWidth + "px")
|
|
63
|
-
const maxTileWidth = computed(() => {
|
|
64
|
-
return fixedWidth.value ? minTileWidth.value + "px" : "1fr"
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const justify = computed(() => {
|
|
68
|
-
return fixedWidth.value ? props.justify : "stretch"
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
const updateGrid = () => {
|
|
72
|
-
if (gridWrapper.value !== null) {
|
|
73
|
-
const wrapperWidth = gridWrapper.value?.offsetWidth ?? 0
|
|
74
|
-
const itemWidth = fixedWidth.value
|
|
75
|
-
? minTileWidth.value
|
|
76
|
-
: Math.floor((wrapperWidth - (columnCount.value - 1) * gapNum.value) / columnCount.value)
|
|
77
|
-
|
|
78
|
-
const colHeights = Array(columnCount.value).fill(0)
|
|
79
|
-
|
|
80
|
-
gridItemsRefs.value.forEach((item) => {
|
|
81
|
-
const minHeight = Math.min(...colHeights)
|
|
82
|
-
const minIndex = colHeights.indexOf(minHeight)
|
|
83
|
-
|
|
84
|
-
item?.style.setProperty("--_position", "absolute")
|
|
85
|
-
item?.style.setProperty("--_position-top", minHeight + "px")
|
|
86
|
-
item?.style.setProperty("--_position-left", minIndex * (100 / columnCount.value) + "%")
|
|
87
|
-
item?.style.setProperty("--_element-width", itemWidth + "px")
|
|
88
|
-
|
|
89
|
-
colHeights[minIndex] += Math.floor(item.offsetHeight + gapNum.value)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const maxHeight = Math.max(...colHeights)
|
|
93
|
-
gridWrapper.value?.style.setProperty("--_wrapper-height", maxHeight + "px")
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
useResizeObserver(gridWrapper, () => {
|
|
98
|
-
updateGrid()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
watch(
|
|
102
|
-
() => fixedWidth.value,
|
|
103
|
-
() => {
|
|
104
|
-
updateGrid()
|
|
105
|
-
}
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
watch(
|
|
109
|
-
() => props.styleClassPassthrough,
|
|
110
|
-
() => {
|
|
111
|
-
resetElementClasses(props.styleClassPassthrough)
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
</script>
|
|
115
|
-
|
|
116
|
-
<style scoped lang="css">
|
|
117
|
-
@layer components {
|
|
118
|
-
.masonry-grid-ordered {
|
|
119
|
-
--_border-color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
|
|
120
|
-
--_transition-duration: 0.3s;
|
|
121
|
-
|
|
122
|
-
container-type: inline-size;
|
|
123
|
-
position: relative;
|
|
124
|
-
|
|
125
|
-
transition: max-width var(--_transition-duration) ease;
|
|
126
|
-
|
|
127
|
-
.masonry-grid-ordered-wrapper {
|
|
128
|
-
display: grid;
|
|
129
|
-
justify-self: v-bind(justify);
|
|
130
|
-
grid-gap: v-bind(gapStr);
|
|
131
|
-
grid-template-columns: repeat(1, minmax(v-bind(minTileWidthStr), v-bind(maxTileWidth)));
|
|
132
|
-
position: relative;
|
|
133
|
-
|
|
134
|
-
height: var(--_wrapper-height);
|
|
135
|
-
|
|
136
|
-
@container (min-width: 768px) {
|
|
137
|
-
grid-template-columns: repeat(2, minmax(v-bind(minTileWidthStr), v-bind(maxTileWidth)));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@container (min-width: 1024px) {
|
|
141
|
-
grid-template-columns: repeat(3, minmax(v-bind(minTileWidthStr), v-bind(maxTileWidth)));
|
|
142
|
-
}
|
|
143
|
-
@container (min-width: 1280px) {
|
|
144
|
-
grid-template-columns: repeat(4, minmax(v-bind(minTileWidthStr), v-bind(maxTileWidth)));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.masonry-grid-ordered-item {
|
|
148
|
-
transition: position var(--_transition-duration) ease, top var(--_transition-duration) ease,
|
|
149
|
-
left var(--_transition-duration) ease;
|
|
150
|
-
|
|
151
|
-
position: var(--_position);
|
|
152
|
-
top: var(--_position-top);
|
|
153
|
-
left: var(--_position-left);
|
|
154
|
-
width: var(--_element-width);
|
|
155
|
-
|
|
156
|
-
outline: 0.1rem solid var(--_border-color);
|
|
157
|
-
padding: 1.2rem;
|
|
158
|
-
border-radius: 4px;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
</style>
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="masonry-grid-ordered" :class="[elementClasses]">
|
|
3
|
-
<div
|
|
4
|
-
ref="gridWrapper"
|
|
5
|
-
class="masonry-grid-ordered-wrapper"
|
|
6
|
-
:class="[{ 'multiple-cols': !isSingleColumn, 'setup-complete': isSetupComplete }]"
|
|
7
|
-
>
|
|
8
|
-
<div v-for="item in props.gridData" :key="item.id" ref="gridItemsRefs" class="masonry-grid-ordered-item">
|
|
9
|
-
<div ref="gridContentRefs" class="masonry-grid-ordered-content">
|
|
10
|
-
<slot :name="item.id"></slot>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import { useElementSize, useResizeObserver } from "@vueuse/core";
|
|
19
|
-
|
|
20
|
-
const props = defineProps({
|
|
21
|
-
gridData: {
|
|
22
|
-
type: Array as PropType<{ id: string }[]>,
|
|
23
|
-
default: () => [],
|
|
24
|
-
},
|
|
25
|
-
minTileWidth: {
|
|
26
|
-
type: Number,
|
|
27
|
-
default: 312,
|
|
28
|
-
},
|
|
29
|
-
gap: {
|
|
30
|
-
type: Number,
|
|
31
|
-
default: 12,
|
|
32
|
-
},
|
|
33
|
-
styleClassPassthrough: {
|
|
34
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
35
|
-
default: () => [],
|
|
36
|
-
},
|
|
37
|
-
fixedWidth: {
|
|
38
|
-
type: Boolean,
|
|
39
|
-
default: false,
|
|
40
|
-
},
|
|
41
|
-
justify: {
|
|
42
|
-
type: String as PropType<"left" | "center" | "right">,
|
|
43
|
-
default: "left",
|
|
44
|
-
validator: (val: string) => ["left", "center", "right"].includes(val),
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
49
|
-
|
|
50
|
-
const gridWrapper = ref<null | HTMLDivElement>(null);
|
|
51
|
-
const gridItemsRefs = ref<HTMLDivElement[]>([]);
|
|
52
|
-
const gridContentRefs = ref<HTMLDivElement[]>([]);
|
|
53
|
-
const { width } = useElementSize(gridWrapper);
|
|
54
|
-
|
|
55
|
-
// Track item properties for masonry positioning
|
|
56
|
-
interface ItemData {
|
|
57
|
-
height: number;
|
|
58
|
-
column: number;
|
|
59
|
-
row: number;
|
|
60
|
-
top: number;
|
|
61
|
-
bottom: number;
|
|
62
|
-
translateY: number;
|
|
63
|
-
}
|
|
64
|
-
const itemDataArray = ref<ItemData[]>([]);
|
|
65
|
-
const isSetupComplete = ref(false);
|
|
66
|
-
|
|
67
|
-
const columnCount = computed(() => {
|
|
68
|
-
if (width.value === 0) return 1;
|
|
69
|
-
// Match the CSS container query breakpoint (636px hard-coded for now)
|
|
70
|
-
// if (width.value < 636) return 1
|
|
71
|
-
return Math.max(1, Math.floor(width.value / (props.minTileWidth + props.gap)));
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const isSingleColumn = computed(() => columnCount.value === 1);
|
|
75
|
-
|
|
76
|
-
const gapStr = computed(() => `${props.gap}px`);
|
|
77
|
-
|
|
78
|
-
const minTileWidthStr = computed(() => `${props.minTileWidth}px`);
|
|
79
|
-
|
|
80
|
-
const updateGrid = () => {
|
|
81
|
-
if (gridWrapper.value !== null && columnCount.value > 1) {
|
|
82
|
-
// Initialize or reset the item data array
|
|
83
|
-
itemDataArray.value = Array(props.gridData.length)
|
|
84
|
-
.fill(null)
|
|
85
|
-
.map(() => ({ height: 0, column: 0, row: 0, top: 0, bottom: 0, translateY: 0 }));
|
|
86
|
-
|
|
87
|
-
// Step 1: Hide items for measurement
|
|
88
|
-
gridItemsRefs.value.forEach((itemEl) => {
|
|
89
|
-
if (itemEl) {
|
|
90
|
-
itemEl.style.setProperty("--_opacity", "0");
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Force a reflow to get accurate measurements in grid layout
|
|
95
|
-
void gridWrapper.value.offsetHeight;
|
|
96
|
-
|
|
97
|
-
// Step 2: Measure heights and store in array
|
|
98
|
-
gridContentRefs.value.forEach((contentEl, index) => {
|
|
99
|
-
if (!contentEl || !gridItemsRefs.value[index] || !itemDataArray.value[index]) return;
|
|
100
|
-
|
|
101
|
-
const itemEl = gridItemsRefs.value[index];
|
|
102
|
-
const contentHeight = contentEl.offsetHeight;
|
|
103
|
-
|
|
104
|
-
// Calculate which column this item would be in based on CSS Grid's auto-fit
|
|
105
|
-
const column = index % columnCount.value;
|
|
106
|
-
|
|
107
|
-
// Calculate which row this item would be in based on CSS Grid's auto-fit
|
|
108
|
-
const row = Math.floor(index / columnCount.value);
|
|
109
|
-
|
|
110
|
-
let top: number;
|
|
111
|
-
let bottom: number;
|
|
112
|
-
let translateY: number;
|
|
113
|
-
|
|
114
|
-
// Always get the actual measured position first
|
|
115
|
-
const wrapperRect = gridWrapper.value!.getBoundingClientRect();
|
|
116
|
-
const itemRect = itemEl.getBoundingClientRect();
|
|
117
|
-
const actualTop = itemRect.top - wrapperRect.top;
|
|
118
|
-
|
|
119
|
-
if (row === 0) {
|
|
120
|
-
// First row: use natural CSS Grid position
|
|
121
|
-
top = actualTop;
|
|
122
|
-
bottom = top + contentHeight;
|
|
123
|
-
translateY = 0; // No transform needed for first row
|
|
124
|
-
} else {
|
|
125
|
-
// Subsequent rows: position based on item above in same column
|
|
126
|
-
const itemAboveIndex = index - columnCount.value;
|
|
127
|
-
const itemAbove = itemDataArray.value[itemAboveIndex];
|
|
128
|
-
|
|
129
|
-
if (itemAbove) {
|
|
130
|
-
top = itemAbove.bottom + props.gap;
|
|
131
|
-
bottom = top + contentHeight;
|
|
132
|
-
// Calculate the transform needed to move from actual position to desired position
|
|
133
|
-
translateY = top - actualTop;
|
|
134
|
-
} else {
|
|
135
|
-
// Fallback to natural position if no item above found
|
|
136
|
-
top = actualTop;
|
|
137
|
-
bottom = top + contentHeight;
|
|
138
|
-
translateY = 0;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Store the data in our tracking array
|
|
143
|
-
itemDataArray.value[index].height = contentHeight;
|
|
144
|
-
itemDataArray.value[index].column = column;
|
|
145
|
-
itemDataArray.value[index].row = row;
|
|
146
|
-
itemDataArray.value[index].top = top;
|
|
147
|
-
itemDataArray.value[index].bottom = bottom;
|
|
148
|
-
itemDataArray.value[index].translateY = translateY;
|
|
149
|
-
|
|
150
|
-
// Set the CSS custom properties
|
|
151
|
-
itemEl.style.setProperty("--_item-height", `${contentHeight}px`);
|
|
152
|
-
itemEl.style.setProperty("--_translate-y", `${translateY}px`);
|
|
153
|
-
itemEl.style.setProperty("--_opacity", "1");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Log the complete item data array when calculations are complete
|
|
157
|
-
// console.log("📊 Item data array:", itemDataArray.value)
|
|
158
|
-
|
|
159
|
-
// Mark setup as complete
|
|
160
|
-
isSetupComplete.value = true;
|
|
161
|
-
} else {
|
|
162
|
-
// Reset setup state for single column
|
|
163
|
-
isSetupComplete.value = false;
|
|
164
|
-
|
|
165
|
-
// Reset item data array
|
|
166
|
-
itemDataArray.value = [];
|
|
167
|
-
|
|
168
|
-
// Single column: reset to normal flow
|
|
169
|
-
gridItemsRefs.value.forEach((itemEl) => {
|
|
170
|
-
if (itemEl) {
|
|
171
|
-
itemEl.style.removeProperty("--_opacity");
|
|
172
|
-
itemEl.style.removeProperty("--_item-height");
|
|
173
|
-
itemEl.style.removeProperty("--_translate-y");
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
gridWrapper.value?.style.removeProperty("--_wrapper-height");
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
useResizeObserver(gridWrapper, async () => {
|
|
181
|
-
// console.log("useResizeObserver triggered")
|
|
182
|
-
// itemDataArray.value = [] // Clear previous data
|
|
183
|
-
// await useSleep(100)
|
|
184
|
-
// console.log("useResizeObserver after sleep")
|
|
185
|
-
nextTick(() => updateGrid());
|
|
186
|
-
|
|
187
|
-
// requestAnimationFrame(() => {
|
|
188
|
-
// updateGrid()
|
|
189
|
-
// nextTick(() => updateGrid())
|
|
190
|
-
// })
|
|
191
|
-
|
|
192
|
-
// Add a small delay to ensure DOM is fully rendered and measured
|
|
193
|
-
// setTimeout(() => {
|
|
194
|
-
// nextTick(() => updateGrid())
|
|
195
|
-
// }, 100)
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
onMounted(() => {
|
|
199
|
-
// Add a small delay to ensure DOM is fully rendered and measured
|
|
200
|
-
setTimeout(() => {
|
|
201
|
-
nextTick(() => updateGrid());
|
|
202
|
-
}, 100);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
watch(
|
|
206
|
-
() => props.fixedWidth,
|
|
207
|
-
() => {
|
|
208
|
-
updateGrid();
|
|
209
|
-
}
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
watch(
|
|
213
|
-
() => props.gridData,
|
|
214
|
-
() => {
|
|
215
|
-
// nextTick(() => updateGrid())
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
watch(
|
|
220
|
-
() => width.value,
|
|
221
|
-
(newWidth) => {
|
|
222
|
-
if (newWidth > 0) {
|
|
223
|
-
// nextTick(() => updateGrid())
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
watch(
|
|
229
|
-
() => props.styleClassPassthrough,
|
|
230
|
-
() => {
|
|
231
|
-
resetElementClasses(props.styleClassPassthrough);
|
|
232
|
-
}
|
|
233
|
-
);
|
|
234
|
-
</script>
|
|
235
|
-
|
|
236
|
-
<style lang="css">
|
|
237
|
-
@layer components {
|
|
238
|
-
.masonry-grid-ordered {
|
|
239
|
-
--_transition-duration: 0.3s;
|
|
240
|
-
|
|
241
|
-
.masonry-grid-ordered-wrapper {
|
|
242
|
-
display: grid;
|
|
243
|
-
gap: v-bind(gapStr);
|
|
244
|
-
grid-template-columns: repeat(auto-fit, minmax(v-bind(minTileWidthStr), 1fr));
|
|
245
|
-
|
|
246
|
-
height: var(--_wrapper-height, auto);
|
|
247
|
-
|
|
248
|
-
&.multiple-cols {
|
|
249
|
-
.masonry-grid-ordered-item {
|
|
250
|
-
height: var(--_item-height, auto);
|
|
251
|
-
opacity: var(--_opacity, 1);
|
|
252
|
-
transform: translateY(var(--_translate-y, 0px));
|
|
253
|
-
transition: transform var(--_transition-duration) ease;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
</style>
|