@rogieking/figui3 3.14.0 → 3.15.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 +150 -117
  2. package/fig.js +397 -23
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -1175,18 +1175,24 @@ input[type="color"] {
1175
1175
  }
1176
1176
 
1177
1177
  fig-chit {
1178
- contain: layout paint;
1179
1178
  --size: 1.5rem;
1179
+ --padding: 5px;
1180
+ --width: var(--size);
1181
+ --height: var(--size);
1182
+ --border-radius: var(--radius-medium);
1180
1183
  --chit-background: #d9d9d9;
1181
1184
  --chit-bg-size: cover;
1182
1185
  --chit-bg-position: center;
1186
+ --selected: 0;
1183
1187
  --alpha: 1;
1184
1188
 
1185
1189
  display: inline-grid;
1186
1190
  width: var(--size);
1187
1191
  height: var(--size);
1188
- border-radius: var(--radius-medium);
1189
- cursor: default;
1192
+ border-radius: var(--border-radius);
1193
+ background-color: var(--figma-color-bg-secondary);
1194
+ box-shadow: inset 0 0 0 calc(var(--selected) * 1px)
1195
+ var(--figma-color-border-selected);
1190
1196
 
1191
1197
  &::before,
1192
1198
  &::after,
@@ -1195,130 +1201,94 @@ fig-chit {
1195
1201
  }
1196
1202
 
1197
1203
  /* Selection ring */
1198
- &[selected]:not([selected="false"])::before {
1199
- content: "";
1200
- width: var(--size);
1201
- height: var(--size);
1202
- z-index: 1;
1203
- border-radius: var(--radius-medium);
1204
- box-shadow: inset 0 0 0 1px var(--figma-color-border-selected);
1204
+ &[selected]:not([selected="false"]) {
1205
+ --selected: 1;
1206
+ background-color: transparent;
1205
1207
  }
1206
1208
 
1207
1209
  &[size="medium"],
1208
1210
  &[size="large"] {
1209
- input[type="color"] {
1210
- padding: 0;
1211
- width: var(--size);
1212
- height: var(--size);
1213
- border-radius: var(--radius-medium);
1214
- }
1215
-
1216
- input[type="color"]::-webkit-color-swatch-wrapper {
1217
- border-radius: var(--radius-medium);
1218
- }
1219
-
1220
- input[type="color"]::-webkit-color-swatch {
1221
- border-radius: var(--radius-medium);
1222
- }
1223
-
1224
- input[type="color"]::-moz-color-swatch {
1225
- border-radius: var(--radius-medium);
1226
- }
1227
-
1228
- &[selected]:not([selected="false"])::before {
1229
- box-shadow:
1230
- inset 0 0 0 1px var(--figma-color-border-selected),
1231
- inset 0 0 0 3px var(--figma-color-bg);
1232
- }
1233
-
1234
- &[data-type="gradient"]::after,
1235
- &[data-type="image"]::after,
1236
- &[data-type="gradient"]::before,
1237
- &[data-type="image"]::before {
1238
- width: var(--size);
1239
- height: var(--size);
1240
- border-radius: var(--radius-medium);
1211
+ --padding: 0px;
1212
+ &[selected]:not([selected="false"]) {
1213
+ --padding: 3px;
1241
1214
  }
1242
1215
  }
1243
1216
 
1244
1217
  &[size="medium"] {
1245
1218
  --size: 1.5rem;
1219
+ --padding: 0px;
1246
1220
  }
1247
1221
 
1248
1222
  &[size="large"] {
1249
1223
  --size: 2rem;
1224
+ --padding: 0px;
1250
1225
  }
1251
1226
 
1252
1227
  &[disabled] {
1253
1228
  pointer-events: none;
1254
1229
  }
1255
1230
 
1231
+ &::before,
1232
+ &::after {
1233
+ content: "";
1234
+ width: var(--width);
1235
+ height: var(--height);
1236
+ border-radius: calc(var(--border-radius) - (var(--padding) / 2));
1237
+ width: calc(var(--width) - var(--padding) * 2);
1238
+ height: calc(var(--height) - var(--padding) * 2);
1239
+ grid-area: 1/1;
1240
+ place-self: center;
1241
+ pointer-events: none;
1242
+ }
1243
+ &::after {
1244
+ box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
1245
+ z-index: 1;
1246
+ }
1256
1247
  &::before {
1257
1248
  content: "";
1258
- width: 0.875rem;
1259
- height: 0.875rem;
1260
- border-radius: 0.125rem;
1249
+ background: var(--chit-background);
1250
+ background-size: var(--chit-bg-size);
1251
+ background-position: var(--chit-bg-position);
1252
+ background-repeat: no-repeat;
1253
+ border-radius: calc(var(--border-radius) - (var(--padding) / 2));
1254
+ mask-image: linear-gradient(
1255
+ to right,
1256
+ black 0%,
1257
+ black 50%,
1258
+ rgba(0, 0, 0, var(--alpha)) 50%
1259
+ );
1261
1260
  grid-area: 1/1;
1262
1261
  place-self: center;
1263
1262
  }
1263
+ &[size="medium"]::before,
1264
1264
  &[size="large"]::before {
1265
- width: var(--size);
1266
- height: var(--size);
1267
- border-radius: var(--radius-medium);
1265
+ border-radius: calc(var(--border-radius) - var(--padding));
1266
+ }
1267
+ input[type="color"] {
1268
+ background: none;
1269
+ width: var(--width);
1270
+ height: var(--height);
1271
+ background: none;
1272
+ grid-area: 1/1;
1273
+ place-self: center;
1274
+ box-sizing: border-box;
1275
+ opacity: 0;
1268
1276
  }
1269
1277
 
1270
1278
  /* Gradient/Image types - inset thumbnail with hidden input */
1271
1279
  &[data-type="gradient"],
1272
1280
  &[data-type="image"] {
1273
1281
  background-color: var(--figma-color-bg-secondary);
1274
-
1275
- &::after {
1276
- content: "";
1277
- width: 0.875rem;
1278
- height: 0.875rem;
1279
-
1280
- background: var(--chit-background);
1281
- background-size: var(--chit-bg-size);
1282
- background-position: var(--chit-bg-position);
1283
- background-repeat: no-repeat;
1284
- border-radius: 0.125rem;
1285
- box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
1286
- mask-image: linear-gradient(
1287
- to right,
1288
- black 0%,
1289
- black 50%,
1290
- rgba(0, 0, 0, var(--alpha)) 50%
1291
- );
1292
- }
1293
-
1294
- &::after,
1295
- input {
1296
- grid-area: 1/1;
1297
- place-self: center;
1298
- }
1299
-
1300
1282
  input[type="color"] {
1301
- opacity: 0;
1302
- }
1303
-
1304
- input[type="color"]::-webkit-color-swatch-wrapper {
1305
- background: none;
1306
- }
1307
-
1308
- input[type="color"]::-moz-color-swatch {
1309
- opacity: 0;
1310
- }
1311
-
1312
- input[type="color"]::-webkit-color-swatch {
1313
- opacity: 0;
1283
+ display: none;
1314
1284
  }
1315
1285
  }
1316
1286
 
1317
1287
  /* Checkerboard for empty/missing images */
1318
- &[data-type="image"]:not([background])::before,
1319
- &[data-type="image"][background=""]::before,
1320
- &[data-type="image"][background="url()"]::before,
1321
- &[checkerboard]:not([checkerboard="false"])::before {
1288
+ &[data-type="image"]:not([background])::after,
1289
+ &[data-type="image"][background=""]::after,
1290
+ &[data-type="image"][background="url()"]::after,
1291
+ &[checkerboard]:not([checkerboard="false"])::after {
1322
1292
  background: var(--checkerboard);
1323
1293
  }
1324
1294
  }
@@ -1338,8 +1308,11 @@ fig-image {
1338
1308
  > * {
1339
1309
  grid-area: 1/1;
1340
1310
  }
1341
- fig-chit {
1311
+ fig-chit[size="large"] {
1342
1312
  --size: 100% !important;
1313
+ --width: 100% !important;
1314
+ --height: 100% !important;
1315
+ --padding: 0px;
1343
1316
  --chit-bg-size: var(--fit);
1344
1317
  &[disabled] {
1345
1318
  opacity: 1;
@@ -2900,7 +2873,8 @@ fig-checkbox,
2900
2873
  fig-radio,
2901
2874
  fig-tab,
2902
2875
  fig-tabs,
2903
- fig-segmented-control {
2876
+ fig-segmented-control,
2877
+ fig-input-palette {
2904
2878
  display: inline-flex;
2905
2879
  gap: var(--spacer-2);
2906
2880
  user-select: none;
@@ -3136,6 +3110,7 @@ fig-input-fill {
3136
3110
  background-color: transparent !important;
3137
3111
  box-shadow: none !important;
3138
3112
  border: 0 !important;
3113
+ --selected: 0;
3139
3114
  }
3140
3115
  }
3141
3116
  fig-chit ~ fig-input-text > input,
@@ -3177,21 +3152,12 @@ fig-input-gradient {
3177
3152
  outline: 1px solid var(--figma-color-border-selected) !important;
3178
3153
  outline-offset: -1px !important;
3179
3154
  }
3180
- fig-chit {
3155
+ & > fig-chit {
3156
+ --padding: 0;
3157
+ --width: 100%;
3181
3158
  flex: 1 1 auto;
3182
3159
  width: 100% !important;
3183
3160
  min-width: 0 !important;
3184
- &::before,
3185
- &::after {
3186
- width: 100% !important;
3187
- place-self: stretch;
3188
- }
3189
- &[data-type="gradient"]::after {
3190
- width: calc(100% - 0.625rem);
3191
- height: 0.875rem;
3192
- justify-self: center;
3193
- align-self: center;
3194
- }
3195
3161
  }
3196
3162
 
3197
3163
  .fig-input-gradient-track {
@@ -3205,19 +3171,90 @@ fig-input-gradient {
3205
3171
  pointer-events: auto;
3206
3172
  cursor: default;
3207
3173
 
3208
- &.dragging {
3209
- fig-color-tip {
3210
- display: none;
3174
+ &:hover {
3175
+ z-index: 5;
3176
+ }
3177
+ }
3178
+ }
3179
+ }
3180
+
3181
+ fig-input-palette {
3182
+ display: inline-grid !important;
3183
+ grid-template-columns: 1fr 24px;
3184
+ grid-template-areas: "inputs button";
3185
+ gap: var(--spacer-1);
3186
+ width: 100%;
3187
+
3188
+ .palette-colors {
3189
+ display: flex;
3190
+ flex-wrap: nowrap;
3191
+ gap: 0;
3192
+ border-radius: var(--radius-medium);
3193
+ overflow: hidden;
3194
+ grid-area: inputs;
3195
+ min-width: 0;
3196
+ width: 100%;
3197
+ }
3198
+
3199
+ .palette-add-btn {
3200
+ grid-area: button;
3201
+ }
3202
+
3203
+ &:not([expanded]),
3204
+ &[expanded="false"] {
3205
+ .palette-colors {
3206
+ display: flex;
3207
+ background-color: var(--figma-color-bg-secondary);
3208
+
3209
+ > fig-input-color {
3210
+ display: contents;
3211
+ }
3212
+
3213
+ fig-chit {
3214
+ --padding: 0px;
3215
+ --border-radius: 0px;
3216
+ --width: 100%;
3217
+ flex: 1;
3218
+ min-width: 0;
3219
+ width: 100% !important;
3220
+ height: var(--size);
3221
+ border-radius: 0 !important;
3222
+ input,
3223
+ &::before,
3224
+ &::after {
3225
+ padding: 0 !important;
3226
+ border-radius: 0 !important;
3211
3227
  }
3212
3228
  }
3229
+ }
3230
+ }
3213
3231
 
3214
- &:hover {
3215
- z-index: 5;
3232
+ &[expanded]:not([expanded="false"]) {
3233
+ .palette-colors {
3234
+ flex-direction: column;
3235
+ overflow: visible;
3236
+ border-radius: 0;
3237
+ gap: var(--spacer-2);
3238
+ background-color: transparent;
3239
+
3240
+ > fig-input-color {
3241
+ min-width: 0;
3242
+ }
3243
+
3244
+ fig-chit {
3245
+ --border-radius: var(--radius-medium);
3246
+ width: auto !important;
3247
+ height: var(--size);
3248
+ border-radius: var(--radius-medium) !important;
3216
3249
  }
3217
3250
  }
3218
3251
  }
3219
3252
  }
3220
3253
 
3254
+ fig-field[direction="horizontal"]:has(> fig-input-palette) {
3255
+ padding-right: var(--spacer-2);
3256
+ }
3257
+
3221
3258
  fig-slider {
3222
3259
  display: flex;
3223
3260
 
@@ -4855,10 +4892,6 @@ fig-color-tip {
4855
4892
  }
4856
4893
  }
4857
4894
 
4858
- &[selected]:not([selected="false"]) {
4859
- outline: var(--tip-selection-width) solid var(--figma-color-border-selected);
4860
- }
4861
-
4862
4895
  &[selected]:not([selected="false"]):before {
4863
4896
  content: "";
4864
4897
  background: var(--figma-color-border-selected);
@@ -4866,7 +4899,7 @@ fig-color-tip {
4866
4899
  width: 6px;
4867
4900
  height: 3px;
4868
4901
  position: absolute;
4869
- top: calc(100% + 1px);
4902
+ top: 100%;
4870
4903
  left: var(--beak-offset, 50%);
4871
4904
  z-index: 1;
4872
4905
  pointer-events: none;
@@ -4881,9 +4914,9 @@ fig-color-tip {
4881
4914
  width: 6px;
4882
4915
  height: 3px;
4883
4916
  position: absolute;
4884
- top: 100%;
4917
+ top: calc(100% - 1px);
4885
4918
  left: var(--beak-offset, 50%);
4886
- z-index: 2;
4919
+ z-index: 3;
4887
4920
  transform: translate(-50%);
4888
4921
  }
4889
4922
 
package/fig.js CHANGED
@@ -591,6 +591,7 @@ class FigTooltip extends HTMLElement {
591
591
  this.#stopObserving();
592
592
  if (this.popup) {
593
593
  this.popup.remove();
594
+ this.popup = null;
594
595
  }
595
596
  // Remove the click outside listener if it was added
596
597
  if (this.action === "click") {
@@ -4520,7 +4521,8 @@ class FigInputColor extends HTMLElement {
4520
4521
  const fpAttrs = this.#buildFillPickerAttrs();
4521
4522
 
4522
4523
  let html = ``;
4523
- if (this.getAttribute("text")) {
4524
+ const showText = this.getAttribute("text") === "true";
4525
+ if (showText) {
4524
4526
  let label = `<fig-input-text
4525
4527
  type="text"
4526
4528
  placeholder="000000"
@@ -4757,7 +4759,7 @@ class FigInputColor extends HTMLElement {
4757
4759
  }
4758
4760
 
4759
4761
  static get observedAttributes() {
4760
- return ["value", "style", "mode", "picker", "experimental", "alpha"];
4762
+ return ["value", "style", "mode", "picker", "experimental", "alpha", "text"];
4761
4763
  }
4762
4764
 
4763
4765
  get mode() {
@@ -4808,6 +4810,7 @@ class FigInputColor extends HTMLElement {
4808
4810
  // Picker type change requires re-render
4809
4811
  break;
4810
4812
  case "alpha":
4813
+ case "text":
4811
4814
  if (this.isConnected) this.#buildUI();
4812
4815
  break;
4813
4816
  }
@@ -5682,6 +5685,287 @@ class FigInputFill extends HTMLElement {
5682
5685
  }
5683
5686
  customElements.define("fig-input-fill", FigInputFill);
5684
5687
 
5688
+ /* Input Palette */
5689
+ /**
5690
+ * A palette of solid colors, each rendered as a fig-input-color swatch.
5691
+ * Manages an internal array of colors with add support.
5692
+ * @attr {string} value - JSON array of hex strings or {color,alpha} objects, or comma-separated hex
5693
+ * @attr {boolean} disabled - Whether the palette is disabled
5694
+ * @attr {number} min - Minimum number of colors (default: 2)
5695
+ * @attr {number} max - Maximum number of colors (default: 8); add button hidden at max
5696
+ * @fires input - During color editing (detail: full color array)
5697
+ * @fires change - On committed color edits or add (detail: full color array)
5698
+ */
5699
+ class FigInputPalette extends HTMLElement {
5700
+ #colors = [];
5701
+ #pickers = [];
5702
+ #renderRAF = null;
5703
+
5704
+ static get observedAttributes() {
5705
+ return ["value", "disabled", "min", "max", "expanded"];
5706
+ }
5707
+
5708
+ get #expanded() {
5709
+ return this.hasAttribute("expanded") && this.getAttribute("expanded") !== "false";
5710
+ }
5711
+
5712
+ get #min() {
5713
+ const v = parseInt(this.getAttribute("min"));
5714
+ return isNaN(v) ? 2 : v;
5715
+ }
5716
+
5717
+ get #max() {
5718
+ const v = parseInt(this.getAttribute("max"));
5719
+ return isNaN(v) ? 8 : v;
5720
+ }
5721
+
5722
+ connectedCallback() {
5723
+ if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
5724
+ this.#renderRAF = requestAnimationFrame(() => {
5725
+ this.#renderRAF = null;
5726
+ this.#parseValue();
5727
+ this.#render();
5728
+ });
5729
+ }
5730
+
5731
+ disconnectedCallback() {
5732
+ if (this.#renderRAF) {
5733
+ cancelAnimationFrame(this.#renderRAF);
5734
+ this.#renderRAF = null;
5735
+ }
5736
+ this.#pickers = [];
5737
+ }
5738
+
5739
+ attributeChangedCallback(name, oldValue, newValue) {
5740
+ if (oldValue === newValue) return;
5741
+
5742
+ switch (name) {
5743
+ case "value":
5744
+ this.#parseValue();
5745
+ this.#syncPickers();
5746
+ break;
5747
+ case "disabled":
5748
+ this.#syncDisabled();
5749
+ break;
5750
+ case "min":
5751
+ case "max":
5752
+ case "expanded":
5753
+ this.#render();
5754
+ break;
5755
+ }
5756
+ }
5757
+
5758
+ #parseValue() {
5759
+ const raw = this.getAttribute("value");
5760
+ if (!raw) {
5761
+ this.#colors = [];
5762
+ return;
5763
+ }
5764
+
5765
+ const trimmed = raw.trim();
5766
+
5767
+ // Try JSON first
5768
+ try {
5769
+ const parsed = JSON.parse(trimmed);
5770
+ if (Array.isArray(parsed)) {
5771
+ this.#colors = parsed.map((entry) => {
5772
+ if (typeof entry === "string") {
5773
+ return { color: entry.slice(0, 7), alpha: entry.length > 7 ? parseInt(entry.slice(7, 9), 16) / 255 : 1 };
5774
+ }
5775
+ if (entry && typeof entry === "object") {
5776
+ return {
5777
+ color: entry.color || "#D9D9D9",
5778
+ alpha: entry.alpha !== undefined ? entry.alpha : (entry.opacity !== undefined ? entry.opacity / 100 : 1),
5779
+ };
5780
+ }
5781
+ return { color: "#D9D9D9", alpha: 1 };
5782
+ });
5783
+ return;
5784
+ }
5785
+ } catch (e) {
5786
+ // Not JSON — try comma-separated hex
5787
+ }
5788
+
5789
+ // Comma-separated hex
5790
+ if (trimmed.includes(",")) {
5791
+ this.#colors = trimmed.split(",").map((s) => {
5792
+ const hex = s.trim();
5793
+ return {
5794
+ color: hex.slice(0, 7),
5795
+ alpha: hex.length > 7 ? parseInt(hex.slice(7, 9), 16) / 255 : 1,
5796
+ };
5797
+ });
5798
+ return;
5799
+ }
5800
+
5801
+ // Single hex
5802
+ if (trimmed.startsWith("#")) {
5803
+ this.#colors = [{
5804
+ color: trimmed.slice(0, 7),
5805
+ alpha: trimmed.length > 7 ? parseInt(trimmed.slice(7, 9), 16) / 255 : 1,
5806
+ }];
5807
+ return;
5808
+ }
5809
+
5810
+ this.#colors = [];
5811
+ }
5812
+
5813
+ get value() {
5814
+ return this.#colors.map((c) => ({ ...c }));
5815
+ }
5816
+
5817
+ set value(val) {
5818
+ if (typeof val === "string") {
5819
+ this.setAttribute("value", val);
5820
+ } else {
5821
+ this.setAttribute("value", JSON.stringify(val));
5822
+ }
5823
+ }
5824
+
5825
+ #render() {
5826
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5827
+
5828
+ this.innerHTML = "";
5829
+ this.#pickers = [];
5830
+
5831
+ const wrap = document.createElement("div");
5832
+ wrap.className = "palette-colors";
5833
+ this.#colors.forEach((entry, i) => {
5834
+ wrap.appendChild(this.#createPicker(entry, i, disabled));
5835
+ });
5836
+ this.appendChild(wrap);
5837
+
5838
+ this.#createAddButton(disabled);
5839
+ }
5840
+
5841
+ #createPicker(entry, index, disabled) {
5842
+ const hexAlpha = entry.alpha < 1
5843
+ ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5844
+ : entry.color;
5845
+ const expanded = this.#expanded;
5846
+ const ic = document.createElement("fig-input-color");
5847
+ ic.setAttribute("value", hexAlpha);
5848
+ ic.setAttribute("text", expanded ? "true" : "false");
5849
+ ic.setAttribute("picker", "figma");
5850
+ ic.setAttribute("alpha", "true");
5851
+ ic.setAttribute("picker-anchor", "self");
5852
+ if (expanded) ic.setAttribute("full", "");
5853
+ if (disabled) ic.setAttribute("disabled", "");
5854
+
5855
+ const updateFromPicker = (e) => {
5856
+ e.stopPropagation();
5857
+ const el = e.currentTarget;
5858
+ this.#colors[index] = {
5859
+ color: el.hexOpaque || this.#colors[index].color,
5860
+ alpha: el.rgba ? el.rgba.a : this.#colors[index].alpha,
5861
+ };
5862
+ };
5863
+
5864
+ ic.addEventListener("input", (e) => {
5865
+ updateFromPicker(e);
5866
+ this.#emitInput();
5867
+ });
5868
+
5869
+ ic.addEventListener("change", (e) => {
5870
+ updateFromPicker(e);
5871
+ this.#emitChange();
5872
+ });
5873
+
5874
+ this.#pickers.push(ic);
5875
+ return ic;
5876
+ }
5877
+
5878
+ #createAddButton(disabled) {
5879
+ const atMax = this.#colors.length >= this.#max;
5880
+ const addBtn = document.createElement("fig-button");
5881
+ addBtn.setAttribute("variant", "ghost");
5882
+ addBtn.setAttribute("icon", "true");
5883
+ addBtn.setAttribute("aria-label", "Add color");
5884
+ addBtn.className = "palette-add-btn";
5885
+ if (disabled || atMax) addBtn.setAttribute("disabled", "");
5886
+ addBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>`;
5887
+ addBtn.addEventListener("click", () => {
5888
+ if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
5889
+ if (this.#colors.length >= this.#max) return;
5890
+ this.#addColor({ color: "#D9D9D9", alpha: 1 });
5891
+ });
5892
+ const tooltip = document.createElement("fig-tooltip");
5893
+ tooltip.setAttribute("text", "Add color");
5894
+ tooltip.appendChild(addBtn);
5895
+ this.appendChild(tooltip);
5896
+ }
5897
+
5898
+ #addColor(entry) {
5899
+ this.#colors.push(entry);
5900
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5901
+ const ic = this.#createPicker(entry, this.#colors.length - 1, disabled);
5902
+ const wrap = this.querySelector(".palette-colors");
5903
+ if (wrap) {
5904
+ wrap.appendChild(ic);
5905
+ } else {
5906
+ this.appendChild(ic);
5907
+ }
5908
+
5909
+ if (this.#colors.length >= this.#max) {
5910
+ const addBtn = this.querySelector(".palette-add-btn");
5911
+ if (addBtn) addBtn.setAttribute("disabled", "");
5912
+ }
5913
+ this.#emitChange();
5914
+ }
5915
+
5916
+ #updateChit(index) {
5917
+ const ic = this.#pickers[index];
5918
+ if (!ic) return;
5919
+ const entry = this.#colors[index];
5920
+ const hexAlpha = entry.alpha < 1
5921
+ ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5922
+ : entry.color;
5923
+ ic.setAttribute("value", hexAlpha);
5924
+ }
5925
+
5926
+ #syncPickers() {
5927
+ if (this.#pickers.length !== this.#colors.length) {
5928
+ this.#render();
5929
+ return;
5930
+ }
5931
+ this.#colors.forEach((_, i) => {
5932
+ this.#updateChit(i);
5933
+ });
5934
+ }
5935
+
5936
+ #syncDisabled() {
5937
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5938
+ this.#pickers.forEach((fp) => {
5939
+ if (disabled) fp.setAttribute("disabled", "");
5940
+ else fp.removeAttribute("disabled");
5941
+ });
5942
+ const addBtn = this.querySelector(".palette-add-btn");
5943
+ if (addBtn) {
5944
+ if (disabled) addBtn.setAttribute("disabled", "");
5945
+ else addBtn.removeAttribute("disabled");
5946
+ }
5947
+ }
5948
+
5949
+ #emitInput() {
5950
+ this.dispatchEvent(
5951
+ new CustomEvent("input", {
5952
+ bubbles: true,
5953
+ detail: this.value,
5954
+ }),
5955
+ );
5956
+ }
5957
+
5958
+ #emitChange() {
5959
+ this.dispatchEvent(
5960
+ new CustomEvent("change", {
5961
+ bubbles: true,
5962
+ detail: this.value,
5963
+ }),
5964
+ );
5965
+ }
5966
+ }
5967
+ customElements.define("fig-input-palette", FigInputPalette);
5968
+
5685
5969
  /* Input Gradient */
5686
5970
  /**
5687
5971
  * A gradient-only fill input built on top of fig-fill-picker.
@@ -5694,6 +5978,7 @@ class FigInputGradient extends HTMLElement {
5694
5978
  #chit;
5695
5979
  #track;
5696
5980
  #handleDragging = false;
5981
+ #arrowTooltipTimer = null;
5697
5982
  #colorObserver = null;
5698
5983
  #gradient = {
5699
5984
  type: "linear",
@@ -5725,17 +6010,67 @@ class FigInputGradient extends HTMLElement {
5725
6010
  }
5726
6011
 
5727
6012
  #onKeyDown = (e) => {
5728
- if (e.key !== "Delete" && e.key !== "Backspace") return;
5729
6013
  const active = document.activeElement;
5730
- if (
6014
+ const isTyping =
5731
6015
  active &&
5732
6016
  (active.tagName === "INPUT" ||
5733
6017
  active.tagName === "TEXTAREA" ||
5734
- active.isContentEditable)
5735
- )
6018
+ active.isContentEditable);
6019
+ if (!this.#track) return;
6020
+
6021
+ if (e.key === "Tab" && !isTyping) {
6022
+ const selected = this.#track.querySelector(
6023
+ "fig-handle[selected]:not(.fig-input-gradient-ghost)",
6024
+ );
6025
+ if (!selected) return;
6026
+ e.preventDefault();
6027
+ const handles = [
6028
+ ...this.#track.querySelectorAll(
6029
+ "fig-handle:not(.fig-input-gradient-ghost)",
6030
+ ),
6031
+ ];
6032
+ const curIdx = handles.indexOf(selected);
6033
+ const next = e.shiftKey
6034
+ ? (curIdx - 1 + handles.length) % handles.length
6035
+ : (curIdx + 1) % handles.length;
6036
+ selected.deselect();
6037
+ handles[next].select();
5736
6038
  return;
6039
+ }
6040
+
6041
+ if ((e.key === "ArrowLeft" || e.key === "ArrowRight") && !isTyping) {
6042
+ const selected = this.#track.querySelector(
6043
+ "fig-handle[selected]:not(.fig-input-gradient-ghost)",
6044
+ );
6045
+ if (!selected) return;
6046
+ const idx = parseInt(selected.dataset.stopIndex, 10);
6047
+ if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6048
+ e.preventDefault();
6049
+ const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? 10 : 1);
6050
+ const stop = this.#gradient.stops[idx];
6051
+ stop.position = Math.max(0, Math.min(100, stop.position + delta));
6052
+ selected.setAttribute("value", `${stop.position}% 50%`);
6053
+ const tip = selected.closest("fig-tooltip");
6054
+ if (tip) {
6055
+ tip.text = `${Math.round(stop.position)}%`;
6056
+ tip.setAttribute("show", "true");
6057
+ tip.showPopup();
6058
+ selected.hideColorTip();
6059
+ clearTimeout(this.#arrowTooltipTimer);
6060
+ this.#arrowTooltipTimer = setTimeout(() => {
6061
+ tip.removeAttribute("show");
6062
+ selected.showColorTip();
6063
+ }, 600);
6064
+ }
6065
+ this.#syncChit();
6066
+ this.#emitInput();
6067
+ this.#emitChange();
6068
+ return;
6069
+ }
6070
+
6071
+ if (e.key !== "Delete" && e.key !== "Backspace") return;
6072
+ if (isTyping) return;
5737
6073
  if (this.#gradient.stops.length <= 2) return;
5738
- if (!this.#track) return;
5739
6074
  const selected = this.#track.querySelector(
5740
6075
  "fig-handle[selected]:not(.fig-input-gradient-ghost)",
5741
6076
  );
@@ -6072,7 +6407,10 @@ class FigInputGradient extends HTMLElement {
6072
6407
  const tooltip = handle.closest("fig-tooltip");
6073
6408
  if (tooltip) {
6074
6409
  tooltip.text = `${Math.round(position)}%`;
6075
- if (!tooltip.hasAttribute("show")) tooltip.setAttribute("show", "true");
6410
+ if (!tooltip.hasAttribute("show")) {
6411
+ tooltip.setAttribute("show", "true");
6412
+ handle.hideColorTip();
6413
+ }
6076
6414
  }
6077
6415
  this.#syncChit();
6078
6416
  this.#emitInput();
@@ -6085,6 +6423,7 @@ class FigInputGradient extends HTMLElement {
6085
6423
  handle.style.zIndex = "";
6086
6424
  const tooltip = handle.closest("fig-tooltip");
6087
6425
  if (tooltip) tooltip.removeAttribute("show");
6426
+ handle.showColorTip();
6088
6427
  const idx = parseInt(handle.dataset.stopIndex, 10);
6089
6428
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6090
6429
  const px = e.detail?.px ?? 0;
@@ -6104,17 +6443,8 @@ class FigInputGradient extends HTMLElement {
6104
6443
  this.#syncStopIndices();
6105
6444
  this.#syncChit();
6106
6445
  this.#emitChange();
6107
- const swallow = (evt) => {
6108
- evt.stopPropagation();
6109
- evt.preventDefault();
6110
- };
6111
- this.#track.addEventListener("click", swallow, {
6112
- capture: true,
6113
- once: true,
6114
- });
6115
6446
  requestAnimationFrame(() => {
6116
6447
  this.#handleDragging = false;
6117
- this.#track.removeEventListener("click", swallow, { capture: true });
6118
6448
  });
6119
6449
  });
6120
6450
 
@@ -10028,6 +10358,7 @@ class FigFillPicker extends HTMLElement {
10028
10358
  this.#teardownColorAreaEvents();
10029
10359
  this.#teardownColorAreaEvents = null;
10030
10360
  }
10361
+ if (this.#chit) this.#chit.removeAttribute("selected");
10031
10362
  if (this.#dialog) {
10032
10363
  this.#dialog.close();
10033
10364
  this.#dialog.remove();
@@ -10205,6 +10536,8 @@ class FigFillPicker extends HTMLElement {
10205
10536
  const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
10206
10537
  if (gamutEl) gamutEl.value = this.#gamut;
10207
10538
 
10539
+ if (this.#chit) this.#chit.setAttribute("selected", "true");
10540
+
10208
10541
  this.#dialog.open = true;
10209
10542
 
10210
10543
  requestAnimationFrame(() => {
@@ -10359,9 +10692,17 @@ class FigFillPicker extends HTMLElement {
10359
10692
  this.#dialog.open = false;
10360
10693
  });
10361
10694
 
10362
- this.#dialog.addEventListener("close", () => {
10695
+ const onDialogClose = () => {
10696
+ if (this.#chit) this.#chit.removeAttribute("selected");
10363
10697
  this.#emitChange();
10698
+ };
10699
+ this.#dialog.addEventListener("close", onDialogClose);
10700
+
10701
+ const observer = new MutationObserver(() => {
10702
+ const isOpen = this.#dialog.hasAttribute("open") && this.#dialog.getAttribute("open") !== "false";
10703
+ if (!isOpen) onDialogClose();
10364
10704
  });
10705
+ observer.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
10365
10706
 
10366
10707
  // Initialize built-in tabs (skip any overridden by custom slots)
10367
10708
  const builtinInits = {
@@ -13012,6 +13353,7 @@ class FigHandle extends HTMLElement {
13012
13353
  this.#syncDrag();
13013
13354
  this.addEventListener("click", this.#handleSelect);
13014
13355
  document.addEventListener("pointerdown", this.#handleDeselect);
13356
+ document.addEventListener("keydown", this.#handleKeyDown);
13015
13357
  const initial = this.getAttribute("value");
13016
13358
  if (initial) this.#applyValue(initial);
13017
13359
  if (this.#hasControlMode && !this.#isGhost) this.#showColorTip();
@@ -13022,25 +13364,43 @@ class FigHandle extends HTMLElement {
13022
13364
  this.#hideColorTip();
13023
13365
  this.removeEventListener("click", this.#handleSelect);
13024
13366
  document.removeEventListener("pointerdown", this.#handleDeselect);
13367
+ document.removeEventListener("keydown", this.#handleKeyDown);
13025
13368
  }
13026
13369
 
13027
- #handleSelect = (e) => {
13370
+ select() {
13028
13371
  if (this.hasAttribute("disabled")) return;
13372
+ this.setAttribute("selected", "");
13373
+ if (this.getAttribute("type") === "color") this.#showColorTip();
13374
+ }
13375
+
13376
+ deselect() {
13377
+ this.removeAttribute("selected");
13378
+ this.#hideColorTip();
13379
+ }
13380
+
13381
+ #handleSelect = (e) => {
13029
13382
  if (this.#hasControlMode) return;
13030
13383
  if (this.#didDrag) {
13031
13384
  this.#didDrag = false;
13032
13385
  return;
13033
13386
  }
13034
- this.setAttribute("selected", "");
13035
- if (this.getAttribute("type") === "color") this.#showColorTip();
13387
+ this.select();
13036
13388
  };
13037
13389
 
13038
13390
  #handleDeselect = (e) => {
13039
13391
  if (this.#hasControlMode) return;
13040
13392
  if (this.contains(e.target)) return;
13041
13393
  if (this.#colorTip && e.target.closest?.("dialog, [popover]")) return;
13042
- this.removeAttribute("selected");
13043
- this.#hideColorTip();
13394
+ this.deselect();
13395
+ };
13396
+
13397
+ #handleKeyDown = (e) => {
13398
+ if (e.key !== "Enter") return;
13399
+ if (!this.hasAttribute("selected")) return;
13400
+ if (this.getAttribute("type") !== "color") return;
13401
+ if (this.#colorTip) return;
13402
+ e.preventDefault();
13403
+ this.#showColorTip();
13044
13404
  };
13045
13405
 
13046
13406
  attributeChangedCallback(name, _old, value) {
@@ -13144,6 +13504,7 @@ class FigHandle extends HTMLElement {
13144
13504
  if (!this.#didDrag) {
13145
13505
  this.classList.add("dragging");
13146
13506
  this.style.cursor = "grabbing";
13507
+ if (!this.hasAttribute("selected")) this.select();
13147
13508
  }
13148
13509
  this.#didDrag = true;
13149
13510
  clampAndApply(e.clientX, e.clientY, e.shiftKey);
@@ -13191,6 +13552,19 @@ class FigHandle extends HTMLElement {
13191
13552
  window.addEventListener("pointerup", onUp);
13192
13553
  }
13193
13554
 
13555
+ showColorTip() {
13556
+ if (this.#colorTip) {
13557
+ this.#colorTip.style.display = "";
13558
+ return;
13559
+ }
13560
+ this.#showColorTip();
13561
+ }
13562
+
13563
+ hideColorTip() {
13564
+ if (!this.#colorTip) return;
13565
+ this.#colorTip.style.display = "none";
13566
+ }
13567
+
13194
13568
  #showColorTip() {
13195
13569
  if (this.#colorTip) return;
13196
13570
  const tip = document.createElement("fig-color-tip");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.14.0",
3
+ "version": "3.15.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",