@ojiepermana/angular-theme 22.0.43 → 22.0.44

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, ChangeDetectionStrategy, 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,211 @@ 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() && position === 'left' && (open ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isDrawer() && position === 'right' && (open ? 'right-0 translate-x-0' : 'right-0 translate-x-full'),
349
+ // overlay → seperti drawer, tapi di atas drawer (z lebih tinggi) dengan backdrop.
350
+ this.isOverlay() &&
351
+ 'absolute inset-y-0 z-30 w-[var(--page-filter-width)] border shadow-md transition-transform duration-200 ease-out', this.isOverlay() && position === 'left' && (open ? 'left-0 translate-x-0' : 'left-0 -translate-x-full'), this.isOverlay() && position === 'right' && (open ? 'right-0 translate-x-0' : 'right-0 translate-x-full'), this.class());
352
+ }, /* @ts-ignore */
353
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
354
+ constructor() {
355
+ setupOverlayFocusManagement({
356
+ document: this.document,
357
+ host: this.host.nativeElement,
358
+ isOpen: this.page.filterOpen,
359
+ isInteractive: this.isInteractive,
360
+ });
361
+ effect(() => {
362
+ this.page.registerFilter({
363
+ placement: this.resolvedPlacement(),
364
+ mode: this.effectiveMode(),
365
+ position: this.resolvedPosition(),
366
+ width: this.resolvedWidth(),
367
+ id: this.resolvedId,
368
+ });
369
+ });
370
+ effect((onCleanup) => {
371
+ if (!this.closeOnEsc() || this.isSticky()) {
372
+ return;
373
+ }
374
+ const defaultView = this.document?.defaultView;
375
+ if (!defaultView) {
376
+ return;
377
+ }
378
+ const handler = (event) => {
379
+ if (event.key !== 'Escape') {
380
+ return;
381
+ }
382
+ if (!untracked(() => this.page.filterOpen())) {
383
+ return;
384
+ }
385
+ untracked(() => {
386
+ this.page.closeFilter();
387
+ });
388
+ };
389
+ defaultView.addEventListener('keydown', handler);
390
+ onCleanup(() => defaultView.removeEventListener('keydown', handler));
391
+ });
392
+ }
393
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
394
+ 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
395
+ }
396
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterComponent, decorators: [{
397
+ type: Component,
398
+ args: [{
399
+ selector: 'PageFilter',
400
+ changeDetection: ChangeDetectionStrategy.OnPush,
401
+ host: {
402
+ '[class]': 'classes()',
403
+ '[attr.id]': 'resolvedId',
404
+ '[attr.data-page-slot]': '"filter"',
405
+ '[attr.data-page-filter-placement]': 'resolvedPlacement()',
406
+ '[attr.data-page-filter-mode]': 'effectiveMode()',
407
+ '[attr.data-page-filter-open]': 'page.filterOpen()',
408
+ '[attr.data-page-position]': 'resolvedPosition()',
409
+ '[attr.aria-hidden]': 'ariaHidden()',
410
+ '[attr.inert]': 'inertWhenClosed()',
411
+ '[attr.role]': 'dialogRole()',
412
+ '[attr.aria-modal]': 'ariaModal()',
413
+ '[attr.aria-label]': 'dialogLabel()',
414
+ '[attr.tabindex]': 'dialogTabindex()',
415
+ '[style.--page-filter-width]': 'resolvedWidth()',
416
+ },
417
+ template: `<ng-content />`,
418
+ }]
419
+ }], 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 }] }] } });
420
+
139
421
  function buildPageBodyClasses(scroll, customClass) {
140
422
  return cn('block min-w-0', scroll === 'content' && 'h-full min-h-0 overflow-auto', scroll === 'page' && 'overflow-visible', customClass);
141
423
  }
@@ -157,10 +439,10 @@ class PageHeaderComponent {
157
439
  // appearance unifies the border with the layout/nav: border-rail = 1.5px.
158
440
  this.isBorderRail() ? 'border-b-[1.5px]' : 'border-b', this.class(), this.resolvedHeight() === 'fix' && 'h-12 overflow-hidden'), /* @ts-ignore */
159
441
  ...(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 });
442
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
443
+ 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
162
444
  }
163
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageHeaderComponent, decorators: [{
445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageHeaderComponent, decorators: [{
164
446
  type: Component,
165
447
  args: [{
166
448
  selector: 'PageHeader',
@@ -183,10 +465,10 @@ class PageContentComponent {
183
465
  ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
184
466
  classes = computed(() => buildPageBodyClasses(this.resolvedScroll(), this.class()), /* @ts-ignore */
185
467
  ...(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 });
468
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
469
+ 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
188
470
  }
189
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageContentComponent, decorators: [{
471
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageContentComponent, decorators: [{
190
472
  type: Component,
191
473
  args: [{
192
474
  selector: 'PageContent',
@@ -206,10 +488,10 @@ class PageDashboardComponent {
206
488
  ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
207
489
  classes = computed(() => buildPageBodyClasses(this.resolvedScroll(), this.class()), /* @ts-ignore */
208
490
  ...(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 });
491
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
492
+ 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
211
493
  }
212
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageDashboardComponent, decorators: [{
494
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageDashboardComponent, decorators: [{
213
495
  type: Component,
214
496
  args: [{
215
497
  selector: 'PageDashboard',
@@ -233,10 +515,10 @@ class PageFooterComponent {
233
515
  // appearance unifies the border with the layout/nav: border-rail = 1.5px.
234
516
  this.isBorderRail() ? 'border-t-[1.5px]' : 'border-t', this.class(), this.resolvedHeight() === 'fix' && 'h-12 overflow-hidden'), /* @ts-ignore */
235
517
  ...(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 });
518
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
519
+ 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
238
520
  }
239
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageFooterComponent, decorators: [{
521
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFooterComponent, decorators: [{
240
522
  type: Component,
241
523
  args: [{
242
524
  selector: 'PageFooter',
@@ -252,6 +534,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
252
534
  let nextPageSideId = 0;
253
535
  class PageSideComponent {
254
536
  document = inject(DOCUMENT, { optional: true });
537
+ host = inject((ElementRef));
255
538
  page = inject(PageStateService);
256
539
  resolvedId = `page-side-${++nextPageSideId}`;
257
540
  mode = input('sticky', /* @ts-ignore */
@@ -262,6 +545,9 @@ class PageSideComponent {
262
545
  ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
263
546
  closeOnEsc = input(true, /* @ts-ignore */
264
547
  ...(ngDevMode ? [{ debugName: "closeOnEsc" }] : /* istanbul ignore next */ []));
548
+ /** Nama aksesibilitas untuk panel saat mode `drawer`/`overlay` (dipasang sebagai `aria-label` dialog). */
549
+ ariaLabel = input('Side panel', /* @ts-ignore */
550
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
265
551
  class = input('', /* @ts-ignore */
266
552
  ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
267
553
  resolvedMode = computed(() => this.mode() ?? this.page.sideMode(), /* @ts-ignore */
@@ -276,19 +562,41 @@ class PageSideComponent {
276
562
  ...(ngDevMode ? [{ debugName: "isDrawer" }] : /* istanbul ignore next */ []));
277
563
  isOverlay = computed(() => this.resolvedMode() === 'overlay', /* @ts-ignore */
278
564
  ...(ngDevMode ? [{ debugName: "isOverlay" }] : /* istanbul ignore next */ []));
565
+ /** Mode mengambang (drawer/overlay) — bersifat dialog dan butuh manajemen fokus + `inert` saat tertutup. */
566
+ isInteractive = computed(() => this.isDrawer() || this.isOverlay(), /* @ts-ignore */
567
+ ...(ngDevMode ? [{ debugName: "isInteractive" }] : /* istanbul ignore next */ []));
279
568
  resolvedScroll = computed(() => this.page.scroll() ?? PAGE_DEFAULT_SCROLL, /* @ts-ignore */
280
569
  ...(ngDevMode ? [{ debugName: "resolvedScroll" }] : /* istanbul ignore next */ []));
281
570
  ariaHidden = computed(() => (!this.isSticky() && !this.page.sideOpen() ? 'true' : null), /* @ts-ignore */
282
571
  ...(ngDevMode ? [{ debugName: "ariaHidden" }] : /* istanbul ignore next */ []));
572
+ /** Saat panel mengambang tertutup, `inert` mengeluarkan isinya dari tab order & pohon aksesibilitas (AXE). */
573
+ inertWhenClosed = computed(() => (this.isInteractive() && !this.page.sideOpen() ? '' : null), /* @ts-ignore */
574
+ ...(ngDevMode ? [{ debugName: "inertWhenClosed" }] : /* istanbul ignore next */ []));
575
+ dialogRole = computed(() => (this.isInteractive() ? 'dialog' : null), /* @ts-ignore */
576
+ ...(ngDevMode ? [{ debugName: "dialogRole" }] : /* istanbul ignore next */ []));
577
+ ariaModal = computed(() => (this.isOverlay() ? 'true' : null), /* @ts-ignore */
578
+ ...(ngDevMode ? [{ debugName: "ariaModal" }] : /* istanbul ignore next */ []));
579
+ dialogLabel = computed(() => (this.isInteractive() ? this.ariaLabel() : null), /* @ts-ignore */
580
+ ...(ngDevMode ? [{ debugName: "dialogLabel" }] : /* istanbul ignore next */ []));
581
+ dialogTabindex = computed(() => (this.isInteractive() ? '-1' : null), /* @ts-ignore */
582
+ ...(ngDevMode ? [{ debugName: "dialogTabindex" }] : /* istanbul ignore next */ []));
283
583
  classes = computed(() => {
284
584
  const position = this.resolvedPosition();
285
585
  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() &&
586
+ 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)]',
587
+ // Urutan DOM body = rail-lalu-content; `left` tak perlu reorder, `right` mendorong rail ke kolom kedua.
588
+ this.isSticky() && position === 'left' && 'border-r', this.isSticky() && position === 'right' && 'order-last border-l', this.isDrawer() &&
287
589
  '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
590
  '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());
289
591
  }, /* @ts-ignore */
290
592
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
291
593
  constructor() {
594
+ setupOverlayFocusManagement({
595
+ document: this.document,
596
+ host: this.host.nativeElement,
597
+ isOpen: this.page.sideOpen,
598
+ isInteractive: this.isInteractive,
599
+ });
292
600
  effect(() => {
293
601
  this.page.registerSide({
294
602
  mode: this.resolvedMode(),
@@ -320,10 +628,10 @@ class PageSideComponent {
320
628
  onCleanup(() => defaultView.removeEventListener('keydown', handler));
321
629
  });
322
630
  }
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 });
631
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
632
+ 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
325
633
  }
326
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageSideComponent, decorators: [{
634
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideComponent, decorators: [{
327
635
  type: Component,
328
636
  args: [{
329
637
  selector: 'PageSide',
@@ -336,11 +644,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
336
644
  '[attr.data-page-side-open]': 'page.sideOpen()',
337
645
  '[attr.data-page-position]': 'resolvedPosition()',
338
646
  '[attr.aria-hidden]': 'ariaHidden()',
647
+ '[attr.inert]': 'inertWhenClosed()',
648
+ '[attr.role]': 'dialogRole()',
649
+ '[attr.aria-modal]': 'ariaModal()',
650
+ '[attr.aria-label]': 'dialogLabel()',
651
+ '[attr.tabindex]': 'dialogTabindex()',
339
652
  '[style.--page-side-width]': 'resolvedWidth()',
340
653
  },
341
654
  template: `<ng-content />`,
342
655
  }]
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 }] }] } });
656
+ }], 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
657
 
345
658
  /** Penghitung instance untuk id unik tiap apps-launcher (hindari bentrok `claimId` saat beberapa `Page` hidup). */
346
659
  let pageAppsLauncherInstanceId = 0;
@@ -353,6 +666,8 @@ class PageComponent {
353
666
  ...(ngDevMode ? [{ debugName: "projectedSide" }] : /* istanbul ignore next */ []));
354
667
  projectedHeader = contentChild(PageHeaderComponent, /* @ts-ignore */
355
668
  ...(ngDevMode ? [{ debugName: "projectedHeader" }] : /* istanbul ignore next */ []));
669
+ projectedFilter = contentChild(PageFilterComponent, /* @ts-ignore */
670
+ ...(ngDevMode ? [{ debugName: "projectedFilter" }] : /* istanbul ignore next */ []));
356
671
  variant = input(PAGE_DEFAULT_VARIANT, /* @ts-ignore */
357
672
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
358
673
  height = input(PAGE_DEFAULT_HEIGHT, /* @ts-ignore */
@@ -370,9 +685,13 @@ class PageComponent {
370
685
  ...(ngDevMode ? [{ debugName: "sideOpen" }] : /* istanbul ignore next */ []));
371
686
  sideWidth = input(PAGE_DEFAULT_SIDE_WIDTH, /* @ts-ignore */
372
687
  ...(ngDevMode ? [{ debugName: "sideWidth" }] : /* istanbul ignore next */ []));
688
+ /** Controlled state untuk `PageFilter` drawer/overlay. `null` = uncontrolled (dikelola toggle/backdrop/Esc). */
689
+ filterOpen = input(null, /* @ts-ignore */
690
+ ...(ngDevMode ? [{ debugName: "filterOpen" }] : /* istanbul ignore next */ []));
373
691
  class = input('', /* @ts-ignore */
374
692
  ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
375
693
  sideOpenChange = output();
694
+ filterOpenChange = output();
376
695
  /** Saat layout `empty`, munculkan tombol apps (flyout main navigation) di pojok kanan-atas. Set `false` untuk menonaktifkan. */
377
696
  appsLauncher = input(true, { ...(ngDevMode ? { debugName: "appsLauncher" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
378
697
  /** Id navigasi yang disurface oleh apps-launcher (default `main`). */
@@ -394,7 +713,25 @@ class PageComponent {
394
713
  ...(ngDevMode ? [{ debugName: "isLeftSide" }] : /* istanbul ignore next */ []));
395
714
  isRightSide = computed(() => this.resolvedPosition() === 'right', /* @ts-ignore */
396
715
  ...(ngDevMode ? [{ debugName: "isRightSide" }] : /* istanbul ignore next */ []));
397
- showsOverlayBackdrop = computed(() => this.variant() === 'side' && this.resolvedSideMode() === 'overlay' && this.page.sideOpen(), /* @ts-ignore */
716
+ // Filter resolusi dibaca langsung dari `PageFilter` yang diprojeksikan (reactive content query).
717
+ // Resolusi filter dibaca dari `PageFilter` yang diprojeksikan, lalu fall back ke page-state
718
+ // (diisi `registerFilter`), lalu default — pola seragam agar Page & PageFilter selalu sepakat.
719
+ hasFilter = computed(() => this.projectedFilter() !== undefined, /* @ts-ignore */
720
+ ...(ngDevMode ? [{ debugName: "hasFilter" }] : /* istanbul ignore next */ []));
721
+ filterPlacement = computed(() => this.projectedFilter()?.placement() ?? this.page.filterPlacement() ?? PAGE_DEFAULT_FILTER_PLACEMENT, /* @ts-ignore */
722
+ ...(ngDevMode ? [{ debugName: "filterPlacement" }] : /* istanbul ignore next */ []));
723
+ /** Mode efektif: `stacked` selalu `sticky`; `side` memakai mode pada `PageFilter`. */
724
+ filterMode = computed(() => this.filterPlacement() === 'stacked' ? 'sticky' : (this.projectedFilter()?.mode() ?? this.page.filterMode()), /* @ts-ignore */
725
+ ...(ngDevMode ? [{ debugName: "filterMode" }] : /* istanbul ignore next */ []));
726
+ filterPosition = computed(() => this.projectedFilter()?.position() ?? this.page.filterPosition(), /* @ts-ignore */
727
+ ...(ngDevMode ? [{ debugName: "filterPosition" }] : /* istanbul ignore next */ []));
728
+ filterWidthVar = computed(() => this.projectedFilter()?.width() ?? this.page.filterWidth() ?? PAGE_DEFAULT_FILTER_WIDTH, /* @ts-ignore */
729
+ ...(ngDevMode ? [{ debugName: "filterWidthVar" }] : /* istanbul ignore next */ []));
730
+ showsSideOverlayBackdrop = computed(() => this.variant() === 'side' && this.resolvedSideMode() === 'overlay' && this.page.sideOpen(), /* @ts-ignore */
731
+ ...(ngDevMode ? [{ debugName: "showsSideOverlayBackdrop" }] : /* istanbul ignore next */ []));
732
+ showsFilterOverlayBackdrop = computed(() => this.hasFilter() && this.filterMode() === 'overlay' && this.page.filterOpen(), /* @ts-ignore */
733
+ ...(ngDevMode ? [{ debugName: "showsFilterOverlayBackdrop" }] : /* istanbul ignore next */ []));
734
+ showsOverlayBackdrop = computed(() => this.showsSideOverlayBackdrop() || this.showsFilterOverlayBackdrop(), /* @ts-ignore */
398
735
  ...(ngDevMode ? [{ debugName: "showsOverlayBackdrop" }] : /* istanbul ignore next */ []));
399
736
  /** Signal data untuk id yang dipilih; di-recompute hanya saat `appsNavId` berubah. */
400
737
  appsNavSource = computed(() => this.navigation?.data(this.appsNavId()) ?? null, /* @ts-ignore */
@@ -437,6 +774,35 @@ class PageComponent {
437
774
  return cn('relative min-w-0', this.scroll() === 'content' && 'min-h-0');
438
775
  }, /* @ts-ignore */
439
776
  ...(ngDevMode ? [{ debugName: "bodyClasses" }] : /* istanbul ignore next */ []));
777
+ /**
778
+ * Wrapper di sekitar `PageFilter` + `PageContent`. Tanpa filter, memakai `display: contents`
779
+ * agar transparan (perilaku body lama dipertahankan persis). Dengan filter, menjadi grid/flex
780
+ * yang menata filter (baris untuk `stacked`, kolom untuk `side`); drawer/overlay menjadikannya
781
+ * `relative` sebagai positioning context untuk panel yang mengambang.
782
+ */
783
+ contentRegionClasses = computed(() => {
784
+ if (!this.hasFilter()) {
785
+ return 'contents';
786
+ }
787
+ const scroll = this.scroll();
788
+ const mode = this.filterMode();
789
+ // drawer/overlay (side only): content mengisi penuh, filter mengambang di atasnya.
790
+ if (mode !== 'sticky') {
791
+ return cn('relative min-w-0', scroll === 'content' && 'min-h-0 h-full');
792
+ }
793
+ if (this.filterPlacement() === 'stacked') {
794
+ // content scroll: filter (auto) di atas, content (1fr) mengisi sisa tinggi & scroll sendiri.
795
+ // page scroll: aliran block biasa; filter dipin via `sticky top-0` pada elemennya.
796
+ return scroll === 'content'
797
+ ? 'grid min-w-0 min-h-0 h-full grid-rows-[auto_minmax(0,1fr)]'
798
+ : 'block min-w-0';
799
+ }
800
+ // side + sticky: filter sebagai kolom; posisi kanan dibalik via `order-last` pada elemen filter.
801
+ return cn('grid min-w-0', scroll === 'content' && 'min-h-0 h-full', this.filterPosition() === 'left'
802
+ ? 'grid-cols-[var(--page-filter-width)_minmax(0,1fr)]'
803
+ : 'grid-cols-[minmax(0,1fr)_var(--page-filter-width)]');
804
+ }, /* @ts-ignore */
805
+ ...(ngDevMode ? [{ debugName: "contentRegionClasses" }] : /* istanbul ignore next */ []));
440
806
  constructor() {
441
807
  effect(() => {
442
808
  this.page.registerRoot({
@@ -452,6 +818,9 @@ class PageComponent {
452
818
  effect(() => {
453
819
  this.page.setControlledSideOpen(this.sideOpen());
454
820
  });
821
+ effect(() => {
822
+ this.page.setControlledFilterOpen(this.filterOpen());
823
+ });
455
824
  // Header memesan ruang kanan hanya saat tombol apps benar-benar mengambang di atas header.
456
825
  effect(() => {
457
826
  this.page.setAppsLauncherReserve(this.showsAppsLauncher() && this.projectedHeader() !== undefined);
@@ -463,16 +832,28 @@ class PageComponent {
463
832
  }
464
833
  this.sideOpenChange.emit(this.page.sideOpenRequest() ?? false);
465
834
  });
835
+ effect(() => {
836
+ const requestVersion = this.page.filterOpenRequestVersion();
837
+ if (requestVersion === 0) {
838
+ return;
839
+ }
840
+ this.filterOpenChange.emit(this.page.filterOpenRequest() ?? false);
841
+ });
466
842
  }
467
843
  handleBackdropClick() {
468
- this.page.closeSide();
844
+ if (this.showsSideOverlayBackdrop()) {
845
+ this.page.closeSide();
846
+ }
847
+ if (this.showsFilterOverlayBackdrop()) {
848
+ this.page.closeFilter();
849
+ }
469
850
  }
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: `
851
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
852
+ 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
853
  @if (showsOverlayBackdrop()) {
473
854
  <button
474
855
  type="button"
475
- aria-label="Close page side"
856
+ aria-label="Close overlay panel"
476
857
  data-page-overlay-backdrop
477
858
  class="absolute inset-0 z-20 bg-[hsl(var(--overlay-backdrop))]"
478
859
  (click)="handleBackdropClick()"></button>
@@ -480,14 +861,17 @@ class PageComponent {
480
861
 
481
862
  <div [class]="shellClasses()">
482
863
  <ng-content select="PageHeader" />
483
- <ng-content select="PageSideToggle" />
864
+ <ng-content select="PageSideToggle, PageFilterToggle" />
484
865
 
485
- <div [class]="bodyClasses()">
866
+ <div [class]="bodyClasses()" data-page-body>
486
867
  @if (variant() === 'side') {
487
868
  <ng-content select="PageSide" />
488
869
  }
489
870
 
490
- <ng-content select="PageContent, PageDashboard" />
871
+ <div [class]="contentRegionClasses()" data-page-content-region>
872
+ <ng-content select="PageFilter" />
873
+ <ng-content select="PageContent, PageDashboard" />
874
+ </div>
491
875
  </div>
492
876
 
493
877
  <ng-content select="PageFooter" />
@@ -520,7 +904,7 @@ class PageComponent {
520
904
  }
521
905
  `, 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 });
522
906
  }
523
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageComponent, decorators: [{
907
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageComponent, decorators: [{
524
908
  type: Component,
525
909
  args: [{
526
910
  selector: 'Page',
@@ -543,13 +927,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
543
927
  '[attr.data-page-position]': 'resolvedPosition()',
544
928
  '[attr.data-page-side-mode]': 'resolvedSideMode()',
545
929
  '[attr.data-page-side-open]': 'page.sideOpen()',
930
+ '[attr.data-page-filter-placement]': 'hasFilter() ? filterPlacement() : null',
931
+ '[attr.data-page-filter-mode]': 'hasFilter() ? filterMode() : null',
932
+ '[attr.data-page-filter-open]': 'hasFilter() ? page.filterOpen() : null',
546
933
  '[style.--page-side-width]': 'sideWidth()',
934
+ '[style.--page-filter-width]': 'filterWidthVar()',
547
935
  },
548
936
  template: `
549
937
  @if (showsOverlayBackdrop()) {
550
938
  <button
551
939
  type="button"
552
- aria-label="Close page side"
940
+ aria-label="Close overlay panel"
553
941
  data-page-overlay-backdrop
554
942
  class="absolute inset-0 z-20 bg-[hsl(var(--overlay-backdrop))]"
555
943
  (click)="handleBackdropClick()"></button>
@@ -557,14 +945,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
557
945
 
558
946
  <div [class]="shellClasses()">
559
947
  <ng-content select="PageHeader" />
560
- <ng-content select="PageSideToggle" />
948
+ <ng-content select="PageSideToggle, PageFilterToggle" />
561
949
 
562
- <div [class]="bodyClasses()">
950
+ <div [class]="bodyClasses()" data-page-body>
563
951
  @if (variant() === 'side') {
564
952
  <ng-content select="PageSide" />
565
953
  }
566
954
 
567
- <ng-content select="PageContent, PageDashboard" />
955
+ <div [class]="contentRegionClasses()" data-page-content-region>
956
+ <ng-content select="PageFilter" />
957
+ <ng-content select="PageContent, PageDashboard" />
958
+ </div>
568
959
  </div>
569
960
 
570
961
  <ng-content select="PageFooter" />
@@ -597,7 +988,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
597
988
  }
598
989
  `,
599
990
  }]
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 }] }] } });
991
+ }], 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 }] }] } });
992
+
993
+ /**
994
+ * `PageFilterToggle` — tombol untuk membuka/menutup `PageFilter` dalam mode `drawer`/`overlay`.
995
+ * Menerima projected content untuk label/ikon kustom; fallback memakai ikon funnel bawaan.
996
+ */
997
+ class PageFilterToggleComponent {
998
+ page = inject(PageStateService);
999
+ ariaLabel = input('Toggle page filter', /* @ts-ignore */
1000
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1001
+ class = input('', /* @ts-ignore */
1002
+ ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
1003
+ toggled = output();
1004
+ hostClasses = computed(() => cn('inline-flex shrink-0', this.class()), /* @ts-ignore */
1005
+ ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
1006
+ 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 */
1007
+ ...(ngDevMode ? [{ debugName: "buttonClasses" }] : /* istanbul ignore next */ []));
1008
+ handleClick() {
1009
+ this.toggled.emit(this.page.toggleFilter());
1010
+ }
1011
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1012
+ 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: `
1013
+ <button
1014
+ type="button"
1015
+ [class]="buttonClasses()"
1016
+ [attr.aria-label]="ariaLabel()"
1017
+ [attr.aria-controls]="page.filterId()"
1018
+ [attr.aria-expanded]="page.filterOpen()"
1019
+ (click)="handleClick()">
1020
+ <ng-content>
1021
+ <svg
1022
+ aria-hidden="true"
1023
+ viewBox="0 0 24 24"
1024
+ class="h-4 w-4"
1025
+ fill="none"
1026
+ stroke="currentColor"
1027
+ stroke-width="2"
1028
+ stroke-linecap="round"
1029
+ stroke-linejoin="round">
1030
+ <path d="M3 5h18l-7 8v5l-4 2v-7z" />
1031
+ </svg>
1032
+ </ng-content>
1033
+ </button>
1034
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1035
+ }
1036
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageFilterToggleComponent, decorators: [{
1037
+ type: Component,
1038
+ args: [{
1039
+ selector: 'PageFilterToggle',
1040
+ changeDetection: ChangeDetectionStrategy.OnPush,
1041
+ host: {
1042
+ '[class]': 'hostClasses()',
1043
+ },
1044
+ template: `
1045
+ <button
1046
+ type="button"
1047
+ [class]="buttonClasses()"
1048
+ [attr.aria-label]="ariaLabel()"
1049
+ [attr.aria-controls]="page.filterId()"
1050
+ [attr.aria-expanded]="page.filterOpen()"
1051
+ (click)="handleClick()">
1052
+ <ng-content>
1053
+ <svg
1054
+ aria-hidden="true"
1055
+ viewBox="0 0 24 24"
1056
+ class="h-4 w-4"
1057
+ fill="none"
1058
+ stroke="currentColor"
1059
+ stroke-width="2"
1060
+ stroke-linecap="round"
1061
+ stroke-linejoin="round">
1062
+ <path d="M3 5h18l-7 8v5l-4 2v-7z" />
1063
+ </svg>
1064
+ </ng-content>
1065
+ </button>
1066
+ `,
1067
+ }]
1068
+ }], 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
1069
 
602
1070
  class PageSideToggleComponent {
603
1071
  page = inject(PageStateService);
@@ -613,8 +1081,8 @@ class PageSideToggleComponent {
613
1081
  handleClick() {
614
1082
  this.toggled.emit(this.page.toggleSide());
615
1083
  }
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: `
1084
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1085
+ 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
1086
  <button
619
1087
  type="button"
620
1088
  [class]="buttonClasses()"
@@ -628,7 +1096,7 @@ class PageSideToggleComponent {
628
1096
  </button>
629
1097
  `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
630
1098
  }
631
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: PageSideToggleComponent, decorators: [{
1099
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: PageSideToggleComponent, decorators: [{
632
1100
  type: Component,
633
1101
  args: [{
634
1102
  selector: 'PageSideToggle',
@@ -656,4 +1124,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
656
1124
  * Generated bundle index. Do not edit.
657
1125
  */
658
1126
 
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 };
1127
+ 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 };