@edgedev/create-edge-app 1.1.25 → 1.1.27
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 +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1475 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +429 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/package.json +1 -1
- package/edge-components-install.sh +0 -1
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import presetWind4 from '@unocss/preset-wind4'
|
|
3
|
+
|
|
4
|
+
import initUnocssRuntime, { defineConfig } from '@unocss/runtime'
|
|
5
|
+
import DOMPurify from 'dompurify'
|
|
6
|
+
|
|
7
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
8
|
+
import { useHead } from '#imports'
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
html: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: '',
|
|
14
|
+
},
|
|
15
|
+
theme: {
|
|
16
|
+
type: Object,
|
|
17
|
+
default: () => ({}),
|
|
18
|
+
},
|
|
19
|
+
isolated: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: true,
|
|
22
|
+
},
|
|
23
|
+
comp: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: false,
|
|
26
|
+
default: null,
|
|
27
|
+
},
|
|
28
|
+
viewportMode: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: 'auto',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits(['loaded'])
|
|
35
|
+
|
|
36
|
+
const scopeId = `hc-${Math.random().toString(36).slice(2)}`
|
|
37
|
+
|
|
38
|
+
// --- UnoCSS Runtime singleton (global, one init for the whole app) ---
|
|
39
|
+
async function ensureUnoRuntime() {
|
|
40
|
+
if (typeof window === 'undefined')
|
|
41
|
+
return
|
|
42
|
+
// If already started, nothing to do.
|
|
43
|
+
if (window.__unoRuntimeStarted === true)
|
|
44
|
+
return
|
|
45
|
+
// If another component is initializing, await that shared promise.
|
|
46
|
+
if (window.__unoInitPromise) {
|
|
47
|
+
await window.__unoInitPromise
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
// Create a shared promise on window so all components can await the same init.
|
|
51
|
+
window.__unoInitPromise = (async () => {
|
|
52
|
+
// Pre-init de-dupe: keep only one style tag per Uno layer if any exist
|
|
53
|
+
const preSheets = Array.from(document.querySelectorAll('style[data-unocss-runtime-layer]'))
|
|
54
|
+
if (preSheets.length > 0) {
|
|
55
|
+
const seen = new Set()
|
|
56
|
+
preSheets.forEach((el) => {
|
|
57
|
+
const layer = el.getAttribute('data-unocss-runtime-layer') || ''
|
|
58
|
+
if (seen.has(layer))
|
|
59
|
+
el.parentNode && el.parentNode.removeChild(el)
|
|
60
|
+
else seen.add(layer)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
await initUnocssRuntime({
|
|
64
|
+
defaults: defineConfig({
|
|
65
|
+
presets: [presetWind4()],
|
|
66
|
+
shortcuts: [],
|
|
67
|
+
}),
|
|
68
|
+
observe: true,
|
|
69
|
+
})
|
|
70
|
+
// Post-init de-dupe: if multiple parallel inits slipped through, collapse to one per layer.
|
|
71
|
+
const postSheets = Array.from(document.querySelectorAll('style[data-unocss-runtime-layer]'))
|
|
72
|
+
if (postSheets.length > 0) {
|
|
73
|
+
const seen = new Set()
|
|
74
|
+
postSheets.forEach((el) => {
|
|
75
|
+
const layer = el.getAttribute('data-unocss-runtime-layer') || ''
|
|
76
|
+
if (seen.has(layer))
|
|
77
|
+
el.parentNode && el.parentNode.removeChild(el)
|
|
78
|
+
else seen.add(layer)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
window.__unoRuntimeStarted = true
|
|
82
|
+
window.__unoInitPromise = null
|
|
83
|
+
})()
|
|
84
|
+
await window.__unoInitPromise
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Global theme variables (single style tag) ---
|
|
88
|
+
function buildGlobalThemeCSS(theme) {
|
|
89
|
+
const t = normalizeTheme(theme || {})
|
|
90
|
+
const { colors, fontFamily, fontSize, borderRadius, boxShadow } = t
|
|
91
|
+
const decls = []
|
|
92
|
+
Object.entries(colors).forEach(([k, v]) => decls.push(`--color-${k}: ${Array.isArray(v) ? v[0] : v};`))
|
|
93
|
+
Object.entries(fontFamily).forEach(([k, v]) => {
|
|
94
|
+
const val = Array.isArray(v) ? v.map(x => (x.includes(' ') ? `'${x}'` : x)).join(', ') : v
|
|
95
|
+
decls.push(`--font-${k}: ${val};`)
|
|
96
|
+
})
|
|
97
|
+
Object.entries(fontSize).forEach(([k, v]) => {
|
|
98
|
+
if (Array.isArray(v)) {
|
|
99
|
+
const [size, opts] = v
|
|
100
|
+
decls.push(`--font-size-${k}: ${size};`)
|
|
101
|
+
if (opts && opts.lineHeight)
|
|
102
|
+
decls.push(`--line-height-${k}: ${opts.lineHeight};`)
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
decls.push(`--font-size-${k}: ${v};`)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
Object.entries(borderRadius).forEach(([k, v]) => decls.push(`--radius-${k}: ${v};`))
|
|
109
|
+
Object.entries(boxShadow).forEach(([k, v]) => decls.push(`--shadow-${k}: ${v};`))
|
|
110
|
+
return `:root{${decls.join('')}}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildScopedThemeCSS(theme, scopeId) {
|
|
114
|
+
const t = normalizeTheme(theme || {})
|
|
115
|
+
const { colors, fontFamily, fontSize, borderRadius, boxShadow } = t
|
|
116
|
+
const decls = []
|
|
117
|
+
Object.entries(colors).forEach(([k, v]) => decls.push(`--color-${k}: ${Array.isArray(v) ? v[0] : v};`))
|
|
118
|
+
Object.entries(fontFamily).forEach(([k, v]) => {
|
|
119
|
+
const val = Array.isArray(v) ? v.map(x => (x.includes(' ') ? `'${x}'` : x)).join(', ') : v
|
|
120
|
+
decls.push(`--font-${k}: ${val};`)
|
|
121
|
+
})
|
|
122
|
+
Object.entries(fontSize).forEach(([k, v]) => {
|
|
123
|
+
if (Array.isArray(v)) {
|
|
124
|
+
const [size, opts] = v
|
|
125
|
+
decls.push(`--font-size-${k}: ${size};`)
|
|
126
|
+
if (opts?.lineHeight)
|
|
127
|
+
decls.push(`--line-height-${k}: ${opts.lineHeight};`)
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
decls.push(`--font-size-${k}: ${v};`)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
Object.entries(borderRadius).forEach(([k, v]) => decls.push(`--radius-${k}: ${v};`))
|
|
134
|
+
Object.entries(boxShadow).forEach(([k, v]) => decls.push(`--shadow-${k}: ${v};`))
|
|
135
|
+
return `[data-theme-scope="${scopeId}"]{${decls.join('')}}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setGlobalThemeVars(theme) {
|
|
139
|
+
if (typeof window === 'undefined')
|
|
140
|
+
return
|
|
141
|
+
const sheetId = 'htmlcontent-theme-global'
|
|
142
|
+
let styleEl = document.getElementById(sheetId)
|
|
143
|
+
if (!styleEl) {
|
|
144
|
+
styleEl = document.createElement('style')
|
|
145
|
+
styleEl.id = sheetId
|
|
146
|
+
document.head.appendChild(styleEl)
|
|
147
|
+
}
|
|
148
|
+
styleEl.textContent = buildGlobalThemeCSS(theme)
|
|
149
|
+
window.__htmlcontentGlobalTheme = true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const hostEl = ref(null)
|
|
153
|
+
let hasMounted = false
|
|
154
|
+
|
|
155
|
+
function notifyLoaded() {
|
|
156
|
+
if (!import.meta.client || !hasMounted)
|
|
157
|
+
return
|
|
158
|
+
requestAnimationFrame(() => emit('loaded'))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- SSR-safe HTML: raw on server, sanitized on client ---
|
|
162
|
+
const safeHtml = computed(() => {
|
|
163
|
+
const c = props.html || ''
|
|
164
|
+
if (typeof window === 'undefined')
|
|
165
|
+
return c
|
|
166
|
+
return DOMPurify.sanitize(c, { ADD_ATTR: ['class'] })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Inject theme CSS variables into <head> for SSR + client
|
|
170
|
+
useHead(() => ({
|
|
171
|
+
style: [
|
|
172
|
+
{ id: 'htmlcontent-theme-global', children: buildScopedThemeCSS(props.theme, scopeId) },
|
|
173
|
+
],
|
|
174
|
+
}))
|
|
175
|
+
|
|
176
|
+
// --- Embla initializer (runs client-side only) ---
|
|
177
|
+
async function initEmblaCarousels(scope) {
|
|
178
|
+
if (!scope || !import.meta.client)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
const [{ default: EmblaCarousel }, { default: Autoplay }, { default: Fade }] = await Promise.all([
|
|
182
|
+
import('embla-carousel'),
|
|
183
|
+
import('embla-carousel-autoplay'),
|
|
184
|
+
import('embla-carousel-fade'),
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
const roots = scope.querySelectorAll('[data-carousel]:not([data-embla])')
|
|
188
|
+
|
|
189
|
+
roots.forEach((root) => {
|
|
190
|
+
// Options via data- attributes
|
|
191
|
+
const loop = !!root.hasAttribute('data-carousel-loop')
|
|
192
|
+
const transition = (root.getAttribute('data-carousel-transition') || 'none').toLowerCase() // 'none' | 'fade'
|
|
193
|
+
const delay = Number(root.getAttribute('data-carousel-interval')) || 5000
|
|
194
|
+
const noPause = root.hasAttribute('data-carousel-no-pause')
|
|
195
|
+
const autoplayOn = root.hasAttribute('data-carousel-autoplay')
|
|
196
|
+
const fadeDuration = Number(root.getAttribute('data-carousel-fade-duration')) || 200
|
|
197
|
+
|
|
198
|
+
const stsBase = root.getAttribute('data-carousel-slides-to-scroll')
|
|
199
|
+
const stsMd = root.getAttribute('data-carousel-slides-to-scroll-md')
|
|
200
|
+
const stsLg = root.getAttribute('data-carousel-slides-to-scroll-lg')
|
|
201
|
+
const stsXl = root.getAttribute('data-carousel-slides-to-scroll-xl')
|
|
202
|
+
const slidesToScroll = stsBase != null ? Number(stsBase) : 1
|
|
203
|
+
|
|
204
|
+
const plugins = []
|
|
205
|
+
if (autoplayOn) {
|
|
206
|
+
plugins.push(
|
|
207
|
+
Autoplay({
|
|
208
|
+
delay,
|
|
209
|
+
stopOnInteraction: !noPause,
|
|
210
|
+
stopOnMouseEnter: !noPause,
|
|
211
|
+
}),
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
if (transition === 'fade') {
|
|
215
|
+
// Pass duration to the plugin and also expose via CSS vars
|
|
216
|
+
// Ensure CSS-driven durations pick this up (covers common var names across versions)
|
|
217
|
+
root.style.setProperty('--embla-fade-duration', `${fadeDuration}ms`)
|
|
218
|
+
root.style.setProperty('--embla-duration', `${fadeDuration}ms`)
|
|
219
|
+
plugins.push(Fade({ duration: fadeDuration, easing: 'ease' }))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const options = {
|
|
223
|
+
loop,
|
|
224
|
+
container: '[data-carousel-track]',
|
|
225
|
+
align: 'start',
|
|
226
|
+
slidesToScroll,
|
|
227
|
+
breakpoints: {
|
|
228
|
+
'(min-width: 768px)': stsMd != null ? { slidesToScroll: Number(stsMd) } : undefined,
|
|
229
|
+
'(min-width: 1024px)': stsLg != null ? { slidesToScroll: Number(stsLg) } : undefined,
|
|
230
|
+
'(min-width: 1280px)': stsXl != null ? { slidesToScroll: Number(stsXl) } : undefined,
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
if (!loop)
|
|
234
|
+
options.containScroll = 'trimSnaps'
|
|
235
|
+
|
|
236
|
+
const api = EmblaCarousel(root, options, plugins)
|
|
237
|
+
|
|
238
|
+
// Force-apply fade duration on slide nodes as inline styles to override any defaults
|
|
239
|
+
if (transition === 'fade') {
|
|
240
|
+
const applyFadeTransitionStyles = () => {
|
|
241
|
+
api.slideNodes().forEach((el) => {
|
|
242
|
+
el.style.transitionProperty = 'opacity, visibility'
|
|
243
|
+
el.style.transitionDuration = `${fadeDuration}ms`
|
|
244
|
+
el.style.transitionTimingFunction = 'ease'
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
applyFadeTransitionStyles()
|
|
248
|
+
api.on('reInit', applyFadeTransitionStyles)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Wire prev/next, keeping disabled state in sync with snaps
|
|
252
|
+
const prevBtn = root.querySelector('[data-carousel-prev]')
|
|
253
|
+
const nextBtn = root.querySelector('[data-carousel-next]')
|
|
254
|
+
const setBtnStates = () => {
|
|
255
|
+
if (loop) {
|
|
256
|
+
if (prevBtn)
|
|
257
|
+
prevBtn.disabled = false
|
|
258
|
+
if (nextBtn)
|
|
259
|
+
nextBtn.disabled = false
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
if (prevBtn)
|
|
263
|
+
prevBtn.disabled = !api.canScrollPrev()
|
|
264
|
+
if (nextBtn)
|
|
265
|
+
nextBtn.disabled = !api.canScrollNext()
|
|
266
|
+
}
|
|
267
|
+
if (prevBtn) {
|
|
268
|
+
prevBtn.addEventListener('click', () => {
|
|
269
|
+
if (loop && !api.canScrollPrev()) {
|
|
270
|
+
const snaps = api.scrollSnapList()
|
|
271
|
+
api.scrollTo(snaps.length - 1)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
api.scrollPrev()
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
if (nextBtn) {
|
|
278
|
+
nextBtn.addEventListener('click', () => {
|
|
279
|
+
if (loop && !api.canScrollNext()) {
|
|
280
|
+
api.scrollTo(0)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
api.scrollNext()
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Build dots based on scroll snaps (respects slidesToScroll & breakpoints)
|
|
288
|
+
const dotsHost = root.querySelector('[data-carousel-dots]')
|
|
289
|
+
let dotButtons = []
|
|
290
|
+
const buildDots = () => {
|
|
291
|
+
if (!dotsHost)
|
|
292
|
+
return
|
|
293
|
+
dotsHost.innerHTML = ''
|
|
294
|
+
dotButtons = []
|
|
295
|
+
const snaps = api.scrollSnapList() // snap positions, not slides
|
|
296
|
+
const initial = api.selectedScrollSnap()
|
|
297
|
+
snaps.forEach((_snap, i) => {
|
|
298
|
+
const b = document.createElement('button')
|
|
299
|
+
b.type = 'button'
|
|
300
|
+
b.className = 'h-2 w-2 rounded-full bg-gray-300 aria-[current=true]:bg-gray-800'
|
|
301
|
+
b.setAttribute('aria-current', String(i === initial))
|
|
302
|
+
b.addEventListener('click', () => {
|
|
303
|
+
api.scrollTo(i)
|
|
304
|
+
})
|
|
305
|
+
dotsHost.appendChild(b)
|
|
306
|
+
dotButtons.push(b)
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
const updateDots = () => {
|
|
310
|
+
if (!dotsHost || !dotButtons.length)
|
|
311
|
+
return
|
|
312
|
+
const idx = api.selectedScrollSnap()
|
|
313
|
+
dotButtons.forEach((d, i) => {
|
|
314
|
+
d.setAttribute('aria-current', String(i === idx))
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Initial sync
|
|
319
|
+
buildDots()
|
|
320
|
+
setBtnStates()
|
|
321
|
+
updateDots()
|
|
322
|
+
|
|
323
|
+
// Keep everything in sync as selection/breakpoints change
|
|
324
|
+
api.on('select', () => {
|
|
325
|
+
setBtnStates()
|
|
326
|
+
updateDots()
|
|
327
|
+
})
|
|
328
|
+
api.on('reInit', () => {
|
|
329
|
+
buildDots() // snaps can change when slidesToScroll/breakpoints change
|
|
330
|
+
setBtnStates()
|
|
331
|
+
updateDots()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Mark initialized
|
|
335
|
+
root.setAttribute('data-embla', 'true')
|
|
336
|
+
|
|
337
|
+
// Optional: store API for cleanup if needed later
|
|
338
|
+
// root._emblaApi = api
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderSafeHtml(content) {
|
|
343
|
+
if (hostEl.value) {
|
|
344
|
+
// The HTML is already in the DOM via v-html; just (re)wire behaviors
|
|
345
|
+
initEmblaCarousels(hostEl.value)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeTheme(input = {}) {
|
|
350
|
+
const t = input || {}
|
|
351
|
+
const ext = t.extend || {}
|
|
352
|
+
return {
|
|
353
|
+
colors: ext.colors || {},
|
|
354
|
+
fontFamily: ext.fontFamily || {},
|
|
355
|
+
fontSize: ext.fontSize || {},
|
|
356
|
+
borderRadius: ext.borderRadius || {},
|
|
357
|
+
boxShadow: ext.boxShadow || {},
|
|
358
|
+
apply: (t.apply || {}),
|
|
359
|
+
slots: (t.slots || {}),
|
|
360
|
+
variants: (t.variants || {}),
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function setScopedThemeVars(scopeEl, theme) {
|
|
365
|
+
if (!scopeEl)
|
|
366
|
+
return
|
|
367
|
+
// ensure a stable scope attribute so the style can target only this block
|
|
368
|
+
if (!scopeEl.hasAttribute('data-theme-scope')) {
|
|
369
|
+
scopeEl.setAttribute('data-theme-scope', Math.random().toString(36).slice(2))
|
|
370
|
+
}
|
|
371
|
+
const scopeId = scopeEl.getAttribute('data-theme-scope')
|
|
372
|
+
|
|
373
|
+
const sheetId = `htmlcontent-theme-${scopeId}`
|
|
374
|
+
let styleEl = document.getElementById(sheetId)
|
|
375
|
+
if (!styleEl) {
|
|
376
|
+
styleEl = document.createElement('style')
|
|
377
|
+
styleEl.id = sheetId
|
|
378
|
+
document.head.appendChild(styleEl)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Build CSS custom properties from theme tokens
|
|
382
|
+
const { colors, fontFamily, fontSize, borderRadius, boxShadow } = theme
|
|
383
|
+
|
|
384
|
+
const decls = []
|
|
385
|
+
// colors
|
|
386
|
+
Object.entries(colors).forEach(([k, v]) => {
|
|
387
|
+
decls.push(`--color-${k}: ${Array.isArray(v) ? v[0] : v};`)
|
|
388
|
+
})
|
|
389
|
+
// fonts
|
|
390
|
+
Object.entries(fontFamily).forEach(([k, v]) => {
|
|
391
|
+
const val = Array.isArray(v) ? v.map(x => (x.includes(' ') ? `'${x}'` : x)).join(', ') : v
|
|
392
|
+
decls.push(`--font-${k}: ${val};`)
|
|
393
|
+
})
|
|
394
|
+
// font sizes
|
|
395
|
+
Object.entries(fontSize).forEach(([k, v]) => {
|
|
396
|
+
if (Array.isArray(v)) {
|
|
397
|
+
const [size, opts] = v
|
|
398
|
+
decls.push(`--font-size-${k}: ${size};`)
|
|
399
|
+
if (opts && opts.lineHeight)
|
|
400
|
+
decls.push(`--line-height-${k}: ${opts.lineHeight};`)
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
decls.push(`--font-size-${k}: ${v};`)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
// radii
|
|
407
|
+
Object.entries(borderRadius).forEach(([k, v]) => {
|
|
408
|
+
decls.push(`--radius-${k}: ${v};`)
|
|
409
|
+
})
|
|
410
|
+
// shadows
|
|
411
|
+
Object.entries(boxShadow).forEach(([k, v]) => {
|
|
412
|
+
decls.push(`--shadow-${k}: ${v};`)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
styleEl.textContent = `
|
|
416
|
+
[data-theme-scope="${scopeId}"]{${decls.join('')}}`
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Convert utility tokens like text-brand/bg-surface/rounded-xl/shadow-card
|
|
420
|
+
// into variable-backed arbitrary values so we don't need to mutate Uno's theme.
|
|
421
|
+
|
|
422
|
+
function toVarBackedUtilities(classList, theme) {
|
|
423
|
+
if (!classList)
|
|
424
|
+
return ''
|
|
425
|
+
const tokens = normalizeTheme(theme)
|
|
426
|
+
const colorKeys = new Set(Object.keys(tokens.colors))
|
|
427
|
+
const radiusKeys = new Set(Object.keys(tokens.borderRadius))
|
|
428
|
+
const shadowKeys = new Set(Object.keys(tokens.boxShadow))
|
|
429
|
+
|
|
430
|
+
return classList
|
|
431
|
+
.split(/\s+/)
|
|
432
|
+
.filter(Boolean)
|
|
433
|
+
.map((cls) => {
|
|
434
|
+
// colors: text-*, bg-*, border-* mapped when key exists
|
|
435
|
+
const colorMatch = /^(text|bg|border)-(.*)$/.exec(cls)
|
|
436
|
+
if (colorMatch) {
|
|
437
|
+
const [, kind, rawKey] = colorMatch
|
|
438
|
+
|
|
439
|
+
// support opacity suffix: bg-secondary/50, text-primary/80, etc.
|
|
440
|
+
let key = rawKey
|
|
441
|
+
let opacity = null
|
|
442
|
+
const alphaMatch = /^(.+)\/(\d{1,3})$/.exec(rawKey)
|
|
443
|
+
if (alphaMatch) {
|
|
444
|
+
key = alphaMatch[1]
|
|
445
|
+
opacity = alphaMatch[2]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (colorKeys.has(key)) {
|
|
449
|
+
const varRef = `var(--color-${key})`
|
|
450
|
+
|
|
451
|
+
// no /opacity → plain var()
|
|
452
|
+
if (!opacity) {
|
|
453
|
+
if (kind === 'text')
|
|
454
|
+
return `text-[${varRef}]`
|
|
455
|
+
if (kind === 'bg')
|
|
456
|
+
return `bg-[${varRef}]`
|
|
457
|
+
if (kind === 'border')
|
|
458
|
+
return `border-[${varRef}]`
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// with /opacity → use slash opacity on arbitrary value
|
|
462
|
+
if (kind === 'text')
|
|
463
|
+
return `text-[${varRef}]/${opacity}`
|
|
464
|
+
if (kind === 'bg')
|
|
465
|
+
return `bg-[${varRef}]/${opacity}`
|
|
466
|
+
if (kind === 'border')
|
|
467
|
+
return `border-[${varRef}]/${opacity}`
|
|
468
|
+
|
|
469
|
+
return cls
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return cls
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// radius
|
|
476
|
+
const radiusMatch = /^rounded-(.*)$/.exec(cls)
|
|
477
|
+
if (radiusMatch) {
|
|
478
|
+
const key = radiusMatch[1]
|
|
479
|
+
if (radiusKeys.has(key))
|
|
480
|
+
return `rounded-[var(--radius-${key})]`
|
|
481
|
+
return cls
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// shadow
|
|
485
|
+
const shadowMatch = /^shadow-(.*)$/.exec(cls)
|
|
486
|
+
if (shadowMatch) {
|
|
487
|
+
const key = shadowMatch[1]
|
|
488
|
+
if (shadowKeys.has(key))
|
|
489
|
+
return `shadow-[var(--shadow-${key})]`
|
|
490
|
+
return cls
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// font families via root apply, including custom keys like "brand"
|
|
494
|
+
if (cls === 'font-sans')
|
|
495
|
+
return 'font-[var(--font-sans)]'
|
|
496
|
+
if (cls === 'font-serif')
|
|
497
|
+
return 'font-[var(--font-serif)]'
|
|
498
|
+
if (cls === 'font-mono')
|
|
499
|
+
return 'font-[var(--font-mono)]'
|
|
500
|
+
|
|
501
|
+
const ffMatch = /^font-([\w-]+)$/.exec(cls)
|
|
502
|
+
if (ffMatch) {
|
|
503
|
+
const key = ffMatch[1]
|
|
504
|
+
if (Object.prototype.hasOwnProperty.call(tokens.fontFamily, key))
|
|
505
|
+
return `font-[var(--font-${key})]`
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return cls
|
|
509
|
+
})
|
|
510
|
+
.join(' ')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function applyThemeClasses(scopeEl, theme, variant = 'light', isolated = true) {
|
|
514
|
+
if (!scopeEl)
|
|
515
|
+
return
|
|
516
|
+
const t = normalizeTheme(theme)
|
|
517
|
+
// merge base + variant overrides for apply & slots
|
|
518
|
+
const v = (t.variants && t.variants[variant]) || {}
|
|
519
|
+
const apply = { ...(t.apply || {}), ...(v.apply || {}) }
|
|
520
|
+
const slots = JSON.parse(JSON.stringify(t.slots || {}))
|
|
521
|
+
if (v.slots) {
|
|
522
|
+
// shallow merge per slot key
|
|
523
|
+
Object.entries(v.slots).forEach(([slotKey, obj]) => {
|
|
524
|
+
slots[slotKey] = { ...(slots[slotKey] || {}), ...obj }
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Root classes
|
|
529
|
+
if (apply.root) {
|
|
530
|
+
const mapped = toVarBackedUtilities(apply.root, t)
|
|
531
|
+
if (isolated) {
|
|
532
|
+
scopeEl.className = `block-content ${mapped}`.trim()
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
const applied = (scopeEl.dataset.themeRootClasses || '').split(/\s+/).filter(Boolean)
|
|
536
|
+
applied.forEach(cls => scopeEl.classList.remove(cls))
|
|
537
|
+
const next = mapped.split(/\s+/).filter(Boolean)
|
|
538
|
+
next.forEach(cls => scopeEl.classList.add(cls))
|
|
539
|
+
scopeEl.classList.add('block-content')
|
|
540
|
+
if (next.length)
|
|
541
|
+
scopeEl.dataset.themeRootClasses = next.join(' ')
|
|
542
|
+
else
|
|
543
|
+
delete scopeEl.dataset.themeRootClasses
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Optional convenience: map a few generic applies
|
|
548
|
+
if (apply.link) {
|
|
549
|
+
scopeEl.querySelectorAll('a').forEach((el) => {
|
|
550
|
+
el.className = `${el.className} ${toVarBackedUtilities(apply.link, t)}`.trim()
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
if (apply.heading) {
|
|
554
|
+
scopeEl.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach((el) => {
|
|
555
|
+
el.className = `${el.className} ${toVarBackedUtilities(apply.heading, t)}`.trim()
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
if (apply.button) {
|
|
559
|
+
scopeEl.querySelectorAll('button,[data-theme="button"]').forEach((el) => {
|
|
560
|
+
el.className = `${el.className} ${toVarBackedUtilities(apply.button, t)}`.trim()
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
if (apply.badge) {
|
|
564
|
+
scopeEl.querySelectorAll('[data-theme="badge"]').forEach((el) => {
|
|
565
|
+
el.className = `${el.className} ${toVarBackedUtilities(apply.badge, t)}`.trim()
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Slot-based mapping via data-slot attributes
|
|
570
|
+
const mapSlot = (slotBase, obj) => {
|
|
571
|
+
if (!obj)
|
|
572
|
+
return
|
|
573
|
+
Object.entries(obj).forEach(([part, classes]) => {
|
|
574
|
+
const sel = `[data-slot="${slotBase}.${part}"]`
|
|
575
|
+
scopeEl.querySelectorAll(sel).forEach((el) => {
|
|
576
|
+
el.className = `${el.className} ${toVarBackedUtilities(classes, t)}`.trim()
|
|
577
|
+
})
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
Object.entries(slots).forEach(([slotKey, val]) => {
|
|
581
|
+
mapSlot(slotKey, val)
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Add new helper to rewrite arbitrary class tokens with responsive and state prefixes
|
|
586
|
+
const BREAKPOINT_MIN_WIDTHS = {
|
|
587
|
+
'sm': 640,
|
|
588
|
+
'md': 768,
|
|
589
|
+
'lg': 1024,
|
|
590
|
+
'xl': 1280,
|
|
591
|
+
'2xl': 1536,
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const VIEWPORT_WIDTHS = {
|
|
595
|
+
auto: null,
|
|
596
|
+
full: null,
|
|
597
|
+
large: 1280,
|
|
598
|
+
medium: 992,
|
|
599
|
+
mobile: 480,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const viewportModeToWidth = (mode) => {
|
|
603
|
+
if (!mode)
|
|
604
|
+
return null
|
|
605
|
+
if (Object.prototype.hasOwnProperty.call(VIEWPORT_WIDTHS, mode))
|
|
606
|
+
return VIEWPORT_WIDTHS[mode]
|
|
607
|
+
return null
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto') {
|
|
611
|
+
if (!scopeEl)
|
|
612
|
+
return
|
|
613
|
+
// Utility regex for Uno/Tailwind classes
|
|
614
|
+
const utilRe = /^-?([pmwhz]|px|py|pt|pr|pb|pl|mx|my|mt|mr|mb|ml|text|font|leading|tracking|bg|border|rounded|shadow|min-w|max-w|min-h|max-h|object|overflow|opacity|order|top|right|bottom|left|inset|translate|rotate|scale|skew|origin|grid|flex|items|justify|content|place|gap|space|columns|col|row|aspect|ring|outline|decoration|underline|line-through|no-underline|whitespace|break|truncate|sr-only|not-sr-only|cursor|select|duration|ease|delay|transition|animate)(-|$|\[)/
|
|
615
|
+
// Mark utility classes as important so block-level styles win over parents.
|
|
616
|
+
const importantify = (core) => {
|
|
617
|
+
if (!core || core.startsWith('!'))
|
|
618
|
+
return core
|
|
619
|
+
// Avoid importantifying custom structural classes/hooks
|
|
620
|
+
if (core === 'block-content' || core.startsWith('embla'))
|
|
621
|
+
return core
|
|
622
|
+
// If it's a typical utility or an arbitrary utility, make it important.
|
|
623
|
+
if (utilRe.test(core) || core.includes('[')) {
|
|
624
|
+
return `!${core}`
|
|
625
|
+
}
|
|
626
|
+
return core
|
|
627
|
+
}
|
|
628
|
+
const forcedWidth = viewportModeToWidth(viewportMode)
|
|
629
|
+
|
|
630
|
+
const TEXT_SIZE_RE = /^text-(xs|sm|base|lg|xl|\d+xl)$/
|
|
631
|
+
|
|
632
|
+
const mapToken = (token) => {
|
|
633
|
+
const parts = token.split(':')
|
|
634
|
+
const core = parts.pop()
|
|
635
|
+
const nakedCore = core.startsWith('!') ? core.slice(1) : core
|
|
636
|
+
|
|
637
|
+
//
|
|
638
|
+
// AUTO MODE: no breakpoint *simulation*, but we still:
|
|
639
|
+
// - map theme utilities
|
|
640
|
+
// - !important text sizes
|
|
641
|
+
// - !important breakpoint-based utilities (sm:, md:, lg:, etc.)
|
|
642
|
+
//
|
|
643
|
+
if (forcedWidth == null) {
|
|
644
|
+
let hadBreakpoint = false
|
|
645
|
+
const nextParts = []
|
|
646
|
+
|
|
647
|
+
for (const part of parts) {
|
|
648
|
+
const normalized = part.replace(/^!/, '')
|
|
649
|
+
if (Object.prototype.hasOwnProperty.call(BREAKPOINT_MIN_WIDTHS, normalized)) {
|
|
650
|
+
hadBreakpoint = true
|
|
651
|
+
}
|
|
652
|
+
nextParts.push(part)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const mappedCore = toVarBackedUtilities(core, theme)
|
|
656
|
+
const isTextSize = TEXT_SIZE_RE.test(nakedCore)
|
|
657
|
+
const shouldImportant = hadBreakpoint || isTextSize
|
|
658
|
+
const finalCore = shouldImportant ? importantify(mappedCore) : mappedCore
|
|
659
|
+
|
|
660
|
+
return [...nextParts, finalCore].filter(Boolean).join(':')
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
//
|
|
664
|
+
// SIZED MODES (mobile/medium/large/full): your existing branch stays as-is
|
|
665
|
+
//
|
|
666
|
+
let drop = false
|
|
667
|
+
let hadBreakpoint = false
|
|
668
|
+
const nextParts = []
|
|
669
|
+
|
|
670
|
+
for (const part of parts) {
|
|
671
|
+
const normalized = part.replace(/^!/, '')
|
|
672
|
+
|
|
673
|
+
if (Object.prototype.hasOwnProperty.call(BREAKPOINT_MIN_WIDTHS, normalized)) {
|
|
674
|
+
hadBreakpoint = true
|
|
675
|
+
const minWidth = BREAKPOINT_MIN_WIDTHS[normalized]
|
|
676
|
+
|
|
677
|
+
if (forcedWidth >= minWidth) {
|
|
678
|
+
// We are "inside" this breakpoint → strip the prefix
|
|
679
|
+
continue
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Too small for this breakpoint → drop the whole token
|
|
683
|
+
drop = true
|
|
684
|
+
break
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
nextParts.push(part)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (drop)
|
|
691
|
+
return ''
|
|
692
|
+
|
|
693
|
+
const mappedCore = toVarBackedUtilities(core, theme)
|
|
694
|
+
const finalCore = hadBreakpoint ? importantify(mappedCore) : mappedCore
|
|
695
|
+
|
|
696
|
+
return [...nextParts, finalCore].filter(Boolean).join(':')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
scopeEl.querySelectorAll('[class]').forEach((el) => {
|
|
700
|
+
let base = el.dataset.viewportBaseClass
|
|
701
|
+
if (typeof base !== 'string') {
|
|
702
|
+
base = el.className || ''
|
|
703
|
+
el.dataset.viewportBaseClass = base
|
|
704
|
+
}
|
|
705
|
+
const orig = base || ''
|
|
706
|
+
if (!orig.trim())
|
|
707
|
+
return
|
|
708
|
+
const origTokens = orig.split(/\s+/).filter(Boolean)
|
|
709
|
+
const mappedTokens = origTokens
|
|
710
|
+
.map(mapToken)
|
|
711
|
+
.filter(Boolean)
|
|
712
|
+
if (isolated) {
|
|
713
|
+
const mapped = mappedTokens.join(' ')
|
|
714
|
+
el.className = mapped
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const prevApplied = (el.dataset.themeAugmentedClasses || '').split(/\s+/).filter(Boolean)
|
|
719
|
+
if (prevApplied.length)
|
|
720
|
+
prevApplied.forEach(cls => el.classList.remove(cls))
|
|
721
|
+
|
|
722
|
+
const additions = mappedTokens.filter(cls => cls && !origTokens.includes(cls))
|
|
723
|
+
additions.forEach(cls => el.classList.add(cls))
|
|
724
|
+
|
|
725
|
+
if (additions.length)
|
|
726
|
+
el.dataset.themeAugmentedClasses = additions.join(' ')
|
|
727
|
+
else
|
|
728
|
+
delete el.dataset.themeAugmentedClasses
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
onMounted(async () => {
|
|
733
|
+
await ensureUnoRuntime()
|
|
734
|
+
|
|
735
|
+
// Initialize carousels/behaviors for SSR-inserted HTML
|
|
736
|
+
initEmblaCarousels(hostEl.value)
|
|
737
|
+
|
|
738
|
+
// Apply global theme once (keeps one style tag for vars; blocks can still override locally if needed)
|
|
739
|
+
// setGlobalThemeVars(props.theme)
|
|
740
|
+
setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
|
|
741
|
+
// If you later need per-block overrides, keep the next line; otherwise, it can be omitted.
|
|
742
|
+
// setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
|
|
743
|
+
applyThemeClasses(hostEl.value, props.theme, (props.theme && props.theme.variant) || 'light')
|
|
744
|
+
rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
|
|
745
|
+
await nextTick()
|
|
746
|
+
hasMounted = true
|
|
747
|
+
notifyLoaded()
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
watch(
|
|
751
|
+
() => props.html,
|
|
752
|
+
async (val) => {
|
|
753
|
+
// Wait for DOM to reflect new v-html, then (re)wire behaviors and class mappings
|
|
754
|
+
await nextTick()
|
|
755
|
+
initEmblaCarousels(hostEl.value)
|
|
756
|
+
// setGlobalThemeVars(props.theme)
|
|
757
|
+
setScopedThemeVars(hostEl.value, normalizeTheme(props.theme))
|
|
758
|
+
|
|
759
|
+
applyThemeClasses(hostEl.value, props.theme, (props.theme && props.theme.variant) || 'light')
|
|
760
|
+
rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
|
|
761
|
+
await nextTick()
|
|
762
|
+
notifyLoaded()
|
|
763
|
+
},
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
watch(
|
|
767
|
+
() => props.theme,
|
|
768
|
+
async (val) => {
|
|
769
|
+
const t = normalizeTheme(val)
|
|
770
|
+
// 1) Write CSS variables globally
|
|
771
|
+
// setGlobalThemeVars(t)
|
|
772
|
+
setScopedThemeVars(hostEl.value, t)
|
|
773
|
+
// 2) Apply classes based on `apply`, `slots`, and optional variants
|
|
774
|
+
applyThemeClasses(hostEl.value, t, (val && val.variant) || 'light')
|
|
775
|
+
rewriteAllClasses(hostEl.value, t, props.isolated, props.viewportMode)
|
|
776
|
+
await nextTick()
|
|
777
|
+
notifyLoaded()
|
|
778
|
+
},
|
|
779
|
+
{ immediate: true, deep: true },
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
watch(
|
|
783
|
+
() => props.viewportMode,
|
|
784
|
+
async () => {
|
|
785
|
+
await nextTick()
|
|
786
|
+
rewriteAllClasses(hostEl.value, props.theme, props.isolated, props.viewportMode)
|
|
787
|
+
},
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
onBeforeUnmount(() => {
|
|
791
|
+
// UnoCSS runtime attaches globally; no per-component teardown required.
|
|
792
|
+
})
|
|
793
|
+
</script>
|
|
794
|
+
|
|
795
|
+
<template>
|
|
796
|
+
<!-- Runtime CSS applies inside this container -->
|
|
797
|
+
<div ref="hostEl" class="block-content" :data-theme-scope="scopeId">
|
|
798
|
+
<component :is="props.comp" v-if="props.comp" />
|
|
799
|
+
<div v-else v-html="safeHtml" />
|
|
800
|
+
</div>
|
|
801
|
+
</template>
|
|
802
|
+
|
|
803
|
+
<style>
|
|
804
|
+
p {
|
|
805
|
+
margin-bottom: 1em;
|
|
806
|
+
}
|
|
807
|
+
</style>
|