@rogieking/figui3 2.23.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.css +8 -2
- package/fig.js +199 -77
- package/index.html +345 -4
- package/package.json +1 -1
package/components.css
CHANGED
|
@@ -466,7 +466,6 @@ input[type="text"][list] {
|
|
|
466
466
|
}
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
-
max-inline-size: var(--max-width);
|
|
470
469
|
padding-block: 0;
|
|
471
470
|
will-change: scale;
|
|
472
471
|
|
|
@@ -599,7 +598,9 @@ fig-dropdown,
|
|
|
599
598
|
display: none !important;
|
|
600
599
|
}
|
|
601
600
|
> select {
|
|
602
|
-
display:
|
|
601
|
+
display: flex;
|
|
602
|
+
align-items: center;
|
|
603
|
+
flex: 1;
|
|
603
604
|
width: 100%;
|
|
604
605
|
}
|
|
605
606
|
|
|
@@ -721,6 +722,7 @@ fig-button {
|
|
|
721
722
|
|
|
722
723
|
&:active {
|
|
723
724
|
background-color: var(--figma-color-bg-secondary);
|
|
725
|
+
color: var(--figma-color-text);
|
|
724
726
|
}
|
|
725
727
|
|
|
726
728
|
&:focus-visible {
|
|
@@ -2985,6 +2987,10 @@ fig-shimmer:not([playing="false"]) > * {
|
|
|
2985
2987
|
/* Fill Picker */
|
|
2986
2988
|
fig-fill-picker {
|
|
2987
2989
|
display: contents;
|
|
2990
|
+
|
|
2991
|
+
> [slot^="mode-"] {
|
|
2992
|
+
display: none;
|
|
2993
|
+
}
|
|
2988
2994
|
}
|
|
2989
2995
|
|
|
2990
2996
|
.fig-fill-picker-dialog {
|
package/fig.js
CHANGED
|
@@ -3384,16 +3384,29 @@ class FigInputColor extends HTMLElement {
|
|
|
3384
3384
|
return this.getAttribute("picker") || "native";
|
|
3385
3385
|
}
|
|
3386
3386
|
|
|
3387
|
+
#buildFillPickerAttrs() {
|
|
3388
|
+
const attrs = {};
|
|
3389
|
+
const experimental = this.getAttribute("experimental");
|
|
3390
|
+
if (experimental) attrs["experimental"] = experimental;
|
|
3391
|
+
// picker-* attributes forwarded to fill picker (except anchor, handled programmatically)
|
|
3392
|
+
for (const { name, value } of this.attributes) {
|
|
3393
|
+
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
3394
|
+
attrs[name.slice(7)] = value;
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
3398
|
+
return Object.entries(attrs)
|
|
3399
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
3400
|
+
.join(" ");
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3387
3403
|
connectedCallback() {
|
|
3388
3404
|
this.#setValues(this.getAttribute("value"));
|
|
3389
3405
|
|
|
3390
3406
|
const useFigmaPicker = this.picker === "figma";
|
|
3391
3407
|
const hidePicker = this.picker === "false";
|
|
3392
3408
|
const showAlpha = this.getAttribute("alpha") === "true";
|
|
3393
|
-
const
|
|
3394
|
-
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
3395
|
-
const dialogPos = this.getAttribute("dialog-position") || "left";
|
|
3396
|
-
const dialogPosAttr = `dialog-position="${dialogPos}"`;
|
|
3409
|
+
const fpAttrs = this.#buildFillPickerAttrs();
|
|
3397
3410
|
|
|
3398
3411
|
let html = ``;
|
|
3399
3412
|
if (this.getAttribute("text")) {
|
|
@@ -3418,7 +3431,7 @@ class FigInputColor extends HTMLElement {
|
|
|
3418
3431
|
let swatchElement = "";
|
|
3419
3432
|
if (!hidePicker) {
|
|
3420
3433
|
swatchElement = useFigmaPicker
|
|
3421
|
-
? `<fig-fill-picker mode="solid" ${
|
|
3434
|
+
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
3422
3435
|
showAlpha ? "" : 'alpha="false"'
|
|
3423
3436
|
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
3424
3437
|
this.alpha
|
|
@@ -3436,7 +3449,7 @@ class FigInputColor extends HTMLElement {
|
|
|
3436
3449
|
html = ``;
|
|
3437
3450
|
} else {
|
|
3438
3451
|
html = useFigmaPicker
|
|
3439
|
-
? `<fig-fill-picker mode="solid" ${
|
|
3452
|
+
? `<fig-fill-picker mode="solid" ${fpAttrs} ${
|
|
3440
3453
|
showAlpha ? "" : 'alpha="false"'
|
|
3441
3454
|
} value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
|
|
3442
3455
|
this.alpha
|
|
@@ -3460,6 +3473,13 @@ class FigInputColor extends HTMLElement {
|
|
|
3460
3473
|
|
|
3461
3474
|
// Setup fill picker (figma picker)
|
|
3462
3475
|
if (this.#fillPicker) {
|
|
3476
|
+
const anchor = this.getAttribute("picker-anchor");
|
|
3477
|
+
if (anchor === "self") {
|
|
3478
|
+
this.#fillPicker.anchorElement = this;
|
|
3479
|
+
} else if (anchor) {
|
|
3480
|
+
const el = document.querySelector(anchor);
|
|
3481
|
+
if (el) this.#fillPicker.anchorElement = el;
|
|
3482
|
+
}
|
|
3463
3483
|
if (this.hasAttribute("disabled")) {
|
|
3464
3484
|
this.#fillPicker.setAttribute("disabled", "");
|
|
3465
3485
|
}
|
|
@@ -3628,7 +3648,7 @@ class FigInputColor extends HTMLElement {
|
|
|
3628
3648
|
}
|
|
3629
3649
|
|
|
3630
3650
|
static get observedAttributes() {
|
|
3631
|
-
return ["value", "style", "mode", "picker", "experimental"
|
|
3651
|
+
return ["value", "style", "mode", "picker", "experimental"];
|
|
3632
3652
|
}
|
|
3633
3653
|
|
|
3634
3654
|
get mode() {
|
|
@@ -3863,6 +3883,25 @@ class FigInputFill extends HTMLElement {
|
|
|
3863
3883
|
}
|
|
3864
3884
|
}
|
|
3865
3885
|
|
|
3886
|
+
#buildFillPickerAttrs() {
|
|
3887
|
+
const attrs = {};
|
|
3888
|
+
// Backward-compat: direct attributes forwarded to fill picker
|
|
3889
|
+
const mode = this.getAttribute("mode");
|
|
3890
|
+
if (mode) attrs["mode"] = mode;
|
|
3891
|
+
const experimental = this.getAttribute("experimental");
|
|
3892
|
+
if (experimental) attrs["experimental"] = experimental;
|
|
3893
|
+
// picker-* overrides (except anchor, handled programmatically)
|
|
3894
|
+
for (const { name, value } of this.attributes) {
|
|
3895
|
+
if (name.startsWith("picker-") && name !== "picker-anchor") {
|
|
3896
|
+
attrs[name.slice(7)] = value;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
3900
|
+
return Object.entries(attrs)
|
|
3901
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
3902
|
+
.join(" ");
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3866
3905
|
#render() {
|
|
3867
3906
|
const disabled = this.hasAttribute("disabled");
|
|
3868
3907
|
const fillPickerValue = JSON.stringify(this.value);
|
|
@@ -3960,13 +3999,12 @@ class FigInputFill extends HTMLElement {
|
|
|
3960
3999
|
break;
|
|
3961
4000
|
}
|
|
3962
4001
|
|
|
3963
|
-
const
|
|
3964
|
-
const experimentalAttr = this.getAttribute("experimental");
|
|
4002
|
+
const fpAttrs = this.#buildFillPickerAttrs();
|
|
3965
4003
|
this.innerHTML = `
|
|
3966
4004
|
<div class="input-combo">
|
|
3967
|
-
<fig-fill-picker
|
|
4005
|
+
<fig-fill-picker ${fpAttrs} value='${fillPickerValue}' ${
|
|
3968
4006
|
disabled ? "disabled" : ""
|
|
3969
|
-
}
|
|
4007
|
+
}></fig-fill-picker>
|
|
3970
4008
|
${controlsHtml}
|
|
3971
4009
|
</div>`;
|
|
3972
4010
|
|
|
@@ -3991,6 +4029,14 @@ class FigInputFill extends HTMLElement {
|
|
|
3991
4029
|
}
|
|
3992
4030
|
|
|
3993
4031
|
if (this.#fillPicker) {
|
|
4032
|
+
const anchor = this.getAttribute("picker-anchor");
|
|
4033
|
+
if (!anchor || anchor === "self") {
|
|
4034
|
+
this.#fillPicker.anchorElement = this;
|
|
4035
|
+
} else {
|
|
4036
|
+
const el = document.querySelector(anchor);
|
|
4037
|
+
if (el) this.#fillPicker.anchorElement = el;
|
|
4038
|
+
}
|
|
4039
|
+
|
|
3994
4040
|
this.#fillPicker.addEventListener("input", (e) => {
|
|
3995
4041
|
e.stopPropagation();
|
|
3996
4042
|
const detail = e.detail;
|
|
@@ -5076,6 +5122,8 @@ customElements.define("fig-chit", FigChit);
|
|
|
5076
5122
|
*/
|
|
5077
5123
|
class FigImage extends HTMLElement {
|
|
5078
5124
|
#src = null;
|
|
5125
|
+
#boundHandleFileInput = this.#handleFileInput.bind(this);
|
|
5126
|
+
#boundHandleDownload = this.#handleDownload.bind(this);
|
|
5079
5127
|
constructor() {
|
|
5080
5128
|
super();
|
|
5081
5129
|
}
|
|
@@ -5108,10 +5156,8 @@ class FigImage extends HTMLElement {
|
|
|
5108
5156
|
this.#updateRefs();
|
|
5109
5157
|
}
|
|
5110
5158
|
disconnectedCallback() {
|
|
5111
|
-
this.fileInput
|
|
5112
|
-
|
|
5113
|
-
this.#handleFileInput.bind(this)
|
|
5114
|
-
);
|
|
5159
|
+
this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
5160
|
+
this.downloadButton?.removeEventListener("click", this.#boundHandleDownload);
|
|
5115
5161
|
}
|
|
5116
5162
|
|
|
5117
5163
|
#updateRefs() {
|
|
@@ -5120,25 +5166,13 @@ class FigImage extends HTMLElement {
|
|
|
5120
5166
|
if (this.upload) {
|
|
5121
5167
|
this.uploadButton = this.querySelector("fig-button[type='upload']");
|
|
5122
5168
|
this.fileInput = this.uploadButton?.querySelector("input");
|
|
5123
|
-
this.fileInput
|
|
5124
|
-
|
|
5125
|
-
this.#handleFileInput.bind(this)
|
|
5126
|
-
);
|
|
5127
|
-
this.fileInput.addEventListener(
|
|
5128
|
-
"change",
|
|
5129
|
-
this.#handleFileInput.bind(this)
|
|
5130
|
-
);
|
|
5169
|
+
this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
5170
|
+
this.fileInput?.addEventListener("change", this.#boundHandleFileInput);
|
|
5131
5171
|
}
|
|
5132
5172
|
if (this.download) {
|
|
5133
5173
|
this.downloadButton = this.querySelector("fig-button[type='download']");
|
|
5134
|
-
this.downloadButton
|
|
5135
|
-
|
|
5136
|
-
this.#handleDownload.bind(this)
|
|
5137
|
-
);
|
|
5138
|
-
this.downloadButton.addEventListener(
|
|
5139
|
-
"click",
|
|
5140
|
-
this.#handleDownload.bind(this)
|
|
5141
|
-
);
|
|
5174
|
+
this.downloadButton?.removeEventListener("click", this.#boundHandleDownload);
|
|
5175
|
+
this.downloadButton?.addEventListener("click", this.#boundHandleDownload);
|
|
5142
5176
|
}
|
|
5143
5177
|
});
|
|
5144
5178
|
}
|
|
@@ -6235,6 +6269,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
6235
6269
|
#chit = null;
|
|
6236
6270
|
#dialog = null;
|
|
6237
6271
|
#activeTab = "solid";
|
|
6272
|
+
anchorElement = null;
|
|
6238
6273
|
|
|
6239
6274
|
// Fill state
|
|
6240
6275
|
#fillType = "solid";
|
|
@@ -6253,6 +6288,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
6253
6288
|
#video = { url: null, scaleMode: "fill", scale: 50 };
|
|
6254
6289
|
#webcam = { stream: null, snapshot: null };
|
|
6255
6290
|
|
|
6291
|
+
// Custom mode slots and data
|
|
6292
|
+
#customSlots = {};
|
|
6293
|
+
#customData = {};
|
|
6294
|
+
|
|
6256
6295
|
// DOM references for solid tab
|
|
6257
6296
|
#colorArea = null;
|
|
6258
6297
|
#colorAreaHandle = null;
|
|
@@ -6287,7 +6326,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
6287
6326
|
}
|
|
6288
6327
|
|
|
6289
6328
|
#setupTrigger() {
|
|
6290
|
-
const child = this.
|
|
6329
|
+
const child = Array.from(this.children).find(
|
|
6330
|
+
(el) => !el.getAttribute("slot")?.startsWith("mode-")
|
|
6331
|
+
);
|
|
6291
6332
|
|
|
6292
6333
|
if (!child) {
|
|
6293
6334
|
// Scenario 1: Empty - create fig-chit
|
|
@@ -6327,6 +6368,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
6327
6368
|
const valueAttr = this.getAttribute("value");
|
|
6328
6369
|
if (!valueAttr) return;
|
|
6329
6370
|
|
|
6371
|
+
const builtinTypes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6372
|
+
|
|
6330
6373
|
try {
|
|
6331
6374
|
const parsed = JSON.parse(valueAttr);
|
|
6332
6375
|
if (parsed.type) this.#fillType = parsed.type;
|
|
@@ -6349,6 +6392,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6349
6392
|
this.#gradient = { ...this.#gradient, ...parsed.gradient };
|
|
6350
6393
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
6351
6394
|
if (parsed.video) this.#video = { ...this.#video, ...parsed.video };
|
|
6395
|
+
|
|
6396
|
+
// Store full parsed data for custom (non-built-in) types
|
|
6397
|
+
if (parsed.type && !builtinTypes.includes(parsed.type)) {
|
|
6398
|
+
const { type, ...rest } = parsed;
|
|
6399
|
+
this.#customData[parsed.type] = rest;
|
|
6400
|
+
}
|
|
6352
6401
|
} catch (e) {
|
|
6353
6402
|
// If not JSON, treat as hex color
|
|
6354
6403
|
if (valueAttr.startsWith("#")) {
|
|
@@ -6399,7 +6448,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
6399
6448
|
}
|
|
6400
6449
|
break;
|
|
6401
6450
|
default:
|
|
6402
|
-
|
|
6451
|
+
const slot = this.#customSlots[this.#fillType];
|
|
6452
|
+
bg = slot?.element?.getAttribute("chit-background") || "#D9D9D9";
|
|
6403
6453
|
}
|
|
6404
6454
|
|
|
6405
6455
|
this.#chit.setAttribute("background", bg);
|
|
@@ -6446,6 +6496,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
6446
6496
|
}
|
|
6447
6497
|
|
|
6448
6498
|
#createDialog() {
|
|
6499
|
+
// Collect slotted custom mode content before any DOM changes
|
|
6500
|
+
this.#customSlots = {};
|
|
6501
|
+
this.querySelectorAll('[slot^="mode-"]').forEach((el) => {
|
|
6502
|
+
const modeName = el.getAttribute("slot").slice(5);
|
|
6503
|
+
this.#customSlots[modeName] = {
|
|
6504
|
+
element: el,
|
|
6505
|
+
label:
|
|
6506
|
+
el.getAttribute("label") ||
|
|
6507
|
+
modeName.charAt(0).toUpperCase() + modeName.slice(1),
|
|
6508
|
+
};
|
|
6509
|
+
});
|
|
6510
|
+
|
|
6449
6511
|
this.#dialog = document.createElement("dialog", { is: "fig-popup" });
|
|
6450
6512
|
this.#dialog.setAttribute("is", "fig-popup");
|
|
6451
6513
|
this.#dialog.setAttribute("drag", "true");
|
|
@@ -6453,14 +6515,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6453
6515
|
this.#dialog.setAttribute("autoresize", "false");
|
|
6454
6516
|
this.#dialog.classList.add("fig-fill-picker-dialog");
|
|
6455
6517
|
|
|
6456
|
-
this.#dialog.anchor = this.#trigger;
|
|
6518
|
+
this.#dialog.anchor = this.anchorElement || this.#trigger;
|
|
6457
6519
|
const dialogPosition = this.getAttribute("dialog-position") || "left";
|
|
6458
6520
|
this.#dialog.setAttribute("position", dialogPosition);
|
|
6459
6521
|
|
|
6460
|
-
|
|
6461
|
-
const
|
|
6462
|
-
const allModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6463
|
-
const modeLabels = {
|
|
6522
|
+
const builtinModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6523
|
+
const builtinLabels = {
|
|
6464
6524
|
solid: "Solid",
|
|
6465
6525
|
gradient: "Gradient",
|
|
6466
6526
|
image: "Image",
|
|
@@ -6468,24 +6528,33 @@ class FigFillPicker extends HTMLElement {
|
|
|
6468
6528
|
webcam: "Webcam",
|
|
6469
6529
|
};
|
|
6470
6530
|
|
|
6471
|
-
//
|
|
6472
|
-
|
|
6531
|
+
// Build allowed modes: built-ins filtered normally, custom names accepted if slot exists
|
|
6532
|
+
const mode = this.getAttribute("mode");
|
|
6533
|
+
let allowedModes;
|
|
6473
6534
|
if (mode) {
|
|
6474
|
-
const
|
|
6475
|
-
allowedModes =
|
|
6476
|
-
|
|
6535
|
+
const requested = mode.split(",").map((m) => m.trim().toLowerCase());
|
|
6536
|
+
allowedModes = requested.filter(
|
|
6537
|
+
(m) => builtinModes.includes(m) || this.#customSlots[m]
|
|
6538
|
+
);
|
|
6539
|
+
if (allowedModes.length === 0) allowedModes = [...builtinModes];
|
|
6540
|
+
} else {
|
|
6541
|
+
allowedModes = [...builtinModes];
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
// Build labels map: built-in labels + custom slot labels
|
|
6545
|
+
const modeLabels = { ...builtinLabels };
|
|
6546
|
+
for (const [name, { label }] of Object.entries(this.#customSlots)) {
|
|
6547
|
+
modeLabels[name] = label;
|
|
6477
6548
|
}
|
|
6478
6549
|
|
|
6479
|
-
// If current fillType not in allowed modes, switch to first allowed
|
|
6480
6550
|
if (!allowedModes.includes(this.#fillType)) {
|
|
6481
6551
|
this.#fillType = allowedModes[0];
|
|
6482
6552
|
this.#activeTab = allowedModes[0];
|
|
6483
6553
|
}
|
|
6484
6554
|
|
|
6485
|
-
// Build header content - label if single mode, dropdown if multiple
|
|
6486
6555
|
const experimental = this.getAttribute("experimental");
|
|
6487
6556
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
6488
|
-
|
|
6557
|
+
|
|
6489
6558
|
let headerContent;
|
|
6490
6559
|
if (allowedModes.length === 1) {
|
|
6491
6560
|
headerContent = `<h3 class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</h3>`;
|
|
@@ -6498,6 +6567,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
6498
6567
|
</fig-dropdown>`;
|
|
6499
6568
|
}
|
|
6500
6569
|
|
|
6570
|
+
// Generate tab containers for all allowed modes
|
|
6571
|
+
const tabDivs = allowedModes
|
|
6572
|
+
.map((m) => `<div class="fig-fill-picker-tab" data-tab="${m}"></div>`)
|
|
6573
|
+
.join("\n ");
|
|
6574
|
+
|
|
6501
6575
|
this.#dialog.innerHTML = `
|
|
6502
6576
|
<fig-header>
|
|
6503
6577
|
${headerContent}
|
|
@@ -6506,16 +6580,33 @@ class FigFillPicker extends HTMLElement {
|
|
|
6506
6580
|
</fig-button>
|
|
6507
6581
|
</fig-header>
|
|
6508
6582
|
<div class="fig-fill-picker-content">
|
|
6509
|
-
|
|
6510
|
-
<div class="fig-fill-picker-tab" data-tab="gradient"></div>
|
|
6511
|
-
<div class="fig-fill-picker-tab" data-tab="image"></div>
|
|
6512
|
-
<div class="fig-fill-picker-tab" data-tab="video"></div>
|
|
6513
|
-
<div class="fig-fill-picker-tab" data-tab="webcam"></div>
|
|
6583
|
+
${tabDivs}
|
|
6514
6584
|
</div>
|
|
6515
6585
|
`;
|
|
6516
6586
|
|
|
6517
6587
|
document.body.appendChild(this.#dialog);
|
|
6518
6588
|
|
|
6589
|
+
// Populate custom tab containers and emit modeready
|
|
6590
|
+
for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
|
|
6591
|
+
const container = this.#dialog.querySelector(
|
|
6592
|
+
`[data-tab="${modeName}"]`
|
|
6593
|
+
);
|
|
6594
|
+
if (!container) continue;
|
|
6595
|
+
|
|
6596
|
+
// Move children (not the element itself) for vanilla HTML usage
|
|
6597
|
+
while (element.firstChild) {
|
|
6598
|
+
container.appendChild(element.firstChild);
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
// Emit modeready so frameworks can render into the container
|
|
6602
|
+
this.dispatchEvent(
|
|
6603
|
+
new CustomEvent("modeready", {
|
|
6604
|
+
bubbles: true,
|
|
6605
|
+
detail: { mode: modeName, container },
|
|
6606
|
+
})
|
|
6607
|
+
);
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6519
6610
|
// Setup type dropdown switching (only if not locked)
|
|
6520
6611
|
const typeDropdown = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
6521
6612
|
if (typeDropdown) {
|
|
@@ -6530,34 +6621,50 @@ class FigFillPicker extends HTMLElement {
|
|
|
6530
6621
|
this.#dialog.open = false;
|
|
6531
6622
|
});
|
|
6532
6623
|
|
|
6533
|
-
// Emit change on close
|
|
6534
6624
|
this.#dialog.addEventListener("close", () => {
|
|
6535
6625
|
this.#emitChange();
|
|
6536
6626
|
});
|
|
6537
6627
|
|
|
6538
|
-
// Initialize tabs
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
const mode = this.getAttribute("mode");
|
|
6549
|
-
const allModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6550
|
-
|
|
6551
|
-
let allowedModes = allModes;
|
|
6552
|
-
if (mode) {
|
|
6553
|
-
const requestedModes = mode.split(",").map((m) => m.trim().toLowerCase());
|
|
6554
|
-
allowedModes = requestedModes.filter((m) => allModes.includes(m));
|
|
6555
|
-
if (allowedModes.length === 0) allowedModes = allModes;
|
|
6628
|
+
// Initialize built-in tabs (skip any overridden by custom slots)
|
|
6629
|
+
const builtinInits = {
|
|
6630
|
+
solid: () => this.#initSolidTab(),
|
|
6631
|
+
gradient: () => this.#initGradientTab(),
|
|
6632
|
+
image: () => this.#initImageTab(),
|
|
6633
|
+
video: () => this.#initVideoTab(),
|
|
6634
|
+
webcam: () => this.#initWebcamTab(),
|
|
6635
|
+
};
|
|
6636
|
+
for (const [name, init] of Object.entries(builtinInits)) {
|
|
6637
|
+
if (!this.#customSlots[name] && allowedModes.includes(name)) init();
|
|
6556
6638
|
}
|
|
6557
6639
|
|
|
6558
|
-
|
|
6559
|
-
|
|
6640
|
+
// Listen for input/change from custom tab content
|
|
6641
|
+
for (const modeName of Object.keys(this.#customSlots)) {
|
|
6642
|
+
if (builtinModes.includes(modeName)) continue;
|
|
6643
|
+
const container = this.#dialog.querySelector(
|
|
6644
|
+
`[data-tab="${modeName}"]`
|
|
6645
|
+
);
|
|
6646
|
+
if (!container) continue;
|
|
6647
|
+
container.addEventListener("input", (e) => {
|
|
6648
|
+
if (e.target === this) return;
|
|
6649
|
+
e.stopPropagation();
|
|
6650
|
+
if (e.detail) this.#customData[modeName] = e.detail;
|
|
6651
|
+
this.#emitInput();
|
|
6652
|
+
});
|
|
6653
|
+
container.addEventListener("change", (e) => {
|
|
6654
|
+
if (e.target === this) return;
|
|
6655
|
+
e.stopPropagation();
|
|
6656
|
+
if (e.detail) this.#customData[modeName] = e.detail;
|
|
6657
|
+
this.#emitChange();
|
|
6658
|
+
});
|
|
6560
6659
|
}
|
|
6660
|
+
}
|
|
6661
|
+
|
|
6662
|
+
#switchTab(tabName) {
|
|
6663
|
+
// Only allow switching to modes that have a tab container in the dialog
|
|
6664
|
+
const tab = this.#dialog?.querySelector(
|
|
6665
|
+
`.fig-fill-picker-tab[data-tab="${tabName}"]`
|
|
6666
|
+
);
|
|
6667
|
+
if (!tab) return;
|
|
6561
6668
|
|
|
6562
6669
|
this.#activeTab = tabName;
|
|
6563
6670
|
this.#fillType = tabName;
|
|
@@ -6578,6 +6685,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6578
6685
|
}
|
|
6579
6686
|
});
|
|
6580
6687
|
|
|
6688
|
+
// Zero out content padding for custom mode tabs
|
|
6689
|
+
const contentEl = this.#dialog.querySelector(".fig-fill-picker-content");
|
|
6690
|
+
if (contentEl) {
|
|
6691
|
+
contentEl.style.padding = this.#customSlots[tabName] ? "0" : "";
|
|
6692
|
+
}
|
|
6693
|
+
|
|
6581
6694
|
// Update tab-specific UI after visibility change
|
|
6582
6695
|
if (tabName === "gradient") {
|
|
6583
6696
|
// Use RAF to ensure layout is complete before updating angle input
|
|
@@ -6999,7 +7112,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
6999
7112
|
<fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
|
|
7000
7113
|
stop.position
|
|
7001
7114
|
}" units="%"></fig-input-number>
|
|
7002
|
-
<fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" dialog-position="right" value="${
|
|
7115
|
+
<fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" picker-dialog-position="right" value="${
|
|
7003
7116
|
stop.color
|
|
7004
7117
|
}"></fig-input-color>
|
|
7005
7118
|
<fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
|
|
@@ -7027,9 +7140,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
7027
7140
|
this.#emitInput();
|
|
7028
7141
|
});
|
|
7029
7142
|
|
|
7030
|
-
row
|
|
7031
|
-
|
|
7032
|
-
|
|
7143
|
+
const stopColor = row.querySelector(".fig-fill-picker-stop-color");
|
|
7144
|
+
const stopFillPicker = stopColor.querySelector("fig-fill-picker");
|
|
7145
|
+
if (stopFillPicker) {
|
|
7146
|
+
stopFillPicker.anchorElement = this.#dialog;
|
|
7147
|
+
} else {
|
|
7148
|
+
requestAnimationFrame(() => {
|
|
7149
|
+
const fp = stopColor.querySelector("fig-fill-picker");
|
|
7150
|
+
if (fp) fp.anchorElement = this.#dialog;
|
|
7151
|
+
});
|
|
7152
|
+
}
|
|
7153
|
+
|
|
7154
|
+
stopColor.addEventListener("input", (e) => {
|
|
7033
7155
|
this.#gradient.stops[index].color =
|
|
7034
7156
|
e.target.hexOpaque || e.target.value;
|
|
7035
7157
|
const parsedAlpha = parseFloat(e.target.alpha);
|
|
@@ -7723,7 +7845,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
7723
7845
|
image: { url: this.#webcam.snapshot, scaleMode: "fill", scale: 50 },
|
|
7724
7846
|
};
|
|
7725
7847
|
default:
|
|
7726
|
-
return base;
|
|
7848
|
+
return { ...base, ...this.#customData[this.#fillType] };
|
|
7727
7849
|
}
|
|
7728
7850
|
}
|
|
7729
7851
|
|
package/index.html
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
<link rel="stylesheet"
|
|
10
10
|
type="text/css"
|
|
11
11
|
href="fig.css">
|
|
12
|
+
<link rel="stylesheet"
|
|
13
|
+
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css">
|
|
12
14
|
<script src="fig.js"></script>
|
|
13
15
|
<style>
|
|
14
16
|
* {
|
|
@@ -161,6 +163,97 @@
|
|
|
161
163
|
color: var(--figma-color-text);
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
pre[class*="language-"] {
|
|
167
|
+
background: var(--figma-color-bg-secondary);
|
|
168
|
+
border: 1px solid var(--figma-color-border);
|
|
169
|
+
border-radius: 6px;
|
|
170
|
+
padding: 12px 16px;
|
|
171
|
+
margin: 0;
|
|
172
|
+
overflow-x: auto;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pre[class*="language-"]>code[class*="language-"] {
|
|
176
|
+
display: block;
|
|
177
|
+
background: transparent;
|
|
178
|
+
padding: 0;
|
|
179
|
+
font-family: "SF Mono", Monaco, Consolas, monospace;
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
color: var(--figma-color-text);
|
|
183
|
+
text-shadow: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
:root {
|
|
187
|
+
--code-token-comment: #6b7280;
|
|
188
|
+
--code-token-punctuation: #374151;
|
|
189
|
+
--code-token-primary: #b42318;
|
|
190
|
+
--code-token-string: #0f766e;
|
|
191
|
+
--code-token-operator: #1d4ed8;
|
|
192
|
+
--code-token-keyword: #6d28d9;
|
|
193
|
+
--code-token-function: #9a3412;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
html[style*="color-scheme: dark"] {
|
|
197
|
+
--code-token-comment: #8b93a7;
|
|
198
|
+
--code-token-punctuation: #c7cfdd;
|
|
199
|
+
--code-token-primary: #ff8f8f;
|
|
200
|
+
--code-token-string: #6dd3b6;
|
|
201
|
+
--code-token-operator: #7fb4ff;
|
|
202
|
+
--code-token-keyword: #b79bff;
|
|
203
|
+
--code-token-function: #ffc27a;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Keep token colors aligned with theme and improve contrast in light mode */
|
|
207
|
+
.token.comment,
|
|
208
|
+
.token.prolog,
|
|
209
|
+
.token.doctype,
|
|
210
|
+
.token.cdata {
|
|
211
|
+
color: var(--code-token-comment);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.token.punctuation {
|
|
215
|
+
color: var(--code-token-punctuation);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.token.property,
|
|
219
|
+
.token.tag,
|
|
220
|
+
.token.boolean,
|
|
221
|
+
.token.number,
|
|
222
|
+
.token.constant,
|
|
223
|
+
.token.symbol,
|
|
224
|
+
.token.deleted {
|
|
225
|
+
color: var(--code-token-primary);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.token.selector,
|
|
229
|
+
.token.attr-name,
|
|
230
|
+
.token.string,
|
|
231
|
+
.token.char,
|
|
232
|
+
.token.builtin,
|
|
233
|
+
.token.inserted {
|
|
234
|
+
color: var(--code-token-string);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.token.operator,
|
|
238
|
+
.token.entity,
|
|
239
|
+
.token.url,
|
|
240
|
+
.language-css .token.string,
|
|
241
|
+
.style .token.string {
|
|
242
|
+
color: var(--code-token-operator);
|
|
243
|
+
background: transparent;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.token.atrule,
|
|
247
|
+
.token.attr-value,
|
|
248
|
+
.token.keyword {
|
|
249
|
+
color: var(--code-token-keyword);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.token.function,
|
|
253
|
+
.token.class-name {
|
|
254
|
+
color: var(--code-token-function);
|
|
255
|
+
}
|
|
256
|
+
|
|
164
257
|
.toolbelt {
|
|
165
258
|
position: fixed;
|
|
166
259
|
bottom: 0.75rem;
|
|
@@ -1577,7 +1670,8 @@
|
|
|
1577
1670
|
</fig-field>
|
|
1578
1671
|
<fig-field direction="horizontal">
|
|
1579
1672
|
<label>Dropdown</label>
|
|
1580
|
-
<fig-dropdown full
|
|
1673
|
+
<fig-dropdown full
|
|
1674
|
+
experimental="modern">
|
|
1581
1675
|
<option>Option One</option>
|
|
1582
1676
|
<option>Option Two</option>
|
|
1583
1677
|
<option>Option Three</option>
|
|
@@ -1649,9 +1743,208 @@
|
|
|
1649
1743
|
</fig-fill-picker>
|
|
1650
1744
|
|
|
1651
1745
|
<h3>Custom Trigger</h3>
|
|
1746
|
+
<p class="description">Any child element can act as the trigger. If the child is not a
|
|
1747
|
+
<code>fig-chit</code>, it becomes click-only — the fill picker won't update its appearance.
|
|
1748
|
+
</p>
|
|
1749
|
+
|
|
1750
|
+
<h4>Button Trigger</h4>
|
|
1652
1751
|
<fig-fill-picker value='{"type":"solid","color":"#95E1D3"}'>
|
|
1653
1752
|
<fig-button>Edit Fill</fig-button>
|
|
1654
1753
|
</fig-fill-picker>
|
|
1754
|
+
<pre
|
|
1755
|
+
style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);"><fig-fill-picker value='{"type":"solid","color":"#95E1D3"}'>
|
|
1756
|
+
<fig-button>Edit Fill</fig-button>
|
|
1757
|
+
</fig-fill-picker></code></pre>
|
|
1758
|
+
|
|
1759
|
+
<h4>Icon Button Trigger</h4>
|
|
1760
|
+
<fig-fill-picker value='{"type":"solid","color":"#667eea"}'>
|
|
1761
|
+
<fig-button icon
|
|
1762
|
+
variant="ghost"
|
|
1763
|
+
title="Pick a fill">
|
|
1764
|
+
<span class="fig-mask-icon"
|
|
1765
|
+
style="--icon: var(--icon-eyedropper)"></span>
|
|
1766
|
+
</fig-button>
|
|
1767
|
+
</fig-fill-picker>
|
|
1768
|
+
<pre
|
|
1769
|
+
style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);"><fig-fill-picker value='{"type":"solid","color":"#667eea"}'>
|
|
1770
|
+
<fig-button icon variant="ghost" title="Pick a fill">
|
|
1771
|
+
<span class="fig-mask-icon" style="--icon: var(--icon-eyedropper)"></span>
|
|
1772
|
+
</fig-button>
|
|
1773
|
+
</fig-fill-picker></code></pre>
|
|
1774
|
+
|
|
1775
|
+
<h4>Inline Text Trigger</h4>
|
|
1776
|
+
<fig-fill-picker value='{"type":"solid","color":"#F38181"}'>
|
|
1777
|
+
<span style="cursor: pointer; text-decoration: underline; color: var(--figma-color-text-brand)">Change
|
|
1778
|
+
background</span>
|
|
1779
|
+
</fig-fill-picker>
|
|
1780
|
+
<pre
|
|
1781
|
+
style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);"><fig-fill-picker value='{"type":"solid","color":"#F38181"}'>
|
|
1782
|
+
<span style="cursor: pointer">Change background</span>
|
|
1783
|
+
</fig-fill-picker></code></pre>
|
|
1784
|
+
|
|
1785
|
+
<h4>Swatch with Custom Size</h4>
|
|
1786
|
+
<fig-fill-picker id="fill-picker-custom-swatch"
|
|
1787
|
+
value='{"type":"gradient","gradient":{"type":"linear","angle":135,"stops":[{"position":0,"color":"#f093fb","opacity":100},{"position":100,"color":"#f5576c","opacity":100}]}}'>
|
|
1788
|
+
<div id="custom-swatch-preview"
|
|
1789
|
+
style="width: 3rem; height: 3rem; border-radius: 0.5rem; cursor: pointer; background: linear-gradient(135deg, #f093fb, #f5576c)">
|
|
1790
|
+
</div>
|
|
1791
|
+
</fig-fill-picker>
|
|
1792
|
+
<pre
|
|
1793
|
+
style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);"><fig-fill-picker value='{"type":"gradient",...}'>
|
|
1794
|
+
<div style="width: 3rem; height: 3rem; border-radius: 0.5rem;
|
|
1795
|
+
cursor: pointer; background: linear-gradient(...)">
|
|
1796
|
+
</div>
|
|
1797
|
+
</fig-fill-picker></code></pre>
|
|
1798
|
+
<script>
|
|
1799
|
+
(() => {
|
|
1800
|
+
const picker = document.getElementById('fill-picker-custom-swatch');
|
|
1801
|
+
const swatch = document.getElementById('custom-swatch-preview');
|
|
1802
|
+
if (!picker || !swatch) return;
|
|
1803
|
+
|
|
1804
|
+
const getSizingForMedia = (scaleMode, scale) => {
|
|
1805
|
+
switch (scaleMode) {
|
|
1806
|
+
case 'fit':
|
|
1807
|
+
return { size: 'contain', position: 'center' };
|
|
1808
|
+
case 'tile':
|
|
1809
|
+
return { size: `${scale || 50}%`, position: 'top left' };
|
|
1810
|
+
case 'fill':
|
|
1811
|
+
case 'crop':
|
|
1812
|
+
default:
|
|
1813
|
+
return { size: 'cover', position: 'center' };
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
const updateSwatchPreview = (fill) => {
|
|
1818
|
+
if (!fill) return;
|
|
1819
|
+
|
|
1820
|
+
let background = 'transparent';
|
|
1821
|
+
let backgroundSize = 'cover';
|
|
1822
|
+
let backgroundPosition = 'center';
|
|
1823
|
+
|
|
1824
|
+
if (fill.type === 'solid') {
|
|
1825
|
+
background = fill.color || '#D9D9D9';
|
|
1826
|
+
} else if (fill.type === 'gradient') {
|
|
1827
|
+
background = fill.css || background;
|
|
1828
|
+
} else if (fill.type === 'image' && fill.image?.url) {
|
|
1829
|
+
background = `url(${fill.image.url})`;
|
|
1830
|
+
const sizing = getSizingForMedia(fill.image.scaleMode, fill.image.scale);
|
|
1831
|
+
backgroundSize = sizing.size;
|
|
1832
|
+
backgroundPosition = sizing.position;
|
|
1833
|
+
} else if (fill.type === 'video' && fill.video?.url) {
|
|
1834
|
+
background = `url(${fill.video.url})`;
|
|
1835
|
+
const sizing = getSizingForMedia(fill.video.scaleMode, fill.video.scale);
|
|
1836
|
+
backgroundSize = sizing.size;
|
|
1837
|
+
backgroundPosition = sizing.position;
|
|
1838
|
+
} else if (fill.type === 'webcam' && fill.image?.url) {
|
|
1839
|
+
background = `url(${fill.image.url})`;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
swatch.style.background = background;
|
|
1843
|
+
swatch.style.backgroundSize = backgroundSize;
|
|
1844
|
+
swatch.style.backgroundPosition = backgroundPosition;
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
picker.addEventListener('input', (e) => updateSwatchPreview(e.detail));
|
|
1848
|
+
picker.addEventListener('change', (e) => updateSwatchPreview(e.detail));
|
|
1849
|
+
updateSwatchPreview(picker.value);
|
|
1850
|
+
})();
|
|
1851
|
+
</script>
|
|
1852
|
+
|
|
1853
|
+
<h3>Custom Mode (Slots)</h3>
|
|
1854
|
+
<p class="description">Add custom modes via <code>slot="mode-{name}"</code> children. The mode name must
|
|
1855
|
+
also appear in the <code>mode</code> attribute. Custom slot content manages its own UI; dispatch
|
|
1856
|
+
<code>input</code>/<code>change</code> events with a <code>detail</code> payload to relay values back.
|
|
1857
|
+
</p>
|
|
1858
|
+
|
|
1859
|
+
<h4>Custom "Shader" Mode</h4>
|
|
1860
|
+
<fig-fill-picker mode="solid,shader"
|
|
1861
|
+
value='{"type":"solid","color":"#95E1D3"}'
|
|
1862
|
+
experimental="modern">
|
|
1863
|
+
<div slot="mode-shader"
|
|
1864
|
+
label="Shader">
|
|
1865
|
+
<fig-field label="Fragment Shader">
|
|
1866
|
+
<fig-input-text multiline
|
|
1867
|
+
placeholder="void main() { ... }"
|
|
1868
|
+
value="void main() { gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0); }"></fig-input-text>
|
|
1869
|
+
</fig-field>
|
|
1870
|
+
</div>
|
|
1871
|
+
</fig-fill-picker>
|
|
1872
|
+
<pre
|
|
1873
|
+
style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);"><fig-fill-picker mode="solid,shader">
|
|
1874
|
+
<div slot="mode-shader" label="Shader">
|
|
1875
|
+
<fig-field label="Fragment Shader">
|
|
1876
|
+
<fig-input-text multiline
|
|
1877
|
+
placeholder="void main() { ... }"
|
|
1878
|
+
value="..."></fig-input-text>
|
|
1879
|
+
</fig-field>
|
|
1880
|
+
</div>
|
|
1881
|
+
</fig-fill-picker></code></pre>
|
|
1882
|
+
|
|
1883
|
+
<h4>React Custom Mode (via <code>modeready</code>)</h4>
|
|
1884
|
+
<p class="description">Frameworks like React can listen for the <code>modeready</code> event and render
|
|
1885
|
+
directly into the provided container. The DOM is never moved — React owns its tree from the start.</p>
|
|
1886
|
+
<fig-fill-picker id="react-picker" mode="solid,react-demo">
|
|
1887
|
+
<div slot="mode-react-demo" label="React"></div>
|
|
1888
|
+
</fig-fill-picker>
|
|
1889
|
+
<script type="module">
|
|
1890
|
+
import React from 'https://esm.sh/react@18';
|
|
1891
|
+
import { createRoot } from 'https://esm.sh/react-dom@18/client';
|
|
1892
|
+
|
|
1893
|
+
const h = React.createElement;
|
|
1894
|
+
const picker = document.getElementById('react-picker');
|
|
1895
|
+
|
|
1896
|
+
picker.addEventListener('modeready', (e) => {
|
|
1897
|
+
if (e.detail.mode !== 'react-demo') return;
|
|
1898
|
+
|
|
1899
|
+
function ColorButtons() {
|
|
1900
|
+
const [color, setColor] = React.useState('#667eea');
|
|
1901
|
+
const colors = ['#FF6B6B', '#4ECDC4', '#667eea', '#f093fb', '#95E1D3'];
|
|
1902
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
1903
|
+
h('p', {
|
|
1904
|
+
style: {
|
|
1905
|
+
fontSize: 11,
|
|
1906
|
+
color: 'var(--figma-color-text-secondary)',
|
|
1907
|
+
margin: 0,
|
|
1908
|
+
}
|
|
1909
|
+
}, `Selected: ${color}`),
|
|
1910
|
+
h('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap' } },
|
|
1911
|
+
colors.map((c) =>
|
|
1912
|
+
h('button', {
|
|
1913
|
+
key: c,
|
|
1914
|
+
onClick: () => setColor(c),
|
|
1915
|
+
style: {
|
|
1916
|
+
width: 28,
|
|
1917
|
+
height: 28,
|
|
1918
|
+
borderRadius: 4,
|
|
1919
|
+
border: c === color ? '2px solid var(--figma-color-text)' : '1px solid var(--figma-color-border)',
|
|
1920
|
+
background: c,
|
|
1921
|
+
cursor: 'pointer',
|
|
1922
|
+
padding: 0,
|
|
1923
|
+
}
|
|
1924
|
+
})
|
|
1925
|
+
)
|
|
1926
|
+
)
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
createRoot(e.detail.container).render(h(ColorButtons));
|
|
1931
|
+
});
|
|
1932
|
+
</script>
|
|
1933
|
+
<pre
|
|
1934
|
+
style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);"><fig-fill-picker id="react-picker" mode="solid,react-demo">
|
|
1935
|
+
<div slot="mode-react-demo" label="React"></div>
|
|
1936
|
+
</fig-fill-picker>
|
|
1937
|
+
|
|
1938
|
+
<script type="module">
|
|
1939
|
+
import React from 'https://esm.sh/react@18';
|
|
1940
|
+
import { createRoot } from 'https://esm.sh/react-dom@18/client';
|
|
1941
|
+
|
|
1942
|
+
const picker = document.getElementById('react-picker');
|
|
1943
|
+
picker.addEventListener('modeready', (e) => {
|
|
1944
|
+
if (e.detail.mode !== 'react-demo') return;
|
|
1945
|
+
createRoot(e.detail.container).render(<MyComponent />);
|
|
1946
|
+
});
|
|
1947
|
+
</script></code></pre>
|
|
1655
1948
|
|
|
1656
1949
|
<h3>Without Alpha</h3>
|
|
1657
1950
|
<fig-fill-picker alpha="false"
|
|
@@ -3273,9 +3566,11 @@
|
|
|
3273
3566
|
</dialog></code></pre>
|
|
3274
3567
|
|
|
3275
3568
|
<h3>Viewport Margin</h3>
|
|
3276
|
-
<p class="description">Use <code>viewport-margin</code> to define safe areas the popup should avoid (e.g. a
|
|
3569
|
+
<p class="description">Use <code>viewport-margin</code> to define safe areas the popup should avoid (e.g. a
|
|
3570
|
+
bottom toolbar). Uses CSS margin shorthand: <code>top right bottom left</code>.</p>
|
|
3277
3571
|
<fig-button id="popup-open-viewport-margin"
|
|
3278
|
-
onclick="document.getElementById('popup-viewport-margin').open = true">Open (64px bottom
|
|
3572
|
+
onclick="document.getElementById('popup-viewport-margin').open = true">Open (64px bottom
|
|
3573
|
+
margin)</fig-button>
|
|
3279
3574
|
<dialog id="popup-viewport-margin"
|
|
3280
3575
|
is="fig-popup"
|
|
3281
3576
|
anchor="#popup-open-viewport-margin"
|
|
@@ -3284,7 +3579,9 @@
|
|
|
3284
3579
|
viewport-margin="8 8 64 8">
|
|
3285
3580
|
<vstack style="min-width: 14rem;">
|
|
3286
3581
|
<strong style="padding: 0 var(--spacer-1);">Viewport Margin</strong>
|
|
3287
|
-
<p
|
|
3582
|
+
<p
|
|
3583
|
+
style="padding: 0 var(--spacer-1); margin: 0; font-size: var(--font-size-small); color: var(--figma-color-text-secondary);">
|
|
3584
|
+
This popup won't overlap the bottom 64px of the viewport.</p>
|
|
3288
3585
|
<fig-input-text placeholder="Try scrolling down"></fig-input-text>
|
|
3289
3586
|
</vstack>
|
|
3290
3587
|
</dialog>
|
|
@@ -4564,7 +4861,50 @@ button.addEventListener('click', () => {
|
|
|
4564
4861
|
</section>
|
|
4565
4862
|
</div>
|
|
4566
4863
|
|
|
4864
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
|
|
4865
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markup.min.js"></script>
|
|
4866
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-css.min.js"></script>
|
|
4867
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-clike.min.js"></script>
|
|
4868
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"></script>
|
|
4567
4869
|
<script>
|
|
4870
|
+
function detectCodeLanguage(codeText) {
|
|
4871
|
+
const source = codeText.trim();
|
|
4872
|
+
if (!source) return 'javascript';
|
|
4873
|
+
|
|
4874
|
+
if (
|
|
4875
|
+
source.startsWith('<') ||
|
|
4876
|
+
source.startsWith('<') ||
|
|
4877
|
+
source.includes('</') ||
|
|
4878
|
+
source.includes('</')
|
|
4879
|
+
) {
|
|
4880
|
+
return 'markup';
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
if (
|
|
4884
|
+
/(^|[\n\r])\s*[@.#a-zA-Z][\w\s.#:[\]-]*\{/.test(source) ||
|
|
4885
|
+
/--[\w-]+\s*:/.test(source)
|
|
4886
|
+
) {
|
|
4887
|
+
return 'css';
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
return 'javascript';
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
function highlightCodeBlocks() {
|
|
4894
|
+
const blocks = document.querySelectorAll('pre > code');
|
|
4895
|
+
blocks.forEach((code) => {
|
|
4896
|
+
if (!code.className.includes('language-')) {
|
|
4897
|
+
const language = detectCodeLanguage(code.textContent || '');
|
|
4898
|
+
code.classList.add(`language-${language}`);
|
|
4899
|
+
code.parentElement?.classList.add(`language-${language}`);
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
|
|
4903
|
+
if (window.Prism) {
|
|
4904
|
+
window.Prism.highlightAll();
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4568
4908
|
// Highlight nav item based on hash
|
|
4569
4909
|
function updateActiveNav() {
|
|
4570
4910
|
const hash = location.hash || '#avatar';
|
|
@@ -4609,6 +4949,7 @@ button.addEventListener('click', () => {
|
|
|
4609
4949
|
|
|
4610
4950
|
// Initial state
|
|
4611
4951
|
window.addEventListener('load', () => {
|
|
4952
|
+
highlightCodeBlocks();
|
|
4612
4953
|
updateActiveNav();
|
|
4613
4954
|
if (location.hash) {
|
|
4614
4955
|
document.querySelector(location.hash)?.scrollIntoView();
|
package/package.json
CHANGED