@edgedev/create-edge-app 1.1.23 → 1.1.26

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 (116) hide show
  1. package/.env +1 -0
  2. package/.env.dev +1 -0
  3. package/README.md +55 -20
  4. package/{agent.md → agents.md} +2 -0
  5. package/bin/cli.js +6 -6
  6. package/edge/components/auth/login.vue +384 -0
  7. package/edge/components/auth/register.vue +396 -0
  8. package/edge/components/auth.vue +108 -0
  9. package/edge/components/autoFileUpload.vue +215 -0
  10. package/edge/components/billing.vue +8 -0
  11. package/edge/components/buttonDivider.vue +14 -0
  12. package/edge/components/chip.vue +34 -0
  13. package/edge/components/clipboardButton.vue +42 -0
  14. package/edge/components/cms/block.vue +529 -0
  15. package/edge/components/cms/blockApi.vue +212 -0
  16. package/edge/components/cms/blockEditor.vue +725 -0
  17. package/edge/components/cms/blockInput.vue +66 -0
  18. package/edge/components/cms/blockPicker.vue +486 -0
  19. package/edge/components/cms/blockRender.vue +78 -0
  20. package/edge/components/cms/blockSheetContent.vue +28 -0
  21. package/edge/components/cms/codeEditor.vue +466 -0
  22. package/edge/components/cms/fontUpload.vue +327 -0
  23. package/edge/components/cms/htmlContent.vue +807 -0
  24. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  25. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  26. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  27. package/edge/components/cms/init_blocks/carousel.html +103 -0
  28. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  29. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  30. package/edge/components/cms/init_blocks/footer.html +24 -0
  31. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  32. package/edge/components/cms/init_blocks/hero.html +35 -0
  33. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  34. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  35. package/edge/components/cms/init_blocks/post_content.html +7 -0
  36. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  37. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  38. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  39. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  40. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  41. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  42. package/edge/components/cms/init_blocks/property_results.html +137 -0
  43. package/edge/components/cms/init_blocks/property_search.html +75 -0
  44. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  45. package/edge/components/cms/mediaCard.vue +116 -0
  46. package/edge/components/cms/mediaManager.vue +386 -0
  47. package/edge/components/cms/menu.vue +1103 -0
  48. package/edge/components/cms/optionsSelect.vue +107 -0
  49. package/edge/components/cms/page.vue +1785 -0
  50. package/edge/components/cms/posts.vue +1083 -0
  51. package/edge/components/cms/site.vue +1298 -0
  52. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  53. package/edge/components/cms/themeEditor.vue +426 -0
  54. package/edge/components/dashboard.vue +776 -0
  55. package/edge/components/editor.vue +671 -0
  56. package/edge/components/fileTree.vue +72 -0
  57. package/edge/components/files.vue +89 -0
  58. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  59. package/edge/components/formSubtypes/users.vue +336 -0
  60. package/edge/components/functionChips.vue +57 -0
  61. package/edge/components/gError.vue +98 -0
  62. package/edge/components/gHelper.vue +67 -0
  63. package/edge/components/gInput.vue +1331 -0
  64. package/edge/components/loggingIn.vue +41 -0
  65. package/edge/components/menu.vue +137 -0
  66. package/edge/components/menuContent.vue +132 -0
  67. package/edge/components/myAccount.vue +317 -0
  68. package/edge/components/myOrganizations.vue +75 -0
  69. package/edge/components/myProfile.vue +122 -0
  70. package/edge/components/orgSwitcher.vue +25 -0
  71. package/edge/components/organizationMembers.vue +522 -0
  72. package/edge/components/organizationSettings.vue +271 -0
  73. package/edge/components/shad/breadcrumbs.vue +35 -0
  74. package/edge/components/shad/button.vue +43 -0
  75. package/edge/components/shad/checkbox.vue +73 -0
  76. package/edge/components/shad/combobox.vue +238 -0
  77. package/edge/components/shad/datepicker.vue +184 -0
  78. package/edge/components/shad/dialog.vue +32 -0
  79. package/edge/components/shad/dropdownMenu.vue +54 -0
  80. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  81. package/edge/components/shad/form.vue +59 -0
  82. package/edge/components/shad/html.vue +877 -0
  83. package/edge/components/shad/input.vue +139 -0
  84. package/edge/components/shad/number.vue +109 -0
  85. package/edge/components/shad/select.vue +151 -0
  86. package/edge/components/shad/selectTags.vue +278 -0
  87. package/edge/components/shad/switch.vue +67 -0
  88. package/edge/components/shad/tags.vue +137 -0
  89. package/edge/components/shad/textarea.vue +102 -0
  90. package/edge/components/shad/typeMoney.vue +167 -0
  91. package/edge/components/sideBar.vue +288 -0
  92. package/edge/components/sideBarContent.vue +268 -0
  93. package/edge/components/sidebarProvider.vue +33 -0
  94. package/edge/components/tooltip.vue +16 -0
  95. package/edge/components/userMenu.vue +148 -0
  96. package/edge/components/v/alert.vue +59 -0
  97. package/edge/components/v/alertTitle.vue +18 -0
  98. package/edge/components/v/card.vue +53 -0
  99. package/edge/components/v/cardActions.vue +18 -0
  100. package/edge/components/v/cardText.vue +18 -0
  101. package/edge/components/v/cardTitle.vue +20 -0
  102. package/edge/components/v/col.vue +56 -0
  103. package/edge/components/v/list.vue +46 -0
  104. package/edge/components/v/listItem.vue +26 -0
  105. package/edge/components/v/listItemTitle.vue +18 -0
  106. package/edge/components/v/row.vue +42 -0
  107. package/edge/components/v/toolbar.vue +24 -0
  108. package/edge/composables/global.ts +519 -0
  109. package/edge-pull.sh +2 -0
  110. package/edge-push.sh +1 -0
  111. package/edge-status.sh +14 -0
  112. package/firebase.json +5 -2
  113. package/firebase_init.sh +21 -6
  114. package/package.json +1 -1
  115. package/plugins/firebase.client.ts +1 -0
  116. 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>