atlas-react-kit 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/COMPONENTS.md +135 -41
  2. package/DESIGN_RULES.md +79 -20
  3. package/README.md +60 -11
  4. package/dist/accordion.d.ts +8 -0
  5. package/dist/accordion.js +20 -0
  6. package/dist/alert.d.ts +19 -0
  7. package/dist/alert.js +37 -0
  8. package/dist/avatar.d.ts +9 -0
  9. package/dist/avatar.js +16 -0
  10. package/dist/badge.d.ts +2 -2
  11. package/dist/badge.js +1 -1
  12. package/dist/breadcrumb.d.ts +16 -0
  13. package/dist/breadcrumb.js +33 -0
  14. package/dist/button.d.ts +6 -5
  15. package/dist/button.js +7 -6
  16. package/dist/calendar.d.ts +1 -1
  17. package/dist/calendar.js +20 -2
  18. package/dist/checkbox.js +1 -1
  19. package/dist/color-legend.d.ts +22 -0
  20. package/dist/color-legend.js +11 -0
  21. package/dist/combobox.d.ts +4 -1
  22. package/dist/combobox.js +11 -5
  23. package/dist/date-picker.js +14 -3
  24. package/dist/date-time-picker.d.ts +36 -0
  25. package/dist/date-time-picker.js +105 -0
  26. package/dist/dropdown-menu.d.ts +26 -0
  27. package/dist/dropdown-menu.js +56 -0
  28. package/dist/error-boundary.d.ts +1 -1
  29. package/dist/field.js +1 -1
  30. package/dist/form/date-time-picker-field.d.ts +16 -0
  31. package/dist/form/date-time-picker-field.js +12 -0
  32. package/dist/form/markdown-field.d.ts +17 -0
  33. package/dist/form/markdown-field.js +26 -0
  34. package/dist/form/text-field.d.ts +7 -1
  35. package/dist/form/text-field.js +3 -3
  36. package/dist/info-tip.d.ts +27 -0
  37. package/dist/info-tip.js +63 -0
  38. package/dist/input.js +1 -1
  39. package/dist/internal/form-control.d.ts +1 -1
  40. package/dist/internal/form-control.js +1 -1
  41. package/dist/internal/tabs-styles.js +3 -3
  42. package/dist/markdown.d.ts +18 -0
  43. package/dist/markdown.js +62 -0
  44. package/dist/meter.d.ts +22 -0
  45. package/dist/meter.js +17 -0
  46. package/dist/pagination.d.ts +18 -0
  47. package/dist/pagination.js +35 -0
  48. package/dist/progress.d.ts +8 -0
  49. package/dist/progress.js +13 -0
  50. package/dist/radio-group.d.ts +13 -0
  51. package/dist/radio-group.js +20 -0
  52. package/dist/search-picker.d.ts +30 -0
  53. package/dist/search-picker.js +100 -0
  54. package/dist/select.js +2 -2
  55. package/dist/slider.d.ts +20 -0
  56. package/dist/slider.js +20 -0
  57. package/dist/spinner.d.ts +8 -0
  58. package/dist/spinner.js +13 -0
  59. package/dist/stat-tile.d.ts +21 -0
  60. package/dist/stat-tile.js +17 -0
  61. package/dist/switch.d.ts +9 -0
  62. package/dist/switch.js +14 -0
  63. package/dist/table.d.ts +2 -0
  64. package/dist/table.js +26 -2
  65. package/dist/tabs.d.ts +6 -3
  66. package/dist/tabs.js +5 -2
  67. package/dist/textarea.js +1 -1
  68. package/dist/theme-provider.d.ts +2 -1
  69. package/dist/theme-provider.js +28 -4
  70. package/dist/toggle-group.js +2 -2
  71. package/dist/toggle.d.ts +11 -1
  72. package/dist/toggle.js +13 -3
  73. package/dist/tooltip.d.ts +15 -0
  74. package/dist/tooltip.js +26 -0
  75. package/package.json +92 -5
  76. package/skills/atlas-react-kit/SKILL.md +148 -0
  77. package/theme.css +28 -14
@@ -0,0 +1,17 @@
1
+ import type { TextFieldApi } from "./text-field.js";
2
+ /**
3
+ * GFM body field: Write is a textarea, Preview is the sanitized Markdown
4
+ * renderer. String-in/string-out — not a WYSIWYG document model.
5
+ */
6
+ export declare function FormMarkdownField({ field, id, label, placeholder, rows, showError, }: {
7
+ field: TextFieldApi;
8
+ /**
9
+ * Control id the label points at. Defaults to `field.name`; pass a unique
10
+ * id when two forms on one page share a field name.
11
+ */
12
+ id?: string;
13
+ label: string;
14
+ placeholder?: string;
15
+ rows?: number;
16
+ showError?: boolean;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Label } from "../label.js";
5
+ import { Markdown } from "../markdown.js";
6
+ import { Textarea } from "../textarea.js";
7
+ import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
8
+ import { onInputChange } from "../utils.js";
9
+ import { FieldError, getFieldErrorMessage } from "./field-error.js";
10
+ /**
11
+ * GFM body field: Write is a textarea, Preview is the sanitized Markdown
12
+ * renderer. String-in/string-out — not a WYSIWYG document model.
13
+ */
14
+ export function FormMarkdownField({ field, id = field.name, label, placeholder = "Markdown supported", rows = 5, showError = true, }) {
15
+ const [mode, setMode] = useState("write");
16
+ const onChange = onInputChange(field.handleChange);
17
+ const errorText = showError
18
+ ? getFieldErrorMessage(field.state.meta.errors[0])
19
+ : undefined;
20
+ const trimmed = field.state.value.trim();
21
+ function handleModeChange(value) {
22
+ if (value === "write" || value === "preview")
23
+ setMode(value);
24
+ }
25
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { htmlFor: id, children: label }), _jsxs(ToggleGroup, { type: "single", value: mode, onValueChange: handleModeChange, variant: "outline", size: "sm", spacing: 0, "aria-label": `${label} mode`, children: [_jsx(ToggleGroupItem, { value: "write", children: "Write" }), _jsx(ToggleGroupItem, { value: "preview", children: "Preview" })] })] }), mode === "write" ? (_jsx(Textarea, { id: id, value: field.state.value, onChange: onChange, onBlur: field.handleBlur, placeholder: placeholder, rows: rows, "aria-invalid": errorText ? true : undefined })) : (_jsx("div", { className: "min-h-16 rounded-none border border-input bg-background px-3 py-2", "aria-live": "polite", children: trimmed ? (_jsx(Markdown, { content: trimmed })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to preview" })) })), showError ? _jsx(FieldError, { text: errorText }) : null] }));
26
+ }
@@ -8,8 +8,14 @@ export type TextFieldApi = {
8
8
  handleChange: (value: string) => void;
9
9
  handleBlur: () => void;
10
10
  };
11
- export declare function FormTextField({ field, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, autoComplete, required, minLength, }: {
11
+ export declare function FormTextField({ field, id, label, placeholder, description, autoFocus, multiline, rows, inputType, showError, autoComplete, required, minLength, }: {
12
12
  field: TextFieldApi;
13
+ /**
14
+ * Control id the label points at. Defaults to `field.name`; pass a unique
15
+ * id when two forms on one page share a field name (login and sign-up
16
+ * `email`), or the second label focuses the first input.
17
+ */
18
+ id?: string;
13
19
  label: string;
14
20
  placeholder?: string;
15
21
  /** Optional hint under the label (employer vs referrer, etc.). */
@@ -5,13 +5,13 @@ import { Label } from "../label.js";
5
5
  import { Textarea } from "../textarea.js";
6
6
  import { onInputChange } from "../utils.js";
7
7
  import { FieldError, getFieldErrorMessage, } from "./field-error.js";
8
- export function FormTextField({ field, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError = true, autoComplete, required, minLength, }) {
8
+ export function FormTextField({ field, id = field.name, label, placeholder, description, autoFocus, multiline, rows = 3, inputType = "text", showError = true, autoComplete, required, minLength, }) {
9
9
  const onChange = onInputChange(field.handleChange);
10
10
  const errorText = showError
11
11
  ? getFieldErrorMessage(field.state.meta.errors[0])
12
12
  : undefined;
13
13
  const controlProps = {
14
- id: field.name,
14
+ id,
15
15
  value: field.state.value,
16
16
  onChange,
17
17
  onBlur: field.handleBlur,
@@ -21,5 +21,5 @@ export function FormTextField({ field, label, placeholder, description, autoFocu
21
21
  minLength,
22
22
  "aria-invalid": errorText ? true : undefined,
23
23
  };
24
- return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: field.name, children: label }), description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: description })) : null, multiline ? (_jsx(Textarea, { ...controlProps, rows: rows })) : (_jsx(Input, { ...controlProps, type: inputType, autoFocus: autoFocus })), showError ? _jsx(FieldError, { text: errorText }) : null] }));
24
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: description })) : null, multiline ? (_jsx(Textarea, { ...controlProps, rows: rows })) : (_jsx(Input, { ...controlProps, type: inputType, autoFocus: autoFocus })), showError ? _jsx(FieldError, { text: errorText }) : null] }));
25
25
  }
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { PopoverContent } from "./popover.js";
3
+ export type InfoTipLayout = "box" | "inline";
4
+ export type InfoTipProps = {
5
+ /** Required name for the icon button (the glyph has no visible text). */
6
+ label: string;
7
+ title?: string;
8
+ description?: React.ReactNode;
9
+ /** Extra body after the title and description. */
10
+ children?: React.ReactNode;
11
+ /** `inline` keeps the surrounding line height; the 44px hit area overflows. */
12
+ layout?: InfoTipLayout;
13
+ side?: React.ComponentProps<typeof PopoverContent>["side"];
14
+ align?: React.ComponentProps<typeof PopoverContent>["align"];
15
+ badge?: React.ReactNode;
16
+ icon?: React.ReactNode;
17
+ className?: string;
18
+ };
19
+ /**
20
+ * Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
21
+ * area. Hover opens only on a fine pointer; click, tap, and keyboard always
22
+ * toggle it. Not a glossary or "coming soon" chip — pass that copy in.
23
+ */
24
+ declare function InfoTip({ label, title, description, children, layout, side, align, badge, icon, className, }: InfoTipProps): React.JSX.Element;
25
+ /** InfoTip that sits in a line of text without stretching the line box. */
26
+ declare function InlineTip(props: Omit<InfoTipProps, "layout">): React.JSX.Element;
27
+ export { InfoTip, InlineTip };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { CircleHelpIcon } from "lucide-react";
5
+ import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, } from "./popover.js";
6
+ import { cn } from "./utils.js";
7
+ /**
8
+ * True when a mouse can hover. Touch and coarse pointers must use click;
9
+ * a hover-open popover would trap them after the first tap.
10
+ */
11
+ function canHoverOpen() {
12
+ return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
13
+ }
14
+ /**
15
+ * Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
16
+ * area. Hover opens only on a fine pointer; click, tap, and keyboard always
17
+ * toggle it. Not a glossary or "coming soon" chip — pass that copy in.
18
+ */
19
+ function InfoTip({ label, title, description, children, layout = "box", side = "top", align = "center", badge, icon, className, }) {
20
+ const [open, setOpen] = React.useState(false);
21
+ const closeTimer = React.useRef(null);
22
+ const hoverOpened = React.useRef(false);
23
+ const inline = layout === "inline";
24
+ const cancelClose = () => {
25
+ if (closeTimer.current !== null) {
26
+ window.clearTimeout(closeTimer.current);
27
+ closeTimer.current = null;
28
+ }
29
+ };
30
+ const openFromHover = () => {
31
+ if (!canHoverOpen())
32
+ return;
33
+ cancelClose();
34
+ hoverOpened.current = true;
35
+ setOpen(true);
36
+ };
37
+ const closeFromHover = () => {
38
+ if (!canHoverOpen() || !hoverOpened.current)
39
+ return;
40
+ cancelClose();
41
+ // Leave a beat so the pointer can travel from the glyph into the panel.
42
+ closeTimer.current = window.setTimeout(() => setOpen(false), 120);
43
+ };
44
+ React.useEffect(() => () => cancelClose(), []);
45
+ return (_jsxs(Popover, { open: open, onOpenChange: (next) => {
46
+ // Hover already opened the panel; the following click should pin
47
+ // it, not toggle it closed (Playwright and real mice both hover
48
+ // before they click).
49
+ if (hoverOpened.current && open && !next) {
50
+ hoverOpened.current = false;
51
+ return;
52
+ }
53
+ hoverOpened.current = false;
54
+ setOpen(next);
55
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn("relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground outline-none hover:text-selected-foreground focus-visible:border-ring focus-visible:text-selected-foreground focus-visible:ring-2 focus-visible:ring-ring", inline
56
+ ? "size-5 align-middle after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2"
57
+ : "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: icon ?? (_jsx(CircleHelpIcon, { className: "size-5", "data-icon": "info-tip", "aria-hidden": true })) }) }), _jsxs(PopoverContent, { side: side, align: align, className: "w-72", onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: [_jsxs(PopoverHeader, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [title ? _jsx(PopoverTitle, { children: title }) : null, badge] }), description ? (_jsx(PopoverDescription, { children: description })) : null] }), children] })] }));
58
+ }
59
+ /** InfoTip that sits in a line of text without stretching the line box. */
60
+ function InlineTip(props) {
61
+ return _jsx(InfoTip, { ...props, layout: "inline" });
62
+ }
63
+ export { InfoTip, InlineTip };
package/dist/input.js CHANGED
@@ -2,6 +2,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "./utils.js";
4
4
  function Input({ className, type, ...props }) {
5
- return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-10 w-full min-w-0 rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none 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 }));
5
+ return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-10 w-full min-w-0 rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none 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
6
  }
7
7
  export { Input };
@@ -6,4 +6,4 @@
6
6
  * control next to real form fields. Hover stays Input-like and cancels the
7
7
  * outline Button's purple selected wash.
8
8
  */
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:border-ring focus-visible:text-foreground focus-visible:ring-ring/30 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";
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:border-ring focus-visible:text-foreground focus-visible:ring-ring 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";
@@ -6,4 +6,4 @@
6
6
  * control next to real form fields. Hover stays Input-like and cancels the
7
7
  * outline Button's purple selected wash.
8
8
  */
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:border-ring focus-visible:text-foreground focus-visible:ring-ring/30 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";
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:border-ring focus-visible:text-foreground focus-visible:ring-ring 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";
@@ -11,9 +11,9 @@ const tabsListVariants = cva('group/tabs-list inline-flex w-fit items-stretch ju
11
11
  },
12
12
  });
13
13
  const tabsTriggerVariants = cva([
14
- 'relative inline-flex h-full min-w-0 flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
15
- 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state="active"]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent',
16
- 'data-[state="active"]:bg-selected data-[state="active"]:text-selected-foreground dark:data-[state="active"]:border-transparent dark:data-[state="active"]:bg-selected dark:data-[state="active"]:text-selected-foreground',
14
+ 'relative inline-flex h-full min-w-0 flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
15
+ 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:border-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent',
16
+ 'data-[state="active"]:border-selected-foreground data-[state="active"]:bg-selected data-[state="active"]:text-selected-foreground',
17
17
  'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation="horizontal"]/tabs:after:inset-x-0 group-data-[orientation="horizontal"]/tabs:after:bottom-[-5px] group-data-[orientation="horizontal"]/tabs:after:h-0.5 group-data-[orientation="vertical"]/tabs:after:inset-y-0 group-data-[orientation="vertical"]/tabs:after:-right-1 group-data-[orientation="vertical"]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state="active"]:after:opacity-100',
18
18
  ]);
19
19
  export { tabsListVariants, tabsTriggerVariants };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Drop javascript:/data:/unknown schemes. react-markdown already omits raw
3
+ * HTML (no rehype-raw), so this is the remaining URL hole.
4
+ */
5
+ export declare function safeMarkdownUrl(url: string): string;
6
+ export type MarkdownProps = {
7
+ content: string;
8
+ className?: string;
9
+ };
10
+ /**
11
+ * GitHub-flavoured Markdown, token-styled and sanitized. No Next, no raw
12
+ * HTML, no typography plugin. `react-markdown` and `remark-gfm` stay in this
13
+ * module so other subpaths do not pay for them.
14
+ */
15
+ declare function Markdown({ content, className }: MarkdownProps): import("react").JSX.Element;
16
+ /** Alias used by apps that imported MarkdownContent. */
17
+ declare const MarkdownContent: typeof Markdown;
18
+ export { Markdown, MarkdownContent };
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import { cn } from "./utils.js";
5
+ /** Stable identity so parent re-renders do not re-parse unchanged content. */
6
+ const REMARK_PLUGINS = [remarkGfm];
7
+ const SAFE_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
8
+ /**
9
+ * Drop javascript:/data:/unknown schemes. react-markdown already omits raw
10
+ * HTML (no rehype-raw), so this is the remaining URL hole.
11
+ */
12
+ export function safeMarkdownUrl(url) {
13
+ const value = url.trim();
14
+ if (!value)
15
+ return "";
16
+ if (value.startsWith("#") || value.startsWith("/"))
17
+ return value;
18
+ try {
19
+ const parsed = new URL(value);
20
+ return SAFE_PROTOCOLS.has(parsed.protocol) ? value : "";
21
+ }
22
+ catch {
23
+ return "";
24
+ }
25
+ }
26
+ function headingClass(size) {
27
+ return cn("font-heading font-semibold tracking-tight text-foreground", size);
28
+ }
29
+ const markdownComponents = {
30
+ h1: ({ children }) => _jsx("h1", { className: headingClass("text-xl"), children: children }),
31
+ h2: ({ children }) => _jsx("h2", { className: headingClass("text-lg"), children: children }),
32
+ h3: ({ children }) => (_jsx("h3", { className: headingClass("text-base"), children: children })),
33
+ p: ({ children }) => _jsx("p", { className: "text-sm leading-relaxed", children: children }),
34
+ ul: ({ children }) => (_jsx("ul", { className: "list-disc space-y-1 pl-5 text-sm", children: children })),
35
+ ol: ({ children }) => (_jsx("ol", { className: "list-decimal space-y-1 pl-5 text-sm", children: children })),
36
+ li: ({ children }) => _jsx("li", { className: "leading-relaxed", children: children }),
37
+ blockquote: ({ children }) => (_jsx("blockquote", { className: "border-l-2 border-input pl-3 text-sm text-muted-foreground", children: children })),
38
+ a: ({ href, children }) => href ? (_jsx("a", { href: href, className: "text-selected-foreground underline underline-offset-2", ...(href.startsWith("http")
39
+ ? { target: "_blank", rel: "noreferrer noopener" }
40
+ : {}), children: children })) : (_jsx("span", { children: children })),
41
+ code: ({ className, children }) => {
42
+ const fenced = /language-/.test(className ?? "");
43
+ return (_jsx("code", { className: cn("font-mono text-xs", fenced ? "bg-transparent" : "bg-muted px-1 py-0.5"), children: children }));
44
+ },
45
+ pre: ({ children }) => (_jsx("pre", { className: "overflow-x-auto bg-muted p-3 text-xs", children: children })),
46
+ hr: () => _jsx("hr", { className: "border-border" }),
47
+ table: ({ children }) => (_jsx("div", { className: "overflow-x-auto", children: _jsx("table", { className: "w-full text-left text-sm", children: children }) })),
48
+ th: ({ children }) => (_jsx("th", { className: "border-b border-input py-1.5 pr-3 font-medium", children: children })),
49
+ td: ({ children }) => (_jsx("td", { className: "border-b border-border py-1.5 pr-3", children: children })),
50
+ img: ({ src, alt }) => (_jsx("img", { src: src, alt: alt ?? "", className: "max-w-full" })),
51
+ };
52
+ /**
53
+ * GitHub-flavoured Markdown, token-styled and sanitized. No Next, no raw
54
+ * HTML, no typography plugin. `react-markdown` and `remark-gfm` stay in this
55
+ * module so other subpaths do not pay for them.
56
+ */
57
+ function Markdown({ content, className }) {
58
+ return (_jsx("article", { "data-slot": "markdown", className: cn("flex flex-col gap-3 text-foreground", className), children: _jsx(ReactMarkdown, { remarkPlugins: REMARK_PLUGINS, urlTransform: safeMarkdownUrl, components: markdownComponents, children: content }) }));
59
+ }
60
+ /** Alias used by apps that imported MarkdownContent. */
61
+ const MarkdownContent = Markdown;
62
+ export { Markdown, MarkdownContent };
@@ -0,0 +1,22 @@
1
+ export type MeterMarker = {
2
+ /** Same scale as `value` / `min` / `max` (not a 0–1 fraction). */
3
+ position: number;
4
+ className?: string;
5
+ };
6
+ export type MeterProps = {
7
+ value: number;
8
+ min?: number;
9
+ max: number;
10
+ /** App-owned fill (status, pace). Defaults to primary. */
11
+ fillClassName?: string;
12
+ markers?: MeterMarker[];
13
+ "aria-label": string;
14
+ className?: string;
15
+ };
16
+ /**
17
+ * Track, fill, and optional ticks for a current value against a scale.
18
+ * Use `Progress` for a plain 0–100 job bar. The app chooses fill color and
19
+ * marker positions (80% of a cap, 1.0× typical, and so on).
20
+ */
21
+ declare function Meter({ value, min, max, fillClassName, markers, "aria-label": ariaLabel, className, }: MeterProps): import("react").JSX.Element;
22
+ export { Meter };
package/dist/meter.js ADDED
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./utils.js";
3
+ /**
4
+ * Track, fill, and optional ticks for a current value against a scale.
5
+ * Use `Progress` for a plain 0–100 job bar. The app chooses fill color and
6
+ * marker positions (80% of a cap, 1.0× typical, and so on).
7
+ */
8
+ function Meter({ value, min = 0, max, fillClassName = "bg-primary", markers, "aria-label": ariaLabel, className, }) {
9
+ const span = max - min;
10
+ const toPercent = (amount) => {
11
+ if (span === 0)
12
+ return 0;
13
+ return Math.min(100, Math.max(0, ((amount - min) / span) * 100));
14
+ };
15
+ return (_jsxs("div", { role: "meter", "data-slot": "meter", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Math.min(max, Math.max(min, value)), "aria-label": ariaLabel, className: cn("relative h-1.5 w-full overflow-hidden bg-muted", className), children: [_jsx("div", { "data-slot": "meter-fill", className: cn("h-full", fillClassName), style: { width: `${toPercent(value)}%` } }), markers?.map((marker) => (_jsx("div", { "aria-hidden": true, "data-slot": "meter-marker", className: cn("absolute inset-y-0 w-px bg-foreground/40", marker.className), style: { left: `${toPercent(marker.position)}%` } }, marker.position)))] }));
16
+ }
17
+ export { Meter };
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Page links for long lists and tables. Links are plain anchors so the page
4
+ * lives in the URL; pass `onClick` with `href` for client-side routing.
5
+ */
6
+ declare function Pagination({ className, ...props }: React.ComponentProps<"nav">): React.JSX.Element;
7
+ declare function PaginationContent({ className, ...props }: React.ComponentProps<"ul">): React.JSX.Element;
8
+ declare function PaginationItem({ ...props }: React.ComponentProps<"li">): React.JSX.Element;
9
+ type PaginationLinkProps = {
10
+ isActive?: boolean;
11
+ size?: "default" | "icon";
12
+ } & React.ComponentProps<"a">;
13
+ /** The current page gets `aria-current="page"` and the selected wash. */
14
+ declare function PaginationLink({ className, isActive, size, ...props }: PaginationLinkProps): React.JSX.Element;
15
+ declare function PaginationPrevious({ className, children, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
16
+ declare function PaginationNext({ className, children, ...props }: React.ComponentProps<typeof PaginationLink>): React.JSX.Element;
17
+ declare function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
18
+ export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, };
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, } from "lucide-react";
4
+ import { buttonVariants } from "./button.js";
5
+ import { cn } from "./utils.js";
6
+ /**
7
+ * Page links for long lists and tables. Links are plain anchors so the page
8
+ * lives in the URL; pass `onClick` with `href` for client-side routing.
9
+ */
10
+ function Pagination({ className, ...props }) {
11
+ return (_jsx("nav", { "aria-label": "Pagination", "data-slot": "pagination", className: cn("mx-auto flex w-full justify-center", className), ...props }));
12
+ }
13
+ function PaginationContent({ className, ...props }) {
14
+ return (_jsx("ul", { "data-slot": "pagination-content", className: cn("flex flex-row items-center gap-1", className), ...props }));
15
+ }
16
+ function PaginationItem({ ...props }) {
17
+ return _jsx("li", { "data-slot": "pagination-item", ...props });
18
+ }
19
+ /** The current page gets `aria-current="page"` and the selected wash. */
20
+ function PaginationLink({ className, isActive, size = "icon", ...props }) {
21
+ return (_jsx("a", { "aria-current": isActive ? "page" : undefined, "data-slot": "pagination-link", "data-active": isActive, className: cn(buttonVariants({
22
+ variant: isActive ? "outline" : "ghost",
23
+ size: size === "icon" ? "icon-sm" : "sm",
24
+ }), "font-medium tracking-normal normal-case tabular-nums data-[active=true]:border-selected-foreground data-[active=true]:bg-selected data-[active=true]:text-selected-foreground", className), ...props }));
25
+ }
26
+ function PaginationPrevious({ className, children = "Previous", ...props }) {
27
+ return (_jsxs(PaginationLink, { "aria-label": "Go to previous page", size: "default", className: cn("gap-1 px-2.5 sm:pl-2", className), ...props, children: [_jsx(ChevronLeftIcon, { "data-icon": "inline-start" }), _jsx("span", { className: "hidden sm:block", children: children })] }));
28
+ }
29
+ function PaginationNext({ className, children = "Next", ...props }) {
30
+ return (_jsxs(PaginationLink, { "aria-label": "Go to next page", size: "default", className: cn("gap-1 px-2.5 sm:pr-2", className), ...props, children: [_jsx("span", { className: "hidden sm:block", children: children }), _jsx(ChevronRightIcon, { "data-icon": "inline-end" })] }));
31
+ }
32
+ function PaginationEllipsis({ className, ...props }) {
33
+ return (_jsxs("span", { "aria-hidden": true, "data-slot": "pagination-ellipsis", className: cn("flex size-9 items-center justify-center text-muted-foreground", className), ...props, children: [_jsx(MoreHorizontalIcon, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "More pages" })] }));
34
+ }
35
+ export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, };
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+ import { Progress as ProgressPrimitive } from "radix-ui";
3
+ /**
4
+ * Determinate progress (uploads, multi-step jobs). `value` is 0–100; omit
5
+ * it for an indeterminate state and pair the bar with a visible label.
6
+ */
7
+ declare function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>): React.JSX.Element;
8
+ export { Progress };
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Progress as ProgressPrimitive } from "radix-ui";
5
+ import { cn } from "./utils.js";
6
+ /**
7
+ * Determinate progress (uploads, multi-step jobs). `value` is 0–100; omit
8
+ * it for an indeterminate state and pair the bar with a visible label.
9
+ */
10
+ function Progress({ className, value, ...props }) {
11
+ return (_jsx(ProgressPrimitive.Root, { "data-slot": "progress", value: value, className: cn("relative h-2 w-full overflow-hidden rounded-none bg-primary/15", className), ...props, children: _jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", className: "h-full w-full flex-1 bg-primary transition-transform", style: { transform: `translateX(-${100 - (value ?? 0)}%)` } }) }));
12
+ }
13
+ export { Progress };
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
3
+ /**
4
+ * One choice from a short, visible list. Arrow keys move the selection.
5
+ * Use `Select` or `Combobox` when the list is long.
6
+ */
7
+ declare function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>): React.JSX.Element;
8
+ /**
9
+ * Radio dot. Radios stay round (a circle, per the shape rules) so they read
10
+ * differently from square checkboxes.
11
+ */
12
+ declare function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>): React.JSX.Element;
13
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
5
+ import { cn } from "./utils.js";
6
+ /**
7
+ * One choice from a short, visible list. Arrow keys move the selection.
8
+ * Use `Select` or `Combobox` when the list is long.
9
+ */
10
+ function RadioGroup({ className, ...props }) {
11
+ return (_jsx(RadioGroupPrimitive.Root, { "data-slot": "radio-group", className: cn("grid gap-3", className), ...props }));
12
+ }
13
+ /**
14
+ * Radio dot. Radios stay round (a circle, per the shape rules) so they read
15
+ * differently from square checkboxes.
16
+ */
17
+ function RadioGroupItem({ className, ...props }) {
18
+ return (_jsx(RadioGroupPrimitive.Item, { "data-slot": "radio-group-item", className: cn("peer relative flex aspect-square size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-transparent shadow-xs transition-shadow outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-checked:border-primary", className), ...props, children: _jsx(RadioGroupPrimitive.Indicator, { "data-slot": "radio-group-indicator", className: "flex items-center justify-center", children: _jsx("span", { className: "size-2 rounded-full bg-primary" }) }) }));
19
+ }
20
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Case-fold and strip combining marks so "nino" matches "Niño".
4
+ */
5
+ export declare function foldSearchText(value: string): string;
6
+ /**
7
+ * Default SearchPicker filter: the folded label includes the folded query.
8
+ */
9
+ export declare function defaultSearchFilter<T>(item: T, query: string, getLabel: (item: T) => string): boolean;
10
+ export type SearchPickerProps<T> = {
11
+ items: T[];
12
+ getValue: (item: T) => string;
13
+ getLabel: (item: T) => string;
14
+ filter?: (item: T, query: string, getLabel: (item: T) => string) => boolean;
15
+ value?: string;
16
+ onSelect: (item: T) => void;
17
+ label: string;
18
+ placeholder?: string;
19
+ emptyText?: string;
20
+ listLabel?: string;
21
+ disabled?: boolean;
22
+ className?: string;
23
+ };
24
+ /**
25
+ * Always-visible combobox: a labelled input and a listbox. Unlike Combobox,
26
+ * the list is not in a popover. Keyboard: arrows, Home/End, PageUp/PageDown,
27
+ * Enter, Escape.
28
+ */
29
+ declare function SearchPicker<T>({ items, getValue, getLabel, filter, value, onSelect, label, placeholder, emptyText, listLabel, disabled, className, }: SearchPickerProps<T>): React.JSX.Element;
30
+ export { SearchPicker };
@@ -0,0 +1,100 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Input } from "./input.js";
5
+ import { Label } from "./label.js";
6
+ import { cn } from "./utils.js";
7
+ const LISTBOX_PAGE_SIZE = 10;
8
+ /**
9
+ * Case-fold and strip combining marks so "nino" matches "Niño".
10
+ */
11
+ export function foldSearchText(value) {
12
+ return value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
13
+ }
14
+ /**
15
+ * Default SearchPicker filter: the folded label includes the folded query.
16
+ */
17
+ export function defaultSearchFilter(item, query, getLabel) {
18
+ const needle = foldSearchText(query.trim());
19
+ if (!needle)
20
+ return true;
21
+ return foldSearchText(getLabel(item)).includes(needle);
22
+ }
23
+ /**
24
+ * Always-visible combobox: a labelled input and a listbox. Unlike Combobox,
25
+ * the list is not in a popover. Keyboard: arrows, Home/End, PageUp/PageDown,
26
+ * Enter, Escape.
27
+ */
28
+ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter, value, onSelect, label, placeholder = "Search", emptyText = "No results", listLabel, disabled, className, }) {
29
+ const [query, setQuery] = React.useState("");
30
+ const listId = React.useId();
31
+ const inputId = React.useId();
32
+ const filtered = React.useMemo(() => items.filter((item) => filter(item, query, getLabel)), [items, filter, query, getLabel]);
33
+ const selectedIndex = React.useMemo(() => {
34
+ const index = filtered.findIndex((item) => getValue(item) === value);
35
+ return index >= 0 ? index : 0;
36
+ }, [filtered, getValue, value]);
37
+ const [highlight, setHighlight] = React.useState(selectedIndex);
38
+ React.useEffect(() => {
39
+ setHighlight(query ? 0 : selectedIndex);
40
+ }, [query, selectedIndex]);
41
+ const optionId = (index) => `${listId}-option-${index}`;
42
+ const active = filtered[highlight];
43
+ const scrollHighlightedOption = React.useCallback((node) => {
44
+ node?.scrollIntoView({ block: "nearest" });
45
+ }, []);
46
+ const selectItem = (item) => {
47
+ onSelect(item);
48
+ setQuery("");
49
+ };
50
+ const onKeyDown = (event) => {
51
+ if (event.key === "ArrowDown") {
52
+ event.preventDefault();
53
+ setHighlight((index) => filtered.length === 0 ? 0 : Math.min(index + 1, filtered.length - 1));
54
+ }
55
+ if (event.key === "ArrowUp") {
56
+ event.preventDefault();
57
+ setHighlight((index) => Math.max(index - 1, 0));
58
+ }
59
+ if (event.key === "Home") {
60
+ event.preventDefault();
61
+ setHighlight(0);
62
+ }
63
+ if (event.key === "End") {
64
+ event.preventDefault();
65
+ setHighlight(filtered.length === 0 ? 0 : filtered.length - 1);
66
+ }
67
+ if (event.key === "PageDown") {
68
+ event.preventDefault();
69
+ setHighlight((index) => filtered.length === 0
70
+ ? 0
71
+ : Math.min(index + LISTBOX_PAGE_SIZE, filtered.length - 1));
72
+ }
73
+ if (event.key === "PageUp") {
74
+ event.preventDefault();
75
+ setHighlight((index) => Math.max(index - LISTBOX_PAGE_SIZE, 0));
76
+ }
77
+ if (event.key === "Enter") {
78
+ event.preventDefault();
79
+ if (active)
80
+ selectItem(active);
81
+ }
82
+ if (event.key === "Escape") {
83
+ event.preventDefault();
84
+ if (query) {
85
+ setQuery("");
86
+ return;
87
+ }
88
+ event.currentTarget.blur();
89
+ }
90
+ };
91
+ return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": active ? optionId(highlight) : undefined, value: query, disabled: disabled, placeholder: placeholder, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown }), _jsx("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? label, className: "max-h-60 overflow-y-auto border border-input bg-background", children: filtered.length === 0 ? (_jsx("li", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText })) : (filtered.map((item, index) => {
92
+ const itemValue = getValue(item);
93
+ const isSelected = itemValue === value;
94
+ const isActive = index === highlight;
95
+ return (_jsx("li", { role: "none", children: _jsx("button", { ref: isActive ? scrollHighlightedOption : undefined, id: optionId(index), type: "button", role: "option", "aria-selected": isSelected, disabled: disabled, className: cn("relative flex w-full cursor-default items-center rounded-none py-2 pr-3 pl-3 text-left text-sm outline-hidden select-none", isSelected && "bg-selected text-selected-foreground", isActive && "shadow-[inset_2px_0_0_var(--color-ring)]", isActive &&
96
+ !isSelected &&
97
+ "bg-accent text-accent-foreground"), onMouseEnter: () => setHighlight(index), onClick: () => selectItem(item), children: _jsx("span", { className: "truncate", children: getLabel(item) }) }) }, itemValue));
98
+ })) })] }));
99
+ }
100
+ export { SearchPicker };