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/README.md +56 -1
- package/dist/{chunk-I3BD3XGC.js → chunk-AR2AM3BR.js} +179 -14
- package/dist/chunk-AR2AM3BR.js.map +1 -0
- package/dist/index.cjs +190 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -4
- package/dist/index.d.ts +165 -4
- package/dist/index.js +1 -1
- package/dist/web.cjs +1087 -21
- package/dist/web.cjs.map +1 -1
- package/dist/web.d.cts +12 -2
- package/dist/web.d.ts +12 -2
- package/dist/web.js +786 -14
- package/dist/web.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-I3BD3XGC.js.map +0 -1
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
|
|
53
|
-
return Math.max(
|
|
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
|
|
66
|
-
let alpha =
|
|
67
|
-
let white =
|
|
68
|
-
let 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,
|
|
95
|
+
function stepPress(state, event, enabled2) {
|
|
96
96
|
switch (event.type) {
|
|
97
97
|
case "down":
|
|
98
|
-
if (!
|
|
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:
|
|
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
|
-
|
|
371
|
-
|
|
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{
|
|
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
|
-
|
|
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;
|