@rogieking/figui3 6.29.1 → 6.31.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
@@ -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/fig.js CHANGED
@@ -13455,6 +13455,7 @@ figDefineElement("fig-video", FigVideo);
13455
13455
  * @attr {boolean} selected - Selected chrome (CSS only)
13456
13456
  * @attr {boolean} disabled - Dim + non-interactive; no `<a>` when disabled
13457
13457
  * @attr {boolean} full - Stretch to available width (CSS; default width is already 100%)
13458
+ * @attr {string} size - Set to `large` for spacer-2 card padding and spacer-1 label spacing
13458
13459
  * @attr {string} aspect-ratio - Forwarded to fig-image (default `1/1`)
13459
13460
  * @attr {string} fit - Forwarded to fig-image (default `contain`)
13460
13461
  * @attr {string} label-line-clamp - `1` (default) or `2`
@@ -13472,6 +13473,7 @@ class FigCard extends HTMLElement {
13472
13473
  "selected",
13473
13474
  "disabled",
13474
13475
  "full",
13476
+ "size",
13475
13477
  "aspect-ratio",
13476
13478
  "fit",
13477
13479
  "label-line-clamp",
@@ -17448,6 +17450,7 @@ const FIG_ICON_TOKENS = {
17448
17450
  more: { medium: "--icon-24-more", small: "--icon-16-more" },
17449
17451
  visible: { medium: "--icon-24-visible", small: "--icon-16-visible" },
17450
17452
  hidden: { medium: "--icon-24-hidden", small: "--icon-16-hidden" },
17453
+ globe: { medium: "--icon-24-globe", small: "--icon-16-globe" },
17451
17454
  };
17452
17455
 
17453
17456
  function figIconCssVar(name, size = "medium") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.29.1",
3
+ "version": "6.31.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",