@rogieking/figui3 6.26.0 → 6.27.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.
package/fig-lab.js CHANGED
@@ -32,6 +32,125 @@ function figLabColorEventAliases(color, alpha, opacity) {
32
32
  return { color, alpha: normalizedAlpha, opacity: normalizedOpacity };
33
33
  }
34
34
 
35
+ const figLabPropskitResetMenus = new WeakMap();
36
+
37
+ function figLabConnectPropskitResetMenu(host) {
38
+ let state = figLabPropskitResetMenus.get(host);
39
+ if (!state) {
40
+ const menu = document.createElement("fig-menu");
41
+ menu.setAttribute("position", "bottom left");
42
+ menu.setAttribute("offset", "0 0");
43
+ const resetItem = document.createElement("fig-menu-item");
44
+ resetItem.setAttribute("value", "reset-default");
45
+ resetItem.textContent = "Reset";
46
+ menu.appendChild(resetItem);
47
+
48
+ let fallbackTimer = 0;
49
+ const clearPendingOpen = () => {
50
+ if (fallbackTimer) {
51
+ clearTimeout(fallbackTimer);
52
+ fallbackTimer = 0;
53
+ }
54
+ window.removeEventListener("pointerup", openMenu, true);
55
+ window.removeEventListener("pointercancel", openMenu, true);
56
+ };
57
+ const openMenu = () => {
58
+ clearPendingOpen();
59
+ const point = state?.point;
60
+ if (point) menu.showAt?.(point.x, point.y);
61
+ };
62
+ const handleContextMenu = (event) => {
63
+ if (
64
+ figLabBooleanAttribute(host, "disabled") ||
65
+ event.target?.closest?.("fig-menu")
66
+ ) {
67
+ return;
68
+ }
69
+ event.preventDefault();
70
+ event.stopPropagation();
71
+ event.stopImmediatePropagation?.();
72
+ state.point = { x: event.clientX, y: event.clientY };
73
+ clearPendingOpen();
74
+ window.addEventListener("pointerup", openMenu, {
75
+ once: true,
76
+ capture: true,
77
+ });
78
+ window.addEventListener("pointercancel", openMenu, {
79
+ once: true,
80
+ capture: true,
81
+ });
82
+ fallbackTimer = window.setTimeout(openMenu, 180);
83
+ };
84
+ const handleChange = (event) => {
85
+ event.stopPropagation();
86
+ if (event.detail?.value === "reset-default") host.resetToDefault?.();
87
+ };
88
+ state = {
89
+ menu,
90
+ point: null,
91
+ clearPendingOpen,
92
+ handleContextMenu,
93
+ handleChange,
94
+ };
95
+ figLabPropskitResetMenus.set(host, state);
96
+ }
97
+
98
+ if (state.menu.parentElement !== host) host.appendChild(state.menu);
99
+ host.removeEventListener("contextmenu", state.handleContextMenu);
100
+ host.addEventListener("contextmenu", state.handleContextMenu);
101
+ state.menu.removeEventListener("change", state.handleChange);
102
+ state.menu.addEventListener("change", state.handleChange);
103
+ }
104
+
105
+ function figLabDisconnectPropskitResetMenu(host) {
106
+ const state = figLabPropskitResetMenus.get(host);
107
+ if (!state) return;
108
+ state.clearPendingOpen();
109
+ host.removeEventListener("contextmenu", state.handleContextMenu);
110
+ state.menu.removeEventListener("change", state.handleChange);
111
+ }
112
+
113
+ function figLabEmitPropskitReset(host, detail) {
114
+ for (const type of ["input", "change"]) {
115
+ host.dispatchEvent(
116
+ new CustomEvent(type, {
117
+ detail,
118
+ bubbles: true,
119
+ cancelable: true,
120
+ composed: true,
121
+ }),
122
+ );
123
+ }
124
+ }
125
+
126
+ function figLabPropskitValuesEqual(value, defaultValue) {
127
+ if (typeof value === "boolean" || typeof defaultValue === "boolean") {
128
+ return Boolean(value) === Boolean(defaultValue);
129
+ }
130
+ return String(value ?? "") === String(defaultValue ?? "");
131
+ }
132
+
133
+ function figLabPerceivedColorTheme(context, color) {
134
+ if (!context || !color) return null;
135
+ const probe = document.createElement("span");
136
+ probe.style.cssText =
137
+ "position:absolute;visibility:hidden;pointer-events:none;color:" + color;
138
+ context.appendChild(probe);
139
+ const resolved = getComputedStyle(probe).color;
140
+ probe.remove();
141
+ if (!resolved.startsWith("rgb")) return null;
142
+ const channels = resolved.match(/[\d.]+/g)?.slice(0, 3).map(Number);
143
+ if (!channels || channels.length < 3 || channels.some(Number.isNaN)) return null;
144
+ const [r, g, b] = channels.map((channel) => {
145
+ const value = channel / 255;
146
+ return value <= 0.04045
147
+ ? value / 12.92
148
+ : ((value + 0.055) / 1.055) ** 2.4;
149
+ });
150
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
151
+ return luminance <= 0.179 ? "light" : "dark";
152
+ }
153
+
35
154
  /* Unique IDs for lab components such as propskit-group. */
36
155
  let figLabUniqueIdCounter = 0;
37
156
  function figLabUniqueId(prefix = "fig-lab") {
@@ -58,6 +177,7 @@ class PropskitSwitch extends HTMLElement {
58
177
  #boundHandleInput = null;
59
178
  #boundHandleChange = null;
60
179
  #boundHandleClick = this.#handleClick.bind(this);
180
+ #initialChecked = false;
61
181
 
62
182
  static get observedAttributes() {
63
183
  return ["label", "direction"];
@@ -70,6 +190,7 @@ class PropskitSwitch extends HTMLElement {
70
190
  this.#bindSwitchEvents();
71
191
  this.removeEventListener("click", this.#boundHandleClick);
72
192
  this.addEventListener("click", this.#boundHandleClick);
193
+ figLabConnectPropskitResetMenu(this);
73
194
 
74
195
  if (!this.#observer) {
75
196
  this.#observer = new MutationObserver((mutations) => {
@@ -100,6 +221,7 @@ class PropskitSwitch extends HTMLElement {
100
221
  this.#observer?.disconnect();
101
222
  this.#unbindSwitchEvents();
102
223
  this.removeEventListener("click", this.#boundHandleClick);
224
+ figLabDisconnectPropskitResetMenu(this);
103
225
  }
104
226
 
105
227
  attributeChangedCallback(name, oldValue, newValue) {
@@ -108,6 +230,7 @@ class PropskitSwitch extends HTMLElement {
108
230
  }
109
231
 
110
232
  #initialize() {
233
+ this.#initialChecked = figLabBooleanAttribute(this, "checked");
111
234
  const initialChildren = Array.from(this.childNodes).filter(
112
235
  (node) =>
113
236
  node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
@@ -171,6 +294,7 @@ class PropskitSwitch extends HTMLElement {
171
294
  "id",
172
295
  "checked",
173
296
  "value",
297
+ "default",
174
298
  ]);
175
299
  return this.getAttributeNames().filter(
176
300
  (name) => !reserved.has(name) && !name.startsWith("data-"),
@@ -233,7 +357,10 @@ class PropskitSwitch extends HTMLElement {
233
357
  }
234
358
 
235
359
  #handleClick(event) {
236
- if (event.target instanceof Element && event.target.closest("fig-segmented-control")) {
360
+ if (
361
+ event.target instanceof Element &&
362
+ event.target.closest("fig-segmented-control, fig-menu")
363
+ ) {
237
364
  return;
238
365
  }
239
366
  const value = this.#switch?.value === "on" ? "off" : "on";
@@ -260,6 +387,25 @@ class PropskitSwitch extends HTMLElement {
260
387
  this.setAttribute("value", nextValue ?? "");
261
388
  }
262
389
 
390
+ get defaultValue() {
391
+ return this.hasAttribute("default")
392
+ ? figLabBooleanAttribute(this, "default")
393
+ : this.#initialChecked;
394
+ }
395
+
396
+ get isDefault() {
397
+ return this.checked === this.defaultValue;
398
+ }
399
+
400
+ resetToDefault() {
401
+ const checked = this.defaultValue;
402
+ this.checked = checked;
403
+ figLabEmitPropskitReset(this, {
404
+ checked,
405
+ value: this.getAttribute("value") ?? "",
406
+ });
407
+ }
408
+
263
409
  focus(options) {
264
410
  const selected =
265
411
  this.#switch?.querySelector("fig-segment[selected]") ||
@@ -297,6 +443,7 @@ class PropskitColor extends HTMLElement {
297
443
  }
298
444
  this.removeEventListener("click", this.#boundHandleClick);
299
445
  this.addEventListener("click", this.#boundHandleClick);
446
+ figLabConnectPropskitResetMenu(this);
300
447
 
301
448
  if (!this.#observer) {
302
449
  this.#observer = new MutationObserver((mutations) => {
@@ -328,6 +475,7 @@ class PropskitColor extends HTMLElement {
328
475
  this.#observer?.disconnect();
329
476
  this.#unbindInputEvents();
330
477
  this.removeEventListener("click", this.#boundHandleClick);
478
+ figLabDisconnectPropskitResetMenu(this);
331
479
  }
332
480
 
333
481
  attributeChangedCallback(name, oldValue, newValue) {
@@ -491,7 +639,10 @@ class PropskitColor extends HTMLElement {
491
639
  }
492
640
 
493
641
  #handleClick(event) {
494
- if (event.target instanceof Element && event.target.closest("fig-input-color")) {
642
+ if (
643
+ event.target instanceof Element &&
644
+ event.target.closest("fig-input-color, fig-menu")
645
+ ) {
495
646
  return;
496
647
  }
497
648
  this.focus();
@@ -527,8 +678,16 @@ class PropskitColor extends HTMLElement {
527
678
  return this.getAttribute("default") ?? this.#initialValue ?? "";
528
679
  }
529
680
 
681
+ get defaultValue() {
682
+ return String(this.#defaultValue());
683
+ }
684
+
685
+ get isDefault() {
686
+ return figLabPropskitValuesEqual(this.value, this.defaultValue);
687
+ }
688
+
530
689
  resetToDefault() {
531
- const next = String(this.#defaultValue());
690
+ const next = this.defaultValue;
532
691
  this.setAttribute("value", next);
533
692
  this.#forceInputValue(next);
534
693
  this.dispatchEvent(
@@ -569,6 +728,7 @@ class PropskitSelect extends HTMLElement {
569
728
  #boundHandlePointerDown = this.#handlePointerDown.bind(this);
570
729
  /** True when pointerdown saw the menu open — skip click-to-open after light-dismiss. */
571
730
  #closeGesture = false;
731
+ #initialValue = null;
572
732
 
573
733
  static get observedAttributes() {
574
734
  return ["label", "direction", "aria-label", "options", "value"];
@@ -583,6 +743,7 @@ class PropskitSelect extends HTMLElement {
583
743
  this.addEventListener("click", this.#boundHandleClick);
584
744
  this.removeEventListener("pointerdown", this.#boundHandlePointerDown, true);
585
745
  this.addEventListener("pointerdown", this.#boundHandlePointerDown, true);
746
+ figLabConnectPropskitResetMenu(this);
586
747
 
587
748
  if (!this.#observer) {
588
749
  this.#observer = new MutationObserver((mutations) => {
@@ -616,6 +777,7 @@ class PropskitSelect extends HTMLElement {
616
777
  this.#unbindSelectEvents();
617
778
  this.removeEventListener("click", this.#boundHandleClick);
618
779
  this.removeEventListener("pointerdown", this.#boundHandlePointerDown, true);
780
+ figLabDisconnectPropskitResetMenu(this);
619
781
  }
620
782
 
621
783
  attributeChangedCallback(name, oldValue, newValue) {
@@ -630,6 +792,7 @@ class PropskitSelect extends HTMLElement {
630
792
  }
631
793
 
632
794
  #initialize() {
795
+ this.#initialValue = this.getAttribute("value") ?? "";
633
796
  const customLabel = this.querySelector(":scope > label");
634
797
  const field = document.createElement("fig-field");
635
798
  const label = customLabel || document.createElement("label");
@@ -687,6 +850,7 @@ class PropskitSelect extends HTMLElement {
687
850
  "size",
688
851
  "aria-label",
689
852
  "full",
853
+ "default",
690
854
  ]);
691
855
  return this.getAttributeNames().filter(
692
856
  (name) => !reserved.has(name) && !name.startsWith("data-"),
@@ -763,6 +927,7 @@ class PropskitSelect extends HTMLElement {
763
927
 
764
928
  #handlePointerDown(event) {
765
929
  if (!(event.target instanceof Element)) return;
930
+ if (event.target.closest("fig-menu")) return;
766
931
  // fig-select owns its trigger/option clicks.
767
932
  if (event.target.closest("fig-select")) {
768
933
  this.#closeGesture = false;
@@ -773,6 +938,9 @@ class PropskitSelect extends HTMLElement {
773
938
  }
774
939
 
775
940
  #handleClick(event) {
941
+ if (event.target instanceof Element && event.target.closest("fig-menu")) {
942
+ return;
943
+ }
776
944
  if (event.target instanceof Element && event.target.closest("fig-select")) {
777
945
  this.#closeGesture = false;
778
946
  return;
@@ -804,6 +972,21 @@ class PropskitSelect extends HTMLElement {
804
972
  }
805
973
  }
806
974
 
975
+ get defaultValue() {
976
+ return this.getAttribute("default") ?? this.#initialValue ?? "";
977
+ }
978
+
979
+ get isDefault() {
980
+ return figLabPropskitValuesEqual(this.value, this.defaultValue);
981
+ }
982
+
983
+ resetToDefault() {
984
+ const value = this.defaultValue;
985
+ this.value = value;
986
+ if (this.#select) this.#select.value = value;
987
+ figLabEmitPropskitReset(this, value);
988
+ }
989
+
807
990
  focus(options) {
808
991
  this.#select?.focus(options);
809
992
  }
@@ -821,6 +1004,7 @@ class PropskitText extends HTMLElement {
821
1004
  #boundHandleInput = null;
822
1005
  #boundHandleChange = null;
823
1006
  #boundHandleClick = this.#handleClick.bind(this);
1007
+ #initialValue = null;
824
1008
 
825
1009
  static get observedAttributes() {
826
1010
  return ["label", "direction", "aria-label"];
@@ -833,6 +1017,7 @@ class PropskitText extends HTMLElement {
833
1017
  this.#bindInputEvents();
834
1018
  this.removeEventListener("click", this.#boundHandleClick);
835
1019
  this.addEventListener("click", this.#boundHandleClick);
1020
+ figLabConnectPropskitResetMenu(this);
836
1021
 
837
1022
  if (!this.#observer) {
838
1023
  this.#observer = new MutationObserver((mutations) => {
@@ -864,6 +1049,7 @@ class PropskitText extends HTMLElement {
864
1049
  this.#observer?.disconnect();
865
1050
  this.#unbindInputEvents();
866
1051
  this.removeEventListener("click", this.#boundHandleClick);
1052
+ figLabDisconnectPropskitResetMenu(this);
867
1053
  }
868
1054
 
869
1055
  attributeChangedCallback(name, oldValue, newValue) {
@@ -874,6 +1060,7 @@ class PropskitText extends HTMLElement {
874
1060
  }
875
1061
 
876
1062
  #initialize() {
1063
+ this.#initialValue = this.getAttribute("value") ?? "";
877
1064
  const initialChildren = Array.from(this.childNodes).filter(
878
1065
  (node) =>
879
1066
  node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
@@ -938,6 +1125,7 @@ class PropskitText extends HTMLElement {
938
1125
  "aria-label",
939
1126
  "multiline",
940
1127
  "resizable",
1128
+ "default",
941
1129
  ]);
942
1130
  return this.getAttributeNames().filter(
943
1131
  (name) => !reserved.has(name) && !name.startsWith("data-"),
@@ -996,7 +1184,10 @@ class PropskitText extends HTMLElement {
996
1184
  }
997
1185
 
998
1186
  #handleClick(event) {
999
- if (event.target instanceof Element && event.target.closest("fig-input-text")) {
1187
+ if (
1188
+ event.target instanceof Element &&
1189
+ event.target.closest("fig-input-text, fig-menu")
1190
+ ) {
1000
1191
  return;
1001
1192
  }
1002
1193
  this.focus();
@@ -1017,6 +1208,20 @@ class PropskitText extends HTMLElement {
1017
1208
  }
1018
1209
  }
1019
1210
 
1211
+ get defaultValue() {
1212
+ return this.getAttribute("default") ?? this.#initialValue ?? "";
1213
+ }
1214
+
1215
+ get isDefault() {
1216
+ return figLabPropskitValuesEqual(this.value, this.defaultValue);
1217
+ }
1218
+
1219
+ resetToDefault() {
1220
+ const value = this.defaultValue;
1221
+ this.value = value;
1222
+ figLabEmitPropskitReset(this, value);
1223
+ }
1224
+
1020
1225
  focus(options) {
1021
1226
  this.#input?.focus(options);
1022
1227
  }
@@ -1034,6 +1239,7 @@ class PropskitNumber extends HTMLElement {
1034
1239
  #boundHandleInput = null;
1035
1240
  #boundHandleChange = null;
1036
1241
  #boundHandleClick = this.#handleClick.bind(this);
1242
+ #initialValue = null;
1037
1243
 
1038
1244
  static get observedAttributes() {
1039
1245
  return ["label", "direction"];
@@ -1046,6 +1252,7 @@ class PropskitNumber extends HTMLElement {
1046
1252
  this.#bindInputEvents();
1047
1253
  this.removeEventListener("click", this.#boundHandleClick);
1048
1254
  this.addEventListener("click", this.#boundHandleClick);
1255
+ figLabConnectPropskitResetMenu(this);
1049
1256
 
1050
1257
  if (!this.#observer) {
1051
1258
  this.#observer = new MutationObserver((mutations) => {
@@ -1076,6 +1283,7 @@ class PropskitNumber extends HTMLElement {
1076
1283
  this.#observer?.disconnect();
1077
1284
  this.#unbindInputEvents();
1078
1285
  this.removeEventListener("click", this.#boundHandleClick);
1286
+ figLabDisconnectPropskitResetMenu(this);
1079
1287
  }
1080
1288
 
1081
1289
  attributeChangedCallback(name, oldValue, newValue) {
@@ -1084,6 +1292,8 @@ class PropskitNumber extends HTMLElement {
1084
1292
  }
1085
1293
 
1086
1294
  #initialize() {
1295
+ this.#initialValue =
1296
+ this.getAttribute("value") ?? this.getAttribute("min") ?? "0";
1087
1297
  const initialChildren = Array.from(this.childNodes).filter(
1088
1298
  (node) =>
1089
1299
  node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
@@ -1140,6 +1350,7 @@ class PropskitNumber extends HTMLElement {
1140
1350
  "class",
1141
1351
  "style",
1142
1352
  "id",
1353
+ "default",
1143
1354
  ]);
1144
1355
  return this.getAttributeNames().filter(
1145
1356
  (name) => !reserved.has(name) && !name.startsWith("data-"),
@@ -1199,7 +1410,10 @@ class PropskitNumber extends HTMLElement {
1199
1410
  }
1200
1411
 
1201
1412
  #handleClick(event) {
1202
- if (event.target instanceof Element && event.target.closest("fig-input-number")) {
1413
+ if (
1414
+ event.target instanceof Element &&
1415
+ event.target.closest("fig-input-number, fig-menu")
1416
+ ) {
1203
1417
  return;
1204
1418
  }
1205
1419
  this.focus();
@@ -1220,6 +1434,20 @@ class PropskitNumber extends HTMLElement {
1220
1434
  }
1221
1435
  }
1222
1436
 
1437
+ get defaultValue() {
1438
+ return this.getAttribute("default") ?? this.#initialValue ?? "0";
1439
+ }
1440
+
1441
+ get isDefault() {
1442
+ return figLabPropskitValuesEqual(this.value, this.defaultValue);
1443
+ }
1444
+
1445
+ resetToDefault() {
1446
+ const value = this.defaultValue;
1447
+ this.value = value;
1448
+ figLabEmitPropskitReset(this, this.#input?.value ?? value);
1449
+ }
1450
+
1223
1451
  focus(options) {
1224
1452
  this.#input?.focus(options);
1225
1453
  }
@@ -1237,12 +1465,12 @@ class PropskitGroup extends HTMLElement {
1237
1465
  "propskit-slider",
1238
1466
  "propskit-switch",
1239
1467
  "propskit-text",
1468
+ "propskit-oscillator",
1240
1469
  ].join(",");
1241
1470
 
1242
1471
  #header = null;
1243
1472
  #chevron = null;
1244
1473
  #resetTooltip = null;
1245
- #defaults = new WeakMap();
1246
1474
  #childObserver = null;
1247
1475
  #dirtyFrame = 0;
1248
1476
  #boundOnControlEvent = () => this.#queueDirtySync();
@@ -1251,7 +1479,6 @@ class PropskitGroup extends HTMLElement {
1251
1479
  this.#render();
1252
1480
  this.#bindDirtyListeners();
1253
1481
  requestAnimationFrame(() => {
1254
- this.#ensureDefaults();
1255
1482
  this.#syncDirtyState();
1256
1483
  });
1257
1484
  }
@@ -1334,12 +1561,16 @@ class PropskitGroup extends HTMLElement {
1334
1561
 
1335
1562
  if (!this.#childObserver) {
1336
1563
  this.#childObserver = new MutationObserver(() => {
1337
- this.#ensureDefaults();
1338
1564
  this.#queueDirtySync();
1339
1565
  });
1340
1566
  }
1341
1567
  this.#childObserver.disconnect();
1342
- this.#childObserver.observe(this, { childList: true, subtree: true });
1568
+ this.#childObserver.observe(this, {
1569
+ childList: true,
1570
+ subtree: true,
1571
+ attributes: true,
1572
+ attributeFilter: ["value", "checked", "default"],
1573
+ });
1343
1574
  }
1344
1575
 
1345
1576
  #unbindDirtyListeners() {
@@ -1352,7 +1583,6 @@ class PropskitGroup extends HTMLElement {
1352
1583
  if (this.#dirtyFrame) return;
1353
1584
  this.#dirtyFrame = requestAnimationFrame(() => {
1354
1585
  this.#dirtyFrame = 0;
1355
- this.#ensureDefaults();
1356
1586
  this.#syncDirtyState();
1357
1587
  });
1358
1588
  }
@@ -1391,48 +1621,15 @@ class PropskitGroup extends HTMLElement {
1391
1621
  #controls() {
1392
1622
  return [
1393
1623
  ...this.querySelectorAll(PropskitGroup.#CONTROL_SELECTOR),
1394
- ].filter((el) => el.closest("propskit-group") === this);
1395
- }
1396
-
1397
- #snapshotControl(el) {
1398
- if (el.localName === "propskit-switch") {
1399
- return { kind: "checked", value: Boolean(el.checked) };
1400
- }
1401
- if ("value" in el) {
1402
- return { kind: "value", value: el.value };
1403
- }
1404
- return { kind: "value", value: el.getAttribute("value") ?? "" };
1405
- }
1406
-
1407
- #ensureDefaults() {
1408
- for (const el of this.#controls()) {
1409
- if (this.#defaults.has(el)) continue;
1410
- if (el.hasAttribute("default")) {
1411
- if (el.localName === "propskit-switch") {
1412
- this.#defaults.set(el, {
1413
- kind: "checked",
1414
- value: figLabBooleanAttribute(el, "default"),
1415
- });
1416
- continue;
1417
- }
1418
- this.#defaults.set(el, {
1419
- kind: "value",
1420
- value: el.getAttribute("default") ?? "",
1421
- });
1422
- continue;
1423
- }
1424
- this.#defaults.set(el, this.#snapshotControl(el));
1425
- }
1624
+ ].filter(
1625
+ (el) =>
1626
+ el.closest("propskit-group") === this &&
1627
+ !el.parentElement?.closest(PropskitGroup.#CONTROL_SELECTOR),
1628
+ );
1426
1629
  }
1427
1630
 
1428
1631
  #controlIsDirty(el) {
1429
- const snap = this.#defaults.get(el);
1430
- if (!snap) return false;
1431
- const cur = this.#snapshotControl(el);
1432
- if (snap.kind === "checked") {
1433
- return Boolean(cur.value) !== Boolean(snap.value);
1434
- }
1435
- return String(cur.value ?? "") !== String(snap.value ?? "");
1632
+ return el.isDefault === false;
1436
1633
  }
1437
1634
 
1438
1635
  #computeDirty() {
@@ -1450,67 +1647,12 @@ class PropskitGroup extends HTMLElement {
1450
1647
  }
1451
1648
 
1452
1649
  #restoreControl(el) {
1453
- const snap = this.#defaults.get(el);
1454
- if (!snap) return;
1455
-
1456
- if (snap.kind === "checked") {
1457
- el.checked = snap.value;
1458
- const detail = {
1459
- checked: Boolean(snap.value),
1460
- value: el.getAttribute("value") ?? "",
1461
- };
1462
- el.dispatchEvent(
1463
- new CustomEvent("input", {
1464
- detail,
1465
- bubbles: true,
1466
- cancelable: true,
1467
- composed: true,
1468
- }),
1469
- );
1470
- el.dispatchEvent(
1471
- new CustomEvent("change", {
1472
- detail,
1473
- bubbles: true,
1474
- cancelable: true,
1475
- composed: true,
1476
- }),
1477
- );
1478
- return;
1479
- }
1480
-
1481
1650
  if (typeof el.resetToDefault === "function") {
1482
- const prevDefault = el.getAttribute("default");
1483
- el.setAttribute("default", String(snap.value ?? ""));
1484
1651
  el.resetToDefault();
1485
- if (prevDefault === null) el.removeAttribute("default");
1486
- else el.setAttribute("default", prevDefault);
1487
- return;
1488
1652
  }
1489
-
1490
- el.value = snap.value;
1491
- const detail =
1492
- el.getAttribute?.("value") ??
1493
- ("value" in el ? el.value : snap.value);
1494
- el.dispatchEvent(
1495
- new CustomEvent("input", {
1496
- detail,
1497
- bubbles: true,
1498
- cancelable: true,
1499
- composed: true,
1500
- }),
1501
- );
1502
- el.dispatchEvent(
1503
- new CustomEvent("change", {
1504
- detail,
1505
- bubbles: true,
1506
- cancelable: true,
1507
- composed: true,
1508
- }),
1509
- );
1510
1653
  }
1511
1654
 
1512
1655
  #resetControls() {
1513
- this.#ensureDefaults();
1514
1656
  for (const el of this.#controls()) this.#restoreControl(el);
1515
1657
  this.#syncDirtyState();
1516
1658
  this.dispatchEvent(
@@ -1652,6 +1794,7 @@ class PropskitSlider extends HTMLElement {
1652
1794
  #contextMenu = null;
1653
1795
  #pendingClickTimer = 0;
1654
1796
  #pendingClickValue = null;
1797
+ #initialValue = null;
1655
1798
  #isElasticTracking = false;
1656
1799
  #elasticMaxPx = 0;
1657
1800
  #elasticRangeRect = null;
@@ -1725,6 +1868,10 @@ class PropskitSlider extends HTMLElement {
1725
1868
  this.#pushExternalValueToSlider();
1726
1869
  continue;
1727
1870
  }
1871
+ if (mutation.attributeName === "color") {
1872
+ syncSlider = true;
1873
+ continue;
1874
+ }
1728
1875
  if (
1729
1876
  mutation.attributeName &&
1730
1877
  this.#ignoredSliderAttrs.has(mutation.attributeName)
@@ -1784,6 +1931,8 @@ class PropskitSlider extends HTMLElement {
1784
1931
  }
1785
1932
 
1786
1933
  #initialize() {
1934
+ this.#initialValue =
1935
+ this.getAttribute("value") ?? this.getAttribute("min") ?? "0";
1787
1936
  const initialChildren = Array.from(this.childNodes).filter((node) => {
1788
1937
  return (
1789
1938
  node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim())
@@ -1825,7 +1974,7 @@ class PropskitSlider extends HTMLElement {
1825
1974
 
1826
1975
  const resetItem = document.createElement("fig-menu-item");
1827
1976
  resetItem.setAttribute("value", "reset-default");
1828
- resetItem.textContent = "Reset to default";
1977
+ resetItem.textContent = "Reset";
1829
1978
  menu.appendChild(resetItem);
1830
1979
  menu.addEventListener("change", this.#boundHandleContextMenuChange);
1831
1980
 
@@ -1903,17 +2052,33 @@ class PropskitSlider extends HTMLElement {
1903
2052
  if (sliderType === "opacity") {
1904
2053
  this.#slider.style.setProperty(
1905
2054
  "--color",
1906
- "light-dark(#444444, #e6e6e6)",
2055
+ this.getAttribute("color") || "var(--figma-color-bg-secondary)",
1907
2056
  );
1908
2057
  } else {
1909
2058
  this.#slider.style.removeProperty("--color");
1910
2059
  }
2060
+ this.#syncNumberTheme(sliderType);
1911
2061
 
1912
2062
  this.#managedSliderAttrs = nextManaged;
1913
2063
  this.#pushExternalValueToSlider();
1914
2064
  this.#queueSteppersSync();
1915
2065
  }
1916
2066
 
2067
+ #syncNumberTheme(sliderType) {
2068
+ const numberInput = this.#slider?.querySelector("fig-input-number");
2069
+ if (!numberInput) return;
2070
+ if (sliderType !== "opacity" || !this.hasAttribute("color")) {
2071
+ numberInput.removeAttribute("theme");
2072
+ return;
2073
+ }
2074
+ const theme = figLabPerceivedColorTheme(
2075
+ this.#slider,
2076
+ this.getAttribute("color"),
2077
+ );
2078
+ if (theme) numberInput.setAttribute("theme", theme);
2079
+ else numberInput.removeAttribute("theme");
2080
+ }
2081
+
1917
2082
  #pushExternalValueToSlider() {
1918
2083
  if (!this.#slider) return;
1919
2084
  const next = this.getAttribute("value") ?? "";
@@ -1977,8 +2142,8 @@ class PropskitSlider extends HTMLElement {
1977
2142
  }
1978
2143
  }
1979
2144
  if (numberInput) {
1980
- numberInput.setAttribute("tabindex", "-1");
1981
- numberInput.setAttribute("aria-hidden", "true");
2145
+ numberInput.removeAttribute("tabindex");
2146
+ numberInput.removeAttribute("aria-hidden");
1982
2147
  }
1983
2148
  }
1984
2149
 
@@ -2228,8 +2393,7 @@ class PropskitSlider extends HTMLElement {
2228
2393
  return (
2229
2394
  this.getAttribute("default") ??
2230
2395
  this.#slider?.getAttribute("default") ??
2231
- this.getAttribute("value") ??
2232
- this.#slider?.getAttribute("value") ??
2396
+ this.#initialValue ??
2233
2397
  this.#rangeInput?.min ??
2234
2398
  "0"
2235
2399
  );
@@ -2347,6 +2511,14 @@ class PropskitSlider extends HTMLElement {
2347
2511
  if (this.#slider) this.#slider.value = next;
2348
2512
  }
2349
2513
 
2514
+ get defaultValue() {
2515
+ return this.#defaultValue();
2516
+ }
2517
+
2518
+ get isDefault() {
2519
+ return figLabPropskitValuesEqual(this.value, this.defaultValue);
2520
+ }
2521
+
2350
2522
  resetToDefault() {
2351
2523
  this.#resetToDefault();
2352
2524
  }
@@ -3749,6 +3921,7 @@ class PropskitOscillator extends HTMLElement {
3749
3921
  #activeFieldInput = null;
3750
3922
  #dragController = null;
3751
3923
  #dragCleanup = null;
3924
+ #initialValue = null;
3752
3925
 
3753
3926
  static TYPES = [
3754
3927
  { name: "Wave", value: "sine" },
@@ -3772,11 +3945,18 @@ class PropskitOscillator extends HTMLElement {
3772
3945
  }
3773
3946
 
3774
3947
  connectedCallback() {
3948
+ if (this.#initialValue === null) {
3949
+ this.#initialValue =
3950
+ this.getAttribute("default") ??
3951
+ this.getAttribute("value") ??
3952
+ JSON.stringify({ waves: [PropskitOscillator.#defaultWave()] });
3953
+ }
3775
3954
  this.#precision = this.#readInteger("precision", 2);
3776
3955
  this.#parseValue(this.getAttribute("value"));
3777
3956
  this.#syncAspectRatio();
3778
3957
  this.#render();
3779
3958
  this.#setupResizeObserver();
3959
+ figLabConnectPropskitResetMenu(this);
3780
3960
  }
3781
3961
 
3782
3962
  disconnectedCallback() {
@@ -3786,6 +3966,7 @@ class PropskitOscillator extends HTMLElement {
3786
3966
  this.#resizeObserver.disconnect();
3787
3967
  this.#resizeObserver = null;
3788
3968
  }
3969
+ figLabDisconnectPropskitResetMenu(this);
3789
3970
  }
3790
3971
 
3791
3972
  attributeChangedCallback(name, oldValue, newValue) {
@@ -3825,6 +4006,27 @@ class PropskitOscillator extends HTMLElement {
3825
4006
  );
3826
4007
  }
3827
4008
 
4009
+ get defaultValue() {
4010
+ return this.getAttribute("default") ?? this.#initialValue ?? "";
4011
+ }
4012
+
4013
+ get isDefault() {
4014
+ const defaultWaves = this.#normalizedWaves(this.defaultValue);
4015
+ if (!defaultWaves) return false;
4016
+ return (
4017
+ JSON.stringify(this.data.waves) ===
4018
+ JSON.stringify(defaultWaves.map((wave) => this.#roundWave(wave)))
4019
+ );
4020
+ }
4021
+
4022
+ resetToDefault() {
4023
+ const value = this.defaultValue;
4024
+ if (!value) return;
4025
+ this.value = value;
4026
+ this.#emit("input");
4027
+ this.#emit("change");
4028
+ }
4029
+
3828
4030
  get data() {
3829
4031
  return {
3830
4032
  waves: this.#waves.map((wave) => this.#roundWave(wave)),
@@ -3866,33 +4068,35 @@ class PropskitOscillator extends HTMLElement {
3866
4068
  }
3867
4069
 
3868
4070
  #parseValue(value) {
3869
- if (!value) return false;
4071
+ const nextWaves = this.#normalizedWaves(value);
4072
+ if (!nextWaves) return false;
4073
+ this.#waves = nextWaves;
4074
+ if (!this.#waves.length) {
4075
+ this.#waves = [PropskitOscillator.#defaultWave()];
4076
+ }
4077
+ this.#activeWaveIndex = Math.min(this.#activeWaveIndex, this.#waves.length - 1);
4078
+ return true;
4079
+ }
3870
4080
 
4081
+ #normalizedWaves(value) {
4082
+ if (!value) return null;
3871
4083
  let parsed = null;
3872
4084
  if (typeof value === "string") {
3873
4085
  try {
3874
4086
  parsed = JSON.parse(value);
3875
4087
  } catch {
3876
- parsed = null;
4088
+ return null;
3877
4089
  }
3878
4090
  } else if (typeof value === "object") {
3879
4091
  parsed = value;
3880
4092
  }
3881
-
3882
- if (!parsed) return false;
3883
-
3884
- const nextWaves = Array.isArray(parsed)
4093
+ if (!parsed) return null;
4094
+ const waves = Array.isArray(parsed)
3885
4095
  ? parsed
3886
4096
  : Array.isArray(parsed.waves)
3887
4097
  ? parsed.waves
3888
4098
  : [parsed];
3889
-
3890
- this.#waves = nextWaves.map((wave) => this.#normalizeWave(wave));
3891
- if (!this.#waves.length) {
3892
- this.#waves = [PropskitOscillator.#defaultWave()];
3893
- }
3894
- this.#activeWaveIndex = Math.min(this.#activeWaveIndex, this.#waves.length - 1);
3895
- return true;
4099
+ return waves.map((wave) => this.#normalizeWave(wave));
3896
4100
  }
3897
4101
 
3898
4102
  #normalizeWave(state = {}) {
@@ -3993,6 +4197,7 @@ class PropskitOscillator extends HTMLElement {
3993
4197
  this.#updateWaveform();
3994
4198
  this.#setupEvents();
3995
4199
  this.#startPlayhead();
4200
+ figLabConnectPropskitResetMenu(this);
3996
4201
  }
3997
4202
 
3998
4203
  #getInnerHTML() {
@@ -4029,9 +4234,7 @@ class PropskitOscillator extends HTMLElement {
4029
4234
  <fig-tooltip text="Remove form">
4030
4235
  <fig-button class="propskit-oscillator-remove-button" variant="ghost" icon data-wave-index="${index}" aria-label="Remove form"${removeDisabled}><fig-icon name="minus"></fig-icon></fig-button>
4031
4236
  </fig-tooltip>
4032
- <fig-tooltip text="Add form">
4033
- ${this.#getWaveTypeSelectHTML("propskit-oscillator-add-type propskit-oscillator-add-type-button", "sine", disabled, index)}
4034
- </fig-tooltip>
4237
+ ${this.#getWaveTypeMenuHTML(disabled, index)}
4035
4238
  </fig-header>
4036
4239
  <div class="propskit-oscillator-fields" data-wave-index="${index}"${active}>
4037
4240
  ${this.#getNumberFieldHTML(index, "frequency", "Frequency", 0.1, 16, 0.1, "")}
@@ -4042,26 +4245,33 @@ class PropskitOscillator extends HTMLElement {
4042
4245
  </fig-group>`;
4043
4246
  }
4044
4247
 
4045
- #getWaveTypeSelectHTML(className, value, disabled, index = null) {
4046
- const options = PropskitOscillator.TYPES.map((type) => {
4047
- const selected = type.value === value ? " selected" : "";
4048
- return `<fig-select-option value="${type.value}" label="${type.name}"${selected}>
4049
- <span slot="prepend">${PropskitOscillator.waveIcon(type.value, 24)}</span>
4248
+ #getWaveTypeMenuHTML(disabled, index) {
4249
+ const items = PropskitOscillator.TYPES.map((type) => {
4250
+ return `<fig-menu-item value="${type.value}">
4251
+ ${PropskitOscillator.waveIcon(type.value, 24)}
4050
4252
  <span>${type.name}</span>
4051
- </fig-select-option>`;
4253
+ </fig-menu-item>`;
4052
4254
  }).join("");
4053
- const indexAttr =
4054
- index === null ? "" : ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
4055
- return `<fig-select class="${className}" value="${value}" label="Add form"${indexAttr}${disabled}><fig-select-options>${options}</fig-select-options></fig-select>`;
4255
+ const indexAttr = ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
4256
+ return `<fig-menu class="propskit-oscillator-add-type" position="bottom right"${indexAttr}${disabled}>
4257
+ <fig-tooltip text="Add form">
4258
+ <fig-button class="propskit-oscillator-add-type-button" variant="ghost" icon fig-menu-trigger aria-label="Add form"><fig-icon name="plus"></fig-icon></fig-button>
4259
+ </fig-tooltip>
4260
+ ${items}
4261
+ </fig-menu>`;
4056
4262
  }
4057
4263
 
4058
4264
  #getNumberFieldHTML(index, name, label, min, max, step, units) {
4059
4265
  const disabled = this.#isDisabled() ? " disabled" : "";
4266
+ const typeAttrs =
4267
+ name === "amplitude" || name === "offset"
4268
+ ? ' type="delta" default="0"'
4269
+ : "";
4060
4270
  const unitsAttr = units
4061
4271
  ? ` units="${PropskitOscillator.#escapeAttribute(units)}"`
4062
4272
  : "";
4063
4273
  const wave = this.#waves[index] || PropskitOscillator.#defaultWave();
4064
- return `<propskit-slider class="propskit-oscillator-field" label="${label}" direction="horizontal" name="${name}" data-wave-index="${index}" value="${this.#round(wave[name])}" min="${min}" max="${max}" step="${step}" precision="${this.#precision}" elastic="false"${unitsAttr}${disabled}></propskit-slider>`;
4274
+ return `<propskit-slider class="propskit-oscillator-field" label="${label}" direction="horizontal" name="${name}" data-wave-index="${index}" value="${this.#round(wave[name])}" min="${min}" max="${max}" step="${step}" precision="${this.#precision}" elastic="false"${typeAttrs}${unitsAttr}${disabled}></propskit-slider>`;
4065
4275
  }
4066
4276
 
4067
4277
  #cacheRefs() {
@@ -4328,7 +4538,7 @@ class PropskitOscillator extends HTMLElement {
4328
4538
  if (this.#isDisabled()) return;
4329
4539
  const insertAfter = this.#indexFromElement(control);
4330
4540
  const insertIndex = insertAfter + 1;
4331
- const type = this.#normalizeType(event.detail ?? control.value);
4541
+ const type = this.#normalizeType(event.detail?.value ?? control.value);
4332
4542
  this.#waves.splice(insertIndex, 0, PropskitOscillator.#defaultWave(type));
4333
4543
  this.#reindexExpandedWavesAfterInsert(insertIndex);
4334
4544
  this.#activeWaveIndex = insertIndex;