@rogieking/figui3 6.21.1 → 6.22.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/.cursor/skills/figui3/SKILL.md +1 -15
- package/.cursor/skills/propkit/SKILL.md +2 -2
- package/README.md +6 -8
- package/components.css +338 -200
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +30 -33
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +32 -103
- package/dist/fig.css +1 -1
- package/dist/fig.js +128 -39
- package/dist/propskit.css +1 -1
- package/fig-editor.js +186 -67
- package/fig-lab.css +32 -307
- package/fig-lab.js +727 -1278
- package/fig.js +1582 -277
- package/package.json +1 -1
package/fig-editor.js
CHANGED
|
@@ -111,10 +111,13 @@ function parseGradientInterpolationSelectValue(val) {
|
|
|
111
111
|
* @attr {boolean} alpha - Whether to show alpha/opacity controls (default: true)
|
|
112
112
|
* @attr {string} dialog-position - Position of the popup (default: "left")
|
|
113
113
|
*/
|
|
114
|
+
let figFillPickerDialogId = 0;
|
|
115
|
+
|
|
114
116
|
class FigFillPicker extends HTMLElement {
|
|
115
117
|
#trigger = null;
|
|
116
118
|
#swatch = null;
|
|
117
119
|
#dialog = null;
|
|
120
|
+
#dialogId = `fig-fill-picker-dialog-${++figFillPickerDialogId}`;
|
|
118
121
|
#activeTab = "solid";
|
|
119
122
|
anchorElement = null;
|
|
120
123
|
|
|
@@ -153,10 +156,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
153
156
|
#teardownColorAreaEvents = null;
|
|
154
157
|
#gradientInterpolationOpenObserver = null;
|
|
155
158
|
#valueAtOpen = null;
|
|
159
|
+
#lastChangeValue = null;
|
|
156
160
|
#webcamStart = null;
|
|
157
161
|
#webcamRequestId = 0;
|
|
158
162
|
#boundTriggerClick = null;
|
|
159
163
|
#boundTriggerKeydown = null;
|
|
164
|
+
#rafIds = new Set();
|
|
165
|
+
#ownedBlobUrls = new Set();
|
|
160
166
|
|
|
161
167
|
constructor() {
|
|
162
168
|
super();
|
|
@@ -170,7 +176,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
170
176
|
"disabled",
|
|
171
177
|
"alpha",
|
|
172
178
|
"mode",
|
|
173
|
-
"experimental",
|
|
174
179
|
"aria-label",
|
|
175
180
|
"aria-labelledby",
|
|
176
181
|
"aria-describedby",
|
|
@@ -181,7 +186,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
181
186
|
// Use display: contents
|
|
182
187
|
this.style.display = "contents";
|
|
183
188
|
|
|
184
|
-
|
|
189
|
+
this.#scheduleFrame(() => {
|
|
185
190
|
this.#setupTrigger();
|
|
186
191
|
this.#parseValue();
|
|
187
192
|
this.#updateSwatch();
|
|
@@ -190,19 +195,45 @@ class FigFillPicker extends HTMLElement {
|
|
|
190
195
|
|
|
191
196
|
disconnectedCallback() {
|
|
192
197
|
this.#discardDialog();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (this.#image.url === this.#webcam.snapshot) this.#image.url = null;
|
|
196
|
-
this.#webcam.snapshot = null;
|
|
197
|
-
}
|
|
198
|
-
if (this.#video.url && this.#video.url.startsWith("blob:")) {
|
|
199
|
-
URL.revokeObjectURL(this.#video.url);
|
|
200
|
-
}
|
|
198
|
+
this.#cancelFrames();
|
|
199
|
+
this.#revokeOwnedBlobUrls();
|
|
201
200
|
if (this.#swatch) this.#swatch.removeAttribute("selected");
|
|
202
201
|
if (this.#trigger) {
|
|
203
202
|
this.#trigger.removeEventListener("click", this.#boundTriggerClick);
|
|
204
203
|
this.#trigger.removeEventListener("keydown", this.#boundTriggerKeydown);
|
|
205
204
|
}
|
|
205
|
+
this.#trigger = null;
|
|
206
|
+
this.#swatch = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#isDisabled() {
|
|
210
|
+
return (
|
|
211
|
+
this.hasAttribute("disabled") &&
|
|
212
|
+
this.getAttribute("disabled") !== "false"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#scheduleFrame(callback) {
|
|
217
|
+
const id = requestAnimationFrame(() => {
|
|
218
|
+
this.#rafIds.delete(id);
|
|
219
|
+
if (this.isConnected) callback();
|
|
220
|
+
});
|
|
221
|
+
this.#rafIds.add(id);
|
|
222
|
+
return id;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#cancelFrames() {
|
|
226
|
+
this.#rafIds.forEach((id) => cancelAnimationFrame(id));
|
|
227
|
+
this.#rafIds.clear();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#revokeOwnedBlobUrls() {
|
|
231
|
+
this.#ownedBlobUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
232
|
+
this.#ownedBlobUrls.clear();
|
|
233
|
+
if (this.#webcam.snapshot?.startsWith("blob:")) {
|
|
234
|
+
if (this.#image.url === this.#webcam.snapshot) this.#image.url = null;
|
|
235
|
+
this.#webcam.snapshot = null;
|
|
236
|
+
}
|
|
206
237
|
}
|
|
207
238
|
|
|
208
239
|
#setupTrigger() {
|
|
@@ -234,7 +265,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
234
265
|
|
|
235
266
|
// Prevent the swatch's internal color input from opening system picker
|
|
236
267
|
if (this.#swatch) {
|
|
237
|
-
|
|
268
|
+
this.#scheduleFrame(() => {
|
|
238
269
|
const input = this.#swatch.querySelector('input[type="color"]');
|
|
239
270
|
if (input) {
|
|
240
271
|
input.remove();
|
|
@@ -250,11 +281,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
250
281
|
|
|
251
282
|
#syncTriggerA11y() {
|
|
252
283
|
if (!this.#trigger) return;
|
|
253
|
-
const disabled = this
|
|
284
|
+
const disabled = this.#isDisabled();
|
|
254
285
|
const labelledBy = this.getAttribute("aria-labelledby");
|
|
255
286
|
if (!this.#trigger.hasAttribute("role")) this.#trigger.setAttribute("role", "button");
|
|
256
287
|
this.#trigger.setAttribute("tabindex", disabled ? "-1" : "0");
|
|
257
288
|
this.#trigger.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
289
|
+
this.#trigger.setAttribute("aria-haspopup", "dialog");
|
|
290
|
+
this.#trigger.setAttribute("aria-expanded", this.#dialog?.open ? "true" : "false");
|
|
291
|
+
this.#trigger.setAttribute("aria-controls", this.#dialogId);
|
|
292
|
+
this.#trigger.removeAttribute("aria-hidden");
|
|
258
293
|
if (labelledBy) {
|
|
259
294
|
this.#trigger.setAttribute("aria-labelledby", labelledBy);
|
|
260
295
|
this.#trigger.removeAttribute("aria-label");
|
|
@@ -273,11 +308,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
273
308
|
}
|
|
274
309
|
|
|
275
310
|
#handleTriggerClick(e) {
|
|
276
|
-
if (
|
|
277
|
-
this.hasAttribute("disabled") &&
|
|
278
|
-
this.getAttribute("disabled") !== "false"
|
|
279
|
-
)
|
|
280
|
-
return;
|
|
311
|
+
if (this.#isDisabled()) return;
|
|
281
312
|
e.stopPropagation();
|
|
282
313
|
e.preventDefault();
|
|
283
314
|
this.#openDialog();
|
|
@@ -285,11 +316,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
285
316
|
|
|
286
317
|
#handleTriggerKeydown(e) {
|
|
287
318
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
288
|
-
if (
|
|
289
|
-
this.hasAttribute("disabled") &&
|
|
290
|
-
this.getAttribute("disabled") !== "false"
|
|
291
|
-
)
|
|
292
|
-
return;
|
|
319
|
+
if (this.#isDisabled()) return;
|
|
293
320
|
e.preventDefault();
|
|
294
321
|
e.stopPropagation();
|
|
295
322
|
this.#openDialog();
|
|
@@ -315,9 +342,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
315
342
|
this.#color = parsed.color;
|
|
316
343
|
}
|
|
317
344
|
}
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
// `alpha` is canonical (0-1); retain `opacity` (0-100) compatibility.
|
|
346
|
+
const parsedAlpha =
|
|
347
|
+
parsed.alpha !== undefined
|
|
348
|
+
? Number(parsed.alpha)
|
|
349
|
+
: parsed.opacity !== undefined
|
|
350
|
+
? Number(parsed.opacity) / 100
|
|
351
|
+
: undefined;
|
|
352
|
+
if (Number.isFinite(parsedAlpha)) {
|
|
353
|
+
this.#color.a = Math.max(0, Math.min(1, parsedAlpha));
|
|
321
354
|
}
|
|
322
355
|
// Gamut UI hidden for now — lock to sRGB.
|
|
323
356
|
this.#gamut = "srgb";
|
|
@@ -416,19 +449,26 @@ class FigFillPicker extends HTMLElement {
|
|
|
416
449
|
}
|
|
417
450
|
|
|
418
451
|
#openDialog() {
|
|
452
|
+
if (this.#isDisabled()) return;
|
|
419
453
|
if (!this.#dialog) {
|
|
420
454
|
this.#createDialog();
|
|
421
455
|
}
|
|
422
456
|
|
|
423
457
|
this.#valueAtOpen = JSON.stringify(this.value);
|
|
458
|
+
this.#lastChangeValue = this.#valueAtOpen;
|
|
424
459
|
this.#switchTab(this.#fillType, { emit: false });
|
|
425
460
|
|
|
426
461
|
if (this.#swatch) this.#swatch.setAttribute("selected", "true");
|
|
427
462
|
|
|
428
463
|
this.#dialog.open = true;
|
|
464
|
+
this.#syncTriggerA11y();
|
|
429
465
|
|
|
430
|
-
|
|
431
|
-
|
|
466
|
+
this.#scheduleFrame(() => {
|
|
467
|
+
const focusTarget = this.#dialog?.querySelector(
|
|
468
|
+
".fig-fill-picker-close, .fig-fill-picker-type, fig-button, button, input, [tabindex='0']",
|
|
469
|
+
);
|
|
470
|
+
focusTarget?.focus?.();
|
|
471
|
+
this.#scheduleFrame(() => {
|
|
432
472
|
this.#drawColorArea();
|
|
433
473
|
this.#updateHandlePosition();
|
|
434
474
|
});
|
|
@@ -440,7 +480,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
440
480
|
}
|
|
441
481
|
|
|
442
482
|
close() {
|
|
443
|
-
if (this.#dialog)
|
|
483
|
+
if (this.#dialog) {
|
|
484
|
+
this.#dialog.open = false;
|
|
485
|
+
this.#syncTriggerA11y();
|
|
486
|
+
}
|
|
444
487
|
}
|
|
445
488
|
|
|
446
489
|
#restoreCustomSlotContent() {
|
|
@@ -468,6 +511,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
468
511
|
this.#dialog = null;
|
|
469
512
|
this.#webcamStart = null;
|
|
470
513
|
this.#valueAtOpen = null;
|
|
514
|
+
this.#lastChangeValue = null;
|
|
515
|
+
this.#colorArea = null;
|
|
516
|
+
this.#colorAreaHandle = null;
|
|
517
|
+
this.#hueSlider = null;
|
|
518
|
+
this.#opacitySlider = null;
|
|
519
|
+
this.#syncingGradientBar = false;
|
|
520
|
+
this.#syncTriggerA11y();
|
|
471
521
|
}
|
|
472
522
|
|
|
473
523
|
#stopWebcam() {
|
|
@@ -497,6 +547,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
497
547
|
|
|
498
548
|
this.#dialog = document.createElement("dialog", { is: "fig-popup" });
|
|
499
549
|
this.#dialog.setAttribute("is", "fig-popup");
|
|
550
|
+
this.#dialog.id = this.#dialogId;
|
|
551
|
+
this.#dialog.setAttribute("aria-label", this.#triggerLabel());
|
|
500
552
|
this.#dialog.setAttribute("drag", "true");
|
|
501
553
|
this.#dialog.setAttribute("handle", "fig-header");
|
|
502
554
|
this.#dialog.setAttribute("autoresize", "false");
|
|
@@ -619,13 +671,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
619
671
|
const onDialogClose = () => {
|
|
620
672
|
if (this.#swatch) this.#swatch.removeAttribute("selected");
|
|
621
673
|
this.#stopWebcam();
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
this.#valueAtOpen !== JSON.stringify(this.value)
|
|
625
|
-
) {
|
|
674
|
+
const closingValue = JSON.stringify(this.value);
|
|
675
|
+
if (this.#lastChangeValue !== null && this.#lastChangeValue !== closingValue) {
|
|
626
676
|
this.#emitChange();
|
|
627
677
|
}
|
|
628
678
|
this.#valueAtOpen = null;
|
|
679
|
+
this.#lastChangeValue = null;
|
|
680
|
+
this.#syncTriggerA11y();
|
|
681
|
+
const returnTarget = this.#trigger;
|
|
682
|
+
this.#scheduleFrame(() => {
|
|
683
|
+
if (returnTarget?.isConnected) {
|
|
684
|
+
HTMLElement.prototype.focus.call(returnTarget);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
629
687
|
this.dispatchEvent(new CustomEvent("close"));
|
|
630
688
|
};
|
|
631
689
|
this.#dialog.addEventListener("close", onDialogClose);
|
|
@@ -703,11 +761,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
703
761
|
// Update tab-specific UI after visibility change
|
|
704
762
|
if (tabName === "gradient") {
|
|
705
763
|
// Use RAF to ensure layout is complete before updating angle input
|
|
706
|
-
|
|
764
|
+
this.#scheduleFrame(() => {
|
|
707
765
|
this.#updateGradientUI();
|
|
708
766
|
const barInput = tab.querySelector(".fig-fill-picker-gradient-bar-input");
|
|
709
767
|
barInput?.refreshLayout?.();
|
|
710
|
-
|
|
768
|
+
this.#scheduleFrame(() => {
|
|
711
769
|
barInput?.refreshLayout?.();
|
|
712
770
|
});
|
|
713
771
|
});
|
|
@@ -719,6 +777,40 @@ class FigFillPicker extends HTMLElement {
|
|
|
719
777
|
if (emit) this.#emitInput();
|
|
720
778
|
}
|
|
721
779
|
|
|
780
|
+
#refreshDialogUI() {
|
|
781
|
+
if (!this.#dialog?.open) return;
|
|
782
|
+
this.#switchTab(this.#fillType, { emit: false });
|
|
783
|
+
|
|
784
|
+
this.#drawColorArea();
|
|
785
|
+
this.#updateHandlePosition();
|
|
786
|
+
this.#updateColorInputs();
|
|
787
|
+
if (this.#hueSlider) this.#hueSlider.setAttribute("value", this.#color.h);
|
|
788
|
+
if (this.#opacitySlider) {
|
|
789
|
+
this.#opacitySlider.setAttribute("value", this.#color.a * 100);
|
|
790
|
+
this.#opacitySlider.setAttribute("color", this.#hsvToHex(this.#color));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.#updateGradientUI();
|
|
794
|
+
|
|
795
|
+
const imageTab = this.#dialog.querySelector('[data-tab="image"]');
|
|
796
|
+
const imageMode = imageTab?.querySelector(".fig-fill-picker-scale-mode");
|
|
797
|
+
const imageScale = imageTab?.querySelector(".fig-fill-picker-scale");
|
|
798
|
+
const imagePreview = imageTab?.querySelector(".fig-fill-picker-image-preview");
|
|
799
|
+
if (imageMode) imageMode.value = this.#image.scaleMode;
|
|
800
|
+
if (imageScale) {
|
|
801
|
+
imageScale.setAttribute("value", this.#image.scale);
|
|
802
|
+
imageScale.style.display =
|
|
803
|
+
this.#image.scaleMode === "tile" ? "block" : "none";
|
|
804
|
+
}
|
|
805
|
+
if (imagePreview) this.#updateImagePreview(imagePreview);
|
|
806
|
+
|
|
807
|
+
const videoTab = this.#dialog.querySelector('[data-tab="video"]');
|
|
808
|
+
const videoMode = videoTab?.querySelector(".fig-fill-picker-scale-mode");
|
|
809
|
+
const videoPreview = videoTab?.querySelector(".fig-fill-picker-video-preview");
|
|
810
|
+
if (videoMode) videoMode.value = this.#video.scaleMode;
|
|
811
|
+
if (videoPreview) this.#updateVideoPreviewStyle(videoPreview);
|
|
812
|
+
}
|
|
813
|
+
|
|
722
814
|
// ============ SOLID TAB ============
|
|
723
815
|
#initSolidTab() {
|
|
724
816
|
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
@@ -729,6 +821,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
729
821
|
<canvas width="200" height="200"></canvas>
|
|
730
822
|
<fig-handle
|
|
731
823
|
aria-label="Color saturation and brightness"
|
|
824
|
+
role="slider"
|
|
825
|
+
aria-valuemin="0"
|
|
826
|
+
aria-valuemax="100"
|
|
732
827
|
type="color"
|
|
733
828
|
color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
|
|
734
829
|
data-no-color-picker
|
|
@@ -770,6 +865,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
770
865
|
// Setup color area
|
|
771
866
|
this.#colorArea = container.querySelector("canvas");
|
|
772
867
|
this.#colorAreaHandle = container.querySelector("fig-handle");
|
|
868
|
+
this.#syncColorAreaA11y();
|
|
773
869
|
this.#drawColorArea();
|
|
774
870
|
this.#updateHandlePosition();
|
|
775
871
|
this.#setupColorAreaEvents();
|
|
@@ -900,12 +996,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
900
996
|
|
|
901
997
|
#updateHandlePosition(retryCount = 0) {
|
|
902
998
|
if (!this.#colorAreaHandle || !this.#colorArea) return;
|
|
999
|
+
this.#syncColorAreaA11y();
|
|
903
1000
|
|
|
904
1001
|
const rect = this.#colorArea.getBoundingClientRect();
|
|
905
1002
|
|
|
906
1003
|
// If the canvas isn't visible yet (0 dimensions), schedule a retry (max 5 attempts)
|
|
907
1004
|
if ((rect.width === 0 || rect.height === 0) && retryCount < 5) {
|
|
908
|
-
|
|
1005
|
+
this.#scheduleFrame(() => this.#updateHandlePosition(retryCount + 1));
|
|
909
1006
|
return;
|
|
910
1007
|
}
|
|
911
1008
|
|
|
@@ -919,6 +1016,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
919
1016
|
);
|
|
920
1017
|
}
|
|
921
1018
|
|
|
1019
|
+
#syncColorAreaA11y() {
|
|
1020
|
+
if (!this.#colorAreaHandle) return;
|
|
1021
|
+
this.#colorAreaHandle.setAttribute(
|
|
1022
|
+
"aria-valuenow",
|
|
1023
|
+
String(Math.round(this.#color.v)),
|
|
1024
|
+
);
|
|
1025
|
+
this.#colorAreaHandle.setAttribute(
|
|
1026
|
+
"aria-valuetext",
|
|
1027
|
+
`Saturation ${Math.round(this.#color.s)}%, brightness ${Math.round(this.#color.v)}%`,
|
|
1028
|
+
);
|
|
1029
|
+
this.#colorAreaHandle.removeAttribute("aria-pressed");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
922
1032
|
#updateColorFromAreaPosition(x, y, opts = {}) {
|
|
923
1033
|
const { updateHandle = true, emitInput = true, emitChange = false } = opts;
|
|
924
1034
|
this.#color.s = Math.max(0, Math.min(100, x * 100));
|
|
@@ -928,6 +1038,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
928
1038
|
"color",
|
|
929
1039
|
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
930
1040
|
);
|
|
1041
|
+
this.#syncColorAreaA11y();
|
|
931
1042
|
}
|
|
932
1043
|
if (updateHandle) this.#updateHandlePosition();
|
|
933
1044
|
this.#updateColorInputs();
|
|
@@ -1110,7 +1221,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1110
1221
|
|
|
1111
1222
|
container.innerHTML = html;
|
|
1112
1223
|
this.#wireColorInputEvents();
|
|
1113
|
-
|
|
1224
|
+
this.#scheduleFrame(() => this.#updateColorInputs());
|
|
1114
1225
|
}
|
|
1115
1226
|
|
|
1116
1227
|
#wireColorInputEvents() {
|
|
@@ -1919,7 +2030,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1919
2030
|
if (stopFillPicker) {
|
|
1920
2031
|
stopFillPicker.anchorElement = this.#dialog;
|
|
1921
2032
|
} else {
|
|
1922
|
-
|
|
2033
|
+
this.#scheduleFrame(() => {
|
|
1923
2034
|
const fp = stopColor.querySelector("fig-fill-picker");
|
|
1924
2035
|
if (fp) fp.anchorElement = this.#dialog;
|
|
1925
2036
|
});
|
|
@@ -2024,9 +2135,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
2024
2135
|
}" units="%" ${
|
|
2025
2136
|
this.#image.scaleMode === "tile" ? "" : 'style="display: none;"'
|
|
2026
2137
|
}></fig-input-number>
|
|
2027
|
-
<fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
|
|
2028
|
-
<fig-icon name="rotate"></fig-icon>
|
|
2029
|
-
</fig-button>
|
|
2030
2138
|
</fig-field>
|
|
2031
2139
|
<fig-image class="fig-fill-picker-media-preview fig-fill-picker-image-preview" upload="true" label="Upload from computer" alt="Image fill preview" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true"></fig-image>
|
|
2032
2140
|
`;
|
|
@@ -2187,9 +2295,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
2187
2295
|
<fig-select-option value="crop">Crop</fig-select-option>
|
|
2188
2296
|
</fig-select-options>
|
|
2189
2297
|
</fig-select>
|
|
2190
|
-
<fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
|
|
2191
|
-
<fig-icon name="rotate"></fig-icon>
|
|
2192
|
-
</fig-button>
|
|
2193
2298
|
</fig-field>
|
|
2194
2299
|
<fig-media class="fig-fill-picker-media-preview fig-fill-picker-video-preview" type="video" upload="true" label="Upload from computer" aria-label="Video fill preview" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" controls muted="true" loop="true"></fig-media>
|
|
2195
2300
|
`;
|
|
@@ -2246,12 +2351,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
2246
2351
|
</fig-field>
|
|
2247
2352
|
<fig-video class="fig-fill-picker-webcam-preview" aria-label="Webcam preview" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" muted="true">
|
|
2248
2353
|
<video class="fig-fill-picker-webcam-video" autoplay muted playsinline></video>
|
|
2249
|
-
<div class="fig-fill-picker-webcam-status">
|
|
2354
|
+
<div class="fig-fill-picker-webcam-status" role="status" aria-live="polite">
|
|
2250
2355
|
<span>Camera access required</span>
|
|
2251
2356
|
</div>
|
|
2252
2357
|
</fig-video>
|
|
2253
2358
|
<div class="fig-fill-picker-webcam-controls">
|
|
2254
|
-
<fig-button class="fig-fill-picker-webcam-capture" variant="secondary" full>
|
|
2359
|
+
<fig-button class="fig-fill-picker-webcam-capture" variant="secondary" full disabled>
|
|
2255
2360
|
Capture
|
|
2256
2361
|
</fig-button>
|
|
2257
2362
|
</div>
|
|
@@ -2272,10 +2377,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
2272
2377
|
const cameraSelect = container.querySelector(
|
|
2273
2378
|
".fig-fill-picker-camera-select",
|
|
2274
2379
|
);
|
|
2380
|
+
const setCaptureReady = (ready) => {
|
|
2381
|
+
if (ready) captureBtn.removeAttribute("disabled");
|
|
2382
|
+
else captureBtn.setAttribute("disabled", "");
|
|
2383
|
+
};
|
|
2384
|
+
const updateFrameReadiness = () => {
|
|
2385
|
+
const ready =
|
|
2386
|
+
!!this.#webcam.stream &&
|
|
2387
|
+
video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA &&
|
|
2388
|
+
video.videoWidth > 0 &&
|
|
2389
|
+
video.videoHeight > 0;
|
|
2390
|
+
setCaptureReady(ready);
|
|
2391
|
+
if (ready) {
|
|
2392
|
+
status.querySelector("span").textContent = "Camera ready";
|
|
2393
|
+
status.style.display = "none";
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
video.addEventListener("loadedmetadata", updateFrameReadiness);
|
|
2397
|
+
video.addEventListener("canplay", updateFrameReadiness);
|
|
2275
2398
|
|
|
2276
2399
|
const startWebcam = async (deviceId = null) => {
|
|
2277
2400
|
this.#stopWebcam();
|
|
2278
2401
|
const requestId = this.#webcamRequestId;
|
|
2402
|
+
setCaptureReady(false);
|
|
2403
|
+
status.querySelector("span").textContent = "Starting camera";
|
|
2404
|
+
status.style.display = "flex";
|
|
2279
2405
|
try {
|
|
2280
2406
|
const constraints = {
|
|
2281
2407
|
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
|
@@ -2294,7 +2420,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2294
2420
|
this.#webcam.stream = stream;
|
|
2295
2421
|
video.srcObject = stream;
|
|
2296
2422
|
video.style.display = "block";
|
|
2297
|
-
|
|
2423
|
+
updateFrameReadiness();
|
|
2298
2424
|
|
|
2299
2425
|
// Enumerate cameras
|
|
2300
2426
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
@@ -2353,6 +2479,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2353
2479
|
status.appendChild(messageElement);
|
|
2354
2480
|
status.style.display = "flex";
|
|
2355
2481
|
video.style.display = "none";
|
|
2482
|
+
setCaptureReady(false);
|
|
2356
2483
|
}
|
|
2357
2484
|
};
|
|
2358
2485
|
this.#webcamStart = startWebcam;
|
|
@@ -2379,8 +2506,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
2379
2506
|
|
|
2380
2507
|
if (this.#webcam.snapshot?.startsWith("blob:")) {
|
|
2381
2508
|
URL.revokeObjectURL(this.#webcam.snapshot);
|
|
2509
|
+
this.#ownedBlobUrls.delete(this.#webcam.snapshot);
|
|
2382
2510
|
}
|
|
2383
2511
|
this.#webcam.snapshot = URL.createObjectURL(blob);
|
|
2512
|
+
this.#ownedBlobUrls.add(this.#webcam.snapshot);
|
|
2384
2513
|
this.#image.url = this.#webcam.snapshot;
|
|
2385
2514
|
|
|
2386
2515
|
const imagePreview = this.#dialog.querySelector(
|
|
@@ -2707,6 +2836,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2707
2836
|
|
|
2708
2837
|
// ============ EVENT EMITTERS ============
|
|
2709
2838
|
#emitInput() {
|
|
2839
|
+
if (this.#isDisabled()) return;
|
|
2710
2840
|
this.#updateSwatch();
|
|
2711
2841
|
this.dispatchEvent(
|
|
2712
2842
|
new CustomEvent("input", {
|
|
@@ -2717,6 +2847,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
2717
2847
|
}
|
|
2718
2848
|
|
|
2719
2849
|
#emitChange() {
|
|
2850
|
+
if (this.#isDisabled()) return;
|
|
2851
|
+
this.#lastChangeValue = JSON.stringify(this.value);
|
|
2720
2852
|
this.dispatchEvent(
|
|
2721
2853
|
new CustomEvent("change", {
|
|
2722
2854
|
bubbles: true,
|
|
@@ -2778,33 +2910,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
2778
2910
|
case "value":
|
|
2779
2911
|
this.#parseValue();
|
|
2780
2912
|
this.#updateSwatch();
|
|
2781
|
-
if (this.#dialog) {
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
// Just update the handle position and color inputs without rebuilding
|
|
2785
|
-
this.#updateHandlePosition();
|
|
2786
|
-
this.#updateColorInputs();
|
|
2787
|
-
// Update hue slider
|
|
2788
|
-
if (this.#hueSlider) {
|
|
2789
|
-
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
2790
|
-
}
|
|
2791
|
-
// Update opacity slider
|
|
2792
|
-
if (this.#opacitySlider) {
|
|
2793
|
-
this.#opacitySlider.setAttribute("value", this.#color.a * 100);
|
|
2794
|
-
this.#opacitySlider.setAttribute(
|
|
2795
|
-
"color",
|
|
2796
|
-
this.#hsvToHex(this.#color),
|
|
2797
|
-
);
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2913
|
+
if (this.#dialog?.open && !this.#isDraggingColor) {
|
|
2914
|
+
this.#refreshDialogUI();
|
|
2915
|
+
this.#lastChangeValue = JSON.stringify(this.value);
|
|
2800
2916
|
}
|
|
2801
2917
|
break;
|
|
2802
2918
|
case "disabled":
|
|
2803
2919
|
this.#syncTriggerA11y();
|
|
2920
|
+
if (this.#isDisabled() && this.#dialog?.open) this.close();
|
|
2804
2921
|
break;
|
|
2805
2922
|
case "alpha":
|
|
2806
|
-
case "mode":
|
|
2807
|
-
case "experimental": {
|
|
2923
|
+
case "mode": {
|
|
2808
2924
|
if (!this.#dialog) break;
|
|
2809
2925
|
const wasOpen = this.#dialog.open;
|
|
2810
2926
|
this.#discardDialog();
|
|
@@ -2812,6 +2928,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
2812
2928
|
break;
|
|
2813
2929
|
}
|
|
2814
2930
|
case "aria-label":
|
|
2931
|
+
if (this.#dialog) {
|
|
2932
|
+
this.#dialog.setAttribute("aria-label", this.#triggerLabel());
|
|
2933
|
+
}
|
|
2815
2934
|
case "aria-labelledby":
|
|
2816
2935
|
case "aria-describedby":
|
|
2817
2936
|
this.#syncTriggerA11y();
|