@rogieking/figui3 4.1.4 → 4.3.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 +211 -207
  2. package/fig.js +380 -2
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -1036,7 +1036,6 @@ fig-tab,
1036
1036
  }
1037
1037
  }
1038
1038
 
1039
- &:has(:checked),
1040
1039
  &[selected]:not([selected="false"]) {
1041
1040
  background-color: var(--figma-color-bg-secondary);
1042
1041
  font-weight: var(--body-medium-strong-fontWeight);
@@ -1046,22 +1045,13 @@ fig-tab,
1046
1045
  &:hover {
1047
1046
  background-color: var(--figma-color-bg-secondary);
1048
1047
  }
1049
-
1050
- & [type="radio"] {
1051
- position: absolute;
1052
- inset: 0;
1053
- border-radius: 0;
1054
- opacity: 0;
1055
- z-index: 1;
1056
- width: 100%;
1057
- height: 100%;
1058
- }
1059
1048
  }
1060
1049
 
1061
1050
  fig-tabs,
1062
1051
  .tabs {
1063
1052
  display: flex;
1064
- gap: var(--spacer-2);
1053
+ gap: var(--spacer-1);
1054
+ padding: var(--spacer-1) var(--spacer-2);
1065
1055
  }
1066
1056
 
1067
1057
  fig-tab-content,
@@ -1990,52 +1980,52 @@ input[type="checkbox"].switch {
1990
1980
  transition: var(--input-transition);
1991
1981
  box-shadow: var(--handle-shadow);
1992
1982
  }
1993
- }
1994
-
1995
- input[type="checkbox"].switch[indeterminate="true"]:after {
1996
- width: 0.625rem;
1997
- height: 0.125rem;
1998
- transform: none;
1999
- }
2000
1983
 
2001
- input[type="checkbox"].switch[indeterminate="true"],
2002
- input[type="checkbox"].switch:checked {
2003
- background-color: var(--figma-color-bg-brand);
2004
- }
1984
+ &[indeterminate="true"]:after {
1985
+ width: 0.625rem;
1986
+ height: 0.125rem;
1987
+ transform: none;
1988
+ }
2005
1989
 
2006
- input[type="checkbox"].switch:checked:after {
2007
- transform: translate(
2008
- calc(
2009
- (var(--width) - var(--knob-width)) * 0.5 +
2010
- (var(--height) - var(--knob-height)) * -0.5
2011
- )
2012
- );
2013
- }
1990
+ &[indeterminate="true"],
1991
+ &:checked {
1992
+ background-color: var(--figma-color-bg-brand);
1993
+ }
2014
1994
 
2015
- input[type="checkbox"].switch:disabled {
2016
- background-color: transparent;
2017
- cursor: not-allowed;
2018
- &:after {
2019
- background-color: var(--figma-color-icon-disabled);
2020
- box-shadow: none;
1995
+ &:checked:after {
1996
+ transform: translate(
1997
+ calc(
1998
+ (var(--width) - var(--knob-width)) * 0.5 +
1999
+ (var(--height) - var(--knob-height)) * -0.5
2000
+ )
2001
+ );
2021
2002
  }
2022
2003
 
2023
- &:checked,
2024
- &[indeterminate="true"] {
2025
- background-color: var(--figma-color-bg-disabled);
2026
- box-shadow: none;
2004
+ &:disabled {
2005
+ background-color: transparent;
2006
+ cursor: not-allowed;
2027
2007
  &:after {
2028
- background-color: var(--figma-color-bg);
2008
+ background-color: var(--figma-color-icon-disabled);
2009
+ box-shadow: none;
2010
+ }
2011
+
2012
+ &:checked,
2013
+ &[indeterminate="true"] {
2014
+ background-color: var(--figma-color-bg-disabled);
2015
+ box-shadow: none;
2016
+ &:after {
2017
+ background-color: var(--figma-color-bg);
2018
+ }
2029
2019
  }
2030
2020
  }
2031
- }
2032
2021
 
2033
- input[type="checkbox"].switch:focus {
2034
- outline: 0;
2035
- }
2022
+ &:focus {
2023
+ outline: 0;
2024
+ }
2036
2025
 
2037
- input[type="checkbox"].switch:checked:focus {
2038
- outline: 0;
2026
+ &:checked:focus {
2027
+ outline: 0;
2028
+ }
2039
2029
  }
2040
2030
 
2041
2031
  /* Checkbox */
@@ -2143,6 +2133,22 @@ input[type="radio"] {
2143
2133
  }
2144
2134
  }
2145
2135
 
2136
+ &:disabled {
2137
+ background-color: transparent;
2138
+ cursor: not-allowed;
2139
+ &:after {
2140
+ background-color: var(--figma-color-icon-disabled);
2141
+ box-shadow: none;
2142
+ }
2143
+ &:checked {
2144
+ background-color: var(--figma-color-bg-disabled);
2145
+ box-shadow: none;
2146
+ &:after {
2147
+ background-color: var(--figma-color-icon-disabled);
2148
+ }
2149
+ }
2150
+ }
2151
+
2146
2152
  &::after {
2147
2153
  content: "";
2148
2154
  width: 0.375rem;
@@ -2242,7 +2248,6 @@ details {
2242
2248
  }
2243
2249
 
2244
2250
  /* Sliders */
2245
- .fig-slider,
2246
2251
  fig-slider {
2247
2252
  --slider-field-height: 1.5rem;
2248
2253
  --slider-height: 1rem;
@@ -2274,17 +2279,28 @@ fig-slider {
2274
2279
  --slider-transition: none;
2275
2280
  --handle-transition: var(--slider-transition);
2276
2281
 
2277
- display: inline-flex;
2282
+ display: flex;
2278
2283
  align-items: center;
2279
2284
  position: relative;
2285
+ min-width: 0;
2280
2286
  height: var(--slider-field-height);
2281
2287
  transition: var(--slider-transition);
2282
2288
 
2289
+ & .slider {
2290
+ flex: 1 1 0;
2291
+ min-width: 0;
2292
+ }
2293
+
2294
+ & fig-input-number {
2295
+ flex: 0 0 3rem;
2296
+ }
2297
+
2283
2298
  .fig-slider-input-container {
2284
2299
  height: var(--slider-height);
2285
2300
  position: relative;
2286
2301
  display: block;
2287
- width: 100%;
2302
+ flex: 1 1 0;
2303
+ min-width: 0;
2288
2304
  transition: var(--slider-transition);
2289
2305
  background: var(--figma-color-bg-secondary);
2290
2306
  border-radius: var(--slider-border-radius);
@@ -2356,31 +2372,36 @@ fig-slider {
2356
2372
  }
2357
2373
  }
2358
2374
 
2359
- /* Chromium */
2360
2375
  input[type="range"] {
2361
2376
  height: var(--slider-height);
2362
2377
  appearance: none;
2363
2378
  -webkit-appearance: none;
2379
+ -moz-appearance: none;
2364
2380
  border-radius: var(--slider-border-radius);
2365
2381
  display: block;
2366
2382
  width: 100%;
2383
+ min-width: 0;
2367
2384
  background-color: transparent;
2368
2385
  transition: var(--slider-transition);
2369
2386
  position: relative;
2370
2387
 
2371
- &:active::-webkit-slider-thumb {
2372
- cursor: grabbing;
2373
- cursor: -webkit-grabbing;
2374
- }
2375
-
2376
2388
  &:focus {
2377
2389
  outline: none;
2378
2390
  }
2379
2391
 
2392
+ &:disabled {
2393
+ cursor: not-allowed;
2394
+ }
2395
+
2396
+ /* Chromium thumb */
2397
+ &:active::-webkit-slider-thumb {
2398
+ cursor: grabbing;
2399
+ }
2400
+
2380
2401
  &::-webkit-slider-thumb {
2381
2402
  appearance: none;
2382
- background: transparent;
2383
2403
  -webkit-appearance: none;
2404
+ background: transparent;
2384
2405
  color-scheme: inherit;
2385
2406
  transition: var(--handle-transition);
2386
2407
  border-radius: var(--slider-thumb-radius);
@@ -2391,7 +2412,6 @@ fig-slider {
2391
2412
  );
2392
2413
  outline: var(--slider-thumb-outline);
2393
2414
  outline-offset: var(--slider-thumb-outline-offset);
2394
-
2395
2415
  aspect-ratio: 1;
2396
2416
  border: none;
2397
2417
  position: relative;
@@ -2401,22 +2421,8 @@ fig-slider {
2401
2421
  opacity: var(--slider-thumb-opacity);
2402
2422
  }
2403
2423
 
2404
- &:disabled {
2405
- cursor: not-allowed;
2406
-
2407
- &::-webkit-slider-runnable-track {
2408
- color-scheme: inherit;
2409
- background: linear-gradient(
2410
- to right,
2411
- var(--figma-color-bg-secondary) 0%,
2412
- var(--figma-color-bg-secondary) var(--slider-percent),
2413
- var(--figma-color-bg) var(--slider-percent)
2414
- );
2415
- }
2416
-
2417
- &::-webkit-slider-thumb {
2418
- box-shadow: inset 0 0 0 1rem var(--figma-color-bg-disabled);
2419
- }
2424
+ &:disabled::-webkit-slider-thumb {
2425
+ box-shadow: inset 0 0 0 1rem var(--figma-color-bg-disabled);
2420
2426
  }
2421
2427
 
2422
2428
  &::-webkit-slider-runnable-track {
@@ -2427,6 +2433,15 @@ fig-slider {
2427
2433
  border-radius: var(--slider-border-radius);
2428
2434
  }
2429
2435
 
2436
+ &:disabled::-webkit-slider-runnable-track {
2437
+ background: linear-gradient(
2438
+ to right,
2439
+ var(--figma-color-bg-secondary) 0%,
2440
+ var(--figma-color-bg-secondary) var(--slider-percent),
2441
+ var(--figma-color-bg) var(--slider-percent)
2442
+ );
2443
+ }
2444
+
2430
2445
  &.hue::-webkit-slider-runnable-track {
2431
2446
  background: var(--bg-hue);
2432
2447
  }
@@ -2436,34 +2451,18 @@ fig-slider {
2436
2451
  linear-gradient(to right, transparent, var(--color)),
2437
2452
  var(--checkerboard);
2438
2453
  }
2439
- }
2440
-
2441
- /* Firefox */
2442
- input[type="range"] {
2443
- height: var(--slider-height);
2444
- appearance: none;
2445
- -moz-appearance: none;
2446
- border-radius: var(--slider-border-radius);
2447
- display: block;
2448
- width: 100%;
2449
- background-color: transparent;
2450
- transition: var(--slider-transition);
2451
- position: relative;
2452
-
2453
- &:focus {
2454
- outline: none;
2455
- }
2456
2454
 
2455
+ /* Firefox thumb */
2457
2456
  &:active::-moz-range-thumb {
2458
2457
  cursor: grabbing;
2459
- cursor: -webkit-grabbing;
2460
2458
  }
2461
2459
 
2462
2460
  &::-moz-range-thumb {
2463
- color-scheme: inherit;
2464
2461
  appearance: none;
2465
2462
  -moz-appearance: none;
2466
2463
  background: transparent;
2464
+ color-scheme: inherit;
2465
+ transition: var(--handle-transition);
2467
2466
  border-radius: var(--slider-thumb-radius);
2468
2467
  height: var(--slider-thumb-height);
2469
2468
  width: var(--slider-thumb-width);
@@ -2475,26 +2474,11 @@ fig-slider {
2475
2474
  z-index: 1;
2476
2475
  cursor: default;
2477
2476
  box-shadow: var(--slider-handle-shadow);
2478
- transition: var(--handle-transition);
2479
2477
  opacity: var(--slider-thumb-opacity);
2480
2478
  }
2481
2479
 
2482
- &:disabled {
2483
- cursor: not-allowed;
2484
-
2485
- &::-moz-range-track {
2486
- color-scheme: inherit;
2487
- background: linear-gradient(
2488
- to right,
2489
- var(--figma-color-bg-secondary) 0%,
2490
- var(--figma-color-bg-secondary) var(--slider-percent),
2491
- var(--figma-color-bg) var(--slider-percent)
2492
- );
2493
- }
2494
-
2495
- &::-moz-range-thumb {
2496
- box-shadow: inset 0 0 0 1rem var(--figma-color-bg-disabled);
2497
- }
2480
+ &:disabled::-moz-range-thumb {
2481
+ box-shadow: inset 0 0 0 1rem var(--figma-color-bg-disabled);
2498
2482
  }
2499
2483
 
2500
2484
  &::-moz-range-track {
@@ -2504,6 +2488,15 @@ fig-slider {
2504
2488
  transition: var(--slider-transition);
2505
2489
  }
2506
2490
 
2491
+ &:disabled::-moz-range-track {
2492
+ background: linear-gradient(
2493
+ to right,
2494
+ var(--figma-color-bg-secondary) 0%,
2495
+ var(--figma-color-bg-secondary) var(--slider-percent),
2496
+ var(--figma-color-bg) var(--slider-percent)
2497
+ );
2498
+ }
2499
+
2507
2500
  &.hue::-moz-range-track {
2508
2501
  background: var(--bg-hue);
2509
2502
  }
@@ -2584,7 +2577,7 @@ fig-slider {
2584
2577
  }
2585
2578
  fig-input-text,
2586
2579
  fig-input-number {
2587
- flex-basis: 5rem;
2580
+ flex-basis: 3rem;
2588
2581
  border-top-left-radius: 0;
2589
2582
  border-bottom-left-radius: 0;
2590
2583
  border-left: 1px solid var(--figma-color-bg);
@@ -2617,7 +2610,8 @@ dialog,
2617
2610
 
2618
2611
  box-shadow: var(--figma-elevation-500-modal-window);
2619
2612
 
2620
- footer {
2613
+ footer,
2614
+ fig-footer {
2621
2615
  display: flex;
2622
2616
  justify-content: flex-end;
2623
2617
  padding: var(--spacer-2);
@@ -2693,6 +2687,19 @@ dialog,
2693
2687
  dialog[is="fig-dialog"] {
2694
2688
  --z-index: 999999;
2695
2689
  z-index: var(--z-index);
2690
+
2691
+ &[resizable]:not([resizable="false"]) {
2692
+ resize: both;
2693
+ overflow: auto;
2694
+ min-width: 12rem;
2695
+ min-height: 6rem;
2696
+ }
2697
+ & > fig-header[dialog-header]:not([borderless]):not([borderless="false"]) {
2698
+ margin-bottom: var(--spacer-2-5);
2699
+ }
2700
+ & > fig-footer:not([borderless]):not([borderless="false"]) {
2701
+ margin-top: var(--spacer-2-5);
2702
+ }
2696
2703
  }
2697
2704
 
2698
2705
  dialog[is="fig-popup"] {
@@ -2948,7 +2955,6 @@ fig-input-fill,
2948
2955
  fig-checkbox,
2949
2956
  fig-radio,
2950
2957
  fig-tab,
2951
- fig-tabs,
2952
2958
  fig-segmented-control,
2953
2959
  fig-input-palette {
2954
2960
  display: inline-flex;
@@ -3013,7 +3019,7 @@ fig-header {
3013
3019
 
3014
3020
  fig-group {
3015
3021
  display: block;
3016
- padding-bottom: var(--spacer-2-5);
3022
+ margin-bottom: var(--spacer-2-5);
3017
3023
 
3018
3024
  /* Sibling groups */
3019
3025
  & + fig-group {
@@ -3021,9 +3027,6 @@ fig-group {
3021
3027
  &:not([name]) {
3022
3028
  padding-top: var(--spacer-2-5);
3023
3029
  }
3024
- &:last-of-type {
3025
- padding-bottom: 0;
3026
- }
3027
3030
  }
3028
3031
 
3029
3032
  & > fig-header {
@@ -3048,7 +3051,7 @@ fig-group {
3048
3051
 
3049
3052
  &[collapsible]:not([open]):not([open="true"]),
3050
3053
  &[collapsible][open="false"] {
3051
- padding-bottom: 0;
3054
+ margin-bottom: 0;
3052
3055
  fig-header {
3053
3056
  color: var(--figma-color-text-secondary);
3054
3057
  &:hover {
@@ -3412,7 +3415,7 @@ fig-input-palette {
3412
3415
  }
3413
3416
  &[open]:not([open="false"]):not([edit="false"]) {
3414
3417
  .palette-colors-expanded {
3415
- display: flex;
3418
+ display: grid;
3416
3419
  }
3417
3420
  }
3418
3421
  &[add="false"] {
@@ -3423,7 +3426,7 @@ fig-input-palette {
3423
3426
  }
3424
3427
  .palette-colors-expanded {
3425
3428
  display: none;
3426
- flex-direction: column;
3429
+ grid-template-columns: [input] 1fr [button] 1.5rem;
3427
3430
  overflow: visible;
3428
3431
  border-radius: 0;
3429
3432
  gap: var(--spacer-2);
@@ -3431,6 +3434,16 @@ fig-input-palette {
3431
3434
 
3432
3435
  > fig-input-color {
3433
3436
  min-width: 0;
3437
+ grid-column: input / -1;
3438
+
3439
+ &:has(+ fig-button) {
3440
+ grid-column: input;
3441
+ }
3442
+ }
3443
+
3444
+ > fig-button,
3445
+ > button {
3446
+ grid-column: button;
3434
3447
  }
3435
3448
 
3436
3449
  fig-chit {
@@ -3442,39 +3455,27 @@ fig-input-palette {
3442
3455
  }
3443
3456
  }
3444
3457
 
3445
- fig-slider {
3446
- display: flex;
3447
-
3448
- & .slider {
3449
- flex-grow: 1;
3450
- }
3451
-
3452
- & fig-input-number {
3453
- flex-basis: 5rem;
3454
- }
3455
-
3456
- fig-field[direction="horizontal"] & {
3457
- flex: 1;
3458
- min-width: 0;
3459
- }
3460
- }
3461
-
3462
- fig-field,
3463
- .fig-field {
3464
- --field-label-width: 4rem;
3465
- display: flex;
3466
- padding: var(--spacer-1) var(--spacer-3);
3458
+ fig-field {
3459
+ --fig-field-gap: var(--spacer-2);
3460
+ --fig-field-left-padding: var(--spacer-3);
3461
+ --fig-field-right-padding: var(--spacer-3);
3462
+ --fig-field-top-padding: var(--spacer-1);
3463
+ --fig-field-bottom-padding: var(--spacer-1);
3464
+ --fig-field-label-ratio: 1fr;
3465
+ --fig-field-input-ratio: 2fr;
3466
+ display: grid;
3467
+ grid-template-columns: var(--fig-field-left-padding) 1fr var(
3468
+ --fig-field-right-padding
3469
+ );
3470
+ grid-template-rows: auto auto;
3471
+ grid-template-areas:
3472
+ "chevron label pad"
3473
+ ". input pad";
3467
3474
  margin: 0;
3468
- flex-direction: column;
3475
+ padding: var(--fig-field-top-padding) 0 var(--fig-field-bottom-padding) 0;
3469
3476
  gap: 0;
3470
3477
  align-items: start;
3471
3478
 
3472
- &[direction="horizontal"] {
3473
- flex-direction: row;
3474
- align-items: center;
3475
- gap: var(--spacer-2);
3476
- }
3477
-
3478
3479
  & > [full] {
3479
3480
  flex: 1 1 auto;
3480
3481
  }
@@ -3485,15 +3486,17 @@ fig-field,
3485
3486
  }
3486
3487
 
3487
3488
  & > label {
3488
- flex: 0;
3489
+ grid-area: label;
3490
+ display: block;
3489
3491
  padding: var(--spacer-1) 0;
3490
- min-height: calc(1rem + var(--spacer-1) * 2);
3491
- display: flex;
3492
- align-items: center;
3493
- width: 100%;
3492
+ line-height: 1rem;
3494
3493
  user-select: none;
3495
3494
  }
3496
3495
 
3496
+ & > *:not(.fig-field-chevron):not(label) {
3497
+ grid-area: input;
3498
+ }
3499
+
3497
3500
  & > .fig-field-chevron {
3498
3501
  --icon: var(--icon-chevron);
3499
3502
  --size: 1rem;
@@ -3502,64 +3505,37 @@ fig-field,
3502
3505
  transition: transform var(--transition-duration)
3503
3506
  var(--transition-timing-function);
3504
3507
  flex-shrink: 0;
3505
- margin: var(--spacer-1) 0;
3506
- margin-left: calc(var(--spacer-3) * -1);
3507
- margin-right: calc(var(--spacer-2) * -1);
3508
+ grid-area: chevron;
3509
+ margin-top: var(--spacer-1);
3508
3510
  }
3509
3511
 
3510
3512
  &:has(> [open]:not([open="false"])) > .fig-field-chevron {
3511
3513
  transform: rotate(0deg);
3512
3514
  }
3513
3515
 
3514
- &[direction="horizontal"] > label {
3515
- display: block;
3516
- width: auto;
3517
- min-width: var(--field-label-width);
3518
- max-width: var(--field-label-width);
3519
- overflow: hidden;
3520
- text-overflow: ellipsis;
3521
- white-space: nowrap;
3522
- padding: 0;
3523
- line-height: 1.5rem;
3524
- }
3525
-
3526
3516
  &[direction="horizontal"] {
3527
- gap: var(--spacer-2);
3517
+ display: grid;
3518
+ grid-template-columns:
3519
+ var(--fig-field-left-padding) minmax(0, var(--fig-field-label-ratio))
3520
+ minmax(0, var(--fig-field-input-ratio)) var(--fig-field-right-padding);
3521
+ grid-template-areas: "chevron label input pad";
3522
+ gap: 0;
3528
3523
  align-items: start;
3529
- flex-direction: row;
3530
- &[columns="thirds"] {
3531
- display: grid;
3532
- grid-template-columns: repeat(3, 1fr);
3533
- gap: var(--spacer-2);
3534
-
3535
- & > label {
3536
- grid-column: 1;
3537
- }
3538
-
3539
- & > label ~ * {
3540
- grid-column: 2 / span 2;
3541
- }
3542
3524
 
3543
- &:not(:has(> label)) > * {
3544
- grid-column: 1 / -1;
3545
- }
3546
- }
3547
3525
  &[columns="half"] {
3548
- display: grid;
3549
- grid-template-columns: repeat(2, 1fr);
3550
- gap: var(--spacer-2);
3551
-
3552
- & > label {
3553
- grid-column: 1;
3554
- }
3555
-
3556
- & > label ~ * {
3557
- grid-column: 2 / span 2;
3558
- }
3559
-
3560
- &:not(:has(> label)) > * {
3561
- grid-column: 1 / -1;
3562
- }
3526
+ --fig-field-label-ratio: 2fr;
3527
+ }
3528
+ & > label {
3529
+ overflow: hidden;
3530
+ text-overflow: ellipsis;
3531
+ white-space: nowrap;
3532
+ padding-right: var(--fig-field-gap);
3533
+ min-width: 0;
3534
+ }
3535
+ /* If the field has no label, set the input ratio to 1fr */
3536
+ &:not(:has(> label)) {
3537
+ --fig-field-input-ratio: 1fr;
3538
+ --fig-field-label-ratio: 0fr;
3563
3539
  }
3564
3540
  }
3565
3541
  }
@@ -3617,14 +3593,18 @@ fig-segmented-control {
3617
3593
 
3618
3594
  & fig-segment {
3619
3595
  flex: 1 1 0;
3620
- display: flex;
3621
- align-items: center;
3622
- justify-content: center;
3596
+ min-width: 0;
3597
+ display: block;
3598
+ text-align: center;
3599
+ line-height: calc(1.5rem - 2px);
3623
3600
  position: relative;
3624
3601
  z-index: 1;
3625
3602
  appearance: none;
3626
3603
  color: var(--figma-color-text-secondary);
3627
3604
  padding: 0 var(--spacer-2);
3605
+ overflow: hidden;
3606
+ text-overflow: ellipsis;
3607
+ white-space: nowrap;
3628
3608
  transition: none;
3629
3609
 
3630
3610
  &[selected]:not([selected="false"]),
@@ -3708,6 +3688,21 @@ fig-segmented-control {
3708
3688
  }
3709
3689
  }
3710
3690
 
3691
+ /* Options */
3692
+ fig-options {
3693
+ display: flex;
3694
+ width: 100%;
3695
+
3696
+ & > fig-segmented-control {
3697
+ flex: 1;
3698
+ min-width: 0;
3699
+ }
3700
+
3701
+ & > fig-dropdown {
3702
+ flex: 1;
3703
+ }
3704
+ }
3705
+
3711
3706
  fig-joystick {
3712
3707
  --size: 100%;
3713
3708
  --aspect-ratio: 1 / 1;
@@ -4547,13 +4542,16 @@ fig-chooser {
4547
4542
 
4548
4543
  display: flex;
4549
4544
  flex-direction: column;
4550
- gap: var(--fig-chooser-gap);
4545
+ gap: 0;
4551
4546
  overflow: visible auto;
4552
4547
  scrollbar-width: none;
4553
4548
  scroll-snap-type: y mandatory;
4554
4549
 
4555
4550
  > * {
4556
4551
  scroll-snap-align: start;
4552
+ &:not(.fig-chooser-nav-start):not(.fig-chooser-nav-end) {
4553
+ margin-block-end: var(--fig-chooser-gap);
4554
+ }
4557
4555
  }
4558
4556
 
4559
4557
  &[padding="false"] {
@@ -4585,6 +4583,12 @@ fig-chooser {
4585
4583
  fig-image[full] {
4586
4584
  min-width: 3rem;
4587
4585
  }
4586
+ > * {
4587
+ &:not(.fig-chooser-nav-start):not(.fig-chooser-nav-end) {
4588
+ margin-inline-end: var(--fig-chooser-gap);
4589
+ margin-block-end: 0;
4590
+ }
4591
+ }
4588
4592
  }
4589
4593
 
4590
4594
  &[layout="grid"] {
package/fig.js CHANGED
@@ -517,6 +517,8 @@ class FigTooltip extends HTMLElement {
517
517
  #boundHandleTouchMove;
518
518
  #boundHandleTouchEnd;
519
519
  #boundHandleTouchCancel;
520
+ #boundHandleDialogClose;
521
+ #parentDialog = null;
520
522
  #touchTimeout;
521
523
  #isTouching = false;
522
524
  #observer = null;
@@ -535,10 +537,15 @@ class FigTooltip extends HTMLElement {
535
537
  this.#boundHandleTouchMove = this.#handleTouchMove.bind(this);
536
538
  this.#boundHandleTouchEnd = this.#handleTouchEnd.bind(this);
537
539
  this.#boundHandleTouchCancel = this.#handleTouchCancel.bind(this);
540
+ this.#boundHandleDialogClose = () => this.hidePopup();
538
541
  }
539
542
  connectedCallback() {
540
543
  this.setup();
541
544
  this.setupEventListeners();
545
+ this.#parentDialog = this.closest("dialog");
546
+ if (this.#parentDialog) {
547
+ this.#parentDialog.addEventListener("close", this.#boundHandleDialogClose);
548
+ }
542
549
  }
543
550
 
544
551
  disconnectedCallback() {
@@ -549,6 +556,10 @@ class FigTooltip extends HTMLElement {
549
556
  true,
550
557
  );
551
558
  this.#stopObserving();
559
+ if (this.#parentDialog) {
560
+ this.#parentDialog.removeEventListener("close", this.#boundHandleDialogClose);
561
+ this.#parentDialog = null;
562
+ }
552
563
 
553
564
  if (this.action === "click") {
554
565
  document.body.removeEventListener(
@@ -1110,6 +1121,9 @@ customElements.define("fig-truncate", FigTruncate);
1110
1121
  * @attr {boolean} drag - Whether the dialog is draggable
1111
1122
  * @attr {string} handle - CSS selector for the drag handle element (e.g., "fig-header"). If not specified, the entire dialog is draggable when drag is enabled.
1112
1123
  * @attr {string} position - Position of the dialog (e.g., "bottom right", "top left", "center center")
1124
+ * @attr {string} title - Title text for the auto-generated header. If no fig-header[dialog-header] exists, one is prepended with this title and a close button.
1125
+ * @attr {boolean} resizable - Whether the dialog can be manually resized by the user (default: false)
1126
+ * @attr {string} closedby - Controls how the dialog can be dismissed: "any" (default, Escape + light dismiss), "closerequest" (Escape only), "none" (programmatic only)
1113
1127
  */
1114
1128
  class FigDialog extends HTMLDialogElement {
1115
1129
  #isDragging = false;
@@ -1140,6 +1154,8 @@ class FigDialog extends HTMLDialogElement {
1140
1154
  this.drag =
1141
1155
  this.hasAttribute("drag") && this.getAttribute("drag") !== "false";
1142
1156
 
1157
+ this.#ensureHeader();
1158
+
1143
1159
  requestAnimationFrame(() => {
1144
1160
  this.#addCloseListeners();
1145
1161
  this.#setupDragListeners();
@@ -1154,6 +1170,29 @@ class FigDialog extends HTMLDialogElement {
1154
1170
  });
1155
1171
  }
1156
1172
 
1173
+ #ensureHeader() {
1174
+ if (this.querySelector("fig-header[dialog-header]")) return;
1175
+ const header = document.createElement("fig-header");
1176
+ header.setAttribute("dialog-header", "");
1177
+ header.setAttribute("data-auto", "");
1178
+ const h3 = document.createElement("h3");
1179
+ h3.textContent = this.getAttribute("title") || "Dialog";
1180
+ const tooltip = document.createElement("fig-tooltip");
1181
+ tooltip.setAttribute("text", "Close");
1182
+ const btn = document.createElement("fig-button");
1183
+ btn.setAttribute("variant", "ghost");
1184
+ btn.setAttribute("icon", "");
1185
+ btn.setAttribute("close-dialog", "");
1186
+ const icon = document.createElement("span");
1187
+ icon.className = "fig-mask-icon";
1188
+ icon.style.setProperty("--icon", "var(--icon-close)");
1189
+ btn.appendChild(icon);
1190
+ tooltip.appendChild(btn);
1191
+ header.appendChild(h3);
1192
+ header.appendChild(tooltip);
1193
+ this.prepend(header);
1194
+ }
1195
+
1157
1196
  #addCloseListeners() {
1158
1197
  this.querySelectorAll("fig-button[close-dialog]").forEach((button) => {
1159
1198
  button.removeEventListener("click", this.#boundClose);
@@ -1370,7 +1409,7 @@ class FigDialog extends HTMLDialogElement {
1370
1409
  }
1371
1410
 
1372
1411
  static get observedAttributes() {
1373
- return ["modal", "drag", "position", "handle"];
1412
+ return ["modal", "drag", "position", "handle", "title", "resizable", "closedby"];
1374
1413
  }
1375
1414
 
1376
1415
  attributeChangedCallback(name, oldValue, newValue) {
@@ -1391,6 +1430,27 @@ class FigDialog extends HTMLDialogElement {
1391
1430
  if (name === "position" && this.#positionInitialized) {
1392
1431
  this.#applyPosition();
1393
1432
  }
1433
+
1434
+ if (name === "modal") {
1435
+ const wasModal = this.modal;
1436
+ this.modal = newValue !== null && newValue !== "false";
1437
+ if (this.open && wasModal !== this.modal) {
1438
+ this.close();
1439
+ if (this.modal) this.showModal();
1440
+ else this.show();
1441
+ }
1442
+ }
1443
+
1444
+ if (name === "closedby") {
1445
+ this.closedby = newValue || "any";
1446
+ }
1447
+
1448
+ if (name === "title") {
1449
+ const autoHeader = this.querySelector("fig-header[data-auto] h3");
1450
+ if (autoHeader) {
1451
+ autoHeader.textContent = newValue || "Dialog";
1452
+ }
1453
+ }
1394
1454
  }
1395
1455
  }
1396
1456
  figDefineCustomizedBuiltIn("fig-dialog", FigDialog, { extends: "dialog" });
@@ -3071,6 +3131,277 @@ class FigSegmentedControl extends HTMLElement {
3071
3131
  }
3072
3132
  customElements.define("fig-segmented-control", FigSegmentedControl);
3073
3133
 
3134
+ /* Options */
3135
+ /**
3136
+ * A responsive option picker that renders as a segmented control by default,
3137
+ * automatically swapping to a dropdown when any label overflows.
3138
+ * @attr {string} options - Comma-separated list of option labels
3139
+ * @attr {string} value - Currently selected value
3140
+ * @attr {boolean} disabled - Disables the control
3141
+ * @attr {boolean} full - Full-width segmented control
3142
+ * @attr {string} sizing - Segment sizing mode: "equal" (default) or "auto"
3143
+ */
3144
+ class FigOptions extends HTMLElement {
3145
+ static get observedAttributes() {
3146
+ return ["options", "value", "disabled", "full", "sizing"];
3147
+ }
3148
+
3149
+ #currentMode = "segments"; // "segments" | "dropdown"
3150
+ #naturalWidth = 0;
3151
+ #resizeObserver = null;
3152
+ #parsedOptions = [];
3153
+ #childControl = null;
3154
+ #suppressEvents = false;
3155
+
3156
+ connectedCallback() {
3157
+ this.#parseOptions();
3158
+ this.#renderSegments();
3159
+ this.#startResizeObserver();
3160
+ requestAnimationFrame(() => {
3161
+ requestAnimationFrame(() => this.#checkOverflow());
3162
+ });
3163
+ }
3164
+
3165
+ disconnectedCallback() {
3166
+ this.#resizeObserver?.disconnect();
3167
+ this.#resizeObserver = null;
3168
+ }
3169
+
3170
+ get value() {
3171
+ return this.getAttribute("value") || "";
3172
+ }
3173
+
3174
+ set value(val) {
3175
+ if (val === null || val === undefined) {
3176
+ this.removeAttribute("value");
3177
+ } else {
3178
+ this.setAttribute("value", String(val));
3179
+ }
3180
+ }
3181
+
3182
+ get options() {
3183
+ return this.#parsedOptions.slice();
3184
+ }
3185
+
3186
+ set options(val) {
3187
+ if (Array.isArray(val)) {
3188
+ const hasComma = val.some((v) => String(v).includes(","));
3189
+ const str = hasComma ? JSON.stringify(val) : val.join(",");
3190
+ this.setAttribute("options", str);
3191
+ } else {
3192
+ this.setAttribute("options", String(val || ""));
3193
+ }
3194
+ }
3195
+
3196
+ attributeChangedCallback(name, oldValue, newValue) {
3197
+ if (oldValue === newValue) return;
3198
+
3199
+ if (name === "options") {
3200
+ this.#parseOptions();
3201
+ this.#rebuildCurrentControl();
3202
+ return;
3203
+ }
3204
+
3205
+ if (name === "value") {
3206
+ this.#syncValueToChild();
3207
+ return;
3208
+ }
3209
+
3210
+ if (name === "disabled") {
3211
+ this.#syncAttrToChild("disabled");
3212
+ return;
3213
+ }
3214
+
3215
+ if (name === "full") {
3216
+ this.#syncAttrToChild("full");
3217
+ return;
3218
+ }
3219
+
3220
+ if (name === "sizing") {
3221
+ this.#syncAttrToChild("sizing");
3222
+ this.#rebuildCurrentControl();
3223
+ }
3224
+ }
3225
+
3226
+ #parseOptions() {
3227
+ const raw = this.getAttribute("options") || "";
3228
+ if (raw.startsWith("[")) {
3229
+ try { this.#parsedOptions = JSON.parse(raw); return; } catch {}
3230
+ }
3231
+ const delimiter = raw.includes("\n") ? "\n" : ",";
3232
+ this.#parsedOptions = raw.split(delimiter).map((s) => s.trim()).filter(Boolean);
3233
+ }
3234
+
3235
+ #renderSegments() {
3236
+ this.innerHTML = "";
3237
+ if (this.#parsedOptions.length === 0) return;
3238
+
3239
+ const sc = document.createElement("fig-segmented-control");
3240
+ sc.setAttribute("sizing", this.getAttribute("sizing") || "equal");
3241
+
3242
+ if (this.hasAttribute("disabled")) sc.setAttribute("disabled", "");
3243
+ if (this.hasAttribute("full")) sc.setAttribute("full", "");
3244
+
3245
+ const currentValue = this.getAttribute("value");
3246
+ let hasSelection = false;
3247
+
3248
+ for (const opt of this.#parsedOptions) {
3249
+ const seg = document.createElement("fig-segment");
3250
+ seg.setAttribute("value", opt);
3251
+ seg.textContent = opt;
3252
+ if (currentValue === opt) {
3253
+ seg.setAttribute("selected", "true");
3254
+ hasSelection = true;
3255
+ }
3256
+ sc.appendChild(seg);
3257
+ }
3258
+
3259
+ if (currentValue) sc.setAttribute("value", currentValue);
3260
+
3261
+ sc.addEventListener("input", (e) => {
3262
+ if (this.#suppressEvents) return;
3263
+ this.#suppressEvents = true;
3264
+ this.setAttribute("value", e.detail);
3265
+ this.#suppressEvents = false;
3266
+ this.dispatchEvent(
3267
+ new CustomEvent("input", { detail: e.detail, bubbles: true }),
3268
+ );
3269
+ });
3270
+ sc.addEventListener("change", (e) => {
3271
+ if (this.#suppressEvents) return;
3272
+ this.dispatchEvent(
3273
+ new CustomEvent("change", { detail: e.detail, bubbles: true }),
3274
+ );
3275
+ });
3276
+
3277
+ this.appendChild(sc);
3278
+ this.#childControl = sc;
3279
+ this.#currentMode = "segments";
3280
+ }
3281
+
3282
+ #renderDropdown() {
3283
+ this.innerHTML = "";
3284
+ if (this.#parsedOptions.length === 0) return;
3285
+
3286
+ const dd = document.createElement("fig-dropdown");
3287
+ if (this.hasAttribute("disabled")) dd.setAttribute("disabled", "");
3288
+
3289
+ const currentValue = this.getAttribute("value");
3290
+
3291
+ for (const opt of this.#parsedOptions) {
3292
+ const option = document.createElement("option");
3293
+ option.value = opt;
3294
+ option.textContent = opt;
3295
+ if (currentValue === opt) option.selected = true;
3296
+ dd.appendChild(option);
3297
+ }
3298
+
3299
+ if (currentValue) dd.setAttribute("value", currentValue);
3300
+
3301
+ dd.addEventListener("input", (e) => {
3302
+ if (this.#suppressEvents) return;
3303
+ this.#suppressEvents = true;
3304
+ this.setAttribute("value", e.detail);
3305
+ this.#suppressEvents = false;
3306
+ this.dispatchEvent(
3307
+ new CustomEvent("input", { detail: e.detail, bubbles: true }),
3308
+ );
3309
+ });
3310
+ dd.addEventListener("change", (e) => {
3311
+ if (this.#suppressEvents) return;
3312
+ this.dispatchEvent(
3313
+ new CustomEvent("change", { detail: e.detail, bubbles: true }),
3314
+ );
3315
+ });
3316
+
3317
+ this.appendChild(dd);
3318
+ this.#childControl = dd;
3319
+ this.#currentMode = "dropdown";
3320
+ }
3321
+
3322
+ #rebuildCurrentControl() {
3323
+ if (this.#currentMode === "segments") {
3324
+ this.#renderSegments();
3325
+ requestAnimationFrame(() => {
3326
+ requestAnimationFrame(() => this.#checkOverflow());
3327
+ });
3328
+ } else {
3329
+ this.#renderDropdown();
3330
+ }
3331
+ }
3332
+
3333
+ #syncValueToChild() {
3334
+ if (!this.#childControl || this.#suppressEvents) return;
3335
+ const val = this.getAttribute("value") || "";
3336
+ this.#childControl.value = val;
3337
+ }
3338
+
3339
+ #syncAttrToChild(attr) {
3340
+ if (!this.#childControl) return;
3341
+ if (this.hasAttribute(attr)) {
3342
+ this.#childControl.setAttribute(attr, this.getAttribute(attr) || "");
3343
+ } else {
3344
+ this.#childControl.removeAttribute(attr);
3345
+ }
3346
+ }
3347
+
3348
+ #startResizeObserver() {
3349
+ this.#resizeObserver?.disconnect();
3350
+ this.#resizeObserver = new ResizeObserver(() => {
3351
+ this.#checkOverflow();
3352
+ });
3353
+ this.#resizeObserver.observe(this);
3354
+ }
3355
+
3356
+ #isSegmentTruncated(seg) {
3357
+ const range = document.createRange();
3358
+ range.selectNodeContents(seg);
3359
+ const textWidth = range.getBoundingClientRect().width;
3360
+ const segRect = seg.getBoundingClientRect();
3361
+ const segWidth = segRect.width;
3362
+ const cs = getComputedStyle(seg);
3363
+ const padL = parseFloat(cs.paddingLeft) || 0;
3364
+ const padR = parseFloat(cs.paddingRight) || 0;
3365
+ const contentWidth = segWidth - padL - padR;
3366
+ return textWidth > contentWidth + 0.5;
3367
+ }
3368
+
3369
+ #anySegmentTruncated() {
3370
+ const segments = this.querySelectorAll("fig-segment");
3371
+ for (const seg of segments) {
3372
+ if (this.#isSegmentTruncated(seg)) return true;
3373
+ }
3374
+ return false;
3375
+ }
3376
+
3377
+ #checkOverflow() {
3378
+ if (this.#parsedOptions.length <= 1) return;
3379
+
3380
+ if (this.#currentMode === "segments") {
3381
+ const sc = this.#childControl;
3382
+ const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
3383
+ if (containerOverflow || this.#anySegmentTruncated()) {
3384
+ this.#naturalWidth = this.clientWidth;
3385
+ this.#renderDropdown();
3386
+ }
3387
+ } else {
3388
+ if (this.#naturalWidth > 0 && this.clientWidth >= this.#naturalWidth) {
3389
+ this.#renderSegments();
3390
+ requestAnimationFrame(() => {
3391
+ requestAnimationFrame(() => {
3392
+ const sc = this.#childControl;
3393
+ const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
3394
+ if (containerOverflow || this.#anySegmentTruncated()) {
3395
+ this.#renderDropdown();
3396
+ }
3397
+ });
3398
+ });
3399
+ }
3400
+ }
3401
+ }
3402
+ }
3403
+ customElements.define("fig-options", FigOptions);
3404
+
3074
3405
  /* Slider */
3075
3406
  /**
3076
3407
  * A custom slider input element.
@@ -6105,6 +6436,7 @@ class FigInputPalette extends HTMLElement {
6105
6436
  expandedWrap.className = "palette-colors-expanded";
6106
6437
  this.#colors.forEach((entry, i) => {
6107
6438
  expandedWrap.appendChild(this.#createPicker(entry, i, disabled));
6439
+ expandedWrap.appendChild(this.#createRemoveButton(i, disabled));
6108
6440
  });
6109
6441
  this.appendChild(expandedWrap);
6110
6442
  }
@@ -6169,6 +6501,31 @@ class FigInputPalette extends HTMLElement {
6169
6501
  return ic;
6170
6502
  }
6171
6503
 
6504
+ #createRemoveButton(index, disabled) {
6505
+ const btn = document.createElement("fig-button");
6506
+ btn.setAttribute("variant", "ghost");
6507
+ btn.setAttribute("icon", "true");
6508
+ btn.setAttribute("aria-label", "Remove color");
6509
+ btn.className = "palette-remove-btn";
6510
+ if (disabled || this.#colors.length <= this.#min) btn.setAttribute("disabled", "");
6511
+ btn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>`;
6512
+ btn.addEventListener("click", () => {
6513
+ if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
6514
+ this.#removeColor(index);
6515
+ });
6516
+ return btn;
6517
+ }
6518
+
6519
+ #removeColor(index) {
6520
+ if (index < 0 || index >= this.#colors.length) return;
6521
+ if (this.#colors.length <= this.#min) return;
6522
+ this.#colors.splice(index, 1);
6523
+ this.#inlinePickers = [];
6524
+ this.#expandedPickers = [];
6525
+ this.#render();
6526
+ this.#emitChange();
6527
+ }
6528
+
6172
6529
  #createAddButton(disabled, parent = this) {
6173
6530
  const atMax = this.#colors.length >= this.#max;
6174
6531
  const addBtn = document.createElement("fig-button");
@@ -6208,7 +6565,10 @@ class FigInputPalette extends HTMLElement {
6208
6565
 
6209
6566
  const expandedIc = this.#createPicker(entry, index, disabled);
6210
6567
  const expandedWrap = this.querySelector(".palette-colors-expanded");
6211
- if (expandedWrap) expandedWrap.appendChild(expandedIc);
6568
+ if (expandedWrap) {
6569
+ expandedWrap.appendChild(expandedIc);
6570
+ expandedWrap.appendChild(this.#createRemoveButton(index, disabled));
6571
+ }
6212
6572
 
6213
6573
  if (this.#colors.length >= this.#max) {
6214
6574
  const addBtn = this.querySelector(".palette-add-btn");
@@ -10652,6 +11012,24 @@ class FigGroup extends HTMLElement {
10652
11012
  }
10653
11013
  customElements.define("fig-group", FigGroup);
10654
11014
 
11015
+ /**
11016
+ * A presentational header element used inside fig-dialog, fig-group, and other containers.
11017
+ * Styling is handled entirely in CSS; this registration makes it a known custom element.
11018
+ *
11019
+ * @attr {boolean} borderless - Removes the bottom border
11020
+ * @attr {boolean} dialog-header - Marks this as a dialog header (auto-generated by fig-dialog)
11021
+ */
11022
+ class FigHeader extends HTMLElement {}
11023
+ customElements.define("fig-header", FigHeader);
11024
+
11025
+ /**
11026
+ * fig-footer
11027
+ * @element fig-footer
11028
+ * @attr {boolean} borderless - Removes the top border
11029
+ */
11030
+ class FigFooter extends HTMLElement {}
11031
+ customElements.define("fig-footer", FigFooter);
11032
+
10655
11033
  // FigFillPicker
10656
11034
  /**
10657
11035
  * A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "4.1.4",
3
+ "version": "4.3.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",