@rogieking/figui3 3.14.1 → 3.16.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 +217 -145
  2. package/fig.js +453 -51
  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
- &[disabled] {
1227
+ &[disabled]:not([disabled="false"]) {
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;
@@ -1400,9 +1373,9 @@ fig-easing-curve {
1400
1373
  --easing-bezier-handle-radius: 5;
1401
1374
  --easing-spring-handle-radius: 5;
1402
1375
  --easing-handle-fill: var(--figma-color-border-strong);
1403
- --easing-duration-bar-width: 5;
1376
+ --easing-duration-bar-width: 7;
1404
1377
  --easing-duration-bar-height: 16;
1405
- --easing-duration-bar-radius: 3;
1378
+ --easing-duration-bar-radius: 4;
1406
1379
  --aspect-ratio: 1 / 1;
1407
1380
 
1408
1381
  width: 100%;
@@ -1465,17 +1438,6 @@ fig-easing-curve {
1465
1438
  overflow: visible;
1466
1439
  pointer-events: all;
1467
1440
  cursor: default;
1468
-
1469
- fig-handle {
1470
- --width: calc(var(--easing-bezier-handle-radius) * 2px);
1471
- --height: calc(var(--easing-bezier-handle-radius) * 2px);
1472
- pointer-events: all;
1473
- cursor: default;
1474
- }
1475
-
1476
- &:active fig-handle {
1477
- cursor: grabbing;
1478
- }
1479
1441
  }
1480
1442
  .fig-easing-curve-endpoint {
1481
1443
  fill: var(--easing-handle-fill);
@@ -2900,7 +2862,8 @@ fig-checkbox,
2900
2862
  fig-radio,
2901
2863
  fig-tab,
2902
2864
  fig-tabs,
2903
- fig-segmented-control {
2865
+ fig-segmented-control,
2866
+ fig-input-palette {
2904
2867
  display: inline-flex;
2905
2868
  gap: var(--spacer-2);
2906
2869
  user-select: none;
@@ -3121,6 +3084,10 @@ fig-input-fill {
3121
3084
  outline: 1px solid var(--figma-color-border-selected) !important;
3122
3085
  outline-offset: -1px !important;
3123
3086
  }
3087
+ &:hover {
3088
+ outline: 1px solid var(--figma-color-border);
3089
+ outline-offset: -1px !important;
3090
+ }
3124
3091
 
3125
3092
  & > .input-combo > fig-chit:not(:only-child),
3126
3093
  & > .input-combo > fig-chit:not(:only-child) input,
@@ -3136,6 +3103,7 @@ fig-input-fill {
3136
3103
  background-color: transparent !important;
3137
3104
  box-shadow: none !important;
3138
3105
  border: 0 !important;
3106
+ --selected: 0;
3139
3107
  }
3140
3108
  }
3141
3109
  fig-chit ~ fig-input-text > input,
@@ -3177,21 +3145,12 @@ fig-input-gradient {
3177
3145
  outline: 1px solid var(--figma-color-border-selected) !important;
3178
3146
  outline-offset: -1px !important;
3179
3147
  }
3180
- fig-chit {
3148
+ & > fig-chit {
3149
+ --padding: 0;
3150
+ --width: 100%;
3181
3151
  flex: 1 1 auto;
3182
3152
  width: 100% !important;
3183
3153
  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
3154
  }
3196
3155
 
3197
3156
  .fig-input-gradient-track {
@@ -3212,6 +3171,98 @@ fig-input-gradient {
3212
3171
  }
3213
3172
  }
3214
3173
 
3174
+ fig-input-palette {
3175
+ display: inline-flex !important;
3176
+ flex-direction: column;
3177
+ gap: var(--spacer-2);
3178
+ min-width: 0;
3179
+
3180
+ .palette-colors {
3181
+ display: flex;
3182
+ flex-wrap: nowrap;
3183
+ gap: 0;
3184
+ border-radius: var(--radius-medium);
3185
+ overflow: hidden;
3186
+ grid-area: inputs;
3187
+ min-width: 0;
3188
+ width: 100%;
3189
+ }
3190
+
3191
+ .palette-add-btn {
3192
+ grid-area: button;
3193
+ }
3194
+ .palette-colors-inline {
3195
+ display: inline-grid !important;
3196
+ grid-template-columns: 1fr 24px;
3197
+ grid-template-areas: "inputs button";
3198
+ border-radius: var(--radius-medium);
3199
+ background-color: var(--figma-color-bg-secondary);
3200
+ width: 100%;
3201
+
3202
+ .palette-colors {
3203
+ display: flex;
3204
+ background-color: var(--figma-color-bg-secondary);
3205
+ fig-input-color {
3206
+ width: 100%;
3207
+ }
3208
+
3209
+ fig-chit {
3210
+ --padding: 0px;
3211
+ --border-radius: 0px;
3212
+ --width: 100%;
3213
+ flex: 1;
3214
+ min-width: 0;
3215
+ width: 100% !important;
3216
+ height: var(--size);
3217
+ border-radius: 0 !important;
3218
+ input,
3219
+ &::before,
3220
+ &::after {
3221
+ padding: 0 !important;
3222
+ border-radius: 0 !important;
3223
+ }
3224
+ }
3225
+ }
3226
+ }
3227
+ &[expanded]:not([expanded="false"]):not([edit="false"]) {
3228
+ .palette-colors-expanded {
3229
+ display: flex;
3230
+ }
3231
+ }
3232
+ &[add="false"] {
3233
+ .palette-colors-expanded,
3234
+ .palette-colors-inline {
3235
+ grid-template-areas: "inputs inputs";
3236
+ }
3237
+ }
3238
+ .palette-colors-expanded {
3239
+ display: none;
3240
+ flex-direction: column;
3241
+ overflow: visible;
3242
+ border-radius: 0;
3243
+ gap: var(--spacer-2);
3244
+ width: 100%;
3245
+
3246
+ > fig-input-color {
3247
+ min-width: 0;
3248
+ }
3249
+
3250
+ fig-chit {
3251
+ --border-radius: var(--radius-medium);
3252
+ width: auto !important;
3253
+ height: var(--size);
3254
+ border-radius: var(--radius-medium) !important;
3255
+ }
3256
+ }
3257
+
3258
+ &[expanded]:not([expanded="false"]) {
3259
+ }
3260
+ }
3261
+
3262
+ fig-field[direction="horizontal"]:has(> fig-input-palette) {
3263
+ padding-right: var(--spacer-2);
3264
+ }
3265
+
3215
3266
  fig-slider {
3216
3267
  display: flex;
3217
3268
 
@@ -4541,8 +4592,20 @@ fig-chooser {
4541
4592
  display: flex;
4542
4593
  flex-direction: column;
4543
4594
  gap: 1px;
4544
- overflow: hidden auto;
4595
+ overflow: visible auto;
4545
4596
  scrollbar-width: none;
4597
+ scroll-snap-type: y mandatory;
4598
+
4599
+ > * {
4600
+ scroll-snap-align: start;
4601
+ }
4602
+
4603
+ &[padding="false"] {
4604
+ gap: var(--spacer-2);
4605
+ fig-choice {
4606
+ --fig-choice-padding: 0px;
4607
+ }
4608
+ }
4546
4609
 
4547
4610
  &[overflow="scrollbar"] {
4548
4611
  scrollbar-width: thin;
@@ -4710,12 +4773,15 @@ fig-chooser {
4710
4773
  }
4711
4774
 
4712
4775
  fig-choice {
4713
- --fig-choice-selection-ring-width: 1px;
4776
+ --fig-choice-selection-ring-width: 1.25px;
4714
4777
  --fig-choice-padding: var(--spacer-2);
4778
+ --fig-choice-border-radius: calc(
4779
+ var(--radius-medium) + var(--fig-choice-padding)
4780
+ );
4715
4781
  display: flex;
4716
4782
  align-items: center;
4717
4783
  flex-direction: column;
4718
- border-radius: var(--radius-large);
4784
+ border-radius: var(--fig-choice-border-radius);
4719
4785
  gap: var(--spacer-2);
4720
4786
  outline: none;
4721
4787
  border: 1px solid transparent;
@@ -4727,14 +4793,25 @@ fig-choice {
4727
4793
  background-color: var(--figma-color-bg-secondary);
4728
4794
  }
4729
4795
 
4796
+ &[padding="false"] {
4797
+ --fig-choice-padding: 0px;
4798
+ }
4799
+
4730
4800
  &[selected] {
4731
- background-color: var(--figma-color-bg-secondary);
4732
- & > fig-image {
4733
- border-radius: var(--radius-medium);
4801
+ position: relative;
4802
+ &::after {
4803
+ content: "";
4804
+ position: absolute;
4805
+ inset: 0;
4806
+ border-radius: var(--fig-choice-border-radius);
4807
+ pointer-events: none;
4808
+ z-index: 1;
4809
+ box-shadow: inset 0 0 0 4px var(--figma-color-bg);
4734
4810
  outline: var(--fig-choice-selection-ring-width) solid
4735
4811
  var(--figma-color-border-selected);
4736
- outline-offset: 0;
4812
+ outline-offset: calc(var(--fig-choice-selection-ring-width) * -1);
4737
4813
  }
4814
+ background-color: var(--figma-color-bg-secondary);
4738
4815
  }
4739
4816
 
4740
4817
  &[disabled]:not([disabled="false"]),
@@ -4750,14 +4827,14 @@ fig-handle {
4750
4827
  --height: 0.875rem;
4751
4828
  --fill: var(--figma-color-bg-brand);
4752
4829
  --border-radius: 50%;
4753
- --ring-width: 1.25px;
4754
- --box-shadow:
4755
- inset 0 0 0 var(--ring-width) var(--handle-color),
4756
- 0px 0 0 0.5px rgba(0, 0, 0, 0.1), var(--elevation-100-canvas);
4830
+ --ring-width: 0.125rem;
4831
+ --box-shadow: 0px 0 0 0.5px rgba(0, 0, 0, 0.1), var(--elevation-100-canvas);
4757
4832
  --outline: none;
4758
4833
  --border: none;
4759
4834
 
4760
- display: inline-grid;
4835
+ display: grid;
4836
+ margin: 0;
4837
+ padding: 0;
4761
4838
  place-items: center;
4762
4839
  width: var(--width);
4763
4840
  height: var(--height);
@@ -4770,8 +4847,8 @@ fig-handle {
4770
4847
  &::before {
4771
4848
  content: "";
4772
4849
  color-scheme: light only;
4773
- width: calc(var(--width) - 4px);
4774
- height: calc(var(--height) - 4px);
4850
+ width: calc(var(--width) - var(--ring-width) * 2);
4851
+ height: calc(var(--height) - var(--ring-width) * 2);
4775
4852
  background: var(--fill);
4776
4853
  border-radius: var(--border-radius);
4777
4854
  box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
@@ -4779,8 +4856,8 @@ fig-handle {
4779
4856
  }
4780
4857
 
4781
4858
  &[size="small"] {
4782
- --width: 0.5625rem;
4783
- --height: 0.5625rem;
4859
+ --width: 11px;
4860
+ --height: 11px;
4784
4861
  }
4785
4862
 
4786
4863
  &[drag]:not([drag="false"]) {
@@ -4789,14 +4866,13 @@ fig-handle {
4789
4866
  }
4790
4867
  &:hover,
4791
4868
  &[selected]:not([selected="false"]) {
4792
- outline: var(--ring-width) solid var(--figma-color-border-selected);
4869
+ outline: 1px solid var(--figma-color-border-selected);
4793
4870
  outline-offset: 0;
4794
4871
  }
4795
4872
 
4796
4873
  &[disabled]:not([disabled="false"]),
4797
4874
  &[aria-disabled="true"] {
4798
4875
  pointer-events: none;
4799
- opacity: 0.4;
4800
4876
  cursor: default;
4801
4877
  }
4802
4878
 
@@ -4806,7 +4882,7 @@ fig-handle {
4806
4882
 
4807
4883
  fig-color-tip {
4808
4884
  position: absolute;
4809
- bottom: calc(100% + 6px);
4885
+ bottom: calc(100% + 2px);
4810
4886
  left: 50%;
4811
4887
  transform: translateX(-50%);
4812
4888
  z-index: 10;
@@ -4849,10 +4925,6 @@ fig-color-tip {
4849
4925
  }
4850
4926
  }
4851
4927
 
4852
- &[selected]:not([selected="false"]) {
4853
- outline: var(--tip-selection-width) solid var(--figma-color-border-selected);
4854
- }
4855
-
4856
4928
  &[selected]:not([selected="false"]):before {
4857
4929
  content: "";
4858
4930
  background: var(--figma-color-border-selected);
@@ -4860,7 +4932,7 @@ fig-color-tip {
4860
4932
  width: 6px;
4861
4933
  height: 3px;
4862
4934
  position: absolute;
4863
- top: calc(100% + 1px);
4935
+ top: 100%;
4864
4936
  left: var(--beak-offset, 50%);
4865
4937
  z-index: 1;
4866
4938
  pointer-events: none;
@@ -4875,9 +4947,9 @@ fig-color-tip {
4875
4947
  width: 6px;
4876
4948
  height: 3px;
4877
4949
  position: absolute;
4878
- top: 100%;
4950
+ top: calc(100% - 1px);
4879
4951
  left: var(--beak-offset, 50%);
4880
- z-index: 2;
4952
+ z-index: 3;
4881
4953
  transform: translate(-50%);
4882
4954
  }
4883
4955
 
package/fig.js CHANGED
@@ -4519,13 +4519,16 @@ class FigInputColor extends HTMLElement {
4519
4519
  const hidePicker = this.picker === "false";
4520
4520
  const showAlpha = this.getAttribute("alpha") === "true";
4521
4521
  const fpAttrs = this.#buildFillPickerAttrs();
4522
+ const disabled = this.#disabled;
4523
+ const disabledAttr = disabled ? " disabled" : "";
4522
4524
 
4523
4525
  let html = ``;
4524
- if (this.getAttribute("text")) {
4526
+ const showText = this.getAttribute("text") === "true";
4527
+ if (showText) {
4525
4528
  let label = `<fig-input-text
4526
4529
  type="text"
4527
4530
  placeholder="000000"
4528
- value="${this.hexOpaque.slice(1).toUpperCase()}">
4531
+ value="${this.hexOpaque.slice(1).toUpperCase()}"${disabledAttr}>
4529
4532
  </fig-input-text>`;
4530
4533
  if (showAlpha) {
4531
4534
  label += `<fig-tooltip text="Opacity">
@@ -4534,7 +4537,7 @@ class FigInputColor extends HTMLElement {
4534
4537
  min="0"
4535
4538
  max="100"
4536
4539
  value="${this.#alphaPercent}"
4537
- units="%">
4540
+ units="%"${disabledAttr}>
4538
4541
  </fig-input-number>
4539
4542
  </fig-tooltip>`;
4540
4543
  }
@@ -4546,8 +4549,8 @@ class FigInputColor extends HTMLElement {
4546
4549
  showAlpha ? "" : 'alpha="false"'
4547
4550
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
4548
4551
  this.#alphaPercent
4549
- }}'></fig-fill-picker>`
4550
- : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"></fig-chit>`;
4552
+ }}'${disabledAttr}></fig-fill-picker>`
4553
+ : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
4551
4554
  }
4552
4555
 
4553
4556
  html = `<div class="input-combo">
@@ -4563,8 +4566,8 @@ class FigInputColor extends HTMLElement {
4563
4566
  showAlpha ? "" : 'alpha="false"'
4564
4567
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
4565
4568
  this.#alphaPercent
4566
- }}'></fig-fill-picker>`
4567
- : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"></fig-chit>`;
4569
+ }}'${disabledAttr}></fig-fill-picker>`
4570
+ : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
4568
4571
  }
4569
4572
  }
4570
4573
  this.innerHTML = html;
@@ -4758,7 +4761,7 @@ class FigInputColor extends HTMLElement {
4758
4761
  }
4759
4762
 
4760
4763
  static get observedAttributes() {
4761
- return ["value", "style", "mode", "picker", "experimental", "alpha"];
4764
+ return ["value", "style", "mode", "picker", "experimental", "alpha", "text", "disabled"];
4762
4765
  }
4763
4766
 
4764
4767
  get mode() {
@@ -4809,8 +4812,29 @@ class FigInputColor extends HTMLElement {
4809
4812
  // Picker type change requires re-render
4810
4813
  break;
4811
4814
  case "alpha":
4815
+ case "text":
4812
4816
  if (this.isConnected) this.#buildUI();
4813
4817
  break;
4818
+ case "disabled":
4819
+ this.#syncDisabled();
4820
+ break;
4821
+ }
4822
+ }
4823
+
4824
+ get #disabled() {
4825
+ return this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
4826
+ }
4827
+
4828
+ #syncDisabled() {
4829
+ const disabled = this.#disabled;
4830
+ for (const child of [this.#swatch, this.#textInput, this.#alphaInput]) {
4831
+ if (!child) continue;
4832
+ if (disabled) child.setAttribute("disabled", "");
4833
+ else child.removeAttribute("disabled");
4834
+ }
4835
+ if (this.#fillPicker) {
4836
+ if (disabled) this.#fillPicker.setAttribute("disabled", "");
4837
+ else this.#fillPicker.removeAttribute("disabled");
4814
4838
  }
4815
4839
  }
4816
4840
 
@@ -5130,6 +5154,15 @@ class FigInputFill extends HTMLElement {
5130
5154
  .join(" ");
5131
5155
  }
5132
5156
 
5157
+ #syncDisabled() {
5158
+ const disabled = this.hasAttribute("disabled");
5159
+ for (const child of [this.#fillPicker, this.#opacityInput, this.#hexInput]) {
5160
+ if (!child) continue;
5161
+ if (disabled) child.setAttribute("disabled", "");
5162
+ else child.removeAttribute("disabled");
5163
+ }
5164
+ }
5165
+
5133
5166
  #render() {
5134
5167
  const disabled = this.hasAttribute("disabled");
5135
5168
  const fillPickerValue = JSON.stringify(this.value);
@@ -5662,10 +5695,7 @@ class FigInputFill extends HTMLElement {
5662
5695
  }
5663
5696
  break;
5664
5697
  case "disabled":
5665
- // Re-render to update disabled state
5666
- if (this.#fillPicker) {
5667
- this.#render();
5668
- }
5698
+ this.#syncDisabled();
5669
5699
  break;
5670
5700
  case "mode":
5671
5701
  case "experimental":
@@ -5683,6 +5713,325 @@ class FigInputFill extends HTMLElement {
5683
5713
  }
5684
5714
  customElements.define("fig-input-fill", FigInputFill);
5685
5715
 
5716
+ /* Input Palette */
5717
+ /**
5718
+ * A palette of solid colors, each rendered as a fig-input-color swatch.
5719
+ * Manages an internal array of colors with add support.
5720
+ * @attr {string} value - JSON array of hex strings or {color,alpha} objects, or comma-separated hex
5721
+ * @attr {boolean} disabled - Whether the palette is disabled
5722
+ * @attr {number} min - Minimum number of colors (default: 2)
5723
+ * @attr {number} max - Maximum number of colors (default: 8); add button hidden at max
5724
+ * @fires input - During color editing (detail: full color array)
5725
+ * @fires change - On committed color edits or add (detail: full color array)
5726
+ */
5727
+ class FigInputPalette extends HTMLElement {
5728
+ #colors = [];
5729
+ #inlinePickers = [];
5730
+ #expandedPickers = [];
5731
+ #renderRAF = null;
5732
+
5733
+ static get observedAttributes() {
5734
+ return ["value", "disabled", "min", "max", "expanded", "add"];
5735
+ }
5736
+
5737
+ get #expanded() {
5738
+ return this.hasAttribute("expanded") && this.getAttribute("expanded") !== "false";
5739
+ }
5740
+
5741
+ get #showAdd() {
5742
+ return !this.hasAttribute("add") || this.getAttribute("add") !== "false";
5743
+ }
5744
+
5745
+ get #min() {
5746
+ const v = parseInt(this.getAttribute("min"));
5747
+ return isNaN(v) ? 2 : v;
5748
+ }
5749
+
5750
+ get #max() {
5751
+ const v = parseInt(this.getAttribute("max"));
5752
+ return isNaN(v) ? 8 : v;
5753
+ }
5754
+
5755
+ connectedCallback() {
5756
+ if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
5757
+ this.#renderRAF = requestAnimationFrame(() => {
5758
+ this.#renderRAF = null;
5759
+ this.#parseValue();
5760
+ this.#render();
5761
+ });
5762
+ }
5763
+
5764
+ disconnectedCallback() {
5765
+ if (this.#renderRAF) {
5766
+ cancelAnimationFrame(this.#renderRAF);
5767
+ this.#renderRAF = null;
5768
+ }
5769
+ this.#inlinePickers = [];
5770
+ this.#expandedPickers = [];
5771
+ }
5772
+
5773
+ attributeChangedCallback(name, oldValue, newValue) {
5774
+ if (oldValue === newValue) return;
5775
+
5776
+ switch (name) {
5777
+ case "value":
5778
+ this.#parseValue();
5779
+ this.#syncPickers();
5780
+ break;
5781
+ case "disabled":
5782
+ this.#syncDisabled();
5783
+ break;
5784
+ case "min":
5785
+ case "max":
5786
+ case "expanded":
5787
+ case "add":
5788
+ this.#render();
5789
+ break;
5790
+ }
5791
+ }
5792
+
5793
+ #parseValue() {
5794
+ const raw = this.getAttribute("value");
5795
+ if (!raw) {
5796
+ this.#colors = [];
5797
+ return;
5798
+ }
5799
+
5800
+ const trimmed = raw.trim();
5801
+
5802
+ // Try JSON first
5803
+ try {
5804
+ const parsed = JSON.parse(trimmed);
5805
+ if (Array.isArray(parsed)) {
5806
+ this.#colors = parsed.map((entry) => {
5807
+ if (typeof entry === "string") {
5808
+ return { color: entry.slice(0, 7), alpha: entry.length > 7 ? parseInt(entry.slice(7, 9), 16) / 255 : 1 };
5809
+ }
5810
+ if (entry && typeof entry === "object") {
5811
+ return {
5812
+ color: entry.color || "#D9D9D9",
5813
+ alpha: entry.alpha !== undefined ? entry.alpha : (entry.opacity !== undefined ? entry.opacity / 100 : 1),
5814
+ };
5815
+ }
5816
+ return { color: "#D9D9D9", alpha: 1 };
5817
+ });
5818
+ return;
5819
+ }
5820
+ } catch (e) {
5821
+ // Not JSON — try comma-separated hex
5822
+ }
5823
+
5824
+ // Comma-separated hex
5825
+ if (trimmed.includes(",")) {
5826
+ this.#colors = trimmed.split(",").map((s) => {
5827
+ const hex = s.trim();
5828
+ return {
5829
+ color: hex.slice(0, 7),
5830
+ alpha: hex.length > 7 ? parseInt(hex.slice(7, 9), 16) / 255 : 1,
5831
+ };
5832
+ });
5833
+ return;
5834
+ }
5835
+
5836
+ // Single hex
5837
+ if (trimmed.startsWith("#")) {
5838
+ this.#colors = [{
5839
+ color: trimmed.slice(0, 7),
5840
+ alpha: trimmed.length > 7 ? parseInt(trimmed.slice(7, 9), 16) / 255 : 1,
5841
+ }];
5842
+ return;
5843
+ }
5844
+
5845
+ this.#colors = [];
5846
+ }
5847
+
5848
+ get value() {
5849
+ return this.#colors.map((c) => ({ ...c }));
5850
+ }
5851
+
5852
+ set value(val) {
5853
+ if (typeof val === "string") {
5854
+ this.setAttribute("value", val);
5855
+ } else {
5856
+ this.setAttribute("value", JSON.stringify(val));
5857
+ }
5858
+ }
5859
+
5860
+ #render() {
5861
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5862
+
5863
+ this.innerHTML = "";
5864
+ this.#inlinePickers = [];
5865
+ this.#expandedPickers = [];
5866
+
5867
+ const inlineWrap = document.createElement("div");
5868
+ inlineWrap.className = "palette-colors-inline";
5869
+
5870
+ const wrap = document.createElement("div");
5871
+ wrap.className = "palette-colors";
5872
+ this.#colors.forEach((entry, i) => {
5873
+ wrap.appendChild(this.#createPicker(entry, i, disabled, { inline: true }));
5874
+ });
5875
+ inlineWrap.appendChild(wrap);
5876
+
5877
+ if (this.#showAdd) this.#createAddButton(disabled, inlineWrap);
5878
+ this.appendChild(inlineWrap);
5879
+
5880
+ const expandedWrap = document.createElement("div");
5881
+ expandedWrap.className = "palette-colors-expanded";
5882
+ this.#colors.forEach((entry, i) => {
5883
+ expandedWrap.appendChild(this.#createPicker(entry, i, disabled));
5884
+ });
5885
+ this.appendChild(expandedWrap);
5886
+ }
5887
+
5888
+ #createPicker(entry, index, disabled, { inline = false } = {}) {
5889
+ const hexAlpha = entry.alpha < 1
5890
+ ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5891
+ : entry.color;
5892
+ const ic = document.createElement("fig-input-color");
5893
+ ic.setAttribute("value", hexAlpha);
5894
+ ic.setAttribute("picker", "figma");
5895
+ ic.setAttribute("picker-anchor", "self");
5896
+ if (inline) {
5897
+ ic.setAttribute("text", "false");
5898
+ ic.setAttribute("alpha", "true");
5899
+ } else {
5900
+ ic.setAttribute("text", "true");
5901
+ ic.setAttribute("alpha", "true");
5902
+ ic.setAttribute("full", "");
5903
+ }
5904
+ if (disabled) ic.setAttribute("disabled", "");
5905
+
5906
+ const siblingList = inline ? this.#expandedPickers : this.#inlinePickers;
5907
+
5908
+ const updateFromPicker = (e) => {
5909
+ e.stopPropagation();
5910
+ const el = e.currentTarget;
5911
+ this.#colors[index] = {
5912
+ color: el.hexOpaque || this.#colors[index].color,
5913
+ alpha: el.rgba ? el.rgba.a : this.#colors[index].alpha,
5914
+ };
5915
+ const sibling = siblingList[index];
5916
+ if (sibling) {
5917
+ const entry = this.#colors[index];
5918
+ const hex = entry.alpha < 1
5919
+ ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5920
+ : entry.color;
5921
+ sibling.setAttribute("value", hex);
5922
+ }
5923
+ };
5924
+
5925
+ ic.addEventListener("input", (e) => {
5926
+ updateFromPicker(e);
5927
+ this.#emitInput();
5928
+ });
5929
+
5930
+ ic.addEventListener("change", (e) => {
5931
+ updateFromPicker(e);
5932
+ this.#emitChange();
5933
+ });
5934
+
5935
+ if (inline) this.#inlinePickers.push(ic);
5936
+ else this.#expandedPickers.push(ic);
5937
+ return ic;
5938
+ }
5939
+
5940
+ #createAddButton(disabled, parent = this) {
5941
+ const atMax = this.#colors.length >= this.#max;
5942
+ const addBtn = document.createElement("fig-button");
5943
+ addBtn.setAttribute("variant", "ghost");
5944
+ addBtn.setAttribute("icon", "true");
5945
+ addBtn.setAttribute("aria-label", "Add color");
5946
+ addBtn.className = "palette-add-btn";
5947
+ if (disabled || atMax) addBtn.setAttribute("disabled", "");
5948
+ addBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>`;
5949
+ addBtn.addEventListener("click", () => {
5950
+ if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
5951
+ if (this.#colors.length >= this.#max) return;
5952
+ this.#addColor({ color: "#D9D9D9", alpha: 1 });
5953
+ });
5954
+ const tooltip = document.createElement("fig-tooltip");
5955
+ tooltip.setAttribute("text", "Add color");
5956
+ tooltip.appendChild(addBtn);
5957
+ parent.appendChild(tooltip);
5958
+ }
5959
+
5960
+ #addColor(entry) {
5961
+ this.#colors.push(entry);
5962
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5963
+ const index = this.#colors.length - 1;
5964
+
5965
+ const inlineIc = this.#createPicker(entry, index, disabled, { inline: true });
5966
+ const wrap = this.querySelector(".palette-colors");
5967
+ if (wrap) wrap.appendChild(inlineIc);
5968
+
5969
+ const expandedIc = this.#createPicker(entry, index, disabled);
5970
+ const expandedWrap = this.querySelector(".palette-colors-expanded");
5971
+ if (expandedWrap) expandedWrap.appendChild(expandedIc);
5972
+
5973
+ if (this.#colors.length >= this.#max) {
5974
+ const addBtn = this.querySelector(".palette-add-btn");
5975
+ if (addBtn) addBtn.setAttribute("disabled", "");
5976
+ }
5977
+ this.#emitChange();
5978
+ }
5979
+
5980
+ #updateChit(index) {
5981
+ const entry = this.#colors[index];
5982
+ if (!entry) return;
5983
+ const hexAlpha = entry.alpha < 1
5984
+ ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5985
+ : entry.color;
5986
+ const inl = this.#inlinePickers[index];
5987
+ if (inl) inl.setAttribute("value", hexAlpha);
5988
+ const exp = this.#expandedPickers[index];
5989
+ if (exp) exp.setAttribute("value", hexAlpha);
5990
+ }
5991
+
5992
+ #syncPickers() {
5993
+ if (this.#inlinePickers.length !== this.#colors.length) {
5994
+ this.#render();
5995
+ return;
5996
+ }
5997
+ this.#colors.forEach((_, i) => {
5998
+ this.#updateChit(i);
5999
+ });
6000
+ }
6001
+
6002
+ #syncDisabled() {
6003
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6004
+ [...this.#inlinePickers, ...this.#expandedPickers].forEach((fp) => {
6005
+ if (disabled) fp.setAttribute("disabled", "");
6006
+ else fp.removeAttribute("disabled");
6007
+ });
6008
+ const addBtn = this.querySelector(".palette-add-btn");
6009
+ if (addBtn) {
6010
+ if (disabled) addBtn.setAttribute("disabled", "");
6011
+ else addBtn.removeAttribute("disabled");
6012
+ }
6013
+ }
6014
+
6015
+ #emitInput() {
6016
+ this.dispatchEvent(
6017
+ new CustomEvent("input", {
6018
+ bubbles: true,
6019
+ detail: this.value,
6020
+ }),
6021
+ );
6022
+ }
6023
+
6024
+ #emitChange() {
6025
+ this.dispatchEvent(
6026
+ new CustomEvent("change", {
6027
+ bubbles: true,
6028
+ detail: this.value,
6029
+ }),
6030
+ );
6031
+ }
6032
+ }
6033
+ customElements.define("fig-input-palette", FigInputPalette);
6034
+
5686
6035
  /* Input Gradient */
5687
6036
  /**
5688
6037
  * A gradient-only fill input built on top of fig-fill-picker.
@@ -5692,6 +6041,7 @@ customElements.define("fig-input-fill", FigInputFill);
5692
6041
  * @fires change - When the gradient value is committed
5693
6042
  */
5694
6043
  class FigInputGradient extends HTMLElement {
6044
+ static SHIFT_SNAP = 5;
5695
6045
  #chit;
5696
6046
  #track;
5697
6047
  #handleDragging = false;
@@ -5763,7 +6113,7 @@ class FigInputGradient extends HTMLElement {
5763
6113
  const idx = parseInt(selected.dataset.stopIndex, 10);
5764
6114
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
5765
6115
  e.preventDefault();
5766
- const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? 10 : 1);
6116
+ const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? FigInputGradient.SHIFT_SNAP : 1);
5767
6117
  const stop = this.#gradient.stops[idx];
5768
6118
  stop.position = Math.max(0, Math.min(100, stop.position + delta));
5769
6119
  selected.setAttribute("value", `${stop.position}% 50%`);
@@ -5959,15 +6309,15 @@ class FigInputGradient extends HTMLElement {
5959
6309
  this.#syncChit();
5960
6310
  this.#emitInput();
5961
6311
  this.#emitChange();
5962
- this.#track?.querySelectorAll("fig-handle[selected]").forEach((h) => {
5963
- h.removeAttribute("selected");
5964
- });
5965
6312
  }
5966
6313
 
5967
6314
  #onTrackDblClick = (e) => {
5968
6315
  if (!this.#track) return;
5969
6316
  if (!e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
5970
6317
  this.#distributeStops();
6318
+ this.#track.querySelectorAll("fig-handle[selected]").forEach((h) => {
6319
+ h.removeAttribute("selected");
6320
+ });
5971
6321
  };
5972
6322
 
5973
6323
  #onTrackClick = (e) => {
@@ -5975,7 +6325,15 @@ class FigInputGradient extends HTMLElement {
5975
6325
  if (this.#handleDragging) return;
5976
6326
  if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
5977
6327
  if (e.shiftKey) {
6328
+ const clickedHandle = e.target.closest("fig-handle");
6329
+ const stopIdx = parseInt(clickedHandle?.dataset.stopIndex, 10);
5978
6330
  this.#distributeStops();
6331
+ if (!isNaN(stopIdx)) {
6332
+ this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6333
+ if (parseInt(h.dataset.stopIndex, 10) === stopIdx) h.select();
6334
+ else h.deselect();
6335
+ });
6336
+ }
5979
6337
  e.stopPropagation();
5980
6338
  }
5981
6339
  return;
@@ -6024,6 +6382,7 @@ class FigInputGradient extends HTMLElement {
6024
6382
  for (let i = 0; i < stops.length; i++) {
6025
6383
  const h = handles[i];
6026
6384
  const stop = stops[i];
6385
+ h.dataset.stopIndex = i;
6027
6386
  h.setAttribute("value", `${stop.position}% 50%`);
6028
6387
  h.setAttribute("color", stop.color);
6029
6388
  const tip = h.closest("fig-tooltip");
@@ -6104,7 +6463,7 @@ class FigInputGradient extends HTMLElement {
6104
6463
  let position = rawPosition;
6105
6464
  const trackW = this.#track.getBoundingClientRect().width;
6106
6465
  if (e.detail?.shiftKey) {
6107
- position = Math.round(position / 10) * 10;
6466
+ position = Math.round(position / FigInputGradient.SHIFT_SNAP) * FigInputGradient.SHIFT_SNAP;
6108
6467
  } else {
6109
6468
  const snapPct = trackW > 0 ? (5 / trackW) * 100 : 0;
6110
6469
  for (let i = 0; i < this.#gradient.stops.length; i++) {
@@ -6232,10 +6591,24 @@ class FigInputGradient extends HTMLElement {
6232
6591
  this.#syncHandles();
6233
6592
  break;
6234
6593
  case "disabled":
6235
- this.#render();
6594
+ this.#syncDisabled();
6236
6595
  break;
6237
6596
  }
6238
6597
  }
6598
+
6599
+ #syncDisabled() {
6600
+ const disabled = this.hasAttribute("disabled");
6601
+ if (this.#chit) {
6602
+ if (disabled) this.#chit.setAttribute("disabled", "");
6603
+ else this.#chit.removeAttribute("disabled");
6604
+ }
6605
+ if (this.#track) {
6606
+ for (const handle of this.#track.querySelectorAll("fig-handle")) {
6607
+ if (disabled) handle.setAttribute("disabled", "");
6608
+ else handle.removeAttribute("disabled");
6609
+ }
6610
+ }
6611
+ }
6239
6612
  }
6240
6613
  customElements.define("fig-input-gradient", FigInputGradient);
6241
6614
 
@@ -7214,11 +7587,10 @@ class FigEasingCurve extends HTMLElement {
7214
7587
  #bounds = null;
7215
7588
  #diagonal = null;
7216
7589
  #resizeObserver = null;
7217
- #bezierHandleRadius = 3.625;
7590
+ #bezierHandleRadius = 5;
7218
7591
  #bezierEndpointRadius = 2;
7219
- #springHandleRadius = 3.625;
7220
- #durationBarWidth = 6;
7221
- #durationBarHeight = 16;
7592
+ #durationBarWidth = 10;
7593
+ #durationBarHeight = 10;
7222
7594
  #durationBarRadius = 3;
7223
7595
 
7224
7596
  static PRESETS = [
@@ -7535,6 +7907,7 @@ class FigEasingCurve extends HTMLElement {
7535
7907
  this.#syncMetricsFromCSS();
7536
7908
  this.innerHTML = this.#getInnerHTML();
7537
7909
  this.#cacheRefs();
7910
+ this.#syncHandleSizes();
7538
7911
  this.#syncViewportSize();
7539
7912
  this.#updatePaths();
7540
7913
  this.#setupEvents();
@@ -7574,8 +7947,6 @@ class FigEasingCurve extends HTMLElement {
7574
7947
  const size = 200;
7575
7948
  const dropdown = this.#getDropdownHTML();
7576
7949
 
7577
- const hs = this.#bezierHandleRadius * 2;
7578
-
7579
7950
  if (this.#mode === "spring") {
7580
7951
  const targetY = 40;
7581
7952
  const startY = 180;
@@ -7584,8 +7955,8 @@ class FigEasingCurve extends HTMLElement {
7584
7955
  <line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
7585
7956
  <line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
7586
7957
  <path class="fig-easing-curve-path"/>
7587
- <foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="${hs}" height="${hs}"><fig-handle></fig-handle></foreignObject>
7588
- <foreignObject class="fig-easing-curve-handle fig-easing-curve-duration-bar" data-handle="duration" width="${this.#durationBarWidth}" height="${this.#durationBarHeight}"><fig-handle></fig-handle></foreignObject>
7958
+ <foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
7959
+ <foreignObject class="fig-easing-curve-handle fig-easing-curve-duration-bar" data-handle="duration" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
7589
7960
  </svg></div>`;
7590
7961
  }
7591
7962
 
@@ -7597,8 +7968,8 @@ class FigEasingCurve extends HTMLElement {
7597
7968
  <path class="fig-easing-curve-path"/>
7598
7969
  <circle class="fig-easing-curve-endpoint" data-endpoint="start" r="${this.#bezierEndpointRadius}"/>
7599
7970
  <circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
7600
- <foreignObject class="fig-easing-curve-handle" data-handle="1" width="${hs}" height="${hs}"><fig-handle></fig-handle></foreignObject>
7601
- <foreignObject class="fig-easing-curve-handle" data-handle="2" width="${hs}" height="${hs}"><fig-handle></fig-handle></foreignObject>
7971
+ <foreignObject class="fig-easing-curve-handle" data-handle="1" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
7972
+ <foreignObject class="fig-easing-curve-handle" data-handle="2" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
7602
7973
  </svg></div>`;
7603
7974
  }
7604
7975
 
@@ -7610,26 +7981,10 @@ class FigEasingCurve extends HTMLElement {
7610
7981
  }
7611
7982
 
7612
7983
  #syncMetricsFromCSS() {
7613
- this.#bezierHandleRadius = this.#readCssNumber(
7614
- "--easing-bezier-handle-radius",
7615
- this.#bezierHandleRadius,
7616
- );
7617
7984
  this.#bezierEndpointRadius = this.#readCssNumber(
7618
7985
  "--easing-bezier-endpoint-radius",
7619
7986
  this.#bezierEndpointRadius,
7620
7987
  );
7621
- this.#springHandleRadius = this.#readCssNumber(
7622
- "--easing-spring-handle-radius",
7623
- this.#springHandleRadius,
7624
- );
7625
- this.#durationBarWidth = this.#readCssNumber(
7626
- "--easing-duration-bar-width",
7627
- this.#durationBarWidth,
7628
- );
7629
- this.#durationBarHeight = this.#readCssNumber(
7630
- "--easing-duration-bar-height",
7631
- this.#durationBarHeight,
7632
- );
7633
7988
  this.#durationBarRadius = this.#readCssNumber(
7634
7989
  "--easing-duration-bar-radius",
7635
7990
  this.#durationBarRadius,
@@ -7655,6 +8010,28 @@ class FigEasingCurve extends HTMLElement {
7655
8010
  this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
7656
8011
  }
7657
8012
 
8013
+ #syncHandleSizes() {
8014
+ const h1El = this.#handle1?.querySelector("fig-handle");
8015
+ const h2El = this.#handle2?.querySelector("fig-handle");
8016
+ if (h1El) {
8017
+ const w = h1El.offsetWidth || this.#bezierHandleRadius * 2;
8018
+ const h = h1El.offsetHeight || this.#bezierHandleRadius * 2;
8019
+ this.#bezierHandleRadius = Math.max(w, h) / 2;
8020
+ this.#handle1.setAttribute("width", w);
8021
+ this.#handle1.setAttribute("height", h);
8022
+ }
8023
+ if (h2El) {
8024
+ const w = h2El.offsetWidth || this.#durationBarWidth;
8025
+ const h = h2El.offsetHeight || this.#durationBarHeight;
8026
+ if (this.#mode === "spring") {
8027
+ this.#durationBarWidth = w;
8028
+ this.#durationBarHeight = h;
8029
+ }
8030
+ this.#handle2.setAttribute("width", w);
8031
+ this.#handle2.setAttribute("height", h);
8032
+ }
8033
+ }
8034
+
7658
8035
  #setupResizeObserver() {
7659
8036
  if (this.#resizeObserver || !window.ResizeObserver) return;
7660
8037
  this.#resizeObserver = new ResizeObserver(() => {
@@ -10075,6 +10452,7 @@ class FigFillPicker extends HTMLElement {
10075
10452
  this.#teardownColorAreaEvents();
10076
10453
  this.#teardownColorAreaEvents = null;
10077
10454
  }
10455
+ if (this.#chit) this.#chit.removeAttribute("selected");
10078
10456
  if (this.#dialog) {
10079
10457
  this.#dialog.close();
10080
10458
  this.#dialog.remove();
@@ -10252,6 +10630,8 @@ class FigFillPicker extends HTMLElement {
10252
10630
  const gamutEl = this.#dialog.querySelector(".fig-fill-picker-gamut");
10253
10631
  if (gamutEl) gamutEl.value = this.#gamut;
10254
10632
 
10633
+ if (this.#chit) this.#chit.setAttribute("selected", "true");
10634
+
10255
10635
  this.#dialog.open = true;
10256
10636
 
10257
10637
  requestAnimationFrame(() => {
@@ -10406,9 +10786,17 @@ class FigFillPicker extends HTMLElement {
10406
10786
  this.#dialog.open = false;
10407
10787
  });
10408
10788
 
10409
- this.#dialog.addEventListener("close", () => {
10789
+ const onDialogClose = () => {
10790
+ if (this.#chit) this.#chit.removeAttribute("selected");
10410
10791
  this.#emitChange();
10792
+ };
10793
+ this.#dialog.addEventListener("close", onDialogClose);
10794
+
10795
+ const observer = new MutationObserver(() => {
10796
+ const isOpen = this.#dialog.hasAttribute("open") && this.#dialog.getAttribute("open") !== "false";
10797
+ if (!isOpen) onDialogClose();
10411
10798
  });
10799
+ observer.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
10412
10800
 
10413
10801
  // Initialize built-in tabs (skip any overridden by custom slots)
10414
10802
  const builtinInits = {
@@ -12409,7 +12797,7 @@ class FigChooser extends HTMLElement {
12409
12797
  }
12410
12798
 
12411
12799
  static get observedAttributes() {
12412
- return ["value", "disabled", "choice-element", "drag", "overflow", "loop"];
12800
+ return ["value", "disabled", "choice-element", "drag", "overflow", "loop", "padding"];
12413
12801
  }
12414
12802
 
12415
12803
  get #overflowMode() {
@@ -13045,8 +13433,8 @@ class FigHandle extends HTMLElement {
13045
13433
  };
13046
13434
 
13047
13435
  const axes = this.#axes;
13048
- if (axes.x) this.style.left = `${resolve(xToken, rect.width, hw)}px`;
13049
- if (axes.y) this.style.top = `${resolve(yToken, rect.height, hh)}px`;
13436
+ if (axes.x) this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
13437
+ if (axes.y) this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
13050
13438
  }
13051
13439
 
13052
13440
  #syncValueAttribute() {
@@ -13059,6 +13447,7 @@ class FigHandle extends HTMLElement {
13059
13447
  this.#syncDrag();
13060
13448
  this.addEventListener("click", this.#handleSelect);
13061
13449
  document.addEventListener("pointerdown", this.#handleDeselect);
13450
+ document.addEventListener("keydown", this.#handleKeyDown);
13062
13451
  const initial = this.getAttribute("value");
13063
13452
  if (initial) this.#applyValue(initial);
13064
13453
  if (this.#hasControlMode && !this.#isGhost) this.#showColorTip();
@@ -13069,12 +13458,13 @@ class FigHandle extends HTMLElement {
13069
13458
  this.#hideColorTip();
13070
13459
  this.removeEventListener("click", this.#handleSelect);
13071
13460
  document.removeEventListener("pointerdown", this.#handleDeselect);
13461
+ document.removeEventListener("keydown", this.#handleKeyDown);
13072
13462
  }
13073
13463
 
13074
13464
  select() {
13075
13465
  if (this.hasAttribute("disabled")) return;
13076
13466
  this.setAttribute("selected", "");
13077
- if (this.getAttribute("type") === "color") this.#showColorTip();
13467
+ if (this.getAttribute("type") === "color" && !this.#isDragging) this.#showColorTip();
13078
13468
  }
13079
13469
 
13080
13470
  deselect() {
@@ -13098,6 +13488,15 @@ class FigHandle extends HTMLElement {
13098
13488
  this.deselect();
13099
13489
  };
13100
13490
 
13491
+ #handleKeyDown = (e) => {
13492
+ if (e.key !== "Enter") return;
13493
+ if (!this.hasAttribute("selected")) return;
13494
+ if (this.getAttribute("type") !== "color") return;
13495
+ if (this.#colorTip) return;
13496
+ e.preventDefault();
13497
+ this.#showColorTip();
13498
+ };
13499
+
13101
13500
  attributeChangedCallback(name, _old, value) {
13102
13501
  if (name === "color") {
13103
13502
  if (!value || value === "false" || value === "true") {
@@ -13105,6 +13504,9 @@ class FigHandle extends HTMLElement {
13105
13504
  } else {
13106
13505
  this.style.setProperty("--fill", value);
13107
13506
  }
13507
+ if (this.#colorTip && value) {
13508
+ this.#colorTip.setAttribute("value", value);
13509
+ }
13108
13510
  }
13109
13511
  if (name === "drag") this.#syncDrag();
13110
13512
  if (name === "value" && !this.#applyingValue && !this.#isDragging) {
@@ -13186,11 +13588,11 @@ class FigHandle extends HTMLElement {
13186
13588
 
13187
13589
  if (axes.x) {
13188
13590
  const left = centerX * rect.width - handleW / 2;
13189
- this.style.left = `${Math.max(-handleW / 2, Math.min(rect.width - handleW / 2, left))}px`;
13591
+ this.style.left = `${Math.round(Math.max(-handleW / 2, Math.min(rect.width - handleW / 2, left)))}px`;
13190
13592
  }
13191
13593
  if (axes.y) {
13192
13594
  const top = centerY * rect.height - handleH / 2;
13193
- this.style.top = `${Math.max(-handleH / 2, Math.min(rect.height - handleH / 2, top))}px`;
13595
+ this.style.top = `${Math.round(Math.max(-handleH / 2, Math.min(rect.height - handleH / 2, top)))}px`;
13194
13596
  }
13195
13597
  };
13196
13598
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.14.1",
3
+ "version": "3.16.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",