@haiyue/ui 0.1.1 → 0.2.1
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 +56 -0
- package/README.md +504 -1
- package/dist/adapters/web/controls.d.ts +19 -0
- package/dist/adapters/web/controls.js +284 -0
- package/dist/adapters/web/dom.d.ts +10 -0
- package/dist/adapters/web/dom.js +98 -0
- package/dist/adapters/web/index.d.ts +3 -0
- package/dist/adapters/web/index.js +3 -0
- package/dist/adapters/web/renderer.d.ts +18 -0
- package/dist/adapters/web/renderer.js +545 -0
- package/dist/adapters/web/styles.d.ts +3 -0
- package/dist/adapters/web/styles.js +62 -0
- package/dist/adapters/web/types.d.ts +46 -0
- package/dist/adapters/web/types.js +2 -0
- package/dist/affix.d.ts +35 -0
- package/dist/affix.js +137 -0
- package/dist/badge.d.ts +45 -0
- package/dist/badge.js +168 -0
- package/dist/border-beam.js +3 -1
- package/dist/breadcrumb.d.ts +37 -0
- package/dist/breadcrumb.js +210 -0
- package/dist/button-group.d.ts +12 -0
- package/dist/button-group.js +72 -0
- package/dist/button.d.ts +15 -1
- package/dist/button.js +101 -12
- package/dist/cascader.d.ts +65 -0
- package/dist/cascader.js +479 -0
- package/dist/collapse.d.ts +55 -0
- package/dist/collapse.js +261 -0
- package/dist/core/actions.d.ts +32 -0
- package/dist/core/actions.js +145 -0
- package/dist/core/data.d.ts +4 -0
- package/dist/core/data.js +27 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/platform-view.d.ts +17 -0
- package/dist/core/platform-view.js +84 -0
- package/dist/core/selection.d.ts +14 -0
- package/dist/core/selection.js +93 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.js +154 -0
- package/dist/core/types.d.ts +250 -0
- package/dist/core/types.js +2 -0
- package/dist/core/validation.d.ts +24 -0
- package/dist/core/validation.js +238 -0
- package/dist/date-picker.d.ts +60 -0
- package/dist/date-picker.js +355 -0
- package/dist/dialog.d.ts +4 -0
- package/dist/dialog.js +23 -5
- package/dist/divider.d.ts +33 -0
- package/dist/divider.js +113 -0
- package/dist/draggable.d.ts +78 -0
- package/dist/draggable.js +255 -0
- package/dist/expandable.d.ts +32 -0
- package/dist/expandable.js +136 -0
- package/dist/index.d.ts +47 -1
- package/dist/index.js +69 -0
- package/dist/input.d.ts +30 -1
- package/dist/input.js +166 -54
- package/dist/portable/index.d.ts +4 -0
- package/dist/portable/index.js +3 -0
- package/dist/portable/page.d.ts +38 -0
- package/dist/portable/page.js +361 -0
- package/dist/portable/theme.d.ts +19 -0
- package/dist/portable/theme.js +40 -0
- package/dist/portable/tree.d.ts +10 -0
- package/dist/portable/tree.js +96 -0
- package/dist/range.d.ts +3 -0
- package/dist/range.js +16 -5
- package/dist/rate.d.ts +46 -0
- package/dist/rate.js +193 -0
- package/dist/resizable.d.ts +97 -0
- package/dist/resizable.js +426 -0
- package/dist/skeleton.d.ts +41 -0
- package/dist/skeleton.js +141 -0
- package/dist/steps.d.ts +45 -0
- package/dist/steps.js +203 -0
- package/dist/table.d.ts +106 -0
- package/dist/table.js +689 -0
- package/dist/tabs.d.ts +7 -0
- package/dist/tabs.js +90 -16
- package/dist/tag.d.ts +55 -0
- package/dist/tag.js +225 -0
- package/dist/time-picker.d.ts +62 -0
- package/dist/time-picker.js +298 -0
- package/dist/timeline.d.ts +48 -0
- package/dist/timeline.js +201 -0
- package/dist/toast.d.ts +55 -0
- package/dist/toast.js +224 -0
- package/dist/tour.d.ts +89 -0
- package/dist/tour.js +306 -0
- package/dist/virtual-keyboard.d.ts +95 -0
- package/dist/virtual-keyboard.js +432 -0
- package/dist/virtual-list.js +12 -10
- package/dist/virtual-mouse.d.ts +82 -0
- package/dist/virtual-mouse.js +358 -0
- package/dist/watermark.d.ts +44 -0
- package/dist/watermark.js +148 -0
- package/package.json +135 -7
package/dist/toast.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const TYPES = ['success', 'info', 'warning', 'error', 'loading'];
|
|
2
|
+
const PLACEMENTS = ['top', 'center', 'bottom'];
|
|
3
|
+
const DEFAULT_DURATION = 3;
|
|
4
|
+
const EXIT_DURATION = 150;
|
|
5
|
+
function isType(value) {
|
|
6
|
+
return typeof value === 'string' && TYPES.includes(value);
|
|
7
|
+
}
|
|
8
|
+
function isPlacement(value) {
|
|
9
|
+
return typeof value === 'string' && PLACEMENTS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
function finiteNonNegative(value, fallback) {
|
|
12
|
+
const number = Number(value);
|
|
13
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
14
|
+
}
|
|
15
|
+
/** Lightweight imperative feedback center for short, transient messages. */
|
|
16
|
+
export class HYToast extends HTMLElement {
|
|
17
|
+
_containers = new Map();
|
|
18
|
+
_toasts = new Map();
|
|
19
|
+
_sequence = 0;
|
|
20
|
+
static get observedAttributes() { return ['placement', 'duration', 'max-count']; }
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
24
|
+
const style = document.createElement('style');
|
|
25
|
+
style.textContent = `
|
|
26
|
+
:host { position: fixed; z-index: var(--hy-toast-z-index, 1200); inset: 0; display: block; pointer-events: none; }
|
|
27
|
+
.container { position: fixed; left: 50%; display: flex; width: min(var(--hy-toast-width, 420px), calc(100vw - 32px)); flex-direction: column; align-items: center; gap: var(--hy-toast-gap, 9px); pointer-events: none; transform: translateX(-50%); }
|
|
28
|
+
.container[data-placement="top"] { top: var(--hy-toast-offset, 24px); }
|
|
29
|
+
.container[data-placement="center"] { top: 50%; transform: translate(-50%, -50%); }
|
|
30
|
+
.container[data-placement="bottom"] { bottom: var(--hy-toast-offset, 24px); flex-direction: column-reverse; }
|
|
31
|
+
.toast {
|
|
32
|
+
--_tone: var(--hy-info-color, var(--hy-accent-color, #79a8ff));
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: 20px minmax(0, 1fr) auto;
|
|
35
|
+
gap: 9px;
|
|
36
|
+
min-width: min(240px, calc(100vw - 32px));
|
|
37
|
+
max-width: 100%;
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
align-items: center;
|
|
40
|
+
padding: 10px 12px;
|
|
41
|
+
border: 1px solid color-mix(in srgb, var(--_tone) 28%, var(--hy-border-color, #303746));
|
|
42
|
+
border-radius: var(--hy-toast-radius, 8px);
|
|
43
|
+
color: var(--hy-text-color, #e8effa);
|
|
44
|
+
background: var(--hy-toast-bg-color, var(--hy-surface-elevated-color, #171e2a));
|
|
45
|
+
box-shadow: 0 10px 30px var(--hy-toast-shadow-color, rgba(0, 0, 0, .3));
|
|
46
|
+
pointer-events: auto;
|
|
47
|
+
font: 12px/1.45 system-ui, sans-serif;
|
|
48
|
+
animation: hy-toast-enter 160ms cubic-bezier(.22, 1, .36, 1);
|
|
49
|
+
transition: opacity 140ms ease, transform 140ms ease;
|
|
50
|
+
}
|
|
51
|
+
.toast[data-type="success"] { --_tone: var(--hy-success-color, #55c98a); }
|
|
52
|
+
.toast[data-type="warning"] { --_tone: var(--hy-warning-color, #e7b75a); }
|
|
53
|
+
.toast[data-type="error"] { --_tone: var(--hy-error-color, #ef6b78); }
|
|
54
|
+
.toast[data-type="loading"] { --_tone: var(--hy-accent-color, #79a8ff); }
|
|
55
|
+
.toast.closing { opacity: 0; transform: translateY(-6px) scale(.97); }
|
|
56
|
+
.container[data-placement="bottom"] .toast.closing { transform: translateY(6px) scale(.97); }
|
|
57
|
+
.icon { display: grid; width: 20px; height: 20px; place-items: center; color: var(--_tone); font-weight: 800; }
|
|
58
|
+
.toast[data-type="loading"] .icon { width: 15px; height: 15px; margin: 2px; border: 2px solid color-mix(in srgb, var(--_tone) 25%, transparent); border-top-color: var(--_tone); border-radius: 50%; animation: hy-toast-spin .75s linear infinite; font-size: 0; }
|
|
59
|
+
.message { min-width: 0; overflow-wrap: anywhere; }
|
|
60
|
+
.close { display: grid; width: 20px; height: 20px; padding: 0; border: 0; border-radius: 4px; place-items: center; color: var(--hy-secondary-text-color, #9aa8ba); background: transparent; cursor: pointer; font: 16px/1 system-ui, sans-serif; }
|
|
61
|
+
.close:hover { color: inherit; background: var(--hy-hover-bg-color, #242d3b); }
|
|
62
|
+
.close:focus-visible { outline: 2px solid var(--hy-focus-border-color, #79a8ff); outline-offset: 1px; }
|
|
63
|
+
@keyframes hy-toast-enter { from { opacity: 0; transform: translateY(-7px) scale(.97); } }
|
|
64
|
+
@keyframes hy-toast-spin { to { transform: rotate(360deg); } }
|
|
65
|
+
@media (prefers-reduced-motion: reduce) { .toast { animation-duration: 1ms; transition-duration: 1ms; } .toast[data-type="loading"] .icon { animation-duration: 1.5s; } }
|
|
66
|
+
`;
|
|
67
|
+
root.append(style);
|
|
68
|
+
for (const placement of PLACEMENTS) {
|
|
69
|
+
const container = document.createElement('div');
|
|
70
|
+
container.className = 'container';
|
|
71
|
+
container.dataset.placement = placement;
|
|
72
|
+
root.append(container);
|
|
73
|
+
this._containers.set(placement, container);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
connectedCallback() { this._syncAttributes(); }
|
|
77
|
+
disconnectedCallback() {
|
|
78
|
+
for (const toast of this._toasts.values())
|
|
79
|
+
this._removeToast(toast, false);
|
|
80
|
+
this._toasts.clear();
|
|
81
|
+
for (const container of this._containers.values())
|
|
82
|
+
container.replaceChildren();
|
|
83
|
+
}
|
|
84
|
+
attributeChangedCallback() { this._syncAttributes(); }
|
|
85
|
+
get placement() { return isPlacement(this.getAttribute('placement')) ? this.getAttribute('placement') : 'top'; }
|
|
86
|
+
set placement(value) { this.setAttribute('placement', isPlacement(value) ? value : 'top'); }
|
|
87
|
+
get duration() { return finiteNonNegative(this.getAttribute('duration'), DEFAULT_DURATION); }
|
|
88
|
+
set duration(value) { this.setAttribute('duration', String(finiteNonNegative(value, DEFAULT_DURATION))); }
|
|
89
|
+
get maxCount() {
|
|
90
|
+
const value = Math.floor(Number(this.getAttribute('max-count')));
|
|
91
|
+
return Number.isFinite(value) ? Math.min(20, Math.max(1, value)) : 5;
|
|
92
|
+
}
|
|
93
|
+
set maxCount(value) {
|
|
94
|
+
const normalized = Number.isFinite(Number(value)) ? Math.min(20, Math.max(1, Math.floor(Number(value)))) : 5;
|
|
95
|
+
this.setAttribute('max-count', String(normalized));
|
|
96
|
+
}
|
|
97
|
+
get closable() { return this.hasAttribute('closable'); }
|
|
98
|
+
set closable(value) { this.toggleAttribute('closable', Boolean(value)); }
|
|
99
|
+
get size() { return this._toasts.size; }
|
|
100
|
+
open(options) {
|
|
101
|
+
const normalized = typeof options === 'string' ? { message: options } : options;
|
|
102
|
+
const type = isType(normalized.type) ? normalized.type : 'info';
|
|
103
|
+
const placement = isPlacement(normalized.placement) ? normalized.placement : this.placement;
|
|
104
|
+
const fallbackDuration = type === 'loading' ? 0 : this.duration;
|
|
105
|
+
const duration = finiteNonNegative(normalized.duration, fallbackDuration);
|
|
106
|
+
const key = normalized.key ? String(normalized.key) : `hy-toast-${++this._sequence}`;
|
|
107
|
+
const existing = this._toasts.get(key);
|
|
108
|
+
if (existing)
|
|
109
|
+
this._removeToast(existing, false);
|
|
110
|
+
const element = this._createToast(key, type, String(normalized.message ?? ''), normalized.closable ?? this.closable);
|
|
111
|
+
const toast = { key, type, placement, element, timer: null, exitTimer: null, closing: false };
|
|
112
|
+
this._toasts.set(key, toast);
|
|
113
|
+
this._containers.get(placement)?.append(element);
|
|
114
|
+
while (this._toasts.size > this.maxCount) {
|
|
115
|
+
const oldest = this._toasts.values().next().value;
|
|
116
|
+
if (!oldest)
|
|
117
|
+
break;
|
|
118
|
+
this._removeToast(oldest, true, 'overflow');
|
|
119
|
+
}
|
|
120
|
+
if (duration > 0)
|
|
121
|
+
toast.timer = window.setTimeout(() => this._closeToast(toast, 'timeout'), duration * 1000);
|
|
122
|
+
this.dispatchEvent(new CustomEvent('toast-open', {
|
|
123
|
+
detail: { key, type, placement },
|
|
124
|
+
bubbles: true,
|
|
125
|
+
composed: true,
|
|
126
|
+
}));
|
|
127
|
+
return key;
|
|
128
|
+
}
|
|
129
|
+
success(options) { return this._openTyped('success', options); }
|
|
130
|
+
info(options) { return this._openTyped('info', options); }
|
|
131
|
+
warning(options) { return this._openTyped('warning', options); }
|
|
132
|
+
error(options) { return this._openTyped('error', options); }
|
|
133
|
+
loading(options) { return this._openTyped('loading', options); }
|
|
134
|
+
close(key) {
|
|
135
|
+
const toast = this._toasts.get(String(key));
|
|
136
|
+
if (toast)
|
|
137
|
+
this._closeToast(toast, 'programmatic');
|
|
138
|
+
}
|
|
139
|
+
clear() {
|
|
140
|
+
for (const toast of [...this._toasts.values()])
|
|
141
|
+
this._closeToast(toast, 'programmatic');
|
|
142
|
+
}
|
|
143
|
+
_openTyped(type, options) {
|
|
144
|
+
return this.open(typeof options === 'string' ? { type, message: options } : { ...options, type });
|
|
145
|
+
}
|
|
146
|
+
_createToast(key, type, message, closable) {
|
|
147
|
+
const toast = document.createElement('div');
|
|
148
|
+
toast.className = 'toast';
|
|
149
|
+
toast.dataset.key = key;
|
|
150
|
+
toast.dataset.type = type;
|
|
151
|
+
toast.part.add('toast');
|
|
152
|
+
toast.setAttribute('role', type === 'error' || type === 'warning' ? 'alert' : 'status');
|
|
153
|
+
toast.setAttribute('aria-live', type === 'error' || type === 'warning' ? 'assertive' : 'polite');
|
|
154
|
+
const icon = document.createElement('span');
|
|
155
|
+
icon.className = 'icon';
|
|
156
|
+
icon.part.add('icon');
|
|
157
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
158
|
+
icon.textContent = { success: '✓', info: 'i', warning: '!', error: '×', loading: '' }[type];
|
|
159
|
+
const text = document.createElement('span');
|
|
160
|
+
text.className = 'message';
|
|
161
|
+
text.part.add('message');
|
|
162
|
+
text.textContent = message;
|
|
163
|
+
toast.append(icon, text);
|
|
164
|
+
if (closable) {
|
|
165
|
+
const close = document.createElement('button');
|
|
166
|
+
close.className = 'close';
|
|
167
|
+
close.part.add('close-button');
|
|
168
|
+
close.type = 'button';
|
|
169
|
+
close.textContent = '×';
|
|
170
|
+
close.setAttribute('aria-label', `Close ${type} toast`);
|
|
171
|
+
close.addEventListener('click', () => {
|
|
172
|
+
const record = this._toasts.get(key);
|
|
173
|
+
if (record)
|
|
174
|
+
this._closeToast(record, 'action');
|
|
175
|
+
});
|
|
176
|
+
toast.append(close);
|
|
177
|
+
}
|
|
178
|
+
return toast;
|
|
179
|
+
}
|
|
180
|
+
_closeToast(toast, reason) {
|
|
181
|
+
if (toast.closing || this._toasts.get(toast.key) !== toast)
|
|
182
|
+
return;
|
|
183
|
+
toast.closing = true;
|
|
184
|
+
if (toast.timer !== null)
|
|
185
|
+
window.clearTimeout(toast.timer);
|
|
186
|
+
toast.timer = null;
|
|
187
|
+
toast.element.classList.add('closing');
|
|
188
|
+
toast.exitTimer = window.setTimeout(() => this._removeToast(toast, true, reason), EXIT_DURATION);
|
|
189
|
+
}
|
|
190
|
+
_removeToast(toast, emit, reason = 'programmatic') {
|
|
191
|
+
if (toast.timer !== null)
|
|
192
|
+
window.clearTimeout(toast.timer);
|
|
193
|
+
if (toast.exitTimer !== null)
|
|
194
|
+
window.clearTimeout(toast.exitTimer);
|
|
195
|
+
toast.timer = null;
|
|
196
|
+
toast.exitTimer = null;
|
|
197
|
+
toast.element.remove();
|
|
198
|
+
if (this._toasts.get(toast.key) === toast)
|
|
199
|
+
this._toasts.delete(toast.key);
|
|
200
|
+
if (!emit)
|
|
201
|
+
return;
|
|
202
|
+
this.dispatchEvent(new CustomEvent('toast-close', {
|
|
203
|
+
detail: { key: toast.key, type: toast.type, placement: toast.placement, reason },
|
|
204
|
+
bubbles: true,
|
|
205
|
+
composed: true,
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
_syncAttributes() {
|
|
209
|
+
const placement = this.getAttribute('placement');
|
|
210
|
+
if (placement !== null && !isPlacement(placement))
|
|
211
|
+
this.setAttribute('placement', 'top');
|
|
212
|
+
const duration = this.getAttribute('duration');
|
|
213
|
+
if (duration !== null && finiteNonNegative(duration, -1) < 0)
|
|
214
|
+
this.setAttribute('duration', String(DEFAULT_DURATION));
|
|
215
|
+
const maxCount = this.getAttribute('max-count');
|
|
216
|
+
if (maxCount !== null && String(this.maxCount) !== maxCount)
|
|
217
|
+
this.setAttribute('max-count', String(this.maxCount));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export function defineToastComponents() {
|
|
221
|
+
if (!customElements.get('hy-toast'))
|
|
222
|
+
customElements.define('hy-toast', HYToast);
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=toast.js.map
|
package/dist/tour.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type HYTourPlacement = 'top' | 'right' | 'bottom' | 'left' | 'center';
|
|
2
|
+
export type HYTourCloseReason = 'close' | 'mask' | 'escape' | 'finish' | 'programmatic';
|
|
3
|
+
export type HYTourTarget = string | Element | (() => Element | null);
|
|
4
|
+
export interface HYTourStep {
|
|
5
|
+
readonly target?: HYTourTarget;
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
readonly placement?: HYTourPlacement;
|
|
9
|
+
readonly nextText?: string;
|
|
10
|
+
readonly previousText?: string;
|
|
11
|
+
readonly mask?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface HYTourChangeDetail {
|
|
14
|
+
readonly current: number;
|
|
15
|
+
readonly previous: number;
|
|
16
|
+
readonly step: HYTourStep;
|
|
17
|
+
}
|
|
18
|
+
export interface HYTourCloseDetail {
|
|
19
|
+
readonly current: number;
|
|
20
|
+
readonly reason: HYTourCloseReason;
|
|
21
|
+
}
|
|
22
|
+
export interface HYTourOpenChangeDetail {
|
|
23
|
+
readonly open: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface HYTourFinishDetail {
|
|
26
|
+
readonly current: number;
|
|
27
|
+
readonly step: HYTourStep;
|
|
28
|
+
}
|
|
29
|
+
/** Multi-step spotlight tour for introducing controls and workflows. */
|
|
30
|
+
export declare class HYTour extends HTMLElement {
|
|
31
|
+
private readonly _masks;
|
|
32
|
+
private readonly _highlight;
|
|
33
|
+
private readonly _popover;
|
|
34
|
+
private readonly _title;
|
|
35
|
+
private readonly _description;
|
|
36
|
+
private readonly _counter;
|
|
37
|
+
private readonly _previous;
|
|
38
|
+
private readonly _next;
|
|
39
|
+
private readonly _close;
|
|
40
|
+
private _steps;
|
|
41
|
+
private _stepsAssigned;
|
|
42
|
+
private _current;
|
|
43
|
+
private _documentAbort;
|
|
44
|
+
private _resizeObserver;
|
|
45
|
+
private _target;
|
|
46
|
+
private _returnFocus;
|
|
47
|
+
private _scrolledStep;
|
|
48
|
+
private _frame;
|
|
49
|
+
static get observedAttributes(): string[];
|
|
50
|
+
constructor();
|
|
51
|
+
connectedCallback(): void;
|
|
52
|
+
disconnectedCallback(): void;
|
|
53
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
54
|
+
get steps(): readonly HYTourStep[];
|
|
55
|
+
set steps(value: readonly HYTourStep[]);
|
|
56
|
+
get open(): boolean;
|
|
57
|
+
set open(value: boolean);
|
|
58
|
+
get current(): number;
|
|
59
|
+
set current(value: number);
|
|
60
|
+
get placement(): HYTourPlacement;
|
|
61
|
+
set placement(value: HYTourPlacement);
|
|
62
|
+
get mask(): boolean;
|
|
63
|
+
set mask(value: boolean);
|
|
64
|
+
get closeOnMask(): boolean;
|
|
65
|
+
set closeOnMask(value: boolean);
|
|
66
|
+
get closeOnEscape(): boolean;
|
|
67
|
+
set closeOnEscape(value: boolean);
|
|
68
|
+
get autoScroll(): boolean;
|
|
69
|
+
set autoScroll(value: boolean);
|
|
70
|
+
get gap(): number;
|
|
71
|
+
set gap(value: number);
|
|
72
|
+
get zIndex(): number;
|
|
73
|
+
set zIndex(value: number);
|
|
74
|
+
start(current?: number): void;
|
|
75
|
+
next(): void;
|
|
76
|
+
previous(): void;
|
|
77
|
+
close(reason?: HYTourCloseReason): void;
|
|
78
|
+
private _readStepsAttribute;
|
|
79
|
+
private _resolvedCurrent;
|
|
80
|
+
private _goTo;
|
|
81
|
+
private _syncOpen;
|
|
82
|
+
private _teardown;
|
|
83
|
+
private _renderStep;
|
|
84
|
+
private _resolveTarget;
|
|
85
|
+
private _schedulePosition;
|
|
86
|
+
private _position;
|
|
87
|
+
private _setBox;
|
|
88
|
+
}
|
|
89
|
+
export declare function defineTourComponents(): void;
|
package/dist/tour.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
function normalizeSteps(value) {
|
|
2
|
+
if (!Array.isArray(value))
|
|
3
|
+
return [];
|
|
4
|
+
return value.flatMap(step => {
|
|
5
|
+
if (!step || typeof step !== 'object')
|
|
6
|
+
return [];
|
|
7
|
+
const record = step;
|
|
8
|
+
if (record.title == null)
|
|
9
|
+
return [];
|
|
10
|
+
const placement = ['top', 'right', 'bottom', 'left', 'center'].includes(String(record.placement)) ? String(record.placement) : undefined;
|
|
11
|
+
return [{
|
|
12
|
+
title: String(record.title),
|
|
13
|
+
...(record.description != null ? { description: String(record.description) } : {}),
|
|
14
|
+
...(typeof record.target === 'string' ? { target: record.target } : {}),
|
|
15
|
+
...(placement ? { placement } : {}),
|
|
16
|
+
...(record.nextText != null ? { nextText: String(record.nextText) } : {}),
|
|
17
|
+
...(record.previousText != null ? { previousText: String(record.previousText) } : {}),
|
|
18
|
+
...(typeof record.mask === 'boolean' ? { mask: record.mask } : {}),
|
|
19
|
+
}];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Multi-step spotlight tour for introducing controls and workflows. */
|
|
23
|
+
export class HYTour extends HTMLElement {
|
|
24
|
+
_masks = [document.createElement('div'), document.createElement('div'), document.createElement('div'), document.createElement('div')];
|
|
25
|
+
_highlight = document.createElement('div');
|
|
26
|
+
_popover = document.createElement('section');
|
|
27
|
+
_title = document.createElement('h2');
|
|
28
|
+
_description = document.createElement('div');
|
|
29
|
+
_counter = document.createElement('span');
|
|
30
|
+
_previous = document.createElement('button');
|
|
31
|
+
_next = document.createElement('button');
|
|
32
|
+
_close = document.createElement('button');
|
|
33
|
+
_steps = [];
|
|
34
|
+
_stepsAssigned = false;
|
|
35
|
+
_current = 0;
|
|
36
|
+
_documentAbort = null;
|
|
37
|
+
_resizeObserver = null;
|
|
38
|
+
_target = null;
|
|
39
|
+
_returnFocus = null;
|
|
40
|
+
_scrolledStep = -1;
|
|
41
|
+
_frame = 0;
|
|
42
|
+
static get observedAttributes() { return ['steps', 'open', 'current', 'placement', 'mask', 'close-on-mask', 'close-on-escape', 'scroll-into-view', 'gap', 'z-index']; }
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
46
|
+
const style = document.createElement('style');
|
|
47
|
+
const header = document.createElement('header');
|
|
48
|
+
const footer = document.createElement('footer');
|
|
49
|
+
style.textContent = `
|
|
50
|
+
:host { position: fixed; z-index: var(--_hy-tour-z-index, 1000); inset: 0; display: block; visibility: hidden; pointer-events: none; color: var(--hy-text-color, #eef3ff); font: 12px/1.5 system-ui, sans-serif; }
|
|
51
|
+
:host([open]) { visibility: visible; }
|
|
52
|
+
.mask { position: absolute; background: var(--hy-tour-mask-color, rgba(3, 7, 18, .68)); pointer-events: auto; }
|
|
53
|
+
.mask[hidden], .highlight[hidden] { display: none; }
|
|
54
|
+
.highlight { position: absolute; box-sizing: border-box; border: 1px solid var(--hy-tour-highlight-color, var(--hy-accent-color, #79a8ff)); border-radius: var(--hy-tour-highlight-radius, 8px); box-shadow: 0 0 0 3px var(--hy-focus-ring-color, rgba(121, 168, 255, .22)); pointer-events: none; }
|
|
55
|
+
.popover { position: absolute; width: min(var(--hy-tour-width, 310px), calc(100vw - 24px)); box-sizing: border-box; padding: 15px; border: 1px solid var(--hy-border-color, #3d4654); border-radius: var(--hy-tour-radius, 10px); outline: none; background: var(--hy-surface-elevated-color, #171d27); box-shadow: 0 18px 48px var(--hy-menu-shadow-color, rgba(0, 0, 0, .4)); pointer-events: auto; }
|
|
56
|
+
header { display: flex; align-items: flex-start; gap: 12px; }
|
|
57
|
+
h2 { flex: 1; margin: 0; color: var(--hy-text-color, #eef3ff); font: 650 14px/1.4 system-ui, sans-serif; }
|
|
58
|
+
.close { width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; color: var(--hy-muted-color, #788697); background: transparent; cursor: pointer; font: 17px/1 system-ui, sans-serif; }
|
|
59
|
+
.close:hover { color: var(--hy-text-color, #eef3ff); background: var(--hy-hover-bg-color, #273241); }
|
|
60
|
+
.description { margin-top: 8px; color: var(--hy-secondary-text-color, #aebbd0); }
|
|
61
|
+
footer { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
|
|
62
|
+
.counter { flex: 1; color: var(--hy-muted-color, #788697); font: 10px/1 ui-monospace, monospace; }
|
|
63
|
+
footer button { min-width: 58px; height: 29px; padding: 0 10px; border: 1px solid var(--hy-border-color, #3d4654); border-radius: 5px; color: var(--hy-secondary-text-color, #aebbd0); background: transparent; cursor: pointer; font: inherit; }
|
|
64
|
+
footer button:hover:not(:disabled) { color: var(--hy-text-color, #eef3ff); border-color: var(--hy-hover-border-color, #596779); background: var(--hy-hover-bg-color, #273241); }
|
|
65
|
+
footer .next { color: var(--hy-selected-text-color, #fff); border-color: var(--hy-selected-bg-color, #554cc6); background: var(--hy-selected-bg-color, #554cc6); }
|
|
66
|
+
footer button:disabled { visibility: hidden; }
|
|
67
|
+
button:focus-visible { outline: 2px solid var(--hy-focus-border-color, #79a8ff); outline-offset: 1px; }
|
|
68
|
+
`;
|
|
69
|
+
this._masks.forEach(mask => { mask.className = 'mask'; mask.part.add('mask'); mask.addEventListener('click', () => { if (this.closeOnMask)
|
|
70
|
+
this.close('mask'); }); });
|
|
71
|
+
this._highlight.className = 'highlight';
|
|
72
|
+
this._highlight.part.add('highlight');
|
|
73
|
+
this._popover.className = 'popover';
|
|
74
|
+
this._popover.part.add('popover');
|
|
75
|
+
this._popover.tabIndex = -1;
|
|
76
|
+
this._popover.setAttribute('role', 'dialog');
|
|
77
|
+
this._title.part.add('title');
|
|
78
|
+
this._description.className = 'description';
|
|
79
|
+
this._description.part.add('description');
|
|
80
|
+
this._counter.className = 'counter';
|
|
81
|
+
this._counter.part.add('counter');
|
|
82
|
+
this._close.type = 'button';
|
|
83
|
+
this._close.className = 'close';
|
|
84
|
+
this._close.part.add('close');
|
|
85
|
+
this._close.setAttribute('aria-label', 'Close tour');
|
|
86
|
+
this._close.textContent = '×';
|
|
87
|
+
this._close.addEventListener('click', () => this.close('close'));
|
|
88
|
+
header.append(this._title, this._close);
|
|
89
|
+
this._previous.type = this._next.type = 'button';
|
|
90
|
+
this._previous.className = 'previous';
|
|
91
|
+
this._previous.part.add('previous');
|
|
92
|
+
this._next.className = 'next';
|
|
93
|
+
this._next.part.add('next');
|
|
94
|
+
this._previous.addEventListener('click', () => this.previous());
|
|
95
|
+
this._next.addEventListener('click', () => this.next());
|
|
96
|
+
footer.append(this._counter, this._previous, this._next);
|
|
97
|
+
this._popover.append(header, this._description, footer);
|
|
98
|
+
root.append(style, ...this._masks, this._highlight, this._popover);
|
|
99
|
+
}
|
|
100
|
+
connectedCallback() { if (!this._stepsAssigned && this.hasAttribute('steps'))
|
|
101
|
+
this._readStepsAttribute(); this._current = Math.max(0, Number(this.getAttribute('current')) || 0); this._syncOpen(); }
|
|
102
|
+
disconnectedCallback() { this._teardown(); }
|
|
103
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
104
|
+
if (oldValue === newValue)
|
|
105
|
+
return;
|
|
106
|
+
if (name === 'steps' && !this._stepsAssigned)
|
|
107
|
+
this._readStepsAttribute();
|
|
108
|
+
if (name === 'current')
|
|
109
|
+
this._current = Math.max(0, Number(newValue) || 0);
|
|
110
|
+
if (name === 'open')
|
|
111
|
+
this._syncOpen();
|
|
112
|
+
else if (this.open)
|
|
113
|
+
this._renderStep();
|
|
114
|
+
}
|
|
115
|
+
get steps() { return this._steps; }
|
|
116
|
+
set steps(value) { this._stepsAssigned = true; this._steps = Array.isArray(value) ? value.filter(step => step && typeof step.title === 'string') : []; if (this.open)
|
|
117
|
+
this._renderStep(); }
|
|
118
|
+
get open() { return this.hasAttribute('open'); }
|
|
119
|
+
set open(value) { this.toggleAttribute('open', Boolean(value)); }
|
|
120
|
+
get current() { return this._resolvedCurrent(); }
|
|
121
|
+
set current(value) { this._current = Math.max(0, Math.floor(Number(value) || 0)); this.setAttribute('current', String(this._current)); }
|
|
122
|
+
get placement() { const value = this.getAttribute('placement'); return ['top', 'right', 'left', 'center'].includes(String(value)) ? value : 'bottom'; }
|
|
123
|
+
set placement(value) { this.setAttribute('placement', ['top', 'right', 'bottom', 'left', 'center'].includes(value) ? value : 'bottom'); }
|
|
124
|
+
get mask() { return this.getAttribute('mask') !== 'false'; }
|
|
125
|
+
set mask(value) { this.setAttribute('mask', String(Boolean(value))); }
|
|
126
|
+
get closeOnMask() { return this.hasAttribute('close-on-mask'); }
|
|
127
|
+
set closeOnMask(value) { this.toggleAttribute('close-on-mask', Boolean(value)); }
|
|
128
|
+
get closeOnEscape() { return this.getAttribute('close-on-escape') !== 'false'; }
|
|
129
|
+
set closeOnEscape(value) { this.setAttribute('close-on-escape', String(Boolean(value))); }
|
|
130
|
+
get autoScroll() { return this.getAttribute('scroll-into-view') !== 'false'; }
|
|
131
|
+
set autoScroll(value) { this.setAttribute('scroll-into-view', String(Boolean(value))); }
|
|
132
|
+
get gap() { const number = Number(this.getAttribute('gap')); return Number.isFinite(number) ? Math.min(80, Math.max(0, number)) : 8; }
|
|
133
|
+
set gap(value) { const number = Number(value); this.setAttribute('gap', String(Number.isFinite(number) ? Math.min(80, Math.max(0, number)) : 8)); }
|
|
134
|
+
get zIndex() { const number = Number(this.getAttribute('z-index')); return Number.isFinite(number) ? Math.round(number) : 1000; }
|
|
135
|
+
set zIndex(value) { this.setAttribute('z-index', String(Math.round(Number(value) || 1000))); }
|
|
136
|
+
start(current = 0) { if (!this._steps.length)
|
|
137
|
+
return; this.current = current; this.open = true; }
|
|
138
|
+
next() {
|
|
139
|
+
if (!this._steps.length)
|
|
140
|
+
return;
|
|
141
|
+
if (this.current >= this._steps.length - 1) {
|
|
142
|
+
const step = this._steps[this.current];
|
|
143
|
+
this.dispatchEvent(new CustomEvent('tour-finish', { bubbles: true, composed: true, detail: { current: this.current, step } }));
|
|
144
|
+
this.close('finish');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this._goTo(this.current + 1);
|
|
148
|
+
}
|
|
149
|
+
previous() { if (this.current > 0)
|
|
150
|
+
this._goTo(this.current - 1); }
|
|
151
|
+
close(reason = 'programmatic') { if (!this.open)
|
|
152
|
+
return; const current = this.current; this.open = false; this.dispatchEvent(new CustomEvent('tour-close', { bubbles: true, composed: true, detail: { current, reason } })); }
|
|
153
|
+
_readStepsAttribute() { try {
|
|
154
|
+
this._steps = normalizeSteps(JSON.parse(this.getAttribute('steps') ?? '[]'));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
this._steps = [];
|
|
158
|
+
} }
|
|
159
|
+
_resolvedCurrent() { return this._steps.length ? Math.min(this._steps.length - 1, this._current) : 0; }
|
|
160
|
+
_goTo(current) { const previous = this.current; this.current = current; const step = this._steps[this.current]; if (step)
|
|
161
|
+
this.dispatchEvent(new CustomEvent('tour-change', { bubbles: true, composed: true, detail: { current: this.current, previous, step } })); }
|
|
162
|
+
_syncOpen() {
|
|
163
|
+
if (!this.isConnected)
|
|
164
|
+
return;
|
|
165
|
+
this.style.setProperty('--_hy-tour-z-index', String(this.zIndex));
|
|
166
|
+
if (!this.open || !this._steps.length) {
|
|
167
|
+
this._teardown();
|
|
168
|
+
this._returnFocus?.focus({ preventScroll: true });
|
|
169
|
+
this._returnFocus = null;
|
|
170
|
+
this.dispatchEvent(new CustomEvent('tour-open-change', { bubbles: true, composed: true, detail: { open: false } }));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!this._documentAbort) {
|
|
174
|
+
this._returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
175
|
+
const abort = new AbortController();
|
|
176
|
+
this._documentAbort = abort;
|
|
177
|
+
document.addEventListener('keydown', event => { if (event.key === 'Escape' && this.closeOnEscape) {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
this.close('escape');
|
|
180
|
+
} }, { signal: abort.signal });
|
|
181
|
+
window.addEventListener('resize', () => this._schedulePosition(), { passive: true, signal: abort.signal });
|
|
182
|
+
window.addEventListener('scroll', () => this._schedulePosition(), { passive: true, capture: true, signal: abort.signal });
|
|
183
|
+
this.dispatchEvent(new CustomEvent('tour-open-change', { bubbles: true, composed: true, detail: { open: true } }));
|
|
184
|
+
}
|
|
185
|
+
this._renderStep();
|
|
186
|
+
}
|
|
187
|
+
_teardown() { this._documentAbort?.abort(); this._documentAbort = null; this._resizeObserver?.disconnect(); this._resizeObserver = null; cancelAnimationFrame(this._frame); this._frame = 0; this._target = null; this._scrolledStep = -1; }
|
|
188
|
+
_renderStep() {
|
|
189
|
+
const step = this._steps[this.current];
|
|
190
|
+
if (!step)
|
|
191
|
+
return;
|
|
192
|
+
this._title.textContent = step.title;
|
|
193
|
+
this._description.textContent = step.description ?? '';
|
|
194
|
+
this._description.hidden = !step.description;
|
|
195
|
+
this._counter.textContent = `${this.current + 1} / ${this._steps.length}`;
|
|
196
|
+
this._previous.textContent = step.previousText ?? 'Previous';
|
|
197
|
+
this._previous.disabled = this.current === 0;
|
|
198
|
+
this._next.textContent = step.nextText ?? (this.current === this._steps.length - 1 ? 'Finish' : 'Next');
|
|
199
|
+
this._popover.setAttribute('aria-label', step.title);
|
|
200
|
+
this.style.setProperty('--_hy-tour-z-index', String(this.zIndex));
|
|
201
|
+
this._target = this._resolveTarget(step.target);
|
|
202
|
+
if (this._target && this.autoScroll && this._scrolledStep !== this.current) {
|
|
203
|
+
this._target.scrollIntoView({ block: 'center', inline: 'center' });
|
|
204
|
+
this._scrolledStep = this.current;
|
|
205
|
+
}
|
|
206
|
+
this._resizeObserver?.disconnect();
|
|
207
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
208
|
+
this._resizeObserver = new ResizeObserver(() => this._schedulePosition());
|
|
209
|
+
this._resizeObserver.observe(this._popover);
|
|
210
|
+
if (this._target instanceof Element)
|
|
211
|
+
this._resizeObserver.observe(this._target);
|
|
212
|
+
}
|
|
213
|
+
this._schedulePosition();
|
|
214
|
+
queueMicrotask(() => this._popover.focus({ preventScroll: true }));
|
|
215
|
+
}
|
|
216
|
+
_resolveTarget(target) { if (typeof target === 'function')
|
|
217
|
+
return target(); if (typeof target === 'string') {
|
|
218
|
+
try {
|
|
219
|
+
return document.querySelector(target);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
} return target instanceof Element ? target : null; }
|
|
225
|
+
_schedulePosition() { cancelAnimationFrame(this._frame); this._frame = requestAnimationFrame(() => this._position()); }
|
|
226
|
+
_position() {
|
|
227
|
+
if (!this.open)
|
|
228
|
+
return;
|
|
229
|
+
const step = this._steps[this.current];
|
|
230
|
+
if (!step)
|
|
231
|
+
return;
|
|
232
|
+
const viewportWidth = window.innerWidth;
|
|
233
|
+
const viewportHeight = window.innerHeight;
|
|
234
|
+
const margin = 12;
|
|
235
|
+
const gap = this.gap;
|
|
236
|
+
const mask = step.mask ?? this.mask;
|
|
237
|
+
this._popover.setAttribute('aria-modal', String(mask));
|
|
238
|
+
if (!this._target) {
|
|
239
|
+
this._masks.forEach(piece => { piece.hidden = !mask; piece.style.cssText = mask ? 'inset:0' : ''; });
|
|
240
|
+
this._highlight.hidden = true;
|
|
241
|
+
const rect = this._popover.getBoundingClientRect();
|
|
242
|
+
this._popover.style.left = `${Math.max(margin, (viewportWidth - rect.width) / 2)}px`;
|
|
243
|
+
this._popover.style.top = `${Math.max(margin, (viewportHeight - rect.height) / 2)}px`;
|
|
244
|
+
this._popover.dataset.placement = 'center';
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const targetRect = this._target.getBoundingClientRect();
|
|
248
|
+
const left = Math.max(0, targetRect.left - gap);
|
|
249
|
+
const top = Math.max(0, targetRect.top - gap);
|
|
250
|
+
const right = Math.min(viewportWidth, targetRect.right + gap);
|
|
251
|
+
const bottom = Math.min(viewportHeight, targetRect.bottom + gap);
|
|
252
|
+
this._setBox(this._highlight, left, top, right - left, bottom - top);
|
|
253
|
+
this._highlight.hidden = !mask;
|
|
254
|
+
const maskTop = this._masks[0];
|
|
255
|
+
const maskLeft = this._masks[1];
|
|
256
|
+
const maskRight = this._masks[2];
|
|
257
|
+
const maskBottom = this._masks[3];
|
|
258
|
+
this._masks.forEach(piece => { piece.hidden = !mask; });
|
|
259
|
+
this._setBox(maskTop, 0, 0, viewportWidth, top);
|
|
260
|
+
this._setBox(maskBottom, 0, bottom, viewportWidth, Math.max(0, viewportHeight - bottom));
|
|
261
|
+
this._setBox(maskLeft, 0, top, left, Math.max(0, bottom - top));
|
|
262
|
+
this._setBox(maskRight, right, top, Math.max(0, viewportWidth - right), Math.max(0, bottom - top));
|
|
263
|
+
const popoverRect = this._popover.getBoundingClientRect();
|
|
264
|
+
let placement = step.placement ?? this.placement;
|
|
265
|
+
if (placement === 'center')
|
|
266
|
+
placement = 'bottom';
|
|
267
|
+
const distance = 12;
|
|
268
|
+
let x = left + (right - left - popoverRect.width) / 2;
|
|
269
|
+
let y = bottom + distance;
|
|
270
|
+
if (placement === 'top')
|
|
271
|
+
y = top - popoverRect.height - distance;
|
|
272
|
+
else if (placement === 'left') {
|
|
273
|
+
x = left - popoverRect.width - distance;
|
|
274
|
+
y = top + (bottom - top - popoverRect.height) / 2;
|
|
275
|
+
}
|
|
276
|
+
else if (placement === 'right') {
|
|
277
|
+
x = right + distance;
|
|
278
|
+
y = top + (bottom - top - popoverRect.height) / 2;
|
|
279
|
+
}
|
|
280
|
+
if (placement === 'top' && y < margin) {
|
|
281
|
+
placement = 'bottom';
|
|
282
|
+
y = bottom + distance;
|
|
283
|
+
}
|
|
284
|
+
else if (placement === 'bottom' && y + popoverRect.height > viewportHeight - margin) {
|
|
285
|
+
placement = 'top';
|
|
286
|
+
y = top - popoverRect.height - distance;
|
|
287
|
+
}
|
|
288
|
+
else if (placement === 'left' && x < margin) {
|
|
289
|
+
placement = 'right';
|
|
290
|
+
x = right + distance;
|
|
291
|
+
}
|
|
292
|
+
else if (placement === 'right' && x + popoverRect.width > viewportWidth - margin) {
|
|
293
|
+
placement = 'left';
|
|
294
|
+
x = left - popoverRect.width - distance;
|
|
295
|
+
}
|
|
296
|
+
this._popover.style.left = `${Math.min(viewportWidth - popoverRect.width - margin, Math.max(margin, x))}px`;
|
|
297
|
+
this._popover.style.top = `${Math.min(viewportHeight - popoverRect.height - margin, Math.max(margin, y))}px`;
|
|
298
|
+
this._popover.dataset.placement = placement;
|
|
299
|
+
}
|
|
300
|
+
_setBox(element, left, top, width, height) { element.style.cssText = `left:${left}px;top:${top}px;width:${Math.max(0, width)}px;height:${Math.max(0, height)}px`; }
|
|
301
|
+
}
|
|
302
|
+
export function defineTourComponents() {
|
|
303
|
+
if (!customElements.get('hy-tour'))
|
|
304
|
+
customElements.define('hy-tour', HYTour);
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=tour.js.map
|