@rogieking/figui3 6.21.0 → 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 +63 -191
- 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/dist/propskit.js +1 -591
- package/fig-editor.js +193 -68
- package/fig-lab.css +32 -307
- package/fig-lab.js +743 -1288
- package/fig.js +1653 -342
- package/package.json +2 -2
package/fig-editor.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import "./fig.js";
|
|
2
2
|
import "./fig-lab.js";
|
|
3
3
|
|
|
4
|
+
function figEditorDefineElement(name, constructor) {
|
|
5
|
+
if (!customElements.get(name)) {
|
|
6
|
+
customElements.define(name, constructor);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
function figEditorEscapeAttribute(value) {
|
|
5
11
|
return String(value ?? "")
|
|
6
12
|
.replace(/&/g, "&")
|
|
@@ -105,10 +111,13 @@ function parseGradientInterpolationSelectValue(val) {
|
|
|
105
111
|
* @attr {boolean} alpha - Whether to show alpha/opacity controls (default: true)
|
|
106
112
|
* @attr {string} dialog-position - Position of the popup (default: "left")
|
|
107
113
|
*/
|
|
114
|
+
let figFillPickerDialogId = 0;
|
|
115
|
+
|
|
108
116
|
class FigFillPicker extends HTMLElement {
|
|
109
117
|
#trigger = null;
|
|
110
118
|
#swatch = null;
|
|
111
119
|
#dialog = null;
|
|
120
|
+
#dialogId = `fig-fill-picker-dialog-${++figFillPickerDialogId}`;
|
|
112
121
|
#activeTab = "solid";
|
|
113
122
|
anchorElement = null;
|
|
114
123
|
|
|
@@ -147,10 +156,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
147
156
|
#teardownColorAreaEvents = null;
|
|
148
157
|
#gradientInterpolationOpenObserver = null;
|
|
149
158
|
#valueAtOpen = null;
|
|
159
|
+
#lastChangeValue = null;
|
|
150
160
|
#webcamStart = null;
|
|
151
161
|
#webcamRequestId = 0;
|
|
152
162
|
#boundTriggerClick = null;
|
|
153
163
|
#boundTriggerKeydown = null;
|
|
164
|
+
#rafIds = new Set();
|
|
165
|
+
#ownedBlobUrls = new Set();
|
|
154
166
|
|
|
155
167
|
constructor() {
|
|
156
168
|
super();
|
|
@@ -164,7 +176,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
164
176
|
"disabled",
|
|
165
177
|
"alpha",
|
|
166
178
|
"mode",
|
|
167
|
-
"experimental",
|
|
168
179
|
"aria-label",
|
|
169
180
|
"aria-labelledby",
|
|
170
181
|
"aria-describedby",
|
|
@@ -175,7 +186,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
175
186
|
// Use display: contents
|
|
176
187
|
this.style.display = "contents";
|
|
177
188
|
|
|
178
|
-
|
|
189
|
+
this.#scheduleFrame(() => {
|
|
179
190
|
this.#setupTrigger();
|
|
180
191
|
this.#parseValue();
|
|
181
192
|
this.#updateSwatch();
|
|
@@ -184,19 +195,45 @@ class FigFillPicker extends HTMLElement {
|
|
|
184
195
|
|
|
185
196
|
disconnectedCallback() {
|
|
186
197
|
this.#discardDialog();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (this.#image.url === this.#webcam.snapshot) this.#image.url = null;
|
|
190
|
-
this.#webcam.snapshot = null;
|
|
191
|
-
}
|
|
192
|
-
if (this.#video.url && this.#video.url.startsWith("blob:")) {
|
|
193
|
-
URL.revokeObjectURL(this.#video.url);
|
|
194
|
-
}
|
|
198
|
+
this.#cancelFrames();
|
|
199
|
+
this.#revokeOwnedBlobUrls();
|
|
195
200
|
if (this.#swatch) this.#swatch.removeAttribute("selected");
|
|
196
201
|
if (this.#trigger) {
|
|
197
202
|
this.#trigger.removeEventListener("click", this.#boundTriggerClick);
|
|
198
203
|
this.#trigger.removeEventListener("keydown", this.#boundTriggerKeydown);
|
|
199
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
|
+
}
|
|
200
237
|
}
|
|
201
238
|
|
|
202
239
|
#setupTrigger() {
|
|
@@ -228,7 +265,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
228
265
|
|
|
229
266
|
// Prevent the swatch's internal color input from opening system picker
|
|
230
267
|
if (this.#swatch) {
|
|
231
|
-
|
|
268
|
+
this.#scheduleFrame(() => {
|
|
232
269
|
const input = this.#swatch.querySelector('input[type="color"]');
|
|
233
270
|
if (input) {
|
|
234
271
|
input.remove();
|
|
@@ -244,11 +281,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
244
281
|
|
|
245
282
|
#syncTriggerA11y() {
|
|
246
283
|
if (!this.#trigger) return;
|
|
247
|
-
const disabled = this
|
|
284
|
+
const disabled = this.#isDisabled();
|
|
248
285
|
const labelledBy = this.getAttribute("aria-labelledby");
|
|
249
286
|
if (!this.#trigger.hasAttribute("role")) this.#trigger.setAttribute("role", "button");
|
|
250
287
|
this.#trigger.setAttribute("tabindex", disabled ? "-1" : "0");
|
|
251
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");
|
|
252
293
|
if (labelledBy) {
|
|
253
294
|
this.#trigger.setAttribute("aria-labelledby", labelledBy);
|
|
254
295
|
this.#trigger.removeAttribute("aria-label");
|
|
@@ -267,11 +308,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
#handleTriggerClick(e) {
|
|
270
|
-
if (
|
|
271
|
-
this.hasAttribute("disabled") &&
|
|
272
|
-
this.getAttribute("disabled") !== "false"
|
|
273
|
-
)
|
|
274
|
-
return;
|
|
311
|
+
if (this.#isDisabled()) return;
|
|
275
312
|
e.stopPropagation();
|
|
276
313
|
e.preventDefault();
|
|
277
314
|
this.#openDialog();
|
|
@@ -279,11 +316,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
279
316
|
|
|
280
317
|
#handleTriggerKeydown(e) {
|
|
281
318
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
282
|
-
if (
|
|
283
|
-
this.hasAttribute("disabled") &&
|
|
284
|
-
this.getAttribute("disabled") !== "false"
|
|
285
|
-
)
|
|
286
|
-
return;
|
|
319
|
+
if (this.#isDisabled()) return;
|
|
287
320
|
e.preventDefault();
|
|
288
321
|
e.stopPropagation();
|
|
289
322
|
this.#openDialog();
|
|
@@ -309,9 +342,15 @@ class FigFillPicker extends HTMLElement {
|
|
|
309
342
|
this.#color = parsed.color;
|
|
310
343
|
}
|
|
311
344
|
}
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
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));
|
|
315
354
|
}
|
|
316
355
|
// Gamut UI hidden for now — lock to sRGB.
|
|
317
356
|
this.#gamut = "srgb";
|
|
@@ -410,19 +449,26 @@ class FigFillPicker extends HTMLElement {
|
|
|
410
449
|
}
|
|
411
450
|
|
|
412
451
|
#openDialog() {
|
|
452
|
+
if (this.#isDisabled()) return;
|
|
413
453
|
if (!this.#dialog) {
|
|
414
454
|
this.#createDialog();
|
|
415
455
|
}
|
|
416
456
|
|
|
417
457
|
this.#valueAtOpen = JSON.stringify(this.value);
|
|
458
|
+
this.#lastChangeValue = this.#valueAtOpen;
|
|
418
459
|
this.#switchTab(this.#fillType, { emit: false });
|
|
419
460
|
|
|
420
461
|
if (this.#swatch) this.#swatch.setAttribute("selected", "true");
|
|
421
462
|
|
|
422
463
|
this.#dialog.open = true;
|
|
464
|
+
this.#syncTriggerA11y();
|
|
423
465
|
|
|
424
|
-
|
|
425
|
-
|
|
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(() => {
|
|
426
472
|
this.#drawColorArea();
|
|
427
473
|
this.#updateHandlePosition();
|
|
428
474
|
});
|
|
@@ -434,7 +480,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
434
480
|
}
|
|
435
481
|
|
|
436
482
|
close() {
|
|
437
|
-
if (this.#dialog)
|
|
483
|
+
if (this.#dialog) {
|
|
484
|
+
this.#dialog.open = false;
|
|
485
|
+
this.#syncTriggerA11y();
|
|
486
|
+
}
|
|
438
487
|
}
|
|
439
488
|
|
|
440
489
|
#restoreCustomSlotContent() {
|
|
@@ -462,6 +511,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
462
511
|
this.#dialog = null;
|
|
463
512
|
this.#webcamStart = null;
|
|
464
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();
|
|
465
521
|
}
|
|
466
522
|
|
|
467
523
|
#stopWebcam() {
|
|
@@ -491,6 +547,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
491
547
|
|
|
492
548
|
this.#dialog = document.createElement("dialog", { is: "fig-popup" });
|
|
493
549
|
this.#dialog.setAttribute("is", "fig-popup");
|
|
550
|
+
this.#dialog.id = this.#dialogId;
|
|
551
|
+
this.#dialog.setAttribute("aria-label", this.#triggerLabel());
|
|
494
552
|
this.#dialog.setAttribute("drag", "true");
|
|
495
553
|
this.#dialog.setAttribute("handle", "fig-header");
|
|
496
554
|
this.#dialog.setAttribute("autoresize", "false");
|
|
@@ -613,13 +671,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
613
671
|
const onDialogClose = () => {
|
|
614
672
|
if (this.#swatch) this.#swatch.removeAttribute("selected");
|
|
615
673
|
this.#stopWebcam();
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
this.#valueAtOpen !== JSON.stringify(this.value)
|
|
619
|
-
) {
|
|
674
|
+
const closingValue = JSON.stringify(this.value);
|
|
675
|
+
if (this.#lastChangeValue !== null && this.#lastChangeValue !== closingValue) {
|
|
620
676
|
this.#emitChange();
|
|
621
677
|
}
|
|
622
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
|
+
});
|
|
623
687
|
this.dispatchEvent(new CustomEvent("close"));
|
|
624
688
|
};
|
|
625
689
|
this.#dialog.addEventListener("close", onDialogClose);
|
|
@@ -697,11 +761,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
697
761
|
// Update tab-specific UI after visibility change
|
|
698
762
|
if (tabName === "gradient") {
|
|
699
763
|
// Use RAF to ensure layout is complete before updating angle input
|
|
700
|
-
|
|
764
|
+
this.#scheduleFrame(() => {
|
|
701
765
|
this.#updateGradientUI();
|
|
702
766
|
const barInput = tab.querySelector(".fig-fill-picker-gradient-bar-input");
|
|
703
767
|
barInput?.refreshLayout?.();
|
|
704
|
-
|
|
768
|
+
this.#scheduleFrame(() => {
|
|
705
769
|
barInput?.refreshLayout?.();
|
|
706
770
|
});
|
|
707
771
|
});
|
|
@@ -713,6 +777,40 @@ class FigFillPicker extends HTMLElement {
|
|
|
713
777
|
if (emit) this.#emitInput();
|
|
714
778
|
}
|
|
715
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
|
+
|
|
716
814
|
// ============ SOLID TAB ============
|
|
717
815
|
#initSolidTab() {
|
|
718
816
|
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
@@ -723,6 +821,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
723
821
|
<canvas width="200" height="200"></canvas>
|
|
724
822
|
<fig-handle
|
|
725
823
|
aria-label="Color saturation and brightness"
|
|
824
|
+
role="slider"
|
|
825
|
+
aria-valuemin="0"
|
|
826
|
+
aria-valuemax="100"
|
|
726
827
|
type="color"
|
|
727
828
|
color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
|
|
728
829
|
data-no-color-picker
|
|
@@ -764,6 +865,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
764
865
|
// Setup color area
|
|
765
866
|
this.#colorArea = container.querySelector("canvas");
|
|
766
867
|
this.#colorAreaHandle = container.querySelector("fig-handle");
|
|
868
|
+
this.#syncColorAreaA11y();
|
|
767
869
|
this.#drawColorArea();
|
|
768
870
|
this.#updateHandlePosition();
|
|
769
871
|
this.#setupColorAreaEvents();
|
|
@@ -894,12 +996,13 @@ class FigFillPicker extends HTMLElement {
|
|
|
894
996
|
|
|
895
997
|
#updateHandlePosition(retryCount = 0) {
|
|
896
998
|
if (!this.#colorAreaHandle || !this.#colorArea) return;
|
|
999
|
+
this.#syncColorAreaA11y();
|
|
897
1000
|
|
|
898
1001
|
const rect = this.#colorArea.getBoundingClientRect();
|
|
899
1002
|
|
|
900
1003
|
// If the canvas isn't visible yet (0 dimensions), schedule a retry (max 5 attempts)
|
|
901
1004
|
if ((rect.width === 0 || rect.height === 0) && retryCount < 5) {
|
|
902
|
-
|
|
1005
|
+
this.#scheduleFrame(() => this.#updateHandlePosition(retryCount + 1));
|
|
903
1006
|
return;
|
|
904
1007
|
}
|
|
905
1008
|
|
|
@@ -913,6 +1016,19 @@ class FigFillPicker extends HTMLElement {
|
|
|
913
1016
|
);
|
|
914
1017
|
}
|
|
915
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
|
+
|
|
916
1032
|
#updateColorFromAreaPosition(x, y, opts = {}) {
|
|
917
1033
|
const { updateHandle = true, emitInput = true, emitChange = false } = opts;
|
|
918
1034
|
this.#color.s = Math.max(0, Math.min(100, x * 100));
|
|
@@ -922,6 +1038,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
922
1038
|
"color",
|
|
923
1039
|
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
924
1040
|
);
|
|
1041
|
+
this.#syncColorAreaA11y();
|
|
925
1042
|
}
|
|
926
1043
|
if (updateHandle) this.#updateHandlePosition();
|
|
927
1044
|
this.#updateColorInputs();
|
|
@@ -1104,7 +1221,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1104
1221
|
|
|
1105
1222
|
container.innerHTML = html;
|
|
1106
1223
|
this.#wireColorInputEvents();
|
|
1107
|
-
|
|
1224
|
+
this.#scheduleFrame(() => this.#updateColorInputs());
|
|
1108
1225
|
}
|
|
1109
1226
|
|
|
1110
1227
|
#wireColorInputEvents() {
|
|
@@ -1913,7 +2030,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1913
2030
|
if (stopFillPicker) {
|
|
1914
2031
|
stopFillPicker.anchorElement = this.#dialog;
|
|
1915
2032
|
} else {
|
|
1916
|
-
|
|
2033
|
+
this.#scheduleFrame(() => {
|
|
1917
2034
|
const fp = stopColor.querySelector("fig-fill-picker");
|
|
1918
2035
|
if (fp) fp.anchorElement = this.#dialog;
|
|
1919
2036
|
});
|
|
@@ -2018,9 +2135,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
2018
2135
|
}" units="%" ${
|
|
2019
2136
|
this.#image.scaleMode === "tile" ? "" : 'style="display: none;"'
|
|
2020
2137
|
}></fig-input-number>
|
|
2021
|
-
<fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
|
|
2022
|
-
<fig-icon name="rotate"></fig-icon>
|
|
2023
|
-
</fig-button>
|
|
2024
2138
|
</fig-field>
|
|
2025
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>
|
|
2026
2140
|
`;
|
|
@@ -2181,9 +2295,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
2181
2295
|
<fig-select-option value="crop">Crop</fig-select-option>
|
|
2182
2296
|
</fig-select-options>
|
|
2183
2297
|
</fig-select>
|
|
2184
|
-
<fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
|
|
2185
|
-
<fig-icon name="rotate"></fig-icon>
|
|
2186
|
-
</fig-button>
|
|
2187
2298
|
</fig-field>
|
|
2188
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>
|
|
2189
2300
|
`;
|
|
@@ -2240,12 +2351,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
2240
2351
|
</fig-field>
|
|
2241
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">
|
|
2242
2353
|
<video class="fig-fill-picker-webcam-video" autoplay muted playsinline></video>
|
|
2243
|
-
<div class="fig-fill-picker-webcam-status">
|
|
2354
|
+
<div class="fig-fill-picker-webcam-status" role="status" aria-live="polite">
|
|
2244
2355
|
<span>Camera access required</span>
|
|
2245
2356
|
</div>
|
|
2246
2357
|
</fig-video>
|
|
2247
2358
|
<div class="fig-fill-picker-webcam-controls">
|
|
2248
|
-
<fig-button class="fig-fill-picker-webcam-capture" variant="secondary" full>
|
|
2359
|
+
<fig-button class="fig-fill-picker-webcam-capture" variant="secondary" full disabled>
|
|
2249
2360
|
Capture
|
|
2250
2361
|
</fig-button>
|
|
2251
2362
|
</div>
|
|
@@ -2266,10 +2377,31 @@ class FigFillPicker extends HTMLElement {
|
|
|
2266
2377
|
const cameraSelect = container.querySelector(
|
|
2267
2378
|
".fig-fill-picker-camera-select",
|
|
2268
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);
|
|
2269
2398
|
|
|
2270
2399
|
const startWebcam = async (deviceId = null) => {
|
|
2271
2400
|
this.#stopWebcam();
|
|
2272
2401
|
const requestId = this.#webcamRequestId;
|
|
2402
|
+
setCaptureReady(false);
|
|
2403
|
+
status.querySelector("span").textContent = "Starting camera";
|
|
2404
|
+
status.style.display = "flex";
|
|
2273
2405
|
try {
|
|
2274
2406
|
const constraints = {
|
|
2275
2407
|
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
|
@@ -2288,7 +2420,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2288
2420
|
this.#webcam.stream = stream;
|
|
2289
2421
|
video.srcObject = stream;
|
|
2290
2422
|
video.style.display = "block";
|
|
2291
|
-
|
|
2423
|
+
updateFrameReadiness();
|
|
2292
2424
|
|
|
2293
2425
|
// Enumerate cameras
|
|
2294
2426
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
@@ -2347,6 +2479,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2347
2479
|
status.appendChild(messageElement);
|
|
2348
2480
|
status.style.display = "flex";
|
|
2349
2481
|
video.style.display = "none";
|
|
2482
|
+
setCaptureReady(false);
|
|
2350
2483
|
}
|
|
2351
2484
|
};
|
|
2352
2485
|
this.#webcamStart = startWebcam;
|
|
@@ -2373,8 +2506,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
2373
2506
|
|
|
2374
2507
|
if (this.#webcam.snapshot?.startsWith("blob:")) {
|
|
2375
2508
|
URL.revokeObjectURL(this.#webcam.snapshot);
|
|
2509
|
+
this.#ownedBlobUrls.delete(this.#webcam.snapshot);
|
|
2376
2510
|
}
|
|
2377
2511
|
this.#webcam.snapshot = URL.createObjectURL(blob);
|
|
2512
|
+
this.#ownedBlobUrls.add(this.#webcam.snapshot);
|
|
2378
2513
|
this.#image.url = this.#webcam.snapshot;
|
|
2379
2514
|
|
|
2380
2515
|
const imagePreview = this.#dialog.querySelector(
|
|
@@ -2701,6 +2836,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
2701
2836
|
|
|
2702
2837
|
// ============ EVENT EMITTERS ============
|
|
2703
2838
|
#emitInput() {
|
|
2839
|
+
if (this.#isDisabled()) return;
|
|
2704
2840
|
this.#updateSwatch();
|
|
2705
2841
|
this.dispatchEvent(
|
|
2706
2842
|
new CustomEvent("input", {
|
|
@@ -2711,6 +2847,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
2711
2847
|
}
|
|
2712
2848
|
|
|
2713
2849
|
#emitChange() {
|
|
2850
|
+
if (this.#isDisabled()) return;
|
|
2851
|
+
this.#lastChangeValue = JSON.stringify(this.value);
|
|
2714
2852
|
this.dispatchEvent(
|
|
2715
2853
|
new CustomEvent("change", {
|
|
2716
2854
|
bubbles: true,
|
|
@@ -2772,33 +2910,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
2772
2910
|
case "value":
|
|
2773
2911
|
this.#parseValue();
|
|
2774
2912
|
this.#updateSwatch();
|
|
2775
|
-
if (this.#dialog) {
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
// Just update the handle position and color inputs without rebuilding
|
|
2779
|
-
this.#updateHandlePosition();
|
|
2780
|
-
this.#updateColorInputs();
|
|
2781
|
-
// Update hue slider
|
|
2782
|
-
if (this.#hueSlider) {
|
|
2783
|
-
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
2784
|
-
}
|
|
2785
|
-
// Update opacity slider
|
|
2786
|
-
if (this.#opacitySlider) {
|
|
2787
|
-
this.#opacitySlider.setAttribute("value", this.#color.a * 100);
|
|
2788
|
-
this.#opacitySlider.setAttribute(
|
|
2789
|
-
"color",
|
|
2790
|
-
this.#hsvToHex(this.#color),
|
|
2791
|
-
);
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2913
|
+
if (this.#dialog?.open && !this.#isDraggingColor) {
|
|
2914
|
+
this.#refreshDialogUI();
|
|
2915
|
+
this.#lastChangeValue = JSON.stringify(this.value);
|
|
2794
2916
|
}
|
|
2795
2917
|
break;
|
|
2796
2918
|
case "disabled":
|
|
2797
2919
|
this.#syncTriggerA11y();
|
|
2920
|
+
if (this.#isDisabled() && this.#dialog?.open) this.close();
|
|
2798
2921
|
break;
|
|
2799
2922
|
case "alpha":
|
|
2800
|
-
case "mode":
|
|
2801
|
-
case "experimental": {
|
|
2923
|
+
case "mode": {
|
|
2802
2924
|
if (!this.#dialog) break;
|
|
2803
2925
|
const wasOpen = this.#dialog.open;
|
|
2804
2926
|
this.#discardDialog();
|
|
@@ -2806,6 +2928,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
2806
2928
|
break;
|
|
2807
2929
|
}
|
|
2808
2930
|
case "aria-label":
|
|
2931
|
+
if (this.#dialog) {
|
|
2932
|
+
this.#dialog.setAttribute("aria-label", this.#triggerLabel());
|
|
2933
|
+
}
|
|
2809
2934
|
case "aria-labelledby":
|
|
2810
2935
|
case "aria-describedby":
|
|
2811
2936
|
this.#syncTriggerA11y();
|
|
@@ -2813,4 +2938,4 @@ class FigFillPicker extends HTMLElement {
|
|
|
2813
2938
|
}
|
|
2814
2939
|
}
|
|
2815
2940
|
}
|
|
2816
|
-
|
|
2941
|
+
figEditorDefineElement("fig-fill-picker", FigFillPicker);
|