@dreamtree-org/twreact-ui 1.3.8 → 1.3.9

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 (54) hide show
  1. package/DESIGN.md +11 -3
  2. package/README.md +7 -1
  3. package/ai-skills/dreamtree-ui.md +2 -2
  4. package/dist/charts/index.js +69 -24
  5. package/dist/components/charts/chartTheme.d.ts +2 -5
  6. package/dist/components/charts/chartTheme.d.ts.map +1 -1
  7. package/dist/components/core/Table/Table.d.ts.map +1 -1
  8. package/dist/components/core/Table/useClickOutside.d.ts +5 -2
  9. package/dist/components/core/Table/useClickOutside.d.ts.map +1 -1
  10. package/dist/components/utility/Portal.d.ts +6 -0
  11. package/dist/components/utility/Portal.d.ts.map +1 -0
  12. package/dist/esm/components/charts/CartesianChart.js +2 -1
  13. package/dist/esm/components/charts/DonutChart.js +2 -1
  14. package/dist/esm/components/charts/Sparkline.js +2 -1
  15. package/dist/esm/components/charts/chartTheme.js +1 -22
  16. package/dist/esm/components/core/Button.js +8 -8
  17. package/dist/esm/components/core/Table/Table.js +11 -5
  18. package/dist/esm/hooks/useClickOutside.js +51 -0
  19. package/dist/esm/hooks/useMediaQuery.js +61 -0
  20. package/dist/esm/hooks/useReducedMotion.js +17 -0
  21. package/dist/esm/hooks/useScrollLock.js +76 -9
  22. package/dist/esm/hooks/useTheme.js +6 -1
  23. package/dist/esm/utils/position.js +6 -0
  24. package/dist/hooks/useClickOutside.d.ts +14 -0
  25. package/dist/hooks/useClickOutside.d.ts.map +1 -0
  26. package/dist/hooks/useFloatingPosition.d.ts +37 -0
  27. package/dist/hooks/useFloatingPosition.d.ts.map +1 -0
  28. package/dist/hooks/useMediaQuery.d.ts +22 -0
  29. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  30. package/dist/hooks/usePresence.d.ts +16 -0
  31. package/dist/hooks/usePresence.d.ts.map +1 -0
  32. package/dist/hooks/usePresentation.d.ts +9 -0
  33. package/dist/hooks/usePresentation.d.ts.map +1 -0
  34. package/dist/hooks/useReducedMotion.d.ts +10 -0
  35. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  36. package/dist/hooks/useScrollLock.d.ts +11 -1
  37. package/dist/hooks/useScrollLock.d.ts.map +1 -1
  38. package/dist/hooks/useTheme.d.ts.map +1 -1
  39. package/dist/index.css +1 -1
  40. package/dist/index.esm.css +1 -1
  41. package/dist/index.esm.js +142 -34
  42. package/dist/index.js +142 -34
  43. package/dist/tokens/tailwind3-preset.cjs +101 -8
  44. package/dist/tokens/theme.css +27 -0
  45. package/dist/tokens/tokens.json +175 -0
  46. package/dist/tokens/vars.css +2 -0
  47. package/dist/utils/position.d.ts.map +1 -1
  48. package/dist/utils/touchTarget.d.ts +1 -0
  49. package/dist/utils/touchTarget.d.ts.map +1 -1
  50. package/mcp/catalog.snapshot.json +1 -1
  51. package/package.json +2 -1
  52. package/tokens/lib.cjs +35 -0
  53. package/tokens/tokens.json +58 -2
  54. package/dist/esm/components/core/Table/useClickOutside.js +0 -26
package/DESIGN.md CHANGED
@@ -37,7 +37,11 @@ typography:
37
37
  rounded:
38
38
  control: 8px
39
39
  card: 12px
40
+ sheet: 16px
40
41
  pill: 9999px
42
+ elevation: [xs, sm, md, lg, xl, overlay]
43
+ zIndex: { base: 0, sticky: 20, dropdown: 30, overlay: 40, modal: 50, popover: 60, toast: 70, tooltip: 80 }
44
+ motion: { fast: 120ms, base: 200ms, slow: 320ms }
41
45
  spacing:
42
46
  grid: 4px
43
47
  steps: [4, 8, 12, 16, 20, 24, 32]
@@ -81,9 +85,12 @@ Every Dreamtree product screen, and every prototype generated by Dreamtree Studi
81
85
  ## 3. Type, space and shape
82
86
 
83
87
  - Font: Inter. Numbers in tables and KPIs use `tabular-nums`.
84
- - Scale: page title `text-2xl font-semibold`, section title `text-base font-semibold`, body `text-sm`, caption `text-xs text-muted`, KPI `text-3xl font-semibold`. For tiny labels use `text-micro`, never `text-[10px]` or `text-[11px]`.
88
+ - Scale: use the role classes `text-page-title`, `text-section-title` (both semibold), `text-body`, `text-caption text-muted` and `text-kpi` (semibold). The older `text-2xl font-semibold` / `text-base` / `text-sm` / `text-xs` / `text-3xl` spellings are equivalent. For tiny labels use `text-micro`, never `text-[10px]` or `text-[11px]`. Code and ids use `font-mono`.
89
+ - Elevation: `shadow-xs` … `shadow-xl`, and `shadow-overlay` for dialogs, sheets, popovers and menus. Shadows deepen automatically in dark mode.
90
+ - Layers: `z-sticky`, `z-dropdown`, `z-overlay`, `z-modal`, `z-popover`, `z-toast`, `z-tooltip`. Never `z-50` or an inline `zIndex`.
91
+ - Motion: `duration-fast` (120 ms), `duration-base` (200 ms), `duration-slow` (320 ms) with `ease-standard`, `ease-emphasized` or `ease-spring-out`. Keyframes: `animate-fade-in` / `animate-fade-out`, `animate-scale-in` / `animate-scale-out`, `animate-slide-up`, `animate-slide-down` / `animate-slide-down-out`, `animate-sheet-in` / `animate-sheet-out`. Always pair one with `motion-reduce:animate-none` (e.g. `animate-sheet-in motion-reduce:animate-none`).
85
92
  - **No arbitrary values** (`w-[312px]`, `text-[13px]`, `p-[7px]`). Use the Tailwind scale on a 4 px grid: steps 1, 2, 3, 4, 5, 6, 8.
86
- - Radius: `rounded-control` (8 px) for buttons, inputs and menus; `rounded-card` (12 px) for cards and dialogs; `rounded-full` for pills and badges.
93
+ - Radius: `rounded-control` (8 px) for buttons, inputs and menus; `rounded-card` (12 px) for cards and dialogs; `rounded-sheet` (16 px) for bottom-sheet top corners; `rounded-pill` (or `rounded-full`) for pills and badges.
87
94
  - Sentence case everywhere ("Add obligation", not "Add Obligation").
88
95
 
89
96
  ## 4. Page anatomy
@@ -107,9 +114,10 @@ Every Dreamtree product screen, and every prototype generated by Dreamtree Studi
107
114
  ## 7. Responsive and accessible
108
115
 
109
116
  - No horizontal page scroll at **390 px**. Below `lg` the shell becomes a top bar with a drawer. Wide tables scroll inside their card.
110
- - Tap targets are at least 40 px. Every control has a visible label. Focus is visible (`focus:ring-2 focus:ring-primary-500 focus:ring-offset-2`).
117
+ - Tap targets are at least **44 px** on touch screens (Apple HIG; WCAG 2.5.5). Icon-only controls grow to 44×44 on `pointer: coarse` and keep their compact size with a mouse. Pressables shrink slightly while pressed (`active:scale-[0.97]`, off under reduced motion). Every control has a visible label. Focus is visible for keyboard users (`focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2`); a mouse click doesn't leave a ring.
111
118
  - Text contrast is at least 4.5:1. Status is never shown by colour alone: badges carry words.
112
119
  - Respect `prefers-reduced-motion`.
120
+ - Phones: pages use `viewport-fit=cover`; bars that touch a screen edge pad with `pt-safe` / `pb-safe` / `px-safe` (or `inset-safe`). Full-height layouts use `min-h-dvh` / `h-dvh`, never `100vh`. Text fields render at 16 px on touch screens so iOS doesn't zoom on focus.
113
121
 
114
122
  ## 8. Data and content
115
123
 
package/README.md CHANGED
@@ -881,7 +881,7 @@ module.exports = {
881
881
  @source "../node_modules/@dreamtree-org/twreact-ui/dist/index.esm.js";
882
882
  ```
883
883
 
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.
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`. Non-colour scales come from the same file: type roles (`text-page-title`, `text-section-title`, `text-body`, `text-caption`, `text-kpi`), `rounded-sheet` / `rounded-pill`, `font-mono`, elevation (`shadow-xs` … `shadow-xl`, `shadow-overlay`; deeper in dark mode via `--dt-shadow-strength`), z-layers (`z-sticky`, `z-dropdown`, `z-overlay`, `z-modal`, `z-popover`, `z-toast`, `z-tooltip`) and motion (`duration-fast|base|slow`, `ease-standard|emphasized|spring-out`). The Tailwind 3 preset and Tailwind 4 theme include them. Add a brand by creating `tokens/brands/<name>.json`; it overrides only the steps and roles you list.
885
885
 
886
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`).
887
887
 
@@ -889,6 +889,10 @@ The palettes are `primary`, `secondary`, `success`, `warning`, `error`, `info` a
889
889
 
890
890
  **SSR:** `Tabs`, `Tooltip` and `Switch` generate ids with `React.useId()`, so server-rendered pages (Next.js) hydrate without mismatches. The `/charts` components do the same for their SVG ids, and they render token colours and reduced-motion state the way the server did until hydration finishes.
891
891
 
892
+ ## Mobile and touch
893
+
894
+ The stylesheet ships a phone baseline: no tap highlight, no iOS zoom when a field is focused (text fields are 16px on touch screens; add `dt-keep-font-size` to opt one out), `color-scheme` that follows the theme, and no rubber-band overscroll on `body` (add `dt-overscroll-auto` to `<html>` to keep pull-to-refresh). Pad edge bars with `pt-safe` / `pb-safe` / `px-safe` / `inset-safe` — they need `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">` — and size full-height layouts with `min-h-dvh` instead of `100vh`. Touch targets grow to 44px on coarse pointers.
895
+
892
896
  ## Dark Mode
893
897
 
894
898
  Enable dark mode with the built-in theme provider:
@@ -905,6 +909,8 @@ function App() {
905
909
  }
906
910
  ```
907
911
 
912
+ `ThemeProvider` sets `data-theme`, the `.dark` class and `color-scheme` on `<html>`, so `dark:` utilities work whether your `tailwind.config.js` uses `darkMode: 'class'` or a `[data-theme="dark"]` selector.
913
+
908
914
  Surfaces such as `Card`, `Sidebar`, `Navbar`, `Table`, `FootNav` and `Dropdown` use the semantic `bg-surface` / `bg-background` / `border-line` / `text-foreground` / `text-muted` tokens, which switch under `.dark` (or `[data-theme="dark"]`). Re-theme them by overriding the `--dt-surface`, `--dt-background`, `--dt-line`, … variables.
909
915
 
910
916
  ## Examples
@@ -131,7 +131,7 @@ The library exports four kinds of things from `@dreamtree-org/twreact-ui`:
131
131
  | Hook | Purpose |
132
132
  | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
133
133
  | `useTheme` | Read/write the current theme (`light | dark`); pairs with `<ThemeProvider>`. Throws if used outside the provider. |
134
- | `ThemeProvider` | Provider that persists theme to `localStorage` (`dreamtree-theme`) and toggles `data-theme` on `<html>`. |
134
+ | `ThemeProvider` | Provider that persists theme to `localStorage` (`dreamtree-theme`) and toggles `data-theme` and the `.dark` class on `<html>`. |
135
135
  | `useApi` | Thin axios wrapper. Returns `{data, error, loading, lastRequest, fetchData, fetchApi, fetchApiWith}`. Accepts `BASE_URL`, `DEFAULT_HEADERS`, `apiMap`. |
136
136
  | `useMixins` | Bridge between component state, Redux store, and an in-memory cache. Power-user hook — prefer local React state for new code. |
137
137
 
@@ -320,7 +320,7 @@ function ThemeToggle() {
320
320
 
321
321
  `useTheme` MUST be called inside `<ThemeProvider>`; otherwise it throws.
322
322
  `<ThemeProvider>` persists to `localStorage` (`dreamtree-theme`) and
323
- toggles `data-theme` on `<html>`.
323
+ toggles `data-theme` and the `.dark` class on `<html>`.
324
324
 
325
325
  ### useApi
326
326
 
@@ -174,10 +174,75 @@ var ChartFrame = /*#__PURE__*/React__default.default.forwardRef(function ChartFr
174
174
  }));
175
175
  });
176
176
 
177
- /** `true` when the user asked the OS for reduced motion. */
178
- function prefersReducedMotion() {
179
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
180
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
177
+ // Internal (not exported). SSR-safe media-query subscription.
178
+ function getMediaQueryList(query) {
179
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return null;
180
+ return window.matchMedia(query) || null;
181
+ }
182
+ /**
183
+ * Subscribes `onChange` to a MediaQueryList's change events. Uses
184
+ * `addEventListener` where available and falls back to the deprecated
185
+ * `addListener` (Safari < 14). Returns the unsubscribe function.
186
+ */
187
+ function subscribeMediaQuery(query, onChange) {
188
+ var mql = getMediaQueryList(query);
189
+ if (!mql) return function () {};
190
+ if (typeof mql.addEventListener === "function") {
191
+ mql.addEventListener("change", onChange);
192
+ return function () {
193
+ var _mql$removeEventListe;
194
+ return (_mql$removeEventListe = mql.removeEventListener) === null || _mql$removeEventListe === void 0 ? void 0 : _mql$removeEventListe.call(mql, "change", onChange);
195
+ };
196
+ }
197
+ if (typeof mql.addListener === "function") {
198
+ mql.addListener(onChange);
199
+ return function () {
200
+ var _mql$removeListener;
201
+ return (_mql$removeListener = mql.removeListener) === null || _mql$removeListener === void 0 ? void 0 : _mql$removeListener.call(mql, onChange);
202
+ };
203
+ }
204
+ return function () {};
205
+ }
206
+ /**
207
+ * `true` while `query` matches; follows changes live.
208
+ *
209
+ * On the server (and while hydrating) it returns `defaultValue`, so server and
210
+ * client markup agree; the real value is applied right after hydration. Without
211
+ * `window.matchMedia` it always returns `defaultValue`.
212
+ *
213
+ * @param {string} query - a CSS media query, e.g. "(max-width: 639px)".
214
+ * @param {{ defaultValue?: boolean }} [options]
215
+ * @returns {boolean}
216
+ */
217
+ function useMediaQuery(query) {
218
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
219
+ _ref$defaultValue = _ref.defaultValue,
220
+ defaultValue = _ref$defaultValue === void 0 ? false : _ref$defaultValue;
221
+ var subscribe = React.useCallback(function (onChange) {
222
+ return subscribeMediaQuery(query, onChange);
223
+ }, [query]);
224
+ var getSnapshot = function getSnapshot() {
225
+ var mql = getMediaQueryList(query);
226
+ return mql ? Boolean(mql.matches) : defaultValue;
227
+ };
228
+ var getServerSnapshot = function getServerSnapshot() {
229
+ return defaultValue;
230
+ };
231
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
232
+ }
233
+
234
+ // Internal (not exported). Shared by the charts and the overlay primitives.
235
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
236
+ /**
237
+ * `true` while the user asks the OS for reduced motion; follows changes live.
238
+ * `false` on the server and while hydrating, so the first render matches.
239
+ *
240
+ * @returns {boolean}
241
+ */
242
+ function useReducedMotion() {
243
+ return useMediaQuery(REDUCED_MOTION_QUERY, {
244
+ defaultValue: false
245
+ });
181
246
  }
182
247
 
183
248
  var CHART_PALETTES = ["primary", "secondary", "success", "warning", "error", "info", "gray"];
@@ -267,26 +332,6 @@ function useChartTheme(categories, colors) {
267
332
  series: seriesColors(categories, colors, live)
268
333
  };
269
334
  }
270
- function subscribeReducedMotion(onChange) {
271
- var _query$addEventListen;
272
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return function () {};
273
- var query = window.matchMedia("(prefers-reduced-motion: reduce)");
274
- (_query$addEventListen = query.addEventListener) === null || _query$addEventListen === void 0 || _query$addEventListen.call(query, "change", onChange);
275
- return function () {
276
- var _query$removeEventLis;
277
- return (_query$removeEventLis = query.removeEventListener) === null || _query$removeEventLis === void 0 ? void 0 : _query$removeEventLis.call(query, "change", onChange);
278
- };
279
- }
280
- var serverReducedMotion = function serverReducedMotion() {
281
- return false;
282
- };
283
- /**
284
- * `true` while the user asks the OS for reduced motion; follows changes live.
285
- * `false` on the server and while hydrating, so the first render matches.
286
- */
287
- function useReducedMotion() {
288
- return React.useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, serverReducedMotion);
289
- }
290
335
  var defaultValueFormatter = function defaultValueFormatter(value) {
291
336
  return typeof value === "number" ? value.toLocaleString() : value == null ? "" : String(value);
292
337
  };
@@ -18,11 +18,6 @@ export function useChartTheme(categories: any, colors: any): {
18
18
  };
19
19
  series: any;
20
20
  };
21
- /**
22
- * `true` while the user asks the OS for reduced motion; follows changes live.
23
- * `false` on the server and while hydrating, so the first render matches.
24
- */
25
- export function useReducedMotion(): boolean;
26
21
  /**
27
22
  * A per-instance id for the SVG defs recharts draws (clip paths). recharts 2
28
23
  * falls back to a module-level counter, which differs between the server and the
@@ -30,7 +25,9 @@ export function useReducedMotion(): boolean;
30
25
  * is safe inside `url(#…)`.
31
26
  */
32
27
  export function useChartId(): string;
28
+ export { useReducedMotion };
33
29
  export const CHART_PALETTES: string[];
34
30
  export const DEFAULT_CHART_COLORS: string[];
35
31
  export function defaultValueFormatter(value: any): string;
32
+ import { useReducedMotion } from "../../hooks/useReducedMotion";
36
33
  //# sourceMappingURL=chartTheme.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chartTheme.d.ts","sourceRoot":"","sources":["../../../src/components/charts/chartTheme.js"],"names":[],"mappings":"AAgBA,sGAAsG;AACtG,oDAGC;AAgBD;;;;GAIG;AACH,8DAGC;AAED,gFAGC;AAwBD;;;;GAIG;AACH;;;;;EAIC;AAWD;;;GAGG;AACH,4CAEC;AAKD;;;;;GAKG;AACH,qCAEC;AA7GD,sCAAsG;AAEtG,4CAA4G;AAgGrG,0DACkF"}
1
+ {"version":3,"file":"chartTheme.d.ts","sourceRoot":"","sources":["../../../src/components/charts/chartTheme.js"],"names":[],"mappings":"AAmBA,sGAAsG;AACtG,oDAGC;AAgBD;;;;GAIG;AACH,8DAGC;AAED,gFAGC;AAwBD;;;;GAIG;AACH;;;;;EAIC;AAKD;;;;;GAKG;AACH,qCAEC;;AA5FD,sCAAsG;AAEtG,4CAA4G;AA+ErG,0DACkF;iCAvFxD,8BAA8B"}
@@ -1 +1 @@
1
- {"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../../../src/components/core/Table/Table.jsx"],"names":[],"mappings":";AA8CA,+EAkhDG;kBAvjDI,OAAO"}
1
+ {"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../../../src/components/core/Table/Table.jsx"],"names":[],"mappings":";AA8CA,+EA8gDG;kBAnjDI,OAAO"}
@@ -1,8 +1,11 @@
1
1
  /**
2
- * Custom hook to detect clicks outside of specified refs
2
+ * Legacy shim — the implementation lives in `src/hooks/useClickOutside.js`.
3
+ * Kept at this path (and with the old positional signature) because
4
+ * `core/Table/index.js` re-exports it.
5
+ *
3
6
  * @param {Array} refs - Array of React refs to check
4
7
  * @param {boolean} isActive - Whether the hook should be active
5
- * @param {Function} onClickOutside - Callback when click is detected outside
8
+ * @param {Function} onClickOutside - Callback when a press lands outside
6
9
  */
7
10
  export function useClickOutside(refs: any[], isActive: boolean, onClickOutside: Function): void;
8
11
  export default useClickOutside;
@@ -1 +1 @@
1
- {"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../../../../src/components/core/Table/useClickOutside.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,uDAHW,OAAO,kCAiBjB"}
1
+ {"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../../../../src/components/core/Table/useClickOutside.js"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,uDAHW,OAAO,kCAKjB"}
@@ -0,0 +1,6 @@
1
+ export default Portal;
2
+ declare function Portal({ container, children }: {
3
+ container: any;
4
+ children: any;
5
+ }): import("react").ReactPortal | null;
6
+ //# sourceMappingURL=Portal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Portal.d.ts","sourceRoot":"","sources":["../../../src/components/utility/Portal.jsx"],"names":[],"mappings":";AAcA;;;uCAOC"}
@@ -4,7 +4,8 @@ import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { ResponsiveContainer, AreaChart, BarChart, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Bar, Area, Line } from 'recharts';
6
6
  import { ChartFrame, ChartTooltipContent } from './ChartFrame.js';
7
- import { useChartTheme, useReducedMotion, useChartId, defaultValueFormatter } from './chartTheme.js';
7
+ import { useChartTheme, useChartId, defaultValueFormatter } from './chartTheme.js';
8
+ import { useReducedMotion } from '../../hooks/useReducedMotion.js';
8
9
 
9
10
  var _excluded = ["kind", "data", "index", "categories", "colors", "valueFormatter", "xAxisFormatter", "height", "showLegend", "showTooltip", "showXAxis", "showYAxis", "showGrid", "yAxisWidth", "curveType", "stacked", "horizontal", "showDots", "connectNulls", "animate", "title", "description", "emptyState", "accessibleTable", "className"];
10
11
  var ROOTS = {
@@ -4,7 +4,8 @@ import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { ResponsiveContainer, PieChart, Tooltip, Pie } from 'recharts';
6
6
  import { ChartFrame, ChartTooltipContent } from './ChartFrame.js';
7
- import { useChartTheme, useReducedMotion, useChartId, defaultValueFormatter } from './chartTheme.js';
7
+ import { useChartTheme, useChartId, defaultValueFormatter } from './chartTheme.js';
8
+ import { useReducedMotion } from '../../hooks/useReducedMotion.js';
8
9
 
9
10
  var _excluded = ["data", "index", "category", "colors", "valueFormatter", "variant", "innerRadius", "outerRadius", "centerLabel", "height", "showLegend", "showTooltip", "animate", "title", "description", "emptyState", "accessibleTable", "className"];
10
11
  var DonutChart = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
@@ -4,7 +4,8 @@ import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { ResponsiveContainer, AreaChart, LineChart, YAxis, Tooltip, Area, Line } from 'recharts';
6
6
  import { ChartFrame, ChartTooltipContent } from './ChartFrame.js';
7
- import { useChartTheme, useReducedMotion, useChartId, defaultValueFormatter } from './chartTheme.js';
7
+ import { useChartTheme, useChartId, defaultValueFormatter } from './chartTheme.js';
8
+ import { useReducedMotion } from '../../hooks/useReducedMotion.js';
8
9
 
9
10
  var _excluded = ["data", "category", "index", "color", "type", "valueFormatter", "height", "showTooltip", "animate", "title", "description", "emptyState", "accessibleTable", "className"];
10
11
  /** A word-sized trend line: no axes, grid or legend — just the shape of the data. */
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
  import { slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { useSyncExternalStore, useId } from 'react';
4
- import { prefersReducedMotion } from '../../utils/position.js';
5
4
 
6
5
  var CHART_PALETTES = ["primary", "secondary", "success", "warning", "error", "info", "gray"];
7
6
  var DEFAULT_CHART_COLORS = ["primary", "success", "warning", "error", "info", "secondary", "gray"];
@@ -90,26 +89,6 @@ function useChartTheme(categories, colors) {
90
89
  series: seriesColors(categories, colors, live)
91
90
  };
92
91
  }
93
- function subscribeReducedMotion(onChange) {
94
- var _query$addEventListen;
95
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return function () {};
96
- var query = window.matchMedia("(prefers-reduced-motion: reduce)");
97
- (_query$addEventListen = query.addEventListener) === null || _query$addEventListen === void 0 || _query$addEventListen.call(query, "change", onChange);
98
- return function () {
99
- var _query$removeEventLis;
100
- return (_query$removeEventLis = query.removeEventListener) === null || _query$removeEventLis === void 0 ? void 0 : _query$removeEventLis.call(query, "change", onChange);
101
- };
102
- }
103
- var serverReducedMotion = function serverReducedMotion() {
104
- return false;
105
- };
106
- /**
107
- * `true` while the user asks the OS for reduced motion; follows changes live.
108
- * `false` on the server and while hydrating, so the first render matches.
109
- */
110
- function useReducedMotion() {
111
- return useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, serverReducedMotion);
112
- }
113
92
  var defaultValueFormatter = function defaultValueFormatter(value) {
114
93
  return typeof value === "number" ? value.toLocaleString() : value == null ? "" : String(value);
115
94
  };
@@ -123,4 +102,4 @@ function useChartId() {
123
102
  return "dt-chart-".concat(useId().replace(/[^A-Za-z0-9_-]/g, ""));
124
103
  }
125
104
 
126
- export { CHART_PALETTES, DEFAULT_CHART_COLORS, defaultValueFormatter, resolveColor, seriesColors, tokenVar, useChartId, useChartTheme, useReducedMotion };
105
+ export { CHART_PALETTES, DEFAULT_CHART_COLORS, defaultValueFormatter, resolveColor, seriesColors, tokenVar, useChartId, useChartTheme };
@@ -26,15 +26,15 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
26
26
  var Component = as || (props.href !== undefined ? 'a' : 'button');
27
27
  var isNativeButton = Component === 'button';
28
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', {
29
+ var baseClasses = cn('inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none', {
30
30
  // Variants
31
- 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500': variant === 'primary',
32
- 'bg-secondary-100 text-secondary-900 hover:bg-secondary-200 focus:ring-secondary-500': variant === 'secondary',
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',
34
- 'bg-transparent text-primary-600 hover:bg-primary-50 focus:ring-primary-500': variant === 'ghost',
35
- 'bg-error-600 text-white hover:bg-error-700 focus:ring-error-500': variant === 'destructive',
36
- 'bg-success-600 text-white hover:bg-success-700 focus:ring-success-500': variant === 'success',
37
- 'bg-warning-600 text-white hover:bg-warning-700 focus:ring-warning-500': variant === 'warning',
31
+ 'bg-primary-600 text-white hover:bg-primary-700 focus-visible:ring-primary-500': variant === 'primary',
32
+ 'bg-secondary-100 text-secondary-900 hover:bg-secondary-200 focus-visible:ring-secondary-500': variant === 'secondary',
33
+ 'bg-transparent border border-gray-300 hover:bg-gray-50 focus-visible:ring-primary-500 dark:border-gray-600 dark:text-gray-100 dark:hover:bg-gray-800 dark:focus-visible:ring-offset-gray-900': variant === 'outline',
34
+ 'bg-transparent text-primary-600 hover:bg-primary-50 focus-visible:ring-primary-500': variant === 'ghost',
35
+ 'bg-error-600 text-white hover:bg-error-700 focus-visible:ring-error-500': variant === 'destructive',
36
+ 'bg-success-600 text-white hover:bg-success-700 focus-visible:ring-success-500': variant === 'success',
37
+ 'bg-warning-600 text-white hover:bg-warning-700 focus-visible:ring-warning-500': variant === 'warning',
38
38
  // Sizes — every value of the shared `Size` type gets a real height.
39
39
  'h-7 px-2 text-xs': size === 'xs',
40
40
  'h-8 px-3 text-xs': size === 'sm',
@@ -8,7 +8,7 @@ import { Plus, RefreshCcw, Save, X, AlertCircle, Search, ChevronUp, ChevronDown,
8
8
  import Select from '../Select.js';
9
9
  import { DEFAULT_ACTIONS } from './constants.js';
10
10
  import { getRowKey, mergeDefaultActions } from './helpers.js';
11
- import { useClickOutside } from './useClickOutside.js';
11
+ import { useClickOutside } from '../../../hooks/useClickOutside.js';
12
12
  import { PaginationHeader, PaginationFooter } from './Pagination.js';
13
13
  import ColumnMenu from './ColumnMenu.js';
14
14
  import MobileFilters from './MobileFilters.js';
@@ -524,8 +524,10 @@ var Table = /*#__PURE__*/forwardRef(function Table(_ref, ref) {
524
524
  });
525
525
  }
526
526
  }, []);
527
- useClickOutside([tableContainerRef], interactive && Boolean(editingCell), function () {
528
- setEditingCell(null);
527
+ useClickOutside([tableContainerRef], function () {
528
+ return setEditingCell(null);
529
+ }, {
530
+ enabled: interactive && Boolean(editingCell)
529
531
  });
530
532
  var actionButtonRefs = useRef({});
531
533
  // Row components register their kebab buttons through this callback rather
@@ -603,8 +605,12 @@ var Table = /*#__PURE__*/forwardRef(function Table(_ref, ref) {
603
605
  var closeActionMenu = useCallback(function () {
604
606
  return setActiveActionRowKey(null);
605
607
  }, []);
606
- useClickOutside([columnMenuRef, columnToggleBtnRef], showColumnMenu, closeColumnMenu);
607
- useClickOutside([actionMenuRef], activeActionRowKey !== null, closeActionMenu);
608
+ useClickOutside([columnMenuRef, columnToggleBtnRef], closeColumnMenu, {
609
+ enabled: showColumnMenu
610
+ });
611
+ useClickOutside([actionMenuRef], closeActionMenu, {
612
+ enabled: activeActionRowKey !== null
613
+ });
608
614
  // Computed values
609
615
  var activeAnchorEl = useMemo(function () {
610
616
  if (activeActionRowKey === null) return null;
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ import { useLatestRef } from './useLatestRef.js';
4
+
5
+ // Internal (not exported). Calls `onClickOutside` when a press lands outside
6
+ // every element in `refs`.
7
+ /** Events to listen to: `pointerdown` where supported, else mouse + touch. */
8
+ function outsidePressEvents() {
9
+ if (typeof window !== "undefined" && typeof window.PointerEvent === "function") {
10
+ return ["pointerdown"];
11
+ }
12
+ return ["mousedown", "touchstart"];
13
+ }
14
+ /**
15
+ * @param {React.RefObject|React.RefObject[]} refs - one ref or an array of
16
+ * refs; a press inside any of them is "inside". Refs with no node are ignored.
17
+ * @param {(event: Event) => void} onClickOutside - called with the event.
18
+ * @param {{ enabled?: boolean }} [options] - `enabled` (default true) attaches
19
+ * or detaches the listeners.
20
+ */
21
+ function useClickOutside(refs, onClickOutside) {
22
+ var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
23
+ _ref$enabled = _ref.enabled,
24
+ enabled = _ref$enabled === void 0 ? true : _ref$enabled;
25
+ var refsRef = useLatestRef(refs);
26
+ var handlerRef = useLatestRef(onClickOutside);
27
+ useEffect(function () {
28
+ if (!enabled || typeof document === "undefined") return undefined;
29
+ var handler = function handler(event) {
30
+ var _handlerRef$current;
31
+ var current = refsRef.current;
32
+ var list = Array.isArray(current) ? current : [current];
33
+ var inside = list.some(function (ref) {
34
+ var node = ref === null || ref === void 0 ? void 0 : ref.current;
35
+ return Boolean(node && typeof node.contains === "function" && node.contains(event.target));
36
+ });
37
+ if (!inside) (_handlerRef$current = handlerRef.current) === null || _handlerRef$current === void 0 || _handlerRef$current.call(handlerRef, event);
38
+ };
39
+ var events = outsidePressEvents();
40
+ events.forEach(function (type) {
41
+ return document.addEventListener(type, handler, true);
42
+ });
43
+ return function () {
44
+ return events.forEach(function (type) {
45
+ return document.removeEventListener(type, handler, true);
46
+ });
47
+ };
48
+ }, [enabled, refsRef, handlerRef]);
49
+ }
50
+
51
+ export { outsidePressEvents, useClickOutside };
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+
4
+ // Internal (not exported). SSR-safe media-query subscription.
5
+ function getMediaQueryList(query) {
6
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return null;
7
+ return window.matchMedia(query) || null;
8
+ }
9
+ /**
10
+ * Subscribes `onChange` to a MediaQueryList's change events. Uses
11
+ * `addEventListener` where available and falls back to the deprecated
12
+ * `addListener` (Safari < 14). Returns the unsubscribe function.
13
+ */
14
+ function subscribeMediaQuery(query, onChange) {
15
+ var mql = getMediaQueryList(query);
16
+ if (!mql) return function () {};
17
+ if (typeof mql.addEventListener === "function") {
18
+ mql.addEventListener("change", onChange);
19
+ return function () {
20
+ var _mql$removeEventListe;
21
+ return (_mql$removeEventListe = mql.removeEventListener) === null || _mql$removeEventListe === void 0 ? void 0 : _mql$removeEventListe.call(mql, "change", onChange);
22
+ };
23
+ }
24
+ if (typeof mql.addListener === "function") {
25
+ mql.addListener(onChange);
26
+ return function () {
27
+ var _mql$removeListener;
28
+ return (_mql$removeListener = mql.removeListener) === null || _mql$removeListener === void 0 ? void 0 : _mql$removeListener.call(mql, onChange);
29
+ };
30
+ }
31
+ return function () {};
32
+ }
33
+ /**
34
+ * `true` while `query` matches; follows changes live.
35
+ *
36
+ * On the server (and while hydrating) it returns `defaultValue`, so server and
37
+ * client markup agree; the real value is applied right after hydration. Without
38
+ * `window.matchMedia` it always returns `defaultValue`.
39
+ *
40
+ * @param {string} query - a CSS media query, e.g. "(max-width: 639px)".
41
+ * @param {{ defaultValue?: boolean }} [options]
42
+ * @returns {boolean}
43
+ */
44
+ function useMediaQuery(query) {
45
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
46
+ _ref$defaultValue = _ref.defaultValue,
47
+ defaultValue = _ref$defaultValue === void 0 ? false : _ref$defaultValue;
48
+ var subscribe = useCallback(function (onChange) {
49
+ return subscribeMediaQuery(query, onChange);
50
+ }, [query]);
51
+ var getSnapshot = function getSnapshot() {
52
+ var mql = getMediaQueryList(query);
53
+ return mql ? Boolean(mql.matches) : defaultValue;
54
+ };
55
+ var getServerSnapshot = function getServerSnapshot() {
56
+ return defaultValue;
57
+ };
58
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
59
+ }
60
+
61
+ export { subscribeMediaQuery, useMediaQuery };
@@ -0,0 +1,17 @@
1
+ import { useMediaQuery } from './useMediaQuery.js';
2
+
3
+ // Internal (not exported). Shared by the charts and the overlay primitives.
4
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
5
+ /**
6
+ * `true` while the user asks the OS for reduced motion; follows changes live.
7
+ * `false` on the server and while hydrating, so the first render matches.
8
+ *
9
+ * @returns {boolean}
10
+ */
11
+ function useReducedMotion() {
12
+ return useMediaQuery(REDUCED_MOTION_QUERY, {
13
+ defaultValue: false
14
+ });
15
+ }
16
+
17
+ export { REDUCED_MOTION_QUERY, useReducedMotion };
@@ -3,24 +3,91 @@ import { useEffect } from 'react';
3
3
 
4
4
  // Internal (not exported). Locks body scroll while any overlay is open. A
5
5
  // counter lets two overlays (e.g. a Drawer and a Tour) overlap without the
6
- // first one to close unlocking the page under the second.
6
+ // first one to close unlocking the page under the second: only the outermost
7
+ // lock touches the body, and only the last release restores it.
8
+ //
9
+ // While locked it also
10
+ // - pads the body by the scrollbar width, so the page does not shift when the
11
+ // scrollbar disappears;
12
+ // - on iOS (or with `{ fixedBody: true }`) pins the body with
13
+ // `position: fixed; top: -scrollY; width: 100%`, because iOS Safari ignores
14
+ // `overflow: hidden` on the body, and restores the scroll position after.
7
15
  var lock = {
8
16
  count: 0,
9
- previous: ""
17
+ saved: null
10
18
  };
19
+ /** iPhone / iPod / iPad, including iPadOS 13+ which reports itself as a Mac. */
20
+ function isIOS() {
21
+ if (typeof navigator === "undefined") return false;
22
+ var platform = navigator.platform || "";
23
+ if (/iP(hone|od|ad)/.test(platform)) return true;
24
+ return platform === "MacIntel" && Number(navigator.maxTouchPoints) > 1;
25
+ }
26
+ function scrollbarWidth() {
27
+ var docWidth = document.documentElement.clientWidth;
28
+ // jsdom (and detached documents) report 0; treat as "no scrollbar".
29
+ if (!docWidth) return 0;
30
+ return Math.max(0, window.innerWidth - docWidth);
31
+ }
32
+ function acquire(fixedBody) {
33
+ var _document = document,
34
+ body = _document.body;
35
+ var style = body.style;
36
+ var saved = {
37
+ overflow: style.overflow,
38
+ paddingRight: style.paddingRight,
39
+ fixed: fixedBody
40
+ };
41
+ var gap = scrollbarWidth();
42
+ if (gap > 0) {
43
+ var current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;
44
+ style.paddingRight = "".concat(current + gap, "px");
45
+ }
46
+ if (fixedBody) {
47
+ saved.position = style.position;
48
+ saved.top = style.top;
49
+ saved.width = style.width;
50
+ saved.scrollX = window.scrollX || window.pageXOffset || 0;
51
+ saved.scrollY = window.scrollY || window.pageYOffset || 0;
52
+ style.position = "fixed";
53
+ style.top = "-".concat(saved.scrollY, "px");
54
+ style.width = "100%";
55
+ }
56
+ style.overflow = "hidden";
57
+ lock.saved = saved;
58
+ }
59
+ function release() {
60
+ var saved = lock.saved;
61
+ lock.saved = null;
62
+ if (!saved) return;
63
+ var style = document.body.style;
64
+ style.overflow = saved.overflow;
65
+ style.paddingRight = saved.paddingRight;
66
+ if (saved.fixed) {
67
+ style.position = saved.position;
68
+ style.top = saved.top;
69
+ style.width = saved.width;
70
+ if (typeof window.scrollTo === "function") window.scrollTo(saved.scrollX, saved.scrollY);
71
+ }
72
+ }
73
+ /**
74
+ * @param {boolean} active - hold the lock while true.
75
+ * @param {{ fixedBody?: boolean }} [options] - force the iOS-safe
76
+ * `position: fixed` mode; defaults to auto-detecting iOS. Only the outermost
77
+ * lock's mode applies.
78
+ */
11
79
  function useScrollLock(active) {
80
+ var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
81
+ fixedBody = _ref.fixedBody;
12
82
  useEffect(function () {
13
83
  if (!active || typeof document === "undefined") return undefined;
14
- if (lock.count === 0) {
15
- lock.previous = document.body.style.overflow;
16
- document.body.style.overflow = "hidden";
17
- }
84
+ if (lock.count === 0) acquire(fixedBody === undefined ? isIOS() : Boolean(fixedBody));
18
85
  lock.count += 1;
19
86
  return function () {
20
87
  lock.count -= 1;
21
- if (lock.count === 0) document.body.style.overflow = lock.previous;
88
+ if (lock.count === 0) release();
22
89
  };
23
- }, [active]);
90
+ }, [active, fixedBody]);
24
91
  }
25
92
 
26
- export { useScrollLock };
93
+ export { isIOS, useScrollLock };