@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.8
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/dist/components/Body.vue +105 -36
- package/dist/components/Button.vue +4 -1
- package/dist/components/CodeBlock.vue +11 -18
- package/dist/components/CodeInline.vue +6 -1
- package/dist/components/Column.vue +30 -5
- package/dist/components/Container.vue +10 -2
- package/dist/components/Divider.vue +28 -0
- package/dist/components/Head.vue +22 -0
- package/dist/components/Heading.vue +28 -0
- package/dist/components/Html.vue +98 -47
- package/dist/components/Layout.vue +93 -0
- package/dist/components/Link.vue +26 -0
- package/dist/components/Markdown.vue +83 -0
- package/dist/components/Outlook.vue +36 -0
- package/dist/components/Overlap.vue +25 -5
- package/dist/components/{Preview.vue → Preheader.vue} +1 -1
- package/dist/components/Row.vue +16 -5
- package/dist/components/Section.vue +83 -0
- package/dist/components/Text.vue +29 -0
- package/dist/components/Vml.vue +165 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
- package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
- package/dist/render/createRenderer.d.mts +2 -3
- package/dist/render/createRenderer.d.mts.map +1 -1
- package/dist/render/createRenderer.mjs +67 -4
- package/dist/render/createRenderer.mjs.map +1 -1
- package/dist/serve.d.mts.map +1 -1
- package/dist/serve.mjs +84 -4
- package/dist/serve.mjs.map +1 -1
- package/dist/server/compatibility.d.mts +1 -2
- package/dist/server/compatibility.d.mts.map +1 -1
- package/dist/server/compatibility.mjs +30 -16
- package/dist/server/compatibility.mjs.map +1 -1
- package/dist/server/email.d.mts +17 -0
- package/dist/server/email.d.mts.map +1 -0
- package/dist/server/email.mjs +41 -0
- package/dist/server/email.mjs.map +1 -0
- package/dist/server/linter.d.mts +1 -2
- package/dist/server/linter.d.mts.map +1 -1
- package/dist/server/linter.mjs +60 -71
- package/dist/server/linter.mjs.map +1 -1
- package/dist/server/ui/App.vue +205 -69
- package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
- package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
- package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
- package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
- package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
- package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
- package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
- package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
- package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
- package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
- package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
- package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
- package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
- package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
- package/dist/server/ui/components/ui/toggle/index.ts +3 -3
- package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
- package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
- package/dist/server/ui/main.css +20 -20
- package/dist/server/ui/pages/Home.vue +12 -5
- package/dist/server/ui/pages/Preview.vue +495 -211
- package/dist/transformers/inlineCSS.d.mts +1 -14
- package/dist/transformers/inlineCSS.d.mts.map +1 -1
- package/dist/transformers/inlineCSS.mjs +25 -34
- package/dist/transformers/inlineCSS.mjs.map +1 -1
- package/dist/transformers/purgeCSS.d.mts.map +1 -1
- package/dist/transformers/purgeCSS.mjs +67 -1
- package/dist/transformers/purgeCSS.mjs.map +1 -1
- package/dist/transformers/tailwindcss.mjs +3 -7
- package/dist/transformers/tailwindcss.mjs.map +1 -1
- package/dist/types/config.d.mts +47 -29
- package/dist/types/config.d.mts.map +1 -1
- package/dist/types/index.d.mts +2 -2
- package/package.json +7 -3
- package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
- package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
- package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
- package/dist/server/ui/components/ui/resizable/index.ts +0 -3
package/dist/server/ui/App.vue
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, onMounted, onUnmounted, watch, watchEffect } from 'vue'
|
|
3
3
|
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router'
|
|
4
|
-
import { Monitor, CodeXml, Smartphone, ChevronDown, ArrowUp, ArrowDown, CornerDownLeft, Check,
|
|
4
|
+
import { Monitor, CodeXml, Smartphone, ChevronDown, ArrowUp, ArrowDown, CornerDownLeft, Check, Search, Camera, FileCode, FileText, Code, BookText, MailQuestion } from 'lucide-vue-next'
|
|
5
|
+
import { toBlob } from 'html-to-image'
|
|
5
6
|
import logoUrl from '@/logo.svg'
|
|
6
7
|
import logoGradientUrl from '@/logo-gradient.svg'
|
|
7
8
|
import { Kbd } from '@/components/ui/kbd'
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
CommandInput,
|
|
22
23
|
CommandItem,
|
|
23
24
|
CommandList,
|
|
25
|
+
CommandShortcut,
|
|
24
26
|
} from '@/components/ui/command'
|
|
25
27
|
import {
|
|
26
28
|
Sidebar,
|
|
@@ -36,7 +38,6 @@ import {
|
|
|
36
38
|
SidebarMenuButton,
|
|
37
39
|
SidebarProvider,
|
|
38
40
|
SidebarTrigger,
|
|
39
|
-
SidebarInput,
|
|
40
41
|
} from '@/components/ui/sidebar'
|
|
41
42
|
|
|
42
43
|
|
|
@@ -54,7 +55,6 @@ watchEffect(() => {
|
|
|
54
55
|
})
|
|
55
56
|
|
|
56
57
|
const templates = ref<Template[]>([])
|
|
57
|
-
const search = ref('')
|
|
58
58
|
const loading = ref(true)
|
|
59
59
|
const viewMode = ref<'preview' | 'source'>('preview')
|
|
60
60
|
const sidebarOpen = ref(localStorage.getItem('maizzle:sidebar') !== 'closed')
|
|
@@ -76,6 +76,7 @@ const devicePresets: DevicePreset[] = [
|
|
|
76
76
|
]
|
|
77
77
|
|
|
78
78
|
const selectedDevice = ref<DevicePreset | null>(null)
|
|
79
|
+
const deviceMenuOpen = ref(false)
|
|
79
80
|
const panelWidth = ref(0)
|
|
80
81
|
const panelHeight = ref(0)
|
|
81
82
|
const isDragging = ref(false)
|
|
@@ -104,14 +105,9 @@ if ((import.meta as any).hot) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
const grouped = computed(() => {
|
|
107
|
-
const filtered = templates.value.filter(t =>
|
|
108
|
-
t.name.toLowerCase().includes(search.value.toLowerCase())
|
|
109
|
-
|| t.path.toLowerCase().includes(search.value.toLowerCase())
|
|
110
|
-
)
|
|
111
|
-
|
|
112
108
|
const groups: Record<string, Template[]> = {}
|
|
113
109
|
|
|
114
|
-
for (const t of
|
|
110
|
+
for (const t of templates.value) {
|
|
115
111
|
const parts = t.path.split('/')
|
|
116
112
|
const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : '.'
|
|
117
113
|
if (!groups[dir]) groups[dir] = []
|
|
@@ -131,7 +127,68 @@ const isPreviewRoute = computed(() => route.path !== '/')
|
|
|
131
127
|
|
|
132
128
|
// Command palette
|
|
133
129
|
const router = useRouter()
|
|
130
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
|
131
|
+
const modKey = isMac ? '⌘' : 'Ctrl'
|
|
134
132
|
const commandOpen = ref(false)
|
|
133
|
+
const commandSearch = ref('')
|
|
134
|
+
|
|
135
|
+
watch(commandOpen, (open) => {
|
|
136
|
+
if (!open) commandSearch.value = ''
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const screenshotting = ref(false)
|
|
140
|
+
|
|
141
|
+
async function copyScreenshot() {
|
|
142
|
+
commandOpen.value = false
|
|
143
|
+
|
|
144
|
+
const iframe = document.querySelector('iframe') as HTMLIFrameElement | null
|
|
145
|
+
const doc = iframe?.contentDocument
|
|
146
|
+
if (!doc?.body) return
|
|
147
|
+
|
|
148
|
+
screenshotting.value = true
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const blob = await toBlob(doc.body, {
|
|
152
|
+
width: doc.body.scrollWidth,
|
|
153
|
+
height: doc.body.scrollHeight,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
if (blob) {
|
|
157
|
+
await navigator.clipboard.write([
|
|
158
|
+
new ClipboardItem({ 'image/png': blob })
|
|
159
|
+
])
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
screenshotting.value = false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function copyHtml() {
|
|
167
|
+
commandOpen.value = false
|
|
168
|
+
const slug = route.params.template as string
|
|
169
|
+
if (!slug) return
|
|
170
|
+
const res = await fetch(`/__maizzle/render/${slug}`)
|
|
171
|
+
await navigator.clipboard.writeText(await res.text())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function copyPlaintext() {
|
|
175
|
+
commandOpen.value = false
|
|
176
|
+
const slug = route.params.template as string
|
|
177
|
+
if (!slug) return
|
|
178
|
+
const res = await fetch(`/__maizzle/plaintext/${slug}`)
|
|
179
|
+
await navigator.clipboard.writeText(await res.text())
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function copySource() {
|
|
183
|
+
commandOpen.value = false
|
|
184
|
+
const slug = route.params.template as string
|
|
185
|
+
if (!slug) return
|
|
186
|
+
const res = await fetch(`/__maizzle/vue-source/${slug}`)
|
|
187
|
+
const html = await res.text()
|
|
188
|
+
const el = document.createElement('div')
|
|
189
|
+
el.innerHTML = html
|
|
190
|
+
await navigator.clipboard.writeText(el.textContent || '')
|
|
191
|
+
}
|
|
135
192
|
|
|
136
193
|
const commandGrouped = computed(() => {
|
|
137
194
|
const groups: Record<string, Template[]> = {}
|
|
@@ -146,6 +203,7 @@ const commandGrouped = computed(() => {
|
|
|
146
203
|
return groups
|
|
147
204
|
})
|
|
148
205
|
|
|
206
|
+
|
|
149
207
|
function getFileName(path: string) {
|
|
150
208
|
return path.split('/').pop() || path
|
|
151
209
|
}
|
|
@@ -155,6 +213,11 @@ function onCommandSelect(href: string) {
|
|
|
155
213
|
router.push(href)
|
|
156
214
|
}
|
|
157
215
|
|
|
216
|
+
function openExternal(url: string) {
|
|
217
|
+
commandOpen.value = false
|
|
218
|
+
window.open(url, '_blank', 'noopener')
|
|
219
|
+
}
|
|
220
|
+
|
|
158
221
|
function onKeydown(e: KeyboardEvent) {
|
|
159
222
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
160
223
|
e.preventDefault()
|
|
@@ -171,6 +234,29 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
171
234
|
if (e.key === '/' && !isInputFocused()) {
|
|
172
235
|
e.preventDefault()
|
|
173
236
|
commandOpen.value = true
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Copy shortcuts (Cmd on Mac, Alt on Win/Linux)
|
|
241
|
+
if ((isMac ? e.metaKey : e.altKey) && !e.shiftKey && isPreviewRoute.value) {
|
|
242
|
+
switch (e.key.toLowerCase()) {
|
|
243
|
+
case 's':
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
copyScreenshot()
|
|
246
|
+
return
|
|
247
|
+
case 'c':
|
|
248
|
+
e.preventDefault()
|
|
249
|
+
copyHtml()
|
|
250
|
+
return
|
|
251
|
+
case 'p':
|
|
252
|
+
e.preventDefault()
|
|
253
|
+
copyPlaintext()
|
|
254
|
+
return
|
|
255
|
+
case 'u':
|
|
256
|
+
e.preventDefault()
|
|
257
|
+
copySource()
|
|
258
|
+
return
|
|
259
|
+
}
|
|
174
260
|
}
|
|
175
261
|
}
|
|
176
262
|
|
|
@@ -181,8 +267,18 @@ function isInputFocused() {
|
|
|
181
267
|
return tag === 'input' || tag === 'textarea' || (el as HTMLElement).isContentEditable
|
|
182
268
|
}
|
|
183
269
|
|
|
184
|
-
|
|
185
|
-
|
|
270
|
+
function onWindowBlur() {
|
|
271
|
+
deviceMenuOpen.value = false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
onMounted(() => {
|
|
275
|
+
document.addEventListener('keydown', onKeydown)
|
|
276
|
+
window.addEventListener('blur', onWindowBlur)
|
|
277
|
+
})
|
|
278
|
+
onUnmounted(() => {
|
|
279
|
+
document.removeEventListener('keydown', onKeydown)
|
|
280
|
+
window.removeEventListener('blur', onWindowBlur)
|
|
281
|
+
})
|
|
186
282
|
</script>
|
|
187
283
|
|
|
188
284
|
<template>
|
|
@@ -193,27 +289,15 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
|
193
289
|
<img :src="logoUrl" alt="Maizzle" class="h-4 dark:hidden">
|
|
194
290
|
<img :src="logoGradientUrl" alt="Maizzle" class="hidden h-4 dark:block">
|
|
195
291
|
</RouterLink>
|
|
196
|
-
<
|
|
292
|
+
<button class="inline-flex items-center gap-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" @click="commandOpen = true">
|
|
293
|
+
<Search class="size-3.5" />
|
|
294
|
+
<kbd class="flex items-center gap-0.5 text-[10px] font-sans">
|
|
295
|
+
<span>{{ modKey }}</span>
|
|
296
|
+
<span class="text-gray-300 dark:text-gray-600">K</span>
|
|
297
|
+
</kbd>
|
|
298
|
+
</button>
|
|
197
299
|
</SidebarHeader>
|
|
198
300
|
|
|
199
|
-
<div class="px-3 pt-3 pb-1">
|
|
200
|
-
<div class="relative flex items-center">
|
|
201
|
-
<SidebarInput
|
|
202
|
-
v-model="search"
|
|
203
|
-
placeholder="Search emails..."
|
|
204
|
-
class="text-xs! pr-7"
|
|
205
|
-
@keydown.esc="search && (search = '')"
|
|
206
|
-
/>
|
|
207
|
-
<button
|
|
208
|
-
v-if="search"
|
|
209
|
-
class="absolute right-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
210
|
-
@click="search = ''"
|
|
211
|
-
>
|
|
212
|
-
<X class="size-3.5" />
|
|
213
|
-
</button>
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
301
|
<SidebarContent>
|
|
218
302
|
<ScrollArea class="flex-1">
|
|
219
303
|
<SidebarGroup v-if="loading">
|
|
@@ -257,24 +341,17 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
|
257
341
|
<SidebarInset>
|
|
258
342
|
<!-- Header toolbar -->
|
|
259
343
|
<header class="grid h-12 grid-cols-[1fr_auto_1fr] items-center border-b px-4">
|
|
260
|
-
<div>
|
|
261
|
-
<
|
|
262
|
-
enter-from-class="opacity-0"
|
|
263
|
-
enter-active-class="transition-opacity duration-150 delay-200"
|
|
264
|
-
leave-active-class="transition-opacity duration-0"
|
|
265
|
-
leave-to-class="opacity-0"
|
|
266
|
-
>
|
|
267
|
-
<SidebarTrigger v-show="!sidebarOpen" />
|
|
268
|
-
</Transition>
|
|
344
|
+
<div class="flex items-center">
|
|
345
|
+
<SidebarTrigger />
|
|
269
346
|
</div>
|
|
270
347
|
|
|
271
348
|
<!-- View mode toggles (centered) -->
|
|
272
349
|
<ToggleGroup v-if="isPreviewRoute" v-model="viewMode" type="single" variant="outline" size="sm">
|
|
273
350
|
<ToggleGroupItem value="preview">
|
|
274
|
-
<Monitor class="size-4" />
|
|
351
|
+
<Monitor class="size-4 dark:text-gray-400" :stroke-width="1" />
|
|
275
352
|
</ToggleGroupItem>
|
|
276
353
|
<ToggleGroupItem value="source">
|
|
277
|
-
<CodeXml class="size-4" />
|
|
354
|
+
<CodeXml class="size-4 dark:text-gray-400" :stroke-width="1" />
|
|
278
355
|
</ToggleGroupItem>
|
|
279
356
|
</ToggleGroup>
|
|
280
357
|
<div v-else />
|
|
@@ -282,31 +359,32 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
|
282
359
|
<div class="flex items-center justify-end gap-3">
|
|
283
360
|
<span
|
|
284
361
|
v-if="isPreviewRoute && (!isFullSize || selectedDevice) && panelWidth"
|
|
285
|
-
class="text-xs font-medium tabular-nums text-gray-500 dark:text-gray-400 select-none"
|
|
362
|
+
class="hidden min-[430px]:inline text-xs font-medium tabular-nums text-gray-500 dark:text-gray-400 select-none"
|
|
286
363
|
>
|
|
287
364
|
{{ panelWidth }} × {{ panelHeight }}
|
|
288
365
|
</span>
|
|
289
|
-
<DropdownMenu v-if="isPreviewRoute">
|
|
366
|
+
<DropdownMenu v-if="isPreviewRoute" v-model:open="deviceMenuOpen" :modal="false">
|
|
290
367
|
<DropdownMenuTrigger as-child>
|
|
291
|
-
<Button variant="
|
|
292
|
-
<Smartphone class="size-4" />
|
|
368
|
+
<Button variant="ghost" size="sm" class="hidden min-[430px]:inline-flex gap-1.5 shadow-none border-none hover:bg-transparent">
|
|
369
|
+
<Smartphone class="size-4 dark:text-gray-400" :stroke-width="1" />
|
|
293
370
|
<span v-if="selectedDevice" class="text-xs">{{ selectedDevice.name }}</span>
|
|
294
|
-
<ChevronDown class="size-3 opacity-50" />
|
|
371
|
+
<ChevronDown class="size-3 opacity-50" :stroke-width="1" />
|
|
295
372
|
</Button>
|
|
296
373
|
</DropdownMenuTrigger>
|
|
297
|
-
<DropdownMenuContent align="end">
|
|
298
|
-
<DropdownMenuItem @click="selectedDevice = null; viewMode = 'preview'; resetKey++">
|
|
299
|
-
<Check v-if="!selectedDevice" class="size-3
|
|
300
|
-
<span :class="!selectedDevice ? '' : 'pl-5
|
|
374
|
+
<DropdownMenuContent align="end" class="min-w-52 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md dark:border-white/10">
|
|
375
|
+
<DropdownMenuItem class="text-xs font-medium text-gray-600 dark:text-gray-400 focus:text-gray-900 dark:focus:text-gray-200" @click="selectedDevice = null; isFullSize = true; viewMode = 'preview'; resetKey++">
|
|
376
|
+
<Check v-if="!selectedDevice && isFullSize" class="size-3 text-gray-900 dark:text-gray-200" />
|
|
377
|
+
<span :class="[!selectedDevice && isFullSize ? 'text-gray-900 dark:text-gray-200' : 'pl-5']">Full size</span>
|
|
301
378
|
</DropdownMenuItem>
|
|
302
379
|
<DropdownMenuItem
|
|
303
380
|
v-for="device in devicePresets"
|
|
304
381
|
:key="device.name"
|
|
382
|
+
class="text-xs font-medium text-gray-600 dark:text-gray-400 focus:text-gray-900 dark:focus:text-gray-200"
|
|
305
383
|
@click="selectDevice(device)"
|
|
306
384
|
>
|
|
307
|
-
<Check v-if="selectedDevice?.name === device.name" class="size-3
|
|
308
|
-
<span :class="selectedDevice?.name === device.name ? '' : 'pl-5
|
|
309
|
-
<span class="ml-auto text-
|
|
385
|
+
<Check v-if="selectedDevice?.name === device.name" class="size-3 text-gray-900 dark:text-gray-200" />
|
|
386
|
+
<span :class="[selectedDevice?.name === device.name ? 'text-gray-900 dark:text-gray-200' : 'pl-5']">{{ device.name }}</span>
|
|
387
|
+
<span class="ml-auto text-[11px] text-gray-400 dark:text-gray-500 tabular-nums tracking-tight">{{ device.width }}×{{ device.height }}</span>
|
|
310
388
|
</DropdownMenuItem>
|
|
311
389
|
</DropdownMenuContent>
|
|
312
390
|
</DropdownMenu>
|
|
@@ -316,31 +394,89 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
|
316
394
|
<!-- Main content -->
|
|
317
395
|
<div class="flex-1 overflow-hidden">
|
|
318
396
|
<RouterView v-slot="{ Component }">
|
|
319
|
-
<component :is="Component" v-model:view-mode="viewMode" :device="selectedDevice" :reset-key="resetKey" v-model:panel-width="panelWidth" v-model:panel-height="panelHeight" v-model:is-dragging="isDragging" v-model:is-full-size="isFullSize" @clear-device="selectedDevice = null" />
|
|
397
|
+
<component :is="Component" v-model:view-mode="viewMode" :device="selectedDevice" :reset-key="resetKey" :templates="templates" v-model:panel-width="panelWidth" v-model:panel-height="panelHeight" v-model:is-dragging="isDragging" v-model:is-full-size="isFullSize" @clear-device="selectedDevice = null; isFullSize = false" />
|
|
320
398
|
</RouterView>
|
|
321
399
|
</div>
|
|
322
400
|
</SidebarInset>
|
|
323
401
|
|
|
324
|
-
<CommandDialog v-model:open="commandOpen" title="
|
|
325
|
-
<CommandInput placeholder="
|
|
402
|
+
<CommandDialog v-model:open="commandOpen" title="Command palette" description="Run commands or search emails">
|
|
403
|
+
<CommandInput v-model="commandSearch" placeholder="Type a command or find an email..." />
|
|
326
404
|
<CommandList>
|
|
327
|
-
<CommandEmpty>No
|
|
328
|
-
|
|
405
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
406
|
+
|
|
407
|
+
<!-- Copy to clipboard commands -->
|
|
408
|
+
<CommandGroup v-if="isPreviewRoute" heading="Copy to clipboard">
|
|
329
409
|
<CommandItem
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
:value="t.path"
|
|
333
|
-
@select="onCommandSelect(t.href)"
|
|
410
|
+
value="Screenshot"
|
|
411
|
+
@select="copyScreenshot"
|
|
334
412
|
>
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
413
|
+
<Camera class="size-3 shrink-0 opacity-50" />
|
|
414
|
+
<span>Screenshot</span>
|
|
415
|
+
<CommandShortcut>{{ isMac ? '⌘' : 'ALT+' }}S</CommandShortcut>
|
|
416
|
+
</CommandItem>
|
|
417
|
+
<CommandItem
|
|
418
|
+
value="HTML"
|
|
419
|
+
@select="copyHtml"
|
|
420
|
+
>
|
|
421
|
+
<FileCode class="size-3 shrink-0 opacity-50" />
|
|
422
|
+
<span>HTML</span>
|
|
423
|
+
<CommandShortcut>{{ isMac ? '⌘' : 'ALT+' }}C</CommandShortcut>
|
|
424
|
+
</CommandItem>
|
|
425
|
+
<CommandItem
|
|
426
|
+
value="Plaintext"
|
|
427
|
+
@select="copyPlaintext"
|
|
428
|
+
>
|
|
429
|
+
<FileText class="size-3 shrink-0 opacity-50" />
|
|
430
|
+
<span>Plaintext</span>
|
|
431
|
+
<CommandShortcut>{{ isMac ? '⌘' : 'ALT+' }}P</CommandShortcut>
|
|
432
|
+
</CommandItem>
|
|
433
|
+
<CommandItem
|
|
434
|
+
value="Vue source"
|
|
435
|
+
@select="copySource"
|
|
436
|
+
>
|
|
437
|
+
<Code class="size-3 shrink-0 opacity-50" />
|
|
438
|
+
<span>Vue source</span>
|
|
439
|
+
<CommandShortcut>{{ isMac ? '⌘' : 'ALT+' }}U</CommandShortcut>
|
|
340
440
|
</CommandItem>
|
|
341
441
|
</CommandGroup>
|
|
442
|
+
|
|
443
|
+
<!-- Resources -->
|
|
444
|
+
<CommandGroup heading="Resources">
|
|
445
|
+
<CommandItem
|
|
446
|
+
value="Documentation"
|
|
447
|
+
@select="openExternal('https://maizzle.com')"
|
|
448
|
+
>
|
|
449
|
+
<BookText class="size-3 shrink-0 opacity-50" />
|
|
450
|
+
<span>Documentation</span>
|
|
451
|
+
</CommandItem>
|
|
452
|
+
<CommandItem
|
|
453
|
+
value="Can I Email"
|
|
454
|
+
@select="openExternal('https://www.caniemail.com')"
|
|
455
|
+
>
|
|
456
|
+
<MailQuestion class="size-3 shrink-0 opacity-50" />
|
|
457
|
+
<span>Can I Email</span>
|
|
458
|
+
</CommandItem>
|
|
459
|
+
</CommandGroup>
|
|
460
|
+
|
|
461
|
+
<!-- Templates -->
|
|
462
|
+
<template v-if="commandSearch">
|
|
463
|
+
<CommandGroup v-for="(items, dir) in commandGrouped" :key="dir" :heading="String(dir)">
|
|
464
|
+
<CommandItem
|
|
465
|
+
v-for="t in items"
|
|
466
|
+
:key="t.path"
|
|
467
|
+
:value="t.path"
|
|
468
|
+
@select="onCommandSelect(t.href)"
|
|
469
|
+
>
|
|
470
|
+
<svg class="size-3 shrink-0 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
471
|
+
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
|
472
|
+
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
|
473
|
+
</svg>
|
|
474
|
+
<span>{{ getFileName(t.path) }}</span>
|
|
475
|
+
</CommandItem>
|
|
476
|
+
</CommandGroup>
|
|
477
|
+
</template>
|
|
342
478
|
</CommandList>
|
|
343
|
-
<div class="flex items-center gap-4 border-t px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
|
|
479
|
+
<div class="flex items-center gap-4 border-t px-3 py-2 text-xs text-gray-500 dark:text-gray-400 cursor-default select-none">
|
|
344
480
|
<span class="inline-flex items-center gap-1">
|
|
345
481
|
<Kbd><ArrowUp class="size-3" /></Kbd>
|
|
346
482
|
<Kbd><ArrowDown class="size-3" /></Kbd>
|
|
@@ -348,7 +484,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
|
348
484
|
</span>
|
|
349
485
|
<span class="inline-flex items-center gap-1">
|
|
350
486
|
<Kbd><CornerDownLeft class="size-3" /></Kbd>
|
|
351
|
-
|
|
487
|
+
{{ commandSearch ? 'View' : 'Run' }}
|
|
352
488
|
</span>
|
|
353
489
|
<span class="inline-flex items-center gap-1">
|
|
354
490
|
<Kbd>Esc</Kbd>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui"
|
|
3
|
+
import type { HTMLAttributes } from "vue"
|
|
4
|
+
import { reactiveOmit } from "@vueuse/core"
|
|
5
|
+
import { Check } from "lucide-vue-next"
|
|
6
|
+
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"] }>()
|
|
10
|
+
const emits = defineEmits<CheckboxRootEmits>()
|
|
11
|
+
|
|
12
|
+
const delegatedProps = reactiveOmit(props, "class")
|
|
13
|
+
|
|
14
|
+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<CheckboxRoot
|
|
19
|
+
v-slot="slotProps"
|
|
20
|
+
data-slot="checkbox"
|
|
21
|
+
v-bind="forwarded"
|
|
22
|
+
:class="
|
|
23
|
+
cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
|
24
|
+
props.class)"
|
|
25
|
+
>
|
|
26
|
+
<CheckboxIndicator
|
|
27
|
+
data-slot="checkbox-indicator"
|
|
28
|
+
class="grid place-content-center text-current transition-none"
|
|
29
|
+
>
|
|
30
|
+
<slot v-bind="slotProps">
|
|
31
|
+
<Check class="size-3.5" />
|
|
32
|
+
</slot>
|
|
33
|
+
</CheckboxIndicator>
|
|
34
|
+
</CheckboxRoot>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Checkbox } from "./Checkbox.vue"
|
|
@@ -18,7 +18,7 @@ const forwarded = useForwardPropsEmits(props, emits)
|
|
|
18
18
|
|
|
19
19
|
<template>
|
|
20
20
|
<Dialog v-slot="slotProps" v-bind="forwarded">
|
|
21
|
-
<DialogContent class="overflow-hidden p-0 ">
|
|
21
|
+
<DialogContent class="overflow-hidden p-0 shadow-2xl shadow-black/10 dark:shadow-black/40" :show-close-button="false">
|
|
22
22
|
<DialogHeader class="sr-only">
|
|
23
23
|
<DialogTitle>{{ title }}</DialogTitle>
|
|
24
24
|
<DialogDescription>{{ description }}</DialogDescription>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ListboxFilterProps } from "reka-ui"
|
|
3
3
|
import type { HTMLAttributes } from "vue"
|
|
4
|
+
import { watch } from "vue"
|
|
4
5
|
import { reactiveOmit } from "@vueuse/core"
|
|
5
6
|
import { Search } from "lucide-vue-next"
|
|
6
7
|
import { ListboxFilter, useForwardProps } from "reka-ui"
|
|
@@ -13,13 +14,30 @@ defineOptions({
|
|
|
13
14
|
|
|
14
15
|
const props = defineProps<ListboxFilterProps & {
|
|
15
16
|
class?: HTMLAttributes["class"]
|
|
17
|
+
modelValue?: string
|
|
16
18
|
}>()
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: "update:modelValue", value: string): void
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const delegatedProps = reactiveOmit(props, "class", "modelValue")
|
|
19
25
|
|
|
20
26
|
const forwardedProps = useForwardProps(delegatedProps)
|
|
21
27
|
|
|
22
28
|
const { filterState } = useCommand()
|
|
29
|
+
|
|
30
|
+
// Sync external v-model → internal filter
|
|
31
|
+
watch(() => props.modelValue, (val) => {
|
|
32
|
+
if (val !== undefined && val !== filterState.search) {
|
|
33
|
+
filterState.search = val
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Sync internal filter → external v-model
|
|
38
|
+
watch(() => filterState.search, (val) => {
|
|
39
|
+
emit("update:modelValue", val)
|
|
40
|
+
})
|
|
23
41
|
</script>
|
|
24
42
|
|
|
25
43
|
<template>
|
|
@@ -66,7 +66,7 @@ onUnmounted(() => {
|
|
|
66
66
|
:id="id"
|
|
67
67
|
ref="itemRef"
|
|
68
68
|
data-slot="command-item"
|
|
69
|
-
:class="cn('data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
|
69
|
+
:class="cn('data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2.5 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
|
70
70
|
@select="() => {
|
|
71
71
|
filterState.search = ''
|
|
72
72
|
}"
|
|
@@ -16,7 +16,7 @@ const forwarded = useForwardProps(delegatedProps)
|
|
|
16
16
|
<ListboxContent
|
|
17
17
|
data-slot="command-list"
|
|
18
18
|
v-bind="forwarded"
|
|
19
|
-
:class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', props.class)"
|
|
19
|
+
:class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto p-2', props.class)"
|
|
20
20
|
>
|
|
21
21
|
<div role="presentation">
|
|
22
22
|
<slot />
|
|
@@ -10,7 +10,7 @@ const props = defineProps<{
|
|
|
10
10
|
<template>
|
|
11
11
|
<span
|
|
12
12
|
data-slot="command-shortcut"
|
|
13
|
-
:class="cn('
|
|
13
|
+
:class="cn('ml-auto text-[10px] tracking-widest text-gray-400 dark:text-gray-500', props.class)"
|
|
14
14
|
>
|
|
15
15
|
<slot />
|
|
16
16
|
</span>
|
|
@@ -4,18 +4,26 @@ import type { HTMLAttributes } from "vue"
|
|
|
4
4
|
import { reactiveOmit } from "@vueuse/core"
|
|
5
5
|
import { DialogOverlay } from "reka-ui"
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
|
+
import stripesUrl from '@/stripes.svg'
|
|
7
8
|
|
|
8
9
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
|
9
10
|
|
|
10
11
|
const delegatedProps = reactiveOmit(props, "class")
|
|
12
|
+
|
|
13
|
+
const stripeBg = {
|
|
14
|
+
backgroundImage: `url(${stripesUrl})`,
|
|
15
|
+
backgroundRepeat: 'repeat',
|
|
16
|
+
backgroundAttachment: 'fixed',
|
|
17
|
+
}
|
|
11
18
|
</script>
|
|
12
19
|
|
|
13
20
|
<template>
|
|
14
21
|
<DialogOverlay
|
|
15
22
|
data-slot="dialog-overlay"
|
|
16
23
|
v-bind="delegatedProps"
|
|
17
|
-
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-white/80', props.class)"
|
|
24
|
+
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-white/80 dark:bg-gray-950/80 backdrop-blur-[1px]', props.class)"
|
|
18
25
|
>
|
|
26
|
+
<div class="absolute inset-0 opacity-2 dark:opacity-3" :style="stripeBg" />
|
|
19
27
|
<slot />
|
|
20
28
|
</DialogOverlay>
|
|
21
29
|
</template>
|
|
@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
|
|
24
24
|
:data-inset="inset ? '' : undefined"
|
|
25
25
|
:data-variant="variant"
|
|
26
26
|
v-bind="forwardedProps"
|
|
27
|
-
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
|
27
|
+
:class="cn('focus:bg-accent dark:focus:bg-white/10 focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
|
28
28
|
>
|
|
29
29
|
<slot />
|
|
30
30
|
</DropdownMenuItem>
|
|
@@ -26,7 +26,7 @@ const delegatedProps = reactiveOmit(props, "class")
|
|
|
26
26
|
>
|
|
27
27
|
<ScrollAreaThumb
|
|
28
28
|
data-slot="scroll-area-thumb"
|
|
29
|
-
class="bg-
|
|
29
|
+
class="bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 relative flex-1 rounded-full transition-colors"
|
|
30
30
|
/>
|
|
31
31
|
</ScrollAreaScrollbar>
|
|
32
32
|
</template>
|
|
@@ -37,7 +37,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
|
37
37
|
<DialogContent
|
|
38
38
|
data-slot="sheet-content"
|
|
39
39
|
:class="cn(
|
|
40
|
-
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-
|
|
40
|
+
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-200 data-[state=open]:duration-200',
|
|
41
41
|
side === 'right'
|
|
42
42
|
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
|
43
43
|
side === 'left'
|
|
@@ -4,18 +4,26 @@ import type { HTMLAttributes } from "vue"
|
|
|
4
4
|
import { reactiveOmit } from "@vueuse/core"
|
|
5
5
|
import { DialogOverlay } from "reka-ui"
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
|
+
import stripesUrl from '@/stripes.svg'
|
|
7
8
|
|
|
8
9
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
|
9
10
|
|
|
10
11
|
const delegatedProps = reactiveOmit(props, "class")
|
|
12
|
+
|
|
13
|
+
const stripeBg = {
|
|
14
|
+
backgroundImage: `url(${stripesUrl})`,
|
|
15
|
+
backgroundRepeat: 'repeat',
|
|
16
|
+
backgroundAttachment: 'fixed',
|
|
17
|
+
}
|
|
11
18
|
</script>
|
|
12
19
|
|
|
13
20
|
<template>
|
|
14
21
|
<DialogOverlay
|
|
15
22
|
data-slot="sheet-overlay"
|
|
16
|
-
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-
|
|
23
|
+
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:duration-200 data-[state=open]:duration-200 fixed inset-0 z-50 bg-white/80 dark:bg-gray-950/80 backdrop-blur-[1px]', props.class)"
|
|
17
24
|
v-bind="delegatedProps"
|
|
18
25
|
>
|
|
26
|
+
<div class="absolute inset-0 opacity-2 dark:opacity-3" :style="stripeBg" />
|
|
19
27
|
<slot />
|
|
20
28
|
</DialogOverlay>
|
|
21
29
|
</template>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { SidebarProps } from "."
|
|
3
|
+
import { watch } from "vue"
|
|
4
|
+
import { useRoute } from "vue-router"
|
|
3
5
|
import { cn } from "@/lib/utils"
|
|
4
6
|
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
|
5
7
|
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
|
@@ -17,7 +19,12 @@ const props = withDefaults(defineProps<SidebarProps>(), {
|
|
|
17
19
|
collapsible: "offcanvas",
|
|
18
20
|
})
|
|
19
21
|
|
|
22
|
+
const route = useRoute()
|
|
20
23
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
|
24
|
+
|
|
25
|
+
watch(() => route.path, () => {
|
|
26
|
+
if (isMobile.value) setOpenMobile(false)
|
|
27
|
+
})
|
|
21
28
|
</script>
|
|
22
29
|
|
|
23
30
|
<template>
|
|
@@ -36,7 +43,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
|
|
36
43
|
data-slot="sidebar"
|
|
37
44
|
data-mobile="true"
|
|
38
45
|
:side="side"
|
|
39
|
-
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
|
46
|
+
class="bg-sidebar! text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
|
40
47
|
:style="{
|
|
41
48
|
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
|
42
49
|
}"
|