@rogieking/figui3 8.9.17 → 8.9.19

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-editor.css CHANGED
@@ -153,13 +153,15 @@ fig-fill-picker {
153
153
  width: 240px;
154
154
  padding: 0;
155
155
 
156
+ & > fig-content {
157
+ padding-bottom: var(--spacer-2-5);
158
+ padding-top: var(--spacer-2);
159
+ }
160
+
156
161
  fig-tab {
157
162
  font-size: 0.6875rem;
158
163
  padding: var(--spacer-1) var(--spacer-1);
159
164
  }
160
- & > fig-content {
161
- padding-bottom: var(--spacer-2-5);
162
- }
163
165
 
164
166
  .fig-fill-picker-close {
165
167
  margin-left: auto;
@@ -172,8 +174,10 @@ fig-fill-picker {
172
174
  display: block;
173
175
  }
174
176
  }
175
- fig-preview{
176
- overflow: visible;
177
+ .fig-fill-picker-tab:is([data-tab="solid"], [data-tab="gradient"]) {
178
+ fig-preview {
179
+ overflow: visible;
180
+ }
177
181
  }
178
182
 
179
183
  .fig-fill-picker-color-area {
@@ -518,11 +522,14 @@ fig-fill-picker {
518
522
  }
519
523
 
520
524
  &.has-media {
521
- fig-button {
525
+ /* Replace/upload only — not fig-media-controls play. */
526
+ fig-preview fig-button,
527
+ fig-input-file fig-button {
522
528
  visibility: hidden;
523
529
  }
524
530
  &:hover {
525
- fig-button {
531
+ fig-preview fig-button,
532
+ fig-input-file fig-button {
526
533
  visibility: visible;
527
534
  }
528
535
  }
package/fig-editor.js CHANGED
@@ -1499,8 +1499,16 @@ function normalizeGradientConfig(gradient) {
1499
1499
  return next;
1500
1500
  }
1501
1501
 
1502
+ function lockFillPickerGradientInterpolation(gradient) {
1503
+ const next = normalizeGradientConfig(gradient);
1504
+ // Interpolation UI hidden for now — lock to sRGB.
1505
+ next.interpolationSpace = "srgb";
1506
+ delete next.hueInterpolation;
1507
+ return next;
1508
+ }
1509
+
1502
1510
  function gradientToValueShape(gradient) {
1503
- const normalized = normalizeGradientConfig(gradient);
1511
+ const normalized = lockFillPickerGradientInterpolation(gradient);
1504
1512
  const output = {
1505
1513
  ...normalized,
1506
1514
  interpolationSpace: normalized.interpolationSpace,
@@ -1991,7 +1999,7 @@ class FigFillPicker extends HTMLElement {
1991
1999
  // Gamut UI hidden for now — lock to sRGB.
1992
2000
  this.#gamut = "srgb";
1993
2001
  if (parsed.gradient) {
1994
- this.#gradient = normalizeGradientConfig({
2002
+ this.#gradient = lockFillPickerGradientInterpolation({
1995
2003
  ...this.#gradient,
1996
2004
  ...parsed.gradient,
1997
2005
  });
@@ -2070,9 +2078,13 @@ class FigFillPicker extends HTMLElement {
2070
2078
  bg = "";
2071
2079
  }
2072
2080
  break;
2073
- default:
2074
- const slot = this.#customSlots[this.#fillType];
2075
- bg = slot?.element?.getAttribute("swatch-background") || "#D9D9D9";
2081
+ default: {
2082
+ const slot =
2083
+ this.#customSlots[this.#fillType]?.element ||
2084
+ this.querySelector(`[slot="mode-${this.#fillType}"]`);
2085
+ bg = slot?.getAttribute("swatch-background") || "";
2086
+ break;
2087
+ }
2076
2088
  }
2077
2089
 
2078
2090
  this.#swatch.setAttribute("background", bg);
@@ -2281,6 +2293,7 @@ class FigFillPicker extends HTMLElement {
2281
2293
  {
2282
2294
  className: "fig-fill-picker-type",
2283
2295
  label: "Fill type",
2296
+ variant: "ghost",
2284
2297
  value: this.#fillType,
2285
2298
  },
2286
2299
  options,
@@ -3212,7 +3225,6 @@ class FigFillPicker extends HTMLElement {
3212
3225
  // ============ GRADIENT TAB ============
3213
3226
  #initGradientTab() {
3214
3227
  const container = this.#dialog.querySelector('[data-tab="gradient"]');
3215
- const interpolationValue = gradientInterpolationSelectValue(this.#gradient);
3216
3228
  const gradientType = figEditorCreateElement(
3217
3229
  "fig-select",
3218
3230
  {
@@ -3314,70 +3326,7 @@ class FigFillPicker extends HTMLElement {
3314
3326
  ),
3315
3327
  ],
3316
3328
  );
3317
- const interpolationModes = figEditorCreateElement(
3318
- "fig-segmented-control",
3319
- {
3320
- className: "fig-fill-picker-gradient-interpolation-modes",
3321
- "aria-label": "Color interpolation",
3322
- value: interpolationValue,
3323
- },
3324
- FigFillPicker.#GRADIENT_INTERPOLATION_MODES.filter(
3325
- (mode) => !mode.advanced,
3326
- ).map(({ value, space, title, subtitle }) =>
3327
- figEditorCreateElement(
3328
- "fig-tooltip",
3329
- { text: `${title} — ${subtitle}` },
3330
- figEditorCreateElement(
3331
- "fig-segment",
3332
- {
3333
- value,
3334
- "data-space": space,
3335
- "aria-label": `${title} — ${subtitle}`,
3336
- },
3337
- figEditorCreateElement("fig-interpolation-swatch", {
3338
- "aria-hidden": "true",
3339
- }),
3340
- ),
3341
- ),
3342
- ),
3343
- );
3344
- const interpolationSelect = figEditorCreateElement(
3345
- "fig-select",
3346
- {
3347
- className: "fig-fill-picker-gradient-space",
3348
- label: "Color interpolation",
3349
- value: interpolationValue,
3350
- },
3351
- figEditorCreateElement(
3352
- "fig-select-options",
3353
- {},
3354
- this.#createGradientInterpolationOptions(),
3355
- ),
3356
- );
3357
- // The select trigger overlays the icon button so its menu anchors correctly.
3358
- const interpolationMore = figEditorCreateElement(
3359
- "div",
3360
- { className: "fig-fill-picker-gradient-interpolation-more" },
3361
- [
3362
- figEditorCreateElement(
3363
- "fig-button",
3364
- {
3365
- icon: true,
3366
- variant: "ghost",
3367
- "aria-hidden": "true",
3368
- tabindex: "-1",
3369
- },
3370
- figEditorCreateIcon("more"),
3371
- ),
3372
- interpolationSelect,
3373
- ],
3374
- );
3375
- const interpolation = figEditorCreateElement(
3376
- "fig-field",
3377
- { className: "fig-fill-picker-gradient-interpolation-field" },
3378
- [interpolationModes, interpolationMore],
3379
- );
3380
- container.replaceChildren(header, preview, interpolation, stops);
3329
+ container.replaceChildren(header, preview, stops);
3381
3330
 
3382
3331
  this.#updateGradientUI();
3383
3332
  this.#setupGradientEvents(container);
@@ -3675,7 +3624,7 @@ class FigFillPicker extends HTMLElement {
3675
3624
  if (this.#syncingGradientBar) return;
3676
3625
  const detail = e.detail;
3677
3626
  if (!detail?.gradient) return;
3678
- this.#gradient = normalizeGradientConfig({
3627
+ this.#gradient = lockFillPickerGradientInterpolation({
3679
3628
  ...this.#gradient,
3680
3629
  ...detail.gradient,
3681
3630
  });
@@ -3846,7 +3795,7 @@ class FigFillPicker extends HTMLElement {
3846
3795
 
3847
3796
  const container = this.#dialog.querySelector('[data-tab="gradient"]');
3848
3797
  if (!container) return;
3849
- this.#gradient = normalizeGradientConfig(this.#gradient);
3798
+ this.#gradient = lockFillPickerGradientInterpolation(this.#gradient);
3850
3799
 
3851
3800
  const interpolationValue = gradientInterpolationSelectValue(this.#gradient);
3852
3801
  const interpolationSelect = container.querySelector(
@@ -4286,6 +4235,11 @@ class FigFillPicker extends HTMLElement {
4286
4235
  }
4287
4236
 
4288
4237
  element.setAttribute("src", this.#video.url);
4238
+ if (this.#video.poster) {
4239
+ element.setAttribute("poster", this.#video.poster);
4240
+ } else {
4241
+ element.removeAttribute("poster");
4242
+ }
4289
4243
  element.classList.add("has-media");
4290
4244
 
4291
4245
  const fileInput = element.querySelector("fig-input-file[data-generated]");
@@ -4623,11 +4577,16 @@ class FigFillPicker extends HTMLElement {
4623
4577
  existing?.getTracks?.().filter((track) => track.readyState !== "ended") ??
4624
4578
  [];
4625
4579
  if (existing && liveTracks.length) {
4626
- const currentId = this.#webcam.deviceId;
4627
- if (!requested || !currentId || requested === currentId) {
4580
+ const liveId =
4581
+ existing.getVideoTracks?.()?.[0]?.getSettings?.()?.deviceId ||
4582
+ this.#webcam.deviceId ||
4583
+ null;
4584
+ // Reuse the live stream only when it is already the requested device.
4585
+ // An unknown liveId is not a match — otherwise the camera select cannot switch.
4586
+ if (!requested || requested === liveId) {
4628
4587
  attachPreview(existing);
4629
4588
  this.#emitWebcamStream();
4630
- populateCameras(null, requested || currentId);
4589
+ populateCameras(null, requested || liveId);
4631
4590
  return;
4632
4591
  }
4633
4592
  }
@@ -4688,8 +4647,7 @@ class FigFillPicker extends HTMLElement {
4688
4647
  cameraSelect.addEventListener("change", (e) => {
4689
4648
  const next =
4690
4649
  typeof e.detail === "string" ? e.detail : e.target?.value;
4691
- if (!next) return;
4692
- this.#webcam.deviceId = next;
4650
+ if (!next || next === this.#webcam.deviceId) return;
4693
4651
  startWebcam(next);
4694
4652
  });
4695
4653
 
package/fig.js CHANGED
@@ -322,7 +322,7 @@ fig-select-options > .fig-overflow-end::before,
322
322
 
323
323
  const FIG_SHADOW_ICON_CSS = `
324
324
  fig-icon {
325
- contain: strict;
325
+ contain: size;
326
326
  --size: var(--spacer-4);
327
327
  display: inline-flex;
328
328
  width: var(--size);
@@ -8979,6 +8979,7 @@ class FigInputFill extends HTMLElement {
8979
8979
  scale: 50,
8980
8980
  opacity: 1,
8981
8981
  };
8982
+ #custom = {};
8982
8983
 
8983
8984
  constructor() {
8984
8985
  super();
@@ -9066,6 +9067,11 @@ class FigInputFill extends HTMLElement {
9066
9067
  if (parsed.opacity !== undefined)
9067
9068
  this.#webcam.opacity = parsed.opacity;
9068
9069
  break;
9070
+ default: {
9071
+ const { type: _type, ...rest } = parsed;
9072
+ this.#custom = rest;
9073
+ break;
9074
+ }
9069
9075
  }
9070
9076
  } catch (e) {
9071
9077
  // If not JSON, treat as hex color
@@ -9142,9 +9148,9 @@ class FigInputFill extends HTMLElement {
9142
9148
  case "webcam":
9143
9149
  return this.#webcam.snapshot
9144
9150
  ? figCssUrl(this.#webcam.snapshot)
9145
- : "#D9D9D9";
9151
+ : "";
9146
9152
  default:
9147
- return "#D9D9D9";
9153
+ return this.#customSwatchBackground();
9148
9154
  }
9149
9155
  }
9150
9156
 
@@ -9173,7 +9179,38 @@ class FigInputFill extends HTMLElement {
9173
9179
  case "webcam":
9174
9180
  return this.#webcam.opacity ?? 1;
9175
9181
  default:
9176
- return 1;
9182
+ return this.#normalizeFillOpacity(
9183
+ this.#custom.opacity ?? this.#custom.alpha,
9184
+ 1,
9185
+ );
9186
+ }
9187
+ }
9188
+
9189
+ #customModeSlot() {
9190
+ return this.querySelector(`[slot="mode-${this.#fillType}"]`);
9191
+ }
9192
+
9193
+ #customTypeLabel() {
9194
+ const slotLabel = this.#customModeSlot()?.getAttribute("label");
9195
+ if (slotLabel) return slotLabel;
9196
+ if (!this.#fillType) return "Fill";
9197
+ return this.#fillType.charAt(0).toUpperCase() + this.#fillType.slice(1);
9198
+ }
9199
+
9200
+ #customSwatchBackground() {
9201
+ return (
9202
+ this.#custom.swatchBackground ||
9203
+ this.#custom.background ||
9204
+ this.#custom.css ||
9205
+ this.#customModeSlot()?.getAttribute("swatch-background") ||
9206
+ ""
9207
+ );
9208
+ }
9209
+
9210
+ #adoptModeSlots() {
9211
+ if (!this.#fillPicker) return;
9212
+ for (const slot of [...this.querySelectorAll(":scope > [slot^='mode-']")]) {
9213
+ this.#fillPicker.append(slot);
9177
9214
  }
9178
9215
  }
9179
9216
 
@@ -9268,6 +9305,13 @@ class FigInputFill extends HTMLElement {
9268
9305
  label = "Webcam";
9269
9306
  opacity = this.#webcam.opacity ?? 1;
9270
9307
  break;
9308
+ default:
9309
+ label = this.#customTypeLabel();
9310
+ opacity = this.#normalizeFillOpacity(
9311
+ this.#custom.opacity ?? this.#custom.alpha,
9312
+ 1,
9313
+ );
9314
+ break;
9271
9315
  }
9272
9316
 
9273
9317
  return [
@@ -9290,6 +9334,7 @@ class FigInputFill extends HTMLElement {
9290
9334
  alpha: this.#fillPickerSwatchAlpha(),
9291
9335
  disabled,
9292
9336
  });
9337
+ const modeSlots = [...this.querySelectorAll("[slot^='mode-']")];
9293
9338
  const picker = figCreateElement(
9294
9339
  "fig-fill-picker",
9295
9340
  {
@@ -9297,7 +9342,7 @@ class FigInputFill extends HTMLElement {
9297
9342
  disabled,
9298
9343
  ...fpAttrs,
9299
9344
  },
9300
- swatch,
9345
+ [swatch, ...modeSlots],
9301
9346
  );
9302
9347
  const combo = figCreateElement(
9303
9348
  "div",
@@ -9307,6 +9352,11 @@ class FigInputFill extends HTMLElement {
9307
9352
  this.replaceChildren(combo);
9308
9353
 
9309
9354
  this.#setupEventListeners();
9355
+ queueMicrotask(() => {
9356
+ this.#adoptModeSlots();
9357
+ this.#syncSwatch();
9358
+ this.#updateControls();
9359
+ });
9310
9360
  }
9311
9361
 
9312
9362
  #setupEventListeners() {
@@ -9375,6 +9425,11 @@ class FigInputFill extends HTMLElement {
9375
9425
  this.#webcam.snapshot = detail.image.url;
9376
9426
  }
9377
9427
  break;
9428
+ default: {
9429
+ const { type: _type, colorSpace: _colorSpace, ...rest } = detail;
9430
+ this.#custom = rest;
9431
+ break;
9432
+ }
9378
9433
  }
9379
9434
  // Update controls (don't re-render to keep dialog open)
9380
9435
  if (typeChanged) {
@@ -9441,6 +9496,9 @@ class FigInputFill extends HTMLElement {
9441
9496
  case "webcam":
9442
9497
  this.#webcam.opacity = alpha;
9443
9498
  break;
9499
+ default:
9500
+ this.#custom.opacity = alpha;
9501
+ break;
9444
9502
  }
9445
9503
  this.#updateFillPicker();
9446
9504
  this.#syncSwatch();
@@ -9510,6 +9568,22 @@ class FigInputFill extends HTMLElement {
9510
9568
  );
9511
9569
  }
9512
9570
  break;
9571
+ default: {
9572
+ const label = this.querySelector(".fig-input-fill-label");
9573
+ if (label) label.textContent = this.#customTypeLabel();
9574
+ if (this.#opacityInput) {
9575
+ this.#opacityInput.setAttribute(
9576
+ "value",
9577
+ Math.round(
9578
+ this.#normalizeFillOpacity(
9579
+ this.#custom.opacity ?? this.#custom.alpha,
9580
+ 1,
9581
+ ) * 100,
9582
+ ),
9583
+ );
9584
+ }
9585
+ break;
9586
+ }
9513
9587
  }
9514
9588
  }
9515
9589
 
@@ -9587,6 +9661,9 @@ class FigInputFill extends HTMLElement {
9587
9661
  case "webcam":
9588
9662
  this.#webcam.opacity = alpha;
9589
9663
  break;
9664
+ default:
9665
+ this.#custom.opacity = alpha;
9666
+ break;
9590
9667
  }
9591
9668
  this.#updateFillPicker();
9592
9669
  this.#updateSwatchAlpha(alpha);
@@ -9703,7 +9780,7 @@ class FigInputFill extends HTMLElement {
9703
9780
  webcam: this.#webcamValue(),
9704
9781
  };
9705
9782
  default:
9706
- return { type: this.#fillType };
9783
+ return { type: this.#fillType, ...this.#custom };
9707
9784
  }
9708
9785
  }
9709
9786
 
@@ -11886,7 +11963,24 @@ class FigSwatch extends HTMLElement {
11886
11963
  }
11887
11964
 
11888
11965
  #render() {
11889
- const rawBg = this.getAttribute("background") || "#D9D9D9";
11966
+ const rawAttr = this.getAttribute("background");
11967
+ const emptyMedia = this.hasAttribute("background") && !rawAttr?.trim();
11968
+ if (emptyMedia) {
11969
+ if (this.#type !== "image" || this.input) {
11970
+ this.#type = "image";
11971
+ this.setAttribute("data-type", "image");
11972
+ if (this.input) {
11973
+ this.input.removeEventListener("input", this.#boundHandleInput);
11974
+ }
11975
+ this.replaceChildren(document.createElement("div"));
11976
+ this.input = null;
11977
+ this.#syncA11y();
11978
+ }
11979
+ this.style.setProperty("--swatch-background", "none");
11980
+ return;
11981
+ }
11982
+
11983
+ const rawBg = rawAttr || "#D9D9D9";
11890
11984
  const isVar = rawBg.includes("var(");
11891
11985
  const bg = isVar ? this.#resolveBackground(rawBg) : rawBg;
11892
11986
  const newType = this.#detectType(bg);
@@ -13382,13 +13476,9 @@ class FigMediaControls extends HTMLElement {
13382
13476
  tooltip.setAttribute("text", "Play");
13383
13477
  const btn = document.createElement("fig-button");
13384
13478
  btn.setAttribute("variant", "ghost");
13385
- btn.setAttribute("size", "small");
13386
13479
  btn.setAttribute("icon", "true");
13387
13480
  btn.setAttribute("aria-label", "Play");
13388
- const icon = createFigIcon("play", {
13389
- className: "fig-media-controls-play-icon",
13390
- });
13391
- btn.append(icon);
13481
+ btn.append(createFigIcon("play"));
13392
13482
  tooltip.append(btn);
13393
13483
  btn.addEventListener("click", (e) => {
13394
13484
  e.preventDefault();
@@ -13478,7 +13568,7 @@ class FigMediaControls extends HTMLElement {
13478
13568
  const playing = this.playing;
13479
13569
  this.#playBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
13480
13570
  this.#playTooltip?.setAttribute("text", playing ? "Pause" : "Play");
13481
- const icon = this.#playBtn.querySelector(".fig-media-controls-play-icon");
13571
+ const icon = this.#playBtn.querySelector("fig-icon");
13482
13572
  if (icon) {
13483
13573
  icon.setAttribute("name", playing ? "pause" : "play");
13484
13574
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.17",
3
+ "version": "8.9.19",
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",