@kitlangton/knobs 0.4.0-next.10 → 0.4.0-next.11

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 CHANGED
@@ -765,7 +765,33 @@ panel.tab("emitter") // shows a tab; panel.tab() answers the one showing
765
765
  none show on every tab. The strip's underline slides to the chosen tab and the rows cross: what
766
766
  leaves folds away as what arrives unfolds, a moment behind. `⌥1`–`⌥9` pick a tab, and `[` / `]`
767
767
  cycle tabs when there's no preset strip. The tab last shown is remembered for these controls (and
768
- in the session).
768
+ in the session). The preset strip sits under the tabs and crosses a tab change with the rows; a
769
+ tab that no preset or saved variant moves shows no strip at all (not even its `+`).
770
+ - **Pinned groups.** Groups that apply to every tab (a global speed, a slow-motion switch) sit at
771
+ the top of the panel, under its one header and above the tab strip, and a tab change never moves
772
+ them. Name them on the store, `pinned: ["Global"]`, or say `pinned: true` on a field (the first
773
+ field of a group that says wins; a field without a group pins itself). A pinned field's `tab` is
774
+ ignored. Pinned groups keep their own die and reset. Presets and saved variants leave them out:
775
+ applying one keeps their values, saving one doesn't record them, a preset's chip doesn't drift
776
+ when they change, and their modified marks are against their defaults. Random rolls them freely
777
+ even under an active preset. The header's Random and Reset still cover everything, pinned
778
+ groups included, as they cover every tab; give a field `random: false` to keep it out of
779
+ Random. Keyboard order and search follow the panel top to bottom: pinned rows, the tabs, the
780
+ preset strip, then the tab's rows. `controls.isPinned(key)` says whether a key is pinned.
781
+
782
+ ```ts
783
+ const motion = defineControls(
784
+ "motion",
785
+ {
786
+ speed: knob(1, { min: 0.25, max: 4, step: 0.05, unit: "×", group: "Global" }),
787
+ slow: toggle(false, { label: "Slow motion (0.25×)", group: "Global" }),
788
+ sidebars: spring({ duration: 0.3, bounce: 0 }, { group: "Sidebars", tab: "Sidebars" }),
789
+ hover: spring({ duration: 0.14, bounce: 0 }, { group: "Experiments", tab: "Experiments" }),
790
+ },
791
+ { pinned: ["Global"], defaultLabel: "Shipped", presets: { snappy, smooth, calm } },
792
+ )
793
+ motion.mount(el, { title: "Knobs", tabs: ["Sidebars", "Experiments"] })
794
+ ```
769
795
  - **Sub-headings.** Fields with the same `subsection` in a group sit under a flat heading inside the
770
796
  group's card, split off by a hairline (never a card inside a card). They're gathered: a group's
771
797
  rows without a sub-heading come first, then each sub-heading's rows together, in the order the
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  __preload
3
- } from "./chunk-3ygqgy1x.js";
3
+ } from "./chunk-mjdgzad5.js";
4
4
  import {
5
5
  applyLook,
6
6
  panelLook,
@@ -9,7 +9,7 @@ import {
9
9
  writeSession,
10
10
  injectStyles,
11
11
  toggleLayer
12
- } from "./chunk-dyq4m24m.js";
12
+ } from "./chunk-j57rhjtm.js";
13
13
 
14
14
  // src/panel/attach.ts
15
15
  function attach(controls, element, options = {}) {
@@ -85,7 +85,7 @@ function attach(controls, element, options = {}) {
85
85
  return;
86
86
  loading = !0;
87
87
  try {
88
- let { createPanel } = await (__preload("5pb7ymrd"), import("./chunk-5pb7ymrd.js"));
88
+ let { createPanel } = await (__preload("bnhzf1tj"), import("./chunk-bnhzf1tj.js"));
89
89
  if (disposed || !controls.enabled || panel)
90
90
  return;
91
91
  if (panel = createPanel(controls, element, close, { restored, chip }), remember)
@@ -44,7 +44,7 @@ import {
44
44
  pushReadout,
45
45
  READOUT,
46
46
  storageKey
47
- } from "./chunk-3ygqgy1x.js";
47
+ } from "./chunk-mjdgzad5.js";
48
48
  import {
49
49
  resolveLook,
50
50
  themeOf,
@@ -116,7 +116,7 @@ import {
116
116
  createHintController,
117
117
  pageHints,
118
118
  toggleLayer
119
- } from "./chunk-dyq4m24m.js";
119
+ } from "./chunk-j57rhjtm.js";
120
120
 
121
121
  // src/panel/die.ts
122
122
  var ROUND = 16 * Math.sqrt(5), DIE = { half: 64 + ROUND, round: ROUND, pip: 12 }, PIPS = {
@@ -994,6 +994,9 @@ function resolveActivePresetEntry(entries, currentValues, lastAppliedId, baseId
994
994
  isCustom: !0
995
995
  };
996
996
  }
997
+ function presetsTouch(entries, keys, defaults) {
998
+ return entries.some((e) => !(e.kind === "preset" && e.rawId === "default") && keys.some((k) => !same(e.values[k], defaults[k])));
999
+ }
997
1000
  function nextPresetIndex(count, activeIndex, direction) {
998
1001
  if (count <= 0)
999
1002
  return -1;
@@ -1001,7 +1004,7 @@ function nextPresetIndex(count, activeIndex, direction) {
1001
1004
  return direction > 0 ? 0 : count - 1;
1002
1005
  return ((activeIndex + direction) % count + count) % count;
1003
1006
  }
1004
- function createPresetStrip(doc, controls, showVariants, run, onAfterChange, presetLabel = (id) => controls.presetLabel(id)) {
1007
+ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, presetLabel = (id) => controls.presetLabel(id), relevant) {
1005
1008
  let wake = () => onAfterChange?.(), el = (tag, cls, text) => {
1006
1009
  let node = doc.createElement(tag);
1007
1010
  if (cls)
@@ -1018,7 +1021,13 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
1018
1021
  saveButton.type = "button", saveButton.tabIndex = -1, saveButton.setAttribute("aria-label", "Save as variant"), saveButton.setAttribute("aria-keyshortcuts", "Shift+S"), saveButton.dataset.hint = "Save", saveButton.onclick = () => run(save);
1019
1022
  if (track.append(pill), root.append(track), saveButton)
1020
1023
  root.append(saveButton);
1021
- let pillEdges = new Edges, pillY = new Spring(0, 28, 0.86), pillAlpha = new Spring(1, 26, 0.9), entries = [], buttons = [], signature = "", lastVariantsRef = null, lastAppliedId = null, activeState = { activeId: "p:default", activeIndex: 0, isCustom: !1 }, prevActiveId = "", prevActiveIndex = -2, prevIsCustom = !1, prevDrifted = !1, initializedPill = !1, slideUntil = 0;
1024
+ let pillEdges = new Edges, pillY = new Spring(0, 28, 0.86), pillAlpha = new Spring(1, 26, 0.9), entries = [], buttons = [], signature = "", lastVariantsRef = null, lastAppliedId = null, activeState = { activeId: "p:default", activeIndex: 0, isCustom: !1 }, prevActiveId = "", prevActiveIndex = -2, prevIsCustom = !1, prevDrifted = !1, initializedPill = !1, slideUntil = 0, unpinned = (values) => {
1025
+ let out;
1026
+ for (let k of Object.keys(values))
1027
+ if (controls.isPinned(k) && !same(values[k], controls.locked[k]))
1028
+ out ??= { ...values }, out[k] = controls.locked[k];
1029
+ return out ?? values;
1030
+ };
1022
1031
  function buildEntries() {
1023
1032
  let presetEntries = Object.entries(controls.presets).map(([id, presetValues]) => ({
1024
1033
  id: `p:${id}`,
@@ -1035,7 +1044,7 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
1035
1044
  kind: "variant",
1036
1045
  rawId: v.id,
1037
1046
  label: v.name ?? v.id.slice(0, 8),
1038
- values: v.values,
1047
+ values: unpinned(v.values),
1039
1048
  apply: (coalesce) => controls.variants.load(v.id, { coalesce })
1040
1049
  }));
1041
1050
  return [...presetEntries, ...variantEntries];
@@ -1070,7 +1079,12 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
1070
1079
  if (pill.style.left = `${left}px`, pill.style.right = `${box.trackWidth - right}px`, pill.style.top = `${top}px`, pill.style.height = `${box.height}px`, pill.style.opacity = activeState.drifted ? String(DRIFTED_PILL) : "1", pill.style.scale = "", first)
1071
1080
  doc.defaultView?.requestAnimationFrame(() => pill.style.transition = "");
1072
1081
  }
1073
- let resolve = () => resolveActivePresetEntry(entries, controls.get(), lastAppliedId, activeEntryId(controls));
1082
+ let resolve = () => resolveActivePresetEntry(entries, unpinned(controls.get()), lastAppliedId, activeEntryId(controls)), wanted = (list) => (list.length > 1 || showVariants) && (relevant?.(list) ?? !0);
1083
+ function syncShown() {
1084
+ let show = wanted(entries);
1085
+ if (root.hidden === show && root.dataset.presence === void 0)
1086
+ root.hidden = !show;
1087
+ }
1074
1088
  function syncSelection(snap = !1) {
1075
1089
  if (activeState = resolve(), lastAppliedId !== null && activeState.activeId !== lastAppliedId)
1076
1090
  lastAppliedId = null;
@@ -1110,12 +1124,12 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
1110
1124
  run(() => {
1111
1125
  let variantsRef = showVariants ? controls.variants.list() : null;
1112
1126
  if (signature !== "" && variantsRef === lastVariantsRef) {
1113
- syncSelection(!1);
1127
+ syncShown(), syncSelection(!1);
1114
1128
  return;
1115
1129
  }
1116
1130
  lastVariantsRef = variantsRef;
1117
- let nextEntries = buildEntries(), nextSig = JSON.stringify(nextEntries.map((e) => [e.id, e.label])), shouldShow = nextEntries.length > 1 || showVariants;
1118
- if (root.hidden = !shouldShow, nextSig !== signature)
1131
+ let nextEntries = buildEntries(), nextSig = JSON.stringify(nextEntries.map((e) => [e.id, e.label]));
1132
+ if (entries = nextEntries, syncShown(), nextSig !== signature)
1119
1133
  signature = nextSig, entries = nextEntries, buttons = entries.map((entry, idx) => {
1120
1134
  let b = el("button", "knobs-preset-chip", entry.label);
1121
1135
  return b.type = "button", b.tabIndex = -1, b.dataset.presetId = entry.id, b.dataset.presetKind = entry.kind, b.dataset.presetName = entry.label, b.setAttribute("role", "radio"), b.setAttribute("aria-label", entry.label), b.onclick = (e) => {
@@ -1286,6 +1300,7 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
1286
1300
  getEntries: () => entries,
1287
1301
  getButtons: () => buttons,
1288
1302
  getActiveState: () => activeState,
1303
+ shows: () => wanted(entries),
1289
1304
  cycle,
1290
1305
  selectById,
1291
1306
  save: () => {
@@ -3762,7 +3777,9 @@ class GpuOverlayManager {
3762
3777
  let ro = new ResizeObserver(() => {
3763
3778
  this.wake();
3764
3779
  });
3765
- ro.observe(panel.root), ro.observe(panel.body), ro.observe(panel.presetStrip.track);
3780
+ if (ro.observe(panel.root), ro.observe(panel.body), panel.pinned)
3781
+ ro.observe(panel.pinned);
3782
+ ro.observe(panel.presetStrip.track);
3766
3783
  let mo = new MutationObserver(() => {
3767
3784
  this.styleVersion++, this.wake();
3768
3785
  });
@@ -5838,8 +5855,9 @@ class GpuOverlayManager {
5838
5855
  ], this.maskCount * MASK_FLOATS), this.curMask = ++this.maskCount;
5839
5856
  }
5840
5857
  }
5841
- let ps = panel.presetStrip, hinting = panel.root.classList.contains("knobs-hinting");
5858
+ let ps = panel.presetStrip, hinting = panel.root.classList.contains("knobs-hinting"), stripAlpha = this.curAlpha;
5842
5859
  if (!ps.element.hidden) {
5860
+ this.curAlpha = stripAlpha * this.presence(ps.element);
5843
5861
  let psHovered = this.curPanelHovered && ps.element.matches(":hover");
5844
5862
  this.curSubHovered = psHovered;
5845
5863
  let psRect = this.rectOf(ps.element), trackRect = this.rectOf(ps.track);
@@ -5941,6 +5959,7 @@ class GpuOverlayManager {
5941
5959
  align: "center"
5942
5960
  });
5943
5961
  }
5962
+ this.curAlpha = stripAlpha;
5944
5963
  }
5945
5964
  let tabs = panel.tabs;
5946
5965
  if (tabs && !tabs.element.hidden)
@@ -6042,7 +6061,7 @@ class GpuOverlayManager {
6042
6061
  }
6043
6062
  if (bodyRevealing || pageMoving)
6044
6063
  this.curBypassRectCache = !0;
6045
- let bodyHovered = this.curPanelHovered && panel.body.matches(":hover");
6064
+ let bodyHovered = this.curPanelHovered && (panel.body.matches(":hover") || Boolean(panel.pinned?.matches(":hover")));
6046
6065
  this.curSubHovered = bodyHovered, this.curPointerRow = bodyHovered && panel.root.hasAttribute("data-k-row-hover");
6047
6066
  let panelAlpha = this.curAlpha;
6048
6067
  for (let g of panel.groups) {
@@ -14856,7 +14875,7 @@ function buildPanel(controls, host, options) {
14856
14875
  if (helpButton?.hidden)
14857
14876
  titleEl.title = `${titleEl.textContent ? `${titleEl.textContent}
14858
14877
  ` : ""}Press ${show("help")} for keyboard shortcuts`;
14859
- let presetLabel = (id) => options.presetLabels?.[id] ?? (id === "default" ? options.defaultLabel : void 0) ?? controls.presetLabel(id), presetStrip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel), tabStrip = createTabStrip(doc, (id, animate) => selectTab(id, animate)), activeTab, searchOptions = typeof options.search === "object" ? options.search : {}, chipTests = /* @__PURE__ */ new Map, search = options.search && !page ? createSearch(doc, {
14878
+ let presetLabel = (id) => options.presetLabels?.[id] ?? (id === "default" ? options.defaultLabel : void 0) ?? controls.presetLabel(id), presetStrip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel, (entries) => presetsTouchTab(entries)), tabStrip = createTabStrip(doc, (id, animate) => selectTab(id, animate)), activeTab, searchOptions = typeof options.search === "object" ? options.search : {}, chipTests = /* @__PURE__ */ new Map, search = options.search && !page ? createSearch(doc, {
14860
14879
  placeholder: searchOptions.placeholder ?? "Search",
14861
14880
  chips: (searchOptions.chips ?? ["modified"]).map((chip) => {
14862
14881
  if (chip === "modified")
@@ -14878,7 +14897,10 @@ function buildPanel(controls, host, options) {
14878
14897
  let allActions = [
14879
14898
  ...controls.actions ?? [],
14880
14899
  ...options.actions ?? []
14881
- ], body = el("div"), tabMotion = tabTransition(body, root, () => wakeGpu()), blocks = [], cards = [], groupResetButtons = /* @__PURE__ */ new Map, groupDice = /* @__PURE__ */ new Map, gpuGroups = [], gpuRows = [], layout, layoutTick = 0, foldSource = null, userFolding = !1, noteFold = (source) => {
14900
+ ], body = el("div"), tabMotion = tabTransition(body, root, () => wakeGpu()), pinnedBody = el("div");
14901
+ if (pinnedBody.className = "knobs-pinned", !page)
14902
+ body.append(presetStrip.element);
14903
+ let blocks = [], cards = [], groupResetButtons = /* @__PURE__ */ new Map, groupDice = /* @__PURE__ */ new Map, gpuGroups = [], gpuRows = [], layout, layoutTick = 0, foldSource = null, userFolding = !1, noteFold = (source) => {
14882
14904
  if (foldSource !== "user")
14883
14905
  foldSource = source;
14884
14906
  }, byUser = (act) => {
@@ -15195,8 +15217,11 @@ function buildPanel(controls, host, options) {
15195
15217
  section: field.section
15196
15218
  });
15197
15219
  }
15198
- return gatherSubsections(specs);
15199
- }, gatherSubsections = (specs) => {
15220
+ let planned = gatherSubsections(specs);
15221
+ if (page || !planned.some(pinnedRow))
15222
+ return planned;
15223
+ return [...planned.filter(pinnedRow), ...planned.filter((spec) => !pinnedRow(spec))];
15224
+ }, pinnedRow = (r) => !page && controls.isPinned(r.keys[0] ?? r.id), gatherSubsections = (specs) => {
15200
15225
  let out = [];
15201
15226
  for (let i = 0;i < specs.length; ) {
15202
15227
  let end = i;
@@ -15720,13 +15745,13 @@ function buildPanel(controls, host, options) {
15720
15745
  } else {
15721
15746
  let runs = [];
15722
15747
  for (let r of next) {
15723
- let last = runs.at(-1);
15724
- if (last && last.group === r.group)
15748
+ let last = runs.at(-1), pinned = pinnedRow(r);
15749
+ if (last && last.group === r.group && last.pinned === pinned)
15725
15750
  last.rows.push(r);
15726
15751
  else
15727
- runs.push({ group: r.group, rows: [r] });
15752
+ runs.push({ group: r.group, pinned, rows: [r] });
15728
15753
  }
15729
- let free = blocks.filter((b) => !b.leaving), chosen = [], usedSubheads = /* @__PURE__ */ new Set;
15754
+ let free = blocks.filter((b) => !b.leaving), chosen = [], pinnedBlocks = /* @__PURE__ */ new Set, usedSubheads = /* @__PURE__ */ new Set;
15730
15755
  for (let run of runs) {
15731
15756
  let at = free.findIndex((b) => b.group === run.group), block = at >= 0 ? free.splice(at, 1)[0] : newBlock(run.group);
15732
15757
  if (at < 0) {
@@ -15741,7 +15766,8 @@ function buildPanel(controls, host, options) {
15741
15766
  motion.push({ node: block.card, show: !0 });
15742
15767
  }
15743
15768
  }
15744
- chosen.push(block);
15769
+ if (chosen.push(block), run.pinned)
15770
+ pinnedBlocks.add(block);
15745
15771
  let nodes = [], under;
15746
15772
  for (let r of run.rows) {
15747
15773
  let name = r.field.subsection;
@@ -15766,7 +15792,8 @@ function buildPanel(controls, host, options) {
15766
15792
  gpuRows.splice(gi, 1);
15767
15793
  subheads.delete(id);
15768
15794
  }
15769
- arrange(body, chosen.flatMap((b) => b.head ? [b.head, b.card] : [b.card]));
15795
+ let nodesOf = (list) => list.flatMap((b) => b.head ? [b.head, b.card] : [b.card]);
15796
+ arrange(pinnedBody, nodesOf(chosen.filter((b) => pinnedBlocks.has(b)))), arrange(body, [presetStrip.element, ...nodesOf(chosen.filter((b) => !pinnedBlocks.has(b)))]);
15770
15797
  for (let block of free) {
15771
15798
  block.leaving = !0;
15772
15799
  let fold = () => {
@@ -15872,15 +15899,24 @@ function buildPanel(controls, host, options) {
15872
15899
  if (f.group === group && f.tab !== void 0)
15873
15900
  return f.tab;
15874
15901
  return;
15875
- }, tabOf = (r) => r.field.tab ?? (r.group !== void 0 ? groupTab(r.group) : void 0), tabList = () => {
15902
+ }, tabOf = (r) => pinnedRow(r) ? void 0 : r.field.tab ?? (r.group !== void 0 ? groupTab(r.group) : void 0), tabList = () => {
15876
15903
  if (page)
15877
15904
  return [];
15878
15905
  let list = (options.tabs ?? []).map((t) => typeof t === "string" ? { id: t, label: readableTitle(t) } : { id: t.id, label: t.label ?? readableTitle(t.id) }), seen = new Set(list.map((t) => t.id));
15879
- for (let f of Object.values(controls.schema))
15880
- if (f.tab !== void 0 && !seen.has(f.tab))
15906
+ for (let [key, f] of Object.entries(controls.schema))
15907
+ if (f.tab !== void 0 && !seen.has(f.tab) && !controls.isPinned(key))
15881
15908
  seen.add(f.tab), list.push({ id: f.tab, label: readableTitle(f.tab) });
15882
15909
  return list;
15883
15910
  };
15911
+ function presetsTouchTab(entries) {
15912
+ if (page || activeTab === void 0)
15913
+ return !0;
15914
+ let keys = rows.flatMap((r) => {
15915
+ let tab = tabOf(r);
15916
+ return !pinnedRow(r) && (tab === void 0 || tab === activeTab) ? r.keys : [];
15917
+ });
15918
+ return presetsTouch(entries, keys, controls.locked);
15919
+ }
15884
15920
  function syncTabs() {
15885
15921
  let list = tabList(), has = (id) => id !== void 0 && list.some((t) => t.id === id);
15886
15922
  if (!has(activeTab))
@@ -15934,7 +15970,7 @@ function buildPanel(controls, host, options) {
15934
15970
  want.set(b.head, any);
15935
15971
  want.set(b.card, any && !(b.group !== void 0 && folds.get(b.group) && !finding));
15936
15972
  }
15937
- return want;
15973
+ return want.set(presetStrip.element, presetStrip.shows()), want;
15938
15974
  }
15939
15975
  let shift = (node, show, delay) => {
15940
15976
  shifting.add(node), node.dataset.presence = show ? "in" : "out", reveal(node, show, () => {
@@ -15951,6 +15987,9 @@ function buildPanel(controls, host, options) {
15951
15987
  }
15952
15988
  if (tabMotion.running)
15953
15989
  tabMotion.finish();
15990
+ let strip = presetStrip.element;
15991
+ if (strip.hidden === want.get(strip))
15992
+ strip.hidden = !want.get(strip);
15954
15993
  let showing = (n) => !n.hidden && n.dataset.leaving !== "true", set = (node, show, animate) => {
15955
15994
  if (showing(node) === show)
15956
15995
  return;
@@ -16155,9 +16194,9 @@ function buildPanel(controls, host, options) {
16155
16194
  } else {
16156
16195
  if (root.append(header), showFooter)
16157
16196
  root.append(popover);
16158
- if (root.append(tabStrip.element), search)
16197
+ if (root.append(pinnedBody, tabStrip.element), search)
16159
16198
  root.append(search.element);
16160
- if (root.append(presetStrip.element, body), panelActions.length > 0)
16199
+ if (root.append(body), panelActions.length > 0)
16161
16200
  root.append(customActions);
16162
16201
  root.append(text, apply, status, live);
16163
16202
  }
@@ -16389,35 +16428,44 @@ function buildPanel(controls, host, options) {
16389
16428
  nodes.push(...r.editor?.stops() ?? [r.input]);
16390
16429
  return nodes.push(...body.querySelectorAll("[data-knobs-stop],.knobs-tile>[data-foldable],.knobs-section-head[data-foldable]")), nodes.filter(visible).sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
16391
16430
  }, actionBarButtons = () => [...customActions.querySelectorAll("button")].filter((b) => visible(b) && !b.disabled), lastActionButton, stops = () => {
16392
- let list = [];
16431
+ let list = [], strips = [];
16393
16432
  if (visible(tabStrip.element))
16394
- list.push({
16395
- contains: (n) => tabStrip.element.contains(n),
16396
- focus: (o) => tabStrip.focus(o)
16433
+ strips.push({
16434
+ at: tabStrip.element,
16435
+ stops: [{ contains: (n) => tabStrip.element.contains(n), focus: (o) => tabStrip.focus(o) }]
16397
16436
  });
16398
16437
  if (visible(presetStrip.element))
16399
- list.push({
16400
- contains: (n) => presetStrip.element.contains(n),
16401
- focus: (o) => presetStrip.element.focus(o)
16438
+ strips.push({
16439
+ at: presetStrip.element,
16440
+ stops: [
16441
+ {
16442
+ contains: (n) => presetStrip.element.contains(n),
16443
+ focus: (o) => presetStrip.element.focus(o)
16444
+ }
16445
+ ]
16402
16446
  });
16403
16447
  if (page) {
16448
+ for (let strip of strips)
16449
+ list.push(...strip.stops);
16404
16450
  for (let stop of pageStops())
16405
16451
  list.push({ contains: (n) => stop.contains(n), focus: (o) => stop.focus(o) });
16406
16452
  return list;
16407
16453
  }
16408
- let runs = rows.map((r) => ({
16454
+ let stopOf = (stop) => ({
16455
+ contains: (n) => stop.contains(n),
16456
+ focus: (o) => stop.focus(o)
16457
+ }), shown = (stops) => stops.filter((s) => !s.closest('[hidden],[data-presence="out"]')).map(stopOf), runs = rows.map((r) => ({
16409
16458
  at: r.row,
16410
- stops: r.editor?.stops() ?? [r.input]
16459
+ stops: shown(r.editor?.stops() ?? [r.input])
16411
16460
  }));
16412
16461
  for (let b of blocks)
16413
16462
  if (b.head?.hasAttribute("data-foldable") && !b.leaving)
16414
- runs.push({ at: b.head, stops: [b.head] });
16463
+ runs.push({ at: b.head, stops: shown([b.head]) });
16415
16464
  for (let sh of subheads.values())
16416
- runs.push({ at: sh.el, stops: [sh.el] });
16417
- runs.sort((a, b) => a.at.compareDocumentPosition(b.at) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
16465
+ runs.push({ at: sh.el, stops: shown([sh.el]) });
16466
+ runs.push(...strips), runs.sort((a, b) => a.at.compareDocumentPosition(b.at) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
16418
16467
  for (let run of runs)
16419
- for (let stop of run.stops.filter((s) => !s.closest('[hidden],[data-presence="out"]')))
16420
- list.push({ contains: (n) => stop.contains(n), focus: (o) => stop.focus(o) });
16468
+ list.push(...run.stops);
16421
16469
  let bar = actionBarButtons();
16422
16470
  if (bar.length)
16423
16471
  list.push({
@@ -17523,6 +17571,7 @@ function buildPanel(controls, host, options) {
17523
17571
  id: controls.id,
17524
17572
  root,
17525
17573
  body,
17574
+ pinned: pinnedBody,
17526
17575
  inline,
17527
17576
  header,
17528
17577
  titleEl,
@@ -10,7 +10,7 @@ import {
10
10
  DEFAULT_LINEAR,
11
11
  motionCss,
12
12
  storageKey
13
- } from "./chunk-3ygqgy1x.js";
13
+ } from "./chunk-mjdgzad5.js";
14
14
 
15
15
  // src/core/theme.ts
16
16
  var property = (k) => k.startsWith("--") ? k : `--k-${k}`;
@@ -2586,6 +2586,9 @@ ${GRAD_PROPS}
2586
2586
  .knobs-group-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:20px;margin:var(--k-group-gap) 0 var(--_k-gap);padding:0 var(--k-group-inset);color:var(--_k-group-label);font-size:var(--k-group-size);font-weight:var(--_k-weight-title);letter-spacing:var(--k-group-tracking);text-transform:var(--k-group-transform)}
2587
2587
  .knobs-panel:not(.knobs-page) .knobs-group-head:not(:first-child){border-top:1px solid var(--k-group-divider);padding-top:var(--k-group-divider-gap)}
2588
2588
  .knobs-group-head:first-child{margin-top:0}
2589
+ .knobs-panel:not(.knobs-page) .knobs-presets+.knobs-group-head{margin-top:0;border-top:0;padding-top:0}
2590
+ .knobs-pinned{margin-bottom:var(--k-group-gap)}
2591
+ .knobs-pinned:empty{display:none}
2589
2592
  .knobs-group-head>span:last-child{display:flex;align-items:center;gap:var(--_k-tray-pad);margin-right:calc((var(--k-icon-sm) - 20px) / 2)}
2590
2593
  .knobs-group-head button{height:20px;padding:0 6px;font-size:var(--_k-text-meta);font-weight:500;letter-spacing:0;text-transform:none;color:var(--_k-fg-2);background:transparent;border-radius:var(--_k-radius-sm);cursor:pointer}
2591
2594
  .knobs-group-head button:is(:hover:not([disabled]),:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg)}
@@ -516,6 +516,7 @@ var META_KEYS = [
516
516
  "folded",
517
517
  "sectionFolded",
518
518
  "tab",
519
+ "pinned",
519
520
  "subsection"
520
521
  ];
521
522
  function pickMeta(source, skip = []) {
@@ -527,7 +528,19 @@ function pickMeta(source, skip = []) {
527
528
  }
528
529
  return out;
529
530
  }
530
- var READOUT_KINDS = /* @__PURE__ */ new Set(["watch", "sparkline", "graph", "meter", "table", "log"]), isReadout2 = (field) => READOUT_KINDS.has(field.kind), hasValue2 = (field) => field.kind !== "action" && field.kind !== "toolbar" && !READOUT_KINDS.has(field.kind), SPRING2 = {
531
+ var READOUT_KINDS = /* @__PURE__ */ new Set(["watch", "sparkline", "graph", "meter", "table", "log"]), isReadout2 = (field) => READOUT_KINDS.has(field.kind), hasValue2 = (field) => field.kind !== "action" && field.kind !== "toolbar" && !READOUT_KINDS.has(field.kind);
532
+ function pinnedKeys(schema, groups = []) {
533
+ let byGroup = new Map(groups.map((g) => [g, !0]));
534
+ for (let f of Object.values(schema))
535
+ if (f.group !== void 0 && f.pinned !== void 0 && !byGroup.has(f.group))
536
+ byGroup.set(f.group, f.pinned);
537
+ let out = /* @__PURE__ */ new Set;
538
+ for (let [key, f] of Object.entries(schema))
539
+ if (f.group === void 0 ? f.pinned === !0 : byGroup.get(f.group) === !0)
540
+ out.add(key);
541
+ return out;
542
+ }
543
+ var SPRING2 = {
531
544
  duration: { min: 0.05, max: 2, step: 0.01 },
532
545
  bounce: { min: -0.5, max: 1, step: 0.01 }
533
546
  };
@@ -1160,4 +1173,4 @@ function urlFlag2(name) {
1160
1173
  }
1161
1174
  var storageKey = (id, part) => `knobs:${encodeURIComponent(id)}:${part}:v1`;
1162
1175
 
1163
- export { __preload, __chunks, maxChroma, oklchToRgb, parseColor2, fitColor, formatColor, colorHex2, EASINGS2, EASING_Y, easingName, bezierPoint2, bezierCss2, normalizeCombo2, letterOf2, comboFromEvent2, heldMods, matchesCombo2, comboParts2, comboLabel2, isTypingTarget, GRADIENT_SAMPLES, sampleGradient2, gradientSamples, gradientCss2, stopColorAt, sampleCurve2, curveSamples, CURVE_SHAPES2, inRange, sameRamp, randomStops, mutateStops, randomPoints, mutatePoints, pickMeta, isReadout2, hasValue2, SPRING2, valueKeys, knob2, choice2, action2, flatSpring2, toggle2, spring2, color2, point2, easing2, vec22, vec32, range2, angle2, seed2, keybind2, gradient2, curve2, toolbarShortcut2, actionShortcut2, toolbar2, watch2, sparkline2, graph2, meter2, table2, log2, wrapAngle2, normalizeField, normalize, isRecord, same, equal, Spring, springMotion2, springResponse, springEasing, springCss2, SPRINGS2, FADE, MOTION, reducedMotion, reducedSpring2, springLinear, DEFAULT_LINEAR, motionCss, RESHAPE, RESET_KEYS, REFRESHES, readoutState, pushReadout, READOUT, memoryStorage2, browserStorage, urlFlag2, storageKey };
1176
+ export { __preload, __chunks, maxChroma, oklchToRgb, parseColor2, fitColor, formatColor, colorHex2, EASINGS2, EASING_Y, easingName, bezierPoint2, bezierCss2, normalizeCombo2, letterOf2, comboFromEvent2, heldMods, matchesCombo2, comboParts2, comboLabel2, isTypingTarget, GRADIENT_SAMPLES, sampleGradient2, gradientSamples, gradientCss2, stopColorAt, sampleCurve2, curveSamples, CURVE_SHAPES2, inRange, sameRamp, randomStops, mutateStops, randomPoints, mutatePoints, pickMeta, isReadout2, hasValue2, pinnedKeys, SPRING2, valueKeys, knob2, choice2, action2, flatSpring2, toggle2, spring2, color2, point2, easing2, vec22, vec32, range2, angle2, seed2, keybind2, gradient2, curve2, toolbarShortcut2, actionShortcut2, toolbar2, watch2, sparkline2, graph2, meter2, table2, log2, wrapAngle2, normalizeField, normalize, isRecord, same, equal, Spring, springMotion2, springResponse, springEasing, springCss2, SPRINGS2, FADE, MOTION, reducedMotion, reducedSpring2, springLinear, DEFAULT_LINEAR, motionCss, RESHAPE, RESET_KEYS, REFRESHES, readoutState, pushReadout, READOUT, memoryStorage2, browserStorage, urlFlag2, storageKey };
package/dist/dev/index.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  pickMeta,
28
28
  isReadout2,
29
29
  hasValue2,
30
+ pinnedKeys,
30
31
  SPRING2,
31
32
  valueKeys,
32
33
  knob2,
@@ -75,8 +76,8 @@ import {
75
76
  browserStorage,
76
77
  urlFlag2,
77
78
  storageKey
78
- } from "./chunk-3ygqgy1x.js";
79
- __chunks(import.meta.url,["kdk2476q","5pb7ymrd","a7cjgy8x","3ygqgy1x","dyq4m24m"],[["./index.js",3],["./chunk-5pb7ymrd.js",3,4],["./chunk-a7cjgy8x.js",3,4],["./chunk-3ygqgy1x.js"],["./chunk-dyq4m24m.js",3]],0);
79
+ } from "./chunk-mjdgzad5.js";
80
+ __chunks(import.meta.url,["js6xfzn6","bnhzf1tj","75gbzbyd","mjdgzad5","j57rhjtm"],[["./index.js",3],["./chunk-bnhzf1tj.js",3,4],["./chunk-75gbzbyd.js",3,4],["./chunk-mjdgzad5.js"],["./chunk-j57rhjtm.js",3]],0);
80
81
  // src/core/random.ts
81
82
  var lattice = (min, max, step, r) => min + Math.floor(r * (Math.floor((max - min) / step + 0.000000001) + 1)) * step, between = (min, max, r) => min + (max - min) * r;
82
83
  function pick(field, random) {
@@ -594,12 +595,30 @@ function defineControls(id, input, options = {}) {
594
595
  let schema = Object.freeze({ ...input }), locked = normalize(schema, {}), actions = Object.freeze([...options.actions ?? []]), { theme, appearance } = options, density = options.density ?? "default", renderer = options.renderer ?? "auto", enabled = () => typeof options.enabled === "function" ? options.enabled() : options.enabled ?? (Boolean(options.ship) || isUnlocked() || typeof location < "u" && ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname)), storage = () => options.storage === void 0 ? browserStorage() : options.storage, persistMode = options.persist ?? "all", valueStorage = () => persistMode === "all" ? storage() : null, variantStorage = () => persistMode === "none" ? null : storage(), draw = options.random ?? Math.random, hostHistory = options.history, unsubscribeHistory, unsubscribeUnlock, listeners = /* @__PURE__ */ new Set, refreshes = 0, notify = () => {
595
596
  for (let cb of [...listeners])
596
597
  cb();
597
- }, mixed = /* @__PURE__ */ new Set, current = locked, read = !1, error = null, locks = /* @__PURE__ */ new Set, bindings = /* @__PURE__ */ new Map, comparing = null, landing = !1, echo = Math.max(0, options.echo ?? 0), grabbed = /* @__PURE__ */ new Set, changedAt = /* @__PURE__ */ new Map, sent = /* @__PURE__ */ new Map, pending = /* @__PURE__ */ new Map, echoTimer, now = () => typeof performance < "u" ? performance.now() : Date.now(), isHeld = (key) => grabbed.has(key) || echo > 0 && now() - (changedAt.get(key) ?? -1 / 0) < echo, NO_BASE = Object.freeze({ preset: null, groups: Object.freeze({}) }), base = NO_BASE, past = [], future = [], HISTORY = 100, stateKey = storageKey(id, "state"), buildPresets = () => Object.freeze(Object.fromEntries([
598
+ }, mixed = /* @__PURE__ */ new Set, current = locked, read = !1, error = null, locks = /* @__PURE__ */ new Set, bindings = /* @__PURE__ */ new Map, comparing = null, landing = !1, echo = Math.max(0, options.echo ?? 0), grabbed = /* @__PURE__ */ new Set, changedAt = /* @__PURE__ */ new Map, sent = /* @__PURE__ */ new Map, pending = /* @__PURE__ */ new Map, echoTimer, now = () => typeof performance < "u" ? performance.now() : Date.now(), isHeld = (key) => grabbed.has(key) || echo > 0 && now() - (changedAt.get(key) ?? -1 / 0) < echo, NO_BASE = Object.freeze({ preset: null, groups: Object.freeze({}) }), base = NO_BASE, past = [], future = [], HISTORY = 100, stateKey = storageKey(id, "state"), pinnedOf, pinned = () => {
599
+ if (pinnedOf?.schema !== schema)
600
+ pinnedOf = { schema, keys: pinnedKeys(schema, options.pinned) };
601
+ return pinnedOf.keys;
602
+ }, unpinned = (values) => {
603
+ let keys = pinned();
604
+ if (keys.size === 0)
605
+ return values;
606
+ let out = { ...values };
607
+ for (let k of keys)
608
+ if (Object.hasOwn(locked, k))
609
+ out[k] = locked[k];
610
+ return Object.freeze(out);
611
+ }, presetPatch = (values) => {
612
+ let keys = pinned();
613
+ if (keys.size === 0)
614
+ return values;
615
+ return Object.fromEntries(Object.entries(values).filter(([k]) => !keys.has(k)));
616
+ }, buildPresets = () => Object.freeze(Object.fromEntries([
598
617
  ["default", locked],
599
618
  ...Object.entries(options.presets ?? {}).map(([id, patch]) => {
600
619
  if (id === "default" || id === "custom")
601
620
  throw Error(`Reserved preset id: ${id}`);
602
- return [id, normalize(schema, { ...locked, ...patch })];
621
+ return [id, unpinned(normalize(schema, { ...locked, ...patch }))];
603
622
  })
604
623
  ])), presets = buildPresets(), buildGroupPresets = () => Object.freeze(Object.fromEntries(Object.entries(options.groupPresets ?? {}).map(([group, list]) => {
605
624
  let named = list.flatMap((p) => Object.keys(p.values).filter((k) => Object.hasOwn(schema, k))), covered = new Set((named.length ? named : Object.keys(schema).filter((k) => schema[k].group === group)).filter((k) => hasValue2(schema[k])));
@@ -700,7 +719,7 @@ function defineControls(id, input, options = {}) {
700
719
  else if (ref?.kind === "variant") {
701
720
  let found = list?.find((v) => v.id === ref.id);
702
721
  if (found)
703
- out = normalize(schema, found.values);
722
+ out = unpinned(normalize(schema, found.values));
704
723
  }
705
724
  let groups = Object.entries(b.groups);
706
725
  if (groups.length) {
@@ -712,11 +731,10 @@ function defineControls(id, input, options = {}) {
712
731
  return baseCache = { base: b, schema, presets, groupPresets, variants: list, values }, values;
713
732
  }
714
733
  let covered = (key) => {
715
- let b = activeBase();
716
- if (b.preset)
734
+ let b = activeBase(), group = schema[key]?.group;
735
+ if (group !== void 0 && Object.hasOwn(b.groups, group))
717
736
  return !0;
718
- let group = schema[key]?.group;
719
- return group !== void 0 && Object.hasOwn(b.groups, group);
737
+ return b.preset !== null && !pinned().has(key);
720
738
  };
721
739
  function declaredShortcut(id) {
722
740
  let slash = id.indexOf("/"), key = slash < 0 ? id : id.slice(0, slash), field = Object.hasOwn(schema, key) ? schema[key] : void 0;
@@ -986,7 +1004,7 @@ function defineControls(id, input, options = {}) {
986
1004
  listen(cb) {
987
1005
  return readoutListeners.add(cb), () => readoutListeners.delete(cb);
988
1006
  }
989
- }, variants = createVariants(() => schema, id, storageKey(id, "variants"), variantStorage, get, (v, variant, coalesce) => write(v, { source: "variant", coalesce }, makeBase(Object.freeze({ kind: "variant", id: variant }), {})), notify, enabled), resettable = (keys) => (keys ?? fieldKeys()).filter((k) => valueField(k) && !blocked(k));
1007
+ }, variants = createVariants(() => schema, id, storageKey(id, "variants"), variantStorage, () => unpinned(get()), (v, variant, coalesce) => write(presetPatch(v), { source: "variant", coalesce }, makeBase(Object.freeze({ kind: "variant", id: variant }), {})), notify, enabled), resettable = (keys) => (keys ?? fieldKeys()).filter((k) => valueField(k) && !blocked(k));
990
1008
  function offBase(keys) {
991
1009
  if (!hasBase())
992
1010
  return !1;
@@ -1071,6 +1089,7 @@ function defineControls(id, input, options = {}) {
1071
1089
  groupFolded: options.groupFolded,
1072
1090
  sectionFolded: options.sectionFolded,
1073
1091
  presetLabel: (presetId) => options.presetLabels?.[presetId] ?? (presetId === "default" ? options.defaultLabel : void 0) ?? presetId,
1092
+ isPinned: (key) => pinned().has(key),
1074
1093
  get,
1075
1094
  set,
1076
1095
  sync,
@@ -1345,9 +1364,12 @@ function defineControls(id, input, options = {}) {
1345
1364
  preset(id, presetOptions) {
1346
1365
  if (!Object.hasOwn(presets, id))
1347
1366
  throw Error(`Unknown preset: ${id}`);
1348
- write(presets[id], { source: "preset", coalesce: presetOptions?.coalesce }, id === "default" ? NO_BASE : makeBase(Object.freeze({ kind: "preset", id }), {}));
1367
+ write(presetPatch(presets[id]), { source: "preset", coalesce: presetOptions?.coalesce }, id === "default" ? NO_BASE : makeBase(Object.freeze({ kind: "preset", id }), {}));
1368
+ },
1369
+ matchingPreset: () => {
1370
+ let cur = unpinned(get());
1371
+ return Object.keys(presets).find((id) => equal(cur, presets[id])) ?? "custom";
1349
1372
  },
1350
- matchingPreset: () => Object.keys(presets).find((id) => equal(get(), presets[id])) ?? "custom",
1351
1373
  get groupPresets() {
1352
1374
  return groupPresets;
1353
1375
  },
@@ -1378,11 +1400,11 @@ function defineControls(id, input, options = {}) {
1378
1400
  code: (format, values) => formatCode(values ?? get(), format),
1379
1401
  variants,
1380
1402
  attach(element, chip) {
1381
- let load = options.ship ? () => options.ship().then((m) => m.attach) : () => (__preload("a7cjgy8x"), import("./chunk-a7cjgy8x.js")).then((m) => m.attach);
1403
+ let load = options.ship ? () => options.ship().then((m) => m.attach) : () => (__preload("75gbzbyd"), import("./chunk-75gbzbyd.js")).then((m) => m.attach);
1382
1404
  return live(load, (attach) => attach(controls, element, chip), (detach) => detach());
1383
1405
  },
1384
1406
  mount(container, mountOptions) {
1385
- let load = options.ship ? () => options.ship().then((m) => m.mountPanel) : () => (__preload("5pb7ymrd"), import("./chunk-5pb7ymrd.js")).then((m) => m.mountPanel);
1407
+ let load = options.ship ? () => options.ship().then((m) => m.mountPanel) : () => (__preload("bnhzf1tj"), import("./chunk-bnhzf1tj.js")).then((m) => m.mountPanel);
1386
1408
  return live(load, (mountPanel) => {
1387
1409
  let panel = mountPanel(controls, container, mountOptions);
1388
1410
  return mountOptions?.onReady?.(panel), panel;
@@ -1674,6 +1696,7 @@ function createRemoteControls(id, descriptors, options = {}) {
1674
1696
  tunable: options.tunable,
1675
1697
  history: options.history,
1676
1698
  presets: options.presets,
1699
+ pinned: options.pinned,
1677
1700
  groupPresets: options.groupPresets,
1678
1701
  onPreset: options.onPreset,
1679
1702
  actions: options.actions,
package/dist/index.js CHANGED
@@ -460,6 +460,7 @@ var META_KEYS = [
460
460
  "folded",
461
461
  "sectionFolded",
462
462
  "tab",
463
+ "pinned",
463
464
  "subsection"
464
465
  ];
465
466
  function pickMeta(source, skip = []) {
@@ -471,7 +472,19 @@ function pickMeta(source, skip = []) {
471
472
  }
472
473
  return out;
473
474
  }
474
- var READOUT_KINDS = /* @__PURE__ */ new Set(["watch", "sparkline", "graph", "meter", "table", "log"]), isReadout = (field) => READOUT_KINDS.has(field.kind), hasValue = (field) => field.kind !== "action" && field.kind !== "toolbar" && !READOUT_KINDS.has(field.kind), SPRING = {
475
+ var READOUT_KINDS = /* @__PURE__ */ new Set(["watch", "sparkline", "graph", "meter", "table", "log"]), isReadout = (field) => READOUT_KINDS.has(field.kind), hasValue = (field) => field.kind !== "action" && field.kind !== "toolbar" && !READOUT_KINDS.has(field.kind);
476
+ function pinnedKeys(schema, groups = []) {
477
+ let byGroup = new Map(groups.map((g) => [g, !0]));
478
+ for (let f of Object.values(schema))
479
+ if (f.group !== void 0 && f.pinned !== void 0 && !byGroup.has(f.group))
480
+ byGroup.set(f.group, f.pinned);
481
+ let out = /* @__PURE__ */ new Set;
482
+ for (let [key, f] of Object.entries(schema))
483
+ if (f.group === void 0 ? f.pinned === !0 : byGroup.get(f.group) === !0)
484
+ out.add(key);
485
+ return out;
486
+ }
487
+ var SPRING = {
475
488
  duration: { min: 0.05, max: 2, step: 0.01 },
476
489
  bounce: { min: -0.5, max: 1, step: 0.01 }
477
490
  };
@@ -1612,12 +1625,30 @@ function defineControls(id, input, options = {}) {
1612
1625
  let schema = Object.freeze({ ...input }), locked = normalize(schema, {}), actions = Object.freeze([...options.actions ?? []]), { theme, appearance } = options, density = options.density ?? "default", renderer = options.renderer ?? "auto", enabled = () => (Boolean(options.ship) || isUnlocked()) && (typeof options.enabled === "function" ? options.enabled() : options.enabled ?? (Boolean(options.ship) || isUnlocked() || typeof location < "u" && ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname))), storage = () => options.storage === void 0 ? browserStorage() : options.storage, persistMode = options.persist ?? "all", valueStorage = () => persistMode === "all" ? storage() : null, variantStorage = () => persistMode === "none" ? null : storage(), draw = options.random ?? Math.random, hostHistory = options.history, unsubscribeHistory, unsubscribeUnlock, listeners = /* @__PURE__ */ new Set, refreshes = 0, notify = () => {
1613
1626
  for (let cb of [...listeners])
1614
1627
  cb();
1615
- }, mixed = /* @__PURE__ */ new Set, current = locked, read = !1, error = null, locks = /* @__PURE__ */ new Set, bindings = /* @__PURE__ */ new Map, comparing = null, landing = !1, echo = Math.max(0, options.echo ?? 0), grabbed = /* @__PURE__ */ new Set, changedAt = /* @__PURE__ */ new Map, sent = /* @__PURE__ */ new Map, pending = /* @__PURE__ */ new Map, echoTimer, now = () => typeof performance < "u" ? performance.now() : Date.now(), isHeld = (key) => grabbed.has(key) || echo > 0 && now() - (changedAt.get(key) ?? -1 / 0) < echo, NO_BASE = Object.freeze({ preset: null, groups: Object.freeze({}) }), base = NO_BASE, past = [], future = [], HISTORY = 100, stateKey = storageKey(id, "state"), buildPresets = () => Object.freeze(Object.fromEntries([
1628
+ }, mixed = /* @__PURE__ */ new Set, current = locked, read = !1, error = null, locks = /* @__PURE__ */ new Set, bindings = /* @__PURE__ */ new Map, comparing = null, landing = !1, echo = Math.max(0, options.echo ?? 0), grabbed = /* @__PURE__ */ new Set, changedAt = /* @__PURE__ */ new Map, sent = /* @__PURE__ */ new Map, pending = /* @__PURE__ */ new Map, echoTimer, now = () => typeof performance < "u" ? performance.now() : Date.now(), isHeld = (key) => grabbed.has(key) || echo > 0 && now() - (changedAt.get(key) ?? -1 / 0) < echo, NO_BASE = Object.freeze({ preset: null, groups: Object.freeze({}) }), base = NO_BASE, past = [], future = [], HISTORY = 100, stateKey = storageKey(id, "state"), pinnedOf, pinned = () => {
1629
+ if (pinnedOf?.schema !== schema)
1630
+ pinnedOf = { schema, keys: pinnedKeys(schema, options.pinned) };
1631
+ return pinnedOf.keys;
1632
+ }, unpinned = (values) => {
1633
+ let keys = pinned();
1634
+ if (keys.size === 0)
1635
+ return values;
1636
+ let out = { ...values };
1637
+ for (let k of keys)
1638
+ if (Object.hasOwn(locked, k))
1639
+ out[k] = locked[k];
1640
+ return Object.freeze(out);
1641
+ }, presetPatch = (values) => {
1642
+ let keys = pinned();
1643
+ if (keys.size === 0)
1644
+ return values;
1645
+ return Object.fromEntries(Object.entries(values).filter(([k]) => !keys.has(k)));
1646
+ }, buildPresets = () => Object.freeze(Object.fromEntries([
1616
1647
  ["default", locked],
1617
1648
  ...Object.entries(options.presets ?? {}).map(([id, patch]) => {
1618
1649
  if (id === "default" || id === "custom")
1619
1650
  throw Error(`Reserved preset id: ${id}`);
1620
- return [id, normalize(schema, { ...locked, ...patch })];
1651
+ return [id, unpinned(normalize(schema, { ...locked, ...patch }))];
1621
1652
  })
1622
1653
  ])), presets = buildPresets(), buildGroupPresets = () => Object.freeze(Object.fromEntries(Object.entries(options.groupPresets ?? {}).map(([group, list]) => {
1623
1654
  let named = list.flatMap((p) => Object.keys(p.values).filter((k) => Object.hasOwn(schema, k))), covered = new Set((named.length ? named : Object.keys(schema).filter((k) => schema[k].group === group)).filter((k) => hasValue(schema[k])));
@@ -1718,7 +1749,7 @@ function defineControls(id, input, options = {}) {
1718
1749
  else if (ref?.kind === "variant") {
1719
1750
  let found = list?.find((v) => v.id === ref.id);
1720
1751
  if (found)
1721
- out = normalize(schema, found.values);
1752
+ out = unpinned(normalize(schema, found.values));
1722
1753
  }
1723
1754
  let groups = Object.entries(b.groups);
1724
1755
  if (groups.length) {
@@ -1730,11 +1761,10 @@ function defineControls(id, input, options = {}) {
1730
1761
  return baseCache = { base: b, schema, presets, groupPresets, variants: list, values }, values;
1731
1762
  }
1732
1763
  let covered = (key) => {
1733
- let b = activeBase();
1734
- if (b.preset)
1764
+ let b = activeBase(), group = schema[key]?.group;
1765
+ if (group !== void 0 && Object.hasOwn(b.groups, group))
1735
1766
  return !0;
1736
- let group = schema[key]?.group;
1737
- return group !== void 0 && Object.hasOwn(b.groups, group);
1767
+ return b.preset !== null && !pinned().has(key);
1738
1768
  };
1739
1769
  function declaredShortcut(id) {
1740
1770
  let slash = id.indexOf("/"), key = slash < 0 ? id : id.slice(0, slash), field = Object.hasOwn(schema, key) ? schema[key] : void 0;
@@ -2004,7 +2034,7 @@ function defineControls(id, input, options = {}) {
2004
2034
  listen(cb) {
2005
2035
  return readoutListeners.add(cb), () => readoutListeners.delete(cb);
2006
2036
  }
2007
- }, variants = createVariants(() => schema, id, storageKey(id, "variants"), variantStorage, get, (v, variant, coalesce) => write(v, { source: "variant", coalesce }, makeBase(Object.freeze({ kind: "variant", id: variant }), {})), notify, enabled), resettable = (keys) => (keys ?? fieldKeys()).filter((k) => valueField(k) && !blocked(k));
2037
+ }, variants = createVariants(() => schema, id, storageKey(id, "variants"), variantStorage, () => unpinned(get()), (v, variant, coalesce) => write(presetPatch(v), { source: "variant", coalesce }, makeBase(Object.freeze({ kind: "variant", id: variant }), {})), notify, enabled), resettable = (keys) => (keys ?? fieldKeys()).filter((k) => valueField(k) && !blocked(k));
2008
2038
  function offBase(keys) {
2009
2039
  if (!hasBase())
2010
2040
  return !1;
@@ -2089,6 +2119,7 @@ function defineControls(id, input, options = {}) {
2089
2119
  groupFolded: options.groupFolded,
2090
2120
  sectionFolded: options.sectionFolded,
2091
2121
  presetLabel: (presetId) => options.presetLabels?.[presetId] ?? (presetId === "default" ? options.defaultLabel : void 0) ?? presetId,
2122
+ isPinned: (key) => pinned().has(key),
2092
2123
  get,
2093
2124
  set,
2094
2125
  sync,
@@ -2363,9 +2394,12 @@ function defineControls(id, input, options = {}) {
2363
2394
  preset(id, presetOptions) {
2364
2395
  if (!Object.hasOwn(presets, id))
2365
2396
  throw Error(`Unknown preset: ${id}`);
2366
- write(presets[id], { source: "preset", coalesce: presetOptions?.coalesce }, id === "default" ? NO_BASE : makeBase(Object.freeze({ kind: "preset", id }), {}));
2397
+ write(presetPatch(presets[id]), { source: "preset", coalesce: presetOptions?.coalesce }, id === "default" ? NO_BASE : makeBase(Object.freeze({ kind: "preset", id }), {}));
2398
+ },
2399
+ matchingPreset: () => {
2400
+ let cur = unpinned(get());
2401
+ return Object.keys(presets).find((id) => equal(cur, presets[id])) ?? "custom";
2367
2402
  },
2368
- matchingPreset: () => Object.keys(presets).find((id) => equal(get(), presets[id])) ?? "custom",
2369
2403
  get groupPresets() {
2370
2404
  return groupPresets;
2371
2405
  },
@@ -2692,6 +2726,7 @@ function createRemoteControls(id, descriptors, options = {}) {
2692
2726
  tunable: options.tunable,
2693
2727
  history: options.history,
2694
2728
  presets: options.presets,
2729
+ pinned: options.pinned,
2695
2730
  groupPresets: options.groupPresets,
2696
2731
  onPreset: options.onPreset,
2697
2732
  actions: options.actions,
package/dist/panel.js CHANGED
@@ -3288,6 +3288,9 @@ ${GRAD_PROPS}
3288
3288
  .knobs-group-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:20px;margin:var(--k-group-gap) 0 var(--_k-gap);padding:0 var(--k-group-inset);color:var(--_k-group-label);font-size:var(--k-group-size);font-weight:var(--_k-weight-title);letter-spacing:var(--k-group-tracking);text-transform:var(--k-group-transform)}
3289
3289
  .knobs-panel:not(.knobs-page) .knobs-group-head:not(:first-child){border-top:1px solid var(--k-group-divider);padding-top:var(--k-group-divider-gap)}
3290
3290
  .knobs-group-head:first-child{margin-top:0}
3291
+ .knobs-panel:not(.knobs-page) .knobs-presets+.knobs-group-head{margin-top:0;border-top:0;padding-top:0}
3292
+ .knobs-pinned{margin-bottom:var(--k-group-gap)}
3293
+ .knobs-pinned:empty{display:none}
3291
3294
  .knobs-group-head>span:last-child{display:flex;align-items:center;gap:var(--_k-tray-pad);margin-right:calc((var(--k-icon-sm) - 20px) / 2)}
3292
3295
  .knobs-group-head button{height:20px;padding:0 6px;font-size:var(--_k-text-meta);font-weight:500;letter-spacing:0;text-transform:none;color:var(--_k-fg-2);background:transparent;border-radius:var(--_k-radius-sm);cursor:pointer}
3293
3296
  .knobs-group-head button:is(:hover:not([disabled]),:focus-visible){background:var(--_k-fill-selected);color:var(--_k-fg)}
@@ -6928,6 +6931,9 @@ function resolveActivePresetEntry(entries, currentValues, lastAppliedId, baseId
6928
6931
  isCustom: true
6929
6932
  };
6930
6933
  }
6934
+ function presetsTouch(entries, keys, defaults) {
6935
+ return entries.some((e) => !(e.kind === "preset" && e.rawId === "default") && keys.some((k) => !same2(e.values[k], defaults[k])));
6936
+ }
6931
6937
  function nextPresetIndex(count, activeIndex, direction) {
6932
6938
  if (count <= 0)
6933
6939
  return -1;
@@ -6935,7 +6941,7 @@ function nextPresetIndex(count, activeIndex, direction) {
6935
6941
  return direction > 0 ? 0 : count - 1;
6936
6942
  return ((activeIndex + direction) % count + count) % count;
6937
6943
  }
6938
- function createPresetStrip(doc, controls, showVariants, run, onAfterChange, presetLabel = (id) => controls.presetLabel(id)) {
6944
+ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, presetLabel = (id) => controls.presetLabel(id), relevant) {
6939
6945
  const wake = () => onAfterChange?.();
6940
6946
  const el = (tag, cls, text) => {
6941
6947
  const node = doc.createElement(tag);
@@ -6981,6 +6987,15 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
6981
6987
  let prevDrifted = false;
6982
6988
  let initializedPill = false;
6983
6989
  let slideUntil = 0;
6990
+ const unpinned = (values) => {
6991
+ let out;
6992
+ for (const k of Object.keys(values))
6993
+ if (controls.isPinned(k) && !same2(values[k], controls.locked[k])) {
6994
+ out ??= { ...values };
6995
+ out[k] = controls.locked[k];
6996
+ }
6997
+ return out ?? values;
6998
+ };
6984
6999
  function buildEntries() {
6985
7000
  const presetEntries = Object.entries(controls.presets).map(([id, presetValues]) => ({
6986
7001
  id: `p:${id}`,
@@ -6998,7 +7013,7 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
6998
7013
  kind: "variant",
6999
7014
  rawId: v.id,
7000
7015
  label: v.name ?? v.id.slice(0, 8),
7001
- values: v.values,
7016
+ values: unpinned(v.values),
7002
7017
  apply: (coalesce) => controls.variants.load(v.id, { coalesce })
7003
7018
  }));
7004
7019
  return [...presetEntries, ...variantEntries];
@@ -7050,7 +7065,13 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7050
7065
  if (first)
7051
7066
  doc.defaultView?.requestAnimationFrame(() => pill.style.transition = "");
7052
7067
  }
7053
- const resolve = () => resolveActivePresetEntry(entries, controls.get(), lastAppliedId, activeEntryId(controls));
7068
+ const resolve = () => resolveActivePresetEntry(entries, unpinned(controls.get()), lastAppliedId, activeEntryId(controls));
7069
+ const wanted = (list) => (list.length > 1 || showVariants) && (relevant?.(list) ?? true);
7070
+ function syncShown() {
7071
+ const show = wanted(entries);
7072
+ if (root.hidden === show && root.dataset.presence === undefined)
7073
+ root.hidden = !show;
7074
+ }
7054
7075
  function syncSelection(snap = false) {
7055
7076
  activeState = resolve();
7056
7077
  if (lastAppliedId !== null && activeState.activeId !== lastAppliedId) {
@@ -7107,14 +7128,15 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7107
7128
  run(() => {
7108
7129
  const variantsRef = showVariants ? controls.variants.list() : null;
7109
7130
  if (signature !== "" && variantsRef === lastVariantsRef) {
7131
+ syncShown();
7110
7132
  syncSelection(false);
7111
7133
  return;
7112
7134
  }
7113
7135
  lastVariantsRef = variantsRef;
7114
7136
  const nextEntries = buildEntries();
7115
7137
  const nextSig = JSON.stringify(nextEntries.map((e) => [e.id, e.label]));
7116
- const shouldShow = nextEntries.length > 1 || showVariants;
7117
- root.hidden = !shouldShow;
7138
+ entries = nextEntries;
7139
+ syncShown();
7118
7140
  if (nextSig !== signature) {
7119
7141
  signature = nextSig;
7120
7142
  entries = nextEntries;
@@ -7385,6 +7407,7 @@ function createPresetStrip(doc, controls, showVariants, run, onAfterChange, pres
7385
7407
  getEntries: () => entries,
7386
7408
  getButtons: () => buttons,
7387
7409
  getActiveState: () => activeState,
7410
+ shows: () => wanted(entries),
7388
7411
  cycle,
7389
7412
  selectById,
7390
7413
  save: () => {
@@ -10164,6 +10187,8 @@ class GpuOverlayManager {
10164
10187
  });
10165
10188
  ro.observe(panel.root);
10166
10189
  ro.observe(panel.body);
10190
+ if (panel.pinned)
10191
+ ro.observe(panel.pinned);
10167
10192
  ro.observe(panel.presetStrip.track);
10168
10193
  const mo = new MutationObserver(() => {
10169
10194
  this.styleVersion++;
@@ -12873,7 +12898,9 @@ class GpuOverlayManager {
12873
12898
  }
12874
12899
  const ps = panel.presetStrip;
12875
12900
  const hinting = panel.root.classList.contains("knobs-hinting");
12901
+ const stripAlpha = this.curAlpha;
12876
12902
  if (!ps.element.hidden) {
12903
+ this.curAlpha = stripAlpha * this.presence(ps.element);
12877
12904
  const psHovered = this.curPanelHovered && ps.element.matches(":hover");
12878
12905
  this.curSubHovered = psHovered;
12879
12906
  const psRect = this.rectOf(ps.element);
@@ -12997,6 +13024,7 @@ class GpuOverlayManager {
12997
13024
  align: "center"
12998
13025
  });
12999
13026
  }
13027
+ this.curAlpha = stripAlpha;
13000
13028
  }
13001
13029
  const tabs = panel.tabs;
13002
13030
  if (tabs && !tabs.element.hidden)
@@ -13112,7 +13140,7 @@ class GpuOverlayManager {
13112
13140
  }
13113
13141
  if (bodyRevealing || pageMoving)
13114
13142
  this.curBypassRectCache = true;
13115
- const bodyHovered = this.curPanelHovered && panel.body.matches(":hover");
13143
+ const bodyHovered = this.curPanelHovered && (panel.body.matches(":hover") || Boolean(panel.pinned?.matches(":hover")));
13116
13144
  this.curSubHovered = bodyHovered;
13117
13145
  this.curPointerRow = bodyHovered && panel.root.hasAttribute("data-k-row-hover");
13118
13146
  const panelAlpha = this.curAlpha;
@@ -25740,7 +25768,7 @@ function buildPanel(controls, host, options) {
25740
25768
  titleEl.title = `${titleEl.textContent ? `${titleEl.textContent}
25741
25769
  ` : ""}Press ${show("help")} for keyboard shortcuts`;
25742
25770
  const presetLabel = (id) => options.presetLabels?.[id] ?? (id === "default" ? options.defaultLabel : undefined) ?? controls.presetLabel(id);
25743
- const presetStrip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel);
25771
+ const presetStrip = createPresetStrip(doc, controls, showVariants, run, () => wakeGpu(), presetLabel, (entries) => presetsTouchTab(entries));
25744
25772
  const tabStrip = createTabStrip(doc, (id, animate) => selectTab(id, animate));
25745
25773
  let activeTab;
25746
25774
  const searchOptions = typeof options.search === "object" ? options.search : {};
@@ -25783,6 +25811,10 @@ function buildPanel(controls, host, options) {
25783
25811
  ];
25784
25812
  const body = el("div");
25785
25813
  const tabMotion = tabTransition(body, root, () => wakeGpu());
25814
+ const pinnedBody = el("div");
25815
+ pinnedBody.className = "knobs-pinned";
25816
+ if (!page)
25817
+ body.append(presetStrip.element);
25786
25818
  const blocks = [];
25787
25819
  const cards = [];
25788
25820
  const groupResetButtons = new Map;
@@ -26264,8 +26296,12 @@ function buildPanel(controls, host, options) {
26264
26296
  section: field.section
26265
26297
  });
26266
26298
  }
26267
- return gatherSubsections(specs);
26299
+ const planned = gatherSubsections(specs);
26300
+ if (page || !planned.some(pinnedRow))
26301
+ return planned;
26302
+ return [...planned.filter(pinnedRow), ...planned.filter((spec) => !pinnedRow(spec))];
26268
26303
  };
26304
+ const pinnedRow = (r) => !page && controls.isPinned(r.keys[0] ?? r.id);
26269
26305
  const gatherSubsections = (specs) => {
26270
26306
  const out = [];
26271
26307
  for (let i = 0;i < specs.length; ) {
@@ -26911,13 +26947,15 @@ function buildPanel(controls, host, options) {
26911
26947
  const runs = [];
26912
26948
  for (const r of next) {
26913
26949
  const last = runs.at(-1);
26914
- if (last && last.group === r.group)
26950
+ const pinned = pinnedRow(r);
26951
+ if (last && last.group === r.group && last.pinned === pinned)
26915
26952
  last.rows.push(r);
26916
26953
  else
26917
- runs.push({ group: r.group, rows: [r] });
26954
+ runs.push({ group: r.group, pinned, rows: [r] });
26918
26955
  }
26919
26956
  const free = blocks.filter((b) => !b.leaving);
26920
26957
  const chosen = [];
26958
+ const pinnedBlocks = new Set;
26921
26959
  const usedSubheads = new Set;
26922
26960
  for (const run of runs) {
26923
26961
  const at = free.findIndex((b) => b.group === run.group);
@@ -26935,6 +26973,8 @@ function buildPanel(controls, host, options) {
26935
26973
  }
26936
26974
  }
26937
26975
  chosen.push(block);
26976
+ if (run.pinned)
26977
+ pinnedBlocks.add(block);
26938
26978
  const nodes = [];
26939
26979
  let under;
26940
26980
  for (const r of run.rows) {
@@ -26966,7 +27006,9 @@ function buildPanel(controls, host, options) {
26966
27006
  gpuRows.splice(gi, 1);
26967
27007
  subheads.delete(id);
26968
27008
  }
26969
- arrange(body, chosen.flatMap((b) => b.head ? [b.head, b.card] : [b.card]));
27009
+ const nodesOf = (list) => list.flatMap((b) => b.head ? [b.head, b.card] : [b.card]);
27010
+ arrange(pinnedBody, nodesOf(chosen.filter((b) => pinnedBlocks.has(b))));
27011
+ arrange(body, [presetStrip.element, ...nodesOf(chosen.filter((b) => !pinnedBlocks.has(b)))]);
26970
27012
  for (const block of free) {
26971
27013
  block.leaving = true;
26972
27014
  const fold = () => {
@@ -27097,19 +27139,28 @@ function buildPanel(controls, host, options) {
27097
27139
  return f.tab;
27098
27140
  return;
27099
27141
  };
27100
- const tabOf = (r) => r.field.tab ?? (r.group !== undefined ? groupTab(r.group) : undefined);
27142
+ const tabOf = (r) => pinnedRow(r) ? undefined : r.field.tab ?? (r.group !== undefined ? groupTab(r.group) : undefined);
27101
27143
  const tabList = () => {
27102
27144
  if (page)
27103
27145
  return [];
27104
27146
  const list = (options.tabs ?? []).map((t) => typeof t === "string" ? { id: t, label: readableTitle(t) } : { id: t.id, label: t.label ?? readableTitle(t.id) });
27105
27147
  const seen = new Set(list.map((t) => t.id));
27106
- for (const f of Object.values(controls.schema))
27107
- if (f.tab !== undefined && !seen.has(f.tab)) {
27148
+ for (const [key, f] of Object.entries(controls.schema))
27149
+ if (f.tab !== undefined && !seen.has(f.tab) && !controls.isPinned(key)) {
27108
27150
  seen.add(f.tab);
27109
27151
  list.push({ id: f.tab, label: readableTitle(f.tab) });
27110
27152
  }
27111
27153
  return list;
27112
27154
  };
27155
+ function presetsTouchTab(entries) {
27156
+ if (page || activeTab === undefined)
27157
+ return true;
27158
+ const keys = rows.flatMap((r) => {
27159
+ const tab = tabOf(r);
27160
+ return !pinnedRow(r) && (tab === undefined || tab === activeTab) ? r.keys : [];
27161
+ });
27162
+ return presetsTouch(entries, keys, controls.locked);
27163
+ }
27113
27164
  function syncTabs() {
27114
27165
  const list = tabList();
27115
27166
  const has = (id) => id !== undefined && list.some((t) => t.id === id);
@@ -27172,6 +27223,7 @@ function buildPanel(controls, host, options) {
27172
27223
  want.set(b.head, any);
27173
27224
  want.set(b.card, any && !(b.group !== undefined && folds.get(b.group) && !finding));
27174
27225
  }
27226
+ want.set(presetStrip.element, presetStrip.shows());
27175
27227
  return want;
27176
27228
  }
27177
27229
  const shift = (node, show, delay) => {
@@ -27195,6 +27247,9 @@ function buildPanel(controls, host, options) {
27195
27247
  }
27196
27248
  if (tabMotion.running)
27197
27249
  tabMotion.finish();
27250
+ const strip = presetStrip.element;
27251
+ if (strip.hidden === want.get(strip))
27252
+ strip.hidden = !want.get(strip);
27198
27253
  const showing = (n) => !n.hidden && n.dataset.leaving !== "true";
27199
27254
  const set = (node, show, animate) => {
27200
27255
  if (showing(node) === show)
@@ -27481,10 +27536,10 @@ function buildPanel(controls, host, options) {
27481
27536
  root.append(header);
27482
27537
  if (showFooter)
27483
27538
  root.append(popover);
27484
- root.append(tabStrip.element);
27539
+ root.append(pinnedBody, tabStrip.element);
27485
27540
  if (search)
27486
27541
  root.append(search.element);
27487
- root.append(presetStrip.element, body);
27542
+ root.append(body);
27488
27543
  if (panelActions.length > 0)
27489
27544
  root.append(customActions);
27490
27545
  root.append(text, apply, status, live);
@@ -27784,34 +27839,47 @@ function buildPanel(controls, host, options) {
27784
27839
  let lastActionButton;
27785
27840
  const stops = () => {
27786
27841
  const list = [];
27842
+ const strips = [];
27787
27843
  if (visible(tabStrip.element))
27788
- list.push({
27789
- contains: (n) => tabStrip.element.contains(n),
27790
- focus: (o) => tabStrip.focus(o)
27844
+ strips.push({
27845
+ at: tabStrip.element,
27846
+ stops: [{ contains: (n) => tabStrip.element.contains(n), focus: (o) => tabStrip.focus(o) }]
27791
27847
  });
27792
27848
  if (visible(presetStrip.element))
27793
- list.push({
27794
- contains: (n) => presetStrip.element.contains(n),
27795
- focus: (o) => presetStrip.element.focus(o)
27849
+ strips.push({
27850
+ at: presetStrip.element,
27851
+ stops: [
27852
+ {
27853
+ contains: (n) => presetStrip.element.contains(n),
27854
+ focus: (o) => presetStrip.element.focus(o)
27855
+ }
27856
+ ]
27796
27857
  });
27797
27858
  if (page) {
27859
+ for (const strip of strips)
27860
+ list.push(...strip.stops);
27798
27861
  for (const stop of pageStops())
27799
27862
  list.push({ contains: (n) => stop.contains(n), focus: (o) => stop.focus(o) });
27800
27863
  return list;
27801
27864
  }
27865
+ const stopOf = (stop) => ({
27866
+ contains: (n) => stop.contains(n),
27867
+ focus: (o) => stop.focus(o)
27868
+ });
27869
+ const shown = (stops) => stops.filter((s) => !s.closest('[hidden],[data-presence="out"]')).map(stopOf);
27802
27870
  const runs = rows.map((r) => ({
27803
27871
  at: r.row,
27804
- stops: r.editor?.stops() ?? [r.input]
27872
+ stops: shown(r.editor?.stops() ?? [r.input])
27805
27873
  }));
27806
27874
  for (const b of blocks)
27807
27875
  if (b.head?.hasAttribute("data-foldable") && !b.leaving)
27808
- runs.push({ at: b.head, stops: [b.head] });
27876
+ runs.push({ at: b.head, stops: shown([b.head]) });
27809
27877
  for (const sh of subheads.values())
27810
- runs.push({ at: sh.el, stops: [sh.el] });
27878
+ runs.push({ at: sh.el, stops: shown([sh.el]) });
27879
+ runs.push(...strips);
27811
27880
  runs.sort((a, b) => a.at.compareDocumentPosition(b.at) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
27812
27881
  for (const run of runs)
27813
- for (const stop of run.stops.filter((s) => !s.closest('[hidden],[data-presence="out"]')))
27814
- list.push({ contains: (n) => stop.contains(n), focus: (o) => stop.focus(o) });
27882
+ list.push(...run.stops);
27815
27883
  const bar = actionBarButtons();
27816
27884
  if (bar.length)
27817
27885
  list.push({
@@ -29265,6 +29333,7 @@ function buildPanel(controls, host, options) {
29265
29333
  id: controls.id,
29266
29334
  root,
29267
29335
  body,
29336
+ pinned: pinnedBody,
29268
29337
  inline,
29269
29338
  header,
29270
29339
  titleEl,
@@ -130,6 +130,8 @@ export interface RemoteControlsOptions {
130
130
  readonly nullDefaults?: boolean;
131
131
  readonly values?: State;
132
132
  readonly presets?: Readonly<Record<string, State>>;
133
+ /** Groups pinned above the tab strip, outside the presets (see `ControlsOptions.pinned`). */
134
+ readonly pinned?: readonly string[];
133
135
  /** Numbered presets per group, e.g. the phone's preset sets (see `ControlsOptions.groupPresets`). */
134
136
  readonly groupPresets?: Readonly<Record<string, readonly GroupPreset[]>>;
135
137
  /** Display labels by group id, section id and preset id (see `ControlsOptions.groupLabels`). */
@@ -50,6 +50,12 @@ export interface FieldMeta {
50
50
  * Fields without a tab show on every tab.
51
51
  */
52
52
  readonly tab?: string;
53
+ /**
54
+ * Pins the field's group to the top of a panel, under the header and above the tab strip, on
55
+ * every tab (the first field of a group that says wins; a field without a group pins itself).
56
+ * Panel presets and saved variants leave pinned fields alone. See `ControlsOptions.pinned`.
57
+ */
58
+ readonly pinned?: boolean;
53
59
  /**
54
60
  * A collapsible sub-heading inside the field's group: a flat heading with a hairline, never a
55
61
  * card inside the card. Consecutive fields that share it sit under one heading.
@@ -57,7 +63,7 @@ export interface FieldMeta {
57
63
  readonly subsection?: string;
58
64
  }
59
65
  /** Meta keys copied from a field's options or a descriptor onto the field. */
60
- export declare const META_KEYS: readonly ["label", "group", "groupLabel", "section", "sectionLabel", "groupDescription", "sectionDescription", "shortcut", "random", "disabled", "readOnly", "folded", "sectionFolded", "tab", "subsection"];
66
+ export declare const META_KEYS: readonly ["label", "group", "groupLabel", "section", "sectionLabel", "groupDescription", "sectionDescription", "shortcut", "random", "disabled", "readOnly", "folded", "sectionFolded", "tab", "pinned", "subsection"];
61
67
  /** The meta `source` gives, without undefined keys. */
62
68
  export declare function pickMeta(source: FieldMeta | undefined, skip?: readonly string[]): FieldMeta;
63
69
  /** Marks a knob as half of a flat spring: two number keys drawn as one spring card. */
@@ -329,6 +335,11 @@ export declare const isReadout: (field: {
329
335
  /** Whether a field holds a value (actions, toolbars and readouts don't). */
330
336
  export declare const hasValue: (field: Field) => field is ValueField;
331
337
  export type Schema = Readonly<Record<string, Field>>;
338
+ /**
339
+ * The schema's pinned keys (values, actions and readouts alike): every key of a group in `groups`,
340
+ * or whose first field that gives `pinned` says `true`, and ungrouped fields that say so themselves.
341
+ */
342
+ export declare function pinnedKeys(schema: Schema, groups?: readonly string[]): ReadonlySet<string>;
332
343
  export type FieldValue<F extends Field> = F extends Knob ? number : F extends Choice<infer T> ? T : F extends Toggle ? boolean : F extends SpringField ? SpringValue : F extends ColorField ? string : F extends PointField ? Point : F extends EasingField ? Bezier : F extends VecField<3> ? Vec3 : F extends VecField<2> ? Point : F extends RangeField ? readonly [number, number] : F extends AngleField | SeedField ? number : F extends KeybindField ? string : F extends GradientField ? readonly GradientStop[] : F extends CurveField ? readonly CurvePoint[] : never;
333
344
  /** The keys of a schema's readouts (what `controls.watch` takes). */
334
345
  export type ReadoutKeys<S extends Schema> = {
@@ -449,6 +460,7 @@ export declare function range(value: readonly [number, number], options: FieldMe
449
460
  folded?: boolean;
450
461
  sectionFolded?: boolean;
451
462
  tab?: string;
463
+ pinned?: boolean;
452
464
  subsection?: string;
453
465
  };
454
466
  /** Degrees on a dial (0–360 and wrapping by default): `angle(45)`, `angle(30, { min: -90, max: 90 })`. */
@@ -348,6 +348,12 @@ export interface ControlsOptions<S extends Schema> extends LabelOptions {
348
348
  /** An undo stack the host owns; undo and redo call it (see `HostHistory`). */
349
349
  history?: HostHistory;
350
350
  presets?: Readonly<Record<string, Partial<Values<S>>>>;
351
+ /**
352
+ * Groups pinned to the top of a panel, under the header and above the tab strip, on every tab
353
+ * (as fields' `pinned: true` pins theirs). Presets and saved variants leave them alone: applying
354
+ * one keeps their values, saving one keeps them out, and they're modified against their defaults.
355
+ */
356
+ pinned?: readonly string[];
351
357
  /**
352
358
  * Numbered presets per group, shown as 1, 2, 3… on the group's card in a page. A group's
353
359
  * presets cover every key any of them names; keys a preset leaves out are at their defaults,
@@ -426,6 +432,8 @@ export interface Controls<S extends Schema = Schema> {
426
432
  readonly sectionFolded: SectionFolded | undefined;
427
433
  /** A preset's display label (`presetLabels`, `defaultLabel`), else its id. */
428
434
  presetLabel(id: string): string;
435
+ /** Whether `key` is pinned (its group is, see `ControlsOptions.pinned`), outside the presets. */
436
+ isPinned(key: string): boolean;
429
437
  readonly error: string | null;
430
438
  get(): Values<S>;
431
439
  getServerSnapshot(): Values<S>;
@@ -407,6 +407,8 @@ export interface GpuPanelBinding {
407
407
  readonly id: string;
408
408
  readonly root: HTMLElement;
409
409
  readonly body: HTMLElement;
410
+ /** The pinned groups' cards, above the tab strip (empty while none are pinned). */
411
+ readonly pinned?: HTMLElement;
410
412
  readonly inline: boolean;
411
413
  readonly header: HTMLElement;
412
414
  readonly titleEl: HTMLElement;
@@ -28,6 +28,8 @@ export declare function activeEntryId(controls: Controls<any>): string | null;
28
28
  * cycling never gets stuck when a saved variant has the exact same values as a built-in preset.
29
29
  */
30
30
  export declare function resolveActivePresetEntry<V extends Record<string, any>>(entries: readonly PresetEntry<V>[], currentValues: V, lastAppliedId: string | null, baseId?: string | null): ActivePresetState;
31
+ /** Whether any entry but the defaults gives one of `keys` a value other than its default. */
32
+ export declare function presetsTouch(entries: readonly PresetEntry<any>[], keys: readonly string[], defaults: Readonly<Record<string, unknown>>): boolean;
31
33
  export declare function nextPresetIndex(count: number, activeIndex: number, direction: number): number;
32
34
  export interface PresetStrip {
33
35
  readonly element: HTMLElement;
@@ -52,9 +54,13 @@ export interface PresetStrip {
52
54
  readonly nameField: () => HTMLInputElement | null;
53
55
  /** Deletes the selected chip when it's a saved variant; returns a status message. */
54
56
  deleteActive(): string | undefined;
57
+ /** Whether the strip should show: presets to pick (or saving on) that touch what's shown. */
58
+ shows(): boolean;
55
59
  update(): void;
56
60
  dispose(): void;
57
61
  }
58
62
  export declare function createPresetStrip<S extends Schema>(doc: Document, controls: Controls<S>, showVariants: boolean, run: (action: () => unknown) => void, onAfterChange?: () => void,
59
63
  /** What a preset's chip says (its id stays its identity); `controls.presetLabel` by default. */
60
- presetLabel?: (id: string) => string): PresetStrip;
64
+ presetLabel?: (id: string) => string,
65
+ /** Whether these entries matter where the strip is shown (a tab they don't touch hides it). */
66
+ relevant?: (entries: readonly PresetEntry<any>[]) => boolean): PresetStrip;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/knobs",
3
- "version": "0.4.0-next.10",
3
+ "version": "0.4.0-next.11",
4
4
  "description": "Per-component tuning controls. Explore in development, ship constants.",
5
5
  "type": "module",
6
6
  "license": "MIT",