@rogieking/figui3 6.29.0 → 6.30.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.css CHANGED
@@ -63,6 +63,12 @@ fig-ai-prompt {
63
63
  }
64
64
  }
65
65
 
66
+ & > fig-attachments {
67
+ box-sizing: border-box;
68
+ width: auto;
69
+ margin: var(--spacer-2) var(--spacer-3) 0;
70
+ }
71
+
66
72
  & > fig-footer {
67
73
  justify-content: space-between;
68
74
  border: none;
@@ -70,6 +76,81 @@ fig-ai-prompt {
70
76
  }
71
77
  }
72
78
 
79
+ fig-attachments {
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ align-items: flex-start;
83
+ gap: var(--spacer-2);
84
+ width: 100%;
85
+ min-width: 0;
86
+ }
87
+
88
+ fig-attachment {
89
+ --fig-attachment-size: 2.5rem;
90
+
91
+ position: relative;
92
+ display: block;
93
+ flex: 0 0 var(--fig-attachment-size);
94
+ width: var(--fig-attachment-size);
95
+ min-width: 0;
96
+
97
+ fig-image {
98
+ display: flex;
99
+ width: 100%;
100
+ }
101
+
102
+ .fig-attachment-fallback {
103
+ position: absolute;
104
+ inset: 0;
105
+ display: none;
106
+ place-items: center;
107
+ border-radius: var(--radius-medium);
108
+ background: var(--figma-color-bg-secondary);
109
+ box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
110
+ color: var(--figma-color-text-secondary);
111
+ font-size: var(--body-medium-fontSize);
112
+ font-weight: 500;
113
+ line-height: 1;
114
+ pointer-events: none;
115
+ }
116
+
117
+ &[data-fallback] {
118
+ .fig-attachment-fallback {
119
+ display: grid;
120
+ }
121
+ }
122
+
123
+ .fig-attachment-remove {
124
+ --spacer-2: 0px;
125
+ --spacer-4: 1.25rem;
126
+
127
+ position: absolute;
128
+ z-index: 3;
129
+ top: var(--spacer-1);
130
+ right: var(--spacer-1);
131
+ width: 1.25rem;
132
+ min-width: 1.25rem;
133
+ height: 1.25rem;
134
+ padding: 0;
135
+ opacity: 0;
136
+ pointer-events: none;
137
+ }
138
+
139
+ &:hover .fig-attachment-remove,
140
+ &:focus-within .fig-attachment-remove {
141
+ opacity: 1;
142
+ pointer-events: auto;
143
+ }
144
+
145
+ &[disabled]:not([disabled="false"]) {
146
+ opacity: 0.5;
147
+
148
+ .fig-attachment-remove {
149
+ pointer-events: none;
150
+ }
151
+ }
152
+ }
153
+
73
154
  fig-chat-message {
74
155
  display: block;
75
156
  box-sizing: border-box;
@@ -234,7 +315,7 @@ fig-content:has(> propskit-group[name]:last-child) {
234
315
  padding-bottom: 0;
235
316
  }
236
317
 
237
- /* Default/large size: more left field padding; nudge trailing controls in from
318
+ /* Large size: more left field padding; nudge trailing controls in from
238
319
  the right. propskit-select applies left inset on label + trigger instead. */
239
320
  :is(
240
321
  propskit-switch,
@@ -243,27 +324,27 @@ fig-content:has(> propskit-group[name]:last-child) {
243
324
  propskit-text,
244
325
  propskit-number,
245
326
  propskit-slider
246
- ):is(:not([size]), [size="large"]) {
327
+ )[size="large"] {
247
328
  fig-field:not([direction="vertical"]) {
248
329
  padding-left: var(--spacer-2-5);
249
330
  }
250
331
  }
251
332
 
252
333
  /* Flex-positioned numbers — margin works. Absolute numbers use right: calc(...) below. */
253
- :is(propskit-color, propskit-number):is(:not([size]), [size="large"]) {
334
+ :is(propskit-color, propskit-number)[size="large"] {
254
335
  fig-field fig-input-number {
255
336
  margin-right: var(--spacer-1);
256
337
  }
257
338
  }
258
339
 
259
- propskit-text:is(:not([size]), [size="large"]) {
340
+ propskit-text[size="large"] {
260
341
  fig-field > fig-input-text {
261
342
  margin-right: var(--spacer-1);
262
343
  }
263
344
  }
264
345
 
265
346
  propskit-switch {
266
- --propskit-switch-height: 2rem;
347
+ --propskit-switch-height: 1.5rem;
267
348
  --propskit-switch-vertical-padding: var(--spacer-1);
268
349
  --propskit-switch-horizontal-padding: var(--spacer-3);
269
350
 
@@ -275,8 +356,9 @@ propskit-switch {
275
356
  padding: var(--propskit-switch-vertical-padding)
276
357
  var(--propskit-switch-horizontal-padding);
277
358
 
278
- &:not([size]),
279
359
  &[size="large"] {
360
+ --propskit-switch-height: 2rem;
361
+
280
362
  fig-field:not([direction="vertical"]) {
281
363
  padding-right: var(--spacer-1);
282
364
  }
@@ -339,7 +421,7 @@ propskit-switch {
339
421
  }
340
422
 
341
423
  propskit-color {
342
- --propskit-color-height: 2rem;
424
+ --propskit-color-height: 1.5rem;
343
425
  --propskit-color-vertical-padding: var(--spacer-1);
344
426
  --propskit-color-horizontal-padding: var(--spacer-3);
345
427
  --figma-focus-outline-offset: 1px;
@@ -352,6 +434,10 @@ propskit-color {
352
434
  padding: var(--propskit-color-vertical-padding)
353
435
  var(--propskit-color-horizontal-padding);
354
436
 
437
+ &[size="large"] {
438
+ --propskit-color-height: 2rem;
439
+ }
440
+
355
441
  &[disabled]:not([disabled="false"]) {
356
442
  pointer-events: none;
357
443
 
@@ -458,7 +544,7 @@ propskit-color {
458
544
  }
459
545
 
460
546
  propskit-gradient {
461
- --propskit-gradient-height: 2rem;
547
+ --propskit-gradient-height: 1.5rem;
462
548
  --propskit-gradient-vertical-padding: var(--spacer-1);
463
549
  --propskit-gradient-horizontal-padding: var(--spacer-3);
464
550
  --figma-focus-outline-offset: 1px;
@@ -471,6 +557,14 @@ propskit-gradient {
471
557
  padding: var(--propskit-gradient-vertical-padding)
472
558
  var(--propskit-gradient-horizontal-padding);
473
559
 
560
+ &[size="large"] {
561
+ --propskit-gradient-height: 2rem;
562
+
563
+ fig-field:not([direction="vertical"]) {
564
+ padding-right: var(--spacer-1);
565
+ }
566
+ }
567
+
474
568
  &[disabled]:not([disabled="false"]) {
475
569
  pointer-events: none;
476
570
 
@@ -538,7 +632,7 @@ propskit-gradient {
538
632
  }
539
633
 
540
634
  propskit-select {
541
- --propskit-select-height: 2rem;
635
+ --propskit-select-height: 1.5rem;
542
636
  --propskit-select-vertical-padding: var(--spacer-1);
543
637
  --propskit-select-horizontal-padding: var(--spacer-3);
544
638
  --figma-focus-outline-offset: 1px;
@@ -551,8 +645,9 @@ propskit-select {
551
645
  padding: var(--propskit-select-vertical-padding)
552
646
  var(--propskit-select-horizontal-padding);
553
647
 
554
- &:not([size]),
555
648
  &[size="large"] {
649
+ --propskit-select-height: 2rem;
650
+
556
651
  fig-field:not([direction="vertical"]) {
557
652
  padding-left: 0;
558
653
 
@@ -648,7 +743,7 @@ propskit-select {
648
743
  }
649
744
 
650
745
  propskit-text {
651
- --propskit-text-height: 2rem;
746
+ --propskit-text-height: 1.5rem;
652
747
  --propskit-text-vertical-padding: var(--spacer-1);
653
748
  --propskit-text-horizontal-padding: var(--spacer-3);
654
749
  --figma-focus-outline-offset: 1px;
@@ -661,6 +756,10 @@ propskit-text {
661
756
  padding: var(--propskit-text-vertical-padding)
662
757
  var(--propskit-text-horizontal-padding);
663
758
 
759
+ &[size="large"] {
760
+ --propskit-text-height: 2rem;
761
+ }
762
+
664
763
  &[disabled]:not([disabled="false"]) {
665
764
  pointer-events: none;
666
765
 
@@ -753,7 +852,7 @@ propskit-text {
753
852
  }
754
853
 
755
854
  propskit-number {
756
- --propskit-number-height: 2rem;
855
+ --propskit-number-height: 1.5rem;
757
856
  --propskit-number-vertical-padding: var(--spacer-1);
758
857
  --propskit-number-horizontal-padding: var(--spacer-3);
759
858
  --figma-focus-outline-offset: 1px;
@@ -766,8 +865,9 @@ propskit-number {
766
865
  padding: var(--propskit-number-vertical-padding)
767
866
  var(--propskit-number-horizontal-padding);
768
867
 
769
- &:not([size]),
770
868
  &[size="large"] {
869
+ --propskit-number-height: 2rem;
870
+
771
871
  fig-field:not([direction="vertical"]) > label {
772
872
  inset: 0 auto 0 var(--spacer-2-5);
773
873
  }
@@ -867,7 +967,7 @@ propskit-slider {
867
967
  --propskit-slider-elastic-scale: 1;
868
968
  --propskit-slider-elastic-origin: left center;
869
969
  --propskit-slider-elastic-transition: 0.18s cubic-bezier(0.2, 1.45, 0.35, 1);
870
- --propskit-slider-height: 2rem;
970
+ --propskit-slider-height: 1.5rem;
871
971
  --propskit-slider-vertical-padding: var(--spacer-1);
872
972
  --propskit-slider-horizontal-padding: var(--spacer-3);
873
973
  --figma-focus-outline-offset: 1px;
@@ -892,8 +992,8 @@ propskit-slider {
892
992
  padding-left: var(--propskit-slider-horizontal-padding);
893
993
  padding-right: var(--propskit-slider-horizontal-padding);
894
994
 
895
- &:not([size]),
896
995
  &[size="large"] {
996
+ --propskit-slider-height: 2rem;
897
997
  --propskit-slider-fig-slider-thumb-height: calc(
898
998
  var(--propskit-slider-fig-slider-height) - var(--spacer-3)
899
999
  );
package/fig-lab.js CHANGED
@@ -193,6 +193,205 @@ figLabDefineElement("fig-ai-prompt", FigAiPrompt);
193
193
  class FigChatMessage extends HTMLElement {}
194
194
  figLabDefineElement("fig-chat-message", FigChatMessage);
195
195
 
196
+ /**
197
+ * A removable image attachment for compact prompt surfaces.
198
+ *
199
+ * @attr {string} src - Image preview URL.
200
+ * @attr {string} name - Attachment name used for the tooltip and image alt text.
201
+ * @attr {string} value - Optional application-owned attachment identifier.
202
+ * @attr {boolean|string} removable - "false" hides the remove button.
203
+ * @attr {boolean|string} disabled - Disables removal.
204
+ * @fires remove - Cancelable request for the owning application to remove the attachment.
205
+ */
206
+ class FigAttachment extends HTMLElement {
207
+ #tooltip = null;
208
+ #image = null;
209
+ #fallback = null;
210
+ #removeTooltip = null;
211
+ #removeButton = null;
212
+ #mediaImage = null;
213
+ #boundHandleRemove = this.#handleRemove.bind(this);
214
+ #boundHandleImageLoad = this.#handleImageLoad.bind(this);
215
+ #boundHandleImageError = this.#handleImageError.bind(this);
216
+
217
+ static get observedAttributes() {
218
+ return ["src", "name", "value", "removable", "disabled"];
219
+ }
220
+
221
+ connectedCallback() {
222
+ if (!this.#image) this.#initialize();
223
+ this.#removeButton.removeEventListener("click", this.#boundHandleRemove);
224
+ this.#removeButton.addEventListener("click", this.#boundHandleRemove);
225
+ this.#bindMediaImage();
226
+ this.#sync();
227
+ }
228
+
229
+ disconnectedCallback() {
230
+ this.#removeButton?.removeEventListener("click", this.#boundHandleRemove);
231
+ this.#unbindMediaImage();
232
+ }
233
+
234
+ attributeChangedCallback(name, oldValue, newValue) {
235
+ if (oldValue === newValue || !this.#image) return;
236
+ this.#sync();
237
+ }
238
+
239
+ #initialize() {
240
+ this.#tooltip = document.createElement("fig-tooltip");
241
+ this.#tooltip.className = "fig-attachment-tooltip";
242
+
243
+ this.#image = document.createElement("fig-image");
244
+ this.#image.setAttribute("aspect-ratio", "1/1");
245
+ this.#image.setAttribute("fit", "cover");
246
+ this.#image.setAttribute("full", "");
247
+
248
+ this.#fallback = document.createElement("span");
249
+ this.#fallback.className = "fig-attachment-fallback";
250
+ this.#fallback.setAttribute("aria-hidden", "true");
251
+
252
+ this.#removeTooltip = document.createElement("fig-tooltip");
253
+ this.#removeTooltip.className = "fig-attachment-remove-tooltip";
254
+ this.#removeTooltip.setAttribute("text", "Remove attachment");
255
+
256
+ this.#removeButton = document.createElement("fig-button");
257
+ this.#removeButton.className = "fig-attachment-remove";
258
+ this.#removeButton.setAttribute("variant", "overlay");
259
+ this.#removeButton.setAttribute("size", "small");
260
+ this.#removeButton.setAttribute("icon", "");
261
+ const closeIcon = document.createElement("fig-icon");
262
+ closeIcon.setAttribute("name", "close");
263
+ closeIcon.setAttribute("size", "small");
264
+ this.#removeButton.append(closeIcon);
265
+ this.#removeTooltip.append(this.#removeButton);
266
+
267
+ this.#image.append(this.#fallback);
268
+ this.#tooltip.append(this.#image);
269
+ this.replaceChildren(this.#tooltip, this.#removeTooltip);
270
+
271
+ this.#removeButton.addEventListener("click", this.#boundHandleRemove);
272
+ this.#bindMediaImage();
273
+ }
274
+
275
+ #bindMediaImage() {
276
+ const mediaImage = this.#image?.mediaEl;
277
+ if (!mediaImage || mediaImage === this.#mediaImage) return;
278
+ this.#unbindMediaImage();
279
+ this.#mediaImage = mediaImage;
280
+ this.#mediaImage.addEventListener("load", this.#boundHandleImageLoad);
281
+ this.#mediaImage.addEventListener("error", this.#boundHandleImageError);
282
+ }
283
+
284
+ #unbindMediaImage() {
285
+ if (!this.#mediaImage) return;
286
+ this.#mediaImage.removeEventListener("load", this.#boundHandleImageLoad);
287
+ this.#mediaImage.removeEventListener("error", this.#boundHandleImageError);
288
+ this.#mediaImage = null;
289
+ }
290
+
291
+ #sync() {
292
+ const src = this.getAttribute("src") || "";
293
+ const name = this.getAttribute("name") || "Attachment";
294
+ const removable =
295
+ !this.hasAttribute("removable") || figLabBooleanAttribute(this, "removable");
296
+ const disabled = figLabBooleanAttribute(this, "disabled");
297
+
298
+ this.#tooltip.setAttribute("text", name);
299
+ this.#image.setAttribute("alt", name);
300
+ if (src) {
301
+ this.#image.setAttribute("src", src);
302
+ } else {
303
+ this.#image.removeAttribute("src");
304
+ }
305
+ this.#bindMediaImage();
306
+
307
+ this.#fallback.textContent = this.#fallbackLabel(name);
308
+ this.#removeButton.hidden = !removable;
309
+ this.#removeButton.toggleAttribute("disabled", disabled);
310
+ this.#removeButton.setAttribute("aria-label", `Remove ${name}`);
311
+ this.toggleAttribute("data-fallback", !src);
312
+ if (disabled) {
313
+ this.setAttribute("aria-disabled", "true");
314
+ } else {
315
+ this.removeAttribute("aria-disabled");
316
+ }
317
+ }
318
+
319
+ #fallbackLabel(name) {
320
+ const baseName = name.split(/[\\/]/).pop() || "";
321
+ const dotIndex = baseName.lastIndexOf(".");
322
+ const extension =
323
+ dotIndex > 0 && dotIndex < baseName.length - 1
324
+ ? baseName.slice(dotIndex + 1).toUpperCase()
325
+ : "FILE";
326
+ return extension.slice(0, 4);
327
+ }
328
+
329
+ #handleImageLoad() {
330
+ if (this.getAttribute("src")) this.removeAttribute("data-fallback");
331
+ }
332
+
333
+ #handleImageError() {
334
+ this.setAttribute("data-fallback", "");
335
+ }
336
+
337
+ #handleRemove(event) {
338
+ event.stopPropagation();
339
+ if (
340
+ figLabBooleanAttribute(this, "disabled") ||
341
+ (this.hasAttribute("removable") &&
342
+ !figLabBooleanAttribute(this, "removable"))
343
+ ) {
344
+ return;
345
+ }
346
+ this.dispatchEvent(
347
+ new CustomEvent("remove", {
348
+ bubbles: true,
349
+ cancelable: true,
350
+ composed: true,
351
+ detail: {
352
+ value: this.getAttribute("value"),
353
+ name: this.getAttribute("name") || "",
354
+ src: this.getAttribute("src") || "",
355
+ attachment: this,
356
+ },
357
+ }),
358
+ );
359
+ }
360
+ }
361
+ figLabDefineElement("fig-attachment", FigAttachment);
362
+
363
+ /* Wrapping presentation container for one or more attachments. */
364
+ class FigAttachments extends HTMLElement {
365
+ #observer = null;
366
+
367
+ connectedCallback() {
368
+ if (!this.hasAttribute("role")) {
369
+ this.setAttribute("role", "list");
370
+ this.setAttribute("data-generated-role", "");
371
+ }
372
+ this.#syncItems();
373
+ if (!this.#observer) {
374
+ this.#observer = new MutationObserver(() => this.#syncItems());
375
+ this.#observer.observe(this, { childList: true });
376
+ }
377
+ }
378
+
379
+ disconnectedCallback() {
380
+ this.#observer?.disconnect();
381
+ this.#observer = null;
382
+ }
383
+
384
+ #syncItems() {
385
+ for (const attachment of this.querySelectorAll(":scope > fig-attachment")) {
386
+ if (!attachment.hasAttribute("role")) {
387
+ attachment.setAttribute("role", "listitem");
388
+ attachment.setAttribute("data-generated-role", "");
389
+ }
390
+ }
391
+ }
392
+ }
393
+ figLabDefineElement("fig-attachments", FigAttachments);
394
+
196
395
  /* Field + Switch wrapper */
197
396
  class PropskitSwitch extends HTMLElement {
198
397
  #field = null;
@@ -900,9 +1099,6 @@ class PropskitGradient extends HTMLElement {
900
1099
  }
901
1100
 
902
1101
  if (!this.hasAttribute("edit")) this.#input.setAttribute("edit", "true");
903
- const size = this.getAttribute("size");
904
- if (!size || size === "large") this.#input.setAttribute("size", "large");
905
- else this.#input.removeAttribute("size");
906
1102
  this.#managedInputAttrs = nextManaged;
907
1103
  }
908
1104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.29.0",
3
+ "version": "6.30.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",