@rogieking/figui3 6.11.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 +58 -27
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +52 -50
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +14 -14
- package/dist/fig.css +1 -1
- package/dist/fig.js +33 -45
- package/fig-editor.css +12 -1
- package/fig-editor.js +317 -334
- package/fig-lab.css +11 -10
- package/fig-lab.js +25 -26
- package/fig.js +642 -227
- 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
|
});
|
|
@@ -1446,7 +1259,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
1446
1259
|
<fig-icon name="add"></fig-icon>
|
|
1447
1260
|
</fig-button>
|
|
1448
1261
|
</fig-header>
|
|
1449
|
-
<div class="fig-fill-picker-gradient-stops-list"
|
|
1262
|
+
<div class="fig-fill-picker-gradient-stops-list">
|
|
1263
|
+
<fig-reorder></fig-reorder>
|
|
1264
|
+
</div>
|
|
1450
1265
|
</div>
|
|
1451
1266
|
<div class="fig-fill-picker-gradient-interpolation">
|
|
1452
1267
|
<fig-header class="fig-fill-picker-gradient-interpolation-header" borderless>
|
|
@@ -1493,7 +1308,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1493
1308
|
this.#updateGradientUI();
|
|
1494
1309
|
this.#emitInput();
|
|
1495
1310
|
};
|
|
1496
|
-
typeDropdown.addEventListener("input", handleTypeChange);
|
|
1497
1311
|
typeDropdown.addEventListener("change", handleTypeChange);
|
|
1498
1312
|
|
|
1499
1313
|
const interpolationDropdown = container.querySelector(
|
|
@@ -1515,7 +1329,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
1515
1329
|
this.#updateGradientUI();
|
|
1516
1330
|
this.#emitInput();
|
|
1517
1331
|
};
|
|
1518
|
-
interpolationDropdown?.addEventListener("input", handleInterpolationChange);
|
|
1519
1332
|
interpolationDropdown?.addEventListener(
|
|
1520
1333
|
"change",
|
|
1521
1334
|
handleInterpolationChange,
|
|
@@ -1536,12 +1349,14 @@ class FigFillPicker extends HTMLElement {
|
|
|
1536
1349
|
const cxInput = container.querySelector(".fig-fill-picker-gradient-cx");
|
|
1537
1350
|
const cyInput = container.querySelector(".fig-fill-picker-gradient-cy");
|
|
1538
1351
|
cxInput?.addEventListener("input", (e) => {
|
|
1539
|
-
|
|
1352
|
+
const value = Number.parseFloat(e.target.value);
|
|
1353
|
+
this.#gradient.centerX = Number.isFinite(value) ? value : 50;
|
|
1540
1354
|
this.#updateGradientPreview();
|
|
1541
1355
|
this.#emitInput();
|
|
1542
1356
|
});
|
|
1543
1357
|
cyInput?.addEventListener("input", (e) => {
|
|
1544
|
-
|
|
1358
|
+
const value = Number.parseFloat(e.target.value);
|
|
1359
|
+
this.#gradient.centerY = Number.isFinite(value) ? value : 50;
|
|
1545
1360
|
this.#updateGradientPreview();
|
|
1546
1361
|
this.#emitInput();
|
|
1547
1362
|
});
|
|
@@ -1580,6 +1395,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1580
1395
|
if (gradientBarInput) {
|
|
1581
1396
|
const syncFromBarInput = (e) => {
|
|
1582
1397
|
e.stopPropagation();
|
|
1398
|
+
if (this.#syncingGradientBar) return;
|
|
1583
1399
|
const detail = e.detail;
|
|
1584
1400
|
if (!detail?.gradient) return;
|
|
1585
1401
|
this.#gradient = normalizeGradientConfig({
|
|
@@ -1598,6 +1414,152 @@ class FigFillPicker extends HTMLElement {
|
|
|
1598
1414
|
this.#emitChange();
|
|
1599
1415
|
});
|
|
1600
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
|
+
}
|
|
1601
1563
|
}
|
|
1602
1564
|
|
|
1603
1565
|
#updateGradientUI() {
|
|
@@ -1647,6 +1609,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1647
1609
|
".fig-fill-picker-gradient-bar-input",
|
|
1648
1610
|
);
|
|
1649
1611
|
if (barInput) {
|
|
1612
|
+
this.#syncingGradientBar = true;
|
|
1650
1613
|
barInput.setAttribute(
|
|
1651
1614
|
"value",
|
|
1652
1615
|
JSON.stringify({
|
|
@@ -1654,6 +1617,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1654
1617
|
gradient: gradientToValueShape(this.#gradient),
|
|
1655
1618
|
}),
|
|
1656
1619
|
);
|
|
1620
|
+
this.#syncingGradientBar = false;
|
|
1657
1621
|
}
|
|
1658
1622
|
|
|
1659
1623
|
this.#updateSwatch();
|
|
@@ -1665,9 +1629,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
1665
1629
|
const list = this.#dialog.querySelector(
|
|
1666
1630
|
".fig-fill-picker-gradient-stops-list",
|
|
1667
1631
|
);
|
|
1668
|
-
|
|
1632
|
+
const reorder = list?.querySelector("fig-reorder");
|
|
1633
|
+
if (!list || !reorder) return;
|
|
1669
1634
|
|
|
1670
|
-
const existingRows =
|
|
1635
|
+
const existingRows = reorder.querySelectorAll(
|
|
1671
1636
|
".fig-fill-picker-gradient-stop-row",
|
|
1672
1637
|
);
|
|
1673
1638
|
|
|
@@ -1678,7 +1643,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
1678
1643
|
const posInput = row.querySelector(".fig-fill-picker-stop-position");
|
|
1679
1644
|
if (posInput) posInput.setAttribute("value", stop.position);
|
|
1680
1645
|
const colorInput = row.querySelector(".fig-fill-picker-stop-color");
|
|
1681
|
-
if (colorInput)
|
|
1646
|
+
if (colorInput) {
|
|
1647
|
+
colorInput.setAttribute("value", this.#formatStopColorValue(stop));
|
|
1648
|
+
}
|
|
1682
1649
|
const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
|
|
1683
1650
|
if (removeBtn) {
|
|
1684
1651
|
if (this.#gradient.stops.length <= 2)
|
|
@@ -1693,16 +1660,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
1693
1660
|
}
|
|
1694
1661
|
|
|
1695
1662
|
#rebuildGradientStopsList(list) {
|
|
1696
|
-
|
|
1663
|
+
const reorder = list.querySelector("fig-reorder");
|
|
1664
|
+
if (!reorder) return;
|
|
1665
|
+
|
|
1666
|
+
reorder.innerHTML = this.#gradient.stops
|
|
1697
1667
|
.map(
|
|
1698
1668
|
(stop, index) => `
|
|
1699
1669
|
<fig-field class="fig-fill-picker-gradient-stop-row" data-index="${index}">
|
|
1700
1670
|
<fig-input-number class="fig-fill-picker-stop-position" aria-label="Gradient stop position" min="0" max="100" value="${
|
|
1701
1671
|
stop.position
|
|
1702
1672
|
}" units="%"></fig-input-number>
|
|
1703
|
-
<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="${
|
|
1704
|
-
stop
|
|
1705
|
-
}"></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>
|
|
1706
1676
|
<fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
|
|
1707
1677
|
this.#gradient.stops.length <= 2 ? "disabled" : ""
|
|
1708
1678
|
} aria-label="Remove gradient stop">
|
|
@@ -1713,16 +1683,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
1713
1683
|
)
|
|
1714
1684
|
.join("");
|
|
1715
1685
|
|
|
1716
|
-
|
|
1686
|
+
reorder
|
|
1717
1687
|
.querySelectorAll(".fig-fill-picker-gradient-stop-row")
|
|
1718
1688
|
.forEach((row) => {
|
|
1719
|
-
const index = parseInt(row.dataset.index);
|
|
1720
|
-
|
|
1721
1689
|
row
|
|
1722
1690
|
.querySelector(".fig-fill-picker-stop-position")
|
|
1723
|
-
.addEventListener("input", (
|
|
1724
|
-
this.#
|
|
1725
|
-
parseFloat(e.target.value) || 0;
|
|
1691
|
+
.addEventListener("input", () => {
|
|
1692
|
+
this.#syncGradientStopRow(row);
|
|
1726
1693
|
this.#updateGradientPreview();
|
|
1727
1694
|
this.#emitInput();
|
|
1728
1695
|
});
|
|
@@ -1738,20 +1705,24 @@ class FigFillPicker extends HTMLElement {
|
|
|
1738
1705
|
});
|
|
1739
1706
|
}
|
|
1740
1707
|
|
|
1741
|
-
|
|
1742
|
-
this.#
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
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;
|
|
1747
1716
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
stopColor.addEventListener("input", syncStopColor);
|
|
1720
|
+
stopColor.addEventListener("change", syncStopColor);
|
|
1751
1721
|
|
|
1752
1722
|
row
|
|
1753
1723
|
.querySelector(".fig-fill-picker-stop-remove")
|
|
1754
1724
|
.addEventListener("click", () => {
|
|
1725
|
+
const index = parseInt(row.dataset.index, 10);
|
|
1755
1726
|
if (this.#gradient.stops.length > 2) {
|
|
1756
1727
|
this.#gradient.stops.splice(index, 1);
|
|
1757
1728
|
this.#updateGradientUI();
|
|
@@ -2081,23 +2052,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
2081
2052
|
);
|
|
2082
2053
|
|
|
2083
2054
|
const startWebcam = async (deviceId = null) => {
|
|
2055
|
+
this.#stopWebcam();
|
|
2056
|
+
const requestId = this.#webcamRequestId;
|
|
2084
2057
|
try {
|
|
2085
2058
|
const constraints = {
|
|
2086
2059
|
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
|
2087
2060
|
};
|
|
2088
2061
|
|
|
2089
|
-
|
|
2090
|
-
|
|
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;
|
|
2091
2070
|
}
|
|
2092
2071
|
|
|
2093
|
-
this.#webcam.stream =
|
|
2094
|
-
|
|
2095
|
-
video.srcObject = this.#webcam.stream;
|
|
2072
|
+
this.#webcam.stream = stream;
|
|
2073
|
+
video.srcObject = stream;
|
|
2096
2074
|
video.style.display = "block";
|
|
2097
2075
|
status.style.display = "none";
|
|
2098
2076
|
|
|
2099
2077
|
// Enumerate cameras
|
|
2100
2078
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
2079
|
+
if (requestId !== this.#webcamRequestId) return;
|
|
2101
2080
|
const cameras = devices.filter((d) => d.kind === "videoinput");
|
|
2102
2081
|
|
|
2103
2082
|
if (cameras.length > 1) {
|
|
@@ -2129,6 +2108,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2129
2108
|
.forEach((option) => option.remove());
|
|
2130
2109
|
}
|
|
2131
2110
|
} catch (err) {
|
|
2111
|
+
if (requestId !== this.#webcamRequestId) return;
|
|
2132
2112
|
console.error("Webcam error:", err.name, err.message);
|
|
2133
2113
|
let message = "Camera access denied";
|
|
2134
2114
|
if (err.name === "NotAllowedError") {
|
|
@@ -2142,21 +2122,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
2142
2122
|
} else if (!window.isSecureContext) {
|
|
2143
2123
|
message = "Camera requires secure context";
|
|
2144
2124
|
}
|
|
2145
|
-
status.
|
|
2125
|
+
status.replaceChildren();
|
|
2126
|
+
const messageElement = document.createElement("span");
|
|
2127
|
+
messageElement.textContent = message;
|
|
2128
|
+
status.appendChild(messageElement);
|
|
2146
2129
|
status.style.display = "flex";
|
|
2147
2130
|
video.style.display = "none";
|
|
2148
2131
|
}
|
|
2149
2132
|
};
|
|
2150
|
-
|
|
2151
|
-
this.#webcamTabObserver = new MutationObserver(() => {
|
|
2152
|
-
if (container.style.display !== "none" && !this.#webcam.stream) {
|
|
2153
|
-
startWebcam();
|
|
2154
|
-
}
|
|
2155
|
-
});
|
|
2156
|
-
this.#webcamTabObserver.observe(container, {
|
|
2157
|
-
attributes: true,
|
|
2158
|
-
attributeFilter: ["style"],
|
|
2159
|
-
});
|
|
2133
|
+
this.#webcamStart = startWebcam;
|
|
2160
2134
|
|
|
2161
2135
|
cameraSelect.addEventListener("change", (e) => {
|
|
2162
2136
|
startWebcam(e.target.value);
|
|
@@ -2552,6 +2526,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
2552
2526
|
case "disabled":
|
|
2553
2527
|
this.#syncTriggerA11y();
|
|
2554
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
|
+
}
|
|
2555
2538
|
case "aria-label":
|
|
2556
2539
|
case "aria-labelledby":
|
|
2557
2540
|
case "aria-describedby":
|