@rogieking/figui3 3.14.1 → 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 +151 -112
  2. package/fig.js +308 -3
  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 {
@@ -3212,6 +3178,83 @@ fig-input-gradient {
3212
3178
  }
3213
3179
  }
3214
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;
3227
+ }
3228
+ }
3229
+ }
3230
+ }
3231
+
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;
3249
+ }
3250
+ }
3251
+ }
3252
+ }
3253
+
3254
+ fig-field[direction="horizontal"]:has(> fig-input-palette) {
3255
+ padding-right: var(--spacer-2);
3256
+ }
3257
+
3215
3258
  fig-slider {
3216
3259
  display: flex;
3217
3260
 
@@ -4849,10 +4892,6 @@ fig-color-tip {
4849
4892
  }
4850
4893
  }
4851
4894
 
4852
- &[selected]:not([selected="false"]) {
4853
- outline: var(--tip-selection-width) solid var(--figma-color-border-selected);
4854
- }
4855
-
4856
4895
  &[selected]:not([selected="false"]):before {
4857
4896
  content: "";
4858
4897
  background: var(--figma-color-border-selected);
@@ -4860,7 +4899,7 @@ fig-color-tip {
4860
4899
  width: 6px;
4861
4900
  height: 3px;
4862
4901
  position: absolute;
4863
- top: calc(100% + 1px);
4902
+ top: 100%;
4864
4903
  left: var(--beak-offset, 50%);
4865
4904
  z-index: 1;
4866
4905
  pointer-events: none;
@@ -4875,9 +4914,9 @@ fig-color-tip {
4875
4914
  width: 6px;
4876
4915
  height: 3px;
4877
4916
  position: absolute;
4878
- top: 100%;
4917
+ top: calc(100% - 1px);
4879
4918
  left: var(--beak-offset, 50%);
4880
- z-index: 2;
4919
+ z-index: 3;
4881
4920
  transform: translate(-50%);
4882
4921
  }
4883
4922
 
package/fig.js CHANGED
@@ -4521,7 +4521,8 @@ class FigInputColor extends HTMLElement {
4521
4521
  const fpAttrs = this.#buildFillPickerAttrs();
4522
4522
 
4523
4523
  let html = ``;
4524
- if (this.getAttribute("text")) {
4524
+ const showText = this.getAttribute("text") === "true";
4525
+ if (showText) {
4525
4526
  let label = `<fig-input-text
4526
4527
  type="text"
4527
4528
  placeholder="000000"
@@ -4758,7 +4759,7 @@ class FigInputColor extends HTMLElement {
4758
4759
  }
4759
4760
 
4760
4761
  static get observedAttributes() {
4761
- return ["value", "style", "mode", "picker", "experimental", "alpha"];
4762
+ return ["value", "style", "mode", "picker", "experimental", "alpha", "text"];
4762
4763
  }
4763
4764
 
4764
4765
  get mode() {
@@ -4809,6 +4810,7 @@ class FigInputColor extends HTMLElement {
4809
4810
  // Picker type change requires re-render
4810
4811
  break;
4811
4812
  case "alpha":
4813
+ case "text":
4812
4814
  if (this.isConnected) this.#buildUI();
4813
4815
  break;
4814
4816
  }
@@ -5683,6 +5685,287 @@ class FigInputFill extends HTMLElement {
5683
5685
  }
5684
5686
  customElements.define("fig-input-fill", FigInputFill);
5685
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
+
5686
5969
  /* Input Gradient */
5687
5970
  /**
5688
5971
  * A gradient-only fill input built on top of fig-fill-picker.
@@ -10075,6 +10358,7 @@ class FigFillPicker extends HTMLElement {
10075
10358
  this.#teardownColorAreaEvents();
10076
10359
  this.#teardownColorAreaEvents = null;
10077
10360
  }
10361
+ if (this.#chit) this.#chit.removeAttribute("selected");
10078
10362
  if (this.#dialog) {
10079
10363
  this.#dialog.close();
10080
10364
  this.#dialog.remove();
@@ -10252,6 +10536,8 @@ class FigFillPicker extends HTMLElement {
10252
10536
  const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
10253
10537
  if (gamutEl) gamutEl.value = this.#gamut;
10254
10538
 
10539
+ if (this.#chit) this.#chit.setAttribute("selected", "true");
10540
+
10255
10541
  this.#dialog.open = true;
10256
10542
 
10257
10543
  requestAnimationFrame(() => {
@@ -10406,9 +10692,17 @@ class FigFillPicker extends HTMLElement {
10406
10692
  this.#dialog.open = false;
10407
10693
  });
10408
10694
 
10409
- this.#dialog.addEventListener("close", () => {
10695
+ const onDialogClose = () => {
10696
+ if (this.#chit) this.#chit.removeAttribute("selected");
10410
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();
10411
10704
  });
10705
+ observer.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
10412
10706
 
10413
10707
  // Initialize built-in tabs (skip any overridden by custom slots)
10414
10708
  const builtinInits = {
@@ -13059,6 +13353,7 @@ class FigHandle extends HTMLElement {
13059
13353
  this.#syncDrag();
13060
13354
  this.addEventListener("click", this.#handleSelect);
13061
13355
  document.addEventListener("pointerdown", this.#handleDeselect);
13356
+ document.addEventListener("keydown", this.#handleKeyDown);
13062
13357
  const initial = this.getAttribute("value");
13063
13358
  if (initial) this.#applyValue(initial);
13064
13359
  if (this.#hasControlMode && !this.#isGhost) this.#showColorTip();
@@ -13069,6 +13364,7 @@ class FigHandle extends HTMLElement {
13069
13364
  this.#hideColorTip();
13070
13365
  this.removeEventListener("click", this.#handleSelect);
13071
13366
  document.removeEventListener("pointerdown", this.#handleDeselect);
13367
+ document.removeEventListener("keydown", this.#handleKeyDown);
13072
13368
  }
13073
13369
 
13074
13370
  select() {
@@ -13098,6 +13394,15 @@ class FigHandle extends HTMLElement {
13098
13394
  this.deselect();
13099
13395
  };
13100
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();
13404
+ };
13405
+
13101
13406
  attributeChangedCallback(name, _old, value) {
13102
13407
  if (name === "color") {
13103
13408
  if (!value || value === "false" || value === "true") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.14.1",
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",