@rogieking/figui3 4.5.1 → 4.6.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/README.md +67 -3
- package/base.css +14 -0
- package/components.css +57 -17
- package/dist/base.css +1 -1
- package/dist/components.css +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +35 -35
- package/fig.js +254 -55
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -8255,60 +8255,115 @@ customElements.define("fig-chit", FigChit);
|
|
|
8255
8255
|
class FigSwatch extends FigChit {}
|
|
8256
8256
|
customElements.define("fig-swatch", FigSwatch);
|
|
8257
8257
|
|
|
8258
|
-
/*
|
|
8258
|
+
/* Media */
|
|
8259
8259
|
/**
|
|
8260
|
-
* @attr {string} src -
|
|
8260
|
+
* @attr {string} src - Media source URL
|
|
8261
|
+
* @attr {string} type - "image" (default) or "video" (for fig-media)
|
|
8262
|
+
* @attr {string} alt - Alt text for the generated image (default "")
|
|
8261
8263
|
* @attr {boolean} upload - Show upload overlay (generates fig-input-file)
|
|
8262
8264
|
* @attr {string} label - Upload button label (default "Upload")
|
|
8263
|
-
* @attr {string} size - small | medium | large | auto
|
|
8264
|
-
* @attr {string} aspect-ratio - CSS aspect-ratio
|
|
8265
|
+
* @attr {string} size - small | medium | large | auto (token-sized square)
|
|
8266
|
+
* @attr {string} aspect-ratio - CSS aspect-ratio value
|
|
8265
8267
|
* @attr {string} fit - CSS object-fit value
|
|
8266
|
-
* @attr {boolean} checkerboard - Show checkerboard behind transparent
|
|
8268
|
+
* @attr {boolean} checkerboard - Show checkerboard behind transparent media
|
|
8269
|
+
* @attr {boolean} controls - Video controls visibility (default false)
|
|
8270
|
+
* @attr {boolean} autoplay - Video autoplay
|
|
8271
|
+
* @attr {boolean} loop - Video loop
|
|
8272
|
+
* @attr {boolean} muted - Video muted
|
|
8273
|
+
* @attr {string} poster - Video poster image URL
|
|
8274
|
+
*
|
|
8275
|
+
* Sizing model:
|
|
8276
|
+
* - Default: host shrinkwraps to its inner <img>/<video> intrinsic size.
|
|
8277
|
+
* - `size` attribute applies a token-sized square.
|
|
8278
|
+
* - `aspect-ratio` attribute fills container width and applies the ratio.
|
|
8267
8279
|
*/
|
|
8268
|
-
class
|
|
8280
|
+
class FigMedia extends HTMLElement {
|
|
8269
8281
|
#src = null;
|
|
8270
8282
|
#chit = null;
|
|
8283
|
+
#mediaEl = null;
|
|
8271
8284
|
#fileInput = null;
|
|
8272
8285
|
#blobUrl = null;
|
|
8273
8286
|
#file = null;
|
|
8274
8287
|
#boundHandleFileInput = this.#handleFileInput.bind(this);
|
|
8288
|
+
#boundHandleMediaPlay = this.#handleMediaPlay.bind(this);
|
|
8289
|
+
#boundHandleMediaPause = this.#handleMediaPause.bind(this);
|
|
8290
|
+
#boundHandleMediaEnded = this.#handleMediaEnded.bind(this);
|
|
8275
8291
|
|
|
8276
8292
|
static get observedAttributes() {
|
|
8277
|
-
return [
|
|
8293
|
+
return [
|
|
8294
|
+
"src",
|
|
8295
|
+
"type",
|
|
8296
|
+
"alt",
|
|
8297
|
+
"upload",
|
|
8298
|
+
"label",
|
|
8299
|
+
"aspect-ratio",
|
|
8300
|
+
"fit",
|
|
8301
|
+
"checkerboard",
|
|
8302
|
+
"controls",
|
|
8303
|
+
"autoplay",
|
|
8304
|
+
"loop",
|
|
8305
|
+
"muted",
|
|
8306
|
+
"poster",
|
|
8307
|
+
];
|
|
8308
|
+
}
|
|
8309
|
+
|
|
8310
|
+
get mediaKind() {
|
|
8311
|
+
const type = (this.getAttribute("type") || "image").toLowerCase();
|
|
8312
|
+
return type === "video" ? "video" : "image";
|
|
8278
8313
|
}
|
|
8279
8314
|
|
|
8280
8315
|
get src() {
|
|
8281
8316
|
return this.#src;
|
|
8282
8317
|
}
|
|
8283
8318
|
set src(value) {
|
|
8284
|
-
this.#src = value;
|
|
8285
|
-
|
|
8319
|
+
this.#src = value || "";
|
|
8320
|
+
if (value === null || value === undefined || value === "") {
|
|
8321
|
+
this.removeAttribute("src");
|
|
8322
|
+
} else {
|
|
8323
|
+
this.setAttribute("src", value);
|
|
8324
|
+
}
|
|
8286
8325
|
}
|
|
8287
8326
|
|
|
8288
8327
|
get file() {
|
|
8289
8328
|
return this.#file;
|
|
8290
8329
|
}
|
|
8291
8330
|
|
|
8331
|
+
/**
|
|
8332
|
+
* Returns a base64 data URL for the loaded image.
|
|
8333
|
+
* Requires a CORS-clean image (same-origin or with appropriate Access-Control headers);
|
|
8334
|
+
* cross-origin images without proper headers will throw a tainted-canvas error.
|
|
8335
|
+
*/
|
|
8292
8336
|
async getBase64() {
|
|
8293
|
-
|
|
8294
|
-
if (!src) return null;
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8337
|
+
if (this.mediaKind !== "image") return null;
|
|
8338
|
+
if (!this.#src) return null;
|
|
8339
|
+
if (!this.#mediaEl) return null;
|
|
8340
|
+
try {
|
|
8341
|
+
if (typeof this.#mediaEl.decode === "function") {
|
|
8342
|
+
await this.#mediaEl.decode();
|
|
8343
|
+
} else if (!this.#mediaEl.complete) {
|
|
8344
|
+
await new Promise((resolve, reject) => {
|
|
8345
|
+
this.#mediaEl.addEventListener("load", resolve, { once: true });
|
|
8346
|
+
this.#mediaEl.addEventListener("error", reject, { once: true });
|
|
8347
|
+
});
|
|
8348
|
+
}
|
|
8349
|
+
} catch {
|
|
8350
|
+
// continue; canvas draw will throw if image truly unusable
|
|
8351
|
+
}
|
|
8352
|
+
const w = this.#mediaEl.naturalWidth;
|
|
8353
|
+
const h = this.#mediaEl.naturalHeight;
|
|
8354
|
+
if (!(w > 0) || !(h > 0)) return null;
|
|
8298
8355
|
const canvas = document.createElement("canvas");
|
|
8299
|
-
canvas.width =
|
|
8300
|
-
canvas.height =
|
|
8301
|
-
canvas.getContext("2d").drawImage(
|
|
8302
|
-
|
|
8303
|
-
const dataUrl = canvas.toDataURL();
|
|
8304
|
-
return dataUrl;
|
|
8356
|
+
canvas.width = w;
|
|
8357
|
+
canvas.height = h;
|
|
8358
|
+
canvas.getContext("2d").drawImage(this.#mediaEl, 0, 0);
|
|
8359
|
+
return canvas.toDataURL();
|
|
8305
8360
|
}
|
|
8306
8361
|
|
|
8307
8362
|
connectedCallback() {
|
|
8308
8363
|
this.#src = this.getAttribute("src") || "";
|
|
8309
8364
|
|
|
8310
8365
|
const ar = this.getAttribute("aspect-ratio");
|
|
8311
|
-
if (ar
|
|
8366
|
+
if (ar) {
|
|
8312
8367
|
this.style.setProperty("--aspect-ratio", ar);
|
|
8313
8368
|
}
|
|
8314
8369
|
const fit = this.getAttribute("fit");
|
|
@@ -8320,7 +8375,7 @@ class FigImage extends HTMLElement {
|
|
|
8320
8375
|
const chit = document.createElement("fig-chit");
|
|
8321
8376
|
chit.setAttribute("data-generated", "");
|
|
8322
8377
|
chit.setAttribute("size", "large");
|
|
8323
|
-
chit.setAttribute("data-type",
|
|
8378
|
+
chit.setAttribute("data-type", this.mediaKind);
|
|
8324
8379
|
chit.setAttribute("disabled", "");
|
|
8325
8380
|
this.#applyChitBackground(chit);
|
|
8326
8381
|
if (this.hasAttribute("checkerboard") && this.getAttribute("checkerboard") !== "false") {
|
|
@@ -8329,19 +8384,19 @@ class FigImage extends HTMLElement {
|
|
|
8329
8384
|
this.prepend(chit);
|
|
8330
8385
|
}
|
|
8331
8386
|
this.#chit = this.querySelector("fig-chit");
|
|
8387
|
+
this.#syncChitType();
|
|
8388
|
+
this.#ensureMediaElement();
|
|
8389
|
+
this.#syncGeneratedMediaElement();
|
|
8332
8390
|
|
|
8333
8391
|
const isUpload = this.hasAttribute("upload") && this.getAttribute("upload") !== "false";
|
|
8334
8392
|
if (isUpload && !this.querySelector("fig-input-file[data-generated]")) {
|
|
8335
8393
|
this.#createFileInput();
|
|
8336
8394
|
}
|
|
8337
|
-
|
|
8338
|
-
if (this.#src && this.getAttribute("aspect-ratio") === "auto") {
|
|
8339
|
-
this.#sniffDimensions(this.#src);
|
|
8340
|
-
}
|
|
8341
8395
|
}
|
|
8342
8396
|
|
|
8343
8397
|
disconnectedCallback() {
|
|
8344
8398
|
this.#fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
8399
|
+
this.#removeMediaElementListeners();
|
|
8345
8400
|
if (this.#blobUrl) {
|
|
8346
8401
|
URL.revokeObjectURL(this.#blobUrl);
|
|
8347
8402
|
this.#blobUrl = null;
|
|
@@ -8350,17 +8405,112 @@ class FigImage extends HTMLElement {
|
|
|
8350
8405
|
|
|
8351
8406
|
#applyChitBackground(chit) {
|
|
8352
8407
|
const cb = this.hasAttribute("checkerboard") && this.getAttribute("checkerboard") !== "false";
|
|
8353
|
-
|
|
8354
|
-
|
|
8408
|
+
chit.setAttribute("background", cb ? "url()" : "var(--figma-color-bg-secondary)");
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8411
|
+
#syncChitType() {
|
|
8412
|
+
if (!this.#chit) return;
|
|
8413
|
+
this.#chit.setAttribute("data-type", this.mediaKind);
|
|
8414
|
+
}
|
|
8415
|
+
|
|
8416
|
+
#removeMediaElementListeners() {
|
|
8417
|
+
if (!this.#mediaEl) return;
|
|
8418
|
+
if (this.#mediaEl.tagName === "VIDEO") {
|
|
8419
|
+
this.#mediaEl.removeEventListener("play", this.#boundHandleMediaPlay);
|
|
8420
|
+
this.#mediaEl.removeEventListener("pause", this.#boundHandleMediaPause);
|
|
8421
|
+
this.#mediaEl.removeEventListener("ended", this.#boundHandleMediaEnded);
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
|
|
8425
|
+
#userProvidedMediaEl() {
|
|
8426
|
+
const tag = this.mediaKind === "video" ? "video" : "img";
|
|
8427
|
+
return this.querySelector(`${tag}:not([data-generated])`);
|
|
8428
|
+
}
|
|
8429
|
+
|
|
8430
|
+
#ensureMediaElement() {
|
|
8431
|
+
const userEl = this.#userProvidedMediaEl();
|
|
8432
|
+
if (userEl) {
|
|
8433
|
+
if (this.#mediaEl && this.#mediaEl !== userEl) {
|
|
8434
|
+
this.#removeMediaElementListeners();
|
|
8435
|
+
if (this.#mediaEl.hasAttribute("data-generated")) {
|
|
8436
|
+
this.#mediaEl.remove();
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
8439
|
+
this.#mediaEl = userEl;
|
|
8440
|
+
return;
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8443
|
+
const expectedTag = this.mediaKind === "video" ? "VIDEO" : "IMG";
|
|
8444
|
+
if (this.#mediaEl && this.#mediaEl.tagName !== expectedTag) {
|
|
8445
|
+
this.#removeMediaElementListeners();
|
|
8446
|
+
if (this.#mediaEl.hasAttribute("data-generated")) {
|
|
8447
|
+
this.#mediaEl.remove();
|
|
8448
|
+
}
|
|
8449
|
+
this.#mediaEl = null;
|
|
8450
|
+
}
|
|
8451
|
+
if (this.#mediaEl) return;
|
|
8452
|
+
|
|
8453
|
+
if (this.mediaKind === "video") {
|
|
8454
|
+
const video = document.createElement("video");
|
|
8455
|
+
video.setAttribute("data-generated", "");
|
|
8456
|
+
video.className = "fig-media-element";
|
|
8457
|
+
video.setAttribute("playsinline", "");
|
|
8458
|
+
video.preload = "metadata";
|
|
8459
|
+
this.prepend(video);
|
|
8460
|
+
this.#mediaEl = video;
|
|
8461
|
+
this.#mediaEl.addEventListener("play", this.#boundHandleMediaPlay);
|
|
8462
|
+
this.#mediaEl.addEventListener("pause", this.#boundHandleMediaPause);
|
|
8463
|
+
this.#mediaEl.addEventListener("ended", this.#boundHandleMediaEnded);
|
|
8355
8464
|
} else {
|
|
8356
|
-
|
|
8465
|
+
const img = document.createElement("img");
|
|
8466
|
+
img.setAttribute("data-generated", "");
|
|
8467
|
+
img.className = "fig-media-element";
|
|
8468
|
+
img.loading = "lazy";
|
|
8469
|
+
img.decoding = "async";
|
|
8470
|
+
img.alt = this.getAttribute("alt") || "";
|
|
8471
|
+
this.prepend(img);
|
|
8472
|
+
this.#mediaEl = img;
|
|
8357
8473
|
}
|
|
8358
8474
|
}
|
|
8359
8475
|
|
|
8476
|
+
#isEnabledAttr(name, defaultEnabled = false) {
|
|
8477
|
+
if (!this.hasAttribute(name)) return defaultEnabled;
|
|
8478
|
+
return this.getAttribute(name) !== "false";
|
|
8479
|
+
}
|
|
8480
|
+
|
|
8481
|
+
#syncGeneratedMediaElement() {
|
|
8482
|
+
if (!this.#mediaEl) return;
|
|
8483
|
+
if (!this.#mediaEl.hasAttribute("data-generated")) return;
|
|
8484
|
+
const src = this.#src || "";
|
|
8485
|
+
if (this.#mediaEl.getAttribute("src") !== src) {
|
|
8486
|
+
if (src) {
|
|
8487
|
+
this.#mediaEl.setAttribute("src", src);
|
|
8488
|
+
} else {
|
|
8489
|
+
this.#mediaEl.removeAttribute("src");
|
|
8490
|
+
if (this.#mediaEl.tagName === "VIDEO") this.#mediaEl.load();
|
|
8491
|
+
}
|
|
8492
|
+
}
|
|
8493
|
+
if (this.#mediaEl.tagName === "IMG") {
|
|
8494
|
+
this.#mediaEl.alt = this.getAttribute("alt") || "";
|
|
8495
|
+
return;
|
|
8496
|
+
}
|
|
8497
|
+
const poster = this.getAttribute("poster");
|
|
8498
|
+
if (poster) {
|
|
8499
|
+
this.#mediaEl.setAttribute("poster", poster);
|
|
8500
|
+
} else {
|
|
8501
|
+
this.#mediaEl.removeAttribute("poster");
|
|
8502
|
+
}
|
|
8503
|
+
this.#mediaEl.controls = this.#isEnabledAttr("controls", false);
|
|
8504
|
+
this.#mediaEl.autoplay = this.#isEnabledAttr("autoplay", false);
|
|
8505
|
+
this.#mediaEl.loop = this.#isEnabledAttr("loop", false);
|
|
8506
|
+
this.#mediaEl.muted = this.#isEnabledAttr("muted", false);
|
|
8507
|
+
this.#mediaEl.playsInline = true;
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8360
8510
|
#createFileInput() {
|
|
8361
8511
|
const fi = document.createElement("fig-input-file");
|
|
8362
8512
|
fi.setAttribute("data-generated", "");
|
|
8363
|
-
fi.setAttribute("accepts", "image/*");
|
|
8513
|
+
fi.setAttribute("accepts", this.mediaKind === "video" ? "video/*" : "image/*");
|
|
8364
8514
|
fi.setAttribute("variant", "overlay");
|
|
8365
8515
|
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8366
8516
|
fi.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
@@ -8422,32 +8572,45 @@ class FigImage extends HTMLElement {
|
|
|
8422
8572
|
}
|
|
8423
8573
|
}
|
|
8424
8574
|
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8575
|
+
#emitPlaybackEvent(type) {
|
|
8576
|
+
if (!this.#mediaEl) return;
|
|
8577
|
+
this.dispatchEvent(
|
|
8578
|
+
new CustomEvent(type, {
|
|
8579
|
+
bubbles: true,
|
|
8580
|
+
cancelable: false,
|
|
8581
|
+
composed: true,
|
|
8582
|
+
detail: {
|
|
8583
|
+
src: this.#src || "",
|
|
8584
|
+
currentTime: this.#mediaEl.currentTime,
|
|
8585
|
+
duration: this.#mediaEl.duration,
|
|
8586
|
+
},
|
|
8587
|
+
}),
|
|
8588
|
+
);
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8591
|
+
#handleMediaPlay() {
|
|
8592
|
+
this.#emitPlaybackEvent("play");
|
|
8593
|
+
}
|
|
8594
|
+
|
|
8595
|
+
#handleMediaPause() {
|
|
8596
|
+
this.#emitPlaybackEvent("pause");
|
|
8597
|
+
}
|
|
8598
|
+
|
|
8599
|
+
#handleMediaEnded() {
|
|
8600
|
+
this.#emitPlaybackEvent("ended");
|
|
8441
8601
|
}
|
|
8442
8602
|
|
|
8443
8603
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
8444
8604
|
if (oldValue === newValue) return;
|
|
8445
8605
|
|
|
8446
8606
|
if (name === "src") {
|
|
8447
|
-
this.#src = newValue;
|
|
8448
|
-
if (this.#
|
|
8449
|
-
|
|
8607
|
+
this.#src = newValue || "";
|
|
8608
|
+
if (this.#blobUrl && this.#src !== this.#blobUrl) {
|
|
8609
|
+
URL.revokeObjectURL(this.#blobUrl);
|
|
8610
|
+
this.#blobUrl = null;
|
|
8611
|
+
this.#file = null;
|
|
8450
8612
|
}
|
|
8613
|
+
this.#syncGeneratedMediaElement();
|
|
8451
8614
|
if (this.#fileInput) {
|
|
8452
8615
|
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8453
8616
|
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
@@ -8457,11 +8620,24 @@ class FigImage extends HTMLElement {
|
|
|
8457
8620
|
this.#fileInput.removeAttribute("url");
|
|
8458
8621
|
}
|
|
8459
8622
|
}
|
|
8460
|
-
|
|
8461
|
-
|
|
8623
|
+
}
|
|
8624
|
+
|
|
8625
|
+
if (name === "type") {
|
|
8626
|
+
this.#syncChitType();
|
|
8627
|
+
this.#ensureMediaElement();
|
|
8628
|
+
this.#syncGeneratedMediaElement();
|
|
8629
|
+
if (this.#fileInput) {
|
|
8630
|
+
this.#fileInput.setAttribute(
|
|
8631
|
+
"accepts",
|
|
8632
|
+
this.mediaKind === "video" ? "video/*" : "image/*",
|
|
8633
|
+
);
|
|
8462
8634
|
}
|
|
8463
8635
|
}
|
|
8464
8636
|
|
|
8637
|
+
if (name === "alt" && this.#mediaEl && this.#mediaEl.tagName === "IMG") {
|
|
8638
|
+
this.#mediaEl.alt = newValue || "";
|
|
8639
|
+
}
|
|
8640
|
+
|
|
8465
8641
|
if (name === "upload") {
|
|
8466
8642
|
const on = newValue !== null && newValue !== "false";
|
|
8467
8643
|
if (on && !this.#fileInput) {
|
|
@@ -8472,12 +8648,10 @@ class FigImage extends HTMLElement {
|
|
|
8472
8648
|
}
|
|
8473
8649
|
|
|
8474
8650
|
if (name === "aspect-ratio") {
|
|
8475
|
-
if (newValue
|
|
8651
|
+
if (newValue) {
|
|
8476
8652
|
this.style.setProperty("--aspect-ratio", newValue);
|
|
8477
|
-
} else
|
|
8653
|
+
} else {
|
|
8478
8654
|
this.style.removeProperty("--aspect-ratio");
|
|
8479
|
-
} else if (newValue === "auto" && this.#src) {
|
|
8480
|
-
this.#sniffDimensions(this.#src);
|
|
8481
8655
|
}
|
|
8482
8656
|
}
|
|
8483
8657
|
|
|
@@ -8496,12 +8670,37 @@ class FigImage extends HTMLElement {
|
|
|
8496
8670
|
} else {
|
|
8497
8671
|
this.#chit.removeAttribute("checkerboard");
|
|
8498
8672
|
}
|
|
8673
|
+
this.#applyChitBackground(this.#chit);
|
|
8499
8674
|
}
|
|
8500
8675
|
}
|
|
8676
|
+
|
|
8677
|
+
if (name === "label" && this.#fileInput) {
|
|
8678
|
+
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8679
|
+
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8680
|
+
}
|
|
8681
|
+
|
|
8682
|
+
if (["controls", "autoplay", "loop", "muted", "poster"].includes(name)) {
|
|
8683
|
+
this.#syncGeneratedMediaElement();
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
|
|
8688
|
+
customElements.define("fig-media", FigMedia);
|
|
8689
|
+
|
|
8690
|
+
class FigImage extends FigMedia {
|
|
8691
|
+
get mediaKind() {
|
|
8692
|
+
return "image";
|
|
8501
8693
|
}
|
|
8502
8694
|
}
|
|
8503
8695
|
customElements.define("fig-image", FigImage);
|
|
8504
8696
|
|
|
8697
|
+
class FigVideo extends FigMedia {
|
|
8698
|
+
get mediaKind() {
|
|
8699
|
+
return "video";
|
|
8700
|
+
}
|
|
8701
|
+
}
|
|
8702
|
+
customElements.define("fig-video", FigVideo);
|
|
8703
|
+
|
|
8505
8704
|
/* File Upload Input */
|
|
8506
8705
|
class FigInputFile extends HTMLElement {
|
|
8507
8706
|
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url"];
|
package/package.json
CHANGED