@wyxos/vibe 1.6.29 → 2.0.2
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/README.md +29 -287
- package/lib/index.cjs +1 -0
- package/lib/index.js +795 -1791
- package/lib/logo-dark.svg +36 -36
- package/lib/logo-light.svg +29 -29
- package/lib/logo.svg +32 -32
- package/lib/manifest.json +1 -1
- package/package.json +82 -96
- package/LICENSE +0 -21
- package/lib/vibe.css +0 -1
- package/lib/vite.svg +0 -1
- package/src/App.vue +0 -35
- package/src/Masonry.vue +0 -1030
- package/src/archive/App.vue +0 -96
- package/src/archive/InfiniteMansonry.spec.ts +0 -10
- package/src/archive/InfiniteMasonry.vue +0 -218
- package/src/assets/vue.svg +0 -1
- package/src/calculateLayout.ts +0 -194
- package/src/components/CodeTabs.vue +0 -158
- package/src/components/MasonryItem.vue +0 -499
- package/src/components/examples/BasicExample.vue +0 -46
- package/src/components/examples/CustomItemExample.vue +0 -87
- package/src/components/examples/HeaderFooterExample.vue +0 -79
- package/src/components/examples/ManualInitExample.vue +0 -78
- package/src/components/examples/SwipeModeExample.vue +0 -40
- package/src/createMasonryTransitions.ts +0 -176
- package/src/main.ts +0 -6
- package/src/masonryUtils.ts +0 -96
- package/src/pages.json +0 -36402
- package/src/router/index.ts +0 -20
- package/src/style.css +0 -32
- package/src/types.ts +0 -101
- package/src/useMasonryDimensions.ts +0 -59
- package/src/useMasonryItems.ts +0 -231
- package/src/useMasonryLayout.ts +0 -164
- package/src/useMasonryPagination.ts +0 -539
- package/src/useMasonryScroll.ts +0 -61
- package/src/useMasonryVirtualization.ts +0 -140
- package/src/useSwipeMode.ts +0 -233
- package/src/utils/errorHandler.ts +0 -8
- package/src/views/Examples.vue +0 -323
- package/src/views/Home.vue +0 -321
- package/toggle-link.mjs +0 -92
package/src/archive/App.vue
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
<!--<script setup>-->
|
|
2
|
-
<!--import InfiniteMasonry from "./components/InfiniteMasonry.vue";-->
|
|
3
|
-
<!--import {nextTick, onMounted, ref} from "vue";-->
|
|
4
|
-
<!--import pages from './pages.json'-->
|
|
5
|
-
|
|
6
|
-
<!--const scrollDetails = ref({-->
|
|
7
|
-
<!-- position: 0,-->
|
|
8
|
-
<!-- direction: 'down',-->
|
|
9
|
-
<!-- isEnd: false,-->
|
|
10
|
-
<!-- isStart: true,-->
|
|
11
|
-
<!-- hasShortColumn: false,-->
|
|
12
|
-
<!--});-->
|
|
13
|
-
|
|
14
|
-
<!--const items = ref([]);-->
|
|
15
|
-
|
|
16
|
-
<!--const count = ref(30);-->
|
|
17
|
-
|
|
18
|
-
<!--const scroller = ref();-->
|
|
19
|
-
|
|
20
|
-
<!--const pageIndex = ref(0);-->
|
|
21
|
-
|
|
22
|
-
<!--const isLoading = ref(false);-->
|
|
23
|
-
|
|
24
|
-
<!--const updateItems = (action) => {-->
|
|
25
|
-
<!-- setTimeout(async () => {-->
|
|
26
|
-
<!-- if (action === 'add') {-->
|
|
27
|
-
<!-- if(items.value.length > 3){-->
|
|
28
|
-
<!-- // remove the first page from items-->
|
|
29
|
-
<!-- items.value.splice(0, 1);-->
|
|
30
|
-
<!-- }-->
|
|
31
|
-
|
|
32
|
-
<!-- items.value.push(pages[pageIndex.value]);-->
|
|
33
|
-
|
|
34
|
-
<!-- pageIndex.value = pageIndex.value + 1;-->
|
|
35
|
-
|
|
36
|
-
<!-- await nextTick()-->
|
|
37
|
-
|
|
38
|
-
<!-- isLoading.value = false;-->
|
|
39
|
-
<!-- } else {-->
|
|
40
|
-
<!-- items.value.splice(-count.value);-->
|
|
41
|
-
|
|
42
|
-
<!-- isLoading.value = false;-->
|
|
43
|
-
<!-- }-->
|
|
44
|
-
<!-- }, 1000)-->
|
|
45
|
-
<!--}-->
|
|
46
|
-
|
|
47
|
-
<!--const onScroll = (attributes) => {-->
|
|
48
|
-
<!-- scrollDetails.value = attributes-->
|
|
49
|
-
|
|
50
|
-
<!-- if (autoLoad.value && attributes.hasShortColumn && !isLoading.value) {-->
|
|
51
|
-
<!-- isLoading.value = true;-->
|
|
52
|
-
|
|
53
|
-
<!-- updateItems('add');-->
|
|
54
|
-
<!-- }-->
|
|
55
|
-
<!--}-->
|
|
56
|
-
|
|
57
|
-
<!--const autoLoad = ref(true);-->
|
|
58
|
-
|
|
59
|
-
<!--onMounted(async () => {-->
|
|
60
|
-
<!-- setTimeout(() => {-->
|
|
61
|
-
<!-- items.value = [pages[pageIndex.value]]-->
|
|
62
|
-
|
|
63
|
-
<!-- pageIndex.value = pageIndex.value + 1;-->
|
|
64
|
-
<!-- }, 2000)-->
|
|
65
|
-
<!--})-->
|
|
66
|
-
<!--</script>-->
|
|
67
|
-
|
|
68
|
-
<!--<template>-->
|
|
69
|
-
<!-- <main class="flex flex-col items-center p-4 bg-slate-100 h-screen overflow-hidden">-->
|
|
70
|
-
<!-- <header class="sticky top-0 z-10 bg-slate-100 w-full p-4 flex flex-col items-center gap-4">-->
|
|
71
|
-
<!-- <h1 class="text-2xl font-semibold mb-4">Vue Infinite Masonry</h1>-->
|
|
72
|
-
|
|
73
|
-
<!-- <p class="text-sm text-gray-500 text-center mb-4">-->
|
|
74
|
-
<!-- 🚀 Built by <a href="https://wyxos.com" target="_blank" class="underline hover:text-black">wyxos.com</a> •-->
|
|
75
|
-
<!-- 💾 <a href="https://github.com/wyxos/vue-infinite-masonry" target="_blank" class="underline hover:text-black">Source on GitHub</a>-->
|
|
76
|
-
<!-- </p>-->
|
|
77
|
-
|
|
78
|
-
<!-- <div class="flex flex-col md:flex-row gap-4 items-center">-->
|
|
79
|
-
<!-- <p>Scroll {{ scrollDetails }}</p>-->
|
|
80
|
-
|
|
81
|
-
<!-- <p>Page: {{ pageIndex }}</p>-->
|
|
82
|
-
|
|
83
|
-
<!-- <p>Pages in array {{ items.length }}</p>-->
|
|
84
|
-
|
|
85
|
-
<!-- </div>-->
|
|
86
|
-
<!-- </header>-->
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<!-- <infinite-masonry-->
|
|
90
|
-
<!-- ref="scroller"-->
|
|
91
|
-
<!-- v-model="items"-->
|
|
92
|
-
<!-- @scroll="onScroll"-->
|
|
93
|
-
<!-- :options="{ gutterY: 50 }"-->
|
|
94
|
-
<!-- />-->
|
|
95
|
-
<!-- </main>-->
|
|
96
|
-
<!--</template>-->
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import {computed, nextTick, onMounted, onUnmounted, ref, watchEffect} from "vue";
|
|
3
|
-
|
|
4
|
-
const scrollPosition = ref(0);
|
|
5
|
-
const scrollDirection = ref('down');
|
|
6
|
-
|
|
7
|
-
const defaultOptions = {
|
|
8
|
-
sizes: {
|
|
9
|
-
1: {min: 0,},
|
|
10
|
-
2: {min: 401,},
|
|
11
|
-
4: {min: 801,},
|
|
12
|
-
6: {min: 1201,},
|
|
13
|
-
8: {min: 1601,},
|
|
14
|
-
10: {min: 2001,},
|
|
15
|
-
},
|
|
16
|
-
gutterX: 10,
|
|
17
|
-
gutterY: 10,
|
|
18
|
-
cellPadding: {
|
|
19
|
-
top: 0,
|
|
20
|
-
bottom: 0,
|
|
21
|
-
left: 0,
|
|
22
|
-
right: 0
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const mergedOptions = computed(() => ({
|
|
27
|
-
...defaultOptions,
|
|
28
|
-
...props.options,
|
|
29
|
-
sizes: props.options?.sizes ?? defaultOptions.sizes
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const props = defineProps({
|
|
34
|
-
modelValue: {
|
|
35
|
-
type: Array,
|
|
36
|
-
required: true
|
|
37
|
-
},
|
|
38
|
-
options: {
|
|
39
|
-
type: Object,
|
|
40
|
-
default: () => ({})
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const columnsCount = ref(7);
|
|
45
|
-
|
|
46
|
-
const internalColumnHeights = ref([]);
|
|
47
|
-
|
|
48
|
-
const maximumHeight = computed(() => Math.max(...internalColumnHeights.value));
|
|
49
|
-
|
|
50
|
-
const container = ref(null);
|
|
51
|
-
|
|
52
|
-
const layouts = ref([]); // contains { id, top, left, width, height, src }
|
|
53
|
-
|
|
54
|
-
watchEffect(() => {
|
|
55
|
-
if (!container.value) return;
|
|
56
|
-
|
|
57
|
-
const scrollbarWidth = getScrollbarWidth(); // ← add this
|
|
58
|
-
const containerWidth = container.value.offsetWidth - scrollbarWidth;
|
|
59
|
-
const totalGutterX = (columnsCount.value - 1) * mergedOptions.value.gutterX;
|
|
60
|
-
const colWidth = Math.floor((containerWidth - totalGutterX) / columnsCount.value);
|
|
61
|
-
const colHeights = Array(columnsCount.value).fill(0);
|
|
62
|
-
|
|
63
|
-
const flatItems = props.modelValue.flatMap(p => p.items);
|
|
64
|
-
layouts.value = flatItems.map((item, index) => {
|
|
65
|
-
const columnIndex = index % columnsCount.value;
|
|
66
|
-
const scaledHeight = Math.round((item.height / item.width) * colWidth);
|
|
67
|
-
const top = colHeights[columnIndex];
|
|
68
|
-
const left = columnIndex * (colWidth + mergedOptions.value.gutterX);
|
|
69
|
-
|
|
70
|
-
// update cumulative column height
|
|
71
|
-
colHeights[columnIndex] += scaledHeight + mergedOptions.value.gutterY;
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
...item,
|
|
75
|
-
width: colWidth,
|
|
76
|
-
height: scaledHeight,
|
|
77
|
-
top,
|
|
78
|
-
left
|
|
79
|
-
};
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
internalColumnHeights.value = colHeights;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const visibleItems = computed(() => {
|
|
86
|
-
const scroll = scrollPosition.value;
|
|
87
|
-
const viewHeight = container.value?.offsetHeight || 0;
|
|
88
|
-
|
|
89
|
-
return layouts.value.filter(item => {
|
|
90
|
-
return (
|
|
91
|
-
item.top + item.height >= scroll - 200 &&
|
|
92
|
-
item.top <= scroll + viewHeight + 200
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const getScrollbarWidth = () => {
|
|
98
|
-
const outer = document.createElement('div');
|
|
99
|
-
outer.style.visibility = 'hidden';
|
|
100
|
-
outer.style.overflow = 'scroll';
|
|
101
|
-
outer.style.msOverflowStyle = 'scrollbar'; // for IE
|
|
102
|
-
outer.style.position = 'absolute';
|
|
103
|
-
outer.style.top = '-9999px';
|
|
104
|
-
outer.style.width = '100px';
|
|
105
|
-
document.body.appendChild(outer);
|
|
106
|
-
|
|
107
|
-
const inner = document.createElement('div');
|
|
108
|
-
inner.style.width = '100%';
|
|
109
|
-
outer.appendChild(inner);
|
|
110
|
-
|
|
111
|
-
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
|
112
|
-
|
|
113
|
-
document.body.removeChild(outer);
|
|
114
|
-
return scrollbarWidth;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const emit = defineEmits(['update:modelValue', 'scroll']);
|
|
118
|
-
|
|
119
|
-
const onScroll = () => {
|
|
120
|
-
const el = container.value;
|
|
121
|
-
if (!el) return;
|
|
122
|
-
|
|
123
|
-
const scroll = el.scrollTop;
|
|
124
|
-
const viewHeight = el.clientHeight;
|
|
125
|
-
const contentHeight = el.scrollHeight;
|
|
126
|
-
|
|
127
|
-
const threshold = 50; // pixels from bottom (you can tweak this)
|
|
128
|
-
|
|
129
|
-
scrollDirection.value = scroll > scrollPosition.value ? 'down' : 'up';
|
|
130
|
-
scrollPosition.value = scroll;
|
|
131
|
-
|
|
132
|
-
const isEnd = scroll + viewHeight >= contentHeight - threshold;
|
|
133
|
-
const isStart = scroll <= threshold;
|
|
134
|
-
|
|
135
|
-
const viewportBottom = scroll + viewHeight;
|
|
136
|
-
const hasShortColumn = internalColumnHeights.value.some(height => height < viewportBottom - 50);
|
|
137
|
-
|
|
138
|
-
emit('scroll', {
|
|
139
|
-
position: scroll,
|
|
140
|
-
direction: scrollDirection.value,
|
|
141
|
-
isEnd,
|
|
142
|
-
isStart,
|
|
143
|
-
hasShortColumn
|
|
144
|
-
});
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const onResize = () => {
|
|
148
|
-
const scrollbarWidth = getScrollbarWidth();
|
|
149
|
-
const containerWidth = container.value.offsetWidth - scrollbarWidth;
|
|
150
|
-
columnsCount.value = 0;
|
|
151
|
-
|
|
152
|
-
const sizes = mergedOptions.value.sizes;
|
|
153
|
-
|
|
154
|
-
const sortedSizes = Object.entries(sizes).sort(
|
|
155
|
-
(a, b) => a[1].min - b[1].min
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
for (const [columns, { min }] of sortedSizes) {
|
|
159
|
-
if (containerWidth >= min) {
|
|
160
|
-
columnsCount.value = Number(columns);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const getCellPosition = (item) => {
|
|
166
|
-
return {
|
|
167
|
-
top: item.top + 'px',
|
|
168
|
-
left: item.left + 'px',
|
|
169
|
-
width: item.width + 'px',
|
|
170
|
-
height: item.height + 'px'
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const remove = () => {
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const restore = () => {
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
defineExpose({
|
|
183
|
-
remove,
|
|
184
|
-
restore
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
onMounted(async () => {
|
|
188
|
-
await nextTick(); // wait for DOM to render and size
|
|
189
|
-
onResize(); // now has proper width
|
|
190
|
-
container.value?.addEventListener('scroll', onScroll);
|
|
191
|
-
window.addEventListener('resize', onResize);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
onUnmounted(() => {
|
|
195
|
-
if (container.value) {
|
|
196
|
-
container.value.removeEventListener('scroll', onScroll);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
window.removeEventListener('resize', onResize);
|
|
200
|
-
})
|
|
201
|
-
</script>
|
|
202
|
-
|
|
203
|
-
<template>
|
|
204
|
-
<div ref="container" class="overflow-auto flex-1 h-full w-full">
|
|
205
|
-
<div :style="{ height: `${maximumHeight}px` }" class="relative w-full">
|
|
206
|
-
<template v-for="item in visibleItems" :key="item.id">
|
|
207
|
-
<slot name="cell" :item="item" :get-cell-position="getCellPosition">
|
|
208
|
-
<div
|
|
209
|
-
class="absolute bg-slate-200 rounded-lg shadow-lg"
|
|
210
|
-
:style="getCellPosition(item)"
|
|
211
|
-
>
|
|
212
|
-
<img :src="item.src" class="w-full h-auto"/>
|
|
213
|
-
</div>
|
|
214
|
-
</slot>
|
|
215
|
-
</template>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
</template>
|
package/src/assets/vue.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
package/src/calculateLayout.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import type {LayoutOptions, MasonryItem, ProcessedMasonryItem} from './types'
|
|
2
|
-
|
|
3
|
-
let __cachedScrollbarWidth: number | null = null
|
|
4
|
-
|
|
5
|
-
function getScrollbarWidth(): number {
|
|
6
|
-
if (__cachedScrollbarWidth != null) return __cachedScrollbarWidth
|
|
7
|
-
const div = document.createElement('div')
|
|
8
|
-
div.style.visibility = 'hidden'
|
|
9
|
-
div.style.overflow = 'scroll'
|
|
10
|
-
;(div.style as any).msOverflowStyle = 'scrollbar'
|
|
11
|
-
div.style.width = '100px'
|
|
12
|
-
div.style.height = '100px'
|
|
13
|
-
document.body.appendChild(div)
|
|
14
|
-
|
|
15
|
-
const inner = document.createElement('div')
|
|
16
|
-
inner.style.width = '100%'
|
|
17
|
-
div.appendChild(inner)
|
|
18
|
-
|
|
19
|
-
const scrollbarWidth = div.offsetWidth - inner.offsetWidth
|
|
20
|
-
document.body.removeChild(div)
|
|
21
|
-
__cachedScrollbarWidth = scrollbarWidth
|
|
22
|
-
return scrollbarWidth
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default function calculateLayout(
|
|
26
|
-
items: MasonryItem[],
|
|
27
|
-
container: HTMLElement,
|
|
28
|
-
columnCount: number,
|
|
29
|
-
options: LayoutOptions = {}
|
|
30
|
-
): ProcessedMasonryItem[] {
|
|
31
|
-
const {
|
|
32
|
-
gutterX = 0,
|
|
33
|
-
gutterY = 0,
|
|
34
|
-
header = 0,
|
|
35
|
-
footer = 0,
|
|
36
|
-
paddingLeft = 0,
|
|
37
|
-
paddingRight = 0,
|
|
38
|
-
sizes = {
|
|
39
|
-
base: 1,
|
|
40
|
-
sm: 2,
|
|
41
|
-
md: 3,
|
|
42
|
-
lg: 4,
|
|
43
|
-
xl: 5,
|
|
44
|
-
'2xl': 6
|
|
45
|
-
},
|
|
46
|
-
placement = 'masonry'
|
|
47
|
-
} = options
|
|
48
|
-
|
|
49
|
-
let cssPaddingLeft = 0
|
|
50
|
-
let cssPaddingRight = 0
|
|
51
|
-
try {
|
|
52
|
-
if (container && container.nodeType === 1 && typeof window !== 'undefined' && window.getComputedStyle) {
|
|
53
|
-
const styles = window.getComputedStyle(container)
|
|
54
|
-
cssPaddingLeft = parseFloat(styles.paddingLeft) || 0
|
|
55
|
-
cssPaddingRight = parseFloat(styles.paddingRight) || 0
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// noop
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const effectivePaddingLeft = (paddingLeft || 0) + cssPaddingLeft
|
|
62
|
-
const effectivePaddingRight = (paddingRight || 0) + cssPaddingRight
|
|
63
|
-
|
|
64
|
-
const measuredScrollbarWidth = container.offsetWidth - container.clientWidth
|
|
65
|
-
const scrollbarWidth = measuredScrollbarWidth > 0 ? measuredScrollbarWidth + 2 : getScrollbarWidth() + 2
|
|
66
|
-
|
|
67
|
-
const usableWidth = container.offsetWidth - scrollbarWidth - effectivePaddingLeft - effectivePaddingRight
|
|
68
|
-
const totalGutterX = gutterX * (columnCount - 1)
|
|
69
|
-
const columnWidth = Math.floor((usableWidth - totalGutterX) / columnCount)
|
|
70
|
-
|
|
71
|
-
const baseHeights = items.map((item) => {
|
|
72
|
-
const originalWidth = item.width
|
|
73
|
-
const originalHeight = item.height
|
|
74
|
-
const imageHeight = Math.round((columnWidth * originalHeight) / originalWidth)
|
|
75
|
-
return imageHeight + footer + header
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
if (placement === 'sequential-balanced') {
|
|
79
|
-
const n = baseHeights.length
|
|
80
|
-
if (n === 0) return []
|
|
81
|
-
|
|
82
|
-
const addWithGutter = (currentSum: number, itemsInGroup: number, nextHeight: number) => {
|
|
83
|
-
return currentSum + (itemsInGroup > 0 ? gutterY : 0) + nextHeight
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let low = Math.max(...baseHeights)
|
|
87
|
-
let high = baseHeights.reduce((sum, h) => sum + h, 0) + gutterY * Math.max(0, n - 1)
|
|
88
|
-
|
|
89
|
-
const canPartition = (cap: number) => {
|
|
90
|
-
let groups = 1
|
|
91
|
-
let sum = 0
|
|
92
|
-
let count = 0
|
|
93
|
-
for (let i = 0; i < n; i++) {
|
|
94
|
-
const h = baseHeights[i]
|
|
95
|
-
const next = addWithGutter(sum, count, h)
|
|
96
|
-
if (next <= cap) {
|
|
97
|
-
sum = next
|
|
98
|
-
count++
|
|
99
|
-
} else {
|
|
100
|
-
groups++
|
|
101
|
-
sum = h
|
|
102
|
-
count = 1
|
|
103
|
-
if (h > cap) return false
|
|
104
|
-
if (groups > columnCount) return false
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return groups <= columnCount
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
while (low < high) {
|
|
111
|
-
const mid = Math.floor((low + high) / 2)
|
|
112
|
-
if (canPartition(mid)) high = mid
|
|
113
|
-
else low = mid + 1
|
|
114
|
-
}
|
|
115
|
-
const cap = high
|
|
116
|
-
|
|
117
|
-
const starts = new Array<number>(columnCount).fill(0)
|
|
118
|
-
let groupIndex = columnCount - 1
|
|
119
|
-
let sum = 0
|
|
120
|
-
let count = 0
|
|
121
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
122
|
-
const h = baseHeights[i]
|
|
123
|
-
const needAtLeast = i < groupIndex
|
|
124
|
-
const canFit = addWithGutter(sum, count, h) <= cap
|
|
125
|
-
if (!canFit || needAtLeast) {
|
|
126
|
-
starts[groupIndex] = i + 1
|
|
127
|
-
groupIndex--
|
|
128
|
-
sum = h
|
|
129
|
-
count = 1
|
|
130
|
-
} else {
|
|
131
|
-
sum = addWithGutter(sum, count, h)
|
|
132
|
-
count++
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
starts[0] = 0
|
|
136
|
-
|
|
137
|
-
const processedItems: ProcessedMasonryItem[] = []
|
|
138
|
-
const tops = new Array<number>(columnCount).fill(0)
|
|
139
|
-
for (let col = 0; col < columnCount; col++) {
|
|
140
|
-
const start = starts[col]
|
|
141
|
-
const end = col + 1 < columnCount ? starts[col + 1] : n
|
|
142
|
-
const left = col * (columnWidth + gutterX)
|
|
143
|
-
for (let i = start; i < end; i++) {
|
|
144
|
-
const item = items[i]
|
|
145
|
-
const newItem: ProcessedMasonryItem = {
|
|
146
|
-
...(item as any),
|
|
147
|
-
columnWidth,
|
|
148
|
-
imageHeight: 0,
|
|
149
|
-
columnHeight: 0,
|
|
150
|
-
left: 0,
|
|
151
|
-
top: 0
|
|
152
|
-
}
|
|
153
|
-
newItem.imageHeight = baseHeights[i] - (footer + header)
|
|
154
|
-
newItem.columnHeight = baseHeights[i]
|
|
155
|
-
newItem.left = left
|
|
156
|
-
newItem.top = tops[col]
|
|
157
|
-
tops[col] += newItem.columnHeight + (i + 1 < end ? gutterY : 0)
|
|
158
|
-
processedItems.push(newItem)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return processedItems
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const columnHeights = new Array<number>(columnCount).fill(0)
|
|
165
|
-
const processedItems: ProcessedMasonryItem[] = []
|
|
166
|
-
|
|
167
|
-
for (let index = 0; index < items.length; index++) {
|
|
168
|
-
const item = items[index]
|
|
169
|
-
const newItem: ProcessedMasonryItem = {
|
|
170
|
-
...(item as any),
|
|
171
|
-
columnWidth: 0,
|
|
172
|
-
imageHeight: 0,
|
|
173
|
-
columnHeight: 0,
|
|
174
|
-
left: 0,
|
|
175
|
-
top: 0
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const col = columnHeights.indexOf(Math.min(...columnHeights))
|
|
179
|
-
const originalWidth = item.width
|
|
180
|
-
const originalHeight = item.height
|
|
181
|
-
|
|
182
|
-
newItem.columnWidth = columnWidth
|
|
183
|
-
newItem.left = col * (columnWidth + gutterX)
|
|
184
|
-
newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth)
|
|
185
|
-
newItem.columnHeight = newItem.imageHeight + footer + header
|
|
186
|
-
newItem.top = columnHeights[col]
|
|
187
|
-
|
|
188
|
-
columnHeights[col] += newItem.columnHeight + gutterY
|
|
189
|
-
|
|
190
|
-
processedItems.push(newItem)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return processedItems
|
|
194
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="bg-slate-900 rounded-lg overflow-hidden shadow-xl">
|
|
3
|
-
<!-- Tabs -->
|
|
4
|
-
<div class="flex items-center gap-1 bg-slate-800/50 px-4 py-2 border-b border-slate-700">
|
|
5
|
-
<button
|
|
6
|
-
v-for="tab in tabs"
|
|
7
|
-
:key="tab"
|
|
8
|
-
@click="activeTab = tab"
|
|
9
|
-
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
|
|
10
|
-
:class="activeTab === tab
|
|
11
|
-
? 'bg-slate-700 text-white'
|
|
12
|
-
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'"
|
|
13
|
-
>
|
|
14
|
-
{{ tab.toUpperCase() }}
|
|
15
|
-
</button>
|
|
16
|
-
<div class="flex-1"></div>
|
|
17
|
-
<button
|
|
18
|
-
@click="copyCode"
|
|
19
|
-
class="px-3 py-1.5 text-xs font-medium text-slate-400 hover:text-white hover:bg-slate-700 rounded-md transition-colors flex items-center gap-2"
|
|
20
|
-
:title="copied ? 'Copied!' : 'Copy code'"
|
|
21
|
-
>
|
|
22
|
-
<i :class="copied ? 'fas fa-check' : 'fas fa-copy'"></i>
|
|
23
|
-
<span>{{ copied ? 'Copied' : 'Copy' }}</span>
|
|
24
|
-
</button>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<!-- Code Content -->
|
|
28
|
-
<div class="relative">
|
|
29
|
-
<pre class="p-4 overflow-x-auto text-sm"><code :class="`language-${activeTab} hljs`" v-html="highlightedCode"></code></pre>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</template>
|
|
33
|
-
|
|
34
|
-
<script setup lang="ts">
|
|
35
|
-
import { ref, computed, watch } from 'vue'
|
|
36
|
-
import hljs from 'highlight.js/lib/core'
|
|
37
|
-
import javascript from 'highlight.js/lib/languages/javascript'
|
|
38
|
-
import xml from 'highlight.js/lib/languages/xml'
|
|
39
|
-
import css from 'highlight.js/lib/languages/css'
|
|
40
|
-
import 'highlight.js/styles/tokyo-night-dark.css'
|
|
41
|
-
|
|
42
|
-
// Register languages
|
|
43
|
-
hljs.registerLanguage('javascript', javascript)
|
|
44
|
-
hljs.registerLanguage('js', javascript)
|
|
45
|
-
hljs.registerLanguage('xml', xml)
|
|
46
|
-
hljs.registerLanguage('html', xml)
|
|
47
|
-
hljs.registerLanguage('vue', xml) // Vue uses XML/HTML highlighting
|
|
48
|
-
hljs.registerLanguage('css', css)
|
|
49
|
-
|
|
50
|
-
const props = defineProps<{
|
|
51
|
-
html?: string
|
|
52
|
-
js?: string
|
|
53
|
-
vue?: string
|
|
54
|
-
css?: string
|
|
55
|
-
}>()
|
|
56
|
-
|
|
57
|
-
const activeTab = ref<'html' | 'js' | 'vue' | 'css'>('vue')
|
|
58
|
-
const copied = ref(false)
|
|
59
|
-
|
|
60
|
-
const tabs = computed(() => {
|
|
61
|
-
const available: ('html' | 'js' | 'vue' | 'css')[] = []
|
|
62
|
-
if (props.html) available.push('html')
|
|
63
|
-
if (props.js) available.push('js')
|
|
64
|
-
if (props.vue) available.push('vue')
|
|
65
|
-
if (props.css) available.push('css')
|
|
66
|
-
return available
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const currentCode = computed(() => {
|
|
70
|
-
switch (activeTab.value) {
|
|
71
|
-
case 'html': return props.html || ''
|
|
72
|
-
case 'js': return props.js || ''
|
|
73
|
-
case 'vue': return props.vue || ''
|
|
74
|
-
case 'css': return props.css || ''
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
const highlightedCode = computed(() => {
|
|
79
|
-
if (!currentCode.value) return ''
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
// Map tab names to highlight.js language names
|
|
83
|
-
const langMap: Record<string, string> = {
|
|
84
|
-
'vue': 'xml',
|
|
85
|
-
'html': 'xml',
|
|
86
|
-
'js': 'javascript',
|
|
87
|
-
'css': 'css'
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const lang = langMap[activeTab.value] || activeTab.value
|
|
91
|
-
const result = hljs.highlight(currentCode.value, { language: lang })
|
|
92
|
-
return result.value
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error('Highlighting error:', error)
|
|
95
|
-
return escapeHtml(currentCode.value)
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
function escapeHtml(text: string): string {
|
|
100
|
-
const div = document.createElement('div')
|
|
101
|
-
div.textContent = text
|
|
102
|
-
return div.innerHTML
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function copyCode() {
|
|
106
|
-
try {
|
|
107
|
-
await navigator.clipboard.writeText(currentCode.value)
|
|
108
|
-
copied.value = true
|
|
109
|
-
setTimeout(() => {
|
|
110
|
-
copied.value = false
|
|
111
|
-
}, 2000)
|
|
112
|
-
} catch (err) {
|
|
113
|
-
console.error('Failed to copy:', err)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Set initial tab to first available
|
|
118
|
-
watch(() => tabs.value, (newTabs) => {
|
|
119
|
-
if (newTabs.length > 0 && !newTabs.includes(activeTab.value)) {
|
|
120
|
-
activeTab.value = newTabs[0]
|
|
121
|
-
}
|
|
122
|
-
}, { immediate: true })
|
|
123
|
-
</script>
|
|
124
|
-
|
|
125
|
-
<style scoped>
|
|
126
|
-
code {
|
|
127
|
-
font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
128
|
-
line-height: 1.6;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
pre {
|
|
132
|
-
max-height: 500px;
|
|
133
|
-
overflow-y: auto;
|
|
134
|
-
overflow-x: auto;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
pre code {
|
|
138
|
-
display: block;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
pre::-webkit-scrollbar {
|
|
142
|
-
width: 8px;
|
|
143
|
-
height: 8px;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
pre::-webkit-scrollbar-track {
|
|
147
|
-
background: #1e293b;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
pre::-webkit-scrollbar-thumb {
|
|
151
|
-
background: #475569;
|
|
152
|
-
border-radius: 4px;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
pre::-webkit-scrollbar-thumb:hover {
|
|
156
|
-
background: #64748b;
|
|
157
|
-
}
|
|
158
|
-
</style>
|