@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.
- package/ai/skills/stacks-dashboard/SKILL.md +2 -2
- package/ai/skills/stacks-native/CATALOG.md +2 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +1 -1
- package/resources/components/Dashboard/Ci/CiDashboard.stx +7 -7
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +3 -3
- package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +5 -5
- package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +2 -2
- package/resources/components/Dashboard/Commerce/CommerceCategoryHierarchy.stx +2 -2
- package/resources/components/Dashboard/Commerce/CommerceCouponBreakdown.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +9 -9
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +5 -5
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +2 -2
- package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceGiftCardBreakdown.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +11 -11
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +13 -13
- package/resources/components/Dashboard/Commerce/CommercePaymentsBreakdown.stx +2 -2
- package/resources/components/Dashboard/Commerce/CommercePosCart.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceProductDetailDashboard.stx +8 -8
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +2 -2
- package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +2 -2
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +3 -3
- package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +6 -6
- package/resources/components/Dashboard/Commerce/PrintLogBreakdown.stx +1 -1
- package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +2 -2
- package/resources/components/Dashboard/Commerce/PrintLogsTable.stx +3 -3
- package/resources/components/Dashboard/Content/AuthorsDashboard.stx +1 -1
- package/resources/components/Dashboard/Content/CommentsDashboard.stx +1 -1
- package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +1 -1
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +53 -61
- package/resources/components/Dashboard/Content/FileStoragePanel.stx +99 -0
- package/resources/components/Dashboard/Content/FileUploadDialog.stx +1 -1
- package/resources/components/Dashboard/Content/PagesDashboard.stx +1 -1
- package/resources/components/Dashboard/Content/PostEditorDialog.stx +2 -2
- package/resources/components/Dashboard/Content/PostsDashboard.stx +1 -1
- package/resources/components/Dashboard/Content/SeoEditorDialog.stx +1 -1
- package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +1 -1
- package/resources/components/Dashboard/Email/CapturedMailDetail.stx +1 -1
- package/resources/components/Dashboard/Feedback/FeedbackForm.stx +4 -4
- package/resources/components/Dashboard/FileViewer/MediaCard.stx +1 -1
- package/resources/components/Dashboard/FileViewer/MediaInfoPanel.stx +4 -4
- package/resources/components/Dashboard/GlobalSearch.stx +4 -4
- package/resources/components/Dashboard/Infrastructure/CloudDashboard.stx +1 -1
- package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +2 -2
- package/resources/components/Dashboard/Infrastructure/LogsDashboard.stx +2 -2
- package/resources/components/Dashboard/Infrastructure/MailboxesDashboard.stx +3 -3
- package/resources/components/Dashboard/Infrastructure/ServerlessDashboard.stx +2 -2
- package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -5
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +1 -1
- package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +2 -2
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +8 -8
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +5 -5
- package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +5 -5
- package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +4 -4
- package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +4 -4
- package/resources/components/Dashboard/Models/ModelsOverview.stx +1 -1
- package/resources/components/Dashboard/Navbar.stx +2 -2
- package/resources/components/Dashboard/Operations/AuditDashboard.stx +1 -1
- package/resources/components/Dashboard/Operations/OperationHistory.stx +1 -1
- package/resources/components/Dashboard/Operations/RecoveryJobList.stx +1 -1
- package/resources/components/Dashboard/Remote/RemoteConsole.stx +1 -1
- package/resources/components/Dashboard/Settings/SettingsDashboard.stx +1 -1
- package/resources/components/Dashboard/UI/Checkbox.stx +1 -1
- package/resources/components/Dashboard/UI/MetricCards.stx +1 -1
- package/resources/components/Dashboard/UI/Pagination.stx +1 -1
- package/resources/components/Dashboard/UI/Tabs.stx +7 -4
- package/resources/components/Dashboard/UI/Textarea.stx +2 -2
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
54
|
-
<div><dt class="text-gray-
|
|
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-
|
|
53
|
-
<div><dt class="text-gray-
|
|
54
|
-
<div><dt class="text-gray-
|
|
55
|
-
<div><dt class="text-gray-
|
|
56
|
-
<div class="col-span-2"><dt class="text-gray-
|
|
57
|
-
<div class="col-span-2"><dt class="text-gray-
|
|
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-
|
|
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-
|
|
43
|
-
<div :if="record()?.metadata && record()?.metadata !== '{}'" class="mt-5"><p class="text-gray-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
413
|
-
<div class="
|
|
414
|
-
<
|
|
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="
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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>
|