@rogieking/figui3 3.22.0 → 3.23.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.
Files changed (3) hide show
  1. package/components.css +81 -0
  2. package/fig.js +293 -0
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -1290,6 +1290,87 @@ fig-chit {
1290
1290
  }
1291
1291
 
1292
1292
  /* Fig Image */
1293
+ fig-truncate {
1294
+ display: inline-block;
1295
+ white-space: nowrap;
1296
+ overflow: hidden;
1297
+ text-overflow: ellipsis;
1298
+ min-width: 0;
1299
+ max-width: 100%;
1300
+ vertical-align: bottom;
1301
+
1302
+ &[position="left"] {
1303
+ display: block;
1304
+ direction: rtl;
1305
+ text-align: left;
1306
+ }
1307
+
1308
+ &[position="middle"] {
1309
+ display: inline-flex;
1310
+ text-overflow: clip;
1311
+ gap: 0;
1312
+
1313
+ & > .start,
1314
+ & > .end {
1315
+ flex: 0 1 auto;
1316
+ white-space: nowrap;
1317
+ overflow: hidden;
1318
+ text-overflow: ellipsis;
1319
+ min-width: 0;
1320
+ }
1321
+
1322
+ & > .end {
1323
+ direction: rtl; /* technique #2 on right half */
1324
+ text-align: left;
1325
+ text-overflow: ellipsis;
1326
+ }
1327
+ &[tail] {
1328
+ & > .end {
1329
+ direction: ltr;
1330
+ text-align: right;
1331
+ flex-shrink: 0;
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ fig-input-file {
1338
+ display: flex;
1339
+ align-items: center;
1340
+ gap: var(--spacer-2);
1341
+ min-width: 0;
1342
+
1343
+ &[full]:not([full="false"]) {
1344
+ flex: 1 1 auto;
1345
+ width: 100%;
1346
+
1347
+ & > fig-tooltip,
1348
+ & > fig-button {
1349
+ flex: 1;
1350
+ min-width: 0;
1351
+ }
1352
+
1353
+ & > fig-tooltip > fig-button {
1354
+ width: 100%;
1355
+ }
1356
+ }
1357
+
1358
+ .fig-input-file-filename {
1359
+ flex: 1;
1360
+ min-width: 0;
1361
+ }
1362
+
1363
+ .fig-input-file-clear {
1364
+ flex-shrink: 0;
1365
+ }
1366
+
1367
+ &[dragover] {
1368
+ outline: 2px dashed var(--figma-color-border-brand);
1369
+ outline-offset: -2px;
1370
+ border-radius: var(--radius-small);
1371
+ }
1372
+ }
1373
+
1293
1374
  fig-image {
1294
1375
  --image-size: 2rem;
1295
1376
  --fit: cover;
package/fig.js CHANGED
@@ -1023,6 +1023,85 @@ class FigTooltip extends HTMLElement {
1023
1023
 
1024
1024
  customElements.define("fig-tooltip", FigTooltip);
1025
1025
 
1026
+ /* Text Truncation */
1027
+ class FigTruncate extends HTMLElement {
1028
+ static observedAttributes = ["position", "tail"];
1029
+
1030
+ #originalText = null;
1031
+ #boundEnter = null;
1032
+ #boundLeave = null;
1033
+
1034
+ connectedCallback() {
1035
+ this.#originalText = this.textContent;
1036
+ requestAnimationFrame(() => {
1037
+ this.#render();
1038
+ this.#setupTooltip();
1039
+ });
1040
+ }
1041
+
1042
+ disconnectedCallback() {
1043
+ this.#teardownTooltip();
1044
+ }
1045
+
1046
+ attributeChangedCallback(name, oldValue, newValue) {
1047
+ if (oldValue === newValue) return;
1048
+ if (this.#originalText === null) return;
1049
+ this.#render();
1050
+ }
1051
+
1052
+ #render() {
1053
+ const position = this.getAttribute("position") || "right";
1054
+ const text = this.#originalText || "";
1055
+ if (position === "middle") {
1056
+ const tail = this.getAttribute("tail");
1057
+ let splitIndex;
1058
+ if (tail) {
1059
+ const idx = text.lastIndexOf(tail);
1060
+ splitIndex = idx > 0 ? idx : Math.ceil(text.length / 2);
1061
+ } else {
1062
+ splitIndex = Math.ceil(text.length / 2);
1063
+ }
1064
+ this.innerHTML = "";
1065
+ const startSpan = document.createElement("span");
1066
+ startSpan.className = "start";
1067
+ startSpan.textContent = text.slice(0, splitIndex);
1068
+ const endSpan = document.createElement("span");
1069
+ endSpan.className = "end";
1070
+ endSpan.textContent = text.slice(splitIndex);
1071
+ this.appendChild(startSpan);
1072
+ this.appendChild(endSpan);
1073
+ } else {
1074
+ this.textContent = text;
1075
+ }
1076
+ }
1077
+
1078
+ #setupTooltip() {
1079
+ if (
1080
+ !this.hasAttribute("tooltip") ||
1081
+ this.getAttribute("tooltip") === "false"
1082
+ )
1083
+ return;
1084
+ this.#boundEnter = () => {
1085
+ if (this.scrollWidth <= this.clientWidth) return;
1086
+ FigTooltip.show(this, this.#originalText);
1087
+ };
1088
+ this.#boundLeave = () => {
1089
+ FigTooltip.hide(this);
1090
+ };
1091
+ this.addEventListener("pointerenter", this.#boundEnter);
1092
+ this.addEventListener("pointerleave", this.#boundLeave);
1093
+ }
1094
+
1095
+ #teardownTooltip() {
1096
+ if (this.#boundEnter)
1097
+ this.removeEventListener("pointerenter", this.#boundEnter);
1098
+ if (this.#boundLeave)
1099
+ this.removeEventListener("pointerleave", this.#boundLeave);
1100
+ FigTooltip.hide(this);
1101
+ }
1102
+ }
1103
+ customElements.define("fig-truncate", FigTruncate);
1104
+
1026
1105
  /* Dialog */
1027
1106
  /**
1028
1107
  * A custom dialog element for modal and non-modal dialogs.
@@ -8183,6 +8262,220 @@ class FigImage extends HTMLElement {
8183
8262
  }
8184
8263
  customElements.define("fig-image", FigImage);
8185
8264
 
8265
+ /* File Upload Input */
8266
+ class FigInputFile extends HTMLElement {
8267
+ static observedAttributes = ["accepts", "label", "disabled", "multiple"];
8268
+
8269
+ #fileInput = null;
8270
+ #filenameEl = null;
8271
+ #clearBtn = null;
8272
+ #tooltipEl = null;
8273
+ #uploadBtn = null;
8274
+ #files = null;
8275
+
8276
+ get files() {
8277
+ return this.#files;
8278
+ }
8279
+
8280
+ get value() {
8281
+ if (!this.#files || this.#files.length === 0) return "";
8282
+ if (this.#files.length === 1) return this.#files[0].name;
8283
+ return `${this.#files.length} files`;
8284
+ }
8285
+
8286
+ connectedCallback() {
8287
+ this.#render();
8288
+ this.addEventListener("dragover", this.#onDragOver);
8289
+ this.addEventListener("dragleave", this.#onDragLeave);
8290
+ this.addEventListener("drop", this.#onDrop);
8291
+ }
8292
+
8293
+ disconnectedCallback() {
8294
+ this.#fileInput?.removeEventListener("change", this.#onFileChange);
8295
+ this.#clearBtn?.removeEventListener("click", this.#onClear);
8296
+ this.removeEventListener("dragover", this.#onDragOver);
8297
+ this.removeEventListener("dragleave", this.#onDragLeave);
8298
+ this.removeEventListener("drop", this.#onDrop);
8299
+ }
8300
+
8301
+ attributeChangedCallback(name, oldValue, newValue) {
8302
+ if (oldValue === newValue) return;
8303
+ this.#render();
8304
+ }
8305
+
8306
+ clear() {
8307
+ this.#files = null;
8308
+ if (this.#fileInput) this.#fileInput.value = "";
8309
+ this.#render();
8310
+ this.#emitEvents();
8311
+ }
8312
+
8313
+ #emitEvents() {
8314
+ const detail = { files: this.#files };
8315
+ this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
8316
+ this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
8317
+ }
8318
+
8319
+ #onFileChange = () => {
8320
+ if (this.#fileInput.files.length > 0) {
8321
+ this.#files = this.#fileInput.files;
8322
+ this.#render();
8323
+ this.#emitEvents();
8324
+ }
8325
+ };
8326
+
8327
+ #onClear = (e) => {
8328
+ e.stopPropagation();
8329
+ this.clear();
8330
+ };
8331
+
8332
+ #onDragOver = (e) => {
8333
+ e.preventDefault();
8334
+ this.setAttribute("dragover", "");
8335
+ };
8336
+
8337
+ #onDragLeave = () => {
8338
+ this.removeAttribute("dragover");
8339
+ };
8340
+
8341
+ #onDrop = (e) => {
8342
+ e.preventDefault();
8343
+ this.removeAttribute("dragover");
8344
+ if (
8345
+ this.hasAttribute("disabled") &&
8346
+ this.getAttribute("disabled") !== "false"
8347
+ )
8348
+ return;
8349
+
8350
+ const accepts = this.getAttribute("accepts");
8351
+ let dropped = Array.from(e.dataTransfer.files);
8352
+ if (accepts) {
8353
+ const allowed = accepts.split(",").map((s) => s.trim().toLowerCase());
8354
+ dropped = dropped.filter((file) => {
8355
+ const ext = "." + file.name.split(".").pop().toLowerCase();
8356
+ const mime = file.type.toLowerCase();
8357
+ return allowed.some(
8358
+ (a) =>
8359
+ a === ext ||
8360
+ a === mime ||
8361
+ (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))),
8362
+ );
8363
+ });
8364
+ }
8365
+ if (!this.hasAttribute("multiple")) {
8366
+ dropped = dropped.slice(0, 1);
8367
+ }
8368
+ if (dropped.length === 0) return;
8369
+
8370
+ const dt = new DataTransfer();
8371
+ dropped.forEach((f) => dt.items.add(f));
8372
+ this.#files = dt.files;
8373
+ if (this.#fileInput) {
8374
+ this.#fileInput.files = dt.files;
8375
+ }
8376
+ this.#render();
8377
+ this.#emitEvents();
8378
+ };
8379
+
8380
+ #render() {
8381
+ const accepts = this.getAttribute("accepts") || "";
8382
+ const label = this.getAttribute("label") || "Upload";
8383
+ const disabled =
8384
+ this.hasAttribute("disabled") &&
8385
+ this.getAttribute("disabled") !== "false";
8386
+ const multiple = this.hasAttribute("multiple");
8387
+ const hasFile = this.#files && this.#files.length > 0;
8388
+
8389
+ this.innerHTML = "";
8390
+
8391
+ if (hasFile) {
8392
+ const tooltipText = accepts
8393
+ ? `Accepts ${accepts
8394
+ .split(",")
8395
+ .map((s) => s.trim())
8396
+ .join(", ")}`
8397
+ : "";
8398
+
8399
+ this.#uploadBtn = document.createElement("fig-button");
8400
+ this.#uploadBtn.setAttribute("variant", "input");
8401
+ this.#uploadBtn.setAttribute("type", "upload");
8402
+ this.#uploadBtn.className = "fig-input-file-filename";
8403
+ if (disabled) this.#uploadBtn.setAttribute("disabled", "");
8404
+ const truncEl = document.createElement("fig-truncate");
8405
+ truncEl.setAttribute("position", "middle");
8406
+ truncEl.setAttribute("tooltip", "");
8407
+ const filename = this.value;
8408
+ const dotIdx = filename.lastIndexOf(".");
8409
+ if (dotIdx > 0) truncEl.setAttribute("tail", filename.slice(dotIdx));
8410
+ truncEl.textContent = filename;
8411
+ this.#uploadBtn.appendChild(truncEl);
8412
+
8413
+ this.#fileInput = document.createElement("input");
8414
+ this.#fileInput.type = "file";
8415
+ this.#fileInput.title = "";
8416
+ if (accepts) this.#fileInput.setAttribute("accept", accepts);
8417
+ if (multiple) this.#fileInput.setAttribute("multiple", "");
8418
+ this.#fileInput.addEventListener("change", this.#onFileChange);
8419
+ this.#uploadBtn.appendChild(this.#fileInput);
8420
+
8421
+ if (tooltipText) {
8422
+ this.#tooltipEl = document.createElement("fig-tooltip");
8423
+ this.#tooltipEl.setAttribute("text", tooltipText);
8424
+ this.#tooltipEl.appendChild(this.#uploadBtn);
8425
+ this.appendChild(this.#tooltipEl);
8426
+ } else {
8427
+ this.appendChild(this.#uploadBtn);
8428
+ }
8429
+
8430
+ const clearTooltip = document.createElement("fig-tooltip");
8431
+ clearTooltip.setAttribute("text", "Remove");
8432
+ this.#clearBtn = document.createElement("fig-button");
8433
+ this.#clearBtn.setAttribute("variant", "ghost");
8434
+ this.#clearBtn.setAttribute("icon", "true");
8435
+ this.#clearBtn.className = "fig-input-file-clear";
8436
+ if (disabled) this.#clearBtn.setAttribute("disabled", "");
8437
+ this.#clearBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-minus);"></span>`;
8438
+ this.#clearBtn.addEventListener("click", this.#onClear);
8439
+ clearTooltip.appendChild(this.#clearBtn);
8440
+ this.appendChild(clearTooltip);
8441
+ } else {
8442
+ const tooltipText = accepts
8443
+ ? `Accepts ${accepts
8444
+ .split(",")
8445
+ .map((s) => s.trim())
8446
+ .join(", ")}`
8447
+ : "";
8448
+
8449
+ if (tooltipText) {
8450
+ this.#tooltipEl = document.createElement("fig-tooltip");
8451
+ this.#tooltipEl.setAttribute("text", tooltipText);
8452
+ }
8453
+
8454
+ this.#uploadBtn = document.createElement("fig-button");
8455
+ this.#uploadBtn.setAttribute("variant", "input");
8456
+ this.#uploadBtn.setAttribute("type", "upload");
8457
+ this.#uploadBtn.textContent = label;
8458
+ if (disabled) this.#uploadBtn.setAttribute("disabled", "");
8459
+
8460
+ this.#fileInput = document.createElement("input");
8461
+ this.#fileInput.type = "file";
8462
+ this.#fileInput.title = "";
8463
+ if (accepts) this.#fileInput.setAttribute("accept", accepts);
8464
+ if (multiple) this.#fileInput.setAttribute("multiple", "");
8465
+ this.#fileInput.addEventListener("change", this.#onFileChange);
8466
+ this.#uploadBtn.appendChild(this.#fileInput);
8467
+
8468
+ if (this.#tooltipEl) {
8469
+ this.#tooltipEl.appendChild(this.#uploadBtn);
8470
+ this.appendChild(this.#tooltipEl);
8471
+ } else {
8472
+ this.appendChild(this.#uploadBtn);
8473
+ }
8474
+ }
8475
+ }
8476
+ }
8477
+ customElements.define("fig-input-file", FigInputFile);
8478
+
8186
8479
  /**
8187
8480
  * A bezier / spring easing curve editor with draggable control points.
8188
8481
  * @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.22.0",
3
+ "version": "3.23.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",