harebear-ui 0.8.0 → 0.9.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 (52) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/admin/app-nav.d.ts +4 -0
  3. package/dist/components/admin/app-nav.js +5 -2
  4. package/dist/components/admin/avatar-upload.d.ts +32 -0
  5. package/dist/components/admin/avatar-upload.js +121 -0
  6. package/dist/components/admin/category.d.ts +17 -0
  7. package/dist/components/admin/category.js +35 -0
  8. package/dist/components/admin/celebration.d.ts +7 -0
  9. package/dist/components/admin/celebration.js +61 -0
  10. package/dist/components/admin/color-swatch-picker.d.ts +15 -0
  11. package/dist/components/admin/color-swatch-picker.js +68 -0
  12. package/dist/components/admin/date-time-picker.d.ts +20 -0
  13. package/dist/components/admin/date-time-picker.js +97 -0
  14. package/dist/components/admin/detail-sheet.d.ts +18 -0
  15. package/dist/components/admin/detail-sheet.js +80 -0
  16. package/dist/components/admin/markdown-editor.d.ts +15 -0
  17. package/dist/components/admin/markdown-editor.js +42 -0
  18. package/dist/components/admin/user-chip.d.ts +34 -3
  19. package/dist/components/admin/user-chip.js +66 -7
  20. package/dist/components/admin/user-menu.d.ts +4 -1
  21. package/dist/components/admin/user-menu.js +2 -1
  22. package/dist/components/ai/ask-box.d.ts +14 -2
  23. package/dist/components/ai/ask-box.js +90 -54
  24. package/dist/components/ai/assistant.d.ts +13 -3
  25. package/dist/components/ai/assistant.js +37 -7
  26. package/dist/components/ai/code-block.d.ts +10 -3
  27. package/dist/components/ai/code-block.js +54 -5
  28. package/dist/components/ai/conversation-list.d.ts +13 -3
  29. package/dist/components/ai/conversation-list.js +15 -26
  30. package/dist/components/auth/auth.js +4 -6
  31. package/dist/components/data/agenda-row.d.ts +26 -0
  32. package/dist/components/data/agenda-row.js +62 -0
  33. package/dist/components/data/day-strip.d.ts +22 -0
  34. package/dist/components/data/day-strip.js +68 -0
  35. package/dist/components/data/sectioned-list.d.ts +21 -6
  36. package/dist/components/data/sectioned-list.js +44 -21
  37. package/dist/components/data/task-row.d.ts +3 -0
  38. package/dist/components/data/task-row.js +6 -14
  39. package/dist/components/ui/avatar.js +1 -1
  40. package/dist/components/ui/kbd.js +1 -1
  41. package/dist/components/ui/password-input.d.ts +8 -0
  42. package/dist/components/ui/password-input.js +32 -0
  43. package/dist/index.d.ts +15 -5
  44. package/dist/index.js +13 -3
  45. package/dist/lib/date.js +8 -0
  46. package/dist/lib/toast-clearance.js +22 -0
  47. package/dist/styles.css +48 -0
  48. package/docs/COMPONENTS.md +165 -15
  49. package/docs/ROADMAP.md +12 -11
  50. package/llms.txt +8 -7
  51. package/package.json +1 -1
  52. package/skills/harebear-ui/SKILL.md +2 -1
@@ -1,11 +1,13 @@
1
1
  "use client";
2
+ import { Callout } from "../admin/callout.js";
2
3
  import { Button } from "../ui/button.js";
3
4
  import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle } from "../ui/code-block.js";
4
5
  import { useCopyToClipboard } from "../../hooks/use-copy-to-clipboard.js";
5
6
  import { useHighlightedCode } from "../../hooks/use-highlighted-code.js";
6
7
  import { cn } from "../../lib/utils.js";
8
+ import * as React from "react";
7
9
  import { jsx, jsxs } from "react/jsx-runtime";
8
- import { CheckIcon, CopyIcon } from "lucide-react";
10
+ import { CheckIcon, CopyIcon, LightbulbIcon } from "lucide-react";
9
11
  // src/components/ai/code-block.tsx
10
12
  /**
11
13
  * One-prop code panel: language label, copy button, shiki highlighting (loaded
@@ -50,16 +52,63 @@ function InlineCode({ truncate, className, children, title, ...props }) {
50
52
  children: cut ? `${children.slice(0, truncate)}…` : children
51
53
  });
52
54
  }
55
+ const TIP = /^\s*Tip:\s*/;
56
+ /** The children with a leading "Tip:" removed, or undefined when they do not start with it. */
57
+ function withoutTip(children) {
58
+ const items = React.Children.toArray(children);
59
+ const i = items.findIndex((c) => !(typeof c === "string" && c.trim() === ""));
60
+ const first = items[i];
61
+ let rest;
62
+ if (typeof first === "string") {
63
+ if (!TIP.test(first)) return void 0;
64
+ rest = first.replace(TIP, "");
65
+ } else if (React.isValidElement(first) && typeof first.type === "string") {
66
+ const inner = withoutTip(first.props.children);
67
+ if (inner === void 0) return void 0;
68
+ rest = React.cloneElement(first, void 0, inner);
69
+ } else return;
70
+ return [
71
+ ...items.slice(0, i),
72
+ rest,
73
+ ...items.slice(i + 1)
74
+ ];
75
+ }
76
+ /**
77
+ * A blockquote that becomes a `Callout` when its first word is "Tip:" (the word is dropped).
78
+ * Prose applies it to blockquote children; pass it to react-markdown as `components={{ blockquote: ProseBlockquote }}`.
79
+ */
80
+ function ProseBlockquote({ children, node: _node, ...props }) {
81
+ const tip = withoutTip(children);
82
+ if (tip === void 0) return /* @__PURE__ */ jsx("blockquote", {
83
+ ...props,
84
+ children
85
+ });
86
+ return /* @__PURE__ */ jsx(Callout, {
87
+ "data-tip": "",
88
+ icon: /* @__PURE__ */ jsx(LightbulbIcon, {}),
89
+ className: props.className,
90
+ children: tip
91
+ });
92
+ }
93
+ /** Swaps plain blockquote elements for ProseBlockquote, at any depth of plain HTML elements. */
94
+ function withTips(children) {
95
+ return React.Children.map(children, (c) => {
96
+ if (!React.isValidElement(c) || typeof c.type !== "string") return c;
97
+ if (c.type === "blockquote") return /* @__PURE__ */ jsx(ProseBlockquote, { ...c.props });
98
+ return c.props.children === void 0 ? c : React.cloneElement(c, void 0, withTips(c.props.children));
99
+ });
100
+ }
53
101
  /**
54
102
  * Typography for long-form and model output. Styles plain HTML children
55
103
  * (p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table), so it works
56
- * with react-markdown or any HTML renderer.
104
+ * with react-markdown or any HTML renderer. A blockquote starting "Tip:" renders as a Callout.
57
105
  */
58
- function Prose({ className, ...props }) {
106
+ function Prose({ className, children, ...props }) {
59
107
  return /* @__PURE__ */ jsx("div", {
60
108
  "data-slot": "prose",
61
109
  className: cn("flex min-w-0 flex-col gap-3.5 text-[15px] leading-[1.65] wrap-anywhere text-foreground", "[&_ol]:flex [&_ol]:list-decimal [&_ol]:flex-col [&_ol]:gap-2 [&_ol]:pl-5.5", "[&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-2 [&_ul]:pl-5.5", "[&_strong]:font-semibold [&_a]:text-primary [&_a]:underline-offset-4 hover:[&_a]:underline", "[&_h1]:text-xl [&_h1]:font-semibold [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:font-semibold [&_h4]:font-semibold", "[&_blockquote]:border-l-2 [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground", "[&_:not(pre)>code]:rounded-[6px] [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[12.5px]", "[&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-4.5 [&_pre]:font-mono [&_pre]:text-[13px]", "[&_table]:w-full [&_table]:text-sm [&_td]:border-b [&_td]:py-2 [&_th]:border-b [&_th]:py-2 [&_th]:text-left [&_th]:font-medium", className),
62
- ...props
110
+ ...props,
111
+ children: withTips(children)
63
112
  });
64
113
  }
65
- export { CodeSnippet, InlineCode, Prose };
114
+ export { CodeSnippet, InlineCode, Prose, ProseBlockquote };
@@ -39,11 +39,21 @@ export interface ConversationListProps extends Omit<React.ComponentProps<"nav">,
39
39
  * and hides itself when the sidebar is collapsed to its icon rail.
40
40
  */
41
41
  declare function ConversationList({ groups, activeId, onSelect, renderAction, onDelete, deleteDescription, empty, className, ...props }: ConversationListProps): React.JSX.Element;
42
+ export interface GroupByDateOptions {
43
+ /** Reference time. Default now; pass it for deterministic tests. */
44
+ now?: number | Date;
45
+ /**
46
+ * `"day"` (default): Today / Yesterday / Previous 7 days / Earlier. `"week"`: Today / This week (the 6 days before
47
+ * today) / Earlier.
48
+ */
49
+ buckets?: "day" | "week";
50
+ }
42
51
  /**
43
- * Buckets items into Today / Yesterday / Previous 7 days / Earlier by a timestamp (ms or Date),
44
- * newest first, and drops empty groups. Pass `now` for deterministic tests.
52
+ * Buckets items into Today / Yesterday / Previous 7 days / Earlier (or Today / This week / Earlier with
53
+ * `buckets: "week"`) by a timestamp (ms or Date), newest first, and drops empty groups. The last argument is the
54
+ * options, or just `now`.
45
55
  */
46
- declare function groupByDate<T>(items: T[], getDate: (item: T) => number | Date, toConversation: (item: T) => Conversation, now?: number | Date): ConversationGroup[];
56
+ declare function groupByDate<T>(items: T[], getDate: (item: T) => number | Date, toConversation: (item: T) => Conversation, options?: number | Date | GroupByDateOptions): ConversationGroup[];
47
57
  /**
48
58
  * Short timestamp for lists and headers: the time today ("9:41 AM"), the date this year ("Nov 3"), else with the
49
59
  * year ("Nov 3, 2025"). Uses the browser locale unless `locale` is given. Pass `now` for deterministic tests.
@@ -94,39 +94,28 @@ function ConversationList({ groups, activeId, onSelect, renderAction, onDelete,
94
94
  });
95
95
  }
96
96
  /**
97
- * Buckets items into Today / Yesterday / Previous 7 days / Earlier by a timestamp (ms or Date),
98
- * newest first, and drops empty groups. Pass `now` for deterministic tests.
97
+ * Buckets items into Today / Yesterday / Previous 7 days / Earlier (or Today / This week / Earlier with
98
+ * `buckets: "week"`) by a timestamp (ms or Date), newest first, and drops empty groups. The last argument is the
99
+ * options, or just `now`.
99
100
  */
100
- function groupByDate(items, getDate, toConversation, now = Date.now()) {
101
+ function groupByDate(items, getDate, toConversation, options = {}) {
102
+ const { now = Date.now(), buckets = "day" } = typeof options === "number" || options instanceof Date ? { now: options } : options;
101
103
  const d = new Date(now);
102
104
  const midnight = (daysAgo) => new Date(d.getFullYear(), d.getMonth(), d.getDate() - daysAgo).getTime();
103
- const [today, yesterday, weekAgo] = [
104
- midnight(0),
105
- midnight(1),
106
- midnight(7)
107
- ];
108
- const groups = [
109
- {
110
- label: "Today",
111
- items: []
112
- },
113
- {
114
- label: "Yesterday",
115
- items: []
116
- },
117
- {
118
- label: "Previous 7 days",
119
- items: []
120
- },
121
- {
122
- label: "Earlier",
123
- items: []
124
- }
105
+ const bounds = buckets === "week" ? [["Today", midnight(0)], ["This week", midnight(6)]] : [
106
+ ["Today", midnight(0)],
107
+ ["Yesterday", midnight(1)],
108
+ ["Previous 7 days", midnight(7)]
125
109
  ];
110
+ const groups = [...bounds.map(([label]) => label), "Earlier"].map((label) => ({
111
+ label,
112
+ items: []
113
+ }));
126
114
  const sorted = [...items].sort((a, b) => +getDate(b) - +getDate(a));
127
115
  for (const item of sorted) {
128
116
  const t = +getDate(item);
129
- groups[t >= today ? 0 : t >= yesterday ? 1 : t >= weekAgo ? 2 : 3]?.items.push(toConversation(item));
117
+ const i = bounds.findIndex(([, from]) => t >= from);
118
+ groups[i === -1 ? bounds.length : i]?.items.push(toConversation(item));
130
119
  }
131
120
  return groups.filter((g) => g.items.length);
132
121
  }
@@ -3,6 +3,7 @@ import { Button } from "../ui/button.js";
3
3
  import { Card, CardContent } from "../ui/card.js";
4
4
  import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldSeparator } from "../ui/field.js";
5
5
  import { Input } from "../ui/input.js";
6
+ import { PasswordInput } from "../ui/password-input.js";
6
7
  import { Spinner } from "../ui/spinner.js";
7
8
  import { cn } from "../../lib/utils.js";
8
9
  import * as React from "react";
@@ -187,10 +188,9 @@ function LoginForm({ title = "Welcome back", description, onSubmit, mode = "pass
187
188
  className: "ms-auto text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline",
188
189
  children: "Forgot password?"
189
190
  })]
190
- }), /* @__PURE__ */ jsx(Input, {
191
+ }), /* @__PURE__ */ jsx(PasswordInput, {
191
192
  id: `${id}-password`,
192
193
  name: "password",
193
- type: "password",
194
194
  autoComplete: "current-password",
195
195
  required: true,
196
196
  disabled: loading
@@ -280,10 +280,9 @@ function SignupForm({ title = "Create your account", description = "Enter your d
280
280
  htmlFor: `${id}-password`,
281
281
  children: "Password"
282
282
  }),
283
- /* @__PURE__ */ jsx(Input, {
283
+ /* @__PURE__ */ jsx(PasswordInput, {
284
284
  id: `${id}-password`,
285
285
  name: "password",
286
- type: "password",
287
286
  autoComplete: "new-password",
288
287
  onInput: (e) => (e.currentTarget.form?.elements.namedItem("confirm"))?.setCustomValidity(""),
289
288
  minLength: minPasswordLength,
@@ -295,10 +294,9 @@ function SignupForm({ title = "Create your account", description = "Enter your d
295
294
  confirmPassword && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
296
295
  htmlFor: `${id}-confirm`,
297
296
  children: "Confirm password"
298
- }), /* @__PURE__ */ jsx(Input, {
297
+ }), /* @__PURE__ */ jsx(PasswordInput, {
299
298
  id: `${id}-confirm`,
300
299
  name: "confirm",
301
- type: "password",
302
300
  autoComplete: "new-password",
303
301
  required: true,
304
302
  disabled: loading,
@@ -0,0 +1,26 @@
1
+ import { CategoryColor } from "../admin/category.js";
2
+ import * as React from "react";
3
+ export interface AgendaRowWho {
4
+ name: string;
5
+ /** Category color of the member or calendar, 1 to 6. */
6
+ color: CategoryColor;
7
+ }
8
+ export interface AgendaRowProps extends Omit<React.ComponentProps<"div">, "title"> {
9
+ title: React.ReactNode;
10
+ /** Left column, e.g. "3:15" or "All day". */
11
+ time?: React.ReactNode;
12
+ /** One line under the title. Truncates. */
13
+ meta?: React.ReactNode;
14
+ /** Whose it is: a category-colored dot and the name at the right. */
15
+ who?: AgendaRowWho;
16
+ /** Pass with `onCheckedChange` to draw a to-do check instead of the event icon. */
17
+ checked?: boolean;
18
+ onCheckedChange?: (checked: boolean) => void;
19
+ /** Warning-tinted check. Also say it in words in `meta`. */
20
+ important?: boolean;
21
+ /** Makes the title a button, e.g. to open an event dialog or a DetailSheet. */
22
+ onOpen?: () => void;
23
+ }
24
+ /** One line of a day's agenda: time, event icon or to-do check, title, whose dot. */
25
+ declare function AgendaRow({ title, time, meta, who, checked, onCheckedChange, important, onOpen, className, ...props }: AgendaRowProps): React.JSX.Element;
26
+ export { AgendaRow };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { CategoryDot } from "../admin/category.js";
3
+ import { CheckCircle } from "../ui/check-circle.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ import * as React from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { CalendarIcon } from "lucide-react";
8
+ // src/components/data/agenda-row.tsx
9
+ /** One line of a day's agenda: time, event icon or to-do check, title, whose dot. */
10
+ function AgendaRow({ title, time, meta, who, checked, onCheckedChange, important, onOpen, className, ...props }) {
11
+ const titleId = React.useId();
12
+ const titleClass = cn("text-[15px] leading-6 wrap-anywhere", checked ? "text-muted-foreground line-through decoration-muted-foreground/60" : "text-foreground");
13
+ return /* @__PURE__ */ jsxs("div", {
14
+ "data-slot": "agenda-row",
15
+ "data-checked": checked || void 0,
16
+ className: cn("flex min-w-0 items-start gap-3 rounded-lg px-2 py-2.5", className),
17
+ ...props,
18
+ children: [
19
+ /* @__PURE__ */ jsx("span", {
20
+ className: "w-14 shrink-0 text-[13px] leading-6 text-muted-foreground tabular-nums",
21
+ children: time
22
+ }),
23
+ onCheckedChange ? /* @__PURE__ */ jsx(CheckCircle, {
24
+ checked: !!checked,
25
+ onCheckedChange: (v) => onCheckedChange(v === true),
26
+ tone: important && !checked ? "important" : "default",
27
+ "aria-labelledby": titleId
28
+ }) : /* @__PURE__ */ jsx("span", {
29
+ className: "flex size-6 shrink-0 items-center justify-center text-muted-foreground",
30
+ children: /* @__PURE__ */ jsx(CalendarIcon, {
31
+ "aria-hidden": "true",
32
+ className: "size-[18px]"
33
+ })
34
+ }),
35
+ /* @__PURE__ */ jsxs("div", {
36
+ className: "flex min-w-0 flex-1 flex-col",
37
+ children: [onOpen ? /* @__PURE__ */ jsx("button", {
38
+ type: "button",
39
+ id: titleId,
40
+ onClick: onOpen,
41
+ className: cn(titleClass, "self-start rounded-sm text-left outline-none hover:underline hover:underline-offset-4 focus-visible:ring-4 focus-visible:ring-ring/20"),
42
+ children: title
43
+ }) : /* @__PURE__ */ jsx("span", {
44
+ id: titleId,
45
+ className: titleClass,
46
+ children: title
47
+ }), meta && /* @__PURE__ */ jsx("span", {
48
+ className: "truncate text-xs text-muted-foreground",
49
+ children: meta
50
+ })]
51
+ }),
52
+ who && /* @__PURE__ */ jsxs("span", {
53
+ className: "flex max-w-32 shrink-0 items-center gap-1.5 text-[13px] leading-6 text-muted-foreground",
54
+ children: [/* @__PURE__ */ jsx(CategoryDot, { color: who.color }), /* @__PURE__ */ jsx("span", {
55
+ className: "truncate",
56
+ children: who.name
57
+ })]
58
+ })
59
+ ]
60
+ });
61
+ }
62
+ export { AgendaRow };
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ export interface DayStripDay {
3
+ /** DOM id of the element to jump to, e.g. a SectionedList section id. */
4
+ id: string;
5
+ /** Floating local date, `YYYY-MM-DD`. */
6
+ date: string;
7
+ /** Shows a dot under the date. */
8
+ hasPlans?: boolean;
9
+ }
10
+ export interface DayStripProps extends React.ComponentProps<"nav"> {
11
+ days: DayStripDay[];
12
+ /** Id of the selected day. */
13
+ value?: string;
14
+ /** Called with the day's id after it scrolls that element into view. */
15
+ onValueChange?: (id: string) => void;
16
+ }
17
+ /**
18
+ * A row of day buttons (weekday, date, a dot when there are plans) that jump to the element with the day's id.
19
+ * Scrolls inside itself when the days do not fit. Smooth scrolling is off under prefers-reduced-motion.
20
+ */
21
+ declare function DayStrip({ days, value, onValueChange, className, "aria-label": ariaLabel, ...props }: DayStripProps): React.JSX.Element;
22
+ export { DayStrip };
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { parseLocalDate } from "../../lib/date.js";
3
+ import { cn } from "../../lib/utils.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ // src/components/data/day-strip.tsx
6
+ /**
7
+ * A row of day buttons (weekday, date, a dot when there are plans) that jump to the element with the day's id.
8
+ * Scrolls inside itself when the days do not fit. Smooth scrolling is off under prefers-reduced-motion.
9
+ */
10
+ function DayStrip({ days, value, onValueChange, className, "aria-label": ariaLabel, ...props }) {
11
+ const jump = (e, id) => {
12
+ e.preventDefault();
13
+ const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
14
+ const el = document.getElementById(id);
15
+ if (el) {
16
+ el.scrollIntoView?.({
17
+ behavior: reduce ? "auto" : "smooth",
18
+ block: "start"
19
+ });
20
+ if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "-1");
21
+ el.focus({ preventScroll: true });
22
+ }
23
+ onValueChange?.(id);
24
+ };
25
+ return /* @__PURE__ */ jsx("nav", {
26
+ "data-slot": "day-strip",
27
+ "aria-label": ariaLabel ?? "Jump to a day",
28
+ className: cn("min-w-0 overflow-x-auto", className),
29
+ ...props,
30
+ children: /* @__PURE__ */ jsx("ul", {
31
+ className: "flex gap-2 p-1",
32
+ children: days.map((day) => {
33
+ const selected = day.id === value;
34
+ const date = parseLocalDate(day.date);
35
+ const label = date.toLocaleDateString(void 0, {
36
+ weekday: "long",
37
+ month: "long",
38
+ day: "numeric"
39
+ });
40
+ return /* @__PURE__ */ jsx("li", {
41
+ className: "flex shrink-0 grow basis-18",
42
+ children: /* @__PURE__ */ jsxs("a", {
43
+ href: `#${day.id}`,
44
+ "aria-current": selected ? "true" : void 0,
45
+ "aria-label": day.hasPlans ? `${label}, has plans` : label,
46
+ onClick: (e) => jump(e, day.id),
47
+ className: cn("flex min-h-21 flex-1 flex-col items-center justify-center gap-0.5 rounded-xl px-1 py-2 outline-none transition-colors focus-visible:ring-4 focus-visible:ring-ring/20 motion-reduce:transition-none", selected ? "bg-primary text-primary-foreground" : "bg-card text-foreground shadow-card hover:bg-muted"),
48
+ children: [
49
+ /* @__PURE__ */ jsx("span", {
50
+ className: cn("text-xs font-medium", selected ? "text-primary-foreground" : "text-muted-foreground"),
51
+ children: date.toLocaleDateString(void 0, { weekday: "short" })
52
+ }),
53
+ /* @__PURE__ */ jsx("span", {
54
+ className: "text-[22px] leading-tight font-semibold tracking-tight tabular-nums",
55
+ children: date.getDate()
56
+ }),
57
+ /* @__PURE__ */ jsx("span", {
58
+ "aria-hidden": "true",
59
+ className: cn("size-1.5 rounded-full", !day.hasPlans && "bg-transparent", day.hasPlans && (selected ? "bg-primary-foreground" : "bg-primary"))
60
+ })
61
+ ]
62
+ })
63
+ }, day.id);
64
+ })
65
+ })
66
+ });
67
+ }
68
+ export { DayStrip };
@@ -8,22 +8,36 @@ export interface SectionedListItem {
8
8
  important?: boolean;
9
9
  assignee?: TaskRowAssignee;
10
10
  }
11
- export interface SectionedListSection {
11
+ export interface SectionedListSection<T extends SectionedListItem = SectionedListItem> {
12
+ /** Also the section's DOM id, so a DayStrip or `#id` link can jump to it. */
12
13
  id: string;
13
14
  /** Section heading, e.g. "Today" or "Groceries". Omit for a section without one. */
14
15
  title?: React.ReactNode;
15
- items: SectionedListItem[];
16
+ /** At the right of the heading, e.g. a small ghost Button "Move to today". */
17
+ action?: React.ReactNode;
18
+ /** Keeps the section shown with this text when it has no open item, e.g. "Nothing planned". */
19
+ empty?: React.ReactNode;
20
+ items: T[];
16
21
  }
17
- export interface SectionedListProps extends Omit<React.ComponentProps<"div">, "onToggle"> {
18
- sections: SectionedListSection[];
22
+ export interface SectionedListProps<T extends SectionedListItem = SectionedListItem> extends Omit<React.ComponentProps<"div">, "onToggle"> {
23
+ sections: SectionedListSection<T>[];
19
24
  /** Called when an item's check changes. Update `done` in your data; the row folds away by itself. */
20
25
  onToggle: (id: string, done: boolean) => void;
26
+ /**
27
+ * Draws a row instead of TaskRow, e.g. an AgendaRow. Call `toggle(done)` from its check so the row lingers and
28
+ * folds away like a TaskRow. Items without `done` (events) always stay.
29
+ */
30
+ renderItem?: (item: T, toggle: (done: boolean) => void) => React.ReactNode;
31
+ /** How long a ticked row stays struck through before it folds away, e.g. about the length of an Undo toast. Default 600. */
32
+ lingerMs?: number;
21
33
  /** Makes item titles buttons, e.g. to open a DetailSheet. */
22
34
  onOpen?: (id: string) => void;
23
35
  /** Shows one add box at the top. Called with the trimmed text on Enter. */
24
36
  onAdd?: (text: string) => void;
25
37
  /** Add box placeholder and label. Default "Add a to-do". */
26
38
  addPlaceholder?: string;
39
+ /** Chips at the right of the add box while it has text, e.g. the date or person parsed from it. Like AskBox `chips`. */
40
+ addChips?: (text: string) => React.ReactNode;
27
41
  /** Shown when no item is open, e.g. <Empty /> or a line of text. */
28
42
  empty?: React.ReactNode;
29
43
  /** Start with the done items shown. Default false. */
@@ -31,7 +45,8 @@ export interface SectionedListProps extends Omit<React.ComponentProps<"div">, "o
31
45
  }
32
46
  /**
33
47
  * A to-do list: one add box, sections with headings, done items folded into a "Show N done" toggle, an empty
34
- * state. Rows fold in and out with Collapse, so nothing jumps.
48
+ * state. Rows fold in and out with Collapse, so nothing jumps. `renderItem` draws other rows (AgendaRow), so a week
49
+ * agenda is one SectionedList with a section per day.
35
50
  */
36
- declare function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder, empty, defaultShowDone, className, ...props }: SectionedListProps): React.JSX.Element;
51
+ declare function SectionedList<T extends SectionedListItem = SectionedListItem>({ sections, onToggle, renderItem, lingerMs, onOpen, onAdd, addPlaceholder, addChips, empty, defaultShowDone, className, ...props }: SectionedListProps<T>): React.JSX.Element;
37
52
  export { SectionedList };
@@ -6,13 +6,12 @@ import * as React from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { ChevronDownIcon, PlusIcon } from "lucide-react";
8
8
  // src/components/data/sectioned-list.tsx
9
- /** How long a ticked row stays in place (so the check is seen) before it folds away. */
10
- const LINGER_MS = 600;
11
9
  /**
12
10
  * A to-do list: one add box, sections with headings, done items folded into a "Show N done" toggle, an empty
13
- * state. Rows fold in and out with Collapse, so nothing jumps.
11
+ * state. Rows fold in and out with Collapse, so nothing jumps. `renderItem` draws other rows (AgendaRow), so a week
12
+ * agenda is one SectionedList with a section per day.
14
13
  */
15
- function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder = "Add a to-do", empty, defaultShowDone = false, className, ...props }) {
14
+ function SectionedList({ sections, onToggle, renderItem, lingerMs = 600, onOpen, onAdd, addPlaceholder = "Add a to-do", addChips, empty, defaultShowDone = false, className, ...props }) {
16
15
  const [showDone, setShowDone] = React.useState(defaultShowDone);
17
16
  const [lingering, setLingering] = React.useState(/* @__PURE__ */ new Set());
18
17
  const timers = React.useRef(/* @__PURE__ */ new Map());
@@ -31,14 +30,14 @@ function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder = "Ad
31
30
  next.delete(id);
32
31
  return next;
33
32
  });
34
- }, LINGER_MS));
33
+ }, lingerMs));
35
34
  }
36
35
  onToggle(id, done);
37
36
  };
38
37
  const shown = (item) => !item.done || lingering.has(item.id);
39
38
  const done = sections.flatMap((s) => s.items.filter((i) => i.done && !lingering.has(i.id)));
40
39
  const anyOpen = sections.some((s) => s.items.some(shown));
41
- const row = (item) => /* @__PURE__ */ jsx(TaskRow, {
40
+ const row = (item) => renderItem ? renderItem(item, (v) => toggle(item.id, v)) : /* @__PURE__ */ jsx(TaskRow, {
42
41
  title: item.title,
43
42
  meta: item.meta,
44
43
  checked: !!item.done,
@@ -54,25 +53,44 @@ function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder = "Ad
54
53
  children: [
55
54
  onAdd && /* @__PURE__ */ jsx(AddBox, {
56
55
  placeholder: addPlaceholder,
57
- onAdd
56
+ onAdd,
57
+ chips: addChips
58
58
  }),
59
59
  sections.map((section) => {
60
- const visible = section.items.some(shown);
60
+ const hasOpen = section.items.some(shown);
61
61
  return /* @__PURE__ */ jsx(Collapse, {
62
- open: visible,
62
+ open: hasOpen || section.empty != null,
63
63
  children: /* @__PURE__ */ jsxs("section", {
64
+ id: section.id,
64
65
  "aria-label": typeof section.title === "string" ? section.title : void 0,
65
- children: [section.title && /* @__PURE__ */ jsx("h2", {
66
- className: "px-2 pb-1 text-[13px] font-semibold text-muted-foreground",
67
- children: section.title
68
- }), /* @__PURE__ */ jsx("ul", {
69
- className: "flex flex-col",
70
- children: section.items.map((item) => /* @__PURE__ */ jsx(Collapse, {
71
- open: shown(item),
72
- role: "listitem",
73
- children: row(item)
74
- }, item.id))
75
- })]
66
+ children: [
67
+ (section.title || section.action) && /* @__PURE__ */ jsxs("div", {
68
+ className: "flex min-w-0 items-center gap-2 px-2 pb-1",
69
+ children: [section.title && /* @__PURE__ */ jsx("h2", {
70
+ className: "min-w-0 text-[13px] font-semibold text-muted-foreground",
71
+ children: section.title
72
+ }), section.action && /* @__PURE__ */ jsx("div", {
73
+ className: "ml-auto flex shrink-0 items-center max-md:[&>*]:min-h-11",
74
+ children: section.action
75
+ })]
76
+ }),
77
+ section.empty != null && /* @__PURE__ */ jsx(Collapse, {
78
+ open: !hasOpen,
79
+ children: /* @__PURE__ */ jsx("p", {
80
+ "data-slot": "sectioned-list-section-empty",
81
+ className: "px-2 py-2.5 text-sm text-muted-foreground",
82
+ children: section.empty
83
+ })
84
+ }),
85
+ /* @__PURE__ */ jsx("ul", {
86
+ className: "flex flex-col",
87
+ children: section.items.map((item) => /* @__PURE__ */ jsx(Collapse, {
88
+ open: shown(item),
89
+ role: "listitem",
90
+ children: row(item)
91
+ }, item.id))
92
+ })
93
+ ]
76
94
  })
77
95
  }, section.id);
78
96
  }),
@@ -102,7 +120,7 @@ function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder = "Ad
102
120
  ]
103
121
  });
104
122
  }
105
- function AddBox({ placeholder, onAdd }) {
123
+ function AddBox({ placeholder, onAdd, chips }) {
106
124
  const [value, setValue] = React.useState("");
107
125
  const id = React.useId();
108
126
  return /* @__PURE__ */ jsxs("form", {
@@ -132,6 +150,11 @@ function AddBox({ placeholder, onAdd }) {
132
150
  placeholder,
133
151
  autoComplete: "off",
134
152
  className: "h-full min-w-0 flex-1 bg-transparent text-[15px] text-foreground outline-none placeholder:text-muted-foreground"
153
+ }),
154
+ chips && value.trim() && /* @__PURE__ */ jsx("div", {
155
+ "data-slot": "sectioned-list-add-chips",
156
+ className: "flex shrink-0 items-center gap-1.5",
157
+ children: chips(value)
135
158
  })
136
159
  ]
137
160
  });
@@ -1,7 +1,10 @@
1
+ import { CategoryColor } from "../admin/category.js";
1
2
  import * as React from "react";
2
3
  export interface TaskRowAssignee {
3
4
  name: string;
4
5
  avatarUrl?: string;
6
+ /** Category colour behind the initials when there is no photo. */
7
+ color?: CategoryColor;
5
8
  }
6
9
  export interface TaskRowProps extends Omit<React.ComponentProps<"div">, "title"> {
7
10
  title: React.ReactNode;
@@ -1,6 +1,5 @@
1
1
  "use client";
2
- import { initials } from "../admin/user-chip.js";
3
- import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
2
+ import { MemberAvatar } from "../admin/user-chip.js";
4
3
  import { CheckCircle } from "../ui/check-circle.js";
5
4
  import { cn } from "../../lib/utils.js";
6
5
  import * as React from "react";
@@ -40,19 +39,12 @@ function TaskRow({ title, meta, checked, onCheckedChange, assignee, important, o
40
39
  children: meta
41
40
  })]
42
41
  }),
43
- assignee && /* @__PURE__ */ jsxs(Avatar, {
42
+ assignee && /* @__PURE__ */ jsx(MemberAvatar, {
44
43
  size: "sm",
45
- role: "img",
46
- "aria-label": assignee.name,
47
- title: assignee.name,
48
- children: [assignee.avatarUrl && /* @__PURE__ */ jsx(AvatarImage, {
49
- src: assignee.avatarUrl,
50
- alt: ""
51
- }), /* @__PURE__ */ jsx(AvatarFallback, {
52
- "aria-hidden": "true",
53
- className: "group-data-[size=sm]/avatar:text-xs",
54
- children: initials(assignee.name)
55
- })]
44
+ name: assignee.name,
45
+ avatarUrl: assignee.avatarUrl,
46
+ color: assignee.color,
47
+ title: assignee.name
56
48
  })
57
49
  ]
58
50
  });
@@ -21,7 +21,7 @@ function AvatarImage({ className, ...props }) {
21
21
  function AvatarFallback({ className, ...props }) {
22
22
  return /* @__PURE__ */ jsx(Avatar$1.Fallback, {
23
23
  "data-slot": "avatar-fallback",
24
- className: cn("flex size-full items-center justify-center rounded-full bg-primary-soft text-[13px] font-semibold text-primary group-data-[size=sm]/avatar:text-[11px] group-data-[size=lg]/avatar:text-lg", className),
24
+ className: cn("flex size-full items-center justify-center rounded-full bg-primary-soft text-[13px] font-semibold text-primary group-data-[size=sm]/avatar:text-xs group-data-[size=lg]/avatar:text-lg", className),
25
25
  ...props
26
26
  });
27
27
  }
@@ -5,7 +5,7 @@ import { jsx } from "react/jsx-runtime";
5
5
  function Kbd({ className, ...props }) {
6
6
  return /* @__PURE__ */ jsx("kbd", {
7
7
  "data-slot": "kbd",
8
- className: cn("pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-[6px] border bg-card px-1.5 font-mono text-[11px] font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3", className),
8
+ className: cn("pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-[6px] border bg-card px-1.5 font-mono text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3", className),
9
9
  ...props
10
10
  });
11
11
  }
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+ export interface PasswordInputProps extends Omit<React.ComponentProps<"input">, "type"> {
3
+ /** Classes for the wrapping div. `className` goes to the input. */
4
+ wrapperClassName?: string;
5
+ }
6
+ /** Password `Input` with a Show/Hide button that switches it to plain text. Props go to the input. */
7
+ declare function PasswordInput({ className, wrapperClassName, disabled, ...props }: PasswordInputProps): React.JSX.Element;
8
+ export { PasswordInput };