@rogieking/figui3 2.24.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 +6 -0
- package/fig.js +191 -57
- package/index.html +343 -3
- package/package.json +1 -1
package/components.css
CHANGED
|
@@ -601,6 +601,7 @@ fig-dropdown,
|
|
|
601
601
|
display: flex;
|
|
602
602
|
align-items: center;
|
|
603
603
|
flex: 1;
|
|
604
|
+
width: 100%;
|
|
604
605
|
}
|
|
605
606
|
|
|
606
607
|
/* Chevron icon using mask-image for light-dark support */
|
|
@@ -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;
|
|
@@ -6223,6 +6269,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
6223
6269
|
#chit = null;
|
|
6224
6270
|
#dialog = null;
|
|
6225
6271
|
#activeTab = "solid";
|
|
6272
|
+
anchorElement = null;
|
|
6226
6273
|
|
|
6227
6274
|
// Fill state
|
|
6228
6275
|
#fillType = "solid";
|
|
@@ -6241,6 +6288,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
6241
6288
|
#video = { url: null, scaleMode: "fill", scale: 50 };
|
|
6242
6289
|
#webcam = { stream: null, snapshot: null };
|
|
6243
6290
|
|
|
6291
|
+
// Custom mode slots and data
|
|
6292
|
+
#customSlots = {};
|
|
6293
|
+
#customData = {};
|
|
6294
|
+
|
|
6244
6295
|
// DOM references for solid tab
|
|
6245
6296
|
#colorArea = null;
|
|
6246
6297
|
#colorAreaHandle = null;
|
|
@@ -6275,7 +6326,9 @@ class FigFillPicker extends HTMLElement {
|
|
|
6275
6326
|
}
|
|
6276
6327
|
|
|
6277
6328
|
#setupTrigger() {
|
|
6278
|
-
const child = this.
|
|
6329
|
+
const child = Array.from(this.children).find(
|
|
6330
|
+
(el) => !el.getAttribute("slot")?.startsWith("mode-")
|
|
6331
|
+
);
|
|
6279
6332
|
|
|
6280
6333
|
if (!child) {
|
|
6281
6334
|
// Scenario 1: Empty - create fig-chit
|
|
@@ -6315,6 +6368,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
6315
6368
|
const valueAttr = this.getAttribute("value");
|
|
6316
6369
|
if (!valueAttr) return;
|
|
6317
6370
|
|
|
6371
|
+
const builtinTypes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6372
|
+
|
|
6318
6373
|
try {
|
|
6319
6374
|
const parsed = JSON.parse(valueAttr);
|
|
6320
6375
|
if (parsed.type) this.#fillType = parsed.type;
|
|
@@ -6337,6 +6392,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6337
6392
|
this.#gradient = { ...this.#gradient, ...parsed.gradient };
|
|
6338
6393
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
6339
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
|
+
}
|
|
6340
6401
|
} catch (e) {
|
|
6341
6402
|
// If not JSON, treat as hex color
|
|
6342
6403
|
if (valueAttr.startsWith("#")) {
|
|
@@ -6387,7 +6448,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
6387
6448
|
}
|
|
6388
6449
|
break;
|
|
6389
6450
|
default:
|
|
6390
|
-
|
|
6451
|
+
const slot = this.#customSlots[this.#fillType];
|
|
6452
|
+
bg = slot?.element?.getAttribute("chit-background") || "#D9D9D9";
|
|
6391
6453
|
}
|
|
6392
6454
|
|
|
6393
6455
|
this.#chit.setAttribute("background", bg);
|
|
@@ -6434,6 +6496,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
6434
6496
|
}
|
|
6435
6497
|
|
|
6436
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
|
+
|
|
6437
6511
|
this.#dialog = document.createElement("dialog", { is: "fig-popup" });
|
|
6438
6512
|
this.#dialog.setAttribute("is", "fig-popup");
|
|
6439
6513
|
this.#dialog.setAttribute("drag", "true");
|
|
@@ -6441,14 +6515,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6441
6515
|
this.#dialog.setAttribute("autoresize", "false");
|
|
6442
6516
|
this.#dialog.classList.add("fig-fill-picker-dialog");
|
|
6443
6517
|
|
|
6444
|
-
this.#dialog.anchor = this.#trigger;
|
|
6518
|
+
this.#dialog.anchor = this.anchorElement || this.#trigger;
|
|
6445
6519
|
const dialogPosition = this.getAttribute("dialog-position") || "left";
|
|
6446
6520
|
this.#dialog.setAttribute("position", dialogPosition);
|
|
6447
6521
|
|
|
6448
|
-
|
|
6449
|
-
const
|
|
6450
|
-
const allModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6451
|
-
const modeLabels = {
|
|
6522
|
+
const builtinModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6523
|
+
const builtinLabels = {
|
|
6452
6524
|
solid: "Solid",
|
|
6453
6525
|
gradient: "Gradient",
|
|
6454
6526
|
image: "Image",
|
|
@@ -6456,24 +6528,33 @@ class FigFillPicker extends HTMLElement {
|
|
|
6456
6528
|
webcam: "Webcam",
|
|
6457
6529
|
};
|
|
6458
6530
|
|
|
6459
|
-
//
|
|
6460
|
-
|
|
6531
|
+
// Build allowed modes: built-ins filtered normally, custom names accepted if slot exists
|
|
6532
|
+
const mode = this.getAttribute("mode");
|
|
6533
|
+
let allowedModes;
|
|
6461
6534
|
if (mode) {
|
|
6462
|
-
const
|
|
6463
|
-
allowedModes =
|
|
6464
|
-
|
|
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;
|
|
6465
6548
|
}
|
|
6466
6549
|
|
|
6467
|
-
// If current fillType not in allowed modes, switch to first allowed
|
|
6468
6550
|
if (!allowedModes.includes(this.#fillType)) {
|
|
6469
6551
|
this.#fillType = allowedModes[0];
|
|
6470
6552
|
this.#activeTab = allowedModes[0];
|
|
6471
6553
|
}
|
|
6472
6554
|
|
|
6473
|
-
// Build header content - label if single mode, dropdown if multiple
|
|
6474
6555
|
const experimental = this.getAttribute("experimental");
|
|
6475
6556
|
const expAttr = experimental ? `experimental="${experimental}"` : "";
|
|
6476
|
-
|
|
6557
|
+
|
|
6477
6558
|
let headerContent;
|
|
6478
6559
|
if (allowedModes.length === 1) {
|
|
6479
6560
|
headerContent = `<h3 class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</h3>`;
|
|
@@ -6486,6 +6567,11 @@ class FigFillPicker extends HTMLElement {
|
|
|
6486
6567
|
</fig-dropdown>`;
|
|
6487
6568
|
}
|
|
6488
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
|
+
|
|
6489
6575
|
this.#dialog.innerHTML = `
|
|
6490
6576
|
<fig-header>
|
|
6491
6577
|
${headerContent}
|
|
@@ -6494,16 +6580,33 @@ class FigFillPicker extends HTMLElement {
|
|
|
6494
6580
|
</fig-button>
|
|
6495
6581
|
</fig-header>
|
|
6496
6582
|
<div class="fig-fill-picker-content">
|
|
6497
|
-
|
|
6498
|
-
<div class="fig-fill-picker-tab" data-tab="gradient"></div>
|
|
6499
|
-
<div class="fig-fill-picker-tab" data-tab="image"></div>
|
|
6500
|
-
<div class="fig-fill-picker-tab" data-tab="video"></div>
|
|
6501
|
-
<div class="fig-fill-picker-tab" data-tab="webcam"></div>
|
|
6583
|
+
${tabDivs}
|
|
6502
6584
|
</div>
|
|
6503
6585
|
`;
|
|
6504
6586
|
|
|
6505
6587
|
document.body.appendChild(this.#dialog);
|
|
6506
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
|
+
|
|
6507
6610
|
// Setup type dropdown switching (only if not locked)
|
|
6508
6611
|
const typeDropdown = this.#dialog.querySelector(".fig-fill-picker-type");
|
|
6509
6612
|
if (typeDropdown) {
|
|
@@ -6518,34 +6621,50 @@ class FigFillPicker extends HTMLElement {
|
|
|
6518
6621
|
this.#dialog.open = false;
|
|
6519
6622
|
});
|
|
6520
6623
|
|
|
6521
|
-
// Emit change on close
|
|
6522
6624
|
this.#dialog.addEventListener("close", () => {
|
|
6523
6625
|
this.#emitChange();
|
|
6524
6626
|
});
|
|
6525
6627
|
|
|
6526
|
-
// Initialize tabs
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
const mode = this.getAttribute("mode");
|
|
6537
|
-
const allModes = ["solid", "gradient", "image", "video", "webcam"];
|
|
6538
|
-
|
|
6539
|
-
let allowedModes = allModes;
|
|
6540
|
-
if (mode) {
|
|
6541
|
-
const requestedModes = mode.split(",").map((m) => m.trim().toLowerCase());
|
|
6542
|
-
allowedModes = requestedModes.filter((m) => allModes.includes(m));
|
|
6543
|
-
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();
|
|
6544
6638
|
}
|
|
6545
6639
|
|
|
6546
|
-
|
|
6547
|
-
|
|
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
|
+
});
|
|
6548
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;
|
|
6549
6668
|
|
|
6550
6669
|
this.#activeTab = tabName;
|
|
6551
6670
|
this.#fillType = tabName;
|
|
@@ -6566,6 +6685,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
6566
6685
|
}
|
|
6567
6686
|
});
|
|
6568
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
|
+
|
|
6569
6694
|
// Update tab-specific UI after visibility change
|
|
6570
6695
|
if (tabName === "gradient") {
|
|
6571
6696
|
// Use RAF to ensure layout is complete before updating angle input
|
|
@@ -6987,7 +7112,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
6987
7112
|
<fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
|
|
6988
7113
|
stop.position
|
|
6989
7114
|
}" units="%"></fig-input-number>
|
|
6990
|
-
<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="${
|
|
6991
7116
|
stop.color
|
|
6992
7117
|
}"></fig-input-color>
|
|
6993
7118
|
<fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
|
|
@@ -7015,9 +7140,18 @@ class FigFillPicker extends HTMLElement {
|
|
|
7015
7140
|
this.#emitInput();
|
|
7016
7141
|
});
|
|
7017
7142
|
|
|
7018
|
-
row
|
|
7019
|
-
|
|
7020
|
-
|
|
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) => {
|
|
7021
7155
|
this.#gradient.stops[index].color =
|
|
7022
7156
|
e.target.hexOpaque || e.target.value;
|
|
7023
7157
|
const parsedAlpha = parseFloat(e.target.alpha);
|
|
@@ -7711,7 +7845,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
7711
7845
|
image: { url: this.#webcam.snapshot, scaleMode: "fill", scale: 50 },
|
|
7712
7846
|
};
|
|
7713
7847
|
default:
|
|
7714
|
-
return base;
|
|
7848
|
+
return { ...base, ...this.#customData[this.#fillType] };
|
|
7715
7849
|
}
|
|
7716
7850
|
}
|
|
7717
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;
|
|
@@ -1650,9 +1743,208 @@
|
|
|
1650
1743
|
</fig-fill-picker>
|
|
1651
1744
|
|
|
1652
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>
|
|
1653
1751
|
<fig-fill-picker value='{"type":"solid","color":"#95E1D3"}'>
|
|
1654
1752
|
<fig-button>Edit Fill</fig-button>
|
|
1655
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>
|
|
1656
1948
|
|
|
1657
1949
|
<h3>Without Alpha</h3>
|
|
1658
1950
|
<fig-fill-picker alpha="false"
|
|
@@ -3274,9 +3566,11 @@
|
|
|
3274
3566
|
</dialog></code></pre>
|
|
3275
3567
|
|
|
3276
3568
|
<h3>Viewport Margin</h3>
|
|
3277
|
-
<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>
|
|
3278
3571
|
<fig-button id="popup-open-viewport-margin"
|
|
3279
|
-
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>
|
|
3280
3574
|
<dialog id="popup-viewport-margin"
|
|
3281
3575
|
is="fig-popup"
|
|
3282
3576
|
anchor="#popup-open-viewport-margin"
|
|
@@ -3285,7 +3579,9 @@
|
|
|
3285
3579
|
viewport-margin="8 8 64 8">
|
|
3286
3580
|
<vstack style="min-width: 14rem;">
|
|
3287
3581
|
<strong style="padding: 0 var(--spacer-1);">Viewport Margin</strong>
|
|
3288
|
-
<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>
|
|
3289
3585
|
<fig-input-text placeholder="Try scrolling down"></fig-input-text>
|
|
3290
3586
|
</vstack>
|
|
3291
3587
|
</dialog>
|
|
@@ -4565,7 +4861,50 @@ button.addEventListener('click', () => {
|
|
|
4565
4861
|
</section>
|
|
4566
4862
|
</div>
|
|
4567
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>
|
|
4568
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
|
+
|
|
4569
4908
|
// Highlight nav item based on hash
|
|
4570
4909
|
function updateActiveNav() {
|
|
4571
4910
|
const hash = location.hash || '#avatar';
|
|
@@ -4610,6 +4949,7 @@ button.addEventListener('click', () => {
|
|
|
4610
4949
|
|
|
4611
4950
|
// Initial state
|
|
4612
4951
|
window.addEventListener('load', () => {
|
|
4952
|
+
highlightCodeBlocks();
|
|
4613
4953
|
updateActiveNav();
|
|
4614
4954
|
if (location.hash) {
|
|
4615
4955
|
document.querySelector(location.hash)?.scrollIntoView();
|
package/package.json
CHANGED