@kitlangton/knobs 0.4.0-next.16 → 0.4.0-next.17

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
@@ -1825,6 +1825,14 @@ var init_schema = __esm(() => {
1825
1825
  doc: "Panel and card radius, concentric with controls.",
1826
1826
  deprecated: ["--k-panel-radius", "--ku-panel-radius"]
1827
1827
  },
1828
+ {
1829
+ name: "radius-frame",
1830
+ tier: "semantic",
1831
+ type: "length",
1832
+ group: "Shape",
1833
+ source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
1834
+ doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0."
1835
+ },
1828
1836
  {
1829
1837
  name: "round",
1830
1838
  tier: "semantic",
@@ -2943,7 +2951,16 @@ var init_glide = __esm(() => {
2943
2951
  "border-subtle",
2944
2952
  "bg-page"
2945
2953
  ];
2946
- LENGTH_TOKENS = ["pad", "inset", "row", "gap", "radius", "radius-panel", "font-size"];
2954
+ LENGTH_TOKENS = [
2955
+ "pad",
2956
+ "inset",
2957
+ "row",
2958
+ "gap",
2959
+ "radius",
2960
+ "radius-panel",
2961
+ "radius-frame",
2962
+ "font-size"
2963
+ ];
2947
2964
  NUMBER_TOKENS = ["weight-label", "blur", "glow", "glass", "glass-tone"];
2948
2965
  ANIMATED = [...COLOR_TOKENS, ...LENGTH_TOKENS, ...NUMBER_TOKENS, "width"];
2949
2966
  TOKEN_TRANSITIONS = ANIMATED.map((t) => `--kt-${t} var(--k-token-t)`).join(",");
@@ -3129,13 +3146,15 @@ ${GRAD_PROPS}
3129
3146
  catches its top. The light rides on the edge (a shadow raised by its width, so only its top
3130
3147
  shows), not inside the card's 1px border, where it read as a second edge. */
3131
3148
  .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);
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;
3149
+ background:color-mix(in srgb,var(--_k-bg) calc(100% - var(--_k-glass) * 20%),transparent);border:1px solid transparent;border-radius:var(--_k-radius-frame);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;
3133
3150
  /* Armed (focus inside: keys come here), the edge takes the accent and a faint glow lifts it. */
3134
3151
  --ka-edge:color-mix(in srgb,var(--_k-accent) calc(var(--k-armed) * var(--k-armed-edge) * 100%),var(--_k-border-subtle));
3135
3152
  --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);
3136
3153
  --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)));
3137
3154
  -webkit-backdrop-filter:var(--k-backdrop);backdrop-filter:var(--k-backdrop);
3138
3155
  animation:knobs-in .16s var(--k-ease);}
3156
+ /* On a phone a floating panel takes the width, less a gutter, and scrolls inside the height. */
3157
+ @media(max-width:420px){.knobs-panel:not(.knobs-inline){width:calc(100vw - 16px)}}
3139
3158
  .knobs-panel[data-restored]{animation:none}
3140
3159
  /* Opening from its chip, the panel's chrome rides a separate shape (see morph.ts) while the
3141
3160
  content, laid out at full size, is clipped to it and revealed top first. */
@@ -3169,8 +3188,9 @@ ${GRAD_PROPS}
3169
3188
  :is(.knobs-panel,.knobs-menu) :is(:focus,:focus-visible):where(:not(.knobs-accessory *)){outline:none}
3170
3189
 
3171
3190
  /* Header */
3172
- .knobs-panel header{position:relative;display:flex;align-items:center;justify-content:space-between;gap:6px;cursor:grab;touch-action:none;padding:0 var(--_k-inset) var(--k-section-gap);min-height:var(--k-icon-btn)}
3173
- .knobs-panel.knobs-inline header,.knobs-panel.knobs-inline header:active{cursor:default}
3191
+ /* A floating panel's header carries it; the hand shows only while it's carrying (data-dragging). */
3192
+ .knobs-panel header{position:relative;display:flex;align-items:center;justify-content:space-between;gap:6px;touch-action:none;padding:0 var(--_k-inset) var(--k-section-gap);min-height:var(--k-icon-btn)}
3193
+ .knobs-panel[data-dragging]>header,.knobs-panel[data-dragging]>header *{cursor:grabbing}
3174
3194
  /* The title row stays pinned while the rows scroll under it, in the panel (it scrolls once it's
3175
3195
  bounded) or, with .knobs-pin-ancestor (which also makes the panel overflow:visible), in the
3176
3196
  nearest scroller around it. Once rows are under it, and only then, it frosts (backdrop blur over
@@ -3185,7 +3205,7 @@ ${GRAD_PROPS}
3185
3205
  @supports (animation-timeline:scroll()){
3186
3206
  .knobs-panel:not(.knobs-page){scroll-timeline:--knobs-self block}
3187
3207
  .knobs-panel.knobs-pin-ancestor:not(.knobs-page){overflow:visible;view-timeline:--knobs-view block}
3188
- .knobs-panel:not(.knobs-page)>header{position:sticky;top:0;z-index:4;border-radius:max(0px,calc(var(--_k-radius-panel) - 1px)) max(0px,calc(var(--_k-radius-panel) - 1px)) 0 0;
3208
+ .knobs-panel:not(.knobs-page)>header{position:sticky;top:0;z-index:4;border-radius:max(0px,calc(var(--_k-radius-frame) - 1px)) max(0px,calc(var(--_k-radius-frame) - 1px)) 0 0;
3189
3209
  --k-head-on:max(var(--k-head-s),var(--k-head-v));--k-head-bg:color-mix(in srgb,var(--_k-bg) 88%,transparent);
3190
3210
  background:color-mix(in srgb,var(--k-head-bg) calc(var(--k-head-on) * 100%),transparent);
3191
3211
  animation:knobs-head-s linear both;animation-timeline:--knobs-self;animation-range:0px 24px}
@@ -3195,7 +3215,6 @@ ${GRAD_PROPS}
3195
3215
  .knobs-panel[data-gpu-active="true"]:not(.knobs-page)>header{background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
3196
3216
  .knobs-panel[data-gpu-active="true"]:not(.knobs-page)>header::after{display:none}
3197
3217
  }
3198
- .knobs-panel header:active{cursor:grabbing}
3199
3218
  .knobs-panel header strong{flex:1;min-width:0;font-weight:var(--_k-weight-title);font-size:var(--_k-text-title);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s var(--k-ease),color ${ARMED_T};
3200
3219
  color:color-mix(in srgb,var(--_k-fg) calc(var(--k-title-rest) * 100%),var(--_k-fg-2))}
3201
3220
  /* At rest a panel's title is a little quiet; armed (keys come here) it's at full strength. */
@@ -5146,12 +5165,24 @@ function createRegistry(doc) {
5146
5165
  return;
5147
5166
  if (e.metaKey || e.ctrlKey || e.altKey || e.key !== PAGE_HINT_KEY)
5148
5167
  return;
5149
- if (isTyping(e.target) || !wide())
5168
+ if (isTyping(e.target) || !wide(e.target))
5150
5169
  return;
5151
5170
  if (registry.start())
5152
5171
  e.preventDefault();
5153
5172
  };
5154
- const wide = () => listening && (sources.length > 1 || [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel")));
5173
+ const hostHints = () => [...doc.querySelectorAll("[data-hint]")].some((el) => !el.closest(".knobs-panel"));
5174
+ const wide = (target) => {
5175
+ if (!listening)
5176
+ return false;
5177
+ if (sources.some((s) => s.listen && s.rank >= 2) || hostHints())
5178
+ return true;
5179
+ if (sources.length < 2)
5180
+ return false;
5181
+ if (target === undefined)
5182
+ return true;
5183
+ const piece = target?.closest?.(".knobs-ui,.knobs-pane-body");
5184
+ return Boolean(piece?.classList.contains("knobs-ui"));
5185
+ };
5155
5186
  const relisten = () => {
5156
5187
  const next = sources.some((s) => s.listen);
5157
5188
  if (next === listening)
@@ -9135,6 +9166,7 @@ function readPanelTokens(panel, version = -1) {
9135
9166
  const trayPad = numProp(style, "tray-pad", unit * 0.5);
9136
9167
  const radius = numProp(style, "radius", 8);
9137
9168
  const panelRadius = numProp(style, "radius-panel", radius + unit);
9169
+ const frameRadius = numProp(style, "radius-frame", radius > 0 ? radius + pad + 1 : 0);
9138
9170
  const radiusMd = Math.max(0, radius - trayPad);
9139
9171
  const radiusSm = Math.max(0, radius - trayPad * 2);
9140
9172
  const fontSize = parseFloat(style.fontSize) || numProp(style, "font-size", 12);
@@ -9191,6 +9223,7 @@ function readPanelTokens(panel, version = -1) {
9191
9223
  trayPad,
9192
9224
  radius,
9193
9225
  panelRadius,
9226
+ frameRadius,
9194
9227
  radiusMd,
9195
9228
  radiusSm,
9196
9229
  fontSize,
@@ -13039,7 +13072,7 @@ class GpuOverlayManager {
13039
13072
  const morph = panel.morph?.(this.frameNow);
13040
13073
  const shown = morph ? clamp2(morph.progress, 0, 1) : 1;
13041
13074
  const card = morph ? new DOMRect(morph.x, morph.y, morph.w, morph.h) : pRect;
13042
- const cardRadius = morph ? morph.radius : tokens.panelRadius;
13075
+ const cardRadius = morph ? morph.radius : tokens.frameRadius;
13043
13076
  if (morph)
13044
13077
  anyMoving = true;
13045
13078
  const masked = morph !== undefined && this.maskCount < MAX_MASKS;
@@ -19880,7 +19913,6 @@ var compareCss = `
19880
19913
  :where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_ku-on-media) 16%,transparent);color:var(--_ku-on-media);transition-duration:calc(var(--_k-hover-in) * 1ms)}
19881
19914
  .knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}
19882
19915
  .knobs-cmp-note:empty{display:none}
19883
- .knobs-cmp-strip-host{flex:none;height:60px;min-height:0}
19884
19916
  .knobs-cmp-timeline-host{flex:none;min-height:0}
19885
19917
  /* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */
19886
19918
  .knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;
@@ -19900,6 +19932,9 @@ var playerCss = `
19900
19932
  .knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;
19901
19933
  opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}
19902
19934
  .knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}
19935
+ /* Mid-scrub, the cached frame under the hand, over the video until it lands. */
19936
+ .knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
19937
+ .knobs-player-preview[hidden]{display:none}
19903
19938
  .knobs-player-line{flex:none;min-height:0}
19904
19939
  /* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share
19905
19940
  about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it
@@ -20706,6 +20741,62 @@ var init_overlay_model = __esm(() => {
20706
20741
  opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
20707
20742
  });
20708
20743
 
20744
+ // src/ui/tooltip-model.ts
20745
+ function tipSchedule(clock, view) {
20746
+ let shown = null;
20747
+ let pending = null;
20748
+ let timer;
20749
+ let hiddenAt = -Infinity;
20750
+ const cancel = () => {
20751
+ if (pending)
20752
+ clock.clear(timer);
20753
+ pending = null;
20754
+ };
20755
+ const hide = (now = false) => {
20756
+ cancel();
20757
+ if (!shown)
20758
+ return;
20759
+ shown = null;
20760
+ hiddenAt = clock.now();
20761
+ view.hide(now);
20762
+ };
20763
+ return {
20764
+ get shown() {
20765
+ return shown;
20766
+ },
20767
+ get pending() {
20768
+ return pending;
20769
+ },
20770
+ want(tip, options = {}) {
20771
+ if (tip.el === shown?.el || tip.el === pending?.el)
20772
+ return;
20773
+ cancel();
20774
+ const handoff = shown !== null || clock.now() - hiddenAt < HANDOFF_MS;
20775
+ if (handoff || options.now) {
20776
+ shown = tip;
20777
+ view.show(tip, handoff);
20778
+ return;
20779
+ }
20780
+ pending = tip;
20781
+ timer = clock.set(() => {
20782
+ pending = null;
20783
+ if (!view.connected(tip))
20784
+ return;
20785
+ shown = tip;
20786
+ view.show(tip, false);
20787
+ }, options.delay ?? TIP_DELAY_MS);
20788
+ },
20789
+ hide,
20790
+ leave(within) {
20791
+ if (pending && within(pending))
20792
+ cancel();
20793
+ if (shown && within(shown))
20794
+ hide();
20795
+ }
20796
+ };
20797
+ }
20798
+ var HANDOFF_MS = 400, TIP_DELAY_MS = 500;
20799
+
20709
20800
  // src/ui/tooltip.ts
20710
20801
  function tipOf(start) {
20711
20802
  let node = start instanceof Element ? start : null;
@@ -20739,9 +20830,6 @@ function controllerOf(doc) {
20739
20830
  let text;
20740
20831
  let keys;
20741
20832
  let target = null;
20742
- let pending = null;
20743
- let timer = 0;
20744
- let hiddenAt = -Infinity;
20745
20833
  let quiet = null;
20746
20834
  let keyboard = false;
20747
20835
  const build = (look) => {
@@ -20786,41 +20874,32 @@ function controllerOf(doc) {
20786
20874
  else
20787
20875
  motion.show();
20788
20876
  };
20789
- const hide = (now = false) => {
20790
- win.clearTimeout(timer);
20791
- pending = null;
20792
- if (!target)
20793
- return;
20794
- target.removeAttribute("aria-describedby");
20795
- target = null;
20796
- hiddenAt = performance.now();
20797
- if (now)
20798
- motion?.snap(0);
20799
- else
20800
- motion?.hide();
20801
- };
20802
- const want = (found) => {
20877
+ const schedule = tipSchedule({
20878
+ set: (run, ms) => win.setTimeout(run, ms),
20879
+ clear: (handle) => win.clearTimeout(handle),
20880
+ now: () => performance.now()
20881
+ }, {
20882
+ show: ({ el, tip }, snap) => show(el, tip, snap),
20883
+ hide: (now) => {
20884
+ target?.removeAttribute("aria-describedby");
20885
+ target = null;
20886
+ if (now)
20887
+ motion?.snap(0);
20888
+ else
20889
+ motion?.hide();
20890
+ },
20891
+ connected: ({ el }) => el.isConnected
20892
+ });
20893
+ const hide = (now = false) => schedule.hide(now);
20894
+ const want = (found, now = false) => {
20803
20895
  if (!found)
20804
20896
  return hide();
20805
- if (found.el === target || found.el === pending?.el)
20806
- return;
20807
20897
  if (found.el === quiet)
20808
20898
  return;
20809
- win.clearTimeout(timer);
20810
- const handoff = target !== null || performance.now() - hiddenAt < HANDOFF_MS;
20811
- if (handoff) {
20812
- pending = null;
20813
- show(found.el, found.tip, true);
20814
- return;
20815
- }
20816
- if (target)
20817
- hide();
20818
- pending = found;
20819
- timer = win.setTimeout(() => {
20820
- pending = null;
20821
- if (found.el.isConnected)
20822
- show(found.el, found.tip, false);
20823
- }, found.tip.opts.delay ?? 500);
20899
+ schedule.want(found, {
20900
+ now,
20901
+ ...found.tip.opts.delay !== undefined ? { delay: found.tip.opts.delay } : {}
20902
+ });
20824
20903
  };
20825
20904
  doc.addEventListener("pointerover", (e) => {
20826
20905
  if (e.pointerType === "touch")
@@ -20854,11 +20933,10 @@ function controllerOf(doc) {
20854
20933
  doc.addEventListener("focusin", (e) => {
20855
20934
  const found = tipOf(e.target);
20856
20935
  if (found && keyboard && e.target.matches(":focus-visible"))
20857
- want(found);
20936
+ want(found, true);
20858
20937
  });
20859
20938
  doc.addEventListener("focusout", (e) => {
20860
- if (target && target.contains(e.target))
20861
- hide();
20939
+ schedule.leave(({ el }) => el.contains(e.target));
20862
20940
  });
20863
20941
  doc.addEventListener("scroll", () => target && hide(true), true);
20864
20942
  const c = { hide };
@@ -20877,7 +20955,7 @@ function tooltip(el, content, options = {}) {
20877
20955
  c.hide(true);
20878
20956
  };
20879
20957
  }
20880
- var tips, HANDOFF_MS = 400, controllers;
20958
+ var tips, controllers;
20881
20959
  var init_tooltip = __esm(() => {
20882
20960
  init_keys();
20883
20961
  init_layer();
@@ -22293,6 +22371,14 @@ var init_inspector = __esm(() => {
22293
22371
  CAP_MORPH = springLinear(SPRINGS.snappy);
22294
22372
  });
22295
22373
 
22374
+ // src/panel/description-ids.ts
22375
+ function descriptionIds(storeId) {
22376
+ const prefix = `knobs-desc-${++panels2}-${storeId.replace(/[^\w-]/g, "-")}`;
22377
+ let n = 0;
22378
+ return () => `${prefix}-${++n}`;
22379
+ }
22380
+ var panels2 = 0;
22381
+
22296
22382
  // src/panel/search.ts
22297
22383
  function createSearch(doc, options) {
22298
22384
  const element = doc.createElement("div");
@@ -25531,7 +25617,7 @@ function morphFromChip(chip, root, wake, restored) {
25531
25617
  if (shape)
25532
25618
  return shape;
25533
25619
  const cs = getComputedStyle(root);
25534
- const r = parseFloat(cs.getPropertyValue("--kv-radius-panel"));
25620
+ const r = parseFloat(cs.getPropertyValue("--kv-radius-frame"));
25535
25621
  if (Number.isFinite(r))
25536
25622
  panelRadius = r;
25537
25623
  shape = doc.createElement("div");
@@ -25747,6 +25833,7 @@ function foldMemory(id, storage) {
25747
25833
  const next = {
25748
25834
  sections: { ...data.sections },
25749
25835
  groups: { ...data.groups },
25836
+ ...data.subsections ? { subsections: { ...data.subsections } } : {},
25750
25837
  [kind]: { ...data[kind], [at]: folded }
25751
25838
  };
25752
25839
  try {
@@ -25758,6 +25845,18 @@ function foldMemory(id, storage) {
25758
25845
  var init_folds = () => {};
25759
25846
 
25760
25847
  // src/panel/position.ts
25848
+ function savedPosition(left, top, width, view) {
25849
+ const right = left + width / 2 > view / 2;
25850
+ return right ? { x: view - left - width, y: top, right } : { x: left, y: top };
25851
+ }
25852
+ function readPosition(raw) {
25853
+ try {
25854
+ const saved = JSON.parse(raw ?? "null");
25855
+ if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y))
25856
+ return { x: saved.x, y: saved.y, ...saved.right === true ? { right: true } : {} };
25857
+ } catch {}
25858
+ return;
25859
+ }
25761
25860
  function positionPanel(panel, header, anchor, id, storage) {
25762
25861
  const doc = panel.ownerDocument;
25763
25862
  const win = doc.defaultView;
@@ -25767,16 +25866,14 @@ function positionPanel(panel, header, anchor, id, storage) {
25767
25866
  let x = bounds.right + w <= win.innerWidth - 8 ? bounds.right + 8 : clampX(bounds.left - w - 8);
25768
25867
  let y = Math.max(8, bounds.top);
25769
25868
  const key = storageKey(id, "position");
25770
- let hasSaved = false;
25869
+ let saved;
25771
25870
  try {
25772
- const saved = JSON.parse(storage()?.getItem(key) ?? "null");
25773
- if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) {
25774
- x = saved.x;
25775
- y = saved.y;
25776
- hasSaved = true;
25777
- }
25871
+ saved = readPosition(storage()?.getItem(key));
25778
25872
  } catch {}
25779
- if (!hasSaved) {
25873
+ if (saved) {
25874
+ x = leftOf(saved, w, win.innerWidth);
25875
+ y = saved.y;
25876
+ } else {
25780
25877
  const others = [...doc.querySelectorAll(".knobs-panel:not(.knobs-inline)")].filter((el) => el !== panel).map((el) => el.getBoundingClientRect());
25781
25878
  if (others.length > 0) {
25782
25879
  const overlaps = (cx, cy) => others.some((r) => Math.abs(cx - r.left) < w - 24 && Math.abs(cy - r.top) < 120);
@@ -25800,6 +25897,8 @@ function positionPanel(panel, header, anchor, id, storage) {
25800
25897
  }
25801
25898
  let shown = { x, y };
25802
25899
  const constrain = () => {
25900
+ if (saved && !drag)
25901
+ x = leftOf(saved, panel.offsetWidth, win.innerWidth);
25803
25902
  shown = {
25804
25903
  x: Math.max(8, Math.min(x, win.innerWidth - panel.offsetWidth - 8)),
25805
25904
  y: Math.max(8, Math.min(y, win.innerHeight - panel.offsetHeight - 8))
@@ -25813,22 +25912,35 @@ function positionPanel(panel, header, anchor, id, storage) {
25813
25912
  return;
25814
25913
  event.preventDefault();
25815
25914
  drag = { id: event.pointerId, x: event.clientX, y: event.clientY, left: shown.x, top: shown.y };
25816
- header.setPointerCapture(event.pointerId);
25817
25915
  };
25818
25916
  header.onpointermove = (event) => {
25819
25917
  if (!drag || event.pointerId !== drag.id)
25820
25918
  return;
25919
+ if (!("dragging" in panel.dataset)) {
25920
+ if (!(event.buttons & 1))
25921
+ return void (drag = undefined);
25922
+ if (Math.hypot(event.clientX - drag.x, event.clientY - drag.y) <= 2)
25923
+ return;
25924
+ panel.dataset.dragging = "";
25925
+ header.setPointerCapture(event.pointerId);
25926
+ }
25821
25927
  x = drag.left + event.clientX - drag.x;
25822
25928
  y = drag.top + event.clientY - drag.y;
25823
25929
  constrain();
25824
25930
  };
25931
+ header.onpointerup = header.onpointercancel = () => {
25932
+ if (drag && !("dragging" in panel.dataset))
25933
+ drag = undefined;
25934
+ };
25825
25935
  header.onlostpointercapture = () => {
25826
25936
  if (!drag)
25827
25937
  return;
25828
25938
  drag = undefined;
25939
+ delete panel.dataset.dragging;
25829
25940
  ({ x, y } = shown);
25941
+ saved = savedPosition(x, y, panel.offsetWidth, win.innerWidth);
25830
25942
  try {
25831
- storage()?.setItem(key, JSON.stringify({ x, y }));
25943
+ storage()?.setItem(key, JSON.stringify(saved));
25832
25944
  } catch {}
25833
25945
  };
25834
25946
  const observer = new ResizeObserver(constrain);
@@ -25843,6 +25955,7 @@ function positionPanel(panel, header, anchor, id, storage) {
25843
25955
  }
25844
25956
  };
25845
25957
  }
25958
+ var leftOf = (saved, width, view) => saved.right ? view - width - saved.x : saved.x;
25846
25959
  var init_position = () => {};
25847
25960
 
25848
25961
  // src/panel/dock.ts
@@ -25986,7 +26099,7 @@ function createPresence(root, options) {
25986
26099
  return;
25987
26100
  const look = presenceLook(spring.x, inline, reduced);
25988
26101
  const r = root.getBoundingClientRect();
25989
- radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-panel")) || 12;
26102
+ radius ??= parseFloat(win.getComputedStyle(root).getPropertyValue("--kv-radius-frame")) || 12;
25990
26103
  const s = look.scale;
25991
26104
  const ox = r.left + r.width / 2;
25992
26105
  const oy = inline ? r.top : r.top + r.height / 2;
@@ -26707,8 +26820,11 @@ function buildPanel(controls, host, options) {
26707
26820
  if (page) {
26708
26821
  if (remember)
26709
26822
  foldsKept?.set("groups", group, next);
26710
- } else if (sessionKept)
26823
+ } else if (sessionKept) {
26711
26824
  writeSession(controls.id, { groups: { ...readSession(controls.id).groups, [group]: next } });
26825
+ if (remember)
26826
+ panelFolds?.set("groups", group, next);
26827
+ }
26712
26828
  refreshFolds();
26713
26829
  positioned?.constrain();
26714
26830
  wakeGpu();
@@ -26757,8 +26873,7 @@ function buildPanel(controls, host, options) {
26757
26873
  gRandom.dataset.hint = `${title} random`;
26758
26874
  gRandom.querySelector(".knobs-icon")?.replaceWith(gDie.node);
26759
26875
  gRandom.onclick = (event) => run(() => rerollGroup(groupName, event.altKey));
26760
- const rollable = Object.values(controls.schema).some((f) => f.group === groupName && hasValue(f));
26761
- if (!bound("random") && !bound("mutate") || !rollable)
26876
+ if (!bound("random") && !bound("mutate"))
26762
26877
  gRandom.hidden = true;
26763
26878
  groupDice.set(groupName, { button: gRandom, die: gDie });
26764
26879
  const presets = page ? (controls.groupPresets[groupName] ?? []).slice(0, 9) : [];
@@ -26877,7 +26992,7 @@ function buildPanel(controls, host, options) {
26877
26992
  gHead.tabIndex = -1;
26878
26993
  gHead.setAttribute("role", "button");
26879
26994
  gHead.setAttribute("aria-label", title);
26880
- const folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName]) ?? foldAt;
26995
+ const folded = (page ? foldsKept?.get("groups", groupName) : session.groups?.[groupName] ?? panelFolds?.get("groups", groupName)) ?? foldAt;
26881
26996
  folds.set(groupName, folded);
26882
26997
  gHead.setAttribute("aria-expanded", String(!folded));
26883
26998
  if (!page)
@@ -26953,6 +27068,7 @@ function buildPanel(controls, host, options) {
26953
27068
  const session = remember ? readSession(controls.id) : {};
26954
27069
  const foldPort = page?.foldStorage !== undefined ? page.foldStorage : controls.persist === "none" ? null : controls.storage;
26955
27070
  const foldsKept = page && foldPort ? foldMemory(controls.id, () => foldPort) : null;
27071
+ const panelFolds = !page && remember && foldPort ? foldMemory(controls.id, () => foldPort) : null;
26956
27072
  const tracks = new Map;
26957
27073
  const toolbars = new Map;
26958
27074
  const keyedActions = new Map;
@@ -26993,7 +27109,8 @@ function buildPanel(controls, host, options) {
26993
27109
  label,
26994
27110
  ...duration.group !== undefined ? { group: duration.group } : {},
26995
27111
  ...duration.section !== undefined ? { section: duration.section } : {},
26996
- ...duration.shortcut !== undefined ? { shortcut: duration.shortcut } : {}
27112
+ ...duration.shortcut !== undefined ? { shortcut: duration.shortcut } : {},
27113
+ ...duration.description !== undefined ? { description: duration.description } : {}
26997
27114
  },
26998
27115
  parts: { duration, bounce },
26999
27116
  group: duration.group,
@@ -27503,6 +27620,41 @@ function buildPanel(controls, host, options) {
27503
27620
  return;
27504
27621
  slot.replaceChildren(...node ? [node] : []);
27505
27622
  }
27623
+ const descriptions = el("div");
27624
+ descriptions.hidden = true;
27625
+ root.append(descriptions);
27626
+ const described = new Map;
27627
+ const descriptionId = descriptionIds(controls.id);
27628
+ function describe(row) {
27629
+ const text = row.field.description?.trim() ?? "";
27630
+ const had = described.get(row);
27631
+ if ((had?.text ?? "") === text)
27632
+ return;
27633
+ const inputs = row.choice ? [row.choice.seg, row.choice.menu] : [row.input];
27634
+ if (had) {
27635
+ had.untip();
27636
+ had.node.remove();
27637
+ described.delete(row);
27638
+ for (const input of inputs)
27639
+ input.removeAttribute("aria-describedby");
27640
+ }
27641
+ if (!text)
27642
+ return;
27643
+ const node = el("span", text);
27644
+ node.id = descriptionId();
27645
+ descriptions.append(node);
27646
+ for (const input of inputs)
27647
+ input.setAttribute("aria-describedby", node.id);
27648
+ described.set(row, { text, node, untip: tooltip(row.row, text) });
27649
+ }
27650
+ const undescribe = (row) => {
27651
+ const had = described.get(row);
27652
+ if (!had)
27653
+ return;
27654
+ had.untip();
27655
+ had.node.remove();
27656
+ described.delete(row);
27657
+ };
27506
27658
  const fieldFlag = (field, name) => {
27507
27659
  const v = field[name];
27508
27660
  return typeof v === "function" ? Boolean(v(controls.get())) : Boolean(v);
@@ -27548,6 +27700,7 @@ function buildPanel(controls, host, options) {
27548
27700
  wakeGpu();
27549
27701
  };
27550
27702
  const dropGpu = (row) => {
27703
+ undescribe(row);
27551
27704
  row.dispose?.();
27552
27705
  row.slider?.dispose();
27553
27706
  row.editor?.dispose();
@@ -27606,11 +27759,13 @@ function buildPanel(controls, host, options) {
27606
27759
  if (existing && existing.shape === shapeOf(spec)) {
27607
27760
  kept.delete(spec.id);
27608
27761
  existing.retune(spec);
27762
+ describe(existing);
27609
27763
  next.push(existing);
27610
27764
  continue;
27611
27765
  }
27612
27766
  const row = buildRow(spec);
27613
27767
  decorate(row);
27768
+ describe(row);
27614
27769
  gpuRows.push(...row.gpu);
27615
27770
  entering.add(row);
27616
27771
  next.push(row);
@@ -27814,7 +27969,7 @@ function buildPanel(controls, host, options) {
27814
27969
  const caret = caretSvg();
27815
27970
  node.append(label, caret);
27816
27971
  if (!subFolds.has(id))
27817
- subFolds.set(id, session.subsections?.[id] ?? options.subsectionFolded?.[name] ?? false);
27972
+ subFolds.set(id, session.subsections?.[id] ?? panelFolds?.get("subsections", id) ?? options.subsectionFolded?.[name] ?? false);
27818
27973
  node.setAttribute("aria-expanded", String(!subFolds.get(id)));
27819
27974
  node.addEventListener("click", (event) => {
27820
27975
  if (event.detail < 2)
@@ -27848,10 +28003,12 @@ function buildPanel(controls, host, options) {
27848
28003
  if (next && rows.some((r) => r.subhead === id && r.row.contains(doc.activeElement)))
27849
28004
  sh.el.focus({ preventScroll: true, focusVisible: false });
27850
28005
  applyVisibility("reveal");
27851
- if (remember)
28006
+ if (remember) {
27852
28007
  writeSession(controls.id, {
27853
28008
  subsections: { ...readSession(controls.id).subsections, [id]: next }
27854
28009
  });
28010
+ panelFolds?.set("subsections", id, next);
28011
+ }
27855
28012
  }
27856
28013
  const groupTab = (group) => {
27857
28014
  for (const f of Object.values(controls.schema))
@@ -27937,7 +28094,7 @@ function buildPanel(controls, host, options) {
27937
28094
  const searching = () => search?.filtering() ?? false;
27938
28095
  const rowMatches = (r) => {
27939
28096
  const query = search.query();
27940
- if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.group && groupDescription(r.group), r.field.subsection))
28097
+ if (query && !matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group), r.group && groupDescription(r.group), r.field.subsection, r.field.description))
27941
28098
  return false;
27942
28099
  for (const chip of search.active())
27943
28100
  if (!r.keys.some((k) => chipTests.get(chip)?.(k)))
@@ -28059,6 +28216,10 @@ function buildPanel(controls, host, options) {
28059
28216
  };
28060
28217
  const keysOf = (indices) => indices.flatMap((i) => rows[i]?.keys ?? []);
28061
28218
  const groupKeys = (group) => rows.filter((r) => r.group === group).flatMap((r) => r.keys);
28219
+ const mayRoll = (key) => {
28220
+ const field = controls.schema[key];
28221
+ return Boolean(field && hasValue(field) && field.random !== false);
28222
+ };
28062
28223
  const rerollGroup = (group, nudge) => {
28063
28224
  const roller = groupDice.get(group)?.die;
28064
28225
  if (!roller)
@@ -28205,22 +28366,29 @@ function buildPanel(controls, host, options) {
28205
28366
  apply.setAttribute("aria-label", "Import JSON");
28206
28367
  apply.hidden = true;
28207
28368
  const copy = async (value, inPlace = true) => {
28208
- try {
28209
- await doc.defaultView.navigator.clipboard.writeText(value);
28369
+ const done = () => {
28210
28370
  if (inPlace && !copyButton.hidden && headerShown())
28211
28371
  copied();
28212
28372
  else
28213
28373
  say("Copied.");
28374
+ };
28375
+ try {
28376
+ await doc.defaultView.navigator.clipboard.writeText(value);
28377
+ done();
28214
28378
  } catch {
28215
- text.value = value;
28216
- if (page)
28217
- togglePopover(true);
28218
- reveal(text, true);
28219
- apply.hidden = true;
28220
- positioned?.constrain();
28221
- text.focus();
28222
- text.select();
28223
- say("Clipboard unavailable. Copy the selected text.", 6000);
28379
+ if (copyByCommand(doc, value))
28380
+ done();
28381
+ else {
28382
+ text.value = value;
28383
+ if (page)
28384
+ togglePopover(true);
28385
+ reveal(text, true);
28386
+ apply.hidden = true;
28387
+ positioned?.constrain();
28388
+ text.focus();
28389
+ text.select();
28390
+ say("Clipboard unavailable. Copy the selected text.", 6000);
28391
+ }
28224
28392
  }
28225
28393
  wakeGpu();
28226
28394
  };
@@ -28468,10 +28636,17 @@ function buildPanel(controls, host, options) {
28468
28636
  }
28469
28637
  if (randomButton.disabled !== (unlocked === 0))
28470
28638
  randomButton.disabled = unlocked === 0;
28639
+ const rolls = (bound("random") || bound("mutate")) && shows("random");
28640
+ const headerHidden = !rolls || !rows.some((r) => r.keys.some(mayRoll));
28641
+ if (randomButton.hidden !== headerHidden)
28642
+ randomButton.hidden = headerHidden;
28471
28643
  for (const [group, { button }] of groupDice) {
28472
28644
  const off = !options.groupRandom && !groupKeys(group).some((k) => controls.canRoll(k));
28473
28645
  if (button.disabled !== off)
28474
28646
  button.disabled = off;
28647
+ const hidden = !bound("random") && !bound("mutate") || !options.groupRandom && !groupKeys(group).some(mayRoll);
28648
+ if (button.hidden !== hidden)
28649
+ button.hidden = hidden;
28475
28650
  }
28476
28651
  if (root.hasAttribute("data-comparing") !== controls.comparing)
28477
28652
  root.toggleAttribute("data-comparing", controls.comparing);
@@ -28656,7 +28831,7 @@ function buildPanel(controls, host, options) {
28656
28831
  layout.change(() => {
28657
28832
  layout.openFolds(Boolean(query));
28658
28833
  for (const r of rows) {
28659
- const show = !query || matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group, r.section), r.group && groupDescription(r.group), r.section, r.section && sectionTitle(r.section), r.section && sectionDescription(r.section));
28834
+ const show = !query || matchesFilter(query, r.display, r.key, r.group, r.group && groupTitle(r.group, r.section), r.group && groupDescription(r.group), r.section, r.section && sectionTitle(r.section), r.section && sectionDescription(r.section), r.field.description);
28660
28835
  if (r.row.hidden === show)
28661
28836
  r.row.hidden = !show;
28662
28837
  }
@@ -29260,7 +29435,20 @@ function buildPanel(controls, host, options) {
29260
29435
  queueScope();
29261
29436
  });
29262
29437
  };
29438
+ const onPressOutside = (event) => {
29439
+ const target = event.target;
29440
+ if (page || !target || root.contains(target))
29441
+ return;
29442
+ if (target.closest?.(".knobs-root,.knobs-layers"))
29443
+ return;
29444
+ setTimeout(() => {
29445
+ const active = doc.activeElement;
29446
+ if (active && root.contains(active))
29447
+ active.blur();
29448
+ });
29449
+ };
29263
29450
  doc.addEventListener("pointerdown", onPress, true);
29451
+ doc.addEventListener("pointerdown", onPressOutside, true);
29264
29452
  doc.addEventListener("pointerup", onRelease, true);
29265
29453
  doc.addEventListener("pointercancel", onRelease, true);
29266
29454
  root.addEventListener("keydown", () => {
@@ -30362,6 +30550,7 @@ function buildPanel(controls, host, options) {
30362
30550
  lens.dispose();
30363
30551
  win?.cancelAnimationFrame(lensFrame);
30364
30552
  doc.removeEventListener("pointerdown", onPress, true);
30553
+ doc.removeEventListener("pointerdown", onPressOutside, true);
30365
30554
  doc.removeEventListener("pointerup", onRelease, true);
30366
30555
  doc.removeEventListener("pointercancel", onRelease, true);
30367
30556
  crumbs?.dispose();
@@ -30569,6 +30758,24 @@ function buildPanel(controls, host, options) {
30569
30758
  }
30570
30759
  return pageApi;
30571
30760
  }
30761
+ function copyByCommand(doc, value) {
30762
+ const was = doc.activeElement;
30763
+ const area = doc.createElement("textarea");
30764
+ area.value = value;
30765
+ area.setAttribute("readonly", "");
30766
+ area.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none";
30767
+ doc.body.append(area);
30768
+ area.select();
30769
+ let ok = false;
30770
+ try {
30771
+ ok = doc.execCommand("copy");
30772
+ } catch {
30773
+ ok = false;
30774
+ }
30775
+ area.remove();
30776
+ was?.focus({ preventScroll: true });
30777
+ return ok;
30778
+ }
30572
30779
  function createPanel(controls, anchor, close, options) {
30573
30780
  return buildPanel(controls, anchor.ownerDocument.body, {
30574
30781
  ...options,
@@ -30601,6 +30808,7 @@ var init_panel = __esm(() => {
30601
30808
  init_readout();
30602
30809
  init_readout_row();
30603
30810
  init_toast();
30811
+ init_tooltip();
30604
30812
  init_tabs();
30605
30813
  init_tab_transition();
30606
30814
  init_choice();
@@ -30747,7 +30955,7 @@ function attach(controls, element, options = {}) {
30747
30955
  const open = async (restored = false) => {
30748
30956
  if (panel) {
30749
30957
  unpress();
30750
- panel.focus();
30958
+ focusPanel(panel);
30751
30959
  return;
30752
30960
  }
30753
30961
  if (leaving?.reopen()) {
@@ -30758,7 +30966,7 @@ function attach(controls, element, options = {}) {
30758
30966
  writeSession(controls.id, { open: true });
30759
30967
  chip.setAttribute("aria-expanded", "true");
30760
30968
  update();
30761
- panel.focus();
30969
+ focusPanel(panel);
30762
30970
  return;
30763
30971
  }
30764
30972
  leaving = undefined;
@@ -30775,7 +30983,7 @@ function attach(controls, element, options = {}) {
30775
30983
  chip.setAttribute("aria-expanded", "true");
30776
30984
  update();
30777
30985
  if (!restored)
30778
- panel.focus();
30986
+ focusPanel(panel);
30779
30987
  } catch (cause) {
30780
30988
  chip.title = String(cause);
30781
30989
  renderFace("Retry knobs");
@@ -30833,6 +31041,7 @@ function attach(controls, element, options = {}) {
30833
31041
  element.removeAttribute("tabindex");
30834
31042
  };
30835
31043
  }
31044
+ var focusPanel = (panel) => panel.element.focus({ preventScroll: true });
30836
31045
  var MODE_KEY = "knobs:mode";
30837
31046
  var MODE_EVENT = "knobs-mode";
30838
31047
  function knobsMode(win) {