@rogieking/figui3 3.20.3 → 3.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fig.js CHANGED
@@ -5053,6 +5053,136 @@ function gradientInterpolationClause(gradient) {
5053
5053
  return `in ${normalized.interpolationSpace}`;
5054
5054
  }
5055
5055
 
5056
+ function figHexToRGB(hex) {
5057
+ const h = hex.replace(/^#/, "");
5058
+ return {
5059
+ r: parseInt(h.substring(0, 2), 16),
5060
+ g: parseInt(h.substring(2, 4), 16),
5061
+ b: parseInt(h.substring(4, 6), 16),
5062
+ };
5063
+ }
5064
+
5065
+ function figRGBToLinear(c) {
5066
+ const s = c / 255;
5067
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
5068
+ }
5069
+
5070
+ function figLinearToSRGB(c) {
5071
+ const v = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
5072
+ return Math.round(Math.max(0, Math.min(1, v)) * 255);
5073
+ }
5074
+
5075
+ function figRGBToOklab(r, g, b) {
5076
+ const lr = figRGBToLinear(r);
5077
+ const lg = figRGBToLinear(g);
5078
+ const lb = figRGBToLinear(b);
5079
+ const l_ = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
5080
+ const m_ = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
5081
+ const s_ = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
5082
+ return {
5083
+ l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
5084
+ a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
5085
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
5086
+ };
5087
+ }
5088
+
5089
+ function figOklabToRGB(L, a, b) {
5090
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
5091
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
5092
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
5093
+ const l = l_ * l_ * l_;
5094
+ const m = m_ * m_ * m_;
5095
+ const s = s_ * s_ * s_;
5096
+ return {
5097
+ r: figLinearToSRGB(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
5098
+ g: figLinearToSRGB(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
5099
+ b: figLinearToSRGB(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
5100
+ };
5101
+ }
5102
+
5103
+ function figOklabToOklch(L, a, b) {
5104
+ return { l: L, c: Math.sqrt(a * a + b * b), h: (Math.atan2(b, a) * 180) / Math.PI };
5105
+ }
5106
+
5107
+ function figOklchToOklab(l, c, h) {
5108
+ const hRad = (h * Math.PI) / 180;
5109
+ return { l, a: c * Math.cos(hRad), b: c * Math.sin(hRad) };
5110
+ }
5111
+
5112
+ function figInterpolateHue(h1, h2, t, mode) {
5113
+ let a = ((h1 % 360) + 360) % 360;
5114
+ let b = ((h2 % 360) + 360) % 360;
5115
+ let diff = b - a;
5116
+ switch (mode) {
5117
+ case "longer":
5118
+ if (diff > 0 && diff < 180) diff -= 360;
5119
+ else if (diff < 0 && diff > -180) diff += 360;
5120
+ else if (diff === 0) diff = 0;
5121
+ break;
5122
+ case "increasing":
5123
+ if (diff < 0) diff += 360;
5124
+ break;
5125
+ case "decreasing":
5126
+ if (diff > 0) diff -= 360;
5127
+ break;
5128
+ default:
5129
+ if (diff > 180) diff -= 360;
5130
+ else if (diff < -180) diff += 360;
5131
+ break;
5132
+ }
5133
+ return ((a + diff * t) % 360 + 360) % 360;
5134
+ }
5135
+
5136
+ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolation) {
5137
+ const sorted = [...stops].sort((a, b) => a.position - b.position);
5138
+ const pos = position * 100;
5139
+ if (sorted.length === 0) return "#888888";
5140
+ if (pos <= sorted[0].position) return sorted[0].color;
5141
+ if (pos >= sorted[sorted.length - 1].position) return sorted[sorted.length - 1].color;
5142
+
5143
+ let i = 0;
5144
+ while (i < sorted.length - 1 && sorted[i + 1].position < pos) i++;
5145
+ const s1 = sorted[i];
5146
+ const s2 = sorted[i + 1];
5147
+ const range = s2.position - s1.position;
5148
+ const t = range > 0 ? (pos - s1.position) / range : 0;
5149
+
5150
+ const c1 = figHexToRGB(s1.color);
5151
+ const c2 = figHexToRGB(s2.color);
5152
+
5153
+ let r, g, b;
5154
+ const space = interpolationSpace || "oklab";
5155
+
5156
+ if (space === "srgb-linear") {
5157
+ const lr1 = figRGBToLinear(c1.r), lg1 = figRGBToLinear(c1.g), lb1 = figRGBToLinear(c1.b);
5158
+ const lr2 = figRGBToLinear(c2.r), lg2 = figRGBToLinear(c2.g), lb2 = figRGBToLinear(c2.b);
5159
+ r = figLinearToSRGB(lr1 + (lr2 - lr1) * t);
5160
+ g = figLinearToSRGB(lg1 + (lg2 - lg1) * t);
5161
+ b = figLinearToSRGB(lb1 + (lb2 - lb1) * t);
5162
+ } else if (space === "oklch") {
5163
+ const lab1 = figRGBToOklab(c1.r, c1.g, c1.b);
5164
+ const lab2 = figRGBToOklab(c2.r, c2.g, c2.b);
5165
+ const lch1 = figOklabToOklch(lab1.l, lab1.a, lab1.b);
5166
+ const lch2 = figOklabToOklch(lab2.l, lab2.a, lab2.b);
5167
+ const L = lch1.l + (lch2.l - lch1.l) * t;
5168
+ const C = lch1.c + (lch2.c - lch1.c) * t;
5169
+ const H = figInterpolateHue(lch1.h, lch2.h, t, hueInterpolation || "shorter");
5170
+ const lab = figOklchToOklab(L, C, H);
5171
+ const rgb = figOklabToRGB(lab.l, lab.a, lab.b);
5172
+ r = rgb.r; g = rgb.g; b = rgb.b;
5173
+ } else {
5174
+ const lab1 = figRGBToOklab(c1.r, c1.g, c1.b);
5175
+ const lab2 = figRGBToOklab(c2.r, c2.g, c2.b);
5176
+ const L = lab1.l + (lab2.l - lab1.l) * t;
5177
+ const a = lab1.a + (lab2.a - lab1.a) * t;
5178
+ const bv = lab1.b + (lab2.b - lab1.b) * t;
5179
+ const rgb = figOklabToRGB(L, a, bv);
5180
+ r = rgb.r; g = rgb.g; b = rgb.b;
5181
+ }
5182
+
5183
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
5184
+ }
5185
+
5056
5186
  function hslToP3(h, s, l) {
5057
5187
  const sRGB = hslToSRGB(h, s, l);
5058
5188
  return sRGB.map((c) => +(c / 255).toFixed(4));
@@ -5259,14 +5389,15 @@ class FigInputFill extends HTMLElement {
5259
5389
  ${opacityHtml(Math.round(this.#solid.alpha * 100))}`;
5260
5390
  break;
5261
5391
 
5262
- case "gradient":
5392
+ case "gradient": {
5263
5393
  const gradientLabel =
5264
5394
  this.#gradient.type.charAt(0).toUpperCase() +
5265
5395
  this.#gradient.type.slice(1);
5266
5396
  controlsHtml = `
5267
5397
  <label class="fig-input-fill-label">${gradientLabel}</label>
5268
- ${opacityHtml(this.#gradient.stops[0]?.opacity ?? 100)}`;
5398
+ ${opacityHtml(100)}`;
5269
5399
  break;
5400
+ }
5270
5401
 
5271
5402
  case "image":
5272
5403
  controlsHtml = `
@@ -5399,10 +5530,6 @@ class FigInputFill extends HTMLElement {
5399
5530
  this.#solid.alpha = alpha;
5400
5531
  break;
5401
5532
  case "gradient":
5402
- // Apply to all stops
5403
- this.#gradient.stops.forEach((stop) => {
5404
- stop.opacity = opacity;
5405
- });
5406
5533
  break;
5407
5534
  case "image":
5408
5535
  this.#image.opacity = alpha;
@@ -5445,12 +5572,6 @@ class FigInputFill extends HTMLElement {
5445
5572
  }
5446
5573
  break;
5447
5574
  case "gradient": {
5448
- if (this.#opacityInput) {
5449
- this.#opacityInput.setAttribute(
5450
- "value",
5451
- this.#gradient.stops[0]?.opacity ?? 100,
5452
- );
5453
- }
5454
5575
  const label = this.querySelector(".fig-input-fill-label");
5455
5576
  if (label) {
5456
5577
  const newLabel =
@@ -5527,7 +5648,7 @@ class FigInputFill extends HTMLElement {
5527
5648
  </fig-input-number>
5528
5649
  </fig-tooltip>`;
5529
5650
  break;
5530
- case "gradient":
5651
+ case "gradient": {
5531
5652
  const gradientLabel =
5532
5653
  this.#gradient.type.charAt(0).toUpperCase() +
5533
5654
  this.#gradient.type.slice(1);
@@ -5539,12 +5660,13 @@ class FigInputFill extends HTMLElement {
5539
5660
  placeholder="##"
5540
5661
  min="0"
5541
5662
  max="100"
5542
- value="${this.#gradient.stops[0]?.opacity ?? 100}"
5663
+ value="100"
5543
5664
  units="%"
5544
5665
  ${disabled ? "disabled" : ""}>
5545
5666
  </fig-input-number>
5546
5667
  </fig-tooltip>`;
5547
5668
  break;
5669
+ }
5548
5670
  case "image":
5549
5671
  controlsHtml = `
5550
5672
  <label class="fig-input-fill-label">Image</label>
@@ -5638,9 +5760,6 @@ class FigInputFill extends HTMLElement {
5638
5760
  this.#solid.alpha = alpha;
5639
5761
  break;
5640
5762
  case "gradient":
5641
- this.#gradient.stops.forEach((stop) => {
5642
- stop.opacity = opacity;
5643
- });
5644
5763
  break;
5645
5764
  case "image":
5646
5765
  this.#image.opacity = alpha;
@@ -6124,13 +6243,24 @@ class FigInputGradient extends HTMLElement {
6124
6243
  }
6125
6244
 
6126
6245
  static get observedAttributes() {
6127
- return ["value", "disabled"];
6246
+ return ["value", "disabled", "edit"];
6247
+ }
6248
+
6249
+ get #editMode() {
6250
+ const attr = this.getAttribute("edit");
6251
+ if (attr === "false") return "false";
6252
+ if (attr === "picker") return "picker";
6253
+ return "true";
6254
+ }
6255
+
6256
+ get #isEditable() {
6257
+ return this.#editMode === "true";
6128
6258
  }
6129
6259
 
6130
6260
  connectedCallback() {
6131
6261
  this.#parseValue();
6132
6262
  this.#render();
6133
- document.addEventListener("keydown", this.#onKeyDown);
6263
+ if (this.#isEditable) document.addEventListener("keydown", this.#onKeyDown);
6134
6264
  }
6135
6265
 
6136
6266
  disconnectedCallback() {
@@ -6251,8 +6381,23 @@ class FigInputGradient extends HTMLElement {
6251
6381
  const sorted = [...this.#gradient.stops].sort(
6252
6382
  (a, b) => a.position - b.position,
6253
6383
  );
6254
- const stops = sorted.map((s) => `${s.color} ${s.position}%`).join(", ");
6255
- return `linear-gradient(${this.#gradient.angle}deg, ${stops})`;
6384
+ const stops = sorted
6385
+ .map((s) => {
6386
+ const alpha = (s.opacity ?? 100) / 100;
6387
+ if (alpha >= 1) return `${s.color} ${s.position}%`;
6388
+ const { r, g, b } = figHexToRGB(s.color);
6389
+ return `rgba(${r}, ${g}, ${b}, ${alpha}) ${s.position}%`;
6390
+ })
6391
+ .join(", ");
6392
+ const interp = gradientInterpolationClause(this.#gradient);
6393
+ return `linear-gradient(${this.#gradient.angle}deg ${interp}, ${stops})`;
6394
+ }
6395
+
6396
+ #stopColorCSS(stop) {
6397
+ const alpha = (stop.opacity ?? 100) / 100;
6398
+ if (alpha >= 1) return stop.color;
6399
+ const { r, g, b } = figHexToRGB(stop.color);
6400
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
6256
6401
  }
6257
6402
 
6258
6403
  #buildStopHandles() {
@@ -6260,7 +6405,7 @@ class FigInputGradient extends HTMLElement {
6260
6405
  return this.#gradient.stops
6261
6406
  .map(
6262
6407
  (stop, i) =>
6263
- `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color" color="${stop.color}" value="${stop.position}% 50%" hit-area="4" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
6408
+ `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color" color="${this.#stopColorCSS(stop)}" value="${stop.position}% 50%" hit-area="4" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
6264
6409
  )
6265
6410
  .join("");
6266
6411
  }
@@ -6270,31 +6415,69 @@ class FigInputGradient extends HTMLElement {
6270
6415
 
6271
6416
  #render() {
6272
6417
  const disabled = this.hasAttribute("disabled");
6418
+ const mode = this.#editMode;
6419
+
6420
+ if (mode === "picker") {
6421
+ const experimental = this.getAttribute("experimental");
6422
+ const expAttr = experimental ? ` experimental="${experimental}"` : "";
6423
+ const gradientValue = JSON.stringify(this.value);
6424
+ this.innerHTML = `
6425
+ <fig-fill-picker mode="gradient"${expAttr} value='${gradientValue}'${disabled ? " disabled" : ""}>
6426
+ <fig-chit size="medium" background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
6427
+ </fig-fill-picker>`;
6428
+ this.#chit = this.querySelector("fig-chit");
6429
+ this.#track = null;
6430
+ this.#setupPickerEvents();
6431
+ return;
6432
+ }
6433
+
6273
6434
  this.innerHTML = `
6274
6435
  <fig-chit size="medium" background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
6275
- <div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>`;
6436
+ ${mode === "true" ? `<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>` : ""}`;
6276
6437
  this.#chit = this.querySelector("fig-chit");
6277
6438
  this.#track = this.querySelector(".fig-input-gradient-track");
6278
- this.#setupGhostHandle();
6279
- this.#setupEventListeners();
6439
+
6440
+ if (mode === "true") {
6441
+ this.#setupGhostHandle();
6442
+ this.#setupEventListeners();
6443
+ requestAnimationFrame(() => this.#repositionHandles());
6444
+ }
6445
+ }
6446
+
6447
+ #setupPickerEvents() {
6448
+ const picker = this.querySelector("fig-fill-picker");
6449
+ if (!picker) return;
6450
+ picker.anchorElement = this;
6451
+
6452
+ const syncFromPicker = (e) => {
6453
+ e.stopPropagation();
6454
+ const detail = e.detail;
6455
+ if (!detail?.gradient) return;
6456
+ this.#gradient = normalizeGradientConfig({
6457
+ ...this.#gradient,
6458
+ ...detail.gradient,
6459
+ });
6460
+ this.#syncChit();
6461
+ };
6462
+
6463
+ picker.addEventListener("input", (e) => {
6464
+ syncFromPicker(e);
6465
+ this.#emitInput();
6466
+ });
6467
+
6468
+ picker.addEventListener("change", (e) => {
6469
+ syncFromPicker(e);
6470
+ this.#emitChange();
6471
+ });
6280
6472
  }
6281
6473
 
6282
6474
  #sampleGradientColor(position) {
6283
- const { ctx } = figGetSharedCanvas(256, 1);
6284
- ctx.clearRect(0, 0, 256, 1);
6285
- const grad = ctx.createLinearGradient(0, 0, 256, 0);
6286
- for (const stop of this.#gradient.stops) {
6287
- try {
6288
- grad.addColorStop(stop.position / 100, stop.color);
6289
- } catch {
6290
- /* skip invalid */
6291
- }
6292
- }
6293
- ctx.fillStyle = grad;
6294
- ctx.fillRect(0, 0, 256, 1);
6295
- const px = Math.round(Math.max(0, Math.min(255, position * 255)));
6296
- const [r, g, b] = ctx.getImageData(px, 0, 1, 1).data;
6297
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
6475
+ return figSampleGradientAt(
6476
+ this.#gradient.stops,
6477
+ position,
6478
+ this.#gradient.interpolationSpace,
6479
+ this.#gradient.hueInterpolation,
6480
+ );
6298
6481
  }
6299
6482
 
6300
6483
  #setupGhostHandle() {
@@ -6430,6 +6613,18 @@ class FigInputGradient extends HTMLElement {
6430
6613
  });
6431
6614
  };
6432
6615
 
6616
+ #repositionHandles() {
6617
+ if (!this.#track) return;
6618
+ const stops = this.#gradient.stops;
6619
+ this.#track
6620
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
6621
+ .forEach((h, i) => {
6622
+ if (i >= stops.length) return;
6623
+ h.removeAttribute("value");
6624
+ h.setAttribute("value", `${stops[i].position}% 50%`);
6625
+ });
6626
+ }
6627
+
6433
6628
  #syncHandles() {
6434
6629
  if (!this.#track) return;
6435
6630
  const handles = this.#track.querySelectorAll(
@@ -6442,6 +6637,7 @@ class FigInputGradient extends HTMLElement {
6442
6637
  this.#track.innerHTML = this.#buildStopHandles();
6443
6638
  if (ghost) this.#track.appendChild(ghost);
6444
6639
  this.#reobserveHandleColors();
6640
+ requestAnimationFrame(() => this.#repositionHandles());
6445
6641
  return;
6446
6642
  }
6447
6643
 
@@ -6450,7 +6646,7 @@ class FigInputGradient extends HTMLElement {
6450
6646
  const stop = stops[i];
6451
6647
  h.dataset.stopIndex = i;
6452
6648
  h.setAttribute("value", `${stop.position}% 50%`);
6453
- h.setAttribute("color", stop.color);
6649
+ h.setAttribute("color", this.#stopColorCSS(stop));
6454
6650
  const tip = h.closest("fig-tooltip");
6455
6651
  if (tip) tip.setAttribute("text", `${Math.round(stop.position)}%`);
6456
6652
  }
@@ -6567,6 +6763,10 @@ class FigInputGradient extends HTMLElement {
6567
6763
  const idx = parseInt(handle.dataset.stopIndex, 10);
6568
6764
  if (!isNaN(idx) && this.#gradient.stops[idx]) {
6569
6765
  this.#gradient.stops[idx].color = e.detail.color;
6766
+ if (e.detail.opacity !== undefined) {
6767
+ this.#gradient.stops[idx].opacity = e.detail.opacity;
6768
+ }
6769
+ handle.setAttribute("color", this.#stopColorCSS(this.#gradient.stops[idx]));
6570
6770
  this.#syncChit();
6571
6771
  this.#emitInput();
6572
6772
  }
@@ -6618,6 +6818,10 @@ class FigInputGradient extends HTMLElement {
6618
6818
  const idx = parseInt(handle.dataset.stopIndex, 10);
6619
6819
  if (!isNaN(idx) && this.#gradient.stops[idx]) {
6620
6820
  this.#gradient.stops[idx].color = e.detail.color;
6821
+ if (e.detail.opacity !== undefined) {
6822
+ this.#gradient.stops[idx].opacity = e.detail.opacity;
6823
+ }
6824
+ handle.setAttribute("color", this.#stopColorCSS(this.#gradient.stops[idx]));
6621
6825
  this.#syncChit();
6622
6826
  this.#emitChange();
6623
6827
  }
@@ -6659,7 +6863,8 @@ class FigInputGradient extends HTMLElement {
6659
6863
  const idx = parseInt(handle.dataset.stopIndex, 10);
6660
6864
  if (isNaN(idx) || !this.#gradient.stops[idx]) continue;
6661
6865
  const newColor = handle.getAttribute("color");
6662
- if (newColor && newColor !== this.#gradient.stops[idx].color) {
6866
+ if (!newColor || !newColor.startsWith("#")) continue;
6867
+ if (newColor !== this.#gradient.stops[idx].color) {
6663
6868
  this.#gradient.stops[idx].color = newColor;
6664
6869
  this.#syncChit();
6665
6870
  this.#emitInput();
@@ -6720,6 +6925,14 @@ class FigInputGradient extends HTMLElement {
6720
6925
  case "disabled":
6721
6926
  this.#syncDisabled();
6722
6927
  break;
6928
+ case "edit":
6929
+ this.#render();
6930
+ if (this.#isEditable) {
6931
+ document.addEventListener("keydown", this.#onKeyDown);
6932
+ } else {
6933
+ document.removeEventListener("keydown", this.#onKeyDown);
6934
+ }
6935
+ break;
6723
6936
  }
6724
6937
  }
6725
6938
 
@@ -7377,8 +7590,8 @@ class FigChit extends HTMLElement {
7377
7590
  }
7378
7591
  }
7379
7592
 
7380
- // Always update CSS variable with raw value so vars stay reactive
7381
- this.style.setProperty("--chit-background", rawBg);
7593
+ const isImage = /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(rawBg);
7594
+ this.style.setProperty("--chit-background", isImage ? rawBg : `linear-gradient(${rawBg}, ${rawBg})`);
7382
7595
  }
7383
7596
 
7384
7597
  #handleInput(e) {
@@ -7404,9 +7617,9 @@ class FigChit extends HTMLElement {
7404
7617
  attributeChangedCallback(name, oldValue, newValue) {
7405
7618
  if (oldValue === newValue) return;
7406
7619
  if (name === "background") {
7407
- // Skip full re-render if this was triggered by internal input
7408
7620
  if (this.#internalUpdate) {
7409
- this.style.setProperty("--chit-background", newValue);
7621
+ const isImg = /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(newValue);
7622
+ this.style.setProperty("--chit-background", isImg ? newValue : `linear-gradient(${newValue}, ${newValue})`);
7410
7623
  return;
7411
7624
  }
7412
7625
  this.#render();
@@ -7428,6 +7641,10 @@ class FigChit extends HTMLElement {
7428
7641
  }
7429
7642
  }
7430
7643
  customElements.define("fig-chit", FigChit);
7644
+ class FigSwatch extends FigChit{
7645
+
7646
+ }
7647
+ customElements.define("fig-swatch", FigSwatch);
7431
7648
 
7432
7649
  /* Upload */
7433
7650
  /**
@@ -10698,7 +10915,6 @@ class FigFillPicker extends HTMLElement {
10698
10915
  this.#chit.style.setProperty("--chit-bg-size", bgSize);
10699
10916
  this.#chit.style.setProperty("--chit-bg-position", bgPosition);
10700
10917
 
10701
- // For solid colors, also update the alpha
10702
10918
  if (this.#fillType === "solid") {
10703
10919
  this.#chit.setAttribute("alpha", this.#color.a);
10704
10920
  } else {
@@ -11522,8 +11738,7 @@ class FigFillPicker extends HTMLElement {
11522
11738
  </fig-tooltip>
11523
11739
  </fig-field>
11524
11740
  <fig-preview class="fig-fill-picker-gradient-preview">
11525
- <div class="fig-fill-picker-gradient-bar"></div>
11526
- <div class="fig-fill-picker-gradient-stops-handles"></div>
11741
+ <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>
11527
11742
  </fig-preview>
11528
11743
  <fig-field class="fig-fill-picker-gradient-interpolation" direction="horizontal">
11529
11744
  <label>Mixing</label>
@@ -11654,6 +11869,30 @@ class FigFillPicker extends HTMLElement {
11654
11869
  this.#updateGradientUI();
11655
11870
  this.#emitInput();
11656
11871
  });
11872
+
11873
+ // Embedded gradient bar input
11874
+ const gradientBarInput = container.querySelector(".fig-fill-picker-gradient-bar-input");
11875
+ if (gradientBarInput) {
11876
+ const syncFromBarInput = (e) => {
11877
+ e.stopPropagation();
11878
+ const detail = e.detail;
11879
+ if (!detail?.gradient) return;
11880
+ this.#gradient = normalizeGradientConfig({
11881
+ ...this.#gradient,
11882
+ ...detail.gradient,
11883
+ });
11884
+ this.#updateChit();
11885
+ this.#updateGradientStopsList();
11886
+ };
11887
+ gradientBarInput.addEventListener("input", (e) => {
11888
+ syncFromBarInput(e);
11889
+ this.#emitInput();
11890
+ });
11891
+ gradientBarInput.addEventListener("change", (e) => {
11892
+ syncFromBarInput(e);
11893
+ this.#emitChange();
11894
+ });
11895
+ }
11657
11896
  }
11658
11897
 
11659
11898
  #updateGradientUI() {
@@ -11699,14 +11938,14 @@ class FigFillPicker extends HTMLElement {
11699
11938
  #updateGradientPreview() {
11700
11939
  if (!this.#dialog) return;
11701
11940
 
11702
- const preview = this.#dialog.querySelector(
11703
- ".fig-fill-picker-gradient-preview",
11941
+ const barInput = this.#dialog.querySelector(
11942
+ ".fig-fill-picker-gradient-bar-input",
11704
11943
  );
11705
- const bar = this.#dialog.querySelector(".fig-fill-picker-gradient-bar");
11706
- if (preview || bar) {
11707
- const css = this.#getGradientCSS();
11708
- if (bar) bar.style.background = css;
11709
- if (preview) preview.style.background = css;
11944
+ if (barInput) {
11945
+ barInput.setAttribute(
11946
+ "value",
11947
+ JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) }),
11948
+ );
11710
11949
  }
11711
11950
 
11712
11951
  this.#updateChit();
@@ -11720,6 +11959,31 @@ class FigFillPicker extends HTMLElement {
11720
11959
  );
11721
11960
  if (!list) return;
11722
11961
 
11962
+ const existingRows = list.querySelectorAll(
11963
+ ".fig-fill-picker-gradient-stop-row",
11964
+ );
11965
+
11966
+ if (existingRows.length === this.#gradient.stops.length) {
11967
+ this.#gradient.stops.forEach((stop, index) => {
11968
+ const row = existingRows[index];
11969
+ row.dataset.index = index;
11970
+ const posInput = row.querySelector(".fig-fill-picker-stop-position");
11971
+ if (posInput) posInput.setAttribute("value", stop.position);
11972
+ const colorInput = row.querySelector(".fig-fill-picker-stop-color");
11973
+ if (colorInput) colorInput.setAttribute("value", stop.color);
11974
+ const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
11975
+ if (removeBtn) {
11976
+ if (this.#gradient.stops.length <= 2) removeBtn.setAttribute("disabled", "");
11977
+ else removeBtn.removeAttribute("disabled");
11978
+ }
11979
+ });
11980
+ return;
11981
+ }
11982
+
11983
+ this.#rebuildGradientStopsList(list);
11984
+ }
11985
+
11986
+ #rebuildGradientStopsList(list) {
11723
11987
  list.innerHTML = this.#gradient.stops
11724
11988
  .map(
11725
11989
  (stop, index) => `
@@ -11740,7 +12004,6 @@ class FigFillPicker extends HTMLElement {
11740
12004
  )
11741
12005
  .join("");
11742
12006
 
11743
- // Setup event listeners for each stop
11744
12007
  list
11745
12008
  .querySelectorAll(".fig-fill-picker-gradient-stop-row")
11746
12009
  .forEach((row) => {
@@ -11769,10 +12032,10 @@ class FigFillPicker extends HTMLElement {
11769
12032
  stopColor.addEventListener("input", (e) => {
11770
12033
  this.#gradient.stops[index].color =
11771
12034
  e.target.hexOpaque || e.target.value;
11772
- const parsedAlpha = parseFloat(e.target.alpha);
11773
- this.#gradient.stops[index].opacity = isNaN(parsedAlpha)
11774
- ? 100
11775
- : parsedAlpha;
12035
+ const a = e.detail?.rgba?.a;
12036
+ if (a !== undefined) {
12037
+ this.#gradient.stops[index].opacity = Math.round(a * 100);
12038
+ }
11776
12039
  this.#updateGradientPreview();
11777
12040
  this.#emitInput();
11778
12041
  });
@@ -11798,9 +12061,10 @@ class FigFillPicker extends HTMLElement {
11798
12061
  const isP3 = this.#gamut === "display-p3";
11799
12062
  const stops = gradient.stops
11800
12063
  .map((s) => {
12064
+ const alpha = (s.opacity ?? 100) / 100;
11801
12065
  const color = isP3
11802
- ? this.#hexToP3(s.color, s.opacity / 100)
11803
- : this.#hexToRGBA(s.color, s.opacity / 100);
12066
+ ? this.#hexToP3(s.color, alpha)
12067
+ : this.#hexToRGBA(s.color, alpha);
11804
12068
  return `${color} ${s.position}%`;
11805
12069
  })
11806
12070
  .join(", ");
@@ -12762,8 +13026,12 @@ class FigColorTip extends HTMLElement {
12762
13026
  }
12763
13027
 
12764
13028
  const eventDetail = { color: this.value };
12765
- if (this.#alphaEnabled && detail?.opacity !== undefined) {
12766
- eventDetail.opacity = detail.opacity;
13029
+ if (this.#alphaEnabled) {
13030
+ if (detail?.opacity !== undefined) {
13031
+ eventDetail.opacity = detail.opacity;
13032
+ } else if (detail?.alpha !== undefined) {
13033
+ eventDetail.opacity = Math.round(detail.alpha * 100);
13034
+ }
12767
13035
  }
12768
13036
 
12769
13037
  this.dispatchEvent(
@@ -14756,19 +15024,25 @@ class FigHandle extends HTMLElement {
14756
15024
  this.#colorTip = null;
14757
15025
  }
14758
15026
 
15027
+ #colorWithOpacity(hex, opacity) {
15028
+ if (opacity === undefined || opacity >= 100) return hex;
15029
+ const { r, g, b } = figHexToRGB(hex);
15030
+ return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
15031
+ }
15032
+
14759
15033
  #handleColorTipInput = (e) => {
14760
15034
  e.stopPropagation();
14761
15035
  if (e.detail?.color) {
14762
- this.setAttribute("color", e.detail.color);
14763
- this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { color: e.detail.color } }));
15036
+ this.setAttribute("color", this.#colorWithOpacity(e.detail.color, e.detail.opacity));
15037
+ this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { color: e.detail.color, opacity: e.detail.opacity } }));
14764
15038
  }
14765
15039
  };
14766
15040
 
14767
15041
  #handleColorTipChange = (e) => {
14768
15042
  e.stopPropagation();
14769
15043
  if (e.detail?.color) {
14770
- this.setAttribute("color", e.detail.color);
14771
- this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { color: e.detail.color } }));
15044
+ this.setAttribute("color", this.#colorWithOpacity(e.detail.color, e.detail.opacity));
15045
+ this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { color: e.detail.color, opacity: e.detail.opacity } }));
14772
15046
  }
14773
15047
  };
14774
15048
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.20.3",
3
+ "version": "3.21.1",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "MIT",