@rogieking/figui3 6.28.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.28.1",
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",