@mk-kit/ui 0.40.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.
package/README.md CHANGED
@@ -83,12 +83,12 @@ export class AppRoot {
83
83
  | **Date & time** | calendar, date picker, date-range picker, time picker, month/year picker, week picker, inline mini date, event calendar with editable week/day grid (drag to move / resize) |
84
84
  | **Tables & grids** (7) | data table — sort, multi-select, expandable rows, tree rows, grouping, sticky header, column resize / reorder / pin, inline cell edit, responsive stacking, CSV export, print styles — plus `MkTableDataSource` for server-side sort/page/filter, query builder (`mk-query-builder` → predicate / readable text / `MkTableDataSource.setQuery()`) |
85
85
  | **Charts** (12) | line/area, bar (stacked, horizontal, label fitting), donut, gauge, progress ring, scatter/bubble, radar, funnel, treemap, heatmap, calendar heatmap, sparkline — SVG, themed, accessible |
86
- | **Data display** (25) | cards, lists, stat cards, badges, tags, chips, avatars & groups, timeline, description list, tree, empty state, countdown, QR code, diff view, JSON viewer, code block, virtual scroll, carousel, kanban |
86
+ | **Data display** (26) | cards, lists, stat cards, badges, badge overlay (`[mkBadgeOverlay]`), tags, chips, avatars & groups, timeline, description list, tree, empty state, countdown, QR code, diff view, JSON viewer, code block, virtual scroll, carousel, kanban |
87
87
  | **Navigation & layout** (21) | app shell (responsive sidebar), stack / flex / grid layout primitives, nav list & groups, tabs, stepper, breadcrumb, pagination, menu, context menu, command palette (⌘K), page header, toolbar, splitter, drawer, scroll area, FAB, back-to-top |
88
88
  | **Feedback & overlays** (21) | dialogs (+ `confirm()` / `alert()` / `prompt()`), bottom sheet, drawer, popover, popconfirm, hovercard, tooltip, toast, snackbar, alert, banner, result page, notification center, product tour, progress bar, loading bar, spinner, skeletons, block UI (region / page overlay), draggable & resizable dialogs |
89
- | **Editors & interactions** (25) | block editor (Notion-style, HTML round-trip), rich text, markdown renderer, code editor, log viewer, drag & drop, sortable list, @mentions, hotkeys, undo/redo history, permissions (`*mkCan`), clipboard, intersect, infinite scroll, ripple, scrollspy, chat (streaming message log, tool cards, prompt box) |
89
+ | **Editors & interactions** (31) | block editor (Notion-style, HTML round-trip), rich text, markdown renderer, code editor, log viewer, drag & drop, sortable list, @mentions, hotkeys, undo/redo history, permissions (`*mkCan`), clipboard, intersect, infinite scroll, ripple, scrollspy, chat (streaming message log, tool cards, prompt box), `Intl` formatting pipes (currency, relative time, file size, initials, truncate, pluralize) |
90
90
  | **Media** (6) | image with states, gallery, lightbox, cropper, media manager |
91
- | **Core services** | theme (light/dark/system + density), breakpoints, overlay & anchored panels, focus trap, live announcer, icon registry (426 built-in icons), i18n |
91
+ | **Core services** | theme (light/dark/system + density + high contrast), breakpoints, overlay & anchored panels, focus trap, live announcer, icon registry (426 built-in icons), i18n |
92
92
 
93
93
  The full, searchable index lives in the docs (`/components-index`).
94
94
 
@@ -9,6 +9,9 @@ const STORAGE_KEY = 'mk-kit-theme';
9
9
  const THEME_ATTR = 'data-mk-theme';
10
10
  const DENSITY_STORAGE_KEY = 'mk-kit-density';
11
11
  const DENSITY_ATTR = 'data-mk-density';
12
+ const CONTRAST_STORAGE_KEY = 'mk-kit-contrast';
13
+ const CONTRAST_ATTR = 'data-mk-contrast';
14
+ const CONTRAST_QUERY = '(prefers-contrast: more)';
12
15
  /**
13
16
  * Reactive theme controller for mk-kit.
14
17
  *
@@ -55,10 +58,59 @@ class MkThemeService {
55
58
  * it.
56
59
  */
57
60
  density = this._density.asReadonly();
61
+ _contrast = signal(this.readInitialContrast(), /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "_contrast" }] : /* istanbul ignore next */ []));
63
+ /**
64
+ * The user's contrast preference: `normal`, `high` or `system` (follow the
65
+ * OS `prefers-contrast` setting — the default). Written as
66
+ * `data-mk-contrast` on `<html>` and persisted; `system` removes the
67
+ * attribute so the pure-CSS `prefers-contrast: more` mapping takes over.
68
+ *
69
+ * Like density, the attribute also works per subtree: put
70
+ * `data-mk-contrast="high"` on any element to raise contrast there only.
71
+ */
72
+ contrast = this._contrast.asReadonly();
73
+ _systemPrefersContrast = signal(this.readSystemContrast(), /* @ts-ignore */
74
+ ...(ngDevMode ? [{ debugName: "_systemPrefersContrast" }] : /* istanbul ignore next */ []));
75
+ /** The concrete contrast in effect (`normal` or `high`). */
76
+ resolvedContrast = computed(() => {
77
+ const pref = this._contrast();
78
+ if (pref === 'system') {
79
+ return this._systemPrefersContrast() ? 'high' : 'normal';
80
+ }
81
+ return pref;
82
+ }, /* @ts-ignore */
83
+ ...(ngDevMode ? [{ debugName: "resolvedContrast" }] : /* istanbul ignore next */ []));
84
+ /** Convenience boolean for template bindings. */
85
+ isHighContrast = computed(() => this.resolvedContrast() === 'high', /* @ts-ignore */
86
+ ...(ngDevMode ? [{ debugName: "isHighContrast" }] : /* istanbul ignore next */ []));
58
87
  constructor() {
59
88
  if (this.isBrowser) {
60
89
  this.watchSystemPreference();
90
+ this.watchSystemContrast();
61
91
  }
92
+ // Keep the contrast attribute + storage in sync. `system` is the ABSENCE
93
+ // of the attribute (the stylesheet then follows `prefers-contrast`);
94
+ // `normal` and `high` name themselves so CSS can tell an explicit opt-out
95
+ // from "no preference".
96
+ effect(() => {
97
+ const contrast = this._contrast();
98
+ if (!this.isBrowser)
99
+ return;
100
+ const root = this.document.documentElement;
101
+ if (contrast === 'system') {
102
+ root.removeAttribute(CONTRAST_ATTR);
103
+ }
104
+ else {
105
+ root.setAttribute(CONTRAST_ATTR, contrast);
106
+ }
107
+ try {
108
+ localStorage.setItem(CONTRAST_STORAGE_KEY, contrast);
109
+ }
110
+ catch {
111
+ /* ignore */
112
+ }
113
+ });
62
114
  // Keep the DOM attribute + storage in sync with the signal.
63
115
  effect(() => {
64
116
  const pref = this._preference();
@@ -128,6 +180,41 @@ class MkThemeService {
128
180
  }
129
181
  return 'comfortable';
130
182
  }
183
+ /** Set the contrast preference explicitly. */
184
+ setContrast(contrast) {
185
+ this._contrast.set(contrast);
186
+ }
187
+ /** Toggle between normal and high contrast (resolving `system` first). */
188
+ toggleContrast() {
189
+ this._contrast.set(this.resolvedContrast() === 'high' ? 'normal' : 'high');
190
+ }
191
+ readInitialContrast() {
192
+ if (!this.isBrowserEnv())
193
+ return 'system';
194
+ try {
195
+ const stored = localStorage.getItem(CONTRAST_STORAGE_KEY);
196
+ if (stored === 'normal' || stored === 'high' || stored === 'system') {
197
+ return stored;
198
+ }
199
+ }
200
+ catch {
201
+ /* ignore */
202
+ }
203
+ return 'system';
204
+ }
205
+ readSystemContrast() {
206
+ if (!this.isBrowserEnv())
207
+ return false;
208
+ return this.document.defaultView?.matchMedia?.(CONTRAST_QUERY).matches ?? false;
209
+ }
210
+ watchSystemContrast() {
211
+ const mql = this.document.defaultView?.matchMedia?.(CONTRAST_QUERY);
212
+ if (!mql)
213
+ return;
214
+ const onChange = (e) => this._systemPrefersContrast.set(e.matches);
215
+ mql.addEventListener('change', onChange);
216
+ this.destroyRef.onDestroy(() => mql.removeEventListener('change', onChange));
217
+ }
131
218
  /** Set the theme preference explicitly. */
132
219
  setTheme(preference) {
133
220
  this._preference.set(preference);