@rogieking/figui3 9.3.0 → 9.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,6 +76,7 @@ Agent skills ship in `.cursor/skills/` (included in the npm package):
76
76
 
77
77
  - `figui3` — core `fig.js` components
78
78
  - `fig-editor` — `fig-select` and `fig-fill-picker`
79
+ - `fig-lab` — experimental audio, wheel, reorder, canvas, and AI components
79
80
  - `propkit` — property-row composition; examples are included in `/figui3`
80
81
 
81
82
  Minimal example:
@@ -103,6 +104,7 @@ Minimal example:
103
104
  | [Radio](#radio) | `<fig-radio>` | Radio button |
104
105
  | [Switch](#switch) | `<fig-switch>` | Toggle switch |
105
106
  | [Slider](#slider) | `<fig-slider>` | Range, hue, opacity, delta, stepper |
107
+ | [Audio Input](#audio-input) | `<fig-input-audio>` | Audio upload with responsive waveform preview |
106
108
  | [Input Wheel](#input-wheel) | `<fig-input-wheel>` | Standalone SVG tick-and-handle numeric scrubber |
107
109
  | [Text Input](#text-input) | `<fig-input-text>` | Styled text/textarea input |
108
110
  | [Number Input](#number-input) | `<fig-input-number>` | Numeric input with units |
@@ -114,10 +116,12 @@ Minimal example:
114
116
  | [Gradient Input](#gradient-input) | `<fig-input-gradient>` | Gradient editor with stops |
115
117
  | [Fill Input](#fill-input) | `<fig-input-fill>` | Solid, gradient, image, video fill |
116
118
  | [Fill Picker](#fill-picker) | `<fig-fill-picker>` | Full fill picker dialog |
119
+ | [Toolbelt](#toolbelt) | `<fig-toolbelt>` / `<fig-toolbelt-group>` / `<fig-toolbelt-item>` | Groupable selectable toolbar with directional overflow |
117
120
  | [Tabs](#tabs) | `<fig-tabs>` / `<fig-tab>` | Tabbed navigation |
118
121
  | [Segmented Control](#segmented-control) | `<fig-segmented-control>` / `<fig-segment>` | Segmented button group |
119
122
  | [Chooser](#chooser) | `<fig-chooser>` / `<fig-choice>` | Selection list with drag scroll |
120
123
  | [Field](#field) | `<fig-field>` | Form field wrapper with layout |
124
+ | [Group](#group) | `<fig-group>` | Named or collapsible section group |
121
125
  | [Joystick](#joystick) | `<fig-joystick>` | 2D position input |
122
126
  | [Origin Grid](#origin-grid) | `<fig-origin-grid>` | Transform-origin grid |
123
127
  | [Easing Curve](#easing-curve) | `<fig-easing-curve>` | Bezier/spring curve editor |
@@ -402,6 +406,37 @@ For `type="range"`, omitting `value` follows native range behavior and starts at
402
406
 
403
407
  ---
404
408
 
409
+ #### Audio Input
410
+
411
+ `<fig-input-audio>`
412
+
413
+ An experimental single-audio upload control with a responsive waveform preview. Requires `fig-lab.js` and `fig-lab.css`. The waveform is preview-only and has no playback or seeking controls.
414
+
415
+ | Attribute | Type | Default | Description |
416
+ |---|---|---|---|
417
+ | `url` | string | — | CORS-enabled audio URL to decode and preview |
418
+ | `filename` | string | URL filename | Display filename for URL-backed audio |
419
+ | `label` | string | `"Upload audio"` | Empty-state upload label |
420
+ | `accepts` | string | `"audio/*"` | Native file accept filter |
421
+ | `variant` | string | `"input"` | Upload button variant |
422
+ | `disabled` | boolean | `false` | Disable upload, replacement, and removal |
423
+ | `full` | boolean | `false` | Fill the available width |
424
+
425
+ Set `--fig-input-audio-waveform-stroke-width` (default `2px`) to control waveform bar thickness.
426
+
427
+ `files` and `value` mirror the composed file input. `clear()` removes the current source. The component emits bubbling, composed `input` and `change` events with `{ files }` and `{ files, cleared: true }` when cleared. URL fetch or decode failures render a quiet flat waveform fallback.
428
+
429
+ ```html
430
+ <fig-input-audio></fig-input-audio>
431
+ <fig-input-audio
432
+ url="https://example.com/audio.wav"
433
+ filename="audio.wav"
434
+ full
435
+ ></fig-input-audio>
436
+ ```
437
+
438
+ ---
439
+
405
440
  #### Input Wheel
406
441
 
407
442
  `<fig-input-wheel>`
@@ -735,6 +770,37 @@ Webcam JSON is `{ type: "webcam", webcam: { live, snapshot, deviceId, scaleMode,
735
770
 
736
771
  ### Selection
737
772
 
773
+ #### Toolbelt
774
+
775
+ `<fig-toolbelt>` / `<fig-toolbelt-group>` / `<fig-toolbelt-item>`
776
+
777
+ A selectable horizontal or vertical toolbar that keeps authored items in the light DOM. Its internal `part="viewport"` owns scrolling and overflow fades, while generated paging buttons remain outside the masked viewport.
778
+
779
+ | Attribute | Type | Default | Description |
780
+ |---|---|---|---|
781
+ | `value` | string | — | Selected item value |
782
+ | `disabled` | boolean | `false` | Disable all items |
783
+ | `layout` | `horizontal` \| `vertical` | `horizontal` | Item and keyboard-navigation direction |
784
+ | `overflow` | `buttons` \| `scrollbar` | `scrollbar` | Generated paging buttons or buttonless scrolling; scrollbars remain hidden |
785
+
786
+ Wrap related items in `fig-toolbelt-group`; adjacent groups receive a direction-aware separator. Add `aria-label` to groups when the grouping has a meaningful name. Items support `value`, `selected`, and `disabled`. `input` and `change` bubble from the toolbelt with the selected value. ArrowLeft/ArrowRight navigate horizontal toolbelts; ArrowUp/ArrowDown navigate vertical toolbelts. Home and End move to the edges; Enter or Space selects the focused item.
787
+
788
+ Use `--fig-toolbelt-gap`, `--fig-toolbelt-group-gap`, `--fig-toolbelt-group-separator-color`, `--fig-toolbelt-overflow-fade-size`, `--fig-toolbelt-padding-block`, `--fig-toolbelt-padding-inline`, `--fig-toolbelt-item-height`, and `--fig-toolbelt-item-padding` to customize layout. The toolbelt fade defaults to `calc(var(--spacer-6) + var(--spacer-2))`.
789
+
790
+ ```html
791
+ <fig-toolbelt layout="horizontal" overflow="buttons" value="move" aria-label="Design tools">
792
+ <fig-toolbelt-group aria-label="Transform tools">
793
+ <fig-toolbelt-item value="move" selected>Move</fig-toolbelt-item>
794
+ <fig-toolbelt-item value="frame">Frame</fig-toolbelt-item>
795
+ </fig-toolbelt-group>
796
+ <fig-toolbelt-group aria-label="Shape tools">
797
+ <fig-toolbelt-item value="shape">Shape</fig-toolbelt-item>
798
+ </fig-toolbelt-group>
799
+ </fig-toolbelt>
800
+ ```
801
+
802
+ ---
803
+
738
804
  #### Tabs
739
805
 
740
806
  `<fig-tabs>` / `<fig-tab>` — [demo](https://rog.ie/figui3/#tabs)
@@ -1175,6 +1241,33 @@ A form field wrapper with flexible layout. Automatically links `<label>` to the
1175
1241
 
1176
1242
  ---
1177
1243
 
1244
+ #### Group
1245
+
1246
+ `<fig-group>` — [demo](https://rog.ie/figui3/#group)
1247
+
1248
+ A section group with an optional generated or authored header. In collapsible groups, the `h3` is the keyboard-accessible toggle and the chevron is clickable; controls placed beside the `h3` in an authored `fig-header` remain independent.
1249
+
1250
+ | Attribute | Type | Default | Description |
1251
+ |---|---|---|---|
1252
+ | `name` | string | — | Generated header text |
1253
+ | `collapsible` | boolean | `false` | Enables collapse behavior |
1254
+ | `open` | boolean | `false` | Expanded state |
1255
+ | `compact` | boolean | `false` | Adds `compact` to the direct `fig-header` |
1256
+
1257
+ ```html
1258
+ <fig-group collapsible open>
1259
+ <fig-header borderless>
1260
+ <h3>Export</h3>
1261
+ <fig-button variant="ghost" icon aria-label="Add export">
1262
+ <fig-icon name="add"></fig-icon>
1263
+ </fig-button>
1264
+ </fig-header>
1265
+ <fig-field>...</fig-field>
1266
+ </fig-group>
1267
+ ```
1268
+
1269
+ ---
1270
+
1178
1271
  #### Dialog
1179
1272
 
1180
1273
  `<fig-dialog>` — [demo](https://rog.ie/figui3/#dialog)
package/components.css CHANGED
@@ -1290,6 +1290,70 @@ fig-tab {
1290
1290
  cursor: default;
1291
1291
  }
1292
1292
  }
1293
+
1294
+
1295
+
1296
+ fig-toolbelt-item {
1297
+ display: inline-flex;
1298
+ flex-shrink: 0;
1299
+ align-items: center;
1300
+ justify-content: center;
1301
+ height: var(--fig-toolbelt-item-height, var(--control-height));
1302
+ padding-inline: var(--fig-toolbelt-item-padding, var(--spacer-2));
1303
+ border-radius: var(--radius-medium);
1304
+ color: var(--figma-color-text-secondary);
1305
+ line-height: var(--line-height);
1306
+ cursor: default;
1307
+ user-select: none;
1308
+
1309
+ &:has(> fig-tooltip > fig-button[icon]:not([icon="false"])) {
1310
+ padding-inline: 0;
1311
+ }
1312
+
1313
+ &[selected]:not([selected="false"]) {
1314
+ color: var(--figma-color-text);
1315
+ background-color: var(--figma-color-bg-secondary);
1316
+
1317
+ fig-button{
1318
+ color: var(--figma-color-icon-onbrand);
1319
+ background-color: var(--figma-color-bg-brand);
1320
+ }
1321
+ }
1322
+
1323
+ &:hover {
1324
+ background-color: var(--figma-color-bg-secondary);
1325
+ }
1326
+
1327
+ &:focus-visible,
1328
+ &[data-focus-visible] {
1329
+ outline: var(--figma-focus-outline);
1330
+ outline-offset: var(--figma-focus-outline-offset);
1331
+ }
1332
+
1333
+ &[disabled]:not([disabled="false"]) {
1334
+ opacity: 0.4;
1335
+ pointer-events: none;
1336
+ }
1337
+ }
1338
+
1339
+ fig-toolbelt-group {
1340
+ display: inline-flex;
1341
+ flex-flow: row nowrap;
1342
+ flex-shrink: 0;
1343
+ align-items: stretch;
1344
+ gap: var(--fig-toolbelt-gap);
1345
+ min-width: 0;
1346
+
1347
+ & + fig-toolbelt-group {
1348
+ margin-inline-start: calc(
1349
+ var(--fig-toolbelt-group-gap) - var(--fig-toolbelt-gap)
1350
+ );
1351
+ padding-inline-start: var(--fig-toolbelt-group-gap);
1352
+ border-inline-start: 1px solid
1353
+ var(--fig-toolbelt-group-separator-color);
1354
+ }
1355
+ }
1356
+
1293
1357
  fig-tab-content {
1294
1358
  display: block;
1295
1359
  width: 100%;
@@ -1410,6 +1474,171 @@ fig-tabs,
1410
1474
  }
1411
1475
  }
1412
1476
 
1477
+ fig-toolbelt {
1478
+ --fig-toolbelt-gap: var(--spacer-1);
1479
+ --fig-toolbelt-group-gap: var(--spacer-2);
1480
+ --fig-toolbelt-group-separator-color: var(--figma-color-border);
1481
+ --fig-toolbelt-overflow-fade-size: calc(var(--spacer-6) + var(--spacer-2));
1482
+ --fig-toolbelt-padding-block: var(--spacer-2);
1483
+ --fig-toolbelt-padding-inline: var(--spacer-2);
1484
+
1485
+ position: relative;
1486
+ display: inline-block;
1487
+ box-sizing: border-box;
1488
+ width: 100%;
1489
+ min-width: 0;
1490
+ margin: 0;
1491
+ background: var(--figma-color-bg) !important;
1492
+ border-radius: var(--radius-large);
1493
+
1494
+ &::after{
1495
+ content: "";
1496
+ position: absolute;
1497
+ inset: 0;
1498
+ pointer-events: none;
1499
+ z-index: 10;
1500
+ box-shadow: var(--figma-elevation-100-canvas);
1501
+ border-radius: var(--radius-large);
1502
+ }
1503
+
1504
+ &[disabled]:not([disabled="false"]) {
1505
+ pointer-events: none;
1506
+ }
1507
+
1508
+ &::part(frame) {
1509
+ position: relative;
1510
+ display: flex;
1511
+ flex-flow: row nowrap;
1512
+ width: 100%;
1513
+ height: 100%;
1514
+ min-width: 0;
1515
+ min-height: 0;
1516
+ }
1517
+
1518
+ &::part(viewport) {
1519
+ display: flex;
1520
+ flex: 1 1 auto;
1521
+ flex-flow: row nowrap;
1522
+ box-sizing: border-box;
1523
+ gap: var(--fig-toolbelt-gap);
1524
+ width: 100%;
1525
+ min-width: 0;
1526
+ padding: var(--fig-toolbelt-padding-block)
1527
+ var(--fig-toolbelt-padding-inline);
1528
+ overflow: auto hidden;
1529
+ scrollbar-width: none;
1530
+ }
1531
+
1532
+ &::part(viewport)::-webkit-scrollbar {
1533
+ display: none;
1534
+ }
1535
+
1536
+ > .fig-overflow {
1537
+ position: absolute;
1538
+ top: 0;
1539
+ bottom: 0;
1540
+ z-index: 3;
1541
+ width: var(--control-height);
1542
+ height: auto;
1543
+ margin: 0;
1544
+ }
1545
+
1546
+ > .fig-overflow-start {
1547
+ left: 0;
1548
+ right: auto;
1549
+ box-shadow: 1px 0 0 0 var(--figma-color-bordertranslucent);
1550
+
1551
+ &::before {
1552
+ border-radius: var(--radius-medium) 0 0 var(--radius-medium);
1553
+ }
1554
+
1555
+ .fig-overflow-chevron {
1556
+ rotate: 90deg;
1557
+ }
1558
+ }
1559
+
1560
+ > .fig-overflow-end {
1561
+ right: 0;
1562
+ left: auto;
1563
+ box-shadow: -1px 0 0 0 var(--figma-color-bordertranslucent);
1564
+
1565
+ &::before {
1566
+ border-radius: 0 var(--radius-medium) var(--radius-medium) 0;
1567
+ }
1568
+
1569
+ .fig-overflow-chevron {
1570
+ rotate: -90deg;
1571
+ }
1572
+ }
1573
+
1574
+ &[layout="vertical"] {
1575
+ width: fit-content;
1576
+ max-width: 100%;
1577
+
1578
+ &::part(frame),
1579
+ &::part(viewport) {
1580
+ flex-flow: column nowrap;
1581
+ }
1582
+
1583
+ &::part(viewport) {
1584
+ overflow: hidden auto;
1585
+ }
1586
+
1587
+ fig-toolbelt-group {
1588
+ flex-flow: column nowrap;
1589
+ width: 100%;
1590
+
1591
+ & + fig-toolbelt-group {
1592
+ margin-block-start: calc(
1593
+ var(--fig-toolbelt-group-gap) - var(--fig-toolbelt-gap)
1594
+ );
1595
+ margin-inline-start: 0;
1596
+ padding-block-start: var(--fig-toolbelt-group-gap);
1597
+ padding-inline-start: 0;
1598
+ border-block-start: 1px solid
1599
+ var(--fig-toolbelt-group-separator-color);
1600
+ border-inline-start: 0;
1601
+ }
1602
+ }
1603
+
1604
+ > .fig-overflow {
1605
+ top: auto;
1606
+ right: 0;
1607
+ left: 0;
1608
+ width: auto;
1609
+ height: var(--control-height);
1610
+ }
1611
+
1612
+ > .fig-overflow-start {
1613
+ top: 0;
1614
+ bottom: auto;
1615
+ box-shadow: 0 1px 0 0 var(--figma-color-bordertranslucent);
1616
+
1617
+ &::before {
1618
+ border-radius: var(--radius-medium) var(--radius-medium) 0 0;
1619
+ }
1620
+
1621
+ .fig-overflow-chevron {
1622
+ rotate: 180deg;
1623
+ }
1624
+ }
1625
+
1626
+ > .fig-overflow-end {
1627
+ top: auto;
1628
+ bottom: 0;
1629
+ box-shadow: 0 -1px 0 0 var(--figma-color-bordertranslucent);
1630
+
1631
+ &::before {
1632
+ border-radius: 0 0 var(--radius-medium) var(--radius-medium);
1633
+ }
1634
+
1635
+ .fig-overflow-chevron {
1636
+ rotate: 0deg;
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+
1413
1642
  /* Avatar */
1414
1643
  fig-avatar,
1415
1644
  .fig-avatar {
@@ -3810,18 +4039,11 @@ fig-group {
3810
4039
  }
3811
4040
  }
3812
4041
 
3813
- &[compact]:not([compact="false"]) {
3814
- & > fig-header{
3815
- --fig-header-height: var(--spacer-5);
3816
- }
3817
- }
3818
-
3819
-
3820
4042
  & > fig-header {
3821
4043
  cursor: default;
3822
4044
  gap: 0;
3823
4045
 
3824
- &:focus-visible {
4046
+ &:has(> h3:focus-visible) {
3825
4047
  --figma-focus-outline-radius: var(--radius-medium);
3826
4048
  outline: var(--figma-focus-outline);
3827
4049
  outline-offset: var(--figma-focus-outline-offset);
@@ -3833,10 +4055,12 @@ fig-group {
3833
4055
  display: block;
3834
4056
  margin:0;
3835
4057
  padding-left: 0;
4058
+ outline: none;
3836
4059
  text-overflow: ellipsis;
3837
4060
  overflow: hidden;
3838
4061
  white-space: nowrap;
3839
4062
  max-width: 100%;
4063
+ cursor: default;
3840
4064
  }
3841
4065
  & > .fig-group-chevron {
3842
4066
  --icon: var(--icon-16-chevron);
@@ -3847,6 +4071,7 @@ fig-group {
3847
4071
  transition: transform var(--transition-duration)
3848
4072
  var(--transition-timing-function);
3849
4073
  margin-left: calc(var(--spacer-2) * -1);
4074
+ cursor: default;
3850
4075
  }
3851
4076
  }
3852
4077
 
@@ -3859,7 +4084,7 @@ fig-group {
3859
4084
  margin-bottom: 0;
3860
4085
  fig-header {
3861
4086
  color: var(--figma-color-text-secondary);
3862
- &:hover {
4087
+ &:has(> h3:hover, > .fig-group-chevron:hover) {
3863
4088
  color: var(--figma-color-text);
3864
4089
  .fig-group-chevron {
3865
4090
  color: inherit;
@@ -3877,10 +4102,6 @@ fig-group {
3877
4102
  display: none;
3878
4103
  }
3879
4104
 
3880
- &[compact]:not([compact="false"]) > fig-header {
3881
- --fig-header-height: var(--spacer-5);
3882
- height: var(--fig-header-height);
3883
- }
3884
4105
  }
3885
4106
 
3886
4107
  fig-footer {
@@ -5389,6 +5610,13 @@ fig-skeleton {
5389
5610
  overflow-y: auto;
5390
5611
  }
5391
5612
 
5613
+ fig-toolbelt::part(viewport) {
5614
+ --fig-overflow-fade-size: var(--fig-toolbelt-overflow-fade-size);
5615
+ --fig-overflow-fade-clear-top: 0px;
5616
+
5617
+ overflow-y: auto;
5618
+ }
5619
+
5392
5620
  .fig-overflow-fade.fig-overflow-fade-horizontal {
5393
5621
  --fig-overflow-fade-clear-left: 0px;
5394
5622
 
@@ -5396,8 +5624,16 @@ fig-skeleton {
5396
5624
  overflow-y: hidden;
5397
5625
  }
5398
5626
 
5627
+ fig-toolbelt:not([layout="vertical"])::part(viewport) {
5628
+ --fig-overflow-fade-clear-left: 0px;
5629
+
5630
+ overflow-x: auto;
5631
+ overflow-y: hidden;
5632
+ }
5633
+
5399
5634
  @supports (animation-timeline: scroll(self block)) {
5400
- .fig-overflow-fade:not(.fig-overflow-fade-horizontal) {
5635
+ .fig-overflow-fade:not(.fig-overflow-fade-horizontal),
5636
+ fig-toolbelt[layout="vertical"]::part(viewport) {
5401
5637
  mask-image: linear-gradient(
5402
5638
  to bottom,
5403
5639
  black 0,
@@ -5531,7 +5767,8 @@ fig-skeleton {
5531
5767
  }
5532
5768
 
5533
5769
  @supports (animation-timeline: scroll(self inline)) {
5534
- .fig-overflow-fade.fig-overflow-fade-horizontal {
5770
+ .fig-overflow-fade.fig-overflow-fade-horizontal,
5771
+ fig-toolbelt:not([layout="vertical"])::part(viewport) {
5535
5772
  mask-image: linear-gradient(
5536
5773
  to right,
5537
5774
  black 0,