@rogieking/figui3 8.9.13 → 8.9.15

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/fig-lab.css CHANGED
@@ -383,7 +383,7 @@ fig-content:has(> propskit-group[name]:last-child) {
383
383
  }
384
384
 
385
385
  /* Flex-positioned numbers — margin works. Absolute numbers use right: calc(...) below. */
386
- :is(propskit-color, propskit-number)[size="large"] {
386
+ propskit-number[size="large"] {
387
387
  fig-field fig-input-number {
388
388
  margin-right: var(--spacer-1);
389
389
  }
@@ -504,7 +504,7 @@ propskit-color {
504
504
  align-items: center;
505
505
  gap: var(--spacer-2);
506
506
  height: 100%;
507
- padding-left: var(--spacer-2);
507
+ padding: 0 0 0 var(--spacer-2);
508
508
  border-radius: var(--radius-medium);
509
509
  background-color: var(--figma-color-bg-secondary);
510
510
 
@@ -525,9 +525,8 @@ propskit-color {
525
525
  z-index: 1;
526
526
  display: flex;
527
527
  align-items: center;
528
- flex: 0 0 auto;
529
- width: auto;
530
- max-width: 50%;
528
+ flex: 0 1 auto;
529
+ max-width: 40%;
531
530
  min-width: 0;
532
531
  margin: 0;
533
532
  padding: 0;
@@ -542,55 +541,26 @@ propskit-color {
542
541
  }
543
542
  }
544
543
 
545
- fig-input-color {
546
- --figma-focus-outline-offset: 1px;
544
+ fig-fill-picker {
545
+ display: contents;
546
+ }
547
547
 
548
+ fig-swatch {
548
549
  position: relative;
549
- align-self: center;
550
- flex: 0 1 auto !important;
551
- width: fit-content !important;
552
- max-width: none;
550
+ flex: 0 0 33%;
551
+ width: 33%;
553
552
  min-width: 0;
554
553
  margin-left: auto;
554
+ --width: 100%;
555
+ --height: var(--propskit-color-height);
556
+ --size: var(--propskit-color-height);
557
+ --border-radius: var(--radius-medium);
555
558
  height: var(--propskit-color-height);
556
559
  border-radius: var(--radius-medium);
557
- background-color: transparent;
558
- box-shadow: none;
559
560
 
560
- /* Field ::after owns hover chrome — don't double-outline the color control. */
561
- &:hover:not(:focus):not(:focus-within):not(.has-popup-open) {
561
+ &:hover {
562
562
  outline: none !important;
563
563
  }
564
-
565
- > .input-combo {
566
- flex: 0 0 auto;
567
- width: auto;
568
- }
569
-
570
- fig-input-text {
571
- flex: 0 0 3.25rem;
572
- width: 3.25rem;
573
-
574
- input {
575
- field-sizing: fixed;
576
- box-sizing: border-box;
577
- width: 100%;
578
- min-width: 0;
579
- }
580
- }
581
-
582
- fig-input-number {
583
- flex: 0 0 3rem;
584
- width: 3rem;
585
- min-width: 3rem;
586
-
587
- input {
588
- field-sizing: fixed;
589
- box-sizing: border-box;
590
- width: 100%;
591
- min-width: 0;
592
- }
593
- }
594
564
  }
595
565
  }
596
566
  }
@@ -612,8 +582,15 @@ propskit-gradient {
612
582
  &[size="large"] {
613
583
  --propskit-gradient-height: 2rem;
614
584
 
615
- fig-field:not([direction="vertical"]) {
616
- padding-right: var(--spacer-1);
585
+ fig-input-gradient {
586
+ --fig-input-gradient-height: var(--propskit-gradient-height);
587
+ height: var(--propskit-gradient-height);
588
+
589
+ fig-swatch {
590
+ --height: var(--propskit-gradient-height);
591
+ --size: var(--propskit-gradient-height);
592
+ height: var(--propskit-gradient-height);
593
+ }
617
594
  }
618
595
  }
619
596
 
@@ -631,7 +608,7 @@ propskit-gradient {
631
608
  align-items: center;
632
609
  gap: var(--spacer-2);
633
610
  height: 100%;
634
- padding: 0 var(--spacer-1) 0 var(--spacer-2);
611
+ padding: 0 0 0 var(--spacer-2);
635
612
  border-radius: var(--radius-medium);
636
613
  background-color: var(--figma-color-bg-secondary);
637
614
 
@@ -670,8 +647,8 @@ propskit-gradient {
670
647
 
671
648
  fig-input-gradient {
672
649
  position: relative;
673
- flex: 0 0 50%;
674
- width: 50%;
650
+ flex: 0 0 33%;
651
+ width: 33%;
675
652
  min-width: 0;
676
653
  margin-left: auto;
677
654
  border-radius: var(--radius-medium);
package/fig-lab.js CHANGED
@@ -699,11 +699,37 @@ class PropskitSwitch extends HTMLElement {
699
699
  }
700
700
  figLabDefineElement("propskit-switch", PropskitSwitch);
701
701
 
702
+ function figLabParseSolidColor(raw) {
703
+ const value = String(raw ?? "").trim();
704
+ if (/^#[0-9a-fA-F]{8}$/.test(value)) {
705
+ return {
706
+ color: value.slice(0, 7),
707
+ alpha: parseInt(value.slice(7, 9), 16) / 255,
708
+ };
709
+ }
710
+ if (/^#[0-9a-fA-F]{6}$/.test(value)) {
711
+ return { color: value, alpha: 1 };
712
+ }
713
+ if (/^#[0-9a-fA-F]{3}$/.test(value)) {
714
+ return {
715
+ color: `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`,
716
+ alpha: 1,
717
+ };
718
+ }
719
+ return { color: value || "#D9D9D9", alpha: 1 };
720
+ }
721
+
722
+ function figLabSolidFillValue(raw) {
723
+ const { color, alpha } = figLabParseSolidColor(raw);
724
+ return JSON.stringify({ type: "solid", color, alpha });
725
+ }
726
+
702
727
  /* Field + Color wrapper */
703
728
  class PropskitColor extends HTMLElement {
704
729
  #field = null;
705
730
  #label = null;
706
731
  #input = null;
732
+ #swatch = null;
707
733
  #hasCustomLabel = false;
708
734
  #observer = null;
709
735
  #managedInputAttrs = new Set();
@@ -779,15 +805,18 @@ class PropskitColor extends HTMLElement {
779
805
  );
780
806
  const field = document.createElement("fig-field");
781
807
  const label = customLabel || document.createElement("label");
782
- const input = document.createElement("fig-input-color");
783
-
808
+ const picker = document.createElement("fig-fill-picker");
809
+ const swatch = document.createElement("fig-swatch");
810
+ picker.append(swatch);
811
+ swatch.setAttribute("tabindex", "0");
784
812
  for (const node of initialChildren) {
785
- if (node !== customLabel) input.appendChild(node);
813
+ if (node !== customLabel) picker.appendChild(node);
786
814
  }
787
- field.append(label, input);
815
+ field.append(label, picker);
788
816
  this.#field = field;
789
817
  this.#label = label;
790
- this.#input = input;
818
+ this.#input = picker;
819
+ this.#swatch = swatch;
791
820
  this.#hasCustomLabel = Boolean(customLabel);
792
821
  this.replaceChildren(field);
793
822
  }
@@ -821,6 +850,15 @@ class PropskitColor extends HTMLElement {
821
850
  );
822
851
  }
823
852
 
853
+ #parsedHostColor() {
854
+ return figLabParseSolidColor(this.getAttribute("value"));
855
+ }
856
+
857
+ #pickerValueFromHost() {
858
+ const { color, alpha } = this.#parsedHostColor();
859
+ return JSON.stringify({ type: "solid", color, alpha });
860
+ }
861
+
824
862
  #getForwardedInputAttrNames() {
825
863
  const reserved = new Set([
826
864
  "label",
@@ -834,6 +872,8 @@ class PropskitColor extends HTMLElement {
834
872
  "aria-label",
835
873
  "text",
836
874
  "default",
875
+ "value",
876
+ "mode",
837
877
  ]);
838
878
  return this.getAttributeNames().filter(
839
879
  (name) => !reserved.has(name) && !name.startsWith("data-"),
@@ -849,19 +889,19 @@ class PropskitColor extends HTMLElement {
849
889
  if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
850
890
  }
851
891
  for (const attrName of inputAttrs) {
852
- const next = this.getAttribute(attrName) ?? "";
853
- // Soft sync only. Never force-clear value here — that fights live edits
854
- // (fig-input-color often keeps a stale value attribute while editing).
855
- if (
856
- attrName === "value" &&
857
- this.#input.getAttribute("value") === next
858
- ) {
859
- continue;
860
- }
861
- this.#input.setAttribute(attrName, next);
892
+ this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
862
893
  }
863
894
 
864
- this.#input.setAttribute("text", "true");
895
+ this.#input.setAttribute("mode", "solid");
896
+ const nextJson = this.#pickerValueFromHost();
897
+ if (this.#input.getAttribute("value") !== nextJson) {
898
+ this.#input.setAttribute("value", nextJson);
899
+ }
900
+ const { color, alpha } = this.#parsedHostColor();
901
+ if (this.#swatch) {
902
+ this.#swatch.setAttribute("background", color);
903
+ this.#swatch.setAttribute("alpha", String(alpha));
904
+ }
865
905
  this.#managedInputAttrs = nextManaged;
866
906
  }
867
907
 
@@ -888,31 +928,62 @@ class PropskitColor extends HTMLElement {
888
928
  event instanceof CustomEvent && event.detail !== undefined
889
929
  ? event.detail
890
930
  : undefined;
891
- if (typeof detail === "string" && detail) return detail;
931
+ if (typeof detail === "string" && detail) {
932
+ return figLabParseSolidColor(detail).color;
933
+ }
892
934
  if (detail && typeof detail === "object") {
893
- if (typeof detail.value === "string" && detail.value) return detail.value;
894
- if (typeof detail.hex === "string" && detail.hex) return detail.hex;
895
935
  if (typeof detail.color === "string" && detail.color) return detail.color;
936
+ if (typeof detail.value === "string" && detail.value) {
937
+ return figLabParseSolidColor(detail.value).color;
938
+ }
939
+ if (typeof detail.hex === "string" && detail.hex) return detail.hex;
940
+ }
941
+ const live = this.#input?.value;
942
+ if (live && typeof live === "object" && typeof live.color === "string") {
943
+ return live.color;
896
944
  }
897
- // Prefer live JS value fig-input-color often leaves the attribute stale.
898
- if (typeof this.#input?.value === "string" && this.#input.value) {
899
- return this.#input.value;
945
+ if (typeof live === "string" && live) {
946
+ return figLabParseSolidColor(live).color;
900
947
  }
901
- return this.#input?.getAttribute("value") ?? "";
948
+ return this.#parsedHostColor().color;
949
+ }
950
+
951
+ #colorDetailFromEvent(event, color) {
952
+ const detail =
953
+ event instanceof CustomEvent && event.detail && typeof event.detail === "object"
954
+ ? event.detail
955
+ : undefined;
956
+ const alpha = Number.isFinite(detail?.alpha)
957
+ ? Math.max(0, Math.min(1, Number(detail.alpha)))
958
+ : this.#parsedHostColor().alpha;
959
+ return {
960
+ color,
961
+ alpha,
962
+ opacity: Math.round(alpha * 100),
963
+ };
902
964
  }
903
965
 
904
966
  #forwardInputEvent(type, event) {
905
967
  event.stopImmediatePropagation();
906
968
  if (figLabBooleanAttribute(this, "disabled")) return;
907
969
  const value = this.#valueFromColorEvent(event);
908
- this.setAttribute("value", value);
909
- if (this.#input && this.#input.getAttribute("value") !== value) {
910
- this.#input.setAttribute("value", value);
970
+ const detail = this.#colorDetailFromEvent(event, value);
971
+ const alphaHex =
972
+ detail.alpha < 1 - 1 / 255
973
+ ? Math.round(detail.alpha * 255)
974
+ .toString(16)
975
+ .padStart(2, "0")
976
+ : "";
977
+ const hostValue = `${detail.color.slice(0, 7)}${alphaHex}`;
978
+ this.setAttribute("value", hostValue);
979
+ const nextJson = this.#pickerValueFromHost();
980
+ if (this.#input && this.#input.getAttribute("value") !== nextJson) {
981
+ this.#input.setAttribute("value", nextJson);
982
+ }
983
+ if (this.#swatch) {
984
+ this.#swatch.setAttribute("background", detail.color);
985
+ this.#swatch.setAttribute("alpha", String(detail.alpha));
911
986
  }
912
- const detail =
913
- event instanceof CustomEvent && event.detail !== undefined
914
- ? event.detail
915
- : value;
916
987
  this.dispatchEvent(
917
988
  new CustomEvent(type, {
918
989
  detail,
@@ -927,7 +998,7 @@ class PropskitColor extends HTMLElement {
927
998
  if (figLabBooleanAttribute(this, "disabled")) return;
928
999
  if (
929
1000
  event.target instanceof Element &&
930
- event.target.closest("fig-input-color, fig-menu")
1001
+ event.target.closest("fig-fill-picker, fig-swatch, fig-menu")
931
1002
  ) {
932
1003
  return;
933
1004
  }
@@ -946,18 +1017,22 @@ class PropskitColor extends HTMLElement {
946
1017
  }
947
1018
  const next = String(nextValue);
948
1019
  this.setAttribute("value", next);
949
- if (this.#input && this.#input.getAttribute("value") !== next) {
950
- this.#input.setAttribute("value", next);
951
- }
1020
+ this.#forceInputValue(next);
952
1021
  }
953
1022
 
954
- /** Force fig-input-color UI refresh even when the hex string is unchanged. */
1023
+ /** Force fill-picker refresh even when the serialized value is unchanged. */
955
1024
  #forceInputValue(next) {
956
1025
  if (!this.#input) return;
957
- if (this.#input.getAttribute("value") === next) {
1026
+ const json = figLabSolidFillValue(next);
1027
+ if (this.#input.getAttribute("value") === json) {
958
1028
  this.#input.removeAttribute("value");
959
1029
  }
960
- this.#input.setAttribute("value", next);
1030
+ this.#input.setAttribute("value", json);
1031
+ if (this.#swatch) {
1032
+ const parsed = figLabParseSolidColor(next);
1033
+ this.#swatch.setAttribute("background", parsed.color);
1034
+ this.#swatch.setAttribute("alpha", String(parsed.alpha));
1035
+ }
961
1036
  }
962
1037
 
963
1038
  #defaultValue() {
@@ -995,7 +1070,8 @@ class PropskitColor extends HTMLElement {
995
1070
  }
996
1071
 
997
1072
  focus(options) {
998
- this.#input?.querySelector("input:not([tabindex='-1'])")?.focus(options);
1073
+ const swatch = this.querySelector("fig-swatch");
1074
+ if (swatch instanceof HTMLElement) swatch.focus(options);
999
1075
  }
1000
1076
  }
1001
1077
  figLabDefineElement("propskit-color", PropskitColor);
@@ -1158,7 +1234,7 @@ class PropskitGradient extends HTMLElement {
1158
1234
  this.#input.setAttribute(attrName, next);
1159
1235
  }
1160
1236
 
1161
- if (!this.hasAttribute("edit")) this.#input.setAttribute("edit", "true");
1237
+ if (!this.hasAttribute("edit")) this.#input.setAttribute("edit", "picker");
1162
1238
  this.#managedInputAttrs = nextManaged;
1163
1239
  }
1164
1240
 
package/fig.js CHANGED
@@ -7742,10 +7742,12 @@ class FigField extends HTMLElement {
7742
7742
  inputTag === "fig-menu" ||
7743
7743
  inputTag === "fig-dropdown" ||
7744
7744
  inputTag === "fig-tooltip" ||
7745
+ inputTag === "fig-fill-picker" ||
7745
7746
  (this.input instanceof HTMLDialogElement);
7746
7747
  this.#toggleable = !!(
7747
7748
  this.input &&
7748
7749
  "open" in this.input &&
7750
+ typeof this.input.open !== "function" &&
7749
7751
  !popupOpenHost
7750
7752
  );
7751
7753
 
@@ -11877,7 +11879,11 @@ class FigSwatch extends HTMLElement {
11877
11879
  }
11878
11880
 
11879
11881
  focus() {
11880
- this.input?.focus();
11882
+ if (this.input?.isConnected) {
11883
+ this.input.focus();
11884
+ return;
11885
+ }
11886
+ HTMLElement.prototype.focus.call(this);
11881
11887
  }
11882
11888
 
11883
11889
  attributeChangedCallback(name, oldValue, newValue) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.13",
3
+ "version": "8.9.15",
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",