@rogieking/figui3 6.28.1 → 6.29.1

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
@@ -234,35 +234,36 @@ fig-content:has(> propskit-group[name]:last-child) {
234
234
  padding-bottom: 0;
235
235
  }
236
236
 
237
- /* Default/large size: more left field padding; nudge trailing controls in from
237
+ /* Large size: more left field padding; nudge trailing controls in from
238
238
  the right. propskit-select applies left inset on label + trigger instead. */
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
245
- ):is(:not([size]), [size="large"]) {
246
+ )[size="large"] {
246
247
  fig-field:not([direction="vertical"]) {
247
248
  padding-left: var(--spacer-2-5);
248
249
  }
249
250
  }
250
251
 
251
252
  /* Flex-positioned numbers — margin works. Absolute numbers use right: calc(...) below. */
252
- :is(propskit-color, propskit-number):is(:not([size]), [size="large"]) {
253
+ :is(propskit-color, propskit-number)[size="large"] {
253
254
  fig-field fig-input-number {
254
255
  margin-right: var(--spacer-1);
255
256
  }
256
257
  }
257
258
 
258
- propskit-text:is(:not([size]), [size="large"]) {
259
+ propskit-text[size="large"] {
259
260
  fig-field > fig-input-text {
260
261
  margin-right: var(--spacer-1);
261
262
  }
262
263
  }
263
264
 
264
265
  propskit-switch {
265
- --propskit-switch-height: 2rem;
266
+ --propskit-switch-height: 1.5rem;
266
267
  --propskit-switch-vertical-padding: var(--spacer-1);
267
268
  --propskit-switch-horizontal-padding: var(--spacer-3);
268
269
 
@@ -274,8 +275,9 @@ propskit-switch {
274
275
  padding: var(--propskit-switch-vertical-padding)
275
276
  var(--propskit-switch-horizontal-padding);
276
277
 
277
- &:not([size]),
278
278
  &[size="large"] {
279
+ --propskit-switch-height: 2rem;
280
+
279
281
  fig-field:not([direction="vertical"]) {
280
282
  padding-right: var(--spacer-1);
281
283
  }
@@ -338,7 +340,7 @@ propskit-switch {
338
340
  }
339
341
 
340
342
  propskit-color {
341
- --propskit-color-height: 2rem;
343
+ --propskit-color-height: 1.5rem;
342
344
  --propskit-color-vertical-padding: var(--spacer-1);
343
345
  --propskit-color-horizontal-padding: var(--spacer-3);
344
346
  --figma-focus-outline-offset: 1px;
@@ -351,6 +353,10 @@ propskit-color {
351
353
  padding: var(--propskit-color-vertical-padding)
352
354
  var(--propskit-color-horizontal-padding);
353
355
 
356
+ &[size="large"] {
357
+ --propskit-color-height: 2rem;
358
+ }
359
+
354
360
  &[disabled]:not([disabled="false"]) {
355
361
  pointer-events: none;
356
362
 
@@ -456,8 +462,96 @@ propskit-color {
456
462
  }
457
463
  }
458
464
 
465
+ propskit-gradient {
466
+ --propskit-gradient-height: 1.5rem;
467
+ --propskit-gradient-vertical-padding: var(--spacer-1);
468
+ --propskit-gradient-horizontal-padding: var(--spacer-3);
469
+ --figma-focus-outline-offset: 1px;
470
+
471
+ display: block;
472
+ width: 100%;
473
+ height: calc(
474
+ var(--propskit-gradient-height) + var(--propskit-gradient-vertical-padding) * 2
475
+ );
476
+ padding: var(--propskit-gradient-vertical-padding)
477
+ var(--propskit-gradient-horizontal-padding);
478
+
479
+ &[size="large"] {
480
+ --propskit-gradient-height: 2rem;
481
+
482
+ fig-field:not([direction="vertical"]) {
483
+ padding-right: var(--spacer-1);
484
+ }
485
+ }
486
+
487
+ &[disabled]:not([disabled="false"]) {
488
+ pointer-events: none;
489
+
490
+ label {
491
+ color: var(--figma-color-text-disabled);
492
+ }
493
+ }
494
+
495
+ fig-field:not([direction="vertical"]) {
496
+ position: relative;
497
+ display: flex;
498
+ align-items: center;
499
+ gap: var(--spacer-2);
500
+ height: 100%;
501
+ padding: 0 var(--spacer-1) 0 var(--spacer-2);
502
+ border-radius: var(--radius-medium);
503
+ background-color: var(--figma-color-bg-secondary);
504
+
505
+ &::after {
506
+ content: "";
507
+ position: absolute;
508
+ inset: 0;
509
+ border-radius: inherit;
510
+ pointer-events: none;
511
+ }
512
+
513
+ &:hover::after {
514
+ box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
515
+ }
516
+
517
+ & > label {
518
+ position: relative;
519
+ z-index: 1;
520
+ display: flex;
521
+ align-items: center;
522
+ flex: 0 1 auto;
523
+ max-width: 40%;
524
+ min-width: 0;
525
+ margin: 0;
526
+ padding: 0;
527
+ overflow: hidden;
528
+ color: var(--figma-color-text-secondary);
529
+ text-overflow: ellipsis;
530
+ white-space: nowrap;
531
+ pointer-events: none;
532
+
533
+ fig-icon:first-child {
534
+ margin-left: calc(var(--spacer-1) * -1);
535
+ }
536
+ }
537
+
538
+ fig-input-gradient {
539
+ position: relative;
540
+ flex: 0 0 50%;
541
+ width: 50%;
542
+ min-width: 0;
543
+ margin-left: auto;
544
+ border-radius: var(--radius-medium);
545
+
546
+ &:hover {
547
+ outline: none !important;
548
+ }
549
+ }
550
+ }
551
+ }
552
+
459
553
  propskit-select {
460
- --propskit-select-height: 2rem;
554
+ --propskit-select-height: 1.5rem;
461
555
  --propskit-select-vertical-padding: var(--spacer-1);
462
556
  --propskit-select-horizontal-padding: var(--spacer-3);
463
557
  --figma-focus-outline-offset: 1px;
@@ -470,8 +564,9 @@ propskit-select {
470
564
  padding: var(--propskit-select-vertical-padding)
471
565
  var(--propskit-select-horizontal-padding);
472
566
 
473
- &:not([size]),
474
567
  &[size="large"] {
568
+ --propskit-select-height: 2rem;
569
+
475
570
  fig-field:not([direction="vertical"]) {
476
571
  padding-left: 0;
477
572
 
@@ -567,7 +662,7 @@ propskit-select {
567
662
  }
568
663
 
569
664
  propskit-text {
570
- --propskit-text-height: 2rem;
665
+ --propskit-text-height: 1.5rem;
571
666
  --propskit-text-vertical-padding: var(--spacer-1);
572
667
  --propskit-text-horizontal-padding: var(--spacer-3);
573
668
  --figma-focus-outline-offset: 1px;
@@ -580,6 +675,10 @@ propskit-text {
580
675
  padding: var(--propskit-text-vertical-padding)
581
676
  var(--propskit-text-horizontal-padding);
582
677
 
678
+ &[size="large"] {
679
+ --propskit-text-height: 2rem;
680
+ }
681
+
583
682
  &[disabled]:not([disabled="false"]) {
584
683
  pointer-events: none;
585
684
 
@@ -672,7 +771,7 @@ propskit-text {
672
771
  }
673
772
 
674
773
  propskit-number {
675
- --propskit-number-height: 2rem;
774
+ --propskit-number-height: 1.5rem;
676
775
  --propskit-number-vertical-padding: var(--spacer-1);
677
776
  --propskit-number-horizontal-padding: var(--spacer-3);
678
777
  --figma-focus-outline-offset: 1px;
@@ -685,8 +784,9 @@ propskit-number {
685
784
  padding: var(--propskit-number-vertical-padding)
686
785
  var(--propskit-number-horizontal-padding);
687
786
 
688
- &:not([size]),
689
787
  &[size="large"] {
788
+ --propskit-number-height: 2rem;
789
+
690
790
  fig-field:not([direction="vertical"]) > label {
691
791
  inset: 0 auto 0 var(--spacer-2-5);
692
792
  }
@@ -786,7 +886,7 @@ propskit-slider {
786
886
  --propskit-slider-elastic-scale: 1;
787
887
  --propskit-slider-elastic-origin: left center;
788
888
  --propskit-slider-elastic-transition: 0.18s cubic-bezier(0.2, 1.45, 0.35, 1);
789
- --propskit-slider-height: 2rem;
889
+ --propskit-slider-height: 1.5rem;
790
890
  --propskit-slider-vertical-padding: var(--spacer-1);
791
891
  --propskit-slider-horizontal-padding: var(--spacer-3);
792
892
  --figma-focus-outline-offset: 1px;
@@ -811,8 +911,8 @@ propskit-slider {
811
911
  padding-left: var(--propskit-slider-horizontal-padding);
812
912
  padding-right: var(--propskit-slider-horizontal-padding);
813
913
 
814
- &:not([size]),
815
914
  &[size="large"] {
915
+ --propskit-slider-height: 2rem;
816
916
  --propskit-slider-fig-slider-thumb-height: calc(
817
917
  var(--propskit-slider-fig-slider-height) - var(--spacer-3)
818
918
  );
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,266 @@ 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
+ this.#managedInputAttrs = nextManaged;
904
+ }
905
+
906
+ #bindInputEvents() {
907
+ if (!this.#input) return;
908
+ this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
909
+ this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
910
+ this.#input.addEventListener("input", this.#boundHandleInput);
911
+ this.#input.addEventListener("change", this.#boundHandleChange);
912
+ }
913
+
914
+ #unbindInputEvents() {
915
+ if (!this.#input) return;
916
+ if (this.#boundHandleInput) {
917
+ this.#input.removeEventListener("input", this.#boundHandleInput);
918
+ }
919
+ if (this.#boundHandleChange) {
920
+ this.#input.removeEventListener("change", this.#boundHandleChange);
921
+ }
922
+ }
923
+
924
+ #valueFromGradientEvent(event) {
925
+ const detail =
926
+ event instanceof CustomEvent && event.detail && typeof event.detail === "object"
927
+ ? event.detail
928
+ : this.#input?.value;
929
+ if (!detail || typeof detail !== "object") return this.value;
930
+ return JSON.stringify(detail);
931
+ }
932
+
933
+ #forwardInputEvent(type, event) {
934
+ event.stopImmediatePropagation();
935
+ const value = this.#valueFromGradientEvent(event);
936
+ this.setAttribute("value", value);
937
+ const detail =
938
+ event instanceof CustomEvent && event.detail !== undefined
939
+ ? event.detail
940
+ : this.#input?.value;
941
+ this.dispatchEvent(
942
+ new CustomEvent(type, {
943
+ detail,
944
+ bubbles: true,
945
+ cancelable: true,
946
+ composed: true,
947
+ }),
948
+ );
949
+ }
950
+
951
+ #handleClick(event) {
952
+ if (
953
+ event.target instanceof Element &&
954
+ event.target.closest("fig-input-gradient, fig-menu")
955
+ ) {
956
+ return;
957
+ }
958
+ this.focus();
959
+ }
960
+
961
+ get value() {
962
+ return (
963
+ this.getAttribute("value") ??
964
+ JSON.stringify(this.#input?.value ?? {})
965
+ );
966
+ }
967
+
968
+ set value(nextValue) {
969
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
970
+ this.removeAttribute("value");
971
+ this.#input?.removeAttribute("value");
972
+ return;
973
+ }
974
+ const next =
975
+ typeof nextValue === "object" ? JSON.stringify(nextValue) : String(nextValue);
976
+ this.setAttribute("value", next);
977
+ if (this.#input && this.#input.getAttribute("value") !== next) {
978
+ this.#input.setAttribute("value", next);
979
+ }
980
+ }
981
+
982
+ get defaultValue() {
983
+ return this.getAttribute("default") ?? this.#initialValue ?? "";
984
+ }
985
+
986
+ get isDefault() {
987
+ return figLabPropskitJsonValuesEqual(this.value, this.defaultValue);
988
+ }
989
+
990
+ resetToDefault() {
991
+ const next = this.defaultValue;
992
+ if (!next) return;
993
+ this.value = next;
994
+ const detail = this.#input?.value;
995
+ figLabEmitPropskitReset(this, detail);
996
+ }
997
+
998
+ focus(options) {
999
+ this.#input?.focus(options);
1000
+ }
1001
+ }
1002
+ figLabDefineElement("propskit-gradient", PropskitGradient);
1003
+
717
1004
  /* Field + Select wrapper */
718
1005
  class PropskitSelect extends HTMLElement {
719
1006
  #field = null;
@@ -1472,6 +1759,7 @@ class PropskitGroup extends HTMLElement {
1472
1759
 
1473
1760
  static #CONTROL_SELECTOR = [
1474
1761
  "propskit-color",
1762
+ "propskit-gradient",
1475
1763
  "propskit-number",
1476
1764
  "propskit-select",
1477
1765
  "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.1",
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",