@mk-kit/ui 0.41.0 → 0.42.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.
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, ChangeDetectionStrategy, Component, booleanAttribute, signal, computed, numberAttribute, contentChildren, ElementRef, effect, output, inject, DestroyRef, afterNextRender, DOCUMENT, PLATFORM_ID, Injector, viewChild, model, forwardRef, untracked, contentChild, TemplateRef, Directive, afterRenderEffect, ViewEncapsulation } from '@angular/core';
3
- import { MkIcon } from '@mk-kit/ui/icon';
2
+ import { input, ChangeDetectionStrategy, Component, booleanAttribute, inject, ElementRef, Renderer2, DOCUMENT, PLATFORM_ID, numberAttribute, computed, effect, Directive, signal, contentChildren, output, DestroyRef, afterNextRender, afterRenderEffect, untracked, Injector, viewChild, model, forwardRef, contentChild, TemplateRef, ViewEncapsulation } from '@angular/core';
3
+ import { isPlatformBrowser, DOCUMENT as DOCUMENT$1, NgTemplateOutlet } from '@angular/common';
4
4
  import { mkUniqueId, MK_I18N, MkFieldContext, mkHighlight } from '@mk-kit/ui/core';
5
- import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
5
+ import { MkIcon } from '@mk-kit/ui/icon';
6
6
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
7
7
  import { MkCopyToClipboard } from '@mk-kit/ui/directives';
8
8
  import { mkMoveItemInArray, MkDropList, MkDrag } from '@mk-kit/ui/dnd';
@@ -151,6 +151,186 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
151
151
  }, template: "@if (!dot()) {\n <ng-content />\n}\n", styles: ["@charset \"UTF-8\";:host{--_main: var(--mk-primary);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;gap:var(--mk-space-1);height:1.375rem;min-width:1.375rem;padding:0 var(--mk-space-2);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-pill);font-family:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);line-height:1;white-space:nowrap;vertical-align:middle}:host([data-tone=primary]){--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_active: var(--mk-primary-active);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text)}:host([data-tone=success]){--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_active: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}:host([data-tone=warning]){--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_active: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}:host([data-tone=danger]){--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_active: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}:host([data-tone=info]){--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_active: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}:host([data-tone=neutral]){--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_active: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}:host(.mk-badge--sm){height:1.125rem;min-width:1.125rem;padding:0 var(--mk-space-1);font-size:var(--mk-font-size-xs)}:host(.mk-badge--lg){height:1.625rem;min-width:1.625rem;padding:0 var(--mk-space-3);font-size:var(--mk-font-size-sm)}:host(.mk-badge--solid){background-color:var(--_main);color:var(--_contrast)}:host(.mk-badge--soft){background-color:var(--_subtle);color:var(--_subtle-text)}:host(.mk-badge--outline){background-color:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}:host(.mk-badge--dot){width:.625rem;height:.625rem;min-width:0;padding:0;border-radius:var(--mk-radius-circle)}:host(.mk-badge--dot.mk-badge--sm){width:.5rem;height:.5rem}:host(.mk-badge--dot.mk-badge--lg){width:.75rem;height:.75rem}\n"] }]
152
152
  }], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
153
153
 
154
+ /**
155
+ * Badge overlay — anchors a small badge (count, dot or short text) to a corner
156
+ * of ANY element: an icon button, an avatar, a tab label. The host stays a
157
+ * single tab stop; the badge is a decorative, non-focusable child.
158
+ *
159
+ * - Numbers above `mkBadgeOverlayMax` (default 99) collapse to `99+`.
160
+ * - `mkBadgeOverlayDot` renders a textless dot ("something is new").
161
+ * - `mkBadgeOverlayAriaLabel` is the screen-reader text ("3 unread"). It is
162
+ * rendered in a visually hidden span and wired to the host with
163
+ * `aria-describedby`, so it is announced after the host's own name even
164
+ * when the host has an `aria-label`. When it is given, the visible badge is
165
+ * `aria-hidden` so the count is not read twice.
166
+ * - Positions use logical insets, so `top-end` sits at the top-right in LTR
167
+ * and the top-left in RTL.
168
+ *
169
+ * The badge is painted with the global `.mk-badge-overlay` styles from
170
+ * `@mk-kit/ui/styles.css` and follows the `--mk-*` tone tokens.
171
+ *
172
+ * ```html
173
+ * <button mkButton iconOnly aria-label="Notifications"
174
+ * [mkBadgeOverlay]="unread()" mkBadgeOverlayTone="danger"
175
+ * [mkBadgeOverlayAriaLabel]="unread() + ' unread'">
176
+ * <mk-icon name="bell" />
177
+ * </button>
178
+ *
179
+ * <mk-avatar name="Ada Lovelace" mkBadgeOverlay mkBadgeOverlayDot
180
+ * mkBadgeOverlayTone="success" mkBadgeOverlayPosition="bottom-end" />
181
+ * ```
182
+ */
183
+ class MkBadgeOverlay {
184
+ host = inject(ElementRef);
185
+ renderer = inject(Renderer2);
186
+ document = inject(DOCUMENT);
187
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
188
+ /**
189
+ * Badge content — a count or a short label. `null` / empty hides the badge
190
+ * unless `mkBadgeOverlayDot` is set. Numeric strings are treated as numbers
191
+ * so `mkBadgeOverlay="120"` also collapses to `99+`.
192
+ */
193
+ content = input(null, { ...(ngDevMode ? { debugName: "content" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlay' });
194
+ /** Corner the badge is anchored to (logical: `end` flips in RTL). */
195
+ position = input('top-end', { ...(ngDevMode ? { debugName: "position" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlayPosition' });
196
+ /** Semantic color tone (same scale as `mk-badge`). */
197
+ tone = input('primary', { ...(ngDevMode ? { debugName: "tone" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlayTone' });
198
+ /** Render a textless dot instead of the content. */
199
+ dot = input(false, { ...(ngDevMode ? { debugName: "dot" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlayDot',
200
+ transform: booleanAttribute });
201
+ /** Numbers above this render as `<max>+`. `0` disables the cap. */
202
+ max = input(99, { ...(ngDevMode ? { debugName: "max" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlayMax',
203
+ transform: numberAttribute });
204
+ /** Hide the badge while keeping the directive (and its aria wiring) in place. */
205
+ hidden = input(false, { ...(ngDevMode ? { debugName: "hidden" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlayHidden',
206
+ transform: booleanAttribute });
207
+ /**
208
+ * Screen-reader text for the badge, e.g. `"3 unread"`. Announced as the
209
+ * host's description; the visible badge becomes `aria-hidden`.
210
+ */
211
+ ariaLabel = input('', { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'mkBadgeOverlayAriaLabel' });
212
+ /** The text painted in the badge (`''` in dot mode). */
213
+ label = computed(() => {
214
+ if (this.dot())
215
+ return '';
216
+ const raw = this.content();
217
+ if (raw === null || raw === undefined || raw === '')
218
+ return '';
219
+ const max = this.max();
220
+ const n = typeof raw === 'number' ? raw : /^\d+$/.test(raw) ? Number(raw) : NaN;
221
+ if (Number.isFinite(n) && max > 0 && n > max)
222
+ return `${max}+`;
223
+ return String(raw);
224
+ }, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
226
+ /** Whether the badge is currently rendered. */
227
+ visible = computed(() => !this.hidden() && (this.dot() || this.label() !== ''), /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
229
+ srId = mkUniqueId('mk-badge-overlay');
230
+ badge = null;
231
+ sr = null;
232
+ /** Set when WE made the host positioned, so we can undo it on removal. */
233
+ positionedHost = false;
234
+ constructor() {
235
+ if (!this.isBrowser)
236
+ return;
237
+ effect(() => {
238
+ const visible = this.visible();
239
+ if (!visible) {
240
+ this.detach();
241
+ return;
242
+ }
243
+ const badge = this.attach();
244
+ const dot = this.dot();
245
+ const srText = this.ariaLabel().trim();
246
+ this.renderer.setAttribute(badge, 'data-tone', this.tone());
247
+ this.renderer.setAttribute(badge, 'data-position', this.position());
248
+ if (dot) {
249
+ this.renderer.addClass(badge, 'mk-badge-overlay--dot');
250
+ }
251
+ else {
252
+ this.renderer.removeClass(badge, 'mk-badge-overlay--dot');
253
+ }
254
+ this.renderer.setProperty(badge, 'textContent', this.label());
255
+ // A dot has nothing to read; a labelled badge is described via the
256
+ // hidden span instead — either way the visual is hidden from AT.
257
+ if (dot || srText) {
258
+ this.renderer.setAttribute(badge, 'aria-hidden', 'true');
259
+ }
260
+ else {
261
+ this.renderer.removeAttribute(badge, 'aria-hidden');
262
+ }
263
+ this.syncSr(srText);
264
+ });
265
+ }
266
+ attach() {
267
+ if (this.badge)
268
+ return this.badge;
269
+ const el = this.host.nativeElement;
270
+ const view = this.document.defaultView;
271
+ const position = view?.getComputedStyle(el).position;
272
+ if (!position || position === 'static') {
273
+ this.renderer.setStyle(el, 'position', 'relative');
274
+ this.positionedHost = true;
275
+ }
276
+ const badge = this.renderer.createElement('span');
277
+ this.renderer.addClass(badge, 'mk-badge-overlay');
278
+ this.renderer.appendChild(el, badge);
279
+ this.badge = badge;
280
+ return badge;
281
+ }
282
+ detach() {
283
+ if (this.badge) {
284
+ this.renderer.removeChild(this.host.nativeElement, this.badge);
285
+ this.badge = null;
286
+ }
287
+ this.syncSr('');
288
+ if (this.positionedHost) {
289
+ this.renderer.removeStyle(this.host.nativeElement, 'position');
290
+ this.positionedHost = false;
291
+ }
292
+ }
293
+ /** Create / update / remove the hidden description and its aria-describedby link. */
294
+ syncSr(text) {
295
+ const el = this.host.nativeElement;
296
+ if (text) {
297
+ if (!this.sr) {
298
+ const sr = this.renderer.createElement('span');
299
+ this.renderer.addClass(sr, 'mk-visually-hidden');
300
+ this.renderer.setAttribute(sr, 'id', this.srId);
301
+ this.renderer.appendChild(el, sr);
302
+ this.sr = sr;
303
+ const ids = (el.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
304
+ if (!ids.includes(this.srId))
305
+ ids.push(this.srId);
306
+ this.renderer.setAttribute(el, 'aria-describedby', ids.join(' '));
307
+ }
308
+ this.renderer.setProperty(this.sr, 'textContent', text);
309
+ }
310
+ else if (this.sr) {
311
+ this.renderer.removeChild(el, this.sr);
312
+ this.sr = null;
313
+ const ids = (el.getAttribute('aria-describedby') ?? '')
314
+ .split(/\s+/)
315
+ .filter((id) => id && id !== this.srId);
316
+ if (ids.length) {
317
+ this.renderer.setAttribute(el, 'aria-describedby', ids.join(' '));
318
+ }
319
+ else {
320
+ this.renderer.removeAttribute(el, 'aria-describedby');
321
+ }
322
+ }
323
+ }
324
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBadgeOverlay, deps: [], target: i0.ɵɵFactoryTarget.Directive });
325
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.7", type: MkBadgeOverlay, isStandalone: true, selector: "[mkBadgeOverlay]", inputs: { content: { classPropertyName: "content", publicName: "mkBadgeOverlay", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "mkBadgeOverlayPosition", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "mkBadgeOverlayTone", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "mkBadgeOverlayDot", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "mkBadgeOverlayMax", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "mkBadgeOverlayHidden", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "mkBadgeOverlayAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
326
+ }
327
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBadgeOverlay, decorators: [{
328
+ type: Directive,
329
+ args: [{
330
+ selector: '[mkBadgeOverlay]',
331
+ }]
332
+ }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlay", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlayPosition", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlayTone", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlayDot", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlayMax", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlayHidden", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mkBadgeOverlayAriaLabel", required: false }] }] } });
333
+
154
334
  /**
155
335
  * Tag — a non-interactive label for categories, keywords or metadata. Unlike
156
336
  * {@link MkChip} it carries no interaction; use it purely for display.
@@ -1165,6 +1345,15 @@ function mkChartTapPin(apply) {
1165
1345
  */
1166
1346
  const CHAR_PX = 6.2;
1167
1347
  const estimateLabelPx = (label) => label.length * CHAR_PX;
1348
+ /**
1349
+ * Fraction of its band a label may fill before `auto` tilts the axis.
1350
+ *
1351
+ * Not 1.0: labels that *technically* fit still read as a cramped run of words
1352
+ * with no gap between them ("mar" "kwi" "maj" touching each other), which is
1353
+ * the look tilting exists to avoid. Leaving a fifth of the band as breathing
1354
+ * room tilts the tight cases and leaves genuinely roomy axes alone.
1355
+ */
1356
+ const TILT_THRESHOLD = 0.8;
1168
1357
  /**
1169
1358
  * BarChart — compares a measure across categories with one or more series
1170
1359
  * (grouped or `stacked`), `vertical` (default) or `horizontal`. Dependency-free
@@ -1257,7 +1446,7 @@ class MkBarChart {
1257
1446
  if (!count)
1258
1447
  return 0;
1259
1448
  const band = Math.max(this.drawWidth() - 58, 1) / count;
1260
- return this.widestLabelPx() <= band ? 0 : 45;
1449
+ return this.widestLabelPx() <= band * TILT_THRESHOLD ? 0 : 45;
1261
1450
  }, /* @ts-ignore */
1262
1451
  ...(ngDevMode ? [{ debugName: "resolvedAngle" }] : /* istanbul ignore next */ []));
1263
1452
  /**
@@ -2222,18 +2411,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
2222
2411
  }, template: "<div class=\"mk-chart__frame\">\n <svg\n class=\"mk-chart__svg\"\n [attr.viewBox]=\"'0 0 ' + width() + ' ' + height()\"\n preserveAspectRatio=\"xMidYMid meet\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <!-- Horizontal gridlines + y-axis labels -->\n @for (row of yGrid(); track row.value) {\n @if (showGrid()) {\n <line\n class=\"mk-chart__grid\"\n [attr.x1]=\"row.x1\"\n [attr.x2]=\"row.x2\"\n [attr.y1]=\"row.y\"\n [attr.y2]=\"row.y\"\n />\n }\n <text\n class=\"mk-chart__axis-label mk-chart__axis-label--y\"\n [attr.x]=\"plot().x - 8\"\n [attr.y]=\"row.y\"\n >\n {{ row.label }}\n </text>\n }\n\n <!-- x-axis labels -->\n @for (col of xGrid(); track col.value) {\n <text\n class=\"mk-chart__axis-label mk-chart__axis-label--x\"\n [attr.x]=\"col.x\"\n [attr.y]=\"height() - 12\"\n >\n {{ col.label }}\n </text>\n }\n\n <!-- Axis titles -->\n @if (xLabel()) {\n <text\n class=\"mk-chart__axis-title mk-chart__axis-title--x\"\n [attr.x]=\"plot().x + plot().w / 2\"\n [attr.y]=\"height() - 1\"\n >\n {{ xLabel() }}\n </text>\n }\n @if (yLabel()) {\n <text\n class=\"mk-chart__axis-title mk-chart__axis-title--y\"\n [attr.transform]=\"'rotate(-90 10 ' + (plot().y + plot().h / 2) + ')'\"\n [attr.x]=\"10\"\n [attr.y]=\"plot().y + plot().h / 2\"\n >\n {{ yLabel() }}\n </text>\n }\n\n <!-- Points -->\n @for (p of points(); track $index; let i = $index) {\n <circle\n class=\"mk-chart__point\"\n [class.mk-chart__point--active]=\"hoverKey() === keyOf(i)\"\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [style.fill]=\"p.color\"\n (pointerenter)=\"setHover(keyOf(i))\"\n (pointerleave)=\"setHover(null)\"\n (pointerdown)=\"pinHover(keyOf(i), $event)\"\n />\n }\n </svg>\n\n @if (tooltip(); as t) {\n <div class=\"mk-chart__tooltip\" [style.left.%]=\"t.left\" [style.top.%]=\"t.top\">\n <span class=\"mk-chart__tooltip-title\">\n <span class=\"mk-chart__swatch\" [style.background]=\"t.color\"></span>\n {{ t.title }}\n </span>\n <span class=\"mk-chart__tooltip-row\">\n <span class=\"mk-chart__tooltip-name\">{{ xLabel() || 'x' }}</span>\n <span class=\"mk-chart__tooltip-value\">{{ t.x }}</span>\n </span>\n <span class=\"mk-chart__tooltip-row\">\n <span class=\"mk-chart__tooltip-name\">{{ yLabel() || 'y' }}</span>\n <span class=\"mk-chart__tooltip-value\">{{ t.y }}</span>\n </span>\n </div>\n }\n</div>\n\n@if (legendVisible()) {\n <ul class=\"mk-chart__legend\">\n @for (item of legend(); track item.name) {\n <li class=\"mk-chart__legend-item\">\n <span class=\"mk-chart__swatch\" [style.background]=\"item.color\"></span>\n {{ item.name }}\n </li>\n }\n </ul>\n}\n\n<table class=\"mk-chart__sr-table\">\n <caption>{{ resolvedLabel() }}</caption>\n <thead>\n <tr>\n <th scope=\"col\">{{ i18n.chartSeries }}</th>\n <th scope=\"col\">{{ xLabel() || 'x' }}</th>\n <th scope=\"col\">{{ yLabel() || 'y' }}</th>\n </tr>\n </thead>\n <tbody>\n @for (s of series(); track s.name) {\n @for (p of s.points; track $index) {\n <tr>\n <th scope=\"row\">{{ s.name }}</th>\n <td>{{ p.x }}</td>\n <td>{{ p.y }}</td>\n </tr>\n }\n }\n </tbody>\n</table>\n", styles: ["@charset \"UTF-8\";:host{display:block;font-family:var(--mk-font-sans);color:var(--mk-text)}.mk-chart__frame{position:relative}.mk-chart__svg{display:block;width:100%;height:auto;overflow:visible}.mk-chart__grid{stroke:var(--mk-chart-grid);stroke-width:1;vector-effect:non-scaling-stroke}.mk-chart__axis-label{fill:var(--mk-chart-axis);font-size:11px}.mk-chart__axis-label--y{text-anchor:end;dominant-baseline:middle}.mk-chart__axis-label--x{text-anchor:middle;dominant-baseline:hanging}.mk-chart__axis-label--tilted{text-anchor:end;dominant-baseline:middle}.mk-chart__bar{transition:opacity var(--mk-duration-fast) var(--mk-ease-standard),filter var(--mk-duration-fast) var(--mk-ease-standard)}.mk-chart__bar:hover{filter:brightness(1.08)}.mk-chart__bar--dim{opacity:.4}.mk-chart__area{stroke:none;opacity:.14}.mk-chart__line{fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}.mk-chart__dot{stroke:var(--mk-surface);stroke-width:1.5;vector-effect:non-scaling-stroke}.mk-chart__crosshair{stroke:var(--mk-chart-axis);stroke-width:1;stroke-dasharray:3 3;vector-effect:non-scaling-stroke;opacity:.6}.mk-chart__point{fill-opacity:.72;stroke:var(--mk-surface);stroke-width:1;vector-effect:non-scaling-stroke;cursor:pointer;transition:fill-opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-chart__point:hover,.mk-chart__point--active{fill-opacity:1}.mk-radar__ring{fill:none}.mk-radar__area{fill-opacity:.16;stroke-width:2;stroke-linejoin:round;vector-effect:non-scaling-stroke}.mk-radar__axis-label{dominant-baseline:middle}.mk-chart__axis-title{fill:var(--mk-chart-axis);font-size:11px;font-weight:var(--mk-font-weight-medium)}.mk-chart__axis-title--x,.mk-chart__axis-title--y{text-anchor:middle}.mk-chart__slice{stroke:var(--mk-surface);stroke-width:2;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-chart__slice--dim{opacity:.45}.mk-chart__tooltip{position:absolute;z-index:1;transform:translate(-50%,calc(-100% - 8px));pointer-events:none;display:flex;flex-direction:column;gap:2px;padding:var(--mk-space-2) var(--mk-space-3);background-color:var(--mk-surface);color:var(--mk-text);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);font-size:var(--mk-font-size-sm);white-space:nowrap}.mk-chart__tooltip-title{font-weight:var(--mk-font-weight-semibold)}.mk-chart__tooltip-row{display:flex;align-items:center;gap:var(--mk-space-2)}.mk-chart__tooltip-name{color:var(--mk-text-muted)}.mk-chart__tooltip-value{margin-inline-start:auto;font-variant-numeric:tabular-nums;font-weight:var(--mk-font-weight-medium)}.mk-chart__swatch{flex:none;width:.75rem;height:.75rem;border-radius:var(--mk-radius-xs)}.mk-chart__legend{display:flex;flex-wrap:wrap;gap:var(--mk-space-2) var(--mk-space-4);margin:var(--mk-space-3) 0 0;padding:0;padding-inline-start:var(--mk-space-2);list-style:none}.mk-chart__legend-item{display:inline-flex;align-items:center;gap:var(--mk-space-2);font-size:var(--mk-font-size-sm);color:var(--mk-text-muted)}.mk-donut__frame{margin:0 auto}.mk-donut__center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;pointer-events:none;text-align:center}.mk-donut__center-label{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold);line-height:1;color:var(--mk-text);font-variant-numeric:tabular-nums}.mk-donut__center-sublabel{font-size:var(--mk-font-size-xs);color:var(--mk-text-subtle);text-transform:uppercase;letter-spacing:.04em}.mk-donut__legend{flex-direction:column;gap:var(--mk-space-2)}.mk-donut__legend .mk-chart__legend-item{width:100%}.mk-donut__legend-name{color:var(--mk-text)}.mk-donut__legend-value{margin-inline-start:auto;color:var(--mk-text-muted);font-variant-numeric:tabular-nums}.mk-chart__sr-table{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(prefers-reduced-motion:reduce){.mk-chart__bar,.mk-chart__slice{transition:none}}.mk-chart__slice--interactive,.mk-chart__bar--interactive,.mk-chart__band--interactive,.mk-chart__legend-item--interactive{cursor:pointer}.mk-chart__slice--interactive:focus-visible,.mk-chart__bar--interactive:focus-visible,.mk-chart__band--interactive:focus-visible{outline:2px solid var(--mk-primary);outline-offset:2px}.mk-chart__legend-button{display:inline-flex;align-items:center;gap:inherit;padding:0;margin:0;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;border-radius:var(--mk-radius-sm)}.mk-chart__legend-button:focus-visible{outline:2px solid var(--mk-primary);outline-offset:2px}\n"] }]
2223
2412
  }], propDecorators: { series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], pointRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "pointRadius", required: false }] }], bubble: [{ type: i0.Input, args: [{ isSignal: true, alias: "bubble", required: false }] }], maxRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRadius", required: false }] }], showGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGrid", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], xLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "xLabel", required: false }] }], yLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "yLabel", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
2224
2413
 
2414
+ /** WCAG AA minimum contrast for normal text — what cell values are held to. */
2415
+ const MK_HEATMAP_MIN_CONTRAST = 4.5;
2416
+ /** WCAG 2.x relative luminance of an sRGB colour. */
2417
+ function mkRelativeLuminance([r, g, b]) {
2418
+ const lin = (c) => {
2419
+ const s = c / 255;
2420
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
2421
+ };
2422
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
2423
+ }
2424
+ /** WCAG 2.x contrast ratio (1–21) between two colours. */
2425
+ function mkContrastRatio(a, b) {
2426
+ const la = mkRelativeLuminance(a);
2427
+ const lb = mkRelativeLuminance(b);
2428
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
2429
+ return (hi + 0.05) / (lo + 0.05);
2430
+ }
2431
+ /** What `color-mix(in srgb, a t%, b)` yields: a linear blend in gamma-encoded sRGB. */
2432
+ function mkMixRgb(a, b, t) {
2433
+ const mix = (x, y) => Math.round(x * t + y * (1 - t));
2434
+ return [mix(a[0], b[0]), mix(a[1], b[1]), mix(a[2], b[2])];
2435
+ }
2436
+ /**
2437
+ * Parse a CSS colour as the CSSOM serialises it — `rgb(r, g, b)` /
2438
+ * `rgba(r, g, b, a)` — or a `#rgb` / `#rrggbb` literal. `null` for anything
2439
+ * else (an unresolved `var()`, `color(...)`, …).
2440
+ */
2441
+ function mkParseRgb(text) {
2442
+ const s = (text ?? '').trim();
2443
+ const fn = /^rgba?\(\s*(\d+(?:\.\d+)?)[\s,]+(\d+(?:\.\d+)?)[\s,]+(\d+(?:\.\d+)?)/i.exec(s);
2444
+ if (fn)
2445
+ return [Number(fn[1]), Number(fn[2]), Number(fn[3])];
2446
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
2447
+ if (!hex)
2448
+ return null;
2449
+ const h = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join('') : hex[1];
2450
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
2451
+ }
2452
+ /** Best of the two text tokens against the cell painted at intensity `t`. */
2453
+ function pickText(t, palette) {
2454
+ const bg = mkMixRgb(palette.accent, palette.surface, t);
2455
+ const onText = mkContrastRatio(palette.text, bg);
2456
+ const onInverse = mkContrastRatio(palette.textInverse, bg);
2457
+ return onInverse > onText
2458
+ ? { intensity: t, inverse: true, contrast: onInverse }
2459
+ : { intensity: t, inverse: false, contrast: onText };
2460
+ }
2461
+ /**
2462
+ * Choose the text token for a cell at intensity `t` (0–1) — light text on
2463
+ * dark cells, dark text on light cells, decided by contrast against the
2464
+ * actual blend rather than a fixed threshold.
2465
+ *
2466
+ * Every accent ramp has a crossover band where neither `--mk-text` nor
2467
+ * `--mk-text-inverse` reaches 4.5:1 (the cell is mid-luminance). When values
2468
+ * are printed in the cells, a cell in that band is painted at the nearest
2469
+ * intensity just outside it, so its value stays readable; the shift is a few
2470
+ * percent of the ramp and the cell keeps its order relative to its
2471
+ * neighbours. Without a resolved palette (server rendering, tests) a plain
2472
+ * threshold is used.
2473
+ */
2474
+ function mkHeatmapCellColors(t, palette, showValues) {
2475
+ const clamped = Math.max(0, Math.min(1, t));
2476
+ if (!palette)
2477
+ return { intensity: clamped, inverse: clamped >= 0.55, contrast: NaN };
2478
+ const best = pickText(clamped, palette);
2479
+ if (!showValues || best.contrast >= MK_HEATMAP_MIN_CONTRAST)
2480
+ return best;
2481
+ // In the crossover band: step outward (down and up alternately, 1 % at a
2482
+ // time) to the closest intensity at which one of the tokens passes.
2483
+ for (let step = 1; step <= 100; step++) {
2484
+ const delta = step / 100;
2485
+ for (const candidate of [clamped - delta, clamped + delta]) {
2486
+ if (candidate < 0 || candidate > 1)
2487
+ continue;
2488
+ const pick = pickText(candidate, palette);
2489
+ if (pick.contrast >= MK_HEATMAP_MIN_CONTRAST)
2490
+ return pick;
2491
+ }
2492
+ }
2493
+ return best;
2494
+ }
2225
2495
  /**
2226
2496
  * Heatmap — a matrix of cells shaded by value intensity, for correlation
2227
2497
  * grids, activity-by-time tables and the like. Rendered as a semantic
2228
2498
  * `<table>` (so it is inherently screen-reader friendly) with cell colours
2229
2499
  * mixed from `accent` over the surface via `color-mix`, so it tracks the theme.
2230
2500
  *
2501
+ * Printed values (`showValues`) pick `--mk-text` or `--mk-text-inverse` per
2502
+ * cell by contrast against that cell's actual colour, in both themes — see
2503
+ * {@link mkHeatmapCellColors}.
2504
+ *
2231
2505
  * ```html
2232
2506
  * <mk-heatmap
2233
2507
  * [xLabels]="hours" [yLabels]="days" [data]="matrix()" showValues />
2234
2508
  * ```
2235
2509
  */
2236
2510
  class MkHeatmap {
2511
+ doc = inject(DOCUMENT$1);
2512
+ host = inject(ElementRef).nativeElement;
2237
2513
  /** Column labels (x axis). */
2238
2514
  xLabels = input([], /* @ts-ignore */
2239
2515
  ...(ngDevMode ? [{ debugName: "xLabels" }] : /* istanbul ignore next */ []));
@@ -2262,6 +2538,25 @@ class MkHeatmap {
2262
2538
  /** Accessible summary; generated from the data when omitted. */
2263
2539
  label = input('', /* @ts-ignore */
2264
2540
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2541
+ /**
2542
+ * The accent and the text/surface tokens resolved through the CSSOM, so
2543
+ * each cell can pick a text colour that actually contrasts with its blend.
2544
+ * `null` until resolved (server, jsdom) — the cells then fall back to a
2545
+ * threshold.
2546
+ */
2547
+ palette = signal(null, /* @ts-ignore */
2548
+ ...(ngDevMode ? [{ debugName: "palette" }] : /* istanbul ignore next */ []));
2549
+ /** Bumped when the document theme changes so the palette is re-resolved. */
2550
+ themeTick = signal(0, /* @ts-ignore */
2551
+ ...(ngDevMode ? [{ debugName: "themeTick" }] : /* istanbul ignore next */ []));
2552
+ constructor() {
2553
+ afterRenderEffect(() => {
2554
+ this.accent();
2555
+ this.themeTick();
2556
+ untracked(() => this.palette.set(this.resolvePalette()));
2557
+ });
2558
+ this.watchTheme();
2559
+ }
2265
2560
  domain = computed(() => {
2266
2561
  const flat = this.data()
2267
2562
  .flat()
@@ -2277,26 +2572,32 @@ class MkHeatmap {
2277
2572
  const span = hi - lo || 1;
2278
2573
  return Math.max(0, Math.min(1, (value - lo) / span));
2279
2574
  }
2575
+ /** The `color-mix()` expression painting a cell at intensity `t`. */
2576
+ cellBackground(t) {
2577
+ return `color-mix(in srgb, ${this.accent()} ${Math.round(t * 100)}%, var(--mk-surface-2))`;
2578
+ }
2280
2579
  rows = computed(() => {
2281
2580
  const xs = this.xLabels();
2282
2581
  const ys = this.yLabels();
2283
2582
  const data = this.data();
2284
2583
  const fmt = this.format();
2584
+ const palette = this.palette();
2585
+ const showValues = this.showValues();
2285
2586
  return ys.map((yLabel, r) => ({
2286
2587
  label: yLabel,
2287
2588
  cells: xs.map((xLabel, c) => {
2288
2589
  const value = data[r]?.[c] ?? null;
2289
2590
  if (value == null) {
2290
- return { value: null, display: '', xLabel, bg: 'transparent', strong: false };
2591
+ return { value: null, display: '', xLabel, bg: 'transparent', inverse: false };
2291
2592
  }
2292
- const t = this.ratio(value);
2593
+ const colors = mkHeatmapCellColors(this.ratio(value), palette, showValues);
2293
2594
  return {
2294
2595
  value,
2295
2596
  display: fmt(value),
2296
2597
  xLabel,
2297
2598
  // Mix the accent over the surface; more intense = more accent.
2298
- bg: `color-mix(in srgb, ${this.accent()} ${Math.round(t * 100)}%, var(--mk-surface-2))`,
2299
- strong: t >= 0.55,
2599
+ bg: this.cellBackground(colors.intensity),
2600
+ inverse: colors.inverse,
2300
2601
  };
2301
2602
  }),
2302
2603
  }));
@@ -2309,7 +2610,7 @@ class MkHeatmap {
2309
2610
  return {
2310
2611
  min: fmt(lo),
2311
2612
  max: fmt(hi),
2312
- swatches: [0, 0.25, 0.5, 0.75, 1].map((t) => `color-mix(in srgb, ${this.accent()} ${t * 100}%, var(--mk-surface-2))`),
2613
+ swatches: [0, 0.25, 0.5, 0.75, 1].map((t) => this.cellBackground(t)),
2313
2614
  };
2314
2615
  }, /* @ts-ignore */
2315
2616
  ...(ngDevMode ? [{ debugName: "scaleStops" }] : /* istanbul ignore next */ []));
@@ -2321,8 +2622,62 @@ class MkHeatmap {
2321
2622
  return `Heatmap, ${rows} rows by ${cols} columns`;
2322
2623
  }, /* @ts-ignore */
2323
2624
  ...(ngDevMode ? [{ debugName: "resolvedLabel" }] : /* istanbul ignore next */ []));
2625
+ /**
2626
+ * Resolve the accent and the tokens to sRGB through a hidden probe: the
2627
+ * probe's computed `color` is what the browser would paint, `var()`s and
2628
+ * `color-mix()` included. `null` when the environment cannot resolve them.
2629
+ */
2630
+ resolvePalette() {
2631
+ const win = this.doc.defaultView;
2632
+ if (!win?.getComputedStyle)
2633
+ return null;
2634
+ const probe = this.doc.createElement('span');
2635
+ probe.setAttribute('aria-hidden', 'true');
2636
+ probe.style.position = 'absolute';
2637
+ probe.style.visibility = 'hidden';
2638
+ this.host.appendChild(probe);
2639
+ try {
2640
+ const read = (expr) => {
2641
+ probe.style.color = '';
2642
+ probe.style.color = expr;
2643
+ return mkParseRgb(win.getComputedStyle(probe).color);
2644
+ };
2645
+ const accent = read(this.accent());
2646
+ const surface = read('var(--mk-surface-2)');
2647
+ const text = read('var(--mk-text)');
2648
+ const textInverse = read('var(--mk-text-inverse)');
2649
+ return accent && surface && text && textInverse
2650
+ ? { accent, surface, text, textInverse }
2651
+ : null;
2652
+ }
2653
+ finally {
2654
+ probe.remove();
2655
+ }
2656
+ }
2657
+ /**
2658
+ * Re-resolve when the theme flips: `data-theme` / class changes on the root
2659
+ * element (MkThemeService and most hand-rolled toggles) and the OS scheme.
2660
+ */
2661
+ watchTheme() {
2662
+ const win = this.doc.defaultView;
2663
+ const root = this.doc.documentElement;
2664
+ if (!win || !root)
2665
+ return;
2666
+ const bump = () => this.themeTick.update((n) => n + 1);
2667
+ const destroyRef = inject(DestroyRef);
2668
+ if (typeof win.MutationObserver === 'function') {
2669
+ const observer = new win.MutationObserver(bump);
2670
+ observer.observe(root, { attributes: true, attributeFilter: ['data-theme', 'class', 'style'] });
2671
+ destroyRef.onDestroy(() => observer.disconnect());
2672
+ }
2673
+ const mql = win.matchMedia?.('(prefers-color-scheme: dark)');
2674
+ if (mql?.addEventListener) {
2675
+ mql.addEventListener('change', bump);
2676
+ destroyRef.onDestroy(() => mql.removeEventListener('change', bump));
2677
+ }
2678
+ }
2324
2679
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHeatmap, deps: [], target: i0.ɵɵFactoryTarget.Component });
2325
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkHeatmap, isStandalone: true, selector: "mk-heatmap", inputs: { xLabels: { classPropertyName: "xLabels", publicName: "xLabels", isSignal: true, isRequired: false, transformFunction: null }, yLabels: { classPropertyName: "yLabels", publicName: "yLabels", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, showValues: { classPropertyName: "showValues", publicName: "showValues", isSignal: true, isRequired: false, transformFunction: null }, showScale: { classPropertyName: "showScale", publicName: "showScale", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "resolvedLabel()" }, classAttribute: "mk-chart mk-heatmap" }, ngImport: i0, template: "<!-- Focusable so keyboard users can scroll the overflowing grid (WCAG 2.1.1). -->\n<div\n class=\"mk-chart__frame mk-heatmap__scroll\"\n role=\"region\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedLabel()\"\n>\n <table class=\"mk-heatmap__grid\">\n <caption class=\"mk-visually-hidden\">{{ resolvedLabel() }}</caption>\n <thead>\n <tr>\n <td class=\"mk-heatmap__corner\"></td>\n @for (x of xLabels(); track $index) {\n <th scope=\"col\" class=\"mk-heatmap__col-head\">{{ x }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.label) {\n <tr>\n <th scope=\"row\" class=\"mk-heatmap__row-head\">{{ row.label }}</th>\n @for (cell of row.cells; track $index) {\n <td\n class=\"mk-heatmap__cell\"\n [class.mk-heatmap__cell--strong]=\"cell.strong\"\n [class.mk-heatmap__cell--empty]=\"cell.value === null\"\n [style.background]=\"cell.bg\"\n [attr.title]=\"\n cell.value === null\n ? null\n : row.label + ' \u00B7 ' + cell.xLabel + ': ' + cell.display\n \"\n >\n @if (cell.value !== null) {\n @if (showValues()) {\n <span class=\"mk-heatmap__value\">{{ cell.display }}</span>\n } @else {\n <!-- Colour must not be the only signal: expose the value to AT. -->\n <span class=\"mk-visually-hidden\">{{ cell.display }}</span>\n }\n }\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n\n@if (showScale()) {\n <div class=\"mk-heatmap__legend\" aria-hidden=\"true\">\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().min }}</span>\n <span class=\"mk-heatmap__legend-ramp\">\n @for (sw of scaleStops().swatches; track $index) {\n <span class=\"mk-heatmap__legend-swatch\" [style.background]=\"sw\"></span>\n }\n </span>\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().max }}</span>\n </div>\n}\n", styles: [":host{display:block;font-family:var(--mk-font-sans)}.mk-heatmap__scroll{width:100%;overflow-x:auto}.mk-heatmap__scroll:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-heatmap__grid{border-collapse:separate;border-spacing:3px;font-size:var(--mk-font-size-sm);color:var(--mk-text)}.mk-heatmap__corner{border:0}.mk-heatmap__col-head,.mk-heatmap__row-head{padding:var(--mk-space-1) var(--mk-space-2);font-weight:var(--mk-font-weight-medium);color:var(--mk-text-muted);white-space:nowrap}.mk-heatmap__col-head{text-align:center;vertical-align:bottom}.mk-heatmap__row-head{text-align:end}.mk-heatmap__cell{width:2.25rem;height:2.25rem;padding:0;text-align:center;vertical-align:middle;border-radius:var(--mk-radius-sm);color:var(--mk-text-muted);transition:outline-color var(--mk-duration-fast) var(--mk-ease-standard);outline:2px solid transparent;outline-offset:-2px}.mk-heatmap__cell:hover{outline-color:var(--mk-border-strong)}.mk-heatmap__cell--strong{color:var(--mk-primary-contrast)}.mk-heatmap__cell--empty{outline:none}.mk-heatmap__cell--empty:hover{outline-color:transparent}.mk-heatmap__value{font-size:var(--mk-font-size-xs);font-variant-numeric:tabular-nums}.mk-heatmap__legend{display:flex;align-items:center;gap:var(--mk-space-2);margin-top:var(--mk-space-3);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-heatmap__legend-ramp{display:inline-flex;gap:2px}.mk-heatmap__legend-swatch{width:1.1rem;height:.75rem;border-radius:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2680
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkHeatmap, isStandalone: true, selector: "mk-heatmap", inputs: { xLabels: { classPropertyName: "xLabels", publicName: "xLabels", isSignal: true, isRequired: false, transformFunction: null }, yLabels: { classPropertyName: "yLabels", publicName: "yLabels", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, accent: { classPropertyName: "accent", publicName: "accent", isSignal: true, isRequired: false, transformFunction: null }, showValues: { classPropertyName: "showValues", publicName: "showValues", isSignal: true, isRequired: false, transformFunction: null }, showScale: { classPropertyName: "showScale", publicName: "showScale", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "resolvedLabel()" }, classAttribute: "mk-chart mk-heatmap" }, ngImport: i0, template: "<!-- Focusable so keyboard users can scroll the overflowing grid (WCAG 2.1.1). -->\n<div\n class=\"mk-chart__frame mk-heatmap__scroll\"\n role=\"region\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedLabel()\"\n>\n <table class=\"mk-heatmap__grid\">\n <caption class=\"mk-visually-hidden\">{{ resolvedLabel() }}</caption>\n <thead>\n <tr>\n <td class=\"mk-heatmap__corner\"></td>\n @for (x of xLabels(); track $index) {\n <th scope=\"col\" class=\"mk-heatmap__col-head\">{{ x }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.label) {\n <tr>\n <th scope=\"row\" class=\"mk-heatmap__row-head\">{{ row.label }}</th>\n @for (cell of row.cells; track $index) {\n <td\n class=\"mk-heatmap__cell\"\n [class.mk-heatmap__cell--inverse]=\"cell.inverse\"\n [class.mk-heatmap__cell--empty]=\"cell.value === null\"\n [style.background]=\"cell.bg\"\n [attr.title]=\"\n cell.value === null\n ? null\n : row.label + ' \u00B7 ' + cell.xLabel + ': ' + cell.display\n \"\n >\n @if (cell.value !== null) {\n @if (showValues()) {\n <span class=\"mk-heatmap__value\">{{ cell.display }}</span>\n } @else {\n <!-- Colour must not be the only signal: expose the value to AT. -->\n <span class=\"mk-visually-hidden\">{{ cell.display }}</span>\n }\n }\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n\n@if (showScale()) {\n <div class=\"mk-heatmap__legend\" aria-hidden=\"true\">\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().min }}</span>\n <span class=\"mk-heatmap__legend-ramp\">\n @for (sw of scaleStops().swatches; track $index) {\n <span class=\"mk-heatmap__legend-swatch\" [style.background]=\"sw\"></span>\n }\n </span>\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().max }}</span>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;font-family:var(--mk-font-sans)}.mk-heatmap__scroll{width:100%;overflow-x:auto}.mk-heatmap__scroll:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-heatmap__grid{border-collapse:separate;border-spacing:3px;font-size:var(--mk-font-size-sm);color:var(--mk-text)}.mk-heatmap__corner{border:0}.mk-heatmap__col-head,.mk-heatmap__row-head{padding:var(--mk-space-1) var(--mk-space-2);font-weight:var(--mk-font-weight-medium);color:var(--mk-text-muted);white-space:nowrap}.mk-heatmap__col-head{text-align:center;vertical-align:bottom}.mk-heatmap__row-head{text-align:end}.mk-heatmap__cell{width:2.25rem;height:2.25rem;padding:0;text-align:center;vertical-align:middle;border-radius:var(--mk-radius-sm);color:var(--mk-text);transition:outline-color var(--mk-duration-fast) var(--mk-ease-standard);outline:2px solid transparent;outline-offset:-2px}.mk-heatmap__cell:hover{outline-color:var(--mk-border-strong)}.mk-heatmap__cell--inverse{color:var(--mk-text-inverse)}.mk-heatmap__cell--empty{outline:none}.mk-heatmap__cell--empty:hover{outline-color:transparent}.mk-heatmap__value{font-size:var(--mk-font-size-xs);font-variant-numeric:tabular-nums}.mk-heatmap__legend{display:flex;align-items:center;gap:var(--mk-space-2);margin-top:var(--mk-space-3);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-heatmap__legend-ramp{display:inline-flex;gap:2px}.mk-heatmap__legend-swatch{width:1.1rem;height:.75rem;border-radius:2px;box-shadow:inset 0 0 0 1px var(--mk-border-subtle)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2326
2681
  }
2327
2682
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHeatmap, decorators: [{
2328
2683
  type: Component,
@@ -2330,8 +2685,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
2330
2685
  class: 'mk-chart mk-heatmap',
2331
2686
  role: 'group',
2332
2687
  '[attr.aria-label]': 'resolvedLabel()',
2333
- }, template: "<!-- Focusable so keyboard users can scroll the overflowing grid (WCAG 2.1.1). -->\n<div\n class=\"mk-chart__frame mk-heatmap__scroll\"\n role=\"region\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedLabel()\"\n>\n <table class=\"mk-heatmap__grid\">\n <caption class=\"mk-visually-hidden\">{{ resolvedLabel() }}</caption>\n <thead>\n <tr>\n <td class=\"mk-heatmap__corner\"></td>\n @for (x of xLabels(); track $index) {\n <th scope=\"col\" class=\"mk-heatmap__col-head\">{{ x }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.label) {\n <tr>\n <th scope=\"row\" class=\"mk-heatmap__row-head\">{{ row.label }}</th>\n @for (cell of row.cells; track $index) {\n <td\n class=\"mk-heatmap__cell\"\n [class.mk-heatmap__cell--strong]=\"cell.strong\"\n [class.mk-heatmap__cell--empty]=\"cell.value === null\"\n [style.background]=\"cell.bg\"\n [attr.title]=\"\n cell.value === null\n ? null\n : row.label + ' \u00B7 ' + cell.xLabel + ': ' + cell.display\n \"\n >\n @if (cell.value !== null) {\n @if (showValues()) {\n <span class=\"mk-heatmap__value\">{{ cell.display }}</span>\n } @else {\n <!-- Colour must not be the only signal: expose the value to AT. -->\n <span class=\"mk-visually-hidden\">{{ cell.display }}</span>\n }\n }\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n\n@if (showScale()) {\n <div class=\"mk-heatmap__legend\" aria-hidden=\"true\">\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().min }}</span>\n <span class=\"mk-heatmap__legend-ramp\">\n @for (sw of scaleStops().swatches; track $index) {\n <span class=\"mk-heatmap__legend-swatch\" [style.background]=\"sw\"></span>\n }\n </span>\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().max }}</span>\n </div>\n}\n", styles: [":host{display:block;font-family:var(--mk-font-sans)}.mk-heatmap__scroll{width:100%;overflow-x:auto}.mk-heatmap__scroll:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-heatmap__grid{border-collapse:separate;border-spacing:3px;font-size:var(--mk-font-size-sm);color:var(--mk-text)}.mk-heatmap__corner{border:0}.mk-heatmap__col-head,.mk-heatmap__row-head{padding:var(--mk-space-1) var(--mk-space-2);font-weight:var(--mk-font-weight-medium);color:var(--mk-text-muted);white-space:nowrap}.mk-heatmap__col-head{text-align:center;vertical-align:bottom}.mk-heatmap__row-head{text-align:end}.mk-heatmap__cell{width:2.25rem;height:2.25rem;padding:0;text-align:center;vertical-align:middle;border-radius:var(--mk-radius-sm);color:var(--mk-text-muted);transition:outline-color var(--mk-duration-fast) var(--mk-ease-standard);outline:2px solid transparent;outline-offset:-2px}.mk-heatmap__cell:hover{outline-color:var(--mk-border-strong)}.mk-heatmap__cell--strong{color:var(--mk-primary-contrast)}.mk-heatmap__cell--empty{outline:none}.mk-heatmap__cell--empty:hover{outline-color:transparent}.mk-heatmap__value{font-size:var(--mk-font-size-xs);font-variant-numeric:tabular-nums}.mk-heatmap__legend{display:flex;align-items:center;gap:var(--mk-space-2);margin-top:var(--mk-space-3);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-heatmap__legend-ramp{display:inline-flex;gap:2px}.mk-heatmap__legend-swatch{width:1.1rem;height:.75rem;border-radius:2px}\n"] }]
2334
- }], propDecorators: { xLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "xLabels", required: false }] }], yLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "yLabels", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], showValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValues", required: false }] }], showScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "showScale", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
2688
+ }, template: "<!-- Focusable so keyboard users can scroll the overflowing grid (WCAG 2.1.1). -->\n<div\n class=\"mk-chart__frame mk-heatmap__scroll\"\n role=\"region\"\n tabindex=\"0\"\n [attr.aria-label]=\"resolvedLabel()\"\n>\n <table class=\"mk-heatmap__grid\">\n <caption class=\"mk-visually-hidden\">{{ resolvedLabel() }}</caption>\n <thead>\n <tr>\n <td class=\"mk-heatmap__corner\"></td>\n @for (x of xLabels(); track $index) {\n <th scope=\"col\" class=\"mk-heatmap__col-head\">{{ x }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.label) {\n <tr>\n <th scope=\"row\" class=\"mk-heatmap__row-head\">{{ row.label }}</th>\n @for (cell of row.cells; track $index) {\n <td\n class=\"mk-heatmap__cell\"\n [class.mk-heatmap__cell--inverse]=\"cell.inverse\"\n [class.mk-heatmap__cell--empty]=\"cell.value === null\"\n [style.background]=\"cell.bg\"\n [attr.title]=\"\n cell.value === null\n ? null\n : row.label + ' \u00B7 ' + cell.xLabel + ': ' + cell.display\n \"\n >\n @if (cell.value !== null) {\n @if (showValues()) {\n <span class=\"mk-heatmap__value\">{{ cell.display }}</span>\n } @else {\n <!-- Colour must not be the only signal: expose the value to AT. -->\n <span class=\"mk-visually-hidden\">{{ cell.display }}</span>\n }\n }\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n\n@if (showScale()) {\n <div class=\"mk-heatmap__legend\" aria-hidden=\"true\">\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().min }}</span>\n <span class=\"mk-heatmap__legend-ramp\">\n @for (sw of scaleStops().swatches; track $index) {\n <span class=\"mk-heatmap__legend-swatch\" [style.background]=\"sw\"></span>\n }\n </span>\n <span class=\"mk-heatmap__legend-label\">{{ scaleStops().max }}</span>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;font-family:var(--mk-font-sans)}.mk-heatmap__scroll{width:100%;overflow-x:auto}.mk-heatmap__scroll:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:calc(-1 * var(--mk-focus-ring-width))}.mk-heatmap__grid{border-collapse:separate;border-spacing:3px;font-size:var(--mk-font-size-sm);color:var(--mk-text)}.mk-heatmap__corner{border:0}.mk-heatmap__col-head,.mk-heatmap__row-head{padding:var(--mk-space-1) var(--mk-space-2);font-weight:var(--mk-font-weight-medium);color:var(--mk-text-muted);white-space:nowrap}.mk-heatmap__col-head{text-align:center;vertical-align:bottom}.mk-heatmap__row-head{text-align:end}.mk-heatmap__cell{width:2.25rem;height:2.25rem;padding:0;text-align:center;vertical-align:middle;border-radius:var(--mk-radius-sm);color:var(--mk-text);transition:outline-color var(--mk-duration-fast) var(--mk-ease-standard);outline:2px solid transparent;outline-offset:-2px}.mk-heatmap__cell:hover{outline-color:var(--mk-border-strong)}.mk-heatmap__cell--inverse{color:var(--mk-text-inverse)}.mk-heatmap__cell--empty{outline:none}.mk-heatmap__cell--empty:hover{outline-color:transparent}.mk-heatmap__value{font-size:var(--mk-font-size-xs);font-variant-numeric:tabular-nums}.mk-heatmap__legend{display:flex;align-items:center;gap:var(--mk-space-2);margin-top:var(--mk-space-3);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-heatmap__legend-ramp{display:inline-flex;gap:2px}.mk-heatmap__legend-swatch{width:1.1rem;height:.75rem;border-radius:2px;box-shadow:inset 0 0 0 1px var(--mk-border-subtle)}\n"] }]
2689
+ }], ctorParameters: () => [], propDecorators: { xLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "xLabels", required: false }] }], yLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "yLabels", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], showValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValues", required: false }] }], showScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "showScale", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
2335
2690
 
2336
2691
  /** Local midnight for `d` (drops the time-of-day). */
2337
2692
  function stripTime(d) {
@@ -6137,5 +6492,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
6137
6492
  * Generated bundle index. Do not edit.
6138
6493
  */
6139
6494
 
6140
- export { MK_CHART_PALETTE_SIZE, MkAvatar, MkAvatarGroup, MkBadge, MkBarChart, MkCalendarHeatmap, MkCard, MkCardFooter, MkCardHeader, MkCardTitle, MkCarousel, MkCarouselSlide, MkCode, MkCountdown, MkDescItem, MkDescriptionList, MkDiff, MkDivider, MkDonutChart, MkEmptyState, MkFunnelChart, MkGauge, MkHeatmap, MkInlineEdit, MkJsonViewer, MkKanban, MkLineChart, MkList, MkListItem, MkLogViewer, MkMarkdown, MkProfileActions, MkProfileCard, MkProfileMeta, MkProgressBar, MkProgressRing, MkQrCode, MkRadarChart, MkScatterChart, MkSkeleton, MkSkeletonPreset, MkSparkline, MkSpinner, MkStatCard, MkTag, MkTimeline, MkTimelineItem, MkTreemap, MkVirtualScroll, mkArcPath, mkAreaPath, mkBuildJsonTree, mkChartColor, mkComputeDiff, mkDiffStats, mkEncodeQr, mkFormatCompact, mkLinePath, mkLinearScale, mkNiceTicks, mkParseAnsi, mkParseMarkdown, mkRenderMarkdown, mkSplitDuration, mkSquarify, mkStripAnsi };
6495
+ export { MK_CHART_PALETTE_SIZE, MK_HEATMAP_MIN_CONTRAST, MkAvatar, MkAvatarGroup, MkBadge, MkBadgeOverlay, MkBarChart, MkCalendarHeatmap, MkCard, MkCardFooter, MkCardHeader, MkCardTitle, MkCarousel, MkCarouselSlide, MkCode, MkCountdown, MkDescItem, MkDescriptionList, MkDiff, MkDivider, MkDonutChart, MkEmptyState, MkFunnelChart, MkGauge, MkHeatmap, MkInlineEdit, MkJsonViewer, MkKanban, MkLineChart, MkList, MkListItem, MkLogViewer, MkMarkdown, MkProfileActions, MkProfileCard, MkProfileMeta, MkProgressBar, MkProgressRing, MkQrCode, MkRadarChart, MkScatterChart, MkSkeleton, MkSkeletonPreset, MkSparkline, MkSpinner, MkStatCard, MkTag, MkTimeline, MkTimelineItem, MkTreemap, MkVirtualScroll, mkArcPath, mkAreaPath, mkBuildJsonTree, mkChartColor, mkComputeDiff, mkContrastRatio, mkDiffStats, mkEncodeQr, mkFormatCompact, mkHeatmapCellColors, mkLinePath, mkLinearScale, mkMixRgb, mkNiceTicks, mkParseAnsi, mkParseMarkdown, mkParseRgb, mkRelativeLuminance, mkRenderMarkdown, mkSplitDuration, mkSquarify, mkStripAnsi };
6141
6496
  //# sourceMappingURL=mk-kit-ui-data.mjs.map