atlas-react-kit 0.12.0 → 1.0.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 (98) hide show
  1. package/COMPONENTS.md +201 -100
  2. package/DESIGN_RULES.md +44 -1
  3. package/README.md +10 -9
  4. package/dist/accordion.d.ts +1 -0
  5. package/dist/alert.d.ts +1 -1
  6. package/dist/avatar.d.ts +1 -0
  7. package/dist/badge.d.ts +4 -5
  8. package/dist/breadcrumb.d.ts +2 -3
  9. package/dist/button.d.ts +4 -5
  10. package/dist/calendar.d.ts +2 -0
  11. package/dist/card.d.ts +3 -0
  12. package/dist/chart.d.ts +152 -0
  13. package/dist/chart.js +177 -0
  14. package/dist/color-legend.d.ts +8 -0
  15. package/dist/combobox.d.ts +13 -4
  16. package/dist/combobox.js +1 -8
  17. package/dist/date-picker.d.ts +16 -6
  18. package/dist/date-picker.js +13 -20
  19. package/dist/date-time-picker.d.ts +5 -2
  20. package/dist/date-time-picker.js +6 -5
  21. package/dist/dialog.d.ts +1 -0
  22. package/dist/dialog.js +4 -2
  23. package/dist/dropdown-menu.d.ts +9 -8
  24. package/dist/empty-panel.d.ts +8 -2
  25. package/dist/empty-panel.js +2 -2
  26. package/dist/error-boundary.d.ts +7 -2
  27. package/dist/field.d.ts +4 -1
  28. package/dist/field.js +1 -1
  29. package/dist/form/checkbox-field.d.ts +45 -0
  30. package/dist/form/checkbox-field.js +39 -0
  31. package/dist/form/combobox-field.d.ts +13 -0
  32. package/dist/form/combobox-field.js +14 -0
  33. package/dist/form/control-field.d.ts +30 -0
  34. package/dist/form/control-field.js +22 -0
  35. package/dist/form/date-picker-field.d.ts +7 -21
  36. package/dist/form/date-picker-field.js +4 -4
  37. package/dist/form/date-time-picker-field.d.ts +7 -22
  38. package/dist/form/date-time-picker-field.js +3 -3
  39. package/dist/form/feedback-field.d.ts +2 -0
  40. package/dist/form/field-error.d.ts +49 -0
  41. package/dist/form/field-shell.d.ts +47 -8
  42. package/dist/form/field-shell.js +24 -7
  43. package/dist/form/markdown-field.d.ts +4 -16
  44. package/dist/form/markdown-field.js +2 -2
  45. package/dist/form/select-field.d.ts +7 -7
  46. package/dist/form/select-field.js +2 -2
  47. package/dist/form/text-field.d.ts +29 -12
  48. package/dist/form/text-field.js +10 -6
  49. package/dist/form/toggle-group-field.d.ts +30 -0
  50. package/dist/form/toggle-group-field.js +19 -0
  51. package/dist/info-tip.d.ts +10 -1
  52. package/dist/internal/as-child.d.ts +8 -0
  53. package/dist/internal/as-child.js +1 -0
  54. package/dist/internal/date-like.d.ts +23 -0
  55. package/dist/internal/form-control.d.ts +3 -2
  56. package/dist/internal/form-control.js +3 -2
  57. package/dist/internal/kanban-layout.d.ts +55 -0
  58. package/dist/internal/kanban-layout.js +80 -0
  59. package/dist/internal/kanban-plugins.d.ts +17 -0
  60. package/dist/internal/kanban-plugins.js +17 -0
  61. package/dist/internal/use-media-query.d.ts +4 -2
  62. package/dist/internal/use-media-query.js +11 -15
  63. package/dist/kanban.d.ts +84 -0
  64. package/dist/kanban.js +197 -0
  65. package/dist/markdown.d.ts +2 -0
  66. package/dist/meter.d.ts +7 -0
  67. package/dist/nav-link.d.ts +23 -0
  68. package/dist/nav-link.js +31 -0
  69. package/dist/next/nav-link.d.ts +11 -0
  70. package/dist/next/nav-link.js +14 -0
  71. package/dist/next/period-stepper.d.ts +11 -0
  72. package/dist/next/period-stepper.js +16 -0
  73. package/dist/page-header.d.ts +19 -0
  74. package/dist/page-header.js +11 -0
  75. package/dist/pagination.d.ts +2 -0
  76. package/dist/period-stepper.d.ts +32 -0
  77. package/dist/period-stepper.js +27 -0
  78. package/dist/picker-shell.d.ts +43 -6
  79. package/dist/picker-shell.js +2 -2
  80. package/dist/progress.d.ts +1 -0
  81. package/dist/search-picker.d.ts +12 -0
  82. package/dist/select.d.ts +1 -0
  83. package/dist/skip-link.d.ts +7 -0
  84. package/dist/skip-link.js +10 -0
  85. package/dist/slider.d.ts +11 -0
  86. package/dist/sortable-table-head.d.ts +2 -0
  87. package/dist/stat-tile.d.ts +5 -0
  88. package/dist/table-body-skeleton.d.ts +2 -0
  89. package/dist/table.d.ts +1 -0
  90. package/dist/tabs.d.ts +9 -5
  91. package/dist/tabs.js +1 -1
  92. package/dist/toast.d.ts +7 -1
  93. package/dist/toggle-group.d.ts +9 -2
  94. package/dist/toggle-group.js +2 -1
  95. package/dist/toggle.d.ts +1 -1
  96. package/package.json +85 -11
  97. package/skills/atlas-react-kit/SKILL.md +2 -0
  98. package/theme.css +11 -0
package/DESIGN_RULES.md CHANGED
@@ -32,6 +32,36 @@ its own soft tint in both themes. Status tones have no paired foreground token;
32
32
  avoid placing text on solid status fills. Application-specific identity colors
33
33
  belong to the application.
34
34
 
35
+ ### Public tokens and slots
36
+
37
+ These are public API from 1.0, so apps may use them and a rename or removal
38
+ is a breaking change:
39
+
40
+ - Color tokens and their `--color-*` utilities: `background`, `foreground`,
41
+ `card`, `popover`, `primary`, `secondary`, `muted`, `accent`, `selected`
42
+ (each with its `-foreground` pair where one exists), `destructive`,
43
+ `success`, `warning`, `border`, `input`, `ring`, `chart-1` to `chart-5`,
44
+ and the `sidebar` set.
45
+ - `--radius` and the `--radius-*` scale, and `--font-heading`.
46
+ - Every `data-slot` listed under "Styling parts" in
47
+ [COMPONENTS.md](./COMPONENTS.md#styling-parts).
48
+
49
+ Everything else in `theme.css` is internal and may change in a minor
50
+ release: the scroll-shadow variables (`--scroll-fade`, `--scroll-fill`,
51
+ `--table-scroll-fade`, `--tabs-scroll-fade`) and any `data-slot` not listed
52
+ in COMPONENTS.md.
53
+
54
+ ### Prop names
55
+
56
+ - A controlled value is `value` with `onValueChange`; open state is `open`
57
+ with `onOpenChange`; a checked state is `checked` with `onCheckedChange`.
58
+ - Sizes run `sm`, `default`, `lg`; `Button` adds `xs` and `icon-*` sizes.
59
+ - `variant="default"` is the plain look; other variants name what they mean
60
+ (`outline`, `ghost`, `destructive`).
61
+
62
+ Deprecated aliases (`onChange` on the date pickers, `EmptyPanel`
63
+ `size="md"`) keep working until 2.0.
64
+
35
65
  ## Accessibility
36
66
 
37
67
  The target is WCAG 2.2 AA. CI runs axe-core's WCAG 2.2 A and AA rules against
@@ -138,9 +168,22 @@ or its `data-slot`.
138
168
  `text-base md:text-sm`, `border-input`, and `shadow-xs`. Do not restyle those
139
169
  triggers as outline buttons in application code.
140
170
 
171
+ `form/*` fields stack label, hint, control, and error with `gap-1.5`, and the
172
+ error sits below the control. It takes no space until it shows, so a row of
173
+ fields keeps its controls on one top edge only with `items-start`
174
+ (`grid grid-cols-2 items-start gap-3`); the default `stretch` is fine for a
175
+ single column. Wrap a control the kit does not ship (a combobox, a segmented
176
+ picker) in `FormControlField` instead of `Field` and `FieldLabel`, whose
177
+ `gap-3` spacing does not match. A field shows its error once
178
+ `meta.isTouched` is true or `submitted` is set, never on a field the person
179
+ has not reached yet.
180
+
141
181
  Use `Tabs` with actual `TabsContent` panels. For URL view switches, use a labelled
142
182
  `TabsNav` with links and `aria-current="page"` on the active link. The optional
143
- Next adapter provides `TabsNavLink` with this behavior. `TabsNav` and `TabsList`
183
+ Next adapter provides `TabsNavLink` with this behavior. App-shell destinations
184
+ (Calendar, Tasks, Settings) use `NavLink` from `nav-link`: a pill with `active`
185
+ from the app, not tabs. Pass `asChild` for a router link, or use the optional
186
+ Next adapter `next/nav-link`. `TabsNav` and `TabsList`
144
187
  scroll overflowing labels inside themselves (`overflow-x-auto overflow-y-hidden`,
145
188
  with padding for the active border and focus ring counted in the scroller
146
189
  height, and an edge fade like `Table`). Do not wrap them in another
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Atlas React Kit
2
2
 
3
3
  Shared React components and a Tailwind theme with square corners, semantic
4
- colors, and distinct selected states. 0.6.0 added generic pieces apps were
5
- keeping locally (`InfoTip`, `ColorLegend`, `SearchPicker`, `Markdown`,
6
- `StatTile`, `Meter`, `Slider`) plus the ToggleGroup selected-state fix.
4
+ colors, and distinct selected states. From 1.0 the API is stable: breaking
5
+ changes to stable components only ship in a major release (see
6
+ [CONTRIBUTING.md](./CONTRIBUTING.md#versions-and-releases)).
7
7
  React 19, Tailwind 4, and Node >=20.19 are required. Next 16 is needed only
8
- when using `next/tabs-nav-link`.
8
+ when using `next/nav-link`, `next/period-stepper`, or `next/tabs-nav-link`.
9
9
 
10
10
  ```sh
11
- npm install --save-exact atlas-react-kit@0.12.0
11
+ npm install --save-exact atlas-react-kit@1.0.0
12
12
  ```
13
13
 
14
14
  Import components by subpath:
@@ -42,8 +42,9 @@ this example assumes it is one directory below the project root.
42
42
  ```
43
43
 
44
44
  The theme includes animations and shared light/dark tokens. Wrap the tree with
45
- `ThemeProvider` from `atlas-react-kit/theme-provider`, or set the `dark` class
46
- on an ancestor to activate dark colors. Supply your own font configuration;
45
+ `ThemeProvider` from `atlas-react-kit/theme-provider` (install
46
+ `next-themes@^0.4.6` first), or set the `dark` class on an ancestor to activate
47
+ dark colors. Supply your own font configuration;
47
48
  `font-heading` uses the configured sans font. Application-specific tokens stay
48
49
  with the application.
49
50
 
@@ -101,7 +102,7 @@ The public site is Storybook in `site/`. `npm run build --prefix site`
101
102
  typechecks the stories, builds Storybook into `site/dist`, then checks the
102
103
  static output. Vercel serves that directory; `/playground` and `/storybook`
103
104
  redirect to `/`. The site and Storybook dependencies are not part of the npm
104
- package. Optional feature libraries (markdown, dates, toasts, theme, Next) are
105
+ package. Optional feature libraries (markdown, dates, toasts, theme, Next, dnd-kit) are
105
106
  peer dependencies and are required only when those entry points are imported.
106
107
  The package does not bundle React.
107
108
 
@@ -128,7 +129,7 @@ artifact with the package owner's npm login:
128
129
  ```sh
129
130
  mkdir -p release-artifacts
130
131
  npm pack --pack-destination release-artifacts
131
- npm publish ./release-artifacts/atlas-react-kit-0.12.0.tgz --access public
132
+ npm publish ./release-artifacts/atlas-react-kit-1.0.0.tgz --access public
132
133
  ```
133
134
 
134
135
  Pin an exact package version in consuming applications. Roll back an upgrade by
@@ -5,6 +5,7 @@ declare function Accordion({ ...props }: React.ComponentProps<typeof AccordionPr
5
5
  declare function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>): React.JSX.Element;
6
6
  /** `headerClassName` styles the heading wrapper (`data-slot="accordion-header"`). */
7
7
  declare function AccordionTrigger({ className, headerClassName, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger> & {
8
+ /** Classes for the heading wrapper around the trigger. */
8
9
  headerClassName?: string;
9
10
  }): React.JSX.Element;
10
11
  /**
package/dist/alert.d.ts CHANGED
@@ -6,7 +6,7 @@ import { type VariantProps } from "class-variance-authority";
6
6
  * (the destructive tint is too faint for text in light mode).
7
7
  */
8
8
  declare const alertVariants: (props?: ({
9
- variant?: "default" | "success" | "warning" | "destructive" | null | undefined;
9
+ variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
10
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
11
  /**
12
12
  * Inline message about the page or a section (save failed, trial ending).
package/dist/avatar.d.ts CHANGED
@@ -6,6 +6,7 @@ import { Avatar as AvatarPrimitive } from "radix-ui";
6
6
  * replaces it.
7
7
  */
8
8
  declare function Avatar({ className, size, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
9
+ /** `sm` 24px, `default` 32px, `lg` 40px. Default `default`. */
9
10
  size?: "sm" | "default" | "lg";
10
11
  }): React.JSX.Element;
11
12
  declare function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>): React.JSX.Element;
package/dist/badge.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
+ import type { AsChildProps } from "./internal/as-child.js";
3
4
  declare const badgeVariants: (props?: ({
4
- variant?: "default" | "link" | "destructive" | "secondary" | "outline" | "ghost" | "soft" | null | undefined;
5
- tone?: "success" | "warning" | "destructive" | "neutral" | null | undefined;
5
+ variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | "soft" | null | undefined;
6
+ tone?: "destructive" | "neutral" | "success" | "warning" | null | undefined;
6
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
- declare function Badge({ className, variant, tone, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
8
- asChild?: boolean;
9
- }): React.JSX.Element;
8
+ declare function Badge({ className, variant, tone, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & AsChildProps): React.JSX.Element;
10
9
  export { Badge, badgeVariants };
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import type { AsChildProps } from "./internal/as-child.js";
2
3
  /**
3
4
  * Where the current page sits in a hierarchy. The last item is
4
5
  * `BreadcrumbPage` (plain text with `aria-current="page"`), not a link.
@@ -7,9 +8,7 @@ declare function Breadcrumb({ ...props }: React.ComponentProps<"nav">): React.JS
7
8
  declare function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">): React.JSX.Element;
8
9
  declare function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">): React.JSX.Element;
9
10
  /** Pass `asChild` to render a router link (e.g. Next `Link`) with these styles. */
10
- declare function BreadcrumbLink({ asChild, className, ...props }: React.ComponentProps<"a"> & {
11
- asChild?: boolean;
12
- }): React.JSX.Element;
11
+ declare function BreadcrumbLink({ asChild, className, ...props }: React.ComponentProps<"a"> & AsChildProps): React.JSX.Element;
13
12
  declare function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
14
13
  declare function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">): React.JSX.Element;
15
14
  declare function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
package/dist/button.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
+ import type { AsChildProps } from "./internal/as-child.js";
3
4
  /**
4
5
  * Outline and ghost hover/focus: selected-foreground border, selected wash, and
5
6
  * `selected-foreground` text (primary in light mode, a lighter purple that
@@ -8,10 +9,8 @@ import { type VariantProps } from "class-variance-authority";
8
9
  */
9
10
  declare const quietButtonInteraction: string;
10
11
  declare const buttonVariants: (props?: ({
11
- variant?: "default" | "link" | "destructive" | "secondary" | "outline" | "ghost" | null | undefined;
12
- size?: "default" | "sm" | "lg" | "xs" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
12
+ variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
13
+ size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
13
14
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
14
- declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
15
- asChild?: boolean;
16
- }): React.JSX.Element;
15
+ declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & AsChildProps): React.JSX.Element;
17
16
  export { Button, buttonVariants, quietButtonInteraction };
@@ -2,9 +2,11 @@ import * as React from "react";
2
2
  import { DayPicker, type DayButton, type Locale } from "react-day-picker";
3
3
  import { Button } from "./button.js";
4
4
  declare function Calendar({ className, classNames, showOutsideDays, captionLayout, buttonVariant, locale, formatters, labels, components, ...props }: React.ComponentProps<typeof DayPicker> & {
5
+ /** Button variant for the previous / next month buttons. Default `ghost`. */
5
6
  buttonVariant?: React.ComponentProps<typeof Button>["variant"];
6
7
  }): React.JSX.Element;
7
8
  declare function CalendarDayButton({ className, day, modifiers, locale, ...props }: React.ComponentProps<typeof DayButton> & {
9
+ /** Locale for the day's `data-day` date string. */
8
10
  locale?: Partial<Locale>;
9
11
  }): React.JSX.Element;
10
12
  export { Calendar, CalendarDayButton };
package/dist/card.d.ts CHANGED
@@ -8,11 +8,14 @@ import * as React from "react";
8
8
  * scroll instead of widening the card. `overflow-hidden` still clips media.
9
9
  */
10
10
  declare function Card({ className, size, flush, role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...props }: React.ComponentProps<"div"> & {
11
+ /** `sm` tightens padding and gaps. Default `default`. */
11
12
  size?: "default" | "sm";
13
+ /** Remove the padding so lists and tables run edge to edge. Default `false`. */
12
14
  flush?: boolean;
13
15
  }): React.JSX.Element;
14
16
  declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
15
17
  declare function CardTitle({ as: Component, className, ...props }: React.ComponentProps<"div"> & {
18
+ /** Element to render, e.g. a heading level for the outline. Default `div`. */
16
19
  as?: "div" | "h1" | "h2" | "h3" | "h4";
17
20
  }): React.JSX.Element;
18
21
  declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
@@ -0,0 +1,152 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Building blocks for small SVG charts: a plot frame in viewBox units, axis
4
+ * math, a gridline axis, a tooltip, a legend, and pointer/arrow-key
5
+ * scrubbing. The chart itself (bars, lines, what they mean) stays in the
6
+ * app; these pieces keep every chart's axis, readout, and keyboard use alike.
7
+ */
8
+ /** Space around the plot, in viewBox units, for axis labels. */
9
+ export type ChartPadding = {
10
+ /** Above the plot. Default 10. */
11
+ top: number;
12
+ /** Right of the plot. Default 12. */
13
+ right: number;
14
+ /** Below the baseline, for x labels. Default 20. */
15
+ bottom: number;
16
+ /** Left of the plot, for y labels. Default 40. */
17
+ left: number;
18
+ };
19
+ /** A chart's viewBox and the plot box inside it, in viewBox units. */
20
+ export type ChartFrame = {
21
+ /** The whole chart's width. */
22
+ width: number;
23
+ /** The whole chart's height. */
24
+ height: number;
25
+ /** The `viewBox` attribute for the chart's `<svg>`. */
26
+ viewBox: string;
27
+ /** The plot's left edge on x. */
28
+ left: number;
29
+ /** The plot's right edge on x. */
30
+ right: number;
31
+ /** The plot's top edge on y. */
32
+ top: number;
33
+ /** The plot's bottom edge on y: the baseline, where 0 sits. */
34
+ bottom: number;
35
+ /** `right - left`. */
36
+ plotWidth: number;
37
+ /** `bottom - top`. */
38
+ plotHeight: number;
39
+ };
40
+ /**
41
+ * Frame for a chart `height` units tall. Charts that share a `width` and
42
+ * padding line up when stacked. The SVG scales to its box, so these are
43
+ * viewBox units, not pixels.
44
+ */
45
+ export declare function chartFrame(height: number, { width, padding, }?: {
46
+ /** The chart's width in viewBox units. Default 640. */
47
+ width?: number;
48
+ /** Overrides for any side of the default padding. */
49
+ padding?: Partial<ChartPadding>;
50
+ }): ChartFrame;
51
+ /**
52
+ * Round `max` up to a clean axis ceiling (1, 2, 2.5, 4, or 5 × 10^n). The 4
53
+ * step keeps a value just past 25 off a half-empty 50 axis.
54
+ */
55
+ export declare function niceCeil(max: number): number;
56
+ /** `count` even steps from 0 to `niceCeil(max)`, both ends included. */
57
+ export declare function niceTicks(max: number, count?: number): number[];
58
+ /** Linear scale from [0, domainMax] onto [0, rangeMax]; a zero domain maps to 0. */
59
+ export declare function scaleLinear(domainMax: number, rangeMax: number): (value: number) => number;
60
+ /** SVG path through the points (`M x y L x y …`); empty for no points. */
61
+ export declare function linePath(points: ReadonlyArray<{
62
+ x: number;
63
+ y: number;
64
+ }>): string;
65
+ /**
66
+ * A bar with a rounded data end and a square baseline. (x, y) is the top
67
+ * left; the bar grows down to y + h. Pass `r = 0` for a square bar.
68
+ */
69
+ export declare function roundedTopBarPath(x: number, y: number, w: number, h: number, r?: number): string;
70
+ /** Short axis label: 1500 → "1.5k", 2000000 → "2M". */
71
+ export declare function compactNumber(n: number): string;
72
+ type IndexRange = {
73
+ /** The first data index the plot can pick. */
74
+ min: number;
75
+ /** The last data index the plot can pick; may stop short of the axis end. */
76
+ max: number;
77
+ };
78
+ /** The index range plus how many intervals the plot's width spans. */
79
+ type PlotSteps = IndexRange & {
80
+ /** Intervals across the plot width (data points minus one on a full axis). */
81
+ steps: number;
82
+ };
83
+ /**
84
+ * Data index at viewBox x `viewX`. The plot spans `steps` intervals from
85
+ * `min`; the result is clamped to [min, max], so `max` can stop short of the
86
+ * axis (days not reached yet).
87
+ */
88
+ export declare function indexAtPlotX(viewX: number, frame: ChartFrame, { steps, min, max }: PlotSteps): number;
89
+ /** Arrow-key move by `delta`, clamped; with nothing picked it starts at `min`. */
90
+ export declare function stepIndex(current: number | null, delta: 1 | -1, { min, max }: IndexRange): number;
91
+ /**
92
+ * Crosshair state: the pointer or the arrow keys pick a data index in
93
+ * [min, max], and Escape or leaving the plot clears it. Put `svgRef` and the
94
+ * handlers on the chart's `<svg>`, which needs `tabIndex={0}`.
95
+ */
96
+ export declare function useChartScrub({ frame, steps, min, max, }: PlotSteps & {
97
+ /** The chart's frame, for mapping pointer x onto the plot. */
98
+ frame: ChartFrame;
99
+ }): {
100
+ svgRef: React.RefObject<SVGSVGElement | null>;
101
+ index: number | null;
102
+ clear: () => void;
103
+ onPointerMove: (event: React.PointerEvent<SVGSVGElement>) => void;
104
+ onKeyDown: (event: React.KeyboardEvent) => void;
105
+ };
106
+ /**
107
+ * Horizontal gridlines from 0 to `niceCeil(yMax)` with labels in the left
108
+ * padding. Render inside the chart's `<svg>`.
109
+ */
110
+ export declare function ChartYAxisGrid({ frame, yMax, tickCount, format, }: {
111
+ /** The chart's frame; lines span the plot, labels sit in the left padding. */
112
+ frame: ChartFrame;
113
+ /** The largest data value; the axis tops out at `niceCeil(yMax)`. */
114
+ yMax: number;
115
+ /** How many even steps from 0 to the top. Default 4. */
116
+ tickCount?: number;
117
+ /** Label text for a tick value. Default `compactNumber`. */
118
+ format?: (value: number) => string;
119
+ }): React.JSX.Element;
120
+ /**
121
+ * Readout for the hovered or focused point, placed in percent of the chart
122
+ * box (its wrapper must be `relative`). It sits right of the point and
123
+ * flips left past 55% so it does not clip at the edge.
124
+ */
125
+ export declare function ChartTooltip({ xPct, yPct, className, style, children, ...props }: {
126
+ /** The point's x, in percent of the chart box's width. */
127
+ xPct: number;
128
+ /** The point's y, in percent of the chart box's height; capped at 60. */
129
+ yPct: number;
130
+ } & React.ComponentProps<"div">): React.JSX.Element;
131
+ /** A series' line key, shared by tooltip rows and legend keys. */
132
+ type SeriesKeyProps = {
133
+ /** The series name. */
134
+ label: React.ReactNode;
135
+ /** The series stroke, e.g. `stroke-primary`. */
136
+ swatchClassName: string;
137
+ /** Draws the swatch dashed, for a dashed series line. */
138
+ dashed?: boolean;
139
+ };
140
+ /** Tooltip row: the value leads, the label follows, keyed by the series stroke. */
141
+ export declare function ChartTooltipRow({ label, value, swatchClassName, dashed, }: SeriesKeyProps & {
142
+ /** The series' value at the point, shown first in bold. */
143
+ value: React.ReactNode;
144
+ }): React.JSX.Element;
145
+ /** Row of legend keys under a chart. */
146
+ export declare function ChartLegend({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
147
+ /** One legend entry: a solid or dashed line swatch and its label. */
148
+ export declare function ChartLegendKey({ label, swatchClassName, dashed, className, }: SeriesKeyProps & {
149
+ /** Classes for the key's wrapper. */
150
+ className?: string;
151
+ }): React.JSX.Element;
152
+ export {};
package/dist/chart.js ADDED
@@ -0,0 +1,177 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { clamp } from "./internal/math.js";
5
+ import { overlaySurface } from "./internal/overlay-styles.js";
6
+ import { cn } from "./utils.js";
7
+ /** Room for compact y labels on the left and x labels underneath. */
8
+ const defaultPadding = {
9
+ top: 10,
10
+ right: 12,
11
+ bottom: 20,
12
+ left: 40,
13
+ };
14
+ /**
15
+ * Frame for a chart `height` units tall. Charts that share a `width` and
16
+ * padding line up when stacked. The SVG scales to its box, so these are
17
+ * viewBox units, not pixels.
18
+ */
19
+ export function chartFrame(height, { width = 640, padding, } = {}) {
20
+ const pad = { ...defaultPadding, ...padding };
21
+ const plotWidth = width - pad.left - pad.right;
22
+ const plotHeight = height - pad.top - pad.bottom;
23
+ return {
24
+ width,
25
+ height,
26
+ viewBox: `0 0 ${width} ${height}`,
27
+ left: pad.left,
28
+ right: pad.left + plotWidth,
29
+ top: pad.top,
30
+ bottom: pad.top + plotHeight,
31
+ plotWidth,
32
+ plotHeight,
33
+ };
34
+ }
35
+ /**
36
+ * Round `max` up to a clean axis ceiling (1, 2, 2.5, 4, or 5 × 10^n). The 4
37
+ * step keeps a value just past 25 off a half-empty 50 axis.
38
+ */
39
+ export function niceCeil(max) {
40
+ if (max <= 0)
41
+ return 1;
42
+ const pow = 10 ** Math.floor(Math.log10(max));
43
+ for (const step of [1, 2, 2.5, 4, 5]) {
44
+ if (max <= step * pow)
45
+ return step * pow;
46
+ }
47
+ return 10 * pow;
48
+ }
49
+ /** `count` even steps from 0 to `niceCeil(max)`, both ends included. */
50
+ export function niceTicks(max, count = 4) {
51
+ const top = niceCeil(max);
52
+ return Array.from({ length: count + 1 }, (_, i) => (top / count) * i);
53
+ }
54
+ /** Linear scale from [0, domainMax] onto [0, rangeMax]; a zero domain maps to 0. */
55
+ export function scaleLinear(domainMax, rangeMax) {
56
+ return (value) => domainMax === 0 ? 0 : (value / domainMax) * rangeMax;
57
+ }
58
+ function round2(n) {
59
+ return Math.round(n * 100) / 100;
60
+ }
61
+ /** SVG path through the points (`M x y L x y …`); empty for no points. */
62
+ export function linePath(points) {
63
+ return points
64
+ .map((p, i) => `${i === 0 ? "M" : "L"} ${round2(p.x)} ${round2(p.y)}`)
65
+ .join(" ");
66
+ }
67
+ /**
68
+ * A bar with a rounded data end and a square baseline. (x, y) is the top
69
+ * left; the bar grows down to y + h. Pass `r = 0` for a square bar.
70
+ */
71
+ export function roundedTopBarPath(x, y, w, h, r = 4) {
72
+ const rad = Math.min(r, w / 2, h);
73
+ return [
74
+ `M ${round2(x)} ${round2(y + h)}`,
75
+ `L ${round2(x)} ${round2(y + rad)}`,
76
+ `Q ${round2(x)} ${round2(y)} ${round2(x + rad)} ${round2(y)}`,
77
+ `L ${round2(x + w - rad)} ${round2(y)}`,
78
+ `Q ${round2(x + w)} ${round2(y)} ${round2(x + w)} ${round2(y + rad)}`,
79
+ `L ${round2(x + w)} ${round2(y + h)}`,
80
+ "Z",
81
+ ].join(" ");
82
+ }
83
+ function trimZero(n) {
84
+ const fixed = n.toFixed(1);
85
+ return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
86
+ }
87
+ /** Short axis label: 1500 → "1.5k", 2000000 → "2M". */
88
+ export function compactNumber(n) {
89
+ const abs = Math.abs(n);
90
+ if (abs >= 1_000_000)
91
+ return `${trimZero(n / 1_000_000)}M`;
92
+ if (abs >= 1_000)
93
+ return `${trimZero(n / 1_000)}k`;
94
+ return String(Math.round(n));
95
+ }
96
+ /**
97
+ * Data index at viewBox x `viewX`. The plot spans `steps` intervals from
98
+ * `min`; the result is clamped to [min, max], so `max` can stop short of the
99
+ * axis (days not reached yet).
100
+ */
101
+ export function indexAtPlotX(viewX, frame, { steps, min, max }) {
102
+ const index = Math.round(((viewX - frame.left) / frame.plotWidth) * steps) + min;
103
+ return clamp(index, min, max);
104
+ }
105
+ /** Arrow-key move by `delta`, clamped; with nothing picked it starts at `min`. */
106
+ export function stepIndex(current, delta, { min, max }) {
107
+ if (current === null)
108
+ return min;
109
+ return clamp(current + delta, min, max);
110
+ }
111
+ /**
112
+ * Crosshair state: the pointer or the arrow keys pick a data index in
113
+ * [min, max], and Escape or leaving the plot clears it. Put `svgRef` and the
114
+ * handlers on the chart's `<svg>`, which needs `tabIndex={0}`.
115
+ */
116
+ export function useChartScrub({ frame, steps, min, max, }) {
117
+ const svgRef = React.useRef(null);
118
+ const [index, setIndex] = React.useState(null);
119
+ const clear = React.useCallback(() => setIndex(null), []);
120
+ const onPointerMove = React.useCallback((event) => {
121
+ const rect = svgRef.current?.getBoundingClientRect();
122
+ if (!rect)
123
+ return;
124
+ const viewX = ((event.clientX - rect.left) / rect.width) * frame.width;
125
+ setIndex(indexAtPlotX(viewX, frame, { steps, min, max }));
126
+ }, [frame, steps, min, max]);
127
+ const onKeyDown = React.useCallback((event) => {
128
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
129
+ const delta = event.key === "ArrowRight" ? 1 : -1;
130
+ setIndex((current) => stepIndex(current, delta, { min, max }));
131
+ event.preventDefault();
132
+ }
133
+ else if (event.key === "Escape") {
134
+ setIndex(null);
135
+ }
136
+ }, [min, max]);
137
+ return { svgRef, index, clear, onPointerMove, onKeyDown };
138
+ }
139
+ /**
140
+ * Horizontal gridlines from 0 to `niceCeil(yMax)` with labels in the left
141
+ * padding. Render inside the chart's `<svg>`.
142
+ */
143
+ export function ChartYAxisGrid({ frame, yMax, tickCount = 4, format = compactNumber, }) {
144
+ const ticks = niceTicks(yMax, tickCount);
145
+ const top = ticks[ticks.length - 1] ?? 1;
146
+ const y = (value) => frame.bottom - (value / top) * frame.plotHeight;
147
+ return (_jsx("g", { "data-slot": "chart-y-axis", children: ticks.map((tick) => (_jsxs("g", { children: [_jsx("line", { x1: frame.left, y1: y(tick), x2: frame.right, y2: y(tick), strokeWidth: 1, className: "stroke-border" }), _jsx("text", { x: frame.left - 6, y: y(tick) + 3, textAnchor: "end", className: "fill-muted-foreground text-[10px] tabular-nums", children: format(tick) })] }, tick))) }));
148
+ }
149
+ /**
150
+ * Readout for the hovered or focused point, placed in percent of the chart
151
+ * box (its wrapper must be `relative`). It sits right of the point and
152
+ * flips left past 55% so it does not clip at the edge.
153
+ */
154
+ export function ChartTooltip({ xPct, yPct, className, style, children, ...props }) {
155
+ const flip = xPct > 55;
156
+ return (_jsx("div", { role: "status", "data-slot": "chart-tooltip", className: cn(overlaySurface, "pointer-events-none absolute z-10 min-w-32 px-2.5 py-2 text-xs", className), style: {
157
+ left: `${xPct}%`,
158
+ top: `${Math.min(yPct, 60)}%`,
159
+ transform: flip ? "translateX(calc(-100% - 12px))" : "translateX(12px)",
160
+ ...style,
161
+ }, ...props, children: children }));
162
+ }
163
+ function SeriesSwatch({ width, strokeWidth, dashed, className, }) {
164
+ return (_jsx("svg", { width: width, height: "4", "aria-hidden": true, className: "shrink-0", children: _jsx("line", { x1: "0", y1: "2", x2: width, y2: "2", strokeWidth: strokeWidth, strokeDasharray: dashed ? "3 2" : undefined, className: className }) }));
165
+ }
166
+ /** Tooltip row: the value leads, the label follows, keyed by the series stroke. */
167
+ export function ChartTooltipRow({ label, value, swatchClassName, dashed, }) {
168
+ return (_jsxs("div", { "data-slot": "chart-tooltip-row", className: "flex items-center gap-2 py-0.5", children: [_jsx(SeriesSwatch, { width: 14, strokeWidth: 2.5, dashed: dashed, className: swatchClassName }), _jsx("span", { className: "font-semibold tabular-nums", children: value }), _jsx("span", { className: "text-muted-foreground", children: label })] }));
169
+ }
170
+ /** Row of legend keys under a chart. */
171
+ export function ChartLegend({ className, ...props }) {
172
+ return (_jsx("div", { "data-slot": "chart-legend", className: cn("flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", className), ...props }));
173
+ }
174
+ /** One legend entry: a solid or dashed line swatch and its label. */
175
+ export function ChartLegendKey({ label, swatchClassName, dashed, className, }) {
176
+ return (_jsxs("span", { "data-slot": "chart-legend-key", className: cn("inline-flex items-center gap-1.5", className), children: [_jsx(SeriesSwatch, { width: 16, strokeWidth: 2, dashed: dashed, className: swatchClassName }), label] }));
177
+ }
@@ -2,15 +2,23 @@ import type * as React from "react";
2
2
  export type ColorLegendItem = {
3
3
  /** CSS color from the app (map bins, status scales). The kit does not own it. */
4
4
  color: string;
5
+ /** Value range for the swatch, such as `0–10`, shown before the label. */
5
6
  range?: string;
7
+ /** What the color means. */
6
8
  label: string;
7
9
  };
8
10
  export type ColorLegendProps = Omit<React.ComponentProps<"figure">, "children" | "title"> & {
11
+ /** Heading above the list, drawn as the figure's caption. */
9
12
  title?: string;
13
+ /** One row per color, in display order. */
10
14
  items: ColorLegendItem[];
15
+ /** A small muted note under the list, such as the data source. */
11
16
  caption?: React.ReactNode;
17
+ /** Extra content after the caption. */
12
18
  children?: React.ReactNode;
19
+ /** Names the legend when there is no `title`. */
13
20
  "aria-label"?: string;
21
+ /** Classes for the figure, e.g. overlay position and card fill. */
14
22
  className?: string;
15
23
  };
16
24
  /**
@@ -1,29 +1,38 @@
1
1
  import * as React from "react";
2
+ import type { SearchPickerProps } from "./search-picker.js";
2
3
  export type ComboboxOption = {
4
+ /** What `onValueChange` reports and `value` matches; unique in the list. */
3
5
  value: string;
6
+ /** The visible text, on the trigger when selected and in the list. */
4
7
  label: string;
5
8
  };
6
9
  type ComboboxProps = {
10
+ /** Every choice, in display order. */
7
11
  options: ComboboxOption[];
12
+ /** The selected option's value; the trigger shows its label. */
8
13
  value?: string;
14
+ /** Called with the chosen (or created) value; the popover then closes. */
9
15
  onValueChange: (value: string) => void;
16
+ /** Trigger text when nothing is selected. Default `"Select"`. */
10
17
  placeholder?: string;
18
+ /** Placeholder and accessible name for the search input. Default `"Search"`. */
11
19
  searchPlaceholder?: string;
12
- emptyText?: string;
20
+ /** Disables the trigger, so the list cannot open. */
13
21
  disabled?: boolean;
22
+ /** Goes on the trigger, so a `<Label htmlFor>` names it. Generated when omitted. */
14
23
  id?: string;
15
24
  /** Classes for the trigger, e.g. its width; the list matches it. */
16
25
  className?: string;
17
- /** Classes for the listbox, e.g. its height. Defaults to `max-h-60`. */
18
- listClassName?: string;
19
26
  /**
20
27
  * Classes for the popover panel, e.g. a width other than the trigger's.
21
28
  * Defaults to the trigger width.
22
29
  */
23
30
  contentClassName?: string;
31
+ /** Marks the trigger invalid, drawing the error border. */
24
32
  "aria-invalid"?: boolean;
25
33
  /** Name the trigger when there is no `<Label htmlFor={id}>`. */
26
34
  "aria-label"?: string;
35
+ /** Id of the element that names the trigger and the list. */
27
36
  "aria-labelledby"?: string;
28
37
  /**
29
38
  * Offer creating the current query when it is not already an option.
@@ -41,7 +50,7 @@ type ComboboxProps = {
41
50
  * `Create "{value}"`.
42
51
  */
43
52
  createLabel?: (value: string) => string;
44
- } & Omit<React.ComponentProps<"button">, "value" | "defaultValue" | "children" | "type" | "role" | "aria-expanded" | "aria-controls" | "onKeyDown">;
53
+ } & Pick<SearchPickerProps<ComboboxOption>, "emptyText" | "listClassName"> & Omit<React.ComponentProps<"button">, "value" | "defaultValue" | "children" | "type" | "role" | "aria-expanded" | "aria-controls" | "onKeyDown">;
45
54
  /**
46
55
  * Searchable single-select built from Button, Popover, and Input.
47
56
  * The trigger uses the same form-control chrome as Input and SelectTrigger.