@omnitend/dashboard-for-laravel 0.42.2 → 0.43.0
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/dist/components/extended/DXCurrencyInput.vue.d.ts +2 -2
- package/dist/components/extended/DXDashboard.vue.d.ts +14 -6
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +12 -3
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +21 -1
- package/dist/components/extended/DXStackingTable.vue.d.ts +25 -0
- package/dist/dashboard-for-laravel.js +9967 -9739
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/style.css +1 -1
- package/dist/types/navigation.d.ts +8 -0
- package/dist/utils/pageScrollLock.d.ts +9 -0
- package/docs/public/api-reference.json +81 -7
- package/docs/public/docs-map.md +3 -2
- package/docs/public/llms.txt +3 -2
- package/package.json +1 -1
- package/resources/css/theme.scss +452 -36
- package/resources/js/components/base/DModal.vue +20 -2
- package/resources/js/components/extended/DXCurrencyInput.vue +3 -1
- package/resources/js/components/extended/DXDashboard.vue +236 -43
- package/resources/js/components/extended/DXDashboardNavbar.vue +23 -13
- package/resources/js/components/extended/DXDashboardSidebar.vue +103 -29
- package/resources/js/components/extended/DXStackingTable.vue +264 -0
- package/resources/js/composables/useContainerWidth.ts +12 -3
- package/resources/js/index.ts +2 -0
- package/resources/js/types/navigation.ts +9 -0
- package/resources/js/utils/pageScrollLock.ts +27 -0
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
any `navbar-*` slot to the navbar, stripping the prefix.
|
|
8
8
|
-->
|
|
9
9
|
<template>
|
|
10
|
-
<div
|
|
10
|
+
<div
|
|
11
|
+
ref="layoutRef"
|
|
12
|
+
class="dashboard-layout d-flex"
|
|
13
|
+
:class="`dashboard-layout--sidebar-${sidebarVariant}`"
|
|
14
|
+
:data-dashboard-id="dashboardId"
|
|
15
|
+
>
|
|
11
16
|
<!-- Sidebar -->
|
|
12
17
|
<DXDashboardSidebar
|
|
18
|
+
:id="sidebarId"
|
|
19
|
+
ref="sidebarComponent"
|
|
20
|
+
phone-menu
|
|
13
21
|
:navigation="navigation"
|
|
14
22
|
:current-url="currentUrl"
|
|
15
23
|
:title="title"
|
|
@@ -17,7 +25,10 @@
|
|
|
17
25
|
:hidden="hidden"
|
|
18
26
|
:collapsible-groups="collapsibleGroups"
|
|
19
27
|
:auto-collapse-inactive-groups="autoCollapseInactiveGroups"
|
|
28
|
+
:variant="sidebarVariant"
|
|
20
29
|
@toggle="toggleSidebar"
|
|
30
|
+
@close="closePhoneMenu"
|
|
31
|
+
@navigate="onSidebarNavigate"
|
|
21
32
|
>
|
|
22
33
|
<!-- Dynamically forward all sidebar-* slots by stripping the prefix -->
|
|
23
34
|
<template
|
|
@@ -39,8 +50,13 @@
|
|
|
39
50
|
</template>
|
|
40
51
|
</DXDashboardSidebar>
|
|
41
52
|
|
|
42
|
-
<!-- Main Content Area
|
|
43
|
-
|
|
53
|
+
<!-- Main Content Area: the navbar and the page. Inert behind the open
|
|
54
|
+
phone menu, which covers both, so neither Tab, a click-through nor a
|
|
55
|
+
screen reader reaches them: focus is contained the way a native modal
|
|
56
|
+
<dialog> contains it, Tab moving between the sidebar's controls and
|
|
57
|
+
the browser's own. Modals teleported to <body> are outside and
|
|
58
|
+
unaffected. -->
|
|
59
|
+
<div class="dashboard-content flex-grow-1" :inert="phoneMenuOpen ? true : undefined">
|
|
44
60
|
<!-- Top Navbar -->
|
|
45
61
|
<DXDashboardNavbar
|
|
46
62
|
:page-title="pageTitle"
|
|
@@ -48,6 +64,8 @@
|
|
|
48
64
|
:search-align="searchAlign"
|
|
49
65
|
:actions-on-mobile="actionsOnMobile"
|
|
50
66
|
:user-menu-label="userMenuLabel"
|
|
67
|
+
:sidebar-expanded="!hidden"
|
|
68
|
+
:sidebar-id="sidebarId"
|
|
51
69
|
@toggle-sidebar="toggleSidebar"
|
|
52
70
|
>
|
|
53
71
|
<!-- Dynamically forward all navbar-* slots by stripping the prefix -->
|
|
@@ -96,13 +114,14 @@
|
|
|
96
114
|
</template>
|
|
97
115
|
|
|
98
116
|
<script setup lang="ts">
|
|
99
|
-
import { ref, computed, useSlots } from 'vue';
|
|
117
|
+
import { ref, computed, useSlots, useId, watch, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue';
|
|
100
118
|
import DXDashboardSidebar from './DXDashboardSidebar.vue';
|
|
101
119
|
import DXDashboardNavbar from './DXDashboardNavbar.vue';
|
|
102
120
|
import DContainer from '../base/DContainer.vue';
|
|
103
121
|
import DRow from '../base/DRow.vue';
|
|
104
122
|
import DCol from '../base/DCol.vue';
|
|
105
|
-
import
|
|
123
|
+
import { acquirePageScrollLock, releasePageScrollLock } from '../../utils/pageScrollLock';
|
|
124
|
+
import type { Navigation, NavbarActionsOnMobile, NavbarSearchAlign, SidebarVariant } from '../../types/navigation';
|
|
106
125
|
|
|
107
126
|
const slots = useSlots();
|
|
108
127
|
|
|
@@ -173,6 +192,14 @@ interface Props {
|
|
|
173
192
|
*/
|
|
174
193
|
autoCollapseInactiveGroups?: boolean;
|
|
175
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Sidebar colour scheme, forwarded to DXDashboardSidebar as `variant`:
|
|
197
|
+
* `'light'` (default: white pane, tinted group headers, green active item,
|
|
198
|
+
* Poppins) or `'dark'` (the navy rail). Colours come from the
|
|
199
|
+
* `--dx-sidebar-*` custom properties, so either can be rebranded in CSS.
|
|
200
|
+
*/
|
|
201
|
+
sidebarVariant?: SidebarVariant;
|
|
202
|
+
|
|
176
203
|
/** LocalStorage key for sidebar state persistence */
|
|
177
204
|
storageKey?: string;
|
|
178
205
|
|
|
@@ -190,6 +217,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
190
217
|
user: null,
|
|
191
218
|
collapsibleGroups: false,
|
|
192
219
|
autoCollapseInactiveGroups: true,
|
|
220
|
+
sidebarVariant: 'light',
|
|
193
221
|
storageKey: 'dashboard-sidebar-hidden',
|
|
194
222
|
dashboardId: '',
|
|
195
223
|
contentMaxWidth: '1140px',
|
|
@@ -224,66 +252,231 @@ const navbarSlots = computed(() => {
|
|
|
224
252
|
return result;
|
|
225
253
|
});
|
|
226
254
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Phone width: below Bootstrap's `sm` breakpoint (576px by default), the same
|
|
257
|
+
* breakpoint at which DModal goes full screen. There the open sidebar covers
|
|
258
|
+
* the whole viewport (theme.scss), so it behaves as a menu rather than a rail:
|
|
259
|
+
* it always starts closed, closes when a link is followed or on Escape, holds
|
|
260
|
+
* keyboard focus while open, and its state is never written to (or read from)
|
|
261
|
+
* `storageKey`, which keeps the desktop preference. Consumer apps navigate
|
|
262
|
+
* with full page loads, so restoring an "open" preference on a phone reopened
|
|
263
|
+
* the menu over every new page.
|
|
264
|
+
*
|
|
265
|
+
* The width comes from the theme, which publishes `breakpoint-max(sm)` as
|
|
266
|
+
* `--dx-dashboard-phone-max-width` on :root, so a theme compiled with its own
|
|
267
|
+
* `$grid-breakpoints` moves this behaviour and the full-screen styles
|
|
268
|
+
* together. 575.98px if the theme's CSS is not loaded.
|
|
269
|
+
*/
|
|
270
|
+
const PHONE_MAX_WIDTH_PROPERTY = '--dx-dashboard-phone-max-width';
|
|
271
|
+
const DEFAULT_PHONE_MAX_WIDTH = '575.98px';
|
|
272
|
+
|
|
273
|
+
const readPhoneMaxWidth = (): string => {
|
|
274
|
+
const published = getComputedStyle(document.documentElement)
|
|
275
|
+
.getPropertyValue(PHONE_MAX_WIDTH_PROPERTY)
|
|
276
|
+
.trim();
|
|
277
|
+
return published !== '' ? published : DEFAULT_PHONE_MAX_WIDTH;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// The query for the width last read. Re-read whenever the phone check runs
|
|
281
|
+
// (setup, mount, toggle, close, Escape), not once: a theme stylesheet that
|
|
282
|
+
// loads after the dashboard mounts must still take effect.
|
|
283
|
+
let phoneMediaQuery: MediaQueryList | null = null;
|
|
284
|
+
let phoneMediaQueryWidth = '';
|
|
285
|
+
let listeningForPhoneChanges = false;
|
|
286
|
+
|
|
287
|
+
const isPhone = ref(false);
|
|
288
|
+
|
|
289
|
+
const refreshPhoneQuery = (): void => {
|
|
290
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
291
|
+
const width = readPhoneMaxWidth();
|
|
292
|
+
if (phoneMediaQuery === null || width !== phoneMediaQueryWidth) {
|
|
293
|
+
if (listeningForPhoneChanges) {
|
|
294
|
+
phoneMediaQuery?.removeEventListener('change', onPhoneMediaChange);
|
|
295
|
+
}
|
|
296
|
+
phoneMediaQuery = window.matchMedia(`(max-width: ${width})`);
|
|
297
|
+
phoneMediaQueryWidth = width;
|
|
298
|
+
if (listeningForPhoneChanges) {
|
|
299
|
+
phoneMediaQuery.addEventListener('change', onPhoneMediaChange);
|
|
300
|
+
}
|
|
232
301
|
}
|
|
302
|
+
isPhone.value = phoneMediaQuery.matches;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Default with nothing stored: hidden for global instances (docs), visible for
|
|
306
|
+
// scoped instances (examples). Also what a server renders, having no storage.
|
|
307
|
+
const defaultHidden = (): boolean => !props.dashboardId;
|
|
233
308
|
|
|
309
|
+
// The remembered desktop preference, or the default when none is stored.
|
|
310
|
+
const readDesktopHidden = (): boolean => {
|
|
234
311
|
try {
|
|
235
312
|
const savedHidden = localStorage.getItem(props.storageKey);
|
|
236
313
|
if (savedHidden !== null) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// If no dashboard ID (global instance), update HTML class for SSR compatibility
|
|
240
|
-
if (!props.dashboardId) {
|
|
241
|
-
if (isHidden) {
|
|
242
|
-
document.documentElement.classList.remove('sidebar-visible');
|
|
243
|
-
} else {
|
|
244
|
-
document.documentElement.classList.add('sidebar-visible');
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return isHidden;
|
|
314
|
+
return JSON.parse(savedHidden);
|
|
249
315
|
}
|
|
250
316
|
} catch (error) {
|
|
251
317
|
console.error('Error loading sidebar state:', error);
|
|
252
318
|
}
|
|
319
|
+
return defaultHidden();
|
|
320
|
+
};
|
|
253
321
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return false; // Show sidebar in scoped instances by default
|
|
322
|
+
// The global instance (no dashboardId) mirrors visibility onto <html> so
|
|
323
|
+
// layout CSS can follow it before hydration (see the docs' inline script).
|
|
324
|
+
const syncHtmlClass = (isHidden: boolean): void => {
|
|
325
|
+
if (props.dashboardId || typeof document === 'undefined') return;
|
|
326
|
+
document.documentElement.classList.toggle('sidebar-visible', !isHidden);
|
|
260
327
|
};
|
|
261
328
|
|
|
262
|
-
|
|
329
|
+
// The state this browser should show: on a phone the menu is closed whatever
|
|
330
|
+
// is stored.
|
|
331
|
+
const getClientHiddenState = (): boolean => (isPhone.value ? true : readDesktopHidden());
|
|
332
|
+
|
|
333
|
+
// The first render uses what a server renders (`defaultHidden`), on the client
|
|
334
|
+
// too, so hydration sees the server's own markup and the state agrees with it;
|
|
335
|
+
// the client's state is applied on mount, before the browser paints a
|
|
336
|
+
// client-only mount. The global <html> class, which layout CSS reads before
|
|
337
|
+
// hydration, is set from the client's state straight away.
|
|
338
|
+
if (typeof window !== 'undefined') {
|
|
339
|
+
refreshPhoneQuery();
|
|
340
|
+
syncHtmlClass(getClientHiddenState());
|
|
341
|
+
}
|
|
342
|
+
const hidden = ref(defaultHidden());
|
|
263
343
|
|
|
264
|
-
const
|
|
265
|
-
|
|
344
|
+
const isMounted = ref(false);
|
|
345
|
+
// False while a <KeepAlive> holds the dashboard out of the page.
|
|
346
|
+
const isActive = ref(true);
|
|
266
347
|
|
|
267
|
-
|
|
268
|
-
|
|
348
|
+
/** The full-screen phone menu is open (and this dashboard is on the page). */
|
|
349
|
+
const phoneMenuOpen = computed(
|
|
350
|
+
() => isMounted.value && isActive.value && isPhone.value && !hidden.value,
|
|
351
|
+
);
|
|
269
352
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
document.documentElement.classList.remove('sidebar-visible');
|
|
274
|
-
} else {
|
|
275
|
-
document.documentElement.classList.add('sidebar-visible');
|
|
276
|
-
}
|
|
277
|
-
}
|
|
353
|
+
const sidebarId = `${useId()}-sidebar`;
|
|
354
|
+
const layoutRef = ref<HTMLElement | null>(null);
|
|
355
|
+
const sidebarComponent = ref<InstanceType<typeof DXDashboardSidebar> | null>(null);
|
|
278
356
|
|
|
279
|
-
|
|
357
|
+
const setHidden = (isHidden: boolean): void => {
|
|
358
|
+
hidden.value = isHidden;
|
|
359
|
+
if (typeof window === 'undefined') return;
|
|
360
|
+
syncHtmlClass(isHidden);
|
|
361
|
+
|
|
362
|
+
// Only desktop toggles are remembered; a phone's open/closed menu is not a
|
|
363
|
+
// preference.
|
|
364
|
+
if (isPhone.value) return;
|
|
280
365
|
try {
|
|
281
|
-
localStorage.setItem(props.storageKey, JSON.stringify(
|
|
366
|
+
localStorage.setItem(props.storageKey, JSON.stringify(isHidden));
|
|
282
367
|
} catch (error) {
|
|
283
368
|
console.error('Error saving sidebar state:', error);
|
|
284
369
|
}
|
|
285
370
|
};
|
|
286
371
|
|
|
372
|
+
const toggleSidebar = () => {
|
|
373
|
+
refreshPhoneQuery();
|
|
374
|
+
setHidden(!hidden.value);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Close the phone menu without touching the stored desktop preference.
|
|
378
|
+
const closePhoneMenu = (): void => {
|
|
379
|
+
refreshPhoneQuery();
|
|
380
|
+
if (isPhone.value && !hidden.value) {
|
|
381
|
+
setHidden(true);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// A link in the sidebar was followed: on a phone, get the menu out of the way
|
|
386
|
+
// (for client-side routing, and so it is gone before a slow full page load).
|
|
387
|
+
const onSidebarNavigate = (): void => {
|
|
388
|
+
closePhoneMenu();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const sidebarElement = (): HTMLElement | null =>
|
|
392
|
+
(sidebarComponent.value?.$el as HTMLElement | undefined) ?? null;
|
|
393
|
+
|
|
394
|
+
const onKeydown = (event: KeyboardEvent): void => {
|
|
395
|
+
if (event.key !== 'Escape') return;
|
|
396
|
+
// An Escape aimed at something outside the dashboard (a modal opened from
|
|
397
|
+
// the menu, teleported to <body>) is that thing's, not the menu's.
|
|
398
|
+
const target = event.target;
|
|
399
|
+
if (target instanceof Node && target !== document.body && !layoutRef.value?.contains(target)) return;
|
|
400
|
+
refreshPhoneQuery();
|
|
401
|
+
if (phoneMenuOpen.value) {
|
|
402
|
+
closePhoneMenu();
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const sidebarToggle = (): HTMLElement | null =>
|
|
407
|
+
layoutRef.value?.querySelector<HTMLElement>(`[aria-controls="${sidebarId}"]`) ?? null;
|
|
408
|
+
|
|
409
|
+
// Opening the phone menu moves focus into it (its close button); closing it
|
|
410
|
+
// hands focus back to the navbar toggle, unless focus has already gone
|
|
411
|
+
// somewhere else on purpose. With no toggle to return to (a consumer removed
|
|
412
|
+
// it), nothing is focused: the hidden menu has already lost focus to <body>.
|
|
413
|
+
// After the DOM update, so the navbar is no longer inert when focus returns.
|
|
414
|
+
watch(
|
|
415
|
+
phoneMenuOpen,
|
|
416
|
+
(isOpen, wasOpen) => {
|
|
417
|
+
const sidebar = sidebarElement();
|
|
418
|
+
if (isOpen) {
|
|
419
|
+
sidebar?.querySelector<HTMLElement>('.sidebar-close')?.focus();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!wasOpen || !hidden.value || !layoutRef.value?.isConnected) return;
|
|
423
|
+
const active = document.activeElement;
|
|
424
|
+
const focusWasInMenu =
|
|
425
|
+
active === null || active === document.body || (sidebar !== null && sidebar.contains(active));
|
|
426
|
+
if (focusWasInMenu) {
|
|
427
|
+
sidebarToggle()?.focus();
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{ flush: 'post' },
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Crossing the breakpoint (rotation, a resized window): entering phone width
|
|
434
|
+
// closes the menu; leaving it restores the remembered desktop preference.
|
|
435
|
+
const onPhoneMediaChange = (event: MediaQueryListEvent): void => {
|
|
436
|
+
isPhone.value = event.matches;
|
|
437
|
+
hidden.value = event.matches ? true : readDesktopHidden();
|
|
438
|
+
syncHtmlClass(hidden.value);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// While the phone menu is open the page behind it must not scroll: a class on
|
|
442
|
+
// <html> (styled in theme.scss, below `sm` only), held per dashboard so one
|
|
443
|
+
// closing or unmounting cannot unlock another's open menu.
|
|
444
|
+
const scrollLockOwner = {};
|
|
445
|
+
watch(phoneMenuOpen, (isOpen) => {
|
|
446
|
+
if (isOpen) {
|
|
447
|
+
acquirePageScrollLock(scrollLockOwner);
|
|
448
|
+
} else {
|
|
449
|
+
releasePageScrollLock(scrollLockOwner);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
onMounted(() => {
|
|
454
|
+
refreshPhoneQuery();
|
|
455
|
+
listeningForPhoneChanges = true;
|
|
456
|
+
phoneMediaQuery?.addEventListener('change', onPhoneMediaChange);
|
|
457
|
+
isMounted.value = true;
|
|
458
|
+
hidden.value = getClientHiddenState();
|
|
459
|
+
document.addEventListener('keydown', onKeydown);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// A <KeepAlive> keeps the dashboard alive off the page: whatever took its
|
|
463
|
+
// place must scroll, and the menu should not reappear open on return.
|
|
464
|
+
onDeactivated(() => {
|
|
465
|
+
closePhoneMenu();
|
|
466
|
+
isActive.value = false;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
onActivated(() => {
|
|
470
|
+
isActive.value = true;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
onBeforeUnmount(() => {
|
|
474
|
+
phoneMediaQuery?.removeEventListener('change', onPhoneMediaChange);
|
|
475
|
+
listeningForPhoneChanges = false;
|
|
476
|
+
document.removeEventListener('keydown', onKeydown);
|
|
477
|
+
releasePageScrollLock(scrollLockOwner);
|
|
478
|
+
});
|
|
479
|
+
|
|
287
480
|
defineExpose({
|
|
288
481
|
/**
|
|
289
482
|
* Show/hide the sidebar (same as clicking the navbar's hamburger). The
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
with a `navbar-` prefix.
|
|
7
7
|
-->
|
|
8
8
|
<template>
|
|
9
|
-
<header ref="headerRef" class="dashboard-navbar
|
|
9
|
+
<header ref="headerRef" class="dashboard-navbar">
|
|
10
10
|
<DContainer fluid>
|
|
11
|
-
<!-- Flex bar
|
|
12
|
-
|
|
11
|
+
<!-- Flex bar: toggle (and title from `md` up), search filling the
|
|
12
|
+
middle, user menu, all on one row at every width. Below `md` the
|
|
13
|
+
actions wrap to a row of their own (theme.scss, "Dashboard navbar
|
|
14
|
+
layout"). -->
|
|
13
15
|
<div ref="barRef" class="dashboard-navbar__bar d-flex flex-wrap align-items-center gap-3">
|
|
14
16
|
<div class="dashboard-navbar__start d-flex align-items-center gap-3">
|
|
15
17
|
<DButton
|
|
@@ -17,6 +19,8 @@
|
|
|
17
19
|
class="text-dark p-0"
|
|
18
20
|
@click="$emit('toggleSidebar')"
|
|
19
21
|
aria-label="Toggle sidebar"
|
|
22
|
+
:aria-expanded="sidebarExpanded === undefined ? undefined : String(sidebarExpanded)"
|
|
23
|
+
:aria-controls="sidebarId || undefined"
|
|
20
24
|
>
|
|
21
25
|
<!-- @slot Custom hamburger/menu icon. Defaults to a three-line SVG. -->
|
|
22
26
|
<slot name="menu-icon">
|
|
@@ -43,7 +47,7 @@
|
|
|
43
47
|
class="dashboard-navbar__search d-flex"
|
|
44
48
|
:class="searchAlignClass"
|
|
45
49
|
>
|
|
46
|
-
<!-- @slot Search input or component.
|
|
50
|
+
<!-- @slot Search input or component. Fills the middle of the bar at every width, aligned per `searchAlign`. The region is a size container named `dx-navbar-search`; a `<kbd>` hint inside it is hidden when it is narrower than 16rem. -->
|
|
47
51
|
<slot name="search" />
|
|
48
52
|
</div>
|
|
49
53
|
|
|
@@ -132,9 +136,8 @@ const props = withDefaults(
|
|
|
132
136
|
pageTitle?: string;
|
|
133
137
|
/**
|
|
134
138
|
* Horizontal alignment of the search slot content within its region
|
|
135
|
-
* (`"start"` = flush left, `"center"` = centred).
|
|
136
|
-
*
|
|
137
|
-
* full-width row below.
|
|
139
|
+
* (`"start"` = flush left, `"center"` = centred). The region sits inline
|
|
140
|
+
* between the toggle/title and the user menu at every width.
|
|
138
141
|
*/
|
|
139
142
|
searchAlign?: NavbarSearchAlign;
|
|
140
143
|
/**
|
|
@@ -148,6 +151,14 @@ const props = withDefaults(
|
|
|
148
151
|
* announces the trigger as the avatar's initial ("J") rather than as a menu.
|
|
149
152
|
*/
|
|
150
153
|
userMenuLabel?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Whether the sidebar the toggle controls is showing, exposed as the
|
|
156
|
+
* toggle's `aria-expanded`. Left unset, the toggle carries none.
|
|
157
|
+
* DXDashboard sets it.
|
|
158
|
+
*/
|
|
159
|
+
sidebarExpanded?: boolean;
|
|
160
|
+
/** `id` of the sidebar the toggle controls (its `aria-controls`). */
|
|
161
|
+
sidebarId?: string;
|
|
151
162
|
}>(),
|
|
152
163
|
{
|
|
153
164
|
user: null,
|
|
@@ -155,6 +166,8 @@ const props = withDefaults(
|
|
|
155
166
|
searchAlign: "start",
|
|
156
167
|
actionsOnMobile: "wrap",
|
|
157
168
|
userMenuLabel: "User menu",
|
|
169
|
+
sidebarExpanded: undefined,
|
|
170
|
+
sidebarId: "",
|
|
158
171
|
},
|
|
159
172
|
);
|
|
160
173
|
|
|
@@ -267,12 +280,9 @@ const getUserInitial = (user: { name: string } | null) => {
|
|
|
267
280
|
z-index: 1000;
|
|
268
281
|
}
|
|
269
282
|
|
|
270
|
-
/* The bar's vertical padding
|
|
271
|
-
|
|
272
|
-
geometry has one source (#102).
|
|
273
|
-
.dashboard-navbar__bar {
|
|
274
|
-
min-height: 3.5rem;
|
|
275
|
-
}
|
|
283
|
+
/* The bar's vertical padding, its height floor and the single-row content
|
|
284
|
+
budget are all owned by theme.scss, derived from `--dx-dashboard-header-height`,
|
|
285
|
+
so the geometry has one source (#102). */
|
|
276
286
|
|
|
277
287
|
/*
|
|
278
288
|
* The bar's responsive LAYOUT (the order/flex rules and the `md` switch) lives
|