kmaterialize 2.3.3-1.0.24 → 2.3.3-1.0.26

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 (46) hide show
  1. package/components/air-datepicker/air-datepicker.ts +2 -2
  2. package/components/extensions/card-drag-handles.scss +44 -0
  3. package/components/extensions/card-drag-handles.ts +113 -0
  4. package/components/extensions/chart-connections.ts +71 -0
  5. package/components/extensions/chart-gestures.ts +57 -0
  6. package/components/extensions/chart-print.ts +68 -0
  7. package/components/extensions/divider.scss +78 -0
  8. package/components/extensions/list.scss +193 -0
  9. package/components/extensions/list.ts +79 -0
  10. package/components/extensions/material-buttons.scss +232 -0
  11. package/components/extensions/material-buttons.ts +89 -0
  12. package/components/extensions/org-chart.scss +241 -0
  13. package/components/extensions/org-chart.ts +495 -0
  14. package/components/extensions/tippy.scss +11 -0
  15. package/components/extensions/tippy.ts +16 -0
  16. package/components/extensions/tooltip-factory.ts +40 -0
  17. package/components/extensions/tooltip-peer.ts +22 -0
  18. package/components/extensions/web/crazy-button.scss +73 -0
  19. package/components/extensions/web/crazy-button.ts +173 -0
  20. package/components/extensions/web/crazy-loading.scss +23 -0
  21. package/components/extensions/web/crazy-loading.ts +69 -0
  22. package/components/extensions/web/kmcomponent.ts +794 -0
  23. package/components/navigation-drawer/_sidenav.scss +25 -1
  24. package/components/number-input/_number-input.scss +85 -0
  25. package/components/number-input/number-input.ts +142 -1
  26. package/components/popup/popup.ts +6 -0
  27. package/components/popup/popupSpec.js +17 -0
  28. package/dist/css/materialize.colors.min.css +1 -1
  29. package/dist/css/materialize.css +1587 -7
  30. package/dist/css/materialize.min.css +2 -2
  31. package/dist/css/materialize.min.css.map +1 -1
  32. package/dist/js/materialize.cjs.js +2071 -5
  33. package/dist/js/materialize.d.ts +579 -3
  34. package/dist/js/materialize.js +2071 -5
  35. package/dist/js/materialize.min.js +21 -2
  36. package/dist/js/materialize.mjs +2061 -6
  37. package/dist/js/tippy.cjs.js +39 -0
  38. package/dist/js/tippy.d.ts +10 -0
  39. package/dist/js/tippy.mjs +37 -0
  40. package/package.json +12 -2
  41. package/sass/enhancement/jspreadsheet_materializecss.scss +329 -0
  42. package/sass/enhancement/jsuites_materializecss.scss +413 -0
  43. package/sass/enhancement/spreadsheet.scss +2 -0
  44. package/sass/enhancement/spreadsheet_editors.scss +99 -0
  45. package/sass/enhancement/textfield_layout.scss +17 -0
  46. package/sass/materialize.scss +14 -0
@@ -111,8 +111,8 @@ export class AirDatepickerField extends Component<AirDatepickerFieldOptions> {
111
111
 
112
112
  const dataset = this.el.dataset;
113
113
  const builtInLocale = dataset.dateLang === 'fr-FR'
114
- ? (await import('air-datepicker/locale/fr')).default
115
- : (await import('air-datepicker/locale/en')).default;
114
+ ? (await import('air-datepicker/locale/fr.js')).default
115
+ : (await import('air-datepicker/locale/en.js')).default;
116
116
  const pickerOptions: AirDatepickerOptions = {
117
117
  dateFormat: dataset.dateFormat || 'yyyy-MM-dd',
118
118
  autoClose: dataset.dateAutoClose !== 'false',
@@ -0,0 +1,44 @@
1
+ .card-drag-handle {
2
+ float: right;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex: 0 0 44px;
7
+ width: 44px;
8
+ height: 44px;
9
+ padding: 0;
10
+ border: 0;
11
+ border-radius: 12px;
12
+ background: transparent;
13
+ color: var(--md-sys-color-on-surface-variant);
14
+ cursor: grab;
15
+ touch-action: none;
16
+ user-select: none;
17
+ &:focus-visible { outline: 2px solid var(--md-sys-color-primary); }
18
+ &:active { cursor: grabbing; }
19
+ }
20
+ .card-handle-dragging { position: absolute !important; opacity: 0; pointer-events: none; }
21
+ .card-drag-ghost { position: fixed !important; z-index: 10000; pointer-events: none; opacity: .85; margin: 0; max-height: 180px; overflow: hidden; background: var(--md-sys-color-surface); box-shadow: 0 8px 24px color-mix(in srgb, var(--md-sys-color-shadow) 20%, transparent); }
22
+ .card-drag-placeholder { border: 2px dashed var(--md-sys-color-primary); border-radius: 14px; background: var(--md-sys-color-primary-container); margin-bottom: 8px; }
23
+
24
+ .kanban-card:has(> .card-drag-handle) {
25
+ position: relative;
26
+ > .card-drag-handle {
27
+ position: absolute;
28
+ top: 4px;
29
+ right: 4px;
30
+ float: none;
31
+ color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 65%, transparent);
32
+ background: transparent;
33
+ border-radius: 50%;
34
+ // Suppress the browser's dotted outline when focusing a handle to drag it.
35
+ outline: none;
36
+ > .material-icons { font-size: 20px; }
37
+ &:focus-visible { outline: 2px solid var(--md-sys-color-primary); }
38
+ &:hover, &:focus-visible, &:active {
39
+ color: var(--md-sys-color-on-surface);
40
+ background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
41
+ }
42
+ }
43
+ > .kanban-card-label, > .kanban-card-title { max-width: calc(100% - 36px); }
44
+ }
@@ -0,0 +1,113 @@
1
+
2
+ interface CardHandleOptions {
3
+ cardSelector: string;
4
+ dropSelector: string;
5
+ enabled: (card: HTMLElement) => boolean;
6
+ onMove: (card: HTMLElement, from: HTMLElement, to: HTMLElement) => void;
7
+ }
8
+
9
+ /** Pointer-based handles keep ordinary swipes available for page scrolling. */
10
+ export function enableCardHandles(root: HTMLElement, options: CardHandleOptions): () => void {
11
+ const events = new AbortController();
12
+ const handles: HTMLButtonElement[] = [];
13
+ let drag: { card: HTMLElement; from: HTMLElement; handle: HTMLButtonElement; pointer: number; ghost: HTMLElement; placeholder: HTMLElement; x: number; y: number } | undefined;
14
+ let frame = 0;
15
+ const targets = () => Array.from(root.querySelectorAll<HTMLElement>(options.dropSelector));
16
+ const cards = (parent: HTMLElement) => Array.from(parent.querySelectorAll<HTMLElement>(options.cardSelector)).filter(card => card !== drag?.card);
17
+ root.addEventListener('carddragcancel', () => end(false), { signal: events.signal });
18
+ function locate() {
19
+ if (!drag) return;
20
+ drag.ghost.style.left = `${drag.x + 12}px`;
21
+ drag.ghost.style.top = `${drag.y + 12}px`;
22
+ const target = document.elementFromPoint(drag.x, drag.y)?.closest<HTMLElement>(options.dropSelector);
23
+ if (!target || !root.contains(target)) { drag.placeholder.remove(); return; }
24
+ const before = cards(target).find(card => {
25
+ const box = card.getBoundingClientRect();
26
+ return drag!.y < box.top + box.height / 2;
27
+ });
28
+ target.insertBefore(drag.placeholder, before || null);
29
+ }
30
+ function scroll() {
31
+ if (!drag) return;
32
+ const box = root.getBoundingClientRect();
33
+ const velocity = (value: number, start: number, end: number) => value < start + 40 ? -8 : value > end - 40 ? 8 : 0;
34
+ root.scrollLeft += velocity(drag.x, box.left, Math.min(box.right, innerWidth));
35
+ root.scrollTop += velocity(drag.y, Math.max(box.top, 0), Math.min(box.bottom, innerHeight));
36
+ window.scrollBy(0, velocity(drag.y, 0, innerHeight));
37
+ locate();
38
+ frame = requestAnimationFrame(scroll);
39
+ }
40
+ function end(commit: boolean) {
41
+ if (!drag) return;
42
+ const current = drag;
43
+ drag = undefined;
44
+ cancelAnimationFrame(frame);
45
+ const to = current.placeholder.parentElement;
46
+ const oldNext = current.card.nextElementSibling;
47
+ if (commit && to) to.insertBefore(current.card, current.placeholder);
48
+ current.placeholder.remove(); current.ghost.remove();
49
+ current.card.classList.remove('card-handle-dragging');
50
+ if (current.handle.hasPointerCapture(current.pointer)) current.handle.releasePointerCapture(current.pointer);
51
+ current.handle.focus({ preventScroll: true });
52
+ if (commit && to && (to !== current.from || current.card.nextElementSibling !== oldNext)) options.onMove(current.card, current.from, to);
53
+ }
54
+ root.querySelectorAll<HTMLElement>(options.cardSelector).forEach(card => {
55
+ if (!options.enabled(card)) return;
56
+ const handle = document.createElement('button');
57
+ handle.type = 'button'; handle.className = 'card-drag-handle';
58
+ handle.setAttribute('aria-label', `Move ${card.querySelector('strong, h4')?.textContent || 'card'}. Drag, or use arrow keys.`);
59
+ const icon = document.createElement('i'); icon.className = 'material-icons'; icon.textContent = 'drag_indicator'; icon.setAttribute('aria-hidden', 'true');
60
+ handle.append(icon); card.prepend(handle); handles.push(handle);
61
+ handle.addEventListener('pointerdown', event => {
62
+ if (event.button !== 0 || drag || !options.enabled(card)) return;
63
+ event.preventDefault(); event.stopPropagation();
64
+ handle.focus({ preventScroll: true });
65
+ const ghost = card.cloneNode(true) as HTMLElement;
66
+ ghost.removeAttribute('id'); ghost.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
67
+ ghost.classList.add('card-drag-ghost'); ghost.setAttribute('aria-hidden', 'true'); ghost.inert = true;
68
+ // The ghost lives outside the zoomed board; the placeholder stays inside.
69
+ const scale = card.getBoundingClientRect().width / card.offsetWidth;
70
+ ghost.style.width = `${card.offsetWidth}px`;
71
+ ghost.style.transform = `scale(${scale})`;
72
+ ghost.style.transformOrigin = 'top left';
73
+ document.body.append(ghost);
74
+ const placeholder = document.createElement('div'); placeholder.className = 'card-drag-placeholder'; placeholder.setAttribute('aria-hidden', 'true');
75
+ placeholder.style.height = `${card.offsetHeight}px`;
76
+ drag = { card, from: card.parentElement!, handle, pointer: event.pointerId, ghost, placeholder, x: event.clientX, y: event.clientY };
77
+ card.classList.add('card-handle-dragging');
78
+ handle.setPointerCapture(event.pointerId); locate(); scroll();
79
+ }, { signal: events.signal });
80
+ handle.addEventListener('pointermove', event => {
81
+ if (!drag || drag.pointer !== event.pointerId) return;
82
+ drag.x = event.clientX; drag.y = event.clientY; locate();
83
+ }, { signal: events.signal });
84
+ handle.addEventListener('pointerup', event => { if (drag?.pointer === event.pointerId) end(true); }, { signal: events.signal });
85
+ handle.addEventListener('pointercancel', event => { if (drag?.pointer === event.pointerId) end(false); }, { signal: events.signal });
86
+ handle.addEventListener('lostpointercapture', () => end(false), { signal: events.signal });
87
+ handle.addEventListener('keydown', event => {
88
+ if (event.key === 'Escape') { end(false); return; }
89
+ if (!options.enabled(card) || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
90
+ event.preventDefault(); event.stopPropagation();
91
+ const from = card.parentElement!;
92
+ const columns = targets();
93
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
94
+ const to = columns[columns.indexOf(from) + (event.key === 'ArrowLeft' ? -1 : 1)];
95
+ if (!to) return;
96
+ to.append(card); options.onMove(card, from, to);
97
+ } else {
98
+ const siblings = cards(from);
99
+ const index = siblings.indexOf(card);
100
+ const adjacent = siblings[index + (event.key === 'ArrowUp' ? -1 : 1)];
101
+ if (!adjacent) return;
102
+ from.insertBefore(card, event.key === 'ArrowUp' ? adjacent : adjacent.nextSibling);
103
+ options.onMove(card, from, from);
104
+ }
105
+ if (handle.isConnected) handle.focus();
106
+ }, { signal: events.signal });
107
+ });
108
+ // The rest of a Kanban card retains its native desktop drag behavior.
109
+ root.addEventListener('dragstart', event => {
110
+ if (drag || (event.target as HTMLElement).closest('.card-drag-handle')) event.preventDefault();
111
+ }, { capture: true, signal: events.signal });
112
+ return () => { end(false); events.abort(); handles.forEach(handle => handle.remove()); };
113
+ }
@@ -0,0 +1,71 @@
1
+ export interface ChartEndpoint { id: string; type: 'person' | 'team' }
2
+
3
+ /** Drag a connection port without moving the underlying card or team. */
4
+ export function enableChartConnections(
5
+ root: HTMLElement, stage: HTMLElement, getZoom: () => number,
6
+ canConnect: (from: ChartEndpoint, to: ChartEndpoint) => boolean,
7
+ connect: (from: ChartEndpoint, to: ChartEndpoint) => void,
8
+ ) {
9
+ const events = new AbortController();
10
+ const signal = events.signal;
11
+ const ns = 'http://www.w3.org/2000/svg';
12
+ let active: { pointer: number; handle: HTMLElement; from: ChartEndpoint; target?: HTMLElement; to?: ChartEndpoint; svg: SVGSVGElement; path: SVGPathElement } | undefined;
13
+ const endpoint = (node: HTMLElement): ChartEndpoint => node.dataset.orgPerson !== undefined
14
+ ? { id: node.dataset.orgPerson, type: 'person' }
15
+ : { id: node.dataset.orgTeam!, type: 'team' };
16
+ const cancel = () => {
17
+ if (!active) return;
18
+ const previous = active;
19
+ active = undefined;
20
+ previous.target?.classList.remove('org-chart-connect-target');
21
+ previous.svg.remove();
22
+ if (previous.handle.hasPointerCapture(previous.pointer)) previous.handle.releasePointerCapture(previous.pointer);
23
+ };
24
+ const move = (event: PointerEvent) => {
25
+ if (!active || event.pointerId !== active.pointer) return;
26
+ event.preventDefault(); event.stopPropagation();
27
+ active.target?.classList.remove('org-chart-connect-target');
28
+ const target = document.elementFromPoint(event.clientX, event.clientY)?.closest<HTMLElement>('[data-org-person], [data-org-team]');
29
+ const to = target && root.contains(target) ? endpoint(target) : undefined;
30
+ active.to = to && canConnect(active.from, to) ? to : undefined;
31
+ active.target = active.to ? target! : undefined;
32
+ active.target?.classList.add('org-chart-connect-target');
33
+ const origin = stage.getBoundingClientRect(), port = active.handle.getBoundingClientRect();
34
+ const zoom = getZoom();
35
+ const x = (port.left + port.width / 2 - origin.left) / zoom;
36
+ const y = (port.top + port.height / 2 - origin.top) / zoom;
37
+ const endX = (event.clientX - origin.left) / zoom, endY = (event.clientY - origin.top) / zoom;
38
+ const bend = Math.max(40, Math.abs(endX - x) / 2);
39
+ active.path.setAttribute('d', `M ${x} ${y} C ${x + bend} ${y}, ${endX - bend} ${endY}, ${endX} ${endY}`);
40
+ };
41
+ root.addEventListener('pointerdown', event => {
42
+ const handle = (event.target as Element).closest<HTMLElement>('.org-chart-connect-port');
43
+ if (!handle || event.button !== 0 || active) return;
44
+ event.preventDefault(); event.stopPropagation();
45
+ root.dispatchEvent(new Event('carddragcancel'));
46
+ const node = handle.closest<HTMLElement>('[data-org-person], [data-org-team]')!;
47
+ const svg = document.createElementNS(ns, 'svg'), path = document.createElementNS(ns, 'path');
48
+ svg.classList.add('org-chart-connection-preview');
49
+ svg.setAttribute('aria-hidden', 'true');
50
+ svg.append(path); stage.append(svg);
51
+ active = { pointer: event.pointerId, handle, from: endpoint(node), svg, path };
52
+ handle.focus({ preventScroll: true });
53
+ handle.setPointerCapture(event.pointerId);
54
+ move(event);
55
+ }, { capture: true, signal });
56
+ root.addEventListener('pointermove', move, { capture: true, signal });
57
+ root.addEventListener('pointerup', event => {
58
+ if (!active || event.pointerId !== active.pointer) return;
59
+ move(event);
60
+ const { from, to } = active;
61
+ cancel();
62
+ if (to) connect(from, to);
63
+ }, { capture: true, signal });
64
+ const lost = (event: PointerEvent) => { if (event.pointerId === active?.pointer) cancel(); };
65
+ root.addEventListener('pointercancel', lost, { capture: true, signal });
66
+ root.addEventListener('lostpointercapture', lost, { capture: true, signal });
67
+ document.addEventListener('keydown', event => {
68
+ if (active && event.key === 'Escape') { event.preventDefault(); cancel(); }
69
+ }, { signal });
70
+ return { cancel, destroy: () => { cancel(); events.abort(); } };
71
+ }
@@ -0,0 +1,57 @@
1
+ /** Two-finger pinch and Ctrl/trackpad wheel zoom; ordinary wheel keeps scrolling. */
2
+ export function enableChartGestures(root: HTMLElement, getZoom: () => number, setZoom: (zoom: number) => void, cancelDrag: () => void): () => void {
3
+ const events = new AbortController();
4
+ const signal = events.signal;
5
+ const oldTouchAction = root.style.touchAction;
6
+ root.style.touchAction = 'none';
7
+ let previous: { x: number; y: number; distance: number } | undefined;
8
+ let panning = false;
9
+ const position = (touches: TouchList) => {
10
+ const first = touches[0], second = touches[1] || first;
11
+ return { x: (first.clientX + second.clientX) / 2, y: (first.clientY + second.clientY) / 2,
12
+ distance: Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) };
13
+ };
14
+ const zoomAt = (value: number, x: number, y: number, oldX = x, oldY = y) => {
15
+ const rect = root.getBoundingClientRect();
16
+ const localX = x - rect.left - root.clientLeft, localY = y - rect.top - root.clientTop;
17
+ const worldX = (root.scrollLeft + oldX - rect.left - root.clientLeft) / getZoom();
18
+ const worldY = (root.scrollTop + oldY - rect.top - root.clientTop) / getZoom();
19
+ setZoom(value);
20
+ root.scrollLeft = worldX * getZoom() - localX;
21
+ root.scrollTop = worldY * getZoom() - localY;
22
+ };
23
+ root.addEventListener('wheel', event => {
24
+ if (!event.ctrlKey && !event.metaKey) return;
25
+ event.preventDefault();
26
+ cancelDrag();
27
+ const delta = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? root.clientHeight : 1);
28
+ zoomAt(getZoom() * Math.exp(-delta * 0.01), event.clientX, event.clientY);
29
+ }, { passive: false, signal });
30
+ root.addEventListener('touchstart', event => {
31
+ previous = position(event.touches);
32
+ panning = event.touches.length > 1 || !(event.target as Element).closest('.card-drag-handle, [data-team], button, input, a, .org-chart-link-label, .org-chart-link-remove');
33
+ if (event.touches.length > 1) {
34
+ event.preventDefault();
35
+ cancelDrag();
36
+ }
37
+ }, { passive: false, capture: true, signal });
38
+ root.addEventListener('touchmove', event => {
39
+ if (!previous || !panning) return;
40
+ event.preventDefault();
41
+ const next = position(event.touches);
42
+ if (event.touches.length > 1 && previous.distance > 0) {
43
+ zoomAt(getZoom() * next.distance / previous.distance, next.x, next.y, previous.x, previous.y);
44
+ } else {
45
+ root.scrollLeft += previous.x - next.x;
46
+ root.scrollTop += previous.y - next.y;
47
+ }
48
+ previous = next;
49
+ }, { passive: false, signal });
50
+ const end = (event: TouchEvent) => {
51
+ previous = event.touches.length ? position(event.touches) : undefined;
52
+ if (!previous) panning = false;
53
+ };
54
+ root.addEventListener('touchend', end, { signal });
55
+ root.addEventListener('touchcancel', end, { signal });
56
+ return () => { events.abort(); root.style.touchAction = oldTouchAction; };
57
+ }
@@ -0,0 +1,68 @@
1
+ export interface ChartPrintOptions {
2
+ /** PDF-only theme. Does not change the application theme. Defaults to light. */
3
+ theme?: 'light' | 'dark';
4
+ }
5
+
6
+ /** Print a full-size snapshot, without changing the live chart or its zoom. */
7
+ export async function printChart(stage: HTMLElement, title: string, options: ChartPrintOptions = {}): Promise<void> {
8
+ const theme = options.theme ?? 'light';
9
+ if (theme !== 'light' && theme !== 'dark') throw new TypeError('Invalid PDF theme.');
10
+ // Open synchronously from the button click so popup blockers allow the preview.
11
+ const preview = window.open('', '_blank');
12
+ if (!preview) throw new Error('Allow popups to open the PDF print preview.');
13
+ const doc = preview.document;
14
+ doc.title = title;
15
+ doc.documentElement.setAttribute('theme', theme);
16
+ doc.documentElement.style.colorScheme = theme;
17
+ doc.documentElement.style.fontSize = getComputedStyle(document.documentElement).fontSize;
18
+ const snapshot = stage.cloneNode(true) as HTMLElement;
19
+ const sourceStyle = getComputedStyle(stage);
20
+ // Keep both Materialize palettes, then select the requested one in the preview.
21
+ // Do not freeze computed card colors: that would bake in the screen's dark mode.
22
+ for (const property of Array.from(sourceStyle)) {
23
+ if (!property.startsWith('--md-')) continue;
24
+ doc.body.style.setProperty(property, sourceStyle.getPropertyValue(property));
25
+ }
26
+ for (const property of Array.from(sourceStyle)) {
27
+ if (property.startsWith('--md-sys-color-') && property.endsWith(`-${theme}`)) {
28
+ doc.body.style.setProperty(property.slice(0, -theme.length - 1), sourceStyle.getPropertyValue(property));
29
+ }
30
+ }
31
+ doc.body.style.fontFamily = sourceStyle.fontFamily;
32
+ doc.body.style.fontSize = sourceStyle.fontSize;
33
+ doc.body.style.lineHeight = sourceStyle.lineHeight;
34
+ // Inline item overrides remain on the clone; theme-based colors resolve afresh.
35
+ [snapshot, ...snapshot.querySelectorAll('*')].forEach(node => node.removeAttribute('id'));
36
+ // Keep stylesheets for pseudo-elements (edge accents) and web fonts.
37
+ const resources = Array.from(document.querySelectorAll('style, link[rel="stylesheet"]')).map(node => {
38
+ const clone = node.cloneNode(true) as HTMLStyleElement | HTMLLinkElement;
39
+ if (clone instanceof HTMLLinkElement) clone.href = (node as HTMLLinkElement).href;
40
+ const ready = clone instanceof HTMLLinkElement
41
+ ? new Promise<void>(resolve => { clone.onload = () => resolve(); clone.onerror = () => resolve(); })
42
+ : Promise.resolve();
43
+ doc.head.append(clone);
44
+ return ready;
45
+ });
46
+ const width = stage.offsetWidth, height = stage.offsetHeight;
47
+ // A3 landscape, 10 mm margins. Shrink large charts to one page.
48
+ const scale = Math.min(1, (400 * 96 / 25.4) / width, (277 * 96 / 25.4) / height);
49
+ const style = doc.createElement('style');
50
+ style.textContent = `@page { size: A3 landscape; margin: 10mm; } html, body { margin: 0; padding: 0; } body { background: ${theme === 'light' ? 'white' : 'var(--md-sys-color-surface)'}; color: var(--md-sys-color-on-surface); } * { print-color-adjust: exact; -webkit-print-color-adjust: exact; }`;
51
+ doc.head.append(style);
52
+ snapshot.style.setProperty('zoom', '1');
53
+ snapshot.style.transform = `scale(${scale})`;
54
+ snapshot.style.transformOrigin = 'top left';
55
+ snapshot.style.position = 'absolute';
56
+ snapshot.style.left = '0'; snapshot.style.top = '0';
57
+ snapshot.querySelectorAll<HTMLElement>('.card-drag-handle, .org-chart-drag-icon, .org-chart-connect-port, .org-chart-link-remove, .org-chart-connection-preview').forEach(handle => {
58
+ // Opacity hides descendants too, even with their copied visibility styles.
59
+ handle.style.opacity = '0';
60
+ });
61
+ const page = doc.createElement('div');
62
+ page.style.cssText = `position:relative;width:${width * scale}px;height:${height * scale}px;overflow:hidden`;
63
+ page.append(snapshot);
64
+ doc.body.replaceChildren(page);
65
+ await Promise.all(resources);
66
+ await doc.fonts.ready;
67
+ if (!preview.closed) { preview.focus(); preview.print(); }
68
+ }
@@ -0,0 +1,78 @@
1
+ // Load after Materialize. The opt-in base preserves existing .divider elements.
2
+ .divider.divider-line {
3
+ --divider-color: var(--md-sys-color-outline-variant);
4
+ --divider-thickness: 1px;
5
+ --divider-style: solid;
6
+ --divider-spacing: 16px;
7
+ --divider-inset: 16px;
8
+ display: block;
9
+ flex-shrink: 0;
10
+ height: 0;
11
+ margin: var(--divider-spacing) 0;
12
+ border: 0;
13
+ border-block-start: var(--divider-thickness) var(--divider-style)
14
+ var(--divider-color);
15
+ background: transparent;
16
+ overflow: visible;
17
+
18
+ // Logical properties support both left-to-right and right-to-left layouts.
19
+ &.divider-inset {
20
+ margin-inline-start: var(--divider-inset);
21
+ }
22
+ &.divider-middle {
23
+ margin-inline: var(--divider-inset);
24
+ }
25
+ &.divider-strong {
26
+ --divider-color: var(--md-sys-color-outline);
27
+ }
28
+ &.divider-accent {
29
+ --divider-color: var(--md-sys-color-primary);
30
+ }
31
+ &.divider-thick {
32
+ --divider-thickness: 4px;
33
+ }
34
+ &.divider-dashed {
35
+ --divider-style: dashed;
36
+ }
37
+ &.divider-dotted {
38
+ --divider-style: dotted;
39
+ }
40
+
41
+ // A flex or grid parent supplies the vertical divider's available height.
42
+ &.divider-vertical {
43
+ width: 0;
44
+ height: auto;
45
+ align-self: stretch;
46
+ margin: 0 var(--divider-spacing);
47
+ border-block-start: 0;
48
+ border-inline-start: var(--divider-thickness) var(--divider-style)
49
+ var(--divider-color);
50
+ }
51
+
52
+ // Real text stays selectable; pseudo-elements draw the surrounding lines.
53
+ &.divider-label {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 16px;
57
+ height: auto;
58
+ border: 0;
59
+ color: var(--md-sys-color-on-surface-variant);
60
+ font-size: 14px;
61
+ line-height: 1.5;
62
+ text-align: center;
63
+ &::before,
64
+ &::after {
65
+ content: "";
66
+ flex: 1;
67
+ min-width: 8px;
68
+ border-block-start: var(--divider-thickness) var(--divider-style)
69
+ var(--divider-color);
70
+ }
71
+ &.divider-label-start::before {
72
+ flex: 0 0 24px;
73
+ }
74
+ &.divider-label-end::after {
75
+ flex: 0 0 24px;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,193 @@
1
+ // Material-themed lists. Load after Materialize; no changes to legacy collections.
2
+ .list {
3
+ --list-gap: 2px;
4
+ margin: 0;
5
+ padding: 8px 0;
6
+ list-style: none;
7
+ background: var(--md-sys-color-surface);
8
+ color: var(--md-sys-color-on-surface);
9
+ > li {
10
+ list-style: none;
11
+ }
12
+ }
13
+ // Edge-to-edge interactive rows, clipped to the enclosing surface's corners.
14
+ .list.list-flush {
15
+ padding: 0;
16
+ border-radius: inherit;
17
+ overflow: hidden;
18
+ }
19
+ .list-item {
20
+ position: relative;
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 16px;
24
+ min-height: 56px;
25
+ width: 100%;
26
+ padding: 8px 16px;
27
+ box-sizing: border-box;
28
+ color: inherit;
29
+ &.list-two-line {
30
+ min-height: 72px;
31
+ }
32
+ &.list-three-line {
33
+ min-height: 88px;
34
+ align-items: flex-start;
35
+ padding-block: 12px;
36
+ }
37
+ }
38
+ .list-content {
39
+ flex: 1;
40
+ min-width: 0;
41
+ overflow-wrap: anywhere;
42
+ }
43
+ .list-headline {
44
+ display: block;
45
+ font-size: 16px;
46
+ line-height: 24px;
47
+ color: var(--md-sys-color-on-surface);
48
+ }
49
+ .list-supporting {
50
+ display: block;
51
+ font-size: 14px;
52
+ line-height: 20px;
53
+ color: var(--md-sys-color-on-surface-variant);
54
+ }
55
+ .list-leading,
56
+ .list-trailing {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ flex-shrink: 0;
61
+ color: var(--md-sys-color-on-surface-variant);
62
+ }
63
+ .list-trailing {
64
+ gap: 8px;
65
+ font-size: 12px;
66
+ line-height: 16px;
67
+ }
68
+ .list-avatar {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ width: 40px;
73
+ height: 40px;
74
+ border-radius: 50%;
75
+ object-fit: cover;
76
+ background: var(--md-sys-color-primary-container);
77
+ color: var(--md-sys-color-on-primary-container);
78
+ }
79
+ .list-image {
80
+ width: 56px;
81
+ height: 56px;
82
+ object-fit: cover;
83
+ border-radius: 8px;
84
+ }
85
+
86
+ // Use native links, buttons, and labels for their built-in keyboard behavior.
87
+ .list-interactive {
88
+ border: 0;
89
+ background: transparent;
90
+ font: inherit;
91
+ text-align: start;
92
+ cursor: pointer;
93
+ &:hover {
94
+ background: color-mix(
95
+ in srgb,
96
+ var(--md-sys-color-on-surface) 8%,
97
+ transparent
98
+ );
99
+ }
100
+ &:focus-visible {
101
+ outline: 2px solid var(--md-sys-color-primary);
102
+ outline-offset: -2px;
103
+ }
104
+ &:disabled {
105
+ opacity: 0.38;
106
+ cursor: default;
107
+ }
108
+ }
109
+ .list-item:has(.list-control:checked),
110
+ .list-item[aria-current="page"] {
111
+ background: var(--md-sys-color-secondary-container);
112
+ color: var(--md-sys-color-on-secondary-container);
113
+ }
114
+ // Checkbox inputs use Materialize's shared checkbox styles without overrides.
115
+ // Keep the standard radio visual while allowing list text to wrap.
116
+ .list .list-control[type="radio"] + .list-content {
117
+ height: auto;
118
+ min-height: 25px;
119
+ }
120
+ .list-item:has(.list-control[type="radio"]:focus-visible) {
121
+ outline: 2px solid var(--md-sys-color-primary);
122
+ outline-offset: -2px;
123
+ }
124
+
125
+ // Segmented rows retain their silhouette when selected.
126
+ .list-segmented {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: var(--list-gap);
130
+ padding: 0;
131
+ background: transparent;
132
+ > li > .list-item {
133
+ border-radius: 4px;
134
+ background-color: var(--md-sys-color-surface-variant);
135
+ }
136
+ > li:first-child > .list-item {
137
+ border-start-start-radius: 16px;
138
+ border-start-end-radius: 16px;
139
+ }
140
+ > li:last-child > .list-item {
141
+ border-end-start-radius: 16px;
142
+ border-end-end-radius: 16px;
143
+ }
144
+ > li > .list-item:has(.list-control:checked),
145
+ > li > .list-item[aria-current="page"] {
146
+ background-color: var(--md-sys-color-secondary-container);
147
+ }
148
+ > li > .list-interactive:hover {
149
+ background-color: color-mix(
150
+ in srgb,
151
+ var(--md-sys-color-surface-variant),
152
+ var(--md-sys-color-on-surface) 8%
153
+ );
154
+ }
155
+ }
156
+ .list-divider {
157
+ height: 1px;
158
+ margin: 0 16px 0 56px;
159
+ background: var(--md-sys-color-outline-variant);
160
+ }
161
+ .list-check-stamp {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 6px;
165
+ margin-top: 8px;
166
+ font-size: 12px;
167
+ color: var(--md-sys-color-on-surface-variant);
168
+ }
169
+ .list-check-stamp[hidden] {
170
+ display: none;
171
+ }
172
+ .list-check-stamp > .material-icons {
173
+ font-size: 16px;
174
+ }
175
+ .list-check-note {
176
+ margin: 8px 0 0;
177
+ padding-inline-start: 12px;
178
+ border-inline-start: 2px solid var(--md-sys-color-outline);
179
+ font-style: italic;
180
+ }
181
+ .list-checklist-footer {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 16px;
185
+ padding: 16px;
186
+ color: var(--md-sys-color-on-surface-variant);
187
+ }
188
+ .list-checklist-footer > progress {
189
+ flex: 1;
190
+ min-width: 0;
191
+ height: 6px;
192
+ accent-color: var(--md-sys-color-primary);
193
+ }