@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.
Files changed (43) hide show
  1. package/README.md +29 -287
  2. package/lib/index.cjs +1 -0
  3. package/lib/index.js +795 -1791
  4. package/lib/logo-dark.svg +36 -36
  5. package/lib/logo-light.svg +29 -29
  6. package/lib/logo.svg +32 -32
  7. package/lib/manifest.json +1 -1
  8. package/package.json +82 -96
  9. package/LICENSE +0 -21
  10. package/lib/vibe.css +0 -1
  11. package/lib/vite.svg +0 -1
  12. package/src/App.vue +0 -35
  13. package/src/Masonry.vue +0 -1030
  14. package/src/archive/App.vue +0 -96
  15. package/src/archive/InfiniteMansonry.spec.ts +0 -10
  16. package/src/archive/InfiniteMasonry.vue +0 -218
  17. package/src/assets/vue.svg +0 -1
  18. package/src/calculateLayout.ts +0 -194
  19. package/src/components/CodeTabs.vue +0 -158
  20. package/src/components/MasonryItem.vue +0 -499
  21. package/src/components/examples/BasicExample.vue +0 -46
  22. package/src/components/examples/CustomItemExample.vue +0 -87
  23. package/src/components/examples/HeaderFooterExample.vue +0 -79
  24. package/src/components/examples/ManualInitExample.vue +0 -78
  25. package/src/components/examples/SwipeModeExample.vue +0 -40
  26. package/src/createMasonryTransitions.ts +0 -176
  27. package/src/main.ts +0 -6
  28. package/src/masonryUtils.ts +0 -96
  29. package/src/pages.json +0 -36402
  30. package/src/router/index.ts +0 -20
  31. package/src/style.css +0 -32
  32. package/src/types.ts +0 -101
  33. package/src/useMasonryDimensions.ts +0 -59
  34. package/src/useMasonryItems.ts +0 -231
  35. package/src/useMasonryLayout.ts +0 -164
  36. package/src/useMasonryPagination.ts +0 -539
  37. package/src/useMasonryScroll.ts +0 -61
  38. package/src/useMasonryVirtualization.ts +0 -140
  39. package/src/useSwipeMode.ts +0 -233
  40. package/src/utils/errorHandler.ts +0 -8
  41. package/src/views/Examples.vue +0 -323
  42. package/src/views/Home.vue +0 -321
  43. package/toggle-link.mjs +0 -92
@@ -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,10 +0,0 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
- import { describe, it, expect } from 'vitest'
5
-
6
- describe('Example Test', () => {
7
- it('should pass a basic truthiness check', () => {
8
- expect(true).toBe(true)
9
- })
10
- })
@@ -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>
@@ -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>
@@ -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>