mlola-ui 1.2.2 → 1.3.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/package.json +1 -1
- package/registry/agents.md +22 -0
- package/registry/catalog.json +18 -1
- package/registry/contract.json +27 -0
- package/registry/examples.json +104 -0
- package/registry/index.json +132 -48
- package/registry/source/packages/components/dial/dial.tsx +277 -0
- package/registry/source-manifest.json +1 -0
package/package.json
CHANGED
package/registry/agents.md
CHANGED
|
@@ -201,6 +201,10 @@ correct. This table is read out of the stylesheet, so it is never stale.
|
|
|
201
201
|
| `.ml-copy-button` | `data-status` | `error` |
|
|
202
202
|
| `.ml-date-picker-panel` | `data-presets` | _presence only_ |
|
|
203
203
|
| `.ml-date-picker-trigger` | `data-empty` | _presence only_ |
|
|
204
|
+
| `.ml-dial` | `data-turning` | _presence only_ |
|
|
205
|
+
| `.ml-dial-field` | `data-disabled` | _presence only_ |
|
|
206
|
+
| `.ml-dial-field` | `data-size` | `lg`, `sm` |
|
|
207
|
+
| `.ml-dial-field` | `data-tone` | `danger`, `info`, `success`, `warning` |
|
|
204
208
|
| `.ml-dropdown-item` | `data-danger` | _presence only_ |
|
|
205
209
|
| `.ml-dropdown-item` | `data-highlighted` | _presence only_ |
|
|
206
210
|
| `.ml-dropdown-menu` | `data-align` | `end` |
|
|
@@ -449,6 +453,24 @@ An on/off control that is not a native checkbox.
|
|
|
449
453
|
- On activate, set aria-checked and data-state to the opposite value.
|
|
450
454
|
- Note: Prefer a native checkbox unless the control genuinely reads as a switch.
|
|
451
455
|
|
|
456
|
+
### dial
|
|
457
|
+
|
|
458
|
+
A single value chosen from a range by turning a knob.
|
|
459
|
+
|
|
460
|
+
- Root: `.ml-dial-field`
|
|
461
|
+
- Parts: control `.ml-dial`, arc `.ml-dial-arc`, ticks `.ml-dial-ticks`, track `.ml-dial-track`, range `.ml-dial-range`, knob `.ml-dial-knob`, readout `.ml-dial-readout`, value `.ml-dial-value`
|
|
462
|
+
- ARIA on control: `role` — slider; `aria-valuenow` — current value; `aria-valuemin` — minimum; `aria-valuemax` — maximum; `aria-valuetext` — the value as it reads, with its unit; `tabindex` — 0 unless disabled
|
|
463
|
+
- Keyboard:
|
|
464
|
+
- <kbd>ArrowRight</kbd> / <kbd>ArrowUp</kbd>: Increase by one step.
|
|
465
|
+
- <kbd>ArrowLeft</kbd> / <kbd>ArrowDown</kbd>: Decrease by one step.
|
|
466
|
+
- <kbd>Home</kbd> / <kbd>End</kbd>: Jump to the minimum or maximum.
|
|
467
|
+
- <kbd>PageUp</kbd> / <kbd>PageDown</kbd>: Move by a larger step.
|
|
468
|
+
- <kbd>Escape</kbd>: During a turn, put back the value it began with.
|
|
469
|
+
- State changes:
|
|
470
|
+
- On mouse or pen down on the ring around the knob, set the value the press points to, snapped to the step to within min and max; a press in the gap at the bottom sets nothing.
|
|
471
|
+
- On mouse or pen down on the dial, then turning round its center, set the value by the angle turned; a turn past an end stops at that end to within min and max.
|
|
472
|
+
- On a finger held still on the dial for touchHold.delay, then turning, set the value by the angle turned; a finger that moves sooner scrolls the page and changes nothing, and a tap on the ring sets the value it points to to within min and max.
|
|
473
|
+
|
|
452
474
|
### slider
|
|
453
475
|
|
|
454
476
|
A single value chosen from a range.
|
package/registry/catalog.json
CHANGED
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
"support",
|
|
110
110
|
"marketing-site",
|
|
111
111
|
"docs-site",
|
|
112
|
-
"finance"
|
|
112
|
+
"finance",
|
|
113
|
+
"smart-home"
|
|
113
114
|
],
|
|
114
115
|
"items": [
|
|
115
116
|
{
|
|
@@ -1505,6 +1506,22 @@
|
|
|
1505
1506
|
"dashboard",
|
|
1506
1507
|
"premium"
|
|
1507
1508
|
]
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
"name": "smart-home",
|
|
1512
|
+
"type": "registry:template",
|
|
1513
|
+
"title": "Smart Home",
|
|
1514
|
+
"description": "A smart home: a thermostat you turn like the one on the wall, scenes that set the whole house at once, rooms with their lights, blinds and devices, today's electricity against yesterday and the solar panels, locks, sensors and the alarm, and automations",
|
|
1515
|
+
"category": "application",
|
|
1516
|
+
"tags": [
|
|
1517
|
+
"template",
|
|
1518
|
+
"smart-home",
|
|
1519
|
+
"iot",
|
|
1520
|
+
"thermostat",
|
|
1521
|
+
"energy",
|
|
1522
|
+
"dashboard",
|
|
1523
|
+
"premium"
|
|
1524
|
+
]
|
|
1508
1525
|
}
|
|
1509
1526
|
]
|
|
1510
1527
|
}
|
package/registry/contract.json
CHANGED
|
@@ -165,6 +165,17 @@
|
|
|
165
165
|
"ml-date-picker-trigger",
|
|
166
166
|
"ml-date-picker-value",
|
|
167
167
|
"ml-definition-list",
|
|
168
|
+
"ml-dial",
|
|
169
|
+
"ml-dial-arc",
|
|
170
|
+
"ml-dial-caption",
|
|
171
|
+
"ml-dial-field",
|
|
172
|
+
"ml-dial-knob",
|
|
173
|
+
"ml-dial-label",
|
|
174
|
+
"ml-dial-range",
|
|
175
|
+
"ml-dial-readout",
|
|
176
|
+
"ml-dial-ticks",
|
|
177
|
+
"ml-dial-track",
|
|
178
|
+
"ml-dial-value",
|
|
168
179
|
"ml-display",
|
|
169
180
|
"ml-divider",
|
|
170
181
|
"ml-dropdown",
|
|
@@ -686,6 +697,22 @@
|
|
|
686
697
|
"ml-date-picker-trigger": {
|
|
687
698
|
"data-empty": []
|
|
688
699
|
},
|
|
700
|
+
"ml-dial": {
|
|
701
|
+
"data-turning": []
|
|
702
|
+
},
|
|
703
|
+
"ml-dial-field": {
|
|
704
|
+
"data-disabled": [],
|
|
705
|
+
"data-size": [
|
|
706
|
+
"lg",
|
|
707
|
+
"sm"
|
|
708
|
+
],
|
|
709
|
+
"data-tone": [
|
|
710
|
+
"danger",
|
|
711
|
+
"info",
|
|
712
|
+
"success",
|
|
713
|
+
"warning"
|
|
714
|
+
]
|
|
715
|
+
},
|
|
689
716
|
"ml-dropdown-item": {
|
|
690
717
|
"data-danger": [],
|
|
691
718
|
"data-highlighted": []
|
package/registry/examples.json
CHANGED
|
@@ -1716,6 +1716,110 @@
|
|
|
1716
1716
|
}
|
|
1717
1717
|
}
|
|
1718
1718
|
},
|
|
1719
|
+
"dial": {
|
|
1720
|
+
"needs": "behavior",
|
|
1721
|
+
"react": "import { Dial } from \"@/components/ui/dial\";\n\nexport default function Example() {\n return <Dial label=\"Temperature\" min={10} max={30} step={0.5} defaultValue={21} format={(value) => `${value} °C`} />;\n}",
|
|
1722
|
+
"html": "<div class=\"ml-dial-field\" data-size=\"md\" data-tone=\"primary\">\n <span class=\"ml-dial-label\" id=\"dial-1-label\">Temperature</span>\n <div class=\"ml-dial\" id=\"dial-1\" role=\"slider\" tabindex=\"0\" aria-valuemin=\"10\" aria-valuemax=\"30\" aria-valuenow=\"21\" aria-valuetext=\"21 °C\" aria-labelledby=\"dial-1-label\" style=\"--ml-dial-angle:13.5deg\">\n <svg class=\"ml-dial-arc\" viewBox=\"0 0 100 100\" aria-hidden=\"true\" focusable=\"false\"><path class=\"ml-dial-ticks\" d=\"M 16.77 83.23 L 14.64 85.36 M 11.50 76.96 L 10.27 77.82 M 7.40 69.86 L 6.04 70.50 M 4.60 62.16 L 3.15 62.55 M 3.18 54.10 L 1.68 54.23 M 3.18 45.90 L 1.68 45.77 M 4.60 37.84 L 3.15 37.45 M 7.40 30.14 L 6.04 29.50 M 11.50 23.04 L 10.27 22.18 M 16.77 16.77 L 14.64 14.64 M 23.04 11.50 L 22.18 10.27 M 30.14 7.40 L 29.50 6.04 M 37.84 4.60 L 37.45 3.15 M 45.90 3.18 L 45.77 1.68 M 54.10 3.18 L 54.23 1.68 M 62.16 4.60 L 62.55 3.15 M 69.86 7.40 L 70.50 6.04 M 76.96 11.50 L 77.82 10.27 M 83.23 16.77 L 85.36 14.64 M 88.50 23.04 L 89.73 22.18 M 92.60 30.14 L 93.96 29.50 M 95.40 37.84 L 96.85 37.45 M 96.82 45.90 L 98.32 45.77 M 96.82 54.10 L 98.32 54.23 M 95.40 62.16 L 96.85 62.55 M 92.60 69.86 L 93.96 70.50 M 88.50 76.96 L 89.73 77.82 M 83.23 83.23 L 85.36 85.36\"></path><path class=\"ml-dial-track\" d=\"M 20.302 79.698 A 42 42 0 1 1 79.698 79.698\"></path><path class=\"ml-dial-range\" d=\"M 20.302 79.698 A 42 42 0 0 1 59.805 9.160\"></path></svg>\n <span class=\"ml-dial-knob\" aria-hidden=\"true\"></span>\n <span class=\"ml-dial-readout\" aria-hidden=\"true\"><span class=\"ml-dial-value\">21 °C</span></span>\n </div>\n</div>",
|
|
1723
|
+
"behavior": {
|
|
1724
|
+
"name": "dial",
|
|
1725
|
+
"html": "<div class=\"ml-dial-field\" data-ml=\"dial\" data-size=\"md\" data-tone=\"warning\" data-ml-step=\"0.5\" data-ml-unit=\" °C\">\n <span class=\"ml-dial-label\" id=\"thermostat-label\">Thermostat</span>\n <div class=\"ml-dial\" id=\"thermostat\" role=\"slider\" tabindex=\"0\"\n aria-labelledby=\"thermostat-label\" aria-describedby=\"thermostat-caption\"\n aria-valuemin=\"10\" aria-valuemax=\"30\" aria-valuenow=\"21.5\" aria-valuetext=\"21.5 °C\"\n style=\"--ml-dial-angle: 20.25deg\">\n <svg class=\"ml-dial-arc\" viewBox=\"0 0 100 100\" aria-hidden=\"true\" focusable=\"false\">\n <path class=\"ml-dial-ticks\" d=\"M 16.77 83.23 L 14.64 85.36 M 11.50 76.96 L 10.27 77.82 M 7.40 69.86 L 6.04 70.50 M 4.60 62.16 L 3.15 62.55 M 3.18 54.10 L 1.68 54.23 M 3.18 45.90 L 1.68 45.77 M 4.60 37.84 L 3.15 37.45 M 7.40 30.14 L 6.04 29.50 M 11.50 23.04 L 10.27 22.18 M 16.77 16.77 L 14.64 14.64 M 23.04 11.50 L 22.18 10.27 M 30.14 7.40 L 29.50 6.04 M 37.84 4.60 L 37.45 3.15 M 45.90 3.18 L 45.77 1.68 M 54.10 3.18 L 54.23 1.68 M 62.16 4.60 L 62.55 3.15 M 69.86 7.40 L 70.50 6.04 M 76.96 11.50 L 77.82 10.27 M 83.23 16.77 L 85.36 14.64 M 88.50 23.04 L 89.73 22.18 M 92.60 30.14 L 93.96 29.50 M 95.40 37.84 L 96.85 37.45 M 96.82 45.90 L 98.32 45.77 M 96.82 54.10 L 98.32 54.23 M 95.40 62.16 L 96.85 62.55 M 92.60 69.86 L 93.96 70.50 M 88.50 76.96 L 89.73 77.82 M 83.23 83.23 L 85.36 85.36\" />\n <path class=\"ml-dial-track\" d=\"M 20.302 79.698 A 42 42 0 1 1 79.698 79.698\" />\n <path class=\"ml-dial-range\" d=\"M 20.302 79.698 A 42 42 0 0 1 64.537 10.596\" />\n </svg>\n <span class=\"ml-dial-knob\" aria-hidden=\"true\"></span>\n <span class=\"ml-dial-readout\" aria-hidden=\"true\"><span class=\"ml-dial-value\">21.5 °C</span></span>\n </div>\n <span class=\"ml-dial-caption\" id=\"thermostat-caption\">Heating from 19.5 °C</span>\n</div>",
|
|
1726
|
+
"module": "@mlola-ui/behavior/dial"
|
|
1727
|
+
},
|
|
1728
|
+
"elements": [
|
|
1729
|
+
{
|
|
1730
|
+
"class": "ml-dial-field",
|
|
1731
|
+
"attributes": {
|
|
1732
|
+
"data-disabled": [],
|
|
1733
|
+
"data-size": [
|
|
1734
|
+
"lg",
|
|
1735
|
+
"sm"
|
|
1736
|
+
],
|
|
1737
|
+
"data-tone": [
|
|
1738
|
+
"danger",
|
|
1739
|
+
"info",
|
|
1740
|
+
"success",
|
|
1741
|
+
"warning"
|
|
1742
|
+
]
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
"class": "ml-dial-label",
|
|
1747
|
+
"attributes": {}
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
"class": "ml-dial-caption",
|
|
1751
|
+
"attributes": {}
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
"class": "ml-dial",
|
|
1755
|
+
"attributes": {
|
|
1756
|
+
"data-turning": []
|
|
1757
|
+
}
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
"class": "ml-dial-arc",
|
|
1761
|
+
"attributes": {}
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
"class": "ml-dial-ticks",
|
|
1765
|
+
"attributes": {}
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
"class": "ml-dial-track",
|
|
1769
|
+
"attributes": {}
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
"class": "ml-dial-range",
|
|
1773
|
+
"attributes": {}
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
"class": "ml-dial-knob",
|
|
1777
|
+
"attributes": {}
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
"class": "ml-dial-readout",
|
|
1781
|
+
"attributes": {}
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
"class": "ml-dial-value",
|
|
1785
|
+
"attributes": {}
|
|
1786
|
+
}
|
|
1787
|
+
],
|
|
1788
|
+
"rendered": {
|
|
1789
|
+
"ml-dial-field": {
|
|
1790
|
+
"data-size": [
|
|
1791
|
+
"lg",
|
|
1792
|
+
"md",
|
|
1793
|
+
"sm"
|
|
1794
|
+
],
|
|
1795
|
+
"data-tone": [
|
|
1796
|
+
"danger",
|
|
1797
|
+
"info",
|
|
1798
|
+
"primary",
|
|
1799
|
+
"success",
|
|
1800
|
+
"warning"
|
|
1801
|
+
]
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
"gallery": [
|
|
1805
|
+
{
|
|
1806
|
+
"slug": "thermostat",
|
|
1807
|
+
"file": "1-thermostat",
|
|
1808
|
+
"title": "A thermostat",
|
|
1809
|
+
"description": "format says the value in a unit, and caption and tone follow what the value means.",
|
|
1810
|
+
"react": "\"use client\";\n\nimport * as React from \"react\";\nimport { Dial } from \"@/components/ui/dial\";\n\nexport default function Example() {\n const room = 19.5;\n const [target, setTarget] = React.useState(21);\n const heating = target > room;\n return (\n <Dial\n label=\"Living room\"\n size=\"lg\"\n min={10}\n max={30}\n step={0.5}\n value={target}\n onValueChange={setTarget}\n format={(value) => `${value.toFixed(1)} °C`}\n tone={heating ? \"warning\" : \"info\"}\n caption={heating ? `Heating from ${room} °C` : `Holding at ${room} °C`}\n />\n );\n}",
|
|
1811
|
+
"html": "<div class=\"ml-dial-field\" data-size=\"lg\" data-tone=\"warning\">\n <span class=\"ml-dial-label\" id=\"dial-thermostat-1-label\">Living room</span>\n <div class=\"ml-dial\" id=\"dial-thermostat-1\" role=\"slider\" tabindex=\"0\" aria-valuemin=\"10\" aria-valuemax=\"30\" aria-valuenow=\"21\" aria-valuetext=\"21.0 °C\" aria-labelledby=\"dial-thermostat-1-label\" aria-describedby=\"dial-thermostat-1-caption\" style=\"--ml-dial-angle:13.5deg\">\n <svg class=\"ml-dial-arc\" viewBox=\"0 0 100 100\" aria-hidden=\"true\" focusable=\"false\"><path class=\"ml-dial-ticks\" d=\"M 16.77 83.23 L 14.64 85.36 M 11.50 76.96 L 10.27 77.82 M 7.40 69.86 L 6.04 70.50 M 4.60 62.16 L 3.15 62.55 M 3.18 54.10 L 1.68 54.23 M 3.18 45.90 L 1.68 45.77 M 4.60 37.84 L 3.15 37.45 M 7.40 30.14 L 6.04 29.50 M 11.50 23.04 L 10.27 22.18 M 16.77 16.77 L 14.64 14.64 M 23.04 11.50 L 22.18 10.27 M 30.14 7.40 L 29.50 6.04 M 37.84 4.60 L 37.45 3.15 M 45.90 3.18 L 45.77 1.68 M 54.10 3.18 L 54.23 1.68 M 62.16 4.60 L 62.55 3.15 M 69.86 7.40 L 70.50 6.04 M 76.96 11.50 L 77.82 10.27 M 83.23 16.77 L 85.36 14.64 M 88.50 23.04 L 89.73 22.18 M 92.60 30.14 L 93.96 29.50 M 95.40 37.84 L 96.85 37.45 M 96.82 45.90 L 98.32 45.77 M 96.82 54.10 L 98.32 54.23 M 95.40 62.16 L 96.85 62.55 M 92.60 69.86 L 93.96 70.50 M 88.50 76.96 L 89.73 77.82 M 83.23 83.23 L 85.36 85.36\"></path><path class=\"ml-dial-track\" d=\"M 20.302 79.698 A 42 42 0 1 1 79.698 79.698\"></path><path class=\"ml-dial-range\" d=\"M 20.302 79.698 A 42 42 0 0 1 59.805 9.160\"></path></svg>\n <span class=\"ml-dial-knob\" aria-hidden=\"true\"></span>\n <span class=\"ml-dial-readout\" aria-hidden=\"true\"><span class=\"ml-dial-value\">21.0 °C</span></span>\n </div>\n <span class=\"ml-dial-caption\" id=\"dial-thermostat-1-caption\">Heating from 19.5 °C</span>\n</div>"
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
"slug": "in-a-form",
|
|
1815
|
+
"file": "2-in-a-form",
|
|
1816
|
+
"title": "In a form",
|
|
1817
|
+
"description": "With a name, the dial submits its value like a native input.",
|
|
1818
|
+
"react": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Dial } from \"@/components/ui/dial\";\n\nexport default function Example() {\n const [sent, setSent] = React.useState<string | null>(null);\n return (\n <form\n className=\"ml-stack\"\n onSubmit={(event) => {\n event.preventDefault();\n setSent(String(new FormData(event.currentTarget).get(\"speed\")));\n }}\n >\n <Dial label=\"Fan speed\" name=\"speed\" size=\"sm\" min={0} max={4} step={1} defaultValue={2} format={(value) => (value === 0 ? \"Off\" : String(value))} />\n <Button type=\"submit\" variant=\"secondary\">\n Save\n </Button>\n <p role=\"status\">{sent === null ? \"\" : `Saved speed ${sent}`}</p>\n </form>\n );\n}",
|
|
1819
|
+
"html": "<form class=\"ml-stack\">\n <div class=\"ml-dial-field\" data-size=\"sm\" data-tone=\"primary\">\n <span class=\"ml-dial-label\" id=\"dial-in-a-form-1-label\">Fan speed</span>\n <div class=\"ml-dial\" id=\"dial-in-a-form-1\" role=\"slider\" tabindex=\"0\" aria-valuemin=\"0\" aria-valuemax=\"4\" aria-valuenow=\"2\" aria-valuetext=\"2\" aria-labelledby=\"dial-in-a-form-1-label\" style=\"--ml-dial-angle:0deg\">\n <svg class=\"ml-dial-arc\" viewBox=\"0 0 100 100\" aria-hidden=\"true\" focusable=\"false\"><path class=\"ml-dial-ticks\" d=\"M 16.77 83.23 L 14.64 85.36 M 11.50 76.96 L 10.27 77.82 M 7.40 69.86 L 6.04 70.50 M 4.60 62.16 L 3.15 62.55 M 3.18 54.10 L 1.68 54.23 M 3.18 45.90 L 1.68 45.77 M 4.60 37.84 L 3.15 37.45 M 7.40 30.14 L 6.04 29.50 M 11.50 23.04 L 10.27 22.18 M 16.77 16.77 L 14.64 14.64 M 23.04 11.50 L 22.18 10.27 M 30.14 7.40 L 29.50 6.04 M 37.84 4.60 L 37.45 3.15 M 45.90 3.18 L 45.77 1.68 M 54.10 3.18 L 54.23 1.68 M 62.16 4.60 L 62.55 3.15 M 69.86 7.40 L 70.50 6.04 M 76.96 11.50 L 77.82 10.27 M 83.23 16.77 L 85.36 14.64 M 88.50 23.04 L 89.73 22.18 M 92.60 30.14 L 93.96 29.50 M 95.40 37.84 L 96.85 37.45 M 96.82 45.90 L 98.32 45.77 M 96.82 54.10 L 98.32 54.23 M 95.40 62.16 L 96.85 62.55 M 92.60 69.86 L 93.96 70.50 M 88.50 76.96 L 89.73 77.82 M 83.23 83.23 L 85.36 85.36\"></path><path class=\"ml-dial-track\" d=\"M 20.302 79.698 A 42 42 0 1 1 79.698 79.698\"></path><path class=\"ml-dial-range\" d=\"M 20.302 79.698 A 42 42 0 0 1 50.000 8.000\"></path></svg>\n <span class=\"ml-dial-knob\" aria-hidden=\"true\"></span>\n <span class=\"ml-dial-readout\" aria-hidden=\"true\"><span class=\"ml-dial-value\">2</span></span>\n </div>\n <input type=\"hidden\" name=\"speed\" value=\"2\"/>\n </div>\n <button class=\"ml-button\" type=\"submit\" data-variant=\"secondary\" data-size=\"md\"><span class=\"ml-button-label\">Save</span></button>\n <p role=\"status\"></p>\n</form>"
|
|
1820
|
+
}
|
|
1821
|
+
]
|
|
1822
|
+
},
|
|
1719
1823
|
"table": {
|
|
1720
1824
|
"needs": "nothing",
|
|
1721
1825
|
"react": "import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\";\n\nconst invoices = [\n { id: \"INV-104\", status: \"Paid\", amount: \"$240.00\" },\n { id: \"INV-105\", status: \"Open\", amount: \"$1,120.00\" },\n];\n\nexport default function Example() {\n return (\n <Table>\n <TableCaption>Recent invoices</TableCaption>\n <TableHeader>\n <TableRow>\n <TableHead>Invoice</TableHead>\n <TableHead>Status</TableHead>\n <TableHead align=\"right\">Amount</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {invoices.map((invoice) => (\n <TableRow key={invoice.id}>\n <TableCell>{invoice.id}</TableCell>\n <TableCell>{invoice.status}</TableCell>\n <TableCell numeric>{invoice.amount}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n );\n}",
|
package/registry/index.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "./schema/registry-index.schema.json",
|
|
3
3
|
"schemaVersion": 2,
|
|
4
4
|
"name": "mlola-ui",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.3.1",
|
|
6
6
|
"homepage": "https://ui.mlola.com",
|
|
7
7
|
"themeContract": {
|
|
8
8
|
"attribute": "data-theme",
|
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
"sheet",
|
|
127
127
|
"skeleton",
|
|
128
128
|
"slider",
|
|
129
|
+
"dial",
|
|
129
130
|
"table",
|
|
130
131
|
"tabs",
|
|
131
132
|
"textarea",
|
|
@@ -177,8 +178,8 @@
|
|
|
177
178
|
},
|
|
178
179
|
"registryDependencies": [],
|
|
179
180
|
"engineDependencies": {
|
|
180
|
-
"@mlola-ui/behavior": "^1.
|
|
181
|
-
"@mlola-ui/icons": "^1.
|
|
181
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
182
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
182
183
|
},
|
|
183
184
|
"options": [
|
|
184
185
|
{
|
|
@@ -239,7 +240,7 @@
|
|
|
239
240
|
},
|
|
240
241
|
"registryDependencies": [],
|
|
241
242
|
"engineDependencies": {
|
|
242
|
-
"@mlola-ui/icons": "^1.
|
|
243
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
243
244
|
},
|
|
244
245
|
"options": [
|
|
245
246
|
{
|
|
@@ -401,7 +402,7 @@
|
|
|
401
402
|
},
|
|
402
403
|
"registryDependencies": [],
|
|
403
404
|
"engineDependencies": {
|
|
404
|
-
"@mlola-ui/icons": "^1.
|
|
405
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
405
406
|
},
|
|
406
407
|
"options": [
|
|
407
408
|
{
|
|
@@ -493,7 +494,7 @@
|
|
|
493
494
|
},
|
|
494
495
|
"registryDependencies": [],
|
|
495
496
|
"engineDependencies": {
|
|
496
|
-
"@mlola-ui/icons": "^1.
|
|
497
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
497
498
|
},
|
|
498
499
|
"options": [],
|
|
499
500
|
"variants": [],
|
|
@@ -546,7 +547,7 @@
|
|
|
546
547
|
"spinner"
|
|
547
548
|
],
|
|
548
549
|
"engineDependencies": {
|
|
549
|
-
"@mlola-ui/motion": "^1.
|
|
550
|
+
"@mlola-ui/motion": "^1.3.1"
|
|
550
551
|
},
|
|
551
552
|
"options": [
|
|
552
553
|
{
|
|
@@ -714,8 +715,8 @@
|
|
|
714
715
|
},
|
|
715
716
|
"registryDependencies": [],
|
|
716
717
|
"engineDependencies": {
|
|
717
|
-
"@mlola-ui/behavior": "^1.
|
|
718
|
-
"@mlola-ui/icons": "^1.
|
|
718
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
719
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
719
720
|
},
|
|
720
721
|
"options": [
|
|
721
722
|
{
|
|
@@ -784,7 +785,7 @@
|
|
|
784
785
|
},
|
|
785
786
|
"registryDependencies": [],
|
|
786
787
|
"engineDependencies": {
|
|
787
|
-
"@mlola-ui/icons": "^1.
|
|
788
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
788
789
|
},
|
|
789
790
|
"options": [],
|
|
790
791
|
"variants": [],
|
|
@@ -839,7 +840,7 @@
|
|
|
839
840
|
"modal"
|
|
840
841
|
],
|
|
841
842
|
"engineDependencies": {
|
|
842
|
-
"@mlola-ui/icons": "^1.
|
|
843
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
843
844
|
},
|
|
844
845
|
"options": [],
|
|
845
846
|
"variants": [],
|
|
@@ -903,8 +904,8 @@
|
|
|
903
904
|
},
|
|
904
905
|
"registryDependencies": [],
|
|
905
906
|
"engineDependencies": {
|
|
906
|
-
"@mlola-ui/behavior": "^1.
|
|
907
|
-
"@mlola-ui/icons": "^1.
|
|
907
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
908
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
908
909
|
},
|
|
909
910
|
"options": [
|
|
910
911
|
{
|
|
@@ -993,7 +994,7 @@
|
|
|
993
994
|
"input"
|
|
994
995
|
],
|
|
995
996
|
"engineDependencies": {
|
|
996
|
-
"@mlola-ui/icons": "^1.
|
|
997
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
997
998
|
},
|
|
998
999
|
"options": [],
|
|
999
1000
|
"variants": [],
|
|
@@ -1218,8 +1219,8 @@
|
|
|
1218
1219
|
},
|
|
1219
1220
|
"registryDependencies": [],
|
|
1220
1221
|
"engineDependencies": {
|
|
1221
|
-
"@mlola-ui/behavior": "^1.
|
|
1222
|
-
"@mlola-ui/icons": "^1.
|
|
1222
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
1223
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
1223
1224
|
},
|
|
1224
1225
|
"options": [
|
|
1225
1226
|
{
|
|
@@ -1370,7 +1371,7 @@
|
|
|
1370
1371
|
},
|
|
1371
1372
|
"registryDependencies": [],
|
|
1372
1373
|
"engineDependencies": {
|
|
1373
|
-
"@mlola-ui/icons": "^1.
|
|
1374
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
1374
1375
|
},
|
|
1375
1376
|
"options": [],
|
|
1376
1377
|
"variants": [],
|
|
@@ -1421,7 +1422,7 @@
|
|
|
1421
1422
|
},
|
|
1422
1423
|
"registryDependencies": [],
|
|
1423
1424
|
"engineDependencies": {
|
|
1424
|
-
"@mlola-ui/behavior": "^1.
|
|
1425
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
1425
1426
|
},
|
|
1426
1427
|
"options": [
|
|
1427
1428
|
{
|
|
@@ -1799,7 +1800,7 @@
|
|
|
1799
1800
|
},
|
|
1800
1801
|
"registryDependencies": [],
|
|
1801
1802
|
"engineDependencies": {
|
|
1802
|
-
"@mlola-ui/behavior": "^1.
|
|
1803
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
1803
1804
|
},
|
|
1804
1805
|
"options": [],
|
|
1805
1806
|
"variants": [],
|
|
@@ -1852,8 +1853,8 @@
|
|
|
1852
1853
|
"input"
|
|
1853
1854
|
],
|
|
1854
1855
|
"engineDependencies": {
|
|
1855
|
-
"@mlola-ui/behavior": "^1.
|
|
1856
|
-
"@mlola-ui/icons": "^1.
|
|
1856
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
1857
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
1857
1858
|
},
|
|
1858
1859
|
"options": [
|
|
1859
1860
|
{
|
|
@@ -1931,8 +1932,8 @@
|
|
|
1931
1932
|
},
|
|
1932
1933
|
"registryDependencies": [],
|
|
1933
1934
|
"engineDependencies": {
|
|
1934
|
-
"@mlola-ui/behavior": "^1.
|
|
1935
|
-
"@mlola-ui/icons": "^1.
|
|
1935
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
1936
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
1936
1937
|
},
|
|
1937
1938
|
"options": [
|
|
1938
1939
|
{
|
|
@@ -2078,7 +2079,7 @@
|
|
|
2078
2079
|
"input"
|
|
2079
2080
|
],
|
|
2080
2081
|
"engineDependencies": {
|
|
2081
|
-
"@mlola-ui/behavior": "^1.
|
|
2082
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
2082
2083
|
},
|
|
2083
2084
|
"options": [
|
|
2084
2085
|
{
|
|
@@ -2122,6 +2123,89 @@
|
|
|
2122
2123
|
]
|
|
2123
2124
|
}
|
|
2124
2125
|
},
|
|
2126
|
+
{
|
|
2127
|
+
"$schema": "../schema/registry-item.schema.json",
|
|
2128
|
+
"schemaVersion": 2,
|
|
2129
|
+
"name": "dial",
|
|
2130
|
+
"title": "Dial",
|
|
2131
|
+
"description": "A value chosen by turning a knob, with a press on the ring to jump, a hold to turn by finger, Escape to undo a turn and the keys of a slider",
|
|
2132
|
+
"category": "forms",
|
|
2133
|
+
"tags": [
|
|
2134
|
+
"form",
|
|
2135
|
+
"input",
|
|
2136
|
+
"range",
|
|
2137
|
+
"knob",
|
|
2138
|
+
"thermostat"
|
|
2139
|
+
],
|
|
2140
|
+
"type": "registry:ui",
|
|
2141
|
+
"usage": {
|
|
2142
|
+
"importPath": "{{aliases.components}}/dial",
|
|
2143
|
+
"exportName": "Dial",
|
|
2144
|
+
"isDefault": false
|
|
2145
|
+
},
|
|
2146
|
+
"registryDependencies": [
|
|
2147
|
+
"input"
|
|
2148
|
+
],
|
|
2149
|
+
"engineDependencies": {
|
|
2150
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
2151
|
+
},
|
|
2152
|
+
"options": [
|
|
2153
|
+
{
|
|
2154
|
+
"component": "Dial",
|
|
2155
|
+
"prop": "size",
|
|
2156
|
+
"values": [
|
|
2157
|
+
"sm",
|
|
2158
|
+
"md",
|
|
2159
|
+
"lg"
|
|
2160
|
+
],
|
|
2161
|
+
"default": "md"
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
"component": "Dial",
|
|
2165
|
+
"prop": "tone",
|
|
2166
|
+
"values": [
|
|
2167
|
+
"primary",
|
|
2168
|
+
"info",
|
|
2169
|
+
"success",
|
|
2170
|
+
"warning",
|
|
2171
|
+
"danger"
|
|
2172
|
+
],
|
|
2173
|
+
"default": "primary",
|
|
2174
|
+
"description": "The arc's color, as the shared vocabulary names it."
|
|
2175
|
+
}
|
|
2176
|
+
],
|
|
2177
|
+
"variants": [],
|
|
2178
|
+
"sizes": [
|
|
2179
|
+
"sm",
|
|
2180
|
+
"md",
|
|
2181
|
+
"lg"
|
|
2182
|
+
],
|
|
2183
|
+
"files": [
|
|
2184
|
+
{
|
|
2185
|
+
"path": "packages/components/_internal/react.ts",
|
|
2186
|
+
"target": "{{aliases.components}}/_internal/react.ts",
|
|
2187
|
+
"type": "registry:ui",
|
|
2188
|
+
"integrity": "sha256-INfds1J2f1MLpN6kBXz0GkfBEMIiRlhv0DXX455qAZo="
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
"path": "packages/components/dial/dial.tsx",
|
|
2192
|
+
"target": "{{aliases.components}}/dial.tsx",
|
|
2193
|
+
"type": "registry:ui",
|
|
2194
|
+
"integrity": "sha256-YhncOvnvBmT1GtWN8o7sgtMjmXvjaXVzTknK98osDno="
|
|
2195
|
+
}
|
|
2196
|
+
],
|
|
2197
|
+
"themeContract": {
|
|
2198
|
+
"attribute": "data-theme",
|
|
2199
|
+
"modeAttribute": "data-mode",
|
|
2200
|
+
"compatibleThemes": [
|
|
2201
|
+
"graphite",
|
|
2202
|
+
"atelier",
|
|
2203
|
+
"machined",
|
|
2204
|
+
"aerogel",
|
|
2205
|
+
"nordic"
|
|
2206
|
+
]
|
|
2207
|
+
}
|
|
2208
|
+
},
|
|
2125
2209
|
{
|
|
2126
2210
|
"$schema": "../schema/registry-item.schema.json",
|
|
2127
2211
|
"schemaVersion": 2,
|
|
@@ -2223,7 +2307,7 @@
|
|
|
2223
2307
|
},
|
|
2224
2308
|
"registryDependencies": [],
|
|
2225
2309
|
"engineDependencies": {
|
|
2226
|
-
"@mlola-ui/behavior": "^1.
|
|
2310
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
2227
2311
|
},
|
|
2228
2312
|
"options": [
|
|
2229
2313
|
{
|
|
@@ -2355,7 +2439,7 @@
|
|
|
2355
2439
|
},
|
|
2356
2440
|
"registryDependencies": [],
|
|
2357
2441
|
"engineDependencies": {
|
|
2358
|
-
"@mlola-ui/icons": "^1.
|
|
2442
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
2359
2443
|
},
|
|
2360
2444
|
"options": [
|
|
2361
2445
|
{
|
|
@@ -2443,8 +2527,8 @@
|
|
|
2443
2527
|
"spinner"
|
|
2444
2528
|
],
|
|
2445
2529
|
"engineDependencies": {
|
|
2446
|
-
"@mlola-ui/behavior": "^1.
|
|
2447
|
-
"@mlola-ui/icons": "^1.
|
|
2530
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
2531
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
2448
2532
|
},
|
|
2449
2533
|
"options": [
|
|
2450
2534
|
{
|
|
@@ -2599,7 +2683,7 @@
|
|
|
2599
2683
|
},
|
|
2600
2684
|
"registryDependencies": [],
|
|
2601
2685
|
"engineDependencies": {
|
|
2602
|
-
"@mlola-ui/behavior": "^1.
|
|
2686
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
2603
2687
|
},
|
|
2604
2688
|
"options": [
|
|
2605
2689
|
{
|
|
@@ -2676,7 +2760,7 @@
|
|
|
2676
2760
|
},
|
|
2677
2761
|
"registryDependencies": [],
|
|
2678
2762
|
"engineDependencies": {
|
|
2679
|
-
"@mlola-ui/behavior": "^1.
|
|
2763
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
2680
2764
|
},
|
|
2681
2765
|
"options": [
|
|
2682
2766
|
{
|
|
@@ -2758,7 +2842,7 @@
|
|
|
2758
2842
|
},
|
|
2759
2843
|
"registryDependencies": [],
|
|
2760
2844
|
"engineDependencies": {
|
|
2761
|
-
"@mlola-ui/icons": "^1.
|
|
2845
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
2762
2846
|
},
|
|
2763
2847
|
"options": [
|
|
2764
2848
|
{
|
|
@@ -2830,7 +2914,7 @@
|
|
|
2830
2914
|
"popover"
|
|
2831
2915
|
],
|
|
2832
2916
|
"engineDependencies": {
|
|
2833
|
-
"@mlola-ui/icons": "^1.
|
|
2917
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
2834
2918
|
},
|
|
2835
2919
|
"options": [
|
|
2836
2920
|
{
|
|
@@ -2900,7 +2984,7 @@
|
|
|
2900
2984
|
"popover"
|
|
2901
2985
|
],
|
|
2902
2986
|
"engineDependencies": {
|
|
2903
|
-
"@mlola-ui/icons": "^1.
|
|
2987
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
2904
2988
|
},
|
|
2905
2989
|
"options": [],
|
|
2906
2990
|
"variants": [],
|
|
@@ -3098,8 +3182,8 @@
|
|
|
3098
3182
|
"input"
|
|
3099
3183
|
],
|
|
3100
3184
|
"engineDependencies": {
|
|
3101
|
-
"@mlola-ui/behavior": "^1.
|
|
3102
|
-
"@mlola-ui/icons": "^1.
|
|
3185
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
3186
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3103
3187
|
},
|
|
3104
3188
|
"options": [
|
|
3105
3189
|
{
|
|
@@ -3188,7 +3272,7 @@
|
|
|
3188
3272
|
"input"
|
|
3189
3273
|
],
|
|
3190
3274
|
"engineDependencies": {
|
|
3191
|
-
"@mlola-ui/icons": "^1.
|
|
3275
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3192
3276
|
},
|
|
3193
3277
|
"options": [],
|
|
3194
3278
|
"variants": [],
|
|
@@ -3243,7 +3327,7 @@
|
|
|
3243
3327
|
"input"
|
|
3244
3328
|
],
|
|
3245
3329
|
"engineDependencies": {
|
|
3246
|
-
"@mlola-ui/icons": "^1.
|
|
3330
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3247
3331
|
},
|
|
3248
3332
|
"options": [],
|
|
3249
3333
|
"variants": [],
|
|
@@ -3306,7 +3390,7 @@
|
|
|
3306
3390
|
"popover"
|
|
3307
3391
|
],
|
|
3308
3392
|
"engineDependencies": {
|
|
3309
|
-
"@mlola-ui/icons": "^1.
|
|
3393
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3310
3394
|
},
|
|
3311
3395
|
"options": [
|
|
3312
3396
|
{
|
|
@@ -3377,7 +3461,7 @@
|
|
|
3377
3461
|
},
|
|
3378
3462
|
"registryDependencies": [],
|
|
3379
3463
|
"engineDependencies": {
|
|
3380
|
-
"@mlola-ui/icons": "^1.
|
|
3464
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3381
3465
|
},
|
|
3382
3466
|
"options": [
|
|
3383
3467
|
{
|
|
@@ -3496,8 +3580,8 @@
|
|
|
3496
3580
|
},
|
|
3497
3581
|
"registryDependencies": [],
|
|
3498
3582
|
"engineDependencies": {
|
|
3499
|
-
"@mlola-ui/behavior": "^1.
|
|
3500
|
-
"@mlola-ui/icons": "^1.
|
|
3583
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
3584
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3501
3585
|
},
|
|
3502
3586
|
"options": [],
|
|
3503
3587
|
"variants": [],
|
|
@@ -3561,7 +3645,7 @@
|
|
|
3561
3645
|
},
|
|
3562
3646
|
"registryDependencies": [],
|
|
3563
3647
|
"engineDependencies": {
|
|
3564
|
-
"@mlola-ui/behavior": "^1.
|
|
3648
|
+
"@mlola-ui/behavior": "^1.3.1"
|
|
3565
3649
|
},
|
|
3566
3650
|
"options": [
|
|
3567
3651
|
{
|
|
@@ -3648,8 +3732,8 @@
|
|
|
3648
3732
|
},
|
|
3649
3733
|
"registryDependencies": [],
|
|
3650
3734
|
"engineDependencies": {
|
|
3651
|
-
"@mlola-ui/behavior": "^1.
|
|
3652
|
-
"@mlola-ui/icons": "^1.
|
|
3735
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
3736
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3653
3737
|
},
|
|
3654
3738
|
"options": [],
|
|
3655
3739
|
"variants": [],
|
|
@@ -3723,7 +3807,7 @@
|
|
|
3723
3807
|
"sheet"
|
|
3724
3808
|
],
|
|
3725
3809
|
"engineDependencies": {
|
|
3726
|
-
"@mlola-ui/icons": "^1.
|
|
3810
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3727
3811
|
},
|
|
3728
3812
|
"options": [],
|
|
3729
3813
|
"variants": [],
|
|
@@ -3846,7 +3930,7 @@
|
|
|
3846
3930
|
"button"
|
|
3847
3931
|
],
|
|
3848
3932
|
"engineDependencies": {
|
|
3849
|
-
"@mlola-ui/icons": "^1.
|
|
3933
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3850
3934
|
},
|
|
3851
3935
|
"options": [
|
|
3852
3936
|
{
|
|
@@ -3950,8 +4034,8 @@
|
|
|
3950
4034
|
"input"
|
|
3951
4035
|
],
|
|
3952
4036
|
"engineDependencies": {
|
|
3953
|
-
"@mlola-ui/behavior": "^1.
|
|
3954
|
-
"@mlola-ui/icons": "^1.
|
|
4037
|
+
"@mlola-ui/behavior": "^1.3.1",
|
|
4038
|
+
"@mlola-ui/icons": "^1.3.1"
|
|
3955
4039
|
},
|
|
3956
4040
|
"options": [
|
|
3957
4041
|
{
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { clampToStep, dialAngle, dialArc, dialFace, dialFit, dialPositionAt, dialTicks, dialTurn, percentOf, sliderValueForKey, touchHold, valueFromRatio } from "@mlola-ui/behavior/logic";
|
|
5
|
+
import { cx, useControllableState } from "../_internal/react";
|
|
6
|
+
import { FieldValue, type FormControlProps } from "../input/input";
|
|
7
|
+
|
|
8
|
+
type DialSize = "sm" | "md" | "lg";
|
|
9
|
+
type DialTone = "primary" | "info" | "success" | "warning" | "danger";
|
|
10
|
+
|
|
11
|
+
interface DialProps extends Omit<FormControlProps, "required"> {
|
|
12
|
+
value?: number;
|
|
13
|
+
defaultValue?: number;
|
|
14
|
+
onValueChange?: (value: number) => void;
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
step?: number;
|
|
18
|
+
label?: React.ReactNode;
|
|
19
|
+
/** How the value reads, in the dial and to a screen reader: (21.5) => "21.5 °C". */
|
|
20
|
+
format?: (value: number) => string;
|
|
21
|
+
/** A line under the value inside the dial: "Heating to 21 °C". */
|
|
22
|
+
caption?: React.ReactNode;
|
|
23
|
+
size?: DialSize;
|
|
24
|
+
/** The arc's color, as the shared vocabulary names it. */
|
|
25
|
+
tone?: DialTone;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The knob fills the circle inside the arc: a press there turns it, a press on the ring sets the value it points to. */
|
|
30
|
+
const KNOB = dialFace.knob;
|
|
31
|
+
|
|
32
|
+
/** The scale of the instrument, drawn once. */
|
|
33
|
+
const TICKS = dialTicks();
|
|
34
|
+
|
|
35
|
+
interface Gesture {
|
|
36
|
+
id: number;
|
|
37
|
+
touch: boolean;
|
|
38
|
+
/** A finger has held still long enough to take the dial from the page's scroll. */
|
|
39
|
+
active: boolean;
|
|
40
|
+
/** The value when the gesture began, for Escape to put back. */
|
|
41
|
+
start: number;
|
|
42
|
+
/** The unrounded position between moves, so small turns add up. */
|
|
43
|
+
position: number;
|
|
44
|
+
/** Where the pointer was last, from the center. */
|
|
45
|
+
last: { dx: number; dy: number };
|
|
46
|
+
/** Where the finger first touched, in the window, to tell a hold from a scroll. */
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
timer?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A value chosen by turning a knob: a thermostat, a fan's speed, a timer.
|
|
54
|
+
* A press on the ring sets the value it points to; a press on the knob turns
|
|
55
|
+
* it from where it is, and a turn past an end stops there. A finger holds
|
|
56
|
+
* still for a moment before it turns the dial, so a finger scrolling past it
|
|
57
|
+
* scrolls the page; a tap on the ring sets the value. Escape during a turn
|
|
58
|
+
* puts the value back. The keys are a slider's: arrows step, Page keys take
|
|
59
|
+
* large steps, Home and End go to the ends.
|
|
60
|
+
*/
|
|
61
|
+
const Dial = React.forwardRef<HTMLDivElement, DialProps>(
|
|
62
|
+
({ value, defaultValue, onValueChange, min = 0, max = 100, step = 1, disabled = false, label, format, caption, size = "md", tone = "primary", className, id, name, form, "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": describedBy }, ref) => {
|
|
63
|
+
const low = Number.isFinite(min) ? min : 0;
|
|
64
|
+
const high = Number.isFinite(max) && max > low ? max : low + 100;
|
|
65
|
+
const increment = Number.isFinite(step) && step > 0 ? step : 1;
|
|
66
|
+
// Shared with the framework-free runtime, so both round and turn identically.
|
|
67
|
+
const bounds = React.useMemo(() => ({ min: low, max: high, step: increment }), [low, high, increment]);
|
|
68
|
+
const normalize = React.useCallback((raw: number) => clampToStep(raw, bounds), [bounds]);
|
|
69
|
+
const [currentRaw, setCurrent] = useControllableState({ value, defaultValue: normalize(defaultValue ?? (low + high) / 2), onChange: onValueChange });
|
|
70
|
+
const current = normalize(currentRaw);
|
|
71
|
+
const [turning, setTurning] = React.useState(false);
|
|
72
|
+
const control = React.useRef<HTMLDivElement | null>(null);
|
|
73
|
+
const gesture = React.useRef<Gesture | null>(null);
|
|
74
|
+
const generated = React.useId();
|
|
75
|
+
const dialId = id ?? generated;
|
|
76
|
+
const labelId = label ? `${dialId}-label` : undefined;
|
|
77
|
+
const captionId = caption ? `${dialId}-caption` : undefined;
|
|
78
|
+
const position = percentOf(current, bounds) / 100;
|
|
79
|
+
const text = format ? format(current) : String(current);
|
|
80
|
+
|
|
81
|
+
const setRef = React.useCallback(
|
|
82
|
+
(node: HTMLDivElement | null) => {
|
|
83
|
+
control.current = node;
|
|
84
|
+
if (typeof ref === "function") ref(node);
|
|
85
|
+
else if (ref) ref.current = node;
|
|
86
|
+
},
|
|
87
|
+
[ref],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// A long value, or a wide typeface, would reach the notch on the knob's
|
|
91
|
+
// rim: the readout shrinks to the room it has. Measured after layout and
|
|
92
|
+
// again when the text's size changes (a web font arriving).
|
|
93
|
+
const readout = React.useRef<HTMLSpanElement | null>(null);
|
|
94
|
+
React.useLayoutEffect(() => {
|
|
95
|
+
const value = readout.current;
|
|
96
|
+
const dial = control.current;
|
|
97
|
+
if (!value || !dial) return;
|
|
98
|
+
const fit = () => dial.style.setProperty("--ml-dial-fit", String(dialFit(value.offsetWidth, dial.offsetWidth)));
|
|
99
|
+
fit();
|
|
100
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
101
|
+
const observer = new ResizeObserver(fit);
|
|
102
|
+
observer.observe(value);
|
|
103
|
+
observer.observe(dial);
|
|
104
|
+
return () => observer.disconnect();
|
|
105
|
+
}, [text, size]);
|
|
106
|
+
|
|
107
|
+
/** The pointer's place from the dial's center, and the knob's radius, in pixels. */
|
|
108
|
+
const measure = (clientX: number, clientY: number) => {
|
|
109
|
+
const box = control.current!.getBoundingClientRect();
|
|
110
|
+
return { dx: clientX - (box.left + box.width / 2), dy: clientY - (box.top + box.height / 2), knob: (box.width / 2) * KNOB };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Window listeners for one gesture: Escape (in the capture phase, ahead of
|
|
114
|
+
// a dialog that would close on it) and a finger's moves once it has hold.
|
|
115
|
+
const listeners = React.useRef<{ key: (event: KeyboardEvent) => void; touchMove: (event: TouchEvent) => void } | null>(null);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Let go of the gesture: its timer and its window listeners. It reads refs
|
|
119
|
+
* only, so it is the same function on every render. Tied to a callback, it
|
|
120
|
+
* would run whenever a parent passed a new `onValueChange` (an inline
|
|
121
|
+
* arrow does, on every render) and end the turn after its first step.
|
|
122
|
+
*/
|
|
123
|
+
const release = React.useCallback(() => {
|
|
124
|
+
const entry = gesture.current;
|
|
125
|
+
gesture.current = null;
|
|
126
|
+
if (!entry) return null;
|
|
127
|
+
window.clearTimeout(entry.timer);
|
|
128
|
+
if (listeners.current) {
|
|
129
|
+
window.removeEventListener("keydown", listeners.current.key, true);
|
|
130
|
+
window.removeEventListener("touchmove", listeners.current.touchMove);
|
|
131
|
+
listeners.current = null;
|
|
132
|
+
}
|
|
133
|
+
return entry;
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const end = (restore: boolean) => {
|
|
137
|
+
const entry = release();
|
|
138
|
+
if (!entry) return;
|
|
139
|
+
setTurning(false);
|
|
140
|
+
if (restore) setCurrent(entry.start);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Leaving the page mid-turn leaves no listener behind.
|
|
144
|
+
React.useEffect(
|
|
145
|
+
() => () => {
|
|
146
|
+
release();
|
|
147
|
+
},
|
|
148
|
+
[release],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const begin = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
152
|
+
if (disabled || event.button !== 0) return;
|
|
153
|
+
const touch = event.pointerType === "touch";
|
|
154
|
+
const where = measure(event.clientX, event.clientY);
|
|
155
|
+
const pointed = dialPositionAt(where.dx, where.dy, where.knob);
|
|
156
|
+
const entry: Gesture = { id: event.pointerId, touch, active: !touch, start: current, position: pointed ?? position, last: { dx: where.dx, dy: where.dy }, x: event.clientX, y: event.clientY };
|
|
157
|
+
gesture.current = entry;
|
|
158
|
+
const key = (keyEvent: KeyboardEvent) => {
|
|
159
|
+
if (keyEvent.key !== "Escape" || !gesture.current?.active) return;
|
|
160
|
+
keyEvent.preventDefault();
|
|
161
|
+
keyEvent.stopPropagation();
|
|
162
|
+
end(true);
|
|
163
|
+
};
|
|
164
|
+
// Once a finger has held, it turns the dial instead of scrolling the page.
|
|
165
|
+
const touchMove = (touchEvent: TouchEvent) => {
|
|
166
|
+
if (gesture.current?.touch && gesture.current.active && touchEvent.cancelable) touchEvent.preventDefault();
|
|
167
|
+
};
|
|
168
|
+
listeners.current = { key, touchMove };
|
|
169
|
+
window.addEventListener("keydown", key, true);
|
|
170
|
+
window.addEventListener("touchmove", touchMove, { passive: false });
|
|
171
|
+
if (touch) {
|
|
172
|
+
entry.timer = window.setTimeout(() => {
|
|
173
|
+
if (gesture.current !== entry) return;
|
|
174
|
+
entry.active = true;
|
|
175
|
+
setTurning(true);
|
|
176
|
+
}, touchHold.delay);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
180
|
+
setTurning(true);
|
|
181
|
+
if (pointed !== null) setCurrent(valueFromRatio(pointed, bounds));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const move = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
185
|
+
const entry = gesture.current;
|
|
186
|
+
if (!entry || entry.id !== event.pointerId) return;
|
|
187
|
+
if (!entry.active) {
|
|
188
|
+
// A finger that moves before the hold ends is scrolling: let it go.
|
|
189
|
+
if (Math.hypot(event.clientX - entry.x, event.clientY - entry.y) > touchHold.slop) end(false);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const where = measure(event.clientX, event.clientY);
|
|
193
|
+
entry.position = dialTurn(entry.position, entry.last, where);
|
|
194
|
+
entry.last = { dx: where.dx, dy: where.dy };
|
|
195
|
+
setCurrent(valueFromRatio(entry.position, bounds));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const finish = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
199
|
+
const entry = gesture.current;
|
|
200
|
+
if (!entry || entry.id !== event.pointerId) return;
|
|
201
|
+
// A tap: a finger lifted before the hold, without moving, sets the value it pointed to on the ring.
|
|
202
|
+
if (entry.touch && !entry.active) {
|
|
203
|
+
const where = measure(event.clientX, event.clientY);
|
|
204
|
+
const pointed = dialPositionAt(where.dx, where.dy, where.knob);
|
|
205
|
+
if (pointed !== null) setCurrent(valueFromRatio(pointed, bounds));
|
|
206
|
+
}
|
|
207
|
+
end(false);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className={cx("ml-dial-field", className)} data-size={size} data-tone={tone} data-disabled={disabled ? "" : undefined}>
|
|
212
|
+
{label ? (
|
|
213
|
+
<span id={labelId} className="ml-dial-label">
|
|
214
|
+
{label}
|
|
215
|
+
</span>
|
|
216
|
+
) : null}
|
|
217
|
+
<div
|
|
218
|
+
ref={setRef}
|
|
219
|
+
id={dialId}
|
|
220
|
+
role="slider"
|
|
221
|
+
tabIndex={disabled ? -1 : 0}
|
|
222
|
+
aria-valuemin={low}
|
|
223
|
+
aria-valuemax={high}
|
|
224
|
+
aria-valuenow={current}
|
|
225
|
+
aria-valuetext={text}
|
|
226
|
+
aria-labelledby={labelledBy ?? labelId}
|
|
227
|
+
aria-label={labelledBy || label ? undefined : (ariaLabel ?? "Value")}
|
|
228
|
+
aria-describedby={[describedBy, captionId].filter(Boolean).join(" ") || undefined}
|
|
229
|
+
aria-disabled={disabled || undefined}
|
|
230
|
+
data-turning={turning ? "" : undefined}
|
|
231
|
+
className="ml-dial"
|
|
232
|
+
style={{ "--ml-dial-angle": `${dialAngle(position)}deg` } as React.CSSProperties}
|
|
233
|
+
onKeyDown={(event) => {
|
|
234
|
+
if (disabled) return;
|
|
235
|
+
const next = sliderValueForKey(event.key, current, bounds);
|
|
236
|
+
if (next === undefined) return;
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
setCurrent(next);
|
|
239
|
+
}}
|
|
240
|
+
onPointerDown={begin}
|
|
241
|
+
onPointerMove={move}
|
|
242
|
+
onPointerUp={finish}
|
|
243
|
+
onPointerCancel={() => end(false)}
|
|
244
|
+
onLostPointerCapture={(event) => {
|
|
245
|
+
if (gesture.current?.id === event.pointerId && !gesture.current.touch) end(false);
|
|
246
|
+
}}
|
|
247
|
+
// A long press on a phone would open the page's menu over the dial.
|
|
248
|
+
onContextMenu={(event) => {
|
|
249
|
+
if (gesture.current?.touch) event.preventDefault();
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<svg className="ml-dial-arc" viewBox="0 0 100 100" aria-hidden="true" focusable="false">
|
|
253
|
+
<path className="ml-dial-ticks" d={TICKS} />
|
|
254
|
+
<path className="ml-dial-track" d={dialArc(0, 1)} />
|
|
255
|
+
<path className="ml-dial-range" d={dialArc(0, position)} />
|
|
256
|
+
</svg>
|
|
257
|
+
<span className="ml-dial-knob" aria-hidden="true" />
|
|
258
|
+
<span className="ml-dial-readout" aria-hidden="true">
|
|
259
|
+
<span ref={readout} className="ml-dial-value">
|
|
260
|
+
{text}
|
|
261
|
+
</span>
|
|
262
|
+
</span>
|
|
263
|
+
</div>
|
|
264
|
+
{caption ? (
|
|
265
|
+
<span id={captionId} className="ml-dial-caption">
|
|
266
|
+
{caption}
|
|
267
|
+
</span>
|
|
268
|
+
) : null}
|
|
269
|
+
<FieldValue name={name} form={form} value={String(current)} disabled={disabled} />
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
Dial.displayName = "Dial";
|
|
275
|
+
|
|
276
|
+
export { Dial };
|
|
277
|
+
export type { DialProps, DialSize, DialTone };
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"packages/components/context-menu/context-menu.tsx": "sha256-OGaKzthZJGaV5T+4sRWYecA64wEEg2KImKnIzMMg1i4=",
|
|
30
30
|
"packages/components/copy-button/copy-button.tsx": "sha256-6fvfifoiDOtW+WxKWon4SGEn3THtERuCX2hqGSZDtVM=",
|
|
31
31
|
"packages/components/date-picker/date-picker.tsx": "sha256-QABmZzPKHc706/Rs+ln3ifDK3T2lXl34rehhuRXW+8o=",
|
|
32
|
+
"packages/components/dial/dial.tsx": "sha256-YhncOvnvBmT1GtWN8o7sgtMjmXvjaXVzTknK98osDno=",
|
|
32
33
|
"packages/components/dropdown-menu/dropdown-menu.tsx": "sha256-TSAsDWcx8mVwLdZVEaCrO0Fkeuiaui7Jw/BbZQmJH1Y=",
|
|
33
34
|
"packages/components/dropzone/dropzone.tsx": "sha256-tHCCpEWE6839UeLOszTo+R2vAL4Sp9UxuRdLkFLfnq4=",
|
|
34
35
|
"packages/components/empty-state/empty-state.tsx": "sha256-/phk3A6TDaiqo61fCcAc/4Bi85e1yBGAtKXFaGhrjyw=",
|