@rogieking/figui3 3.20.2 → 3.21.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.
Files changed (3) hide show
  1. package/components.css +45 -14
  2. package/fig.js +315 -46
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -3146,6 +3146,7 @@ fig-input-fill {
3146
3146
  }
3147
3147
  fig-input-gradient {
3148
3148
  --height: 1.5rem;
3149
+ --chit-height: var(--height);
3149
3150
  position: relative;
3150
3151
  height: var(--height);
3151
3152
  width: 100%;
@@ -3157,12 +3158,34 @@ fig-input-gradient {
3157
3158
  outline: 1px solid var(--figma-color-border-selected) !important;
3158
3159
  outline-offset: -1px !important;
3159
3160
  }
3160
- & > fig-chit {
3161
+
3162
+ &[edit="false"] {
3163
+ pointer-events: none;
3164
+
3165
+ &:focus,
3166
+ &:active,
3167
+ &:focus-within {
3168
+ outline: none !important;
3169
+ }
3170
+ }
3171
+
3172
+ &[edit="picker"] {
3173
+ & > fig-fill-picker {
3174
+ display: contents;
3175
+ }
3176
+ }
3177
+ & > fig-chit,
3178
+ & > fig-fill-picker > fig-chit {
3161
3179
  --padding: 0;
3162
3180
  --width: 100%;
3181
+ --height: var(--chit-height);
3163
3182
  flex: 1 1 auto;
3164
3183
  width: 100% !important;
3165
3184
  min-width: 0 !important;
3185
+ min-height: var(--chit-height) !important;
3186
+ }
3187
+ &[size="large"] {
3188
+ --height: 3rem;
3166
3189
  }
3167
3190
 
3168
3191
  .fig-input-gradient-track {
@@ -3194,7 +3217,7 @@ fig-input-palette {
3194
3217
  display: flex;
3195
3218
  flex-wrap: nowrap;
3196
3219
  gap: 0;
3197
- border-radius: var(--radius-medium);
3220
+ border-radius: var(--radius-small);
3198
3221
  overflow: hidden;
3199
3222
  grid-area: inputs;
3200
3223
  min-width: 0;
@@ -3211,9 +3234,15 @@ fig-input-palette {
3211
3234
  border-radius: var(--radius-medium);
3212
3235
  background-color: var(--figma-color-bg-secondary);
3213
3236
  width: 100%;
3237
+ gap: var(--spacer-1);
3238
+ place-items: center;
3214
3239
 
3215
3240
  .palette-colors {
3216
3241
  display: flex;
3242
+ --palette-colors-height: calc(1.5rem - var(--spacer-1));
3243
+ height: var(--palette-colors-height);
3244
+ margin-left: var(--spacer-1);
3245
+
3217
3246
  background-color: var(--figma-color-bg-secondary);
3218
3247
  fig-input-color {
3219
3248
  width: 100%;
@@ -3223,10 +3252,11 @@ fig-input-palette {
3223
3252
  --padding: 0px;
3224
3253
  --border-radius: 0px;
3225
3254
  --width: 100%;
3255
+ --height: var(--palette-colors-height);
3256
+ --size: var(--palette-colors-height);
3226
3257
  flex: 1;
3227
3258
  min-width: 0;
3228
3259
  width: 100% !important;
3229
- height: var(--size);
3230
3260
  border-radius: 0 !important;
3231
3261
  input,
3232
3262
  &::before,
@@ -3248,6 +3278,11 @@ fig-input-palette {
3248
3278
  grid-template-areas: "inputs inputs";
3249
3279
  }
3250
3280
  }
3281
+ &[color-strip="false"] {
3282
+ .palette-colors-inline {
3283
+ display: none !important;
3284
+ }
3285
+ }
3251
3286
  .palette-colors-expanded {
3252
3287
  display: none;
3253
3288
  flex-direction: column;
@@ -4375,17 +4410,13 @@ fig-preview {
4375
4410
  }
4376
4411
  }
4377
4412
 
4378
- .fig-fill-picker-gradient-bar {
4379
- background: linear-gradient(90deg, #d9d9d9 0%, #737373 100%);
4380
- }
4381
-
4382
- .fig-fill-picker-gradient-stops-handles {
4383
- position: absolute;
4384
- top: 0;
4385
- left: 0;
4386
- right: 0;
4387
- bottom: 0;
4388
- pointer-events: none;
4413
+ .fig-fill-picker-gradient-preview {
4414
+ display: block;
4415
+ .fig-fill-picker-gradient-bar-input {
4416
+ --width: 100%;
4417
+ --height: 100%;
4418
+ aspect-ratio: 1/1;
4419
+ }
4389
4420
  }
4390
4421
 
4391
4422
  .fig-fill-picker-gradient-stops {
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));
@@ -6124,13 +6254,24 @@ class FigInputGradient extends HTMLElement {
6124
6254
  }
6125
6255
 
6126
6256
  static get observedAttributes() {
6127
- return ["value", "disabled"];
6257
+ return ["value", "disabled", "edit"];
6258
+ }
6259
+
6260
+ get #editMode() {
6261
+ const attr = this.getAttribute("edit");
6262
+ if (attr === "false") return "false";
6263
+ if (attr === "picker") return "picker";
6264
+ return "true";
6265
+ }
6266
+
6267
+ get #isEditable() {
6268
+ return this.#editMode === "true";
6128
6269
  }
6129
6270
 
6130
6271
  connectedCallback() {
6131
6272
  this.#parseValue();
6132
6273
  this.#render();
6133
- document.addEventListener("keydown", this.#onKeyDown);
6274
+ if (this.#isEditable) document.addEventListener("keydown", this.#onKeyDown);
6134
6275
  }
6135
6276
 
6136
6277
  disconnectedCallback() {
@@ -6252,7 +6393,8 @@ class FigInputGradient extends HTMLElement {
6252
6393
  (a, b) => a.position - b.position,
6253
6394
  );
6254
6395
  const stops = sorted.map((s) => `${s.color} ${s.position}%`).join(", ");
6255
- return `linear-gradient(${this.#gradient.angle}deg, ${stops})`;
6396
+ const interp = gradientInterpolationClause(this.#gradient);
6397
+ return `linear-gradient(${this.#gradient.angle}deg ${interp}, ${stops})`;
6256
6398
  }
6257
6399
 
6258
6400
  #buildStopHandles() {
@@ -6260,40 +6402,79 @@ class FigInputGradient extends HTMLElement {
6260
6402
  return this.#gradient.stops
6261
6403
  .map(
6262
6404
  (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%" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
6405
+ `<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>`,
6264
6406
  )
6265
6407
  .join("");
6266
6408
  }
6267
6409
 
6268
6410
  #ghostHandle = null;
6411
+ #addedOnPointerDown = false;
6269
6412
 
6270
6413
  #render() {
6271
6414
  const disabled = this.hasAttribute("disabled");
6415
+ const mode = this.#editMode;
6416
+
6417
+ if (mode === "picker") {
6418
+ const experimental = this.getAttribute("experimental");
6419
+ const expAttr = experimental ? ` experimental="${experimental}"` : "";
6420
+ const gradientValue = JSON.stringify(this.value);
6421
+ this.innerHTML = `
6422
+ <fig-fill-picker mode="gradient"${expAttr} value='${gradientValue}'${disabled ? " disabled" : ""}>
6423
+ <fig-chit size="medium" background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
6424
+ </fig-fill-picker>`;
6425
+ this.#chit = this.querySelector("fig-chit");
6426
+ this.#track = null;
6427
+ this.#setupPickerEvents();
6428
+ return;
6429
+ }
6430
+
6272
6431
  this.innerHTML = `
6273
6432
  <fig-chit size="medium" background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
6274
- <div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>`;
6433
+ ${mode === "true" ? `<div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>` : ""}`;
6275
6434
  this.#chit = this.querySelector("fig-chit");
6276
6435
  this.#track = this.querySelector(".fig-input-gradient-track");
6277
- this.#setupGhostHandle();
6278
- this.#setupEventListeners();
6436
+
6437
+ if (mode === "true") {
6438
+ this.#setupGhostHandle();
6439
+ this.#setupEventListeners();
6440
+ requestAnimationFrame(() => this.#repositionHandles());
6441
+ }
6442
+ }
6443
+
6444
+ #setupPickerEvents() {
6445
+ const picker = this.querySelector("fig-fill-picker");
6446
+ if (!picker) return;
6447
+ picker.anchorElement = this;
6448
+
6449
+ const syncFromPicker = (e) => {
6450
+ e.stopPropagation();
6451
+ const detail = e.detail;
6452
+ if (!detail?.gradient) return;
6453
+ this.#gradient = normalizeGradientConfig({
6454
+ ...this.#gradient,
6455
+ ...detail.gradient,
6456
+ });
6457
+ this.#syncChit();
6458
+ };
6459
+
6460
+ picker.addEventListener("input", (e) => {
6461
+ syncFromPicker(e);
6462
+ this.#emitInput();
6463
+ });
6464
+
6465
+ picker.addEventListener("change", (e) => {
6466
+ syncFromPicker(e);
6467
+ this.#emitChange();
6468
+ });
6279
6469
  }
6280
6470
 
6281
6471
  #sampleGradientColor(position) {
6282
- const { ctx } = figGetSharedCanvas(256, 1);
6283
- ctx.clearRect(0, 0, 256, 1);
6284
- const grad = ctx.createLinearGradient(0, 0, 256, 0);
6285
- for (const stop of this.#gradient.stops) {
6286
- try {
6287
- grad.addColorStop(stop.position / 100, stop.color);
6288
- } catch {
6289
- /* skip invalid */
6290
- }
6291
- }
6292
- ctx.fillStyle = grad;
6293
- ctx.fillRect(0, 0, 256, 1);
6294
- const px = Math.round(Math.max(0, Math.min(255, position * 255)));
6295
- const [r, g, b] = ctx.getImageData(px, 0, 1, 1).data;
6296
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
6472
+ return figSampleGradientAt(
6473
+ this.#gradient.stops,
6474
+ position,
6475
+ this.#gradient.interpolationSpace,
6476
+ this.#gradient.hueInterpolation,
6477
+ );
6297
6478
  }
6298
6479
 
6299
6480
  #setupGhostHandle() {
@@ -6301,22 +6482,14 @@ class FigInputGradient extends HTMLElement {
6301
6482
 
6302
6483
  const ghost = document.createElement("fig-handle");
6303
6484
  ghost.classList.add("fig-input-gradient-ghost");
6485
+ ghost.setAttribute("type", "color");
6486
+ ghost.setAttribute("control", "add");
6304
6487
  ghost.style.position = "absolute";
6305
6488
  ghost.style.top = "50%";
6306
6489
  ghost.style.transform = "translate(-50%, -50%)";
6307
6490
  ghost.style.pointerEvents = "none";
6308
6491
  ghost.style.opacity = "0";
6309
6492
  ghost.style.transition = "opacity 0.15s";
6310
- ghost.style.overflow = "visible";
6311
-
6312
- const tip = document.createElement("fig-color-tip");
6313
- tip.setAttribute("control", "add");
6314
- tip.style.position = "absolute";
6315
- tip.style.bottom = "calc(100% + 6px)";
6316
- tip.style.left = "50%";
6317
- tip.style.transform = "translateX(-50%)";
6318
- tip.style.zIndex = "10";
6319
- ghost.appendChild(tip);
6320
6493
 
6321
6494
  this.#track.appendChild(ghost);
6322
6495
  this.#ghostHandle = ghost;
@@ -6392,6 +6565,10 @@ class FigInputGradient extends HTMLElement {
6392
6565
  #onTrackClick = (e) => {
6393
6566
  if (!this.#track) return;
6394
6567
  if (this.#handleDragging) return;
6568
+ if (this.#addedOnPointerDown) {
6569
+ this.#addedOnPointerDown = false;
6570
+ return;
6571
+ }
6395
6572
  if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
6396
6573
  if (e.shiftKey) {
6397
6574
  const clickedHandle = e.target.closest("fig-handle");
@@ -6433,6 +6610,18 @@ class FigInputGradient extends HTMLElement {
6433
6610
  });
6434
6611
  };
6435
6612
 
6613
+ #repositionHandles() {
6614
+ if (!this.#track) return;
6615
+ const stops = this.#gradient.stops;
6616
+ this.#track
6617
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
6618
+ .forEach((h, i) => {
6619
+ if (i >= stops.length) return;
6620
+ h.removeAttribute("value");
6621
+ h.setAttribute("value", `${stops[i].position}% 50%`);
6622
+ });
6623
+ }
6624
+
6436
6625
  #syncHandles() {
6437
6626
  if (!this.#track) return;
6438
6627
  const handles = this.#track.querySelectorAll(
@@ -6445,6 +6634,7 @@ class FigInputGradient extends HTMLElement {
6445
6634
  this.#track.innerHTML = this.#buildStopHandles();
6446
6635
  if (ghost) this.#track.appendChild(ghost);
6447
6636
  this.#reobserveHandleColors();
6637
+ requestAnimationFrame(() => this.#repositionHandles());
6448
6638
  return;
6449
6639
  }
6450
6640
 
@@ -6524,6 +6714,7 @@ class FigInputGradient extends HTMLElement {
6524
6714
  if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
6525
6715
  if (e.button !== 0) return;
6526
6716
  e.preventDefault();
6717
+ e.stopPropagation();
6527
6718
 
6528
6719
  const trackRect = this.#track.getBoundingClientRect();
6529
6720
  const pct = Math.max(0, Math.min(1, (e.clientX - trackRect.left) / trackRect.width));
@@ -6534,6 +6725,7 @@ class FigInputGradient extends HTMLElement {
6534
6725
  const newIndex = this.#gradient.stops.findIndex(
6535
6726
  (s) => s.position === position && s.color === color,
6536
6727
  );
6728
+ this.#addedOnPointerDown = true;
6537
6729
  this.#syncHandles();
6538
6730
  this.#syncChit();
6539
6731
  this.#emitInput();
@@ -6544,6 +6736,9 @@ class FigInputGradient extends HTMLElement {
6544
6736
  );
6545
6737
  const newHandle = handles[newIndex];
6546
6738
  if (newHandle) {
6739
+ this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6740
+ if (h !== newHandle) h.deselect();
6741
+ });
6547
6742
  newHandle.select();
6548
6743
  newHandle.dispatchEvent(new PointerEvent("pointerdown", {
6549
6744
  bubbles: true,
@@ -6561,6 +6756,15 @@ class FigInputGradient extends HTMLElement {
6561
6756
  const handle = e.target.closest("fig-handle");
6562
6757
  if (!handle) return;
6563
6758
  e.stopPropagation();
6759
+ if (e.detail?.color) {
6760
+ const idx = parseInt(handle.dataset.stopIndex, 10);
6761
+ if (!isNaN(idx) && this.#gradient.stops[idx]) {
6762
+ this.#gradient.stops[idx].color = e.detail.color;
6763
+ this.#syncChit();
6764
+ this.#emitInput();
6765
+ }
6766
+ return;
6767
+ }
6564
6768
  if (!this.#handleDragging) handle.style.zIndex = "5";
6565
6769
  this.#handleDragging = true;
6566
6770
  const idx = parseInt(handle.dataset.stopIndex, 10);
@@ -6603,6 +6807,15 @@ class FigInputGradient extends HTMLElement {
6603
6807
  const handle = e.target.closest("fig-handle");
6604
6808
  if (!handle) return;
6605
6809
  e.stopPropagation();
6810
+ if (e.detail?.color) {
6811
+ const idx = parseInt(handle.dataset.stopIndex, 10);
6812
+ if (!isNaN(idx) && this.#gradient.stops[idx]) {
6813
+ this.#gradient.stops[idx].color = e.detail.color;
6814
+ this.#syncChit();
6815
+ this.#emitChange();
6816
+ }
6817
+ return;
6818
+ }
6606
6819
  handle.style.zIndex = "";
6607
6820
  const tooltip = handle.closest("fig-tooltip");
6608
6821
  if (tooltip) tooltip.removeAttribute("show");
@@ -6700,6 +6913,14 @@ class FigInputGradient extends HTMLElement {
6700
6913
  case "disabled":
6701
6914
  this.#syncDisabled();
6702
6915
  break;
6916
+ case "edit":
6917
+ this.#render();
6918
+ if (this.#isEditable) {
6919
+ document.addEventListener("keydown", this.#onKeyDown);
6920
+ } else {
6921
+ document.removeEventListener("keydown", this.#onKeyDown);
6922
+ }
6923
+ break;
6703
6924
  }
6704
6925
  }
6705
6926
 
@@ -11502,8 +11723,7 @@ class FigFillPicker extends HTMLElement {
11502
11723
  </fig-tooltip>
11503
11724
  </fig-field>
11504
11725
  <fig-preview class="fig-fill-picker-gradient-preview">
11505
- <div class="fig-fill-picker-gradient-bar"></div>
11506
- <div class="fig-fill-picker-gradient-stops-handles"></div>
11726
+ <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>
11507
11727
  </fig-preview>
11508
11728
  <fig-field class="fig-fill-picker-gradient-interpolation" direction="horizontal">
11509
11729
  <label>Mixing</label>
@@ -11634,6 +11854,30 @@ class FigFillPicker extends HTMLElement {
11634
11854
  this.#updateGradientUI();
11635
11855
  this.#emitInput();
11636
11856
  });
11857
+
11858
+ // Embedded gradient bar input
11859
+ const gradientBarInput = container.querySelector(".fig-fill-picker-gradient-bar-input");
11860
+ if (gradientBarInput) {
11861
+ const syncFromBarInput = (e) => {
11862
+ e.stopPropagation();
11863
+ const detail = e.detail;
11864
+ if (!detail?.gradient) return;
11865
+ this.#gradient = normalizeGradientConfig({
11866
+ ...this.#gradient,
11867
+ ...detail.gradient,
11868
+ });
11869
+ this.#updateChit();
11870
+ this.#updateGradientStopsList();
11871
+ };
11872
+ gradientBarInput.addEventListener("input", (e) => {
11873
+ syncFromBarInput(e);
11874
+ this.#emitInput();
11875
+ });
11876
+ gradientBarInput.addEventListener("change", (e) => {
11877
+ syncFromBarInput(e);
11878
+ this.#emitChange();
11879
+ });
11880
+ }
11637
11881
  }
11638
11882
 
11639
11883
  #updateGradientUI() {
@@ -11679,14 +11923,14 @@ class FigFillPicker extends HTMLElement {
11679
11923
  #updateGradientPreview() {
11680
11924
  if (!this.#dialog) return;
11681
11925
 
11682
- const preview = this.#dialog.querySelector(
11683
- ".fig-fill-picker-gradient-preview",
11926
+ const barInput = this.#dialog.querySelector(
11927
+ ".fig-fill-picker-gradient-bar-input",
11684
11928
  );
11685
- const bar = this.#dialog.querySelector(".fig-fill-picker-gradient-bar");
11686
- if (preview || bar) {
11687
- const css = this.#getGradientCSS();
11688
- if (bar) bar.style.background = css;
11689
- if (preview) preview.style.background = css;
11929
+ if (barInput) {
11930
+ barInput.setAttribute(
11931
+ "value",
11932
+ JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) }),
11933
+ );
11690
11934
  }
11691
11935
 
11692
11936
  this.#updateChit();
@@ -11700,6 +11944,31 @@ class FigFillPicker extends HTMLElement {
11700
11944
  );
11701
11945
  if (!list) return;
11702
11946
 
11947
+ const existingRows = list.querySelectorAll(
11948
+ ".fig-fill-picker-gradient-stop-row",
11949
+ );
11950
+
11951
+ if (existingRows.length === this.#gradient.stops.length) {
11952
+ this.#gradient.stops.forEach((stop, index) => {
11953
+ const row = existingRows[index];
11954
+ row.dataset.index = index;
11955
+ const posInput = row.querySelector(".fig-fill-picker-stop-position");
11956
+ if (posInput) posInput.setAttribute("value", stop.position);
11957
+ const colorInput = row.querySelector(".fig-fill-picker-stop-color");
11958
+ if (colorInput) colorInput.setAttribute("value", stop.color);
11959
+ const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
11960
+ if (removeBtn) {
11961
+ if (this.#gradient.stops.length <= 2) removeBtn.setAttribute("disabled", "");
11962
+ else removeBtn.removeAttribute("disabled");
11963
+ }
11964
+ });
11965
+ return;
11966
+ }
11967
+
11968
+ this.#rebuildGradientStopsList(list);
11969
+ }
11970
+
11971
+ #rebuildGradientStopsList(list) {
11703
11972
  list.innerHTML = this.#gradient.stops
11704
11973
  .map(
11705
11974
  (stop, index) => `
@@ -11720,7 +11989,6 @@ class FigFillPicker extends HTMLElement {
11720
11989
  )
11721
11990
  .join("");
11722
11991
 
11723
- // Setup event listeners for each stop
11724
11992
  list
11725
11993
  .querySelectorAll(".fig-fill-picker-gradient-stop-row")
11726
11994
  .forEach((row) => {
@@ -11778,9 +12046,10 @@ class FigFillPicker extends HTMLElement {
11778
12046
  const isP3 = this.#gamut === "display-p3";
11779
12047
  const stops = gradient.stops
11780
12048
  .map((s) => {
12049
+ const alpha = (s.opacity ?? 100) / 100;
11781
12050
  const color = isP3
11782
- ? this.#hexToP3(s.color, s.opacity / 100)
11783
- : this.#hexToRGBA(s.color, s.opacity / 100);
12051
+ ? this.#hexToP3(s.color, alpha)
12052
+ : this.#hexToRGBA(s.color, alpha);
11784
12053
  return `${color} ${s.position}%`;
11785
12054
  })
11786
12055
  .join(", ");
@@ -14480,7 +14749,7 @@ class FigHandle extends HTMLElement {
14480
14749
  document.addEventListener("keydown", this.#handleKeyDown);
14481
14750
  const initial = this.getAttribute("value");
14482
14751
  if (initial) this.#applyValue(initial);
14483
- if (this.#hasControlMode && !this.#isGhost) this.#showColorTip();
14752
+ if (this.#hasControlMode) this.#showColorTip();
14484
14753
  }
14485
14754
 
14486
14755
  disconnectedCallback() {
@@ -14544,7 +14813,7 @@ class FigHandle extends HTMLElement {
14544
14813
  if (name === "value" && !this.#applyingValue && !this.#isDragging) {
14545
14814
  this.#applyValue(value);
14546
14815
  }
14547
- if (name === "control" && !this.#isGhost) {
14816
+ if (name === "control") {
14548
14817
  if (this.#hasControlMode) {
14549
14818
  this.#hideColorTip();
14550
14819
  this.#showColorTip();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.20.2",
3
+ "version": "3.21.0",
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",