atlas-react-kit 0.1.0 → 0.2.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.
package/COMPONENTS.md CHANGED
@@ -12,21 +12,27 @@ no root component barrel.
12
12
  | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
13
13
  | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush` |
14
14
  | `checkbox` | `Checkbox` |
15
+ | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text |
16
+ | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`; `value` is `Date` or `yyyy-MM-dd` |
15
17
  | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
16
18
  | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
19
+ | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
17
20
  | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
18
21
  | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
19
22
  | `popover` | `Popover`, trigger, anchor, content, header, title, description |
20
23
  | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size` |
21
24
  | `separator`, `skeleton` | `Separator`, `Skeleton` |
25
+ | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \| `desc`), `onClick`; sets `aria-sort` |
22
26
  | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
23
27
  | `table` | Table, header, body, footer, row, head, cell, caption |
24
28
  | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
25
29
  | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`; list `variant` is `default` or `line` |
26
30
  | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size` |
27
31
  | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size` |
32
+ | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
28
33
  | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
29
34
  | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
35
+ | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder |
30
36
  | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
31
37
  | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
32
38
  | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
@@ -51,8 +57,28 @@ type TextFieldApi = {
51
57
 
52
58
  `FormTextField` accepts `field` and `label`; use `multiline` and `rows` for a
53
59
  textarea, `inputType` for a specific input, and `showError={false}` to suppress
54
- error text and invalid styling. `FormSelectField` accepts the same error meta
55
- shape. `FieldError` accepts either `message` or pre-resolved `text`.
60
+ error text and invalid styling. `FormSelectField` and `FormDatePickerField`
61
+ accept the same error meta shape. `FormDatePickerField` takes a `Date` or
62
+ `yyyy-MM-dd` string and calls `onValueChange` with `Date | undefined`.
63
+ `FieldError` accepts either `message` or pre-resolved `text`.
64
+
65
+ ## Theme
66
+
67
+ ```tsx
68
+ import { ThemeProvider } from "atlas-react-kit/theme-provider"
69
+
70
+ export function Providers({ children }: { children: React.ReactNode }) {
71
+ return (
72
+ <ThemeProvider storageKey="app-theme" enableShortcut>
73
+ {children}
74
+ </ThemeProvider>
75
+ )
76
+ }
77
+ ```
78
+
79
+ `ThemeProvider` is a client component and does not import Next.js. It stores the
80
+ choice under `atlas-theme` unless `storageKey` is set. Pass `enableShortcut={false}`
81
+ to disable the `d` dark/light toggle.
56
82
 
57
83
  ## URL navigation in Next.js
58
84
 
package/DESIGN_RULES.md CHANGED
@@ -35,6 +35,9 @@ Use `Tabs` with actual `TabsContent` panels. For URL view switches, use a labell
35
35
  `TabsNav` with links and `aria-current="page"` on the active link. The optional
36
36
  Next adapter provides `TabsNavLink` with this behavior.
37
37
 
38
- Use `FormTextField`, `FormSelectField`, and `FormFeedbackField` for portable form
39
- controls, and `FieldError` for field messages. More specialized controls belong
40
- to the application.
38
+ Use `FormTextField`, `FormSelectField`, `FormDatePickerField`, and
39
+ `FormFeedbackField` for portable form controls, and `FieldError` for field
40
+ messages. `Combobox`, `DatePickerButton`, `ThemeProvider`, `ErrorBoundary`, and
41
+ `SortableTableHead` are first-class kit pieces. Domain data and application
42
+ shells stay with the application: currency or timezone option lists, query-library
43
+ error views, markdown editors, kanban boards, and dashboard widgets.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Shared React components and a Tailwind theme with square corners, semantic
4
4
  colors, and distinct selected states. React 19 and Tailwind 4 are required.
5
5
 
6
6
  ```sh
7
- npm install --save-exact atlas-react-kit@0.1.0
7
+ npm install --save-exact atlas-react-kit@0.2.0
8
8
  ```
9
9
 
10
10
  Import components by subpath:
@@ -34,7 +34,8 @@ this example assumes it is one directory below the project root.
34
34
  @source "../node_modules/atlas-react-kit/dist";
35
35
  ```
36
36
 
37
- The theme includes animations and shared light/dark tokens. Set the `dark` class
37
+ The theme includes animations and shared light/dark tokens. Wrap the tree with
38
+ `ThemeProvider` from `atlas-react-kit/theme-provider`, or set the `dark` class
38
39
  on an ancestor to activate dark colors. Supply your own font configuration;
39
40
  `font-heading` uses the configured sans font. Application-specific tokens stay
40
41
  with the application.
@@ -58,26 +59,19 @@ without compilation and processed by the consuming application's Tailwind build.
58
59
  The optional Next.js 16 adapter requires Next only when imported. The package
59
60
  does not bundle React.
60
61
 
61
- For the first release, create and validate a packed artifact:
62
+ 0.1.0 is already on npm. Later releases use explicit `v<version>` tags matching
63
+ `package.json`; CI runs checks before publishing through npm OIDC. Pushing to
64
+ `main` runs validation only.
65
+
66
+ If trusted publishing is not yet configured, pack and publish a validated
67
+ artifact with the package owner's npm login:
62
68
 
63
69
  ```sh
64
70
  mkdir -p release-artifacts
65
71
  npm pack --pack-destination release-artifacts
72
+ npm publish ./release-artifacts/atlas-react-kit-0.2.0.tgz --access public
66
73
  ```
67
74
 
68
- Install that tarball in consumer fixtures and complete their checks. The package
69
- owner then publishes that exact validated artifact, without rebuilding it:
70
-
71
- ```sh
72
- npm publish ./release-artifacts/atlas-react-kit-0.1.0.tgz --access public
73
- ```
74
-
75
- The owner then configures npm trusted publishing for this repository's
76
- `release.yml` workflow on a GitHub-hosted runner.
77
- Later releases use explicit `v<version>` tags matching `package.json`; CI runs
78
- checks before publishing through npm OIDC. Pushing to `main` runs validation only.
79
- Do not push a release tag for the initial manual publication.
80
-
81
75
  Pin an exact package version in consuming applications. Roll back an upgrade by
82
76
  restoring the previous version and lockfile. License: MIT; upstream notices are
83
77
  preserved in [NOTICE](./NOTICE).
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ export type ComboboxOption = {
3
+ value: string;
4
+ label: string;
5
+ };
6
+ type ComboboxProps = {
7
+ options: ComboboxOption[];
8
+ value?: string;
9
+ onValueChange: (value: string) => void;
10
+ placeholder?: string;
11
+ searchPlaceholder?: string;
12
+ emptyText?: string;
13
+ disabled?: boolean;
14
+ id?: string;
15
+ className?: string;
16
+ "aria-invalid"?: boolean;
17
+ };
18
+ /**
19
+ * Searchable single-select built from Button, Popover, and Input.
20
+ * Options are a plain `{ value, label }` list; domain data stays in the app.
21
+ */
22
+ export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, "aria-invalid": ariaInvalid, }: ComboboxProps): React.JSX.Element;
23
+ export {};
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { CheckIcon, ChevronDownIcon } from "lucide-react";
5
+ import { Button } from "./button.js";
6
+ import { Input } from "./input.js";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
8
+ import { cn, onInputChange } from "./utils.js";
9
+ /**
10
+ * Searchable single-select built from Button, Popover, and Input.
11
+ * Options are a plain `{ value, label }` list; domain data stays in the app.
12
+ */
13
+ export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, "aria-invalid": ariaInvalid, }) {
14
+ const [open, setOpen] = React.useState(false);
15
+ const [query, setQuery] = React.useState("");
16
+ const [highlight, setHighlight] = React.useState(0);
17
+ const inputRef = React.useRef(null);
18
+ const listId = React.useId();
19
+ const selected = options.find((option) => option.value === value);
20
+ const filtered = React.useMemo(() => {
21
+ const needle = query.trim().toLowerCase();
22
+ if (!needle)
23
+ return options;
24
+ return options.filter((option) => option.label.toLowerCase().includes(needle));
25
+ }, [options, query]);
26
+ // Highlight and search reset when the popover opens or closes; options/value
27
+ // are read for the initial highlight rather than subscribed as deps.
28
+ React.useEffect(() => {
29
+ if (!open) {
30
+ setQuery("");
31
+ return;
32
+ }
33
+ const selectedIndex = options.findIndex((option) => option.value === value);
34
+ setHighlight(selectedIndex >= 0 ? selectedIndex : 0);
35
+ const frame = window.requestAnimationFrame(() => inputRef.current?.focus());
36
+ return () => window.cancelAnimationFrame(frame);
37
+ }, [open]);
38
+ const onQueryChange = onInputChange((next) => {
39
+ setQuery(next);
40
+ setHighlight(0);
41
+ });
42
+ const selectValue = (next) => {
43
+ onValueChange(next);
44
+ setOpen(false);
45
+ };
46
+ const activeOption = filtered[highlight];
47
+ const onSearchKeyDown = (event) => {
48
+ if (event.key === "ArrowDown") {
49
+ event.preventDefault();
50
+ setHighlight((index) => filtered.length === 0 ? 0 : Math.min(index + 1, filtered.length - 1));
51
+ }
52
+ if (event.key === "ArrowUp") {
53
+ event.preventDefault();
54
+ setHighlight((index) => Math.max(index - 1, 0));
55
+ }
56
+ if (event.key === "Enter") {
57
+ event.preventDefault();
58
+ const option = filtered[highlight];
59
+ if (option)
60
+ selectValue(option.value);
61
+ }
62
+ };
63
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, disabled: disabled, className: cn("h-10 w-full justify-between font-normal tracking-normal normal-case", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: selected?.label ?? placeholder }), _jsx(ChevronDownIcon, { "data-icon": "inline-end", className: "opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", className: "w-[var(--radix-popover-trigger-width)] gap-0 rounded-none p-0", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { ref: inputRef, value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-autocomplete": "list", "aria-controls": listId, "aria-activedescendant": activeOption
64
+ ? `${listId}-option-${activeOption.value}`
65
+ : undefined }) }), _jsx("ul", { id: listId, role: "listbox", className: "max-h-60 overflow-y-auto p-1", children: filtered.length === 0 ? (_jsx("li", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText })) : (filtered.map((option, index) => {
66
+ const isSelected = option.value === value;
67
+ const isActive = index === highlight;
68
+ return (_jsx("li", { role: "none", children: _jsxs("button", { id: `${listId}-option-${option.value}`, type: "button", role: "option", "aria-selected": isSelected, className: cn("relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-3 text-left text-sm outline-hidden select-none", isSelected && "bg-selected text-selected-foreground", isActive && "bg-accent text-accent-foreground"), onMouseEnter: () => setHighlight(index), onClick: () => selectValue(option.value), children: [_jsx("span", { className: "truncate", children: option.label }), isSelected ? (_jsx(CheckIcon, { className: "pointer-events-none absolute right-2 size-3.5" })) : null] }) }, option.value));
69
+ })) })] })] }));
70
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ export type DateLike = Date | string | number | null | undefined;
3
+ /**
4
+ * Coerce a date-like value to a local `Date`.
5
+ * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
6
+ * shift when the runtime timezone is behind UTC.
7
+ */
8
+ export declare function toDate(value: DateLike): Date | undefined;
9
+ /** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
10
+ export declare function formatLocalDate(date: Date): string;
11
+ type DatePickerButtonProps = {
12
+ id?: string;
13
+ value?: DateLike;
14
+ onChange: (date: Date | undefined) => void;
15
+ placeholder?: string;
16
+ disabled?: boolean;
17
+ className?: string;
18
+ "aria-invalid"?: boolean;
19
+ };
20
+ /**
21
+ * Calendar popover trigger. Accepts `Date` or `yyyy-MM-dd` strings; emits
22
+ * `Date | undefined`. Square corners follow the shared shape rules.
23
+ */
24
+ export declare function DatePickerButton({ id, value, onChange, placeholder, disabled, className, "aria-invalid": ariaInvalid, }: DatePickerButtonProps): React.JSX.Element;
25
+ export {};
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { format, parseISO } from "date-fns";
5
+ import { CalendarIcon } from "lucide-react";
6
+ import { Button } from "./button.js";
7
+ import { Calendar } from "./calendar.js";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
9
+ import { cn } from "./utils.js";
10
+ /**
11
+ * Coerce a date-like value to a local `Date`.
12
+ * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
13
+ * shift when the runtime timezone is behind UTC.
14
+ */
15
+ export function toDate(value) {
16
+ if (value == null || value === "")
17
+ return undefined;
18
+ if (value instanceof Date) {
19
+ return Number.isNaN(value.getTime()) ? undefined : value;
20
+ }
21
+ if (typeof value === "number") {
22
+ const date = new Date(value);
23
+ return Number.isNaN(date.getTime()) ? undefined : date;
24
+ }
25
+ const trimmed = value.trim();
26
+ const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
27
+ if (ymd) {
28
+ const year = Number(ymd[1]);
29
+ const month = Number(ymd[2]);
30
+ const day = Number(ymd[3]);
31
+ const date = new Date(year, month - 1, day);
32
+ if (date.getFullYear() !== year ||
33
+ date.getMonth() !== month - 1 ||
34
+ date.getDate() !== day) {
35
+ return undefined;
36
+ }
37
+ return date;
38
+ }
39
+ const parsed = parseISO(trimmed);
40
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed;
41
+ }
42
+ /** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
43
+ export function formatLocalDate(date) {
44
+ return format(date, "yyyy-MM-dd");
45
+ }
46
+ /**
47
+ * Calendar popover trigger. Accepts `Date` or `yyyy-MM-dd` strings; emits
48
+ * `Date | undefined`. Square corners follow the shared shape rules.
49
+ */
50
+ export function DatePickerButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, }) {
51
+ const [open, setOpen] = React.useState(false);
52
+ const date = toDate(value);
53
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": !date, className: cn("h-10 w-full justify-start font-normal tracking-normal normal-case data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), date ? format(date, "PPP") : placeholder] }) }), _jsx(PopoverContent, { align: "start", className: "w-auto rounded-none p-0", children: _jsx(Calendar, { mode: "single", selected: date, defaultMonth: date, onSelect: (next) => {
54
+ onChange(next);
55
+ setOpen(false);
56
+ } }) })] }));
57
+ }
@@ -0,0 +1,34 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
+ type ErrorFallbackProps = {
3
+ /** User-facing recovery copy. */
4
+ message?: string;
5
+ onRetry?: () => void;
6
+ onReload?: () => void;
7
+ };
8
+ /**
9
+ * Presentational recovery UI used by `ErrorBoundary`. Exported so the markup
10
+ * can be tested without throwing during render.
11
+ */
12
+ export declare function ErrorFallback({ message, onRetry, onReload, }: ErrorFallbackProps): import("react").JSX.Element;
13
+ type ErrorBoundaryProps = {
14
+ children: ReactNode;
15
+ /** Overrides the default recovery message. */
16
+ message?: string;
17
+ onError?: (error: Error, info: ErrorInfo) => void;
18
+ };
19
+ type ErrorBoundaryState = {
20
+ error: Error | null;
21
+ };
22
+ /**
23
+ * Generic React error boundary with retry (reset) and full-page reload.
24
+ * Not tied to any data library.
25
+ */
26
+ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
27
+ state: ErrorBoundaryState;
28
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
29
+ componentDidCatch(error: Error, info: ErrorInfo): void;
30
+ retry: () => void;
31
+ reload: () => void;
32
+ render(): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
33
+ }
34
+ export {};
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Component } from "react";
4
+ import { Button } from "./button.js";
5
+ /**
6
+ * Presentational recovery UI used by `ErrorBoundary`. Exported so the markup
7
+ * can be tested without throwing during render.
8
+ */
9
+ export function ErrorFallback({ message = "Something went wrong.", onRetry, onReload, }) {
10
+ return (_jsxs("div", { role: "alert", "data-slot": "error-boundary", className: "flex flex-col items-center gap-4 rounded-none border border-dashed bg-muted/30 px-4 py-8 text-center", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: message }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [onRetry ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })) : null, onReload ? (_jsx(Button, { type: "button", size: "sm", onClick: onReload, children: "Reload" })) : null] })] }));
11
+ }
12
+ /**
13
+ * Generic React error boundary with retry (reset) and full-page reload.
14
+ * Not tied to any data library.
15
+ */
16
+ export class ErrorBoundary extends Component {
17
+ state = { error: null };
18
+ static getDerivedStateFromError(error) {
19
+ return { error };
20
+ }
21
+ componentDidCatch(error, info) {
22
+ this.props.onError?.(error, info);
23
+ }
24
+ retry = () => {
25
+ this.setState({ error: null });
26
+ };
27
+ reload = () => {
28
+ window.location.reload();
29
+ };
30
+ render() {
31
+ if (!this.state.error)
32
+ return this.props.children;
33
+ return (_jsx(ErrorFallback, { message: this.props.message, onRetry: this.retry, onReload: this.reload }));
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ import { type DateLike } from "../date-picker.js";
2
+ import { type FieldMetaState } from "./field-error.js";
3
+ type Props = {
4
+ value: DateLike;
5
+ onValueChange: (date: Date | undefined) => void;
6
+ label: string;
7
+ meta?: FieldMetaState;
8
+ placeholder?: string;
9
+ disabled?: boolean;
10
+ };
11
+ /** Date picker with built-in field error text and invalid styling. */
12
+ export declare function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, }: Props): import("react").JSX.Element;
13
+ export {};
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { DatePickerButton } from "../date-picker.js";
5
+ import { Label } from "../label.js";
6
+ import { FieldError, getFieldErrorMessage, } from "./field-error.js";
7
+ /** Date picker with built-in field error text and invalid styling. */
8
+ export function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, }) {
9
+ const errorText = meta ? getFieldErrorMessage(meta.errors[0]) : undefined;
10
+ const id = useId();
11
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(DatePickerButton, { id: id, value: value, onChange: onValueChange, placeholder: placeholder, disabled: disabled, "aria-invalid": errorText ? true : undefined }), _jsx(FieldError, { text: errorText })] }));
12
+ }
@@ -0,0 +1,15 @@
1
+ import type { ComponentProps } from "react";
2
+ import { TableHead } from "./table.js";
3
+ export type SortDirection = "asc" | "desc";
4
+ type SortableTableHeadProps = ComponentProps<typeof TableHead> & {
5
+ /** Whether this column is the active sort key. */
6
+ sorted?: boolean;
7
+ direction?: SortDirection;
8
+ onClick?: () => void;
9
+ };
10
+ /**
11
+ * Table header cell with `aria-sort` and a button affordance.
12
+ * Column keys and comparators stay in the application.
13
+ */
14
+ export declare function SortableTableHead({ children, sorted, direction, onClick, className, ...props }: SortableTableHeadProps): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronDownIcon, ChevronUpIcon, ChevronsUpDownIcon, } from "lucide-react";
4
+ import { TableHead } from "./table.js";
5
+ /**
6
+ * Table header cell with `aria-sort` and a button affordance.
7
+ * Column keys and comparators stay in the application.
8
+ */
9
+ export function SortableTableHead({ children, sorted = false, direction = "asc", onClick, className, ...props }) {
10
+ const ariaSort = sorted
11
+ ? direction === "asc"
12
+ ? "ascending"
13
+ : "descending"
14
+ : "none";
15
+ return (_jsx(TableHead, { "aria-sort": ariaSort, className: className, ...props, children: _jsxs("button", { type: "button", onClick: onClick, className: "inline-flex items-center gap-1 rounded-none hover:text-foreground", children: [children, sorted ? (direction === "asc" ? (_jsx(ChevronUpIcon, { className: "size-3.5", "aria-hidden": "true" })) : (_jsx(ChevronDownIcon, { className: "size-3.5", "aria-hidden": "true" }))) : (_jsx(ChevronsUpDownIcon, { className: "size-3.5 opacity-40", "aria-hidden": "true" }))] }) }));
16
+ }
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ import { useTheme, type ThemeProviderProps as NextThemesProviderProps } from "next-themes";
3
+ export type ThemeProviderProps = NextThemesProviderProps & {
4
+ /**
5
+ * When true, pressing `d` toggles dark/light unless focus is in an editable
6
+ * field. Defaults to true.
7
+ */
8
+ enableShortcut?: boolean;
9
+ };
10
+ /**
11
+ * Class-based theme wrapper around `next-themes`. Does not require Next.js;
12
+ * pass `storageKey` from the application (`atlas-theme` is the kit default).
13
+ */
14
+ declare function ThemeProvider({ children, storageKey, attribute, defaultTheme, enableSystem, disableTransitionOnChange, enableShortcut, ...props }: ThemeProviderProps): React.JSX.Element;
15
+ export { ThemeProvider, useTheme };
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { ThemeProvider as NextThemesProvider, useTheme, } from "next-themes";
5
+ const EDITABLE_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false'])";
6
+ function isEditableTarget(target) {
7
+ if (!(target instanceof HTMLElement))
8
+ return false;
9
+ return Boolean(target.closest(EDITABLE_SELECTOR));
10
+ }
11
+ /** Listens for the optional `d` shortcut inside an existing theme context. */
12
+ function ThemeShortcut() {
13
+ const { resolvedTheme, setTheme } = useTheme();
14
+ React.useEffect(() => {
15
+ function onKeyDown(event) {
16
+ if (event.metaKey || event.ctrlKey || event.altKey)
17
+ return;
18
+ if (event.key !== "d" && event.key !== "D")
19
+ return;
20
+ if (isEditableTarget(event.target))
21
+ return;
22
+ event.preventDefault();
23
+ setTheme(resolvedTheme === "dark" ? "light" : "dark");
24
+ }
25
+ window.addEventListener("keydown", onKeyDown);
26
+ return () => window.removeEventListener("keydown", onKeyDown);
27
+ }, [resolvedTheme, setTheme]);
28
+ return null;
29
+ }
30
+ /**
31
+ * Class-based theme wrapper around `next-themes`. Does not require Next.js;
32
+ * pass `storageKey` from the application (`atlas-theme` is the kit default).
33
+ */
34
+ function ThemeProvider({ children, storageKey = "atlas-theme", attribute = "class", defaultTheme = "system", enableSystem = true, disableTransitionOnChange = true, enableShortcut = true, ...props }) {
35
+ return (_jsxs(NextThemesProvider, { storageKey: storageKey, attribute: attribute, defaultTheme: defaultTheme, enableSystem: enableSystem, disableTransitionOnChange: disableTransitionOnChange, ...props, children: [enableShortcut ? _jsx(ThemeShortcut, {}) : null, children] }));
36
+ }
37
+ export { ThemeProvider, useTheme };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,6 +39,14 @@
39
39
  "types": "./dist/checkbox.d.ts",
40
40
  "default": "./dist/checkbox.js"
41
41
  },
42
+ "./combobox": {
43
+ "types": "./dist/combobox.d.ts",
44
+ "default": "./dist/combobox.js"
45
+ },
46
+ "./date-picker": {
47
+ "types": "./dist/date-picker.d.ts",
48
+ "default": "./dist/date-picker.js"
49
+ },
42
50
  "./dialog": {
43
51
  "types": "./dist/dialog.d.ts",
44
52
  "default": "./dist/dialog.js"
@@ -47,6 +55,10 @@
47
55
  "types": "./dist/empty-panel.d.ts",
48
56
  "default": "./dist/empty-panel.js"
49
57
  },
58
+ "./error-boundary": {
59
+ "types": "./dist/error-boundary.d.ts",
60
+ "default": "./dist/error-boundary.js"
61
+ },
50
62
  "./field": {
51
63
  "types": "./dist/field.d.ts",
52
64
  "default": "./dist/field.js"
@@ -75,6 +87,10 @@
75
87
  "types": "./dist/skeleton.d.ts",
76
88
  "default": "./dist/skeleton.js"
77
89
  },
90
+ "./sortable-table-head": {
91
+ "types": "./dist/sortable-table-head.d.ts",
92
+ "default": "./dist/sortable-table-head.js"
93
+ },
78
94
  "./sonner": {
79
95
  "types": "./dist/sonner.d.ts",
80
96
  "default": "./dist/sonner.js"
@@ -95,6 +111,10 @@
95
111
  "types": "./dist/textarea.d.ts",
96
112
  "default": "./dist/textarea.js"
97
113
  },
114
+ "./theme-provider": {
115
+ "types": "./dist/theme-provider.d.ts",
116
+ "default": "./dist/theme-provider.js"
117
+ },
98
118
  "./toggle": {
99
119
  "types": "./dist/toggle.d.ts",
100
120
  "default": "./dist/toggle.js"
@@ -103,6 +123,10 @@
103
123
  "types": "./dist/toggle-group.d.ts",
104
124
  "default": "./dist/toggle-group.js"
105
125
  },
126
+ "./form/date-picker-field": {
127
+ "types": "./dist/form/date-picker-field.d.ts",
128
+ "default": "./dist/form/date-picker-field.js"
129
+ },
106
130
  "./form/field-error": {
107
131
  "types": "./dist/form/field-error.d.ts",
108
132
  "default": "./dist/form/field-error.js"
@@ -158,6 +182,7 @@
158
182
  "dependencies": {
159
183
  "class-variance-authority": "^0.7.1",
160
184
  "clsx": "^2.1.1",
185
+ "date-fns": "^4.4.0",
161
186
  "lucide-react": "^1.16.0",
162
187
  "next-themes": "^0.4.6",
163
188
  "radix-ui": "^1.4.3",