@ojiepermana/angular-theme 22.0.43 → 22.0.45

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject, input, ChangeDetectionStrategy, Component, effect, untracked, contentChild, output, booleanAttribute } from '@angular/core';
2
+ import { effect, signal, computed, Injectable, inject, ElementRef, input, booleanAttribute, untracked, Component, contentChild, output } from '@angular/core';
3
3
  import { cn } from '@ojiepermana/angular-component/utils';
4
4
  import { NavigationContainerComponent, NavigationFlyoutComponent, NavigationHeaderComponent, NavigationFooterComponent } from '@ojiepermana/angular-navigation';
5
5
  import { NavigationService } from '@ojiepermana/angular-navigation/service';
@@ -7,6 +7,45 @@ import { LayoutService } from '@ojiepermana/angular-theme/layout';
7
7
  import { LayoutIdentityService, LayoutBrand, LayoutUser } from '@ojiepermana/angular-theme/layout/wrapper';
8
8
  import { DOCUMENT } from '@angular/common';
9
9
 
10
+ /**
11
+ * Manajemen fokus untuk panel mengambang (drawer/overlay): saat terbuka, fokus dipindahkan ke
12
+ * panel; saat tertutup, fokus dikembalikan ke elemen pemicu (mis. tombol toggle). Memenuhi
13
+ * WCAG 2.4.3 (Focus Order). Hanya aktif untuk mode interaktif, aman saat SSR (tanpa `defaultView`),
14
+ * dan tidak mencuri fokus pada render awal.
15
+ *
16
+ * Harus dipanggil di dalam injection context (constructor) karena membuat `effect`.
17
+ */
18
+ function setupOverlayFocusManagement(config) {
19
+ let initialized = false;
20
+ let wasOpen = false;
21
+ let returnFocus = null;
22
+ effect(() => {
23
+ const interactive = config.isInteractive();
24
+ const open = interactive && config.isOpen();
25
+ const view = config.document?.defaultView ?? null;
26
+ // Lewati render pertama agar tidak mencuri fokus saat panel sudah controlled-open sejak awal.
27
+ if (!initialized) {
28
+ initialized = true;
29
+ wasOpen = open;
30
+ return;
31
+ }
32
+ if (view) {
33
+ if (open && !wasOpen) {
34
+ returnFocus = config.document?.activeElement ?? null;
35
+ view.requestAnimationFrame(() => config.host.focus());
36
+ }
37
+ else if (!open && wasOpen) {
38
+ const target = returnFocus;
39
+ returnFocus = null;
40
+ if (target && typeof target.focus === 'function') {
41
+ view.requestAnimationFrame(() => target.focus());
42
+ }
43
+ }
44
+ }
45
+ wasOpen = open;
46
+ });
47
+ }
48
+
10
49
  const PAGE_VARIANTS = ['stacked', 'side'];
11
50
  const PAGE_SIDE_POSITIONS = ['left', 'right'];
12
51
  const PAGE_SIDE_MODES = ['sticky', 'drawer', 'overlay'];
@@ -14,6 +53,8 @@ const PAGE_SCROLL_VALUES = ['content', 'page'];
14
53
  const PAGE_HEIGHT_VALUES = ['auto', 'fix'];
15
54
  /** Visual appearance shared with the layout/navigation axes — unifies borders. */
16
55
  const PAGE_APPEARANCES = ['flat', 'border-rail'];
56
+ /** Penempatan `PageFilter`: `stacked` = bar antara header & content; `side` = kolom di samping content. */
57
+ const PAGE_FILTER_PLACEMENTS = ['stacked', 'side'];
17
58
  const PAGE_DEFAULT_VARIANT = 'stacked';
18
59
  const PAGE_DEFAULT_SIDE_POSITION = 'left';
19
60
  const PAGE_DEFAULT_SIDE_MODE = 'sticky';
@@ -21,6 +62,14 @@ const PAGE_DEFAULT_SCROLL = 'content';
21
62
  const PAGE_DEFAULT_HEIGHT = 'auto';
22
63
  const PAGE_DEFAULT_APPEARANCE = 'flat';
23
64
  const PAGE_DEFAULT_SIDE_WIDTH = '16rem';
65
+ /** Placement default `PageFilter`. */
66
+ const PAGE_DEFAULT_FILTER_PLACEMENT = 'stacked';
67
+ /** Mode default `PageFilter` saat `placement="side"`; `stacked` selalu berperilaku `sticky`. */
68
+ const PAGE_DEFAULT_FILTER_MODE = 'sticky';
69
+ /** Posisi default kolom `PageFilter` saat `placement="side"`. */
70
+ const PAGE_DEFAULT_FILTER_POSITION = 'left';
71
+ /** Lebar default kolom `PageFilter` saat `placement="side"` (sticky/drawer/overlay). */
72
+ const PAGE_DEFAULT_FILTER_WIDTH = '18rem';
24
73
  function isUiPageVariant(value) {
25
74
  return value !== null && PAGE_VARIANTS.includes(value);
26
75
  }
@@ -39,6 +88,9 @@ function isUiPageHeight(value) {
39
88
  function isUiPageAppearance(value) {
40
89
  return value !== null && PAGE_APPEARANCES.includes(value);
41
90
  }
91
+ function isUiPageFilterPlacement(value) {
92
+ return value !== null && PAGE_FILTER_PLACEMENTS.includes(value);
93
+ }
42
94
 
43
95
  class PageStateService {
44
96
  variantState = signal(PAGE_DEFAULT_VARIANT, /* @ts-ignore */
@@ -68,6 +120,25 @@ class PageStateService {
68
120
  /** Aktif saat apps-launcher mengambang di atas `PageHeader`; header memesan ruang kanan agar isinya tidak tertimpa. */
69
121
  appsLauncherReserveState = signal(false, /* @ts-ignore */
70
122
  ...(ngDevMode ? [{ debugName: "appsLauncherReserveState" }] : /* istanbul ignore next */ []));
123
+ // Filter state — paralel dengan side state agar `PageFilter` punya open/close sendiri yang independen.
124
+ filterPlacementState = signal(PAGE_DEFAULT_FILTER_PLACEMENT, /* @ts-ignore */
125
+ ...(ngDevMode ? [{ debugName: "filterPlacementState" }] : /* istanbul ignore next */ []));
126
+ filterModeState = signal(PAGE_DEFAULT_FILTER_MODE, /* @ts-ignore */
127
+ ...(ngDevMode ? [{ debugName: "filterModeState" }] : /* istanbul ignore next */ []));
128
+ filterPositionState = signal(PAGE_DEFAULT_FILTER_POSITION, /* @ts-ignore */
129
+ ...(ngDevMode ? [{ debugName: "filterPositionState" }] : /* istanbul ignore next */ []));
130
+ filterWidthState = signal(PAGE_DEFAULT_FILTER_WIDTH, /* @ts-ignore */
131
+ ...(ngDevMode ? [{ debugName: "filterWidthState" }] : /* istanbul ignore next */ []));
132
+ filterIdState = signal(null, /* @ts-ignore */
133
+ ...(ngDevMode ? [{ debugName: "filterIdState" }] : /* istanbul ignore next */ []));
134
+ internalFilterOpenState = signal(false, /* @ts-ignore */
135
+ ...(ngDevMode ? [{ debugName: "internalFilterOpenState" }] : /* istanbul ignore next */ []));
136
+ controlledFilterOpenState = signal(null, /* @ts-ignore */
137
+ ...(ngDevMode ? [{ debugName: "controlledFilterOpenState" }] : /* istanbul ignore next */ []));
138
+ filterOpenRequestState = signal(null, /* @ts-ignore */
139
+ ...(ngDevMode ? [{ debugName: "filterOpenRequestState" }] : /* istanbul ignore next */ []));
140
+ filterOpenRequestVersionState = signal(0, /* @ts-ignore */
141
+ ...(ngDevMode ? [{ debugName: "filterOpenRequestVersionState" }] : /* istanbul ignore next */ []));
71
142
  variant = this.variantState.asReadonly();
72
143
  height = this.heightState.asReadonly();
73
144
  scroll = this.scrollState.asReadonly();
@@ -85,6 +156,19 @@ class PageStateService {
85
156
  ...(ngDevMode ? [{ debugName: "isSideInteractive" }] : /* istanbul ignore next */ []));
86
157
  isSideVisible = computed(() => this.sideMode() === 'sticky' || this.sideOpen(), /* @ts-ignore */
87
158
  ...(ngDevMode ? [{ debugName: "isSideVisible" }] : /* istanbul ignore next */ []));
159
+ filterPlacement = this.filterPlacementState.asReadonly();
160
+ filterMode = this.filterModeState.asReadonly();
161
+ filterPosition = this.filterPositionState.asReadonly();
162
+ filterWidth = this.filterWidthState.asReadonly();
163
+ filterId = this.filterIdState.asReadonly();
164
+ filterOpenRequest = this.filterOpenRequestState.asReadonly();
165
+ filterOpenRequestVersion = this.filterOpenRequestVersionState.asReadonly();
166
+ filterOpen = computed(() => this.controlledFilterOpenState() ?? this.internalFilterOpenState(), /* @ts-ignore */
167
+ ...(ngDevMode ? [{ debugName: "filterOpen" }] : /* istanbul ignore next */ []));
168
+ isFilterInteractive = computed(() => this.filterMode() === 'drawer' || this.filterMode() === 'overlay', /* @ts-ignore */
169
+ ...(ngDevMode ? [{ debugName: "isFilterInteractive" }] : /* istanbul ignore next */ []));
170
+ isFilterVisible = computed(() => this.filterMode() === 'sticky' || this.filterOpen(), /* @ts-ignore */
171
+ ...(ngDevMode ? [{ debugName: "isFilterVisible" }] : /* istanbul ignore next */ []));
88
172
  registerRoot(config) {
89
173
  this.variantState.set(config.variant);
90
174
  this.heightState.set(config.height);
@@ -129,13 +213,218 @@ class PageStateService {
129
213
  this.sideOpenRequestVersionState.update((version) => version + 1);
130
214
  return open;
131
215
  }
132
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
133
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageStateService });
216
+ registerFilter(config) {
217
+ this.filterPlacementState.set(config.placement);
218
+ this.filterModeState.set(config.mode);
219
+ this.filterPositionState.set(config.position);
220
+ this.filterWidthState.set(config.width);
221
+ this.filterIdState.set(config.id);
222
+ }
223
+ setControlledFilterOpen(open) {
224
+ this.controlledFilterOpenState.set(open);
225
+ if (open !== null) {
226
+ this.internalFilterOpenState.set(open);
227
+ }
228
+ }
229
+ setFilterOpen(open) {
230
+ this.internalFilterOpenState.set(open);
231
+ }
232
+ openFilter() {
233
+ return this.requestFilterOpenChange(true);
234
+ }
235
+ closeFilter() {
236
+ return this.requestFilterOpenChange(false);
237
+ }
238
+ toggleFilter() {
239
+ return this.requestFilterOpenChange(!this.filterOpen());
240
+ }
241
+ requestFilterOpenChange(open) {
242
+ if (this.controlledFilterOpenState() === null) {
243
+ this.internalFilterOpenState.set(open);
244
+ }
245
+ this.filterOpenRequestState.set(open);
246
+ this.filterOpenRequestVersionState.update((version) => version + 1);
247
+ return open;
248
+ }
249
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
250
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageStateService });
134
251
  }
135
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageStateService, decorators: [{
252
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageStateService, decorators: [{
136
253
  type: Injectable
137
254
  }] });
138
255
 
256
+ let nextPageFilterId = 0;
257
+ /**
258
+ * `PageFilter` — slot filter di antara `PageHeader` dan `PageContent` (`placement="stacked"`)
259
+ * atau di samping `PageContent` (`placement="side"`). Pada `stacked`, filter mendorong content
260
+ * ke bawah dan content menyesuaikan tinggi sisa. Pada `side`, filter mendorong content ke samping
261
+ * dengan mode `sticky | drawer | overlay` (mengikuti pola `PageSide`).
262
+ *
263
+ * `placement="stacked"` selalu berperilaku `sticky` — `drawer`/`overlay` hanya berlaku untuk `side`.
264
+ */
265
+ class PageFilterComponent {
266
+ document = inject(DOCUMENT, { optional: true });
267
+ host = inject((ElementRef));
268
+ page = inject(PageStateService);
269
+ resolvedId = `page-filter-${++nextPageFilterId}`;
270
+ placement = input(PAGE_DEFAULT_FILTER_PLACEMENT, /* @ts-ignore */
271
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
272
+ mode = input(PAGE_DEFAULT_FILTER_MODE, /* @ts-ignore */
273
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
274
+ position = input(null, /* @ts-ignore */
275
+ ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
276
+ width = input(null, /* @ts-ignore */
277
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
278
+ closeOnEsc = input(true, /* @ts-ignore */
279
+ ...(ngDevMode ? [{ debugName: "closeOnEsc" }] : /* istanbul ignore next */ []));
280
+ /**
281
+ * Khusus `placement="stacked"`: jadikan bar dapat dibuka/tutup lewat `PageFilterToggle`
282
+ * (default tertutup). Saat tertutup, bar disembunyikan dan content mengisi ruangnya kembali.
283
+ * Diabaikan untuk `placement="side"` (gunakan `mode="drawer"`/`overlay"` untuk side yang bisa ditutup).
284
+ */
285
+ collapsible = input(false, { ...(ngDevMode ? { debugName: "collapsible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
286
+ /** Nama aksesibilitas untuk panel saat mode `drawer`/`overlay` (dipasang sebagai `aria-label` dialog). */
287
+ ariaLabel = input('Filters', /* @ts-ignore */
288
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
289
+ class = input('', /* @ts-ignore */
290
+ ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
291
+ resolvedPlacement = computed(() => this.placement(), /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "resolvedPlacement" }] : /* istanbul ignore next */ []));
293
+ /** `stacked` adalah bar horizontal — tidak punya drawer/overlay, jadi mode efektifnya selalu `sticky`. */
294
+ effectiveMode = computed(() => this.resolvedPlacement() === 'stacked' ? 'sticky' : this.mode(), /* @ts-ignore */
295
+ ...(ngDevMode ? [{ debugName: "effectiveMode" }] : /* istanbul ignore next */ []));
296
+ resolvedPosition = computed(() => this.position() ?? this.page.filterPosition(), /* @ts-ignore */
297
+ ...(ngDevMode ? [{ debugName: "resolvedPosition" }] : /* istanbul ignore next */ []));
298
+ resolvedWidth = computed(() => this.width() ?? this.page.filterWidth() ?? PAGE_DEFAULT_FILTER_WIDTH, /* @ts-ignore */
299
+ ...(ngDevMode ? [{ debugName: "resolvedWidth" }] : /* istanbul ignore next */ []));
300
+ resolvedScroll = computed(() => this.page.scroll() ?? PAGE_DEFAULT_SCROLL, /* @ts-ignore */
301
+ ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
302
+ isStacked = computed(() => this.resolvedPlacement() === 'stacked', /* @ts-ignore */
303
+ ...(ngDevMode ? [{ debugName: "isStacked" }] : /* istanbul ignore next */ []));
304
+ /** Bar stacked yang dapat ditutup dan sedang tertutup → disembunyikan dari layout. */
305
+ isCollapsedStacked = computed(() => this.isStacked() && this.collapsible() && !this.page.filterOpen(), /* @ts-ignore */
306
+ ...(ngDevMode ? [{ debugName: "isCollapsedStacked" }] : /* istanbul ignore next */ []));
307
+ isSticky = computed(() => this.effectiveMode() === 'sticky', /* @ts-ignore */
308
+ ...(ngDevMode ? [{ debugName: "isSticky" }] : /* istanbul ignore next */ []));
309
+ isDrawer = computed(() => this.effectiveMode() === 'drawer', /* @ts-ignore */
310
+ ...(ngDevMode ? [{ debugName: "isDrawer" }] : /* istanbul ignore next */ []));
311
+ isOverlay = computed(() => this.effectiveMode() === 'overlay', /* @ts-ignore */
312
+ ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
313
+ /** Mode mengambang (drawer/overlay) — bersifat dialog dan butuh manajemen fokus + `inert` saat tertutup. */
314
+ isInteractive = computed(() => this.isDrawer() || this.isOverlay(), /* @ts-ignore */
315
+ ...(ngDevMode ? [{ debugName: "isInteractive" }] : /* istanbul ignore next */ []));
316
+ /** Drawer/overlay yang tertutup disembunyikan dari pohon aksesibilitas. */
317
+ ariaHidden = computed(() => !this.isSticky() && !this.page.filterOpen() ? 'true' : null, /* @ts-ignore */
318
+ ...(ngDevMode ? [{ debugName: "ariaHidden" }] : /* istanbul ignore next */ []));
319
+ /** Saat panel mengambang tertutup, `inert` mengeluarkan isinya dari tab order & pohon aksesibilitas (AXE). */
320
+ inertWhenClosed = computed(() => this.isInteractive() && !this.page.filterOpen() ? '' : null, /* @ts-ignore */
321
+ ...(ngDevMode ? [{ debugName: "inertWhenClosed" }] : /* istanbul ignore next */ []));
322
+ dialogRole = computed(() => (this.isInteractive() ? 'dialog' : null), /* @ts-ignore */
323
+ ...(ngDevMode ? [{ debugName: "dialogRole" }] : /* istanbul ignore next */ []));
324
+ ariaModal = computed(() => (this.isOverlay() ? 'true' : null), /* @ts-ignore */
325
+ ...(ngDevMode ? [{ debugName: "ariaModal" }] : /* istanbul ignore next */ []));
326
+ dialogLabel = computed(() => (this.isInteractive() ? this.ariaLabel() : null), /* @ts-ignore */
327
+ ...(ngDevMode ? [{ debugName: "dialogLabel" }] : /* istanbul ignore next */ []));
328
+ dialogTabindex = computed(() => (this.isInteractive() ? '-1' : null), /* @ts-ignore */
329
+ ...(ngDevMode ? [{ debugName: "dialogTabindex" }] : /* istanbul ignore next */ []));
330
+ classes = computed(() => {
331
+ const position = this.resolvedPosition();
332
+ const scroll = this.resolvedScroll();
333
+ const open = this.page.filterOpen();
334
+ // STACKED: bar selebar content yang mendorong content ke bawah (shrink-0 = tinggi mengikuti isi).
335
+ if (this.isStacked()) {
336
+ return cn('block shrink-0 border-b border-border bg-background',
337
+ // Saat seluruh page yang scroll, filter dipin ke atas area scroll agar tetap terjangkau.
338
+ scroll === 'page' && 'sticky top-0 z-10', this.class(),
339
+ // Collapsible & tertutup → `hidden` (menang atas `block`); content mengisi ruangnya kembali.
340
+ this.isCollapsedStacked() && 'hidden');
341
+ }
342
+ // SIDE: kolom di samping content.
343
+ return cn('block min-h-0 border-border bg-background', scroll === 'content' && 'h-full overflow-auto', scroll === 'page' && 'overflow-visible',
344
+ // sticky → mendorong content (push); urutan kolom di-handle oleh `order-last` saat di kanan.
345
+ this.isSticky() && 'shrink-0 w-[var(--page-filter-width)]', this.isSticky() && position === 'left' && 'border-r', this.isSticky() && position === 'right' && 'order-last border-l',
346
+ // drawer → meluncur dari tepi content region, mengambang di atas content.
347
+ this.isDrawer() &&
348
+ 'absolute inset-y-0 z-20 w-[var(--page-filter-width)] border shadow-sm transition-transform duration-200 ease-out', this.isDrawer() &&
349
+ position === 'left' &&
350
+ (open ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isDrawer() &&
351
+ position === 'right' &&
352
+ (open ? 'right-0 translate-x-0' : 'right-0 translate-x-full'),
353
+ // overlay → seperti drawer, tapi di atas drawer (z lebih tinggi) dengan backdrop.
354
+ this.isOverlay() &&
355
+ 'absolute inset-y-0 z-30 w-[var(--page-filter-width)] border shadow-md transition-transform duration-200 ease-out', this.isOverlay() &&
356
+ position === 'left' &&
357
+ (open ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isOverlay() &&
358
+ position === 'right' &&
359
+ (open ? 'right-0 translate-x-0' : 'right-0 translate-x-full'), this.class());
360
+ }, /* @ts-ignore */
361
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
362
+ constructor() {
363
+ setupOverlayFocusManagement({
364
+ document: this.document,
365
+ host: this.host.nativeElement,
366
+ isOpen: this.page.filterOpen,
367
+ isInteractive: this.isInteractive,
368
+ });
369
+ effect(() => {
370
+ this.page.registerFilter({
371
+ placement: this.resolvedPlacement(),
372
+ mode: this.effectiveMode(),
373
+ position: this.resolvedPosition(),
374
+ width: this.resolvedWidth(),
375
+ id: this.resolvedId,
376
+ });
377
+ });
378
+ effect((onCleanup) => {
379
+ if (!this.closeOnEsc() || this.isSticky()) {
380
+ return;
381
+ }
382
+ const defaultView = this.document?.defaultView;
383
+ if (!defaultView) {
384
+ return;
385
+ }
386
+ const handler = (event) => {
387
+ if (event.key !== 'Escape') {
388
+ return;
389
+ }
390
+ if (!untracked(() => this.page.filterOpen())) {
391
+ return;
392
+ }
393
+ untracked(() => {
394
+ this.page.closeFilter();
395
+ });
396
+ };
397
+ defaultView.addEventListener('keydown', handler);
398
+ onCleanup(() => defaultView.removeEventListener('keydown', handler));
399
+ });
400
+ }
401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
402
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageFilterComponent, isStandalone: true, selector: "PageFilter", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, closeOnEsc: { classPropertyName: "closeOnEsc", publicName: "closeOnEsc", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "resolvedId", "attr.data-page-slot": "\"filter\"", "attr.data-page-filter-placement": "resolvedPlacement()", "attr.data-page-filter-mode": "effectiveMode()", "attr.data-page-filter-open": "page.filterOpen()", "attr.data-page-position": "resolvedPosition()", "attr.aria-hidden": "ariaHidden()", "attr.inert": "inertWhenClosed()", "attr.role": "dialogRole()", "attr.aria-modal": "ariaModal()", "attr.aria-label": "dialogLabel()", "attr.tabindex": "dialogTabindex()", "style.--page-filter-width": "resolvedWidth()" } }, ngImport: i0, template: `<ng-content />`, isInline: true });
403
+ }
404
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterComponent, decorators: [{
405
+ type: Component,
406
+ args: [{
407
+ selector: 'PageFilter',
408
+ host: {
409
+ '[class]': 'classes()',
410
+ '[attr.id]': 'resolvedId',
411
+ '[attr.data-page-slot]': '"filter"',
412
+ '[attr.data-page-filter-placement]': 'resolvedPlacement()',
413
+ '[attr.data-page-filter-mode]': 'effectiveMode()',
414
+ '[attr.data-page-filter-open]': 'page.filterOpen()',
415
+ '[attr.data-page-position]': 'resolvedPosition()',
416
+ '[attr.aria-hidden]': 'ariaHidden()',
417
+ '[attr.inert]': 'inertWhenClosed()',
418
+ '[attr.role]': 'dialogRole()',
419
+ '[attr.aria-modal]': 'ariaModal()',
420
+ '[attr.aria-label]': 'dialogLabel()',
421
+ '[attr.tabindex]': 'dialogTabindex()',
422
+ '[style.--page-filter-width]': 'resolvedWidth()',
423
+ },
424
+ template: `<ng-content />`,
425
+ }]
426
+ }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], closeOnEsc: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEsc", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
427
+
139
428
  function buildPageBodyClasses(scroll, customClass) {
140
429
  return cn('block min-w-0', scroll === 'content' && 'h-full min-h-0 overflow-auto', scroll === 'page' && 'overflow-visible', customClass);
141
430
  }
@@ -157,14 +446,13 @@ class PageHeaderComponent {
157
446
  // appearance unifies the border with the layout/nav: border-rail = 1.5px.
158
447
  this.isBorderRail() ? 'border-b-[1.5px]' : 'border-b', this.class(), this.resolvedHeight() === 'fix' && 'h-12 overflow-hidden'), /* @ts-ignore */
159
448
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
160
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
161
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: PageHeaderComponent, isStandalone: true, selector: "PageHeader", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "header" }, properties: { "class": "classes()", "style.padding-right": "appsLauncherReservePadding()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
449
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
450
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageHeaderComponent, isStandalone: true, selector: "PageHeader", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "header" }, properties: { "class": "classes()", "style.padding-right": "appsLauncherReservePadding()" } }, ngImport: i0, template: `<ng-content />`, isInline: true });
162
451
  }
163
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageHeaderComponent, decorators: [{
452
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageHeaderComponent, decorators: [{
164
453
  type: Component,
165
454
  args: [{
166
455
  selector: 'PageHeader',
167
- changeDetection: ChangeDetectionStrategy.OnPush,
168
456
  host: {
169
457
  '[class]': 'classes()',
170
458
  // Inline style menang atas utility responsif konsumen (mis. `md:px-6`), jadi ruang
@@ -183,14 +471,13 @@ class PageContentComponent {
183
471
  ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
184
472
  classes = computed(() => buildPageBodyClasses(this.resolvedScroll(), this.class()), /* @ts-ignore */
185
473
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
186
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
187
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: PageContentComponent, isStandalone: true, selector: "PageContent", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "content" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
474
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
475
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageContentComponent, isStandalone: true, selector: "PageContent", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "content" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true });
188
476
  }
189
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageContentComponent, decorators: [{
477
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageContentComponent, decorators: [{
190
478
  type: Component,
191
479
  args: [{
192
480
  selector: 'PageContent',
193
- changeDetection: ChangeDetectionStrategy.OnPush,
194
481
  host: {
195
482
  '[class]': 'classes()',
196
483
  'data-page-slot': 'content',
@@ -206,14 +493,13 @@ class PageDashboardComponent {
206
493
  ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
207
494
  classes = computed(() => buildPageBodyClasses(this.resolvedScroll(), this.class()), /* @ts-ignore */
208
495
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
209
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
210
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: PageDashboardComponent, isStandalone: true, selector: "PageDashboard", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "dashboard" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
496
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
497
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageDashboardComponent, isStandalone: true, selector: "PageDashboard", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "dashboard" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true });
211
498
  }
212
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageDashboardComponent, decorators: [{
499
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageDashboardComponent, decorators: [{
213
500
  type: Component,
214
501
  args: [{
215
502
  selector: 'PageDashboard',
216
- changeDetection: ChangeDetectionStrategy.OnPush,
217
503
  host: {
218
504
  '[class]': 'classes()',
219
505
  'data-page-slot': 'dashboard',
@@ -233,14 +519,13 @@ class PageFooterComponent {
233
519
  // appearance unifies the border with the layout/nav: border-rail = 1.5px.
234
520
  this.isBorderRail() ? 'border-t-[1.5px]' : 'border-t', this.class(), this.resolvedHeight() === 'fix' && 'h-12 overflow-hidden'), /* @ts-ignore */
235
521
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
236
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
237
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: PageFooterComponent, isStandalone: true, selector: "PageFooter", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "footer" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
522
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
523
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageFooterComponent, isStandalone: true, selector: "PageFooter", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-page-slot": "footer" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true });
238
524
  }
239
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageFooterComponent, decorators: [{
525
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFooterComponent, decorators: [{
240
526
  type: Component,
241
527
  args: [{
242
528
  selector: 'PageFooter',
243
- changeDetection: ChangeDetectionStrategy.OnPush,
244
529
  host: {
245
530
  '[class]': 'classes()',
246
531
  'data-page-slot': 'footer',
@@ -252,6 +537,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
252
537
  let nextPageSideId = 0;
253
538
  class PageSideComponent {
254
539
  document = inject(DOCUMENT, { optional: true });
540
+ host = inject((ElementRef));
255
541
  page = inject(PageStateService);
256
542
  resolvedId = `page-side-${++nextPageSideId}`;
257
543
  mode = input('sticky', /* @ts-ignore */
@@ -262,6 +548,9 @@ class PageSideComponent {
262
548
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
263
549
  closeOnEsc = input(true, /* @ts-ignore */
264
550
  ...(ngDevMode ? [{ debugName: "closeOnEsc" }] : /* istanbul ignore next */ []));
551
+ /** Nama aksesibilitas untuk panel saat mode `drawer`/`overlay` (dipasang sebagai `aria-label` dialog). */
552
+ ariaLabel = input('Side panel', /* @ts-ignore */
553
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
265
554
  class = input('', /* @ts-ignore */
266
555
  ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
267
556
  resolvedMode = computed(() => this.mode() ?? this.page.sideMode(), /* @ts-ignore */
@@ -276,19 +565,49 @@ class PageSideComponent {
276
565
  ...(ngDevMode ? [{ debugName: "isDrawer" }] : /* istanbul ignore next */ []));
277
566
  isOverlay = computed(() => this.resolvedMode() === 'overlay', /* @ts-ignore */
278
567
  ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
568
+ /** Mode mengambang (drawer/overlay) — bersifat dialog dan butuh manajemen fokus + `inert` saat tertutup. */
569
+ isInteractive = computed(() => this.isDrawer() || this.isOverlay(), /* @ts-ignore */
570
+ ...(ngDevMode ? [{ debugName: "isInteractive" }] : /* istanbul ignore next */ []));
279
571
  resolvedScroll = computed(() => this.page.scroll() ?? PAGE_DEFAULT_SCROLL, /* @ts-ignore */
280
572
  ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
281
- ariaHidden = computed(() => (!this.isSticky() && !this.page.sideOpen() ? 'true' : null), /* @ts-ignore */
573
+ ariaHidden = computed(() => !this.isSticky() && !this.page.sideOpen() ? 'true' : null, /* @ts-ignore */
282
574
  ...(ngDevMode ? [{ debugName: "ariaHidden" }] : /* istanbul ignore next */ []));
575
+ /** Saat panel mengambang tertutup, `inert` mengeluarkan isinya dari tab order & pohon aksesibilitas (AXE). */
576
+ inertWhenClosed = computed(() => this.isInteractive() && !this.page.sideOpen() ? '' : null, /* @ts-ignore */
577
+ ...(ngDevMode ? [{ debugName: "inertWhenClosed" }] : /* istanbul ignore next */ []));
578
+ dialogRole = computed(() => (this.isInteractive() ? 'dialog' : null), /* @ts-ignore */
579
+ ...(ngDevMode ? [{ debugName: "dialogRole" }] : /* istanbul ignore next */ []));
580
+ ariaModal = computed(() => (this.isOverlay() ? 'true' : null), /* @ts-ignore */
581
+ ...(ngDevMode ? [{ debugName: "ariaModal" }] : /* istanbul ignore next */ []));
582
+ dialogLabel = computed(() => (this.isInteractive() ? this.ariaLabel() : null), /* @ts-ignore */
583
+ ...(ngDevMode ? [{ debugName: "dialogLabel" }] : /* istanbul ignore next */ []));
584
+ dialogTabindex = computed(() => (this.isInteractive() ? '-1' : null), /* @ts-ignore */
585
+ ...(ngDevMode ? [{ debugName: "dialogTabindex" }] : /* istanbul ignore next */ []));
283
586
  classes = computed(() => {
284
587
  const position = this.resolvedPosition();
285
588
  const sideOpen = this.page.sideOpen();
286
- return cn('block min-h-0 border-border bg-background', this.resolvedScroll() === 'content' && 'h-full overflow-auto', this.resolvedScroll() === 'page' && 'overflow-visible', this.isSticky() && 'shrink-0 w-[var(--page-side-width)]', this.isSticky() && position === 'left' && 'order-1 border-r', this.isSticky() && position === 'right' && 'order-2 border-l', this.isDrawer() &&
287
- 'absolute inset-y-0 z-20 w-[var(--page-side-width)] border shadow-sm transition-transform duration-200 ease-out', this.isDrawer() && position === 'left' && (sideOpen ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isDrawer() && position === 'right' && (sideOpen ? 'right-0 translate-x-0' : 'right-0 translate-x-full'), this.isOverlay() &&
288
- 'absolute inset-y-0 z-30 w-[var(--page-side-width)] border shadow-md transition-transform duration-200 ease-out', this.isOverlay() && position === 'left' && (sideOpen ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isOverlay() && position === 'right' && (sideOpen ? 'right-0 translate-x-0' : 'right-0 translate-x-full'), this.class());
589
+ return cn('block min-h-0 border-border bg-background', this.resolvedScroll() === 'content' && 'h-full overflow-auto', this.resolvedScroll() === 'page' && 'overflow-visible', this.isSticky() && 'shrink-0 w-[var(--page-side-width)]',
590
+ // Urutan DOM body = rail-lalu-content; `left` tak perlu reorder, `right` mendorong rail ke kolom kedua.
591
+ this.isSticky() && position === 'left' && 'border-r', this.isSticky() && position === 'right' && 'order-last border-l', this.isDrawer() &&
592
+ 'absolute inset-y-0 z-20 w-[var(--page-side-width)] border shadow-sm transition-transform duration-200 ease-out', this.isDrawer() &&
593
+ position === 'left' &&
594
+ (sideOpen ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isDrawer() &&
595
+ position === 'right' &&
596
+ (sideOpen ? 'right-0 translate-x-0' : 'right-0 translate-x-full'), this.isOverlay() &&
597
+ 'absolute inset-y-0 z-30 w-[var(--page-side-width)] border shadow-md transition-transform duration-200 ease-out', this.isOverlay() &&
598
+ position === 'left' &&
599
+ (sideOpen ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isOverlay() &&
600
+ position === 'right' &&
601
+ (sideOpen ? 'right-0 translate-x-0' : 'right-0 translate-x-full'), this.class());
289
602
  }, /* @ts-ignore */
290
603
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
291
604
  constructor() {
605
+ setupOverlayFocusManagement({
606
+ document: this.document,
607
+ host: this.host.nativeElement,
608
+ isOpen: this.page.sideOpen,
609
+ isInteractive: this.isInteractive,
610
+ });
292
611
  effect(() => {
293
612
  this.page.registerSide({
294
613
  mode: this.resolvedMode(),
@@ -320,14 +639,13 @@ class PageSideComponent {
320
639
  onCleanup(() => defaultView.removeEventListener('keydown', handler));
321
640
  });
322
641
  }
323
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageSideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
324
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: PageSideComponent, isStandalone: true, selector: "PageSide", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, closeOnEsc: { classPropertyName: "closeOnEsc", publicName: "closeOnEsc", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "resolvedId", "attr.data-page-slot": "\"side\"", "attr.data-page-side-mode": "resolvedMode()", "attr.data-page-side-open": "page.sideOpen()", "attr.data-page-position": "resolvedPosition()", "attr.aria-hidden": "ariaHidden()", "style.--page-side-width": "resolvedWidth()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
642
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
643
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageSideComponent, isStandalone: true, selector: "PageSide", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, closeOnEsc: { classPropertyName: "closeOnEsc", publicName: "closeOnEsc", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "resolvedId", "attr.data-page-slot": "\"side\"", "attr.data-page-side-mode": "resolvedMode()", "attr.data-page-side-open": "page.sideOpen()", "attr.data-page-position": "resolvedPosition()", "attr.aria-hidden": "ariaHidden()", "attr.inert": "inertWhenClosed()", "attr.role": "dialogRole()", "attr.aria-modal": "ariaModal()", "attr.aria-label": "dialogLabel()", "attr.tabindex": "dialogTabindex()", "style.--page-side-width": "resolvedWidth()" } }, ngImport: i0, template: `<ng-content />`, isInline: true });
325
644
  }
326
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageSideComponent, decorators: [{
645
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideComponent, decorators: [{
327
646
  type: Component,
328
647
  args: [{
329
648
  selector: 'PageSide',
330
- changeDetection: ChangeDetectionStrategy.OnPush,
331
649
  host: {
332
650
  '[class]': 'classes()',
333
651
  '[attr.id]': 'resolvedId',
@@ -336,11 +654,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
336
654
  '[attr.data-page-side-open]': 'page.sideOpen()',
337
655
  '[attr.data-page-position]': 'resolvedPosition()',
338
656
  '[attr.aria-hidden]': 'ariaHidden()',
657
+ '[attr.inert]': 'inertWhenClosed()',
658
+ '[attr.role]': 'dialogRole()',
659
+ '[attr.aria-modal]': 'ariaModal()',
660
+ '[attr.aria-label]': 'dialogLabel()',
661
+ '[attr.tabindex]': 'dialogTabindex()',
339
662
  '[style.--page-side-width]': 'resolvedWidth()',
340
663
  },
341
664
  template: `<ng-content />`,
342
665
  }]
343
- }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], closeOnEsc: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEsc", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
666
+ }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], closeOnEsc: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEsc", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
344
667
 
345
668
  /** Penghitung instance untuk id unik tiap apps-launcher (hindari bentrok `claimId` saat beberapa `Page` hidup). */
346
669
  let pageAppsLauncherInstanceId = 0;
@@ -353,6 +676,8 @@ class PageComponent {
353
676
  ...(ngDevMode ? [{ debugName: "projectedSide" }] : /* istanbul ignore next */ []));
354
677
  projectedHeader = contentChild(PageHeaderComponent, /* @ts-ignore */
355
678
  ...(ngDevMode ? [{ debugName: "projectedHeader" }] : /* istanbul ignore next */ []));
679
+ projectedFilter = contentChild(PageFilterComponent, /* @ts-ignore */
680
+ ...(ngDevMode ? [{ debugName: "projectedFilter" }] : /* istanbul ignore next */ []));
356
681
  variant = input(PAGE_DEFAULT_VARIANT, /* @ts-ignore */
357
682
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
358
683
  height = input(PAGE_DEFAULT_HEIGHT, /* @ts-ignore */
@@ -370,9 +695,13 @@ class PageComponent {
370
695
  ...(ngDevMode ? [{ debugName: "sideOpen" }] : /* istanbul ignore next */ []));
371
696
  sideWidth = input(PAGE_DEFAULT_SIDE_WIDTH, /* @ts-ignore */
372
697
  ...(ngDevMode ? [{ debugName: "sideWidth" }] : /* istanbul ignore next */ []));
698
+ /** Controlled state untuk `PageFilter` drawer/overlay. `null` = uncontrolled (dikelola toggle/backdrop/Esc). */
699
+ filterOpen = input(null, /* @ts-ignore */
700
+ ...(ngDevMode ? [{ debugName: "filterOpen" }] : /* istanbul ignore next */ []));
373
701
  class = input('', /* @ts-ignore */
374
702
  ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
375
703
  sideOpenChange = output();
704
+ filterOpenChange = output();
376
705
  /** Saat layout `empty`, munculkan tombol apps (flyout main navigation) di pojok kanan-atas. Set `false` untuk menonaktifkan. */
377
706
  appsLauncher = input(true, { ...(ngDevMode ? { debugName: "appsLauncher" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
378
707
  /** Id navigasi yang disurface oleh apps-launcher (default `main`). */
@@ -394,7 +723,29 @@ class PageComponent {
394
723
  ...(ngDevMode ? [{ debugName: "isLeftSide" }] : /* istanbul ignore next */ []));
395
724
  isRightSide = computed(() => this.resolvedPosition() === 'right', /* @ts-ignore */
396
725
  ...(ngDevMode ? [{ debugName: "isRightSide" }] : /* istanbul ignore next */ []));
397
- showsOverlayBackdrop = computed(() => this.variant() === 'side' && this.resolvedSideMode() === 'overlay' && this.page.sideOpen(), /* @ts-ignore */
726
+ // Filter resolusi dibaca langsung dari `PageFilter` yang diprojeksikan (reactive content query).
727
+ // Resolusi filter dibaca dari `PageFilter` yang diprojeksikan, lalu fall back ke page-state
728
+ // (diisi `registerFilter`), lalu default — pola seragam agar Page & PageFilter selalu sepakat.
729
+ hasFilter = computed(() => this.projectedFilter() !== undefined, /* @ts-ignore */
730
+ ...(ngDevMode ? [{ debugName: "hasFilter" }] : /* istanbul ignore next */ []));
731
+ filterPlacement = computed(() => this.projectedFilter()?.placement() ??
732
+ this.page.filterPlacement() ??
733
+ PAGE_DEFAULT_FILTER_PLACEMENT, /* @ts-ignore */
734
+ ...(ngDevMode ? [{ debugName: "filterPlacement" }] : /* istanbul ignore next */ []));
735
+ /** Mode efektif: `stacked` selalu `sticky`; `side` memakai mode pada `PageFilter`. */
736
+ filterMode = computed(() => this.filterPlacement() === 'stacked'
737
+ ? 'sticky'
738
+ : (this.projectedFilter()?.mode() ?? this.page.filterMode()), /* @ts-ignore */
739
+ ...(ngDevMode ? [{ debugName: "filterMode" }] : /* istanbul ignore next */ []));
740
+ filterPosition = computed(() => this.projectedFilter()?.position() ?? this.page.filterPosition(), /* @ts-ignore */
741
+ ...(ngDevMode ? [{ debugName: "filterPosition" }] : /* istanbul ignore next */ []));
742
+ filterWidthVar = computed(() => this.projectedFilter()?.width() ?? this.page.filterWidth() ?? PAGE_DEFAULT_FILTER_WIDTH, /* @ts-ignore */
743
+ ...(ngDevMode ? [{ debugName: "filterWidthVar" }] : /* istanbul ignore next */ []));
744
+ showsSideOverlayBackdrop = computed(() => this.variant() === 'side' && this.resolvedSideMode() === 'overlay' && this.page.sideOpen(), /* @ts-ignore */
745
+ ...(ngDevMode ? [{ debugName: "showsSideOverlayBackdrop" }] : /* istanbul ignore next */ []));
746
+ showsFilterOverlayBackdrop = computed(() => this.hasFilter() && this.filterMode() === 'overlay' && this.page.filterOpen(), /* @ts-ignore */
747
+ ...(ngDevMode ? [{ debugName: "showsFilterOverlayBackdrop" }] : /* istanbul ignore next */ []));
748
+ showsOverlayBackdrop = computed(() => this.showsSideOverlayBackdrop() || this.showsFilterOverlayBackdrop(), /* @ts-ignore */
398
749
  ...(ngDevMode ? [{ debugName: "showsOverlayBackdrop" }] : /* istanbul ignore next */ []));
399
750
  /** Signal data untuk id yang dipilih; di-recompute hanya saat `appsNavId` berubah. */
400
751
  appsNavSource = computed(() => this.navigation?.data(this.appsNavId()) ?? null, /* @ts-ignore */
@@ -437,6 +788,35 @@ class PageComponent {
437
788
  return cn('relative min-w-0', this.scroll() === 'content' && 'min-h-0');
438
789
  }, /* @ts-ignore */
439
790
  ...(ngDevMode ? [{ debugName: "bodyClasses" }] : /* istanbul ignore next */ []));
791
+ /**
792
+ * Wrapper di sekitar `PageFilter` + `PageContent`. Tanpa filter, memakai `display: contents`
793
+ * agar transparan (perilaku body lama dipertahankan persis). Dengan filter, menjadi grid/flex
794
+ * yang menata filter (baris untuk `stacked`, kolom untuk `side`); drawer/overlay menjadikannya
795
+ * `relative` sebagai positioning context untuk panel yang mengambang.
796
+ */
797
+ contentRegionClasses = computed(() => {
798
+ if (!this.hasFilter()) {
799
+ return 'contents';
800
+ }
801
+ const scroll = this.scroll();
802
+ const mode = this.filterMode();
803
+ // drawer/overlay (side only): content mengisi penuh, filter mengambang di atasnya.
804
+ if (mode !== 'sticky') {
805
+ return cn('relative min-w-0', scroll === 'content' && 'min-h-0 h-full');
806
+ }
807
+ if (this.filterPlacement() === 'stacked') {
808
+ // content scroll: filter (auto) di atas, content (1fr) mengisi sisa tinggi & scroll sendiri.
809
+ // page scroll: aliran block biasa; filter dipin via `sticky top-0` pada elemennya.
810
+ return scroll === 'content'
811
+ ? 'grid min-w-0 min-h-0 h-full grid-rows-[auto_minmax(0,1fr)]'
812
+ : 'block min-w-0';
813
+ }
814
+ // side + sticky: filter sebagai kolom; posisi kanan dibalik via `order-last` pada elemen filter.
815
+ return cn('grid min-w-0', scroll === 'content' && 'min-h-0 h-full', this.filterPosition() === 'left'
816
+ ? 'grid-cols-[var(--page-filter-width)_minmax(0,1fr)]'
817
+ : 'grid-cols-[minmax(0,1fr)_var(--page-filter-width)]');
818
+ }, /* @ts-ignore */
819
+ ...(ngDevMode ? [{ debugName: "contentRegionClasses" }] : /* istanbul ignore next */ []));
440
820
  constructor() {
441
821
  effect(() => {
442
822
  this.page.registerRoot({
@@ -452,6 +832,9 @@ class PageComponent {
452
832
  effect(() => {
453
833
  this.page.setControlledSideOpen(this.sideOpen());
454
834
  });
835
+ effect(() => {
836
+ this.page.setControlledFilterOpen(this.filterOpen());
837
+ });
455
838
  // Header memesan ruang kanan hanya saat tombol apps benar-benar mengambang di atas header.
456
839
  effect(() => {
457
840
  this.page.setAppsLauncherReserve(this.showsAppsLauncher() && this.projectedHeader() !== undefined);
@@ -463,31 +846,47 @@ class PageComponent {
463
846
  }
464
847
  this.sideOpenChange.emit(this.page.sideOpenRequest() ?? false);
465
848
  });
849
+ effect(() => {
850
+ const requestVersion = this.page.filterOpenRequestVersion();
851
+ if (requestVersion === 0) {
852
+ return;
853
+ }
854
+ this.filterOpenChange.emit(this.page.filterOpenRequest() ?? false);
855
+ });
466
856
  }
467
857
  handleBackdropClick() {
468
- this.page.closeSide();
858
+ if (this.showsSideOverlayBackdrop()) {
859
+ this.page.closeSide();
860
+ }
861
+ if (this.showsFilterOverlayBackdrop()) {
862
+ this.page.closeFilter();
863
+ }
469
864
  }
470
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
471
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.3", type: PageComponent, isStandalone: true, selector: "Page", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, scroll: { classPropertyName: "scroll", publicName: "scroll", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, sideMode: { classPropertyName: "sideMode", publicName: "sideMode", isSignal: true, isRequired: false, transformFunction: null }, sideOpen: { classPropertyName: "sideOpen", publicName: "sideOpen", isSignal: true, isRequired: false, transformFunction: null }, sideWidth: { classPropertyName: "sideWidth", publicName: "sideWidth", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, appsLauncher: { classPropertyName: "appsLauncher", publicName: "appsLauncher", isSignal: true, isRequired: false, transformFunction: null }, appsNavId: { classPropertyName: "appsNavId", publicName: "appsNavId", isSignal: true, isRequired: false, transformFunction: null }, appsIcon: { classPropertyName: "appsIcon", publicName: "appsIcon", isSignal: true, isRequired: false, transformFunction: null }, appsLabel: { classPropertyName: "appsLabel", publicName: "appsLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sideOpenChange: "sideOpenChange" }, host: { properties: { "class": "hostClasses()", "attr.data-page-variant": "variant()", "attr.data-page-height": "height()", "attr.data-page-scroll": "scroll()", "attr.data-page-appearance": "appearance()", "attr.data-page-position": "resolvedPosition()", "attr.data-page-side-mode": "resolvedSideMode()", "attr.data-page-side-open": "page.sideOpen()", "style.--page-side-width": "sideWidth()" } }, providers: [PageStateService], queries: [{ propertyName: "projectedSide", first: true, predicate: PageSideComponent, descendants: true, isSignal: true }, { propertyName: "projectedHeader", first: true, predicate: PageHeaderComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
865
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
866
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: PageComponent, isStandalone: true, selector: "Page", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, scroll: { classPropertyName: "scroll", publicName: "scroll", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, sideMode: { classPropertyName: "sideMode", publicName: "sideMode", isSignal: true, isRequired: false, transformFunction: null }, sideOpen: { classPropertyName: "sideOpen", publicName: "sideOpen", isSignal: true, isRequired: false, transformFunction: null }, sideWidth: { classPropertyName: "sideWidth", publicName: "sideWidth", isSignal: true, isRequired: false, transformFunction: null }, filterOpen: { classPropertyName: "filterOpen", publicName: "filterOpen", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, appsLauncher: { classPropertyName: "appsLauncher", publicName: "appsLauncher", isSignal: true, isRequired: false, transformFunction: null }, appsNavId: { classPropertyName: "appsNavId", publicName: "appsNavId", isSignal: true, isRequired: false, transformFunction: null }, appsIcon: { classPropertyName: "appsIcon", publicName: "appsIcon", isSignal: true, isRequired: false, transformFunction: null }, appsLabel: { classPropertyName: "appsLabel", publicName: "appsLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sideOpenChange: "sideOpenChange", filterOpenChange: "filterOpenChange" }, host: { properties: { "class": "hostClasses()", "attr.data-page-variant": "variant()", "attr.data-page-height": "height()", "attr.data-page-scroll": "scroll()", "attr.data-page-appearance": "appearance()", "attr.data-page-position": "resolvedPosition()", "attr.data-page-side-mode": "resolvedSideMode()", "attr.data-page-side-open": "page.sideOpen()", "attr.data-page-filter-placement": "hasFilter() ? filterPlacement() : null", "attr.data-page-filter-mode": "hasFilter() ? filterMode() : null", "attr.data-page-filter-open": "hasFilter() ? page.filterOpen() : null", "style.--page-side-width": "sideWidth()", "style.--page-filter-width": "filterWidthVar()" } }, providers: [PageStateService], queries: [{ propertyName: "projectedSide", first: true, predicate: PageSideComponent, descendants: true, isSignal: true }, { propertyName: "projectedHeader", first: true, predicate: PageHeaderComponent, descendants: true, isSignal: true }, { propertyName: "projectedFilter", first: true, predicate: PageFilterComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
472
867
  @if (showsOverlayBackdrop()) {
473
868
  <button
474
869
  type="button"
475
- aria-label="Close page side"
870
+ aria-label="Close overlay panel"
476
871
  data-page-overlay-backdrop
477
872
  class="absolute inset-0 z-20 bg-[hsl(var(--overlay-backdrop))]"
478
- (click)="handleBackdropClick()"></button>
873
+ (click)="handleBackdropClick()"
874
+ ></button>
479
875
  }
480
876
 
481
877
  <div [class]="shellClasses()">
482
878
  <ng-content select="PageHeader" />
483
- <ng-content select="PageSideToggle" />
879
+ <ng-content select="PageSideToggle, PageFilterToggle" />
484
880
 
485
- <div [class]="bodyClasses()">
881
+ <div [class]="bodyClasses()" data-page-body>
486
882
  @if (variant() === 'side') {
487
883
  <ng-content select="PageSide" />
488
884
  }
489
885
 
490
- <ng-content select="PageContent, PageDashboard" />
886
+ <div [class]="contentRegionClasses()" data-page-content-region>
887
+ <ng-content select="PageFilter" />
888
+ <ng-content select="PageContent, PageDashboard" />
889
+ </div>
491
890
  </div>
492
891
 
493
892
  <ng-content select="PageFooter" />
@@ -498,13 +897,15 @@ class PageComponent {
498
897
  [id]="launcherNavId"
499
898
  [data]="appsNavData()"
500
899
  ariaLabel="Application navigation"
501
- [class]="appsLauncherHostClass()">
900
+ [class]="appsLauncherHostClass()"
901
+ >
502
902
  <NavigationFlyout
503
903
  [icon]="appsIcon()"
504
904
  icon-only
505
905
  trigger-variant="plain"
506
906
  [nav-appearance]="appsLauncherAppearance()"
507
- [label]="appsLabel()">
907
+ [label]="appsLabel()"
908
+ >
508
909
  @if (appsBrand(); as brand) {
509
910
  <NavigationHeader>
510
911
  <LayoutBrand [brand]="brand" />
@@ -518,13 +919,12 @@ class PageComponent {
518
919
  </NavigationFlyout>
519
920
  </Navigation>
520
921
  }
521
- `, isInline: true, dependencies: [{ kind: "component", type: NavigationContainerComponent, selector: "Navigation", inputs: ["id", "data", "ariaLabel", "compact", "collapse-tree", "class", "itemClass", "nav-group-class", "activeIds", "activeUrl", "openedIds"], outputs: ["openedIdsChange", "itemSelected"] }, { kind: "component", type: NavigationFlyoutComponent, selector: "NavigationFlyout", inputs: ["label", "icon", "icon-only", "icon-position", "trigger-variant", "trigger-floating", "trigger-class", "nav-position", "nav-appearance", "class"] }, { kind: "component", type: NavigationHeaderComponent, selector: "NavigationHeader", inputs: ["toggle", "class"] }, { kind: "component", type: NavigationFooterComponent, selector: "NavigationFooter", inputs: ["class"] }, { kind: "component", type: LayoutBrand, selector: "LayoutBrand", inputs: ["brand", "compact"] }, { kind: "component", type: LayoutUser, selector: "LayoutUser", inputs: ["user", "detailed", "logoutLabel", "logoutIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
922
+ `, isInline: true, dependencies: [{ kind: "component", type: NavigationContainerComponent, selector: "Navigation", inputs: ["id", "data", "ariaLabel", "compact", "collapse-tree", "class", "itemClass", "nav-group-class", "activeIds", "activeUrl", "openedIds"], outputs: ["openedIdsChange", "itemSelected"] }, { kind: "component", type: NavigationFlyoutComponent, selector: "NavigationFlyout", inputs: ["label", "icon", "icon-only", "icon-position", "trigger-variant", "trigger-floating", "trigger-class", "nav-position", "nav-appearance", "class"] }, { kind: "component", type: NavigationHeaderComponent, selector: "NavigationHeader", inputs: ["toggle", "class"] }, { kind: "component", type: NavigationFooterComponent, selector: "NavigationFooter", inputs: ["class"] }, { kind: "component", type: LayoutBrand, selector: "LayoutBrand", inputs: ["brand", "compact"] }, { kind: "component", type: LayoutUser, selector: "LayoutUser", inputs: ["user", "detailed", "logoutLabel", "logoutIcon"] }] });
522
923
  }
523
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageComponent, decorators: [{
924
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageComponent, decorators: [{
524
925
  type: Component,
525
926
  args: [{
526
927
  selector: 'Page',
527
- changeDetection: ChangeDetectionStrategy.OnPush,
528
928
  providers: [PageStateService],
529
929
  imports: [
530
930
  NavigationContainerComponent,
@@ -543,28 +943,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
543
943
  '[attr.data-page-position]': 'resolvedPosition()',
544
944
  '[attr.data-page-side-mode]': 'resolvedSideMode()',
545
945
  '[attr.data-page-side-open]': 'page.sideOpen()',
946
+ '[attr.data-page-filter-placement]': 'hasFilter() ? filterPlacement() : null',
947
+ '[attr.data-page-filter-mode]': 'hasFilter() ? filterMode() : null',
948
+ '[attr.data-page-filter-open]': 'hasFilter() ? page.filterOpen() : null',
546
949
  '[style.--page-side-width]': 'sideWidth()',
950
+ '[style.--page-filter-width]': 'filterWidthVar()',
547
951
  },
548
952
  template: `
549
953
  @if (showsOverlayBackdrop()) {
550
954
  <button
551
955
  type="button"
552
- aria-label="Close page side"
956
+ aria-label="Close overlay panel"
553
957
  data-page-overlay-backdrop
554
958
  class="absolute inset-0 z-20 bg-[hsl(var(--overlay-backdrop))]"
555
- (click)="handleBackdropClick()"></button>
959
+ (click)="handleBackdropClick()"
960
+ ></button>
556
961
  }
557
962
 
558
963
  <div [class]="shellClasses()">
559
964
  <ng-content select="PageHeader" />
560
- <ng-content select="PageSideToggle" />
965
+ <ng-content select="PageSideToggle, PageFilterToggle" />
561
966
 
562
- <div [class]="bodyClasses()">
967
+ <div [class]="bodyClasses()" data-page-body>
563
968
  @if (variant() === 'side') {
564
969
  <ng-content select="PageSide" />
565
970
  }
566
971
 
567
- <ng-content select="PageContent, PageDashboard" />
972
+ <div [class]="contentRegionClasses()" data-page-content-region>
973
+ <ng-content select="PageFilter" />
974
+ <ng-content select="PageContent, PageDashboard" />
975
+ </div>
568
976
  </div>
569
977
 
570
978
  <ng-content select="PageFooter" />
@@ -575,13 +983,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
575
983
  [id]="launcherNavId"
576
984
  [data]="appsNavData()"
577
985
  ariaLabel="Application navigation"
578
- [class]="appsLauncherHostClass()">
986
+ [class]="appsLauncherHostClass()"
987
+ >
579
988
  <NavigationFlyout
580
989
  [icon]="appsIcon()"
581
990
  icon-only
582
991
  trigger-variant="plain"
583
992
  [nav-appearance]="appsLauncherAppearance()"
584
- [label]="appsLabel()">
993
+ [label]="appsLabel()"
994
+ >
585
995
  @if (appsBrand(); as brand) {
586
996
  <NavigationHeader>
587
997
  <LayoutBrand [brand]="brand" />
@@ -597,7 +1007,87 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
597
1007
  }
598
1008
  `,
599
1009
  }]
600
- }], ctorParameters: () => [], propDecorators: { projectedSide: [{ type: i0.ContentChild, args: [i0.forwardRef(() => PageSideComponent), { isSignal: true }] }], projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => PageHeaderComponent), { isSignal: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], scroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "scroll", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], sideMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideMode", required: false }] }], sideOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOpen", required: false }] }], sideWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideWidth", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], sideOpenChange: [{ type: i0.Output, args: ["sideOpenChange"] }], appsLauncher: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsLauncher", required: false }] }], appsNavId: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsNavId", required: false }] }], appsIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsIcon", required: false }] }], appsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsLabel", required: false }] }] } });
1010
+ }], ctorParameters: () => [], propDecorators: { projectedSide: [{ type: i0.ContentChild, args: [i0.forwardRef(() => PageSideComponent), { isSignal: true }] }], projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => PageHeaderComponent), { isSignal: true }] }], projectedFilter: [{ type: i0.ContentChild, args: [i0.forwardRef(() => PageFilterComponent), { isSignal: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], scroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "scroll", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], sideMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideMode", required: false }] }], sideOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideOpen", required: false }] }], sideWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "sideWidth", required: false }] }], filterOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterOpen", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], sideOpenChange: [{ type: i0.Output, args: ["sideOpenChange"] }], filterOpenChange: [{ type: i0.Output, args: ["filterOpenChange"] }], appsLauncher: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsLauncher", required: false }] }], appsNavId: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsNavId", required: false }] }], appsIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsIcon", required: false }] }], appsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "appsLabel", required: false }] }] } });
1011
+
1012
+ /**
1013
+ * `PageFilterToggle` — tombol untuk membuka/menutup `PageFilter` dalam mode `drawer`/`overlay`.
1014
+ * Menerima projected content untuk label/ikon kustom; fallback memakai ikon funnel bawaan.
1015
+ */
1016
+ class PageFilterToggleComponent {
1017
+ page = inject(PageStateService);
1018
+ ariaLabel = input('Toggle page filter', /* @ts-ignore */
1019
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1020
+ class = input('', /* @ts-ignore */
1021
+ ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
1022
+ toggled = output();
1023
+ hostClasses = computed(() => cn('inline-flex shrink-0', this.class()), /* @ts-ignore */
1024
+ ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
1025
+ buttonClasses = computed(() => cn('inline-flex h-9 min-w-9 items-center justify-center rounded-md border border-border bg-background px-3 text-sm text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'), /* @ts-ignore */
1026
+ ...(ngDevMode ? [{ debugName: "buttonClasses" }] : /* istanbul ignore next */ []));
1027
+ handleClick() {
1028
+ this.toggled.emit(this.page.toggleFilter());
1029
+ }
1030
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1031
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageFilterToggleComponent, isStandalone: true, selector: "PageFilterToggle", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled" }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
1032
+ <button
1033
+ type="button"
1034
+ [class]="buttonClasses()"
1035
+ [attr.aria-label]="ariaLabel()"
1036
+ [attr.aria-controls]="page.filterId()"
1037
+ [attr.aria-expanded]="page.filterOpen()"
1038
+ (click)="handleClick()"
1039
+ >
1040
+ <ng-content>
1041
+ <svg
1042
+ aria-hidden="true"
1043
+ viewBox="0 0 24 24"
1044
+ class="h-4 w-4"
1045
+ fill="none"
1046
+ stroke="currentColor"
1047
+ stroke-width="2"
1048
+ stroke-linecap="round"
1049
+ stroke-linejoin="round"
1050
+ >
1051
+ <path d="M3 5h18l-7 8v5l-4 2v-7z" />
1052
+ </svg>
1053
+ </ng-content>
1054
+ </button>
1055
+ `, isInline: true });
1056
+ }
1057
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterToggleComponent, decorators: [{
1058
+ type: Component,
1059
+ args: [{
1060
+ selector: 'PageFilterToggle',
1061
+ host: {
1062
+ '[class]': 'hostClasses()',
1063
+ },
1064
+ template: `
1065
+ <button
1066
+ type="button"
1067
+ [class]="buttonClasses()"
1068
+ [attr.aria-label]="ariaLabel()"
1069
+ [attr.aria-controls]="page.filterId()"
1070
+ [attr.aria-expanded]="page.filterOpen()"
1071
+ (click)="handleClick()"
1072
+ >
1073
+ <ng-content>
1074
+ <svg
1075
+ aria-hidden="true"
1076
+ viewBox="0 0 24 24"
1077
+ class="h-4 w-4"
1078
+ fill="none"
1079
+ stroke="currentColor"
1080
+ stroke-width="2"
1081
+ stroke-linecap="round"
1082
+ stroke-linejoin="round"
1083
+ >
1084
+ <path d="M3 5h18l-7 8v5l-4 2v-7z" />
1085
+ </svg>
1086
+ </ng-content>
1087
+ </button>
1088
+ `,
1089
+ }]
1090
+ }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggled: [{ type: i0.Output, args: ["toggled"] }] } });
601
1091
 
602
1092
  class PageSideToggleComponent {
603
1093
  page = inject(PageStateService);
@@ -613,26 +1103,26 @@ class PageSideToggleComponent {
613
1103
  handleClick() {
614
1104
  this.toggled.emit(this.page.toggleSide());
615
1105
  }
616
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageSideToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
617
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: PageSideToggleComponent, isStandalone: true, selector: "PageSideToggle", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled" }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
1106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1107
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: PageSideToggleComponent, isStandalone: true, selector: "PageSideToggle", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggled: "toggled" }, host: { properties: { "class": "hostClasses()" } }, ngImport: i0, template: `
618
1108
  <button
619
1109
  type="button"
620
1110
  [class]="buttonClasses()"
621
1111
  [attr.aria-label]="ariaLabel()"
622
1112
  [attr.aria-controls]="page.sideId()"
623
1113
  [attr.aria-expanded]="page.sideOpen()"
624
- (click)="handleClick()">
1114
+ (click)="handleClick()"
1115
+ >
625
1116
  <ng-content>
626
1117
  <span aria-hidden="true" class="text-lg leading-none">☰</span>
627
1118
  </ng-content>
628
1119
  </button>
629
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1120
+ `, isInline: true });
630
1121
  }
631
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageSideToggleComponent, decorators: [{
1122
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideToggleComponent, decorators: [{
632
1123
  type: Component,
633
1124
  args: [{
634
1125
  selector: 'PageSideToggle',
635
- changeDetection: ChangeDetectionStrategy.OnPush,
636
1126
  host: {
637
1127
  '[class]': 'hostClasses()',
638
1128
  },
@@ -643,7 +1133,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
643
1133
  [attr.aria-label]="ariaLabel()"
644
1134
  [attr.aria-controls]="page.sideId()"
645
1135
  [attr.aria-expanded]="page.sideOpen()"
646
- (click)="handleClick()">
1136
+ (click)="handleClick()"
1137
+ >
647
1138
  <ng-content>
648
1139
  <span aria-hidden="true" class="text-lg leading-none">☰</span>
649
1140
  </ng-content>
@@ -656,4 +1147,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
656
1147
  * Generated bundle index. Do not edit.
657
1148
  */
658
1149
 
659
- export { PAGE_APPEARANCES, PAGE_DEFAULT_APPEARANCE, PAGE_DEFAULT_HEIGHT, PAGE_DEFAULT_SCROLL, PAGE_DEFAULT_SIDE_MODE, PAGE_DEFAULT_SIDE_POSITION, PAGE_DEFAULT_SIDE_WIDTH, PAGE_DEFAULT_VARIANT, PAGE_HEIGHT_VALUES, PAGE_SCROLL_VALUES, PAGE_SIDE_MODES, PAGE_SIDE_POSITIONS, PAGE_VARIANTS, PageComponent, PageContentComponent, PageDashboardComponent, PageFooterComponent, PageHeaderComponent, PageSideComponent, PageSideToggleComponent, isUiPageAppearance, isUiPageHeight, isUiPageScroll, isUiPageSideMode, isUiPageSidePosition, isUiPageVariant };
1150
+ export { PAGE_APPEARANCES, PAGE_DEFAULT_APPEARANCE, PAGE_DEFAULT_FILTER_MODE, PAGE_DEFAULT_FILTER_PLACEMENT, PAGE_DEFAULT_FILTER_POSITION, PAGE_DEFAULT_FILTER_WIDTH, PAGE_DEFAULT_HEIGHT, PAGE_DEFAULT_SCROLL, PAGE_DEFAULT_SIDE_MODE, PAGE_DEFAULT_SIDE_POSITION, PAGE_DEFAULT_SIDE_WIDTH, PAGE_DEFAULT_VARIANT, PAGE_FILTER_PLACEMENTS, PAGE_HEIGHT_VALUES, PAGE_SCROLL_VALUES, PAGE_SIDE_MODES, PAGE_SIDE_POSITIONS, PAGE_VARIANTS, PageComponent, PageContentComponent, PageDashboardComponent, PageFilterComponent, PageFilterToggleComponent, PageFooterComponent, PageHeaderComponent, PageSideComponent, PageSideToggleComponent, isUiPageAppearance, isUiPageFilterPlacement, isUiPageHeight, isUiPageScroll, isUiPageSideMode, isUiPageSidePosition, isUiPageVariant };