@kennycorrea/ui 0.11.3 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +189 -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/Card/Card.d.ts +5 -3
  16. package/dist/atoms/Card/Card.js +21 -4
  17. package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
  18. package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
  19. package/dist/atoms/Drawer/Drawer.js +1 -1
  20. package/dist/atoms/Icon/Icon.d.ts +7 -9
  21. package/dist/atoms/Icon/Icon.js +23 -8
  22. package/dist/atoms/Pill/Pill.d.ts +17 -0
  23. package/dist/atoms/Pill/Pill.js +12 -0
  24. package/dist/atoms/Pill/index.d.ts +1 -0
  25. package/dist/atoms/Pill/index.js +1 -0
  26. package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +9 -2
  27. package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +6 -15
  28. package/dist/atoms/Sheet/Sheet.js +1 -1
  29. package/dist/atoms/StatusIndicator/StatusIndicator.d.ts +14 -0
  30. package/dist/atoms/StatusIndicator/StatusIndicator.js +8 -0
  31. package/dist/atoms/StatusIndicator/index.d.ts +1 -0
  32. package/dist/atoms/StatusIndicator/index.js +1 -0
  33. package/dist/atoms/TextLink/TextLink.d.ts +36 -0
  34. package/dist/atoms/TextLink/TextLink.js +17 -0
  35. package/dist/atoms/TextLink/index.d.ts +1 -0
  36. package/dist/atoms/TextLink/index.js +1 -0
  37. package/dist/atoms/Toast/Toast.d.ts +1 -1
  38. package/dist/atoms/Toast/Toast.js +1 -1
  39. package/dist/atoms/index.d.ts +4 -0
  40. package/dist/atoms/index.js +4 -0
  41. package/dist/hooks/breakpoints.d.ts +20 -0
  42. package/dist/hooks/breakpoints.js +13 -0
  43. package/dist/hooks/index.d.ts +4 -0
  44. package/dist/hooks/index.js +4 -0
  45. package/dist/hooks/useControllableState.d.ts +17 -0
  46. package/dist/hooks/useControllableState.js +22 -0
  47. package/dist/hooks/useElementWidth.d.ts +7 -0
  48. package/dist/hooks/useElementWidth.js +22 -0
  49. package/dist/hooks/useMediaQuery.d.ts +18 -0
  50. package/dist/hooks/useMediaQuery.js +51 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.js +4 -0
  53. package/dist/molecules/AccentPicker/AccentPicker.d.ts +30 -0
  54. package/dist/molecules/AccentPicker/AccentPicker.js +27 -0
  55. package/dist/molecules/AccentPicker/index.d.ts +1 -0
  56. package/dist/molecules/AccentPicker/index.js +1 -0
  57. package/dist/molecules/Breadcrumbs/Breadcrumbs.d.ts +27 -0
  58. package/dist/molecules/Breadcrumbs/Breadcrumbs.js +40 -0
  59. package/dist/molecules/Breadcrumbs/index.d.ts +1 -0
  60. package/dist/molecules/Breadcrumbs/index.js +1 -0
  61. package/dist/molecules/Calendar/Calendar.d.ts +42 -0
  62. package/dist/molecules/Calendar/Calendar.js +111 -0
  63. package/dist/molecules/Calendar/index.d.ts +1 -0
  64. package/dist/molecules/Calendar/index.js +1 -0
  65. package/dist/molecules/DateInput/DateInput.d.ts +35 -0
  66. package/dist/molecules/DateInput/DateInput.js +41 -0
  67. package/dist/molecules/DateInput/index.d.ts +1 -0
  68. package/dist/molecules/DateInput/index.js +1 -0
  69. package/dist/molecules/EmptyState/EmptyState.d.ts +19 -0
  70. package/dist/molecules/EmptyState/EmptyState.js +7 -0
  71. package/dist/molecules/EmptyState/index.d.ts +1 -0
  72. package/dist/molecules/EmptyState/index.js +1 -0
  73. package/dist/molecules/ErrorState/ErrorState.d.ts +18 -0
  74. package/dist/molecules/ErrorState/ErrorState.js +12 -0
  75. package/dist/molecules/ErrorState/index.d.ts +1 -0
  76. package/dist/molecules/ErrorState/index.js +1 -0
  77. package/dist/molecules/ErrorSummary/ErrorSummary.d.ts +25 -0
  78. package/dist/molecules/ErrorSummary/ErrorSummary.js +32 -0
  79. package/dist/molecules/ErrorSummary/index.d.ts +1 -0
  80. package/dist/molecules/ErrorSummary/index.js +1 -0
  81. package/dist/molecules/FilterChips/FilterChips.d.ts +10 -0
  82. package/dist/molecules/FilterChips/FilterChips.js +13 -0
  83. package/dist/molecules/FilterChips/index.d.ts +1 -0
  84. package/dist/molecules/FilterChips/index.js +1 -0
  85. package/dist/molecules/FilterPopover/FilterPopover.d.ts +27 -0
  86. package/dist/molecules/FilterPopover/FilterPopover.js +32 -0
  87. package/dist/molecules/FilterPopover/index.d.ts +1 -0
  88. package/dist/molecules/FilterPopover/index.js +1 -0
  89. package/dist/molecules/FormActions/FormActions.d.ts +16 -0
  90. package/dist/molecules/FormActions/FormActions.js +5 -0
  91. package/dist/molecules/FormActions/index.d.ts +1 -0
  92. package/dist/molecules/FormActions/index.js +1 -0
  93. package/dist/molecules/FormField/FormField.d.ts +52 -0
  94. package/dist/molecules/FormField/FormField.js +63 -0
  95. package/dist/molecules/FormField/index.d.ts +1 -0
  96. package/dist/molecules/FormField/index.js +1 -0
  97. package/dist/molecules/FormSection/FormSection.d.ts +14 -0
  98. package/dist/molecules/FormSection/FormSection.js +10 -0
  99. package/dist/molecules/FormSection/index.d.ts +1 -0
  100. package/dist/molecules/FormSection/index.js +1 -0
  101. package/dist/molecules/KeyValueList/KeyValueList.d.ts +22 -0
  102. package/dist/molecules/KeyValueList/KeyValueList.js +9 -0
  103. package/dist/molecules/KeyValueList/index.d.ts +1 -0
  104. package/dist/molecules/KeyValueList/index.js +1 -0
  105. package/dist/molecules/LoadingState/LoadingState.d.ts +20 -0
  106. package/dist/molecules/LoadingState/LoadingState.js +22 -0
  107. package/dist/molecules/LoadingState/index.d.ts +1 -0
  108. package/dist/molecules/LoadingState/index.js +1 -0
  109. package/dist/molecules/NavItem/NavItem.d.ts +31 -0
  110. package/dist/molecules/NavItem/NavItem.js +72 -0
  111. package/dist/molecules/NavItem/context.d.ts +14 -0
  112. package/dist/molecules/NavItem/context.js +5 -0
  113. package/dist/molecules/NavItem/index.d.ts +1 -0
  114. package/dist/molecules/NavItem/index.js +1 -0
  115. package/dist/molecules/Pagination/Pagination.d.ts +32 -0
  116. package/dist/molecules/Pagination/Pagination.js +57 -0
  117. package/dist/molecules/Pagination/index.d.ts +1 -0
  118. package/dist/molecules/Pagination/index.js +1 -0
  119. package/dist/molecules/PasswordInput/PasswordInput.d.ts +14 -0
  120. package/dist/molecules/PasswordInput/PasswordInput.js +16 -0
  121. package/dist/molecules/PasswordInput/index.d.ts +1 -0
  122. package/dist/molecules/PasswordInput/index.js +1 -0
  123. package/dist/molecules/SearchField/SearchField.d.ts +24 -0
  124. package/dist/molecules/SearchField/SearchField.js +58 -0
  125. package/dist/molecules/SearchField/index.d.ts +1 -0
  126. package/dist/molecules/SearchField/index.js +1 -0
  127. package/dist/molecules/StatGroup/StatGroup.d.ts +21 -0
  128. package/dist/molecules/StatGroup/StatGroup.js +10 -0
  129. package/dist/molecules/StatGroup/index.d.ts +1 -0
  130. package/dist/molecules/StatGroup/index.js +1 -0
  131. package/dist/molecules/ThemeToggle/ThemeToggle.d.ts +23 -0
  132. package/dist/molecules/ThemeToggle/ThemeToggle.js +26 -0
  133. package/dist/molecules/ThemeToggle/index.d.ts +1 -0
  134. package/dist/molecules/ThemeToggle/index.js +1 -0
  135. package/dist/molecules/UserMenu/UserMenu.d.ts +20 -0
  136. package/dist/molecules/UserMenu/UserMenu.js +14 -0
  137. package/dist/molecules/UserMenu/index.d.ts +1 -0
  138. package/dist/molecules/UserMenu/index.js +1 -0
  139. package/dist/molecules/index.d.ts +21 -0
  140. package/dist/molecules/index.js +21 -0
  141. package/dist/organisms/AppShell/AppShell.d.ts +29 -0
  142. package/dist/organisms/AppShell/AppShell.js +34 -0
  143. package/dist/organisms/AppShell/context.d.ts +14 -0
  144. package/dist/organisms/AppShell/context.js +6 -0
  145. package/dist/organisms/AppShell/index.d.ts +1 -0
  146. package/dist/organisms/AppShell/index.js +1 -0
  147. package/dist/organisms/AuthForms/AuthForms.d.ts +83 -0
  148. package/dist/organisms/AuthForms/AuthForms.js +57 -0
  149. package/dist/organisms/AuthForms/index.d.ts +2 -0
  150. package/dist/organisms/AuthForms/index.js +1 -0
  151. package/dist/organisms/AuthForms/shared.d.ts +36 -0
  152. package/dist/organisms/AuthForms/shared.js +36 -0
  153. package/dist/organisms/BulkActionBar/BulkActionBar.d.ts +25 -0
  154. package/dist/organisms/BulkActionBar/BulkActionBar.js +18 -0
  155. package/dist/organisms/BulkActionBar/index.d.ts +1 -0
  156. package/dist/organisms/BulkActionBar/index.js +1 -0
  157. package/dist/organisms/Chart/CategoryCharts.d.ts +34 -0
  158. package/dist/organisms/Chart/CategoryCharts.js +93 -0
  159. package/dist/organisms/Chart/ChartFrame.d.ts +26 -0
  160. package/dist/organisms/Chart/ChartFrame.js +18 -0
  161. package/dist/organisms/Chart/DonutChart.d.ts +25 -0
  162. package/dist/organisms/Chart/DonutChart.js +53 -0
  163. package/dist/organisms/Chart/Sparkline.d.ts +15 -0
  164. package/dist/organisms/Chart/Sparkline.js +18 -0
  165. package/dist/organisms/Chart/index.d.ts +5 -0
  166. package/dist/organisms/Chart/index.js +5 -0
  167. package/dist/organisms/Chart/shared.d.ts +51 -0
  168. package/dist/organisms/Chart/shared.js +113 -0
  169. package/dist/organisms/DataTable/DataTable.d.ts +63 -0
  170. package/dist/organisms/DataTable/DataTable.js +119 -0
  171. package/dist/organisms/DataTable/index.d.ts +3 -0
  172. package/dist/organisms/DataTable/index.js +3 -0
  173. package/dist/organisms/DataTable/types.d.ts +39 -0
  174. package/dist/organisms/DataTable/types.js +1 -0
  175. package/dist/organisms/DataTable/useClientTable.d.ts +27 -0
  176. package/dist/organisms/DataTable/useClientTable.js +51 -0
  177. package/dist/organisms/DateRangePicker/DateRangePicker.d.ts +43 -0
  178. package/dist/organisms/DateRangePicker/DateRangePicker.js +54 -0
  179. package/dist/organisms/DateRangePicker/index.d.ts +1 -0
  180. package/dist/organisms/DateRangePicker/index.js +1 -0
  181. package/dist/organisms/FilterBar/FilterBar.d.ts +28 -0
  182. package/dist/organisms/FilterBar/FilterBar.js +16 -0
  183. package/dist/organisms/FilterBar/index.d.ts +1 -0
  184. package/dist/organisms/FilterBar/index.js +1 -0
  185. package/dist/organisms/PageHeader/PageHeader.d.ts +31 -0
  186. package/dist/organisms/PageHeader/PageHeader.js +19 -0
  187. package/dist/organisms/PageHeader/index.d.ts +1 -0
  188. package/dist/organisms/PageHeader/index.js +1 -0
  189. package/dist/organisms/PageTabs/PageTabs.d.ts +27 -0
  190. package/dist/organisms/PageTabs/PageTabs.js +31 -0
  191. package/dist/organisms/PageTabs/index.d.ts +1 -0
  192. package/dist/organisms/PageTabs/index.js +1 -0
  193. package/dist/organisms/Sidebar/Sidebar.d.ts +48 -0
  194. package/dist/organisms/Sidebar/Sidebar.js +61 -0
  195. package/dist/organisms/Sidebar/index.d.ts +1 -0
  196. package/dist/organisms/Sidebar/index.js +1 -0
  197. package/dist/organisms/Topbar/Topbar.d.ts +24 -0
  198. package/dist/organisms/Topbar/Topbar.js +18 -0
  199. package/dist/organisms/Topbar/index.d.ts +1 -0
  200. package/dist/organisms/Topbar/index.js +1 -0
  201. package/dist/organisms/index.d.ts +11 -0
  202. package/dist/organisms/index.js +11 -0
  203. package/dist/strings.d.ts +108 -15
  204. package/dist/strings.js +73 -7
  205. package/dist/templates/AdminLayout/AdminLayout.d.ts +14 -0
  206. package/dist/templates/AdminLayout/AdminLayout.js +9 -0
  207. package/dist/templates/AdminLayout/index.d.ts +1 -0
  208. package/dist/templates/AdminLayout/index.js +1 -0
  209. package/dist/templates/AuthLayout/AuthLayout.d.ts +28 -0
  210. package/dist/templates/AuthLayout/AuthLayout.js +11 -0
  211. package/dist/templates/AuthLayout/index.d.ts +1 -0
  212. package/dist/templates/AuthLayout/index.js +1 -0
  213. package/dist/templates/DashboardPage/DashboardPage.d.ts +25 -0
  214. package/dist/templates/DashboardPage/DashboardPage.js +12 -0
  215. package/dist/templates/DashboardPage/index.d.ts +1 -0
  216. package/dist/templates/DashboardPage/index.js +1 -0
  217. package/dist/templates/DetailPage/DetailPage.d.ts +20 -0
  218. package/dist/templates/DetailPage/DetailPage.js +11 -0
  219. package/dist/templates/DetailPage/index.d.ts +1 -0
  220. package/dist/templates/DetailPage/index.js +1 -0
  221. package/dist/templates/FormPage/FormPage.d.ts +26 -0
  222. package/dist/templates/FormPage/FormPage.js +11 -0
  223. package/dist/templates/FormPage/index.d.ts +1 -0
  224. package/dist/templates/FormPage/index.js +1 -0
  225. package/dist/templates/ListPage/ListPage.d.ts +17 -0
  226. package/dist/templates/ListPage/ListPage.js +8 -0
  227. package/dist/templates/ListPage/index.d.ts +1 -0
  228. package/dist/templates/ListPage/index.js +1 -0
  229. package/dist/templates/SettingsPage/SettingsPage.d.ts +32 -0
  230. package/dist/templates/SettingsPage/SettingsPage.js +13 -0
  231. package/dist/templates/SettingsPage/index.d.ts +1 -0
  232. package/dist/templates/SettingsPage/index.js +1 -0
  233. package/dist/templates/StatusPage/StatusPage.d.ts +18 -0
  234. package/dist/templates/StatusPage/StatusPage.js +8 -0
  235. package/dist/templates/StatusPage/index.d.ts +1 -0
  236. package/dist/templates/StatusPage/index.js +1 -0
  237. package/dist/templates/index.d.ts +8 -0
  238. package/dist/templates/index.js +8 -0
  239. package/dist/utils/dates.d.ts +67 -0
  240. package/dist/utils/dates.js +154 -0
  241. package/dist/utils/index.d.ts +1 -0
  242. package/dist/utils/index.js +1 -0
  243. package/dist/utils/theme.d.ts +4 -0
  244. package/dist/utils/theme.js +7 -3
  245. package/package.json +4 -1
  246. package/tokens.css +513 -21
@@ -0,0 +1 @@
1
+ export * from './AuthForms.js';
@@ -0,0 +1,36 @@
1
+ import { type FormEvent, type ReactNode } from 'react';
2
+ /** What every auth form takes besides its own labels. */
3
+ export interface AuthFormBaseProps<Field extends string> {
4
+ /**
5
+ * Field messages from your validation or your server, by field. After a submit, focus moves
6
+ * to the first field that has one.
7
+ */
8
+ errors?: Partial<Record<Field, string>>;
9
+ /** A message about the whole attempt ("Those details did not match"). Announced as an alert. */
10
+ formError?: string;
11
+ /** Disables the submit button and shows its spinner while a request is in flight. */
12
+ pending?: boolean;
13
+ /** Under the form: "No account? Sign up", legal links. */
14
+ footer?: ReactNode;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * The ids for a form's fields, and focus on the first invalid one after each submit.
19
+ *
20
+ * Focus moves only in answer to a submit, never when `errors` changes for another reason, so a
21
+ * field validated as the person types does not pull the cursor away from where they are.
22
+ */
23
+ export declare function useAuthForm<Field extends string>(fields: readonly Field[], errors: Partial<Record<Field, string>> | undefined): {
24
+ ids: Record<Field, string>;
25
+ markSubmitted: () => void;
26
+ };
27
+ export declare function AuthFormShell({ label, onSubmit, formError, submitLabel, pending, footer, className, children, }: {
28
+ label: string;
29
+ onSubmit: (e: FormEvent<HTMLFormElement>) => void;
30
+ formError?: string;
31
+ submitLabel: string;
32
+ pending?: boolean;
33
+ footer?: ReactNode;
34
+ className?: string;
35
+ children: ReactNode;
36
+ }): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef } from 'react';
3
+ import { Alert } from '../../atoms/Alert/Alert.js';
4
+ import { Button } from '../../atoms/Button/Button.js';
5
+ /**
6
+ * The ids for a form's fields, and focus on the first invalid one after each submit.
7
+ *
8
+ * Focus moves only in answer to a submit, never when `errors` changes for another reason, so a
9
+ * field validated as the person types does not pull the cursor away from where they are.
10
+ */
11
+ export function useAuthForm(fields, errors) {
12
+ const base = useId();
13
+ const ids = Object.fromEntries(fields.map((f) => [f, `${base}-${f}`]));
14
+ const submitted = useRef(false);
15
+ // biome-ignore lint/correctness/useExhaustiveDependencies: ids and fields are stable for the form's lifetime.
16
+ useEffect(() => {
17
+ if (!submitted.current || !errors)
18
+ return;
19
+ const first = fields.find((f) => errors[f]);
20
+ if (!first)
21
+ return;
22
+ submitted.current = false;
23
+ document.getElementById(ids[first])?.focus();
24
+ }, [errors]);
25
+ const markSubmitted = () => {
26
+ submitted.current = true;
27
+ };
28
+ return { ids, markSubmitted };
29
+ }
30
+ export function AuthFormShell({ label, onSubmit, formError, submitLabel, pending, footer, className, children, }) {
31
+ return (_jsxs("form", { noValidate: true, "aria-label": label, className: ['ui-auth-form', className].filter(Boolean).join(' '), onSubmit: (e) => {
32
+ e.preventDefault();
33
+ if (!pending)
34
+ onSubmit(e);
35
+ }, children: [formError ? _jsx(Alert, { type: "error", body: formError }) : null, children, _jsx(Button, { type: "submit", size: "touch", fullWidth: true, loading: pending, children: submitLabel }), footer ? _jsx("div", { className: "ui-auth-form__footer", children: footer }) : null] }));
36
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface BulkAction {
3
+ id: string;
4
+ label: string;
5
+ icon?: ReactNode;
6
+ /** Red, for the one that deletes or cancels. */
7
+ destructive?: boolean;
8
+ disabled?: boolean;
9
+ }
10
+ export interface BulkActionBarProps {
11
+ selectedCount: number;
12
+ actions: BulkAction[];
13
+ onAction: (id: string) => void;
14
+ onClear: () => void;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * What to do with the selected rows: a floating toolbar that appears while anything is
19
+ * selected and sticks to the bottom of the content, above the TabBar on small screens.
20
+ *
21
+ * A Radix `Toolbar`, so its buttons are one Tab stop with arrow keys between them. The count
22
+ * sits in a polite live region that stays in the page while the bar is hidden, so a screen
23
+ * reader hears the selection change.
24
+ */
25
+ export declare function BulkActionBar({ selectedCount, actions, onAction, onClear, className, }: BulkActionBarProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Cross2Icon } from '@radix-ui/react-icons';
3
+ import * as Toolbar from '@radix-ui/react-toolbar';
4
+ import { Button as ThemedButton } from '@radix-ui/themes';
5
+ import { useUiStrings } from '../../strings.js';
6
+ /**
7
+ * What to do with the selected rows: a floating toolbar that appears while anything is
8
+ * selected and sticks to the bottom of the content, above the TabBar on small screens.
9
+ *
10
+ * A Radix `Toolbar`, so its buttons are one Tab stop with arrow keys between them. The count
11
+ * sits in a polite live region that stays in the page while the bar is hidden, so a screen
12
+ * reader hears the selection change.
13
+ */
14
+ export function BulkActionBar({ selectedCount, actions, onAction, onClear, className, }) {
15
+ const strings = useUiStrings();
16
+ const count = strings.selectedCount(selectedCount);
17
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "ui-sr-only", "aria-live": "polite", children: selectedCount > 0 ? count : '' }), selectedCount > 0 ? (_jsxs(Toolbar.Root, { "aria-label": strings.bulkActions, className: ['ui-bulk-bar', className].filter(Boolean).join(' '), children: [_jsx("span", { className: "ui-bulk-bar__count", "aria-hidden": "true", children: count }), actions.map((action) => (_jsx(Toolbar.Button, { asChild: true, disabled: action.disabled, children: _jsxs(ThemedButton, { size: "2", variant: action.destructive ? 'solid' : 'soft', color: action.destructive ? 'red' : undefined, onClick: () => onAction(action.id), children: [action.icon, action.label] }) }, action.id))), _jsx(Toolbar.Button, { asChild: true, children: _jsxs(ThemedButton, { size: "2", variant: "ghost", color: "gray", onClick: onClear, children: [_jsx(Cross2Icon, { "aria-hidden": "true" }), strings.clearSelection] }) })] })) : null] }));
18
+ }
@@ -0,0 +1 @@
1
+ export * from './BulkActionBar.js';
@@ -0,0 +1 @@
1
+ export * from './BulkActionBar.js';
@@ -0,0 +1,34 @@
1
+ import { type ChartSeries } from './shared.js';
2
+ export interface CategoryChartProps {
3
+ /** The x-axis labels, one per value. */
4
+ categories: string[];
5
+ series: ChartSeries[];
6
+ /**
7
+ * What the chart shows, in a sentence: its accessible name ("Revenue by month, January to
8
+ * June, peaking in April"). The chart itself is one image to a screen reader; ChartFrame's
9
+ * table view carries the numbers.
10
+ */
11
+ label: string;
12
+ /** Plot height in pixels. Default 240. The width follows the container. */
13
+ height?: number;
14
+ /** Formats axis ticks and values. Default: `Intl.NumberFormat` in the provider locale. */
15
+ formatValue?: (value: number) => string;
16
+ /** Shows the legend under the chart. Default: when there is more than one series. */
17
+ legend?: boolean;
18
+ className?: string;
19
+ }
20
+ export interface BarChartProps extends CategoryChartProps {
21
+ /** Series stack on top of each other instead of standing side by side. */
22
+ stacked?: boolean;
23
+ }
24
+ /**
25
+ * Vertical bars per category, grouped or stacked, in plain SVG. Every series after the first
26
+ * also carries a pattern, and the legend repeats it, so series can be told apart without colour.
27
+ */
28
+ export declare function BarChart({ categories, series, label, height, formatValue, legend, stacked, className, }: BarChartProps): import("react").JSX.Element;
29
+ /**
30
+ * Lines per series over the categories, in plain SVG. Each series has its own point marker
31
+ * (circle, square, triangle…), repeated in the legend, so lines can be told apart without
32
+ * colour. `null` values break the line.
33
+ */
34
+ export declare function LineChart({ categories, series, label, height, formatValue, legend, className, }: CategoryChartProps): import("react").JSX.Element;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef } from 'react';
3
+ import { useElementWidth } from '../../hooks/useElementWidth.js';
4
+ import { useUiLocale } from '../../strings.js';
5
+ import { ChartLegend, chartColor, niceTicks, SeriesMarker, SeriesPattern, } from './shared.js';
6
+ const M = { top: 8, right: 8, bottom: 28, left: 48 };
7
+ const FALLBACK_WIDTH = 600;
8
+ function useFrame(categories, series, height, stackedTotals) {
9
+ const ref = useRef(null);
10
+ const width = useElementWidth(ref) || FALLBACK_WIDTH;
11
+ const values = stackedTotals ?? series.flatMap((s) => s.values.filter((v) => v !== null));
12
+ const ticks = niceTicks(Math.min(0, ...values), Math.max(0, ...values), Math.max(3, Math.round(height / 60)));
13
+ const lo = ticks[0];
14
+ const hi = ticks[ticks.length - 1];
15
+ const plotW = Math.max(1, width - M.left - M.right);
16
+ const plotH = Math.max(1, height - M.top - M.bottom);
17
+ const y = (v) => M.top + plotH - ((v - lo) / (hi - lo || 1)) * plotH;
18
+ const band = plotW / Math.max(1, categories.length);
19
+ // Thin out x labels so they never overlap: roughly 7px per character plus a gap.
20
+ const longest = Math.max(1, ...categories.map((c) => c.length));
21
+ const every = Math.max(1, Math.ceil((longest * 7 + 8) / band));
22
+ return { ref, width, height, ticks, y, band, plotW, plotH, every };
23
+ }
24
+ function Axes({ frame, categories, format, }) {
25
+ return (_jsxs("g", { children: [frame.ticks.map((t) => (_jsxs("g", { children: [_jsx("line", { x1: M.left, x2: M.left + frame.plotW, y1: frame.y(t), y2: frame.y(t), className: t === 0 ? 'ui-chart__axis' : 'ui-chart__grid' }), _jsx("text", { x: M.left - 6, y: frame.y(t), dy: "0.32em", textAnchor: "end", className: "ui-chart__tick", children: format(t) })] }, t))), categories.map((c, i) => i % frame.every === 0 ? (_jsx("text", { x: M.left + frame.band * (i + 0.5), y: frame.height - 8, textAnchor: "middle", className: "ui-chart__tick", children: c }, c)) : null)] }));
26
+ }
27
+ function useFormat(formatValue) {
28
+ const locale = useUiLocale();
29
+ if (formatValue)
30
+ return formatValue;
31
+ const nf = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
32
+ return (n) => nf.format(n);
33
+ }
34
+ /**
35
+ * Vertical bars per category, grouped or stacked, in plain SVG. Every series after the first
36
+ * also carries a pattern, and the legend repeats it, so series can be told apart without colour.
37
+ */
38
+ export function BarChart({ categories, series, label, height = 240, formatValue, legend, stacked = false, className, }) {
39
+ const id = useId().replace(/[^a-zA-Z0-9-]/g, '');
40
+ const format = useFormat(formatValue);
41
+ const totals = stacked
42
+ ? categories.map((_, i) => series.reduce((sum, s) => sum + Math.max(0, s.values[i] ?? 0), 0))
43
+ : undefined;
44
+ const frame = useFrame(categories, series, height, totals);
45
+ const groupW = frame.band * 0.72;
46
+ const barW = stacked ? groupW : groupW / Math.max(1, series.length);
47
+ const zero = frame.y(Math.max(0, frame.ticks[0]));
48
+ return (_jsxs("div", { ref: frame.ref, className: ['ui-chart', className].filter(Boolean).join(' '), children: [_jsxs("svg", { role: "img", "aria-label": label, width: "100%", height: height, viewBox: `0 0 ${frame.width} ${height}`, children: [_jsx("defs", { children: series.map((s, si) => (_jsx(SeriesPattern, { id: `${id}-p${si}`, index: si }, s.id))) }), _jsx(Axes, { frame: frame, categories: categories, format: format }), _jsx("g", { children: categories.map((c, ci) => {
49
+ const x0 = M.left + frame.band * ci + (frame.band - groupW) / 2;
50
+ let stackTop = zero;
51
+ return (_jsx("g", { children: series.map((s, si) => {
52
+ const v = s.values[ci];
53
+ if (v === null || v === undefined)
54
+ return null;
55
+ let top;
56
+ let bottom;
57
+ if (stacked) {
58
+ const h = zero - frame.y(Math.max(0, v));
59
+ bottom = stackTop;
60
+ top = stackTop - h;
61
+ stackTop = top;
62
+ }
63
+ else {
64
+ top = Math.min(frame.y(v), zero);
65
+ bottom = Math.max(frame.y(v), zero);
66
+ }
67
+ const x = stacked ? x0 : x0 + barW * si;
68
+ return (_jsx("rect", { x: x, y: top, width: Math.max(1, barW - (stacked ? 0 : 2)), height: Math.max(0, bottom - top), rx: 2, fill: `url(#${id}-p${si})`, children: _jsx("title", { children: `${s.label}, ${c}: ${format(v)}` }) }, s.id));
69
+ }) }, c));
70
+ }) })] }), (legend ?? series.length > 1) ? (_jsx(ChartLegend, { kind: "fill", idPrefix: id, entries: series.map((s) => ({ id: s.id, label: s.label })) })) : null] }));
71
+ }
72
+ /**
73
+ * Lines per series over the categories, in plain SVG. Each series has its own point marker
74
+ * (circle, square, triangle…), repeated in the legend, so lines can be told apart without
75
+ * colour. `null` values break the line.
76
+ */
77
+ export function LineChart({ categories, series, label, height = 240, formatValue, legend, className, }) {
78
+ const format = useFormat(formatValue);
79
+ const frame = useFrame(categories, series, height);
80
+ const x = (i) => M.left + frame.band * (i + 0.5);
81
+ return (_jsxs("div", { ref: frame.ref, className: ['ui-chart', className].filter(Boolean).join(' '), children: [_jsxs("svg", { role: "img", "aria-label": label, width: "100%", height: height, viewBox: `0 0 ${frame.width} ${height}`, children: [_jsx(Axes, { frame: frame, categories: categories, format: format }), _jsx("g", { children: series.map((s, si) => {
82
+ const segments = [[]];
83
+ s.values.forEach((v, i) => {
84
+ if (v === null)
85
+ segments.push([]);
86
+ else
87
+ segments[segments.length - 1].push(`${x(i)},${frame.y(v)}`);
88
+ });
89
+ return (_jsxs("g", { children: [segments
90
+ .filter((pts) => pts.length > 1)
91
+ .map((pts) => (_jsx("polyline", { points: pts.join(' '), fill: "none", stroke: chartColor(si), strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round" }, pts[0]))), s.values.map((v, i) => v === null ? null : (_jsxs("g", { children: [_jsx("title", { children: `${s.label}, ${categories[i]}: ${format(v)}` }), _jsx(SeriesMarker, { index: si, x: x(i), y: frame.y(v) })] }, categories[i])))] }, s.id));
92
+ }) })] }), (legend ?? series.length > 1) ? (_jsx(ChartLegend, { kind: "line", idPrefix: "line", entries: series.map((s) => ({ id: s.id, label: s.label })) })) : null] }));
93
+ }
@@ -0,0 +1,26 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type CardVariant, type Responsive } from '../../atoms/Card/Card.js';
3
+ import type { ChartTableData } from './shared.js';
4
+ export interface ChartFrameProps {
5
+ title: ReactNode;
6
+ description?: ReactNode;
7
+ /** The chart: one of this library's, or any other library's SVG or canvas. */
8
+ children: ReactNode;
9
+ /**
10
+ * The same numbers as a table (see `chartTable` and `donutTable`). With it, a "View as table"
11
+ * toggle is shown, so the data is never only in the picture.
12
+ */
13
+ table?: ChartTableData;
14
+ /** Beside the toggle: a period select, an export button. */
15
+ actions?: ReactNode;
16
+ /** The title's level. Default `h2`; `h3` inside a section that already has an `h2`. */
17
+ headingLevel?: 2 | 3;
18
+ variant?: Responsive<CardVariant>;
19
+ className?: string;
20
+ }
21
+ /**
22
+ * The card a chart sits in: a heading, an optional description, the chart, and a switch to
23
+ * the same data as a table. The frame works with any chart, including another library's, so
24
+ * an app can bring its own and keep the heading, the table and the layout.
25
+ */
26
+ export declare function ChartFrame({ title, description, children, table, actions, headingLevel, variant, className, }: ChartFrameProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // biome-ignore-all lint/suspicious/noArrayIndexKey: table columns are positional and header text can repeat or be empty.
3
+ import { useId, useState } from 'react';
4
+ import { Button } from '../../atoms/Button/Button.js';
5
+ import { Card } from '../../atoms/Card/Card.js';
6
+ import { useUiStrings } from '../../strings.js';
7
+ /**
8
+ * The card a chart sits in: a heading, an optional description, the chart, and a switch to
9
+ * the same data as a table. The frame works with any chart, including another library's, so
10
+ * an app can bring its own and keep the heading, the table and the layout.
11
+ */
12
+ export function ChartFrame({ title, description, children, table, actions, headingLevel = 2, variant, className, }) {
13
+ const strings = useUiStrings();
14
+ const [asTable, setAsTable] = useState(false);
15
+ const titleId = useId();
16
+ const Heading = headingLevel === 3 ? 'h3' : 'h2';
17
+ return (_jsx(Card, { variant: variant, className: ['ui-chart-frame', className].filter(Boolean).join(' '), children: _jsxs("section", { "aria-labelledby": titleId, children: [_jsxs("div", { className: "ui-chart-frame__header", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Heading, { id: titleId, className: "ui-chart-frame__title", children: title }), description ? _jsx("p", { className: "ui-chart-frame__description", children: description }) : null] }), _jsxs("div", { className: "ui-chart-frame__actions", children: [actions, table ? (_jsx(Button, { variant: "ghost", size: "sm", "aria-pressed": asTable, onClick: () => setAsTable((v) => !v), children: asTable ? strings.viewAsChart : strings.viewAsTable })) : null] })] }), _jsx("div", { className: "ui-chart-frame__body", children: asTable && table ? (_jsx("div", { className: "ui-chart-frame__table", children: _jsxs("table", { className: "ui-table__table", children: [_jsx("caption", { className: "ui-sr-only", children: title }), _jsx("thead", { children: _jsx("tr", { children: table.headers.map((h, i) => (_jsx("th", { scope: "col", "data-align": i > 0 ? 'end' : undefined, children: h }, `${i}-${h}`))) }) }), _jsx("tbody", { children: table.rows.map((row) => (_jsx("tr", { children: row.map((cell, i) => i === 0 ? (_jsx("th", { scope: "row", children: cell }, "h")) : (_jsx("td", { "data-align": "end", children: cell }, `${i}-${table.headers[i]}`))) }, String(row[0])))) })] }) })) : (children) })] }) }));
18
+ }
@@ -0,0 +1,25 @@
1
+ import { type ChartTableData } from './shared.js';
2
+ export interface DonutSlice {
3
+ id: string;
4
+ label: string;
5
+ value: number;
6
+ }
7
+ export interface DonutChartProps {
8
+ data: DonutSlice[];
9
+ /** The accessible name: what the parts are of ("Orders by status, mostly paid"). */
10
+ label: string;
11
+ /** Under the total in the middle, e.g. "orders". */
12
+ centerLabel?: string;
13
+ /** Diameter in pixels. Default 180. */
14
+ size?: number;
15
+ formatValue?: (value: number) => string;
16
+ className?: string;
17
+ }
18
+ /** The slices as a table: label, value and share. */
19
+ export declare function donutTable(data: DonutSlice[], format?: (n: number) => string, headers?: [string, string, string]): ChartTableData;
20
+ /**
21
+ * Parts of a whole, with the total in the middle. The legend lists every slice with its value
22
+ * and share, so the numbers are readable without matching colours to wedges, and each slice
23
+ * carries the same pattern as its legend swatch.
24
+ */
25
+ export declare function DonutChart({ data, label, centerLabel, size, formatValue, className, }: DonutChartProps): import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { useUiLocale } from '../../strings.js';
4
+ import { ChartLegend, SeriesPattern } from './shared.js';
5
+ function arc(cx, cy, r, r0, a0, a1) {
6
+ const p = (rad, a) => [cx + rad * Math.sin(a), cy - rad * Math.cos(a)];
7
+ const large = a1 - a0 > Math.PI ? 1 : 0;
8
+ const [x0, y0] = p(r, a0);
9
+ const [x1, y1] = p(r, a1);
10
+ const [x2, y2] = p(r0, a1);
11
+ const [x3, y3] = p(r0, a0);
12
+ return `M${x0},${y0} A${r},${r} 0 ${large} 1 ${x1},${y1} L${x2},${y2} A${r0},${r0} 0 ${large} 0 ${x3},${y3} Z`;
13
+ }
14
+ /** The slices as a table: label, value and share. */
15
+ export function donutTable(data, format = String, headers = ['', '', '%']) {
16
+ const total = data.reduce((s, d) => s + Math.max(0, d.value), 0) || 1;
17
+ return {
18
+ headers,
19
+ rows: data.map((d) => [
20
+ d.label,
21
+ format(d.value),
22
+ `${Math.round((Math.max(0, d.value) / total) * 100)}%`,
23
+ ]),
24
+ };
25
+ }
26
+ /**
27
+ * Parts of a whole, with the total in the middle. The legend lists every slice with its value
28
+ * and share, so the numbers are readable without matching colours to wedges, and each slice
29
+ * carries the same pattern as its legend swatch.
30
+ */
31
+ export function DonutChart({ data, label, centerLabel, size = 180, formatValue, className, }) {
32
+ const id = useId().replace(/[^a-zA-Z0-9-]/g, '');
33
+ const locale = useUiLocale();
34
+ const nf = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
35
+ const format = formatValue ?? ((n) => nf.format(n));
36
+ const pct = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 0 });
37
+ const total = data.reduce((s, d) => s + Math.max(0, d.value), 0);
38
+ const r = size / 2;
39
+ const r0 = r * 0.62;
40
+ let angle = 0;
41
+ return (_jsxs("div", { className: ['ui-chart ui-chart--donut', className].filter(Boolean).join(' '), children: [_jsxs("svg", { role: "img", "aria-label": label, width: size, height: size, viewBox: `0 0 ${size} ${size}`, children: [_jsx("defs", { children: data.map((d, i) => (_jsx(SeriesPattern, { id: `${id}-p${i}`, index: i }, d.id))) }), _jsxs("g", { children: [total > 0 ? (data.map((d, i) => {
42
+ const share = Math.max(0, d.value) / total;
43
+ const a0 = angle;
44
+ // A full circle cannot be drawn as one arc, so a single slice stops just short.
45
+ const a1 = angle + Math.min(share * Math.PI * 2, Math.PI * 2 - 0.0001);
46
+ angle += share * Math.PI * 2;
47
+ return share > 0 ? (_jsx("path", { d: arc(r, r, r - 1, r0, a0, a1), fill: `url(#${id}-p${i})`, stroke: "var(--color-panel-solid)", strokeWidth: 1.5, children: _jsx("title", { children: `${d.label}: ${format(d.value)} (${pct.format(share)})` }) }, d.id)) : null;
48
+ })) : (_jsx("circle", { cx: r, cy: r, r: (r + r0) / 2, fill: "none", stroke: "var(--gray-a4)", strokeWidth: r - r0 })), _jsx("text", { x: r, y: r, textAnchor: "middle", dy: centerLabel ? '-0.1em' : '0.35em', className: "ui-chart__total", children: format(total) }), centerLabel ? (_jsx("text", { x: r, y: r, textAnchor: "middle", dy: "1.3em", className: "ui-chart__tick", children: centerLabel })) : null] })] }), _jsx(ChartLegend, { kind: "fill", idPrefix: id, entries: data.map((d) => ({
49
+ id: d.id,
50
+ label: d.label,
51
+ detail: `${format(d.value)} · ${pct.format(total ? Math.max(0, d.value) / total : 0)}`,
52
+ })) })] }));
53
+ }
@@ -0,0 +1,15 @@
1
+ export interface SparklineProps {
2
+ values: number[];
3
+ /** The accessible name: the trend in words ("Up 12% over 30 days"). */
4
+ label: string;
5
+ /** Default 32. The width follows the container. */
6
+ height?: number;
7
+ /** Shades the area under the line. */
8
+ area?: boolean;
9
+ className?: string;
10
+ }
11
+ /**
12
+ * A small trend line with no axes, sized for StatTile's `sparkline` slot. It draws in
13
+ * `currentColor`, so a parent's text colour (or the default accent) sets it.
14
+ */
15
+ export declare function Sparkline({ values, label, height, area, className }: SparklineProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { useElementWidth } from '../../hooks/useElementWidth.js';
4
+ /**
5
+ * A small trend line with no axes, sized for StatTile's `sparkline` slot. It draws in
6
+ * `currentColor`, so a parent's text colour (or the default accent) sets it.
7
+ */
8
+ export function Sparkline({ values, label, height = 32, area = false, className }) {
9
+ const ref = useRef(null);
10
+ const width = useElementWidth(ref) || 120;
11
+ const lo = Math.min(...values);
12
+ const hi = Math.max(...values);
13
+ const pad = 2;
14
+ const x = (i) => pad + (i / Math.max(1, values.length - 1)) * (width - pad * 2);
15
+ const y = (v) => pad + (1 - (v - lo) / (hi - lo || 1)) * (height - pad * 2);
16
+ const points = values.map((v, i) => `${x(i)},${y(v)}`).join(' ');
17
+ return (_jsx("div", { ref: ref, className: ['ui-sparkline', className].filter(Boolean).join(' '), children: _jsx("svg", { role: "img", "aria-label": label, width: "100%", height: height, viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", children: values.length > 1 ? (_jsxs("g", { children: [area ? (_jsx("polygon", { points: `${x(0)},${height} ${points} ${x(values.length - 1)},${height}`, fill: "currentColor", opacity: 0.12 })) : null, _jsx("polyline", { points: points, fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })] })) : null }) }));
18
+ }
@@ -0,0 +1,5 @@
1
+ export * from './CategoryCharts.js';
2
+ export * from './ChartFrame.js';
3
+ export * from './DonutChart.js';
4
+ export * from './Sparkline.js';
5
+ export { CHART_COLORS, ChartLegend, type ChartSeries, type ChartTableData, chartColor, chartTable, type LegendEntry, niceTicks, } from './shared.js';
@@ -0,0 +1,5 @@
1
+ export * from './CategoryCharts.js';
2
+ export * from './ChartFrame.js';
3
+ export * from './DonutChart.js';
4
+ export * from './Sparkline.js';
5
+ export { CHART_COLORS, ChartLegend, chartColor, chartTable, niceTicks, } from './shared.js';
@@ -0,0 +1,51 @@
1
+ export interface ChartSeries {
2
+ id: string;
3
+ label: string;
4
+ /** One value per category, in category order. `null` is a gap. */
5
+ values: (number | null)[];
6
+ }
7
+ export interface ChartTableData {
8
+ headers: string[];
9
+ rows: (string | number)[][];
10
+ }
11
+ /** The palette has eight entries; series past the eighth repeat with a different pattern. */
12
+ export declare const CHART_COLORS = 8;
13
+ export declare function chartColor(index: number): string;
14
+ /**
15
+ * Round tick values covering `[min, max]`: steps of 1, 2, 2.5 or 5 times a power of ten, about
16
+ * `count` of them, always including zero when the range crosses it.
17
+ */
18
+ export declare function niceTicks(min: number, max: number, count?: number): number[];
19
+ /** A table of the same numbers a category chart draws, for ChartFrame's table view. */
20
+ export declare function chartTable(categories: string[], series: ChartSeries[], format?: (n: number) => string, categoryHeader?: string): ChartTableData;
21
+ type PatternKind = 'solid' | 'diagonal' | 'dots' | 'cross' | 'horizontal' | 'vertical' | 'reverse' | 'grid';
22
+ /** The fill for series `index`: a colour, and for every series after the first a pattern too. */
23
+ export declare function patternFor(index: number): PatternKind;
24
+ /** The `<pattern>` for series `index`, to place in an SVG's `<defs>`. */
25
+ export declare function SeriesPattern({ id, index }: {
26
+ id: string;
27
+ index: number;
28
+ }): import("react").JSX.Element;
29
+ /** A point marker for line series `index`, centred on (x, y). Each series has its own shape. */
30
+ export declare function SeriesMarker({ index, x, y, r, }: {
31
+ index: number;
32
+ x: number;
33
+ y: number;
34
+ r?: number;
35
+ }): import("react").JSX.Element;
36
+ export interface LegendEntry {
37
+ id: string;
38
+ label: string;
39
+ /** Extra text after the label, e.g. a value and share for a donut. */
40
+ detail?: string;
41
+ }
42
+ /**
43
+ * The key to a chart's series. Each swatch repeats the pattern (bars, donut) or the marker
44
+ * (lines) the chart uses, so the legend works without telling colours apart.
45
+ */
46
+ export declare function ChartLegend({ entries, kind, idPrefix, }: {
47
+ entries: LegendEntry[];
48
+ kind: 'fill' | 'line';
49
+ idPrefix: string;
50
+ }): import("react").JSX.Element;
51
+ export {};
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /** The palette has eight entries; series past the eighth repeat with a different pattern. */
3
+ export const CHART_COLORS = 8;
4
+ export function chartColor(index) {
5
+ return `var(--chart-${(index % CHART_COLORS) + 1})`;
6
+ }
7
+ /**
8
+ * Round tick values covering `[min, max]`: steps of 1, 2, 2.5 or 5 times a power of ten, about
9
+ * `count` of them, always including zero when the range crosses it.
10
+ */
11
+ export function niceTicks(min, max, count = 5) {
12
+ if (!Number.isFinite(min) || !Number.isFinite(max))
13
+ return [0];
14
+ if (min === max) {
15
+ if (min === 0)
16
+ return [0, 1];
17
+ const pad = Math.abs(min) * 0.5;
18
+ return niceTicks(Math.min(0, min - pad), Math.max(0, max + pad), count);
19
+ }
20
+ const lo = Math.min(min, max);
21
+ const hi = Math.max(min, max);
22
+ const raw = (hi - lo) / Math.max(1, count - 1);
23
+ const power = 10 ** Math.floor(Math.log10(raw));
24
+ const step = [1, 2, 2.5, 5, 10].map((m) => m * power).find((s) => s >= raw) ?? 10 * power;
25
+ const start = Math.floor(lo / step) * step;
26
+ const end = Math.ceil(hi / step) * step;
27
+ const ticks = [];
28
+ for (let v = start; v <= end + step / 2; v += step)
29
+ ticks.push(Number(v.toFixed(10)));
30
+ return ticks;
31
+ }
32
+ /** A table of the same numbers a category chart draws, for ChartFrame's table view. */
33
+ export function chartTable(categories, series, format = String, categoryHeader = '') {
34
+ return {
35
+ headers: [categoryHeader, ...series.map((s) => s.label)],
36
+ rows: categories.map((c, i) => [
37
+ c,
38
+ ...series.map((s) => (s.values[i] === null ? '' : format(s.values[i]))),
39
+ ]),
40
+ };
41
+ }
42
+ const PATTERNS = [
43
+ 'solid',
44
+ 'diagonal',
45
+ 'dots',
46
+ 'cross',
47
+ 'horizontal',
48
+ 'vertical',
49
+ 'reverse',
50
+ 'grid',
51
+ ];
52
+ /** The fill for series `index`: a colour, and for every series after the first a pattern too. */
53
+ export function patternFor(index) {
54
+ return PATTERNS[index % PATTERNS.length];
55
+ }
56
+ /** The `<pattern>` for series `index`, to place in an SVG's `<defs>`. */
57
+ export function SeriesPattern({ id, index }) {
58
+ const color = chartColor(index);
59
+ const kind = patternFor(index);
60
+ const ink = 'var(--color-panel-solid)';
61
+ const marks = {
62
+ solid: null,
63
+ diagonal: _jsx("path", { d: "M-1,1 l2,-2 M0,6 l6,-6 M5,7 l2,-2", stroke: ink, strokeWidth: "1.2" }),
64
+ reverse: _jsx("path", { d: "M-1,5 l2,2 M0,0 l6,6 M5,-1 l2,2", stroke: ink, strokeWidth: "1.2" }),
65
+ dots: _jsx("circle", { cx: "3", cy: "3", r: "1.1", fill: ink }),
66
+ cross: _jsx("path", { d: "M0,0 l6,6 M6,0 l-6,6", stroke: ink, strokeWidth: "1" }),
67
+ horizontal: _jsx("path", { d: "M0,3 h6", stroke: ink, strokeWidth: "1.2" }),
68
+ vertical: _jsx("path", { d: "M3,0 v6", stroke: ink, strokeWidth: "1.2" }),
69
+ grid: _jsx("path", { d: "M0,3 h6 M3,0 v6", stroke: ink, strokeWidth: "1" }),
70
+ };
71
+ return (_jsxs("pattern", { id: id, width: "6", height: "6", patternUnits: "userSpaceOnUse", children: [_jsx("rect", { width: "6", height: "6", fill: color }), marks[kind]] }));
72
+ }
73
+ const MARKERS = [
74
+ 'circle',
75
+ 'square',
76
+ 'triangle',
77
+ 'diamond',
78
+ 'down',
79
+ 'cross',
80
+ 'plus',
81
+ 'star',
82
+ ];
83
+ /** A point marker for line series `index`, centred on (x, y). Each series has its own shape. */
84
+ export function SeriesMarker({ index, x, y, r = 3.5, }) {
85
+ const color = chartColor(index);
86
+ const kind = MARKERS[index % MARKERS.length];
87
+ const common = { fill: color, stroke: 'var(--color-panel-solid)', strokeWidth: 1 };
88
+ switch (kind) {
89
+ case 'circle':
90
+ return _jsx("circle", { cx: x, cy: y, r: r, ...common });
91
+ case 'square':
92
+ return _jsx("rect", { x: x - r, y: y - r, width: r * 2, height: r * 2, ...common });
93
+ case 'triangle':
94
+ return (_jsx("path", { d: `M${x},${y - r * 1.2} L${x + r * 1.1},${y + r * 0.8} L${x - r * 1.1},${y + r * 0.8} Z`, ...common }));
95
+ case 'down':
96
+ return (_jsx("path", { d: `M${x},${y + r * 1.2} L${x + r * 1.1},${y - r * 0.8} L${x - r * 1.1},${y - r * 0.8} Z`, ...common }));
97
+ case 'diamond':
98
+ return (_jsx("path", { d: `M${x},${y - r * 1.3} L${x + r * 1.3},${y} L${x},${y + r * 1.3} L${x - r * 1.3},${y} Z`, ...common }));
99
+ case 'cross':
100
+ return (_jsx("path", { d: `M${x - r},${y - r} L${x + r},${y + r} M${x + r},${y - r} L${x - r},${y + r}`, stroke: color, strokeWidth: 2 }));
101
+ case 'plus':
102
+ return (_jsx("path", { d: `M${x - r},${y} H${x + r} M${x},${y - r} V${y + r}`, stroke: color, strokeWidth: 2 }));
103
+ default:
104
+ return (_jsx("path", { d: `M${x},${y - r * 1.3} L${x + r * 0.4},${y - r * 0.4} L${x + r * 1.3},${y} L${x + r * 0.4},${y + r * 0.4} L${x},${y + r * 1.3} L${x - r * 0.4},${y + r * 0.4} L${x - r * 1.3},${y} L${x - r * 0.4},${y - r * 0.4} Z`, ...common }));
105
+ }
106
+ }
107
+ /**
108
+ * The key to a chart's series. Each swatch repeats the pattern (bars, donut) or the marker
109
+ * (lines) the chart uses, so the legend works without telling colours apart.
110
+ */
111
+ export function ChartLegend({ entries, kind, idPrefix, }) {
112
+ return (_jsx("ul", { className: "ui-chart-legend", children: entries.map((entry, i) => (_jsxs("li", { children: [_jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", children: kind === 'fill' ? (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx(SeriesPattern, { id: `${idPrefix}-legend-${i}`, index: i }) }), _jsx("rect", { width: "14", height: "14", rx: "3", fill: `url(#${idPrefix}-legend-${i})` })] })) : (_jsxs(_Fragment, { children: [_jsx("path", { d: "M0,7 H14", stroke: chartColor(i), strokeWidth: "2" }), _jsx(SeriesMarker, { index: i, x: 7, y: 7, r: 3.5 })] })) }), _jsx("span", { children: entry.label }), entry.detail ? _jsx("span", { className: "ui-chart-legend__detail", children: entry.detail }) : null] }, entry.id))) }));
113
+ }