@dreamtree-org/twreact-ui 1.3.4 → 1.3.6

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 (36) hide show
  1. package/README.md +37 -20
  2. package/ai-skills/dreamtree-ui.md +5 -1
  3. package/dist/ai-schema.json +44 -2
  4. package/dist/components/core/Button.d.ts.map +1 -1
  5. package/dist/components/core/Radio.d.ts.map +1 -1
  6. package/dist/components/core/Select.d.ts.map +1 -1
  7. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  8. package/dist/components/feedback/Drawer.d.ts.map +1 -1
  9. package/dist/components/navigation/FootNav.d.ts.map +1 -1
  10. package/dist/components/utility/Badge.d.ts.map +1 -1
  11. package/dist/components/utility/Stepper.d.ts.map +1 -1
  12. package/dist/esm/components/blocks/StatusPill.js +1 -1
  13. package/dist/esm/components/core/Button.js +23 -6
  14. package/dist/esm/components/core/Radio.js +3 -2
  15. package/dist/esm/components/core/Select.js +12 -2
  16. package/dist/esm/components/feedback/Dialog.js +4 -2
  17. package/dist/esm/components/feedback/Drawer.js +39 -10
  18. package/dist/esm/components/navigation/FootNav.js +41 -14
  19. package/dist/esm/components/utility/Badge.js +1 -0
  20. package/dist/esm/components/utility/Stepper.js +5 -2
  21. package/dist/esm/hooks/useSwipeToClose.js +66 -0
  22. package/dist/esm/utils/touchTarget.js +6 -0
  23. package/dist/hooks/useSwipeToClose.d.ts +24 -0
  24. package/dist/hooks/useSwipeToClose.d.ts.map +1 -0
  25. package/dist/index.css +1 -1
  26. package/dist/index.esm.css +1 -1
  27. package/dist/index.esm.js +191 -38
  28. package/dist/index.js +191 -38
  29. package/dist/types/index.d.ts +28 -2
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/dist/utils/touchTarget.d.ts +2 -0
  32. package/dist/utils/touchTarget.d.ts.map +1 -0
  33. package/doc/README.md +1 -1
  34. package/mcp/README.md +1 -1
  35. package/mcp/catalog.snapshot.json +51 -21
  36. package/package.json +6 -6
package/README.md CHANGED
@@ -2,38 +2,42 @@
2
2
 
3
3
  A comprehensive React + Tailwind CSS components library for building modern web applications. Built with accessibility, customization, and developer experience in mind.
4
4
 
5
+ [![npm version](https://badge.fury.io/js/@dreamtree-org%2Ftwreact-ui.svg)](https://www.npmjs.com/package/@dreamtree-org/twreact-ui)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Website](https://img.shields.io/badge/by-Dreamtree--Org-0ea5e9)](https://www.dreamtreeglobal.com)
8
+
5
9
  ## About the Company
6
10
 
7
- Dreamtree UI is built and maintained under **[Dreamtree Global](http://dreamtreeglobal.com/)** — a team crafting thoughtful, accessible, developer-first software for the modern web. We believe great products start with a great foundation, and that's exactly what this design system aims to be: a polished, open, and dependable building block for teams everywhere.
11
+ Dreamtree UI is built and maintained under **[Dreamtree-Org](https://www.dreamtreeglobal.com)** — a team crafting thoughtful, developer-first software for the modern web. We believe great products start with a great foundation, and that's exactly what this design system aims to be: a polished, open, and dependable building block for teams everywhere.
8
12
 
9
- 🌐 **Website:** [dreamtreeglobal.com](http://dreamtreeglobal.com/)
13
+ 🌐 **Website:** [www.dreamtreeglobal.com](https://www.dreamtreeglobal.com)
10
14
 
11
15
  ## Author & Maintainer
12
16
 
13
- **Partha Preetham Krishna M L**
17
+ **Partha Preetham Krishna** — [Dreamtree-Org](https://www.dreamtreeglobal.com)
14
18
  📧 [preetham.krishna.dev@gmail.com](mailto:preetham.krishna.dev@gmail.com)
15
19
 
16
- Have an idea, a bug, or just want to say hi? Reach out anytime — feedback from developers like you is what keeps this library growing.
20
+ Have an idea, a bug, or just want to say hi? Reach out anytime — feedback from developers like you is what keeps this design system growing.
17
21
 
18
22
  ## 💚 Sponsor & Support
19
23
 
20
- Dreamtree UI is **free and open source**, built in the open and powered entirely by passion and late nights. Every component you use — every accessible button, dark-mode-ready table, and tree-shakeable export — represents hours of careful craft so that *your* product can ship faster and feel better.
24
+ Dreamtree UI is **free and open source**, built in the open and powered entirely by passion and late nights. Every feature you rely on — every accessible button, dark-mode-ready table, and tree-shakeable export — represents hours of careful craft so that _your_ product can ship faster and feel better.
21
25
 
22
- If this library has saved you time, sparked an idea, or made your app a little more beautiful, please consider supporting its continued development. **Your sponsorship keeps the updates coming, the bugs squashed, and the docs sharp — for the whole community.** Even the smallest contribution is a huge encouragement. 🙏
26
+ If this design system has saved you time, sparked an idea, or made your app a little more beautiful, please consider supporting its continued development. **Your sponsorship keeps the updates coming, the bugs squashed, and the docs sharp — for the whole community.** Even the smallest contribution is a huge encouragement. 🙏
23
27
 
24
28
  ### ☕ Make a one-tap donation via UPI
25
29
 
26
30
  **📷 Scan the QR with any UPI app** (Google Pay, PhonePe, Paytm, BHIM, …) to pay instantly:
27
31
 
28
32
  <p>
29
- <img src="https://unpkg.com/@dreamtree-org/twreact-ui@latest/doc/assets/upi-qr.png" alt="Scan to pay Dreamtree Global via UPI" width="220" height="220" />
33
+ <img src="https://unpkg.com/@dreamtree-org/twreact-ui@latest/doc/assets/upi-qr.png" alt="Scan to pay Dreamtree-Org via UPI" width="220" height="220" />
30
34
  </p>
31
35
 
32
- > **UPI ID:** `dhrugantha.llp@kotak` &nbsp;•&nbsp; **Payee:** Dreamtree Global
36
+ > **UPI ID:** `dhrugantha.llp@kotak` &nbsp;•&nbsp; **Payee:** Dreamtree-Org
33
37
  >
34
38
  > 📱 On mobile, you can also tap the button below — it opens your UPI app directly. On desktop, scan the QR above or copy the UPI ID into your payment app.
35
39
 
36
- [![Pay via UPI](https://img.shields.io/badge/Pay-via%20UPI-22c55e?style=for-the-badge&logo=googlepay&logoColor=white)](upi://pay?pa=dhrugantha.llp@kotak&pn=Dreamtree%20Global&cu=INR&tn=Support%20Dreamtree%20UI)
40
+ [![Pay via UPI](https://img.shields.io/badge/Pay-via%20UPI-22c55e?style=for-the-badge&logo=googlepay&logoColor=white)](upi://pay?pa=dhrugantha.llp@kotak&pn=Dreamtree-Org&cu=INR&tn=Support%20Dreamtree%20UI)
37
41
 
38
42
  ### 🤝 Become a Sponsor
39
43
 
@@ -214,12 +218,16 @@ Versatile button component with multiple variants and states.
214
218
  - `leftIcon`: ReactNode
215
219
  - `rightIcon`: ReactNode
216
220
  - `fullWidth`: boolean
221
+ - `as`: ElementType — render as a link or router `Link` with the Button's look (a bare `href` renders an `<a>`)
222
+ - `href`: string
217
223
  - `className`: string
218
224
 
219
225
  ```jsx
220
226
  <Button variant="primary" size="lg" loading>
221
227
  Loading...
222
228
  </Button>
229
+ <Button href="tel:+919800000000" variant="outline">Call</Button>
230
+ <Button as={Link} to="/move-in">Move in</Button>
223
231
  ```
224
232
 
225
233
  #### Input
@@ -279,6 +287,7 @@ Advanced select component with search, multi-select, and grouping support.
279
287
  - `closeOnSelect`: boolean
280
288
  - `maxTagCount`: number (default: 3)
281
289
  - `allowClear`: boolean (default: false; set it when the field may be emptied)
290
+ - `size`: 'sm' | 'md' | 'lg' (default: 'md') — `md` grows to the 44px touch target on coarse pointers; `lg` is 44px everywhere
282
291
 
283
292
  ```jsx
284
293
  <Select
@@ -510,10 +519,10 @@ Top navigation bar with user menu and notifications.
510
519
  Fixed bottom tab bar for phones (`md:hidden`). Uses the semantic `bg-surface` / `border-line` tokens, so it follows dark mode.
511
520
 
512
521
  **Props:**
513
- - `items`: Array<{ id, label, icon, onClick?, active?, badge? }>
522
+ - `items`: Array<{ id, label, icon, onClick?, active?, badge?, variant?, 'aria-*'?, 'data-*'? }>
514
523
  - `className`: string
515
524
 
516
- The `active` item gets a tinted primary background, `text-primary-700` and `aria-current="page"` (inactive tabs are neutral `text-muted`). Badges sit on their own item's icon. Items share the width and labels truncate, so five tabs fit a 375px screen.
525
+ `variant: 'action'` renders the item as a raised circular primary button (FAB), e.g. a centre "+" that opens a sheet. `aria-*` and `data-*` item keys reach the button, so an item can carry `aria-haspopup="dialog"` and `aria-expanded`. The `active` item gets a tinted primary background, `text-primary-700` and `aria-current="page"` (inactive tabs are neutral `text-muted`). Badges sit on their own item's icon. Items share the width and labels truncate, so five tabs fit a 375px screen.
517
526
 
518
527
  ```jsx
519
528
  <FootNav
@@ -665,7 +674,7 @@ Status indicators and labels with different variants.
665
674
 
666
675
  **Props:**
667
676
  - `children`: ReactNode
668
- - `variant`: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'outline' (default: 'default')
677
+ - `variant`: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'outline' (default: 'default')
669
678
  - `size`: 'sm' | 'md' | 'lg' (default: 'md')
670
679
  - `rounded`: boolean (default: true)
671
680
 
@@ -708,7 +717,7 @@ Interaction primitives that products used to compose by hand. All are `forwardRe
708
717
 
709
718
  | Component | Group | Use for |
710
719
  |---|---|---|
711
- | [`Drawer`](doc/Drawer.md) | Feedback | Side/edge sheet (`placement` right/left/top/bottom, `size` sm–full) — record peeks and editors. Same `isOpen`/`onClose` contract as `Dialog`; focus trap, Escape/backdrop close, focus restore, scroll lock; `Drawer.Header/Body/Footer` |
720
+ | [`Drawer`](doc/Drawer.md) | Feedback | Side/edge sheet (`placement` right/left/top/bottom, `size` sm–full) — record peeks and editors. `placement="bottom"` is a phone bottom sheet (grab `handle`, `swipeToClose`, rounded top, safe-area padding). Same `isOpen`/`onClose` contract as `Dialog`; focus trap, Escape/backdrop close, focus restore, scroll lock; `Drawer.Header/Body/Footer` |
712
721
  | [`Popover`](doc/Popover.md) | Core | Anchored, click-triggered panel with interactive content (unlike `Tooltip`). Controlled/uncontrolled `open`, `placement` with flip, Escape/outside-click close, `aria-expanded`/`aria-controls`/`aria-haspopup="dialog"` |
713
722
  | [`Textarea`](doc/Textarea.md) | Core | Multi-line input mirroring `Input`: `label`, `error`, `helperText`, `required`, `showCount`/`maxLength`, `autoResize`/`maxRows` |
714
723
  | [`Slider`](doc/Slider.md) | Core | Single value or `[low, high]` range; `min`/`max`/`step`, arrows/PageUp/PageDown/Home/End, `role="slider"` with full `aria-value*` |
@@ -872,7 +881,7 @@ module.exports = {
872
881
  @source "../node_modules/@dreamtree-org/twreact-ui/dist/index.esm.js";
873
882
  ```
874
883
 
875
- This adds semantic utilities next to the palettes: `bg-surface`, `bg-background`, `text-foreground`, `text-muted`, `border-line`, `bg-shell`, `text-micro`, `rounded-control` and `rounded-card`. Add a brand by creating `tokens/brands/<name>.json`; it overrides only the steps and roles you list.
884
+ The palettes are `primary`, `secondary`, `success`, `warning`, `error`, `info` and `gray` (steps 50–950), so `bg-info-50` / `text-info-600` work out of the box; `info` is for informational and scheduled states that are not the brand accent. This adds semantic utilities next to the palettes: `bg-surface`, `bg-background`, `text-foreground`, `text-muted`, `border-line`, `bg-shell`, `text-micro`, `rounded-control` and `rounded-card`. Add a brand by creating `tokens/brands/<name>.json`; it overrides only the steps and roles you list.
876
885
 
877
886
  **Design rules:** [`DESIGN.md`](DESIGN.md) is the rule set for product screens and AI agents: tokens only, blocks first, loading, empty and error states, 390 px, no native dialogs. It is generated from the tokens (`npm run build:tokens`).
878
887
 
@@ -1019,16 +1028,24 @@ function UserTable() {
1019
1028
 
1020
1029
  ## Contributing
1021
1030
 
1022
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
1031
+ 1. Fork the repository
1032
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
1033
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
1034
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
1035
+ 5. Open a Pull Request
1023
1036
 
1024
1037
  ## License
1025
1038
 
1026
- MIT License - see [LICENSE](LICENSE) for details.
1039
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1027
1040
 
1028
1041
  ## Support
1029
1042
 
1030
1043
  - 📖 [Storybook](https://storybook.app.drm3.org)
1031
- - 🐛 [Issue Tracker](https://github.com/dreamtree-ui/dreamtree-ui/issues)
1032
- - 💬 [Discussions](https://github.com/dreamtree-ui/dreamtree-ui/discussions)
1033
- - 🌐 [Dreamtree Global](http://dreamtreeglobal.com/)
1034
- - 📧 [preetham.krishna.dev@gmail.com](mailto:preetham.krishna.dev@gmail.com)
1044
+ - 🐛 [Issue Tracker](https://github.com/dreamtree-org/dreamtree-org.twreact-ui/issues)
1045
+ - 📦 [npm package](https://www.npmjs.com/package/@dreamtree-org/twreact-ui)
1046
+ - 🌐 [Dreamtree-Org](https://www.dreamtreeglobal.com)
1047
+ - 📧 [preetham.krishna.dev@gmail.com](mailto:preetham.krishna.dev@gmail.com)
1048
+
1049
+ ---
1050
+
1051
+ **Made with ❤️ by [Partha Preetham Krishna](https://www.linkedin.com/in/partha-preetham-krishna-68ba00197/)** under [Dreamtree-Org](https://www.dreamtreeglobal.com) · [www.dreamtreeglobal.com](https://www.dreamtreeglobal.com)
@@ -193,7 +193,11 @@ When suggesting a component or new variant value, use this exact vocabulary.
193
193
 
194
194
  **Interaction primitives** — reach for these before hand-rolling the pattern:
195
195
 
196
- - `Drawer` — side/edge sheet for record peeks and editors (`isOpen`, `onClose`, `title`, `placement`, `size`, `footer`); don't fake one with `Dialog size="lg"`.
196
+ - `Drawer` — side/edge sheet for record peeks and editors (`isOpen`, `onClose`, `title`, `placement`, `size`, `footer`); don't fake one with `Dialog size="lg"`. On phones use `placement="bottom"` (+ `swipeToClose`) as the bottom sheet: it already has a grab `handle`, rounded top and safe-area padding — don't add them with `className`.
197
+ - `FootNav` — phone tab bar. A centre "+" is an item with `variant: 'action'` (raised primary circle); give it `'aria-haspopup': 'dialog'` and `'aria-expanded'` when it opens a sheet.
198
+ - `Button` as a link — `<Button href="tel:…">` or `<Button as={Link} to="/x">`; never copy Button's class string onto an `<a>`.
199
+ - `Select size="lg"` — 44px trigger for phone forms (`md` already grows to 44px on touch screens).
200
+ - `Stepper` steps take `title` (or its alias `label`). `Radio` follows the primary token by default — don't pass `radioColor` to match the brand.
197
201
  - `Popover` — click-triggered panel with controls (`trigger={<Button>…</Button>}`, children or `({ close }) => …`); `Tooltip` is for read-only hover text only.
198
202
  - `Textarea` — same API as `Input` plus `autoResize`/`maxRows`; never a raw `<textarea>`.
199
203
  - `Slider` — `defaultValue={40}` for one thumb, `[low, high]` for a range; `onChangeEnd` to save after a drag.
@@ -304,6 +304,7 @@
304
304
  "success",
305
305
  "warning",
306
306
  "error",
307
+ "info",
307
308
  "outline"
308
309
  ],
309
310
  "default": "\"default\""
@@ -527,6 +528,27 @@
527
528
  "submit",
528
529
  "reset"
529
530
  ]
531
+ },
532
+ "as": {
533
+ "type": "ElementType",
534
+ "required": false,
535
+ "default": "\"button\""
536
+ },
537
+ "href": {
538
+ "type": "string",
539
+ "required": false
540
+ },
541
+ "target": {
542
+ "type": "string",
543
+ "required": false
544
+ },
545
+ "rel": {
546
+ "type": "string",
547
+ "required": false
548
+ },
549
+ "to": {
550
+ "type": "string | Record<string, unknown>",
551
+ "required": false
530
552
  }
531
553
  }
532
554
  },
@@ -1098,6 +1120,16 @@
1098
1120
  "initialFocusRef": {
1099
1121
  "type": "RefObject",
1100
1122
  "required": false
1123
+ },
1124
+ "handle": {
1125
+ "type": "boolean",
1126
+ "required": false,
1127
+ "default": "true for placement=\"bottom\", else false"
1128
+ },
1129
+ "swipeToClose": {
1130
+ "type": "boolean",
1131
+ "required": false,
1132
+ "default": "false"
1101
1133
  }
1102
1134
  }
1103
1135
  },
@@ -1906,7 +1938,7 @@
1906
1938
  "radioColor": {
1907
1939
  "type": "string",
1908
1940
  "required": false,
1909
- "default": "\"blue\""
1941
+ "default": "\"rgb(var(--dt-primary-600))\""
1910
1942
  },
1911
1943
  "ariaLabel": {
1912
1944
  "type": "string",
@@ -2264,6 +2296,16 @@
2264
2296
  "renderGroupLabel": {
2265
2297
  "type": "(label: string) => ReactNode",
2266
2298
  "required": false
2299
+ },
2300
+ "size": {
2301
+ "type": "enum",
2302
+ "required": false,
2303
+ "values": [
2304
+ "sm",
2305
+ "md",
2306
+ "lg"
2307
+ ],
2308
+ "default": "\"md\""
2267
2309
  }
2268
2310
  }
2269
2311
  },
@@ -2692,7 +2734,7 @@
2692
2734
  "forwardRef": true,
2693
2735
  "props": {
2694
2736
  "steps": {
2695
- "type": "StepperStep[]",
2737
+ "type": "Array",
2696
2738
  "required": true,
2697
2739
  "default": "[]"
2698
2740
  },
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/core/Button.jsx"],"names":[],"mappings":";AAIA,gFAmDG;kBAvD+B,OAAO"}
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/core/Button.jsx"],"names":[],"mappings":";AAIA,gFAkEG;kBAtE+B,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../src/components/core/Radio.jsx"],"names":[],"mappings":";AAYA,+EAqIG;kBAhJgD,OAAO"}
1
+ {"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../src/components/core/Radio.jsx"],"names":[],"mappings":";AAYA,+EAsIG;kBAjJgD,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/core/Select.jsx"],"names":[],"mappings":";AAqGA,gFAszBE;kBAn5BK,OAAO"}
1
+ {"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/core/Select.jsx"],"names":[],"mappings":";AA8GA,gFAwzBE;kBA95BK,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/Dialog.jsx"],"names":[],"mappings":";AAmCA,gFAuJG;kBA1LsC,OAAO"}
1
+ {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/Dialog.jsx"],"names":[],"mappings":";AAoCA,gFA0JG;kBA9LsC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/Drawer.jsx"],"names":[],"mappings":";AAiCA,gFAyFG;kBA1HkC,OAAO"}
1
+ {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/Drawer.jsx"],"names":[],"mappings":";AAqCA,gFAqHG;kBA1JkC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"FootNav.d.ts","sourceRoot":"","sources":["../../../src/components/navigation/FootNav.jsx"],"names":[],"mappings":";AAKA,iFA8CG;kBAnDe,OAAO"}
1
+ {"version":3,"file":"FootNav.d.ts","sourceRoot":"","sources":["../../../src/components/navigation/FootNav.jsx"],"names":[],"mappings":";AAmBA,iFA0DG;kBA7Ee,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../../src/components/utility/Badge.jsx"],"names":[],"mappings":";AAGA,+EA6CG;kBAhDe,OAAO"}
1
+ {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../../src/components/utility/Badge.jsx"],"names":[],"mappings":";AAGA,+EA8CG;kBAjDe,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"Stepper.d.ts","sourceRoot":"","sources":["../../../src/components/utility/Stepper.jsx"],"names":[],"mappings":";AAIA,iFAoIG;kBAxIe,OAAO"}
1
+ {"version":3,"file":"Stepper.d.ts","sourceRoot":"","sources":["../../../src/components/utility/Stepper.jsx"],"names":[],"mappings":";AAIA,iFAuIG;kBA3Ie,OAAO"}
@@ -10,7 +10,7 @@ var _excluded = ["status", "map", "tone", "icon", "children", "className"];
10
10
  // One map from domain state to tone + human label; unknown states are humanized, never shown raw.
11
11
  var TONE_TO_VARIANT = {
12
12
  neutral: 'secondary',
13
- info: 'primary',
13
+ info: 'info',
14
14
  success: 'success',
15
15
  warning: 'warning',
16
16
  danger: 'error',
@@ -5,7 +5,7 @@ import { forwardRef } from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
6
  import { Loader2 } from 'lucide-react';
7
7
 
8
- var _excluded = ["className", "variant", "size", "disabled", "loading", "children", "leftIcon", "rightIcon", "fullWidth"];
8
+ var _excluded = ["className", "variant", "size", "disabled", "loading", "children", "leftIcon", "rightIcon", "fullWidth", "as", "onClick"];
9
9
  var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
10
10
  var className = _ref.className,
11
11
  _ref$variant = _ref.variant,
@@ -18,12 +18,19 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
18
18
  leftIcon = _ref.leftIcon,
19
19
  rightIcon = _ref.rightIcon,
20
20
  fullWidth = _ref.fullWidth,
21
+ as = _ref.as,
22
+ onClick = _ref.onClick,
21
23
  props = _objectWithoutProperties(_ref, _excluded);
22
- var baseClasses = cn('inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none', {
24
+ // `as` renders any element or component (an <a>, a router Link) with the
25
+ // Button's look; a bare `href` implies an <a>.
26
+ var Component = as || (props.href !== undefined ? 'a' : 'button');
27
+ var isNativeButton = Component === 'button';
28
+ var isDisabled = disabled || loading;
29
+ var baseClasses = cn('inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none', {
23
30
  // Variants
24
31
  'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500': variant === 'primary',
25
32
  'bg-secondary-100 text-secondary-900 hover:bg-secondary-200 focus:ring-secondary-500': variant === 'secondary',
26
- 'bg-transparent border border-gray-300 hover:bg-gray-50 focus:ring-primary-500': variant === 'outline',
33
+ 'bg-transparent border border-gray-300 hover:bg-gray-50 focus:ring-primary-500 dark:border-gray-600 dark:text-gray-100 dark:hover:bg-gray-800 dark:focus:ring-offset-gray-900': variant === 'outline',
27
34
  'bg-transparent text-primary-600 hover:bg-primary-50 focus:ring-primary-500': variant === 'ghost',
28
35
  'bg-error-600 text-white hover:bg-error-700 focus:ring-error-500': variant === 'destructive',
29
36
  'bg-success-600 text-white hover:bg-success-700 focus:ring-success-500': variant === 'success',
@@ -38,10 +45,20 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
38
45
  // Full width
39
46
  'w-full': fullWidth
40
47
  }, className);
41
- return jsxs("button", _objectSpread2(_objectSpread2({
48
+ return jsxs(Component, _objectSpread2(_objectSpread2(_objectSpread2({
42
49
  ref: ref,
43
- className: baseClasses,
44
- disabled: disabled || loading
50
+ className: baseClasses
51
+ }, isNativeButton ? {
52
+ disabled: isDisabled
53
+ }
54
+ // Links can't be `disabled`: announce it and swallow the click instead.
55
+ : isDisabled && {
56
+ 'aria-disabled': true,
57
+ tabIndex: -1
58
+ }), {}, {
59
+ onClick: !isNativeButton && isDisabled ? function (e) {
60
+ return e.preventDefault();
61
+ } : onClick
45
62
  }, props), {}, {
46
63
  children: [loading && jsx(Loader2, {
47
64
  className: "mr-2 h-4 w-4 animate-spin"
@@ -51,7 +51,8 @@ var Radio = /*#__PURE__*/forwardRef(function Radio(_ref, ref) {
51
51
  props = _objectWithoutProperties(_ref, _excluded);
52
52
  var autoId = useId();
53
53
  var inputId = id || "".concat(autoId, "-radio");
54
- var accentColor = radioColor || "blue";
54
+ // Default to the brand's primary token so the radio follows theming.
55
+ var accentColor = radioColor || "rgb(var(--dt-primary-600))";
55
56
  var _useState = useState(!!defaultChecked),
56
57
  _useState2 = _slicedToArray(_useState, 2),
57
58
  internalChecked = _useState2[0],
@@ -109,7 +110,7 @@ var Radio = /*#__PURE__*/forwardRef(function Radio(_ref, ref) {
109
110
  "--radio-accent": accentColor
110
111
  },
111
112
  children: jsx("span", {
112
- className: cn("flex items-center justify-center rounded-full border transition-colors duration-150 ease-in-out border-slate-300", sizeClasses.outer, disabled ? "bg-slate-100" : "bg-white", isChecked ? "shadow-inner" : ""),
113
+ className: cn("flex items-center justify-center rounded-full border transition-colors duration-150 ease-in-out border-slate-300 dark:border-gray-600", sizeClasses.outer, disabled ? "bg-slate-100 dark:bg-gray-800" : "bg-white dark:bg-gray-900", isChecked ? "shadow-inner" : ""),
113
114
  children: jsx("span", {
114
115
  "aria-hidden": true,
115
116
  className: cn("rounded-full transform transition-all duration-200 ease-out", sizeClasses.dot),
@@ -7,7 +7,7 @@ import { cn } from '../../utils/cn.js';
7
7
  import { useLatestRef } from '../../hooks/useLatestRef.js';
8
8
  import { ChevronDown, X, Check, Search, Plus } from 'lucide-react';
9
9
 
10
- var _excluded = ["options", "value", "onChange", "placeholder", "label", "error", "helperText", "disabled", "required", "multiSelect", "searchable", "grouped", "onMenuItemRender", "className", "allowClear", "creatable", "onCreateOption", "onSearch", "loading", "selectAllOption", "closeOnSelect", "maxTagCount", "renderGroupLabel", "name"];
10
+ var _excluded = ["options", "value", "onChange", "placeholder", "label", "error", "helperText", "disabled", "required", "multiSelect", "searchable", "grouped", "onMenuItemRender", "className", "allowClear", "creatable", "onCreateOption", "onSearch", "loading", "selectAllOption", "closeOnSelect", "maxTagCount", "renderGroupLabel", "name", "size"];
11
11
  /**
12
12
  * Enhanced Select component (debounce removed)
13
13
  * - Supports single and multi select
@@ -106,6 +106,14 @@ var highlight = function highlight() {
106
106
  }), label.slice(idx + term.length)]
107
107
  });
108
108
  };
109
+ // Trigger sizes. min-h (not h) so multi-select tags can wrap. `md` keeps its
110
+ // desktop look but grows to the 44px touch target on coarse pointers; `lg` is
111
+ // 44px everywhere.
112
+ var SIZE_CLASSES = {
113
+ sm: "min-h-8 px-2.5 py-1 text-xs",
114
+ md: "min-h-[38px] px-3 py-2 text-sm [@media(pointer:coarse)]:min-h-11",
115
+ lg: "min-h-11 px-4 py-2.5 text-base"
116
+ };
109
117
  var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
110
118
  var _ref$options = _ref.options,
111
119
  options = _ref$options === void 0 ? [] : _ref$options,
@@ -142,6 +150,8 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
142
150
  maxTagCount = _ref$maxTagCount === void 0 ? 3 : _ref$maxTagCount,
143
151
  renderGroupLabel = _ref.renderGroupLabel,
144
152
  name = _ref.name,
153
+ _ref$size = _ref.size,
154
+ size = _ref$size === void 0 ? "md" : _ref$size,
145
155
  props = _objectWithoutProperties(_ref, _excluded);
146
156
  var _useState = useState(false),
147
157
  _useState2 = _slicedToArray(_useState, 2),
@@ -712,7 +722,7 @@ var Select = /*#__PURE__*/React.forwardRef(function (_ref, forwardedRef) {
712
722
  "aria-labelledby": label ? "".concat(labelId, " ").concat(valueId) : valueId,
713
723
  "aria-describedby": describedBy,
714
724
  "aria-invalid": error ? true : undefined,
715
- className: cn("flex w-full items-center justify-between rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50", {
725
+ className: cn("flex w-full items-center justify-between rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50", SIZE_CLASSES[size] || SIZE_CLASSES.md, {
716
726
  "border-error-500 focus:ring-error-500": error,
717
727
  "text-primary-500": !selectedOptions.length,
718
728
  "pr-14": showClearButton
@@ -6,6 +6,7 @@ import { createPortal } from 'react-dom';
6
6
  import { cn } from '../../utils/cn.js';
7
7
  import { X } from 'lucide-react';
8
8
  import { useLatestRef } from '../../hooks/useLatestRef.js';
9
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
9
10
 
10
11
  var _excluded = ["isOpen", "onClose", "title", "children", "size", "dismissible", "showCloseButton", "footer", "className", "onOpen"],
11
12
  _excluded2 = ["children", "className"],
@@ -141,11 +142,12 @@ var Dialog = /*#__PURE__*/React.forwardRef(function (_ref, _ref3) {
141
142
  children: title
142
143
  }), showCloseButton && jsx("button", {
143
144
  type: "button",
144
- className: "rounded-md p-1 text-primary-400 hover:text-primary-600 hover:bg-gray-100",
145
+ className: cn('inline-flex items-center justify-center rounded-md p-1 text-primary-400 hover:text-primary-600 hover:bg-gray-100', TOUCH_TARGET),
145
146
  onClick: onClose,
146
147
  "aria-label": "Close dialog",
147
148
  children: jsx(X, {
148
- className: "h-5 w-5"
149
+ className: "h-5 w-5",
150
+ "aria-hidden": "true"
149
151
  })
150
152
  })]
151
153
  }), jsx("div", {
@@ -8,8 +8,10 @@ import { cn } from '../../utils/cn.js';
8
8
  import { setRefs } from '../../utils/position.js';
9
9
  import { useFocusTrap } from '../../hooks/useFocusTrap.js';
10
10
  import { useScrollLock } from '../../hooks/useScrollLock.js';
11
+ import { useSwipeToClose } from '../../hooks/useSwipeToClose.js';
12
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
11
13
 
12
- var _excluded = ["isOpen", "onClose", "title", "description", "children", "placement", "size", "dismissible", "showCloseButton", "footer", "initialFocusRef", "className"],
14
+ var _excluded = ["isOpen", "onClose", "title", "description", "children", "placement", "size", "dismissible", "showCloseButton", "footer", "initialFocusRef", "handle", "swipeToClose", "className", "style"],
13
15
  _excluded2 = ["className"],
14
16
  _excluded3 = ["className"],
15
17
  _excluded4 = ["className"];
@@ -17,7 +19,8 @@ var PLACEMENT_CLASSES = {
17
19
  right: "inset-y-0 right-0 h-full w-full border-l",
18
20
  left: "inset-y-0 left-0 h-full w-full border-r",
19
21
  top: "inset-x-0 top-0 w-full border-b",
20
- bottom: "inset-x-0 bottom-0 w-full border-t"
22
+ // Bottom sheet: rounded top and room for the home indicator (safe area).
23
+ bottom: "inset-x-0 bottom-0 w-full overflow-hidden rounded-t-2xl border-t pb-[env(safe-area-inset-bottom)]"
21
24
  };
22
25
  var SIDE_SIZES = {
23
26
  sm: "max-w-xs",
@@ -40,7 +43,7 @@ var CloseButton = function CloseButton(_ref) {
40
43
  type: "button",
41
44
  onClick: onClose,
42
45
  "aria-label": "Close drawer",
43
- className: cn("rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2", "dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:ring-offset-gray-900", className),
46
+ className: cn("inline-flex items-center justify-center rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2", "dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:ring-offset-gray-900", TOUCH_TARGET, className),
44
47
  children: jsx(X, {
45
48
  className: "h-5 w-5",
46
49
  "aria-hidden": "true"
@@ -63,7 +66,11 @@ var Drawer = /*#__PURE__*/React.forwardRef(function (_ref2, _ref3) {
63
66
  showCloseButton = _ref2$showCloseButton === void 0 ? true : _ref2$showCloseButton,
64
67
  footer = _ref2.footer,
65
68
  initialFocusRef = _ref2.initialFocusRef,
69
+ handle = _ref2.handle,
70
+ _ref2$swipeToClose = _ref2.swipeToClose,
71
+ swipeToClose = _ref2$swipeToClose === void 0 ? false : _ref2$swipeToClose,
66
72
  className = _ref2.className,
73
+ style = _ref2.style,
67
74
  props = _objectWithoutProperties(_ref2, _excluded);
68
75
  var panelRef = useRef(null);
69
76
  var titleId = "drawer-title-".concat(useId());
@@ -73,6 +80,17 @@ var Drawer = /*#__PURE__*/React.forwardRef(function (_ref2, _ref3) {
73
80
  // Without title/description/footer the children are the panel content, so
74
81
  // Drawer.Header / Drawer.Body / Drawer.Footer compose without double padding.
75
82
  var composed = !title && !description && !footer;
83
+ var showHandle = handle !== null && handle !== void 0 ? handle : side === "bottom";
84
+ var swipe = useSwipeToClose({
85
+ enabled: swipeToClose && side === "bottom" && dismissible,
86
+ onClose: onClose
87
+ });
88
+ // The grab zone (handle + header) starts a swipe; the scrolling body never does.
89
+ var grabProps = swipeToClose && side === "bottom" && dismissible ? _objectSpread2(_objectSpread2({}, swipe.dragProps), {}, {
90
+ className: "touch-none cursor-grab"
91
+ }) : {
92
+ className: ""
93
+ };
76
94
  useScrollLock(isOpen);
77
95
  useFocusTrap(panelRef, {
78
96
  active: isOpen,
@@ -91,7 +109,7 @@ var Drawer = /*#__PURE__*/React.forwardRef(function (_ref2, _ref3) {
91
109
  onClick: function onClick() {
92
110
  return dismissible && (onClose === null || onClose === void 0 ? void 0 : onClose());
93
111
  }
94
- }), jsx("div", _objectSpread2(_objectSpread2({
112
+ }), jsxs("div", _objectSpread2(_objectSpread2({
95
113
  ref: function ref(node) {
96
114
  return setRefs(node, panelRef, _ref3);
97
115
  },
@@ -101,16 +119,27 @@ var Drawer = /*#__PURE__*/React.forwardRef(function (_ref2, _ref3) {
101
119
  "aria-describedby": description ? descriptionId : undefined,
102
120
  tabIndex: -1,
103
121
  "data-placement": side,
104
- className: cn("absolute flex flex-col border-gray-200 bg-white text-gray-900 shadow-xl focus:outline-none", "dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100", "animate-fade-in motion-reduce:animate-none", PLACEMENT_CLASSES[side], (isEdge ? EDGE_SIZES : SIDE_SIZES)[size] || (isEdge ? EDGE_SIZES.md : SIDE_SIZES.md), className)
122
+ className: cn("absolute flex flex-col border-gray-200 bg-white text-gray-900 shadow-xl focus:outline-none", "dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100", "animate-fade-in motion-reduce:animate-none", PLACEMENT_CLASSES[side], (isEdge ? EDGE_SIZES : SIDE_SIZES)[size] || (isEdge ? EDGE_SIZES.md : SIDE_SIZES.md), !swipe.dragging && "transition-transform duration-200 ease-out motion-reduce:transition-none", className),
123
+ style: swipe.offset ? _objectSpread2(_objectSpread2({}, style), {}, {
124
+ transform: "translateY(".concat(swipe.offset, "px)")
125
+ }) : style
105
126
  }, props), {}, {
106
- children: composed ? jsxs(Fragment, {
127
+ children: [showHandle && jsx("div", _objectSpread2(_objectSpread2({
128
+ "aria-hidden": "true",
129
+ "data-testid": "drawer-handle"
130
+ }, grabProps), {}, {
131
+ className: cn("flex shrink-0 justify-center pb-1 pt-3", grabProps.className),
132
+ children: jsx("span", {
133
+ className: "h-1.5 w-10 rounded-full bg-gray-300 dark:bg-gray-600"
134
+ })
135
+ })), composed ? jsxs(Fragment, {
107
136
  children: [showCloseButton && jsx(CloseButton, {
108
137
  onClose: onClose,
109
138
  className: "absolute right-3 top-3"
110
139
  }), children]
111
140
  }) : jsxs(Fragment, {
112
- children: [jsxs("div", {
113
- className: "flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-4 dark:border-gray-700",
141
+ children: [jsxs("div", _objectSpread2(_objectSpread2({}, grabProps), {}, {
142
+ className: cn("flex items-start justify-between gap-4 border-b border-gray-200 px-6 py-4 dark:border-gray-700", showHandle && "pt-2", grabProps.className),
114
143
  children: [jsxs("div", {
115
144
  className: "min-w-0",
116
145
  children: [title && jsx("h2", {
@@ -125,14 +154,14 @@ var Drawer = /*#__PURE__*/React.forwardRef(function (_ref2, _ref3) {
125
154
  }), showCloseButton && jsx(CloseButton, {
126
155
  onClose: onClose
127
156
  })]
128
- }), jsx("div", {
157
+ })), jsx("div", {
129
158
  className: "min-h-0 flex-1 overflow-y-auto px-6 py-4",
130
159
  children: children
131
160
  }), footer && jsx("div", {
132
161
  className: "flex items-center justify-end gap-3 border-t border-gray-200 px-6 py-4 dark:border-gray-700",
133
162
  children: footer
134
163
  })]
135
- })
164
+ })]
136
165
  }))]
137
166
  }), document.body);
138
167
  });