@kitlangton/knobs 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +135 -17
  2. package/dist/dev/{chunk-gezhc24g.js → chunk-7xgkerd2.js} +14 -5
  3. package/dist/dev/{chunk-5cr85vv6.js → chunk-jx1ktap3.js} +7843 -7933
  4. package/dist/dev/{chunk-693jd1gj.js → chunk-pmkvb9ep.js} +1544 -160
  5. package/dist/dev/{chunk-4km87zfx.js → chunk-v8ssbcbk.js} +18 -7
  6. package/dist/dev/index.js +187 -31
  7. package/dist/index.js +197 -33
  8. package/dist/panel.js +20260 -18462
  9. package/dist/types/core/keys.d.ts +2 -0
  10. package/dist/types/core/remote.d.ts +2 -0
  11. package/dist/types/core/schema.d.ts +5 -0
  12. package/dist/types/core/store.d.ts +29 -2
  13. package/dist/types/core/unlock.d.ts +77 -0
  14. package/dist/types/index.d.ts +1 -0
  15. package/dist/types/panel/attach.d.ts +2 -0
  16. package/dist/types/panel/crumbs.d.ts +19 -0
  17. package/dist/types/panel/editors.d.ts +9 -0
  18. package/dist/types/panel/feel.d.ts +103 -0
  19. package/dist/types/panel/give.d.ts +34 -1
  20. package/dist/types/panel/gpu/color.d.ts +4 -4
  21. package/dist/types/panel/gpu/renderer.d.ts +32 -0
  22. package/dist/types/panel/gpu/shader.wgsl.d.ts +1 -1
  23. package/dist/types/panel/index.d.ts +2 -1
  24. package/dist/types/panel/inspector.d.ts +1 -1
  25. package/dist/types/panel/lens.d.ts +113 -0
  26. package/dist/types/panel/motion.d.ts +2 -0
  27. package/dist/types/panel/page-hints.d.ts +2 -1
  28. package/dist/types/panel/prefs.d.ts +2 -2
  29. package/dist/types/panel/presence.d.ts +20 -0
  30. package/dist/types/panel/scope.d.ts +29 -0
  31. package/dist/types/panel/slider.d.ts +21 -4
  32. package/dist/types/panel/tab-transition.d.ts +15 -0
  33. package/dist/types/panel/tabs.d.ts +1 -1
  34. package/dist/types/panel/toggle.d.ts +27 -0
  35. package/dist/types/ui/look.d.ts +1 -1
  36. package/dist/types/ui/scrollbar.d.ts +12 -0
  37. package/dist/types/ui/split-model.d.ts +4 -3
  38. package/dist/types/ui/split.d.ts +8 -6
  39. package/dist/ui-react.js +232 -132
  40. package/dist/ui-solid.js +232 -132
  41. package/dist/ui.js +285 -174
  42. package/package.json +1 -1
package/dist/ui.js CHANGED
@@ -276,7 +276,7 @@ var open = springEasing(2 * Math.PI / 0.22, 1);
276
276
  var activityCss = `
277
277
  :where(.knobs-activity,.knobs-activity *){box-sizing:border-box}
278
278
  :where(.knobs-activity){color-scheme:dark}
279
- .knobs-ui.knobs-activity{position:relative;display:block;height:100%;overflow:auto;overflow-anchor:none;outline:none;scrollbar-width:thin;overscroll-behavior:contain;user-select:none;-webkit-user-select:none;
279
+ .knobs-ui.knobs-activity{position:relative;display:block;height:100%;overflow:auto;overflow-anchor:none;outline:none;scrollbar-width:none;overscroll-behavior:contain;user-select:none;-webkit-user-select:none;
280
280
  font:var(--ku-font-weight) var(--ku-font-size)/1.35 var(--ku-font);color:var(--ku-text);text-align:left;letter-spacing:normal;-webkit-font-smoothing:antialiased;
281
281
  --ka-gutter:var(--k-activity-gutter,28px);--ka-row:var(--k-activity-row,var(--ku-row));--ka-row-child:var(--k-activity-row-child,calc(var(--ka-row) - 6px));
282
282
  --ka-line:var(--k-activity-line,var(--ku-line-hi));--ka-select:var(--k-activity-select,var(--ku-wash-hi));--ka-hover:var(--k-activity-hover,var(--ku-wash));
@@ -627,6 +627,160 @@ var readoutCss = `
627
627
  @media(prefers-reduced-motion:reduce){.knobs-rd :where(*){transition:none!important}}
628
628
  `;
629
629
 
630
+ // src/core/keys.ts
631
+ var MODS = ["Ctrl", "Alt", "Shift", "Meta"];
632
+ var ALIASES = {
633
+ ctrl: "Ctrl",
634
+ control: "Ctrl",
635
+ "⌃": "Ctrl",
636
+ alt: "Alt",
637
+ option: "Alt",
638
+ opt: "Alt",
639
+ "⌥": "Alt",
640
+ shift: "Shift",
641
+ "⇧": "Shift",
642
+ meta: "Meta",
643
+ cmd: "Meta",
644
+ command: "Meta",
645
+ "⌘": "Meta",
646
+ mod: "Mod"
647
+ };
648
+ var NAMED = {
649
+ " ": "Space",
650
+ space: "Space",
651
+ spacebar: "Space",
652
+ esc: "Escape",
653
+ escape: "Escape",
654
+ enter: "Enter",
655
+ return: "Enter",
656
+ tab: "Tab",
657
+ backspace: "Backspace",
658
+ delete: "Delete",
659
+ del: "Delete",
660
+ up: "ArrowUp",
661
+ down: "ArrowDown",
662
+ left: "ArrowLeft",
663
+ right: "ArrowRight",
664
+ arrowup: "ArrowUp",
665
+ arrowdown: "ArrowDown",
666
+ arrowleft: "ArrowLeft",
667
+ arrowright: "ArrowRight",
668
+ home: "Home",
669
+ end: "End",
670
+ pageup: "PageUp",
671
+ pagedown: "PageDown"
672
+ };
673
+ var isApple = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
674
+ var keyName = (raw) => {
675
+ if (!raw)
676
+ return "";
677
+ const named = NAMED[raw.toLowerCase()];
678
+ if (named)
679
+ return named;
680
+ if (raw.length === 1)
681
+ return raw.toUpperCase();
682
+ if (/^f\d{1,2}$/i.test(raw))
683
+ return raw.toUpperCase();
684
+ return raw.charAt(0).toUpperCase() + raw.slice(1);
685
+ };
686
+ function normalizeCombo(combo) {
687
+ const trimmed = combo.trim();
688
+ if (!trimmed)
689
+ return "";
690
+ const parts = trimmed === "+" ? ["+"] : trimmed.replace(/\+\+$/, "+plus").split("+");
691
+ const mods = new Set;
692
+ let key = "";
693
+ for (const part of parts) {
694
+ const alias = ALIASES[part.trim().toLowerCase()] ?? ALIASES[part.trim()];
695
+ if (alias === "Mod")
696
+ mods.add(isApple() ? "Meta" : "Ctrl");
697
+ else if (alias)
698
+ mods.add(alias);
699
+ else
700
+ key = part === "plus" ? "+" : keyName(part.trim() || part);
701
+ }
702
+ if (!key)
703
+ return "";
704
+ return [...MODS.filter((m) => mods.has(m)), key].join("+");
705
+ }
706
+ var layout = null;
707
+ var layoutAsked = false;
708
+ function askLayout() {
709
+ if (layoutAsked)
710
+ return;
711
+ layoutAsked = true;
712
+ const keyboard = globalThis.navigator?.keyboard;
713
+ keyboard?.getLayoutMap?.().then((map) => {
714
+ layout = map;
715
+ }).catch(() => {});
716
+ }
717
+ function letterOf(event) {
718
+ askLayout();
719
+ const code = event.code ?? "";
720
+ if (/^[a-z]$/i.test(event.key) && !event.altKey)
721
+ return event.key.toUpperCase();
722
+ const mapped = layout?.get(code);
723
+ if (mapped && /^[a-z]$/i.test(mapped))
724
+ return mapped.toUpperCase();
725
+ return /^Key[A-Z]$/.test(code) ? code.slice(3) : "";
726
+ }
727
+ function comboFromEvent(event) {
728
+ if (["Shift", "Alt", "Meta", "Control", "CapsLock", "Fn", "OS"].includes(event.key))
729
+ return "";
730
+ const code = event.code ?? "";
731
+ const letter = letterOf(event);
732
+ const key = letter ? letter : /^Digit\d$/.test(code) ? code.slice(5) : code === "Space" ? "Space" : keyName(event.key);
733
+ return [
734
+ event.ctrlKey ? "Ctrl" : "",
735
+ event.altKey ? "Alt" : "",
736
+ event.shiftKey ? "Shift" : "",
737
+ event.metaKey ? "Meta" : "",
738
+ key
739
+ ].filter(Boolean).join("+");
740
+ }
741
+ function matchesCombo(combo, event) {
742
+ const want = normalizeCombo(combo);
743
+ return want !== "" && want === comboFromEvent(event);
744
+ }
745
+ var GLYPHS = { Ctrl: "⌃", Alt: "⌥", Shift: "⇧", Meta: "⌘" };
746
+ var WORDS = { Ctrl: "Ctrl", Alt: "Alt", Shift: "Shift", Meta: "Meta" };
747
+ var KEY_GLYPHS = {
748
+ ArrowUp: "↑",
749
+ ArrowDown: "↓",
750
+ ArrowLeft: "←",
751
+ ArrowRight: "→",
752
+ Backspace: "⌫",
753
+ Delete: "⌦",
754
+ Enter: "↩",
755
+ Escape: "Esc",
756
+ Tab: "⇥",
757
+ PageUp: "PgUp",
758
+ PageDown: "PgDn"
759
+ };
760
+ function comboParts(combo, apple = isApple()) {
761
+ const norm = normalizeCombo(combo);
762
+ if (!norm)
763
+ return [];
764
+ const parts = norm === "+" ? ["+"] : norm.split("+");
765
+ if (norm.endsWith("++"))
766
+ parts.splice(-2, 2, "+");
767
+ const key = parts.pop();
768
+ const mods = parts;
769
+ const shown = KEY_GLYPHS[key] ?? key;
770
+ return apple ? [...mods.map((m) => GLYPHS[m]), shown] : [...mods.map((m) => WORDS[m]), shown];
771
+ }
772
+ function isTypingTarget(target) {
773
+ const el = target;
774
+ if (!el || typeof el.tagName !== "string")
775
+ return false;
776
+ const tag = el.tagName;
777
+ if (tag === "SELECT" || el.isContentEditable)
778
+ return true;
779
+ if (tag === "TEXTAREA")
780
+ return !el.readOnly;
781
+ return tag === "INPUT" && !el.readOnly && !["range", "checkbox", "radio", "button", "submit", "reset", "color"].includes(el.type);
782
+ }
783
+
630
784
  // src/ui/look.ts
631
785
  var ease = springEasing(26, 1);
632
786
  var TOKENS = {
@@ -744,6 +898,17 @@ var css = `
744
898
  }
745
899
  @supports not (animation-timeline:scroll()){.knobs-scroll-fade-x{text-overflow:ellipsis}}
746
900
 
901
+ /* Overlay scrollbars (trees, activity rails): a thin thumb over the content, so the rows never
902
+ give up width to a scrollbar as they start or stop overflowing. It shows while the content
903
+ scrolls or the pointer is over it. */
904
+ :is(.knobs-tree,.knobs-activity)::-webkit-scrollbar{display:none}
905
+ .knobs-scrollbar{position:sticky;top:0;z-index:3;height:0;pointer-events:none}
906
+ .knobs-scrollbar[hidden]{display:none}
907
+ .knobs-scrollbar-thumb{position:absolute;top:0;right:2px;width:5px;border-radius:3px;background:color-mix(in srgb,var(--ku-text) 30%,transparent);opacity:0;
908
+ transition:opacity .3s ease-out,background .15s}
909
+ :hover>.knobs-scrollbar>.knobs-scrollbar-thumb,.knobs-scrollbar:is([data-active],[data-dragging])>.knobs-scrollbar-thumb{opacity:1;pointer-events:auto;transition-duration:.12s,.15s}
910
+ .knobs-scrollbar-thumb:hover,.knobs-scrollbar[data-dragging]>.knobs-scrollbar-thumb{background:color-mix(in srgb,var(--ku-text) 48%,transparent)}
911
+
747
912
  /* Split: panes along a row or column, hairline dividers with a wider grip. */
748
913
  .knobs-split{position:relative;display:flex;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden}
749
914
  .knobs-split[data-direction="column"]{flex-direction:column}
@@ -780,7 +945,11 @@ var css = `
780
945
  .knobs-split[data-direction="column"]>.knobs-pane[data-edge="start"]>.knobs-pane-rail{bottom:0}
781
946
  .knobs-split[data-direction="column"]>.knobs-pane[data-edge="end"]>.knobs-pane-rail{top:0}
782
947
  .knobs-pane[data-collapsed]>.knobs-pane-rail{pointer-events:auto}
783
- .knobs-ui .knobs-pane[data-collapsed]>.knobs-pane-rail:is(:hover,:focus-visible){background-image:linear-gradient(var(--ku-wash),var(--ku-wash));color:var(--ku-text)}
948
+ /* Its hover wash fades, rather than going in one frame as a click opens the pane. */
949
+ .knobs-ui .knobs-pane-rail::before{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--ku-wash);opacity:0;transition:opacity .15s;pointer-events:none}
950
+ .knobs-pane-rail>*{position:relative}
951
+ .knobs-ui .knobs-pane[data-collapsed]>.knobs-pane-rail:is(:hover,:focus-visible){color:var(--ku-text)}
952
+ .knobs-ui .knobs-pane[data-collapsed]>.knobs-pane-rail:is(:hover,:focus-visible)::before{opacity:1}
784
953
  .knobs-pane-rail-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
785
954
  .knobs-pane-chevron{display:flex;flex:none;width:11px;height:11px;writing-mode:horizontal-tb;color:var(--ku-dim)}
786
955
  .knobs-pane-turn{display:flex;width:100%;height:100%;transform-origin:50% 50%}
@@ -817,7 +986,7 @@ var css = `
817
986
  :root[data-knobs-resizing] :is(iframe,canvas:not(.knobs-gpu-canvas)){pointer-events:none}
818
987
 
819
988
  /* Tree: one scroller, rows placed by transform, only those near the view in the DOM. */
820
- .knobs-tree{position:relative;overflow:auto;height:100%;outline:none;scrollbar-width:thin;overscroll-behavior:contain;user-select:none;-webkit-user-select:none}
989
+ .knobs-tree{position:relative;overflow:auto;height:100%;outline:none;scrollbar-width:none;overscroll-behavior:contain;user-select:none;-webkit-user-select:none}
821
990
  .knobs-tree-space{position:relative;width:100%;pointer-events:none}
822
991
  .knobs-tree-row{position:absolute;left:var(--ku-row-x);right:var(--ku-row-x);top:0;display:flex;align-items:center;height:var(--ku-row);padding-right:calc(var(--ku-inset) - 4px);
823
992
  color:var(--ku-text);white-space:nowrap;cursor:default;will-change:transform;contain:layout style}
@@ -1018,7 +1187,6 @@ var tune = (s, m) => {
1018
1187
  s.zeta = m.zeta;
1019
1188
  return s;
1020
1189
  };
1021
- var isTypingTarget = (t) => t instanceof HTMLElement && (t.isContentEditable || t instanceof HTMLSelectElement || t instanceof HTMLTextAreaElement || t instanceof HTMLInputElement && !t.readOnly && !["range", "checkbox", "radio", "button"].includes(t.type));
1022
1190
  function notifyLayout(doc) {
1023
1191
  doc.dispatchEvent(new Event("knobs-layout"));
1024
1192
  }
@@ -1057,149 +1225,6 @@ var ICONS = {
1057
1225
  lock: svg('<rect x="3.5" y="7" width="9" height="6.5" rx="1.2"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"/>'),
1058
1226
  unlock: svg('<rect x="3.5" y="7" width="9" height="6.5" rx="1.2"/><path d="M5.5 7V5a2.5 2.5 0 0 1 4.8-1"/>')
1059
1227
  };
1060
- // src/core/keys.ts
1061
- var MODS = ["Ctrl", "Alt", "Shift", "Meta"];
1062
- var ALIASES = {
1063
- ctrl: "Ctrl",
1064
- control: "Ctrl",
1065
- "⌃": "Ctrl",
1066
- alt: "Alt",
1067
- option: "Alt",
1068
- opt: "Alt",
1069
- "⌥": "Alt",
1070
- shift: "Shift",
1071
- "⇧": "Shift",
1072
- meta: "Meta",
1073
- cmd: "Meta",
1074
- command: "Meta",
1075
- "⌘": "Meta",
1076
- mod: "Mod"
1077
- };
1078
- var NAMED = {
1079
- " ": "Space",
1080
- space: "Space",
1081
- spacebar: "Space",
1082
- esc: "Escape",
1083
- escape: "Escape",
1084
- enter: "Enter",
1085
- return: "Enter",
1086
- tab: "Tab",
1087
- backspace: "Backspace",
1088
- delete: "Delete",
1089
- del: "Delete",
1090
- up: "ArrowUp",
1091
- down: "ArrowDown",
1092
- left: "ArrowLeft",
1093
- right: "ArrowRight",
1094
- arrowup: "ArrowUp",
1095
- arrowdown: "ArrowDown",
1096
- arrowleft: "ArrowLeft",
1097
- arrowright: "ArrowRight",
1098
- home: "Home",
1099
- end: "End",
1100
- pageup: "PageUp",
1101
- pagedown: "PageDown"
1102
- };
1103
- var isApple = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
1104
- var keyName = (raw) => {
1105
- if (!raw)
1106
- return "";
1107
- const named = NAMED[raw.toLowerCase()];
1108
- if (named)
1109
- return named;
1110
- if (raw.length === 1)
1111
- return raw.toUpperCase();
1112
- if (/^f\d{1,2}$/i.test(raw))
1113
- return raw.toUpperCase();
1114
- return raw.charAt(0).toUpperCase() + raw.slice(1);
1115
- };
1116
- function normalizeCombo(combo) {
1117
- const trimmed = combo.trim();
1118
- if (!trimmed)
1119
- return "";
1120
- const parts = trimmed === "+" ? ["+"] : trimmed.replace(/\+\+$/, "+plus").split("+");
1121
- const mods = new Set;
1122
- let key = "";
1123
- for (const part of parts) {
1124
- const alias = ALIASES[part.trim().toLowerCase()] ?? ALIASES[part.trim()];
1125
- if (alias === "Mod")
1126
- mods.add(isApple() ? "Meta" : "Ctrl");
1127
- else if (alias)
1128
- mods.add(alias);
1129
- else
1130
- key = part === "plus" ? "+" : keyName(part.trim() || part);
1131
- }
1132
- if (!key)
1133
- return "";
1134
- return [...MODS.filter((m) => mods.has(m)), key].join("+");
1135
- }
1136
- var layout = null;
1137
- var layoutAsked = false;
1138
- function askLayout() {
1139
- if (layoutAsked)
1140
- return;
1141
- layoutAsked = true;
1142
- const keyboard = globalThis.navigator?.keyboard;
1143
- keyboard?.getLayoutMap?.().then((map) => {
1144
- layout = map;
1145
- }).catch(() => {});
1146
- }
1147
- function letterOf(event) {
1148
- askLayout();
1149
- const code = event.code ?? "";
1150
- if (/^[a-z]$/i.test(event.key) && !event.altKey)
1151
- return event.key.toUpperCase();
1152
- const mapped = layout?.get(code);
1153
- if (mapped && /^[a-z]$/i.test(mapped))
1154
- return mapped.toUpperCase();
1155
- return /^Key[A-Z]$/.test(code) ? code.slice(3) : "";
1156
- }
1157
- function comboFromEvent(event) {
1158
- if (["Shift", "Alt", "Meta", "Control", "CapsLock", "Fn", "OS"].includes(event.key))
1159
- return "";
1160
- const code = event.code ?? "";
1161
- const letter = letterOf(event);
1162
- const key = letter ? letter : /^Digit\d$/.test(code) ? code.slice(5) : code === "Space" ? "Space" : keyName(event.key);
1163
- return [
1164
- event.ctrlKey ? "Ctrl" : "",
1165
- event.altKey ? "Alt" : "",
1166
- event.shiftKey ? "Shift" : "",
1167
- event.metaKey ? "Meta" : "",
1168
- key
1169
- ].filter(Boolean).join("+");
1170
- }
1171
- function matchesCombo(combo, event) {
1172
- const want = normalizeCombo(combo);
1173
- return want !== "" && want === comboFromEvent(event);
1174
- }
1175
- var GLYPHS = { Ctrl: "⌃", Alt: "⌥", Shift: "⇧", Meta: "⌘" };
1176
- var WORDS = { Ctrl: "Ctrl", Alt: "Alt", Shift: "Shift", Meta: "Meta" };
1177
- var KEY_GLYPHS = {
1178
- ArrowUp: "↑",
1179
- ArrowDown: "↓",
1180
- ArrowLeft: "←",
1181
- ArrowRight: "→",
1182
- Backspace: "⌫",
1183
- Delete: "⌦",
1184
- Enter: "↩",
1185
- Escape: "Esc",
1186
- Tab: "⇥",
1187
- PageUp: "PgUp",
1188
- PageDown: "PgDn"
1189
- };
1190
- function comboParts(combo, apple = isApple()) {
1191
- const norm = normalizeCombo(combo);
1192
- if (!norm)
1193
- return [];
1194
- const parts = norm === "+" ? ["+"] : norm.split("+");
1195
- if (norm.endsWith("++"))
1196
- parts.splice(-2, 2, "+");
1197
- const key = parts.pop();
1198
- const mods = parts;
1199
- const shown = KEY_GLYPHS[key] ?? key;
1200
- return apple ? [...mods.map((m) => GLYPHS[m]), shown] : [...mods.map((m) => WORDS[m]), shown];
1201
- }
1202
-
1203
1228
  // src/core/schema.ts
1204
1229
  var READOUT_KINDS = new Set(["watch", "sparkline", "graph", "meter", "table", "log"]);
1205
1230
  var isReadout = (field) => READOUT_KINDS.has(field.kind);
@@ -1760,15 +1785,7 @@ function claimsHintKey(doc, key) {
1760
1785
  const holder = doc;
1761
1786
  return key === PAGE_HINT_KEY && Boolean(holder[KEY]?.listening);
1762
1787
  }
1763
- var isTyping = (target) => {
1764
- const el = target;
1765
- if (!el || typeof el.tagName !== "string")
1766
- return false;
1767
- const tag = el.tagName;
1768
- if (tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable)
1769
- return true;
1770
- return tag === "INPUT" && !["range", "checkbox", "radio", "button", "submit", "reset", "color"].includes(el.type);
1771
- };
1788
+ var isTyping = isTypingTarget;
1772
1789
  var PRESSABLE = "button,a[href],summary,[role=button],[role=tab],[role=radio],[role=switch],[role=checkbox],[role=menuitem],[role=option]";
1773
1790
  function activateElement(el) {
1774
1791
  const pressable = el.matches(PRESSABLE) || el.tabIndex < 0 && !el.matches("input,select,textarea");
@@ -3353,7 +3370,18 @@ function parseCssColor(raw, fallback = [1, 1, 1, 1]) {
3353
3370
  parseInt(full.slice(4, 6), 16) / 255,
3354
3371
  full.length === 8 ? parseInt(full.slice(6, 8), 16) / 255 : 1
3355
3372
  ];
3356
- } else {
3373
+ } else if (s.startsWith("rgb")) {
3374
+ const nums = s.match(/[\d.]+/g)?.map(Number);
3375
+ if (nums && nums.length >= 3) {
3376
+ out = [
3377
+ Math.min(1, Math.max(0, nums[0] / 255)),
3378
+ Math.min(1, Math.max(0, nums[1] / 255)),
3379
+ Math.min(1, Math.max(0, nums[2] / 255)),
3380
+ nums.length >= 4 ? Math.min(1, Math.max(0, nums[3])) : 1
3381
+ ];
3382
+ }
3383
+ }
3384
+ if (!out) {
3357
3385
  const ctx = probeCtx();
3358
3386
  if (ctx) {
3359
3387
  ctx.clearRect(0, 0, 1, 1);
@@ -4073,7 +4101,7 @@ function paletteOf(el, now = performance.now()) {
4073
4101
  palettes.set(el, { at: now, p });
4074
4102
  return p;
4075
4103
  }
4076
- var reduced = (doc) => doc.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
4104
+ var reduced = (doc) => reducedMotion(doc.defaultView);
4077
4105
  function make(doc) {
4078
4106
  return (tag, cls, text) => {
4079
4107
  const node = doc.createElement(tag);
@@ -4664,7 +4692,7 @@ function createGraph(env) {
4664
4692
  font: "meta",
4665
4693
  align: "left"
4666
4694
  });
4667
- items.push(...k.value.texts(p, hoverAt >= 0 ? p.text : p.text));
4695
+ items.push(...k.value.texts(p, p.text));
4668
4696
  }
4669
4697
  items.push({
4670
4698
  el: top,
@@ -5700,10 +5728,10 @@ function foldFaces(prev, p) {
5700
5728
  function chevronTurn(p, edge) {
5701
5729
  return (edge === "start" ? 180 : -180) * (1 - clamp01(p));
5702
5730
  }
5703
- function chevronFlip(f) {
5704
- return 2 * clamp01(f) - 1;
5731
+ function chevronFlip(f, edge) {
5732
+ return chevronTurn(f, edge);
5705
5733
  }
5706
- var FOLD_STYLE = { chevron: "flip", title: "travel", duration: 0.3 };
5734
+ var FOLD_STYLE = { chevron: "flip", title: "travel", duration: 0.25 };
5707
5735
  function foldStyle(options = {}) {
5708
5736
  const duration = options.duration;
5709
5737
  return {
@@ -5744,10 +5772,12 @@ function serializeLayout(layout) {
5744
5772
  }
5745
5773
 
5746
5774
  // src/ui/split.ts
5747
- var FLIP = springMotion({ duration: 0.18, bounce: 0 });
5775
+ var FLIP = springMotion({ duration: 0.25, bounce: 0 });
5776
+ var FLIP_DEPTH = 28;
5748
5777
  var SETTLE = springMotion({ duration: 0.3, bounce: 0 });
5749
5778
  var CONTENT_BLUR = 3;
5750
5779
  var LABEL_BLUR = 2;
5780
+ var COUNT_CLEAR = 0.12;
5751
5781
  var put = (el, prop, value) => {
5752
5782
  if (el.style.getPropertyValue(prop) !== value)
5753
5783
  el.style.setProperty(prop, value);
@@ -5884,7 +5914,12 @@ function split(container, options) {
5884
5914
  if (!row) {
5885
5915
  head.dataset.toggles = "";
5886
5916
  head.addEventListener("click", () => setCollapsed(spec.id, !collapsed[spec.id]));
5887
- }
5917
+ } else
5918
+ head.addEventListener("dblclick", (e) => {
5919
+ if (e.target.closest(".knobs-pane-btn"))
5920
+ return;
5921
+ setCollapsed(spec.id, true);
5922
+ });
5888
5923
  }
5889
5924
  inner.append(head);
5890
5925
  view.head = head;
@@ -6053,6 +6088,8 @@ function split(container, options) {
6053
6088
  const label = v.railLabel;
6054
6089
  const travel = morph && style.title === "travel" && Boolean(v.titleEl) && label.textContent === v.titleEl.textContent;
6055
6090
  v.el.toggleAttribute("data-travel", travel && folding);
6091
+ if (v.countEl)
6092
+ put(v.countEl, "opacity", travel && folding && p > 0 ? Math.max(0, 1 - p / COUNT_CLEAR).toFixed(3) : "");
6056
6093
  const k = 1 - p;
6057
6094
  if (travel && !folding) {
6058
6095
  put(label, "opacity", "0");
@@ -6080,8 +6117,9 @@ function split(container, options) {
6080
6117
  if (style.chevron === "turn")
6081
6118
  put(v.turn, "transform", k === 0 ? "" : `${move} rotate(${chevronTurn(p, v.edge).toFixed(2)}deg) ${size}`);
6082
6119
  else {
6083
- const w = chevronFlip(v.flip.x);
6084
- put(v.turn, "transform", k === 0 && w === 1 ? "" : `${move} ${size} scaleX(${w.toFixed(4)})`);
6120
+ const turn = chevronFlip(v.flip.x, v.edge);
6121
+ const over = v.flip.resting ? turn === 0 ? "" : "scaleX(-1)" : `perspective(${FLIP_DEPTH}px) rotateY(${turn.toFixed(2)}deg)`;
6122
+ put(v.turn, "transform", k === 0 && !over ? "" : `${move} ${size} ${over}`);
6085
6123
  }
6086
6124
  } else {
6087
6125
  const a = v.faces.rail;
@@ -7446,8 +7484,81 @@ function timeline(container, options) {
7446
7484
  }
7447
7485
  var PLAY = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.6v8.8c0 .5.5.8.9.5l6.6-4.4c.4-.3.4-.8 0-1L5.9 3.1c-.4-.3-.9 0-.9.5Z" fill="currentColor"/></svg>';
7448
7486
  var PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/><rect x="9.2" y="3.2" width="2.8" height="9.6" rx="1" fill="currentColor"/></svg>';
7487
+ // src/ui/scrollbar.ts
7488
+ function overlayScrollbar(scroller, content) {
7489
+ const doc = scroller.ownerDocument;
7490
+ const win = doc.defaultView;
7491
+ const track = doc.createElement("div");
7492
+ track.className = "knobs-scrollbar";
7493
+ track.setAttribute("aria-hidden", "true");
7494
+ const thumb = doc.createElement("div");
7495
+ thumb.className = "knobs-scrollbar-thumb";
7496
+ track.append(thumb);
7497
+ scroller.prepend(track);
7498
+ let size = 0;
7499
+ const update = () => {
7500
+ const view = scroller.clientHeight;
7501
+ const over = scroller.scrollHeight - view;
7502
+ const shown = over > 1 && view > 0;
7503
+ track.hidden = !shown;
7504
+ if (!shown)
7505
+ return;
7506
+ size = Math.max(20, view * view / (view + over));
7507
+ const y = Math.min(over, Math.max(0, scroller.scrollTop)) / over * (view - size);
7508
+ thumb.style.height = `${size.toFixed(1)}px`;
7509
+ thumb.style.transform = `translate3d(0,${y.toFixed(1)}px,0)`;
7510
+ };
7511
+ let idle = 0;
7512
+ const onScroll = () => {
7513
+ update();
7514
+ track.dataset.active = "";
7515
+ win.clearTimeout(idle);
7516
+ idle = win.setTimeout(() => delete track.dataset.active, 800);
7517
+ };
7518
+ scroller.addEventListener("scroll", onScroll, { passive: true });
7519
+ let drag;
7520
+ thumb.addEventListener("pointerdown", (e) => {
7521
+ if (e.button !== 0)
7522
+ return;
7523
+ e.preventDefault();
7524
+ e.stopPropagation();
7525
+ drag = { y: e.clientY, top: scroller.scrollTop, pointer: e.pointerId };
7526
+ thumb.setPointerCapture(e.pointerId);
7527
+ track.dataset.dragging = "";
7528
+ });
7529
+ thumb.addEventListener("pointermove", (e) => {
7530
+ if (!drag || e.pointerId !== drag.pointer)
7531
+ return;
7532
+ const view = scroller.clientHeight;
7533
+ const over = scroller.scrollHeight - view;
7534
+ scroller.scrollTop = drag.top + (e.clientY - drag.y) * over / Math.max(1, view - size);
7535
+ });
7536
+ const end = (e) => {
7537
+ if (!drag || e.pointerId !== drag.pointer)
7538
+ return;
7539
+ drag = undefined;
7540
+ delete track.dataset.dragging;
7541
+ };
7542
+ thumb.addEventListener("pointerup", end);
7543
+ thumb.addEventListener("pointercancel", end);
7544
+ thumb.addEventListener("lostpointercapture", end);
7545
+ const ro = new ResizeObserver(update);
7546
+ ro.observe(scroller);
7547
+ ro.observe(content);
7548
+ update();
7549
+ return {
7550
+ update,
7551
+ dispose() {
7552
+ ro.disconnect();
7553
+ win.clearTimeout(idle);
7554
+ scroller.removeEventListener("scroll", onScroll);
7555
+ track.remove();
7556
+ }
7557
+ };
7558
+ }
7559
+
7449
7560
  // src/ui/tree.ts
7450
- var MOVE = springMotion({ duration: 0.34, bounce: 0.02 });
7561
+ var MOVE = springMotion({ duration: 0.3, bounce: 0 });
7451
7562
  var EDGE = 28;
7452
7563
  function tree(container, options) {
7453
7564
  const doc = container.ownerDocument;
@@ -7486,6 +7597,7 @@ function tree(container, options) {
7486
7597
  empty.hidden = true;
7487
7598
  root.append(space, line, empty);
7488
7599
  container.append(root);
7600
+ const scrollbar = overlayScrollbar(root, space);
7489
7601
  let h = 26;
7490
7602
  let indent = 14;
7491
7603
  let inset = 8;
@@ -8335,6 +8447,7 @@ function tree(container, options) {
8335
8447
  loop.stop();
8336
8448
  autoScroll.stop();
8337
8449
  ro.disconnect();
8450
+ scrollbar.dispose();
8338
8451
  dragging?.ghost.remove();
8339
8452
  root.remove();
8340
8453
  }
@@ -8680,11 +8793,7 @@ var SLIDE_BY = 0.12;
8680
8793
  var EXIT_BLUR = 3;
8681
8794
  var px = (x) => new Spring(x, LAYOUT.omega, LAYOUT.zeta, 0.05);
8682
8795
  var unit = (x) => new Spring(x, ENTER.omega, ENTER.zeta, 0.0005);
8683
- var drive = (s, m, target) => {
8684
- s.omega = m.omega;
8685
- s.zeta = m.zeta;
8686
- return s.set(target);
8687
- };
8796
+ var drive = (s, m, target) => tune(s, m).set(target);
8688
8797
  var probed = new Map;
8689
8798
  var smallest = (srcset) => {
8690
8799
  let best;
@@ -9445,7 +9554,7 @@ function bento(container, options) {
9445
9554
  }
9446
9555
  zoomIn(t.id);
9447
9556
  };
9448
- const typing = (target) => target instanceof HTMLElement && target !== root && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName));
9557
+ const typing = (target) => target !== root && isTypingTarget(target);
9449
9558
  const reveal = (id) => {
9450
9559
  const r = rectOf(id);
9451
9560
  if (!r)
@@ -10512,7 +10621,7 @@ var ACTIVITY_ICONS = {
10512
10621
  shipped: svg('<circle cx="8" cy="8" r="5.8"/><path d="m5.6 8.2 1.7 1.7 3.2-3.6"/>')
10513
10622
  };
10514
10623
  var PLAY2 = '<svg viewBox="0 0 8 8" aria-hidden="true"><path d="M1.5 1v6l5.2-3Z" fill="currentColor"/></svg>';
10515
- var REFLOW = springMotion({ duration: 0.4, bounce: 0 });
10624
+ var REFLOW = springMotion({ duration: 0.3, bounce: 0 });
10516
10625
  var OPEN = springMotion({ duration: 0.22, bounce: 0 });
10517
10626
  var CLOSE = springMotion({ duration: 0.18, bounce: 0 });
10518
10627
  var BLOOM = springMotion({ duration: 0.3, bounce: 0 });
@@ -10555,6 +10664,7 @@ function activity(container, options) {
10555
10664
  space.append(hl, line);
10556
10665
  root.append(empty, space);
10557
10666
  container.append(root);
10667
+ const scrollbar = overlayScrollbar(root, space);
10558
10668
  let heights = [32, 26];
10559
10669
  const measure = () => {
10560
10670
  syncFrame(root);
@@ -11178,6 +11288,7 @@ function activity(container, options) {
11178
11288
  destroy() {
11179
11289
  loop.stop();
11180
11290
  ro.disconnect();
11291
+ scrollbar.dispose();
11181
11292
  if (keysFrom === "page") {
11182
11293
  win.removeEventListener("keydown", onKey);
11183
11294
  win.removeEventListener("pointerdown", onPointer, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/knobs",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Per-component tuning controls. Explore in development, ship constants.",
5
5
  "type": "module",
6
6
  "license": "MIT",