@kennycorrea/ui 0.11.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +251 -1
  3. package/dist/atoms/ActionMenu/ActionMenu.d.ts +37 -0
  4. package/dist/atoms/ActionMenu/ActionMenu.js +28 -0
  5. package/dist/atoms/ActionMenu/index.d.ts +1 -0
  6. package/dist/atoms/ActionMenu/index.js +1 -0
  7. package/dist/atoms/ActionSheet/ActionSheet.d.ts +10 -4
  8. package/dist/atoms/ActionSheet/ActionSheet.js +6 -15
  9. package/dist/atoms/ActionSheet/confirm.d.ts +1 -1
  10. package/dist/atoms/ActionSheet/confirm.js +1 -1
  11. package/dist/atoms/Avatar/Avatar.d.ts +1 -1
  12. package/dist/atoms/Badge/Badge.js +4 -4
  13. package/dist/atoms/Button/Button.js +2 -3
  14. package/dist/atoms/Button/Button.types.d.ts +5 -8
  15. package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
  16. package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
  17. package/dist/atoms/Icon/Icon.d.ts +7 -9
  18. package/dist/atoms/Icon/Icon.js +23 -8
  19. package/dist/atoms/Pill/Pill.d.ts +17 -0
  20. package/dist/atoms/Pill/Pill.js +12 -0
  21. package/dist/atoms/Pill/index.d.ts +1 -0
  22. package/dist/atoms/Pill/index.js +1 -0
  23. package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +9 -2
  24. package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +6 -15
  25. package/dist/atoms/StatusIndicator/StatusIndicator.d.ts +14 -0
  26. package/dist/atoms/StatusIndicator/StatusIndicator.js +8 -0
  27. package/dist/atoms/StatusIndicator/index.d.ts +1 -0
  28. package/dist/atoms/StatusIndicator/index.js +1 -0
  29. package/dist/atoms/TextLink/TextLink.d.ts +36 -0
  30. package/dist/atoms/TextLink/TextLink.js +17 -0
  31. package/dist/atoms/TextLink/index.d.ts +1 -0
  32. package/dist/atoms/TextLink/index.js +1 -0
  33. package/dist/atoms/Toast/Toast.d.ts +1 -1
  34. package/dist/atoms/Toast/Toast.js +1 -1
  35. package/dist/atoms/index.d.ts +4 -0
  36. package/dist/atoms/index.js +4 -0
  37. package/dist/hooks/breakpoints.d.ts +20 -0
  38. package/dist/hooks/breakpoints.js +13 -0
  39. package/dist/hooks/index.d.ts +4 -0
  40. package/dist/hooks/index.js +4 -0
  41. package/dist/hooks/useControllableState.d.ts +17 -0
  42. package/dist/hooks/useControllableState.js +22 -0
  43. package/dist/hooks/useElementWidth.d.ts +7 -0
  44. package/dist/hooks/useElementWidth.js +22 -0
  45. package/dist/hooks/useMediaQuery.d.ts +18 -0
  46. package/dist/hooks/useMediaQuery.js +51 -0
  47. package/dist/index.d.ts +4 -0
  48. package/dist/index.js +4 -0
  49. package/dist/molecules/AccentPicker/AccentPicker.d.ts +30 -0
  50. package/dist/molecules/AccentPicker/AccentPicker.js +27 -0
  51. package/dist/molecules/AccentPicker/index.d.ts +1 -0
  52. package/dist/molecules/AccentPicker/index.js +1 -0
  53. package/dist/molecules/Breadcrumbs/Breadcrumbs.d.ts +27 -0
  54. package/dist/molecules/Breadcrumbs/Breadcrumbs.js +40 -0
  55. package/dist/molecules/Breadcrumbs/index.d.ts +1 -0
  56. package/dist/molecules/Breadcrumbs/index.js +1 -0
  57. package/dist/molecules/Calendar/Calendar.d.ts +42 -0
  58. package/dist/molecules/Calendar/Calendar.js +111 -0
  59. package/dist/molecules/Calendar/index.d.ts +1 -0
  60. package/dist/molecules/Calendar/index.js +1 -0
  61. package/dist/molecules/DateInput/DateInput.d.ts +35 -0
  62. package/dist/molecules/DateInput/DateInput.js +41 -0
  63. package/dist/molecules/DateInput/index.d.ts +1 -0
  64. package/dist/molecules/DateInput/index.js +1 -0
  65. package/dist/molecules/EmptyState/EmptyState.d.ts +19 -0
  66. package/dist/molecules/EmptyState/EmptyState.js +7 -0
  67. package/dist/molecules/EmptyState/index.d.ts +1 -0
  68. package/dist/molecules/EmptyState/index.js +1 -0
  69. package/dist/molecules/ErrorState/ErrorState.d.ts +18 -0
  70. package/dist/molecules/ErrorState/ErrorState.js +12 -0
  71. package/dist/molecules/ErrorState/index.d.ts +1 -0
  72. package/dist/molecules/ErrorState/index.js +1 -0
  73. package/dist/molecules/ErrorSummary/ErrorSummary.d.ts +25 -0
  74. package/dist/molecules/ErrorSummary/ErrorSummary.js +32 -0
  75. package/dist/molecules/ErrorSummary/index.d.ts +1 -0
  76. package/dist/molecules/ErrorSummary/index.js +1 -0
  77. package/dist/molecules/FilterChips/FilterChips.d.ts +10 -0
  78. package/dist/molecules/FilterChips/FilterChips.js +13 -0
  79. package/dist/molecules/FilterChips/index.d.ts +1 -0
  80. package/dist/molecules/FilterChips/index.js +1 -0
  81. package/dist/molecules/FilterPopover/FilterPopover.d.ts +27 -0
  82. package/dist/molecules/FilterPopover/FilterPopover.js +32 -0
  83. package/dist/molecules/FilterPopover/index.d.ts +1 -0
  84. package/dist/molecules/FilterPopover/index.js +1 -0
  85. package/dist/molecules/FormActions/FormActions.d.ts +16 -0
  86. package/dist/molecules/FormActions/FormActions.js +5 -0
  87. package/dist/molecules/FormActions/index.d.ts +1 -0
  88. package/dist/molecules/FormActions/index.js +1 -0
  89. package/dist/molecules/FormField/FormField.d.ts +52 -0
  90. package/dist/molecules/FormField/FormField.js +63 -0
  91. package/dist/molecules/FormField/index.d.ts +1 -0
  92. package/dist/molecules/FormField/index.js +1 -0
  93. package/dist/molecules/FormSection/FormSection.d.ts +14 -0
  94. package/dist/molecules/FormSection/FormSection.js +10 -0
  95. package/dist/molecules/FormSection/index.d.ts +1 -0
  96. package/dist/molecules/FormSection/index.js +1 -0
  97. package/dist/molecules/KeyValueList/KeyValueList.d.ts +22 -0
  98. package/dist/molecules/KeyValueList/KeyValueList.js +9 -0
  99. package/dist/molecules/KeyValueList/index.d.ts +1 -0
  100. package/dist/molecules/KeyValueList/index.js +1 -0
  101. package/dist/molecules/LoadingState/LoadingState.d.ts +20 -0
  102. package/dist/molecules/LoadingState/LoadingState.js +22 -0
  103. package/dist/molecules/LoadingState/index.d.ts +1 -0
  104. package/dist/molecules/LoadingState/index.js +1 -0
  105. package/dist/molecules/NavItem/NavItem.d.ts +34 -0
  106. package/dist/molecules/NavItem/NavItem.js +72 -0
  107. package/dist/molecules/NavItem/context.d.ts +19 -0
  108. package/dist/molecules/NavItem/context.js +5 -0
  109. package/dist/molecules/NavItem/index.d.ts +1 -0
  110. package/dist/molecules/NavItem/index.js +1 -0
  111. package/dist/molecules/Pagination/Pagination.d.ts +32 -0
  112. package/dist/molecules/Pagination/Pagination.js +57 -0
  113. package/dist/molecules/Pagination/index.d.ts +1 -0
  114. package/dist/molecules/Pagination/index.js +1 -0
  115. package/dist/molecules/PasswordInput/PasswordInput.d.ts +14 -0
  116. package/dist/molecules/PasswordInput/PasswordInput.js +16 -0
  117. package/dist/molecules/PasswordInput/index.d.ts +1 -0
  118. package/dist/molecules/PasswordInput/index.js +1 -0
  119. package/dist/molecules/SearchField/SearchField.d.ts +24 -0
  120. package/dist/molecules/SearchField/SearchField.js +58 -0
  121. package/dist/molecules/SearchField/index.d.ts +1 -0
  122. package/dist/molecules/SearchField/index.js +1 -0
  123. package/dist/molecules/StatGroup/StatGroup.d.ts +21 -0
  124. package/dist/molecules/StatGroup/StatGroup.js +10 -0
  125. package/dist/molecules/StatGroup/index.d.ts +1 -0
  126. package/dist/molecules/StatGroup/index.js +1 -0
  127. package/dist/molecules/ThemeToggle/ThemeToggle.d.ts +23 -0
  128. package/dist/molecules/ThemeToggle/ThemeToggle.js +26 -0
  129. package/dist/molecules/ThemeToggle/index.d.ts +1 -0
  130. package/dist/molecules/ThemeToggle/index.js +1 -0
  131. package/dist/molecules/UserMenu/UserMenu.d.ts +20 -0
  132. package/dist/molecules/UserMenu/UserMenu.js +14 -0
  133. package/dist/molecules/UserMenu/index.d.ts +1 -0
  134. package/dist/molecules/UserMenu/index.js +1 -0
  135. package/dist/molecules/index.d.ts +21 -0
  136. package/dist/molecules/index.js +21 -0
  137. package/dist/organisms/AppShell/AppShell.d.ts +29 -0
  138. package/dist/organisms/AppShell/AppShell.js +34 -0
  139. package/dist/organisms/AppShell/context.d.ts +14 -0
  140. package/dist/organisms/AppShell/context.js +6 -0
  141. package/dist/organisms/AppShell/index.d.ts +1 -0
  142. package/dist/organisms/AppShell/index.js +1 -0
  143. package/dist/organisms/AuthForms/AuthForms.d.ts +83 -0
  144. package/dist/organisms/AuthForms/AuthForms.js +57 -0
  145. package/dist/organisms/AuthForms/index.d.ts +2 -0
  146. package/dist/organisms/AuthForms/index.js +1 -0
  147. package/dist/organisms/AuthForms/shared.d.ts +36 -0
  148. package/dist/organisms/AuthForms/shared.js +36 -0
  149. package/dist/organisms/BulkActionBar/BulkActionBar.d.ts +25 -0
  150. package/dist/organisms/BulkActionBar/BulkActionBar.js +18 -0
  151. package/dist/organisms/BulkActionBar/index.d.ts +1 -0
  152. package/dist/organisms/BulkActionBar/index.js +1 -0
  153. package/dist/organisms/Chart/CategoryCharts.d.ts +34 -0
  154. package/dist/organisms/Chart/CategoryCharts.js +93 -0
  155. package/dist/organisms/Chart/ChartFrame.d.ts +26 -0
  156. package/dist/organisms/Chart/ChartFrame.js +18 -0
  157. package/dist/organisms/Chart/DonutChart.d.ts +25 -0
  158. package/dist/organisms/Chart/DonutChart.js +53 -0
  159. package/dist/organisms/Chart/Sparkline.d.ts +15 -0
  160. package/dist/organisms/Chart/Sparkline.js +18 -0
  161. package/dist/organisms/Chart/index.d.ts +5 -0
  162. package/dist/organisms/Chart/index.js +5 -0
  163. package/dist/organisms/Chart/shared.d.ts +51 -0
  164. package/dist/organisms/Chart/shared.js +113 -0
  165. package/dist/organisms/DataTable/DataTable.d.ts +63 -0
  166. package/dist/organisms/DataTable/DataTable.js +120 -0
  167. package/dist/organisms/DataTable/index.d.ts +3 -0
  168. package/dist/organisms/DataTable/index.js +3 -0
  169. package/dist/organisms/DataTable/types.d.ts +39 -0
  170. package/dist/organisms/DataTable/types.js +1 -0
  171. package/dist/organisms/DataTable/useClientTable.d.ts +27 -0
  172. package/dist/organisms/DataTable/useClientTable.js +51 -0
  173. package/dist/organisms/DateRangePicker/DateRangePicker.d.ts +43 -0
  174. package/dist/organisms/DateRangePicker/DateRangePicker.js +54 -0
  175. package/dist/organisms/DateRangePicker/index.d.ts +1 -0
  176. package/dist/organisms/DateRangePicker/index.js +1 -0
  177. package/dist/organisms/FilterBar/FilterBar.d.ts +28 -0
  178. package/dist/organisms/FilterBar/FilterBar.js +16 -0
  179. package/dist/organisms/FilterBar/index.d.ts +1 -0
  180. package/dist/organisms/FilterBar/index.js +1 -0
  181. package/dist/organisms/PageHeader/PageHeader.d.ts +31 -0
  182. package/dist/organisms/PageHeader/PageHeader.js +19 -0
  183. package/dist/organisms/PageHeader/index.d.ts +1 -0
  184. package/dist/organisms/PageHeader/index.js +1 -0
  185. package/dist/organisms/PageTabs/PageTabs.d.ts +27 -0
  186. package/dist/organisms/PageTabs/PageTabs.js +31 -0
  187. package/dist/organisms/PageTabs/index.d.ts +1 -0
  188. package/dist/organisms/PageTabs/index.js +1 -0
  189. package/dist/organisms/Sidebar/Sidebar.d.ts +48 -0
  190. package/dist/organisms/Sidebar/Sidebar.js +73 -0
  191. package/dist/organisms/Sidebar/index.d.ts +1 -0
  192. package/dist/organisms/Sidebar/index.js +1 -0
  193. package/dist/organisms/Topbar/Topbar.d.ts +24 -0
  194. package/dist/organisms/Topbar/Topbar.js +18 -0
  195. package/dist/organisms/Topbar/index.d.ts +1 -0
  196. package/dist/organisms/Topbar/index.js +1 -0
  197. package/dist/organisms/index.d.ts +11 -0
  198. package/dist/organisms/index.js +11 -0
  199. package/dist/strings.d.ts +108 -15
  200. package/dist/strings.js +73 -7
  201. package/dist/templates/AdminLayout/AdminLayout.d.ts +14 -0
  202. package/dist/templates/AdminLayout/AdminLayout.js +9 -0
  203. package/dist/templates/AdminLayout/index.d.ts +1 -0
  204. package/dist/templates/AdminLayout/index.js +1 -0
  205. package/dist/templates/AuthLayout/AuthLayout.d.ts +28 -0
  206. package/dist/templates/AuthLayout/AuthLayout.js +11 -0
  207. package/dist/templates/AuthLayout/index.d.ts +1 -0
  208. package/dist/templates/AuthLayout/index.js +1 -0
  209. package/dist/templates/DashboardPage/DashboardPage.d.ts +25 -0
  210. package/dist/templates/DashboardPage/DashboardPage.js +12 -0
  211. package/dist/templates/DashboardPage/index.d.ts +1 -0
  212. package/dist/templates/DashboardPage/index.js +1 -0
  213. package/dist/templates/DetailPage/DetailPage.d.ts +20 -0
  214. package/dist/templates/DetailPage/DetailPage.js +11 -0
  215. package/dist/templates/DetailPage/index.d.ts +1 -0
  216. package/dist/templates/DetailPage/index.js +1 -0
  217. package/dist/templates/FormPage/FormPage.d.ts +26 -0
  218. package/dist/templates/FormPage/FormPage.js +11 -0
  219. package/dist/templates/FormPage/index.d.ts +1 -0
  220. package/dist/templates/FormPage/index.js +1 -0
  221. package/dist/templates/ListPage/ListPage.d.ts +17 -0
  222. package/dist/templates/ListPage/ListPage.js +8 -0
  223. package/dist/templates/ListPage/index.d.ts +1 -0
  224. package/dist/templates/ListPage/index.js +1 -0
  225. package/dist/templates/SettingsPage/SettingsPage.d.ts +32 -0
  226. package/dist/templates/SettingsPage/SettingsPage.js +13 -0
  227. package/dist/templates/SettingsPage/index.d.ts +1 -0
  228. package/dist/templates/SettingsPage/index.js +1 -0
  229. package/dist/templates/StatusPage/StatusPage.d.ts +18 -0
  230. package/dist/templates/StatusPage/StatusPage.js +8 -0
  231. package/dist/templates/StatusPage/index.d.ts +1 -0
  232. package/dist/templates/StatusPage/index.js +1 -0
  233. package/dist/templates/index.d.ts +8 -0
  234. package/dist/templates/index.js +8 -0
  235. package/dist/utils/dates.d.ts +67 -0
  236. package/dist/utils/dates.js +154 -0
  237. package/dist/utils/index.d.ts +1 -0
  238. package/dist/utils/index.js +1 -0
  239. package/dist/utils/theme.d.ts +4 -0
  240. package/dist/utils/theme.js +7 -3
  241. package/package.json +4 -1
  242. package/tokens.css +500 -21
@@ -25,7 +25,7 @@ function subscribe(l) {
25
25
  * Asks and resolves `true` on accept, `false` on Cancel, Escape or any other dismissal.
26
26
  * Never rejects.
27
27
  *
28
- * if (await confirm({ title: 'Delete booking?', confirmLabel: 'Delete', tone: 'danger' })) …
28
+ * if (await confirm({ title: 'Delete item?', confirmLabel: 'Delete', tone: 'danger' })) …
29
29
  */
30
30
  export function confirm(options) {
31
31
  return new Promise((resolve) => emit([...queue, { ...options, resolve }]));
@@ -1,5 +1,5 @@
1
1
  export interface AvatarProps {
2
- /** Absent for most providers — the fallback is the normal case, not the error case. */
2
+ /** Often absent — the fallback is the normal case, not the error case. */
3
3
  src?: string;
4
4
  name?: string;
5
5
  className?: string;
@@ -5,12 +5,12 @@ import { warnMissingScales } from '../../utils/scales.js';
5
5
  * The five meanings this library names, in Themes' colour vocabulary.
6
6
  *
7
7
  * **`brand` names no colour on purpose.** Themes falls back to whatever accent the theme
8
- * carries, so a badge that means "this is ours" follows the business's chosen colour while
8
+ * carries, so a badge that means "this is ours" follows the app's chosen colour while
9
9
  * the four that mean something specific do not. It said `jade` until accents became
10
- * choosable, which left the service badges green on a purple dashboard.
10
+ * choosable, which left brand badges green in a purple-accented app.
11
11
  *
12
- * `success` is jade and stays jade. Green means fine in a way purple does not, and a shop
13
- * that liked orange should not end up with an orange "paid".
12
+ * `success` is jade and stays jade. Green means fine in a way purple does not, and an app
13
+ * whose accent is orange should not end up with an orange "paid".
14
14
  */
15
15
  const COLORS = {
16
16
  neutral: 'gray',
@@ -5,10 +5,9 @@ import { cn } from '../../utils/cn.js';
5
5
  /**
6
6
  * The house intents, expressed in Radix Themes' variant-and-colour vocabulary.
7
7
  *
8
- * The mapping exists so three apps keep saying `variant="danger"` rather than
8
+ * The mapping exists so apps keep saying `variant="danger"` rather than
9
9
  * `variant="solid" color="red"` at sixty call sites — and so that when the house style changes
10
- * its mind about what "secondary" looks like, it changes here and everywhere at once. That
11
- * second half is what the dashboard and the site did not have, and why they drifted.
10
+ * its mind about what "secondary" looks like, it changes here and everywhere at once.
12
11
  */
13
12
  const VARIANTS = {
14
13
  primary: { variant: 'solid' },
@@ -3,13 +3,10 @@ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  /**
4
4
  * The house button intents.
5
5
  *
6
- * **`outline` is new, and it is how the disagreement was resolved.** The dashboard's
7
- * `secondary` mapped to Themes' `soft` and the marketing site's mapped to `surface` — the same
8
- * word meaning two things in two products, which is what `docs/ecosystem.md` named as the cost
9
- * of three repositories with nothing shared. Making one of them win would have been a decision
10
- * about the word rather than about the need: the site wants the bordered, translucent button
11
- * because it sits over a dark hero, and the dashboard wants the tinted one because it sits on a
12
- * panel. So `secondary` means exactly one thing everywhere and the other look has its own name.
6
+ * **`secondary` and `outline` are two looks with two names.** `secondary` is Themes' `soft`, a
7
+ * tinted button for use on a panel. `outline` is Themes' `surface`, a bordered, translucent
8
+ * button for use over a dark or busy background. One word for both would mean different things
9
+ * in different places, so each look has its own name.
13
10
  */
14
11
  export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
15
12
  /** `touch` is `md` type with a 44px minimum height, for primary actions on touch screens. */
@@ -37,7 +34,7 @@ export interface ButtonProps extends Omit<ComponentPropsWithoutRef<typeof Themed
37
34
  /**
38
35
  * Renders an anchor, keeping Radix's focus ring and sizing.
39
36
  *
40
- * From the marketing site, where almost every button is a link. It is `asChild` underneath,
37
+ * For pages where most buttons are really links. It is `asChild` underneath,
41
38
  * so the anchor stays an anchor — middle-click, copy link, the lot — and it is here rather
42
39
  * than left to each app because "a button that is really a link" is a house concern.
43
40
  */
@@ -17,7 +17,7 @@ export interface ContextMenuProps extends Omit<HTMLAttributes<HTMLElement>, 'onS
17
17
  * The menu a right-click opens, in the house vocabulary.
18
18
  *
19
19
  * Items are data rather than children, like `Select`'s options, because every caller
20
- * builds them from something — a booking's status decides which of them make sense.
20
+ * builds them from something — a record's status decides which of them make sense.
21
21
  *
22
22
  * **A shortcut, never the only way.** Right-click is invisible, mouse-only and absent on
23
23
  * touch, so everything here also has a path through the form the block opens on click.
@@ -5,7 +5,7 @@ import { Fragment, forwardRef } from 'react';
5
5
  * The menu a right-click opens, in the house vocabulary.
6
6
  *
7
7
  * Items are data rather than children, like `Select`'s options, because every caller
8
- * builds them from something — a booking's status decides which of them make sense.
8
+ * builds them from something — a record's status decides which of them make sense.
9
9
  *
10
10
  * **A shortcut, never the only way.** Right-click is invisible, mouse-only and absent on
11
11
  * touch, so everything here also has a path through the form the block opens on click.
@@ -1,12 +1,10 @@
1
- /**
2
- * The handful of icons this app actually uses, drawn inline.
3
- *
4
- * No icon library: a dependency for twelve glyphs costs more in bundle and upgrade surface
5
- * than it saves, and these are all one or two paths. They inherit `currentColor` and size
6
- * with the text around them, which is what makes them work in a collapsed sidebar and a
7
- * button alike.
8
- */
9
1
  declare const PATHS: {
2
+ readonly home: "M3 12l9-8 9 8M5 10v10h5v-6h4v6h5V10";
3
+ readonly list: "M4 7h16M4 12h16M4 17h10";
4
+ readonly calendar: "M7 3v3M17 3v3M4 8h16M5 6h14a1 1 0 011 1v12a1 1 0 01-1 1H5a1 1 0 01-1-1V7a1 1 0 011-1z";
5
+ readonly users: "M16 20v-2a4 4 0 00-4-4H7a4 4 0 00-4 4v2M9.5 9.5a3 3 0 100-6 3 3 0 000 6zM21 20v-2a4 4 0 00-3-3.87";
6
+ readonly card: "M3 7h18v12H3zM3 11h18M7 15h3";
7
+ readonly document: "M14 3H7a1 1 0 00-1 1v16a1 1 0 001 1h10a1 1 0 001-1V7zM14 3v4h4M9 13h6M9 17h4";
10
8
  readonly overview: "M3 12l9-8 9 8M5 10v10h5v-6h4v6h5V10";
11
9
  readonly services: "M4 7h16M4 12h16M4 17h10";
12
10
  readonly bookings: "M7 3v3M17 3v3M4 8h16M5 6h14a1 1 0 011 1v12a1 1 0 01-1 1H5a1 1 0 01-1-1V7a1 1 0 011-1z";
@@ -44,7 +42,7 @@ export interface IconProps {
44
42
  * A label makes it an image with a name; leaving it out makes it decoration.
45
43
  *
46
44
  * Decoration is the right answer whenever adjacent text already says the same thing —
47
- * a screen reader announcing "Bookings, Bookings" is worse than saying it once.
45
+ * a screen reader announcing "Settings, Settings" is worse than saying it once.
48
46
  */
49
47
  label?: string;
50
48
  }
@@ -1,20 +1,35 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cn } from '../../utils/cn.js';
3
3
  /**
4
- * The handful of icons this app actually uses, drawn inline.
4
+ * A small set of interface icons, drawn inline. For anything else, use `@radix-ui/react-icons`
5
+ * (re-exported from this package).
5
6
  *
6
- * No icon library: a dependency for twelve glyphs costs more in bundle and upgrade surface
7
+ * No icon library: a dependency for a few dozen glyphs costs more in bundle and upgrade surface
7
8
  * than it saves, and these are all one or two paths. They inherit `currentColor` and size
8
9
  * with the text around them, which is what makes them work in a collapsed sidebar and a
9
10
  * button alike.
10
11
  */
12
+ const HOME = 'M3 12l9-8 9 8M5 10v10h5v-6h4v6h5V10';
13
+ const LIST = 'M4 7h16M4 12h16M4 17h10';
14
+ const CALENDAR = 'M7 3v3M17 3v3M4 8h16M5 6h14a1 1 0 011 1v12a1 1 0 01-1 1H5a1 1 0 01-1-1V7a1 1 0 011-1z';
15
+ const USERS = 'M16 20v-2a4 4 0 00-4-4H7a4 4 0 00-4 4v2M9.5 9.5a3 3 0 100-6 3 3 0 000 6zM21 20v-2a4 4 0 00-3-3.87';
16
+ const CARD = 'M3 7h18v12H3zM3 11h18M7 15h3';
17
+ const DOCUMENT = 'M14 3H7a1 1 0 00-1 1v16a1 1 0 001 1h10a1 1 0 001-1V7zM14 3v4h4M9 13h6M9 17h4';
11
18
  const PATHS = {
12
- overview: 'M3 12l9-8 9 8M5 10v10h5v-6h4v6h5V10',
13
- services: 'M4 7h16M4 12h16M4 17h10',
14
- bookings: 'M7 3v3M17 3v3M4 8h16M5 6h14a1 1 0 011 1v12a1 1 0 01-1 1H5a1 1 0 01-1-1V7a1 1 0 011-1z',
15
- clients: 'M16 20v-2a4 4 0 00-4-4H7a4 4 0 00-4 4v2M9.5 9.5a3 3 0 100-6 3 3 0 000 6zM21 20v-2a4 4 0 00-3-3.87',
16
- payments: 'M3 7h18v12H3zM3 11h18M7 15h3',
17
- compliance: 'M14 3H7a1 1 0 00-1 1v16a1 1 0 001 1h10a1 1 0 001-1V7zM14 3v4h4M9 13h6M9 17h4',
19
+ home: HOME,
20
+ list: LIST,
21
+ calendar: CALENDAR,
22
+ users: USERS,
23
+ card: CARD,
24
+ document: DOCUMENT,
25
+ // Legacy names for the six above, named after what one app used them for. Kept so nothing
26
+ // that already passes them breaks; prefer the names above in new code.
27
+ overview: HOME,
28
+ services: LIST,
29
+ bookings: CALENDAR,
30
+ clients: USERS,
31
+ payments: CARD,
32
+ compliance: DOCUMENT,
18
33
  settings: 'M12 15.5a3.5 3.5 0 100-7 3.5 3.5 0 000 7zM19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09A1.65 1.65 0 008 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9c.14.35.4.65.73.85',
19
34
  collapse: 'M15 6l-6 6 6 6',
20
35
  expand: 'M9 6l6 6-6 6',
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface PillProps {
3
+ children: ReactNode;
4
+ /** Shows a remove button. */
5
+ onRemove?: () => void;
6
+ /**
7
+ * The remove button's name. Default: the `remove` UI string with the pill's text, which
8
+ * needs `children` to be a string; pass this when it is not.
9
+ */
10
+ removeLabel?: string;
11
+ className?: string;
12
+ }
13
+ /**
14
+ * A small token, removable or not: an active filter, a chosen tag. The remove button has its
15
+ * own name ("Remove Status: Active"), and a 44px hit area on touch screens.
16
+ */
17
+ export declare function Pill({ children, onRemove, removeLabel, className }: PillProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Cross2Icon } from '@radix-ui/react-icons';
3
+ import { useUiStrings } from '../../strings.js';
4
+ /**
5
+ * A small token, removable or not: an active filter, a chosen tag. The remove button has its
6
+ * own name ("Remove Status: Active"), and a 44px hit area on touch screens.
7
+ */
8
+ export function Pill({ children, onRemove, removeLabel, className }) {
9
+ const strings = useUiStrings();
10
+ const label = removeLabel ?? (typeof children === 'string' ? strings.remove(children) : undefined);
11
+ return (_jsxs("span", { className: ['ui-pill', className].filter(Boolean).join(' '), "data-removable": onRemove ? '' : undefined, children: [children, onRemove ? (_jsx("button", { type: "button", className: "ui-pill__remove", "aria-label": label, onClick: onRemove, children: _jsx(Cross2Icon, { "aria-hidden": "true" }) })) : null] }));
12
+ }
@@ -0,0 +1 @@
1
+ export * from './Pill.js';
@@ -0,0 +1 @@
1
+ export * from './Pill.js';
@@ -1,13 +1,20 @@
1
+ import { type Breakpoint } from '../../hooks/breakpoints.js';
1
2
  import { type DialogProps } from '../Dialog/Dialog.js';
2
3
  import { type SheetDetent } from '../Sheet/Sheet.js';
3
4
  export interface ResponsiveDialogProps extends DialogProps {
4
- /** The detent the sheet opens at below `md`. Default `fit`. */
5
+ /** The detent the sheet opens at below `breakpoint`. Default `fit`. */
5
6
  sheetDetent?: SheetDetent;
7
+ /**
8
+ * From this breakpoint up it is a Dialog; below it, a Sheet. Default `sm` (768px), which is
9
+ * what Tailwind calls `md`.
10
+ */
11
+ breakpoint?: Breakpoint;
6
12
  }
7
13
  /**
8
14
  * The same props as {@link Dialog}, presented the way the screen asks: a centred dialog on a
9
15
  * desktop, a bottom sheet on a phone. Both sit on Radix Dialog, so the role, the focus trap,
10
16
  * Escape and the labelling are the same either way. Crossing the breakpoint while open remounts
11
17
  * the content, which loses uncommitted state inside `children` — keep form state in the parent.
18
+ * Without `matchMedia` (SSR) it renders the Dialog.
12
19
  */
13
- export declare function ResponsiveDialog({ sheetDetent, ...props }: ResponsiveDialogProps): import("react").JSX.Element;
20
+ export declare function ResponsiveDialog({ sheetDetent, breakpoint, ...props }: ResponsiveDialogProps): import("react").JSX.Element;
@@ -1,27 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useSyncExternalStore } from 'react';
2
+ import { minWidth } from '../../hooks/breakpoints.js';
3
+ import { useMediaQuery } from '../../hooks/useMediaQuery.js';
3
4
  import { Dialog } from '../Dialog/Dialog.js';
4
5
  import { Sheet } from '../Sheet/Sheet.js';
5
- /** Tailwind's `md`. Below it the dialog becomes a bottom sheet. */
6
- const MD = '(min-width: 768px)';
7
- function subscribe(onChange) {
8
- const query = window.matchMedia(MD);
9
- query.addEventListener('change', onChange);
10
- return () => query.removeEventListener('change', onChange);
11
- }
12
- /** A Dialog at `md` and up, a Sheet below. Without `matchMedia` (SSR) the Dialog. */
13
- function useIsMdUp() {
14
- return useSyncExternalStore(subscribe, () => window.matchMedia(MD).matches, () => true);
15
- }
16
6
  /**
17
7
  * The same props as {@link Dialog}, presented the way the screen asks: a centred dialog on a
18
8
  * desktop, a bottom sheet on a phone. Both sit on Radix Dialog, so the role, the focus trap,
19
9
  * Escape and the labelling are the same either way. Crossing the breakpoint while open remounts
20
10
  * the content, which loses uncommitted state inside `children` — keep form state in the parent.
11
+ * Without `matchMedia` (SSR) it renders the Dialog.
21
12
  */
22
- export function ResponsiveDialog({ sheetDetent = 'fit', ...props }) {
23
- const mdUp = useIsMdUp();
24
- if (mdUp)
13
+ export function ResponsiveDialog({ sheetDetent = 'fit', breakpoint = 'sm', ...props }) {
14
+ const wide = useMediaQuery(minWidth(breakpoint));
15
+ if (wide)
25
16
  return _jsx(Dialog, { ...props });
26
17
  return (_jsx(Sheet, { open: props.open, onOpenChange: props.onOpenChange, title: props.title, description: props.description, footer: props.footer, className: props.className, detent: sheetDetent, showCloseButton: true, children: props.children }));
27
18
  }
@@ -0,0 +1,14 @@
1
+ export type StatusTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
2
+ export interface StatusIndicatorProps {
3
+ tone?: StatusTone;
4
+ /** Always shown: the colour is never the only signal. */
5
+ label: string;
6
+ /** A slow pulse, for something in progress. Off under reduced motion. */
7
+ pulse?: boolean;
8
+ className?: string;
9
+ }
10
+ /**
11
+ * A coloured dot and a word: "Active", "Failed", "Syncing". Lighter than a Badge, for table
12
+ * cells and lists where a filled pill per row would be loud.
13
+ */
14
+ export declare function StatusIndicator({ tone, label, pulse, className, }: StatusIndicatorProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A coloured dot and a word: "Active", "Failed", "Syncing". Lighter than a Badge, for table
4
+ * cells and lists where a filled pill per row would be loud.
5
+ */
6
+ export function StatusIndicator({ tone = 'neutral', label, pulse, className, }) {
7
+ return (_jsxs("span", { className: ['ui-status', className].filter(Boolean).join(' '), "data-tone": tone, "data-pulse": pulse ? '' : undefined, children: [_jsx("span", { className: "ui-status__dot", "aria-hidden": "true" }), label] }));
8
+ }
@@ -0,0 +1 @@
1
+ export * from './StatusIndicator.js';
@@ -0,0 +1 @@
1
+ export * from './StatusIndicator.js';
@@ -0,0 +1,36 @@
1
+ import type { AnchorHTMLAttributes, MouseEventHandler, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * What a `render` function receives to build a router link. Spread it onto the element you
4
+ * return, e.g. `render={(p) => <RouterLink to={p.href} {...p} />}`.
5
+ *
6
+ * Every component in this library that navigates (TextLink, NavItem, Breadcrumbs, PageTabs)
7
+ * takes the same `render`, so the library never imports a router.
8
+ */
9
+ export interface LinkRenderProps {
10
+ href: string;
11
+ className?: string;
12
+ children: ReactNode;
13
+ 'aria-current'?: 'page';
14
+ onClick?: MouseEventHandler<HTMLElement>;
15
+ }
16
+ export type RenderLink = (props: LinkRenderProps) => ReactElement;
17
+ export interface TextLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'color' | 'children'> {
18
+ href: string;
19
+ children: ReactNode;
20
+ /** Wraps a router link. Receives {@link LinkRenderProps}. */
21
+ render?: RenderLink;
22
+ /**
23
+ * Opens in a new tab with `rel="noopener noreferrer"`, and says so to screen readers
24
+ * (the `opensInNewTab` UI string).
25
+ */
26
+ external?: boolean;
27
+ /** Default `hover`, Themes' own default. */
28
+ underline?: 'auto' | 'hover' | 'always';
29
+ }
30
+ /**
31
+ * An inline text link in the theme's link style, router-agnostic.
32
+ *
33
+ * Named `TextLink` because `Link` is already Radix Themes' own component, re-exported from
34
+ * this package as a primitive.
35
+ */
36
+ export declare function TextLink({ href, children, render, external, underline, ...rest }: TextLinkProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link as ThemedLink } from '@radix-ui/themes';
3
+ import { useUiStrings } from '../../strings.js';
4
+ /**
5
+ * An inline text link in the theme's link style, router-agnostic.
6
+ *
7
+ * Named `TextLink` because `Link` is already Radix Themes' own component, re-exported from
8
+ * this package as a primitive.
9
+ */
10
+ export function TextLink({ href, children, render, external = false, underline, ...rest }) {
11
+ const strings = useUiStrings();
12
+ const content = external ? (_jsxs(_Fragment, { children: [children, " ", _jsx("span", { className: "ui-sr-only", children: strings.opensInNewTab })] })) : (children);
13
+ if (render) {
14
+ return (_jsx(ThemedLink, { asChild: true, underline: underline, children: render({ href, children: content }) }));
15
+ }
16
+ return (_jsx(ThemedLink, { href: href, underline: underline, ...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {}), ...rest, children: content }));
17
+ }
@@ -0,0 +1 @@
1
+ export * from './TextLink.js';
@@ -0,0 +1 @@
1
+ export * from './TextLink.js';
@@ -30,7 +30,7 @@ type Shorthand = (options: Omit<ToastOptions, 'type'>) => string;
30
30
  *
31
31
  * toast({ title: 'Saved' });
32
32
  * toast.error({ title: 'Payment failed', duration: Infinity });
33
- * toast.success({ title: 'Booking cancelled', action: { label: 'Undo', onClick: restore } });
33
+ * toast.success({ title: 'Item archived', action: { label: 'Undo', onClick: restore } });
34
34
  * toast.dismiss(id);
35
35
  */
36
36
  export declare const toast: typeof show & {
@@ -36,7 +36,7 @@ function show(options) {
36
36
  *
37
37
  * toast({ title: 'Saved' });
38
38
  * toast.error({ title: 'Payment failed', duration: Infinity });
39
- * toast.success({ title: 'Booking cancelled', action: { label: 'Undo', onClick: restore } });
39
+ * toast.success({ title: 'Item archived', action: { label: 'Undo', onClick: restore } });
40
40
  * toast.dismiss(id);
41
41
  */
42
42
  export const toast = Object.assign(show, {
@@ -1,3 +1,4 @@
1
+ export * from './ActionMenu/index.js';
1
2
  export * from './ActionSheet/index.js';
2
3
  export * from './Alert/index.js';
3
4
  export * from './Avatar/index.js';
@@ -15,6 +16,7 @@ export * from './Label/index.js';
15
16
  export * from './List/index.js';
16
17
  export * from './NavBar/index.js';
17
18
  export * from './NotificationCard/index.js';
19
+ export * from './Pill/index.js';
18
20
  export * from './Popover/index.js';
19
21
  export * from './Progress/index.js';
20
22
  export * from './PullToRefresh/index.js';
@@ -29,8 +31,10 @@ export * from './Skeleton/index.js';
29
31
  export * from './Slider/index.js';
30
32
  export * from './Spinner/index.js';
31
33
  export * from './StatTile/index.js';
34
+ export * from './StatusIndicator/index.js';
32
35
  export * from './Switch/index.js';
33
36
  export * from './TabBar/index.js';
34
37
  export * from './Textarea/index.js';
38
+ export * from './TextLink/index.js';
35
39
  export * from './Toast/index.js';
36
40
  export * from './Tooltip/index.js';
@@ -1,3 +1,4 @@
1
+ export * from './ActionMenu/index.js';
1
2
  export * from './ActionSheet/index.js';
2
3
  export * from './Alert/index.js';
3
4
  export * from './Avatar/index.js';
@@ -15,6 +16,7 @@ export * from './Label/index.js';
15
16
  export * from './List/index.js';
16
17
  export * from './NavBar/index.js';
17
18
  export * from './NotificationCard/index.js';
19
+ export * from './Pill/index.js';
18
20
  export * from './Popover/index.js';
19
21
  export * from './Progress/index.js';
20
22
  export * from './PullToRefresh/index.js';
@@ -29,8 +31,10 @@ export * from './Skeleton/index.js';
29
31
  export * from './Slider/index.js';
30
32
  export * from './Spinner/index.js';
31
33
  export * from './StatTile/index.js';
34
+ export * from './StatusIndicator/index.js';
32
35
  export * from './Switch/index.js';
33
36
  export * from './TabBar/index.js';
34
37
  export * from './Textarea/index.js';
38
+ export * from './TextLink/index.js';
35
39
  export * from './Toast/index.js';
36
40
  export * from './Tooltip/index.js';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Radix Themes' breakpoints, in pixels. `Card`, `TabBar`, `ScrollStrip` and the media queries in
3
+ * `tokens.css` already use this scale; this constant is the one place JavaScript reads it from.
4
+ *
5
+ * Note that `sm` here is 768px, which is what Tailwind calls `md`. A class cannot be generated
6
+ * for both scales, so components that switch structure in JavaScript take a name from this
7
+ * scale rather than a Tailwind one.
8
+ */
9
+ export declare const BREAKPOINTS: {
10
+ readonly xs: 520;
11
+ readonly sm: 768;
12
+ readonly md: 1024;
13
+ readonly lg: 1280;
14
+ readonly xl: 1640;
15
+ };
16
+ export type Breakpoint = keyof typeof BREAKPOINTS;
17
+ /** `initial` is below `xs`: the unconditional value, as in `Responsive<T>`. */
18
+ export type BreakpointOrInitial = 'initial' | Breakpoint;
19
+ /** The media query that matches from `bp` up, e.g. `(min-width: 1024px)` for `md`. */
20
+ export declare function minWidth(bp: Breakpoint): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Radix Themes' breakpoints, in pixels. `Card`, `TabBar`, `ScrollStrip` and the media queries in
3
+ * `tokens.css` already use this scale; this constant is the one place JavaScript reads it from.
4
+ *
5
+ * Note that `sm` here is 768px, which is what Tailwind calls `md`. A class cannot be generated
6
+ * for both scales, so components that switch structure in JavaScript take a name from this
7
+ * scale rather than a Tailwind one.
8
+ */
9
+ export const BREAKPOINTS = { xs: 520, sm: 768, md: 1024, lg: 1280, xl: 1640 };
10
+ /** The media query that matches from `bp` up, e.g. `(min-width: 1024px)` for `md`. */
11
+ export function minWidth(bp) {
12
+ return `(min-width: ${BREAKPOINTS[bp]}px)`;
13
+ }
@@ -0,0 +1,4 @@
1
+ export * from './breakpoints.js';
2
+ export * from './useControllableState.js';
3
+ export * from './useElementWidth.js';
4
+ export * from './useMediaQuery.js';
@@ -0,0 +1,4 @@
1
+ export * from './breakpoints.js';
2
+ export * from './useControllableState.js';
3
+ export * from './useElementWidth.js';
4
+ export * from './useMediaQuery.js';
@@ -0,0 +1,17 @@
1
+ export interface UseControllableStateOptions<T> {
2
+ /** Controlled value. When it is not `undefined`, the hook holds no state of its own. */
3
+ value?: T;
4
+ /** Initial value when uncontrolled. */
5
+ defaultValue: T;
6
+ /** Called with every requested change, whether controlled or not. */
7
+ onChange?: (value: T) => void;
8
+ }
9
+ /**
10
+ * The `value` / `defaultValue` / `onValueChange` pattern in one place, so every component that
11
+ * offers both modes behaves the same way: a controlled component never changes on its own,
12
+ * and an uncontrolled one still reports each change.
13
+ *
14
+ * Whether it is controlled is decided by the first render. Switching between the two is a
15
+ * bug in the caller, the same rule React applies to inputs.
16
+ */
17
+ export declare function useControllableState<T>({ value, defaultValue, onChange, }: UseControllableStateOptions<T>): [T, (next: T) => void];
@@ -0,0 +1,22 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ /**
3
+ * The `value` / `defaultValue` / `onValueChange` pattern in one place, so every component that
4
+ * offers both modes behaves the same way: a controlled component never changes on its own,
5
+ * and an uncontrolled one still reports each change.
6
+ *
7
+ * Whether it is controlled is decided by the first render. Switching between the two is a
8
+ * bug in the caller, the same rule React applies to inputs.
9
+ */
10
+ export function useControllableState({ value, defaultValue, onChange, }) {
11
+ const controlled = useRef(value !== undefined).current;
12
+ const [inner, setInner] = useState(defaultValue);
13
+ const current = controlled ? value : inner;
14
+ const onChangeRef = useRef(onChange);
15
+ onChangeRef.current = onChange;
16
+ const set = useCallback((next) => {
17
+ if (!controlled)
18
+ setInner(next);
19
+ onChangeRef.current?.(next);
20
+ }, [controlled]);
21
+ return [current, set];
22
+ }
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * The content width of an element, kept current with one ResizeObserver. `0` until measured,
4
+ * and where ResizeObserver does not exist (the server, some test environments), so callers
5
+ * should fall back to a sensible default.
6
+ */
7
+ export declare function useElementWidth(ref: RefObject<HTMLElement | null>): number;
@@ -0,0 +1,22 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ /**
3
+ * The content width of an element, kept current with one ResizeObserver. `0` until measured,
4
+ * and where ResizeObserver does not exist (the server, some test environments), so callers
5
+ * should fall back to a sensible default.
6
+ */
7
+ export function useElementWidth(ref) {
8
+ const [width, setWidth] = useState(0);
9
+ useLayoutEffect(() => {
10
+ const el = ref.current;
11
+ if (!el || typeof ResizeObserver === 'undefined')
12
+ return;
13
+ setWidth(el.getBoundingClientRect().width);
14
+ const observer = new ResizeObserver(([entry]) => {
15
+ if (entry)
16
+ setWidth(entry.contentRect.width);
17
+ });
18
+ observer.observe(el);
19
+ return () => observer.disconnect();
20
+ }, [ref]);
21
+ return width;
22
+ }
@@ -0,0 +1,18 @@
1
+ import { type BreakpointOrInitial } from './breakpoints.js';
2
+ /**
3
+ * Whether a media query matches, kept current as it changes.
4
+ *
5
+ * For switching *structure* (a dialog that becomes a sheet). A visual change belongs in CSS,
6
+ * where it needs no JavaScript and does not flash on hydration.
7
+ *
8
+ * On the server, and wherever `matchMedia` is missing, it returns `serverValue`. The default is
9
+ * `true` because the existing atoms render their desktop branch first.
10
+ */
11
+ export declare function useMediaQuery(query: string, serverValue?: boolean): boolean;
12
+ /**
13
+ * The largest breakpoint the viewport has reached, or `initial` below `xs`.
14
+ *
15
+ * On the server it returns `serverValue`, default `lg`, for the same desktop-first reason as
16
+ * {@link useMediaQuery}.
17
+ */
18
+ export declare function useBreakpoint(serverValue?: BreakpointOrInitial): BreakpointOrInitial;
@@ -0,0 +1,51 @@
1
+ import { useMemo, useSyncExternalStore } from 'react';
2
+ import { BREAKPOINTS, minWidth } from './breakpoints.js';
3
+ function hasMatchMedia() {
4
+ return typeof window !== 'undefined' && typeof window.matchMedia === 'function';
5
+ }
6
+ function subscribeTo(queries) {
7
+ return (onChange) => {
8
+ if (!hasMatchMedia())
9
+ return () => { };
10
+ const lists = queries.map((q) => window.matchMedia(q));
11
+ for (const l of lists)
12
+ l.addEventListener('change', onChange);
13
+ return () => {
14
+ for (const l of lists)
15
+ l.removeEventListener('change', onChange);
16
+ };
17
+ };
18
+ }
19
+ /**
20
+ * Whether a media query matches, kept current as it changes.
21
+ *
22
+ * For switching *structure* (a dialog that becomes a sheet). A visual change belongs in CSS,
23
+ * where it needs no JavaScript and does not flash on hydration.
24
+ *
25
+ * On the server, and wherever `matchMedia` is missing, it returns `serverValue`. The default is
26
+ * `true` because the existing atoms render their desktop branch first.
27
+ */
28
+ export function useMediaQuery(query, serverValue = true) {
29
+ // A new subscribe function each render would make React resubscribe on every render.
30
+ const subscribe = useMemo(() => subscribeTo([query]), [query]);
31
+ return useSyncExternalStore(subscribe, () => (hasMatchMedia() ? window.matchMedia(query).matches : serverValue), () => serverValue);
32
+ }
33
+ const ORDER = Object.keys(BREAKPOINTS);
34
+ const subscribeToBreakpoints = subscribeTo(ORDER.map(minWidth));
35
+ /**
36
+ * The largest breakpoint the viewport has reached, or `initial` below `xs`.
37
+ *
38
+ * On the server it returns `serverValue`, default `lg`, for the same desktop-first reason as
39
+ * {@link useMediaQuery}.
40
+ */
41
+ export function useBreakpoint(serverValue = 'lg') {
42
+ return useSyncExternalStore(subscribeToBreakpoints, () => {
43
+ if (!hasMatchMedia())
44
+ return serverValue;
45
+ let current = 'initial';
46
+ for (const bp of ORDER)
47
+ if (window.matchMedia(minWidth(bp)).matches)
48
+ current = bp;
49
+ return current;
50
+ }, () => serverValue);
51
+ }