@rogieking/figui3 6.23.3 → 6.24.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-editor.js CHANGED
@@ -1521,7 +1521,7 @@ class FigFillPicker extends HTMLElement {
1521
1521
  })
1522
1522
  .join("");
1523
1523
  const separator = group.label || group.separator
1524
- ? `<fig-menu-separator${group.label ? ` label="${figEditorEscapeAttribute(group.label)}"` : ""}></fig-menu-separator>`
1524
+ ? `<fig-separator${group.label ? ` label="${figEditorEscapeAttribute(group.label)}"` : ""}></fig-separator>`
1525
1525
  : "";
1526
1526
  return `${separator}${options}`;
1527
1527
  })
package/fig-lab.css CHANGED
@@ -8,6 +8,109 @@
8
8
  * <link rel="stylesheet" href="fig-lab.css">
9
9
  */
10
10
 
11
+ fig-ai-prompt {
12
+ --fig-ai-prompt-margin-inline: var(--spacer-2);
13
+
14
+ display: grid;
15
+ grid-template-columns: 1fr auto;
16
+ grid-template-rows: minmax(var(--spacer-6), 1fr) auto;
17
+ position: relative;
18
+ gap: var(--spacer-2);
19
+ box-sizing: border-box;
20
+ width: calc(100% - var(--fig-ai-prompt-margin-inline) * 2);
21
+ min-height: 8rem;
22
+ margin-inline: var(--fig-ai-prompt-margin-inline);
23
+ padding: var(--spacer-3);
24
+ border: 1px solid var(--figma-color-border);
25
+ border-radius: var(--radius-large);
26
+ background: var(--figma-color-bg);
27
+ color: var(--figma-color-text);
28
+
29
+ &:focus-within {
30
+ outline: var(--figma-focus-outline);
31
+ outline-offset: var(--figma-focus-outline-offset);
32
+ }
33
+
34
+ & > fig-input-text {
35
+ position: absolute;
36
+ inset: 0;
37
+ width: 100%;
38
+ height: 100%;
39
+ border-radius: 0;
40
+ background: transparent;
41
+ box-shadow: none;
42
+ font-size: var(--body-large-fontSize);
43
+
44
+ &:hover,
45
+ &:has(textarea:focus) {
46
+ outline: 0;
47
+ box-shadow: none;
48
+ }
49
+
50
+ & > textarea {
51
+ box-sizing: border-box;
52
+ width: 100%;
53
+ height: 100%;
54
+ min-height: var(--spacer-6);
55
+ padding: var(--spacer-3);
56
+ padding-bottom: calc(var(--spacer-5) + var(--spacer-3));
57
+ resize: none;
58
+ font-size: inherit;
59
+ }
60
+ }
61
+
62
+ & > fig-button {
63
+ grid-row: 2;
64
+ align-self: end;
65
+ position: relative;
66
+ z-index: 1;
67
+ color: var(--figma-color-text);
68
+
69
+ &:first-of-type {
70
+ justify-self: start;
71
+ }
72
+
73
+ &:last-of-type {
74
+ justify-self: end;
75
+ }
76
+ }
77
+ }
78
+
79
+ fig-chat-message {
80
+ display: block;
81
+ box-sizing: border-box;
82
+ width: 100%;
83
+ max-width: 100%;
84
+ color: var(--figma-color-text);
85
+ font-size: var(--body-large-fontSize);
86
+ line-height: calc(var(--line-height) + var(--spacer-1));
87
+ overflow-wrap: anywhere;
88
+ white-space: pre-wrap;
89
+
90
+ &[from="user"] {
91
+ width: auto;
92
+ margin-inline-start: var(--spacer-4);
93
+ padding: var(--spacer-2) var(--spacer-2-5);
94
+ border-radius: var(--radius-large);
95
+ background: var(--figma-color-bg-secondary);
96
+ }
97
+
98
+ &[from="user"]:has(> fig-avatar) {
99
+ position: relative;
100
+ margin-inline-end: calc(var(--spacer-4) + var(--spacer-2));
101
+
102
+ & > fig-avatar {
103
+ position: absolute;
104
+ right: calc(-1 * (var(--spacer-4) + var(--spacer-2)));
105
+ bottom: 0;
106
+ }
107
+ }
108
+
109
+ & + fig-chat-message {
110
+ margin-top: var(--spacer-3);
111
+ }
112
+ }
113
+
11
114
  propskit-group {
12
115
  display: block;
13
116
  width: 100%;
package/fig-lab.js CHANGED
@@ -25,6 +25,14 @@ function figLabUniqueId(prefix = "fig-lab") {
25
25
  return `${prefix}-${figLabUniqueIdCounter}`;
26
26
  }
27
27
 
28
+ /* Presentation-only composition surface for AI prompt controls. */
29
+ class FigAiPrompt extends HTMLElement {}
30
+ figLabDefineElement("fig-ai-prompt", FigAiPrompt);
31
+
32
+ /* Presentation-only message surface for AI conversations. */
33
+ class FigChatMessage extends HTMLElement {}
34
+ figLabDefineElement("fig-chat-message", FigChatMessage);
35
+
28
36
  /* Field + Switch wrapper */
29
37
  class PropskitSwitch extends HTMLElement {
30
38
  #field = null;
@@ -1685,6 +1693,10 @@ class PropskitSlider extends HTMLElement {
1685
1693
 
1686
1694
  for (const mutation of mutations) {
1687
1695
  if (mutation.type === "attributes") {
1696
+ if (mutation.attributeName === "value") {
1697
+ this.#pushExternalValueToSlider();
1698
+ continue;
1699
+ }
1688
1700
  if (
1689
1701
  mutation.attributeName &&
1690
1702
  this.#ignoredSliderAttrs.has(mutation.attributeName)
@@ -1822,7 +1834,9 @@ class PropskitSlider extends HTMLElement {
1822
1834
  if (!this.#slider) return;
1823
1835
  const hostAttrs = this.#getForwardedSliderAttrNames();
1824
1836
 
1825
- const nextManaged = new Set(hostAttrs.filter((name) => name !== "text"));
1837
+ const nextManaged = new Set(
1838
+ hostAttrs.filter((name) => name !== "text" && name !== "value"),
1839
+ );
1826
1840
 
1827
1841
  for (const attrName of this.#managedSliderAttrs) {
1828
1842
  if (!nextManaged.has(attrName)) {
@@ -1831,7 +1845,7 @@ class PropskitSlider extends HTMLElement {
1831
1845
  }
1832
1846
 
1833
1847
  for (const attrName of hostAttrs) {
1834
- if (attrName === "text") continue;
1848
+ if (attrName === "text" || attrName === "value") continue;
1835
1849
  const value = this.getAttribute(attrName) ?? "";
1836
1850
  if (this.#slider.getAttribute(attrName) !== value) {
1837
1851
  this.#slider.setAttribute(attrName, value);
@@ -1868,9 +1882,18 @@ class PropskitSlider extends HTMLElement {
1868
1882
  }
1869
1883
 
1870
1884
  this.#managedSliderAttrs = nextManaged;
1885
+ this.#pushExternalValueToSlider();
1871
1886
  this.#queueSteppersSync();
1872
1887
  }
1873
1888
 
1889
+ #pushExternalValueToSlider() {
1890
+ if (!this.#slider) return;
1891
+ const next = this.getAttribute("value") ?? "";
1892
+ if (String(this.#slider.value) !== next) {
1893
+ this.#slider.value = next;
1894
+ }
1895
+ }
1896
+
1874
1897
  #getForwardedSliderAttrNames() {
1875
1898
  const reserved = new Set([
1876
1899
  "label",
@@ -2259,7 +2282,10 @@ class PropskitSlider extends HTMLElement {
2259
2282
  ? event.detail
2260
2283
  : this.#slider?.value;
2261
2284
  if (this.#slider?.value !== undefined) {
2262
- this.setAttribute("value", String(this.#slider.value));
2285
+ const next = String(this.#slider.value);
2286
+ if (this.getAttribute("value") !== next) {
2287
+ this.setAttribute("value", next);
2288
+ }
2263
2289
  }
2264
2290
  this.dispatchEvent(
2265
2291
  new CustomEvent(type, {
package/fig.js CHANGED
@@ -5425,7 +5425,7 @@ class FigSelect extends HTMLElement {
5425
5425
  return (
5426
5426
  node?.nodeType === 1 &&
5427
5427
  (node.tagName === "FIG-SELECT-OPTION" ||
5428
- node.tagName === "FIG-MENU-SEPARATOR" ||
5428
+ node.tagName === "FIG-SEPARATOR" ||
5429
5429
  node.tagName === "FIG-SELECT-OPTIONS")
5430
5430
  );
5431
5431
  }
@@ -6282,6 +6282,7 @@ figDefineElement("fig-select", FigSelect);
6282
6282
  */
6283
6283
  class FigSlider extends HTMLElement {
6284
6284
  #isInteracting = false;
6285
+ #pendingRegeneration = false;
6285
6286
  #showEmptyTextValue = false;
6286
6287
  #isSyncingValueAttribute = false;
6287
6288
  #value = "";
@@ -6347,6 +6348,10 @@ class FigSlider extends HTMLElement {
6347
6348
  };
6348
6349
  this.#boundRangePointerUp = () => {
6349
6350
  this.#isInteracting = false;
6351
+ if (this.#pendingRegeneration && this.isConnected) {
6352
+ this.#pendingRegeneration = false;
6353
+ this.#regenerateInnerHTML();
6354
+ }
6350
6355
  };
6351
6356
  }
6352
6357
 
@@ -6616,6 +6621,7 @@ class FigSlider extends HTMLElement {
6616
6621
  }
6617
6622
 
6618
6623
  disconnectedCallback() {
6624
+ this.#pendingRegeneration = false;
6619
6625
  if (this.input) {
6620
6626
  this.input.removeEventListener("input", this.#boundHandleInput);
6621
6627
  this.input.removeEventListener("change", this.#boundHandleChange);
@@ -6828,6 +6834,62 @@ class FigSlider extends HTMLElement {
6828
6834
  }
6829
6835
  }
6830
6836
 
6837
+ #syncRangeConstraint(name, rawValue) {
6838
+ if (!this.input) return;
6839
+ const defaults = this.#typeDefaults[this.type] || this.#typeDefaults.range;
6840
+ const parsed =
6841
+ rawValue === null || String(rawValue).trim() === ""
6842
+ ? defaults[name]
6843
+ : Number(rawValue);
6844
+ let next = Number.isFinite(parsed) ? parsed : defaults[name];
6845
+ if (name === "step" && !(next > 0)) {
6846
+ next = defaults.step > 0 ? defaults.step : 1;
6847
+ }
6848
+ if (this[name] === next && this.input[name] === String(next)) return;
6849
+
6850
+ this[name] = next;
6851
+ this.input[name] = String(next);
6852
+ if (name === "min") {
6853
+ this.input.setAttribute("aria-valuemin", String(next));
6854
+ } else if (name === "max") {
6855
+ this.input.setAttribute("aria-valuemax", String(next));
6856
+ }
6857
+ if (this.figInputNumber) {
6858
+ this.figInputNumber.setAttribute(name, String(next));
6859
+ }
6860
+ if (!this.hasAttribute("default") && this.type !== "delta") {
6861
+ this.default = this.min;
6862
+ }
6863
+ this.value = this.#normalizeSliderValue(this.value);
6864
+ this.#syncProperties();
6865
+ this.#syncStepperDatalist();
6866
+ }
6867
+
6868
+ #syncStepperDatalist() {
6869
+ if (this.type !== "stepper" || !this.datalist) return;
6870
+ const steps = Math.min(
6871
+ 1001,
6872
+ Math.max(0, Math.floor((this.max - this.min) / this.step) + 1),
6873
+ );
6874
+ const fragment = document.createDocumentFragment();
6875
+ for (let i = 0; i < steps; i++) {
6876
+ const option = document.createElement("option");
6877
+ option.setAttribute("value", this.min + i * this.step);
6878
+ fragment.append(option);
6879
+ }
6880
+ this.datalist.replaceChildren(fragment);
6881
+ this.input.setAttribute("list", this.datalist.id);
6882
+ }
6883
+
6884
+ #regenerateWhenIdle() {
6885
+ if (this.#isInteracting) {
6886
+ this.#pendingRegeneration = true;
6887
+ return;
6888
+ }
6889
+ this.#pendingRegeneration = false;
6890
+ this.#regenerateInnerHTML();
6891
+ }
6892
+
6831
6893
  static get observedAttributes() {
6832
6894
  return [
6833
6895
  "value",
@@ -6862,6 +6924,7 @@ class FigSlider extends HTMLElement {
6862
6924
  }
6863
6925
 
6864
6926
  attributeChangedCallback(name, oldValue, newValue) {
6927
+ if (oldValue === newValue) return;
6865
6928
  if (name === "value" && this.#isSyncingValueAttribute) return;
6866
6929
  if (this.input) {
6867
6930
  switch (name) {
@@ -6922,15 +6985,26 @@ class FigSlider extends HTMLElement {
6922
6985
  case "min":
6923
6986
  case "max":
6924
6987
  case "step":
6988
+ this.#syncRangeConstraint(name, newValue);
6989
+ break;
6990
+ case "units":
6991
+ this.units = newValue || "";
6992
+ if (this.figInputNumber) {
6993
+ if (this.units) {
6994
+ this.figInputNumber.setAttribute("units", this.units);
6995
+ } else {
6996
+ this.figInputNumber.removeAttribute("units");
6997
+ }
6998
+ }
6999
+ break;
6925
7000
  case "type":
6926
7001
  case "variant":
6927
- case "units":
6928
- this[name] = newValue;
6929
- this.#regenerateInnerHTML();
7002
+ if (!this.#isInteracting) this[name] = newValue;
7003
+ this.#regenerateWhenIdle();
6930
7004
  break;
6931
7005
  case "text":
6932
- this.text = newValue !== "false";
6933
- this.#regenerateInnerHTML();
7006
+ if (!this.#isInteracting) this.text = newValue !== "false";
7007
+ this.#regenerateWhenIdle();
6934
7008
  break;
6935
7009
  case "aria-label":
6936
7010
  case "aria-labelledby":
@@ -14448,7 +14522,7 @@ class FigEasingCurve extends HTMLElement {
14448
14522
  if (!this.#isEditEnabled() && !p.value && !p.spring) continue;
14449
14523
  if (p.group !== currentGroup) {
14450
14524
  if (p.group) {
14451
- optionsHTML += `<fig-menu-separator label="${FigEasingCurve.#escapeAttribute(p.group)}"></fig-menu-separator>`;
14525
+ optionsHTML += `<fig-separator label="${FigEasingCurve.#escapeAttribute(p.group)}"></fig-separator>`;
14452
14526
  }
14453
14527
  currentGroup = p.group;
14454
14528
  }
@@ -19276,10 +19350,10 @@ class FigMenuItem extends HTMLElement {
19276
19350
  figDefineElement("fig-menu-item", FigMenuItem);
19277
19351
 
19278
19352
  /**
19279
- * Visual divider between menu item groups.
19353
+ * Visual divider between content groups.
19280
19354
  * @attr {string} label - Optional group label shown in secondary text under the line.
19281
19355
  */
19282
- class FigMenuSeparator extends HTMLElement {
19356
+ class FigSeparator extends HTMLElement {
19283
19357
  static get observedAttributes() {
19284
19358
  return ["label"];
19285
19359
  }
@@ -19311,7 +19385,7 @@ class FigMenuSeparator extends HTMLElement {
19311
19385
  else this.removeAttribute("aria-label");
19312
19386
  }
19313
19387
  }
19314
- figDefineElement("fig-menu-separator", FigMenuSeparator);
19388
+ figDefineElement("fig-separator", FigSeparator);
19315
19389
 
19316
19390
  class FigMenu extends HTMLElement {
19317
19391
  #popup = null;
@@ -19390,7 +19464,7 @@ class FigMenu extends HTMLElement {
19390
19464
  this.#popup.removeEventListener("close", this.#boundPopupClose);
19391
19465
  const items = Array.from(
19392
19466
  this.#panel?.querySelectorAll(
19393
- ":scope > fig-menu-item, :scope > fig-menu-separator",
19467
+ ":scope > fig-menu-item, :scope > fig-separator",
19394
19468
  ) ?? [],
19395
19469
  );
19396
19470
  for (const item of items) this.insertBefore(item, this.#popup);
@@ -19433,7 +19507,7 @@ class FigMenu extends HTMLElement {
19433
19507
  #detectTrigger() {
19434
19508
  this.#trigger =
19435
19509
  this.querySelector("[fig-menu-trigger]") ||
19436
- this.querySelector(":scope > :not(fig-menu-item):not(fig-menu-separator)");
19510
+ this.querySelector(":scope > :not(fig-menu-item):not(fig-separator)");
19437
19511
  }
19438
19512
 
19439
19513
  #usesContextMenuTrigger() {
@@ -19473,7 +19547,7 @@ class FigMenu extends HTMLElement {
19473
19547
  #moveItemsToPopup() {
19474
19548
  if (!this.#panel) return;
19475
19549
  const items = Array.from(this.querySelectorAll(
19476
- ":scope > fig-menu-item, :scope > fig-menu-separator"
19550
+ ":scope > fig-menu-item, :scope > fig-separator"
19477
19551
  ));
19478
19552
  for (const item of items) {
19479
19553
  this.#panel.appendChild(item);
@@ -19513,7 +19587,8 @@ class FigMenu extends HTMLElement {
19513
19587
  for (const node of mutation.addedNodes) {
19514
19588
  if (node.nodeType !== 1 || node === this.#popup) continue;
19515
19589
  if (
19516
- (node.tagName === "FIG-MENU-ITEM" || node.tagName === "FIG-MENU-SEPARATOR") &&
19590
+ (node.tagName === "FIG-MENU-ITEM" ||
19591
+ node.tagName === "FIG-SEPARATOR") &&
19517
19592
  node.parentElement === this
19518
19593
  ) {
19519
19594
  this.#panel?.appendChild(node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.23.3",
3
+ "version": "6.24.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",