@mk-kit/ui 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/block-editor/README.md +254 -0
  4. package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
  5. package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
  6. package/fesm2022/mk-kit-ui-button.mjs +81 -0
  7. package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
  8. package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
  9. package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
  10. package/fesm2022/mk-kit-ui-chip.mjs +122 -0
  11. package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
  12. package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
  13. package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
  14. package/fesm2022/mk-kit-ui-core.mjs +1576 -0
  15. package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
  16. package/fesm2022/mk-kit-ui-data.mjs +6055 -0
  17. package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
  18. package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
  19. package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
  20. package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
  21. package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
  22. package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
  23. package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
  24. package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
  25. package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
  26. package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
  27. package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
  28. package/fesm2022/mk-kit-ui-icon.mjs +470 -0
  29. package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
  30. package/fesm2022/mk-kit-ui-media.mjs +896 -0
  31. package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
  32. package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
  33. package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
  34. package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
  35. package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
  36. package/fesm2022/mk-kit-ui-table.mjs +1378 -0
  37. package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
  38. package/fesm2022/mk-kit-ui.mjs +32 -0
  39. package/fesm2022/mk-kit-ui.mjs.map +1 -0
  40. package/package.json +130 -0
  41. package/schematics/collection.json +10 -0
  42. package/schematics/ng-add/index.js +113 -0
  43. package/schematics/ng-add/schema.json +22 -0
  44. package/schematics/package.json +3 -0
  45. package/styles/mk-kit.css +750 -0
  46. package/types/mk-kit-ui-block-editor.d.ts +292 -0
  47. package/types/mk-kit-ui-button.d.ts +40 -0
  48. package/types/mk-kit-ui-checkbox.d.ts +62 -0
  49. package/types/mk-kit-ui-chip.d.ts +59 -0
  50. package/types/mk-kit-ui-context-menu.d.ts +57 -0
  51. package/types/mk-kit-ui-core.d.ts +1105 -0
  52. package/types/mk-kit-ui-data.d.ts +2580 -0
  53. package/types/mk-kit-ui-datetime.d.ts +1171 -0
  54. package/types/mk-kit-ui-directives.d.ts +807 -0
  55. package/types/mk-kit-ui-dnd.d.ts +423 -0
  56. package/types/mk-kit-ui-feedback.d.ts +1270 -0
  57. package/types/mk-kit-ui-forms.d.ts +3586 -0
  58. package/types/mk-kit-ui-icon.d.ts +108 -0
  59. package/types/mk-kit-ui-media.d.ts +549 -0
  60. package/types/mk-kit-ui-navigation.d.ts +1169 -0
  61. package/types/mk-kit-ui-rich-text.d.ts +187 -0
  62. package/types/mk-kit-ui-table.d.ts +739 -0
  63. package/types/mk-kit-ui.d.ts +17 -0
@@ -0,0 +1,2426 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, input, booleanAttribute, output, computed, ChangeDetectionStrategy, Component, ElementRef, ApplicationRef, EnvironmentInjector, DOCUMENT, PLATFORM_ID, createComponent, Directive, Injector, viewChild, signal, afterNextRender, numberAttribute, model, ViewEncapsulation, Injectable, InjectionToken, DestroyRef } from '@angular/core';
3
+ import { MK_I18N, mkUniqueId, mkComputeAnchoredPosition, mkGetFocusable, MkAnchoredPanel, MkOverlayRef, MK_OVERLAY_DATA, MkOverlayService } from '@mk-kit/ui/core';
4
+ import { isPlatformBrowser } from '@angular/common';
5
+ import { MkButton } from '@mk-kit/ui/button';
6
+ import { MkBadge, MkEmptyState } from '@mk-kit/ui/data';
7
+ import { MkIcon } from '@mk-kit/ui/icon';
8
+ import { MkInput, MkFormField } from '@mk-kit/ui/forms';
9
+
10
+ /**
11
+ * Alert — an inline banner that communicates a contextual status message.
12
+ * Fully themed via `--mk-*` tokens using the same tone/local-var pattern as
13
+ * the Button. Danger/warning alerts use `role="alert"` (assertive), all other
14
+ * tones use `role="status"` (polite).
15
+ *
16
+ * ```html
17
+ * <mk-alert tone="success" title="Saved">Your changes were saved.</mk-alert>
18
+ *
19
+ * <mk-alert tone="danger" variant="outline" dismissible (dismissed)="hide()">
20
+ * <svg mkAlertIcon>…</svg>
21
+ * Could not reach the server.
22
+ * </mk-alert>
23
+ * ```
24
+ */
25
+ class MkAlert {
26
+ /** Localised strings (override globally via `provideMkI18n`). */
27
+ i18n = inject(MK_I18N);
28
+ /** Semantic color tone. */
29
+ tone = input('info', /* @ts-ignore */
30
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
31
+ /** Visual treatment. */
32
+ variant = input('soft', /* @ts-ignore */
33
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
34
+ /** Optional bold heading rendered above the message. */
35
+ title = input(/* @ts-ignore */
36
+ ...(ngDevMode ? [undefined, { debugName: "title" }] : /* istanbul ignore next */ []));
37
+ /** When set, renders a keyboard-accessible close button that emits `dismissed`. */
38
+ dismissible = input(false, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
39
+ /** Emitted when the user activates the close button. */
40
+ dismissed = output();
41
+ /** Assertive roles for urgent tones, polite `status` otherwise. */
42
+ role = computed(() => this.tone() === 'danger' || this.tone() === 'warning' ? 'alert' : 'status', /* @ts-ignore */
43
+ ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
44
+ onDismiss() {
45
+ this.dismissed.emit();
46
+ }
47
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAlert, deps: [], target: i0.ɵɵFactoryTarget.Component });
48
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkAlert, isStandalone: true, selector: "mk-alert", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed" }, host: { properties: { "attr.data-tone": "tone()", "class.mk-alert--soft": "variant() === 'soft'", "class.mk-alert--solid": "variant() === 'solid'", "class.mk-alert--outline": "variant() === 'outline'", "attr.role": "role()" }, classAttribute: "mk-alert" }, ngImport: i0, template: "<span class=\"mk-alert__icon\" aria-hidden=\"true\">\n <ng-content select=\"[mkAlertIcon]\" />\n</span>\n\n<div class=\"mk-alert__content\">\n @if (title(); as heading) {\n <p class=\"mk-alert__title\">{{ heading }}</p>\n }\n <div class=\"mk-alert__message\"><ng-content /></div>\n</div>\n\n@if (dismissible()) {\n <button\n type=\"button\"\n class=\"mk-alert__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"onDismiss()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n}\n", styles: ["@charset \"UTF-8\";:host{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text);display:flex;align-items:flex-start;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);color:var(--mk-text)}:host([data-tone=info]){--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=success]){--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}:host([data-tone=warning]){--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}:host([data-tone=danger]){--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}:host([data-tone=neutral]){--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}:host(.mk-alert--soft){background-color:var(--_subtle);border-color:color-mix(in srgb,var(--_main) 22%,transparent);color:var(--_subtle-text)}:host(.mk-alert--outline){background-color:var(--mk-surface);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong));color:var(--mk-text)}:host(.mk-alert--solid){background-color:var(--_main);border-color:var(--_main);color:var(--_contrast)}.mk-alert__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.25rem;height:1.25rem;margin-top:1px;color:var(--_main)}:host(.mk-alert--solid) .mk-alert__icon{color:currentColor}.mk-alert__icon:empty{display:none}.mk-alert__content{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-alert__title{margin:0;font-weight:var(--mk-font-weight-semibold);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-snug)}.mk-alert__message{margin:0}.mk-alert__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin:-2px calc(var(--mk-space-1) * -1) 0 0;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:inherit;cursor:pointer;opacity:.7;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-alert__close:hover{opacity:1;background-image:linear-gradient(var(--mk-hover-overlay),var(--mk-hover-overlay))}.mk-alert__close:focus-visible{opacity:1;outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
49
+ }
50
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAlert, decorators: [{
51
+ type: Component,
52
+ args: [{ selector: 'mk-alert', changeDetection: ChangeDetectionStrategy.OnPush, host: {
53
+ class: 'mk-alert',
54
+ '[attr.data-tone]': 'tone()',
55
+ '[class.mk-alert--soft]': "variant() === 'soft'",
56
+ '[class.mk-alert--solid]': "variant() === 'solid'",
57
+ '[class.mk-alert--outline]': "variant() === 'outline'",
58
+ '[attr.role]': 'role()',
59
+ }, template: "<span class=\"mk-alert__icon\" aria-hidden=\"true\">\n <ng-content select=\"[mkAlertIcon]\" />\n</span>\n\n<div class=\"mk-alert__content\">\n @if (title(); as heading) {\n <p class=\"mk-alert__title\">{{ heading }}</p>\n }\n <div class=\"mk-alert__message\"><ng-content /></div>\n</div>\n\n@if (dismissible()) {\n <button\n type=\"button\"\n class=\"mk-alert__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"onDismiss()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n}\n", styles: ["@charset \"UTF-8\";:host{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text);display:flex;align-items:flex-start;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);color:var(--mk-text)}:host([data-tone=info]){--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=success]){--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}:host([data-tone=warning]){--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}:host([data-tone=danger]){--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}:host([data-tone=neutral]){--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}:host(.mk-alert--soft){background-color:var(--_subtle);border-color:color-mix(in srgb,var(--_main) 22%,transparent);color:var(--_subtle-text)}:host(.mk-alert--outline){background-color:var(--mk-surface);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong));color:var(--mk-text)}:host(.mk-alert--solid){background-color:var(--_main);border-color:var(--_main);color:var(--_contrast)}.mk-alert__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.25rem;height:1.25rem;margin-top:1px;color:var(--_main)}:host(.mk-alert--solid) .mk-alert__icon{color:currentColor}.mk-alert__icon:empty{display:none}.mk-alert__content{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-alert__title{margin:0;font-weight:var(--mk-font-weight-semibold);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-snug)}.mk-alert__message{margin:0}.mk-alert__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin:-2px calc(var(--mk-space-1) * -1) 0 0;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:inherit;cursor:pointer;opacity:.7;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-alert__close:hover{opacity:1;background-image:linear-gradient(var(--mk-hover-overlay),var(--mk-hover-overlay))}.mk-alert__close:focus-visible{opacity:1;outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
60
+ }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
61
+
62
+ /** Delay (ms) before a pointer-triggered tooltip opens. Focus opens instantly. */
63
+ const OPEN_DELAY = 400;
64
+ /**
65
+ * Grace period (ms) after the pointer leaves the trigger (or the panel) before
66
+ * the tooltip hides. Long enough to cross the 8px gap onto the panel — WCAG
67
+ * 1.4.13 "hoverable" — without making the tip feel sticky.
68
+ */
69
+ const CLOSE_DELAY = 150;
70
+ /**
71
+ * Body-level presentation surface for {@link MkTooltip}. Rendered imperatively
72
+ * by the directive; not intended for direct template use.
73
+ */
74
+ class MkTooltipPanel {
75
+ /** Text content shown inside the tooltip. */
76
+ text = input('', /* @ts-ignore */
77
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
78
+ /** DOM id used to wire `aria-describedby` on the trigger. */
79
+ id = input(/* @ts-ignore */
80
+ ...(ngDevMode ? [undefined, { debugName: "id" }] : /* istanbul ignore next */ []));
81
+ /** Resolved placement relative to the trigger. */
82
+ placement = input('top', /* @ts-ignore */
83
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
84
+ /** Drives the entrance/exit transition. */
85
+ visible = input(false, { ...(ngDevMode ? { debugName: "visible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
86
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTooltipPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
87
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkTooltipPanel, isStandalone: true, selector: "mk-tooltip", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tooltip" }, properties: { "attr.id": "id()", "attr.data-placement": "placement()", "class.mk-tooltip--visible": "visible()" }, classAttribute: "mk-tooltip" }, ngImport: i0, template: "{{ text() }}\n", styles: [":host{position:fixed;z-index:var(--mk-z-tooltip);max-width:16rem;padding:var(--mk-space-1) var(--mk-space-2);background-color:var(--mk-surface-inverse);color:var(--mk-text-inverse);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);line-height:var(--mk-line-height-snug);text-align:center;border-radius:var(--mk-radius-sm);box-shadow:var(--mk-shadow-md);pointer-events:none;white-space:normal;overflow-wrap:break-word;opacity:0;transform:scale(.96);transition:opacity var(--mk-duration-fast) var(--mk-ease-out),transform var(--mk-duration-fast) var(--mk-ease-out)}:host(.mk-tooltip--visible){opacity:1;transform:scale(1)}@media(prefers-reduced-motion:reduce){:host{transition:none;transform:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
88
+ }
89
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTooltipPanel, decorators: [{
90
+ type: Component,
91
+ args: [{ selector: 'mk-tooltip', changeDetection: ChangeDetectionStrategy.OnPush, host: {
92
+ class: 'mk-tooltip',
93
+ role: 'tooltip',
94
+ '[attr.id]': 'id()',
95
+ '[attr.data-placement]': 'placement()',
96
+ '[class.mk-tooltip--visible]': 'visible()',
97
+ }, template: "{{ text() }}\n", styles: [":host{position:fixed;z-index:var(--mk-z-tooltip);max-width:16rem;padding:var(--mk-space-1) var(--mk-space-2);background-color:var(--mk-surface-inverse);color:var(--mk-text-inverse);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);line-height:var(--mk-line-height-snug);text-align:center;border-radius:var(--mk-radius-sm);box-shadow:var(--mk-shadow-md);pointer-events:none;white-space:normal;overflow-wrap:break-word;opacity:0;transform:scale(.96);transition:opacity var(--mk-duration-fast) var(--mk-ease-out),transform var(--mk-duration-fast) var(--mk-ease-out)}:host(.mk-tooltip--visible){opacity:1;transform:scale(1)}@media(prefers-reduced-motion:reduce){:host{transition:none;transform:none}}\n"] }]
98
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }] } });
99
+ /**
100
+ * Tooltip directive — shows a themed tooltip on hover AND keyboard focus, and
101
+ * hides on blur, mouse-leave (after a short grace period — the pointer may
102
+ * move onto the tooltip itself, which keeps it open, per WCAG 1.4.13), or
103
+ * Escape pressed anywhere while it is visible. The tooltip is appended to
104
+ * `document.body`, positioned relative to the host, and wired to the host via
105
+ * `aria-describedby` for screen-reader users. Respects `prefers-reduced-motion`.
106
+ *
107
+ * ```html
108
+ * <button mkButton [mkTooltip]="'Delete row'" mkTooltipPlacement="bottom">
109
+ * <svg>…</svg>
110
+ * </button>
111
+ * ```
112
+ */
113
+ class MkTooltip {
114
+ host = inject(ElementRef);
115
+ appRef = inject(ApplicationRef);
116
+ envInjector = inject(EnvironmentInjector);
117
+ document = inject(DOCUMENT);
118
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
119
+ /** Set by pointerdown so the focusin it causes doesn't re-open the tip. */
120
+ suppressFocusShow = false;
121
+ /** Tooltip text. When empty the tooltip is suppressed. */
122
+ mkTooltip = input('', /* @ts-ignore */
123
+ ...(ngDevMode ? [{ debugName: "mkTooltip" }] : /* istanbul ignore next */ []));
124
+ /** Preferred placement relative to the host. */
125
+ mkTooltipPlacement = input('top', /* @ts-ignore */
126
+ ...(ngDevMode ? [{ debugName: "mkTooltipPlacement" }] : /* istanbul ignore next */ []));
127
+ tooltipId = mkUniqueId('mk-tooltip');
128
+ ref;
129
+ openTimer;
130
+ hideTimer;
131
+ previousDescribedBy = null;
132
+ /**
133
+ * Hovering the tooltip itself keeps it open (WCAG 1.4.13 "hoverable"): the
134
+ * pointer may cross the gap between trigger and panel, so leaving either one
135
+ * only SCHEDULES a hide, and entering either one cancels it.
136
+ */
137
+ onPanelEnter = () => {
138
+ clearTimeout(this.hideTimer);
139
+ };
140
+ onPanelLeave = () => {
141
+ this.scheduleHide();
142
+ };
143
+ /**
144
+ * WCAG 1.4.13 "dismissible": Escape hides a visible tooltip no matter where
145
+ * focus is. Attached to the document only while the tooltip is shown, and
146
+ * preventDefault only fires when a tooltip was actually hidden.
147
+ */
148
+ onDocumentKeydown = (event) => {
149
+ if (event.key !== 'Escape' || !this.ref)
150
+ return;
151
+ event.preventDefault();
152
+ this.hide();
153
+ };
154
+ onPointerEnter() {
155
+ // Re-entering the trigger during the hide grace period keeps the tip up.
156
+ clearTimeout(this.hideTimer);
157
+ this.scheduleShow(OPEN_DELAY);
158
+ }
159
+ onPointerLeave() {
160
+ clearTimeout(this.openTimer);
161
+ this.scheduleHide();
162
+ }
163
+ scheduleHide() {
164
+ if (!this.ref)
165
+ return;
166
+ clearTimeout(this.hideTimer);
167
+ this.hideTimer = setTimeout(() => this.hide(), CLOSE_DELAY);
168
+ }
169
+ onFocus() {
170
+ // Pointer-driven focus already dismissed the tip on pointerdown; showing
171
+ // it again here would defeat that. Keyboard focus still gets a tooltip.
172
+ if (this.suppressFocusShow) {
173
+ this.suppressFocusShow = false;
174
+ return;
175
+ }
176
+ this.scheduleShow(0);
177
+ }
178
+ onPointerDown(event) {
179
+ this.suppressFocusShow = true;
180
+ // Touch toggle: hover never happens on a touchscreen, so a tap on the
181
+ // trigger is the show gesture. Tap again (ref exists → fall through to
182
+ // hide) or tap elsewhere (document pointerdown) dismisses. Mouse and pen
183
+ // keep the plain dismiss-on-pointerdown behaviour.
184
+ if (event.pointerType === 'touch' && !this.ref) {
185
+ this.scheduleShow(0);
186
+ return;
187
+ }
188
+ this.hide();
189
+ }
190
+ /**
191
+ * Outside-tap dismissal (attached to the document only while visible): a
192
+ * pointerdown that lands on neither the trigger nor the panel hides the
193
+ * tooltip. The trigger's own pointerdown handles show/hide/toggle itself,
194
+ * and a press on the panel keeps it up (WCAG 1.4.13 "hoverable").
195
+ */
196
+ onDocumentPointerdown = (event) => {
197
+ const target = event.target;
198
+ if (!target || this.host.nativeElement.contains(target))
199
+ return;
200
+ const panel = this.ref?.location.nativeElement;
201
+ if (panel?.contains(target))
202
+ return;
203
+ this.hide();
204
+ };
205
+ scheduleShow(delay) {
206
+ if (!this.isBrowser || !this.mkTooltip().trim() || this.ref)
207
+ return;
208
+ clearTimeout(this.openTimer);
209
+ if (delay === 0) {
210
+ this.show();
211
+ }
212
+ else {
213
+ this.openTimer = setTimeout(() => this.show(), delay);
214
+ }
215
+ }
216
+ hide() {
217
+ clearTimeout(this.openTimer);
218
+ clearTimeout(this.hideTimer);
219
+ if (!this.ref)
220
+ return;
221
+ this.document.removeEventListener('keydown', this.onDocumentKeydown, true);
222
+ this.document.removeEventListener('pointerdown', this.onDocumentPointerdown, true);
223
+ // Restore the trigger's original aria-describedby.
224
+ const el = this.host.nativeElement;
225
+ if (this.previousDescribedBy === null) {
226
+ el.removeAttribute('aria-describedby');
227
+ }
228
+ else {
229
+ el.setAttribute('aria-describedby', this.previousDescribedBy);
230
+ }
231
+ this.previousDescribedBy = null;
232
+ const panel = this.ref.location.nativeElement;
233
+ if (panel.matches('[popover]') && typeof panel.hidePopover === 'function') {
234
+ try {
235
+ panel.hidePopover();
236
+ }
237
+ catch {
238
+ // Already disconnected — nothing to do.
239
+ }
240
+ }
241
+ this.appRef.detachView(this.ref.hostView);
242
+ this.ref.destroy();
243
+ this.ref = undefined;
244
+ }
245
+ show() {
246
+ if (this.ref)
247
+ return;
248
+ const ref = createComponent(MkTooltipPanel, {
249
+ environmentInjector: this.envInjector,
250
+ });
251
+ ref.setInput('text', this.mkTooltip());
252
+ ref.setInput('id', this.tooltipId);
253
+ ref.setInput('placement', this.mkTooltipPlacement());
254
+ this.appRef.attachView(ref.hostView);
255
+ const panel = ref.location.nativeElement;
256
+ this.document.body.appendChild(panel);
257
+ this.ref = ref;
258
+ // Hoverable (the listeners die with the panel element on hide) …
259
+ panel.addEventListener('mouseenter', this.onPanelEnter);
260
+ panel.addEventListener('mouseleave', this.onPanelLeave);
261
+ // … and dismissible from anywhere while visible (removed in hide()).
262
+ // Capture phase so the Escape that hides the tooltip is marked
263
+ // defaultPrevented before any bubbling handler (e.g. a dialog deciding
264
+ // whether to close) sees it — layered dismissal, tooltip first.
265
+ this.document.addEventListener('keydown', this.onDocumentKeydown, true);
266
+ // … and from an outside tap — the touch analogue of mouseleave. Capture
267
+ // phase so the tooltip is gone before the tap's own target reacts.
268
+ this.document.addEventListener('pointerdown', this.onDocumentPointerdown, true);
269
+ // Promote into the top layer so the tooltip is never clipped by an
270
+ // ancestor's overflow/transform or hidden behind a stacking context.
271
+ const withPopover = panel;
272
+ if (typeof withPopover.showPopover === 'function') {
273
+ panel.setAttribute('popover', 'manual');
274
+ try {
275
+ withPopover.showPopover();
276
+ }
277
+ catch {
278
+ // Unsupported context — fall back to the plain body portal.
279
+ }
280
+ }
281
+ // Wire aria-describedby, preserving any pre-existing value.
282
+ const trigger = this.host.nativeElement;
283
+ this.previousDescribedBy = trigger.getAttribute('aria-describedby');
284
+ trigger.setAttribute('aria-describedby', this.previousDescribedBy
285
+ ? `${this.previousDescribedBy} ${this.tooltipId}`
286
+ : this.tooltipId);
287
+ this.position(panel);
288
+ // Reveal on the next frame so the entrance transition runs.
289
+ this.document.defaultView?.requestAnimationFrame(() => {
290
+ this.ref?.setInput('visible', true);
291
+ });
292
+ }
293
+ position(panel) {
294
+ const rect = this.host.nativeElement.getBoundingClientRect();
295
+ const tip = panel.getBoundingClientRect();
296
+ const pos = mkComputeAnchoredPosition(rect, { width: tip.width, height: tip.height }, {
297
+ width: this.document.documentElement.clientWidth,
298
+ height: this.document.documentElement.clientHeight,
299
+ },
300
+ // Clamp back on-screen but keep the requested side (no flip) so the
301
+ // panel's `data-placement` styling stays correct.
302
+ {
303
+ placement: this.mkTooltipPlacement(),
304
+ gap: 8,
305
+ flip: false,
306
+ clamp: true,
307
+ rtl: this.document.defaultView?.getComputedStyle(this.host.nativeElement)
308
+ .direction === 'rtl',
309
+ });
310
+ panel.style.position = 'fixed';
311
+ panel.style.margin = '0';
312
+ panel.style.inset = 'auto';
313
+ panel.style.top = `${pos.top}px`;
314
+ panel.style.left = `${pos.left}px`;
315
+ }
316
+ ngOnDestroy() {
317
+ this.hide();
318
+ }
319
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTooltip, deps: [], target: i0.ɵɵFactoryTarget.Directive });
320
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkTooltip, isStandalone: true, selector: "[mkTooltip]", inputs: { mkTooltip: { classPropertyName: "mkTooltip", publicName: "mkTooltip", isSignal: true, isRequired: false, transformFunction: null }, mkTooltipPlacement: { classPropertyName: "mkTooltipPlacement", publicName: "mkTooltipPlacement", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "onPointerEnter()", "mouseleave": "onPointerLeave()", "focusin": "onFocus()", "focusout": "hide()", "keydown.escape": "hide()", "pointerdown": "onPointerDown($event)" } }, ngImport: i0 });
321
+ }
322
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTooltip, decorators: [{
323
+ type: Directive,
324
+ args: [{
325
+ selector: '[mkTooltip]',
326
+ host: {
327
+ '(mouseenter)': 'onPointerEnter()',
328
+ '(mouseleave)': 'onPointerLeave()',
329
+ '(focusin)': 'onFocus()',
330
+ '(focusout)': 'hide()',
331
+ '(keydown.escape)': 'hide()',
332
+ // A tooltip must not outlive the interaction that spawned it. Without
333
+ // this, tapping a tooltipped control leaves the tip on screen — and since
334
+ // tooltips deliberately sit ABOVE dialogs (so a tip inside a modal is
335
+ // visible), a stale one floats over whatever the tap just opened.
336
+ // Exception: a TOUCH tap on the trigger of a hidden tooltip SHOWS it —
337
+ // touch has no hover, so this is the only way a finger reaches the tip.
338
+ '(pointerdown)': 'onPointerDown($event)',
339
+ },
340
+ }]
341
+ }], propDecorators: { mkTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkTooltip", required: false }] }], mkTooltipPlacement: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkTooltipPlacement", required: false }] }] } });
342
+
343
+ /**
344
+ * Popover — a non-modal floating panel for rich (non-text) content, anchored to
345
+ * a trigger. Unlike {@link MkTooltip} (text, hover) it holds arbitrary projected
346
+ * content and is opened by click via the `mkPopoverTriggerFor` directive (or the
347
+ * `open`/`toggle` API).
348
+ *
349
+ * The panel renders in the browser top layer through {@link MkAnchoredPanel}, so
350
+ * it is never clipped by an ancestor's `overflow`/`transform`. On open, focus
351
+ * moves into the panel; Escape closes and restores focus to the trigger, an
352
+ * outside click or Tabbing out closes it (non-modal — no keyboard trap).
353
+ *
354
+ * ```html
355
+ * <button mkButton [mkPopoverTriggerFor]="info">Details</button>
356
+ * <mk-popover #info ariaLabel="Details">
357
+ * <h4>Shipping</h4><p>Ships in 2–3 business days.</p>
358
+ * </mk-popover>
359
+ * ```
360
+ */
361
+ class MkPopover {
362
+ injector = inject(Injector);
363
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
364
+ panelRef = viewChild('panel', /* @ts-ignore */
365
+ ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
366
+ /** Stable id for `aria-controls` on the trigger. */
367
+ panelId = mkUniqueId('mk-popover');
368
+ /** Preferred placement relative to the trigger. */
369
+ placement = input('bottom', /* @ts-ignore */
370
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
371
+ /** Accessible label for the dialog panel. */
372
+ ariaLabel = input(/* @ts-ignore */
373
+ ...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
374
+ /** Move focus into the panel on open. Default `true`. */
375
+ autoFocus = input(true, { ...(ngDevMode ? { debugName: "autoFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
376
+ /** Emitted whenever the popover closes (any reason). */
377
+ closed = output();
378
+ _open = signal(false, /* @ts-ignore */
379
+ ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
380
+ /** Whether the popover is open. */
381
+ opened = this._open.asReadonly();
382
+ anchorEl = signal(undefined, /* @ts-ignore */
383
+ ...(ngDevMode ? [{ debugName: "anchorEl" }] : /* istanbul ignore next */ []));
384
+ triggerEl = null;
385
+ /** Open the popover anchored to `trigger`. */
386
+ open(trigger) {
387
+ if (!this.isBrowser || this._open())
388
+ return;
389
+ this.triggerEl = trigger;
390
+ this.anchorEl.set(trigger);
391
+ this._open.set(true);
392
+ if (this.autoFocus()) {
393
+ afterNextRender(() => this.focusInitial(), { injector: this.injector });
394
+ }
395
+ }
396
+ /** Close the popover; optionally restore focus to the trigger. */
397
+ close(restoreFocus = true) {
398
+ if (!this._open())
399
+ return;
400
+ this._open.set(false);
401
+ this.anchorEl.set(undefined);
402
+ const trigger = this.triggerEl;
403
+ this.triggerEl = null;
404
+ if (restoreFocus)
405
+ trigger?.focus();
406
+ this.closed.emit();
407
+ }
408
+ /** Toggle open/closed from a trigger. */
409
+ toggle(trigger) {
410
+ this._open() ? this.close(true) : this.open(trigger);
411
+ }
412
+ focusInitial() {
413
+ const panel = this.panelRef()?.nativeElement;
414
+ if (!panel)
415
+ return;
416
+ (mkGetFocusable(panel)[0] ?? panel).focus();
417
+ }
418
+ onEscape(event) {
419
+ event.preventDefault();
420
+ event.stopPropagation();
421
+ this.close(true);
422
+ }
423
+ onPanelFocusout(event) {
424
+ const related = event.relatedTarget;
425
+ // Focus moved to a non-focusable spot (stays in document.body) — keep open;
426
+ // outside pointerdown is handled separately by the anchored panel.
427
+ if (!related)
428
+ return;
429
+ const panel = this.panelRef()?.nativeElement;
430
+ if (panel?.contains(related) || this.triggerEl?.contains(related))
431
+ return;
432
+ // Tabbed out of the panel — close without yanking focus back.
433
+ this.close(false);
434
+ }
435
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
436
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkPopover, isStandalone: true, selector: "mk-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { classAttribute: "mk-popover" }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], exportAs: ["mkPopover"], ngImport: i0, template: "@if (opened()) {\n <div\n #panel\n class=\"mk-popover__panel\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel()\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"anchorEl()\"\n [placement]=\"placement()\"\n [gap]=\"8\"\n (dismiss)=\"close(false)\"\n (keydown.escape)=\"onEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <ng-content />\n </div>\n}\n", styles: [":host{display:contents}.mk-popover__panel{min-width:12rem;max-width:min(22rem,100vw - var(--mk-space-4));padding:var(--mk-space-3) var(--mk-space-4);color:var(--mk-text);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);animation:mk-popover-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-popover__panel:focus-visible{outline:none}@keyframes mk-popover-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-popover__panel{animation:none}}\n"], dependencies: [{ kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
437
+ }
438
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopover, decorators: [{
439
+ type: Component,
440
+ args: [{ selector: 'mk-popover', exportAs: 'mkPopover', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAnchoredPanel], host: {
441
+ class: 'mk-popover',
442
+ }, template: "@if (opened()) {\n <div\n #panel\n class=\"mk-popover__panel\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel()\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"anchorEl()\"\n [placement]=\"placement()\"\n [gap]=\"8\"\n (dismiss)=\"close(false)\"\n (keydown.escape)=\"onEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <ng-content />\n </div>\n}\n", styles: [":host{display:contents}.mk-popover__panel{min-width:12rem;max-width:min(22rem,100vw - var(--mk-space-4));padding:var(--mk-space-3) var(--mk-space-4);color:var(--mk-text);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);animation:mk-popover-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-popover__panel:focus-visible{outline:none}@keyframes mk-popover-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-popover__panel{animation:none}}\n"] }]
443
+ }], propDecorators: { panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
444
+
445
+ /**
446
+ * Turns its host element into a click trigger for an `<mk-popover>`. Wires
447
+ * `aria-haspopup="dialog"` / `aria-expanded` / `aria-controls`, toggles on click
448
+ * and closes on Escape.
449
+ *
450
+ * ```html
451
+ * <button mkButton [mkPopoverTriggerFor]="info">Details</button>
452
+ * <mk-popover #info>…</mk-popover>
453
+ * ```
454
+ */
455
+ class MkPopoverTrigger {
456
+ host = inject(ElementRef);
457
+ /** The popover instance to control. */
458
+ popover = input.required({ ...(ngDevMode ? { debugName: "popover" } : /* istanbul ignore next */ {}), alias: 'mkPopoverTriggerFor' });
459
+ onClick(event) {
460
+ event.preventDefault();
461
+ this.popover().toggle(this.host.nativeElement);
462
+ }
463
+ onKeydown(event) {
464
+ if (event.key === 'Escape' && this.popover().opened()) {
465
+ event.preventDefault();
466
+ this.popover().close(true);
467
+ }
468
+ }
469
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopoverTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
470
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkPopoverTrigger, isStandalone: true, selector: "[mkPopoverTriggerFor]", inputs: { popover: { classPropertyName: "popover", publicName: "mkPopoverTriggerFor", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "dialog" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.aria-expanded": "popover().opened()", "attr.aria-controls": "popover().panelId" } }, exportAs: ["mkPopoverTrigger"], ngImport: i0 });
471
+ }
472
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopoverTrigger, decorators: [{
473
+ type: Directive,
474
+ args: [{
475
+ selector: '[mkPopoverTriggerFor]',
476
+ exportAs: 'mkPopoverTrigger',
477
+ host: {
478
+ 'aria-haspopup': 'dialog',
479
+ '[attr.aria-expanded]': 'popover().opened()',
480
+ '[attr.aria-controls]': 'popover().panelId',
481
+ '(click)': 'onClick($event)',
482
+ '(keydown)': 'onKeydown($event)',
483
+ },
484
+ }]
485
+ }], propDecorators: { popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkPopoverTriggerFor", required: true }] }] } });
486
+
487
+ /**
488
+ * Popconfirm — an inline confirmation popover (e.g. a row-level "Delete?").
489
+ * Composes {@link MkPopover} with a message and cancel/confirm buttons, so it
490
+ * inherits the top-layer positioning + focus behaviour. Attach it with the
491
+ * `mkPopconfirmFor` directive.
492
+ *
493
+ * ```html
494
+ * <button mkButton tone="danger" [mkPopconfirmFor]="del">Delete</button>
495
+ * <mk-popconfirm #del message="Delete this item?" (confirm)="remove()" />
496
+ * ```
497
+ */
498
+ class MkPopconfirm {
499
+ popover = viewChild(MkPopover, /* @ts-ignore */
500
+ ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
501
+ i18n = inject(MK_I18N);
502
+ /** Confirmation message. */
503
+ message = input(this.i18n.confirmMessage, /* @ts-ignore */
504
+ ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
505
+ /** Optional bold heading above the message. */
506
+ title = input(/* @ts-ignore */
507
+ ...(ngDevMode ? [undefined, { debugName: "title" }] : /* istanbul ignore next */ []));
508
+ /** Confirm button label. */
509
+ confirmText = input(this.i18n.confirm, /* @ts-ignore */
510
+ ...(ngDevMode ? [{ debugName: "confirmText" }] : /* istanbul ignore next */ []));
511
+ /** Cancel button label. */
512
+ cancelText = input(this.i18n.cancel, /* @ts-ignore */
513
+ ...(ngDevMode ? [{ debugName: "cancelText" }] : /* istanbul ignore next */ []));
514
+ /** Tone of the confirm button (destructive actions use `danger`). */
515
+ tone = input('danger', /* @ts-ignore */
516
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
517
+ /** Preferred placement relative to the trigger. */
518
+ placement = input('top', /* @ts-ignore */
519
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
520
+ /** Emitted when the user confirms. */
521
+ confirm = output();
522
+ /** Emitted when the user cancels (button, Escape or outside click). */
523
+ cancel = output();
524
+ /** Id of the underlying dialog panel (for the trigger's `aria-controls`). */
525
+ get panelId() {
526
+ return this.popover()?.panelId ?? null;
527
+ }
528
+ /** Whether the popconfirm is open. */
529
+ opened() {
530
+ return this.popover()?.opened() ?? false;
531
+ }
532
+ /** Open anchored to `trigger`. */
533
+ open(trigger) {
534
+ this.popover()?.open(trigger);
535
+ }
536
+ /** Toggle from a trigger. */
537
+ toggle(trigger) {
538
+ this.popover()?.toggle(trigger);
539
+ }
540
+ /** Close the popconfirm. */
541
+ close() {
542
+ this.popover()?.close(true);
543
+ }
544
+ /** Set when the user confirms, so the close handler doesn't also cancel. */
545
+ confirmed = false;
546
+ onConfirm() {
547
+ this.confirmed = true;
548
+ this.confirm.emit();
549
+ this.popover()?.close(true);
550
+ }
551
+ onCancel() {
552
+ // Closing emits `closed`, which routes to `cancel` (see onPopoverClosed).
553
+ this.popover()?.close(true);
554
+ }
555
+ /** Any close that wasn't a confirm counts as a cancel (Escape / outside). */
556
+ onPopoverClosed() {
557
+ if (this.confirmed) {
558
+ this.confirmed = false;
559
+ return;
560
+ }
561
+ this.cancel.emit();
562
+ }
563
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopconfirm, deps: [], target: i0.ɵɵFactoryTarget.Component });
564
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkPopconfirm, isStandalone: true, selector: "mk-popconfirm", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, confirmText: { classPropertyName: "confirmText", publicName: "confirmText", isSignal: true, isRequired: false, transformFunction: null }, cancelText: { classPropertyName: "cancelText", publicName: "cancelText", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { confirm: "confirm", cancel: "cancel" }, host: { classAttribute: "mk-popconfirm" }, viewQueries: [{ propertyName: "popover", first: true, predicate: MkPopover, descendants: true, isSignal: true }], exportAs: ["mkPopconfirm"], ngImport: i0, template: "<mk-popover\n [placement]=\"placement()\"\n [ariaLabel]=\"title() || message()\"\n (closed)=\"onPopoverClosed()\"\n>\n <div class=\"mk-popconfirm__body\">\n @if (title()) {\n <p class=\"mk-popconfirm__title\">{{ title() }}</p>\n }\n <p class=\"mk-popconfirm__message\">{{ message() }}</p>\n <div class=\"mk-popconfirm__actions\">\n <button mkButton variant=\"ghost\" tone=\"neutral\" size=\"sm\" (click)=\"onCancel()\">\n {{ cancelText() }}\n </button>\n <button mkButton [tone]=\"tone()\" size=\"sm\" (click)=\"onConfirm()\">\n {{ confirmText() }}\n </button>\n </div>\n </div>\n</mk-popover>\n", styles: [":host{display:contents}.mk-popconfirm__body{display:flex;flex-direction:column;gap:var(--mk-space-2);min-width:12rem;max-width:18rem}.mk-popconfirm__title{margin:0;font-size:var(--mk-font-size-md);font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}.mk-popconfirm__message{margin:0;font-size:var(--mk-font-size-sm);color:var(--mk-text-muted);line-height:var(--mk-line-height-normal)}.mk-popconfirm__actions{display:flex;justify-content:flex-end;gap:var(--mk-space-2);margin-top:var(--mk-space-1)}\n"], dependencies: [{ kind: "component", type: MkPopover, selector: "mk-popover", inputs: ["placement", "ariaLabel", "autoFocus"], outputs: ["closed"], exportAs: ["mkPopover"] }, { kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
565
+ }
566
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopconfirm, decorators: [{
567
+ type: Component,
568
+ args: [{ selector: 'mk-popconfirm', exportAs: 'mkPopconfirm', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkPopover, MkButton], host: {
569
+ class: 'mk-popconfirm',
570
+ }, template: "<mk-popover\n [placement]=\"placement()\"\n [ariaLabel]=\"title() || message()\"\n (closed)=\"onPopoverClosed()\"\n>\n <div class=\"mk-popconfirm__body\">\n @if (title()) {\n <p class=\"mk-popconfirm__title\">{{ title() }}</p>\n }\n <p class=\"mk-popconfirm__message\">{{ message() }}</p>\n <div class=\"mk-popconfirm__actions\">\n <button mkButton variant=\"ghost\" tone=\"neutral\" size=\"sm\" (click)=\"onCancel()\">\n {{ cancelText() }}\n </button>\n <button mkButton [tone]=\"tone()\" size=\"sm\" (click)=\"onConfirm()\">\n {{ confirmText() }}\n </button>\n </div>\n </div>\n</mk-popover>\n", styles: [":host{display:contents}.mk-popconfirm__body{display:flex;flex-direction:column;gap:var(--mk-space-2);min-width:12rem;max-width:18rem}.mk-popconfirm__title{margin:0;font-size:var(--mk-font-size-md);font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}.mk-popconfirm__message{margin:0;font-size:var(--mk-font-size-sm);color:var(--mk-text-muted);line-height:var(--mk-line-height-normal)}.mk-popconfirm__actions{display:flex;justify-content:flex-end;gap:var(--mk-space-2);margin-top:var(--mk-space-1)}\n"] }]
571
+ }], propDecorators: { popover: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MkPopover), { isSignal: true }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], confirmText: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmText", required: false }] }], cancelText: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelText", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], confirm: [{ type: i0.Output, args: ["confirm"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
572
+
573
+ /**
574
+ * Turns its host element into a click trigger for an `<mk-popconfirm>`. Wires
575
+ * `aria-haspopup="dialog"` / `aria-expanded` / `aria-controls`, toggles on click
576
+ * and closes on Escape.
577
+ *
578
+ * ```html
579
+ * <button mkButton tone="danger" [mkPopconfirmFor]="del">Delete</button>
580
+ * <mk-popconfirm #del message="Delete this item?" (confirm)="remove()" />
581
+ * ```
582
+ */
583
+ class MkPopconfirmTrigger {
584
+ host = inject(ElementRef);
585
+ /** The popconfirm instance to control. */
586
+ popconfirm = input.required({ ...(ngDevMode ? { debugName: "popconfirm" } : /* istanbul ignore next */ {}), alias: 'mkPopconfirmFor' });
587
+ onClick(event) {
588
+ event.preventDefault();
589
+ this.popconfirm().toggle(this.host.nativeElement);
590
+ }
591
+ onKeydown(event) {
592
+ if (event.key === 'Escape' && this.popconfirm().opened()) {
593
+ event.preventDefault();
594
+ this.popconfirm().close();
595
+ }
596
+ }
597
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopconfirmTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
598
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkPopconfirmTrigger, isStandalone: true, selector: "[mkPopconfirmFor]", inputs: { popconfirm: { classPropertyName: "popconfirm", publicName: "mkPopconfirmFor", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "dialog" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "attr.aria-expanded": "popconfirm().opened()", "attr.aria-controls": "popconfirm().panelId" } }, exportAs: ["mkPopconfirmTrigger"], ngImport: i0 });
599
+ }
600
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPopconfirmTrigger, decorators: [{
601
+ type: Directive,
602
+ args: [{
603
+ selector: '[mkPopconfirmFor]',
604
+ exportAs: 'mkPopconfirmTrigger',
605
+ host: {
606
+ 'aria-haspopup': 'dialog',
607
+ '[attr.aria-expanded]': 'popconfirm().opened()',
608
+ '[attr.aria-controls]': 'popconfirm().panelId',
609
+ '(click)': 'onClick($event)',
610
+ '(keydown)': 'onKeydown($event)',
611
+ },
612
+ }]
613
+ }], propDecorators: { popconfirm: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkPopconfirmFor", required: true }] }] } });
614
+
615
+ /**
616
+ * Result — a centred, full-page status state: a success confirmation, an error,
617
+ * an empty 404/403/500 page. Renders a large status icon (chosen from
618
+ * `status`), a title, a muted subtitle, projected body content, and a row of
619
+ * action buttons at the bottom.
620
+ *
621
+ * Slots: `[mkResultIcon]` (custom icon, overrides the built-in glyph),
622
+ * `[mkResultTitle]` (custom heading, used when `resultTitle` is unset), default
623
+ * content (extra body), `[mkResultActions]` (buttons).
624
+ *
625
+ * ```html
626
+ * <mk-result status="success" resultTitle="Payment complete"
627
+ * subtitle="Your order is on its way.">
628
+ * <button mkButton mkResultActions>Back home</button>
629
+ * </mk-result>
630
+ * ```
631
+ */
632
+ class MkResult {
633
+ /** Status preset that selects the built-in icon glyph and its tone colour. */
634
+ status = input('info', /* @ts-ignore */
635
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
636
+ /** Main heading rendered as an `<h2>` (falls back to the `[mkResultTitle]` slot). */
637
+ resultTitle = input('', /* @ts-ignore */
638
+ ...(ngDevMode ? [{ debugName: "resultTitle" }] : /* istanbul ignore next */ []));
639
+ /** Supporting muted sentence under the title. */
640
+ subtitle = input('', /* @ts-ignore */
641
+ ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
642
+ /** Built-in glyph for the current status. */
643
+ iconName = computed(() => {
644
+ switch (this.status()) {
645
+ case 'success':
646
+ return 'check';
647
+ case 'error':
648
+ case '500':
649
+ return 'x';
650
+ case 'warning':
651
+ case '403':
652
+ return 'alert';
653
+ case '404':
654
+ return 'search';
655
+ default:
656
+ return 'info';
657
+ }
658
+ }, /* @ts-ignore */
659
+ ...(ngDevMode ? [{ debugName: "iconName" }] : /* istanbul ignore next */ []));
660
+ /** Tone colour applied to the status icon. */
661
+ tone = computed(() => {
662
+ switch (this.status()) {
663
+ case 'success':
664
+ return 'success';
665
+ case 'error':
666
+ case '500':
667
+ return 'danger';
668
+ case 'warning':
669
+ case '403':
670
+ return 'warning';
671
+ case '404':
672
+ return 'muted';
673
+ default:
674
+ return 'info';
675
+ }
676
+ }, /* @ts-ignore */
677
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
678
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkResult, deps: [], target: i0.ɵɵFactoryTarget.Component });
679
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkResult, isStandalone: true, selector: "mk-result", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, resultTitle: { classPropertyName: "resultTitle", publicName: "resultTitle", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "status" }, properties: { "attr.data-status": "status()", "attr.data-tone": "tone()" }, classAttribute: "mk-result" }, ngImport: i0, template: "<div class=\"mk-result__icon\" aria-hidden=\"true\">\n <span class=\"mk-result__icon-custom\"><ng-content select=\"[mkResultIcon]\" /></span>\n <span class=\"mk-result__icon-default\">\n @switch (iconName()) {\n @case ('check') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M8 12.5l2.5 2.5L16 9.5\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n @case ('x') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M9 9l6 6M15 9l-6 6\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linecap=\"round\" />\n </svg>\n }\n @case ('alert') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <path d=\"M12 3.5l9 15.5H3z\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linejoin=\"round\" />\n <path d=\"M12 10v4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\n <circle cx=\"12\" cy=\"16.5\" r=\"1\" fill=\"currentColor\" />\n </svg>\n }\n @case ('search') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"10.5\" cy=\"10.5\" r=\"6.5\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M15.5 15.5L21 21\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linecap=\"round\" />\n </svg>\n }\n @default {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M12 11v5\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\n <circle cx=\"12\" cy=\"8\" r=\"1\" fill=\"currentColor\" />\n </svg>\n }\n }\n </span>\n</div>\n\n@if (resultTitle(); as heading) {\n <h2 class=\"mk-result__title\">{{ heading }}</h2>\n} @else {\n <ng-content select=\"[mkResultTitle]\" />\n}\n\n@if (subtitle(); as text) {\n <p class=\"mk-result__subtitle\">{{ text }}</p>\n}\n\n<div class=\"mk-result__body\">\n <ng-content />\n</div>\n\n<div class=\"mk-result__actions\">\n <ng-content select=\"[mkResultActions]\" />\n</div>\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--mk-space-2);max-width:32rem;margin-inline:auto;padding:var(--mk-space-12) var(--mk-space-6);text-align:center;font-family:var(--mk-font-sans);color:var(--mk-text)}.mk-result__icon{display:inline-flex;align-items:center;justify-content:center;margin-bottom:var(--mk-space-2);color:var(--mk-text-muted)}:host([data-tone=success]) .mk-result__icon{color:var(--mk-success)}:host([data-tone=danger]) .mk-result__icon{color:var(--mk-danger-text)}:host([data-tone=warning]) .mk-result__icon{color:var(--mk-warning)}:host([data-tone=info]) .mk-result__icon{color:var(--mk-info, var(--mk-primary))}:host([data-tone=muted]) .mk-result__icon{color:var(--mk-text-muted)}.mk-result__icon-custom:empty{display:none}.mk-result__icon:has(.mk-result__icon-custom>*) .mk-result__icon-default{display:none}.mk-result__title{margin:0;font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-result__subtitle{margin:0;max-width:40ch;font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-normal);color:var(--mk-text-muted)}.mk-result__body:empty{display:none}.mk-result__actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--mk-space-2);margin-top:var(--mk-space-4)}.mk-result__actions:empty{display:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
680
+ }
681
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkResult, decorators: [{
682
+ type: Component,
683
+ args: [{ selector: 'mk-result', changeDetection: ChangeDetectionStrategy.OnPush, host: {
684
+ class: 'mk-result',
685
+ role: 'status',
686
+ '[attr.data-status]': 'status()',
687
+ '[attr.data-tone]': 'tone()',
688
+ }, template: "<div class=\"mk-result__icon\" aria-hidden=\"true\">\n <span class=\"mk-result__icon-custom\"><ng-content select=\"[mkResultIcon]\" /></span>\n <span class=\"mk-result__icon-default\">\n @switch (iconName()) {\n @case ('check') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M8 12.5l2.5 2.5L16 9.5\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n }\n @case ('x') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M9 9l6 6M15 9l-6 6\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linecap=\"round\" />\n </svg>\n }\n @case ('alert') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <path d=\"M12 3.5l9 15.5H3z\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linejoin=\"round\" />\n <path d=\"M12 10v4\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\n <circle cx=\"12\" cy=\"16.5\" r=\"1\" fill=\"currentColor\" />\n </svg>\n }\n @case ('search') {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"10.5\" cy=\"10.5\" r=\"6.5\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M15.5 15.5L21 21\" stroke=\"currentColor\" stroke-width=\"1.6\"\n stroke-linecap=\"round\" />\n </svg>\n }\n @default {\n <svg viewBox=\"0 0 24 24\" width=\"64\" height=\"64\" fill=\"none\" focusable=\"false\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" stroke-width=\"1.6\" />\n <path d=\"M12 11v5\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" />\n <circle cx=\"12\" cy=\"8\" r=\"1\" fill=\"currentColor\" />\n </svg>\n }\n }\n </span>\n</div>\n\n@if (resultTitle(); as heading) {\n <h2 class=\"mk-result__title\">{{ heading }}</h2>\n} @else {\n <ng-content select=\"[mkResultTitle]\" />\n}\n\n@if (subtitle(); as text) {\n <p class=\"mk-result__subtitle\">{{ text }}</p>\n}\n\n<div class=\"mk-result__body\">\n <ng-content />\n</div>\n\n<div class=\"mk-result__actions\">\n <ng-content select=\"[mkResultActions]\" />\n</div>\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--mk-space-2);max-width:32rem;margin-inline:auto;padding:var(--mk-space-12) var(--mk-space-6);text-align:center;font-family:var(--mk-font-sans);color:var(--mk-text)}.mk-result__icon{display:inline-flex;align-items:center;justify-content:center;margin-bottom:var(--mk-space-2);color:var(--mk-text-muted)}:host([data-tone=success]) .mk-result__icon{color:var(--mk-success)}:host([data-tone=danger]) .mk-result__icon{color:var(--mk-danger-text)}:host([data-tone=warning]) .mk-result__icon{color:var(--mk-warning)}:host([data-tone=info]) .mk-result__icon{color:var(--mk-info, var(--mk-primary))}:host([data-tone=muted]) .mk-result__icon{color:var(--mk-text-muted)}.mk-result__icon-custom:empty{display:none}.mk-result__icon:has(.mk-result__icon-custom>*) .mk-result__icon-default{display:none}.mk-result__title{margin:0;font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-result__subtitle{margin:0;max-width:40ch;font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-normal);color:var(--mk-text-muted)}.mk-result__body:empty{display:none}.mk-result__actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--mk-space-2);margin-top:var(--mk-space-4)}.mk-result__actions:empty{display:none}\n"] }]
689
+ }], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], resultTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "resultTitle", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }] } });
690
+
691
+ /**
692
+ * Hovercard — a rich hover-preview panel anchored to a trigger, in the spirit of
693
+ * GitHub's user hovercards. Unlike {@link MkTooltip} (short text) it projects
694
+ * arbitrary content, and unlike {@link MkPopover} (click) it opens on hover/focus
695
+ * after a short delay and closes when the pointer leaves both the trigger and the
696
+ * card. It is driven by the `mkHovercardFor` directive (or the `open`/`close` API).
697
+ *
698
+ * The panel renders in the browser top layer through {@link MkAnchoredPanel}, so
699
+ * it is never clipped by an ancestor's `overflow`/`transform`. It is non-modal —
700
+ * no focus trap. Moving the pointer from the trigger onto the card keeps it open;
701
+ * Escape closes it.
702
+ *
703
+ * ```html
704
+ * <a href="/u/ada" [mkHovercardFor]="ada">@ada</a>
705
+ * <mk-hovercard #ada ariaLabel="Ada Lovelace">
706
+ * <img src="/ada.png" alt="" /><strong>Ada Lovelace</strong>
707
+ * </mk-hovercard>
708
+ * ```
709
+ */
710
+ class MkHovercard {
711
+ document = inject(DOCUMENT);
712
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
713
+ /** Stable id for `aria-controls` on the trigger. */
714
+ panelId = mkUniqueId('mk-hovercard');
715
+ /** Preferred placement relative to the trigger. */
716
+ placement = input('bottom-start', /* @ts-ignore */
717
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
718
+ /** Accessible label for the panel. */
719
+ ariaLabel = input(/* @ts-ignore */
720
+ ...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
721
+ /** Delay in ms before the card opens on hover/focus. Default `300`. */
722
+ openDelay = input(300, { ...(ngDevMode ? { debugName: "openDelay" } : /* istanbul ignore next */ {}), transform: numberAttribute });
723
+ /** Delay in ms before the card closes after the pointer leaves. Default `200`. */
724
+ closeDelay = input(200, { ...(ngDevMode ? { debugName: "closeDelay" } : /* istanbul ignore next */ {}), transform: numberAttribute });
725
+ /** Close the card when Escape is pressed while it is open. Default `true`. */
726
+ closeOnEscape = input(true, { ...(ngDevMode ? { debugName: "closeOnEscape" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
727
+ /** Emitted whenever the hovercard closes (any reason). */
728
+ closed = output();
729
+ _open = signal(false, /* @ts-ignore */
730
+ ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
731
+ /** Whether the hovercard is open. */
732
+ opened = this._open.asReadonly();
733
+ anchorEl = signal(undefined, /* @ts-ignore */
734
+ ...(ngDevMode ? [{ debugName: "anchorEl" }] : /* istanbul ignore next */ []));
735
+ triggerEl = null;
736
+ openTimer = null;
737
+ closeTimer = null;
738
+ /** Open the card anchored to `trigger` after `openDelay` (cancels a pending close). */
739
+ scheduleOpen(trigger) {
740
+ if (!this.isBrowser)
741
+ return;
742
+ this.clearCloseTimer();
743
+ if (this._open()) {
744
+ // Already open — just make sure the anchor is current.
745
+ this.triggerEl = trigger;
746
+ this.anchorEl.set(trigger);
747
+ return;
748
+ }
749
+ if (this.openTimer !== null)
750
+ return;
751
+ this.openTimer = this.setTimer(() => {
752
+ this.openTimer = null;
753
+ this.open(trigger);
754
+ }, this.openDelay());
755
+ }
756
+ /** Close the card after `closeDelay` (cancels a pending open). */
757
+ scheduleClose() {
758
+ if (!this.isBrowser)
759
+ return;
760
+ this.clearOpenTimer();
761
+ if (!this._open() || this.closeTimer !== null)
762
+ return;
763
+ this.closeTimer = this.setTimer(() => {
764
+ this.closeTimer = null;
765
+ this.close();
766
+ }, this.closeDelay());
767
+ }
768
+ /**
769
+ * Keep the card open: cancel any pending open/close. Called when the pointer
770
+ * enters the card itself, so moving from the trigger to the card is seamless.
771
+ */
772
+ keepOpen() {
773
+ this.clearCloseTimer();
774
+ }
775
+ /** Open the card immediately, anchored to `trigger`. */
776
+ open(trigger) {
777
+ if (!this.isBrowser || this._open())
778
+ return;
779
+ this.clearOpenTimer();
780
+ this.triggerEl = trigger;
781
+ this.anchorEl.set(trigger);
782
+ this._open.set(true);
783
+ }
784
+ /** Close the card immediately. */
785
+ close() {
786
+ this.clearOpenTimer();
787
+ this.clearCloseTimer();
788
+ if (!this._open())
789
+ return;
790
+ this._open.set(false);
791
+ this.anchorEl.set(undefined);
792
+ this.triggerEl = null;
793
+ this.closed.emit();
794
+ }
795
+ onCardEnter() {
796
+ this.keepOpen();
797
+ }
798
+ onCardLeave() {
799
+ this.scheduleClose();
800
+ }
801
+ /**
802
+ * Keyboard mirror of `mouseleave`: when focus moves somewhere outside BOTH
803
+ * the card and its trigger, schedule a close. Focus hopping between the
804
+ * trigger and the card (either direction) keeps it open, so the card's
805
+ * content stays reachable with Tab.
806
+ */
807
+ onCardFocusout(event) {
808
+ const related = event.relatedTarget;
809
+ if (!related)
810
+ return;
811
+ const panel = event.currentTarget;
812
+ if (panel?.contains(related) || this.triggerEl?.contains(related))
813
+ return;
814
+ this.scheduleClose();
815
+ }
816
+ onEscape(event) {
817
+ if (!this.closeOnEscape())
818
+ return;
819
+ event.preventDefault();
820
+ event.stopPropagation();
821
+ // Escape may be pressed while focus is INSIDE the panel; closing destroys
822
+ // it, which would otherwise drop focus on <body>. Hand it back to the
823
+ // trigger so the keyboard user keeps their place.
824
+ const trigger = this.triggerEl;
825
+ this.close();
826
+ trigger?.focus();
827
+ }
828
+ setTimer(fn, delay) {
829
+ const view = this.document.defaultView;
830
+ return view ? view.setTimeout(fn, delay) : null;
831
+ }
832
+ clearOpenTimer() {
833
+ if (this.openTimer !== null) {
834
+ this.document.defaultView?.clearTimeout(this.openTimer);
835
+ this.openTimer = null;
836
+ }
837
+ }
838
+ clearCloseTimer() {
839
+ if (this.closeTimer !== null) {
840
+ this.document.defaultView?.clearTimeout(this.closeTimer);
841
+ this.closeTimer = null;
842
+ }
843
+ }
844
+ ngOnDestroy() {
845
+ this.clearOpenTimer();
846
+ this.clearCloseTimer();
847
+ }
848
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHovercard, deps: [], target: i0.ɵɵFactoryTarget.Component });
849
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkHovercard, isStandalone: true, selector: "mk-hovercard", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, openDelay: { classPropertyName: "openDelay", publicName: "openDelay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null }, closeOnEscape: { classPropertyName: "closeOnEscape", publicName: "closeOnEscape", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { classAttribute: "mk-hovercard" }, exportAs: ["mkHovercard"], ngImport: i0, template: "@if (opened()) {\n <div\n #panel\n class=\"mk-hovercard__panel\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel()\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"anchorEl()\"\n [placement]=\"placement()\"\n [gap]=\"8\"\n (dismiss)=\"close()\"\n (mouseenter)=\"onCardEnter()\"\n (mouseleave)=\"onCardLeave()\"\n (focusin)=\"onCardEnter()\"\n (focusout)=\"onCardFocusout($event)\"\n (keydown.escape)=\"onEscape($event)\"\n >\n <ng-content />\n </div>\n}\n", styles: [":host{display:contents}.mk-hovercard__panel{pointer-events:auto;min-width:16rem;max-width:min(24rem,100vw - var(--mk-space-4));padding:var(--mk-space-4);color:var(--mk-text);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);animation:mk-hovercard-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-hovercard__panel:focus-visible{outline:none}@keyframes mk-hovercard-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-hovercard__panel{animation:none}}\n"], dependencies: [{ kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
850
+ }
851
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHovercard, decorators: [{
852
+ type: Component,
853
+ args: [{ selector: 'mk-hovercard', exportAs: 'mkHovercard', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAnchoredPanel], host: {
854
+ class: 'mk-hovercard',
855
+ }, template: "@if (opened()) {\n <div\n #panel\n class=\"mk-hovercard__panel\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel()\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"anchorEl()\"\n [placement]=\"placement()\"\n [gap]=\"8\"\n (dismiss)=\"close()\"\n (mouseenter)=\"onCardEnter()\"\n (mouseleave)=\"onCardLeave()\"\n (focusin)=\"onCardEnter()\"\n (focusout)=\"onCardFocusout($event)\"\n (keydown.escape)=\"onEscape($event)\"\n >\n <ng-content />\n </div>\n}\n", styles: [":host{display:contents}.mk-hovercard__panel{pointer-events:auto;min-width:16rem;max-width:min(24rem,100vw - var(--mk-space-4));padding:var(--mk-space-4);color:var(--mk-text);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);animation:mk-hovercard-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-hovercard__panel:focus-visible{outline:none}@keyframes mk-hovercard-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-hovercard__panel{animation:none}}\n"] }]
856
+ }], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], openDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "openDelay", required: false }] }], closeDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeDelay", required: false }] }], closeOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEscape", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
857
+
858
+ /**
859
+ * A touch tap fires emulated `mouseenter` + `focus` right after `pointerdown`;
860
+ * for this long after a touch toggle those synthetic enters are ignored, so a
861
+ * tap that just CLOSED the card is not undone by its own compatibility events.
862
+ */
863
+ const TOUCH_ENTER_SUPPRESS_MS = 500;
864
+ /**
865
+ * Turns its host element into a hover/focus trigger for an `<mk-hovercard>`.
866
+ * On `mouseenter`/`focus` it opens the card (after the card's `openDelay`),
867
+ * anchored to the host; on `mouseleave`/`blur` it schedules a close (after the
868
+ * card's `closeDelay`). Moving the pointer — or keyboard focus — from the host
869
+ * onto the card cancels that close, so the card stays open and its content is
870
+ * reachable with Tab. Escape closes it.
871
+ *
872
+ * On touch there is no hover, so a tap toggles instead: tap opens the card
873
+ * immediately, tapping the trigger again closes it, and a tap anywhere else
874
+ * closes it through the panel's outside-pointerdown dismissal.
875
+ *
876
+ * ```html
877
+ * <a href="/u/ada" [mkHovercardFor]="ada">@ada</a>
878
+ * <mk-hovercard #ada>…</mk-hovercard>
879
+ * ```
880
+ */
881
+ class MkHovercardTrigger {
882
+ host = inject(ElementRef);
883
+ /** Timestamp of the last touch toggle — guards `onEnter` (see above). */
884
+ lastTouchToggle = Number.NEGATIVE_INFINITY;
885
+ /** The hovercard instance to control. */
886
+ hovercard = input.required({ ...(ngDevMode ? { debugName: "hovercard" } : /* istanbul ignore next */ {}), alias: 'mkHovercardFor' });
887
+ onEnter() {
888
+ if (Date.now() - this.lastTouchToggle < TOUCH_ENTER_SUPPRESS_MS)
889
+ return;
890
+ this.hovercard().scheduleOpen(this.host.nativeElement);
891
+ }
892
+ /**
893
+ * Touch tap-toggle. Mouse and pen are ignored here — they keep the pure
894
+ * hover/focus behaviour. A tap while the card is open closes it (a tap
895
+ * OUTSIDE both trigger and panel is handled by the anchored panel's own
896
+ * outside-pointerdown dismiss, which excludes the anchor, so the two paths
897
+ * never double-fire).
898
+ */
899
+ onPointerDown(event) {
900
+ if (event.pointerType !== 'touch')
901
+ return;
902
+ this.lastTouchToggle = Date.now();
903
+ const card = this.hovercard();
904
+ if (card.opened()) {
905
+ card.close();
906
+ }
907
+ else {
908
+ // No openDelay on touch — the tap is deliberate, not a passing pointer.
909
+ card.open(this.host.nativeElement);
910
+ }
911
+ }
912
+ onLeave() {
913
+ this.hovercard().scheduleClose();
914
+ }
915
+ onKeydown(event) {
916
+ if (event.key === 'Escape' && this.hovercard().opened()) {
917
+ event.preventDefault();
918
+ this.hovercard().close();
919
+ }
920
+ }
921
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHovercardTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
922
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkHovercardTrigger, isStandalone: true, selector: "[mkHovercardFor]", inputs: { hovercard: { classPropertyName: "hovercard", publicName: "mkHovercardFor", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "aria-haspopup": "dialog" }, listeners: { "mouseenter": "onEnter()", "focus": "onEnter()", "mouseleave": "onLeave()", "blur": "onLeave()", "pointerdown": "onPointerDown($event)", "keydown": "onKeydown($event)" }, properties: { "attr.aria-expanded": "hovercard().opened()", "attr.aria-controls": "hovercard().panelId" } }, exportAs: ["mkHovercardTrigger"], ngImport: i0 });
923
+ }
924
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHovercardTrigger, decorators: [{
925
+ type: Directive,
926
+ args: [{
927
+ selector: '[mkHovercardFor]',
928
+ exportAs: 'mkHovercardTrigger',
929
+ host: {
930
+ 'aria-haspopup': 'dialog',
931
+ '[attr.aria-expanded]': 'hovercard().opened()',
932
+ '[attr.aria-controls]': 'hovercard().panelId',
933
+ '(mouseenter)': 'onEnter()',
934
+ '(focus)': 'onEnter()',
935
+ '(mouseleave)': 'onLeave()',
936
+ '(blur)': 'onLeave()',
937
+ '(pointerdown)': 'onPointerDown($event)',
938
+ '(keydown)': 'onKeydown($event)',
939
+ },
940
+ }]
941
+ }], propDecorators: { hovercard: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkHovercardFor", required: true }] }] } });
942
+
943
+ /**
944
+ * NotificationCenter — a bell trigger with an unread-count badge that opens a
945
+ * dropdown panel listing notifications, each markable as read.
946
+ *
947
+ * The panel renders in the browser top layer through {@link MkAnchoredPanel},
948
+ * so it is never clipped by an ancestor's `overflow`/`transform`. Clicking the
949
+ * bell toggles it; an outside click, Tabbing out or Escape closes it and
950
+ * restores focus to the bell. Clicking a row marks that one read (updating the
951
+ * two-way `notifications` immutably); "Mark all read" clears every unread item.
952
+ *
953
+ * ```html
954
+ * <mk-notification-center
955
+ * [(notifications)]="items"
956
+ * (itemClick)="open($event)"
957
+ * (markedAllRead)="sync()" />
958
+ * ```
959
+ */
960
+ class MkNotificationCenter {
961
+ injector = inject(Injector);
962
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
963
+ panelRef = viewChild('panel', /* @ts-ignore */
964
+ ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
965
+ triggerRef = viewChild('trigger', /* @ts-ignore */
966
+ ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
967
+ i18n = inject(MK_I18N);
968
+ /** Stable id linking the trigger to the panel via `aria-controls`. */
969
+ panelId = mkUniqueId('mk-notification-center');
970
+ /** Stable id for the panel's labelling heading. */
971
+ titleId = mkUniqueId('mk-notification-center-title');
972
+ /** Two-way list of notifications rendered in the panel. */
973
+ notifications = model([], /* @ts-ignore */
974
+ ...(ngDevMode ? [{ debugName: "notifications" }] : /* istanbul ignore next */ []));
975
+ /** Heading shown at the top of the panel. */
976
+ panelTitle = input(this.i18n.notificationsTitle, /* @ts-ignore */
977
+ ...(ngDevMode ? [{ debugName: "panelTitle" }] : /* istanbul ignore next */ []));
978
+ /** Message shown by the empty state when there are no notifications. */
979
+ emptyText = input(this.i18n.allCaughtUp, /* @ts-ignore */
980
+ ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
981
+ /** Max height of the scrollable list (any CSS length). */
982
+ maxHeight = input('24rem', /* @ts-ignore */
983
+ ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
984
+ /** Emitted with a notification when its row is clicked. */
985
+ itemClick = output();
986
+ /** Emitted with a notification when it transitions to read. */
987
+ read = output();
988
+ /** Emitted when every notification is marked read at once. */
989
+ markedAllRead = output();
990
+ _open = signal(false, /* @ts-ignore */
991
+ ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
992
+ /** Whether the panel is open. */
993
+ open = this._open.asReadonly();
994
+ /** Count of notifications that are not yet read. */
995
+ unreadCount = computed(() => this.notifications().filter((n) => !n.read).length, /* @ts-ignore */
996
+ ...(ngDevMode ? [{ debugName: "unreadCount" }] : /* istanbul ignore next */ []));
997
+ /** Accessible label for the bell trigger. */
998
+ triggerLabel = computed(() => this.i18n.notificationsUnread(this.unreadCount()), /* @ts-ignore */
999
+ ...(ngDevMode ? [{ debugName: "triggerLabel" }] : /* istanbul ignore next */ []));
1000
+ /** Badge text, capped at "99+". */
1001
+ badgeText = computed(() => {
1002
+ const n = this.unreadCount();
1003
+ return n > 99 ? '99+' : `${n}`;
1004
+ }, /* @ts-ignore */
1005
+ ...(ngDevMode ? [{ debugName: "badgeText" }] : /* istanbul ignore next */ []));
1006
+ /** Toggle the panel open/closed. */
1007
+ toggle() {
1008
+ this._open() ? this.close() : this.openPanel();
1009
+ }
1010
+ /** Open the panel, moving focus into it on the next render. */
1011
+ openPanel() {
1012
+ if (!this.isBrowser || this._open())
1013
+ return;
1014
+ this._open.set(true);
1015
+ afterNextRender(() => this.focusInitial(), { injector: this.injector });
1016
+ }
1017
+ /** Close the panel; optionally restore focus to the bell trigger. */
1018
+ close(restoreFocus = true) {
1019
+ if (!this._open())
1020
+ return;
1021
+ this._open.set(false);
1022
+ if (restoreFocus)
1023
+ this.triggerRef()?.nativeElement.focus();
1024
+ }
1025
+ /** Mark every notification read and emit {@link markedAllRead}. */
1026
+ markAllRead() {
1027
+ if (this.unreadCount() === 0)
1028
+ return;
1029
+ this.notifications.update((list) => list.map((n) => (n.read ? n : { ...n, read: true })));
1030
+ this.markedAllRead.emit();
1031
+ }
1032
+ /** Handle a row click: mark it read (if needed) and emit {@link itemClick}. */
1033
+ onRowClick(item) {
1034
+ if (!item.read)
1035
+ this.markRead(item);
1036
+ this.itemClick.emit(item);
1037
+ }
1038
+ markRead(item) {
1039
+ let updated;
1040
+ this.notifications.update((list) => list.map((n) => {
1041
+ if (n !== item || n.read)
1042
+ return n;
1043
+ updated = { ...n, read: true };
1044
+ return updated;
1045
+ }));
1046
+ if (updated)
1047
+ this.read.emit(updated);
1048
+ }
1049
+ onEscape(event) {
1050
+ event.preventDefault();
1051
+ event.stopPropagation();
1052
+ this.close(true);
1053
+ }
1054
+ onPanelFocusout(event) {
1055
+ const related = event.relatedTarget;
1056
+ if (!related)
1057
+ return;
1058
+ const panel = this.panelRef()?.nativeElement;
1059
+ const trigger = this.triggerRef()?.nativeElement;
1060
+ if (panel?.contains(related) || trigger?.contains(related))
1061
+ return;
1062
+ this.close(false);
1063
+ }
1064
+ focusInitial() {
1065
+ const panel = this.panelRef()?.nativeElement;
1066
+ if (!panel)
1067
+ return;
1068
+ (mkGetFocusable(panel)[0] ?? panel).focus();
1069
+ }
1070
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkNotificationCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1071
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkNotificationCenter, isStandalone: true, selector: "mk-notification-center", inputs: { notifications: { classPropertyName: "notifications", publicName: "notifications", isSignal: true, isRequired: false, transformFunction: null }, panelTitle: { classPropertyName: "panelTitle", publicName: "panelTitle", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { notifications: "notificationsChange", itemClick: "itemClick", read: "read", markedAllRead: "markedAllRead" }, host: { classAttribute: "mk-notification-center" }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<button\n #trigger\n type=\"button\"\n class=\"mk-notification-center__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId : null\"\n [attr.aria-label]=\"triggerLabel()\"\n (click)=\"toggle()\"\n>\n <mk-icon name=\"bell\" />\n @if (unreadCount() > 0) {\n <mk-badge\n class=\"mk-notification-center__count\"\n tone=\"danger\"\n variant=\"solid\"\n size=\"sm\"\n aria-hidden=\"true\"\n >\n {{ badgeText() }}\n </mk-badge>\n }\n</button>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-notification-center__panel\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-labelledby]=\"titleId\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"trigger\"\n placement=\"bottom-end\"\n [gap]=\"8\"\n (dismiss)=\"close(false)\"\n (keydown.escape)=\"onEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <header class=\"mk-notification-center__header\">\n <h2 [id]=\"titleId\" class=\"mk-notification-center__title\">\n {{ panelTitle() }}\n </h2>\n <!--\n aria-disabled (not [disabled]) so the button stays focusable when it\n disables itself after activation; markAllRead() guards the no-op case.\n -->\n <button\n type=\"button\"\n class=\"mk-notification-center__mark-all\"\n [attr.aria-disabled]=\"unreadCount() === 0\"\n (click)=\"markAllRead()\"\n >\n {{ i18n.markAllRead }}\n </button>\n </header>\n\n @if (notifications().length > 0) {\n <ul\n class=\"mk-notification-center__list\"\n [style.max-height]=\"maxHeight()\"\n >\n @for (item of notifications(); track item.id) {\n <li>\n <button\n type=\"button\"\n class=\"mk-notification-center__item\"\n [class.mk-notification-center__item--unread]=\"!item.read\"\n (click)=\"onRowClick(item)\"\n >\n <span\n class=\"mk-notification-center__dot\"\n aria-hidden=\"true\"\n ></span>\n @if (!item.read) {\n <span class=\"mk-visually-hidden\">{{ i18n.unread }}</span>\n }\n <span class=\"mk-notification-center__body\">\n <span class=\"mk-notification-center__item-title\">\n {{ item.title }}\n </span>\n @if (item.body) {\n <span class=\"mk-notification-center__item-text\">\n {{ item.body }}\n </span>\n }\n @if (item.time) {\n <span class=\"mk-notification-center__item-time\">\n {{ item.time }}\n </span>\n }\n </span>\n </button>\n </li>\n }\n </ul>\n } @else {\n <mk-empty-state\n class=\"mk-notification-center__empty\"\n icon=\"bell\"\n size=\"sm\"\n [title]=\"emptyText()\"\n />\n }\n </div>\n}\n", styles: [":host{display:inline-block}.mk-notification-center__trigger{position:relative;display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;padding:0;color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;font-family:var(--mk-font-sans);line-height:1}.mk-notification-center__trigger:hover{background:var(--mk-hover-overlay)}.mk-notification-center__trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-notification-center__count{position:absolute;top:.125rem;inset-inline-end:.125rem;pointer-events:none;transform:translate(35%,-35%)}.mk-notification-center__count:dir(rtl){transform:translate(-35%,-35%)}.mk-notification-center__panel{display:flex;flex-direction:column;width:min(22rem,100vw - var(--mk-space-4));color:var(--mk-text);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);overflow:hidden;animation:mk-notification-center-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-notification-center__panel:focus-visible{outline:none}.mk-notification-center__header{display:flex;align-items:center;justify-content:space-between;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-notification-center__title{margin:0;font-size:var(--mk-font-size-md);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug)}.mk-notification-center__mark-all{padding:0;color:var(--mk-primary);background:transparent;border:none;cursor:pointer;font:inherit;font-size:var(--mk-font-size-sm);white-space:nowrap}.mk-notification-center__mark-all:hover:not([aria-disabled=true]){text-decoration:underline}.mk-notification-center__mark-all:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-sm)}.mk-notification-center__mark-all[aria-disabled=true]{color:var(--mk-text-muted);cursor:default}.mk-notification-center__list{margin:0;padding:0;list-style:none;overflow-y:auto}.mk-notification-center__list li+li{border-top:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-notification-center__item{display:flex;align-items:flex-start;gap:var(--mk-space-2);width:100%;padding:var(--mk-space-3) var(--mk-space-4);color:inherit;background:transparent;border:none;cursor:pointer;font:inherit;text-align:start}.mk-notification-center__item:hover{background:var(--mk-hover-overlay)}.mk-notification-center__item:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-notification-center__dot{flex:none;width:.5rem;height:.5rem;margin-top:.375rem;border-radius:var(--mk-radius-circle);background:transparent}.mk-notification-center__item--unread .mk-notification-center__dot{background:var(--mk-primary)}.mk-notification-center__body{display:flex;flex-direction:column;gap:var(--mk-space-1);min-width:0}.mk-notification-center__item-title{font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-notification-center__item--unread .mk-notification-center__item-title{font-weight:var(--mk-font-weight-semibold)}.mk-notification-center__item-text{font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);color:var(--mk-text-muted)}.mk-notification-center__item-time{font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}@keyframes mk-notification-center-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-notification-center__panel{animation:none}}\n"], dependencies: [{ kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }, { kind: "component", type: MkBadge, selector: "mk-badge", inputs: ["tone", "variant", "size", "dot"] }, { kind: "component", type: MkEmptyState, selector: "mk-empty-state", inputs: ["icon", "title", "description", "size"] }, { kind: "component", type: MkIcon, selector: "mk-icon", inputs: ["name", "size", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1072
+ }
1073
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkNotificationCenter, decorators: [{
1074
+ type: Component,
1075
+ args: [{ selector: 'mk-notification-center', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAnchoredPanel, MkBadge, MkEmptyState, MkIcon], host: {
1076
+ class: 'mk-notification-center',
1077
+ }, template: "<button\n #trigger\n type=\"button\"\n class=\"mk-notification-center__trigger\"\n aria-haspopup=\"dialog\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"open() ? panelId : null\"\n [attr.aria-label]=\"triggerLabel()\"\n (click)=\"toggle()\"\n>\n <mk-icon name=\"bell\" />\n @if (unreadCount() > 0) {\n <mk-badge\n class=\"mk-notification-center__count\"\n tone=\"danger\"\n variant=\"solid\"\n size=\"sm\"\n aria-hidden=\"true\"\n >\n {{ badgeText() }}\n </mk-badge>\n }\n</button>\n\n@if (open()) {\n <div\n #panel\n class=\"mk-notification-center__panel\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-labelledby]=\"titleId\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"trigger\"\n placement=\"bottom-end\"\n [gap]=\"8\"\n (dismiss)=\"close(false)\"\n (keydown.escape)=\"onEscape($event)\"\n (focusout)=\"onPanelFocusout($event)\"\n >\n <header class=\"mk-notification-center__header\">\n <h2 [id]=\"titleId\" class=\"mk-notification-center__title\">\n {{ panelTitle() }}\n </h2>\n <!--\n aria-disabled (not [disabled]) so the button stays focusable when it\n disables itself after activation; markAllRead() guards the no-op case.\n -->\n <button\n type=\"button\"\n class=\"mk-notification-center__mark-all\"\n [attr.aria-disabled]=\"unreadCount() === 0\"\n (click)=\"markAllRead()\"\n >\n {{ i18n.markAllRead }}\n </button>\n </header>\n\n @if (notifications().length > 0) {\n <ul\n class=\"mk-notification-center__list\"\n [style.max-height]=\"maxHeight()\"\n >\n @for (item of notifications(); track item.id) {\n <li>\n <button\n type=\"button\"\n class=\"mk-notification-center__item\"\n [class.mk-notification-center__item--unread]=\"!item.read\"\n (click)=\"onRowClick(item)\"\n >\n <span\n class=\"mk-notification-center__dot\"\n aria-hidden=\"true\"\n ></span>\n @if (!item.read) {\n <span class=\"mk-visually-hidden\">{{ i18n.unread }}</span>\n }\n <span class=\"mk-notification-center__body\">\n <span class=\"mk-notification-center__item-title\">\n {{ item.title }}\n </span>\n @if (item.body) {\n <span class=\"mk-notification-center__item-text\">\n {{ item.body }}\n </span>\n }\n @if (item.time) {\n <span class=\"mk-notification-center__item-time\">\n {{ item.time }}\n </span>\n }\n </span>\n </button>\n </li>\n }\n </ul>\n } @else {\n <mk-empty-state\n class=\"mk-notification-center__empty\"\n icon=\"bell\"\n size=\"sm\"\n [title]=\"emptyText()\"\n />\n }\n </div>\n}\n", styles: [":host{display:inline-block}.mk-notification-center__trigger{position:relative;display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;padding:0;color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);cursor:pointer;font-family:var(--mk-font-sans);line-height:1}.mk-notification-center__trigger:hover{background:var(--mk-hover-overlay)}.mk-notification-center__trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-notification-center__count{position:absolute;top:.125rem;inset-inline-end:.125rem;pointer-events:none;transform:translate(35%,-35%)}.mk-notification-center__count:dir(rtl){transform:translate(-35%,-35%)}.mk-notification-center__panel{display:flex;flex-direction:column;width:min(22rem,100vw - var(--mk-space-4));color:var(--mk-text);background:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);overflow:hidden;animation:mk-notification-center-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-notification-center__panel:focus-visible{outline:none}.mk-notification-center__header{display:flex;align-items:center;justify-content:space-between;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-notification-center__title{margin:0;font-size:var(--mk-font-size-md);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug)}.mk-notification-center__mark-all{padding:0;color:var(--mk-primary);background:transparent;border:none;cursor:pointer;font:inherit;font-size:var(--mk-font-size-sm);white-space:nowrap}.mk-notification-center__mark-all:hover:not([aria-disabled=true]){text-decoration:underline}.mk-notification-center__mark-all:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-sm)}.mk-notification-center__mark-all[aria-disabled=true]{color:var(--mk-text-muted);cursor:default}.mk-notification-center__list{margin:0;padding:0;list-style:none;overflow-y:auto}.mk-notification-center__list li+li{border-top:var(--mk-border-width) solid var(--mk-border-subtle)}.mk-notification-center__item{display:flex;align-items:flex-start;gap:var(--mk-space-2);width:100%;padding:var(--mk-space-3) var(--mk-space-4);color:inherit;background:transparent;border:none;cursor:pointer;font:inherit;text-align:start}.mk-notification-center__item:hover{background:var(--mk-hover-overlay)}.mk-notification-center__item:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-notification-center__dot{flex:none;width:.5rem;height:.5rem;margin-top:.375rem;border-radius:var(--mk-radius-circle);background:transparent}.mk-notification-center__item--unread .mk-notification-center__dot{background:var(--mk-primary)}.mk-notification-center__body{display:flex;flex-direction:column;gap:var(--mk-space-1);min-width:0}.mk-notification-center__item-title{font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-notification-center__item--unread .mk-notification-center__item-title{font-weight:var(--mk-font-weight-semibold)}.mk-notification-center__item-text{font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);color:var(--mk-text-muted)}.mk-notification-center__item-time{font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}@keyframes mk-notification-center-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-notification-center__panel{animation:none}}\n"] }]
1078
+ }], propDecorators: { panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], notifications: [{ type: i0.Input, args: [{ isSignal: true, alias: "notifications", required: false }] }, { type: i0.Output, args: ["notificationsChange"] }], panelTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelTitle", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], read: [{ type: i0.Output, args: ["read"] }], markedAllRead: [{ type: i0.Output, args: ["markedAllRead"] }] } });
1079
+
1080
+ /**
1081
+ * Accessible heading for a dialog. Renders as a level-2 heading and exposes a
1082
+ * stable id that {@link MkDialog} wires to the panel's `aria-labelledby`.
1083
+ *
1084
+ * ```html
1085
+ * <mk-dialog>
1086
+ * <mk-dialog-title>Delete workspace</mk-dialog-title>
1087
+ * …
1088
+ * </mk-dialog>
1089
+ * ```
1090
+ */
1091
+ class MkDialogTitle {
1092
+ /** Stable id used for `aria-labelledby` wiring. */
1093
+ id = input(mkUniqueId('mk-dialog-title'), /* @ts-ignore */
1094
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
1095
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialogTitle, deps: [], target: i0.ɵɵFactoryTarget.Component });
1096
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkDialogTitle, isStandalone: true, selector: "mk-dialog-title", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "heading", "aria-level": "2" }, properties: { "attr.id": "id()" }, classAttribute: "mk-dialog__title" }, ngImport: i0, template: '<ng-content />', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1097
+ }
1098
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialogTitle, decorators: [{
1099
+ type: Component,
1100
+ args: [{
1101
+ selector: 'mk-dialog-title',
1102
+ template: '<ng-content />',
1103
+ changeDetection: ChangeDetectionStrategy.OnPush,
1104
+ host: {
1105
+ class: 'mk-dialog__title',
1106
+ role: 'heading',
1107
+ 'aria-level': '2',
1108
+ '[attr.id]': 'id()',
1109
+ },
1110
+ }]
1111
+ }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
1112
+ /**
1113
+ * Dialog layout — a surface with a sticky header, scrollable body, and sticky
1114
+ * footer, designed to be rendered inside an {@link MkOverlayService} panel. The
1115
+ * header exposes a close button wired to the injected {@link MkOverlayRef} and
1116
+ * the dialog's title is linked to the panel via `aria-labelledby`.
1117
+ *
1118
+ * Slots: `[mkDialogHeader]` / `<mk-dialog-title>` (header), default content
1119
+ * (body), `[mkDialogFooter]` (footer).
1120
+ *
1121
+ * ```html
1122
+ * <mk-dialog>
1123
+ * <mk-dialog-title>Rename</mk-dialog-title>
1124
+ * <p>Choose a new name.</p>
1125
+ * <div mkDialogFooter>
1126
+ * <button mkButton variant="ghost">Cancel</button>
1127
+ * <button mkButton>Save</button>
1128
+ * </div>
1129
+ * </mk-dialog>
1130
+ * ```
1131
+ */
1132
+ class MkDialog {
1133
+ host = inject(ElementRef);
1134
+ /** Localised strings (override globally via `provideMkI18n`). */
1135
+ i18n = inject(MK_I18N);
1136
+ overlayRef = inject(MkOverlayRef, { optional: true });
1137
+ /** Optional plain-text title. Ignored when a `<mk-dialog-title>` is projected. */
1138
+ dialogTitle = input(/* @ts-ignore */
1139
+ ...(ngDevMode ? [undefined, { debugName: "dialogTitle" }] : /* istanbul ignore next */ []));
1140
+ /** Stable id for the built-in title element. */
1141
+ titleId = input(mkUniqueId('mk-dialog-title'), /* @ts-ignore */
1142
+ ...(ngDevMode ? [{ debugName: "titleId" }] : /* istanbul ignore next */ []));
1143
+ /** Hide the header close button. */
1144
+ hideClose = input(false, { ...(ngDevMode ? { debugName: "hideClose" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1145
+ constructor() {
1146
+ afterNextRender(() => this.wireLabelledBy());
1147
+ }
1148
+ /** Close the surrounding overlay, if any. */
1149
+ close() {
1150
+ this.overlayRef?.close();
1151
+ }
1152
+ /** Point the owning dialog panel's `aria-labelledby` at the title element. */
1153
+ wireLabelledBy() {
1154
+ const el = this.host.nativeElement;
1155
+ const panel = el.closest('[role="dialog"], [role="alertdialog"]');
1156
+ if (!panel)
1157
+ return;
1158
+ const titleEl = el.querySelector('mk-dialog-title, .mk-dialog__title');
1159
+ if (!titleEl)
1160
+ return;
1161
+ if (!titleEl.id)
1162
+ titleEl.id = this.titleId();
1163
+ panel.setAttribute('aria-labelledby', titleEl.id);
1164
+ }
1165
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
1166
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkDialog, isStandalone: true, selector: "mk-dialog", inputs: { dialogTitle: { classPropertyName: "dialogTitle", publicName: "dialogTitle", isSignal: true, isRequired: false, transformFunction: null }, titleId: { classPropertyName: "titleId", publicName: "titleId", isSignal: true, isRequired: false, transformFunction: null }, hideClose: { classPropertyName: "hideClose", publicName: "hideClose", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mk-dialog" }, ngImport: i0, template: "<header class=\"mk-dialog__header\">\n <div class=\"mk-dialog__heading\">\n <ng-content select=\"[mkDialogHeader], mk-dialog-title\" />\n @if (dialogTitle(); as heading) {\n <h2 class=\"mk-dialog__title\" [id]=\"titleId()\">{{ heading }}</h2>\n }\n </div>\n @if (!hideClose()) {\n <button\n type=\"button\"\n class=\"mk-dialog__close\"\n [attr.aria-label]=\"i18n.close\"\n (click)=\"close()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n</header>\n\n<div class=\"mk-dialog__body\">\n <ng-content />\n</div>\n\n<footer class=\"mk-dialog__footer\">\n <ng-content select=\"[mkDialogFooter]\" />\n</footer>\n", styles: ["@charset \"UTF-8\";.mk-dialog{display:flex;flex-direction:column;min-height:0;max-height:inherit;flex:1 1 auto;font-family:var(--mk-font-sans)}.mk-dialog__header{position:sticky;top:0;z-index:1;display:flex;align-items:flex-start;gap:var(--mk-space-3);flex-shrink:0;padding:var(--mk-space-4) var(--mk-space-5);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);background-color:var(--mk-surface)}.mk-dialog__heading{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-dialog__title{margin:0;min-width:0;overflow-wrap:anywhere;font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-dialog__body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:var(--mk-space-5);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-normal);color:var(--mk-text)}.mk-dialog__footer{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;gap:var(--mk-space-2);padding:var(--mk-space-4) var(--mk-space-5);border-top:var(--mk-border-width) solid var(--mk-border-subtle);background-color:var(--mk-surface)}.mk-dialog__footer:empty{display:none}.mk-dialog__footer>*{display:flex;flex:1 1 auto;align-items:center;justify-content:flex-end;gap:var(--mk-space-2);flex-wrap:wrap}.mk-dialog__footer>[data-align=start]{justify-content:flex-start}.mk-dialog__footer>[data-align=center]{justify-content:center}.mk-dialog__footer>[data-align=between]{justify-content:space-between}.mk-dialog__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;margin:-4px -4px 0 0;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:var(--mk-text-muted);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-dialog__close:hover{color:var(--mk-text);background-color:var(--mk-hover-overlay)}.mk-dialog__close:focus-visible{color:var(--mk-text);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1167
+ }
1168
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialog, decorators: [{
1169
+ type: Component,
1170
+ args: [{ selector: 'mk-dialog', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'mk-dialog' }, template: "<header class=\"mk-dialog__header\">\n <div class=\"mk-dialog__heading\">\n <ng-content select=\"[mkDialogHeader], mk-dialog-title\" />\n @if (dialogTitle(); as heading) {\n <h2 class=\"mk-dialog__title\" [id]=\"titleId()\">{{ heading }}</h2>\n }\n </div>\n @if (!hideClose()) {\n <button\n type=\"button\"\n class=\"mk-dialog__close\"\n [attr.aria-label]=\"i18n.close\"\n (click)=\"close()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n</header>\n\n<div class=\"mk-dialog__body\">\n <ng-content />\n</div>\n\n<footer class=\"mk-dialog__footer\">\n <ng-content select=\"[mkDialogFooter]\" />\n</footer>\n", styles: ["@charset \"UTF-8\";.mk-dialog{display:flex;flex-direction:column;min-height:0;max-height:inherit;flex:1 1 auto;font-family:var(--mk-font-sans)}.mk-dialog__header{position:sticky;top:0;z-index:1;display:flex;align-items:flex-start;gap:var(--mk-space-3);flex-shrink:0;padding:var(--mk-space-4) var(--mk-space-5);border-bottom:var(--mk-border-width) solid var(--mk-border-subtle);background-color:var(--mk-surface)}.mk-dialog__heading{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-dialog__title{margin:0;min-width:0;overflow-wrap:anywhere;font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-dialog__body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:var(--mk-space-5);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-normal);color:var(--mk-text)}.mk-dialog__footer{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;gap:var(--mk-space-2);padding:var(--mk-space-4) var(--mk-space-5);border-top:var(--mk-border-width) solid var(--mk-border-subtle);background-color:var(--mk-surface)}.mk-dialog__footer:empty{display:none}.mk-dialog__footer>*{display:flex;flex:1 1 auto;align-items:center;justify-content:flex-end;gap:var(--mk-space-2);flex-wrap:wrap}.mk-dialog__footer>[data-align=start]{justify-content:flex-start}.mk-dialog__footer>[data-align=center]{justify-content:center}.mk-dialog__footer>[data-align=between]{justify-content:space-between}.mk-dialog__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;margin:-4px -4px 0 0;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:var(--mk-text-muted);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-dialog__close:hover{color:var(--mk-text);background-color:var(--mk-hover-overlay)}.mk-dialog__close:focus-visible{color:var(--mk-text);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
1171
+ }], ctorParameters: () => [], propDecorators: { dialogTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "dialogTitle", required: false }] }], titleId: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleId", required: false }] }], hideClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideClose", required: false }] }] } });
1172
+
1173
+ /**
1174
+ * Internal component backing `MkDialogService.confirm`. Renders a title,
1175
+ * message, and cancel/confirm buttons. Resolves the overlay ref with `true`
1176
+ * on confirm and `false` on cancel. Uses `alertdialog` semantics for the
1177
+ * `danger` tone (set on the panel by the service).
1178
+ */
1179
+ class MkConfirmDialog {
1180
+ ref = inject(MkOverlayRef);
1181
+ data = inject(MK_OVERLAY_DATA);
1182
+ i18n = inject(MK_I18N);
1183
+ confirmTone = computed(() => this.data.tone ?? 'primary', /* @ts-ignore */
1184
+ ...(ngDevMode ? [{ debugName: "confirmTone" }] : /* istanbul ignore next */ []));
1185
+ confirmText = computed(() => this.data.confirmText ?? this.i18n.confirm, /* @ts-ignore */
1186
+ ...(ngDevMode ? [{ debugName: "confirmText" }] : /* istanbul ignore next */ []));
1187
+ cancelText = computed(() => this.data.cancelText ?? this.i18n.cancel, /* @ts-ignore */
1188
+ ...(ngDevMode ? [{ debugName: "cancelText" }] : /* istanbul ignore next */ []));
1189
+ confirm() {
1190
+ this.ref.close(true);
1191
+ }
1192
+ cancel() {
1193
+ this.ref.close(false);
1194
+ }
1195
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkConfirmDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
1196
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkConfirmDialog, isStandalone: true, selector: "mk-confirm-dialog", ngImport: i0, template: "<mk-dialog [dialogTitle]=\"data.title\" hideClose>\n <div class=\"mk-confirm-dialog__body\">\n @if (data.icon; as icon) {\n <mk-icon\n class=\"mk-confirm-dialog__icon\"\n [class.mk-confirm-dialog__icon--danger]=\"data.tone === 'danger'\"\n [name]=\"icon\"\n [size]=\"22\"\n />\n }\n <p class=\"mk-confirm-dialog__message\">{{ data.message }}</p>\n </div>\n\n <div mkDialogFooter>\n @if (!data.hideCancel) {\n <button mkButton variant=\"ghost\" tone=\"neutral\" (click)=\"cancel()\">\n {{ cancelText() }}\n </button>\n }\n <button mkButton [tone]=\"confirmTone()\" (click)=\"confirm()\">\n {{ confirmText() }}\n </button>\n </div>\n</mk-dialog>\n", styles: [".mk-confirm-dialog__message{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-confirm-dialog__body{display:flex;align-items:flex-start;gap:var(--mk-space-3)}.mk-confirm-dialog__icon{flex-shrink:0;margin-top:.125rem;color:var(--mk-text-muted)}.mk-confirm-dialog__icon--danger{color:var(--mk-danger-text)}\n"], dependencies: [{ kind: "component", type: MkDialog, selector: "mk-dialog", inputs: ["dialogTitle", "titleId", "hideClose"] }, { kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }, { kind: "component", type: MkIcon, selector: "mk-icon", inputs: ["name", "size", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1197
+ }
1198
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkConfirmDialog, decorators: [{
1199
+ type: Component,
1200
+ args: [{ selector: 'mk-confirm-dialog', imports: [MkDialog, MkButton, MkIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<mk-dialog [dialogTitle]=\"data.title\" hideClose>\n <div class=\"mk-confirm-dialog__body\">\n @if (data.icon; as icon) {\n <mk-icon\n class=\"mk-confirm-dialog__icon\"\n [class.mk-confirm-dialog__icon--danger]=\"data.tone === 'danger'\"\n [name]=\"icon\"\n [size]=\"22\"\n />\n }\n <p class=\"mk-confirm-dialog__message\">{{ data.message }}</p>\n </div>\n\n <div mkDialogFooter>\n @if (!data.hideCancel) {\n <button mkButton variant=\"ghost\" tone=\"neutral\" (click)=\"cancel()\">\n {{ cancelText() }}\n </button>\n }\n <button mkButton [tone]=\"confirmTone()\" (click)=\"confirm()\">\n {{ confirmText() }}\n </button>\n </div>\n</mk-dialog>\n", styles: [".mk-confirm-dialog__message{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-confirm-dialog__body{display:flex;align-items:flex-start;gap:var(--mk-space-3)}.mk-confirm-dialog__icon{flex-shrink:0;margin-top:.125rem;color:var(--mk-text-muted)}.mk-confirm-dialog__icon--danger{color:var(--mk-danger-text)}\n"] }]
1201
+ }] });
1202
+
1203
+ /**
1204
+ * Internal component backing `MkDialogService.prompt`. Renders a single text
1205
+ * field and resolves the overlay ref with the entered string on confirm, or
1206
+ * `null` on cancel / Escape / backdrop. Submitting the form (Enter) confirms.
1207
+ */
1208
+ class MkPromptDialog {
1209
+ ref = inject(MkOverlayRef);
1210
+ data = inject(MK_OVERLAY_DATA);
1211
+ i18n = inject(MK_I18N);
1212
+ inputRef = viewChild('input', /* @ts-ignore */
1213
+ ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
1214
+ value = signal(this.data.value ?? '', /* @ts-ignore */
1215
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1216
+ confirmText = computed(() => this.data.confirmText ?? this.i18n.ok, /* @ts-ignore */
1217
+ ...(ngDevMode ? [{ debugName: "confirmText" }] : /* istanbul ignore next */ []));
1218
+ cancelText = computed(() => this.data.cancelText ?? this.i18n.cancel, /* @ts-ignore */
1219
+ ...(ngDevMode ? [{ debugName: "cancelText" }] : /* istanbul ignore next */ []));
1220
+ canConfirm = computed(() => !this.data.required || this.value().trim().length > 0, /* @ts-ignore */
1221
+ ...(ngDevMode ? [{ debugName: "canConfirm" }] : /* istanbul ignore next */ []));
1222
+ constructor() {
1223
+ afterNextRender(() => this.inputRef()?.nativeElement.focus());
1224
+ }
1225
+ onInput(event) {
1226
+ this.value.set(event.target.value);
1227
+ }
1228
+ submit() {
1229
+ if (this.canConfirm())
1230
+ this.ref.close(this.value());
1231
+ }
1232
+ cancel() {
1233
+ this.ref.close(null);
1234
+ }
1235
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPromptDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
1236
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkPromptDialog, isStandalone: true, selector: "mk-prompt-dialog", viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: "<mk-dialog [dialogTitle]=\"data.title\" hideClose>\n @if (data.message) {\n <p class=\"mk-confirm-dialog__message\">{{ data.message }}</p>\n }\n\n <mk-form-field [label]=\"data.label ?? ''\">\n <input\n #input\n mkInput\n [attr.type]=\"data.inputType ?? 'text'\"\n [value]=\"value()\"\n [attr.placeholder]=\"data.placeholder ?? null\"\n [attr.required]=\"data.required ? '' : null\"\n (input)=\"onInput($event)\"\n (keydown.enter)=\"submit()\"\n />\n </mk-form-field>\n\n <div mkDialogFooter>\n <button mkButton variant=\"ghost\" tone=\"neutral\" (click)=\"cancel()\">\n {{ cancelText() }}\n </button>\n <button mkButton (click)=\"submit()\" [disabled]=\"!canConfirm()\">\n {{ confirmText() }}\n </button>\n </div>\n</mk-dialog>\n", styles: [".mk-confirm-dialog__message{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-confirm-dialog__body{display:flex;align-items:flex-start;gap:var(--mk-space-3)}.mk-confirm-dialog__icon{flex-shrink:0;margin-top:.125rem;color:var(--mk-text-muted)}.mk-confirm-dialog__icon--danger{color:var(--mk-danger-text)}\n"], dependencies: [{ kind: "component", type: MkDialog, selector: "mk-dialog", inputs: ["dialogTitle", "titleId", "hideClose"] }, { kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }, { kind: "component", type: MkInput, selector: "input[mkInput], textarea[mkInput]", inputs: ["size", "invalid"] }, { kind: "component", type: MkFormField, selector: "mk-form-field", inputs: ["label", "hint", "error", "errorMessages", "errorOn", "required", "disabled", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1237
+ }
1238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPromptDialog, decorators: [{
1239
+ type: Component,
1240
+ args: [{ selector: 'mk-prompt-dialog', imports: [MkDialog, MkButton, MkInput, MkFormField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<mk-dialog [dialogTitle]=\"data.title\" hideClose>\n @if (data.message) {\n <p class=\"mk-confirm-dialog__message\">{{ data.message }}</p>\n }\n\n <mk-form-field [label]=\"data.label ?? ''\">\n <input\n #input\n mkInput\n [attr.type]=\"data.inputType ?? 'text'\"\n [value]=\"value()\"\n [attr.placeholder]=\"data.placeholder ?? null\"\n [attr.required]=\"data.required ? '' : null\"\n (input)=\"onInput($event)\"\n (keydown.enter)=\"submit()\"\n />\n </mk-form-field>\n\n <div mkDialogFooter>\n <button mkButton variant=\"ghost\" tone=\"neutral\" (click)=\"cancel()\">\n {{ cancelText() }}\n </button>\n <button mkButton (click)=\"submit()\" [disabled]=\"!canConfirm()\">\n {{ confirmText() }}\n </button>\n </div>\n</mk-dialog>\n", styles: [".mk-confirm-dialog__message{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-confirm-dialog__body{display:flex;align-items:flex-start;gap:var(--mk-space-3)}.mk-confirm-dialog__icon{flex-shrink:0;margin-top:.125rem;color:var(--mk-text-muted)}.mk-confirm-dialog__icon--danger{color:var(--mk-danger-text)}\n"] }]
1241
+ }], ctorParameters: () => [], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }] } });
1242
+
1243
+ const DIALOG_PANEL_CLASS = 'mk-dialog-panel';
1244
+ /**
1245
+ * Dialog service — a thin, opinionated layer over {@link MkOverlayService} that
1246
+ * renders content on a themed `--mk-surface` panel (shadow, radius, entrance
1247
+ * animation) and adds a `confirm()` convenience.
1248
+ *
1249
+ * ```ts
1250
+ * const ref = dialog.open(EditUserDialog, { data: user });
1251
+ * const saved = await ref.afterClosed;
1252
+ *
1253
+ * if (await dialog.confirm({ title: 'Delete?', message: 'This cannot be undone.', tone: 'danger' })) {
1254
+ * remove();
1255
+ * }
1256
+ * ```
1257
+ */
1258
+ class MkDialogService {
1259
+ overlay = inject(MkOverlayService);
1260
+ i18n = inject(MK_I18N);
1261
+ /** Open `component` inside a themed dialog panel. */
1262
+ open(component, config = {}) {
1263
+ const sizeClass = `mk-dialog-panel--${config.size ?? 'sm'}`;
1264
+ return this.overlay.open(component, {
1265
+ ...config,
1266
+ role: config.role ?? 'dialog',
1267
+ panelClass: this.mergePanelClass(config.panelClass, sizeClass),
1268
+ });
1269
+ }
1270
+ /**
1271
+ * Open a confirmation dialog. Resolves `true` if confirmed, `false` if the
1272
+ * user cancels, presses Escape, or clicks the backdrop.
1273
+ */
1274
+ confirm(data) {
1275
+ const ref = this.open(MkConfirmDialog, {
1276
+ data,
1277
+ role: data.tone === 'danger' ? 'alertdialog' : 'dialog',
1278
+ ariaLabel: data.title,
1279
+ });
1280
+ return ref.afterClosed.then((result) => result === true);
1281
+ }
1282
+ /**
1283
+ * Open a single-button acknowledgement dialog. Resolves when the user
1284
+ * dismisses it (button, Escape, or backdrop).
1285
+ */
1286
+ alert(data) {
1287
+ const ref = this.open(MkConfirmDialog, {
1288
+ data: {
1289
+ ...data,
1290
+ hideCancel: true,
1291
+ confirmText: data.confirmText ?? this.i18n.ok,
1292
+ },
1293
+ role: data.tone === 'danger' ? 'alertdialog' : 'dialog',
1294
+ ariaLabel: data.title,
1295
+ });
1296
+ return ref.afterClosed.then(() => undefined);
1297
+ }
1298
+ /**
1299
+ * Open a single-field prompt dialog. Resolves with the entered string, or
1300
+ * `null` if the user cancels, presses Escape, or clicks the backdrop.
1301
+ */
1302
+ prompt(data) {
1303
+ const ref = this.open(MkPromptDialog, { data, ariaLabel: data.title });
1304
+ return ref.afterClosed.then((result) => result ?? null);
1305
+ }
1306
+ mergePanelClass(panelClass, sizeClass) {
1307
+ const base = [DIALOG_PANEL_CLASS, sizeClass];
1308
+ if (!panelClass)
1309
+ return base;
1310
+ const extra = Array.isArray(panelClass) ? panelClass : [panelClass];
1311
+ return [...base, ...extra];
1312
+ }
1313
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1314
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialogService, providedIn: 'root' });
1315
+ }
1316
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDialogService, decorators: [{
1317
+ type: Injectable,
1318
+ args: [{ providedIn: 'root' }]
1319
+ }] });
1320
+
1321
+ /**
1322
+ * A single toast card. Internal to the toast system — rendered by
1323
+ * {@link MkToastContainer}. Displays a tone accent, optional title, message,
1324
+ * optional action button, and (when dismissible) a close button. Auto-dismiss
1325
+ * pauses while the toast is hovered or focused.
1326
+ */
1327
+ class MkToast {
1328
+ service = inject(MkToastService);
1329
+ i18n = inject(MK_I18N);
1330
+ /** The toast model to render. */
1331
+ item = input.required(/* @ts-ignore */
1332
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
1333
+ /** Assertive alert for danger, polite status otherwise. */
1334
+ role = computed(() => this.item().tone === 'danger' ? 'alert' : 'status', /* @ts-ignore */
1335
+ ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
1336
+ dismiss() {
1337
+ this.service.dismiss(this.item().id);
1338
+ }
1339
+ runAction() {
1340
+ this.service.runAction(this.item());
1341
+ }
1342
+ pause() {
1343
+ this.service.pause(this.item().id);
1344
+ }
1345
+ resume() {
1346
+ this.service.resume(this.item().id);
1347
+ }
1348
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToast, deps: [], target: i0.ɵɵFactoryTarget.Component });
1349
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkToast, isStandalone: true, selector: "mk-toast", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "mouseenter": "pause()", "mouseleave": "resume()", "focusin": "pause()", "focusout": "resume()" }, properties: { "attr.data-tone": "item().tone", "attr.role": "role()" }, classAttribute: "mk-toast" }, ngImport: i0, template: "<div class=\"mk-toast__body\">\n @if (item().title; as title) {\n <p class=\"mk-toast__title\">{{ title }}</p>\n }\n <p class=\"mk-toast__message\">{{ item().message }}</p>\n\n @if (item().action; as action) {\n <div class=\"mk-toast__actions\">\n <button mkButton variant=\"link\" size=\"sm\" (click)=\"runAction()\">\n {{ action.label }}\n </button>\n </div>\n }\n</div>\n\n@if (item().dismissible) {\n <button\n type=\"button\"\n class=\"mk-toast__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"dismiss()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n}\n", styles: ["@charset \"UTF-8\";:host{--_accent: var(--mk-info);display:flex;align-items:flex-start;gap:var(--mk-space-3);width:22rem;max-width:calc(100vw - var(--mk-space-8));padding:var(--mk-space-3) var(--mk-space-4);background-color:var(--mk-surface);color:var(--mk-text);border:var(--mk-border-width) solid var(--mk-border-subtle);border-inline-start:3px solid var(--_accent);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);pointer-events:auto;animation:mk-toast-in var(--mk-duration-normal) var(--mk-ease-emphasized)}@media(prefers-reduced-motion:reduce){:host{animation:none}}:host([data-tone=info]){--_accent: var(--mk-info)}:host([data-tone=success]){--_accent: var(--mk-success)}:host([data-tone=warning]){--_accent: var(--mk-warning)}:host([data-tone=danger]){--_accent: var(--mk-danger)}:host([data-tone=neutral]){--_accent: var(--mk-border-strong)}.mk-toast__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-toast__title{margin:0;font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-toast__message{margin:0;font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);color:var(--mk-text-muted)}.mk-toast__actions{margin-top:var(--mk-space-1)}.mk-toast__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin:-2px calc(var(--mk-space-1) * -1) 0 0;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:var(--mk-text-subtle);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-toast__close:hover{color:var(--mk-text);background-color:var(--mk-hover-overlay)}.mk-toast__close:focus-visible{color:var(--mk-text);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], dependencies: [{ kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1350
+ }
1351
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToast, decorators: [{
1352
+ type: Component,
1353
+ args: [{ selector: 'mk-toast', imports: [MkButton], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1354
+ class: 'mk-toast',
1355
+ '[attr.data-tone]': 'item().tone',
1356
+ '[attr.role]': 'role()',
1357
+ '(mouseenter)': 'pause()',
1358
+ '(mouseleave)': 'resume()',
1359
+ '(focusin)': 'pause()',
1360
+ '(focusout)': 'resume()',
1361
+ }, template: "<div class=\"mk-toast__body\">\n @if (item().title; as title) {\n <p class=\"mk-toast__title\">{{ title }}</p>\n }\n <p class=\"mk-toast__message\">{{ item().message }}</p>\n\n @if (item().action; as action) {\n <div class=\"mk-toast__actions\">\n <button mkButton variant=\"link\" size=\"sm\" (click)=\"runAction()\">\n {{ action.label }}\n </button>\n </div>\n }\n</div>\n\n@if (item().dismissible) {\n <button\n type=\"button\"\n class=\"mk-toast__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"dismiss()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n}\n", styles: ["@charset \"UTF-8\";:host{--_accent: var(--mk-info);display:flex;align-items:flex-start;gap:var(--mk-space-3);width:22rem;max-width:calc(100vw - var(--mk-space-8));padding:var(--mk-space-3) var(--mk-space-4);background-color:var(--mk-surface);color:var(--mk-text);border:var(--mk-border-width) solid var(--mk-border-subtle);border-inline-start:3px solid var(--_accent);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);pointer-events:auto;animation:mk-toast-in var(--mk-duration-normal) var(--mk-ease-emphasized)}@media(prefers-reduced-motion:reduce){:host{animation:none}}:host([data-tone=info]){--_accent: var(--mk-info)}:host([data-tone=success]){--_accent: var(--mk-success)}:host([data-tone=warning]){--_accent: var(--mk-warning)}:host([data-tone=danger]){--_accent: var(--mk-danger)}:host([data-tone=neutral]){--_accent: var(--mk-border-strong)}.mk-toast__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-toast__title{margin:0;font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-toast__message{margin:0;font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);color:var(--mk-text-muted)}.mk-toast__actions{margin-top:var(--mk-space-1)}.mk-toast__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;margin:-2px calc(var(--mk-space-1) * -1) 0 0;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:var(--mk-text-subtle);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-toast__close:hover{color:var(--mk-text);background-color:var(--mk-hover-overlay)}.mk-toast__close:focus-visible{color:var(--mk-text);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
1362
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }] } });
1363
+
1364
+ /**
1365
+ * Fixed, bottom-right stack that renders the active toasts from
1366
+ * {@link MkToastService}. Mounted once, lazily, into `document.body` by the
1367
+ * service — not intended for direct template use.
1368
+ */
1369
+ class MkToastContainer {
1370
+ service = inject(MkToastService);
1371
+ /** Live list of toasts to render. */
1372
+ toasts = this.service.toasts;
1373
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToastContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
1374
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkToastContainer, isStandalone: true, selector: "mk-toast-container", host: { attributes: { "aria-label": "Notifications" }, classAttribute: "mk-toast-container" }, ngImport: i0, template: "@for (toast of toasts(); track toast.id) {\n <mk-toast [item]=\"toast\" />\n}\n", styles: [":host{position:fixed;bottom:calc(var(--mk-space-4) + env(safe-area-inset-bottom,0px));inset-inline-end:var(--mk-space-4);z-index:var(--mk-z-toast);display:flex;flex-direction:column;gap:var(--mk-space-2);align-items:flex-end;max-height:calc(100vh - var(--mk-space-8));max-height:calc(100dvh - var(--mk-space-8));overflow:visible;pointer-events:none}\n"], dependencies: [{ kind: "component", type: MkToast, selector: "mk-toast", inputs: ["item"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1375
+ }
1376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToastContainer, decorators: [{
1377
+ type: Component,
1378
+ args: [{ selector: 'mk-toast-container', imports: [MkToast], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1379
+ class: 'mk-toast-container',
1380
+ 'aria-label': 'Notifications',
1381
+ }, template: "@for (toast of toasts(); track toast.id) {\n <mk-toast [item]=\"toast\" />\n}\n", styles: [":host{position:fixed;bottom:calc(var(--mk-space-4) + env(safe-area-inset-bottom,0px));inset-inline-end:var(--mk-space-4);z-index:var(--mk-z-toast);display:flex;flex-direction:column;gap:var(--mk-space-2);align-items:flex-end;max-height:calc(100vh - var(--mk-space-8));max-height:calc(100dvh - var(--mk-space-8));overflow:visible;pointer-events:none}\n"] }]
1382
+ }] });
1383
+
1384
+ const DEFAULT_DURATION$1 = 5000;
1385
+ let nextId$1 = 0;
1386
+ /**
1387
+ * Toast service — enqueues transient notifications rendered by a single,
1388
+ * lazily-mounted {@link MkToastContainer} at the bottom-right of the viewport.
1389
+ * Each rendered toast is itself a live region (`role="alert"` for `danger`,
1390
+ * else `role="status"`), so assistive tech announces it exactly once. Toasts
1391
+ * auto-dismiss after `duration` ms unless it is `0`.
1392
+ *
1393
+ * ```ts
1394
+ * toasts.success('Saved');
1395
+ * toasts.show({ title: 'Upload failed', message: 'Retry?', tone: 'danger',
1396
+ * duration: 0, action: { label: 'Retry', handler: () => retry() } });
1397
+ * ```
1398
+ */
1399
+ class MkToastService {
1400
+ appRef = inject(ApplicationRef);
1401
+ envInjector = inject(EnvironmentInjector);
1402
+ document = inject(DOCUMENT);
1403
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1404
+ _toasts = signal([], /* @ts-ignore */
1405
+ ...(ngDevMode ? [{ debugName: "_toasts" }] : /* istanbul ignore next */ []));
1406
+ /** Reactive list of currently visible toasts. */
1407
+ toasts = this._toasts.asReadonly();
1408
+ timers = new Map();
1409
+ containerRef;
1410
+ /** Show a toast, returning its id (usable with {@link dismiss}). */
1411
+ show(config) {
1412
+ const item = {
1413
+ id: nextId$1++,
1414
+ title: config.title,
1415
+ message: config.message,
1416
+ tone: config.tone ?? 'info',
1417
+ duration: config.duration ?? DEFAULT_DURATION$1,
1418
+ dismissible: config.dismissible ?? true,
1419
+ action: config.action,
1420
+ };
1421
+ if (!this.isBrowser)
1422
+ return item.id;
1423
+ this.ensureContainer();
1424
+ // The toast element itself carries role="alert"/"status" — no separate
1425
+ // live-announcer call, or screen readers would read everything twice.
1426
+ this._toasts.update((list) => [...list, item]);
1427
+ if (item.duration > 0)
1428
+ this.startTimer(item.id, item.duration);
1429
+ return item.id;
1430
+ }
1431
+ /** Show an `info` toast. */
1432
+ info(message, config = {}) {
1433
+ return this.show({ ...config, message, tone: 'info' });
1434
+ }
1435
+ /** Show a `success` toast. */
1436
+ success(message, config = {}) {
1437
+ return this.show({ ...config, message, tone: 'success' });
1438
+ }
1439
+ /** Show a `warning` toast. */
1440
+ warning(message, config = {}) {
1441
+ return this.show({ ...config, message, tone: 'warning' });
1442
+ }
1443
+ /** Show a `danger` toast. */
1444
+ danger(message, config = {}) {
1445
+ return this.show({ ...config, message, tone: 'danger' });
1446
+ }
1447
+ /** Show a `neutral` toast. */
1448
+ neutral(message, config = {}) {
1449
+ return this.show({ ...config, message, tone: 'neutral' });
1450
+ }
1451
+ /** Dismiss a toast by id. */
1452
+ dismiss(id) {
1453
+ this.clearTimer(id);
1454
+ this._toasts.update((list) => list.filter((t) => t.id !== id));
1455
+ }
1456
+ /** Dismiss every visible toast. */
1457
+ clear() {
1458
+ this.timers.forEach((t) => clearTimeout(t.handle));
1459
+ this.timers.clear();
1460
+ this._toasts.set([]);
1461
+ }
1462
+ /** Run a toast action, then dismiss it. */
1463
+ runAction(item) {
1464
+ item.action?.handler?.();
1465
+ this.dismiss(item.id);
1466
+ }
1467
+ /** Pause a toast's auto-dismiss countdown (e.g. on hover/focus). */
1468
+ pause(id) {
1469
+ const timer = this.timers.get(id);
1470
+ if (!timer)
1471
+ return;
1472
+ clearTimeout(timer.handle);
1473
+ timer.remaining -= Date.now() - timer.startedAt;
1474
+ }
1475
+ /** Resume a paused countdown. */
1476
+ resume(id) {
1477
+ const timer = this.timers.get(id);
1478
+ if (!timer)
1479
+ return;
1480
+ this.startTimer(id, Math.max(timer.remaining, 0));
1481
+ }
1482
+ startTimer(id, duration) {
1483
+ this.timers.set(id, {
1484
+ handle: setTimeout(() => this.dismiss(id), duration),
1485
+ remaining: duration,
1486
+ startedAt: Date.now(),
1487
+ });
1488
+ }
1489
+ clearTimer(id) {
1490
+ const timer = this.timers.get(id);
1491
+ if (timer)
1492
+ clearTimeout(timer.handle);
1493
+ this.timers.delete(id);
1494
+ }
1495
+ ensureContainer() {
1496
+ if (this.containerRef)
1497
+ return;
1498
+ const ref = createComponent(MkToastContainer, {
1499
+ environmentInjector: this.envInjector,
1500
+ });
1501
+ this.appRef.attachView(ref.hostView);
1502
+ this.document.body.appendChild(ref.location.nativeElement);
1503
+ this.containerRef = ref;
1504
+ }
1505
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1506
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToastService, providedIn: 'root' });
1507
+ }
1508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkToastService, decorators: [{
1509
+ type: Injectable,
1510
+ args: [{ providedIn: 'root' }]
1511
+ }] });
1512
+
1513
+ /**
1514
+ * The visual snackbar bar. Internal to the snackbar system — rendered by
1515
+ * {@link MkSnackbarContainer}. Shows a message, an optional action button and
1516
+ * (when `dismissible`) a close button. Auto-dismiss pauses on hover/focus.
1517
+ */
1518
+ class MkSnackbar {
1519
+ service = inject(MkSnackbarService);
1520
+ i18n = inject(MK_I18N);
1521
+ /** The snackbar model to render. */
1522
+ item = input.required(/* @ts-ignore */
1523
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
1524
+ /** Assertive alert for danger/warning, polite status otherwise. */
1525
+ role = computed(() => this.item().tone === 'danger' || this.item().tone === 'warning'
1526
+ ? 'alert'
1527
+ : 'status', /* @ts-ignore */
1528
+ ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
1529
+ runAction() {
1530
+ this.service.runAction(this.item().id);
1531
+ }
1532
+ dismiss() {
1533
+ this.service.dismiss(this.item().id);
1534
+ }
1535
+ pause() {
1536
+ this.service.pause();
1537
+ }
1538
+ resume() {
1539
+ this.service.resume();
1540
+ }
1541
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
1542
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkSnackbar, isStandalone: true, selector: "mk-snackbar", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "mouseenter": "pause()", "mouseleave": "resume()", "focusin": "pause()", "focusout": "resume()" }, properties: { "attr.data-tone": "item().tone", "attr.role": "role()" }, classAttribute: "mk-snackbar" }, ngImport: i0, template: "<span class=\"mk-snackbar__message\">{{ item().message }}</span>\n\n@if (item().actionLabel) {\n <button type=\"button\" class=\"mk-snackbar__action\" (click)=\"runAction()\">\n {{ item().actionLabel }}\n </button>\n}\n\n@if (item().dismissible) {\n <button\n type=\"button\"\n class=\"mk-snackbar__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"dismiss()\"\n ></button>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;align-items:center;gap:var(--mk-space-3);min-width:18rem;max-width:min(36rem,100vw - 2 * var(--mk-space-4));padding:var(--mk-space-3) var(--mk-space-4);background-color:var(--mk-surface-inverse);color:var(--mk-text-inverse);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);pointer-events:auto;animation:mk-snackbar-in var(--mk-duration-normal) var(--mk-ease-emphasized)}:host([data-tone=info]){border-inline-start:3px solid var(--mk-info)}:host([data-tone=success]){border-inline-start:3px solid var(--mk-success)}:host([data-tone=warning]){border-inline-start:3px solid var(--mk-warning)}:host([data-tone=danger]){border-inline-start:3px solid var(--mk-danger)}.mk-snackbar__message{flex:1 1 auto;min-width:0;line-height:var(--mk-line-height-snug)}.mk-snackbar__action{flex:none;padding:var(--mk-space-1) var(--mk-space-2);margin:calc(-1 * var(--mk-space-1)) 0;background:transparent;border:none;border-radius:var(--mk-radius-sm);color:color-mix(in srgb,var(--mk-primary) 65%,white);font-family:inherit;font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-semibold);text-transform:uppercase;letter-spacing:.02em;cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-snackbar__action:hover{background-color:color-mix(in srgb,transparent 88%,white)}.mk-snackbar__action:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-snackbar__close{position:relative;flex:none;width:1.5rem;height:1.5rem;padding:0;background:transparent;border:none;border-radius:var(--mk-radius-circle);color:inherit;opacity:.7;cursor:pointer;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-snackbar__close:hover{opacity:1;background-color:color-mix(in srgb,transparent 85%,white)}.mk-snackbar__close:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-snackbar__close:before,.mk-snackbar__close:after{content:\"\";position:absolute;top:50%;left:50%;width:.75rem;height:2px;background-color:currentColor;border-radius:var(--mk-radius-pill)}.mk-snackbar__close:before{transform:translate(-50%,-50%) rotate(45deg)}.mk-snackbar__close:after{transform:translate(-50%,-50%) rotate(-45deg)}@keyframes mk-snackbar-in{0%{opacity:0;transform:translateY(.75rem)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){:host{animation:none}.mk-snackbar__action,.mk-snackbar__close{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1543
+ }
1544
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbar, decorators: [{
1545
+ type: Component,
1546
+ args: [{ selector: 'mk-snackbar', changeDetection: ChangeDetectionStrategy.OnPush, host: {
1547
+ class: 'mk-snackbar',
1548
+ '[attr.data-tone]': 'item().tone',
1549
+ '[attr.role]': 'role()',
1550
+ '(mouseenter)': 'pause()',
1551
+ '(mouseleave)': 'resume()',
1552
+ '(focusin)': 'pause()',
1553
+ '(focusout)': 'resume()',
1554
+ }, template: "<span class=\"mk-snackbar__message\">{{ item().message }}</span>\n\n@if (item().actionLabel) {\n <button type=\"button\" class=\"mk-snackbar__action\" (click)=\"runAction()\">\n {{ item().actionLabel }}\n </button>\n}\n\n@if (item().dismissible) {\n <button\n type=\"button\"\n class=\"mk-snackbar__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"dismiss()\"\n ></button>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;align-items:center;gap:var(--mk-space-3);min-width:18rem;max-width:min(36rem,100vw - 2 * var(--mk-space-4));padding:var(--mk-space-3) var(--mk-space-4);background-color:var(--mk-surface-inverse);color:var(--mk-text-inverse);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);pointer-events:auto;animation:mk-snackbar-in var(--mk-duration-normal) var(--mk-ease-emphasized)}:host([data-tone=info]){border-inline-start:3px solid var(--mk-info)}:host([data-tone=success]){border-inline-start:3px solid var(--mk-success)}:host([data-tone=warning]){border-inline-start:3px solid var(--mk-warning)}:host([data-tone=danger]){border-inline-start:3px solid var(--mk-danger)}.mk-snackbar__message{flex:1 1 auto;min-width:0;line-height:var(--mk-line-height-snug)}.mk-snackbar__action{flex:none;padding:var(--mk-space-1) var(--mk-space-2);margin:calc(-1 * var(--mk-space-1)) 0;background:transparent;border:none;border-radius:var(--mk-radius-sm);color:color-mix(in srgb,var(--mk-primary) 65%,white);font-family:inherit;font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-semibold);text-transform:uppercase;letter-spacing:.02em;cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-snackbar__action:hover{background-color:color-mix(in srgb,transparent 88%,white)}.mk-snackbar__action:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-snackbar__close{position:relative;flex:none;width:1.5rem;height:1.5rem;padding:0;background:transparent;border:none;border-radius:var(--mk-radius-circle);color:inherit;opacity:.7;cursor:pointer;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-snackbar__close:hover{opacity:1;background-color:color-mix(in srgb,transparent 85%,white)}.mk-snackbar__close:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-snackbar__close:before,.mk-snackbar__close:after{content:\"\";position:absolute;top:50%;left:50%;width:.75rem;height:2px;background-color:currentColor;border-radius:var(--mk-radius-pill)}.mk-snackbar__close:before{transform:translate(-50%,-50%) rotate(45deg)}.mk-snackbar__close:after{transform:translate(-50%,-50%) rotate(-45deg)}@keyframes mk-snackbar-in{0%{opacity:0;transform:translateY(.75rem)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){:host{animation:none}.mk-snackbar__action,.mk-snackbar__close{transition:none}}\n"] }]
1555
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }] } });
1556
+
1557
+ /**
1558
+ * Fixed, bottom-centre host that renders the single active snackbar from
1559
+ * {@link MkSnackbarService}. Mounted once, lazily, into `document.body` by the
1560
+ * service — not intended for direct template use.
1561
+ */
1562
+ class MkSnackbarContainer {
1563
+ service = inject(MkSnackbarService);
1564
+ /** The active snackbar to render, or `null`. */
1565
+ active = this.service.active;
1566
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbarContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
1567
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkSnackbarContainer, isStandalone: true, selector: "mk-snackbar-container", host: { classAttribute: "mk-snackbar-container" }, ngImport: i0, template: "@if (active(); as item) {\n <mk-snackbar [item]=\"item\" />\n}\n", styles: [":host{position:fixed;bottom:calc(var(--mk-space-5) + env(safe-area-inset-bottom,0px));left:50%;transform:translate(-50%);z-index:var(--mk-z-toast);display:flex;justify-content:center;pointer-events:none}\n"], dependencies: [{ kind: "component", type: MkSnackbar, selector: "mk-snackbar", inputs: ["item"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1568
+ }
1569
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbarContainer, decorators: [{
1570
+ type: Component,
1571
+ args: [{ selector: 'mk-snackbar-container', imports: [MkSnackbar], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1572
+ class: 'mk-snackbar-container',
1573
+ }, template: "@if (active(); as item) {\n <mk-snackbar [item]=\"item\" />\n}\n", styles: [":host{position:fixed;bottom:calc(var(--mk-space-5) + env(safe-area-inset-bottom,0px));left:50%;transform:translate(-50%);z-index:var(--mk-z-toast);display:flex;justify-content:center;pointer-events:none}\n"] }]
1574
+ }] });
1575
+
1576
+ const DEFAULT_DURATION = 5000;
1577
+ let nextId = 0;
1578
+ /**
1579
+ * Handle to an opened snackbar. Await {@link onAction} to react to the action
1580
+ * button, or {@link afterDismissed} to run code once it closes.
1581
+ */
1582
+ class MkSnackbarRef {
1583
+ id;
1584
+ service;
1585
+ actionResolve;
1586
+ dismissResolve;
1587
+ actionPromise = new Promise((r) => {
1588
+ this.actionResolve = r;
1589
+ });
1590
+ dismissPromise = new Promise((r) => {
1591
+ this.dismissResolve = r;
1592
+ });
1593
+ constructor(
1594
+ /** Unique id of the snackbar this ref controls. */
1595
+ id, service) {
1596
+ this.id = id;
1597
+ this.service = service;
1598
+ }
1599
+ /** Dismiss the snackbar programmatically. */
1600
+ dismiss() {
1601
+ this.service.dismiss(this.id);
1602
+ }
1603
+ /** Resolves when the action button is activated. */
1604
+ onAction() {
1605
+ return this.actionPromise;
1606
+ }
1607
+ /** Resolves when the snackbar has closed, with the reason it closed. */
1608
+ afterDismissed() {
1609
+ return this.dismissPromise;
1610
+ }
1611
+ /** @internal */
1612
+ _notifyAction() {
1613
+ this.actionResolve();
1614
+ }
1615
+ /** @internal */
1616
+ _notifyDismissed(reason) {
1617
+ this.dismissResolve(reason);
1618
+ }
1619
+ }
1620
+ /**
1621
+ * Snackbar service — shows a single, brief message at the bottom-centre of the
1622
+ * viewport with an optional action button, following the Material snackbar
1623
+ * model: only one is visible at a time, and opening a new one replaces the
1624
+ * current. For stacked, persistent notifications use `MkToastService` instead.
1625
+ *
1626
+ * ```ts
1627
+ * const ref = snackbar.open('Message archived', 'Undo');
1628
+ * ref.onAction().then(() => restore());
1629
+ *
1630
+ * snackbar.open('Changes saved', undefined, { tone: 'success' });
1631
+ * ```
1632
+ */
1633
+ class MkSnackbarService {
1634
+ appRef = inject(ApplicationRef);
1635
+ envInjector = inject(EnvironmentInjector);
1636
+ document = inject(DOCUMENT);
1637
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1638
+ _active = signal(null, /* @ts-ignore */
1639
+ ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
1640
+ /** The currently visible snackbar, or `null`. */
1641
+ active = this._active.asReadonly();
1642
+ refs = new Map();
1643
+ timer;
1644
+ remaining = 0;
1645
+ startedAt = 0;
1646
+ containerRef;
1647
+ /**
1648
+ * Open a snackbar. `action` is the (optional) button label — react to it via
1649
+ * the returned ref's {@link MkSnackbarRef.onAction}.
1650
+ */
1651
+ open(message, action, config = {}) {
1652
+ const id = nextId++;
1653
+ const ref = new MkSnackbarRef(id, this);
1654
+ if (!this.isBrowser)
1655
+ return ref;
1656
+ // Replace any currently-open snackbar (Material single-slot behaviour).
1657
+ const current = this._active();
1658
+ if (current)
1659
+ this.dismiss(current.id);
1660
+ const item = {
1661
+ id,
1662
+ message,
1663
+ actionLabel: action,
1664
+ tone: config.tone ?? 'neutral',
1665
+ dismissible: config.dismissible ?? false,
1666
+ };
1667
+ this.ensureContainer();
1668
+ this.refs.set(id, ref);
1669
+ // The snackbar element itself carries role="alert"/"status" — no separate
1670
+ // live-announcer call, or screen readers would read everything twice.
1671
+ this._active.set(item);
1672
+ const duration = config.duration ?? DEFAULT_DURATION;
1673
+ if (duration > 0)
1674
+ this.startTimer(id, duration);
1675
+ return ref;
1676
+ }
1677
+ /** Dismiss the snackbar with the given id (if it is the active one). */
1678
+ dismiss(id) {
1679
+ this.close(id, 'dismiss');
1680
+ }
1681
+ /** Run the action for the active snackbar, then close it. */
1682
+ runAction(id) {
1683
+ this.refs.get(id)?._notifyAction();
1684
+ this.close(id, 'action');
1685
+ }
1686
+ /** Pause the auto-dismiss countdown (e.g. on hover/focus). */
1687
+ pause() {
1688
+ if (!this.timer)
1689
+ return;
1690
+ clearTimeout(this.timer);
1691
+ this.timer = undefined;
1692
+ this.remaining -= Date.now() - this.startedAt;
1693
+ }
1694
+ /** Resume a paused countdown. */
1695
+ resume() {
1696
+ const active = this._active();
1697
+ if (!active || this.timer || this.remaining <= 0)
1698
+ return;
1699
+ this.startTimer(active.id, this.remaining);
1700
+ }
1701
+ close(id, reason) {
1702
+ if (this._active()?.id !== id)
1703
+ return;
1704
+ this.clearTimer();
1705
+ this._active.set(null);
1706
+ const ref = this.refs.get(id);
1707
+ this.refs.delete(id);
1708
+ ref?._notifyDismissed(reason);
1709
+ }
1710
+ startTimer(id, duration) {
1711
+ this.remaining = duration;
1712
+ this.startedAt = Date.now();
1713
+ this.timer = setTimeout(() => this.close(id, 'timeout'), duration);
1714
+ }
1715
+ clearTimer() {
1716
+ if (this.timer)
1717
+ clearTimeout(this.timer);
1718
+ this.timer = undefined;
1719
+ this.remaining = 0;
1720
+ }
1721
+ ensureContainer() {
1722
+ if (this.containerRef)
1723
+ return;
1724
+ const ref = createComponent(MkSnackbarContainer, {
1725
+ environmentInjector: this.envInjector,
1726
+ });
1727
+ this.appRef.attachView(ref.hostView);
1728
+ this.document.body.appendChild(ref.location.nativeElement);
1729
+ this.containerRef = ref;
1730
+ }
1731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1732
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbarService, providedIn: 'root' });
1733
+ }
1734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkSnackbarService, decorators: [{
1735
+ type: Injectable,
1736
+ args: [{ providedIn: 'root' }]
1737
+ }] });
1738
+
1739
+ /** Distance in px the sheet must be dragged down before it dismisses. */
1740
+ const DISMISS_THRESHOLD = 120;
1741
+ /**
1742
+ * BottomSheet layout — the surface rendered inside an {@link MkOverlayService}
1743
+ * panel by {@link MkBottomSheetService}. Provides a drag handle (swipe down to
1744
+ * dismiss), an optional titled header with a close button, and a scrollable
1745
+ * body for projected content.
1746
+ *
1747
+ * Slots: `[mkBottomSheetHeader]` (extra header content), default content
1748
+ * (body), `[mkBottomSheetFooter]` (sticky footer).
1749
+ *
1750
+ * ```html
1751
+ * <mk-bottom-sheet sheetTitle="Share">
1752
+ * <button mkButton fullWidth variant="ghost">Copy link</button>
1753
+ * <button mkButton fullWidth variant="ghost">Email</button>
1754
+ * </mk-bottom-sheet>
1755
+ * ```
1756
+ */
1757
+ class MkBottomSheet {
1758
+ host = inject(ElementRef);
1759
+ /** Localised strings (override globally via `provideMkI18n`). */
1760
+ i18n = inject(MK_I18N);
1761
+ overlayRef = inject(MkOverlayRef, { optional: true });
1762
+ /** Optional plain-text title rendered in the header. */
1763
+ sheetTitle = input(/* @ts-ignore */
1764
+ ...(ngDevMode ? [undefined, { debugName: "sheetTitle" }] : /* istanbul ignore next */ []));
1765
+ /** Stable id for the built-in title element. */
1766
+ titleId = input(mkUniqueId('mk-bottom-sheet-title'), /* @ts-ignore */
1767
+ ...(ngDevMode ? [{ debugName: "titleId" }] : /* istanbul ignore next */ []));
1768
+ /** Hide the drag handle (also disables swipe-to-dismiss). */
1769
+ hideHandle = input(false, { ...(ngDevMode ? { debugName: "hideHandle" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1770
+ /** Hide the header close button. */
1771
+ hideClose = input(false, { ...(ngDevMode ? { debugName: "hideClose" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1772
+ panel = null;
1773
+ dragging = false;
1774
+ startY = 0;
1775
+ constructor() {
1776
+ afterNextRender(() => this.wireLabelledBy());
1777
+ }
1778
+ /** Close the surrounding overlay, if any. */
1779
+ close() {
1780
+ this.overlayRef?.close();
1781
+ }
1782
+ onHandlePointerDown(event) {
1783
+ this.panel ??= this.host.nativeElement.closest('.mk-bottom-sheet-panel');
1784
+ if (!this.panel)
1785
+ return;
1786
+ this.dragging = true;
1787
+ this.startY = event.clientY;
1788
+ event.target.setPointerCapture?.(event.pointerId);
1789
+ this.panel.style.transition = 'none';
1790
+ }
1791
+ onHandlePointerMove(event) {
1792
+ if (!this.dragging || !this.panel)
1793
+ return;
1794
+ const dy = Math.max(0, event.clientY - this.startY);
1795
+ this.panel.style.transform = `translateY(${dy}px)`;
1796
+ }
1797
+ onHandlePointerUp(event) {
1798
+ if (!this.dragging || !this.panel)
1799
+ return;
1800
+ this.dragging = false;
1801
+ const dy = Math.max(0, event.clientY - this.startY);
1802
+ this.panel.style.transition = '';
1803
+ if (dy > DISMISS_THRESHOLD) {
1804
+ this.overlayRef?.close();
1805
+ }
1806
+ else {
1807
+ this.panel.style.transform = '';
1808
+ }
1809
+ }
1810
+ /** Point the owning panel's `aria-labelledby` at the title element. */
1811
+ wireLabelledBy() {
1812
+ const el = this.host.nativeElement;
1813
+ const panel = el.closest('[role="dialog"], [role="alertdialog"]');
1814
+ const titleEl = el.querySelector('.mk-bottom-sheet__title');
1815
+ if (!panel || !titleEl)
1816
+ return;
1817
+ if (!titleEl.id)
1818
+ titleEl.id = this.titleId();
1819
+ panel.setAttribute('aria-labelledby', titleEl.id);
1820
+ }
1821
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBottomSheet, deps: [], target: i0.ɵɵFactoryTarget.Component });
1822
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBottomSheet, isStandalone: true, selector: "mk-bottom-sheet", inputs: { sheetTitle: { classPropertyName: "sheetTitle", publicName: "sheetTitle", isSignal: true, isRequired: false, transformFunction: null }, titleId: { classPropertyName: "titleId", publicName: "titleId", isSignal: true, isRequired: false, transformFunction: null }, hideHandle: { classPropertyName: "hideHandle", publicName: "hideHandle", isSignal: true, isRequired: false, transformFunction: null }, hideClose: { classPropertyName: "hideClose", publicName: "hideClose", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mk-bottom-sheet" }, ngImport: i0, template: "@if (!hideHandle()) {\n <div\n class=\"mk-bottom-sheet__handle-area\"\n (pointerdown)=\"onHandlePointerDown($event)\"\n (pointermove)=\"onHandlePointerMove($event)\"\n (pointerup)=\"onHandlePointerUp($event)\"\n (pointercancel)=\"onHandlePointerUp($event)\"\n >\n <span class=\"mk-bottom-sheet__handle\" aria-hidden=\"true\"></span>\n </div>\n}\n\n@if (sheetTitle() || !hideClose()) {\n <header class=\"mk-bottom-sheet__header\">\n @if (sheetTitle(); as heading) {\n <h2 class=\"mk-bottom-sheet__title\" [id]=\"titleId()\">{{ heading }}</h2>\n }\n <ng-content select=\"[mkBottomSheetHeader]\" />\n @if (!hideClose()) {\n <button\n type=\"button\"\n class=\"mk-bottom-sheet__close\"\n [attr.aria-label]=\"i18n.close\"\n (click)=\"close()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n </header>\n}\n\n<div class=\"mk-bottom-sheet__body\">\n <ng-content />\n</div>\n\n<footer class=\"mk-bottom-sheet__footer\">\n <ng-content select=\"[mkBottomSheetFooter]\" />\n</footer>\n", styles: ["@charset \"UTF-8\";.mk-bottom-sheet{display:flex;flex-direction:column;min-height:0;max-height:inherit;flex:1 1 auto;font-family:var(--mk-font-sans)}.mk-bottom-sheet__handle-area{flex-shrink:0;display:flex;align-items:center;justify-content:center;padding:var(--mk-space-2) 0;cursor:grab;touch-action:none}.mk-bottom-sheet__handle-area:active{cursor:grabbing}.mk-bottom-sheet__handle{width:2.25rem;height:.25rem;border-radius:var(--mk-radius-pill);background-color:var(--mk-border-strong)}.mk-bottom-sheet__header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--mk-space-3);flex-shrink:0;padding:var(--mk-space-2) var(--mk-space-5) var(--mk-space-3);background-color:var(--mk-surface)}.mk-bottom-sheet__title{flex:1;min-width:0;margin:0;font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-bottom-sheet__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:var(--mk-text-muted);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-bottom-sheet__close:hover{color:var(--mk-text);background-color:var(--mk-hover-overlay)}.mk-bottom-sheet__close:focus-visible{color:var(--mk-text);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-bottom-sheet__body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:var(--mk-space-2) var(--mk-space-5) var(--mk-space-5);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-normal);color:var(--mk-text)}.mk-bottom-sheet__footer{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;gap:var(--mk-space-2);padding:var(--mk-space-4) var(--mk-space-5);border-top:var(--mk-border-width) solid var(--mk-border-subtle);background-color:var(--mk-surface)}.mk-bottom-sheet__footer:empty{display:none}@media(prefers-reduced-motion:reduce){.mk-bottom-sheet__close{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1823
+ }
1824
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBottomSheet, decorators: [{
1825
+ type: Component,
1826
+ args: [{ selector: 'mk-bottom-sheet', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'mk-bottom-sheet' }, template: "@if (!hideHandle()) {\n <div\n class=\"mk-bottom-sheet__handle-area\"\n (pointerdown)=\"onHandlePointerDown($event)\"\n (pointermove)=\"onHandlePointerMove($event)\"\n (pointerup)=\"onHandlePointerUp($event)\"\n (pointercancel)=\"onHandlePointerUp($event)\"\n >\n <span class=\"mk-bottom-sheet__handle\" aria-hidden=\"true\"></span>\n </div>\n}\n\n@if (sheetTitle() || !hideClose()) {\n <header class=\"mk-bottom-sheet__header\">\n @if (sheetTitle(); as heading) {\n <h2 class=\"mk-bottom-sheet__title\" [id]=\"titleId()\">{{ heading }}</h2>\n }\n <ng-content select=\"[mkBottomSheetHeader]\" />\n @if (!hideClose()) {\n <button\n type=\"button\"\n class=\"mk-bottom-sheet__close\"\n [attr.aria-label]=\"i18n.close\"\n (click)=\"close()\"\n >\n <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\" focusable=\"false\">\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n </header>\n}\n\n<div class=\"mk-bottom-sheet__body\">\n <ng-content />\n</div>\n\n<footer class=\"mk-bottom-sheet__footer\">\n <ng-content select=\"[mkBottomSheetFooter]\" />\n</footer>\n", styles: ["@charset \"UTF-8\";.mk-bottom-sheet{display:flex;flex-direction:column;min-height:0;max-height:inherit;flex:1 1 auto;font-family:var(--mk-font-sans)}.mk-bottom-sheet__handle-area{flex-shrink:0;display:flex;align-items:center;justify-content:center;padding:var(--mk-space-2) 0;cursor:grab;touch-action:none}.mk-bottom-sheet__handle-area:active{cursor:grabbing}.mk-bottom-sheet__handle{width:2.25rem;height:.25rem;border-radius:var(--mk-radius-pill);background-color:var(--mk-border-strong)}.mk-bottom-sheet__header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--mk-space-3);flex-shrink:0;padding:var(--mk-space-2) var(--mk-space-5) var(--mk-space-3);background-color:var(--mk-surface)}.mk-bottom-sheet__title{flex:1;min-width:0;margin:0;font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold);line-height:var(--mk-line-height-snug);color:var(--mk-text)}.mk-bottom-sheet__close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;background:transparent;border:0;border-radius:var(--mk-radius-sm);color:var(--mk-text-muted);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-bottom-sheet__close:hover{color:var(--mk-text);background-color:var(--mk-hover-overlay)}.mk-bottom-sheet__close:focus-visible{color:var(--mk-text);outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-bottom-sheet__body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:var(--mk-space-2) var(--mk-space-5) var(--mk-space-5);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-normal);color:var(--mk-text)}.mk-bottom-sheet__footer{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;gap:var(--mk-space-2);padding:var(--mk-space-4) var(--mk-space-5);border-top:var(--mk-border-width) solid var(--mk-border-subtle);background-color:var(--mk-surface)}.mk-bottom-sheet__footer:empty{display:none}@media(prefers-reduced-motion:reduce){.mk-bottom-sheet__close{transition:none}}\n"] }]
1827
+ }], ctorParameters: () => [], propDecorators: { sheetTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "sheetTitle", required: false }] }], titleId: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleId", required: false }] }], hideHandle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideHandle", required: false }] }], hideClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideClose", required: false }] }] } });
1828
+
1829
+ const BOTTOM_SHEET_PANEL_CLASS = 'mk-bottom-sheet-panel';
1830
+ /**
1831
+ * BottomSheet service — a thin layer over {@link MkOverlayService} that renders
1832
+ * a component in a panel anchored to the bottom edge of the viewport: full
1833
+ * width on small screens, a centred card capped at ~42rem on large ones. It
1834
+ * slides up on open and can be swiped down to dismiss (via {@link MkBottomSheet}).
1835
+ *
1836
+ * Use it for contextual actions, pickers and mobile-style menus. For a centred
1837
+ * modal use `MkDialogService` instead.
1838
+ *
1839
+ * ```ts
1840
+ * const ref = bottomSheet.open(ShareSheet, { data: { url } });
1841
+ * const choice = await ref.afterClosed;
1842
+ * ```
1843
+ */
1844
+ class MkBottomSheetService {
1845
+ overlay = inject(MkOverlayService);
1846
+ /** Open `component` inside a bottom-anchored sheet panel. */
1847
+ open(component, config = {}) {
1848
+ return this.overlay.open(component, {
1849
+ ...config,
1850
+ role: config.role ?? 'dialog',
1851
+ panelClass: this.mergePanelClass(config.panelClass),
1852
+ });
1853
+ }
1854
+ mergePanelClass(panelClass) {
1855
+ if (!panelClass)
1856
+ return [BOTTOM_SHEET_PANEL_CLASS];
1857
+ const extra = Array.isArray(panelClass) ? panelClass : [panelClass];
1858
+ return [BOTTOM_SHEET_PANEL_CLASS, ...extra];
1859
+ }
1860
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBottomSheetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1861
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBottomSheetService, providedIn: 'root' });
1862
+ }
1863
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBottomSheetService, decorators: [{
1864
+ type: Injectable,
1865
+ args: [{ providedIn: 'root' }]
1866
+ }] });
1867
+
1868
+ /**
1869
+ * Banner — a persistent, full-width page/section message with a tone, optional
1870
+ * title, an actions slot and optional dismiss. Heavier than an inline
1871
+ * {@link MkAlert} toast; use it for standing notices ("Trial ends in 3 days").
1872
+ *
1873
+ * Project an icon with `[mkBannerIcon]` and buttons with `[mkBannerActions]`.
1874
+ * Danger/warning banners announce assertively (`role="alert"`).
1875
+ *
1876
+ * ```html
1877
+ * <mk-banner tone="warning" title="Storage almost full" dismissible>
1878
+ * You've used 90% of your quota.
1879
+ * <button mkButton size="sm" mkBannerActions>Upgrade</button>
1880
+ * </mk-banner>
1881
+ * ```
1882
+ */
1883
+ class MkBanner {
1884
+ /** Localised strings (override globally via `provideMkI18n`). */
1885
+ i18n = inject(MK_I18N);
1886
+ /** Semantic color tone. */
1887
+ tone = input('info', /* @ts-ignore */
1888
+ ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
1889
+ /** Optional bold title above the message. */
1890
+ title = input('', /* @ts-ignore */
1891
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
1892
+ /** Show a dismiss (×) button. */
1893
+ dismissible = input(false, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1894
+ /** Two-way open state; set false to hide. */
1895
+ open = model(true, /* @ts-ignore */
1896
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1897
+ /** Emitted when the banner is dismissed. */
1898
+ dismissed = output();
1899
+ role = computed(() => {
1900
+ const t = this.tone();
1901
+ return t === 'danger' || t === 'warning' ? 'alert' : 'status';
1902
+ }, /* @ts-ignore */
1903
+ ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));
1904
+ dismiss() {
1905
+ this.open.set(false);
1906
+ this.dismissed.emit();
1907
+ }
1908
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBanner, deps: [], target: i0.ɵɵFactoryTarget.Component });
1909
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBanner, isStandalone: true, selector: "mk-banner", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", dismissed: "dismissed" }, host: { properties: { "attr.data-tone": "tone()" }, classAttribute: "mk-banner" }, ngImport: i0, template: "@if (open()) {\n <div class=\"mk-banner__box\">\n <span class=\"mk-banner__icon\" aria-hidden=\"true\"\n ><ng-content select=\"[mkBannerIcon]\"\n /></span>\n <!-- The live region wraps only the title + message, keeping the actions\n and the dismiss button out of what gets announced. -->\n <div class=\"mk-banner__body\" [attr.role]=\"role()\">\n @if (title()) {\n <p class=\"mk-banner__title\">{{ title() }}</p>\n }\n <div class=\"mk-banner__message\"><ng-content /></div>\n </div>\n <div class=\"mk-banner__actions\"><ng-content select=\"[mkBannerActions]\" /></div>\n @if (dismissible()) {\n <button\n type=\"button\"\n class=\"mk-banner__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"dismiss()\"\n >\n <span aria-hidden=\"true\">&times;</span>\n </button>\n }\n </div>\n}\n", styles: [":host{display:contents;--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_active: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=primary]){--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_active: var(--mk-primary-active);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text)}:host([data-tone=success]){--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_active: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}:host([data-tone=warning]){--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_active: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}:host([data-tone=danger]){--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_active: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}:host([data-tone=info]){--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_active: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=neutral]){--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_active: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-banner__box{display:flex;align-items:flex-start;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);background-color:var(--_subtle);border:var(--mk-border-width) solid transparent;border-inline-start:3px solid var(--_main);border-radius:var(--mk-radius-md);color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal)}.mk-banner__icon{flex:none;display:grid;place-items:center;color:var(--_main);line-height:0}.mk-banner__icon:empty{display:none}.mk-banner__body{flex:1 1 auto;min-width:0}.mk-banner__title{margin:0 0 var(--mk-space-1);font-weight:var(--mk-font-weight-semibold);color:var(--_subtle-text)}.mk-banner__message{color:var(--mk-text)}.mk-banner__actions{flex:none;display:flex;align-items:center;gap:var(--mk-space-2)}.mk-banner__actions:empty{display:none}.mk-banner__close{flex:none;display:grid;place-items:center;width:1.5rem;height:1.5rem;padding:0;font-size:var(--mk-font-size-lg);line-height:1;color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-circle);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-banner__close:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-banner__close:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}@media(prefers-reduced-motion:reduce){.mk-banner__close{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1910
+ }
1911
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBanner, decorators: [{
1912
+ type: Component,
1913
+ args: [{ selector: 'mk-banner', changeDetection: ChangeDetectionStrategy.OnPush, host: {
1914
+ class: 'mk-banner',
1915
+ '[attr.data-tone]': 'tone()',
1916
+ }, template: "@if (open()) {\n <div class=\"mk-banner__box\">\n <span class=\"mk-banner__icon\" aria-hidden=\"true\"\n ><ng-content select=\"[mkBannerIcon]\"\n /></span>\n <!-- The live region wraps only the title + message, keeping the actions\n and the dismiss button out of what gets announced. -->\n <div class=\"mk-banner__body\" [attr.role]=\"role()\">\n @if (title()) {\n <p class=\"mk-banner__title\">{{ title() }}</p>\n }\n <div class=\"mk-banner__message\"><ng-content /></div>\n </div>\n <div class=\"mk-banner__actions\"><ng-content select=\"[mkBannerActions]\" /></div>\n @if (dismissible()) {\n <button\n type=\"button\"\n class=\"mk-banner__close\"\n [attr.aria-label]=\"i18n.dismiss\"\n (click)=\"dismiss()\"\n >\n <span aria-hidden=\"true\">&times;</span>\n </button>\n }\n </div>\n}\n", styles: [":host{display:contents;--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_active: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=primary]){--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_active: var(--mk-primary-active);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text)}:host([data-tone=success]){--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_active: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}:host([data-tone=warning]){--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_active: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}:host([data-tone=danger]){--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_active: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}:host([data-tone=info]){--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_active: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=neutral]){--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_active: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-banner__box{display:flex;align-items:flex-start;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);background-color:var(--_subtle);border:var(--mk-border-width) solid transparent;border-inline-start:3px solid var(--_main);border-radius:var(--mk-radius-md);color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal)}.mk-banner__icon{flex:none;display:grid;place-items:center;color:var(--_main);line-height:0}.mk-banner__icon:empty{display:none}.mk-banner__body{flex:1 1 auto;min-width:0}.mk-banner__title{margin:0 0 var(--mk-space-1);font-weight:var(--mk-font-weight-semibold);color:var(--_subtle-text)}.mk-banner__message{color:var(--mk-text)}.mk-banner__actions{flex:none;display:flex;align-items:center;gap:var(--mk-space-2)}.mk-banner__actions:empty{display:none}.mk-banner__close{flex:none;display:grid;place-items:center;width:1.5rem;height:1.5rem;padding:0;font-size:var(--mk-font-size-lg);line-height:1;color:var(--mk-text-muted);background:transparent;border:none;border-radius:var(--mk-radius-circle);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-banner__close:hover{background-color:var(--mk-hover-overlay);color:var(--mk-text)}.mk-banner__close:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}@media(prefers-reduced-motion:reduce){.mk-banner__close{transition:none}}\n"] }]
1917
+ }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
1918
+
1919
+ /**
1920
+ * Drives a global {@link MkLoadingBar} — the thin top-of-page progress bar for
1921
+ * route changes and background work. `start()` shows it and trickles toward 90%;
1922
+ * `complete()` finishes and hides it. Also supports a determinate `set(value)`.
1923
+ *
1924
+ * ```ts
1925
+ * const bar = inject(MkLoadingBarService);
1926
+ * router.events.subscribe(e => {
1927
+ * if (e instanceof NavigationStart) bar.start();
1928
+ * if (e instanceof NavigationEnd) bar.complete();
1929
+ * });
1930
+ * ```
1931
+ */
1932
+ class MkLoadingBarService {
1933
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1934
+ _progress = signal(0, /* @ts-ignore */
1935
+ ...(ngDevMode ? [{ debugName: "_progress" }] : /* istanbul ignore next */ []));
1936
+ _active = signal(false, /* @ts-ignore */
1937
+ ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
1938
+ /** Current progress, 0–100. */
1939
+ progress = this._progress.asReadonly();
1940
+ /** Whether the bar is visible. */
1941
+ active = this._active.asReadonly();
1942
+ timer;
1943
+ doneTimer;
1944
+ /** Show the bar and start trickling toward 90%. */
1945
+ start() {
1946
+ if (!this.isBrowser)
1947
+ return;
1948
+ this.clearTimers();
1949
+ this._active.set(true);
1950
+ this._progress.set(8);
1951
+ this.armTrickle();
1952
+ }
1953
+ /** Set an explicit determinate value (0–100). */
1954
+ set(value) {
1955
+ this.clearTimers();
1956
+ this._active.set(true);
1957
+ this._progress.set(Math.max(0, Math.min(100, value)));
1958
+ }
1959
+ /** Nudge progress up by `amount` (default 5), capped at 90%. */
1960
+ inc(amount = 5) {
1961
+ if (!this._active())
1962
+ this.start();
1963
+ this._progress.set(Math.min(90, this._progress() + amount));
1964
+ // Re-arm the trickle if it stopped at the ceiling (or after a `set()`)
1965
+ // and there is still room to move.
1966
+ if (this._progress() < 90)
1967
+ this.armTrickle();
1968
+ }
1969
+ /** Finish (jump to 100%) then fade out. */
1970
+ complete() {
1971
+ if (!this.isBrowser) {
1972
+ this._active.set(false);
1973
+ return;
1974
+ }
1975
+ this.clearTimers();
1976
+ this._progress.set(100);
1977
+ this.doneTimer = setTimeout(() => {
1978
+ this._active.set(false);
1979
+ this._progress.set(0);
1980
+ }, 350);
1981
+ }
1982
+ /** Hide immediately without finishing. */
1983
+ stop() {
1984
+ this.clearTimers();
1985
+ this._active.set(false);
1986
+ this._progress.set(0);
1987
+ }
1988
+ /**
1989
+ * Auto-slowing trickle so the bar keeps moving without ever completing.
1990
+ * Stops its own interval at the 90% ceiling — otherwise a navigation that
1991
+ * never `complete()`s would leave a timer firing (and, in a zoneless app,
1992
+ * scheduling change detection) forever. `start()`/`inc()` re-arm it.
1993
+ */
1994
+ trickle() {
1995
+ const p = this._progress();
1996
+ if (p >= 90) {
1997
+ this.stopTrickle();
1998
+ return;
1999
+ }
2000
+ const step = p < 20 ? 10 : p < 50 ? 4 : p < 80 ? 2 : 0.5;
2001
+ const next = Math.min(90, p + step);
2002
+ this._progress.set(next);
2003
+ if (next >= 90)
2004
+ this.stopTrickle();
2005
+ }
2006
+ /** Start the 350ms trickle interval unless it is already running. */
2007
+ armTrickle() {
2008
+ if (!this.isBrowser || this.timer)
2009
+ return;
2010
+ this.timer = setInterval(() => this.trickle(), 350);
2011
+ }
2012
+ stopTrickle() {
2013
+ if (this.timer) {
2014
+ clearInterval(this.timer);
2015
+ this.timer = undefined;
2016
+ }
2017
+ }
2018
+ clearTimers() {
2019
+ if (this.timer)
2020
+ clearInterval(this.timer);
2021
+ if (this.doneTimer)
2022
+ clearTimeout(this.doneTimer);
2023
+ this.timer = undefined;
2024
+ this.doneTimer = undefined;
2025
+ }
2026
+ /** True while finishing (progress full but not yet hidden). */
2027
+ finishing = computed(() => this._active() && this._progress() >= 100, /* @ts-ignore */
2028
+ ...(ngDevMode ? [{ debugName: "finishing" }] : /* istanbul ignore next */ []));
2029
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLoadingBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2030
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLoadingBarService, providedIn: 'root' });
2031
+ }
2032
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLoadingBarService, decorators: [{
2033
+ type: Injectable,
2034
+ args: [{ providedIn: 'root' }]
2035
+ }] });
2036
+
2037
+ /**
2038
+ * LoadingBar — a thin progress bar fixed to the top of the viewport, driven by
2039
+ * {@link MkLoadingBarService}. Place it once (e.g. in the app shell) and call
2040
+ * `start()` / `complete()` on navigation or async work.
2041
+ *
2042
+ * ```html
2043
+ * <mk-loading-bar />
2044
+ * ```
2045
+ */
2046
+ class MkLoadingBar {
2047
+ service = inject(MkLoadingBarService);
2048
+ i18n = inject(MK_I18N);
2049
+ /** Bar colour. */
2050
+ color = input('var(--mk-primary)', /* @ts-ignore */
2051
+ ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
2052
+ /** Bar thickness in px. */
2053
+ height = input(3, { ...(ngDevMode ? { debugName: "height" } : /* istanbul ignore next */ {}), transform: numberAttribute });
2054
+ progress = this.service.progress;
2055
+ active = computed(() => this.service.active(), /* @ts-ignore */
2056
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
2057
+ /**
2058
+ * The fill is a full-width bar scaled from the inline start — `transform`
2059
+ * animates on the compositor, so each trickle tick avoids the layout+paint
2060
+ * cost that animating `width` incurred. Origin flips for RTL in the styles.
2061
+ */
2062
+ fillTransform = computed(() => `scaleX(${this.progress() / 100})`, /* @ts-ignore */
2063
+ ...(ngDevMode ? [{ debugName: "fillTransform" }] : /* istanbul ignore next */ []));
2064
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLoadingBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2065
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkLoadingBar, isStandalone: true, selector: "mk-loading-bar", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar", "aria-valuemin": "0", "aria-valuemax": "100" }, properties: { "attr.aria-valuenow": "active() ? progress() : null", "attr.aria-hidden": "active() ? null : 'true'", "attr.aria-label": "i18n.loading", "class.mk-loading-bar--active": "active()", "style.height.px": "height()" }, classAttribute: "mk-loading-bar" }, ngImport: i0, template: `<div
2066
+ class="mk-loading-bar__fill"
2067
+ [style.transform]="fillTransform()"
2068
+ [style.background]="color()"
2069
+ ></div>`, isInline: true, styles: [":host{position:fixed;top:0;left:0;right:0;z-index:var(--mk-z-toast, 1400);pointer-events:none;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-loading-bar--active){opacity:1}.mk-loading-bar__fill{height:100%;width:100%;transform:scaleX(0);transform-origin:left center;will-change:transform;background-color:var(--mk-primary);box-shadow:0 0 8px color-mix(in srgb,var(--mk-primary) 60%,transparent);transition:transform var(--mk-duration-normal, .24s) var(--mk-ease-standard)}.mk-loading-bar__fill:dir(rtl){transform-origin:right center}@media(prefers-reduced-motion:reduce){:host,.mk-loading-bar__fill{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2070
+ }
2071
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLoadingBar, decorators: [{
2072
+ type: Component,
2073
+ args: [{ selector: 'mk-loading-bar', template: `<div
2074
+ class="mk-loading-bar__fill"
2075
+ [style.transform]="fillTransform()"
2076
+ [style.background]="color()"
2077
+ ></div>`, changeDetection: ChangeDetectionStrategy.OnPush, host: {
2078
+ class: 'mk-loading-bar',
2079
+ role: 'progressbar',
2080
+ 'aria-valuemin': '0',
2081
+ 'aria-valuemax': '100',
2082
+ '[attr.aria-valuenow]': 'active() ? progress() : null',
2083
+ '[attr.aria-hidden]': "active() ? null : 'true'",
2084
+ '[attr.aria-label]': 'i18n.loading',
2085
+ '[class.mk-loading-bar--active]': 'active()',
2086
+ '[style.height.px]': 'height()',
2087
+ }, styles: [":host{position:fixed;top:0;left:0;right:0;z-index:var(--mk-z-toast, 1400);pointer-events:none;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}:host(.mk-loading-bar--active){opacity:1}.mk-loading-bar__fill{height:100%;width:100%;transform:scaleX(0);transform-origin:left center;will-change:transform;background-color:var(--mk-primary);box-shadow:0 0 8px color-mix(in srgb,var(--mk-primary) 60%,transparent);transition:transform var(--mk-duration-normal, .24s) var(--mk-ease-standard)}.mk-loading-bar__fill:dir(rtl){transform-origin:right center}@media(prefers-reduced-motion:reduce){:host,.mk-loading-bar__fill{transition:none}}\n"] }]
2088
+ }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
2089
+
2090
+ /** DI token carrying the current {@link MkTourData} to the popup. */
2091
+ const MK_TOUR_DATA = new InjectionToken('MK_TOUR_DATA');
2092
+ /**
2093
+ * The floating coach-mark popover for one tour step. Rendered dynamically by
2094
+ * {@link MkTourService} (one instance per step) and anchored to the step's
2095
+ * target through {@link MkAnchoredPanel}, so it lives in the browser top layer
2096
+ * and is never clipped by an ancestor's `overflow`/`transform`.
2097
+ */
2098
+ class MkTourPopup {
2099
+ injector = inject(Injector);
2100
+ panelRef = viewChild('panel', /* @ts-ignore */
2101
+ ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
2102
+ i18n = inject(MK_I18N);
2103
+ /** The step + navigation data for this popup. */
2104
+ data = inject(MK_TOUR_DATA);
2105
+ /** Whether this is the final step (Next becomes "Done"). */
2106
+ isLast = computed(() => this.data.index >= this.data.total - 1, /* @ts-ignore */
2107
+ ...(ngDevMode ? [{ debugName: "isLast" }] : /* istanbul ignore next */ []));
2108
+ constructor() {
2109
+ afterNextRender(() => this.focusInitial(), { injector: this.injector });
2110
+ }
2111
+ next() {
2112
+ this.data.controller.next();
2113
+ }
2114
+ prev() {
2115
+ this.data.controller.prev();
2116
+ }
2117
+ skip() {
2118
+ this.data.controller.end();
2119
+ }
2120
+ onEscape(event) {
2121
+ event.preventDefault();
2122
+ event.stopPropagation();
2123
+ this.data.controller.end();
2124
+ }
2125
+ /**
2126
+ * Ignore anchored-panel dismisses (outside pointerdown / window blur): a tour
2127
+ * should stay put while the user reads it — it ends only via the buttons or
2128
+ * Escape.
2129
+ */
2130
+ onDismiss() {
2131
+ /* intentionally inert */
2132
+ }
2133
+ /**
2134
+ * Trap Tab inside the popup (the dialog is `aria-modal` over a scrim): wrap
2135
+ * from the last tabbable back to the first and vice versa, and pull focus in
2136
+ * if it somehow ended up outside.
2137
+ */
2138
+ onTab(event) {
2139
+ const e = event;
2140
+ const panel = this.panelRef()?.nativeElement;
2141
+ if (!panel)
2142
+ return;
2143
+ const focusable = mkGetFocusable(panel);
2144
+ if (focusable.length === 0) {
2145
+ e.preventDefault();
2146
+ panel.focus();
2147
+ return;
2148
+ }
2149
+ const first = focusable[0];
2150
+ const last = focusable[focusable.length - 1];
2151
+ const active = panel.ownerDocument.activeElement;
2152
+ if (!panel.contains(active)) {
2153
+ e.preventDefault();
2154
+ first.focus();
2155
+ }
2156
+ else if (e.shiftKey && active === first) {
2157
+ e.preventDefault();
2158
+ last.focus();
2159
+ }
2160
+ else if (!e.shiftKey && active === last) {
2161
+ e.preventDefault();
2162
+ first.focus();
2163
+ }
2164
+ }
2165
+ focusInitial() {
2166
+ const panel = this.panelRef()?.nativeElement;
2167
+ if (!panel)
2168
+ return;
2169
+ (mkGetFocusable(panel)[0] ?? panel).focus();
2170
+ }
2171
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTourPopup, deps: [], target: i0.ɵɵFactoryTarget.Component });
2172
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkTourPopup, isStandalone: true, selector: "mk-tour-popup", host: { classAttribute: "mk-tour" }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #panel\n class=\"mk-tour__popup\"\n role=\"dialog\"\n aria-modal=\"true\"\n tabindex=\"-1\"\n [attr.aria-label]=\"data.step.title ?? data.step.body\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"data.anchor\"\n [placement]=\"data.step.placement ?? 'bottom'\"\n [gap]=\"12\"\n (dismiss)=\"onDismiss()\"\n (keydown.escape)=\"onEscape($event)\"\n (keydown.tab)=\"onTab($event)\"\n (keydown.shift.tab)=\"onTab($event)\"\n>\n @if (data.step.title; as title) {\n <h3 class=\"mk-tour__title\">{{ title }}</h3>\n }\n <p class=\"mk-tour__body\">{{ data.step.body }}</p>\n\n <div class=\"mk-tour__footer\">\n <span class=\"mk-tour__indicator\">\n {{ i18n.tourStepOf(data.index + 1, data.total) }}\n </span>\n <div class=\"mk-tour__actions\">\n <button type=\"button\" class=\"mk-tour__btn mk-tour__btn--ghost\" (click)=\"skip()\">\n {{ i18n.tourSkip }}\n </button>\n @if (data.index > 0) {\n <button type=\"button\" class=\"mk-tour__btn\" (click)=\"prev()\">\n {{ i18n.tourPrevious }}\n </button>\n }\n <button\n type=\"button\"\n class=\"mk-tour__btn mk-tour__btn--primary\"\n (click)=\"next()\"\n >\n {{ isLast() ? i18n.tourDone : i18n.tourNext }}\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:contents}.mk-tour__popup{box-sizing:border-box;width:min(20rem,100vw - var(--mk-space-4));padding:var(--mk-space-4);color:var(--mk-text);background:var(--mk-surface);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);animation:mk-tour-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-tour__popup:focus-visible{outline:none}.mk-tour__title{margin:0 0 var(--mk-space-2);font-size:var(--mk-font-size-lg);font-weight:600;line-height:1.3}.mk-tour__body{margin:0;color:var(--mk-text-muted);line-height:1.5}.mk-tour__footer{display:flex;align-items:center;justify-content:space-between;gap:var(--mk-space-3);margin-top:var(--mk-space-4)}.mk-tour__indicator{color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);white-space:nowrap}.mk-tour__actions{display:flex;gap:var(--mk-space-2)}.mk-tour__btn{padding:var(--mk-space-1) var(--mk-space-3);color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer}.mk-tour__btn:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-tour__btn--ghost{color:var(--mk-text-muted);border-color:transparent}.mk-tour__btn--primary{color:var(--mk-primary-contrast);background:var(--mk-primary);border-color:var(--mk-primary)}@keyframes mk-tour-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-tour__popup{animation:none}}\n"], dependencies: [{ kind: "directive", type: MkAnchoredPanel, selector: "[mkAnchoredPanel]", inputs: ["mkAnchoredPanelFor", "anchorRect", "placement", "gap", "matchWidth", "flip", "clamp"], outputs: ["dismiss"], exportAs: ["mkAnchoredPanel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2173
+ }
2174
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTourPopup, decorators: [{
2175
+ type: Component,
2176
+ args: [{ selector: 'mk-tour-popup', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAnchoredPanel], host: {
2177
+ class: 'mk-tour',
2178
+ }, template: "<div\n #panel\n class=\"mk-tour__popup\"\n role=\"dialog\"\n aria-modal=\"true\"\n tabindex=\"-1\"\n [attr.aria-label]=\"data.step.title ?? data.step.body\"\n mkAnchoredPanel\n [mkAnchoredPanelFor]=\"data.anchor\"\n [placement]=\"data.step.placement ?? 'bottom'\"\n [gap]=\"12\"\n (dismiss)=\"onDismiss()\"\n (keydown.escape)=\"onEscape($event)\"\n (keydown.tab)=\"onTab($event)\"\n (keydown.shift.tab)=\"onTab($event)\"\n>\n @if (data.step.title; as title) {\n <h3 class=\"mk-tour__title\">{{ title }}</h3>\n }\n <p class=\"mk-tour__body\">{{ data.step.body }}</p>\n\n <div class=\"mk-tour__footer\">\n <span class=\"mk-tour__indicator\">\n {{ i18n.tourStepOf(data.index + 1, data.total) }}\n </span>\n <div class=\"mk-tour__actions\">\n <button type=\"button\" class=\"mk-tour__btn mk-tour__btn--ghost\" (click)=\"skip()\">\n {{ i18n.tourSkip }}\n </button>\n @if (data.index > 0) {\n <button type=\"button\" class=\"mk-tour__btn\" (click)=\"prev()\">\n {{ i18n.tourPrevious }}\n </button>\n }\n <button\n type=\"button\"\n class=\"mk-tour__btn mk-tour__btn--primary\"\n (click)=\"next()\"\n >\n {{ isLast() ? i18n.tourDone : i18n.tourNext }}\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:contents}.mk-tour__popup{box-sizing:border-box;width:min(20rem,100vw - var(--mk-space-4));padding:var(--mk-space-4);color:var(--mk-text);background:var(--mk-surface);border-radius:var(--mk-radius-lg);box-shadow:var(--mk-shadow-lg);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);animation:mk-tour-in var(--mk-duration-fast) var(--mk-ease-out)}.mk-tour__popup:focus-visible{outline:none}.mk-tour__title{margin:0 0 var(--mk-space-2);font-size:var(--mk-font-size-lg);font-weight:600;line-height:1.3}.mk-tour__body{margin:0;color:var(--mk-text-muted);line-height:1.5}.mk-tour__footer{display:flex;align-items:center;justify-content:space-between;gap:var(--mk-space-3);margin-top:var(--mk-space-4)}.mk-tour__indicator{color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);white-space:nowrap}.mk-tour__actions{display:flex;gap:var(--mk-space-2)}.mk-tour__btn{padding:var(--mk-space-1) var(--mk-space-3);color:var(--mk-text);background:transparent;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer}.mk-tour__btn:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-tour__btn--ghost{color:var(--mk-text-muted);border-color:transparent}.mk-tour__btn--primary{color:var(--mk-primary-contrast);background:var(--mk-primary);border-color:var(--mk-primary)}@keyframes mk-tour-in{0%{opacity:0;transform:translateY(calc(-1 * var(--mk-space-1))) scale(.98)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.mk-tour__popup{animation:none}}\n"] }]
2179
+ }], ctorParameters: () => [], propDecorators: { panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
2180
+
2181
+ /**
2182
+ * Product-onboarding tour service — walks the user through a sequence of
2183
+ * {@link MkTourStep}s. For each step it scrolls the target into view, dims the
2184
+ * page behind a scrim, highlights the target with a bright ring, and anchors a
2185
+ * coach-mark popover ({@link MkTourPopup}) to it in the browser top layer via
2186
+ * `MkAnchoredPanel`.
2187
+ *
2188
+ * ```ts
2189
+ * const tour = inject(MkTourService);
2190
+ * await tour.start([
2191
+ * { target: '#new-btn', title: 'Create', body: 'Start a project here.' },
2192
+ * { target: '#inbox', body: 'Your notifications land here.', placement: 'left' },
2193
+ * ]);
2194
+ * ```
2195
+ */
2196
+ class MkTourService {
2197
+ appRef = inject(ApplicationRef);
2198
+ envInjector = inject(EnvironmentInjector);
2199
+ document = inject(DOCUMENT);
2200
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2201
+ _steps = signal([], /* @ts-ignore */
2202
+ ...(ngDevMode ? [{ debugName: "_steps" }] : /* istanbul ignore next */ []));
2203
+ _index = signal(-1, /* @ts-ignore */
2204
+ ...(ngDevMode ? [{ debugName: "_index" }] : /* istanbul ignore next */ []));
2205
+ _running = signal(false, /* @ts-ignore */
2206
+ ...(ngDevMode ? [{ debugName: "_running" }] : /* istanbul ignore next */ []));
2207
+ /** Zero-based index of the current step, or `-1` when no tour is running. */
2208
+ index = this._index.asReadonly();
2209
+ /** Whether a tour is currently active. */
2210
+ running = this._running.asReadonly();
2211
+ /** Total number of steps in the active tour. */
2212
+ total = computed(() => this._steps().length, /* @ts-ignore */
2213
+ ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
2214
+ /** The current step, or `null`. */
2215
+ currentStep = computed(() => {
2216
+ const i = this._index();
2217
+ return i >= 0 ? (this._steps()[i] ?? null) : null;
2218
+ }, /* @ts-ignore */
2219
+ ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
2220
+ popupRef;
2221
+ scrim;
2222
+ snapshot;
2223
+ endResolve;
2224
+ endPromise = Promise.resolve();
2225
+ /** Element focused before start(); focus is restored to it on end(). */
2226
+ previouslyFocused = null;
2227
+ constructor() {
2228
+ // If a tour is still active when the app (root injector) is torn down,
2229
+ // end it — otherwise the capture-phase document keydown listener, scrim
2230
+ // and popup DOM would outlive the application.
2231
+ inject(DestroyRef).onDestroy(() => this.end());
2232
+ }
2233
+ /**
2234
+ * Begin a tour at step 0. Returns a promise that resolves when the tour ends
2235
+ * (finished, skipped, or Escaped).
2236
+ */
2237
+ start(steps) {
2238
+ if (!this.isBrowser || steps.length === 0)
2239
+ return Promise.resolve();
2240
+ // Restart cleanly if one is already running.
2241
+ if (this._running())
2242
+ this.end();
2243
+ this.endPromise = new Promise((resolve) => {
2244
+ this.endResolve = resolve;
2245
+ });
2246
+ this.previouslyFocused = this.document
2247
+ .activeElement;
2248
+ this._steps.set([...steps]);
2249
+ this._running.set(true);
2250
+ this.document.addEventListener('keydown', this.onKeydown, true);
2251
+ this.ensureScrim();
2252
+ this.showStep(0, 1);
2253
+ return this.endPromise;
2254
+ }
2255
+ /** Resolves when the active (or next) tour ends. */
2256
+ afterEnd() {
2257
+ return this.endPromise;
2258
+ }
2259
+ /** Advance to the next step, or finish on the last step. */
2260
+ next() {
2261
+ if (!this._running())
2262
+ return;
2263
+ this.showStep(this._index() + 1, 1);
2264
+ }
2265
+ /** Return to the previous step (no-op at the first step). */
2266
+ prev() {
2267
+ if (!this._running() || this._index() <= 0)
2268
+ return;
2269
+ this.showStep(this._index() - 1, -1);
2270
+ }
2271
+ /** End the tour and clean up all DOM, highlight, and listeners. */
2272
+ end() {
2273
+ if (!this._running())
2274
+ return;
2275
+ this._running.set(false);
2276
+ this.teardownStep();
2277
+ this.removeScrim();
2278
+ this.document.removeEventListener('keydown', this.onKeydown, true);
2279
+ this._index.set(-1);
2280
+ this._steps.set([]);
2281
+ // Return focus to wherever the user was before the tour started
2282
+ // (Done/Skip/Escape all funnel through here).
2283
+ this.previouslyFocused?.focus?.();
2284
+ this.previouslyFocused = null;
2285
+ const resolve = this.endResolve;
2286
+ this.endResolve = undefined;
2287
+ resolve?.();
2288
+ }
2289
+ onKeydown = (e) => {
2290
+ if (e.key === 'Escape') {
2291
+ e.stopPropagation();
2292
+ this.end();
2293
+ }
2294
+ };
2295
+ /**
2296
+ * Render the step at `index`. Missing targets are skipped by continuing in
2297
+ * `dir` (+1 forward, -1 back); if none remain the tour ends.
2298
+ */
2299
+ showStep(index, dir) {
2300
+ this.teardownStep();
2301
+ const steps = this._steps();
2302
+ let i = index;
2303
+ let target = null;
2304
+ while (i >= 0 && i < steps.length) {
2305
+ target = this.resolveTarget(steps[i].target);
2306
+ if (target)
2307
+ break;
2308
+ i += dir;
2309
+ }
2310
+ if (!target || i < 0 || i >= steps.length) {
2311
+ this.end();
2312
+ return;
2313
+ }
2314
+ this._index.set(i);
2315
+ this.highlight(target);
2316
+ target.scrollIntoView?.({ block: 'center', inline: 'center', behavior: 'smooth' });
2317
+ const data = {
2318
+ step: steps[i],
2319
+ index: i,
2320
+ total: steps.length,
2321
+ anchor: target,
2322
+ controller: {
2323
+ next: () => this.next(),
2324
+ prev: () => this.prev(),
2325
+ end: () => this.end(),
2326
+ },
2327
+ };
2328
+ const ref = createComponent(MkTourPopup, {
2329
+ environmentInjector: this.envInjector,
2330
+ elementInjector: Injector.create({
2331
+ parent: this.envInjector,
2332
+ providers: [{ provide: MK_TOUR_DATA, useValue: data }],
2333
+ }),
2334
+ });
2335
+ this.appRef.attachView(ref.hostView);
2336
+ this.document.body.appendChild(ref.location.nativeElement);
2337
+ this.popupRef = ref;
2338
+ }
2339
+ /** Destroy the current popup and restore the current target's styles. */
2340
+ teardownStep() {
2341
+ if (this.popupRef) {
2342
+ // At app teardown the ApplicationRef destroys (and detaches) its views
2343
+ // before injector destroy hooks run — don't touch it then.
2344
+ if (!this.appRef.destroyed)
2345
+ this.appRef.detachView(this.popupRef.hostView);
2346
+ this.popupRef.destroy();
2347
+ this.popupRef.location.nativeElement.remove();
2348
+ this.popupRef = undefined;
2349
+ }
2350
+ this.unhighlight();
2351
+ }
2352
+ resolveTarget(target) {
2353
+ if (typeof target !== 'string') {
2354
+ return target.isConnected ? target : null;
2355
+ }
2356
+ return this.document.querySelector(target);
2357
+ }
2358
+ /** Raise the target above the scrim and draw a bright ring around it. */
2359
+ highlight(el) {
2360
+ const style = el.style;
2361
+ this.snapshot = {
2362
+ el,
2363
+ position: style.position,
2364
+ zIndex: style.zIndex,
2365
+ boxShadow: style.boxShadow,
2366
+ borderRadius: style.borderRadius,
2367
+ transition: style.transition,
2368
+ };
2369
+ el.classList.add('mk-tour-target');
2370
+ if (getComputedStyle(el).position === 'static') {
2371
+ style.position = 'relative';
2372
+ }
2373
+ style.setProperty('z-index', 'calc(var(--mk-z-overlay, 1000) + 1)');
2374
+ style.boxShadow =
2375
+ '0 0 0 3px var(--mk-primary), 0 0 0 6px var(--mk-primary-contrast)';
2376
+ if (!style.borderRadius)
2377
+ style.borderRadius = 'var(--mk-radius-md)';
2378
+ style.transition = 'box-shadow var(--mk-duration-fast, 140ms) ease';
2379
+ }
2380
+ unhighlight() {
2381
+ const snap = this.snapshot;
2382
+ if (!snap)
2383
+ return;
2384
+ const { el } = snap;
2385
+ el.classList.remove('mk-tour-target');
2386
+ el.style.position = snap.position;
2387
+ el.style.zIndex = snap.zIndex;
2388
+ el.style.boxShadow = snap.boxShadow;
2389
+ el.style.borderRadius = snap.borderRadius;
2390
+ el.style.transition = snap.transition;
2391
+ this.snapshot = undefined;
2392
+ }
2393
+ ensureScrim() {
2394
+ if (this.scrim)
2395
+ return;
2396
+ const scrim = this.document.createElement('div');
2397
+ scrim.className = 'mk-tour-scrim';
2398
+ scrim.style.cssText =
2399
+ 'position:fixed;inset:0;background:var(--mk-overlay-scrim,rgba(0,0,0,0.45));animation:mk-overlay-fade var(--mk-duration-fast,140ms) var(--mk-ease-out,ease);';
2400
+ scrim.style.setProperty('z-index', 'var(--mk-z-overlay, 1000)');
2401
+ this.document.body.appendChild(scrim);
2402
+ this.scrim = scrim;
2403
+ }
2404
+ removeScrim() {
2405
+ this.scrim?.remove();
2406
+ this.scrim = undefined;
2407
+ }
2408
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTourService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2409
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTourService, providedIn: 'root' });
2410
+ }
2411
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkTourService, decorators: [{
2412
+ type: Injectable,
2413
+ args: [{ providedIn: 'root' }]
2414
+ }], ctorParameters: () => [] });
2415
+
2416
+ /**
2417
+ * Feedback & Overlay component group barrel.
2418
+ * Re-exports every public symbol (components, directives, services, types).
2419
+ */
2420
+
2421
+ /**
2422
+ * Generated bundle index. Do not edit.
2423
+ */
2424
+
2425
+ export { MK_TOUR_DATA, MkAlert, MkBanner, MkBottomSheet, MkBottomSheetService, MkConfirmDialog, MkDialog, MkDialogService, MkDialogTitle, MkHovercard, MkHovercardTrigger, MkLoadingBar, MkLoadingBarService, MkNotificationCenter, MkPopconfirm, MkPopconfirmTrigger, MkPopover, MkPopoverTrigger, MkPromptDialog, MkResult, MkSnackbar, MkSnackbarContainer, MkSnackbarRef, MkSnackbarService, MkToast, MkToastContainer, MkToastService, MkTooltip, MkTooltipPanel, MkTourPopup, MkTourService };
2426
+ //# sourceMappingURL=mk-kit-ui-feedback.mjs.map