@stacksjs/defaults 0.70.362 → 0.70.365
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-browse/SKILL.md +2 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +20 -2
- package/ai/skills/stacks-dashboard/SKILL.md +4 -0
- package/app/Actions/Auth/LogoutAction.ts +11 -4
- package/app/Actions/Auth/LogoutAllAction.ts +8 -4
- package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
- package/app/Actions/Dashboard/Models/model-write.ts +5 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Buttons/Counter.stx +1 -1
- package/resources/components/Buttons/ToggleDark.stx +1 -1
- package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
- package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
- package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
- package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
- package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
- package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
- package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
- package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
- package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
- package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
- package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
- package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
- package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
- package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
- package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
- package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
- package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
- package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
- package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
- package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
- package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
- package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
- package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
- package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
- package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
- package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
- package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
- package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
- package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
- package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
- package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
- package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
- package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
- package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
- package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
- package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
- package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
- package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
- package/resources/components/Dashboard/GlobalSearch.stx +3 -9
- package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
- package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
- package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
- package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
- package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
- package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
- package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
- package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
- package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
- package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
- package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
- package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
- package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
- package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
- package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
- package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
- package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
- package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
- package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
- package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
- package/resources/components/Dashboard/Navbar.stx +5 -5
- package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
- package/resources/components/Dashboard/UI/Button.stx +4 -0
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +4 -0
- package/resources/components/Dashboard/UI/Drawer.stx +114 -0
- package/resources/components/Dashboard/UI/Modal.stx +19 -12
- package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
- package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
- package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
- package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
- package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
- package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
- package/resources/components/Docs/TeamMember.stx +16 -9
- package/resources/components/Forum/ForumReplyForm.stx +7 -7
- package/resources/components/Forum/ForumSidebar.stx +10 -6
- package/resources/components/Marketing/Faqs.stx +5 -5
- package/resources/components/Marketing/Feature.stx +11 -6
- package/resources/components/Marketing/Layout.stx +12 -8
- package/resources/components/Marketing/Pricing.stx +11 -5
- package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
- package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
- package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
- package/resources/components/Marketing/Testimonials.stx +8 -5
- package/resources/views/index.stx +4 -4
- package/views/system-tray/index.stx +1 -1
|
@@ -32,6 +32,12 @@ interface SeoEntry extends PostRow {
|
|
|
32
32
|
score: number
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
interface SeoDraft {
|
|
36
|
+
focusKeyword: string | null
|
|
37
|
+
metaDescription: string | null
|
|
38
|
+
canonicalUrl: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
const entries = state<SeoEntry[]>([])
|
|
36
42
|
const loading = state(true)
|
|
37
43
|
const saving = state(false)
|
|
@@ -40,9 +46,6 @@ const editError = state('')
|
|
|
40
46
|
const searchQuery = state('')
|
|
41
47
|
const scoreFilter = state<'all' | 'good' | 'medium' | 'poor'>('all')
|
|
42
48
|
const editingEntry = state<SeoEntry | null>(null)
|
|
43
|
-
const editFocusKeyword = state('')
|
|
44
|
-
const editMetaDescription = state('')
|
|
45
|
-
const editCanonical = state('')
|
|
46
49
|
|
|
47
50
|
function seoScore(post: PostRow): number {
|
|
48
51
|
const title = String(post.title || '').trim()
|
|
@@ -143,9 +146,6 @@ function formatDate(value?: string | null): string {
|
|
|
143
146
|
|
|
144
147
|
function openEdit(entry: SeoEntry): void {
|
|
145
148
|
editingEntry.set(entry)
|
|
146
|
-
editFocusKeyword.set(entry.focus_keyword || '')
|
|
147
|
-
editMetaDescription.set(entry.meta_description || '')
|
|
148
|
-
editCanonical.set(entry.canonical_url || '')
|
|
149
149
|
editError.set('')
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -156,7 +156,7 @@ function closeEdit(): void {
|
|
|
156
156
|
editError.set('')
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async function saveEdit(): Promise<void> {
|
|
159
|
+
async function saveEdit(draft: SeoDraft): Promise<void> {
|
|
160
160
|
const entry = editingEntry()
|
|
161
161
|
if (!entry || saving())
|
|
162
162
|
return
|
|
@@ -170,9 +170,9 @@ async function saveEdit(): Promise<void> {
|
|
|
170
170
|
method: 'PATCH',
|
|
171
171
|
body: {
|
|
172
172
|
fields: {
|
|
173
|
-
focus_keyword:
|
|
174
|
-
meta_description:
|
|
175
|
-
canonical_url:
|
|
173
|
+
focus_keyword: draft.focusKeyword,
|
|
174
|
+
meta_description: draft.metaDescription,
|
|
175
|
+
canonical_url: draft.canonicalUrl,
|
|
176
176
|
},
|
|
177
177
|
},
|
|
178
178
|
},
|
|
@@ -311,79 +311,13 @@ void loadEntries()
|
|
|
311
311
|
@endif
|
|
312
312
|
</Card>
|
|
313
313
|
|
|
314
|
-
<
|
|
315
|
-
:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
aria-labelledby="seo-edit-title"
|
|
323
|
-
class="overflow-hidden max-w-lg w-full bg-white dark:bg-neutral-900 rounded-xl shadow-2xl"
|
|
324
|
-
@click.stop
|
|
325
|
-
@submit.prevent="saveEdit()"
|
|
326
|
-
>
|
|
327
|
-
<div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200/60 dark:border-white/10">
|
|
328
|
-
<div>
|
|
329
|
-
<h2 id="seo-edit-title" class="font-semibold text-neutral-900 dark:text-white">Edit SEO metadata</h2>
|
|
330
|
-
<p class="mt-1 text-neutral-500 text-xs dark:text-neutral-400">{{ editingEntry()?.title }}</p>
|
|
331
|
-
</div>
|
|
332
|
-
<Button variant="ghost" size="sm" iconOnly ariaLabel="Close SEO editor" @click="closeEdit()">
|
|
333
|
-
<span class="block h-4 w-4 i-hugeicons-cancel-01"></span>
|
|
334
|
-
</Button>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<div class="p-5 space-y-4">
|
|
338
|
-
@if(editError())
|
|
339
|
-
<div role="alert" class="px-3 py-2 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-500/10 border border-red-200 rounded-lg dark:border-red-500/20">
|
|
340
|
-
{{ editError() }}
|
|
341
|
-
</div>
|
|
342
|
-
@endif
|
|
343
|
-
|
|
344
|
-
<div>
|
|
345
|
-
<label for="seo-focus-keyword" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Focus keyword</label>
|
|
346
|
-
<input
|
|
347
|
-
id="seo-focus-keyword"
|
|
348
|
-
type="text"
|
|
349
|
-
maxlength="100"
|
|
350
|
-
x-model="editFocusKeyword"
|
|
351
|
-
class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
352
|
-
>
|
|
353
|
-
</div>
|
|
354
|
-
<div>
|
|
355
|
-
<div class="flex items-center justify-between">
|
|
356
|
-
<label for="seo-meta-description" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Meta description</label>
|
|
357
|
-
<span class="tabular-nums text-neutral-400 text-xs">{{ editMetaDescription().length }}/160</span>
|
|
358
|
-
</div>
|
|
359
|
-
<textarea
|
|
360
|
-
id="seo-meta-description"
|
|
361
|
-
rows="4"
|
|
362
|
-
maxlength="160"
|
|
363
|
-
x-model="editMetaDescription"
|
|
364
|
-
class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
365
|
-
></textarea>
|
|
366
|
-
</div>
|
|
367
|
-
<div>
|
|
368
|
-
<label for="seo-canonical-url" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Canonical URL</label>
|
|
369
|
-
<input
|
|
370
|
-
id="seo-canonical-url"
|
|
371
|
-
type="url"
|
|
372
|
-
placeholder="https://example.com/post"
|
|
373
|
-
x-model="editCanonical"
|
|
374
|
-
class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white placeholder:text-neutral-400 bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
375
|
-
>
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
<div class="flex gap-2 items-center justify-end px-5 py-4 bg-neutral-50 dark:bg-white/[0.03] border-neutral-200/60 border-t dark:border-white/10">
|
|
380
|
-
<Button type="button" variant="secondary" size="sm" :disabled="saving()" @click="closeEdit()">Cancel</Button>
|
|
381
|
-
<Button type="submit" variant="primary" size="sm" :disabled="saving()">
|
|
382
|
-
<span :if="!saving()">Save metadata</span>
|
|
383
|
-
<span :if="saving()">Saving...</span>
|
|
384
|
-
</Button>
|
|
385
|
-
</div>
|
|
386
|
-
</form>
|
|
387
|
-
</div>
|
|
314
|
+
<SeoEditorDialog
|
|
315
|
+
:open="Boolean(editingEntry())"
|
|
316
|
+
:entry="editingEntry()"
|
|
317
|
+
:busy="saving()"
|
|
318
|
+
:error="editError()"
|
|
319
|
+
@close="closeEdit"
|
|
320
|
+
@submit="saveEdit"
|
|
321
|
+
/>
|
|
388
322
|
</div>
|
|
389
323
|
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
interface SeoEntry {
|
|
3
|
+
title: string
|
|
4
|
+
focus_keyword?: string | null
|
|
5
|
+
meta_description?: string | null
|
|
6
|
+
canonical_url?: string | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SeoDraft {
|
|
10
|
+
focusKeyword: string | null
|
|
11
|
+
metaDescription: string | null
|
|
12
|
+
canonicalUrl: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const open = useReactiveProp('open', false)
|
|
16
|
+
const entry = useReactiveProp('entry', null as SeoEntry | null)
|
|
17
|
+
const busy = useReactiveProp('busy', false)
|
|
18
|
+
const error = useReactiveProp('error', '')
|
|
19
|
+
const focusKeyword = state('')
|
|
20
|
+
const metaDescription = state('')
|
|
21
|
+
const canonicalUrl = state('')
|
|
22
|
+
const emit = defineEmits()
|
|
23
|
+
let wasOpen = false
|
|
24
|
+
|
|
25
|
+
effect(() => {
|
|
26
|
+
const isOpen = open()
|
|
27
|
+
if (isOpen && !wasOpen) {
|
|
28
|
+
const record = entry()
|
|
29
|
+
focusKeyword.set(record?.focus_keyword || '')
|
|
30
|
+
metaDescription.set(record?.meta_description || '')
|
|
31
|
+
canonicalUrl.set(record?.canonical_url || '')
|
|
32
|
+
}
|
|
33
|
+
wasOpen = isOpen
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function close(): void {
|
|
37
|
+
if (!busy())
|
|
38
|
+
emit('close')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function submit(): void {
|
|
42
|
+
const draft: SeoDraft = {
|
|
43
|
+
focusKeyword: focusKeyword().trim() || null,
|
|
44
|
+
metaDescription: metaDescription().trim() || null,
|
|
45
|
+
canonicalUrl: canonicalUrl().trim() || null,
|
|
46
|
+
}
|
|
47
|
+
emit('submit', draft)
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<Modal
|
|
52
|
+
:isOpen="open()"
|
|
53
|
+
title="Edit SEO metadata"
|
|
54
|
+
:description="entry()?.title || ''"
|
|
55
|
+
:closable="!busy()"
|
|
56
|
+
@close="close"
|
|
57
|
+
>
|
|
58
|
+
<form id="seo-editor-form" class="space-y-4" @submit.prevent="submit">
|
|
59
|
+
<div>
|
|
60
|
+
<label for="seo-focus-keyword" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Focus keyword</label>
|
|
61
|
+
<input
|
|
62
|
+
id="seo-focus-keyword"
|
|
63
|
+
name="focus_keyword"
|
|
64
|
+
type="text"
|
|
65
|
+
maxlength="100"
|
|
66
|
+
x-model="focusKeyword"
|
|
67
|
+
class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
68
|
+
>
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<div class="flex items-center justify-between">
|
|
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>
|
|
74
|
+
</div>
|
|
75
|
+
<textarea
|
|
76
|
+
id="seo-meta-description"
|
|
77
|
+
name="meta_description"
|
|
78
|
+
rows="4"
|
|
79
|
+
maxlength="160"
|
|
80
|
+
x-model="metaDescription"
|
|
81
|
+
class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
82
|
+
></textarea>
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<label for="seo-canonical-url" class="block font-medium text-neutral-700 text-sm dark:text-neutral-300">Canonical URL</label>
|
|
86
|
+
<input
|
|
87
|
+
id="seo-canonical-url"
|
|
88
|
+
name="canonical_url"
|
|
89
|
+
type="url"
|
|
90
|
+
inputmode="url"
|
|
91
|
+
placeholder="https://example.com/post"
|
|
92
|
+
x-model="canonicalUrl"
|
|
93
|
+
class="block mt-1.5 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white placeholder:text-neutral-400 bg-white dark:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
94
|
+
>
|
|
95
|
+
</div>
|
|
96
|
+
</form>
|
|
97
|
+
|
|
98
|
+
<div :if="error()" role="alert" class="mt-4 px-3 py-2 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-500/10 border border-red-200 rounded-lg dark:border-red-500/20">
|
|
99
|
+
{{ error() }}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<template #footer>
|
|
103
|
+
<div class="flex gap-2 items-center justify-end w-full">
|
|
104
|
+
<Button type="button" variant="secondary" :disabled="busy()" @click="close">Cancel</Button>
|
|
105
|
+
<Button type="submit" form="seo-editor-form" :loading="busy()">Save metadata</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
108
|
+
</Modal>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
interface TaxonomyDraft {
|
|
3
|
+
name: string
|
|
4
|
+
slug: string
|
|
5
|
+
description: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const open = useReactiveProp('open', false)
|
|
9
|
+
const kind = useReactiveProp('kind', 'category')
|
|
10
|
+
const busy = useReactiveProp('busy', false)
|
|
11
|
+
const error = useReactiveProp('error', '')
|
|
12
|
+
const name = state('')
|
|
13
|
+
const slug = state('')
|
|
14
|
+
const description = state('')
|
|
15
|
+
const slugTouched = state(false)
|
|
16
|
+
const emit = defineEmits()
|
|
17
|
+
let wasOpen = false
|
|
18
|
+
|
|
19
|
+
function slugify(value: string): string {
|
|
20
|
+
return value
|
|
21
|
+
.normalize('NFKD')
|
|
22
|
+
.replace(/[\u0300-\u036F]/g, '')
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/['"]/g, '')
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
effect(() => {
|
|
31
|
+
const isOpen = open()
|
|
32
|
+
if (isOpen && !wasOpen) {
|
|
33
|
+
name.set('')
|
|
34
|
+
slug.set('')
|
|
35
|
+
description.set('')
|
|
36
|
+
slugTouched.set(false)
|
|
37
|
+
}
|
|
38
|
+
wasOpen = isOpen
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
effect(() => {
|
|
42
|
+
if (!open() || slugTouched())
|
|
43
|
+
return
|
|
44
|
+
slug.set(slugify(name()))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function markSlugTouched(): void {
|
|
48
|
+
slugTouched.set(true)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeSlug(): void {
|
|
52
|
+
const normalized = slugify(slug())
|
|
53
|
+
if (normalized !== slug())
|
|
54
|
+
slug.set(normalized)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function close(): void {
|
|
58
|
+
if (!busy())
|
|
59
|
+
emit('close')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function submit(): void {
|
|
63
|
+
const draft: TaxonomyDraft = {
|
|
64
|
+
name: name().trim(),
|
|
65
|
+
slug: slugify(slug()),
|
|
66
|
+
description: description().trim(),
|
|
67
|
+
}
|
|
68
|
+
emit('submit', draft)
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<Modal
|
|
73
|
+
:isOpen="open()"
|
|
74
|
+
:title="'New ' + kind()"
|
|
75
|
+
description="Add a reusable classification for your published content."
|
|
76
|
+
:closable="!busy()"
|
|
77
|
+
@close="close"
|
|
78
|
+
>
|
|
79
|
+
<form id="taxonomy-editor-form" class="grid gap-4" @submit.prevent="submit">
|
|
80
|
+
<label>
|
|
81
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span>
|
|
82
|
+
<input x-model="name" name="name" required maxlength="255" 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" />
|
|
83
|
+
</label>
|
|
84
|
+
<label>
|
|
85
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Slug</span>
|
|
86
|
+
<input x-model="slug" name="slug" required maxlength="255" 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" />
|
|
87
|
+
</label>
|
|
88
|
+
<label>
|
|
89
|
+
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span>
|
|
90
|
+
<textarea x-model="description" name="description" rows="3" maxlength="1000" 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>
|
|
91
|
+
</label>
|
|
92
|
+
</form>
|
|
93
|
+
|
|
94
|
+
<div :if="error()" role="alert" class="mt-4 p-3 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">
|
|
95
|
+
{{ error() }}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<template #footer>
|
|
99
|
+
<div class="flex gap-2 justify-end w-full">
|
|
100
|
+
<Button :disabled="busy()" variant="secondary" @click="close">Cancel</Button>
|
|
101
|
+
<Button type="submit" form="taxonomy-editor-form" :disabled="!name().trim() || !slug().trim()" :loading="busy()">
|
|
102
|
+
Create {{ kind() }}
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
106
|
+
</Modal>
|
|
@@ -63,87 +63,76 @@ function close(): void {
|
|
|
63
63
|
}
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<footer class="flex gap-2 items-center justify-end px-5 py-4 bg-neutral-50/50 dark:bg-neutral-800/50 border-neutral-200 border-t dark:border-neutral-700">
|
|
138
|
-
<Button :disabled="busy()" variant="secondary" @click="close">
|
|
139
|
-
Cancel
|
|
140
|
-
</Button>
|
|
141
|
-
<Button type="submit" :disabled="busy() || fields().length === 0" :loading="busy()">
|
|
142
|
-
{{ busy() ? 'Creating' : `Create ${modelName()}` }}
|
|
143
|
-
</Button>
|
|
144
|
-
</footer>
|
|
145
|
-
</form>
|
|
146
|
-
</section>
|
|
66
|
+
<Modal
|
|
67
|
+
:isOpen="open()"
|
|
68
|
+
:title="'Create ' + modelName()"
|
|
69
|
+
description="Fields come from the model's fillable attributes and useApi store contract."
|
|
70
|
+
:closable="!busy()"
|
|
71
|
+
@close="close"
|
|
72
|
+
>
|
|
73
|
+
<form id="create-record-form" @submit.prevent="submit">
|
|
74
|
+
<div class="overflow-y-auto space-y-4 max-h-[65vh]">
|
|
75
|
+
<div :if="error()" role="alert" class="p-3 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">
|
|
76
|
+
{{ error() }}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<template :for="field in fields()">
|
|
80
|
+
<label :if="field.type === 'boolean'" class="flex gap-3 items-start p-3 border border-neutral-200 rounded-lg dark:border-neutral-700">
|
|
81
|
+
<input
|
|
82
|
+
:name="field.name"
|
|
83
|
+
type="checkbox"
|
|
84
|
+
:checked="Boolean(values()[field.name])"
|
|
85
|
+
:disabled="busy()"
|
|
86
|
+
class="mt-0.5 h-4 w-4 text-blue-600 border-neutral-300 rounded"
|
|
87
|
+
@change="setValue(field.name, $event.target.checked)"
|
|
88
|
+
/>
|
|
89
|
+
<span>
|
|
90
|
+
<span class="block font-medium text-neutral-700 text-sm dark:text-neutral-200">{{ field.label }}</span>
|
|
91
|
+
<span :if="fieldError(field.name)" class="block mt-0.5 text-red-600 text-xs dark:text-red-400">{{ fieldError(field.name) }}</span>
|
|
92
|
+
</span>
|
|
93
|
+
</label>
|
|
94
|
+
|
|
95
|
+
<Select
|
|
96
|
+
:if="field.type === 'enum'"
|
|
97
|
+
:name="field.name"
|
|
98
|
+
:value="fieldValue(field.name)"
|
|
99
|
+
:options="field.options"
|
|
100
|
+
:label="field.label"
|
|
101
|
+
:ariaLabel="field.label"
|
|
102
|
+
:required="field.required"
|
|
103
|
+
:disabled="busy()"
|
|
104
|
+
:error="fieldError(field.name)"
|
|
105
|
+
@update:value="setValue(field.name, $event)"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<Input
|
|
109
|
+
:if="field.type !== 'boolean' && field.type !== 'enum'"
|
|
110
|
+
:name="field.name"
|
|
111
|
+
:value="fieldValue(field.name)"
|
|
112
|
+
:type="inputType(field)"
|
|
113
|
+
:label="field.label"
|
|
114
|
+
:ariaLabel="field.label"
|
|
115
|
+
:required="field.required"
|
|
116
|
+
:disabled="busy()"
|
|
117
|
+
:error="fieldError(field.name)"
|
|
118
|
+
@update:value="setValue(field.name, $event)"
|
|
119
|
+
/>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<p :if="fields().length === 0" class="py-8 text-center text-neutral-500 text-sm dark:text-neutral-400">
|
|
123
|
+
This model exposes no safe fillable fields.
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
<template #footer>
|
|
129
|
+
<div class="flex gap-2 items-center justify-end w-full">
|
|
130
|
+
<Button :disabled="busy()" variant="secondary" @click="close">
|
|
131
|
+
Cancel
|
|
132
|
+
</Button>
|
|
133
|
+
<Button type="submit" form="create-record-form" :disabled="busy() || fields().length === 0" :loading="busy()">
|
|
134
|
+
{{ busy() ? 'Creating' : `Create ${modelName()}` }}
|
|
135
|
+
</Button>
|
|
147
136
|
</div>
|
|
148
|
-
</
|
|
149
|
-
</
|
|
137
|
+
</template>
|
|
138
|
+
</Modal>
|
|
@@ -68,7 +68,7 @@ function listClass(): string {
|
|
|
68
68
|
<p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ email.preview }}</p>
|
|
69
69
|
</div>
|
|
70
70
|
<div class="flex flex-shrink-0 gap-2 items-center">
|
|
71
|
-
<span :if="email.hasAttachments" class="h-4 w-4 text-gray-400 i-hugeicons-attachment-01" title="Has attachments"></span>
|
|
71
|
+
<span :if="email.hasAttachments" class="h-4 w-4 text-gray-400 i-hugeicons-attachment-01" x-tooltip="Has attachments" title="Has attachments"></span>
|
|
72
72
|
<time class="text-gray-500 text-xs dark:text-neutral-400" :datetime="email.date">{{ formatDate(email.date) }}</time>
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
@@ -10,7 +10,6 @@ const isOpen = state(false)
|
|
|
10
10
|
const selectedIndex = state(0)
|
|
11
11
|
const error = state('')
|
|
12
12
|
const searchInput = useRef('globalSearchInput')
|
|
13
|
-
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
|
14
13
|
let searchRequest: AbortController | null = null
|
|
15
14
|
|
|
16
15
|
function flatResults(): SearchResultItem[] {
|
|
@@ -74,13 +73,9 @@ async function runSearch(value: string): Promise<void> {
|
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
searchTimer = null
|
|
81
|
-
void runSearch(value)
|
|
82
|
-
}, 250)
|
|
83
|
-
}
|
|
76
|
+
const scheduleSearch = useDebounce((value: string) => {
|
|
77
|
+
void runSearch(value)
|
|
78
|
+
}, 250)
|
|
84
79
|
|
|
85
80
|
function moveUp(): void {
|
|
86
81
|
const items = flatResults()
|
|
@@ -137,7 +132,6 @@ function handleGlobalKeydown(event: KeyboardEvent): void {
|
|
|
137
132
|
useEventListener('keydown', handleGlobalKeydown)
|
|
138
133
|
|
|
139
134
|
onDestroy(() => {
|
|
140
|
-
if (searchTimer) clearTimeout(searchTimer)
|
|
141
135
|
searchRequest?.abort()
|
|
142
136
|
})
|
|
143
137
|
</script>
|
|
@@ -6,19 +6,7 @@ const item = useReactiveProp('item', null as DashboardCloudResource | null)
|
|
|
6
6
|
const emit = defineEmits()
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
|
-
<
|
|
10
|
-
<button type="button" aria-label="Close cloud resource details" class="absolute inset-0 bg-black/45" @click="emit('close')"></button>
|
|
11
|
-
<section role="dialog" aria-modal="true" aria-labelledby="cloud-resource-title" class="overflow-y-auto relative max-h-[90vh] max-w-lg w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
|
|
12
|
-
<header class="flex gap-4 items-start justify-between px-5 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
13
|
-
<div>
|
|
14
|
-
<p class="capitalize text-neutral-500 text-xs dark:text-neutral-400">{{ item()?.category }} resource</p>
|
|
15
|
-
<h2 id="cloud-resource-title" class="mt-1 font-semibold text-lg text-neutral-900 dark:text-white">{{ item()?.name }}</h2>
|
|
16
|
-
</div>
|
|
17
|
-
<Button variant="ghost" iconOnly ariaLabel="Close cloud resource details" @click="emit('close')">
|
|
18
|
-
<span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
|
|
19
|
-
</Button>
|
|
20
|
-
</header>
|
|
21
|
-
<div class="p-5">
|
|
9
|
+
<Modal :isOpen="open() && Boolean(item())" :title="item()?.name || 'Cloud resource'" :description="`${item()?.category || 'Cloud'} resource`" size="md" @close="emit('close')">
|
|
22
10
|
<div class="flex gap-3 items-center">
|
|
23
11
|
<CloudStatusBadge :status="item()?.status || 'inactive'" />
|
|
24
12
|
<span class="capitalize text-neutral-500 text-sm dark:text-neutral-400">{{ item()?.type }}</span>
|
|
@@ -31,6 +19,4 @@ const emit = defineEmits()
|
|
|
31
19
|
</div>
|
|
32
20
|
</dl>
|
|
33
21
|
<p :if="(item()?.details || []).length === 0" class="mt-5 p-3 text-neutral-500 text-sm dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 rounded-lg">No additional configuration fields are recorded for this resource.</p>
|
|
34
|
-
|
|
35
|
-
</section>
|
|
36
|
-
</div>
|
|
22
|
+
</Modal>
|
|
@@ -10,25 +10,13 @@ function ttlLabel(): string {
|
|
|
10
10
|
}
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
<section role="dialog" aria-modal="true" aria-labelledby="dns-record-title" class="overflow-y-auto relative max-h-[90vh] max-w-lg w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
|
|
16
|
-
<header class="flex gap-4 items-start justify-between px-5 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
17
|
-
<div>
|
|
18
|
-
<p class="text-neutral-500 text-xs dark:text-neutral-400">Configured DNS record</p>
|
|
19
|
-
<h2 id="dns-record-title" class="mt-1 font-semibold text-lg text-neutral-900 dark:text-white">{{ record()?.type }} · {{ record()?.name }}</h2>
|
|
20
|
-
</div>
|
|
21
|
-
<Button variant="ghost" iconOnly ariaLabel="Close DNS record details" @click="emit('close')">
|
|
22
|
-
<span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
|
|
23
|
-
</Button>
|
|
24
|
-
</header>
|
|
25
|
-
<dl class="grid gap-5 sm:grid-cols-2 p-5 text-sm">
|
|
13
|
+
<Modal :isOpen="open() && Boolean(record())" :title="`${record()?.type || 'DNS'} · ${record()?.name || 'record'}`" description="Configured DNS record" size="md" @close="emit('close')">
|
|
14
|
+
<dl class="grid gap-5 sm:grid-cols-2 text-sm">
|
|
26
15
|
<div><dt class="text-neutral-500 dark:text-neutral-400">Type</dt><dd class="mt-1 font-semibold text-neutral-900 dark:text-white">{{ record()?.type }}</dd></div>
|
|
27
16
|
<div><dt class="text-neutral-500 dark:text-neutral-400">Name</dt><dd class="mt-1 break-all font-mono text-neutral-900 dark:text-white">{{ record()?.name }}</dd></div>
|
|
28
17
|
<div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">Value</dt><dd class="mt-1 break-all font-mono text-neutral-900 dark:text-white">{{ record()?.value || 'Not set' }}</dd></div>
|
|
29
18
|
<div><dt class="text-neutral-500 dark:text-neutral-400">TTL</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ ttlLabel() }}</dd></div>
|
|
30
19
|
<div :if="record()?.priority !== null"><dt class="text-neutral-500 dark:text-neutral-400">Priority</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ record()?.priority }}</dd></div>
|
|
31
20
|
</dl>
|
|
32
|
-
<p class="
|
|
33
|
-
|
|
34
|
-
</div>
|
|
21
|
+
<p class="mt-5 p-3 text-neutral-500 text-xs dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 rounded-lg">Source: config/dns.ts</p>
|
|
22
|
+
</Modal>
|