@stacksjs/defaults 0.75.328 → 0.75.330

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 (78) hide show
  1. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  2. package/ai/skills/stacks-native/CATALOG.md +2 -2
  3. package/ide/vscode/package.json +1 -1
  4. package/package.json +4 -4
  5. package/resources/components/Dashboard/Auth/ForgotPassword.stx +1 -1
  6. package/resources/components/Dashboard/Ci/CiDashboard.stx +7 -7
  7. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +3 -3
  8. package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +1 -1
  9. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +5 -5
  10. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +2 -2
  11. package/resources/components/Dashboard/Commerce/CommerceCategoryHierarchy.stx +2 -2
  12. package/resources/components/Dashboard/Commerce/CommerceCouponBreakdown.stx +1 -1
  13. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +9 -9
  14. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +1 -1
  15. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +5 -5
  16. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +2 -2
  17. package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +1 -1
  18. package/resources/components/Dashboard/Commerce/CommerceGiftCardBreakdown.stx +1 -1
  19. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +11 -11
  20. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +13 -13
  21. package/resources/components/Dashboard/Commerce/CommercePaymentsBreakdown.stx +2 -2
  22. package/resources/components/Dashboard/Commerce/CommercePosCart.stx +1 -1
  23. package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +1 -1
  24. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +1 -1
  25. package/resources/components/Dashboard/Commerce/CommerceProductDetailDashboard.stx +8 -8
  26. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +1 -1
  27. package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +2 -2
  28. package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +1 -1
  29. package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +1 -1
  30. package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +1 -1
  31. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +2 -2
  32. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +3 -3
  33. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +6 -6
  34. package/resources/components/Dashboard/Commerce/PrintLogBreakdown.stx +1 -1
  35. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +2 -2
  36. package/resources/components/Dashboard/Commerce/PrintLogsTable.stx +3 -3
  37. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +1 -1
  38. package/resources/components/Dashboard/Content/CommentsDashboard.stx +1 -1
  39. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +1 -1
  40. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +53 -61
  41. package/resources/components/Dashboard/Content/FileStoragePanel.stx +99 -0
  42. package/resources/components/Dashboard/Content/FileUploadDialog.stx +1 -1
  43. package/resources/components/Dashboard/Content/PagesDashboard.stx +1 -1
  44. package/resources/components/Dashboard/Content/PostEditorDialog.stx +2 -2
  45. package/resources/components/Dashboard/Content/PostsDashboard.stx +1 -1
  46. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +1 -1
  47. package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +1 -1
  48. package/resources/components/Dashboard/Email/CapturedMailDetail.stx +1 -1
  49. package/resources/components/Dashboard/Feedback/FeedbackForm.stx +4 -4
  50. package/resources/components/Dashboard/FileViewer/MediaCard.stx +1 -1
  51. package/resources/components/Dashboard/FileViewer/MediaInfoPanel.stx +4 -4
  52. package/resources/components/Dashboard/GlobalSearch.stx +4 -4
  53. package/resources/components/Dashboard/Infrastructure/CloudDashboard.stx +1 -1
  54. package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +2 -2
  55. package/resources/components/Dashboard/Infrastructure/LogsDashboard.stx +2 -2
  56. package/resources/components/Dashboard/Infrastructure/MailboxesDashboard.stx +3 -3
  57. package/resources/components/Dashboard/Infrastructure/ServerlessDashboard.stx +2 -2
  58. package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -5
  59. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +1 -1
  60. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +2 -2
  61. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +8 -8
  62. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +5 -5
  63. package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +5 -5
  64. package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +4 -4
  65. package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +4 -4
  66. package/resources/components/Dashboard/Models/ModelsOverview.stx +1 -1
  67. package/resources/components/Dashboard/Navbar.stx +2 -2
  68. package/resources/components/Dashboard/Operations/AuditDashboard.stx +1 -1
  69. package/resources/components/Dashboard/Operations/OperationHistory.stx +1 -1
  70. package/resources/components/Dashboard/Operations/RecoveryJobList.stx +1 -1
  71. package/resources/components/Dashboard/Remote/RemoteConsole.stx +1 -1
  72. package/resources/components/Dashboard/Settings/SettingsDashboard.stx +1 -1
  73. package/resources/components/Dashboard/UI/Checkbox.stx +1 -1
  74. package/resources/components/Dashboard/UI/MetricCards.stx +1 -1
  75. package/resources/components/Dashboard/UI/Pagination.stx +1 -1
  76. package/resources/components/Dashboard/UI/Tabs.stx +7 -4
  77. package/resources/components/Dashboard/UI/Textarea.stx +2 -2
  78. package/resources/components/Dashboard/UI/Toggle.stx +1 -1
@@ -150,7 +150,7 @@ function errorMessages(): string[] {
150
150
  </ul>
151
151
  <div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
152
152
  <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span><input x-model="name" type="text" maxlength="100" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
153
- <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Slug</span><input x-model="slug" type="text" maxlength="255" required spellcheck="false" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" @input="markSlugTouched" @blur="normalizeSlug" /><span class="block mt-1 text-gray-400 text-xs dark:text-neutral-500">The storefront address, /products/{{ slug() || 'slug' }}.</span></label>
153
+ <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Slug</span><input x-model="slug" type="text" maxlength="255" required spellcheck="false" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" @input="markSlugTouched" @blur="normalizeSlug" /><span class="block mt-1 text-gray-500 text-xs dark:text-neutral-500">The storefront address, /products/{{ slug() || 'slug' }}.</span></label>
154
154
  <label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span><textarea x-model="description" rows="3" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea></label>
155
155
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Price ({{ currency() }})</span><input x-model="price" type="number" min="0" :step="moneyInputStep(currency())" inputmode="decimal" required :aria-invalid="price() !== '' && priceError() !== ''" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /><span :if="price() !== '' && priceError()" class="block mt-1 text-red-600 text-xs dark:text-red-400">{{ priceError() }}</span></label>
156
156
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Inventory count</span><input x-model="inventoryCount" type="number" min="0" step="1" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
@@ -85,13 +85,13 @@ function handleRowAction(payload: { action: string, row: CommerceProductRecord }
85
85
  <div class="flex flex-none items-center justify-center h-9 w-9 font-semibold text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-md" x-text="row.initials"></div>
86
86
  <div class="min-w-0">
87
87
  <p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.nameLabel"></p>
88
- <p class="mt-1 max-w-52 text-gray-400 text-xs truncate dark:text-neutral-500" x-text="row.descriptionLabel"></p>
88
+ <p class="mt-1 max-w-52 text-gray-500 text-xs truncate dark:text-neutral-500" x-text="row.descriptionLabel"></p>
89
89
  </div>
90
90
  </div>
91
91
  </template>
92
92
  <template :if="column.key === 'inventoryCount'">
93
93
  <p :class="'font-medium text-sm ' + (row.outOfStock ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-neutral-200')" x-text="row.inventoryLabel"></p>
94
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500" x-text="row.prepLabel"></p>
94
+ <p class="mt-1 text-gray-500 text-xs dark:text-neutral-500" x-text="row.prepLabel"></p>
95
95
  </template>
96
96
  </template>
97
97
  </Table>
@@ -111,7 +111,7 @@ function handleRowAction(payload: { action: string, row: ReviewRecord }): void {
111
111
  <div class="max-w-lg min-w-72">
112
112
  <p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.titleLabel"></p>
113
113
  <p class="mt-1 line-clamp-2 text-gray-500 text-sm dark:text-neutral-400" x-text="row.contentLabel"></p>
114
- <div class="flex flex-wrap gap-x-3 gap-y-1 items-center mt-2 text-gray-400 text-xs dark:text-neutral-500">
114
+ <div class="flex flex-wrap gap-x-3 gap-y-1 items-center mt-2 text-gray-500 text-xs dark:text-neutral-500">
115
115
  <span :aria-label="row.starsLabel" class="text-amber-500 text-sm" x-text="row.stars"></span>
116
116
  <span x-text="row.helpfulLabel"></span>
117
117
  <span x-text="row.unhelpfulLabel"></span>
@@ -74,7 +74,7 @@ function handleRowAction(payload: { action: string, row: ProductVariantRecord })
74
74
  <span class="px-2 py-0.5 text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-full" x-text="option"></span>
75
75
  </template>
76
76
  <span :if="row.moreOptions" class="px-2 py-0.5 text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full" x-text="row.moreOptions"></span>
77
- <span :if="row.shownOptions.length === 0" class="text-gray-400 text-xs">No options</span>
77
+ <span :if="row.shownOptions.length === 0" class="text-gray-500 text-xs dark:text-gray-400">No options</span>
78
78
  </div>
79
79
  </template>
80
80
  </template>
@@ -182,7 +182,7 @@ onMount(() => {
182
182
  <span :class="'h-5 w-5 text-blue-600 dark:text-blue-400 ' + statIcon(item.label)"></span>
183
183
  </div>
184
184
  <p class="mt-2 font-semibold tabular-nums text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
185
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
185
+ <p class="mt-1 text-gray-500 text-xs dark:text-neutral-500">{{ item.detail }}</p>
186
186
  </article>
187
187
  </template>
188
188
  </section>
@@ -93,8 +93,8 @@ function submit(): void {
93
93
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Method name</span><input x-model="name" type="text" required maxlength="255" autocomplete="off" placeholder="Secure download" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
94
94
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span><textarea x-model="description" required rows="3" placeholder="Explain how customers receive and access files." class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700 resize-y"></textarea></label>
95
95
  <div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
96
- <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Download limit</span><input x-model="downloadLimit" type="number" min="0" step="1" inputmode="numeric" placeholder="Unlimited" class="mt-1 px-3 py-2 w-full tabular-nums text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /><span class="block mt-1 text-gray-400 text-xs dark:text-neutral-500">Leave blank for unlimited downloads.</span></label>
97
- <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Expiry days</span><input x-model="expiryDays" type="number" required min="0" step="1" inputmode="numeric" class="mt-1 px-3 py-2 w-full tabular-nums text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /><span class="block mt-1 text-gray-400 text-xs dark:text-neutral-500">Use 0 for access without expiry.</span></label>
96
+ <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Download limit</span><input x-model="downloadLimit" type="number" min="0" step="1" inputmode="numeric" placeholder="Unlimited" class="mt-1 px-3 py-2 w-full tabular-nums text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /><span class="block mt-1 text-gray-500 text-xs dark:text-neutral-500">Leave blank for unlimited downloads.</span></label>
97
+ <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Expiry days</span><input x-model="expiryDays" type="number" required min="0" step="1" inputmode="numeric" class="mt-1 px-3 py-2 w-full tabular-nums text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /><span class="block mt-1 text-gray-500 text-xs dark:text-neutral-500">Use 0 for access without expiry.</span></label>
98
98
  </div>
99
99
  <div class="grid gap-3 grid-cols-1 sm:grid-cols-2">
100
100
  <label class="flex gap-3 items-start p-3 border border-gray-200 rounded-lg dark:border-neutral-700"><input x-model="requiresLogin" type="checkbox" class="mt-0.5 h-4 w-4 text-blue-600 bg-white dark:bg-neutral-900 border-gray-300 rounded dark:border-neutral-600" /><span><strong class="block text-gray-900 text-sm dark:text-white">Require login</strong><span class="block mt-0.5 text-gray-500 text-xs dark:text-neutral-400">Only authenticated customers can download.</span></span></label>
@@ -48,10 +48,10 @@ function refundable(): boolean {
48
48
  </template>
49
49
  <dl class="grid gap-x-6 gap-y-4 sm:grid-cols-2">
50
50
  <template :for="item in detailRows()" :key="item.label">
51
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">{{ item.label }}</dt><dd class="mt-1 break-all text-gray-800 text-sm dark:text-neutral-100">{{ item.value }}</dd></div>
51
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">{{ item.label }}</dt><dd class="mt-1 break-all text-gray-800 text-sm dark:text-neutral-100">{{ item.value }}</dd></div>
52
52
  </template>
53
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Method</dt><dd class="mt-1 capitalize text-gray-800 text-sm dark:text-neutral-100">{{ label(record()?.method || '') }}</dd></div>
54
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Refund recorded</dt><dd class="mt-1 text-gray-800 text-sm dark:text-neutral-100">{{ record()?.refundAmount ? money(record()?.refundAmount || 0, record()?.currency || 'USD') : record()?.status === 'refunded' && !record()?.refundRecorded ? 'Amount missing' : money(0, record()?.currency || 'USD') }}</dd></div>
53
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">Method</dt><dd class="mt-1 capitalize text-gray-800 text-sm dark:text-neutral-100">{{ label(record()?.method || '') }}</dd></div>
54
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">Refund recorded</dt><dd class="mt-1 text-gray-800 text-sm dark:text-neutral-100">{{ record()?.refundAmount ? money(record()?.refundAmount || 0, record()?.currency || 'USD') : record()?.status === 'refunded' && !record()?.refundRecorded ? 'Amount missing' : money(0, record()?.currency || 'USD') }}</dd></div>
55
55
  </dl>
56
56
 
57
57
  <div :if="record()?.notes" class="mt-5 p-3 text-gray-600 text-sm dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900/60 rounded-lg">{{ record()?.notes }}</div>
@@ -49,12 +49,12 @@ function deleteRecord(): void {
49
49
  <span class="px-2 py-1 capitalize font-semibold text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ label(record()?.status || '') }}</span>
50
50
  </template>
51
51
  <dl class="grid gap-4 grid-cols-2">
52
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Location</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.location || 'Not recorded' }}</dd></div>
53
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Terminal</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.terminal || 'Not recorded' }}</dd></div>
54
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Recorded prints</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ (record()?.printCount || 0).toLocaleString() }}</dd></div>
55
- <div><dt class="text-gray-400 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
56
- <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Last heartbeat</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ formatDate(record()?.lastPing || 0) }}</dd></div>
57
- <div class="col-span-2"><dt class="text-gray-400 text-xs dark:text-neutral-500">Created</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ formatDate(record()?.createdAt || '') }}</dd></div>
52
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">Location</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ record()?.location || 'Not recorded' }}</dd></div>
53
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">Terminal</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.terminal || 'Not recorded' }}</dd></div>
54
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">Recorded prints</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ (record()?.printCount || 0).toLocaleString() }}</dd></div>
55
+ <div><dt class="text-gray-500 text-xs dark:text-neutral-500">Record ID</dt><dd class="mt-1 font-mono text-gray-900 text-sm dark:text-white">{{ record()?.id || 'Not recorded' }}</dd></div>
56
+ <div class="col-span-2"><dt class="text-gray-500 text-xs dark:text-neutral-500">Last heartbeat</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ formatDate(record()?.lastPing || 0) }}</dd></div>
57
+ <div class="col-span-2"><dt class="text-gray-500 text-xs dark:text-neutral-500">Created</dt><dd class="mt-1 text-gray-900 text-sm dark:text-white">{{ formatDate(record()?.createdAt || '') }}</dd></div>
58
58
  </dl>
59
59
  <template #footer>
60
60
  <div class="flex gap-2 justify-between w-full">
@@ -67,7 +67,7 @@ function hourlyRows(): CountRow[] {
67
67
  </article>
68
68
 
69
69
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
70
- <div class="flex items-start justify-between"><h2 class="font-semibold text-gray-900 text-sm dark:text-white">Prints by hour</h2><span class="text-gray-400 text-xs dark:text-neutral-500">Local time</span></div>
70
+ <div class="flex items-start justify-between"><h2 class="font-semibold text-gray-900 text-sm dark:text-white">Prints by hour</h2><span class="text-gray-500 text-xs dark:text-neutral-500">Local time</span></div>
71
71
  <div class="mt-4 space-y-3">
72
72
  <template :for="item in hourlyRows()" :key="item.key">
73
73
  <div class="grid gap-3 grid-cols-[4rem_1fr_auto] items-center"><span class="text-gray-500 text-xs dark:text-neutral-400">{{ item.key }}</span><div class="h-1.5 bg-gray-100 dark:bg-neutral-700 rounded-full"><div :style="'width:' + item.percentage + '%'" class="h-1.5 bg-violet-500 rounded-full"></div></div><span class="text-gray-500 text-xs dark:text-neutral-400">{{ item.count }}</span></div>
@@ -39,7 +39,7 @@ function details(): Array<{ label: string, value: string }> {
39
39
  <template #header-trailing>
40
40
  <span class="px-2 py-1 capitalize font-medium text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record()?.status }}</span>
41
41
  </template>
42
- <dl class="grid gap-x-6 gap-y-4 sm:grid-cols-2"><template :for="item in details()" :key="item.label"><div><dt class="text-gray-400 text-xs dark:text-neutral-500">{{ item.label }}</dt><dd class="mt-1 break-all text-gray-800 text-sm dark:text-neutral-100">{{ item.value }}</dd></div></template></dl>
43
- <div :if="record()?.metadata && record()?.metadata !== '{}'" class="mt-5"><p class="text-gray-400 text-xs dark:text-neutral-500">Metadata</p><pre class="overflow-x-auto mt-1 p-3 text-gray-700 text-xs whitespace-pre-wrap dark:text-neutral-200 bg-gray-50 dark:bg-neutral-900/60 rounded-lg">{{ record()?.metadata }}</pre></div>
42
+ <dl class="grid gap-x-6 gap-y-4 sm:grid-cols-2"><template :for="item in details()" :key="item.label"><div><dt class="text-gray-500 text-xs dark:text-neutral-500">{{ item.label }}</dt><dd class="mt-1 break-all text-gray-800 text-sm dark:text-neutral-100">{{ item.value }}</dd></div></template></dl>
43
+ <div :if="record()?.metadata && record()?.metadata !== '{}'" class="mt-5"><p class="text-gray-500 text-xs dark:text-neutral-500">Metadata</p><pre class="overflow-x-auto mt-1 p-3 text-gray-700 text-xs whitespace-pre-wrap dark:text-neutral-200 bg-gray-50 dark:bg-neutral-900/60 rounded-lg">{{ record()?.metadata }}</pre></div>
44
44
  <template #footer><Button variant="secondary" @click="emit('close')">Close</Button></template>
45
45
  </Modal>
@@ -31,10 +31,10 @@ function statusClass(status: string): string {
31
31
  <tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
32
32
  <template :for="record in records()" :key="record.id">
33
33
  <tr>
34
- <td class="px-4 py-4"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.document || 'Untitled print job' }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">Log #{{ record.id }}</p></td>
35
- <td class="px-4 py-4"><p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.printer || 'Unknown printer' }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.printDeviceId ? 'Device #' + record.printDeviceId : 'No linked device' }}</p></td>
34
+ <td class="px-4 py-4"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.document || 'Untitled print job' }}</p><p class="mt-1 text-gray-500 text-xs dark:text-neutral-500">Log #{{ record.id }}</p></td>
35
+ <td class="px-4 py-4"><p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.printer || 'Unknown printer' }}</p><p class="mt-1 text-gray-500 text-xs dark:text-neutral-500">{{ record.printDeviceId ? 'Device #' + record.printDeviceId : 'No linked device' }}</p></td>
36
36
  <td class="px-4 py-4"><span :class="'inline-flex px-2 py-1 font-medium text-xs capitalize rounded-full ' + statusClass(record.status)">{{ record.status || 'unknown' }}</span></td>
37
- <td class="px-4 py-4"><p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.pages }} pages</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.sizeKb }} KB · {{ record.durationSeconds }}s</p></td>
37
+ <td class="px-4 py-4"><p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.pages }} pages</p><p class="mt-1 text-gray-500 text-xs dark:text-neutral-500">{{ record.sizeKb }} KB · {{ record.durationSeconds }}s</p></td>
38
38
  <td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ date(record.timestamp) }}</td>
39
39
  <td class="px-4 py-4"><div class="flex gap-1 justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'View print log ' + record.id" @click="emit('view', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-view"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete print log ' + record.id" @click="emit('delete', record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span></Button></div></td>
40
40
  </tr>
@@ -193,7 +193,7 @@ onMount(() => {
193
193
 
194
194
  <div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadAuthors()">Retry</Button></div>
195
195
 
196
- <section aria-label="Author metrics" class="grid gap-3 sm:grid-cols-3">
196
+ <section aria-label="Author metrics" class="grid gap-3 grid-cols-1 sm:grid-cols-3">
197
197
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Total</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ authors().length }}</p></article>
198
198
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Assigned posts</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ assignedPostCount() }}</p></article>
199
199
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Newest</p><p class="mt-2 font-semibold text-gray-900 text-lg truncate dark:text-white">{{ authors()[0]?.name || 'None' }}</p></article>
@@ -183,7 +183,7 @@ onMount(() => {
183
183
  <div class="divide-gray-200 divide-y dark:divide-neutral-700">
184
184
  <div :if="loading()" class="px-4 py-14 text-center text-gray-500 text-sm dark:text-neutral-400"><span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading comments</div>
185
185
  <div :else-if="visibleComments().length === 0" class="px-4 py-14 text-center"><span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-comment-01"></span><p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No comments found</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the search or status filter.</p></div>
186
- <template :else><template :for="comment in visibleComments()"><article class="p-4"><div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"><div class="min-w-0"><div class="flex flex-wrap gap-2 items-center"><p class="font-semibold text-gray-900 text-sm dark:text-white">{{ comment.authorName || 'Anonymous' }}</p><span :class="statusClass(comment.status)">{{ comment.status }}</span><span class="text-gray-400 text-xs">{{ formatDate(comment.createdAt) }}</span></div><p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ comment.authorEmail }}<span :if="comment.postTitle"> on {{ comment.postTitle }}</span></p><p class="mt-3 max-w-4xl text-gray-700 text-sm whitespace-pre-wrap dark:text-neutral-200">{{ comment.content }}</p></div><div class="flex flex-shrink-0 flex-wrap gap-2"><Button :if="comment.status !== 'approved'" variant="success" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'approved')">Approve</Button><Button :if="comment.status !== 'pending'" variant="secondary" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'pending')">Restore</Button><Button :if="comment.status !== 'spam'" variant="warning" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'spam')">Spam</Button><Button :if="comment.status !== 'trash' && comment.source === 'comments'" variant="outline" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'trash')">Trash</Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Permanently delete comment by ' + comment.authorName" :disabled="Boolean(updatingId())" @click="openDelete(comment)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></div></article></template></template>
186
+ <template :else><template :for="comment in visibleComments()"><article class="p-4"><div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"><div class="min-w-0"><div class="flex flex-wrap gap-2 items-center"><p class="font-semibold text-gray-900 text-sm dark:text-white">{{ comment.authorName || 'Anonymous' }}</p><span :class="statusClass(comment.status)">{{ comment.status }}</span><span class="text-gray-500 text-xs dark:text-gray-400">{{ formatDate(comment.createdAt) }}</span></div><p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ comment.authorEmail }}<span :if="comment.postTitle"> on {{ comment.postTitle }}</span></p><p class="mt-3 max-w-4xl text-gray-700 text-sm whitespace-pre-wrap dark:text-neutral-200">{{ comment.content }}</p></div><div class="flex flex-shrink-0 flex-wrap gap-2"><Button :if="comment.status !== 'approved'" variant="success" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'approved')">Approve</Button><Button :if="comment.status !== 'pending'" variant="secondary" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'pending')">Restore</Button><Button :if="comment.status !== 'spam'" variant="warning" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'spam')">Spam</Button><Button :if="comment.status !== 'trash' && comment.source === 'comments'" variant="outline" size="sm" :disabled="Boolean(updatingId())" @click="setStatus(comment, 'trash')">Trash</Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Permanently delete comment by ' + comment.authorName" :disabled="Boolean(updatingId())" @click="openDelete(comment)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></div></article></template></template>
187
187
  </div>
188
188
  </section>
189
189
  </div>
@@ -206,7 +206,7 @@ onMount(() => {
206
206
 
207
207
  <div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadRecords()">Retry</Button></div>
208
208
 
209
- <section :aria-label="title() + ' metrics'" class="grid gap-3 sm:grid-cols-3">
209
+ <section :aria-label="title() + ' metrics'" class="grid gap-3 grid-cols-1 sm:grid-cols-3">
210
210
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Total</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ records().length }}</p></article>
211
211
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">{{ secondaryMetricLabel() }}</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ secondaryMetric() }}</p></article>
212
212
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Newest</p><p class="mt-2 font-semibold text-gray-900 text-lg truncate dark:text-white">{{ records()[0]?.name || 'None' }}</p></article>
@@ -77,7 +77,11 @@ const searchQuery = state('')
77
77
  const sortBy = state<'name' | 'lastModified' | 'size'>('name')
78
78
  const sortOrder = state<'asc' | 'desc'>('asc')
79
79
  const viewMode = state<'grid' | 'list'>('grid')
80
- const showSidePanel = state(true)
80
+ // A wide screen shows the storage panel beside the files; a narrow one has
81
+ // no room for it, so the same panel opens in a drawer instead.
82
+ const sidePanelOpen = state(true)
83
+ const storageDrawerOpen = state(false)
84
+ const isWideScreen = useMediaQuery('(min-width: 1024px)')
81
85
  const loading = state(true)
82
86
  const loadError = state('')
83
87
  const truncated = state(false)
@@ -165,7 +169,7 @@ const typeRows = derived(() => [
165
169
  { key: 'videos', label: 'Videos', bytes: stats().byType.videos, color: 'bg-amber-500' },
166
170
  { key: 'audio', label: 'Audio', bytes: stats().byType.audio, color: 'bg-emerald-500' },
167
171
  { key: 'other', label: 'Other', bytes: stats().byType.other, color: 'bg-stone-400 dark:bg-neutral-500' },
168
- ])
172
+ ].map(row => ({ key: row.key, label: row.label, color: row.color, size: formatFileSize(row.bytes), barStyle: typeBarStyle(row.bytes) })))
169
173
 
170
174
  const volumePercent = derived(() => {
171
175
  const total = stats().disk.totalBytes
@@ -173,9 +177,31 @@ const volumePercent = derived(() => {
173
177
  return total && used !== null ? Math.min(100, Math.max(0, used / total * 100)) : 0
174
178
  })
175
179
  const capacityKnown = derived(() => stats().disk.totalBytes !== null && stats().disk.availableBytes !== null)
180
+ const storageSummary = derived(() => ({
181
+ files: stats().files,
182
+ folders: stats().folders,
183
+ contentSize: formatFileSize(stats().contentBytes),
184
+ capacityKnown: capacityKnown(),
185
+ capacityPercent: volumePercent(),
186
+ available: formatFileSize(stats().disk.availableBytes),
187
+ total: formatFileSize(stats().disk.totalBytes),
188
+ }))
189
+ const storageFolders = derived(() => topLevelFolders().map(folder => ({ path: folder.path, name: folder.name, count: (folder.items || []).length })))
176
190
  const currentFolderLabel = derived(() => currentFolderPath() || 'Storage root')
177
191
  const emptyHint = derived(() => searchQuery() ? 'No items match your search.' : 'This folder is empty.')
178
- const sidePanelClass = derived(() => `border-r border-black/[0.06] dark:border-white/[0.08] bg-[#F5F3EE]/70 dark:bg-neutral-900/40 backdrop-blur-2xl transition-all duration-300 ${showSidePanel() ? 'w-64' : 'w-0 overflow-hidden'}`)
192
+
193
+ function toggleSidePanel(): void {
194
+ if (isWideScreen())
195
+ sidePanelOpen.set(!sidePanelOpen())
196
+ else
197
+ storageDrawerOpen.set(true)
198
+ }
199
+
200
+ effect(() => {
201
+ if (isWideScreen() && storageDrawerOpen())
202
+ storageDrawerOpen.set(false)
203
+ })
204
+
179
205
  const segmentBase = 'inline-flex items-center px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors'
180
206
  const segmentActive = 'bg-white dark:bg-neutral-700 text-stone-800 dark:text-white shadow-sm'
181
207
  const segmentInactive = 'text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200'
@@ -183,13 +209,6 @@ const gridViewClass = derived(() => `${segmentBase} ${viewMode() === 'grid' ? se
183
209
  const listViewClass = derived(() => `${segmentBase} ${viewMode() === 'list' ? segmentActive : segmentInactive}`)
184
210
  const sortOrderIconClass = derived(() => `${sortOrder() === 'asc' ? 'i-hugeicons-arrow-up-02' : 'i-hugeicons-arrow-down-02'} h-5 w-5`)
185
211
 
186
- function navRowClass(active: boolean): string {
187
- const base = 'group flex items-center gap-2.5 px-2.5 py-1.5 w-full text-left font-medium text-[13px] rounded-lg transition-colors'
188
- return active
189
- ? `${base} bg-black/[0.06] dark:bg-white/[0.09] text-stone-900 dark:text-white`
190
- : `${base} text-stone-600 dark:text-neutral-300 hover:bg-black/[0.04] dark:hover:bg-white/[0.06]`
191
- }
192
-
193
212
  function gridCardClass(item: FileItem): string {
194
213
  const selected = selectedItem()?.id === item.id
195
214
  return `group relative overflow-hidden bg-white dark:bg-neutral-800 border rounded-lg shadow-sm hover:shadow-md transition-all cursor-pointer ${selected ? 'border-blue-400 ring-2 ring-blue-500/20' : 'border-gray-200 dark:border-neutral-700 hover:border-blue-300 dark:hover:border-blue-700'}`
@@ -275,6 +294,22 @@ function navigateToFolder(folder: FileItem): void {
275
294
  selectedItem.set(null)
276
295
  }
277
296
 
297
+ function openStorageFolder(path: string): void {
298
+ currentFolderPath.set(path)
299
+ selectedItem.set(null)
300
+ storageDrawerOpen.set(false)
301
+ }
302
+
303
+ function selectStorageDisk(name: string): void {
304
+ selectedDisk.set(name)
305
+ changeDisk()
306
+ }
307
+
308
+ function uploadFromStoragePanel(): void {
309
+ storageDrawerOpen.set(false)
310
+ openUpload()
311
+ }
312
+
278
313
  function onItemClick(item: FileItem): void {
279
314
  if (item.type === 'folder')
280
315
  navigateToFolder(item)
@@ -409,56 +444,9 @@ onMount(() => { void loadFiles() })
409
444
  </script>
410
445
 
411
446
  <div class="flex min-h-[calc(100dvh-3rem)] bg-white dark:bg-neutral-900">
412
- <aside :class="sidePanelClass()">
413
- <div class="flex flex-col h-full min-h-[calc(100dvh-3rem)] w-64">
414
- <div class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
415
- <label class="block">
416
- <span class="font-semibold text-[11px] text-stone-400 tracking-wider uppercase dark:text-neutral-500">Storage disk</span>
417
- <select x-model="selectedDisk" :disabled="loading()" class="mt-2 px-2.5 py-2 w-full text-sm text-stone-800 dark:text-white bg-white/80 dark:bg-neutral-800 border border-stone-200 rounded-lg dark:border-neutral-700 disabled:opacity-60" @change="changeDisk()">
418
- <template :for="disk in disks"><option :value="disk.name">{{ disk.name }}{{ disk.public ? ' (public)' : '' }}</option></template>
419
- </select>
420
- </label>
421
- </div>
422
-
423
- <div class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
424
- <div class="flex items-center justify-between">
425
- <p class="font-semibold text-[11px] text-stone-400 tracking-wider uppercase dark:text-neutral-500">Stored content</p>
426
- <span class="font-mono text-[11px] text-stone-500 dark:text-neutral-400">{{ formatFileSize(stats().contentBytes) }}</span>
427
- </div>
428
- <div class="grid gap-2 grid-cols-2 mt-3">
429
- <div class="p-2.5 bg-white/65 dark:bg-white/[0.05] rounded-lg"><p class="font-semibold text-lg text-stone-900 dark:text-white">{{ stats().files }}</p><p class="text-[11px] text-stone-500 dark:text-neutral-400">Files</p></div>
430
- <div class="p-2.5 bg-white/65 dark:bg-white/[0.05] rounded-lg"><p class="font-semibold text-lg text-stone-900 dark:text-white">{{ stats().folders }}</p><p class="text-[11px] text-stone-500 dark:text-neutral-400">Folders</p></div>
431
- </div>
432
- </div>
433
-
434
- <div class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
435
- <p class="font-semibold text-[11px] text-stone-400 tracking-wider uppercase dark:text-neutral-500">Content by type</p>
436
- <div class="mt-3 space-y-3">
437
- <template :for="row in typeRows">
438
- <div>
439
- <div class="flex items-center justify-between text-[11px]"><span class="text-stone-600 dark:text-neutral-300">{{ row.label }}</span><span class="font-mono text-stone-400 dark:text-neutral-500">{{ formatFileSize(row.bytes) }}</span></div>
440
- <div class="overflow-hidden mt-1 h-1.5 bg-black/[0.06] dark:bg-white/[0.08] rounded-full"><div :class="'h-full rounded-full ' + row.color" :style="typeBarStyle(row.bytes)"></div></div>
441
- </div>
442
- </template>
443
- </div>
444
- </div>
445
-
446
- <div :if="capacityKnown()" class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
447
- <div class="flex items-center justify-between"><p class="font-semibold text-[11px] text-stone-400 tracking-wider uppercase dark:text-neutral-500">Volume capacity</p><span class="font-mono text-[11px] text-stone-500 dark:text-neutral-400">{{ volumePercent().toFixed(0) }}%</span></div>
448
- <div class="overflow-hidden mt-2 h-1.5 bg-black/[0.06] dark:bg-white/[0.08] rounded-full"><div class="h-full bg-blue-500 rounded-full" :style="'width: ' + volumePercent().toFixed(2) + '%'"></div></div>
449
- <p class="mt-2 text-[11px] text-stone-500 dark:text-neutral-400">{{ formatFileSize(stats().disk.availableBytes) }} available of {{ formatFileSize(stats().disk.totalBytes) }}</p>
450
- </div>
451
-
452
- <nav aria-label="Storage folders" class="overflow-y-auto flex-1 p-2">
453
- <button type="button" :class="navRowClass(currentFolderPath() === '')" @click="currentFolderPath.set('')"><span class="h-[18px] w-[18px] text-blue-500 i-hugeicons-home-02"></span>Home</button>
454
- <template :for="folder in topLevelFolders">
455
- <button type="button" :class="navRowClass(currentFolderPath() === folder.path)" @click="navigateToFolder(folder)"><span class="h-[18px] w-[18px] text-amber-500 i-hugeicons-folder-02"></span><span class="truncate">{{ folder.name }}</span><span class="ml-auto tabular-nums text-[11px] text-stone-400 dark:text-neutral-500">{{ (folder.items || []).length }}</span></button>
456
- </template>
457
- </nav>
458
-
459
- <div class="p-3 border-black/[0.06] border-t dark:border-white/[0.08]">
460
- <Button :disabled="loading()" fullWidth @click="openUpload()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-upload-01"></span>Upload files</Button>
461
- </div>
447
+ <aside class="hidden lg:block shrink-0 bg-[#F5F3EE]/70 dark:bg-neutral-900/40 border-black/[0.06] border-r dark:border-white/[0.08] backdrop-blur-2xl duration-300 transition-all" :class="sidePanelOpen() ? 'w-64' : 'w-0 overflow-hidden'">
448
+ <div class="h-full min-h-[calc(100dvh-3rem)] w-64">
449
+ <FileStoragePanel :disks="disks()" :disk="selectedDisk()" :busy="loading()" :summary="storageSummary()" :type-rows="typeRows()" :folders="storageFolders()" :current-path="currentFolderPath()" @disk="selectStorageDisk" @navigate="openStorageFolder" @upload="uploadFromStoragePanel()" />
462
450
  </div>
463
451
  </aside>
464
452
 
@@ -466,7 +454,7 @@ onMount(() => { void loadFiles() })
466
454
  <header class="sticky top-0 z-10 p-4 bg-white/85 dark:bg-neutral-900/85 border-b border-black/[0.06] dark:border-white/[0.08] backdrop-blur-xl">
467
455
  <div class="flex flex-wrap gap-3 items-center justify-between">
468
456
  <div class="flex items-center min-w-0">
469
- <Button variant="ghost" iconOnly ariaLabel="Toggle storage sidebar" @click="showSidePanel.set(!showSidePanel())"><span class="block h-5 w-5 i-hugeicons-menu-01"></span></Button>
457
+ <Button variant="ghost" iconOnly ariaLabel="Toggle storage sidebar" @click="toggleSidePanel()"><span class="block h-5 w-5 i-hugeicons-menu-01"></span></Button>
470
458
  <div class="ml-3 min-w-0"><p class="font-semibold text-stone-900 truncate dark:text-white">{{ currentFolderLabel }}</p><p class="mt-0.5 text-stone-500 text-xs dark:text-neutral-400">{{ selectedDisk }} disk</p></div>
471
459
  </div>
472
460
  <div class="flex flex-wrap gap-2 items-center">
@@ -541,6 +529,10 @@ onMount(() => { void loadFiles() })
541
529
  </div>
542
530
  </div>
543
531
 
532
+ <Drawer :isOpen="storageDrawerOpen()" title="Storage" :description="selectedDisk() + ' disk'" size="sm" @close="storageDrawerOpen.set(false)">
533
+ <FileStoragePanel :disks="disks()" :disk="selectedDisk()" :busy="loading()" :summary="storageSummary()" :type-rows="typeRows()" :folders="storageFolders()" :current-path="currentFolderPath()" @disk="selectStorageDisk" @navigate="openStorageFolder" @upload="uploadFromStoragePanel()" />
534
+ </Drawer>
535
+
544
536
  <FileDetailsDrawer :item="selectedItem()" :disk="selectedDisk()" @close="selectedItem.set(null)" />
545
537
 
546
538
  <CreateFolderDialog
@@ -0,0 +1,99 @@
1
+ <script client>
2
+ interface StorageDisk {
3
+ name: string
4
+ public: boolean
5
+ }
6
+
7
+ interface StorageSummary {
8
+ files: number
9
+ folders: number
10
+ contentSize: string
11
+ capacityKnown: boolean
12
+ capacityPercent: number
13
+ available: string
14
+ total: string
15
+ }
16
+
17
+ interface StorageTypeRow {
18
+ key: string
19
+ label: string
20
+ size: string
21
+ color: string
22
+ barStyle: string
23
+ }
24
+
25
+ interface StorageFolder {
26
+ path: string
27
+ name: string
28
+ count: number
29
+ }
30
+
31
+ const emptySummary: StorageSummary = { files: 0, folders: 0, contentSize: '0 Bytes', capacityKnown: false, capacityPercent: 0, available: '', total: '' }
32
+
33
+ const disks = useReactiveProp('disks', [] as StorageDisk[])
34
+ const disk = useReactiveProp('disk', '')
35
+ const busy = useReactiveProp('busy', false)
36
+ const summary = useReactiveProp('summary', emptySummary)
37
+ const typeRows = useReactiveProp('typeRows', [] as StorageTypeRow[])
38
+ const folders = useReactiveProp('folders', [] as StorageFolder[])
39
+ const currentPath = useReactiveProp('currentPath', '')
40
+ const emit = defineEmits(['disk', 'navigate', 'upload'])
41
+
42
+ function navRowClass(active: boolean): string {
43
+ const base = 'group flex items-center gap-2.5 px-2.5 py-1.5 w-full text-left font-medium text-[13px] rounded-lg transition-colors'
44
+ return active
45
+ ? `${base} bg-black/[0.06] dark:bg-white/[0.09] text-stone-900 dark:text-white`
46
+ : `${base} text-stone-600 dark:text-neutral-300 hover:bg-black/[0.04] dark:hover:bg-white/[0.06]`
47
+ }
48
+ </script>
49
+
50
+ <div class="flex flex-col h-full">
51
+ <div class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
52
+ <label class="block">
53
+ <span class="font-semibold text-[11px] text-stone-500 tracking-wider uppercase dark:text-neutral-500">Storage disk</span>
54
+ <select x-model="disk" :disabled="busy()" class="mt-2 px-2.5 py-2 w-full text-sm text-stone-800 dark:text-white bg-white/80 dark:bg-neutral-800 border border-stone-200 rounded-lg dark:border-neutral-700 disabled:opacity-60" @change="emit('disk', $event.target.value)">
55
+ <template :for="option in disks"><option :value="option.name">{{ option.name }}{{ option.public ? ' (public)' : '' }}</option></template>
56
+ </select>
57
+ </label>
58
+ </div>
59
+
60
+ <div class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
61
+ <div class="flex items-center justify-between">
62
+ <p class="font-semibold text-[11px] text-stone-500 tracking-wider uppercase dark:text-neutral-500">Stored content</p>
63
+ <span class="font-mono text-[11px] text-stone-500 dark:text-neutral-400">{{ summary().contentSize }}</span>
64
+ </div>
65
+ <div class="grid gap-2 grid-cols-2 mt-3">
66
+ <div class="p-2.5 bg-white/65 dark:bg-white/[0.05] rounded-lg"><p class="font-semibold text-lg text-stone-900 dark:text-white">{{ summary().files }}</p><p class="text-[11px] text-stone-500 dark:text-neutral-400">Files</p></div>
67
+ <div class="p-2.5 bg-white/65 dark:bg-white/[0.05] rounded-lg"><p class="font-semibold text-lg text-stone-900 dark:text-white">{{ summary().folders }}</p><p class="text-[11px] text-stone-500 dark:text-neutral-400">Folders</p></div>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
72
+ <p class="font-semibold text-[11px] text-stone-500 tracking-wider uppercase dark:text-neutral-500">Content by type</p>
73
+ <div class="mt-3 space-y-3">
74
+ <template :for="row in typeRows">
75
+ <div>
76
+ <div class="flex items-center justify-between text-[11px]"><span class="text-stone-600 dark:text-neutral-300">{{ row.label }}</span><span class="font-mono text-stone-500 dark:text-neutral-500">{{ row.size }}</span></div>
77
+ <div class="overflow-hidden mt-1 h-1.5 bg-black/[0.06] dark:bg-white/[0.08] rounded-full"><div :class="'h-full rounded-full ' + row.color" :style="row.barStyle"></div></div>
78
+ </div>
79
+ </template>
80
+ </div>
81
+ </div>
82
+
83
+ <div :if="summary().capacityKnown" class="p-4 border-b border-black/[0.06] dark:border-white/[0.08]">
84
+ <div class="flex items-center justify-between"><p class="font-semibold text-[11px] text-stone-500 tracking-wider uppercase dark:text-neutral-500">Volume capacity</p><span class="font-mono text-[11px] text-stone-500 dark:text-neutral-400">{{ summary().capacityPercent.toFixed(0) }}%</span></div>
85
+ <div class="overflow-hidden mt-2 h-1.5 bg-black/[0.06] dark:bg-white/[0.08] rounded-full"><div class="h-full bg-blue-500 rounded-full" :style="'width: ' + summary().capacityPercent.toFixed(2) + '%'"></div></div>
86
+ <p class="mt-2 text-[11px] text-stone-500 dark:text-neutral-400">{{ summary().available }} available of {{ summary().total }}</p>
87
+ </div>
88
+
89
+ <nav aria-label="Storage folders" class="overflow-y-auto flex-1 p-2">
90
+ <button type="button" :class="navRowClass(currentPath() === '')" @click="emit('navigate', '')"><span class="h-[18px] w-[18px] text-blue-500 i-hugeicons-home-02"></span>Home</button>
91
+ <template :for="folder in folders">
92
+ <button type="button" :class="navRowClass(currentPath() === folder.path)" @click="emit('navigate', folder.path)"><span class="h-[18px] w-[18px] text-amber-500 i-hugeicons-folder-02"></span><span class="truncate">{{ folder.name }}</span><span class="ml-auto tabular-nums text-[11px] text-stone-500 dark:text-neutral-500">{{ folder.count }}</span></button>
93
+ </template>
94
+ </nav>
95
+
96
+ <div class="p-3 border-black/[0.06] border-t dark:border-white/[0.08]">
97
+ <Button :disabled="busy()" fullWidth @click="emit('upload')"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-upload-01"></span>Upload files</Button>
98
+ </div>
99
+ </div>
@@ -73,7 +73,7 @@ function submit(): void {
73
73
  </label>
74
74
  <ul :if="files().length" class="overflow-y-auto mt-4 space-y-1 max-h-32 text-sm">
75
75
  <template :for="file in files">
76
- <li class="flex gap-4 items-center justify-between px-3 py-2 bg-stone-50 dark:bg-neutral-900 rounded-lg"><span class="text-stone-700 truncate dark:text-neutral-200">{{ file.name }}</span><span class="flex-shrink-0 font-mono text-stone-400 text-xs dark:text-neutral-500">{{ formatFileSize(file.size) }}</span></li>
76
+ <li class="flex gap-4 items-center justify-between px-3 py-2 bg-stone-50 dark:bg-neutral-900 rounded-lg"><span class="text-stone-700 truncate dark:text-neutral-200">{{ file.name }}</span><span class="flex-shrink-0 font-mono text-stone-500 text-xs dark:text-neutral-500">{{ formatFileSize(file.size) }}</span></li>
77
77
  </template>
78
78
  </ul>
79
79
  </form>
@@ -207,7 +207,7 @@ onMount(() => {
207
207
 
208
208
  <div :if="loadError()" role="alert" class="flex gap-4 items-center justify-between p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadPages()">Retry</Button></div>
209
209
 
210
- <section aria-label="Page metrics" class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
210
+ <section aria-label="Page metrics" class="grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
211
211
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Total</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ pages().length }}</p></article>
212
212
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Views</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ formatNumber(totalViews()) }}</p></article>
213
213
  <article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Conversions</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ formatNumber(totalConversions()) }}</p></article>
@@ -136,12 +136,12 @@ function submit(): void {
136
136
  <label>
137
137
  <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Excerpt</span>
138
138
  <textarea x-model="excerpt" name="excerpt" rows="3" maxlength="500" placeholder="Optional, at least 10 characters when provided" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea>
139
- <span class="block mt-1 text-gray-400 text-xs">{{ excerpt().length }}/500</span>
139
+ <span class="block mt-1 text-gray-500 text-xs dark:text-gray-400">{{ excerpt().length }}/500</span>
140
140
  </label>
141
141
  <label>
142
142
  <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Content</span>
143
143
  <textarea x-model="content" name="content" required minlength="10" maxlength="100000" rows="10" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea>
144
- <span class="block mt-1 text-gray-400 text-xs">{{ content().length }}/100000</span>
144
+ <span class="block mt-1 text-gray-500 text-xs dark:text-gray-400">{{ content().length }}/100000</span>
145
145
  </label>
146
146
  <div class="grid gap-4 lg:grid-cols-2">
147
147
  <fieldset class="p-4 border border-gray-200 rounded-lg dark:border-neutral-700">
@@ -293,7 +293,7 @@ onMount(() => { void loadPosts() })
293
293
  <span :if="row.featured" x-tooltip="Featured" title="Featured" class="flex-shrink-0 h-4 w-4 text-amber-500 i-hugeicons-star"></span>
294
294
  </div>
295
295
  <p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400" x-text="row.summaryLabel"></p>
296
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500" x-text="row.authorLabel"></p>
296
+ <p class="mt-1 text-gray-500 text-xs dark:text-neutral-500" x-text="row.authorLabel"></p>
297
297
  </template>
298
298
  <template :else>
299
299
  <p class="text-gray-500 text-xs truncate dark:text-neutral-400"><span class="font-medium text-gray-700 dark:text-neutral-200">Categories:</span> <span x-text="row.categoriesLabel"></span></p>
@@ -70,7 +70,7 @@ function submit(): void {
70
70
  <div>
71
71
  <div class="flex items-center justify-between">
72
72
  <label for="seo-meta-description" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Meta description</label>
73
- <span class="tabular-nums text-neutral-400 text-xs">{{ metaDescription().length }}/160</span>
73
+ <span class="tabular-nums text-neutral-500 text-xs dark:text-neutral-400">{{ metaDescription().length }}/160</span>
74
74
  </div>
75
75
  <textarea
76
76
  id="seo-meta-description"
@@ -81,7 +81,7 @@ function icon(operation: DeploymentPreviewOperation): string {
81
81
  </div>
82
82
  <div class="flex-1 min-w-0">
83
83
  <div class="flex gap-2 items-center">
84
- <span class="font-mono text-neutral-400 text-xs">{{ index + 1 }}</span>
84
+ <span class="font-mono text-neutral-500 text-xs dark:text-neutral-400">{{ index + 1 }}</span>
85
85
  <h5 class="font-semibold text-neutral-900 text-sm dark:text-white">{{ operation.label }}</h5>
86
86
  </div>
87
87
  <p class="mt-1 text-neutral-600 text-sm dark:text-neutral-300">{{ operation.detail }}</p>
@@ -60,7 +60,7 @@ function formatBytes(value: number): string {
60
60
  </div>
61
61
  <div class="flex-shrink-0 text-right">
62
62
  <time class="block text-gray-500 text-xs dark:text-neutral-400" :datetime="selected()?.sentAt || ''">{{ formatDate(selected()?.sentAt || '') }}</time>
63
- <span class="block mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ formatBytes(selected()?.size || 0) }}</span>
63
+ <span class="block mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ formatBytes(selected()?.size || 0) }}</span>
64
64
  </div>
65
65
  </div>
66
66
  </header>