harebear-ui 0.7.0 → 0.8.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 (35) hide show
  1. package/AGENTS.md +15 -12
  2. package/README.md +8 -7
  3. package/dist/components/admin/app-nav.d.ts +57 -0
  4. package/dist/components/admin/app-nav.js +172 -0
  5. package/dist/components/admin/focus-layout.d.ts +14 -0
  6. package/dist/components/admin/focus-layout.js +36 -0
  7. package/dist/components/admin/settings.d.ts +31 -0
  8. package/dist/components/admin/settings.js +78 -0
  9. package/dist/components/ai/action-receipt.d.ts +16 -0
  10. package/dist/components/ai/action-receipt.js +47 -0
  11. package/dist/components/ai/ask-box.d.ts +27 -0
  12. package/dist/components/ai/ask-box.js +85 -0
  13. package/dist/components/ai/assistant.d.ts +94 -0
  14. package/dist/components/ai/assistant.js +229 -0
  15. package/dist/components/ai/dictation.d.ts +57 -0
  16. package/dist/components/ai/dictation.js +148 -0
  17. package/dist/components/data/sectioned-list.d.ts +37 -0
  18. package/dist/components/data/sectioned-list.js +139 -0
  19. package/dist/components/data/task-row.d.ts +22 -0
  20. package/dist/components/data/task-row.js +60 -0
  21. package/dist/components/layout/collapse.d.ts +14 -0
  22. package/dist/components/layout/collapse.js +24 -0
  23. package/dist/components/ui/check-circle.d.ts +16 -0
  24. package/dist/components/ui/check-circle.js +39 -0
  25. package/dist/components/ui/sonner.d.ts +15 -2
  26. package/dist/components/ui/sonner.js +20 -3
  27. package/dist/index.d.ts +13 -2
  28. package/dist/index.js +13 -2
  29. package/dist/styles.css +75 -0
  30. package/docs/COMPONENTS.md +136 -21
  31. package/docs/ROADMAP.md +95 -0
  32. package/docs/guides/theming.md +10 -9
  33. package/llms.txt +7 -4
  34. package/package.json +1 -1
  35. package/skills/harebear-ui/SKILL.md +1 -0
package/AGENTS.md CHANGED
@@ -25,13 +25,15 @@ src/
25
25
  index.ts Generated barrel. Do not edit by hand.
26
26
  components/ui/ shadcn primitives (radix-nova style), restyled. Keep upstream structure.
27
27
  components/theme/ ThemeProvider, ThemeToggle, ThemeScript.
28
- components/admin/ Layout and dashboard composites (AppShell, StatCard, PageHeader...).
29
- components/data/ DataTable, TrendChart, Sparkline, BarList, StatusBars.
30
- components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput, ModelPicker, CodeSnippet...).
28
+ components/admin/ Layout and dashboard composites (AppShell, AppNav, FocusLayout, SettingsSection, StatCard...).
29
+ components/data/ DataTable, TrendChart, Sparkline, BarList, StatusBars, TaskRow, SectionedList.
30
+ components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput...) and the assistant layer (AssistantProvider, AssistantDock, AskBox, DictationButton, ActionReceipt).
31
31
  components/auth/ AuthLayout, LoginForm, SignupForm (parameterized shadcn login/signup blocks).
32
+ components/layout/ Prop-driven layout and text (Stack, Text, Metric, ShowMore, Collapse).
32
33
  hooks/, lib/utils.ts Shared hooks and cn().
33
- playground/screens/ Reference screens that use only public exports (#admin, #chat, #records, #home, #auth, #catalog). Visual spec and usage examples.
34
- docs/COMPONENTS.md Consumer-facing catalog. Shipped. Test-enforced to mention every export.
34
+ playground/screens/ Reference screens that use only public exports (#admin, #chat, #agent, #records, #home, #household, #settings, #auth, #catalog). Visual spec and usage examples.
35
+ docs/COMPONENTS.md Consumer-facing catalog. Shipped. Test-enforced to mention every export. "Agent-first apps" is the recipe for household/assistant apps.
36
+ docs/ROADMAP.md Proposed and built components for household and agent-first apps. Mark items built when they land.
35
37
  docs/guides/ Getting started (per framework), theming, working with agents. Shipped.
36
38
  llms.txt, skills/ Consumer-facing agent docs. Shipped.
37
39
  bin/harebear-ui.mjs `npx harebear-ui init|docs [name...]` for consuming apps. Zero dependencies.
@@ -42,17 +44,18 @@ test/ Vitest: API snapshot, docs coverage, token integrity, be
42
44
 
43
45
  ## Adding a component
44
46
 
45
- 1. Pick the folder by purpose (`admin`, `data`, `ai`, `auth`; `ui` only for shadcn primitives).
47
+ 1. Pick the folder by purpose (`admin`, `layout`, `data`, `ai`, `auth`; `ui` only for shadcn primitives and small harebear primitives such as `CheckCircle`).
46
48
  2. Follow the existing files: `"use client"` first line, named function component, props interface exported as `<Name>Props`, `data-slot="<kebab-name>"` on the root, `className` merged last with `cn()`, rest props spread on the root.
47
- 3. Style only with tokens (`bg-card`, `text-muted-foreground`, `bg-primary-soft`, `shadow-card`, `rounded-xl`). No hex, no palette colors. Status takes a `tone` (`success | warning | danger | info | neutral`).
48
- 4. `pnpm gen`, then add the component to `docs/COMPONENTS.md` (the docs test fails otherwise) and to the index in `llms.txt`. Use a heading like ``### `Name` `` or a table row with the name in backticks, so `npx harebear-ui docs Name` finds it.
49
- 5. Use it in a playground screen if it is visual, and look at it in light and dark mode and at 375px width.
50
- 6. Add a behavior test in `test/components.test.tsx` if it has logic. `pnpm test -u` to accept the new export in the snapshot.
51
- 7. `pnpm check`.
49
+ 3. Style only with tokens (`bg-card`, `text-muted-foreground`, `bg-primary-soft`, `shadow-card`, `rounded-xl`). No hex, no palette colors. Status takes a `tone` (`success | warning | danger | info | neutral`); categories (members, lists) use `category-1..6`. A box around a bare field uses `focus-box`. Motion is CSS only and off under `prefers-reduced-motion` (`motion-reduce:`; `Collapse` for height changes).
50
+ 4. Floors: checks 24px, phone touch targets 44px, no text under 12px, icon-only buttons have `aria-label`.
51
+ 5. `pnpm gen`, then add the component to `docs/COMPONENTS.md` (the docs test fails otherwise) and to the index in `llms.txt`. Use a heading like ``### `Name` `` or a table row with the name in backticks, so `npx harebear-ui docs Name` finds it.
52
+ 6. Use it in a playground screen if it is visual, and look at it in light and dark mode and at 375px width.
53
+ 7. Add a behavior test in `test/` if it has logic (`components.test.tsx`, `building-blocks.test.tsx`, `household.test.tsx` for the household and assistant layer). `pnpm test -u` to accept the new export in the snapshot.
54
+ 8. `pnpm check`.
52
55
 
53
56
  ## Looking at your work
54
57
 
55
- - `pnpm dev`, then open http://localhost:5199/#admin (or `#chat`, `#agent`, `#records`, `#home`, `#auth`, `#catalog`).
58
+ - `pnpm dev`, then open http://localhost:5199/#admin (or `#chat`, `#agent`, `#records`, `#home`, `#household`, `#settings`, `#auth`, `#catalog`).
56
59
  - `?mode=dark` or `?mode=light` pins the theme without touching localStorage; `?shot` hides the screen switcher. Example: http://localhost:5199/?mode=dark&shot#agent
57
60
  - A headless capture: `pnpm shots admin` writes `.github/assets/screens/admin-*.webp`.
58
61
 
package/README.md CHANGED
@@ -139,13 +139,14 @@ Prompts that work, and what to check before you accept an agent's UI: [Working w
139
139
 
140
140
  ## What's inside
141
141
 
142
- | Group | Exports |
143
- | ------------- | --------------------------------------------------------------------------------------------------------------------------- |
144
- | Layout, admin | `SidebarLayout`, `AppShell`, `PageHeader`, `StatCard`, `StatusBadge`, `UsageMeter`, `Timeline`, `UserMenu`, `ConfirmDialog` |
145
- | Data | `DataTable`, `TrendChart`, `BarList`, `Sparkline`, `StatusBars` |
146
- | AI and chat | `ChatShell`, `ChatPanel`, `ChatMessage`, `PromptInput`, `ModelPicker`, `ToolGroup`, `ApprovalCard`, `CodeSnippet`, `Prose` |
147
- | Auth | `AuthLayout`, `LoginForm`, `SignupForm` |
148
- | Primitives | The full shadcn/ui registry: `Button`, `Dialog`, `Select`, `Sidebar`, `Tabs`, `Command`, `Chart`, and the rest |
142
+ | Group | Exports |
143
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
144
+ | Layout, admin | `SidebarLayout`, `AppShell`, `AppNav`, `FocusLayout`, `SettingsSection`, `PageHeader`, `StatCard`, `StatusBadge`, `UserMenu`, `ConfirmDialog` |
145
+ | Data | `SectionedList`, `TaskRow`, `DataTable`, `TrendChart`, `BarList`, `Sparkline`, `StatusBars` |
146
+ | Assistant | `AssistantProvider`, `AssistantDock`, `AskBox`, `DictationButton`, `ActionReceipt`, `toastUndo` |
147
+ | AI and chat | `ChatShell`, `ChatPanel`, `ChatMessage`, `PromptInput`, `ModelPicker`, `ToolGroup`, `ApprovalCard`, `CodeSnippet`, `Prose` |
148
+ | Auth | `AuthLayout`, `LoginForm`, `SignupForm` |
149
+ | Primitives | The full shadcn/ui registry: `Button`, `Dialog`, `Select`, `Sidebar`, `Tabs`, `Command`, `Chart`, and the rest |
149
150
 
150
151
  The full list with props: [docs/COMPONENTS.md](docs/COMPONENTS.md).
151
152
 
@@ -0,0 +1,57 @@
1
+ import { AppNavItem } from "./app-shell.js";
2
+ import { SidebarLayoutProps } from "./sidebar-layout.js";
3
+ import * as React from "react";
4
+ export interface AppNavPlace extends Omit<AppNavItem, "badge"> {
5
+ /** Count at the right in the sidebar and on the icon in the tab bar, e.g. open to-dos. Hidden when 0. */
6
+ count?: number;
7
+ /** Show in the phone tab bar. Default true; the first 5 that are not false are shown. */
8
+ tab?: boolean;
9
+ }
10
+ export interface AppNavGroup {
11
+ /** Group heading, e.g. "Lists". */
12
+ label: string;
13
+ items: AppNavPlace[];
14
+ /** Expanded at first. Default true. */
15
+ defaultOpen?: boolean;
16
+ /** Shows an add button next to the heading. */
17
+ onAdd?: () => void;
18
+ /** Accessible label of the add button, e.g. "New list". Default "Add". */
19
+ addLabel?: string;
20
+ }
21
+ export interface AppNavAction {
22
+ label: string;
23
+ icon: React.ReactNode;
24
+ onSelect: () => void;
25
+ }
26
+ export interface AppNavProps {
27
+ /** Product mark at the top of the sidebar, usually <AppBrand />. */
28
+ brand?: React.ReactNode;
29
+ /** Top-level places (Today, Calendar, Docs). Sidebar items on desktop, tabs on phones. */
30
+ places: AppNavPlace[];
31
+ /** One collapsible group under the places, e.g. Lists with an add button. Desktop sidebar only. */
32
+ group?: AppNavGroup;
33
+ /** Sidebar bottom, usually <UserMenu />. Desktop sidebar only. */
34
+ footer?: React.ReactNode;
35
+ /** Primary action: a button under the brand on desktop, a round floating button above the tab bar on phones. */
36
+ action?: AppNavAction;
37
+ /** Accessible name of the navigation. Default "Main". */
38
+ label?: string;
39
+ /** Component used for links. Default "a". Pass Next.js Link or React Router Link. */
40
+ linkComponent?: React.ElementType;
41
+ collapsible?: SidebarLayoutProps["collapsible"];
42
+ storageKey?: string;
43
+ defaultOpen?: boolean;
44
+ /** Sidebar width. Default "15.75rem". */
45
+ width?: string;
46
+ /** Classes for the main column. */
47
+ className?: string;
48
+ children?: React.ReactNode;
49
+ }
50
+ /**
51
+ * App navigation from one config. Desktop (768px and up): SidebarLayout with the brand, action, places, a
52
+ * collapsible group and the footer; the active place is a raised card. Phones: a bottom tab bar with up to 5 places
53
+ * and an optional round action button. The main column gets bottom padding and sets `--hb-bottom-inset` (and
54
+ * `--hb-fab-inset`) so an AssistantDock rendered inside sits above the tab bar and the action button.
55
+ */
56
+ declare function AppNav({ brand, places, group, footer, action, label, linkComponent, collapsible, storageKey, defaultOpen, width, className, children }: AppNavProps): React.JSX.Element;
57
+ export { AppNav };
@@ -0,0 +1,172 @@
1
+ "use client";
2
+ import { SidebarLayout } from "./sidebar-layout.js";
3
+ import { Badge } from "../ui/badge.js";
4
+ import { Button } from "../ui/button.js";
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
6
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar.js";
7
+ import { cn } from "../../lib/utils.js";
8
+ import * as React from "react";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import { ChevronDownIcon, PlusIcon } from "lucide-react";
11
+ // src/components/admin/app-nav.tsx
12
+ const TAB_LIMIT = 5;
13
+ /**
14
+ * App navigation from one config. Desktop (768px and up): SidebarLayout with the brand, action, places, a
15
+ * collapsible group and the footer; the active place is a raised card. Phones: a bottom tab bar with up to 5 places
16
+ * and an optional round action button. The main column gets bottom padding and sets `--hb-bottom-inset` (and
17
+ * `--hb-fab-inset`) so an AssistantDock rendered inside sits above the tab bar and the action button.
18
+ */
19
+ function AppNav({ brand, places, group, footer, action, label = "Main", linkComponent = "a", collapsible, storageKey, defaultOpen, width = "15.75rem", className, children }) {
20
+ const tabs = places.filter((p) => p.tab !== false).slice(0, TAB_LIMIT);
21
+ return /* @__PURE__ */ jsxs(SidebarLayout, {
22
+ brand,
23
+ collapsible,
24
+ storageKey,
25
+ defaultOpen,
26
+ width,
27
+ sidebarFooter: footer,
28
+ sidebarActions: action && /* @__PURE__ */ jsxs(Button, {
29
+ type: "button",
30
+ "aria-label": action.label,
31
+ onClick: action.onSelect,
32
+ className: "w-full justify-start group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0",
33
+ children: [action.icon, /* @__PURE__ */ jsx("span", {
34
+ className: "group-data-[collapsible=icon]:hidden",
35
+ children: action.label
36
+ })]
37
+ }),
38
+ sidebar: /* @__PURE__ */ jsxs("nav", {
39
+ "aria-label": label,
40
+ "data-slot": "app-nav",
41
+ className: "flex flex-col gap-5",
42
+ children: [/* @__PURE__ */ jsx(SidebarMenu, {
43
+ className: "gap-1 group-data-[collapsible=icon]:items-center",
44
+ children: places.map((place) => /* @__PURE__ */ jsx(PlaceItem, {
45
+ place,
46
+ Link: linkComponent
47
+ }, place.label))
48
+ }), group && /* @__PURE__ */ jsx(NavGroup, {
49
+ group,
50
+ Link: linkComponent
51
+ })]
52
+ }),
53
+ className: cn("max-md:pb-[calc(4.5rem+env(safe-area-inset-bottom))] max-md:[--hb-bottom-inset:calc(4.5rem+env(safe-area-inset-bottom))]", action && "max-md:[--hb-fab-inset:4.5rem]", className),
54
+ children: [
55
+ children,
56
+ tabs.length > 0 && /* @__PURE__ */ jsx(TabBar, {
57
+ tabs,
58
+ label,
59
+ Link: linkComponent
60
+ }),
61
+ action && /* @__PURE__ */ jsx(Button, {
62
+ type: "button",
63
+ "data-slot": "app-nav-fab",
64
+ size: "icon-lg",
65
+ shape: "pill",
66
+ "aria-label": action.label,
67
+ onClick: action.onSelect,
68
+ className: "fixed right-4 bottom-[calc(4.5rem+env(safe-area-inset-bottom)+1rem)] z-30 size-14 shadow-card md:hidden [&_svg:not([class*='size-'])]:size-6",
69
+ children: action.icon
70
+ })
71
+ ]
72
+ });
73
+ }
74
+ function PlaceItem({ place, Link }) {
75
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
76
+ place.icon,
77
+ /* @__PURE__ */ jsx("span", {
78
+ className: "flex-1",
79
+ children: place.label
80
+ }),
81
+ !!place.count && /* @__PURE__ */ jsx(Badge, {
82
+ variant: "soft",
83
+ className: "h-5 px-2 group-data-[collapsible=icon]:hidden",
84
+ children: place.count
85
+ })
86
+ ] });
87
+ return /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsx(SidebarMenuButton, {
88
+ asChild: !!place.href,
89
+ isActive: place.active,
90
+ "aria-current": place.active ? "page" : void 0,
91
+ onClick: place.onSelect,
92
+ tooltip: place.label,
93
+ children: place.href ? /* @__PURE__ */ jsx(Link, {
94
+ href: place.href,
95
+ children: content
96
+ }) : content
97
+ }) });
98
+ }
99
+ function NavGroup({ group, Link }) {
100
+ const [open, setOpen] = React.useState(group.defaultOpen ?? true);
101
+ return /* @__PURE__ */ jsxs(Collapsible, {
102
+ open,
103
+ onOpenChange: setOpen,
104
+ "data-slot": "app-nav-group",
105
+ className: "flex flex-col gap-1 group-data-[collapsible=icon]:hidden",
106
+ children: [/* @__PURE__ */ jsxs("div", {
107
+ className: "flex items-center gap-1 pr-1",
108
+ children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
109
+ className: "flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md px-3 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40",
110
+ children: [/* @__PURE__ */ jsx("span", {
111
+ className: "truncate",
112
+ children: group.label
113
+ }), /* @__PURE__ */ jsx(ChevronDownIcon, {
114
+ "aria-hidden": "true",
115
+ className: cn("size-3.5 transition-transform motion-reduce:transition-none", !open && "-rotate-90")
116
+ })]
117
+ }), group.onAdd && /* @__PURE__ */ jsx(Button, {
118
+ type: "button",
119
+ variant: "ghost",
120
+ size: "icon-sm",
121
+ "aria-label": group.addLabel ?? "Add",
122
+ onClick: group.onAdd,
123
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
124
+ })]
125
+ }), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx(SidebarMenu, {
126
+ className: "gap-1",
127
+ children: group.items.map((place) => /* @__PURE__ */ jsx(PlaceItem, {
128
+ place,
129
+ Link
130
+ }, place.label))
131
+ }) })]
132
+ });
133
+ }
134
+ function TabBar({ tabs, label, Link }) {
135
+ return /* @__PURE__ */ jsx("nav", {
136
+ "aria-label": label,
137
+ "data-slot": "app-nav-tabs",
138
+ className: "fixed inset-x-0 bottom-0 z-30 border-t border-border bg-background/95 px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))] backdrop-blur md:hidden",
139
+ children: /* @__PURE__ */ jsx("ul", {
140
+ className: "mx-auto flex max-w-lg",
141
+ children: tabs.map((tab) => {
142
+ const Comp = tab.href ? Link : "button";
143
+ return /* @__PURE__ */ jsx("li", {
144
+ className: "flex min-w-0 flex-1",
145
+ children: /* @__PURE__ */ jsxs(Comp, {
146
+ ...tab.href ? { href: tab.href } : {
147
+ type: "button",
148
+ onClick: tab.onSelect
149
+ },
150
+ "aria-current": tab.active ? "page" : void 0,
151
+ className: "flex min-h-14 min-w-0 flex-1 flex-col items-center justify-center gap-1 rounded-xl px-1 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20 aria-[current=page]:text-primary",
152
+ children: [/* @__PURE__ */ jsxs("span", {
153
+ className: "relative [&_svg]:size-6",
154
+ children: [tab.icon, !!tab.count && /* @__PURE__ */ jsx("span", {
155
+ "aria-hidden": "true",
156
+ className: "absolute -top-1.5 left-[calc(100%-0.5rem)] grid h-4.5 min-w-4.5 place-items-center rounded-full bg-primary px-1 text-xs leading-none font-semibold text-primary-foreground",
157
+ children: tab.count > 99 ? "99+" : tab.count
158
+ })]
159
+ }), /* @__PURE__ */ jsxs("span", {
160
+ className: "max-w-full truncate",
161
+ children: [tab.label, !!tab.count && /* @__PURE__ */ jsxs("span", {
162
+ className: "sr-only",
163
+ children: [", ", tab.count]
164
+ })]
165
+ })]
166
+ })
167
+ }, tab.label);
168
+ })
169
+ })
170
+ });
171
+ }
172
+ export { AppNav };
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+ export interface FocusLayoutProps extends Omit<React.ComponentProps<"div">, "title"> {
3
+ /** Page title in the top bar. */
4
+ title?: React.ReactNode;
5
+ /** Leading control, usually a close or back link: <Button asChild variant="ghost" size="icon"><a aria-label="Close">…</a></Button>. */
6
+ back?: React.ReactNode;
7
+ /** Trailing top bar controls, e.g. a Save button. */
8
+ actions?: React.ReactNode;
9
+ /** Column width. Default "md" (42rem). */
10
+ width?: "sm" | "md" | "lg";
11
+ }
12
+ /** A page outside the app frame (settings, onboarding, setup): slim top bar with back link and title, one centred column. */
13
+ declare function FocusLayout({ title, back, actions, width, className, children, ...props }: FocusLayoutProps): React.JSX.Element;
14
+ export { FocusLayout };
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ // src/components/admin/focus-layout.tsx
5
+ const widthClass = {
6
+ sm: "max-w-xl",
7
+ md: "max-w-2xl",
8
+ lg: "max-w-4xl"
9
+ };
10
+ /** A page outside the app frame (settings, onboarding, setup): slim top bar with back link and title, one centred column. */
11
+ function FocusLayout({ title, back, actions, width = "md", className, children, ...props }) {
12
+ return /* @__PURE__ */ jsxs("div", {
13
+ "data-slot": "focus-layout",
14
+ className: cn("flex min-h-dvh flex-col bg-background", className),
15
+ ...props,
16
+ children: [/* @__PURE__ */ jsxs("header", {
17
+ className: "sticky top-0 z-20 flex min-h-14 items-center gap-2 bg-background/90 px-3 pt-[env(safe-area-inset-top)] backdrop-blur sm:px-4",
18
+ children: [
19
+ back,
20
+ title && /* @__PURE__ */ jsx("h1", {
21
+ className: "min-w-0 flex-1 truncate text-[15px] font-semibold",
22
+ children: title
23
+ }),
24
+ !title && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
25
+ actions && /* @__PURE__ */ jsx("div", {
26
+ className: "flex items-center gap-2",
27
+ children: actions
28
+ })
29
+ ]
30
+ }), /* @__PURE__ */ jsx("main", {
31
+ className: cn("mx-auto flex w-full flex-col gap-6 px-4 pt-4 pb-[max(3rem,env(safe-area-inset-bottom))]", widthClass[width]),
32
+ children
33
+ })]
34
+ });
35
+ }
36
+ export { FocusLayout };
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ export interface SettingsSectionProps extends Omit<React.ComponentProps<"section">, "title"> {
3
+ title: React.ReactNode;
4
+ description?: React.ReactNode;
5
+ /** SettingsRow children. */
6
+ children?: React.ReactNode;
7
+ }
8
+ /** A card of settings: heading, optional description, then SettingsRows separated by hairlines. */
9
+ declare function SettingsSection({ title, description, className, children, ...props }: SettingsSectionProps): React.JSX.Element;
10
+ export interface SettingsRowProps extends Omit<React.ComponentProps<"div">, "content"> {
11
+ label: React.ReactNode;
12
+ /** One line under the label. */
13
+ description?: React.ReactNode;
14
+ /** id of the control, so the label is a <label> for it (Switch, Select trigger, Input). */
15
+ htmlFor?: string;
16
+ /** The control at the right: Switch, Select, ToggleGroup, Button. */
17
+ control?: React.ReactNode;
18
+ /**
19
+ * Inline content that expands under the row (change password, add a connection). Without a `control`, the row
20
+ * gets a toggle button (`expandLabel` / "Cancel").
21
+ */
22
+ content?: React.ReactNode;
23
+ open?: boolean;
24
+ defaultOpen?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
26
+ /** Label of the default toggle button. Default "Change". */
27
+ expandLabel?: React.ReactNode;
28
+ }
29
+ /** One setting: label and description at the left, a control at the right, optional expandable content. */
30
+ declare function SettingsRow({ label, description, htmlFor, control, content, open: openProp, defaultOpen, onOpenChange, expandLabel, className, ...props }: SettingsRowProps): React.JSX.Element;
31
+ export { SettingsRow, SettingsSection };
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { Collapse } from "../layout/collapse.js";
3
+ import { Button } from "../ui/button.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ import * as React from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ // src/components/admin/settings.tsx
8
+ /** A card of settings: heading, optional description, then SettingsRows separated by hairlines. */
9
+ function SettingsSection({ title, description, className, children, ...props }) {
10
+ const id = React.useId();
11
+ return /* @__PURE__ */ jsxs("section", {
12
+ "data-slot": "settings-section",
13
+ "aria-labelledby": id,
14
+ className: cn("flex min-w-0 flex-col rounded-xl bg-card shadow-card", className),
15
+ ...props,
16
+ children: [/* @__PURE__ */ jsxs("div", {
17
+ className: "flex flex-col gap-1 px-5 pt-5 pb-2",
18
+ children: [/* @__PURE__ */ jsx("h2", {
19
+ id,
20
+ className: "text-base font-semibold",
21
+ children: title
22
+ }), description && /* @__PURE__ */ jsx("p", {
23
+ className: "text-[13px] text-muted-foreground",
24
+ children: description
25
+ })]
26
+ }), /* @__PURE__ */ jsx("div", {
27
+ className: "flex flex-col divide-y divide-border px-5 pb-1",
28
+ children
29
+ })]
30
+ });
31
+ }
32
+ /** One setting: label and description at the left, a control at the right, optional expandable content. */
33
+ function SettingsRow({ label, description, htmlFor, control, content, open: openProp, defaultOpen = false, onOpenChange, expandLabel = "Change", className, ...props }) {
34
+ const [openState, setOpenState] = React.useState(defaultOpen);
35
+ const open = openProp ?? openState;
36
+ const setOpen = (next) => {
37
+ if (openProp === void 0) setOpenState(next);
38
+ onOpenChange?.(next);
39
+ };
40
+ const contentId = React.useId();
41
+ const Label = htmlFor ? "label" : "div";
42
+ return /* @__PURE__ */ jsxs("div", {
43
+ "data-slot": "settings-row",
44
+ "data-state": content ? open ? "open" : "closed" : void 0,
45
+ className: cn("py-3.5", className),
46
+ ...props,
47
+ children: [/* @__PURE__ */ jsxs("div", {
48
+ className: "flex flex-wrap items-center gap-x-4 gap-y-2",
49
+ children: [/* @__PURE__ */ jsxs("div", {
50
+ className: "flex min-w-0 flex-1 basis-48 flex-col",
51
+ children: [/* @__PURE__ */ jsx(Label, {
52
+ htmlFor,
53
+ className: "text-sm font-medium",
54
+ children: label
55
+ }), description && /* @__PURE__ */ jsx("span", {
56
+ className: "text-[13px] text-muted-foreground",
57
+ children: description
58
+ })]
59
+ }), control ?? (content ? /* @__PURE__ */ jsx(Button, {
60
+ type: "button",
61
+ variant: "outline",
62
+ size: "sm",
63
+ "aria-expanded": open,
64
+ "aria-controls": contentId,
65
+ onClick: () => setOpen(!open),
66
+ children: open ? "Cancel" : expandLabel
67
+ }) : null)]
68
+ }), content && /* @__PURE__ */ jsx(Collapse, {
69
+ open,
70
+ id: contentId,
71
+ children: /* @__PURE__ */ jsx("div", {
72
+ className: "pt-3.5",
73
+ children: content
74
+ })
75
+ })]
76
+ });
77
+ }
78
+ export { SettingsRow, SettingsSection };
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ export interface ActionReceiptProps extends Omit<React.ComponentProps<"div">, "title"> {
3
+ /** What was done, past tense: "Added milk to Groceries". */
4
+ title: React.ReactNode;
5
+ /** One line, e.g. "Groceries · just now". */
6
+ meta?: React.ReactNode;
7
+ /** Shows an Undo button. */
8
+ onUndo?: () => void;
9
+ /** Default "Undo". */
10
+ undoLabel?: React.ReactNode;
11
+ /** After undo: the check goes away, the title is struck through and the button is replaced by "Undone". */
12
+ undone?: boolean;
13
+ }
14
+ /** A compact card for one action the assistant took on its own: round check, title, one meta line, Undo. */
15
+ declare function ActionReceipt({ title, meta, onUndo, undoLabel, undone, className, ...props }: ActionReceiptProps): React.JSX.Element;
16
+ export { ActionReceipt };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { cn } from "../../lib/utils.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { CheckIcon } from "lucide-react";
6
+ // src/components/ai/action-receipt.tsx
7
+ /** A compact card for one action the assistant took on its own: round check, title, one meta line, Undo. */
8
+ function ActionReceipt({ title, meta, onUndo, undoLabel = "Undo", undone, className, ...props }) {
9
+ return /* @__PURE__ */ jsxs("div", {
10
+ "data-slot": "action-receipt",
11
+ "data-undone": undone || void 0,
12
+ className: cn("flex min-w-0 items-center gap-3 rounded-xl border border-border bg-card px-3.5 py-2.5", className),
13
+ ...props,
14
+ children: [
15
+ /* @__PURE__ */ jsx("span", {
16
+ "aria-hidden": "true",
17
+ className: cn("grid size-6 shrink-0 place-items-center rounded-full", undone ? "border-2 border-muted-foreground/40" : "bg-success text-card"),
18
+ children: !undone && /* @__PURE__ */ jsx(CheckIcon, {
19
+ className: "size-3.5",
20
+ strokeWidth: 3
21
+ })
22
+ }),
23
+ /* @__PURE__ */ jsxs("div", {
24
+ className: "flex min-w-0 flex-1 flex-col leading-snug",
25
+ children: [/* @__PURE__ */ jsx("span", {
26
+ className: cn("text-sm font-medium wrap-anywhere", undone && "text-muted-foreground line-through"),
27
+ children: title
28
+ }), meta && /* @__PURE__ */ jsx("span", {
29
+ className: "truncate text-xs text-muted-foreground",
30
+ children: meta
31
+ })]
32
+ }),
33
+ undone ? /* @__PURE__ */ jsx("span", {
34
+ className: "shrink-0 text-xs font-medium text-muted-foreground",
35
+ children: "Undone"
36
+ }) : onUndo && /* @__PURE__ */ jsx(Button, {
37
+ type: "button",
38
+ variant: "ghost",
39
+ size: "sm",
40
+ className: "shrink-0",
41
+ onClick: onUndo,
42
+ children: undoLabel
43
+ })
44
+ ]
45
+ });
46
+ }
47
+ export { ActionReceipt };
@@ -0,0 +1,27 @@
1
+ import { SpeechSource } from "./dictation.js";
2
+ import * as React from "react";
3
+ export interface AskBoxProps extends Omit<React.ComponentProps<"form">, "onSubmit" | "onChange"> {
4
+ value: string;
5
+ onValueChange: (value: string) => void;
6
+ /** Called with the trimmed text on Enter or the send button. Shift+Enter inserts a newline. */
7
+ onSubmit: (value: string) => void;
8
+ /** One per app, e.g. "Ask HomeBot anything". Also the field's accessible label. Default "Ask anything". */
9
+ placeholder?: string;
10
+ /** Shown under the text while there is text, e.g. chips for the date or list it picked up. */
11
+ chips?: React.ReactNode;
12
+ /** Pills under the box. Clicking one submits it (or calls onSuggestion). */
13
+ suggestions?: string[];
14
+ onSuggestion?: (suggestion: string) => void;
15
+ /** Speech source for the mic. Default the browser's; `false` hides the mic. */
16
+ dictation?: SpeechSource | false;
17
+ /** BCP 47 language for dictation. */
18
+ lang?: string;
19
+ disabled?: boolean;
20
+ autoFocus?: boolean;
21
+ }
22
+ /**
23
+ * The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
24
+ * DictationButton (mic, stop, send). The focus ring is on the whole box.
25
+ */
26
+ declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, className, ...props }: AskBoxProps): React.JSX.Element;
27
+ export { AskBox };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { DictationButton } from "./dictation.js";
3
+ import { Button } from "../ui/button.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ import * as React from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ // src/components/ai/ask-box.tsx
8
+ const noSpeech = {
9
+ isSupported: () => false,
10
+ start: () => () => {}
11
+ };
12
+ /**
13
+ * The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
14
+ * DictationButton (mic, stop, send). The focus ring is on the whole box.
15
+ */
16
+ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, className, ...props }) {
17
+ const id = React.useId();
18
+ const submit = () => {
19
+ const text = value.trim();
20
+ if (text && !disabled) onSubmit(text);
21
+ };
22
+ const hasText = value.trim().length > 0;
23
+ return /* @__PURE__ */ jsxs("form", {
24
+ "data-slot": "ask-box",
25
+ className: cn("flex w-full min-w-0 flex-col gap-3", className),
26
+ onSubmit: (e) => {
27
+ e.preventDefault();
28
+ submit();
29
+ },
30
+ ...props,
31
+ children: [/* @__PURE__ */ jsxs("div", {
32
+ className: "focus-box flex flex-col gap-2 rounded-3xl border border-transparent bg-muted py-2.5 pr-2.5 pl-5",
33
+ children: [/* @__PURE__ */ jsxs("div", {
34
+ className: "flex items-end gap-2",
35
+ children: [
36
+ /* @__PURE__ */ jsx("label", {
37
+ htmlFor: id,
38
+ className: "sr-only",
39
+ children: placeholder
40
+ }),
41
+ /* @__PURE__ */ jsx("textarea", {
42
+ id,
43
+ rows: 1,
44
+ value,
45
+ disabled,
46
+ autoFocus,
47
+ placeholder,
48
+ onChange: (e) => onValueChange(e.target.value),
49
+ onKeyDown: (e) => {
50
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
51
+ e.preventDefault();
52
+ submit();
53
+ }
54
+ },
55
+ className: "field-sizing-content max-h-48 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent py-2 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground"
56
+ }),
57
+ /* @__PURE__ */ jsx(DictationButton, {
58
+ value,
59
+ disabled,
60
+ source: dictation === false ? noSpeech : dictation,
61
+ lang,
62
+ onTranscript: (text) => onValueChange(value ? `${value.replace(/\s+$/, "")} ${text}` : text)
63
+ })
64
+ ]
65
+ }), hasText && chips && /* @__PURE__ */ jsx("div", {
66
+ "data-slot": "ask-box-chips",
67
+ className: "flex flex-wrap gap-1.5 pb-1",
68
+ children: chips
69
+ })]
70
+ }), suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
71
+ "data-slot": "ask-box-suggestions",
72
+ className: "flex flex-wrap gap-2",
73
+ children: suggestions.map((s) => /* @__PURE__ */ jsx(Button, {
74
+ type: "button",
75
+ variant: "outline",
76
+ size: "sm",
77
+ shape: "pill",
78
+ disabled,
79
+ onClick: () => onSuggestion ? onSuggestion(s) : onSubmit(s),
80
+ children: s
81
+ }, s))
82
+ })]
83
+ });
84
+ }
85
+ export { AskBox };