@rogieking/figui3 6.18.3 → 6.19.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/components.css +2 -1
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +17 -17
- package/dist/fig.css +1 -1
- package/dist/fig.js +1 -1
- package/fig-lab.css +151 -7
- package/fig-lab.js +609 -22
- package/fig.js +1 -1
- package/package.json +1 -1
package/fig-lab.js
CHANGED
|
@@ -243,6 +243,7 @@ class PropskitColor extends HTMLElement {
|
|
|
243
243
|
#boundHandleInput = null;
|
|
244
244
|
#boundHandleChange = null;
|
|
245
245
|
#boundHandleClick = this.#handleClick.bind(this);
|
|
246
|
+
#initialValue = null;
|
|
246
247
|
|
|
247
248
|
static get observedAttributes() {
|
|
248
249
|
return ["label", "direction", "aria-label"];
|
|
@@ -253,6 +254,10 @@ class PropskitColor extends HTMLElement {
|
|
|
253
254
|
this.#syncField();
|
|
254
255
|
this.#syncInputAttributes();
|
|
255
256
|
this.#bindInputEvents();
|
|
257
|
+
if (this.#initialValue === null) {
|
|
258
|
+
this.#initialValue =
|
|
259
|
+
this.getAttribute("default") ?? this.getAttribute("value") ?? "";
|
|
260
|
+
}
|
|
256
261
|
this.removeEventListener("click", this.#boundHandleClick);
|
|
257
262
|
this.addEventListener("click", this.#boundHandleClick);
|
|
258
263
|
|
|
@@ -359,6 +364,7 @@ class PropskitColor extends HTMLElement {
|
|
|
359
364
|
"size",
|
|
360
365
|
"aria-label",
|
|
361
366
|
"text",
|
|
367
|
+
"default",
|
|
362
368
|
]);
|
|
363
369
|
return this.getAttributeNames().filter(
|
|
364
370
|
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
@@ -374,7 +380,16 @@ class PropskitColor extends HTMLElement {
|
|
|
374
380
|
if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
|
|
375
381
|
}
|
|
376
382
|
for (const attrName of inputAttrs) {
|
|
377
|
-
|
|
383
|
+
const next = this.getAttribute(attrName) ?? "";
|
|
384
|
+
// Soft sync only. Never force-clear value here — that fights live edits
|
|
385
|
+
// (fig-input-color often keeps a stale value attribute while editing).
|
|
386
|
+
if (
|
|
387
|
+
attrName === "value" &&
|
|
388
|
+
this.#input.getAttribute("value") === next
|
|
389
|
+
) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
this.#input.setAttribute(attrName, next);
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
this.#input.setAttribute("text", "true");
|
|
@@ -399,10 +414,31 @@ class PropskitColor extends HTMLElement {
|
|
|
399
414
|
}
|
|
400
415
|
}
|
|
401
416
|
|
|
417
|
+
#valueFromColorEvent(event) {
|
|
418
|
+
const detail =
|
|
419
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
420
|
+
? event.detail
|
|
421
|
+
: undefined;
|
|
422
|
+
if (typeof detail === "string" && detail) return detail;
|
|
423
|
+
if (detail && typeof detail === "object") {
|
|
424
|
+
if (typeof detail.value === "string" && detail.value) return detail.value;
|
|
425
|
+
if (typeof detail.hex === "string" && detail.hex) return detail.hex;
|
|
426
|
+
if (typeof detail.color === "string" && detail.color) return detail.color;
|
|
427
|
+
}
|
|
428
|
+
// Prefer live JS value — fig-input-color often leaves the attribute stale.
|
|
429
|
+
if (typeof this.#input?.value === "string" && this.#input.value) {
|
|
430
|
+
return this.#input.value;
|
|
431
|
+
}
|
|
432
|
+
return this.#input?.getAttribute("value") ?? "";
|
|
433
|
+
}
|
|
434
|
+
|
|
402
435
|
#forwardInputEvent(type, event) {
|
|
403
436
|
event.stopImmediatePropagation();
|
|
404
|
-
const value = this.#
|
|
437
|
+
const value = this.#valueFromColorEvent(event);
|
|
405
438
|
this.setAttribute("value", value);
|
|
439
|
+
if (this.#input && this.#input.getAttribute("value") !== value) {
|
|
440
|
+
this.#input.setAttribute("value", value);
|
|
441
|
+
}
|
|
406
442
|
const detail =
|
|
407
443
|
event instanceof CustomEvent && event.detail !== undefined
|
|
408
444
|
? event.detail
|
|
@@ -425,15 +461,55 @@ class PropskitColor extends HTMLElement {
|
|
|
425
461
|
}
|
|
426
462
|
|
|
427
463
|
get value() {
|
|
428
|
-
return this
|
|
464
|
+
return this.getAttribute("value") ?? this.#input?.value ?? this.#input?.getAttribute("value") ?? "";
|
|
429
465
|
}
|
|
430
466
|
|
|
431
467
|
set value(nextValue) {
|
|
432
468
|
if (nextValue === null || nextValue === undefined || nextValue === "") {
|
|
433
469
|
this.removeAttribute("value");
|
|
434
|
-
|
|
435
|
-
|
|
470
|
+
this.#input?.removeAttribute("value");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const next = String(nextValue);
|
|
474
|
+
this.setAttribute("value", next);
|
|
475
|
+
if (this.#input && this.#input.getAttribute("value") !== next) {
|
|
476
|
+
this.#input.setAttribute("value", next);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Force fig-input-color UI refresh even when the hex string is unchanged. */
|
|
481
|
+
#forceInputValue(next) {
|
|
482
|
+
if (!this.#input) return;
|
|
483
|
+
if (this.#input.getAttribute("value") === next) {
|
|
484
|
+
this.#input.removeAttribute("value");
|
|
436
485
|
}
|
|
486
|
+
this.#input.setAttribute("value", next);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#defaultValue() {
|
|
490
|
+
return this.getAttribute("default") ?? this.#initialValue ?? "";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
resetToDefault() {
|
|
494
|
+
const next = String(this.#defaultValue());
|
|
495
|
+
this.setAttribute("value", next);
|
|
496
|
+
this.#forceInputValue(next);
|
|
497
|
+
this.dispatchEvent(
|
|
498
|
+
new CustomEvent("input", {
|
|
499
|
+
detail: next,
|
|
500
|
+
bubbles: true,
|
|
501
|
+
cancelable: true,
|
|
502
|
+
composed: true,
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
this.dispatchEvent(
|
|
506
|
+
new CustomEvent("change", {
|
|
507
|
+
detail: next,
|
|
508
|
+
bubbles: true,
|
|
509
|
+
cancelable: true,
|
|
510
|
+
composed: true,
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
437
513
|
}
|
|
438
514
|
|
|
439
515
|
focus(options) {
|
|
@@ -521,6 +597,8 @@ class PropskitSelect extends HTMLElement {
|
|
|
521
597
|
const field = document.createElement("fig-field");
|
|
522
598
|
const label = customLabel || document.createElement("label");
|
|
523
599
|
const select = document.createElement("fig-select");
|
|
600
|
+
// Match menu width to the full-surface control.
|
|
601
|
+
select.setAttribute("full", "");
|
|
524
602
|
field.append(label, select);
|
|
525
603
|
this.#field = field;
|
|
526
604
|
this.#label = label;
|
|
@@ -556,6 +634,8 @@ class PropskitSelect extends HTMLElement {
|
|
|
556
634
|
this.#label.textContent?.trim() ||
|
|
557
635
|
"Select",
|
|
558
636
|
);
|
|
637
|
+
// Always match menu width to the control surface.
|
|
638
|
+
this.#select.setAttribute("full", "");
|
|
559
639
|
}
|
|
560
640
|
|
|
561
641
|
#getForwardedSelectAttrNames() {
|
|
@@ -569,6 +649,7 @@ class PropskitSelect extends HTMLElement {
|
|
|
569
649
|
"id",
|
|
570
650
|
"size",
|
|
571
651
|
"aria-label",
|
|
652
|
+
"full",
|
|
572
653
|
]);
|
|
573
654
|
return this.getAttributeNames().filter(
|
|
574
655
|
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
@@ -1101,6 +1182,397 @@ class PropskitNumber extends HTMLElement {
|
|
|
1101
1182
|
}
|
|
1102
1183
|
customElements.define("propskit-number", PropskitNumber);
|
|
1103
1184
|
|
|
1185
|
+
/* Collapsible property group — always collapsible (no collapsible attr). */
|
|
1186
|
+
class PropskitGroup extends HTMLElement {
|
|
1187
|
+
static observedAttributes = ["name", "open", "show-reset"];
|
|
1188
|
+
|
|
1189
|
+
static #CONTROL_SELECTOR = [
|
|
1190
|
+
"propskit-color",
|
|
1191
|
+
"propskit-number",
|
|
1192
|
+
"propskit-select",
|
|
1193
|
+
"propskit-slider",
|
|
1194
|
+
"propskit-switch",
|
|
1195
|
+
"propskit-text",
|
|
1196
|
+
].join(",");
|
|
1197
|
+
|
|
1198
|
+
#header = null;
|
|
1199
|
+
#chevron = null;
|
|
1200
|
+
#resetTooltip = null;
|
|
1201
|
+
#defaults = new WeakMap();
|
|
1202
|
+
#childObserver = null;
|
|
1203
|
+
#dirtyFrame = 0;
|
|
1204
|
+
#boundOnControlEvent = () => this.#queueDirtySync();
|
|
1205
|
+
|
|
1206
|
+
connectedCallback() {
|
|
1207
|
+
this.#render();
|
|
1208
|
+
this.#bindDirtyListeners();
|
|
1209
|
+
requestAnimationFrame(() => {
|
|
1210
|
+
this.#ensureDefaults();
|
|
1211
|
+
this.#syncDirtyState();
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
disconnectedCallback() {
|
|
1216
|
+
this.#unbindDirtyListeners();
|
|
1217
|
+
if (this.#dirtyFrame) {
|
|
1218
|
+
cancelAnimationFrame(this.#dirtyFrame);
|
|
1219
|
+
this.#dirtyFrame = 0;
|
|
1220
|
+
}
|
|
1221
|
+
if (this.#header) {
|
|
1222
|
+
this.#header.removeEventListener("click", this.#handleToggle);
|
|
1223
|
+
this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
|
|
1224
|
+
}
|
|
1225
|
+
const resetBtn = this.#resetTooltip?.querySelector("fig-button");
|
|
1226
|
+
resetBtn?.removeEventListener("click", this.#handleReset);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1230
|
+
if (oldValue === newValue) return;
|
|
1231
|
+
if (name === "open") {
|
|
1232
|
+
this.#header?.setAttribute("aria-expanded", String(this.open));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (name === "show-reset") {
|
|
1236
|
+
this.#syncResetButton();
|
|
1237
|
+
this.#syncDirtyState();
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
this.#render();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
get open() {
|
|
1244
|
+
const attr = this.getAttribute("open");
|
|
1245
|
+
return attr !== null && attr !== "false";
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
set open(value) {
|
|
1249
|
+
const was = this.open;
|
|
1250
|
+
if (value) {
|
|
1251
|
+
this.setAttribute("open", "true");
|
|
1252
|
+
} else {
|
|
1253
|
+
this.setAttribute("open", "false");
|
|
1254
|
+
}
|
|
1255
|
+
this.#header?.setAttribute("aria-expanded", String(!!value));
|
|
1256
|
+
if (was !== !!value) {
|
|
1257
|
+
this.dispatchEvent(
|
|
1258
|
+
new CustomEvent("openchange", {
|
|
1259
|
+
detail: { open: !!value },
|
|
1260
|
+
bubbles: true,
|
|
1261
|
+
}),
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** When true (default), show the reset control while the group is dirty. */
|
|
1267
|
+
get showReset() {
|
|
1268
|
+
const attr = this.getAttribute("show-reset");
|
|
1269
|
+
if (attr === null) return true;
|
|
1270
|
+
return attr !== "false";
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
set showReset(value) {
|
|
1274
|
+
if (value) this.setAttribute("show-reset", "true");
|
|
1275
|
+
else this.setAttribute("show-reset", "false");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
get dirty() {
|
|
1279
|
+
return this.hasAttribute("data-dirty") && this.getAttribute("data-dirty") !== "false";
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Restore all propskit controls in this group to their captured defaults. */
|
|
1283
|
+
resetProperties() {
|
|
1284
|
+
this.#resetControls();
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
#bindDirtyListeners() {
|
|
1288
|
+
this.removeEventListener("input", this.#boundOnControlEvent, true);
|
|
1289
|
+
this.removeEventListener("change", this.#boundOnControlEvent, true);
|
|
1290
|
+
this.addEventListener("input", this.#boundOnControlEvent, true);
|
|
1291
|
+
this.addEventListener("change", this.#boundOnControlEvent, true);
|
|
1292
|
+
|
|
1293
|
+
if (!this.#childObserver) {
|
|
1294
|
+
this.#childObserver = new MutationObserver(() => {
|
|
1295
|
+
this.#ensureDefaults();
|
|
1296
|
+
this.#queueDirtySync();
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
this.#childObserver.disconnect();
|
|
1300
|
+
this.#childObserver.observe(this, { childList: true, subtree: true });
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
#unbindDirtyListeners() {
|
|
1304
|
+
this.removeEventListener("input", this.#boundOnControlEvent, true);
|
|
1305
|
+
this.removeEventListener("change", this.#boundOnControlEvent, true);
|
|
1306
|
+
this.#childObserver?.disconnect();
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
#queueDirtySync() {
|
|
1310
|
+
if (this.#dirtyFrame) return;
|
|
1311
|
+
this.#dirtyFrame = requestAnimationFrame(() => {
|
|
1312
|
+
this.#dirtyFrame = 0;
|
|
1313
|
+
this.#ensureDefaults();
|
|
1314
|
+
this.#syncDirtyState();
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
#isResetTarget(target) {
|
|
1319
|
+
return (
|
|
1320
|
+
target instanceof Element &&
|
|
1321
|
+
Boolean(
|
|
1322
|
+
target.closest(
|
|
1323
|
+
".propskit-group-reset, .propskit-group-reset-tooltip",
|
|
1324
|
+
),
|
|
1325
|
+
)
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
#handleToggle = (e) => {
|
|
1330
|
+
if (this.#isResetTarget(e.target)) return;
|
|
1331
|
+
e.stopPropagation();
|
|
1332
|
+
this.open = !this.open;
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
#handleHeaderKeyDown = (e) => {
|
|
1336
|
+
if (this.#isResetTarget(e.target)) return;
|
|
1337
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
1338
|
+
e.preventDefault();
|
|
1339
|
+
e.stopPropagation();
|
|
1340
|
+
this.open = !this.open;
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
#handleReset = (e) => {
|
|
1344
|
+
e.preventDefault();
|
|
1345
|
+
e.stopPropagation();
|
|
1346
|
+
this.#resetControls();
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
#controls() {
|
|
1350
|
+
return [
|
|
1351
|
+
...this.querySelectorAll(PropskitGroup.#CONTROL_SELECTOR),
|
|
1352
|
+
].filter((el) => el.closest("propskit-group") === this);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
#snapshotControl(el) {
|
|
1356
|
+
if (el.localName === "propskit-switch") {
|
|
1357
|
+
return { kind: "checked", value: Boolean(el.checked) };
|
|
1358
|
+
}
|
|
1359
|
+
if ("value" in el) {
|
|
1360
|
+
return { kind: "value", value: el.value };
|
|
1361
|
+
}
|
|
1362
|
+
return { kind: "value", value: el.getAttribute("value") ?? "" };
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
#ensureDefaults() {
|
|
1366
|
+
for (const el of this.#controls()) {
|
|
1367
|
+
if (this.#defaults.has(el)) continue;
|
|
1368
|
+
if (el.hasAttribute("default")) {
|
|
1369
|
+
if (el.localName === "propskit-switch") {
|
|
1370
|
+
this.#defaults.set(el, {
|
|
1371
|
+
kind: "checked",
|
|
1372
|
+
value: figLabBooleanAttribute(el, "default"),
|
|
1373
|
+
});
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
this.#defaults.set(el, {
|
|
1377
|
+
kind: "value",
|
|
1378
|
+
value: el.getAttribute("default") ?? "",
|
|
1379
|
+
});
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
this.#defaults.set(el, this.#snapshotControl(el));
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
#controlIsDirty(el) {
|
|
1387
|
+
const snap = this.#defaults.get(el);
|
|
1388
|
+
if (!snap) return false;
|
|
1389
|
+
const cur = this.#snapshotControl(el);
|
|
1390
|
+
if (snap.kind === "checked") {
|
|
1391
|
+
return Boolean(cur.value) !== Boolean(snap.value);
|
|
1392
|
+
}
|
|
1393
|
+
return String(cur.value ?? "") !== String(snap.value ?? "");
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
#computeDirty() {
|
|
1397
|
+
for (const el of this.#controls()) {
|
|
1398
|
+
if (this.#controlIsDirty(el)) return true;
|
|
1399
|
+
}
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
#syncDirtyState() {
|
|
1404
|
+
const dirty = this.#computeDirty();
|
|
1405
|
+
if (dirty) this.setAttribute("data-dirty", "");
|
|
1406
|
+
else this.removeAttribute("data-dirty");
|
|
1407
|
+
this.#syncResetButton();
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
#restoreControl(el) {
|
|
1411
|
+
const snap = this.#defaults.get(el);
|
|
1412
|
+
if (!snap) return;
|
|
1413
|
+
|
|
1414
|
+
if (snap.kind === "checked") {
|
|
1415
|
+
el.checked = snap.value;
|
|
1416
|
+
const detail = {
|
|
1417
|
+
checked: Boolean(snap.value),
|
|
1418
|
+
value: el.getAttribute("value") ?? "",
|
|
1419
|
+
};
|
|
1420
|
+
el.dispatchEvent(
|
|
1421
|
+
new CustomEvent("input", {
|
|
1422
|
+
detail,
|
|
1423
|
+
bubbles: true,
|
|
1424
|
+
cancelable: true,
|
|
1425
|
+
composed: true,
|
|
1426
|
+
}),
|
|
1427
|
+
);
|
|
1428
|
+
el.dispatchEvent(
|
|
1429
|
+
new CustomEvent("change", {
|
|
1430
|
+
detail,
|
|
1431
|
+
bubbles: true,
|
|
1432
|
+
cancelable: true,
|
|
1433
|
+
composed: true,
|
|
1434
|
+
}),
|
|
1435
|
+
);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (typeof el.resetToDefault === "function") {
|
|
1440
|
+
const prevDefault = el.getAttribute("default");
|
|
1441
|
+
el.setAttribute("default", String(snap.value ?? ""));
|
|
1442
|
+
el.resetToDefault();
|
|
1443
|
+
if (prevDefault === null) el.removeAttribute("default");
|
|
1444
|
+
else el.setAttribute("default", prevDefault);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
el.value = snap.value;
|
|
1449
|
+
const detail =
|
|
1450
|
+
el.getAttribute?.("value") ??
|
|
1451
|
+
("value" in el ? el.value : snap.value);
|
|
1452
|
+
el.dispatchEvent(
|
|
1453
|
+
new CustomEvent("input", {
|
|
1454
|
+
detail,
|
|
1455
|
+
bubbles: true,
|
|
1456
|
+
cancelable: true,
|
|
1457
|
+
composed: true,
|
|
1458
|
+
}),
|
|
1459
|
+
);
|
|
1460
|
+
el.dispatchEvent(
|
|
1461
|
+
new CustomEvent("change", {
|
|
1462
|
+
detail,
|
|
1463
|
+
bubbles: true,
|
|
1464
|
+
cancelable: true,
|
|
1465
|
+
composed: true,
|
|
1466
|
+
}),
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
#resetControls() {
|
|
1471
|
+
this.#ensureDefaults();
|
|
1472
|
+
for (const el of this.#controls()) this.#restoreControl(el);
|
|
1473
|
+
this.#syncDirtyState();
|
|
1474
|
+
this.dispatchEvent(
|
|
1475
|
+
new CustomEvent("reset", {
|
|
1476
|
+
bubbles: true,
|
|
1477
|
+
composed: true,
|
|
1478
|
+
}),
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
#syncResetButton() {
|
|
1483
|
+
if (!this.showReset) {
|
|
1484
|
+
this.#resetTooltip?.setAttribute("hidden", "");
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
this.#ensureResetButton();
|
|
1488
|
+
this.#resetTooltip?.removeAttribute("hidden");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
#ensureResetButton() {
|
|
1492
|
+
if (!this.#header || !this.showReset) return;
|
|
1493
|
+
let tip = this.#header.querySelector(":scope > .propskit-group-reset-tooltip");
|
|
1494
|
+
if (!tip) {
|
|
1495
|
+
tip = document.createElement("fig-tooltip");
|
|
1496
|
+
tip.className = "propskit-group-reset-tooltip";
|
|
1497
|
+
tip.setAttribute("text", "Reset properties");
|
|
1498
|
+
const btn = document.createElement("fig-button");
|
|
1499
|
+
btn.className = "propskit-group-reset";
|
|
1500
|
+
btn.setAttribute("variant", "ghost");
|
|
1501
|
+
btn.setAttribute("icon", "");
|
|
1502
|
+
btn.setAttribute("aria-label", "Reset properties");
|
|
1503
|
+
const icon = document.createElement("fig-icon");
|
|
1504
|
+
icon.setAttribute("name", "reset");
|
|
1505
|
+
btn.appendChild(icon);
|
|
1506
|
+
tip.appendChild(btn);
|
|
1507
|
+
this.#header.appendChild(tip);
|
|
1508
|
+
btn.addEventListener("click", this.#handleReset);
|
|
1509
|
+
} else {
|
|
1510
|
+
const btn = tip.querySelector("fig-button");
|
|
1511
|
+
btn?.removeEventListener("click", this.#handleReset);
|
|
1512
|
+
btn?.addEventListener("click", this.#handleReset);
|
|
1513
|
+
}
|
|
1514
|
+
this.#resetTooltip = tip;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
#render() {
|
|
1518
|
+
const nameAttr = this.getAttribute("name");
|
|
1519
|
+
const label = nameAttr || "Group";
|
|
1520
|
+
const userHeader = this.querySelector(":scope > fig-header");
|
|
1521
|
+
|
|
1522
|
+
if (userHeader) {
|
|
1523
|
+
this.#header = userHeader;
|
|
1524
|
+
} else if (!this.#header || !this.#header.dataset.generated) {
|
|
1525
|
+
this.#header = document.createElement("fig-header");
|
|
1526
|
+
this.#header.setAttribute("borderless", "");
|
|
1527
|
+
this.#header.dataset.generated = "true";
|
|
1528
|
+
this.prepend(this.#header);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
let h3 = this.#header.querySelector("h3");
|
|
1532
|
+
if (!h3) {
|
|
1533
|
+
h3 = document.createElement("h3");
|
|
1534
|
+
this.#header.prepend(h3);
|
|
1535
|
+
}
|
|
1536
|
+
if (!h3.id) h3.id = figLabUniqueId("propskit-group");
|
|
1537
|
+
if (this.#header.dataset.generated) {
|
|
1538
|
+
// Preserve chevron while updating the title text node.
|
|
1539
|
+
const chevron = h3.querySelector(".propskit-group-chevron");
|
|
1540
|
+
h3.textContent = label;
|
|
1541
|
+
if (chevron) h3.prepend(chevron);
|
|
1542
|
+
}
|
|
1543
|
+
if (!this.hasAttribute("role")) this.setAttribute("role", "group");
|
|
1544
|
+
if (
|
|
1545
|
+
!this.hasAttribute("aria-label") &&
|
|
1546
|
+
!this.hasAttribute("aria-labelledby")
|
|
1547
|
+
) {
|
|
1548
|
+
this.setAttribute("aria-labelledby", h3.id);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (!h3.querySelector(".propskit-group-chevron")) {
|
|
1552
|
+
const chevron = document.createElement("fig-icon");
|
|
1553
|
+
chevron.setAttribute("name", "chevron");
|
|
1554
|
+
chevron.setAttribute("size", "small");
|
|
1555
|
+
chevron.className = "propskit-group-chevron";
|
|
1556
|
+
h3.prepend(chevron);
|
|
1557
|
+
}
|
|
1558
|
+
this.#chevron = h3.querySelector(".propskit-group-chevron");
|
|
1559
|
+
this.#syncResetButton();
|
|
1560
|
+
this.#header.removeEventListener("click", this.#handleToggle);
|
|
1561
|
+
this.#header.addEventListener("click", this.#handleToggle);
|
|
1562
|
+
this.#header.setAttribute("role", "button");
|
|
1563
|
+
this.#header.setAttribute("tabindex", "0");
|
|
1564
|
+
this.#header.setAttribute("aria-expanded", String(this.open));
|
|
1565
|
+
this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
|
|
1566
|
+
this.#header.addEventListener("keydown", this.#handleHeaderKeyDown);
|
|
1567
|
+
|
|
1568
|
+
if (!this.hasAttribute("open")) {
|
|
1569
|
+
this.setAttribute("open", "false");
|
|
1570
|
+
this.#header.setAttribute("aria-expanded", "false");
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
customElements.define("propskit-group", PropskitGroup);
|
|
1575
|
+
|
|
1104
1576
|
/* Field + Slider wrapper */
|
|
1105
1577
|
class PropskitSlider extends HTMLElement {
|
|
1106
1578
|
#field = null;
|
|
@@ -1758,6 +2230,11 @@ class FigCanvasControl extends HTMLElement {
|
|
|
1758
2230
|
#moveCursorPrevBodyCursor = "";
|
|
1759
2231
|
#moveCursorPrevBodyCursorPriority = "";
|
|
1760
2232
|
#boundMoveCursorEnd = null;
|
|
2233
|
+
#rotateCursorPointerId = null;
|
|
2234
|
+
#rotateCursorHandle = null;
|
|
2235
|
+
#rotateCursorPrevBodyCursor = "";
|
|
2236
|
+
#rotateCursorPrevBodyCursorPriority = "";
|
|
2237
|
+
#boundRotateCursorEnd = null;
|
|
1761
2238
|
|
|
1762
2239
|
get #type() {
|
|
1763
2240
|
return this.getAttribute("type") || "point";
|
|
@@ -1858,7 +2335,9 @@ class FigCanvasControl extends HTMLElement {
|
|
|
1858
2335
|
disconnectedCallback() {
|
|
1859
2336
|
this.#teardownRadiusDrag();
|
|
1860
2337
|
this.#deactivateMoveCursor();
|
|
2338
|
+
this.#deactivateRotateCursor();
|
|
1861
2339
|
document.body.classList.remove("fig-lab-move-active");
|
|
2340
|
+
document.body.classList.remove("fig-lab-rotate-active");
|
|
1862
2341
|
}
|
|
1863
2342
|
|
|
1864
2343
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
@@ -2020,19 +2499,27 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2020
2499
|
this.#createSecondHandle(disabled, tooltips, handleSurface);
|
|
2021
2500
|
}
|
|
2022
2501
|
|
|
2023
|
-
this.#
|
|
2024
|
-
this.#
|
|
2025
|
-
this.#
|
|
2502
|
+
this.#setupHandleDragCursor(this.#pointHandle);
|
|
2503
|
+
this.#setupHandleDragCursor(this.#angleHandle);
|
|
2504
|
+
this.#setupHandleDragCursor(this.#secondHandle);
|
|
2026
2505
|
this.#setupEventListeners();
|
|
2027
2506
|
this.#wireHoverTooltips();
|
|
2028
2507
|
requestAnimationFrame(() => this.#syncPositions());
|
|
2029
2508
|
}
|
|
2030
2509
|
|
|
2031
|
-
#
|
|
2510
|
+
#setupHandleDragCursor(handle) {
|
|
2032
2511
|
if (!handle) return;
|
|
2033
2512
|
handle.addEventListener(
|
|
2034
2513
|
"pointerdown",
|
|
2035
|
-
(e) =>
|
|
2514
|
+
(e) => {
|
|
2515
|
+
// Hit-area (outside ring) = rotate; handle body = move/resize.
|
|
2516
|
+
const onHitArea = e.target?.classList?.contains("fig-handle-hit-area");
|
|
2517
|
+
if (onHitArea && handle.querySelector(".fig-handle-hit-area")) {
|
|
2518
|
+
this.#activateRotateCursor(e, handle);
|
|
2519
|
+
} else {
|
|
2520
|
+
this.#activateMoveCursor(e);
|
|
2521
|
+
}
|
|
2522
|
+
},
|
|
2036
2523
|
{ capture: true },
|
|
2037
2524
|
);
|
|
2038
2525
|
}
|
|
@@ -2101,6 +2588,86 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2101
2588
|
}
|
|
2102
2589
|
}
|
|
2103
2590
|
|
|
2591
|
+
#rotateDegForHandle(handle) {
|
|
2592
|
+
if (!handle) return 0;
|
|
2593
|
+
if (handle === this.#angleHandle) return this.#angle;
|
|
2594
|
+
if (!this.#hasSecondPoint) return 0;
|
|
2595
|
+
const lineDeg = this.#pointPointLineDeg();
|
|
2596
|
+
return handle === this.#pointHandle ? lineDeg + 180 : lineDeg;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
#refreshBodyRotateCursor() {
|
|
2600
|
+
if (this.#rotateCursorPointerId === null) return;
|
|
2601
|
+
const cursor = this.#rotateCursorSvg(
|
|
2602
|
+
this.#rotateDegForHandle(this.#rotateCursorHandle),
|
|
2603
|
+
);
|
|
2604
|
+
document.body.style.setProperty("--fig-lab-cursor-rotate", cursor);
|
|
2605
|
+
document.body.style.setProperty("cursor", cursor, "important");
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
#activateRotateCursor(e, handle) {
|
|
2609
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
2610
|
+
if (e?.button !== undefined && e.button !== 0) return;
|
|
2611
|
+
if (e?.isPrimary === false) return;
|
|
2612
|
+
|
|
2613
|
+
if (this.#rotateCursorPointerId === null) {
|
|
2614
|
+
this.#rotateCursorPrevBodyCursor =
|
|
2615
|
+
document.body.style.getPropertyValue("cursor");
|
|
2616
|
+
this.#rotateCursorPrevBodyCursorPriority =
|
|
2617
|
+
document.body.style.getPropertyPriority("cursor");
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
this.#rotateCursorPointerId = e?.pointerId ?? -1;
|
|
2621
|
+
this.#rotateCursorHandle = handle;
|
|
2622
|
+
document.body.classList.add("fig-lab-rotate-active");
|
|
2623
|
+
this.#refreshBodyRotateCursor();
|
|
2624
|
+
|
|
2625
|
+
if (!this.#boundRotateCursorEnd) {
|
|
2626
|
+
this.#boundRotateCursorEnd = (event) => {
|
|
2627
|
+
if (
|
|
2628
|
+
event?.pointerId !== undefined &&
|
|
2629
|
+
this.#rotateCursorPointerId !== null &&
|
|
2630
|
+
event.pointerId !== this.#rotateCursorPointerId
|
|
2631
|
+
) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
if (event?.type === "blur") {
|
|
2635
|
+
this.#deactivateRotateCursor();
|
|
2636
|
+
} else {
|
|
2637
|
+
requestAnimationFrame(() => this.#deactivateRotateCursor());
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
window.addEventListener("pointerup", this.#boundRotateCursorEnd);
|
|
2643
|
+
window.addEventListener("pointercancel", this.#boundRotateCursorEnd);
|
|
2644
|
+
window.addEventListener("blur", this.#boundRotateCursorEnd);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
#deactivateRotateCursor() {
|
|
2648
|
+
if (this.#rotateCursorPointerId === null) return;
|
|
2649
|
+
document.body.classList.remove("fig-lab-rotate-active");
|
|
2650
|
+
document.body.style.removeProperty("--fig-lab-cursor-rotate");
|
|
2651
|
+
if (this.#rotateCursorPrevBodyCursor) {
|
|
2652
|
+
document.body.style.setProperty(
|
|
2653
|
+
"cursor",
|
|
2654
|
+
this.#rotateCursorPrevBodyCursor,
|
|
2655
|
+
this.#rotateCursorPrevBodyCursorPriority,
|
|
2656
|
+
);
|
|
2657
|
+
} else {
|
|
2658
|
+
document.body.style.removeProperty("cursor");
|
|
2659
|
+
}
|
|
2660
|
+
this.#rotateCursorPointerId = null;
|
|
2661
|
+
this.#rotateCursorHandle = null;
|
|
2662
|
+
this.#rotateCursorPrevBodyCursor = "";
|
|
2663
|
+
this.#rotateCursorPrevBodyCursorPriority = "";
|
|
2664
|
+
if (this.#boundRotateCursorEnd) {
|
|
2665
|
+
window.removeEventListener("pointerup", this.#boundRotateCursorEnd);
|
|
2666
|
+
window.removeEventListener("pointercancel", this.#boundRotateCursorEnd);
|
|
2667
|
+
window.removeEventListener("blur", this.#boundRotateCursorEnd);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2104
2671
|
#wireHoverTooltip(target, getTooltip, getText, isDraggingRef) {
|
|
2105
2672
|
if (!target) return;
|
|
2106
2673
|
const shouldSuppress = () => !!isDraggingRef?.();
|
|
@@ -2401,11 +2968,27 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2401
2968
|
return `url("data:image/svg+xml,%3Csvg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='rotate(${r} 16 16)'%3E%3Cg filter='url(%23f)'%3E%3Cpath d='M12.5607 22.4393L12.0216 21.9002C17.1558 21.2216 21.2216 17.1558 21.9002 12.0216L22.4393 12.5607C23.0251 13.1464 23.9749 13.1464 24.5607 12.5607C25.1464 11.9749 25.1464 11.0251 24.5607 10.4393L21.5607 7.43934C20.9749 6.85355 20.0251 6.85355 19.4393 7.43934L16.4393 10.4393C15.8536 11.0251 15.8536 11.9749 16.4393 12.5607C17.0251 13.1464 17.9749 13.1464 18.5607 12.5607L18.8056 12.3157C18.1013 15.5527 15.5527 18.1013 12.3157 18.8056L12.5607 18.5607C13.1464 17.9749 13.1464 17.0251 12.5607 16.4393C11.9749 15.8536 11.0251 15.8536 10.4393 16.4393L7.43934 19.4393C6.85356 20.0251 6.85356 20.9749 7.43934 21.5607L10.4393 24.5607C11.0251 25.1464 11.9749 25.1464 12.5607 24.5607C13.1464 23.9749 13.1464 23.0251 12.5607 22.4393Z' fill='white'/%3E%3C/g%3E%3Cpath d='M23.8536 11.8536C23.6583 12.0488 23.3417 12.0488 23.1464 11.8536L21 9.70711V10.5C21 16.299 16.299 21 10.5 21H9.70711L11.8536 23.1464C12.0488 23.3417 12.0488 23.6583 11.8536 23.8536C11.6583 24.0488 11.3417 24.0488 11.1464 23.8536L8.14645 20.8536C7.95119 20.6583 7.95119 20.3417 8.14645 20.1464L11.1464 17.1464C11.3417 16.9512 11.6583 16.9512 11.8536 17.1464C12.0488 17.3417 12.0488 17.6583 11.8536 17.8536L9.70711 20H10.5C15.7467 20 20 15.7467 20 10.5V9.70711L17.8536 11.8536C17.6583 12.0488 17.3417 12.0488 17.1464 11.8536C16.9512 11.6583 16.9512 11.3417 17.1464 11.1464L20.1464 8.14645C20.3417 7.95119 20.6583 7.95119 20.8536 8.14645L23.8536 11.1464C24.0488 11.3417 24.0488 11.6583 23.8536 11.8536Z' fill='black'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='f' x='4' y='5' width='24' height='24' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeFlood flood-opacity='0' result='a'/%3E%3CfeColorMatrix in='SourceAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0' result='b'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='1.5'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0'/%3E%3CfeBlend in2='a' result='c'/%3E%3CfeBlend in='SourceGraphic' in2='c'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E") 16 16, pointer`;
|
|
2402
2969
|
}
|
|
2403
2970
|
|
|
2971
|
+
#setHitCursor(el, cursor) {
|
|
2972
|
+
if (!el) return;
|
|
2973
|
+
// !important — fig-canvas-control hover rules override plain inline cursor.
|
|
2974
|
+
el.style.setProperty("cursor", cursor, "important");
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
#applyRotateCursor(handle, deg) {
|
|
2978
|
+
if (!handle) return;
|
|
2979
|
+
// Rotate only on the outside hit-area ring — handle body uses normal point cursor.
|
|
2980
|
+
const hitArea = handle.querySelector(".fig-handle-hit-area");
|
|
2981
|
+
if (!hitArea) return;
|
|
2982
|
+
hitArea.setAttribute("data-cursor", "rotate");
|
|
2983
|
+
this.#setHitCursor(hitArea, this.#rotateCursorSvg(deg));
|
|
2984
|
+
if (this.#rotateCursorHandle === handle) {
|
|
2985
|
+
this.#refreshBodyRotateCursor();
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2404
2989
|
#syncAngleCursor() {
|
|
2405
2990
|
if (!this.#angleHandle || !this.#hasAngle) return;
|
|
2406
|
-
|
|
2407
|
-
if (!hitArea) return;
|
|
2408
|
-
hitArea.style.cursor = this.#rotateCursorSvg(this.#angle);
|
|
2991
|
+
this.#applyRotateCursor(this.#angleHandle, this.#angle);
|
|
2409
2992
|
}
|
|
2410
2993
|
|
|
2411
2994
|
#pointPointLineDeg() {
|
|
@@ -2415,13 +2998,11 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2415
2998
|
#syncPointPointCursors() {
|
|
2416
2999
|
if (!this.#hasSecondPoint) return;
|
|
2417
3000
|
const deg = this.#pointPointLineDeg();
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
setHitCursor(this.#pointHandle, deg + 180);
|
|
2424
|
-
setHitCursor(this.#secondHandle, deg);
|
|
3001
|
+
// Handle body: leave cursor to CSS (same as default point handle).
|
|
3002
|
+
this.#pointHandle?.style.removeProperty("cursor");
|
|
3003
|
+
this.#secondHandle?.style.removeProperty("cursor");
|
|
3004
|
+
this.#applyRotateCursor(this.#pointHandle, deg + 180);
|
|
3005
|
+
this.#applyRotateCursor(this.#secondHandle, deg);
|
|
2425
3006
|
}
|
|
2426
3007
|
|
|
2427
3008
|
#positionHandle(handle, xPct, yPct, rect) {
|
|
@@ -2795,7 +3376,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2795
3376
|
(Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx) *
|
|
2796
3377
|
180) /
|
|
2797
3378
|
Math.PI;
|
|
2798
|
-
circle
|
|
3379
|
+
this.#setHitCursor(circle, this.#resizeCursorSvg(deg));
|
|
2799
3380
|
});
|
|
2800
3381
|
const onDown = (e) => {
|
|
2801
3382
|
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
@@ -5387,7 +5968,12 @@ class FigSelect extends HTMLElement {
|
|
|
5387
5968
|
|
|
5388
5969
|
const selectedOffsetX = optionTextRect.left - popupRect.left;
|
|
5389
5970
|
const selectedOffsetY = optionTextRect.top - popupRect.top;
|
|
5390
|
-
|
|
5971
|
+
const full = figLabBooleanAttribute(this, "full");
|
|
5972
|
+
// [full]: pin menu to host width/edges. Otherwise overlay selected
|
|
5973
|
+
// option text on the trigger label (blend-mode style).
|
|
5974
|
+
let left = full
|
|
5975
|
+
? this.getBoundingClientRect().left
|
|
5976
|
+
: labelRect.left - selectedOffsetX;
|
|
5391
5977
|
let top = labelRect.top - selectedOffsetY;
|
|
5392
5978
|
|
|
5393
5979
|
// Keep the whole menu in-view when aligning over the selected option
|
|
@@ -5768,6 +6354,7 @@ class FigSelect extends HTMLElement {
|
|
|
5768
6354
|
const full = figLabBooleanAttribute(this, "full");
|
|
5769
6355
|
|
|
5770
6356
|
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
6357
|
+
// [full]: lock to host. Otherwise content-sized with host as min and 20rem max.
|
|
5771
6358
|
if (full) {
|
|
5772
6359
|
this.#popup.style.setProperty("width", `${anchorWidth}px`, "important");
|
|
5773
6360
|
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|