@wyxos/vibe 1.2.1

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.
@@ -0,0 +1,218 @@
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>
@@ -0,0 +1 @@
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>
@@ -0,0 +1,55 @@
1
+ function getScrollbarWidth() {
2
+ // Create a temporary div
3
+ const div = document.createElement('div')
4
+ div.style.visibility = 'hidden'
5
+ div.style.overflow = 'scroll' // force scrollbar
6
+ div.style.msOverflowStyle = 'scrollbar' // for IE
7
+ div.style.width = '100px'
8
+ div.style.height = '100px'
9
+
10
+ // Append to body
11
+ document.body.appendChild(div)
12
+
13
+ // Create inner div and measure difference
14
+ const inner = document.createElement('div')
15
+ inner.style.width = '100%'
16
+ div.appendChild(inner)
17
+
18
+ const scrollbarWidth = div.offsetWidth - inner.offsetWidth
19
+
20
+ // Clean up
21
+ document.body.removeChild(div)
22
+
23
+ return scrollbarWidth
24
+ }
25
+
26
+ export default function calculateLayout(items, container, columnCount, gutterX = 0, gutterY = 0, header = 0, footer = 0) {
27
+ const measuredScrollbarWidth = container.offsetWidth - container.clientWidth;
28
+ const scrollbarWidth = measuredScrollbarWidth > 0 ? measuredScrollbarWidth + 2 : getScrollbarWidth() + 2;
29
+ const usableWidth = container.offsetWidth - scrollbarWidth;
30
+ const totalGutterX = gutterX * (columnCount - 1);
31
+ const columnWidth = Math.floor((usableWidth - totalGutterX) / columnCount);
32
+
33
+ const columnHeights = new Array(columnCount).fill(0);
34
+ const processedItems = [];
35
+
36
+ for (let index = 0; index < items.length; index++) {
37
+ const item = items[index];
38
+ const newItem = { ...item };
39
+
40
+ const col = index % columnCount;
41
+ const originalWidth = item.width;
42
+ const originalHeight = item.height;
43
+
44
+ newItem.columnWidth = columnWidth;
45
+ newItem.left = col * (columnWidth + gutterX);
46
+ newItem.columnHeight = Math.round((columnWidth * originalHeight) / originalWidth);
47
+ newItem.top = columnHeights[col];
48
+
49
+ columnHeights[col] += newItem.columnHeight + gutterY;
50
+
51
+ processedItems.push(newItem);
52
+ }
53
+
54
+ return processedItems;
55
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import calculateLayout from './calculateLayout'
3
+ import fixture from './pages.json'
4
+
5
+ describe('calculateLayout', () => {
6
+ it('should calculate correct layout positions for known inputs', () => {
7
+ const items = fixture[0].items.slice(0, 3) // first 3 items
8
+ const mockContainer = {
9
+ offsetWidth: 1000,
10
+ clientWidth: 980 // scrollbarWidth = 20
11
+ }
12
+ const columnCount = 3
13
+ const gutterX = 10
14
+ const gutterY = 10
15
+
16
+ const scrollbarWidth = mockContainer.offsetWidth - mockContainer.clientWidth
17
+ const usableWidth = mockContainer.offsetWidth - scrollbarWidth
18
+ const totalGutterX = gutterX * (columnCount - 1)
19
+ const expectedColumnWidth = Math.floor((usableWidth - totalGutterX) / columnCount)
20
+
21
+ const result = calculateLayout(items, mockContainer, columnCount, gutterX, gutterY)
22
+
23
+ expect(result.length).toBe(3)
24
+
25
+ result.forEach((item, i) => {
26
+ const original = items[i]
27
+ const expectedColumn = i % columnCount
28
+ const expectedLeft = expectedColumn * (expectedColumnWidth + gutterX)
29
+ const expectedHeight = Math.round((expectedColumnWidth * original.height) / original.width)
30
+
31
+ expect(item.left).toBe(expectedLeft)
32
+ expect(item.columnWidth).toBe(expectedColumnWidth)
33
+ expect(item.columnHeight).toBe(expectedHeight)
34
+ })
35
+
36
+ // Test top stacking logic: second item in same column should be placed below with gutter
37
+ const secondBatch = fixture[0].items.slice(0, 6)
38
+ const stacked = calculateLayout(secondBatch, mockContainer, columnCount, gutterX, gutterY)
39
+ for (let col = 0; col < columnCount; col++) {
40
+ const colItems = stacked.filter((_, i) => i % columnCount === col)
41
+ for (let j = 1; j < colItems.length; j++) {
42
+ expect(colItems[j].top).toBe(
43
+ colItems[j - 1].top + colItems[j - 1].columnHeight + gutterY
44
+ )
45
+ }
46
+ }
47
+ })
48
+ })
package/src/main.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue'
2
+ import './style.css'
3
+ import App from './App.vue'
4
+
5
+ createApp(App).mount('#app')