@rogieking/figui3 4.15.10 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/skills/figui3/SKILL.md +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 +45 -203
- package/fig-editor.css +333 -0
- package/fig-editor.js +2195 -0
- package/fig.js +735 -2297
- 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
|
|
@@ -3628,8 +3676,7 @@ class FigSlider extends HTMLElement {
|
|
|
3628
3676
|
const rawValue = this.getAttribute("value");
|
|
3629
3677
|
this.type = this.getAttribute("type") || "range";
|
|
3630
3678
|
this.variant = this.getAttribute("variant") || "default";
|
|
3631
|
-
this.text =
|
|
3632
|
-
this.hasAttribute("text") && this.getAttribute("text") !== "false";
|
|
3679
|
+
this.text = this.getAttribute("text") !== "false";
|
|
3633
3680
|
this.units = this.getAttribute("units") || "";
|
|
3634
3681
|
this.transform = Number(this.getAttribute("transform") || 1);
|
|
3635
3682
|
this.disabled = this.getAttribute("disabled") ? true : false;
|
|
@@ -3997,7 +4044,7 @@ class FigSlider extends HTMLElement {
|
|
|
3997
4044
|
this.#regenerateInnerHTML();
|
|
3998
4045
|
break;
|
|
3999
4046
|
case "text":
|
|
4000
|
-
this.text = newValue !==
|
|
4047
|
+
this.text = newValue !== "false";
|
|
4001
4048
|
this.#regenerateInnerHTML();
|
|
4002
4049
|
break;
|
|
4003
4050
|
default:
|
|
@@ -4024,11 +4071,13 @@ customElements.define("fig-slider", FigSlider);
|
|
|
4024
4071
|
*/
|
|
4025
4072
|
class FigInputText extends HTMLElement {
|
|
4026
4073
|
#isInteracting = false;
|
|
4074
|
+
#passwordVisible = false;
|
|
4027
4075
|
#boundMouseMove;
|
|
4028
4076
|
#boundMouseUp;
|
|
4029
4077
|
#boundWindowBlur;
|
|
4030
4078
|
#boundMouseDown;
|
|
4031
4079
|
#boundInputChange;
|
|
4080
|
+
#boundNativeInput;
|
|
4032
4081
|
|
|
4033
4082
|
constructor() {
|
|
4034
4083
|
super();
|
|
@@ -4041,6 +4090,9 @@ class FigInputText extends HTMLElement {
|
|
|
4041
4090
|
e.stopPropagation();
|
|
4042
4091
|
this.#handleInputChange(e);
|
|
4043
4092
|
};
|
|
4093
|
+
this.#boundNativeInput = () => {
|
|
4094
|
+
this.#syncSearchClearVisibility();
|
|
4095
|
+
};
|
|
4044
4096
|
}
|
|
4045
4097
|
|
|
4046
4098
|
connectedCallback() {
|
|
@@ -4109,6 +4161,10 @@ class FigInputText extends HTMLElement {
|
|
|
4109
4161
|
|
|
4110
4162
|
this.input = this.querySelector("input,textarea");
|
|
4111
4163
|
this.input.readOnly = this.readonly;
|
|
4164
|
+
this.#syncSearchPrefix();
|
|
4165
|
+
this.#syncSearchClear();
|
|
4166
|
+
this.#syncSearchClearVisibility();
|
|
4167
|
+
this.#syncPasswordToggle();
|
|
4112
4168
|
|
|
4113
4169
|
if (this.type === "number") {
|
|
4114
4170
|
if (this.getAttribute("min")) {
|
|
@@ -4124,12 +4180,15 @@ class FigInputText extends HTMLElement {
|
|
|
4124
4180
|
}
|
|
4125
4181
|
this.input.removeEventListener("change", this.#boundInputChange);
|
|
4126
4182
|
this.input.addEventListener("change", this.#boundInputChange);
|
|
4183
|
+
this.input.removeEventListener("input", this.#boundNativeInput);
|
|
4184
|
+
this.input.addEventListener("input", this.#boundNativeInput);
|
|
4127
4185
|
});
|
|
4128
4186
|
}
|
|
4129
4187
|
|
|
4130
4188
|
disconnectedCallback() {
|
|
4131
4189
|
if (this.input) {
|
|
4132
4190
|
this.input.removeEventListener("change", this.#boundInputChange);
|
|
4191
|
+
this.input.removeEventListener("input", this.#boundNativeInput);
|
|
4133
4192
|
}
|
|
4134
4193
|
this.removeEventListener("pointerdown", this.#boundMouseDown);
|
|
4135
4194
|
window.removeEventListener("pointermove", this.#boundMouseMove);
|
|
@@ -4140,6 +4199,132 @@ class FigInputText extends HTMLElement {
|
|
|
4140
4199
|
focus() {
|
|
4141
4200
|
this.input.focus();
|
|
4142
4201
|
}
|
|
4202
|
+
#syncSearchPrefix() {
|
|
4203
|
+
const generated = this.querySelector(
|
|
4204
|
+
'[slot="prepend"][data-generated="search-prefix"]',
|
|
4205
|
+
);
|
|
4206
|
+
if (this.type !== "search") {
|
|
4207
|
+
generated?.remove();
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
const prepend = this.querySelector('[slot="prepend"]');
|
|
4211
|
+
if (prepend && prepend !== generated) return;
|
|
4212
|
+
if (generated) return;
|
|
4213
|
+
|
|
4214
|
+
const icon = createFigIcon("search");
|
|
4215
|
+
icon.setAttribute("slot", "prepend");
|
|
4216
|
+
icon.setAttribute("data-generated", "search-prefix");
|
|
4217
|
+
icon.setAttribute("color", "var(--figma-color-icon)");
|
|
4218
|
+
icon.addEventListener("click", this.focus.bind(this));
|
|
4219
|
+
this.prepend(icon);
|
|
4220
|
+
}
|
|
4221
|
+
#syncSearchClear() {
|
|
4222
|
+
const generated = this.querySelector(
|
|
4223
|
+
'[slot="append"][data-generated="search-clear"]',
|
|
4224
|
+
);
|
|
4225
|
+
if (this.type !== "search") {
|
|
4226
|
+
generated?.remove();
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
const append = this.querySelector('[slot="append"]');
|
|
4230
|
+
if (append && append !== generated) return;
|
|
4231
|
+
if (generated) return;
|
|
4232
|
+
|
|
4233
|
+
const wrapper = document.createElement("span");
|
|
4234
|
+
wrapper.setAttribute("slot", "append");
|
|
4235
|
+
wrapper.setAttribute("data-generated", "search-clear");
|
|
4236
|
+
|
|
4237
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
4238
|
+
tooltip.setAttribute("text", "Clear search");
|
|
4239
|
+
|
|
4240
|
+
const button = document.createElement("fig-button");
|
|
4241
|
+
button.setAttribute("variant", "ghost");
|
|
4242
|
+
button.setAttribute("icon", "");
|
|
4243
|
+
button.setAttribute("aria-label", "Clear search");
|
|
4244
|
+
|
|
4245
|
+
const icon = createFigIcon("", { size: "small" });
|
|
4246
|
+
icon.setAttribute("color", "var(--figma-color-icon-secondary)");
|
|
4247
|
+
button.append(icon);
|
|
4248
|
+
tooltip.append(button);
|
|
4249
|
+
wrapper.append(tooltip);
|
|
4250
|
+
this.append(wrapper);
|
|
4251
|
+
icon.style.setProperty("--icon", "var(--icon-16-close)");
|
|
4252
|
+
|
|
4253
|
+
button.addEventListener("click", (e) => {
|
|
4254
|
+
e.preventDefault();
|
|
4255
|
+
e.stopPropagation();
|
|
4256
|
+
if (!this.input || this.input.value === "") {
|
|
4257
|
+
this.focus();
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
this.value = "";
|
|
4261
|
+
this.input.value = "";
|
|
4262
|
+
this.dispatchEvent(new CustomEvent("input", { detail: "", bubbles: true }));
|
|
4263
|
+
this.dispatchEvent(new CustomEvent("change", { detail: "", bubbles: true }));
|
|
4264
|
+
this.#syncSearchClearVisibility();
|
|
4265
|
+
this.focus();
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
#syncSearchClearVisibility() {
|
|
4269
|
+
if (this.type !== "search") {
|
|
4270
|
+
this.removeAttribute("data-search-has-value");
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
this.toggleAttribute("data-search-has-value", !!this.input?.value);
|
|
4274
|
+
}
|
|
4275
|
+
#syncPasswordToggle() {
|
|
4276
|
+
const generated = this.querySelector(
|
|
4277
|
+
'[slot="append"][data-generated="password-toggle"]',
|
|
4278
|
+
);
|
|
4279
|
+
if (this.type !== "password") {
|
|
4280
|
+
generated?.remove();
|
|
4281
|
+
this.#passwordVisible = false;
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
const append = this.querySelector('[slot="append"]');
|
|
4285
|
+
if (append && append !== generated) return;
|
|
4286
|
+
if (generated) {
|
|
4287
|
+
this.#updatePasswordToggle(generated);
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
const wrapper = document.createElement("span");
|
|
4292
|
+
wrapper.setAttribute("slot", "append");
|
|
4293
|
+
wrapper.setAttribute("data-generated", "password-toggle");
|
|
4294
|
+
|
|
4295
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
4296
|
+
const button = document.createElement("fig-button");
|
|
4297
|
+
button.setAttribute("variant", "ghost");
|
|
4298
|
+
button.setAttribute("icon", "");
|
|
4299
|
+
|
|
4300
|
+
const icon = createFigIcon("visible", { size: "small" });
|
|
4301
|
+
icon.setAttribute("color", "var(--figma-color-icon-secondary)");
|
|
4302
|
+
button.append(icon);
|
|
4303
|
+
tooltip.append(button);
|
|
4304
|
+
wrapper.append(tooltip);
|
|
4305
|
+
this.append(wrapper);
|
|
4306
|
+
this.#updatePasswordToggle(wrapper);
|
|
4307
|
+
|
|
4308
|
+
button.addEventListener("click", (e) => {
|
|
4309
|
+
e.preventDefault();
|
|
4310
|
+
e.stopPropagation();
|
|
4311
|
+
this.#passwordVisible = !this.#passwordVisible;
|
|
4312
|
+
if (this.input) {
|
|
4313
|
+
this.input.type = this.#passwordVisible ? "text" : "password";
|
|
4314
|
+
}
|
|
4315
|
+
this.#updatePasswordToggle(wrapper);
|
|
4316
|
+
this.focus();
|
|
4317
|
+
});
|
|
4318
|
+
}
|
|
4319
|
+
#updatePasswordToggle(wrapper) {
|
|
4320
|
+
const tooltip = wrapper.querySelector("fig-tooltip");
|
|
4321
|
+
const button = wrapper.querySelector("fig-button");
|
|
4322
|
+
const icon = wrapper.querySelector("fig-icon");
|
|
4323
|
+
const label = this.#passwordVisible ? "Hide password" : "Show password";
|
|
4324
|
+
tooltip?.setAttribute("text", label);
|
|
4325
|
+
button?.setAttribute("aria-label", label);
|
|
4326
|
+
icon?.setAttribute("name", this.#passwordVisible ? "visible" : "hidden");
|
|
4327
|
+
}
|
|
4143
4328
|
#transformNumber(value) {
|
|
4144
4329
|
if (value === "") return "";
|
|
4145
4330
|
let transformed = Number(value) * (this.transform || 1);
|
|
@@ -4157,6 +4342,7 @@ class FigInputText extends HTMLElement {
|
|
|
4157
4342
|
}
|
|
4158
4343
|
this.value = value;
|
|
4159
4344
|
this.input.value = valueTransformed;
|
|
4345
|
+
this.#syncSearchClearVisibility();
|
|
4160
4346
|
this.dispatchEvent(
|
|
4161
4347
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4162
4348
|
);
|
|
@@ -4293,6 +4479,7 @@ class FigInputText extends HTMLElement {
|
|
|
4293
4479
|
this.value = value;
|
|
4294
4480
|
this.input.value = value;
|
|
4295
4481
|
}
|
|
4482
|
+
this.#syncSearchClearVisibility();
|
|
4296
4483
|
break;
|
|
4297
4484
|
case "min":
|
|
4298
4485
|
case "max":
|
|
@@ -4310,6 +4497,14 @@ class FigInputText extends HTMLElement {
|
|
|
4310
4497
|
this.placeholder = newValue ?? "";
|
|
4311
4498
|
this.input.placeholder = this.placeholder;
|
|
4312
4499
|
break;
|
|
4500
|
+
case "type":
|
|
4501
|
+
this.type = newValue || "text";
|
|
4502
|
+
this.input.type = this.type;
|
|
4503
|
+
this.#syncSearchPrefix();
|
|
4504
|
+
this.#syncSearchClear();
|
|
4505
|
+
this.#syncSearchClearVisibility();
|
|
4506
|
+
this.#syncPasswordToggle();
|
|
4507
|
+
break;
|
|
4313
4508
|
default:
|
|
4314
4509
|
this[name] = this.input[name] = newValue;
|
|
4315
4510
|
break;
|
|
@@ -4382,7 +4577,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4382
4577
|
e.preventDefault();
|
|
4383
4578
|
e.stopPropagation();
|
|
4384
4579
|
const btn = e.target.closest("button");
|
|
4385
|
-
if (!btn || this.disabled) return;
|
|
4580
|
+
if (!btn || this.disabled || btn.disabled) return;
|
|
4386
4581
|
const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
|
|
4387
4582
|
this.#stepValue(dir);
|
|
4388
4583
|
this.input.focus();
|
|
@@ -4392,6 +4587,31 @@ class FigInputNumber extends HTMLElement {
|
|
|
4392
4587
|
this.#stepperEl.remove();
|
|
4393
4588
|
this.#stepperEl = null;
|
|
4394
4589
|
}
|
|
4590
|
+
this.#syncStepperState();
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
#syncStepperState() {
|
|
4594
|
+
if (!this.#stepperEl) return;
|
|
4595
|
+
const up = this.#stepperEl.querySelector(".fig-stepper-up");
|
|
4596
|
+
const down = this.#stepperEl.querySelector(".fig-stepper-down");
|
|
4597
|
+
if (!up || !down) return;
|
|
4598
|
+
|
|
4599
|
+
const numericValue = this.input
|
|
4600
|
+
? this.#getNumericValue(this.input.value)
|
|
4601
|
+
: this.value;
|
|
4602
|
+
const current =
|
|
4603
|
+
numericValue !== "" && numericValue !== null && numericValue !== undefined
|
|
4604
|
+
? Number(numericValue) / (this.transform || 1)
|
|
4605
|
+
: Number(this.value);
|
|
4606
|
+
const hasCurrent = Number.isFinite(current);
|
|
4607
|
+
const disabled = Boolean(this.disabled);
|
|
4608
|
+
const atMin =
|
|
4609
|
+
hasCurrent && typeof this.min === "number" && current <= this.min;
|
|
4610
|
+
const atMax =
|
|
4611
|
+
hasCurrent && typeof this.max === "number" && current >= this.max;
|
|
4612
|
+
|
|
4613
|
+
up.disabled = disabled || atMax;
|
|
4614
|
+
down.disabled = disabled || atMin;
|
|
4395
4615
|
}
|
|
4396
4616
|
|
|
4397
4617
|
#stepValue(direction) {
|
|
@@ -4403,6 +4623,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4403
4623
|
value = this.#sanitizeInput(value, false);
|
|
4404
4624
|
this.value = value;
|
|
4405
4625
|
this.input.value = this.#formatWithUnit(this.value);
|
|
4626
|
+
this.#syncStepperState();
|
|
4406
4627
|
this.dispatchEvent(
|
|
4407
4628
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4408
4629
|
);
|
|
@@ -4513,6 +4734,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4513
4734
|
const disabledAttr = this.getAttribute("disabled");
|
|
4514
4735
|
this.disabled = this.input.disabled = disabledAttr !== "false";
|
|
4515
4736
|
}
|
|
4737
|
+
this.#syncStepperState();
|
|
4516
4738
|
|
|
4517
4739
|
this.addEventListener("pointerdown", this.#boundMouseDown);
|
|
4518
4740
|
this.input.removeEventListener("change", this.#boundInputChange);
|
|
@@ -4624,6 +4846,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4624
4846
|
this.value = "";
|
|
4625
4847
|
e.target.value = "";
|
|
4626
4848
|
}
|
|
4849
|
+
this.#syncStepperState();
|
|
4627
4850
|
this.dispatchEvent(
|
|
4628
4851
|
new CustomEvent("change", { detail: this.value, bubbles: true }),
|
|
4629
4852
|
);
|
|
@@ -4649,6 +4872,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4649
4872
|
value = this.#sanitizeInput(value, false);
|
|
4650
4873
|
this.value = value;
|
|
4651
4874
|
this.input.value = this.#formatWithUnit(this.value);
|
|
4875
|
+
this.#syncStepperState();
|
|
4652
4876
|
|
|
4653
4877
|
this.dispatchEvent(
|
|
4654
4878
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
@@ -4665,6 +4889,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4665
4889
|
} else {
|
|
4666
4890
|
this.value = "";
|
|
4667
4891
|
}
|
|
4892
|
+
this.#syncStepperState();
|
|
4668
4893
|
this.dispatchEvent(
|
|
4669
4894
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4670
4895
|
);
|
|
@@ -4682,6 +4907,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4682
4907
|
this.value = "";
|
|
4683
4908
|
e.target.value = "";
|
|
4684
4909
|
}
|
|
4910
|
+
this.#syncStepperState();
|
|
4685
4911
|
this.dispatchEvent(
|
|
4686
4912
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4687
4913
|
);
|
|
@@ -4702,6 +4928,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4702
4928
|
value = this.#sanitizeInput(value, false);
|
|
4703
4929
|
this.value = value;
|
|
4704
4930
|
this.input.value = this.#formatWithUnit(this.value);
|
|
4931
|
+
this.#syncStepperState();
|
|
4705
4932
|
this.dispatchEvent(
|
|
4706
4933
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
4707
4934
|
);
|
|
@@ -4784,6 +5011,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
4784
5011
|
case "disabled":
|
|
4785
5012
|
this.disabled = this.input.disabled =
|
|
4786
5013
|
newValue !== null && newValue !== "false";
|
|
5014
|
+
this.#syncStepperState();
|
|
4787
5015
|
break;
|
|
4788
5016
|
case "units":
|
|
4789
5017
|
this.#rawUnits = newValue || "";
|
|
@@ -4816,15 +5044,18 @@ class FigInputNumber extends HTMLElement {
|
|
|
4816
5044
|
}
|
|
4817
5045
|
this.value = value;
|
|
4818
5046
|
this.input.value = this.#formatWithUnit(this.value);
|
|
5047
|
+
this.#syncStepperState();
|
|
4819
5048
|
break;
|
|
4820
5049
|
case "min":
|
|
4821
5050
|
case "max":
|
|
4822
5051
|
case "step":
|
|
4823
5052
|
if (newValue === null || newValue === "") {
|
|
4824
5053
|
this[name] = undefined;
|
|
5054
|
+
this.#syncStepperState();
|
|
4825
5055
|
break;
|
|
4826
5056
|
}
|
|
4827
5057
|
this[name] = Number(newValue);
|
|
5058
|
+
this.#syncStepperState();
|
|
4828
5059
|
break;
|
|
4829
5060
|
case "steppers": {
|
|
4830
5061
|
const hasSteppers = newValue !== null && newValue !== "false";
|
|
@@ -5018,17 +5249,13 @@ class FigInputColor extends HTMLElement {
|
|
|
5018
5249
|
#fillPicker;
|
|
5019
5250
|
#textInput;
|
|
5020
5251
|
#alphaInput;
|
|
5252
|
+
#suppressNativeColorClick = false;
|
|
5253
|
+
#pendingFillPickerPointerOpen = false;
|
|
5254
|
+
#nativeColorClickTimer = null;
|
|
5021
5255
|
constructor() {
|
|
5022
5256
|
super();
|
|
5023
5257
|
}
|
|
5024
5258
|
|
|
5025
|
-
get picker() {
|
|
5026
|
-
return this.getAttribute("picker") || "native";
|
|
5027
|
-
}
|
|
5028
|
-
set picker(value) {
|
|
5029
|
-
this.setAttribute("picker", value);
|
|
5030
|
-
}
|
|
5031
|
-
|
|
5032
5259
|
get alpha() {
|
|
5033
5260
|
return this.getAttribute("alpha");
|
|
5034
5261
|
}
|
|
@@ -5040,17 +5267,21 @@ class FigInputColor extends HTMLElement {
|
|
|
5040
5267
|
}
|
|
5041
5268
|
}
|
|
5042
5269
|
|
|
5043
|
-
#
|
|
5270
|
+
#fillPickerAttrs() {
|
|
5044
5271
|
const attrs = {};
|
|
5045
5272
|
const experimental = this.getAttribute("experimental");
|
|
5046
5273
|
if (experimental) attrs["experimental"] = experimental;
|
|
5047
|
-
// picker-* attributes forwarded to fill picker (except anchor, handled programmatically)
|
|
5048
5274
|
for (const { name, value } of this.attributes) {
|
|
5049
5275
|
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
5050
5276
|
attrs[name.slice(7)] = value;
|
|
5051
5277
|
}
|
|
5052
5278
|
}
|
|
5053
5279
|
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
5280
|
+
return attrs;
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
#buildFillPickerAttrs() {
|
|
5284
|
+
const attrs = this.#fillPickerAttrs();
|
|
5054
5285
|
return Object.entries(attrs)
|
|
5055
5286
|
.map(([k, v]) => `${k}="${v}"`)
|
|
5056
5287
|
.join(" ");
|
|
@@ -5069,10 +5300,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5069
5300
|
#buildUI() {
|
|
5070
5301
|
this.#setValues(this.getAttribute("value"));
|
|
5071
5302
|
|
|
5072
|
-
const
|
|
5073
|
-
const hidePicker = this.picker === "false";
|
|
5074
|
-
const showAlpha = this.getAttribute("alpha") === "true";
|
|
5075
|
-
const fpAttrs = this.#buildFillPickerAttrs();
|
|
5303
|
+
const showAlpha = this.getAttribute("alpha") !== "false";
|
|
5076
5304
|
const disabled = this.#disabled;
|
|
5077
5305
|
const disabledAttr = disabled ? " disabled" : "";
|
|
5078
5306
|
|
|
@@ -5097,32 +5325,14 @@ class FigInputColor extends HTMLElement {
|
|
|
5097
5325
|
}
|
|
5098
5326
|
|
|
5099
5327
|
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
|
-
}
|
|
5328
|
+
swatchElement = `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
|
|
5109
5329
|
|
|
5110
5330
|
html = `<div class="input-combo">
|
|
5111
5331
|
${swatchElement}
|
|
5112
5332
|
${label}
|
|
5113
5333
|
</div>`;
|
|
5114
5334
|
} 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
|
-
}
|
|
5335
|
+
html = `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
|
|
5126
5336
|
}
|
|
5127
5337
|
this.innerHTML = html;
|
|
5128
5338
|
|
|
@@ -5143,31 +5353,16 @@ class FigInputColor extends HTMLElement {
|
|
|
5143
5353
|
swatchInput?.setAttribute("disabled", "");
|
|
5144
5354
|
if (swatchInput) swatchInput.style.pointerEvents = "none";
|
|
5145
5355
|
}
|
|
5356
|
+
this.#swatch.addEventListener("pointerdown", this.#handleSwatchPointerDown.bind(this), {
|
|
5357
|
+
capture: true,
|
|
5358
|
+
});
|
|
5359
|
+
this.#swatch.addEventListener("click", this.#handleSwatchClick.bind(this), {
|
|
5360
|
+
capture: true,
|
|
5361
|
+
});
|
|
5362
|
+
swatchInput?.addEventListener("keydown", this.#handleSwatchKeyDown.bind(this));
|
|
5146
5363
|
this.#swatch.addEventListener("input", this.#handleInput.bind(this));
|
|
5147
5364
|
}
|
|
5148
5365
|
|
|
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
5366
|
if (this.#textInput) {
|
|
5172
5367
|
const hex = this.rgbAlphaToHex(this.rgba, 1);
|
|
5173
5368
|
// Display without # prefix
|
|
@@ -5197,8 +5392,103 @@ class FigInputColor extends HTMLElement {
|
|
|
5197
5392
|
}
|
|
5198
5393
|
});
|
|
5199
5394
|
}
|
|
5395
|
+
|
|
5396
|
+
#syncFillPicker() {
|
|
5397
|
+
if (!this.#fillPicker) return;
|
|
5398
|
+
for (const [name, value] of Object.entries(this.#fillPickerAttrs())) {
|
|
5399
|
+
this.#fillPicker.setAttribute(name, value);
|
|
5400
|
+
}
|
|
5401
|
+
this.#fillPicker.setAttribute("mode", "solid");
|
|
5402
|
+
if (this.getAttribute("alpha") !== "false") {
|
|
5403
|
+
this.#fillPicker.removeAttribute("alpha");
|
|
5404
|
+
} else {
|
|
5405
|
+
this.#fillPicker.setAttribute("alpha", "false");
|
|
5406
|
+
}
|
|
5407
|
+
if (this.hasAttribute("disabled")) {
|
|
5408
|
+
this.#fillPicker.setAttribute("disabled", "");
|
|
5409
|
+
} else {
|
|
5410
|
+
this.#fillPicker.removeAttribute("disabled");
|
|
5411
|
+
}
|
|
5412
|
+
this.#fillPicker.anchorElement = this;
|
|
5413
|
+
this.#fillPicker.setAttribute(
|
|
5414
|
+
"value",
|
|
5415
|
+
JSON.stringify({
|
|
5416
|
+
type: "solid",
|
|
5417
|
+
color: this.hexOpaque,
|
|
5418
|
+
opacity: this.#alphaPercent,
|
|
5419
|
+
}),
|
|
5420
|
+
);
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
#ensureFillPicker() {
|
|
5424
|
+
if (!hasFigFillPicker()) return null;
|
|
5425
|
+
if (this.#fillPicker?.isConnected) {
|
|
5426
|
+
this.#syncFillPicker();
|
|
5427
|
+
return this.#fillPicker;
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
const picker = document.createElement("fig-fill-picker");
|
|
5431
|
+
picker.innerHTML = "<span hidden></span>";
|
|
5432
|
+
picker.addEventListener("input", this.#handleFillPickerInput.bind(this));
|
|
5433
|
+
picker.addEventListener("change", this.#handleChange.bind(this));
|
|
5434
|
+
this.appendChild(picker);
|
|
5435
|
+
this.#fillPicker = picker;
|
|
5436
|
+
this.#syncFillPicker();
|
|
5437
|
+
return picker;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
#openFillPicker() {
|
|
5441
|
+
if (this.hasAttribute("disabled") || this.hasAttribute("swatch-disabled")) return false;
|
|
5442
|
+
const picker = this.#ensureFillPicker();
|
|
5443
|
+
if (!picker) return false;
|
|
5444
|
+
requestAnimationFrame(() => picker.open?.());
|
|
5445
|
+
return true;
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
#cancelNativeColorEvent(event) {
|
|
5449
|
+
event.preventDefault();
|
|
5450
|
+
event.stopPropagation();
|
|
5451
|
+
event.stopImmediatePropagation?.();
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
#handleSwatchPointerDown(event) {
|
|
5455
|
+
if (!hasFigFillPicker()) return;
|
|
5456
|
+
if (this.hasAttribute("disabled") || this.hasAttribute("swatch-disabled")) return;
|
|
5457
|
+
this.#pendingFillPickerPointerOpen = true;
|
|
5458
|
+
this.#suppressNativeColorClick = true;
|
|
5459
|
+
if (this.#nativeColorClickTimer) clearTimeout(this.#nativeColorClickTimer);
|
|
5460
|
+
this.#nativeColorClickTimer = setTimeout(() => {
|
|
5461
|
+
this.#suppressNativeColorClick = false;
|
|
5462
|
+
this.#pendingFillPickerPointerOpen = false;
|
|
5463
|
+
this.#nativeColorClickTimer = null;
|
|
5464
|
+
}, 500);
|
|
5465
|
+
this.#cancelNativeColorEvent(event);
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5468
|
+
#handleSwatchClick(event) {
|
|
5469
|
+
if (!this.#suppressNativeColorClick) return;
|
|
5470
|
+
this.#suppressNativeColorClick = false;
|
|
5471
|
+
if (this.#nativeColorClickTimer) {
|
|
5472
|
+
clearTimeout(this.#nativeColorClickTimer);
|
|
5473
|
+
this.#nativeColorClickTimer = null;
|
|
5474
|
+
}
|
|
5475
|
+
this.#cancelNativeColorEvent(event);
|
|
5476
|
+
if (this.#pendingFillPickerPointerOpen) {
|
|
5477
|
+
this.#pendingFillPickerPointerOpen = false;
|
|
5478
|
+
this.#openFillPicker();
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
#handleSwatchKeyDown(event) {
|
|
5483
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
5484
|
+
if (!hasFigFillPicker()) return;
|
|
5485
|
+
if (!this.#openFillPicker()) return;
|
|
5486
|
+
this.#cancelNativeColorEvent(event);
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5200
5489
|
#setValues(hexValue) {
|
|
5201
|
-
|
|
5490
|
+
const colorValue = hexValue || "#D9D9D9";
|
|
5491
|
+
this.rgba = this.convertToRGBA(colorValue);
|
|
5202
5492
|
this.value = this.rgbAlphaToHex(
|
|
5203
5493
|
{
|
|
5204
5494
|
r: isNaN(this.rgba.r) ? 0 : this.rgba.r,
|
|
@@ -5209,9 +5499,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5209
5499
|
);
|
|
5210
5500
|
this.hexWithAlpha = this.value.toUpperCase();
|
|
5211
5501
|
this.hexOpaque = this.hexWithAlpha.slice(0, 7);
|
|
5212
|
-
|
|
5213
|
-
this.#alphaPercent = (this.rgba.a * 100).toFixed(0);
|
|
5214
|
-
}
|
|
5502
|
+
this.#alphaPercent = colorValue.length > 7 ? (this.rgba.a * 100).toFixed(0) : 100;
|
|
5215
5503
|
this.style.setProperty("--alpha", this.rgba.a);
|
|
5216
5504
|
}
|
|
5217
5505
|
|
|
@@ -5335,7 +5623,6 @@ class FigInputColor extends HTMLElement {
|
|
|
5335
5623
|
"value",
|
|
5336
5624
|
"style",
|
|
5337
5625
|
"mode",
|
|
5338
|
-
"picker",
|
|
5339
5626
|
"experimental",
|
|
5340
5627
|
"alpha",
|
|
5341
5628
|
"text",
|
|
@@ -5382,13 +5669,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5382
5669
|
// Emitting here causes infinite loops with React and other frameworks.
|
|
5383
5670
|
break;
|
|
5384
5671
|
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
|
|
5672
|
+
this.#syncFillPicker();
|
|
5392
5673
|
break;
|
|
5393
5674
|
case "alpha":
|
|
5394
5675
|
case "text":
|
|
@@ -5414,8 +5695,7 @@ class FigInputColor extends HTMLElement {
|
|
|
5414
5695
|
else child.removeAttribute("disabled");
|
|
5415
5696
|
}
|
|
5416
5697
|
if (this.#fillPicker) {
|
|
5417
|
-
|
|
5418
|
-
else this.#fillPicker.removeAttribute("disabled");
|
|
5698
|
+
this.#syncFillPicker();
|
|
5419
5699
|
}
|
|
5420
5700
|
}
|
|
5421
5701
|
|
|
@@ -5535,7 +5815,7 @@ const GRADIENT_HUE_INTERPOLATIONS = [
|
|
|
5535
5815
|
|
|
5536
5816
|
const GRADIENT_PICKER_SPACES = ["srgb-linear", "oklab", "oklch"];
|
|
5537
5817
|
|
|
5538
|
-
function normalizeGradientConfig(gradient) {
|
|
5818
|
+
export function normalizeGradientConfig(gradient) {
|
|
5539
5819
|
const next = { ...(gradient ?? {}) };
|
|
5540
5820
|
let interpolationSpace = String(
|
|
5541
5821
|
next.interpolationSpace ?? "oklab",
|
|
@@ -5557,7 +5837,7 @@ function normalizeGradientConfig(gradient) {
|
|
|
5557
5837
|
return next;
|
|
5558
5838
|
}
|
|
5559
5839
|
|
|
5560
|
-
function gradientToValueShape(gradient) {
|
|
5840
|
+
export function gradientToValueShape(gradient) {
|
|
5561
5841
|
const normalized = normalizeGradientConfig(gradient);
|
|
5562
5842
|
const output = {
|
|
5563
5843
|
...normalized,
|
|
@@ -5571,7 +5851,7 @@ function gradientToValueShape(gradient) {
|
|
|
5571
5851
|
return output;
|
|
5572
5852
|
}
|
|
5573
5853
|
|
|
5574
|
-
function gradientInterpolationClause(gradient) {
|
|
5854
|
+
export function gradientInterpolationClause(gradient) {
|
|
5575
5855
|
const normalized = normalizeGradientConfig(gradient);
|
|
5576
5856
|
if (normalized.interpolationSpace === "oklch") {
|
|
5577
5857
|
return `in oklch ${normalized.hueInterpolation} hue`;
|
|
@@ -5900,6 +6180,46 @@ class FigInputFill extends HTMLElement {
|
|
|
5900
6180
|
.join(" ");
|
|
5901
6181
|
}
|
|
5902
6182
|
|
|
6183
|
+
#fillPickerChitBackground() {
|
|
6184
|
+
switch (this.#fillType) {
|
|
6185
|
+
case "solid":
|
|
6186
|
+
return this.#solid.color;
|
|
6187
|
+
case "gradient": {
|
|
6188
|
+
const sorted = [...this.#gradient.stops].sort(
|
|
6189
|
+
(a, b) => a.position - b.position,
|
|
6190
|
+
);
|
|
6191
|
+
const stops = sorted
|
|
6192
|
+
.map((stop) => {
|
|
6193
|
+
const alpha = (stop.opacity ?? 100) / 100;
|
|
6194
|
+
if (alpha >= 1) return `${stop.color} ${stop.position}%`;
|
|
6195
|
+
const { r, g, b } = figHexToRGB(stop.color);
|
|
6196
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha}) ${stop.position}%`;
|
|
6197
|
+
})
|
|
6198
|
+
.join(", ");
|
|
6199
|
+
return `linear-gradient(${this.#gradient.angle}deg ${gradientInterpolationClause(this.#gradient)}, ${stops})`;
|
|
6200
|
+
}
|
|
6201
|
+
case "image":
|
|
6202
|
+
return this.#image.url ? `url(${this.#image.url})` : "#D9D9D9";
|
|
6203
|
+
default:
|
|
6204
|
+
return "#D9D9D9";
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
#fillPickerChitAlpha() {
|
|
6209
|
+
switch (this.#fillType) {
|
|
6210
|
+
case "solid":
|
|
6211
|
+
return this.#solid.alpha;
|
|
6212
|
+
case "image":
|
|
6213
|
+
return this.#image.opacity ?? 1;
|
|
6214
|
+
case "video":
|
|
6215
|
+
return this.#video.opacity ?? 1;
|
|
6216
|
+
case "webcam":
|
|
6217
|
+
return this.#webcam.opacity ?? 1;
|
|
6218
|
+
default:
|
|
6219
|
+
return 1;
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
|
|
5903
6223
|
#syncDisabled() {
|
|
5904
6224
|
const disabled = this.hasAttribute("disabled");
|
|
5905
6225
|
for (const child of [
|
|
@@ -5982,7 +6302,9 @@ class FigInputFill extends HTMLElement {
|
|
|
5982
6302
|
<div class="input-combo">
|
|
5983
6303
|
<fig-fill-picker ${fpAttrs} value='${fillPickerValue}' ${
|
|
5984
6304
|
disabled ? "disabled" : ""
|
|
5985
|
-
}
|
|
6305
|
+
}>
|
|
6306
|
+
<fig-chit background="${this.#fillPickerChitBackground()}" alpha="${this.#fillPickerChitAlpha()}"${disabled ? " disabled" : ""}></fig-chit>
|
|
6307
|
+
</fig-fill-picker>
|
|
5986
6308
|
${controlsHtml}
|
|
5987
6309
|
</div>`;
|
|
5988
6310
|
|
|
@@ -7097,7 +7419,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
7097
7419
|
const disabled = this.hasAttribute("disabled");
|
|
7098
7420
|
const mode = this.#editMode;
|
|
7099
7421
|
|
|
7100
|
-
if (mode === "picker") {
|
|
7422
|
+
if (mode === "picker" && hasFigFillPicker()) {
|
|
7101
7423
|
const experimental = this.getAttribute("experimental");
|
|
7102
7424
|
const expAttr = experimental ? ` experimental="${experimental}"` : "";
|
|
7103
7425
|
const gradientValue = JSON.stringify(this.value);
|
|
@@ -7113,11 +7435,11 @@ class FigInputGradient extends HTMLElement {
|
|
|
7113
7435
|
|
|
7114
7436
|
this.innerHTML = `
|
|
7115
7437
|
<fig-chit background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
|
|
7116
|
-
${mode === "true" ? `<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>` : ""}`;
|
|
7438
|
+
${mode === "true" || mode === "picker" ? `<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>` : ""}`;
|
|
7117
7439
|
this.#chit = this.querySelector("fig-chit");
|
|
7118
7440
|
this.#track = this.querySelector(".fig-input-gradient-track");
|
|
7119
7441
|
|
|
7120
|
-
if (mode === "true") {
|
|
7442
|
+
if (mode === "true" || mode === "picker") {
|
|
7121
7443
|
this.#setupGhostHandle();
|
|
7122
7444
|
this.#setupEventListeners();
|
|
7123
7445
|
requestAnimationFrame(() => this.#repositionHandles());
|
|
@@ -8450,6 +8772,7 @@ class FigMedia extends HTMLElement {
|
|
|
8450
8772
|
#mediaEl = null;
|
|
8451
8773
|
#fileInput = null;
|
|
8452
8774
|
#blobUrl = null;
|
|
8775
|
+
#previewSrc = null;
|
|
8453
8776
|
#file = null;
|
|
8454
8777
|
#boundHandleFileInput = this.#handleFileInput.bind(this);
|
|
8455
8778
|
#boundHandleMediaPlay = this.#handleMediaPlay.bind(this);
|
|
@@ -8502,6 +8825,10 @@ class FigMedia extends HTMLElement {
|
|
|
8502
8825
|
return this.#file;
|
|
8503
8826
|
}
|
|
8504
8827
|
|
|
8828
|
+
#currentMediaSrc() {
|
|
8829
|
+
return this.#previewSrc || this.#src || "";
|
|
8830
|
+
}
|
|
8831
|
+
|
|
8505
8832
|
/**
|
|
8506
8833
|
* Returns a base64 data URL for the loaded image.
|
|
8507
8834
|
* Requires a CORS-clean image (same-origin or with appropriate Access-Control headers);
|
|
@@ -8509,7 +8836,7 @@ class FigMedia extends HTMLElement {
|
|
|
8509
8836
|
*/
|
|
8510
8837
|
async getBase64() {
|
|
8511
8838
|
if (this.mediaKind !== "image") return null;
|
|
8512
|
-
if (!this.#
|
|
8839
|
+
if (!this.#currentMediaSrc()) return null;
|
|
8513
8840
|
if (!this.#mediaEl) return null;
|
|
8514
8841
|
try {
|
|
8515
8842
|
if (typeof this.#mediaEl.decode === "function") {
|
|
@@ -8565,6 +8892,7 @@ class FigMedia extends HTMLElement {
|
|
|
8565
8892
|
URL.revokeObjectURL(this.#blobUrl);
|
|
8566
8893
|
this.#blobUrl = null;
|
|
8567
8894
|
}
|
|
8895
|
+
this.#previewSrc = null;
|
|
8568
8896
|
}
|
|
8569
8897
|
|
|
8570
8898
|
#removeMediaElementListeners() {
|
|
@@ -8642,7 +8970,7 @@ class FigMedia extends HTMLElement {
|
|
|
8642
8970
|
#syncGeneratedMediaElement() {
|
|
8643
8971
|
if (!this.#mediaEl) return;
|
|
8644
8972
|
if (!this.#mediaEl.hasAttribute("data-generated")) return;
|
|
8645
|
-
const src = this.#
|
|
8973
|
+
const src = this.#currentMediaSrc();
|
|
8646
8974
|
if (this.#mediaEl.getAttribute("src") !== src) {
|
|
8647
8975
|
if (src) {
|
|
8648
8976
|
this.#mediaEl.setAttribute("src", src);
|
|
@@ -8827,7 +9155,11 @@ class FigMedia extends HTMLElement {
|
|
|
8827
9155
|
fi.setAttribute("variant", "overlay");
|
|
8828
9156
|
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8829
9157
|
fi.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8830
|
-
if (this.#
|
|
9158
|
+
if (this.#file?.name) {
|
|
9159
|
+
fi.setAttribute("filename", this.#file.name);
|
|
9160
|
+
} else if (this.#src) {
|
|
9161
|
+
fi.setAttribute("url", this.#src);
|
|
9162
|
+
}
|
|
8831
9163
|
fi.addEventListener("change", this.#boundHandleFileInput);
|
|
8832
9164
|
this.append(fi);
|
|
8833
9165
|
this.#fileInput = fi;
|
|
@@ -8844,6 +9176,7 @@ class FigMedia extends HTMLElement {
|
|
|
8844
9176
|
#handleFileInput(e) {
|
|
8845
9177
|
if (e.target !== this.#fileInput) return;
|
|
8846
9178
|
const file = e.detail?.files?.[0];
|
|
9179
|
+
const cleared = e.detail?.cleared === true;
|
|
8847
9180
|
|
|
8848
9181
|
if (!file) {
|
|
8849
9182
|
if (this.#blobUrl) {
|
|
@@ -8851,7 +9184,19 @@ class FigMedia extends HTMLElement {
|
|
|
8851
9184
|
this.#blobUrl = null;
|
|
8852
9185
|
}
|
|
8853
9186
|
this.#file = null;
|
|
8854
|
-
this
|
|
9187
|
+
this.#previewSrc = null;
|
|
9188
|
+
if (cleared) this.src = "";
|
|
9189
|
+
this.#syncGeneratedMediaElement();
|
|
9190
|
+
if (this.#fileInput) {
|
|
9191
|
+
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
9192
|
+
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
9193
|
+
this.#fileInput.removeAttribute("filename");
|
|
9194
|
+
if (this.#src) {
|
|
9195
|
+
this.#fileInput.setAttribute("url", this.#src);
|
|
9196
|
+
} else {
|
|
9197
|
+
this.#fileInput.removeAttribute("url");
|
|
9198
|
+
}
|
|
9199
|
+
}
|
|
8855
9200
|
this.dispatchEvent(
|
|
8856
9201
|
new CustomEvent("change", { bubbles: true, cancelable: true }),
|
|
8857
9202
|
);
|
|
@@ -8863,8 +9208,9 @@ class FigMedia extends HTMLElement {
|
|
|
8863
9208
|
}
|
|
8864
9209
|
this.#file = file;
|
|
8865
9210
|
this.#blobUrl = URL.createObjectURL(file);
|
|
9211
|
+
this.#previewSrc = this.#blobUrl;
|
|
8866
9212
|
|
|
8867
|
-
this
|
|
9213
|
+
this.#syncGeneratedMediaElement();
|
|
8868
9214
|
|
|
8869
9215
|
this.dispatchEvent(
|
|
8870
9216
|
new CustomEvent("loaded", {
|
|
@@ -8878,9 +9224,8 @@ class FigMedia extends HTMLElement {
|
|
|
8878
9224
|
);
|
|
8879
9225
|
|
|
8880
9226
|
if (this.#fileInput) {
|
|
8881
|
-
this.#fileInput.
|
|
8882
|
-
this.#fileInput.
|
|
8883
|
-
this.#fileInput.addEventListener("change", this.#boundHandleFileInput);
|
|
9227
|
+
this.#fileInput.removeAttribute("url");
|
|
9228
|
+
this.#fileInput.setAttribute("filename", file.name);
|
|
8884
9229
|
this.#fileInput.setAttribute("label", "Replace");
|
|
8885
9230
|
}
|
|
8886
9231
|
}
|
|
@@ -8917,10 +9262,14 @@ class FigMedia extends HTMLElement {
|
|
|
8917
9262
|
if (oldValue === newValue) return;
|
|
8918
9263
|
|
|
8919
9264
|
if (name === "src") {
|
|
8920
|
-
|
|
8921
|
-
|
|
9265
|
+
const nextSrc = newValue || "";
|
|
9266
|
+
const isCurrentPreviewBlob =
|
|
9267
|
+
nextSrc && (nextSrc === this.#previewSrc || nextSrc === this.#blobUrl);
|
|
9268
|
+
this.#src = nextSrc;
|
|
9269
|
+
if (this.#blobUrl && !isCurrentPreviewBlob) {
|
|
8922
9270
|
URL.revokeObjectURL(this.#blobUrl);
|
|
8923
9271
|
this.#blobUrl = null;
|
|
9272
|
+
this.#previewSrc = null;
|
|
8924
9273
|
this.#file = null;
|
|
8925
9274
|
}
|
|
8926
9275
|
this.#syncGeneratedMediaElement();
|
|
@@ -8928,9 +9277,16 @@ class FigMedia extends HTMLElement {
|
|
|
8928
9277
|
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8929
9278
|
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8930
9279
|
if (this.#src) {
|
|
8931
|
-
|
|
9280
|
+
if (this.#file?.name) {
|
|
9281
|
+
this.#fileInput.removeAttribute("url");
|
|
9282
|
+
this.#fileInput.setAttribute("filename", this.#file.name);
|
|
9283
|
+
} else {
|
|
9284
|
+
this.#fileInput.setAttribute("url", this.#src);
|
|
9285
|
+
this.#fileInput.removeAttribute("filename");
|
|
9286
|
+
}
|
|
8932
9287
|
} else {
|
|
8933
9288
|
this.#fileInput.removeAttribute("url");
|
|
9289
|
+
this.#fileInput.removeAttribute("filename");
|
|
8934
9290
|
}
|
|
8935
9291
|
}
|
|
8936
9292
|
}
|
|
@@ -9101,7 +9457,7 @@ class FigMediaControls extends HTMLElement {
|
|
|
9101
9457
|
});
|
|
9102
9458
|
|
|
9103
9459
|
const slider = document.createElement("fig-slider");
|
|
9104
|
-
slider.setAttribute("
|
|
9460
|
+
slider.setAttribute("text", "false");
|
|
9105
9461
|
slider.setAttribute("min", "0");
|
|
9106
9462
|
slider.setAttribute("max", String(this.duration));
|
|
9107
9463
|
slider.setAttribute("step", "0.1");
|
|
@@ -9197,7 +9553,7 @@ customElements.define("fig-media-controls", FigMediaControls);
|
|
|
9197
9553
|
|
|
9198
9554
|
/* File Upload Input */
|
|
9199
9555
|
class FigInputFile extends HTMLElement {
|
|
9200
|
-
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url"];
|
|
9556
|
+
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url", "filename"];
|
|
9201
9557
|
|
|
9202
9558
|
#fileInput = null;
|
|
9203
9559
|
#filenameEl = null;
|
|
@@ -9211,6 +9567,8 @@ class FigInputFile extends HTMLElement {
|
|
|
9211
9567
|
}
|
|
9212
9568
|
|
|
9213
9569
|
get #urlFilename() {
|
|
9570
|
+
const filename = this.getAttribute("filename");
|
|
9571
|
+
if (filename) return filename;
|
|
9214
9572
|
const url = this.getAttribute("url");
|
|
9215
9573
|
if (!url) return "";
|
|
9216
9574
|
try {
|
|
@@ -9254,12 +9612,13 @@ class FigInputFile extends HTMLElement {
|
|
|
9254
9612
|
this.#files = null;
|
|
9255
9613
|
if (this.#fileInput) this.#fileInput.value = "";
|
|
9256
9614
|
this.removeAttribute("url");
|
|
9615
|
+
this.removeAttribute("filename");
|
|
9257
9616
|
this.#render();
|
|
9258
|
-
this.#emitEvents();
|
|
9617
|
+
this.#emitEvents({ cleared: true });
|
|
9259
9618
|
}
|
|
9260
9619
|
|
|
9261
|
-
#emitEvents() {
|
|
9262
|
-
const detail = { files: this.#files };
|
|
9620
|
+
#emitEvents(extraDetail = {}) {
|
|
9621
|
+
const detail = { files: this.#files, ...extraDetail };
|
|
9263
9622
|
this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
|
|
9264
9623
|
this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
|
|
9265
9624
|
}
|
|
@@ -9349,7 +9708,10 @@ class FigInputFile extends HTMLElement {
|
|
|
9349
9708
|
this.getAttribute("disabled") !== "false";
|
|
9350
9709
|
const multiple = this.hasAttribute("multiple");
|
|
9351
9710
|
const variant = this.getAttribute("variant") || "input";
|
|
9352
|
-
const hasFile =
|
|
9711
|
+
const hasFile =
|
|
9712
|
+
(this.#files && this.#files.length > 0) ||
|
|
9713
|
+
!!this.getAttribute("url") ||
|
|
9714
|
+
!!this.getAttribute("filename");
|
|
9353
9715
|
|
|
9354
9716
|
this.innerHTML = "";
|
|
9355
9717
|
|
|
@@ -9395,7 +9757,7 @@ class FigInputFile extends HTMLElement {
|
|
|
9395
9757
|
const clearTooltip = document.createElement("fig-tooltip");
|
|
9396
9758
|
clearTooltip.setAttribute("text", "Remove");
|
|
9397
9759
|
this.#clearBtn = document.createElement("fig-button");
|
|
9398
|
-
this.#clearBtn.setAttribute("variant", "ghost");
|
|
9760
|
+
this.#clearBtn.setAttribute("variant", variant === "overlay" ? "overlay" : "ghost");
|
|
9399
9761
|
this.#clearBtn.setAttribute("icon", "true");
|
|
9400
9762
|
this.#clearBtn.className = "fig-input-file-clear";
|
|
9401
9763
|
if (disabled) this.#clearBtn.setAttribute("disabled", "");
|
|
@@ -9445,7 +9807,7 @@ customElements.define("fig-input-file", FigInputFile);
|
|
|
9445
9807
|
* A bezier / spring easing curve editor with draggable control points.
|
|
9446
9808
|
* @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
|
|
9447
9809
|
* @attr {number} precision - Decimal places for output values (default 2)
|
|
9448
|
-
* @attr {boolean}
|
|
9810
|
+
* @attr {boolean} edit - Show the editor and custom preset options (default true; set "false" for presets only)
|
|
9449
9811
|
*/
|
|
9450
9812
|
class FigEasingCurve extends HTMLElement {
|
|
9451
9813
|
#cp1 = { x: 0.42, y: 0 };
|
|
@@ -9463,6 +9825,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9463
9825
|
#bezierEndpointStart = null;
|
|
9464
9826
|
#bezierEndpointEnd = null;
|
|
9465
9827
|
#dropdown = null;
|
|
9828
|
+
#valueInput = null;
|
|
9466
9829
|
#presetName = null;
|
|
9467
9830
|
#targetLine = null;
|
|
9468
9831
|
#springDuration = 0.8;
|
|
@@ -9544,7 +9907,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9544
9907
|
];
|
|
9545
9908
|
|
|
9546
9909
|
static get observedAttributes() {
|
|
9547
|
-
return ["value", "precision", "aspect-ratio"];
|
|
9910
|
+
return ["value", "precision", "aspect-ratio", "edit"];
|
|
9548
9911
|
}
|
|
9549
9912
|
|
|
9550
9913
|
connectedCallback() {
|
|
@@ -9566,6 +9929,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9566
9929
|
}
|
|
9567
9930
|
|
|
9568
9931
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
9932
|
+
if (oldValue === newValue) return;
|
|
9933
|
+
|
|
9569
9934
|
if (name === "aspect-ratio") {
|
|
9570
9935
|
figSyncCssVar(this, "--aspect-ratio", newValue);
|
|
9571
9936
|
if (this.#svg) {
|
|
@@ -9575,16 +9940,21 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9575
9940
|
return;
|
|
9576
9941
|
}
|
|
9577
9942
|
|
|
9578
|
-
if (
|
|
9943
|
+
if (name === "edit") {
|
|
9944
|
+
if (this.isConnected) this.#render();
|
|
9945
|
+
return;
|
|
9946
|
+
}
|
|
9947
|
+
|
|
9579
9948
|
if (name === "value" && newValue) {
|
|
9580
9949
|
const prevMode = this.#mode;
|
|
9581
9950
|
this.#parseValue(newValue);
|
|
9582
9951
|
this.#presetName = this.#matchPreset();
|
|
9583
|
-
if (prevMode !== this.#mode) {
|
|
9952
|
+
if (prevMode !== this.#mode && this.#isEditEnabled()) {
|
|
9584
9953
|
this.#render();
|
|
9585
9954
|
} else {
|
|
9586
|
-
this.#updatePaths();
|
|
9955
|
+
if (this.#svg) this.#updatePaths();
|
|
9587
9956
|
this.#syncDropdown();
|
|
9957
|
+
this.#syncValueInput();
|
|
9588
9958
|
}
|
|
9589
9959
|
}
|
|
9590
9960
|
if (name === "precision") {
|
|
@@ -9635,7 +10005,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9635
10005
|
this.#spring.stiffness = parseFloat(springMatch[1]);
|
|
9636
10006
|
this.#spring.damping = parseFloat(springMatch[2]);
|
|
9637
10007
|
this.#spring.mass = parseFloat(springMatch[3]);
|
|
9638
|
-
return;
|
|
10008
|
+
return true;
|
|
9639
10009
|
}
|
|
9640
10010
|
const parts = str.split(",").map((s) => parseFloat(s.trim()));
|
|
9641
10011
|
if (parts.length >= 4 && parts.every((n) => !isNaN(n))) {
|
|
@@ -9644,7 +10014,9 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9644
10014
|
this.#cp1.y = parts[1];
|
|
9645
10015
|
this.#cp2.x = parts[2];
|
|
9646
10016
|
this.#cp2.y = parts[3];
|
|
10017
|
+
return true;
|
|
9647
10018
|
}
|
|
10019
|
+
return false;
|
|
9648
10020
|
}
|
|
9649
10021
|
|
|
9650
10022
|
#matchPreset() {
|
|
@@ -9777,23 +10149,38 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9777
10149
|
|
|
9778
10150
|
// --- Rendering ---
|
|
9779
10151
|
|
|
10152
|
+
#isEditEnabled() {
|
|
10153
|
+
return this.getAttribute("edit") !== "false";
|
|
10154
|
+
}
|
|
10155
|
+
|
|
9780
10156
|
#render() {
|
|
9781
10157
|
this.classList.toggle("spring-mode", this.#mode === "spring");
|
|
9782
10158
|
this.classList.toggle("bezier-mode", this.#mode !== "spring");
|
|
9783
10159
|
this.#syncMetricsFromCSS();
|
|
9784
10160
|
this.innerHTML = this.#getInnerHTML();
|
|
9785
10161
|
this.#cacheRefs();
|
|
9786
|
-
this.#
|
|
9787
|
-
|
|
9788
|
-
|
|
10162
|
+
if (this.#svg) {
|
|
10163
|
+
this.#syncHandleSizes();
|
|
10164
|
+
this.#syncViewportSize();
|
|
10165
|
+
this.#updatePaths();
|
|
10166
|
+
}
|
|
10167
|
+
this.#syncValueInput();
|
|
9789
10168
|
this.#setupEvents();
|
|
9790
10169
|
}
|
|
9791
10170
|
|
|
10171
|
+
static #escapeAttribute(value) {
|
|
10172
|
+
return String(value)
|
|
10173
|
+
.replace(/&/g, "&")
|
|
10174
|
+
.replace(/"/g, """)
|
|
10175
|
+
.replace(/</g, "<")
|
|
10176
|
+
.replace(/>/g, ">");
|
|
10177
|
+
}
|
|
10178
|
+
|
|
9792
10179
|
#getDropdownHTML() {
|
|
9793
|
-
if (this.getAttribute("dropdown") !== "true") return "";
|
|
9794
10180
|
let optionsHTML = "";
|
|
9795
10181
|
let currentGroup = undefined;
|
|
9796
10182
|
for (const p of FigEasingCurve.PRESETS) {
|
|
10183
|
+
if (!this.#isEditEnabled() && !p.value && !p.spring) continue;
|
|
9797
10184
|
if (p.group !== currentGroup) {
|
|
9798
10185
|
if (currentGroup !== undefined) optionsHTML += `</optgroup>`;
|
|
9799
10186
|
if (p.group) optionsHTML += `<optgroup label="${p.group}">`;
|
|
@@ -9822,6 +10209,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9822
10209
|
#getInnerHTML() {
|
|
9823
10210
|
const size = 200;
|
|
9824
10211
|
const dropdown = this.#getDropdownHTML();
|
|
10212
|
+
if (!this.#isEditEnabled()) return dropdown;
|
|
10213
|
+
const valueInput = `<fig-input-text class="fig-easing-curve-value-input" value="${FigEasingCurve.#escapeAttribute(this.value)}" full></fig-input-text>`;
|
|
9825
10214
|
|
|
9826
10215
|
if (this.#mode === "spring") {
|
|
9827
10216
|
const targetY = 40;
|
|
@@ -9833,7 +10222,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9833
10222
|
<path class="fig-easing-curve-path"/>
|
|
9834
10223
|
<foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9835
10224
|
<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
|
|
10225
|
+
</svg></div>${valueInput}`;
|
|
9837
10226
|
}
|
|
9838
10227
|
|
|
9839
10228
|
return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
@@ -9846,7 +10235,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9846
10235
|
<circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
|
|
9847
10236
|
<foreignObject class="fig-easing-curve-handle" data-handle="1" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9848
10237
|
<foreignObject class="fig-easing-curve-handle" data-handle="2" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
|
|
9849
|
-
</svg></div
|
|
10238
|
+
</svg></div>${valueInput}`;
|
|
9850
10239
|
}
|
|
9851
10240
|
|
|
9852
10241
|
#readCssNumber(name, fallback) {
|
|
@@ -9881,6 +10270,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9881
10270
|
this.#bezierEndpointStart = this.querySelector('[data-endpoint="start"]');
|
|
9882
10271
|
this.#bezierEndpointEnd = this.querySelector('[data-endpoint="end"]');
|
|
9883
10272
|
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
10273
|
+
this.#valueInput = this.querySelector(".fig-easing-curve-value-input");
|
|
9884
10274
|
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
9885
10275
|
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
9886
10276
|
this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
|
|
@@ -9964,6 +10354,17 @@ class FigEasingCurve extends HTMLElement {
|
|
|
9964
10354
|
}
|
|
9965
10355
|
}
|
|
9966
10356
|
|
|
10357
|
+
#syncActiveBezierArm() {
|
|
10358
|
+
this.#line1?.classList.toggle(
|
|
10359
|
+
"is-active",
|
|
10360
|
+
this.#mode === "bezier" && this.#isDragging === 1,
|
|
10361
|
+
);
|
|
10362
|
+
this.#line2?.classList.toggle(
|
|
10363
|
+
"is-active",
|
|
10364
|
+
this.#mode === "bezier" && this.#isDragging === 2,
|
|
10365
|
+
);
|
|
10366
|
+
}
|
|
10367
|
+
|
|
9967
10368
|
#updateBezierPaths() {
|
|
9968
10369
|
if (this.#bounds) {
|
|
9969
10370
|
this.#bounds.setAttribute("x", "0");
|
|
@@ -10096,6 +10497,47 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10096
10497
|
this.#refreshCustomPresetIcons();
|
|
10097
10498
|
}
|
|
10098
10499
|
|
|
10500
|
+
#syncValueInput() {
|
|
10501
|
+
if (!this.#valueInput) return;
|
|
10502
|
+
this.#valueInput.setAttribute("value", this.value);
|
|
10503
|
+
}
|
|
10504
|
+
|
|
10505
|
+
#parseManualBezierValue(value) {
|
|
10506
|
+
const parts = value.split(",").map((part) => Number.parseFloat(part.trim()));
|
|
10507
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
10508
|
+
return null;
|
|
10509
|
+
}
|
|
10510
|
+
if (parts[0] < 0 || parts[0] > 1 || parts[2] < 0 || parts[2] > 1) {
|
|
10511
|
+
return null;
|
|
10512
|
+
}
|
|
10513
|
+
return parts;
|
|
10514
|
+
}
|
|
10515
|
+
|
|
10516
|
+
#applyManualValue(value, eventType) {
|
|
10517
|
+
const parts = this.#parseManualBezierValue(value);
|
|
10518
|
+
if (!parts) {
|
|
10519
|
+
if (eventType === "change") this.#syncValueInput();
|
|
10520
|
+
return;
|
|
10521
|
+
}
|
|
10522
|
+
|
|
10523
|
+
const prevMode = this.#mode;
|
|
10524
|
+
this.#mode = "bezier";
|
|
10525
|
+
this.#cp1.x = parts[0];
|
|
10526
|
+
this.#cp1.y = parts[1];
|
|
10527
|
+
this.#cp2.x = parts[2];
|
|
10528
|
+
this.#cp2.y = parts[3];
|
|
10529
|
+
|
|
10530
|
+
this.#presetName = this.#matchPreset();
|
|
10531
|
+
if (prevMode !== this.#mode) {
|
|
10532
|
+
this.#render();
|
|
10533
|
+
} else {
|
|
10534
|
+
this.#updatePaths();
|
|
10535
|
+
this.#syncDropdown();
|
|
10536
|
+
if (eventType === "change") this.#syncValueInput();
|
|
10537
|
+
}
|
|
10538
|
+
this.#emit(eventType);
|
|
10539
|
+
}
|
|
10540
|
+
|
|
10099
10541
|
#setOptionIconByValue(root, optionValue, icon) {
|
|
10100
10542
|
if (!root) return;
|
|
10101
10543
|
for (const option of root.querySelectorAll("option")) {
|
|
@@ -10107,6 +10549,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10107
10549
|
|
|
10108
10550
|
#refreshCustomPresetIcons() {
|
|
10109
10551
|
if (!this.#dropdown) return;
|
|
10552
|
+
if (!this.#isEditEnabled()) return;
|
|
10110
10553
|
const bezierIcon = FigEasingCurve.curveIcon(
|
|
10111
10554
|
this.#cp1.x,
|
|
10112
10555
|
this.#cp1.y,
|
|
@@ -10147,7 +10590,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10147
10590
|
}
|
|
10148
10591
|
|
|
10149
10592
|
#setupEvents() {
|
|
10150
|
-
if (this.#mode === "bezier") {
|
|
10593
|
+
if (this.#svg && this.#mode === "bezier") {
|
|
10151
10594
|
this.#handle1.addEventListener("pointerdown", (e) =>
|
|
10152
10595
|
this.#startBezierDrag(e, 1),
|
|
10153
10596
|
);
|
|
@@ -10165,7 +10608,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10165
10608
|
this.#startBezierDrag(e, this.#bezierHandleForClientHalf(e));
|
|
10166
10609
|
});
|
|
10167
10610
|
}
|
|
10168
|
-
} else {
|
|
10611
|
+
} else if (this.#svg) {
|
|
10169
10612
|
this.#handle1.addEventListener("pointerdown", (e) => {
|
|
10170
10613
|
e.stopPropagation();
|
|
10171
10614
|
this.#startSpringDrag(e, "bounce");
|
|
@@ -10204,8 +10647,9 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10204
10647
|
if (this.#mode !== "bezier") {
|
|
10205
10648
|
this.#mode = "bezier";
|
|
10206
10649
|
this.#render();
|
|
10207
|
-
} else {
|
|
10650
|
+
} else if (this.#svg) {
|
|
10208
10651
|
this.#updatePaths();
|
|
10652
|
+
this.#syncValueInput();
|
|
10209
10653
|
}
|
|
10210
10654
|
} else if (preset.type === "spring") {
|
|
10211
10655
|
if (preset.spring) {
|
|
@@ -10215,14 +10659,30 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10215
10659
|
if (this.#mode !== "spring") {
|
|
10216
10660
|
this.#mode = "spring";
|
|
10217
10661
|
this.#render();
|
|
10218
|
-
} else {
|
|
10662
|
+
} else if (this.#svg) {
|
|
10219
10663
|
this.#updatePaths();
|
|
10664
|
+
this.#syncValueInput();
|
|
10220
10665
|
}
|
|
10221
10666
|
}
|
|
10222
10667
|
this.#emit("input");
|
|
10223
10668
|
this.#emit("change");
|
|
10224
10669
|
});
|
|
10225
10670
|
}
|
|
10671
|
+
|
|
10672
|
+
if (this.#valueInput) {
|
|
10673
|
+
this.#valueInput.addEventListener("input", (e) => {
|
|
10674
|
+
e.stopPropagation();
|
|
10675
|
+
const value = e.detail ?? e.target?.value;
|
|
10676
|
+
if (typeof value !== "string") return;
|
|
10677
|
+
this.#applyManualValue(value, "input");
|
|
10678
|
+
});
|
|
10679
|
+
this.#valueInput.addEventListener("change", (e) => {
|
|
10680
|
+
e.stopPropagation();
|
|
10681
|
+
const value = e.detail ?? e.target?.value;
|
|
10682
|
+
if (typeof value !== "string") return;
|
|
10683
|
+
this.#applyManualValue(value, "change");
|
|
10684
|
+
});
|
|
10685
|
+
}
|
|
10226
10686
|
}
|
|
10227
10687
|
|
|
10228
10688
|
#clientToSVG(e) {
|
|
@@ -10243,6 +10703,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10243
10703
|
#startBezierDrag(e, handle) {
|
|
10244
10704
|
e.preventDefault();
|
|
10245
10705
|
this.#isDragging = handle;
|
|
10706
|
+
this.#syncActiveBezierArm();
|
|
10246
10707
|
|
|
10247
10708
|
const onMove = (e) => {
|
|
10248
10709
|
if (!this.#isDragging) return;
|
|
@@ -10263,11 +10724,13 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10263
10724
|
this.#updatePaths();
|
|
10264
10725
|
this.#presetName = this.#matchPreset();
|
|
10265
10726
|
this.#syncDropdown();
|
|
10727
|
+
this.#syncValueInput();
|
|
10266
10728
|
this.#emit("input");
|
|
10267
10729
|
};
|
|
10268
10730
|
|
|
10269
10731
|
const onUp = () => {
|
|
10270
10732
|
this.#isDragging = null;
|
|
10733
|
+
this.#syncActiveBezierArm();
|
|
10271
10734
|
document.removeEventListener("pointermove", onMove);
|
|
10272
10735
|
document.removeEventListener("pointerup", onUp);
|
|
10273
10736
|
this.#emit("change");
|
|
@@ -10311,6 +10774,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
10311
10774
|
this.#updatePaths();
|
|
10312
10775
|
this.#presetName = this.#matchPreset();
|
|
10313
10776
|
this.#syncDropdown();
|
|
10777
|
+
this.#syncValueInput();
|
|
10314
10778
|
this.#emit("input");
|
|
10315
10779
|
};
|
|
10316
10780
|
|
|
@@ -11818,11 +12282,34 @@ customElements.define("fig-spinner", FigSpinner);
|
|
|
11818
12282
|
/**
|
|
11819
12283
|
* A styled visual preview layer for arbitrary content such as images, canvas,
|
|
11820
12284
|
* video, SVG, or custom rendered surfaces.
|
|
12285
|
+
* @attr {string} fit - CSS object-fit value for direct media children
|
|
11821
12286
|
*/
|
|
11822
|
-
class FigPreview extends HTMLElement {
|
|
12287
|
+
class FigPreview extends HTMLElement {
|
|
12288
|
+
static get observedAttributes() {
|
|
12289
|
+
return ["fit"];
|
|
12290
|
+
}
|
|
12291
|
+
|
|
12292
|
+
connectedCallback() {
|
|
12293
|
+
this.#syncFit();
|
|
12294
|
+
}
|
|
12295
|
+
|
|
12296
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
12297
|
+
if (oldValue === newValue) return;
|
|
12298
|
+
if (name === "fit") this.#syncFit();
|
|
12299
|
+
}
|
|
12300
|
+
|
|
12301
|
+
#syncFit() {
|
|
12302
|
+
const fit = this.getAttribute("fit");
|
|
12303
|
+
if (fit) {
|
|
12304
|
+
this.style.setProperty("--fig-preview-fit", fit);
|
|
12305
|
+
} else {
|
|
12306
|
+
this.style.removeProperty("--fig-preview-fit");
|
|
12307
|
+
}
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
11823
12310
|
customElements.define("fig-preview", FigPreview);
|
|
11824
12311
|
|
|
11825
|
-
/** @type {Record<string, string>} */
|
|
12312
|
+
/** @type {Record<string, string | { medium: string, small: string }>} */
|
|
11826
12313
|
const FIG_ICON_TOKENS = {
|
|
11827
12314
|
chevron: "--icon-16-chevron",
|
|
11828
12315
|
checkmark: "--icon-16-checkmark",
|
|
@@ -11834,16 +12321,25 @@ const FIG_ICON_TOKENS = {
|
|
|
11834
12321
|
minus: "--icon-24-minus",
|
|
11835
12322
|
back: "--icon-24-back",
|
|
11836
12323
|
forward: "--icon-24-forward",
|
|
11837
|
-
close: "--icon-24-close",
|
|
12324
|
+
close: { medium: "--icon-24-close", small: "--icon-16-close" },
|
|
11838
12325
|
rotate: "--icon-24-rotate",
|
|
11839
12326
|
swap: "--icon-24-swap",
|
|
11840
12327
|
play: "--icon-24-play",
|
|
11841
12328
|
pause: "--icon-24-pause",
|
|
12329
|
+
search: "--icon-24-search",
|
|
12330
|
+
visible: { medium: "--icon-24-visible", small: "--icon-16-visible" },
|
|
12331
|
+
hidden: { medium: "--icon-24-hidden", small: "--icon-16-hidden" },
|
|
11842
12332
|
};
|
|
11843
12333
|
|
|
11844
|
-
function figIconCssVar(name) {
|
|
12334
|
+
function figIconCssVar(name, size = "medium") {
|
|
11845
12335
|
const token = name && FIG_ICON_TOKENS[name];
|
|
11846
|
-
|
|
12336
|
+
if (!token) return "";
|
|
12337
|
+
|
|
12338
|
+
const tokenName =
|
|
12339
|
+
typeof token === "string"
|
|
12340
|
+
? token
|
|
12341
|
+
: token[size === "small" ? "small" : "medium"];
|
|
12342
|
+
return tokenName ? `var(${tokenName})` : "";
|
|
11847
12343
|
}
|
|
11848
12344
|
|
|
11849
12345
|
/**
|
|
@@ -11867,11 +12363,11 @@ class FigIcon extends HTMLElement {
|
|
|
11867
12363
|
|
|
11868
12364
|
#sync() {
|
|
11869
12365
|
const iconName = this.getAttribute("name");
|
|
11870
|
-
const
|
|
12366
|
+
const size = this.getAttribute("size") || "medium";
|
|
12367
|
+
const cssVar = figIconCssVar(iconName, size);
|
|
11871
12368
|
if (cssVar) this.style.setProperty("--icon", cssVar);
|
|
11872
12369
|
else this.style.removeProperty("--icon");
|
|
11873
12370
|
|
|
11874
|
-
const size = this.getAttribute("size") || "medium";
|
|
11875
12371
|
if (size === "small") {
|
|
11876
12372
|
this.style.setProperty("--size", "var(--spacer-3)");
|
|
11877
12373
|
} else {
|
|
@@ -11901,2175 +12397,23 @@ customElements.define("fig-button-combo", FigButtonCombo);
|
|
|
11901
12397
|
class FigInputCombo extends HTMLElement {}
|
|
11902
12398
|
customElements.define("fig-input-combo", FigInputCombo);
|
|
11903
12399
|
|
|
11904
|
-
|
|
12400
|
+
|
|
12401
|
+
|
|
12402
|
+
/* Color Tip */
|
|
11905
12403
|
/**
|
|
11906
|
-
* A
|
|
11907
|
-
*
|
|
11908
|
-
*
|
|
11909
|
-
* @attr {
|
|
11910
|
-
* @
|
|
11911
|
-
* @
|
|
11912
|
-
* @attr {string} dialog-position - Position of the popup (default: "left")
|
|
12404
|
+
* A compact solid-color tip that wraps fig-fill-picker.
|
|
12405
|
+
* @attr {string} value - Solid color string (hex/rgb/hsl/named)
|
|
12406
|
+
* @attr {boolean} selected - Whether the tip is selected
|
|
12407
|
+
* @attr {boolean} disabled - Whether the tip is disabled
|
|
12408
|
+
* @fires input - While color changes
|
|
12409
|
+
* @fires change - When color is committed
|
|
11913
12410
|
*/
|
|
11914
|
-
class
|
|
11915
|
-
#
|
|
12411
|
+
class FigColorTip extends HTMLElement {
|
|
12412
|
+
#fillPicker = null;
|
|
11916
12413
|
#chit = null;
|
|
11917
|
-
#
|
|
11918
|
-
#
|
|
11919
|
-
|
|
11920
|
-
|
|
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
|
-
|
|
14058
|
-
/* Color Tip */
|
|
14059
|
-
/**
|
|
14060
|
-
* A compact solid-color tip that wraps fig-fill-picker.
|
|
14061
|
-
* @attr {string} value - Solid color string (hex/rgb/hsl/named)
|
|
14062
|
-
* @attr {boolean} selected - Whether the tip is selected
|
|
14063
|
-
* @attr {boolean} disabled - Whether the tip is disabled
|
|
14064
|
-
* @fires input - While color changes
|
|
14065
|
-
* @fires change - When color is committed
|
|
14066
|
-
*/
|
|
14067
|
-
class FigColorTip extends HTMLElement {
|
|
14068
|
-
#fillPicker = null;
|
|
14069
|
-
#chit = null;
|
|
14070
|
-
#chitSelectedObserver = null;
|
|
14071
|
-
#boundHandleInput = this.#handlePickerInput.bind(this);
|
|
14072
|
-
#boundHandleChange = this.#handlePickerChange.bind(this);
|
|
12414
|
+
#chitSelectedObserver = null;
|
|
12415
|
+
#boundHandleInput = this.#handlePickerInput.bind(this);
|
|
12416
|
+
#boundHandleChange = this.#handlePickerChange.bind(this);
|
|
14073
12417
|
|
|
14074
12418
|
static get observedAttributes() {
|
|
14075
12419
|
return ["value", "selected", "disabled", "alpha", "control"];
|
|
@@ -14094,6 +12438,10 @@ class FigColorTip extends HTMLElement {
|
|
|
14094
12438
|
this.#fillPicker.removeEventListener("input", this.#boundHandleInput);
|
|
14095
12439
|
this.#fillPicker.removeEventListener("change", this.#boundHandleChange);
|
|
14096
12440
|
}
|
|
12441
|
+
if (this.#chit) {
|
|
12442
|
+
this.#chit.removeEventListener("input", this.#boundHandleInput);
|
|
12443
|
+
this.#chit.removeEventListener("change", this.#boundHandleChange);
|
|
12444
|
+
}
|
|
14097
12445
|
if (this.#chitSelectedObserver) {
|
|
14098
12446
|
this.#chitSelectedObserver.disconnect();
|
|
14099
12447
|
this.#chitSelectedObserver = null;
|
|
@@ -14151,16 +12499,22 @@ class FigColorTip extends HTMLElement {
|
|
|
14151
12499
|
opacity: Math.round(alpha * 100),
|
|
14152
12500
|
})
|
|
14153
12501
|
: JSON.stringify({ type: "solid", color });
|
|
14154
|
-
|
|
14155
|
-
|
|
14156
|
-
|
|
14157
|
-
|
|
12502
|
+
const chitAlphaAttr = alpha < 1 ? ` alpha="${alpha}"` : "";
|
|
12503
|
+
this.innerHTML = hasFigFillPicker()
|
|
12504
|
+
? `<fig-fill-picker mode="solid" ${alphaAttr} value='${pickerValue}'>
|
|
12505
|
+
<fig-chit background="${color}"${chitAlphaAttr}></fig-chit>
|
|
12506
|
+
</fig-fill-picker>`
|
|
12507
|
+
: `<fig-chit background="${color}"${chitAlphaAttr}></fig-chit>`;
|
|
14158
12508
|
|
|
14159
12509
|
this.#fillPicker = this.querySelector("fig-fill-picker");
|
|
14160
12510
|
this.#chit = this.querySelector("fig-chit");
|
|
14161
12511
|
this.#teardownListeners();
|
|
14162
12512
|
this.#fillPicker?.addEventListener("input", this.#boundHandleInput);
|
|
14163
12513
|
this.#fillPicker?.addEventListener("change", this.#boundHandleChange);
|
|
12514
|
+
if (!this.#fillPicker) {
|
|
12515
|
+
this.#chit?.addEventListener("input", this.#boundHandleInput);
|
|
12516
|
+
this.#chit?.addEventListener("change", this.#boundHandleChange);
|
|
12517
|
+
}
|
|
14164
12518
|
this.#observeChitSelected();
|
|
14165
12519
|
}
|
|
14166
12520
|
|
|
@@ -14187,8 +12541,13 @@ class FigColorTip extends HTMLElement {
|
|
|
14187
12541
|
#extractAlpha(colorValue) {
|
|
14188
12542
|
if (!colorValue) return 1;
|
|
14189
12543
|
const v = String(colorValue).trim();
|
|
14190
|
-
|
|
14191
|
-
|
|
12544
|
+
const hex = v.replace(/^#/, "");
|
|
12545
|
+
if (/^[0-9a-f]{4}$/i.test(hex)) {
|
|
12546
|
+
const a = hex[3];
|
|
12547
|
+
return parseInt(`${a}${a}`, 16) / 255;
|
|
12548
|
+
}
|
|
12549
|
+
if (/^[0-9a-f]{8}$/i.test(hex)) {
|
|
12550
|
+
return parseInt(hex.slice(6, 8), 16) / 255;
|
|
14192
12551
|
}
|
|
14193
12552
|
const rgbaMatch = v.match(
|
|
14194
12553
|
/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/i,
|
|
@@ -14215,6 +12574,9 @@ class FigColorTip extends HTMLElement {
|
|
|
14215
12574
|
if (value.startsWith("#")) {
|
|
14216
12575
|
return this.#normalizeHex(value);
|
|
14217
12576
|
}
|
|
12577
|
+
if (/^[0-9a-f]{3,4}$|^[0-9a-f]{6}$|^[0-9a-f]{8}$/i.test(value)) {
|
|
12578
|
+
return this.#normalizeHex(value);
|
|
12579
|
+
}
|
|
14218
12580
|
|
|
14219
12581
|
try {
|
|
14220
12582
|
const { ctx } = figGetSharedCanvas(1, 1);
|
|
@@ -14268,6 +12630,11 @@ class FigColorTip extends HTMLElement {
|
|
|
14268
12630
|
|
|
14269
12631
|
if (this.#chit) {
|
|
14270
12632
|
this.#chit.setAttribute("background", color);
|
|
12633
|
+
if (alpha < 1) {
|
|
12634
|
+
this.#chit.setAttribute("alpha", String(alpha));
|
|
12635
|
+
} else {
|
|
12636
|
+
this.#chit.removeAttribute("alpha");
|
|
12637
|
+
}
|
|
14271
12638
|
if (this.hasAttribute("disabled")) {
|
|
14272
12639
|
this.#chit.setAttribute("disabled", "");
|
|
14273
12640
|
} else {
|
|
@@ -14306,12 +12673,12 @@ class FigColorTip extends HTMLElement {
|
|
|
14306
12673
|
|
|
14307
12674
|
#handlePickerInput(event) {
|
|
14308
12675
|
event.stopPropagation();
|
|
14309
|
-
this.#updateColorFromPicker(event.detail, "input");
|
|
12676
|
+
this.#updateColorFromPicker(event.detail || { color: event.target?.value }, "input");
|
|
14310
12677
|
}
|
|
14311
12678
|
|
|
14312
12679
|
#handlePickerChange(event) {
|
|
14313
12680
|
event.stopPropagation();
|
|
14314
|
-
this.#updateColorFromPicker(event.detail, "change");
|
|
12681
|
+
this.#updateColorFromPicker(event.detail || { color: event.target?.value }, "change");
|
|
14315
12682
|
}
|
|
14316
12683
|
|
|
14317
12684
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -14998,6 +13365,7 @@ class FigHandle extends HTMLElement {
|
|
|
14998
13365
|
#applyingValue = false;
|
|
14999
13366
|
#colorTip = null;
|
|
15000
13367
|
#directColorPicker = null;
|
|
13368
|
+
#nativeColorInput = null;
|
|
15001
13369
|
#hitAreaEl = null;
|
|
15002
13370
|
|
|
15003
13371
|
get #controlMode() {
|
|
@@ -15240,6 +13608,7 @@ class FigHandle extends HTMLElement {
|
|
|
15240
13608
|
this.#teardownDrag();
|
|
15241
13609
|
this.#hideColorTip();
|
|
15242
13610
|
this.#removeDirectColorPicker();
|
|
13611
|
+
this.#removeNativeColorInput();
|
|
15243
13612
|
if (this.#hitAreaEl) {
|
|
15244
13613
|
this.#hitAreaEl.remove();
|
|
15245
13614
|
this.#hitAreaEl = null;
|
|
@@ -15566,6 +13935,7 @@ class FigHandle extends HTMLElement {
|
|
|
15566
13935
|
}
|
|
15567
13936
|
|
|
15568
13937
|
#ensureDirectColorPicker() {
|
|
13938
|
+
if (!hasFigFillPicker()) return null;
|
|
15569
13939
|
if (this.#directColorPicker) return this.#directColorPicker;
|
|
15570
13940
|
|
|
15571
13941
|
const picker = document.createElement("fig-fill-picker");
|
|
@@ -15591,6 +13961,10 @@ class FigHandle extends HTMLElement {
|
|
|
15591
13961
|
if (this.hasAttribute("disabled")) return;
|
|
15592
13962
|
this.#hideColorTip();
|
|
15593
13963
|
const picker = this.#ensureDirectColorPicker();
|
|
13964
|
+
if (!picker) {
|
|
13965
|
+
this.#openNativeColorPicker();
|
|
13966
|
+
return;
|
|
13967
|
+
}
|
|
15594
13968
|
this.setAttribute("selected", "");
|
|
15595
13969
|
this.#syncDirectColorPickerValue();
|
|
15596
13970
|
picker.open();
|
|
@@ -15607,6 +13981,40 @@ class FigHandle extends HTMLElement {
|
|
|
15607
13981
|
this.removeAttribute("selected");
|
|
15608
13982
|
}
|
|
15609
13983
|
|
|
13984
|
+
#ensureNativeColorInput() {
|
|
13985
|
+
if (this.#nativeColorInput) return this.#nativeColorInput;
|
|
13986
|
+
const input = document.createElement("input");
|
|
13987
|
+
input.type = "color";
|
|
13988
|
+
input.tabIndex = -1;
|
|
13989
|
+
input.setAttribute("aria-hidden", "true");
|
|
13990
|
+
input.style.position = "fixed";
|
|
13991
|
+
input.style.inlineSize = "1px";
|
|
13992
|
+
input.style.blockSize = "1px";
|
|
13993
|
+
input.style.opacity = "0";
|
|
13994
|
+
input.style.pointerEvents = "none";
|
|
13995
|
+
input.addEventListener("input", this.#handleNativeColorInput);
|
|
13996
|
+
input.addEventListener("change", this.#handleNativeColorChange);
|
|
13997
|
+
this.appendChild(input);
|
|
13998
|
+
this.#nativeColorInput = input;
|
|
13999
|
+
return input;
|
|
14000
|
+
}
|
|
14001
|
+
|
|
14002
|
+
#openNativeColorPicker() {
|
|
14003
|
+
const input = this.#ensureNativeColorInput();
|
|
14004
|
+
const { color } = this.#normalizeColorForPicker();
|
|
14005
|
+
input.value = color;
|
|
14006
|
+
this.setAttribute("selected", "");
|
|
14007
|
+
input.click();
|
|
14008
|
+
}
|
|
14009
|
+
|
|
14010
|
+
#removeNativeColorInput() {
|
|
14011
|
+
if (!this.#nativeColorInput) return;
|
|
14012
|
+
this.#nativeColorInput.removeEventListener("input", this.#handleNativeColorInput);
|
|
14013
|
+
this.#nativeColorInput.removeEventListener("change", this.#handleNativeColorChange);
|
|
14014
|
+
this.#nativeColorInput.remove();
|
|
14015
|
+
this.#nativeColorInput = null;
|
|
14016
|
+
}
|
|
14017
|
+
|
|
15610
14018
|
#closeColorPickerForDrag() {
|
|
15611
14019
|
if (this.getAttribute("type") !== "color") return;
|
|
15612
14020
|
this.#hideColorTip();
|
|
@@ -15684,6 +14092,36 @@ class FigHandle extends HTMLElement {
|
|
|
15684
14092
|
);
|
|
15685
14093
|
};
|
|
15686
14094
|
|
|
14095
|
+
#detailFromNativeColor(value) {
|
|
14096
|
+
const { opacity } = this.#normalizeColorForPicker();
|
|
14097
|
+
return opacity < 100 ? { color: value, opacity } : { color: value };
|
|
14098
|
+
}
|
|
14099
|
+
|
|
14100
|
+
#handleNativeColorInput = (e) => {
|
|
14101
|
+
e.stopPropagation();
|
|
14102
|
+
const detail = this.#detailFromNativeColor(e.target.value);
|
|
14103
|
+
this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
|
|
14104
|
+
this.dispatchEvent(
|
|
14105
|
+
new CustomEvent("input", {
|
|
14106
|
+
bubbles: true,
|
|
14107
|
+
detail,
|
|
14108
|
+
}),
|
|
14109
|
+
);
|
|
14110
|
+
};
|
|
14111
|
+
|
|
14112
|
+
#handleNativeColorChange = (e) => {
|
|
14113
|
+
e.stopPropagation();
|
|
14114
|
+
const detail = this.#detailFromNativeColor(e.target.value);
|
|
14115
|
+
this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
|
|
14116
|
+
this.removeAttribute("selected");
|
|
14117
|
+
this.dispatchEvent(
|
|
14118
|
+
new CustomEvent("change", {
|
|
14119
|
+
bubbles: true,
|
|
14120
|
+
detail,
|
|
14121
|
+
}),
|
|
14122
|
+
);
|
|
14123
|
+
};
|
|
14124
|
+
|
|
15687
14125
|
#handleDirectColorPickerClose = () => {
|
|
15688
14126
|
this.removeAttribute("selected");
|
|
15689
14127
|
};
|