@haiyue/ui 0.2.1 → 0.2.2
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/CHANGELOG.md +16 -0
- package/LICENSE +27 -0
- package/README.md +201 -3
- package/dist/adapters/web/controls.js +28 -5
- package/dist/adapters/web/renderer.js +10 -2
- package/dist/adapters/web/styles.d.ts +2 -2
- package/dist/adapters/web/styles.js +26 -5
- package/dist/carousel.d.ts +92 -0
- package/dist/carousel.js +509 -0
- package/dist/collapse.d.ts +13 -0
- package/dist/collapse.js +94 -7
- package/dist/core/types.d.ts +22 -3
- package/dist/core/validation.js +9 -4
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/internal/qrcodegen.d.ts +94 -0
- package/dist/internal/qrcodegen.js +856 -0
- package/dist/masonry.d.ts +36 -0
- package/dist/masonry.js +171 -0
- package/dist/modal.d.ts +73 -0
- package/dist/modal.js +361 -0
- package/dist/portable/icons.d.ts +5 -0
- package/dist/portable/icons.js +24 -0
- package/dist/portable/index.d.ts +3 -0
- package/dist/portable/index.js +2 -0
- package/dist/portable/responsive.d.ts +20 -0
- package/dist/portable/responsive.js +15 -0
- package/dist/portable/theme.d.ts +6 -1
- package/dist/portable/theme.js +23 -3
- package/dist/qr-code.d.ts +62 -0
- package/dist/qr-code.js +224 -0
- package/package.json +18 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface HYMasonryLayoutChangeDetail {
|
|
2
|
+
columns: number;
|
|
3
|
+
itemCount: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
/** Places default-slot HTML elements into the shortest column without changing DOM/focus order. */
|
|
7
|
+
export declare class HYMasonry extends HTMLElement {
|
|
8
|
+
private readonly _layout;
|
|
9
|
+
private readonly _slot;
|
|
10
|
+
private readonly _resizeObserver;
|
|
11
|
+
private readonly _mutationObserver;
|
|
12
|
+
private readonly _items;
|
|
13
|
+
private _frame;
|
|
14
|
+
private _connected;
|
|
15
|
+
private _lastLayout;
|
|
16
|
+
static get observedAttributes(): string[];
|
|
17
|
+
constructor();
|
|
18
|
+
connectedCallback(): void;
|
|
19
|
+
disconnectedCallback(): void;
|
|
20
|
+
attributeChangedCallback(): void;
|
|
21
|
+
/** Zero (the default) selects as many columns as fit minColumnWidth. */
|
|
22
|
+
get columns(): number;
|
|
23
|
+
set columns(value: number);
|
|
24
|
+
/** Preferred minimum column width in CSS pixels; narrow containers still fit one column. */
|
|
25
|
+
get minColumnWidth(): number;
|
|
26
|
+
set minColumnWidth(value: number);
|
|
27
|
+
/** Horizontal and vertical spacing in CSS pixels. */
|
|
28
|
+
get gap(): number;
|
|
29
|
+
set gap(value: number);
|
|
30
|
+
/** Coalesces layout requests into the next animation frame. Safe while disconnected. */
|
|
31
|
+
refresh: () => void;
|
|
32
|
+
private _releaseItem;
|
|
33
|
+
private _setItemProperty;
|
|
34
|
+
private _updateLayout;
|
|
35
|
+
}
|
|
36
|
+
export declare function defineMasonryComponents(): void;
|
package/dist/masonry.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const itemProperties = ['--_hy-masonry-width', '--_hy-masonry-x', '--_hy-masonry-y'];
|
|
2
|
+
function numberOption(value, fallback, min, max) {
|
|
3
|
+
if (value === null || value === '')
|
|
4
|
+
return fallback;
|
|
5
|
+
const number = Number(value);
|
|
6
|
+
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
|
|
7
|
+
}
|
|
8
|
+
/** Places default-slot HTML elements into the shortest column without changing DOM/focus order. */
|
|
9
|
+
export class HYMasonry extends HTMLElement {
|
|
10
|
+
_layout = document.createElement('div');
|
|
11
|
+
_slot = document.createElement('slot');
|
|
12
|
+
_resizeObserver = new ResizeObserver(() => this.refresh());
|
|
13
|
+
_mutationObserver = new MutationObserver(() => this.refresh());
|
|
14
|
+
_items = new Map();
|
|
15
|
+
_frame = 0;
|
|
16
|
+
_connected = false;
|
|
17
|
+
_lastLayout = '';
|
|
18
|
+
static get observedAttributes() { return ['columns', 'min-column-width', 'gap']; }
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
22
|
+
const style = document.createElement('style');
|
|
23
|
+
style.textContent = `
|
|
24
|
+
:host { display: block; min-width: 0; }
|
|
25
|
+
:host([hidden]) { display: none; }
|
|
26
|
+
.layout { position: relative; min-width: 0; }
|
|
27
|
+
slot { display: contents; }
|
|
28
|
+
::slotted(*) {
|
|
29
|
+
position: absolute;
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
max-width: none;
|
|
33
|
+
width: var(--_hy-masonry-width);
|
|
34
|
+
inset-inline-start: var(--_hy-masonry-x);
|
|
35
|
+
top: var(--_hy-masonry-y);
|
|
36
|
+
margin: 0;
|
|
37
|
+
}
|
|
38
|
+
::slotted([hidden]) { display: none !important; }
|
|
39
|
+
`;
|
|
40
|
+
this._layout.className = 'layout';
|
|
41
|
+
this._layout.part.add('layout');
|
|
42
|
+
this._layout.append(this._slot);
|
|
43
|
+
root.append(style, this._layout);
|
|
44
|
+
}
|
|
45
|
+
connectedCallback() {
|
|
46
|
+
if (this._connected)
|
|
47
|
+
return;
|
|
48
|
+
this._connected = true;
|
|
49
|
+
this._slot.addEventListener('slotchange', this.refresh);
|
|
50
|
+
this._resizeObserver.observe(this._layout);
|
|
51
|
+
this._mutationObserver.observe(this, {
|
|
52
|
+
childList: true, subtree: true, attributes: true,
|
|
53
|
+
attributeFilter: ['hidden', 'class', 'style', 'slot'],
|
|
54
|
+
});
|
|
55
|
+
this.refresh();
|
|
56
|
+
}
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this._connected = false;
|
|
59
|
+
this._slot.removeEventListener('slotchange', this.refresh);
|
|
60
|
+
this._resizeObserver.disconnect();
|
|
61
|
+
this._mutationObserver.disconnect();
|
|
62
|
+
if (this._frame)
|
|
63
|
+
cancelAnimationFrame(this._frame);
|
|
64
|
+
this._frame = 0;
|
|
65
|
+
for (const item of this._items.keys())
|
|
66
|
+
this._releaseItem(item);
|
|
67
|
+
this._layout.style.removeProperty('height');
|
|
68
|
+
this._lastLayout = '';
|
|
69
|
+
}
|
|
70
|
+
attributeChangedCallback() { this.refresh(); }
|
|
71
|
+
/** Zero (the default) selects as many columns as fit minColumnWidth. */
|
|
72
|
+
get columns() { return Math.floor(numberOption(this.getAttribute('columns'), 0, 0, 128)); }
|
|
73
|
+
set columns(value) { this.setAttribute('columns', String(Math.floor(numberOption(value, 0, 0, 128)))); }
|
|
74
|
+
/** Preferred minimum column width in CSS pixels; narrow containers still fit one column. */
|
|
75
|
+
get minColumnWidth() { return numberOption(this.getAttribute('min-column-width'), 240, 1, 100_000); }
|
|
76
|
+
set minColumnWidth(value) { this.setAttribute('min-column-width', String(numberOption(value, 240, 1, 100_000))); }
|
|
77
|
+
/** Horizontal and vertical spacing in CSS pixels. */
|
|
78
|
+
get gap() { return numberOption(this.getAttribute('gap'), 16, 0, 10_000); }
|
|
79
|
+
set gap(value) { this.setAttribute('gap', String(numberOption(value, 16, 0, 10_000))); }
|
|
80
|
+
/** Coalesces layout requests into the next animation frame. Safe while disconnected. */
|
|
81
|
+
refresh = () => {
|
|
82
|
+
if (!this._connected || this._frame)
|
|
83
|
+
return;
|
|
84
|
+
this._frame = requestAnimationFrame(() => {
|
|
85
|
+
this._frame = 0;
|
|
86
|
+
this._updateLayout();
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
_releaseItem(item) {
|
|
90
|
+
const saved = this._items.get(item);
|
|
91
|
+
if (!saved)
|
|
92
|
+
return;
|
|
93
|
+
itemProperties.forEach((property, index) => {
|
|
94
|
+
const { value, priority } = saved[index];
|
|
95
|
+
if (value)
|
|
96
|
+
item.style.setProperty(property, value, priority);
|
|
97
|
+
else
|
|
98
|
+
item.style.removeProperty(property);
|
|
99
|
+
});
|
|
100
|
+
this._resizeObserver.unobserve(item);
|
|
101
|
+
this._items.delete(item);
|
|
102
|
+
}
|
|
103
|
+
_setItemProperty(item, property, value) {
|
|
104
|
+
const text = `${value}px`;
|
|
105
|
+
if (item.style.getPropertyValue(property) !== text)
|
|
106
|
+
item.style.setProperty(property, text);
|
|
107
|
+
}
|
|
108
|
+
_updateLayout() {
|
|
109
|
+
const items = this._slot.assignedElements({ flatten: true }).filter((item) => item instanceof HTMLElement);
|
|
110
|
+
const current = new Set(items);
|
|
111
|
+
for (const item of this._items.keys())
|
|
112
|
+
if (!current.has(item))
|
|
113
|
+
this._releaseItem(item);
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
if (this._items.has(item))
|
|
116
|
+
continue;
|
|
117
|
+
this._items.set(item, itemProperties.map(property => ({
|
|
118
|
+
value: item.style.getPropertyValue(property), priority: item.style.getPropertyPriority(property),
|
|
119
|
+
})));
|
|
120
|
+
this._resizeObserver.observe(item, { box: 'border-box' });
|
|
121
|
+
}
|
|
122
|
+
const width = this._layout.clientWidth;
|
|
123
|
+
if (width <= 0)
|
|
124
|
+
return; // A hidden container is measured again when ResizeObserver sees it.
|
|
125
|
+
const gap = this.gap;
|
|
126
|
+
const requested = this.columns || Math.max(1, Math.floor((width + gap) / (this.minColumnWidth + gap)));
|
|
127
|
+
const columns = Math.min(128, requested, Math.max(1, Math.floor((width + gap) / (1 + gap))));
|
|
128
|
+
const itemWidth = (width - gap * (columns - 1)) / columns;
|
|
129
|
+
const visible = items.filter(item => !item.hidden && getComputedStyle(item).display !== 'none');
|
|
130
|
+
for (const item of visible)
|
|
131
|
+
this._setItemProperty(item, itemProperties[0], itemWidth);
|
|
132
|
+
// Read used CSS sizes before writing positions. Unlike DOMRect, these are
|
|
133
|
+
// unaffected by transforms, and unlike offsetHeight they retain fractions.
|
|
134
|
+
const itemHeights = visible.map(item => {
|
|
135
|
+
const style = getComputedStyle(item);
|
|
136
|
+
const contentHeight = Number.parseFloat(style.height);
|
|
137
|
+
if (!Number.isFinite(contentHeight))
|
|
138
|
+
return item.offsetHeight;
|
|
139
|
+
return contentHeight + (style.boxSizing === 'border-box' ? 0 :
|
|
140
|
+
Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom) +
|
|
141
|
+
Number.parseFloat(style.borderTopWidth) + Number.parseFloat(style.borderBottomWidth));
|
|
142
|
+
});
|
|
143
|
+
const heights = Array(columns).fill(0);
|
|
144
|
+
const positions = [];
|
|
145
|
+
visible.forEach((item, index) => {
|
|
146
|
+
const column = heights.indexOf(Math.min(...heights));
|
|
147
|
+
const x = column * (itemWidth + gap);
|
|
148
|
+
const y = heights[column];
|
|
149
|
+
this._setItemProperty(item, itemProperties[1], x);
|
|
150
|
+
this._setItemProperty(item, itemProperties[2], y);
|
|
151
|
+
const height = itemHeights[index];
|
|
152
|
+
heights[column] = y + height + gap;
|
|
153
|
+
positions.push(x, y, height);
|
|
154
|
+
});
|
|
155
|
+
const height = visible.length ? Math.max(...heights) - gap : 0;
|
|
156
|
+
if (this._layout.style.height !== `${height}px`)
|
|
157
|
+
this._layout.style.height = `${height}px`;
|
|
158
|
+
const signature = JSON.stringify([columns, itemWidth, height, positions]);
|
|
159
|
+
if (signature === this._lastLayout)
|
|
160
|
+
return;
|
|
161
|
+
this._lastLayout = signature;
|
|
162
|
+
this.dispatchEvent(new CustomEvent('layout-change', {
|
|
163
|
+
detail: { columns, itemCount: visible.length, height }, bubbles: true, composed: true,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function defineMasonryComponents() {
|
|
168
|
+
if (!customElements.get('hy-masonry'))
|
|
169
|
+
customElements.define('hy-masonry', HYMasonry);
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=masonry.js.map
|
package/dist/modal.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type HYModalCloseReason = 'ok' | 'cancel' | 'close-button' | 'mask' | 'escape' | 'programmatic';
|
|
2
|
+
export interface HYModalActionDetail {
|
|
3
|
+
readonly reason: Exclude<HYModalCloseReason, 'programmatic'>;
|
|
4
|
+
}
|
|
5
|
+
export interface HYModalCloseDetail {
|
|
6
|
+
readonly reason: HYModalCloseReason | string;
|
|
7
|
+
}
|
|
8
|
+
/** A native modal with default actions, async confirmation support and slotted content. */
|
|
9
|
+
export declare class HYModal extends HTMLElement {
|
|
10
|
+
private readonly _dialog;
|
|
11
|
+
private readonly _headingSlot;
|
|
12
|
+
private readonly _titleText;
|
|
13
|
+
private readonly _closeButton;
|
|
14
|
+
private readonly _footer;
|
|
15
|
+
private readonly _cancelButton;
|
|
16
|
+
private readonly _okButton;
|
|
17
|
+
private readonly _okLabel;
|
|
18
|
+
private readonly _spinner;
|
|
19
|
+
private readonly _detachedContent;
|
|
20
|
+
private readonly _contentObserver;
|
|
21
|
+
private _returnFocus;
|
|
22
|
+
private _shown;
|
|
23
|
+
private _scrollDocument;
|
|
24
|
+
private _session;
|
|
25
|
+
private _closeReason;
|
|
26
|
+
private _pointerDownOutside;
|
|
27
|
+
static get observedAttributes(): string[];
|
|
28
|
+
constructor();
|
|
29
|
+
connectedCallback(): void;
|
|
30
|
+
disconnectedCallback(): void;
|
|
31
|
+
attributeChangedCallback(name: string, oldValue: string | null, value: string | null): void;
|
|
32
|
+
get open(): boolean;
|
|
33
|
+
set open(value: boolean);
|
|
34
|
+
get heading(): string;
|
|
35
|
+
set heading(value: string);
|
|
36
|
+
get width(): number;
|
|
37
|
+
set width(value: number);
|
|
38
|
+
get centered(): boolean;
|
|
39
|
+
set centered(value: boolean);
|
|
40
|
+
get closable(): boolean;
|
|
41
|
+
set closable(value: boolean);
|
|
42
|
+
get maskClosable(): boolean;
|
|
43
|
+
set maskClosable(value: boolean);
|
|
44
|
+
get keyboard(): boolean;
|
|
45
|
+
set keyboard(value: boolean);
|
|
46
|
+
get confirmLoading(): boolean;
|
|
47
|
+
set confirmLoading(value: boolean);
|
|
48
|
+
get confirmDisabled(): boolean;
|
|
49
|
+
set confirmDisabled(value: boolean);
|
|
50
|
+
get okText(): string;
|
|
51
|
+
set okText(value: string);
|
|
52
|
+
get cancelText(): string;
|
|
53
|
+
set cancelText(value: string);
|
|
54
|
+
get showFooter(): boolean;
|
|
55
|
+
set showFooter(value: boolean);
|
|
56
|
+
get controlled(): boolean;
|
|
57
|
+
set controlled(value: boolean);
|
|
58
|
+
get destroyOnHidden(): boolean;
|
|
59
|
+
set destroyOnHidden(value: boolean);
|
|
60
|
+
showModal(): void;
|
|
61
|
+
close(reason?: HYModalCloseDetail['reason']): void;
|
|
62
|
+
private _syncView;
|
|
63
|
+
private _syncOpen;
|
|
64
|
+
private _finalizeClose;
|
|
65
|
+
private _requestAction;
|
|
66
|
+
private _outside;
|
|
67
|
+
private _stashContent;
|
|
68
|
+
private _restoreContent;
|
|
69
|
+
private _restoreFocus;
|
|
70
|
+
private _lockScroll;
|
|
71
|
+
private _unlockScroll;
|
|
72
|
+
}
|
|
73
|
+
export declare function defineModalComponents(): void;
|
package/dist/modal.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const scrollLocks = new WeakMap();
|
|
2
|
+
function focusedElement(document) {
|
|
3
|
+
let element = document.activeElement;
|
|
4
|
+
while (element?.shadowRoot?.activeElement)
|
|
5
|
+
element = element.shadowRoot.activeElement;
|
|
6
|
+
return element instanceof HTMLElement ? element : null;
|
|
7
|
+
}
|
|
8
|
+
function focusableElements(root) {
|
|
9
|
+
const result = [];
|
|
10
|
+
const visit = (element) => {
|
|
11
|
+
if (element instanceof HTMLElement && (element.hidden || element.inert || element.matches(':disabled')))
|
|
12
|
+
return;
|
|
13
|
+
const style = getComputedStyle(element);
|
|
14
|
+
if (style.display === 'none' || style.visibility === 'hidden')
|
|
15
|
+
return;
|
|
16
|
+
if (element instanceof HTMLSlotElement) {
|
|
17
|
+
const assigned = element.assignedElements({ flatten: true });
|
|
18
|
+
for (const child of assigned.length ? assigned : element.children)
|
|
19
|
+
visit(child);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (element instanceof HTMLElement && element.getClientRects().length &&
|
|
23
|
+
(element.tabIndex >= 0 || (element.isContentEditable && !element.hasAttribute('tabindex'))))
|
|
24
|
+
result.push(element);
|
|
25
|
+
for (const child of element.shadowRoot?.children ?? element.children)
|
|
26
|
+
visit(child);
|
|
27
|
+
};
|
|
28
|
+
for (const child of root.children)
|
|
29
|
+
visit(child);
|
|
30
|
+
return result.sort((a, b) => (a.tabIndex > 0 ? a.tabIndex : Infinity) - (b.tabIndex > 0 ? b.tabIndex : Infinity));
|
|
31
|
+
}
|
|
32
|
+
/** A native modal with default actions, async confirmation support and slotted content. */
|
|
33
|
+
export class HYModal extends HTMLElement {
|
|
34
|
+
_dialog = document.createElement('dialog');
|
|
35
|
+
_headingSlot = document.createElement('slot');
|
|
36
|
+
_titleText = document.createElement('span');
|
|
37
|
+
_closeButton = document.createElement('button');
|
|
38
|
+
_footer = document.createElement('footer');
|
|
39
|
+
_cancelButton = document.createElement('button');
|
|
40
|
+
_okButton = document.createElement('button');
|
|
41
|
+
_okLabel = document.createElement('span');
|
|
42
|
+
_spinner = document.createElement('span');
|
|
43
|
+
_detachedContent = document.createDocumentFragment();
|
|
44
|
+
_contentObserver = new MutationObserver(() => {
|
|
45
|
+
if (this.isConnected && !this._shown && this.destroyOnHidden)
|
|
46
|
+
this._stashContent();
|
|
47
|
+
});
|
|
48
|
+
_returnFocus = null;
|
|
49
|
+
_shown = false;
|
|
50
|
+
_scrollDocument = null;
|
|
51
|
+
_session = 0;
|
|
52
|
+
_closeReason = 'programmatic';
|
|
53
|
+
_pointerDownOutside = false;
|
|
54
|
+
static get observedAttributes() {
|
|
55
|
+
return ['open', 'heading', 'width', 'centered', 'closable', 'mask-closable', 'keyboard', 'confirm-loading', 'confirm-disabled', 'ok-text', 'cancel-text', 'show-footer', 'controlled', 'destroy-on-hidden', 'aria-label'];
|
|
56
|
+
}
|
|
57
|
+
constructor() {
|
|
58
|
+
super();
|
|
59
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
60
|
+
const style = document.createElement('style');
|
|
61
|
+
style.textContent = `
|
|
62
|
+
:host { color: var(--hy-text-color, #eef0ff); font: 13px/1.5 system-ui, sans-serif; }
|
|
63
|
+
dialog { box-sizing: border-box; flex-direction: column; width: min(var(--_hy-modal-width, var(--hy-modal-width, 520px)), calc(100vw - 32px)); max-width: calc(100vw - 32px); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); margin: auto; padding: 0; overflow: hidden; border: 1px solid var(--hy-border-color, #343765); border-radius: var(--hy-modal-radius, 12px); color: inherit; background: var(--hy-modal-bg-color, var(--hy-surface-elevated-color, #181a40)); box-shadow: 0 24px 80px var(--hy-modal-shadow-color, rgba(0,0,0,.45)); }
|
|
64
|
+
dialog:not([open]), [hidden] { display: none !important; }
|
|
65
|
+
dialog[open] { display: flex; animation: hy-modal-enter var(--hy-modal-duration, 160ms) ease-out; }
|
|
66
|
+
dialog::backdrop { background: var(--hy-modal-mask-color, var(--hy-dialog-backdrop-color, rgba(3,7,12,.66))); backdrop-filter: blur(var(--hy-modal-mask-blur, 2px)); }
|
|
67
|
+
:host([centered="false"]) dialog { margin: min(64px, 10vh) auto auto; max-height: calc(100vh - 96px); max-height: calc(100dvh - 96px); }
|
|
68
|
+
.header { display: flex; flex: 0 0 auto; align-items: center; gap: 12px; min-height: 56px; padding: 12px 16px 12px 20px; box-sizing: border-box; border-bottom: 1px solid var(--hy-border-color, #343765); }
|
|
69
|
+
h2 { flex: 1; min-width: 0; margin: 0; font-size: 16px; font-weight: 650; overflow-wrap: anywhere; }
|
|
70
|
+
.body { flex: 1 1 auto; min-height: 0; padding: var(--hy-modal-body-padding, 20px); overflow: auto; color: var(--hy-secondary-text-color, #a9add4); }
|
|
71
|
+
.footer { flex: 0 0 auto; padding: 12px 20px; border-top: 1px solid var(--hy-border-color, #343765); background: var(--hy-modal-footer-bg-color, var(--hy-surface-color, #10132f)); }
|
|
72
|
+
.actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
73
|
+
button { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 32px; padding: 6px 14px; border: 1px solid var(--hy-border-strong-color, #484b84); border-radius: var(--hy-modal-button-radius, 6px); color: var(--hy-text-color, #eef0ff); background: var(--hy-surface-elevated-color, #181a40); font: inherit; cursor: pointer; }
|
|
74
|
+
button:hover:not(:disabled) { background: var(--hy-hover-bg-color, #242752); }
|
|
75
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
76
|
+
button:focus-visible, dialog:focus-visible { outline: 2px solid var(--hy-focus-border-color, #9c8fff); outline-offset: -3px; }
|
|
77
|
+
.close { flex: 0 0 auto; width: 30px; height: 30px; min-height: 0; padding: 0; border: 0; background: transparent; color: var(--hy-secondary-text-color, #a9add4); font-size: 22px; }
|
|
78
|
+
.ok { border-color: transparent; color: var(--hy-selected-text-color, #fff); background: var(--hy-accent-strong-color, #554cc6); }
|
|
79
|
+
.ok:hover:not(:disabled) { background: var(--hy-accent-color, #8177f2); }
|
|
80
|
+
.spinner { width: 12px; height: 12px; border: 2px solid currentColor; border-inline-end-color: transparent; border-radius: 50%; animation: hy-modal-spin .7s linear infinite; }
|
|
81
|
+
::slotted([slot="footer"]) { margin: 0; }
|
|
82
|
+
@keyframes hy-modal-enter { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
|
|
83
|
+
@keyframes hy-modal-spin { to { transform: rotate(360deg); } }
|
|
84
|
+
@media (prefers-reduced-motion: reduce) { dialog[open], .spinner { animation: none; } }
|
|
85
|
+
`;
|
|
86
|
+
this._dialog.part.add('panel');
|
|
87
|
+
this._dialog.tabIndex = -1;
|
|
88
|
+
this._dialog.setAttribute('aria-modal', 'true');
|
|
89
|
+
const header = document.createElement('header');
|
|
90
|
+
header.className = 'header';
|
|
91
|
+
header.part.add('header');
|
|
92
|
+
const title = document.createElement('h2');
|
|
93
|
+
title.id = 'modal-heading';
|
|
94
|
+
title.part.add('heading');
|
|
95
|
+
this._headingSlot.name = 'heading';
|
|
96
|
+
this._headingSlot.append(this._titleText);
|
|
97
|
+
title.append(this._headingSlot);
|
|
98
|
+
this._closeButton.className = 'close';
|
|
99
|
+
this._closeButton.type = 'button';
|
|
100
|
+
this._closeButton.part.add('close-button');
|
|
101
|
+
this._closeButton.setAttribute('aria-label', 'Close modal');
|
|
102
|
+
this._closeButton.textContent = '×';
|
|
103
|
+
header.append(title, this._closeButton);
|
|
104
|
+
const body = document.createElement('div');
|
|
105
|
+
body.className = 'body';
|
|
106
|
+
body.part.add('body');
|
|
107
|
+
body.append(document.createElement('slot'));
|
|
108
|
+
this._footer.className = 'footer';
|
|
109
|
+
this._footer.part.add('footer');
|
|
110
|
+
const footerSlot = document.createElement('slot');
|
|
111
|
+
footerSlot.name = 'footer';
|
|
112
|
+
const actions = document.createElement('div');
|
|
113
|
+
actions.className = 'actions';
|
|
114
|
+
actions.part.add('actions');
|
|
115
|
+
this._cancelButton.type = this._okButton.type = 'button';
|
|
116
|
+
this._cancelButton.part.add('cancel-button');
|
|
117
|
+
this._okButton.part.add('ok-button');
|
|
118
|
+
this._okButton.className = 'ok';
|
|
119
|
+
this._spinner.className = 'spinner';
|
|
120
|
+
this._spinner.part.add('spinner');
|
|
121
|
+
this._spinner.setAttribute('aria-hidden', 'true');
|
|
122
|
+
this._okButton.append(this._spinner, this._okLabel);
|
|
123
|
+
actions.append(this._cancelButton, this._okButton);
|
|
124
|
+
footerSlot.append(actions);
|
|
125
|
+
this._footer.append(footerSlot);
|
|
126
|
+
this._dialog.append(header, body, this._footer);
|
|
127
|
+
root.append(style, this._dialog);
|
|
128
|
+
this._closeButton.addEventListener('click', () => this._requestAction('close-button'));
|
|
129
|
+
this._cancelButton.addEventListener('click', () => this._requestAction('cancel'));
|
|
130
|
+
this._okButton.addEventListener('click', () => this._requestAction('ok'));
|
|
131
|
+
this._dialog.addEventListener('cancel', event => { event.preventDefault(); if (this.keyboard)
|
|
132
|
+
this._requestAction('escape'); });
|
|
133
|
+
this._dialog.addEventListener('keydown', event => {
|
|
134
|
+
if (event.key !== 'Tab' || event.defaultPrevented || event.composedPath().find(node => node instanceof HTMLDialogElement) !== this._dialog)
|
|
135
|
+
return;
|
|
136
|
+
const elements = focusableElements(this._dialog);
|
|
137
|
+
const active = focusedElement(this.ownerDocument);
|
|
138
|
+
const first = elements[0];
|
|
139
|
+
const last = elements.at(-1);
|
|
140
|
+
if (!first || !active || !elements.includes(active) || (event.shiftKey ? active === first : active === last)) {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
(event.shiftKey ? last ?? this._dialog : first ?? this._dialog).focus();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
this._dialog.addEventListener('pointerdown', event => { this._pointerDownOutside = event.target === this._dialog && this._outside(event); });
|
|
146
|
+
this._dialog.addEventListener('pointercancel', () => { this._pointerDownOutside = false; });
|
|
147
|
+
this._dialog.addEventListener('click', event => {
|
|
148
|
+
const outside = this._pointerDownOutside && event.target === this._dialog && this._outside(event);
|
|
149
|
+
this._pointerDownOutside = false;
|
|
150
|
+
if (outside && this.maskClosable)
|
|
151
|
+
this._requestAction('mask');
|
|
152
|
+
});
|
|
153
|
+
this._dialog.addEventListener('close', () => {
|
|
154
|
+
// Native close is queued; an old event must not close a newly opened session.
|
|
155
|
+
if (!this.isConnected || this._dialog.open || !this._shown)
|
|
156
|
+
return;
|
|
157
|
+
this._finalizeClose(this._dialog.returnValue || 'programmatic');
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
connectedCallback() {
|
|
161
|
+
this._contentObserver.observe(this, { childList: true });
|
|
162
|
+
this._syncView();
|
|
163
|
+
this._syncOpen();
|
|
164
|
+
if (!this._shown && this.destroyOnHidden)
|
|
165
|
+
this._stashContent();
|
|
166
|
+
}
|
|
167
|
+
disconnectedCallback() {
|
|
168
|
+
this._contentObserver.disconnect();
|
|
169
|
+
this._shown = false;
|
|
170
|
+
this._session++;
|
|
171
|
+
this._pointerDownOutside = false;
|
|
172
|
+
if (this._dialog.open)
|
|
173
|
+
this._dialog.close();
|
|
174
|
+
this._unlockScroll();
|
|
175
|
+
this._restoreFocus();
|
|
176
|
+
}
|
|
177
|
+
attributeChangedCallback(name, oldValue, value) {
|
|
178
|
+
if (oldValue === value || !this.isConnected)
|
|
179
|
+
return;
|
|
180
|
+
this._syncView();
|
|
181
|
+
if (name === 'open')
|
|
182
|
+
this._syncOpen();
|
|
183
|
+
if (name === 'destroy-on-hidden' && !this._shown) {
|
|
184
|
+
if (this.destroyOnHidden)
|
|
185
|
+
this._stashContent();
|
|
186
|
+
else
|
|
187
|
+
this._restoreContent();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
get open() { return this.hasAttribute('open'); }
|
|
191
|
+
set open(value) { this.toggleAttribute('open', Boolean(value)); }
|
|
192
|
+
get heading() { return this.getAttribute('heading') ?? ''; }
|
|
193
|
+
set heading(value) { this.setAttribute('heading', value); }
|
|
194
|
+
get width() { const number = Number(this.getAttribute('width') ?? 520); return Number.isFinite(number) ? Math.max(240, Math.min(2000, number)) : 520; }
|
|
195
|
+
set width(value) { this.setAttribute('width', String(Number.isFinite(value) ? Math.max(240, Math.min(2000, value)) : 520)); }
|
|
196
|
+
get centered() { return this.getAttribute('centered') !== 'false'; }
|
|
197
|
+
set centered(value) { this.setAttribute('centered', String(Boolean(value))); }
|
|
198
|
+
get closable() { return this.getAttribute('closable') !== 'false'; }
|
|
199
|
+
set closable(value) { this.setAttribute('closable', String(Boolean(value))); }
|
|
200
|
+
get maskClosable() { return this.getAttribute('mask-closable') !== 'false'; }
|
|
201
|
+
set maskClosable(value) { this.setAttribute('mask-closable', String(Boolean(value))); }
|
|
202
|
+
get keyboard() { return this.getAttribute('keyboard') !== 'false'; }
|
|
203
|
+
set keyboard(value) { this.setAttribute('keyboard', String(Boolean(value))); }
|
|
204
|
+
get confirmLoading() { return this.hasAttribute('confirm-loading') && this.getAttribute('confirm-loading') !== 'false'; }
|
|
205
|
+
set confirmLoading(value) { if (value)
|
|
206
|
+
this.setAttribute('confirm-loading', '');
|
|
207
|
+
else
|
|
208
|
+
this.removeAttribute('confirm-loading'); }
|
|
209
|
+
get confirmDisabled() { return this.hasAttribute('confirm-disabled') && this.getAttribute('confirm-disabled') !== 'false'; }
|
|
210
|
+
set confirmDisabled(value) { if (value)
|
|
211
|
+
this.setAttribute('confirm-disabled', '');
|
|
212
|
+
else
|
|
213
|
+
this.removeAttribute('confirm-disabled'); }
|
|
214
|
+
get okText() { return this.getAttribute('ok-text') ?? 'OK'; }
|
|
215
|
+
set okText(value) { this.setAttribute('ok-text', value); }
|
|
216
|
+
get cancelText() { return this.getAttribute('cancel-text') ?? 'Cancel'; }
|
|
217
|
+
set cancelText(value) { this.setAttribute('cancel-text', value); }
|
|
218
|
+
get showFooter() { return this.getAttribute('show-footer') !== 'false'; }
|
|
219
|
+
set showFooter(value) { this.setAttribute('show-footer', String(Boolean(value))); }
|
|
220
|
+
get controlled() { return this.hasAttribute('controlled'); }
|
|
221
|
+
set controlled(value) { this.toggleAttribute('controlled', Boolean(value)); }
|
|
222
|
+
get destroyOnHidden() { return this.hasAttribute('destroy-on-hidden'); }
|
|
223
|
+
set destroyOnHidden(value) { this.toggleAttribute('destroy-on-hidden', Boolean(value)); }
|
|
224
|
+
showModal() { this.open = true; if (this.isConnected)
|
|
225
|
+
this._syncOpen(); }
|
|
226
|
+
close(reason = 'programmatic') {
|
|
227
|
+
this._closeReason = reason;
|
|
228
|
+
this.open = false;
|
|
229
|
+
if (this.isConnected)
|
|
230
|
+
this._syncOpen();
|
|
231
|
+
}
|
|
232
|
+
_syncView() {
|
|
233
|
+
this._titleText.textContent = this.heading || 'Modal';
|
|
234
|
+
const label = this.getAttribute('aria-label');
|
|
235
|
+
if (label) {
|
|
236
|
+
this._dialog.setAttribute('aria-label', label);
|
|
237
|
+
this._dialog.removeAttribute('aria-labelledby');
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
this._dialog.removeAttribute('aria-label');
|
|
241
|
+
this._dialog.setAttribute('aria-labelledby', 'modal-heading');
|
|
242
|
+
}
|
|
243
|
+
if (this.hasAttribute('width'))
|
|
244
|
+
this._dialog.style.setProperty('--_hy-modal-width', `${this.width}px`);
|
|
245
|
+
else
|
|
246
|
+
this._dialog.style.removeProperty('--_hy-modal-width');
|
|
247
|
+
this._closeButton.hidden = !this.closable;
|
|
248
|
+
this._closeButton.disabled = this.confirmLoading;
|
|
249
|
+
this._footer.hidden = !this.showFooter;
|
|
250
|
+
this._cancelButton.textContent = this.cancelText;
|
|
251
|
+
this._okLabel.textContent = this.okText;
|
|
252
|
+
this._okButton.disabled = this.confirmLoading || this.confirmDisabled;
|
|
253
|
+
this._cancelButton.disabled = this.confirmLoading;
|
|
254
|
+
this._okButton.setAttribute('aria-busy', String(this.confirmLoading));
|
|
255
|
+
this._dialog.setAttribute('aria-busy', String(this.confirmLoading));
|
|
256
|
+
this._spinner.hidden = !this.confirmLoading;
|
|
257
|
+
}
|
|
258
|
+
_syncOpen() {
|
|
259
|
+
if (this.open && !this._shown) {
|
|
260
|
+
this._restoreContent();
|
|
261
|
+
this._syncView();
|
|
262
|
+
this._returnFocus = focusedElement(this.ownerDocument);
|
|
263
|
+
this._dialog.returnValue = '';
|
|
264
|
+
this._shown = true;
|
|
265
|
+
const session = ++this._session;
|
|
266
|
+
this._closeReason = 'programmatic';
|
|
267
|
+
this._lockScroll();
|
|
268
|
+
try {
|
|
269
|
+
this._dialog.showModal();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
this._shown = false;
|
|
273
|
+
this._unlockScroll();
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
if (this._shown && session === this._session)
|
|
277
|
+
this.dispatchEvent(new CustomEvent('modal-open', { bubbles: true, composed: true }));
|
|
278
|
+
}
|
|
279
|
+
else if (!this.open && this._shown) {
|
|
280
|
+
const reason = this._closeReason;
|
|
281
|
+
const session = this._session;
|
|
282
|
+
this._shown = false;
|
|
283
|
+
if (this._dialog.open)
|
|
284
|
+
this._dialog.close();
|
|
285
|
+
if (session === this._session)
|
|
286
|
+
this._finalizeClose(reason);
|
|
287
|
+
else
|
|
288
|
+
this.dispatchEvent(new CustomEvent('modal-close', { detail: { reason }, bubbles: true, composed: true }));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
_finalizeClose(reason) {
|
|
292
|
+
this._shown = false;
|
|
293
|
+
this._closeReason = 'programmatic';
|
|
294
|
+
this._pointerDownOutside = false;
|
|
295
|
+
this.removeAttribute('open');
|
|
296
|
+
this._unlockScroll();
|
|
297
|
+
if (this.destroyOnHidden)
|
|
298
|
+
this._stashContent();
|
|
299
|
+
this._restoreFocus();
|
|
300
|
+
this.dispatchEvent(new CustomEvent('modal-close', { detail: { reason }, bubbles: true, composed: true }));
|
|
301
|
+
}
|
|
302
|
+
_requestAction(reason) {
|
|
303
|
+
if (!this._shown || this.confirmLoading || (reason === 'ok' && this.confirmDisabled))
|
|
304
|
+
return;
|
|
305
|
+
const session = this._session;
|
|
306
|
+
const accepted = this.dispatchEvent(new CustomEvent(reason === 'ok' ? 'modal-ok' : 'modal-cancel', {
|
|
307
|
+
detail: { reason }, bubbles: true, composed: true, cancelable: true,
|
|
308
|
+
}));
|
|
309
|
+
if (accepted && !this.controlled && !this.confirmLoading && this._shown && session === this._session)
|
|
310
|
+
this.close(reason);
|
|
311
|
+
}
|
|
312
|
+
_outside(event) {
|
|
313
|
+
const rect = this._dialog.getBoundingClientRect();
|
|
314
|
+
return event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
|
|
315
|
+
}
|
|
316
|
+
_stashContent() { if (this.childNodes.length)
|
|
317
|
+
this._detachedContent.append(...this.childNodes); }
|
|
318
|
+
_restoreContent() { if (this._detachedContent.childNodes.length)
|
|
319
|
+
this.prepend(this._detachedContent); }
|
|
320
|
+
_restoreFocus() {
|
|
321
|
+
const element = this._returnFocus;
|
|
322
|
+
this._returnFocus = null;
|
|
323
|
+
if (element?.isConnected)
|
|
324
|
+
element.focus({ preventScroll: true });
|
|
325
|
+
}
|
|
326
|
+
_lockScroll() {
|
|
327
|
+
if (this._scrollDocument)
|
|
328
|
+
return;
|
|
329
|
+
const document = this.ownerDocument;
|
|
330
|
+
const style = document.documentElement.style;
|
|
331
|
+
const state = scrollLocks.get(document) ?? { count: 0, value: style.getPropertyValue('overflow'), priority: style.getPropertyPriority('overflow') };
|
|
332
|
+
state.count++;
|
|
333
|
+
scrollLocks.set(document, state);
|
|
334
|
+
style.setProperty('overflow', 'hidden');
|
|
335
|
+
this._scrollDocument = document;
|
|
336
|
+
}
|
|
337
|
+
_unlockScroll() {
|
|
338
|
+
const document = this._scrollDocument;
|
|
339
|
+
if (!document)
|
|
340
|
+
return;
|
|
341
|
+
this._scrollDocument = null;
|
|
342
|
+
const state = scrollLocks.get(document);
|
|
343
|
+
if (!state)
|
|
344
|
+
return;
|
|
345
|
+
if (--state.count > 0)
|
|
346
|
+
return;
|
|
347
|
+
scrollLocks.delete(document);
|
|
348
|
+
const style = document.documentElement.style;
|
|
349
|
+
if (style.getPropertyValue('overflow') !== 'hidden' || style.getPropertyPriority('overflow'))
|
|
350
|
+
return;
|
|
351
|
+
if (state.value)
|
|
352
|
+
style.setProperty('overflow', state.value, state.priority);
|
|
353
|
+
else
|
|
354
|
+
style.removeProperty('overflow');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export function defineModalComponents() {
|
|
358
|
+
if (!customElements.get('hy-modal'))
|
|
359
|
+
customElements.define('hy-modal', HYModal);
|
|
360
|
+
}
|
|
361
|
+
//# sourceMappingURL=modal.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HYIconName } from '../core/types.js';
|
|
2
|
+
/** Public vector geometry. 24 × 24, 1.8 unit round strokes, no font or platform assets. */
|
|
3
|
+
export declare const HY_ICONS: Readonly<Record<HYIconName, readonly (readonly (readonly [number, number])[])[]>>;
|
|
4
|
+
/** Safe SVG built only from the library's numeric paths. Color inherits from its host. */
|
|
5
|
+
export declare function hyIconSvg(name: HYIconName): string;
|