@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,1779 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, DOCUMENT, PLATFORM_ID, input, booleanAttribute, output, effect, Directive, numberAttribute, signal, afterNextRender, Injector, computed, Injectable, DestroyRef, TemplateRef, ViewContainerRef } from '@angular/core';
3
+ import { isPlatformBrowser } from '@angular/common';
4
+
5
+ /**
6
+ * Emits when a pointer press lands outside the host element — the building block
7
+ * for dismissing menus, popovers and custom dropdowns. Listens on the document
8
+ * in the capture phase so it fires before the target handles the event.
9
+ *
10
+ * ```html
11
+ * <div class="panel" (mkClickOutside)="close()">…</div>
12
+ * ```
13
+ */
14
+ class MkClickOutside {
15
+ host = inject(ElementRef);
16
+ document = inject(DOCUMENT);
17
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
18
+ /** Disable the listener without removing the directive. */
19
+ enabled = input(true, { ...(ngDevMode ? { debugName: "enabled" } : /* istanbul ignore next */ {}), alias: 'mkClickOutsideEnabled',
20
+ transform: booleanAttribute });
21
+ /** Emits the originating event when a pointerdown occurs outside the host. */
22
+ mkClickOutside = output();
23
+ onPointerdown = (event) => {
24
+ // Defensive: the listener detaches on the effect flush after `enabled`
25
+ // flips false, so also ignore any event racing in before that.
26
+ if (!this.enabled())
27
+ return;
28
+ const target = event.target;
29
+ if (target && !this.host.nativeElement.contains(target)) {
30
+ this.mkClickOutside.emit(event);
31
+ }
32
+ };
33
+ constructor() {
34
+ // The document listener exists only while `enabled()` — an idle directive
35
+ // must not run its handler (and wake change detection) on every pointer
36
+ // press on the page. Effect cleanup also detaches it on destroy.
37
+ effect((onCleanup) => {
38
+ if (!this.isBrowser || !this.enabled())
39
+ return;
40
+ this.document.addEventListener('pointerdown', this.onPointerdown, true);
41
+ onCleanup(() => this.document.removeEventListener('pointerdown', this.onPointerdown, true));
42
+ });
43
+ }
44
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkClickOutside, deps: [], target: i0.ɵɵFactoryTarget.Directive });
45
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkClickOutside, isStandalone: true, selector: "[mkClickOutside]", inputs: { enabled: { classPropertyName: "enabled", publicName: "mkClickOutsideEnabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkClickOutside: "mkClickOutside" }, exportAs: ["mkClickOutside"], ngImport: i0 });
46
+ }
47
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkClickOutside, decorators: [{
48
+ type: Directive,
49
+ args: [{
50
+ selector: '[mkClickOutside]',
51
+ exportAs: 'mkClickOutside',
52
+ }]
53
+ }], ctorParameters: () => [], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkClickOutsideEnabled", required: false }] }], mkClickOutside: [{ type: i0.Output, args: ["mkClickOutside"] }] } });
54
+
55
+ /**
56
+ * Copies text to the clipboard when the host is clicked. Emits `(copied)` on
57
+ * success and `(copyFailed)` on error, and exposes a transient `copied` signal
58
+ * (via `exportAs`) so a template can show a "Copied!" state without extra code.
59
+ *
60
+ * ```html
61
+ * <button mkButton [mkCopyToClipboard]="token" #c="mkCopyToClipboard">
62
+ * {{ c.justCopied() ? 'Copied!' : 'Copy' }}
63
+ * </button>
64
+ * ```
65
+ */
66
+ class MkCopyToClipboard {
67
+ document = inject(DOCUMENT);
68
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
69
+ /** The text to copy. */
70
+ text = input.required({ ...(ngDevMode ? { debugName: "text" } : /* istanbul ignore next */ {}), alias: 'mkCopyToClipboard' });
71
+ /** How long (ms) the `copied` signal stays true after a successful copy. */
72
+ feedbackDuration = input(1500, { ...(ngDevMode ? { debugName: "feedbackDuration" } : /* istanbul ignore next */ {}), alias: 'mkCopyFeedbackDuration',
73
+ transform: numberAttribute });
74
+ /** Emits the copied text on success. */
75
+ copied = output({ alias: 'copiedText' });
76
+ /** Emits the error on failure. */
77
+ copyFailed = output();
78
+ _copied = signal(false, /* @ts-ignore */
79
+ ...(ngDevMode ? [{ debugName: "_copied" }] : /* istanbul ignore next */ []));
80
+ /** True for `feedbackDuration` ms after a successful copy. */
81
+ justCopied = this._copied.asReadonly();
82
+ resetTimer;
83
+ /** Copy the text now. Bound to the host click, but callable directly too. */
84
+ async copy() {
85
+ if (!this.isBrowser)
86
+ return;
87
+ const value = this.text();
88
+ try {
89
+ await this.write(value);
90
+ this.copied.emit(value);
91
+ this.flagCopied();
92
+ }
93
+ catch (err) {
94
+ this.copyFailed.emit(err);
95
+ }
96
+ }
97
+ async write(value) {
98
+ const clipboard = this.document.defaultView?.navigator?.clipboard;
99
+ if (clipboard?.writeText) {
100
+ await clipboard.writeText(value);
101
+ return;
102
+ }
103
+ // Fallback for non-secure contexts: a hidden textarea + execCommand.
104
+ const ta = this.document.createElement('textarea');
105
+ ta.value = value;
106
+ ta.setAttribute('readonly', '');
107
+ ta.style.cssText = 'position:fixed;top:-9999px;opacity:0;';
108
+ this.document.body.appendChild(ta);
109
+ ta.select();
110
+ const ok = typeof this.document.execCommand === 'function' &&
111
+ this.document.execCommand('copy');
112
+ ta.remove();
113
+ if (!ok)
114
+ throw new Error('Clipboard copy failed');
115
+ }
116
+ flagCopied() {
117
+ this._copied.set(true);
118
+ if (this.resetTimer)
119
+ clearTimeout(this.resetTimer);
120
+ // Reset after the feedback window (guarded so tests without a view work).
121
+ this.resetTimer = setTimeout(() => this._copied.set(false), this.feedbackDuration());
122
+ }
123
+ ngOnDestroy() {
124
+ if (this.resetTimer)
125
+ clearTimeout(this.resetTimer);
126
+ }
127
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCopyToClipboard, deps: [], target: i0.ɵɵFactoryTarget.Directive });
128
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkCopyToClipboard, isStandalone: true, selector: "[mkCopyToClipboard]", inputs: { text: { classPropertyName: "text", publicName: "mkCopyToClipboard", isSignal: true, isRequired: true, transformFunction: null }, feedbackDuration: { classPropertyName: "feedbackDuration", publicName: "mkCopyFeedbackDuration", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { copied: "copiedText", copyFailed: "copyFailed" }, host: { listeners: { "click": "copy()" } }, exportAs: ["mkCopyToClipboard"], ngImport: i0 });
129
+ }
130
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCopyToClipboard, decorators: [{
131
+ type: Directive,
132
+ args: [{
133
+ selector: '[mkCopyToClipboard]',
134
+ exportAs: 'mkCopyToClipboard',
135
+ host: {
136
+ '(click)': 'copy()',
137
+ },
138
+ }]
139
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCopyToClipboard", required: true }] }], feedbackDuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCopyFeedbackDuration", required: false }] }], copied: [{ type: i0.Output, args: ["copiedText"] }], copyFailed: [{ type: i0.Output, args: ["copyFailed"] }] } });
140
+
141
+ /**
142
+ * Grows a `<textarea>` to fit its content, between `minRows` and `maxRows`
143
+ * (0 = unbounded). Resizes on input and when the bound value changes.
144
+ *
145
+ * ```html
146
+ * <textarea mkInput mkAutosize [mkAutosizeMaxRows]="8" [(ngModel)]="note"></textarea>
147
+ * ```
148
+ */
149
+ class MkAutosize {
150
+ el = inject(ElementRef);
151
+ document = inject(DOCUMENT);
152
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
153
+ /** Minimum height in rows. */
154
+ minRows = input(1, { ...(ngDevMode ? { debugName: "minRows" } : /* istanbul ignore next */ {}), alias: 'mkAutosizeMinRows',
155
+ transform: numberAttribute });
156
+ /** Maximum height in rows before scrolling (0 = unbounded). */
157
+ maxRows = input(0, { ...(ngDevMode ? { debugName: "maxRows" } : /* istanbul ignore next */ {}), alias: 'mkAutosizeMaxRows',
158
+ transform: numberAttribute });
159
+ /** Bind the textarea's value so programmatic changes trigger a resize. */
160
+ value = input(undefined, { ...(ngDevMode ? { debugName: "value" } : /* istanbul ignore next */ {}), alias: 'mkAutosizeValue' });
161
+ constructor() {
162
+ afterNextRender(() => this.resize());
163
+ // Re-fit whenever the bound value (or the row bounds) change.
164
+ effect(() => {
165
+ this.value();
166
+ this.minRows();
167
+ this.maxRows();
168
+ if (this.isBrowser)
169
+ queueMicrotask(() => this.resize());
170
+ });
171
+ }
172
+ /** Recompute the textarea height from its content. */
173
+ resize() {
174
+ if (!this.isBrowser)
175
+ return;
176
+ const ta = this.el.nativeElement;
177
+ const style = this.document.defaultView?.getComputedStyle(ta);
178
+ const line = parseFloat(style?.lineHeight ?? '') || 20;
179
+ const vPad = parseFloat(style?.paddingTop ?? '0') +
180
+ parseFloat(style?.paddingBottom ?? '0') +
181
+ parseFloat(style?.borderTopWidth ?? '0') +
182
+ parseFloat(style?.borderBottomWidth ?? '0');
183
+ ta.style.height = 'auto';
184
+ const content = ta.scrollHeight;
185
+ const min = this.minRows() * line + vPad;
186
+ const max = this.maxRows() > 0 ? this.maxRows() * line + vPad : Infinity;
187
+ const height = Math.min(max, Math.max(min, content));
188
+ ta.style.height = `${height}px`;
189
+ ta.style.overflowY = content > max ? 'auto' : 'hidden';
190
+ }
191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAutosize, deps: [], target: i0.ɵɵFactoryTarget.Directive });
192
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkAutosize, isStandalone: true, selector: "textarea[mkAutosize]", inputs: { minRows: { classPropertyName: "minRows", publicName: "mkAutosizeMinRows", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "mkAutosizeMaxRows", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "mkAutosizeValue", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "resize()" }, styleAttribute: "resize: none;" }, ngImport: i0 });
193
+ }
194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAutosize, decorators: [{
195
+ type: Directive,
196
+ args: [{
197
+ selector: 'textarea[mkAutosize]',
198
+ host: {
199
+ '(input)': 'resize()',
200
+ style: 'resize: none;',
201
+ },
202
+ }]
203
+ }], ctorParameters: () => [], propDecorators: { minRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkAutosizeMinRows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkAutosizeMaxRows", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkAutosizeValue", required: false }] }] } });
204
+
205
+ /**
206
+ * Focuses the host element once it renders. Handy for dialogs, drawers and
207
+ * newly-revealed forms. Disable conditionally with `[mkAutofocus]="cond"`.
208
+ *
209
+ * ```html
210
+ * <input mkInput mkAutofocus />
211
+ * <input mkInput [mkAutofocus]="isEditing()" [mkAutofocusDelay]="100" />
212
+ * ```
213
+ */
214
+ class MkAutofocus {
215
+ el = inject(ElementRef);
216
+ document = inject(DOCUMENT);
217
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
218
+ /** Whether to focus. Bare `mkAutofocus` = true. */
219
+ enabled = input(true, { ...(ngDevMode ? { debugName: "enabled" } : /* istanbul ignore next */ {}), alias: 'mkAutofocus',
220
+ transform: booleanAttribute });
221
+ /** Delay before focusing, in ms (e.g. to wait out an entrance animation). */
222
+ delay = input(0, { ...(ngDevMode ? { debugName: "delay" } : /* istanbul ignore next */ {}), alias: 'mkAutofocusDelay',
223
+ transform: numberAttribute });
224
+ timeoutId;
225
+ constructor() {
226
+ afterNextRender(() => {
227
+ if (!this.isBrowser || !this.enabled())
228
+ return;
229
+ if (this.delay() > 0) {
230
+ this.timeoutId = this.document.defaultView?.setTimeout(() => this.focus(), this.delay());
231
+ }
232
+ else {
233
+ this.focus();
234
+ }
235
+ });
236
+ }
237
+ focus() {
238
+ this.el.nativeElement.focus();
239
+ }
240
+ ngOnDestroy() {
241
+ if (this.timeoutId != null) {
242
+ this.document.defaultView?.clearTimeout(this.timeoutId);
243
+ this.timeoutId = undefined;
244
+ }
245
+ }
246
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAutofocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
247
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkAutofocus, isStandalone: true, selector: "[mkAutofocus]", inputs: { enabled: { classPropertyName: "enabled", publicName: "mkAutofocus", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "mkAutofocusDelay", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
248
+ }
249
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAutofocus, decorators: [{
250
+ type: Directive,
251
+ args: [{
252
+ selector: '[mkAutofocus]',
253
+ }]
254
+ }], ctorParameters: () => [], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkAutofocus", required: false }] }], delay: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkAutofocusDelay", required: false }] }] } });
255
+
256
+ /**
257
+ * Scrollspy — tracks which section is currently in view and exposes its `id`,
258
+ * so a table of contents / "on this page" nav can highlight the active link.
259
+ *
260
+ * Apply it to any element (typically the nav) and point `mkScrollspy` at a CSS
261
+ * selector matching the section elements to watch (each needs an `id`). Read the
262
+ * active id via the exported instance, or bind `(activeChange)`:
263
+ *
264
+ * ```html
265
+ * <nav mkScrollspy="main :is(h2, h3)[id]" #spy="mkScrollspy">
266
+ * <a href="#intro" [class.is-active]="spy.activeId() === 'intro'">Intro</a>
267
+ * …
268
+ * </nav>
269
+ * ```
270
+ *
271
+ * A section activates once its top passes `offset` px below the top of the
272
+ * scroll root (default the viewport). When scrolling through a gap between
273
+ * sections the previous one stays active until the next crosses the line.
274
+ */
275
+ class MkScrollspy {
276
+ document = inject(DOCUMENT);
277
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
278
+ /** CSS selector for the section elements to track (queried within `root`). */
279
+ sections = input.required({ ...(ngDevMode ? { debugName: "sections" } : /* istanbul ignore next */ {}), alias: 'mkScrollspy' });
280
+ /**
281
+ * Scroll container. Sections are queried inside it and its top edge is the
282
+ * activation reference. Defaults to the whole document / viewport.
283
+ */
284
+ root = input(null, /* @ts-ignore */
285
+ ...(ngDevMode ? [{ debugName: "root" }] : /* istanbul ignore next */ []));
286
+ /**
287
+ * Distance (px) below the scroll root's top at which a section becomes
288
+ * active. Raise it to account for a sticky header.
289
+ */
290
+ offset = input(96, /* @ts-ignore */
291
+ ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
292
+ /** The id of the currently active section (`null` before the first one). */
293
+ activeId = signal(null, /* @ts-ignore */
294
+ ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
295
+ /** Emitted whenever the active section changes. */
296
+ activeChange = output();
297
+ scrollSource;
298
+ constructor() {
299
+ // afterNextRender runs once inputs are bound, so root()/sections() resolve
300
+ // and the projected sections exist in the DOM.
301
+ afterNextRender(() => {
302
+ if (!this.isBrowser)
303
+ return;
304
+ const view = this.document.defaultView;
305
+ this.scrollSource = this.root() ?? view ?? undefined;
306
+ this.scrollSource?.addEventListener('scroll', this.onScroll, {
307
+ passive: true,
308
+ });
309
+ view?.addEventListener('resize', this.onScroll);
310
+ this.compute();
311
+ });
312
+ }
313
+ /** Re-evaluate the active section (call after sections change). */
314
+ refresh() {
315
+ if (this.isBrowser)
316
+ this.compute();
317
+ }
318
+ querySections() {
319
+ const rootEl = this.root() ?? this.document;
320
+ return Array.from(rootEl.querySelectorAll(this.sections()));
321
+ }
322
+ computeRaf = null;
323
+ /** rAF-coalesced — several scroll ticks per frame trigger one layout read. */
324
+ onScroll = () => {
325
+ if (this.computeRaf != null)
326
+ return;
327
+ this.computeRaf =
328
+ this.document.defaultView?.requestAnimationFrame(() => {
329
+ this.computeRaf = null;
330
+ this.compute();
331
+ }) ?? null;
332
+ };
333
+ compute() {
334
+ const sections = this.querySections();
335
+ if (!sections.length) {
336
+ this.setActive(null);
337
+ return;
338
+ }
339
+ // At the very bottom the final section may be too short to reach the line —
340
+ // activate it explicitly so the last nav item can still highlight.
341
+ if (this.atBottom()) {
342
+ this.setActive(sections[sections.length - 1].id || null);
343
+ return;
344
+ }
345
+ const rootEl = this.root();
346
+ const line = (rootEl ? rootEl.getBoundingClientRect().top : 0) + this.offset();
347
+ // Active = the last section whose top has crossed the activation line.
348
+ let active = null;
349
+ for (const section of sections) {
350
+ if (section.getBoundingClientRect().top - line <= 1)
351
+ active = section;
352
+ else
353
+ break;
354
+ }
355
+ // Nothing crossed yet (still above the first section) → highlight the first.
356
+ this.setActive((active ?? sections[0]).id || null);
357
+ }
358
+ /** Whether the scroll root is scrolled to (or within 1px of) its bottom. */
359
+ atBottom() {
360
+ const root = this.root();
361
+ if (root) {
362
+ return root.scrollTop + root.clientHeight >= root.scrollHeight - 1;
363
+ }
364
+ const view = this.document.defaultView;
365
+ if (!view)
366
+ return false;
367
+ const doc = this.document.documentElement;
368
+ return view.scrollY + view.innerHeight >= doc.scrollHeight - 1;
369
+ }
370
+ setActive(id) {
371
+ if (this.activeId() === id)
372
+ return;
373
+ this.activeId.set(id);
374
+ this.activeChange.emit(id);
375
+ }
376
+ ngOnDestroy() {
377
+ if (this.computeRaf != null) {
378
+ this.document.defaultView?.cancelAnimationFrame(this.computeRaf);
379
+ this.computeRaf = null;
380
+ }
381
+ this.scrollSource?.removeEventListener('scroll', this.onScroll);
382
+ this.document.defaultView?.removeEventListener('resize', this.onScroll);
383
+ }
384
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkScrollspy, deps: [], target: i0.ɵɵFactoryTarget.Directive });
385
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkScrollspy, isStandalone: true, selector: "[mkScrollspy]", inputs: { sections: { classPropertyName: "sections", publicName: "mkScrollspy", isSignal: true, isRequired: true, transformFunction: null }, root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeChange: "activeChange" }, exportAs: ["mkScrollspy"], ngImport: i0 });
386
+ }
387
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkScrollspy, decorators: [{
388
+ type: Directive,
389
+ args: [{
390
+ selector: '[mkScrollspy]',
391
+ exportAs: 'mkScrollspy',
392
+ }]
393
+ }], ctorParameters: () => [], propDecorators: { sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkScrollspy", required: true }] }], root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], activeChange: [{ type: i0.Output, args: ["activeChange"] }] } });
394
+
395
+ /**
396
+ * Intersect — a thin `IntersectionObserver` wrapper. Emits `(mkIntersect)` with
397
+ * the host's visibility whenever it enters or leaves the viewport (or `root`),
398
+ * and exposes an `intersecting` signal via `exportAs`. Set `once` to fire a
399
+ * single time and disconnect — the primitive for reveal-on-scroll animations
400
+ * and lazy loading.
401
+ *
402
+ * ```html
403
+ * <img [attr.src]="loaded() ? src : null" mkIntersect once
404
+ * (mkIntersect)="loaded.set(true)" />
405
+ *
406
+ * <div mkIntersect #io="mkIntersect" [class.in-view]="io.intersecting()">…</div>
407
+ * ```
408
+ */
409
+ class MkIntersect {
410
+ host = inject(ElementRef);
411
+ document = inject(DOCUMENT);
412
+ injector = inject(Injector);
413
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
414
+ /** Element used as the viewport for the check (defaults to the browser one). */
415
+ root = input(null, /* @ts-ignore */
416
+ ...(ngDevMode ? [{ debugName: "root" }] : /* istanbul ignore next */ []));
417
+ /** Margin grown/shrunk around the root before computing intersection. */
418
+ rootMargin = input('0px', /* @ts-ignore */
419
+ ...(ngDevMode ? [{ debugName: "rootMargin" }] : /* istanbul ignore next */ []));
420
+ /** Visibility ratio(s) at which the observer fires. */
421
+ threshold = input(0, /* @ts-ignore */
422
+ ...(ngDevMode ? [{ debugName: "threshold" }] : /* istanbul ignore next */ []));
423
+ /** Emit the first time the host becomes visible, then disconnect. */
424
+ once = input(false, { ...(ngDevMode ? { debugName: "once" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
425
+ /** Emits the host's `isIntersecting` state on every change. */
426
+ mkIntersect = output();
427
+ /** The current visibility, also readable via `exportAs`. */
428
+ intersecting = signal(false, /* @ts-ignore */
429
+ ...(ngDevMode ? [{ debugName: "intersecting" }] : /* istanbul ignore next */ []));
430
+ observer;
431
+ constructor() {
432
+ afterNextRender(() => {
433
+ if (!this.isBrowser || typeof IntersectionObserver === 'undefined')
434
+ return;
435
+ this.observer = new IntersectionObserver((entries) => this.onEntries(entries), {
436
+ root: this.root(),
437
+ rootMargin: this.rootMargin(),
438
+ threshold: this.threshold(),
439
+ });
440
+ this.observer.observe(this.host.nativeElement);
441
+ }, { injector: this.injector });
442
+ }
443
+ onEntries(entries) {
444
+ // Ignore any callback queued before we disconnected (e.g. after `once`).
445
+ if (!this.observer)
446
+ return;
447
+ const entry = entries[entries.length - 1];
448
+ const visible = entry.isIntersecting;
449
+ if (visible === this.intersecting())
450
+ return;
451
+ this.intersecting.set(visible);
452
+ this.mkIntersect.emit(visible);
453
+ if (visible && this.once())
454
+ this.disconnect();
455
+ }
456
+ disconnect() {
457
+ this.observer?.disconnect();
458
+ this.observer = undefined;
459
+ }
460
+ ngOnDestroy() {
461
+ this.disconnect();
462
+ }
463
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkIntersect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
464
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkIntersect, isStandalone: true, selector: "[mkIntersect]", inputs: { root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: false, transformFunction: null }, rootMargin: { classPropertyName: "rootMargin", publicName: "rootMargin", isSignal: true, isRequired: false, transformFunction: null }, threshold: { classPropertyName: "threshold", publicName: "threshold", isSignal: true, isRequired: false, transformFunction: null }, once: { classPropertyName: "once", publicName: "once", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkIntersect: "mkIntersect" }, exportAs: ["mkIntersect"], ngImport: i0 });
465
+ }
466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkIntersect, decorators: [{
467
+ type: Directive,
468
+ args: [{
469
+ selector: '[mkIntersect]',
470
+ exportAs: 'mkIntersect',
471
+ }]
472
+ }], ctorParameters: () => [], propDecorators: { root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: false }] }], rootMargin: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootMargin", required: false }] }], threshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "threshold", required: false }] }], once: [{ type: i0.Input, args: [{ isSignal: true, alias: "once", required: false }] }], mkIntersect: [{ type: i0.Output, args: ["mkIntersect"] }] } });
473
+
474
+ /**
475
+ * InfiniteScroll — emits `(mkInfiniteScroll)` when the host scroll container is
476
+ * scrolled within `distance` px of its bottom, so a list can load its next
477
+ * page. Apply it to the scrollable element (`overflow: auto`). It fires once per
478
+ * approach and re-arms after you scroll back up past the threshold, so a single
479
+ * gesture never triggers repeated loads. Set `disabled` while a page is loading
480
+ * or once everything is loaded.
481
+ *
482
+ * ```html
483
+ * <div class="feed" mkInfiniteScroll [disabled]="loading() || done()"
484
+ * (mkInfiniteScroll)="loadMore()">
485
+ * @for (item of items(); track item.id) { … }
486
+ * </div>
487
+ * ```
488
+ */
489
+ class MkInfiniteScroll {
490
+ host = inject(ElementRef);
491
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
492
+ /** Distance (px) from the bottom at which the output fires. */
493
+ distance = input(200, { ...(ngDevMode ? { debugName: "distance" } : /* istanbul ignore next */ {}), transform: numberAttribute });
494
+ /** Pause firing (e.g. while a page is loading or all items are loaded). */
495
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
496
+ /** Emitted when the host is scrolled near its bottom. */
497
+ mkInfiniteScroll = output();
498
+ /** Whether the next bottom-approach is allowed to fire. */
499
+ armed = true;
500
+ constructor() {
501
+ afterNextRender(() => {
502
+ if (!this.isBrowser)
503
+ return;
504
+ this.host.nativeElement.addEventListener('scroll', this.onScroll, {
505
+ passive: true,
506
+ });
507
+ // Fire immediately if the initial content doesn't fill the container.
508
+ this.check();
509
+ });
510
+ }
511
+ onScroll = () => this.check();
512
+ check() {
513
+ const el = this.host.nativeElement;
514
+ const remaining = el.scrollHeight - el.scrollTop - el.clientHeight;
515
+ if (remaining > this.distance()) {
516
+ // Scrolled away from the bottom — allow the next approach to fire.
517
+ this.armed = true;
518
+ return;
519
+ }
520
+ if (!this.armed || this.disabled())
521
+ return;
522
+ this.armed = false;
523
+ this.mkInfiniteScroll.emit();
524
+ }
525
+ ngOnDestroy() {
526
+ this.host.nativeElement.removeEventListener('scroll', this.onScroll);
527
+ }
528
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkInfiniteScroll, deps: [], target: i0.ɵɵFactoryTarget.Directive });
529
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkInfiniteScroll, isStandalone: true, selector: "[mkInfiniteScroll]", inputs: { distance: { classPropertyName: "distance", publicName: "distance", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkInfiniteScroll: "mkInfiniteScroll" }, exportAs: ["mkInfiniteScroll"], ngImport: i0 });
530
+ }
531
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkInfiniteScroll, decorators: [{
532
+ type: Directive,
533
+ args: [{
534
+ selector: '[mkInfiniteScroll]',
535
+ exportAs: 'mkInfiniteScroll',
536
+ }]
537
+ }], ctorParameters: () => [], propDecorators: { distance: [{ type: i0.Input, args: [{ isSignal: true, alias: "distance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], mkInfiniteScroll: [{ type: i0.Output, args: ["mkInfiniteScroll"] }] } });
538
+
539
+ /**
540
+ * Ripple — a Material-style touch ripple that expands from the pointer on press.
541
+ * Apply it to any pressable surface (button, list item, card). The directive
542
+ * makes the host a positioned, clipping container and paints a short-lived wave;
543
+ * it respects `prefers-reduced-motion`.
544
+ *
545
+ * ```html
546
+ * <button mkRipple>Save</button>
547
+ * <li mkRipple mkRippleColor="var(--mk-primary)">Item</li>
548
+ * ```
549
+ */
550
+ class MkRipple {
551
+ host = inject(ElementRef);
552
+ document = inject(DOCUMENT);
553
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
554
+ /** Wave colour (any CSS colour). Defaults to the host's `currentColor`. */
555
+ color = input('', { ...(ngDevMode ? { debugName: "color" } : /* istanbul ignore next */ {}), alias: 'mkRippleColor' });
556
+ /** Start every ripple from the centre instead of the pointer. */
557
+ centered = input(false, { ...(ngDevMode ? { debugName: "centered" } : /* istanbul ignore next */ {}), alias: 'mkRippleCentered',
558
+ transform: booleanAttribute });
559
+ /** Turn the effect off. */
560
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), alias: 'mkRippleDisabled',
561
+ transform: booleanAttribute });
562
+ spawn(event) {
563
+ if (!this.isBrowser || this.disabled())
564
+ return;
565
+ const el = this.host.nativeElement;
566
+ const view = this.document.defaultView;
567
+ if (view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
568
+ return;
569
+ // Make the host a positioned, clipping container (idempotent).
570
+ const cs = view?.getComputedStyle(el);
571
+ if (cs && cs.position === 'static')
572
+ el.style.position = 'relative';
573
+ el.style.overflow = 'hidden';
574
+ const rect = el.getBoundingClientRect();
575
+ const x = this.centered() ? rect.width / 2 : event.clientX - rect.left;
576
+ const y = this.centered() ? rect.height / 2 : event.clientY - rect.top;
577
+ // Radius that reaches the farthest corner from the origin.
578
+ const radius = Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y));
579
+ const wave = this.document.createElement('span');
580
+ wave.className = 'mk-ripple__wave';
581
+ Object.assign(wave.style, {
582
+ position: 'absolute',
583
+ left: `${x - radius}px`,
584
+ top: `${y - radius}px`,
585
+ width: `${radius * 2}px`,
586
+ height: `${radius * 2}px`,
587
+ borderRadius: '50%',
588
+ background: this.color() || 'currentColor',
589
+ pointerEvents: 'none',
590
+ transform: 'scale(0)',
591
+ opacity: '0.3',
592
+ });
593
+ el.appendChild(wave);
594
+ const cleanup = () => wave.remove();
595
+ const anim = wave.animate?.([
596
+ { transform: 'scale(0)', opacity: 0.3 },
597
+ { transform: 'scale(1)', opacity: 0 },
598
+ ], { duration: 500, easing: 'ease-out' });
599
+ if (anim)
600
+ anim.onfinish = cleanup;
601
+ // Fallback removal in case the animation never resolves (or is unsupported).
602
+ view?.setTimeout(cleanup, 650);
603
+ }
604
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkRipple, deps: [], target: i0.ɵɵFactoryTarget.Directive });
605
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkRipple, isStandalone: true, selector: "[mkRipple]", inputs: { color: { classPropertyName: "color", publicName: "mkRippleColor", isSignal: true, isRequired: false, transformFunction: null }, centered: { classPropertyName: "centered", publicName: "mkRippleCentered", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "mkRippleDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "spawn($event)" } }, ngImport: i0 });
606
+ }
607
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkRipple, decorators: [{
608
+ type: Directive,
609
+ args: [{
610
+ selector: '[mkRipple]',
611
+ host: {
612
+ '(pointerdown)': 'spawn($event)',
613
+ },
614
+ }]
615
+ }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkRippleColor", required: false }] }], centered: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkRippleCentered", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkRippleDisabled", required: false }] }] } });
616
+
617
+ /** Token → predicate for the character it accepts. */
618
+ const MK_MASK_TOKENS = {
619
+ '0': /[0-9]/, // digit
620
+ A: /[A-Za-z]/, // letter
621
+ '*': /[0-9A-Za-z]/, // alphanumeric
622
+ };
623
+ const ALNUM = /[0-9A-Za-z]/;
624
+ /**
625
+ * Format `value` against a mask `pattern`. Pattern tokens: `0` = digit,
626
+ * `A` = letter, `*` = alphanumeric; any other character is a literal that is
627
+ * inserted automatically. Characters that don't fit the current token are
628
+ * skipped, so pasted / mistyped input is coerced into shape.
629
+ *
630
+ * Returns the `masked` (display) string and the `unmasked` string (only the
631
+ * token characters — the value you usually store).
632
+ */
633
+ function mkApplyMask(value, pattern) {
634
+ let masked = '';
635
+ let unmasked = '';
636
+ let vi = 0;
637
+ for (let pi = 0; pi < pattern.length; pi++) {
638
+ const pc = pattern[pi];
639
+ const rule = MK_MASK_TOKENS[pc];
640
+ if (rule) {
641
+ while (vi < value.length && !rule.test(value[vi]))
642
+ vi++;
643
+ if (vi >= value.length)
644
+ break;
645
+ masked += value[vi];
646
+ unmasked += value[vi];
647
+ vi++;
648
+ }
649
+ else {
650
+ // Stop rather than emit a trailing literal with no content after it.
651
+ if (vi >= value.length)
652
+ break;
653
+ masked += pc;
654
+ if (value[vi] === pc)
655
+ vi++;
656
+ }
657
+ }
658
+ return { masked, unmasked };
659
+ }
660
+ /**
661
+ * Compute where the caret belongs after re-masking: after the same number of
662
+ * value (alphanumeric) characters the caret used to sit behind, absorbing any
663
+ * literal(s) that immediately follow — so typing "1" in "(0)" lands the caret
664
+ * after the "(". Shared by {@link MkMask} and the masked form controls.
665
+ */
666
+ function mkMaskCaret(oldValue, oldCaret, masked) {
667
+ const before = oldValue.slice(0, oldCaret);
668
+ const target = (before.match(/[0-9A-Za-z]/g) ?? []).length;
669
+ let pos = 0;
670
+ let seen = 0;
671
+ while (pos < masked.length && seen < target) {
672
+ if (ALNUM.test(masked[pos]))
673
+ seen++;
674
+ pos++;
675
+ }
676
+ while (pos < masked.length && !ALNUM.test(masked[pos]))
677
+ pos++;
678
+ return pos;
679
+ }
680
+ /**
681
+ * Mask — formats an `<input>` as the user types against a token pattern
682
+ * (phone, card, date, custom). Apply it to any text input; read the raw value
683
+ * via `(unmaskedChange)` (or the exported `unmasked` getter) and the formatted
684
+ * value via `(maskedChange)`.
685
+ *
686
+ * ```html
687
+ * <input mkInput mkMask="(000) 000-0000" (unmaskedChange)="phone.set($event)" />
688
+ * <input mkInput mkMask="0000 0000 0000 0000" />
689
+ * ```
690
+ */
691
+ class MkMask {
692
+ host = inject(ElementRef);
693
+ /** The mask pattern (token string). */
694
+ pattern = input.required({ ...(ngDevMode ? { debugName: "pattern" } : /* istanbul ignore next */ {}), alias: 'mkMask' });
695
+ /** Override the derived `inputmode` (`numeric` for digit-only masks). */
696
+ maskInputmode = input(null, { ...(ngDevMode ? { debugName: "maskInputmode" } : /* istanbul ignore next */ {}), alias: 'mkMaskInputmode' });
697
+ /** Mobile keyboard hint: numeric for digit-only patterns unless overridden. */
698
+ inputMode = computed(() => this.maskInputmode() ?? (/[A*]/.test(this.pattern()) ? 'text' : 'numeric'), /* @ts-ignore */
699
+ ...(ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []));
700
+ /** The formatted value, on every change. */
701
+ maskedChange = output();
702
+ /** The raw value (token characters only), on every change. */
703
+ unmaskedChange = output();
704
+ _masked = '';
705
+ _unmasked = '';
706
+ /** The current formatted value. */
707
+ get value() {
708
+ return this._masked;
709
+ }
710
+ /** The current raw value (token characters only). */
711
+ get unmasked() {
712
+ return this._unmasked;
713
+ }
714
+ onInput() {
715
+ const el = this.host.nativeElement;
716
+ const oldValue = el.value;
717
+ const oldCaret = el.selectionStart ?? oldValue.length;
718
+ const { masked, unmasked } = mkApplyMask(oldValue, this.pattern());
719
+ el.value = masked;
720
+ this.setCaret(el, oldValue, oldCaret, masked);
721
+ if (masked === this._masked)
722
+ return;
723
+ this._masked = masked;
724
+ this._unmasked = unmasked;
725
+ this.maskedChange.emit(masked);
726
+ this.unmaskedChange.emit(unmasked);
727
+ }
728
+ /** Position the caret after the same number of value chars as before. */
729
+ setCaret(el, oldValue, oldCaret, masked) {
730
+ const pos = mkMaskCaret(oldValue, oldCaret, masked);
731
+ try {
732
+ el.setSelectionRange(pos, pos);
733
+ }
734
+ catch {
735
+ // Some input types (e.g. number, email) don't support selection.
736
+ }
737
+ }
738
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkMask, deps: [], target: i0.ɵɵFactoryTarget.Directive });
739
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkMask, isStandalone: true, selector: "[mkMask]", inputs: { pattern: { classPropertyName: "pattern", publicName: "mkMask", isSignal: true, isRequired: true, transformFunction: null }, maskInputmode: { classPropertyName: "maskInputmode", publicName: "mkMaskInputmode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { maskedChange: "maskedChange", unmaskedChange: "unmaskedChange" }, host: { listeners: { "input": "onInput()" }, properties: { "attr.inputmode": "inputMode()" } }, exportAs: ["mkMask"], ngImport: i0 });
740
+ }
741
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkMask, decorators: [{
742
+ type: Directive,
743
+ args: [{
744
+ selector: '[mkMask]',
745
+ exportAs: 'mkMask',
746
+ host: {
747
+ '(input)': 'onInput()',
748
+ '[attr.inputmode]': 'inputMode()',
749
+ },
750
+ }]
751
+ }], propDecorators: { pattern: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkMask", required: true }] }], maskInputmode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkMaskInputmode", required: false }] }], maskedChange: [{ type: i0.Output, args: ["maskedChange"] }], unmaskedChange: [{ type: i0.Output, args: ["unmaskedChange"] }] } });
752
+
753
+ /** The attributes a field preset can control. */
754
+ const MK_FIELD_ATTRS = [
755
+ 'type',
756
+ 'inputmode',
757
+ 'autocomplete',
758
+ 'autocapitalize',
759
+ 'autocorrect',
760
+ 'spellcheck',
761
+ 'enterkeyhint',
762
+ ];
763
+ /**
764
+ * The attribute bundle per field kind. Exported so hosts can inspect what a
765
+ * kind resolves to, or spread an entry into their own preset table.
766
+ *
767
+ * `autocorrect` (non-standard, honoured by iOS Safari) is set to `off`
768
+ * wherever `spellcheck` is `false`.
769
+ */
770
+ const MK_FIELD_PRESETS = {
771
+ email: {
772
+ type: 'email',
773
+ inputmode: 'email',
774
+ autocomplete: 'email',
775
+ autocapitalize: 'off',
776
+ autocorrect: 'off',
777
+ spellcheck: 'false',
778
+ },
779
+ tel: { type: 'tel', inputmode: 'tel', autocomplete: 'tel' },
780
+ url: {
781
+ type: 'url',
782
+ inputmode: 'url',
783
+ autocomplete: 'url',
784
+ autocapitalize: 'off',
785
+ autocorrect: 'off',
786
+ spellcheck: 'false',
787
+ },
788
+ search: {
789
+ type: 'search',
790
+ inputmode: 'search',
791
+ autocomplete: 'off',
792
+ autocapitalize: 'off',
793
+ autocorrect: 'off',
794
+ spellcheck: 'false',
795
+ enterkeyhint: 'search',
796
+ },
797
+ numeric: { inputmode: 'numeric' },
798
+ 'given-name': {
799
+ type: 'text',
800
+ autocomplete: 'given-name',
801
+ autocapitalize: 'words',
802
+ },
803
+ 'family-name': {
804
+ type: 'text',
805
+ autocomplete: 'family-name',
806
+ autocapitalize: 'words',
807
+ },
808
+ name: { type: 'text', autocomplete: 'name', autocapitalize: 'words' },
809
+ organization: {
810
+ type: 'text',
811
+ autocomplete: 'organization',
812
+ autocapitalize: 'words',
813
+ },
814
+ street: {
815
+ type: 'text',
816
+ autocomplete: 'street-address',
817
+ autocapitalize: 'words',
818
+ },
819
+ 'address-line1': {
820
+ type: 'text',
821
+ autocomplete: 'address-line1',
822
+ autocapitalize: 'words',
823
+ },
824
+ 'address-line2': {
825
+ type: 'text',
826
+ autocomplete: 'address-line2',
827
+ autocapitalize: 'words',
828
+ },
829
+ city: {
830
+ type: 'text',
831
+ autocomplete: 'address-level2',
832
+ autocapitalize: 'words',
833
+ },
834
+ region: {
835
+ type: 'text',
836
+ autocomplete: 'address-level1',
837
+ autocapitalize: 'words',
838
+ },
839
+ country: {
840
+ type: 'text',
841
+ autocomplete: 'country-name',
842
+ autocapitalize: 'words',
843
+ },
844
+ 'postal-code': {
845
+ type: 'text',
846
+ autocomplete: 'postal-code',
847
+ autocapitalize: 'characters',
848
+ autocorrect: 'off',
849
+ spellcheck: 'false',
850
+ },
851
+ username: {
852
+ type: 'text',
853
+ autocomplete: 'username',
854
+ autocapitalize: 'off',
855
+ autocorrect: 'off',
856
+ spellcheck: 'false',
857
+ },
858
+ 'one-time-code': {
859
+ type: 'text',
860
+ inputmode: 'numeric',
861
+ autocomplete: 'one-time-code',
862
+ autocapitalize: 'off',
863
+ autocorrect: 'off',
864
+ spellcheck: 'false',
865
+ },
866
+ };
867
+ /**
868
+ * Field — applies the semantic mobile-keyboard and autofill attribute bundle
869
+ * for a kind of field, so you never again ship an email input that iOS
870
+ * capitalises or an address input the browser can't autofill.
871
+ *
872
+ * It sets `type`, `inputmode`, `autocomplete`, `autocapitalize`, `autocorrect`,
873
+ * `spellcheck` and `enterkeyhint` (see {@link MK_FIELD_PRESETS}) and nothing
874
+ * else — no styling, no value handling — so it composes with `mkInput`,
875
+ * `mk-form-field`, `[(ngModel)]` and reactive forms untouched.
876
+ *
877
+ * ```html
878
+ * <input mkInput mkField="email" formControlName="email" />
879
+ * <input mkInput mkField="street" formControlName="street" />
880
+ * <input mkInput [mkField]="isCompany() ? 'organization' : 'name'" />
881
+ * ```
882
+ *
883
+ * ## Precedence
884
+ *
885
+ * A **static** attribute you write on the element wins: the directive reads the
886
+ * seven attributes once at construction (before any host binding has run) and
887
+ * keeps whatever was already there, filling in only what you left out. So
888
+ * `<input mkField="username" type="password" />` stays a password field but
889
+ * still gets `autocomplete="username"`, `autocapitalize="off"`, …
890
+ *
891
+ * A **template binding** for the same attribute (`[attr.autocomplete]="…"`)
892
+ * does *not* win — Angular applies directive host bindings after template
893
+ * bindings, so the preset overwrites it. Use a static attribute to override, or
894
+ * drop `mkField` and set the bundle yourself.
895
+ */
896
+ class MkField {
897
+ el = inject(ElementRef);
898
+ /** Which kind of field this is. */
899
+ kind = input.required({ ...(ngDevMode ? { debugName: "kind" } : /* istanbul ignore next */ {}), alias: 'mkField' });
900
+ /**
901
+ * Attributes the consumer put on the element themselves, read once before
902
+ * any host binding has run — these always win over the preset.
903
+ */
904
+ authored = Object.fromEntries(MK_FIELD_ATTRS.map((name) => [name, this.el.nativeElement.getAttribute(name)]).filter(([, value]) => value !== null));
905
+ /** The resolved attribute bundle: authored value, else preset, else unset. */
906
+ attrs = computed(() => {
907
+ const preset = MK_FIELD_PRESETS[this.kind()] ?? {};
908
+ const resolved = {};
909
+ for (const name of MK_FIELD_ATTRS) {
910
+ resolved[name] = this.authored[name] ?? preset[name] ?? null;
911
+ }
912
+ return resolved;
913
+ }, /* @ts-ignore */
914
+ ...(ngDevMode ? [{ debugName: "attrs" }] : /* istanbul ignore next */ []));
915
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
916
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkField, isStandalone: true, selector: "[mkField]", inputs: { kind: { classPropertyName: "kind", publicName: "mkField", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.type": "attrs().type", "attr.inputmode": "attrs().inputmode", "attr.autocomplete": "attrs().autocomplete", "attr.autocapitalize": "attrs().autocapitalize", "attr.autocorrect": "attrs().autocorrect", "attr.spellcheck": "attrs().spellcheck", "attr.enterkeyhint": "attrs().enterkeyhint" } }, ngImport: i0 });
917
+ }
918
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkField, decorators: [{
919
+ type: Directive,
920
+ args: [{
921
+ selector: '[mkField]',
922
+ host: {
923
+ '[attr.type]': 'attrs().type',
924
+ '[attr.inputmode]': 'attrs().inputmode',
925
+ '[attr.autocomplete]': 'attrs().autocomplete',
926
+ '[attr.autocapitalize]': 'attrs().autocapitalize',
927
+ '[attr.autocorrect]': 'attrs().autocorrect',
928
+ '[attr.spellcheck]': 'attrs().spellcheck',
929
+ '[attr.enterkeyhint]': 'attrs().enterkeyhint',
930
+ },
931
+ }]
932
+ }], propDecorators: { kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkField", required: true }] }] } });
933
+
934
+ /**
935
+ * MkHistoryStack — a pure, signal-based, linear undo/redo command stack with
936
+ * no Angular DI involved. Instantiate it directly for a local editing session
937
+ * (a dialog, a canvas, a form wizard), or use the app-wide singleton
938
+ * {@link MkHistoryService}.
939
+ *
940
+ * ```ts
941
+ * const stack = new MkHistoryStack();
942
+ * deleteRow(row); // 1. do the work yourself
943
+ * stack.push({ // 2. record how to revert/redo it
944
+ * label: 'Delete row',
945
+ * undo: () => insertRow(row),
946
+ * redo: () => deleteRow(row),
947
+ * });
948
+ * stack.undo(); // row is back
949
+ * ```
950
+ *
951
+ * ### Linear history
952
+ * `push()` clears the redo branch — after undoing twice and pushing a new
953
+ * entry, the two undone entries are gone (the standard editor model).
954
+ *
955
+ * ### ⚠️ Re-entrancy guard — the classic footgun
956
+ * Pushes made **while** `undo()` or `redo()` is executing are **silently
957
+ * ignored**. Naive consumers often record history from a generic change
958
+ * handler (`(cellEdit)`, `valueChanges`, …); when an undo replays the old
959
+ * value, that same handler fires again and would push a mirror entry,
960
+ * corrupting the stack into an undo/redo ping-pong. The guard makes that
961
+ * pattern safe by construction. If you need to record something new in
962
+ * response to an undo, do it after `undo()` returns.
963
+ */
964
+ class MkHistoryStack {
965
+ undoStack = signal([], /* @ts-ignore */
966
+ ...(ngDevMode ? [{ debugName: "undoStack" }] : /* istanbul ignore next */ []));
967
+ redoStack = signal([], /* @ts-ignore */
968
+ ...(ngDevMode ? [{ debugName: "redoStack" }] : /* istanbul ignore next */ []));
969
+ /** True while an entry's `undo()`/`redo()` is running (re-entrancy guard). */
970
+ replaying = false;
971
+ /** Depth of nested `batch()` calls; > 0 while a batch is collecting. */
972
+ batchDepth = 0;
973
+ batchLabel = '';
974
+ batchBuffer = [];
975
+ maxSize = 100;
976
+ /** True when at least one entry can be undone. */
977
+ canUndo = computed(() => this.undoStack().length > 0, /* @ts-ignore */
978
+ ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
979
+ /** True when at least one undone entry can be redone. */
980
+ canRedo = computed(() => this.redoStack().length > 0, /* @ts-ignore */
981
+ ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
982
+ /** Label of the entry `undo()` would revert, or `null` ("Undo *Delete row*"). */
983
+ undoLabel = computed(() => this.undoStack().at(-1)?.label ?? null, /* @ts-ignore */
984
+ ...(ngDevMode ? [{ debugName: "undoLabel" }] : /* istanbul ignore next */ []));
985
+ /** Label of the entry `redo()` would re-apply, or `null`. */
986
+ redoLabel = computed(() => this.redoStack().at(-1)?.label ?? null, /* @ts-ignore */
987
+ ...(ngDevMode ? [{ debugName: "redoLabel" }] : /* istanbul ignore next */ []));
988
+ /** Number of undoable entries currently on the stack. */
989
+ size = computed(() => this.undoStack().length, /* @ts-ignore */
990
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
991
+ /**
992
+ * Maximum number of undoable entries kept (default `100`). When exceeded the
993
+ * **oldest** entries are dropped. Settable at any time; lowering it trims
994
+ * the stack immediately. Values below 1 are clamped to 1.
995
+ */
996
+ get limit() {
997
+ return this.maxSize;
998
+ }
999
+ set limit(value) {
1000
+ this.maxSize = Math.max(1, Math.floor(value));
1001
+ this.undoStack.update((stack) => stack.length > this.maxSize ? stack.slice(stack.length - this.maxSize) : stack);
1002
+ }
1003
+ /**
1004
+ * Record an already-performed action. Clears the redo branch and evicts the
1005
+ * oldest entry beyond {@link limit}. Ignored while `undo()`/`redo()` is
1006
+ * executing (see the re-entrancy note on the class) — inside a `batch()` the
1007
+ * entry is collected into the composite instead.
1008
+ */
1009
+ push(entry) {
1010
+ if (this.replaying)
1011
+ return;
1012
+ if (this.batchDepth > 0) {
1013
+ this.batchBuffer.push(entry);
1014
+ return;
1015
+ }
1016
+ this.commit(entry);
1017
+ }
1018
+ /** Undo the most recent entry. Returns `false` when there is nothing to undo. */
1019
+ undo() {
1020
+ const entry = this.undoStack().at(-1);
1021
+ if (!entry)
1022
+ return false;
1023
+ this.undoStack.update((stack) => stack.slice(0, -1));
1024
+ this.replaying = true;
1025
+ try {
1026
+ entry.undo();
1027
+ }
1028
+ finally {
1029
+ this.replaying = false;
1030
+ }
1031
+ this.redoStack.update((stack) => [...stack, entry]);
1032
+ return true;
1033
+ }
1034
+ /** Redo the most recently undone entry. Returns `false` when there is nothing to redo. */
1035
+ redo() {
1036
+ const entry = this.redoStack().at(-1);
1037
+ if (!entry)
1038
+ return false;
1039
+ this.redoStack.update((stack) => stack.slice(0, -1));
1040
+ this.replaying = true;
1041
+ try {
1042
+ entry.redo();
1043
+ }
1044
+ finally {
1045
+ this.replaying = false;
1046
+ }
1047
+ this.undoStack.update((stack) => [...stack, entry]);
1048
+ return true;
1049
+ }
1050
+ /** Drop all entries (both branches) and any in-flight batch collection. */
1051
+ clear() {
1052
+ this.undoStack.set([]);
1053
+ this.redoStack.set([]);
1054
+ this.batchBuffer = [];
1055
+ }
1056
+ /**
1057
+ * Group every `push()` made during `work()` into **one** undoable step.
1058
+ * Undo runs the children in reverse order; redo runs them forward. Nested
1059
+ * `batch()` calls flatten into the outermost batch (its label wins). An
1060
+ * empty batch records nothing. Returns `work()`'s result; if `work()`
1061
+ * throws, entries pushed before the throw are still committed.
1062
+ */
1063
+ batch(label, work) {
1064
+ if (this.replaying)
1065
+ return work();
1066
+ this.batchDepth++;
1067
+ if (this.batchDepth === 1) {
1068
+ this.batchLabel = label;
1069
+ this.batchBuffer = [];
1070
+ }
1071
+ try {
1072
+ return work();
1073
+ }
1074
+ finally {
1075
+ this.batchDepth--;
1076
+ if (this.batchDepth === 0 && this.batchBuffer.length > 0) {
1077
+ const children = this.batchBuffer;
1078
+ this.batchBuffer = [];
1079
+ this.commit({
1080
+ label: this.batchLabel,
1081
+ undo: () => {
1082
+ for (let i = children.length - 1; i >= 0; i--)
1083
+ children[i].undo();
1084
+ },
1085
+ redo: () => {
1086
+ for (const child of children)
1087
+ child.redo();
1088
+ },
1089
+ });
1090
+ }
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Create an **independent** stack with the same API — e.g. for a dialog's
1095
+ * local editing session that should not pollute the app-wide history.
1096
+ * Discard it (or `clear()` it) when the session ends.
1097
+ */
1098
+ createScope() {
1099
+ return new MkHistoryStack();
1100
+ }
1101
+ /** Append to the undo stack: clear the redo branch, evict beyond the limit. */
1102
+ commit(entry) {
1103
+ this.redoStack.set([]);
1104
+ this.undoStack.update((stack) => {
1105
+ const next = [...stack, entry];
1106
+ return next.length > this.maxSize ? next.slice(next.length - this.maxSize) : next;
1107
+ });
1108
+ }
1109
+ }
1110
+ /**
1111
+ * MkHistoryService — the app-wide {@link MkHistoryStack} singleton. Inject it
1112
+ * anywhere; call {@link MkHistoryStack.createScope} for an isolated local
1113
+ * stack, or wire `mod+z` / `mod+shift+z` / `mod+y` with
1114
+ * {@link registerHistoryHotkeys}.
1115
+ *
1116
+ * ### Recipe — undoable `mk-table` cell edits
1117
+ * Wire the table's `(cellEdit)` output so each saved cell becomes one
1118
+ * undoable step. Apply the edit yourself first (`push()` records an action
1119
+ * that already happened), and match rows by a stable id — the row object is
1120
+ * replaced by the immutable update:
1121
+ *
1122
+ * ```ts
1123
+ * // <mk-table [rows]="rows()" [columns]="columns" (cellEdit)="onCellEdit($event)" />
1124
+ * private readonly history = inject(MkHistoryService);
1125
+ * readonly rows = signal<Order[]>([...]);
1126
+ *
1127
+ * onCellEdit(edit: MkCellEdit<Order>): void {
1128
+ * const previous = edit.row[edit.key as keyof Order];
1129
+ * const apply = (value: unknown) =>
1130
+ * this.rows.update((rows) =>
1131
+ * rows.map((r) => (r.id === edit.row.id ? { ...r, [edit.key]: value } : r)),
1132
+ * );
1133
+ * apply(edit.value); // first execution is the caller's job
1134
+ * this.history.push({
1135
+ * label: `Edit ${edit.key}`,
1136
+ * undo: () => apply(previous),
1137
+ * redo: () => apply(edit.value),
1138
+ * });
1139
+ * }
1140
+ * ```
1141
+ *
1142
+ * Undoing replays the old value through the same `apply` function; if your
1143
+ * change pipeline re-emits into `onCellEdit` during that replay, the
1144
+ * re-entrancy guard drops the spurious `push()` (see {@link MkHistoryStack}).
1145
+ */
1146
+ class MkHistoryService extends MkHistoryStack {
1147
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHistoryService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1148
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHistoryService, providedIn: 'root' });
1149
+ }
1150
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHistoryService, decorators: [{
1151
+ type: Injectable,
1152
+ args: [{ providedIn: 'root' }]
1153
+ }] });
1154
+
1155
+ /** Tokens that count as modifiers rather than the trigger key. */
1156
+ const MODIFIERS = new Set(['mod', 'ctrl', 'control', 'meta', 'cmd', 'command', 'alt', 'option', 'shift']);
1157
+ /** Human-friendly aliases mapped onto their `KeyboardEvent.key` (lowercased) form. */
1158
+ const KEY_ALIASES = {
1159
+ esc: 'escape',
1160
+ space: ' ',
1161
+ spacebar: ' ',
1162
+ up: 'arrowup',
1163
+ down: 'arrowdown',
1164
+ left: 'arrowleft',
1165
+ right: 'arrowright',
1166
+ del: 'delete',
1167
+ return: 'enter',
1168
+ plus: '+',
1169
+ };
1170
+ /** Keys that are themselves modifiers — ignored as sequence/trigger keys. */
1171
+ const MODIFIER_KEYS = new Set(['control', 'shift', 'alt', 'meta', 'os', 'altgraph']);
1172
+ /** True on Apple platforms, where `mod` maps to the Command (meta) key. */
1173
+ function mkIsMacPlatform() {
1174
+ return typeof navigator !== 'undefined' && (navigator.platform ?? '').includes('Mac');
1175
+ }
1176
+ /**
1177
+ * Parse a single hotkey combo (e.g. `'mod+k'`, `'ctrl+shift+p'`, `'?'`) into a
1178
+ * structured {@link MkParsedHotkey}. Tokens are joined by `+`; the last
1179
+ * non-modifier token is the trigger key. Pure and SSR-safe.
1180
+ *
1181
+ * For a chord (space-separated sequence like `'g i'`), split on whitespace and
1182
+ * parse each step — {@link MkHotkeysService} does this for you.
1183
+ */
1184
+ function mkParseHotkey(combo) {
1185
+ const parsed = {
1186
+ key: '',
1187
+ mod: false,
1188
+ ctrl: false,
1189
+ meta: false,
1190
+ alt: false,
1191
+ shift: false,
1192
+ };
1193
+ const tokens = combo
1194
+ .trim()
1195
+ .split('+')
1196
+ .map((t) => t.trim().toLowerCase())
1197
+ .filter((t) => t.length > 0);
1198
+ for (const token of tokens) {
1199
+ switch (token) {
1200
+ case 'mod':
1201
+ parsed.mod = true;
1202
+ break;
1203
+ case 'ctrl':
1204
+ case 'control':
1205
+ parsed.ctrl = true;
1206
+ break;
1207
+ case 'meta':
1208
+ case 'cmd':
1209
+ case 'command':
1210
+ parsed.meta = true;
1211
+ break;
1212
+ case 'alt':
1213
+ case 'option':
1214
+ parsed.alt = true;
1215
+ break;
1216
+ case 'shift':
1217
+ parsed.shift = true;
1218
+ break;
1219
+ default:
1220
+ parsed.key = KEY_ALIASES[token] ?? token;
1221
+ }
1222
+ }
1223
+ return parsed;
1224
+ }
1225
+ /**
1226
+ * Does a `keydown` event satisfy a hotkey? `combo` may be a raw string (parsed
1227
+ * on the fly) or an already-parsed {@link MkParsedHotkey}.
1228
+ *
1229
+ * The key is compared case-insensitively; modifier flags must match exactly.
1230
+ * `mod` resolves to Meta on macOS and Ctrl elsewhere. Shift is not enforced for
1231
+ * symbol keys (e.g. `'?'`), whose shifted state varies by keyboard layout.
1232
+ */
1233
+ function mkMatchesHotkey(event, combo) {
1234
+ const parsed = typeof combo === 'string' ? mkParseHotkey(combo) : combo;
1235
+ if (!parsed.key)
1236
+ return false;
1237
+ if (event.key.toLowerCase() !== parsed.key)
1238
+ return false;
1239
+ const mac = mkIsMacPlatform();
1240
+ const wantCtrl = parsed.ctrl || (parsed.mod && !mac);
1241
+ const wantMeta = parsed.meta || (parsed.mod && mac);
1242
+ if (event.ctrlKey !== wantCtrl)
1243
+ return false;
1244
+ if (event.metaKey !== wantMeta)
1245
+ return false;
1246
+ if (event.altKey !== parsed.alt)
1247
+ return false;
1248
+ // Shift: enforce when requested; for symbol keys (non-alphanumeric single
1249
+ // chars) the shifted state is layout-dependent, so don't enforce its absence.
1250
+ if (parsed.shift) {
1251
+ if (!event.shiftKey)
1252
+ return false;
1253
+ }
1254
+ else {
1255
+ const symbolKey = parsed.key.length === 1 && !/[a-z0-9]/i.test(parsed.key);
1256
+ if (!symbolKey && event.shiftKey)
1257
+ return false;
1258
+ }
1259
+ return true;
1260
+ }
1261
+ /** Window (ms) to complete a two-step chord before the pending key is dropped. */
1262
+ const SEQUENCE_TIMEOUT = 1000;
1263
+ /**
1264
+ * MkHotkeysService — app-wide keyboard shortcuts. Register a combo string and a
1265
+ * handler; a single `keydown` listener (attached lazily on the first
1266
+ * registration, browser only) dispatches to every match.
1267
+ *
1268
+ * Supports single combos (`'mod+k'`, `'ctrl+shift+p'`, `'?'`) and simple
1269
+ * two-step chords (`'g i'`). By default hotkeys are ignored while an editable
1270
+ * field is focused — pass `allowInInput` to opt in per registration.
1271
+ *
1272
+ * ```ts
1273
+ * const off = hotkeys.register('mod+k', (e) => openPalette(), { preventDefault: true });
1274
+ * // …later
1275
+ * off();
1276
+ * ```
1277
+ */
1278
+ class MkHotkeysService {
1279
+ document = inject(DOCUMENT);
1280
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1281
+ registrations = new Set();
1282
+ attached = false;
1283
+ /** The first key of an in-progress chord, awaiting its second key. */
1284
+ pendingEvent = null;
1285
+ pendingTimer;
1286
+ /**
1287
+ * Register `combo` → `handler`. Returns a disposer that unregisters it.
1288
+ * `combo` may be a single combo or a space-separated two-step chord.
1289
+ */
1290
+ register(combo, handler, options = {}) {
1291
+ const steps = combo
1292
+ .trim()
1293
+ .split(/\s+/)
1294
+ .filter((s) => s.length > 0)
1295
+ .map(mkParseHotkey);
1296
+ const registration = {
1297
+ steps,
1298
+ handler,
1299
+ preventDefault: options.preventDefault ?? false,
1300
+ allowInInput: options.allowInInput ?? false,
1301
+ };
1302
+ this.registrations.add(registration);
1303
+ this.ensureAttached();
1304
+ return () => {
1305
+ this.registrations.delete(registration);
1306
+ if (this.registrations.size === 0)
1307
+ this.detach();
1308
+ };
1309
+ }
1310
+ /** Remove every registration and detach the listener. */
1311
+ unregisterAll() {
1312
+ this.registrations.clear();
1313
+ this.clearPending();
1314
+ this.detach();
1315
+ }
1316
+ ensureAttached() {
1317
+ if (this.attached || !this.isBrowser)
1318
+ return;
1319
+ this.document.addEventListener('keydown', this.onKeydown, true);
1320
+ this.attached = true;
1321
+ }
1322
+ detach() {
1323
+ if (!this.attached)
1324
+ return;
1325
+ this.document.removeEventListener('keydown', this.onKeydown, true);
1326
+ this.attached = false;
1327
+ }
1328
+ onKeydown = (event) => {
1329
+ const e = event;
1330
+ // Bare modifier presses neither trigger nor advance a chord.
1331
+ if (MODIFIER_KEYS.has(e.key.toLowerCase()))
1332
+ return;
1333
+ const editable = this.isEditable(e.target);
1334
+ // 1. Try to complete an in-progress chord.
1335
+ if (this.pendingEvent) {
1336
+ const first = this.pendingEvent;
1337
+ let completed = false;
1338
+ for (const reg of this.registrations) {
1339
+ if (reg.steps.length !== 2)
1340
+ continue;
1341
+ if (editable && !reg.allowInInput)
1342
+ continue;
1343
+ if (mkMatchesHotkey(first, reg.steps[0]) && mkMatchesHotkey(e, reg.steps[1])) {
1344
+ this.trigger(reg, e);
1345
+ completed = true;
1346
+ }
1347
+ }
1348
+ this.clearPending();
1349
+ if (completed)
1350
+ return;
1351
+ // Otherwise fall through: this key may fire a single hotkey or open a chord.
1352
+ }
1353
+ // 2. Single-combo matches.
1354
+ let matched = false;
1355
+ for (const reg of this.registrations) {
1356
+ if (reg.steps.length !== 1)
1357
+ continue;
1358
+ if (editable && !reg.allowInInput)
1359
+ continue;
1360
+ if (mkMatchesHotkey(e, reg.steps[0])) {
1361
+ this.trigger(reg, e);
1362
+ matched = true;
1363
+ }
1364
+ }
1365
+ if (matched)
1366
+ return;
1367
+ // 3. Open a chord if this key matches the first step of any two-step combo.
1368
+ for (const reg of this.registrations) {
1369
+ if (reg.steps.length !== 2)
1370
+ continue;
1371
+ if (editable && !reg.allowInInput)
1372
+ continue;
1373
+ if (mkMatchesHotkey(e, reg.steps[0])) {
1374
+ this.pendingEvent = e;
1375
+ this.pendingTimer = setTimeout(() => this.clearPending(), SEQUENCE_TIMEOUT);
1376
+ return;
1377
+ }
1378
+ }
1379
+ };
1380
+ trigger(reg, e) {
1381
+ if (reg.preventDefault)
1382
+ e.preventDefault();
1383
+ reg.handler(e);
1384
+ }
1385
+ clearPending() {
1386
+ this.pendingEvent = null;
1387
+ if (this.pendingTimer) {
1388
+ clearTimeout(this.pendingTimer);
1389
+ this.pendingTimer = undefined;
1390
+ }
1391
+ }
1392
+ isEditable(target) {
1393
+ const el = target;
1394
+ if (!el || typeof el.tagName !== 'string')
1395
+ return false;
1396
+ const tag = el.tagName;
1397
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
1398
+ return true;
1399
+ return el.isContentEditable === true;
1400
+ }
1401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHotkeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1402
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHotkeysService, providedIn: 'root' });
1403
+ }
1404
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHotkeysService, decorators: [{
1405
+ type: Injectable,
1406
+ args: [{ providedIn: 'root' }]
1407
+ }] });
1408
+
1409
+ /**
1410
+ * Opt-in helper wiring the standard editor shortcuts to a history stack
1411
+ * through {@link MkHotkeysService}:
1412
+ *
1413
+ * - `mod+z` → `undo()`
1414
+ * - `mod+shift+z` **and** `mod+y` → `redo()`
1415
+ *
1416
+ * Must be called in an **injection context** (constructor, field initializer,
1417
+ * or `runInInjectionContext`). Nothing is registered automatically — apps own
1418
+ * their shortcuts and call this where (and if) they want them:
1419
+ *
1420
+ * ```ts
1421
+ * export class AppShell {
1422
+ * private readonly disposeHistoryKeys = registerHistoryHotkeys();
1423
+ * }
1424
+ * ```
1425
+ *
1426
+ * Pass a specific stack (e.g. a `createScope()` stack for a dialog) to bind
1427
+ * the shortcuts to it instead of the app-wide {@link MkHistoryService}.
1428
+ *
1429
+ * Returns a dispose function that unregisters all three shortcuts; it is also
1430
+ * called automatically when the current injection context is destroyed.
1431
+ *
1432
+ * Notes:
1433
+ * - `MkHotkeysService` ignores keydowns while an editable element
1434
+ * (input/textarea/select/contentEditable) is focused, so native text-field
1435
+ * undo keeps working — no extra handling needed here.
1436
+ * - The shortcuts register with `preventDefault` so the browser's own
1437
+ * history/document undo does not fire alongside yours.
1438
+ */
1439
+ function registerHistoryHotkeys(history = inject(MkHistoryService)) {
1440
+ const hotkeys = inject(MkHotkeysService);
1441
+ const disposers = [
1442
+ hotkeys.register('mod+z', () => void history.undo(), { preventDefault: true }),
1443
+ hotkeys.register('mod+shift+z', () => void history.redo(), { preventDefault: true }),
1444
+ hotkeys.register('mod+y', () => void history.redo(), { preventDefault: true }),
1445
+ ];
1446
+ let disposed = false;
1447
+ const dispose = () => {
1448
+ if (disposed)
1449
+ return;
1450
+ disposed = true;
1451
+ for (const off of disposers)
1452
+ off();
1453
+ };
1454
+ inject(DestroyRef, { optional: true })?.onDestroy(dispose);
1455
+ return dispose;
1456
+ }
1457
+
1458
+ /**
1459
+ * `aria-keyshortcuts` names (UI Events `KeyboardEvent.key` values) keyed by
1460
+ * the lowercase form {@link mkParseHotkey} produces. Space and plus have
1461
+ * dedicated names because they collide with the ARIA separators.
1462
+ */
1463
+ const ARIA_KEY_NAMES = {
1464
+ ' ': 'Space',
1465
+ '+': 'Plus',
1466
+ escape: 'Escape',
1467
+ enter: 'Enter',
1468
+ tab: 'Tab',
1469
+ backspace: 'Backspace',
1470
+ delete: 'Delete',
1471
+ insert: 'Insert',
1472
+ home: 'Home',
1473
+ end: 'End',
1474
+ pageup: 'PageUp',
1475
+ pagedown: 'PageDown',
1476
+ arrowup: 'ArrowUp',
1477
+ arrowdown: 'ArrowDown',
1478
+ arrowleft: 'ArrowLeft',
1479
+ arrowright: 'ArrowRight',
1480
+ };
1481
+ /** Map a parsed (lowercase) key to its `aria-keyshortcuts` spelling. */
1482
+ function ariaKeyName(key) {
1483
+ const mapped = ARIA_KEY_NAMES[key];
1484
+ if (mapped)
1485
+ return mapped;
1486
+ if (/^f\d{1,2}$/.test(key))
1487
+ return key.toUpperCase(); // F1 … F12
1488
+ return key.length === 1 ? key.toUpperCase() : key;
1489
+ }
1490
+ /**
1491
+ * One parsed combo as an ARIA `+`-joined token, e.g. `Control+Shift+P`.
1492
+ * `mod` resolves the same way the matcher does: Meta on macOS, Control
1493
+ * elsewhere ({@link mkIsMacPlatform}).
1494
+ */
1495
+ function ariaCombo(parsed) {
1496
+ const mac = mkIsMacPlatform();
1497
+ const tokens = [];
1498
+ if (parsed.ctrl || (parsed.mod && !mac))
1499
+ tokens.push('Control');
1500
+ if (parsed.alt)
1501
+ tokens.push('Alt');
1502
+ if (parsed.shift)
1503
+ tokens.push('Shift');
1504
+ if (parsed.meta || (parsed.mod && mac))
1505
+ tokens.push('Meta');
1506
+ if (parsed.key)
1507
+ tokens.push(ariaKeyName(parsed.key));
1508
+ return tokens.join('+');
1509
+ }
1510
+ /**
1511
+ * `[mkHotkey]` — declaratively bind a keyboard shortcut to a host element.
1512
+ *
1513
+ * When the combo fires, a `<button>` or `<a>` host is `click()`ed; any other
1514
+ * host emits `(mkHotkeyPressed)`. Registration is scoped to the element's
1515
+ * lifetime (auto-unregistered on destroy) via {@link MkHotkeysService}.
1516
+ *
1517
+ * ```html
1518
+ * <button mkHotkey="mod+s" (click)="save()">Save</button>
1519
+ * <div mkHotkey="?" (mkHotkeyPressed)="showHelp()">…</div>
1520
+ * ```
1521
+ */
1522
+ class MkHotkey {
1523
+ host = inject(ElementRef);
1524
+ hotkeys = inject(MkHotkeysService);
1525
+ /** The combo (or space-separated chord) to bind, e.g. `mod+k`, `g i`, `?`. */
1526
+ mkHotkey = input.required(/* @ts-ignore */
1527
+ ...(ngDevMode ? [{ debugName: "mkHotkey" }] : /* istanbul ignore next */ []));
1528
+ /** Fire even while an editable field is focused. */
1529
+ mkHotkeyAllowInInput = input(false, { ...(ngDevMode ? { debugName: "mkHotkeyAllowInInput" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1530
+ /** Call `preventDefault()` when the hotkey fires. Defaults to `true`. */
1531
+ mkHotkeyPreventDefault = input(true, { ...(ngDevMode ? { debugName: "mkHotkeyPreventDefault" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1532
+ /** Emitted when the hotkey fires and the host is not a button/link. */
1533
+ mkHotkeyPressed = output();
1534
+ /**
1535
+ * The combo in `aria-keyshortcuts` syntax — per-step tokens `+`-joined
1536
+ * (`Control+K`), chord steps space-separated (`G I`). Exposed on the host.
1537
+ */
1538
+ ariaKeyshortcuts = computed(() => {
1539
+ const combos = this.mkHotkey()
1540
+ .trim()
1541
+ .split(/\s+/)
1542
+ .filter((s) => s.length > 0)
1543
+ .map((step) => ariaCombo(mkParseHotkey(step)))
1544
+ .filter((s) => s.length > 0);
1545
+ return combos.length ? combos.join(' ') : null;
1546
+ }, /* @ts-ignore */
1547
+ ...(ngDevMode ? [{ debugName: "ariaKeyshortcuts" }] : /* istanbul ignore next */ []));
1548
+ dispose;
1549
+ constructor() {
1550
+ afterNextRender(() => {
1551
+ this.dispose = this.hotkeys.register(this.mkHotkey(), (e) => this.onTrigger(e), {
1552
+ allowInInput: this.mkHotkeyAllowInInput(),
1553
+ preventDefault: this.mkHotkeyPreventDefault(),
1554
+ });
1555
+ });
1556
+ }
1557
+ onTrigger(e) {
1558
+ const el = this.host.nativeElement;
1559
+ const tag = el.tagName;
1560
+ if (tag === 'BUTTON' || tag === 'A') {
1561
+ el.click();
1562
+ return;
1563
+ }
1564
+ this.mkHotkeyPressed.emit(e);
1565
+ }
1566
+ ngOnDestroy() {
1567
+ this.dispose?.();
1568
+ }
1569
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHotkey, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1570
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkHotkey, isStandalone: true, selector: "[mkHotkey]", inputs: { mkHotkey: { classPropertyName: "mkHotkey", publicName: "mkHotkey", isSignal: true, isRequired: true, transformFunction: null }, mkHotkeyAllowInInput: { classPropertyName: "mkHotkeyAllowInInput", publicName: "mkHotkeyAllowInInput", isSignal: true, isRequired: false, transformFunction: null }, mkHotkeyPreventDefault: { classPropertyName: "mkHotkeyPreventDefault", publicName: "mkHotkeyPreventDefault", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mkHotkeyPressed: "mkHotkeyPressed" }, host: { properties: { "attr.aria-keyshortcuts": "ariaKeyshortcuts()" } }, exportAs: ["mkHotkey"], ngImport: i0 });
1571
+ }
1572
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHotkey, decorators: [{
1573
+ type: Directive,
1574
+ args: [{
1575
+ selector: '[mkHotkey]',
1576
+ exportAs: 'mkHotkey',
1577
+ host: {
1578
+ '[attr.aria-keyshortcuts]': 'ariaKeyshortcuts()',
1579
+ },
1580
+ }]
1581
+ }], ctorParameters: () => [], propDecorators: { mkHotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkHotkey", required: true }] }], mkHotkeyAllowInInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkHotkeyAllowInInput", required: false }] }], mkHotkeyPreventDefault: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkHotkeyPreventDefault", required: false }] }], mkHotkeyPressed: [{ type: i0.Output, args: ["mkHotkeyPressed"] }] } });
1582
+
1583
+ /**
1584
+ * Permission policy — the contract the permission directives (`*mkCan`,
1585
+ * `*mkCannot`, `[mkCanDisable]`) check against. Provide an implementation
1586
+ * once at bootstrap; the directives inject it optionally and, when no policy
1587
+ * is provided at all, treat every permission as **granted** — an app that
1588
+ * never wires permissions must not have its whole UI hidden.
1589
+ *
1590
+ * `can` may return a plain `boolean` (static role checks) or a
1591
+ * `Signal<boolean>` (live permissions). The directives normalise both, and a
1592
+ * signal-returning policy re-evaluates reactively: views appear/disappear and
1593
+ * controls enable/disable the moment the underlying permissions change.
1594
+ *
1595
+ * ```ts
1596
+ * // A signal-based policy fed from an auth service:
1597
+ * @Injectable()
1598
+ * export class AppPermissionPolicy extends MkPermissionPolicy {
1599
+ * private readonly auth = inject(AuthService);
1600
+ *
1601
+ * can(permission: string): Signal<boolean> {
1602
+ * // auth.permissions is a Signal<string[]> that updates on login/logout.
1603
+ * return computed(() => this.auth.permissions().includes(permission));
1604
+ * }
1605
+ * }
1606
+ *
1607
+ * bootstrapApplication(App, {
1608
+ * providers: [
1609
+ * { provide: MkPermissionPolicy, useClass: AppPermissionPolicy },
1610
+ * ],
1611
+ * });
1612
+ * ```
1613
+ *
1614
+ * A static policy is just as valid:
1615
+ *
1616
+ * ```ts
1617
+ * class StaticPolicy extends MkPermissionPolicy {
1618
+ * can(permission: string): boolean {
1619
+ * return CURRENT_USER.permissions.includes(permission);
1620
+ * }
1621
+ * }
1622
+ * ```
1623
+ */
1624
+ class MkPermissionPolicy {
1625
+ }
1626
+ /**
1627
+ * Resolve a policy verdict to a boolean, unwrapping signal-based results.
1628
+ * Call it inside a reactive context (`computed` / `effect`) so a
1629
+ * signal-returning policy stays live. A missing policy grants everything.
1630
+ */
1631
+ function mkPermissionGranted(policy, permission) {
1632
+ if (!policy)
1633
+ return true;
1634
+ const verdict = policy.can(permission);
1635
+ return typeof verdict === 'boolean' ? verdict : verdict();
1636
+ }
1637
+
1638
+ /**
1639
+ * Shared view management for `*mkCan` / `*mkCannot`. Must be called from the
1640
+ * directive's constructor (an injection context). Creates the embedded view
1641
+ * while the (possibly negated) verdict holds, the `else` template while it
1642
+ * does not, and destroys/recreates views when a signal-based policy flips.
1643
+ */
1644
+ function setupPermissionView(permission, fallback, negate) {
1645
+ const policy = inject(MkPermissionPolicy, { optional: true });
1646
+ const templateRef = inject(TemplateRef);
1647
+ const viewContainer = inject(ViewContainerRef);
1648
+ const visible = computed(() => {
1649
+ const granted = mkPermissionGranted(policy, permission());
1650
+ return negate ? !granted : granted;
1651
+ }, /* @ts-ignore */
1652
+ ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1653
+ // Tracks the currently rendered template so a change to the *other* branch
1654
+ // (e.g. the else template resolving while the view is shown) is a no-op.
1655
+ let rendered = null;
1656
+ effect(() => {
1657
+ const next = visible() ? templateRef : fallback();
1658
+ if (next === rendered)
1659
+ return;
1660
+ rendered = next;
1661
+ viewContainer.clear();
1662
+ if (next)
1663
+ viewContainer.createEmbeddedView(next);
1664
+ });
1665
+ }
1666
+ /**
1667
+ * Structural directive that renders its template only while the app's
1668
+ * {@link MkPermissionPolicy} grants the given permission. With a
1669
+ * signal-based policy the check is live — the view is created and destroyed
1670
+ * as permissions change. When no policy is provided, everything is granted.
1671
+ *
1672
+ * ```html
1673
+ * <button *mkCan="'posts.delete'" (click)="remove()">Delete</button>
1674
+ *
1675
+ * <!-- With a fallback template, like *ngIf's else: -->
1676
+ * <section *mkCan="'billing.view'; else locked">…</section>
1677
+ * <ng-template #locked><mk-empty-state title="No access" /></ng-template>
1678
+ * ```
1679
+ */
1680
+ class MkCan {
1681
+ /** The permission key to check, e.g. `'posts.delete'`. */
1682
+ permission = input.required({ ...(ngDevMode ? { debugName: "permission" } : /* istanbul ignore next */ {}), alias: 'mkCan' });
1683
+ /** Template rendered while the permission is denied (`; else ref`). */
1684
+ elseTemplate = input(null, { ...(ngDevMode ? { debugName: "elseTemplate" } : /* istanbul ignore next */ {}), alias: 'mkCanElse' });
1685
+ constructor() {
1686
+ setupPermissionView(this.permission, this.elseTemplate, false);
1687
+ }
1688
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCan, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1689
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkCan, isStandalone: true, selector: "[mkCan]", inputs: { permission: { classPropertyName: "permission", publicName: "mkCan", isSignal: true, isRequired: true, transformFunction: null }, elseTemplate: { classPropertyName: "elseTemplate", publicName: "mkCanElse", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1690
+ }
1691
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCan, decorators: [{
1692
+ type: Directive,
1693
+ args: [{ selector: '[mkCan]' }]
1694
+ }], ctorParameters: () => [], propDecorators: { permission: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCan", required: true }] }], elseTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCanElse", required: false }] }] } });
1695
+ /**
1696
+ * The negation of {@link MkCan}: renders its template only while the
1697
+ * permission is **denied** — for upsell hints, "ask an admin" notes and the
1698
+ * like. With no policy provided everything is granted, so `*mkCannot`
1699
+ * renders nothing.
1700
+ *
1701
+ * ```html
1702
+ * <p *mkCannot="'reports.export'">Exporting requires an admin role.</p>
1703
+ * ```
1704
+ */
1705
+ class MkCannot {
1706
+ /** The permission key to check. */
1707
+ permission = input.required({ ...(ngDevMode ? { debugName: "permission" } : /* istanbul ignore next */ {}), alias: 'mkCannot' });
1708
+ /** Template rendered while the permission is granted (`; else ref`). */
1709
+ elseTemplate = input(null, { ...(ngDevMode ? { debugName: "elseTemplate" } : /* istanbul ignore next */ {}), alias: 'mkCannotElse' });
1710
+ constructor() {
1711
+ setupPermissionView(this.permission, this.elseTemplate, true);
1712
+ }
1713
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCannot, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1714
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkCannot, isStandalone: true, selector: "[mkCannot]", inputs: { permission: { classPropertyName: "permission", publicName: "mkCannot", isSignal: true, isRequired: true, transformFunction: null }, elseTemplate: { classPropertyName: "elseTemplate", publicName: "mkCannotElse", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1715
+ }
1716
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCannot, decorators: [{
1717
+ type: Directive,
1718
+ args: [{ selector: '[mkCannot]' }]
1719
+ }], ctorParameters: () => [], propDecorators: { permission: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCannot", required: true }] }], elseTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCannotElse", required: false }] }] } });
1720
+
1721
+ /**
1722
+ * Disables the host while the app's {@link MkPermissionPolicy} denies the
1723
+ * given permission — the "keep it visible but inert" alternative to `*mkCan`
1724
+ * for buttons, inputs and menu items.
1725
+ *
1726
+ * While denied, the host gets `aria-disabled="true"`, and — when the element
1727
+ * supports it (button, input, select, textarea, fieldset…) — its native
1728
+ * `disabled` property is set too. Non-disableable elements (links, custom
1729
+ * menu items) get `aria-disabled` only; pair it with CSS / a click guard as
1730
+ * needed. The directive owns the disabled state: it also re-enables the host
1731
+ * when the permission is granted. With a signal-based policy the state flips
1732
+ * live; with no policy provided, everything is granted.
1733
+ *
1734
+ * ```html
1735
+ * <button [mkCanDisable]="'posts.delete'" (click)="remove()">Delete</button>
1736
+ * <a [mkCanDisable]="'billing.view'" routerLink="/billing">Billing</a>
1737
+ * ```
1738
+ */
1739
+ class MkCanDisable {
1740
+ policy = inject(MkPermissionPolicy, { optional: true });
1741
+ host = inject(ElementRef);
1742
+ /** The permission key to check, e.g. `'posts.delete'`. */
1743
+ permission = input.required({ ...(ngDevMode ? { debugName: "permission" } : /* istanbul ignore next */ {}), alias: 'mkCanDisable' });
1744
+ /** Whether the permission is currently denied. */
1745
+ denied = computed(() => !mkPermissionGranted(this.policy, this.permission()), /* @ts-ignore */
1746
+ ...(ngDevMode ? [{ debugName: "denied" }] : /* istanbul ignore next */ []));
1747
+ constructor() {
1748
+ const el = this.host.nativeElement;
1749
+ effect(() => {
1750
+ const denied = this.denied();
1751
+ // Only elements with a native `disabled` member (form controls) get the
1752
+ // property; everything else relies on `aria-disabled` alone.
1753
+ if ('disabled' in el)
1754
+ el.disabled = denied;
1755
+ });
1756
+ }
1757
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCanDisable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1758
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkCanDisable, isStandalone: true, selector: "[mkCanDisable]", inputs: { permission: { classPropertyName: "permission", publicName: "mkCanDisable", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.aria-disabled": "denied() ? 'true' : null" } }, ngImport: i0 });
1759
+ }
1760
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCanDisable, decorators: [{
1761
+ type: Directive,
1762
+ args: [{
1763
+ selector: '[mkCanDisable]',
1764
+ host: {
1765
+ '[attr.aria-disabled]': "denied() ? 'true' : null",
1766
+ },
1767
+ }]
1768
+ }], ctorParameters: () => [], propDecorators: { permission: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkCanDisable", required: true }] }] } });
1769
+
1770
+ /**
1771
+ * DIRECTIVES / utilities group barrel for @mk-kit/ui.
1772
+ */
1773
+
1774
+ /**
1775
+ * Generated bundle index. Do not edit.
1776
+ */
1777
+
1778
+ export { MK_FIELD_PRESETS, MkAutofocus, MkAutosize, MkCan, MkCanDisable, MkCannot, MkClickOutside, MkCopyToClipboard, MkField, MkHistoryService, MkHistoryStack, MkHotkey, MkHotkeysService, MkInfiniteScroll, MkIntersect, MkMask, MkPermissionPolicy, MkRipple, MkScrollspy, mkApplyMask, mkMaskCaret, mkMatchesHotkey, mkParseHotkey, mkPermissionGranted, registerHistoryHotkeys };
1779
+ //# sourceMappingURL=mk-kit-ui-directives.mjs.map