srcdev-nuxt-components 6.2.11 → 6.2.12
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/app/components/canvas-switcher/CanvasSwitcher.vue +33 -4
- package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +265 -0
- package/app/components/responsive-header/ResponsiveHeader.vue +3 -3
- package/app/composables/useNavigationState.ts +1 -1
- package/app/types/index.ts +1 -0
- package/package.json +1 -1
|
@@ -22,18 +22,31 @@
|
|
|
22
22
|
<Icon name="ic:outline-desktop-mac" class="icon" :class="[{ current: canvasName === 'desktopCanvas' }]" />
|
|
23
23
|
</button>
|
|
24
24
|
</li>
|
|
25
|
+
<li>
|
|
26
|
+
<button type="button" @click.stop.prevent="updateCanvas('fullWidthCanvas')">
|
|
27
|
+
<Icon
|
|
28
|
+
name="pixelarticons:viewport-wide"
|
|
29
|
+
class="icon"
|
|
30
|
+
:class="[{ current: canvasName === 'fullWidthCanvas' }]"
|
|
31
|
+
/>
|
|
32
|
+
</button>
|
|
33
|
+
</li>
|
|
25
34
|
</ul>
|
|
26
35
|
</div>
|
|
27
36
|
</template>
|
|
28
37
|
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
export type MediaCanvas = "mobileCanvas" | "tabletCanvas" | "laptopCanvas" | "desktopCanvas" | "fullWidthCanvas"
|
|
40
|
+
</script>
|
|
41
|
+
|
|
29
42
|
<script setup lang="ts">
|
|
30
|
-
import type { MediaCanvas } from
|
|
43
|
+
// import type { MediaCanvas } from "@/types"
|
|
31
44
|
|
|
32
|
-
const canvasName = defineModel<MediaCanvas>(
|
|
45
|
+
const canvasName = defineModel<MediaCanvas>("canvasName")
|
|
33
46
|
|
|
34
47
|
const updateCanvas = (setCanvas: MediaCanvas) => {
|
|
35
|
-
canvasName.value = setCanvas
|
|
36
|
-
}
|
|
48
|
+
canvasName.value = setCanvas
|
|
49
|
+
}
|
|
37
50
|
</script>
|
|
38
51
|
|
|
39
52
|
<style lang="css">
|
|
@@ -74,4 +87,20 @@ const updateCanvas = (setCanvas: MediaCanvas) => {
|
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
}
|
|
90
|
+
|
|
91
|
+
.mobileCanvas {
|
|
92
|
+
max-width: 412px;
|
|
93
|
+
}
|
|
94
|
+
.tabletCanvas {
|
|
95
|
+
max-width: 768px;
|
|
96
|
+
}
|
|
97
|
+
.laptopCanvas {
|
|
98
|
+
max-width: 1060px;
|
|
99
|
+
}
|
|
100
|
+
.desktopCanvas {
|
|
101
|
+
max-width: 1280px;
|
|
102
|
+
}
|
|
103
|
+
.fullWidthCanvas {
|
|
104
|
+
max-width: unset;
|
|
105
|
+
}
|
|
77
106
|
</style>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="masonry-grid-ordered" :class="[elementClasses]">
|
|
3
|
+
<div
|
|
4
|
+
class="masonry-grid-ordered-wrapper"
|
|
5
|
+
:class="[{ 'multiple-cols': !isSingleColumn, 'setup-complete': isSetupComplete }]"
|
|
6
|
+
ref="gridWrapper"
|
|
7
|
+
>
|
|
8
|
+
<div v-for="item in props.gridData" :key="item.id" class="masonry-grid-ordered-item" ref="gridItemsRefs">
|
|
9
|
+
<div class="masonry-grid-ordered-content" ref="gridContentRefs">
|
|
10
|
+
<slot :name="item.id"></slot>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed, nextTick, onMounted, ref, watch, type PropType } from "vue"
|
|
19
|
+
import { useElementSize, useResizeObserver } from "@vueuse/core"
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
gridData: {
|
|
23
|
+
type: Array as PropType<{ id: string }[]>,
|
|
24
|
+
default: () => [],
|
|
25
|
+
},
|
|
26
|
+
minTileWidth: {
|
|
27
|
+
type: Number,
|
|
28
|
+
default: 312,
|
|
29
|
+
},
|
|
30
|
+
gap: {
|
|
31
|
+
type: Number,
|
|
32
|
+
default: 12,
|
|
33
|
+
},
|
|
34
|
+
styleClassPassthrough: {
|
|
35
|
+
type: [String, Array] as PropType<string | string[]>,
|
|
36
|
+
default: () => [],
|
|
37
|
+
},
|
|
38
|
+
fixedWidth: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
justify: {
|
|
43
|
+
type: String as PropType<"left" | "center" | "right">,
|
|
44
|
+
default: "left",
|
|
45
|
+
validator: (val: string) => ["left", "center", "right"].includes(val),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
|
|
50
|
+
|
|
51
|
+
const gridWrapper = ref<null | HTMLDivElement>(null)
|
|
52
|
+
const gridItemsRefs = ref<HTMLDivElement[]>([])
|
|
53
|
+
const gridContentRefs = ref<HTMLDivElement[]>([])
|
|
54
|
+
const { width } = useElementSize(gridWrapper)
|
|
55
|
+
|
|
56
|
+
// Track item properties for masonry positioning
|
|
57
|
+
interface ItemData {
|
|
58
|
+
height: number
|
|
59
|
+
column: number
|
|
60
|
+
row: number
|
|
61
|
+
top: number
|
|
62
|
+
bottom: number
|
|
63
|
+
translateY: number
|
|
64
|
+
}
|
|
65
|
+
const itemDataArray = ref<ItemData[]>([])
|
|
66
|
+
const isSetupComplete = ref(false)
|
|
67
|
+
|
|
68
|
+
const columnCount = computed(() => {
|
|
69
|
+
if (width.value === 0) return 1
|
|
70
|
+
// Match the CSS container query breakpoint (636px hard-coded for now)
|
|
71
|
+
// if (width.value < 636) return 1
|
|
72
|
+
return Math.max(1, Math.floor(width.value / (props.minTileWidth + props.gap)))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const isSingleColumn = computed(() => columnCount.value === 1)
|
|
76
|
+
|
|
77
|
+
const gapStr = computed(() => `${props.gap}px`)
|
|
78
|
+
|
|
79
|
+
const minTileWidthStr = computed(() => `${props.minTileWidth}px`)
|
|
80
|
+
const maxTileWidth = computed(() => {
|
|
81
|
+
return props.fixedWidth ? `${props.minTileWidth}px` : "1fr"
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const justifyContent = computed(() => {
|
|
85
|
+
return props.fixedWidth ? props.justify : "stretch"
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const updateGrid = () => {
|
|
89
|
+
if (gridWrapper.value !== null && columnCount.value > 1) {
|
|
90
|
+
// Initialize or reset the item data array
|
|
91
|
+
itemDataArray.value = Array(props.gridData.length)
|
|
92
|
+
.fill(null)
|
|
93
|
+
.map(() => ({ height: 0, column: 0, row: 0, top: 0, bottom: 0, translateY: 0 }))
|
|
94
|
+
|
|
95
|
+
// Step 1: Hide items for measurement
|
|
96
|
+
gridItemsRefs.value.forEach((itemEl) => {
|
|
97
|
+
if (itemEl) {
|
|
98
|
+
itemEl.style.setProperty("--_opacity", "0")
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Force a reflow to get accurate measurements in grid layout
|
|
103
|
+
void gridWrapper.value.offsetHeight
|
|
104
|
+
|
|
105
|
+
// Step 2: Measure heights and store in array
|
|
106
|
+
gridContentRefs.value.forEach((contentEl, index) => {
|
|
107
|
+
if (!contentEl || !gridItemsRefs.value[index] || !itemDataArray.value[index]) return
|
|
108
|
+
|
|
109
|
+
const itemEl = gridItemsRefs.value[index]
|
|
110
|
+
const contentHeight = contentEl.offsetHeight
|
|
111
|
+
|
|
112
|
+
// Calculate which column this item would be in based on CSS Grid's auto-fit
|
|
113
|
+
const column = index % columnCount.value
|
|
114
|
+
|
|
115
|
+
// Calculate which row this item would be in based on CSS Grid's auto-fit
|
|
116
|
+
const row = Math.floor(index / columnCount.value)
|
|
117
|
+
|
|
118
|
+
let top: number
|
|
119
|
+
let bottom: number
|
|
120
|
+
let translateY: number
|
|
121
|
+
|
|
122
|
+
// Always get the actual measured position first
|
|
123
|
+
const wrapperRect = gridWrapper.value!.getBoundingClientRect()
|
|
124
|
+
const itemRect = itemEl.getBoundingClientRect()
|
|
125
|
+
const actualTop = itemRect.top - wrapperRect.top
|
|
126
|
+
|
|
127
|
+
if (row === 0) {
|
|
128
|
+
// First row: use natural CSS Grid position
|
|
129
|
+
top = actualTop
|
|
130
|
+
bottom = top + contentHeight
|
|
131
|
+
translateY = 0 // No transform needed for first row
|
|
132
|
+
} else {
|
|
133
|
+
// Subsequent rows: position based on item above in same column
|
|
134
|
+
const itemAboveIndex = index - columnCount.value
|
|
135
|
+
const itemAbove = itemDataArray.value[itemAboveIndex]
|
|
136
|
+
|
|
137
|
+
if (itemAbove) {
|
|
138
|
+
top = itemAbove.bottom + props.gap
|
|
139
|
+
bottom = top + contentHeight
|
|
140
|
+
// Calculate the transform needed to move from actual position to desired position
|
|
141
|
+
translateY = top - actualTop
|
|
142
|
+
} else {
|
|
143
|
+
// Fallback to natural position if no item above found
|
|
144
|
+
top = actualTop
|
|
145
|
+
bottom = top + contentHeight
|
|
146
|
+
translateY = 0
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Store the data in our tracking array
|
|
151
|
+
itemDataArray.value[index].height = contentHeight
|
|
152
|
+
itemDataArray.value[index].column = column
|
|
153
|
+
itemDataArray.value[index].row = row
|
|
154
|
+
itemDataArray.value[index].top = top
|
|
155
|
+
itemDataArray.value[index].bottom = bottom
|
|
156
|
+
itemDataArray.value[index].translateY = translateY
|
|
157
|
+
|
|
158
|
+
// Set the CSS custom properties
|
|
159
|
+
itemEl.style.setProperty("--_item-height", `${contentHeight}px`)
|
|
160
|
+
itemEl.style.setProperty("--_translate-y", `${translateY}px`)
|
|
161
|
+
itemEl.style.setProperty("--_opacity", "1")
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Log the complete item data array when calculations are complete
|
|
165
|
+
// console.log("📊 Item data array:", itemDataArray.value)
|
|
166
|
+
|
|
167
|
+
// Mark setup as complete
|
|
168
|
+
isSetupComplete.value = true
|
|
169
|
+
} else {
|
|
170
|
+
// Reset setup state for single column
|
|
171
|
+
isSetupComplete.value = false
|
|
172
|
+
|
|
173
|
+
// Reset item data array
|
|
174
|
+
itemDataArray.value = []
|
|
175
|
+
|
|
176
|
+
// Single column: reset to normal flow
|
|
177
|
+
gridItemsRefs.value.forEach((itemEl) => {
|
|
178
|
+
if (itemEl) {
|
|
179
|
+
itemEl.style.removeProperty("--_opacity")
|
|
180
|
+
itemEl.style.removeProperty("--_item-height")
|
|
181
|
+
itemEl.style.removeProperty("--_translate-y")
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
gridWrapper.value?.style.removeProperty("--_wrapper-height")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
useResizeObserver(gridWrapper, async () => {
|
|
189
|
+
// console.log("useResizeObserver triggered")
|
|
190
|
+
// itemDataArray.value = [] // Clear previous data
|
|
191
|
+
// await useSleep(100)
|
|
192
|
+
// console.log("useResizeObserver after sleep")
|
|
193
|
+
nextTick(() => updateGrid())
|
|
194
|
+
|
|
195
|
+
// requestAnimationFrame(() => {
|
|
196
|
+
// updateGrid()
|
|
197
|
+
// nextTick(() => updateGrid())
|
|
198
|
+
// })
|
|
199
|
+
|
|
200
|
+
// Add a small delay to ensure DOM is fully rendered and measured
|
|
201
|
+
// setTimeout(() => {
|
|
202
|
+
// nextTick(() => updateGrid())
|
|
203
|
+
// }, 100)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
onMounted(() => {
|
|
207
|
+
// Add a small delay to ensure DOM is fully rendered and measured
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
nextTick(() => updateGrid())
|
|
210
|
+
}, 100)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
watch(
|
|
214
|
+
() => props.fixedWidth,
|
|
215
|
+
() => {
|
|
216
|
+
updateGrid()
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
watch(
|
|
221
|
+
() => props.gridData,
|
|
222
|
+
() => {
|
|
223
|
+
// nextTick(() => updateGrid())
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
watch(
|
|
228
|
+
() => width.value,
|
|
229
|
+
(newWidth) => {
|
|
230
|
+
if (newWidth > 0) {
|
|
231
|
+
// nextTick(() => updateGrid())
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
watch(
|
|
237
|
+
() => props.styleClassPassthrough,
|
|
238
|
+
() => {
|
|
239
|
+
resetElementClasses(props.styleClassPassthrough)
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
</script>
|
|
243
|
+
|
|
244
|
+
<style lang="css">
|
|
245
|
+
.masonry-grid-ordered {
|
|
246
|
+
--_transition-duration: 0.3s;
|
|
247
|
+
|
|
248
|
+
.masonry-grid-ordered-wrapper {
|
|
249
|
+
display: grid;
|
|
250
|
+
gap: v-bind(gapStr);
|
|
251
|
+
grid-template-columns: repeat(auto-fit, minmax(v-bind(minTileWidthStr), 1fr));
|
|
252
|
+
|
|
253
|
+
height: var(--_wrapper-height, auto);
|
|
254
|
+
|
|
255
|
+
&.multiple-cols {
|
|
256
|
+
.masonry-grid-ordered-item {
|
|
257
|
+
height: var(--_item-height, auto);
|
|
258
|
+
opacity: var(--_opacity, 1);
|
|
259
|
+
transform: translateY(var(--_translate-y, 0px));
|
|
260
|
+
transition: transform var(--_transition-duration) ease;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
v-for="(navGroup, groupKey) in responsiveNavLinks"
|
|
6
6
|
:key="groupKey"
|
|
7
7
|
class="main-navigation-list"
|
|
8
|
-
:ref="(el:
|
|
8
|
+
:ref="(el: Element | ComponentPublicInstance | null) => setNavRef(String(groupKey), el as HTMLUListElement | null)"
|
|
9
9
|
>
|
|
10
10
|
<li
|
|
11
11
|
v-for="(link, localIndex) in navGroup"
|
|
@@ -263,8 +263,8 @@ const mainNavigationState = ref<ResponsiveHeaderState>({
|
|
|
263
263
|
|
|
264
264
|
const navRefs = ref<Record<string, HTMLUListElement | null>>({})
|
|
265
265
|
|
|
266
|
-
const setNavRef = (key: string, el:
|
|
267
|
-
navRefs.value[key] = el
|
|
266
|
+
const setNavRef = (key: string, el: Element | ComponentPublicInstance | null) => {
|
|
267
|
+
navRefs.value[key] = el as HTMLUListElement | null
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
const navigationWrapperRects = computed({
|
package/app/types/index.ts
CHANGED
|
@@ -4,3 +4,4 @@ export * from "../components/carousel-basic/CarouselBasic.vue"
|
|
|
4
4
|
export * from "../components/image-galleries/SliderGallery.vue"
|
|
5
5
|
export * from "../components/display-toast/DisplayToast.vue"
|
|
6
6
|
export * from "../components/alert-mask/AlertMaskCore.vue"
|
|
7
|
+
export * from "../components/canvas-switcher/CanvasSwitcher.vue"
|
package/package.json
CHANGED