@rogieking/figui3 1.9.7 → 2.0.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 (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 +202 -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,14 @@ 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
+ this.append(this.labelElement);
2342
+ }
2343
+ }
2344
+
2300
2345
  render() {}
2301
2346
 
2302
2347
  focus() {
@@ -2310,7 +2355,13 @@ class FigCheckbox extends HTMLElement {
2310
2355
  attributeChangedCallback(name, oldValue, newValue) {
2311
2356
  switch (name) {
2312
2357
  case "label":
2313
- this.labelElement.innerText = newValue;
2358
+ if (newValue) {
2359
+ this.#createLabel();
2360
+ this.labelElement.innerText = newValue;
2361
+ } else if (this.labelElement) {
2362
+ this.labelElement.remove();
2363
+ this.labelElement = null;
2364
+ }
2314
2365
  break;
2315
2366
  case "checked":
2316
2367
  this.checked = this.input.checked =
@@ -2368,29 +2419,139 @@ class FigSwitch extends FigCheckbox {
2368
2419
  }
2369
2420
  window.customElements.define("fig-switch", FigSwitch);
2370
2421
 
2371
- /* Bell */
2372
- class FigBell extends HTMLElement {
2422
+ /* Toast */
2423
+ /**
2424
+ * A toast notification element for non-modal, time-based messages.
2425
+ * Always positioned at bottom center of the screen.
2426
+ * @attr {number} duration - Auto-dismiss duration in ms (0 = no auto-dismiss, default: 5000)
2427
+ * @attr {number} offset - Distance from bottom edge in pixels (default: 16)
2428
+ * @attr {string} theme - Visual theme: "dark" (default), "light", "danger", "brand"
2429
+ * @attr {boolean} open - Whether the toast is visible
2430
+ */
2431
+ class FigToast extends HTMLDialogElement {
2432
+ #defaultOffset = 16; // 1rem in pixels
2433
+ #autoCloseTimer = null;
2434
+
2373
2435
  constructor() {
2374
2436
  super();
2375
2437
  }
2376
- }
2377
- window.customElements.define("fig-bell", FigBell);
2378
2438
 
2379
- /* Badge */
2380
- class FigBadge extends HTMLElement {
2381
- constructor() {
2382
- super();
2439
+ get #offset() {
2440
+ return parseInt(this.getAttribute("offset") ?? this.#defaultOffset);
2383
2441
  }
2384
- }
2385
- window.customElements.define("fig-badge", FigBadge);
2386
2442
 
2387
- /* Accordion */
2388
- class FigAccordion extends HTMLElement {
2389
- constructor() {
2390
- super();
2443
+ connectedCallback() {
2444
+ // Set default theme if not specified
2445
+ if (!this.hasAttribute("theme")) {
2446
+ this.setAttribute("theme", "dark");
2447
+ }
2448
+
2449
+ // Ensure toast is closed by default
2450
+ // Remove native open attribute if present and not explicitly "true"
2451
+ const shouldOpen =
2452
+ this.getAttribute("open") === "true" || this.getAttribute("open") === "";
2453
+ if (this.hasAttribute("open") && !shouldOpen) {
2454
+ this.removeAttribute("open");
2455
+ }
2456
+
2457
+ // Close the dialog initially (override native behavior)
2458
+ if (!shouldOpen) {
2459
+ this.close();
2460
+ }
2461
+
2462
+ requestAnimationFrame(() => {
2463
+ this.#addCloseListeners();
2464
+ this.#applyPosition();
2465
+
2466
+ // Auto-show if open attribute is explicitly true
2467
+ if (shouldOpen) {
2468
+ this.showToast();
2469
+ }
2470
+ });
2471
+ }
2472
+
2473
+ disconnectedCallback() {
2474
+ this.#clearAutoClose();
2475
+ }
2476
+
2477
+ #addCloseListeners() {
2478
+ this.querySelectorAll("[close-toast]").forEach((button) => {
2479
+ button.removeEventListener("click", this.#handleClose);
2480
+ button.addEventListener("click", this.#handleClose.bind(this));
2481
+ });
2482
+ }
2483
+
2484
+ #handleClose() {
2485
+ this.hideToast();
2486
+ }
2487
+
2488
+ #applyPosition() {
2489
+ // Always bottom center
2490
+ this.style.position = "fixed";
2491
+ this.style.margin = "0";
2492
+ this.style.top = "auto";
2493
+ this.style.bottom = `${this.#offset}px`;
2494
+ this.style.left = "50%";
2495
+ this.style.right = "auto";
2496
+ this.style.transform = "translateX(-50%)";
2497
+ }
2498
+
2499
+ #startAutoClose() {
2500
+ this.#clearAutoClose();
2501
+
2502
+ const duration = parseInt(this.getAttribute("duration") ?? "5000");
2503
+ if (duration > 0) {
2504
+ this.#autoCloseTimer = setTimeout(() => {
2505
+ this.hideToast();
2506
+ }, duration);
2507
+ }
2508
+ }
2509
+
2510
+ #clearAutoClose() {
2511
+ if (this.#autoCloseTimer) {
2512
+ clearTimeout(this.#autoCloseTimer);
2513
+ this.#autoCloseTimer = null;
2514
+ }
2515
+ }
2516
+
2517
+ /**
2518
+ * Show the toast notification (non-modal)
2519
+ */
2520
+ showToast() {
2521
+ this.show(); // Non-modal show
2522
+ this.#applyPosition();
2523
+ this.#startAutoClose();
2524
+ this.dispatchEvent(new CustomEvent("toast-show", { bubbles: true }));
2525
+ }
2526
+
2527
+ /**
2528
+ * Hide the toast notification
2529
+ */
2530
+ hideToast() {
2531
+ this.#clearAutoClose();
2532
+ this.close();
2533
+ this.dispatchEvent(new CustomEvent("toast-hide", { bubbles: true }));
2534
+ }
2535
+
2536
+ static get observedAttributes() {
2537
+ return ["duration", "offset", "open", "theme"];
2538
+ }
2539
+
2540
+ attributeChangedCallback(name, oldValue, newValue) {
2541
+ if (name === "offset") {
2542
+ this.#applyPosition();
2543
+ }
2544
+
2545
+ if (name === "open") {
2546
+ if (newValue !== null && newValue !== "false") {
2547
+ this.showToast();
2548
+ } else {
2549
+ this.hideToast();
2550
+ }
2551
+ }
2391
2552
  }
2392
2553
  }
2393
- window.customElements.define("fig-accordion", FigAccordion);
2554
+ customElements.define("fig-toast", FigToast, { extends: "dialog" });
2394
2555
 
2395
2556
  /* Combo Input */
2396
2557
  /**
@@ -3065,15 +3226,14 @@ class FigInputAngle extends HTMLElement {
3065
3226
  </div>
3066
3227
  ${
3067
3228
  this.text
3068
- ? `<fig-input-text
3069
- type="number"
3229
+ ? `<fig-input-number
3070
3230
  name="angle"
3071
3231
  step="0.1"
3072
3232
  value="${this.angle}"
3073
3233
  min="0"
3074
- max="360">
3075
- <span slot="append">°</span>
3076
- </fig-input-text>`
3234
+ max="360"
3235
+ units="°">
3236
+ </fig-input-number>`
3077
3237
  : ""
3078
3238
  }
3079
3239
  `;
@@ -3082,7 +3242,7 @@ class FigInputAngle extends HTMLElement {
3082
3242
  #setupListeners() {
3083
3243
  this.handle = this.querySelector(".fig-input-angle-handle");
3084
3244
  this.plane = this.querySelector(".fig-input-angle-plane");
3085
- this.angleInput = this.querySelector("fig-input-text[name='angle']");
3245
+ this.angleInput = this.querySelector("fig-input-number[name='angle']");
3086
3246
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
3087
3247
  this.plane.addEventListener(
3088
3248
  "touchstart",
@@ -3091,7 +3251,6 @@ class FigInputAngle extends HTMLElement {
3091
3251
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
3092
3252
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
3093
3253
  if (this.text && this.angleInput) {
3094
- this.angleInput = this.querySelector("fig-input-text");
3095
3254
  this.angleInput.addEventListener(
3096
3255
  "input",
3097
3256
  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.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {