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.
- package/AGENTS.md +2 -2
- package/dist/components/admin/app-nav.d.ts +4 -0
- package/dist/components/admin/app-nav.js +5 -2
- package/dist/components/admin/avatar-upload.d.ts +32 -0
- package/dist/components/admin/avatar-upload.js +121 -0
- package/dist/components/admin/category.d.ts +17 -0
- package/dist/components/admin/category.js +35 -0
- package/dist/components/admin/celebration.d.ts +7 -0
- package/dist/components/admin/celebration.js +61 -0
- package/dist/components/admin/color-swatch-picker.d.ts +15 -0
- package/dist/components/admin/color-swatch-picker.js +68 -0
- package/dist/components/admin/date-time-picker.d.ts +20 -0
- package/dist/components/admin/date-time-picker.js +97 -0
- package/dist/components/admin/detail-sheet.d.ts +18 -0
- package/dist/components/admin/detail-sheet.js +80 -0
- package/dist/components/admin/markdown-editor.d.ts +15 -0
- package/dist/components/admin/markdown-editor.js +42 -0
- package/dist/components/admin/user-chip.d.ts +34 -3
- package/dist/components/admin/user-chip.js +66 -7
- package/dist/components/admin/user-menu.d.ts +4 -1
- package/dist/components/admin/user-menu.js +2 -1
- package/dist/components/ai/ask-box.d.ts +14 -2
- package/dist/components/ai/ask-box.js +90 -54
- package/dist/components/ai/assistant.d.ts +13 -3
- package/dist/components/ai/assistant.js +37 -7
- package/dist/components/ai/code-block.d.ts +10 -3
- package/dist/components/ai/code-block.js +54 -5
- package/dist/components/ai/conversation-list.d.ts +13 -3
- package/dist/components/ai/conversation-list.js +15 -26
- package/dist/components/auth/auth.js +4 -6
- package/dist/components/data/agenda-row.d.ts +26 -0
- package/dist/components/data/agenda-row.js +62 -0
- package/dist/components/data/day-strip.d.ts +22 -0
- package/dist/components/data/day-strip.js +68 -0
- package/dist/components/data/sectioned-list.d.ts +21 -6
- package/dist/components/data/sectioned-list.js +44 -21
- package/dist/components/data/task-row.d.ts +3 -0
- package/dist/components/data/task-row.js +6 -14
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/kbd.js +1 -1
- package/dist/components/ui/password-input.d.ts +8 -0
- package/dist/components/ui/password-input.js +32 -0
- package/dist/index.d.ts +15 -5
- package/dist/index.js +13 -3
- package/dist/lib/date.js +8 -0
- package/dist/lib/toast-clearance.js +22 -0
- package/dist/styles.css +48 -0
- package/docs/COMPONENTS.md +165 -15
- package/docs/ROADMAP.md +12 -11
- package/llms.txt +8 -7
- package/package.json +1 -1
- 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
|
|
44
|
-
* newest first, and drops empty groups.
|
|
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,
|
|
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
|
|
98
|
-
* newest first, and drops empty groups.
|
|
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,
|
|
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 [
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
60
|
+
const hasOpen = section.items.some(shown);
|
|
61
61
|
return /* @__PURE__ */ jsx(Collapse, {
|
|
62
|
-
open:
|
|
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: [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 {
|
|
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__ */
|
|
42
|
+
assignee && /* @__PURE__ */ jsx(MemberAvatar, {
|
|
44
43
|
size: "sm",
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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-
|
|
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-
|
|
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 };
|