@rogieking/figui3 3.1.0 → 3.3.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 +65 -35
  2. package/fig.js +77 -65
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -367,6 +367,10 @@ textarea,
367
367
  input[type="text"],
368
368
  input[type="number"],
369
369
  input[type="password"],
370
+ input[type="url"],
371
+ input[type="email"],
372
+ input[type="search"],
373
+ input[type="tel"],
370
374
  .input {
371
375
  background-color: var(--figma-color-bg-secondary);
372
376
  border: 0;
@@ -395,28 +399,47 @@ input[type="password"],
395
399
  }
396
400
  }
397
401
 
398
- fig-input-text,
399
- fig-input-number {
400
- &:has([slot="append"]) input[type="number"] {
401
- &::-webkit-outer-spin-button,
402
- &::-webkit-inner-spin-button {
403
- display: none;
404
- color-scheme: inherit;
405
- margin: 0;
406
- opacity: 0;
402
+ fig-input-number .fig-steppers {
403
+ display: flex;
404
+ flex-direction: column;
405
+ flex-shrink: 0;
406
+ border-radius: 0 var(--radius-medium) var(--radius-medium) 0;
407
+ overflow: hidden;
408
+
409
+ & button {
410
+ display: block;
411
+ appearance: none;
412
+ border: 0;
413
+ padding: 0;
414
+ margin: 0;
415
+ width: 1.5rem;
416
+ flex: 1;
417
+ background-color: var(--figma-color-text-secondary);
418
+ cursor: pointer;
419
+ mask-size: 1rem;
420
+ mask-repeat: no-repeat;
421
+ mask-position: center -3.5px;
422
+
423
+ &:hover {
424
+ background-color: var(--figma-color-text);
407
425
  }
408
- }
409
- &[steppers="true"] {
410
- input[type="number"]::-webkit-inner-spin-button {
411
- display: block;
412
- opacity: 1;
413
- height: 1.5rem;
414
- width: 1.5rem;
415
- margin-right: calc(var(--spacer-2) * -1);
416
- background-color: var(--figma-color-text-secondary);
417
- mask-image: var(--icon-steppers);
426
+ &:active {
427
+ background-color: var(--figma-color-text-tertiary);
418
428
  }
419
429
  }
430
+
431
+ fig-input-number[disabled]:not([disabled="false"]) & button {
432
+ pointer-events: none;
433
+ background-color: var(--figma-color-text-disabled);
434
+ }
435
+
436
+ & .fig-stepper-up {
437
+ mask-image: var(--icon-chevron);
438
+ rotate: 180deg;
439
+ }
440
+ & .fig-stepper-down {
441
+ mask-image: var(--icon-chevron);
442
+ }
420
443
  }
421
444
 
422
445
  /* Textarea */
@@ -2681,6 +2704,7 @@ dialog[is="fig-toast"] {
2681
2704
  justify-content: center;
2682
2705
  min-width: 0;
2683
2706
  color: var(--figma-color-text);
2707
+ background-color: var(--figma-color-bg);
2684
2708
 
2685
2709
  &[open] {
2686
2710
  display: flex;
@@ -2703,32 +2727,34 @@ dialog[is="fig-toast"] {
2703
2727
 
2704
2728
  /* Theme: Dark (default) - hardcoded to ensure consistency regardless of global theme */
2705
2729
  &[theme="dark"] {
2706
- background-color: #2c2c2c;
2707
- color: rgba(255, 255, 255, 0.9);
2708
2730
  color-scheme: dark;
2709
2731
  }
2710
2732
 
2711
2733
  /* Theme: Light - hardcoded to ensure consistency regardless of global theme */
2712
2734
  &[theme="light"] {
2713
- background-color: #ffffff;
2714
- color: rgba(0, 0, 0, 0.9);
2715
- box-shadow: var(--figma-elevation-500-modal-window);
2716
2735
  color-scheme: light;
2717
2736
  }
2718
2737
 
2719
2738
  /* Theme: Danger - hardcoded to ensure consistency regardless of global theme */
2720
2739
  &[theme="danger"] {
2721
- background-color: #f24822;
2722
- color: #ffffff;
2740
+ background-color: var(--figma-color-bg-danger);
2723
2741
  color-scheme: dark;
2724
2742
  }
2725
2743
 
2726
2744
  /* Theme: Brand - hardcoded to ensure consistency regardless of global theme */
2727
2745
  &[theme="brand"] {
2728
- background-color: #0d99ff;
2729
- color: #ffffff;
2746
+ background-color: var(--figma-color-bg-brand);
2747
+ color-scheme: dark;
2748
+ }
2749
+
2750
+ &[theme="success"] {
2751
+ background-color: var(--figma-color-bg-success);
2730
2752
  color-scheme: dark;
2731
2753
  }
2754
+
2755
+ &[theme="auto"] {
2756
+ /* color-scheme is resolved at runtime by FigToast JS */
2757
+ }
2732
2758
  }
2733
2759
 
2734
2760
  /* Tooltip */
@@ -2958,9 +2984,13 @@ fig-input-number {
2958
2984
  flex: 1;
2959
2985
  color: var(--figma-color-text);
2960
2986
 
2987
+ &[full]:not([full="false"]) {
2988
+ display: flex;
2989
+ width: 100%;
2990
+ }
2961
2991
  &[multiline] {
2962
2992
  display: flex;
2963
- width: 100%; /* Multiline defaults to full width */
2993
+ width: 100%;
2964
2994
  }
2965
2995
  &[autoresize] input,
2966
2996
  &[autoresize] textarea {
@@ -3064,8 +3094,7 @@ fig-input-fill {
3064
3094
  }
3065
3095
  }
3066
3096
 
3067
- fig-field[direction="horizontal"] &,
3068
- fig-field[direction="row"] & {
3097
+ fig-field[direction="horizontal"] & {
3069
3098
  flex: 1;
3070
3099
  min-width: 0;
3071
3100
  }
@@ -3082,8 +3111,7 @@ fig-slider {
3082
3111
  flex-basis: 5rem;
3083
3112
  }
3084
3113
 
3085
- fig-field[direction="horizontal"] &,
3086
- fig-field[direction="row"] & {
3114
+ fig-field[direction="horizontal"] & {
3087
3115
  flex: 1;
3088
3116
  min-width: 0;
3089
3117
  }
@@ -3099,7 +3127,6 @@ fig-field,
3099
3127
  gap: 0;
3100
3128
  align-items: start;
3101
3129
 
3102
- &[direction="row"],
3103
3130
  &[direction="horizontal"] {
3104
3131
  flex-direction: row;
3105
3132
  align-items: center;
@@ -3121,10 +3148,13 @@ fig-field,
3121
3148
  width: 100%;
3122
3149
  }
3123
3150
 
3124
- &[direction="row"] > label,
3125
3151
  &[direction="horizontal"] > label {
3126
3152
  width: auto;
3127
3153
  min-width: var(--field-label-width);
3154
+ max-width: var(--field-label-width);
3155
+ overflow: hidden;
3156
+ text-overflow: ellipsis;
3157
+ white-space: nowrap;
3128
3158
  }
3129
3159
 
3130
3160
  &[direction="horizontal"] {
package/fig.js CHANGED
@@ -2253,15 +2253,15 @@ class FigTabs extends HTMLElement {
2253
2253
  this.setAttribute("role", "tablist");
2254
2254
  this.addEventListener("click", this.handleClick.bind(this));
2255
2255
  this.addEventListener("keydown", this.#handleKeyDown.bind(this));
2256
- // Set initial selected tab based on value
2257
- const value = this.getAttribute("value");
2258
- if (value) {
2259
- this.#selectByValue(value);
2260
- }
2261
- // Apply disabled state
2262
- if (this.hasAttribute("disabled")) {
2263
- this.#applyDisabled(true);
2264
- }
2256
+ requestAnimationFrame(() => {
2257
+ const value = this.getAttribute("value");
2258
+ if (value) {
2259
+ this.#selectByValue(value);
2260
+ }
2261
+ if (this.hasAttribute("disabled")) {
2262
+ this.#applyDisabled(true);
2263
+ }
2264
+ });
2265
2265
  }
2266
2266
 
2267
2267
  #applyDisabled(disabled) {
@@ -2333,6 +2333,7 @@ class FigTabs extends HTMLElement {
2333
2333
  for (const tab of tabs) {
2334
2334
  if (tab.getAttribute("value") === value) {
2335
2335
  this.selectedTab = tab;
2336
+ tab.setAttribute("selected", "true");
2336
2337
  } else {
2337
2338
  tab.removeAttribute("selected");
2338
2339
  }
@@ -2353,7 +2354,6 @@ class FigTabs extends HTMLElement {
2353
2354
  }
2354
2355
 
2355
2356
  handleClick(event) {
2356
- // Ignore clicks when disabled
2357
2357
  if (this.hasAttribute("disabled")) return;
2358
2358
  const target = event.target;
2359
2359
  if (target.nodeName.toLowerCase() === "fig-tab") {
@@ -2361,6 +2361,7 @@ class FigTabs extends HTMLElement {
2361
2361
  for (const tab of tabs) {
2362
2362
  if (tab === target) {
2363
2363
  this.selectedTab = tab;
2364
+ tab.setAttribute("selected", "true");
2364
2365
  this.setAttribute("value", tab.getAttribute("value") || "");
2365
2366
  } else {
2366
2367
  tab.removeAttribute("selected");
@@ -3459,20 +3460,46 @@ class FigInputNumber extends HTMLElement {
3459
3460
  #unitPosition;
3460
3461
  #precision;
3461
3462
  #isInteracting = false;
3463
+ #stepperEl = null;
3464
+
3465
+ #syncSteppers(hasSteppers) {
3466
+ if (hasSteppers && !this.#stepperEl) {
3467
+ this.#stepperEl = document.createElement("span");
3468
+ this.#stepperEl.className = "fig-steppers";
3469
+ this.#stepperEl.innerHTML =
3470
+ `<button class="fig-stepper-up" tabindex="-1" aria-label="Increase"></button>` +
3471
+ `<button class="fig-stepper-down" tabindex="-1" aria-label="Decrease"></button>`;
3472
+ this.#stepperEl.addEventListener("pointerdown", (e) => {
3473
+ e.preventDefault();
3474
+ e.stopPropagation();
3475
+ const btn = e.target.closest("button");
3476
+ if (!btn || this.disabled) return;
3477
+ const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
3478
+ this.#stepValue(dir);
3479
+ this.input.focus();
3480
+ });
3481
+ this.append(this.#stepperEl);
3482
+ } else if (!hasSteppers && this.#stepperEl) {
3483
+ this.#stepperEl.remove();
3484
+ this.#stepperEl = null;
3485
+ }
3486
+ }
3462
3487
 
3463
- #syncNativeNumberAttributes() {
3464
- if (!this.input || this.input.type !== "number") return;
3465
-
3466
- ["min", "max", "step"].forEach((name) => {
3467
- const attrValue = this.getAttribute(name);
3468
- if (attrValue === null || attrValue === "") {
3469
- this.input.removeAttribute(name);
3470
- this.input[name] = "";
3471
- return;
3472
- }
3473
- this.input.setAttribute(name, attrValue);
3474
- this.input[name] = attrValue;
3475
- });
3488
+ #stepValue(direction) {
3489
+ const step = this.step || 1;
3490
+ let numericValue = this.#getNumericValue(this.input.value);
3491
+ let value =
3492
+ (numericValue !== "" ? Number(numericValue) / (this.transform || 1) : 0) +
3493
+ step * direction;
3494
+ value = this.#sanitizeInput(value, false);
3495
+ this.value = value;
3496
+ this.input.value = this.#formatWithUnit(this.value);
3497
+ this.dispatchEvent(
3498
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3499
+ );
3500
+ this.dispatchEvent(
3501
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3502
+ );
3476
3503
  }
3477
3504
 
3478
3505
  constructor() {
@@ -3528,19 +3555,12 @@ class FigInputNumber extends HTMLElement {
3528
3555
  this.hasAttribute("steppers") &&
3529
3556
  this.getAttribute("steppers") !== "false";
3530
3557
 
3531
- // Use type="number" when steppers are enabled (for native spin buttons)
3532
- const inputType = hasSteppers ? "number" : "text";
3533
- const inputMode = hasSteppers ? "" : 'inputmode="decimal"';
3534
- const inputValue = hasSteppers
3535
- ? this.#transformNumber(this.value)
3536
- : this.#formatWithUnit(this.value);
3537
-
3538
3558
  let html = `<input
3539
- type="${inputType}"
3540
- ${inputMode}
3559
+ type="text"
3560
+ inputmode="decimal"
3541
3561
  ${this.name ? `name="${this.name}"` : ""}
3542
3562
  placeholder="${this.placeholder}"
3543
- value="${inputValue}" />`;
3563
+ value="${this.#formatWithUnit(this.value)}" />`;
3544
3564
 
3545
3565
  //child nodes hack
3546
3566
  requestAnimationFrame(() => {
@@ -3569,7 +3589,8 @@ class FigInputNumber extends HTMLElement {
3569
3589
  if (this.getAttribute("step")) {
3570
3590
  this.step = Number(this.getAttribute("step"));
3571
3591
  }
3572
- this.#syncNativeNumberAttributes();
3592
+
3593
+ this.#syncSteppers(hasSteppers);
3573
3594
 
3574
3595
  // Set disabled state if present
3575
3596
  if (this.hasAttribute("disabled")) {
@@ -3845,24 +3866,15 @@ class FigInputNumber extends HTMLElement {
3845
3866
  break;
3846
3867
  case "units":
3847
3868
  this.#units = newValue || "";
3848
- this.input.value =
3849
- this.input.type === "number"
3850
- ? this.#transformNumber(this.value)
3851
- : this.#formatWithUnit(this.value);
3869
+ this.input.value = this.#formatWithUnit(this.value);
3852
3870
  break;
3853
3871
  case "unit-position":
3854
3872
  this.#unitPosition = newValue || "suffix";
3855
- this.input.value =
3856
- this.input.type === "number"
3857
- ? this.#transformNumber(this.value)
3858
- : this.#formatWithUnit(this.value);
3873
+ this.input.value = this.#formatWithUnit(this.value);
3859
3874
  break;
3860
3875
  case "transform":
3861
3876
  this.transform = Number(newValue) || 1;
3862
- this.input.value =
3863
- this.input.type === "number"
3864
- ? this.#transformNumber(this.value)
3865
- : this.#formatWithUnit(this.value);
3877
+ this.input.value = this.#formatWithUnit(this.value);
3866
3878
  break;
3867
3879
  case "value":
3868
3880
  if (this.#isInteracting) break;
@@ -3872,41 +3884,25 @@ class FigInputNumber extends HTMLElement {
3872
3884
  value = this.#sanitizeInput(value, false);
3873
3885
  }
3874
3886
  this.value = value;
3875
- this.input.value =
3876
- this.input.type === "number"
3877
- ? this.#transformNumber(this.value)
3878
- : this.#formatWithUnit(this.value);
3887
+ this.input.value = this.#formatWithUnit(this.value);
3879
3888
  break;
3880
3889
  case "min":
3881
3890
  case "max":
3882
3891
  case "step":
3883
3892
  if (newValue === null || newValue === "") {
3884
3893
  this[name] = undefined;
3885
- this.#syncNativeNumberAttributes();
3886
3894
  break;
3887
3895
  }
3888
3896
  this[name] = Number(newValue);
3889
- this.#syncNativeNumberAttributes();
3890
3897
  break;
3891
3898
  case "steppers": {
3892
3899
  const hasSteppers = newValue !== null && newValue !== "false";
3893
- this.input.type = hasSteppers ? "number" : "text";
3894
- if (hasSteppers) {
3895
- this.input.removeAttribute("inputmode");
3896
- this.#syncNativeNumberAttributes();
3897
- this.input.value = this.#transformNumber(this.value);
3898
- } else {
3899
- this.input.setAttribute("inputmode", "decimal");
3900
- this.input.value = this.#formatWithUnit(this.value);
3901
- }
3900
+ this.#syncSteppers(hasSteppers);
3902
3901
  break;
3903
3902
  }
3904
3903
  case "precision":
3905
3904
  this.#precision = newValue !== null ? Number(newValue) : 2;
3906
- this.input.value =
3907
- this.input.type === "number"
3908
- ? this.#transformNumber(this.value)
3909
- : this.#formatWithUnit(this.value);
3905
+ this.input.value = this.#formatWithUnit(this.value);
3910
3906
  break;
3911
3907
  case "name":
3912
3908
  this[name] = this.input[name] = newValue;
@@ -5425,10 +5421,18 @@ class FigToast extends HTMLDialogElement {
5425
5421
  }
5426
5422
  }
5427
5423
 
5424
+ #resolveAutoTheme() {
5425
+ if (this.getAttribute("theme") !== "auto") return;
5426
+ const cs = getComputedStyle(document.documentElement).colorScheme || "";
5427
+ const isDark = cs.includes("dark");
5428
+ this.style.colorScheme = isDark ? "light" : "dark";
5429
+ }
5430
+
5428
5431
  /**
5429
5432
  * Show the toast notification (non-modal)
5430
5433
  */
5431
5434
  showToast() {
5435
+ this.#resolveAutoTheme();
5432
5436
  this.show(); // Non-modal show
5433
5437
  this.applyPosition();
5434
5438
  this.startAutoClose();
@@ -5460,6 +5464,14 @@ class FigToast extends HTMLDialogElement {
5460
5464
  this.hideToast();
5461
5465
  }
5462
5466
  }
5467
+
5468
+ if (name === "theme") {
5469
+ if (newValue === "auto") {
5470
+ this.#resolveAutoTheme();
5471
+ } else {
5472
+ this.style.removeProperty("color-scheme");
5473
+ }
5474
+ }
5463
5475
  }
5464
5476
  }
5465
5477
  figDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.1.0",
3
+ "version": "3.3.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",