@rogieking/figui3 6.10.0 → 6.12.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 +64 -27
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +73 -67
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +21 -21
- package/dist/fig.css +1 -1
- package/dist/fig.js +36 -48
- package/fig-editor.css +12 -13
- package/fig-editor.js +343 -356
- package/fig-lab.css +79 -3
- package/fig-lab.js +459 -11
- package/fig.js +662 -230
- package/package.json +1 -1
package/fig-editor.js
CHANGED
|
@@ -1,244 +1,15 @@
|
|
|
1
|
-
import "./fig
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(
|
|
11
|
-
const isDesktopWebKit =
|
|
12
|
-
/\bAppleWebKit\b/.test(userAgent) &&
|
|
13
|
-
!/\b(Chrome|Chromium|Edg|OPR|SamsungBrowser)\b/.test(userAgent);
|
|
14
|
-
return isIOSBrowser || isDesktopWebKit;
|
|
1
|
+
import "./fig.js";
|
|
2
|
+
import "./fig-lab.js";
|
|
3
|
+
|
|
4
|
+
function figEditorEscapeAttribute(value) {
|
|
5
|
+
return String(value ?? "")
|
|
6
|
+
.replace(/&/g, "&")
|
|
7
|
+
.replace(/"/g, """)
|
|
8
|
+
.replace(/'/g, "'")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">");
|
|
15
11
|
}
|
|
16
12
|
|
|
17
|
-
function figEditorSupportsCustomizedBuiltIns() {
|
|
18
|
-
if (
|
|
19
|
-
typeof window === "undefined" ||
|
|
20
|
-
!window.customElements ||
|
|
21
|
-
typeof HTMLButtonElement === "undefined"
|
|
22
|
-
) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const testName = `fig-editor-builtin-probe-${Math.random().toString(36).slice(2)}`;
|
|
27
|
-
class FigEditorCustomizedBuiltInProbe extends HTMLButtonElement {}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
customElements.define(testName, FigEditorCustomizedBuiltInProbe, {
|
|
31
|
-
extends: "button",
|
|
32
|
-
});
|
|
33
|
-
const probe = document.createElement("button", { is: testName });
|
|
34
|
-
return probe instanceof FigEditorCustomizedBuiltInProbe;
|
|
35
|
-
} catch (_error) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const figEditorNeedsBuiltInPolyfill =
|
|
41
|
-
figEditorIsWebKitOrIOSBrowser() && !figEditorSupportsCustomizedBuiltIns();
|
|
42
|
-
const figEditorBuiltInPolyfillReady = (
|
|
43
|
-
figEditorNeedsBuiltInPolyfill
|
|
44
|
-
? import("./polyfills/custom-elements-webkit.js")
|
|
45
|
-
: Promise.resolve()
|
|
46
|
-
)
|
|
47
|
-
.then(() => {})
|
|
48
|
-
.catch((error) => {
|
|
49
|
-
throw error;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
function figEditorDefineCustomizedBuiltIn(name, constructor, options) {
|
|
53
|
-
const define = () => {
|
|
54
|
-
if (!customElements.get(name)) {
|
|
55
|
-
customElements.define(name, constructor, options);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
if (!figEditorNeedsBuiltInPolyfill) {
|
|
60
|
-
define();
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
figEditorBuiltInPolyfillReady.then(define).catch((error) => {
|
|
65
|
-
console.error(
|
|
66
|
-
`[figui3] Failed to load customized built-in polyfill for "${name}".`,
|
|
67
|
-
error,
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/* Toast */
|
|
73
|
-
/**
|
|
74
|
-
* A toast notification element for non-modal, time-based messages.
|
|
75
|
-
* Always positioned at bottom center of the screen.
|
|
76
|
-
* @attr {number} duration - Auto-dismiss duration in ms (0 = no auto-dismiss, default: 5000)
|
|
77
|
-
* @attr {number} offset - Distance from bottom edge in pixels (default: 16)
|
|
78
|
-
* @attr {string} theme - Visual theme: "dark" (default), "light", "danger", "brand"
|
|
79
|
-
* @attr {boolean} open - Whether the toast is visible
|
|
80
|
-
*/
|
|
81
|
-
class FigToast extends HTMLDialogElement {
|
|
82
|
-
constructor() {
|
|
83
|
-
super();
|
|
84
|
-
this._figInit();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
_figInit() {
|
|
88
|
-
if (this._figInitialized) return;
|
|
89
|
-
this._figInitialized = true;
|
|
90
|
-
this._defaultOffset = 16;
|
|
91
|
-
this._autoCloseTimer = null;
|
|
92
|
-
this._boundHandleClose = this.handleClose.bind(this);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getOffset() {
|
|
96
|
-
return parseInt(this.getAttribute("offset") ?? this._defaultOffset);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
connectedCallback() {
|
|
100
|
-
this._figInit();
|
|
101
|
-
|
|
102
|
-
if (!this.hasAttribute("theme")) {
|
|
103
|
-
this.setAttribute("theme", "dark");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this.syncLiveRegion();
|
|
107
|
-
|
|
108
|
-
const shouldOpen =
|
|
109
|
-
this.getAttribute("open") === "true" || this.getAttribute("open") === "";
|
|
110
|
-
if (this.hasAttribute("open") && !shouldOpen) {
|
|
111
|
-
this.removeAttribute("open");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!shouldOpen) {
|
|
115
|
-
this.close();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
requestAnimationFrame(() => {
|
|
119
|
-
this.addCloseListeners();
|
|
120
|
-
this.applyPosition();
|
|
121
|
-
|
|
122
|
-
if (shouldOpen) {
|
|
123
|
-
this.showToast();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
disconnectedCallback() {
|
|
129
|
-
this._figInit();
|
|
130
|
-
this.clearAutoClose();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
addCloseListeners() {
|
|
134
|
-
this.querySelectorAll("[close-toast]").forEach((button) => {
|
|
135
|
-
button.removeEventListener("click", this._boundHandleClose);
|
|
136
|
-
button.addEventListener("click", this._boundHandleClose);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
handleClose() {
|
|
141
|
-
this.hideToast();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
applyPosition() {
|
|
145
|
-
this.style.position = "fixed";
|
|
146
|
-
this.style.margin = "0";
|
|
147
|
-
this.style.top = "auto";
|
|
148
|
-
this.style.bottom = `${this.getOffset()}px`;
|
|
149
|
-
this.style.left = "50%";
|
|
150
|
-
this.style.right = "auto";
|
|
151
|
-
this.style.transform = "translateX(-50%)";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
startAutoClose() {
|
|
155
|
-
this.clearAutoClose();
|
|
156
|
-
|
|
157
|
-
const duration = parseInt(this.getAttribute("duration") ?? "5000");
|
|
158
|
-
if (duration > 0) {
|
|
159
|
-
this._autoCloseTimer = setTimeout(() => {
|
|
160
|
-
this.hideToast();
|
|
161
|
-
}, duration);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
syncLiveRegion() {
|
|
166
|
-
const assertive =
|
|
167
|
-
this.getAttribute("live") === "assertive" ||
|
|
168
|
-
this.getAttribute("theme") === "danger";
|
|
169
|
-
if (!this.hasAttribute("role")) {
|
|
170
|
-
this.setAttribute("role", assertive ? "alert" : "status");
|
|
171
|
-
}
|
|
172
|
-
if (!this.hasAttribute("aria-live")) {
|
|
173
|
-
this.setAttribute("aria-live", assertive ? "assertive" : "polite");
|
|
174
|
-
}
|
|
175
|
-
if (!this.hasAttribute("aria-atomic")) {
|
|
176
|
-
this.setAttribute("aria-atomic", "true");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
clearAutoClose() {
|
|
181
|
-
if (this._autoCloseTimer) {
|
|
182
|
-
clearTimeout(this._autoCloseTimer);
|
|
183
|
-
this._autoCloseTimer = null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
_resolveAutoTheme() {
|
|
188
|
-
if (this.getAttribute("theme") !== "auto") return;
|
|
189
|
-
const cs = getComputedStyle(document.documentElement).colorScheme || "";
|
|
190
|
-
const isDark = cs.includes("dark");
|
|
191
|
-
this.style.colorScheme = isDark ? "light" : "dark";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
showToast() {
|
|
195
|
-
this._resolveAutoTheme();
|
|
196
|
-
if (!this.open) this.show();
|
|
197
|
-
this.applyPosition();
|
|
198
|
-
this.startAutoClose();
|
|
199
|
-
this.dispatchEvent(new CustomEvent("toast-show", { bubbles: true }));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
hideToast() {
|
|
203
|
-
this.clearAutoClose();
|
|
204
|
-
this.close();
|
|
205
|
-
this.dispatchEvent(new CustomEvent("toast-hide", { bubbles: true }));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
static get observedAttributes() {
|
|
209
|
-
return ["duration", "offset", "open", "theme", "live"];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
213
|
-
this._figInit();
|
|
214
|
-
if (!this.isConnected) return;
|
|
215
|
-
if (name === "offset") {
|
|
216
|
-
this.applyPosition();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (name === "open") {
|
|
220
|
-
if (newValue !== null && newValue !== "false") {
|
|
221
|
-
this.showToast();
|
|
222
|
-
} else {
|
|
223
|
-
this.hideToast();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (name === "theme") {
|
|
228
|
-
if (newValue === "auto") {
|
|
229
|
-
this._resolveAutoTheme();
|
|
230
|
-
} else {
|
|
231
|
-
this.style.removeProperty("color-scheme");
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (name === "theme" || name === "live") {
|
|
236
|
-
this.syncLiveRegion();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
figEditorDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
|
|
241
|
-
|
|
242
13
|
// FigFillPicker
|
|
243
14
|
const GRADIENT_INTERPOLATION_SPACES = [
|
|
244
15
|
"srgb",
|
|
@@ -345,9 +116,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
345
116
|
#hueSlider = null;
|
|
346
117
|
#opacitySlider = null;
|
|
347
118
|
#isDraggingColor = false;
|
|
119
|
+
#syncingGradientBar = false;
|
|
348
120
|
#teardownColorAreaEvents = null;
|
|
349
|
-
#
|
|
350
|
-
#
|
|
121
|
+
#valueAtOpen = null;
|
|
122
|
+
#webcamStart = null;
|
|
123
|
+
#webcamRequestId = 0;
|
|
351
124
|
#boundTriggerClick = null;
|
|
352
125
|
#boundTriggerKeydown = null;
|
|
353
126
|
|
|
@@ -382,24 +155,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
382
155
|
}
|
|
383
156
|
|
|
384
157
|
disconnectedCallback() {
|
|
385
|
-
|
|
386
|
-
this.#teardownColorAreaEvents();
|
|
387
|
-
this.#teardownColorAreaEvents = null;
|
|
388
|
-
}
|
|
389
|
-
if (this.#dialogOpenObserver) {
|
|
390
|
-
this.#dialogOpenObserver.disconnect();
|
|
391
|
-
this.#dialogOpenObserver = null;
|
|
392
|
-
}
|
|
393
|
-
if (this.#webcamTabObserver) {
|
|
394
|
-
this.#webcamTabObserver.disconnect();
|
|
395
|
-
this.#webcamTabObserver = null;
|
|
396
|
-
}
|
|
397
|
-
if (this.#webcam.stream) {
|
|
398
|
-
this.#webcam.stream.getTracks().forEach((track) => track.stop());
|
|
399
|
-
this.#webcam.stream = null;
|
|
400
|
-
}
|
|
158
|
+
this.#discardDialog();
|
|
401
159
|
if (this.#webcam.snapshot?.startsWith("blob:")) {
|
|
402
160
|
URL.revokeObjectURL(this.#webcam.snapshot);
|
|
161
|
+
if (this.#image.url === this.#webcam.snapshot) this.#image.url = null;
|
|
403
162
|
this.#webcam.snapshot = null;
|
|
404
163
|
}
|
|
405
164
|
if (this.#video.url && this.#video.url.startsWith("blob:")) {
|
|
@@ -410,11 +169,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
410
169
|
this.#trigger.removeEventListener("click", this.#boundTriggerClick);
|
|
411
170
|
this.#trigger.removeEventListener("keydown", this.#boundTriggerKeydown);
|
|
412
171
|
}
|
|
413
|
-
if (this.#dialog) {
|
|
414
|
-
this.#dialog.close();
|
|
415
|
-
this.#dialog.remove();
|
|
416
|
-
this.#dialog = null;
|
|
417
|
-
}
|
|
418
172
|
}
|
|
419
173
|
|
|
420
174
|
#setupTrigger() {
|
|
@@ -485,7 +239,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
485
239
|
}
|
|
486
240
|
|
|
487
241
|
#handleTriggerClick(e) {
|
|
488
|
-
if (
|
|
242
|
+
if (
|
|
243
|
+
this.hasAttribute("disabled") &&
|
|
244
|
+
this.getAttribute("disabled") !== "false"
|
|
245
|
+
)
|
|
246
|
+
return;
|
|
489
247
|
e.stopPropagation();
|
|
490
248
|
e.preventDefault();
|
|
491
249
|
this.#openDialog();
|
|
@@ -493,7 +251,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
493
251
|
|
|
494
252
|
#handleTriggerKeydown(e) {
|
|
495
253
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
496
|
-
if (
|
|
254
|
+
if (
|
|
255
|
+
this.hasAttribute("disabled") &&
|
|
256
|
+
this.getAttribute("disabled") !== "false"
|
|
257
|
+
)
|
|
258
|
+
return;
|
|
497
259
|
e.preventDefault();
|
|
498
260
|
e.stopPropagation();
|
|
499
261
|
this.#openDialog();
|
|
@@ -625,7 +387,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
625
387
|
this.#createDialog();
|
|
626
388
|
}
|
|
627
389
|
|
|
628
|
-
this.#
|
|
390
|
+
this.#valueAtOpen = JSON.stringify(this.value);
|
|
391
|
+
this.#switchTab(this.#fillType, { emit: false });
|
|
629
392
|
|
|
630
393
|
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
631
394
|
if (gamutEl) gamutEl.value = this.#gamut;
|
|
@@ -650,6 +413,43 @@ class FigFillPicker extends HTMLElement {
|
|
|
650
413
|
if (this.#dialog) this.#dialog.open = false;
|
|
651
414
|
}
|
|
652
415
|
|
|
416
|
+
#restoreCustomSlotContent() {
|
|
417
|
+
if (!this.#dialog) return;
|
|
418
|
+
for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
|
|
419
|
+
const container = Array.from(
|
|
420
|
+
this.#dialog.querySelectorAll(".fig-fill-picker-tab"),
|
|
421
|
+
).find((candidate) => candidate.dataset.tab === modeName);
|
|
422
|
+
if (!container) continue;
|
|
423
|
+
while (container.firstChild) element.appendChild(container.firstChild);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#discardDialog() {
|
|
428
|
+
if (this.#teardownColorAreaEvents) {
|
|
429
|
+
this.#teardownColorAreaEvents();
|
|
430
|
+
this.#teardownColorAreaEvents = null;
|
|
431
|
+
}
|
|
432
|
+
this.#stopWebcam();
|
|
433
|
+
if (!this.#dialog) return;
|
|
434
|
+
this.#restoreCustomSlotContent();
|
|
435
|
+
this.#dialog.remove();
|
|
436
|
+
this.#dialog = null;
|
|
437
|
+
this.#webcamStart = null;
|
|
438
|
+
this.#valueAtOpen = null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#stopWebcam() {
|
|
442
|
+
this.#webcamRequestId += 1;
|
|
443
|
+
if (this.#webcam.stream) {
|
|
444
|
+
this.#webcam.stream.getTracks().forEach((track) => track.stop());
|
|
445
|
+
this.#webcam.stream = null;
|
|
446
|
+
}
|
|
447
|
+
const video = this.#dialog?.querySelector(
|
|
448
|
+
".fig-fill-picker-webcam-video",
|
|
449
|
+
);
|
|
450
|
+
if (video) video.srcObject = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
653
453
|
#createDialog() {
|
|
654
454
|
// Collect slotted custom mode content before any DOM changes
|
|
655
455
|
this.#customSlots = {};
|
|
@@ -709,23 +509,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
709
509
|
}
|
|
710
510
|
|
|
711
511
|
const experimental = this.getAttribute("experimental");
|
|
712
|
-
const expAttr = experimental
|
|
512
|
+
const expAttr = experimental
|
|
513
|
+
? `experimental="${figEditorEscapeAttribute(experimental)}"`
|
|
514
|
+
: "";
|
|
713
515
|
|
|
714
516
|
let headerContent;
|
|
715
517
|
if (allowedModes.length === 1) {
|
|
716
|
-
headerContent = `<h3 class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</h3>`;
|
|
518
|
+
headerContent = `<h3 class="fig-fill-picker-type-label">${figEditorEscapeAttribute(modeLabels[allowedModes[0]])}</h3>`;
|
|
717
519
|
} else {
|
|
718
520
|
const options = allowedModes
|
|
719
|
-
.map(
|
|
521
|
+
.map(
|
|
522
|
+
(m) =>
|
|
523
|
+
`<option value="${figEditorEscapeAttribute(m)}">${figEditorEscapeAttribute(modeLabels[m])}</option>`,
|
|
524
|
+
)
|
|
720
525
|
.join("\n ");
|
|
721
|
-
headerContent = `<fig-dropdown class="fig-fill-picker-type" label="Fill type" ${expAttr} value="${this.#fillType}">
|
|
526
|
+
headerContent = `<fig-dropdown class="fig-fill-picker-type" label="Fill type" ${expAttr} value="${figEditorEscapeAttribute(this.#fillType)}">
|
|
722
527
|
${options}
|
|
723
528
|
</fig-dropdown>`;
|
|
724
529
|
}
|
|
725
530
|
|
|
726
531
|
// Generate tab containers for all allowed modes
|
|
727
532
|
const tabDivs = allowedModes
|
|
728
|
-
.map(
|
|
533
|
+
.map(
|
|
534
|
+
(m) =>
|
|
535
|
+
`<div class="fig-fill-picker-tab" data-tab="${figEditorEscapeAttribute(m)}"></div>`,
|
|
536
|
+
)
|
|
729
537
|
.join("\n ");
|
|
730
538
|
|
|
731
539
|
const gamutDropdown = `<fig-dropdown class="fig-fill-picker-gamut" label="Color gamut" ${expAttr} value="${this.#gamut}">
|
|
@@ -750,7 +558,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
750
558
|
|
|
751
559
|
// Populate custom tab containers and emit modeready
|
|
752
560
|
for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
|
|
753
|
-
const container =
|
|
561
|
+
const container = Array.from(
|
|
562
|
+
this.#dialog.querySelectorAll(".fig-fill-picker-tab"),
|
|
563
|
+
).find((candidate) => candidate.dataset.tab === modeName);
|
|
754
564
|
if (!container) continue;
|
|
755
565
|
|
|
756
566
|
// Move children (not the element itself) for vanilla HTML usage
|
|
@@ -785,7 +595,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
785
595
|
this.#onGamutChange();
|
|
786
596
|
}
|
|
787
597
|
};
|
|
788
|
-
gamutEl.addEventListener("input", handleGamutChange);
|
|
789
598
|
gamutEl.addEventListener("change", handleGamutChange);
|
|
790
599
|
}
|
|
791
600
|
|
|
@@ -797,22 +606,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
797
606
|
|
|
798
607
|
const onDialogClose = () => {
|
|
799
608
|
if (this.#swatch) this.#swatch.removeAttribute("selected");
|
|
800
|
-
this.#
|
|
609
|
+
this.#stopWebcam();
|
|
610
|
+
if (
|
|
611
|
+
this.#valueAtOpen !== null &&
|
|
612
|
+
this.#valueAtOpen !== JSON.stringify(this.value)
|
|
613
|
+
) {
|
|
614
|
+
this.#emitChange();
|
|
615
|
+
}
|
|
616
|
+
this.#valueAtOpen = null;
|
|
801
617
|
this.dispatchEvent(new CustomEvent("close"));
|
|
802
618
|
};
|
|
803
619
|
this.#dialog.addEventListener("close", onDialogClose);
|
|
804
620
|
|
|
805
|
-
this.#dialogOpenObserver = new MutationObserver(() => {
|
|
806
|
-
const isOpen =
|
|
807
|
-
this.#dialog.hasAttribute("open") &&
|
|
808
|
-
this.#dialog.getAttribute("open") !== "false";
|
|
809
|
-
if (!isOpen) onDialogClose();
|
|
810
|
-
});
|
|
811
|
-
this.#dialogOpenObserver.observe(this.#dialog, {
|
|
812
|
-
attributes: true,
|
|
813
|
-
attributeFilter: ["open"],
|
|
814
|
-
});
|
|
815
|
-
|
|
816
621
|
// Initialize built-in tabs (skip any overridden by custom slots)
|
|
817
622
|
const builtinInits = {
|
|
818
623
|
solid: () => this.#initSolidTab(),
|
|
@@ -828,7 +633,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
828
633
|
// Listen for input/change from custom tab content
|
|
829
634
|
for (const modeName of Object.keys(this.#customSlots)) {
|
|
830
635
|
if (builtinModes.includes(modeName)) continue;
|
|
831
|
-
const container =
|
|
636
|
+
const container = Array.from(
|
|
637
|
+
this.#dialog.querySelectorAll(".fig-fill-picker-tab"),
|
|
638
|
+
).find((candidate) => candidate.dataset.tab === modeName);
|
|
832
639
|
if (!container) continue;
|
|
833
640
|
container.addEventListener("input", (e) => {
|
|
834
641
|
if (e.target === this) return;
|
|
@@ -845,15 +652,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
845
652
|
}
|
|
846
653
|
}
|
|
847
654
|
|
|
848
|
-
#switchTab(tabName) {
|
|
655
|
+
#switchTab(tabName, { emit = true } = {}) {
|
|
849
656
|
// Only allow switching to modes that have a tab container in the dialog
|
|
850
|
-
const tab =
|
|
851
|
-
|
|
852
|
-
);
|
|
657
|
+
const tab = Array.from(
|
|
658
|
+
this.#dialog?.querySelectorAll(".fig-fill-picker-tab") ?? [],
|
|
659
|
+
).find((candidate) => candidate.dataset.tab === tabName);
|
|
853
660
|
if (!tab) return;
|
|
854
661
|
|
|
662
|
+
const previousTab = this.#activeTab;
|
|
855
663
|
this.#activeTab = tabName;
|
|
856
664
|
this.#fillType = tabName;
|
|
665
|
+
if (previousTab === "webcam" && tabName !== "webcam") {
|
|
666
|
+
this.#stopWebcam();
|
|
667
|
+
}
|
|
857
668
|
|
|
858
669
|
// Update dropdown selection (only exists if not locked)
|
|
859
670
|
const typeDropdown = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
@@ -890,8 +701,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
890
701
|
});
|
|
891
702
|
}
|
|
892
703
|
|
|
704
|
+
if (tabName === "webcam") this.#webcamStart?.();
|
|
705
|
+
|
|
893
706
|
this.#updateSwatch();
|
|
894
|
-
this.#emitInput();
|
|
707
|
+
if (emit) this.#emitInput();
|
|
895
708
|
}
|
|
896
709
|
|
|
897
710
|
// ============ SOLID TAB ============
|
|
@@ -978,7 +791,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
978
791
|
|
|
979
792
|
// Setup color input mode dropdown
|
|
980
793
|
const modeDropdown = container.querySelector(".fig-fill-picker-input-mode");
|
|
981
|
-
modeDropdown.addEventListener("
|
|
794
|
+
modeDropdown.addEventListener("change", (e) => {
|
|
982
795
|
this.#colorInputMode = e.target.value;
|
|
983
796
|
this.#rebuildColorInputFields();
|
|
984
797
|
});
|
|
@@ -1439,28 +1252,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1439
1252
|
<fig-preview class="fig-fill-picker-gradient-preview">
|
|
1440
1253
|
<fig-input-gradient class="fig-fill-picker-gradient-bar-input" aria-label="Gradient stops" edit="true" mode="tip" size="large" value='${JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) })}'></fig-input-gradient>
|
|
1441
1254
|
</fig-preview>
|
|
1442
|
-
<fig-field class="fig-fill-picker-gradient-interpolation">
|
|
1443
|
-
<label>Mixing</label>
|
|
1444
|
-
<fig-dropdown class="fig-fill-picker-gradient-space" label="Gradient mixing" full ${expAttr} value="${
|
|
1445
|
-
this.#gradient.interpolationSpace === "oklch"
|
|
1446
|
-
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
1447
|
-
: this.#gradient.interpolationSpace
|
|
1448
|
-
}">
|
|
1449
|
-
<optgroup label="sRGB">
|
|
1450
|
-
<option value="srgb">Classic</option>
|
|
1451
|
-
<option value="srgb-linear">Linear</option>
|
|
1452
|
-
</optgroup>
|
|
1453
|
-
<optgroup label="OKLab">
|
|
1454
|
-
<option value="oklab">Perceptual</option>
|
|
1455
|
-
</optgroup>
|
|
1456
|
-
<optgroup label="OKLCH">
|
|
1457
|
-
<option value="oklch-shorter">Shorter hue</option>
|
|
1458
|
-
<option value="oklch-longer">Longer hue</option>
|
|
1459
|
-
<option value="oklch-increasing">Increasing hue</option>
|
|
1460
|
-
<option value="oklch-decreasing">Decreasing hue</option>
|
|
1461
|
-
</optgroup>
|
|
1462
|
-
</fig-dropdown>
|
|
1463
|
-
</fig-field>
|
|
1464
1255
|
<div class="fig-fill-picker-gradient-stops">
|
|
1465
1256
|
<fig-header class="fig-fill-picker-gradient-stops-header" borderless>
|
|
1466
1257
|
<span>Stops</span>
|
|
@@ -1468,7 +1259,35 @@ class FigFillPicker extends HTMLElement {
|
|
|
1468
1259
|
<fig-icon name="add"></fig-icon>
|
|
1469
1260
|
</fig-button>
|
|
1470
1261
|
</fig-header>
|
|
1471
|
-
<div class="fig-fill-picker-gradient-stops-list"
|
|
1262
|
+
<div class="fig-fill-picker-gradient-stops-list">
|
|
1263
|
+
<fig-reorder></fig-reorder>
|
|
1264
|
+
</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
<div class="fig-fill-picker-gradient-interpolation">
|
|
1267
|
+
<fig-header class="fig-fill-picker-gradient-interpolation-header" borderless>
|
|
1268
|
+
<span>Color interpolation</span>
|
|
1269
|
+
</fig-header>
|
|
1270
|
+
<fig-field class="fig-fill-picker-gradient-interpolation-field">
|
|
1271
|
+
<fig-dropdown class="fig-fill-picker-gradient-space" label="Color interpolation" full ${expAttr} value="${
|
|
1272
|
+
this.#gradient.interpolationSpace === "oklch"
|
|
1273
|
+
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
1274
|
+
: this.#gradient.interpolationSpace
|
|
1275
|
+
}">
|
|
1276
|
+
<optgroup label="sRGB">
|
|
1277
|
+
<option value="srgb">Classic</option>
|
|
1278
|
+
<option value="srgb-linear">Linear</option>
|
|
1279
|
+
</optgroup>
|
|
1280
|
+
<optgroup label="OKLab">
|
|
1281
|
+
<option value="oklab">Perceptual</option>
|
|
1282
|
+
</optgroup>
|
|
1283
|
+
<optgroup label="OKLCH">
|
|
1284
|
+
<option value="oklch-shorter">Shorter hue</option>
|
|
1285
|
+
<option value="oklch-longer">Longer hue</option>
|
|
1286
|
+
<option value="oklch-increasing">Increasing hue</option>
|
|
1287
|
+
<option value="oklch-decreasing">Decreasing hue</option>
|
|
1288
|
+
</optgroup>
|
|
1289
|
+
</fig-dropdown>
|
|
1290
|
+
</fig-field>
|
|
1472
1291
|
</div>
|
|
1473
1292
|
`;
|
|
1474
1293
|
|
|
@@ -1489,7 +1308,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1489
1308
|
this.#updateGradientUI();
|
|
1490
1309
|
this.#emitInput();
|
|
1491
1310
|
};
|
|
1492
|
-
typeDropdown.addEventListener("input", handleTypeChange);
|
|
1493
1311
|
typeDropdown.addEventListener("change", handleTypeChange);
|
|
1494
1312
|
|
|
1495
1313
|
const interpolationDropdown = container.querySelector(
|
|
@@ -1511,7 +1329,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1511
1329
|
this.#updateGradientUI();
|
|
1512
1330
|
this.#emitInput();
|
|
1513
1331
|
};
|
|
1514
|
-
interpolationDropdown?.addEventListener("input", handleInterpolationChange);
|
|
1515
1332
|
interpolationDropdown?.addEventListener(
|
|
1516
1333
|
"change",
|
|
1517
1334
|
handleInterpolationChange,
|
|
@@ -1532,12 +1349,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
1532
1349
|
const cxInput = container.querySelector(".fig-fill-picker-gradient-cx");
|
|
1533
1350
|
const cyInput = container.querySelector(".fig-fill-picker-gradient-cy");
|
|
1534
1351
|
cxInput?.addEventListener("input", (e) => {
|
|
1535
|
-
|
|
1352
|
+
const value = Number.parseFloat(e.target.value);
|
|
1353
|
+
this.#gradient.centerX = Number.isFinite(value) ? value : 50;
|
|
1536
1354
|
this.#updateGradientPreview();
|
|
1537
1355
|
this.#emitInput();
|
|
1538
1356
|
});
|
|
1539
1357
|
cyInput?.addEventListener("input", (e) => {
|
|
1540
|
-
|
|
1358
|
+
const value = Number.parseFloat(e.target.value);
|
|
1359
|
+
this.#gradient.centerY = Number.isFinite(value) ? value : 50;
|
|
1541
1360
|
this.#updateGradientPreview();
|
|
1542
1361
|
this.#emitInput();
|
|
1543
1362
|
});
|
|
@@ -1576,6 +1395,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1576
1395
|
if (gradientBarInput) {
|
|
1577
1396
|
const syncFromBarInput = (e) => {
|
|
1578
1397
|
e.stopPropagation();
|
|
1398
|
+
if (this.#syncingGradientBar) return;
|
|
1579
1399
|
const detail = e.detail;
|
|
1580
1400
|
if (!detail?.gradient) return;
|
|
1581
1401
|
this.#gradient = normalizeGradientConfig({
|
|
@@ -1594,6 +1414,152 @@ class FigFillPicker extends HTMLElement {
|
|
|
1594
1414
|
this.#emitChange();
|
|
1595
1415
|
});
|
|
1596
1416
|
}
|
|
1417
|
+
|
|
1418
|
+
const stopsReorder = container.querySelector(
|
|
1419
|
+
".fig-fill-picker-gradient-stops-list > fig-reorder",
|
|
1420
|
+
);
|
|
1421
|
+
stopsReorder?.addEventListener("reorder", (event) => {
|
|
1422
|
+
this.#handleGradientStopsReorder(event);
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
#formatStopColorValue(stop) {
|
|
1427
|
+
const hex = String(stop.color || "#888888")
|
|
1428
|
+
.replace(/^#/, "")
|
|
1429
|
+
.slice(0, 6);
|
|
1430
|
+
const opacity = stop.opacity ?? 100;
|
|
1431
|
+
if (opacity >= 100) return `#${hex}`;
|
|
1432
|
+
const alpha = Math.round((opacity / 100) * 255)
|
|
1433
|
+
.toString(16)
|
|
1434
|
+
.padStart(2, "0");
|
|
1435
|
+
return `#${hex}${alpha}`;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
#readGradientStopColor(colorInput) {
|
|
1439
|
+
if (!(colorInput instanceof HTMLElement)) return "#888888";
|
|
1440
|
+
|
|
1441
|
+
if (colorInput.hexOpaque) {
|
|
1442
|
+
return this.#normalizeStopHex(colorInput.hexOpaque);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const liveValue = colorInput.value;
|
|
1446
|
+
if (typeof liveValue === "string" && liveValue.startsWith("#")) {
|
|
1447
|
+
return this.#normalizeStopHex(
|
|
1448
|
+
liveValue.length > 7 ? liveValue.slice(0, 7) : liveValue,
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const textInput = colorInput.querySelector(
|
|
1453
|
+
'fig-input-text:not([type="number"])',
|
|
1454
|
+
);
|
|
1455
|
+
const textHex = String(
|
|
1456
|
+
textInput?.value ?? textInput?.getAttribute("value") ?? "",
|
|
1457
|
+
)
|
|
1458
|
+
.replace(/#/g, "")
|
|
1459
|
+
.slice(0, 6);
|
|
1460
|
+
if (/^[0-9A-Fa-f]{6}$/.test(textHex)) {
|
|
1461
|
+
return `#${textHex.toUpperCase()}`;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const attr = colorInput.getAttribute("value");
|
|
1465
|
+
if (typeof attr === "string" && attr.startsWith("#")) {
|
|
1466
|
+
return this.#normalizeStopHex(
|
|
1467
|
+
attr.length > 7 ? attr.slice(0, 7) : attr,
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return "#888888";
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
#normalizeStopHex(color) {
|
|
1475
|
+
const hex = String(color || "#888888")
|
|
1476
|
+
.replace(/^#/, "")
|
|
1477
|
+
.slice(0, 6);
|
|
1478
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) return "#888888";
|
|
1479
|
+
return `#${hex.toUpperCase()}`;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
#readGradientStopFromRow(row) {
|
|
1483
|
+
const posInput = row.querySelector(".fig-fill-picker-stop-position");
|
|
1484
|
+
const colorInput = row.querySelector(".fig-fill-picker-stop-color");
|
|
1485
|
+
|
|
1486
|
+
const position =
|
|
1487
|
+
parseFloat(posInput?.value ?? posInput?.getAttribute("value") ?? "0") ||
|
|
1488
|
+
0;
|
|
1489
|
+
|
|
1490
|
+
const color = this.#readGradientStopColor(colorInput);
|
|
1491
|
+
let opacity = 100;
|
|
1492
|
+
|
|
1493
|
+
if (colorInput instanceof HTMLElement && colorInput.rgba?.a !== undefined) {
|
|
1494
|
+
opacity = Math.round(colorInput.rgba.a * 100);
|
|
1495
|
+
} else {
|
|
1496
|
+
const oldIndex = parseInt(row.dataset.index, 10);
|
|
1497
|
+
if (!isNaN(oldIndex) && this.#gradient.stops[oldIndex]) {
|
|
1498
|
+
opacity = this.#gradient.stops[oldIndex].opacity ?? 100;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return { position, color, opacity };
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
#syncGradientStopRow(row) {
|
|
1506
|
+
const index = parseInt(row.dataset.index, 10);
|
|
1507
|
+
if (isNaN(index) || index < 0 || index >= this.#gradient.stops.length) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const stop = {
|
|
1511
|
+
...this.#gradient.stops[index],
|
|
1512
|
+
...this.#readGradientStopFromRow(row),
|
|
1513
|
+
};
|
|
1514
|
+
this.#gradient.stops[index] = stop;
|
|
1515
|
+
// Persist to the input value attributes so that reordering (which detaches
|
|
1516
|
+
// and re-attaches the row's DOM nodes) doesn't reset them to stale markup.
|
|
1517
|
+
this.#persistGradientStopRowAttrs(row, stop);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
#persistGradientStopRowAttrs(row, stop) {
|
|
1521
|
+
const posInput = row.querySelector(".fig-fill-picker-stop-position");
|
|
1522
|
+
if (posInput) posInput.setAttribute("value", String(stop.position));
|
|
1523
|
+
const colorInput = row.querySelector(".fig-fill-picker-stop-color");
|
|
1524
|
+
if (colorInput) {
|
|
1525
|
+
colorInput.setAttribute("value", this.#formatStopColorValue(stop));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
#handleGradientStopsReorder(event) {
|
|
1530
|
+
const { oldIndex, newIndex } = event.detail ?? {};
|
|
1531
|
+
if (
|
|
1532
|
+
oldIndex == null ||
|
|
1533
|
+
newIndex == null ||
|
|
1534
|
+
oldIndex === newIndex ||
|
|
1535
|
+
oldIndex < 0 ||
|
|
1536
|
+
newIndex < 0 ||
|
|
1537
|
+
oldIndex >= this.#gradient.stops.length ||
|
|
1538
|
+
newIndex >= this.#gradient.stops.length
|
|
1539
|
+
) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const reorder = event.currentTarget;
|
|
1544
|
+
const rows = reorder.querySelectorAll(".fig-fill-picker-gradient-stop-row");
|
|
1545
|
+
|
|
1546
|
+
// Rows still carry their pre-drag data-index; flush live inputs first.
|
|
1547
|
+
rows.forEach((row) => this.#syncGradientStopRow(row));
|
|
1548
|
+
|
|
1549
|
+
const [stop] = this.#gradient.stops.splice(oldIndex, 1);
|
|
1550
|
+
this.#gradient.stops.splice(newIndex, 0, stop);
|
|
1551
|
+
|
|
1552
|
+
rows.forEach((row, index) => {
|
|
1553
|
+
row.dataset.index = String(index);
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
this.#syncingGradientBar = true;
|
|
1557
|
+
try {
|
|
1558
|
+
this.#updateGradientPreview();
|
|
1559
|
+
this.#emitInput();
|
|
1560
|
+
} finally {
|
|
1561
|
+
this.#syncingGradientBar = false;
|
|
1562
|
+
}
|
|
1597
1563
|
}
|
|
1598
1564
|
|
|
1599
1565
|
#updateGradientUI() {
|
|
@@ -1643,6 +1609,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1643
1609
|
".fig-fill-picker-gradient-bar-input",
|
|
1644
1610
|
);
|
|
1645
1611
|
if (barInput) {
|
|
1612
|
+
this.#syncingGradientBar = true;
|
|
1646
1613
|
barInput.setAttribute(
|
|
1647
1614
|
"value",
|
|
1648
1615
|
JSON.stringify({
|
|
@@ -1650,6 +1617,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1650
1617
|
gradient: gradientToValueShape(this.#gradient),
|
|
1651
1618
|
}),
|
|
1652
1619
|
);
|
|
1620
|
+
this.#syncingGradientBar = false;
|
|
1653
1621
|
}
|
|
1654
1622
|
|
|
1655
1623
|
this.#updateSwatch();
|
|
@@ -1661,9 +1629,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
1661
1629
|
const list = this.#dialog.querySelector(
|
|
1662
1630
|
".fig-fill-picker-gradient-stops-list",
|
|
1663
1631
|
);
|
|
1664
|
-
|
|
1632
|
+
const reorder = list?.querySelector("fig-reorder");
|
|
1633
|
+
if (!list || !reorder) return;
|
|
1665
1634
|
|
|
1666
|
-
const existingRows =
|
|
1635
|
+
const existingRows = reorder.querySelectorAll(
|
|
1667
1636
|
".fig-fill-picker-gradient-stop-row",
|
|
1668
1637
|
);
|
|
1669
1638
|
|
|
@@ -1674,7 +1643,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
1674
1643
|
const posInput = row.querySelector(".fig-fill-picker-stop-position");
|
|
1675
1644
|
if (posInput) posInput.setAttribute("value", stop.position);
|
|
1676
1645
|
const colorInput = row.querySelector(".fig-fill-picker-stop-color");
|
|
1677
|
-
if (colorInput)
|
|
1646
|
+
if (colorInput) {
|
|
1647
|
+
colorInput.setAttribute("value", this.#formatStopColorValue(stop));
|
|
1648
|
+
}
|
|
1678
1649
|
const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
|
|
1679
1650
|
if (removeBtn) {
|
|
1680
1651
|
if (this.#gradient.stops.length <= 2)
|
|
@@ -1689,16 +1660,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
1689
1660
|
}
|
|
1690
1661
|
|
|
1691
1662
|
#rebuildGradientStopsList(list) {
|
|
1692
|
-
|
|
1663
|
+
const reorder = list.querySelector("fig-reorder");
|
|
1664
|
+
if (!reorder) return;
|
|
1665
|
+
|
|
1666
|
+
reorder.innerHTML = this.#gradient.stops
|
|
1693
1667
|
.map(
|
|
1694
1668
|
(stop, index) => `
|
|
1695
1669
|
<fig-field class="fig-fill-picker-gradient-stop-row" data-index="${index}">
|
|
1696
1670
|
<fig-input-number class="fig-fill-picker-stop-position" aria-label="Gradient stop position" min="0" max="100" value="${
|
|
1697
1671
|
stop.position
|
|
1698
1672
|
}" units="%"></fig-input-number>
|
|
1699
|
-
<fig-input-color class="fig-fill-picker-stop-color" aria-label="Gradient stop color" text="true" alpha="true" picker="figma" picker-dialog-position="right" value="${
|
|
1700
|
-
stop
|
|
1701
|
-
}"></fig-input-color>
|
|
1673
|
+
<fig-input-color class="fig-fill-picker-stop-color" aria-label="Gradient stop color" text="true" alpha="true" picker="figma" picker-dialog-position="right" value="${this.#formatStopColorValue(
|
|
1674
|
+
stop,
|
|
1675
|
+
)}"></fig-input-color>
|
|
1702
1676
|
<fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
|
|
1703
1677
|
this.#gradient.stops.length <= 2 ? "disabled" : ""
|
|
1704
1678
|
} aria-label="Remove gradient stop">
|
|
@@ -1709,16 +1683,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
1709
1683
|
)
|
|
1710
1684
|
.join("");
|
|
1711
1685
|
|
|
1712
|
-
|
|
1686
|
+
reorder
|
|
1713
1687
|
.querySelectorAll(".fig-fill-picker-gradient-stop-row")
|
|
1714
1688
|
.forEach((row) => {
|
|
1715
|
-
const index = parseInt(row.dataset.index);
|
|
1716
|
-
|
|
1717
1689
|
row
|
|
1718
1690
|
.querySelector(".fig-fill-picker-stop-position")
|
|
1719
|
-
.addEventListener("input", (
|
|
1720
|
-
this.#
|
|
1721
|
-
parseFloat(e.target.value) || 0;
|
|
1691
|
+
.addEventListener("input", () => {
|
|
1692
|
+
this.#syncGradientStopRow(row);
|
|
1722
1693
|
this.#updateGradientPreview();
|
|
1723
1694
|
this.#emitInput();
|
|
1724
1695
|
});
|
|
@@ -1734,20 +1705,24 @@ class FigFillPicker extends HTMLElement {
|
|
|
1734
1705
|
});
|
|
1735
1706
|
}
|
|
1736
1707
|
|
|
1737
|
-
|
|
1738
|
-
this.#
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
this.#
|
|
1708
|
+
const syncStopColor = () => {
|
|
1709
|
+
this.#syncGradientStopRow(row);
|
|
1710
|
+
this.#syncingGradientBar = true;
|
|
1711
|
+
try {
|
|
1712
|
+
this.#updateGradientPreview();
|
|
1713
|
+
this.#emitInput();
|
|
1714
|
+
} finally {
|
|
1715
|
+
this.#syncingGradientBar = false;
|
|
1743
1716
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
stopColor.addEventListener("input", syncStopColor);
|
|
1720
|
+
stopColor.addEventListener("change", syncStopColor);
|
|
1747
1721
|
|
|
1748
1722
|
row
|
|
1749
1723
|
.querySelector(".fig-fill-picker-stop-remove")
|
|
1750
1724
|
.addEventListener("click", () => {
|
|
1725
|
+
const index = parseInt(row.dataset.index, 10);
|
|
1751
1726
|
if (this.#gradient.stops.length > 2) {
|
|
1752
1727
|
this.#gradient.stops.splice(index, 1);
|
|
1753
1728
|
this.#updateGradientUI();
|
|
@@ -2077,23 +2052,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
2077
2052
|
);
|
|
2078
2053
|
|
|
2079
2054
|
const startWebcam = async (deviceId = null) => {
|
|
2055
|
+
this.#stopWebcam();
|
|
2056
|
+
const requestId = this.#webcamRequestId;
|
|
2080
2057
|
try {
|
|
2081
2058
|
const constraints = {
|
|
2082
2059
|
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
|
2083
2060
|
};
|
|
2084
2061
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2062
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
2063
|
+
if (
|
|
2064
|
+
requestId !== this.#webcamRequestId ||
|
|
2065
|
+
!this.isConnected ||
|
|
2066
|
+
this.#activeTab !== "webcam"
|
|
2067
|
+
) {
|
|
2068
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
2069
|
+
return;
|
|
2087
2070
|
}
|
|
2088
2071
|
|
|
2089
|
-
this.#webcam.stream =
|
|
2090
|
-
|
|
2091
|
-
video.srcObject = this.#webcam.stream;
|
|
2072
|
+
this.#webcam.stream = stream;
|
|
2073
|
+
video.srcObject = stream;
|
|
2092
2074
|
video.style.display = "block";
|
|
2093
2075
|
status.style.display = "none";
|
|
2094
2076
|
|
|
2095
2077
|
// Enumerate cameras
|
|
2096
2078
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
2079
|
+
if (requestId !== this.#webcamRequestId) return;
|
|
2097
2080
|
const cameras = devices.filter((d) => d.kind === "videoinput");
|
|
2098
2081
|
|
|
2099
2082
|
if (cameras.length > 1) {
|
|
@@ -2125,6 +2108,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2125
2108
|
.forEach((option) => option.remove());
|
|
2126
2109
|
}
|
|
2127
2110
|
} catch (err) {
|
|
2111
|
+
if (requestId !== this.#webcamRequestId) return;
|
|
2128
2112
|
console.error("Webcam error:", err.name, err.message);
|
|
2129
2113
|
let message = "Camera access denied";
|
|
2130
2114
|
if (err.name === "NotAllowedError") {
|
|
@@ -2138,21 +2122,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
2138
2122
|
} else if (!window.isSecureContext) {
|
|
2139
2123
|
message = "Camera requires secure context";
|
|
2140
2124
|
}
|
|
2141
|
-
status.
|
|
2125
|
+
status.replaceChildren();
|
|
2126
|
+
const messageElement = document.createElement("span");
|
|
2127
|
+
messageElement.textContent = message;
|
|
2128
|
+
status.appendChild(messageElement);
|
|
2142
2129
|
status.style.display = "flex";
|
|
2143
2130
|
video.style.display = "none";
|
|
2144
2131
|
}
|
|
2145
2132
|
};
|
|
2146
|
-
|
|
2147
|
-
this.#webcamTabObserver = new MutationObserver(() => {
|
|
2148
|
-
if (container.style.display !== "none" && !this.#webcam.stream) {
|
|
2149
|
-
startWebcam();
|
|
2150
|
-
}
|
|
2151
|
-
});
|
|
2152
|
-
this.#webcamTabObserver.observe(container, {
|
|
2153
|
-
attributes: true,
|
|
2154
|
-
attributeFilter: ["style"],
|
|
2155
|
-
});
|
|
2133
|
+
this.#webcamStart = startWebcam;
|
|
2156
2134
|
|
|
2157
2135
|
cameraSelect.addEventListener("change", (e) => {
|
|
2158
2136
|
startWebcam(e.target.value);
|
|
@@ -2548,6 +2526,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
2548
2526
|
case "disabled":
|
|
2549
2527
|
this.#syncTriggerA11y();
|
|
2550
2528
|
break;
|
|
2529
|
+
case "alpha":
|
|
2530
|
+
case "mode":
|
|
2531
|
+
case "experimental": {
|
|
2532
|
+
if (!this.#dialog) break;
|
|
2533
|
+
const wasOpen = this.#dialog.open;
|
|
2534
|
+
this.#discardDialog();
|
|
2535
|
+
if (wasOpen && this.isConnected) this.#openDialog();
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2551
2538
|
case "aria-label":
|
|
2552
2539
|
case "aria-labelledby":
|
|
2553
2540
|
case "aria-describedby":
|