harebear-ui 0.6.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.
- package/AGENTS.md +32 -12
- package/README.md +109 -36
- package/bin/harebear-ui.mjs +66 -3
- package/dist/components/admin/app-nav.d.ts +57 -0
- package/dist/components/admin/app-nav.js +172 -0
- package/dist/components/admin/focus-layout.d.ts +14 -0
- package/dist/components/admin/focus-layout.js +36 -0
- package/dist/components/admin/settings.d.ts +31 -0
- package/dist/components/admin/settings.js +78 -0
- package/dist/components/ai/action-receipt.d.ts +16 -0
- package/dist/components/ai/action-receipt.js +47 -0
- package/dist/components/ai/ask-box.d.ts +27 -0
- package/dist/components/ai/ask-box.js +85 -0
- package/dist/components/ai/assistant.d.ts +94 -0
- package/dist/components/ai/assistant.js +229 -0
- package/dist/components/ai/dictation.d.ts +57 -0
- package/dist/components/ai/dictation.js +148 -0
- package/dist/components/data/sectioned-list.d.ts +37 -0
- package/dist/components/data/sectioned-list.js +139 -0
- package/dist/components/data/task-row.d.ts +22 -0
- package/dist/components/data/task-row.js +60 -0
- package/dist/components/layout/collapse.d.ts +14 -0
- package/dist/components/layout/collapse.js +24 -0
- package/dist/components/layout/media-item.d.ts +13 -0
- package/dist/components/layout/media-item.js +54 -0
- package/dist/components/layout/metric.d.ts +15 -0
- package/dist/components/layout/metric.js +51 -0
- package/dist/components/layout/show-more.d.ts +17 -0
- package/dist/components/layout/show-more.js +30 -0
- package/dist/components/layout/stack.d.ts +39 -0
- package/dist/components/layout/stack.js +114 -0
- package/dist/components/layout/text.d.ts +37 -0
- package/dist/components/layout/text.js +52 -0
- package/dist/components/theme/theme-toggle.d.ts +4 -1
- package/dist/components/theme/theme-toggle.js +11 -6
- package/dist/components/ui/check-circle.d.ts +16 -0
- package/dist/components/ui/check-circle.js +39 -0
- package/dist/components/ui/sonner.d.ts +15 -2
- package/dist/components/ui/sonner.js +20 -3
- package/dist/index.d.ts +18 -2
- package/dist/index.js +18 -2
- package/dist/styles.css +75 -0
- package/docs/COMPONENTS.md +160 -21
- package/docs/ROADMAP.md +95 -0
- package/docs/guides/agents.md +75 -0
- package/docs/guides/getting-started.md +217 -0
- package/docs/guides/theming.md +99 -0
- package/llms.txt +11 -3
- package/package.json +2 -1
- package/skills/harebear-ui/SKILL.md +2 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { ArrowUpIcon, MicIcon } from "lucide-react";
|
|
7
|
+
// src/components/ai/dictation.tsx
|
|
8
|
+
function recognitionCtor() {
|
|
9
|
+
if (typeof window === "undefined") return void 0;
|
|
10
|
+
const w = window;
|
|
11
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
12
|
+
}
|
|
13
|
+
/** SpeechSource on the browser's SpeechRecognition API. Unsupported (Firefox, most WebViews) means no mic button. */
|
|
14
|
+
const browserSpeechSource = {
|
|
15
|
+
isSupported: () => recognitionCtor() !== void 0,
|
|
16
|
+
start(handlers, { lang }) {
|
|
17
|
+
const Ctor = recognitionCtor();
|
|
18
|
+
if (!Ctor) {
|
|
19
|
+
handlers.onError("not-supported");
|
|
20
|
+
handlers.onEnd();
|
|
21
|
+
return () => {};
|
|
22
|
+
}
|
|
23
|
+
const rec = new Ctor();
|
|
24
|
+
if (lang) rec.lang = lang;
|
|
25
|
+
rec.interimResults = true;
|
|
26
|
+
rec.continuous = false;
|
|
27
|
+
rec.onresult = (e) => {
|
|
28
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
29
|
+
const r = e.results[i];
|
|
30
|
+
if (r) handlers.onResult(r[0].transcript, r.isFinal);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
rec.onerror = (e) => handlers.onError(e.error);
|
|
34
|
+
rec.onend = () => handlers.onEnd();
|
|
35
|
+
rec.start();
|
|
36
|
+
return () => rec.stop();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const noopSubscribe = () => () => {};
|
|
40
|
+
/** Dictation state for a custom control: `{ supported, listening, interim, error, start, stop }`. */
|
|
41
|
+
function useDictation({ source = browserSpeechSource, lang, onTranscript }) {
|
|
42
|
+
const supported = React.useSyncExternalStore(noopSubscribe, source.isSupported, () => false);
|
|
43
|
+
const [listening, setListening] = React.useState(false);
|
|
44
|
+
const [interim, setInterim] = React.useState("");
|
|
45
|
+
const [error, setError] = React.useState(null);
|
|
46
|
+
const stopRef = React.useRef(null);
|
|
47
|
+
const onTranscriptRef = React.useRef(onTranscript);
|
|
48
|
+
onTranscriptRef.current = onTranscript;
|
|
49
|
+
const stop = React.useCallback(() => {
|
|
50
|
+
stopRef.current?.();
|
|
51
|
+
}, []);
|
|
52
|
+
const start = React.useCallback(() => {
|
|
53
|
+
if (stopRef.current) return;
|
|
54
|
+
setError(null);
|
|
55
|
+
setInterim("");
|
|
56
|
+
setListening(true);
|
|
57
|
+
stopRef.current = source.start({
|
|
58
|
+
onResult: (text, final) => {
|
|
59
|
+
if (final) {
|
|
60
|
+
setInterim("");
|
|
61
|
+
onTranscriptRef.current(text.trim());
|
|
62
|
+
} else setInterim(text);
|
|
63
|
+
},
|
|
64
|
+
onError: (e) => setError(e),
|
|
65
|
+
onEnd: () => {
|
|
66
|
+
stopRef.current = null;
|
|
67
|
+
setInterim("");
|
|
68
|
+
setListening(false);
|
|
69
|
+
}
|
|
70
|
+
}, { lang });
|
|
71
|
+
}, [source, lang]);
|
|
72
|
+
React.useEffect(() => () => stopRef.current?.(), []);
|
|
73
|
+
return {
|
|
74
|
+
supported,
|
|
75
|
+
listening,
|
|
76
|
+
interim,
|
|
77
|
+
error,
|
|
78
|
+
start,
|
|
79
|
+
stop
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One round button for a composer: a mic when empty, stop (with listening bars) while listening, send once there is
|
|
84
|
+
* text. Announces "Listening" in a polite live region. Without a supported speech source the mic is hidden and the
|
|
85
|
+
* button is just send.
|
|
86
|
+
*/
|
|
87
|
+
function DictationButton({ value, onTranscript, onSend, source, lang, disabled, sendLabel = "Send message", className, ...props }) {
|
|
88
|
+
const d = useDictation({
|
|
89
|
+
source,
|
|
90
|
+
lang,
|
|
91
|
+
onTranscript
|
|
92
|
+
});
|
|
93
|
+
const hasText = value.trim().length > 0;
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
95
|
+
"data-slot": "dictation-button",
|
|
96
|
+
className: cn("flex shrink-0 items-center", className),
|
|
97
|
+
...props,
|
|
98
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
99
|
+
role: "status",
|
|
100
|
+
"aria-live": "polite",
|
|
101
|
+
className: "sr-only",
|
|
102
|
+
children: d.listening ? "Listening" : ""
|
|
103
|
+
}), d.listening ? /* @__PURE__ */ jsx(Button, {
|
|
104
|
+
type: "button",
|
|
105
|
+
size: "icon",
|
|
106
|
+
shape: "pill",
|
|
107
|
+
className: "size-10 gap-0.5",
|
|
108
|
+
"aria-label": "Stop dictation",
|
|
109
|
+
onClick: d.stop,
|
|
110
|
+
children: /* @__PURE__ */ jsx(ListeningBars, {})
|
|
111
|
+
}) : !hasText && d.supported ? /* @__PURE__ */ jsx(Button, {
|
|
112
|
+
type: "button",
|
|
113
|
+
variant: "ghost",
|
|
114
|
+
size: "icon",
|
|
115
|
+
shape: "pill",
|
|
116
|
+
className: "size-10",
|
|
117
|
+
"aria-label": "Dictate",
|
|
118
|
+
disabled,
|
|
119
|
+
onClick: d.start,
|
|
120
|
+
children: /* @__PURE__ */ jsx(MicIcon, {})
|
|
121
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
122
|
+
type: onSend ? "button" : "submit",
|
|
123
|
+
size: "icon",
|
|
124
|
+
shape: "pill",
|
|
125
|
+
className: "size-10",
|
|
126
|
+
"aria-label": sendLabel,
|
|
127
|
+
disabled: disabled || !hasText,
|
|
128
|
+
onClick: onSend,
|
|
129
|
+
children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
|
|
130
|
+
})]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function ListeningBars() {
|
|
134
|
+
return /* @__PURE__ */ jsx("span", {
|
|
135
|
+
"aria-hidden": "true",
|
|
136
|
+
className: "flex h-4 items-center gap-0.5",
|
|
137
|
+
children: [
|
|
138
|
+
0,
|
|
139
|
+
150,
|
|
140
|
+
300,
|
|
141
|
+
450
|
|
142
|
+
].map((delay) => /* @__PURE__ */ jsx("span", {
|
|
143
|
+
className: "h-full w-0.75 origin-center animate-listening rounded-full bg-current motion-reduce:scale-y-75",
|
|
144
|
+
style: { animationDelay: `${delay}ms` }
|
|
145
|
+
}, delay))
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export { DictationButton, browserSpeechSource, useDictation };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TaskRowAssignee } from "./task-row.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface SectionedListItem {
|
|
4
|
+
id: string;
|
|
5
|
+
title: React.ReactNode;
|
|
6
|
+
meta?: React.ReactNode;
|
|
7
|
+
done?: boolean;
|
|
8
|
+
important?: boolean;
|
|
9
|
+
assignee?: TaskRowAssignee;
|
|
10
|
+
}
|
|
11
|
+
export interface SectionedListSection {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Section heading, e.g. "Today" or "Groceries". Omit for a section without one. */
|
|
14
|
+
title?: React.ReactNode;
|
|
15
|
+
items: SectionedListItem[];
|
|
16
|
+
}
|
|
17
|
+
export interface SectionedListProps extends Omit<React.ComponentProps<"div">, "onToggle"> {
|
|
18
|
+
sections: SectionedListSection[];
|
|
19
|
+
/** Called when an item's check changes. Update `done` in your data; the row folds away by itself. */
|
|
20
|
+
onToggle: (id: string, done: boolean) => void;
|
|
21
|
+
/** Makes item titles buttons, e.g. to open a DetailSheet. */
|
|
22
|
+
onOpen?: (id: string) => void;
|
|
23
|
+
/** Shows one add box at the top. Called with the trimmed text on Enter. */
|
|
24
|
+
onAdd?: (text: string) => void;
|
|
25
|
+
/** Add box placeholder and label. Default "Add a to-do". */
|
|
26
|
+
addPlaceholder?: string;
|
|
27
|
+
/** Shown when no item is open, e.g. <Empty /> or a line of text. */
|
|
28
|
+
empty?: React.ReactNode;
|
|
29
|
+
/** Start with the done items shown. Default false. */
|
|
30
|
+
defaultShowDone?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 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.
|
|
35
|
+
*/
|
|
36
|
+
declare function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder, empty, defaultShowDone, className, ...props }: SectionedListProps): React.JSX.Element;
|
|
37
|
+
export { SectionedList };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { TaskRow } from "./task-row.js";
|
|
3
|
+
import { Collapse } from "../layout/collapse.js";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { ChevronDownIcon, PlusIcon } from "lucide-react";
|
|
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
|
+
/**
|
|
12
|
+
* 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.
|
|
14
|
+
*/
|
|
15
|
+
function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder = "Add a to-do", empty, defaultShowDone = false, className, ...props }) {
|
|
16
|
+
const [showDone, setShowDone] = React.useState(defaultShowDone);
|
|
17
|
+
const [lingering, setLingering] = React.useState(/* @__PURE__ */ new Set());
|
|
18
|
+
const timers = React.useRef(/* @__PURE__ */ new Map());
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
const map = timers.current;
|
|
21
|
+
return () => map.forEach(clearTimeout);
|
|
22
|
+
}, []);
|
|
23
|
+
const toggle = (id, done) => {
|
|
24
|
+
clearTimeout(timers.current.get(id));
|
|
25
|
+
if (done) {
|
|
26
|
+
setLingering((s) => new Set(s).add(id));
|
|
27
|
+
timers.current.set(id, setTimeout(() => {
|
|
28
|
+
timers.current.delete(id);
|
|
29
|
+
setLingering((s) => {
|
|
30
|
+
const next = new Set(s);
|
|
31
|
+
next.delete(id);
|
|
32
|
+
return next;
|
|
33
|
+
});
|
|
34
|
+
}, LINGER_MS));
|
|
35
|
+
}
|
|
36
|
+
onToggle(id, done);
|
|
37
|
+
};
|
|
38
|
+
const shown = (item) => !item.done || lingering.has(item.id);
|
|
39
|
+
const done = sections.flatMap((s) => s.items.filter((i) => i.done && !lingering.has(i.id)));
|
|
40
|
+
const anyOpen = sections.some((s) => s.items.some(shown));
|
|
41
|
+
const row = (item) => /* @__PURE__ */ jsx(TaskRow, {
|
|
42
|
+
title: item.title,
|
|
43
|
+
meta: item.meta,
|
|
44
|
+
checked: !!item.done,
|
|
45
|
+
important: item.important,
|
|
46
|
+
assignee: item.assignee,
|
|
47
|
+
onCheckedChange: (v) => toggle(item.id, v),
|
|
48
|
+
onOpen: onOpen ? () => onOpen(item.id) : void 0
|
|
49
|
+
});
|
|
50
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
51
|
+
"data-slot": "sectioned-list",
|
|
52
|
+
className: cn("flex min-w-0 flex-col gap-6", className),
|
|
53
|
+
...props,
|
|
54
|
+
children: [
|
|
55
|
+
onAdd && /* @__PURE__ */ jsx(AddBox, {
|
|
56
|
+
placeholder: addPlaceholder,
|
|
57
|
+
onAdd
|
|
58
|
+
}),
|
|
59
|
+
sections.map((section) => {
|
|
60
|
+
const visible = section.items.some(shown);
|
|
61
|
+
return /* @__PURE__ */ jsx(Collapse, {
|
|
62
|
+
open: visible,
|
|
63
|
+
children: /* @__PURE__ */ jsxs("section", {
|
|
64
|
+
"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
|
+
})]
|
|
76
|
+
})
|
|
77
|
+
}, section.id);
|
|
78
|
+
}),
|
|
79
|
+
!anyOpen && empty && /* @__PURE__ */ jsx("div", {
|
|
80
|
+
"data-slot": "sectioned-list-empty",
|
|
81
|
+
children: empty
|
|
82
|
+
}),
|
|
83
|
+
done.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
84
|
+
className: "flex flex-col gap-1",
|
|
85
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
86
|
+
type: "button",
|
|
87
|
+
"aria-expanded": showDone,
|
|
88
|
+
onClick: () => setShowDone((v) => !v),
|
|
89
|
+
className: "flex min-h-11 items-center gap-1.5 self-start rounded-md px-2 text-[13px] font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20",
|
|
90
|
+
children: [/* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
91
|
+
"aria-hidden": "true",
|
|
92
|
+
className: cn("size-4 transition-transform motion-reduce:transition-none", !showDone && "-rotate-90")
|
|
93
|
+
}), showDone ? "Hide done" : `Show ${done.length} done`]
|
|
94
|
+
}), /* @__PURE__ */ jsx(Collapse, {
|
|
95
|
+
open: showDone,
|
|
96
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
97
|
+
className: "flex flex-col",
|
|
98
|
+
children: done.map((item) => /* @__PURE__ */ jsx("li", { children: row(item) }, item.id))
|
|
99
|
+
})
|
|
100
|
+
})]
|
|
101
|
+
})
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function AddBox({ placeholder, onAdd }) {
|
|
106
|
+
const [value, setValue] = React.useState("");
|
|
107
|
+
const id = React.useId();
|
|
108
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
109
|
+
"data-slot": "sectioned-list-add",
|
|
110
|
+
onSubmit: (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
const text = value.trim();
|
|
113
|
+
if (!text) return;
|
|
114
|
+
onAdd(text);
|
|
115
|
+
setValue("");
|
|
116
|
+
},
|
|
117
|
+
className: "focus-box flex h-12 items-center gap-2.5 rounded-xl border border-transparent bg-muted px-3.5",
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ jsx(PlusIcon, {
|
|
120
|
+
"aria-hidden": "true",
|
|
121
|
+
className: "size-5 shrink-0 text-muted-foreground"
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsx("label", {
|
|
124
|
+
htmlFor: id,
|
|
125
|
+
className: "sr-only",
|
|
126
|
+
children: placeholder
|
|
127
|
+
}),
|
|
128
|
+
/* @__PURE__ */ jsx("input", {
|
|
129
|
+
id,
|
|
130
|
+
value,
|
|
131
|
+
onChange: (e) => setValue(e.target.value),
|
|
132
|
+
placeholder,
|
|
133
|
+
autoComplete: "off",
|
|
134
|
+
className: "h-full min-w-0 flex-1 bg-transparent text-[15px] text-foreground outline-none placeholder:text-muted-foreground"
|
|
135
|
+
})
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export { SectionedList };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface TaskRowAssignee {
|
|
3
|
+
name: string;
|
|
4
|
+
avatarUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TaskRowProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
7
|
+
title: React.ReactNode;
|
|
8
|
+
/** One line under the title, e.g. "Today · Groceries". Truncates. */
|
|
9
|
+
meta?: React.ReactNode;
|
|
10
|
+
checked: boolean;
|
|
11
|
+
onCheckedChange: (checked: boolean) => void;
|
|
12
|
+
/** Avatar at the right; the name is its accessible label. */
|
|
13
|
+
assignee?: TaskRowAssignee;
|
|
14
|
+
/** Warning-tinted check. Also say it in words, e.g. meta="Important · Today". */
|
|
15
|
+
important?: boolean;
|
|
16
|
+
/** Makes the title a button, e.g. to open a DetailSheet. */
|
|
17
|
+
onOpen?: () => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** One to-do: round check aligned with the first line, title, one meta line, optional assignee. */
|
|
21
|
+
declare function TaskRow({ title, meta, checked, onCheckedChange, assignee, important, onOpen, disabled, className, ...props }: TaskRowProps): React.JSX.Element;
|
|
22
|
+
export { TaskRow };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { initials } from "../admin/user-chip.js";
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
|
|
4
|
+
import { CheckCircle } from "../ui/check-circle.js";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
// src/components/data/task-row.tsx
|
|
9
|
+
/** One to-do: round check aligned with the first line, title, one meta line, optional assignee. */
|
|
10
|
+
function TaskRow({ title, meta, checked, onCheckedChange, assignee, important, onOpen, disabled, 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": "task-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(CheckCircle, {
|
|
20
|
+
checked,
|
|
21
|
+
onCheckedChange: (v) => onCheckedChange(v === true),
|
|
22
|
+
tone: important && !checked ? "important" : "default",
|
|
23
|
+
"aria-labelledby": titleId,
|
|
24
|
+
disabled
|
|
25
|
+
}),
|
|
26
|
+
/* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
28
|
+
children: [onOpen ? /* @__PURE__ */ jsx("button", {
|
|
29
|
+
type: "button",
|
|
30
|
+
id: titleId,
|
|
31
|
+
onClick: onOpen,
|
|
32
|
+
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"),
|
|
33
|
+
children: title
|
|
34
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
35
|
+
id: titleId,
|
|
36
|
+
className: titleClass,
|
|
37
|
+
children: title
|
|
38
|
+
}), meta && /* @__PURE__ */ jsx("span", {
|
|
39
|
+
className: "truncate text-xs text-muted-foreground",
|
|
40
|
+
children: meta
|
|
41
|
+
})]
|
|
42
|
+
}),
|
|
43
|
+
assignee && /* @__PURE__ */ jsxs(Avatar, {
|
|
44
|
+
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
|
+
})]
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export { TaskRow };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface CollapseProps extends React.ComponentProps<"div"> {
|
|
3
|
+
/** Shown at full height when true; animates to zero height when false. */
|
|
4
|
+
open: boolean;
|
|
5
|
+
/** Classes for the inner element that holds the children. */
|
|
6
|
+
contentClassName?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Animates its children's height between auto and 0 with a CSS grid-template-rows transition, so neighbours move
|
|
10
|
+
* smoothly instead of jumping. Closed content is `inert` and hidden from assistive tech. No animation under
|
|
11
|
+
* prefers-reduced-motion.
|
|
12
|
+
*/
|
|
13
|
+
declare function Collapse({ open, contentClassName, className, children, ...props }: CollapseProps): React.JSX.Element;
|
|
14
|
+
export { Collapse };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
// src/components/layout/collapse.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Animates its children's height between auto and 0 with a CSS grid-template-rows transition, so neighbours move
|
|
7
|
+
* smoothly instead of jumping. Closed content is `inert` and hidden from assistive tech. No animation under
|
|
8
|
+
* prefers-reduced-motion.
|
|
9
|
+
*/
|
|
10
|
+
function Collapse({ open, contentClassName, className, children, ...props }) {
|
|
11
|
+
return /* @__PURE__ */ jsx("div", {
|
|
12
|
+
"data-slot": "collapse",
|
|
13
|
+
"data-state": open ? "open" : "closed",
|
|
14
|
+
"aria-hidden": open ? void 0 : true,
|
|
15
|
+
inert: !open,
|
|
16
|
+
className: cn("grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none", open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0", className),
|
|
17
|
+
...props,
|
|
18
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: cn("min-h-0 overflow-hidden", contentClassName),
|
|
20
|
+
children
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export { Collapse };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MediaItemProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
3
|
+
title: React.ReactNode;
|
|
4
|
+
/** Opens in a new tab. */
|
|
5
|
+
href?: string;
|
|
6
|
+
image?: string;
|
|
7
|
+
imageShape?: "poster" | "square" | "wide";
|
|
8
|
+
meta?: React.ReactNode;
|
|
9
|
+
description?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
|
|
12
|
+
declare function MediaItem({ title, href, image, imageShape, meta, description, className, ...props }: MediaItemProps): React.JSX.Element;
|
|
13
|
+
export { MediaItem };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/components/layout/media-item.tsx
|
|
5
|
+
const shapeClass = {
|
|
6
|
+
poster: "aspect-[2/3] w-14",
|
|
7
|
+
square: "aspect-square w-12",
|
|
8
|
+
wide: "aspect-video w-24"
|
|
9
|
+
};
|
|
10
|
+
/** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
|
|
11
|
+
function MediaItem({ title, href, image, imageShape = "poster", meta, description, className, ...props }) {
|
|
12
|
+
const img = image ? /* @__PURE__ */ jsx("img", {
|
|
13
|
+
src: image,
|
|
14
|
+
alt: "",
|
|
15
|
+
loading: "lazy",
|
|
16
|
+
className: cn("shrink-0 rounded-md bg-muted object-cover", shapeClass[imageShape])
|
|
17
|
+
}) : null;
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
"data-slot": "media-item",
|
|
20
|
+
className: cn("flex min-w-0 items-start gap-3", className),
|
|
21
|
+
...props,
|
|
22
|
+
children: [img && href ? /* @__PURE__ */ jsx("a", {
|
|
23
|
+
href,
|
|
24
|
+
target: "_blank",
|
|
25
|
+
rel: "noreferrer",
|
|
26
|
+
tabIndex: -1,
|
|
27
|
+
className: "shrink-0",
|
|
28
|
+
children: img
|
|
29
|
+
}) : img, /* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: "flex min-w-0 flex-col gap-0.5",
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ jsx("span", {
|
|
33
|
+
className: "line-clamp-2 font-medium",
|
|
34
|
+
children: href ? /* @__PURE__ */ jsx("a", {
|
|
35
|
+
href,
|
|
36
|
+
target: "_blank",
|
|
37
|
+
rel: "noreferrer",
|
|
38
|
+
className: "outline-none hover:underline focus-visible:underline",
|
|
39
|
+
children: title
|
|
40
|
+
}) : title
|
|
41
|
+
}),
|
|
42
|
+
meta && /* @__PURE__ */ jsx("span", {
|
|
43
|
+
className: "text-xs text-muted-foreground",
|
|
44
|
+
children: meta
|
|
45
|
+
}),
|
|
46
|
+
description && /* @__PURE__ */ jsx("span", {
|
|
47
|
+
className: "line-clamp-2 text-[13px] text-muted-foreground",
|
|
48
|
+
children: description
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export { MediaItem };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MetricProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
3
|
+
label: React.ReactNode;
|
|
4
|
+
value: React.ReactNode;
|
|
5
|
+
unit?: React.ReactNode;
|
|
6
|
+
/** Percent change. Positive reads as good unless `lowerIsBetter`. */
|
|
7
|
+
delta?: number;
|
|
8
|
+
lowerIsBetter?: boolean;
|
|
9
|
+
align?: "start" | "center";
|
|
10
|
+
}
|
|
11
|
+
/** A number with a unit, a label under it, and an optional percent change over it. */
|
|
12
|
+
declare function Metric({ label, value, unit, delta, lowerIsBetter, align, className, ...props }: MetricProps): React.JSX.Element;
|
|
13
|
+
/** Metrics side by side, evenly spaced. */
|
|
14
|
+
declare function MetricGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
15
|
+
export { Metric, MetricGroup };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react";
|
|
5
|
+
// src/components/layout/metric.tsx
|
|
6
|
+
/** A number with a unit, a label under it, and an optional percent change over it. */
|
|
7
|
+
function Metric({ label, value, unit, delta, lowerIsBetter, align = "center", className, ...props }) {
|
|
8
|
+
const good = delta === void 0 || delta === 0 ? void 0 : lowerIsBetter ? delta < 0 : delta > 0;
|
|
9
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
10
|
+
"data-slot": "metric",
|
|
11
|
+
className: cn("flex min-w-0 flex-col gap-0.5", align === "center" ? "items-center text-center" : "items-start", className),
|
|
12
|
+
...props,
|
|
13
|
+
children: [
|
|
14
|
+
delta !== void 0 && Number.isFinite(delta) && /* @__PURE__ */ jsxs("span", {
|
|
15
|
+
className: cn("inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", good === void 0 ? "text-muted-foreground" : good ? "text-success" : "text-destructive"),
|
|
16
|
+
children: [
|
|
17
|
+
delta > 0 ? "+" : "",
|
|
18
|
+
delta.toFixed(1),
|
|
19
|
+
"%",
|
|
20
|
+
delta > 0 ? /* @__PURE__ */ jsx(ArrowUpRightIcon, {
|
|
21
|
+
"aria-hidden": true,
|
|
22
|
+
className: "size-3.5"
|
|
23
|
+
}) : delta < 0 ? /* @__PURE__ */ jsx(ArrowDownRightIcon, {
|
|
24
|
+
"aria-hidden": true,
|
|
25
|
+
className: "size-3.5"
|
|
26
|
+
}) : null
|
|
27
|
+
]
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ jsxs("span", {
|
|
30
|
+
className: "text-xl font-semibold tracking-tight whitespace-nowrap tabular-nums @sm:text-2xl",
|
|
31
|
+
children: [value, unit && /* @__PURE__ */ jsx("span", {
|
|
32
|
+
className: "ml-0.5 text-xs font-medium text-muted-foreground",
|
|
33
|
+
children: unit
|
|
34
|
+
})]
|
|
35
|
+
}),
|
|
36
|
+
/* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "text-xs font-medium tracking-wide text-muted-foreground uppercase",
|
|
38
|
+
children: label
|
|
39
|
+
})
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Metrics side by side, evenly spaced. */
|
|
44
|
+
function MetricGroup({ className, ...props }) {
|
|
45
|
+
return /* @__PURE__ */ jsx("div", {
|
|
46
|
+
"data-slot": "metric-group",
|
|
47
|
+
className: cn("grid auto-cols-fr grid-flow-col gap-2", className),
|
|
48
|
+
...props
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export { Metric, MetricGroup };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
declare const gapClass: {
|
|
3
|
+
readonly none: "gap-0";
|
|
4
|
+
readonly xs: "gap-1";
|
|
5
|
+
readonly sm: "gap-2";
|
|
6
|
+
readonly md: "gap-3";
|
|
7
|
+
readonly lg: "gap-4";
|
|
8
|
+
readonly xl: "gap-6";
|
|
9
|
+
};
|
|
10
|
+
export interface ShowMoreProps extends React.ComponentProps<"div"> {
|
|
11
|
+
/** Children shown before the "Show N more" button. Default 5. */
|
|
12
|
+
initial?: number;
|
|
13
|
+
gap?: keyof typeof gapClass;
|
|
14
|
+
}
|
|
15
|
+
/** A list that collapses after `initial` children, with a button to show the rest. */
|
|
16
|
+
declare function ShowMore({ initial, gap, className, children, ...props }: ShowMoreProps): React.JSX.Element;
|
|
17
|
+
export { ShowMore };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
// src/components/layout/show-more.tsx
|
|
6
|
+
const gapClass = {
|
|
7
|
+
none: "gap-0",
|
|
8
|
+
xs: "gap-1",
|
|
9
|
+
sm: "gap-2",
|
|
10
|
+
md: "gap-3",
|
|
11
|
+
lg: "gap-4",
|
|
12
|
+
xl: "gap-6"
|
|
13
|
+
};
|
|
14
|
+
/** A list that collapses after `initial` children, with a button to show the rest. */
|
|
15
|
+
function ShowMore({ initial = 5, gap = "md", className, children, ...props }) {
|
|
16
|
+
const [open, setOpen] = React.useState(false);
|
|
17
|
+
const all = React.Children.toArray(children);
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
"data-slot": "show-more",
|
|
20
|
+
className: cn("flex min-w-0 flex-col", gapClass[gap], className),
|
|
21
|
+
...props,
|
|
22
|
+
children: [open ? all : all.slice(0, initial), all.length > initial && /* @__PURE__ */ jsx("button", {
|
|
23
|
+
type: "button",
|
|
24
|
+
onClick: () => setOpen(!open),
|
|
25
|
+
className: "self-start rounded-md text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20",
|
|
26
|
+
children: open ? "Show less" : `Show ${all.length - initial} more`
|
|
27
|
+
})]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export { ShowMore };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type StackSpace = "none" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
3
|
+
export type StackAlign = "start" | "center" | "end" | "stretch" | "baseline";
|
|
4
|
+
export type StackJustify = "start" | "center" | "end" | "between";
|
|
5
|
+
export interface StackProps extends React.ComponentProps<"div"> {
|
|
6
|
+
gap?: StackSpace;
|
|
7
|
+
align?: StackAlign;
|
|
8
|
+
}
|
|
9
|
+
/** Vertical stack with a named gap. */
|
|
10
|
+
declare function Stack({ gap, align, className, ...props }: StackProps): React.JSX.Element;
|
|
11
|
+
export interface RowProps extends React.ComponentProps<"div"> {
|
|
12
|
+
gap?: StackSpace;
|
|
13
|
+
align?: StackAlign;
|
|
14
|
+
justify?: StackJustify;
|
|
15
|
+
wrap?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Horizontal row with a named gap. */
|
|
18
|
+
declare function Row({ gap, align, justify, wrap, className, ...props }: RowProps): React.JSX.Element;
|
|
19
|
+
export type GridCols = 1 | 2 | 3 | 4 | 5 | 6;
|
|
20
|
+
export type ResponsiveGridCols = GridCols | {
|
|
21
|
+
base?: GridCols;
|
|
22
|
+
sm?: GridCols;
|
|
23
|
+
md?: GridCols;
|
|
24
|
+
lg?: GridCols;
|
|
25
|
+
};
|
|
26
|
+
export interface GridProps extends React.ComponentProps<"div"> {
|
|
27
|
+
/** Equal columns: a number, or per breakpoint `{ base: 1, sm: 2, md: 3 }`. */
|
|
28
|
+
cols?: ResponsiveGridCols;
|
|
29
|
+
gap?: StackSpace;
|
|
30
|
+
/**
|
|
31
|
+
* What sm/md/lg measure. "viewport" (default): the screen, Tailwind's sm/md/lg.
|
|
32
|
+
* "container": the nearest `@container` ancestor (sm >= 28rem, md >= 42rem, lg >= 56rem),
|
|
33
|
+
* for blocks that sit in cards of any width.
|
|
34
|
+
*/
|
|
35
|
+
query?: "viewport" | "container";
|
|
36
|
+
}
|
|
37
|
+
/** Grid of equal columns with named gaps and per-breakpoint column counts. */
|
|
38
|
+
declare function Grid({ cols: c, gap, query, className, ...props }: GridProps): React.JSX.Element;
|
|
39
|
+
export { Grid, Row, Stack };
|