@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,1576 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, DOCUMENT, DestroyRef, PLATFORM_ID, signal, computed, effect, Injectable, InjectionToken, ApplicationRef, EnvironmentInjector, createComponent, Injector, ElementRef, input, output, Directive, untracked } from '@angular/core';
3
+ import { isPlatformBrowser } from '@angular/common';
4
+ import { Observable } from 'rxjs';
5
+
6
+ /** Shared primitive types used across mk-kit components. */
7
+
8
+ const STORAGE_KEY = 'mk-kit-theme';
9
+ const THEME_ATTR = 'data-mk-theme';
10
+ const DENSITY_STORAGE_KEY = 'mk-kit-density';
11
+ const DENSITY_ATTR = 'data-mk-density';
12
+ /**
13
+ * Reactive theme controller for mk-kit.
14
+ *
15
+ * - `preference()` is the user's choice: `light`, `dark`, or `system`.
16
+ * - `resolvedTheme()` is the concrete theme currently applied.
17
+ * - Writes `data-mk-theme` on `<html>` and persists the choice to
18
+ * `localStorage`. When set to `system`, it live-tracks the OS setting and
19
+ * removes the attribute so pure-CSS `prefers-color-scheme` takes over.
20
+ *
21
+ * Provided in root — inject it anywhere and bind to the signals.
22
+ */
23
+ class MkThemeService {
24
+ document = inject(DOCUMENT);
25
+ destroyRef = inject(DestroyRef);
26
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
27
+ _preference = signal(this.readInitial(), /* @ts-ignore */
28
+ ...(ngDevMode ? [{ debugName: "_preference" }] : /* istanbul ignore next */ []));
29
+ /** The user's theme preference. */
30
+ preference = this._preference.asReadonly();
31
+ _systemPrefersDark = signal(this.readSystemDark(), /* @ts-ignore */
32
+ ...(ngDevMode ? [{ debugName: "_systemPrefersDark" }] : /* istanbul ignore next */ []));
33
+ /** The concrete theme in effect (`light` or `dark`). */
34
+ resolvedTheme = computed(() => {
35
+ const pref = this._preference();
36
+ if (pref === 'system') {
37
+ return this._systemPrefersDark() ? 'dark' : 'light';
38
+ }
39
+ return pref;
40
+ }, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "resolvedTheme" }] : /* istanbul ignore next */ []));
42
+ /** Convenience boolean for template bindings. */
43
+ isDark = computed(() => this.resolvedTheme() === 'dark', /* @ts-ignore */
44
+ ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
45
+ _density = signal(this.readInitialDensity(), /* @ts-ignore */
46
+ ...(ngDevMode ? [{ debugName: "_density" }] : /* istanbul ignore next */ []));
47
+ /**
48
+ * The global density mode. `compact` tightens control heights and the core
49
+ * spacing steps via the `data-mk-density` attribute, `touch` enlarges them —
50
+ * every component follows automatically because they read the same tokens.
51
+ *
52
+ * This signal is the GLOBAL mode only. To make one screen or dialog touch-
53
+ * sized inside an otherwise comfortable app, put `data-mk-density="touch"`
54
+ * on that element instead; the tokens inherit and this service stays out of
55
+ * it.
56
+ */
57
+ density = this._density.asReadonly();
58
+ constructor() {
59
+ if (this.isBrowser) {
60
+ this.watchSystemPreference();
61
+ }
62
+ // Keep the DOM attribute + storage in sync with the signal.
63
+ effect(() => {
64
+ const pref = this._preference();
65
+ if (!this.isBrowser)
66
+ return;
67
+ const root = this.document.documentElement;
68
+ if (pref === 'system') {
69
+ root.removeAttribute(THEME_ATTR);
70
+ }
71
+ else {
72
+ root.setAttribute(THEME_ATTR, pref);
73
+ }
74
+ try {
75
+ localStorage.setItem(STORAGE_KEY, pref);
76
+ }
77
+ catch {
78
+ /* storage may be unavailable (private mode) — ignore */
79
+ }
80
+ });
81
+ // Keep the density attribute + storage in sync.
82
+ effect(() => {
83
+ const density = this._density();
84
+ if (!this.isBrowser)
85
+ return;
86
+ const root = this.document.documentElement;
87
+ // `comfortable` is the token default, so it is the ABSENCE of the
88
+ // attribute — anything else names itself. Written this way so a new mode
89
+ // needs no change here.
90
+ if (density === 'comfortable') {
91
+ root.removeAttribute(DENSITY_ATTR);
92
+ }
93
+ else {
94
+ root.setAttribute(DENSITY_ATTR, density);
95
+ }
96
+ try {
97
+ localStorage.setItem(DENSITY_STORAGE_KEY, density);
98
+ }
99
+ catch {
100
+ /* ignore */
101
+ }
102
+ });
103
+ }
104
+ /** Set the global density mode. */
105
+ setDensity(density) {
106
+ this._density.set(density);
107
+ }
108
+ /**
109
+ * Toggle between comfortable and compact — the two modes a density switch in
110
+ * a UI offers. `touch` is a deliberate choice for a specific screen, not
111
+ * something to land on by toggling, so from `touch` this returns to
112
+ * comfortable rather than cycling.
113
+ */
114
+ toggleDensity() {
115
+ this._density.update((d) => (d === 'comfortable' ? 'compact' : 'comfortable'));
116
+ }
117
+ readInitialDensity() {
118
+ if (!this.isBrowser)
119
+ return 'comfortable';
120
+ try {
121
+ const stored = localStorage.getItem(DENSITY_STORAGE_KEY);
122
+ if (stored === 'compact' || stored === 'comfortable' || stored === 'touch') {
123
+ return stored;
124
+ }
125
+ }
126
+ catch {
127
+ /* ignore */
128
+ }
129
+ return 'comfortable';
130
+ }
131
+ /** Set the theme preference explicitly. */
132
+ setTheme(preference) {
133
+ this._preference.set(preference);
134
+ }
135
+ /** Toggle between light and dark (resolving `system` first). */
136
+ toggle() {
137
+ this._preference.set(this.resolvedTheme() === 'dark' ? 'light' : 'dark');
138
+ }
139
+ readInitial() {
140
+ if (!this.isBrowserEnv())
141
+ return 'system';
142
+ try {
143
+ const stored = localStorage.getItem(STORAGE_KEY);
144
+ if (stored === 'light' || stored === 'dark' || stored === 'system') {
145
+ return stored;
146
+ }
147
+ }
148
+ catch {
149
+ /* ignore */
150
+ }
151
+ return 'system';
152
+ }
153
+ readSystemDark() {
154
+ if (!this.isBrowserEnv())
155
+ return false;
156
+ return this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)')
157
+ .matches ?? false;
158
+ }
159
+ watchSystemPreference() {
160
+ const mql = this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)');
161
+ if (!mql)
162
+ return;
163
+ const onChange = (e) => this._systemPrefersDark.set(e.matches);
164
+ mql.addEventListener('change', onChange);
165
+ // Detach when the injector dies (repeated bootstraps in SSR/HMR/tests).
166
+ this.destroyRef.onDestroy(() => mql.removeEventListener('change', onChange));
167
+ }
168
+ isBrowserEnv() {
169
+ return this.isBrowser && typeof this.document !== 'undefined';
170
+ }
171
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
172
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkThemeService, providedIn: 'root' });
173
+ }
174
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkThemeService, decorators: [{
175
+ type: Injectable,
176
+ args: [{ providedIn: 'root' }]
177
+ }], ctorParameters: () => [] });
178
+
179
+ /**
180
+ * Announces messages to assistive technology via a visually-hidden
181
+ * `aria-live` region. Used by toasts, form validation, sort changes, etc.
182
+ * so state changes are perceivable without sight. WCAG 4.1.3 (Status Messages).
183
+ */
184
+ class MkLiveAnnouncer {
185
+ document = inject(DOCUMENT);
186
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
187
+ region;
188
+ clearTimer;
189
+ /** Announce `message`. `politeness` controls interruption behavior. */
190
+ announce(message, politeness = 'polite') {
191
+ if (!this.isBrowser)
192
+ return;
193
+ const region = this.ensureRegion();
194
+ region.setAttribute('aria-live', politeness);
195
+ // Clear then set on the next frame so identical consecutive messages
196
+ // are still re-announced by screen readers.
197
+ region.textContent = '';
198
+ if (this.clearTimer)
199
+ clearTimeout(this.clearTimer);
200
+ this.document.defaultView?.requestAnimationFrame(() => {
201
+ region.textContent = message;
202
+ });
203
+ // Tidy up after a while to keep the DOM clean.
204
+ this.clearTimer = setTimeout(() => {
205
+ if (this.region)
206
+ this.region.textContent = '';
207
+ }, 1000);
208
+ }
209
+ ensureRegion() {
210
+ if (this.region)
211
+ return this.region;
212
+ const el = this.document.createElement('div');
213
+ el.setAttribute('aria-atomic', 'true');
214
+ el.setAttribute('aria-live', 'polite');
215
+ el.className = 'mk-visually-hidden';
216
+ el.style.cssText =
217
+ 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';
218
+ this.document.body.appendChild(el);
219
+ this.region = el;
220
+ return el;
221
+ }
222
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLiveAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
223
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLiveAnnouncer, providedIn: 'root' });
224
+ }
225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkLiveAnnouncer, decorators: [{
226
+ type: Injectable,
227
+ args: [{ providedIn: 'root' }]
228
+ }] });
229
+
230
+ const FOCUSABLE_SELECTOR = [
231
+ 'a[href]',
232
+ 'button:not([disabled])',
233
+ 'input:not([disabled]):not([type="hidden"])',
234
+ 'select:not([disabled])',
235
+ 'textarea:not([disabled])',
236
+ '[tabindex]:not([tabindex="-1"])',
237
+ '[contenteditable="true"]',
238
+ ].join(',');
239
+ /** Returns the tabbable elements inside `root`, in DOM order. */
240
+ function mkGetFocusable(root) {
241
+ const win = root.ownerDocument.defaultView;
242
+ return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => {
243
+ if (el === root.ownerDocument.activeElement)
244
+ return true;
245
+ if (el.offsetWidth <= 0 && el.offsetHeight <= 0)
246
+ return false;
247
+ // `visibility:hidden` elements keep their box, so the size check above
248
+ // passes — yet they are NOT tabbable, and counting one as a Tab-wrap
249
+ // boundary makes the wrap focus a dead element. `getComputedStyle`
250
+ // degrades safely in jsdom, where visibility resolves 'visible' by
251
+ // default, so test environments without layout keep working.
252
+ const visibility = win?.getComputedStyle(el).visibility;
253
+ return visibility !== 'hidden' && visibility !== 'collapse';
254
+ });
255
+ }
256
+ /**
257
+ * Traps keyboard focus within `root` (wrapping Tab / Shift+Tab), moves focus
258
+ * inside on activation, and restores focus to the previously-focused element
259
+ * on release. Essential for accessible modals/menus (WCAG 2.4.3, 2.1.2).
260
+ */
261
+ class MkFocusTrap {
262
+ root;
263
+ previouslyFocused = null;
264
+ active = false;
265
+ keydownHandler = (e) => this.onKeydown(e);
266
+ constructor(root) {
267
+ this.root = root;
268
+ }
269
+ /** Activate the trap and move focus to the first focusable element (or root). */
270
+ activate(initialFocus) {
271
+ this.previouslyFocused = this.root.ownerDocument
272
+ .activeElement;
273
+ this.active = true;
274
+ this.root.addEventListener('keydown', this.keydownHandler, true);
275
+ const target = initialFocus ??
276
+ mkGetFocusable(this.root)[0] ??
277
+ this.root;
278
+ // Ensure programmatic focus works even on a non-tabbable container.
279
+ if (target === this.root && !this.root.hasAttribute('tabindex')) {
280
+ this.root.setAttribute('tabindex', '-1');
281
+ }
282
+ // Skip if the trap was released within the same tick, so the deferred
283
+ // focus can't steal focus back after release() restored it.
284
+ queueMicrotask(() => {
285
+ if (this.active)
286
+ target.focus();
287
+ });
288
+ }
289
+ /** Deactivate and restore focus to the trigger element. */
290
+ release() {
291
+ this.active = false;
292
+ this.root.removeEventListener('keydown', this.keydownHandler, true);
293
+ const previous = this.previouslyFocused;
294
+ this.previouslyFocused = null;
295
+ if (!previous)
296
+ return;
297
+ if (previous.isConnected) {
298
+ previous.focus?.();
299
+ }
300
+ else {
301
+ // The trigger left the DOM while the trap was active (a deleted row, a
302
+ // route change). Focusing a detached node is a silent no-op at best —
303
+ // land on the body instead so focus has a real, connected home.
304
+ this.root.ownerDocument.body?.focus?.();
305
+ }
306
+ }
307
+ onKeydown(e) {
308
+ if (e.key !== 'Tab')
309
+ return;
310
+ const focusable = mkGetFocusable(this.root);
311
+ if (focusable.length === 0) {
312
+ e.preventDefault();
313
+ this.root.focus();
314
+ return;
315
+ }
316
+ const first = focusable[0];
317
+ const last = focusable[focusable.length - 1];
318
+ const active = this.root.ownerDocument.activeElement;
319
+ if (e.shiftKey && active === first) {
320
+ e.preventDefault();
321
+ last.focus();
322
+ }
323
+ else if (!e.shiftKey && active === last) {
324
+ e.preventDefault();
325
+ first.focus();
326
+ }
327
+ }
328
+ }
329
+
330
+ let counter = 0;
331
+ /**
332
+ * Generates a stable, unique DOM id for wiring `aria-*` relationships
333
+ * (labels, descriptions, controls). Prefer this over `Math.random()` so
334
+ * ids are deterministic within a render and SSR-safe.
335
+ *
336
+ * @param prefix short semantic prefix, e.g. `mk-input`.
337
+ */
338
+ function mkUniqueId(prefix = 'mk') {
339
+ return `${prefix}-${++counter}`;
340
+ }
341
+
342
+ /** Injection token exposing the data passed to an overlay component. */
343
+ const MK_OVERLAY_DATA = new InjectionToken('MK_OVERLAY_DATA');
344
+ /**
345
+ * Handle to an open overlay. Injected into the rendered component and returned
346
+ * from `MkOverlayService.open`. Resolve the overlay by calling `close`.
347
+ *
348
+ * The close result is exposed three ways so the handle fits whatever style the
349
+ * calling code already uses — they all settle together:
350
+ *
351
+ * - `result` — a signal, for template binding and signal-based components.
352
+ * - `closed$` — an Observable that emits once and completes, for RxJS
353
+ * pipelines. A service that hands a dialog result back to its callers can
354
+ * return this directly instead of wrapping the promise in `from(...)`.
355
+ * - `afterClosed` — a Promise, for `await` / `.then()`.
356
+ */
357
+ class MkOverlayRef {
358
+ /** The rendered component instance (set by the service after creation). */
359
+ componentRef;
360
+ _closed = signal(false, /* @ts-ignore */
361
+ ...(ngDevMode ? [{ debugName: "_closed" }] : /* istanbul ignore next */ []));
362
+ /** Becomes `true` once the overlay has been dismissed. */
363
+ closed = this._closed.asReadonly();
364
+ _result = signal(undefined, /* @ts-ignore */
365
+ ...(ngDevMode ? [{ debugName: "_result" }] : /* istanbul ignore next */ []));
366
+ /**
367
+ * The value passed to `close`, or `undefined` while open and for a dismissal
368
+ * (Escape / backdrop). Read `closed()` to tell "closed with no result" apart
369
+ * from "still open".
370
+ */
371
+ result = this._result.asReadonly();
372
+ resolveClosed;
373
+ /** Resolves with the close result when the overlay is dismissed. */
374
+ afterClosed = new Promise((resolve) => {
375
+ this.resolveClosed = resolve;
376
+ });
377
+ emitClosed;
378
+ /**
379
+ * Emits the close result once, then completes. Subscribing after the overlay
380
+ * has already closed replays the result immediately, so a late subscriber
381
+ * never hangs.
382
+ */
383
+ closed$ = new Observable((subscriber) => {
384
+ if (this._closed()) {
385
+ subscriber.next(this._result());
386
+ subscriber.complete();
387
+ return;
388
+ }
389
+ this.emitClosed = (result) => {
390
+ subscriber.next(result);
391
+ subscriber.complete();
392
+ };
393
+ return () => {
394
+ this.emitClosed = undefined;
395
+ };
396
+ });
397
+ /** Internal disposer wired up by the service. */
398
+ _dispose = () => { };
399
+ /** Close the overlay, optionally returning a result. */
400
+ close(result) {
401
+ if (this._closed())
402
+ return;
403
+ this._closed.set(true);
404
+ this._result.set(result);
405
+ this._dispose(result);
406
+ this.resolveClosed(result);
407
+ this.emitClosed?.(result);
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Body-level elements a modal must NOT inert:
413
+ * - `[popover]` — anchored panels / tooltips teleport to `document.body` and
414
+ * may open ABOVE the dialog after it (a select inside a dialog); inerting
415
+ * them would kill the very widget the dialog just opened.
416
+ * - toast / snackbar containers — transient status surfaces that must stay
417
+ * reachable and announceable while a dialog is up.
418
+ * - `[aria-live]` — the live announcer region; an inert live region stops
419
+ * announcing entirely.
420
+ */
421
+ const INERT_EXEMPT_SELECTOR = '[popover], [aria-live], mk-toast-container, mk-snackbar-container';
422
+ /**
423
+ * Lightweight, dependency-free overlay renderer. Instantiates a standalone
424
+ * component into a body-level host, manages the backdrop, focus trapping,
425
+ * Escape handling, and scroll locking. Powers Dialog, Menu, and others.
426
+ */
427
+ class MkOverlayService {
428
+ appRef = inject(ApplicationRef);
429
+ envInjector = inject(EnvironmentInjector);
430
+ document = inject(DOCUMENT);
431
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
432
+ // All coordination state lives on the INSTANCE, not the module: the service
433
+ // is a root singleton, so one app still shares one stack — but each test
434
+ // injector gets a fresh world instead of leaking overlays/locks/listeners
435
+ // across spec files that share a worker.
436
+ /** Reference count for the body scroll lock (first open locks, last close unlocks). */
437
+ openOverlays = 0;
438
+ bodyScrollLock = null;
439
+ /**
440
+ * Every overlay currently on screen, in open order (Map preserves insertion
441
+ * order, so the last entry is the topmost). Kept so {@link closeAll} can
442
+ * dismiss them and so the shared Escape listener can find the topmost
443
+ * dismissible overlay. Entries remove themselves on dispose.
444
+ */
445
+ openRefs = new Map();
446
+ /** The document currently carrying the shared Escape listener, if any. */
447
+ escapeListenerDoc = null;
448
+ /**
449
+ * The one document-level Escape listener, shared by every open overlay.
450
+ *
451
+ * Registered on the BUBBLE phase on purpose: widgets living inside an
452
+ * overlay (a menu, an anchored panel, a picker) handle Escape on their own
453
+ * elements during bubbling and call `preventDefault()`, so a consumed
454
+ * Escape arrives here already `defaultPrevented` and closes nothing. A
455
+ * per-overlay capture listener with `stopPropagation()` cannot do this:
456
+ * stopPropagation does not stop OTHER listeners on the same target, so one
457
+ * Escape used to close every stacked overlay at once.
458
+ */
459
+ onDocumentEscape = (event) => {
460
+ if (event.key !== 'Escape' || event.defaultPrevented)
461
+ return;
462
+ // Close ONLY the topmost open overlay that opted into Escape dismissal.
463
+ const entries = [...this.openRefs.entries()];
464
+ for (let i = entries.length - 1; i >= 0; i--) {
465
+ const [ref, { closeOnEscape }] = entries[i];
466
+ if (closeOnEscape) {
467
+ ref.close();
468
+ event.preventDefault();
469
+ return;
470
+ }
471
+ }
472
+ };
473
+ /** (Un)register the shared Escape listener to match the open-overlay set. */
474
+ syncEscapeListener(doc) {
475
+ const needed = [...this.openRefs.values()].some((e) => e.closeOnEscape);
476
+ if (needed && this.escapeListenerDoc === null) {
477
+ doc.addEventListener('keydown', this.onDocumentEscape);
478
+ this.escapeListenerDoc = doc;
479
+ }
480
+ else if (!needed && this.escapeListenerDoc !== null) {
481
+ this.escapeListenerDoc.removeEventListener('keydown', this.onDocumentEscape);
482
+ this.escapeListenerDoc = null;
483
+ }
484
+ }
485
+ lockBodyScroll(doc) {
486
+ const { style } = doc.body;
487
+ this.bodyScrollLock = {
488
+ scrollY: doc.defaultView?.scrollY ?? 0,
489
+ prev: {
490
+ position: style.position,
491
+ top: style.top,
492
+ left: style.left,
493
+ right: style.right,
494
+ width: style.width,
495
+ overflow: style.overflow,
496
+ },
497
+ };
498
+ // overflow:hidden still does the whole job on desktop (and avoids a
499
+ // scroll jump there); the fixed-body treatment is what actually stops iOS
500
+ // touch scrolling. `top: -scrollY` keeps the page visually where it was.
501
+ style.overflow = 'hidden';
502
+ style.position = 'fixed';
503
+ style.top = `-${this.bodyScrollLock.scrollY}px`;
504
+ style.left = '0';
505
+ style.right = '0';
506
+ style.width = '100%';
507
+ }
508
+ unlockBodyScroll(doc) {
509
+ if (!this.bodyScrollLock)
510
+ return;
511
+ const { scrollY, prev } = this.bodyScrollLock;
512
+ this.bodyScrollLock = null;
513
+ const { style } = doc.body;
514
+ style.position = prev.position;
515
+ style.top = prev.top;
516
+ style.left = prev.left;
517
+ style.right = prev.right;
518
+ style.width = prev.width;
519
+ style.overflow = prev.overflow;
520
+ // Fixing the body collapsed the document's scroll position to 0; jump
521
+ // back instantly (never smooth — the restoration must be invisible).
522
+ // Guarded for jsdom, where scrollTo is unimplemented.
523
+ const view = doc.defaultView;
524
+ if (scrollY !== 0 && typeof view?.scrollTo === 'function') {
525
+ view.scrollTo({ top: scrollY, behavior: 'instant' });
526
+ }
527
+ }
528
+ /** How many overlays are currently open. */
529
+ get openCount() {
530
+ return this.openRefs.size;
531
+ }
532
+ /**
533
+ * Close every open overlay, newest first. Each is closed with no result, so
534
+ * `afterClosed` resolves undefined exactly as a backdrop click or Escape
535
+ * would. Safe to call when nothing is open.
536
+ */
537
+ closeAll() {
538
+ // Iterate a COPY: close() disposes synchronously, which mutates the map.
539
+ for (const ref of [...this.openRefs.keys()].reverse())
540
+ ref.close();
541
+ }
542
+ /**
543
+ * Injector teardown closes everything still open. In an app this runs only
544
+ * at shutdown; in tests it runs on every `TestBed.resetTestingModule()`,
545
+ * so an overlay a spec forgot to close cannot leak its scroll lock, focus
546
+ * trap, inert marks, or document listeners into later spec files sharing
547
+ * the worker.
548
+ */
549
+ ngOnDestroy() {
550
+ this.closeAll();
551
+ }
552
+ open(component, config = {}) {
553
+ const { hasBackdrop = true, closeOnBackdropClick = true, closeOnEscape = true, trapFocus = true, autoFocus = true, role = 'dialog', } = config;
554
+ const overlayRef = new MkOverlayRef();
555
+ if (!this.isBrowser) {
556
+ // Nothing to render on the server; return an inert ref.
557
+ return overlayRef;
558
+ }
559
+ const container = this.document.createElement('div');
560
+ container.className = 'mk-overlay-container';
561
+ container.style.cssText =
562
+ 'position:fixed;inset:0;z-index:var(--mk-z-overlay,1000);display:flex;align-items:center;justify-content:center;';
563
+ if (!hasBackdrop) {
564
+ // Without a scrim the fullscreen container must not swallow clicks
565
+ // meant for the page; the panel re-enables pointer events for itself.
566
+ container.style.pointerEvents = 'none';
567
+ }
568
+ let backdrop;
569
+ if (hasBackdrop) {
570
+ backdrop = this.document.createElement('div');
571
+ backdrop.className = 'mk-overlay-backdrop';
572
+ backdrop.style.cssText =
573
+ 'position:absolute;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);';
574
+ container.appendChild(backdrop);
575
+ if (closeOnBackdropClick) {
576
+ backdrop.addEventListener('click', () => overlayRef.close());
577
+ }
578
+ }
579
+ const panel = this.document.createElement('div');
580
+ panel.className = 'mk-overlay-panel';
581
+ if (config.panelClass) {
582
+ const classes = Array.isArray(config.panelClass)
583
+ ? config.panelClass
584
+ : [config.panelClass];
585
+ panel.classList.add(...classes);
586
+ }
587
+ panel.style.cssText = 'position:relative;max-width:100%;max-height:100%;';
588
+ if (!hasBackdrop)
589
+ panel.style.pointerEvents = 'auto';
590
+ panel.setAttribute('role', role);
591
+ panel.setAttribute('aria-modal', role === 'menu' ? 'false' : 'true');
592
+ if (config.ariaLabel)
593
+ panel.setAttribute('aria-label', config.ariaLabel);
594
+ container.appendChild(panel);
595
+ // Create the component with an injector exposing the data + ref.
596
+ const componentRef = createComponent(component, {
597
+ environmentInjector: this.envInjector,
598
+ hostElement: panel,
599
+ elementInjector: Injector.create({
600
+ parent: config.injector ?? this.envInjector,
601
+ providers: [
602
+ { provide: MK_OVERLAY_DATA, useValue: config.data ?? null },
603
+ { provide: MkOverlayRef, useValue: overlayRef },
604
+ ],
605
+ }),
606
+ });
607
+ overlayRef.componentRef = componentRef;
608
+ this.appRef.attachView(componentRef.hostView);
609
+ this.document.body.appendChild(container);
610
+ // Lock body scroll while any overlay is open (see lockBodyScroll for the
611
+ // iOS-proof fixed-body technique and the reference counting).
612
+ if (this.openOverlays++ === 0) {
613
+ this.lockBodyScroll(this.document);
614
+ }
615
+ // Make the rest of the page inert while a MODAL overlay is open —
616
+ // `aria-modal="true"` alone does not stop Tab or a screen reader's
617
+ // reading cursor from reaching background content (WCAG 2.4.3 / 1.3.2).
618
+ // A backdropless overlay is deliberately non-blocking, so it inerts
619
+ // nothing. Only the elements THIS overlay inerted are recorded, so
620
+ // nested modals unwind correctly: the inner dialog skips (and therefore
621
+ // never un-inerts) whatever the outer dialog already inerted.
622
+ const isModal = hasBackdrop && role !== 'menu';
623
+ const inerted = [];
624
+ if (isModal) {
625
+ for (const sibling of Array.from(this.document.body.children)) {
626
+ if (sibling === container ||
627
+ sibling.hasAttribute('inert') ||
628
+ sibling.matches(INERT_EXEMPT_SELECTOR)) {
629
+ continue;
630
+ }
631
+ sibling.setAttribute('inert', '');
632
+ inerted.push(sibling);
633
+ }
634
+ }
635
+ // Focus management.
636
+ const focusTrap = trapFocus ? new MkFocusTrap(panel) : undefined;
637
+ // `autoFocus: false` focuses the panel rather than its first control, so
638
+ // the trap still contains Tab and restores focus on close.
639
+ focusTrap?.activate(autoFocus ? undefined : panel);
640
+ // Escape handling goes through ONE shared document listener (see
641
+ // `onDocumentEscape`) so stacked overlays close one per keypress,
642
+ // topmost first, and inner widgets can consume Escape before us.
643
+ this.openRefs.set(overlayRef, { closeOnEscape });
644
+ this.syncEscapeListener(this.document);
645
+ overlayRef._dispose = () => {
646
+ this.openRefs.delete(overlayRef);
647
+ this.syncEscapeListener(this.document);
648
+ for (const el of inerted)
649
+ el.removeAttribute('inert');
650
+ focusTrap?.release();
651
+ // ApplicationRef destroys its views before root-injector destroy hooks
652
+ // run — detaching then would throw (same guard as MkTourService).
653
+ if (!this.appRef.destroyed)
654
+ this.appRef.detachView(componentRef.hostView);
655
+ componentRef.destroy();
656
+ container.remove();
657
+ if (--this.openOverlays === 0) {
658
+ this.unlockBodyScroll(this.document);
659
+ }
660
+ };
661
+ return overlayRef;
662
+ }
663
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
664
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkOverlayService, providedIn: 'root' });
665
+ }
666
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkOverlayService, decorators: [{
667
+ type: Injectable,
668
+ args: [{ providedIn: 'root' }]
669
+ }] });
670
+
671
+ /**
672
+ * Never shrink a panel below this via the viewport size cap — a sub-120px
673
+ * dropdown is unusable; at that point overflowing beats collapsing.
674
+ */
675
+ const MIN_SIZE_CAP = 120;
676
+ function sideOf(placement) {
677
+ if (placement.startsWith('top'))
678
+ return 'top';
679
+ if (placement.startsWith('bottom'))
680
+ return 'bottom';
681
+ return placement;
682
+ }
683
+ function alignOf(placement) {
684
+ if (placement.endsWith('-start'))
685
+ return 'start';
686
+ if (placement.endsWith('-end'))
687
+ return 'end';
688
+ return 'center';
689
+ }
690
+ /**
691
+ * Pure viewport-positioning maths shared by every anchored overlay
692
+ * ({@link MkAnchoredPanel} and the tooltip). Given the anchor rect, the panel
693
+ * size and the viewport size, returns the top/left for a `position: fixed`
694
+ * panel — flipping to the opposite side when it would overflow and clamping
695
+ * back inside the viewport.
696
+ */
697
+ function mkComputeAnchoredPosition(anchor, panel, viewport, opts) {
698
+ const { gap, flip, clamp } = opts;
699
+ let side = sideOf(opts.placement);
700
+ const align = alignOf(opts.placement);
701
+ const { width: w, height: h } = panel;
702
+ const { width: vw, height: vh } = viewport;
703
+ // Vertical flip for top/bottom placements.
704
+ if (flip && (side === 'top' || side === 'bottom')) {
705
+ const fitsBelow = anchor.bottom + gap + h <= vh;
706
+ const fitsAbove = anchor.top - gap - h >= 0;
707
+ if (side === 'bottom' && !fitsBelow && fitsAbove)
708
+ side = 'top';
709
+ else if (side === 'top' && !fitsAbove && fitsBelow)
710
+ side = 'bottom';
711
+ }
712
+ // Horizontal flip for left/right placements.
713
+ if (flip && (side === 'left' || side === 'right')) {
714
+ const fitsRight = anchor.right + gap + w <= vw;
715
+ const fitsLeft = anchor.left - gap - w >= 0;
716
+ if (side === 'right' && !fitsRight && fitsLeft)
717
+ side = 'left';
718
+ else if (side === 'left' && !fitsLeft && fitsRight)
719
+ side = 'right';
720
+ }
721
+ let top = 0;
722
+ let left = 0;
723
+ if (side === 'bottom')
724
+ top = anchor.bottom + gap;
725
+ else if (side === 'top')
726
+ top = anchor.top - h - gap;
727
+ else if (side === 'right')
728
+ left = anchor.right + gap;
729
+ else
730
+ left = anchor.left - w - gap;
731
+ if (side === 'top' || side === 'bottom') {
732
+ // Cross axis is horizontal; in RTL, inline-start is the anchor's right edge.
733
+ const startLeft = opts.rtl ? anchor.right - w : anchor.left;
734
+ const endLeft = opts.rtl ? anchor.left : anchor.right - w;
735
+ if (align === 'start')
736
+ left = startLeft;
737
+ else if (align === 'end')
738
+ left = endLeft;
739
+ else
740
+ left = anchor.left + anchor.width / 2 - w / 2;
741
+ }
742
+ else {
743
+ // Left/right: centre on the cross (vertical) axis.
744
+ top = anchor.top + anchor.height / 2 - h / 2;
745
+ }
746
+ if (clamp) {
747
+ left = Math.max(gap, Math.min(left, vw - w - gap));
748
+ top = Math.max(gap, Math.min(top, vh - h - gap));
749
+ }
750
+ return { top: Math.round(top), left: Math.round(left), placement: `${side}${align === 'center' ? '' : `-${align}`}` };
751
+ }
752
+ /**
753
+ * Anchored-overlay directive. Apply it to a floating panel element (a dropdown
754
+ * list, calendar, menu, …) that is rendered inside its component's own template
755
+ * — typically inside an `@if (open()) { … }` block. On init the directive
756
+ * **teleports the panel to `document.body` and into the browser top layer** via
757
+ * the native Popover API (`popover="manual"` + `showPopover()`), then positions
758
+ * it against the anchor with `position: fixed`. Because the element stays part
759
+ * of the component's Angular view, all bindings, `@for` content, events and
760
+ * projected content keep working after the move.
761
+ *
762
+ * The top layer is immune to ancestor `overflow`, `transform` and `z-index`
763
+ * stacking contexts, so the panel can never be clipped by a container or hidden
764
+ * behind sibling content. Where the Popover API is unavailable the panel still
765
+ * renders in a `document.body` portal with a `z-index` fallback.
766
+ *
767
+ * ```html
768
+ * <ul #panel mkAnchoredPanel [mkAnchoredPanelFor]="trigger" [matchWidth]="true"
769
+ * (dismiss)="close()"> … </ul>
770
+ * ```
771
+ */
772
+ class MkAnchoredPanel {
773
+ host = inject(ElementRef);
774
+ document = inject(DOCUMENT);
775
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
776
+ /** The trigger element to position against. */
777
+ anchor = input(undefined, { ...(ngDevMode ? { debugName: "anchor" } : /* istanbul ignore next */ {}), alias: 'mkAnchoredPanelFor' });
778
+ /** Viewport-point anchor (e.g. a right-click position) — takes precedence. */
779
+ anchorRect = input(undefined, /* @ts-ignore */
780
+ ...(ngDevMode ? [{ debugName: "anchorRect" }] : /* istanbul ignore next */ []));
781
+ /** Preferred placement relative to the anchor. */
782
+ placement = input('bottom-start', /* @ts-ignore */
783
+ ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
784
+ /** Distance in px between the anchor and the panel. */
785
+ gap = input(4, /* @ts-ignore */
786
+ ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
787
+ /** Set the panel's `min-width` to the anchor's width (dropdowns). */
788
+ matchWidth = input(false, /* @ts-ignore */
789
+ ...(ngDevMode ? [{ debugName: "matchWidth" }] : /* istanbul ignore next */ []));
790
+ /** Flip to the opposite side when the preferred side would overflow. */
791
+ flip = input(true, /* @ts-ignore */
792
+ ...(ngDevMode ? [{ debugName: "flip" }] : /* istanbul ignore next */ []));
793
+ /** Clamp the panel inside the viewport. */
794
+ clamp = input(true, /* @ts-ignore */
795
+ ...(ngDevMode ? [{ debugName: "clamp" }] : /* istanbul ignore next */ []));
796
+ /** Emitted on an outside pointerdown or when the window loses focus. */
797
+ dismiss = output();
798
+ popover = false;
799
+ /** Whether {@link position} applied an inline viewport size cap (`max-*`). */
800
+ sizeCapped = false;
801
+ repositionRaf = null;
802
+ /**
803
+ * Scroll-driven repositions track the anchor without clamping, so the
804
+ * panel follows its trigger instead of detaching and hugging the viewport
805
+ * edge; open/resize positioning clamps as usual.
806
+ */
807
+ pendingTrack = true;
808
+ /** rAF-coalesced repositioning — at most one layout pass per frame. */
809
+ reposition(track) {
810
+ // A clamped (open/resize) request in the same frame wins over tracking.
811
+ this.pendingTrack &&= track;
812
+ if (this.repositionRaf != null)
813
+ return;
814
+ this.repositionRaf =
815
+ this.document.defaultView?.requestAnimationFrame(() => {
816
+ this.repositionRaf = null;
817
+ const wasTrack = this.pendingTrack;
818
+ this.pendingTrack = true;
819
+ this.position(wasTrack);
820
+ }) ?? null;
821
+ }
822
+ onScroll = () => this.reposition(true);
823
+ onResize = () => this.reposition(false);
824
+ onDocPointerdown = (e) => {
825
+ const target = e.target;
826
+ if (this.host.nativeElement.contains(target))
827
+ return;
828
+ const anchorEl = this.resolveAnchorEl();
829
+ if (anchorEl?.contains(target))
830
+ return;
831
+ this.dismiss.emit();
832
+ };
833
+ onWindowBlur = () => this.dismiss.emit();
834
+ ngAfterViewInit() {
835
+ if (!this.isBrowser)
836
+ return;
837
+ const el = this.host.nativeElement;
838
+ el.style.position = 'fixed';
839
+ el.style.margin = '0';
840
+ el.style.inset = 'auto';
841
+ // setProperty (not .style.zIndex) so the CSS var() value is accepted. Only
842
+ // matters in the no-Popover fallback; the top layer ignores z-index.
843
+ el.style.setProperty('z-index', 'var(--mk-z-menu)');
844
+ this.document.body.appendChild(el);
845
+ // Promote into the top layer when the Popover API is available.
846
+ const withPopover = el;
847
+ if (typeof withPopover.showPopover === 'function') {
848
+ el.setAttribute('popover', 'manual');
849
+ try {
850
+ withPopover.showPopover();
851
+ this.popover = true;
852
+ }
853
+ catch {
854
+ // Already open or unsupported context — fall back to the body portal.
855
+ // The attribute must go too: with it left on, the UA sheet's
856
+ // `[popover]:not(:popover-open) { display: none }` hides the panel.
857
+ el.removeAttribute('popover');
858
+ this.popover = false;
859
+ }
860
+ }
861
+ this.position();
862
+ const view = this.document.defaultView;
863
+ view?.addEventListener('scroll', this.onScroll, {
864
+ capture: true,
865
+ passive: true,
866
+ });
867
+ view?.addEventListener('resize', this.onResize);
868
+ view?.addEventListener('blur', this.onWindowBlur);
869
+ // iOS's software keyboard (and pinch-zoom) resizes/pans only the VISUAL
870
+ // viewport — `window` never fires resize/scroll for it — so listen there
871
+ // too, into the same reposition paths. Feature-detected: jsdom and older
872
+ // browsers have no visualViewport.
873
+ const visual = view?.visualViewport;
874
+ visual?.addEventListener('resize', this.onResize);
875
+ visual?.addEventListener('scroll', this.onScroll);
876
+ this.document.addEventListener('pointerdown', this.onDocPointerdown, true);
877
+ // Re-measure once layout has settled (fonts, async content).
878
+ view?.requestAnimationFrame(() => this.position());
879
+ }
880
+ /**
881
+ * Recompute and apply the panel position. Safe to call at any time.
882
+ * With `track` (scroll-driven), the panel follows the anchor unclamped and
883
+ * dismisses once the anchor leaves the viewport (matching the CDK's
884
+ * reposition-with-auto-close scroll behaviour).
885
+ */
886
+ position(track = false) {
887
+ if (!this.isBrowser)
888
+ return;
889
+ const el = this.host.nativeElement;
890
+ const view = this.document.defaultView;
891
+ if (!view)
892
+ return;
893
+ const anchor = this.resolveAnchorRect();
894
+ if (!anchor)
895
+ return;
896
+ // Prefer the visual viewport when it exists and is measured: on iOS the
897
+ // software keyboard shrinks only `visualViewport`, never the layout
898
+ // viewport, so clamp/flip maths against `documentElement.client*` would
899
+ // keep placing panels underneath the keyboard. Fallback (no visualViewport,
900
+ // or one reporting 0×0 pre-measure) keeps the existing behaviour, including
901
+ // the jsdom "unmeasured viewport" guards below.
902
+ const visual = view.visualViewport;
903
+ const useVisual = visual != null && visual.width > 0 && visual.height > 0;
904
+ const vw = useVisual ? visual.width : this.document.documentElement.clientWidth;
905
+ const vh = useVisual ? visual.height : this.document.documentElement.clientHeight;
906
+ // Anchor scrolled fully out of view — the panel would float detached.
907
+ // Zero-size anchor rects and a zero-size viewport are skipped: they mean
908
+ // "not yet measured" (jsdom, SSR hydration), not "off-screen".
909
+ if (track &&
910
+ !this.anchorRect() &&
911
+ vw > 0 &&
912
+ vh > 0 &&
913
+ (anchor.width > 0 || anchor.height > 0) &&
914
+ (anchor.bottom <= 0 || anchor.top >= vh || anchor.right <= 0 || anchor.left >= vw)) {
915
+ this.dismiss.emit();
916
+ return;
917
+ }
918
+ if (this.matchWidth() && anchor.width > 0) {
919
+ el.style.minWidth = `${Math.round(anchor.width)}px`;
920
+ }
921
+ const gap = this.gap();
922
+ // Viewport size cap: a panel wider or taller than the screen (a fat menu
923
+ // on a 360px phone) gets an inline `max-width`/`max-height` so it scrolls
924
+ // internally instead of overflowing. An inline max-* OVERRIDES any
925
+ // stylesheet max (e.g. a select list's own 16rem max-height), so the cap
926
+ // is applied only when the panel's NATURAL size exceeds it — it never
927
+ // enlarges a panel that already limits itself. A previously applied cap is
928
+ // cleared first so the natural size is re-measured against the current
929
+ // viewport. Scroll-tracking frames skip all of this: the viewport has not
930
+ // resized mid-scroll, and clearing/re-measuring would force two extra
931
+ // reflows per frame.
932
+ if (!track && this.sizeCapped) {
933
+ el.style.removeProperty('max-width');
934
+ el.style.removeProperty('max-height');
935
+ this.sizeCapped = false;
936
+ }
937
+ let rect = el.getBoundingClientRect();
938
+ if (!track && vw > 0 && vh > 0) {
939
+ const capW = Math.max(MIN_SIZE_CAP, Math.floor(vw - 2 * gap));
940
+ const capH = Math.max(MIN_SIZE_CAP, Math.floor(vh - 2 * gap));
941
+ const capWidth = rect.width > capW;
942
+ const capHeight = rect.height > capH;
943
+ if (capWidth)
944
+ el.style.maxWidth = `${capW}px`;
945
+ if (capHeight)
946
+ el.style.maxHeight = `${capH}px`;
947
+ if (capWidth || capHeight) {
948
+ this.sizeCapped = true;
949
+ rect = el.getBoundingClientRect();
950
+ }
951
+ }
952
+ const pos = mkComputeAnchoredPosition(anchor, { width: rect.width, height: rect.height }, { width: vw, height: vh }, {
953
+ placement: this.placement(),
954
+ gap,
955
+ flip: this.flip(),
956
+ clamp: this.clamp() && !track,
957
+ rtl: this.isAnchorRtl(),
958
+ });
959
+ el.style.top = `${pos.top}px`;
960
+ el.style.left = `${pos.left}px`;
961
+ el.setAttribute('data-placement', pos.placement);
962
+ }
963
+ /** Whether the anchor renders in a right-to-left context. */
964
+ isAnchorRtl() {
965
+ const el = this.resolveAnchorEl();
966
+ const view = this.document.defaultView;
967
+ if (!el || !view)
968
+ return false;
969
+ return view.getComputedStyle(el).direction === 'rtl';
970
+ }
971
+ resolveAnchorEl() {
972
+ const a = this.anchor();
973
+ if (!a)
974
+ return null;
975
+ return a instanceof ElementRef ? a.nativeElement : a;
976
+ }
977
+ resolveAnchorRect() {
978
+ const point = this.anchorRect();
979
+ if (point) {
980
+ return {
981
+ top: point.y,
982
+ left: point.x,
983
+ right: point.x,
984
+ bottom: point.y,
985
+ width: 0,
986
+ height: 0,
987
+ };
988
+ }
989
+ return this.resolveAnchorEl()?.getBoundingClientRect() ?? null;
990
+ }
991
+ ngOnDestroy() {
992
+ if (!this.isBrowser)
993
+ return;
994
+ const el = this.host.nativeElement;
995
+ const view = this.document.defaultView;
996
+ if (this.repositionRaf != null) {
997
+ view?.cancelAnimationFrame(this.repositionRaf);
998
+ this.repositionRaf = null;
999
+ }
1000
+ view?.removeEventListener('scroll', this.onScroll, true);
1001
+ view?.removeEventListener('resize', this.onResize);
1002
+ view?.removeEventListener('blur', this.onWindowBlur);
1003
+ const visual = view?.visualViewport;
1004
+ visual?.removeEventListener('resize', this.onResize);
1005
+ visual?.removeEventListener('scroll', this.onScroll);
1006
+ this.document.removeEventListener('pointerdown', this.onDocPointerdown, true);
1007
+ const withPopover = el;
1008
+ if (this.popover && typeof withPopover.hidePopover === 'function') {
1009
+ try {
1010
+ withPopover.hidePopover();
1011
+ }
1012
+ catch {
1013
+ // Ignore — the element may already be disconnected.
1014
+ }
1015
+ el.removeAttribute('popover');
1016
+ }
1017
+ // Remove the teleported node ourselves. It was moved out of the component's
1018
+ // view into `document.body`, so Angular's view teardown removes it by
1019
+ // reference (`node.remove()`); re-inserting it anywhere would orphan a
1020
+ // detached copy, leaking one panel per open/close cycle. A plain remove is
1021
+ // idempotent whichever teardown step runs first.
1022
+ el.remove();
1023
+ }
1024
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAnchoredPanel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1025
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkAnchoredPanel, isStandalone: true, selector: "[mkAnchoredPanel]", inputs: { anchor: { classPropertyName: "anchor", publicName: "mkAnchoredPanelFor", isSignal: true, isRequired: false, transformFunction: null }, anchorRect: { classPropertyName: "anchorRect", publicName: "anchorRect", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, matchWidth: { classPropertyName: "matchWidth", publicName: "matchWidth", isSignal: true, isRequired: false, transformFunction: null }, flip: { classPropertyName: "flip", publicName: "flip", isSignal: true, isRequired: false, transformFunction: null }, clamp: { classPropertyName: "clamp", publicName: "clamp", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, exportAs: ["mkAnchoredPanel"], ngImport: i0 });
1026
+ }
1027
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAnchoredPanel, decorators: [{
1028
+ type: Directive,
1029
+ args: [{
1030
+ selector: '[mkAnchoredPanel]',
1031
+ exportAs: 'mkAnchoredPanel',
1032
+ }]
1033
+ }], propDecorators: { anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkAnchoredPanelFor", required: false }] }], anchorRect: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorRect", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], matchWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchWidth", required: false }] }], flip: [{ type: i0.Input, args: [{ isSignal: true, alias: "flip", required: false }] }], clamp: [{ type: i0.Input, args: [{ isSignal: true, alias: "clamp", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
1034
+
1035
+ /** The built-in English validation messages. */
1036
+ const MK_DEFAULT_VALIDATION = {
1037
+ required: 'This field is required',
1038
+ email: 'Enter a valid email address',
1039
+ min: ({ min }) => `Must be ${min} or more`,
1040
+ max: ({ max }) => `Must be ${max} or less`,
1041
+ minlength: ({ requiredLength }) => `Must be at least ${requiredLength} characters`,
1042
+ maxlength: ({ requiredLength }) => `Must be at most ${requiredLength} characters`,
1043
+ pattern: 'Enter a value in the expected format',
1044
+ mkMinDate: ({ min }) => `Must be on or after ${min.toLocaleDateString()}`,
1045
+ mkMaxDate: ({ max }) => `Must be on or before ${max.toLocaleDateString()}`,
1046
+ mkDateFilter: 'This date is not available',
1047
+ mkDateRangeIncomplete: 'Select both a start and an end date',
1048
+ mkMinTime: ({ min }) => `Must be at or after ${min}`,
1049
+ mkMaxTime: ({ max }) => `Must be at or before ${max}`,
1050
+ mkMaxItems: ({ max }) => `Select at most ${max} ${max === 1 ? 'item' : 'items'}`,
1051
+ mkFileSize: ({ name, maxLabel }) => `${name} is larger than ${maxLabel}`,
1052
+ mkFileType: ({ name }) => `${name} is not an accepted file type`,
1053
+ cardNumber: 'Enter a valid card number',
1054
+ iban: ({ expectedLength }) => expectedLength
1055
+ ? `Enter a valid IBAN (${expectedLength} characters)`
1056
+ : 'Enter a valid IBAN',
1057
+ postalCode: ({ example }) => `Enter a valid postal code, e.g. ${example}`,
1058
+ taxId: ({ label, example }) => `Enter a valid ${label}, e.g. ${example}`,
1059
+ unknown: 'This value is not valid',
1060
+ };
1061
+ /** The built-in English date names. */
1062
+ const MK_DEFAULT_DATE_NAMES = {
1063
+ months: [
1064
+ 'January', 'February', 'March', 'April', 'May', 'June',
1065
+ 'July', 'August', 'September', 'October', 'November', 'December',
1066
+ ],
1067
+ monthsShort: [
1068
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
1069
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
1070
+ ],
1071
+ weekdays: [
1072
+ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
1073
+ ],
1074
+ weekdaysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
1075
+ weekdaysNarrow: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
1076
+ };
1077
+ const PASSWORD_STRENGTH_LABELS = ['Weak', 'Weak', 'Fair', 'Good', 'Strong'];
1078
+ /** The built-in English strings. */
1079
+ const MK_DEFAULT_I18N = {
1080
+ validation: MK_DEFAULT_VALIDATION,
1081
+ close: 'Close',
1082
+ dismiss: 'Dismiss',
1083
+ clear: 'Clear',
1084
+ confirm: 'Confirm',
1085
+ cancel: 'Cancel',
1086
+ ok: 'OK',
1087
+ save: 'Save',
1088
+ submit: 'Submit',
1089
+ edit: 'Edit',
1090
+ remove: 'Remove',
1091
+ removeItem: (name) => `Remove ${name}`,
1092
+ empty: 'Empty',
1093
+ optional: 'Optional',
1094
+ filter: 'Filter…',
1095
+ confirmMessage: 'Are you sure?',
1096
+ decrease: 'Decrease',
1097
+ increase: 'Increase',
1098
+ loading: 'Loading…',
1099
+ noOptions: 'No options',
1100
+ noResults: 'No results',
1101
+ noData: 'No data to display',
1102
+ resultsCount: (count) => (count === 1 ? '1 result' : `${count} results`),
1103
+ previousPage: 'Go to previous page',
1104
+ nextPage: 'Go to next page',
1105
+ goToPage: (page) => `Go to page ${page}`,
1106
+ paginationLabel: 'Pagination',
1107
+ previousSlide: 'Previous slide',
1108
+ nextSlide: 'Next slide',
1109
+ goToSlide: (slide) => `Go to slide ${slide}`,
1110
+ carouselLabel: 'Carousel',
1111
+ pauseSlideshow: 'Pause slideshow',
1112
+ playSlideshow: 'Play slideshow',
1113
+ slideOf: (slide, total) => `Slide ${slide} of ${total}`,
1114
+ dateNames: MK_DEFAULT_DATE_NAMES,
1115
+ previousMonth: 'Previous month',
1116
+ nextMonth: 'Next month',
1117
+ previousYear: 'Previous year',
1118
+ nextYear: 'Next year',
1119
+ previousYears: 'Previous years',
1120
+ nextYears: 'Next years',
1121
+ selectDate: 'Select date…',
1122
+ selectRange: 'Select range…',
1123
+ selectTime: 'Select time…',
1124
+ selectMonth: 'Select month…',
1125
+ selectYear: 'Select year…',
1126
+ selectWeek: 'Select week…',
1127
+ selectPlaceholder: 'Select…',
1128
+ chooseDate: 'Choose date',
1129
+ chooseDateRange: 'Choose date range',
1130
+ chooseMonth: 'Choose month',
1131
+ chooseYear: 'Choose year',
1132
+ chooseWeek: 'Choose week',
1133
+ openCalendar: 'Open calendar',
1134
+ openTimeList: 'Open time list',
1135
+ daySegment: 'Day',
1136
+ monthSegment: 'Month',
1137
+ yearSegment: 'Year',
1138
+ countdownDays: 'days',
1139
+ countdownHours: 'hrs',
1140
+ countdownMinutes: 'min',
1141
+ countdownSeconds: 'sec',
1142
+ countdownFinished: 'Finished',
1143
+ dayEvents: (count, titles) => `${count} ${count === 1 ? 'event' : 'events'}${titles ? `: ${titles}` : ''}`,
1144
+ moreEvents: (count) => `+${count} more`,
1145
+ selectAllRows: 'Select all rows',
1146
+ selectRow: (row) => (row ? `Select row ${row}` : 'Select row'),
1147
+ expandHeader: 'Expand',
1148
+ expandRow: 'Expand row',
1149
+ collapseRow: 'Collapse row',
1150
+ expandGroup: 'Expand group',
1151
+ collapseGroup: 'Collapse group',
1152
+ groupCount: (count) => `${count} item${count === 1 ? '' : 's'}`,
1153
+ resizeColumn: 'Resize column',
1154
+ columnWidth: (column, width) => `${column} column width ${width} pixels`,
1155
+ columnMoved: (column, position, total) => `${column} moved to position ${position} of ${total}`,
1156
+ editCell: 'Press Enter to edit',
1157
+ cellSaved: (value) => `Saved ${value}`,
1158
+ sortedBy: (column, direction) => `Sorted by ${column} ${direction === 'asc' ? 'ascending' : 'descending'}`,
1159
+ sortingCleared: (column) => `Sorting cleared on ${column}`,
1160
+ showPassword: 'Show password',
1161
+ hidePassword: 'Hide password',
1162
+ passwordRuleMinLength: (length) => `At least ${length} characters`,
1163
+ passwordRuleUppercase: 'An uppercase letter',
1164
+ passwordRuleNumber: 'A number',
1165
+ passwordRuleSymbol: 'A symbol',
1166
+ passwordStrength: (score) => PASSWORD_STRENGTH_LABELS[Math.max(0, Math.min(4, score))],
1167
+ passwordStrengthLabel: 'Password strength:',
1168
+ ruleMet: 'Met:',
1169
+ ruleNotMet: 'Not met:',
1170
+ oneTimeCode: 'One-time code',
1171
+ otpDigit: (position) => `Digit ${position}`,
1172
+ numericKeypadLabel: 'Numeric keypad',
1173
+ keypadClear: 'Clear',
1174
+ keypadBackspace: 'Backspace',
1175
+ keypadDigitsEntered: (count, length) => `${count} of ${length} digits entered`,
1176
+ onScreenKeyboardLabel: 'On-screen keyboard',
1177
+ keyboardShift: 'Shift',
1178
+ keyboardSpace: 'Space',
1179
+ keyboardEnter: 'Enter',
1180
+ keyboardAltLayer: 'More characters',
1181
+ keyboardBaseLayer: 'Letters',
1182
+ ratingLabel: 'Rating',
1183
+ ratingValueText: (value, max) => `${value} of ${max} stars`,
1184
+ minimum: 'Minimum',
1185
+ maximum: 'Maximum',
1186
+ chooseColor: 'Choose color',
1187
+ hexValue: 'Hex value',
1188
+ presetColors: 'Preset colors',
1189
+ chooseCountry: 'Choose country',
1190
+ searchCountries: 'Search countries…',
1191
+ phoneNumber: 'Phone number',
1192
+ postalCode: 'Postal code',
1193
+ amount: 'Amount',
1194
+ cardNumber: 'Card number',
1195
+ cardBrand: (brand) => `Card brand: ${brand}`,
1196
+ iban: 'IBAN',
1197
+ taxId: 'Tax ID',
1198
+ signature: 'Signature',
1199
+ jsonLabel: 'JSON',
1200
+ logViewerLabel: 'Log output',
1201
+ logFollow: 'Follow',
1202
+ logCopyAll: 'Copy log',
1203
+ logWrapLines: 'Wrap lines',
1204
+ eventCalendarGrabbed: (title, from, to) => `${title} grabbed, ${from} – ${to}. Use the arrow keys to move, ` +
1205
+ 'Shift with Up or Down to change the end time, Enter to save, ' +
1206
+ 'Escape to cancel.',
1207
+ eventCalendarPosition: (title, day, from, to) => `${title}, ${day}, ${from} – ${to}.`,
1208
+ eventCalendarMoved: (title, day, from, to) => `${title} moved to ${day}, ${from} – ${to}.`,
1209
+ eventCalendarResized: (title, to) => `${title} now ends at ${to}.`,
1210
+ eventCalendarEditCancelled: 'Move cancelled. The event keeps its original time.',
1211
+ eventCalendarMovableEvent: 'Movable event',
1212
+ previousImage: 'Previous image',
1213
+ nextImage: 'Next image',
1214
+ imageOf: (index, total) => `Image ${index} of ${total}`,
1215
+ viewImage: (alt) => (alt ? `View ${alt}` : 'View image'),
1216
+ imageFailed: 'Image failed to load',
1217
+ zoom: 'Zoom',
1218
+ zoomIn: 'Zoom in',
1219
+ zoomOut: 'Zoom out',
1220
+ mediaLibrary: 'Media library',
1221
+ dropzoneLabel: 'Drag files here or click to browse',
1222
+ uploadFailed: 'Upload failed',
1223
+ retryUpload: 'Retry upload',
1224
+ errorSummaryTitle: 'There is a problem',
1225
+ available: 'Available',
1226
+ selected: 'Selected',
1227
+ transferSelected: (target) => `Move selected to ${target}`,
1228
+ transferAll: (target) => `Move all to ${target}`,
1229
+ itemsMoved: (count, target) => `Moved ${count} ${count === 1 ? 'item' : 'items'} to ${target}`,
1230
+ notificationsTitle: 'Notifications',
1231
+ allCaughtUp: "You're all caught up",
1232
+ markAllRead: 'Mark all read',
1233
+ notificationsUnread: (count) => `Notifications, ${count} unread`,
1234
+ unread: 'Unread',
1235
+ tourStepOf: (step, total) => `Step ${step} of ${total}`,
1236
+ tourSkip: 'Skip',
1237
+ tourPrevious: 'Previous',
1238
+ tourNext: 'Next',
1239
+ tourDone: 'Done',
1240
+ commandPalettePlaceholder: 'Type a command or search…',
1241
+ commandPaletteLabel: 'Command palette',
1242
+ stepCompleted: 'Completed',
1243
+ stepError: 'Has errors',
1244
+ diffAddedLine: 'Added:',
1245
+ diffRemovedLine: 'Removed:',
1246
+ itemAdded: (name) => `${name} added`,
1247
+ itemRemoved: (name) => `${name} removed`,
1248
+ resizePanes: 'Resize panes',
1249
+ backToTop: 'Back to top',
1250
+ breadcrumbLabel: 'Breadcrumb',
1251
+ fabLabel: 'Actions',
1252
+ diffBefore: 'Before',
1253
+ diffAfter: 'After',
1254
+ diffChanges: 'Changes',
1255
+ skipToContent: 'Skip to content',
1256
+ primaryNav: 'Primary',
1257
+ dndPickedUp: (position, total) => `Picked up. Item ${position} of ${total}. ` +
1258
+ 'Use the arrow keys to move, space or enter to drop, escape to cancel.',
1259
+ dndMoved: (position, total) => `Moved to position ${position} of ${total}.`,
1260
+ dndMovedToList: (list, position, total) => `Moved to ${list}, position ${position} of ${total}.`,
1261
+ dndDropped: (position) => `Dropped at position ${position}.`,
1262
+ dndCancelled: 'Movement cancelled. Item returned to its starting position.',
1263
+ repeaterAddRow: 'Add row',
1264
+ repeaterRemoveRow: (index) => `Remove row ${index}`,
1265
+ repeaterReorderRow: (index) => `Reorder row ${index}`,
1266
+ repeaterRowMoved: (from, to) => `Row moved from position ${from} to position ${to}.`,
1267
+ fileRejectedType: (name) => `${name}: unsupported type`,
1268
+ fileRejectedSize: (name, limit) => `${name}: exceeds ${limit}`,
1269
+ fileRejectedCount: (name, max) => `${name}: over the ${max}-file limit`,
1270
+ chartCategory: 'Category',
1271
+ chartValue: 'Value',
1272
+ chartSeries: 'Series',
1273
+ chartSlice: 'Slice',
1274
+ chartStage: 'Stage',
1275
+ chartConversion: 'Conversion',
1276
+ chartAxis: 'Axis',
1277
+ chartShare: 'Share',
1278
+ chartLabel: 'Label',
1279
+ qrCodeLabel: (text) => `QR code: ${text}`,
1280
+ blockEditor: {
1281
+ addBlock: 'Add block',
1282
+ addFirstBlock: 'Add your first block',
1283
+ insertBlockHere: 'Insert a block here',
1284
+ blockInserter: 'Block inserter',
1285
+ searchBlocks: 'Search blocks…',
1286
+ blocks: 'Blocks',
1287
+ moveBlockUp: 'Move block up',
1288
+ moveBlockDown: 'Move block down',
1289
+ blockOptions: 'Block options',
1290
+ duplicate: 'Duplicate',
1291
+ remove: 'Remove',
1292
+ textFormatting: 'Text formatting',
1293
+ altText: 'Alt text',
1294
+ caption: 'Caption',
1295
+ alignment: 'Alignment',
1296
+ replaceImage: 'Replace image',
1297
+ imageUrl: 'Image URL',
1298
+ externalContent: 'External content',
1299
+ embedUrl: 'Embed URL',
1300
+ columnSettings: 'Column settings',
1301
+ columns: 'Columns',
1302
+ ratio: 'Ratio',
1303
+ gap: 'Gap',
1304
+ align: 'Align',
1305
+ justify: 'Justify',
1306
+ headingLevel: (level) => `Heading level ${level}`,
1307
+ editorLabel: 'Block content editor',
1308
+ emptyBlockPlaceholder: 'Type / to choose a block, or start writing…',
1309
+ dragHandle: 'Drag handle',
1310
+ turnInto: (label) => `Turn into ${label}`,
1311
+ unknownBlock: (type) => `Unknown block: ${type}`,
1312
+ blockAdded: (label) => `${label} added`,
1313
+ blockDuplicated: 'Block duplicated',
1314
+ blockDeleted: (label) => `${label} deleted`,
1315
+ blockMovedUp: 'Block moved up',
1316
+ blockMovedDown: 'Block moved down',
1317
+ turnedInto: (label) => `Turned into ${label}`,
1318
+ noBlocksMatch: (query) => `No blocks match “${query}”.`,
1319
+ groupText: 'Text',
1320
+ groupMedia: 'Media',
1321
+ groupLayout: 'Layout',
1322
+ blockParagraph: 'Paragraph',
1323
+ blockParagraphDesc: 'Rich text with inline formatting.',
1324
+ blockHeading: 'Heading',
1325
+ blockHeadingDesc: 'A section title (H1–H4).',
1326
+ blockList: 'List',
1327
+ blockListDesc: 'Bulleted or numbered list.',
1328
+ blockQuote: 'Quote',
1329
+ blockQuoteDesc: 'A blockquote with optional citation.',
1330
+ blockCode: 'Code',
1331
+ blockCodeDesc: 'Preformatted, monospace code.',
1332
+ blockImage: 'Image',
1333
+ blockImageDesc: 'Upload or link an image.',
1334
+ blockEmbed: 'Embed',
1335
+ blockEmbedDesc: 'YouTube, Vimeo or any URL.',
1336
+ blockButton: 'Button',
1337
+ blockButtonDesc: 'A call-to-action link button.',
1338
+ blockDivider: 'Divider',
1339
+ blockDividerDesc: 'A horizontal separator.',
1340
+ blockColumns: 'Columns',
1341
+ blockColumnsDesc: 'A responsive multi-column layout.',
1342
+ bold: 'Bold',
1343
+ italic: 'Italic',
1344
+ underline: 'Underline',
1345
+ strikethrough: 'Strikethrough',
1346
+ inlineCode: 'Inline code',
1347
+ link: 'Link',
1348
+ clearFormatting: 'Clear formatting',
1349
+ linkUrlPrompt: 'Link URL',
1350
+ editableText: 'Editable text',
1351
+ headingPlaceholder: (level) => `Heading ${level}`,
1352
+ headingLevelGroup: 'Heading level',
1353
+ listStyle: 'List style',
1354
+ bulleted: 'Bulleted',
1355
+ numbered: 'Numbered',
1356
+ listItem: 'List item',
1357
+ quoteText: 'Quote text',
1358
+ citation: 'Citation',
1359
+ addCitation: '— Add a citation',
1360
+ codeLanguage: 'Code language',
1361
+ codeLanguagePlaceholder: 'language (optional)',
1362
+ enterCode: 'Enter code…',
1363
+ imageWidth: (percent) => `Width: ${percent}%`,
1364
+ uploading: 'Uploading…',
1365
+ dropImagePrompt: 'Drag & drop an image, or',
1366
+ chooseFile: 'Choose file',
1367
+ pasteImageUrl: '…or paste an image URL',
1368
+ notAnImage: 'Please choose an image file.',
1369
+ uploadFailed: 'Upload failed. Try again or paste a URL.',
1370
+ imageAdded: 'Image added',
1371
+ pasteEmbedUrl: 'Paste a YouTube, Vimeo or other URL…',
1372
+ embedFallbackNote: 'That URL can’t be embedded, but it will render as a link.',
1373
+ embedTitle: (provider) => `${provider} embed`,
1374
+ embedAdded: (provider) => `${provider} embed added`,
1375
+ embeddedContent: 'Embedded content',
1376
+ buttonLabel: 'Label',
1377
+ buttonLink: 'Link (href)',
1378
+ buttonTone: 'Tone',
1379
+ buttonVariant: 'Variant',
1380
+ buttonDefaultLabel: 'Click me',
1381
+ alignLeft: 'Left',
1382
+ alignCenter: 'Center',
1383
+ alignRight: 'Right',
1384
+ tonePrimary: 'Primary',
1385
+ toneNeutral: 'Neutral',
1386
+ toneSuccess: 'Success',
1387
+ toneWarning: 'Warning',
1388
+ toneDanger: 'Danger',
1389
+ toneInfo: 'Info',
1390
+ variantSolid: 'Solid',
1391
+ variantSoft: 'Soft',
1392
+ variantOutline: 'Outline',
1393
+ ratioEqual: 'Equal',
1394
+ alignStretch: 'Stretch',
1395
+ alignTop: 'Top',
1396
+ alignMiddle: 'Middle',
1397
+ alignBottom: 'Bottom',
1398
+ justifyStart: 'Start',
1399
+ justifyCenter: 'Center',
1400
+ justifyEnd: 'End',
1401
+ justifyBetween: 'Space between',
1402
+ },
1403
+ };
1404
+ /**
1405
+ * The active string map. Defaults to {@link MK_DEFAULT_I18N}; override with
1406
+ * {@link provideMkI18n}. Inject it (`inject(MK_I18N)`) wherever a built-in
1407
+ * string is rendered.
1408
+ */
1409
+ const MK_I18N = new InjectionToken('MK_I18N', {
1410
+ providedIn: 'root',
1411
+ factory: () => MK_DEFAULT_I18N,
1412
+ });
1413
+ /**
1414
+ * Provide localised strings (merged over the English defaults) — pass any
1415
+ * subset. The nested `dateNames`, `blockEditor` and `validation` groups are
1416
+ * merged deeply, so partial overrides of those work too.
1417
+ *
1418
+ * ```ts
1419
+ * bootstrapApplication(App, {
1420
+ * providers: [provideMkI18n({ noResults: 'Brak wyników', close: 'Zamknij' })],
1421
+ * });
1422
+ * ```
1423
+ */
1424
+ function provideMkI18n(overrides) {
1425
+ const value = {
1426
+ ...MK_DEFAULT_I18N,
1427
+ ...overrides,
1428
+ dateNames: { ...MK_DEFAULT_DATE_NAMES, ...overrides.dateNames },
1429
+ blockEditor: { ...MK_DEFAULT_I18N.blockEditor, ...overrides.blockEditor },
1430
+ validation: { ...MK_DEFAULT_VALIDATION, ...overrides.validation },
1431
+ };
1432
+ return { provide: MK_I18N, useValue: value };
1433
+ }
1434
+
1435
+ /**
1436
+ * Field context — the contract a field wrapper (`mk-form-field`) exposes to
1437
+ * the control nested inside it. Controls inject it optionally
1438
+ * (`inject(MkFieldContext, { optional: true })`) to adopt the wrapper's
1439
+ * control id and to reflect its required/error state and `aria-describedby`
1440
+ * wiring — without depending on the `@mk-kit/ui/forms` entry point.
1441
+ *
1442
+ * `MkFormField` provides itself under this token; standalone usage (no
1443
+ * wrapper) simply yields `null`.
1444
+ */
1445
+ class MkFieldContext {
1446
+ }
1447
+
1448
+ /**
1449
+ * Dependency-free syntax highlighting for {@link MkCodeEditor}. Each function
1450
+ * takes source text and returns an HTML string of `<span class="mk-tok-…">`
1451
+ * tokens. Input is HTML-escaped first, so the result is safe to render.
1452
+ */
1453
+ /** Escape the three characters that could break out of HTML text content. */
1454
+ function mkEscapeHtml(src) {
1455
+ return src.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1456
+ }
1457
+ /**
1458
+ * Highlight a JSON document. Recognises object keys, strings, numbers, the
1459
+ * `true`/`false`/`null` literals and structural punctuation. Invalid JSON is
1460
+ * still highlighted token-by-token (the editor validates separately).
1461
+ */
1462
+ function mkHighlightJson(src) {
1463
+ const esc = mkEscapeHtml(src);
1464
+ return esc.replace(
1465
+ // 1: string (+ optional following colon → key) · 2: colon
1466
+ // 3: keyword · 4: number · 5: punctuation
1467
+ /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}\[\],:])/g, (match, str, colon, keyword, num, punc) => {
1468
+ if (str !== undefined) {
1469
+ if (colon !== undefined) {
1470
+ return `<span class="mk-tok-key">${str}</span><span class="mk-tok-punc">${colon}</span>`;
1471
+ }
1472
+ return `<span class="mk-tok-str">${str}</span>`;
1473
+ }
1474
+ if (keyword !== undefined)
1475
+ return `<span class="mk-tok-kw">${keyword}</span>`;
1476
+ if (num !== undefined)
1477
+ return `<span class="mk-tok-num">${num}</span>`;
1478
+ if (punc !== undefined)
1479
+ return `<span class="mk-tok-punc">${punc}</span>`;
1480
+ return match;
1481
+ });
1482
+ }
1483
+ /** Highlight `src` for `language`, falling back to escaped plain text. */
1484
+ function mkHighlight(src, language) {
1485
+ return language === 'json' ? mkHighlightJson(src) : mkEscapeHtml(src);
1486
+ }
1487
+
1488
+ /**
1489
+ * Re-validates the bound form control whenever a validator's inputs change.
1490
+ *
1491
+ * A `Validator` whose constraints come from component inputs (`[min]`,
1492
+ * `[max]`, `[required]`, …) must tell Angular when those inputs change,
1493
+ * otherwise the control keeps the verdict computed under the old constraints.
1494
+ * `NgModel`/`FormControlName` pass a callback to `registerOnValidatorChange`
1495
+ * for exactly this; call it from an effect over the constraint signals.
1496
+ *
1497
+ * Must be called from an injection context (i.e. as a field initialiser).
1498
+ *
1499
+ * ```ts
1500
+ * private readonly validatorChange = mkValidatorChange(() => {
1501
+ * this.min();
1502
+ * this.max();
1503
+ * });
1504
+ *
1505
+ * registerOnValidatorChange(fn: () => void): void {
1506
+ * this.validatorChange.register(fn);
1507
+ * }
1508
+ * ```
1509
+ *
1510
+ * @param deps Reads every signal the validator depends on.
1511
+ */
1512
+ function mkValidatorChange(deps) {
1513
+ let onChange = () => { };
1514
+ let primed = false;
1515
+ effect(() => {
1516
+ deps();
1517
+ // Skip the effect's initial run: the control validates once on bind
1518
+ // anyway, and re-entering validation during setup is wasted work.
1519
+ if (!primed) {
1520
+ primed = true;
1521
+ return;
1522
+ }
1523
+ // The callback synchronously calls `updateValueAndValidity()`, which reads
1524
+ // and writes signals — keep it out of this effect's dependency graph.
1525
+ untracked(() => onChange());
1526
+ });
1527
+ return {
1528
+ register(fn) {
1529
+ onChange = fn;
1530
+ },
1531
+ notify() {
1532
+ onChange();
1533
+ },
1534
+ };
1535
+ }
1536
+
1537
+ /**
1538
+ * Resolves the message for the first error on a control, mirroring how
1539
+ * `mat-error` shows one message at a time.
1540
+ *
1541
+ * Lookup order per key: the field's `overrides`, then the i18n `validation`
1542
+ * table, then the error payload's own `message` string (so a custom validator
1543
+ * can carry its text), then the generic fallback. Keys are visited in the
1544
+ * order the validators put them on the control, so composing
1545
+ * `[Validators.required, Validators.email]` surfaces "required" while empty.
1546
+ *
1547
+ * @returns The message, or `null` when there are no errors.
1548
+ */
1549
+ function mkFirstErrorMessage(errors, strings, overrides) {
1550
+ if (!errors)
1551
+ return null;
1552
+ for (const key of Object.keys(errors)) {
1553
+ const payload = errors[key];
1554
+ const override = overrides?.[key];
1555
+ if (typeof override === 'string')
1556
+ return override;
1557
+ if (typeof override === 'function')
1558
+ return override(payload);
1559
+ const builtin = strings[key];
1560
+ if (typeof builtin === 'string')
1561
+ return builtin;
1562
+ if (typeof builtin === 'function')
1563
+ return builtin(payload);
1564
+ if (payload && typeof payload === 'object' && typeof payload.message === 'string') {
1565
+ return payload.message;
1566
+ }
1567
+ }
1568
+ return errors ? strings.unknown : null;
1569
+ }
1570
+
1571
+ /**
1572
+ * Generated bundle index. Do not edit.
1573
+ */
1574
+
1575
+ export { MK_DEFAULT_DATE_NAMES, MK_DEFAULT_I18N, MK_DEFAULT_VALIDATION, MK_I18N, MK_OVERLAY_DATA, MkAnchoredPanel, MkFieldContext, MkFocusTrap, MkLiveAnnouncer, MkOverlayRef, MkOverlayService, MkThemeService, mkComputeAnchoredPosition, mkFirstErrorMessage, mkGetFocusable, mkHighlight, mkHighlightJson, mkUniqueId, mkValidatorChange, provideMkI18n };
1576
+ //# sourceMappingURL=mk-kit-ui-core.mjs.map