atlas-react-kit 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/COMPONENTS.md +63 -52
  2. package/DESIGN_RULES.md +32 -15
  3. package/README.md +6 -3
  4. package/dist/accordion.js +2 -1
  5. package/dist/badge.js +7 -2
  6. package/dist/button.d.ts +2 -2
  7. package/dist/button.js +14 -4
  8. package/dist/calendar.js +1 -1
  9. package/dist/checkbox.js +2 -1
  10. package/dist/color-legend.d.ts +22 -0
  11. package/dist/color-legend.js +11 -0
  12. package/dist/combobox.js +2 -1
  13. package/dist/dropdown-menu.js +2 -1
  14. package/dist/form/markdown-field.d.ts +17 -0
  15. package/dist/form/markdown-field.js +27 -0
  16. package/dist/info-tip.d.ts +27 -0
  17. package/dist/info-tip.js +64 -0
  18. package/dist/input.js +2 -1
  19. package/dist/internal/form-control.d.ts +1 -1
  20. package/dist/internal/form-control.js +1 -1
  21. package/dist/internal/styles.d.ts +50 -0
  22. package/dist/internal/styles.js +50 -0
  23. package/dist/internal/tabs-styles.js +6 -3
  24. package/dist/markdown.d.ts +18 -0
  25. package/dist/markdown.js +62 -0
  26. package/dist/meter.d.ts +22 -0
  27. package/dist/meter.js +17 -0
  28. package/dist/pagination.js +2 -1
  29. package/dist/radio-group.js +2 -1
  30. package/dist/search-picker.d.ts +30 -0
  31. package/dist/search-picker.js +101 -0
  32. package/dist/select.js +3 -2
  33. package/dist/slider.d.ts +20 -0
  34. package/dist/slider.js +21 -0
  35. package/dist/stat-tile.d.ts +21 -0
  36. package/dist/stat-tile.js +17 -0
  37. package/dist/switch.js +2 -1
  38. package/dist/table.js +3 -2
  39. package/dist/textarea.js +2 -1
  40. package/dist/toggle-group.js +2 -2
  41. package/dist/toggle.d.ts +7 -1
  42. package/dist/toggle.js +17 -3
  43. package/package.json +37 -1
  44. package/skills/atlas-react-kit/SKILL.md +41 -32
  45. package/theme.css +23 -9
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./utils.js";
3
+ /**
4
+ * Uppercase muted label used on tiles and thin section chrome. Apps that
5
+ * only needed a heading class can import this instead of a wrapper.
6
+ */
7
+ export const sectionLabel = "text-xs font-semibold tracking-widest text-muted-foreground uppercase";
8
+ /** Row of tiles: stacked on phones, divided columns from `sm`. */
9
+ export const statTileGrid = "grid ring-1 ring-foreground/5 sm:divide-x";
10
+ /**
11
+ * Label, big tabular figure, optional extra (a meter), muted sub line.
12
+ * Domain copy and status colors stay in the app.
13
+ */
14
+ function StatTile({ label, value, sub, children, className }) {
15
+ return (_jsxs("div", { "data-slot": "stat-tile", className: cn("flex flex-col gap-1 px-3 py-2", className), children: [_jsx("span", { className: sectionLabel, children: label }), _jsx("span", { className: "text-lg font-semibold tracking-tight tabular-nums", children: value }), children, sub ? (_jsx("span", { className: "text-xs leading-snug text-muted-foreground", children: sub })) : null] }));
16
+ }
17
+ export { StatTile };
package/dist/switch.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Switch as SwitchPrimitive } from "radix-ui";
5
+ import { disabledState, focusRing, invalidState } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
8
  * On/off setting that applies immediately (notifications, dark mode). Use
@@ -9,6 +10,6 @@ import { cn } from "./utils.js";
9
10
  * primitives; checked uses the primary fill, as `Checkbox` does.
10
11
  */
11
12
  function Switch({ className, ...props }) {
12
- return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn("peer inline-flex h-5 w-9 shrink-0 items-center rounded-none border border-transparent bg-input p-0.5 shadow-xs transition-colors outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-checked:bg-primary dark:bg-input/80 dark:data-checked:bg-primary", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "pointer-events-none block size-3.5 rounded-none bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground" }) }));
13
+ return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn(focusRing, invalidState, disabledState, "peer inline-flex h-5 w-9 shrink-0 items-center rounded-none border border-transparent bg-input p-0.5 shadow-xs transition-colors data-checked:bg-primary dark:data-checked:bg-primary", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "pointer-events-none block size-3.5 rounded-none bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground" }) }));
13
14
  }
14
15
  export { Switch };
package/dist/table.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { focusRing } from "./internal/styles.js";
4
5
  import { cn } from "./utils.js";
5
6
  /**
6
7
  * True while the element's content is wider than its box. Re-checked when
@@ -32,7 +33,7 @@ function useHorizontalOverflow(ref) {
32
33
  function Table({ className, ...props }) {
33
34
  const containerRef = React.useRef(null);
34
35
  const scrollable = useHorizontalOverflow(containerRef);
35
- return (_jsx("div", { ref: containerRef, "data-slot": "table-container", tabIndex: scrollable ? 0 : undefined, className: "relative w-full min-w-0 max-w-full overflow-x-auto overscroll-x-contain outline-none focus-visible:ring-2 focus-visible:ring-ring/30", children: _jsx("table", { "data-slot": "table", className: cn("w-full min-w-full caption-bottom text-sm", className), ...props }) }));
36
+ return (_jsx("div", { ref: containerRef, "data-slot": "table-container", tabIndex: scrollable ? 0 : undefined, className: cn(focusRing, "relative w-full min-w-0 max-w-full overflow-x-auto overscroll-x-contain"), children: _jsx("table", { "data-slot": "table", className: cn("w-full min-w-full caption-bottom text-sm", className), ...props }) }));
36
37
  }
37
38
  function TableHeader({ className, ...props }) {
38
39
  return (_jsx("thead", { "data-slot": "table-header", className: cn("[&_tr]:border-b", className), ...props }));
@@ -44,7 +45,7 @@ function TableFooter({ className, ...props }) {
44
45
  return (_jsx("tfoot", { "data-slot": "table-footer", className: cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className), ...props }));
45
46
  }
46
47
  function TableRow({ className, ...props }) {
47
- return (_jsx("tr", { "data-slot": "table-row", className: cn("border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-selected", className), ...props }));
48
+ return (_jsx("tr", { "data-slot": "table-row", className: cn("border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-selected data-[state=selected]:shadow-[inset_2px_0_0_var(--color-selected-foreground)]", className), ...props }));
48
49
  }
49
50
  function TableHead({ className, ...props }) {
50
51
  return (_jsx("th", { "data-slot": "table-head", className: cn("h-10 px-2 text-left align-middle text-xs font-medium tracking-wider whitespace-nowrap text-muted-foreground uppercase sm:h-12 sm:px-3 [&:has([role=checkbox])]:pr-0", className), ...props }));
package/dist/textarea.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { disabledState, fieldSurface, focusRing, invalidState, } from "./internal/styles.js";
3
4
  import { cn } from "./utils.js";
4
5
  function Textarea({ className, ...props }) {
5
- return (_jsx("textarea", { "data-slot": "textarea", className: cn("flex field-sizing-content min-h-16 w-full resize-none rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
6
+ return (_jsx("textarea", { "data-slot": "textarea", className: cn(fieldSurface, focusRing, invalidState, disabledState, "flex field-sizing-content min-h-16 resize-none", className), ...props }));
6
7
  }
7
8
  export { Textarea };
@@ -16,9 +16,9 @@ function ToggleGroup({ className, variant, size, spacing = 2, orientation = "hor
16
16
  }
17
17
  function ToggleGroupItem({ className, children, variant = "default", size = "default", ...props }) {
18
18
  const context = React.useContext(ToggleGroupContext);
19
- return (_jsx(ToggleGroupPrimitive.Item, { "data-slot": "toggle-group-item", "data-variant": context.variant || variant, "data-size": context.size || size, "data-spacing": context.spacing, className: cn("shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none data-[state=on]:bg-selected data-[state=on]:text-selected-foreground group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t", toggleVariants({
19
+ return (_jsx(ToggleGroupPrimitive.Item, { "data-slot": "toggle-group-item", "data-variant": context.variant || variant, "data-size": context.size || size, "data-spacing": context.spacing, className: cn(toggleVariants({
20
20
  variant: context.variant || variant,
21
21
  size: context.size || size,
22
- }), className), ...props, children: children }));
22
+ }), "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t", className), ...props, children: children }));
23
23
  }
24
24
  export { ToggleGroup, ToggleGroupItem };
package/dist/toggle.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  import { Toggle as TogglePrimitive } from "radix-ui";
4
+ /**
5
+ * Selected chrome shared by Toggle and ToggleGroupItem (see
6
+ * `selectedState.toggle`): the wash, selected text, and a selected border
7
+ * for `data-state=on`, `aria-pressed`, and `aria-checked`.
8
+ */
9
+ declare const selectedToggleChrome: string;
4
10
  declare const toggleVariants: (props?: ({
5
11
  variant?: "default" | "outline" | null | undefined;
6
12
  size?: "default" | "sm" | "lg" | null | undefined;
7
13
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
14
  declare function Toggle({ className, variant, size, ...props }: React.ComponentProps<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>): React.JSX.Element;
9
- export { Toggle, toggleVariants };
15
+ export { Toggle, toggleVariants, selectedToggleChrome };
package/dist/toggle.js CHANGED
@@ -3,11 +3,25 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Toggle as TogglePrimitive } from "radix-ui";
6
+ import { disabledState, focusRing, invalidState, selectedState, } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
- const toggleVariants = cva("group/toggle inline-flex items-center justify-center gap-1.5 rounded-none text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-colors outline-none hover:border-primary hover:bg-selected hover:text-selected-foreground focus-visible:border-primary focus-visible:text-selected-foreground focus-visible:ring-[3px] focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-selected aria-pressed:text-selected-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", {
8
+ /**
9
+ * Selected chrome shared by Toggle and ToggleGroupItem (see
10
+ * `selectedState.toggle`): the wash, selected text, and a selected border
11
+ * for `data-state=on`, `aria-pressed`, and `aria-checked`.
12
+ */
13
+ const selectedToggleChrome = selectedState.toggle;
14
+ const toggleVariants = cva([
15
+ focusRing,
16
+ selectedState.hover,
17
+ selectedToggleChrome,
18
+ invalidState,
19
+ disabledState,
20
+ "group/toggle inline-flex items-center justify-center gap-1.5 rounded-none text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-colors focus-visible:text-selected-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
21
+ ], {
8
22
  variants: {
9
23
  variant: {
10
- default: "bg-transparent",
24
+ default: "border border-transparent bg-transparent",
11
25
  outline: "border border-input bg-transparent",
12
26
  },
13
27
  size: {
@@ -24,4 +38,4 @@ const toggleVariants = cva("group/toggle inline-flex items-center justify-center
24
38
  function Toggle({ className, variant = "default", size = "default", ...props }) {
25
39
  return (_jsx(TogglePrimitive.Root, { "data-slot": "toggle", className: cn(toggleVariants({ variant, size, className })), ...props }));
26
40
  }
27
- export { Toggle, toggleVariants };
41
+ export { Toggle, toggleVariants, selectedToggleChrome };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,6 +56,10 @@
56
56
  "types": "./dist/checkbox.d.ts",
57
57
  "default": "./dist/checkbox.js"
58
58
  },
59
+ "./color-legend": {
60
+ "types": "./dist/color-legend.d.ts",
61
+ "default": "./dist/color-legend.js"
62
+ },
59
63
  "./combobox": {
60
64
  "types": "./dist/combobox.d.ts",
61
65
  "default": "./dist/combobox.js"
@@ -88,6 +92,10 @@
88
92
  "types": "./dist/field.d.ts",
89
93
  "default": "./dist/field.js"
90
94
  },
95
+ "./info-tip": {
96
+ "types": "./dist/info-tip.d.ts",
97
+ "default": "./dist/info-tip.js"
98
+ },
91
99
  "./input": {
92
100
  "types": "./dist/input.d.ts",
93
101
  "default": "./dist/input.js"
@@ -96,6 +104,14 @@
96
104
  "types": "./dist/label.d.ts",
97
105
  "default": "./dist/label.js"
98
106
  },
107
+ "./markdown": {
108
+ "types": "./dist/markdown.d.ts",
109
+ "default": "./dist/markdown.js"
110
+ },
111
+ "./meter": {
112
+ "types": "./dist/meter.d.ts",
113
+ "default": "./dist/meter.js"
114
+ },
99
115
  "./pagination": {
100
116
  "types": "./dist/pagination.d.ts",
101
117
  "default": "./dist/pagination.js"
@@ -112,6 +128,10 @@
112
128
  "types": "./dist/radio-group.d.ts",
113
129
  "default": "./dist/radio-group.js"
114
130
  },
131
+ "./search-picker": {
132
+ "types": "./dist/search-picker.d.ts",
133
+ "default": "./dist/search-picker.js"
134
+ },
115
135
  "./select": {
116
136
  "types": "./dist/select.d.ts",
117
137
  "default": "./dist/select.js"
@@ -124,6 +144,10 @@
124
144
  "types": "./dist/skeleton.d.ts",
125
145
  "default": "./dist/skeleton.js"
126
146
  },
147
+ "./slider": {
148
+ "types": "./dist/slider.d.ts",
149
+ "default": "./dist/slider.js"
150
+ },
127
151
  "./sonner": {
128
152
  "types": "./dist/sonner.d.ts",
129
153
  "default": "./dist/sonner.js"
@@ -136,6 +160,10 @@
136
160
  "types": "./dist/spinner.d.ts",
137
161
  "default": "./dist/spinner.js"
138
162
  },
163
+ "./stat-tile": {
164
+ "types": "./dist/stat-tile.d.ts",
165
+ "default": "./dist/stat-tile.js"
166
+ },
139
167
  "./switch": {
140
168
  "types": "./dist/switch.d.ts",
141
169
  "default": "./dist/switch.js"
@@ -188,6 +216,10 @@
188
216
  "types": "./dist/form/feedback-field.d.ts",
189
217
  "default": "./dist/form/feedback-field.js"
190
218
  },
219
+ "./form/markdown-field": {
220
+ "types": "./dist/form/markdown-field.d.ts",
221
+ "default": "./dist/form/markdown-field.js"
222
+ },
191
223
  "./form/select-field": {
192
224
  "types": "./dist/form/select-field.d.ts",
193
225
  "default": "./dist/form/select-field.js"
@@ -213,6 +245,7 @@
213
245
  "build": "tsc",
214
246
  "check": "npm run build && prettier --check .",
215
247
  "test": "npm run build && vitest run",
248
+ "test:coverage": "npm run build && vitest run --coverage",
216
249
  "prepack": "npm run build"
217
250
  },
218
251
  "engines": {
@@ -240,6 +273,8 @@
240
273
  "next-themes": "^0.4.6",
241
274
  "radix-ui": "^1.4.3",
242
275
  "react-day-picker": "^10.0.1",
276
+ "react-markdown": "^10.1.0",
277
+ "remark-gfm": "^4.0.1",
243
278
  "sonner": "^2.0.7",
244
279
  "tailwind-merge": "^3.6.0",
245
280
  "tw-animate-css": "^1.4.0"
@@ -252,6 +287,7 @@
252
287
  "@types/node": "^25.5.0",
253
288
  "@types/react": "^19.2.14",
254
289
  "@types/react-dom": "^19.2.3",
290
+ "@vitest/coverage-v8": "4.1.11",
255
291
  "jsdom": "^29.1.1",
256
292
  "next": "16.2.6",
257
293
  "prettier": "^3.8.1",
@@ -49,33 +49,40 @@ Import each component from its subpath, e.g.
49
49
  `import { Button } from "atlas-react-kit/button"`. There is no root barrel, and
50
50
  `dist/` or `internal/` paths are not public API.
51
51
 
52
- | Need | Use |
53
- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- |
54
- | Short fixed list (a handful of options) | `Select` family from `select` |
55
- | Long or searchable list (timezones, currencies) | `Combobox` from `combobox` with `{ value, label }[]` options |
56
- | One date, or a from/to range | `DatePickerButton` from `date-picker` (`mode="range"` for ranges) |
57
- | Date and time (not `datetime-local`) | `DateTimePickerButton` from `date-time-picker` |
58
- | Labelled field with error text | `FormTextField`, `FormSelectField`, `FormDatePickerField`, `FormDateTimePickerField` from `form/*` |
59
- | Form-level error (save failed) | `FormFeedbackField` from `form/feedback-field` |
60
- | Content panels on one page | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` from `tabs` |
61
- | View switch that changes the URL | `TabsNav` with links: `TabsNavLink` from `next/tabs-nav-link` in Next, else `tabsTriggerVariants()` |
62
- | Status chip (paid, pending, failed) | `Badge variant="soft"` with `tone="success"`, `"warning"`, `"destructive"`, or `"neutral"` |
63
- | Data table | `Table` family from `table`; `SortableTableHead`; `TableBodySkeleton` while loading |
64
- | Nothing to show yet | `EmptyPanel` from `empty-panel` |
65
- | Grouped content | `Card` family from `card`; `<Card flush>` for an edge-to-edge table or list |
66
- | Modal | `Dialog` family from `dialog` |
67
- | Label for an icon button, or a short hint | `Tooltip` from `tooltip`; keep `aria-label` on icon buttons |
68
- | Row actions or an overflow menu of commands | `DropdownMenu` family from `dropdown-menu` |
69
- | Setting that applies immediately | `Switch` from `switch`; `Checkbox` for choices submitted with a form |
70
- | One choice from a few visible options | `RadioGroup` and `RadioGroupItem` from `radio-group` |
71
- | Inline message about a page or section | `Alert` from `alert` (`success`, `warning`, `destructive` variants) |
72
- | Person or workspace image | `Avatar`, `AvatarImage`, `AvatarFallback` from `avatar` |
73
- | Sections that expand in place | `Accordion` family from `accordion` |
74
- | Pages of a long list | `Pagination` family from `pagination` |
75
- | Where the page sits in a hierarchy | `Breadcrumb` family from `breadcrumb` |
76
- | Work in progress | `Progress` from `progress` when the percent is known, else `Spinner` from `spinner` |
77
- | Crash recovery around a section | `ErrorBoundary` from `error-boundary` |
78
- | Conditional classes | `cn` from `utils` |
52
+ | Need | Use |
53
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
54
+ | Short fixed list (a handful of options) | `Select` family from `select` |
55
+ | Long or searchable list (timezones, currencies) | `Combobox` from `combobox` with `{ value, label }[]` options |
56
+ | Always-visible searchable list (places) | `SearchPicker` from `search-picker` |
57
+ | One date, or a from/to range | `DatePickerButton` from `date-picker` (`mode="range"` for ranges) |
58
+ | Date and time (not `datetime-local`) | `DateTimePickerButton` from `date-time-picker` |
59
+ | Labelled field with error text | `FormTextField`, `FormSelectField`, `FormDatePickerField`, `FormDateTimePickerField`, `FormMarkdownField` from `form/*` |
60
+ | Form-level error (save failed) | `FormFeedbackField` from `form/feedback-field` |
61
+ | Content panels on one page | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` from `tabs` |
62
+ | View switch that changes the URL | `TabsNav` with links: `TabsNavLink` from `next/tabs-nav-link` in Next, else `tabsTriggerVariants()` |
63
+ | Status chip (paid, pending, failed) | `Badge variant="soft"` with `tone="success"`, `"warning"`, `"destructive"`, or `"neutral"` |
64
+ | Data table | `Table` family from `table`; `SortableTableHead`; `TableBodySkeleton` while loading |
65
+ | Nothing to show yet | `EmptyPanel` from `empty-panel` |
66
+ | Grouped content | `Card` family from `card`; `<Card flush>` for an edge-to-edge table or list |
67
+ | Modal | `Dialog` family from `dialog` |
68
+ | Label for an icon button, or a short hint | `Tooltip` from `tooltip`; keep `aria-label` on icon buttons |
69
+ | Longer help from a glyph | `InfoTip` / `InlineTip` from `info-tip` (not `Tooltip`) |
70
+ | Color scale key | `ColorLegend` from `color-legend` |
71
+ | GitHub-flavoured Markdown | `Markdown` from `markdown`; write/preview with `FormMarkdownField` |
72
+ | Summary figure in a strip | `StatTile` from `stat-tile` (`statTileGrid`, `sectionLabel`) |
73
+ | Value against a scale, optional ticks | `Meter` from `meter` (not `Progress`) |
74
+ | Number on a continuous scale | `Slider` from `slider` |
75
+ | Row actions or an overflow menu of commands | `DropdownMenu` family from `dropdown-menu` |
76
+ | Setting that applies immediately | `Switch` from `switch`; `Checkbox` for choices submitted with a form |
77
+ | One choice from a few visible options | `RadioGroup` and `RadioGroupItem` from `radio-group` |
78
+ | Inline message about a page or section | `Alert` from `alert` (`success`, `warning`, `destructive` variants) |
79
+ | Person or workspace image | `Avatar`, `AvatarImage`, `AvatarFallback` from `avatar` |
80
+ | Sections that expand in place | `Accordion` family from `accordion` |
81
+ | Pages of a long list | `Pagination` family from `pagination` |
82
+ | Where the page sits in a hierarchy | `Breadcrumb` family from `breadcrumb` |
83
+ | Work in progress | `Progress` from `progress` when the percent is known, else `Spinner` from `spinner` |
84
+ | Crash recovery around a section | `ErrorBoundary` from `error-boundary` |
85
+ | Conditional classes | `cn` from `utils` |
79
86
 
80
87
  If nothing fits, build the piece in the app from kit primitives and semantic
81
88
  tokens. Do not copy kit source into the app; a missing shared component belongs
@@ -95,9 +102,10 @@ list; the ones that come up most:
95
102
  `border-border`, `bg-card`, `text-destructive`. Raw palette classes
96
103
  (`bg-purple-600`, `text-gray-500`) and paired `dark:` colors drift from the
97
104
  theme; tokens already switch with light and dark.
98
- - **Selected and active state**: `bg-selected text-selected-foreground` for the
99
- current nav item, chip, row, or toggle. Grey fills read as hover, not
100
- selection.
105
+ - **Selected and active state**: `bg-selected text-selected-foreground` plus
106
+ a `border-selected-foreground` border (or bar) for the current nav item,
107
+ chip, row, or toggle; the wash alone is too faint to show state. Grey fills
108
+ read as hover, not selection.
101
109
  - **Status**: `success`, `warning`, and `destructive` for good, caution, and
102
110
  bad. Use them as text or soft tints (the `Badge` tones), which meet AA in
103
111
  both themes. Don't override a soft badge's colors. There is no foreground
@@ -106,8 +114,9 @@ list; the ones that come up most:
106
114
  which is too dark to read on the dark background.
107
115
  - **Accessibility**: the kit targets WCAG 2.2 AA. Give every control a name
108
116
  (`Label htmlFor`, or `aria-label` on icon-only buttons and an unlabelled
109
- `Combobox`), keep the shipped focus rings, and don't lower contrast with raw
110
- colors or opacity.
117
+ `Combobox`), keep the shipped focus rings (solid `ring-ring`, never
118
+ `ring-ring/30`), keep field borders on `border-input` (3:1; `border-border`
119
+ is for dividers), and don't lower contrast with raw colors or opacity.
111
120
  - **Square corners**: primitives have none. Use `rounded-full` only for real
112
121
  circles (avatars, dots). Adding `rounded-md` to a card or button breaks the
113
122
  system.
package/theme.css CHANGED
@@ -19,6 +19,11 @@
19
19
  @slot;
20
20
  }
21
21
  }
22
+ @custom-variant data-on {
23
+ &:where([data-state="on"]) {
24
+ @slot;
25
+ }
26
+ }
22
27
  @custom-variant data-disabled {
23
28
  &:where([data-disabled="true"]),
24
29
  &:where([data-disabled]:not([data-disabled="false"])) {
@@ -100,8 +105,11 @@
100
105
  --muted-foreground: oklch(0.53 0 0);
101
106
  --accent: oklch(0.97 0 0);
102
107
  --accent-foreground: oklch(0.205 0 0);
103
- /* Soft brand-purple wash for selected/active chrome (nav, tabs, chips, rows). */
104
- --selected: color-mix(in oklch, var(--primary) 12%, var(--background));
108
+ /* Soft brand-purple wash for selected/active chrome (nav, tabs, chips, rows).
109
+ Mixed in oklab: in oklch, white's hue (0) pulls the mix toward pink. The
110
+ wash is too faint to mark state alone, so selected chrome also takes a
111
+ `selected-foreground` border or bar (WCAG 1.4.11). */
112
+ --selected: color-mix(in oklab, var(--primary) 12%, var(--background));
105
113
  --selected-foreground: var(--primary);
106
114
  /* AA as text on the page (6.4:1) and on its own /10 and /15 tints. */
107
115
  --destructive: oklch(0.505 0.213 27.518);
@@ -110,9 +118,13 @@
110
118
  nothing puts text on a solid fill. See DESIGN_RULES.md. */
111
119
  --success: oklch(0.45 0.11 160);
112
120
  --warning: oklch(0.48 0.115 70);
121
+ /* Dividers and card edges: decorative, so no contrast minimum. */
113
122
  --border: oklch(0.922 0 0);
114
- --input: oklch(0.922 0 0);
115
- --ring: oklch(0.708 0 0);
123
+ /* Form-control boundaries (fields, checkboxes, radios, switch track): 3:1
124
+ on the page and on cards, as WCAG 1.4.11 requires. */
125
+ --input: oklch(0.62 0 0);
126
+ /* Focus ring: solid brand purple, 7.3:1 on the page. */
127
+ --ring: var(--primary);
116
128
  --chart-1: oklch(0.87 0 0);
117
129
  --chart-2: oklch(0.556 0 0);
118
130
  --chart-3: oklch(0.439 0 0);
@@ -136,7 +148,9 @@
136
148
  --card-foreground: oklch(0.985 0 0);
137
149
  --popover: oklch(0.205 0 0);
138
150
  --popover-foreground: oklch(0.985 0 0);
139
- --primary: oklch(0.432 0.232 292.759);
151
+ /* Light enough for checked fills and borders to reach 3:1 on the page and
152
+ cards, dark enough for primary-foreground text (5:1). */
153
+ --primary: oklch(0.55 0.25 292.7);
140
154
  --primary-foreground: oklch(0.969 0.016 293.756);
141
155
  --secondary: oklch(0.274 0.006 286.033);
142
156
  --secondary-foreground: oklch(0.985 0 0);
@@ -144,14 +158,14 @@
144
158
  --muted-foreground: oklch(0.708 0 0);
145
159
  --accent: oklch(0.269 0 0);
146
160
  --accent-foreground: oklch(0.985 0 0);
147
- --selected: color-mix(in oklch, var(--primary) 24%, var(--background));
161
+ --selected: color-mix(in oklab, var(--primary) 24%, var(--background));
148
162
  --selected-foreground: oklch(0.85 0.06 293);
149
163
  --destructive: oklch(0.704 0.191 22.216);
150
164
  --success: oklch(0.765 0.177 163.223);
151
165
  --warning: oklch(0.828 0.189 84.429);
152
166
  --border: oklch(1 0 0 / 10%);
153
- --input: oklch(1 0 0 / 15%);
154
- --ring: oklch(0.556 0 0);
167
+ --input: oklch(0.52 0 0);
168
+ --ring: oklch(0.75 0.14 293);
155
169
  --chart-1: oklch(0.87 0 0);
156
170
  --chart-2: oklch(0.556 0 0);
157
171
  --chart-3: oklch(0.439 0 0);
@@ -169,7 +183,7 @@
169
183
 
170
184
  @layer base {
171
185
  * {
172
- @apply border-border outline-ring/50;
186
+ @apply border-border outline-ring;
173
187
  }
174
188
  body {
175
189
  @apply bg-background text-foreground;