@rogieking/figui3 1.9.6 → 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 +344 -411
  3. package/example.html +1906 -1051
  4. package/fig.js +380 -27
  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
 
@@ -592,20 +607,232 @@ customElements.define("fig-popover", FigPopover);
592
607
  * @attr {boolean} modal - Whether the dialog should be modal
593
608
  */
594
609
  class FigDialog extends HTMLDialogElement {
610
+ #isDragging = false;
611
+ #dragOffset = { x: 0, y: 0 };
612
+ #boundPointerDown;
613
+ #boundPointerMove;
614
+ #boundPointerUp;
615
+ #offset = 16; // 1rem in pixels
616
+ #positionInitialized = false;
617
+
618
+ constructor() {
619
+ super();
620
+ this.#boundPointerDown = this.#handlePointerDown.bind(this);
621
+ this.#boundPointerMove = this.#handlePointerMove.bind(this);
622
+ this.#boundPointerUp = this.#handlePointerUp.bind(this);
623
+ }
624
+
595
625
  connectedCallback() {
596
626
  this.modal =
597
627
  this.hasAttribute("modal") && this.getAttribute("modal") !== "false";
628
+
629
+ // Set up drag functionality
630
+ this.drag =
631
+ this.hasAttribute("drag") && this.getAttribute("drag") !== "false";
632
+
598
633
  requestAnimationFrame(() => {
599
634
  this.#addCloseListeners();
635
+ this.#setupDragListeners();
636
+ this.#applyPosition();
600
637
  });
601
638
  }
602
639
 
640
+ disconnectedCallback() {
641
+ this.#removeDragListeners();
642
+ }
643
+
603
644
  #addCloseListeners() {
604
645
  this.querySelectorAll("fig-button[close-dialog]").forEach((button) => {
605
646
  button.removeEventListener("click", this.close);
606
647
  button.addEventListener("click", this.close.bind(this));
607
648
  });
608
649
  }
650
+
651
+ #applyPosition() {
652
+ const position = this.getAttribute("position") || "";
653
+
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";
664
+
665
+ // Parse position attribute
666
+ const hasTop = position.includes("top");
667
+ const hasBottom = position.includes("bottom");
668
+ const hasLeft = position.includes("left");
669
+ const hasRight = position.includes("right");
670
+ const hasVCenter = position.includes("center") && !hasTop && !hasBottom;
671
+ const hasHCenter = position.includes("center") && !hasLeft && !hasRight;
672
+
673
+ // Vertical positioning
674
+ if (hasTop) {
675
+ this.style.top = `${this.#offset}px`;
676
+ } else if (hasBottom) {
677
+ this.style.bottom = `${this.#offset}px`;
678
+ } else if (hasVCenter) {
679
+ this.style.top = "50%";
680
+ }
681
+
682
+ // Horizontal positioning
683
+ if (hasLeft) {
684
+ this.style.left = `${this.#offset}px`;
685
+ } else if (hasRight) {
686
+ this.style.right = `${this.#offset}px`;
687
+ } else if (hasHCenter) {
688
+ this.style.left = "50%";
689
+ }
690
+
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
+ }
699
+
700
+ this.#positionInitialized = true;
701
+ }
702
+
703
+ #setupDragListeners() {
704
+ if (this.drag) {
705
+ this.addEventListener("pointerdown", this.#boundPointerDown);
706
+ // Set move cursor only on fig-header elements
707
+ const header = this.querySelector("fig-header, header");
708
+ if (header) {
709
+ header.style.cursor = "move";
710
+ }
711
+ }
712
+ }
713
+
714
+ #removeDragListeners() {
715
+ this.removeEventListener("pointerdown", this.#boundPointerDown);
716
+ document.removeEventListener("pointermove", this.#boundPointerMove);
717
+ document.removeEventListener("pointerup", this.#boundPointerUp);
718
+ }
719
+
720
+ #isInteractiveElement(element) {
721
+ // List of interactive element types and attributes to avoid dragging on
722
+ const interactiveSelectors = [
723
+ "input",
724
+ "button",
725
+ "select",
726
+ "textarea",
727
+ "a",
728
+ "label",
729
+ '[contenteditable="true"]',
730
+ "[tabindex]",
731
+ "fig-button",
732
+ "fig-input-text",
733
+ "fig-input-number",
734
+ "fig-slider",
735
+ "fig-checkbox",
736
+ "fig-radio",
737
+ "fig-tab",
738
+ "fig-dropdown",
739
+ "fig-chit",
740
+ ];
741
+
742
+ // Check if the element itself is interactive
743
+ if (interactiveSelectors.some((selector) => element.matches?.(selector))) {
744
+ return true;
745
+ }
746
+
747
+ // Check if any parent element up to the dialog is interactive
748
+ let parent = element.parentElement;
749
+ while (parent && parent !== this) {
750
+ if (interactiveSelectors.some((selector) => parent.matches?.(selector))) {
751
+ return true;
752
+ }
753
+ parent = parent.parentElement;
754
+ }
755
+
756
+ return false;
757
+ }
758
+
759
+ #handlePointerDown(e) {
760
+ if (!this.drag || this.#isInteractiveElement(e.target)) {
761
+ return;
762
+ }
763
+
764
+ this.#isDragging = true;
765
+ this.setPointerCapture(e.pointerId);
766
+
767
+ // Get current position from computed style
768
+ const rect = this.getBoundingClientRect();
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
+
776
+ // Store offset from pointer to dialog top-left corner
777
+ this.#dragOffset.x = e.clientX - rect.left;
778
+ this.#dragOffset.y = e.clientY - rect.top;
779
+
780
+ document.addEventListener("pointermove", this.#boundPointerMove);
781
+ document.addEventListener("pointerup", this.#boundPointerUp);
782
+
783
+ e.preventDefault();
784
+ }
785
+
786
+ #handlePointerMove(e) {
787
+ if (!this.#isDragging) return;
788
+
789
+ // Calculate new position based on pointer position minus offset
790
+ const newLeft = e.clientX - this.#dragOffset.x;
791
+ const newTop = e.clientY - this.#dragOffset.y;
792
+
793
+ // Apply position directly with pixels
794
+ this.style.left = `${newLeft}px`;
795
+ this.style.top = `${newTop}px`;
796
+
797
+ e.preventDefault();
798
+ }
799
+
800
+ #handlePointerUp(e) {
801
+ if (!this.#isDragging) return;
802
+
803
+ this.#isDragging = false;
804
+ this.releasePointerCapture(e.pointerId);
805
+
806
+ document.removeEventListener("pointermove", this.#boundPointerMove);
807
+ document.removeEventListener("pointerup", this.#boundPointerUp);
808
+
809
+ e.preventDefault();
810
+ }
811
+
812
+ static get observedAttributes() {
813
+ return ["modal", "drag", "position"];
814
+ }
815
+
816
+ attributeChangedCallback(name, oldValue, newValue) {
817
+ if (name === "drag") {
818
+ this.drag = newValue !== null && newValue !== "false";
819
+
820
+ if (this.drag) {
821
+ this.#setupDragListeners();
822
+ } else {
823
+ this.#removeDragListeners();
824
+ // Remove move cursor from header
825
+ const header = this.querySelector("fig-header, header");
826
+ if (header) {
827
+ header.style.cursor = "";
828
+ }
829
+ }
830
+ }
831
+
832
+ if (name === "position" && this.#positionInitialized) {
833
+ this.#applyPosition();
834
+ }
835
+ }
609
836
  }
610
837
  customElements.define("fig-dialog", FigDialog, { extends: "dialog" });
611
838
 
@@ -2078,8 +2305,7 @@ class FigCheckbox extends HTMLElement {
2078
2305
  this.input.setAttribute("id", figUniqueId());
2079
2306
  this.input.setAttribute("name", this.name);
2080
2307
  this.input.setAttribute("type", "checkbox");
2081
- this.labelElement = document.createElement("label");
2082
- this.labelElement.setAttribute("for", this.input.id);
2308
+ this.labelElement = null;
2083
2309
  }
2084
2310
  connectedCallback() {
2085
2311
  this.checked = this.input.checked =
@@ -2095,7 +2321,12 @@ class FigCheckbox extends HTMLElement {
2095
2321
  }
2096
2322
 
2097
2323
  this.append(this.input);
2098
- 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
+ }
2099
2330
 
2100
2331
  this.render();
2101
2332
  }
@@ -2103,6 +2334,14 @@ class FigCheckbox extends HTMLElement {
2103
2334
  return ["disabled", "label", "checked", "name", "value"];
2104
2335
  }
2105
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
+
2106
2345
  render() {}
2107
2346
 
2108
2347
  focus() {
@@ -2116,7 +2355,13 @@ class FigCheckbox extends HTMLElement {
2116
2355
  attributeChangedCallback(name, oldValue, newValue) {
2117
2356
  switch (name) {
2118
2357
  case "label":
2119
- 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
+ }
2120
2365
  break;
2121
2366
  case "checked":
2122
2367
  this.checked = this.input.checked =
@@ -2174,29 +2419,139 @@ class FigSwitch extends FigCheckbox {
2174
2419
  }
2175
2420
  window.customElements.define("fig-switch", FigSwitch);
2176
2421
 
2177
- /* Bell */
2178
- 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
+
2179
2435
  constructor() {
2180
2436
  super();
2181
2437
  }
2182
- }
2183
- window.customElements.define("fig-bell", FigBell);
2184
2438
 
2185
- /* Badge */
2186
- class FigBadge extends HTMLElement {
2187
- constructor() {
2188
- super();
2439
+ get #offset() {
2440
+ return parseInt(this.getAttribute("offset") ?? this.#defaultOffset);
2189
2441
  }
2190
- }
2191
- window.customElements.define("fig-badge", FigBadge);
2192
2442
 
2193
- /* Accordion */
2194
- class FigAccordion extends HTMLElement {
2195
- constructor() {
2196
- 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
+ }
2197
2552
  }
2198
2553
  }
2199
- window.customElements.define("fig-accordion", FigAccordion);
2554
+ customElements.define("fig-toast", FigToast, { extends: "dialog" });
2200
2555
 
2201
2556
  /* Combo Input */
2202
2557
  /**
@@ -2871,15 +3226,14 @@ class FigInputAngle extends HTMLElement {
2871
3226
  </div>
2872
3227
  ${
2873
3228
  this.text
2874
- ? `<fig-input-text
2875
- type="number"
3229
+ ? `<fig-input-number
2876
3230
  name="angle"
2877
3231
  step="0.1"
2878
3232
  value="${this.angle}"
2879
3233
  min="0"
2880
- max="360">
2881
- <span slot="append">°</span>
2882
- </fig-input-text>`
3234
+ max="360"
3235
+ units="°">
3236
+ </fig-input-number>`
2883
3237
  : ""
2884
3238
  }
2885
3239
  `;
@@ -2888,7 +3242,7 @@ class FigInputAngle extends HTMLElement {
2888
3242
  #setupListeners() {
2889
3243
  this.handle = this.querySelector(".fig-input-angle-handle");
2890
3244
  this.plane = this.querySelector(".fig-input-angle-plane");
2891
- this.angleInput = this.querySelector("fig-input-text[name='angle']");
3245
+ this.angleInput = this.querySelector("fig-input-number[name='angle']");
2892
3246
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
2893
3247
  this.plane.addEventListener(
2894
3248
  "touchstart",
@@ -2897,7 +3251,6 @@ class FigInputAngle extends HTMLElement {
2897
3251
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
2898
3252
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
2899
3253
  if (this.text && this.angleInput) {
2900
- this.angleInput = this.querySelector("fig-input-text");
2901
3254
  this.angleInput.addEventListener(
2902
3255
  "input",
2903
3256
  this.#handleAngleInput.bind(this)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "1.9.6",
3
+ "version": "2.0.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {