@rogieking/figui3 6.25.0 → 6.26.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/fig-lab.js CHANGED
@@ -18,6 +18,20 @@ function figLabBooleanAttribute(element, name) {
18
18
  return element.hasAttribute(name) && element.getAttribute(name) !== "false";
19
19
  }
20
20
 
21
+ function figLabColorEventAliases(color, alpha, opacity) {
22
+ const numericAlpha = Number(alpha);
23
+ const numericOpacity = Number(opacity);
24
+ const normalizedAlpha = Number.isFinite(numericAlpha)
25
+ ? Math.max(0, Math.min(1, numericAlpha))
26
+ : Number.isFinite(numericOpacity)
27
+ ? Math.max(0, Math.min(100, numericOpacity)) / 100
28
+ : 1;
29
+ const normalizedOpacity = Number.isFinite(numericOpacity)
30
+ ? Math.max(0, Math.min(100, numericOpacity))
31
+ : Math.round(normalizedAlpha * 100);
32
+ return { color, alpha: normalizedAlpha, opacity: normalizedOpacity };
33
+ }
34
+
21
35
  /* Unique IDs for lab components such as propskit-group. */
22
36
  let figLabUniqueIdCounter = 0;
23
37
  function figLabUniqueId(prefix = "fig-lab") {
@@ -3243,18 +3257,29 @@ class FigCanvasControl extends HTMLElement {
3243
3257
  this.#syncPointPointCursors();
3244
3258
  }
3245
3259
 
3246
- #emitInput() {
3260
+ #emitInput(detail = this.value) {
3247
3261
  this.dispatchEvent(
3248
- new CustomEvent("input", { bubbles: true, detail: this.value }),
3262
+ new CustomEvent("input", { bubbles: true, detail }),
3249
3263
  );
3250
3264
  }
3251
3265
 
3252
- #emitChange() {
3266
+ #emitChange(detail = this.value) {
3253
3267
  this.dispatchEvent(
3254
- new CustomEvent("change", { bubbles: true, detail: this.value }),
3268
+ new CustomEvent("change", { bubbles: true, detail }),
3255
3269
  );
3256
3270
  }
3257
3271
 
3272
+ #colorEventDetail(detail) {
3273
+ return {
3274
+ ...this.value,
3275
+ ...figLabColorEventAliases(
3276
+ detail.color,
3277
+ detail.alpha,
3278
+ detail.opacity,
3279
+ ),
3280
+ };
3281
+ }
3282
+
3258
3283
  #syncValueAttribute() {
3259
3284
  this.setAttribute("value", JSON.stringify(this.value));
3260
3285
  }
@@ -3265,8 +3290,11 @@ class FigCanvasControl extends HTMLElement {
3265
3290
  this.#pointHandle.addEventListener("input", (e) => {
3266
3291
  e.stopPropagation();
3267
3292
  if (e.detail?.color) {
3268
- this.setAttribute("color", e.detail.color);
3269
- this.#emitInput();
3293
+ this.setAttribute(
3294
+ "color",
3295
+ this.#pointHandle.getAttribute("color") || e.detail.color,
3296
+ );
3297
+ this.#emitInput(this.#colorEventDetail(e.detail));
3270
3298
  return;
3271
3299
  }
3272
3300
  this.#isDragging = true;
@@ -3285,8 +3313,11 @@ class FigCanvasControl extends HTMLElement {
3285
3313
  this.#pointHandle.addEventListener("change", (e) => {
3286
3314
  e.stopPropagation();
3287
3315
  if (e.detail?.color) {
3288
- this.setAttribute("color", e.detail.color);
3289
- this.#emitChange();
3316
+ this.setAttribute(
3317
+ "color",
3318
+ this.#pointHandle.getAttribute("color") || e.detail.color,
3319
+ );
3320
+ this.#emitChange(this.#colorEventDetail(e.detail));
3290
3321
  return;
3291
3322
  }
3292
3323
  const px = e.detail?.px ?? this.#x / 100;
package/fig.js CHANGED
@@ -39,6 +39,20 @@ function figBooleanAttribute(element, name) {
39
39
  return element.hasAttribute(name) && element.getAttribute(name) !== "false";
40
40
  }
41
41
 
42
+ function figColorEventAliases(color, alpha, opacity) {
43
+ const numericAlpha = Number(alpha);
44
+ const numericOpacity = Number(opacity);
45
+ const normalizedAlpha = Number.isFinite(numericAlpha)
46
+ ? Math.max(0, Math.min(1, numericAlpha))
47
+ : Number.isFinite(numericOpacity)
48
+ ? Math.max(0, Math.min(100, numericOpacity)) / 100
49
+ : 1;
50
+ const normalizedOpacity = Number.isFinite(numericOpacity)
51
+ ? Math.max(0, Math.min(100, numericOpacity))
52
+ : Math.round(normalizedAlpha * 100);
53
+ return { color, alpha: normalizedAlpha, opacity: normalizedOpacity };
54
+ }
55
+
42
56
  function figNormalizeTextOnlyInputSlots(host) {
43
57
  host
44
58
  .querySelectorAll(':scope > [slot="prepend"], :scope > [slot="append"]')
@@ -9115,7 +9129,7 @@ class FigInputColor extends HTMLElement {
9115
9129
  new CustomEvent("input", {
9116
9130
  bubbles: true,
9117
9131
  cancelable: true,
9118
- detail: { value: this.value, hex: this.hex, rgba: this.rgba },
9132
+ detail: this.#eventDetail(),
9119
9133
  }),
9120
9134
  );
9121
9135
  }
@@ -9124,11 +9138,24 @@ class FigInputColor extends HTMLElement {
9124
9138
  new CustomEvent("change", {
9125
9139
  bubbles: true,
9126
9140
  cancelable: true,
9127
- detail: { value: this.value, hex: this.hex, rgba: this.rgba },
9141
+ detail: this.#eventDetail(),
9128
9142
  }),
9129
9143
  );
9130
9144
  }
9131
9145
 
9146
+ #eventDetail() {
9147
+ return {
9148
+ value: this.value,
9149
+ hex: this.hex,
9150
+ rgba: this.rgba,
9151
+ ...figColorEventAliases(
9152
+ this.hexOpaque,
9153
+ this.rgba?.a,
9154
+ Math.round((this.rgba?.a ?? 1) * 100),
9155
+ ),
9156
+ };
9157
+ }
9158
+
9132
9159
  static get observedAttributes() {
9133
9160
  return [
9134
9161
  "value",
@@ -11605,6 +11632,10 @@ class FigInputGradient extends HTMLElement {
11605
11632
  this.#gradient.stops[idx].color = e.detail.color;
11606
11633
  if (e.detail.opacity !== undefined) {
11607
11634
  this.#gradient.stops[idx].opacity = e.detail.opacity;
11635
+ } else if (e.detail.alpha !== undefined) {
11636
+ this.#gradient.stops[idx].opacity = Math.round(
11637
+ e.detail.alpha * 100,
11638
+ );
11608
11639
  }
11609
11640
  handle.setAttribute(
11610
11641
  "color",
@@ -13367,7 +13398,8 @@ figDefineElement("fig-video", FigVideo);
13367
13398
  /**
13368
13399
  * <fig-card> — Media card with optional link, selection chrome, and truncated label.
13369
13400
  *
13370
- * Composes a generated `fig-image` (or authored fig-image/fig-media/fig-preview).
13401
+ * Composes a generated `fig-image` when `src` is set. Without `src`, authored
13402
+ * children remain direct children and generated label content is appended.
13371
13403
  * Selection is attribute-only (`selected`); apps own toggle/group logic.
13372
13404
  * When `href` is set (and not disabled), content wraps in a real `<a>`.
13373
13405
  *
@@ -13405,18 +13437,39 @@ class FigCard extends HTMLElement {
13405
13437
  }
13406
13438
 
13407
13439
  #linkEl = null;
13408
- #mediaWrap = null;
13409
13440
  #textWrap = null;
13410
13441
  #imageEl = null;
13411
13442
  #labelEl = null;
13412
13443
  #sublabelEl = null;
13413
13444
  #usesAuthoredMedia = false;
13445
+ #mediaObserver = new MutationObserver((mutations) => {
13446
+ const authoredMediaChanged = mutations.some((mutation) =>
13447
+ [...mutation.addedNodes, ...mutation.removedNodes].some(
13448
+ (node) =>
13449
+ node instanceof Element &&
13450
+ (node.matches(
13451
+ "fig-image:not([data-generated]), fig-media:not([data-generated]), fig-preview:not([data-generated])",
13452
+ ) ||
13453
+ node.querySelector(
13454
+ "fig-image:not([data-generated]), fig-media:not([data-generated]), fig-preview:not([data-generated])",
13455
+ )),
13456
+ ),
13457
+ );
13458
+ if (!authoredMediaChanged || !this.isConnected) return;
13459
+ this.#ensureStructure();
13460
+ this.#sync();
13461
+ });
13414
13462
 
13415
13463
  connectedCallback() {
13464
+ this.#mediaObserver.observe(this, { childList: true, subtree: true });
13416
13465
  this.#ensureStructure();
13417
13466
  this.#sync();
13418
13467
  }
13419
13468
 
13469
+ disconnectedCallback() {
13470
+ this.#mediaObserver.disconnect();
13471
+ }
13472
+
13420
13473
  attributeChangedCallback() {
13421
13474
  if (!this.isConnected) return;
13422
13475
  this.#ensureStructure();
@@ -13441,13 +13494,27 @@ class FigCard extends HTMLElement {
13441
13494
  return raw === "2" ? "2" : "1";
13442
13495
  }
13443
13496
 
13497
+ #usesGeneratedMedia() {
13498
+ return Boolean(this.getAttribute("src"));
13499
+ }
13500
+
13444
13501
  #findAuthoredMedia() {
13445
13502
  return this.querySelector(
13446
- ":scope > fig-image:not([data-generated]), :scope > fig-media:not([data-generated]), :scope > fig-preview:not([data-generated]), :scope > .fig-card-link > .fig-card-media > fig-image:not([data-generated]), :scope > .fig-card-link > .fig-card-media > fig-media:not([data-generated]), :scope > .fig-card-link > .fig-card-media > fig-preview:not([data-generated])",
13503
+ ":scope > fig-image:not([data-generated]), :scope > fig-media:not([data-generated]), :scope > fig-preview:not([data-generated]), :scope > .fig-card-link > fig-image:not([data-generated]), :scope > .fig-card-link > fig-media:not([data-generated]), :scope > .fig-card-link > fig-preview:not([data-generated]), :scope > .fig-card-link > .fig-card-media > fig-image:not([data-generated]), :scope > .fig-card-link > .fig-card-media > fig-media:not([data-generated]), :scope > .fig-card-link > .fig-card-media > fig-preview:not([data-generated])",
13447
13504
  );
13448
13505
  }
13449
13506
 
13450
13507
  #ensureStructure() {
13508
+ if (!this.#usesGeneratedMedia()) {
13509
+ this.#ensureManualStructure();
13510
+ return;
13511
+ }
13512
+
13513
+ this.querySelectorAll(":scope > .fig-card-text[data-generated]").forEach(
13514
+ (element) => element.remove(),
13515
+ );
13516
+ if (this.#textWrap?.parentElement === this) this.#textWrap = null;
13517
+
13451
13518
  const authored = this.#findAuthoredMedia();
13452
13519
  this.#usesAuthoredMedia = Boolean(authored);
13453
13520
 
@@ -13468,29 +13535,30 @@ class FigCard extends HTMLElement {
13468
13535
  this.#linkEl = next;
13469
13536
  }
13470
13537
 
13471
- if (!this.#mediaWrap || !this.#mediaWrap.isConnected) {
13472
- this.#mediaWrap = this.#linkEl.querySelector(":scope > .fig-card-media");
13473
- if (!this.#mediaWrap) {
13474
- this.#mediaWrap = document.createElement("div");
13475
- this.#mediaWrap.className = "fig-card-media";
13476
- this.#linkEl.prepend(this.#mediaWrap);
13538
+ const legacyMediaWrap = this.#linkEl.querySelector(
13539
+ ":scope > .fig-card-media",
13540
+ );
13541
+ if (legacyMediaWrap) {
13542
+ while (legacyMediaWrap.firstChild) {
13543
+ this.#linkEl.insertBefore(legacyMediaWrap.firstChild, legacyMediaWrap);
13477
13544
  }
13545
+ legacyMediaWrap.remove();
13478
13546
  }
13479
13547
 
13480
- if (authored && authored.parentElement !== this.#mediaWrap) {
13481
- this.#mediaWrap
13482
- .querySelectorAll("[data-generated]")
13548
+ if (authored && authored.parentElement !== this.#linkEl) {
13549
+ this.#linkEl
13550
+ .querySelectorAll(":scope > [data-generated]:not(.fig-card-text)")
13483
13551
  .forEach((el) => el.remove());
13484
- this.#mediaWrap.appendChild(authored);
13552
+ this.#linkEl.prepend(authored);
13485
13553
  this.#imageEl = null;
13486
13554
  }
13487
13555
 
13488
13556
  if (!this.#usesAuthoredMedia) {
13489
13557
  this.#imageEl =
13490
- this.#mediaWrap.querySelector(":scope > fig-image[data-generated]") ||
13558
+ this.#linkEl.querySelector(":scope > fig-image[data-generated]") ||
13491
13559
  null;
13492
13560
  if (!this.#imageEl) {
13493
- this.#mediaWrap
13561
+ this.#linkEl
13494
13562
  .querySelectorAll(
13495
13563
  ":scope > fig-image, :scope > fig-media, :scope > fig-preview",
13496
13564
  )
@@ -13501,12 +13569,26 @@ class FigCard extends HTMLElement {
13501
13569
  this.#imageEl.setAttribute("data-generated", "");
13502
13570
  this.#imageEl.setAttribute("full", "");
13503
13571
  this.#imageEl.setAttribute("size", "auto");
13504
- this.#mediaWrap.appendChild(this.#imageEl);
13572
+ this.#linkEl.prepend(this.#imageEl);
13505
13573
  }
13506
13574
  }
13507
13575
 
13508
13576
  if (!this.#textWrap || !this.#textWrap.isConnected) {
13509
13577
  this.#textWrap = this.#linkEl.querySelector(":scope > .fig-card-text");
13578
+ if (this.#textWrap?.tagName.toLowerCase() !== "div") {
13579
+ const text = document.createElement("div");
13580
+ text.className = "fig-card-text";
13581
+ text.setAttribute("data-generated", "");
13582
+ if (this.#textWrap) {
13583
+ while (this.#textWrap.firstChild) {
13584
+ text.appendChild(this.#textWrap.firstChild);
13585
+ }
13586
+ this.#textWrap.replaceWith(text);
13587
+ } else {
13588
+ this.#linkEl.appendChild(text);
13589
+ }
13590
+ this.#textWrap = text;
13591
+ }
13510
13592
  if (!this.#textWrap) {
13511
13593
  this.#textWrap = document.createElement("div");
13512
13594
  this.#textWrap.className = "fig-card-text";
@@ -13542,7 +13624,7 @@ class FigCard extends HTMLElement {
13542
13624
  ":scope > .fig-card-sublabel[data-generated]",
13543
13625
  ) || null;
13544
13626
  if (!this.#sublabelEl) {
13545
- this.#sublabelEl = document.createElement("span");
13627
+ this.#sublabelEl = document.createElement("label");
13546
13628
  this.#sublabelEl.className = "fig-card-sublabel";
13547
13629
  this.#sublabelEl.setAttribute("data-generated", "");
13548
13630
  }
@@ -13551,7 +13633,49 @@ class FigCard extends HTMLElement {
13551
13633
  }
13552
13634
  }
13553
13635
 
13636
+ #ensureManualStructure() {
13637
+ const staleLink =
13638
+ this.#linkEl || this.querySelector(":scope > .fig-card-link");
13639
+ if (staleLink) {
13640
+ staleLink
13641
+ .querySelectorAll(
13642
+ "fig-image:not([data-generated]), fig-media:not([data-generated]), fig-preview:not([data-generated])",
13643
+ )
13644
+ .forEach((media) => this.insertBefore(media, staleLink));
13645
+ staleLink.remove();
13646
+ }
13647
+ this.querySelectorAll(":scope > [data-generated]").forEach((element) =>
13648
+ element.remove(),
13649
+ );
13650
+
13651
+ this.#linkEl = null;
13652
+ this.#imageEl = null;
13653
+ this.#usesAuthoredMedia = true;
13654
+
13655
+ this.#textWrap = document.createElement("div");
13656
+ this.#textWrap.className = "fig-card-text";
13657
+ this.#textWrap.setAttribute("data-generated", "");
13658
+ this.#labelEl = document.createElement("label");
13659
+ this.#labelEl.className = "fig-card-label";
13660
+ this.#labelEl.setAttribute("data-generated", "");
13661
+ this.#textWrap.appendChild(this.#labelEl);
13662
+
13663
+ const sublabel = this.#sublabelText();
13664
+ this.#sublabelEl = null;
13665
+ if (sublabel) {
13666
+ this.#sublabelEl = document.createElement("label");
13667
+ this.#sublabelEl.className = "fig-card-sublabel";
13668
+ this.#sublabelEl.setAttribute("data-generated", "");
13669
+ this.#textWrap.appendChild(this.#sublabelEl);
13670
+ }
13671
+ this.appendChild(this.#textWrap);
13672
+ }
13673
+
13554
13674
  #sync() {
13675
+ if (!this.#usesGeneratedMedia()) {
13676
+ this.#syncManual();
13677
+ return;
13678
+ }
13555
13679
  if (!this.#linkEl) return;
13556
13680
 
13557
13681
  const disabled = figBooleanAttribute(this, "disabled");
@@ -13634,6 +13758,29 @@ class FigCard extends HTMLElement {
13634
13758
  this.#textWrap.hidden = !label && !sublabel;
13635
13759
  }
13636
13760
  }
13761
+
13762
+ #syncManual() {
13763
+ const disabled = figBooleanAttribute(this, "disabled");
13764
+ const label = this.#labelText();
13765
+ const sublabel = this.#sublabelText();
13766
+
13767
+ this.style.setProperty(
13768
+ "--fig-card-label-line-clamp",
13769
+ this.#lineClamp(),
13770
+ );
13771
+ this.setAttribute("role", "group");
13772
+ const ariaLabel = [label, sublabel].filter(Boolean).join(", ");
13773
+ if (ariaLabel) this.setAttribute("aria-label", ariaLabel);
13774
+ else this.removeAttribute("aria-label");
13775
+ if (disabled) this.setAttribute("aria-disabled", "true");
13776
+ else this.removeAttribute("aria-disabled");
13777
+
13778
+ if (this.#labelEl) {
13779
+ this.#labelEl.textContent = label;
13780
+ this.#labelEl.hidden = !label;
13781
+ }
13782
+ if (this.#sublabelEl) this.#sublabelEl.textContent = sublabel;
13783
+ }
13637
13784
  }
13638
13785
  figDefineElement("fig-card", FigCard);
13639
13786
 
@@ -17637,6 +17784,14 @@ class FigColorTip extends HTMLElement {
17637
17784
  eventDetail.opacity = Math.round(detail.alpha * 100);
17638
17785
  }
17639
17786
  }
17787
+ Object.assign(
17788
+ eventDetail,
17789
+ figColorEventAliases(
17790
+ eventDetail.color,
17791
+ this.#alphaEnabled ? detail?.alpha : 1,
17792
+ eventDetail.opacity,
17793
+ ),
17794
+ );
17640
17795
 
17641
17796
  this.dispatchEvent(
17642
17797
  new CustomEvent(type, {
@@ -19183,7 +19338,11 @@ class FigHandle extends HTMLElement {
19183
19338
  : detail.alpha !== undefined
19184
19339
  ? Math.round(detail.alpha * 100)
19185
19340
  : undefined;
19186
- return { color: detail.color, opacity };
19341
+ return {
19342
+ color: detail.color,
19343
+ opacity,
19344
+ ...figColorEventAliases(detail.color, detail.alpha, opacity),
19345
+ };
19187
19346
  }
19188
19347
 
19189
19348
  #handleDirectColorPickerInput = (e) => {
@@ -19214,7 +19373,11 @@ class FigHandle extends HTMLElement {
19214
19373
 
19215
19374
  #detailFromNativeColor(value) {
19216
19375
  const { opacity } = this.#normalizeColorForPicker();
19217
- return opacity < 100 ? { color: value, opacity } : { color: value };
19376
+ const detail = opacity < 100 ? { color: value, opacity } : { color: value };
19377
+ return {
19378
+ ...detail,
19379
+ ...figColorEventAliases(value, undefined, opacity),
19380
+ };
19218
19381
  }
19219
19382
 
19220
19383
  #handleNativeColorInput = (e) => {
@@ -19249,14 +19412,23 @@ class FigHandle extends HTMLElement {
19249
19412
  #handleColorTipInput = (e) => {
19250
19413
  e.stopPropagation();
19251
19414
  if (e.detail?.color) {
19415
+ const detail = {
19416
+ color: e.detail.color,
19417
+ opacity: e.detail.opacity,
19418
+ ...figColorEventAliases(
19419
+ e.detail.color,
19420
+ e.detail.alpha,
19421
+ e.detail.opacity,
19422
+ ),
19423
+ };
19252
19424
  this.setAttribute(
19253
19425
  "color",
19254
- this.#colorWithOpacity(e.detail.color, e.detail.opacity),
19426
+ this.#colorWithOpacity(detail.color, detail.opacity),
19255
19427
  );
19256
19428
  this.dispatchEvent(
19257
19429
  new CustomEvent("input", {
19258
19430
  bubbles: true,
19259
- detail: { color: e.detail.color, opacity: e.detail.opacity },
19431
+ detail,
19260
19432
  }),
19261
19433
  );
19262
19434
  }
@@ -19265,14 +19437,23 @@ class FigHandle extends HTMLElement {
19265
19437
  #handleColorTipChange = (e) => {
19266
19438
  e.stopPropagation();
19267
19439
  if (e.detail?.color) {
19440
+ const detail = {
19441
+ color: e.detail.color,
19442
+ opacity: e.detail.opacity,
19443
+ ...figColorEventAliases(
19444
+ e.detail.color,
19445
+ e.detail.alpha,
19446
+ e.detail.opacity,
19447
+ ),
19448
+ };
19268
19449
  this.setAttribute(
19269
19450
  "color",
19270
- this.#colorWithOpacity(e.detail.color, e.detail.opacity),
19451
+ this.#colorWithOpacity(detail.color, detail.opacity),
19271
19452
  );
19272
19453
  this.dispatchEvent(
19273
19454
  new CustomEvent("change", {
19274
19455
  bubbles: true,
19275
- detail: { color: e.detail.color, opacity: e.detail.opacity },
19456
+ detail,
19276
19457
  }),
19277
19458
  );
19278
19459
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.25.0",
3
+ "version": "6.26.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",