@stacksjs/defaults 0.70.223 → 0.70.225
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/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/views/dashboard/layouts/default.stx +133 -330
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.225",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
<script server>
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
2
|
+
// Dashboard chrome. The navigation pane is `@stacksjs/components`' <Sidebar>
|
|
3
|
+
// in its `macos` theme — the same component Craft apps use, so the dashboard
|
|
4
|
+
// and a native Craft build render the identical source list.
|
|
5
|
+
//
|
|
6
|
+
// This used to be a hand-rolled <aside> plus ~180 lines of plain CSS, because
|
|
7
|
+
// @stacksjs/components 0.2.9x shipped only its hashed dist/ output and the
|
|
8
|
+
// component fatal-errored with an ENOENT (stacksjs/stacks#1989). That package
|
|
9
|
+
// now publishes its .stx sources, so the real component resolves and the
|
|
10
|
+
// duplicate markup is gone.
|
|
11
|
+
//
|
|
12
|
+
// Project-root-anchored (`~/`), not relative: the stx client bundler resolves
|
|
13
|
+
// a relative import against the PAGE that extends this layout, not against
|
|
14
|
+
// this file, so `../../..` pointed somewhere different for every page depth.
|
|
15
|
+
import { buildWebSidebarSections } from '~/storage/framework/defaults/resources/functions/dashboard/sidebar'
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
export const
|
|
17
|
+
export const sidebarSections = buildWebSidebarSections()
|
|
18
|
+
|
|
19
|
+
// Role gating (stacksjs/stacks#1843). Models can declare
|
|
20
|
+
// `dashboard.roles: ['admin', 'dev']`; rows for those models are hidden from
|
|
21
|
+
// viewers who hold none of them. Emitted as an id-keyed map rather than a
|
|
22
|
+
// `data-required-roles` attribute on each row, which keeps the generic
|
|
23
|
+
// Sidebar component free of Stacks-specific attributes.
|
|
24
|
+
export const sidebarRoleMap = JSON.stringify(
|
|
25
|
+
Object.fromEntries(
|
|
26
|
+
sidebarSections.flatMap(section =>
|
|
27
|
+
section.items
|
|
28
|
+
.filter(item => item.roles && item.roles.length > 0)
|
|
29
|
+
.map(item => [item.id, item.roles]),
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
21
33
|
</script>
|
|
22
34
|
|
|
23
35
|
<!-- Dashboard icon stylesheet. Most dashboard icons are applied via dynamic
|
|
@@ -42,13 +54,6 @@ canvas[id$="_chart"] {
|
|
|
42
54
|
height: 100% !important;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
/*
|
|
46
|
-
* Dashboard body + sidebar. Plain CSS on purpose: stx serves a layout's raw
|
|
47
|
-
* style rules verbatim, but it does NOT expand Crosswind @apply here, so these
|
|
48
|
-
* are hand-written. Class names match the markup built in
|
|
49
|
-
* resources/functions/dashboard/sidebar.ts. Fixed 250px pane: Home pinned top,
|
|
50
|
-
* sections scroll in the middle, Settings pinned bottom.
|
|
51
|
-
*/
|
|
52
57
|
/*
|
|
53
58
|
* The window background is owned by the main content pane, not the body element.
|
|
54
59
|
* In the native Craft window the WKWebView is transparent and an
|
|
@@ -61,156 +66,22 @@ canvas[id$="_chart"] {
|
|
|
61
66
|
body {
|
|
62
67
|
background: transparent;
|
|
63
68
|
}
|
|
69
|
+
/*
|
|
70
|
+
* The pane offset follows the sidebar's own width variable, which <Sidebar>
|
|
71
|
+
* writes onto this element via `shell-selector`. The literal fallback keeps
|
|
72
|
+
* the layout correct on the first paint, before the controller mounts, and
|
|
73
|
+
* the collapsed state animates because the variable changes rather than a
|
|
74
|
+
* class toggling a hard-coded margin.
|
|
75
|
+
*/
|
|
64
76
|
[data-stx-content] {
|
|
65
77
|
min-height: 100vh;
|
|
78
|
+
margin-left: var(--stx-sidebar-width, 250px);
|
|
66
79
|
background: #f4f4f5;
|
|
80
|
+
transition: margin-left 0.3s ease-out;
|
|
67
81
|
}
|
|
68
82
|
.dark [data-stx-content] {
|
|
69
83
|
background: #0a0a0a;
|
|
70
84
|
}
|
|
71
|
-
.dashboard-sidebar {
|
|
72
|
-
position: fixed;
|
|
73
|
-
inset: 0 auto 0 0;
|
|
74
|
-
width: 250px;
|
|
75
|
-
display: flex;
|
|
76
|
-
flex-direction: column;
|
|
77
|
-
padding: 0 12px 10px;
|
|
78
|
-
overflow: hidden;
|
|
79
|
-
/* Translucent so the native NSVisualEffectView vibrancy reads through in the
|
|
80
|
-
Craft window; the blur is the frosted-glass fallback in a plain browser,
|
|
81
|
-
where there is no material behind the pane. */
|
|
82
|
-
background: rgba(255, 255, 255, 0.55);
|
|
83
|
-
-webkit-backdrop-filter: saturate(180%) blur(24px);
|
|
84
|
-
backdrop-filter: saturate(180%) blur(24px);
|
|
85
|
-
/* No hard border: a soft cast shadow to the right lifts the translucent pane
|
|
86
|
-
ABOVE the content, so it reads as a glass layer laid on top (Finder-style)
|
|
87
|
-
rather than a flat column butted beside it. The 1px inset is the hairline. */
|
|
88
|
-
box-shadow: 1px 0 0 rgba(0, 0, 0, 0.05), 12px 0 34px -10px rgba(0, 0, 0, 0.18);
|
|
89
|
-
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
|
90
|
-
-webkit-font-smoothing: antialiased;
|
|
91
|
-
z-index: 40;
|
|
92
|
-
}
|
|
93
|
-
.sidebar-header {
|
|
94
|
-
flex-shrink: 0;
|
|
95
|
-
height: 38px;
|
|
96
|
-
}
|
|
97
|
-
.sidebar-nav {
|
|
98
|
-
flex: 1 1 auto;
|
|
99
|
-
min-height: 0;
|
|
100
|
-
overflow-y: auto;
|
|
101
|
-
scrollbar-width: none;
|
|
102
|
-
}
|
|
103
|
-
.sidebar-nav::-webkit-scrollbar {
|
|
104
|
-
width: 0;
|
|
105
|
-
}
|
|
106
|
-
.sidebar-link {
|
|
107
|
-
display: flex;
|
|
108
|
-
align-items: center;
|
|
109
|
-
gap: 10px;
|
|
110
|
-
flex-shrink: 0;
|
|
111
|
-
height: 30px;
|
|
112
|
-
padding: 0 10px;
|
|
113
|
-
border-radius: 6px;
|
|
114
|
-
color: #3a3a3c;
|
|
115
|
-
font-size: 13px;
|
|
116
|
-
font-weight: 450;
|
|
117
|
-
text-decoration: none;
|
|
118
|
-
white-space: nowrap;
|
|
119
|
-
overflow: hidden;
|
|
120
|
-
text-overflow: ellipsis;
|
|
121
|
-
transition: background-color 0.12s ease, color 0.12s ease;
|
|
122
|
-
}
|
|
123
|
-
.sidebar-link:hover {
|
|
124
|
-
background: rgba(0, 0, 0, 0.05);
|
|
125
|
-
}
|
|
126
|
-
.sidebar-link[data-active="true"] {
|
|
127
|
-
background: #007aff;
|
|
128
|
-
color: #fff;
|
|
129
|
-
}
|
|
130
|
-
.sidebar-link-home {
|
|
131
|
-
font-weight: 550;
|
|
132
|
-
}
|
|
133
|
-
.sidebar-nav .sidebar-link {
|
|
134
|
-
margin-top: 1px;
|
|
135
|
-
}
|
|
136
|
-
.sidebar-icon {
|
|
137
|
-
display: inline-flex;
|
|
138
|
-
align-items: center;
|
|
139
|
-
justify-content: center;
|
|
140
|
-
flex-shrink: 0;
|
|
141
|
-
width: 18px;
|
|
142
|
-
height: 18px;
|
|
143
|
-
opacity: 0.82;
|
|
144
|
-
}
|
|
145
|
-
.sidebar-icon svg {
|
|
146
|
-
width: 18px;
|
|
147
|
-
height: 18px;
|
|
148
|
-
}
|
|
149
|
-
.sidebar-link[data-active="true"] .sidebar-icon {
|
|
150
|
-
opacity: 1;
|
|
151
|
-
}
|
|
152
|
-
.sidebar-section {
|
|
153
|
-
margin-top: 12px;
|
|
154
|
-
}
|
|
155
|
-
.sidebar-section-label {
|
|
156
|
-
display: flex;
|
|
157
|
-
align-items: center;
|
|
158
|
-
justify-content: space-between;
|
|
159
|
-
width: 100%;
|
|
160
|
-
height: 22px;
|
|
161
|
-
padding: 0 10px;
|
|
162
|
-
background: none;
|
|
163
|
-
border: 0;
|
|
164
|
-
cursor: pointer;
|
|
165
|
-
color: #8a8a8e;
|
|
166
|
-
font-size: 11px;
|
|
167
|
-
font-weight: 600;
|
|
168
|
-
letter-spacing: 0.03em;
|
|
169
|
-
text-transform: uppercase;
|
|
170
|
-
}
|
|
171
|
-
.sidebar-section-label:hover {
|
|
172
|
-
color: #6a6a6e;
|
|
173
|
-
}
|
|
174
|
-
.sidebar-section-list {
|
|
175
|
-
list-style: none;
|
|
176
|
-
margin: 2px 0 0;
|
|
177
|
-
padding: 0;
|
|
178
|
-
}
|
|
179
|
-
.sidebar-section.collapsed .sidebar-section-list {
|
|
180
|
-
display: none;
|
|
181
|
-
}
|
|
182
|
-
.sidebar-chevron {
|
|
183
|
-
width: 7px;
|
|
184
|
-
height: 7px;
|
|
185
|
-
border-right: 1.5px solid currentColor;
|
|
186
|
-
border-bottom: 1.5px solid currentColor;
|
|
187
|
-
transform: rotate(45deg);
|
|
188
|
-
transition: transform 0.15s ease;
|
|
189
|
-
opacity: 0.7;
|
|
190
|
-
}
|
|
191
|
-
.sidebar-section.collapsed .sidebar-chevron {
|
|
192
|
-
transform: rotate(-45deg);
|
|
193
|
-
}
|
|
194
|
-
.dark .dashboard-sidebar {
|
|
195
|
-
background: rgba(28, 28, 30, 0.55);
|
|
196
|
-
box-shadow: 1px 0 0 rgba(255, 255, 255, 0.06), 12px 0 34px -10px rgba(0, 0, 0, 0.55);
|
|
197
|
-
}
|
|
198
|
-
.dark .sidebar-link {
|
|
199
|
-
color: #d1d1d6;
|
|
200
|
-
}
|
|
201
|
-
.dark .sidebar-link:hover {
|
|
202
|
-
background: rgba(255, 255, 255, 0.06);
|
|
203
|
-
}
|
|
204
|
-
.dark .sidebar-link[data-active="true"] {
|
|
205
|
-
background: #0a84ff;
|
|
206
|
-
color: #fff;
|
|
207
|
-
}
|
|
208
|
-
.dark .sidebar-section-label {
|
|
209
|
-
color: #8d8d92;
|
|
210
|
-
}
|
|
211
|
-
.dark .sidebar-section-label:hover {
|
|
212
|
-
color: #aeaeb2;
|
|
213
|
-
}
|
|
214
85
|
</style>
|
|
215
86
|
|
|
216
87
|
<script>
|
|
@@ -274,86 +145,6 @@ body {
|
|
|
274
145
|
</script>
|
|
275
146
|
|
|
276
147
|
<script client>
|
|
277
|
-
// Craft native sidebar integration
|
|
278
|
-
// Maps sidebar itemIds to STX page routes for SPA navigation
|
|
279
|
-
const sidebarRoutes = {
|
|
280
|
-
'home': '/',
|
|
281
|
-
'dependencies': '/dependencies',
|
|
282
|
-
'services': '/health',
|
|
283
|
-
'ci': '/ci',
|
|
284
|
-
'components': '/components',
|
|
285
|
-
'functions': '/functions',
|
|
286
|
-
'releases': '/releases',
|
|
287
|
-
'packages': '/packages',
|
|
288
|
-
'content-dashboard': '/content/dashboard',
|
|
289
|
-
'files': '/content/files',
|
|
290
|
-
'blog': '/content/blog',
|
|
291
|
-
'pages': '/content/pages',
|
|
292
|
-
'posts': '/content/posts',
|
|
293
|
-
'categories': '/content/categories',
|
|
294
|
-
'tags': '/content/tags',
|
|
295
|
-
'comments': '/content/comments',
|
|
296
|
-
'authors': '/content/authors',
|
|
297
|
-
'seo': '/content/seo',
|
|
298
|
-
'deployments': '/deployments',
|
|
299
|
-
'requests': '/requests',
|
|
300
|
-
'realtime': '/realtime',
|
|
301
|
-
'actions': '/actions',
|
|
302
|
-
'commands': '/commands',
|
|
303
|
-
'queue': '/queue',
|
|
304
|
-
'jobs': '/jobs',
|
|
305
|
-
'inbox': '/inbox',
|
|
306
|
-
'queries': '/queries',
|
|
307
|
-
'notifications': '/notifications/dashboard',
|
|
308
|
-
'data-dashboard': '/data/dashboard',
|
|
309
|
-
'activity': '/data/activity',
|
|
310
|
-
'users': '/data/users',
|
|
311
|
-
'teams': '/data/teams',
|
|
312
|
-
'subscribers': '/data/subscribers',
|
|
313
|
-
'commerce-dashboard': '/commerce/dashboard',
|
|
314
|
-
'pos': '/commerce/pos',
|
|
315
|
-
'customers': '/commerce/customers',
|
|
316
|
-
'orders': '/commerce/orders',
|
|
317
|
-
'products': '/commerce/products',
|
|
318
|
-
'coupons': '/commerce/coupons',
|
|
319
|
-
'gift-cards': '/commerce/gift-cards',
|
|
320
|
-
'payments': '/commerce/payments',
|
|
321
|
-
'delivery': '/commerce/delivery',
|
|
322
|
-
'taxes': '/commerce/taxes',
|
|
323
|
-
'lists': '/marketing/lists',
|
|
324
|
-
'social-posts': '/marketing/social-posts',
|
|
325
|
-
'campaigns': '/marketing/campaigns',
|
|
326
|
-
'marketing-reviews': '/marketing/reviews',
|
|
327
|
-
'analytics-web': '/analytics/web',
|
|
328
|
-
'analytics-blog': '/analytics/blog',
|
|
329
|
-
'analytics-goals': '/analytics/goals',
|
|
330
|
-
'analytics-commerce': '/analytics/commerce/web',
|
|
331
|
-
'analytics-sales': '/analytics/commerce/sales',
|
|
332
|
-
'analytics-marketing': '/analytics/marketing',
|
|
333
|
-
'cloud': '/cloud',
|
|
334
|
-
'servers': '/servers',
|
|
335
|
-
'serverless': '/serverless',
|
|
336
|
-
'dns': '/dns',
|
|
337
|
-
'permissions': '/management/permissions',
|
|
338
|
-
'kanban': '/kanban',
|
|
339
|
-
'mailboxes': '/mailboxes',
|
|
340
|
-
'logs': '/logs',
|
|
341
|
-
'buddy': '/buddy',
|
|
342
|
-
'environment': '/environment',
|
|
343
|
-
'access-tokens': '/access-tokens',
|
|
344
|
-
'settings': '/settings',
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Handle Craft native sidebar selection events
|
|
348
|
-
window.craft = window.craft || {}
|
|
349
|
-
window.craft._sidebarSelectHandler = (event) => {
|
|
350
|
-
const url = sidebarRoutes[event.itemId]
|
|
351
|
-
|| (event.itemId && event.itemId.startsWith('model-') ? '/models/' + event.itemId.slice(6) : null)
|
|
352
|
-
if (url) {
|
|
353
|
-
navigate(url)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
148
|
// Global toast queue. Composables across the dashboard call
|
|
358
149
|
// `pushToast(...)` (defined in defaults/functions/toasts.ts) when
|
|
359
150
|
// network requests fail or succeed; that signal is rendered in the
|
|
@@ -382,20 +173,17 @@ const toastWrapperClass = (type) => `pointer-events-auto overflow-hidden bg-whit
|
|
|
382
173
|
|
|
383
174
|
// Role-gated sidebar filter (stacksjs/stacks#1843).
|
|
384
175
|
//
|
|
385
|
-
// The server emits sidebar `<a>` items with `data-required-roles="…"` for
|
|
386
|
-
// any model that declares `dashboard.roles: ['admin', 'dev', …]`. We
|
|
387
|
-
// walk those nodes on hydration and on auth-store changes, hiding rows
|
|
388
|
-
// the current viewer can't access.
|
|
389
|
-
//
|
|
390
176
|
// Permissive default: when the viewer is unauthenticated (the localhost
|
|
391
|
-
// dev dashboard)
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
// Guarded: `useStore('auth')` THROWS when the store was never registered, and
|
|
396
|
-
// throw here aborts scope registration for the entire page, leaving every
|
|
397
|
-
// on every dashboard page unbound. A missing auth store must degrade to
|
|
398
|
-
// permissive default
|
|
177
|
+
// dev dashboard) every gated row stays visible. In production deployments
|
|
178
|
+
// where `/api/dashboard/*` sits behind auth, the endpoint returns real role
|
|
179
|
+
// data and the filter hides what the viewer can't reach.
|
|
180
|
+
//
|
|
181
|
+
// Guarded: `useStore('auth')` THROWS when the store was never registered, and
|
|
182
|
+
// a throw here aborts scope registration for the entire page, leaving every
|
|
183
|
+
// signal on every dashboard page unbound. A missing auth store must degrade to
|
|
184
|
+
// the permissive default, not take the dashboard down with it.
|
|
185
|
+
const sidebarRoles = {{ sidebarRoleMap }}
|
|
186
|
+
|
|
399
187
|
const auth = (() => {
|
|
400
188
|
try {
|
|
401
189
|
const store = useStore('auth')
|
|
@@ -408,59 +196,64 @@ const auth = (() => {
|
|
|
408
196
|
})()
|
|
409
197
|
|
|
410
198
|
function applyRoleFilter() {
|
|
411
|
-
// No auth store means no role data, which is the unauthenticated case: show
|
|
412
|
-
// every gated row rather than hiding the whole sidebar.
|
|
413
199
|
const roleList = auth ? auth.roles() : []
|
|
414
200
|
const unauth = auth ? auth.unauthenticated() : true
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
// the authoritative answer.
|
|
201
|
+
for (const [itemId, required] of Object.entries(sidebarRoles)) {
|
|
202
|
+
const row = document.querySelector(`[data-sidebar-row][data-item-id="${itemId}"]`)
|
|
203
|
+
if (!row) continue
|
|
204
|
+
// Unauthenticated viewers see everything — keeps the dev dashboard
|
|
205
|
+
// usable. Once authenticated, role membership is authoritative. CSS
|
|
206
|
+
// `display: none` keeps the DOM intact for SPA nav, so a later role
|
|
207
|
+
// change can re-show the row without a re-render.
|
|
423
208
|
const allowed = unauth || required.some(r => roleList.includes(r))
|
|
424
|
-
|
|
425
|
-
// link itself. Either way the row disappears from the rendered
|
|
426
|
-
// sidebar; CSS `display: none` keeps the DOM intact for SPA nav so
|
|
427
|
-
// a later role change can re-show without a re-render.
|
|
428
|
-
const target = el.closest('li') || el
|
|
429
|
-
target.style.display = allowed ? '' : 'none'
|
|
209
|
+
row.style.display = allowed ? '' : 'none'
|
|
430
210
|
}
|
|
431
211
|
}
|
|
432
212
|
|
|
433
213
|
effect(() => {
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
// the auth fetch resolves.
|
|
214
|
+
// Re-runs whenever `auth.roles()` / `auth.unauthenticated()` change,
|
|
215
|
+
// including the first run after the auth fetch resolves.
|
|
437
216
|
applyRoleFilter()
|
|
438
217
|
})
|
|
439
218
|
</script>
|
|
440
219
|
|
|
441
|
-
<
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
220
|
+
<Sidebar
|
|
221
|
+
:sections="sidebarSections"
|
|
222
|
+
theme="macos"
|
|
223
|
+
width="250"
|
|
224
|
+
placement="fixed"
|
|
225
|
+
persist-key="stacks-dashboard-sidebar"
|
|
226
|
+
shell-selector="[data-stx-content]"
|
|
227
|
+
>
|
|
228
|
+
<template #header>
|
|
229
|
+
<!-- Chrome strip. The traffic lights are native (the Craft window is
|
|
230
|
+
titlebarHidden), so this reserves their space and acts as the window
|
|
231
|
+
drag region; in a plain browser it is the same empty strip macOS
|
|
232
|
+
sidebars carry above their first row. -->
|
|
233
|
+
<SidebarHeader :show-window-controls="false" />
|
|
234
|
+
</template>
|
|
235
|
+
</Sidebar>
|
|
449
236
|
|
|
450
237
|
<script>
|
|
451
|
-
//
|
|
238
|
+
// Sidebar wiring the component intentionally leaves to the host app: SPA
|
|
239
|
+
// navigation, keeping the highlight in sync with the real route, and Craft's
|
|
240
|
+
// native selection events.
|
|
241
|
+
//
|
|
452
242
|
// Runs as a plain <script> IIFE (like the import-map script above), NOT via
|
|
453
243
|
// onMount: stx gives a LAYOUT no reactive scope of its own, so its onMount /
|
|
454
|
-
// effect callbacks never fire ("global mountCallbacks: 0")
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
// SPA route change without depending on a scope mount.
|
|
244
|
+
// effect callbacks never fire ("global mountCallbacks: 0"). A DOMContentLoaded
|
|
245
|
+
// IIFE plus the stx:navigate event cover hard loads and every SPA route change
|
|
246
|
+
// without depending on a scope mount.
|
|
458
247
|
;(function () {
|
|
459
248
|
if (typeof document === 'undefined') return
|
|
460
249
|
|
|
461
|
-
|
|
250
|
+
function rows(pane) {
|
|
251
|
+
return pane.querySelectorAll('a[data-sidebar-item][href]')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Highlight the sidebar row for the CURRENT route. Longest-prefix match, so a
|
|
462
255
|
// nested/detail page (e.g. /commerce/products/42) still lights up its section
|
|
463
|
-
//
|
|
256
|
+
// row (/commerce/products); the root row (/) only ever matches the home page.
|
|
464
257
|
function syncActive(url) {
|
|
465
258
|
var pane = document.querySelector('[data-stx-sidebar]')
|
|
466
259
|
if (!pane) return
|
|
@@ -471,7 +264,7 @@ effect(() => {
|
|
|
471
264
|
var cur = norm(path)
|
|
472
265
|
var best = null
|
|
473
266
|
var bestLen = -1
|
|
474
|
-
var links = pane
|
|
267
|
+
var links = rows(pane)
|
|
475
268
|
for (var i = 0; i < links.length; i++) {
|
|
476
269
|
var href = links[i].getAttribute('href') || ''
|
|
477
270
|
if (href.indexOf('http') === 0) continue
|
|
@@ -483,51 +276,61 @@ effect(() => {
|
|
|
483
276
|
bestLen = h.length
|
|
484
277
|
}
|
|
485
278
|
}
|
|
486
|
-
var actives = pane.querySelectorAll('
|
|
487
|
-
for (var j = 0; j < actives.length; j++)
|
|
488
|
-
|
|
279
|
+
var actives = pane.querySelectorAll('[data-sidebar-item][data-active="true"]')
|
|
280
|
+
for (var j = 0; j < actives.length; j++) {
|
|
281
|
+
actives[j].removeAttribute('data-active')
|
|
282
|
+
actives[j].removeAttribute('aria-current')
|
|
283
|
+
}
|
|
284
|
+
if (best) {
|
|
285
|
+
best.setAttribute('data-active', 'true')
|
|
286
|
+
best.setAttribute('aria-current', 'page')
|
|
287
|
+
}
|
|
489
288
|
}
|
|
490
289
|
|
|
491
290
|
function init() {
|
|
492
291
|
var pane = document.querySelector('[data-stx-sidebar]')
|
|
493
292
|
if (!pane) return
|
|
494
293
|
|
|
495
|
-
// Restore per-section collapsed state persisted by the section toggle
|
|
496
|
-
// (the inline onclick in renderSection writes localStorage['sidebar-collapsed']).
|
|
497
|
-
try {
|
|
498
|
-
var saved = JSON.parse(localStorage.getItem('sidebar-collapsed') || '{}')
|
|
499
|
-
var sections = pane.querySelectorAll('.sidebar-section')
|
|
500
|
-
for (var i = 0; i < sections.length; i++) {
|
|
501
|
-
if (saved[sections[i].getAttribute('data-section')]) sections[i].classList.add('collapsed')
|
|
502
|
-
}
|
|
503
|
-
} catch (e) {}
|
|
504
|
-
|
|
505
294
|
syncActive()
|
|
506
295
|
|
|
507
296
|
// Bind route + click listeners once. This script can re-run on SPA fragment
|
|
508
297
|
// swaps, so guard against stacking duplicate handlers on the persistent pane.
|
|
509
|
-
if (
|
|
510
|
-
|
|
298
|
+
if (pane.__sidebarWired) return
|
|
299
|
+
pane.__sidebarWired = true
|
|
300
|
+
|
|
301
|
+
// Follow EVERY route change, not just sidebar clicks. navigate() dispatches
|
|
302
|
+
// stx:navigate (content links, programmatic navigate, and popstate
|
|
303
|
+
// back/forward all route through it), so the highlight tracks the real
|
|
304
|
+
// route instead of only updating when a sidebar row is clicked.
|
|
305
|
+
window.addEventListener('stx:navigate', function (event) {
|
|
306
|
+
syncActive(event && event.detail && event.detail.url)
|
|
307
|
+
})
|
|
511
308
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
309
|
+
// SPA-navigate sidebar rows; fall back to full loads when the router isn't
|
|
310
|
+
// available or the target is external. The component already moves
|
|
311
|
+
// `data-active` on click, and the stx:navigate listener re-syncs after.
|
|
312
|
+
pane.addEventListener('click', function (event) {
|
|
313
|
+
var link = event.target.closest('a[data-sidebar-item]')
|
|
314
|
+
if (!link) return
|
|
315
|
+
// Disclosure chevrons live inside the row and only expand a subtree.
|
|
316
|
+
if (event.target.closest('[data-sidebar-disclosure]')) return
|
|
317
|
+
var href = link.getAttribute('href')
|
|
318
|
+
if (!href || href.indexOf('http') === 0 || typeof navigate !== 'function') return
|
|
319
|
+
event.preventDefault()
|
|
320
|
+
navigate(href)
|
|
321
|
+
})
|
|
519
322
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
323
|
+
// Craft native sidebar selection. When a Craft build installs a real
|
|
324
|
+
// NSOutlineView, it reports the selected row's id; resolve that back to a
|
|
325
|
+
// route from the rendered markup rather than a hand-maintained id-to-URL
|
|
326
|
+
// table, so the two can never drift apart.
|
|
327
|
+
window.craft = window.craft || {}
|
|
328
|
+
window.craft._sidebarSelectHandler = function (event) {
|
|
329
|
+
var id = event && event.itemId
|
|
330
|
+
if (!id) return
|
|
331
|
+
var row = pane.querySelector('[data-sidebar-row][data-item-id="' + id + '"] a[data-sidebar-item][href]')
|
|
332
|
+
var href = row && row.getAttribute('href')
|
|
333
|
+
if (href) navigate(href)
|
|
531
334
|
}
|
|
532
335
|
}
|
|
533
336
|
|
|
@@ -536,7 +339,7 @@ effect(() => {
|
|
|
536
339
|
})();
|
|
537
340
|
</script>
|
|
538
341
|
|
|
539
|
-
<main data-stx-content
|
|
342
|
+
<main data-stx-content>
|
|
540
343
|
@slot('content')
|
|
541
344
|
</main>
|
|
542
345
|
|