tweakit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/AGENTS.md +5 -5
  2. package/README.md +3 -3
  3. package/dist/tweaks/auto.js +1 -1
  4. package/dist/tweaks/auto.js.map +1 -1
  5. package/dist/tweaks/bezier-S2BOXQBM.js +2 -0
  6. package/dist/tweaks/bezier-S2BOXQBM.js.map +6 -0
  7. package/dist/tweaks/chunk-3MZAAMC2.js +2 -0
  8. package/dist/tweaks/chunk-3MZAAMC2.js.map +6 -0
  9. package/dist/tweaks/chunk-ETB35KFV.js +2 -0
  10. package/dist/tweaks/chunk-ETB35KFV.js.map +6 -0
  11. package/dist/tweaks/chunk-YB5GKWZI.js +2 -0
  12. package/dist/tweaks/chunk-YB5GKWZI.js.map +6 -0
  13. package/dist/tweaks/clip-A3VI53BK.js +2 -0
  14. package/dist/tweaks/clip-A3VI53BK.js.map +6 -0
  15. package/dist/tweaks/color-UGYMAF3W.js +2 -0
  16. package/dist/tweaks/core.js +1 -1
  17. package/dist/tweaks/core.js.map +3 -3
  18. package/dist/tweaks/fold-GY47HIQA.js +2 -0
  19. package/dist/tweaks/fold-GY47HIQA.js.map +6 -0
  20. package/dist/tweaks/gradient-BTX25WSH.js +2 -0
  21. package/dist/tweaks/gradient-BTX25WSH.js.map +6 -0
  22. package/dist/tweaks/{heavy-CLD7UAVI.js → heavy-NPFIGHRM.js} +2 -2
  23. package/dist/tweaks/image-HTQQ4WX6.js +4 -0
  24. package/dist/tweaks/image-HTQQ4WX6.js.map +6 -0
  25. package/dist/tweaks/{image-store-WRKY2NPU.js → image-store-QD5SMZXG.js} +1 -1
  26. package/dist/tweaks/image-store-QD5SMZXG.js.map +6 -0
  27. package/dist/tweaks/interval-BHPI4LRN.js +2 -0
  28. package/dist/tweaks/interval-BHPI4LRN.js.map +6 -0
  29. package/dist/tweaks/markup-QM53TQJZ.js +2 -0
  30. package/dist/tweaks/markup-QM53TQJZ.js.map +6 -0
  31. package/dist/tweaks/monitor-722WIYGW.js +3 -0
  32. package/dist/tweaks/monitor-722WIYGW.js.map +6 -0
  33. package/dist/tweaks/motion-HMCZK6HJ.js +2 -0
  34. package/dist/tweaks/motion-HMCZK6HJ.js.map +6 -0
  35. package/dist/tweaks/plot-D3NQ6XNN.js +3 -0
  36. package/dist/tweaks/plot-D3NQ6XNN.js.map +6 -0
  37. package/dist/tweaks/point-XDXKMDXH.js +2 -0
  38. package/dist/tweaks/point-XDXKMDXH.js.map +6 -0
  39. package/dist/tweaks/presets-EUWNSCUW.js +2 -0
  40. package/dist/tweaks/presets-EUWNSCUW.js.map +6 -0
  41. package/dist/tweaks/shadow-AM7VSRNS.js +2 -0
  42. package/dist/tweaks/shadow-AM7VSRNS.js.map +6 -0
  43. package/dist/tweaks/shadow-root-RYUV3AGS.js +3 -0
  44. package/dist/tweaks/shadow-root-RYUV3AGS.js.map +6 -0
  45. package/dist/tweaks/sides-JWIURDA4.js +2 -0
  46. package/dist/tweaks/sides-JWIURDA4.js.map +6 -0
  47. package/dist/tweaks/spring-BF5GBTPM.js +2 -0
  48. package/dist/tweaks/spring-BF5GBTPM.js.map +6 -0
  49. package/dist/tweaks/tabs-PRVUUZXZ.js +2 -0
  50. package/dist/tweaks/tabs-PRVUUZXZ.js.map +6 -0
  51. package/dist/tweaks.css +1 -1
  52. package/dist/tweaks.js +5 -4
  53. package/dist/tweaks.js.map +3 -3
  54. package/dist/types/tweaks/enhance.d.ts +3 -2
  55. package/dist/types/tweaks/types.d.ts +278 -72
  56. package/package.json +5 -2
  57. package/src/tweaks/auto.ts +9 -5
  58. package/src/tweaks/clip.ts +84 -22
  59. package/src/tweaks/controls/basic.ts +463 -139
  60. package/src/tweaks/controls/bezier.ts +152 -47
  61. package/src/tweaks/controls/color.ts +610 -161
  62. package/src/tweaks/controls/gradient.ts +256 -77
  63. package/src/tweaks/controls/image.ts +388 -132
  64. package/src/tweaks/controls/interval.ts +126 -41
  65. package/src/tweaks/controls/monitor.ts +142 -36
  66. package/src/tweaks/controls/motion.ts +231 -56
  67. package/src/tweaks/controls/plot.ts +321 -101
  68. package/src/tweaks/controls/point.ts +134 -45
  69. package/src/tweaks/controls/shadow.ts +103 -30
  70. package/src/tweaks/controls/sides.ts +150 -40
  71. package/src/tweaks/controls/spring.ts +220 -60
  72. package/src/tweaks/controls/tabs.ts +39 -18
  73. package/src/tweaks/core.ts +1 -1
  74. package/src/tweaks/easing.ts +45 -20
  75. package/src/tweaks/enhance.ts +21 -6
  76. package/src/tweaks/feedback.ts +133 -34
  77. package/src/tweaks/fold.ts +22 -10
  78. package/src/tweaks/heavy.ts +461 -137
  79. package/src/tweaks/icons.ts +12 -5
  80. package/src/tweaks/image-store.ts +131 -43
  81. package/src/tweaks/lazy.ts +36 -19
  82. package/src/tweaks/markup.ts +255 -92
  83. package/src/tweaks/panel.ts +1262 -438
  84. package/src/tweaks/presets.ts +85 -0
  85. package/src/tweaks/schema.ts +213 -51
  86. package/src/tweaks/shadow-root.ts +178 -0
  87. package/src/tweaks/shared.ts +802 -190
  88. package/src/tweaks/standalone.ts +120 -30
  89. package/src/tweaks/types.ts +644 -143
  90. package/src/tweaks/undo.ts +104 -0
  91. package/src/tweaks.css +556 -550
  92. package/src/wide-gamut.ts +473 -125
  93. package/dist/tweaks/bezier-JFEZMCLP.js +0 -2
  94. package/dist/tweaks/bezier-JFEZMCLP.js.map +0 -6
  95. package/dist/tweaks/chunk-6UO4DHBN.js +0 -2
  96. package/dist/tweaks/chunk-6UO4DHBN.js.map +0 -6
  97. package/dist/tweaks/chunk-DIQMLBAS.js +0 -2
  98. package/dist/tweaks/chunk-DIQMLBAS.js.map +0 -6
  99. package/dist/tweaks/chunk-XE6OUJBG.js +0 -2
  100. package/dist/tweaks/chunk-XE6OUJBG.js.map +0 -6
  101. package/dist/tweaks/clip-WW7JYO4X.js +0 -2
  102. package/dist/tweaks/clip-WW7JYO4X.js.map +0 -6
  103. package/dist/tweaks/color-YMMFURP6.js +0 -2
  104. package/dist/tweaks/fold-U6YRZLCJ.js +0 -2
  105. package/dist/tweaks/fold-U6YRZLCJ.js.map +0 -6
  106. package/dist/tweaks/gradient-JER7PW4D.js +0 -2
  107. package/dist/tweaks/gradient-JER7PW4D.js.map +0 -6
  108. package/dist/tweaks/image-H6QSLGO3.js +0 -4
  109. package/dist/tweaks/image-H6QSLGO3.js.map +0 -6
  110. package/dist/tweaks/image-store-WRKY2NPU.js.map +0 -6
  111. package/dist/tweaks/interval-TEUHQGZN.js +0 -2
  112. package/dist/tweaks/interval-TEUHQGZN.js.map +0 -6
  113. package/dist/tweaks/markup-NIBJD6E2.js +0 -2
  114. package/dist/tweaks/markup-NIBJD6E2.js.map +0 -6
  115. package/dist/tweaks/monitor-3BY7YCGX.js +0 -3
  116. package/dist/tweaks/monitor-3BY7YCGX.js.map +0 -6
  117. package/dist/tweaks/motion-FQPUBKKM.js +0 -2
  118. package/dist/tweaks/motion-FQPUBKKM.js.map +0 -6
  119. package/dist/tweaks/plot-2HBNBZSG.js +0 -3
  120. package/dist/tweaks/plot-2HBNBZSG.js.map +0 -6
  121. package/dist/tweaks/point-VV6XV3I6.js +0 -2
  122. package/dist/tweaks/point-VV6XV3I6.js.map +0 -6
  123. package/dist/tweaks/shadow-5E4E55I2.js +0 -2
  124. package/dist/tweaks/shadow-5E4E55I2.js.map +0 -6
  125. package/dist/tweaks/sides-P3STEVYY.js +0 -2
  126. package/dist/tweaks/sides-P3STEVYY.js.map +0 -6
  127. package/dist/tweaks/spring-HUVSXNVN.js +0 -2
  128. package/dist/tweaks/spring-HUVSXNVN.js.map +0 -6
  129. package/dist/tweaks/tabs-76FW7LQ7.js +0 -2
  130. package/dist/tweaks/tabs-76FW7LQ7.js.map +0 -6
  131. /package/dist/tweaks/{color-YMMFURP6.js.map → color-UGYMAF3W.js.map} +0 -0
  132. /package/dist/tweaks/{heavy-CLD7UAVI.js.map → heavy-NPFIGHRM.js.map} +0 -0
@@ -6,14 +6,21 @@
6
6
  // bright hotspot: each icon is one path. Copy and presets keep separate parts so the hover can
7
7
  // move them (tweaks.css), drawn to abut rather than overlap.
8
8
  import { icon } from "./shared.js";
9
- const ICON_COPY = icon('<path class="tw-copy-back" d="m5.5 16.5-.7-.2q-1.5-.6-2.1-2.1-.2-.7-.2-2.7v-2c0-3.3 0-5 1-6s2.7-1 6-1h2c1.4 0 2 0 2.7.2q1.6.5 2.1 2.1l.2.7"/><path class="tw-copy-front" d="M7.5 14.5c0-3.3 0-5 1-6s2.7-1 6-1 5 0 6 1 1 2.7 1 6 0 5-1 6-2.7 1-6 1-5 0-6-1-1-2.7-1-6"/>', "tw-toolbar-btn__copy");
10
- const ICON_CHECK = icon('<path d="m5 14 3.5 3.5L19 6.5"/>', "tw-toolbar-btn__check", 2.4);
9
+ const ICON_COPY = icon(
10
+ '<path class="twk-copy-back" d="m5.5 16.5-.7-.2q-1.5-.6-2.1-2.1-.2-.7-.2-2.7v-2c0-3.3 0-5 1-6s2.7-1 6-1h2c1.4 0 2 0 2.7.2q1.6.5 2.1 2.1l.2.7"/><path class="twk-copy-front" d="M7.5 14.5c0-3.3 0-5 1-6s2.7-1 6-1 5 0 6 1 1 2.7 1 6 0 5-1 6-2.7 1-6 1-5 0-6-1-1-2.7-1-6"/>',
11
+ "twk-toolbar-btn__copy",
12
+ );
13
+ const ICON_CHECK = icon('<path d="m5 14 3.5 3.5L19 6.5"/>', "twk-toolbar-btn__check", 2.4);
11
14
  // Reset is Hugeicons' undo, a near-full circle, so the spin on click reads as a turn about the center.
12
15
  const ICON_RESET = icon('<path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-7.6 4.2M3.3 3l.2 2q0 2.2.6 2.7t2.7.2l2-.2"/>');
13
16
  const chevronIcon = (cls: string) => icon('<path d="M18 9s-4.4 6-6 6-6-6-6-6"/>', cls, 2.5);
14
- const ICON_CHEVRON = chevronIcon("tw-chevron");
15
- const ICON_PRESETS = icon('<path d="M20.2 13.5q1.8.8 1.8 1.5 0 .8-2.7 2l-5 2.2Q13 20 12 20q-.7 0-2.4-.8l-4.9-2.3Q2 16 2 15t1.8-1.5"/><path class="tw-layer-top" d="M9.6 4.8Q11.3 4 12 4t2.4.8l4.9 2.3Q22 8 22 9c0 .7-.9 1.1-2.7 2l-5 2.2Q13 14 12 14q-.7 0-2.4-.8l-4.9-2.3Q2 10 2 9c0-.7.9-1.1 2.7-2z"/>');
17
+ const ICON_CHEVRON = chevronIcon("twk-chevron");
18
+ const ICON_PRESETS = icon(
19
+ '<path d="M20.2 13.5q1.8.8 1.8 1.5 0 .8-2.7 2l-5 2.2Q13 20 12 20q-.7 0-2.4-.8l-4.9-2.3Q2 16 2 15t1.8-1.5"/><path class="twk-layer-top" d="M9.6 4.8Q11.3 4 12 4t2.4.8l4.9 2.3Q22 8 22 9c0 .7-.9 1.1-2.7 2l-5 2.2Q13 14 12 14q-.7 0-2.4-.8l-4.9-2.3Q2 10 2 9c0-.7.9-1.1 2.7-2z"/>',
20
+ );
16
21
  const ICON_X = icon('<path d="M18 6 6 18m12 0L6 6"/>', "", 2.2);
17
- const ICON_INFO = icon('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4m-.25-3.75a.25.25 0 0 0 .5 0 .25.25 0 0 0-.5 0"/>');
22
+ const ICON_INFO = icon(
23
+ '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4m-.25-3.75a.25.25 0 0 0 .5 0 .25.25 0 0 0-.5 0"/>',
24
+ );
18
25
 
19
26
  export { ICON_COPY, ICON_CHECK, ICON_RESET, ICON_CHEVRON, ICON_PRESETS, ICON_X, ICON_INFO, chevronIcon };
@@ -6,34 +6,75 @@
6
6
  const REF = "tw-image:";
7
7
  type Store = IDBObjectStore;
8
8
  let db: Promise<IDBDatabase | null> | null = null;
9
- // An open that never settles (another tab's pending delete or upgrade) reads as unavailable after 3s rather than holding
10
- // panel.ready forever. The connection closes for another tab's upgrade or delete, and one the browser drops is reopened.
11
- const open = () => db ||= new Promise<IDBDatabase | null>((done) => {
12
- let late = false;
13
- const t = setTimeout(() => { late = true; db = null; done(null); }, 3000);
14
- try {
15
- const r = indexedDB.open("tweakit", 1);
16
- r.onupgradeneeded = () => r.result.createObjectStore("images");
17
- r.onsuccess = () => {
18
- const d = r.result; clearTimeout(t);
19
- if (late) { d.close(); return; }
20
- d.onversionchange = () => { d.close(); db = null; }; d.onclose = () => { db = null; };
21
- done(d);
22
- };
23
- r.onerror = () => { clearTimeout(t); db = null; done(null); };
24
- } catch { clearTimeout(t); done(null); } // no indexedDB at all
25
- });
9
+ // An open that never settles (another tab's pending delete or upgrade) reads as unavailable after 3s rather than
10
+ // holding panel.ready forever. The connection closes for another tab's upgrade or delete, and one the browser drops is
11
+ // reopened.
12
+ const open = () =>
13
+ (db ||= new Promise<IDBDatabase | null>((done) => {
14
+ let late = false;
15
+ const t = setTimeout(() => {
16
+ late = true;
17
+ db = null;
18
+ done(null);
19
+ }, 3000);
20
+ // no indexedDB at all
21
+ try {
22
+ const r = indexedDB.open("tweakit", 1);
23
+ r.onupgradeneeded = () => r.result.createObjectStore("images");
24
+ r.onsuccess = () => {
25
+ const d = r.result;
26
+ clearTimeout(t);
27
+ if (late) {
28
+ d.close();
29
+ return;
30
+ }
31
+ d.onversionchange = () => {
32
+ d.close();
33
+ db = null;
34
+ };
35
+ d.onclose = () => {
36
+ db = null;
37
+ };
38
+ done(d);
39
+ };
40
+ r.onerror = () => {
41
+ clearTimeout(t);
42
+ db = null;
43
+ done(null);
44
+ };
45
+ } catch {
46
+ clearTimeout(t);
47
+ done(null);
48
+ }
49
+ }));
26
50
  // One transaction; resolves with what `fn` returns once it commits, null if it fails.
27
- const run = <T>(mode: IDBTransactionMode, fn: (s: Store) => T) => open().then((d) => d && new Promise<T | null>((done) => {
28
- try { const t = d.transaction("images", mode), out = fn(t.objectStore("images")); t.oncomplete = () => done(out); t.onerror = t.onabort = () => done(null); }
29
- catch { done(null); }
30
- }));
31
- const range = (key: string) => IDBKeyRange.bound([key], [key, []]); // an array sorts after every string, so [key, []] caps the range
51
+ const run = <T>(mode: IDBTransactionMode, fn: (s: Store) => T) =>
52
+ open().then(
53
+ (d) =>
54
+ d &&
55
+ new Promise<T | null>((done) => {
56
+ try {
57
+ const t = d.transaction("images", mode),
58
+ out = fn(t.objectStore("images"));
59
+ t.oncomplete = () => done(out);
60
+ t.onerror = t.onabort = () => done(null);
61
+ } catch {
62
+ done(null);
63
+ }
64
+ }),
65
+ );
66
+ // an array sorts after every string, so [key, []] caps the range
67
+ const range = (key: string) => IDBKeyRange.bound([key], [key, []]);
32
68
 
33
69
  // cyrb53: a 53-bit string hash, plenty to tell one panel's few images apart.
34
70
  const hash = (s: string) => {
35
- let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
36
- for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); h1 = Math.imul(h1 ^ c, 2654435761); h2 = Math.imul(h2 ^ c, 1597334677); }
71
+ let h1 = 0xdeadbeef,
72
+ h2 = 0x41c6ce57;
73
+ for (let i = 0; i < s.length; i++) {
74
+ const c = s.charCodeAt(i);
75
+ h1 = Math.imul(h1 ^ c, 2654435761);
76
+ h2 = Math.imul(h2 ^ c, 1597334677);
77
+ }
37
78
  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
38
79
  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
39
80
  return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36) + s.length.toString(36);
@@ -42,22 +83,42 @@ const hash = (s: string) => {
42
83
  const refFor = (dataUrl: string) => REF + hash(dataUrl);
43
84
 
44
85
  /** Every image stored under `key`, ref → data URL; null when IndexedDB can't be used. */
45
- const load = (key: string) => run("readonly", (s) => {
46
- const out = new Map<string, string>();
47
- s.openCursor(range(key)).onsuccess = function () { const c = this.result; if (!c) return; if (typeof c.value === "string") out.set(REF + (c.key as string[])[1], c.value); c.continue(); };
48
- return out;
49
- });
86
+ const load = (key: string) =>
87
+ run("readonly", (s) => {
88
+ const out = new Map<string, string>();
89
+ s.openCursor(range(key)).onsuccess = function () {
90
+ const c = this.result;
91
+ if (!c) return;
92
+ if (typeof c.value === "string") out.set(REF + (c.key as string[])[1], c.value);
93
+ c.continue();
94
+ };
95
+ return out;
96
+ });
50
97
  /** One image by its ref (a preset another tab saved after this panel loaded); null when it isn't there. */
51
- const get = (key: string, ref: string) => run("readonly", (s) => { const r = s.get([key, ref.slice(REF.length)]); return r; }).then((r) => (r && typeof r.result === "string" ? r.result : null));
52
- const put = (key: string, ref: string, dataUrl: string) => run("readwrite", (s) => { s.put(dataUrl, [key, ref.slice(REF.length)]); return true; });
98
+ const get = (key: string, ref: string) =>
99
+ run("readonly", (s) => {
100
+ const r = s.get([key, ref.slice(REF.length)]);
101
+ return r;
102
+ }).then((r) => (r && typeof r.result === "string" ? r.result : null));
103
+ const put = (key: string, ref: string, dataUrl: string) =>
104
+ run("readwrite", (s) => {
105
+ s.put(dataUrl, [key, ref.slice(REF.length)]);
106
+ return true;
107
+ });
53
108
  /** Delete the images under `key` that `refs` name. */
54
- const prune = (key: string, refs: string[]) => run("readwrite", (s) => { for (const r of refs) s.delete([key, r.slice(REF.length)]); return true; });
109
+ const prune = (key: string, refs: string[]) =>
110
+ run("readwrite", (s) => {
111
+ for (const r of refs) s.delete([key, r.slice(REF.length)]);
112
+ return true;
113
+ });
55
114
 
56
- /** Each ref a stored snapshot or preset names, read from its JSON whatever the schema: a renamed field's preset still counts. */
115
+ /** Each ref a stored snapshot or preset names, read from its JSON whatever the schema: a renamed field's preset still
116
+ * counts. */
57
117
  const refsIn = (v: unknown) => new Set<string>(JSON.stringify(v ?? null).match(/tw-image:[0-9a-z]+/g) || []);
58
118
  /** Delete the records under `key` this panel knows (`known`) that nothing in `named` names any more. */
59
119
  const pruneUnnamed = (key: string, known: Map<string, string>, named: unknown[]) => {
60
- const keep = refsIn(named), gone = [...known.keys()].filter((r) => !keep.has(r));
120
+ const keep = refsIn(named),
121
+ gone = [...known.keys()].filter((r) => !keep.has(r));
61
122
  for (const r of gone) known.delete(r);
62
123
  if (gone.length) prune(key, gone);
63
124
  };
@@ -67,13 +128,29 @@ const swapRef = (presets: unknown, ref: string, withText: string) => {
67
128
  return text.includes(ref) ? JSON.parse(text.split(ref).join(JSON.stringify(withText).slice(1, -1))) : null;
68
129
  };
69
130
 
70
- /** The store bound to one panel: `known` holds the records it has loaded or written (ref → data URL), `refused` runs when
71
- * a write fails with its ref (the panel saves again, that image inline), and the presets pair reads and writes its presets. */
72
- const bind = (key: string, known: Map<string, string>, refused: (ref: string) => void, readPresets: () => unknown, writePresets: (v: unknown) => boolean) => {
73
- const failed = new Set<string>(), hashed = new WeakMap<object, [string, string]>();
131
+ /** The store bound to one panel: `known` holds the records it has loaded or written (ref → data URL), `refused` runs
132
+ * when
133
+ * a write fails with its ref (the panel saves again, that image inline), and the presets pair reads and writes its
134
+ * presets. */
135
+ const bind = (
136
+ key: string,
137
+ known: Map<string, string>,
138
+ refused: (ref: string) => void,
139
+ readPresets: () => unknown,
140
+ writePresets: (v: unknown) => boolean,
141
+ ) => {
142
+ const failed = new Set<string>(),
143
+ hashed = new WeakMap<object, [string, string]>();
74
144
  // Each row's last data URL and its ref, so a save that didn't touch the image doesn't hash megabytes again.
75
- const refOf = (row: object, v: string) => { const c = hashed.get(row); if (c?.[0] === v) return c[1]; const r = refFor(v); hashed.set(row, [v, r]); return r; };
76
- // A record the store refused: a preset that names it takes the data URL inline, or an empty image if storage can't fit it.
145
+ const refOf = (row: object, v: string) => {
146
+ const c = hashed.get(row);
147
+ if (c?.[0] === v) return c[1];
148
+ const r = refFor(v);
149
+ hashed.set(row, [v, r]);
150
+ return r;
151
+ };
152
+ // A record the store refused: a preset that names it takes the data URL inline, or an empty image if storage can't
153
+ // fit it.
77
154
  const inlinePresets = (ref: string, v: string) => {
78
155
  const inlined = swapRef(readPresets(), ref, v);
79
156
  if (!inlined || writePresets(inlined)) return;
@@ -81,20 +158,31 @@ const bind = (key: string, known: Map<string, string>, refused: (ref: string) =>
81
158
  console.warn("[tweaks] persist: an image a preset holds didn't fit in storage — the preset keeps the rest");
82
159
  };
83
160
  return {
84
- /** The ref `row`'s image saves as, or null to save it inline (the store refused it). Written once, and again whenever
161
+ /** The ref `row`'s image saves as, or null to save it inline (the store refused it). Written once, and again
162
+ * whenever
85
163
  * `stored` (what storage holds now) no longer names it: a tab that replaced it may have pruned the record. */
86
164
  save(row: object, v: string, stored: unknown) {
87
165
  const ref = refOf(row, v);
88
166
  if (failed.has(ref)) return null;
89
167
  if (!known.has(ref) || !refsIn(stored).has(ref)) {
90
168
  known.set(ref, v);
91
- put(key, ref, v).then((ok) => { if (ok) return; known.delete(ref); failed.add(ref); refused(ref); inlinePresets(ref, v); });
169
+ put(key, ref, v).then((ok) => {
170
+ if (ok) return;
171
+ known.delete(ref);
172
+ failed.add(ref);
173
+ refused(ref);
174
+ inlinePresets(ref, v);
175
+ });
92
176
  }
93
177
  return ref;
94
178
  },
95
179
  prune: (named: unknown[]) => pruneUnnamed(key, known, named),
96
180
  /** A ref this panel hasn't seen (a preset another tab saved), read from the store. */
97
- fetch: (ref: string) => get(key, ref).then((url) => { if (url) known.set(ref, url); return url; }),
181
+ fetch: (ref: string) =>
182
+ get(key, ref).then((url) => {
183
+ if (url) known.set(ref, url);
184
+ return url;
185
+ }),
98
186
  };
99
187
  };
100
188
 
@@ -10,26 +10,43 @@ type MetaNode = Pick<Meta, "type" | "children" | "pages">;
10
10
  // TW_SPLIT is an esbuild `define`: `false` for the single-file bundle, where single.ts imports
11
11
  // every control statically, so this map is empty and esbuild drops the import()s.
12
12
  declare const TW_SPLIT: boolean;
13
- const LAZY_IMPORT: Record<string, () => Promise<unknown>> = TW_SPLIT ? {
14
- number: () => import("./heavy.js"), // heavy.js registers the Number control itself
15
- interval: () => import("./controls/interval.js"),
16
- color: () => import("./controls/color.js"),
17
- gradient: () => import("./controls/gradient.js"),
18
- shadow: () => import("./controls/shadow.js"),
19
- sides: () => import("./controls/sides.js"),
20
- tabs: () => import("./controls/tabs.js"),
21
- image: () => import("./controls/image.js"),
22
- fpsgraph: () => import("./controls/monitor.js"),
23
- monitor: () => import("./controls/monitor.js"),
24
- spring: () => import("./controls/spring.js"),
25
- cubicbezier: () => import("./controls/bezier.js"),
26
- // motion registers last, so a failed editor chunk leaves it unregistered for the next panel to retry.
27
- motion: () => Promise.all([import("./controls/bezier.js"), import("./controls/spring.js")]).then(() => import("./controls/motion.js")),
28
- point: () => import("./controls/point.js"),
29
- plot: () => import("./controls/plot.js"),
30
- } : {};
13
+ // null-prototype: "constructor" isn't a chunk
14
+ const LAZY_IMPORT: Record<string, () => Promise<unknown>> = Object.assign(
15
+ Object.create(null),
16
+ TW_SPLIT
17
+ ? {
18
+ number: () => import("./heavy.js"), // heavy.js registers the Number control itself
19
+ interval: () => import("./controls/interval.js"),
20
+ color: () => import("./controls/color.js"),
21
+ gradient: () => import("./controls/gradient.js"),
22
+ shadow: () => import("./controls/shadow.js"),
23
+ sides: () => import("./controls/sides.js"),
24
+ tabs: () => import("./controls/tabs.js"),
25
+ image: () => import("./controls/image.js"),
26
+ fpsgraph: () => import("./controls/monitor.js"),
27
+ monitor: () => import("./controls/monitor.js"),
28
+ spring: () => import("./controls/spring.js"),
29
+ cubicbezier: () => import("./controls/bezier.js"),
30
+ // motion registers last, so a failed editor chunk leaves it unregistered for the next panel to retry.
31
+ motion: () =>
32
+ Promise.all([import("./controls/bezier.js"), import("./controls/spring.js")]).then(
33
+ () => import("./controls/motion.js"),
34
+ ),
35
+ point: () => import("./controls/point.js"),
36
+ plot: () => import("./controls/plot.js"),
37
+ }
38
+ : {},
39
+ );
31
40
  const loading: Record<string, Promise<unknown>> = {};
32
- const ensure = (type: string) => (getControl(type) || !LAZY_IMPORT[type]) ? null : (loading[type] ||= LAZY_IMPORT[type]().catch((e) => { delete loading[type]; console.error(`[tweaks] control chunk "${type}" failed to load:`, e); throw e; })); // a rejection isn't cached, so a later panel retries
41
+ // a rejection isn't cached, so a later panel retries
42
+ const ensure = (type: string) =>
43
+ getControl(type) || !LAZY_IMPORT[type]
44
+ ? null
45
+ : (loading[type] ||= LAZY_IMPORT[type]().catch((e) => {
46
+ delete loading[type];
47
+ console.error(`[tweaks] control chunk "${type}" failed to load:`, e);
48
+ throw e;
49
+ }));
33
50
  const scanTypes = (metas: MetaNode[], set = new Set<string>()) => {
34
51
  for (const m of metas) {
35
52
  set.add(m.type);