atlas-react-kit 0.13.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 (87) hide show
  1. package/COMPONENTS.md +118 -98
  2. package/DESIGN_RULES.md +30 -0
  3. package/README.md +10 -9
  4. package/dist/accordion.d.ts +1 -0
  5. package/dist/avatar.d.ts +1 -0
  6. package/dist/badge.d.ts +2 -3
  7. package/dist/breadcrumb.d.ts +2 -3
  8. package/dist/button.d.ts +2 -3
  9. package/dist/calendar.d.ts +2 -0
  10. package/dist/card.d.ts +3 -0
  11. package/dist/chart.d.ts +152 -0
  12. package/dist/chart.js +177 -0
  13. package/dist/color-legend.d.ts +8 -0
  14. package/dist/combobox.d.ts +13 -4
  15. package/dist/combobox.js +1 -8
  16. package/dist/date-picker.d.ts +14 -2
  17. package/dist/date-picker.js +12 -19
  18. package/dist/date-time-picker.d.ts +5 -2
  19. package/dist/date-time-picker.js +6 -5
  20. package/dist/dialog.d.ts +1 -0
  21. package/dist/dialog.js +4 -2
  22. package/dist/dropdown-menu.d.ts +9 -8
  23. package/dist/empty-panel.d.ts +8 -2
  24. package/dist/empty-panel.js +2 -2
  25. package/dist/error-boundary.d.ts +6 -1
  26. package/dist/field.d.ts +3 -0
  27. package/dist/field.js +1 -1
  28. package/dist/form/checkbox-field.d.ts +45 -0
  29. package/dist/form/checkbox-field.js +39 -0
  30. package/dist/form/combobox-field.d.ts +13 -0
  31. package/dist/form/combobox-field.js +14 -0
  32. package/dist/form/control-field.d.ts +6 -7
  33. package/dist/form/date-picker-field.d.ts +6 -20
  34. package/dist/form/date-picker-field.js +2 -2
  35. package/dist/form/date-time-picker-field.d.ts +6 -21
  36. package/dist/form/date-time-picker-field.js +1 -1
  37. package/dist/form/feedback-field.d.ts +2 -0
  38. package/dist/form/field-error.d.ts +31 -0
  39. package/dist/form/field-shell.d.ts +29 -1
  40. package/dist/form/markdown-field.d.ts +4 -16
  41. package/dist/form/select-field.d.ts +6 -6
  42. package/dist/form/text-field.d.ts +29 -10
  43. package/dist/form/text-field.js +4 -0
  44. package/dist/form/toggle-group-field.d.ts +30 -0
  45. package/dist/form/toggle-group-field.js +19 -0
  46. package/dist/info-tip.d.ts +10 -1
  47. package/dist/internal/as-child.d.ts +8 -0
  48. package/dist/internal/as-child.js +1 -0
  49. package/dist/internal/form-control.d.ts +3 -2
  50. package/dist/internal/form-control.js +3 -2
  51. package/dist/internal/kanban-layout.d.ts +55 -0
  52. package/dist/internal/kanban-layout.js +80 -0
  53. package/dist/internal/kanban-plugins.d.ts +17 -0
  54. package/dist/internal/kanban-plugins.js +17 -0
  55. package/dist/internal/use-media-query.d.ts +4 -2
  56. package/dist/internal/use-media-query.js +11 -15
  57. package/dist/kanban.d.ts +84 -0
  58. package/dist/kanban.js +197 -0
  59. package/dist/markdown.d.ts +2 -0
  60. package/dist/meter.d.ts +7 -0
  61. package/dist/nav-link.d.ts +3 -2
  62. package/dist/next/period-stepper.d.ts +11 -0
  63. package/dist/next/period-stepper.js +16 -0
  64. package/dist/page-header.d.ts +19 -0
  65. package/dist/page-header.js +11 -0
  66. package/dist/pagination.d.ts +2 -0
  67. package/dist/period-stepper.d.ts +32 -0
  68. package/dist/period-stepper.js +27 -0
  69. package/dist/picker-shell.d.ts +12 -0
  70. package/dist/picker-shell.js +1 -1
  71. package/dist/progress.d.ts +1 -0
  72. package/dist/search-picker.d.ts +12 -0
  73. package/dist/select.d.ts +1 -0
  74. package/dist/skip-link.d.ts +7 -0
  75. package/dist/skip-link.js +10 -0
  76. package/dist/slider.d.ts +11 -0
  77. package/dist/sortable-table-head.d.ts +2 -0
  78. package/dist/stat-tile.d.ts +5 -0
  79. package/dist/table-body-skeleton.d.ts +2 -0
  80. package/dist/table.d.ts +1 -0
  81. package/dist/tabs.d.ts +9 -5
  82. package/dist/tabs.js +1 -1
  83. package/dist/toast.d.ts +7 -1
  84. package/dist/toggle-group.d.ts +9 -2
  85. package/dist/toggle-group.js +2 -1
  86. package/package.json +69 -7
  87. package/theme.css +11 -0
@@ -1,5 +1,6 @@
1
1
  import type { ComponentProps, ReactNode } from "react";
2
- import { type ErrorTimingProps, type FieldMetaState } from "./field-error.js";
2
+ import type { DateLike, DatePickerCalendarProps } from "../date-picker.js";
3
+ import { type ErrorTimingProps, type FieldMetaState, type FormFieldBaseProps, type FormFieldMetaProps } from "./field-error.js";
3
4
  /**
4
5
  * Not a public entry. The layout and error wiring every `form/*` field
5
6
  * shares, so a field only says which control it renders. Custom controls
@@ -29,6 +30,26 @@ export declare function requiredIdFor(id: string): string;
29
30
  * text while there is one, or `undefined` when there is nothing to read.
30
31
  */
31
32
  export declare function describedBy(id: string, errorText: string | undefined, ...hintIds: Array<string | undefined>): string | undefined;
33
+ /**
34
+ * Props the date and date-time fields share; each adds its own `value`,
35
+ * `onValueChange`, and `placeholder`.
36
+ */
37
+ export type PickerFieldBaseProps = FormFieldBaseProps & FormFieldMetaProps & {
38
+ /** Disables the trigger so no date can be picked. */
39
+ disabled?: boolean;
40
+ /** Marks the label and adds "Required" to the trigger's description. */
41
+ required?: boolean;
42
+ /** Earliest day that can be picked; earlier days are disabled. */
43
+ minDate?: DateLike;
44
+ /** Latest day that can be picked; later days are disabled. */
45
+ maxDate?: DateLike;
46
+ /** Forwarded to the picker trigger. */
47
+ className?: string;
48
+ /** Forwarded to the picker popover. */
49
+ contentClassName?: string;
50
+ /** Day Picker options forwarded to the calendar (disabled days, etc.). */
51
+ calendarProps?: DatePickerCalendarProps;
52
+ };
32
53
  /**
33
54
  * Trigger ARIA for the date and date-time fields: invalid while there is
34
55
  * error text, named "label, value" (a `<label for>` alone would hide the
@@ -53,12 +74,19 @@ export declare function pickerFieldAria(id: string, errorText: string | undefine
53
74
  * so put fields in a grid row with `items-start` to keep controls aligned.
54
75
  */
55
76
  export declare function FormFieldShell({ id, label, labelAction, description, errorText, labelFor, required, className, children, ...props }: Omit<ComponentProps<"div">, "id" | "children"> & {
77
+ /** Id of the control; the label, hint, and error ids derive from it. */
56
78
  id: string;
79
+ /** Visible label text. */
57
80
  label: string;
81
+ /** Content on the label's row, after the label (e.g. a mode toggle). */
58
82
  labelAction?: ReactNode;
83
+ /** Hint between the label and the control, with id `descriptionIdFor(id)`. */
59
84
  description?: ReactNode;
85
+ /** Error text under the control; `undefined` shows none. */
60
86
  errorText: string | undefined;
87
+ /** When false, the label does not point at the control. Default true. */
61
88
  labelFor?: boolean;
89
+ /** Marks the label and renders the hidden "Required" text. */
62
90
  required?: boolean;
63
91
  children: ReactNode;
64
92
  }): import("react").JSX.Element;
@@ -1,25 +1,13 @@
1
- import type { ErrorTimingProps } from "./field-error.js";
2
- import type { TextFieldApi } from "./text-field.js";
1
+ import type { TextFieldBaseProps } from "./text-field.js";
3
2
  /**
4
3
  * GFM body field: Write is a textarea, Preview is the sanitized Markdown
5
4
  * renderer. String-in/string-out — not a WYSIWYG document model.
6
5
  */
7
- export declare function FormMarkdownField({ field, id, label, placeholder, rows, showError, submitted, required, className, fieldClassName, previewClassName, }: ErrorTimingProps & {
8
- field: TextFieldApi;
9
- /**
10
- * Control id the label points at. Defaults to `field.name`; pass a unique
11
- * id when two forms on one page share a field name.
12
- */
13
- id?: string;
14
- label: string;
6
+ export declare function FormMarkdownField({ field, id, label, placeholder, rows, showError, submitted, required, className, fieldClassName, previewClassName, }: TextFieldBaseProps & {
7
+ /** Write textarea placeholder. Default "Markdown supported". */
15
8
  placeholder?: string;
9
+ /** Write textarea rows. Default 5. */
16
10
  rows?: number;
17
- /** Sets `required` on the Write textarea and marks the label. */
18
- required?: boolean;
19
- /** Forwarded to the Write textarea. */
20
- className?: string;
21
- /** Classes for the field wrapper (label row, body, and error). */
22
- fieldClassName?: string;
23
11
  /** Classes for the Preview surface, e.g. a taller `min-h-40`. */
24
12
  previewClassName?: string;
25
13
  }): import("react").JSX.Element;
@@ -1,19 +1,19 @@
1
1
  import { type ReactNode } from "react";
2
- import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
3
- type Props = ErrorTimingProps & {
2
+ import type { FormFieldBaseProps, FormFieldMetaProps } from "./field-error.js";
3
+ type Props = FormFieldBaseProps & FormFieldMetaProps & {
4
+ /** The selected item's value, or `undefined` for none. */
4
5
  value: string | undefined;
5
- meta?: FieldMetaState;
6
- label: string;
6
+ /** Called with the value of the item the person picks. */
7
7
  onValueChange: (value: string) => void;
8
+ /** Trigger text while no item is selected. */
8
9
  placeholder?: string;
9
10
  /** Requires a value (native form validation) and marks the label. */
10
11
  required?: boolean;
11
12
  /** Forwarded to the select trigger. */
12
13
  className?: string;
13
- /** Classes for the field wrapper (label, trigger, and error). */
14
- fieldClassName?: string;
15
14
  /** Forwarded to the select's option list (`SelectContent`). */
16
15
  contentClassName?: string;
16
+ /** The `SelectItem`s (and groups) to choose from. */
17
17
  children: ReactNode;
18
18
  };
19
19
  /** Select control with built-in field error text and invalid styling. */
@@ -1,14 +1,24 @@
1
- import type { ErrorTimingProps, FieldMetaState } from "./field-error.js";
1
+ import type { FieldMetaState, FormFieldBaseProps } from "./field-error.js";
2
2
  export type TextFieldApi = {
3
+ /** Field name; the default control id. */
3
4
  name: string;
4
5
  state: {
6
+ /** Current text. */
5
7
  value: string;
8
+ /** Error meta; its first error shows under the control. */
6
9
  meta: FieldMetaState;
7
10
  };
11
+ /** Called with the new text on every change. */
8
12
  handleChange: (value: string) => void;
13
+ /** Called when the control loses focus, to mark the field touched. */
9
14
  handleBlur: () => void;
10
15
  };
11
- export declare function FormTextField({ field, id, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, submitted, autoComplete, required, minLength, className, fieldClassName, }: ErrorTimingProps & {
16
+ /**
17
+ * Props the text and Markdown fields share: both bind to a TanStack Form
18
+ * `field` and render a text control.
19
+ */
20
+ export type TextFieldBaseProps = FormFieldBaseProps & {
21
+ /** The TanStack Form field (or the same shape) the control reads and writes. */
12
22
  field: TextFieldApi;
13
23
  /**
14
24
  * Control id the label points at. Defaults to `field.name`; pass a unique
@@ -16,21 +26,30 @@ export declare function FormTextField({ field, id, label, placeholder, descripti
16
26
  * `email`), or the second label focuses the first input.
17
27
  */
18
28
  id?: string;
19
- label: string;
29
+ /** Sets `required` on the text control and marks the label. */
30
+ required?: boolean;
31
+ /** Forwarded to the text control (the input or textarea). */
32
+ className?: string;
33
+ };
34
+ /**
35
+ * Text input, or a textarea with `multiline`, bound to a TanStack Form
36
+ * `field`, with a label, optional hint, and the `form/*` error text.
37
+ */
38
+ export declare function FormTextField({ field, id, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, submitted, autoComplete, required, minLength, className, fieldClassName, }: TextFieldBaseProps & {
39
+ /** Text shown in the empty control. */
20
40
  placeholder?: string;
21
41
  /** Optional hint under the label (employer vs referrer, etc.). */
22
42
  description?: string;
43
+ /** Focuses the input on mount (ignored with `multiline`). */
23
44
  autoFocus?: boolean;
45
+ /** Renders a textarea instead of an input. */
24
46
  multiline?: boolean;
25
- /** Textarea rows when `multiline` is set. */
47
+ /** Textarea rows when `multiline` is set. Default 3. */
26
48
  rows?: number;
49
+ /** The input's `type` when not `multiline`. Default "text". */
27
50
  inputType?: "text" | "email" | "password" | "date" | "datetime-local" | "time" | "number";
51
+ /** Forwarded to the control's `autoComplete`. */
28
52
  autoComplete?: string;
29
- /** Sets `required` on the control and marks the label. */
30
- required?: boolean;
53
+ /** Minimum text length (native form validation). */
31
54
  minLength?: number;
32
- /** Forwarded to the input or textarea. */
33
- className?: string;
34
- /** Classes for the field wrapper (label, hint, control, and error). */
35
- fieldClassName?: string;
36
55
  }): import("react").JSX.Element;
@@ -4,6 +4,10 @@ import { Input } from "../input.js";
4
4
  import { Textarea } from "../textarea.js";
5
5
  import { onInputChange } from "../utils.js";
6
6
  import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, metaErrorText, } from "./field-shell.js";
7
+ /**
8
+ * Text input, or a textarea with `multiline`, bound to a TanStack Form
9
+ * `field`, with a label, optional hint, and the `form/*` error text.
10
+ */
7
11
  export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError, submitted, autoComplete, required, minLength, className, fieldClassName, }) {
8
12
  const onChange = onInputChange(field.handleChange);
9
13
  const errorText = metaErrorText(field.state.meta, { submitted, showError });
@@ -0,0 +1,30 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FormFieldBaseProps, FormFieldDescriptionProps, FormFieldMetaProps } from "./field-error.js";
3
+ export type ToggleGroupFieldOption<T extends string> = {
4
+ /** Value passed to `onValueChange` when this item is pressed. */
5
+ value: T;
6
+ /** Content of the toggle item. */
7
+ label: ReactNode;
8
+ };
9
+ type Props<T extends string> = FormFieldBaseProps & FormFieldMetaProps & FormFieldDescriptionProps & {
10
+ /** The selected option, or `""` for none yet. */
11
+ value: T | "";
12
+ /** Called with an option's value; pressing the active item keeps it. */
13
+ onValueChange: (value: T) => void;
14
+ /** The items to choose from, in order. */
15
+ options: ToggleGroupFieldOption<T>[];
16
+ /** Marks the label. */
17
+ required?: boolean;
18
+ /** Disables every item in the group. */
19
+ disabled?: boolean;
20
+ /** Size of the toggle items. Default "sm". */
21
+ size?: "default" | "sm" | "lg";
22
+ /** Forwarded to the toggle group. */
23
+ className?: string;
24
+ };
25
+ /**
26
+ * One choice from a few as a joined outline toggle group (a segmented
27
+ * control), with a visible label and the `form/*` error wiring.
28
+ */
29
+ export declare function FormToggleGroupField<T extends string>({ label, description, value, onValueChange, options, meta, submitted, showError, required, disabled, size, className, fieldClassName, }: Props<T>): import("react").JSX.Element;
30
+ export {};
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
5
+ import { describedBy, descriptionIdFor, FormFieldShell, invalidWhen, labelIdFor, metaErrorText, } from "./field-shell.js";
6
+ /**
7
+ * One choice from a few as a joined outline toggle group (a segmented
8
+ * control), with a visible label and the `form/*` error wiring.
9
+ */
10
+ export function FormToggleGroupField({ label, description, value, onValueChange, options, meta, submitted, showError, required, disabled, size = "sm", className, fieldClassName, }) {
11
+ const id = useId();
12
+ const errorText = metaErrorText(meta, { submitted, showError });
13
+ function handleValueChange(next) {
14
+ const option = options.find((o) => o.value === next);
15
+ if (option)
16
+ onValueChange(option.value);
17
+ }
18
+ return (_jsx(FormFieldShell, { id: id, label: label, labelFor: false, description: description, errorText: errorText, required: required, className: fieldClassName, children: _jsx(ToggleGroup, { id: id, type: "single", value: value, onValueChange: handleValueChange, variant: "outline", size: size, spacing: 0, disabled: disabled, "aria-labelledby": labelIdFor(id), "aria-invalid": invalidWhen(errorText), "aria-describedby": describedBy(id, errorText, description ? descriptionIdFor(id) : undefined), className: className, children: options.map((option) => (_jsx(ToggleGroupItem, { value: option.value, children: option.label }, option.value))) }) }));
19
+ }
@@ -4,15 +4,24 @@ export type InfoTipLayout = "box" | "inline";
4
4
  export type InfoTipProps = {
5
5
  /** Required name for the icon button (the glyph has no visible text). */
6
6
  label: string;
7
+ /** Panel heading; also names the panel. Without it the panel uses `label`. */
7
8
  title?: string;
9
+ /** Muted text under the title. */
8
10
  description?: React.ReactNode;
9
11
  /** Extra body after the title and description. */
10
12
  children?: React.ReactNode;
11
- /** `inline` keeps the surrounding line height; the 44px hit area overflows. */
13
+ /**
14
+ * `inline` keeps the surrounding line height; the 44px hit area overflows.
15
+ * Default `box`.
16
+ */
12
17
  layout?: InfoTipLayout;
18
+ /** Panel side. Default `top`; Radix still flips on collision. */
13
19
  side?: React.ComponentProps<typeof PopoverContent>["side"];
20
+ /** Panel alignment against the trigger. Default `center`. */
14
21
  align?: React.ComponentProps<typeof PopoverContent>["align"];
22
+ /** Shown beside the title, e.g. a `Badge`. */
15
23
  badge?: React.ReactNode;
24
+ /** Replaces the default question-mark glyph. */
16
25
  icon?: React.ReactNode;
17
26
  /** Classes for the trigger button. */
18
27
  className?: string;
@@ -0,0 +1,8 @@
1
+ /** Not a public entry. The Slot prop that Button, Badge, and the links share. */
2
+ export type AsChildProps = {
3
+ /**
4
+ * Render the single child (e.g. a router `Link`) with these styles instead
5
+ * of the default element. Default `false`.
6
+ */
7
+ asChild?: boolean;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -9,9 +9,10 @@
9
9
  export declare const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
10
10
  /**
11
11
  * Date and date-time picker trigger: the form-control chrome, start-aligned,
12
- * with muted text while empty (`data-empty`).
12
+ * with muted text while empty (`data-empty`). Intrinsic padding makes one
13
+ * line 40px tall while longer values wrap and consumer height classes work.
13
14
  */
14
- export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background justify-start data-[empty=true]:text-muted-foreground";
15
+ export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-[empty=true]:text-muted-foreground";
15
16
  /** Calendar popover body: fits the viewport height and scrolls inside it. */
16
17
  export declare const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
17
18
  /** Gutter used when a calendar popover collides with the viewport. */
@@ -9,9 +9,10 @@
9
9
  export const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
10
10
  /**
11
11
  * Date and date-time picker trigger: the form-control chrome, start-aligned,
12
- * with muted text while empty (`data-empty`).
12
+ * with muted text while empty (`data-empty`). Intrinsic padding makes one
13
+ * line 40px tall while longer values wrap and consumer height classes work.
13
14
  */
14
- export const pickerTriggerClassName = `${formControlTriggerClassName} justify-start data-[empty=true]:text-muted-foreground`;
15
+ export const pickerTriggerClassName = `${formControlTriggerClassName} h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-[empty=true]:text-muted-foreground`;
15
16
  /** Calendar popover body: fits the viewport height and scrolls inside it. */
16
17
  export const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
17
18
  /** Gutter used when a calendar popover collides with the viewport. */
@@ -0,0 +1,55 @@
1
+ /** Column id → ordered card ids, derived from the consumer's `items`. */
2
+ export type KanbanGroups = Record<string, string[]>;
3
+ /** Drop zone ids for the drag-time action rail. */
4
+ export declare const KANBAN_COMPLETE_ID = "__kanban_complete__";
5
+ export declare const KANBAN_DELETE_ID = "__kanban_delete__";
6
+ export declare function buildKanbanGroups<T extends {
7
+ id: string;
8
+ }>(columns: {
9
+ id: string;
10
+ }[], items: T[], getColumnId: (item: T) => string): KanbanGroups;
11
+ /** What dnd-kit reports at drag end, reduced to plain ids. */
12
+ export type KanbanDropInput = {
13
+ itemId: string;
14
+ /** Droppable under the pointer: a column, a card, or a rail zone. */
15
+ targetId: string | null;
16
+ /** The target card's column, when the target is a card. */
17
+ targetGroup: string | null;
18
+ /** Column the card started in. */
19
+ initialGroup: string | null;
20
+ /** Column the card is over now. */
21
+ currentGroup: string | null;
22
+ /** Sortable index in the target column. */
23
+ index: number | undefined;
24
+ };
25
+ export type KanbanDrop = {
26
+ kind: "complete";
27
+ itemId: string;
28
+ } | {
29
+ kind: "delete";
30
+ itemId: string;
31
+ } | {
32
+ kind: "move";
33
+ itemId: string;
34
+ toColumnId: string;
35
+ beforeId: string | null;
36
+ afterId: string | null;
37
+ };
38
+ /**
39
+ * Turn a drop into a complete, delete, or move with the neighbours at the
40
+ * insertion slot. `null` when the drop changes nothing.
41
+ */
42
+ export declare function resolveKanbanDrop(input: KanbanDropInput, groups: KanbanGroups): KanbanDrop | null;
43
+ export type KanbanOverflowEdges = {
44
+ start: boolean;
45
+ end: boolean;
46
+ };
47
+ /** Whether a horizontal scroller has more content past either edge. */
48
+ export declare function kanbanOverflowEdges(scrollLeft: number, clientWidth: number, scrollWidth: number): KanbanOverflowEdges;
49
+ /** One "page" of columns: most of the viewport, at least one narrow column. */
50
+ export declare function kanbanScrollStepPx(clientWidth: number): number;
51
+ /** Smooth-scroll toward the start (`-1`) or end (`1`); no-op without a scroller. */
52
+ export declare function kanbanScrollBy(scroller: {
53
+ clientWidth: number;
54
+ scrollBy: (options: ScrollToOptions) => void;
55
+ } | null, direction: -1 | 1): void;
@@ -0,0 +1,80 @@
1
+ /** Drop zone ids for the drag-time action rail. */
2
+ export const KANBAN_COMPLETE_ID = "__kanban_complete__";
3
+ export const KANBAN_DELETE_ID = "__kanban_delete__";
4
+ export function buildKanbanGroups(columns, items, getColumnId) {
5
+ const groups = {};
6
+ for (const column of columns)
7
+ groups[column.id] = [];
8
+ for (const item of items) {
9
+ const columnId = getColumnId(item);
10
+ const group = groups[columnId];
11
+ if (!group) {
12
+ console.warn(`[KanbanBoard] item "${item.id}" maps to unknown column "${columnId}" and is not rendered`);
13
+ continue;
14
+ }
15
+ group.push(item.id);
16
+ }
17
+ return groups;
18
+ }
19
+ function targetColumn(input, groups) {
20
+ const { targetId, targetGroup, currentGroup } = input;
21
+ if (targetId != null) {
22
+ if (targetId in groups)
23
+ return targetId;
24
+ if (targetGroup != null)
25
+ return targetGroup;
26
+ for (const [columnId, ids] of Object.entries(groups)) {
27
+ if (ids.includes(targetId))
28
+ return columnId;
29
+ }
30
+ }
31
+ return currentGroup;
32
+ }
33
+ /**
34
+ * Turn a drop into a complete, delete, or move with the neighbours at the
35
+ * insertion slot. `null` when the drop changes nothing.
36
+ */
37
+ export function resolveKanbanDrop(input, groups) {
38
+ const { itemId, targetId, initialGroup, index: rawIndex } = input;
39
+ if (targetId === KANBAN_COMPLETE_ID)
40
+ return { kind: "complete", itemId };
41
+ if (targetId === KANBAN_DELETE_ID)
42
+ return { kind: "delete", itemId };
43
+ const toColumnId = targetColumn(input, groups);
44
+ if (!initialGroup || !toColumnId)
45
+ return null;
46
+ const columnItems = groups[toColumnId] ?? [];
47
+ const remaining = columnItems.filter((id) => id !== itemId);
48
+ const index = Math.max(0, Math.min(rawIndex ?? remaining.length, remaining.length));
49
+ if (initialGroup === toColumnId && index === columnItems.indexOf(itemId)) {
50
+ return null;
51
+ }
52
+ return {
53
+ kind: "move",
54
+ itemId,
55
+ toColumnId,
56
+ beforeId: index > 0 ? (remaining[index - 1] ?? null) : null,
57
+ afterId: remaining[index] ?? null,
58
+ };
59
+ }
60
+ /** Pixel slop so sub-pixel scrollWidth vs clientWidth does not flicker cues. */
61
+ const OVERFLOW_EDGE_PX = 8;
62
+ /** Whether a horizontal scroller has more content past either edge. */
63
+ export function kanbanOverflowEdges(scrollLeft, clientWidth, scrollWidth) {
64
+ const maxScroll = Math.max(0, scrollWidth - clientWidth);
65
+ return {
66
+ start: scrollLeft > OVERFLOW_EDGE_PX,
67
+ end: scrollLeft < maxScroll - OVERFLOW_EDGE_PX,
68
+ };
69
+ }
70
+ /** One "page" of columns: most of the viewport, at least one narrow column. */
71
+ export function kanbanScrollStepPx(clientWidth) {
72
+ return Math.max(140, Math.round(clientWidth * 0.75));
73
+ }
74
+ /** Smooth-scroll toward the start (`-1`) or end (`1`); no-op without a scroller. */
75
+ export function kanbanScrollBy(scroller, direction) {
76
+ scroller?.scrollBy({
77
+ left: direction * kanbanScrollStepPx(scroller.clientWidth),
78
+ behavior: "smooth",
79
+ });
80
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComponentProps } from "react";
2
+ import type { DragDropProvider } from "@dnd-kit/react";
3
+ type PluginsProp = NonNullable<ComponentProps<typeof DragDropProvider>["plugins"]>;
4
+ /**
5
+ * Board plugins. AutoScroller stays (edge scroll while dragging). Feedback
6
+ * stays because it positions and sizes `DragOverlay`; `feedback: "none"`
7
+ * would leave the overlay behind. The drop animation is off so it does not
8
+ * snap twice against an optimistic update.
9
+ */
10
+ export declare const configureKanbanPlugins: Extract<PluginsProp, (...args: never[]) => unknown>;
11
+ /**
12
+ * Cards leave out OptimisticSortingPlugin: it moves DOM nodes under React and
13
+ * throws `removeChild` on re-render (dnd-kit #1747). Column layout comes from
14
+ * the consumer's `items`.
15
+ */
16
+ export declare function configureCardSortablePlugins<T>(defaults: readonly T[]): T[];
17
+ export {};
@@ -0,0 +1,17 @@
1
+ import { Feedback } from "@dnd-kit/dom";
2
+ import { OptimisticSortingPlugin } from "@dnd-kit/dom/sortable";
3
+ /**
4
+ * Board plugins. AutoScroller stays (edge scroll while dragging). Feedback
5
+ * stays because it positions and sizes `DragOverlay`; `feedback: "none"`
6
+ * would leave the overlay behind. The drop animation is off so it does not
7
+ * snap twice against an optimistic update.
8
+ */
9
+ export const configureKanbanPlugins = (defaults) => defaults.map((plugin) => plugin === Feedback ? Feedback.configure({ dropAnimation: null }) : plugin);
10
+ /**
11
+ * Cards leave out OptimisticSortingPlugin: it moves DOM nodes under React and
12
+ * throws `removeChild` on re-render (dnd-kit #1747). Column layout comes from
13
+ * the consumer's `items`.
14
+ */
15
+ export function configureCardSortablePlugins(defaults) {
16
+ return defaults.filter((plugin) => plugin !== OptimisticSortingPlugin);
17
+ }
@@ -1,7 +1,9 @@
1
1
  /** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
2
2
  export declare const MD_MEDIA_QUERY = "(min-width: 768px)";
3
3
  /**
4
- * Subscribe to a CSS media query. `defaultMatches` covers SSR and environments
5
- * without `matchMedia` (jsdom unless the test stubs it).
4
+ * Subscribe to a CSS media query with `useSyncExternalStore`. The server
5
+ * render and hydration use `defaultMatches`, so the markup always agrees;
6
+ * React then re-renders with the real value. `defaultMatches` also covers
7
+ * environments without `matchMedia` (jsdom unless the test stubs it).
6
8
  */
7
9
  export declare function useMediaQuery(query: string, defaultMatches?: boolean): boolean;
@@ -2,26 +2,22 @@
2
2
  import * as React from "react";
3
3
  /** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
4
4
  export const MD_MEDIA_QUERY = "(min-width: 768px)";
5
+ function hasMatchMedia() {
6
+ return (typeof window !== "undefined" && typeof window.matchMedia === "function");
7
+ }
5
8
  /**
6
- * Subscribe to a CSS media query. `defaultMatches` covers SSR and environments
7
- * without `matchMedia` (jsdom unless the test stubs it).
9
+ * Subscribe to a CSS media query with `useSyncExternalStore`. The server
10
+ * render and hydration use `defaultMatches`, so the markup always agrees;
11
+ * React then re-renders with the real value. `defaultMatches` also covers
12
+ * environments without `matchMedia` (jsdom unless the test stubs it).
8
13
  */
9
14
  export function useMediaQuery(query, defaultMatches = false) {
10
- const [matches, setMatches] = React.useState(() => {
11
- if (typeof window === "undefined" ||
12
- typeof window.matchMedia !== "function") {
13
- return defaultMatches;
14
- }
15
- return window.matchMedia(query).matches;
16
- });
17
- React.useEffect(() => {
18
- if (typeof window.matchMedia !== "function")
19
- return;
15
+ const subscribe = React.useCallback((onChange) => {
16
+ if (!hasMatchMedia())
17
+ return () => { };
20
18
  const media = window.matchMedia(query);
21
- const onChange = () => setMatches(media.matches);
22
- onChange();
23
19
  media.addEventListener("change", onChange);
24
20
  return () => media.removeEventListener("change", onChange);
25
21
  }, [query]);
26
- return matches;
22
+ return React.useSyncExternalStore(subscribe, () => (hasMatchMedia() ? window.matchMedia(query).matches : defaultMatches), () => defaultMatches);
27
23
  }
@@ -0,0 +1,84 @@
1
+ import * as React from "react";
2
+ /**
3
+ * One board column. Columns render in the order given. `color` (any CSS
4
+ * color) draws a status dot; `badge` is trailing header meta such as a total.
5
+ */
6
+ export type KanbanColumn = {
7
+ /** Matches what `getColumnId` returns for the column's cards. */
8
+ id: string;
9
+ /** Header text. */
10
+ label: string;
11
+ /** Any CSS color; draws a status dot before the label. No dot when omitted. */
12
+ color?: string;
13
+ /** Trailing header meta under the card count, such as a total. */
14
+ badge?: React.ReactNode;
15
+ /** Replaces the board's `emptyLabel` for this column. */
16
+ emptyLabel?: string;
17
+ };
18
+ /**
19
+ * Positions of the cards on either side of the drop slot (`undefined` at a
20
+ * column edge). Feed them to a fractional-index helper to store the order.
21
+ */
22
+ export type KanbanDropTarget = {
23
+ /** `position` of the card above the drop slot; `undefined` at the top. */
24
+ beforePosition: number | undefined;
25
+ /** `position` of the card below the drop slot; `undefined` at the bottom. */
26
+ afterPosition: number | undefined;
27
+ };
28
+ export type KanbanBoardProps<T extends {
29
+ id: string;
30
+ position: number;
31
+ }> = {
32
+ /** The board's columns, in display order. */
33
+ columns: KanbanColumn[];
34
+ /** Every card, in display order; sort by `position` first. A card whose column id is unknown is not shown. */
35
+ items: T[];
36
+ /** The id of the column a card belongs in. */
37
+ getColumnId: (item: T) => string;
38
+ /** Every drop that changes a column or the order, including same-column reorders. */
39
+ onMove: (item: T, toColumnId: string, target: KanbanDropTarget) => void;
40
+ /** Shows a Finish zone while dragging. */
41
+ onComplete?: (itemId: string) => void;
42
+ /** Shows a Remove zone while dragging. */
43
+ onDelete?: (itemId: string) => void;
44
+ /**
45
+ * Card body; the board adds the grip, chrome, and sorting. Cards re-render
46
+ * only when their item changes while this and `onCardClick` keep their
47
+ * identity, so define them outside the render or wrap them in `useCallback`.
48
+ */
49
+ renderCard: (item: T) => React.ReactNode;
50
+ /** Makes the card body a button. */
51
+ onCardClick?: (item: T) => void;
52
+ /** Skeleton cards on a first load with no items; dims the board on a refetch. */
53
+ isLoading?: boolean;
54
+ /** Text in a column with no cards. Default `"Drop here"`. */
55
+ emptyLabel?: string;
56
+ /** Text on the Finish zone. Default `"Finish"`. */
57
+ completeLabel?: string;
58
+ /** Text on the Remove zone. Default `"Remove"`. */
59
+ deleteLabel?: string;
60
+ /** Classes for the horizontal scroller that holds the columns. */
61
+ scrollerClassName?: string;
62
+ /** Classes for every column, e.g. its width. */
63
+ columnClassName?: string;
64
+ /** Classes for every card, including the one under the pointer while dragging. */
65
+ cardClassName?: string;
66
+ } & Omit<React.ComponentProps<"div">, "children">;
67
+ /**
68
+ * Drag-and-drop board built on dnd-kit. The consumer owns `items` and maps
69
+ * each to a column; the board derives its layout from them, so an optimistic
70
+ * cache update is the only state. Cards drag by their grip, the body stays
71
+ * clickable, and off-screen columns get edge fades and scroll buttons.
72
+ */
73
+ declare function KanbanBoard<T extends {
74
+ id: string;
75
+ position: number;
76
+ }>({ columns, items, getColumnId, onMove, onComplete, onDelete, renderCard, onCardClick, isLoading, emptyLabel, completeLabel, deleteLabel, className, scrollerClassName, columnClassName, cardClassName, ...props }: KanbanBoardProps<T>): React.JSX.Element;
77
+ /** Board-shaped placeholder for a route `loading` file. */
78
+ declare function KanbanSkeleton({ columnCount, className, columnClassName, ...props }: {
79
+ /** How many placeholder columns to draw. Default 4. */
80
+ columnCount?: number;
81
+ /** Classes for every placeholder column, e.g. to match the board's width. */
82
+ columnClassName?: string;
83
+ } & Omit<React.ComponentProps<"div">, "children">): React.JSX.Element;
84
+ export { KanbanBoard, KanbanSkeleton };