@rogieking/figui3 8.9.19 → 8.9.21
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/.cursor/skills/fig-lab/SKILL.md +4 -3
- package/.cursor/skills/propkit/SKILL.md +1 -1
- package/README.md +26 -4
- package/components.css +40 -10
- package/dist/components.css +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +2 -2
- package/dist/fig.css +1 -1
- package/dist/fig.js +4 -4
- package/fig-editor.js +125 -5
- package/fig-lab.css +46 -8
- package/fig-lab.js +497 -2
- package/fig.js +29 -8
- package/package.json +1 -1
package/fig-lab.js
CHANGED
|
@@ -724,6 +724,121 @@ function figLabSolidFillValue(raw) {
|
|
|
724
724
|
return JSON.stringify({ type: "solid", color, alpha });
|
|
725
725
|
}
|
|
726
726
|
|
|
727
|
+
function figLabCssUrl(url) {
|
|
728
|
+
return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function figLabParseFillValue(raw) {
|
|
732
|
+
if (raw && typeof raw === "object") return raw;
|
|
733
|
+
const text = String(raw ?? "").trim();
|
|
734
|
+
if (!text) return { type: "solid", color: "#D9D9D9", alpha: 1 };
|
|
735
|
+
if (text.startsWith("{")) {
|
|
736
|
+
try {
|
|
737
|
+
const parsed = JSON.parse(text);
|
|
738
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
739
|
+
} catch {}
|
|
740
|
+
}
|
|
741
|
+
if (text.startsWith("#")) {
|
|
742
|
+
const { color, alpha } = figLabParseSolidColor(text);
|
|
743
|
+
return { type: "solid", color, alpha };
|
|
744
|
+
}
|
|
745
|
+
return { type: "solid", color: "#D9D9D9", alpha: 1 };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function figLabSerializeFillValue(fill) {
|
|
749
|
+
if (fill == null) return "";
|
|
750
|
+
return typeof fill === "string" ? fill : JSON.stringify(fill);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function figLabGradientCss(gradient) {
|
|
754
|
+
if (!gradient) return "";
|
|
755
|
+
const stops = [...(gradient.stops || [])]
|
|
756
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
757
|
+
.map((stop) => `${stop.color} ${stop.position ?? 0}%`)
|
|
758
|
+
.join(", ");
|
|
759
|
+
if (!stops) return "";
|
|
760
|
+
switch (gradient.type) {
|
|
761
|
+
case "radial":
|
|
762
|
+
return `radial-gradient(circle, ${stops})`;
|
|
763
|
+
case "angular":
|
|
764
|
+
return `conic-gradient(from ${gradient.angle || 0}deg, ${stops})`;
|
|
765
|
+
default:
|
|
766
|
+
return `linear-gradient(${gradient.angle ?? 180}deg, ${stops})`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function figLabFillSwatchBackground(fill, slot) {
|
|
771
|
+
switch (fill?.type) {
|
|
772
|
+
case "solid":
|
|
773
|
+
return fill.color || "";
|
|
774
|
+
case "gradient":
|
|
775
|
+
return fill.css || figLabGradientCss(fill.gradient) || "";
|
|
776
|
+
case "image":
|
|
777
|
+
return fill.image?.url ? figLabCssUrl(fill.image.url) : "";
|
|
778
|
+
case "video":
|
|
779
|
+
return fill.video?.poster ? figLabCssUrl(fill.video.poster) : "";
|
|
780
|
+
case "webcam":
|
|
781
|
+
return fill.webcam?.snapshot ? figLabCssUrl(fill.webcam.snapshot) : "";
|
|
782
|
+
default:
|
|
783
|
+
return (
|
|
784
|
+
fill?.swatchBackground ||
|
|
785
|
+
fill?.background ||
|
|
786
|
+
fill?.css ||
|
|
787
|
+
slot?.getAttribute("swatch-background") ||
|
|
788
|
+
""
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function figLabFillSwatchMedia(fill) {
|
|
794
|
+
if (!fill || typeof fill !== "object") return null;
|
|
795
|
+
if (fill.type === "image") return fill.image;
|
|
796
|
+
if (fill.type === "video") return fill.video;
|
|
797
|
+
if (fill.type === "webcam") return fill.webcam;
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function figLabFillSwatchSizing(fill) {
|
|
802
|
+
const media = figLabFillSwatchMedia(fill);
|
|
803
|
+
const scale = Number(media?.scale);
|
|
804
|
+
switch (media?.scaleMode) {
|
|
805
|
+
case "fit":
|
|
806
|
+
return { size: "contain", position: "center" };
|
|
807
|
+
case "tile":
|
|
808
|
+
return {
|
|
809
|
+
size: `${Number.isFinite(scale) ? scale : 50}%`,
|
|
810
|
+
position: "top left",
|
|
811
|
+
};
|
|
812
|
+
case "fill":
|
|
813
|
+
case "crop":
|
|
814
|
+
return { size: "cover", position: "center" };
|
|
815
|
+
default:
|
|
816
|
+
return fill?.type === "webcam" || fill?.type === "image" || fill?.type === "video"
|
|
817
|
+
? { size: "cover", position: "center" }
|
|
818
|
+
: null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function figLabFillSwatchAlpha(fill) {
|
|
823
|
+
switch (fill?.type) {
|
|
824
|
+
case "solid": {
|
|
825
|
+
if (Number.isFinite(fill.alpha)) return fill.alpha;
|
|
826
|
+
if (Number.isFinite(fill.opacity)) return fill.opacity / 100;
|
|
827
|
+
return 1;
|
|
828
|
+
}
|
|
829
|
+
case "gradient":
|
|
830
|
+
return fill.gradient?.opacity ?? fill.opacity ?? 1;
|
|
831
|
+
case "image":
|
|
832
|
+
return fill.image?.opacity ?? 1;
|
|
833
|
+
case "video":
|
|
834
|
+
return fill.video?.opacity ?? 1;
|
|
835
|
+
case "webcam":
|
|
836
|
+
return fill.webcam?.opacity ?? 1;
|
|
837
|
+
default:
|
|
838
|
+
return fill?.opacity ?? fill?.alpha ?? 1;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
727
842
|
/* Field + Color wrapper */
|
|
728
843
|
class PropskitColor extends HTMLElement {
|
|
729
844
|
#field = null;
|
|
@@ -807,6 +922,7 @@ class PropskitColor extends HTMLElement {
|
|
|
807
922
|
const label = customLabel || document.createElement("label");
|
|
808
923
|
const picker = document.createElement("fig-fill-picker");
|
|
809
924
|
const swatch = document.createElement("fig-swatch");
|
|
925
|
+
picker.anchorElement = this;
|
|
810
926
|
picker.append(swatch);
|
|
811
927
|
swatch.setAttribute("tabindex", "0");
|
|
812
928
|
for (const node of initialChildren) {
|
|
@@ -996,13 +1112,17 @@ class PropskitColor extends HTMLElement {
|
|
|
996
1112
|
|
|
997
1113
|
#handleClick(event) {
|
|
998
1114
|
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1115
|
+
if (event.target instanceof Element && event.target.closest("fig-menu")) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
999
1118
|
if (
|
|
1000
1119
|
event.target instanceof Element &&
|
|
1001
|
-
event.target.closest("fig-fill-picker, fig-swatch
|
|
1120
|
+
event.target.closest("fig-fill-picker, fig-swatch")
|
|
1002
1121
|
) {
|
|
1003
1122
|
return;
|
|
1004
1123
|
}
|
|
1005
1124
|
this.focus();
|
|
1125
|
+
this.#input?.open?.();
|
|
1006
1126
|
}
|
|
1007
1127
|
|
|
1008
1128
|
get value() {
|
|
@@ -1076,6 +1196,375 @@ class PropskitColor extends HTMLElement {
|
|
|
1076
1196
|
}
|
|
1077
1197
|
figLabDefineElement("propskit-color", PropskitColor);
|
|
1078
1198
|
|
|
1199
|
+
/* Field + Fill wrapper — color-style swatch that opens the full fill picker */
|
|
1200
|
+
class PropskitFill extends HTMLElement {
|
|
1201
|
+
#field = null;
|
|
1202
|
+
#label = null;
|
|
1203
|
+
#input = null;
|
|
1204
|
+
#swatch = null;
|
|
1205
|
+
#hasCustomLabel = false;
|
|
1206
|
+
#observer = null;
|
|
1207
|
+
#managedInputAttrs = new Set();
|
|
1208
|
+
#boundHandleInput = null;
|
|
1209
|
+
#boundHandleChange = null;
|
|
1210
|
+
#boundHandleClick = this.#handleClick.bind(this);
|
|
1211
|
+
#initialValue = null;
|
|
1212
|
+
|
|
1213
|
+
static get observedAttributes() {
|
|
1214
|
+
return ["label", "direction", "aria-label"];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
connectedCallback() {
|
|
1218
|
+
if (!this.#field) this.#initialize();
|
|
1219
|
+
this.#syncField();
|
|
1220
|
+
this.#syncInputAttributes();
|
|
1221
|
+
this.#bindInputEvents();
|
|
1222
|
+
if (this.#initialValue === null) {
|
|
1223
|
+
this.#initialValue =
|
|
1224
|
+
this.getAttribute("default") ?? this.getAttribute("value") ?? "";
|
|
1225
|
+
}
|
|
1226
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
1227
|
+
this.addEventListener("click", this.#boundHandleClick);
|
|
1228
|
+
figLabConnectPropskitResetMenu(this);
|
|
1229
|
+
|
|
1230
|
+
if (!this.#observer) {
|
|
1231
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
1232
|
+
let syncField = false;
|
|
1233
|
+
let syncInput = false;
|
|
1234
|
+
|
|
1235
|
+
for (const mutation of mutations) {
|
|
1236
|
+
if (mutation.type !== "attributes") continue;
|
|
1237
|
+
if (
|
|
1238
|
+
mutation.attributeName === "label" ||
|
|
1239
|
+
mutation.attributeName === "direction" ||
|
|
1240
|
+
mutation.attributeName === "aria-label"
|
|
1241
|
+
) {
|
|
1242
|
+
syncField = true;
|
|
1243
|
+
} else {
|
|
1244
|
+
syncInput = true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (syncField) this.#syncField();
|
|
1249
|
+
if (syncInput) this.#syncInputAttributes();
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
this.#observer.observe(this, { attributes: true });
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
disconnectedCallback() {
|
|
1257
|
+
this.#observer?.disconnect();
|
|
1258
|
+
this.#unbindInputEvents();
|
|
1259
|
+
this.removeEventListener("click", this.#boundHandleClick);
|
|
1260
|
+
figLabDisconnectPropskitResetMenu(this);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1264
|
+
if (oldValue === newValue || !this.#field) return;
|
|
1265
|
+
if (name === "label" || name === "direction" || name === "aria-label") {
|
|
1266
|
+
this.#syncField();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
#initialize() {
|
|
1271
|
+
const initialChildren = Array.from(this.childNodes).filter(
|
|
1272
|
+
(node) =>
|
|
1273
|
+
node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
|
|
1274
|
+
);
|
|
1275
|
+
const customLabel = initialChildren.find(
|
|
1276
|
+
(node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
|
|
1277
|
+
);
|
|
1278
|
+
const field = document.createElement("fig-field");
|
|
1279
|
+
const label = customLabel || document.createElement("label");
|
|
1280
|
+
const picker = document.createElement("fig-fill-picker");
|
|
1281
|
+
const swatch = document.createElement("fig-swatch");
|
|
1282
|
+
picker.anchorElement = this;
|
|
1283
|
+
picker.append(swatch);
|
|
1284
|
+
swatch.setAttribute("tabindex", "0");
|
|
1285
|
+
for (const node of initialChildren) {
|
|
1286
|
+
if (node !== customLabel) picker.appendChild(node);
|
|
1287
|
+
}
|
|
1288
|
+
field.append(label, picker);
|
|
1289
|
+
this.#field = field;
|
|
1290
|
+
this.#label = label;
|
|
1291
|
+
this.#input = picker;
|
|
1292
|
+
this.#swatch = swatch;
|
|
1293
|
+
this.#hasCustomLabel = Boolean(customLabel);
|
|
1294
|
+
this.replaceChildren(field);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
#syncField() {
|
|
1298
|
+
if (!this.#field || !this.#label || !this.#input) return;
|
|
1299
|
+
const hasLabelAttr = this.hasAttribute("label");
|
|
1300
|
+
const rawLabel = this.getAttribute("label");
|
|
1301
|
+
const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
|
|
1302
|
+
|
|
1303
|
+
if (isBlankLabel) {
|
|
1304
|
+
this.#label.remove();
|
|
1305
|
+
} else {
|
|
1306
|
+
if (!this.#hasCustomLabel) {
|
|
1307
|
+
this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
|
|
1308
|
+
}
|
|
1309
|
+
if (this.#label.parentElement !== this.#field) {
|
|
1310
|
+
this.#field.prepend(this.#label);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
this.#field.setAttribute(
|
|
1315
|
+
"direction",
|
|
1316
|
+
this.getAttribute("direction") || "horizontal",
|
|
1317
|
+
);
|
|
1318
|
+
this.#input.setAttribute(
|
|
1319
|
+
"aria-label",
|
|
1320
|
+
this.getAttribute("aria-label") ||
|
|
1321
|
+
this.#label.textContent?.trim() ||
|
|
1322
|
+
"Fill",
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
#parsedHostFill() {
|
|
1327
|
+
return figLabParseFillValue(this.getAttribute("value"));
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
#pickerValueFromHost() {
|
|
1331
|
+
return figLabSerializeFillValue(this.#parsedHostFill());
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
#customModeSlot() {
|
|
1335
|
+
const type = this.#parsedHostFill()?.type;
|
|
1336
|
+
if (!type) return null;
|
|
1337
|
+
return this.querySelector(`[slot="mode-${type}"]`);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
#livePickerFill() {
|
|
1341
|
+
const live = this.#input?.value;
|
|
1342
|
+
if (live && typeof live === "object") return live;
|
|
1343
|
+
if (typeof live === "string" && live) return figLabParseFillValue(live);
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
#captureFill(fill) {
|
|
1348
|
+
const live = this.#livePickerFill();
|
|
1349
|
+
if (!fill || typeof fill !== "object") return fill;
|
|
1350
|
+
if (fill.type !== "webcam") return fill;
|
|
1351
|
+
const snapshot = fill.webcam?.snapshot || live?.webcam?.snapshot;
|
|
1352
|
+
if (!snapshot || fill.webcam?.snapshot) return fill;
|
|
1353
|
+
return {
|
|
1354
|
+
...fill,
|
|
1355
|
+
webcam: {
|
|
1356
|
+
...(live?.webcam && typeof live.webcam === "object" ? live.webcam : {}),
|
|
1357
|
+
...fill.webcam,
|
|
1358
|
+
snapshot,
|
|
1359
|
+
},
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
#syncSwatch(fill = this.#parsedHostFill()) {
|
|
1364
|
+
if (!this.#swatch) return;
|
|
1365
|
+
const next = this.#captureFill(fill);
|
|
1366
|
+
let background = figLabFillSwatchBackground(next, this.#customModeSlot());
|
|
1367
|
+
if (!background) {
|
|
1368
|
+
background = figLabFillSwatchBackground(
|
|
1369
|
+
this.#livePickerFill(),
|
|
1370
|
+
this.#customModeSlot(),
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
if (!background) {
|
|
1374
|
+
const painted = this.#swatch.getAttribute("background") ?? "";
|
|
1375
|
+
if (painted.startsWith("url(")) background = painted;
|
|
1376
|
+
}
|
|
1377
|
+
this.#swatch.setAttribute("background", background);
|
|
1378
|
+
this.#swatch.setAttribute("alpha", String(figLabFillSwatchAlpha(next)));
|
|
1379
|
+
const sizing = figLabFillSwatchSizing(next || fill);
|
|
1380
|
+
if (sizing) {
|
|
1381
|
+
this.#swatch.style.setProperty("--swatch-bg-size", sizing.size);
|
|
1382
|
+
this.#swatch.style.setProperty("--swatch-bg-position", sizing.position);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
#getForwardedInputAttrNames() {
|
|
1387
|
+
const reserved = new Set([
|
|
1388
|
+
"label",
|
|
1389
|
+
"direction",
|
|
1390
|
+
"oninput",
|
|
1391
|
+
"onchange",
|
|
1392
|
+
"class",
|
|
1393
|
+
"style",
|
|
1394
|
+
"id",
|
|
1395
|
+
"size",
|
|
1396
|
+
"aria-label",
|
|
1397
|
+
"text",
|
|
1398
|
+
"default",
|
|
1399
|
+
"value",
|
|
1400
|
+
]);
|
|
1401
|
+
return this.getAttributeNames().filter(
|
|
1402
|
+
(name) => !reserved.has(name) && !name.startsWith("data-"),
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
#syncInputAttributes() {
|
|
1407
|
+
if (!this.#input) return;
|
|
1408
|
+
const inputAttrs = this.#getForwardedInputAttrNames();
|
|
1409
|
+
const nextManaged = new Set(inputAttrs);
|
|
1410
|
+
|
|
1411
|
+
for (const attrName of this.#managedInputAttrs) {
|
|
1412
|
+
if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
|
|
1413
|
+
}
|
|
1414
|
+
for (const attrName of inputAttrs) {
|
|
1415
|
+
this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const nextJson = this.#pickerValueFromHost();
|
|
1419
|
+
if (this.#input.getAttribute("value") !== nextJson) {
|
|
1420
|
+
this.#input.setAttribute("value", nextJson);
|
|
1421
|
+
}
|
|
1422
|
+
this.#syncSwatch();
|
|
1423
|
+
this.#managedInputAttrs = nextManaged;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
#bindInputEvents() {
|
|
1427
|
+
if (!this.#input) return;
|
|
1428
|
+
this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
|
|
1429
|
+
this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
|
|
1430
|
+
this.#input.addEventListener("input", this.#boundHandleInput);
|
|
1431
|
+
this.#input.addEventListener("change", this.#boundHandleChange);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
#unbindInputEvents() {
|
|
1435
|
+
if (!this.#input) return;
|
|
1436
|
+
if (this.#boundHandleInput) {
|
|
1437
|
+
this.#input.removeEventListener("input", this.#boundHandleInput);
|
|
1438
|
+
}
|
|
1439
|
+
if (this.#boundHandleChange) {
|
|
1440
|
+
this.#input.removeEventListener("change", this.#boundHandleChange);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
#fillFromEvent(event) {
|
|
1445
|
+
const detail =
|
|
1446
|
+
event instanceof CustomEvent && event.detail !== undefined
|
|
1447
|
+
? event.detail
|
|
1448
|
+
: undefined;
|
|
1449
|
+
if (detail && typeof detail === "object") return detail;
|
|
1450
|
+
if (typeof detail === "string" && detail) {
|
|
1451
|
+
return figLabParseFillValue(detail);
|
|
1452
|
+
}
|
|
1453
|
+
const live = this.#input?.value;
|
|
1454
|
+
if (live && typeof live === "object") return live;
|
|
1455
|
+
if (typeof live === "string" && live) return figLabParseFillValue(live);
|
|
1456
|
+
return this.#parsedHostFill();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
#forwardInputEvent(type, event) {
|
|
1460
|
+
event.stopImmediatePropagation();
|
|
1461
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1462
|
+
const fill = this.#captureFill(this.#fillFromEvent(event));
|
|
1463
|
+
const serialized = figLabSerializeFillValue(fill);
|
|
1464
|
+
this.setAttribute("value", serialized);
|
|
1465
|
+
if (this.#input && this.#input.getAttribute("value") !== serialized) {
|
|
1466
|
+
this.#input.setAttribute("value", serialized);
|
|
1467
|
+
}
|
|
1468
|
+
this.#syncSwatch(fill);
|
|
1469
|
+
this.dispatchEvent(
|
|
1470
|
+
new CustomEvent(type, {
|
|
1471
|
+
detail: fill,
|
|
1472
|
+
bubbles: true,
|
|
1473
|
+
cancelable: true,
|
|
1474
|
+
composed: true,
|
|
1475
|
+
}),
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
#handleClick(event) {
|
|
1480
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1481
|
+
if (event.target instanceof Element && event.target.closest("fig-menu")) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (
|
|
1485
|
+
event.target instanceof Element &&
|
|
1486
|
+
event.target.closest("fig-fill-picker, fig-swatch")
|
|
1487
|
+
) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
this.focus();
|
|
1491
|
+
this.#input?.open?.();
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
get value() {
|
|
1495
|
+
return (
|
|
1496
|
+
this.getAttribute("value") ??
|
|
1497
|
+
figLabSerializeFillValue(this.#input?.value) ??
|
|
1498
|
+
""
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
set value(nextValue) {
|
|
1503
|
+
if (nextValue === null || nextValue === undefined || nextValue === "") {
|
|
1504
|
+
this.removeAttribute("value");
|
|
1505
|
+
this.#input?.removeAttribute("value");
|
|
1506
|
+
this.#syncSwatch({ type: "solid", color: "", alpha: 1 });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const serialized = figLabSerializeFillValue(
|
|
1510
|
+
typeof nextValue === "string" ? figLabParseFillValue(nextValue) : nextValue,
|
|
1511
|
+
);
|
|
1512
|
+
this.setAttribute("value", serialized);
|
|
1513
|
+
this.#forceInputValue(serialized);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
#forceInputValue(next) {
|
|
1517
|
+
if (!this.#input) return;
|
|
1518
|
+
const json = figLabSerializeFillValue(figLabParseFillValue(next));
|
|
1519
|
+
if (this.#input.getAttribute("value") === json) {
|
|
1520
|
+
this.#input.removeAttribute("value");
|
|
1521
|
+
}
|
|
1522
|
+
this.#input.setAttribute("value", json);
|
|
1523
|
+
this.#syncSwatch(figLabParseFillValue(json));
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
#defaultValue() {
|
|
1527
|
+
return this.getAttribute("default") ?? this.#initialValue ?? "";
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
get defaultValue() {
|
|
1531
|
+
return String(this.#defaultValue());
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
get isDefault() {
|
|
1535
|
+
return figLabPropskitJsonValuesEqual(this.value, this.defaultValue);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
resetToDefault() {
|
|
1539
|
+
const next = this.defaultValue;
|
|
1540
|
+
this.setAttribute("value", next);
|
|
1541
|
+
this.#forceInputValue(next);
|
|
1542
|
+
const fill = figLabParseFillValue(next);
|
|
1543
|
+
this.dispatchEvent(
|
|
1544
|
+
new CustomEvent("input", {
|
|
1545
|
+
detail: fill,
|
|
1546
|
+
bubbles: true,
|
|
1547
|
+
cancelable: true,
|
|
1548
|
+
composed: true,
|
|
1549
|
+
}),
|
|
1550
|
+
);
|
|
1551
|
+
this.dispatchEvent(
|
|
1552
|
+
new CustomEvent("change", {
|
|
1553
|
+
detail: fill,
|
|
1554
|
+
bubbles: true,
|
|
1555
|
+
cancelable: true,
|
|
1556
|
+
composed: true,
|
|
1557
|
+
}),
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
focus(options) {
|
|
1562
|
+
const swatch = this.querySelector("fig-swatch");
|
|
1563
|
+
if (swatch instanceof HTMLElement) swatch.focus(options);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
figLabDefineElement("propskit-fill", PropskitFill);
|
|
1567
|
+
|
|
1079
1568
|
/* Field + Gradient wrapper */
|
|
1080
1569
|
class PropskitGradient extends HTMLElement {
|
|
1081
1570
|
#field = null;
|
|
@@ -1159,6 +1648,7 @@ class PropskitGradient extends HTMLElement {
|
|
|
1159
1648
|
const field = document.createElement("fig-field");
|
|
1160
1649
|
const label = customLabel || document.createElement("label");
|
|
1161
1650
|
const input = document.createElement("fig-input-gradient");
|
|
1651
|
+
input.anchorElement = this;
|
|
1162
1652
|
|
|
1163
1653
|
field.append(label, input);
|
|
1164
1654
|
this.#field = field;
|
|
@@ -1286,13 +1776,17 @@ class PropskitGradient extends HTMLElement {
|
|
|
1286
1776
|
|
|
1287
1777
|
#handleClick(event) {
|
|
1288
1778
|
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1779
|
+
if (event.target instanceof Element && event.target.closest("fig-menu")) {
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1289
1782
|
if (
|
|
1290
1783
|
event.target instanceof Element &&
|
|
1291
|
-
event.target.closest("fig-input-gradient, fig-
|
|
1784
|
+
event.target.closest("fig-input-gradient, fig-fill-picker, fig-swatch")
|
|
1292
1785
|
) {
|
|
1293
1786
|
return;
|
|
1294
1787
|
}
|
|
1295
1788
|
this.focus();
|
|
1789
|
+
this.#input?.querySelector?.("fig-fill-picker")?.open?.();
|
|
1296
1790
|
}
|
|
1297
1791
|
|
|
1298
1792
|
get value() {
|
|
@@ -3718,6 +4212,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
3718
4212
|
|
|
3719
4213
|
static #CONTROL_SELECTOR = [
|
|
3720
4214
|
"propskit-color",
|
|
4215
|
+
"propskit-fill",
|
|
3721
4216
|
"propskit-gradient",
|
|
3722
4217
|
"propskit-number",
|
|
3723
4218
|
"propskit-position",
|
package/fig.js
CHANGED
|
@@ -9055,8 +9055,11 @@ class FigInputFill extends HTMLElement {
|
|
|
9055
9055
|
break;
|
|
9056
9056
|
case "webcam":
|
|
9057
9057
|
if (parsed.webcam && typeof parsed.webcam === "object") {
|
|
9058
|
-
const { stream: _ignored, ...rest } = parsed.webcam;
|
|
9058
|
+
const { stream: _ignored, snapshot, ...rest } = parsed.webcam;
|
|
9059
9059
|
this.#webcam = { ...this.#webcam, ...rest };
|
|
9060
|
+
if (typeof snapshot === "string" && snapshot) {
|
|
9061
|
+
this.#webcam.snapshot = snapshot;
|
|
9062
|
+
}
|
|
9060
9063
|
} else if (parsed.image?.url) {
|
|
9061
9064
|
this.#webcam.snapshot = parsed.image.url;
|
|
9062
9065
|
if (parsed.image.scaleMode)
|
|
@@ -9145,10 +9148,12 @@ class FigInputFill extends HTMLElement {
|
|
|
9145
9148
|
return this.#image.url ? figCssUrl(this.#image.url) : "#D9D9D9";
|
|
9146
9149
|
case "video":
|
|
9147
9150
|
return this.#video.poster ? figCssUrl(this.#video.poster) : "#D9D9D9";
|
|
9148
|
-
case "webcam":
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9151
|
+
case "webcam": {
|
|
9152
|
+
const snapshot =
|
|
9153
|
+
this.#webcam.snapshot ||
|
|
9154
|
+
this.#fillPicker?.value?.webcam?.snapshot;
|
|
9155
|
+
return snapshot ? figCssUrl(snapshot) : "";
|
|
9156
|
+
}
|
|
9152
9157
|
default:
|
|
9153
9158
|
return this.#customSwatchBackground();
|
|
9154
9159
|
}
|
|
@@ -9419,11 +9424,18 @@ class FigInputFill extends HTMLElement {
|
|
|
9419
9424
|
break;
|
|
9420
9425
|
case "webcam":
|
|
9421
9426
|
if (detail.webcam) {
|
|
9422
|
-
const { stream: _ignored, ...rest } = detail.webcam;
|
|
9427
|
+
const { stream: _ignored, snapshot, ...rest } = detail.webcam;
|
|
9423
9428
|
this.#webcam = { ...this.#webcam, ...rest };
|
|
9429
|
+
if (typeof snapshot === "string" && snapshot) {
|
|
9430
|
+
this.#webcam.snapshot = snapshot;
|
|
9431
|
+
}
|
|
9424
9432
|
} else if (detail.image?.url) {
|
|
9425
9433
|
this.#webcam.snapshot = detail.image.url;
|
|
9426
9434
|
}
|
|
9435
|
+
if (!this.#webcam.snapshot) {
|
|
9436
|
+
const fromPicker = this.#fillPicker?.value?.webcam?.snapshot;
|
|
9437
|
+
if (fromPicker) this.#webcam.snapshot = fromPicker;
|
|
9438
|
+
}
|
|
9427
9439
|
break;
|
|
9428
9440
|
default: {
|
|
9429
9441
|
const { type: _type, colorSpace: _colorSpace, ...rest } = detail;
|
|
@@ -9686,7 +9698,15 @@ class FigInputFill extends HTMLElement {
|
|
|
9686
9698
|
#syncSwatch() {
|
|
9687
9699
|
const swatch = this.#fillPicker?.querySelector("fig-swatch");
|
|
9688
9700
|
if (!swatch) return;
|
|
9689
|
-
|
|
9701
|
+
const background = this.#fillPickerSwatchBackground();
|
|
9702
|
+
if (this.#fillType === "webcam" && !background) {
|
|
9703
|
+
const current = swatch.getAttribute("background") || "";
|
|
9704
|
+
if (current.startsWith("url(")) {
|
|
9705
|
+
swatch.setAttribute("alpha", this.#fillPickerSwatchAlpha());
|
|
9706
|
+
return;
|
|
9707
|
+
}
|
|
9708
|
+
}
|
|
9709
|
+
swatch.setAttribute("background", background);
|
|
9690
9710
|
swatch.setAttribute("alpha", this.#fillPickerSwatchAlpha());
|
|
9691
9711
|
}
|
|
9692
9712
|
|
|
@@ -10343,6 +10363,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
10343
10363
|
#colorObserver = null;
|
|
10344
10364
|
#repositionRAF = null;
|
|
10345
10365
|
#pickerDefinitionPromise = null;
|
|
10366
|
+
anchorElement = null;
|
|
10346
10367
|
#gradient = {
|
|
10347
10368
|
type: "linear",
|
|
10348
10369
|
angle: 90,
|
|
@@ -10713,7 +10734,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
10713
10734
|
#setupPickerEvents() {
|
|
10714
10735
|
const picker = this.querySelector("fig-fill-picker");
|
|
10715
10736
|
if (!picker) return;
|
|
10716
|
-
picker.anchorElement = this;
|
|
10737
|
+
picker.anchorElement = this.anchorElement || this;
|
|
10717
10738
|
|
|
10718
10739
|
const syncFromPicker = (e) => {
|
|
10719
10740
|
e.stopPropagation();
|
package/package.json
CHANGED