@luxalgo/vela 0.6.0 → 0.6.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/dist/{DataProvider-DNx9ntBD.d.ts → DataProvider-CNmk84SH.d.cts} +27 -3
- package/dist/{DataProvider-Ce6PuCzQ.d.cts → DataProvider-DHX4x6-r.d.ts} +27 -3
- package/dist/{chunk-CA3N3PYT.js → chunk-7D6YIF34.js} +2151 -2464
- package/dist/{chunk-ZNL2X6U2.js → chunk-7Y7CJ7EN.js} +1 -1
- package/dist/{chunk-EMS6PO2T.js → chunk-PN5KWFZ4.js} +6 -1
- package/dist/chunk-QHZ7IXFL.js +2598 -0
- package/dist/chunk-TP56QRMK.js +503 -0
- package/dist/{chunk-7ROONW6H.js → chunk-U5KCQHAC.js} +141 -10
- package/dist/{contributions-Dlc7ZWZz.d.cts → contributions-DDpv7hEL.d.cts} +6 -2
- package/dist/{contributions-DToMhiw3.d.ts → contributions-ciV6Neyd.d.ts} +6 -2
- package/dist/{history-BZADxpKx.d.cts → history-CFqZm5re.d.ts} +21 -3
- package/dist/{history-BuHXJZJ2.d.ts → history-ChoT34nz.d.cts} +21 -3
- package/dist/index.cjs +3724 -2742
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -4
- package/dist/{options-BlQ00Fju.d.ts → options-BqGeFHtp.d.cts} +15 -1
- package/dist/{options-BlQ00Fju.d.cts → options-BqGeFHtp.d.ts} +15 -1
- package/dist/{plugin-BqjEa5r3.d.ts → plugin-C97gUCVf.d.ts} +7 -3
- package/dist/{plugin-BNPLOQO1.d.cts → plugin-CSb_sTiN.d.cts} +7 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/ui.cjs +1001 -247
- package/dist/ui.d.cts +258 -11
- package/dist/ui.d.ts +258 -11
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +12358 -3961
- package/dist/vela.global.min.js +479 -124
- package/dist/widget.cjs +3463 -2867
- package/dist/widget.d.cts +20 -6
- package/dist/widget.d.ts +20 -6
- package/dist/widget.js +56 -11
- package/dist/workspace.cjs +3137 -2538
- package/dist/workspace.d.cts +22 -5
- package/dist/workspace.d.ts +22 -5
- package/dist/workspace.js +56 -8
- package/package.json +1 -1
- package/dist/chunk-OGRQW3M6.js +0 -1328
- package/dist/chunk-QWIEKZRE.js +0 -1042
package/dist/chunk-QWIEKZRE.js
DELETED
|
@@ -1,1042 +0,0 @@
|
|
|
1
|
-
import { injectStyles, iconEl } from './chunk-EMS6PO2T.js';
|
|
2
|
-
import { VanillaMachine, normalizeProps, spreadProps } from '@zag-js/vanilla';
|
|
3
|
-
export { normalizeProps, spreadProps } from '@zag-js/vanilla';
|
|
4
|
-
import * as tooltip from '@zag-js/tooltip';
|
|
5
|
-
import * as menu from '@zag-js/menu';
|
|
6
|
-
import * as dialog from '@zag-js/dialog';
|
|
7
|
-
|
|
8
|
-
function runMachine(machine5, props, render) {
|
|
9
|
-
const m = new VanillaMachine(machine5, props);
|
|
10
|
-
const unsub = m.subscribe(render);
|
|
11
|
-
m.start();
|
|
12
|
-
render(m.service);
|
|
13
|
-
return {
|
|
14
|
-
service: m.service,
|
|
15
|
-
flush: () => render(m.service),
|
|
16
|
-
stop: () => {
|
|
17
|
-
unsub();
|
|
18
|
-
m.stop();
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
var uid = 0;
|
|
23
|
-
function nextUid(prefix) {
|
|
24
|
-
return `${prefix}-${++uid}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// src/ui/keymap.ts
|
|
28
|
-
var KEY_ALIASES = {
|
|
29
|
-
esc: "escape",
|
|
30
|
-
space: " ",
|
|
31
|
-
plus: "+",
|
|
32
|
-
minus: "-",
|
|
33
|
-
del: "delete",
|
|
34
|
-
return: "enter",
|
|
35
|
-
left: "arrowleft",
|
|
36
|
-
right: "arrowright",
|
|
37
|
-
up: "arrowup",
|
|
38
|
-
down: "arrowdown"
|
|
39
|
-
};
|
|
40
|
-
var MAC_GLYPHS = { meta: "\u2318", ctrl: "\u2303", alt: "\u2325", shift: "\u21E7" };
|
|
41
|
-
function parseChord(spec, mac) {
|
|
42
|
-
const parts = spec.toLowerCase().split("+").map((p) => p.trim()).filter((p, i, a) => p !== "" || a[i - 1] === "");
|
|
43
|
-
const chord = { ctrl: false, meta: false, alt: false, shift: false, key: "" };
|
|
44
|
-
for (const raw of parts) {
|
|
45
|
-
const p = raw === "" ? "+" : raw;
|
|
46
|
-
if (p === "mod") mac ? chord.meta = true : chord.ctrl = true;
|
|
47
|
-
else if (p === "ctrl" || p === "control") chord.ctrl = true;
|
|
48
|
-
else if (p === "meta" || p === "cmd" || p === "command") chord.meta = true;
|
|
49
|
-
else if (p === "alt" || p === "option") chord.alt = true;
|
|
50
|
-
else if (p === "shift") chord.shift = true;
|
|
51
|
-
else chord.key = KEY_ALIASES[p] ?? p;
|
|
52
|
-
}
|
|
53
|
-
return chord;
|
|
54
|
-
}
|
|
55
|
-
function eventMatches(ev, c) {
|
|
56
|
-
return ev.ctrlKey === c.ctrl && ev.metaKey === c.meta && ev.altKey === c.alt && // Shift is part of producing many printable keys ('?', '+') — only enforce it
|
|
57
|
-
// when the chord names a non-printable/letter key where shift is a real modifier.
|
|
58
|
-
(c.key.length > 1 || /^[a-z0-9 ]$/.test(c.key) ? ev.shiftKey === c.shift : true) && ev.key.toLowerCase() === c.key;
|
|
59
|
-
}
|
|
60
|
-
function isEditableTarget(ev) {
|
|
61
|
-
const t = ev.target;
|
|
62
|
-
if (!t || typeof t !== "object") return false;
|
|
63
|
-
const tag = (t.tagName ?? "").toLowerCase();
|
|
64
|
-
if (tag === "input" || tag === "textarea" || tag === "select") return true;
|
|
65
|
-
if (t.isContentEditable === true) return true;
|
|
66
|
-
return (typeof t.getAttribute === "function" ? t.getAttribute("role") : null) === "textbox";
|
|
67
|
-
}
|
|
68
|
-
function displayChord(spec, mac) {
|
|
69
|
-
const c = parseChord(spec, mac);
|
|
70
|
-
const keyLabel = c.key === " " ? "Space" : c.key.length === 1 ? c.key.toUpperCase() : c.key.charAt(0).toUpperCase() + c.key.slice(1);
|
|
71
|
-
if (mac) {
|
|
72
|
-
return (c.ctrl ? MAC_GLYPHS.ctrl : "") + (c.alt ? MAC_GLYPHS.alt : "") + (c.shift ? MAC_GLYPHS.shift : "") + (c.meta ? MAC_GLYPHS.meta : "") + keyLabel;
|
|
73
|
-
}
|
|
74
|
-
const mods = [c.ctrl && "Ctrl", c.alt && "Alt", c.shift && "Shift", c.meta && "Win"].filter(Boolean);
|
|
75
|
-
return [...mods, keyLabel].join("+");
|
|
76
|
-
}
|
|
77
|
-
var KeymapManager = class {
|
|
78
|
-
constructor(opts = {}) {
|
|
79
|
-
this.descriptors = /* @__PURE__ */ new Map();
|
|
80
|
-
this.rebinds = /* @__PURE__ */ new Map();
|
|
81
|
-
this.scopeStack = [];
|
|
82
|
-
this.target = null;
|
|
83
|
-
this.onKeydown = (ev) => {
|
|
84
|
-
this.handleKeydown(ev);
|
|
85
|
-
};
|
|
86
|
-
this.mac = opts.platform !== void 0 ? opts.platform === "mac" : typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
|
|
87
|
-
this.baseScope = opts.baseScope ?? "chart";
|
|
88
|
-
}
|
|
89
|
-
/** Register (or replace, by id) a binding. Returns a disposer. */
|
|
90
|
-
register(desc) {
|
|
91
|
-
this.descriptors.set(desc.id, desc);
|
|
92
|
-
return () => {
|
|
93
|
-
if (this.descriptors.get(desc.id) === desc) this.descriptors.delete(desc.id);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
unregister(id) {
|
|
97
|
-
this.descriptors.delete(id);
|
|
98
|
-
this.rebinds.delete(id);
|
|
99
|
-
}
|
|
100
|
-
/** User-level rebinding: overrides the descriptor's default chords (null resets). */
|
|
101
|
-
rebind(id, keys) {
|
|
102
|
-
if (keys === null) this.rebinds.delete(id);
|
|
103
|
-
else this.rebinds.set(id, Array.isArray(keys) ? [...keys] : [keys]);
|
|
104
|
-
}
|
|
105
|
-
/** Snapshot for a shortcuts help panel / rebinding UI. */
|
|
106
|
-
bindings() {
|
|
107
|
-
return [...this.descriptors.values()].map((d) => {
|
|
108
|
-
const keys = this.activeKeys(d);
|
|
109
|
-
return {
|
|
110
|
-
id: d.id,
|
|
111
|
-
label: d.label,
|
|
112
|
-
category: d.category ?? "General",
|
|
113
|
-
scope: d.scope ?? this.baseScope,
|
|
114
|
-
keys,
|
|
115
|
-
display: keys.map((k) => displayChord(k, this.mac))
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
pushScope(scope) {
|
|
120
|
-
this.scopeStack.push(scope);
|
|
121
|
-
return () => this.popScope(scope);
|
|
122
|
-
}
|
|
123
|
-
/** Pops the TOPMOST occurrence of `scope` (tolerates out-of-order teardown). */
|
|
124
|
-
popScope(scope) {
|
|
125
|
-
const i = this.scopeStack.lastIndexOf(scope);
|
|
126
|
-
if (i >= 0) this.scopeStack.splice(i, 1);
|
|
127
|
-
}
|
|
128
|
-
get activeScope() {
|
|
129
|
-
return this.scopeStack[this.scopeStack.length - 1] ?? this.baseScope;
|
|
130
|
-
}
|
|
131
|
-
attach(target) {
|
|
132
|
-
this.detach();
|
|
133
|
-
this.target = target;
|
|
134
|
-
target.addEventListener("keydown", this.onKeydown);
|
|
135
|
-
}
|
|
136
|
-
detach() {
|
|
137
|
-
this.target?.removeEventListener("keydown", this.onKeydown);
|
|
138
|
-
this.target = null;
|
|
139
|
-
}
|
|
140
|
-
/** The matcher — public so hosts/tests can feed events from their own listeners. */
|
|
141
|
-
handleKeydown(ev) {
|
|
142
|
-
const editable = isEditableTarget(ev);
|
|
143
|
-
for (const d of this.descriptors.values()) {
|
|
144
|
-
const scope = d.scope ?? this.baseScope;
|
|
145
|
-
if (scope !== "global" && scope !== this.activeScope) continue;
|
|
146
|
-
if (editable && !d.allowInInput) continue;
|
|
147
|
-
if (d.when && !d.when()) continue;
|
|
148
|
-
for (const spec of this.activeKeys(d)) {
|
|
149
|
-
if (eventMatches(ev, parseChord(spec, this.mac))) {
|
|
150
|
-
if (d.preventDefault !== false) {
|
|
151
|
-
ev.preventDefault?.();
|
|
152
|
-
ev.stopPropagation?.();
|
|
153
|
-
}
|
|
154
|
-
d.run(ev);
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
destroy() {
|
|
162
|
-
this.detach();
|
|
163
|
-
this.descriptors.clear();
|
|
164
|
-
this.rebinds.clear();
|
|
165
|
-
this.scopeStack.length = 0;
|
|
166
|
-
}
|
|
167
|
-
activeKeys(d) {
|
|
168
|
-
return this.rebinds.get(d.id) ?? (Array.isArray(d.keys) ? d.keys : [d.keys]);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
function tooltipController(opts = {}) {
|
|
172
|
-
return {
|
|
173
|
-
machine: tooltip.machine,
|
|
174
|
-
props: {
|
|
175
|
-
id: nextUid("vela-tooltip"),
|
|
176
|
-
openDelay: opts.openDelay ?? 0,
|
|
177
|
-
closeDelay: opts.closeDelay ?? 0,
|
|
178
|
-
interactive: opts.interactive ?? false,
|
|
179
|
-
ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
|
|
180
|
-
positioning: { placement: opts.placement ?? "top" }
|
|
181
|
-
},
|
|
182
|
-
connect: (service) => tooltip.connect(service, normalizeProps)
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// src/ui/components/tooltip/styles.ts
|
|
187
|
-
var TOOLTIP_STYLE_ID = "vela-ui-tooltip";
|
|
188
|
-
var TOOLTIP_CSS = `
|
|
189
|
-
.vela-tooltip {
|
|
190
|
-
background: var(--vela-bg);
|
|
191
|
-
color: var(--vela-fg);
|
|
192
|
-
border: 1px solid var(--vela-border-soft);
|
|
193
|
-
border-radius: var(--vela-radius-md);
|
|
194
|
-
box-shadow: var(--vela-shadow);
|
|
195
|
-
font-size: var(--vela-font-size-md);
|
|
196
|
-
line-height: 1.4;
|
|
197
|
-
padding: var(--vela-space-1) var(--vela-space-2);
|
|
198
|
-
max-width: 280px;
|
|
199
|
-
pointer-events: none;
|
|
200
|
-
z-index: var(--vela-z-tooltip);
|
|
201
|
-
}
|
|
202
|
-
.vela-tooltip[data-interactive] { pointer-events: auto; }
|
|
203
|
-
.vela-tooltip[data-state='open'] { animation: vela-tooltip-in 0.12s ease; }
|
|
204
|
-
@keyframes vela-tooltip-in {
|
|
205
|
-
from { opacity: 0; transform: scale(0.97); }
|
|
206
|
-
to { opacity: 1; transform: scale(1); }
|
|
207
|
-
}
|
|
208
|
-
`;
|
|
209
|
-
function resolveHost(trigger, host) {
|
|
210
|
-
return host ?? trigger.closest(".vela-ui") ?? trigger.ownerDocument.body;
|
|
211
|
-
}
|
|
212
|
-
var Tooltip = class {
|
|
213
|
-
constructor(trigger, opts) {
|
|
214
|
-
this.trigger = trigger;
|
|
215
|
-
const doc = trigger.ownerDocument;
|
|
216
|
-
injectStyles(TOOLTIP_STYLE_ID, TOOLTIP_CSS, doc);
|
|
217
|
-
this.positioner = doc.createElement("div");
|
|
218
|
-
this.positioner.className = "vela-ui-layer";
|
|
219
|
-
this.content = doc.createElement("div");
|
|
220
|
-
this.content.className = "vela-tooltip";
|
|
221
|
-
this.positioner.appendChild(this.content);
|
|
222
|
-
resolveHost(trigger, opts.host).appendChild(this.positioner);
|
|
223
|
-
this.setContent(opts.content);
|
|
224
|
-
const ctrl = tooltipController(opts);
|
|
225
|
-
const mid = String(ctrl.props.id);
|
|
226
|
-
if (opts.triggerId) trigger.id = opts.triggerId;
|
|
227
|
-
this.handle = runMachine(ctrl.machine, ctrl.props, (service) => {
|
|
228
|
-
const api = ctrl.connect(service);
|
|
229
|
-
spreadProps(trigger, api.getTriggerProps(), mid);
|
|
230
|
-
spreadProps(this.positioner, api.getPositionerProps(), mid);
|
|
231
|
-
spreadProps(this.content, api.getContentProps(), mid);
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
setContent(content) {
|
|
235
|
-
this.content.replaceChildren(typeof content === "function" ? content() : content);
|
|
236
|
-
}
|
|
237
|
-
destroy() {
|
|
238
|
-
this.handle.stop();
|
|
239
|
-
this.positioner.remove();
|
|
240
|
-
this.trigger.removeAttribute("data-scope");
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
function menuController(opts) {
|
|
244
|
-
return {
|
|
245
|
-
machine: menu.machine,
|
|
246
|
-
props: {
|
|
247
|
-
id: opts.id ?? nextUid("vela-menu"),
|
|
248
|
-
ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
|
|
249
|
-
positioning: {
|
|
250
|
-
placement: opts.placement ?? "bottom-start",
|
|
251
|
-
...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
|
|
252
|
-
},
|
|
253
|
-
onSelect: (d) => opts.onSelect?.(d.value),
|
|
254
|
-
onOpenChange: (d) => opts.onOpenChange?.(d.open)
|
|
255
|
-
},
|
|
256
|
-
connect: (service) => menu.connect(service, normalizeProps)
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// src/ui/components/menu/styles.ts
|
|
261
|
-
var MENU_STYLE_ID = "vela-ui-menu";
|
|
262
|
-
var MENU_CSS = `
|
|
263
|
-
.vela-menu {
|
|
264
|
-
/* The list is a <ul>: without this, block rows (the separator) paint a ::marker dot. */
|
|
265
|
-
list-style: none;
|
|
266
|
-
margin: 0;
|
|
267
|
-
background: var(--vela-surface-elev);
|
|
268
|
-
color: var(--vela-fg);
|
|
269
|
-
border: 1px solid var(--vela-border-strong);
|
|
270
|
-
border-radius: 6px;
|
|
271
|
-
box-shadow: var(--vela-shadow);
|
|
272
|
-
padding: 6px;
|
|
273
|
-
min-width: 180px;
|
|
274
|
-
max-height: 60vh;
|
|
275
|
-
overflow-y: auto;
|
|
276
|
-
font-size: 13px;
|
|
277
|
-
z-index: var(--vela-z-menu);
|
|
278
|
-
outline: none;
|
|
279
|
-
}
|
|
280
|
-
.vela-menu[data-state='open'] { animation: vela-menu-in var(--vela-dur-fast) var(--vela-ease); }
|
|
281
|
-
@keyframes vela-menu-in {
|
|
282
|
-
from { opacity: 0; transform: translateY(-3px); }
|
|
283
|
-
to { opacity: 1; transform: translateY(0); }
|
|
284
|
-
}
|
|
285
|
-
.vela-menu-item {
|
|
286
|
-
display: flex;
|
|
287
|
-
align-items: center;
|
|
288
|
-
gap: 10px;
|
|
289
|
-
padding: 7px 10px;
|
|
290
|
-
border-radius: 4px;
|
|
291
|
-
cursor: pointer;
|
|
292
|
-
user-select: none;
|
|
293
|
-
white-space: nowrap;
|
|
294
|
-
}
|
|
295
|
-
.vela-menu-item[data-highlighted] { background: var(--vela-hover); }
|
|
296
|
-
.vela-menu-item[data-disabled] { opacity: 0.4; cursor: default; }
|
|
297
|
-
.vela-menu-item .vela-icon { width: 16px; height: 16px; font-size: 16px; justify-content: center; color: var(--vela-fg-muted); }
|
|
298
|
-
.vela-menu-item .vela-menu-label { flex: 1 1 auto; }
|
|
299
|
-
.vela-menu-item .vela-menu-hint { color: var(--vela-fg-faint); font-size: var(--vela-font-size-sm); }
|
|
300
|
-
/* Active entry: the row surface carries the selection \u2014 a stronger background wash
|
|
301
|
-
than the hover one, plus bright ink. Declared after the hover rule so a selected
|
|
302
|
-
row stays visibly selected while highlighted. */
|
|
303
|
-
.vela-menu-item[data-checked] {
|
|
304
|
-
background: var(--vela-hover-strong);
|
|
305
|
-
color: var(--vela-fg-bright);
|
|
306
|
-
}
|
|
307
|
-
/* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
|
|
308
|
-
same control language as the settings dialog's toggles. */
|
|
309
|
-
.vela-menu-switch {
|
|
310
|
-
order: 99;
|
|
311
|
-
margin-left: auto;
|
|
312
|
-
position: relative;
|
|
313
|
-
flex: none;
|
|
314
|
-
width: 34px;
|
|
315
|
-
height: 18px;
|
|
316
|
-
border-radius: 9px;
|
|
317
|
-
background: var(--vela-surface-overlay);
|
|
318
|
-
border: 1px solid var(--vela-border-soft);
|
|
319
|
-
transition: background 0.16s ease, border-color 0.16s ease;
|
|
320
|
-
}
|
|
321
|
-
.vela-menu-switch::after {
|
|
322
|
-
content: '';
|
|
323
|
-
position: absolute;
|
|
324
|
-
top: 2px;
|
|
325
|
-
left: 2px;
|
|
326
|
-
width: 12px;
|
|
327
|
-
height: 12px;
|
|
328
|
-
border-radius: 50%;
|
|
329
|
-
background: var(--vela-fg-muted);
|
|
330
|
-
transition: transform 0.16s ease, background 0.16s ease;
|
|
331
|
-
}
|
|
332
|
-
.vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
|
|
333
|
-
.vela-menu-switch.on::after { transform: translateX(16px); background: var(--vela-bg); }
|
|
334
|
-
/* Favorite star (rows carrying \`favorite\`): reserved space always \u2014 it fades in on row
|
|
335
|
-
hover instead of shifting the layout \u2014 and a starred row keeps its filled star visible.
|
|
336
|
-
Same control language as the drawing toolbar's flyout stars. */
|
|
337
|
-
.vela-menu-item .vela-menu-star {
|
|
338
|
-
order: 98;
|
|
339
|
-
/* Pad the 16px icon slot into a bigger hit target without moving the row's layout. */
|
|
340
|
-
margin: -3px -5px -3px 0;
|
|
341
|
-
padding: 3px 5px;
|
|
342
|
-
box-sizing: content-box;
|
|
343
|
-
border-radius: var(--vela-radius-sm);
|
|
344
|
-
opacity: 0;
|
|
345
|
-
transition: opacity 0.1s ease, background 0.1s ease;
|
|
346
|
-
}
|
|
347
|
-
.vela-menu-item[data-highlighted] .vela-menu-star { opacity: 0.55; }
|
|
348
|
-
.vela-menu-item .vela-menu-star:hover { opacity: 1; background: var(--vela-hover-strong); }
|
|
349
|
-
.vela-menu-item .vela-menu-star.vela-fav { opacity: 0.95; color: var(--vela-highlight); }
|
|
350
|
-
.vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
|
|
351
|
-
/* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
|
|
352
|
-
list is open so the path you came down remains readable. */
|
|
353
|
-
.vela-menu-item .vela-menu-arrow {
|
|
354
|
-
order: 99;
|
|
355
|
-
margin-left: auto;
|
|
356
|
-
width: 12px;
|
|
357
|
-
flex: none;
|
|
358
|
-
font-size: 11px;
|
|
359
|
-
color: var(--vela-fg-faint);
|
|
360
|
-
}
|
|
361
|
-
.vela-menu-item[data-state='open'] { background: var(--vela-hover); }
|
|
362
|
-
`;
|
|
363
|
-
var Surface = class _Surface {
|
|
364
|
-
constructor(doc, opts) {
|
|
365
|
-
this.items = [];
|
|
366
|
-
/** Branch item id → the surface it opens. */
|
|
367
|
-
this.subs = /* @__PURE__ */ new Map();
|
|
368
|
-
/** Trigger rect captured when this level opened — the list stays put if the
|
|
369
|
-
* trigger then moves (favorite chips shifting the caret). */
|
|
370
|
-
this.pinnedAnchor = null;
|
|
371
|
-
this.doc = doc;
|
|
372
|
-
this.host = opts.host;
|
|
373
|
-
this.onSelect = opts.onSelect;
|
|
374
|
-
this.onFavorite = opts.onFavorite;
|
|
375
|
-
this.positioner = doc.createElement("div");
|
|
376
|
-
this.positioner.className = "vela-ui-layer";
|
|
377
|
-
this.list = doc.createElement("ul");
|
|
378
|
-
this.list.className = "vela-menu";
|
|
379
|
-
if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
|
|
380
|
-
this.positioner.appendChild(this.list);
|
|
381
|
-
this.host.appendChild(this.positioner);
|
|
382
|
-
const trigger = opts.trigger;
|
|
383
|
-
this.ctrl = menuController({
|
|
384
|
-
items: [],
|
|
385
|
-
id: opts.id,
|
|
386
|
-
placement: opts.placement,
|
|
387
|
-
onSelect: (id) => this.onSelect(id),
|
|
388
|
-
getAnchorRect: () => this.pinnedAnchor,
|
|
389
|
-
onOpenChange: (open) => {
|
|
390
|
-
if (open && trigger) {
|
|
391
|
-
const r = trigger.getBoundingClientRect();
|
|
392
|
-
this.pinnedAnchor = { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
393
|
-
} else {
|
|
394
|
-
this.pinnedAnchor = null;
|
|
395
|
-
}
|
|
396
|
-
opts.onOpenChange?.(open);
|
|
397
|
-
},
|
|
398
|
-
triggerId: opts.triggerId
|
|
399
|
-
});
|
|
400
|
-
this.mid = String(this.ctrl.props.id);
|
|
401
|
-
if (opts.triggerId && trigger) trigger.id = opts.triggerId;
|
|
402
|
-
this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
|
|
403
|
-
const api = this.ctrl.connect(service);
|
|
404
|
-
if (trigger) spreadProps(trigger, api.getTriggerProps(), this.mid);
|
|
405
|
-
spreadProps(this.positioner, api.getPositionerProps(), this.mid);
|
|
406
|
-
spreadProps(this.list, api.getContentProps(), this.mid);
|
|
407
|
-
this.project(api);
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
get api() {
|
|
411
|
-
return this.ctrl.connect(this.handle.service);
|
|
412
|
-
}
|
|
413
|
-
setItems(items) {
|
|
414
|
-
this.items = items;
|
|
415
|
-
for (const sub of this.subs.values()) sub.destroy();
|
|
416
|
-
this.subs.clear();
|
|
417
|
-
this.build();
|
|
418
|
-
const api = this.api;
|
|
419
|
-
for (const sub of this.subs.values()) {
|
|
420
|
-
api.setChild(sub.handle.service);
|
|
421
|
-
sub.api.setParent(this.handle.service);
|
|
422
|
-
}
|
|
423
|
-
this.handle.flush();
|
|
424
|
-
if (this.subs.size > 0) queueMicrotask(() => this.handle.flush());
|
|
425
|
-
}
|
|
426
|
-
destroy() {
|
|
427
|
-
for (const sub of this.subs.values()) sub.destroy();
|
|
428
|
-
this.subs.clear();
|
|
429
|
-
this.handle.stop();
|
|
430
|
-
this.positioner.remove();
|
|
431
|
-
}
|
|
432
|
-
byId(id) {
|
|
433
|
-
return this.items.find((i) => i.id === id);
|
|
434
|
-
}
|
|
435
|
-
/** Push the machine's item props onto the rendered rows. A branch takes the submenu
|
|
436
|
-
* trigger props, which carry the child's ids and hover handlers; a leaf takes plain ones. */
|
|
437
|
-
project(api) {
|
|
438
|
-
for (const li of this.list.children) {
|
|
439
|
-
const id = li.dataset.veiId;
|
|
440
|
-
if (!id) continue;
|
|
441
|
-
const sub = this.subs.get(id);
|
|
442
|
-
if (sub) {
|
|
443
|
-
spreadProps(li, api.getTriggerItemProps(sub.api), this.mid);
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const item = this.byId(id);
|
|
447
|
-
spreadProps(li, api.getItemProps({ value: id, disabled: item?.disabled, closeOnSelect: item?.toggle ? false : void 0 }), this.mid);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
build() {
|
|
451
|
-
const doc = this.doc;
|
|
452
|
-
this.list.replaceChildren();
|
|
453
|
-
for (const item of this.items) {
|
|
454
|
-
if (item.separatorBefore) {
|
|
455
|
-
const sep = doc.createElement("li");
|
|
456
|
-
sep.className = "vela-menu-sep";
|
|
457
|
-
sep.setAttribute("role", "separator");
|
|
458
|
-
this.list.appendChild(sep);
|
|
459
|
-
}
|
|
460
|
-
const branch = item.submenu !== void 0 && item.submenu.length > 0;
|
|
461
|
-
const li = doc.createElement("li");
|
|
462
|
-
li.className = "vela-menu-item";
|
|
463
|
-
li.dataset.veiId = item.id;
|
|
464
|
-
if (item.toggle) {
|
|
465
|
-
li.dataset.toggle = "1";
|
|
466
|
-
} else if (!branch && item.checked) {
|
|
467
|
-
li.dataset.checked = "1";
|
|
468
|
-
}
|
|
469
|
-
if (item.icon) li.appendChild(iconEl(item.icon, doc));
|
|
470
|
-
const label = doc.createElement("span");
|
|
471
|
-
label.className = "vela-menu-label";
|
|
472
|
-
label.textContent = item.label;
|
|
473
|
-
li.appendChild(label);
|
|
474
|
-
if (item.hint) {
|
|
475
|
-
const hint = doc.createElement("span");
|
|
476
|
-
hint.className = "vela-menu-hint";
|
|
477
|
-
hint.textContent = item.hint;
|
|
478
|
-
li.appendChild(hint);
|
|
479
|
-
}
|
|
480
|
-
if (item.favorite !== void 0 && this.onFavorite) {
|
|
481
|
-
const on = item.favorite;
|
|
482
|
-
const star = iconEl(on ? "star-filled" : "star", doc);
|
|
483
|
-
star.classList.add("vela-menu-star");
|
|
484
|
-
if (on) star.classList.add("vela-fav");
|
|
485
|
-
star.removeAttribute("aria-hidden");
|
|
486
|
-
star.setAttribute("role", "button");
|
|
487
|
-
star.setAttribute("aria-label", on ? "Remove from favorites" : "Add to favorites");
|
|
488
|
-
star.setAttribute("aria-pressed", on ? "true" : "false");
|
|
489
|
-
for (const ev of ["pointerdown", "pointerup", "mousedown", "mouseup"]) {
|
|
490
|
-
star.addEventListener(ev, (e) => e.stopPropagation());
|
|
491
|
-
}
|
|
492
|
-
star.addEventListener("click", (e) => {
|
|
493
|
-
e.stopPropagation();
|
|
494
|
-
e.preventDefault();
|
|
495
|
-
this.onFavorite?.(item.id, !on);
|
|
496
|
-
});
|
|
497
|
-
li.appendChild(star);
|
|
498
|
-
}
|
|
499
|
-
if (item.toggle) {
|
|
500
|
-
const sw = doc.createElement("span");
|
|
501
|
-
sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
|
|
502
|
-
sw.setAttribute("aria-hidden", "true");
|
|
503
|
-
li.appendChild(sw);
|
|
504
|
-
}
|
|
505
|
-
if (branch) {
|
|
506
|
-
li.dataset.branch = "1";
|
|
507
|
-
const arrow = iconEl("chevron-right", doc);
|
|
508
|
-
arrow.classList.add("vela-menu-arrow");
|
|
509
|
-
li.appendChild(arrow);
|
|
510
|
-
const sub = new _Surface(doc, {
|
|
511
|
-
host: this.host,
|
|
512
|
-
placement: "right-start",
|
|
513
|
-
onSelect: this.onSelect,
|
|
514
|
-
onFavorite: this.onFavorite,
|
|
515
|
-
id: `${this.mid}--${item.id}`
|
|
516
|
-
});
|
|
517
|
-
sub.setItems(item.submenu ?? []);
|
|
518
|
-
this.subs.set(item.id, sub);
|
|
519
|
-
}
|
|
520
|
-
this.list.appendChild(li);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
var Menu = class {
|
|
525
|
-
constructor(opts) {
|
|
526
|
-
const anchor = opts.trigger ?? opts.host ?? document.body;
|
|
527
|
-
const doc = anchor.ownerDocument;
|
|
528
|
-
injectStyles(MENU_STYLE_ID, MENU_CSS, doc);
|
|
529
|
-
const host = opts.host ?? anchor.closest?.(".vela-ui") ?? doc.body;
|
|
530
|
-
this.root = new Surface(doc, {
|
|
531
|
-
host,
|
|
532
|
-
placement: opts.placement,
|
|
533
|
-
onSelect: (id) => opts.onSelect?.(id),
|
|
534
|
-
onOpenChange: opts.onOpenChange,
|
|
535
|
-
trigger: opts.trigger,
|
|
536
|
-
triggerId: opts.triggerId,
|
|
537
|
-
id: opts.id,
|
|
538
|
-
minWidth: opts.minWidth,
|
|
539
|
-
onFavorite: opts.onFavorite
|
|
540
|
-
});
|
|
541
|
-
this.root.setItems(opts.items);
|
|
542
|
-
}
|
|
543
|
-
get api() {
|
|
544
|
-
return this.root.api;
|
|
545
|
-
}
|
|
546
|
-
open() {
|
|
547
|
-
this.api.setOpen(true);
|
|
548
|
-
}
|
|
549
|
-
/** Open anchored to a viewport point (context menus). */
|
|
550
|
-
openAt(clientX, clientY) {
|
|
551
|
-
const api = this.api;
|
|
552
|
-
api.setOpen(true);
|
|
553
|
-
api.reposition({ getAnchorRect: () => ({ x: clientX, y: clientY, width: 0, height: 0 }) });
|
|
554
|
-
}
|
|
555
|
-
close() {
|
|
556
|
-
this.api.setOpen(false);
|
|
557
|
-
}
|
|
558
|
-
/** Swap the item descriptors (e.g. checked states) and re-project. */
|
|
559
|
-
setItems(items) {
|
|
560
|
-
this.root.setItems(items);
|
|
561
|
-
}
|
|
562
|
-
destroy() {
|
|
563
|
-
this.root.destroy();
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
function dialogController(opts = {}) {
|
|
567
|
-
return {
|
|
568
|
-
machine: dialog.machine,
|
|
569
|
-
props: {
|
|
570
|
-
id: nextUid("vela-dialog"),
|
|
571
|
-
modal: opts.modal ?? true,
|
|
572
|
-
closeOnEscape: opts.closeOnEscape ?? true,
|
|
573
|
-
closeOnInteractOutside: opts.closeOnInteractOutside ?? false,
|
|
574
|
-
initialFocusEl: opts.initialFocusEl,
|
|
575
|
-
onOpenChange: (d) => opts.onOpenChange?.(d.open)
|
|
576
|
-
},
|
|
577
|
-
connect: (service) => dialog.connect(service, normalizeProps)
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// src/ui/components/dialog/styles.ts
|
|
582
|
-
var DIALOG_STYLE_ID = "vela-ui-dialog";
|
|
583
|
-
var DIALOG_CSS = `
|
|
584
|
-
.vela-dialog-backdrop {
|
|
585
|
-
position: fixed;
|
|
586
|
-
inset: 0;
|
|
587
|
-
background: var(--vela-backdrop);
|
|
588
|
-
z-index: var(--vela-z-dialog);
|
|
589
|
-
}
|
|
590
|
-
/* Non-dimming variant: still catches outside clicks, but the page stays readable. */
|
|
591
|
-
.vela-dialog-backdrop--clear { background: transparent; }
|
|
592
|
-
.vela-dialog-positioner {
|
|
593
|
-
position: fixed;
|
|
594
|
-
inset: 0;
|
|
595
|
-
display: flex;
|
|
596
|
-
align-items: flex-start;
|
|
597
|
-
justify-content: center;
|
|
598
|
-
padding-top: 10vh;
|
|
599
|
-
z-index: var(--vela-z-dialog);
|
|
600
|
-
}
|
|
601
|
-
.vela-dialog {
|
|
602
|
-
background: var(--vela-surface);
|
|
603
|
-
color: var(--vela-fg);
|
|
604
|
-
border: 1px solid var(--vela-border-strong);
|
|
605
|
-
border-radius: 10px;
|
|
606
|
-
box-shadow: var(--vela-shadow-dialog);
|
|
607
|
-
font-size: 13px;
|
|
608
|
-
min-width: 300px;
|
|
609
|
-
max-width: min(92vw, 560px);
|
|
610
|
-
max-height: 70vh;
|
|
611
|
-
display: flex;
|
|
612
|
-
flex-direction: column;
|
|
613
|
-
overflow: hidden;
|
|
614
|
-
outline: none;
|
|
615
|
-
}
|
|
616
|
-
.vela-dialog[data-state='open'] { animation: vela-dialog-in var(--vela-dur-med) var(--vela-ease); }
|
|
617
|
-
@keyframes vela-dialog-in {
|
|
618
|
-
from { opacity: 0; transform: translateY(6px) scale(0.98); }
|
|
619
|
-
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
620
|
-
}
|
|
621
|
-
.vela-dialog-header {
|
|
622
|
-
display: flex;
|
|
623
|
-
align-items: center;
|
|
624
|
-
padding: 9px 9px 9px 16px;
|
|
625
|
-
border-bottom: 1px solid var(--vela-border);
|
|
626
|
-
user-select: none;
|
|
627
|
-
}
|
|
628
|
-
.vela-dialog-title { flex: 1; font-size: 17px; font-weight: 600; letter-spacing: 0.2px; color: var(--vela-fg-bright); }
|
|
629
|
-
.vela-dialog-close {
|
|
630
|
-
all: unset;
|
|
631
|
-
cursor: pointer;
|
|
632
|
-
width: 30px;
|
|
633
|
-
height: 30px;
|
|
634
|
-
display: flex;
|
|
635
|
-
align-items: center;
|
|
636
|
-
justify-content: center;
|
|
637
|
-
border-radius: 4px;
|
|
638
|
-
color: var(--vela-fg-muted);
|
|
639
|
-
line-height: 1;
|
|
640
|
-
font-size: 15px;
|
|
641
|
-
}
|
|
642
|
-
.vela-dialog-close:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
643
|
-
.vela-dialog-body { padding: var(--vela-space-4); overflow: auto; }
|
|
644
|
-
.vela-dialog-body::-webkit-scrollbar { width: 8px; }
|
|
645
|
-
.vela-dialog-body::-webkit-scrollbar-thumb {
|
|
646
|
-
background: var(--vela-scroll);
|
|
647
|
-
border-radius: 4px;
|
|
648
|
-
border: 2px solid transparent;
|
|
649
|
-
background-clip: padding-box;
|
|
650
|
-
}
|
|
651
|
-
/* \u2500\u2500 mobile chrome: dialogs fill the shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
652
|
-
Inside a shell in the mobile size class ([data-layout='mobile'] on the widget root,
|
|
653
|
-
which is position:relative) every kit dialog presents fullscreen: desktop cards are
|
|
654
|
-
unusable at phone widths, and the shell's bounds \u2014 not the viewport \u2014 are the honest
|
|
655
|
-
"screen" for an embedded chart. */
|
|
656
|
-
[data-layout='mobile'] .vela-dialog-backdrop { position: absolute; }
|
|
657
|
-
[data-layout='mobile'] .vela-dialog-positioner {
|
|
658
|
-
position: absolute;
|
|
659
|
-
padding: 0;
|
|
660
|
-
align-items: stretch;
|
|
661
|
-
}
|
|
662
|
-
[data-layout='mobile'] .vela-dialog {
|
|
663
|
-
width: 100%;
|
|
664
|
-
min-width: 0;
|
|
665
|
-
max-width: none;
|
|
666
|
-
max-height: none;
|
|
667
|
-
flex: 1 1 auto;
|
|
668
|
-
border: none;
|
|
669
|
-
border-radius: 0;
|
|
670
|
-
transform: none !important; /* a desktop drag offset must not survive the flip */
|
|
671
|
-
}
|
|
672
|
-
[data-layout='mobile'] .vela-dialog-close { width: 40px; height: 40px; }
|
|
673
|
-
[data-layout='mobile'] .vela-dialog-body { padding-bottom: calc(var(--vela-space-4) + env(safe-area-inset-bottom, 0px)); }
|
|
674
|
-
`;
|
|
675
|
-
var Dialog = class {
|
|
676
|
-
constructor(opts = {}) {
|
|
677
|
-
const doc = (opts.host ?? document.body).ownerDocument;
|
|
678
|
-
injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
|
|
679
|
-
const host = opts.host ?? doc.body;
|
|
680
|
-
this.backdrop = doc.createElement("div");
|
|
681
|
-
this.backdrop.className = "vela-dialog-backdrop vela-ui-layer";
|
|
682
|
-
if (opts.dimBackdrop !== true) this.backdrop.classList.add("vela-dialog-backdrop--clear");
|
|
683
|
-
this.positioner = doc.createElement("div");
|
|
684
|
-
this.positioner.className = "vela-dialog-positioner vela-ui-layer";
|
|
685
|
-
this.panel = doc.createElement("div");
|
|
686
|
-
this.panel.className = "vela-dialog";
|
|
687
|
-
this.panel.tabIndex = -1;
|
|
688
|
-
const header = doc.createElement("div");
|
|
689
|
-
header.className = "vela-dialog-header";
|
|
690
|
-
if (opts.draggable) {
|
|
691
|
-
header.style.cursor = "move";
|
|
692
|
-
let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
|
|
693
|
-
header.addEventListener("pointerdown", (e) => {
|
|
694
|
-
if (e.target.closest(".vela-dialog-close")) return;
|
|
695
|
-
dragging = true;
|
|
696
|
-
sx = e.clientX - ox;
|
|
697
|
-
sy = e.clientY - oy;
|
|
698
|
-
header.setPointerCapture(e.pointerId);
|
|
699
|
-
});
|
|
700
|
-
header.addEventListener("pointermove", (e) => {
|
|
701
|
-
if (!dragging) return;
|
|
702
|
-
ox = e.clientX - sx;
|
|
703
|
-
oy = e.clientY - sy;
|
|
704
|
-
this.panel.style.transform = `translate(${ox}px, ${oy}px)`;
|
|
705
|
-
});
|
|
706
|
-
header.addEventListener("pointerup", () => dragging = false);
|
|
707
|
-
}
|
|
708
|
-
const title = doc.createElement("div");
|
|
709
|
-
title.className = "vela-dialog-title";
|
|
710
|
-
title.textContent = opts.title ?? "";
|
|
711
|
-
const close = doc.createElement("button");
|
|
712
|
-
close.className = "vela-dialog-close";
|
|
713
|
-
close.appendChild(iconEl("close", doc));
|
|
714
|
-
header.append(title, close);
|
|
715
|
-
this.body = doc.createElement("div");
|
|
716
|
-
this.body.className = "vela-dialog-body";
|
|
717
|
-
if (opts.content instanceof Node) this.body.appendChild(opts.content);
|
|
718
|
-
else if (typeof opts.content === "function") opts.content(this.body);
|
|
719
|
-
this.panel.append(header, this.body);
|
|
720
|
-
this.positioner.appendChild(this.panel);
|
|
721
|
-
host.append(this.backdrop, this.positioner);
|
|
722
|
-
this.ctrl = dialogController({
|
|
723
|
-
...opts,
|
|
724
|
-
// Mobile chrome (fullscreen presentation): opening must never land focus on
|
|
725
|
-
// an input — that pops the on-screen keyboard over the just-opened dialog.
|
|
726
|
-
// Focus goes to the panel; a caller that WANTS the keyboard focuses its
|
|
727
|
-
// input explicitly. Desktop keeps the caller's pick, else the machine's
|
|
728
|
-
// first-tabbable default.
|
|
729
|
-
initialFocusEl: () => {
|
|
730
|
-
if (this.positioner.closest('[data-layout="mobile"]')) return this.panel;
|
|
731
|
-
return opts.initialFocusEl?.() ?? null;
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
const mid = String(this.ctrl.props.id);
|
|
735
|
-
this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
|
|
736
|
-
const api = this.ctrl.connect(service);
|
|
737
|
-
spreadProps(this.backdrop, api.getBackdropProps(), mid);
|
|
738
|
-
spreadProps(this.positioner, api.getPositionerProps(), mid);
|
|
739
|
-
spreadProps(this.panel, api.getContentProps(), mid);
|
|
740
|
-
spreadProps(title, api.getTitleProps(), mid);
|
|
741
|
-
spreadProps(close, api.getCloseTriggerProps(), mid);
|
|
742
|
-
this.backdrop.style.display = api.open ? "" : "none";
|
|
743
|
-
this.positioner.style.display = api.open ? "" : "none";
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
get open() {
|
|
747
|
-
return this.ctrl.connect(this.handle.service).open;
|
|
748
|
-
}
|
|
749
|
-
show() {
|
|
750
|
-
this.ctrl.connect(this.handle.service).setOpen(true);
|
|
751
|
-
}
|
|
752
|
-
hide() {
|
|
753
|
-
this.ctrl.connect(this.handle.service).setOpen(false);
|
|
754
|
-
}
|
|
755
|
-
destroy() {
|
|
756
|
-
this.handle.stop();
|
|
757
|
-
this.backdrop.remove();
|
|
758
|
-
this.positioner.remove();
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
function drawerController(opts = {}) {
|
|
762
|
-
return {
|
|
763
|
-
machine: dialog.machine,
|
|
764
|
-
props: {
|
|
765
|
-
id: nextUid("vela-drawer"),
|
|
766
|
-
modal: true,
|
|
767
|
-
closeOnEscape: opts.closeOnEscape ?? true,
|
|
768
|
-
closeOnInteractOutside: opts.closeOnInteractOutside ?? true,
|
|
769
|
-
initialFocusEl: opts.initialFocusEl,
|
|
770
|
-
onOpenChange: (d) => opts.onOpenChange?.(d.open)
|
|
771
|
-
},
|
|
772
|
-
connect: (service) => dialog.connect(service, normalizeProps)
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// src/ui/components/drawer/styles.ts
|
|
777
|
-
var DRAWER_STYLE_ID = "vela-ui-drawer";
|
|
778
|
-
var DRAWER_CSS = `
|
|
779
|
-
.vela-drawer-backdrop {
|
|
780
|
-
position: fixed;
|
|
781
|
-
inset: 0;
|
|
782
|
-
background: var(--vela-backdrop);
|
|
783
|
-
z-index: var(--vela-z-dialog);
|
|
784
|
-
}
|
|
785
|
-
.vela-drawer-positioner {
|
|
786
|
-
position: fixed;
|
|
787
|
-
inset: 0;
|
|
788
|
-
display: flex;
|
|
789
|
-
align-items: flex-end;
|
|
790
|
-
justify-content: center;
|
|
791
|
-
z-index: var(--vela-z-dialog);
|
|
792
|
-
}
|
|
793
|
-
/* Inside a shell that declares a size class, the sheet scopes to the SHELL's bounds
|
|
794
|
-
(the widget root is position:relative) instead of the whole viewport \u2014 an embedded
|
|
795
|
-
chart must not curtain the host page. */
|
|
796
|
-
[data-layout] .vela-drawer-backdrop, [data-layout] .vela-drawer-positioner { position: absolute; }
|
|
797
|
-
.vela-drawer {
|
|
798
|
-
background: var(--vela-surface);
|
|
799
|
-
color: var(--vela-fg);
|
|
800
|
-
border: 1px solid var(--vela-border-strong);
|
|
801
|
-
border-bottom: none;
|
|
802
|
-
border-radius: 14px 14px 0 0;
|
|
803
|
-
box-shadow: var(--vela-shadow-dialog);
|
|
804
|
-
font-size: 13px;
|
|
805
|
-
width: 100%;
|
|
806
|
-
max-height: 85%;
|
|
807
|
-
display: flex;
|
|
808
|
-
flex-direction: column;
|
|
809
|
-
overflow: hidden;
|
|
810
|
-
outline: none;
|
|
811
|
-
}
|
|
812
|
-
.vela-drawer[data-state='open'] { animation: vela-drawer-in var(--vela-dur-med) var(--vela-ease); }
|
|
813
|
-
@keyframes vela-drawer-in {
|
|
814
|
-
from { transform: translateY(100%); }
|
|
815
|
-
to { transform: translateY(0); }
|
|
816
|
-
}
|
|
817
|
-
/* The grab zone owns its touches (drag-to-dismiss), so the browser must not scroll it. */
|
|
818
|
-
.vela-drawer-grab {
|
|
819
|
-
flex: none;
|
|
820
|
-
display: flex;
|
|
821
|
-
align-items: center;
|
|
822
|
-
justify-content: center;
|
|
823
|
-
padding: 10px 0 6px;
|
|
824
|
-
cursor: grab;
|
|
825
|
-
touch-action: none;
|
|
826
|
-
user-select: none;
|
|
827
|
-
}
|
|
828
|
-
.vela-drawer-grab::before {
|
|
829
|
-
content: '';
|
|
830
|
-
width: 36px;
|
|
831
|
-
height: 4px;
|
|
832
|
-
border-radius: 2px;
|
|
833
|
-
background: var(--vela-border-strong);
|
|
834
|
-
}
|
|
835
|
-
.vela-drawer-title {
|
|
836
|
-
flex: none;
|
|
837
|
-
padding: 0 16px 10px;
|
|
838
|
-
font-size: 15px;
|
|
839
|
-
font-weight: 600;
|
|
840
|
-
letter-spacing: 0.2px;
|
|
841
|
-
color: var(--vela-fg-bright);
|
|
842
|
-
user-select: none;
|
|
843
|
-
}
|
|
844
|
-
.vela-drawer-title:empty { display: none; }
|
|
845
|
-
.vela-drawer-body {
|
|
846
|
-
flex: 1 1 auto;
|
|
847
|
-
min-height: 0;
|
|
848
|
-
overflow-y: auto;
|
|
849
|
-
overscroll-behavior: contain;
|
|
850
|
-
-webkit-overflow-scrolling: touch;
|
|
851
|
-
/* Vertical pans stay native scrolling; horizontal moves reach the sheet's gesture
|
|
852
|
-
recognizer as pointer events (tab swipes). Without this the browser claims a
|
|
853
|
-
sideways touch as a scroll attempt and CANCELS the pointer stream, so swipes
|
|
854
|
-
never registered on real touch devices. Sideways-scrolling strips inside the
|
|
855
|
-
body opt back in with their own touch-action: pan-x. */
|
|
856
|
-
touch-action: pan-y;
|
|
857
|
-
padding: 0 var(--vela-space-3) calc(var(--vela-space-3) + env(safe-area-inset-bottom, 0px));
|
|
858
|
-
}
|
|
859
|
-
.vela-drawer-body::-webkit-scrollbar { width: 8px; }
|
|
860
|
-
.vela-drawer-body::-webkit-scrollbar-thumb {
|
|
861
|
-
background: var(--vela-scroll);
|
|
862
|
-
border-radius: 4px;
|
|
863
|
-
border: 2px solid transparent;
|
|
864
|
-
background-clip: padding-box;
|
|
865
|
-
}
|
|
866
|
-
`;
|
|
867
|
-
var DISMISS_FRACTION = 0.33;
|
|
868
|
-
var DISMISS_PX = 96;
|
|
869
|
-
var SLOP_PX = 8;
|
|
870
|
-
var HSWIPE_MIN_PX = 48;
|
|
871
|
-
function classifyGesture(dx, dy, ctx) {
|
|
872
|
-
if (Math.max(Math.abs(dx), Math.abs(dy)) < SLOP_PX) return "pending";
|
|
873
|
-
if (Math.abs(dy) > Math.abs(dx)) return dy > 0 && !ctx.scrolled ? "drag" : "scroll";
|
|
874
|
-
return ctx.canSwipe && !ctx.hScrollable ? "hswipe" : "scroll";
|
|
875
|
-
}
|
|
876
|
-
function dragDismisses(dy, panelHeightPx) {
|
|
877
|
-
return dy >= Math.min(DISMISS_PX, panelHeightPx * DISMISS_FRACTION);
|
|
878
|
-
}
|
|
879
|
-
function swipeDirection(dx, dy) {
|
|
880
|
-
if (Math.abs(dx) < HSWIPE_MIN_PX || Math.abs(dx) <= Math.abs(dy)) return null;
|
|
881
|
-
return dx < 0 ? "left" : "right";
|
|
882
|
-
}
|
|
883
|
-
var Drawer = class {
|
|
884
|
-
constructor(opts = {}) {
|
|
885
|
-
const doc = (opts.host ?? document.body).ownerDocument;
|
|
886
|
-
injectStyles(DRAWER_STYLE_ID, DRAWER_CSS, doc);
|
|
887
|
-
const host = opts.host ?? doc.body;
|
|
888
|
-
this.backdrop = doc.createElement("div");
|
|
889
|
-
this.backdrop.className = "vela-drawer-backdrop vela-ui-layer";
|
|
890
|
-
this.positioner = doc.createElement("div");
|
|
891
|
-
this.positioner.className = "vela-drawer-positioner vela-ui-layer";
|
|
892
|
-
this.panel = doc.createElement("div");
|
|
893
|
-
this.panel.className = "vela-drawer";
|
|
894
|
-
this.panel.tabIndex = -1;
|
|
895
|
-
const grab = doc.createElement("div");
|
|
896
|
-
grab.className = "vela-drawer-grab";
|
|
897
|
-
this.titleEl = doc.createElement("div");
|
|
898
|
-
this.titleEl.className = "vela-drawer-title";
|
|
899
|
-
this.titleEl.textContent = opts.title ?? "";
|
|
900
|
-
this.body = doc.createElement("div");
|
|
901
|
-
this.body.className = "vela-drawer-body";
|
|
902
|
-
if (opts.content instanceof Node) this.body.appendChild(opts.content);
|
|
903
|
-
else if (typeof opts.content === "function") opts.content(this.body);
|
|
904
|
-
this.panel.append(grab, this.titleEl, this.body);
|
|
905
|
-
this.positioner.appendChild(this.panel);
|
|
906
|
-
host.append(this.backdrop, this.positioner);
|
|
907
|
-
this.wireGestures(grab, opts.onSwipe);
|
|
908
|
-
this.ctrl = drawerController({ ...opts, initialFocusEl: () => this.panel });
|
|
909
|
-
const mid = String(this.ctrl.props.id);
|
|
910
|
-
this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
|
|
911
|
-
const api = this.ctrl.connect(service);
|
|
912
|
-
spreadProps(this.backdrop, api.getBackdropProps(), mid);
|
|
913
|
-
spreadProps(this.positioner, api.getPositionerProps(), mid);
|
|
914
|
-
spreadProps(this.panel, api.getContentProps(), mid);
|
|
915
|
-
spreadProps(this.titleEl, api.getTitleProps(), mid);
|
|
916
|
-
this.backdrop.style.display = api.open ? "" : "none";
|
|
917
|
-
this.positioner.style.display = api.open ? "" : "none";
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
/** Any element between `from` and the panel that has already been scrolled down —
|
|
921
|
-
* a downward pull there must scroll it back up, never drag the sheet. */
|
|
922
|
-
scrolledAncestor(from) {
|
|
923
|
-
let el = from instanceof Element ? from : null;
|
|
924
|
-
while (el && el !== this.panel) {
|
|
925
|
-
if (el.scrollTop > 0) return true;
|
|
926
|
-
el = el.parentElement;
|
|
927
|
-
}
|
|
928
|
-
return false;
|
|
929
|
-
}
|
|
930
|
-
/** Any element between `from` and the panel that scrolls horizontally on its own
|
|
931
|
-
* (the tab strip, chip rows) — a sideways move there is ITS scroll, not a swipe. */
|
|
932
|
-
hScrollableAncestor(from) {
|
|
933
|
-
let el = from instanceof Element ? from : null;
|
|
934
|
-
while (el && el !== this.panel) {
|
|
935
|
-
if (el.scrollWidth > el.clientWidth + 1) return true;
|
|
936
|
-
el = el.parentElement;
|
|
937
|
-
}
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* One gesture recognizer for the whole sheet. A downward pull dismisses from
|
|
942
|
-
* anywhere — the grab handle immediately, the content once it is decidedly vertical
|
|
943
|
-
* and its scroller is at rest (a scrolled list keeps native scrolling). A decidedly
|
|
944
|
-
* horizontal move becomes an `onSwipe` (tabbed drawers flip pages with it). The
|
|
945
|
-
* non-passive touchmove hook is what keeps the browser from claiming the pull as a
|
|
946
|
-
* scroll once the sheet is (or may become) the drag target.
|
|
947
|
-
*/
|
|
948
|
-
wireGestures(grab, onSwipe) {
|
|
949
|
-
let startX = 0;
|
|
950
|
-
let startY = 0;
|
|
951
|
-
let dx = 0;
|
|
952
|
-
let dy = 0;
|
|
953
|
-
let mode = "idle";
|
|
954
|
-
const beginDrag = (e) => {
|
|
955
|
-
mode = "drag";
|
|
956
|
-
startY = e.clientY;
|
|
957
|
-
this.panel.style.transition = "none";
|
|
958
|
-
try {
|
|
959
|
-
this.panel.setPointerCapture(e.pointerId);
|
|
960
|
-
} catch {
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
this.panel.addEventListener("pointerdown", (e) => {
|
|
964
|
-
if (e.isPrimary === false) return;
|
|
965
|
-
startX = e.clientX;
|
|
966
|
-
startY = e.clientY;
|
|
967
|
-
dx = 0;
|
|
968
|
-
dy = 0;
|
|
969
|
-
if (grab.contains(e.target)) beginDrag(e);
|
|
970
|
-
else mode = "pending";
|
|
971
|
-
});
|
|
972
|
-
this.panel.addEventListener("pointermove", (e) => {
|
|
973
|
-
if (mode === "idle" || mode === "scroll") return;
|
|
974
|
-
dx = e.clientX - startX;
|
|
975
|
-
dy = e.clientY - startY;
|
|
976
|
-
if (mode === "pending") {
|
|
977
|
-
const intent = classifyGesture(dx, dy, {
|
|
978
|
-
canSwipe: !!onSwipe,
|
|
979
|
-
scrolled: this.scrolledAncestor(e.target),
|
|
980
|
-
hScrollable: this.hScrollableAncestor(e.target)
|
|
981
|
-
});
|
|
982
|
-
if (intent === "pending") return;
|
|
983
|
-
if (intent === "drag") beginDrag(e);
|
|
984
|
-
else mode = intent;
|
|
985
|
-
}
|
|
986
|
-
if (mode === "drag") {
|
|
987
|
-
dy = Math.max(0, e.clientY - startY);
|
|
988
|
-
this.panel.style.transform = dy > 0 ? `translateY(${dy}px)` : "";
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
this.panel.addEventListener(
|
|
992
|
-
"touchmove",
|
|
993
|
-
(e) => {
|
|
994
|
-
if (mode === "drag" || mode === "hswipe") {
|
|
995
|
-
e.preventDefault();
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
if (mode !== "pending") return;
|
|
999
|
-
const t = e.touches[0];
|
|
1000
|
-
if (!t) return;
|
|
1001
|
-
const mdx = t.clientX - startX;
|
|
1002
|
-
const mdy = t.clientY - startY;
|
|
1003
|
-
if (mdy > Math.abs(mdx) && !this.scrolledAncestor(e.target)) e.preventDefault();
|
|
1004
|
-
},
|
|
1005
|
-
{ passive: false }
|
|
1006
|
-
);
|
|
1007
|
-
const settle = () => {
|
|
1008
|
-
if (mode === "idle") return;
|
|
1009
|
-
const finished = mode;
|
|
1010
|
-
mode = "idle";
|
|
1011
|
-
if (finished === "drag") {
|
|
1012
|
-
this.panel.style.transition = "";
|
|
1013
|
-
this.panel.style.transform = "";
|
|
1014
|
-
if (dragDismisses(dy, this.panel.getBoundingClientRect().height)) this.hide();
|
|
1015
|
-
} else if (finished === "hswipe") {
|
|
1016
|
-
const dir = swipeDirection(dx, dy);
|
|
1017
|
-
if (dir) onSwipe?.(dir);
|
|
1018
|
-
}
|
|
1019
|
-
};
|
|
1020
|
-
this.panel.addEventListener("pointerup", settle);
|
|
1021
|
-
this.panel.addEventListener("pointercancel", settle);
|
|
1022
|
-
}
|
|
1023
|
-
setTitle(title) {
|
|
1024
|
-
this.titleEl.textContent = title;
|
|
1025
|
-
}
|
|
1026
|
-
get open() {
|
|
1027
|
-
return this.ctrl.connect(this.handle.service).open;
|
|
1028
|
-
}
|
|
1029
|
-
show() {
|
|
1030
|
-
this.ctrl.connect(this.handle.service).setOpen(true);
|
|
1031
|
-
}
|
|
1032
|
-
hide() {
|
|
1033
|
-
this.ctrl.connect(this.handle.service).setOpen(false);
|
|
1034
|
-
}
|
|
1035
|
-
destroy() {
|
|
1036
|
-
this.handle.stop();
|
|
1037
|
-
this.backdrop.remove();
|
|
1038
|
-
this.positioner.remove();
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
export { Dialog, Drawer, KeymapManager, Menu, Tooltip, dialogController, drawerController, isEditableTarget, menuController, nextUid, runMachine, tooltipController };
|