@nanahoshi/mona-ui 0.3.0 → 0.5.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/card/package.json +4 -0
- package/collapsible/package.json +4 -0
- package/fesm2022/nanahoshi-mona-ui-button.mjs +1 -2
- package/fesm2022/nanahoshi-mona-ui-card.mjs +307 -0
- package/fesm2022/nanahoshi-mona-ui-collapsible.mjs +304 -0
- package/fesm2022/nanahoshi-mona-ui-common.mjs +19 -1
- package/fesm2022/nanahoshi-mona-ui-contextmenu.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-dropdown-button.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-expansion-panel.mjs +20 -9
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-cell.component-B6FBLtrr.mjs → nanahoshi-mona-ui-grid-grid-footer-cell.component-D-JCCvYJ.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-table-cell.directive-OJe4UYuw.mjs → nanahoshi-mona-ui-grid-grid-footer-table-cell.directive-7zwgl7u1.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-DCd8K6ng.mjs → nanahoshi-mona-ui-grid-grid-list.component-n3eQNIza.mjs} +5 -5
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-toggle.component-Dl8Hlkhj.mjs → nanahoshi-mona-ui-grid-grid-toggle.component-BhS7c7rW.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-C5FRZlvv.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-B4VB3xFw.mjs} +5 -5
- package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-BRZUd8cE.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-A6Cq2a4Q.mjs} +11 -11
- package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-menubar.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-popup-menu.mjs +18 -16
- package/fesm2022/nanahoshi-mona-ui-sidebar.mjs +2025 -0
- package/fesm2022/nanahoshi-mona-ui-skeleton.mjs +96 -0
- package/fesm2022/nanahoshi-mona-ui-split-button.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-theme.mjs +71 -25
- package/fesm2022/nanahoshi-mona-ui-tooltip.mjs +49 -2
- package/package.json +17 -1
- package/sidebar/package.json +4 -0
- package/skeleton/package.json +4 -0
- package/types/nanahoshi-mona-ui-card.d.ts +104 -0
- package/types/nanahoshi-mona-ui-circular-progress-bar.d.ts +2 -1
- package/types/nanahoshi-mona-ui-collapsible.d.ts +92 -0
- package/types/nanahoshi-mona-ui-common.d.ts +6 -2
- package/types/nanahoshi-mona-ui-expansion-panel.d.ts +11 -3
- package/types/nanahoshi-mona-ui-popup-menu.d.ts +6 -4
- package/types/nanahoshi-mona-ui-sidebar.d.ts +695 -0
- package/types/nanahoshi-mona-ui-skeleton.d.ts +51 -0
- package/types/nanahoshi-mona-ui-theme.d.ts +28 -2
|
@@ -0,0 +1,2025 @@
|
|
|
1
|
+
import * as i1 from '@angular/cdk/a11y';
|
|
2
|
+
import { CdkTrapFocus } from '@angular/cdk/a11y';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { signal, computed, Service, InjectionToken, inject, PLATFORM_ID, linkedSignal, effect, untracked, ElementRef, Injector, input, model, afterNextRender, ChangeDetectionStrategy, Component, DestroyRef, Directive, makeEnvironmentProviders } from '@angular/core';
|
|
5
|
+
import { classInputToClass } from '@nanahoshi/mona-ui/common';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
|
+
import { createElementControlId, themeRaisedBackdropClasses } from '@nanahoshi/mona-ui/internal';
|
|
8
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
9
|
+
import { cva } from 'class-variance-authority';
|
|
10
|
+
import { SkeletonComponent } from '@nanahoshi/mona-ui/skeleton';
|
|
11
|
+
import * as i1$1 from '@nanahoshi/mona-ui/text-box';
|
|
12
|
+
import { TextBoxDirective } from '@nanahoshi/mona-ui/text-box';
|
|
13
|
+
import * as i1$2 from '@nanahoshi/mona-ui/button';
|
|
14
|
+
import { ButtonDirective } from '@nanahoshi/mona-ui/button';
|
|
15
|
+
import { CollapsibleToken } from '@nanahoshi/mona-ui/collapsible';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Layout-wide state shared by every sidebar under one `mona-sidebar-layout`. Provided by
|
|
19
|
+
* `SidebarLayoutComponent`, while each `mona-sidebar` provides its own `SidebarService`.
|
|
20
|
+
*
|
|
21
|
+
* A layout can hold more than one sidebar — a navigation column on one edge and an inspector on the
|
|
22
|
+
* other — so the things that genuinely belong to the layout rather than to any one sidebar live here:
|
|
23
|
+
* the breakpoint, the registry the layout and its siblings look sidebars up in, and the rule that only
|
|
24
|
+
* one drawer is open at a time.
|
|
25
|
+
*
|
|
26
|
+
* This class is internal, and is not reachable through the public API. Consumers read a single
|
|
27
|
+
* sidebar's state through `injectSidebar()`.
|
|
28
|
+
*/
|
|
29
|
+
class SidebarLayoutService {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.#compact = signal(false, /* @ts-ignore */
|
|
32
|
+
...(ngDevMode ? [{ debugName: "#compact" }] : /* istanbul ignore next */ []));
|
|
33
|
+
this.#controllers = signal(new Map(), /* @ts-ignore */
|
|
34
|
+
...(ngDevMode ? [{ debugName: "#controllers" }] : /* istanbul ignore next */ []));
|
|
35
|
+
/**
|
|
36
|
+
* @description Whether any sidebar's drawer is open. The layout paints one backdrop for all of
|
|
37
|
+
* them, and the region beside them steps out of the way for whichever one is showing.
|
|
38
|
+
*/
|
|
39
|
+
this.anyMobileOpen = computed(() => [...this.#controllers().values()].some(controller => controller.mobileOpen()), /* @ts-ignore */
|
|
40
|
+
...(ngDevMode ? [{ debugName: "anyMobileOpen" }] : /* istanbul ignore next */ []));
|
|
41
|
+
/** @description Whether the viewport is narrow enough for sidebars to present as overlay drawers. */
|
|
42
|
+
this.compact = this.#compact.asReadonly();
|
|
43
|
+
/**
|
|
44
|
+
* @description The first sidebar registered in the layout, in author order.
|
|
45
|
+
*
|
|
46
|
+
* What the parts that sit beside the sidebars rather than inside one fall back to: a trigger in a
|
|
47
|
+
* shared header with no `for` set, and the inset's surface. Best effort by design — with more than
|
|
48
|
+
* one sidebar in the layout there is no single right answer, and author order is the predictable
|
|
49
|
+
* one. Name a sidebar with `for` to be explicit.
|
|
50
|
+
*/
|
|
51
|
+
this.primaryController = computed(() => this.#controllers().values().next().value ?? null, /* @ts-ignore */
|
|
52
|
+
...(ngDevMode ? [{ debugName: "primaryController" }] : /* istanbul ignore next */ []));
|
|
53
|
+
/** @description The variant of the primary sidebar, which is the one the inset takes its surface from. */
|
|
54
|
+
this.primaryVariant = computed(() => this.primaryController()?.variant() ?? "sidebar", /* @ts-ignore */
|
|
55
|
+
...(ngDevMode ? [{ debugName: "primaryVariant" }] : /* istanbul ignore next */ []));
|
|
56
|
+
}
|
|
57
|
+
#compact;
|
|
58
|
+
#controllers;
|
|
59
|
+
/** Closes every open drawer. Backs the shared backdrop and the layout's Escape handling. */
|
|
60
|
+
closeAll() {
|
|
61
|
+
for (const controller of this.#controllers().values()) {
|
|
62
|
+
if (controller.mobileOpen()) {
|
|
63
|
+
controller.collapse();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** @description The sidebar registered under `id`, or `null` when no sidebar answers to it. */
|
|
68
|
+
getController(id) {
|
|
69
|
+
return this.#controllers().get(id) ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Closes any other open drawer, so only one is ever showing. Called by a sidebar as it opens its
|
|
73
|
+
* own: two drawers overlaying the same layout would stack their backdrops and fight over the focus
|
|
74
|
+
* trap, and on the viewport that puts them there, there is only room for one.
|
|
75
|
+
*/
|
|
76
|
+
notifyOpening(openingId) {
|
|
77
|
+
for (const [id, controller] of this.#controllers()) {
|
|
78
|
+
if (id !== openingId && controller.mobileOpen()) {
|
|
79
|
+
controller.collapse();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Adds a sidebar to the registry and returns the callback that removes it again. `mona-sidebar`
|
|
85
|
+
* re-runs this whenever its resolved id changes and calls the result on teardown.
|
|
86
|
+
*
|
|
87
|
+
* Two sidebars sharing an id collide here, and the later registration wins. That is a mistake in
|
|
88
|
+
* the markup rather than something to recover from — ids are already required to be unique in a
|
|
89
|
+
* document — so it is left to the duplicate `id` attributes to surface it.
|
|
90
|
+
*/
|
|
91
|
+
register(id, controller) {
|
|
92
|
+
this.#controllers.update(controllers => new Map(controllers).set(id, controller));
|
|
93
|
+
return () => {
|
|
94
|
+
this.#controllers.update(controllers => {
|
|
95
|
+
// Only if this registration is still the current one. An id that has since been taken
|
|
96
|
+
// over by another sidebar belongs to that sidebar now, and must survive this cleanup.
|
|
97
|
+
if (controllers.get(id) !== controller) {
|
|
98
|
+
return controllers;
|
|
99
|
+
}
|
|
100
|
+
const next = new Map(controllers);
|
|
101
|
+
next.delete(id);
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** @internal Published by `mona-sidebar-layout` from its breakpoint query. */
|
|
107
|
+
setCompact(compact) {
|
|
108
|
+
this.#compact.set(compact);
|
|
109
|
+
}
|
|
110
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
111
|
+
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.6", ngImport: i0, type: SidebarLayoutService, autoProvided: false }); }
|
|
112
|
+
}
|
|
113
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarLayoutService, decorators: [{
|
|
114
|
+
type: Service,
|
|
115
|
+
args: [{ autoProvided: false }]
|
|
116
|
+
}] });
|
|
117
|
+
|
|
118
|
+
function toLogicalSide(side) {
|
|
119
|
+
return side === "end" || side === "right" ? "end" : "start";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The default `SidebarStorage`: `localStorage`, under the sidebar's `persistKey`.
|
|
124
|
+
*
|
|
125
|
+
* Reads report `null` anywhere `localStorage` cannot answer, and writes are dropped there. That
|
|
126
|
+
* covers the server, where there is no such thing, and a browser that refuses it — Safari's private
|
|
127
|
+
* mode throws on access rather than returning empty, and a full quota throws on write. None of that
|
|
128
|
+
* is worth taking an application down over: the sidebar simply opens the way its input says.
|
|
129
|
+
*
|
|
130
|
+
* A server-rendered application gets the default state in its markup and the remembered one on
|
|
131
|
+
* hydration, which shows as a flash. Storage that the server can read too — a cookie, or state
|
|
132
|
+
* transferred from the request — avoids it; supply it through `provideSidebarStorage()`.
|
|
133
|
+
*/
|
|
134
|
+
class LocalStorageSidebarStorage {
|
|
135
|
+
#browser;
|
|
136
|
+
/** Takes the value of `PLATFORM_ID`, which is a string in practice though typed loosely. */
|
|
137
|
+
constructor(platformId) {
|
|
138
|
+
this.#browser = isPlatformBrowser(platformId);
|
|
139
|
+
}
|
|
140
|
+
read(key) {
|
|
141
|
+
if (!this.#browser) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const stored = localStorage.getItem(key);
|
|
146
|
+
return stored === null ? null : stored === "true";
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
write(key, expanded) {
|
|
153
|
+
if (!this.#browser) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
localStorage.setItem(key, String(expanded));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Nothing to recover: the sidebar still works, it just will not be remembered.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Where sidebars given a `persistKey` remember whether they were left open. Defaults to
|
|
167
|
+
* `localStorage`. Replace it with `provideSidebarStorage()` — with something the server can read too,
|
|
168
|
+
* for an application that renders there.
|
|
169
|
+
*/
|
|
170
|
+
const SIDEBAR_STORAGE = new InjectionToken("Mona UI sidebar storage", {
|
|
171
|
+
providedIn: "root",
|
|
172
|
+
factory: () => new LocalStorageSidebarStorage(inject(PLATFORM_ID))
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* State for one sidebar region. Provided by the `mona-sidebar` it belongs to and injectable by any
|
|
177
|
+
* descendant, so triggers and menu parts can read and drive that sidebar without input chains.
|
|
178
|
+
*
|
|
179
|
+
* One instance per sidebar, not per layout: a layout can hold a navigation column on one edge and an
|
|
180
|
+
* inspector on the other, and they collapse, open and sit on their own sides independently. What the
|
|
181
|
+
* two genuinely share — the breakpoint, and the rule that only one drawer is open at a time — lives on
|
|
182
|
+
* `SidebarLayoutService` instead.
|
|
183
|
+
*
|
|
184
|
+
* This class is internal. Consumers reach the same state through `injectSidebar()`, which hands back
|
|
185
|
+
* a `SidebarController`: the reads, and the commands that are safe from anywhere. The structural
|
|
186
|
+
* setters here exist so `mona-sidebar` can publish its own inputs, and calling them from elsewhere
|
|
187
|
+
* would leave the service and the component disagreeing until the next change detection pass.
|
|
188
|
+
*/
|
|
189
|
+
class SidebarService {
|
|
190
|
+
#collapsible;
|
|
191
|
+
/**
|
|
192
|
+
* Read from the layout rather than held here, because the breakpoint is one fact about the
|
|
193
|
+
* viewport that every sidebar in the layout answers to. A `mona-sidebar` standing on its own
|
|
194
|
+
* without a layout around it has nothing watching the viewport, and stays docked.
|
|
195
|
+
*/
|
|
196
|
+
#compact;
|
|
197
|
+
#expanded;
|
|
198
|
+
#generatedId;
|
|
199
|
+
#layoutService;
|
|
200
|
+
#persistKey;
|
|
201
|
+
#providedId;
|
|
202
|
+
#side;
|
|
203
|
+
#storage;
|
|
204
|
+
#variant;
|
|
205
|
+
/**
|
|
206
|
+
* Reset every time the viewport crosses the breakpoint. A drawer left open on a phone must not
|
|
207
|
+
* still count as open when the window is widened back to a desktop layout, where the same flag
|
|
208
|
+
* would be read as the docked sidebar's state.
|
|
209
|
+
*/
|
|
210
|
+
#mobileOpen;
|
|
211
|
+
constructor() {
|
|
212
|
+
this.#collapsible = signal("offcanvas", /* @ts-ignore */
|
|
213
|
+
...(ngDevMode ? [{ debugName: "#collapsible" }] : /* istanbul ignore next */ []));
|
|
214
|
+
/**
|
|
215
|
+
* Read from the layout rather than held here, because the breakpoint is one fact about the
|
|
216
|
+
* viewport that every sidebar in the layout answers to. A `mona-sidebar` standing on its own
|
|
217
|
+
* without a layout around it has nothing watching the viewport, and stays docked.
|
|
218
|
+
*/
|
|
219
|
+
this.#compact = computed(() => this.#layoutService?.compact() ?? false, /* @ts-ignore */
|
|
220
|
+
...(ngDevMode ? [{ debugName: "#compact" }] : /* istanbul ignore next */ []));
|
|
221
|
+
this.#expanded = signal(true, /* @ts-ignore */
|
|
222
|
+
...(ngDevMode ? [{ debugName: "#expanded" }] : /* istanbul ignore next */ []));
|
|
223
|
+
this.#generatedId = createElementControlId();
|
|
224
|
+
this.#layoutService = inject(SidebarLayoutService, { optional: true });
|
|
225
|
+
this.#persistKey = signal("", /* @ts-ignore */
|
|
226
|
+
...(ngDevMode ? [{ debugName: "#persistKey" }] : /* istanbul ignore next */ []));
|
|
227
|
+
this.#providedId = signal(null, /* @ts-ignore */
|
|
228
|
+
...(ngDevMode ? [{ debugName: "#providedId" }] : /* istanbul ignore next */ []));
|
|
229
|
+
this.#side = signal("start", /* @ts-ignore */
|
|
230
|
+
...(ngDevMode ? [{ debugName: "#side" }] : /* istanbul ignore next */ []));
|
|
231
|
+
this.#storage = inject(SIDEBAR_STORAGE);
|
|
232
|
+
this.#variant = signal("sidebar", /* @ts-ignore */
|
|
233
|
+
...(ngDevMode ? [{ debugName: "#variant" }] : /* istanbul ignore next */ []));
|
|
234
|
+
/**
|
|
235
|
+
* Reset every time the viewport crosses the breakpoint. A drawer left open on a phone must not
|
|
236
|
+
* still count as open when the window is widened back to a desktop layout, where the same flag
|
|
237
|
+
* would be read as the docked sidebar's state.
|
|
238
|
+
*/
|
|
239
|
+
this.#mobileOpen = linkedSignal({ ...(ngDevMode ? { debugName: "#mobileOpen" } : /* istanbul ignore next */ {}), source: () => this.#compact(),
|
|
240
|
+
computation: () => false });
|
|
241
|
+
/** @description How the sidebar behaves when collapsed. Set by the `mona-sidebar` it belongs to. */
|
|
242
|
+
this.collapsible = this.#collapsible.asReadonly();
|
|
243
|
+
/** @description Whether the sidebar presents as an overlay drawer rather than as a column. */
|
|
244
|
+
this.compact = this.#compact;
|
|
245
|
+
/**
|
|
246
|
+
* @description Whether the sidebar is open. While compact this reports the drawer, so one binding
|
|
247
|
+
* drives both presentations. Always `true` while `collapsible` is `"none"` on a wide viewport.
|
|
248
|
+
*/
|
|
249
|
+
this.expanded = computed(() => {
|
|
250
|
+
if (this.#compact()) {
|
|
251
|
+
return this.#mobileOpen();
|
|
252
|
+
}
|
|
253
|
+
return this.#collapsible() === "none" || this.#expanded();
|
|
254
|
+
}, /* @ts-ignore */
|
|
255
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
256
|
+
/**
|
|
257
|
+
* @description Whether the sidebar is currently showing its icon rail, meaning parts that only make
|
|
258
|
+
* sense at full width — labels, trailing actions, submenus — should step aside. Never true while
|
|
259
|
+
* compact: a drawer has the room to present everything at full width.
|
|
260
|
+
*/
|
|
261
|
+
this.iconOnly = computed(() => !this.#compact() && this.#collapsible() === "icon" && !this.expanded(), /* @ts-ignore */
|
|
262
|
+
...(ngDevMode ? [{ debugName: "iconOnly" }] : /* istanbul ignore next */ []));
|
|
263
|
+
/** @description Whether the overlay drawer is open. Only meaningful while `compact` is true. */
|
|
264
|
+
this.mobileOpen = computed(() => this.#compact() && this.#mobileOpen(), /* @ts-ignore */
|
|
265
|
+
...(ngDevMode ? [{ debugName: "mobileOpen" }] : /* istanbul ignore next */ []));
|
|
266
|
+
/** @description Which edge the sidebar sits on, resolved to a direction-relative side. */
|
|
267
|
+
this.side = computed(() => toLogicalSide(this.#side()), /* @ts-ignore */
|
|
268
|
+
...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
|
|
269
|
+
/**
|
|
270
|
+
* @description Id assigned to the sidebar element and referenced by triggers through `aria-controls`.
|
|
271
|
+
* Falls back to a generated id when the consumer supplies none.
|
|
272
|
+
*/
|
|
273
|
+
this.sidebarId = computed(() => this.#providedId() ?? this.#generatedId, /* @ts-ignore */
|
|
274
|
+
...(ngDevMode ? [{ debugName: "sidebarId" }] : /* istanbul ignore next */ []));
|
|
275
|
+
/** @description The expanded state as a string, for `data-state` bindings and styling hooks. */
|
|
276
|
+
this.state = computed(() => (this.expanded() ? "expanded" : "collapsed"), /* @ts-ignore */
|
|
277
|
+
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
278
|
+
/** @description The visual treatment of the sidebar. Set by the `mona-sidebar` it belongs to. */
|
|
279
|
+
this.variant = this.#variant.asReadonly();
|
|
280
|
+
/** @description The subset of this service that is safe to hand to a consumer. */
|
|
281
|
+
this.controller = {
|
|
282
|
+
collapse: () => this.collapse(),
|
|
283
|
+
collapsible: this.collapsible,
|
|
284
|
+
compact: this.compact,
|
|
285
|
+
expand: () => this.expand(),
|
|
286
|
+
expanded: this.expanded,
|
|
287
|
+
iconOnly: this.iconOnly,
|
|
288
|
+
mobileOpen: this.mobileOpen,
|
|
289
|
+
side: this.side,
|
|
290
|
+
sidebarId: this.sidebarId,
|
|
291
|
+
state: this.state,
|
|
292
|
+
toggle: () => this.toggle(),
|
|
293
|
+
variant: this.variant
|
|
294
|
+
};
|
|
295
|
+
// Restore before persist, so the first pass cannot write the default back over what was
|
|
296
|
+
// stored. Both sit idle until `mona-sidebar` publishes a key, which it only does when the
|
|
297
|
+
// consumer asked for one.
|
|
298
|
+
effect(() => {
|
|
299
|
+
const key = this.#persistKey();
|
|
300
|
+
if (!key) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
untracked(() => {
|
|
304
|
+
const stored = this.#storage.read(key);
|
|
305
|
+
if (stored !== null) {
|
|
306
|
+
this.#expanded.set(stored);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
/*
|
|
311
|
+
* The docked signal, not `expanded`. That one reports the drawer while compact, and a drawer
|
|
312
|
+
* is not what is being remembered here: opening one on a phone would otherwise overwrite the
|
|
313
|
+
* state the desktop is meant to come back to.
|
|
314
|
+
*/
|
|
315
|
+
effect(() => {
|
|
316
|
+
const key = this.#persistKey();
|
|
317
|
+
const expanded = this.#expanded();
|
|
318
|
+
if (!key) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
untracked(() => this.#storage.write(key, expanded));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
collapse() {
|
|
325
|
+
this.setExpanded(false);
|
|
326
|
+
}
|
|
327
|
+
expand() {
|
|
328
|
+
this.setExpanded(true);
|
|
329
|
+
}
|
|
330
|
+
/** @internal Published by `mona-sidebar` from its own input. */
|
|
331
|
+
setCollapsible(collapsible) {
|
|
332
|
+
this.#collapsible.set(collapsible);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Routed to whichever presentation is current, so a trigger, a rail and a `[(expanded)]` binding
|
|
336
|
+
* all mean the same thing on a phone as they do on a desktop.
|
|
337
|
+
*/
|
|
338
|
+
setExpanded(expanded) {
|
|
339
|
+
if (this.#compact()) {
|
|
340
|
+
// Announced before opening, so the layout can close any other drawer first. Closing takes
|
|
341
|
+
// the `expanded === false` path on that sidebar and never comes back through here.
|
|
342
|
+
if (expanded) {
|
|
343
|
+
this.#layoutService?.notifyOpening(this.sidebarId());
|
|
344
|
+
}
|
|
345
|
+
this.#mobileOpen.set(expanded);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (this.#collapsible() === "none") {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.#expanded.set(expanded);
|
|
352
|
+
}
|
|
353
|
+
/** @internal Published by `mona-sidebar` from its own input. */
|
|
354
|
+
setId(id) {
|
|
355
|
+
this.#providedId.set(id || null);
|
|
356
|
+
}
|
|
357
|
+
/** @internal Published by `mona-sidebar` from its own input. */
|
|
358
|
+
setPersistKey(persistKey) {
|
|
359
|
+
this.#persistKey.set(persistKey);
|
|
360
|
+
}
|
|
361
|
+
/** @internal Published by `mona-sidebar` from its own input. */
|
|
362
|
+
setSide(side) {
|
|
363
|
+
this.#side.set(side);
|
|
364
|
+
}
|
|
365
|
+
/** @internal Published by `mona-sidebar` from its own input. */
|
|
366
|
+
setVariant(variant) {
|
|
367
|
+
this.#variant.set(variant);
|
|
368
|
+
}
|
|
369
|
+
toggle() {
|
|
370
|
+
this.setExpanded(!this.expanded());
|
|
371
|
+
}
|
|
372
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
373
|
+
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.6", ngImport: i0, type: SidebarService, autoProvided: false }); }
|
|
374
|
+
}
|
|
375
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarService, decorators: [{
|
|
376
|
+
type: Service,
|
|
377
|
+
args: [{ autoProvided: false }]
|
|
378
|
+
}], ctorParameters: () => [] });
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* How much of the sidebar's inline axis each variant's own border takes up.
|
|
382
|
+
*
|
|
383
|
+
* Borders are painted inside the box under the global `border-box`, so they come out of the width the
|
|
384
|
+
* parts inside are measured against: an icon rail of `3rem` less a region's `p-2` is meant to leave
|
|
385
|
+
* exactly the `2rem` square a menu button becomes, and a border silently makes it 1px or 2px short —
|
|
386
|
+
* enough to clip the edge off a footer avatar sized to that square. `SidebarComponent` adds this back
|
|
387
|
+
* on, so the authored width is the width the contents actually get.
|
|
388
|
+
*
|
|
389
|
+
* It lives here, beside the variants that draw those borders, so the two cannot drift apart. It is a
|
|
390
|
+
* plain length rather than a custom property because it lands in a transitioned `width`, and a
|
|
391
|
+
* `var()` there resolves to a pending-substitution value that engines are not obliged to interpolate.
|
|
392
|
+
*/
|
|
393
|
+
const sidebarBorderAllowance = Object.freeze({
|
|
394
|
+
// A single edge border, whichever side it sits on.
|
|
395
|
+
sidebar: "1px",
|
|
396
|
+
// Bordered on all four sides, so the inline axis loses two of them.
|
|
397
|
+
floating: "2px",
|
|
398
|
+
// Draws no border of its own; the raised surface moves to the region beside it.
|
|
399
|
+
inset: "0px"
|
|
400
|
+
});
|
|
401
|
+
/*
|
|
402
|
+
* Every part that changes shape when the sidebar collapses runs on `--mona-motion-standard` with the
|
|
403
|
+
* same easing, so the whole region moves as one piece instead of each part arriving on its own
|
|
404
|
+
* schedule. The utilities are written out in full at each use site rather than shared through a
|
|
405
|
+
* constant, because Tailwind extracts class names from source text and cannot follow an identifier.
|
|
406
|
+
*/
|
|
407
|
+
/**
|
|
408
|
+
* `relative` is what scopes the drawer and its backdrop to this layout. They are positioned against
|
|
409
|
+
* it rather than the viewport, so a sidebar embedded in a bounded region — a documentation example, a
|
|
410
|
+
* split pane — overlays only that region, and a layout that fills the page behaves identically.
|
|
411
|
+
*/
|
|
412
|
+
const sidebarLayoutBaseThemeVariants = cva(`
|
|
413
|
+
relative flex flex-row w-full h-screen h-dvh
|
|
414
|
+
overflow-hidden
|
|
415
|
+
`);
|
|
416
|
+
const sidebarContentThemeVariants = cva("flex-1", {
|
|
417
|
+
variants: {
|
|
418
|
+
iconOnly: {
|
|
419
|
+
true: "overflow-hidden",
|
|
420
|
+
false: "overflow-x-hidden overflow-y-auto"
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
const sidebarFooterThemeVariants = cva("p-2 shrink-0");
|
|
425
|
+
const sidebarGroupThemeVariants = cva(`
|
|
426
|
+
relative flex flex-col w-full p-2
|
|
427
|
+
`);
|
|
428
|
+
const sidebarGroupContentThemeVariants = cva("w-full");
|
|
429
|
+
/**
|
|
430
|
+
* This stays a static host class because consumers commonly compose `monaButton` on the same
|
|
431
|
+
* element. The button owns `[class]`, so the sidebar publishes its state through `data-hidden`
|
|
432
|
+
* instead of adding a competing class binding.
|
|
433
|
+
*/
|
|
434
|
+
const sidebarGroupActionClasses = `
|
|
435
|
+
p-1 w-auto h-auto
|
|
436
|
+
visible opacity-100
|
|
437
|
+
transition-[opacity,visibility] duration-(--mona-motion-standard) ease-out
|
|
438
|
+
motion-reduce:transition-none
|
|
439
|
+
data-[hidden=true]:invisible data-[hidden=true]:opacity-0
|
|
440
|
+
`;
|
|
441
|
+
const sidebarInsetThemeVariants = cva(`
|
|
442
|
+
flex flex-col grow flex-1 min-w-0
|
|
443
|
+
overflow-auto
|
|
444
|
+
`, {
|
|
445
|
+
variants: {
|
|
446
|
+
variant: {
|
|
447
|
+
sidebar: "h-full",
|
|
448
|
+
floating: "h-full",
|
|
449
|
+
// The `inset` variant moves the raised surface from the sidebar onto this region.
|
|
450
|
+
inset: `
|
|
451
|
+
m-2 h-[calc(100%-1rem)] rounded-lg
|
|
452
|
+
border border-(--color-border) bg-(--color-surface) shadow-(--shadow-raised)
|
|
453
|
+
`
|
|
454
|
+
},
|
|
455
|
+
behindDrawer: {
|
|
456
|
+
true: "overflow-hidden!",
|
|
457
|
+
false: ""
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
const sidebarGroupHeaderThemeVariants = cva(`
|
|
462
|
+
flex items-center px-2 overflow-hidden
|
|
463
|
+
transition-[height] duration-(--mona-motion-standard) ease-out
|
|
464
|
+
motion-reduce:transition-none
|
|
465
|
+
`, {
|
|
466
|
+
variants: {
|
|
467
|
+
// On the rail the label and any action have stood down, so the row closes rather than
|
|
468
|
+
// reserving empty space. Closing it by height keeps it in step with the sidebar's width;
|
|
469
|
+
// the inset stays put so nothing inside it slides while it does.
|
|
470
|
+
iconOnly: {
|
|
471
|
+
true: "h-0",
|
|
472
|
+
false: "h-8"
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
const sidebarGroupLabelThemeVariants = cva(`
|
|
477
|
+
flex grow-1 items-center shrink-0
|
|
478
|
+
text-xs font-medium whitespace-nowrap text-(--color-sidebar-foreground)/70
|
|
479
|
+
transition-[opacity,visibility] duration-(--mona-motion-standard) ease-out
|
|
480
|
+
motion-reduce:transition-none
|
|
481
|
+
`, {
|
|
482
|
+
variants: {
|
|
483
|
+
hidden: {
|
|
484
|
+
true: "invisible opacity-0",
|
|
485
|
+
false: "visible opacity-100"
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
const sidebarHeaderThemeVariants = cva("p-2 shrink-0");
|
|
490
|
+
/**
|
|
491
|
+
* `monaTextBox` owns `[class]`, so the rail state is exposed through `data-hidden` and consumed by
|
|
492
|
+
* this static class rather than by a second class binding.
|
|
493
|
+
*/
|
|
494
|
+
const sidebarInputClasses = cva(`
|
|
495
|
+
w-full h-8
|
|
496
|
+
data-[hidden=true]:hidden
|
|
497
|
+
`);
|
|
498
|
+
const sidebarMenuActionClasses = cva(`
|
|
499
|
+
w-auto h-auto p-1!
|
|
500
|
+
hover:bg-(--color-sidebar-accent)! hover:text-(--color-sidebar-accent-foreground)!
|
|
501
|
+
group-data-[active=true]/menu-item:text-(--color-sidebar-primary-foreground)!
|
|
502
|
+
data-[hidden=true]:hidden
|
|
503
|
+
`);
|
|
504
|
+
const sidebarMenuSkeletonThemeVariants = cva(`
|
|
505
|
+
flex h-8 w-full items-center overflow-hidden
|
|
506
|
+
transition-[gap,padding] duration-(--mona-motion-standard) ease-out
|
|
507
|
+
motion-reduce:transition-none
|
|
508
|
+
`, {
|
|
509
|
+
variants: {
|
|
510
|
+
iconOnly: {
|
|
511
|
+
true: "gap-8 px-2",
|
|
512
|
+
false: "gap-2 px-1"
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
const sidebarMenuItemThemeVariants = cva(`
|
|
517
|
+
flex text-base gap-1
|
|
518
|
+
group/menu-item
|
|
519
|
+
data-[active=true]:bg-(--color-sidebar-primary)
|
|
520
|
+
data-[active=true]:text-(--color-sidebar-primary-foreground)
|
|
521
|
+
data-[active=true]:hover:bg-(--color-sidebar-primary)
|
|
522
|
+
data-[active=true]:hover:text-(--color-sidebar-primary-foreground)
|
|
523
|
+
`, {
|
|
524
|
+
variants: {
|
|
525
|
+
collapsible: {
|
|
526
|
+
true: "flex-col items-stretch",
|
|
527
|
+
false: "flex-row items-center rounded-md hover:bg-(--color-sidebar-accent)"
|
|
528
|
+
},
|
|
529
|
+
// The item takes its width from the menu, which takes it from the sidebar, so it narrows
|
|
530
|
+
// with the animation instead of snapping to a rail-sized box of its own.
|
|
531
|
+
iconOnly: {
|
|
532
|
+
true: "",
|
|
533
|
+
false: ""
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
compoundVariants: [
|
|
537
|
+
// On the rail a collapsible item has nothing left to stack, so it behaves like any other row.
|
|
538
|
+
{
|
|
539
|
+
collapsible: true,
|
|
540
|
+
iconOnly: true,
|
|
541
|
+
class: "flex-row items-center rounded-md hover:bg-(--color-sidebar-accent)"
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
});
|
|
545
|
+
/**
|
|
546
|
+
* The menu row's own appearance.
|
|
547
|
+
*
|
|
548
|
+
* It used to compose `ButtonDirective`, which owns a `[class]` binding of its own; every sidebar rule
|
|
549
|
+
* that had to beat it needed `!important`, and the composition ruled anchors out entirely. Owning the
|
|
550
|
+
* class here costs a handful of declarations and removes both problems.
|
|
551
|
+
*
|
|
552
|
+
* `shrink-0` on the leading visual is what stops a long label crushing it. It applies to the first
|
|
553
|
+
* child whatever it is — an icon, an avatar — and to any icon elsewhere in the row.
|
|
554
|
+
*/
|
|
555
|
+
const sidebarMenuButtonThemeVariants = cva(`
|
|
556
|
+
flex w-full items-center justify-start overflow-hidden
|
|
557
|
+
rounded-md border-0 bg-transparent
|
|
558
|
+
text-start text-sm font-normal whitespace-nowrap no-underline
|
|
559
|
+
outline-none
|
|
560
|
+
focus-visible:ring-2 focus-visible:ring-(--color-sidebar-ring)/60
|
|
561
|
+
[&>*:first-child]:shrink-0 [&>svg]:shrink-0
|
|
562
|
+
[transition:gap_var(--mona-motion-standard)_ease-out,height_var(--mona-motion-standard)_ease-out,padding_var(--mona-motion-standard)_ease-out,background-color_100ms_ease-in-out,color_100ms_ease-in-out]
|
|
563
|
+
motion-reduce:[transition:none]
|
|
564
|
+
`, {
|
|
565
|
+
variants: {
|
|
566
|
+
active: {
|
|
567
|
+
true: "text-(--color-sidebar-primary-foreground)",
|
|
568
|
+
false: `
|
|
569
|
+
text-(--color-sidebar-foreground)
|
|
570
|
+
hover:bg-(--color-sidebar-accent) hover:text-(--color-sidebar-accent-foreground)
|
|
571
|
+
`
|
|
572
|
+
},
|
|
573
|
+
disabled: {
|
|
574
|
+
true: "opacity-50 pointer-events-none cursor-default",
|
|
575
|
+
false: "cursor-pointer"
|
|
576
|
+
},
|
|
577
|
+
iconOnly: {
|
|
578
|
+
true: "shrink-0 gap-8 h-8",
|
|
579
|
+
false: "gap-2 p-1"
|
|
580
|
+
},
|
|
581
|
+
size: {
|
|
582
|
+
medium: "",
|
|
583
|
+
large: ""
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
compoundVariants: [
|
|
587
|
+
{ iconOnly: false, size: "medium", class: "h-8" },
|
|
588
|
+
{ iconOnly: false, size: "large", class: "h-12" },
|
|
589
|
+
{ iconOnly: true, size: "medium", class: "p-2" },
|
|
590
|
+
{ iconOnly: true, size: "large", class: "p-0" }
|
|
591
|
+
]
|
|
592
|
+
});
|
|
593
|
+
const sidebarMenuSubThemeVariants = cva(`
|
|
594
|
+
flex flex-col w-full space-y-1
|
|
595
|
+
ms-3.5 ps-2.5
|
|
596
|
+
border-s border-(--color-sidebar-border)
|
|
597
|
+
`);
|
|
598
|
+
/**
|
|
599
|
+
* The menu carries no inset of its own. Its enclosing region — a group, header or footer — owns the
|
|
600
|
+
* one inset, and holds it steady in both states, which is what makes the icon rail's arithmetic work
|
|
601
|
+
* out: a `3rem` rail less that region's `p-2` on each side leaves exactly one `2rem` icon square.
|
|
602
|
+
*
|
|
603
|
+
* It also means a menu placed in a region that is already inset needs no override. An override was
|
|
604
|
+
* the only reason a consumer ever reached for `!important` here, because `px-*` and the logical
|
|
605
|
+
* `ps-*`/`pe-*` an inset would have used are separate groups to `tailwind-merge`: it keeps both, and
|
|
606
|
+
* which one lands is left to stylesheet order.
|
|
607
|
+
*/
|
|
608
|
+
const sidebarMenuThemeVariants = cva(`
|
|
609
|
+
flex flex-col w-full space-y-1
|
|
610
|
+
`);
|
|
611
|
+
const sidebarThemeVariants = cva(`
|
|
612
|
+
relative flex flex-col shrink-0
|
|
613
|
+
bg-(--color-sidebar) text-(--color-sidebar-foreground) overflow-hidden
|
|
614
|
+
transition-[width] duration-(--mona-motion-standard) ease-out
|
|
615
|
+
motion-reduce:transition-none
|
|
616
|
+
`, {
|
|
617
|
+
variants: {
|
|
618
|
+
// `side` only decides placement; the border it implies is applied per variant below,
|
|
619
|
+
// so the `floating` and `inset` treatments are not left fighting a single edge border.
|
|
620
|
+
// Flex order is already direction-relative, which is why the borders below have to be
|
|
621
|
+
// logical too — a physical `border-r` would land on the wrong edge under RTL.
|
|
622
|
+
side: {
|
|
623
|
+
start: "order-first",
|
|
624
|
+
end: "order-last"
|
|
625
|
+
},
|
|
626
|
+
variant: {
|
|
627
|
+
sidebar: `h-full ${themeRaisedBackdropClasses}`,
|
|
628
|
+
// Any change to the borders below must be matched in `sidebarBorderAllowance` above.
|
|
629
|
+
floating: `
|
|
630
|
+
m-2 h-[calc(100%-1rem)] rounded-lg
|
|
631
|
+
border border-(--color-sidebar-border) shadow-(--shadow-raised)
|
|
632
|
+
${themeRaisedBackdropClasses}
|
|
633
|
+
`,
|
|
634
|
+
inset: "m-2 h-[calc(100%-1rem)] bg-transparent"
|
|
635
|
+
},
|
|
636
|
+
// Once an offcanvas sidebar is fully collapsed any margin would leave a visible gutter, and
|
|
637
|
+
// the border would leave a 1px sliver of surface down the edge of a panel that is supposed
|
|
638
|
+
// to be gone entirely.
|
|
639
|
+
flush: {
|
|
640
|
+
true: "m-0 border-0",
|
|
641
|
+
false: ""
|
|
642
|
+
},
|
|
643
|
+
/**
|
|
644
|
+
* The compact presentation. The panel leaves the flex flow and becomes an overlay pinned to
|
|
645
|
+
* one edge, sliding in on `translate` rather than `width` — a drawer that animated its width
|
|
646
|
+
* would reflow its own contents for the whole transition.
|
|
647
|
+
*/
|
|
648
|
+
drawer: {
|
|
649
|
+
true: `
|
|
650
|
+
absolute inset-y-0 z-50 h-full m-0
|
|
651
|
+
shadow-(--shadow-overlay)
|
|
652
|
+
transition-[translate] duration-(--mona-motion-standard) ease-out
|
|
653
|
+
motion-reduce:transition-none
|
|
654
|
+
`,
|
|
655
|
+
false: ""
|
|
656
|
+
},
|
|
657
|
+
open: {
|
|
658
|
+
true: "",
|
|
659
|
+
false: ""
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
compoundVariants: [
|
|
663
|
+
{ drawer: false, variant: "sidebar", side: "start", class: "border-e border-(--color-sidebar-border)" },
|
|
664
|
+
{ drawer: false, variant: "sidebar", side: "end", class: "border-s border-(--color-sidebar-border)" },
|
|
665
|
+
{ drawer: false, variant: "floating", flush: true, class: "h-full" },
|
|
666
|
+
{ drawer: false, variant: "inset", flush: true, class: "h-full" },
|
|
667
|
+
// A drawer is a surface in its own right whatever the docked variant looks like, so the
|
|
668
|
+
// variant's margins, rounding and transparency are all dropped for it.
|
|
669
|
+
{ drawer: true, side: "start", class: "start-0" },
|
|
670
|
+
{ drawer: true, side: "end", class: "end-0" },
|
|
671
|
+
{ drawer: true, open: false, side: "start", class: "-translate-x-full" },
|
|
672
|
+
{ drawer: true, open: false, side: "end", class: "translate-x-full" },
|
|
673
|
+
{ drawer: true, variant: "floating", class: "rounded-none" },
|
|
674
|
+
{ drawer: true, variant: "inset", class: "bg-(--color-sidebar)" }
|
|
675
|
+
]
|
|
676
|
+
});
|
|
677
|
+
/**
|
|
678
|
+
* The scrim behind an open drawer. Fades rather than sliding, so it is not competing with the panel's
|
|
679
|
+
* own movement, and stays below it in the stacking order.
|
|
680
|
+
*/
|
|
681
|
+
const sidebarBackdropThemeVariants = cva(`
|
|
682
|
+
absolute inset-0 z-40
|
|
683
|
+
bg-black/50
|
|
684
|
+
transition-opacity duration-(--mona-motion-standard) ease-out
|
|
685
|
+
motion-reduce:transition-none
|
|
686
|
+
`, {
|
|
687
|
+
variants: {
|
|
688
|
+
open: {
|
|
689
|
+
true: "opacity-100",
|
|
690
|
+
false: "opacity-0 pointer-events-none"
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
const sidebarMenuBadgeThemeVariants = cva(`
|
|
695
|
+
ms-auto flex items-center justify-center shrink-0
|
|
696
|
+
min-w-5 h-5 px-1.5
|
|
697
|
+
rounded-md text-xs font-medium tabular-nums
|
|
698
|
+
bg-(--color-sidebar-accent) text-(--color-sidebar-accent-foreground)
|
|
699
|
+
pointer-events-none select-none
|
|
700
|
+
`, {
|
|
701
|
+
variants: {
|
|
702
|
+
hidden: {
|
|
703
|
+
true: "hidden!",
|
|
704
|
+
false: ""
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
const sidebarRailThemeVariants = cva(`
|
|
709
|
+
absolute inset-y-0 z-20 hidden w-1
|
|
710
|
+
cursor-pointer
|
|
711
|
+
transition-colors hover:bg-(--color-sidebar-border)
|
|
712
|
+
focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-(--color-sidebar-ring)
|
|
713
|
+
sm:flex
|
|
714
|
+
`, {
|
|
715
|
+
variants: {
|
|
716
|
+
// The rail sits on the sidebar's inner edge, which is the opposite one to the side it is
|
|
717
|
+
// docked against. Logical, so it stays on the inner edge under RTL.
|
|
718
|
+
side: {
|
|
719
|
+
start: "end-0",
|
|
720
|
+
end: "start-0"
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
const sidebarSeparatorThemeVariants = cva(`
|
|
725
|
+
shrink-0 h-px bg-(--color-sidebar-border)
|
|
726
|
+
transition-[margin] duration-(--mona-motion-standard) ease-out
|
|
727
|
+
motion-reduce:transition-none
|
|
728
|
+
`, {
|
|
729
|
+
variants: {
|
|
730
|
+
iconOnly: {
|
|
731
|
+
true: "mx-1",
|
|
732
|
+
false: "mx-2"
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* @description
|
|
739
|
+
* The sidebar region itself. Content is projected in author order, so header, content and footer
|
|
740
|
+
* parts appear exactly where they are written.
|
|
741
|
+
*
|
|
742
|
+
* Below the layout's `mobileBreakpoint` it presents as a modal overlay drawer: focus is trapped
|
|
743
|
+
* inside it while open, restored to whatever opened it on close, and the rest of the layout is made
|
|
744
|
+
* inert. It is not a navigation landmark by itself — wrap the menus in a labelled `nav`, or set
|
|
745
|
+
* `role` and `ariaLabel` if the whole region is navigation.
|
|
746
|
+
*/
|
|
747
|
+
class SidebarComponent {
|
|
748
|
+
#focusTrap;
|
|
749
|
+
#host;
|
|
750
|
+
#injector;
|
|
751
|
+
#layoutService;
|
|
752
|
+
#sidebarService;
|
|
753
|
+
#restoreFocusTo;
|
|
754
|
+
constructor() {
|
|
755
|
+
this.#focusTrap = inject(CdkTrapFocus);
|
|
756
|
+
this.#host = inject(ElementRef);
|
|
757
|
+
this.#injector = inject(Injector);
|
|
758
|
+
this.#layoutService = inject(SidebarLayoutService, { optional: true });
|
|
759
|
+
this.#sidebarService = inject(SidebarService);
|
|
760
|
+
this.#restoreFocusTo = null;
|
|
761
|
+
this.baseClass = computed(() => {
|
|
762
|
+
const variantClass = sidebarThemeVariants({
|
|
763
|
+
drawer: this.drawer(),
|
|
764
|
+
open: this.#sidebarService.mobileOpen(),
|
|
765
|
+
side: this.#sidebarService.side(),
|
|
766
|
+
variant: this.variant(),
|
|
767
|
+
flush: this.offCanvasClosed()
|
|
768
|
+
});
|
|
769
|
+
return twMerge(variantClass, this.userClass());
|
|
770
|
+
}, /* @ts-ignore */
|
|
771
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
772
|
+
/** The compact presentation: an overlay pinned to one edge rather than a column in the flow. */
|
|
773
|
+
this.drawer = this.#sidebarService.compact;
|
|
774
|
+
/** Published rather than the raw input, so a `left`/`right` alias still reports as `start`/`end`. */
|
|
775
|
+
this.logicalSide = this.#sidebarService.side;
|
|
776
|
+
/**
|
|
777
|
+
* Whether the panel is out of view entirely. The icon rail does not count — it is still on screen
|
|
778
|
+
* and still operable — but a closed drawer and a fully collapsed off-canvas sidebar both do.
|
|
779
|
+
*/
|
|
780
|
+
this.hidden = computed(() => {
|
|
781
|
+
if (this.drawer()) {
|
|
782
|
+
return !this.#sidebarService.mobileOpen();
|
|
783
|
+
}
|
|
784
|
+
return this.offCanvasClosed();
|
|
785
|
+
}, /* @ts-ignore */
|
|
786
|
+
...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
787
|
+
this.offCanvasClosed = computed(() => !this.drawer() && !this.#sidebarService.expanded() && this.collapsible() === "offcanvas", /* @ts-ignore */
|
|
788
|
+
...(ngDevMode ? [{ debugName: "offCanvasClosed" }] : /* istanbul ignore next */ []));
|
|
789
|
+
/** A drawer is a modal surface; a docked sidebar keeps whatever role the consumer gave it. */
|
|
790
|
+
this.resolvedRole = computed(() => (this.drawer() ? "dialog" : this.role() || null), /* @ts-ignore */
|
|
791
|
+
...(ngDevMode ? [{ debugName: "resolvedRole" }] : /* istanbul ignore next */ []));
|
|
792
|
+
this.sidebarId = this.#sidebarService.sidebarId;
|
|
793
|
+
this.state = this.#sidebarService.state;
|
|
794
|
+
/**
|
|
795
|
+
* `width` and `iconWidth` are the width the sidebar's *contents* get, because that is what the
|
|
796
|
+
* parts inside are measured against — an icon rail less a region's padding is meant to leave
|
|
797
|
+
* exactly one icon square. The variant's border is painted inside the box under the global
|
|
798
|
+
* `border-box`, so it is added back on here rather than being taken out of that measurement.
|
|
799
|
+
*/
|
|
800
|
+
this.widthString = computed(() => {
|
|
801
|
+
// A drawer is always at full width; it is moved out of view rather than shrunk.
|
|
802
|
+
if (this.drawer()) {
|
|
803
|
+
return this.#withBorderAllowance(toCssLength(this.mobileWidth()));
|
|
804
|
+
}
|
|
805
|
+
if (this.#sidebarService.expanded()) {
|
|
806
|
+
return this.#withBorderAllowance(toCssLength(this.width()));
|
|
807
|
+
}
|
|
808
|
+
// A fully collapsed sidebar is the exception: it has no contents to make room for.
|
|
809
|
+
return this.collapsible() === "icon" ? this.#withBorderAllowance(toCssLength(this.iconWidth())) : "0px";
|
|
810
|
+
}, /* @ts-ignore */
|
|
811
|
+
...(ngDevMode ? [{ debugName: "widthString" }] : /* istanbul ignore next */ []));
|
|
812
|
+
/**
|
|
813
|
+
* @description Accessible name for the sidebar region. Required when `role` is set, and used as the
|
|
814
|
+
* drawer's name on compact viewports.
|
|
815
|
+
* @default ""
|
|
816
|
+
*/
|
|
817
|
+
this.ariaLabel = input("", { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: "aria-label" });
|
|
818
|
+
/**
|
|
819
|
+
* @description How the sidebar behaves when collapsed. `"icon"` keeps a narrow rail of icons visible,
|
|
820
|
+
* `"offcanvas"` removes it from the layout entirely, and `"none"` disables collapsing altogether.
|
|
821
|
+
* @default "offcanvas"
|
|
822
|
+
*/
|
|
823
|
+
this.collapsible = input("offcanvas", /* @ts-ignore */
|
|
824
|
+
...(ngDevMode ? [{ debugName: "collapsible" }] : /* istanbul ignore next */ []));
|
|
825
|
+
/**
|
|
826
|
+
* @description Sets whether this sidebar is open. Supports two-way binding. While the viewport is
|
|
827
|
+
* compact this reflects the drawer, so one binding drives both presentations.
|
|
828
|
+
* @default true
|
|
829
|
+
*/
|
|
830
|
+
this.expanded = model(true, /* @ts-ignore */
|
|
831
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
832
|
+
/**
|
|
833
|
+
* @description Width of the icon rail while collapsed. Only applies when `collapsible` is `"icon"`.
|
|
834
|
+
* Numbers are treated as pixels.
|
|
835
|
+
* @default "3rem"
|
|
836
|
+
*/
|
|
837
|
+
this.iconWidth = input("3rem", /* @ts-ignore */
|
|
838
|
+
...(ngDevMode ? [{ debugName: "iconWidth" }] : /* istanbul ignore next */ []));
|
|
839
|
+
/**
|
|
840
|
+
* @description Id for the sidebar element, referenced by triggers through `aria-controls`. Supply a
|
|
841
|
+
* stable value when the markup is server rendered, so the client does not hydrate onto a different
|
|
842
|
+
* generated id. Falls back to a generated id.
|
|
843
|
+
* @default ""
|
|
844
|
+
*/
|
|
845
|
+
this.id = input("", /* @ts-ignore */
|
|
846
|
+
...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
|
|
847
|
+
/**
|
|
848
|
+
* @description Width of the overlay drawer on compact viewports. Numbers are treated as pixels.
|
|
849
|
+
* @default "18rem"
|
|
850
|
+
*/
|
|
851
|
+
this.mobileWidth = input("18rem", /* @ts-ignore */
|
|
852
|
+
...(ngDevMode ? [{ debugName: "mobileWidth" }] : /* istanbul ignore next */ []));
|
|
853
|
+
/**
|
|
854
|
+
* @description Remembers whether this sidebar was left open, under this key. Unset means nothing is
|
|
855
|
+
* stored — no key, no storage written.
|
|
856
|
+
*
|
|
857
|
+
* Only the docked state is kept. A drawer is never restored open: on the viewport that makes one,
|
|
858
|
+
* it would cover the page on load. A stored value wins over `expanded`, and is pushed back out
|
|
859
|
+
* through that binding, so do not set a key on a sidebar whose state is already restored elsewhere.
|
|
860
|
+
*
|
|
861
|
+
* Where it is kept is `SIDEBAR_STORAGE`, which defaults to `localStorage`. A server-rendered
|
|
862
|
+
* application cannot read that until hydration and so flashes the default first; supply storage the
|
|
863
|
+
* server can read through `provideSidebarStorage()`.
|
|
864
|
+
* @default ""
|
|
865
|
+
*/
|
|
866
|
+
this.persistKey = input("", /* @ts-ignore */
|
|
867
|
+
...(ngDevMode ? [{ debugName: "persistKey" }] : /* istanbul ignore next */ []));
|
|
868
|
+
/**
|
|
869
|
+
* @description ARIA role for the sidebar region. Leave unset and wrap the menus in a labelled `nav`
|
|
870
|
+
* when the sidebar holds more than navigation; set `"navigation"` with an `aria-label` when it does
|
|
871
|
+
* not. Ignored on compact viewports, where the drawer is a `dialog`.
|
|
872
|
+
* @default ""
|
|
873
|
+
*/
|
|
874
|
+
this.role = input("", /* @ts-ignore */
|
|
875
|
+
...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
|
|
876
|
+
/**
|
|
877
|
+
* @description Which edge the sidebar sits on. Prefer `"start"` and `"end"`, which follow the
|
|
878
|
+
* document's writing direction; `"left"` and `"right"` are aliases of them.
|
|
879
|
+
* @default "start"
|
|
880
|
+
*/
|
|
881
|
+
this.side = input("start", /* @ts-ignore */
|
|
882
|
+
...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
|
|
883
|
+
/**
|
|
884
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
885
|
+
* @default ""
|
|
886
|
+
*/
|
|
887
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
888
|
+
transform: value => classInputToClass(value) });
|
|
889
|
+
/**
|
|
890
|
+
* @description The visual treatment of the sidebar. `"floating"` detaches it from the edge on its own
|
|
891
|
+
* surface, and `"inset"` moves that raised surface onto the region beside it instead.
|
|
892
|
+
* @default "sidebar"
|
|
893
|
+
*/
|
|
894
|
+
this.variant = input("sidebar", /* @ts-ignore */
|
|
895
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
896
|
+
/**
|
|
897
|
+
* @description Width of the sidebar while expanded. Numbers are treated as pixels.
|
|
898
|
+
* @default "16rem"
|
|
899
|
+
*/
|
|
900
|
+
this.width = input("16rem", /* @ts-ignore */
|
|
901
|
+
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
902
|
+
// These are authored here but read by descendants, so they have to reach the shared service.
|
|
903
|
+
effect(() => {
|
|
904
|
+
const collapsible = this.collapsible();
|
|
905
|
+
const id = this.id();
|
|
906
|
+
const persistKey = this.persistKey();
|
|
907
|
+
const side = this.side();
|
|
908
|
+
const variant = this.variant();
|
|
909
|
+
untracked(() => {
|
|
910
|
+
this.#sidebarService.setCollapsible(collapsible);
|
|
911
|
+
this.#sidebarService.setId(id);
|
|
912
|
+
this.#sidebarService.setPersistKey(persistKey);
|
|
913
|
+
this.#sidebarService.setSide(side);
|
|
914
|
+
this.#sidebarService.setVariant(variant);
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
effect(() => {
|
|
918
|
+
const expanded = this.expanded();
|
|
919
|
+
untracked(() => this.#sidebarService.setExpanded(expanded));
|
|
920
|
+
});
|
|
921
|
+
effect(() => {
|
|
922
|
+
const expanded = this.#sidebarService.expanded();
|
|
923
|
+
untracked(() => this.expanded.set(expanded));
|
|
924
|
+
});
|
|
925
|
+
// Keyed on the resolved id rather than the input, so a sidebar left to generate its own id is
|
|
926
|
+
// still reachable by a trigger. The effect's cleanup covers both halves of a changed id — the
|
|
927
|
+
// old key is dropped before the new one is added — as well as teardown.
|
|
928
|
+
effect(onCleanup => {
|
|
929
|
+
const layoutService = this.#layoutService;
|
|
930
|
+
if (!layoutService) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const id = this.#sidebarService.sidebarId();
|
|
934
|
+
onCleanup(layoutService.register(id, this.#sidebarService.controller));
|
|
935
|
+
});
|
|
936
|
+
effect(() => {
|
|
937
|
+
const trapped = this.drawer() && this.#sidebarService.mobileOpen();
|
|
938
|
+
untracked(() => this.#setFocusTrapped(trapped));
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* The trap is only armed for the drawer. A docked sidebar sits in the page beside its content, and
|
|
943
|
+
* holding focus inside it would leave the keyboard unable to reach the rest of the layout at all.
|
|
944
|
+
*/
|
|
945
|
+
#setFocusTrapped(trapped) {
|
|
946
|
+
this.#focusTrap.enabled = trapped;
|
|
947
|
+
if (trapped) {
|
|
948
|
+
const active = document.activeElement;
|
|
949
|
+
this.#restoreFocusTo = active instanceof HTMLElement ? active : null;
|
|
950
|
+
this.#focusTrap.focusTrap.focusInitialElementWhenReady();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const restoreTo = this.#restoreFocusTo;
|
|
954
|
+
this.#restoreFocusTo = null;
|
|
955
|
+
// Only pull focus back if it is still inside the panel that is closing. Moving it otherwise
|
|
956
|
+
// would yank the caret away from wherever the user has since gone.
|
|
957
|
+
if (!restoreTo || !this.#host.nativeElement.contains(document.activeElement)) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
/*
|
|
961
|
+
* Deferred to the next render, not called here. Whatever opened the drawer is almost always
|
|
962
|
+
* behind it, and the region behind it is still `inert` at this point in the change detection
|
|
963
|
+
* pass — `focus()` on an element inside an inert subtree fails silently, which left focus
|
|
964
|
+
* stranded inside the panel that was about to become inert itself.
|
|
965
|
+
*/
|
|
966
|
+
afterNextRender(() => {
|
|
967
|
+
if (restoreTo.isConnected) {
|
|
968
|
+
restoreTo.focus();
|
|
969
|
+
}
|
|
970
|
+
}, { injector: this.#injector });
|
|
971
|
+
}
|
|
972
|
+
#withBorderAllowance(width) {
|
|
973
|
+
const allowance = sidebarBorderAllowance[this.variant()];
|
|
974
|
+
// The `inset` variant draws no border, so it is left as an ordinary length.
|
|
975
|
+
return allowance === "0px" ? width : `calc(${width} + ${allowance})`;
|
|
976
|
+
}
|
|
977
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
978
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.6", type: SidebarComponent, isStandalone: true, selector: "mona-sidebar", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, iconWidth: { classPropertyName: "iconWidth", publicName: "iconWidth", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, mobileWidth: { classPropertyName: "mobileWidth", publicName: "mobileWidth", isSignal: true, isRequired: false, transformFunction: null }, persistKey: { classPropertyName: "persistKey", publicName: "persistKey", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expanded: "expandedChange" }, host: { properties: { "attr.aria-hidden": "hidden() ? 'true' : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-modal": "drawer() ? 'true' : null", "attr.data-collapsible": "collapsible()", "attr.data-side": "logicalSide()", "attr.data-state": "state()", "attr.data-variant": "variant()", "attr.id": "sidebarId()", "attr.inert": "hidden() ? '' : null", "attr.role": "resolvedRole()", "class": "baseClass()", "style.width": "widthString()" } }, providers: [SidebarService], hostDirectives: [{ directive: i1.CdkTrapFocus }], ngImport: i0, template: `<ng-content></ng-content>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
979
|
+
}
|
|
980
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarComponent, decorators: [{
|
|
981
|
+
type: Component,
|
|
982
|
+
args: [{
|
|
983
|
+
selector: "mona-sidebar",
|
|
984
|
+
template: `<ng-content></ng-content>`,
|
|
985
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
986
|
+
hostDirectives: [CdkTrapFocus],
|
|
987
|
+
host: {
|
|
988
|
+
"[attr.aria-hidden]": "hidden() ? 'true' : null",
|
|
989
|
+
"[attr.aria-label]": "ariaLabel() || null",
|
|
990
|
+
"[attr.aria-modal]": "drawer() ? 'true' : null",
|
|
991
|
+
"[attr.data-collapsible]": "collapsible()",
|
|
992
|
+
"[attr.data-side]": "logicalSide()",
|
|
993
|
+
"[attr.data-state]": "state()",
|
|
994
|
+
"[attr.data-variant]": "variant()",
|
|
995
|
+
"[attr.id]": "sidebarId()",
|
|
996
|
+
// A panel that is out of view must be out of the tab order and out of the accessibility tree
|
|
997
|
+
// with it. `inert` is the one attribute that does both; width alone left every control in the
|
|
998
|
+
// collapsed panel reachable by keyboard and readable by assistive technology.
|
|
999
|
+
"[attr.inert]": "hidden() ? '' : null",
|
|
1000
|
+
"[attr.role]": "resolvedRole()",
|
|
1001
|
+
"[class]": "baseClass()",
|
|
1002
|
+
"[style.width]": "widthString()"
|
|
1003
|
+
},
|
|
1004
|
+
// One service per sidebar, not per layout, so a layout can hold more than one of these and each
|
|
1005
|
+
// keeps its own side, width and open state. Descendants resolve the nearest one, which is theirs.
|
|
1006
|
+
providers: [SidebarService]
|
|
1007
|
+
}]
|
|
1008
|
+
}], ctorParameters: () => [], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], iconWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconWidth", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], mobileWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileWidth", required: false }] }], persistKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "persistKey", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }] } });
|
|
1009
|
+
function toCssLength(value) {
|
|
1010
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* @description
|
|
1015
|
+
* Lays its children out in a row and owns what they share. Project one or more `mona-sidebar`
|
|
1016
|
+
* elements and an element marked with `monaSidebarInset` into it. Each sidebar keeps its own open
|
|
1017
|
+
* state, on its own `[(expanded)]` binding, so a layout can hold a navigation column on one edge and
|
|
1018
|
+
* an inspector on the other.
|
|
1019
|
+
*
|
|
1020
|
+
* Below `mobileBreakpoint` sidebars present as overlay drawers instead of columns. A drawer has its
|
|
1021
|
+
* own open state, so a sidebar collapsed on a desktop does not come back open on a phone, and it is
|
|
1022
|
+
* reset whenever the viewport crosses the breakpoint. Only one drawer is open at a time: opening one
|
|
1023
|
+
* closes any other, and they share the single backdrop this layout paints behind them.
|
|
1024
|
+
*/
|
|
1025
|
+
class SidebarLayoutComponent {
|
|
1026
|
+
#layoutService;
|
|
1027
|
+
#matches;
|
|
1028
|
+
constructor() {
|
|
1029
|
+
this.#layoutService = inject(SidebarLayoutService);
|
|
1030
|
+
this.#matches = signal(false, /* @ts-ignore */
|
|
1031
|
+
...(ngDevMode ? [{ debugName: "#matches" }] : /* istanbul ignore next */ []));
|
|
1032
|
+
this.backdropClass = computed(() => sidebarBackdropThemeVariants({ open: this.mobileOpen() }), /* @ts-ignore */
|
|
1033
|
+
...(ngDevMode ? [{ debugName: "backdropClass" }] : /* istanbul ignore next */ []));
|
|
1034
|
+
this.baseClass = computed(() => twMerge(sidebarLayoutBaseThemeVariants(), this.userClass()), /* @ts-ignore */
|
|
1035
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1036
|
+
this.compact = this.#layoutService.compact;
|
|
1037
|
+
/** One backdrop serves every sidebar, because only one of their drawers is ever open. */
|
|
1038
|
+
this.mobileOpen = this.#layoutService.anyMobileOpen;
|
|
1039
|
+
/**
|
|
1040
|
+
* @description Viewport width, in pixels, below which the sidebar presents as an overlay drawer.
|
|
1041
|
+
* Set to `0` to keep the docked presentation at every size.
|
|
1042
|
+
* @default 768
|
|
1043
|
+
*/
|
|
1044
|
+
this.mobileBreakpoint = input(768, /* @ts-ignore */
|
|
1045
|
+
...(ngDevMode ? [{ debugName: "mobileBreakpoint" }] : /* istanbul ignore next */ []));
|
|
1046
|
+
/**
|
|
1047
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1048
|
+
* @default ""
|
|
1049
|
+
*/
|
|
1050
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1051
|
+
transform: value => classInputToClass(value) });
|
|
1052
|
+
this.#watchBreakpoint();
|
|
1053
|
+
}
|
|
1054
|
+
onBackdropClick() {
|
|
1055
|
+
this.#layoutService.closeAll();
|
|
1056
|
+
}
|
|
1057
|
+
onEscape(event) {
|
|
1058
|
+
// Only a drawer is dismissible. A docked sidebar is part of the page, and closing it on
|
|
1059
|
+
// Escape would take the key from anything inside it that wanted to handle it.
|
|
1060
|
+
if (!this.#layoutService.anyMobileOpen()) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
event.preventDefault();
|
|
1064
|
+
this.#layoutService.closeAll();
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Kept on `matchMedia` rather than a resize listener, so the browser decides when the threshold is
|
|
1068
|
+
* crossed and the query is rebuilt only when the breakpoint input itself changes.
|
|
1069
|
+
*/
|
|
1070
|
+
#watchBreakpoint() {
|
|
1071
|
+
const destroyRef = inject(DestroyRef);
|
|
1072
|
+
effect(() => {
|
|
1073
|
+
const compact = this.#matches();
|
|
1074
|
+
untracked(() => this.#layoutService.setCompact(compact));
|
|
1075
|
+
});
|
|
1076
|
+
// Server-rendered and jsdom hosts have no media queries; both keep the docked presentation,
|
|
1077
|
+
// which is the one that degrades gracefully without JavaScript.
|
|
1078
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
let current = null;
|
|
1082
|
+
const onChange = (event) => this.#matches.set(event.matches);
|
|
1083
|
+
const detach = () => current?.removeEventListener("change", onChange);
|
|
1084
|
+
destroyRef.onDestroy(detach);
|
|
1085
|
+
effect(() => {
|
|
1086
|
+
const breakpoint = this.mobileBreakpoint();
|
|
1087
|
+
untracked(() => {
|
|
1088
|
+
detach();
|
|
1089
|
+
if (breakpoint <= 0) {
|
|
1090
|
+
current = null;
|
|
1091
|
+
this.#matches.set(false);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
current = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
1095
|
+
current.addEventListener("change", onChange);
|
|
1096
|
+
this.#matches.set(current.matches);
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1101
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SidebarLayoutComponent, isStandalone: true, selector: "mona-sidebar-layout", inputs: { mobileBreakpoint: { classPropertyName: "mobileBreakpoint", publicName: "mobileBreakpoint", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown.escape": "onEscape($event)" }, properties: { "attr.data-compact": "compact() ? 'true' : null", "class": "baseClass()" } }, providers: [SidebarLayoutService], ngImport: i0, template: `
|
|
1102
|
+
<ng-content></ng-content>
|
|
1103
|
+
@if (compact()) {
|
|
1104
|
+
<div
|
|
1105
|
+
[class]="backdropClass()"
|
|
1106
|
+
[attr.aria-hidden]="'true'"
|
|
1107
|
+
[attr.data-state]="mobileOpen() ? 'open' : 'closed'"
|
|
1108
|
+
(click)="onBackdropClick()"></div>
|
|
1109
|
+
}
|
|
1110
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1111
|
+
}
|
|
1112
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarLayoutComponent, decorators: [{
|
|
1113
|
+
type: Component,
|
|
1114
|
+
args: [{
|
|
1115
|
+
selector: "mona-sidebar-layout",
|
|
1116
|
+
template: `
|
|
1117
|
+
<ng-content></ng-content>
|
|
1118
|
+
@if (compact()) {
|
|
1119
|
+
<div
|
|
1120
|
+
[class]="backdropClass()"
|
|
1121
|
+
[attr.aria-hidden]="'true'"
|
|
1122
|
+
[attr.data-state]="mobileOpen() ? 'open' : 'closed'"
|
|
1123
|
+
(click)="onBackdropClick()"></div>
|
|
1124
|
+
}
|
|
1125
|
+
`,
|
|
1126
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1127
|
+
host: {
|
|
1128
|
+
"[attr.data-compact]": "compact() ? 'true' : null",
|
|
1129
|
+
"[class]": "baseClass()",
|
|
1130
|
+
// Escape closes the drawer from anywhere inside the layout, including from within the
|
|
1131
|
+
// trapped focus inside the drawer itself.
|
|
1132
|
+
"(keydown.escape)": "onEscape($event)"
|
|
1133
|
+
},
|
|
1134
|
+
providers: [SidebarLayoutService]
|
|
1135
|
+
}]
|
|
1136
|
+
}], ctorParameters: () => [], propDecorators: { mobileBreakpoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileBreakpoint", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* @description
|
|
1140
|
+
* A loading placeholder shaped like a menu item. Drops its label bar on the icon rail, where the
|
|
1141
|
+
* real labels are not visible either.
|
|
1142
|
+
*/
|
|
1143
|
+
class SidebarMenuSkeletonComponent {
|
|
1144
|
+
constructor() {
|
|
1145
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1146
|
+
// Laid out exactly like a real menu button, so the two animate as one: the same inset centres the
|
|
1147
|
+
// icon square on the rail, and the same widened gap carries the label bar out of the clipped box.
|
|
1148
|
+
this.baseClass = computed(() => sidebarMenuSkeletonThemeVariants({ iconOnly: this.iconOnly() }), /* @ts-ignore */
|
|
1149
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1150
|
+
this.iconOnly = computed(() => this.#sidebarService?.iconOnly() ?? false, /* @ts-ignore */
|
|
1151
|
+
...(ngDevMode ? [{ debugName: "iconOnly" }] : /* istanbul ignore next */ []));
|
|
1152
|
+
/**
|
|
1153
|
+
* @description Width of the label bar. Varying it across a list keeps the placeholder from looking
|
|
1154
|
+
* like a rigid grid. Numbers are treated as pixels.
|
|
1155
|
+
* @default "60%"
|
|
1156
|
+
*/
|
|
1157
|
+
this.labelWidth = input("60%", /* @ts-ignore */
|
|
1158
|
+
...(ngDevMode ? [{ debugName: "labelWidth" }] : /* istanbul ignore next */ []));
|
|
1159
|
+
/**
|
|
1160
|
+
* @description Renders a square standing in for the item's icon.
|
|
1161
|
+
* @default true
|
|
1162
|
+
*/
|
|
1163
|
+
this.showIcon = input(true, /* @ts-ignore */
|
|
1164
|
+
...(ngDevMode ? [{ debugName: "showIcon" }] : /* istanbul ignore next */ []));
|
|
1165
|
+
}
|
|
1166
|
+
#sidebarService;
|
|
1167
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1168
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SidebarMenuSkeletonComponent, isStandalone: true, selector: "mona-sidebar-menu-skeleton", inputs: { labelWidth: { classPropertyName: "labelWidth", publicName: "labelWidth", isSignal: true, isRequired: false, transformFunction: null }, showIcon: { classPropertyName: "showIcon", publicName: "showIcon", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0, template: `
|
|
1169
|
+
@if (showIcon()) {
|
|
1170
|
+
<mona-skeleton width="1rem" height="1rem" rounded="small"></mona-skeleton>
|
|
1171
|
+
}
|
|
1172
|
+
<mona-skeleton [width]="labelWidth()" height="1rem" rounded="small"></mona-skeleton>
|
|
1173
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SkeletonComponent, selector: "mona-skeleton", inputs: ["height", "rounded", "class", "width"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1174
|
+
}
|
|
1175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuSkeletonComponent, decorators: [{
|
|
1176
|
+
type: Component,
|
|
1177
|
+
args: [{
|
|
1178
|
+
selector: "mona-sidebar-menu-skeleton",
|
|
1179
|
+
template: `
|
|
1180
|
+
@if (showIcon()) {
|
|
1181
|
+
<mona-skeleton width="1rem" height="1rem" rounded="small"></mona-skeleton>
|
|
1182
|
+
}
|
|
1183
|
+
<mona-skeleton [width]="labelWidth()" height="1rem" rounded="small"></mona-skeleton>
|
|
1184
|
+
`,
|
|
1185
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1186
|
+
imports: [SkeletonComponent],
|
|
1187
|
+
host: {
|
|
1188
|
+
"[class]": "baseClass()"
|
|
1189
|
+
}
|
|
1190
|
+
}]
|
|
1191
|
+
}], propDecorators: { labelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelWidth", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }] } });
|
|
1192
|
+
|
|
1193
|
+
class SidebarContentDirective {
|
|
1194
|
+
constructor() {
|
|
1195
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1196
|
+
/**
|
|
1197
|
+
* A scrollbar takes its width out of the content box, and the icon rail has none to give: at a
|
|
1198
|
+
* `3rem` rail it claims about a third of it and squeezes every icon in the region. There is nothing
|
|
1199
|
+
* to read on the rail anyway — the labels are gone — so the overflow is simply clipped there.
|
|
1200
|
+
*/
|
|
1201
|
+
this.baseClass = computed(() => sidebarContentThemeVariants({ iconOnly: this.#sidebarService?.iconOnly() ?? false }), /* @ts-ignore */
|
|
1202
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1203
|
+
}
|
|
1204
|
+
#sidebarService;
|
|
1205
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1206
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarContentDirective, isStandalone: true, selector: "[monaSidebarContent]", host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1207
|
+
}
|
|
1208
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarContentDirective, decorators: [{
|
|
1209
|
+
type: Directive,
|
|
1210
|
+
args: [{
|
|
1211
|
+
selector: "[monaSidebarContent]",
|
|
1212
|
+
host: {
|
|
1213
|
+
"[class]": "baseClass()"
|
|
1214
|
+
}
|
|
1215
|
+
}]
|
|
1216
|
+
}] });
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* @description
|
|
1220
|
+
* The region at the bottom of the sidebar, below its content. Holds what stays put while the content
|
|
1221
|
+
* scrolls — a profile menu, a sign-out.
|
|
1222
|
+
*/
|
|
1223
|
+
class SidebarFooterDirective {
|
|
1224
|
+
constructor() {
|
|
1225
|
+
this.baseClass = computed(() => twMerge(sidebarFooterThemeVariants(), this.userClass()), /* @ts-ignore */
|
|
1226
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1227
|
+
/**
|
|
1228
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1229
|
+
* @default ""
|
|
1230
|
+
*/
|
|
1231
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1232
|
+
transform: value => classInputToClass(value) });
|
|
1233
|
+
}
|
|
1234
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarFooterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1235
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarFooterDirective, isStandalone: true, selector: "[monaSidebarFooter]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1236
|
+
}
|
|
1237
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarFooterDirective, decorators: [{
|
|
1238
|
+
type: Directive,
|
|
1239
|
+
args: [{
|
|
1240
|
+
selector: "[monaSidebarFooter]",
|
|
1241
|
+
host: {
|
|
1242
|
+
"[class]": "baseClass()"
|
|
1243
|
+
}
|
|
1244
|
+
}]
|
|
1245
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* @description
|
|
1249
|
+
* A titled section of the sidebar, grouping a label and its menu. Stack as many as the sidebar
|
|
1250
|
+
* needs; each is an ordinary block, so a `monaSidebarSeparator` between them is optional.
|
|
1251
|
+
*/
|
|
1252
|
+
class SidebarGroupDirective {
|
|
1253
|
+
constructor() {
|
|
1254
|
+
// The group owns the inset for everything it contains, and keeps it the same in both states.
|
|
1255
|
+
// A `3rem` icon rail less this padding leaves exactly the `2rem` square a menu button becomes.
|
|
1256
|
+
this.baseClass = sidebarGroupThemeVariants();
|
|
1257
|
+
}
|
|
1258
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1259
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarGroupDirective, isStandalone: true, selector: "[monaSidebarGroup]", host: { properties: { "class": "baseClass" } }, ngImport: i0 }); }
|
|
1260
|
+
}
|
|
1261
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupDirective, decorators: [{
|
|
1262
|
+
type: Directive,
|
|
1263
|
+
args: [{
|
|
1264
|
+
selector: "[monaSidebarGroup]",
|
|
1265
|
+
host: {
|
|
1266
|
+
"[class]": "baseClass"
|
|
1267
|
+
}
|
|
1268
|
+
}]
|
|
1269
|
+
}] });
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* @description
|
|
1273
|
+
* A control in the corner of a group header — an add or overflow button. Stands down on the icon
|
|
1274
|
+
* rail, where there is no header row left to sit in. Give it an `aria-label`: it is icon-only.
|
|
1275
|
+
*/
|
|
1276
|
+
class SidebarGroupActionDirective {
|
|
1277
|
+
constructor() {
|
|
1278
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1279
|
+
// The group header it belongs to has no room on the rail.
|
|
1280
|
+
this.hidden = computed(() => this.#sidebarService?.iconOnly() ?? false, /* @ts-ignore */
|
|
1281
|
+
...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1282
|
+
}
|
|
1283
|
+
#sidebarService;
|
|
1284
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1285
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarGroupActionDirective, isStandalone: true, selector: "button[monaSidebarGroupAction]", host: { properties: { "attr.data-hidden": "hidden() ? 'true' : null" }, classAttribute: "\n p-1 w-auto h-auto\n visible opacity-100\n transition-[opacity,visibility] duration-(--mona-motion-standard) ease-out\n motion-reduce:transition-none\n data-[hidden=true]:invisible data-[hidden=true]:opacity-0\n" }, ngImport: i0 }); }
|
|
1286
|
+
}
|
|
1287
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupActionDirective, decorators: [{
|
|
1288
|
+
type: Directive,
|
|
1289
|
+
args: [{
|
|
1290
|
+
selector: "button[monaSidebarGroupAction]",
|
|
1291
|
+
host: {
|
|
1292
|
+
// Consumers commonly add `monaButton` to this element, which owns the `[class]` binding.
|
|
1293
|
+
// A data attribute lets the static sidebar recipe respond without competing with it.
|
|
1294
|
+
// Faded rather than removed: the header around it closes by height over the same interval, so
|
|
1295
|
+
// dropping it outright would empty the row before it had finished shrinking. `visibility`
|
|
1296
|
+
// transitions discretely, flipping only once the fade is done, and takes it out of tab order.
|
|
1297
|
+
"[attr.data-hidden]": "hidden() ? 'true' : null",
|
|
1298
|
+
class: sidebarGroupActionClasses
|
|
1299
|
+
}
|
|
1300
|
+
}]
|
|
1301
|
+
}] });
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* @description
|
|
1305
|
+
* The body of a `monaSidebarGroup`, below its header. Wraps the menu itself.
|
|
1306
|
+
*/
|
|
1307
|
+
class SidebarGroupContentDirective {
|
|
1308
|
+
constructor() {
|
|
1309
|
+
this.baseClass = sidebarGroupContentThemeVariants();
|
|
1310
|
+
}
|
|
1311
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1312
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarGroupContentDirective, isStandalone: true, selector: "[monaSidebarGroupContent]", host: { properties: { "class": "baseClass" } }, ngImport: i0 }); }
|
|
1313
|
+
}
|
|
1314
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupContentDirective, decorators: [{
|
|
1315
|
+
type: Directive,
|
|
1316
|
+
args: [{
|
|
1317
|
+
selector: "[monaSidebarGroupContent]",
|
|
1318
|
+
host: {
|
|
1319
|
+
"[class]": "baseClass"
|
|
1320
|
+
}
|
|
1321
|
+
}]
|
|
1322
|
+
}] });
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* @description
|
|
1326
|
+
* The row above a group's content, holding its label and any group action. Collapses to nothing on
|
|
1327
|
+
* the icon rail rather than leaving an empty gap where the label used to be.
|
|
1328
|
+
*/
|
|
1329
|
+
class SidebarGroupHeaderDirective {
|
|
1330
|
+
constructor() {
|
|
1331
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1332
|
+
this.baseClass = computed(() => sidebarGroupHeaderThemeVariants({ iconOnly: this.#sidebarService?.iconOnly() ?? false }), /* @ts-ignore */
|
|
1333
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1334
|
+
}
|
|
1335
|
+
#sidebarService;
|
|
1336
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1337
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarGroupHeaderDirective, isStandalone: true, selector: "[monaSidebarGroupHeader]", host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1338
|
+
}
|
|
1339
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupHeaderDirective, decorators: [{
|
|
1340
|
+
type: Directive,
|
|
1341
|
+
args: [{
|
|
1342
|
+
selector: "[monaSidebarGroupHeader]",
|
|
1343
|
+
host: {
|
|
1344
|
+
"[class]": "baseClass()"
|
|
1345
|
+
}
|
|
1346
|
+
}]
|
|
1347
|
+
}] });
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* @description
|
|
1351
|
+
* The name of a group. Fades out on the icon rail, where there is no width to read it in.
|
|
1352
|
+
*/
|
|
1353
|
+
class SidebarGroupLabelDirective {
|
|
1354
|
+
constructor() {
|
|
1355
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1356
|
+
// Faded rather than removed from the layout: the header around it closes by height over the same
|
|
1357
|
+
// interval, so dropping the label outright would empty the row before it had finished shrinking.
|
|
1358
|
+
// `visibility` follows the fade so it leaves the accessibility tree with it.
|
|
1359
|
+
this.baseClass = computed(() => sidebarGroupLabelThemeVariants({ hidden: this.#sidebarService?.iconOnly() ?? false }), /* @ts-ignore */
|
|
1360
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1361
|
+
}
|
|
1362
|
+
#sidebarService;
|
|
1363
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1364
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarGroupLabelDirective, isStandalone: true, selector: "[monaSidebarGroupLabel]", host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1365
|
+
}
|
|
1366
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarGroupLabelDirective, decorators: [{
|
|
1367
|
+
type: Directive,
|
|
1368
|
+
args: [{
|
|
1369
|
+
selector: "[monaSidebarGroupLabel]",
|
|
1370
|
+
host: {
|
|
1371
|
+
"[class]": "baseClass()"
|
|
1372
|
+
}
|
|
1373
|
+
}]
|
|
1374
|
+
}] });
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* @description
|
|
1378
|
+
* The region at the top of the sidebar, above its content. Holds what stays put while the content
|
|
1379
|
+
* scrolls — a workspace switcher, a search box.
|
|
1380
|
+
*/
|
|
1381
|
+
class SidebarHeaderDirective {
|
|
1382
|
+
constructor() {
|
|
1383
|
+
this.baseClass = computed(() => twMerge(sidebarHeaderThemeVariants(), this.userClass()), /* @ts-ignore */
|
|
1384
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1385
|
+
/**
|
|
1386
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1387
|
+
* @default ""
|
|
1388
|
+
*/
|
|
1389
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1390
|
+
transform: value => classInputToClass(value) });
|
|
1391
|
+
}
|
|
1392
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1393
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarHeaderDirective, isStandalone: true, selector: "[monaSidebarHeader]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1394
|
+
}
|
|
1395
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarHeaderDirective, decorators: [{
|
|
1396
|
+
type: Directive,
|
|
1397
|
+
args: [{
|
|
1398
|
+
selector: "[monaSidebarHeader]",
|
|
1399
|
+
host: {
|
|
1400
|
+
"[class]": "baseClass()"
|
|
1401
|
+
}
|
|
1402
|
+
}]
|
|
1403
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* @description
|
|
1407
|
+
* A text input sized for the sidebar, composing `monaTextBox`. Stands down on the icon rail,
|
|
1408
|
+
* where there is no room to type.
|
|
1409
|
+
*/
|
|
1410
|
+
class SidebarInputDirective {
|
|
1411
|
+
constructor() {
|
|
1412
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1413
|
+
this.baseClass = sidebarInputClasses();
|
|
1414
|
+
this.hidden = computed(() => this.#sidebarService?.iconOnly() ?? false, /* @ts-ignore */
|
|
1415
|
+
...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1416
|
+
}
|
|
1417
|
+
#sidebarService;
|
|
1418
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1419
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarInputDirective, isStandalone: true, selector: "input[monaSidebarInput]", host: { properties: { "attr.data-hidden": "hidden() ? 'true' : null", "class": "baseClass" } }, hostDirectives: [{ directive: i1$1.TextBoxDirective }], ngImport: i0 }); }
|
|
1420
|
+
}
|
|
1421
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarInputDirective, decorators: [{
|
|
1422
|
+
type: Directive,
|
|
1423
|
+
args: [{
|
|
1424
|
+
selector: "input[monaSidebarInput]",
|
|
1425
|
+
hostDirectives: [TextBoxDirective],
|
|
1426
|
+
host: {
|
|
1427
|
+
// `TextBoxDirective` owns `[class]`, so a data attribute drives the static sidebar recipe.
|
|
1428
|
+
"[attr.data-hidden]": "hidden() ? 'true' : null",
|
|
1429
|
+
"[class]": "baseClass"
|
|
1430
|
+
}
|
|
1431
|
+
}]
|
|
1432
|
+
}] });
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* @description
|
|
1436
|
+
* Marks the main region beside the sidebar. Fills the remaining width and scrolls independently.
|
|
1437
|
+
* Takes on the raised surface when the sidebar uses the `inset` variant.
|
|
1438
|
+
*
|
|
1439
|
+
* It sits beside the sidebars rather than inside one, so it answers to the layout as a whole: it steps
|
|
1440
|
+
* out of the way for whichever drawer is open, and where a layout holds more than one sidebar it takes
|
|
1441
|
+
* its surface from the first one in author order.
|
|
1442
|
+
*/
|
|
1443
|
+
class SidebarInsetDirective {
|
|
1444
|
+
constructor() {
|
|
1445
|
+
this.#layoutService = inject(SidebarLayoutService, { optional: true });
|
|
1446
|
+
this.baseClass = computed(() => {
|
|
1447
|
+
const variantClass = sidebarInsetThemeVariants({
|
|
1448
|
+
behindDrawer: this.behindDrawer(),
|
|
1449
|
+
variant: this.#layoutService?.primaryVariant() ?? "sidebar"
|
|
1450
|
+
});
|
|
1451
|
+
return twMerge(variantClass, this.userClass());
|
|
1452
|
+
}, /* @ts-ignore */
|
|
1453
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1454
|
+
this.behindDrawer = computed(() => this.#layoutService?.anyMobileOpen() ?? false, /* @ts-ignore */
|
|
1455
|
+
...(ngDevMode ? [{ debugName: "behindDrawer" }] : /* istanbul ignore next */ []));
|
|
1456
|
+
/**
|
|
1457
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1458
|
+
* @default ""
|
|
1459
|
+
*/
|
|
1460
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1461
|
+
transform: value => classInputToClass(value) });
|
|
1462
|
+
}
|
|
1463
|
+
#layoutService;
|
|
1464
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarInsetDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1465
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarInsetDirective, isStandalone: true, selector: "[monaSidebarInset]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()", "attr.inert": "behindDrawer() ? '' : null" } }, ngImport: i0 }); }
|
|
1466
|
+
}
|
|
1467
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarInsetDirective, decorators: [{
|
|
1468
|
+
type: Directive,
|
|
1469
|
+
args: [{
|
|
1470
|
+
selector: "[monaSidebarInset]",
|
|
1471
|
+
host: {
|
|
1472
|
+
"[class]": "baseClass()",
|
|
1473
|
+
// The drawer is modal, so everything behind it leaves the tab order and the accessibility
|
|
1474
|
+
// tree. `inert` covers both, and unlike `aria-hidden` it also stops pointer interaction.
|
|
1475
|
+
"[attr.inert]": "behindDrawer() ? '' : null"
|
|
1476
|
+
// The scoped equivalent of locking body scroll. The layout already clips its own overflow, so
|
|
1477
|
+
// this region is the only thing behind the drawer that scrolls; suppressing it here avoids
|
|
1478
|
+
// reaching out to mutate `document.body`, which a library has no business owning.
|
|
1479
|
+
}
|
|
1480
|
+
}]
|
|
1481
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* @description
|
|
1485
|
+
* A trailing control on a menu item — an overflow menu, a pin. Sits inside the row's highlight and
|
|
1486
|
+
* stands down on the icon rail. Give it an `aria-label`: it is icon-only.
|
|
1487
|
+
*/
|
|
1488
|
+
class SidebarMenuActionDirective {
|
|
1489
|
+
#button;
|
|
1490
|
+
#sidebarService;
|
|
1491
|
+
constructor() {
|
|
1492
|
+
this.#button = inject(ButtonDirective);
|
|
1493
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1494
|
+
this.baseClass = sidebarMenuActionClasses();
|
|
1495
|
+
// A trailing action would crowd out the icon it belongs to on the rail.
|
|
1496
|
+
this.hidden = computed(() => this.#sidebarService?.iconOnly() ?? false, /* @ts-ignore */
|
|
1497
|
+
...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
|
|
1498
|
+
effect(() => this.#button.look.set("ghost"));
|
|
1499
|
+
}
|
|
1500
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1501
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarMenuActionDirective, isStandalone: true, selector: "button[monaSidebarMenuAction]", host: { properties: { "attr.data-hidden": "hidden() ? 'true' : null", "class": "baseClass" } }, hostDirectives: [{ directive: i1$2.ButtonDirective }], ngImport: i0 }); }
|
|
1502
|
+
}
|
|
1503
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuActionDirective, decorators: [{
|
|
1504
|
+
type: Directive,
|
|
1505
|
+
args: [{
|
|
1506
|
+
selector: "button[monaSidebarMenuAction]",
|
|
1507
|
+
hostDirectives: [ButtonDirective],
|
|
1508
|
+
host: {
|
|
1509
|
+
"[attr.data-hidden]": "hidden() ? 'true' : null",
|
|
1510
|
+
"[class]": "baseClass"
|
|
1511
|
+
}
|
|
1512
|
+
}]
|
|
1513
|
+
}], ctorParameters: () => [] });
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* @description
|
|
1517
|
+
* A trailing count or status badge on a menu item. Stands down on the icon rail, where it would
|
|
1518
|
+
* crowd out the icon it belongs to.
|
|
1519
|
+
*/
|
|
1520
|
+
class SidebarMenuBadgeDirective {
|
|
1521
|
+
constructor() {
|
|
1522
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1523
|
+
this.baseClass = computed(() => twMerge(sidebarMenuBadgeThemeVariants({ hidden: this.#sidebarService?.iconOnly() ?? false }), this.userClass()), /* @ts-ignore */
|
|
1524
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1525
|
+
/**
|
|
1526
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1527
|
+
* @default ""
|
|
1528
|
+
*/
|
|
1529
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1530
|
+
transform: value => classInputToClass(value) });
|
|
1531
|
+
}
|
|
1532
|
+
#sidebarService;
|
|
1533
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuBadgeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1534
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarMenuBadgeDirective, isStandalone: true, selector: "[monaSidebarMenuBadge]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1535
|
+
}
|
|
1536
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuBadgeDirective, decorators: [{
|
|
1537
|
+
type: Directive,
|
|
1538
|
+
args: [{
|
|
1539
|
+
selector: "[monaSidebarMenuBadge]",
|
|
1540
|
+
host: {
|
|
1541
|
+
"[class]": "baseClass()"
|
|
1542
|
+
}
|
|
1543
|
+
}]
|
|
1544
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1545
|
+
|
|
1546
|
+
class SidebarMenuDirective {
|
|
1547
|
+
constructor() {
|
|
1548
|
+
this.baseClass = computed(() => twMerge(sidebarMenuThemeVariants(), this.userClass()), /* @ts-ignore */
|
|
1549
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1550
|
+
/**
|
|
1551
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1552
|
+
* @default ""
|
|
1553
|
+
*/
|
|
1554
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1555
|
+
transform: value => classInputToClass(value) });
|
|
1556
|
+
}
|
|
1557
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1558
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarMenuDirective, isStandalone: true, selector: "ul[monaSidebarMenu]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1559
|
+
}
|
|
1560
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuDirective, decorators: [{
|
|
1561
|
+
type: Directive,
|
|
1562
|
+
args: [{
|
|
1563
|
+
selector: "ul[monaSidebarMenu]",
|
|
1564
|
+
host: {
|
|
1565
|
+
"[class]": "baseClass()"
|
|
1566
|
+
}
|
|
1567
|
+
}]
|
|
1568
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1569
|
+
|
|
1570
|
+
class SidebarMenuItemDirective {
|
|
1571
|
+
// Applying `monaCollapsible` to the same element turns the item into a disclosure row, so it has to
|
|
1572
|
+
// stack its trigger and submenu instead of laying them out side-by-side.
|
|
1573
|
+
#collapsible;
|
|
1574
|
+
#sidebarService;
|
|
1575
|
+
#expandedBeforeRail;
|
|
1576
|
+
constructor() {
|
|
1577
|
+
// Applying `monaCollapsible` to the same element turns the item into a disclosure row, so it has to
|
|
1578
|
+
// stack its trigger and submenu instead of laying them out side-by-side.
|
|
1579
|
+
this.#collapsible = inject(CollapsibleToken, { optional: true, self: true });
|
|
1580
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1581
|
+
this.#expandedBeforeRail = false;
|
|
1582
|
+
this.baseClass = computed(() => {
|
|
1583
|
+
const variantClass = sidebarMenuItemThemeVariants({
|
|
1584
|
+
collapsible: this.#collapsible !== null,
|
|
1585
|
+
iconOnly: this.#sidebarService?.iconOnly() ?? false
|
|
1586
|
+
});
|
|
1587
|
+
return twMerge(variantClass, this.userClass());
|
|
1588
|
+
}, /* @ts-ignore */
|
|
1589
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1590
|
+
/**
|
|
1591
|
+
* @description Marks this row as the current sidebar destination. The row owns the selected
|
|
1592
|
+
* surface so trailing badges and actions remain inside one continuous highlight.
|
|
1593
|
+
* @default false
|
|
1594
|
+
*/
|
|
1595
|
+
this.active = input(false, /* @ts-ignore */
|
|
1596
|
+
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
1597
|
+
/**
|
|
1598
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1599
|
+
* @default ""
|
|
1600
|
+
*/
|
|
1601
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1602
|
+
transform: value => classInputToClass(value) });
|
|
1603
|
+
this.#keepSubmenuClosedOnRail();
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* A submenu cannot render in a rail one icon wide, and the previous answer — hiding it with
|
|
1607
|
+
* `display: none` — left the trigger still reporting `aria-expanded="true"` for something that was
|
|
1608
|
+
* not there. A screen reader user could open a submenu and be told it was open while nothing
|
|
1609
|
+
* appeared, and tab straight into its items.
|
|
1610
|
+
*
|
|
1611
|
+
* Closing the disclosure for real keeps the trigger's own state truthful and lets the collapsible
|
|
1612
|
+
* content directive apply the `inert` it already applies when closed. The prior state is restored
|
|
1613
|
+
* on the way back out, so collapsing and expanding the sidebar does not quietly discard it.
|
|
1614
|
+
*/
|
|
1615
|
+
#keepSubmenuClosedOnRail() {
|
|
1616
|
+
const collapsible = this.#collapsible;
|
|
1617
|
+
if (!collapsible) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
// Tracked separately from the signal so the two cases can be told apart: arriving on the rail
|
|
1621
|
+
// with the submenu already open, which is worth restoring, and trying to open it while on the
|
|
1622
|
+
// rail, which is not.
|
|
1623
|
+
let onRail = false;
|
|
1624
|
+
effect(() => {
|
|
1625
|
+
const iconOnly = this.#sidebarService?.iconOnly() ?? false;
|
|
1626
|
+
const expanded = collapsible.expanded();
|
|
1627
|
+
untracked(() => {
|
|
1628
|
+
if (iconOnly) {
|
|
1629
|
+
if (expanded) {
|
|
1630
|
+
this.#expandedBeforeRail ||= !onRail;
|
|
1631
|
+
collapsible.collapse();
|
|
1632
|
+
}
|
|
1633
|
+
onRail = true;
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (!onRail) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
onRail = false;
|
|
1640
|
+
if (this.#expandedBeforeRail) {
|
|
1641
|
+
this.#expandedBeforeRail = false;
|
|
1642
|
+
collapsible.expand();
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1648
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarMenuItemDirective, isStandalone: true, selector: "li[monaSidebarMenuItem]", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-active": "active() ? 'true' : null", "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1649
|
+
}
|
|
1650
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuItemDirective, decorators: [{
|
|
1651
|
+
type: Directive,
|
|
1652
|
+
args: [{
|
|
1653
|
+
selector: "li[monaSidebarMenuItem]",
|
|
1654
|
+
host: {
|
|
1655
|
+
"[attr.data-active]": "active() ? 'true' : null",
|
|
1656
|
+
"[class]": "baseClass()"
|
|
1657
|
+
}
|
|
1658
|
+
}]
|
|
1659
|
+
}], ctorParameters: () => [], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* @description
|
|
1663
|
+
* A row in a sidebar menu. Applies to a `button` for an action and to an `a` for a destination —
|
|
1664
|
+
* an anchor keeps `href`, `routerLink`, middle-click, open-in-new-tab and the browser's own status
|
|
1665
|
+
* bar preview, none of which a button can offer.
|
|
1666
|
+
*
|
|
1667
|
+
* On a compact viewport, following a link closes the drawer, since the destination it navigates to is
|
|
1668
|
+
* underneath it.
|
|
1669
|
+
*/
|
|
1670
|
+
class SidebarMenuButtonDirective {
|
|
1671
|
+
constructor() {
|
|
1672
|
+
this.#element = inject(ElementRef).nativeElement;
|
|
1673
|
+
this.#menuItem = inject(SidebarMenuItemDirective, { optional: true });
|
|
1674
|
+
this.#nativeButton = this.#element.tagName === "BUTTON";
|
|
1675
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1676
|
+
this.ariaDisabled = computed(() => (this.disabled() ? "true" : null), /* @ts-ignore */
|
|
1677
|
+
...(ngDevMode ? [{ debugName: "ariaDisabled" }] : /* istanbul ignore next */ []));
|
|
1678
|
+
this.baseClass = computed(() => {
|
|
1679
|
+
const variantClass = sidebarMenuButtonThemeVariants({
|
|
1680
|
+
active: this.#menuItem?.active() ?? false,
|
|
1681
|
+
disabled: this.disabled(),
|
|
1682
|
+
iconOnly: this.railBox(),
|
|
1683
|
+
size: this.size()
|
|
1684
|
+
});
|
|
1685
|
+
return twMerge(variantClass, this.userClass());
|
|
1686
|
+
}, /* @ts-ignore */
|
|
1687
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1688
|
+
/**
|
|
1689
|
+
* Marks the row the user is currently on. `page` regardless of host element: `active` describes a
|
|
1690
|
+
* destination, and a sidebar built from buttons rather than anchors is still a set of destinations
|
|
1691
|
+
* as far as a screen reader is concerned.
|
|
1692
|
+
*/
|
|
1693
|
+
this.current = computed(() => ((this.#menuItem?.active() ?? false) ? "page" : null), /* @ts-ignore */
|
|
1694
|
+
...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
|
|
1695
|
+
this.nativeDisabled = computed(() => (this.#nativeButton && this.disabled() ? "" : null), /* @ts-ignore */
|
|
1696
|
+
...(ngDevMode ? [{ debugName: "nativeDisabled" }] : /* istanbul ignore next */ []));
|
|
1697
|
+
/**
|
|
1698
|
+
* On the rail the row becomes a square exactly the size of its leading visual and clips its own
|
|
1699
|
+
* overflow, so any label is pushed out of view instead of competing for space.
|
|
1700
|
+
*/
|
|
1701
|
+
this.railBox = computed(() => this.#sidebarService?.iconOnly() ?? false, /* @ts-ignore */
|
|
1702
|
+
...(ngDevMode ? [{ debugName: "railBox" }] : /* istanbul ignore next */ []));
|
|
1703
|
+
// The label is clipped away on the rail, so the leading visual needs something to identify it. Only
|
|
1704
|
+
// set there, so the title never duplicates a label that is already legible.
|
|
1705
|
+
this.railTitle = computed(() => {
|
|
1706
|
+
const tooltip = this.tooltip();
|
|
1707
|
+
return tooltip && this.railBox() ? tooltip : null;
|
|
1708
|
+
}, /* @ts-ignore */
|
|
1709
|
+
...(ngDevMode ? [{ debugName: "railTitle" }] : /* istanbul ignore next */ []));
|
|
1710
|
+
this.tabIndex = computed(() => (this.disabled() && !this.#nativeButton ? 0 : null), /* @ts-ignore */
|
|
1711
|
+
...(ngDevMode ? [{ debugName: "tabIndex" }] : /* istanbul ignore next */ []));
|
|
1712
|
+
// Guards against a sidebar row inside a form submitting it.
|
|
1713
|
+
this.type = this.#nativeButton ? "button" : null;
|
|
1714
|
+
/**
|
|
1715
|
+
* @description Closes the overlay drawer after this row is activated. Only applies on compact
|
|
1716
|
+
* viewports, where the drawer covers the destination being navigated to. Set to `false` for rows
|
|
1717
|
+
* that do not navigate, such as a disclosure trigger.
|
|
1718
|
+
* @default true
|
|
1719
|
+
*/
|
|
1720
|
+
this.closeOnSelect = input(true, /* @ts-ignore */
|
|
1721
|
+
...(ngDevMode ? [{ debugName: "closeOnSelect" }] : /* istanbul ignore next */ []));
|
|
1722
|
+
/**
|
|
1723
|
+
* @description Renders the row as unavailable and suppresses activation.
|
|
1724
|
+
* @default false
|
|
1725
|
+
*/
|
|
1726
|
+
this.disabled = input(false, /* @ts-ignore */
|
|
1727
|
+
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1728
|
+
/**
|
|
1729
|
+
* @description How much room this row's leading visual needs. Use `"large"` when it opens with
|
|
1730
|
+
* something avatar sized, so the icon rail lets it fill the square instead of insetting and clipping it.
|
|
1731
|
+
* @default "medium"
|
|
1732
|
+
*/
|
|
1733
|
+
this.size = input("medium", /* @ts-ignore */
|
|
1734
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1735
|
+
/**
|
|
1736
|
+
* @description Text identifying this item while the sidebar is collapsed to its icon rail, where the
|
|
1737
|
+
* label is clipped away. Applied as the host's `title`, so the browser surfaces it on hover, and an
|
|
1738
|
+
* ancestor carrying `monaTooltip` with `mode="content"` renders it as a styled tooltip instead.
|
|
1739
|
+
* @default ""
|
|
1740
|
+
*/
|
|
1741
|
+
this.tooltip = input("", /* @ts-ignore */
|
|
1742
|
+
...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
|
|
1743
|
+
/**
|
|
1744
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1745
|
+
* @default ""
|
|
1746
|
+
*/
|
|
1747
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1748
|
+
transform: value => classInputToClass(value) });
|
|
1749
|
+
}
|
|
1750
|
+
#element;
|
|
1751
|
+
#menuItem;
|
|
1752
|
+
#nativeButton;
|
|
1753
|
+
#sidebarService;
|
|
1754
|
+
onClick(event) {
|
|
1755
|
+
if (this.disabled()) {
|
|
1756
|
+
// `aria-disabled` is advisory: an anchor with an `href` still navigates without this.
|
|
1757
|
+
event.preventDefault();
|
|
1758
|
+
event.stopImmediatePropagation();
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (this.closeOnSelect() && this.#sidebarService?.mobileOpen()) {
|
|
1762
|
+
this.#sidebarService.collapse();
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1766
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarMenuButtonDirective, isStandalone: true, selector: "a[monaSidebarMenuButton], button[monaSidebarMenuButton]", inputs: { closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick($event)" }, properties: { "attr.aria-current": "current()", "attr.aria-disabled": "ariaDisabled()", "attr.disabled": "nativeDisabled()", "attr.tabindex": "tabIndex()", "attr.title": "railTitle()", "attr.type": "type", "class": "baseClass()" } }, exportAs: ["monaSidebarMenuButton"], ngImport: i0 }); }
|
|
1767
|
+
}
|
|
1768
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuButtonDirective, decorators: [{
|
|
1769
|
+
type: Directive,
|
|
1770
|
+
args: [{
|
|
1771
|
+
selector: "a[monaSidebarMenuButton], button[monaSidebarMenuButton]",
|
|
1772
|
+
exportAs: "monaSidebarMenuButton",
|
|
1773
|
+
host: {
|
|
1774
|
+
"[attr.aria-current]": "current()",
|
|
1775
|
+
"[attr.aria-disabled]": "ariaDisabled()",
|
|
1776
|
+
"[attr.disabled]": "nativeDisabled()",
|
|
1777
|
+
// An anchor without an `href` is not focusable or clickable, so a disabled link has to be
|
|
1778
|
+
// given back a tab stop to stay announceable, and its activation suppressed instead.
|
|
1779
|
+
"[attr.tabindex]": "tabIndex()",
|
|
1780
|
+
"[attr.title]": "railTitle()",
|
|
1781
|
+
"[attr.type]": "type",
|
|
1782
|
+
"[class]": "baseClass()",
|
|
1783
|
+
"(click)": "onClick($event)"
|
|
1784
|
+
}
|
|
1785
|
+
}]
|
|
1786
|
+
}], propDecorators: { closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* @description
|
|
1790
|
+
* An indented submenu inside a `monaSidebarMenuItem` that is also a `monaCollapsible`.
|
|
1791
|
+
*
|
|
1792
|
+
* It carries no hiding of its own on the icon rail. `SidebarMenuItemDirective` closes the disclosure
|
|
1793
|
+
* there instead, which leaves the trigger's `aria-expanded` truthful and lets the collapsible content
|
|
1794
|
+
* directive apply its own `inert`. Hiding it here as well used to produce the opposite: a submenu the
|
|
1795
|
+
* trigger still described as expanded, with focusable items, that nobody could see.
|
|
1796
|
+
*/
|
|
1797
|
+
class SidebarMenuSubDirective {
|
|
1798
|
+
constructor() {
|
|
1799
|
+
this.baseClass = computed(() => twMerge(sidebarMenuSubThemeVariants(), this.userClass()), /* @ts-ignore */
|
|
1800
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1801
|
+
/**
|
|
1802
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1803
|
+
* @default ""
|
|
1804
|
+
*/
|
|
1805
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1806
|
+
transform: value => classInputToClass(value) });
|
|
1807
|
+
}
|
|
1808
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuSubDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1809
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarMenuSubDirective, isStandalone: true, selector: "ul[monaSidebarMenuSub]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1810
|
+
}
|
|
1811
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarMenuSubDirective, decorators: [{
|
|
1812
|
+
type: Directive,
|
|
1813
|
+
args: [{
|
|
1814
|
+
selector: "ul[monaSidebarMenuSub]",
|
|
1815
|
+
host: {
|
|
1816
|
+
"[class]": "baseClass()"
|
|
1817
|
+
}
|
|
1818
|
+
}]
|
|
1819
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* @description
|
|
1823
|
+
* A thin strip along the sidebar's inner edge that toggles it. Place it inside `mona-sidebar`,
|
|
1824
|
+
* which positions it. Hidden on narrow viewports, where the edge is too easy to hit by accident.
|
|
1825
|
+
*
|
|
1826
|
+
* Deliberately outside the tab order: it is a pointer shortcut for what `monaSidebarTrigger` already
|
|
1827
|
+
* does, and a second tab stop onto an unlabelled 4px strip would be noise for keyboard users. Provide
|
|
1828
|
+
* a `monaSidebarTrigger` as well — the rail is not a substitute for it — and give the rail an
|
|
1829
|
+
* `aria-label` so pointer users of assistive technology can still identify it.
|
|
1830
|
+
*/
|
|
1831
|
+
class SidebarRailDirective {
|
|
1832
|
+
constructor() {
|
|
1833
|
+
this.#sidebarService = inject(SidebarService);
|
|
1834
|
+
this.baseClass = computed(() => sidebarRailThemeVariants({ side: this.#sidebarService.side() }), /* @ts-ignore */
|
|
1835
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1836
|
+
this.expanded = this.#sidebarService.expanded;
|
|
1837
|
+
this.sidebarId = this.#sidebarService.sidebarId;
|
|
1838
|
+
}
|
|
1839
|
+
#sidebarService;
|
|
1840
|
+
onClick() {
|
|
1841
|
+
this.#sidebarService.toggle();
|
|
1842
|
+
}
|
|
1843
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarRailDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1844
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SidebarRailDirective, isStandalone: true, selector: "button[monaSidebarRail]", host: { listeners: { "click": "onClick()" }, properties: { "attr.aria-controls": "sidebarId()", "attr.aria-expanded": "expanded()", "attr.tabindex": "-1", "attr.type": "'button'", "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1845
|
+
}
|
|
1846
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarRailDirective, decorators: [{
|
|
1847
|
+
type: Directive,
|
|
1848
|
+
args: [{
|
|
1849
|
+
selector: "button[monaSidebarRail]",
|
|
1850
|
+
host: {
|
|
1851
|
+
"[attr.aria-controls]": "sidebarId()",
|
|
1852
|
+
"[attr.aria-expanded]": "expanded()",
|
|
1853
|
+
"[attr.tabindex]": "-1",
|
|
1854
|
+
// Without this a rail inside a form submits it, because `submit` is a button's default type.
|
|
1855
|
+
"[attr.type]": "'button'",
|
|
1856
|
+
"[class]": "baseClass()",
|
|
1857
|
+
"(click)": "onClick()"
|
|
1858
|
+
}
|
|
1859
|
+
}]
|
|
1860
|
+
}] });
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* @description
|
|
1864
|
+
* A horizontal rule between sidebar regions. Tightens its inset on the icon rail.
|
|
1865
|
+
*/
|
|
1866
|
+
class SidebarSeparatorDirective {
|
|
1867
|
+
constructor() {
|
|
1868
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1869
|
+
this.baseClass = computed(() => {
|
|
1870
|
+
const variantClass = sidebarSeparatorThemeVariants({
|
|
1871
|
+
iconOnly: this.#sidebarService?.iconOnly() ?? false
|
|
1872
|
+
});
|
|
1873
|
+
return twMerge(variantClass, this.userClass());
|
|
1874
|
+
}, /* @ts-ignore */
|
|
1875
|
+
...(ngDevMode ? [{ debugName: "baseClass" }] : /* istanbul ignore next */ []));
|
|
1876
|
+
/**
|
|
1877
|
+
* @description Additional CSS classes merged onto the host element via `tailwind-merge`.
|
|
1878
|
+
* @default ""
|
|
1879
|
+
*/
|
|
1880
|
+
this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class",
|
|
1881
|
+
transform: value => classInputToClass(value) });
|
|
1882
|
+
}
|
|
1883
|
+
#sidebarService;
|
|
1884
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarSeparatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1885
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarSeparatorDirective, isStandalone: true, selector: "[monaSidebarSeparator]", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0 }); }
|
|
1886
|
+
}
|
|
1887
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarSeparatorDirective, decorators: [{
|
|
1888
|
+
type: Directive,
|
|
1889
|
+
args: [{
|
|
1890
|
+
selector: "[monaSidebarSeparator]",
|
|
1891
|
+
host: {
|
|
1892
|
+
"[class]": "baseClass()"
|
|
1893
|
+
}
|
|
1894
|
+
}]
|
|
1895
|
+
}], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
|
|
1896
|
+
|
|
1897
|
+
/**
|
|
1898
|
+
* @description
|
|
1899
|
+
* Toggles a sidebar. Can be placed anywhere inside a `mona-sidebar-layout`, including in the inset,
|
|
1900
|
+
* and adds no styling of its own.
|
|
1901
|
+
*
|
|
1902
|
+
* By default it drives the sidebar it is written inside. Set `for` to the id of a `mona-sidebar` to
|
|
1903
|
+
* drive that one instead, which is what a trigger in a shared header needs when the layout holds more
|
|
1904
|
+
* than one sidebar. A `for` that matches no sidebar leaves the trigger inert rather than throwing, so
|
|
1905
|
+
* a sidebar that has not rendered yet does not take the page down with it.
|
|
1906
|
+
*
|
|
1907
|
+
* A `button` is the right host and needs nothing further. On any other element the directive supplies
|
|
1908
|
+
* the rest of the button contract — `role`, a tab stop, and Enter/Space activation — so a `div` or
|
|
1909
|
+
* `span` trigger is still operable by keyboard rather than silently mouse-only.
|
|
1910
|
+
*/
|
|
1911
|
+
class SidebarTriggerDirective {
|
|
1912
|
+
constructor() {
|
|
1913
|
+
this.#element = inject(ElementRef).nativeElement;
|
|
1914
|
+
this.#layoutService = inject(SidebarLayoutService, { optional: true });
|
|
1915
|
+
this.#nativeAnchor = this.#element.tagName === "A";
|
|
1916
|
+
this.#nativeButton = this.#element.tagName === "BUTTON" || this.#element.tagName === "INPUT" || this.#element.tagName === "SUMMARY";
|
|
1917
|
+
// Optional, because a trigger aimed at a sidebar by id is usually written outside every sidebar.
|
|
1918
|
+
this.#sidebarService = inject(SidebarService, { optional: true });
|
|
1919
|
+
/**
|
|
1920
|
+
* The sidebar this trigger drives: the one named by `for`, else the one it is written inside, else
|
|
1921
|
+
* the layout's first. That last step is what lets a trigger in the inset or a header go on working
|
|
1922
|
+
* without naming anything, which is all a layout with a single sidebar ever needs.
|
|
1923
|
+
*/
|
|
1924
|
+
this.#targetController = computed(() => {
|
|
1925
|
+
const targetId = this.for();
|
|
1926
|
+
if (targetId) {
|
|
1927
|
+
return this.#layoutService?.getController(targetId) ?? null;
|
|
1928
|
+
}
|
|
1929
|
+
return this.#sidebarService?.controller ?? this.#layoutService?.primaryController() ?? null;
|
|
1930
|
+
}, /* @ts-ignore */
|
|
1931
|
+
...(ngDevMode ? [{ debugName: "#targetController" }] : /* istanbul ignore next */ []));
|
|
1932
|
+
// Null rather than false when nothing is targeted: a trigger that controls nothing has no
|
|
1933
|
+
// expanded state to report, and claiming one would announce a control that does not exist.
|
|
1934
|
+
this.expanded = computed(() => this.#targetController()?.expanded() ?? null, /* @ts-ignore */
|
|
1935
|
+
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
1936
|
+
this.role = this.#nativeButton || this.#nativeAnchor ? null : "button";
|
|
1937
|
+
this.sidebarId = computed(() => this.#targetController()?.sidebarId() ?? null, /* @ts-ignore */
|
|
1938
|
+
...(ngDevMode ? [{ debugName: "sidebarId" }] : /* istanbul ignore next */ []));
|
|
1939
|
+
this.tabIndex = this.#nativeButton || this.#nativeAnchor ? null : 0;
|
|
1940
|
+
this.type = this.#element.tagName === "BUTTON" ? "button" : null;
|
|
1941
|
+
/**
|
|
1942
|
+
* @description Id of the `mona-sidebar` to toggle. Leave unset to toggle the sidebar this trigger
|
|
1943
|
+
* is written inside.
|
|
1944
|
+
* @default ""
|
|
1945
|
+
*/
|
|
1946
|
+
this.for = input("", /* @ts-ignore */
|
|
1947
|
+
...(ngDevMode ? [{ debugName: "for" }] : /* istanbul ignore next */ []));
|
|
1948
|
+
}
|
|
1949
|
+
#element;
|
|
1950
|
+
#layoutService;
|
|
1951
|
+
#nativeAnchor;
|
|
1952
|
+
#nativeButton;
|
|
1953
|
+
// Optional, because a trigger aimed at a sidebar by id is usually written outside every sidebar.
|
|
1954
|
+
#sidebarService;
|
|
1955
|
+
/**
|
|
1956
|
+
* The sidebar this trigger drives: the one named by `for`, else the one it is written inside, else
|
|
1957
|
+
* the layout's first. That last step is what lets a trigger in the inset or a header go on working
|
|
1958
|
+
* without naming anything, which is all a layout with a single sidebar ever needs.
|
|
1959
|
+
*/
|
|
1960
|
+
#targetController;
|
|
1961
|
+
onClick() {
|
|
1962
|
+
this.#targetController()?.toggle();
|
|
1963
|
+
}
|
|
1964
|
+
onEnterKeydown(event) {
|
|
1965
|
+
// Buttons and anchors already synthesize a click for Enter.
|
|
1966
|
+
if (this.#nativeButton || this.#nativeAnchor) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
event.preventDefault();
|
|
1970
|
+
this.#targetController()?.toggle();
|
|
1971
|
+
}
|
|
1972
|
+
onSpaceKeydown(event) {
|
|
1973
|
+
// Buttons already synthesize a click for Space; anchors scroll the page instead.
|
|
1974
|
+
if (this.#nativeButton) {
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
event.preventDefault();
|
|
1978
|
+
this.#targetController()?.toggle();
|
|
1979
|
+
}
|
|
1980
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1981
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SidebarTriggerDirective, isStandalone: true, selector: "[monaSidebarTrigger]", inputs: { for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick()", "keydown.enter": "onEnterKeydown($event)", "keydown.space": "onSpaceKeydown($event)" }, properties: { "attr.aria-controls": "sidebarId()", "attr.aria-expanded": "expanded()", "attr.role": "role", "attr.tabindex": "tabIndex", "attr.type": "type" } }, exportAs: ["monaSidebarTrigger"], ngImport: i0 }); }
|
|
1982
|
+
}
|
|
1983
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SidebarTriggerDirective, decorators: [{
|
|
1984
|
+
type: Directive,
|
|
1985
|
+
args: [{
|
|
1986
|
+
selector: "[monaSidebarTrigger]",
|
|
1987
|
+
exportAs: "monaSidebarTrigger",
|
|
1988
|
+
host: {
|
|
1989
|
+
"[attr.aria-controls]": "sidebarId()",
|
|
1990
|
+
"[attr.aria-expanded]": "expanded()",
|
|
1991
|
+
"[attr.role]": "role",
|
|
1992
|
+
"[attr.tabindex]": "tabIndex",
|
|
1993
|
+
// Without this a trigger inside a form submits it, because `submit` is a button's default type.
|
|
1994
|
+
"[attr.type]": "type",
|
|
1995
|
+
"(click)": "onClick()",
|
|
1996
|
+
"(keydown.enter)": "onEnterKeydown($event)",
|
|
1997
|
+
"(keydown.space)": "onSpaceKeydown($event)"
|
|
1998
|
+
}
|
|
1999
|
+
}]
|
|
2000
|
+
}], propDecorators: { for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }] } });
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Replaces where sidebars remember whether they were left open. The default is `localStorage`, which
|
|
2004
|
+
* the server cannot read; supply a cookie- or request-backed storage here to have a server-rendered
|
|
2005
|
+
* application render the remembered state rather than correcting it on hydration.
|
|
2006
|
+
*/
|
|
2007
|
+
function provideSidebarStorage(storage) {
|
|
2008
|
+
return makeEnvironmentProviders([{ provide: SIDEBAR_STORAGE, useValue: storage }]);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function injectSidebar(options) {
|
|
2012
|
+
const service = inject(SidebarService, { optional: options?.optional ?? false });
|
|
2013
|
+
return service?.controller ?? null;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/*
|
|
2017
|
+
* Public API Surface of @nanahoshi/mona-ui/sidebar
|
|
2018
|
+
*/
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Generated bundle index. Do not edit.
|
|
2022
|
+
*/
|
|
2023
|
+
|
|
2024
|
+
export { LocalStorageSidebarStorage, SIDEBAR_STORAGE, SidebarComponent, SidebarContentDirective, SidebarFooterDirective, SidebarGroupActionDirective, SidebarGroupContentDirective, SidebarGroupDirective, SidebarGroupHeaderDirective, SidebarGroupLabelDirective, SidebarHeaderDirective, SidebarInputDirective, SidebarInsetDirective, SidebarLayoutComponent, SidebarMenuActionDirective, SidebarMenuBadgeDirective, SidebarMenuButtonDirective, SidebarMenuDirective, SidebarMenuItemDirective, SidebarMenuSkeletonComponent, SidebarMenuSubDirective, SidebarRailDirective, SidebarSeparatorDirective, SidebarTriggerDirective, injectSidebar, provideSidebarStorage };
|
|
2025
|
+
//# sourceMappingURL=nanahoshi-mona-ui-sidebar.mjs.map
|