@rogieking/figui3 4.15.10 → 5.1.1
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 +5 -8
- package/.cursor/skills/propkit/SKILL.md +3 -3
- package/README.md +40 -28
- package/components.css +165 -440
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -0
- package/dist/fig-editor.js +167 -0
- package/dist/fig-fill-picker.css +1 -0
- package/dist/fig-fill-picker.js +160 -0
- package/dist/fig.css +1 -1
- package/dist/fig.js +40 -198
- package/fig-editor.css +333 -0
- package/fig-editor.js +2251 -0
- package/fig.js +742 -2280
- package/package.json +9 -3
package/fig.js
CHANGED
|
@@ -25,6 +25,10 @@ function createFigIcon(name, options = {}) {
|
|
|
25
25
|
return icon;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function hasFigFillPicker() {
|
|
29
|
+
return typeof customElements !== "undefined" && !!customElements.get("fig-fill-picker");
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
function figSupportsCustomizedBuiltIns() {
|
|
29
33
|
if (
|
|
30
34
|
typeof window === "undefined" ||
|
|
@@ -2441,7 +2445,51 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2441
2445
|
// Always use the rendered popup rect so beak alignment matches real final placement.
|
|
2442
2446
|
const resolvedLeft = rect.left;
|
|
2443
2447
|
const resolvedTop = rect.top;
|
|
2444
|
-
const
|
|
2448
|
+
const styles = getComputedStyle(this);
|
|
2449
|
+
const toPx = (value, fallback = 0) => {
|
|
2450
|
+
const raw = String(value || "").trim();
|
|
2451
|
+
const n = parseFloat(raw);
|
|
2452
|
+
if (!Number.isFinite(n)) return fallback;
|
|
2453
|
+
if (raw.endsWith("rem")) {
|
|
2454
|
+
return n * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
2455
|
+
}
|
|
2456
|
+
if (raw.endsWith("em")) {
|
|
2457
|
+
return n * parseFloat(styles.fontSize);
|
|
2458
|
+
}
|
|
2459
|
+
return n;
|
|
2460
|
+
};
|
|
2461
|
+
const radiusForSide = (side) => {
|
|
2462
|
+
if (side === "top") {
|
|
2463
|
+
return Math.max(
|
|
2464
|
+
toPx(styles.borderTopLeftRadius),
|
|
2465
|
+
toPx(styles.borderTopRightRadius),
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
if (side === "bottom") {
|
|
2469
|
+
return Math.max(
|
|
2470
|
+
toPx(styles.borderBottomLeftRadius),
|
|
2471
|
+
toPx(styles.borderBottomRightRadius),
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
if (side === "left") {
|
|
2475
|
+
return Math.max(
|
|
2476
|
+
toPx(styles.borderTopLeftRadius),
|
|
2477
|
+
toPx(styles.borderBottomLeftRadius),
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
if (side === "right") {
|
|
2481
|
+
return Math.max(
|
|
2482
|
+
toPx(styles.borderTopRightRadius),
|
|
2483
|
+
toPx(styles.borderBottomRightRadius),
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
return 0;
|
|
2487
|
+
};
|
|
2488
|
+
const beakWidth = toPx(
|
|
2489
|
+
styles.getPropertyValue("--fig-popup-beak-width"),
|
|
2490
|
+
16,
|
|
2491
|
+
);
|
|
2492
|
+
const edgeInset = Math.max(10, radiusForSide(beakSide) + beakWidth / 2);
|
|
2445
2493
|
|
|
2446
2494
|
let beakOffset;
|
|
2447
2495
|
if (beakSide === "top" || beakSide === "bottom") {
|
|
@@ -3574,7 +3622,7 @@ customElements.define("fig-options", FigOptions);
|
|
|
3574
3622
|
* @attr {number} min - The minimum value
|
|
3575
3623
|
* @attr {number} max - The maximum value
|
|
3576
3624
|
* @attr {number} step - The step increment
|
|
3577
|
-
* @attr {boolean} text - Whether to show a text input alongside the slider
|
|
3625
|
+
* @attr {boolean} text - Whether to show a text input alongside the slider (default true)
|
|
3578
3626
|
* @attr {string} placeholder - Placeholder for the number input when text is enabled
|
|
3579
3627
|
* @attr {string} units - The units to display after the value
|
|
3580
3628
|
* @attr {number} transform - A multiplier for the displayed value
|
|
@@ -3584,6 +3632,7 @@ customElements.define("fig-options", FigOptions);
|
|
|
3584
3632
|
class FigSlider extends HTMLElement {
|
|
3585
3633
|
#isInteracting = false;
|
|
3586
3634
|
#showEmptyTextValue = false;
|
|
3635
|
+
#value = "";
|
|
3587
3636
|
// Private fields declarations
|
|
3588
3637
|
#typeDefaults = {
|
|
3589
3638
|
range: { min: 0, max: 100, step: 1 },
|
|
@@ -3628,8 +3677,7 @@ class FigSlider extends HTMLElement {
|
|
|
3628
3677
|
const rawValue = this.getAttribute("value");
|
|
3629
3678
|
this.type = this.getAttribute("type") || "range";
|
|
3630
3679
|
this.variant = this.getAttribute("variant") || "default";
|
|
3631
|
-
this.text =
|
|
3632
|
-
this.hasAttribute("text") && this.getAttribute("text") !== "false";
|
|
3680
|
+
this.text = this.getAttribute("text") !== "false";
|
|
3633
3681
|
this.units = this.getAttribute("units") || "";
|
|
3634
3682
|
this.transform = Number(this.getAttribute("transform") || 1);
|
|
3635
3683
|
this.disabled = this.getAttribute("disabled") ? true : false;
|
|
@@ -3779,6 +3827,29 @@ class FigSlider extends HTMLElement {
|
|
|
3779
3827
|
this.#regenerateInnerHTML();
|
|
3780
3828
|
}
|
|
3781
3829
|
|
|
3830
|
+
get value() {
|
|
3831
|
+
if (this.#value !== "") return this.#value;
|
|
3832
|
+
const rawValue = this.getAttribute("value");
|
|
3833
|
+
if (rawValue !== null) return String(this.#normalizeSliderValue(rawValue));
|
|
3834
|
+
return "";
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
set value(value) {
|
|
3838
|
+
const normalized = String(this.#normalizeSliderValue(value));
|
|
3839
|
+
this.#value = normalized;
|
|
3840
|
+
if (this.input && this.input.value !== normalized) {
|
|
3841
|
+
this.input.value = normalized;
|
|
3842
|
+
this.input.setAttribute("aria-valuenow", normalized);
|
|
3843
|
+
}
|
|
3844
|
+
if (this.figInputNumber) {
|
|
3845
|
+
this.figInputNumber.setAttribute(
|
|
3846
|
+
"value",
|
|
3847
|
+
this.#showEmptyTextValue ? "" : normalized,
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
if (this.input) this.#syncProperties();
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3782
3853
|
disconnectedCallback() {
|
|
3783
3854
|
if (this.input) {
|
|
3784
3855
|
this.input.removeEventListener("input", this.#boundHandleInput);
|
|
@@ -3997,7 +4068,7 @@ class FigSlider extends HTMLElement {
|
|
|
3997
4068
|
this.#regenerateInnerHTML();
|
|
3998
4069
|
break;
|
|
3999
4070
|
case "text":
|
|
4000
|
-
this.text = newValue !==
|
|
4071
|
+
this.text = newValue !== "false";
|
|
4001
4072
|
this.#regenerateInnerHTML();
|
|
4002
4073
|
break;
|
|
4003
4074
|
default:
|
|
@@ -4024,11 +4095,13 @@ customElements.define("fig-slider", FigSlider);
|
|
|
4024
4095
|
*/
|
|
4025
4096
|
class FigInputText extends HTMLElement {
|
|
4026
4097
|
#isInteracting = false;
|
|
4098
|
+
#passwordVisible = false;
|
|
4027
4099
|
#boundMouseMove;
|
|
4028
4100
|
#boundMouseUp;
|
|
4029
4101
|
#boundWindowBlur;
|
|
4030
4102
|
#boundMouseDown;
|
|
4031
4103
|
#boundInputChange;
|
|
4104
|
+
#boundNativeInput;
|
|
4032
4105
|
|
|
4033
4106
|
constructor() {
|
|
4034
4107
|
super();
|
|
@@ -4041,6 +4114,9 @@ class FigInputText extends HTMLElement {
|
|
|
4041
4114
|
e.stopPropagation();
|
|
4042
4115
|
this.#handleInputChange(e);
|
|
4043
4116
|
};
|
|
4117
|
+
this.#boundNativeInput = () => {
|
|
4118
|
+
this.#syncSearchClearVisibility();
|
|
4119
|
+
};
|
|
4044
4120
|
}
|
|
4045
4121
|
|
|
4046
4122
|
connectedCallback() {
|
|
@@ -4109,6 +4185,10 @@ class FigInputText extends HTMLElement {
|
|
|
4109
4185
|
|
|
4110
4186
|
this.input = this.querySelector("input,textarea");
|
|
4111
4187
|
this.input.readOnly = this.readonly;
|
|
4188
|
+
this.#syncSearchPrefix();
|
|
4189
|
+
this.#syncSearchClear();
|
|
4190
|
+
this.#syncSearchClearVisibility();
|
|
4191
|
+
this.#syncPasswordToggle();
|
|
4112
4192
|
|
|
4113
4193
|
if (this.type === "number") {
|
|
4114
4194
|
if (this.getAttribute("min")) {
|
|
@@ -4124,12 +4204,15 @@ class FigInputText extends HTMLElement {
|
|
|
4124
4204
|
}
|
|
4125
4205
|
this.input.removeEventListener("change", this.#boundInputChange);
|
|
4126
4206
|
this.input.addEventListener("change", this.#boundInputChange);
|
|
4207
|
+
this.input.removeEventListener("input", this.#boundNativeInput);
|
|
4208
|
+
this.input.addEventListener("input", this.#boundNativeInput);
|
|
4127
4209
|
});
|
|
4128
4210
|
}
|
|
4129
4211
|
|
|
4130
4212
|
disconnectedCallback() {
|
|
4131
4213
|
if (this.input) {
|
|
4132
4214
|
this.input.removeEventListener("change", this.#boundInputChange);
|
|
4215
|
+
this.input.removeEventListener("input", this.#boundNativeInput);
|
|
4133
4216
|
}
|
|
4134
4217
|
this.removeEventListener("pointerdown", this.#boundMouseDown);
|
|
4135
4218
|
window.removeEventListener("pointermove", this.#boundMouseMove);
|
|
@@ -4140,6 +4223,132 @@ class FigInputText extends HTMLElement {
|
|
|
4140
4223
|
focus() {
|
|
4141
4224
|
this.input.focus();
|
|
4142
4225
|
}
|
|
4226
|
+
#syncSearchPrefix() {
|
|
4227
|
+
const generated = this.querySelector(
|
|
4228
|
+
'[slot="prepend"][data-generated="search-prefix"]',
|
|
4229
|
+
);
|
|
4230
|
+
if (this.type !== "search") {
|
|
4231
|
+
generated?.remove();
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
const prepend = this.querySelector('[slot="prepend"]');
|
|
4235
|
+
if (prepend && prepend !== generated) return;
|
|
4236
|
+
if (generated) return;
|
|
4237
|
+
|
|
4238
|
+
const icon = createFigIcon("search");
|
|
4239
|
+
icon.setAttribute("slot", "prepend");
|
|
4240
|
+
icon.setAttribute("data-generated", "search-prefix");
|
|
4241
|
+
icon.setAttribute("color", "var(--figma-color-icon)");
|
|
4242
|
+
icon.addEventListener("click", this.focus.bind(this));
|
|
4243
|
+
this.prepend(icon);
|
|
4244
|
+
}
|
|
4245
|
+
#syncSearchClear() {
|
|
4246
|
+
const generated = this.querySelector(
|
|
4247
|
+
'[slot="append"][data-generated="search-clear"]',
|
|
4248
|
+
);
|
|
4249
|
+
if (this.type !== "search") {
|
|
4250
|
+
generated?.remove();
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
const append = this.querySelector('[slot="append"]');
|
|
4254
|
+
if (append && append !== generated) return;
|
|
4255
|
+
if (generated) return;
|
|
4256
|
+
|
|
4257
|
+
const wrapper = document.createElement("span");
|
|
4258
|
+
wrapper.setAttribute("slot", "append");
|
|
4259
|
+
wrapper.setAttribute("data-generated", "search-clear");
|
|
4260
|
+
|
|
4261
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
4262
|
+
tooltip.setAttribute("text", "Clear search");
|
|
4263
|
+
|
|
4264
|
+
const button = document.createElement("fig-button");
|
|
4265
|
+
button.setAttribute("variant", "ghost");
|
|
4266
|
+
button.setAttribute("icon", "");
|
|
4267
|
+
button.setAttribute("aria-label", "Clear search");
|
|
4268
|
+
|
|
4269
|
+
const icon = createFigIcon("", { size: "small" });
|
|
4270
|
+
icon.setAttribute("color", "var(--figma-color-icon-secondary)");
|
|
4271
|
+
button.append(icon);
|
|
4272
|
+
tooltip.append(button);
|
|
4273
|
+
wrapper.append(tooltip);
|
|
4274
|
+
this.append(wrapper);
|
|
4275
|
+
icon.style.setProperty("--icon", "var(--icon-16-close)");
|
|
4276
|
+
|
|
4277
|
+
button.addEventListener("click", (e) => {
|
|
4278
|
+
e.preventDefault();
|
|
4279
|
+
e.stopPropagation();
|
|
4280
|
+
if (!this.input || this.input.value === "") {
|
|
4281
|
+
this.focus();
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
this.value = "";
|
|
4285
|
+
this.input.value = "";
|
|
4286
|
+
this.dispatchEvent(new CustomEvent("input", { detail: "", bubbles: true }));
|
|
4287
|
+
this.dispatchEvent(new CustomEvent("change", { detail: "", bubbles: true }));
|
|
4288
|
+
this.#syncSearchClearVisibility();
|
|
4289
|
+
this.focus();
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
#syncSearchClearVisibility() {
|
|
4293
|
+
if (this.type !== "search") {
|
|
4294
|
+
this.removeAttribute("data-search-has-value");
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
this.toggleAttribute("data-search-has-value", !!this.input?.value);
|
|
4298
|
+
}
|
|
4299
|
+
#syncPasswordToggle() {
|
|
4300
|
+
const generated = this.querySelector(
|
|
4301
|
+
'[slot="append"][data-generated="password-toggle"]',
|
|
4302
|
+
);
|
|
4303
|
+
if (this.type !== "password") {
|
|
4304
|
+
generated?.remove();
|
|
4305
|
+
this.#passwordVisible = false;
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
const append = this.querySelector('[slot="append"]');
|
|
4309
|
+
if (append && append !== generated) return;
|
|
4310
|
+
if (generated) {
|
|
4311
|
+
this.#updatePasswordToggle(generated);
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
const wrapper = document.createElement("span");
|
|
4316
|
+
wrapper.setAttribute("slot", "append");
|
|
4317
|
+
wrapper.setAttribute("data-generated", "password-toggle");
|
|
4318
|
+
|
|
4319
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
4320
|
+
const button = document.createElement("fig-button");
|
|
4321
|
+
button.setAttribute("variant", "ghost");
|
|
4322
|
+
button.setAttribute("icon", "");
|
|
4323
|
+
|
|
4324
|
+
const icon = createFigIcon("visible", { size: "small" });
|
|
4325
|
+
icon.setAttribute("color", "var(--figma-color-icon-secondary)");
|
|
4326
|
+
button.append(icon);
|
|
4327
|
+
tooltip.append(button);
|
|
4328
|
+
wrapper.append(tooltip);
|
|
4329
|
+
this.append(wrapper);
|
|
4330
|
+
this.#updatePasswordToggle(wrapper);
|
|
4331
|
+
|
|
4332
|
+
button.addEventListener("click", (e) => {
|
|
4333
|
+
e.preventDefault();
|
|
4334
|
+
e.stopPropagation();
|
|
4335
|
+
this.#passwordVisible = !this.#passwordVisible;
|
|
4336
|
+
if (this.input) {
|
|
4337
|
+
this.input.type = this.#passwordVisible ? "text" : "password";
|
|
4338
|
+
}
|
|
4339
|
+
this.#updatePasswordToggle(wrapper);
|
|
4340
|
+
this.focus();
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
#updatePasswordToggle(wrapper) {
|
|
4344
|
+
const tooltip = wrapper.querySelector("fig-tooltip");
|
|
4345
|
+
const button = wrapper.querySelector("fig-button");
|
|
4346
|
+
const icon = wrapper.querySelector("fig-icon");
|
|
4347
|
+
const label = this.#passwordVisible ? "Hide password" : "Show password";
|
|
4348
|
+
tooltip?.setAttribute("text", label);
|
|
4349
|
+
button?.setAttribute("aria-label", label);
|
|
4350
|
+
icon?.setAttribute("name", this.#passwordVisible ? "visible" : "hidden");
|
|
4351
|
+
}
|
|
4143
4352
|
#transformNumber(value) {
|
|
4144
4353
|
if (value === "") return "";
|
|
4145
4354
|
let transformed = Number(value) * (this.transform || 1);
|
|
@@ -4157,6 +4366,7 @@ class FigInputText extends HTMLElement {
|
|
|
4157
4366
|
}
|
|
4158
4367
|
this.value = value;
|
|
4159
4368
|
this.input.value = valueTransformed;
|
|
4369
|
+
this.#syncSearchClearVisibility();
|
|
4160
4370
|
this.dispatchEvent(
|
|
4161
4371
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4162
4372
|
);
|
|
@@ -4293,6 +4503,7 @@ class FigInputText extends HTMLElement {
|
|
|
4293
4503
|
this.value = value;
|
|
4294
4504
|
this.input.value = value;
|
|
4295
4505
|
}
|
|
4506
|
+
this.#syncSearchClearVisibility();
|
|
4296
4507
|
break;
|
|
4297
4508
|
case "min":
|
|
4298
4509
|
case "max":
|
|
@@ -4310,6 +4521,14 @@ class FigInputText extends HTMLElement {
|
|
|
4310
4521
|
this.placeholder = newValue ?? "";
|
|
4311
4522
|
this.input.placeholder = this.placeholder;
|
|
4312
4523
|
break;
|
|
4524
|
+
case "type":
|
|
4525
|
+
this.type = newValue || "text";
|
|
4526
|
+
this.input.type = this.type;
|
|
4527
|
+
this.#syncSearchPrefix();
|
|
4528
|
+
this.#syncSearchClear();
|
|
4529
|
+
this.#syncSearchClearVisibility();
|
|
4530
|
+
this.#syncPasswordToggle();
|
|
4531
|
+
break;
|
|
4313
4532
|
default:
|
|
4314
4533
|
this[name] = this.input[name] = newValue;
|
|
4315
4534
|
break;
|
|
@@ -4382,7 +4601,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4382
4601
|
e.preventDefault();
|
|
4383
4602
|
e.stopPropagation();
|
|
4384
4603
|
const btn = e.target.closest("button");
|
|
4385
|
-
if (!btn || this.disabled) return;
|
|
4604
|
+
if (!btn || this.disabled || btn.disabled) return;
|
|
4386
4605
|
const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
|
|
4387
4606
|
this.#stepValue(dir);
|
|
4388
4607
|
this.input.focus();
|
|
@@ -4392,6 +4611,31 @@ class FigInputNumber extends HTMLElement {
|
|
|
4392
4611
|
this.#stepperEl.remove();
|
|
4393
4612
|
this.#stepperEl = null;
|
|
4394
4613
|
}
|
|
4614
|
+
this.#syncStepperState();
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
#syncStepperState() {
|
|
4618
|
+
if (!this.#stepperEl) return;
|
|
4619
|
+
const up = this.#stepperEl.querySelector(".fig-stepper-up");
|
|
4620
|
+
const down = this.#stepperEl.querySelector(".fig-stepper-down");
|
|
4621
|
+
if (!up || !down) return;
|
|
4622
|
+
|
|
4623
|
+
const numericValue = this.input
|
|
4624
|
+
? this.#getNumericValue(this.input.value)
|
|
4625
|
+
: this.value;
|
|
4626
|
+
const current =
|
|
4627
|
+
numericValue !== "" && numericValue !== null && numericValue !== undefined
|
|
4628
|
+
? Number(numericValue) / (this.transform || 1)
|
|
4629
|
+
: Number(this.value);
|
|
4630
|
+
const hasCurrent = Number.isFinite(current);
|
|
4631
|
+
const disabled = Boolean(this.disabled);
|
|
4632
|
+
const atMin =
|
|
4633
|
+
hasCurrent && typeof this.min === "number" && current <= this.min;
|
|
4634
|
+
const atMax =
|
|
4635
|
+
hasCurrent && typeof this.max === "number" && current >= this.max;
|
|
4636
|
+
|
|
4637
|
+
up.disabled = disabled || atMax;
|
|
4638
|
+
down.disabled = disabled || atMin;
|
|
4395
4639
|
}
|
|
4396
4640
|
|
|
4397
4641
|
#stepValue(direction) {
|
|
@@ -4403,6 +4647,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4403
4647
|
value = this.#sanitizeInput(value, false);
|
|
4404
4648
|
this.value = value;
|
|
4405
4649
|
this.input.value = this.#formatWithUnit(this.value);
|
|
4650
|
+
this.#syncStepperState();
|
|
4406
4651
|
this.dispatchEvent(
|
|
4407
4652
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4408
4653
|
);
|
|
@@ -4513,6 +4758,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4513
4758
|
const disabledAttr = this.getAttribute("disabled");
|
|
4514
4759
|
this.disabled = this.input.disabled = disabledAttr !== "false";
|
|
4515
4760
|
}
|
|
4761
|
+
this.#syncStepperState();
|
|
4516
4762
|
|
|
4517
4763
|
this.addEventListener("pointerdown", this.#boundMouseDown);
|
|
4518
4764
|
this.input.removeEventListener("change", this.#boundInputChange);
|
|
@@ -4624,6 +4870,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4624
4870
|
this.value = "";
|
|
4625
4871
|
e.target.value = "";
|
|
4626
4872
|
}
|
|
4873
|
+
this.#syncStepperState();
|
|
4627
4874
|
this.dispatchEvent(
|
|
4628
4875
|
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
4629
4876
|
);
|
|
@@ -4649,6 +4896,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4649
4896
|
value = this.#sanitizeInput(value, false);
|
|
4650
4897
|
this.value = value;
|
|
4651
4898
|
this.input.value = this.#formatWithUnit(this.value);
|
|
4899
|
+
this.#syncStepperState();
|
|
4652
4900
|
|
|
4653
4901
|
this.dispatchEvent(
|
|
4654
4902
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
@@ -4665,6 +4913,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4665
4913
|
} else {
|
|
4666
4914
|
this.value = "";
|
|
4667
4915
|
}
|
|
4916
|
+
this.#syncStepperState();
|
|
4668
4917
|
this.dispatchEvent(
|
|
4669
4918
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4670
4919
|
);
|
|
@@ -4682,6 +4931,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4682
4931
|
this.value = "";
|
|
4683
4932
|
e.target.value = "";
|
|
4684
4933
|
}
|
|
4934
|
+
this.#syncStepperState();
|
|
4685
4935
|
this.dispatchEvent(
|
|
4686
4936
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4687
4937
|
);
|
|
@@ -4702,6 +4952,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4702
4952
|
value = this.#sanitizeInput(value, false);
|
|
4703
4953
|
this.value = value;
|
|
4704
4954
|
this.input.value = this.#formatWithUnit(this.value);
|
|
4955
|
+
this.#syncStepperState();
|
|
4705
4956
|
this.dispatchEvent(
|
|
4706
4957
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4707
4958
|
);
|
|
@@ -4784,6 +5035,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4784
5035
|
case "disabled":
|
|
4785
5036
|
this.disabled = this.input.disabled =
|
|
4786
5037
|
newValue !== null && newValue !== "false";
|
|
5038
|
+
this.#syncStepperState();
|
|
4787
5039
|
break;
|
|
4788
5040
|
case "units":
|
|
4789
5041
|
this.#rawUnits = newValue || "";
|
|
@@ -4816,15 +5068,18 @@ class FigInputNumber extends HTMLElement {
|
|
|
4816
5068
|
}
|
|
4817
5069
|
this.value = value;
|
|
4818
5070
|
this.input.value = this.#formatWithUnit(this.value);
|
|
5071
|
+
this.#syncStepperState();
|
|
4819
5072
|
break;
|
|
4820
5073
|
case "min":
|
|
4821
5074
|
case "max":
|
|
4822
5075
|
case "step":
|
|
4823
5076
|
if (newValue === null || newValue === "") {
|
|
4824
5077
|
this[name] = undefined;
|
|
5078
|
+
this.#syncStepperState();
|
|
4825
5079
|
break;
|
|
4826
5080
|
}
|
|
4827
5081
|
this[name] = Number(newValue);
|
|
5082
|
+
this.#syncStepperState();
|
|
4828
5083
|
break;
|
|
4829
5084
|
case "steppers": {
|
|
4830
5085
|
const hasSteppers = newValue !== null && newValue !== "false";
|
|
@@ -5018,17 +5273,13 @@ class FigInputColor extends HTMLElement {
|
|
|
5018
5273
|
#fillPicker;
|
|
5019
5274
|
#textInput;
|
|
5020
5275
|
#alphaInput;
|
|
5276
|
+
#suppressNativeColorClick = false;
|
|
5277
|
+
#pendingFillPickerPointerOpen = false;
|
|
5278
|
+
#nativeColorClickTimer = null;
|
|
5021
5279
|
constructor() {
|
|
5022
5280
|
super();
|
|
5023
5281
|
}
|
|
5024
5282
|
|
|
5025
|
-
get picker() {
|
|
5026
|
-
return this.getAttribute("picker") || "native";
|
|
5027
|
-
}
|
|
5028
|
-
set picker(value) {
|
|
5029
|
-
this.setAttribute("picker", value);
|
|
5030
|
-
}
|
|
5031
|
-
|
|
5032
5283
|
get alpha() {
|
|
5033
5284
|
return this.getAttribute("alpha");
|
|
5034
5285
|
}
|
|
@@ -5040,17 +5291,21 @@ class FigInputColor extends HTMLElement {
|
|
|
5040
5291
|
}
|
|
5041
5292
|
}
|
|
5042
5293
|
|
|
5043
|
-
#
|
|
5294
|
+
#fillPickerAttrs() {
|
|
5044
5295
|
const attrs = {};
|
|
5045
5296
|
const experimental = this.getAttribute("experimental");
|
|
5046
5297
|
if (experimental) attrs["experimental"] = experimental;
|
|
5047
|
-
// picker-* attributes forwarded to fill picker (except anchor, handled programmatically)
|
|
5048
5298
|
for (const { name, value } of this.attributes) {
|
|
5049
5299
|
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
5050
5300
|
attrs[name.slice(7)] = value;
|
|
5051
5301
|
}
|
|
5052
5302
|
}
|
|
5053
5303
|
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
5304
|
+
return attrs;
|
|
5305
|
+
}
|
|
5306
|
+
|
|
5307
|
+
#buildFillPickerAttrs() {
|
|
5308
|
+
const attrs = this.#fillPickerAttrs();
|
|
5054
5309
|
return Object.entries(attrs)
|
|
5055
5310
|
.map(([k, v]) => `${k}="${v}"`)
|
|
5056
5311
|
.join(" ");
|
|
@@ -5069,10 +5324,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5069
5324
|
#buildUI() {
|
|
5070
5325
|
this.#setValues(this.getAttribute("value"));
|
|
5071
5326
|
|
|
5072
|
-
const
|
|
5073
|
-
const hidePicker = this.picker === "false";
|
|
5074
|
-
const showAlpha = this.getAttribute("alpha") === "true";
|
|
5075
|
-
const fpAttrs = this.#buildFillPickerAttrs();
|
|
5327
|
+
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
5076
5328
|
const disabled = this.#disabled;
|
|
5077
5329
|
const disabledAttr = disabled ? " disabled" : "";
|
|
5078
5330
|
|
|
@@ -5097,32 +5349,14 @@ class FigInputColor extends HTMLElement {
|
|
|
5097
5349
|
}
|
|
5098
5350
|
|
|
5099
5351
|
let swatchElement = "";
|
|
5100
|
-
|
|
5101
|
-
swatchElement = useFigmaPicker
|
|
5102
|
-
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
5103
|
-
showAlpha ? "" : 'alpha="false"'
|
|
5104
|
-
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
5105
|
-
this.#alphaPercent
|
|
5106
|
-
}}'${disabledAttr}></fig-fill-picker>`
|
|
5107
|
-
: `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
|
|
5108
|
-
}
|
|
5352
|
+
swatchElement = `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
|
|
5109
5353
|
|
|
5110
5354
|
html = `<div class="input-combo">
|
|
5111
5355
|
${swatchElement}
|
|
5112
5356
|
${label}
|
|
5113
5357
|
</div>`;
|
|
5114
5358
|
} else {
|
|
5115
|
-
|
|
5116
|
-
html = ``;
|
|
5117
|
-
} else {
|
|
5118
|
-
html = useFigmaPicker
|
|
5119
|
-
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
5120
|
-
showAlpha ? "" : 'alpha="false"'
|
|
5121
|
-
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
5122
|
-
this.#alphaPercent
|
|
5123
|
-
}}'${disabledAttr}></fig-fill-picker>`
|
|
5124
|
-
: `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
|
|
5125
|
-
}
|
|
5359
|
+
html = `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
|
|
5126
5360
|
}
|
|
5127
5361
|
this.innerHTML = html;
|
|
5128
5362
|
|
|
@@ -5143,31 +5377,16 @@ class FigInputColor extends HTMLElement {
|
|
|
5143
5377
|
swatchInput?.setAttribute("disabled", "");
|
|
5144
5378
|
if (swatchInput) swatchInput.style.pointerEvents = "none";
|
|
5145
5379
|
}
|
|
5380
|
+
this.#swatch.addEventListener("pointerdown", this.#handleSwatchPointerDown.bind(this), {
|
|
5381
|
+
capture: true,
|
|
5382
|
+
});
|
|
5383
|
+
this.#swatch.addEventListener("click", this.#handleSwatchClick.bind(this), {
|
|
5384
|
+
capture: true,
|
|
5385
|
+
});
|
|
5386
|
+
swatchInput?.addEventListener("keydown", this.#handleSwatchKeyDown.bind(this));
|
|
5146
5387
|
this.#swatch.addEventListener("input", this.#handleInput.bind(this));
|
|
5147
5388
|
}
|
|
5148
5389
|
|
|
5149
|
-
// Setup fill picker (figma picker)
|
|
5150
|
-
if (this.#fillPicker) {
|
|
5151
|
-
const anchor = this.getAttribute("picker-anchor");
|
|
5152
|
-
if (anchor === "self") {
|
|
5153
|
-
this.#fillPicker.anchorElement = this;
|
|
5154
|
-
} else if (anchor) {
|
|
5155
|
-
const el = document.querySelector(anchor);
|
|
5156
|
-
if (el) this.#fillPicker.anchorElement = el;
|
|
5157
|
-
}
|
|
5158
|
-
if (this.hasAttribute("disabled")) {
|
|
5159
|
-
this.#fillPicker.setAttribute("disabled", "");
|
|
5160
|
-
}
|
|
5161
|
-
this.#fillPicker.addEventListener(
|
|
5162
|
-
"input",
|
|
5163
|
-
this.#handleFillPickerInput.bind(this),
|
|
5164
|
-
);
|
|
5165
|
-
this.#fillPicker.addEventListener(
|
|
5166
|
-
"change",
|
|
5167
|
-
this.#handleChange.bind(this),
|
|
5168
|
-
);
|
|
5169
|
-
}
|
|
5170
|
-
|
|
5171
5390
|
if (this.#textInput) {
|
|
5172
5391
|
const hex = this.rgbAlphaToHex(this.rgba, 1);
|
|
5173
5392
|
// Display without # prefix
|
|
@@ -5197,8 +5416,103 @@ class FigInputColor extends HTMLElement {
|
|
|
5197
5416
|
}
|
|
5198
5417
|
});
|
|
5199
5418
|
}
|
|
5419
|
+
|
|
5420
|
+
#syncFillPicker() {
|
|
5421
|
+
if (!this.#fillPicker) return;
|
|
5422
|
+
for (const [name, value] of Object.entries(this.#fillPickerAttrs())) {
|
|
5423
|
+
this.#fillPicker.setAttribute(name, value);
|
|
5424
|
+
}
|
|
5425
|
+
this.#fillPicker.setAttribute("mode", "solid");
|
|
5426
|
+
if (this.getAttribute("alpha") !== "false") {
|
|
5427
|
+
this.#fillPicker.removeAttribute("alpha");
|
|
5428
|
+
} else {
|
|
5429
|
+
this.#fillPicker.setAttribute("alpha", "false");
|
|
5430
|
+
}
|
|
5431
|
+
if (this.hasAttribute("disabled")) {
|
|
5432
|
+
this.#fillPicker.setAttribute("disabled", "");
|
|
5433
|
+
} else {
|
|
5434
|
+
this.#fillPicker.removeAttribute("disabled");
|
|
5435
|
+
}
|
|
5436
|
+
this.#fillPicker.anchorElement = this;
|
|
5437
|
+
this.#fillPicker.setAttribute(
|
|
5438
|
+
"value",
|
|
5439
|
+
JSON.stringify({
|
|
5440
|
+
type: "solid",
|
|
5441
|
+
color: this.hexOpaque,
|
|
5442
|
+
opacity: this.#alphaPercent,
|
|
5443
|
+
}),
|
|
5444
|
+
);
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
#ensureFillPicker() {
|
|
5448
|
+
if (!hasFigFillPicker()) return null;
|
|
5449
|
+
if (this.#fillPicker?.isConnected) {
|
|
5450
|
+
this.#syncFillPicker();
|
|
5451
|
+
return this.#fillPicker;
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
const picker = document.createElement("fig-fill-picker");
|
|
5455
|
+
picker.innerHTML = "<span hidden></span>";
|
|
5456
|
+
picker.addEventListener("input", this.#handleFillPickerInput.bind(this));
|
|
5457
|
+
picker.addEventListener("change", this.#handleChange.bind(this));
|
|
5458
|
+
this.appendChild(picker);
|
|
5459
|
+
this.#fillPicker = picker;
|
|
5460
|
+
this.#syncFillPicker();
|
|
5461
|
+
return picker;
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5464
|
+
#openFillPicker() {
|
|
5465
|
+
if (this.hasAttribute("disabled") || this.hasAttribute("swatch-disabled")) return false;
|
|
5466
|
+
const picker = this.#ensureFillPicker();
|
|
5467
|
+
if (!picker) return false;
|
|
5468
|
+
requestAnimationFrame(() => picker.open?.());
|
|
5469
|
+
return true;
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
#cancelNativeColorEvent(event) {
|
|
5473
|
+
event.preventDefault();
|
|
5474
|
+
event.stopPropagation();
|
|
5475
|
+
event.stopImmediatePropagation?.();
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
#handleSwatchPointerDown(event) {
|
|
5479
|
+
if (!hasFigFillPicker()) return;
|
|
5480
|
+
if (this.hasAttribute("disabled") || this.hasAttribute("swatch-disabled")) return;
|
|
5481
|
+
this.#pendingFillPickerPointerOpen = true;
|
|
5482
|
+
this.#suppressNativeColorClick = true;
|
|
5483
|
+
if (this.#nativeColorClickTimer) clearTimeout(this.#nativeColorClickTimer);
|
|
5484
|
+
this.#nativeColorClickTimer = setTimeout(() => {
|
|
5485
|
+
this.#suppressNativeColorClick = false;
|
|
5486
|
+
this.#pendingFillPickerPointerOpen = false;
|
|
5487
|
+
this.#nativeColorClickTimer = null;
|
|
5488
|
+
}, 500);
|
|
5489
|
+
this.#cancelNativeColorEvent(event);
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
#handleSwatchClick(event) {
|
|
5493
|
+
if (!this.#suppressNativeColorClick) return;
|
|
5494
|
+
this.#suppressNativeColorClick = false;
|
|
5495
|
+
if (this.#nativeColorClickTimer) {
|
|
5496
|
+
clearTimeout(this.#nativeColorClickTimer);
|
|
5497
|
+
this.#nativeColorClickTimer = null;
|
|
5498
|
+
}
|
|
5499
|
+
this.#cancelNativeColorEvent(event);
|
|
5500
|
+
if (this.#pendingFillPickerPointerOpen) {
|
|
5501
|
+
this.#pendingFillPickerPointerOpen = false;
|
|
5502
|
+
this.#openFillPicker();
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
#handleSwatchKeyDown(event) {
|
|
5507
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
5508
|
+
if (!hasFigFillPicker()) return;
|
|
5509
|
+
if (!this.#openFillPicker()) return;
|
|
5510
|
+
this.#cancelNativeColorEvent(event);
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5200
5513
|
#setValues(hexValue) {
|
|
5201
|
-
|
|
5514
|
+
const colorValue = hexValue || "#D9D9D9";
|
|
5515
|
+
this.rgba = this.convertToRGBA(colorValue);
|
|
5202
5516
|
this.value = this.rgbAlphaToHex(
|
|
5203
5517
|
{
|
|
5204
5518
|
r: isNaN(this.rgba.r) ? 0 : this.rgba.r,
|
|
@@ -5209,9 +5523,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5209
5523
|
);
|
|
5210
5524
|
this.hexWithAlpha = this.value.toUpperCase();
|
|
5211
5525
|
this.hexOpaque = this.hexWithAlpha.slice(0, 7);
|
|
5212
|
-
|
|
5213
|
-
this.#alphaPercent = (this.rgba.a * 100).toFixed(0);
|
|
5214
|
-
}
|
|
5526
|
+
this.#alphaPercent = colorValue.length > 7 ? (this.rgba.a * 100).toFixed(0) : 100;
|
|
5215
5527
|
this.style.setProperty("--alpha", this.rgba.a);
|
|
5216
5528
|
}
|
|
5217
5529
|
|
|
@@ -5335,7 +5647,6 @@ class FigInputColor extends HTMLElement {
|
|
|
5335
5647
|
"value",
|
|
5336
5648
|
"style",
|
|
5337
5649
|
"mode",
|
|
5338
|
-
"picker",
|
|
5339
5650
|
"experimental",
|
|
5340
5651
|
"alpha",
|
|
5341
5652
|
"text",
|
|
@@ -5382,13 +5693,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5382
5693
|
// Emitting here causes infinite loops with React and other frameworks.
|
|
5383
5694
|
break;
|
|
5384
5695
|
case "mode":
|
|
5385
|
-
|
|
5386
|
-
if (this.#fillPicker && newValue) {
|
|
5387
|
-
this.#fillPicker.setAttribute("mode", newValue);
|
|
5388
|
-
}
|
|
5389
|
-
break;
|
|
5390
|
-
case "picker":
|
|
5391
|
-
// Picker type change requires re-render
|
|
5696
|
+
this.#syncFillPicker();
|
|
5392
5697
|
break;
|
|
5393
5698
|
case "alpha":
|
|
5394
5699
|
case "text":
|
|
@@ -5414,8 +5719,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5414
5719
|
else child.removeAttribute("disabled");
|
|
5415
5720
|
}
|
|
5416
5721
|
if (this.#fillPicker) {
|
|
5417
|
-
|
|
5418
|
-
else this.#fillPicker.removeAttribute("disabled");
|
|
5722
|
+
this.#syncFillPicker();
|
|
5419
5723
|
}
|
|
5420
5724
|
}
|
|
5421
5725
|
|
|
@@ -5900,6 +6204,46 @@ class FigInputFill extends HTMLElement {
|
|
|
5900
6204
|
.join(" ");
|
|
5901
6205
|
}
|
|
5902
6206
|
|
|
6207
|
+
#fillPickerChitBackground() {
|
|
6208
|
+
switch (this.#fillType) {
|
|
6209
|
+
case "solid":
|
|
6210
|
+
return this.#solid.color;
|
|
6211
|
+
case "gradient": {
|
|
6212
|
+
const sorted = [...this.#gradient.stops].sort(
|
|
6213
|
+
(a, b) => a.position - b.position,
|
|
6214
|
+
);
|
|
6215
|
+
const stops = sorted
|
|
6216
|
+
.map((stop) => {
|
|
6217
|
+
const alpha = (stop.opacity ?? 100) / 100;
|
|
6218
|
+
if (alpha >= 1) return `${stop.color} ${stop.position}%`;
|
|
6219
|
+
const { r, g, b } = figHexToRGB(stop.color);
|
|
6220
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha}) ${stop.position}%`;
|
|
6221
|
+
})
|
|
6222
|
+
.join(", ");
|
|
6223
|
+
return `linear-gradient(${this.#gradient.angle}deg ${gradientInterpolationClause(this.#gradient)}, ${stops})`;
|
|
6224
|
+
}
|
|
6225
|
+
case "image":
|
|
6226
|
+
return this.#image.url ? `url(${this.#image.url})` : "#D9D9D9";
|
|
6227
|
+
default:
|
|
6228
|
+
return "#D9D9D9";
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
|
|
6232
|
+
#fillPickerChitAlpha() {
|
|
6233
|
+
switch (this.#fillType) {
|
|
6234
|
+
case "solid":
|
|
6235
|
+
return this.#solid.alpha;
|
|
6236
|
+
case "image":
|
|
6237
|
+
return this.#image.opacity ?? 1;
|
|
6238
|
+
case "video":
|
|
6239
|
+
return this.#video.opacity ?? 1;
|
|
6240
|
+
case "webcam":
|
|
6241
|
+
return this.#webcam.opacity ?? 1;
|
|
6242
|
+
default:
|
|
6243
|
+
return 1;
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
|
|
5903
6247
|
#syncDisabled() {
|
|
5904
6248
|
const disabled = this.hasAttribute("disabled");
|
|
5905
6249
|
for (const child of [
|
|
@@ -5982,7 +6326,9 @@ class FigInputFill extends HTMLElement {
|
|
|
5982
6326
|
<div class="input-combo">
|
|
5983
6327
|
<fig-fill-picker ${fpAttrs} value='${fillPickerValue}' ${
|
|
5984
6328
|
disabled ? "disabled" : ""
|
|
5985
|
-
}
|
|
6329
|
+
}>
|
|
6330
|
+
<fig-chit background="${this.#fillPickerChitBackground()}" alpha="${this.#fillPickerChitAlpha()}"${disabled ? " disabled" : ""}></fig-chit>
|
|
6331
|
+
</fig-fill-picker>
|
|
5986
6332
|
${controlsHtml}
|
|
5987
6333
|
</div>`;
|
|
5988
6334
|
|
|
@@ -7097,7 +7443,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
7097
7443
|
const disabled = this.hasAttribute("disabled");
|
|
7098
7444
|
const mode = this.#editMode;
|
|
7099
7445
|
|
|
7100
|
-
if (mode === "picker") {
|
|
7446
|
+
if (mode === "picker" && hasFigFillPicker()) {
|
|
7101
7447
|
const experimental = this.getAttribute("experimental");
|
|
7102
7448
|
const expAttr = experimental ? ` experimental="${experimental}"` : "";
|
|
7103
7449
|
const gradientValue = JSON.stringify(this.value);
|
|
@@ -7113,11 +7459,11 @@ class FigInputGradient extends HTMLElement {
|
|
|
7113
7459
|
|
|
7114
7460
|
this.innerHTML = `
|
|
7115
7461
|
<fig-chit background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
|
|
7116
|
-
${mode === "true" ? `<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>` : ""}`;
|
|
7462
|
+
${mode === "true" || mode === "picker" ? `<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>` : ""}`;
|
|
7117
7463
|
this.#chit = this.querySelector("fig-chit");
|
|
7118
7464
|
this.#track = this.querySelector(".fig-input-gradient-track");
|
|
7119
7465
|
|
|
7120
|
-
if (mode === "true") {
|
|
7466
|
+
if (mode === "true" || mode === "picker") {
|
|
7121
7467
|
this.#setupGhostHandle();
|
|
7122
7468
|
this.#setupEventListeners();
|
|
7123
7469
|
requestAnimationFrame(() => this.#repositionHandles());
|
|
@@ -8450,6 +8796,7 @@ class FigMedia extends HTMLElement {
|
|
|
8450
8796
|
#mediaEl = null;
|
|
8451
8797
|
#fileInput = null;
|
|
8452
8798
|
#blobUrl = null;
|
|
8799
|
+
#previewSrc = null;
|
|
8453
8800
|
#file = null;
|
|
8454
8801
|
#boundHandleFileInput = this.#handleFileInput.bind(this);
|
|
8455
8802
|
#boundHandleMediaPlay = this.#handleMediaPlay.bind(this);
|
|
@@ -8502,6 +8849,10 @@ class FigMedia extends HTMLElement {
|
|
|
8502
8849
|
return this.#file;
|
|
8503
8850
|
}
|
|
8504
8851
|
|
|
8852
|
+
#currentMediaSrc() {
|
|
8853
|
+
return this.#previewSrc || this.#src || "";
|
|
8854
|
+
}
|
|
8855
|
+
|
|
8505
8856
|
/**
|
|
8506
8857
|
* Returns a base64 data URL for the loaded image.
|
|
8507
8858
|
* Requires a CORS-clean image (same-origin or with appropriate Access-Control headers);
|
|
@@ -8509,7 +8860,7 @@ class FigMedia extends HTMLElement {
|
|
|
8509
8860
|
*/
|
|
8510
8861
|
async getBase64() {
|
|
8511
8862
|
if (this.mediaKind !== "image") return null;
|
|
8512
|
-
if (!this.#
|
|
8863
|
+
if (!this.#currentMediaSrc()) return null;
|
|
8513
8864
|
if (!this.#mediaEl) return null;
|
|
8514
8865
|
try {
|
|
8515
8866
|
if (typeof this.#mediaEl.decode === "function") {
|
|
@@ -8565,6 +8916,7 @@ class FigMedia extends HTMLElement {
|
|
|
8565
8916
|
URL.revokeObjectURL(this.#blobUrl);
|
|
8566
8917
|
this.#blobUrl = null;
|
|
8567
8918
|
}
|
|
8919
|
+
this.#previewSrc = null;
|
|
8568
8920
|
}
|
|
8569
8921
|
|
|
8570
8922
|
#removeMediaElementListeners() {
|
|
@@ -8642,7 +8994,7 @@ class FigMedia extends HTMLElement {
|
|
|
8642
8994
|
#syncGeneratedMediaElement() {
|
|
8643
8995
|
if (!this.#mediaEl) return;
|
|
8644
8996
|
if (!this.#mediaEl.hasAttribute("data-generated")) return;
|
|
8645
|
-
const src = this.#
|
|
8997
|
+
const src = this.#currentMediaSrc();
|
|
8646
8998
|
if (this.#mediaEl.getAttribute("src") !== src) {
|
|
8647
8999
|
if (src) {
|
|
8648
9000
|
this.#mediaEl.setAttribute("src", src);
|
|
@@ -8827,7 +9179,11 @@ class FigMedia extends HTMLElement {
|
|
|
8827
9179
|
fi.setAttribute("variant", "overlay");
|
|
8828
9180
|
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8829
9181
|
fi.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8830
|
-
if (this.#
|
|
9182
|
+
if (this.#file?.name) {
|
|
9183
|
+
fi.setAttribute("filename", this.#file.name);
|
|
9184
|
+
} else if (this.#src) {
|
|
9185
|
+
fi.setAttribute("url", this.#src);
|
|
9186
|
+
}
|
|
8831
9187
|
fi.addEventListener("change", this.#boundHandleFileInput);
|
|
8832
9188
|
this.append(fi);
|
|
8833
9189
|
this.#fileInput = fi;
|
|
@@ -8844,6 +9200,7 @@ class FigMedia extends HTMLElement {
|
|
|
8844
9200
|
#handleFileInput(e) {
|
|
8845
9201
|
if (e.target !== this.#fileInput) return;
|
|
8846
9202
|
const file = e.detail?.files?.[0];
|
|
9203
|
+
const cleared = e.detail?.cleared === true;
|
|
8847
9204
|
|
|
8848
9205
|
if (!file) {
|
|
8849
9206
|
if (this.#blobUrl) {
|
|
@@ -8851,7 +9208,19 @@ class FigMedia extends HTMLElement {
|
|
|
8851
9208
|
this.#blobUrl = null;
|
|
8852
9209
|
}
|
|
8853
9210
|
this.#file = null;
|
|
8854
|
-
this
|
|
9211
|
+
this.#previewSrc = null;
|
|
9212
|
+
if (cleared) this.src = "";
|
|
9213
|
+
this.#syncGeneratedMediaElement();
|
|
9214
|
+
if (this.#fileInput) {
|
|
9215
|
+
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
9216
|
+
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
9217
|
+
this.#fileInput.removeAttribute("filename");
|
|
9218
|
+
if (this.#src) {
|
|
9219
|
+
this.#fileInput.setAttribute("url", this.#src);
|
|
9220
|
+
} else {
|
|
9221
|
+
this.#fileInput.removeAttribute("url");
|
|
9222
|
+
}
|
|
9223
|
+
}
|
|
8855
9224
|
this.dispatchEvent(
|
|
8856
9225
|
new CustomEvent("change", { bubbles: true, cancelable: true }),
|
|
8857
9226
|
);
|
|
@@ -8863,8 +9232,9 @@ class FigMedia extends HTMLElement {
|
|
|
8863
9232
|
}
|
|
8864
9233
|
this.#file = file;
|
|
8865
9234
|
this.#blobUrl = URL.createObjectURL(file);
|
|
9235
|
+
this.#previewSrc = this.#blobUrl;
|
|
8866
9236
|
|
|
8867
|
-
this
|
|
9237
|
+
this.#syncGeneratedMediaElement();
|
|
8868
9238
|
|
|
8869
9239
|
this.dispatchEvent(
|
|
8870
9240
|
new CustomEvent("loaded", {
|
|
@@ -8878,9 +9248,8 @@ class FigMedia extends HTMLElement {
|
|
|
8878
9248
|
);
|
|
8879
9249
|
|
|
8880
9250
|
if (this.#fileInput) {
|
|
8881
|
-
this.#fileInput.
|
|
8882
|
-
this.#fileInput.
|
|
8883
|
-
this.#fileInput.addEventListener("change", this.#boundHandleFileInput);
|
|
9251
|
+
this.#fileInput.removeAttribute("url");
|
|
9252
|
+
this.#fileInput.setAttribute("filename", file.name);
|
|
8884
9253
|
this.#fileInput.setAttribute("label", "Replace");
|
|
8885
9254
|
}
|
|
8886
9255
|
}
|
|
@@ -8917,10 +9286,14 @@ class FigMedia extends HTMLElement {
|
|
|
8917
9286
|
if (oldValue === newValue) return;
|
|
8918
9287
|
|
|
8919
9288
|
if (name === "src") {
|
|
8920
|
-
|
|
8921
|
-
|
|
9289
|
+
const nextSrc = newValue || "";
|
|
9290
|
+
const isCurrentPreviewBlob =
|
|
9291
|
+
nextSrc && (nextSrc === this.#previewSrc || nextSrc === this.#blobUrl);
|
|
9292
|
+
this.#src = nextSrc;
|
|
9293
|
+
if (this.#blobUrl && !isCurrentPreviewBlob) {
|
|
8922
9294
|
URL.revokeObjectURL(this.#blobUrl);
|
|
8923
9295
|
this.#blobUrl = null;
|
|
9296
|
+
this.#previewSrc = null;
|
|
8924
9297
|
this.#file = null;
|
|
8925
9298
|
}
|
|
8926
9299
|
this.#syncGeneratedMediaElement();
|
|
@@ -8928,9 +9301,16 @@ class FigMedia extends HTMLElement {
|
|
|
8928
9301
|
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8929
9302
|
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8930
9303
|
if (this.#src) {
|
|
8931
|
-
|
|
9304
|
+
if (this.#file?.name) {
|
|
9305
|
+
this.#fileInput.removeAttribute("url");
|
|
9306
|
+
this.#fileInput.setAttribute("filename", this.#file.name);
|
|
9307
|
+
} else {
|
|
9308
|
+
this.#fileInput.setAttribute("url", this.#src);
|
|
9309
|
+
this.#fileInput.removeAttribute("filename");
|
|
9310
|
+
}
|
|
8932
9311
|
} else {
|
|
8933
9312
|
this.#fileInput.removeAttribute("url");
|
|
9313
|
+
this.#fileInput.removeAttribute("filename");
|
|
8934
9314
|
}
|
|
8935
9315
|
}
|
|
8936
9316
|
}
|
|
@@ -9101,7 +9481,7 @@ class FigMediaControls extends HTMLElement {
|
|
|
9101
9481
|
});
|
|
9102
9482
|
|
|
9103
9483
|
const slider = document.createElement("fig-slider");
|
|
9104
|
-
slider.setAttribute("
|
|
9484
|
+
slider.setAttribute("text", "false");
|
|
9105
9485
|
slider.setAttribute("min", "0");
|
|
9106
9486
|
slider.setAttribute("max", String(this.duration));
|
|
9107
9487
|
slider.setAttribute("step", "0.1");
|
|
@@ -9197,7 +9577,7 @@ customElements.define("fig-media-controls", FigMediaControls);
|
|
|
9197
9577
|
|
|
9198
9578
|
/* File Upload Input */
|
|
9199
9579
|
class FigInputFile extends HTMLElement {
|
|
9200
|
-
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url"];
|
|
9580
|
+
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url", "filename"];
|
|
9201
9581
|
|
|
9202
9582
|
#fileInput = null;
|
|
9203
9583
|
#filenameEl = null;
|
|
@@ -9211,6 +9591,8 @@ class FigInputFile extends HTMLElement {
|
|
|
9211
9591
|
}
|
|
9212
9592
|
|
|
9213
9593
|
get #urlFilename() {
|
|
9594
|
+
const filename = this.getAttribute("filename");
|
|
9595
|
+
if (filename) return filename;
|
|
9214
9596
|
const url = this.getAttribute("url");
|
|
9215
9597
|
if (!url) return "";
|
|
9216
9598
|
try {
|
|
@@ -9254,12 +9636,13 @@ class FigInputFile extends HTMLElement {
|
|
|
9254
9636
|
this.#files = null;
|
|
9255
9637
|
if (this.#fileInput) this.#fileInput.value = "";
|
|
9256
9638
|
this.removeAttribute("url");
|
|
9639
|
+
this.removeAttribute("filename");
|
|
9257
9640
|
this.#render();
|
|
9258
|
-
this.#emitEvents();
|
|
9641
|
+
this.#emitEvents({ cleared: true });
|
|
9259
9642
|
}
|
|
9260
9643
|
|
|
9261
|
-
#emitEvents() {
|
|
9262
|
-
const detail = { files: this.#files };
|
|
9644
|
+
#emitEvents(extraDetail = {}) {
|
|
9645
|
+
const detail = { files: this.#files, ...extraDetail };
|
|
9263
9646
|
this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
|
|
9264
9647
|
this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
|
|
9265
9648
|
}
|
|
@@ -9349,7 +9732,10 @@ class FigInputFile extends HTMLElement {
|
|
|
9349
9732
|
this.getAttribute("disabled") !== "false";
|
|
9350
9733
|
const multiple = this.hasAttribute("multiple");
|
|
9351
9734
|
const variant = this.getAttribute("variant") || "input";
|
|
9352
|
-
const hasFile =
|
|
9735
|
+
const hasFile =
|
|
9736
|
+
(this.#files && this.#files.length > 0) ||
|
|
9737
|
+
!!this.getAttribute("url") ||
|
|
9738
|
+
!!this.getAttribute("filename");
|
|
9353
9739
|
|
|
9354
9740
|
this.innerHTML = "";
|
|
9355
9741
|
|
|
@@ -9395,7 +9781,7 @@ class FigInputFile extends HTMLElement {
|
|
|
9395
9781
|
const clearTooltip = document.createElement("fig-tooltip");
|
|
9396
9782
|
clearTooltip.setAttribute("text", "Remove");
|
|
9397
9783
|
this.#clearBtn = document.createElement("fig-button");
|
|
9398
|
-
this.#clearBtn.setAttribute("variant", "ghost");
|
|
9784
|
+
this.#clearBtn.setAttribute("variant", variant === "overlay" ? "overlay" : "ghost");
|
|
9399
9785
|
this.#clearBtn.setAttribute("icon", "true");
|
|
9400
9786
|
this.#clearBtn.className = "fig-input-file-clear";
|
|
9401
9787
|
if (disabled) this.#clearBtn.setAttribute("disabled", "");
|
|
@@ -9445,7 +9831,7 @@ customElements.define("fig-input-file", FigInputFile);
|
|
|
9445
9831
|
* A bezier / spring easing curve editor with draggable control points.
|
|
9446
9832
|
* @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
|
|
9447
9833
|
* @attr {number} precision - Decimal places for output values (default 2)
|
|
9448
|
-
* @attr {boolean}
|
|
9834
|
+
* @attr {boolean} edit - Show the editor and custom preset options (default true; set "false" for presets only)
|
|
9449
9835
|
*/
|
|
9450
9836
|
class FigEasingCurve extends HTMLElement {
|
|
9451
9837
|
#cp1 = { x: 0.42, y: 0 };
|
|
@@ -9463,6 +9849,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9463
9849
|
#bezierEndpointStart = null;
|
|
9464
9850
|
#bezierEndpointEnd = null;
|
|
9465
9851
|
#dropdown = null;
|
|
9852
|
+
#valueInput = null;
|
|
9466
9853
|
#presetName = null;
|
|
9467
9854
|
#targetLine = null;
|
|
9468
9855
|
#springDuration = 0.8;
|
|
@@ -9544,7 +9931,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9544
9931
|
];
|
|
9545
9932
|
|
|
9546
9933
|
static get observedAttributes() {
|
|
9547
|
-
return ["value", "precision", "aspect-ratio"];
|
|
9934
|
+
return ["value", "precision", "aspect-ratio", "edit"];
|
|
9548
9935
|
}
|
|
9549
9936
|
|
|
9550
9937
|
connectedCallback() {
|
|
@@ -9566,6 +9953,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9566
9953
|
}
|
|
9567
9954
|
|
|
9568
9955
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
9956
|
+
if (oldValue === newValue) return;
|
|
9957
|
+
|
|
9569
9958
|
if (name === "aspect-ratio") {
|
|
9570
9959
|
figSyncCssVar(this, "--aspect-ratio", newValue);
|
|
9571
9960
|
if (this.#svg) {
|
|
@@ -9575,16 +9964,21 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9575
9964
|
return;
|
|
9576
9965
|
}
|
|
9577
9966
|
|
|
9578
|
-
if (
|
|
9967
|
+
if (name === "edit") {
|
|
9968
|
+
if (this.isConnected) this.#render();
|
|
9969
|
+
return;
|
|
9970
|
+
}
|
|
9971
|
+
|
|
9579
9972
|
if (name === "value" && newValue) {
|
|
9580
9973
|
const prevMode = this.#mode;
|
|
9581
9974
|
this.#parseValue(newValue);
|
|
9582
9975
|
this.#presetName = this.#matchPreset();
|
|
9583
|
-
if (prevMode !== this.#mode) {
|
|
9976
|
+
if (prevMode !== this.#mode && this.#isEditEnabled()) {
|
|
9584
9977
|
this.#render();
|
|
9585
9978
|
} else {
|
|
9586
|
-
this.#updatePaths();
|
|
9979
|
+
if (this.#svg) this.#updatePaths();
|
|
9587
9980
|
this.#syncDropdown();
|
|
9981
|
+
this.#syncValueInput();
|
|
9588
9982
|
}
|
|
9589
9983
|
}
|
|
9590
9984
|
if (name === "precision") {
|
|
@@ -9635,7 +10029,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9635
10029
|
this.#spring.stiffness = parseFloat(springMatch[1]);
|
|
9636
10030
|
this.#spring.damping = parseFloat(springMatch[2]);
|
|
9637
10031
|
this.#spring.mass = parseFloat(springMatch[3]);
|
|
9638
|
-
return;
|
|
10032
|
+
return true;
|
|
9639
10033
|
}
|
|
9640
10034
|
const parts = str.split(",").map((s) => parseFloat(s.trim()));
|
|
9641
10035
|
if (parts.length >= 4 && parts.every((n) => !isNaN(n))) {
|
|
@@ -9644,7 +10038,9 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9644
10038
|
this.#cp1.y = parts[1];
|
|
9645
10039
|
this.#cp2.x = parts[2];
|
|
9646
10040
|
this.#cp2.y = parts[3];
|
|
10041
|
+
return true;
|
|
9647
10042
|
}
|
|
10043
|
+
return false;
|
|
9648
10044
|
}
|
|
9649
10045
|
|
|
9650
10046
|
#matchPreset() {
|
|
@@ -9777,23 +10173,38 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9777
10173
|
|
|
9778
10174
|
// --- Rendering ---
|
|
9779
10175
|
|
|
10176
|
+
#isEditEnabled() {
|
|
10177
|
+
return this.getAttribute("edit") !== "false";
|
|
10178
|
+
}
|
|
10179
|
+
|
|
9780
10180
|
#render() {
|
|
9781
10181
|
this.classList.toggle("spring-mode", this.#mode === "spring");
|
|
9782
10182
|
this.classList.toggle("bezier-mode", this.#mode !== "spring");
|
|
9783
10183
|
this.#syncMetricsFromCSS();
|
|
9784
10184
|
this.innerHTML = this.#getInnerHTML();
|
|
9785
10185
|
this.#cacheRefs();
|
|
9786
|
-
this.#
|
|
9787
|
-
|
|
9788
|
-
|
|
10186
|
+
if (this.#svg) {
|
|
10187
|
+
this.#syncHandleSizes();
|
|
10188
|
+
this.#syncViewportSize();
|
|
10189
|
+
this.#updatePaths();
|
|
10190
|
+
}
|
|
10191
|
+
this.#syncValueInput();
|
|
9789
10192
|
this.#setupEvents();
|
|
9790
10193
|
}
|
|
9791
10194
|
|
|
10195
|
+
static #escapeAttribute(value) {
|
|
10196
|
+
return String(value)
|
|
10197
|
+
.replace(/&/g, "&")
|
|
10198
|
+
.replace(/"/g, """)
|
|
10199
|
+
.replace(/</g, "<")
|
|
10200
|
+
.replace(/>/g, ">");
|
|
10201
|
+
}
|
|
10202
|
+
|
|
9792
10203
|
#getDropdownHTML() {
|
|
9793
|
-
if (this.getAttribute("dropdown") !== "true") return "";
|
|
9794
10204
|
let optionsHTML = "";
|
|
9795
10205
|
let currentGroup = undefined;
|
|
9796
10206
|
for (const p of FigEasingCurve.PRESETS) {
|
|
10207
|
+
if (!this.#isEditEnabled() && !p.value && !p.spring) continue;
|
|
9797
10208
|
if (p.group !== currentGroup) {
|
|
9798
10209
|
if (currentGroup !== undefined) optionsHTML += `</optgroup>`;
|
|
9799
10210
|
if (p.group) optionsHTML += `<optgroup label="${p.group}">`;
|
|
@@ -9822,6 +10233,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9822
10233
|
#getInnerHTML() {
|
|
9823
10234
|
const size = 200;
|
|
9824
10235
|
const dropdown = this.#getDropdownHTML();
|
|
10236
|
+
if (!this.#isEditEnabled()) return dropdown;
|
|
10237
|
+
const valueInput = `<fig-input-text class="fig-easing-curve-value-input" value="${FigEasingCurve.#escapeAttribute(this.value)}" full></fig-input-text>`;
|
|
9825
10238
|
|
|
9826
10239
|
if (this.#mode === "spring") {
|
|
9827
10240
|
const targetY = 40;
|
|
@@ -9833,7 +10246,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9833
10246
|
<path class="fig-easing-curve-path"/>
|
|
9834
10247
|
<foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9835
10248
|
<foreignObject class="fig-easing-curve-handle fig-easing-curve-duration-bar" data-handle="duration" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9836
|
-
</svg></div
|
|
10249
|
+
</svg></div>${valueInput}`;
|
|
9837
10250
|
}
|
|
9838
10251
|
|
|
9839
10252
|
return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
@@ -9846,7 +10259,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9846
10259
|
<circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
|
|
9847
10260
|
<foreignObject class="fig-easing-curve-handle" data-handle="1" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9848
10261
|
<foreignObject class="fig-easing-curve-handle" data-handle="2" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9849
|
-
</svg></div
|
|
10262
|
+
</svg></div>${valueInput}`;
|
|
9850
10263
|
}
|
|
9851
10264
|
|
|
9852
10265
|
#readCssNumber(name, fallback) {
|
|
@@ -9881,6 +10294,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9881
10294
|
this.#bezierEndpointStart = this.querySelector('[data-endpoint="start"]');
|
|
9882
10295
|
this.#bezierEndpointEnd = this.querySelector('[data-endpoint="end"]');
|
|
9883
10296
|
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
10297
|
+
this.#valueInput = this.querySelector(".fig-easing-curve-value-input");
|
|
9884
10298
|
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
9885
10299
|
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
9886
10300
|
this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
|
|
@@ -9964,6 +10378,17 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9964
10378
|
}
|
|
9965
10379
|
}
|
|
9966
10380
|
|
|
10381
|
+
#syncActiveBezierArm() {
|
|
10382
|
+
this.#line1?.classList.toggle(
|
|
10383
|
+
"is-active",
|
|
10384
|
+
this.#mode === "bezier" && this.#isDragging === 1,
|
|
10385
|
+
);
|
|
10386
|
+
this.#line2?.classList.toggle(
|
|
10387
|
+
"is-active",
|
|
10388
|
+
this.#mode === "bezier" && this.#isDragging === 2,
|
|
10389
|
+
);
|
|
10390
|
+
}
|
|
10391
|
+
|
|
9967
10392
|
#updateBezierPaths() {
|
|
9968
10393
|
if (this.#bounds) {
|
|
9969
10394
|
this.#bounds.setAttribute("x", "0");
|
|
@@ -10096,6 +10521,47 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10096
10521
|
this.#refreshCustomPresetIcons();
|
|
10097
10522
|
}
|
|
10098
10523
|
|
|
10524
|
+
#syncValueInput() {
|
|
10525
|
+
if (!this.#valueInput) return;
|
|
10526
|
+
this.#valueInput.setAttribute("value", this.value);
|
|
10527
|
+
}
|
|
10528
|
+
|
|
10529
|
+
#parseManualBezierValue(value) {
|
|
10530
|
+
const parts = value.split(",").map((part) => Number.parseFloat(part.trim()));
|
|
10531
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
10532
|
+
return null;
|
|
10533
|
+
}
|
|
10534
|
+
if (parts[0] < 0 || parts[0] > 1 || parts[2] < 0 || parts[2] > 1) {
|
|
10535
|
+
return null;
|
|
10536
|
+
}
|
|
10537
|
+
return parts;
|
|
10538
|
+
}
|
|
10539
|
+
|
|
10540
|
+
#applyManualValue(value, eventType) {
|
|
10541
|
+
const parts = this.#parseManualBezierValue(value);
|
|
10542
|
+
if (!parts) {
|
|
10543
|
+
if (eventType === "change") this.#syncValueInput();
|
|
10544
|
+
return;
|
|
10545
|
+
}
|
|
10546
|
+
|
|
10547
|
+
const prevMode = this.#mode;
|
|
10548
|
+
this.#mode = "bezier";
|
|
10549
|
+
this.#cp1.x = parts[0];
|
|
10550
|
+
this.#cp1.y = parts[1];
|
|
10551
|
+
this.#cp2.x = parts[2];
|
|
10552
|
+
this.#cp2.y = parts[3];
|
|
10553
|
+
|
|
10554
|
+
this.#presetName = this.#matchPreset();
|
|
10555
|
+
if (prevMode !== this.#mode) {
|
|
10556
|
+
this.#render();
|
|
10557
|
+
} else {
|
|
10558
|
+
this.#updatePaths();
|
|
10559
|
+
this.#syncDropdown();
|
|
10560
|
+
if (eventType === "change") this.#syncValueInput();
|
|
10561
|
+
}
|
|
10562
|
+
this.#emit(eventType);
|
|
10563
|
+
}
|
|
10564
|
+
|
|
10099
10565
|
#setOptionIconByValue(root, optionValue, icon) {
|
|
10100
10566
|
if (!root) return;
|
|
10101
10567
|
for (const option of root.querySelectorAll("option")) {
|
|
@@ -10107,6 +10573,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10107
10573
|
|
|
10108
10574
|
#refreshCustomPresetIcons() {
|
|
10109
10575
|
if (!this.#dropdown) return;
|
|
10576
|
+
if (!this.#isEditEnabled()) return;
|
|
10110
10577
|
const bezierIcon = FigEasingCurve.curveIcon(
|
|
10111
10578
|
this.#cp1.x,
|
|
10112
10579
|
this.#cp1.y,
|
|
@@ -10147,7 +10614,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10147
10614
|
}
|
|
10148
10615
|
|
|
10149
10616
|
#setupEvents() {
|
|
10150
|
-
if (this.#mode === "bezier") {
|
|
10617
|
+
if (this.#svg && this.#mode === "bezier") {
|
|
10151
10618
|
this.#handle1.addEventListener("pointerdown", (e) =>
|
|
10152
10619
|
this.#startBezierDrag(e, 1),
|
|
10153
10620
|
);
|
|
@@ -10165,7 +10632,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10165
10632
|
this.#startBezierDrag(e, this.#bezierHandleForClientHalf(e));
|
|
10166
10633
|
});
|
|
10167
10634
|
}
|
|
10168
|
-
} else {
|
|
10635
|
+
} else if (this.#svg) {
|
|
10169
10636
|
this.#handle1.addEventListener("pointerdown", (e) => {
|
|
10170
10637
|
e.stopPropagation();
|
|
10171
10638
|
this.#startSpringDrag(e, "bounce");
|
|
@@ -10204,8 +10671,9 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10204
10671
|
if (this.#mode !== "bezier") {
|
|
10205
10672
|
this.#mode = "bezier";
|
|
10206
10673
|
this.#render();
|
|
10207
|
-
} else {
|
|
10674
|
+
} else if (this.#svg) {
|
|
10208
10675
|
this.#updatePaths();
|
|
10676
|
+
this.#syncValueInput();
|
|
10209
10677
|
}
|
|
10210
10678
|
} else if (preset.type === "spring") {
|
|
10211
10679
|
if (preset.spring) {
|
|
@@ -10215,14 +10683,30 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10215
10683
|
if (this.#mode !== "spring") {
|
|
10216
10684
|
this.#mode = "spring";
|
|
10217
10685
|
this.#render();
|
|
10218
|
-
} else {
|
|
10686
|
+
} else if (this.#svg) {
|
|
10219
10687
|
this.#updatePaths();
|
|
10688
|
+
this.#syncValueInput();
|
|
10220
10689
|
}
|
|
10221
10690
|
}
|
|
10222
10691
|
this.#emit("input");
|
|
10223
10692
|
this.#emit("change");
|
|
10224
10693
|
});
|
|
10225
10694
|
}
|
|
10695
|
+
|
|
10696
|
+
if (this.#valueInput) {
|
|
10697
|
+
this.#valueInput.addEventListener("input", (e) => {
|
|
10698
|
+
e.stopPropagation();
|
|
10699
|
+
const value = e.detail ?? e.target?.value;
|
|
10700
|
+
if (typeof value !== "string") return;
|
|
10701
|
+
this.#applyManualValue(value, "input");
|
|
10702
|
+
});
|
|
10703
|
+
this.#valueInput.addEventListener("change", (e) => {
|
|
10704
|
+
e.stopPropagation();
|
|
10705
|
+
const value = e.detail ?? e.target?.value;
|
|
10706
|
+
if (typeof value !== "string") return;
|
|
10707
|
+
this.#applyManualValue(value, "change");
|
|
10708
|
+
});
|
|
10709
|
+
}
|
|
10226
10710
|
}
|
|
10227
10711
|
|
|
10228
10712
|
#clientToSVG(e) {
|
|
@@ -10243,6 +10727,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10243
10727
|
#startBezierDrag(e, handle) {
|
|
10244
10728
|
e.preventDefault();
|
|
10245
10729
|
this.#isDragging = handle;
|
|
10730
|
+
this.#syncActiveBezierArm();
|
|
10246
10731
|
|
|
10247
10732
|
const onMove = (e) => {
|
|
10248
10733
|
if (!this.#isDragging) return;
|
|
@@ -10263,11 +10748,13 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10263
10748
|
this.#updatePaths();
|
|
10264
10749
|
this.#presetName = this.#matchPreset();
|
|
10265
10750
|
this.#syncDropdown();
|
|
10751
|
+
this.#syncValueInput();
|
|
10266
10752
|
this.#emit("input");
|
|
10267
10753
|
};
|
|
10268
10754
|
|
|
10269
10755
|
const onUp = () => {
|
|
10270
10756
|
this.#isDragging = null;
|
|
10757
|
+
this.#syncActiveBezierArm();
|
|
10271
10758
|
document.removeEventListener("pointermove", onMove);
|
|
10272
10759
|
document.removeEventListener("pointerup", onUp);
|
|
10273
10760
|
this.#emit("change");
|
|
@@ -10311,6 +10798,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10311
10798
|
this.#updatePaths();
|
|
10312
10799
|
this.#presetName = this.#matchPreset();
|
|
10313
10800
|
this.#syncDropdown();
|
|
10801
|
+
this.#syncValueInput();
|
|
10314
10802
|
this.#emit("input");
|
|
10315
10803
|
};
|
|
10316
10804
|
|
|
@@ -11818,11 +12306,34 @@ customElements.define("fig-spinner", FigSpinner);
|
|
|
11818
12306
|
/**
|
|
11819
12307
|
* A styled visual preview layer for arbitrary content such as images, canvas,
|
|
11820
12308
|
* video, SVG, or custom rendered surfaces.
|
|
12309
|
+
* @attr {string} fit - CSS object-fit value for direct media children
|
|
11821
12310
|
*/
|
|
11822
|
-
class FigPreview extends HTMLElement {
|
|
12311
|
+
class FigPreview extends HTMLElement {
|
|
12312
|
+
static get observedAttributes() {
|
|
12313
|
+
return ["fit"];
|
|
12314
|
+
}
|
|
12315
|
+
|
|
12316
|
+
connectedCallback() {
|
|
12317
|
+
this.#syncFit();
|
|
12318
|
+
}
|
|
12319
|
+
|
|
12320
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
12321
|
+
if (oldValue === newValue) return;
|
|
12322
|
+
if (name === "fit") this.#syncFit();
|
|
12323
|
+
}
|
|
12324
|
+
|
|
12325
|
+
#syncFit() {
|
|
12326
|
+
const fit = this.getAttribute("fit");
|
|
12327
|
+
if (fit) {
|
|
12328
|
+
this.style.setProperty("--fig-preview-fit", fit);
|
|
12329
|
+
} else {
|
|
12330
|
+
this.style.removeProperty("--fig-preview-fit");
|
|
12331
|
+
}
|
|
12332
|
+
}
|
|
12333
|
+
}
|
|
11823
12334
|
customElements.define("fig-preview", FigPreview);
|
|
11824
12335
|
|
|
11825
|
-
/** @type {Record<string, string>} */
|
|
12336
|
+
/** @type {Record<string, string | { medium: string, small: string }>} */
|
|
11826
12337
|
const FIG_ICON_TOKENS = {
|
|
11827
12338
|
chevron: "--icon-16-chevron",
|
|
11828
12339
|
checkmark: "--icon-16-checkmark",
|
|
@@ -11834,16 +12345,25 @@ const FIG_ICON_TOKENS = {
|
|
|
11834
12345
|
minus: "--icon-24-minus",
|
|
11835
12346
|
back: "--icon-24-back",
|
|
11836
12347
|
forward: "--icon-24-forward",
|
|
11837
|
-
close: "--icon-24-close",
|
|
12348
|
+
close: { medium: "--icon-24-close", small: "--icon-16-close" },
|
|
11838
12349
|
rotate: "--icon-24-rotate",
|
|
11839
12350
|
swap: "--icon-24-swap",
|
|
11840
12351
|
play: "--icon-24-play",
|
|
11841
12352
|
pause: "--icon-24-pause",
|
|
12353
|
+
search: "--icon-24-search",
|
|
12354
|
+
visible: { medium: "--icon-24-visible", small: "--icon-16-visible" },
|
|
12355
|
+
hidden: { medium: "--icon-24-hidden", small: "--icon-16-hidden" },
|
|
11842
12356
|
};
|
|
11843
12357
|
|
|
11844
|
-
function figIconCssVar(name) {
|
|
12358
|
+
function figIconCssVar(name, size = "medium") {
|
|
11845
12359
|
const token = name && FIG_ICON_TOKENS[name];
|
|
11846
|
-
|
|
12360
|
+
if (!token) return "";
|
|
12361
|
+
|
|
12362
|
+
const tokenName =
|
|
12363
|
+
typeof token === "string"
|
|
12364
|
+
? token
|
|
12365
|
+
: token[size === "small" ? "small" : "medium"];
|
|
12366
|
+
return tokenName ? `var(${tokenName})` : "";
|
|
11847
12367
|
}
|
|
11848
12368
|
|
|
11849
12369
|
/**
|
|
@@ -11867,11 +12387,11 @@ class FigIcon extends HTMLElement {
|
|
|
11867
12387
|
|
|
11868
12388
|
#sync() {
|
|
11869
12389
|
const iconName = this.getAttribute("name");
|
|
11870
|
-
const
|
|
12390
|
+
const size = this.getAttribute("size") || "medium";
|
|
12391
|
+
const cssVar = figIconCssVar(iconName, size);
|
|
11871
12392
|
if (cssVar) this.style.setProperty("--icon", cssVar);
|
|
11872
12393
|
else this.style.removeProperty("--icon");
|
|
11873
12394
|
|
|
11874
|
-
const size = this.getAttribute("size") || "medium";
|
|
11875
12395
|
if (size === "small") {
|
|
11876
12396
|
this.style.setProperty("--size", "var(--spacer-3)");
|
|
11877
12397
|
} else {
|
|
@@ -11901,2159 +12421,7 @@ customElements.define("fig-button-combo", FigButtonCombo);
|
|
|
11901
12421
|
class FigInputCombo extends HTMLElement {}
|
|
11902
12422
|
customElements.define("fig-input-combo", FigInputCombo);
|
|
11903
12423
|
|
|
11904
|
-
// FigFillPicker
|
|
11905
|
-
/**
|
|
11906
|
-
* A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
|
|
11907
|
-
* Uses display: contents and wraps a trigger element that opens a dialog picker.
|
|
11908
|
-
*
|
|
11909
|
-
* @attr {string} value - JSON-encoded fill value
|
|
11910
|
-
* @attr {boolean} disabled - Whether the picker is disabled
|
|
11911
|
-
* @attr {boolean} alpha - Whether to show alpha/opacity controls (default: true)
|
|
11912
|
-
* @attr {string} dialog-position - Position of the popup (default: "left")
|
|
11913
|
-
*/
|
|
11914
|
-
class FigFillPicker extends HTMLElement {
|
|
11915
|
-
#trigger = null;
|
|
11916
|
-
#chit = null;
|
|
11917
|
-
#dialog = null;
|
|
11918
|
-
#activeTab = "solid";
|
|
11919
|
-
anchorElement = null;
|
|
11920
12424
|
|
|
11921
|
-
// Fill state
|
|
11922
|
-
#fillType = "solid";
|
|
11923
|
-
#gamut = "srgb"; // "srgb" or "display-p3"
|
|
11924
|
-
#color = { h: 0, s: 0, v: 85, a: 1 }; // Default gray #D9D9D9
|
|
11925
|
-
#colorInputMode = "hex";
|
|
11926
|
-
#gradient = {
|
|
11927
|
-
type: "linear",
|
|
11928
|
-
angle: 0,
|
|
11929
|
-
centerX: 50,
|
|
11930
|
-
centerY: 50,
|
|
11931
|
-
interpolationSpace: "oklab",
|
|
11932
|
-
hueInterpolation: "shorter",
|
|
11933
|
-
stops: [
|
|
11934
|
-
{ position: 0, color: "#D9D9D9", opacity: 100 },
|
|
11935
|
-
{ position: 100, color: "#737373", opacity: 100 },
|
|
11936
|
-
],
|
|
11937
|
-
};
|
|
11938
|
-
#image = { url: null, scaleMode: "fill", scale: 50 };
|
|
11939
|
-
#video = { url: null, scaleMode: "fill", scale: 50 };
|
|
11940
|
-
#webcam = { stream: null, snapshot: null };
|
|
11941
|
-
|
|
11942
|
-
// Custom mode slots and data
|
|
11943
|
-
#customSlots = {};
|
|
11944
|
-
#customData = {};
|
|
11945
|
-
|
|
11946
|
-
// DOM references for solid tab
|
|
11947
|
-
#colorArea = null;
|
|
11948
|
-
#colorAreaHandle = null;
|
|
11949
|
-
#hueSlider = null;
|
|
11950
|
-
#opacitySlider = null;
|
|
11951
|
-
#isDraggingColor = false;
|
|
11952
|
-
#teardownColorAreaEvents = null;
|
|
11953
|
-
#dialogOpenObserver = null;
|
|
11954
|
-
#webcamTabObserver = null;
|
|
11955
|
-
|
|
11956
|
-
constructor() {
|
|
11957
|
-
super();
|
|
11958
|
-
}
|
|
11959
|
-
|
|
11960
|
-
static get observedAttributes() {
|
|
11961
|
-
return ["value", "disabled", "alpha", "mode", "experimental"];
|
|
11962
|
-
}
|
|
11963
|
-
|
|
11964
|
-
connectedCallback() {
|
|
11965
|
-
// Use display: contents
|
|
11966
|
-
this.style.display = "contents";
|
|
11967
|
-
|
|
11968
|
-
requestAnimationFrame(() => {
|
|
11969
|
-
this.#setupTrigger();
|
|
11970
|
-
this.#parseValue();
|
|
11971
|
-
this.#updateChit();
|
|
11972
|
-
});
|
|
11973
|
-
}
|
|
11974
|
-
|
|
11975
|
-
disconnectedCallback() {
|
|
11976
|
-
if (this.#teardownColorAreaEvents) {
|
|
11977
|
-
this.#teardownColorAreaEvents();
|
|
11978
|
-
this.#teardownColorAreaEvents = null;
|
|
11979
|
-
}
|
|
11980
|
-
if (this.#dialogOpenObserver) {
|
|
11981
|
-
this.#dialogOpenObserver.disconnect();
|
|
11982
|
-
this.#dialogOpenObserver = null;
|
|
11983
|
-
}
|
|
11984
|
-
if (this.#webcamTabObserver) {
|
|
11985
|
-
this.#webcamTabObserver.disconnect();
|
|
11986
|
-
this.#webcamTabObserver = null;
|
|
11987
|
-
}
|
|
11988
|
-
if (this.#webcam.stream) {
|
|
11989
|
-
this.#webcam.stream.getTracks().forEach((track) => track.stop());
|
|
11990
|
-
this.#webcam.stream = null;
|
|
11991
|
-
}
|
|
11992
|
-
if (this.#video.url && this.#video.url.startsWith("blob:")) {
|
|
11993
|
-
URL.revokeObjectURL(this.#video.url);
|
|
11994
|
-
}
|
|
11995
|
-
if (this.#chit) this.#chit.removeAttribute("selected");
|
|
11996
|
-
if (this.#dialog) {
|
|
11997
|
-
this.#dialog.close();
|
|
11998
|
-
this.#dialog.remove();
|
|
11999
|
-
this.#dialog = null;
|
|
12000
|
-
}
|
|
12001
|
-
}
|
|
12002
|
-
|
|
12003
|
-
#setupTrigger() {
|
|
12004
|
-
const child = Array.from(this.children).find(
|
|
12005
|
-
(el) => !el.getAttribute("slot")?.startsWith("mode-"),
|
|
12006
|
-
);
|
|
12007
|
-
|
|
12008
|
-
if (!child) {
|
|
12009
|
-
// Scenario 1: Empty - create fig-chit
|
|
12010
|
-
this.#chit = document.createElement("fig-chit");
|
|
12011
|
-
this.#chit.setAttribute("background", "#D9D9D9");
|
|
12012
|
-
this.appendChild(this.#chit);
|
|
12013
|
-
this.#trigger = this.#chit;
|
|
12014
|
-
} else if (child.tagName === "FIG-CHIT") {
|
|
12015
|
-
// Scenario 2: Has fig-chit - use and populate it
|
|
12016
|
-
this.#chit = child;
|
|
12017
|
-
this.#trigger = child;
|
|
12018
|
-
} else {
|
|
12019
|
-
// Scenario 3: Other element - trigger only, no populate
|
|
12020
|
-
this.#trigger = child;
|
|
12021
|
-
this.#chit = null;
|
|
12022
|
-
}
|
|
12023
|
-
|
|
12024
|
-
this.#trigger.addEventListener("click", (e) => {
|
|
12025
|
-
if (this.hasAttribute("disabled")) return;
|
|
12026
|
-
e.stopPropagation();
|
|
12027
|
-
e.preventDefault();
|
|
12028
|
-
this.#openDialog();
|
|
12029
|
-
});
|
|
12030
|
-
|
|
12031
|
-
// Prevent fig-chit's internal color input from opening system picker
|
|
12032
|
-
if (this.#chit) {
|
|
12033
|
-
requestAnimationFrame(() => {
|
|
12034
|
-
const input = this.#chit.querySelector('input[type="color"]');
|
|
12035
|
-
if (input) {
|
|
12036
|
-
input.style.pointerEvents = "none";
|
|
12037
|
-
}
|
|
12038
|
-
});
|
|
12039
|
-
}
|
|
12040
|
-
}
|
|
12041
|
-
|
|
12042
|
-
#parseValue() {
|
|
12043
|
-
const valueAttr = this.getAttribute("value");
|
|
12044
|
-
if (!valueAttr) return;
|
|
12045
|
-
|
|
12046
|
-
const builtinTypes = ["solid", "gradient", "image", "video", "webcam"];
|
|
12047
|
-
|
|
12048
|
-
try {
|
|
12049
|
-
const parsed = JSON.parse(valueAttr);
|
|
12050
|
-
if (parsed.type) this.#fillType = parsed.type;
|
|
12051
|
-
if (parsed.color) {
|
|
12052
|
-
// Handle both hex string and HSV object
|
|
12053
|
-
if (typeof parsed.color === "string") {
|
|
12054
|
-
this.#color = this.#hexToHSV(parsed.color);
|
|
12055
|
-
} else if (
|
|
12056
|
-
typeof parsed.color === "object" &&
|
|
12057
|
-
parsed.color.h !== undefined
|
|
12058
|
-
) {
|
|
12059
|
-
this.#color = parsed.color;
|
|
12060
|
-
}
|
|
12061
|
-
}
|
|
12062
|
-
// Parse opacity (0-100) and convert to alpha (0-1)
|
|
12063
|
-
if (parsed.opacity !== undefined) {
|
|
12064
|
-
this.#color.a = parsed.opacity / 100;
|
|
12065
|
-
}
|
|
12066
|
-
if (parsed.colorSpace === "display-p3" || parsed.colorSpace === "srgb") {
|
|
12067
|
-
this.#gamut = parsed.colorSpace;
|
|
12068
|
-
}
|
|
12069
|
-
if (parsed.gradient) {
|
|
12070
|
-
this.#gradient = normalizeGradientConfig({
|
|
12071
|
-
...this.#gradient,
|
|
12072
|
-
...parsed.gradient,
|
|
12073
|
-
});
|
|
12074
|
-
}
|
|
12075
|
-
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
12076
|
-
if (parsed.video) this.#video = { ...this.#video, ...parsed.video };
|
|
12077
|
-
|
|
12078
|
-
// Store full parsed data for custom (non-built-in) types
|
|
12079
|
-
if (parsed.type && !builtinTypes.includes(parsed.type)) {
|
|
12080
|
-
const { type, ...rest } = parsed;
|
|
12081
|
-
this.#customData[parsed.type] = rest;
|
|
12082
|
-
}
|
|
12083
|
-
} catch (e) {
|
|
12084
|
-
// If not JSON, treat as hex color
|
|
12085
|
-
if (valueAttr.startsWith("#")) {
|
|
12086
|
-
this.#fillType = "solid";
|
|
12087
|
-
this.#color = this.#hexToHSV(valueAttr);
|
|
12088
|
-
}
|
|
12089
|
-
}
|
|
12090
|
-
}
|
|
12091
|
-
|
|
12092
|
-
#updateChit() {
|
|
12093
|
-
if (!this.#chit) return;
|
|
12094
|
-
|
|
12095
|
-
let bg;
|
|
12096
|
-
let bgSize = "cover";
|
|
12097
|
-
let bgPosition = "center";
|
|
12098
|
-
|
|
12099
|
-
switch (this.#fillType) {
|
|
12100
|
-
case "solid":
|
|
12101
|
-
bg = this.#hsvToHex(this.#color);
|
|
12102
|
-
break;
|
|
12103
|
-
case "gradient":
|
|
12104
|
-
bg = this.#getGradientCSS();
|
|
12105
|
-
break;
|
|
12106
|
-
case "image":
|
|
12107
|
-
if (this.#image.url) {
|
|
12108
|
-
bg = `url(${this.#image.url})`;
|
|
12109
|
-
const sizing = this.#getBackgroundSizing(
|
|
12110
|
-
this.#image.scaleMode,
|
|
12111
|
-
this.#image.scale,
|
|
12112
|
-
);
|
|
12113
|
-
bgSize = sizing.size;
|
|
12114
|
-
bgPosition = sizing.position;
|
|
12115
|
-
} else {
|
|
12116
|
-
bg = "";
|
|
12117
|
-
}
|
|
12118
|
-
break;
|
|
12119
|
-
case "video":
|
|
12120
|
-
if (this.#video.url) {
|
|
12121
|
-
bg = `url(${this.#video.url})`;
|
|
12122
|
-
const sizing = this.#getBackgroundSizing(
|
|
12123
|
-
this.#video.scaleMode,
|
|
12124
|
-
this.#video.scale,
|
|
12125
|
-
);
|
|
12126
|
-
bgSize = sizing.size;
|
|
12127
|
-
bgPosition = sizing.position;
|
|
12128
|
-
} else {
|
|
12129
|
-
bg = "";
|
|
12130
|
-
}
|
|
12131
|
-
break;
|
|
12132
|
-
default:
|
|
12133
|
-
const slot = this.#customSlots[this.#fillType];
|
|
12134
|
-
bg = slot?.element?.getAttribute("chit-background") || "#D9D9D9";
|
|
12135
|
-
}
|
|
12136
|
-
|
|
12137
|
-
this.#chit.setAttribute("background", bg);
|
|
12138
|
-
this.#chit.style.setProperty("--chit-bg-size", bgSize);
|
|
12139
|
-
this.#chit.style.setProperty("--chit-bg-position", bgPosition);
|
|
12140
|
-
|
|
12141
|
-
if (this.#fillType === "solid") {
|
|
12142
|
-
this.#chit.setAttribute("alpha", this.#color.a);
|
|
12143
|
-
} else {
|
|
12144
|
-
this.#chit.removeAttribute("alpha");
|
|
12145
|
-
}
|
|
12146
|
-
}
|
|
12147
|
-
|
|
12148
|
-
#getBackgroundSizing(scaleMode, scale) {
|
|
12149
|
-
switch (scaleMode) {
|
|
12150
|
-
case "fill":
|
|
12151
|
-
return { size: "cover", position: "center" };
|
|
12152
|
-
case "fit":
|
|
12153
|
-
return { size: "contain", position: "center" };
|
|
12154
|
-
case "crop":
|
|
12155
|
-
return { size: "cover", position: "center" };
|
|
12156
|
-
case "tile":
|
|
12157
|
-
return { size: `${scale}%`, position: "top left" };
|
|
12158
|
-
default:
|
|
12159
|
-
return { size: "cover", position: "center" };
|
|
12160
|
-
}
|
|
12161
|
-
}
|
|
12162
|
-
|
|
12163
|
-
#openDialog() {
|
|
12164
|
-
if (!this.#dialog) {
|
|
12165
|
-
this.#createDialog();
|
|
12166
|
-
}
|
|
12167
|
-
|
|
12168
|
-
this.#switchTab(this.#fillType);
|
|
12169
|
-
|
|
12170
|
-
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
12171
|
-
if (gamutEl) gamutEl.value = this.#gamut;
|
|
12172
|
-
|
|
12173
|
-
if (this.#chit) this.#chit.setAttribute("selected", "true");
|
|
12174
|
-
|
|
12175
|
-
this.#dialog.open = true;
|
|
12176
|
-
|
|
12177
|
-
requestAnimationFrame(() => {
|
|
12178
|
-
requestAnimationFrame(() => {
|
|
12179
|
-
this.#drawColorArea();
|
|
12180
|
-
this.#updateHandlePosition();
|
|
12181
|
-
});
|
|
12182
|
-
});
|
|
12183
|
-
}
|
|
12184
|
-
|
|
12185
|
-
open() {
|
|
12186
|
-
this.#openDialog();
|
|
12187
|
-
}
|
|
12188
|
-
|
|
12189
|
-
close() {
|
|
12190
|
-
if (this.#dialog) this.#dialog.open = false;
|
|
12191
|
-
}
|
|
12192
|
-
|
|
12193
|
-
#createDialog() {
|
|
12194
|
-
// Collect slotted custom mode content before any DOM changes
|
|
12195
|
-
this.#customSlots = {};
|
|
12196
|
-
this.querySelectorAll('[slot^="mode-"]').forEach((el) => {
|
|
12197
|
-
const modeName = el.getAttribute("slot").slice(5);
|
|
12198
|
-
this.#customSlots[modeName] = {
|
|
12199
|
-
element: el,
|
|
12200
|
-
label:
|
|
12201
|
-
el.getAttribute("label") ||
|
|
12202
|
-
modeName.charAt(0).toUpperCase() + modeName.slice(1),
|
|
12203
|
-
};
|
|
12204
|
-
});
|
|
12205
|
-
|
|
12206
|
-
this.#dialog = document.createElement("dialog", { is: "fig-popup" });
|
|
12207
|
-
this.#dialog.setAttribute("is", "fig-popup");
|
|
12208
|
-
this.#dialog.setAttribute("drag", "true");
|
|
12209
|
-
this.#dialog.setAttribute("handle", "fig-header");
|
|
12210
|
-
this.#dialog.setAttribute("autoresize", "false");
|
|
12211
|
-
this.#dialog.classList.add("fig-fill-picker-dialog");
|
|
12212
|
-
|
|
12213
|
-
this.#dialog.anchor = this.anchorElement || this.#trigger;
|
|
12214
|
-
const dialogPosition = this.getAttribute("dialog-position") || "left";
|
|
12215
|
-
this.#dialog.setAttribute("position", dialogPosition);
|
|
12216
|
-
this.#dialog.setAttribute("offset", this.getAttribute("dialog-offset") || "8 8");
|
|
12217
|
-
|
|
12218
|
-
const builtinModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
12219
|
-
const builtinLabels = {
|
|
12220
|
-
solid: "Solid",
|
|
12221
|
-
gradient: "Gradient",
|
|
12222
|
-
image: "Image",
|
|
12223
|
-
video: "Video",
|
|
12224
|
-
webcam: "Webcam",
|
|
12225
|
-
};
|
|
12226
|
-
|
|
12227
|
-
// Build allowed modes: built-ins filtered normally, custom names accepted if slot exists
|
|
12228
|
-
const mode = this.getAttribute("mode");
|
|
12229
|
-
let allowedModes;
|
|
12230
|
-
if (mode) {
|
|
12231
|
-
const requested = mode.split(",").map((m) => m.trim().toLowerCase());
|
|
12232
|
-
allowedModes = requested.filter(
|
|
12233
|
-
(m) => builtinModes.includes(m) || this.#customSlots[m],
|
|
12234
|
-
);
|
|
12235
|
-
if (allowedModes.length === 0) allowedModes = [...builtinModes];
|
|
12236
|
-
} else {
|
|
12237
|
-
allowedModes = [...builtinModes];
|
|
12238
|
-
}
|
|
12239
|
-
|
|
12240
|
-
// Build labels map: built-in labels + custom slot labels
|
|
12241
|
-
const modeLabels = { ...builtinLabels };
|
|
12242
|
-
for (const [name, { label }] of Object.entries(this.#customSlots)) {
|
|
12243
|
-
modeLabels[name] = label;
|
|
12244
|
-
}
|
|
12245
|
-
|
|
12246
|
-
if (!allowedModes.includes(this.#fillType)) {
|
|
12247
|
-
this.#fillType = allowedModes[0];
|
|
12248
|
-
this.#activeTab = allowedModes[0];
|
|
12249
|
-
}
|
|
12250
|
-
|
|
12251
|
-
const experimental = this.getAttribute("experimental");
|
|
12252
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
12253
|
-
|
|
12254
|
-
let headerContent;
|
|
12255
|
-
if (allowedModes.length === 1) {
|
|
12256
|
-
headerContent = `<h3 class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</h3>`;
|
|
12257
|
-
} else {
|
|
12258
|
-
const options = allowedModes
|
|
12259
|
-
.map((m) => `<option value="${m}">${modeLabels[m]}</option>`)
|
|
12260
|
-
.join("\n ");
|
|
12261
|
-
headerContent = `<fig-dropdown class="fig-fill-picker-type" ${expAttr} value="${this.#fillType}">
|
|
12262
|
-
${options}
|
|
12263
|
-
</fig-dropdown>`;
|
|
12264
|
-
}
|
|
12265
|
-
|
|
12266
|
-
// Generate tab containers for all allowed modes
|
|
12267
|
-
const tabDivs = allowedModes
|
|
12268
|
-
.map((m) => `<div class="fig-fill-picker-tab" data-tab="${m}"></div>`)
|
|
12269
|
-
.join("\n ");
|
|
12270
|
-
|
|
12271
|
-
const gamutDropdown = `<fig-dropdown class="fig-fill-picker-gamut" ${expAttr} value="${this.#gamut}">
|
|
12272
|
-
<option value="srgb">sRGB</option>
|
|
12273
|
-
<option value="display-p3">Display P3</option>
|
|
12274
|
-
</fig-dropdown>`;
|
|
12275
|
-
|
|
12276
|
-
this.#dialog.innerHTML = `
|
|
12277
|
-
<fig-header>
|
|
12278
|
-
${headerContent}
|
|
12279
|
-
${gamutDropdown}
|
|
12280
|
-
<fig-button icon variant="ghost" class="fig-fill-picker-close">
|
|
12281
|
-
<fig-icon name="close"></fig-icon>
|
|
12282
|
-
</fig-button>
|
|
12283
|
-
</fig-header>
|
|
12284
|
-
<fig-content>
|
|
12285
|
-
${tabDivs}
|
|
12286
|
-
</fig-content>
|
|
12287
|
-
`;
|
|
12288
|
-
|
|
12289
|
-
document.body.appendChild(this.#dialog);
|
|
12290
|
-
|
|
12291
|
-
// Populate custom tab containers and emit modeready
|
|
12292
|
-
for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
|
|
12293
|
-
const container = this.#dialog.querySelector(`[data-tab="${modeName}"]`);
|
|
12294
|
-
if (!container) continue;
|
|
12295
|
-
|
|
12296
|
-
// Move children (not the element itself) for vanilla HTML usage
|
|
12297
|
-
while (element.firstChild) {
|
|
12298
|
-
container.appendChild(element.firstChild);
|
|
12299
|
-
}
|
|
12300
|
-
|
|
12301
|
-
// Emit modeready so frameworks can render into the container
|
|
12302
|
-
this.dispatchEvent(
|
|
12303
|
-
new CustomEvent("modeready", {
|
|
12304
|
-
bubbles: true,
|
|
12305
|
-
detail: { mode: modeName, container },
|
|
12306
|
-
}),
|
|
12307
|
-
);
|
|
12308
|
-
}
|
|
12309
|
-
|
|
12310
|
-
// Setup type dropdown switching (only if not locked)
|
|
12311
|
-
const typeDropdown = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
12312
|
-
if (typeDropdown) {
|
|
12313
|
-
typeDropdown.addEventListener("change", (e) => {
|
|
12314
|
-
this.#switchTab(e.target.value);
|
|
12315
|
-
});
|
|
12316
|
-
}
|
|
12317
|
-
|
|
12318
|
-
// Setup gamut dropdown
|
|
12319
|
-
const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
|
|
12320
|
-
if (gamutEl) {
|
|
12321
|
-
const handleGamutChange = (e) => {
|
|
12322
|
-
const val = e.currentTarget?.value ?? e.target?.value ?? e.detail;
|
|
12323
|
-
if (val && val !== this.#gamut) {
|
|
12324
|
-
this.#gamut = val;
|
|
12325
|
-
this.#onGamutChange();
|
|
12326
|
-
}
|
|
12327
|
-
};
|
|
12328
|
-
gamutEl.addEventListener("input", handleGamutChange);
|
|
12329
|
-
gamutEl.addEventListener("change", handleGamutChange);
|
|
12330
|
-
}
|
|
12331
|
-
|
|
12332
|
-
this.#dialog
|
|
12333
|
-
.querySelector(".fig-fill-picker-close")
|
|
12334
|
-
.addEventListener("click", () => {
|
|
12335
|
-
this.#dialog.open = false;
|
|
12336
|
-
});
|
|
12337
|
-
|
|
12338
|
-
const onDialogClose = () => {
|
|
12339
|
-
if (this.#chit) this.#chit.removeAttribute("selected");
|
|
12340
|
-
this.#emitChange();
|
|
12341
|
-
this.dispatchEvent(new CustomEvent("close"));
|
|
12342
|
-
};
|
|
12343
|
-
this.#dialog.addEventListener("close", onDialogClose);
|
|
12344
|
-
|
|
12345
|
-
this.#dialogOpenObserver = new MutationObserver(() => {
|
|
12346
|
-
const isOpen =
|
|
12347
|
-
this.#dialog.hasAttribute("open") &&
|
|
12348
|
-
this.#dialog.getAttribute("open") !== "false";
|
|
12349
|
-
if (!isOpen) onDialogClose();
|
|
12350
|
-
});
|
|
12351
|
-
this.#dialogOpenObserver.observe(this.#dialog, {
|
|
12352
|
-
attributes: true,
|
|
12353
|
-
attributeFilter: ["open"],
|
|
12354
|
-
});
|
|
12355
|
-
|
|
12356
|
-
// Initialize built-in tabs (skip any overridden by custom slots)
|
|
12357
|
-
const builtinInits = {
|
|
12358
|
-
solid: () => this.#initSolidTab(),
|
|
12359
|
-
gradient: () => this.#initGradientTab(),
|
|
12360
|
-
image: () => this.#initImageTab(),
|
|
12361
|
-
video: () => this.#initVideoTab(),
|
|
12362
|
-
webcam: () => this.#initWebcamTab(),
|
|
12363
|
-
};
|
|
12364
|
-
for (const [name, init] of Object.entries(builtinInits)) {
|
|
12365
|
-
if (!this.#customSlots[name] && allowedModes.includes(name)) init();
|
|
12366
|
-
}
|
|
12367
|
-
|
|
12368
|
-
// Listen for input/change from custom tab content
|
|
12369
|
-
for (const modeName of Object.keys(this.#customSlots)) {
|
|
12370
|
-
if (builtinModes.includes(modeName)) continue;
|
|
12371
|
-
const container = this.#dialog.querySelector(`[data-tab="${modeName}"]`);
|
|
12372
|
-
if (!container) continue;
|
|
12373
|
-
container.addEventListener("input", (e) => {
|
|
12374
|
-
if (e.target === this) return;
|
|
12375
|
-
e.stopPropagation();
|
|
12376
|
-
if (e.detail) this.#customData[modeName] = e.detail;
|
|
12377
|
-
this.#emitInput();
|
|
12378
|
-
});
|
|
12379
|
-
container.addEventListener("change", (e) => {
|
|
12380
|
-
if (e.target === this) return;
|
|
12381
|
-
e.stopPropagation();
|
|
12382
|
-
if (e.detail) this.#customData[modeName] = e.detail;
|
|
12383
|
-
this.#emitChange();
|
|
12384
|
-
});
|
|
12385
|
-
}
|
|
12386
|
-
}
|
|
12387
|
-
|
|
12388
|
-
#switchTab(tabName) {
|
|
12389
|
-
// Only allow switching to modes that have a tab container in the dialog
|
|
12390
|
-
const tab = this.#dialog?.querySelector(
|
|
12391
|
-
`.fig-fill-picker-tab[data-tab="${tabName}"]`,
|
|
12392
|
-
);
|
|
12393
|
-
if (!tab) return;
|
|
12394
|
-
|
|
12395
|
-
this.#activeTab = tabName;
|
|
12396
|
-
this.#fillType = tabName;
|
|
12397
|
-
|
|
12398
|
-
// Update dropdown selection (only exists if not locked)
|
|
12399
|
-
const typeDropdown = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
12400
|
-
if (typeDropdown && typeDropdown.value !== tabName) {
|
|
12401
|
-
typeDropdown.value = tabName;
|
|
12402
|
-
}
|
|
12403
|
-
|
|
12404
|
-
// Show/hide tab content
|
|
12405
|
-
const tabContents = this.#dialog.querySelectorAll(".fig-fill-picker-tab");
|
|
12406
|
-
tabContents.forEach((content) => {
|
|
12407
|
-
if (content.dataset.tab === tabName) {
|
|
12408
|
-
content.style.display = "block";
|
|
12409
|
-
} else {
|
|
12410
|
-
content.style.display = "none";
|
|
12411
|
-
}
|
|
12412
|
-
});
|
|
12413
|
-
|
|
12414
|
-
// Zero out content padding for custom mode tabs
|
|
12415
|
-
const contentEl = this.#dialog.querySelector("fig-content");
|
|
12416
|
-
if (contentEl) {
|
|
12417
|
-
contentEl.style.padding = this.#customSlots[tabName] ? "0" : "";
|
|
12418
|
-
}
|
|
12419
|
-
|
|
12420
|
-
// Update tab-specific UI after visibility change
|
|
12421
|
-
if (tabName === "gradient") {
|
|
12422
|
-
// Use RAF to ensure layout is complete before updating angle input
|
|
12423
|
-
requestAnimationFrame(() => {
|
|
12424
|
-
this.#updateGradientUI();
|
|
12425
|
-
const barInput = tab.querySelector(".fig-fill-picker-gradient-bar-input");
|
|
12426
|
-
barInput?.refreshLayout?.();
|
|
12427
|
-
requestAnimationFrame(() => {
|
|
12428
|
-
barInput?.refreshLayout?.();
|
|
12429
|
-
});
|
|
12430
|
-
});
|
|
12431
|
-
}
|
|
12432
|
-
|
|
12433
|
-
this.#updateChit();
|
|
12434
|
-
this.#emitInput();
|
|
12435
|
-
}
|
|
12436
|
-
|
|
12437
|
-
// ============ SOLID TAB ============
|
|
12438
|
-
#initSolidTab() {
|
|
12439
|
-
const container = this.#dialog.querySelector('[data-tab="solid"]');
|
|
12440
|
-
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
12441
|
-
const experimental = this.getAttribute("experimental");
|
|
12442
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
12443
|
-
|
|
12444
|
-
container.innerHTML = `
|
|
12445
|
-
<fig-preview class="fig-fill-picker-color-area">
|
|
12446
|
-
<canvas width="200" height="200"></canvas>
|
|
12447
|
-
<fig-handle
|
|
12448
|
-
type="color"
|
|
12449
|
-
color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
|
|
12450
|
-
data-no-color-picker
|
|
12451
|
-
drag
|
|
12452
|
-
drag-surface=".fig-fill-picker-color-area"
|
|
12453
|
-
drag-axes="x,y"
|
|
12454
|
-
drag-snapping="modifier"
|
|
12455
|
-
></fig-handle>
|
|
12456
|
-
</fig-preview>
|
|
12457
|
-
<div class="fig-fill-picker-sliders">
|
|
12458
|
-
<fig-tooltip text="Sample color"><fig-button icon variant="ghost" class="fig-fill-picker-eyedropper"><fig-icon name="eyedropper"></fig-icon></fig-button></fig-tooltip>
|
|
12459
|
-
<fig-slider type="hue" variant="neue" min="0" max="360" value="${
|
|
12460
|
-
this.#color.h
|
|
12461
|
-
}"></fig-slider>
|
|
12462
|
-
${
|
|
12463
|
-
showAlpha
|
|
12464
|
-
? `<fig-slider type="opacity" variant="neue" text="true" units="%" min="0" max="100" value="${
|
|
12465
|
-
this.#color.a * 100
|
|
12466
|
-
}" color="${this.#hsvToHex(this.#color)}"></fig-slider>`
|
|
12467
|
-
: ""
|
|
12468
|
-
}
|
|
12469
|
-
</div>
|
|
12470
|
-
<fig-field class="fig-fill-picker-inputs" direction="horizontal">
|
|
12471
|
-
<fig-dropdown class="fig-fill-picker-input-mode" ${expAttr} value="${this.#colorInputMode}">
|
|
12472
|
-
<option value="hex">Hex</option>
|
|
12473
|
-
<option value="rgb">RGB</option>
|
|
12474
|
-
<option value="hsl">HSL</option>
|
|
12475
|
-
<option value="hsb">HSB</option>
|
|
12476
|
-
<option value="lab">LAB</option>
|
|
12477
|
-
<option value="lch">LCH</option>
|
|
12478
|
-
</fig-dropdown>
|
|
12479
|
-
<span class="fig-fill-picker-input-fields"></span>
|
|
12480
|
-
</fig-field>
|
|
12481
|
-
`;
|
|
12482
|
-
|
|
12483
|
-
// Setup color area
|
|
12484
|
-
this.#colorArea = container.querySelector("canvas");
|
|
12485
|
-
this.#colorAreaHandle = container.querySelector("fig-handle");
|
|
12486
|
-
this.#drawColorArea();
|
|
12487
|
-
this.#updateHandlePosition();
|
|
12488
|
-
this.#setupColorAreaEvents();
|
|
12489
|
-
|
|
12490
|
-
// Setup hue slider
|
|
12491
|
-
this.#hueSlider = container.querySelector('fig-slider[type="hue"]');
|
|
12492
|
-
this.#hueSlider.addEventListener("input", (e) => {
|
|
12493
|
-
this.#color.h = parseFloat(e.target.value);
|
|
12494
|
-
this.#drawColorArea();
|
|
12495
|
-
this.#updateHandlePosition();
|
|
12496
|
-
this.#updateColorInputs();
|
|
12497
|
-
this.#emitInput();
|
|
12498
|
-
});
|
|
12499
|
-
this.#hueSlider.addEventListener("change", () => {
|
|
12500
|
-
this.#emitChange();
|
|
12501
|
-
});
|
|
12502
|
-
|
|
12503
|
-
// Setup opacity slider
|
|
12504
|
-
if (showAlpha) {
|
|
12505
|
-
this.#opacitySlider = container.querySelector(
|
|
12506
|
-
'fig-slider[type="opacity"]',
|
|
12507
|
-
);
|
|
12508
|
-
this.#opacitySlider.addEventListener("input", (e) => {
|
|
12509
|
-
this.#color.a = parseFloat(e.target.value) / 100;
|
|
12510
|
-
this.#updateColorInputs();
|
|
12511
|
-
this.#emitInput();
|
|
12512
|
-
});
|
|
12513
|
-
this.#opacitySlider.addEventListener("change", () => {
|
|
12514
|
-
this.#emitChange();
|
|
12515
|
-
});
|
|
12516
|
-
}
|
|
12517
|
-
|
|
12518
|
-
// Setup color input mode dropdown
|
|
12519
|
-
const modeDropdown = container.querySelector(".fig-fill-picker-input-mode");
|
|
12520
|
-
modeDropdown.addEventListener("input", (e) => {
|
|
12521
|
-
this.#colorInputMode = e.target.value;
|
|
12522
|
-
this.#rebuildColorInputFields();
|
|
12523
|
-
});
|
|
12524
|
-
|
|
12525
|
-
// Build initial color input fields
|
|
12526
|
-
this.#rebuildColorInputFields();
|
|
12527
|
-
|
|
12528
|
-
// Setup eyedropper
|
|
12529
|
-
const eyedropper = container.querySelector(".fig-fill-picker-eyedropper");
|
|
12530
|
-
if ("EyeDropper" in window) {
|
|
12531
|
-
eyedropper.addEventListener("click", async () => {
|
|
12532
|
-
try {
|
|
12533
|
-
const dropper = new EyeDropper();
|
|
12534
|
-
const result = await dropper.open();
|
|
12535
|
-
this.#color = { ...this.#hexToHSV(result.sRGBHex), a: this.#color.a };
|
|
12536
|
-
this.#drawColorArea();
|
|
12537
|
-
this.#updateHandlePosition();
|
|
12538
|
-
this.#updateColorInputs();
|
|
12539
|
-
this.#emitInput();
|
|
12540
|
-
} catch (e) {
|
|
12541
|
-
// User cancelled or error
|
|
12542
|
-
}
|
|
12543
|
-
});
|
|
12544
|
-
} else {
|
|
12545
|
-
eyedropper.setAttribute("disabled", "");
|
|
12546
|
-
eyedropper.title = "EyeDropper not supported in this browser";
|
|
12547
|
-
}
|
|
12548
|
-
}
|
|
12549
|
-
|
|
12550
|
-
#onGamutChange() {
|
|
12551
|
-
// Recreate the solid canvas with the new color space
|
|
12552
|
-
const solidContainer = this.#dialog?.querySelector('[data-tab="solid"]');
|
|
12553
|
-
if (solidContainer) {
|
|
12554
|
-
const oldCanvas = solidContainer.querySelector("canvas");
|
|
12555
|
-
if (oldCanvas) {
|
|
12556
|
-
const newCanvas = document.createElement("canvas");
|
|
12557
|
-
newCanvas.width = oldCanvas.width;
|
|
12558
|
-
newCanvas.height = oldCanvas.height;
|
|
12559
|
-
oldCanvas.replaceWith(newCanvas);
|
|
12560
|
-
this.#colorArea = newCanvas;
|
|
12561
|
-
this.#setupColorAreaEvents();
|
|
12562
|
-
}
|
|
12563
|
-
this.#drawColorArea();
|
|
12564
|
-
this.#updateHandlePosition();
|
|
12565
|
-
}
|
|
12566
|
-
// Refresh gradient preview if gradient tab exists
|
|
12567
|
-
this.#updateGradientPreview();
|
|
12568
|
-
this.#emitInput();
|
|
12569
|
-
}
|
|
12570
|
-
|
|
12571
|
-
#drawColorArea() {
|
|
12572
|
-
// Refresh canvas reference in case DOM changed
|
|
12573
|
-
if (!this.#colorArea && this.#dialog) {
|
|
12574
|
-
this.#colorArea = this.#dialog.querySelector('[data-tab="solid"] canvas');
|
|
12575
|
-
}
|
|
12576
|
-
if (!this.#colorArea) return;
|
|
12577
|
-
|
|
12578
|
-
const colorSpace = this.#gamut === "display-p3" ? "display-p3" : "srgb";
|
|
12579
|
-
const ctx = this.#colorArea.getContext("2d", { colorSpace });
|
|
12580
|
-
if (!ctx) return;
|
|
12581
|
-
|
|
12582
|
-
const width = this.#colorArea.width;
|
|
12583
|
-
const height = this.#colorArea.height;
|
|
12584
|
-
|
|
12585
|
-
ctx.clearRect(0, 0, width, height);
|
|
12586
|
-
|
|
12587
|
-
const hue = this.#color.h;
|
|
12588
|
-
const isP3 = this.#gamut === "display-p3";
|
|
12589
|
-
|
|
12590
|
-
const gradH = ctx.createLinearGradient(0, 0, width, 0);
|
|
12591
|
-
if (isP3) {
|
|
12592
|
-
gradH.addColorStop(0, "color(display-p3 1 1 1)");
|
|
12593
|
-
const [r, g, b] = hslToP3(hue, 100, 50);
|
|
12594
|
-
gradH.addColorStop(1, `color(display-p3 ${r} ${g} ${b})`);
|
|
12595
|
-
} else {
|
|
12596
|
-
gradH.addColorStop(0, "#FFFFFF");
|
|
12597
|
-
gradH.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
|
|
12598
|
-
}
|
|
12599
|
-
|
|
12600
|
-
ctx.fillStyle = gradH;
|
|
12601
|
-
ctx.fillRect(0, 0, width, height);
|
|
12602
|
-
|
|
12603
|
-
const gradV = ctx.createLinearGradient(0, 0, 0, height);
|
|
12604
|
-
gradV.addColorStop(0, "rgba(0,0,0,0)");
|
|
12605
|
-
gradV.addColorStop(1, "rgba(0,0,0,1)");
|
|
12606
|
-
|
|
12607
|
-
ctx.fillStyle = gradV;
|
|
12608
|
-
ctx.fillRect(0, 0, width, height);
|
|
12609
|
-
}
|
|
12610
|
-
|
|
12611
|
-
#updateHandlePosition(retryCount = 0) {
|
|
12612
|
-
if (!this.#colorAreaHandle || !this.#colorArea) return;
|
|
12613
|
-
|
|
12614
|
-
const rect = this.#colorArea.getBoundingClientRect();
|
|
12615
|
-
|
|
12616
|
-
// If the canvas isn't visible yet (0 dimensions), schedule a retry (max 5 attempts)
|
|
12617
|
-
if ((rect.width === 0 || rect.height === 0) && retryCount < 5) {
|
|
12618
|
-
requestAnimationFrame(() => this.#updateHandlePosition(retryCount + 1));
|
|
12619
|
-
return;
|
|
12620
|
-
}
|
|
12621
|
-
|
|
12622
|
-
const xPct = Math.max(0, Math.min(100, this.#color.s));
|
|
12623
|
-
const yPct = Math.max(0, Math.min(100, 100 - this.#color.v));
|
|
12624
|
-
|
|
12625
|
-
this.#colorAreaHandle.setAttribute("value", `${xPct}% ${yPct}%`);
|
|
12626
|
-
this.#colorAreaHandle.setAttribute(
|
|
12627
|
-
"color",
|
|
12628
|
-
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
12629
|
-
);
|
|
12630
|
-
}
|
|
12631
|
-
|
|
12632
|
-
#updateColorFromAreaPosition(x, y, opts = {}) {
|
|
12633
|
-
const { updateHandle = true, emitInput = true, emitChange = false } = opts;
|
|
12634
|
-
this.#color.s = Math.max(0, Math.min(100, x * 100));
|
|
12635
|
-
this.#color.v = Math.max(0, Math.min(100, (1 - y) * 100));
|
|
12636
|
-
if (this.#colorAreaHandle) {
|
|
12637
|
-
this.#colorAreaHandle.setAttribute(
|
|
12638
|
-
"color",
|
|
12639
|
-
this.#hsvToHex({ ...this.#color, a: 1 }),
|
|
12640
|
-
);
|
|
12641
|
-
}
|
|
12642
|
-
if (updateHandle) this.#updateHandlePosition();
|
|
12643
|
-
this.#updateColorInputs();
|
|
12644
|
-
if (emitInput) this.#emitInput();
|
|
12645
|
-
if (emitChange) this.#emitChange();
|
|
12646
|
-
}
|
|
12647
|
-
|
|
12648
|
-
#setupColorAreaEvents() {
|
|
12649
|
-
if (this.#teardownColorAreaEvents) {
|
|
12650
|
-
this.#teardownColorAreaEvents();
|
|
12651
|
-
this.#teardownColorAreaEvents = null;
|
|
12652
|
-
}
|
|
12653
|
-
if (!this.#colorArea || !this.#colorAreaHandle) return;
|
|
12654
|
-
|
|
12655
|
-
const colorAreaEl = this.#colorArea.parentElement || this.#colorArea;
|
|
12656
|
-
const colorAreaHandleEl = this.#colorAreaHandle;
|
|
12657
|
-
|
|
12658
|
-
let isPlaneDragging = false;
|
|
12659
|
-
|
|
12660
|
-
const updatePlaneFromEvent = (e, opts = {}) => {
|
|
12661
|
-
const rect = colorAreaEl.getBoundingClientRect();
|
|
12662
|
-
if (rect.width === 0 || rect.height === 0) return;
|
|
12663
|
-
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
12664
|
-
const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
12665
|
-
this.#updateColorFromAreaPosition(x / rect.width, y / rect.height, opts);
|
|
12666
|
-
};
|
|
12667
|
-
|
|
12668
|
-
const onPlanePointerDown = (e) => {
|
|
12669
|
-
if (e.button !== 0) return;
|
|
12670
|
-
if (
|
|
12671
|
-
e.target === colorAreaHandleEl ||
|
|
12672
|
-
colorAreaHandleEl.contains(e.target)
|
|
12673
|
-
)
|
|
12674
|
-
return;
|
|
12675
|
-
isPlaneDragging = true;
|
|
12676
|
-
this.#isDraggingColor = true;
|
|
12677
|
-
colorAreaEl.setPointerCapture(e.pointerId);
|
|
12678
|
-
updatePlaneFromEvent(e, { updateHandle: true, emitInput: true });
|
|
12679
|
-
};
|
|
12680
|
-
|
|
12681
|
-
const onPlanePointerMove = (e) => {
|
|
12682
|
-
if (!isPlaneDragging) return;
|
|
12683
|
-
if (e.buttons === 0) {
|
|
12684
|
-
onPlaneDragEnd();
|
|
12685
|
-
return;
|
|
12686
|
-
}
|
|
12687
|
-
updatePlaneFromEvent(e, { updateHandle: true, emitInput: true });
|
|
12688
|
-
};
|
|
12689
|
-
|
|
12690
|
-
const onPlaneDragEnd = () => {
|
|
12691
|
-
if (!isPlaneDragging) return;
|
|
12692
|
-
isPlaneDragging = false;
|
|
12693
|
-
this.#isDraggingColor = false;
|
|
12694
|
-
this.#emitChange();
|
|
12695
|
-
};
|
|
12696
|
-
|
|
12697
|
-
const onHandleInput = (e) => {
|
|
12698
|
-
this.#isDraggingColor = true;
|
|
12699
|
-
const px = e.detail?.px;
|
|
12700
|
-
const py = e.detail?.py;
|
|
12701
|
-
if (!Number.isFinite(px) || !Number.isFinite(py)) return;
|
|
12702
|
-
colorAreaHandleEl.setAttribute("value", `${px * 100}% ${py * 100}%`);
|
|
12703
|
-
this.#updateColorFromAreaPosition(px, py, {
|
|
12704
|
-
updateHandle: false,
|
|
12705
|
-
emitInput: true,
|
|
12706
|
-
});
|
|
12707
|
-
};
|
|
12708
|
-
|
|
12709
|
-
const onHandleChange = (e) => {
|
|
12710
|
-
const px = e.detail?.px;
|
|
12711
|
-
const py = e.detail?.py;
|
|
12712
|
-
if (Number.isFinite(px) && Number.isFinite(py)) {
|
|
12713
|
-
colorAreaHandleEl.setAttribute("value", `${px * 100}% ${py * 100}%`);
|
|
12714
|
-
this.#updateColorFromAreaPosition(px, py, {
|
|
12715
|
-
updateHandle: false,
|
|
12716
|
-
emitInput: false,
|
|
12717
|
-
});
|
|
12718
|
-
}
|
|
12719
|
-
this.#isDraggingColor = false;
|
|
12720
|
-
this.#emitChange();
|
|
12721
|
-
};
|
|
12722
|
-
|
|
12723
|
-
colorAreaEl.addEventListener("pointerdown", onPlanePointerDown);
|
|
12724
|
-
colorAreaEl.addEventListener("pointermove", onPlanePointerMove);
|
|
12725
|
-
colorAreaEl.addEventListener("pointerup", onPlaneDragEnd);
|
|
12726
|
-
colorAreaEl.addEventListener("pointercancel", onPlaneDragEnd);
|
|
12727
|
-
colorAreaEl.addEventListener("lostpointercapture", onPlaneDragEnd);
|
|
12728
|
-
|
|
12729
|
-
colorAreaHandleEl.addEventListener("input", onHandleInput);
|
|
12730
|
-
colorAreaHandleEl.addEventListener("change", onHandleChange);
|
|
12731
|
-
|
|
12732
|
-
this.#teardownColorAreaEvents = () => {
|
|
12733
|
-
colorAreaEl.removeEventListener("pointerdown", onPlanePointerDown);
|
|
12734
|
-
colorAreaEl.removeEventListener("pointermove", onPlanePointerMove);
|
|
12735
|
-
colorAreaEl.removeEventListener("pointerup", onPlaneDragEnd);
|
|
12736
|
-
colorAreaEl.removeEventListener("pointercancel", onPlaneDragEnd);
|
|
12737
|
-
colorAreaEl.removeEventListener("lostpointercapture", onPlaneDragEnd);
|
|
12738
|
-
|
|
12739
|
-
colorAreaHandleEl.removeEventListener("input", onHandleInput);
|
|
12740
|
-
colorAreaHandleEl.removeEventListener("change", onHandleChange);
|
|
12741
|
-
this.#isDraggingColor = false;
|
|
12742
|
-
};
|
|
12743
|
-
}
|
|
12744
|
-
|
|
12745
|
-
#rebuildColorInputFields() {
|
|
12746
|
-
const container = this.#dialog?.querySelector(
|
|
12747
|
-
".fig-fill-picker-input-fields",
|
|
12748
|
-
);
|
|
12749
|
-
if (!container) return;
|
|
12750
|
-
|
|
12751
|
-
const wrap = (tooltip, html) =>
|
|
12752
|
-
`<fig-tooltip text="${tooltip}">${html}</fig-tooltip>`;
|
|
12753
|
-
|
|
12754
|
-
const num = (cls, min, max, step) =>
|
|
12755
|
-
`<fig-input-number class="${cls}" min="${min}" max="${max}"${step != null ? ` step="${step}"` : ""}></fig-input-number>`;
|
|
12756
|
-
|
|
12757
|
-
let html;
|
|
12758
|
-
switch (this.#colorInputMode) {
|
|
12759
|
-
case "rgb":
|
|
12760
|
-
html = `<div class="input-combo">
|
|
12761
|
-
${wrap("Red", num("fig-fill-picker-ci-r", 0, 255))}
|
|
12762
|
-
${wrap("Green", num("fig-fill-picker-ci-g", 0, 255))}
|
|
12763
|
-
${wrap("Blue", num("fig-fill-picker-ci-b", 0, 255))}
|
|
12764
|
-
</div>`;
|
|
12765
|
-
break;
|
|
12766
|
-
case "hsl":
|
|
12767
|
-
html = `<div class="input-combo">
|
|
12768
|
-
${wrap("Hue", num("fig-fill-picker-ci-h", 0, 360))}
|
|
12769
|
-
${wrap("Saturation", num("fig-fill-picker-ci-s", 0, 100))}
|
|
12770
|
-
${wrap("Lightness", num("fig-fill-picker-ci-l", 0, 100))}
|
|
12771
|
-
</div>`;
|
|
12772
|
-
break;
|
|
12773
|
-
case "hsb":
|
|
12774
|
-
html = `<div class="input-combo">
|
|
12775
|
-
${wrap("Hue", num("fig-fill-picker-ci-h", 0, 360))}
|
|
12776
|
-
${wrap("Saturation", num("fig-fill-picker-ci-s", 0, 100))}
|
|
12777
|
-
${wrap("Brightness", num("fig-fill-picker-ci-v", 0, 100))}
|
|
12778
|
-
</div>`;
|
|
12779
|
-
break;
|
|
12780
|
-
case "lab":
|
|
12781
|
-
html = `<div class="input-combo">
|
|
12782
|
-
${wrap("Lightness", num("fig-fill-picker-ci-okl", 0, 100))}
|
|
12783
|
-
${wrap("Green-Red axis", num("fig-fill-picker-ci-oka", -0.4, 0.4, 0.001))}
|
|
12784
|
-
${wrap("Blue-Yellow axis", num("fig-fill-picker-ci-okb", -0.4, 0.4, 0.001))}
|
|
12785
|
-
</div>`;
|
|
12786
|
-
break;
|
|
12787
|
-
case "lch":
|
|
12788
|
-
html = `<div class="input-combo">
|
|
12789
|
-
${wrap("Lightness", num("fig-fill-picker-ci-okl", 0, 100))}
|
|
12790
|
-
${wrap("Chroma", num("fig-fill-picker-ci-okc", 0, 0.4, 0.001))}
|
|
12791
|
-
${wrap("Hue", num("fig-fill-picker-ci-okh", 0, 360))}
|
|
12792
|
-
</div>`;
|
|
12793
|
-
break;
|
|
12794
|
-
default: // hex
|
|
12795
|
-
html = `<fig-input-text class="fig-fill-picker-ci-hex" placeholder="FFFFFF"></fig-input-text>`;
|
|
12796
|
-
break;
|
|
12797
|
-
}
|
|
12798
|
-
|
|
12799
|
-
container.innerHTML = html;
|
|
12800
|
-
this.#wireColorInputEvents();
|
|
12801
|
-
requestAnimationFrame(() => this.#updateColorInputs());
|
|
12802
|
-
}
|
|
12803
|
-
|
|
12804
|
-
#wireColorInputEvents() {
|
|
12805
|
-
const container = this.#dialog?.querySelector(
|
|
12806
|
-
".fig-fill-picker-input-fields",
|
|
12807
|
-
);
|
|
12808
|
-
if (!container) return;
|
|
12809
|
-
|
|
12810
|
-
const onInput = () => {
|
|
12811
|
-
if (this.#isDraggingColor) return;
|
|
12812
|
-
const color = this.#readColorFromInputs();
|
|
12813
|
-
if (!color) return;
|
|
12814
|
-
this.#color = { ...color, a: this.#color.a };
|
|
12815
|
-
this.#drawColorArea();
|
|
12816
|
-
this.#updateHandlePosition();
|
|
12817
|
-
if (this.#hueSlider) {
|
|
12818
|
-
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
12819
|
-
}
|
|
12820
|
-
this.#emitInput();
|
|
12821
|
-
};
|
|
12822
|
-
|
|
12823
|
-
const onChange = () => this.#emitChange();
|
|
12824
|
-
|
|
12825
|
-
const inputs = container.querySelectorAll(
|
|
12826
|
-
"fig-input-number, fig-input-text",
|
|
12827
|
-
);
|
|
12828
|
-
inputs.forEach((el) => {
|
|
12829
|
-
el.addEventListener("input", onInput);
|
|
12830
|
-
el.addEventListener("change", onChange);
|
|
12831
|
-
});
|
|
12832
|
-
}
|
|
12833
|
-
|
|
12834
|
-
#readColorFromInputs() {
|
|
12835
|
-
const q = (cls) => this.#dialog?.querySelector(`.${cls}`);
|
|
12836
|
-
const val = (cls) => parseFloat(q(cls)?.value ?? 0);
|
|
12837
|
-
|
|
12838
|
-
switch (this.#colorInputMode) {
|
|
12839
|
-
case "rgb":
|
|
12840
|
-
return this.#rgbToHSV({
|
|
12841
|
-
r: val("fig-fill-picker-ci-r"),
|
|
12842
|
-
g: val("fig-fill-picker-ci-g"),
|
|
12843
|
-
b: val("fig-fill-picker-ci-b"),
|
|
12844
|
-
});
|
|
12845
|
-
case "hsl": {
|
|
12846
|
-
const rgb = this.#hslToRGB({
|
|
12847
|
-
h: val("fig-fill-picker-ci-h"),
|
|
12848
|
-
s: val("fig-fill-picker-ci-s"),
|
|
12849
|
-
l: val("fig-fill-picker-ci-l"),
|
|
12850
|
-
});
|
|
12851
|
-
return this.#rgbToHSV(rgb);
|
|
12852
|
-
}
|
|
12853
|
-
case "hsb":
|
|
12854
|
-
return {
|
|
12855
|
-
h: val("fig-fill-picker-ci-h"),
|
|
12856
|
-
s: val("fig-fill-picker-ci-s"),
|
|
12857
|
-
v: val("fig-fill-picker-ci-v"),
|
|
12858
|
-
a: 1,
|
|
12859
|
-
};
|
|
12860
|
-
case "lab": {
|
|
12861
|
-
const rgb = this.#oklabToRGB({
|
|
12862
|
-
l: val("fig-fill-picker-ci-okl") / 100,
|
|
12863
|
-
a: val("fig-fill-picker-ci-oka"),
|
|
12864
|
-
b: val("fig-fill-picker-ci-okb"),
|
|
12865
|
-
});
|
|
12866
|
-
return this.#rgbToHSV(rgb);
|
|
12867
|
-
}
|
|
12868
|
-
case "lch": {
|
|
12869
|
-
const rgb = this.#oklchToRGB({
|
|
12870
|
-
l: val("fig-fill-picker-ci-okl") / 100,
|
|
12871
|
-
c: val("fig-fill-picker-ci-okc"),
|
|
12872
|
-
h: val("fig-fill-picker-ci-okh"),
|
|
12873
|
-
});
|
|
12874
|
-
return this.#rgbToHSV(rgb);
|
|
12875
|
-
}
|
|
12876
|
-
default: {
|
|
12877
|
-
// hex
|
|
12878
|
-
const hexEl = q("fig-fill-picker-ci-hex");
|
|
12879
|
-
if (!hexEl) return null;
|
|
12880
|
-
let hex = hexEl.value.replace(/^#/, "");
|
|
12881
|
-
if (hex.length === 3)
|
|
12882
|
-
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
12883
|
-
if (hex.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
12884
|
-
return this.#hexToHSV(`#${hex}`);
|
|
12885
|
-
}
|
|
12886
|
-
}
|
|
12887
|
-
}
|
|
12888
|
-
|
|
12889
|
-
#updateColorInputs() {
|
|
12890
|
-
if (!this.#dialog) return;
|
|
12891
|
-
|
|
12892
|
-
const hex = this.#hsvToHex(this.#color);
|
|
12893
|
-
const rgb = this.#hsvToRGB(this.#color);
|
|
12894
|
-
const q = (cls) => this.#dialog.querySelector(`.${cls}`);
|
|
12895
|
-
const set = (cls, v) => {
|
|
12896
|
-
const el = q(cls);
|
|
12897
|
-
if (el) el.setAttribute("value", v);
|
|
12898
|
-
};
|
|
12899
|
-
|
|
12900
|
-
switch (this.#colorInputMode) {
|
|
12901
|
-
case "rgb":
|
|
12902
|
-
set("fig-fill-picker-ci-r", rgb.r);
|
|
12903
|
-
set("fig-fill-picker-ci-g", rgb.g);
|
|
12904
|
-
set("fig-fill-picker-ci-b", rgb.b);
|
|
12905
|
-
break;
|
|
12906
|
-
case "hsl": {
|
|
12907
|
-
const hsl = this.#rgbToHSL(rgb);
|
|
12908
|
-
set("fig-fill-picker-ci-h", Math.round(hsl.h));
|
|
12909
|
-
set("fig-fill-picker-ci-s", Math.round(hsl.s));
|
|
12910
|
-
set("fig-fill-picker-ci-l", Math.round(hsl.l));
|
|
12911
|
-
break;
|
|
12912
|
-
}
|
|
12913
|
-
case "hsb":
|
|
12914
|
-
set("fig-fill-picker-ci-h", Math.round(this.#color.h));
|
|
12915
|
-
set("fig-fill-picker-ci-s", Math.round(this.#color.s));
|
|
12916
|
-
set("fig-fill-picker-ci-v", Math.round(this.#color.v));
|
|
12917
|
-
break;
|
|
12918
|
-
case "lab": {
|
|
12919
|
-
const lab = this.#rgbToOKLAB(rgb);
|
|
12920
|
-
set("fig-fill-picker-ci-okl", Math.round(lab.l * 100));
|
|
12921
|
-
set("fig-fill-picker-ci-oka", +lab.a.toFixed(3));
|
|
12922
|
-
set("fig-fill-picker-ci-okb", +lab.b.toFixed(3));
|
|
12923
|
-
break;
|
|
12924
|
-
}
|
|
12925
|
-
case "lch": {
|
|
12926
|
-
const lch = this.#rgbToOKLCH(rgb);
|
|
12927
|
-
set("fig-fill-picker-ci-okl", Math.round(lch.l * 100));
|
|
12928
|
-
set("fig-fill-picker-ci-okc", +lch.c.toFixed(3));
|
|
12929
|
-
set("fig-fill-picker-ci-okh", Math.round(lch.h));
|
|
12930
|
-
break;
|
|
12931
|
-
}
|
|
12932
|
-
default: // hex
|
|
12933
|
-
set("fig-fill-picker-ci-hex", hex.replace(/^#/, "").toUpperCase());
|
|
12934
|
-
break;
|
|
12935
|
-
}
|
|
12936
|
-
|
|
12937
|
-
if (this.#opacitySlider) {
|
|
12938
|
-
this.#opacitySlider.setAttribute("color", hex);
|
|
12939
|
-
}
|
|
12940
|
-
|
|
12941
|
-
this.#updateChit();
|
|
12942
|
-
}
|
|
12943
|
-
|
|
12944
|
-
// ============ GRADIENT TAB ============
|
|
12945
|
-
#initGradientTab() {
|
|
12946
|
-
const container = this.#dialog.querySelector('[data-tab="gradient"]');
|
|
12947
|
-
const experimental = this.getAttribute("experimental");
|
|
12948
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
12949
|
-
|
|
12950
|
-
container.innerHTML = `
|
|
12951
|
-
<fig-field class="fig-fill-picker-gradient-header" direction="horizontal">
|
|
12952
|
-
<fig-dropdown class="fig-fill-picker-gradient-type" ${expAttr} value="${
|
|
12953
|
-
this.#gradient.type
|
|
12954
|
-
}">
|
|
12955
|
-
<option value="linear" selected>Linear</option>
|
|
12956
|
-
<option value="radial">Radial</option>
|
|
12957
|
-
<option value="angular">Angular</option>
|
|
12958
|
-
</fig-dropdown>
|
|
12959
|
-
<fig-tooltip text="Rotate gradient">
|
|
12960
|
-
<fig-input-number class="fig-fill-picker-gradient-angle" value="${
|
|
12961
|
-
(this.#gradient.angle - 90 + 360) % 360
|
|
12962
|
-
}" min="0" max="360" units="°" wrap></fig-input-number>
|
|
12963
|
-
</fig-tooltip>
|
|
12964
|
-
<div class="fig-fill-picker-gradient-center input-combo" style="display: none;">
|
|
12965
|
-
<fig-input-number min="0" max="100" value="${
|
|
12966
|
-
this.#gradient.centerX
|
|
12967
|
-
}" units="%" class="fig-fill-picker-gradient-cx"></fig-input-number>
|
|
12968
|
-
<fig-input-number min="0" max="100" value="${
|
|
12969
|
-
this.#gradient.centerY
|
|
12970
|
-
}" units="%" class="fig-fill-picker-gradient-cy"></fig-input-number>
|
|
12971
|
-
</div>
|
|
12972
|
-
<fig-tooltip text="Flip gradient">
|
|
12973
|
-
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-flip">
|
|
12974
|
-
<fig-icon name="swap"></fig-icon>
|
|
12975
|
-
</fig-button>
|
|
12976
|
-
</fig-tooltip>
|
|
12977
|
-
</fig-field>
|
|
12978
|
-
<fig-preview class="fig-fill-picker-gradient-preview">
|
|
12979
|
-
<fig-input-gradient class="fig-fill-picker-gradient-bar-input" edit="true" size="large" value='${JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) })}'></fig-input-gradient>
|
|
12980
|
-
</fig-preview>
|
|
12981
|
-
<fig-field class="fig-fill-picker-gradient-interpolation" direction="horizontal">
|
|
12982
|
-
<label>Mixing</label>
|
|
12983
|
-
<fig-dropdown class="fig-fill-picker-gradient-space" full ${expAttr} value="${
|
|
12984
|
-
this.#gradient.interpolationSpace === "oklch"
|
|
12985
|
-
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
12986
|
-
: this.#gradient.interpolationSpace
|
|
12987
|
-
}">
|
|
12988
|
-
<optgroup label="sRGB">
|
|
12989
|
-
<option value="srgb-linear">Linear</option>
|
|
12990
|
-
</optgroup>
|
|
12991
|
-
<optgroup label="OKLab">
|
|
12992
|
-
<option value="oklab">Perceptual</option>
|
|
12993
|
-
</optgroup>
|
|
12994
|
-
<optgroup label="OKLCH">
|
|
12995
|
-
<option value="oklch-shorter">Shorter hue</option>
|
|
12996
|
-
<option value="oklch-longer">Longer hue</option>
|
|
12997
|
-
<option value="oklch-increasing">Increasing hue</option>
|
|
12998
|
-
<option value="oklch-decreasing">Decreasing hue</option>
|
|
12999
|
-
</optgroup>
|
|
13000
|
-
</fig-dropdown>
|
|
13001
|
-
</fig-field>
|
|
13002
|
-
<div class="fig-fill-picker-gradient-stops">
|
|
13003
|
-
<fig-header class="fig-fill-picker-gradient-stops-header" borderless>
|
|
13004
|
-
<span>Stops</span>
|
|
13005
|
-
<fig-button icon variant="ghost" class="fig-fill-picker-gradient-add" title="Add stop">
|
|
13006
|
-
<fig-icon name="add"></fig-icon>
|
|
13007
|
-
</fig-button>
|
|
13008
|
-
</fig-header>
|
|
13009
|
-
<div class="fig-fill-picker-gradient-stops-list"></div>
|
|
13010
|
-
</div>
|
|
13011
|
-
`;
|
|
13012
|
-
|
|
13013
|
-
this.#updateGradientUI();
|
|
13014
|
-
this.#setupGradientEvents(container);
|
|
13015
|
-
}
|
|
13016
|
-
|
|
13017
|
-
#setupGradientEvents(container) {
|
|
13018
|
-
// Type dropdown
|
|
13019
|
-
const typeDropdown = container.querySelector(
|
|
13020
|
-
".fig-fill-picker-gradient-type",
|
|
13021
|
-
);
|
|
13022
|
-
const getDropdownValue = (event) =>
|
|
13023
|
-
event.currentTarget?.value ?? event.target?.value ?? event.detail;
|
|
13024
|
-
|
|
13025
|
-
const handleTypeChange = (e) => {
|
|
13026
|
-
this.#gradient.type = getDropdownValue(e);
|
|
13027
|
-
this.#updateGradientUI();
|
|
13028
|
-
this.#emitInput();
|
|
13029
|
-
};
|
|
13030
|
-
typeDropdown.addEventListener("input", handleTypeChange);
|
|
13031
|
-
typeDropdown.addEventListener("change", handleTypeChange);
|
|
13032
|
-
|
|
13033
|
-
const interpolationDropdown = container.querySelector(
|
|
13034
|
-
".fig-fill-picker-gradient-space",
|
|
13035
|
-
);
|
|
13036
|
-
const handleInterpolationChange = (e) => {
|
|
13037
|
-
const val = getDropdownValue(e);
|
|
13038
|
-
let space = val;
|
|
13039
|
-
let hue = "shorter";
|
|
13040
|
-
if (val.startsWith("oklch-")) {
|
|
13041
|
-
space = "oklch";
|
|
13042
|
-
hue = val.slice(6);
|
|
13043
|
-
}
|
|
13044
|
-
this.#gradient = normalizeGradientConfig({
|
|
13045
|
-
...this.#gradient,
|
|
13046
|
-
interpolationSpace: space,
|
|
13047
|
-
hueInterpolation: hue,
|
|
13048
|
-
});
|
|
13049
|
-
this.#updateGradientUI();
|
|
13050
|
-
this.#emitInput();
|
|
13051
|
-
};
|
|
13052
|
-
interpolationDropdown?.addEventListener("input", handleInterpolationChange);
|
|
13053
|
-
interpolationDropdown?.addEventListener(
|
|
13054
|
-
"change",
|
|
13055
|
-
handleInterpolationChange,
|
|
13056
|
-
);
|
|
13057
|
-
|
|
13058
|
-
// Angle input
|
|
13059
|
-
const angleInput = container.querySelector(
|
|
13060
|
-
".fig-fill-picker-gradient-angle",
|
|
13061
|
-
);
|
|
13062
|
-
angleInput.addEventListener("input", (e) => {
|
|
13063
|
-
const pickerAngle = parseFloat(e.target.value) || 0;
|
|
13064
|
-
this.#gradient.angle = (pickerAngle + 90) % 360;
|
|
13065
|
-
this.#updateGradientPreview();
|
|
13066
|
-
this.#emitInput();
|
|
13067
|
-
});
|
|
13068
|
-
|
|
13069
|
-
// Center X/Y inputs
|
|
13070
|
-
const cxInput = container.querySelector(".fig-fill-picker-gradient-cx");
|
|
13071
|
-
const cyInput = container.querySelector(".fig-fill-picker-gradient-cy");
|
|
13072
|
-
cxInput?.addEventListener("input", (e) => {
|
|
13073
|
-
this.#gradient.centerX = parseFloat(e.target.value) || 50;
|
|
13074
|
-
this.#updateGradientPreview();
|
|
13075
|
-
this.#emitInput();
|
|
13076
|
-
});
|
|
13077
|
-
cyInput?.addEventListener("input", (e) => {
|
|
13078
|
-
this.#gradient.centerY = parseFloat(e.target.value) || 50;
|
|
13079
|
-
this.#updateGradientPreview();
|
|
13080
|
-
this.#emitInput();
|
|
13081
|
-
});
|
|
13082
|
-
|
|
13083
|
-
// Flip button
|
|
13084
|
-
container
|
|
13085
|
-
.querySelector(".fig-fill-picker-gradient-flip")
|
|
13086
|
-
.addEventListener("click", () => {
|
|
13087
|
-
this.#gradient.stops.forEach((stop) => {
|
|
13088
|
-
stop.position = 100 - stop.position;
|
|
13089
|
-
});
|
|
13090
|
-
this.#gradient.stops.sort((a, b) => a.position - b.position);
|
|
13091
|
-
this.#updateGradientUI();
|
|
13092
|
-
this.#emitInput();
|
|
13093
|
-
});
|
|
13094
|
-
|
|
13095
|
-
// Add stop button
|
|
13096
|
-
container
|
|
13097
|
-
.querySelector(".fig-fill-picker-gradient-add")
|
|
13098
|
-
.addEventListener("click", () => {
|
|
13099
|
-
const midPosition = 50;
|
|
13100
|
-
this.#gradient.stops.push({
|
|
13101
|
-
position: midPosition,
|
|
13102
|
-
color: "#888888",
|
|
13103
|
-
opacity: 100,
|
|
13104
|
-
});
|
|
13105
|
-
this.#gradient.stops.sort((a, b) => a.position - b.position);
|
|
13106
|
-
this.#updateGradientUI();
|
|
13107
|
-
this.#emitInput();
|
|
13108
|
-
});
|
|
13109
|
-
|
|
13110
|
-
// Embedded gradient bar input
|
|
13111
|
-
const gradientBarInput = container.querySelector(
|
|
13112
|
-
".fig-fill-picker-gradient-bar-input",
|
|
13113
|
-
);
|
|
13114
|
-
if (gradientBarInput) {
|
|
13115
|
-
const syncFromBarInput = (e) => {
|
|
13116
|
-
e.stopPropagation();
|
|
13117
|
-
const detail = e.detail;
|
|
13118
|
-
if (!detail?.gradient) return;
|
|
13119
|
-
this.#gradient = normalizeGradientConfig({
|
|
13120
|
-
...this.#gradient,
|
|
13121
|
-
...detail.gradient,
|
|
13122
|
-
});
|
|
13123
|
-
this.#updateChit();
|
|
13124
|
-
this.#updateGradientStopsList();
|
|
13125
|
-
};
|
|
13126
|
-
gradientBarInput.addEventListener("input", (e) => {
|
|
13127
|
-
syncFromBarInput(e);
|
|
13128
|
-
this.#emitInput();
|
|
13129
|
-
});
|
|
13130
|
-
gradientBarInput.addEventListener("change", (e) => {
|
|
13131
|
-
syncFromBarInput(e);
|
|
13132
|
-
this.#emitChange();
|
|
13133
|
-
});
|
|
13134
|
-
}
|
|
13135
|
-
}
|
|
13136
|
-
|
|
13137
|
-
#updateGradientUI() {
|
|
13138
|
-
if (!this.#dialog) return;
|
|
13139
|
-
|
|
13140
|
-
const container = this.#dialog.querySelector('[data-tab="gradient"]');
|
|
13141
|
-
if (!container) return;
|
|
13142
|
-
this.#gradient = normalizeGradientConfig(this.#gradient);
|
|
13143
|
-
|
|
13144
|
-
// Show/hide angle vs center inputs
|
|
13145
|
-
const angleInput = container.querySelector(
|
|
13146
|
-
".fig-fill-picker-gradient-angle",
|
|
13147
|
-
);
|
|
13148
|
-
const centerInputs = container.querySelector(
|
|
13149
|
-
".fig-fill-picker-gradient-center",
|
|
13150
|
-
);
|
|
13151
|
-
|
|
13152
|
-
if (this.#gradient.type === "radial") {
|
|
13153
|
-
angleInput.style.display = "none";
|
|
13154
|
-
centerInputs.style.display = "flex";
|
|
13155
|
-
} else {
|
|
13156
|
-
angleInput.style.display = "block";
|
|
13157
|
-
centerInputs.style.display = "none";
|
|
13158
|
-
// Sync angle input value (convert CSS angle to picker angle)
|
|
13159
|
-
const pickerAngle = (this.#gradient.angle - 90 + 360) % 360;
|
|
13160
|
-
angleInput.setAttribute("value", pickerAngle);
|
|
13161
|
-
}
|
|
13162
|
-
|
|
13163
|
-
const interpolationDropdown = container.querySelector(
|
|
13164
|
-
".fig-fill-picker-gradient-space",
|
|
13165
|
-
);
|
|
13166
|
-
if (interpolationDropdown) {
|
|
13167
|
-
interpolationDropdown.value =
|
|
13168
|
-
this.#gradient.interpolationSpace === "oklch"
|
|
13169
|
-
? `oklch-${this.#gradient.hueInterpolation || "shorter"}`
|
|
13170
|
-
: this.#gradient.interpolationSpace;
|
|
13171
|
-
}
|
|
13172
|
-
|
|
13173
|
-
this.#updateGradientPreview();
|
|
13174
|
-
this.#updateGradientStopsList();
|
|
13175
|
-
}
|
|
13176
|
-
|
|
13177
|
-
#updateGradientPreview() {
|
|
13178
|
-
if (!this.#dialog) return;
|
|
13179
|
-
|
|
13180
|
-
const barInput = this.#dialog.querySelector(
|
|
13181
|
-
".fig-fill-picker-gradient-bar-input",
|
|
13182
|
-
);
|
|
13183
|
-
if (barInput) {
|
|
13184
|
-
barInput.setAttribute(
|
|
13185
|
-
"value",
|
|
13186
|
-
JSON.stringify({
|
|
13187
|
-
type: "gradient",
|
|
13188
|
-
gradient: gradientToValueShape(this.#gradient),
|
|
13189
|
-
}),
|
|
13190
|
-
);
|
|
13191
|
-
}
|
|
13192
|
-
|
|
13193
|
-
this.#updateChit();
|
|
13194
|
-
}
|
|
13195
|
-
|
|
13196
|
-
#updateGradientStopsList() {
|
|
13197
|
-
if (!this.#dialog) return;
|
|
13198
|
-
|
|
13199
|
-
const list = this.#dialog.querySelector(
|
|
13200
|
-
".fig-fill-picker-gradient-stops-list",
|
|
13201
|
-
);
|
|
13202
|
-
if (!list) return;
|
|
13203
|
-
|
|
13204
|
-
const existingRows = list.querySelectorAll(
|
|
13205
|
-
".fig-fill-picker-gradient-stop-row",
|
|
13206
|
-
);
|
|
13207
|
-
|
|
13208
|
-
if (existingRows.length === this.#gradient.stops.length) {
|
|
13209
|
-
this.#gradient.stops.forEach((stop, index) => {
|
|
13210
|
-
const row = existingRows[index];
|
|
13211
|
-
row.dataset.index = index;
|
|
13212
|
-
const posInput = row.querySelector(".fig-fill-picker-stop-position");
|
|
13213
|
-
if (posInput) posInput.setAttribute("value", stop.position);
|
|
13214
|
-
const colorInput = row.querySelector(".fig-fill-picker-stop-color");
|
|
13215
|
-
if (colorInput) colorInput.setAttribute("value", stop.color);
|
|
13216
|
-
const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
|
|
13217
|
-
if (removeBtn) {
|
|
13218
|
-
if (this.#gradient.stops.length <= 2)
|
|
13219
|
-
removeBtn.setAttribute("disabled", "");
|
|
13220
|
-
else removeBtn.removeAttribute("disabled");
|
|
13221
|
-
}
|
|
13222
|
-
});
|
|
13223
|
-
return;
|
|
13224
|
-
}
|
|
13225
|
-
|
|
13226
|
-
this.#rebuildGradientStopsList(list);
|
|
13227
|
-
}
|
|
13228
|
-
|
|
13229
|
-
#rebuildGradientStopsList(list) {
|
|
13230
|
-
list.innerHTML = this.#gradient.stops
|
|
13231
|
-
.map(
|
|
13232
|
-
(stop, index) => `
|
|
13233
|
-
<fig-field class="fig-fill-picker-gradient-stop-row" direction="horizontal" data-index="${index}">
|
|
13234
|
-
<fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
|
|
13235
|
-
stop.position
|
|
13236
|
-
}" units="%"></fig-input-number>
|
|
13237
|
-
<fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" picker-dialog-position="right" value="${
|
|
13238
|
-
stop.color
|
|
13239
|
-
}"></fig-input-color>
|
|
13240
|
-
<fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
|
|
13241
|
-
this.#gradient.stops.length <= 2 ? "disabled" : ""
|
|
13242
|
-
}>
|
|
13243
|
-
<fig-icon name="minus"></fig-icon>
|
|
13244
|
-
</fig-button>
|
|
13245
|
-
</fig-field>
|
|
13246
|
-
`,
|
|
13247
|
-
)
|
|
13248
|
-
.join("");
|
|
13249
|
-
|
|
13250
|
-
list
|
|
13251
|
-
.querySelectorAll(".fig-fill-picker-gradient-stop-row")
|
|
13252
|
-
.forEach((row) => {
|
|
13253
|
-
const index = parseInt(row.dataset.index);
|
|
13254
|
-
|
|
13255
|
-
row
|
|
13256
|
-
.querySelector(".fig-fill-picker-stop-position")
|
|
13257
|
-
.addEventListener("input", (e) => {
|
|
13258
|
-
this.#gradient.stops[index].position =
|
|
13259
|
-
parseFloat(e.target.value) || 0;
|
|
13260
|
-
this.#updateGradientPreview();
|
|
13261
|
-
this.#emitInput();
|
|
13262
|
-
});
|
|
13263
|
-
|
|
13264
|
-
const stopColor = row.querySelector(".fig-fill-picker-stop-color");
|
|
13265
|
-
const stopFillPicker = stopColor.querySelector("fig-fill-picker");
|
|
13266
|
-
if (stopFillPicker) {
|
|
13267
|
-
stopFillPicker.anchorElement = this.#dialog;
|
|
13268
|
-
} else {
|
|
13269
|
-
requestAnimationFrame(() => {
|
|
13270
|
-
const fp = stopColor.querySelector("fig-fill-picker");
|
|
13271
|
-
if (fp) fp.anchorElement = this.#dialog;
|
|
13272
|
-
});
|
|
13273
|
-
}
|
|
13274
|
-
|
|
13275
|
-
stopColor.addEventListener("input", (e) => {
|
|
13276
|
-
this.#gradient.stops[index].color =
|
|
13277
|
-
e.target.hexOpaque || e.target.value;
|
|
13278
|
-
const a = e.detail?.rgba?.a;
|
|
13279
|
-
if (a !== undefined) {
|
|
13280
|
-
this.#gradient.stops[index].opacity = Math.round(a * 100);
|
|
13281
|
-
}
|
|
13282
|
-
this.#updateGradientPreview();
|
|
13283
|
-
this.#emitInput();
|
|
13284
|
-
});
|
|
13285
|
-
|
|
13286
|
-
row
|
|
13287
|
-
.querySelector(".fig-fill-picker-stop-remove")
|
|
13288
|
-
.addEventListener("click", () => {
|
|
13289
|
-
if (this.#gradient.stops.length > 2) {
|
|
13290
|
-
this.#gradient.stops.splice(index, 1);
|
|
13291
|
-
this.#updateGradientUI();
|
|
13292
|
-
this.#emitInput();
|
|
13293
|
-
}
|
|
13294
|
-
});
|
|
13295
|
-
});
|
|
13296
|
-
}
|
|
13297
|
-
|
|
13298
|
-
#buildGradientCSS(interpolationSpaceOverride, includeInterpolation = true) {
|
|
13299
|
-
const gradient = normalizeGradientConfig({
|
|
13300
|
-
...this.#gradient,
|
|
13301
|
-
interpolationSpace:
|
|
13302
|
-
interpolationSpaceOverride ?? this.#gradient.interpolationSpace,
|
|
13303
|
-
});
|
|
13304
|
-
const isP3 = this.#gamut === "display-p3";
|
|
13305
|
-
const stops = gradient.stops
|
|
13306
|
-
.map((s) => {
|
|
13307
|
-
const alpha = (s.opacity ?? 100) / 100;
|
|
13308
|
-
const color = isP3
|
|
13309
|
-
? this.#hexToP3(s.color, alpha)
|
|
13310
|
-
: this.#hexToRGBA(s.color, alpha);
|
|
13311
|
-
return `${color} ${s.position}%`;
|
|
13312
|
-
})
|
|
13313
|
-
.join(", ");
|
|
13314
|
-
const interpolation = includeInterpolation
|
|
13315
|
-
? ` ${gradientInterpolationClause(gradient)}`
|
|
13316
|
-
: "";
|
|
13317
|
-
switch (gradient.type) {
|
|
13318
|
-
case "linear":
|
|
13319
|
-
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
13320
|
-
case "radial":
|
|
13321
|
-
return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
|
|
13322
|
-
case "angular":
|
|
13323
|
-
return `conic-gradient(from ${gradient.angle}deg${interpolation}, ${stops})`;
|
|
13324
|
-
default:
|
|
13325
|
-
return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
|
|
13326
|
-
}
|
|
13327
|
-
}
|
|
13328
|
-
|
|
13329
|
-
static #gradientSupportCache = new Map();
|
|
13330
|
-
#testGradientSupport(css) {
|
|
13331
|
-
const cached = FigFillPicker.#gradientSupportCache.get(css);
|
|
13332
|
-
if (cached !== undefined) return cached;
|
|
13333
|
-
const el = document.createElement("div");
|
|
13334
|
-
el.style.background = css;
|
|
13335
|
-
const result = !!el.style.background;
|
|
13336
|
-
FigFillPicker.#gradientSupportCache.set(css, result);
|
|
13337
|
-
return result;
|
|
13338
|
-
}
|
|
13339
|
-
|
|
13340
|
-
#getGradientCSS() {
|
|
13341
|
-
const preferred = this.#buildGradientCSS(undefined, true);
|
|
13342
|
-
if (this.#testGradientSupport(preferred)) return preferred;
|
|
13343
|
-
|
|
13344
|
-
const oklabFallback = this.#buildGradientCSS("oklab", true);
|
|
13345
|
-
if (this.#testGradientSupport(oklabFallback)) return oklabFallback;
|
|
13346
|
-
|
|
13347
|
-
return this.#buildGradientCSS("oklab", false);
|
|
13348
|
-
}
|
|
13349
|
-
|
|
13350
|
-
// ============ IMAGE TAB ============
|
|
13351
|
-
#initImageTab() {
|
|
13352
|
-
const container = this.#dialog.querySelector('[data-tab="image"]');
|
|
13353
|
-
const experimental = this.getAttribute("experimental");
|
|
13354
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
13355
|
-
|
|
13356
|
-
container.innerHTML = `
|
|
13357
|
-
<fig-field class="fig-fill-picker-media-header" direction="horizontal">
|
|
13358
|
-
<fig-dropdown class="fig-fill-picker-scale-mode" ${expAttr} value="${
|
|
13359
|
-
this.#image.scaleMode
|
|
13360
|
-
}">
|
|
13361
|
-
<option value="fill" selected>Fill</option>
|
|
13362
|
-
<option value="fit">Fit</option>
|
|
13363
|
-
<option value="crop">Crop</option>
|
|
13364
|
-
<option value="tile">Tile</option>
|
|
13365
|
-
</fig-dropdown>
|
|
13366
|
-
<fig-input-number class="fig-fill-picker-scale" min="1" max="200" value="${
|
|
13367
|
-
this.#image.scale
|
|
13368
|
-
}" units="%" ${
|
|
13369
|
-
this.#image.scaleMode === "tile" ? "" : 'style="display: none;"'
|
|
13370
|
-
}></fig-input-number>
|
|
13371
|
-
</fig-field>
|
|
13372
|
-
<fig-image class="fig-fill-picker-media-preview fig-fill-picker-image-preview" upload="true" label="Upload from computer" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true"></fig-image>
|
|
13373
|
-
`;
|
|
13374
|
-
|
|
13375
|
-
this.#setupImageEvents(container);
|
|
13376
|
-
}
|
|
13377
|
-
|
|
13378
|
-
#setupImageEvents(container) {
|
|
13379
|
-
const scaleModeDropdown = container.querySelector(
|
|
13380
|
-
".fig-fill-picker-scale-mode",
|
|
13381
|
-
);
|
|
13382
|
-
const scaleInput = container.querySelector(".fig-fill-picker-scale");
|
|
13383
|
-
const preview = container.querySelector(".fig-fill-picker-image-preview");
|
|
13384
|
-
|
|
13385
|
-
scaleModeDropdown.addEventListener("change", (e) => {
|
|
13386
|
-
this.#image.scaleMode = e.target.value;
|
|
13387
|
-
scaleInput.style.display = e.target.value === "tile" ? "block" : "none";
|
|
13388
|
-
this.#updateImagePreview(preview);
|
|
13389
|
-
this.#updateChit();
|
|
13390
|
-
this.#emitInput();
|
|
13391
|
-
});
|
|
13392
|
-
|
|
13393
|
-
scaleInput.addEventListener("input", (e) => {
|
|
13394
|
-
this.#image.scale = parseFloat(e.target.value) || 100;
|
|
13395
|
-
this.#updateImagePreview(preview);
|
|
13396
|
-
this.#updateChit();
|
|
13397
|
-
this.#emitInput();
|
|
13398
|
-
});
|
|
13399
|
-
|
|
13400
|
-
preview.addEventListener("loaded", (e) => {
|
|
13401
|
-
const src = e.detail?.src || preview.src;
|
|
13402
|
-
if (!src) return;
|
|
13403
|
-
this.#image.url = src;
|
|
13404
|
-
this.#updateImagePreview(preview);
|
|
13405
|
-
this.#updateChit();
|
|
13406
|
-
this.#emitInput();
|
|
13407
|
-
});
|
|
13408
|
-
|
|
13409
|
-
preview.addEventListener("change", () => {
|
|
13410
|
-
if (preview.src) return;
|
|
13411
|
-
this.#image.url = null;
|
|
13412
|
-
this.#updateImagePreview(preview);
|
|
13413
|
-
this.#updateChit();
|
|
13414
|
-
this.#emitInput();
|
|
13415
|
-
});
|
|
13416
|
-
|
|
13417
|
-
this.#updateImagePreview(preview);
|
|
13418
|
-
}
|
|
13419
|
-
|
|
13420
|
-
#updateImagePreview(element) {
|
|
13421
|
-
if (!this.#image.url) {
|
|
13422
|
-
element.removeAttribute("src");
|
|
13423
|
-
element.classList.remove("has-media", "is-tiled");
|
|
13424
|
-
element.style.backgroundImage = "";
|
|
13425
|
-
element.style.backgroundPosition = "";
|
|
13426
|
-
element.style.backgroundRepeat = "";
|
|
13427
|
-
element.style.backgroundSize = "";
|
|
13428
|
-
return;
|
|
13429
|
-
}
|
|
13430
|
-
|
|
13431
|
-
element.setAttribute("src", this.#image.url);
|
|
13432
|
-
element.classList.add("has-media");
|
|
13433
|
-
element.style.backgroundImage = "";
|
|
13434
|
-
element.style.backgroundPosition = "";
|
|
13435
|
-
element.style.backgroundRepeat = "";
|
|
13436
|
-
element.style.backgroundSize = "";
|
|
13437
|
-
element.mediaEl?.style.removeProperty("opacity");
|
|
13438
|
-
|
|
13439
|
-
const fileInput = element.querySelector("fig-input-file[data-generated]");
|
|
13440
|
-
if (fileInput) {
|
|
13441
|
-
fileInput.setAttribute("label", "Replace");
|
|
13442
|
-
fileInput.removeAttribute("url");
|
|
13443
|
-
}
|
|
13444
|
-
|
|
13445
|
-
switch (this.#image.scaleMode) {
|
|
13446
|
-
case "fill":
|
|
13447
|
-
element.classList.remove("is-tiled");
|
|
13448
|
-
element.setAttribute("fit", "cover");
|
|
13449
|
-
break;
|
|
13450
|
-
case "crop":
|
|
13451
|
-
element.classList.remove("is-tiled");
|
|
13452
|
-
element.setAttribute("fit", "cover");
|
|
13453
|
-
break;
|
|
13454
|
-
case "fit":
|
|
13455
|
-
element.classList.remove("is-tiled");
|
|
13456
|
-
element.setAttribute("fit", "contain");
|
|
13457
|
-
break;
|
|
13458
|
-
case "tile":
|
|
13459
|
-
element.classList.add("is-tiled");
|
|
13460
|
-
element.setAttribute("fit", "none");
|
|
13461
|
-
element.style.backgroundImage = `url(${this.#image.url})`;
|
|
13462
|
-
element.style.backgroundPosition = "top left";
|
|
13463
|
-
element.style.backgroundSize = `${this.#image.scale}%`;
|
|
13464
|
-
element.style.backgroundRepeat = "repeat";
|
|
13465
|
-
if (element.mediaEl) element.mediaEl.style.opacity = "0";
|
|
13466
|
-
break;
|
|
13467
|
-
}
|
|
13468
|
-
}
|
|
13469
|
-
|
|
13470
|
-
// For video elements (still uses object-fit)
|
|
13471
|
-
#updateVideoPreviewStyle(element) {
|
|
13472
|
-
if (element.tagName === "FIG-MEDIA") {
|
|
13473
|
-
if (!this.#video.url) {
|
|
13474
|
-
element.removeAttribute("src");
|
|
13475
|
-
element.classList.remove("has-media");
|
|
13476
|
-
return;
|
|
13477
|
-
}
|
|
13478
|
-
|
|
13479
|
-
element.setAttribute("src", this.#video.url);
|
|
13480
|
-
element.classList.add("has-media");
|
|
13481
|
-
|
|
13482
|
-
const fileInput = element.querySelector("fig-input-file[data-generated]");
|
|
13483
|
-
if (fileInput) {
|
|
13484
|
-
fileInput.setAttribute("label", "Replace");
|
|
13485
|
-
fileInput.removeAttribute("url");
|
|
13486
|
-
}
|
|
13487
|
-
|
|
13488
|
-
switch (this.#video.scaleMode) {
|
|
13489
|
-
case "fill":
|
|
13490
|
-
case "crop":
|
|
13491
|
-
element.setAttribute("fit", "cover");
|
|
13492
|
-
break;
|
|
13493
|
-
case "fit":
|
|
13494
|
-
element.setAttribute("fit", "contain");
|
|
13495
|
-
break;
|
|
13496
|
-
}
|
|
13497
|
-
return;
|
|
13498
|
-
}
|
|
13499
|
-
|
|
13500
|
-
element.style.objectPosition = "center";
|
|
13501
|
-
element.style.width = "100%";
|
|
13502
|
-
element.style.height = "100%";
|
|
13503
|
-
|
|
13504
|
-
switch (this.#video.scaleMode) {
|
|
13505
|
-
case "fill":
|
|
13506
|
-
case "crop":
|
|
13507
|
-
element.style.objectFit = "cover";
|
|
13508
|
-
break;
|
|
13509
|
-
case "fit":
|
|
13510
|
-
element.style.objectFit = "contain";
|
|
13511
|
-
break;
|
|
13512
|
-
}
|
|
13513
|
-
}
|
|
13514
|
-
|
|
13515
|
-
// ============ VIDEO TAB ============
|
|
13516
|
-
#initVideoTab() {
|
|
13517
|
-
const container = this.#dialog.querySelector('[data-tab="video"]');
|
|
13518
|
-
const experimental = this.getAttribute("experimental");
|
|
13519
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
13520
|
-
|
|
13521
|
-
container.innerHTML = `
|
|
13522
|
-
<fig-field class="fig-fill-picker-media-header" direction="horizontal">
|
|
13523
|
-
<fig-dropdown class="fig-fill-picker-scale-mode" ${expAttr} value="${
|
|
13524
|
-
this.#video.scaleMode
|
|
13525
|
-
}">
|
|
13526
|
-
<option value="fill" selected>Fill</option>
|
|
13527
|
-
<option value="fit">Fit</option>
|
|
13528
|
-
<option value="crop">Crop</option>
|
|
13529
|
-
</fig-dropdown>
|
|
13530
|
-
</fig-field>
|
|
13531
|
-
<fig-media class="fig-fill-picker-media-preview fig-fill-picker-video-preview" type="video" upload="true" label="Upload from computer" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" muted="true" loop="true"></fig-media>
|
|
13532
|
-
`;
|
|
13533
|
-
|
|
13534
|
-
this.#setupVideoEvents(container);
|
|
13535
|
-
}
|
|
13536
|
-
|
|
13537
|
-
#setupVideoEvents(container) {
|
|
13538
|
-
const scaleModeDropdown = container.querySelector(
|
|
13539
|
-
".fig-fill-picker-scale-mode",
|
|
13540
|
-
);
|
|
13541
|
-
const preview = container.querySelector(".fig-fill-picker-video-preview");
|
|
13542
|
-
|
|
13543
|
-
scaleModeDropdown.addEventListener("change", (e) => {
|
|
13544
|
-
this.#video.scaleMode = e.target.value;
|
|
13545
|
-
this.#updateVideoPreviewStyle(preview);
|
|
13546
|
-
this.#updateChit();
|
|
13547
|
-
this.#emitInput();
|
|
13548
|
-
});
|
|
13549
|
-
|
|
13550
|
-
preview.addEventListener("loaded", (e) => {
|
|
13551
|
-
const src = e.detail?.src || preview.src;
|
|
13552
|
-
if (!src) return;
|
|
13553
|
-
this.#video.url = src;
|
|
13554
|
-
this.#updateVideoPreviewStyle(preview);
|
|
13555
|
-
preview.play?.();
|
|
13556
|
-
this.#updateChit();
|
|
13557
|
-
this.#emitInput();
|
|
13558
|
-
});
|
|
13559
|
-
|
|
13560
|
-
preview.addEventListener("change", () => {
|
|
13561
|
-
if (preview.src) return;
|
|
13562
|
-
this.#video.url = null;
|
|
13563
|
-
this.#updateVideoPreviewStyle(preview);
|
|
13564
|
-
this.#updateChit();
|
|
13565
|
-
this.#emitInput();
|
|
13566
|
-
});
|
|
13567
|
-
|
|
13568
|
-
this.#updateVideoPreviewStyle(preview);
|
|
13569
|
-
}
|
|
13570
|
-
|
|
13571
|
-
// ============ WEBCAM TAB ============
|
|
13572
|
-
#initWebcamTab() {
|
|
13573
|
-
const container = this.#dialog.querySelector('[data-tab="webcam"]');
|
|
13574
|
-
const experimental = this.getAttribute("experimental");
|
|
13575
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
13576
|
-
|
|
13577
|
-
container.innerHTML = `
|
|
13578
|
-
<div class="fig-fill-picker-webcam-preview">
|
|
13579
|
-
<div class="fig-fill-picker-checkerboard"></div>
|
|
13580
|
-
<video class="fig-fill-picker-webcam-video" autoplay muted playsinline></video>
|
|
13581
|
-
<div class="fig-fill-picker-webcam-status">
|
|
13582
|
-
<span>Camera access required</span>
|
|
13583
|
-
</div>
|
|
13584
|
-
</div>
|
|
13585
|
-
<fig-field class="fig-fill-picker-webcam-controls" direction="horizontal">
|
|
13586
|
-
<fig-dropdown class="fig-fill-picker-camera-select" ${expAttr} style="display: none;">
|
|
13587
|
-
</fig-dropdown>
|
|
13588
|
-
<fig-button class="fig-fill-picker-webcam-capture" variant="primary">
|
|
13589
|
-
Capture
|
|
13590
|
-
</fig-button>
|
|
13591
|
-
</fig-field>
|
|
13592
|
-
`;
|
|
13593
|
-
|
|
13594
|
-
this.#setupWebcamEvents(container);
|
|
13595
|
-
}
|
|
13596
|
-
|
|
13597
|
-
#setupWebcamEvents(container) {
|
|
13598
|
-
const video = container.querySelector(".fig-fill-picker-webcam-video");
|
|
13599
|
-
const status = container.querySelector(".fig-fill-picker-webcam-status");
|
|
13600
|
-
const captureBtn = container.querySelector(
|
|
13601
|
-
".fig-fill-picker-webcam-capture",
|
|
13602
|
-
);
|
|
13603
|
-
const cameraSelect = container.querySelector(
|
|
13604
|
-
".fig-fill-picker-camera-select",
|
|
13605
|
-
);
|
|
13606
|
-
|
|
13607
|
-
const startWebcam = async (deviceId = null) => {
|
|
13608
|
-
try {
|
|
13609
|
-
const constraints = {
|
|
13610
|
-
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
|
13611
|
-
};
|
|
13612
|
-
|
|
13613
|
-
if (this.#webcam.stream) {
|
|
13614
|
-
this.#webcam.stream.getTracks().forEach((track) => track.stop());
|
|
13615
|
-
}
|
|
13616
|
-
|
|
13617
|
-
this.#webcam.stream =
|
|
13618
|
-
await navigator.mediaDevices.getUserMedia(constraints);
|
|
13619
|
-
video.srcObject = this.#webcam.stream;
|
|
13620
|
-
video.style.display = "block";
|
|
13621
|
-
status.style.display = "none";
|
|
13622
|
-
|
|
13623
|
-
// Enumerate cameras
|
|
13624
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
13625
|
-
const cameras = devices.filter((d) => d.kind === "videoinput");
|
|
13626
|
-
|
|
13627
|
-
if (cameras.length > 1) {
|
|
13628
|
-
cameraSelect.style.display = "block";
|
|
13629
|
-
cameraSelect.innerHTML = cameras
|
|
13630
|
-
.map(
|
|
13631
|
-
(cam, i) =>
|
|
13632
|
-
`<option value="${cam.deviceId}">${
|
|
13633
|
-
cam.label || `Camera ${i + 1}`
|
|
13634
|
-
}</option>`,
|
|
13635
|
-
)
|
|
13636
|
-
.join("");
|
|
13637
|
-
}
|
|
13638
|
-
} catch (err) {
|
|
13639
|
-
console.error("Webcam error:", err.name, err.message);
|
|
13640
|
-
let message = "Camera access denied";
|
|
13641
|
-
if (err.name === "NotAllowedError") {
|
|
13642
|
-
message = "Camera permission denied";
|
|
13643
|
-
} else if (err.name === "NotFoundError") {
|
|
13644
|
-
message = "No camera found";
|
|
13645
|
-
} else if (err.name === "NotReadableError") {
|
|
13646
|
-
message = "Camera in use by another app";
|
|
13647
|
-
} else if (err.name === "OverconstrainedError") {
|
|
13648
|
-
message = "Camera constraints not supported";
|
|
13649
|
-
} else if (!window.isSecureContext) {
|
|
13650
|
-
message = "Camera requires secure context";
|
|
13651
|
-
}
|
|
13652
|
-
status.innerHTML = `<span>${message}</span>`;
|
|
13653
|
-
status.style.display = "flex";
|
|
13654
|
-
video.style.display = "none";
|
|
13655
|
-
}
|
|
13656
|
-
};
|
|
13657
|
-
|
|
13658
|
-
this.#webcamTabObserver = new MutationObserver(() => {
|
|
13659
|
-
if (container.style.display !== "none" && !this.#webcam.stream) {
|
|
13660
|
-
startWebcam();
|
|
13661
|
-
}
|
|
13662
|
-
});
|
|
13663
|
-
this.#webcamTabObserver.observe(container, {
|
|
13664
|
-
attributes: true,
|
|
13665
|
-
attributeFilter: ["style"],
|
|
13666
|
-
});
|
|
13667
|
-
|
|
13668
|
-
cameraSelect.addEventListener("change", (e) => {
|
|
13669
|
-
startWebcam(e.target.value);
|
|
13670
|
-
});
|
|
13671
|
-
|
|
13672
|
-
captureBtn.addEventListener("click", () => {
|
|
13673
|
-
if (!this.#webcam.stream) return;
|
|
13674
|
-
|
|
13675
|
-
const canvas = document.createElement("canvas");
|
|
13676
|
-
canvas.width = video.videoWidth;
|
|
13677
|
-
canvas.height = video.videoHeight;
|
|
13678
|
-
canvas.getContext("2d").drawImage(video, 0, 0);
|
|
13679
|
-
|
|
13680
|
-
this.#webcam.snapshot = canvas.toDataURL("image/png");
|
|
13681
|
-
this.#image.url = this.#webcam.snapshot;
|
|
13682
|
-
this.#fillType = "image";
|
|
13683
|
-
this.#updateChit();
|
|
13684
|
-
this.#emitInput();
|
|
13685
|
-
|
|
13686
|
-
// Switch to image tab to show result
|
|
13687
|
-
this.#switchTab("image");
|
|
13688
|
-
const tabs = this.#dialog.querySelector("fig-tabs");
|
|
13689
|
-
tabs.value = "image";
|
|
13690
|
-
});
|
|
13691
|
-
}
|
|
13692
|
-
|
|
13693
|
-
// ============ COLOR CONVERSION UTILITIES ============
|
|
13694
|
-
#hsvToRGB(hsv) {
|
|
13695
|
-
const h = hsv.h / 360;
|
|
13696
|
-
const s = hsv.s / 100;
|
|
13697
|
-
const v = hsv.v / 100;
|
|
13698
|
-
|
|
13699
|
-
let r, g, b;
|
|
13700
|
-
const i = Math.floor(h * 6);
|
|
13701
|
-
const f = h * 6 - i;
|
|
13702
|
-
const p = v * (1 - s);
|
|
13703
|
-
const q = v * (1 - f * s);
|
|
13704
|
-
const t = v * (1 - (1 - f) * s);
|
|
13705
|
-
|
|
13706
|
-
switch (i % 6) {
|
|
13707
|
-
case 0:
|
|
13708
|
-
r = v;
|
|
13709
|
-
g = t;
|
|
13710
|
-
b = p;
|
|
13711
|
-
break;
|
|
13712
|
-
case 1:
|
|
13713
|
-
r = q;
|
|
13714
|
-
g = v;
|
|
13715
|
-
b = p;
|
|
13716
|
-
break;
|
|
13717
|
-
case 2:
|
|
13718
|
-
r = p;
|
|
13719
|
-
g = v;
|
|
13720
|
-
b = t;
|
|
13721
|
-
break;
|
|
13722
|
-
case 3:
|
|
13723
|
-
r = p;
|
|
13724
|
-
g = q;
|
|
13725
|
-
b = v;
|
|
13726
|
-
break;
|
|
13727
|
-
case 4:
|
|
13728
|
-
r = t;
|
|
13729
|
-
g = p;
|
|
13730
|
-
b = v;
|
|
13731
|
-
break;
|
|
13732
|
-
case 5:
|
|
13733
|
-
r = v;
|
|
13734
|
-
g = p;
|
|
13735
|
-
b = q;
|
|
13736
|
-
break;
|
|
13737
|
-
}
|
|
13738
|
-
|
|
13739
|
-
return {
|
|
13740
|
-
r: Math.round(r * 255),
|
|
13741
|
-
g: Math.round(g * 255),
|
|
13742
|
-
b: Math.round(b * 255),
|
|
13743
|
-
};
|
|
13744
|
-
}
|
|
13745
|
-
|
|
13746
|
-
#rgbToHSV(rgb) {
|
|
13747
|
-
const r = rgb.r / 255;
|
|
13748
|
-
const g = rgb.g / 255;
|
|
13749
|
-
const b = rgb.b / 255;
|
|
13750
|
-
|
|
13751
|
-
const max = Math.max(r, g, b);
|
|
13752
|
-
const min = Math.min(r, g, b);
|
|
13753
|
-
const d = max - min;
|
|
13754
|
-
|
|
13755
|
-
let h = 0;
|
|
13756
|
-
const s = max === 0 ? 0 : d / max;
|
|
13757
|
-
const v = max;
|
|
13758
|
-
|
|
13759
|
-
if (max !== min) {
|
|
13760
|
-
switch (max) {
|
|
13761
|
-
case r:
|
|
13762
|
-
h = (g - b) / d + (g < b ? 6 : 0);
|
|
13763
|
-
break;
|
|
13764
|
-
case g:
|
|
13765
|
-
h = (b - r) / d + 2;
|
|
13766
|
-
break;
|
|
13767
|
-
case b:
|
|
13768
|
-
h = (r - g) / d + 4;
|
|
13769
|
-
break;
|
|
13770
|
-
}
|
|
13771
|
-
h /= 6;
|
|
13772
|
-
}
|
|
13773
|
-
|
|
13774
|
-
return {
|
|
13775
|
-
h: h * 360,
|
|
13776
|
-
s: s * 100,
|
|
13777
|
-
v: v * 100,
|
|
13778
|
-
a: 1,
|
|
13779
|
-
};
|
|
13780
|
-
}
|
|
13781
|
-
|
|
13782
|
-
#hsvToHex(hsv) {
|
|
13783
|
-
// Safety check for valid HSV object
|
|
13784
|
-
if (
|
|
13785
|
-
!hsv ||
|
|
13786
|
-
typeof hsv.h !== "number" ||
|
|
13787
|
-
typeof hsv.s !== "number" ||
|
|
13788
|
-
typeof hsv.v !== "number"
|
|
13789
|
-
) {
|
|
13790
|
-
return "#D9D9D9"; // Default gray
|
|
13791
|
-
}
|
|
13792
|
-
const rgb = this.#hsvToRGB(hsv);
|
|
13793
|
-
const toHex = (n) => {
|
|
13794
|
-
const val = isNaN(n) ? 217 : Math.max(0, Math.min(255, Math.round(n)));
|
|
13795
|
-
return val.toString(16).padStart(2, "0");
|
|
13796
|
-
};
|
|
13797
|
-
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
|
|
13798
|
-
}
|
|
13799
|
-
|
|
13800
|
-
#hexToHSV(hex) {
|
|
13801
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
13802
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
13803
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
13804
|
-
return this.#rgbToHSV({ r, g, b });
|
|
13805
|
-
}
|
|
13806
|
-
|
|
13807
|
-
#hexToRGBA(hex, alpha = 1) {
|
|
13808
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
13809
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
13810
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
13811
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
13812
|
-
}
|
|
13813
|
-
|
|
13814
|
-
#hexToP3(hex, alpha = 1) {
|
|
13815
|
-
const r = +(parseInt(hex.slice(1, 3), 16) / 255).toFixed(4);
|
|
13816
|
-
const g = +(parseInt(hex.slice(3, 5), 16) / 255).toFixed(4);
|
|
13817
|
-
const b = +(parseInt(hex.slice(5, 7), 16) / 255).toFixed(4);
|
|
13818
|
-
return `color(display-p3 ${r} ${g} ${b} / ${alpha})`;
|
|
13819
|
-
}
|
|
13820
|
-
|
|
13821
|
-
#rgbToHSL(rgb) {
|
|
13822
|
-
const r = rgb.r / 255;
|
|
13823
|
-
const g = rgb.g / 255;
|
|
13824
|
-
const b = rgb.b / 255;
|
|
13825
|
-
|
|
13826
|
-
const max = Math.max(r, g, b);
|
|
13827
|
-
const min = Math.min(r, g, b);
|
|
13828
|
-
let h, s;
|
|
13829
|
-
const l = (max + min) / 2;
|
|
13830
|
-
|
|
13831
|
-
if (max === min) {
|
|
13832
|
-
h = s = 0;
|
|
13833
|
-
} else {
|
|
13834
|
-
const d = max - min;
|
|
13835
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
13836
|
-
|
|
13837
|
-
switch (max) {
|
|
13838
|
-
case r:
|
|
13839
|
-
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
13840
|
-
break;
|
|
13841
|
-
case g:
|
|
13842
|
-
h = ((b - r) / d + 2) / 6;
|
|
13843
|
-
break;
|
|
13844
|
-
case b:
|
|
13845
|
-
h = ((r - g) / d + 4) / 6;
|
|
13846
|
-
break;
|
|
13847
|
-
}
|
|
13848
|
-
}
|
|
13849
|
-
|
|
13850
|
-
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
13851
|
-
}
|
|
13852
|
-
|
|
13853
|
-
#hslToRGB(hsl) {
|
|
13854
|
-
const h = hsl.h / 360;
|
|
13855
|
-
const s = hsl.s / 100;
|
|
13856
|
-
const l = hsl.l / 100;
|
|
13857
|
-
|
|
13858
|
-
let r, g, b;
|
|
13859
|
-
|
|
13860
|
-
if (s === 0) {
|
|
13861
|
-
r = g = b = l;
|
|
13862
|
-
} else {
|
|
13863
|
-
const hue2rgb = (p, q, t) => {
|
|
13864
|
-
if (t < 0) t += 1;
|
|
13865
|
-
if (t > 1) t -= 1;
|
|
13866
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
13867
|
-
if (t < 1 / 2) return q;
|
|
13868
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
13869
|
-
return p;
|
|
13870
|
-
};
|
|
13871
|
-
|
|
13872
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
13873
|
-
const p = 2 * l - q;
|
|
13874
|
-
|
|
13875
|
-
r = hue2rgb(p, q, h + 1 / 3);
|
|
13876
|
-
g = hue2rgb(p, q, h);
|
|
13877
|
-
b = hue2rgb(p, q, h - 1 / 3);
|
|
13878
|
-
}
|
|
13879
|
-
|
|
13880
|
-
return {
|
|
13881
|
-
r: Math.round(r * 255),
|
|
13882
|
-
g: Math.round(g * 255),
|
|
13883
|
-
b: Math.round(b * 255),
|
|
13884
|
-
};
|
|
13885
|
-
}
|
|
13886
|
-
|
|
13887
|
-
// OKLAB/OKLCH conversions (simplified)
|
|
13888
|
-
#rgbToOKLAB(rgb) {
|
|
13889
|
-
// Convert to linear sRGB
|
|
13890
|
-
const toLinear = (c) => {
|
|
13891
|
-
c = c / 255;
|
|
13892
|
-
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
13893
|
-
};
|
|
13894
|
-
|
|
13895
|
-
const r = toLinear(rgb.r);
|
|
13896
|
-
const g = toLinear(rgb.g);
|
|
13897
|
-
const b = toLinear(rgb.b);
|
|
13898
|
-
|
|
13899
|
-
// Convert to LMS
|
|
13900
|
-
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
13901
|
-
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
13902
|
-
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
13903
|
-
|
|
13904
|
-
// Convert to Oklab
|
|
13905
|
-
const l_ = Math.cbrt(l);
|
|
13906
|
-
const m_ = Math.cbrt(m);
|
|
13907
|
-
const s_ = Math.cbrt(s);
|
|
13908
|
-
|
|
13909
|
-
return {
|
|
13910
|
-
l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
|
|
13911
|
-
a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
|
|
13912
|
-
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
|
|
13913
|
-
};
|
|
13914
|
-
}
|
|
13915
|
-
|
|
13916
|
-
#rgbToOKLCH(rgb) {
|
|
13917
|
-
const lab = this.#rgbToOKLAB(rgb);
|
|
13918
|
-
return {
|
|
13919
|
-
l: lab.l,
|
|
13920
|
-
c: Math.sqrt(lab.a * lab.a + lab.b * lab.b),
|
|
13921
|
-
h: ((Math.atan2(lab.b, lab.a) * 180) / Math.PI + 360) % 360,
|
|
13922
|
-
};
|
|
13923
|
-
}
|
|
13924
|
-
|
|
13925
|
-
#oklabToRGB(lab) {
|
|
13926
|
-
const l_ = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
|
|
13927
|
-
const m_ = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
|
|
13928
|
-
const s_ = lab.l - 0.0894841775 * lab.a - 1.291485548 * lab.b;
|
|
13929
|
-
|
|
13930
|
-
const l = l_ * l_ * l_;
|
|
13931
|
-
const m = m_ * m_ * m_;
|
|
13932
|
-
const s = s_ * s_ * s_;
|
|
13933
|
-
|
|
13934
|
-
const toSRGB = (c) => {
|
|
13935
|
-
const v =
|
|
13936
|
-
c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
13937
|
-
return Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
13938
|
-
};
|
|
13939
|
-
|
|
13940
|
-
return {
|
|
13941
|
-
r: toSRGB(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
13942
|
-
g: toSRGB(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
13943
|
-
b: toSRGB(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
13944
|
-
};
|
|
13945
|
-
}
|
|
13946
|
-
|
|
13947
|
-
#oklchToRGB(lch) {
|
|
13948
|
-
const hRad = (lch.h * Math.PI) / 180;
|
|
13949
|
-
return this.#oklabToRGB({
|
|
13950
|
-
l: lch.l,
|
|
13951
|
-
a: lch.c * Math.cos(hRad),
|
|
13952
|
-
b: lch.c * Math.sin(hRad),
|
|
13953
|
-
});
|
|
13954
|
-
}
|
|
13955
|
-
|
|
13956
|
-
// ============ EVENT EMITTERS ============
|
|
13957
|
-
#emitInput() {
|
|
13958
|
-
this.#updateChit();
|
|
13959
|
-
this.dispatchEvent(
|
|
13960
|
-
new CustomEvent("input", {
|
|
13961
|
-
bubbles: true,
|
|
13962
|
-
detail: this.value,
|
|
13963
|
-
}),
|
|
13964
|
-
);
|
|
13965
|
-
}
|
|
13966
|
-
|
|
13967
|
-
#emitChange() {
|
|
13968
|
-
this.dispatchEvent(
|
|
13969
|
-
new CustomEvent("change", {
|
|
13970
|
-
bubbles: true,
|
|
13971
|
-
detail: this.value,
|
|
13972
|
-
}),
|
|
13973
|
-
);
|
|
13974
|
-
}
|
|
13975
|
-
|
|
13976
|
-
// ============ PUBLIC API ============
|
|
13977
|
-
get value() {
|
|
13978
|
-
const base = { type: this.#fillType, colorSpace: this.#gamut };
|
|
13979
|
-
|
|
13980
|
-
switch (this.#fillType) {
|
|
13981
|
-
case "solid":
|
|
13982
|
-
return {
|
|
13983
|
-
...base,
|
|
13984
|
-
color: this.#hsvToHex(this.#color),
|
|
13985
|
-
alpha: this.#color.a,
|
|
13986
|
-
hsv: { ...this.#color },
|
|
13987
|
-
};
|
|
13988
|
-
case "gradient":
|
|
13989
|
-
return {
|
|
13990
|
-
...base,
|
|
13991
|
-
gradient: gradientToValueShape(this.#gradient),
|
|
13992
|
-
css: this.#getGradientCSS(),
|
|
13993
|
-
};
|
|
13994
|
-
case "image":
|
|
13995
|
-
return {
|
|
13996
|
-
...base,
|
|
13997
|
-
image: { ...this.#image },
|
|
13998
|
-
};
|
|
13999
|
-
case "video":
|
|
14000
|
-
return {
|
|
14001
|
-
...base,
|
|
14002
|
-
video: { ...this.#video },
|
|
14003
|
-
};
|
|
14004
|
-
case "webcam":
|
|
14005
|
-
return {
|
|
14006
|
-
...base,
|
|
14007
|
-
image: { url: this.#webcam.snapshot, scaleMode: "fill", scale: 50 },
|
|
14008
|
-
};
|
|
14009
|
-
default:
|
|
14010
|
-
return { ...base, ...this.#customData[this.#fillType] };
|
|
14011
|
-
}
|
|
14012
|
-
}
|
|
14013
|
-
|
|
14014
|
-
set value(val) {
|
|
14015
|
-
if (typeof val === "string") {
|
|
14016
|
-
this.setAttribute("value", val);
|
|
14017
|
-
} else {
|
|
14018
|
-
this.setAttribute("value", JSON.stringify(val));
|
|
14019
|
-
}
|
|
14020
|
-
}
|
|
14021
|
-
|
|
14022
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
14023
|
-
if (oldValue === newValue) return;
|
|
14024
|
-
|
|
14025
|
-
switch (name) {
|
|
14026
|
-
case "value":
|
|
14027
|
-
this.#parseValue();
|
|
14028
|
-
this.#updateChit();
|
|
14029
|
-
if (this.#dialog) {
|
|
14030
|
-
// Update dialog UI if open - but don't rebuild if user is dragging
|
|
14031
|
-
if (!this.#isDraggingColor) {
|
|
14032
|
-
// Just update the handle position and color inputs without rebuilding
|
|
14033
|
-
this.#updateHandlePosition();
|
|
14034
|
-
this.#updateColorInputs();
|
|
14035
|
-
// Update hue slider
|
|
14036
|
-
if (this.#hueSlider) {
|
|
14037
|
-
this.#hueSlider.setAttribute("value", this.#color.h);
|
|
14038
|
-
}
|
|
14039
|
-
// Update opacity slider
|
|
14040
|
-
if (this.#opacitySlider) {
|
|
14041
|
-
this.#opacitySlider.setAttribute("value", this.#color.a * 100);
|
|
14042
|
-
this.#opacitySlider.setAttribute(
|
|
14043
|
-
"color",
|
|
14044
|
-
this.#hsvToHex(this.#color),
|
|
14045
|
-
);
|
|
14046
|
-
}
|
|
14047
|
-
}
|
|
14048
|
-
}
|
|
14049
|
-
break;
|
|
14050
|
-
case "disabled":
|
|
14051
|
-
// Handled in click listener
|
|
14052
|
-
break;
|
|
14053
|
-
}
|
|
14054
|
-
}
|
|
14055
|
-
}
|
|
14056
|
-
customElements.define("fig-fill-picker", FigFillPicker);
|
|
14057
12425
|
|
|
14058
12426
|
/* Color Tip */
|
|
14059
12427
|
/**
|
|
@@ -14094,6 +12462,10 @@ class FigColorTip extends HTMLElement {
|
|
|
14094
12462
|
this.#fillPicker.removeEventListener("input", this.#boundHandleInput);
|
|
14095
12463
|
this.#fillPicker.removeEventListener("change", this.#boundHandleChange);
|
|
14096
12464
|
}
|
|
12465
|
+
if (this.#chit) {
|
|
12466
|
+
this.#chit.removeEventListener("input", this.#boundHandleInput);
|
|
12467
|
+
this.#chit.removeEventListener("change", this.#boundHandleChange);
|
|
12468
|
+
}
|
|
14097
12469
|
if (this.#chitSelectedObserver) {
|
|
14098
12470
|
this.#chitSelectedObserver.disconnect();
|
|
14099
12471
|
this.#chitSelectedObserver = null;
|
|
@@ -14151,16 +12523,22 @@ class FigColorTip extends HTMLElement {
|
|
|
14151
12523
|
opacity: Math.round(alpha * 100),
|
|
14152
12524
|
})
|
|
14153
12525
|
: JSON.stringify({ type: "solid", color });
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
14157
|
-
|
|
12526
|
+
const chitAlphaAttr = alpha < 1 ? ` alpha="${alpha}"` : "";
|
|
12527
|
+
this.innerHTML = hasFigFillPicker()
|
|
12528
|
+
? `<fig-fill-picker mode="solid" ${alphaAttr} value='${pickerValue}'>
|
|
12529
|
+
<fig-chit background="${color}"${chitAlphaAttr}></fig-chit>
|
|
12530
|
+
</fig-fill-picker>`
|
|
12531
|
+
: `<fig-chit background="${color}"${chitAlphaAttr}></fig-chit>`;
|
|
14158
12532
|
|
|
14159
12533
|
this.#fillPicker = this.querySelector("fig-fill-picker");
|
|
14160
12534
|
this.#chit = this.querySelector("fig-chit");
|
|
14161
12535
|
this.#teardownListeners();
|
|
14162
12536
|
this.#fillPicker?.addEventListener("input", this.#boundHandleInput);
|
|
14163
12537
|
this.#fillPicker?.addEventListener("change", this.#boundHandleChange);
|
|
12538
|
+
if (!this.#fillPicker) {
|
|
12539
|
+
this.#chit?.addEventListener("input", this.#boundHandleInput);
|
|
12540
|
+
this.#chit?.addEventListener("change", this.#boundHandleChange);
|
|
12541
|
+
}
|
|
14164
12542
|
this.#observeChitSelected();
|
|
14165
12543
|
}
|
|
14166
12544
|
|
|
@@ -14187,8 +12565,13 @@ class FigColorTip extends HTMLElement {
|
|
|
14187
12565
|
#extractAlpha(colorValue) {
|
|
14188
12566
|
if (!colorValue) return 1;
|
|
14189
12567
|
const v = String(colorValue).trim();
|
|
14190
|
-
|
|
14191
|
-
|
|
12568
|
+
const hex = v.replace(/^#/, "");
|
|
12569
|
+
if (/^[0-9a-f]{4}$/i.test(hex)) {
|
|
12570
|
+
const a = hex[3];
|
|
12571
|
+
return parseInt(`${a}${a}`, 16) / 255;
|
|
12572
|
+
}
|
|
12573
|
+
if (/^[0-9a-f]{8}$/i.test(hex)) {
|
|
12574
|
+
return parseInt(hex.slice(6, 8), 16) / 255;
|
|
14192
12575
|
}
|
|
14193
12576
|
const rgbaMatch = v.match(
|
|
14194
12577
|
/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/i,
|
|
@@ -14215,6 +12598,9 @@ class FigColorTip extends HTMLElement {
|
|
|
14215
12598
|
if (value.startsWith("#")) {
|
|
14216
12599
|
return this.#normalizeHex(value);
|
|
14217
12600
|
}
|
|
12601
|
+
if (/^[0-9a-f]{3,4}$|^[0-9a-f]{6}$|^[0-9a-f]{8}$/i.test(value)) {
|
|
12602
|
+
return this.#normalizeHex(value);
|
|
12603
|
+
}
|
|
14218
12604
|
|
|
14219
12605
|
try {
|
|
14220
12606
|
const { ctx } = figGetSharedCanvas(1, 1);
|
|
@@ -14268,6 +12654,11 @@ class FigColorTip extends HTMLElement {
|
|
|
14268
12654
|
|
|
14269
12655
|
if (this.#chit) {
|
|
14270
12656
|
this.#chit.setAttribute("background", color);
|
|
12657
|
+
if (alpha < 1) {
|
|
12658
|
+
this.#chit.setAttribute("alpha", String(alpha));
|
|
12659
|
+
} else {
|
|
12660
|
+
this.#chit.removeAttribute("alpha");
|
|
12661
|
+
}
|
|
14271
12662
|
if (this.hasAttribute("disabled")) {
|
|
14272
12663
|
this.#chit.setAttribute("disabled", "");
|
|
14273
12664
|
} else {
|
|
@@ -14306,12 +12697,12 @@ class FigColorTip extends HTMLElement {
|
|
|
14306
12697
|
|
|
14307
12698
|
#handlePickerInput(event) {
|
|
14308
12699
|
event.stopPropagation();
|
|
14309
|
-
this.#updateColorFromPicker(event.detail, "input");
|
|
12700
|
+
this.#updateColorFromPicker(event.detail || { color: event.target?.value }, "input");
|
|
14310
12701
|
}
|
|
14311
12702
|
|
|
14312
12703
|
#handlePickerChange(event) {
|
|
14313
12704
|
event.stopPropagation();
|
|
14314
|
-
this.#updateColorFromPicker(event.detail, "change");
|
|
12705
|
+
this.#updateColorFromPicker(event.detail || { color: event.target?.value }, "change");
|
|
14315
12706
|
}
|
|
14316
12707
|
|
|
14317
12708
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -14998,6 +13389,7 @@ class FigHandle extends HTMLElement {
|
|
|
14998
13389
|
#applyingValue = false;
|
|
14999
13390
|
#colorTip = null;
|
|
15000
13391
|
#directColorPicker = null;
|
|
13392
|
+
#nativeColorInput = null;
|
|
15001
13393
|
#hitAreaEl = null;
|
|
15002
13394
|
|
|
15003
13395
|
get #controlMode() {
|
|
@@ -15240,6 +13632,7 @@ class FigHandle extends HTMLElement {
|
|
|
15240
13632
|
this.#teardownDrag();
|
|
15241
13633
|
this.#hideColorTip();
|
|
15242
13634
|
this.#removeDirectColorPicker();
|
|
13635
|
+
this.#removeNativeColorInput();
|
|
15243
13636
|
if (this.#hitAreaEl) {
|
|
15244
13637
|
this.#hitAreaEl.remove();
|
|
15245
13638
|
this.#hitAreaEl = null;
|
|
@@ -15566,6 +13959,7 @@ class FigHandle extends HTMLElement {
|
|
|
15566
13959
|
}
|
|
15567
13960
|
|
|
15568
13961
|
#ensureDirectColorPicker() {
|
|
13962
|
+
if (!hasFigFillPicker()) return null;
|
|
15569
13963
|
if (this.#directColorPicker) return this.#directColorPicker;
|
|
15570
13964
|
|
|
15571
13965
|
const picker = document.createElement("fig-fill-picker");
|
|
@@ -15591,6 +13985,10 @@ class FigHandle extends HTMLElement {
|
|
|
15591
13985
|
if (this.hasAttribute("disabled")) return;
|
|
15592
13986
|
this.#hideColorTip();
|
|
15593
13987
|
const picker = this.#ensureDirectColorPicker();
|
|
13988
|
+
if (!picker) {
|
|
13989
|
+
this.#openNativeColorPicker();
|
|
13990
|
+
return;
|
|
13991
|
+
}
|
|
15594
13992
|
this.setAttribute("selected", "");
|
|
15595
13993
|
this.#syncDirectColorPickerValue();
|
|
15596
13994
|
picker.open();
|
|
@@ -15607,6 +14005,40 @@ class FigHandle extends HTMLElement {
|
|
|
15607
14005
|
this.removeAttribute("selected");
|
|
15608
14006
|
}
|
|
15609
14007
|
|
|
14008
|
+
#ensureNativeColorInput() {
|
|
14009
|
+
if (this.#nativeColorInput) return this.#nativeColorInput;
|
|
14010
|
+
const input = document.createElement("input");
|
|
14011
|
+
input.type = "color";
|
|
14012
|
+
input.tabIndex = -1;
|
|
14013
|
+
input.setAttribute("aria-hidden", "true");
|
|
14014
|
+
input.style.position = "fixed";
|
|
14015
|
+
input.style.inlineSize = "1px";
|
|
14016
|
+
input.style.blockSize = "1px";
|
|
14017
|
+
input.style.opacity = "0";
|
|
14018
|
+
input.style.pointerEvents = "none";
|
|
14019
|
+
input.addEventListener("input", this.#handleNativeColorInput);
|
|
14020
|
+
input.addEventListener("change", this.#handleNativeColorChange);
|
|
14021
|
+
this.appendChild(input);
|
|
14022
|
+
this.#nativeColorInput = input;
|
|
14023
|
+
return input;
|
|
14024
|
+
}
|
|
14025
|
+
|
|
14026
|
+
#openNativeColorPicker() {
|
|
14027
|
+
const input = this.#ensureNativeColorInput();
|
|
14028
|
+
const { color } = this.#normalizeColorForPicker();
|
|
14029
|
+
input.value = color;
|
|
14030
|
+
this.setAttribute("selected", "");
|
|
14031
|
+
input.click();
|
|
14032
|
+
}
|
|
14033
|
+
|
|
14034
|
+
#removeNativeColorInput() {
|
|
14035
|
+
if (!this.#nativeColorInput) return;
|
|
14036
|
+
this.#nativeColorInput.removeEventListener("input", this.#handleNativeColorInput);
|
|
14037
|
+
this.#nativeColorInput.removeEventListener("change", this.#handleNativeColorChange);
|
|
14038
|
+
this.#nativeColorInput.remove();
|
|
14039
|
+
this.#nativeColorInput = null;
|
|
14040
|
+
}
|
|
14041
|
+
|
|
15610
14042
|
#closeColorPickerForDrag() {
|
|
15611
14043
|
if (this.getAttribute("type") !== "color") return;
|
|
15612
14044
|
this.#hideColorTip();
|
|
@@ -15684,6 +14116,36 @@ class FigHandle extends HTMLElement {
|
|
|
15684
14116
|
);
|
|
15685
14117
|
};
|
|
15686
14118
|
|
|
14119
|
+
#detailFromNativeColor(value) {
|
|
14120
|
+
const { opacity } = this.#normalizeColorForPicker();
|
|
14121
|
+
return opacity < 100 ? { color: value, opacity } : { color: value };
|
|
14122
|
+
}
|
|
14123
|
+
|
|
14124
|
+
#handleNativeColorInput = (e) => {
|
|
14125
|
+
e.stopPropagation();
|
|
14126
|
+
const detail = this.#detailFromNativeColor(e.target.value);
|
|
14127
|
+
this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
|
|
14128
|
+
this.dispatchEvent(
|
|
14129
|
+
new CustomEvent("input", {
|
|
14130
|
+
bubbles: true,
|
|
14131
|
+
detail,
|
|
14132
|
+
}),
|
|
14133
|
+
);
|
|
14134
|
+
};
|
|
14135
|
+
|
|
14136
|
+
#handleNativeColorChange = (e) => {
|
|
14137
|
+
e.stopPropagation();
|
|
14138
|
+
const detail = this.#detailFromNativeColor(e.target.value);
|
|
14139
|
+
this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
|
|
14140
|
+
this.removeAttribute("selected");
|
|
14141
|
+
this.dispatchEvent(
|
|
14142
|
+
new CustomEvent("change", {
|
|
14143
|
+
bubbles: true,
|
|
14144
|
+
detail,
|
|
14145
|
+
}),
|
|
14146
|
+
);
|
|
14147
|
+
};
|
|
14148
|
+
|
|
15687
14149
|
#handleDirectColorPickerClose = () => {
|
|
15688
14150
|
this.removeAttribute("selected");
|
|
15689
14151
|
};
|