@memberjunction/ng-ui-components 5.51.0 → 6.1.0-edge.1

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.
Files changed (46) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +51 -2
  3. package/dist/lib/bottom-sheet/bottom-sheet.component.d.ts +96 -0
  4. package/dist/lib/bottom-sheet/bottom-sheet.component.d.ts.map +1 -0
  5. package/dist/lib/bottom-sheet/bottom-sheet.component.js +300 -0
  6. package/dist/lib/bottom-sheet/bottom-sheet.component.js.map +1 -0
  7. package/dist/lib/left-nav/left-nav.component.d.ts +142 -3
  8. package/dist/lib/left-nav/left-nav.component.d.ts.map +1 -1
  9. package/dist/lib/left-nav/left-nav.component.js +391 -117
  10. package/dist/lib/left-nav/left-nav.component.js.map +1 -1
  11. package/dist/lib/slide-panel/slide-panel.component.js +6 -6
  12. package/dist/lib/slide-panel/slide-panel.component.js.map +1 -1
  13. package/dist/lib/tabs/tab-chrome-guard.d.ts +14 -0
  14. package/dist/lib/tabs/tab-chrome-guard.d.ts.map +1 -0
  15. package/dist/lib/tabs/tab-chrome-guard.js +36 -0
  16. package/dist/lib/tabs/tab-chrome-guard.js.map +1 -0
  17. package/dist/lib/tabs/tab-list.directive.d.ts +93 -0
  18. package/dist/lib/tabs/tab-list.directive.d.ts.map +1 -0
  19. package/dist/lib/tabs/tab-list.directive.js +202 -0
  20. package/dist/lib/tabs/tab-list.directive.js.map +1 -0
  21. package/dist/lib/tabs/tabs.scss +242 -0
  22. package/dist/lib/workspace-tabs/workspace-card.component.d.ts +63 -0
  23. package/dist/lib/workspace-tabs/workspace-card.component.d.ts.map +1 -0
  24. package/dist/lib/workspace-tabs/workspace-card.component.js +256 -0
  25. package/dist/lib/workspace-tabs/workspace-card.component.js.map +1 -0
  26. package/dist/lib/workspace-tabs/workspace-tab-store.d.ts +47 -0
  27. package/dist/lib/workspace-tabs/workspace-tab-store.d.ts.map +1 -0
  28. package/dist/lib/workspace-tabs/workspace-tab-store.js +112 -0
  29. package/dist/lib/workspace-tabs/workspace-tab-store.js.map +1 -0
  30. package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts +75 -0
  31. package/dist/lib/workspace-tabs/workspace-tab-strip.component.d.ts.map +1 -0
  32. package/dist/lib/workspace-tabs/workspace-tab-strip.component.js +252 -0
  33. package/dist/lib/workspace-tabs/workspace-tab-strip.component.js.map +1 -0
  34. package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts +40 -0
  35. package/dist/lib/workspace-tabs/workspace-tabs.types.d.ts.map +1 -0
  36. package/dist/lib/workspace-tabs/workspace-tabs.types.js +13 -0
  37. package/dist/lib/workspace-tabs/workspace-tabs.types.js.map +1 -0
  38. package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts +34 -0
  39. package/dist/lib/workspace-tabs/workspace-tip.directive.d.ts.map +1 -0
  40. package/dist/lib/workspace-tabs/workspace-tip.directive.js +123 -0
  41. package/dist/lib/workspace-tabs/workspace-tip.directive.js.map +1 -0
  42. package/dist/public-api.d.ts +8 -0
  43. package/dist/public-api.d.ts.map +1 -1
  44. package/dist/public-api.js +8 -0
  45. package/dist/public-api.js.map +1 -1
  46. package/package.json +14 -12
@@ -1 +1 @@
1
- {"version":3,"file":"left-nav.component.js","sourceRoot":"","sources":["../../../src/lib/left-nav/left-nav.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, EventEmitter, HostBinding, HostListener, Input, Output } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n/**\n * A single item in an `<mj-left-nav>`.\n */\nexport interface MJLeftNavItem {\n /** Stable identifier — used for active-state matching and emitted on click. */\n id: string;\n /** Primary label rendered as the item's main text. */\n label: string;\n /** Optional Font Awesome icon class (e.g. `'fa-solid fa-users'`). */\n icon?: string;\n /** Optional muted secondary line shown under the label. */\n description?: string;\n /** Optional count badge rendered at the right of the item. */\n badge?: string | number;\n /** When `true`, the item renders dimmed and is not clickable. */\n disabled?: boolean;\n /**\n * Optional child items for tree-structured nav (e.g. Testing Explorer's\n * Test Suites tree). When `children` is `undefined` the item renders as a\n * plain flat-list item (existing behavior). When defined — even as an empty\n * array — the item participates in tree rendering: a chevron toggle shows\n * for non-empty arrays, and an equal-width placeholder reserves alignment\n * space for leaves so siblings line up. Expansion state is owned by the\n * consumer via `[ExpandedIds]` on `<mj-left-nav>`.\n */\n children?: MJLeftNavItem[];\n}\n\n/**\n * A grouped section in an `<mj-left-nav>`. The optional `label` renders as an\n * uppercase section header above the items. Sections WITHOUT a label still\n * group items together (use multiple unlabeled sections to get visual dividers\n * without headers).\n */\nexport interface MJLeftNavSection {\n /** Optional uppercase header above the items in this section. */\n label?: string;\n /** The clickable items in this section. */\n items: MJLeftNavItem[];\n}\n\n/**\n * `<mj-left-nav>` — Canonical left rail for MJ Explorer dashboards that have\n * an internal section-nav (Admin shells, KH Config, KH Analytics, AI Analytics,\n * Communication, Credentials, APIKeys, etc.).\n *\n * Replaces the bespoke `.{name}-nav` / `.{name}-nav-item` patterns each of\n * those dashboards used to declare in their own CSS files.\n *\n * Supports:\n * - Plain items (icon + label)\n * - Items with a muted secondary description line (Admin shells pattern)\n * - Items with a count badge (APIKeys pattern)\n * - Sections with uppercase headers (Communication / Credentials pattern)\n * - Optional `[header]` and `[footer]` content slots (KH Config logo header, etc.)\n * - Responsive mobile drawer (≤700px): the rail hides off-canvas and a compact\n * section switcher takes its place; tapping it slides the full rail in over a\n * scrim. The rail keeps its full content (trees, slots, descriptions, badges)\n * inside the drawer — no flattening. Set `[MobileTitle]` for the drawer label.\n *\n * ## Example\n *\n * ```typescript\n * sections: MJLeftNavSection[] = [\n * {\n * items: [\n * { id: 'users', icon: 'fa-solid fa-users', label: 'Users', description: 'Manage user accounts' },\n * { id: 'roles', icon: 'fa-solid fa-user-shield', label: 'Roles', description: 'Define roles and assignments' },\n * ]\n * }\n * ];\n * activeId = 'users';\n * ```\n *\n * ```html\n * <mj-left-nav\n * [Sections]=\"sections\"\n * [ActiveId]=\"activeId\"\n * (ItemClicked)=\"onItemClicked($event)\">\n * </mj-left-nav>\n * ```\n *\n * Responsive — at ≤700px the rail becomes an off-canvas drawer (see the feature\n * list above). The consumer should still switch the parent's flex-direction from\n * row → column at the same breakpoint (e.g. `<mj-page-body>` Direction=\"row\" with\n * a `@media` override) so the in-flow section switcher stacks above the content\n * pane rather than competing with it for horizontal space.\n */\n@Component({\n selector: 'mj-left-nav',\n standalone: true,\n imports: [CommonModule],\n template: `\n <!-- Mobile-only section switcher (hidden ≥701px via CSS). Shows the active\n section and opens the full rail as an off-canvas drawer on tap. The rail\n itself is too rich (trees, header/footer slots, descriptions, badges) to\n flatten into a dropdown or tab strip, so we surface it as-is in a drawer. -->\n <button\n type=\"button\"\n class=\"mj-left-nav__switcher\"\n [attr.aria-expanded]=\"MobileNavOpen\"\n aria-haspopup=\"dialog\"\n (click)=\"OpenMobileNav()\">\n @if (ActiveTriggerItem?.icon) {\n <i class=\"mj-left-nav__switcher-icon\" [class]=\"ActiveTriggerItem!.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"mj-left-nav__switcher-label\">{{ ActiveTriggerItem?.label || MobileTitle }}</span>\n <i class=\"fa-solid fa-bars-staggered mj-left-nav__switcher-hint\" aria-hidden=\"true\"></i>\n </button>\n\n <!-- Scrim behind the mobile drawer (hidden ≥701px / when closed via CSS). -->\n <div class=\"mj-left-nav__scrim\" (click)=\"CloseMobileNav()\" aria-hidden=\"true\"></div>\n\n <aside\n class=\"mj-left-nav\"\n [style.width.px]=\"Width\"\n role=\"navigation\"\n [attr.aria-modal]=\"MobileNavOpen ? true : null\">\n <!-- Drawer header — only visible on mobile when the rail is a drawer. -->\n <div class=\"mj-left-nav__drawer-header\">\n <span class=\"mj-left-nav__drawer-title\">{{ MobileTitle }}</span>\n <button\n type=\"button\"\n class=\"mj-left-nav__drawer-close\"\n aria-label=\"Close menu\"\n (click)=\"CloseMobileNav()\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <ng-content select=\"[header]\"></ng-content>\n\n @for (section of Sections; track $index) {\n @if (section.label) {\n <div class=\"mj-left-nav__section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <ng-container\n *ngTemplateOutlet=\"itemTpl; context: { item: item, depth: 0, hasExpandable: HasExpandableItems(section.items) }\">\n </ng-container>\n }\n }\n\n <ng-content select=\"[footer]\"></ng-content>\n </aside>\n\n <!-- Recursive item template — renders one item plus, when expanded,\n its children indented by depth. The hasExpandable flag is computed\n once per section and threaded through the recursion so leaf nodes\n only reserve a chevron-column placeholder when a sibling actually\n shows a chevron. -->\n <ng-template #itemTpl let-item=\"item\" let-depth=\"depth\" let-hasExpandable=\"hasExpandable\">\n @if (item.children !== undefined && hasExpandable) {\n <!-- Tree row: chevron (when this node has children) or placeholder\n (when a sibling has children but this leaf needs alignment). -->\n <div\n class=\"mj-left-nav__row\"\n [style.padding-left.px]=\"depth * 14\">\n @if (item.children.length > 0) {\n <button\n type=\"button\"\n class=\"mj-left-nav__chevron\"\n [attr.aria-label]=\"IsExpanded(item.id) ? 'Collapse' : 'Expand'\"\n [attr.aria-expanded]=\"IsExpanded(item.id)\"\n (click)=\"OnItemToggle($event, item)\">\n <i class=\"fa-solid\"\n [class.fa-chevron-down]=\"IsExpanded(item.id)\"\n [class.fa-chevron-right]=\"!IsExpanded(item.id)\"\n aria-hidden=\"true\"></i>\n </button>\n } @else {\n <span class=\"mj-left-nav__chevron-placeholder\" aria-hidden=\"true\"></span>\n }\n <ng-container\n *ngTemplateOutlet=\"itemBtn; context: { item: item }\">\n </ng-container>\n </div>\n\n @if (item.children.length > 0 && IsExpanded(item.id)) {\n @for (child of item.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"itemTpl; context: { item: child, depth: depth + 1, hasExpandable: hasExpandable }\">\n </ng-container>\n }\n }\n } @else {\n <!-- Flat render: item.children is undefined, OR no sibling in this\n section is actually expandable — render the button directly with\n no chevron column reserved. -->\n <ng-container\n *ngTemplateOutlet=\"itemBtn; context: { item: item }\">\n </ng-container>\n }\n </ng-template>\n\n <!-- Item button — shared by both flat and tree rendering paths -->\n <ng-template #itemBtn let-item=\"item\">\n <button\n type=\"button\"\n class=\"mj-left-nav__item\"\n [class.mj-left-nav__item--active]=\"item.id === ActiveId\"\n [class.mj-left-nav__item--disabled]=\"item.disabled\"\n [disabled]=\"item.disabled\"\n [attr.aria-current]=\"item.id === ActiveId ? 'page' : null\"\n (click)=\"OnItemClick(item)\">\n @if (item.icon) {\n <i class=\"mj-left-nav__icon\" [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"mj-left-nav__text\">\n <span class=\"mj-left-nav__label\">{{ item.label }}</span>\n @if (item.description) {\n <span class=\"mj-left-nav__description\">{{ item.description }}</span>\n }\n </span>\n @if (item.badge != null) {\n <span class=\"mj-left-nav__badge\">{{ item.badge }}</span>\n }\n </button>\n </ng-template>\n `,\n styles: [`\n :host {\n display: block;\n flex-shrink: 0;\n }\n\n .mj-left-nav {\n height: 100%;\n background: var(--mj-bg-surface);\n border-right: 1px solid var(--mj-border-default);\n overflow-y: auto;\n padding: 8px;\n box-sizing: border-box;\n }\n\n .mj-left-nav__section-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--mj-text-muted);\n padding: 12px 12px 6px;\n }\n\n .mj-left-nav__section-label:first-child {\n padding-top: 6px;\n }\n\n .mj-left-nav__item {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px 12px;\n width: 100%;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease;\n margin-bottom: 2px;\n font-family: inherit;\n color: var(--mj-text-secondary);\n text-align: left;\n }\n\n .mj-left-nav__item:hover:not(.mj-left-nav__item--disabled) {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n }\n\n .mj-left-nav__item--active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n }\n\n .mj-left-nav__item--active:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n }\n\n .mj-left-nav__item--disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n .mj-left-nav__icon {\n width: 18px;\n text-align: center;\n font-size: 13px;\n margin-top: 1px;\n flex-shrink: 0;\n color: inherit;\n }\n\n .mj-left-nav__text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n }\n\n .mj-left-nav__label {\n font-size: 13px;\n font-weight: 500;\n line-height: 1.2;\n }\n\n .mj-left-nav__description {\n font-size: 11px;\n color: var(--mj-text-muted);\n margin-top: 3px;\n line-height: 1.3;\n }\n\n .mj-left-nav__item--active .mj-left-nav__description {\n color: color-mix(in srgb, var(--mj-brand-primary) 70%, var(--mj-text-muted));\n }\n\n .mj-left-nav__badge {\n margin-left: auto;\n font-size: 11px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 999px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-secondary);\n align-self: center;\n }\n\n .mj-left-nav__item--active .mj-left-nav__badge {\n background: color-mix(in srgb, var(--mj-brand-primary) 18%, transparent);\n color: var(--mj-brand-primary);\n }\n\n /* ── Tree rendering ─────────────────────────────────────────────────────\n Used when MJLeftNavItem.children is defined (even as an empty array).\n The row wraps a chevron/placeholder + the item button so they sit on\n one line, and depth-based padding-left is applied to the row (not the\n item) so the chevron aligns with the depth column. */\n\n .mj-left-nav__row {\n display: flex;\n align-items: stretch;\n gap: 2px;\n }\n\n .mj-left-nav__row .mj-left-nav__item {\n flex: 1;\n min-width: 0;\n }\n\n .mj-left-nav__chevron {\n flex-shrink: 0;\n width: 20px;\n height: 32px;\n align-self: center;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 10px;\n padding: 0;\n transition: background-color 0.12s ease, color 0.12s ease;\n font-family: inherit;\n }\n\n .mj-left-nav__chevron:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n }\n\n .mj-left-nav__chevron-placeholder {\n flex-shrink: 0;\n width: 20px;\n display: inline-block;\n }\n\n /* ── Mobile drawer pattern (≤700px) ──────────────────────────────────────\n Desktop shows the rail inline. On mobile the rail would otherwise eat the\n top of the screen, so we hide it off-canvas and surface a compact section\n switcher in its place. Tapping the switcher slides the full rail in from\n the left over a scrim — preserving trees, header/footer slots and badges\n (item descriptions hide in the drawer for a tighter list).\n\n The switcher / scrim / drawer-header are display:none above the\n breakpoint; the rail keeps its desktop layout there. */\n .mj-left-nav__switcher,\n .mj-left-nav__scrim,\n .mj-left-nav__drawer-header {\n display: none;\n }\n\n @media (max-width: 700px) {\n /* Host collapses to the switcher's footprint; the rail goes out of flow. */\n :host {\n width: 100% !important;\n }\n\n /* Hide item descriptions in the drawer — the label + icon are enough on a\n narrow sheet; the secondary line just adds clutter. */\n .mj-left-nav__description {\n display: none;\n }\n\n /* Section switcher — reads as a picker for the current section. */\n .mj-left-nav__switcher {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 11px 14px;\n border: none;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n text-align: left;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n .mj-left-nav__switcher:active {\n background: var(--mj-bg-surface-active);\n }\n .mj-left-nav__switcher-icon {\n width: 18px;\n text-align: center;\n font-size: 14px;\n flex-shrink: 0;\n color: var(--mj-brand-primary);\n }\n .mj-left-nav__switcher-label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .mj-left-nav__switcher-hint {\n flex-shrink: 0;\n font-size: 15px;\n color: var(--mj-text-muted);\n }\n\n /* Scrim — fades in with the drawer. */\n .mj-left-nav__scrim {\n display: block;\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.24s ease;\n z-index: 9998;\n }\n :host(.mj-left-nav-host--open) .mj-left-nav__scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n /* Rail becomes a left off-canvas drawer. */\n .mj-left-nav {\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n width: min(320px, 84vw) !important;\n height: 100%;\n max-height: none;\n border-right: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-lg);\n transform: translateX(-100%);\n transition: transform 0.24s ease;\n z-index: 9999;\n padding-top: 0;\n }\n :host(.mj-left-nav-host--open) .mj-left-nav {\n transform: translateX(0);\n }\n\n /* Drawer header — sticky title + close button. */\n .mj-left-nav__drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n position: sticky;\n top: 0;\n z-index: 1;\n padding: 14px 12px 10px;\n margin: 0 -8px 4px;\n padding-left: 16px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-subtle);\n }\n .mj-left-nav__drawer-title {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--mj-text-muted);\n }\n .mj-left-nav__drawer-close {\n flex-shrink: 0;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n color: var(--mj-text-secondary);\n font-size: 16px;\n font-family: inherit;\n }\n .mj-left-nav__drawer-close:active {\n background: var(--mj-bg-surface-active);\n }\n }\n\n @media (max-width: 700px) and (prefers-reduced-motion: reduce) {\n .mj-left-nav,\n .mj-left-nav__scrim {\n transition: none;\n }\n }\n `]\n})\nexport class MJLeftNavComponent {\n /**\n * The sections + items that make up the rail. Sections render top-to-bottom\n * in array order; items inside each section render in array order.\n */\n @Input() Sections: MJLeftNavSection[] = [];\n\n /**\n * The id of the currently active item. Items matching this id get the\n * active styling and `aria-current=\"page\"`.\n */\n @Input() ActiveId: string | null = null;\n\n /**\n * Width of the rail in pixels. Defaults to 240. Set to a smaller value\n * (e.g., 200) for denser shells. Only applies to the desktop rail — on mobile\n * the rail becomes a fixed-width off-canvas drawer.\n */\n @Input() Width: number = 240;\n\n /**\n * Label shown on the mobile drawer header and used as the switcher fallback\n * label when no item is active. Defaults to `'Menu'`. Set to something\n * domain-specific (e.g. `'Sections'`, `'Test Suites'`) for clearer context.\n */\n @Input() MobileTitle: string = 'Menu';\n\n /**\n * Ids of currently-expanded tree items. Items whose id appears here render\n * with a down-chevron and their `children` are visible; items not in this\n * list render with a right-chevron and their children stay collapsed.\n * Consumer owns the state — toggle in response to `(ItemToggled)`.\n *\n * Only applies to items where `children !== undefined`; flat items ignore it.\n */\n @Input() ExpandedIds: string[] = [];\n\n /**\n * Emitted when a non-disabled item is clicked.\n */\n @Output() ItemClicked = new EventEmitter<MJLeftNavItem>();\n\n /**\n * Emitted when a tree item's chevron is clicked. Consumer should toggle\n * the item's id in / out of `ExpandedIds`. Does NOT also fire `ItemClicked`.\n */\n @Output() ItemToggled = new EventEmitter<MJLeftNavItem>();\n\n /**\n * Whether the mobile off-canvas drawer is open. Component-owned, transient UI\n * state — no effect above the 700px breakpoint where the rail is always inline.\n */\n public MobileNavOpen = false;\n\n /** Reflects {@link MobileNavOpen} onto the host so CSS can drive the drawer. */\n @HostBinding('class.mj-left-nav-host--open')\n get isMobileNavOpen(): boolean {\n return this.MobileNavOpen;\n }\n\n OpenMobileNav(): void {\n this.MobileNavOpen = true;\n }\n\n CloseMobileNav(): void {\n this.MobileNavOpen = false;\n }\n\n /** Escape closes the mobile drawer. No-op when it's already closed. */\n @HostListener('document:keydown.escape')\n onEscape(): void {\n if (this.MobileNavOpen) {\n this.MobileNavOpen = false;\n }\n }\n\n OnItemClick(item: MJLeftNavItem): void {\n if (item.disabled) return;\n // Selecting a leaf dismisses the mobile drawer; harmless on desktop.\n this.MobileNavOpen = false;\n this.ItemClicked.emit(item);\n }\n\n OnItemToggle(event: Event, item: MJLeftNavItem): void {\n event.stopPropagation();\n // Expanding/collapsing a tree branch keeps the drawer open.\n this.ItemToggled.emit(item);\n }\n\n /**\n * The item matching {@link ActiveId} (searched recursively through trees),\n * used to label the mobile section switcher with the current section's icon\n * and label. Returns `undefined` when nothing is active.\n */\n get ActiveTriggerItem(): MJLeftNavItem | undefined {\n if (!this.ActiveId) return undefined;\n return this.findItemById(this.Sections.flatMap((s) => s.items), this.ActiveId);\n }\n\n private findItemById(items: MJLeftNavItem[], id: string): MJLeftNavItem | undefined {\n for (const item of items) {\n if (item.id === id) return item;\n if (item.children?.length) {\n const found = this.findItemById(item.children, id);\n if (found) return found;\n }\n }\n return undefined;\n }\n\n IsExpanded(id: string): boolean {\n return this.ExpandedIds.indexOf(id) >= 0;\n }\n\n /**\n * Whether any top-level item in this section actually has children to expand.\n * The template calls this once per section; the result is threaded through\n * the recursive itemTpl so leaf nodes only reserve a chevron-column\n * placeholder when a sibling will actually render a chevron.\n *\n * Top-level check is sufficient — if a deeply-nested item has children, its\n * parent's `children.length > 0` already qualifies the section as expandable.\n */\n HasExpandableItems(items: MJLeftNavItem[]): boolean {\n for (const item of items) {\n if (item.children && item.children.length > 0) return true;\n }\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"left-nav.component.js","sourceRoot":"","sources":["../../../src/lib/left-nav/left-nav.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectorRef,\n Component,\n EventEmitter,\n HostBinding,\n HostListener,\n Input,\n NgZone,\n OnDestroy,\n Output,\n inject,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n/** Source of per-instance DOM ids — see {@link MJLeftNavComponent.NavRegionId}. */\nlet navRegionSeq = 0;\n\n/**\n * A single item in an `<mj-left-nav>`.\n */\nexport interface MJLeftNavItem {\n /** Stable identifier — used for active-state matching and emitted on click. */\n id: string;\n /** Primary label rendered as the item's main text. */\n label: string;\n /** Optional Font Awesome icon class (e.g. `'fa-solid fa-users'`). */\n icon?: string;\n /** Optional muted secondary line shown under the label. */\n description?: string;\n /** Optional count badge rendered at the right of the item. */\n badge?: string | number;\n /** When `true`, the item renders dimmed and is not clickable. */\n disabled?: boolean;\n /**\n * Optional child items for tree-structured nav (e.g. Testing Explorer's\n * Test Suites tree). When `children` is `undefined` the item renders as a\n * plain flat-list item (existing behavior). When defined — even as an empty\n * array — the item participates in tree rendering: a chevron toggle shows\n * for non-empty arrays, and an equal-width placeholder reserves alignment\n * space for leaves so siblings line up. Expansion state is owned by the\n * consumer via `[ExpandedIds]` on `<mj-left-nav>`.\n */\n children?: MJLeftNavItem[];\n}\n\n/**\n * A grouped section in an `<mj-left-nav>`. The optional `label` renders as an\n * uppercase section header above the items. Sections WITHOUT a label still\n * group items together (use multiple unlabeled sections to get visual dividers\n * without headers).\n */\nexport interface MJLeftNavSection {\n /** Optional uppercase header above the items in this section. */\n label?: string;\n /** The clickable items in this section. */\n items: MJLeftNavItem[];\n}\n\n/**\n * `<mj-left-nav>` — Canonical left rail for MJ Explorer dashboards that have\n * an internal section-nav (Admin shells, KH Config, KH Analytics, AI Analytics,\n * Communication, Credentials, APIKeys, etc.).\n *\n * Replaces the bespoke `.{name}-nav` / `.{name}-nav-item` patterns each of\n * those dashboards used to declare in their own CSS files.\n *\n * Supports:\n * - Plain items (icon + label)\n * - Items with a muted secondary description line (Admin shells pattern)\n * - Items with a count badge (APIKeys pattern)\n * - Sections with uppercase headers (Communication / Credentials pattern)\n * - Optional `[header]` and `[footer]` content slots (KH Config logo header, etc.)\n * - Responsive mobile drawer (≤700px): the rail hides off-canvas and a compact\n * section switcher takes its place; tapping it slides the full rail in over a\n * scrim. The rail keeps its full content (trees, slots, descriptions, badges)\n * inside the drawer — no flattening. Set `[MobileTitle]` for the drawer label.\n *\n * ## Example\n *\n * ```typescript\n * sections: MJLeftNavSection[] = [\n * {\n * items: [\n * { id: 'users', icon: 'fa-solid fa-users', label: 'Users', description: 'Manage user accounts' },\n * { id: 'roles', icon: 'fa-solid fa-user-shield', label: 'Roles', description: 'Define roles and assignments' },\n * ]\n * }\n * ];\n * activeId = 'users';\n * ```\n *\n * ```html\n * <mj-left-nav\n * [Sections]=\"sections\"\n * [ActiveId]=\"activeId\"\n * (ItemClicked)=\"onItemClicked($event)\">\n * </mj-left-nav>\n * ```\n *\n * Responsive — at ≤700px the rail becomes an off-canvas drawer (see the feature\n * list above). The consumer should still switch the parent's flex-direction from\n * row → column at the same breakpoint (e.g. `<mj-page-body>` Direction=\"row\" with\n * a `@media` override) so the in-flow section switcher stacks above the content\n * pane rather than competing with it for horizontal space.\n */\n@Component({\n selector: 'mj-left-nav',\n standalone: true,\n imports: [CommonModule],\n template: `\n <!-- Mobile-only section switcher (hidden ≥701px via CSS). Shows the active\n section and opens the full rail as an off-canvas drawer on tap. The rail\n itself is too rich (trees, header/footer slots, descriptions, badges) to\n flatten into a dropdown or tab strip, so we surface it as-is in a drawer. -->\n <button\n type=\"button\"\n class=\"mj-left-nav__switcher\"\n [attr.aria-expanded]=\"MobileNavOpen\"\n aria-haspopup=\"dialog\"\n (click)=\"OpenMobileNav()\">\n @if (ActiveTriggerItem?.icon) {\n <i class=\"mj-left-nav__switcher-icon\" [class]=\"ActiveTriggerItem!.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"mj-left-nav__switcher-label\">{{ ActiveTriggerItem?.label || MobileTitle }}</span>\n <i class=\"fa-solid fa-bars-staggered mj-left-nav__switcher-hint\" aria-hidden=\"true\"></i>\n </button>\n\n <!-- Scrim behind the mobile drawer (hidden ≥701px / when closed via CSS). -->\n <div class=\"mj-left-nav__scrim\" (click)=\"CloseMobileNav()\" aria-hidden=\"true\"></div>\n\n <aside\n class=\"mj-left-nav\"\n [id]=\"NavRegionId\"\n [style.width.px]=\"EffectiveWidth\"\n role=\"navigation\"\n [attr.aria-modal]=\"MobileNavOpen ? true : null\">\n <!-- Desktop collapse toggle (opt-in via [Collapsible]) — a compact double-angle chip, NOT a\n hamburger (a lone hamburger row reads as a broken nav item) and NOT a nav-row lookalike.\n The chip is LOCKED in one spot — centred in the collapsed band — in BOTH modes: only the\n glyph flips on toggle, so the user never re-aims (a control that relocates when clicked\n makes the user chase it). « = collapse toward the left edge, » = expand. Hidden ≤700px:\n the mobile drawer is its own affordance and collapsing a drawer is meaningless. -->\n @if (Collapsible) {\n <button\n type=\"button\"\n class=\"mj-left-nav__collapse-toggle\"\n [style.margin-left.px]=\"CollapseToggleMarginPx\"\n [attr.aria-label]=\"Collapsed ? 'Expand navigation' : 'Collapse navigation'\"\n [attr.aria-expanded]=\"!Collapsed\"\n [attr.aria-controls]=\"NavRegionId\"\n [title]=\"Collapsed ? 'Expand navigation' : 'Collapse navigation'\"\n (click)=\"ToggleCollapsed()\">\n <i\n class=\"fa-solid mj-left-nav__collapse-icon\"\n [class.fa-angles-right]=\"Collapsed\"\n [class.fa-angles-left]=\"!Collapsed\"\n aria-hidden=\"true\"></i>\n </button>\n <!-- Wordless hairline between the toggle and the nav content: without a boundary the chip\n reads as a destination. Same height in both modes, so items never shift on toggle. -->\n <div class=\"mj-left-nav__collapse-divider\" aria-hidden=\"true\"></div>\n }\n\n <!-- Drawer header — only visible on mobile when the rail is a drawer. -->\n <div class=\"mj-left-nav__drawer-header\">\n <span class=\"mj-left-nav__drawer-title\">{{ MobileTitle }}</span>\n <button\n type=\"button\"\n class=\"mj-left-nav__drawer-close\"\n aria-label=\"Close menu\"\n (click)=\"CloseMobileNav()\">\n <i class=\"fa-solid fa-xmark\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <ng-content select=\"[header]\"></ng-content>\n\n @for (section of Sections; track $index) {\n @if (section.label) {\n <div class=\"mj-left-nav__section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <!-- hasExpandable goes false while icons-only, collapsing each section to its TOP-LEVEL\n items only — see ShowTreeStructure for why a tree can't survive a 60px rail. -->\n <ng-container\n *ngTemplateOutlet=\"itemTpl; context: { item: item, depth: 0, hasExpandable: ShowTreeStructure && HasExpandableItems(section.items) }\">\n </ng-container>\n }\n }\n\n <ng-content select=\"[footer]\"></ng-content>\n </aside>\n\n <!-- Recursive item template — renders one item plus, when expanded,\n its children indented by depth. The hasExpandable flag is computed\n once per section and threaded through the recursion so leaf nodes\n only reserve a chevron-column placeholder when a sibling actually\n shows a chevron. -->\n <ng-template #itemTpl let-item=\"item\" let-depth=\"depth\" let-hasExpandable=\"hasExpandable\">\n @if (item.children !== undefined && hasExpandable) {\n <!-- Tree row: chevron (when this node has children) or placeholder\n (when a sibling has children but this leaf needs alignment). -->\n <div\n class=\"mj-left-nav__row\"\n [style.padding-left.px]=\"depth * 14\">\n @if (item.children.length > 0) {\n <button\n type=\"button\"\n class=\"mj-left-nav__chevron\"\n [attr.aria-label]=\"IsExpanded(item.id) ? 'Collapse' : 'Expand'\"\n [attr.aria-expanded]=\"IsExpanded(item.id)\"\n (click)=\"OnItemToggle($event, item)\">\n <i class=\"fa-solid\"\n [class.fa-chevron-down]=\"IsExpanded(item.id)\"\n [class.fa-chevron-right]=\"!IsExpanded(item.id)\"\n aria-hidden=\"true\"></i>\n </button>\n } @else {\n <span class=\"mj-left-nav__chevron-placeholder\" aria-hidden=\"true\"></span>\n }\n <ng-container\n *ngTemplateOutlet=\"itemBtn; context: { item: item }\">\n </ng-container>\n </div>\n\n @if (item.children.length > 0 && IsExpanded(item.id)) {\n @for (child of item.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"itemTpl; context: { item: child, depth: depth + 1, hasExpandable: hasExpandable }\">\n </ng-container>\n }\n }\n } @else {\n <!-- Flat render: item.children is undefined, OR no sibling in this\n section is actually expandable — render the button directly with\n no chevron column reserved. -->\n <ng-container\n *ngTemplateOutlet=\"itemBtn; context: { item: item }\">\n </ng-container>\n }\n </ng-template>\n\n <!-- Item button — shared by both flat and tree rendering paths -->\n <ng-template #itemBtn let-item=\"item\">\n <button\n type=\"button\"\n class=\"mj-left-nav__item\"\n [class.mj-left-nav__item--active]=\"IsItemActive(item)\"\n [class.mj-left-nav__item--disabled]=\"item.disabled\"\n [disabled]=\"item.disabled\"\n [attr.aria-current]=\"AriaCurrentFor(item)\"\n [attr.title]=\"EffectiveIconOnly ? ItemTooltip(item) : null\"\n [attr.aria-label]=\"EffectiveIconOnly ? ItemTooltip(item) : null\"\n (click)=\"OnItemClick(item)\">\n @if (item.icon) {\n <i class=\"mj-left-nav__icon\" [class]=\"item.icon\" aria-hidden=\"true\"></i>\n } @else if (EffectiveIconOnly) {\n <!-- An icon-less item collapses to a button with NOTHING visible in it (the label is the\n only content and it is clipped away) — an invisible-but-clickable row. The icon is\n optional on MJLeftNavItem, so the rail cannot require one; a monogram keeps every item\n identifiable and hittable. Only rendered icons-only: expanded rails are unchanged. -->\n <span class=\"mj-left-nav__initial\" aria-hidden=\"true\">{{ ItemInitial(item) }}</span>\n }\n <span class=\"mj-left-nav__text\">\n <span class=\"mj-left-nav__label\">{{ item.label }}</span>\n @if (item.description) {\n <span class=\"mj-left-nav__description\">{{ item.description }}</span>\n }\n </span>\n @if (item.badge != null) {\n <span class=\"mj-left-nav__badge\">{{ item.badge }}</span>\n }\n </button>\n </ng-template>\n `,\n styles: [`\n :host {\n display: block;\n flex-shrink: 0;\n }\n\n .mj-left-nav {\n height: 100%;\n background: var(--mj-bg-surface);\n border-right: 1px solid var(--mj-border-default);\n overflow-y: auto;\n padding: 8px;\n box-sizing: border-box;\n }\n\n .mj-left-nav__section-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--mj-text-muted);\n padding: 12px 12px 6px;\n }\n\n .mj-left-nav__section-label:first-child {\n padding-top: 6px;\n }\n\n .mj-left-nav__item {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px 12px;\n width: 100%;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease;\n margin-bottom: 2px;\n font-family: inherit;\n color: var(--mj-text-secondary);\n text-align: left;\n }\n\n .mj-left-nav__item:hover:not(.mj-left-nav__item--disabled) {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n }\n\n .mj-left-nav__item--active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--mj-brand-primary);\n }\n\n .mj-left-nav__item--active:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n }\n\n .mj-left-nav__item--disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n .mj-left-nav__icon {\n width: 18px;\n text-align: center;\n font-size: 13px;\n margin-top: 1px;\n flex-shrink: 0;\n color: inherit;\n }\n\n /* Stand-in glyph for an icon-less item while the rail is icons-only. Same 18px box as\n .mj-left-nav__icon so it sits on the identical optical column — the rail reads as one\n consistent strip whether an item brought an icon or not. */\n .mj-left-nav__initial {\n width: 18px;\n text-align: center;\n font-size: 12px;\n font-weight: 600;\n line-height: 1.2;\n margin-top: 1px;\n flex-shrink: 0;\n color: inherit;\n text-transform: uppercase;\n }\n\n .mj-left-nav__text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n }\n\n .mj-left-nav__label {\n font-size: 13px;\n font-weight: 500;\n line-height: 1.2;\n }\n\n .mj-left-nav__description {\n font-size: 11px;\n color: var(--mj-text-muted);\n margin-top: 3px;\n line-height: 1.3;\n }\n\n .mj-left-nav__item--active .mj-left-nav__description {\n color: color-mix(in srgb, var(--mj-brand-primary) 70%, var(--mj-text-muted));\n }\n\n .mj-left-nav__badge {\n margin-left: auto;\n font-size: 11px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 999px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-secondary);\n align-self: center;\n }\n\n .mj-left-nav__item--active .mj-left-nav__badge {\n background: color-mix(in srgb, var(--mj-brand-primary) 18%, transparent);\n color: var(--mj-brand-primary);\n }\n\n /* ── Tree rendering ─────────────────────────────────────────────────────\n Used when MJLeftNavItem.children is defined (even as an empty array).\n The row wraps a chevron/placeholder + the item button so they sit on\n one line, and depth-based padding-left is applied to the row (not the\n item) so the chevron aligns with the depth column. */\n\n .mj-left-nav__row {\n display: flex;\n align-items: stretch;\n gap: 2px;\n }\n\n .mj-left-nav__row .mj-left-nav__item {\n flex: 1;\n min-width: 0;\n }\n\n .mj-left-nav__chevron {\n flex-shrink: 0;\n width: 20px;\n height: 32px;\n align-self: center;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n border-radius: 4px;\n cursor: pointer;\n color: var(--mj-text-muted);\n font-size: 10px;\n padding: 0;\n transition: background-color 0.12s ease, color 0.12s ease;\n font-family: inherit;\n }\n\n .mj-left-nav__chevron:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n }\n\n .mj-left-nav__chevron-placeholder {\n flex-shrink: 0;\n width: 20px;\n display: inline-block;\n }\n\n /* ── Desktop collapse (opt-in via [Collapsible]) ──────────────────────────\n The toggle chip + the icons-only collapsed presentation. DESKTOP ONLY: below 701px the rail\n is the off-canvas drawer, which is already dismissable — collapsing it would be nonsense —\n so the toggle hides and the collapsed rules never apply there. Hover-to-peek is deliberately\n NOT part of this feature (removed by ruling 2026-07-30: a rail that expands when the pointer\n grazes it moves the target the user was aiming at). */\n\n .mj-left-nav__collapse-toggle {\n display: flex;\n align-items: center;\n justify-content: center;\n width: fit-content;\n padding: 8px 10px;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--mj-text-muted);\n font-family: inherit;\n cursor: pointer;\n box-sizing: border-box;\n transition: background-color 0.12s ease, color 0.12s ease;\n margin-bottom: 2px;\n }\n\n .mj-left-nav__collapse-toggle:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n }\n\n .mj-left-nav__collapse-toggle:focus-visible {\n outline: 2px solid var(--mj-border-focus);\n outline-offset: -2px;\n }\n\n /* 18px box = the same footprint as .mj-left-nav__icon, so the chip sits on the items' optical\n icon column when centred in the collapsed band. Slightly smaller glyph than the nav icons —\n this is a chrome control, not a destination. */\n .mj-left-nav__collapse-icon {\n width: 18px;\n text-align: center;\n font-size: 12px;\n flex-shrink: 0;\n }\n\n .mj-left-nav__collapse-divider {\n height: 1px;\n margin: 4px 0;\n background: var(--mj-border-default);\n }\n\n @media (max-width: 700px) {\n .mj-left-nav__collapse-toggle,\n .mj-left-nav__collapse-divider {\n display: none;\n }\n }\n\n @media (min-width: 701px) {\n /* ITEM labels are HIDDEN VISUALLY, NOT REMOVED. display:none would drop them from the\n accessibility tree too, leaving every rail button unnamed — an icon-only rail a screen\n reader cannot read. This keeps the name and only takes the pixels. (The tooltips/aria the\n items grow while collapsed come from EffectiveIconOnly.) */\n :host(.mj-left-nav-host--collapsed) .mj-left-nav__text {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n }\n\n /* With the text out of flow the icon is the only thing left; centre it in the narrow rail. */\n :host(.mj-left-nav-host--collapsed) .mj-left-nav__item {\n justify-content: center;\n gap: 0;\n margin-inline: auto;\n position: relative;\n }\n\n /* SECTION labels become a small centred divider line, keeping the label's OWN box (padding +\n height) so the vertical rhythm is IDENTICAL to the expanded rail — the words come back in\n the exact same place on expand. color: transparent hides the glyphs without removing them\n from the a11y tree or changing the box height; overflow clips the transparent glyphs so\n they can never make the narrow rail horizontally scrollable. */\n :host(.mj-left-nav-host--collapsed) .mj-left-nav__section-label {\n position: relative;\n color: transparent;\n overflow: hidden;\n }\n :host(.mj-left-nav-host--collapsed) .mj-left-nav__section-label::after {\n content: '';\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n width: 20px;\n height: 1px;\n background: var(--mj-border-default);\n }\n\n /* No row edge exists in the narrow rail, so the badge docks on the icon's top-right corner\n (the activity-bar idiom) as a compact attention count — presence = action needed. The\n centred icon itself never moves. */\n :host(.mj-left-nav-host--collapsed) .mj-left-nav__badge {\n margin-left: 0;\n align-self: auto;\n position: absolute;\n left: calc(50% + 4px);\n top: 4px;\n min-width: 15px;\n height: 15px;\n padding: 0 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 9px;\n line-height: 1;\n white-space: nowrap;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n }\n\n /* Belt-and-braces: a collapsed rail never scrolls sideways. */\n :host(.mj-left-nav-host--collapsed) .mj-left-nav {\n overflow-x: hidden;\n }\n }\n\n /* ── Mobile drawer pattern (≤700px) ──────────────────────────────────────\n Desktop shows the rail inline. On mobile the rail would otherwise eat the\n top of the screen, so we hide it off-canvas and surface a compact section\n switcher in its place. Tapping the switcher slides the full rail in from\n the left over a scrim — preserving trees, header/footer slots and badges\n (item descriptions hide in the drawer for a tighter list).\n\n The switcher / scrim / drawer-header are display:none above the\n breakpoint; the rail keeps its desktop layout there. */\n .mj-left-nav__switcher,\n .mj-left-nav__scrim,\n .mj-left-nav__drawer-header {\n display: none;\n }\n\n @media (max-width: 700px) {\n /* Host collapses to the switcher's footprint; the rail goes out of flow. */\n :host {\n width: 100% !important;\n }\n\n /* Hide item descriptions in the drawer — the label + icon are enough on a\n narrow sheet; the secondary line just adds clutter. */\n .mj-left-nav__description {\n display: none;\n }\n\n /* Section switcher — reads as a picker for the current section. */\n .mj-left-nav__switcher {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 11px 14px;\n border: none;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n text-align: left;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n }\n .mj-left-nav__switcher:active {\n background: var(--mj-bg-surface-active);\n }\n .mj-left-nav__switcher-icon {\n width: 18px;\n text-align: center;\n font-size: 14px;\n flex-shrink: 0;\n color: var(--mj-brand-primary);\n }\n .mj-left-nav__switcher-label {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .mj-left-nav__switcher-hint {\n flex-shrink: 0;\n font-size: 15px;\n color: var(--mj-text-muted);\n }\n\n /* Scrim — fades in with the drawer. */\n .mj-left-nav__scrim {\n display: block;\n position: fixed;\n inset: 0;\n background: var(--mj-bg-overlay);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.24s ease;\n z-index: 9998;\n }\n :host(.mj-left-nav-host--open) .mj-left-nav__scrim {\n opacity: 1;\n pointer-events: auto;\n }\n\n /* Rail becomes a left off-canvas drawer. */\n .mj-left-nav {\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n width: min(320px, 84vw) !important;\n height: 100%;\n max-height: none;\n border-right: 1px solid var(--mj-border-default);\n box-shadow: var(--mj-shadow-lg);\n transform: translateX(-100%);\n transition: transform 0.24s ease;\n z-index: 9999;\n padding-top: 0;\n }\n :host(.mj-left-nav-host--open) .mj-left-nav {\n transform: translateX(0);\n }\n\n /* Drawer header — sticky title + close button. */\n .mj-left-nav__drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n position: sticky;\n top: 0;\n z-index: 1;\n padding: 14px 12px 10px;\n margin: 0 -8px 4px;\n padding-left: 16px;\n background: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-subtle);\n }\n .mj-left-nav__drawer-title {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--mj-text-muted);\n }\n .mj-left-nav__drawer-close {\n flex-shrink: 0;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: transparent;\n border-radius: 8px;\n cursor: pointer;\n color: var(--mj-text-secondary);\n font-size: 16px;\n font-family: inherit;\n }\n .mj-left-nav__drawer-close:active {\n background: var(--mj-bg-surface-active);\n }\n }\n\n @media (max-width: 700px) and (prefers-reduced-motion: reduce) {\n .mj-left-nav,\n .mj-left-nav__scrim {\n transition: none;\n }\n }\n `]\n})\nexport class MJLeftNavComponent implements OnDestroy {\n /**\n * DOM id of the `<aside>`, so the collapse toggle's `aria-expanded` can name the region it\n * expands via `aria-controls`. Per-instance because a page may host more than one rail.\n */\n public readonly NavRegionId: string = `mj-left-nav-${++navRegionSeq}`;\n\n /**\n * The sections + items that make up the rail. Sections render top-to-bottom\n * in array order; items inside each section render in array order.\n */\n @Input() Sections: MJLeftNavSection[] = [];\n\n /**\n * The id of the currently active item. Items matching this id get the\n * active styling and `aria-current=\"page\"`.\n */\n @Input() ActiveId: string | null = null;\n\n /**\n * Width of the rail in pixels. Defaults to 240. Set to a smaller value\n * (e.g., 200) for denser shells. Only applies to the desktop rail — on mobile\n * the rail becomes a fixed-width off-canvas drawer.\n */\n @Input() Width: number = 240;\n\n /**\n * Set by a consumer that has visually collapsed the rail to an icons-only strip (via `Width` plus\n * its own CSS hiding the labels). When true, each item gains a native `title` tooltip AND an\n * `aria-label` carrying its full text.\n *\n * Why it is opt-in rather than always-on:\n * - **Tooltip**: with labels visible, a tooltip that just repeats the label is redundant noise on\n * every hover. Icon-only rails everywhere (VS Code's activity bar, Slack, Linear, Jira, Figma)\n * show one; expanded rails do not.\n * - **aria-label**: when the label IS visible it already names the button, and adding an identical\n * aria-label is at best a no-op. When the consumer's CSS hides `.mj-left-nav__text`, the button's\n * accessible name goes EMPTY — a screen reader announces \"button\" with no destination. That is\n * the hole this closes.\n *\n * Leaving it false reproduces the previous markup exactly, so existing consumers are unaffected.\n */\n @Input() IconOnly: boolean = false;\n\n /**\n * Opt-in desktop collapse: renders a compact double-angle toggle chip at the top of the rail and,\n * when {@link Collapsed}, presents the rail as an icons-only strip ({@link CollapsedWidth} wide) —\n * labels visually hidden but kept in the a11y tree, section labels folded to divider lines,\n * badges docked on the icon corner, and per-item tooltips/aria auto-enabled.\n *\n * The component owns the PRESENTATION only — the consumer owns the STATE (bind `[(Collapsed)]`\n * and persist it however the app persists preferences), the same split as `ExpandedIds`.\n * Desktop-only: ≤700px the rail is the off-canvas drawer and the toggle hides.\n *\n * Two things to know when the rail's content is richer than a flat icon list:\n * - **Tree sections fold to their top level while collapsed** (see {@link ShowTreeStructure});\n * a top-level item stands in as active for an active descendant, and the tree returns intact\n * on expand. Items with no `icon` render a monogram so they stay visible and hittable.\n * - **The `[header]` / `[footer]` slots are yours.** The rail can't restyle projected content,\n * so a wide header (logo lockup + wordmark, a filter box) will overflow a `CollapsedWidth`\n * strip. Bind the same collapsed flag in the consumer and project a compact variant.\n *\n * Deliberately NOT included: hover-to-peek (auto-expand on pointer hover) — removed by ruling\n * (2026-07-30): a rail that expands when the pointer grazes it moves the target the user was\n * aiming at. Deliberate toggling beats clever hovering.\n */\n @Input() Collapsible: boolean = false;\n\n /** Whether the rail is currently collapsed to icons. Two-way bindable via `[(Collapsed)]`. */\n @Input() Collapsed: boolean = false;\n\n /** Emitted when the user clicks the collapse toggle. Consumer updates + persists the state. */\n @Output() CollapsedChange = new EventEmitter<boolean>();\n\n /**\n * Width of the collapsed icons-only strip. The default 60 is derived from the rail's own\n * numbers — 8+8 container padding, 12+12 item padding, 18px icon, 1px border — so icons stay\n * centred; retune it if those change.\n */\n @Input() CollapsedWidth: number = 60;\n\n /**\n * True below the 701px breakpoint, where the rail stops being a rail and becomes the off-canvas\n * drawer. Tracked in TS — not just CSS — because collapse is more than styling: it also decides\n * whether tree sections fold and whether items are named by tooltip. The collapsed STYLES are\n * gated to ≥701px, so without this the drawer would keep the collapsed BEHAVIOR while showing\n * full labels: a flattened tree, and a monogram beside the very label it stands in for.\n *\n * Mirrors the matchMedia pattern in `mj-filter-popover` (same package).\n */\n private isMobileViewport: boolean = false;\n\n private readonly zone = inject(NgZone);\n private readonly cdr = inject(ChangeDetectorRef);\n\n private readonly mediaQuery: MediaQueryList | null =\n typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(max-width: 700px)')\n : null;\n\n // matchMedia 'change' fires OUTSIDE Angular's zone, so re-enter before flipping state the\n // template reads.\n private readonly onMediaChange = (e: MediaQueryListEvent): void => {\n this.zone.run(() => {\n this.isMobileViewport = e.matches;\n this.cdr.markForCheck();\n });\n };\n\n constructor() {\n if (this.mediaQuery) {\n this.isMobileViewport = this.mediaQuery.matches;\n this.mediaQuery.addEventListener('change', this.onMediaChange);\n }\n }\n\n ngOnDestroy(): void {\n this.mediaQuery?.removeEventListener('change', this.onMediaChange);\n }\n\n /**\n * Whether the rail is collapsed *and* in a viewport where collapse means anything. Below 701px\n * the rail is the off-canvas drawer — already dismissable, already showing full labels — so a\n * persisted `Collapsed = true` must not follow the user down there.\n */\n public get IsNativelyCollapsed(): boolean {\n return this.Collapsible && this.Collapsed && !this.isMobileViewport;\n }\n\n /** The width the rail actually renders at, honoring the collapse state. */\n public get EffectiveWidth(): number {\n return this.IsNativelyCollapsed ? this.CollapsedWidth : this.Width;\n }\n\n /**\n * Icon-only presentation is on when the consumer forces it OR the rail is natively collapsed.\n *\n * `IconOnly` is deliberately NOT viewport-gated: that flag means the CONSUMER's own CSS has\n * narrowed the rail, and we can't know which breakpoints they gated it to. Withdrawing the\n * accessible names on their behalf could leave a genuinely icons-only rail unnamed — the exact\n * hole `IconOnly` exists to close. Native collapse is ours to gate; theirs is theirs.\n */\n public get EffectiveIconOnly(): boolean {\n return this.IconOnly || this.IsNativelyCollapsed;\n }\n\n /**\n * Whether tree structure (chevrons, depth indentation, expanded children) renders at all.\n *\n * An icons-only rail is ~60px wide and shows nothing but a centred glyph per row, which leaves a\n * tree with no way to express itself: the chevron would eat a third of the width and shove the\n * icon off-centre, each depth level would push its icon 14px further right until the rail clipped\n * it, and a child's icon (optional, and usually absent on tree leaves) would say nothing about\n * which branch it belongs to. So while icons-only, each section renders its TOP-LEVEL items only —\n * the same choice every icon rail makes (VS Code's activity bar shows one icon per container and\n * defers the tree to the expanded pane).\n *\n * Nothing is lost on the way back: `ExpandedIds` is consumer-owned and untouched here, so the tree\n * returns exactly as the user left it the moment the rail expands.\n */\n public get ShowTreeStructure(): boolean {\n return !this.EffectiveIconOnly;\n }\n\n /**\n * Whether an item paints as active. Normally an exact {@link ActiveId} match — but while the tree\n * is suppressed (see {@link ShowTreeStructure}) the active item may be a child that isn't rendered,\n * which would leave the collapsed rail with NOTHING highlighted and no sense of place. In that\n * state a top-level item stands in for its active descendant.\n */\n public IsItemActive(item: MJLeftNavItem): boolean {\n if (item.id === this.ActiveId) return true;\n return this.EffectiveIconOnly && this.hasActiveDescendant(item);\n }\n\n /**\n * `'page'` for the active item itself; `'true'` for a stand-in ancestor, which is current in the\n * set but is not the page the user is on. Distinguishing them keeps the announcement honest.\n */\n public AriaCurrentFor(item: MJLeftNavItem): 'page' | 'true' | null {\n if (item.id === this.ActiveId) return 'page';\n return this.IsItemActive(item) ? 'true' : null;\n }\n\n private hasActiveDescendant(item: MJLeftNavItem): boolean {\n if (!this.ActiveId || !item.children?.length) return false;\n return this.findItemById(item.children, this.ActiveId) !== undefined;\n }\n\n /**\n * Monogram for an icon-less item in an icons-only rail. `Array.from` rather than `charAt` so a\n * label starting with an emoji or an astral character yields that whole character instead of half\n * a surrogate pair. Falls back to a bullet for a blank label so the button is never empty.\n */\n public ItemInitial(item: MJLeftNavItem): string {\n return Array.from(item.label?.trim() ?? '')[0] ?? '•';\n }\n\n /**\n * Left margin that pins the toggle chip at the collapsed band's centre in BOTH modes, so the\n * chip never relocates on toggle (the user never re-aims). Derived, not magic:\n * (CollapsedWidth − 38px chip)/2 from the rail edge, minus the rail's 8px container padding.\n * 38 = the 18px icon box + 2×10px chip padding.\n */\n public get CollapseToggleMarginPx(): number {\n return Math.max(0, Math.round((this.CollapsedWidth - 38) / 2 - 8));\n }\n\n /** Reflects the collapsed state onto the host so the collapsed CSS can scope to it. */\n @HostBinding('class.mj-left-nav-host--collapsed')\n get isCollapsedHost(): boolean {\n return this.IsNativelyCollapsed;\n }\n\n /** Flip + announce. The consumer receives the new value and persists it. */\n public ToggleCollapsed(): void {\n this.Collapsed = !this.Collapsed;\n this.CollapsedChange.emit(this.Collapsed);\n }\n\n /**\n * The text surfaced as an icon-only item's tooltip / accessible name. Folds in the description and\n * badge because collapsing the rail hides both: the badge survives as an unlabeled corner count,\n * so \"Batch approvals (2)\" is the only place a hovering user learns what the 2 counts.\n */\n public ItemTooltip(item: MJLeftNavItem): string {\n const parts = [item.label];\n if (item.description) parts.push(`— ${item.description}`);\n if (item.badge != null) parts.push(`(${item.badge})`);\n return parts.join(' ');\n }\n\n /**\n * Label shown on the mobile drawer header and used as the switcher fallback\n * label when no item is active. Defaults to `'Menu'`. Set to something\n * domain-specific (e.g. `'Sections'`, `'Test Suites'`) for clearer context.\n */\n @Input() MobileTitle: string = 'Menu';\n\n /**\n * Ids of currently-expanded tree items. Items whose id appears here render\n * with a down-chevron and their `children` are visible; items not in this\n * list render with a right-chevron and their children stay collapsed.\n * Consumer owns the state — toggle in response to `(ItemToggled)`.\n *\n * Only applies to items where `children !== undefined`; flat items ignore it.\n */\n @Input() ExpandedIds: string[] = [];\n\n /**\n * Emitted when a non-disabled item is clicked.\n */\n @Output() ItemClicked = new EventEmitter<MJLeftNavItem>();\n\n /**\n * Emitted when a tree item's chevron is clicked. Consumer should toggle\n * the item's id in / out of `ExpandedIds`. Does NOT also fire `ItemClicked`.\n */\n @Output() ItemToggled = new EventEmitter<MJLeftNavItem>();\n\n /**\n * Whether the mobile off-canvas drawer is open. Component-owned, transient UI\n * state — no effect above the 700px breakpoint where the rail is always inline.\n */\n public MobileNavOpen = false;\n\n /** Reflects {@link MobileNavOpen} onto the host so CSS can drive the drawer. */\n @HostBinding('class.mj-left-nav-host--open')\n get isMobileNavOpen(): boolean {\n return this.MobileNavOpen;\n }\n\n OpenMobileNav(): void {\n this.MobileNavOpen = true;\n }\n\n CloseMobileNav(): void {\n this.MobileNavOpen = false;\n }\n\n /** Escape closes the mobile drawer. No-op when it's already closed. */\n @HostListener('document:keydown.escape')\n onEscape(): void {\n if (this.MobileNavOpen) {\n this.MobileNavOpen = false;\n }\n }\n\n OnItemClick(item: MJLeftNavItem): void {\n if (item.disabled) return;\n // Selecting a leaf dismisses the mobile drawer; harmless on desktop.\n this.MobileNavOpen = false;\n this.ItemClicked.emit(item);\n }\n\n OnItemToggle(event: Event, item: MJLeftNavItem): void {\n event.stopPropagation();\n // Expanding/collapsing a tree branch keeps the drawer open.\n this.ItemToggled.emit(item);\n }\n\n /**\n * The item matching {@link ActiveId} (searched recursively through trees),\n * used to label the mobile section switcher with the current section's icon\n * and label. Returns `undefined` when nothing is active.\n */\n get ActiveTriggerItem(): MJLeftNavItem | undefined {\n if (!this.ActiveId) return undefined;\n return this.findItemById(this.Sections.flatMap((s) => s.items), this.ActiveId);\n }\n\n private findItemById(items: MJLeftNavItem[], id: string): MJLeftNavItem | undefined {\n for (const item of items) {\n if (item.id === id) return item;\n if (item.children?.length) {\n const found = this.findItemById(item.children, id);\n if (found) return found;\n }\n }\n return undefined;\n }\n\n IsExpanded(id: string): boolean {\n return this.ExpandedIds.indexOf(id) >= 0;\n }\n\n /**\n * Whether any top-level item in this section actually has children to expand.\n * The template calls this once per section; the result is threaded through\n * the recursive itemTpl so leaf nodes only reserve a chevron-column\n * placeholder when a sibling will actually render a chevron.\n *\n * Top-level check is sufficient — if a deeply-nested item has children, its\n * parent's `children.length > 0` already qualifies the section as expandable.\n */\n HasExpandableItems(items: MJLeftNavItem[]): boolean {\n for (const item of items) {\n if (item.children && item.children.length > 0) return true;\n }\n return false;\n }\n}\n"]}
@@ -33,7 +33,7 @@ function MjSlidePanelComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
33
33
  } if (rf & 2) {
34
34
  const ctx_r1 = i0.ɵɵnextContext();
35
35
  i0.ɵɵstyleProp("width", ctx_r1.WidthPx, "px")("max-width", ctx_r1.MaxWidthRatio * 100 + "vw");
36
- i0.ɵɵclassProp("visible", ctx_r1.IsVisible);
36
+ i0.ɵɵclassProp("sp-open", ctx_r1.IsVisible);
37
37
  i0.ɵɵadvance();
38
38
  i0.ɵɵconditional(ctx_r1.Resizable ? 1 : -1);
39
39
  i0.ɵɵadvance();
@@ -62,7 +62,7 @@ function MjSlidePanelComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
62
62
  } if (rf & 2) {
63
63
  const ctx_r1 = i0.ɵɵnextContext();
64
64
  i0.ɵɵstyleProp("width", ctx_r1.WidthPx, "px");
65
- i0.ɵɵclassProp("visible", ctx_r1.IsVisible);
65
+ i0.ɵɵclassProp("sp-open", ctx_r1.IsVisible);
66
66
  i0.ɵɵadvance();
67
67
  i0.ɵɵconditional(ctx_r1.Title ? 1 : -1);
68
68
  } }
@@ -210,7 +210,7 @@ export class MjSlidePanelComponent {
210
210
  static ɵfac = function MjSlidePanelComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MjSlidePanelComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.NgZone), i0.ɵɵdirectiveInject(i0.ElementRef)); };
211
211
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MjSlidePanelComponent, selectors: [["mj-slide-panel"]], hostBindings: function MjSlidePanelComponent_HostBindings(rf, ctx) { if (rf & 1) {
212
212
  i0.ɵɵlistener("keydown.escape", function MjSlidePanelComponent_keydown_escape_HostBindingHandler() { return ctx.OnEscapeKey(); }, i0.ɵɵresolveDocument);
213
- } }, inputs: { Mode: "Mode", Title: "Title", Visible: "Visible", Resizable: "Resizable", MinWidthPx: "MinWidthPx", MaxWidthRatio: "MaxWidthRatio", WidthPx: "WidthPx", CanClose: "CanClose" }, outputs: { Closed: "Closed", WidthChanged: "WidthChanged" }, ngContentSelectors: _c1, decls: 3, vars: 6, consts: [[1, "sp-backdrop", 3, "click"], [1, "sp-panel", 3, "visible", "width", "max-width"], [1, "sp-dialog", 3, "visible", "width"], [1, "sp-panel"], [1, "sp-resize-handle"], [1, "sp-header"], [1, "sp-body"], [1, "sp-resize-handle", 3, "mousedown"], [1, "sp-resize-grip"], [1, "sp-title"], [1, "sp-close-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "sp-dialog"], [1, "sp-body", "sp-dialog-body"]], template: function MjSlidePanelComponent_Template(rf, ctx) { if (rf & 1) {
213
+ } }, inputs: { Mode: "Mode", Title: "Title", Visible: "Visible", Resizable: "Resizable", MinWidthPx: "MinWidthPx", MaxWidthRatio: "MaxWidthRatio", WidthPx: "WidthPx", CanClose: "CanClose" }, outputs: { Closed: "Closed", WidthChanged: "WidthChanged" }, ngContentSelectors: _c1, decls: 3, vars: 6, consts: [[1, "sp-backdrop", 3, "click"], [1, "sp-panel", 3, "sp-open", "width", "max-width"], [1, "sp-dialog", 3, "sp-open", "width"], [1, "sp-panel"], [1, "sp-resize-handle"], [1, "sp-header"], [1, "sp-body"], [1, "sp-resize-handle", 3, "mousedown"], [1, "sp-resize-grip"], [1, "sp-title"], [1, "sp-close-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "sp-dialog"], [1, "sp-body", "sp-dialog-body"]], template: function MjSlidePanelComponent_Template(rf, ctx) { if (rf & 1) {
214
214
  i0.ɵɵprojectionDef(_c0);
215
215
  i0.ɵɵdomElementStart(0, "div", 0);
216
216
  i0.ɵɵdomListener("click", function MjSlidePanelComponent_Template_div_click_0_listener() { return ctx.OnBackdropClick(); });
@@ -218,16 +218,16 @@ export class MjSlidePanelComponent {
218
218
  i0.ɵɵconditionalCreate(1, MjSlidePanelComponent_Conditional_1_Template, 5, 8, "div", 1);
219
219
  i0.ɵɵconditionalCreate(2, MjSlidePanelComponent_Conditional_2_Template, 4, 5, "div", 2);
220
220
  } if (rf & 2) {
221
- i0.ɵɵclassProp("visible", ctx.IsVisible)("sp-dialog-backdrop", ctx.Mode === "dialog");
221
+ i0.ɵɵclassProp("sp-open", ctx.IsVisible)("sp-dialog-backdrop", ctx.Mode === "dialog");
222
222
  i0.ɵɵadvance();
223
223
  i0.ɵɵconditional(ctx.Mode === "slide" ? 1 : -1);
224
224
  i0.ɵɵadvance();
225
225
  i0.ɵɵconditional(ctx.Mode === "dialog" ? 2 : -1);
226
- } }, styles: ["\n\n\n\n\n\n\n.sp-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 1000;\n transition: background 0.3s ease;\n pointer-events: none;\n}\n\n.sp-backdrop.visible[_ngcontent-%COMP%] {\n background: var(--mj-bg-overlay);\n pointer-events: auto;\n}\n\n.sp-dialog-backdrop.visible[_ngcontent-%COMP%] {\n background: var(--mj-bg-overlay);\n}\n\n\n\n\n\n\n\n\n.sp-panel[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n right: 0;\n height: 100vh;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 400px;\n max-width: 92vw;\n}\n\n.sp-panel.visible[_ngcontent-%COMP%] {\n \n\n\n\n\n\n transform: none;\n}\n\n\n\n\n\n\n\n\n.sp-dialog[_ngcontent-%COMP%] {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) scale(0.95);\n max-height: 85vh;\n background: var(--mj-bg-surface);\n border-radius: 16px;\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n opacity: 0;\n transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 360px;\n max-width: 95vw;\n}\n\n.sp-dialog.visible[_ngcontent-%COMP%] {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1);\n}\n\n\n\n\n\n\n\n\n.sp-resize-handle[_ngcontent-%COMP%] {\n position: absolute;\n left: -4px;\n top: 0;\n width: 8px;\n height: 100%;\n cursor: col-resize;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sp-resize-handle[_ngcontent-%COMP%]:hover .sp-resize-grip[_ngcontent-%COMP%], \n.sp-resize-handle[_ngcontent-%COMP%]:active .sp-resize-grip[_ngcontent-%COMP%] {\n opacity: 1;\n background: var(--mj-brand-primary);\n}\n\n.sp-resize-grip[_ngcontent-%COMP%] {\n width: 3px;\n height: 40px;\n background: var(--mj-border-strong);\n border-radius: 3px;\n opacity: 0;\n transition: opacity 0.2s ease, background 0.2s ease;\n}\n\n.sp-resize-handle[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 4%, transparent);\n}\n\n\n\n\n\n\n\n\n.sp-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.sp-title[_ngcontent-%COMP%] {\n font-size: 18px;\n font-weight: 700;\n color: var(--mj-text-primary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n}\n\n.sp-close-btn[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: none;\n border: none;\n border-radius: 8px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.15s ease;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.sp-close-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n\n\n\n\n\n\n\n.sp-body[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.sp-dialog-body[_ngcontent-%COMP%] {\n padding: 0;\n}\n\n\n\n\n\n\n\n\n@media (max-width: 768px) {\n .sp-panel[_ngcontent-%COMP%] {\n width: 100% !important;\n min-width: unset;\n }\n\n .sp-resize-handle[_ngcontent-%COMP%] {\n display: none;\n }\n\n .sp-dialog[_ngcontent-%COMP%] {\n width: 95vw !important;\n max-height: 90vh;\n }\n}"], changeDetection: 0 });
226
+ } }, styles: ["\n\n\n\n\n\n\n.sp-backdrop[_ngcontent-%COMP%] {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 1000;\n transition: background 0.3s ease;\n pointer-events: none;\n}\n\n\n\n\n\n\n\n.sp-backdrop.sp-open[_ngcontent-%COMP%] {\n background: var(--mj-bg-overlay);\n pointer-events: auto;\n}\n\n.sp-dialog-backdrop.sp-open[_ngcontent-%COMP%] {\n background: var(--mj-bg-overlay);\n}\n\n\n\n\n\n\n\n\n.sp-panel[_ngcontent-%COMP%] {\n position: fixed;\n top: 0;\n right: 0;\n height: 100vh;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 400px;\n max-width: 92vw;\n}\n\n.sp-panel.sp-open[_ngcontent-%COMP%] {\n \n\n\n\n\n\n transform: none;\n}\n\n\n\n\n\n\n\n\n.sp-dialog[_ngcontent-%COMP%] {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) scale(0.95);\n max-height: 85vh;\n background: var(--mj-bg-surface);\n border-radius: 16px;\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n opacity: 0;\n transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 360px;\n max-width: 95vw;\n}\n\n.sp-dialog.sp-open[_ngcontent-%COMP%] {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1);\n}\n\n\n\n\n\n\n\n\n.sp-resize-handle[_ngcontent-%COMP%] {\n position: absolute;\n left: -4px;\n top: 0;\n width: 8px;\n height: 100%;\n cursor: col-resize;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sp-resize-handle[_ngcontent-%COMP%]:hover .sp-resize-grip[_ngcontent-%COMP%], \n.sp-resize-handle[_ngcontent-%COMP%]:active .sp-resize-grip[_ngcontent-%COMP%] {\n opacity: 1;\n background: var(--mj-brand-primary);\n}\n\n.sp-resize-grip[_ngcontent-%COMP%] {\n width: 3px;\n height: 40px;\n background: var(--mj-border-strong);\n border-radius: 3px;\n opacity: 0;\n transition: opacity 0.2s ease, background 0.2s ease;\n}\n\n.sp-resize-handle[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 4%, transparent);\n}\n\n\n\n\n\n\n\n\n.sp-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.sp-title[_ngcontent-%COMP%] {\n font-size: 18px;\n font-weight: 700;\n color: var(--mj-text-primary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n}\n\n.sp-close-btn[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: none;\n border: none;\n border-radius: 8px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.15s ease;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.sp-close-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n\n\n\n\n\n\n\n.sp-body[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.sp-dialog-body[_ngcontent-%COMP%] {\n padding: 0;\n}\n\n\n\n\n\n\n\n\n@media (max-width: 768px) {\n .sp-panel[_ngcontent-%COMP%] {\n width: 100% !important;\n min-width: unset;\n }\n\n .sp-resize-handle[_ngcontent-%COMP%] {\n display: none;\n }\n\n .sp-dialog[_ngcontent-%COMP%] {\n width: 95vw !important;\n max-height: 90vh;\n }\n}"], changeDetection: 0 });
227
227
  }
228
228
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MjSlidePanelComponent, [{
229
229
  type: Component,
230
- args: [{ standalone: true, selector: 'mj-slide-panel', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Backdrop overlay -->\n<div class=\"sp-backdrop\"\n [class.visible]=\"IsVisible\"\n [class.sp-dialog-backdrop]=\"Mode === 'dialog'\"\n (click)=\"OnBackdropClick()\">\n</div>\n\n<!-- Panel (slide-in from right) -->\n@if (Mode === 'slide') {\n <div\n class=\"sp-panel\"\n [class.visible]=\"IsVisible\"\n [style.width.px]=\"WidthPx\"\n [style.max-width]=\"(MaxWidthRatio * 100) + 'vw'\">\n <!-- Resize handle -->\n @if (Resizable) {\n <div class=\"sp-resize-handle\" (mousedown)=\"OnResizeStart($event)\">\n <div class=\"sp-resize-grip\"></div>\n </div>\n }\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body\">\n <ng-content></ng-content>\n </div>\n </div>\n}\n\n<!-- Dialog (centered modal) -->\n@if (Mode === 'dialog') {\n <div\n class=\"sp-dialog\"\n [class.visible]=\"IsVisible\"\n [style.width.px]=\"WidthPx\">\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body sp-dialog-body\">\n <ng-content select=\"[dialog-content]\"></ng-content>\n </div>\n </div>\n}\n", styles: ["/* ================================================================= */\n/* BACKDROP */\n/* ================================================================= */\n\n.sp-backdrop {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 1000;\n transition: background 0.3s ease;\n pointer-events: none;\n}\n\n.sp-backdrop.visible {\n background: var(--mj-bg-overlay);\n pointer-events: auto;\n}\n\n.sp-dialog-backdrop.visible {\n background: var(--mj-bg-overlay);\n}\n\n/* ================================================================= */\n/* SLIDE PANEL */\n/* ================================================================= */\n\n.sp-panel {\n position: fixed;\n top: 0;\n right: 0;\n height: 100vh;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 400px;\n max-width: 92vw;\n}\n\n.sp-panel.visible {\n /* `none` (not translateX(0)) on purpose: a non-none transform makes this panel\n the containing block for position:fixed DESCENDANTS (dropdown panels, popovers\n that position via getBoundingClientRect viewport math), displacing them\n offscreen. `none` interpolates as the identity matrix, so the slide-in\n animation is unaffected while open-state descendants get the real viewport. */\n transform: none;\n}\n\n/* ================================================================= */\n/* DIALOG (centered modal) */\n/* ================================================================= */\n\n.sp-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) scale(0.95);\n max-height: 85vh;\n background: var(--mj-bg-surface);\n border-radius: 16px;\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n opacity: 0;\n transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 360px;\n max-width: 95vw;\n}\n\n.sp-dialog.visible {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* ================================================================= */\n/* RESIZE HANDLE */\n/* ================================================================= */\n\n.sp-resize-handle {\n position: absolute;\n left: -4px;\n top: 0;\n width: 8px;\n height: 100%;\n cursor: col-resize;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sp-resize-handle:hover .sp-resize-grip,\n.sp-resize-handle:active .sp-resize-grip {\n opacity: 1;\n background: var(--mj-brand-primary);\n}\n\n.sp-resize-grip {\n width: 3px;\n height: 40px;\n background: var(--mj-border-strong);\n border-radius: 3px;\n opacity: 0;\n transition: opacity 0.2s ease, background 0.2s ease;\n}\n\n.sp-resize-handle:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 4%, transparent);\n}\n\n/* ================================================================= */\n/* HEADER */\n/* ================================================================= */\n\n.sp-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.sp-title {\n font-size: 18px;\n font-weight: 700;\n color: var(--mj-text-primary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n}\n\n.sp-close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: none;\n border: none;\n border-radius: 8px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.15s ease;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.sp-close-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n/* ================================================================= */\n/* BODY */\n/* ================================================================= */\n\n.sp-body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.sp-dialog-body {\n padding: 0;\n}\n\n/* ================================================================= */\n/* RESPONSIVE */\n/* ================================================================= */\n\n@media (max-width: 768px) {\n .sp-panel {\n width: 100% !important;\n min-width: unset;\n }\n\n .sp-resize-handle {\n display: none;\n }\n\n .sp-dialog {\n width: 95vw !important;\n max-height: 90vh;\n }\n}\n"] }]
230
+ args: [{ standalone: true, selector: 'mj-slide-panel', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Backdrop overlay -->\n<div class=\"sp-backdrop\"\n [class.sp-open]=\"IsVisible\"\n [class.sp-dialog-backdrop]=\"Mode === 'dialog'\"\n (click)=\"OnBackdropClick()\">\n</div>\n\n<!-- Panel (slide-in from right) -->\n@if (Mode === 'slide') {\n <div\n class=\"sp-panel\"\n [class.sp-open]=\"IsVisible\"\n [style.width.px]=\"WidthPx\"\n [style.max-width]=\"(MaxWidthRatio * 100) + 'vw'\">\n <!-- Resize handle -->\n @if (Resizable) {\n <div class=\"sp-resize-handle\" (mousedown)=\"OnResizeStart($event)\">\n <div class=\"sp-resize-grip\"></div>\n </div>\n }\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body\">\n <ng-content></ng-content>\n </div>\n </div>\n}\n\n<!-- Dialog (centered modal) -->\n@if (Mode === 'dialog') {\n <div\n class=\"sp-dialog\"\n [class.sp-open]=\"IsVisible\"\n [style.width.px]=\"WidthPx\">\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body sp-dialog-body\">\n <ng-content select=\"[dialog-content]\"></ng-content>\n </div>\n </div>\n}\n", styles: ["/* ================================================================= */\n/* BACKDROP */\n/* ================================================================= */\n\n.sp-backdrop {\n position: fixed;\n inset: 0;\n background: transparent;\n z-index: 1000;\n transition: background 0.3s ease;\n pointer-events: none;\n}\n\n/* State class is sp-open, NEVER `visible`: explorer-app ships a global\n `.visible { visibility: visible !important }` utility, and a state class\n named `visible` accidentally matches it \u2014 forcing the panel to punch\n through ancestors hidden with visibility:hidden (e.g. the shell's\n region-hidden main area). */\n.sp-backdrop.sp-open {\n background: var(--mj-bg-overlay);\n pointer-events: auto;\n}\n\n.sp-dialog-backdrop.sp-open {\n background: var(--mj-bg-overlay);\n}\n\n/* ================================================================= */\n/* SLIDE PANEL */\n/* ================================================================= */\n\n.sp-panel {\n position: fixed;\n top: 0;\n right: 0;\n height: 100vh;\n background: var(--mj-bg-surface);\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 400px;\n max-width: 92vw;\n}\n\n.sp-panel.sp-open {\n /* `none` (not translateX(0)) on purpose: a non-none transform makes this panel\n the containing block for position:fixed DESCENDANTS (dropdown panels, popovers\n that position via getBoundingClientRect viewport math), displacing them\n offscreen. `none` interpolates as the identity matrix, so the slide-in\n animation is unaffected while open-state descendants get the real viewport. */\n transform: none;\n}\n\n/* ================================================================= */\n/* DIALOG (centered modal) */\n/* ================================================================= */\n\n.sp-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) scale(0.95);\n max-height: 85vh;\n background: var(--mj-bg-surface);\n border-radius: 16px;\n box-shadow: var(--mj-shadow-lg);\n z-index: 1001;\n display: flex;\n flex-direction: column;\n opacity: 0;\n transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);\n min-width: 360px;\n max-width: 95vw;\n}\n\n.sp-dialog.sp-open {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* ================================================================= */\n/* RESIZE HANDLE */\n/* ================================================================= */\n\n.sp-resize-handle {\n position: absolute;\n left: -4px;\n top: 0;\n width: 8px;\n height: 100%;\n cursor: col-resize;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sp-resize-handle:hover .sp-resize-grip,\n.sp-resize-handle:active .sp-resize-grip {\n opacity: 1;\n background: var(--mj-brand-primary);\n}\n\n.sp-resize-grip {\n width: 3px;\n height: 40px;\n background: var(--mj-border-strong);\n border-radius: 3px;\n opacity: 0;\n transition: opacity 0.2s ease, background 0.2s ease;\n}\n\n.sp-resize-handle:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 4%, transparent);\n}\n\n/* ================================================================= */\n/* HEADER */\n/* ================================================================= */\n\n.sp-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 24px;\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n}\n\n.sp-title {\n font-size: 18px;\n font-weight: 700;\n color: var(--mj-text-primary);\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n}\n\n.sp-close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n background: none;\n border: none;\n border-radius: 8px;\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.15s ease;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.sp-close-btn:hover {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-primary);\n}\n\n/* ================================================================= */\n/* BODY */\n/* ================================================================= */\n\n.sp-body {\n flex: 1;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.sp-dialog-body {\n padding: 0;\n}\n\n/* ================================================================= */\n/* RESPONSIVE */\n/* ================================================================= */\n\n@media (max-width: 768px) {\n .sp-panel {\n width: 100% !important;\n min-width: unset;\n }\n\n .sp-resize-handle {\n display: none;\n }\n\n .sp-dialog {\n width: 95vw !important;\n max-height: 90vh;\n }\n}\n"] }]
231
231
  }], () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.ElementRef }], { Mode: [{
232
232
  type: Input
233
233
  }], Title: [{
@@ -1 +1 @@
1
- {"version":3,"file":"slide-panel.component.js","sourceRoot":"","sources":["../../../src/lib/slide-panel/slide-panel.component.ts","../../../src/lib/slide-panel/slide-panel.component.html"],"names":[],"mappings":"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","sourcesContent":["import {\n Component, Input, Output, EventEmitter, OnInit, OnDestroy,\n ChangeDetectorRef, ChangeDetectionStrategy, HostListener, ElementRef, NgZone\n} from '@angular/core';\n\n/** Display mode for the slide panel container. */\nexport type SlidePanelMode = 'slide' | 'dialog';\n\n/**\n * Generic slide-in / centered-dialog panel primitive.\n *\n * - `Mode='slide'` → slides in from the right edge, full viewport height, optionally resizable.\n * - `Mode='dialog'` → centered modal card.\n *\n * Pure chrome: it owns the backdrop, open/close animation, resize handle, optional\n * title bar, and content projection. It performs no data work and has no routing.\n * Consumers project their content and handle Save/close via the projected UI and the\n * `Closed` output.\n *\n * @example\n * ```html\n * <mj-slide-panel [Visible]=\"open\" Title=\"Details\" (Closed)=\"open = false\">\n * <my-content></my-content>\n * </mj-slide-panel>\n * ```\n */\n@Component({\n standalone: true,\n selector: 'mj-slide-panel',\n templateUrl: './slide-panel.component.html',\n styleUrls: ['./slide-panel.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class MjSlidePanelComponent implements OnInit, OnDestroy {\n @Input() Mode: SlidePanelMode = 'slide';\n @Input() Title = '';\n @Input()\n set Visible(value: boolean) {\n const changed = this._visible !== value;\n this._visible = value;\n if (changed && this.initialized) {\n if (value) {\n // Opening: animate in on next microtask\n Promise.resolve().then(() => {\n this.IsVisible = true;\n this.cdr.markForCheck();\n });\n } else {\n this.IsVisible = false;\n this.cdr.markForCheck();\n }\n }\n }\n get Visible(): boolean {\n return this._visible;\n }\n private _visible = true;\n private initialized = false;\n\n @Input() Resizable = true;\n @Input() MinWidthPx = 400;\n @Input() MaxWidthRatio = 0.92;\n\n /** Initial width in pixels. Defaults to 65% of viewport for slide, 800px for dialog. */\n @Input()\n set WidthPx(value: number) {\n this._widthPx = value;\n }\n get WidthPx(): number {\n return this._widthPx;\n }\n\n @Output() Closed = new EventEmitter<void>();\n @Output() WidthChanged = new EventEmitter<number>();\n\n /**\n * Optional guard called before any close gesture (X button, backdrop click, Escape).\n * Return `false` to cancel the close — e.g., when an in-progress operation is running.\n * The panel itself never shows a dialog; the consumer is responsible for any confirmation UI.\n */\n @Input() CanClose: (() => boolean) | null = null;\n\n public IsVisible = false;\n private _widthPx = 0;\n private isResizing = false;\n\n private boundOnResizeMove = this.onResizeMove.bind(this);\n private boundOnResizeEnd = this.onResizeEnd.bind(this);\n\n constructor(\n private cdr: ChangeDetectorRef,\n private ngZone: NgZone,\n private elRef: ElementRef\n ) {}\n\n ngOnInit(): void {\n if (this._widthPx === 0) {\n this._widthPx = this.Mode === 'dialog'\n ? 800\n : Math.max(this.MinWidthPx, Math.min(window.innerWidth * 0.65, 1000));\n }\n\n this.initialized = true;\n\n // Animate in on next microtask if initially visible\n if (this._visible) {\n Promise.resolve().then(() => {\n this.IsVisible = true;\n this.cdr.markForCheck();\n });\n }\n }\n\n ngOnDestroy(): void {\n document.removeEventListener('mousemove', this.boundOnResizeMove);\n document.removeEventListener('mouseup', this.boundOnResizeEnd);\n }\n\n @HostListener('document:keydown.escape')\n public OnEscapeKey(): void {\n this.OnClose();\n }\n\n public OnClose(): void {\n if (this.CanClose && !this.CanClose()) return;\n this.IsVisible = false;\n this._visible = false;\n this.cdr.markForCheck();\n // Wait for CSS transition to complete\n setTimeout(() => this.Closed.emit(), 300);\n }\n\n public OnBackdropClick(): void {\n this.OnClose();\n }\n\n // =========================================================================\n // Resize\n // =========================================================================\n\n public OnResizeStart(event: MouseEvent): void {\n if (!this.Resizable || this.Mode === 'dialog') return;\n event.preventDefault();\n this.isResizing = true;\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n\n this.ngZone.runOutsideAngular(() => {\n document.addEventListener('mousemove', this.boundOnResizeMove);\n document.addEventListener('mouseup', this.boundOnResizeEnd);\n });\n }\n\n private onResizeMove(event: MouseEvent): void {\n if (!this.isResizing) return;\n const viewportWidth = window.innerWidth;\n const maxWidth = viewportWidth * this.MaxWidthRatio;\n this._widthPx = Math.max(this.MinWidthPx, Math.min(maxWidth, viewportWidth - event.clientX));\n this.ngZone.run(() => this.cdr.markForCheck());\n }\n\n private onResizeEnd(): void {\n if (!this.isResizing) return;\n this.isResizing = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', this.boundOnResizeMove);\n document.removeEventListener('mouseup', this.boundOnResizeEnd);\n this.WidthChanged.emit(this._widthPx);\n }\n}\n","<!-- Backdrop overlay -->\n<div class=\"sp-backdrop\"\n [class.visible]=\"IsVisible\"\n [class.sp-dialog-backdrop]=\"Mode === 'dialog'\"\n (click)=\"OnBackdropClick()\">\n</div>\n\n<!-- Panel (slide-in from right) -->\n@if (Mode === 'slide') {\n <div\n class=\"sp-panel\"\n [class.visible]=\"IsVisible\"\n [style.width.px]=\"WidthPx\"\n [style.max-width]=\"(MaxWidthRatio * 100) + 'vw'\">\n <!-- Resize handle -->\n @if (Resizable) {\n <div class=\"sp-resize-handle\" (mousedown)=\"OnResizeStart($event)\">\n <div class=\"sp-resize-grip\"></div>\n </div>\n }\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body\">\n <ng-content></ng-content>\n </div>\n </div>\n}\n\n<!-- Dialog (centered modal) -->\n@if (Mode === 'dialog') {\n <div\n class=\"sp-dialog\"\n [class.visible]=\"IsVisible\"\n [style.width.px]=\"WidthPx\">\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body sp-dialog-body\">\n <ng-content select=\"[dialog-content]\"></ng-content>\n </div>\n </div>\n}\n"]}
1
+ {"version":3,"file":"slide-panel.component.js","sourceRoot":"","sources":["../../../src/lib/slide-panel/slide-panel.component.ts","../../../src/lib/slide-panel/slide-panel.component.html"],"names":[],"mappings":"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","sourcesContent":["import {\n Component, Input, Output, EventEmitter, OnInit, OnDestroy,\n ChangeDetectorRef, ChangeDetectionStrategy, HostListener, ElementRef, NgZone\n} from '@angular/core';\n\n/** Display mode for the slide panel container. */\nexport type SlidePanelMode = 'slide' | 'dialog';\n\n/**\n * Generic slide-in / centered-dialog panel primitive.\n *\n * - `Mode='slide'` → slides in from the right edge, full viewport height, optionally resizable.\n * - `Mode='dialog'` → centered modal card.\n *\n * Pure chrome: it owns the backdrop, open/close animation, resize handle, optional\n * title bar, and content projection. It performs no data work and has no routing.\n * Consumers project their content and handle Save/close via the projected UI and the\n * `Closed` output.\n *\n * @example\n * ```html\n * <mj-slide-panel [Visible]=\"open\" Title=\"Details\" (Closed)=\"open = false\">\n * <my-content></my-content>\n * </mj-slide-panel>\n * ```\n */\n@Component({\n standalone: true,\n selector: 'mj-slide-panel',\n templateUrl: './slide-panel.component.html',\n styleUrls: ['./slide-panel.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class MjSlidePanelComponent implements OnInit, OnDestroy {\n @Input() Mode: SlidePanelMode = 'slide';\n @Input() Title = '';\n @Input()\n set Visible(value: boolean) {\n const changed = this._visible !== value;\n this._visible = value;\n if (changed && this.initialized) {\n if (value) {\n // Opening: animate in on next microtask\n Promise.resolve().then(() => {\n this.IsVisible = true;\n this.cdr.markForCheck();\n });\n } else {\n this.IsVisible = false;\n this.cdr.markForCheck();\n }\n }\n }\n get Visible(): boolean {\n return this._visible;\n }\n private _visible = true;\n private initialized = false;\n\n @Input() Resizable = true;\n @Input() MinWidthPx = 400;\n @Input() MaxWidthRatio = 0.92;\n\n /** Initial width in pixels. Defaults to 65% of viewport for slide, 800px for dialog. */\n @Input()\n set WidthPx(value: number) {\n this._widthPx = value;\n }\n get WidthPx(): number {\n return this._widthPx;\n }\n\n @Output() Closed = new EventEmitter<void>();\n @Output() WidthChanged = new EventEmitter<number>();\n\n /**\n * Optional guard called before any close gesture (X button, backdrop click, Escape).\n * Return `false` to cancel the close — e.g., when an in-progress operation is running.\n * The panel itself never shows a dialog; the consumer is responsible for any confirmation UI.\n */\n @Input() CanClose: (() => boolean) | null = null;\n\n public IsVisible = false;\n private _widthPx = 0;\n private isResizing = false;\n\n private boundOnResizeMove = this.onResizeMove.bind(this);\n private boundOnResizeEnd = this.onResizeEnd.bind(this);\n\n constructor(\n private cdr: ChangeDetectorRef,\n private ngZone: NgZone,\n private elRef: ElementRef\n ) {}\n\n ngOnInit(): void {\n if (this._widthPx === 0) {\n this._widthPx = this.Mode === 'dialog'\n ? 800\n : Math.max(this.MinWidthPx, Math.min(window.innerWidth * 0.65, 1000));\n }\n\n this.initialized = true;\n\n // Animate in on next microtask if initially visible\n if (this._visible) {\n Promise.resolve().then(() => {\n this.IsVisible = true;\n this.cdr.markForCheck();\n });\n }\n }\n\n ngOnDestroy(): void {\n document.removeEventListener('mousemove', this.boundOnResizeMove);\n document.removeEventListener('mouseup', this.boundOnResizeEnd);\n }\n\n @HostListener('document:keydown.escape')\n public OnEscapeKey(): void {\n this.OnClose();\n }\n\n public OnClose(): void {\n if (this.CanClose && !this.CanClose()) return;\n this.IsVisible = false;\n this._visible = false;\n this.cdr.markForCheck();\n // Wait for CSS transition to complete\n setTimeout(() => this.Closed.emit(), 300);\n }\n\n public OnBackdropClick(): void {\n this.OnClose();\n }\n\n // =========================================================================\n // Resize\n // =========================================================================\n\n public OnResizeStart(event: MouseEvent): void {\n if (!this.Resizable || this.Mode === 'dialog') return;\n event.preventDefault();\n this.isResizing = true;\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n\n this.ngZone.runOutsideAngular(() => {\n document.addEventListener('mousemove', this.boundOnResizeMove);\n document.addEventListener('mouseup', this.boundOnResizeEnd);\n });\n }\n\n private onResizeMove(event: MouseEvent): void {\n if (!this.isResizing) return;\n const viewportWidth = window.innerWidth;\n const maxWidth = viewportWidth * this.MaxWidthRatio;\n this._widthPx = Math.max(this.MinWidthPx, Math.min(maxWidth, viewportWidth - event.clientX));\n this.ngZone.run(() => this.cdr.markForCheck());\n }\n\n private onResizeEnd(): void {\n if (!this.isResizing) return;\n this.isResizing = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n document.removeEventListener('mousemove', this.boundOnResizeMove);\n document.removeEventListener('mouseup', this.boundOnResizeEnd);\n this.WidthChanged.emit(this._widthPx);\n }\n}\n","<!-- Backdrop overlay -->\n<div class=\"sp-backdrop\"\n [class.sp-open]=\"IsVisible\"\n [class.sp-dialog-backdrop]=\"Mode === 'dialog'\"\n (click)=\"OnBackdropClick()\">\n</div>\n\n<!-- Panel (slide-in from right) -->\n@if (Mode === 'slide') {\n <div\n class=\"sp-panel\"\n [class.sp-open]=\"IsVisible\"\n [style.width.px]=\"WidthPx\"\n [style.max-width]=\"(MaxWidthRatio * 100) + 'vw'\">\n <!-- Resize handle -->\n @if (Resizable) {\n <div class=\"sp-resize-handle\" (mousedown)=\"OnResizeStart($event)\">\n <div class=\"sp-resize-grip\"></div>\n </div>\n }\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body\">\n <ng-content></ng-content>\n </div>\n </div>\n}\n\n<!-- Dialog (centered modal) -->\n@if (Mode === 'dialog') {\n <div\n class=\"sp-dialog\"\n [class.sp-open]=\"IsVisible\"\n [style.width.px]=\"WidthPx\">\n <!-- Header (if Title provided) -->\n @if (Title) {\n <div class=\"sp-header\">\n <h2 class=\"sp-title\">{{Title}}</h2>\n <button class=\"sp-close-btn\" (click)=\"OnClose()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n }\n <!-- Content projection -->\n <div class=\"sp-body sp-dialog-body\">\n <ng-content select=\"[dialog-content]\"></ng-content>\n </div>\n </div>\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Dev-mode guard for the tab chrome's one genuine failure mode: the `.mj-tabs*` styles live in a
3
+ * GLOBAL stylesheet (`@memberjunction/ng-ui-components/dist/lib/tabs/tabs.scss`), and a standalone
4
+ * host that forgets the import gets structurally-working but completely unstyled strips — bare divs,
5
+ * silently, with no error anywhere. This turns that silence into an actionable console warning.
6
+ *
7
+ * Detection: `tabs.scss` sets `--mj-tabs-chrome-loaded: 1` on `.mj-tabs`. If the computed value is
8
+ * absent on a rendered strip, the sheet isn't in the page. Dev-mode only, once per app, and never
9
+ * throws — a missing stylesheet must not take the component down with it.
10
+ *
11
+ * Called by BOTH strips (`mj-tabstrip` and `mj-workspace-tab-strip`) after view init.
12
+ */
13
+ export declare function warnIfTabChromeMissing(element: HTMLElement | null | undefined): void;
14
+ //# sourceMappingURL=tab-chrome-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-chrome-guard.d.ts","sourceRoot":"","sources":["../../../src/lib/tabs/tab-chrome-guard.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAmBpF"}
@@ -0,0 +1,36 @@
1
+ import { isDevMode } from '@angular/core';
2
+ /** Warn once per application, not once per strip — the fix is app-level either way. */
3
+ let warned = false;
4
+ /**
5
+ * Dev-mode guard for the tab chrome's one genuine failure mode: the `.mj-tabs*` styles live in a
6
+ * GLOBAL stylesheet (`@memberjunction/ng-ui-components/dist/lib/tabs/tabs.scss`), and a standalone
7
+ * host that forgets the import gets structurally-working but completely unstyled strips — bare divs,
8
+ * silently, with no error anywhere. This turns that silence into an actionable console warning.
9
+ *
10
+ * Detection: `tabs.scss` sets `--mj-tabs-chrome-loaded: 1` on `.mj-tabs`. If the computed value is
11
+ * absent on a rendered strip, the sheet isn't in the page. Dev-mode only, once per app, and never
12
+ * throws — a missing stylesheet must not take the component down with it.
13
+ *
14
+ * Called by BOTH strips (`mj-tabstrip` and `mj-workspace-tab-strip`) after view init.
15
+ */
16
+ export function warnIfTabChromeMissing(element) {
17
+ if (warned || !isDevMode() || !element || typeof getComputedStyle !== 'function') {
18
+ return;
19
+ }
20
+ try {
21
+ const target = element.classList.contains('mj-tabs') ? element : element.querySelector('.mj-tabs');
22
+ if (!target)
23
+ return;
24
+ if (!getComputedStyle(target).getPropertyValue('--mj-tabs-chrome-loaded').trim()) {
25
+ warned = true;
26
+ console.warn("[MJ tabs] The shared tab chrome stylesheet is not loaded, so tab strips will render unstyled. " +
27
+ "Add to your app's global stylesheet:\n" +
28
+ " @import '@memberjunction/ng-ui-components/dist/lib/tabs/tabs';\n" +
29
+ "(Apps on the MJ Explorer shell get this via ng-explorer-app's styles automatically.)");
30
+ }
31
+ }
32
+ catch {
33
+ // Never let diagnostics break the strip.
34
+ }
35
+ }
36
+ //# sourceMappingURL=tab-chrome-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-chrome-guard.js","sourceRoot":"","sources":["../../../src/lib/tabs/tab-chrome-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,uFAAuF;AACvF,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAuC;IAC5E,IAAI,MAAM,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,IAAI,OAAO,gBAAgB,KAAK,UAAU,EAAE,CAAC;QACjF,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAc,UAAU,CAAC,CAAC;QAChH,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,yBAAyB,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACjF,MAAM,GAAG,IAAI,CAAC;YACd,OAAO,CAAC,IAAI,CACV,gGAAgG;gBAChG,wCAAwC;gBACxC,oEAAoE;gBACpE,sFAAsF,CACvF,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,yCAAyC;IAC3C,CAAC;AACH,CAAC","sourcesContent":["import { isDevMode } from '@angular/core';\n\n/** Warn once per application, not once per strip — the fix is app-level either way. */\nlet warned = false;\n\n/**\n * Dev-mode guard for the tab chrome's one genuine failure mode: the `.mj-tabs*` styles live in a\n * GLOBAL stylesheet (`@memberjunction/ng-ui-components/dist/lib/tabs/tabs.scss`), and a standalone\n * host that forgets the import gets structurally-working but completely unstyled strips — bare divs,\n * silently, with no error anywhere. This turns that silence into an actionable console warning.\n *\n * Detection: `tabs.scss` sets `--mj-tabs-chrome-loaded: 1` on `.mj-tabs`. If the computed value is\n * absent on a rendered strip, the sheet isn't in the page. Dev-mode only, once per app, and never\n * throws — a missing stylesheet must not take the component down with it.\n *\n * Called by BOTH strips (`mj-tabstrip` and `mj-workspace-tab-strip`) after view init.\n */\nexport function warnIfTabChromeMissing(element: HTMLElement | null | undefined): void {\n if (warned || !isDevMode() || !element || typeof getComputedStyle !== 'function') {\n return;\n }\n try {\n const target = element.classList.contains('mj-tabs') ? element : element.querySelector<HTMLElement>('.mj-tabs');\n if (!target) return;\n if (!getComputedStyle(target).getPropertyValue('--mj-tabs-chrome-loaded').trim()) {\n warned = true;\n console.warn(\n \"[MJ tabs] The shared tab chrome stylesheet is not loaded, so tab strips will render unstyled. \" +\n \"Add to your app's global stylesheet:\\n\" +\n \" @import '@memberjunction/ng-ui-components/dist/lib/tabs/tabs';\\n\" +\n \"(Apps on the MJ Explorer shell get this via ng-explorer-app's styles automatically.)\"\n );\n }\n } catch {\n // Never let diagnostics break the strip.\n }\n}\n"]}
@@ -0,0 +1,93 @@
1
+ import { AfterViewChecked, AfterViewInit, EventEmitter, OnDestroy } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /** A keyboard-driven request from the tab list, carrying the tab it refers to. */
4
+ export interface MJTabListRequest {
5
+ /** Position of the tab among its siblings, which is all this directive can know about identity. */
6
+ Index: number;
7
+ /** The tab element itself, for hosts that key off a data attribute rather than position. */
8
+ Element: HTMLElement;
9
+ }
10
+ /**
11
+ * `mjTabList` — the ARIA tabs keyboard contract, shared by every MJ tab strip.
12
+ *
13
+ * Applied to the element that holds the tabs. It manages the things the APG tabs pattern requires
14
+ * and that hand-rolled strips almost always miss:
15
+ *
16
+ * - **roving tabindex** — exactly ONE tab is in the page tab order at a time (the selected one),
17
+ * so Tab moves past the whole strip rather than through every tab in it;
18
+ * - **arrow-key navigation** — Left/Right move between tabs and activate, Home/End jump to the
19
+ * ends, wrapping at the edges;
20
+ * - **Enter / Space** activate the focused tab;
21
+ * - **Delete / Backspace** request closure of the focused tab, which is the APG answer for
22
+ * closeable tabs — a close button inside a tab cannot be its own tab stop without putting two
23
+ * stops per tab in the page order.
24
+ *
25
+ * ## Why a directive rather than a base class
26
+ *
27
+ * MJ's two strips disagree about what a tab IS: one projects `<mj-tab>` component instances and
28
+ * addresses them by positional index, the other renders a data array and addresses it by stable id.
29
+ * A shared base class would have to pick one of those models. This directive picks neither — it
30
+ * operates on the DOM, so each strip keeps its own contract and only has to agree that a tab is
31
+ * `[role="tab"]` and that the selected one carries `aria-selected="true"`. That agreement is the
32
+ * accessibility requirement anyway, so nothing is invented to make the sharing work.
33
+ *
34
+ * ## What the host must do
35
+ *
36
+ * Mark each tab `role="tab"`, keep `aria-selected` truthful, and act on `(TabActivateRequested)` /
37
+ * `(TabCloseRequested)`. The directive sets `role="tablist"` and owns `tabindex` — do not also bind
38
+ * it, or the two will fight.
39
+ */
40
+ export declare class MJTabListDirective implements AfterViewInit, AfterViewChecked, OnDestroy {
41
+ private readonly host;
42
+ /** The user asked to move to / activate a tab. The host maps the index onto its own model. */
43
+ TabActivateRequested: EventEmitter<MJTabListRequest>;
44
+ /** The user pressed Delete/Backspace on a tab. Hosts without closeable tabs simply ignore this. */
45
+ TabCloseRequested: EventEmitter<MJTabListRequest>;
46
+ /**
47
+ * Backstop for tab changes that happen OUTSIDE the host's change detection — a strip that mutates
48
+ * its own DOM, or a projected tab whose component re-renders itself without the parent view being
49
+ * checked. Change detection is the primary trigger (see `ngAfterViewChecked`); this catches the
50
+ * rest. Both funnel into the same idempotent write.
51
+ */
52
+ private readonly observer;
53
+ constructor();
54
+ ngAfterViewInit(): void;
55
+ /**
56
+ * The primary trigger. Deliberately a lifecycle hook rather than only the MutationObserver:
57
+ * observer callbacks are asynchronous, so between a selection change and the flush the tab order
58
+ * would point at the PREVIOUS tab — and a tab with no `tabindex` at all is not focusable, making
59
+ * `focus()` a silent no-op. Running here keeps the tab order correct in the same frame the
60
+ * selection changed.
61
+ *
62
+ * Safe to run every cycle: it writes only when a value actually differs, so it cannot loop.
63
+ */
64
+ ngAfterViewChecked(): void;
65
+ ngOnDestroy(): void;
66
+ /** All tabs in DOM order. Read live rather than cached — the host re-renders them freely. */
67
+ private get tabs();
68
+ /**
69
+ * A tab hidden with `display: none` still matches `[role="tab"]`, but it cannot take focus
70
+ * (`focus()` silently no-ops) and hosts reject activating it — `mj-tabstrip` throws on selecting
71
+ * an invisible tab, and Entity Actions really does hide tabs until a record is saved. So
72
+ * navigation and the roving tab stop must skip them.
73
+ *
74
+ * Checked via INLINE style up the chain rather than `offsetParent`/`getClientRects`, because
75
+ * jsdom computes no layout (those report every element as invisible in tests) while both strips
76
+ * hide tabs exactly this way — `mj-tab` binds `[style.display]` on its host.
77
+ */
78
+ private isTabVisible;
79
+ /**
80
+ * Exactly one tab in the page tab order. Falls back to the first tab when nothing is selected,
81
+ * so the strip is never a keyboard dead end (a list where every tab is -1 cannot be entered).
82
+ */
83
+ private applyRovingTabIndex;
84
+ OnKeydown(event: KeyboardEvent): void;
85
+ /**
86
+ * Follow-focus activation: moving to a tab selects it, which is the APG default for tab lists
87
+ * whose panels are cheap to show, and is what every desktop browser does.
88
+ */
89
+ private move;
90
+ static ɵfac: i0.ɵɵFactoryDeclaration<MJTabListDirective, never>;
91
+ static ɵdir: i0.ɵɵDirectiveDeclaration<MJTabListDirective, "[mjTabList]", never, {}, { "TabActivateRequested": "TabActivateRequested"; "TabCloseRequested": "TabCloseRequested"; }, never, never, true, never>;
92
+ }
93
+ //# sourceMappingURL=tab-list.directive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-list.directive.d.ts","sourceRoot":"","sources":["../../../src/lib/tabs/tab-list.directive.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,EAGb,YAAY,EAEZ,SAAS,EAGV,MAAM,eAAe,CAAC;;AAEvB,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,mGAAmG;IACnG,KAAK,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAKa,kBAAmB,YAAW,aAAa,EAAE,gBAAgB,EAAE,SAAS;IACnF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmD;IAExE,8FAA8F;IACpF,oBAAoB,iCAAwC;IAEtE,mGAAmG;IACzF,iBAAiB,iCAAwC;IAEnE;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CACgF;;IAWzG,eAAe,IAAI,IAAI;IAIvB;;;;;;;;OAQG;IACH,kBAAkB,IAAI,IAAI;IAI1B,WAAW,IAAI,IAAI;IAInB,6FAA6F;IAC7F,OAAO,KAAK,IAAI,GAEf;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IASpB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAmBpB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAsD5C;;;OAGG;IACH,OAAO,CAAC,IAAI;yCAzJD,kBAAkB;2CAAlB,kBAAkB;CAkK9B"}