harebear-ui 0.5.0 → 0.7.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 (44) hide show
  1. package/AGENTS.md +19 -2
  2. package/README.md +108 -36
  3. package/bin/harebear-ui.mjs +66 -3
  4. package/dist/components/admin/status.d.ts +3 -1
  5. package/dist/components/admin/status.js +5 -2
  6. package/dist/components/ai/approval.d.ts +3 -1
  7. package/dist/components/ai/approval.js +6 -3
  8. package/dist/components/ai/chat.d.ts +5 -2
  9. package/dist/components/ai/chat.js +1 -1
  10. package/dist/components/ai/code-block.d.ts +5 -1
  11. package/dist/components/ai/code-block.js +5 -3
  12. package/dist/components/ai/conversation-list.d.ts +16 -2
  13. package/dist/components/ai/conversation-list.js +88 -37
  14. package/dist/components/ai/model-picker.d.ts +6 -1
  15. package/dist/components/ai/model-picker.js +43 -23
  16. package/dist/components/layout/media-item.d.ts +13 -0
  17. package/dist/components/layout/media-item.js +54 -0
  18. package/dist/components/layout/metric.d.ts +15 -0
  19. package/dist/components/layout/metric.js +51 -0
  20. package/dist/components/layout/show-more.d.ts +17 -0
  21. package/dist/components/layout/show-more.js +30 -0
  22. package/dist/components/layout/stack.d.ts +39 -0
  23. package/dist/components/layout/stack.js +114 -0
  24. package/dist/components/layout/text.d.ts +37 -0
  25. package/dist/components/layout/text.js +52 -0
  26. package/dist/components/theme/theme-toggle.d.ts +4 -1
  27. package/dist/components/theme/theme-toggle.js +11 -6
  28. package/dist/components/ui/badge.d.ts +4 -1
  29. package/dist/components/ui/badge.js +10 -1
  30. package/dist/components/ui/button.d.ts +3 -1
  31. package/dist/components/ui/button.js +13 -3
  32. package/dist/components/ui/empty.d.ts +4 -1
  33. package/dist/components/ui/empty.js +3 -2
  34. package/dist/components/ui/item.d.ts +4 -1
  35. package/dist/components/ui/item.js +7 -2
  36. package/dist/index.d.ts +8 -3
  37. package/dist/index.js +7 -2
  38. package/docs/COMPONENTS.md +96 -71
  39. package/docs/guides/agents.md +75 -0
  40. package/docs/guides/getting-started.md +217 -0
  41. package/docs/guides/theming.md +98 -0
  42. package/llms.txt +6 -1
  43. package/package.json +2 -1
  44. package/skills/harebear-ui/SKILL.md +1 -1
@@ -1,19 +1,35 @@
1
1
  "use client";
2
+ import { ConfirmDialog } from "../admin/confirm-dialog.js";
2
3
  import { StatusDot } from "../admin/status.js";
3
4
  import { Button } from "../ui/button.js";
4
5
  import { Empty, EmptyDescription } from "../ui/empty.js";
5
6
  import { useSidebarOptional } from "../ui/sidebar.js";
6
7
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
7
8
  import { cn } from "../../lib/utils.js";
9
+ import * as React from "react";
8
10
  import { jsx, jsxs } from "react/jsx-runtime";
9
- import { PlusIcon } from "lucide-react";
11
+ import { PlusIcon, Trash2Icon } from "lucide-react";
10
12
  // src/components/ai/conversation-list.tsx
11
13
  /**
12
14
  * Grouped list of chat threads for a sidebar. Inside a Sidebar it closes the mobile sheet on select,
13
15
  * and hides itself when the sidebar is collapsed to its icon rail.
14
16
  */
15
- function ConversationList({ groups, activeId, onSelect, renderAction, empty = "No conversations yet", className, ...props }) {
17
+ function ConversationList({ groups, activeId, onSelect, renderAction, onDelete, deleteDescription = "This cannot be undone.", empty = "No conversations yet", className, ...props }) {
16
18
  const sidebar = useSidebarOptional();
19
+ const [confirm, setConfirm] = React.useState(null);
20
+ const [confirmOpen, setConfirmOpen] = React.useState(false);
21
+ const deleteButton = (c) => /* @__PURE__ */ jsx(Button, {
22
+ variant: "ghost",
23
+ size: "icon-sm",
24
+ "aria-label": `Delete ${c.title}`,
25
+ title: typeof c.deleteDisabled === "string" ? c.deleteDisabled : "Delete conversation",
26
+ disabled: !!c.deleteDisabled,
27
+ onClick: () => {
28
+ setConfirm(c);
29
+ setConfirmOpen(true);
30
+ },
31
+ children: /* @__PURE__ */ jsx(Trash2Icon, {})
32
+ });
17
33
  const select = (id) => {
18
34
  onSelect?.(id);
19
35
  sidebar?.setOpenMobile(false);
@@ -24,43 +40,57 @@ function ConversationList({ groups, activeId, onSelect, renderAction, empty = "N
24
40
  "aria-label": "Conversations",
25
41
  className: cn("flex flex-col gap-4.5 group-data-[collapsible=icon]:hidden", className),
26
42
  ...props,
27
- children: [visible.map((g) => /* @__PURE__ */ jsxs("div", {
28
- className: "flex flex-col gap-0.5",
29
- children: [/* @__PURE__ */ jsx("div", {
30
- className: "px-3 pb-1.5 text-xs font-medium text-muted-foreground",
31
- children: g.label
32
- }), g.items.map((c) => {
33
- const active = c.id === activeId;
34
- const action = renderAction?.(c);
35
- return /* @__PURE__ */ jsxs("div", {
36
- "data-slot": "conversation-item",
37
- "data-active": active || void 0,
38
- className: cn("group/conversation flex min-w-0 items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", active && "bg-card text-foreground shadow-card hover:bg-card"),
39
- children: [/* @__PURE__ */ jsxs("button", {
40
- type: "button",
41
- "aria-current": active ? "page" : void 0,
42
- onClick: () => select(c.id),
43
- className: cn("flex min-w-0 flex-1 flex-col rounded-md py-2.25 pl-3 text-left font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/40", action ? "pr-1" : "pr-3"),
44
- children: [/* @__PURE__ */ jsx("span", {
45
- className: "truncate",
46
- children: c.title
47
- }), c.description !== void 0 && /* @__PURE__ */ jsxs("span", {
48
- className: "flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground tabular-nums",
49
- children: [c.tone && /* @__PURE__ */ jsx(StatusDot, {
50
- tone: c.tone,
51
- pulse: c.pulse
52
- }), /* @__PURE__ */ jsx("span", {
43
+ children: [
44
+ visible.map((g) => /* @__PURE__ */ jsxs("div", {
45
+ className: "flex flex-col gap-0.5",
46
+ children: [/* @__PURE__ */ jsx("div", {
47
+ className: "px-3 pb-1.5 text-xs font-medium text-muted-foreground",
48
+ children: g.label
49
+ }), g.items.map((c) => {
50
+ const active = c.id === activeId;
51
+ const action = renderAction ? renderAction(c) : onDelete ? deleteButton(c) : null;
52
+ return /* @__PURE__ */ jsxs("div", {
53
+ "data-slot": "conversation-item",
54
+ "data-active": active || void 0,
55
+ className: cn("group/conversation flex min-w-0 items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", active && "bg-card text-foreground shadow-card hover:bg-card"),
56
+ children: [/* @__PURE__ */ jsxs("button", {
57
+ type: "button",
58
+ "aria-current": active ? "page" : void 0,
59
+ onClick: () => select(c.id),
60
+ className: cn("flex min-w-0 flex-1 flex-col rounded-md py-2.25 pl-3 text-left font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/40", action ? "pr-1" : "pr-3"),
61
+ children: [/* @__PURE__ */ jsx("span", {
53
62
  className: "truncate",
54
- children: c.description
63
+ children: c.title
64
+ }), c.description !== void 0 && /* @__PURE__ */ jsxs("span", {
65
+ className: "flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground tabular-nums",
66
+ children: [c.tone && /* @__PURE__ */ jsx(StatusDot, {
67
+ tone: c.tone,
68
+ pulse: c.pulse
69
+ }), /* @__PURE__ */ jsx("span", {
70
+ className: "truncate",
71
+ children: c.description
72
+ })]
55
73
  })]
74
+ }), action && /* @__PURE__ */ jsx("div", {
75
+ className: "mr-1 flex shrink-0 items-center [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/conversation:opacity-100 [@media(hover:hover)]:group-focus-within/conversation:opacity-100",
76
+ children: action
56
77
  })]
57
- }), action && /* @__PURE__ */ jsx("div", {
58
- className: "mr-1 flex shrink-0 items-center [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/conversation:opacity-100 [@media(hover:hover)]:group-focus-within/conversation:opacity-100",
59
- children: action
60
- })]
61
- }, c.id);
62
- })]
63
- }, g.label)), !visible.length && empty && /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(EmptyDescription, { children: empty }) })]
78
+ }, c.id);
79
+ })]
80
+ }, g.label)),
81
+ !visible.length && empty && /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(EmptyDescription, { children: empty }) }),
82
+ onDelete && /* @__PURE__ */ jsx(ConfirmDialog, {
83
+ open: confirmOpen,
84
+ onOpenChange: setConfirmOpen,
85
+ title: `Delete “${confirm?.title ?? ""}”?`,
86
+ description: deleteDescription,
87
+ confirmLabel: "Delete",
88
+ destructive: true,
89
+ onConfirm: () => {
90
+ if (confirm) onDelete(confirm);
91
+ }
92
+ })
93
+ ]
64
94
  });
65
95
  }
66
96
  /**
@@ -101,6 +131,27 @@ function groupByDate(items, getDate, toConversation, now = Date.now()) {
101
131
  return groups.filter((g) => g.items.length);
102
132
  }
103
133
  /**
134
+ * Short timestamp for lists and headers: the time today ("9:41 AM"), the date this year ("Nov 3"), else with the
135
+ * year ("Nov 3, 2025"). Uses the browser locale unless `locale` is given. Pass `now` for deterministic tests.
136
+ */
137
+ function formatWhen(date, now = Date.now(), locale) {
138
+ const d = new Date(date);
139
+ const n = new Date(now);
140
+ if (d.toDateString() === n.toDateString()) return d.toLocaleTimeString(locale, {
141
+ hour: "numeric",
142
+ minute: "2-digit"
143
+ });
144
+ if (d.getFullYear() === n.getFullYear()) return d.toLocaleDateString(locale, {
145
+ month: "short",
146
+ day: "numeric"
147
+ });
148
+ return d.toLocaleDateString(locale, {
149
+ month: "short",
150
+ day: "numeric",
151
+ year: "numeric"
152
+ });
153
+ }
154
+ /**
104
155
  * Full-width "New conversation" button for a sidebar header. In the icon rail it shrinks to an icon with a
105
156
  * tooltip; on mobile it closes the sidebar sheet after the click.
106
157
  */
@@ -136,4 +187,4 @@ function NewConversationButton({ label = "New conversation", icon = /* @__PURE__
136
187
  children: label
137
188
  })] });
138
189
  }
139
- export { ConversationList, NewConversationButton, groupByDate };
190
+ export { ConversationList, NewConversationButton, formatWhen, groupByDate };
@@ -7,6 +7,8 @@ export interface ModelOption {
7
7
  description?: React.ReactNode;
8
8
  /** Availability dot. Default "success". */
9
9
  status?: StatusTone;
10
+ /** Section heading in the menu, e.g. the provider. Models with the same group are listed together. */
11
+ group?: string;
10
12
  }
11
13
  export interface ModelPickerProps {
12
14
  models: ModelOption[];
@@ -16,6 +18,9 @@ export interface ModelPickerProps {
16
18
  className?: string;
17
19
  disabled?: boolean;
18
20
  }
19
- /** Pill button showing the active model with a status dot; opens a menu of models. */
21
+ /**
22
+ * Pill button showing the active model with a status dot; opens a menu of models, sectioned by `group`.
23
+ * A `value` missing from `models` is still listed (and checked) at the top.
24
+ */
20
25
  declare function ModelPicker({ models, value, onValueChange, label, className, disabled }: ModelPickerProps): React.JSX.Element;
21
26
  export { ModelPicker };
@@ -1,14 +1,27 @@
1
1
  "use client";
2
2
  import { StatusDot } from "../admin/status.js";
3
3
  import { Button } from "../ui/button.js";
4
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
5
5
  import { cn } from "../../lib/utils.js";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { CheckIcon, ChevronDownIcon } from "lucide-react";
8
8
  // src/components/ai/model-picker.tsx
9
- /** Pill button showing the active model with a status dot; opens a menu of models. */
9
+ /**
10
+ * Pill button showing the active model with a status dot; opens a menu of models, sectioned by `group`.
11
+ * A `value` missing from `models` is still listed (and checked) at the top.
12
+ */
10
13
  function ModelPicker({ models, value, onValueChange, label = "Model", className, disabled }) {
11
14
  const current = models.find((m) => m.id === value);
15
+ const all = current || !value ? models : [{ id: value }, ...models];
16
+ const groups = [];
17
+ for (const m of all) {
18
+ const g = groups.find((x) => x.name === m.group);
19
+ if (g) g.items.push(m);
20
+ else groups.push({
21
+ name: m.group,
22
+ items: [m]
23
+ });
24
+ }
12
25
  return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
13
26
  asChild: true,
14
27
  disabled,
@@ -29,27 +42,34 @@ function ModelPicker({ models, value, onValueChange, label = "Model", className,
29
42
  }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
30
43
  align: "end",
31
44
  className: "min-w-56",
32
- children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: label }), models.map((m) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
33
- onSelect: () => onValueChange(m.id),
34
- className: "items-start gap-2.5",
35
- children: [
36
- /* @__PURE__ */ jsx(StatusDot, {
37
- tone: m.status ?? "success",
38
- className: "mt-1.5"
39
- }),
40
- /* @__PURE__ */ jsxs("div", {
41
- className: "flex min-w-0 flex-1 flex-col",
42
- children: [/* @__PURE__ */ jsx("span", {
43
- className: "font-mono text-xs",
44
- children: m.label ?? m.id
45
- }), m.description && /* @__PURE__ */ jsx("span", {
46
- className: "text-xs text-muted-foreground",
47
- children: m.description
48
- })]
49
- }),
50
- m.id === value && /* @__PURE__ */ jsx(CheckIcon, { className: "mt-0.5" })
51
- ]
52
- }, m.id))]
45
+ children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: label }), groups.map((g, i) => /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
46
+ i > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
47
+ g.name && /* @__PURE__ */ jsx(DropdownMenuLabel, {
48
+ className: "text-xs font-medium text-muted-foreground",
49
+ children: g.name
50
+ }),
51
+ g.items.map((m) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
52
+ onSelect: () => onValueChange(m.id),
53
+ className: "items-start gap-2.5",
54
+ children: [
55
+ /* @__PURE__ */ jsx(StatusDot, {
56
+ tone: m.status ?? "success",
57
+ className: "mt-1.5"
58
+ }),
59
+ /* @__PURE__ */ jsxs("div", {
60
+ className: "flex min-w-0 flex-1 flex-col",
61
+ children: [/* @__PURE__ */ jsx("span", {
62
+ className: "font-mono text-xs",
63
+ children: m.label ?? m.id
64
+ }), m.description && /* @__PURE__ */ jsx("span", {
65
+ className: "text-xs text-muted-foreground",
66
+ children: m.description
67
+ })]
68
+ }),
69
+ m.id === value && /* @__PURE__ */ jsx(CheckIcon, { className: "mt-0.5" })
70
+ ]
71
+ }, m.id))
72
+ ] }, g.name ?? `group-${i}`))]
53
73
  })] });
54
74
  }
55
75
  export { ModelPicker };
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ export interface MediaItemProps extends Omit<React.ComponentProps<"div">, "title"> {
3
+ title: React.ReactNode;
4
+ /** Opens in a new tab. */
5
+ href?: string;
6
+ image?: string;
7
+ imageShape?: "poster" | "square" | "wide";
8
+ meta?: React.ReactNode;
9
+ description?: React.ReactNode;
10
+ }
11
+ /** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
12
+ declare function MediaItem({ title, href, image, imageShape, meta, description, className, ...props }: MediaItemProps): React.JSX.Element;
13
+ export { MediaItem };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ // src/components/layout/media-item.tsx
5
+ const shapeClass = {
6
+ poster: "aspect-[2/3] w-14",
7
+ square: "aspect-square w-12",
8
+ wide: "aspect-video w-24"
9
+ };
10
+ /** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
11
+ function MediaItem({ title, href, image, imageShape = "poster", meta, description, className, ...props }) {
12
+ const img = image ? /* @__PURE__ */ jsx("img", {
13
+ src: image,
14
+ alt: "",
15
+ loading: "lazy",
16
+ className: cn("shrink-0 rounded-md bg-muted object-cover", shapeClass[imageShape])
17
+ }) : null;
18
+ return /* @__PURE__ */ jsxs("div", {
19
+ "data-slot": "media-item",
20
+ className: cn("flex min-w-0 items-start gap-3", className),
21
+ ...props,
22
+ children: [img && href ? /* @__PURE__ */ jsx("a", {
23
+ href,
24
+ target: "_blank",
25
+ rel: "noreferrer",
26
+ tabIndex: -1,
27
+ className: "shrink-0",
28
+ children: img
29
+ }) : img, /* @__PURE__ */ jsxs("div", {
30
+ className: "flex min-w-0 flex-col gap-0.5",
31
+ children: [
32
+ /* @__PURE__ */ jsx("span", {
33
+ className: "line-clamp-2 font-medium",
34
+ children: href ? /* @__PURE__ */ jsx("a", {
35
+ href,
36
+ target: "_blank",
37
+ rel: "noreferrer",
38
+ className: "outline-none hover:underline focus-visible:underline",
39
+ children: title
40
+ }) : title
41
+ }),
42
+ meta && /* @__PURE__ */ jsx("span", {
43
+ className: "text-xs text-muted-foreground",
44
+ children: meta
45
+ }),
46
+ description && /* @__PURE__ */ jsx("span", {
47
+ className: "line-clamp-2 text-[13px] text-muted-foreground",
48
+ children: description
49
+ })
50
+ ]
51
+ })]
52
+ });
53
+ }
54
+ export { MediaItem };
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ export interface MetricProps extends Omit<React.ComponentProps<"div">, "children"> {
3
+ label: React.ReactNode;
4
+ value: React.ReactNode;
5
+ unit?: React.ReactNode;
6
+ /** Percent change. Positive reads as good unless `lowerIsBetter`. */
7
+ delta?: number;
8
+ lowerIsBetter?: boolean;
9
+ align?: "start" | "center";
10
+ }
11
+ /** A number with a unit, a label under it, and an optional percent change over it. */
12
+ declare function Metric({ label, value, unit, delta, lowerIsBetter, align, className, ...props }: MetricProps): React.JSX.Element;
13
+ /** Metrics side by side, evenly spaced. */
14
+ declare function MetricGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
15
+ export { Metric, MetricGroup };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react";
5
+ // src/components/layout/metric.tsx
6
+ /** A number with a unit, a label under it, and an optional percent change over it. */
7
+ function Metric({ label, value, unit, delta, lowerIsBetter, align = "center", className, ...props }) {
8
+ const good = delta === void 0 || delta === 0 ? void 0 : lowerIsBetter ? delta < 0 : delta > 0;
9
+ return /* @__PURE__ */ jsxs("div", {
10
+ "data-slot": "metric",
11
+ className: cn("flex min-w-0 flex-col gap-0.5", align === "center" ? "items-center text-center" : "items-start", className),
12
+ ...props,
13
+ children: [
14
+ delta !== void 0 && Number.isFinite(delta) && /* @__PURE__ */ jsxs("span", {
15
+ className: cn("inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", good === void 0 ? "text-muted-foreground" : good ? "text-success" : "text-destructive"),
16
+ children: [
17
+ delta > 0 ? "+" : "",
18
+ delta.toFixed(1),
19
+ "%",
20
+ delta > 0 ? /* @__PURE__ */ jsx(ArrowUpRightIcon, {
21
+ "aria-hidden": true,
22
+ className: "size-3.5"
23
+ }) : delta < 0 ? /* @__PURE__ */ jsx(ArrowDownRightIcon, {
24
+ "aria-hidden": true,
25
+ className: "size-3.5"
26
+ }) : null
27
+ ]
28
+ }),
29
+ /* @__PURE__ */ jsxs("span", {
30
+ className: "text-xl font-semibold tracking-tight whitespace-nowrap tabular-nums @sm:text-2xl",
31
+ children: [value, unit && /* @__PURE__ */ jsx("span", {
32
+ className: "ml-0.5 text-xs font-medium text-muted-foreground",
33
+ children: unit
34
+ })]
35
+ }),
36
+ /* @__PURE__ */ jsx("span", {
37
+ className: "text-xs font-medium tracking-wide text-muted-foreground uppercase",
38
+ children: label
39
+ })
40
+ ]
41
+ });
42
+ }
43
+ /** Metrics side by side, evenly spaced. */
44
+ function MetricGroup({ className, ...props }) {
45
+ return /* @__PURE__ */ jsx("div", {
46
+ "data-slot": "metric-group",
47
+ className: cn("grid auto-cols-fr grid-flow-col gap-2", className),
48
+ ...props
49
+ });
50
+ }
51
+ export { Metric, MetricGroup };
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ declare const gapClass: {
3
+ readonly none: "gap-0";
4
+ readonly xs: "gap-1";
5
+ readonly sm: "gap-2";
6
+ readonly md: "gap-3";
7
+ readonly lg: "gap-4";
8
+ readonly xl: "gap-6";
9
+ };
10
+ export interface ShowMoreProps extends React.ComponentProps<"div"> {
11
+ /** Children shown before the "Show N more" button. Default 5. */
12
+ initial?: number;
13
+ gap?: keyof typeof gapClass;
14
+ }
15
+ /** A list that collapses after `initial` children, with a button to show the rest. */
16
+ declare function ShowMore({ initial, gap, className, children, ...props }: ShowMoreProps): React.JSX.Element;
17
+ export { ShowMore };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import * as React from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ // src/components/layout/show-more.tsx
6
+ const gapClass = {
7
+ none: "gap-0",
8
+ xs: "gap-1",
9
+ sm: "gap-2",
10
+ md: "gap-3",
11
+ lg: "gap-4",
12
+ xl: "gap-6"
13
+ };
14
+ /** A list that collapses after `initial` children, with a button to show the rest. */
15
+ function ShowMore({ initial = 5, gap = "md", className, children, ...props }) {
16
+ const [open, setOpen] = React.useState(false);
17
+ const all = React.Children.toArray(children);
18
+ return /* @__PURE__ */ jsxs("div", {
19
+ "data-slot": "show-more",
20
+ className: cn("flex min-w-0 flex-col", gapClass[gap], className),
21
+ ...props,
22
+ children: [open ? all : all.slice(0, initial), all.length > initial && /* @__PURE__ */ jsx("button", {
23
+ type: "button",
24
+ onClick: () => setOpen(!open),
25
+ className: "self-start rounded-md text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20",
26
+ children: open ? "Show less" : `Show ${all.length - initial} more`
27
+ })]
28
+ });
29
+ }
30
+ export { ShowMore };
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ export type StackSpace = "none" | "xs" | "sm" | "md" | "lg" | "xl";
3
+ export type StackAlign = "start" | "center" | "end" | "stretch" | "baseline";
4
+ export type StackJustify = "start" | "center" | "end" | "between";
5
+ export interface StackProps extends React.ComponentProps<"div"> {
6
+ gap?: StackSpace;
7
+ align?: StackAlign;
8
+ }
9
+ /** Vertical stack with a named gap. */
10
+ declare function Stack({ gap, align, className, ...props }: StackProps): React.JSX.Element;
11
+ export interface RowProps extends React.ComponentProps<"div"> {
12
+ gap?: StackSpace;
13
+ align?: StackAlign;
14
+ justify?: StackJustify;
15
+ wrap?: boolean;
16
+ }
17
+ /** Horizontal row with a named gap. */
18
+ declare function Row({ gap, align, justify, wrap, className, ...props }: RowProps): React.JSX.Element;
19
+ export type GridCols = 1 | 2 | 3 | 4 | 5 | 6;
20
+ export type ResponsiveGridCols = GridCols | {
21
+ base?: GridCols;
22
+ sm?: GridCols;
23
+ md?: GridCols;
24
+ lg?: GridCols;
25
+ };
26
+ export interface GridProps extends React.ComponentProps<"div"> {
27
+ /** Equal columns: a number, or per breakpoint `{ base: 1, sm: 2, md: 3 }`. */
28
+ cols?: ResponsiveGridCols;
29
+ gap?: StackSpace;
30
+ /**
31
+ * What sm/md/lg measure. "viewport" (default): the screen, Tailwind's sm/md/lg.
32
+ * "container": the nearest `@container` ancestor (sm >= 28rem, md >= 42rem, lg >= 56rem),
33
+ * for blocks that sit in cards of any width.
34
+ */
35
+ query?: "viewport" | "container";
36
+ }
37
+ /** Grid of equal columns with named gaps and per-breakpoint column counts. */
38
+ declare function Grid({ cols: c, gap, query, className, ...props }: GridProps): React.JSX.Element;
39
+ export { Grid, Row, Stack };
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ // src/components/layout/stack.tsx
5
+ const gapClass = {
6
+ none: "gap-0",
7
+ xs: "gap-1",
8
+ sm: "gap-2",
9
+ md: "gap-3",
10
+ lg: "gap-4",
11
+ xl: "gap-6"
12
+ };
13
+ const alignClass = {
14
+ start: "items-start",
15
+ center: "items-center",
16
+ end: "items-end",
17
+ stretch: "items-stretch",
18
+ baseline: "items-baseline"
19
+ };
20
+ const justifyClass = {
21
+ start: "justify-start",
22
+ center: "justify-center",
23
+ end: "justify-end",
24
+ between: "justify-between"
25
+ };
26
+ /** Vertical stack with a named gap. */
27
+ function Stack({ gap = "md", align = "stretch", className, ...props }) {
28
+ return /* @__PURE__ */ jsx("div", {
29
+ "data-slot": "stack",
30
+ className: cn("flex min-w-0 flex-col", gapClass[gap], alignClass[align], className),
31
+ ...props
32
+ });
33
+ }
34
+ /** Horizontal row with a named gap. */
35
+ function Row({ gap = "sm", align = "center", justify = "start", wrap, className, ...props }) {
36
+ return /* @__PURE__ */ jsx("div", {
37
+ "data-slot": "row",
38
+ className: cn("flex min-w-0", gapClass[gap], alignClass[align], justifyClass[justify], wrap && "flex-wrap", className),
39
+ ...props
40
+ });
41
+ }
42
+ const cols = {
43
+ base: {
44
+ 1: "grid-cols-1",
45
+ 2: "grid-cols-2",
46
+ 3: "grid-cols-3",
47
+ 4: "grid-cols-4",
48
+ 5: "grid-cols-5",
49
+ 6: "grid-cols-6"
50
+ },
51
+ viewport: {
52
+ sm: {
53
+ 1: "sm:grid-cols-1",
54
+ 2: "sm:grid-cols-2",
55
+ 3: "sm:grid-cols-3",
56
+ 4: "sm:grid-cols-4",
57
+ 5: "sm:grid-cols-5",
58
+ 6: "sm:grid-cols-6"
59
+ },
60
+ md: {
61
+ 1: "md:grid-cols-1",
62
+ 2: "md:grid-cols-2",
63
+ 3: "md:grid-cols-3",
64
+ 4: "md:grid-cols-4",
65
+ 5: "md:grid-cols-5",
66
+ 6: "md:grid-cols-6"
67
+ },
68
+ lg: {
69
+ 1: "lg:grid-cols-1",
70
+ 2: "lg:grid-cols-2",
71
+ 3: "lg:grid-cols-3",
72
+ 4: "lg:grid-cols-4",
73
+ 5: "lg:grid-cols-5",
74
+ 6: "lg:grid-cols-6"
75
+ }
76
+ },
77
+ container: {
78
+ sm: {
79
+ 1: "@md:grid-cols-1",
80
+ 2: "@md:grid-cols-2",
81
+ 3: "@md:grid-cols-3",
82
+ 4: "@md:grid-cols-4",
83
+ 5: "@md:grid-cols-5",
84
+ 6: "@md:grid-cols-6"
85
+ },
86
+ md: {
87
+ 1: "@2xl:grid-cols-1",
88
+ 2: "@2xl:grid-cols-2",
89
+ 3: "@2xl:grid-cols-3",
90
+ 4: "@2xl:grid-cols-4",
91
+ 5: "@2xl:grid-cols-5",
92
+ 6: "@2xl:grid-cols-6"
93
+ },
94
+ lg: {
95
+ 1: "@4xl:grid-cols-1",
96
+ 2: "@4xl:grid-cols-2",
97
+ 3: "@4xl:grid-cols-3",
98
+ 4: "@4xl:grid-cols-4",
99
+ 5: "@4xl:grid-cols-5",
100
+ 6: "@4xl:grid-cols-6"
101
+ }
102
+ }
103
+ };
104
+ /** Grid of equal columns with named gaps and per-breakpoint column counts. */
105
+ function Grid({ cols: c = 2, gap = "md", query = "viewport", className, ...props }) {
106
+ const r = typeof c === "number" ? { base: c } : c;
107
+ const q = cols[query];
108
+ return /* @__PURE__ */ jsx("div", {
109
+ "data-slot": "grid",
110
+ className: cn("grid min-w-0", gapClass[gap], cols.base[r.base ?? 1], r.sm && q.sm[r.sm], r.md && q.md[r.md], r.lg && q.lg[r.lg], className),
111
+ ...props
112
+ });
113
+ }
114
+ export { Grid, Row, Stack };
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ export type TextTone = "default" | "muted" | "success" | "warning" | "danger" | "info";
3
+ declare const sizeClass: {
4
+ readonly xs: "text-xs";
5
+ readonly sm: "text-[13px]";
6
+ readonly base: "text-sm";
7
+ readonly lg: "text-base";
8
+ readonly xl: "text-xl";
9
+ readonly "2xl": "text-2xl";
10
+ };
11
+ declare const weightClass: {
12
+ readonly normal: "font-normal";
13
+ readonly medium: "font-medium";
14
+ readonly semibold: "font-semibold";
15
+ };
16
+ export interface TextProps extends Omit<React.ComponentProps<"span">, "ref"> {
17
+ size?: keyof typeof sizeClass;
18
+ tone?: TextTone;
19
+ weight?: keyof typeof weightClass;
20
+ mono?: boolean;
21
+ /** One line with an ellipsis. */
22
+ truncate?: boolean;
23
+ /** Clamp to this many lines. */
24
+ lines?: 1 | 2 | 3;
25
+ /** Fixed-width digits for numbers that update. */
26
+ tabular?: boolean;
27
+ as?: "span" | "p" | "div";
28
+ }
29
+ /** Text with props instead of classes: size, tone, weight, mono, truncate, lines. */
30
+ declare function Text({ size, tone, weight, mono, truncate, lines, tabular, as: As, className, ...props }: TextProps): React.JSX.Element;
31
+ export interface HeadingProps extends React.ComponentProps<"h3"> {
32
+ /** 3: a section title in a card. 4: a small uppercase label over a group. */
33
+ level?: 3 | 4;
34
+ }
35
+ /** Section heading inside a card or panel. */
36
+ declare function Heading({ level, className, ...props }: HeadingProps): React.JSX.Element;
37
+ export { Heading, Text };