@stacksjs/defaults 0.75.244 → 0.75.246
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 +7 -7
- package/ai/skills/stacks-path/SKILL.md +19 -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/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/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/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'
|
|
@@ -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'
|
|
17
|
+
import { rbacStore } from '@stacksjs/defaults/views/dashboard/stores/rbac'
|
|
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'
|
|
10
|
+
import { DashboardApiError, dashboardApi } from '@stacksjs/defaults/functions/dashboard-api'
|
|
11
|
+
import { pushToast } from '@stacksjs/defaults/functions/toasts'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
3
|
+
import { useAuth } from '@stacksjs/defaults/functions/auth'
|
|
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>
|
|
@@ -77,6 +77,17 @@ interface DashboardTableColumn {
|
|
|
77
77
|
* "On delivery".
|
|
78
78
|
*/
|
|
79
79
|
badgeLabels?: Record<string, string>
|
|
80
|
+
/**
|
|
81
|
+
* Row field holding the pill's text, when it varies per row rather than per
|
|
82
|
+
* value.
|
|
83
|
+
*
|
|
84
|
+
* `badgeLabels` is a static map, which covers a status whose wording is
|
|
85
|
+
* fixed. It cannot cover a pill coloured by state but labelled with
|
|
86
|
+
* something row-specific - a publish pill that is green when published and
|
|
87
|
+
* grey when not, and reads as the date it happened. Wins over
|
|
88
|
+
* `badgeLabels` when both are given.
|
|
89
|
+
*/
|
|
90
|
+
badgeLabelKey?: string
|
|
80
91
|
/**
|
|
81
92
|
* Row field rendered as a second, quieter line under the value.
|
|
82
93
|
*
|
|
@@ -355,6 +366,12 @@ function badgeLabel(row: DashboardTableRow, column: DashboardTableColumn): strin
|
|
|
355
366
|
if (value === null || value === undefined || value === '')
|
|
356
367
|
return column.emptyText ?? '-'
|
|
357
368
|
|
|
369
|
+
if (column.badgeLabelKey) {
|
|
370
|
+
const label = rawValue(row, column.badgeLabelKey)
|
|
371
|
+
if (label !== null && label !== undefined && label !== '')
|
|
372
|
+
return String(label)
|
|
373
|
+
}
|
|
374
|
+
|
|
358
375
|
const key = String(value)
|
|
359
376
|
const mapped = column.badgeLabels?.[key]
|
|
360
377
|
if (mapped)
|
|
@@ -696,16 +713,16 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
696
713
|
|
|
697
714
|
<div class="flex gap-2 items-center">
|
|
698
715
|
<div :if="columnToggle()" class="relative">
|
|
699
|
-
<
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
716
|
+
<Button
|
|
717
|
+
variant="outline"
|
|
718
|
+
size="sm"
|
|
719
|
+
ariaHasPopup="true"
|
|
720
|
+
:ariaExpanded="columnMenuOpen() ? 'true' : 'false'"
|
|
703
721
|
@click="columnMenuOpen.set(!columnMenuOpen())"
|
|
704
|
-
class="inline-flex gap-1.5 items-center px-2.5 py-1.5 text-neutral-600 text-xs dark:text-neutral-300 hover:bg-stone-50 dark:hover:bg-neutral-800 border border-stone-300 rounded-lg dark:border-neutral-600"
|
|
705
722
|
>
|
|
706
723
|
<span aria-hidden="true" class="h-3.5 w-3.5 i-hugeicons-layout-table-01"></span>
|
|
707
724
|
Columns
|
|
708
|
-
</
|
|
725
|
+
</Button>
|
|
709
726
|
|
|
710
727
|
<div
|
|
711
728
|
:if="columnMenuOpen()"
|
|
@@ -774,10 +791,14 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
774
791
|
:style="column.width ? 'width: ' + column.width : undefined"
|
|
775
792
|
:aria-sort="ariaSort(column)"
|
|
776
793
|
>
|
|
777
|
-
|
|
794
|
+
{{-- `bare`: a header button fills its `<th>` and carries no box
|
|
795
|
+
of its own, so none of the sizes fit. It still comes through
|
|
796
|
+
`UI/Button` for the shared focus ring and disabled handling.
|
|
797
|
+
See stacksjs/stacks#2926. --}}
|
|
798
|
+
<Button
|
|
778
799
|
:if="column.sortable"
|
|
779
|
-
|
|
780
|
-
:
|
|
800
|
+
variant="bare"
|
|
801
|
+
:ariaLabel="sortLabel(column)"
|
|
781
802
|
class="inline-flex gap-1.5 items-center hover:text-stone-700 dark:hover:text-neutral-200"
|
|
782
803
|
@click="sort(column)"
|
|
783
804
|
>
|
|
@@ -786,7 +807,7 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
786
807
|
aria-hidden="true"
|
|
787
808
|
:class="'h-3.5 w-3.5 ' + (sortKey() === column.key ? (sortDirection() === 'asc' ? 'i-hugeicons-arrow-up-02' : 'i-hugeicons-arrow-down-02') : 'opacity-30 i-hugeicons-arrow-up-down')"
|
|
788
809
|
></span>
|
|
789
|
-
</
|
|
810
|
+
</Button>
|
|
790
811
|
<span :else>{{ column.label }}</span>
|
|
791
812
|
</th>
|
|
792
813
|
</template>
|
|
@@ -936,25 +957,27 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
936
957
|
</div>
|
|
937
958
|
|
|
938
959
|
<nav class="flex gap-1 items-center" aria-label="Pagination">
|
|
939
|
-
<
|
|
940
|
-
|
|
941
|
-
|
|
960
|
+
<Button
|
|
961
|
+
variant="outline"
|
|
962
|
+
size="sm"
|
|
963
|
+
iconOnly
|
|
964
|
+
ariaLabel="Previous page"
|
|
942
965
|
:disabled="page() <= 1"
|
|
943
966
|
@click="goToPage(page() - 1)"
|
|
944
|
-
class="p-1.5 text-neutral-600 dark:text-neutral-300 hover:bg-white dark:hover:bg-neutral-800 border border-stone-300 rounded dark:border-neutral-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
945
967
|
>
|
|
946
968
|
<span aria-hidden="true" class="h-4 w-4 i-hugeicons-arrow-left-01"></span>
|
|
947
|
-
</
|
|
969
|
+
</Button>
|
|
948
970
|
<span class="px-2 tabular-nums text-neutral-600 text-sm dark:text-neutral-300">{{ page() }} / {{ pageCount() }}</span>
|
|
949
|
-
<
|
|
950
|
-
|
|
951
|
-
|
|
971
|
+
<Button
|
|
972
|
+
variant="outline"
|
|
973
|
+
size="sm"
|
|
974
|
+
iconOnly
|
|
975
|
+
ariaLabel="Next page"
|
|
952
976
|
:disabled="page() >= pageCount()"
|
|
953
977
|
@click="goToPage(page() + 1)"
|
|
954
|
-
class="p-1.5 text-neutral-600 dark:text-neutral-300 hover:bg-white dark:hover:bg-neutral-800 border border-stone-300 rounded dark:border-neutral-600 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
955
978
|
>
|
|
956
979
|
<span aria-hidden="true" class="h-4 w-4 i-hugeicons-arrow-right-01"></span>
|
|
957
|
-
</
|
|
980
|
+
</Button>
|
|
958
981
|
</nav>
|
|
959
982
|
</div>
|
|
960
983
|
|
|
@@ -21,15 +21,36 @@ import { basename, dirname, resolve } from 'node:path'
|
|
|
21
21
|
|
|
22
22
|
let projectRootCache: string | null = null
|
|
23
23
|
|
|
24
|
+
const VENDORED_DEFAULTS_PREFIX = 'storage/framework/defaults/'
|
|
25
|
+
let defaultsRootCache: string | null = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The framework defaults tree: `storage/framework/defaults` in a project that
|
|
29
|
+
* vendors it, `node_modules/@stacksjs/defaults` in one that does not. Counted
|
|
30
|
+
* from this file first, which is inside that tree in both layouts; the project
|
|
31
|
+
* root is the fallback for a copy that was bundled somewhere else.
|
|
32
|
+
*/
|
|
33
|
+
function defaultsRoot(): string {
|
|
34
|
+
if (defaultsRootCache) return defaultsRootCache
|
|
35
|
+
const own = resolve(dirname(new URL(import.meta.url).pathname), '../../..')
|
|
36
|
+
const candidates = [
|
|
37
|
+
own,
|
|
38
|
+
resolve(projectRoot(), 'storage/framework/defaults'),
|
|
39
|
+
resolve(projectRoot(), 'node_modules/@stacksjs/defaults'),
|
|
40
|
+
]
|
|
41
|
+
defaultsRootCache = candidates.find(candidate => existsSync(resolve(candidate, 'app/Models'))) ?? candidates[1]!
|
|
42
|
+
return defaultsRootCache
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
function projectRoot(): string {
|
|
25
46
|
if (projectRootCache) return projectRootCache
|
|
26
47
|
// Walk up from this file location to find the project root (the dir
|
|
27
|
-
// that contains `storage/framework
|
|
48
|
+
// that contains `storage/framework`). This mirrors what
|
|
28
49
|
// `@stacksjs/path` does, but without pulling in the path package
|
|
29
50
|
// (which has its own evaluation graph and would slow page renders).
|
|
30
51
|
let dir = dirname(new URL(import.meta.url).pathname)
|
|
31
52
|
for (let i = 0; i < 12; i++) {
|
|
32
|
-
if (existsSync(resolve(dir, 'storage/framework
|
|
53
|
+
if (existsSync(resolve(dir, 'storage/framework'))) {
|
|
33
54
|
projectRootCache = dir
|
|
34
55
|
return dir
|
|
35
56
|
}
|
|
@@ -132,7 +153,7 @@ async function discoverModelPaths(): Promise<Map<string, string[]>> {
|
|
|
132
153
|
const paths = new Map<string, string[]>()
|
|
133
154
|
const modelRoots = [
|
|
134
155
|
resolve(root, 'app/Models'),
|
|
135
|
-
resolve(
|
|
156
|
+
resolve(defaultsRoot(), 'app/Models'),
|
|
136
157
|
]
|
|
137
158
|
|
|
138
159
|
for (const modelsRoot of modelRoots) {
|
|
@@ -159,7 +180,9 @@ async function discoverModelPaths(): Promise<Map<string, string[]>> {
|
|
|
159
180
|
async function modelCandidatePaths(name: string): Promise<string[]> {
|
|
160
181
|
const root = projectRoot()
|
|
161
182
|
const discovered = (await discoverModelPaths()).get(name) ?? []
|
|
162
|
-
const explicit = (MODEL_PATHS[name] ?? []).map(path =>
|
|
183
|
+
const explicit = (MODEL_PATHS[name] ?? []).map(path => path.startsWith(VENDORED_DEFAULTS_PREFIX)
|
|
184
|
+
? resolve(defaultsRoot(), path.slice(VENDORED_DEFAULTS_PREFIX.length))
|
|
185
|
+
: resolve(root, path))
|
|
163
186
|
return [...new Set([...discovered, ...explicit])]
|
|
164
187
|
}
|
|
165
188
|
|
|
@@ -111,12 +111,13 @@ interface NavItem {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* Pull the discovered-models manifest
|
|
114
|
+
* Pull the discovered-models manifest `buddy dev:dashboard` writes into the
|
|
115
|
+
* project's runtime state.
|
|
115
116
|
* Returns the default empty payload when the file is absent (fresh project
|
|
116
117
|
* before the first dashboard run regenerates it).
|
|
117
118
|
*/
|
|
118
119
|
export function loadDiscoveredManifest(
|
|
119
|
-
manifestPath = resolve(process.cwd(), 'storage/framework/
|
|
120
|
+
manifestPath = resolve(process.cwd(), 'storage/framework/runtime/dashboard/discovered-models.json'),
|
|
120
121
|
): DiscoveredManifest {
|
|
121
122
|
if (!existsSync(manifestPath))
|
|
122
123
|
return { models: [], sections: defaultToggles(), nav: [] }
|
|
@@ -166,13 +166,21 @@ if (!isRepl && !isPostinstall) {
|
|
|
166
166
|
// `upgrade` that would fix it. Falling through leaves stx and ts-cloud on
|
|
167
167
|
// their own defaults, which is degraded but recoverable.
|
|
168
168
|
const pathPkg = '@stacksjs/' + 'path'
|
|
169
|
-
const { applyRuntimeDirectoryEnv } = await import('../../../core/path/src/index.ts')
|
|
169
|
+
const { applyRuntimeDirectoryEnv, linkPackagedDefaults } = await import('../../../core/path/src/index.ts')
|
|
170
170
|
.catch(() => import(pathPkg))
|
|
171
171
|
|
|
172
172
|
if (typeof applyRuntimeDirectoryEnv === 'function')
|
|
173
173
|
applyRuntimeDirectoryEnv()
|
|
174
174
|
else
|
|
175
175
|
console.warn('[stacks] installed @stacksjs/path has no applyRuntimeDirectoryEnv; stx and ts-cloud will use their default state directories. Run `buddy upgrade` to refresh the framework packages.')
|
|
176
|
+
|
|
177
|
+
// An app that does not vendor the framework defaults gets a gitignored
|
|
178
|
+
// `storage/framework/defaults` link to the installed package. A fresh clone
|
|
179
|
+
// has no link until something makes one, so boot does: one lstat when it is
|
|
180
|
+
// already there, and a no-op for a vendored tree. Guarded for the same
|
|
181
|
+
// published-version reason as the helper above.
|
|
182
|
+
if (typeof linkPackagedDefaults === 'function')
|
|
183
|
+
linkPackagedDefaults()
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
// stx template engine plugin
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
// now publishes its .stx sources, so the real component resolves and the
|
|
10
10
|
// duplicate markup is gone.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
12
|
+
// A package specifier, not relative: the stx client bundler resolves a
|
|
13
|
+
// relative import against the PAGE that extends this layout, not against this
|
|
14
|
+
// file, so `../../..` pointed somewhere different for every page depth. Nor
|
|
15
|
+
// a path from the project root (`~/`), where the tree exists only while it is
|
|
16
|
+
// vendored or linked. `@stacksjs/defaults/*` is mapped onto the vendored tree in
|
|
17
|
+
// `storage/framework/tsconfig.app.json` and falls back to the installed
|
|
18
|
+
// package when there is none, so it resolves in both layouts.
|
|
19
|
+
import { buildWebSidebarSections } from '@stacksjs/defaults/resources/functions/dashboard/sidebar'
|
|
16
20
|
|
|
17
21
|
export const sidebarSections = buildWebSidebarSections()
|
|
18
22
|
|
|
@@ -398,15 +402,15 @@ html[data-appearance="arc"] [data-sidebar-section] > div:first-child {
|
|
|
398
402
|
// network requests fail or succeed; that signal is rendered in the
|
|
399
403
|
// fixed-position container below the <main> slot. Mounted in the
|
|
400
404
|
// layout so every dashboard page picks it up without opting in.
|
|
401
|
-
//
|
|
402
|
-
import type { ToastType } from '
|
|
403
|
-
import type { CraftSidebarSelectEvent } from '
|
|
404
|
-
import { useToasts, dismissToast } from '
|
|
405
|
-
import { ensureCraftNativeSidebarMarker, useCraftSidebarSelection } from '
|
|
406
|
-
import { readStoredAppearance, useAppearance } from '
|
|
407
|
-
import { authStore } from '
|
|
408
|
-
import { kanbanStore } from '
|
|
409
|
-
import { rbacStore } from '
|
|
405
|
+
// A package specifier for the same reason as the sidebar import above.
|
|
406
|
+
import type { ToastType } from '@stacksjs/defaults/functions/toasts'
|
|
407
|
+
import type { CraftSidebarSelectEvent } from '@stacksjs/defaults/functions/craft'
|
|
408
|
+
import { useToasts, dismissToast } from '@stacksjs/defaults/functions/toasts'
|
|
409
|
+
import { ensureCraftNativeSidebarMarker, useCraftSidebarSelection } from '@stacksjs/defaults/functions/craft'
|
|
410
|
+
import { readStoredAppearance, useAppearance } from '@stacksjs/defaults/views/dashboard/composables/useAppearance'
|
|
411
|
+
import { authStore } from '@stacksjs/defaults/views/dashboard/stores/auth'
|
|
412
|
+
import { kanbanStore } from '@stacksjs/defaults/views/dashboard/stores/kanban'
|
|
413
|
+
import { rbacStore } from '@stacksjs/defaults/views/dashboard/stores/rbac'
|
|
410
414
|
const { toasts } = useToasts()
|
|
411
415
|
registerStoresClient({ authStore, kanbanStore, rbacStore })
|
|
412
416
|
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<script client>
|
|
2
|
-
const placeholder = useReactiveProp('placeholder', 'Search...')
|
|
3
|
-
const query = state('')
|
|
4
|
-
const emit = defineEmits()
|
|
5
|
-
|
|
6
|
-
effect(() => emit('search', query()))
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<div>
|
|
10
|
-
<div class="relative rounded-md shadow-sm">
|
|
11
|
-
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
12
|
-
<span class="h-5 w-5 text-gray-400 i-hugeicons-search-01"></span>
|
|
13
|
-
</div>
|
|
14
|
-
<input
|
|
15
|
-
type="search"
|
|
16
|
-
name="search"
|
|
17
|
-
:placeholder="placeholder()"
|
|
18
|
-
:aria-label="placeholder()"
|
|
19
|
-
x-model="query"
|
|
20
|
-
autocomplete="off"
|
|
21
|
-
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"
|
|
22
|
-
>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|