@surflane/ui 0.5.0 → 0.6.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 (175) hide show
  1. package/README.md +1 -1
  2. package/dist/app-shell.d.ts +44 -3
  3. package/dist/app-shell.js +81 -8
  4. package/dist/bottom-navigation.d.ts +66 -0
  5. package/dist/bottom-navigation.js +36 -0
  6. package/dist/chart.d.ts +16 -0
  7. package/dist/chart.js +103 -48
  8. package/dist/css/accordion.css +11 -0
  9. package/dist/css/alert-dialog.css +11 -0
  10. package/{src → dist/css}/all.css +1 -1
  11. package/dist/css/app-shell.css +12 -0
  12. package/dist/css/autocomplete.css +15 -0
  13. package/dist/css/avatar.css +10 -0
  14. package/dist/css/badge.css +10 -0
  15. package/dist/css/bottom-navigation.css +8 -0
  16. package/dist/css/breadcrumbs.css +12 -0
  17. package/dist/css/button.css +8 -0
  18. package/dist/css/card.css +11 -0
  19. package/dist/css/chart.css +13 -0
  20. package/dist/css/checkbox-group.css +9 -0
  21. package/dist/css/checkbox.css +9 -0
  22. package/dist/css/choice-label.css +9 -0
  23. package/dist/css/code.css +13 -0
  24. package/dist/css/collapsible.css +11 -0
  25. package/dist/css/combobox.css +14 -0
  26. package/dist/css/context-menu.css +15 -0
  27. package/dist/css/date-picker.css +12 -0
  28. package/dist/css/dialog.css +10 -0
  29. package/dist/css/drawer.css +11 -0
  30. package/dist/css/empty-state.css +9 -0
  31. package/dist/css/field.css +10 -0
  32. package/dist/css/fieldset.css +8 -0
  33. package/dist/css/file-upload.css +17 -0
  34. package/dist/css/filter-bar.css +8 -0
  35. package/dist/css/form.css +8 -0
  36. package/dist/css/icon.css +8 -0
  37. package/dist/css/inline-message.css +10 -0
  38. package/dist/css/input.css +9 -0
  39. package/dist/css/layout.css +9 -0
  40. package/dist/css/link.css +9 -0
  41. package/dist/css/list.css +9 -0
  42. package/dist/css/markdown.css +18 -0
  43. package/dist/css/menu.css +14 -0
  44. package/dist/css/menubar.css +9 -0
  45. package/dist/css/meter.css +9 -0
  46. package/dist/css/native-select.css +13 -0
  47. package/dist/css/navigation-menu.css +11 -0
  48. package/dist/css/number-field.css +11 -0
  49. package/dist/css/otp-field.css +10 -0
  50. package/dist/css/page-header.css +11 -0
  51. package/dist/css/pagination.css +11 -0
  52. package/dist/css/popover.css +9 -0
  53. package/dist/css/preview-card.css +11 -0
  54. package/dist/css/progress.css +9 -0
  55. package/dist/css/radio-group.css +9 -0
  56. package/dist/css/rules/accordion.css +21 -0
  57. package/dist/css/rules/alert-dialog.css +23 -0
  58. package/dist/css/rules/app-shell.css +220 -0
  59. package/dist/css/rules/autocomplete.css +3 -0
  60. package/dist/css/rules/avatar.css +17 -0
  61. package/dist/css/rules/badge.css +17 -0
  62. package/dist/css/rules/bar.css +38 -0
  63. package/dist/css/rules/base.css +46 -0
  64. package/dist/css/rules/bottom-navigation.css +142 -0
  65. package/dist/css/rules/breadcrumbs.css +9 -0
  66. package/dist/css/rules/button.css +131 -0
  67. package/dist/css/rules/card.css +10 -0
  68. package/dist/css/rules/chart.css +256 -0
  69. package/dist/css/rules/checkbox-group.css +3 -0
  70. package/dist/css/rules/checkbox.css +25 -0
  71. package/dist/css/rules/choice-label.css +52 -0
  72. package/dist/css/rules/choice.css +104 -0
  73. package/dist/css/rules/code.css +9 -0
  74. package/dist/css/rules/collapsible.css +12 -0
  75. package/dist/css/rules/combobox.css +177 -0
  76. package/dist/css/rules/context-menu.css +9 -0
  77. package/dist/css/rules/date-picker.css +301 -0
  78. package/dist/css/rules/dialog.css +160 -0
  79. package/dist/css/rules/disclosure.css +107 -0
  80. package/dist/css/rules/drawer.css +18 -0
  81. package/dist/css/rules/empty-state.css +9 -0
  82. package/dist/css/rules/field-control.css +70 -0
  83. package/dist/css/rules/field.css +56 -0
  84. package/dist/css/rules/fieldset.css +23 -0
  85. package/dist/css/rules/file-upload.css +156 -0
  86. package/dist/css/rules/filter-bar.css +56 -0
  87. package/dist/css/rules/form.css +15 -0
  88. package/dist/css/rules/gauge.css +61 -0
  89. package/dist/css/rules/icon.css +27 -0
  90. package/dist/css/rules/inline-message.css +10 -0
  91. package/dist/css/rules/input-group.css +69 -0
  92. package/dist/css/rules/input.css +33 -0
  93. package/dist/css/rules/item.css +14 -0
  94. package/dist/css/rules/layout.css +3 -0
  95. package/dist/css/rules/link.css +9 -0
  96. package/dist/css/rules/list.css +10 -0
  97. package/dist/css/rules/markdown.css +63 -0
  98. package/dist/css/rules/menu.css +97 -0
  99. package/dist/css/rules/menubar.css +18 -0
  100. package/dist/css/rules/meter.css +3 -0
  101. package/dist/css/rules/native-select.css +3 -0
  102. package/dist/css/rules/navigation-menu.css +159 -0
  103. package/dist/css/rules/number-field.css +55 -0
  104. package/dist/css/rules/otp-field.css +68 -0
  105. package/dist/css/rules/page-header.css +3 -0
  106. package/dist/css/rules/pagination.css +3 -0
  107. package/dist/css/rules/panel.css +221 -0
  108. package/dist/css/rules/popover.css +41 -0
  109. package/dist/css/rules/popup.css +27 -0
  110. package/dist/css/rules/preview-card.css +60 -0
  111. package/dist/css/rules/progress.css +14 -0
  112. package/dist/css/rules/radio-group.css +26 -0
  113. package/dist/css/rules/scroll-area.css +101 -0
  114. package/dist/css/rules/select.css +88 -0
  115. package/dist/css/rules/separator.css +3 -0
  116. package/dist/css/rules/sheet.css +3 -0
  117. package/dist/css/rules/sizing.css +140 -0
  118. package/dist/css/rules/skeleton.css +41 -0
  119. package/dist/css/rules/slider.css +123 -0
  120. package/dist/css/rules/sparkline.css +52 -0
  121. package/dist/css/rules/spinner.css +38 -0
  122. package/dist/css/rules/stat.css +59 -0
  123. package/dist/css/rules/switch.css +50 -0
  124. package/dist/css/rules/syntax-highlighter.css +82 -0
  125. package/dist/css/rules/table.css +28 -0
  126. package/dist/css/rules/tabs.css +126 -0
  127. package/dist/css/rules/textarea.css +21 -0
  128. package/dist/css/rules/toast.css +247 -0
  129. package/dist/css/rules/toggle-group.css +3 -0
  130. package/dist/css/rules/toggle.css +27 -0
  131. package/dist/css/rules/toolbar.css +62 -0
  132. package/dist/css/rules/tooltip.css +38 -0
  133. package/dist/css/rules/tree.css +98 -0
  134. package/dist/css/rules/typography.css +3 -0
  135. package/dist/css/scroll-area.css +8 -0
  136. package/dist/css/select.css +12 -0
  137. package/dist/css/separator.css +9 -0
  138. package/dist/css/sheet.css +11 -0
  139. package/dist/css/sizing.css +7 -0
  140. package/dist/css/skeleton.css +8 -0
  141. package/dist/css/slider.css +8 -0
  142. package/dist/css/sparkline.css +8 -0
  143. package/dist/css/spinner.css +8 -0
  144. package/dist/css/stat.css +9 -0
  145. package/dist/css/styles.css +100 -0
  146. package/dist/css/switch.css +9 -0
  147. package/dist/css/syntax-highlighter.css +9 -0
  148. package/dist/css/table.css +12 -0
  149. package/dist/css/tabs.css +8 -0
  150. package/dist/css/textarea.css +10 -0
  151. package/dist/css/toast.css +11 -0
  152. package/dist/css/toggle-group.css +9 -0
  153. package/dist/css/toggle.css +10 -0
  154. package/dist/css/toolbar.css +12 -0
  155. package/dist/css/tooltip.css +9 -0
  156. package/dist/css/tree.css +10 -0
  157. package/dist/css/typography.css +9 -0
  158. package/dist/format.d.ts +25 -0
  159. package/dist/format.js +42 -0
  160. package/dist/icon.d.ts +1 -0
  161. package/dist/icon.js +1 -0
  162. package/dist/index.d.ts +7 -3
  163. package/dist/index.js +6 -2
  164. package/dist/sparkline.d.ts +41 -0
  165. package/dist/sparkline.js +50 -0
  166. package/dist/stat.d.ts +59 -0
  167. package/dist/stat.js +26 -0
  168. package/dist/status.d.ts +1 -1
  169. package/dist/status.js +1 -0
  170. package/dist/toast.d.ts +57 -1
  171. package/dist/toast.js +73 -2
  172. package/dist/tooltip.d.ts +9 -3
  173. package/dist/tooltip.js +4 -4
  174. package/package.json +90 -7
  175. package/src/styles.css +0 -3948
package/README.md CHANGED
@@ -18,7 +18,7 @@ Import the stylesheets once at the application root. `all.css` brings the tokens
18
18
  @import '@surflane/ui/all.css';
19
19
  ```
20
20
 
21
- The rules sit in `surflane.*` cascade layers, so your unlayered CSS overrides them. Set `data-theme="light"` or `data-theme="dark"` on `<html>` before the first paint and add `pui-typography` to `<body>`. The [install guide](https://surflane.page/install) walks through Vite, Next.js, React Router and Astro, with or without Tailwind CSS.
21
+ To ship only what you use, import component stylesheets instead, such as `@surflane/ui/button.css` and `@surflane/ui/dialog.css`. Each brings the tokens and the styles of what its component renders. The rules sit in `surflane.*` cascade layers, so your unlayered CSS overrides them. Set `data-theme="light"` or `data-theme="dark"` on `<html>` before the first paint and add `pui-typography` to `<body>`. The [install guide](https://surflane.page/install) walks through Vite, Next.js, React Router and Astro, with or without Tailwind CSS.
22
22
 
23
23
  ```tsx
24
24
  import {
@@ -1,11 +1,43 @@
1
1
  import * as React from 'react';
2
+ import { type ButtonProps } from './button.js';
3
+ export type AppShellRootProps = React.ComponentPropsWithoutRef<'div'> & {
4
+ layout?: 'header' | 'workspace';
5
+ /** Whether the sidebar is collapsed to its rail. Pass it to control the state. */
6
+ collapsed?: boolean;
7
+ /** The starting state when the shell controls it. */
8
+ defaultCollapsed?: boolean;
9
+ onCollapsedChange?: (collapsed: boolean) => void;
10
+ /** A localStorage key that remembers the state across visits. A server-rendered
11
+ * app reads a cookie into defaultCollapsed instead, so the first paint is right. */
12
+ persist?: string;
13
+ /** ⌘B, or Ctrl+B, toggles the sidebar unless focus is in a text field. */
14
+ shortcut?: boolean;
15
+ };
16
+ export type AppShellSidebarToggleProps = Omit<ButtonProps, 'children'> & {
17
+ /** The button's name in each state; it is also the tooltip. */
18
+ labels?: {
19
+ collapse?: string;
20
+ expand?: string;
21
+ };
22
+ children?: React.ReactNode;
23
+ };
2
24
  export declare const AppShell: {
3
25
  Root: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
4
- collapsed?: boolean;
5
26
  layout?: "header" | "workspace";
27
+ /** Whether the sidebar is collapsed to its rail. Pass it to control the state. */
28
+ collapsed?: boolean;
29
+ /** The starting state when the shell controls it. */
30
+ defaultCollapsed?: boolean;
31
+ onCollapsedChange?: (collapsed: boolean) => void;
32
+ /** A localStorage key that remembers the state across visits. A server-rendered
33
+ * app reads a cookie into defaultCollapsed instead, so the first paint is right. */
34
+ persist?: string;
35
+ /** ⌘B, or Ctrl+B, toggles the sidebar unless focus is in a text field. */
36
+ shortcut?: boolean;
6
37
  } & React.RefAttributes<HTMLDivElement>>;
7
38
  Header: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
8
39
  Sidebar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>;
40
+ SidebarToggle: React.ForwardRefExoticComponent<Omit<AppShellSidebarToggleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
9
41
  Content: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
10
42
  Page: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
11
43
  Footer: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
@@ -16,9 +48,18 @@ export declare const AppShell: {
16
48
  } & React.RefAttributes<HTMLAnchorElement>>;
17
49
  };
18
50
  export declare const AppShellRoot: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
19
- collapsed?: boolean;
20
51
  layout?: "header" | "workspace";
21
- } & React.RefAttributes<HTMLDivElement>>, AppShellHeader: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellSidebar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>, AppShellContent: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellPage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellFooter: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellLabel: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & React.RefAttributes<HTMLSpanElement>>, AppShellLink: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "ref">, "children"> & {
52
+ /** Whether the sidebar is collapsed to its rail. Pass it to control the state. */
53
+ collapsed?: boolean;
54
+ /** The starting state when the shell controls it. */
55
+ defaultCollapsed?: boolean;
56
+ onCollapsedChange?: (collapsed: boolean) => void;
57
+ /** A localStorage key that remembers the state across visits. A server-rendered
58
+ * app reads a cookie into defaultCollapsed instead, so the first paint is right. */
59
+ persist?: string;
60
+ /** ⌘B, or Ctrl+B, toggles the sidebar unless focus is in a text field. */
61
+ shortcut?: boolean;
62
+ } & React.RefAttributes<HTMLDivElement>>, AppShellHeader: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellSidebar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React.RefAttributes<HTMLElement>>, AppShellSidebarToggle: React.ForwardRefExoticComponent<Omit<AppShellSidebarToggleProps, "ref"> & React.RefAttributes<HTMLButtonElement>>, AppShellContent: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellPage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellFooter: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>, AppShellLabel: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & React.RefAttributes<HTMLSpanElement>>, AppShellLink: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "ref">, "children"> & {
22
63
  icon: React.ReactNode;
23
64
  label: string;
24
65
  } & React.RefAttributes<HTMLAnchorElement>>;
package/dist/app-shell.js CHANGED
@@ -1,16 +1,85 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
- import { cx } from './parts.js';
5
- /** Layout only. Routing, session, theme and mobile disclosure belong to the caller. */
6
- const Root = /* @__PURE__ */ React.forwardRef(function AppShellRoot({ collapsed = false, layout = 'header', className, ...props }, ref) {
7
- return (_jsx("div", { ...props, ref: ref, "data-layout": layout, "data-sidebar": collapsed ? 'collapsed' : 'expanded', className: cx('pui-app-shell', className) }));
4
+ import { cx, skin } from './parts.js';
5
+ import { Button } from './button.js';
6
+ import { Icon } from './icon.js';
7
+ import { Tooltip } from './tooltip.js';
8
+ const ShellContext = /* @__PURE__ */ React.createContext(null);
9
+ const SidebarContext = /* @__PURE__ */ React.createContext(false);
10
+ const editable = (target) => target instanceof HTMLElement &&
11
+ (target.isContentEditable ||
12
+ /^(input|textarea|select)$/i.test(target.tagName));
13
+ const apple = () => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
14
+ /** The sidebar shortcut as this platform writes it. */
15
+ const shortcutLabel = () => (apple() ? '⌘B' : 'Ctrl+B');
16
+ /** Layout only. Routing, session, theme and mobile disclosure belong to the caller;
17
+ * the shell keeps the sidebar's collapsed state unless you control it. */
18
+ const Root = /* @__PURE__ */ React.forwardRef(function AppShellRoot({ collapsed, defaultCollapsed = false, onCollapsedChange, persist, shortcut = true, layout = 'header', className, children, ...props }, ref) {
19
+ const [own, setOwn] = React.useState(defaultCollapsed);
20
+ const value = collapsed ?? own;
21
+ const latest = React.useRef(value);
22
+ latest.current = value;
23
+ const setCollapsed = React.useCallback((next) => {
24
+ if (collapsed === undefined)
25
+ setOwn(next);
26
+ onCollapsedChange?.(next);
27
+ if (persist)
28
+ try {
29
+ localStorage.setItem(persist, next ? 'collapsed' : 'expanded');
30
+ }
31
+ catch { }
32
+ }, [collapsed, onCollapsedChange, persist]);
33
+ React.useLayoutEffect(() => {
34
+ if (!persist || collapsed !== undefined)
35
+ return;
36
+ try {
37
+ const stored = localStorage.getItem(persist);
38
+ if (stored)
39
+ setOwn(stored === 'collapsed');
40
+ }
41
+ catch { }
42
+ }, [persist, collapsed]);
43
+ React.useEffect(() => {
44
+ if (!shortcut)
45
+ return;
46
+ const toggle = (event) => {
47
+ if (event.key.toLowerCase() !== 'b' ||
48
+ event.altKey ||
49
+ event.shiftKey ||
50
+ !(apple() ? event.metaKey : event.ctrlKey) ||
51
+ editable(event.target))
52
+ return;
53
+ event.preventDefault();
54
+ setCollapsed(!latest.current);
55
+ };
56
+ document.addEventListener('keydown', toggle);
57
+ return () => document.removeEventListener('keydown', toggle);
58
+ }, [shortcut, setCollapsed]);
59
+ const sidebarId = React.useId();
60
+ const shell = React.useMemo(() => ({ collapsed: value, setCollapsed, sidebarId, shortcut }), [value, setCollapsed, sidebarId, shortcut]);
61
+ return (_jsx(ShellContext.Provider, { value: shell, children: _jsx("div", { ...props, ref: ref, "data-layout": layout, "data-sidebar": value ? 'collapsed' : 'expanded', className: cx('pui-app-shell', className), children: children }) }));
62
+ });
63
+ /** Collapses the sidebar to its rail and back. Its tooltip names the shortcut. */
64
+ const SidebarToggle = /* @__PURE__ */ React.forwardRef(function AppShellSidebarToggle({ labels, className, children, onClick, ...props }, ref) {
65
+ const shell = React.useContext(ShellContext);
66
+ if (!shell)
67
+ throw new Error('AppShell.SidebarToggle belongs in AppShell.Root');
68
+ const label = shell.collapsed
69
+ ? (labels?.expand ?? 'Expand sidebar')
70
+ : (labels?.collapse ?? 'Collapse sidebar');
71
+ return (_jsx(Tooltip, { label: label, shortcut: shell.shortcut ? shortcutLabel() : undefined, children: _jsx(Button, { variant: 'ghost', size: 'icon', ...props, ref: ref, "aria-label": label, "aria-expanded": !shell.collapsed, "aria-controls": shell.sidebarId, className: skin('pui-app-shell-toggle', className), onClick: (event) => {
72
+ onClick?.(event);
73
+ if (!event.defaultPrevented)
74
+ shell.setCollapsed(!shell.collapsed);
75
+ }, children: children ?? _jsx(Icon, { name: 'sidebar' }) }) }));
8
76
  });
9
77
  const Header = /* @__PURE__ */ React.forwardRef(function AppShellHeader({ className, ...props }, ref) {
10
78
  return (_jsx("div", { ...props, ref: ref, className: cx('pui-app-shell-header', className) }));
11
79
  });
12
- const Sidebar = /* @__PURE__ */ React.forwardRef(function AppShellSidebar({ className, ...props }, ref) {
13
- return (_jsx("aside", { ...props, ref: ref, className: cx('pui-app-shell-sidebar', className) }));
80
+ const Sidebar = /* @__PURE__ */ React.forwardRef(function AppShellSidebar({ className, id, ...props }, ref) {
81
+ const shell = React.useContext(ShellContext);
82
+ return (_jsx(SidebarContext.Provider, { value: true, children: _jsx("aside", { ...props, ref: ref, id: id ?? shell?.sidebarId, className: cx('pui-app-shell-sidebar', className) }) }));
14
83
  });
15
84
  const Content = /* @__PURE__ */ React.forwardRef(function AppShellContent({ className, ...props }, ref) {
16
85
  return (_jsx("div", { ...props, ref: ref, className: cx('pui-app-shell-content', className) }));
@@ -29,12 +98,16 @@ const Label = /* @__PURE__ */ React.forwardRef(function AppShellLabel({ classNam
29
98
  return (_jsx("span", { ...props, ref: ref, className: cx('pui-app-shell-label', className) }));
30
99
  });
31
100
  const Link = /* @__PURE__ */ React.forwardRef(function AppShellLink({ icon, label, className, ...props }, ref) {
32
- return (_jsxs("a", { ...props, ref: ref, "aria-label": props['aria-label'] ?? label, className: cx('pui-app-shell-link', className), children: [_jsx("span", { className: 'pui-app-shell-icon', "aria-hidden": 'true', children: icon }), _jsx(Label, { "aria-hidden": 'true', children: label })] }));
101
+ const shell = React.useContext(ShellContext);
102
+ const inSidebar = React.useContext(SidebarContext);
103
+ // On the collapsed rail only the icon shows, so the name appears as a tooltip.
104
+ return (_jsx(Tooltip, { label: label, enabled: Boolean(shell?.collapsed && inSidebar), children: _jsxs("a", { ...props, ref: ref, "aria-label": props['aria-label'] ?? label, className: cx('pui-app-shell-link', className), children: [_jsx("span", { className: 'pui-app-shell-icon', "aria-hidden": 'true', children: icon }), _jsx(Label, { "aria-hidden": 'true', children: label })] }) }));
33
105
  });
34
106
  export const AppShell = {
35
107
  Root,
36
108
  Header,
37
109
  Sidebar,
110
+ SidebarToggle,
38
111
  Content,
39
112
  Page,
40
113
  Footer,
@@ -42,4 +115,4 @@ export const AppShell = {
42
115
  Link,
43
116
  };
44
117
  // Flat names for React Server Components, which cannot dot into client exports.
45
- export const { Root: AppShellRoot, Header: AppShellHeader, Sidebar: AppShellSidebar, Content: AppShellContent, Page: AppShellPage, Footer: AppShellFooter, Label: AppShellLabel, Link: AppShellLink, } = AppShell;
118
+ export const { Root: AppShellRoot, Header: AppShellHeader, Sidebar: AppShellSidebar, SidebarToggle: AppShellSidebarToggle, Content: AppShellContent, Page: AppShellPage, Footer: AppShellFooter, Label: AppShellLabel, Link: AppShellLink, } = AppShell;
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import { useRender } from '@base-ui/react/use-render';
3
+ export type BottomNavigationRootProps = React.ComponentPropsWithoutRef<'nav'> & {
4
+ /** Names the navigation, such as "Primary". */
5
+ 'aria-label': string;
6
+ /** mobile: shown at 760px and narrower, where AppShell hides its sidebar. always:
7
+ * at every width. */
8
+ visible?: 'mobile' | 'always';
9
+ /** fixed: pinned to the bottom of the viewport, above the safe area. static: in the
10
+ * flow, for a frame of your own. */
11
+ position?: 'fixed' | 'static';
12
+ };
13
+ export type BottomNavigationItemProps = Omit<useRender.ComponentProps<'a'>, 'ref' | 'children'> & {
14
+ icon: React.ReactNode;
15
+ label: string;
16
+ /** Marks the current destination with aria-current="page". */
17
+ current?: boolean;
18
+ /** A count, shown up to 99+, or true for a dot. */
19
+ badge?: number | boolean;
20
+ /** What the badge means, read after the label, such as "3 unread". A count reads
21
+ * as its number without it; a dot needs it. */
22
+ badgeLabel?: string;
23
+ };
24
+ export declare const BottomNavigation: {
25
+ Root: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & {
26
+ /** Names the navigation, such as "Primary". */
27
+ 'aria-label': string;
28
+ /** mobile: shown at 760px and narrower, where AppShell hides its sidebar. always:
29
+ * at every width. */
30
+ visible?: "mobile" | "always";
31
+ /** fixed: pinned to the bottom of the viewport, above the safe area. static: in the
32
+ * flow, for a frame of your own. */
33
+ position?: "fixed" | "static";
34
+ } & React.RefAttributes<HTMLElement>>;
35
+ Item: React.ForwardRefExoticComponent<Omit<useRender.ComponentProps<"a", {}, import("@base-ui/react/types").HTMLProps>, "children" | "ref"> & {
36
+ icon: React.ReactNode;
37
+ label: string;
38
+ /** Marks the current destination with aria-current="page". */
39
+ current?: boolean;
40
+ /** A count, shown up to 99+, or true for a dot. */
41
+ badge?: number | boolean;
42
+ /** What the badge means, read after the label, such as "3 unread". A count reads
43
+ * as its number without it; a dot needs it. */
44
+ badgeLabel?: string;
45
+ } & React.RefAttributes<HTMLAnchorElement>>;
46
+ };
47
+ export declare const BottomNavigationRoot: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & {
48
+ /** Names the navigation, such as "Primary". */
49
+ 'aria-label': string;
50
+ /** mobile: shown at 760px and narrower, where AppShell hides its sidebar. always:
51
+ * at every width. */
52
+ visible?: "mobile" | "always";
53
+ /** fixed: pinned to the bottom of the viewport, above the safe area. static: in the
54
+ * flow, for a frame of your own. */
55
+ position?: "fixed" | "static";
56
+ } & React.RefAttributes<HTMLElement>>, BottomNavigationItem: React.ForwardRefExoticComponent<Omit<useRender.ComponentProps<"a", {}, import("@base-ui/react/types").HTMLProps>, "children" | "ref"> & {
57
+ icon: React.ReactNode;
58
+ label: string;
59
+ /** Marks the current destination with aria-current="page". */
60
+ current?: boolean;
61
+ /** A count, shown up to 99+, or true for a dot. */
62
+ badge?: number | boolean;
63
+ /** What the badge means, read after the label, such as "3 unread". A count reads
64
+ * as its number without it; a dot needs it. */
65
+ badgeLabel?: string;
66
+ } & React.RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import * as React from 'react';
4
+ import { useRender } from '@base-ui/react/use-render';
5
+ import { cx } from './parts.js';
6
+ /** A phone's primary destinations along the bottom edge, within thumb reach. Pad the
7
+ * scrolling content by --size-bottom-navigation so the last line stays visible. */
8
+ const Root = /* @__PURE__ */ React.forwardRef(function BottomNavigationRoot({ visible = 'mobile', position = 'fixed', className, children, ...props }, ref) {
9
+ return (_jsx("nav", { ...props, ref: ref, "data-visible": visible, "data-position": position, className: cx('pui-bottom-nav', className), children: _jsx("ul", { role: 'list', className: 'pui-bottom-nav-list', children: children }) }));
10
+ });
11
+ /** One destination: an icon above its label. Pass a router's link element as
12
+ * `render` to keep client navigation. */
13
+ const Item = /* @__PURE__ */ React.forwardRef(function BottomNavigationItem({ icon, label, current, badge, badgeLabel, render, className, ...props }, ref) {
14
+ const count = typeof badge === 'number' && badge > 0
15
+ ? badge > 99
16
+ ? '99+'
17
+ : String(badge)
18
+ : undefined;
19
+ const shown = Boolean(count || badge === true);
20
+ const spoken = badgeLabel ?? count;
21
+ const link = useRender({
22
+ defaultTagName: 'a',
23
+ render,
24
+ ref,
25
+ props: {
26
+ ...props,
27
+ 'aria-current': current ? 'page' : props['aria-current'],
28
+ className: cx('pui-bottom-nav-item', className),
29
+ children: (_jsxs(_Fragment, { children: [_jsxs("span", { className: 'pui-bottom-nav-icon', "aria-hidden": 'true', children: [icon, shown && (_jsx("span", { className: 'pui-bottom-nav-badge', "data-dot": count ? undefined : '', children: count }))] }), _jsx("span", { className: 'pui-bottom-nav-label', children: label }), shown && spoken && (_jsxs("span", { className: 'pui-feedback-sr', children: [", ", spoken] }))] })),
30
+ },
31
+ });
32
+ return _jsx("li", { className: 'pui-bottom-nav-entry', children: link });
33
+ });
34
+ export const BottomNavigation = { Root, Item };
35
+ // Flat names for React Server Components, which cannot dot into client exports.
36
+ export const { Root: BottomNavigationRoot, Item: BottomNavigationItem } = BottomNavigation;
package/dist/chart.d.ts CHANGED
@@ -18,6 +18,12 @@ export type ChartProps = Omit<React.ComponentPropsWithoutRef<'figure'>, 'childre
18
18
  yLabel: string;
19
19
  type?: 'line' | 'bar' | 'area';
20
20
  height?: number;
21
+ /** Keeps 0 on the value axis. Turn it off for values far from zero, such as
22
+ * prices, so the line uses the height; bars always start at zero. */
23
+ includeZero?: boolean;
24
+ /** neutral tells series apart by ink, dashes and markers. categorical adds six hues,
25
+ * for categories that color must separate, such as buckets or sectors. */
26
+ palette?: 'neutral' | 'categorical';
21
27
  locale?: string;
22
28
  formatValue?: (value: number) => string;
23
29
  /** Text the chart shows or announces; pass translations here. */
@@ -40,6 +46,10 @@ export type ChartLabels = {
40
46
  viewTable: string;
41
47
  table: (title: string) => string;
42
48
  tableCaption: (unit: string) => string;
49
+ /** The readout before any point is inspected. */
50
+ inspectHint: string;
51
+ /** The data table's only row when there is no data. */
52
+ noRows: string;
43
53
  };
44
54
  /** Cartesian trends with a persistent, keyboard-readable tooltip region and a
45
55
  * full data-table alternative. Missing values are gaps, never fabricated zeros. */
@@ -53,6 +63,12 @@ export declare const Chart: React.ForwardRefExoticComponent<Omit<Omit<React.Deta
53
63
  yLabel: string;
54
64
  type?: "line" | "bar" | "area";
55
65
  height?: number;
66
+ /** Keeps 0 on the value axis. Turn it off for values far from zero, such as
67
+ * prices, so the line uses the height; bars always start at zero. */
68
+ includeZero?: boolean;
69
+ /** neutral tells series apart by ink, dashes and markers. categorical adds six hues,
70
+ * for categories that color must separate, such as buckets or sectors. */
71
+ palette?: "neutral" | "categorical";
56
72
  locale?: string;
57
73
  formatValue?: (value: number) => string;
58
74
  /** Text the chart shows or announces; pass translations here. */
package/dist/chart.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
- import { Area, Bar, CartesianGrid, ComposedChart, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts';
4
+ import { Area, Bar, CartesianGrid, ComposedChart, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, useActiveTooltipCoordinate, useActiveTooltipLabel, useIsTooltipActive, usePlotArea, } from 'recharts';
5
+ import { createPortal } from 'react-dom';
5
6
  import { Table } from './table.js';
6
7
  import { Button } from './button.js';
7
8
  import { cx } from './parts.js';
@@ -15,9 +16,17 @@ const chartLabels = {
15
16
  viewTable: 'View data table',
16
17
  table: (title) => `${title} data table`,
17
18
  tableCaption: (unit) => `All input measurements, including hidden series. Unit: ${unit}.`,
19
+ inspectHint: 'Hover, touch or use chart arrow keys to inspect a measurement.',
20
+ noRows: 'No rows.',
18
21
  };
22
+ /** The round step at or above a raw step: 1, 2, 2.5 or 5 times a power of ten. */
23
+ function roundStep(raw) {
24
+ if (!(raw > 0))
25
+ return 0;
26
+ const power = 10 ** Math.floor(Math.log10(raw));
27
+ return power * ([1, 2, 2.5, 5, 10].find((f) => f * power >= raw) ?? 10);
28
+ }
19
29
  const dashes = [undefined, '7 4', '2 4', '10 3 2 3', '12 5', '4 3'];
20
- const colorFor = (index) => `var(--chart-series-${(index % 3) + 1})`;
21
30
  function Marker({ cx, cy, color, index, }) {
22
31
  if (typeof cx !== 'number' ||
23
32
  typeof cy !== 'number' ||
@@ -30,9 +39,37 @@ function Marker({ cx, cy, color, index, }) {
30
39
  function Pattern({ id, index, color, }) {
31
40
  return (_jsxs("pattern", { id: id, patternUnits: 'userSpaceOnUse', width: '8', height: '8', children: [_jsx("rect", { width: '8', height: '8', fill: color }), index % 3 === 1 ? (_jsx("path", { d: 'M-2 2L2-2M0 8L8 0M6 10L10 6', stroke: 'var(--canvas)', strokeWidth: '1.5' })) : index % 3 === 2 ? (_jsx("circle", { cx: '4', cy: '4', r: '1.4', fill: 'var(--canvas)' })) : null] }));
32
41
  }
42
+ /** A series' swatch, as the legend and the floating values draw it. */
43
+ function SeriesMark({ type, index, color, pattern, }) {
44
+ return (_jsx("svg", { className: 'pui-chart-mark', viewBox: '0 0 32 20', "aria-hidden": 'true', focusable: 'false', children: type === 'bar' ? (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx(Pattern, { id: pattern, color: color, index: index }) }), _jsx("rect", { x: '4', y: '3', width: '24', height: '14', rx: '2', fill: `url(#${pattern})` })] })) : (_jsxs(_Fragment, { children: [_jsx("path", { d: 'M1 10h30', stroke: color, strokeWidth: '2', strokeDasharray: dashes[index % dashes.length] }), _jsx(Marker, { cx: 16, cy: 10, index: index, color: color })] })) }));
45
+ }
46
+ /** The active point's values beside it, for pointer and touch users. The readout
47
+ * below the plot announces the same values, so this copy stays out of the
48
+ * accessibility tree. It sits on the side of the point with more room and stays
49
+ * inside the plot. */
50
+ function FloatingValues({ host, hidden, render, }) {
51
+ const active = useIsTooltipActive();
52
+ const label = useActiveTooltipLabel();
53
+ const point = useActiveTooltipCoordinate();
54
+ const plot = usePlotArea();
55
+ const box = React.useRef(null);
56
+ const [width, setWidth] = React.useState(0);
57
+ const shown = Boolean(host && !hidden && active && label !== undefined && point && plot);
58
+ React.useLayoutEffect(() => {
59
+ if (shown && box.current)
60
+ setWidth(box.current.offsetWidth);
61
+ }, [shown, label, plot?.width]);
62
+ if (!shown || !point || !plot)
63
+ return null;
64
+ // The gap between the point and the box, in px: the --space-3 step.
65
+ const gap = 12;
66
+ const start = point.x > plot.x + plot.width / 2;
67
+ const left = Math.min(Math.max(start ? point.x - gap - width : point.x + gap, plot.x), plot.x + plot.width - width);
68
+ return createPortal(_jsx("div", { ref: box, className: 'pui-chart-tooltip', "aria-hidden": 'true', style: { left, top: plot.y, visibility: width ? undefined : 'hidden' }, children: render(String(label)) }), host);
69
+ }
33
70
  /** Cartesian trends with a persistent, keyboard-readable tooltip region and a
34
71
  * full data-table alternative. Missing values are gaps, never fabricated zeros. */
35
- export const Chart = /* @__PURE__ */ React.forwardRef(function Chart({ title, description, data, series, unit, xLabel, yLabel, type = 'line', height = 280, locale = 'en-US', formatValue, labels, missingLabel, emptyLabel, hiddenSeries, defaultHiddenSeries = [], onHiddenSeriesChange, className, ...props }, ref) {
72
+ export const Chart = /* @__PURE__ */ React.forwardRef(function Chart({ title, description, data, series, unit, xLabel, yLabel, type = 'line', height = 280, includeZero = true, palette = 'neutral', locale = 'en-US', formatValue, labels, missingLabel, emptyLabel, hiddenSeries, defaultHiddenSeries = [], onHiddenSeriesChange, className, ...props }, ref) {
36
73
  const text = {
37
74
  ...chartLabels,
38
75
  ...(missingLabel === undefined ? {} : { missing: missingLabel }),
@@ -40,10 +77,14 @@ export const Chart = /* @__PURE__ */ React.forwardRef(function Chart({ title, de
40
77
  ...labels,
41
78
  };
42
79
  const id = React.useId().replace(/[^a-zA-Z0-9_-]/g, '_');
80
+ const colorFor = (index) => palette === 'categorical'
81
+ ? `var(--chart-category-${(index % 6) + 1})`
82
+ : `var(--chart-series-${(index % 3) + 1})`;
43
83
  const [internalHidden, setInternalHidden] = React.useState(defaultHiddenSeries);
44
84
  const hidden = new Set(hiddenSeries ?? internalHidden);
45
85
  const visible = series.filter((s) => !hidden.has(s.key));
46
86
  const [host, setHost] = React.useState(null);
87
+ const [floatHost, setFloatHost] = React.useState(null);
47
88
  const [dismissed, setDismissed] = React.useState(false);
48
89
  const firstLegend = React.useRef(null);
49
90
  const [restoreRequested, setRestoreRequested] = React.useState(false);
@@ -89,7 +130,16 @@ export const Chart = /* @__PURE__ */ React.forwardRef(function Chart({ title, de
89
130
  const values = rows
90
131
  .flatMap((row) => visible.map((s) => row.values[s.key]))
91
132
  .filter((value) => typeof value === 'number');
92
- const min = values.reduce((a, b) => Math.min(a, b), 0), max = values.reduce((a, b) => Math.max(a, b), 0);
133
+ // Bars grow from zero, so they always keep it on the axis. Without zero, the axis
134
+ // spans the values, widened to round steps so its ticks read cleanly.
135
+ const keepZero = includeZero || type === 'bar';
136
+ const start = keepZero ? 0 : (values[0] ?? 0);
137
+ const low = values.reduce((a, b) => Math.min(a, b), start), high = values.reduce((a, b) => Math.max(a, b), start);
138
+ const step = keepZero ? 0 : roundStep((high - low) / 4);
139
+ const min = step ? Math.floor(low / step) * step : low, max = step ? Math.ceil(high / step) * step : high;
140
+ const ticks = step
141
+ ? Array.from({ length: Math.round((max - min) / step) + 1 }, (_, index) => min + index * step)
142
+ : undefined;
93
143
  const hasValues = values.length > 0;
94
144
  const changeHidden = (next) => {
95
145
  if (hiddenSeries === undefined)
@@ -100,48 +150,53 @@ export const Chart = /* @__PURE__ */ React.forwardRef(function Chart({ title, de
100
150
  .filter((s) => (s.key === key ? !hidden.has(key) : hidden.has(s.key)))
101
151
  .map((s) => s.key));
102
152
  const chartHeight = Number.isFinite(height) ? Math.max(200, height) : 280;
103
- return (_jsxs("figure", { ...props, ref: ref, className: cx('pui-chart', className), "aria-labelledby": `${id}-title`, children: [_jsxs("figcaption", { className: 'pui-chart-caption', children: [_jsx("strong", { id: `${id}-title`, children: title }), _jsx("p", { id: `${id}-summary`, children: description })] }), _jsx("ul", { className: 'pui-chart-legend', "aria-label": `${title} series`, children: series.map((s, index) => (_jsx("li", { children: _jsxs("button", { ref: index === 0 ? firstLegend : undefined, type: 'button', className: 'pui-chart-legend-button', "aria-label": `${s.label} series`, "aria-pressed": !hidden.has(s.key), onClick: () => toggle(s.key), children: [_jsx("svg", { width: '32', height: '20', viewBox: '0 0 32 20', "aria-hidden": 'true', children: type === 'bar' ? (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx(Pattern, { id: `${id}-legend-${index}`, color: colorFor(index), index: index }) }), _jsx("rect", { x: '4', y: '3', width: '24', height: '14', rx: '2', fill: `url(#${id}-legend-${index})` })] })) : (_jsxs(_Fragment, { children: [_jsx("path", { d: 'M1 10h30', stroke: colorFor(index), strokeWidth: '2', strokeDasharray: dashes[index % dashes.length] }), _jsx(Marker, { cx: 16, cy: 10, index: index, color: colorFor(index) })] })) }), _jsx("span", { children: s.label })] }) }, s.key))) }), _jsxs("p", { className: 'pui-chart-axis-label', children: [yLabel, " (", unit, ")"] }), hasValues ? (_jsxs(_Fragment, { children: [_jsx("p", { className: 'pui-chart-help', id: `${id}-keys`, children: text.keyboardHelp }), _jsx("div", { className: 'pui-chart-plot', onKeyDownCapture: (event) => {
104
- if (event.key === 'Escape')
105
- setDismissed(true);
106
- else if (event.key === 'ArrowLeft' ||
107
- event.key === 'ArrowRight')
108
- setDismissed(false);
109
- }, onMouseMoveCapture: () => setDismissed(false), onTouchStartCapture: () => setDismissed(false), onFocusCapture: () => setDismissed(false), children: _jsx(ResponsiveContainer, { width: '100%', height: chartHeight, minWidth: 0, initialDimension: { width: 320, height: chartHeight }, children: _jsxs(ComposedChart, { data: rows, accessibilityLayer: true, role: 'application', "aria-label": title, "aria-describedby": `${id}-summary ${id}-keys`, margin: { top: 16, right: 20, bottom: 8, left: 0 }, children: [_jsx("defs", { children: series.map((s, index) => (_jsx(Pattern, { id: `${id}-pattern-${index}`, index: index, color: colorFor(index) }, s.key))) }), _jsx(CartesianGrid, { vertical: false, stroke: 'var(--line)' }), _jsx(XAxis, { dataKey: 'id', tickFormatter: (value) => {
110
- const label = byId.get(String(value))?.label ?? String(value);
111
- // minTickGap already drops ticks that would overlap; the cap
112
- // only keeps one very long label from crowding the axis.
113
- return label.length > 24
114
- ? label.slice(0, 23) + '…'
115
- : label;
116
- }, tick: { fill: 'var(--ink-muted)', fontSize: 12 }, tickLine: false, axisLine: { stroke: 'var(--line)' }, minTickGap: 24, interval: 'preserveStartEnd' }), _jsx(YAxis, { width: 56, tickFormatter: (value) => compact.format(Number(value)), tick: { fill: 'var(--ink-muted)', fontSize: 12 }, tickLine: false, axisLine: false, domain: [min, max === min ? max + 1 : max] }), _jsx(ReferenceLine, { y: 0, stroke: 'var(--ink-muted)' }), host && (_jsx(Tooltip, { portal: host, isAnimationActive: false, active: dismissed ? false : undefined, filterNull: false, cursor: dismissed
117
- ? false
118
- : {
119
- stroke: 'var(--control-border)',
120
- fill: 'transparent',
121
- }, wrapperStyle: {
122
- position: 'static',
123
- visibility: 'visible',
124
- pointerEvents: 'auto',
125
- }, content: (tooltip) => {
126
- const row = tooltip.active && !dismissed
127
- ? byId.get(String(tooltip.label))
128
- : undefined;
129
- return (_jsx("div", { className: 'pui-chart-readout', "data-chart-readout": row?.id, children: row ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsx("ul", { children: visible.map((s) => (_jsxs("li", { children: [s.label, ": ", format(row.values[s.key])] }, s.key))) })] })) : (_jsx("span", { children: "Hover, touch or use chart arrow keys to inspect a measurement." })) }));
130
- } })), series.map((s, index) => {
131
- const common = {
132
- name: s.label,
133
- dataKey: (row) => row.values[s.key] ?? null,
134
- hide: hidden.has(s.key),
135
- isAnimationActive: false,
136
- stroke: colorFor(index),
137
- };
138
- return type === 'bar' ? (_jsx(Bar, { ...common, fill: `url(#${id}-pattern-${index})`, maxBarSize: 48 }, s.key)) : type === 'area' ? (_jsx(Area, { ...common, type: 'linear', strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], fill: `url(#${id}-pattern-${index})`, fillOpacity: 0.12, connectNulls: false, dot: rows.length > 40
139
- ? false
140
- : (dot) => (_jsx(Marker, { ...dot, color: colorFor(index), index: index })), activeDot: false }, s.key)) : (_jsx(Line, { ...common, type: 'linear', strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], connectNulls: false, dot: rows.length > 40
141
- ? false
142
- : (dot) => (_jsx(Marker, { ...dot, color: colorFor(index), index: index })), activeDot: false }, s.key));
143
- })] }) }) }), _jsx("p", { className: 'pui-chart-axis-label pui-chart-x-label', children: xLabel })] })) : (_jsx("div", { className: 'pui-chart-empty', role: 'status', children: visible.length === 0 && series.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("p", { children: text.noSeries }), _jsx(Button, { variant: 'outline', onClick: () => {
144
- setRestoreRequested(true);
145
- changeHidden([]);
146
- }, children: text.showAll })] })) : (_jsx("p", { children: text.empty })) })), _jsx("div", { className: 'pui-chart-inspector', ref: setHost, role: 'status', "aria-live": 'polite', "aria-atomic": 'true', "aria-label": text.values(title), hidden: !hasValues }), _jsxs("details", { className: 'pui-chart-table', children: [_jsx("summary", { children: text.viewTable }), _jsx(Table.Container, { label: text.table(title), orientation: 'both', scrollChain: 'contain', children: _jsxs(Table.Root, { children: [_jsx(Table.Caption, { children: text.tableCaption(unit) }), _jsx(Table.Head, { children: _jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'col', children: xLabel }), series.map((s) => (_jsxs(Table.HeaderCell, { scope: 'col', children: [s.label, " (", unit, ")"] }, s.key)))] }) }), _jsx(Table.Body, { children: rows.length ? (rows.map((row) => (_jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'row', children: row.label }), series.map((s) => (_jsx(Table.Cell, { children: format(row.values[s.key]) }, s.key)))] }, row.id)))) : (_jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: series.length + 1, children: "No rows." }) })) })] }) })] })] }));
153
+ return (_jsxs("figure", { ...props, ref: ref, className: cx('pui-chart', className), "aria-labelledby": `${id}-title`, children: [_jsxs("figcaption", { className: 'pui-chart-caption', children: [_jsx("strong", { id: `${id}-title`, children: title }), _jsx("p", { id: `${id}-summary`, children: description })] }), _jsxs("div", { className: 'pui-chart-frame', children: [_jsxs("div", { className: 'pui-chart-key', children: [_jsxs("p", { className: 'pui-chart-axis-label', children: [yLabel, " (", unit, ")"] }), _jsx("ul", { className: 'pui-chart-legend', "aria-label": `${title} series`, children: series.map((s, index) => (_jsx("li", { children: _jsxs("button", { ref: index === 0 ? firstLegend : undefined, type: 'button', className: 'pui-chart-legend-button', "aria-label": `${s.label} series`, "aria-pressed": !hidden.has(s.key), onClick: () => toggle(s.key), children: [_jsx(SeriesMark, { type: type, index: index, color: colorFor(index), pattern: `${id}-legend-${index}` }), _jsx("span", { children: s.label })] }) }, s.key))) })] }), hasValues ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: 'pui-chart-plot', onKeyDownCapture: (event) => {
154
+ if (event.key === 'Escape')
155
+ setDismissed(true);
156
+ else if (event.key === 'ArrowLeft' ||
157
+ event.key === 'ArrowRight')
158
+ setDismissed(false);
159
+ }, onMouseMoveCapture: () => setDismissed(false), onTouchStartCapture: () => setDismissed(false), onFocusCapture: () => setDismissed(false), children: [_jsx(ResponsiveContainer, { width: '100%', height: chartHeight, minWidth: 0, initialDimension: { width: 320, height: chartHeight }, children: _jsxs(ComposedChart, { data: rows, accessibilityLayer: true, role: 'application', "aria-label": title, "aria-describedby": `${id}-summary ${id}-keys`, margin: { top: 16, right: 20, bottom: 8, left: 0 }, children: [_jsx("defs", { children: series.map((s, index) => (_jsx(Pattern, { id: `${id}-pattern-${index}`, index: index, color: colorFor(index) }, s.key))) }), _jsx(CartesianGrid, { vertical: false, stroke: 'var(--line)' }), _jsx(XAxis, { dataKey: 'id', tickFormatter: (value) => {
160
+ const label = byId.get(String(value))?.label ?? String(value);
161
+ // minTickGap already drops ticks that would overlap; the cap
162
+ // only keeps one very long label from crowding the axis.
163
+ return label.length > 24
164
+ ? label.slice(0, 23) + '…'
165
+ : label;
166
+ }, tick: { fill: 'var(--ink-muted)', fontSize: 12 }, tickLine: false, axisLine: { stroke: 'var(--line)' }, minTickGap: 24, interval: 'preserveStartEnd' }), _jsx(YAxis, { width: 56, tickFormatter: (value) => compact.format(Number(value)), tick: { fill: 'var(--ink-muted)', fontSize: 12 }, tickLine: false, axisLine: false, domain: [min, max === min ? max + 1 : max], ticks: ticks }), _jsx(ReferenceLine, { y: 0, stroke: 'var(--ink-muted)' }), host && (_jsx(Tooltip, { portal: host, isAnimationActive: false, active: dismissed ? false : undefined, filterNull: false, cursor: dismissed
167
+ ? false
168
+ : {
169
+ stroke: 'var(--control-border)',
170
+ fill: 'transparent',
171
+ }, wrapperStyle: {
172
+ position: 'static',
173
+ visibility: 'visible',
174
+ pointerEvents: 'auto',
175
+ }, content: (tooltip) => {
176
+ const row = tooltip.active && !dismissed
177
+ ? byId.get(String(tooltip.label))
178
+ : undefined;
179
+ return (_jsx("div", { className: 'pui-chart-readout', "data-chart-readout": row?.id, children: row ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsx("ul", { children: visible.map((s) => (_jsxs("li", { children: [s.label, ": ", format(row.values[s.key])] }, s.key))) })] })) : (_jsx("span", { children: text.inspectHint })) }));
180
+ } })), series.map((s, index) => {
181
+ const common = {
182
+ name: s.label,
183
+ dataKey: (row) => row.values[s.key] ?? null,
184
+ hide: hidden.has(s.key),
185
+ isAnimationActive: false,
186
+ stroke: colorFor(index),
187
+ };
188
+ return type === 'bar' ? (_jsx(Bar, { ...common, fill: `url(#${id}-pattern-${index})`, maxBarSize: 48 }, s.key)) : type === 'area' ? (_jsx(Area, { ...common, type: 'linear', strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], fill: `url(#${id}-pattern-${index})`, fillOpacity: 0.12, connectNulls: false, dot: rows.length > 40
189
+ ? false
190
+ : (dot) => (_jsx(Marker, { ...dot, color: colorFor(index), index: index })), activeDot: false }, s.key)) : (_jsx(Line, { ...common, type: 'linear', strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], connectNulls: false, dot: rows.length > 40
191
+ ? false
192
+ : (dot) => (_jsx(Marker, { ...dot, color: colorFor(index), index: index })), activeDot: false }, s.key));
193
+ }), _jsx(FloatingValues, { host: floatHost, hidden: dismissed, render: (label) => {
194
+ const row = byId.get(label);
195
+ if (!row)
196
+ return null;
197
+ return (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsx("ul", { children: series.map((s, index) => hidden.has(s.key) ? null : (_jsxs("li", { children: [_jsx(SeriesMark, { type: type, index: index, color: colorFor(index), pattern: `${id}-float-${index}` }), _jsx("span", { children: s.label }), _jsx("span", { className: 'pui-chart-tooltip-value', children: format(row.values[s.key]) })] }, s.key))) })] }));
198
+ } })] }) }), _jsx("div", { className: 'pui-chart-float', ref: setFloatHost })] }), _jsx("p", { className: 'pui-chart-axis-label pui-chart-x-label', children: xLabel })] })) : (_jsx("div", { className: 'pui-chart-empty', role: 'status', children: visible.length === 0 && series.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("p", { children: text.noSeries }), _jsx(Button, { variant: 'outline', onClick: () => {
199
+ setRestoreRequested(true);
200
+ changeHidden([]);
201
+ }, children: text.showAll })] })) : (_jsx("p", { children: text.empty })) }))] }), hasValues && (_jsx("p", { className: 'pui-chart-help', id: `${id}-keys`, children: text.keyboardHelp })), _jsx("div", { className: 'pui-chart-inspector', ref: setHost, role: 'status', "aria-live": 'polite', "aria-atomic": 'true', "aria-label": text.values(title), hidden: !hasValues }), _jsxs("details", { className: 'pui-chart-table', children: [_jsx("summary", { children: text.viewTable }), _jsx(Table.Container, { label: text.table(title), orientation: 'both', scrollChain: 'contain', children: _jsxs(Table.Root, { children: [_jsx(Table.Caption, { children: text.tableCaption(unit) }), _jsx(Table.Head, { children: _jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'col', children: xLabel }), series.map((s) => (_jsxs(Table.HeaderCell, { scope: 'col', children: [s.label, " (", unit, ")"] }, s.key)))] }) }), _jsx(Table.Body, { children: rows.length ? (rows.map((row) => (_jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'row', children: row.label }), series.map((s) => (_jsx(Table.Cell, { children: format(row.values[s.key]) }, s.key)))] }, row.id)))) : (_jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: series.length + 1, children: text.noRows }) })) })] }) })] })] }));
147
202
  });
@@ -0,0 +1,11 @@
1
+ /* Accordion, with the styles of what it renders. */
2
+ @layer surflane.tokens, surflane.themes, surflane.base, surflane.components,
3
+ surflane.sizes;
4
+ @import '@surflane/design-tokens/tokens.css';
5
+ @import '@surflane/design-tokens/presentation/base.css';
6
+ @import './rules/base.css';
7
+ @import './rules/sizing.css';
8
+ @import './rules/icon.css';
9
+ @import './rules/button.css';
10
+ @import './rules/disclosure.css';
11
+ @import './rules/accordion.css';
@@ -0,0 +1,11 @@
1
+ /* Alert Dialog, with the styles of what it renders. */
2
+ @layer surflane.tokens, surflane.themes, surflane.base, surflane.components,
3
+ surflane.sizes;
4
+ @import '@surflane/design-tokens/tokens.css';
5
+ @import '@surflane/design-tokens/presentation/base.css';
6
+ @import './rules/base.css';
7
+ @import './rules/sizing.css';
8
+ @import './rules/icon.css';
9
+ @import '@surflane/design-tokens/controls.css';
10
+ @import './rules/dialog.css';
11
+ @import './rules/alert-dialog.css';
@@ -1,6 +1,6 @@
1
1
  /* Every Surflane stylesheet in one import, in the recommended order: tokens, controls,
2
2
  * composition, document typography and components. Each file keeps its cascade layer. */
3
- @layer surflane.tokens, surflane.themes, surflane.base, surflane.components;
3
+ @layer surflane.tokens, surflane.themes, surflane.base, surflane.components, surflane.sizes;
4
4
  @import '@surflane/design-tokens/tokens.css';
5
5
  @import '@surflane/design-tokens/controls.css';
6
6
  @import '@surflane/design-tokens/foundations.css';
@@ -0,0 +1,12 @@
1
+ /* App shell / Sidebar, with the styles of what it renders. */
2
+ @layer surflane.tokens, surflane.themes, surflane.base, surflane.components,
3
+ surflane.sizes;
4
+ @import '@surflane/design-tokens/tokens.css';
5
+ @import '@surflane/design-tokens/presentation/base.css';
6
+ @import './rules/base.css';
7
+ @import './rules/sizing.css';
8
+ @import './rules/popup.css';
9
+ @import './rules/icon.css';
10
+ @import './rules/tooltip.css';
11
+ @import './rules/button.css';
12
+ @import './rules/app-shell.css';