@maizzle/framework 6.0.0-rc.6 → 6.0.0-rc.7

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 (63) hide show
  1. package/dist/components/CodeBlock.vue +12 -19
  2. package/dist/components/Markdown.vue +70 -0
  3. package/dist/plugins/postcss/tailwindCleanup.mjs +22 -13
  4. package/dist/plugins/postcss/tailwindCleanup.mjs.map +1 -1
  5. package/dist/render/createRenderer.d.mts +2 -3
  6. package/dist/render/createRenderer.d.mts.map +1 -1
  7. package/dist/render/createRenderer.mjs +55 -4
  8. package/dist/render/createRenderer.mjs.map +1 -1
  9. package/dist/serve.d.mts.map +1 -1
  10. package/dist/serve.mjs +83 -3
  11. package/dist/serve.mjs.map +1 -1
  12. package/dist/server/compatibility.d.mts +1 -2
  13. package/dist/server/compatibility.d.mts.map +1 -1
  14. package/dist/server/compatibility.mjs +15 -15
  15. package/dist/server/compatibility.mjs.map +1 -1
  16. package/dist/server/email.d.mts +17 -0
  17. package/dist/server/email.d.mts.map +1 -0
  18. package/dist/server/email.mjs +40 -0
  19. package/dist/server/email.mjs.map +1 -0
  20. package/dist/server/ui/App.vue +204 -68
  21. package/dist/server/ui/components/ui/checkbox/Checkbox.vue +35 -0
  22. package/dist/server/ui/components/ui/checkbox/index.ts +1 -0
  23. package/dist/server/ui/components/ui/command/CommandDialog.vue +1 -1
  24. package/dist/server/ui/components/ui/command/CommandInput.vue +19 -1
  25. package/dist/server/ui/components/ui/command/CommandItem.vue +1 -1
  26. package/dist/server/ui/components/ui/command/CommandList.vue +1 -1
  27. package/dist/server/ui/components/ui/command/CommandShortcut.vue +1 -1
  28. package/dist/server/ui/components/ui/dialog/DialogOverlay.vue +9 -1
  29. package/dist/server/ui/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  30. package/dist/server/ui/components/ui/scroll-area/ScrollBar.vue +1 -1
  31. package/dist/server/ui/components/ui/sheet/SheetContent.vue +1 -1
  32. package/dist/server/ui/components/ui/sheet/SheetOverlay.vue +9 -1
  33. package/dist/server/ui/components/ui/sidebar/Sidebar.vue +8 -1
  34. package/dist/server/ui/components/ui/sidebar/SidebarProvider.vue +1 -1
  35. package/dist/server/ui/components/ui/sidebar/SidebarTrigger.vue +5 -4
  36. package/dist/server/ui/components/ui/tags-input/TagsInput.vue +26 -0
  37. package/dist/server/ui/components/ui/tags-input/TagsInputInput.vue +17 -0
  38. package/dist/server/ui/components/ui/tags-input/TagsInputItem.vue +19 -0
  39. package/dist/server/ui/components/ui/tags-input/TagsInputItemDelete.vue +22 -0
  40. package/dist/server/ui/components/ui/tags-input/TagsInputItemText.vue +17 -0
  41. package/dist/server/ui/components/ui/tags-input/index.ts +5 -0
  42. package/dist/server/ui/components/ui/toggle/index.ts +3 -3
  43. package/dist/server/ui/components/ui/toggle-group/ToggleGroup.vue +1 -1
  44. package/dist/server/ui/components/ui/toggle-group/ToggleGroupItem.vue +2 -2
  45. package/dist/server/ui/main.css +20 -20
  46. package/dist/server/ui/pages/Home.vue +12 -5
  47. package/dist/server/ui/pages/Preview.vue +369 -150
  48. package/dist/transformers/inlineCSS.mjs +9 -0
  49. package/dist/transformers/inlineCSS.mjs.map +1 -1
  50. package/dist/transformers/purgeCSS.d.mts.map +1 -1
  51. package/dist/transformers/purgeCSS.mjs +67 -1
  52. package/dist/transformers/purgeCSS.mjs.map +1 -1
  53. package/dist/transformers/tailwindcss.mjs +3 -7
  54. package/dist/transformers/tailwindcss.mjs.map +1 -1
  55. package/dist/types/config.d.mts +38 -4
  56. package/dist/types/config.d.mts.map +1 -1
  57. package/dist/types/index.d.mts +2 -2
  58. package/package.json +7 -3
  59. package/dist/server/ui/components/ui/resizable/ResizableHandle.vue +0 -30
  60. package/dist/server/ui/components/ui/resizable/ResizablePanel.vue +0 -21
  61. package/dist/server/ui/components/ui/resizable/ResizablePanelGroup.vue +0 -25
  62. package/dist/server/ui/components/ui/resizable/index.ts +0 -3
  63. /package/dist/components/{Preview.vue → Preheader.vue} +0 -0
@@ -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, X } from 'lucide-vue-next'
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 filtered) {
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
- onMounted(() => document.addEventListener('keydown', onKeydown))
185
- onUnmounted(() => document.removeEventListener('keydown', onKeydown))
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
- <SidebarTrigger class="-mr-1" />
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
- <Transition
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 />
@@ -286,27 +363,28 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
286
363
  >
287
364
  {{ panelWidth }} &times; {{ 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="outline" size="sm" class="gap-1.5">
292
- <Smartphone class="size-4" />
368
+ <Button variant="ghost" size="sm" class="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.5" />
300
- <span :class="!selectedDevice ? '' : 'pl-5.5'">Responsive</span>
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.5" />
308
- <span :class="selectedDevice?.name === device.name ? '' : 'pl-5.5'">{{ device.name }}</span>
309
- <span class="ml-auto text-xs text-gray-500 dark:text-gray-400 tabular-nums">{{ device.width }}&times;{{ device.height }}</span>
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 }}&times;{{ 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" 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="Search emails" description="Search and navigate to an email">
325
- <CommandInput placeholder="Search emails..." />
402
+ <CommandDialog v-model:open="commandOpen" title="Command palette" description="Run commands or search emails">
403
+ <CommandInput v-model="commandSearch" :placeholder="isPreviewRoute ? 'Type a command or search...' : 'Search emails...'" />
326
404
  <CommandList>
327
- <CommandEmpty>No emails found.</CommandEmpty>
328
- <CommandGroup v-for="(items, dir) in commandGrouped" :key="dir" :heading="String(dir)">
405
+ <CommandEmpty>No results found.</CommandEmpty>
406
+
407
+ <!-- Copy to clipboard commands: shown when not searching -->
408
+ <CommandGroup v-if="!commandSearch && isPreviewRoute" heading="Copy to clipboard">
329
409
  <CommandItem
330
- v-for="t in items"
331
- :key="t.path"
332
- :value="t.path"
333
- @select="onCommandSelect(t.href)"
410
+ value="Screenshot"
411
+ @select="copyScreenshot"
334
412
  >
335
- <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">
336
- <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
337
- <path d="M14 2v4a2 2 0 0 0 2 2h4" />
338
- </svg>
339
- <span>{{ getFileName(t.path) }}</span>
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: always shown when not searching -->
444
+ <CommandGroup v-if="!commandSearch" 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: shown when searching -->
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
- Open
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 delegatedProps = reactiveOmit(props, "class")
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('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
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-border relative flex-1 rounded-full"
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-300 data-[state=open]:duration-500',
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-black/80', props.class)"
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
  }"