vireuikit 0.6.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 CHANGED
@@ -49,8 +49,8 @@ var DISABLED_PRESENCE = 0.25;
49
49
  var HOVER_GAIN = 0.35;
50
50
  function activation(state) {
51
51
  if (state.disabled) return 0;
52
- const clamp = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
53
- return Math.max(clamp(state.selected), clamp(state.pressed), clamp(state.hovered) * HOVER_GAIN);
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
54
  }
55
55
  function atomMaterial(base, kind, state = ATOM_AT_REST) {
56
56
  const out = vireglass.activeMaterial(base, activation(state));
@@ -62,10 +62,10 @@ var PRESSED_BODY = { alpha: 0.34, white: 0.35, lift: 0 };
62
62
  var FROST_LIFT = 0.26;
63
63
  function atomBody(base, state = ATOM_AT_REST, frost = false) {
64
64
  const on = activation(state);
65
- const mix = (key) => REST_BODY[key] + (PRESSED_BODY[key] - REST_BODY[key]) * on;
66
- let alpha = mix("alpha");
67
- let white = mix("white");
68
- let lift = mix("lift");
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
69
  if (state.disabled) alpha *= DISABLED_PRESENCE;
70
70
  if (frost) {
71
71
  const density = vireglass.applyAccessibility(vireglass.resolveOptics(base), {
@@ -92,14 +92,14 @@ function stepHover(event, canHover) {
92
92
 
93
93
  // src/kit/behavior/press.ts
94
94
  var PRESS_IDLE = { by: null };
95
- function stepPress(state, event, enabled) {
95
+ function stepPress(state, event, enabled2) {
96
96
  switch (event.type) {
97
97
  case "down":
98
- if (!enabled || state.by !== null) return { state, activated: false };
98
+ if (!enabled2 || state.by !== null) return { state, activated: false };
99
99
  return { state: { by: event.source }, activated: false };
100
100
  case "up":
101
101
  if (state.by !== event.source) return { state, activated: false };
102
- return { state: PRESS_IDLE, activated: enabled };
102
+ return { state: PRESS_IDLE, activated: enabled2 };
103
103
  case "cancel":
104
104
  return { state: PRESS_IDLE, activated: false };
105
105
  case "leave":
@@ -178,6 +178,137 @@ function stepSpring(s, dt, response, damping, instant = false) {
178
178
  }
179
179
  return true;
180
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
+ }
181
312
 
182
313
  // src/kit/behavior/visual.ts
183
314
  var createControlVisual = () => ({
@@ -244,6 +375,19 @@ function iconButtonSize(size) {
244
375
  const spec = BUTTON_SIZES[size];
245
376
  return { ...spec, width: spec.height, paddingX: 0 };
246
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
+ }
247
391
  function defaultSize(input) {
248
392
  return input.touch ? "touch" : "medium";
249
393
  }
@@ -301,7 +445,7 @@ function effectiveA11y(own) {
301
445
 
302
446
  // src/web/glass.ts
303
447
  async function attachControlGlass(el, params, stillWanted) {
304
- const { attachGlass } = await import('vireglass/dom');
448
+ const { attachGlass, probeBackdrop } = await import('vireglass/dom');
305
449
  if (!stillWanted()) return null;
306
450
  const opts = {
307
451
  material: params.material,
@@ -312,6 +456,7 @@ async function attachControlGlass(el, params, stillWanted) {
312
456
  // near black under reduced transparency with light ink (docs/acceptance.md, "Resting body").
313
457
  body: false
314
458
  };
459
+ if (params.interactive === false) opts.interactive = false;
315
460
  if (params.sample) opts.sample = params.sample;
316
461
  const handle = attachGlass(el, opts);
317
462
  return {
@@ -323,9 +468,16 @@ async function attachControlGlass(el, params, stillWanted) {
323
468
  opts.geometry = geometry;
324
469
  handle.update();
325
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
+ },
326
477
  setAppear: (t) => handle.setAppear(t),
327
478
  setMorph: (morph) => handle.setMorph(morph),
328
479
  setSample: (sample) => handle.setSample(sample),
480
+ probe: () => probeBackdrop(el),
329
481
  destroy: () => handle.destroy()
330
482
  };
331
483
  }
@@ -346,30 +498,37 @@ function watchCoarsePointer(onChange) {
346
498
  // src/web/styles.ts
347
499
  var HOST = ":is(vire-button,vire-icon-button)[data-vk]";
348
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)";
349
509
  var STYLES = `
350
- vire-button,vire-icon-button{display:inline-flex;vertical-align:middle}
351
- ${HOST}{position:relative;--vk-press:0;--vk-hover:0;--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}
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}
352
512
  ${HOST}::before{content:'';position:absolute;inset:calc(var(--vk-slop-y)*-1px) calc(var(--vk-slop-x)*-1px)}
353
513
  ${BODY}{appearance:none;-webkit-appearance:none;box-sizing:border-box;margin:0;border:0;outline:none;
354
514
  font:inherit;font-weight:500;line-height:1;white-space:nowrap;color:inherit;
355
515
  display:inline-flex;align-items:center;justify-content:center;gap:8px;position:relative;
356
516
  height:var(--vk-height);min-width:var(--vk-height);padding:0 var(--vk-pad-x);border-radius:var(--vk-radius);
357
- -webkit-text-fill-color:color-mix(in srgb,currentColor calc((1 - var(--vk-press)*.55)*100%),transparent);
358
- text-shadow:0 0 calc(var(--vk-press)*var(--vk-defocus)*1px) currentColor;
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);
359
519
  background:transparent;cursor:pointer;touch-action:manipulation;user-select:none;-webkit-tap-highlight-color:transparent;
360
520
  transform:scale(calc(1 + var(--vk-press)*var(--vk-grow)))}
361
521
  vire-icon-button[data-vk]>button{padding:0;width:var(--vk-height)}
362
522
  ${BODY}>svg{width:var(--vk-icon);height:var(--vk-icon);flex:none;
363
- opacity:calc(1 - var(--vk-press)*.45);filter:blur(calc(var(--vk-press)*var(--vk-defocus)*1px))}
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))}
364
524
  ${BODY}:disabled{cursor:default}
365
525
  ${HOST}[data-disabled]>button{-webkit-text-fill-color:color-mix(in srgb,currentColor 40%,transparent)}
366
526
  ${HOST}[data-disabled]>button>svg{opacity:.4}
367
527
  ${HOST}[data-focus]>button{outline:2px solid currentColor;outline-offset:2px}
368
528
 
369
529
  ${HOST}[data-surface=glass]>button{color:var(--vireglass-ink-color,inherit);
370
- 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);
371
- 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))}
372
- ${HOST}[data-frost][data-surface=glass]>button{color:oklab(from var(--vireglass-tint-color,#fff) calc(.95 - .87*clamp(0,(l + var(--vk-body-lift) - .6)*20,1)) 0 0)}
530
+ ${GLASS_BODY}}
531
+ ${HOST}[data-frost][data-surface=glass]>button{${FROST_INK}}
373
532
 
374
533
  ${HOST}[data-surface=plain]>button{background:color-mix(in srgb,currentColor calc(10% + var(--vk-hover)*6% + var(--vk-press)*14%),transparent)}
375
534
 
@@ -378,19 +537,64 @@ ${HOST}[data-surface=frosted]>button{
378
537
  backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
379
538
  box-shadow:0 1px 3px color-mix(in srgb,currentColor 22%,transparent)}
380
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
+
381
574
  @supports not (color:oklab(from red l a b)){
382
- ${HOST}[data-frost][data-surface=glass]>button{color:inherit;background:color-mix(in srgb,Canvas 82%,currentColor)}
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)}
383
576
  }
384
577
 
385
578
  ${HOST}[data-contrast]:is([data-surface=plain],[data-surface=frosted])>button{box-shadow:inset 0 0 0 2px currentColor}
386
579
  ${HOST}[data-contrast][data-surface=frosted]>button{background:Canvas}
387
- ${HOST}[data-contrast][data-surface=glass]>button{background:color-mix(in srgb,color-mix(in srgb,#fff calc((1 - var(--vireglass-ink,0))*100%),#000) 88%,transparent)}
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}
388
586
 
389
587
  @media (forced-colors:active){
390
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}
391
589
  ${BODY}>svg{opacity:1;filter:none}
392
590
  ${HOST}[data-disabled]>button{-webkit-text-fill-color:currentColor;color:GrayText;border-color:GrayText}
393
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)}
394
598
  }
395
599
  `;
396
600
  var STYLE_ATTR = "data-vireuikit";
@@ -440,6 +644,8 @@ function createControlElementClass(kind, options = {}) {
440
644
  #slop = { x: 0, y: 0 };
441
645
  #morph = null;
442
646
  #sample = null;
647
+ #carried = null;
648
+ #hadStyle = { host: false, control: false };
443
649
  get a11y() {
444
650
  return this.#own;
445
651
  }
@@ -458,6 +664,14 @@ function createControlElementClass(kind, options = {}) {
458
664
  this.#morph = morph;
459
665
  this.#glass?.setMorph(morph);
460
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
+ }
461
675
  connectedCallback() {
462
676
  for (const key of ["a11y", "sample"]) {
463
677
  if (!Object.prototype.hasOwnProperty.call(this, key)) continue;
@@ -465,6 +679,7 @@ function createControlElementClass(kind, options = {}) {
465
679
  Reflect.deleteProperty(this, key);
466
680
  Reflect.set(this, key, value);
467
681
  }
682
+ this.#hadStyle.host = this.hasAttribute("style");
468
683
  ensureStyles(this, options.nonce);
469
684
  this.#cleanups.push(watchCoarsePointer(() => this.#onInputChange()));
470
685
  this.#cleanups.push(watchSystemA11y(() => this.#refreshMode()));
@@ -477,11 +692,14 @@ function createControlElementClass(kind, options = {}) {
477
692
  clearTimeout(this.#suppressTimer);
478
693
  this.#frame = 0;
479
694
  this.#dropGlass();
695
+ if (this.#control && !this.#hadStyle.control && this.#control.getAttribute("style") === "") this.#control.removeAttribute("style");
480
696
  this.#control = null;
481
697
  this.#state = CONTROL_IDLE;
482
698
  this.#suppressClick = false;
483
- for (const name of ["data-vk", "data-surface", "data-glass", "data-frost", "data-size"]) this.removeAttribute(name);
699
+ this.#carried = null;
700
+ for (const name of ["data-vk", "data-surface", "data-glass", "data-frost", "data-size", "data-carried"]) this.removeAttribute(name);
484
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");
485
703
  this.#presentation = null;
486
704
  this.#mode = null;
487
705
  }
@@ -516,6 +734,7 @@ function createControlElementClass(kind, options = {}) {
516
734
  }
517
735
  #upgrade(control) {
518
736
  this.#control = control;
737
+ this.#hadStyle.control = control.hasAttribute("style");
519
738
  this.setAttribute("data-vk", "");
520
739
  this.#caps = isCoarsePointer() ? TOUCH_INPUT : POINTER_INPUT;
521
740
  this.#listen(control);
@@ -743,6 +962,7 @@ function createControlElementClass(kind, options = {}) {
743
962
  if (!session) return;
744
963
  this.#glass = session;
745
964
  if (this.#morph) session.setMorph(this.#morph);
965
+ if (this.#carried !== null) session.setAppear(0);
746
966
  if (this.#sample !== sample) session.setSample(this.#sample);
747
967
  this.#pushMaterial(true);
748
968
  this.dispatchEvent(new CustomEvent("vire-glass-ready"));
@@ -799,10 +1019,854 @@ function createControlElementClass(kind, options = {}) {
799
1019
  }
800
1020
  };
801
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
+ }
802
1865
 
803
1866
  // src/web/index.ts
804
1867
  var BUTTON_TAG = "vire-button";
805
1868
  var ICON_BUTTON_TAG = "vire-icon-button";
1869
+ var MENU_TAG = "vire-menu";
806
1870
  function defineVireElements(options = {}) {
807
1871
  const registry = options.registry ?? (typeof customElements === "undefined" ? void 0 : customElements);
808
1872
  if (!registry) return;
@@ -813,10 +1877,12 @@ function defineVireElements(options = {}) {
813
1877
  for (const [tag, kind] of pairs) {
814
1878
  if (!registry.get(tag)) registry.define(tag, createControlElementClass(kind, { nonce: options.nonce }));
815
1879
  }
1880
+ if (!registry.get(MENU_TAG)) registry.define(MENU_TAG, createMenuElementClass({ nonce: options.nonce }));
816
1881
  }
817
1882
 
818
1883
  exports.BUTTON_TAG = BUTTON_TAG;
819
1884
  exports.ICON_BUTTON_TAG = ICON_BUTTON_TAG;
1885
+ exports.MENU_TAG = MENU_TAG;
820
1886
  exports.STYLES = STYLES;
821
1887
  exports.defineVireElements = defineVireElements;
822
1888
  exports.getA11yOverride = getA11yOverride;