vireuikit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web.cjs ADDED
@@ -0,0 +1,1892 @@
1
+ 'use strict';
2
+
3
+ var vireglass = require('vireglass');
4
+
5
+ // src/web/element.ts
6
+
7
+ // src/kit/a11y.ts
8
+ var A11Y_NONE = {
9
+ reduceTransparency: false,
10
+ increaseContrast: false,
11
+ reduceMotion: false
12
+ };
13
+ function resolveA11y(system, override = {}) {
14
+ return {
15
+ reduceTransparency: override.reduceTransparency ?? system.reduceTransparency,
16
+ increaseContrast: override.increaseContrast ?? system.increaseContrast,
17
+ reduceMotion: override.reduceMotion ?? system.reduceMotion
18
+ };
19
+ }
20
+ function presentationFor(mode, wantsGlass) {
21
+ return {
22
+ glass: wantsGlass,
23
+ frost: wantsGlass && mode.reduceTransparency,
24
+ surface: wantsGlass ? "glass" : "plain",
25
+ contrastBorder: mode.increaseContrast,
26
+ instantSprings: mode.reduceMotion
27
+ };
28
+ }
29
+ function sameA11y(a, b) {
30
+ return a.reduceTransparency === b.reduceTransparency && a.increaseContrast === b.increaseContrast && a.reduceMotion === b.reduceMotion;
31
+ }
32
+ var ATOM_AT_REST = {
33
+ pressed: 0,
34
+ hovered: 0,
35
+ selected: 0,
36
+ disabled: false
37
+ };
38
+ function atomLift(kind) {
39
+ switch (kind) {
40
+ case "iconButton":
41
+ case "switch":
42
+ case "slider":
43
+ return 1;
44
+ default:
45
+ return 0;
46
+ }
47
+ }
48
+ var DISABLED_PRESENCE = 0.25;
49
+ var HOVER_GAIN = 0.35;
50
+ function activation(state) {
51
+ if (state.disabled) return 0;
52
+ const clamp2 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
53
+ return Math.max(clamp2(state.selected), clamp2(state.pressed), clamp2(state.hovered) * HOVER_GAIN);
54
+ }
55
+ function atomMaterial(base, kind, state = ATOM_AT_REST) {
56
+ const out = vireglass.activeMaterial(base, activation(state));
57
+ if (!state.disabled) return out;
58
+ return { ...out, presence: out.presence * DISABLED_PRESENCE };
59
+ }
60
+ var REST_BODY = { alpha: 0.25, white: 0.15, lift: 0.1 };
61
+ var PRESSED_BODY = { alpha: 0.34, white: 0.35, lift: 0 };
62
+ var FROST_LIFT = 0.26;
63
+ function atomBody(base, state = ATOM_AT_REST, frost = false) {
64
+ const on = activation(state);
65
+ const mix2 = (key) => REST_BODY[key] + (PRESSED_BODY[key] - REST_BODY[key]) * on;
66
+ let alpha = mix2("alpha");
67
+ let white = mix2("white");
68
+ let lift = mix2("lift");
69
+ if (state.disabled) alpha *= DISABLED_PRESENCE;
70
+ if (frost) {
71
+ const density = vireglass.applyAccessibility(vireglass.resolveOptics(base), {
72
+ reduceTransparency: true,
73
+ increaseContrast: false,
74
+ reduceMotion: false
75
+ }).bodyDensity;
76
+ alpha = Math.max(alpha, density);
77
+ white = 0;
78
+ lift = FROST_LIFT;
79
+ }
80
+ return { alpha, white, lift };
81
+ }
82
+
83
+ // src/kit/behavior/focus.ts
84
+ function stepFocus(event) {
85
+ return event.type === "focus" && event.visible;
86
+ }
87
+
88
+ // src/kit/behavior/hover.ts
89
+ function stepHover(event, canHover) {
90
+ return canHover && event.type === "enter";
91
+ }
92
+
93
+ // src/kit/behavior/press.ts
94
+ var PRESS_IDLE = { by: null };
95
+ function stepPress(state, event, enabled2) {
96
+ switch (event.type) {
97
+ case "down":
98
+ if (!enabled2 || state.by !== null) return { state, activated: false };
99
+ return { state: { by: event.source }, activated: false };
100
+ case "up":
101
+ if (state.by !== event.source) return { state, activated: false };
102
+ return { state: PRESS_IDLE, activated: enabled2 };
103
+ case "cancel":
104
+ return { state: PRESS_IDLE, activated: false };
105
+ case "leave":
106
+ return state.by === "pointer" ? { state: PRESS_IDLE, activated: false } : { state, activated: false };
107
+ }
108
+ }
109
+
110
+ // src/kit/behavior/control.ts
111
+ var TOUCH_INPUT = { hover: false };
112
+ var POINTER_INPUT = { hover: true };
113
+ var CONTROL_IDLE = {
114
+ press: PRESS_IDLE,
115
+ hovered: false,
116
+ focused: false,
117
+ disabled: false
118
+ };
119
+ function stepControl(state, event, caps) {
120
+ switch (event.type) {
121
+ case "down":
122
+ case "up":
123
+ case "cancel": {
124
+ const next = stepPress(state.press, event, !state.disabled);
125
+ return { state: { ...state, press: next.state }, activated: next.activated };
126
+ }
127
+ case "leave": {
128
+ const next = stepPress(state.press, event, !state.disabled);
129
+ return { state: { ...state, press: next.state, hovered: false }, activated: false };
130
+ }
131
+ case "enter":
132
+ return {
133
+ state: { ...state, hovered: !state.disabled && stepHover(event, caps.hover) },
134
+ activated: false
135
+ };
136
+ case "focus":
137
+ case "blur": {
138
+ const press = event.type === "blur" && state.press.by === "keyboard" ? PRESS_IDLE : state.press;
139
+ return {
140
+ state: { ...state, press, focused: !state.disabled && stepFocus(event) },
141
+ activated: false
142
+ };
143
+ }
144
+ case "disable":
145
+ return {
146
+ state: event.disabled ? { press: PRESS_IDLE, hovered: false, focused: false, disabled: true } : { ...state, disabled: false },
147
+ activated: false
148
+ };
149
+ }
150
+ }
151
+ function controlTargets(state) {
152
+ return {
153
+ pressed: state.press.by !== null ? 1 : 0,
154
+ hovered: state.hovered ? 1 : 0,
155
+ focus: state.focused ? 1 : 0,
156
+ disabled: state.disabled
157
+ };
158
+ }
159
+ function stepSpring(s, dt, response, damping, instant = false) {
160
+ if (instant) {
161
+ const moved = s.x !== s.target || s.v !== 0;
162
+ s.x = s.target;
163
+ s.v = 0;
164
+ return moved;
165
+ }
166
+ const stiffness = (2 * Math.PI / response) ** 2;
167
+ const friction = 4 * Math.PI * damping / response;
168
+ const h = 1 / 240;
169
+ for (let left = Math.min(dt, 0.05); left > 0; left -= h) {
170
+ const step = Math.min(left, h);
171
+ s.v += (stiffness * (s.target - s.x) - friction * s.v) * step;
172
+ s.x += s.v * step;
173
+ }
174
+ if (Math.abs(s.target - s.x) < 1e-3 && Math.abs(s.v) < 0.01) {
175
+ s.x = s.target;
176
+ s.v = 0;
177
+ return false;
178
+ }
179
+ return true;
180
+ }
181
+ var smoothstep = (e0, e1, x) => {
182
+ const t = Math.min(Math.max((x - e0) / (e1 - e0), 0), 1);
183
+ return t * t * (3 - 2 * t);
184
+ };
185
+ var mix = (a, b, t) => a + (b - a) * t;
186
+ var DROP_DIAMETER = 1.1;
187
+ var DROP_LIFT = 0.5;
188
+ var NECK_GAIN = 0.3;
189
+ var NECK_RAMP = 4;
190
+ var CORNER_EASE_START = 0.55;
191
+ var INK_OUT_END = 0.4;
192
+ var INK_IN_START = 0.9;
193
+ var INK_IN_END = 1;
194
+ function growthInk(phase) {
195
+ const a = Math.min(Math.max(phase.shrink, 0), 1);
196
+ const b = Math.max(phase.grow, 0);
197
+ return {
198
+ outgoing: 1 - smoothstep(0, INK_OUT_END, a),
199
+ incoming: smoothstep(INK_IN_START, INK_IN_END, b)
200
+ };
201
+ }
202
+ function largerOf(a, b) {
203
+ return vireglass.halfMinDp(a.geometry) >= vireglass.halfMinDp(b.geometry) ? { main: a, other: b } : { main: b, other: a };
204
+ }
205
+ function growthFrame(source, target, anchor, phase) {
206
+ const dx = target.center.x - source.center.x;
207
+ const dy = target.center.y - source.center.y;
208
+ const len = Math.hypot(dx, dy) || 1;
209
+ const sourceSize = vireglass.halfMinDp(source.geometry) * 2;
210
+ const lift = DROP_LIFT * sourceSize;
211
+ const drop = {
212
+ x: source.center.x + dx / len * lift,
213
+ y: source.center.y + dy / len * lift
214
+ };
215
+ const a = Math.min(Math.max(phase.shrink, 0), 1);
216
+ const b = Math.max(phase.grow, 0);
217
+ const keep = Math.max(1 - b, 0);
218
+ const d = DROP_DIAMETER * sourceSize;
219
+ const shrunkGeometry = {
220
+ width: mix(source.geometry.width, 0.6 * d, a) * keep,
221
+ height: mix(source.geometry.height, 0.6 * d, a) * keep,
222
+ cornerRadius: 0
223
+ };
224
+ shrunkGeometry.cornerRadius = Math.min(
225
+ mix(source.geometry.cornerRadius, vireglass.halfMinDp(shrunkGeometry), a),
226
+ vireglass.halfMinDp(shrunkGeometry)
227
+ );
228
+ const shrunk = {
229
+ geometry: shrunkGeometry,
230
+ center: { x: mix(source.center.x, drop.x, a), y: mix(source.center.y, drop.y, a) }
231
+ };
232
+ const seed = d * Math.sqrt(a);
233
+ const cornerX = Math.sign(anchor.x - source.center.x);
234
+ const cornerY = Math.sign(anchor.y - source.center.y);
235
+ const edgeX = mix(drop.x + cornerX * (seed / 2), anchor.x, Math.min(b, 1));
236
+ const edgeY = mix(drop.y + cornerY * (seed / 2), anchor.y, Math.min(b, 1));
237
+ const width = mix(seed, target.geometry.width, b);
238
+ const height = mix(seed, target.geometry.height, b);
239
+ const grownGeometry = { width, height, cornerRadius: 0 };
240
+ grownGeometry.cornerRadius = Math.min(
241
+ mix(vireglass.halfMinDp(grownGeometry), target.geometry.cornerRadius, smoothstep(CORNER_EASE_START, 1, b)),
242
+ vireglass.halfMinDp(grownGeometry)
243
+ );
244
+ const grown = {
245
+ geometry: grownGeometry,
246
+ center: { x: edgeX - cornerX * (width / 2), y: edgeY - cornerY * (height / 2) }
247
+ };
248
+ const { main, other } = largerOf(grown, shrunk);
249
+ const neck = NECK_GAIN * sourceSize * Math.min(a * NECK_RAMP, 1) * keep;
250
+ const united = neck > 0.01 && other.geometry.width > 0.5 && other.geometry.height > 0.5;
251
+ return {
252
+ body: main,
253
+ grown,
254
+ merge: united ? {
255
+ offsetX: other.center.x - main.center.x,
256
+ offsetY: other.center.y - main.center.y,
257
+ width: other.geometry.width,
258
+ height: other.geometry.height,
259
+ cornerRadius: other.geometry.cornerRadius,
260
+ smoothing: neck
261
+ } : void 0,
262
+ ink: growthInk(phase)
263
+ };
264
+ }
265
+ var createGrowthState = () => ({
266
+ shrink: { x: 0, v: 0, target: 0 },
267
+ grow: { x: 0, v: 0, target: 0 }
268
+ });
269
+ var growthPhase = (state) => ({
270
+ shrink: state.shrink.x,
271
+ grow: state.grow.x
272
+ });
273
+ var DEFAULT_GROWTH_TIMING = {
274
+ shrinkOpen: 0.21,
275
+ shrinkClose: 0.14,
276
+ growOpen: 0.38,
277
+ growClose: 0.4,
278
+ growDampingOpen: 0.72
279
+ };
280
+ var GROW_DAMPING_CLOSE = 1;
281
+ var SHRINK_DAMPING = 1;
282
+ var GROW_STARTS_AT = 0.8;
283
+ var SHRINK_RETURNS_AT = 0.12;
284
+ function stepGrowth(state, dt, open, reduceMotion = false, timing = DEFAULT_GROWTH_TIMING) {
285
+ const { shrink, grow } = state;
286
+ if (reduceMotion) {
287
+ shrink.target = open ? 1 : 0;
288
+ grow.target = open ? 1 : 0;
289
+ } else if (open) {
290
+ shrink.target = 1;
291
+ if (shrink.x > GROW_STARTS_AT) grow.target = 1;
292
+ } else {
293
+ grow.target = 0;
294
+ if (grow.x < SHRINK_RETURNS_AT) shrink.target = 0;
295
+ }
296
+ const movedShrink = stepSpring(
297
+ shrink,
298
+ dt,
299
+ open ? timing.shrinkOpen : timing.shrinkClose,
300
+ SHRINK_DAMPING,
301
+ reduceMotion
302
+ );
303
+ const movedGrow = stepSpring(
304
+ grow,
305
+ dt,
306
+ open ? timing.growOpen : timing.growClose,
307
+ open ? timing.growDampingOpen : GROW_DAMPING_CLOSE,
308
+ reduceMotion
309
+ );
310
+ return movedShrink || movedGrow;
311
+ }
312
+
313
+ // src/kit/behavior/visual.ts
314
+ var createControlVisual = () => ({
315
+ pressed: { x: 0, v: 0, target: 0 },
316
+ hovered: { x: 0, v: 0, target: 0 },
317
+ focus: { x: 0, v: 0, target: 0 }
318
+ });
319
+ var PRESS = { response: 0.18, damping: 0.78 };
320
+ var HOVER = { response: 0.22, damping: 1 };
321
+ var FOCUS = { response: 0.16, damping: 1 };
322
+ function stepControlVisual(visual, dt, state, instant = false) {
323
+ const t = controlTargets(state);
324
+ visual.pressed.target = t.pressed;
325
+ visual.hovered.target = t.hovered;
326
+ visual.focus.target = t.focus;
327
+ const a = stepSpring(visual.pressed, dt, PRESS.response, PRESS.damping, instant);
328
+ const b = stepSpring(visual.hovered, dt, HOVER.response, HOVER.damping, instant);
329
+ const c = stepSpring(visual.focus, dt, FOCUS.response, FOCUS.damping, instant);
330
+ return a || b || c;
331
+ }
332
+ function visualAtomState(visual, state) {
333
+ return {
334
+ pressed: visual.pressed.x,
335
+ hovered: visual.hovered.x,
336
+ selected: 0,
337
+ disabled: state.disabled
338
+ };
339
+ }
340
+ function settleControlVisual(visual, state) {
341
+ stepControlVisual(visual, 0, state, true);
342
+ }
343
+
344
+ // src/kit/response.ts
345
+ var LIFTING = { grow: 0.18, defocus: 1.6 };
346
+ var SINKING = { grow: 0.06, defocus: 1 };
347
+ function pressResponse(kind) {
348
+ return atomLift(kind) === 1 ? LIFTING : SINKING;
349
+ }
350
+ var CONTROL_SIZES = ["touch", "small", "medium", "large", "xlarge"];
351
+ var MIN_TOUCH_TARGET_DP = 44;
352
+ var MIN_POINTER_TARGET_DP = 24;
353
+ var capsule = (height, paddingX) => ({
354
+ height,
355
+ paddingX,
356
+ icon: Math.round(height / 2),
357
+ radius: height / 2,
358
+ shape: "capsule"
359
+ });
360
+ var rounded = (height, paddingX) => ({
361
+ height,
362
+ paddingX,
363
+ icon: Math.round(height / 2),
364
+ radius: height / 4,
365
+ shape: "rounded"
366
+ });
367
+ var BUTTON_SIZES = {
368
+ touch: capsule(44, 20),
369
+ small: rounded(24, 10),
370
+ medium: rounded(32, 14),
371
+ large: capsule(40, 18),
372
+ xlarge: capsule(48, 22)
373
+ };
374
+ function iconButtonSize(size) {
375
+ const spec = BUTTON_SIZES[size];
376
+ return { ...spec, width: spec.height, paddingX: 0 };
377
+ }
378
+ var MENU_PADDING = 0.3;
379
+ function menuSize(size) {
380
+ const spec = BUTTON_SIZES[size];
381
+ const padding = Math.round(spec.height * MENU_PADDING);
382
+ return {
383
+ row: spec.height,
384
+ padding,
385
+ itemRadius: spec.radius,
386
+ radius: spec.radius + padding,
387
+ icon: spec.icon,
388
+ labelPad: spec.paddingX
389
+ };
390
+ }
391
+ function defaultSize(input) {
392
+ return input.touch ? "touch" : "medium";
393
+ }
394
+ function hitSlop(bodyDp, minTargetDp) {
395
+ return bodyDp >= minTargetDp ? 0 : (minTargetDp - bodyDp) / 2;
396
+ }
397
+ function minTarget(input) {
398
+ return input.touch ? MIN_TOUCH_TARGET_DP : MIN_POINTER_TARGET_DP;
399
+ }
400
+
401
+ // src/web/a11y.ts
402
+ var QUERIES = {
403
+ reduceMotion: "(prefers-reduced-motion: reduce)",
404
+ increaseContrast: "(prefers-contrast: more)",
405
+ reduceTransparency: "(prefers-reduced-transparency: reduce)"
406
+ };
407
+ var KEYS = Object.keys(QUERIES);
408
+ function systemA11y() {
409
+ if (typeof matchMedia === "undefined") return A11Y_NONE;
410
+ return {
411
+ reduceMotion: matchMedia(QUERIES.reduceMotion).matches,
412
+ increaseContrast: matchMedia(QUERIES.increaseContrast).matches,
413
+ reduceTransparency: matchMedia(QUERIES.reduceTransparency).matches
414
+ };
415
+ }
416
+ function watchSystemA11y(onChange) {
417
+ if (typeof matchMedia === "undefined") return () => {
418
+ };
419
+ const lists = KEYS.map((key) => matchMedia(QUERIES[key]));
420
+ for (const list of lists) list.addEventListener("change", onChange);
421
+ return () => {
422
+ for (const list of lists) list.removeEventListener("change", onChange);
423
+ };
424
+ }
425
+ var globalOverride = {};
426
+ var listeners = /* @__PURE__ */ new Set();
427
+ function setA11yOverride(next) {
428
+ globalOverride = { ...globalOverride, ...next };
429
+ for (const listener of listeners) listener();
430
+ }
431
+ function getA11yOverride() {
432
+ return globalOverride;
433
+ }
434
+ function watchA11yOverride(onChange) {
435
+ listeners.add(onChange);
436
+ return () => {
437
+ listeners.delete(onChange);
438
+ };
439
+ }
440
+ function effectiveA11y(own) {
441
+ const merged = { ...globalOverride };
442
+ for (const key of KEYS) if (own[key] !== void 0) merged[key] = own[key];
443
+ return resolveA11y(systemA11y(), merged);
444
+ }
445
+
446
+ // src/web/glass.ts
447
+ async function attachControlGlass(el, params, stillWanted) {
448
+ const { attachGlass, probeBackdrop } = await import('vireglass/dom');
449
+ if (!stillWanted()) return null;
450
+ const opts = {
451
+ material: params.material,
452
+ geometry: params.geometry,
453
+ accessibility: params.accessibility,
454
+ variant: params.onTouch ? "interactive" : "present",
455
+ // The kit paints its own body: the core's is not lighter than its surround over a light backdrop and
456
+ // near black under reduced transparency with light ink (docs/acceptance.md, "Resting body").
457
+ body: false
458
+ };
459
+ if (params.interactive === false) opts.interactive = false;
460
+ if (params.sample) opts.sample = params.sample;
461
+ const handle = attachGlass(el, opts);
462
+ return {
463
+ setMaterial(material) {
464
+ opts.material = material;
465
+ handle.update();
466
+ },
467
+ setGeometry(geometry) {
468
+ opts.geometry = geometry;
469
+ handle.update();
470
+ },
471
+ frame(next) {
472
+ if (next.material) opts.material = next.material;
473
+ if (next.geometry) opts.geometry = next.geometry;
474
+ if (next.morph !== void 0) handle.setMorph(next.morph);
475
+ else handle.update();
476
+ },
477
+ setAppear: (t) => handle.setAppear(t),
478
+ setMorph: (morph) => handle.setMorph(morph),
479
+ setSample: (sample) => handle.setSample(sample),
480
+ probe: () => probeBackdrop(el),
481
+ destroy: () => handle.destroy()
482
+ };
483
+ }
484
+
485
+ // src/web/input.ts
486
+ var COARSE = "(pointer: coarse)";
487
+ function isCoarsePointer() {
488
+ return typeof matchMedia !== "undefined" && matchMedia(COARSE).matches;
489
+ }
490
+ function watchCoarsePointer(onChange) {
491
+ if (typeof matchMedia === "undefined") return () => {
492
+ };
493
+ const list = matchMedia(COARSE);
494
+ list.addEventListener("change", onChange);
495
+ return () => list.removeEventListener("change", onChange);
496
+ }
497
+
498
+ // src/web/styles.ts
499
+ var HOST = ":is(vire-button,vire-icon-button)[data-vk]";
500
+ var BODY = `${HOST}>button`;
501
+ var MENU = "vire-menu[data-vk]";
502
+ var SURFACE = `${MENU}>[popover]`;
503
+ var ITEM = `${SURFACE}>button`;
504
+ var GLASS_BODY = `background:color-mix(in srgb,color-mix(in srgb,#fff calc((var(--vk-body-white) + var(--vk-body-lift)*2)*100%),var(--vireglass-tint-color,#fff)) calc(var(--vk-body-alpha)*100%),transparent);
505
+ background:oklab(from color-mix(in srgb,#fff calc(var(--vk-body-white)*100%),var(--vireglass-tint-color,#fff)) calc(l + var(--vk-body-lift)) a b / var(--vk-body-alpha))`;
506
+ var FROST_INK = "color:oklab(from var(--vireglass-tint-color,#fff) calc(.95 - .87*clamp(0,(l + var(--vk-body-lift) - .6)*20,1)) 0 0)";
507
+ var CONTRAST_GLASS = "background:color-mix(in srgb,color-mix(in srgb,#fff calc((1 - var(--vireglass-ink,0))*100%),#000) 88%,transparent)";
508
+ var SHADOW = "box-shadow:0 0 0 1px color-mix(in srgb,currentColor 14%,transparent),0 10px 30px color-mix(in srgb,currentColor 18%,transparent)";
509
+ var STYLES = `
510
+ vire-button,vire-icon-button,vire-menu{display:inline-flex;vertical-align:middle}
511
+ ${HOST}{position:relative;--vk-press:0;--vk-hover:0;--vk-carry:1;--vk-body-alpha:0;--vk-body-white:0;--vk-body-lift:0;--vk-grow:0;--vk-defocus:0;--vk-slop-x:0;--vk-slop-y:0}
512
+ ${HOST}::before{content:'';position:absolute;inset:calc(var(--vk-slop-y)*-1px) calc(var(--vk-slop-x)*-1px)}
513
+ ${BODY}{appearance:none;-webkit-appearance:none;box-sizing:border-box;margin:0;border:0;outline:none;
514
+ font:inherit;font-weight:500;line-height:1;white-space:nowrap;color:inherit;
515
+ display:inline-flex;align-items:center;justify-content:center;gap:8px;position:relative;
516
+ height:var(--vk-height);min-width:var(--vk-height);padding:0 var(--vk-pad-x);border-radius:var(--vk-radius);
517
+ -webkit-text-fill-color:color-mix(in srgb,currentColor calc((1 - var(--vk-press)*.55)*var(--vk-carry)*100%),transparent);
518
+ text-shadow:0 0 calc((var(--vk-press)*var(--vk-defocus) + (1 - var(--vk-carry))*4)*1px) color-mix(in srgb,currentColor calc(var(--vk-carry)*100%),transparent);
519
+ background:transparent;cursor:pointer;touch-action:manipulation;user-select:none;-webkit-tap-highlight-color:transparent;
520
+ transform:scale(calc(1 + var(--vk-press)*var(--vk-grow)))}
521
+ vire-icon-button[data-vk]>button{padding:0;width:var(--vk-height)}
522
+ ${BODY}>svg{width:var(--vk-icon);height:var(--vk-icon);flex:none;
523
+ opacity:calc((1 - var(--vk-press)*.45)*var(--vk-carry));filter:blur(calc((var(--vk-press)*var(--vk-defocus) + (1 - var(--vk-carry))*4)*1px))}
524
+ ${BODY}:disabled{cursor:default}
525
+ ${HOST}[data-disabled]>button{-webkit-text-fill-color:color-mix(in srgb,currentColor 40%,transparent)}
526
+ ${HOST}[data-disabled]>button>svg{opacity:.4}
527
+ ${HOST}[data-focus]>button{outline:2px solid currentColor;outline-offset:2px}
528
+
529
+ ${HOST}[data-surface=glass]>button{color:var(--vireglass-ink-color,inherit);
530
+ ${GLASS_BODY}}
531
+ ${HOST}[data-frost][data-surface=glass]>button{${FROST_INK}}
532
+
533
+ ${HOST}[data-surface=plain]>button{background:color-mix(in srgb,currentColor calc(10% + var(--vk-hover)*6% + var(--vk-press)*14%),transparent)}
534
+
535
+ ${HOST}[data-surface=frosted]>button{
536
+ background:color-mix(in srgb,Canvas calc(88% - var(--vk-hover)*4% - var(--vk-press)*10%),currentColor);
537
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
538
+ box-shadow:0 1px 3px color-mix(in srgb,currentColor 22%,transparent)}
539
+
540
+ ${MENU}{position:relative;--vk-ink-in:1;--vk-cx:0px;--vk-cy:0px}
541
+ ${SURFACE}{position:fixed;inset:auto;left:var(--vk-x,0);top:var(--vk-y,0);width:var(--vk-w,auto);height:var(--vk-h,auto);
542
+ margin:0;padding:var(--vk-menu-pad);border:0;border-radius:var(--vk-r,var(--vk-menu-radius));box-sizing:border-box;
543
+ overflow:clip;color:inherit;font:inherit;background:transparent;outline:none;
544
+ --vk-body-alpha:0;--vk-body-white:0;--vk-body-lift:0}
545
+ ${SURFACE}:popover-open{display:flex;flex-direction:column;align-items:flex-start}
546
+ ${MENU}[data-phase=open]>[popover][data-scroll]{overflow-y:auto}
547
+ ${ITEM}{appearance:none;-webkit-appearance:none;box-sizing:border-box;margin:0;border:0;outline:none;flex:none;position:relative;
548
+ font:inherit;line-height:1.2;color:inherit;text-align:start;white-space:nowrap;
549
+ display:flex;align-items:center;width:var(--vk-content-w,auto);height:var(--vk-menu-row);
550
+ padding-block:0;padding-inline:var(--vk-label-pad) var(--vk-menu-row);border-radius:var(--vk-item-radius);
551
+ background:color-mix(in srgb,currentColor calc(var(--vk-fill,0)*100%),transparent);
552
+ cursor:pointer;user-select:none;touch-action:manipulation;-webkit-tap-highlight-color:transparent}
553
+ ${SURFACE}:has(>button>svg)>button{padding-inline-start:var(--vk-menu-row)}
554
+ ${ITEM}>svg{position:absolute;inset-inline-start:calc((var(--vk-menu-row) - var(--vk-menu-icon))/2);top:calc((var(--vk-menu-row) - var(--vk-menu-icon))/2);
555
+ width:var(--vk-menu-icon);height:var(--vk-menu-icon)}
556
+ ${ITEM}:is(:focus,[data-vire-force-state=hover]){--vk-fill:var(--vk-fill-active)}
557
+ ${ITEM}:is([data-pressed],[data-vire-force-state=pressed]){--vk-fill:var(--vk-fill-pressed)}
558
+ ${ITEM}:focus-visible{outline:2px solid currentColor;outline-offset:-2px}
559
+ ${ITEM}:is(:disabled,[aria-disabled=true]){cursor:default;--vk-fill:0;-webkit-text-fill-color:color-mix(in srgb,currentColor 40%,transparent)}
560
+ ${ITEM}:is(:disabled,[aria-disabled=true])>svg{opacity:.4}
561
+ ${MENU}[data-phase=moving]>[popover]{padding:0}
562
+ ${MENU}[data-phase=moving]>[popover]>*{transform:translate(calc(var(--vk-cx) + var(--vk-menu-pad)),calc(var(--vk-cy) + var(--vk-menu-pad)));
563
+ opacity:min(1,calc(var(--vk-ink-in)*2));filter:blur(calc((1 - var(--vk-ink-in))*8px))}
564
+ ${MENU}[data-measure]>[popover]{width:max-content!important;height:auto!important;left:0!important;top:0!important;padding:var(--vk-menu-pad)!important;visibility:hidden}
565
+ ${MENU}[data-measure]>[popover]>*{width:auto!important;transform:none!important}
566
+
567
+ ${MENU}[data-surface=glass]>[popover]{color:var(--vireglass-ink-color,inherit);
568
+ ${GLASS_BODY}}
569
+ ${MENU}[data-frost][data-surface=glass]>[popover]{${FROST_INK}}
570
+ ${MENU}[data-surface=plain]>[popover]{background:Canvas;${SHADOW}}
571
+ ${MENU}[data-surface=frosted]>[popover]{background:color-mix(in srgb,Canvas 88%,currentColor);
572
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);${SHADOW}}
573
+
574
+ @supports not (color:oklab(from red l a b)){
575
+ ${HOST}[data-frost][data-surface=glass]>button,${MENU}[data-frost][data-surface=glass]>[popover]{color:inherit;background:color-mix(in srgb,Canvas 82%,currentColor)}
576
+ }
577
+
578
+ ${HOST}[data-contrast]:is([data-surface=plain],[data-surface=frosted])>button{box-shadow:inset 0 0 0 2px currentColor}
579
+ ${HOST}[data-contrast][data-surface=frosted]>button{background:Canvas}
580
+ ${HOST}[data-contrast][data-surface=glass]>button{${CONTRAST_GLASS}}
581
+ ${MENU}[data-contrast]:is([data-surface=plain],[data-surface=frosted])>[popover]{background:Canvas;box-shadow:inset 0 0 0 2px currentColor}
582
+ ${MENU}[data-contrast][data-surface=glass]>[popover]{${CONTRAST_GLASS}}
583
+
584
+ ${HOST}[data-carried]>button{background:transparent!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important;box-shadow:none!important;pointer-events:none}
585
+ ${HOST}[data-carried]>button::after{visibility:hidden}
586
+
587
+ @media (forced-colors:active){
588
+ ${BODY}{-webkit-text-fill-color:currentColor;text-shadow:none;color:ButtonText;background:ButtonFace;border:1px solid ButtonText;backdrop-filter:none;-webkit-backdrop-filter:none;transform:none}
589
+ ${BODY}>svg{opacity:1;filter:none}
590
+ ${HOST}[data-disabled]>button{-webkit-text-fill-color:currentColor;color:GrayText;border-color:GrayText}
591
+ ${HOST}[data-focus]>button{outline-color:Highlight}
592
+ ${SURFACE}{color:CanvasText!important;background:Canvas!important;border:1px solid CanvasText;backdrop-filter:none!important;-webkit-backdrop-filter:none!important}
593
+ ${SURFACE}>*{filter:none!important;opacity:1!important}
594
+ ${ITEM}{-webkit-text-fill-color:currentColor!important;color:CanvasText;background:Canvas}
595
+ ${ITEM}:is(:focus,[data-vire-force-state=hover],[data-pressed]){color:HighlightText;background:Highlight}
596
+ ${ITEM}:is(:disabled,[aria-disabled=true]){color:GrayText;background:Canvas}
597
+ ${HOST}[data-carried]>button{opacity:var(--vk-carry)}
598
+ }
599
+ `;
600
+ var STYLE_ATTR = "data-vireuikit";
601
+ function ensureStyles(from, nonce) {
602
+ const root = from.getRootNode();
603
+ const host = root instanceof ShadowRoot ? root : from.ownerDocument;
604
+ if (host.querySelector(`style[${STYLE_ATTR}]`)) return;
605
+ const style = from.ownerDocument.createElement("style");
606
+ style.setAttribute(STYLE_ATTR, "");
607
+ if (nonce) style.nonce = nonce;
608
+ style.textContent = STYLES;
609
+ (host instanceof ShadowRoot ? host : host.head).appendChild(style);
610
+ }
611
+
612
+ // src/web/element.ts
613
+ var FORCE_ATTRIBUTE = "data-vire-force-state";
614
+ var FORCED = {
615
+ rest: CONTROL_IDLE,
616
+ hover: { ...CONTROL_IDLE, hovered: true },
617
+ focus: { ...CONTROL_IDLE, focused: true },
618
+ pressed: { ...CONTROL_IDLE, press: { by: "pointer" } },
619
+ disabled: { ...CONTROL_IDLE, disabled: true }
620
+ };
621
+ var MATERIAL_STEPS = 10;
622
+ var SUPPRESS_MS = 100;
623
+ function createControlElementClass(kind, options = {}) {
624
+ const response = pressResponse(kind);
625
+ return class VireControl extends HTMLElement {
626
+ static observedAttributes = ["glass", "size", "disabled", FORCE_ATTRIBUTE];
627
+ #control = null;
628
+ #state = CONTROL_IDLE;
629
+ #caps = POINTER_INPUT;
630
+ #visual = createControlVisual();
631
+ #own = {};
632
+ #mode = null;
633
+ #presentation = null;
634
+ #glass = null;
635
+ #glassToken = 0;
636
+ #glassKey = "";
637
+ #glassBroken = false;
638
+ #lastMaterialKey = "";
639
+ #frame = 0;
640
+ #lastFrame = 0;
641
+ #cleanups = [];
642
+ #suppressClick = false;
643
+ #suppressTimer = 0;
644
+ #slop = { x: 0, y: 0 };
645
+ #morph = null;
646
+ #sample = null;
647
+ #carried = null;
648
+ #hadStyle = { host: false, control: false };
649
+ get a11y() {
650
+ return this.#own;
651
+ }
652
+ set a11y(next) {
653
+ this.#own = { ...next };
654
+ if (this.#control) this.#refreshMode();
655
+ }
656
+ get sample() {
657
+ return this.#sample;
658
+ }
659
+ set sample(next) {
660
+ this.#sample = next;
661
+ this.#glass?.setSample(next);
662
+ }
663
+ setMorph(morph) {
664
+ this.#morph = morph;
665
+ this.#glass?.setMorph(morph);
666
+ }
667
+ setCarried(ink) {
668
+ const handedOver = this.#carried === null !== (ink === null);
669
+ this.#carried = ink === null ? null : Math.min(Math.max(ink, 0), 1);
670
+ this.toggleAttribute("data-carried", this.#carried !== null);
671
+ if (this.#carried === null) this.style.removeProperty("--vk-carry");
672
+ else this.style.setProperty("--vk-carry", this.#carried.toFixed(3));
673
+ if (handedOver) this.#glass?.setAppear(this.#carried === null && this.#glassMode() !== "touch" ? 1 : 0);
674
+ }
675
+ connectedCallback() {
676
+ for (const key of ["a11y", "sample"]) {
677
+ if (!Object.prototype.hasOwnProperty.call(this, key)) continue;
678
+ const value = Reflect.get(this, key);
679
+ Reflect.deleteProperty(this, key);
680
+ Reflect.set(this, key, value);
681
+ }
682
+ this.#hadStyle.host = this.hasAttribute("style");
683
+ ensureStyles(this, options.nonce);
684
+ this.#cleanups.push(watchCoarsePointer(() => this.#onInputChange()));
685
+ this.#cleanups.push(watchSystemA11y(() => this.#refreshMode()));
686
+ this.#cleanups.push(watchA11yOverride(() => this.#refreshMode()));
687
+ this.#awaitControl();
688
+ }
689
+ disconnectedCallback() {
690
+ for (const cleanup of this.#cleanups.splice(0)) cleanup();
691
+ cancelAnimationFrame(this.#frame);
692
+ clearTimeout(this.#suppressTimer);
693
+ this.#frame = 0;
694
+ this.#dropGlass();
695
+ if (this.#control && !this.#hadStyle.control && this.#control.getAttribute("style") === "") this.#control.removeAttribute("style");
696
+ this.#control = null;
697
+ this.#state = CONTROL_IDLE;
698
+ this.#suppressClick = false;
699
+ this.#carried = null;
700
+ for (const name of ["data-vk", "data-surface", "data-glass", "data-frost", "data-size", "data-carried"]) this.removeAttribute(name);
701
+ for (const name of Array.from(this.style)) if (name.startsWith("--vk-")) this.style.removeProperty(name);
702
+ if (!this.#hadStyle.host && this.getAttribute("style") === "") this.removeAttribute("style");
703
+ this.#presentation = null;
704
+ this.#mode = null;
705
+ }
706
+ attributeChangedCallback(name) {
707
+ if (!this.#control) return;
708
+ if (name === "disabled") {
709
+ const wanted = this.hasAttribute("disabled");
710
+ if (this.#control.disabled !== wanted) this.#control.disabled = wanted;
711
+ this.#syncDisabled();
712
+ } else if (name === FORCE_ATTRIBUTE) {
713
+ this.#applyForced();
714
+ } else {
715
+ this.#refreshMode(true);
716
+ }
717
+ }
718
+ // The parser connects the element before its children exist, so the button is waited for
719
+ // rather than made up.
720
+ #awaitControl() {
721
+ const found = this.querySelector(":scope > button");
722
+ if (found) {
723
+ this.#upgrade(found);
724
+ return;
725
+ }
726
+ const observer = new MutationObserver(() => {
727
+ const button = this.querySelector(":scope > button");
728
+ if (!button) return;
729
+ observer.disconnect();
730
+ this.#upgrade(button);
731
+ });
732
+ observer.observe(this, { childList: true });
733
+ this.#cleanups.push(() => observer.disconnect());
734
+ }
735
+ #upgrade(control) {
736
+ this.#control = control;
737
+ this.#hadStyle.control = control.hasAttribute("style");
738
+ this.setAttribute("data-vk", "");
739
+ this.#caps = isCoarsePointer() ? TOUCH_INPUT : POINTER_INPUT;
740
+ this.#listen(control);
741
+ this.#cleanups.push(this.#observeSize(control));
742
+ this.#syncDisabled();
743
+ if (control.matches(":focus")) {
744
+ this.#dispatch({ type: "focus", visible: control.matches(":focus-visible") });
745
+ }
746
+ this.#mode = null;
747
+ this.#refreshMode();
748
+ }
749
+ #onInputChange() {
750
+ if (!this.#control) return;
751
+ this.#caps = isCoarsePointer() ? TOUCH_INPUT : POINTER_INPUT;
752
+ this.#refreshMode(true);
753
+ }
754
+ #size() {
755
+ const wanted = this.getAttribute("size");
756
+ if (wanted && CONTROL_SIZES.includes(wanted)) return wanted;
757
+ return defaultSize({ touch: isCoarsePointer() });
758
+ }
759
+ #glassMode() {
760
+ const wanted = this.getAttribute("glass");
761
+ if (wanted === "touch") return "touch";
762
+ return wanted === "" || wanted === "present" ? "present" : "off";
763
+ }
764
+ #forced() {
765
+ const wanted = this.getAttribute(FORCE_ATTRIBUTE);
766
+ return wanted !== null && Object.hasOwn(FORCED, wanted) ? wanted : null;
767
+ }
768
+ #dispatch(event) {
769
+ if (this.#forced()) return false;
770
+ const step = stepControl(this.#state, event, this.#caps);
771
+ this.#state = step.state;
772
+ this.#render();
773
+ return step.activated;
774
+ }
775
+ #listen(control) {
776
+ const on = (target, type, handler, capture = false) => {
777
+ target.addEventListener(type, handler, capture);
778
+ this.#cleanups.push(() => target.removeEventListener(type, handler, capture));
779
+ };
780
+ const inside = (e) => {
781
+ const r = this.getBoundingClientRect();
782
+ return e.clientX >= r.left - this.#slop.x && e.clientX <= r.right + this.#slop.x && e.clientY >= r.top - this.#slop.y && e.clientY <= r.bottom + this.#slop.y;
783
+ };
784
+ const holdSuppression = () => {
785
+ this.#suppressClick = true;
786
+ clearTimeout(this.#suppressTimer);
787
+ this.#suppressTimer = window.setTimeout(() => this.#suppressClick = false, SUPPRESS_MS);
788
+ };
789
+ const releaseSuppression = () => {
790
+ this.#suppressClick = false;
791
+ clearTimeout(this.#suppressTimer);
792
+ };
793
+ const bind = (target, own) => {
794
+ const mine = (e) => own || e.target === this;
795
+ on(target, "pointerdown", (e) => {
796
+ if (e.button !== 0 || !mine(e)) return;
797
+ releaseSuppression();
798
+ this.#dispatch({ type: "down", source: "pointer" });
799
+ });
800
+ on(target, "pointermove", (e) => {
801
+ if (!mine(e)) return;
802
+ if (this.#state.press.by === "pointer" && !inside(e)) this.#dispatch({ type: "leave" });
803
+ });
804
+ on(target, "pointerup", (e) => {
805
+ if (!mine(e)) return;
806
+ const wasPressed = this.#state.press.by === "pointer";
807
+ const activated = this.#dispatch({ type: "up", source: "pointer" });
808
+ if (own && !activated && !wasPressed && !inside(e)) holdSuppression();
809
+ });
810
+ on(target, "pointercancel", (e) => {
811
+ if (mine(e)) this.#dispatch({ type: "cancel" });
812
+ });
813
+ on(target, "pointerenter", (e) => {
814
+ if (mine(e) && e.pointerType !== "touch") this.#dispatch({ type: "enter" });
815
+ });
816
+ on(target, "pointerleave", (e) => {
817
+ if (mine(e) && !control.hasPointerCapture?.(e.pointerId)) this.#dispatch({ type: "leave" });
818
+ });
819
+ };
820
+ bind(control, true);
821
+ bind(this, false);
822
+ on(
823
+ this,
824
+ "click",
825
+ (e) => {
826
+ if (e.target !== this) return;
827
+ e.stopImmediatePropagation();
828
+ e.preventDefault();
829
+ control.click();
830
+ },
831
+ true
832
+ );
833
+ on(
834
+ control,
835
+ "click",
836
+ (e) => {
837
+ if (!this.#suppressClick) return;
838
+ releaseSuppression();
839
+ if (e.detail === 0) return;
840
+ e.preventDefault();
841
+ e.stopImmediatePropagation();
842
+ },
843
+ true
844
+ );
845
+ on(control, "keydown", (e) => {
846
+ releaseSuppression();
847
+ if ((e.key === " " || e.key === "Enter") && !e.repeat) this.#dispatch({ type: "down", source: "keyboard" });
848
+ });
849
+ on(control, "keyup", (e) => {
850
+ if (e.key === " " || e.key === "Enter") this.#dispatch({ type: "up", source: "keyboard" });
851
+ });
852
+ on(control, "focus", () => this.#dispatch({ type: "focus", visible: control.matches(":focus-visible") }));
853
+ on(control, "blur", () => this.#dispatch({ type: "blur" }));
854
+ on(control, "focusout", () => this.#dispatch({ type: "blur" }));
855
+ const mutations = new MutationObserver(() => this.#syncDisabled());
856
+ mutations.observe(control, { attributes: true, attributeFilter: ["disabled"] });
857
+ this.#cleanups.push(() => mutations.disconnect());
858
+ }
859
+ #observeSize(control) {
860
+ if (typeof ResizeObserver === "undefined") return () => {
861
+ };
862
+ const observer = new ResizeObserver(() => {
863
+ this.#applySlop();
864
+ this.#glass?.setGeometry(this.#geometry(control));
865
+ });
866
+ observer.observe(control);
867
+ return () => observer.disconnect();
868
+ }
869
+ #geometry(control) {
870
+ const radius = parseFloat(getComputedStyle(control).borderTopLeftRadius) || 0;
871
+ return { width: control.offsetWidth, height: control.offsetHeight, cornerRadius: radius };
872
+ }
873
+ #applySlop() {
874
+ const control = this.#control;
875
+ if (!control) return;
876
+ const floor = minTarget({ touch: isCoarsePointer() });
877
+ this.#slop = { x: hitSlop(control.offsetWidth, floor), y: hitSlop(control.offsetHeight, floor) };
878
+ this.style.setProperty("--vk-slop-x", String(this.#slop.x));
879
+ this.style.setProperty("--vk-slop-y", String(this.#slop.y));
880
+ }
881
+ #syncDisabled() {
882
+ const control = this.#control;
883
+ if (!control) return;
884
+ if (this.hasAttribute("disabled") && !control.disabled) control.disabled = true;
885
+ const disabled = control.disabled;
886
+ if (this.#forced()) return;
887
+ if (disabled !== this.#state.disabled) {
888
+ this.#state = stepControl(this.#state, { type: "disable", disabled }, this.#caps).state;
889
+ this.#render();
890
+ }
891
+ }
892
+ #applyForced() {
893
+ const forced = this.#forced();
894
+ if (forced) {
895
+ this.#state = FORCED[forced];
896
+ settleControlVisual(this.#visual, this.#state);
897
+ if (forced === "disabled" && this.#control && !this.#control.disabled) this.#control.disabled = true;
898
+ } else {
899
+ this.#state = CONTROL_IDLE;
900
+ this.#syncDisabled();
901
+ }
902
+ this.#render(true);
903
+ }
904
+ #refreshMode(force = false, afterFailure = false) {
905
+ if (!afterFailure) this.#glassBroken = false;
906
+ const mode = effectiveA11y(this.#own);
907
+ let next = presentationFor(mode, this.#glassMode() !== "off");
908
+ if (this.#glassBroken && next.glass) next = { ...next, glass: false, surface: next.frost ? "frosted" : "plain" };
909
+ const changed = force || !this.#mode || !sameA11y(this.#mode, mode) || next.surface !== this.#presentation?.surface;
910
+ this.#mode = mode;
911
+ this.#presentation = next;
912
+ this.#applySize();
913
+ this.setAttribute("data-surface", next.surface);
914
+ this.toggleAttribute("data-contrast", next.contrastBorder);
915
+ this.toggleAttribute("data-frost", next.frost);
916
+ this.setAttribute("data-glass", next.glass ? "on" : "off");
917
+ if (changed) this.#reattachGlass(mode);
918
+ if (this.#forced()) this.#applyForced();
919
+ else this.#render(true);
920
+ }
921
+ #applySize() {
922
+ const size = this.#size();
923
+ const spec = kind === "iconButton" ? iconButtonSize(size) : BUTTON_SIZES[size];
924
+ const set = (name, value) => this.style.setProperty(name, value);
925
+ set("--vk-height", `${spec.height}px`);
926
+ set("--vk-pad-x", `${spec.paddingX}px`);
927
+ set("--vk-icon", `${spec.icon}px`);
928
+ set("--vk-radius", `${spec.radius}px`);
929
+ set("--vk-grow", String(response.grow));
930
+ set("--vk-defocus", String(response.defocus));
931
+ this.setAttribute("data-size", size);
932
+ this.#applySlop();
933
+ }
934
+ #dropGlass() {
935
+ this.#glassToken += 1;
936
+ this.#glass?.destroy();
937
+ this.#glass = null;
938
+ this.#glassKey = "";
939
+ this.#lastMaterialKey = "";
940
+ }
941
+ #reattachGlass(mode) {
942
+ const control = this.#control;
943
+ const want = this.#presentation?.glass === true && control !== null;
944
+ const key = want ? `${this.#glassMode()}|${JSON.stringify(mode)}|${this.#size()}` : "";
945
+ if (key === this.#glassKey) return;
946
+ this.#dropGlass();
947
+ this.#glassKey = key;
948
+ if (!want || !control) return;
949
+ const token = this.#glassToken;
950
+ const sample = this.#sample;
951
+ void attachControlGlass(
952
+ control,
953
+ {
954
+ material: vireglass.VIREGLASS_CONTROL_MATERIAL,
955
+ geometry: this.#geometry(control),
956
+ accessibility: mode,
957
+ onTouch: this.#glassMode() === "touch",
958
+ sample
959
+ },
960
+ () => token === this.#glassToken && this.isConnected
961
+ ).then((session) => {
962
+ if (!session) return;
963
+ this.#glass = session;
964
+ if (this.#morph) session.setMorph(this.#morph);
965
+ if (this.#carried !== null) session.setAppear(0);
966
+ if (this.#sample !== sample) session.setSample(this.#sample);
967
+ this.#pushMaterial(true);
968
+ this.dispatchEvent(new CustomEvent("vire-glass-ready"));
969
+ }).catch(() => {
970
+ if (token !== this.#glassToken) return;
971
+ this.#glassKey = "";
972
+ this.#glassBroken = true;
973
+ this.#refreshMode(false, true);
974
+ });
975
+ }
976
+ #pushMaterial(force = false) {
977
+ const glass = this.#glass;
978
+ if (!glass) return;
979
+ const atom = visualAtomState(this.#visual, this.#state);
980
+ const snap = (v) => Math.round(Math.min(Math.max(v, 0), 1) * MATERIAL_STEPS) / MATERIAL_STEPS;
981
+ const quantized = { ...atom, pressed: snap(atom.pressed), hovered: snap(atom.hovered) };
982
+ const key = `${quantized.pressed}|${quantized.hovered}|${quantized.disabled}`;
983
+ if (!force && key === this.#lastMaterialKey) return;
984
+ this.#lastMaterialKey = key;
985
+ const material = atomMaterial(vireglass.VIREGLASS_CONTROL_MATERIAL, kind, quantized);
986
+ glass.setMaterial(material);
987
+ if (this.#glassMode() === "touch" && this.#forced()) glass.setAppear(quantized.pressed);
988
+ }
989
+ #render(immediate = false) {
990
+ if (immediate && this.#forced()) settleControlVisual(this.#visual, this.#state);
991
+ this.#paint();
992
+ if (this.#frame || immediate && this.#forced()) return;
993
+ this.#lastFrame = 0;
994
+ this.#frame = requestAnimationFrame((t) => this.#tick(t));
995
+ }
996
+ #tick(now) {
997
+ this.#frame = 0;
998
+ if (!this.isConnected) return;
999
+ const dt = this.#lastFrame ? Math.min((now - this.#lastFrame) / 1e3, 0.05) : 1 / 60;
1000
+ this.#lastFrame = now;
1001
+ const moving = stepControlVisual(this.#visual, dt, this.#state, this.#presentation?.instantSprings ?? false);
1002
+ this.#paint();
1003
+ if (moving) this.#frame = requestAnimationFrame((t) => this.#tick(t));
1004
+ }
1005
+ #paint() {
1006
+ const v = this.#visual;
1007
+ const set = (name, value) => this.style.setProperty(name, value.toFixed(3));
1008
+ set("--vk-press", v.pressed.x);
1009
+ set("--vk-hover", v.hovered.x);
1010
+ const body = atomBody(vireglass.VIREGLASS_CONTROL_MATERIAL, visualAtomState(v, this.#state), this.#presentation?.frost ?? false);
1011
+ set("--vk-body-alpha", body.alpha);
1012
+ set("--vk-body-white", body.white);
1013
+ set("--vk-body-lift", body.lift);
1014
+ this.toggleAttribute("data-pressed", this.#state.press.by !== null);
1015
+ this.toggleAttribute("data-hovered", this.#state.hovered);
1016
+ this.toggleAttribute("data-focus", this.#state.focused);
1017
+ this.toggleAttribute("data-disabled", this.#state.disabled);
1018
+ this.#pushMaterial();
1019
+ }
1020
+ };
1021
+ }
1022
+ var MENU_CLOSED = { open: false, active: -1, typed: "", typedAt: Number.NEGATIVE_INFINITY };
1023
+ var TYPEAHEAD_SECONDS = 0.5;
1024
+ var exists = (items, i) => i >= 0 && i < items.length;
1025
+ var enabled = (items, i) => exists(items, i) && !items[i].disabled;
1026
+ var reachable = (items, i) => exists(items, i) && (items[i].focusable ?? !items[i].disabled);
1027
+ function seek(items, from, step) {
1028
+ const n = items.length;
1029
+ for (let k = 1; k <= n; k += 1) {
1030
+ const i = ((from + step * k) % n + n) % n;
1031
+ if (reachable(items, i)) return i;
1032
+ }
1033
+ return -1;
1034
+ }
1035
+ var first = (items) => seek(items, -1, 1);
1036
+ var last = (items) => seek(items, items.length, -1);
1037
+ var toItem = (index) => index < 0 ? { to: "menu" } : { to: "item", index };
1038
+ function typeahead(state, items, key, at) {
1039
+ const fresh = at - state.typedAt > TYPEAHEAD_SECONDS;
1040
+ const typed = (fresh ? "" : state.typed) + key.toLowerCase();
1041
+ const repeated = typed.length > 1 && [...typed].every((c) => c === typed[0]);
1042
+ const needle = repeated ? typed[0] : typed;
1043
+ const start = needle.length > 1 ? state.active - 1 : state.active;
1044
+ const n = items.length;
1045
+ for (let k = 1; k <= n; k += 1) {
1046
+ const i = ((start + k) % n + n) % n;
1047
+ if (reachable(items, i) && items[i].label.trim().toLowerCase().startsWith(needle)) {
1048
+ return { state: { ...state, active: i, typed, typedAt: at }, focus: { to: "item", index: i }, chosen: null };
1049
+ }
1050
+ }
1051
+ return { state: { ...state, typed, typedAt: at }, focus: { to: "stay" }, chosen: null };
1052
+ }
1053
+ function stepMenu(state, event, items) {
1054
+ const stay = { state, focus: { to: "stay" }, chosen: null };
1055
+ if (event.type === "open") {
1056
+ const active = event.focus === "first" ? first(items) : event.focus === "last" ? last(items) : -1;
1057
+ return { state: { ...MENU_CLOSED, open: true, active }, focus: toItem(active), chosen: null };
1058
+ }
1059
+ if (!state.open) return stay;
1060
+ switch (event.type) {
1061
+ case "move": {
1062
+ const active = event.to === "first" ? first(items) : event.to === "last" ? last(items) : seek(items, state.active < 0 && event.to === "previous" ? items.length : state.active, event.to === "next" ? 1 : -1);
1063
+ if (active < 0) return stay;
1064
+ return { state: { ...state, active, typed: "" }, focus: { to: "item", index: active }, chosen: null };
1065
+ }
1066
+ case "point":
1067
+ if (!reachable(items, event.index) || event.index === state.active) return stay;
1068
+ return { state: { ...state, active: event.index, typed: "" }, focus: { to: "item", index: event.index }, chosen: null };
1069
+ case "type":
1070
+ return typeahead(state, items, event.key, event.at);
1071
+ case "choose":
1072
+ if (!enabled(items, event.index)) return stay;
1073
+ return { state: MENU_CLOSED, focus: { to: "trigger" }, chosen: event.index };
1074
+ case "dismiss":
1075
+ return { state: MENU_CLOSED, focus: event.reason === "outside" ? { to: "none" } : { to: "trigger" }, chosen: null };
1076
+ }
1077
+ }
1078
+ var MENU_CORNERS = {
1079
+ "top-left": { x: -1, y: -1 },
1080
+ "top-right": { x: 1, y: -1 },
1081
+ "bottom-left": { x: -1, y: 1 },
1082
+ "bottom-right": { x: 1, y: 1 }
1083
+ };
1084
+ function menuCorner(trigger, viewport) {
1085
+ return {
1086
+ x: trigger.x + trigger.width / 2 <= viewport.width / 2 ? -1 : 1,
1087
+ y: trigger.y + trigger.height / 2 <= viewport.height / 2 ? -1 : 1
1088
+ };
1089
+ }
1090
+ function along(start, length, size, side) {
1091
+ return side < 0 ? start : start + length - size;
1092
+ }
1093
+ function fits(at, size, limit, margin) {
1094
+ return at >= margin && at + size <= limit - margin;
1095
+ }
1096
+ var clamp = (v, lo, hi) => Math.min(Math.max(v, lo), Math.max(lo, hi));
1097
+ function menuPlacement(trigger, menu, viewport, options = {}) {
1098
+ const margin = options.margin ?? 8;
1099
+ const width = Math.min(menu.width, viewport.width - 2 * margin);
1100
+ const height = Math.min(menu.height, viewport.height - 2 * margin);
1101
+ const corner = { ...options.corner ?? menuCorner(trigger, viewport) };
1102
+ let x = along(trigger.x, trigger.width, width, corner.x);
1103
+ let y = along(trigger.y, trigger.height, height, corner.y);
1104
+ if (!options.corner) {
1105
+ const fx = along(trigger.x, trigger.width, width, corner.x === -1 ? 1 : -1);
1106
+ if (!fits(x, width, viewport.width, margin) && fits(fx, width, viewport.width, margin)) {
1107
+ corner.x = corner.x === -1 ? 1 : -1;
1108
+ x = fx;
1109
+ }
1110
+ const fy = along(trigger.y, trigger.height, height, corner.y === -1 ? 1 : -1);
1111
+ if (!fits(y, height, viewport.height, margin) && fits(fy, height, viewport.height, margin)) {
1112
+ corner.y = corner.y === -1 ? 1 : -1;
1113
+ y = fy;
1114
+ }
1115
+ }
1116
+ x = clamp(x, margin, viewport.width - margin - width);
1117
+ y = clamp(y, margin, viewport.height - margin - height);
1118
+ const rect = { x, y, width, height };
1119
+ return {
1120
+ rect,
1121
+ corner,
1122
+ anchor: { x: corner.x < 0 ? x : x + width, y: corner.y < 0 ? y : y + height }
1123
+ };
1124
+ }
1125
+ var center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
1126
+ function menuActivity(phase) {
1127
+ const shrink = Math.min(Math.max(phase.shrink, 0), 1);
1128
+ const grow = Math.min(Math.max(phase.grow, 0), 1);
1129
+ return shrink * (1 - grow);
1130
+ }
1131
+ function menuSurfaceFrame(trigger, menu, anchor, phase) {
1132
+ const frame = growthFrame(
1133
+ { geometry: { width: trigger.width, height: trigger.height, cornerRadius: trigger.radius }, center: center(trigger) },
1134
+ { geometry: { width: menu.width, height: menu.height, cornerRadius: menu.radius }, center: center(menu) },
1135
+ anchor,
1136
+ phase
1137
+ );
1138
+ const { geometry, center: c } = frame.body;
1139
+ const box = {
1140
+ x: c.x - geometry.width / 2,
1141
+ y: c.y - geometry.height / 2,
1142
+ width: geometry.width,
1143
+ height: geometry.height,
1144
+ radius: geometry.cornerRadius
1145
+ };
1146
+ return {
1147
+ box,
1148
+ morph: frame.merge,
1149
+ content: { x: menu.x - box.x, y: menu.y - box.y },
1150
+ ink: frame.ink,
1151
+ activity: menuActivity(phase)
1152
+ };
1153
+ }
1154
+ function menuGlass(base) {
1155
+ return { ...base, roughness: Math.max(base.roughness, vireglass.VIREGLASS_SHEET_MATERIAL.roughness) };
1156
+ }
1157
+ function menuMaterial(base, activity) {
1158
+ return atomMaterial(menuGlass(base), "menu", { ...ATOM_AT_REST, pressed: activity });
1159
+ }
1160
+ var MENU_DROP_BODY = { alpha: 0.9, white: 0.1, lift: 0.2 };
1161
+ function menuBody(base, activity, frost = false) {
1162
+ const rest = atomBody(base, ATOM_AT_REST, frost);
1163
+ if (frost) return rest;
1164
+ const k = Math.min(Math.max(activity, 0), 1);
1165
+ const mix2 = (key) => rest[key] + (MENU_DROP_BODY[key] - rest[key]) * k;
1166
+ return { alpha: mix2("alpha"), white: mix2("white"), lift: mix2("lift") };
1167
+ }
1168
+ var MENU_ITEM_FILL = { active: 0.1, pressed: 0.18 };
1169
+
1170
+ // src/web/menu.ts
1171
+ var FORCE_ATTRIBUTE2 = "data-vire-force-state";
1172
+ var FORCED2 = {
1173
+ closed: null,
1174
+ growing: { shrink: 1, grow: 0.3 },
1175
+ open: { shrink: 1, grow: 1 },
1176
+ retracting: { shrink: 0.55, grow: 0 }
1177
+ };
1178
+ var FORCED_PHASE = /^\d*\.?\d+ \d*\.?\d+$/;
1179
+ var GLASS_ON = /* @__PURE__ */ new Set(["", "present", "touch"]);
1180
+ var MATERIAL_STEPS2 = 10;
1181
+ var VIEWPORT_MARGIN = 8;
1182
+ var TRIGGER = ":scope > button, :scope > vire-button > button, :scope > vire-icon-button > button";
1183
+ var ACTIVATION_KEYS = /* @__PURE__ */ new Set(["Enter", " "]);
1184
+ var ids = 0;
1185
+ var nativelyDisabled = (item) => item.hasAttribute("disabled");
1186
+ var isDisabled = (item) => nativelyDisabled(item) || item.getAttribute("aria-disabled") === "true";
1187
+ function rootsOf(el) {
1188
+ const roots = [];
1189
+ let node = el.getRootNode();
1190
+ while (node instanceof ShadowRoot) {
1191
+ roots.push(node);
1192
+ node = node.host.getRootNode();
1193
+ }
1194
+ roots.push(el.ownerDocument);
1195
+ return roots;
1196
+ }
1197
+ function dropEmptyStyle(el, hadStyle) {
1198
+ if (!hadStyle && el.getAttribute("style") === "") el.removeAttribute("style");
1199
+ }
1200
+ function createMenuElementClass(options = {}) {
1201
+ return class VireMenu extends HTMLElement {
1202
+ static observedAttributes = ["glass", "size", "corner", FORCE_ATTRIBUTE2];
1203
+ #trigger = null;
1204
+ #carrier = null;
1205
+ #list = null;
1206
+ #restore = [];
1207
+ #itemRestore = /* @__PURE__ */ new Map();
1208
+ #hadStyle = { host: false, list: false };
1209
+ #cleanups = [];
1210
+ #state = MENU_CLOSED;
1211
+ #growth = createGrowthState();
1212
+ #from = null;
1213
+ #to = null;
1214
+ #anchor = null;
1215
+ #natural = null;
1216
+ #out = false;
1217
+ #viaPointer = false;
1218
+ #frame = 0;
1219
+ #placeFrame = 0;
1220
+ #inertTimer = 0;
1221
+ #lastFrame = 0;
1222
+ /** When the content was last fully in focus; null while it is still arriving or leaving. */
1223
+ #readySince = null;
1224
+ /** When the content came into focus, which is when a deliberate key may choose. */
1225
+ #inSince = null;
1226
+ #downAt = Number.NEGATIVE_INFINITY;
1227
+ #own = {};
1228
+ #mode = null;
1229
+ #presentation = null;
1230
+ #glass = null;
1231
+ #glassToken = 0;
1232
+ #glassKey = "";
1233
+ #glassBroken = false;
1234
+ #lastLens = { material: "", morph: "", geometry: "" };
1235
+ #sample = null;
1236
+ #held = null;
1237
+ get a11y() {
1238
+ return this.#own;
1239
+ }
1240
+ set a11y(next) {
1241
+ this.#own = { ...next };
1242
+ if (this.#list) this.#refreshMode();
1243
+ }
1244
+ get sample() {
1245
+ return this.#sample;
1246
+ }
1247
+ set sample(next) {
1248
+ this.#sample = next;
1249
+ if (!this.#glass) return;
1250
+ if (this.#out) this.#holdSample();
1251
+ else this.#glass.setSample(next);
1252
+ }
1253
+ get open() {
1254
+ return this.#state.open;
1255
+ }
1256
+ connectedCallback() {
1257
+ for (const key of ["a11y", "sample"]) {
1258
+ if (!Object.prototype.hasOwnProperty.call(this, key)) continue;
1259
+ const value = Reflect.get(this, key);
1260
+ Reflect.deleteProperty(this, key);
1261
+ Reflect.set(this, key, value);
1262
+ }
1263
+ this.#hadStyle.host = this.hasAttribute("style");
1264
+ ensureStyles(this, options.nonce);
1265
+ this.#cleanups.push(watchCoarsePointer(() => this.#list && this.#refreshMode(true)));
1266
+ this.#cleanups.push(watchSystemA11y(() => this.#list && this.#refreshMode()));
1267
+ this.#cleanups.push(watchA11yOverride(() => this.#list && this.#refreshMode()));
1268
+ this.#awaitParts();
1269
+ }
1270
+ disconnectedCallback() {
1271
+ for (const cleanup of this.#cleanups.splice(0)) cleanup();
1272
+ cancelAnimationFrame(this.#frame);
1273
+ cancelAnimationFrame(this.#placeFrame);
1274
+ clearTimeout(this.#inertTimer);
1275
+ this.#frame = this.#placeFrame = this.#inertTimer = 0;
1276
+ this.#dropGlass();
1277
+ const list = this.#list;
1278
+ if (list) {
1279
+ if (list.matches(":popover-open")) list.hidePopover();
1280
+ list.inert = false;
1281
+ list.removeAttribute("data-scroll");
1282
+ for (const name of Array.from(list.style)) if (name.startsWith("--vk-")) list.style.removeProperty(name);
1283
+ for (const item of this.#items()) item.removeAttribute("data-pressed");
1284
+ }
1285
+ this.#carrier?.setCarried?.(null);
1286
+ for (const restore of this.#itemRestore.values()) restore();
1287
+ this.#itemRestore.clear();
1288
+ for (const restore of this.#restore.splice(0).reverse()) restore();
1289
+ if (list) dropEmptyStyle(list, this.#hadStyle.list);
1290
+ this.#trigger = null;
1291
+ this.#carrier = null;
1292
+ this.#list = null;
1293
+ this.#state = MENU_CLOSED;
1294
+ this.#growth = createGrowthState();
1295
+ this.#out = false;
1296
+ this.#from = this.#to = this.#anchor = null;
1297
+ this.#natural = null;
1298
+ this.#held = null;
1299
+ this.#readySince = null;
1300
+ this.#inSince = null;
1301
+ for (const name of ["data-vk", "data-surface", "data-glass", "data-frost", "data-contrast", "data-size", "data-phase", "data-measure"]) {
1302
+ this.removeAttribute(name);
1303
+ }
1304
+ for (const name of Array.from(this.style)) if (name.startsWith("--vk-")) this.style.removeProperty(name);
1305
+ dropEmptyStyle(this, this.#hadStyle.host);
1306
+ this.#presentation = null;
1307
+ this.#mode = null;
1308
+ }
1309
+ attributeChangedCallback(name) {
1310
+ if (!this.#list) return;
1311
+ if (name === FORCE_ATTRIBUTE2) this.#applyForced();
1312
+ else if (name === "corner") this.#out && this.#place();
1313
+ else this.#refreshMode(true);
1314
+ }
1315
+ // The parser connects the element before its children exist, so the parts are waited for, not made up.
1316
+ #awaitParts() {
1317
+ if (this.#tryUpgrade()) return;
1318
+ const observer = new MutationObserver(() => {
1319
+ if (this.#tryUpgrade()) observer.disconnect();
1320
+ });
1321
+ observer.observe(this, { childList: true, subtree: true });
1322
+ this.#cleanups.push(() => observer.disconnect());
1323
+ }
1324
+ #tryUpgrade() {
1325
+ const list = this.querySelector(":scope > [popover]");
1326
+ const trigger = this.querySelector(TRIGGER);
1327
+ if (!list || !trigger) return false;
1328
+ this.#upgrade(trigger, list);
1329
+ return true;
1330
+ }
1331
+ #keep(el, name, value, into = this.#restore) {
1332
+ const was = el.getAttribute(name);
1333
+ if (value === null) el.removeAttribute(name);
1334
+ else el.setAttribute(name, value);
1335
+ into.push(() => was === null ? el.removeAttribute(name) : el.setAttribute(name, was));
1336
+ }
1337
+ #upgrade(trigger, list) {
1338
+ this.#trigger = trigger;
1339
+ this.#list = list;
1340
+ this.#hadStyle.list = list.hasAttribute("style");
1341
+ const parent = trigger.parentElement;
1342
+ this.#carrier = parent && parent !== this ? parent : null;
1343
+ const wasOpen = list.matches(":popover-open");
1344
+ this.setAttribute("data-vk", "");
1345
+ ids += 1;
1346
+ if (!list.id) this.#keep(list, "id", `vire-menu-${ids}`);
1347
+ if (!trigger.id) this.#keep(trigger, "id", `vire-menu-trigger-${ids}`);
1348
+ this.#keep(trigger, "popovertarget", null);
1349
+ this.#keep(trigger, "popovertargetaction", null);
1350
+ this.#keep(trigger, "aria-haspopup", "menu");
1351
+ this.#keep(trigger, "aria-expanded", "false");
1352
+ this.#keep(trigger, "aria-controls", list.id);
1353
+ this.#keep(list, "popover", "manual");
1354
+ this.#keep(list, "role", "menu");
1355
+ this.#keep(list, "tabindex", "-1");
1356
+ this.#keep(list, "aria-labelledby", trigger.id);
1357
+ this.#adoptItems();
1358
+ const items = new MutationObserver((records) => {
1359
+ let changed = false;
1360
+ for (const record of records) {
1361
+ if (record.type === "childList" && record.target === list) {
1362
+ changed = true;
1363
+ for (const node of record.removedNodes) this.#releaseItem(node);
1364
+ } else if (record.type === "attributes") {
1365
+ this.#keepFocusOff(record.target);
1366
+ }
1367
+ }
1368
+ if (!changed) return;
1369
+ this.#adoptItems();
1370
+ if (this.#out) {
1371
+ this.#natural = null;
1372
+ this.#place();
1373
+ }
1374
+ });
1375
+ items.observe(list, { childList: true, subtree: true, attributes: true, attributeFilter: ["disabled"] });
1376
+ this.#cleanups.push(() => items.disconnect());
1377
+ this.#listen(trigger, list);
1378
+ this.#mode = null;
1379
+ this.#refreshMode();
1380
+ if (this.#forced()) this.#applyForced();
1381
+ else if (wasOpen) this.#show("menu");
1382
+ }
1383
+ #adoptItems() {
1384
+ for (const item of this.#items()) {
1385
+ if (this.#itemRestore.has(item)) continue;
1386
+ const restore = [];
1387
+ this.#keep(item, "popovertarget", null, restore);
1388
+ this.#keep(item, "popovertargetaction", null, restore);
1389
+ this.#keep(item, "role", "menuitem", restore);
1390
+ this.#keep(item, "tabindex", "-1", restore);
1391
+ this.#itemRestore.set(item, () => {
1392
+ for (const undo of restore.reverse()) undo();
1393
+ });
1394
+ }
1395
+ }
1396
+ /** A focused item that turns natively disabled loses focus to the page; the menu keeps it instead. */
1397
+ #keepFocusOff(item) {
1398
+ const list = this.#list;
1399
+ if (!list || !this.#state.open || item.parentElement !== list || !nativelyDisabled(item)) return;
1400
+ const active = list.getRootNode().activeElement;
1401
+ if (active !== item && list.contains(active)) return;
1402
+ this.#state = { ...this.#state, active: -1 };
1403
+ list.focus({ preventScroll: true });
1404
+ }
1405
+ #releaseItem(node) {
1406
+ const restore = this.#itemRestore.get(node);
1407
+ if (!restore) return;
1408
+ this.#itemRestore.delete(node);
1409
+ restore();
1410
+ }
1411
+ #items() {
1412
+ return this.#list ? Array.from(this.#list.querySelectorAll(":scope > button")) : [];
1413
+ }
1414
+ #model() {
1415
+ return this.#items().map((item) => ({
1416
+ label: item.getAttribute("aria-label") ?? item.textContent ?? "",
1417
+ disabled: isDisabled(item),
1418
+ focusable: !nativelyDisabled(item)
1419
+ }));
1420
+ }
1421
+ #listen(trigger, list) {
1422
+ const on = (target, type, handler, capture = false) => {
1423
+ target.addEventListener(type, handler, capture);
1424
+ this.#cleanups.push(() => target.removeEventListener(type, handler, capture));
1425
+ };
1426
+ const itemOf = (target) => {
1427
+ const item = target instanceof Element ? target.closest("button") : null;
1428
+ return item && item.parentElement === list ? item : null;
1429
+ };
1430
+ const drop = (e) => {
1431
+ e.preventDefault();
1432
+ e.stopImmediatePropagation();
1433
+ };
1434
+ on(this.#carrier ?? trigger, "pointerdown", () => this.#viaPointer = true);
1435
+ on(trigger, "keydown", (e) => {
1436
+ this.#viaPointer = false;
1437
+ if (this.#forced()) return;
1438
+ if (e.repeat && ACTIVATION_KEYS.has(e.key)) {
1439
+ e.preventDefault();
1440
+ } else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1441
+ e.preventDefault();
1442
+ this.#show(e.key === "ArrowDown" ? "first" : "last");
1443
+ } else if (e.key === "Escape" && this.#state.open) {
1444
+ e.preventDefault();
1445
+ this.#close({ type: "dismiss", reason: "escape" });
1446
+ }
1447
+ });
1448
+ on(trigger, "click", (e) => {
1449
+ if (this.#forced()) return;
1450
+ const pointer = e.detail > 0 || this.#viaPointer;
1451
+ const early = e.detail > 1 || pointer && this.#readySince === null;
1452
+ if (this.#state.open && !early) this.#close({ type: "dismiss", reason: "trigger" });
1453
+ else if (!this.#state.open) this.#show(this.#viaPointer ? "menu" : "first");
1454
+ this.#viaPointer = false;
1455
+ });
1456
+ on(list, "keydown", (e) => {
1457
+ if (!this.#state.open || this.#forced()) return;
1458
+ const move = { ArrowDown: "next", ArrowUp: "previous", Home: "first", End: "last" };
1459
+ if (e.repeat && ACTIVATION_KEYS.has(e.key)) {
1460
+ e.preventDefault();
1461
+ } else if (e.key in move) {
1462
+ e.preventDefault();
1463
+ this.#step({ type: "move", to: move[e.key] });
1464
+ } else if (e.key === "Escape") {
1465
+ e.preventDefault();
1466
+ this.#close({ type: "dismiss", reason: "escape" });
1467
+ } else if (e.key === "Tab") {
1468
+ this.#close({ type: "dismiss", reason: "tab" });
1469
+ } else if (e.key.length === 1 && e.key !== " " && !e.ctrlKey && !e.metaKey && !e.altKey) {
1470
+ this.#step({ type: "type", key: e.key, at: e.timeStamp / 1e3 });
1471
+ } else if (ACTIVATION_KEYS.has(e.key)) {
1472
+ const item = itemOf(e.target);
1473
+ if (item && !isDisabled(item)) item.setAttribute("data-pressed", "");
1474
+ }
1475
+ });
1476
+ on(list, "keyup", (e) => itemOf(e.target)?.removeAttribute("data-pressed"));
1477
+ on(
1478
+ list,
1479
+ "click",
1480
+ (e) => {
1481
+ const item = itemOf(e.target);
1482
+ if (!item || this.#forced()) return;
1483
+ if (isDisabled(item)) return drop(e);
1484
+ if (!this.#state.open) {
1485
+ if (this.#out) drop(e);
1486
+ return;
1487
+ }
1488
+ const ready = this.#readySince;
1489
+ const pointerEarly = e.detail > 0 && (ready === null || this.#downAt < ready);
1490
+ if (e.detail > 1 || pointerEarly || this.#inSince === null) return drop(e);
1491
+ this.#close({ type: "choose", index: this.#items().indexOf(item) });
1492
+ },
1493
+ true
1494
+ );
1495
+ on(
1496
+ list,
1497
+ "pointerdown",
1498
+ (e) => {
1499
+ this.#downAt = e.timeStamp;
1500
+ const item = itemOf(e.target);
1501
+ if (e.button === 0 && item && !isDisabled(item) && !this.#forced()) item.setAttribute("data-pressed", "");
1502
+ },
1503
+ true
1504
+ );
1505
+ on(list, "pointermove", (e) => {
1506
+ const item = itemOf(e.target);
1507
+ if (item && this.#state.open && !this.#forced()) this.#step({ type: "point", index: this.#items().indexOf(item) });
1508
+ });
1509
+ for (const type of ["pointerup", "pointercancel", "pointerout"]) {
1510
+ on(list, type, (e) => itemOf(e.target)?.removeAttribute("data-pressed"));
1511
+ }
1512
+ on(list, "focusout", (e) => {
1513
+ const to = e.relatedTarget;
1514
+ if (!this.#state.open || !to || this.#forced()) return;
1515
+ if (list.contains(to) || trigger.contains(to) || this.#carrier?.contains(to)) return;
1516
+ this.#close({ type: "dismiss", reason: "outside" });
1517
+ });
1518
+ const roots = rootsOf(this);
1519
+ roots.forEach((root, i) => {
1520
+ const inner = i === 0 ? null : roots[i - 1].host;
1521
+ on(
1522
+ root,
1523
+ "pointerdown",
1524
+ (e) => {
1525
+ if (!this.#state.open || this.#forced()) return;
1526
+ const path = e.composedPath();
1527
+ if (inner && path.includes(inner)) return;
1528
+ if ([list, trigger, this.#carrier].some((node) => node !== null && path.includes(node))) return;
1529
+ this.#close({ type: "dismiss", reason: "outside" });
1530
+ },
1531
+ true
1532
+ );
1533
+ on(root, "scroll", (e) => this.#follow(e), true);
1534
+ });
1535
+ on(window, "resize", (e) => this.#follow(e));
1536
+ if (typeof ResizeObserver !== "undefined") {
1537
+ const resized = new ResizeObserver(() => this.#out && this.#follow());
1538
+ resized.observe(this.#carrier ?? trigger);
1539
+ this.#cleanups.push(() => resized.disconnect());
1540
+ }
1541
+ }
1542
+ /** Puts the menu back over its trigger after a scroll or a resize, once per frame. */
1543
+ #follow(e) {
1544
+ if (!this.#out || this.#placeFrame) return;
1545
+ if (e?.target instanceof Node && this.#list?.contains(e.target)) return;
1546
+ this.#placeFrame = requestAnimationFrame(() => {
1547
+ this.#placeFrame = 0;
1548
+ if (this.#out) this.#place();
1549
+ });
1550
+ }
1551
+ #step(event) {
1552
+ const step = stepMenu(this.#state, event, this.#model());
1553
+ this.#state = step.state;
1554
+ this.#applyFocus(step.focus);
1555
+ }
1556
+ #applyFocus(focus) {
1557
+ const list = this.#list;
1558
+ if (focus.to === "item") this.#items()[focus.index]?.focus({ preventScroll: true });
1559
+ else if (focus.to === "menu") list.focus({ preventScroll: true });
1560
+ else if (focus.to === "trigger") this.#trigger.focus({ preventScroll: true });
1561
+ else if (focus.to === "none") {
1562
+ const active = list.getRootNode().activeElement;
1563
+ if (active instanceof HTMLElement && list.contains(active)) active.blur();
1564
+ }
1565
+ }
1566
+ #show(focus) {
1567
+ const list = this.#list;
1568
+ const step = stepMenu(this.#state, { type: "open", focus }, this.#model());
1569
+ this.#state = step.state;
1570
+ this.#readySince = null;
1571
+ this.#inSince = null;
1572
+ clearTimeout(this.#inertTimer);
1573
+ list.inert = false;
1574
+ this.#trigger.setAttribute("aria-expanded", "true");
1575
+ this.#comeOut();
1576
+ this.#run();
1577
+ this.#applyFocus(step.focus);
1578
+ }
1579
+ #close(event) {
1580
+ const step = stepMenu(this.#state, event, this.#model());
1581
+ const closing = this.#state.open && !step.state.open;
1582
+ this.#state = step.state;
1583
+ this.#applyFocus(step.focus);
1584
+ if (!closing) return;
1585
+ this.#readySince = null;
1586
+ this.#inSince = null;
1587
+ this.#trigger.setAttribute("aria-expanded", "false");
1588
+ for (const item of this.#items()) item.removeAttribute("data-pressed");
1589
+ clearTimeout(this.#inertTimer);
1590
+ this.#inertTimer = window.setTimeout(() => {
1591
+ if (this.#list && !this.#state.open && this.#out) this.#list.inert = true;
1592
+ });
1593
+ this.#run();
1594
+ }
1595
+ #comeOut() {
1596
+ const list = this.#list;
1597
+ const wasOut = this.#out;
1598
+ if (!list.matches(":popover-open")) list.showPopover();
1599
+ this.#out = true;
1600
+ if (!wasOut) this.#natural = null;
1601
+ this.#place();
1602
+ if (wasOut) return;
1603
+ this.#carrier?.setCarried?.(1);
1604
+ this.#holdSample();
1605
+ }
1606
+ #triggerBox() {
1607
+ const trigger = this.#trigger;
1608
+ const r = (this.#carrier ?? trigger).getBoundingClientRect();
1609
+ const radius = parseFloat(getComputedStyle(trigger).borderTopLeftRadius) || 0;
1610
+ return { x: r.x, y: r.y, width: r.width, height: r.height, radius: Math.min(radius, r.width / 2, r.height / 2) };
1611
+ }
1612
+ #corner() {
1613
+ const wanted = this.getAttribute("corner");
1614
+ return wanted && Object.hasOwn(MENU_CORNERS, wanted) ? MENU_CORNERS[wanted] : void 0;
1615
+ }
1616
+ #measure() {
1617
+ const list = this.#list;
1618
+ this.setAttribute("data-measure", "");
1619
+ const natural = { width: list.offsetWidth, height: list.offsetHeight };
1620
+ this.removeAttribute("data-measure");
1621
+ return natural;
1622
+ }
1623
+ #place() {
1624
+ const list = this.#list;
1625
+ const spec = menuSize(this.#size());
1626
+ this.#natural ??= this.#measure();
1627
+ const natural = this.#natural;
1628
+ const viewport = { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight };
1629
+ const trigger = this.#triggerBox();
1630
+ const placement = menuPlacement(trigger, natural, viewport, { corner: this.#corner(), margin: VIEWPORT_MARGIN });
1631
+ this.#from = trigger;
1632
+ this.#to = { ...placement.rect, radius: spec.radius };
1633
+ this.#anchor = placement.anchor;
1634
+ list.style.setProperty("--vk-content-w", `${placement.rect.width - 2 * spec.padding}px`);
1635
+ list.toggleAttribute("data-scroll", placement.rect.height < natural.height);
1636
+ this.#paint();
1637
+ }
1638
+ #holdSample() {
1639
+ const glass = this.#glass;
1640
+ if (!glass || !this.#to) return;
1641
+ let sample = this.#sample;
1642
+ if (!sample) {
1643
+ this.#setBox(this.#to);
1644
+ sample = glass.probe();
1645
+ }
1646
+ this.#held = sample;
1647
+ glass.setSample(sample);
1648
+ this.#paint();
1649
+ }
1650
+ #setBox(box) {
1651
+ const s = this.#list.style;
1652
+ const px = (v) => `${v.toFixed(2)}px`;
1653
+ s.setProperty("--vk-x", px(box.x));
1654
+ s.setProperty("--vk-y", px(box.y));
1655
+ s.setProperty("--vk-w", px(box.width));
1656
+ s.setProperty("--vk-h", px(box.height));
1657
+ s.setProperty("--vk-r", px(box.radius));
1658
+ }
1659
+ #run() {
1660
+ const instant = this.#presentation?.instantSprings ?? false;
1661
+ if (instant) stepGrowth(this.#growth, 0, this.#state.open, true);
1662
+ this.#paint();
1663
+ if (instant) {
1664
+ this.#settle();
1665
+ return;
1666
+ }
1667
+ if (this.#frame) return;
1668
+ this.#lastFrame = 0;
1669
+ this.#frame = requestAnimationFrame((t) => this.#tick(t));
1670
+ }
1671
+ #tick(now) {
1672
+ this.#frame = 0;
1673
+ if (!this.isConnected || this.#forced()) return;
1674
+ const dt = this.#lastFrame ? Math.min((now - this.#lastFrame) / 1e3, 0.05) : 1 / 60;
1675
+ this.#lastFrame = now;
1676
+ const instant = this.#presentation?.instantSprings ?? false;
1677
+ const moving = stepGrowth(this.#growth, dt, this.#state.open, instant);
1678
+ this.#paint();
1679
+ if (moving) this.#frame = requestAnimationFrame((t) => this.#tick(t));
1680
+ else this.#settle();
1681
+ }
1682
+ #settle() {
1683
+ if (this.#state.open) {
1684
+ this.#paint();
1685
+ return;
1686
+ }
1687
+ const list = this.#list;
1688
+ if (list.matches(":popover-open")) list.hidePopover();
1689
+ clearTimeout(this.#inertTimer);
1690
+ list.inert = false;
1691
+ list.removeAttribute("data-scroll");
1692
+ for (const name of Array.from(list.style)) if (name.startsWith("--vk-")) list.style.removeProperty(name);
1693
+ this.removeAttribute("data-phase");
1694
+ this.#out = false;
1695
+ this.#held = null;
1696
+ this.#from = this.#to = this.#anchor = null;
1697
+ this.#glass?.setSample(this.#sample);
1698
+ this.#carrier?.setCarried?.(null);
1699
+ }
1700
+ #paint() {
1701
+ const list = this.#list;
1702
+ if (!list || !this.#out || !this.#from || !this.#to || !this.#anchor) return;
1703
+ const phase = growthPhase(this.#growth);
1704
+ const f = menuSurfaceFrame(this.#from, this.#to, this.#anchor, phase);
1705
+ const settled = phase.shrink === 1 && phase.grow === 1;
1706
+ const inkIn = Math.round(f.ink.incoming * 1e3) / 1e3;
1707
+ if (this.#state.open && inkIn >= 1) {
1708
+ this.#inSince ??= performance.now();
1709
+ if (this.#growth.grow.v <= 0) this.#readySince ??= performance.now();
1710
+ }
1711
+ this.setAttribute("data-phase", settled ? "open" : "moving");
1712
+ this.#setBox(f.box);
1713
+ const s = list.style;
1714
+ s.setProperty("--vk-cx", `${f.content.x.toFixed(2)}px`);
1715
+ s.setProperty("--vk-cy", `${f.content.y.toFixed(2)}px`);
1716
+ s.setProperty("--vk-ink-in", inkIn.toFixed(3));
1717
+ const body = menuBody(vireglass.VIREGLASS_CONTROL_MATERIAL, f.activity, this.#presentation?.frost ?? false);
1718
+ s.setProperty("--vk-body-alpha", body.alpha.toFixed(3));
1719
+ s.setProperty("--vk-body-white", body.white.toFixed(3));
1720
+ s.setProperty("--vk-body-lift", body.lift.toFixed(3));
1721
+ this.#carrier?.setCarried?.(f.ink.outgoing);
1722
+ this.#pushGlass(f.box, f.morph, f.activity);
1723
+ }
1724
+ /** Hands the lens only what changed: a new morph or material rebuilds its filter, a moved box needs nothing. */
1725
+ #pushGlass(box, morph, activity) {
1726
+ const glass = this.#glass;
1727
+ if (!glass) return;
1728
+ const round = Math.round;
1729
+ const step = round(Math.min(Math.max(activity, 0), 1) * MATERIAL_STEPS2) / MATERIAL_STEPS2;
1730
+ const geometry = { width: round(box.width), height: round(box.height), cornerRadius: round(box.radius) };
1731
+ const lens = morph ? {
1732
+ smoothing: round(morph.smoothing),
1733
+ shape: {
1734
+ offsetX: round(morph.offsetX),
1735
+ offsetY: round(morph.offsetY),
1736
+ width: round(morph.width),
1737
+ height: round(morph.height),
1738
+ cornerRadius: round(morph.cornerRadius)
1739
+ }
1740
+ } : null;
1741
+ const key = {
1742
+ material: String(step),
1743
+ morph: lens ? JSON.stringify(lens) : "none",
1744
+ geometry: `${geometry.width}x${geometry.height}r${geometry.cornerRadius}`
1745
+ };
1746
+ const last2 = this.#lastLens;
1747
+ if (key.material === last2.material && key.morph === last2.morph && key.geometry === last2.geometry) return;
1748
+ const next = {};
1749
+ if (key.geometry !== last2.geometry) next.geometry = geometry;
1750
+ if (key.material !== last2.material) next.material = menuMaterial(vireglass.VIREGLASS_CONTROL_MATERIAL, step);
1751
+ if (key.morph !== last2.morph) next.morph = lens;
1752
+ this.#lastLens = key;
1753
+ glass.frame(next);
1754
+ }
1755
+ #forced() {
1756
+ const wanted = this.getAttribute(FORCE_ATTRIBUTE2);
1757
+ return wanted !== null && (Object.hasOwn(FORCED2, wanted) || FORCED_PHASE.test(wanted)) ? wanted : null;
1758
+ }
1759
+ #applyForced() {
1760
+ const forced = this.#forced();
1761
+ const [shrink, grow] = (forced ?? "").split(" ").map(Number);
1762
+ const phase = !forced ? null : Object.hasOwn(FORCED2, forced) ? FORCED2[forced] : { shrink, grow };
1763
+ cancelAnimationFrame(this.#frame);
1764
+ this.#frame = 0;
1765
+ if (!phase) {
1766
+ this.#state = MENU_CLOSED;
1767
+ this.#growth = createGrowthState();
1768
+ this.#trigger.setAttribute("aria-expanded", "false");
1769
+ if (this.#out) this.#settle();
1770
+ return;
1771
+ }
1772
+ this.#state = { ...MENU_CLOSED, open: forced !== "retracting" };
1773
+ this.#growth = {
1774
+ shrink: { x: phase.shrink, v: 0, target: phase.shrink },
1775
+ grow: { x: phase.grow, v: 0, target: phase.grow }
1776
+ };
1777
+ this.#trigger.setAttribute("aria-expanded", String(this.#state.open));
1778
+ this.#list.inert = true;
1779
+ this.#comeOut();
1780
+ }
1781
+ #size() {
1782
+ const wanted = this.getAttribute("size");
1783
+ if (wanted && CONTROL_SIZES.includes(wanted)) return wanted;
1784
+ return defaultSize({ touch: isCoarsePointer() });
1785
+ }
1786
+ #wantsGlass() {
1787
+ const wanted = this.getAttribute("glass");
1788
+ return wanted !== null && GLASS_ON.has(wanted);
1789
+ }
1790
+ #refreshMode(force = false, afterFailure = false) {
1791
+ if (!afterFailure) this.#glassBroken = false;
1792
+ const mode = effectiveA11y(this.#own);
1793
+ let next = presentationFor(mode, this.#wantsGlass());
1794
+ if (this.#glassBroken && next.glass) next = { ...next, glass: false, surface: next.frost ? "frosted" : "plain" };
1795
+ const changed = force || !this.#mode || !sameA11y(this.#mode, mode) || next.surface !== this.#presentation?.surface;
1796
+ this.#mode = mode;
1797
+ this.#presentation = next;
1798
+ this.#applySize();
1799
+ this.setAttribute("data-surface", next.surface);
1800
+ this.toggleAttribute("data-contrast", next.contrastBorder);
1801
+ this.toggleAttribute("data-frost", next.frost);
1802
+ this.setAttribute("data-glass", next.glass ? "on" : "off");
1803
+ if (changed) this.#reattachGlass(mode);
1804
+ if (this.#out) {
1805
+ this.#natural = null;
1806
+ this.#place();
1807
+ }
1808
+ }
1809
+ #applySize() {
1810
+ const spec = menuSize(this.#size());
1811
+ const set = (name, value) => this.style.setProperty(name, value);
1812
+ set("--vk-menu-row", `${spec.row}px`);
1813
+ set("--vk-menu-pad", `${spec.padding}px`);
1814
+ set("--vk-menu-radius", `${spec.radius}px`);
1815
+ set("--vk-item-radius", `${spec.itemRadius}px`);
1816
+ set("--vk-menu-icon", `${spec.icon}px`);
1817
+ set("--vk-label-pad", `${spec.labelPad}px`);
1818
+ set("--vk-fill-active", String(MENU_ITEM_FILL.active));
1819
+ set("--vk-fill-pressed", String(MENU_ITEM_FILL.pressed));
1820
+ this.setAttribute("data-size", this.#size());
1821
+ }
1822
+ #dropGlass() {
1823
+ this.#glassToken += 1;
1824
+ this.#glass?.destroy();
1825
+ this.#glass = null;
1826
+ this.#glassKey = "";
1827
+ this.#lastLens = { material: "", morph: "", geometry: "" };
1828
+ }
1829
+ #reattachGlass(mode) {
1830
+ const list = this.#list;
1831
+ const want = this.#presentation?.glass === true && list !== null;
1832
+ const key = want ? `${JSON.stringify(mode)}|${this.#size()}` : "";
1833
+ if (key === this.#glassKey) return;
1834
+ this.#dropGlass();
1835
+ this.#glassKey = key;
1836
+ if (!want || !list) return;
1837
+ const token = this.#glassToken;
1838
+ const box = this.#from ?? this.#triggerBox();
1839
+ void attachControlGlass(
1840
+ list,
1841
+ {
1842
+ material: menuMaterial(vireglass.VIREGLASS_CONTROL_MATERIAL, 0),
1843
+ geometry: { width: Math.round(box.width), height: Math.round(box.height), cornerRadius: menuSize(this.#size()).radius },
1844
+ accessibility: mode,
1845
+ onTouch: false,
1846
+ sample: this.#held ?? this.#sample,
1847
+ interactive: false
1848
+ },
1849
+ () => token === this.#glassToken && this.isConnected
1850
+ ).then((session) => {
1851
+ if (!session) return;
1852
+ this.#glass = session;
1853
+ this.#lastLens = { material: "", morph: "", geometry: "" };
1854
+ if (this.#out) this.#holdSample();
1855
+ this.dispatchEvent(new CustomEvent("vire-glass-ready"));
1856
+ }).catch(() => {
1857
+ if (token !== this.#glassToken) return;
1858
+ this.#glassKey = "";
1859
+ this.#glassBroken = true;
1860
+ this.#refreshMode(false, true);
1861
+ });
1862
+ }
1863
+ };
1864
+ }
1865
+
1866
+ // src/web/index.ts
1867
+ var BUTTON_TAG = "vire-button";
1868
+ var ICON_BUTTON_TAG = "vire-icon-button";
1869
+ var MENU_TAG = "vire-menu";
1870
+ function defineVireElements(options = {}) {
1871
+ const registry = options.registry ?? (typeof customElements === "undefined" ? void 0 : customElements);
1872
+ if (!registry) return;
1873
+ const pairs = [
1874
+ [BUTTON_TAG, "button"],
1875
+ [ICON_BUTTON_TAG, "iconButton"]
1876
+ ];
1877
+ for (const [tag, kind] of pairs) {
1878
+ if (!registry.get(tag)) registry.define(tag, createControlElementClass(kind, { nonce: options.nonce }));
1879
+ }
1880
+ if (!registry.get(MENU_TAG)) registry.define(MENU_TAG, createMenuElementClass({ nonce: options.nonce }));
1881
+ }
1882
+
1883
+ exports.BUTTON_TAG = BUTTON_TAG;
1884
+ exports.ICON_BUTTON_TAG = ICON_BUTTON_TAG;
1885
+ exports.MENU_TAG = MENU_TAG;
1886
+ exports.STYLES = STYLES;
1887
+ exports.defineVireElements = defineVireElements;
1888
+ exports.getA11yOverride = getA11yOverride;
1889
+ exports.setA11yOverride = setA11yOverride;
1890
+ exports.systemA11y = systemA11y;
1891
+ //# sourceMappingURL=web.cjs.map
1892
+ //# sourceMappingURL=web.cjs.map