@kitlangton/knobs 0.4.0-next.12 → 0.4.0-next.14

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/panel.js CHANGED
@@ -3099,7 +3099,7 @@ ${GRAD_PROPS}
3099
3099
 
3100
3100
  /* The chip: a round cog button, or a capsule with a label beside the cog. */
3101
3101
  .knobs-chip{position:fixed;z-index:2147480000;display:inline-flex;align-items:center;justify-content:center;gap:5px;width:26px;height:26px;padding:0;border-radius:999px;cursor:pointer;background:var(--_k-bg);border:1px solid transparent;
3102
- color:var(--_k-fg-2);font-size:var(--_k-text-control);line-height:1;letter-spacing:.01em;white-space:nowrap;--k-chip-edge:var(--_k-border-subtle);box-shadow:0 0 0 var(--k-rim-width) var(--k-chip-edge),0 4px 16px rgba(0,0,0,calc(.35 * var(--_k-shadow-k,1))),inset 0 var(--k-rim-width) 0 var(--_k-rim);transition:color .15s,border-color .15s,transform .2s var(--k-ease),opacity .2s,scale .12s var(--k-ease),${TOKEN_TRANSITIONS};}
3102
+ color:var(--_k-fg-2);font-size:var(--_k-text-control);line-height:1;letter-spacing:.01em;white-space:nowrap;--k-chip-edge:var(--_k-border-subtle);box-shadow:0 calc(var(--k-rim-width) * -1) 0 var(--_k-rim),0 0 0 var(--k-rim-width) var(--k-chip-edge),0 4px 16px rgba(0,0,0,calc(.35 * var(--_k-shadow-k,1)));transition:color .15s,border-color .15s,transform .2s var(--k-ease),opacity .2s,scale .12s var(--k-ease),${TOKEN_TRANSITIONS};}
3103
3103
  .knobs-chip[data-label]{width:auto;padding:0 10px 0 7px}
3104
3104
  .knobs-chip .knobs-icon{display:block;flex:none;width:15px;height:15px}
3105
3105
  .knobs-chip:hover{color:var(--_k-fg);--k-chip-edge:var(--_k-border-strong)}
@@ -3126,9 +3126,10 @@ ${GRAD_PROPS}
3126
3126
  /* Panel material: --k-glass (0 opaque … 1 clearest) thins the surface over a blurred, saturated
3127
3127
  backdrop, toned toward the theme (--k-glass-tone dims it under dark panels) so text keeps its
3128
3128
  contrast. Its edge is a hairline (--k-edge, --k-rim-width wide) and a hairline of light (--k-rim)
3129
- catches its top. */
3129
+ catches its top. The light rides on the edge (a shadow raised by its width, so only its top
3130
+ shows), not inside the card's 1px border, where it read as a second edge. */
3130
3131
  .knobs-panel{-webkit-user-select:none;user-select:none;transition:${TOKEN_TRANSITIONS},--k-armed ${ARMED_T};position:fixed;z-index:2147480001;width:var(--_k-width);max-width:calc(100vw - 16px);max-height:calc(100dvh - 16px);overflow:auto;padding:var(--_k-pad);
3131
- background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-panel);box-shadow:var(--_k-shadow-overlay),0 0 0 var(--k-rim-width) var(--ka-edge),inset 0 var(--k-rim-width) 0 var(--_k-rim),var(--ka-glow);scrollbar-width:thin;
3132
+ background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-panel);box-shadow:var(--_k-shadow-overlay),0 calc(var(--k-rim-width) * -1) 0 var(--_k-rim),0 0 0 var(--k-rim-width) var(--ka-edge),var(--ka-glow);scrollbar-width:thin;
3132
3133
  /* Armed (focus inside: keys come here), the edge takes the accent and a faint glow lifts it. */
3133
3134
  --ka-edge:color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-edge) * 100%),var(--_k-border-subtle));
3134
3135
  --ka-glow:0 0 calc(var(--k-armed-glow) * 1px) color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-glow-a) * var(--k-chrome,1) * 100%),transparent);
@@ -3141,7 +3142,7 @@ ${GRAD_PROPS}
3141
3142
  .knobs-panel[data-morphing]{background:transparent;border-color:transparent;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;animation:none}
3142
3143
  .knobs-morph{position:fixed;left:0;top:0;z-index:2147480001;pointer-events:none;box-sizing:border-box;--p:0;--spec:0;
3143
3144
  background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20% * var(--p)),transparent);border:1px solid transparent;
3144
- box-shadow:0 0 0 var(--k-rim-width) var(--_k-border-subtle),inset 0 0 0 var(--k-rim-width) rgb(255 255 255/var(--spec)),inset 0 var(--k-rim-width) 0 color-mix(in srgb,var(--_k-rim) calc(var(--p) * 100%),transparent);
3145
+ box-shadow:0 calc(var(--k-rim-width) * -1) 0 color-mix(in srgb,var(--_k-rim) calc(var(--p) * 100%),transparent),0 0 0 var(--k-rim-width) var(--_k-border-subtle),inset 0 0 0 var(--k-rim-width) rgb(255 255 255/var(--spec));
3145
3146
  --k-backdrop:blur(calc(var(--_k-glass) * 24px)) saturate(calc(1 + var(--_k-glass) * .8)) brightness(calc(1 + (var(--_k-glass-tone) - 1) * var(--_k-glass)));
3146
3147
  -webkit-backdrop-filter:var(--k-backdrop);backdrop-filter:var(--k-backdrop)}
3147
3148
  .knobs-morph::before,.knobs-morph::after{content:"";position:absolute;inset:-1px;border-radius:inherit;pointer-events:none}
@@ -3149,7 +3150,7 @@ ${GRAD_PROPS}
3149
3150
  .knobs-morph::after{box-shadow:var(--_k-shadow-overlay);opacity:var(--p)}
3150
3151
  /* Meanwhile the chip is just its label, fading on the shape. */
3151
3152
  .knobs-chip.knobs-chip[data-morph]{visibility:visible;pointer-events:auto;transform:none;transition:none;background:transparent;border-color:transparent;box-shadow:none}
3152
- .knobs-panel.knobs-inline{position:relative;z-index:auto;left:auto!important;top:auto!important;max-width:100%;max-height:none;box-shadow:0 0 0 var(--k-rim-width) color-mix(in srgb,var(--ka-edge) calc(var(--k-chrome) * 100%),transparent),inset 0 var(--k-rim-width) 0 color-mix(in srgb,var(--_k-rim) calc(var(--k-chrome) * 100%),transparent),var(--ka-glow);animation:none;
3153
+ .knobs-panel.knobs-inline{position:relative;z-index:auto;left:auto!important;top:auto!important;max-width:100%;max-height:none;box-shadow:0 calc(var(--k-rim-width) * -1) 0 color-mix(in srgb,var(--_k-rim) calc(var(--k-chrome) * 100%),transparent),0 0 0 var(--k-rim-width) color-mix(in srgb,var(--ka-edge) calc(var(--k-chrome) * 100%),transparent),var(--ka-glow);animation:none;
3153
3154
  /* --k-chrome 0: an embedded panel sheds its card (background, border, rim) for the host's column,
3154
3155
  border width too, so its rows line up with the column's edges. */
3155
3156
  border-width:calc(var(--k-chrome) * 1px);
@@ -3302,9 +3303,16 @@ ${GRAD_PROPS}
3302
3303
  /* The active preset's modified mark: the values have drifted off it (a row's dot, in its corner). */
3303
3304
  .knobs-panel .knobs-preset-chip::after{content:"";position:absolute;top:4px;right:4px;width:4px;height:4px;border-radius:50%;background:var(--_k-modified);opacity:0;transform:scale(.4);pointer-events:none;transition:opacity .16s ease-out,transform .28s cubic-bezier(.34,1.5,.64,1)}
3304
3305
  .knobs-panel .knobs-preset-chip[data-drifted]::after{opacity:.95;transform:none}
3306
+ /* Digits that stay on the chips (hints: { digits: "always" }): quiet, in a column of their own. */
3307
+ .knobs-panel .knobs-preset-key{display:inline-block;margin-right:6px;color:var(--_k-fg-3);font-size:var(--_k-text-meta);font-weight:var(--_k-weight-label);font-variant-numeric:tabular-nums}
3308
+ /* A group's own presets at the top of its card: the chips sit on the card, with no tray. */
3309
+ .knobs-panel .knobs-card>.knobs-presets{margin:0;padding:max(4px,calc((var(--_k-row) - var(--k-button-h,var(--_k-control-h))) / 2)) var(--_k-inset);background:transparent;border-radius:0}
3310
+ .knobs-panel .knobs-card>.knobs-presets[data-display="list"]{padding-inline:0}
3311
+ .knobs-card>.knobs-presets:hover{background:transparent}
3305
3312
  /* Naming a saved preset: a field over its chip that opens out from the middle to the chip's width;
3306
3313
  the chip's own name hides beneath it. Leaving, it fades. */
3307
3314
  .knobs-panel .knobs-preset-chip[data-naming]{color:transparent!important}
3315
+ .knobs-panel .knobs-preset-chip[data-naming] .knobs-preset-key{color:transparent}
3308
3316
  .knobs-panel .knobs-preset-name{position:absolute;z-index:2;box-sizing:border-box;min-width:0;margin:0;padding:0 6px;border:1px solid color-mix(in srgb,var(--_k-accent) 55%,transparent);border-radius:var(--k-button-radius,var(--_k-radius-md));outline:none;background:color-mix(in srgb,var(--_k-accent) 10%,transparent);color:var(--_k-fg);caret-color:var(--_k-accent);
3309
3317
  font-family:inherit;font-size:var(--k-button-font-size,var(--_k-text-control));font-weight:var(--_k-weight-title);letter-spacing:inherit;text-align:center;animation:knobs-name-in .25s cubic-bezier(.2,.8,.2,1);transition:opacity .11s cubic-bezier(.4,0,1,1)}
3310
3318
  .knobs-preset-name::selection{background:color-mix(in srgb,var(--_k-accent) 24%,transparent);color:var(--_k-fg)}
@@ -3915,7 +3923,7 @@ ${POINTER_ROW} .knobs-slider.knobs-modified:not(:hover,.knobs-active) .knobs-val
3915
3923
  .knobs-panel[data-gpu-active="true"] :where(header strong,header button,.knobs-tile,.knobs-section-head,.knobs-group-label,.knobs-peek,.knobs-card>*,.knobs-presets,.knobs-preset-chip,.knobs-group-head,.knobs-group-head button,.knobs-card,.knobs-row,.knobs-row>span:first-child,.knobs-slider,.knobs-track,.knobs-label,.knobs-value,.knobs-digits,.knobs-digits *,.knobs-unit,.knobs-seg,.knobs-seg button,.knobs-custom-actions button,.knobs-action,.knobs-select,.knobs-select-value,
3916
3924
  .knobs-sub,.knobs-x-head,.knobs-x-label,.knobs-x-read,.knobs-strip-label,.knobs-strip-read,.knobs-pad,.knobs-ease,.knobs-spring-curve,.knobs-ease-chips,.knobs-ease-chip,.knobs-graph,.knobs-graph-read,
3917
3925
  .knobs-tabs,.knobs-tab,.knobs-subhead,.knobs-subhead-label,.knobs-match,.knobs-cell,.knobs-cell-axis,.knobs-cell-read,.knobs-link,.knobs-range-read,.knobs-angle,.knobs-angle-read,.knobs-dial,
3918
- .knobs-seed,.knobs-seed-read,.knobs-seed-digits,.knobs-seed-digits *,.knobs-seed-roll,.knobs-keys,.knobs-keys-text,.knobs-tool,.knobs-tool-label,.knobs-tool-key,
3926
+ .knobs-seed,.knobs-seed-read,.knobs-seed-digits,.knobs-seed-digits *,.knobs-seed-roll,.knobs-keys,.knobs-keys-text,.knobs-tool,.knobs-tool-label,.knobs-tool-key,.knobs-preset-key,
3919
3927
  .knobs-section-name,.knobs-section-summary,.knobs-section-desc,.knobs-group-desc,.knobs-group-summary,.knobs-tab-count):not(.knobs-live,.knobs-live *){
3920
3928
  background:transparent!important;color:transparent!important;border-color:transparent!important;box-shadow:none!important;
3921
3929
  }
@@ -6955,8 +6963,8 @@ var init_swipe = __esm(() => {
6955
6963
  });
6956
6964
 
6957
6965
  // src/panel/presets.ts
6958
- function activeEntryId(controls) {
6959
- const ref = controls.activePreset();
6966
+ function activeEntryId(controls, scope = "") {
6967
+ const ref = controls.activePreset(scope);
6960
6968
  return ref ? `${ref.kind === "preset" ? "p" : "v"}:${ref.id}` : null;
6961
6969
  }
6962
6970
  function resolveActivePresetEntry(entries, currentValues, lastAppliedId, baseId = null) {
@@ -7009,7 +7017,7 @@ function nextPresetIndex(count, activeIndex, direction) {
7009
7017
  return direction > 0 ? 0 : count - 1;
7010
7018
  return ((activeIndex + direction) % count + count) % count;
7011
7019
  }
7012
- function createPresetStrip(doc, controls, showVariants, run, onAfterChange, presetLabel = (id) => controls.presetLabel(id), relevant) {
7020
+ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, presetLabel = (id, scope) => controls.presetLabel(id, scope), relevant, scopeOf = () => "") {
7013
7021
  const wake = () => onAfterChange?.();
7014
7022
  const el = (tag, cls, text) => {
7015
7023
  const node = doc.createElement(tag);
@@ -7056,35 +7064,43 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7056
7064
  let initializedPill = false;
7057
7065
  let slideUntil = 0;
7058
7066
  let display = "auto";
7067
+ let digits = false;
7059
7068
  let chipMin = 0;
7069
+ let scope = null;
7070
+ const at = () => scope ?? "";
7071
+ const source = () => scope === null ? undefined : scope === "" ? { presets: controls.presets, variants: controls.variants } : controls.scopes[scope];
7060
7072
  const unpinned = (values) => {
7061
7073
  let out;
7062
7074
  for (const k of Object.keys(values))
7063
- if (controls.isPinned(k) && !same2(values[k], controls.locked[k])) {
7075
+ if (controls.scopeOf(k) !== at() && !same2(values[k], controls.locked[k])) {
7064
7076
  out ??= { ...values };
7065
7077
  out[k] = controls.locked[k];
7066
7078
  }
7067
7079
  return out ?? values;
7068
7080
  };
7069
7081
  function buildEntries() {
7070
- const presetEntries = Object.entries(controls.presets).map(([id, presetValues]) => ({
7082
+ const from = source();
7083
+ if (!from)
7084
+ return [];
7085
+ const where = at();
7086
+ const presetEntries = Object.entries(from.presets).map(([id, presetValues]) => ({
7071
7087
  id: `p:${id}`,
7072
7088
  kind: "preset",
7073
7089
  rawId: id,
7074
- label: presetLabel(id),
7090
+ label: presetLabel(id, where),
7075
7091
  values: presetValues,
7076
- apply: (coalesce) => controls.preset(id, { coalesce })
7092
+ apply: (coalesce) => controls.preset(id, { coalesce, scope: where })
7077
7093
  }));
7078
7094
  if (!showVariants)
7079
7095
  return presetEntries;
7080
- const variants = controls.variants.list();
7096
+ const variants = from.variants.list();
7081
7097
  const variantEntries = variants.map((v) => ({
7082
7098
  id: `v:${v.id}`,
7083
7099
  kind: "variant",
7084
7100
  rawId: v.id,
7085
7101
  label: v.name ?? v.id.slice(0, 8),
7086
7102
  values: unpinned(v.values),
7087
- apply: (coalesce) => controls.variants.load(v.id, { coalesce })
7103
+ apply: (coalesce) => from.variants.load(v.id, { coalesce })
7088
7104
  }));
7089
7105
  return [...presetEntries, ...variantEntries];
7090
7106
  }
@@ -7135,8 +7151,8 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7135
7151
  if (first)
7136
7152
  doc.defaultView?.requestAnimationFrame(() => pill.style.transition = "");
7137
7153
  }
7138
- const resolve = () => resolveActivePresetEntry(entries, unpinned(controls.get()), lastAppliedId, activeEntryId(controls));
7139
- const wanted = (list) => (list.length > 1 || showVariants) && (relevant?.(list) ?? true);
7154
+ const resolve = () => resolveActivePresetEntry(entries, unpinned(controls.get()), lastAppliedId, activeEntryId(controls, at()));
7155
+ const wanted = (list) => scope !== null && (list.length > 1 || showVariants) && (relevant?.(list) ?? true);
7140
7156
  function syncShown() {
7141
7157
  const show = wanted(entries);
7142
7158
  if (root.hidden === show && root.dataset.presence === undefined)
@@ -7196,22 +7212,33 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7196
7212
  }
7197
7213
  function update() {
7198
7214
  run(() => {
7199
- const variantsRef = showVariants ? controls.variants.list() : null;
7200
- if (signature !== "" && variantsRef === lastVariantsRef) {
7215
+ const was = scope;
7216
+ scope = scopeOf();
7217
+ if (scope !== was)
7218
+ lastAppliedId = null;
7219
+ const variantsRef = showVariants ? source()?.variants.list() ?? null : null;
7220
+ if (signature !== "" && scope === was && variantsRef === lastVariantsRef) {
7201
7221
  syncShown();
7202
7222
  syncSelection(false);
7203
7223
  return;
7204
7224
  }
7205
7225
  lastVariantsRef = variantsRef;
7206
7226
  const nextEntries = buildEntries();
7207
- const nextSig = JSON.stringify(nextEntries.map((e) => [e.id, e.label]));
7227
+ const nextSig = JSON.stringify([scope, digits, nextEntries.map((e) => [e.id, e.label])]);
7208
7228
  entries = nextEntries;
7209
7229
  syncShown();
7210
7230
  if (nextSig !== signature) {
7211
7231
  signature = nextSig;
7212
7232
  entries = nextEntries;
7213
7233
  buttons = entries.map((entry, idx) => {
7214
- const b = el("button", "knobs-preset-chip", entry.label);
7234
+ const b = el("button", "knobs-preset-chip");
7235
+ if (digits && idx < 9) {
7236
+ const key = el("span", "knobs-preset-key", String(idx + 1));
7237
+ key.setAttribute("aria-hidden", "true");
7238
+ b.append(key, el("span", "knobs-preset-label", entry.label));
7239
+ b.setAttribute("aria-keyshortcuts", String(idx + 1));
7240
+ } else
7241
+ b.textContent = entry.label;
7215
7242
  b.type = "button";
7216
7243
  b.tabIndex = -1;
7217
7244
  b.dataset.presetId = entry.id;
@@ -7269,12 +7296,15 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7269
7296
  }
7270
7297
  function save() {
7271
7298
  finishName(true);
7272
- const used = new Set(controls.variants.list().map((v) => v.name));
7299
+ const variants = source()?.variants;
7300
+ if (!variants)
7301
+ return "";
7302
+ const used = new Set(variants.list().map((v) => v.name));
7273
7303
  let n = 1;
7274
7304
  while (used.has(`Variant ${n}`))
7275
7305
  n++;
7276
- const variant = controls.variants.save(`Variant ${n}`);
7277
- controls.variants.load(variant.id);
7306
+ const variant = variants.save(`Variant ${n}`);
7307
+ variants.load(variant.id);
7278
7308
  lastAppliedId = `v:${variant.id}`;
7279
7309
  update();
7280
7310
  startName(entries.findIndex((e) => e.id === `v:${variant.id}`), true);
@@ -7349,7 +7379,7 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7349
7379
  focusQuietly();
7350
7380
  let message;
7351
7381
  if (keep && entry?.kind === "variant" && name && name !== was) {
7352
- controls.variants.rename(entry.rawId, name);
7382
+ source()?.variants.rename(entry.rawId, name);
7353
7383
  update();
7354
7384
  message = `Named “${name}”.`;
7355
7385
  }
@@ -7370,7 +7400,7 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7370
7400
  const entry = entries[activeState.activeIndex];
7371
7401
  if (!entry || entry.kind !== "variant")
7372
7402
  return;
7373
- controls.variants.remove(entry.rawId);
7403
+ source()?.variants.remove(entry.rawId);
7374
7404
  lastAppliedId = null;
7375
7405
  update();
7376
7406
  onAfterChange?.();
@@ -7506,6 +7536,16 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7506
7536
  if (placeDisplay())
7507
7537
  measurePill(true);
7508
7538
  },
7539
+ digits(on) {
7540
+ if (digits === on)
7541
+ return;
7542
+ digits = on;
7543
+ root.toggleAttribute("data-digits", on);
7544
+ if (signature === "")
7545
+ return;
7546
+ signature = "";
7547
+ update();
7548
+ },
7509
7549
  save: () => {
7510
7550
  let message = "";
7511
7551
  run(() => message = save());
@@ -9459,7 +9499,8 @@ fn hash21(q: vec2f) -> f32 {
9459
9499
 
9460
9500
  // kind 0: Panel card surface + elevation shadow + border + specular rim. With a hairline width
9461
9501
  // (extra.z, whole device px) the edge is that hairline just outside the card, like CSS's
9462
- // 0 0 0 w spread shadow, and the rim is CSS's inset 0 w 0; otherwise a 1px border and a 1px rim.
9502
+ // 0 0 0 w spread shadow, and the rim lights its top, like CSS's 0 -w 0 (one edge, not two);
9503
+ // otherwise a 1px border and a 1px rim inside it.
9463
9504
  fn shadePanel(p: Prim, local: vec2f, size: vec2f) -> vec4f {
9464
9505
  let r = p.look.x;
9465
9506
  let dist = roundedBox(local, size, r);
@@ -9468,6 +9509,8 @@ fn shadePanel(p: Prim, local: vec2f, size: vec2f) -> vec4f {
9468
9509
  var out = vec4f(0.0);
9469
9510
  if (hair > 0.0 && dist > -1.0) {
9470
9511
  out = premul(p.color2) * clamp(covSd(dist - hair) - bodyCov, 0.0, 1.0);
9512
+ let raised = covSd(roundedBox(local + vec2f(0.0, hair), size, r));
9513
+ out = over(out, premul(p.aux2) * clamp(raised - bodyCov, 0.0, 1.0));
9471
9514
  }
9472
9515
 
9473
9516
  // Floating drop shadow when glowOrBlur (look.z) > 0; like box-shadow, never under the card.
@@ -9481,13 +9524,16 @@ fn shadePanel(p: Prim, local: vec2f, size: vec2f) -> vec4f {
9481
9524
 
9482
9525
  var card = premul(p.color) * bodyCov;
9483
9526
 
9484
- // The rim, built like CSS's inset 0 w 0: inside the border, minus that shape moved down w.
9527
+ // Without a hairline, the rim is CSS's inset 0 1px 0: inside the border, minus that shape
9528
+ // moved down.
9485
9529
  let inner = local - vec2f(1.0);
9486
9530
  let innerSize = size - vec2f(2.0);
9487
9531
  let w = select(1.0, hair, hair > 0.0);
9488
9532
  let innerCov = covSd(roundedBox(inner, innerSize, r - 1.0));
9489
- let rimCov = innerCov * (1.0 - covSd(roundedBox(inner - vec2f(0.0, w), innerSize, r - 1.0)));
9490
- card = over(card, premul(p.aux2) * rimCov);
9533
+ if (hair <= 0.0) {
9534
+ let rimCov = innerCov * (1.0 - covSd(roundedBox(inner - vec2f(0.0, w), innerSize, r - 1.0)));
9535
+ card = over(card, premul(p.aux2) * rimCov);
9536
+ }
9491
9537
 
9492
9538
  // A morphing card's specular ring (extra.y), w wide inside the border like CSS's inset 0 0 0 w.
9493
9539
  if (p.extra.y > 0.001) {
@@ -10617,6 +10663,135 @@ class GpuOverlayManager {
10617
10663
  d[b + 42] = more ? more[2] : 0;
10618
10664
  d[b + 43] = more ? more[3] : 0;
10619
10665
  }
10666
+ drawPresetStrip(ps, tokens, clip, hinting, reducedMotion, dt, tray) {
10667
+ let moving = false;
10668
+ const psHovered = this.curPanelHovered && ps.element.matches(":hover");
10669
+ this.curSubHovered = psHovered;
10670
+ const psRect = this.rectOf(ps.element);
10671
+ const trackRect = this.rectOf(ps.track);
10672
+ if (tray && !tokens.boxed)
10673
+ this.pushPrim({
10674
+ x: psRect.left,
10675
+ y: psRect.top,
10676
+ w: psRect.width,
10677
+ h: psRect.height,
10678
+ color: tokens.segBg,
10679
+ color2: tokens.accent,
10680
+ radius: tokens.radius,
10681
+ glowOrBlur: tokens.glowStrength * 8,
10682
+ kind: 1,
10683
+ p0: psHovered || this.isFocused(ps.element) ? 0.4 : 0
10684
+ });
10685
+ const stripClip = intersectClip(clip, [psRect.left, psRect.top, psRect.right, psRect.bottom]);
10686
+ const activeState = ps.getActiveState();
10687
+ const chipButtons = ps.getButtons();
10688
+ if (tokens.boxed)
10689
+ for (const btn of chipButtons) {
10690
+ const br = this.rectOf(btn);
10691
+ if (br.right < stripClip[0] || br.left > stripClip[2])
10692
+ continue;
10693
+ this.drawButtonBox(btn, br, this.isHovered(btn), this.missed(btn, hinting), stripClip);
10694
+ }
10695
+ const activeBtn = activeState.activeIndex >= 0 ? chipButtons[activeState.activeIndex] : undefined;
10696
+ const pe = ps.pillEdges;
10697
+ const activeBtnRect = activeBtn ? this.rectOf(activeBtn) : undefined;
10698
+ const panned = livePill(ps.track);
10699
+ if (activeBtn && activeBtnRect && !activeState.isCustom) {
10700
+ const left = activeBtnRect.left - trackRect.left;
10701
+ const right = left + Math.max(20, activeBtnRect.width);
10702
+ const top = activeBtnRect.top - trackRect.top;
10703
+ this.carry(pe, activeBtn, left, right, ps.pillY, top);
10704
+ if (panned) {
10705
+ pe.snap(panned.left, panned.right);
10706
+ ps.pillY.snap(panned.top);
10707
+ } else if (reducedMotion || pe.end.x - pe.start.x <= 1) {
10708
+ pe.snap(left, right);
10709
+ ps.pillY.snap(top);
10710
+ } else {
10711
+ pe.to(left, right, 0, trailScale(tokens.blurStrength));
10712
+ ps.pillY.set(top);
10713
+ }
10714
+ ps.pillAlpha.set(activeState.drifted ? DRIFTED_PILL : 1);
10715
+ } else {
10716
+ ps.pillAlpha.set(0);
10717
+ }
10718
+ for (const sp of [pe.start, pe.end, ps.pillY, ps.pillAlpha]) {
10719
+ if (reducedMotion)
10720
+ sp.snap(sp.target);
10721
+ else {
10722
+ sp.step(dt);
10723
+ if (!sp.resting)
10724
+ moving = true;
10725
+ }
10726
+ }
10727
+ const pillAlpha = clamp2(ps.pillAlpha.x, 0, 1);
10728
+ if (pillAlpha > 0.01) {
10729
+ this.pushPrim({
10730
+ x: trackRect.left + pe.start.x,
10731
+ y: trackRect.top + ps.pillY.x,
10732
+ w: Math.max(18, pe.end.x - pe.start.x),
10733
+ h: Math.max(16, activeBtnRect?.height ?? trackRect.height),
10734
+ clip: stripClip,
10735
+ color: withAlpha2(tokens.pillBg, pillAlpha),
10736
+ color2: withAlpha2(tokens.pillBorder, pillAlpha),
10737
+ strokeWidth: this.hairline(tokens),
10738
+ aux2: withAlpha2(tokens.rim, tokens.rim[3] * pillAlpha),
10739
+ radius: tokens.buttonRadius ?? tokens.radiusMd,
10740
+ glowOrBlur: tokens.glowStrength,
10741
+ kind: 3,
10742
+ blurX: reducedMotion ? 0 : clamp2(pe.velocity * 0.006 * tokens.motionBlur, -8, 8)
10743
+ });
10744
+ }
10745
+ for (let i = 0;i < chipButtons.length; i++) {
10746
+ const btn = chipButtons[i];
10747
+ const br = this.rectOf(btn);
10748
+ if (br.right < stripClip[0] || br.left > stripClip[2])
10749
+ continue;
10750
+ const isActive = i === activeState.activeIndex;
10751
+ const chipHovered = this.isHovered(btn);
10752
+ if (chipHovered)
10753
+ this.curSubHovered = false;
10754
+ const mark = this.ease(btn, "dot", btn.hasAttribute("data-drifted") ? 1 : 0);
10755
+ if (mark > 0.01) {
10756
+ const size = 4 * (0.4 + 0.6 * mark);
10757
+ this.pushPrim({
10758
+ x: br.right - 6 - size / 2,
10759
+ y: br.top + 6 - size / 2,
10760
+ w: size,
10761
+ h: size,
10762
+ clip: stripClip,
10763
+ color: withAlpha2(tokens.modified, 0.95 * mark),
10764
+ glowOrBlur: 0.5,
10765
+ kind: 7
10766
+ });
10767
+ }
10768
+ if (btn.hasAttribute("data-naming"))
10769
+ continue;
10770
+ const key = btn.querySelector(".knobs-preset-key");
10771
+ const name = key && btn.querySelector(".knobs-preset-label");
10772
+ if (key && name) {
10773
+ const kr = this.rectOf(key);
10774
+ this.drawText(key.textContent ?? "", kr.left, br.top + br.height * 0.5, withAlpha2(tokens.dim, this.missed(btn, hinting)), { px: tokens.textMeta, weight: tokens.labelWeight, clip: stripClip });
10775
+ }
10776
+ const nr = name ? this.rectOf(name) : undefined;
10777
+ this.drawText(name ? name.textContent ?? "" : btn.textContent ?? "", nr ? nr.left : br.left + br.width * 0.5, br.top + br.height * 0.5, withAlpha2(this.hot(btn, isActive || chipHovered, tokens.buttonColor ?? tokens.label), this.missed(btn, hinting)), {
10778
+ px: tokens.buttonFontSize ?? tokens.textControl,
10779
+ weight: isActive ? tokens.titleWeight : tokens.labelWeight,
10780
+ align: nr ? "left" : "center",
10781
+ clip: stripClip
10782
+ });
10783
+ }
10784
+ if (ps.saveButton && !ps.saveButton.hidden) {
10785
+ const sr = this.rectOf(ps.saveButton);
10786
+ const hot = this.isHovered(ps.saveButton);
10787
+ this.drawText("+", sr.left + sr.width * 0.5, sr.top + sr.height * 0.5, this.hot(ps.saveButton, hot, tokens.dim), {
10788
+ px: tokens.textTitle,
10789
+ weight: 400,
10790
+ align: "center"
10791
+ });
10792
+ }
10793
+ return moving;
10794
+ }
10620
10795
  hairline(tokens) {
10621
10796
  return Math.max(1, Math.round(tokens.rimWidth * this.curScale)) / this.curScale;
10622
10797
  }
@@ -12830,7 +13005,6 @@ class GpuOverlayManager {
12830
13005
  this.placeBackdrop(panel);
12831
13006
  continue;
12832
13007
  }
12833
- const pillFill = tokens.pillBg;
12834
13008
  if (pageB)
12835
13009
  this.placeBackdrop(panel);
12836
13010
  const covers = drawn.some((r) => r.left < card.right && r.right > card.left && r.top < card.bottom && r.bottom > card.top);
@@ -12998,129 +13172,7 @@ class GpuOverlayManager {
12998
13172
  const stripAlpha = this.curAlpha;
12999
13173
  if (!ps.element.hidden) {
13000
13174
  this.curAlpha = stripAlpha * this.presence(ps.element);
13001
- const psHovered = this.curPanelHovered && ps.element.matches(":hover");
13002
- this.curSubHovered = psHovered;
13003
- const psRect = this.rectOf(ps.element);
13004
- const trackRect = this.rectOf(ps.track);
13005
- if (!tokens.boxed)
13006
- this.pushPrim({
13007
- x: psRect.left,
13008
- y: psRect.top,
13009
- w: psRect.width,
13010
- h: psRect.height,
13011
- color: tokens.segBg,
13012
- color2: tokens.accent,
13013
- radius: tokens.radius,
13014
- glowOrBlur: tokens.glowStrength * 8,
13015
- kind: 1,
13016
- p0: psHovered || this.isFocused(ps.element) ? 0.4 : 0
13017
- });
13018
- const stripClip = intersectClip(bodyClip, [
13019
- psRect.left,
13020
- psRect.top,
13021
- psRect.right,
13022
- psRect.bottom
13023
- ]);
13024
- const activeState = ps.getActiveState();
13025
- const chipButtons = ps.getButtons();
13026
- if (tokens.boxed)
13027
- for (const btn of chipButtons) {
13028
- const br = this.rectOf(btn);
13029
- if (br.right < stripClip[0] || br.left > stripClip[2])
13030
- continue;
13031
- this.drawButtonBox(btn, br, this.isHovered(btn), this.missed(btn, hinting), stripClip);
13032
- }
13033
- const activeBtn = activeState.activeIndex >= 0 ? chipButtons[activeState.activeIndex] : undefined;
13034
- const pe = ps.pillEdges;
13035
- const activeBtnRect = activeBtn ? this.rectOf(activeBtn) : undefined;
13036
- const panned = livePill(ps.track);
13037
- if (activeBtn && activeBtnRect && !activeState.isCustom) {
13038
- const left = activeBtnRect.left - trackRect.left;
13039
- const right = left + Math.max(20, activeBtnRect.width);
13040
- const top = activeBtnRect.top - trackRect.top;
13041
- this.carry(pe, activeBtn, left, right, ps.pillY, top);
13042
- if (panned) {
13043
- pe.snap(panned.left, panned.right);
13044
- ps.pillY.snap(panned.top);
13045
- } else if (reducedMotion || pe.end.x - pe.start.x <= 1) {
13046
- pe.snap(left, right);
13047
- ps.pillY.snap(top);
13048
- } else {
13049
- pe.to(left, right, 0, trailScale(tokens.blurStrength));
13050
- ps.pillY.set(top);
13051
- }
13052
- ps.pillAlpha.set(activeState.drifted ? DRIFTED_PILL : 1);
13053
- } else {
13054
- ps.pillAlpha.set(0);
13055
- }
13056
- for (const sp of [pe.start, pe.end, ps.pillY, ps.pillAlpha]) {
13057
- if (reducedMotion)
13058
- sp.snap(sp.target);
13059
- else {
13060
- sp.step(dt);
13061
- if (!sp.resting)
13062
- anyMoving = true;
13063
- }
13064
- }
13065
- const pillAlpha = clamp2(ps.pillAlpha.x, 0, 1);
13066
- if (pillAlpha > 0.01) {
13067
- this.pushPrim({
13068
- x: trackRect.left + pe.start.x,
13069
- y: trackRect.top + ps.pillY.x,
13070
- w: Math.max(18, pe.end.x - pe.start.x),
13071
- h: Math.max(16, activeBtnRect?.height ?? trackRect.height),
13072
- clip: stripClip,
13073
- color: withAlpha2(pillFill, pillAlpha),
13074
- color2: withAlpha2(tokens.pillBorder, pillAlpha),
13075
- strokeWidth: this.hairline(tokens),
13076
- aux2: withAlpha2(tokens.rim, tokens.rim[3] * pillAlpha),
13077
- radius: tokens.buttonRadius ?? tokens.radiusMd,
13078
- glowOrBlur: tokens.glowStrength,
13079
- kind: 3,
13080
- blurX: reducedMotion ? 0 : clamp2(pe.velocity * 0.006 * tokens.motionBlur, -8, 8)
13081
- });
13082
- }
13083
- for (let i = 0;i < chipButtons.length; i++) {
13084
- const btn = chipButtons[i];
13085
- const br = this.rectOf(btn);
13086
- if (br.right < stripClip[0] || br.left > stripClip[2])
13087
- continue;
13088
- const isActive = i === activeState.activeIndex;
13089
- const chipHovered = this.isHovered(btn);
13090
- if (chipHovered)
13091
- this.curSubHovered = false;
13092
- const mark = this.ease(btn, "dot", btn.hasAttribute("data-drifted") ? 1 : 0);
13093
- if (mark > 0.01) {
13094
- const size = 4 * (0.4 + 0.6 * mark);
13095
- this.pushPrim({
13096
- x: br.right - 6 - size / 2,
13097
- y: br.top + 6 - size / 2,
13098
- w: size,
13099
- h: size,
13100
- clip: stripClip,
13101
- color: withAlpha2(tokens.modified, 0.95 * mark),
13102
- glowOrBlur: 0.5,
13103
- kind: 7
13104
- });
13105
- }
13106
- if (btn.hasAttribute("data-naming"))
13107
- continue;
13108
- this.drawText(btn.textContent ?? "", br.left + br.width * 0.5, br.top + br.height * 0.5, withAlpha2(this.hot(btn, isActive || chipHovered, tokens.buttonColor ?? tokens.label), this.missed(btn, hinting)), {
13109
- px: tokens.buttonFontSize ?? tokens.textControl,
13110
- weight: isActive ? tokens.titleWeight : tokens.labelWeight,
13111
- align: "center",
13112
- clip: stripClip
13113
- });
13114
- }
13115
- if (ps.saveButton && !ps.saveButton.hidden) {
13116
- const sr = this.rectOf(ps.saveButton);
13117
- const hot = this.isHovered(ps.saveButton);
13118
- this.drawText("+", sr.left + sr.width * 0.5, sr.top + sr.height * 0.5, this.hot(ps.saveButton, hot, tokens.dim), {
13119
- px: tokens.textTitle,
13120
- weight: 400,
13121
- align: "center"
13122
- });
13123
- }
13175
+ anyMoving = this.drawPresetStrip(ps, tokens, bodyClip, hinting, reducedMotion, dt, true) || anyMoving;
13124
13176
  this.curAlpha = stripAlpha;
13125
13177
  }
13126
13178
  const tabs = panel.tabs;
@@ -13377,6 +13429,15 @@ class GpuOverlayManager {
13377
13429
  this.drawScope(card, cr, tokens.radius, 1);
13378
13430
  }
13379
13431
  }
13432
+ for (const gs of panel.groupStrips?.() ?? []) {
13433
+ const card = gs.element.parentElement;
13434
+ if (!card || gs.element.closest("[hidden]"))
13435
+ continue;
13436
+ const cr = cardRects.get(card) ?? this.rectOf(card);
13437
+ this.curClip = clipToTile(card, intersectClip(bodyClip, [cr.left, cr.top, cr.right, cr.bottom]));
13438
+ this.curAlpha = panelAlpha * this.presence(card) * fadeOf(card);
13439
+ anyMoving = this.drawPresetStrip(gs, tokens, this.curClip, hinting, reducedMotion, dt, false) || anyMoving;
13440
+ }
13380
13441
  this.curClip = bodyClip;
13381
13442
  this.curAlpha = panelAlpha;
13382
13443
  const washed = new Set;
@@ -20492,7 +20553,7 @@ function presence(el, o = {}) {
20492
20553
  });
20493
20554
  const tune = (seconds, zeta) => {
20494
20555
  const reduced = reducedMotion(win);
20495
- spring.omega = 2 * Math.PI / (reduced ? FADE.duration : seconds);
20556
+ spring.omega = 2 * Math.PI / (reduced ? Math.min(FADE.duration, seconds) : seconds);
20496
20557
  spring.zeta = reduced ? 1 : zeta;
20497
20558
  };
20498
20559
  render();
@@ -26046,8 +26107,9 @@ function buildPanel(controls, host, options) {
26046
26107
  const group = scope === "row" ? rows[rowAt(from)]?.group : scope === "group" ? cardOf(from)?.dataset.group ?? undefined : undefined;
26047
26108
  const keys = scope === "row" ? rows[rowAt(from)]?.keys ?? [] : scope === "group" ? cardKeys(cardOf(from)) : scope === "selection" ? selectionKeys() ?? [] : undefined;
26048
26109
  const index = group === undefined ? -1 : controls.activeGroupPreset(group);
26049
- const ref = controls.activePreset();
26050
- const name = index >= 0 ? controls.groupPresets[group]?.[index]?.name : ref?.kind === "preset" ? presetLabel(ref.id) : ref ? presetStrip.getEntries().find((e) => e.id === `v:${ref.id}`)?.label : undefined;
26110
+ const where = (keys?.length ? controls.scopeOf(keys[0]) : stripScope()) ?? null;
26111
+ const ref = where === null ? null : controls.activePreset(where);
26112
+ const name = index >= 0 ? controls.groupPresets[group]?.[index]?.name : ref?.kind === "preset" ? presetLabel(ref.id, where) : ref ? (where === "" ? controls.variants : controls.scopes[where]?.variants)?.list().find((v) => v.id === ref.id)?.name : undefined;
26051
26113
  if (!name)
26052
26114
  return null;
26053
26115
  const off = keys ? keys.some((k) => controls.isModified(k)) : controls.isModified();
@@ -26200,8 +26262,10 @@ function buildPanel(controls, host, options) {
26200
26262
  if (helpButton?.hidden)
26201
26263
  titleEl.title = `${titleEl.textContent ? `${titleEl.textContent}
26202
26264
  ` : ""}Press ${show("help")} for keyboard shortcuts`;
26203
- const presetLabel = (id) => options.presetLabels?.[id] ?? (id === "default" ? options.defaultLabel : undefined) ?? controls.presetLabel(id);
26204
- const presetStrip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel, (entries) => presetsTouchTab(entries));
26265
+ const presetLabel = (id, scope = "") => options.presetLabels?.[id] ?? (id === "default" ? typeof options.defaultLabel === "object" ? options.defaultLabel[scope] : options.defaultLabel : undefined) ?? controls.presetLabel(id, scope);
26266
+ const hintReach = (typeof options.hints === "object" ? options.hints.reach : options.hints) ?? "page";
26267
+ const stripDigitsShown = typeof options.hints === "object" && options.hints.digits === "always";
26268
+ const presetStrip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel, (entries) => presetsTouchTab(entries), () => stripScope());
26205
26269
  const tabStrip = createTabStrip(doc, (id, animate) => selectTab(id, animate));
26206
26270
  let activeTab;
26207
26271
  const searchOptions = typeof options.search === "object" ? options.search : {};
@@ -26254,6 +26318,27 @@ function buildPanel(controls, host, options) {
26254
26318
  reveal(header, any, () => wakeGpu());
26255
26319
  };
26256
26320
  presetStrip.display(page ? "chips" : options.presetDisplay ?? "auto");
26321
+ presetStrip.digits(stripDigitsShown);
26322
+ const groupStrips = new Map;
26323
+ const tabNamed = (id) => (options.tabs ?? []).some((t) => (typeof t === "string" ? t : t.id) === id) || Object.values(controls.schema).some((f) => f.tab === id);
26324
+ const groupStripFor = (group) => {
26325
+ if (page || !Object.hasOwn(controls.scopes, group) || tabNamed(group))
26326
+ return;
26327
+ let strip = groupStrips.get(group);
26328
+ if (!strip) {
26329
+ strip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel, undefined, () => stripScope() === group ? null : group);
26330
+ strip.element.setAttribute("aria-label", `${groupTitle(group)} presets`);
26331
+ strip.display(options.presetDisplay ?? "auto");
26332
+ strip.digits(stripDigitsShown);
26333
+ groupStrips.set(group, strip);
26334
+ }
26335
+ return strip;
26336
+ };
26337
+ const updateStrips = () => {
26338
+ presetStrip.update();
26339
+ for (const strip of groupStrips.values())
26340
+ strip.update();
26341
+ };
26257
26342
  const allActions = [
26258
26343
  ...controls.actions ?? [],
26259
26344
  ...options.actions ?? []
@@ -26712,7 +26797,7 @@ function buildPanel(controls, host, options) {
26712
26797
  const specs = [];
26713
26798
  const placed = new Set;
26714
26799
  for (const [key, field] of entries) {
26715
- if (field.kind === "choice" && field.variants && !controls.isPinned(key))
26800
+ if (field.kind === "choice" && field.variants && controls.inPresets(key))
26716
26801
  continue;
26717
26802
  const pair = field.kind === "knob" && field.spring ? halves.get(field.spring.id) : undefined;
26718
26803
  if (pair?.duration && pair.bounce && field.kind === "knob" && field.spring) {
@@ -27410,6 +27495,7 @@ function buildPanel(controls, host, options) {
27410
27495
  const chosen = [];
27411
27496
  const pinnedBlocks = new Set;
27412
27497
  const usedSubheads = new Set;
27498
+ const stripped = new Set;
27413
27499
  for (const run of runs) {
27414
27500
  const at = free.findIndex((b) => b.group === run.group);
27415
27501
  const block = at >= 0 ? free.splice(at, 1)[0] : newBlock(run.group);
@@ -27444,7 +27530,10 @@ function buildPanel(controls, host, options) {
27444
27530
  under = name;
27445
27531
  nodes.push(r.row);
27446
27532
  }
27447
- arrange(block.card, nodes);
27533
+ const strip = run.group !== undefined && !run.pinned && !stripped.has(run.group) ? groupStripFor(run.group) : undefined;
27534
+ if (strip)
27535
+ stripped.add(run.group);
27536
+ arrange(block.card, strip ? [strip.element, ...nodes] : nodes);
27448
27537
  if (animate && at >= 0) {
27449
27538
  for (const r of run.rows)
27450
27539
  if (entering.has(r))
@@ -27606,13 +27695,13 @@ function buildPanel(controls, host, options) {
27606
27695
  return list;
27607
27696
  };
27608
27697
  function presetsTouchTab(entries) {
27609
- if (page || activeTab === undefined)
27698
+ if (page || activeTab === undefined || stripScope() !== "")
27610
27699
  return true;
27611
27700
  const keys = rows.flatMap((r) => {
27612
27701
  const tab = tabOf(r);
27613
- return !pinnedRow(r) && (tab === undefined || tab === activeTab) ? r.keys : [];
27702
+ return !pinnedRow(r) && (tab === undefined || tab === activeTab) ? r.keys.filter((k) => controls.scopeOf(k) === "") : [];
27614
27703
  });
27615
- const v = variantsOf(controls.schema, controls.isPinned);
27704
+ const v = variantsOf(controls.schema, (key) => !controls.inPresets(key));
27616
27705
  if (v) {
27617
27706
  const tab = v.field.tab ?? (v.field.group !== undefined ? groupTab(v.field.group) : undefined);
27618
27707
  if (tab === undefined || tab === activeTab)
@@ -27620,6 +27709,22 @@ function buildPanel(controls, host, options) {
27620
27709
  }
27621
27710
  return presetsTouch(entries, keys, controls.locked);
27622
27711
  }
27712
+ function stripScope() {
27713
+ const named = Object.keys(controls.scopes);
27714
+ const panelWide = named.length === 0 || Object.keys(controls.presets).length > 1;
27715
+ if (page)
27716
+ return panelWide ? "" : null;
27717
+ if (activeTab === undefined)
27718
+ return panelWide ? "" : named[0] ?? null;
27719
+ if (named.includes(activeTab))
27720
+ return activeTab;
27721
+ const group = named.find((id) => rows.some((r) => r.group === id && tabOf(r) === activeTab));
27722
+ return group ?? (panelWide ? "" : null);
27723
+ }
27724
+ const stripVariants = () => {
27725
+ const scope = stripScope();
27726
+ return (scope ? controls.scopes[scope]?.variants : undefined) ?? controls.variants;
27727
+ };
27623
27728
  function syncTabs() {
27624
27729
  const list = tabList();
27625
27730
  const has = (id) => id !== undefined && list.some((t) => t.id === id);
@@ -27636,6 +27741,7 @@ function buildPanel(controls, host, options) {
27636
27741
  writeSession(controls.id, { tab: id });
27637
27742
  tabStrip.select(id, animate);
27638
27743
  const focused = doc.activeElement;
27744
+ updateStrips();
27639
27745
  applyVisibility(animate ? "tab" : "instant");
27640
27746
  if (focused && root.contains(focused) && focused.closest("[hidden],[data-leaving]"))
27641
27747
  tabStrip.focus({ preventScroll: true, focusVisible: false });
@@ -27911,7 +28017,7 @@ function buildPanel(controls, host, options) {
27911
28017
  text.spellcheck = false;
27912
28018
  text.setAttribute("aria-label", "Import or copy text");
27913
28019
  const apply = button("Apply import", () => {
27914
- controls.variants.import(text.value);
28020
+ stripVariants().import(text.value);
27915
28021
  say("Presets imported.");
27916
28022
  reveal(text, false);
27917
28023
  reveal(apply, false);
@@ -27957,7 +28063,7 @@ function buildPanel(controls, host, options) {
27957
28063
  if (showVariants && showFooter) {
27958
28064
  const transfer = el("div");
27959
28065
  transfer.className = "knobs-transfer";
27960
- transfer.append(button("Export presets", () => void copy(controls.variants.export(), false)), button("Import presets", () => {
28066
+ transfer.append(button("Export presets", () => void copy(stripVariants().export(), false)), button("Import presets", () => {
27961
28067
  reveal(text, true);
27962
28068
  reveal(apply, true);
27963
28069
  text.value = "";
@@ -28053,6 +28159,7 @@ function buildPanel(controls, host, options) {
28053
28159
  let shownBase = "";
28054
28160
  const baseSignature = () => JSON.stringify([
28055
28161
  controls.activePreset(),
28162
+ Object.keys(controls.scopes).map((s) => controls.activePreset(s)),
28056
28163
  Object.keys(controls.groupPresets).map((g) => controls.activeGroupPreset(g))
28057
28164
  ]);
28058
28165
  function update() {
@@ -28081,7 +28188,7 @@ function buildPanel(controls, host, options) {
28081
28188
  }
28082
28189
  const stagger = initialized ? cascade(changedCount) : 0;
28083
28190
  const moving = initialized ? rows.filter(changed) : rows;
28084
- presetStrip.update();
28191
+ updateStrips();
28085
28192
  batchMeasures(() => {
28086
28193
  for (const row of moving) {
28087
28194
  row.slider?.prime();
@@ -28251,14 +28358,15 @@ function buildPanel(controls, host, options) {
28251
28358
  if (s.label?.hasAttribute("data-foldable"))
28252
28359
  add(s.label, sectionTitle(s.name), () => s.label.focus(), s.label.firstElementChild);
28253
28360
  }
28254
- presetStrip.getEntries().forEach((entry, i) => {
28255
- const chip = presetStrip.getButtons()[i];
28256
- if (chip)
28257
- add(chip, entry.label, () => {
28258
- presetStrip.selectById(entry.id);
28259
- presetStrip.element.focus({ preventScroll: true });
28260
- });
28261
- });
28361
+ for (const strip of [presetStrip, ...groupStrips.values()])
28362
+ strip.getEntries().forEach((entry, i) => {
28363
+ const chip = strip.getButtons()[i];
28364
+ if (chip)
28365
+ add(chip, entry.label, () => {
28366
+ strip.selectById(entry.id);
28367
+ strip.element.focus({ preventScroll: true });
28368
+ });
28369
+ });
28262
28370
  for (const b of root.querySelectorAll(page ? ".knobs-head-cmd, .knobs-group-head button:not(.knobs-group-die,.knobs-group-reset,.knobs-chips button), .knobs-custom-actions button, .knobs-preset-save" : ".knobs-head-cmd, .knobs-group-head button, .knobs-custom-actions button, .knobs-preset-save, .knobs-popover button")) {
28263
28371
  add(b, b.dataset.hint ?? b.getAttribute("aria-label") ?? b.textContent ?? "", () => b.focus());
28264
28372
  }
@@ -28277,7 +28385,7 @@ function buildPanel(controls, host, options) {
28277
28385
  }
28278
28386
  return targets;
28279
28387
  };
28280
- const unregisterHints = (options.hints ?? "page") === "page" && bound("hint") ? pageHints(doc).register(root, () => collectHintTargets().map((t) => ({
28388
+ const unregisterHints = hintReach === "page" && bound("hint") ? pageHints(doc).register(root, () => collectHintTargets().map((t) => ({
28281
28389
  el: t.el,
28282
28390
  label: t.display,
28283
28391
  ...t.labelEl ? { labelEl: t.labelEl } : {},
@@ -28343,6 +28451,9 @@ function buildPanel(controls, host, options) {
28343
28451
  runs.push({ at: b.head, stops: shown([b.head]) });
28344
28452
  for (const sh of subheads.values())
28345
28453
  runs.push({ at: sh.el, stops: shown([sh.el]) });
28454
+ for (const gs of groupStrips.values())
28455
+ if (visible(gs.element))
28456
+ runs.push({ at: gs.element, stops: shown([gs.element]) });
28346
28457
  runs.push(...strips);
28347
28458
  runs.sort((a, b) => a.at.compareDocumentPosition(b.at) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
28348
28459
  for (const run of runs)
@@ -28820,6 +28931,9 @@ function buildPanel(controls, host, options) {
28820
28931
  return { el: sub, level: "row" };
28821
28932
  return { el: r.row.closest(".knobs-card > *") ?? r.row, level: "row" };
28822
28933
  }
28934
+ const strip = active.closest(".knobs-card > .knobs-presets");
28935
+ if (strip)
28936
+ return { el: strip, level: "row" };
28823
28937
  const card = cardOf(active);
28824
28938
  return card ? { el: card, level: "card" } : null;
28825
28939
  };
@@ -28834,7 +28948,7 @@ function buildPanel(controls, host, options) {
28834
28948
  editor: t.level === "editor" ? partName(t.el, active) : undefined
28835
28949
  });
28836
28950
  };
28837
- const crumbs = options.crumbs === false || header.hidden || !titleEl.isConnected ? undefined : createCrumbs({
28951
+ const crumbs = options.crumbs !== true || header.hidden || !titleEl.isConnected ? undefined : createCrumbs({
28838
28952
  header,
28839
28953
  title: titleEl,
28840
28954
  reduced: () => reducedMotion(),
@@ -28858,6 +28972,22 @@ function buildPanel(controls, host, options) {
28858
28972
  to.focus(quiet);
28859
28973
  queueScope();
28860
28974
  };
28975
+ root.addEventListener("mousedown", (event) => {
28976
+ if (event.button !== 0 || event.shiftKey || event.metaKey || event.ctrlKey)
28977
+ return;
28978
+ const target = event.target instanceof Element ? event.target : null;
28979
+ const r = rows[rowAt(target)];
28980
+ if (!target || !r || r.leaving || r.row.closest("[hidden]"))
28981
+ return;
28982
+ const own = target.closest("button,input,select,textarea,a[href],[tabindex],[contenteditable]");
28983
+ if (own && r.row.contains(own))
28984
+ return;
28985
+ event.preventDefault();
28986
+ r.input.focus({ preventScroll: true, focusVisible: false });
28987
+ const t = lensTarget(doc.activeElement);
28988
+ jumpedTo = t && t !== "out" ? t.el : null;
28989
+ queueScope();
28990
+ }, true);
28861
28991
  let titleDown;
28862
28992
  titleEl.addEventListener("pointerdown", (event) => {
28863
28993
  titleDown = [event.clientX, event.clientY];
@@ -28993,6 +29123,23 @@ function buildPanel(controls, host, options) {
28993
29123
  return;
28994
29124
  return optionsOf(r);
28995
29125
  };
29126
+ const groupStripAt = (node) => node ? [...groupStrips.values()].find((s) => s.element.contains(node)) : undefined;
29127
+ const groupStripOptions = new Map;
29128
+ const optionsOfStrip = (strip) => {
29129
+ let o = groupStripOptions.get(strip);
29130
+ if (!o) {
29131
+ o = {
29132
+ get count() {
29133
+ return strip.getEntries().length;
29134
+ },
29135
+ choose: (i) => strip.select(i),
29136
+ shown: () => strip.getButtons(),
29137
+ anchor: strip.element
29138
+ };
29139
+ groupStripOptions.set(strip, o);
29140
+ }
29141
+ return o;
29142
+ };
28996
29143
  const stripDigits = () => visible(presetStrip.element) && presetStrip.getEntries().length > 1;
28997
29144
  const stripOptions = {
28998
29145
  get count() {
@@ -29003,7 +29150,12 @@ function buildPanel(controls, host, options) {
29003
29150
  anchor: presetStrip.element
29004
29151
  };
29005
29152
  const stripOptionsAt = (node) => {
29006
- if (hintCtrl.isActive || !node || !stripDigits())
29153
+ if (hintCtrl.isActive || !node || stripDigitsShown)
29154
+ return;
29155
+ const own = groupStripAt(node);
29156
+ if (own)
29157
+ return visible(own.element) && own.getEntries().length > 1 && !own.nameField() ? optionsOfStrip(own) : undefined;
29158
+ if (!stripDigits())
29007
29159
  return;
29008
29160
  const at = presetStrip.element.contains(node) || focusLevel(node) === "panel";
29009
29161
  return at && !presetStrip.nameField() ? stripOptions : undefined;
@@ -29240,9 +29392,10 @@ function buildPanel(controls, host, options) {
29240
29392
  }
29241
29393
  if (pressed("presets", event)) {
29242
29394
  const direction = key === keys.presets[0] ? -1 : 1;
29243
- const presetsShown = visible(presetStrip.element) && presetStrip.getEntries().length > 1;
29395
+ const strip = groupStripAt(target) ?? presetStrip;
29396
+ const presetsShown = visible(strip.element) && strip.getEntries().length > 1;
29244
29397
  if (presetsShown)
29245
- presetStrip.cycle(direction);
29398
+ strip.cycle(direction);
29246
29399
  else if (!cycleTab(direction))
29247
29400
  return;
29248
29401
  event.preventDefault();
@@ -29275,6 +29428,8 @@ function buildPanel(controls, host, options) {
29275
29428
  search.focus();
29276
29429
  return;
29277
29430
  }
29431
+ if (/^[1-9]$/.test(key) && groupStripAt(target))
29432
+ return;
29278
29433
  if (/^[1-9]$/.test(key)) {
29279
29434
  const group = page ? focusedGroup(target) : undefined;
29280
29435
  const groupDigits = Boolean(group && groupChips.has(group));
@@ -29335,7 +29490,7 @@ function buildPanel(controls, host, options) {
29335
29490
  }
29336
29491
  if (pressed("save", event) && showVariants) {
29337
29492
  event.preventDefault();
29338
- presetStrip.save();
29493
+ (groupStripAt(target) ?? presetStrip).save();
29339
29494
  return;
29340
29495
  }
29341
29496
  const index = rowAt(target);
@@ -29834,6 +29989,7 @@ function buildPanel(controls, host, options) {
29834
29989
  ...undock ? {} : { headerDie: { button: randomButton, die } },
29835
29990
  locks: lockIcons,
29836
29991
  presetStrip,
29992
+ groupStrips: () => groupStrips.values(),
29837
29993
  tabs: tabStrip,
29838
29994
  groups: gpuGroups,
29839
29995
  cards,
@@ -29962,6 +30118,8 @@ function buildPanel(controls, host, options) {
29962
30118
  for (const c of groupChips.values())
29963
30119
  c.dispose();
29964
30120
  presetStrip.dispose();
30121
+ for (const strip of groupStrips.values())
30122
+ strip.dispose();
29965
30123
  tabStrip.dispose();
29966
30124
  tabMotion.finish();
29967
30125
  doc.removeEventListener("keydown", onGlobalKey);