@rogieking/figui3 6.29.1 → 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;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.29.1",
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",