@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.
Files changed (5) hide show
  1. package/base.css +13 -0
  2. package/components.css +346 -383
  3. package/example.html +1906 -1052
  4. package/fig.js +209 -43
  5. 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
- // Default to centered
643
- let top = (viewportHeight - rect.height) / 2;
644
- let left = (viewportWidth - rect.width) / 2;
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
- top = viewportHeight - rect.height - this.#offset;
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
- left = viewportWidth - rect.width - this.#offset;
686
+ this.style.right = `${this.#offset}px`;
687
+ } else if (hasHCenter) {
688
+ this.style.left = "50%";
664
689
  }
665
690
 
666
- // Apply position using fixed positioning with pixels
667
- this.style.position = "fixed";
668
- this.style.top = `${top}px`;
669
- this.style.left = `${left}px`;
670
- this.style.transform = "none";
671
- this.style.margin = "0";
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 = document.createElement("label");
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
- this.append(this.labelElement);
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
- this.labelElement.innerText = newValue;
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
- /* Bell */
2372
- class FigBell extends HTMLElement {
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
- /* Badge */
2380
- class FigBadge extends HTMLElement {
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
- /* Accordion */
2388
- class FigAccordion extends HTMLElement {
2389
- constructor() {
2390
- super();
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
- window.customElements.define("fig-accordion", FigAccordion);
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-text
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
- <span slot="append">°</span>
3076
- </fig-input-text>`
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-text[name='angle']");
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "1.9.7",
3
+ "version": "2.0.1",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {