@mk-kit/ui 0.34.1 → 0.36.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
@@ -2,14 +2,14 @@
2
2
 
3
3
  **Themable, accessible Angular component library for admin dashboards, back-offices and internal tools.**
4
4
 
5
- 165+ standalone components, directives and services — data tables, charts,
5
+ 170+ standalone components, directives and services — data tables, charts,
6
6
  date & time pickers, editors, kanban, overlays, an app shell — written for
7
7
  Angular 22 with signals and `OnPush` from day one. Every visual value is a
8
8
  `--mk-*` CSS custom property; light and dark ship out of the box; WCAG 2.1 AA
9
9
  is the target, not the marketing. MIT licensed. Zero runtime dependencies
10
10
  beyond Angular.
11
11
 
12
- - **Docs & live demos:** <https://github.com/mk-kit/mk-kit> (site: mk-kit.dev — coming soon)
12
+ - **Docs & live demos:** <https://mk-kit.dev>
13
13
  - **Changelog:** [CHANGELOG.md](https://github.com/mk-kit/mk-kit/blob/main/CHANGELOG.md)
14
14
  - **Issues:** <https://github.com/mk-kit/mk-kit/issues>
15
15
 
@@ -73,14 +73,14 @@ export class AppRoot {
73
73
  |---|---|
74
74
  | **Forms & inputs** (49) | text, number, password (strength meter), OTP, phone (country prefix), postal code, currency, card number, IBAN, tax id, masked input, tag input, select, autocomplete, multi-select, tree-select, transfer list, checkbox, radio, switch, slider, range slider, rating, colour picker, file upload (dropzone), signature pad, numeric keypad & on-screen keyboard, repeater (`FormArray`), form field with automatic validation messages, form error summary |
75
75
  | **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) |
76
- | **Tables & grids** (6) | data table — sort, multi-select, expandable rows, grouping, sticky header, column resize / reorder / pin, inline cell edit, responsive stacking — plus `MkTableDataSource` for server-side sort/page/filter |
76
+ | **Tables & grids** (6) | 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 |
77
77
  | **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 |
78
78
  | **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 |
79
- | **Navigation & layout** (18) | app shell (responsive sidebar), nav list & groups, tabs, stepper, breadcrumb, pagination, menu, context menu, command palette (⌘K), page header, toolbar, splitter, drawer, scroll area, FAB, back-to-top |
79
+ | **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 |
80
80
  | **Feedback & overlays** (20) | 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 |
81
81
  | **Editors & interactions** (22) | 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 |
82
82
  | **Media** (6) | image with states, gallery, lightbox, cropper, media manager |
83
- | **Core services** | theme (light/dark/system + density), overlay & anchored panels, focus trap, live announcer, icon registry, i18n |
83
+ | **Core services** | theme (light/dark/system + density), breakpoints, overlay & anchored panels, focus trap, live announcer, icon registry (426 built-in icons), i18n |
84
84
 
85
85
  The full, searchable index lives in the docs (`/components-index`).
86
86
 
@@ -96,7 +96,7 @@ import { MkDatePicker } from '@mk-kit/ui/datetime';
96
96
 
97
97
  Available: `core`, `forms`, `datetime`, `table`, `data`, `navigation`,
98
98
  `feedback`, `directives`, `dnd`, `media`, `icon`, `button`, `checkbox`, `chip`,
99
- `context-menu`, `rich-text`, `block-editor`. `sideEffects: false` throughout.
99
+ `context-menu`, `layout`, `rich-text`, `block-editor`. `sideEffects: false` throughout.
100
100
 
101
101
  ## Theming
102
102
 
@@ -196,4 +196,5 @@ for use of the name and logo.
196
196
 
197
197
  ## License
198
198
 
199
- MIT © Mateusz Kornaś
199
+ MIT © Mateusz Kornaś. Part of the built-in icon set is derived from
200
+ [Lucide](https://lucide.dev) (ISC) — see `THIRD_PARTY_NOTICES.md`.
@@ -0,0 +1,28 @@
1
+ # Third-party notices
2
+
3
+ ## Lucide Icons
4
+
5
+ Part of the built-in icon set (`MK_EXTENDED_ICONS`, generated by
6
+ `scripts/gen-icons.mjs`) is derived from [Lucide](https://lucide.dev),
7
+ distributed under the ISC licence:
8
+
9
+ ```
10
+ ISC License
11
+
12
+ Copyright (c) 2026 Lucide Icons and Contributors
13
+
14
+ Permission to use, copy, modify, and/or distribute this software for any
15
+ purpose with or without fee is hereby granted, provided that the above
16
+ copyright notice and this permission notice appear in all copies.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
19
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
20
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
21
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
23
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
24
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
25
+ ```
26
+
27
+ Lucide itself is derived in part from [Feather](https://feathericons.com)
28
+ (MIT, Copyright (c) 2013-2023 Cole Bemis).
@@ -176,6 +176,142 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
176
176
  args: [{ providedIn: 'root' }]
177
177
  }], ctorParameters: () => [] });
178
178
 
179
+ /** The default scale (same numbers as Tailwind, so mental models transfer). */
180
+ const MK_DEFAULT_BREAKPOINTS = {
181
+ sm: 640,
182
+ md: 768,
183
+ lg: 1024,
184
+ xl: 1280,
185
+ '2xl': 1536,
186
+ };
187
+ /** Override the breakpoint scale app-wide: `{ provide: MK_BREAKPOINTS, useValue: { …, lg: 1100 } }`. */
188
+ const MK_BREAKPOINTS = new InjectionToken('MK_BREAKPOINTS', {
189
+ providedIn: 'root',
190
+ factory: () => MK_DEFAULT_BREAKPOINTS,
191
+ });
192
+ const ORDER = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
193
+ /** True when `value` is a per-breakpoint map rather than a plain value. */
194
+ function mkIsResponsive(value) {
195
+ return (typeof value === 'object' &&
196
+ value !== null &&
197
+ !Array.isArray(value) &&
198
+ Object.keys(value).every((k) => ORDER.includes(k)));
199
+ }
200
+ /**
201
+ * Reactive viewport breakpoints, one `matchMedia` listener per step.
202
+ *
203
+ * ```ts
204
+ * private readonly bp = inject(MkBreakpointService);
205
+ * readonly compact = this.bp.down('md'); // Signal<boolean>
206
+ * readonly columns = computed(() => this.bp.resolve({ xs: 1, md: 2, xl: 4 }));
207
+ * ```
208
+ *
209
+ * On the server (no `window`) everything reports `xs`.
210
+ */
211
+ class MkBreakpointService {
212
+ /** The breakpoint scale in effect (see {@link MK_BREAKPOINTS}). */
213
+ breakpoints = inject(MK_BREAKPOINTS);
214
+ document = inject(DOCUMENT);
215
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
216
+ /** `matches` per step, in scale order (`sm` first). */
217
+ steps = new Map();
218
+ cleanup = [];
219
+ queries = new Map();
220
+ ups = new Map();
221
+ downs = new Map();
222
+ /** The widest breakpoint the viewport currently satisfies. */
223
+ current;
224
+ constructor() {
225
+ for (const name of ORDER.slice(1)) {
226
+ const min = this.breakpoints[name];
227
+ this.steps.set(name, this.watch(`(min-width: ${min}px)`));
228
+ }
229
+ this.current = computed(() => {
230
+ let current = 'xs';
231
+ for (const name of ORDER.slice(1))
232
+ if (this.steps.get(name)())
233
+ current = name;
234
+ return current;
235
+ }, /* @ts-ignore */
236
+ ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
237
+ }
238
+ /** True while the viewport is at least `bp` wide (`up('md')` = md, lg, xl, 2xl). */
239
+ up(bp) {
240
+ let s = this.ups.get(bp);
241
+ if (!s) {
242
+ s = bp === 'xs' ? signal(true).asReadonly() : this.steps.get(bp).asReadonly();
243
+ this.ups.set(bp, s);
244
+ }
245
+ return s;
246
+ }
247
+ /** True while the viewport is narrower than `bp` (`down('md')` = xs, sm). */
248
+ down(bp) {
249
+ let s = this.downs.get(bp);
250
+ if (!s) {
251
+ const up = this.up(bp);
252
+ s = computed(() => !up());
253
+ this.downs.set(bp, s);
254
+ }
255
+ return s;
256
+ }
257
+ /** True while the viewport is at least `from` and narrower than `to`. */
258
+ between(from, to) {
259
+ const lo = this.up(from);
260
+ const hi = this.down(to);
261
+ return computed(() => lo() && hi());
262
+ }
263
+ /** Any media query as a signal, e.g. `observe('(orientation: portrait)')`. */
264
+ observe(query) {
265
+ let s = this.queries.get(query);
266
+ if (!s) {
267
+ s = this.watch(query).asReadonly();
268
+ this.queries.set(query, s);
269
+ }
270
+ return s;
271
+ }
272
+ /**
273
+ * Pick the entry of a responsive value for the current viewport —
274
+ * mobile-first, so `{ xs: 1, md: 2 }` gives 1 on `sm` and 2 on `xl`.
275
+ * A plain value is returned as is. Read inside a `computed` to track it.
276
+ */
277
+ resolve(value) {
278
+ if (!mkIsResponsive(value))
279
+ return value;
280
+ const idx = ORDER.indexOf(this.current());
281
+ for (let i = idx; i >= 0; i--) {
282
+ const v = value[ORDER[i]];
283
+ if (v !== undefined)
284
+ return v;
285
+ }
286
+ return undefined;
287
+ }
288
+ ngOnDestroy() {
289
+ for (const off of this.cleanup)
290
+ off();
291
+ this.cleanup.length = 0;
292
+ }
293
+ watch(query) {
294
+ const s = signal(false, /* @ts-ignore */
295
+ ...(ngDevMode ? [{ debugName: "s" }] : /* istanbul ignore next */ []));
296
+ if (!this.isBrowser)
297
+ return s;
298
+ const mql = this.document.defaultView?.matchMedia?.(query);
299
+ if (!mql)
300
+ return s;
301
+ s.set(mql.matches);
302
+ const onChange = (e) => s.set(e.matches);
303
+ mql.addEventListener('change', onChange);
304
+ this.cleanup.push(() => mql.removeEventListener('change', onChange));
305
+ return s;
306
+ }
307
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBreakpointService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
308
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBreakpointService, providedIn: 'root' });
309
+ }
310
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBreakpointService, decorators: [{
311
+ type: Injectable,
312
+ args: [{ providedIn: 'root' }]
313
+ }], ctorParameters: () => [] });
314
+
179
315
  /**
180
316
  * Announces messages to assistive technology via a visually-hidden
181
317
  * `aria-live` region. Used by toasts, form validation, sort changes, etc.
@@ -678,7 +814,7 @@ function sideOf(placement) {
678
814
  return 'top';
679
815
  if (placement.startsWith('bottom'))
680
816
  return 'bottom';
681
- return placement;
817
+ return placement.startsWith('left') ? 'left' : 'right';
682
818
  }
683
819
  function alignOf(placement) {
684
820
  if (placement.endsWith('-start'))
@@ -740,8 +876,14 @@ function mkComputeAnchoredPosition(anchor, panel, viewport, opts) {
740
876
  left = anchor.left + anchor.width / 2 - w / 2;
741
877
  }
742
878
  else {
743
- // Left/right: centre on the cross (vertical) axis.
744
- top = anchor.top + anchor.height / 2 - h / 2;
879
+ // Left/right: the cross axis is vertical. `-start` tops the panel with the
880
+ // anchor (a submenu beside its item), `-end` bottoms it, else centre.
881
+ if (align === 'start')
882
+ top = anchor.top;
883
+ else if (align === 'end')
884
+ top = anchor.bottom - h;
885
+ else
886
+ top = anchor.top + anchor.height / 2 - h / 2;
745
887
  }
746
888
  if (clamp) {
747
889
  left = Math.max(gap, Math.min(left, vw - w - gap));
@@ -793,6 +935,13 @@ class MkAnchoredPanel {
793
935
  /** Clamp the panel inside the viewport. */
794
936
  clamp = input(true, /* @ts-ignore */
795
937
  ...(ngDevMode ? [{ debugName: "clamp" }] : /* istanbul ignore next */ []));
938
+ /**
939
+ * Predicate consulted before an outside pointerdown dismisses the panel.
940
+ * Return `true` for targets that must keep it open — e.g. a nested panel
941
+ * (a submenu) that lives in the top layer outside this element.
942
+ */
943
+ keepOpenWhen = input(null, /* @ts-ignore */
944
+ ...(ngDevMode ? [{ debugName: "keepOpenWhen" }] : /* istanbul ignore next */ []));
796
945
  /** Emitted on an outside pointerdown or when the window loses focus. */
797
946
  dismiss = output();
798
947
  popover = false;
@@ -828,6 +977,8 @@ class MkAnchoredPanel {
828
977
  const anchorEl = this.resolveAnchorEl();
829
978
  if (anchorEl?.contains(target))
830
979
  return;
980
+ if (this.keepOpenWhen()?.(target))
981
+ return;
831
982
  this.dismiss.emit();
832
983
  };
833
984
  onWindowBlur = () => this.dismiss.emit();
@@ -1022,7 +1173,7 @@ class MkAnchoredPanel {
1022
1173
  el.remove();
1023
1174
  }
1024
1175
  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 });
1176
+ 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 }, keepOpenWhen: { classPropertyName: "keepOpenWhen", publicName: "keepOpenWhen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, exportAs: ["mkAnchoredPanel"], ngImport: i0 });
1026
1177
  }
1027
1178
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkAnchoredPanel, decorators: [{
1028
1179
  type: Directive,
@@ -1030,7 +1181,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1030
1181
  selector: '[mkAnchoredPanel]',
1031
1182
  exportAs: 'mkAnchoredPanel',
1032
1183
  }]
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"] }] } });
1184
+ }], 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 }] }], keepOpenWhen: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepOpenWhen", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
1034
1185
 
1035
1186
  /** The built-in English validation messages. */
1036
1187
  const MK_DEFAULT_VALIDATION = {
@@ -1132,6 +1283,11 @@ const MK_DEFAULT_I18N = {
1132
1283
  chooseWeek: 'Choose week',
1133
1284
  openCalendar: 'Open calendar',
1134
1285
  openTimeList: 'Open time list',
1286
+ moreActions: 'More actions',
1287
+ selectDateTime: 'Select date and time…',
1288
+ chooseDateTime: 'Choose date and time',
1289
+ chooseTime: 'Choose time',
1290
+ openDateTimePicker: 'Open date and time picker',
1135
1291
  daySegment: 'Day',
1136
1292
  monthSegment: 'Month',
1137
1293
  yearSegment: 'Year',
@@ -1147,6 +1303,8 @@ const MK_DEFAULT_I18N = {
1147
1303
  expandHeader: 'Expand',
1148
1304
  expandRow: 'Expand row',
1149
1305
  collapseRow: 'Collapse row',
1306
+ expandTreeRow: 'Show child rows',
1307
+ collapseTreeRow: 'Hide child rows',
1150
1308
  expandGroup: 'Expand group',
1151
1309
  collapseGroup: 'Collapse group',
1152
1310
  groupCount: (count) => `${count} item${count === 1 ? '' : 's'}`,
@@ -1572,5 +1730,5 @@ function mkFirstErrorMessage(errors, strings, overrides) {
1572
1730
  * Generated bundle index. Do not edit.
1573
1731
  */
1574
1732
 
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 };
1733
+ export { MK_BREAKPOINTS, MK_DEFAULT_BREAKPOINTS, MK_DEFAULT_DATE_NAMES, MK_DEFAULT_I18N, MK_DEFAULT_VALIDATION, MK_I18N, MK_OVERLAY_DATA, MkAnchoredPanel, MkBreakpointService, MkFieldContext, MkFocusTrap, MkLiveAnnouncer, MkOverlayRef, MkOverlayService, MkThemeService, mkComputeAnchoredPosition, mkFirstErrorMessage, mkGetFocusable, mkHighlight, mkHighlightJson, mkIsResponsive, mkUniqueId, mkValidatorChange, provideMkI18n };
1576
1734
  //# sourceMappingURL=mk-kit-ui-core.mjs.map