@stacksjs/defaults 0.75.243 → 0.75.245
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 +9 -9
- package/ai/skills/stacks-path/SKILL.md +18 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +7 -1
- package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +2 -1
- package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +2 -1
- package/app/Actions/Dashboard/Source/source-inventory.ts +2 -1
- package/app/Models/QueryLog.ts +10 -0
- package/functions/dashboard.ts +10 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/project/storage/framework/tsconfig.app.json +8 -0
- package/resources/components/Dashboard/Analytics/WebAnalyticsDashboard.stx +5 -5
- package/resources/components/Dashboard/Ci/CiDashboard.stx +2 -2
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +1 -1
- package/resources/components/Dashboard/Content/AuthorsDashboard.stx +59 -20
- package/resources/components/Dashboard/Content/BlogDashboard.stx +2 -2
- package/resources/components/Dashboard/Content/ContentDashboard.stx +2 -2
- package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +50 -18
- package/resources/components/Dashboard/Content/PagesDashboard.stx +54 -10
- package/resources/components/Dashboard/Content/PostsDashboard.stx +91 -20
- package/resources/components/Dashboard/Content/SeoDashboard.stx +61 -59
- package/resources/components/Dashboard/Email/InboxSettingsDashboard.stx +3 -3
- package/resources/components/Dashboard/Environment/EnvironmentDashboard.stx +1 -1
- package/resources/components/Dashboard/Home/DashboardHome.stx +20 -2
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +3 -3
- package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +2 -2
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +1 -1
- package/resources/components/Dashboard/Library/ComponentsDashboard.stx +47 -81
- package/resources/components/Dashboard/Library/FunctionsDashboard.stx +24 -15
- package/resources/components/Dashboard/Library/PackagesDashboard.stx +68 -13
- package/resources/components/Dashboard/Management/PermissionsDashboard.stx +12 -15
- package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +3 -3
- package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +3 -3
- package/resources/components/Dashboard/Settings/SettingsDashboard.stx +1 -1
- package/resources/components/Dashboard/Teams/TeamInvitationDashboard.stx +2 -2
- package/resources/components/Dashboard/UI/Button.stx +29 -3
- package/resources/components/Dashboard/UI/SearchFilter.stx +46 -0
- package/resources/components/Dashboard/UI/Table.stx +43 -20
- package/resources/functions/dashboard/data.ts +27 -4
- package/resources/functions/dashboard/sidebar.ts +3 -2
- package/resources/functions/dashboard/toggles.ts +47 -11
- package/resources/plugins/preloader.ts +9 -1
- package/views/dashboard/layouts/default.stx +17 -13
- package/views/system-tray/index.stx +1 -1
- package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +0 -24
- package/resources/components/Dashboard/UI/FilterBar.stx +0 -62
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@import('../UI/Button')
|
|
2
|
+
@import('../UI/Table')
|
|
2
3
|
|
|
3
4
|
<script client>
|
|
4
5
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
@@ -25,18 +26,28 @@ const totalBytes = state(0)
|
|
|
25
26
|
const sourceRoot = state('resources/components')
|
|
26
27
|
const loading = state(true)
|
|
27
28
|
const loadError = state('')
|
|
28
|
-
const search = state('')
|
|
29
29
|
const selectedCategory = state('All')
|
|
30
30
|
const dialogOpen = state(false)
|
|
31
31
|
const saving = state(false)
|
|
32
32
|
const saveError = state('')
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const COLUMNS = [
|
|
35
|
+
{ key: 'name', label: 'Component', sortable: true, class: 'font-medium text-gray-900 dark:text-white' },
|
|
36
|
+
// A neutral pill with no per-value styling, which is what a badge column
|
|
37
|
+
// renders when it is given no `badgeClasses`.
|
|
38
|
+
{ key: 'category', label: 'Category', type: 'badge' as const, sortable: true },
|
|
39
|
+
{ key: 'path', label: 'Path', sortable: true, class: 'font-mono text-xs text-gray-500 dark:text-neutral-400' },
|
|
40
|
+
// `sizeLabel` is what the cell shows; `bytes` stays on the row so a future
|
|
41
|
+
// numeric sort has something to read.
|
|
42
|
+
{ key: 'sizeLabel', label: 'Size', sortable: true, class: 'text-gray-500 dark:text-neutral-400' },
|
|
43
|
+
{ key: 'updatedAt', label: 'Updated', type: 'datetime' as const, sortable: true, class: 'text-gray-500 dark:text-neutral-400' },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/** `UI/Table` owns the search, so this applies only the category filter. */
|
|
47
|
+
function visibleComponents(): Array<ComponentRow & { sizeLabel: string }> {
|
|
48
|
+
return components()
|
|
49
|
+
.filter(component => selectedCategory() === 'All' || component.category === selectedCategory())
|
|
50
|
+
.map(component => ({ ...component, sizeLabel: formatBytes(component.bytes) }))
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
function formatBytes(bytes: number): string {
|
|
@@ -45,13 +56,6 @@ function formatBytes(bytes: number): string {
|
|
|
45
56
|
return `${(bytes / 1024).toFixed(1)} kB`
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
function formatDate(value: string): string {
|
|
49
|
-
const date = new Date(value)
|
|
50
|
-
return Number.isNaN(date.getTime())
|
|
51
|
-
? value
|
|
52
|
-
: new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
59
|
async function loadComponents(): Promise<void> {
|
|
56
60
|
loading.set(true)
|
|
57
61
|
loadError.set('')
|
|
@@ -167,73 +171,35 @@ onMount(() => {
|
|
|
167
171
|
</section>
|
|
168
172
|
|
|
169
173
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Size</th>
|
|
200
|
-
<th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Updated</th>
|
|
201
|
-
</tr>
|
|
202
|
-
</thead>
|
|
203
|
-
<tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
204
|
-
<template :if="loading()">
|
|
205
|
-
<tr>
|
|
206
|
-
<td colspan="5" class="px-4 py-12 text-center text-gray-500 text-sm dark:text-neutral-400">
|
|
207
|
-
<span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
|
|
208
|
-
Scanning component sources
|
|
209
|
-
</td>
|
|
210
|
-
</tr>
|
|
211
|
-
</template>
|
|
212
|
-
<template :else-if="visibleComponents().length === 0">
|
|
213
|
-
<tr>
|
|
214
|
-
<td colspan="5" class="px-4 py-14 text-center">
|
|
215
|
-
<span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-layout-table-01"></span>
|
|
216
|
-
<p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No components found</p>
|
|
217
|
-
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Create a component or adjust the filters.</p>
|
|
218
|
-
</td>
|
|
219
|
-
</tr>
|
|
220
|
-
</template>
|
|
221
|
-
<template :else>
|
|
222
|
-
<template :for="component in visibleComponents()">
|
|
223
|
-
<tr>
|
|
224
|
-
<td class="px-4 py-3 font-medium text-gray-900 text-sm dark:text-white">{{ component.name }}</td>
|
|
225
|
-
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">
|
|
226
|
-
<span class="px-2 py-1 text-xs bg-gray-100 dark:bg-neutral-700 rounded-full" :text="component.category"></span>
|
|
227
|
-
</td>
|
|
228
|
-
<td class="px-4 py-3 font-mono text-gray-500 text-xs dark:text-neutral-400">{{ component.path }}</td>
|
|
229
|
-
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatBytes(component.bytes) }}</td>
|
|
230
|
-
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatDate(component.updatedAt) }}</td>
|
|
231
|
-
</tr>
|
|
232
|
-
</template>
|
|
233
|
-
</template>
|
|
234
|
-
</tbody>
|
|
235
|
-
</table>
|
|
236
|
-
</div>
|
|
174
|
+
<Table
|
|
175
|
+
:columns="COLUMNS"
|
|
176
|
+
:data="visibleComponents()"
|
|
177
|
+
:loading="loading()"
|
|
178
|
+
row-key="path"
|
|
179
|
+
searchable
|
|
180
|
+
search-placeholder="Search name, category, or path"
|
|
181
|
+
paginated
|
|
182
|
+
:page-size="25"
|
|
183
|
+
empty-title="No components found"
|
|
184
|
+
empty-description="Adjust the search or category filter."
|
|
185
|
+
>
|
|
186
|
+
<template #toolbar>
|
|
187
|
+
<label>
|
|
188
|
+
<span class="sr-only">Filter by category</span>
|
|
189
|
+
{{-- `x-text` rather than `{{ category }}`: an interpolation inside
|
|
190
|
+
slot content is preserved but never evaluated on the installed
|
|
191
|
+
stx, while the attribute forms read the loop variable fine.
|
|
192
|
+
See stacksjs/stx#2095. --}}
|
|
193
|
+
<select
|
|
194
|
+
x-model="selectedCategory"
|
|
195
|
+
aria-label="Filter by category"
|
|
196
|
+
class="px-2.5 py-1.5 text-sm bg-white dark:bg-neutral-800 border border-stone-300 rounded-lg dark:border-neutral-600"
|
|
197
|
+
>
|
|
198
|
+
<option :for="category in categories()" :value="category" x-text="category"></option>
|
|
199
|
+
</select>
|
|
200
|
+
</label>
|
|
201
|
+
</template>
|
|
202
|
+
</Table>
|
|
237
203
|
</section>
|
|
238
204
|
</div>
|
|
239
205
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@import('../UI/Button')
|
|
2
|
+
@import('../UI/Table')
|
|
2
3
|
|
|
3
4
|
<script client>
|
|
4
5
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
@@ -18,14 +19,21 @@ const totalBytes = state(0)
|
|
|
18
19
|
const sourceRoot = state('resources/functions')
|
|
19
20
|
const loading = state(true)
|
|
20
21
|
const loadError = state('')
|
|
21
|
-
const search = state('')
|
|
22
22
|
const dialogOpen = state(false)
|
|
23
23
|
const saving = state(false)
|
|
24
24
|
const saveError = state('')
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const COLUMNS = [
|
|
27
|
+
{ key: 'name', label: 'Function', sortable: true, class: 'font-medium text-gray-900 dark:text-white' },
|
|
28
|
+
{ key: 'path', label: 'Path', sortable: true, class: 'font-mono text-xs text-gray-500 dark:text-neutral-400' },
|
|
29
|
+
// `bytes` so it sorts numerically; `sizeLabel` is what the cell shows.
|
|
30
|
+
{ key: 'sizeLabel', label: 'Size', sortable: true, class: 'text-gray-500 dark:text-neutral-400' },
|
|
31
|
+
{ key: 'updatedAt', label: 'Updated', type: 'datetime' as const, sortable: true, class: 'text-gray-500 dark:text-neutral-400' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
/** `UI/Table` owns the search, so this only shapes the rows it reads. */
|
|
35
|
+
function visibleFunctions(): Array<FunctionRow & { sizeLabel: string }> {
|
|
36
|
+
return functions().map(fn => ({ ...fn, sizeLabel: formatBytes(fn.bytes) }))
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
function formatBytes(bytes: number): string {
|
|
@@ -34,11 +42,6 @@ function formatBytes(bytes: number): string {
|
|
|
34
42
|
return `${(bytes / 1024).toFixed(1)} kB`
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
function formatDate(value: string): string {
|
|
38
|
-
const date = new Date(value)
|
|
39
|
-
return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
45
|
async function loadFunctions(): Promise<void> {
|
|
43
46
|
loading.set(true)
|
|
44
47
|
loadError.set('')
|
|
@@ -107,12 +110,18 @@ onMount(() => { void loadFunctions() })
|
|
|
107
110
|
</section>
|
|
108
111
|
|
|
109
112
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
<Table
|
|
114
|
+
:columns="COLUMNS"
|
|
115
|
+
:data="visibleFunctions()"
|
|
116
|
+
:loading="loading()"
|
|
117
|
+
row-key="path"
|
|
118
|
+
searchable
|
|
119
|
+
search-placeholder="Search name or path"
|
|
120
|
+
paginated
|
|
121
|
+
:page-size="25"
|
|
122
|
+
empty-title="No functions found"
|
|
123
|
+
empty-description="Create a function or adjust the search."
|
|
124
|
+
/>
|
|
116
125
|
</section>
|
|
117
126
|
</div>
|
|
118
127
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@import('../UI/Button')
|
|
2
|
+
@import('../UI/Table')
|
|
2
3
|
|
|
3
4
|
<script client>
|
|
4
5
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
@@ -21,15 +22,42 @@ const privateCount = state(0)
|
|
|
21
22
|
const dependencyCount = state(0)
|
|
22
23
|
const loading = state(true)
|
|
23
24
|
const loadError = state('')
|
|
24
|
-
const search = state('')
|
|
25
25
|
const visibility = state('all')
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const COLUMNS = [
|
|
28
|
+
// Slotted for the conditional link: a package with a `url` is a link to it,
|
|
29
|
+
// one without is plain text, and the description sits under either.
|
|
30
|
+
{ key: 'name', label: 'Package', sortable: true, slotted: true, class: 'max-w-md' },
|
|
31
|
+
{ key: 'version', label: 'Version', sortable: true, emptyText: 'Unversioned', class: 'font-mono text-gray-500 dark:text-neutral-400' },
|
|
32
|
+
{
|
|
33
|
+
key: 'visibility',
|
|
34
|
+
label: 'Visibility',
|
|
35
|
+
type: 'badge' as const,
|
|
36
|
+
sortable: true,
|
|
37
|
+
searchable: false,
|
|
38
|
+
badgeClasses: {
|
|
39
|
+
public: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
|
|
40
|
+
private: 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{ key: 'dependencyCount', label: 'Dependencies', type: 'number' as const, sortable: true },
|
|
44
|
+
{ key: 'path', label: 'Path', sortable: true, class: 'font-mono text-xs text-gray-500 dark:text-neutral-400' },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `UI/Table` owns the search, so this only applies the visibility filter.
|
|
49
|
+
* `visibility` and `descriptionLabel` are computed per row because a column
|
|
50
|
+
* reads a field: a badge column maps a row value, and the description needs
|
|
51
|
+
* its own fallback wording.
|
|
52
|
+
*/
|
|
53
|
+
function visiblePackages(): Array<PackageRow & { visibility: string, descriptionLabel: string }> {
|
|
54
|
+
return packages()
|
|
55
|
+
.filter(pkg => visibility() === 'all' || (visibility() === 'public' ? !pkg.private : pkg.private))
|
|
56
|
+
.map(pkg => ({
|
|
57
|
+
...pkg,
|
|
58
|
+
visibility: pkg.private ? 'private' : 'public',
|
|
59
|
+
descriptionLabel: pkg.description || 'No package description',
|
|
60
|
+
}))
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
async function loadPackages(): Promise<void> {
|
|
@@ -72,11 +100,38 @@ onMount(() => { void loadPackages() })
|
|
|
72
100
|
</section>
|
|
73
101
|
|
|
74
102
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
{{-- Slot bodies use `x-text` rather than `{{ }}`: a scoped slot binds
|
|
104
|
+
`row` at runtime, and the attribute forms read it, while an
|
|
105
|
+
interpolation in slot content is preserved but never evaluated.
|
|
106
|
+
See stacksjs/stx#2095. --}}
|
|
107
|
+
<Table
|
|
108
|
+
:columns="COLUMNS"
|
|
109
|
+
:data="visiblePackages()"
|
|
110
|
+
:loading="loading()"
|
|
111
|
+
row-key="path"
|
|
112
|
+
searchable
|
|
113
|
+
search-placeholder="Search name, description, or path"
|
|
114
|
+
paginated
|
|
115
|
+
:page-size="25"
|
|
116
|
+
empty-title="No packages found"
|
|
117
|
+
empty-description="Adjust the search or visibility filter."
|
|
118
|
+
>
|
|
119
|
+
<template #toolbar>
|
|
120
|
+
<label>
|
|
121
|
+
<span class="sr-only">Filter package visibility</span>
|
|
122
|
+
<select x-model="visibility" class="px-2.5 py-1.5 text-sm bg-white dark:bg-neutral-800 border border-stone-300 rounded-lg dark:border-neutral-600">
|
|
123
|
+
<option value="all">All workspaces</option>
|
|
124
|
+
<option value="public">Public</option>
|
|
125
|
+
<option value="private">Private</option>
|
|
126
|
+
</select>
|
|
127
|
+
</label>
|
|
128
|
+
</template>
|
|
129
|
+
|
|
130
|
+
<template #cell="{ row }">
|
|
131
|
+
<a :if="row.url" :href="row.url" target="_blank" rel="noreferrer" class="font-medium text-blue-700 text-sm dark:text-blue-300 hover:underline" x-text="row.name"></a>
|
|
132
|
+
<span :else class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.name"></span>
|
|
133
|
+
<p class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400" x-text="row.descriptionLabel"></p>
|
|
134
|
+
</template>
|
|
135
|
+
</Table>
|
|
81
136
|
</section>
|
|
82
137
|
</div>
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
// - All state in the rbacStore; the page is pure presentation.
|
|
14
14
|
// - Mutations are optimistic with rollback on failure (store handles
|
|
15
15
|
// the snapshot/rollback dance).
|
|
16
|
-
import { useRole } from '
|
|
17
|
-
import { rbacStore } from '
|
|
16
|
+
import { useRole } from '@stacksjs/defaults/views/dashboard/composables/useRole.ts'
|
|
17
|
+
import { rbacStore } from '@stacksjs/defaults/views/dashboard/stores/rbac.ts'
|
|
18
18
|
|
|
19
19
|
const rbac = rbacStore
|
|
20
20
|
// Admin-only surface. Per `useRole.ts`, `isAdmin()` returns true for
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
|
|
64
64
|
const activeTab = state('roles')
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
const PERMISSION_TABS = [
|
|
67
|
+
{ id: 'roles', label: 'Roles' },
|
|
68
|
+
{ id: 'permissions', label: 'Permissions' },
|
|
69
|
+
{ id: 'users', label: 'Users' },
|
|
70
|
+
{ id: 'matrix', label: 'Role × Permission' },
|
|
71
|
+
]
|
|
72
72
|
|
|
73
73
|
function selectTab(id: string) {
|
|
74
74
|
activeTab.set(id)
|
|
@@ -356,13 +356,10 @@
|
|
|
356
356
|
</div>
|
|
357
357
|
|
|
358
358
|
<div :if="!loading">
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<button type="button" @click="selectTab('users')" :class="tabClass('users')">Users</button>
|
|
364
|
-
<button type="button" @click="selectTab('matrix')" :class="tabClass('matrix')">Role × Permission</button>
|
|
365
|
-
</div>
|
|
359
|
+
{{-- `UI/Tabs` rather than four buttons: it carries the `role="tab"` /
|
|
360
|
+
`aria-selected` semantics this strip had none of, so the tab row is
|
|
361
|
+
navigable by assistive tech. See stacksjs/stacks#2924. --}}
|
|
362
|
+
<Tabs class="mb-6" :tabs="PERMISSION_TABS" :active-tab="activeTab()" @update:activeTab="selectTab($event)" />
|
|
366
363
|
|
|
367
364
|
<!-- ─── Roles tab ─────────────────────────────────────── -->
|
|
368
365
|
<div :if="activeTab() === 'roles'" class="space-y-6">
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// All querying is server-side (`GET /api/dashboard/models/{slug}`), so this
|
|
7
7
|
// works the same on a table with 10 rows and one with a million. Rows and
|
|
8
8
|
// writes go through the model, so scopes, casts and observers apply.
|
|
9
|
-
import { useNavigation } from '
|
|
10
|
-
import { DashboardApiError, dashboardApi } from '
|
|
11
|
-
import { pushToast } from '
|
|
9
|
+
import { useNavigation } from '@stacksjs/defaults/views/dashboard/composables/useNavigation.ts'
|
|
10
|
+
import { DashboardApiError, dashboardApi } from '@stacksjs/defaults/functions/dashboard-api.ts'
|
|
11
|
+
import { pushToast } from '@stacksjs/defaults/functions/toasts.ts'
|
|
12
12
|
|
|
13
13
|
interface ColumnMeta {
|
|
14
14
|
name: string
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// layout uses, so the list can never drift from what the sidebar renders -
|
|
4
4
|
// including sections a project has switched off in config/dashboard.ts, which
|
|
5
5
|
// simply are not here to toggle.
|
|
6
|
-
import { buildWebSidebarSections } from '
|
|
6
|
+
import { buildWebSidebarSections } from '@stacksjs/defaults/resources/functions/dashboard/sidebar.ts'
|
|
7
7
|
|
|
8
8
|
export const navSections = buildWebSidebarSections()
|
|
9
9
|
.filter(section => section.label && section.items.length > 0)
|
|
@@ -19,12 +19,12 @@ defineClientPayload({ navSections })
|
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
21
|
<script client>
|
|
22
|
-
import type { ColorMode, SidebarStyle } from '
|
|
22
|
+
import type { ColorMode, SidebarStyle } from '@stacksjs/defaults/views/dashboard/composables/useAppearance.ts'
|
|
23
23
|
import {
|
|
24
24
|
COLOR_MODE_OPTIONS,
|
|
25
25
|
SIDEBAR_STYLE_OPTIONS,
|
|
26
26
|
useAppearance,
|
|
27
|
-
} from '
|
|
27
|
+
} from '@stacksjs/defaults/views/dashboard/composables/useAppearance.ts'
|
|
28
28
|
|
|
29
29
|
const appearance = useAppearance()
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
|
-
import { dashboardApi } from '
|
|
3
|
-
import { useAuth } from '
|
|
2
|
+
import { dashboardApi } from '@stacksjs/defaults/functions/dashboard-api.ts'
|
|
3
|
+
import { useAuth } from '@stacksjs/defaults/functions/auth.ts'
|
|
4
4
|
|
|
5
5
|
interface Invitation {
|
|
6
6
|
teamId: number
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script server>
|
|
2
2
|
interface ButtonProps {
|
|
3
|
-
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning'
|
|
3
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'warning' | 'bare'
|
|
4
4
|
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
5
5
|
disabled?: boolean
|
|
6
6
|
loading?: boolean
|
|
@@ -40,7 +40,19 @@ const variantClasses = {
|
|
|
40
40
|
ghost: 'bg-transparent text-neutral-600 dark:text-neutral-300 hover:bg-black/5 dark:hover:bg-white/8 active:bg-black/10 dark:active:bg-white/12',
|
|
41
41
|
danger: 'bg-gradient-to-b from-red-500 to-red-600 text-white shadow-sm shadow-red-600/25 hover:from-red-400 hover:to-red-500 active:from-red-600 active:to-red-700',
|
|
42
42
|
success: 'bg-gradient-to-b from-green-500 to-green-600 text-white shadow-sm shadow-green-600/25 hover:from-green-400 hover:to-green-500 active:from-green-600 active:to-green-700',
|
|
43
|
-
warning: 'bg-gradient-to-b from-orange-500 to-orange-600 text-white shadow-sm shadow-orange-600/25 hover:from-orange-400 hover:to-orange-500 active:from-orange-600 active:to-orange-700'
|
|
43
|
+
warning: 'bg-gradient-to-b from-orange-500 to-orange-600 text-white shadow-sm shadow-orange-600/25 hover:from-orange-400 hover:to-orange-500 active:from-orange-600 active:to-orange-700',
|
|
44
|
+
/**
|
|
45
|
+
* No chrome at all: the caller supplies every visual through `class`.
|
|
46
|
+
*
|
|
47
|
+
* For an affordance that has to sit inside someone else's layout and carry
|
|
48
|
+
* no box of its own - a sortable table header fills its `<th>`, and the
|
|
49
|
+
* padding and fixed height the sizes impose would change the header row.
|
|
50
|
+
* What it still gets, and the reason to come through this component rather
|
|
51
|
+
* than write a raw `<button>`, is the shared focus-visible ring, the
|
|
52
|
+
* disabled and aria-disabled handling, and the press feedback.
|
|
53
|
+
* See stacksjs/stacks#2926.
|
|
54
|
+
*/
|
|
55
|
+
bare: ''
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
// Size classes
|
|
@@ -71,7 +83,8 @@ const buttonClass = [
|
|
|
71
83
|
: '',
|
|
72
84
|
customClass,
|
|
73
85
|
fullWidth ? 'w-full' : '',
|
|
74
|
-
|
|
86
|
+
// `bare` takes neither: its whole point is to impose no box.
|
|
87
|
+
variant === 'bare' ? '' : (iconOnly ? iconOnlySizes[size] : sizeClasses[size]),
|
|
75
88
|
variantClasses[variant]
|
|
76
89
|
].filter(Boolean).join(' ')
|
|
77
90
|
</script>
|
|
@@ -86,6 +99,17 @@ const liveRel = useReactiveProp('rel', '')
|
|
|
86
99
|
const liveType = useReactiveProp('type', 'button')
|
|
87
100
|
const liveForm = useReactiveProp('form', '')
|
|
88
101
|
const liveAriaLabel = useReactiveProp('ariaLabel', '')
|
|
102
|
+
/**
|
|
103
|
+
* For a button that opens a menu or a disclosure.
|
|
104
|
+
*
|
|
105
|
+
* An arbitrary `aria-*` attribute written on `<Button>` is not forwarded, so a
|
|
106
|
+
* caller that needs these has to be given them. The column-visibility toggle
|
|
107
|
+
* in `UI/Table` lost both when it moved off a raw `<button>` - the menu still
|
|
108
|
+
* worked and a screen reader no longer knew it was a popup, or whether it was
|
|
109
|
+
* open. See stacksjs/stacks#2926.
|
|
110
|
+
*/
|
|
111
|
+
const liveAriaHasPopup = useReactiveProp('ariaHasPopup', '')
|
|
112
|
+
const liveAriaExpanded = useReactiveProp('ariaExpanded', '')
|
|
89
113
|
</script>
|
|
90
114
|
@if(tag === 'a' || (href && href.length > 0))
|
|
91
115
|
<a
|
|
@@ -126,6 +150,8 @@ const liveAriaLabel = useReactiveProp('ariaLabel', '')
|
|
|
126
150
|
:form="liveForm() || undefined"
|
|
127
151
|
class="{{ buttonClass }}"
|
|
128
152
|
:aria-label="liveAriaLabel() || undefined"
|
|
153
|
+
:aria-haspopup="liveAriaHasPopup() || undefined"
|
|
154
|
+
:aria-expanded="liveAriaExpanded() || undefined"
|
|
129
155
|
:aria-busy="liveLoading() ? 'true' : 'false'"
|
|
130
156
|
:disabled="liveDisabled() || liveLoading()"
|
|
131
157
|
>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
/**
|
|
3
|
+
* The dashboard's search input.
|
|
4
|
+
*
|
|
5
|
+
* Promoted here from `Commerce/Delivery/` because it is the shape that won.
|
|
6
|
+
* `UI/FilterBar` offered the same input plus a slot for sibling filters and an
|
|
7
|
+
* items-per-page select, and had no callers at all after all this time, while
|
|
8
|
+
* this had seven - callers lay out their own filter grid and want one input to
|
|
9
|
+
* drop into it, not a bar that owns the row. FilterBar is deleted rather than
|
|
10
|
+
* kept unused. See stacksjs/stacks#2924.
|
|
11
|
+
*
|
|
12
|
+
* `searchValue` is the one thing FilterBar had right and this did not: the
|
|
13
|
+
* parent can seed the box, so a filter restored from the URL or from storage
|
|
14
|
+
* shows up in it instead of being invisible.
|
|
15
|
+
*/
|
|
16
|
+
const placeholder = useReactiveProp('placeholder', 'Search...')
|
|
17
|
+
const requested = useReactiveProp('searchValue', '')
|
|
18
|
+
const query = state(requested())
|
|
19
|
+
const emit = defineEmits()
|
|
20
|
+
|
|
21
|
+
effect(() => {
|
|
22
|
+
query.set(requested())
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
effect(() => {
|
|
26
|
+
emit('update:searchValue', query())
|
|
27
|
+
emit('search', query())
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<div class="relative rounded-md shadow-sm">
|
|
33
|
+
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
34
|
+
<span class="h-5 w-5 text-gray-400 i-hugeicons-search-01"></span>
|
|
35
|
+
</div>
|
|
36
|
+
<input
|
|
37
|
+
type="search"
|
|
38
|
+
name="search"
|
|
39
|
+
:placeholder="placeholder()"
|
|
40
|
+
:aria-label="placeholder()"
|
|
41
|
+
x-model="query"
|
|
42
|
+
autocomplete="off"
|
|
43
|
+
class="block pl-10 pr-3 py-2.5 w-full leading-6 text-gray-900 text-sm dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600 focus:ring-inset"
|
|
44
|
+
>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|