@rogieking/figui3 1.9.7 → 2.0.1
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/base.css +13 -0
- package/components.css +346 -383
- package/example.html +1906 -1052
- package/fig.js +209 -43
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -26,7 +26,7 @@ class FigButton extends HTMLElement {
|
|
|
26
26
|
#selected;
|
|
27
27
|
constructor() {
|
|
28
28
|
super();
|
|
29
|
-
this.attachShadow({ mode: "open" });
|
|
29
|
+
this.attachShadow({ mode: "open", delegatesFocus: true });
|
|
30
30
|
}
|
|
31
31
|
connectedCallback() {
|
|
32
32
|
this.type = this.getAttribute("type") || "button";
|
|
@@ -48,6 +48,11 @@ class FigButton extends HTMLElement {
|
|
|
48
48
|
background: transparent;
|
|
49
49
|
margin: calc(var(--spacer-2)*-1);
|
|
50
50
|
height: var(--spacer-4);
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
width: 100%;
|
|
55
|
+
min-width: 0;
|
|
51
56
|
}
|
|
52
57
|
</style>
|
|
53
58
|
<button type="${this.type}">
|
|
@@ -62,6 +67,16 @@ class FigButton extends HTMLElement {
|
|
|
62
67
|
requestAnimationFrame(() => {
|
|
63
68
|
this.button = this.shadowRoot.querySelector("button");
|
|
64
69
|
this.button.addEventListener("click", this.#handleClick.bind(this));
|
|
70
|
+
|
|
71
|
+
// Forward focus-visible state to host element
|
|
72
|
+
this.button.addEventListener("focus", () => {
|
|
73
|
+
if (this.button.matches(":focus-visible")) {
|
|
74
|
+
this.setAttribute("data-focus-visible", "");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.button.addEventListener("blur", () => {
|
|
78
|
+
this.removeAttribute("data-focus-visible");
|
|
79
|
+
});
|
|
65
80
|
});
|
|
66
81
|
}
|
|
67
82
|
|
|
@@ -635,40 +650,52 @@ class FigDialog extends HTMLDialogElement {
|
|
|
635
650
|
|
|
636
651
|
#applyPosition() {
|
|
637
652
|
const position = this.getAttribute("position") || "";
|
|
638
|
-
const rect = this.getBoundingClientRect();
|
|
639
|
-
const viewportWidth = window.innerWidth;
|
|
640
|
-
const viewportHeight = window.innerHeight;
|
|
641
653
|
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
654
|
+
// Apply common styles
|
|
655
|
+
this.style.position = "fixed";
|
|
656
|
+
this.style.margin = "0";
|
|
657
|
+
|
|
658
|
+
// Reset position properties
|
|
659
|
+
this.style.top = "auto";
|
|
660
|
+
this.style.bottom = "auto";
|
|
661
|
+
this.style.left = "auto";
|
|
662
|
+
this.style.right = "auto";
|
|
663
|
+
this.style.transform = "none";
|
|
645
664
|
|
|
646
665
|
// Parse position attribute
|
|
647
666
|
const hasTop = position.includes("top");
|
|
648
667
|
const hasBottom = position.includes("bottom");
|
|
649
668
|
const hasLeft = position.includes("left");
|
|
650
669
|
const hasRight = position.includes("right");
|
|
670
|
+
const hasVCenter = position.includes("center") && !hasTop && !hasBottom;
|
|
671
|
+
const hasHCenter = position.includes("center") && !hasLeft && !hasRight;
|
|
651
672
|
|
|
652
673
|
// Vertical positioning
|
|
653
674
|
if (hasTop) {
|
|
654
|
-
top = this.#offset
|
|
675
|
+
this.style.top = `${this.#offset}px`;
|
|
655
676
|
} else if (hasBottom) {
|
|
656
|
-
|
|
677
|
+
this.style.bottom = `${this.#offset}px`;
|
|
678
|
+
} else if (hasVCenter) {
|
|
679
|
+
this.style.top = "50%";
|
|
657
680
|
}
|
|
658
681
|
|
|
659
682
|
// Horizontal positioning
|
|
660
683
|
if (hasLeft) {
|
|
661
|
-
left = this.#offset
|
|
684
|
+
this.style.left = `${this.#offset}px`;
|
|
662
685
|
} else if (hasRight) {
|
|
663
|
-
|
|
686
|
+
this.style.right = `${this.#offset}px`;
|
|
687
|
+
} else if (hasHCenter) {
|
|
688
|
+
this.style.left = "50%";
|
|
664
689
|
}
|
|
665
690
|
|
|
666
|
-
// Apply
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
691
|
+
// Apply transform for centering
|
|
692
|
+
if (hasVCenter && hasHCenter) {
|
|
693
|
+
this.style.transform = "translate(-50%, -50%)";
|
|
694
|
+
} else if (hasVCenter) {
|
|
695
|
+
this.style.transform = "translateY(-50%)";
|
|
696
|
+
} else if (hasHCenter) {
|
|
697
|
+
this.style.transform = "translateX(-50%)";
|
|
698
|
+
}
|
|
672
699
|
|
|
673
700
|
this.#positionInitialized = true;
|
|
674
701
|
}
|
|
@@ -740,6 +767,12 @@ class FigDialog extends HTMLDialogElement {
|
|
|
740
767
|
// Get current position from computed style
|
|
741
768
|
const rect = this.getBoundingClientRect();
|
|
742
769
|
|
|
770
|
+
// Ensure we are using top/left for dragging by converting current position
|
|
771
|
+
this.style.top = `${rect.top}px`;
|
|
772
|
+
this.style.left = `${rect.left}px`;
|
|
773
|
+
this.style.bottom = "auto";
|
|
774
|
+
this.style.right = "auto";
|
|
775
|
+
|
|
743
776
|
// Store offset from pointer to dialog top-left corner
|
|
744
777
|
this.#dragOffset.x = e.clientX - rect.left;
|
|
745
778
|
this.#dragOffset.y = e.clientY - rect.top;
|
|
@@ -2272,8 +2305,7 @@ class FigCheckbox extends HTMLElement {
|
|
|
2272
2305
|
this.input.setAttribute("id", figUniqueId());
|
|
2273
2306
|
this.input.setAttribute("name", this.name);
|
|
2274
2307
|
this.input.setAttribute("type", "checkbox");
|
|
2275
|
-
this.labelElement =
|
|
2276
|
-
this.labelElement.setAttribute("for", this.input.id);
|
|
2308
|
+
this.labelElement = null;
|
|
2277
2309
|
}
|
|
2278
2310
|
connectedCallback() {
|
|
2279
2311
|
this.checked = this.input.checked =
|
|
@@ -2289,7 +2321,12 @@ class FigCheckbox extends HTMLElement {
|
|
|
2289
2321
|
}
|
|
2290
2322
|
|
|
2291
2323
|
this.append(this.input);
|
|
2292
|
-
|
|
2324
|
+
|
|
2325
|
+
// Only create label if label attribute is present
|
|
2326
|
+
if (this.hasAttribute("label")) {
|
|
2327
|
+
this.#createLabel();
|
|
2328
|
+
this.labelElement.innerText = this.getAttribute("label");
|
|
2329
|
+
}
|
|
2293
2330
|
|
|
2294
2331
|
this.render();
|
|
2295
2332
|
}
|
|
@@ -2297,6 +2334,21 @@ class FigCheckbox extends HTMLElement {
|
|
|
2297
2334
|
return ["disabled", "label", "checked", "name", "value"];
|
|
2298
2335
|
}
|
|
2299
2336
|
|
|
2337
|
+
#createLabel() {
|
|
2338
|
+
if (!this.labelElement) {
|
|
2339
|
+
this.labelElement = document.createElement("label");
|
|
2340
|
+
this.labelElement.setAttribute("for", this.input.id);
|
|
2341
|
+
}
|
|
2342
|
+
// Add to DOM if not already there and input is in the DOM
|
|
2343
|
+
if (
|
|
2344
|
+
this.labelElement &&
|
|
2345
|
+
!this.labelElement.parentNode &&
|
|
2346
|
+
this.input.parentNode
|
|
2347
|
+
) {
|
|
2348
|
+
this.input.after(this.labelElement);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2300
2352
|
render() {}
|
|
2301
2353
|
|
|
2302
2354
|
focus() {
|
|
@@ -2310,7 +2362,13 @@ class FigCheckbox extends HTMLElement {
|
|
|
2310
2362
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
2311
2363
|
switch (name) {
|
|
2312
2364
|
case "label":
|
|
2313
|
-
|
|
2365
|
+
if (newValue) {
|
|
2366
|
+
this.#createLabel();
|
|
2367
|
+
this.labelElement.innerText = newValue;
|
|
2368
|
+
} else if (this.labelElement) {
|
|
2369
|
+
this.labelElement.remove();
|
|
2370
|
+
this.labelElement = null;
|
|
2371
|
+
}
|
|
2314
2372
|
break;
|
|
2315
2373
|
case "checked":
|
|
2316
2374
|
this.checked = this.input.checked =
|
|
@@ -2368,29 +2426,139 @@ class FigSwitch extends FigCheckbox {
|
|
|
2368
2426
|
}
|
|
2369
2427
|
window.customElements.define("fig-switch", FigSwitch);
|
|
2370
2428
|
|
|
2371
|
-
/*
|
|
2372
|
-
|
|
2429
|
+
/* Toast */
|
|
2430
|
+
/**
|
|
2431
|
+
* A toast notification element for non-modal, time-based messages.
|
|
2432
|
+
* Always positioned at bottom center of the screen.
|
|
2433
|
+
* @attr {number} duration - Auto-dismiss duration in ms (0 = no auto-dismiss, default: 5000)
|
|
2434
|
+
* @attr {number} offset - Distance from bottom edge in pixels (default: 16)
|
|
2435
|
+
* @attr {string} theme - Visual theme: "dark" (default), "light", "danger", "brand"
|
|
2436
|
+
* @attr {boolean} open - Whether the toast is visible
|
|
2437
|
+
*/
|
|
2438
|
+
class FigToast extends HTMLDialogElement {
|
|
2439
|
+
#defaultOffset = 16; // 1rem in pixels
|
|
2440
|
+
#autoCloseTimer = null;
|
|
2441
|
+
|
|
2373
2442
|
constructor() {
|
|
2374
2443
|
super();
|
|
2375
2444
|
}
|
|
2376
|
-
}
|
|
2377
|
-
window.customElements.define("fig-bell", FigBell);
|
|
2378
2445
|
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
constructor() {
|
|
2382
|
-
super();
|
|
2446
|
+
get #offset() {
|
|
2447
|
+
return parseInt(this.getAttribute("offset") ?? this.#defaultOffset);
|
|
2383
2448
|
}
|
|
2384
|
-
}
|
|
2385
|
-
window.customElements.define("fig-badge", FigBadge);
|
|
2386
2449
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2450
|
+
connectedCallback() {
|
|
2451
|
+
// Set default theme if not specified
|
|
2452
|
+
if (!this.hasAttribute("theme")) {
|
|
2453
|
+
this.setAttribute("theme", "dark");
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// Ensure toast is closed by default
|
|
2457
|
+
// Remove native open attribute if present and not explicitly "true"
|
|
2458
|
+
const shouldOpen =
|
|
2459
|
+
this.getAttribute("open") === "true" || this.getAttribute("open") === "";
|
|
2460
|
+
if (this.hasAttribute("open") && !shouldOpen) {
|
|
2461
|
+
this.removeAttribute("open");
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// Close the dialog initially (override native behavior)
|
|
2465
|
+
if (!shouldOpen) {
|
|
2466
|
+
this.close();
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
requestAnimationFrame(() => {
|
|
2470
|
+
this.#addCloseListeners();
|
|
2471
|
+
this.#applyPosition();
|
|
2472
|
+
|
|
2473
|
+
// Auto-show if open attribute is explicitly true
|
|
2474
|
+
if (shouldOpen) {
|
|
2475
|
+
this.showToast();
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
disconnectedCallback() {
|
|
2481
|
+
this.#clearAutoClose();
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
#addCloseListeners() {
|
|
2485
|
+
this.querySelectorAll("[close-toast]").forEach((button) => {
|
|
2486
|
+
button.removeEventListener("click", this.#handleClose);
|
|
2487
|
+
button.addEventListener("click", this.#handleClose.bind(this));
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
#handleClose() {
|
|
2492
|
+
this.hideToast();
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
#applyPosition() {
|
|
2496
|
+
// Always bottom center
|
|
2497
|
+
this.style.position = "fixed";
|
|
2498
|
+
this.style.margin = "0";
|
|
2499
|
+
this.style.top = "auto";
|
|
2500
|
+
this.style.bottom = `${this.#offset}px`;
|
|
2501
|
+
this.style.left = "50%";
|
|
2502
|
+
this.style.right = "auto";
|
|
2503
|
+
this.style.transform = "translateX(-50%)";
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
#startAutoClose() {
|
|
2507
|
+
this.#clearAutoClose();
|
|
2508
|
+
|
|
2509
|
+
const duration = parseInt(this.getAttribute("duration") ?? "5000");
|
|
2510
|
+
if (duration > 0) {
|
|
2511
|
+
this.#autoCloseTimer = setTimeout(() => {
|
|
2512
|
+
this.hideToast();
|
|
2513
|
+
}, duration);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
#clearAutoClose() {
|
|
2518
|
+
if (this.#autoCloseTimer) {
|
|
2519
|
+
clearTimeout(this.#autoCloseTimer);
|
|
2520
|
+
this.#autoCloseTimer = null;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Show the toast notification (non-modal)
|
|
2526
|
+
*/
|
|
2527
|
+
showToast() {
|
|
2528
|
+
this.show(); // Non-modal show
|
|
2529
|
+
this.#applyPosition();
|
|
2530
|
+
this.#startAutoClose();
|
|
2531
|
+
this.dispatchEvent(new CustomEvent("toast-show", { bubbles: true }));
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* Hide the toast notification
|
|
2536
|
+
*/
|
|
2537
|
+
hideToast() {
|
|
2538
|
+
this.#clearAutoClose();
|
|
2539
|
+
this.close();
|
|
2540
|
+
this.dispatchEvent(new CustomEvent("toast-hide", { bubbles: true }));
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
static get observedAttributes() {
|
|
2544
|
+
return ["duration", "offset", "open", "theme"];
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
2548
|
+
if (name === "offset") {
|
|
2549
|
+
this.#applyPosition();
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
if (name === "open") {
|
|
2553
|
+
if (newValue !== null && newValue !== "false") {
|
|
2554
|
+
this.showToast();
|
|
2555
|
+
} else {
|
|
2556
|
+
this.hideToast();
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2391
2559
|
}
|
|
2392
2560
|
}
|
|
2393
|
-
|
|
2561
|
+
customElements.define("fig-toast", FigToast, { extends: "dialog" });
|
|
2394
2562
|
|
|
2395
2563
|
/* Combo Input */
|
|
2396
2564
|
/**
|
|
@@ -3065,15 +3233,14 @@ class FigInputAngle extends HTMLElement {
|
|
|
3065
3233
|
</div>
|
|
3066
3234
|
${
|
|
3067
3235
|
this.text
|
|
3068
|
-
? `<fig-input-
|
|
3069
|
-
type="number"
|
|
3236
|
+
? `<fig-input-number
|
|
3070
3237
|
name="angle"
|
|
3071
3238
|
step="0.1"
|
|
3072
3239
|
value="${this.angle}"
|
|
3073
3240
|
min="0"
|
|
3074
|
-
max="360"
|
|
3075
|
-
|
|
3076
|
-
</fig-input-
|
|
3241
|
+
max="360"
|
|
3242
|
+
units="°">
|
|
3243
|
+
</fig-input-number>`
|
|
3077
3244
|
: ""
|
|
3078
3245
|
}
|
|
3079
3246
|
`;
|
|
@@ -3082,7 +3249,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
3082
3249
|
#setupListeners() {
|
|
3083
3250
|
this.handle = this.querySelector(".fig-input-angle-handle");
|
|
3084
3251
|
this.plane = this.querySelector(".fig-input-angle-plane");
|
|
3085
|
-
this.angleInput = this.querySelector("fig-input-
|
|
3252
|
+
this.angleInput = this.querySelector("fig-input-number[name='angle']");
|
|
3086
3253
|
this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
|
|
3087
3254
|
this.plane.addEventListener(
|
|
3088
3255
|
"touchstart",
|
|
@@ -3091,7 +3258,6 @@ class FigInputAngle extends HTMLElement {
|
|
|
3091
3258
|
window.addEventListener("keydown", this.#handleKeyDown.bind(this));
|
|
3092
3259
|
window.addEventListener("keyup", this.#handleKeyUp.bind(this));
|
|
3093
3260
|
if (this.text && this.angleInput) {
|
|
3094
|
-
this.angleInput = this.querySelector("fig-input-text");
|
|
3095
3261
|
this.angleInput.addEventListener(
|
|
3096
3262
|
"input",
|
|
3097
3263
|
this.#handleAngleInput.bind(this)
|