@rogieking/figui3 9.2.3 → 9.4.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.
package/README.md CHANGED
@@ -76,7 +76,8 @@ 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
- - `propkit` — `/propskit` property-row composition
79
+ - `fig-lab` — experimental audio, wheel, reorder, canvas, and AI components
80
+ - `propkit` — property-row composition; examples are included in `/figui3`
80
81
 
81
82
  Minimal example:
82
83
 
@@ -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,6 +116,7 @@ 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 |
@@ -402,6 +405,37 @@ For `type="range"`, omitting `value` follows native range behavior and starts at
402
405
 
403
406
  ---
404
407
 
408
+ #### Audio Input
409
+
410
+ `<fig-input-audio>`
411
+
412
+ 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.
413
+
414
+ | Attribute | Type | Default | Description |
415
+ |---|---|---|---|
416
+ | `url` | string | — | CORS-enabled audio URL to decode and preview |
417
+ | `filename` | string | URL filename | Display filename for URL-backed audio |
418
+ | `label` | string | `"Upload audio"` | Empty-state upload label |
419
+ | `accepts` | string | `"audio/*"` | Native file accept filter |
420
+ | `variant` | string | `"input"` | Upload button variant |
421
+ | `disabled` | boolean | `false` | Disable upload, replacement, and removal |
422
+ | `full` | boolean | `false` | Fill the available width |
423
+
424
+ Set `--fig-input-audio-waveform-stroke-width` (default `2px`) to control waveform bar thickness.
425
+
426
+ `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.
427
+
428
+ ```html
429
+ <fig-input-audio></fig-input-audio>
430
+ <fig-input-audio
431
+ url="https://example.com/audio.wav"
432
+ filename="audio.wav"
433
+ full
434
+ ></fig-input-audio>
435
+ ```
436
+
437
+ ---
438
+
405
439
  #### Input Wheel
406
440
 
407
441
  `<fig-input-wheel>`
@@ -735,6 +769,37 @@ Webcam JSON is `{ type: "webcam", webcam: { live, snapshot, deviceId, scaleMode,
735
769
 
736
770
  ### Selection
737
771
 
772
+ #### Toolbelt
773
+
774
+ `<fig-toolbelt>` / `<fig-toolbelt-group>` / `<fig-toolbelt-item>`
775
+
776
+ 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.
777
+
778
+ | Attribute | Type | Default | Description |
779
+ |---|---|---|---|
780
+ | `value` | string | — | Selected item value |
781
+ | `disabled` | boolean | `false` | Disable all items |
782
+ | `layout` | `horizontal` \| `vertical` | `horizontal` | Item and keyboard-navigation direction |
783
+ | `overflow` | `buttons` \| `scrollbar` | `scrollbar` | Generated paging buttons or buttonless scrolling; scrollbars remain hidden |
784
+
785
+ 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.
786
+
787
+ 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))`.
788
+
789
+ ```html
790
+ <fig-toolbelt layout="horizontal" overflow="buttons" value="move" aria-label="Design tools">
791
+ <fig-toolbelt-group aria-label="Transform tools">
792
+ <fig-toolbelt-item value="move" selected>Move</fig-toolbelt-item>
793
+ <fig-toolbelt-item value="frame">Frame</fig-toolbelt-item>
794
+ </fig-toolbelt-group>
795
+ <fig-toolbelt-group aria-label="Shape tools">
796
+ <fig-toolbelt-item value="shape">Shape</fig-toolbelt-item>
797
+ </fig-toolbelt-group>
798
+ </fig-toolbelt>
799
+ ```
800
+
801
+ ---
802
+
738
803
  #### Tabs
739
804
 
740
805
  `<fig-tabs>` / `<fig-tab>` — [demo](https://rog.ie/figui3/#tabs)
@@ -1780,7 +1845,7 @@ git clone https://github.com/rogie/figui3.git
1780
1845
  cd figui3
1781
1846
  bun install
1782
1847
  bun dev # Component docs at http://localhost:3000
1783
- npm run dev:playground # Playground at http://localhost:5173 (/figui3, /propkit, /sandbox)
1848
+ npm run dev:playground # Playground at http://localhost:6600 (/figui3, /lab, /sandbox)
1784
1849
  npm run build # Build minified dist/ (JS + CSS)
1785
1850
  npm run build:css # Build minified CSS only
1786
1851
  ```
@@ -1809,8 +1874,8 @@ import "@rogieking/figui3/src/fig.css"; // unminified source
1809
1874
 
1810
1875
  The playground is the fastest way to explore and validate component markup:
1811
1876
 
1812
- - **`/figui3`** — component examples with attribute controls
1813
- - **`/propkit`** — property panel patterns
1877
+ - **`/figui3`** — component examples and property panel patterns with attribute controls
1878
+ - **`/lab`** — experimental component examples
1814
1879
  - **`/sandbox`** — styled React sample app
1815
1880
 
1816
1881
  ---
package/components.css CHANGED
@@ -1120,7 +1120,7 @@ fig-property-button {
1120
1120
  stroke: currentColor;
1121
1121
  }
1122
1122
  }
1123
- &:has(svg:first-child):not([icon]) {
1123
+ &:not(fig-property-button):has(svg:first-child):not([icon]) {
1124
1124
  padding-left: var(--spacer-1);
1125
1125
  }
1126
1126
 
@@ -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 {
@@ -5389,6 +5618,13 @@ fig-skeleton {
5389
5618
  overflow-y: auto;
5390
5619
  }
5391
5620
 
5621
+ fig-toolbelt::part(viewport) {
5622
+ --fig-overflow-fade-size: var(--fig-toolbelt-overflow-fade-size);
5623
+ --fig-overflow-fade-clear-top: 0px;
5624
+
5625
+ overflow-y: auto;
5626
+ }
5627
+
5392
5628
  .fig-overflow-fade.fig-overflow-fade-horizontal {
5393
5629
  --fig-overflow-fade-clear-left: 0px;
5394
5630
 
@@ -5396,8 +5632,16 @@ fig-skeleton {
5396
5632
  overflow-y: hidden;
5397
5633
  }
5398
5634
 
5635
+ fig-toolbelt:not([layout="vertical"])::part(viewport) {
5636
+ --fig-overflow-fade-clear-left: 0px;
5637
+
5638
+ overflow-x: auto;
5639
+ overflow-y: hidden;
5640
+ }
5641
+
5399
5642
  @supports (animation-timeline: scroll(self block)) {
5400
- .fig-overflow-fade:not(.fig-overflow-fade-horizontal) {
5643
+ .fig-overflow-fade:not(.fig-overflow-fade-horizontal),
5644
+ fig-toolbelt[layout="vertical"]::part(viewport) {
5401
5645
  mask-image: linear-gradient(
5402
5646
  to bottom,
5403
5647
  black 0,
@@ -5531,7 +5775,8 @@ fig-skeleton {
5531
5775
  }
5532
5776
 
5533
5777
  @supports (animation-timeline: scroll(self inline)) {
5534
- .fig-overflow-fade.fig-overflow-fade-horizontal {
5778
+ .fig-overflow-fade.fig-overflow-fade-horizontal,
5779
+ fig-toolbelt:not([layout="vertical"])::part(viewport) {
5535
5780
  mask-image: linear-gradient(
5536
5781
  to right,
5537
5782
  black 0,