@rogieking/figui3 6.28.0 → 6.29.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.
package/fig-lab.css CHANGED
@@ -239,6 +239,7 @@ fig-content:has(> propskit-group[name]:last-child) {
239
239
  :is(
240
240
  propskit-switch,
241
241
  propskit-color,
242
+ propskit-gradient,
242
243
  propskit-text,
243
244
  propskit-number,
244
245
  propskit-slider
@@ -456,6 +457,86 @@ propskit-color {
456
457
  }
457
458
  }
458
459
 
460
+ propskit-gradient {
461
+ --propskit-gradient-height: 2rem;
462
+ --propskit-gradient-vertical-padding: var(--spacer-1);
463
+ --propskit-gradient-horizontal-padding: var(--spacer-3);
464
+ --figma-focus-outline-offset: 1px;
465
+
466
+ display: block;
467
+ width: 100%;
468
+ height: calc(
469
+ var(--propskit-gradient-height) + var(--propskit-gradient-vertical-padding) * 2
470
+ );
471
+ padding: var(--propskit-gradient-vertical-padding)
472
+ var(--propskit-gradient-horizontal-padding);
473
+
474
+ &[disabled]:not([disabled="false"]) {
475
+ pointer-events: none;
476
+
477
+ label {
478
+ color: var(--figma-color-text-disabled);
479
+ }
480
+ }
481
+
482
+ fig-field:not([direction="vertical"]) {
483
+ position: relative;
484
+ display: flex;
485
+ align-items: center;
486
+ gap: var(--spacer-2);
487
+ height: 100%;
488
+ padding: 0 var(--spacer-1) 0 var(--spacer-2);
489
+ border-radius: var(--radius-medium);
490
+ background-color: var(--figma-color-bg-secondary);
491
+
492
+ &::after {
493
+ content: "";
494
+ position: absolute;
495
+ inset: 0;
496
+ border-radius: inherit;
497
+ pointer-events: none;
498
+ }
499
+
500
+ &:hover::after {
501
+ box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
502
+ }
503
+
504
+ & > label {
505
+ position: relative;
506
+ z-index: 1;
507
+ display: flex;
508
+ align-items: center;
509
+ flex: 0 1 auto;
510
+ max-width: 40%;
511
+ min-width: 0;
512
+ margin: 0;
513
+ padding: 0;
514
+ overflow: hidden;
515
+ color: var(--figma-color-text-secondary);
516
+ text-overflow: ellipsis;
517
+ white-space: nowrap;
518
+ pointer-events: none;
519
+
520
+ fig-icon:first-child {
521
+ margin-left: calc(var(--spacer-1) * -1);
522
+ }
523
+ }
524
+
525
+ fig-input-gradient {
526
+ position: relative;
527
+ flex: 0 0 50%;
528
+ width: 50%;
529
+ min-width: 0;
530
+ margin-left: auto;
531
+ border-radius: var(--radius-medium);
532
+
533
+ &:hover {
534
+ outline: none !important;
535
+ }
536
+ }
537
+ }
538
+ }
539
+
459
540
  propskit-select {
460
541
  --propskit-select-height: 2rem;
461
542
  --propskit-select-vertical-padding: var(--spacer-1);
package/fig-lab.js CHANGED
@@ -130,6 +130,33 @@ function figLabPropskitValuesEqual(value, defaultValue) {
130
130
  return String(value ?? "") === String(defaultValue ?? "");
131
131
  }
132
132
 
133
+ function figLabPropskitJsonValuesEqual(value, defaultValue) {
134
+ const normalize = (input) => {
135
+ if (Array.isArray(input)) return input.map(normalize);
136
+ if (input && typeof input === "object") {
137
+ return Object.keys(input)
138
+ .sort()
139
+ .reduce((result, key) => {
140
+ result[key] = normalize(input[key]);
141
+ return result;
142
+ }, {});
143
+ }
144
+ return input;
145
+ };
146
+
147
+ try {
148
+ const parsedValue = typeof value === "string" ? JSON.parse(value) : value;
149
+ const parsedDefault =
150
+ typeof defaultValue === "string" ? JSON.parse(defaultValue) : defaultValue;
151
+ return (
152
+ JSON.stringify(normalize(parsedValue)) ===
153
+ JSON.stringify(normalize(parsedDefault))
154
+ );
155
+ } catch {
156
+ return figLabPropskitValuesEqual(value, defaultValue);
157
+ }
158
+ }
159
+
133
160
  function figLabPerceivedColorTheme(context, color) {
134
161
  if (!context || !color) return null;
135
162
  const probe = document.createElement("span");
@@ -714,6 +741,269 @@ class PropskitColor extends HTMLElement {
714
741
  }
715
742
  figLabDefineElement("propskit-color", PropskitColor);
716
743
 
744
+ /* Field + Gradient wrapper */
745
+ class PropskitGradient extends HTMLElement {
746
+ #field = null;
747
+ #label = null;
748
+ #input = null;
749
+ #hasCustomLabel = false;
750
+ #observer = null;
751
+ #managedInputAttrs = new Set();
752
+ #boundHandleInput = null;
753
+ #boundHandleChange = null;
754
+ #boundHandleClick = this.#handleClick.bind(this);
755
+ #initialValue = null;
756
+
757
+ static get observedAttributes() {
758
+ return ["label", "direction", "aria-label"];
759
+ }
760
+
761
+ connectedCallback() {
762
+ if (!this.#field) this.#initialize();
763
+ this.#syncField();
764
+ this.#syncInputAttributes();
765
+ this.#bindInputEvents();
766
+ if (this.#initialValue === null) {
767
+ this.#initialValue =
768
+ this.getAttribute("default") ??
769
+ this.getAttribute("value") ??
770
+ JSON.stringify(this.#input?.value ?? {});
771
+ }
772
+ this.removeEventListener("click", this.#boundHandleClick);
773
+ this.addEventListener("click", this.#boundHandleClick);
774
+ figLabConnectPropskitResetMenu(this);
775
+
776
+ if (!this.#observer) {
777
+ this.#observer = new MutationObserver((mutations) => {
778
+ let syncField = false;
779
+ let syncInput = false;
780
+
781
+ for (const mutation of mutations) {
782
+ if (mutation.type !== "attributes") continue;
783
+ if (
784
+ mutation.attributeName === "label" ||
785
+ mutation.attributeName === "direction" ||
786
+ mutation.attributeName === "aria-label"
787
+ ) {
788
+ syncField = true;
789
+ } else {
790
+ syncInput = true;
791
+ }
792
+ }
793
+
794
+ if (syncField) this.#syncField();
795
+ if (syncInput) this.#syncInputAttributes();
796
+ });
797
+ }
798
+
799
+ this.#observer.observe(this, { attributes: true });
800
+ }
801
+
802
+ disconnectedCallback() {
803
+ this.#observer?.disconnect();
804
+ this.#unbindInputEvents();
805
+ this.removeEventListener("click", this.#boundHandleClick);
806
+ figLabDisconnectPropskitResetMenu(this);
807
+ }
808
+
809
+ attributeChangedCallback(name, oldValue, newValue) {
810
+ if (oldValue === newValue || !this.#field) return;
811
+ if (name === "label" || name === "direction" || name === "aria-label") {
812
+ this.#syncField();
813
+ }
814
+ }
815
+
816
+ #initialize() {
817
+ const initialChildren = Array.from(this.childNodes).filter(
818
+ (node) =>
819
+ node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
820
+ );
821
+ const customLabel = initialChildren.find(
822
+ (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
823
+ );
824
+ const field = document.createElement("fig-field");
825
+ const label = customLabel || document.createElement("label");
826
+ const input = document.createElement("fig-input-gradient");
827
+
828
+ field.append(label, input);
829
+ this.#field = field;
830
+ this.#label = label;
831
+ this.#input = input;
832
+ this.#hasCustomLabel = Boolean(customLabel);
833
+ this.replaceChildren(field);
834
+ }
835
+
836
+ #syncField() {
837
+ if (!this.#field || !this.#label || !this.#input) return;
838
+ const hasLabelAttr = this.hasAttribute("label");
839
+ const rawLabel = this.getAttribute("label");
840
+ const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
841
+
842
+ if (isBlankLabel) {
843
+ this.#label.remove();
844
+ } else {
845
+ if (!this.#hasCustomLabel) {
846
+ this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
847
+ }
848
+ if (this.#label.parentElement !== this.#field) {
849
+ this.#field.prepend(this.#label);
850
+ }
851
+ }
852
+
853
+ this.#field.setAttribute(
854
+ "direction",
855
+ this.getAttribute("direction") || "horizontal",
856
+ );
857
+ this.#input.setAttribute(
858
+ "aria-label",
859
+ this.getAttribute("aria-label") ||
860
+ this.#label.textContent?.trim() ||
861
+ "Gradient",
862
+ );
863
+ }
864
+
865
+ #getForwardedInputAttrNames() {
866
+ const reserved = new Set([
867
+ "label",
868
+ "direction",
869
+ "oninput",
870
+ "onchange",
871
+ "class",
872
+ "style",
873
+ "id",
874
+ "size",
875
+ "aria-label",
876
+ "default",
877
+ ]);
878
+ return this.getAttributeNames().filter(
879
+ (name) => !reserved.has(name) && !name.startsWith("data-"),
880
+ );
881
+ }
882
+
883
+ #syncInputAttributes() {
884
+ if (!this.#input) return;
885
+ const inputAttrs = this.#getForwardedInputAttrNames();
886
+ const nextManaged = new Set(inputAttrs);
887
+
888
+ for (const attrName of this.#managedInputAttrs) {
889
+ if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
890
+ }
891
+ for (const attrName of inputAttrs) {
892
+ const next = this.getAttribute(attrName) ?? "";
893
+ if (
894
+ attrName === "value" &&
895
+ this.#input.getAttribute("value") === next
896
+ ) {
897
+ continue;
898
+ }
899
+ this.#input.setAttribute(attrName, next);
900
+ }
901
+
902
+ if (!this.hasAttribute("edit")) this.#input.setAttribute("edit", "true");
903
+ const size = this.getAttribute("size");
904
+ if (!size || size === "large") this.#input.setAttribute("size", "large");
905
+ else this.#input.removeAttribute("size");
906
+ this.#managedInputAttrs = nextManaged;
907
+ }
908
+
909
+ #bindInputEvents() {
910
+ if (!this.#input) return;
911
+ this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
912
+ this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
913
+ this.#input.addEventListener("input", this.#boundHandleInput);
914
+ this.#input.addEventListener("change", this.#boundHandleChange);
915
+ }
916
+
917
+ #unbindInputEvents() {
918
+ if (!this.#input) return;
919
+ if (this.#boundHandleInput) {
920
+ this.#input.removeEventListener("input", this.#boundHandleInput);
921
+ }
922
+ if (this.#boundHandleChange) {
923
+ this.#input.removeEventListener("change", this.#boundHandleChange);
924
+ }
925
+ }
926
+
927
+ #valueFromGradientEvent(event) {
928
+ const detail =
929
+ event instanceof CustomEvent && event.detail && typeof event.detail === "object"
930
+ ? event.detail
931
+ : this.#input?.value;
932
+ if (!detail || typeof detail !== "object") return this.value;
933
+ return JSON.stringify(detail);
934
+ }
935
+
936
+ #forwardInputEvent(type, event) {
937
+ event.stopImmediatePropagation();
938
+ const value = this.#valueFromGradientEvent(event);
939
+ this.setAttribute("value", value);
940
+ const detail =
941
+ event instanceof CustomEvent && event.detail !== undefined
942
+ ? event.detail
943
+ : this.#input?.value;
944
+ this.dispatchEvent(
945
+ new CustomEvent(type, {
946
+ detail,
947
+ bubbles: true,
948
+ cancelable: true,
949
+ composed: true,
950
+ }),
951
+ );
952
+ }
953
+
954
+ #handleClick(event) {
955
+ if (
956
+ event.target instanceof Element &&
957
+ event.target.closest("fig-input-gradient, fig-menu")
958
+ ) {
959
+ return;
960
+ }
961
+ this.focus();
962
+ }
963
+
964
+ get value() {
965
+ return (
966
+ this.getAttribute("value") ??
967
+ JSON.stringify(this.#input?.value ?? {})
968
+ );
969
+ }
970
+
971
+ set value(nextValue) {
972
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
973
+ this.removeAttribute("value");
974
+ this.#input?.removeAttribute("value");
975
+ return;
976
+ }
977
+ const next =
978
+ typeof nextValue === "object" ? JSON.stringify(nextValue) : String(nextValue);
979
+ this.setAttribute("value", next);
980
+ if (this.#input && this.#input.getAttribute("value") !== next) {
981
+ this.#input.setAttribute("value", next);
982
+ }
983
+ }
984
+
985
+ get defaultValue() {
986
+ return this.getAttribute("default") ?? this.#initialValue ?? "";
987
+ }
988
+
989
+ get isDefault() {
990
+ return figLabPropskitJsonValuesEqual(this.value, this.defaultValue);
991
+ }
992
+
993
+ resetToDefault() {
994
+ const next = this.defaultValue;
995
+ if (!next) return;
996
+ this.value = next;
997
+ const detail = this.#input?.value;
998
+ figLabEmitPropskitReset(this, detail);
999
+ }
1000
+
1001
+ focus(options) {
1002
+ this.#input?.focus(options);
1003
+ }
1004
+ }
1005
+ figLabDefineElement("propskit-gradient", PropskitGradient);
1006
+
717
1007
  /* Field + Select wrapper */
718
1008
  class PropskitSelect extends HTMLElement {
719
1009
  #field = null;
@@ -1472,6 +1762,7 @@ class PropskitGroup extends HTMLElement {
1472
1762
 
1473
1763
  static #CONTROL_SELECTOR = [
1474
1764
  "propskit-color",
1765
+ "propskit-gradient",
1475
1766
  "propskit-number",
1476
1767
  "propskit-select",
1477
1768
  "propskit-slider",
package/fig.js CHANGED
@@ -10987,6 +10987,7 @@ class FigInputGradient extends HTMLElement {
10987
10987
  #arrowTooltipTimer = null;
10988
10988
  #colorObserver = null;
10989
10989
  #repositionRAF = null;
10990
+ #pickerDefinitionPromise = null;
10990
10991
  #gradient = {
10991
10992
  type: "linear",
10992
10993
  angle: 90,
@@ -11060,11 +11061,22 @@ class FigInputGradient extends HTMLElement {
11060
11061
  connectedCallback() {
11061
11062
  this.#parseValue();
11062
11063
  this.#render();
11064
+ this.#renderPickerWhenAvailable();
11063
11065
  this.removeEventListener("keydown", this.#onPickerKeyDown);
11064
11066
  this.addEventListener("keydown", this.#onPickerKeyDown);
11065
11067
  if (this.#isEditable) document.addEventListener("keydown", this.#onKeyDown);
11066
11068
  }
11067
11069
 
11070
+ #renderPickerWhenAvailable() {
11071
+ if (this.#editMode !== "picker" || hasFigFillPicker()) return;
11072
+ this.#pickerDefinitionPromise ??= customElements
11073
+ .whenDefined("fig-fill-picker")
11074
+ .then(() => {
11075
+ if (!this.isConnected || this.#editMode !== "picker") return;
11076
+ this.#render();
11077
+ });
11078
+ }
11079
+
11068
11080
  disconnectedCallback() {
11069
11081
  document.removeEventListener("keydown", this.#onKeyDown);
11070
11082
  this.removeEventListener("keydown", this.#onPickerKeyDown);
@@ -11851,6 +11863,7 @@ class FigInputGradient extends HTMLElement {
11851
11863
  break;
11852
11864
  case "edit":
11853
11865
  this.#render();
11866
+ this.#renderPickerWhenAvailable();
11854
11867
  if (this.#isEditable) {
11855
11868
  document.addEventListener("keydown", this.#onKeyDown);
11856
11869
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.28.0",
3
+ "version": "6.29.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",