@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
|
@@ -46,6 +46,7 @@ interface ResponseShape {
|
|
|
46
46
|
writable: boolean
|
|
47
47
|
writeCapabilities: WriteCapabilities
|
|
48
48
|
createFields: CreateField[]
|
|
49
|
+
updateColumns: string[]
|
|
49
50
|
error: string | null
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -66,15 +67,14 @@ const search = state(navigation.urlQuery('q') || '')
|
|
|
66
67
|
const filters = state<Record<string, string>>({})
|
|
67
68
|
const writeCapabilities = state<WriteCapabilities>({ create: false, update: false, destroy: false })
|
|
68
69
|
const createFields = state<CreateField[]>([])
|
|
70
|
+
const updateColumns = state<string[]>([])
|
|
69
71
|
const error = state<string | null>(null)
|
|
70
72
|
const loading = state(true)
|
|
71
73
|
const showFilters = state(false)
|
|
72
74
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// it was and a failed save does not desync the two.
|
|
75
|
+
// The row being edited. `null` closes the editor; EditRecordModal owns the
|
|
76
|
+
// working copy so cancelling leaves the table row unchanged.
|
|
76
77
|
const editing = state<Record<string, unknown> | null>(null)
|
|
77
|
-
const editValues = state<Record<string, string>>({})
|
|
78
78
|
const saving = state(false)
|
|
79
79
|
const confirmingDelete = state<Record<string, unknown> | null>(null)
|
|
80
80
|
const showCreate = state(false)
|
|
@@ -113,9 +113,9 @@ const deleteDescription = derived(() => {
|
|
|
113
113
|
return `Row ${row.id} will be removed through the model, so soft deletes and observers apply.`
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
// Fields for the edit
|
|
117
|
-
// row being edited rather than from
|
|
118
|
-
// once per open: deriving it from the working copy
|
|
116
|
+
// Fields for the edit modal, carrying their starting value. Derived from the
|
|
117
|
+
// row being edited rather than from its mutable draft, so the list changes
|
|
118
|
+
// exactly once per open: deriving it from the working copy would rebuild
|
|
119
119
|
// every input on each keystroke and steal focus mid-word.
|
|
120
120
|
const editFields = derived(() => {
|
|
121
121
|
const row = editing()
|
|
@@ -124,10 +124,9 @@ const editFields = derived(() => {
|
|
|
124
124
|
name: column.name,
|
|
125
125
|
label: column.label,
|
|
126
126
|
value: row[column.name] === null || row[column.name] === undefined ? '' : String(row[column.name]),
|
|
127
|
-
// The
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
readonly: column.name === 'id' || column.name === 'uuid' || column.name.endsWith('_at'),
|
|
127
|
+
// The model API accepts only fillable attributes and declared belongs-to
|
|
128
|
+
// keys. Every other database column stays visible as read-only context.
|
|
129
|
+
readonly: !updateColumns().includes(column.name),
|
|
131
130
|
}))
|
|
132
131
|
})
|
|
133
132
|
|
|
@@ -213,6 +212,7 @@ async function load() {
|
|
|
213
212
|
sort.set(data.sort ?? '')
|
|
214
213
|
writeCapabilities.set(data.writeCapabilities ?? { create: false, update: false, destroy: false })
|
|
215
214
|
createFields.set(data.createFields ?? [])
|
|
215
|
+
updateColumns.set(data.updateColumns ?? [])
|
|
216
216
|
error.set(data.error ?? null)
|
|
217
217
|
}
|
|
218
218
|
catch (e) {
|
|
@@ -230,26 +230,17 @@ function toggleSort(column: ColumnMeta) {
|
|
|
230
230
|
void load()
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
// Debounced so a query is not fired per keystroke.
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
searchTimer = setTimeout(() => {
|
|
241
|
-
page.set(1)
|
|
242
|
-
void load()
|
|
243
|
-
}, 250)
|
|
244
|
-
}
|
|
233
|
+
// Debounced so a query is not fired per keystroke. Search and the per-column
|
|
234
|
+
// filters deliberately share one debounce, the way they shared one timer id
|
|
235
|
+
// before: typing in either should schedule a single reload, not two.
|
|
236
|
+
const onSearchInput = useDebounce(() => {
|
|
237
|
+
page.set(1)
|
|
238
|
+
void load()
|
|
239
|
+
}, 250)
|
|
245
240
|
|
|
246
241
|
function onFilterInput(column: string, event: Event) {
|
|
247
242
|
filters.set({ ...filters(), [column]: (event.target as HTMLInputElement).value })
|
|
248
|
-
|
|
249
|
-
searchTimer = setTimeout(() => {
|
|
250
|
-
page.set(1)
|
|
251
|
-
void load()
|
|
252
|
-
}, 250)
|
|
243
|
+
onSearchInput()
|
|
253
244
|
}
|
|
254
245
|
|
|
255
246
|
function clearFilters() {
|
|
@@ -272,12 +263,6 @@ function onPerPageChange() {
|
|
|
272
263
|
}
|
|
273
264
|
|
|
274
265
|
function startEdit(row: Record<string, unknown>) {
|
|
275
|
-
const values: Record<string, string> = {}
|
|
276
|
-
for (const column of columnMeta()) {
|
|
277
|
-
const raw = row[column.name]
|
|
278
|
-
values[column.name] = raw === null || raw === undefined ? '' : String(raw)
|
|
279
|
-
}
|
|
280
|
-
editValues.set(values)
|
|
281
266
|
editing.set(row)
|
|
282
267
|
}
|
|
283
268
|
|
|
@@ -319,17 +304,13 @@ async function createRecord(fields: Record<string, unknown>): Promise<void> {
|
|
|
319
304
|
}
|
|
320
305
|
}
|
|
321
306
|
|
|
322
|
-
function
|
|
323
|
-
editValues.set({ ...editValues(), [column]: (event.target as HTMLInputElement).value })
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function saveEdit() {
|
|
307
|
+
async function saveEdit(values: Record<string, string>) {
|
|
327
308
|
const row = editing()
|
|
328
309
|
if (!row) return
|
|
329
310
|
// Send only what actually changed, so a save never rewrites columns the
|
|
330
311
|
// editor merely displayed.
|
|
331
312
|
const fields: Record<string, unknown> = {}
|
|
332
|
-
for (const [column, value] of Object.entries(
|
|
313
|
+
for (const [column, value] of Object.entries(values)) {
|
|
333
314
|
const original = row[column]
|
|
334
315
|
const originalText = original === null || original === undefined ? '' : String(original)
|
|
335
316
|
if (value !== originalText) fields[column] = value === '' ? null : value
|
|
@@ -492,7 +473,7 @@ void load()
|
|
|
492
473
|
</button>
|
|
493
474
|
</th>
|
|
494
475
|
</template>
|
|
495
|
-
<th :if="canManageRows()" scope="col" class="px-3 py-2 w-24 font-medium text-[11px] text-neutral-500 text-right tracking-wide uppercase dark:text-neutral-400">Actions</th>
|
|
476
|
+
<th :if="canManageRows()" scope="col" class="right-0 sticky z-20 px-3 py-2 w-24 font-medium text-[11px] text-neutral-500 text-right tracking-wide uppercase dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-800 border-black/5 border-l dark:border-white/10">Actions</th>
|
|
496
477
|
</tr>
|
|
497
478
|
|
|
498
479
|
<!-- Per-column filter row -->
|
|
@@ -508,7 +489,7 @@ void load()
|
|
|
508
489
|
>
|
|
509
490
|
</th>
|
|
510
491
|
</template>
|
|
511
|
-
<th :if="canManageRows()"></th>
|
|
492
|
+
<th :if="canManageRows()" class="right-0 sticky z-20 bg-white dark:bg-neutral-900 border-black/5 border-l dark:border-white/10"></th>
|
|
512
493
|
</tr>
|
|
513
494
|
</thead>
|
|
514
495
|
|
|
@@ -524,7 +505,7 @@ void load()
|
|
|
524
505
|
<div class="h-3 bg-neutral-200 dark:bg-white/10 rounded animate-pulse"></div>
|
|
525
506
|
</td>
|
|
526
507
|
</template>
|
|
527
|
-
<td :if="canManageRows()" class="px-3 py-2">
|
|
508
|
+
<td :if="canManageRows()" class="right-0 sticky z-10 px-3 py-2 bg-white dark:bg-neutral-900 border-black/5 border-l dark:border-white/10">
|
|
528
509
|
<div class="ml-auto h-3 w-16 bg-neutral-200 dark:bg-white/10 rounded animate-pulse"></div>
|
|
529
510
|
</td>
|
|
530
511
|
</tr>
|
|
@@ -533,19 +514,19 @@ void load()
|
|
|
533
514
|
|
|
534
515
|
<tbody :if="!loading()" class="divide-black/5 divide-y dark:divide-white/8">
|
|
535
516
|
<template :for="row in rows()">
|
|
536
|
-
<tr class="hover:bg-neutral-50 dark:hover:bg-white/5 transition-colors">
|
|
517
|
+
<tr class="hover:bg-neutral-50 dark:hover:bg-white/5 transition-colors group">
|
|
537
518
|
<template :for="column in columnMeta()">
|
|
538
519
|
<td :class="cellClass(column)" :title="cellValue(row, column)">{{ cellValue(row, column) }}</td>
|
|
539
520
|
</template>
|
|
540
|
-
<td :if="canManageRows()" class="px-3 py-2 text-right">
|
|
541
|
-
<
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
521
|
+
<td :if="canManageRows()" class="right-0 sticky z-10 px-3 py-2 text-right bg-white dark:bg-neutral-900 group-hover:bg-neutral-50 dark:group-hover:bg-white/5 border-black/5 border-l dark:border-white/10 transition-colors">
|
|
522
|
+
<ModelRecordActions
|
|
523
|
+
:record="row"
|
|
524
|
+
:modelName="modelName()"
|
|
525
|
+
:canUpdate="writeCapabilities().update"
|
|
526
|
+
:canDestroy="writeCapabilities().destroy"
|
|
527
|
+
@edit="startEdit"
|
|
528
|
+
@delete="confirmingDelete.set($event)"
|
|
529
|
+
/>
|
|
549
530
|
</td>
|
|
550
531
|
</tr>
|
|
551
532
|
</template>
|
|
@@ -565,47 +546,14 @@ void load()
|
|
|
565
546
|
</div>
|
|
566
547
|
</div>
|
|
567
548
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
>
|
|
577
|
-
<div class="flex items-center justify-between px-4 py-3 border-b border-black/5 dark:border-white/10">
|
|
578
|
-
<h2 id="model-edit-title" class="font-semibold text-[14px] text-neutral-900 dark:text-white">Edit <span :text="modelName()"></span></h2>
|
|
579
|
-
<Button variant="ghost" size="sm" iconOnly ariaLabel="Close record editor" @click="editing.set(null)">
|
|
580
|
-
<span class="h-4 w-4 i-hugeicons-cancel-01"></span>
|
|
581
|
-
</Button>
|
|
582
|
-
</div>
|
|
583
|
-
|
|
584
|
-
<div class="overflow-y-auto flex-1 p-4 space-y-3">
|
|
585
|
-
<template :for="field in editFields()">
|
|
586
|
-
<div class="flex flex-col gap-1.5">
|
|
587
|
-
<label :for="'model-edit-' + field.name" class="font-medium text-[12px] text-neutral-600 dark:text-neutral-300">{{ field.label }}</label>
|
|
588
|
-
<input
|
|
589
|
-
:id="'model-edit-' + field.name"
|
|
590
|
-
type="text"
|
|
591
|
-
:value="field.value"
|
|
592
|
-
@input="onEditInput(field.name, $event)"
|
|
593
|
-
:readonly="field.readonly"
|
|
594
|
-
class="px-2.5 py-1.5 text-[13px] text-neutral-900 dark:text-neutral-100 read-only:text-neutral-500 dark:read-only:text-neutral-400 bg-white dark:bg-white/10 read-only:bg-neutral-50 dark:read-only:bg-white/5 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
|
595
|
-
>
|
|
596
|
-
</div>
|
|
597
|
-
</template>
|
|
598
|
-
</div>
|
|
599
|
-
|
|
600
|
-
<div class="flex gap-2 items-center justify-end px-4 py-3 bg-neutral-50 dark:bg-white/5 border-black/5 border-t dark:border-white/10">
|
|
601
|
-
<Button :disabled="saving()" variant="secondary" @click="editing.set(null)">Cancel</Button>
|
|
602
|
-
<Button :loading="saving()" @click="saveEdit()">
|
|
603
|
-
<span :if="!saving()">Save changes</span>
|
|
604
|
-
<span :if="saving()">Saving</span>
|
|
605
|
-
</Button>
|
|
606
|
-
</div>
|
|
607
|
-
</div>
|
|
608
|
-
</div>
|
|
549
|
+
<EditRecordModal
|
|
550
|
+
:open="Boolean(editing())"
|
|
551
|
+
:modelName="modelName()"
|
|
552
|
+
:fields="editFields()"
|
|
553
|
+
:busy="saving()"
|
|
554
|
+
@close="editing.set(null)"
|
|
555
|
+
@submit="saveEdit($event)"
|
|
556
|
+
/>
|
|
609
557
|
|
|
610
558
|
<ConfirmDialog
|
|
611
559
|
:isOpen="Boolean(confirmingDelete())"
|
|
@@ -69,27 +69,27 @@ useClickOutside(userMenu, () => setDropdownOpen(false))
|
|
|
69
69
|
</StxLink>
|
|
70
70
|
|
|
71
71
|
<nav class="flex gap-x-4 items-center lg:gap-x-6" aria-label="Dashboard utilities">
|
|
72
|
-
<a href="/errors" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/errors') ? 'active-header-link' : '' }}" title="View errors">
|
|
72
|
+
<a href="/errors" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/errors') ? 'active-header-link' : '' }}" x-tooltip="View errors" title="View errors">
|
|
73
73
|
<span class="sr-only">View errors</span>
|
|
74
74
|
<span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-alert-01"></span>
|
|
75
75
|
</a>
|
|
76
76
|
|
|
77
|
-
<a href="/health" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/health') ? 'active-header-link' : '' }}" title="View health">
|
|
77
|
+
<a href="/health" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/health') ? 'active-header-link' : '' }}" x-tooltip="View health" title="View health">
|
|
78
78
|
<span class="sr-only">View health</span>
|
|
79
79
|
<span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-health"></span>
|
|
80
80
|
</a>
|
|
81
81
|
|
|
82
|
-
<a href="/insights" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/insights') ? 'active-header-link' : '' }}" title="View cloud insights">
|
|
82
|
+
<a href="/insights" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/insights') ? 'active-header-link' : '' }}" x-tooltip="View cloud insights" title="View cloud insights">
|
|
83
83
|
<span class="sr-only">View cloud insights</span>
|
|
84
84
|
<span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-pulse-01"></span>
|
|
85
85
|
</a>
|
|
86
86
|
|
|
87
|
-
<a href="/inbox" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/inbox') ? 'active-header-link' : '' }}" title="View inbox">
|
|
87
|
+
<a href="/inbox" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/inbox') ? 'active-header-link' : '' }}" x-tooltip="View inbox" title="View inbox">
|
|
88
88
|
<span class="sr-only">View inbox</span>
|
|
89
89
|
<span aria-hidden="true" class="h-6 w-6 duration-150 ease-in-out transition i-hugeicons-mail-02"></span>
|
|
90
90
|
</a>
|
|
91
91
|
|
|
92
|
-
<a href="/notifications/dashboard" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/notifications/dashboard') ? 'active-header-link' : '' }}" title="View notifications">
|
|
92
|
+
<a href="/notifications/dashboard" data-stx-link class="header-icon-button header-link {{ isActiveRoute('/notifications/dashboard') ? 'active-header-link' : '' }}" x-tooltip="View notifications" title="View notifications">
|
|
93
93
|
<span class="sr-only">View notifications</span>
|
|
94
94
|
<span aria-hidden="true" class="h-6 w-6 i-hugeicons-notification-02"></span>
|
|
95
95
|
</a>
|
|
@@ -21,64 +21,44 @@ function metadataText(): string {
|
|
|
21
21
|
}
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<
|
|
25
|
-
:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
<Modal
|
|
25
|
+
:isOpen="open()"
|
|
26
|
+
:title="`Delivery attempt #${delivery()?.id || 'not recorded'}`"
|
|
27
|
+
:description="delivery()?.channel || 'Notification'"
|
|
28
|
+
size="lg"
|
|
29
|
+
@close="emit('close')"
|
|
30
30
|
>
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<dd class="mt-1 p-3 text-gray-800 text-sm whitespace-pre-wrap dark:text-neutral-200 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ delivery?.body || 'No body was recorded.' }}</dd>
|
|
66
|
-
</div>
|
|
67
|
-
<div :if="delivery?.error" class="sm:col-span-2">
|
|
68
|
-
<dt class="font-medium text-red-600 text-xs uppercase dark:text-red-400">Error</dt>
|
|
69
|
-
<dd class="mt-1 p-3 text-red-800 text-sm whitespace-pre-wrap dark:text-red-200 bg-red-50 dark:bg-red-950/40 rounded-md">{{ delivery?.error }}</dd>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="sm:col-span-2">
|
|
72
|
-
<dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Metadata</dt>
|
|
73
|
-
<dd class="overflow-x-auto mt-1 p-3 font-mono text-gray-700 text-xs whitespace-pre dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ metadataText() }}</dd>
|
|
74
|
-
</div>
|
|
75
|
-
</dl>
|
|
76
|
-
|
|
77
|
-
<div class="flex justify-end mt-6">
|
|
78
|
-
<Button variant="secondary" @click="emit('close')">
|
|
79
|
-
Close
|
|
80
|
-
</Button>
|
|
81
|
-
</div>
|
|
82
|
-
</section>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
31
|
+
<dl class="grid gap-x-6 gap-y-4 grid-cols-1 sm:grid-cols-2">
|
|
32
|
+
<div>
|
|
33
|
+
<dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Recipient</dt>
|
|
34
|
+
<dd class="mt-1 break-all text-gray-900 text-sm dark:text-neutral-100">{{ delivery()?.recipient || 'Not recorded' }}</dd>
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Attempted</dt>
|
|
38
|
+
<dd class="mt-1 text-gray-900 text-sm dark:text-neutral-100">{{ formatDate(delivery()?.sent_at) }}</dd>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="sm:col-span-2">
|
|
41
|
+
<dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Subject</dt>
|
|
42
|
+
<dd class="mt-1 text-gray-900 text-sm dark:text-neutral-100">{{ delivery()?.subject || 'No subject' }}</dd>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="sm:col-span-2">
|
|
45
|
+
<dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Body</dt>
|
|
46
|
+
<dd class="mt-1 p-3 text-gray-800 text-sm whitespace-pre-wrap dark:text-neutral-200 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ delivery()?.body || 'No body was recorded.' }}</dd>
|
|
47
|
+
</div>
|
|
48
|
+
<div :if="delivery()?.error" class="sm:col-span-2">
|
|
49
|
+
<dt class="font-medium text-red-600 text-xs uppercase dark:text-red-400">Error</dt>
|
|
50
|
+
<dd class="mt-1 p-3 text-red-800 text-sm whitespace-pre-wrap dark:text-red-200 bg-red-50 dark:bg-red-950/40 rounded-md">{{ delivery()?.error }}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="sm:col-span-2">
|
|
53
|
+
<dt class="font-medium text-gray-500 text-xs uppercase dark:text-neutral-400">Metadata</dt>
|
|
54
|
+
<dd class="overflow-x-auto mt-1 p-3 font-mono text-gray-700 text-xs whitespace-pre dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900 rounded-md">{{ metadataText() }}</dd>
|
|
55
|
+
</div>
|
|
56
|
+
</dl>
|
|
57
|
+
<template #footer>
|
|
58
|
+
<div class="flex justify-end w-full">
|
|
59
|
+
<Button variant="secondary" @click="emit('close')">
|
|
60
|
+
Close
|
|
61
|
+
</Button>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
</Modal>
|
|
@@ -15,6 +15,7 @@ interface ButtonProps {
|
|
|
15
15
|
rel?: string
|
|
16
16
|
ariaLabel?: string
|
|
17
17
|
type?: string
|
|
18
|
+
form?: string
|
|
18
19
|
class?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -83,6 +84,7 @@ const liveDownload = useReactiveProp('download', '')
|
|
|
83
84
|
const liveTarget = useReactiveProp('target', '')
|
|
84
85
|
const liveRel = useReactiveProp('rel', '')
|
|
85
86
|
const liveType = useReactiveProp('type', 'button')
|
|
87
|
+
const liveForm = useReactiveProp('form', '')
|
|
86
88
|
const liveAriaLabel = useReactiveProp('ariaLabel', '')
|
|
87
89
|
</script>
|
|
88
90
|
@if(tag === 'a' || (href && href.length > 0))
|
|
@@ -106,6 +108,7 @@ const liveAriaLabel = useReactiveProp('ariaLabel', '')
|
|
|
106
108
|
@if(interaction === 'toggle')
|
|
107
109
|
<button
|
|
108
110
|
:type="liveType()"
|
|
111
|
+
:form="liveForm() || undefined"
|
|
109
112
|
class="{{ buttonClass }}"
|
|
110
113
|
:aria-label="liveAriaLabel() || undefined"
|
|
111
114
|
:aria-busy="liveLoading() ? 'true' : 'false'"
|
|
@@ -120,6 +123,7 @@ const liveAriaLabel = useReactiveProp('ariaLabel', '')
|
|
|
120
123
|
@else
|
|
121
124
|
<button
|
|
122
125
|
:type="liveType()"
|
|
126
|
+
:form="liveForm() || undefined"
|
|
123
127
|
class="{{ buttonClass }}"
|
|
124
128
|
:aria-label="liveAriaLabel() || undefined"
|
|
125
129
|
:aria-busy="liveLoading() ? 'true' : 'false'"
|
|
@@ -9,6 +9,7 @@ const cancelLabel = useReactiveProp('cancelLabel', 'Cancel')
|
|
|
9
9
|
const variant = useReactiveProp('variant', 'danger' as ConfirmVariant)
|
|
10
10
|
const busy = useReactiveProp('busy', false)
|
|
11
11
|
const disabled = useReactiveProp('disabled', false)
|
|
12
|
+
const warning = useReactiveProp('warning', '')
|
|
12
13
|
const error = useReactiveProp('error', '')
|
|
13
14
|
const emit = defineEmits()
|
|
14
15
|
|
|
@@ -51,6 +52,9 @@ function iconName(): string {
|
|
|
51
52
|
|
|
52
53
|
<template #footer>
|
|
53
54
|
<div class="w-full">
|
|
55
|
+
<div :if="warning()" role="note" class="mb-4 p-3 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-lg dark:border-amber-900">
|
|
56
|
+
{{ warning() }}
|
|
57
|
+
</div>
|
|
54
58
|
<div :if="error()" role="alert" class="mb-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">
|
|
55
59
|
{{ error() }}
|
|
56
60
|
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
interface DrawerProps {
|
|
3
|
+
isOpen?: boolean
|
|
4
|
+
title?: string
|
|
5
|
+
description?: string
|
|
6
|
+
size?: 'sm' | 'md' | 'lg'
|
|
7
|
+
closable?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { isOpen = false, title = '', description = '', size = 'md', closable = true } = defineProps<DrawerProps>()
|
|
11
|
+
|
|
12
|
+
const sizeClasses = {
|
|
13
|
+
sm: 'max-w-sm',
|
|
14
|
+
md: 'max-w-lg',
|
|
15
|
+
lg: 'max-w-2xl',
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script client>
|
|
20
|
+
const open = useReactiveProp('isOpen', false)
|
|
21
|
+
const liveClosable = useReactiveProp('closable', true)
|
|
22
|
+
const liveTitle = useReactiveProp('title', '')
|
|
23
|
+
const liveDescription = useReactiveProp('description', '')
|
|
24
|
+
const dialog = useRef<HTMLDialogElement>('dashboardDrawerDialog')
|
|
25
|
+
const activeElement = useActiveElement()
|
|
26
|
+
const scrollLocked = useScrollLock()
|
|
27
|
+
const emit = defineEmits()
|
|
28
|
+
let restoreFocus: HTMLElement | null = null
|
|
29
|
+
|
|
30
|
+
function close(): void {
|
|
31
|
+
if (liveClosable())
|
|
32
|
+
emit('close')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function handleCancel(event: Event): void {
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
close()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleBackdropClick(event: MouseEvent): void {
|
|
41
|
+
if (event.target === event.currentTarget)
|
|
42
|
+
close()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
effect(() => {
|
|
46
|
+
const isOpen = open()
|
|
47
|
+
scrollLocked.value = isOpen
|
|
48
|
+
|
|
49
|
+
if (isOpen) {
|
|
50
|
+
const focused = peek(() => activeElement.value)
|
|
51
|
+
if (focused instanceof HTMLElement)
|
|
52
|
+
restoreFocus = focused
|
|
53
|
+
void nextTick(() => {
|
|
54
|
+
const element = dialog.current
|
|
55
|
+
if (!element)
|
|
56
|
+
return
|
|
57
|
+
if (!element.open)
|
|
58
|
+
element.showModal()
|
|
59
|
+
element.focus()
|
|
60
|
+
})
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (dialog.current?.open)
|
|
65
|
+
dialog.current.close()
|
|
66
|
+
const target = restoreFocus
|
|
67
|
+
restoreFocus = null
|
|
68
|
+
if (target)
|
|
69
|
+
void nextTick(() => target.focus())
|
|
70
|
+
})
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<dialog
|
|
74
|
+
:if="open()"
|
|
75
|
+
ref="dashboardDrawerDialog"
|
|
76
|
+
role="dialog"
|
|
77
|
+
aria-modal="true"
|
|
78
|
+
:aria-label="liveTitle() || 'Drawer'"
|
|
79
|
+
class="fixed inset-0 z-[55] m-0 p-0 h-dvh max-h-none max-w-none w-screen bg-transparent dashboard-drawer-dialog dashboard-modal-layer"
|
|
80
|
+
@cancel="handleCancel"
|
|
81
|
+
>
|
|
82
|
+
<div class="flex justify-end min-h-full" @click="handleBackdropClick">
|
|
83
|
+
<section class="flex relative flex-col h-dvh w-full bg-white dark:bg-neutral-950 border-neutral-200 border-l dark:border-neutral-800 shadow-2xl {{ sizeClasses[size] }}" @click.stop>
|
|
84
|
+
<header class="flex gap-4 items-start px-5 py-4 border-b border-neutral-200 dark:border-neutral-800">
|
|
85
|
+
<slot name="header-leading" />
|
|
86
|
+
<div class="flex-1 min-w-0">
|
|
87
|
+
<h2 :if="liveTitle()" class="font-semibold text-lg text-neutral-900 truncate dark:text-white">{{ liveTitle() }}</h2>
|
|
88
|
+
<p :if="liveDescription()" class="mt-1 text-neutral-500 text-sm dark:text-neutral-400">{{ liveDescription() }}</p>
|
|
89
|
+
</div>
|
|
90
|
+
<slot name="header-trailing" />
|
|
91
|
+
<Button :if="liveClosable()" variant="ghost" size="sm" iconOnly ariaLabel="Close drawer" @click="close">
|
|
92
|
+
<span aria-hidden="true" class="h-5 w-5 i-hugeicons-cancel-01"></span>
|
|
93
|
+
</Button>
|
|
94
|
+
</header>
|
|
95
|
+
|
|
96
|
+
<div class="overflow-y-auto flex-1 min-h-0">
|
|
97
|
+
<slot />
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
@if($slots.footer)
|
|
101
|
+
<footer class="px-5 py-4 border-neutral-200 border-t dark:border-neutral-800">
|
|
102
|
+
<slot name="footer" />
|
|
103
|
+
</footer>
|
|
104
|
+
@endif
|
|
105
|
+
</section>
|
|
106
|
+
</div>
|
|
107
|
+
</dialog>
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
.dashboard-drawer-dialog::backdrop {
|
|
111
|
+
background: rgb(0 0 0 / 45%);
|
|
112
|
+
backdrop-filter: blur(2px);
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -58,7 +58,11 @@ effect(() => {
|
|
|
58
58
|
if (!element) return
|
|
59
59
|
if (!element.open)
|
|
60
60
|
element.showModal()
|
|
61
|
-
element.
|
|
61
|
+
const autofocusTarget = element.querySelector<HTMLElement>('[autofocus]:not([disabled])')
|
|
62
|
+
if (autofocusTarget)
|
|
63
|
+
autofocusTarget.focus()
|
|
64
|
+
else
|
|
65
|
+
element.focus()
|
|
62
66
|
})
|
|
63
67
|
return
|
|
64
68
|
}
|
|
@@ -77,7 +81,7 @@ effect(() => {
|
|
|
77
81
|
:role="liveRole()"
|
|
78
82
|
aria-modal="true"
|
|
79
83
|
:aria-label="liveTitle() || 'Dialog'"
|
|
80
|
-
class="fixed inset-0 m-0 p-0 h-dvh max-h-none
|
|
84
|
+
class="fixed inset-0 overflow-y-auto z-[55] m-0 p-0 h-dvh max-h-none max-w-none w-screen bg-transparent dashboard-modal-dialog dashboard-modal-layer"
|
|
81
85
|
@cancel="handleCancel"
|
|
82
86
|
>
|
|
83
87
|
<!-- Modal container -->
|
|
@@ -99,16 +103,19 @@ effect(() => {
|
|
|
99
103
|
</div>
|
|
100
104
|
</div>
|
|
101
105
|
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
<div class="flex flex-shrink-0 gap-2 items-center">
|
|
107
|
+
<slot name="header-trailing" />
|
|
108
|
+
<Button
|
|
109
|
+
:if="liveClosable()"
|
|
110
|
+
variant="ghost"
|
|
111
|
+
size="sm"
|
|
112
|
+
iconOnly
|
|
113
|
+
ariaLabel="Close modal"
|
|
114
|
+
@click="closeModal"
|
|
115
|
+
>
|
|
116
|
+
<div class="h-5 w-5 i-hugeicons-cancel-01"></div>
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
112
119
|
</div>
|
|
113
120
|
|
|
114
121
|
<!-- Body -->
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
interface ComboboxDemoProps {
|
|
3
|
-
listItems?: string
|
|
3
|
+
listItems?: Array<{ id: number, name: string }>
|
|
4
4
|
selected?: any
|
|
5
5
|
query?: string
|
|
6
6
|
isOpen?: boolean
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
const { listItems = [
|
|
10
|
-
|
|
11
|
-
{ id: 1, name: 'Chris Breuer' },
|
|
9
|
+
const { listItems = [
|
|
10
|
+
{ id: 1, name: 'Chris Breuer' },
|
|
12
11
|
{ id: 2, name: 'Avery Hill' },
|
|
13
12
|
{ id: 3, name: 'Glenn Michael' },
|
|
14
13
|
{ id: 4, name: 'Michael Vincent' },
|
|
15
14
|
{ id: 5, name: 'Blake Ayer' },
|
|
16
|
-
]
|
|
15
|
+
], selected = null, query = '', isOpen = false } = defineProps<ComboboxDemoProps>()
|
|
17
16
|
function filterItems(items, query) {
|
|
18
17
|
if (query === '') return items
|
|
19
18
|
return items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
|