harebear-ui 0.8.1 → 0.10.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 +15 -1
- package/dist/components/admin/app-nav.js +28 -11
- 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/checklist-editor.d.ts +20 -0
- package/dist/components/admin/checklist-editor.js +69 -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/comment-thread.d.ts +26 -0
- package/dist/components/admin/comment-thread.js +68 -0
- package/dist/components/admin/copy-field.d.ts +3 -1
- package/dist/components/admin/copy-field.js +10 -5
- 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 +12 -0
- package/dist/components/admin/markdown-editor.js +46 -0
- package/dist/components/admin/page-header.d.ts +3 -1
- package/dist/components/admin/page-header.js +17 -9
- package/dist/components/admin/page.d.ts +16 -0
- package/dist/components/admin/page.js +42 -0
- package/dist/components/admin/prompt-dialog.d.ts +19 -0
- package/dist/components/admin/prompt-dialog.js +83 -0
- package/dist/components/admin/settings.d.ts +4 -2
- package/dist/components/admin/settings.js +37 -30
- package/dist/components/admin/time-zone-picker.d.ts +12 -0
- package/dist/components/admin/time-zone-picker.js +31 -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 +6 -1
- package/dist/components/admin/user-menu.js +22 -10
- 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 +36 -8
- package/dist/components/ai/code-block.d.ts +10 -3
- package/dist/components/ai/code-block.js +57 -8
- package/dist/components/ai/conversation-list.d.ts +13 -3
- package/dist/components/ai/conversation-list.js +15 -26
- package/dist/components/ai/markdown.d.ts +13 -0
- package/dist/components/ai/markdown.js +41 -0
- package/dist/components/auth/auth.d.ts +41 -5
- package/dist/components/auth/auth.js +167 -159
- 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 +27 -0
- package/dist/components/data/day-strip.js +78 -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/layout/collapse.js +1 -1
- package/dist/components/layout/text.d.ts +3 -3
- package/dist/components/layout/text.js +6 -1
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/check-circle.js +1 -1
- package/dist/components/ui/code-block.js +2 -2
- package/dist/components/ui/combobox.js +1 -0
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input.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/components/ui/sonner.d.ts +2 -2
- package/dist/components/ui/sonner.js +1 -1
- package/dist/components/ui/toggle.js +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.js +21 -5
- package/dist/lib/internal/date.js +8 -0
- package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
- package/dist/styles.css +31 -1
- package/docs/COMPONENTS.md +302 -28
- package/docs/ROADMAP.md +13 -11
- package/llms.txt +10 -8
- package/package.json +3 -1
- package/skills/harebear-ui/SKILL.md +5 -3
|
@@ -3,10 +3,10 @@ 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 {
|
|
6
|
+
import { PasswordInput } from "../ui/password-input.js";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
8
|
import * as React from "react";
|
|
9
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
// src/components/auth/auth.tsx
|
|
11
11
|
/** Full-page wrapper for sign-in and sign-up screens. Put LoginForm or SignupForm inside. */
|
|
12
12
|
function AuthLayout({ variant = "centered", brand, image, footer, className, children }) {
|
|
@@ -70,7 +70,7 @@ function AuthLayout({ variant = "centered", brand, image, footer, className, chi
|
|
|
70
70
|
})
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
-
function ProviderButtons({ providers, verb, onProviderClick
|
|
73
|
+
function ProviderButtons({ providers, verb, onProviderClick }) {
|
|
74
74
|
const compact = providers.length >= 3 && providers.every((p) => p.icon);
|
|
75
75
|
return /* @__PURE__ */ jsx(Field, {
|
|
76
76
|
className: cn(compact && "grid gap-3"),
|
|
@@ -78,7 +78,6 @@ function ProviderButtons({ providers, verb, onProviderClick, disabled }) {
|
|
|
78
78
|
children: providers.map((p) => /* @__PURE__ */ jsxs(Button, {
|
|
79
79
|
type: "button",
|
|
80
80
|
variant: "outline",
|
|
81
|
-
disabled,
|
|
82
81
|
onClick: () => onProviderClick?.(p.id),
|
|
83
82
|
"aria-label": compact ? `${verb} with ${p.label}` : void 0,
|
|
84
83
|
children: [p.icon, !compact && `${verb} with ${p.label}`]
|
|
@@ -119,64 +118,127 @@ function AppleIcon(props) {
|
|
|
119
118
|
...props
|
|
120
119
|
});
|
|
121
120
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
121
|
+
/** The shell LoginForm, SignupForm and NewPasswordForm share: heading, your fields, error, submit button. */
|
|
122
|
+
function AuthForm({ title, description, children, error, submitLabel, loading, onSubmit, footer, className, ...props }) {
|
|
123
|
+
return /* @__PURE__ */ jsx("form", {
|
|
124
|
+
"data-slot": "auth-form",
|
|
125
|
+
className: cn("flex flex-col gap-6", className),
|
|
126
|
+
onSubmit: (e) => {
|
|
127
|
+
if (!onSubmit) return;
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
onSubmit(new FormData(e.currentTarget));
|
|
130
|
+
},
|
|
131
|
+
"aria-busy": loading || void 0,
|
|
132
|
+
...props,
|
|
133
|
+
children: /* @__PURE__ */ jsx("fieldset", {
|
|
134
|
+
disabled: loading,
|
|
135
|
+
className: "contents",
|
|
136
|
+
children: /* @__PURE__ */ jsxs(FieldGroup, { children: [
|
|
137
|
+
/* @__PURE__ */ jsxs("div", {
|
|
138
|
+
className: "flex flex-col items-center gap-1.5 text-center",
|
|
139
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
140
|
+
className: "text-2xl font-semibold tracking-[-0.02em]",
|
|
141
|
+
children: title
|
|
142
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
143
|
+
className: "text-sm text-balance text-muted-foreground",
|
|
144
|
+
children: description
|
|
145
|
+
})]
|
|
146
|
+
}),
|
|
147
|
+
children,
|
|
148
|
+
error && /* @__PURE__ */ jsx(FieldError, { children: error }),
|
|
149
|
+
/* @__PURE__ */ jsx(Field, { children: /* @__PURE__ */ jsx(Button, {
|
|
150
|
+
type: "submit",
|
|
151
|
+
loading,
|
|
152
|
+
children: submitLabel
|
|
153
|
+
}) }),
|
|
154
|
+
footer
|
|
155
|
+
] })
|
|
156
|
+
})
|
|
132
157
|
});
|
|
133
158
|
}
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
const text = (data, name) => {
|
|
160
|
+
const v = data.get(name);
|
|
161
|
+
return typeof v === "string" ? v : "";
|
|
162
|
+
};
|
|
163
|
+
function EmailField({ id }) {
|
|
164
|
+
return /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
165
|
+
htmlFor: `${id}-email`,
|
|
166
|
+
children: "Email"
|
|
167
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
168
|
+
id: `${id}-email`,
|
|
169
|
+
name: "email",
|
|
170
|
+
type: "email",
|
|
171
|
+
autoComplete: "email",
|
|
172
|
+
placeholder: "you@example.com",
|
|
173
|
+
required: true
|
|
174
|
+
})] });
|
|
175
|
+
}
|
|
176
|
+
/** "password" and optional "confirm" fields; a mismatch makes "confirm" invalid so the browser blocks the submit. */
|
|
177
|
+
function NewPasswordFields({ id, label, confirm, minLength, hint }) {
|
|
178
|
+
const check = (form) => {
|
|
179
|
+
const password = form?.elements.namedItem("password");
|
|
180
|
+
const again = form?.elements.namedItem("confirm");
|
|
181
|
+
again?.setCustomValidity(again.value && again.value !== password?.value ? "Passwords do not match" : "");
|
|
182
|
+
};
|
|
183
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Field, { children: [
|
|
184
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
185
|
+
htmlFor: `${id}-password`,
|
|
186
|
+
children: label
|
|
187
|
+
}),
|
|
188
|
+
/* @__PURE__ */ jsx(PasswordInput, {
|
|
189
|
+
id: `${id}-password`,
|
|
190
|
+
name: "password",
|
|
191
|
+
autoComplete: "new-password",
|
|
192
|
+
minLength,
|
|
193
|
+
required: true,
|
|
194
|
+
onInput: (e) => check(e.currentTarget.form)
|
|
195
|
+
}),
|
|
196
|
+
/* @__PURE__ */ jsx(FieldDescription, { children: hint ?? `At least ${minLength} characters.` })
|
|
197
|
+
] }), confirm && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
198
|
+
htmlFor: `${id}-confirm`,
|
|
199
|
+
children: "Confirm password"
|
|
200
|
+
}), /* @__PURE__ */ jsx(PasswordInput, {
|
|
201
|
+
id: `${id}-confirm`,
|
|
202
|
+
name: "confirm",
|
|
203
|
+
autoComplete: "new-password",
|
|
204
|
+
required: true,
|
|
205
|
+
onInput: (e) => check(e.currentTarget.form)
|
|
206
|
+
})] })] });
|
|
136
207
|
}
|
|
137
208
|
/** Email + password sign-in form with optional SSO buttons. Pair with AuthLayout. */
|
|
138
|
-
function LoginForm({ title = "Welcome back", description, onSubmit, mode = "password", providers = [], onProviderClick, providersFirst, forgotPasswordHref, signupHref, submitLabel,
|
|
209
|
+
function LoginForm({ title = "Welcome back", description, onSubmit, mode = "password", providers = [], onProviderClick, providersFirst, forgotPasswordHref, signupHref, submitLabel, ...props }) {
|
|
139
210
|
const id = React.useId();
|
|
140
211
|
const emailOnly = mode === "email-only";
|
|
141
|
-
const desc = description ?? (emailOnly ? "Enter your email and we'll send you a sign-in link" : "Sign in to your account");
|
|
142
212
|
const sso = providers.length > 0 && /* @__PURE__ */ jsx(ProviderButtons, {
|
|
143
213
|
providers,
|
|
144
214
|
verb: "Continue",
|
|
145
|
-
onProviderClick
|
|
146
|
-
disabled: loading
|
|
215
|
+
onProviderClick
|
|
147
216
|
});
|
|
148
|
-
return /* @__PURE__ */
|
|
217
|
+
return /* @__PURE__ */ jsxs(AuthForm, {
|
|
149
218
|
"data-slot": "login-form",
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
219
|
+
title,
|
|
220
|
+
description: description ?? (emailOnly ? "Enter your email and we'll send you a sign-in link" : "Sign in to your account"),
|
|
221
|
+
submitLabel: submitLabel ?? (emailOnly ? "Send sign-in link" : "Sign in"),
|
|
222
|
+
onSubmit: onSubmit && ((data) => onSubmit(emailOnly ? { email: text(data, "email") } : {
|
|
223
|
+
email: text(data, "email"),
|
|
224
|
+
password: text(data, "password")
|
|
225
|
+
})),
|
|
226
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
227
|
+
!providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
|
|
228
|
+
!providersFirst && sso,
|
|
229
|
+
signupHref && /* @__PURE__ */ jsxs(FieldDescription, {
|
|
230
|
+
className: "text-center",
|
|
231
|
+
children: ["Don't have an account? ", /* @__PURE__ */ jsx("a", {
|
|
232
|
+
href: signupHref,
|
|
233
|
+
children: "Sign up"
|
|
234
|
+
})]
|
|
235
|
+
})
|
|
236
|
+
] }),
|
|
160
237
|
...props,
|
|
161
|
-
children:
|
|
162
|
-
/* @__PURE__ */ jsx(Header, {
|
|
163
|
-
title,
|
|
164
|
-
description: desc
|
|
165
|
-
}),
|
|
238
|
+
children: [
|
|
166
239
|
providersFirst && sso,
|
|
167
240
|
providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with email" }),
|
|
168
|
-
/* @__PURE__ */
|
|
169
|
-
htmlFor: `${id}-email`,
|
|
170
|
-
children: "Email"
|
|
171
|
-
}), /* @__PURE__ */ jsx(Input, {
|
|
172
|
-
id: `${id}-email`,
|
|
173
|
-
name: "email",
|
|
174
|
-
type: "email",
|
|
175
|
-
autoComplete: "email",
|
|
176
|
-
placeholder: "you@example.com",
|
|
177
|
-
required: true,
|
|
178
|
-
disabled: loading
|
|
179
|
-
})] }),
|
|
241
|
+
/* @__PURE__ */ jsx(EmailField, { id }),
|
|
180
242
|
!emailOnly && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsxs("div", {
|
|
181
243
|
className: "flex items-center",
|
|
182
244
|
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
@@ -184,75 +246,52 @@ function LoginForm({ title = "Welcome back", description, onSubmit, mode = "pass
|
|
|
184
246
|
children: "Password"
|
|
185
247
|
}), forgotPasswordHref && /* @__PURE__ */ jsx("a", {
|
|
186
248
|
href: forgotPasswordHref,
|
|
187
|
-
className: "ms-auto text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline",
|
|
249
|
+
className: "ms-auto rounded-sm text-sm text-muted-foreground underline-offset-4 outline-none hover:text-foreground hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
188
250
|
children: "Forgot password?"
|
|
189
251
|
})]
|
|
190
|
-
}), /* @__PURE__ */ jsx(
|
|
252
|
+
}), /* @__PURE__ */ jsx(PasswordInput, {
|
|
191
253
|
id: `${id}-password`,
|
|
192
254
|
name: "password",
|
|
193
|
-
type: "password",
|
|
194
255
|
autoComplete: "current-password",
|
|
195
|
-
required: true
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
error && /* @__PURE__ */ jsx(FieldError, { children: error }),
|
|
199
|
-
/* @__PURE__ */ jsx(Field, { children: /* @__PURE__ */ jsxs(Button, {
|
|
200
|
-
type: "submit",
|
|
201
|
-
disabled: loading,
|
|
202
|
-
children: [loading && /* @__PURE__ */ jsx(Spinner, {}), submitLabel ?? (emailOnly ? "Send sign-in link" : "Sign in")]
|
|
203
|
-
}) }),
|
|
204
|
-
!providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
|
|
205
|
-
!providersFirst && sso,
|
|
206
|
-
signupHref && /* @__PURE__ */ jsxs(FieldDescription, {
|
|
207
|
-
className: "text-center",
|
|
208
|
-
children: ["Don't have an account? ", /* @__PURE__ */ jsx("a", {
|
|
209
|
-
href: signupHref,
|
|
210
|
-
children: "Sign up"
|
|
211
|
-
})]
|
|
212
|
-
})
|
|
213
|
-
] })
|
|
256
|
+
required: true
|
|
257
|
+
})] })
|
|
258
|
+
]
|
|
214
259
|
});
|
|
215
260
|
}
|
|
216
261
|
/** Account creation form: name, email, password (+ confirm), optional SSO. Pair with AuthLayout. */
|
|
217
|
-
function SignupForm({ title = "Create your account", description = "Enter your details to get started", onSubmit, nameField = true, confirmPassword = true, passwordHint, minPasswordLength = 8, providers = [], onProviderClick, providersFirst, loginHref, submitLabel = "Create account",
|
|
262
|
+
function SignupForm({ title = "Create your account", description = "Enter your details to get started", onSubmit, children, nameField = true, confirmPassword = true, passwordHint, minPasswordLength = 8, providers = [], onProviderClick, providersFirst, loginHref, submitLabel = "Create account", ...props }) {
|
|
218
263
|
const id = React.useId();
|
|
219
264
|
const sso = providers.length > 0 && /* @__PURE__ */ jsx(ProviderButtons, {
|
|
220
265
|
providers,
|
|
221
266
|
verb: "Sign up",
|
|
222
|
-
onProviderClick
|
|
223
|
-
disabled: loading
|
|
267
|
+
onProviderClick
|
|
224
268
|
});
|
|
225
|
-
return /* @__PURE__ */
|
|
269
|
+
return /* @__PURE__ */ jsxs(AuthForm, {
|
|
226
270
|
"data-slot": "signup-form",
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
"aria-busy": loading || void 0,
|
|
271
|
+
title,
|
|
272
|
+
description,
|
|
273
|
+
submitLabel,
|
|
274
|
+
onSubmit: onSubmit && ((data) => onSubmit({
|
|
275
|
+
...nameField ? { name: text(data, "name") } : {},
|
|
276
|
+
email: text(data, "email"),
|
|
277
|
+
password: text(data, "password")
|
|
278
|
+
}, data)),
|
|
279
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
280
|
+
!providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
|
|
281
|
+
!providersFirst && sso,
|
|
282
|
+
loginHref && /* @__PURE__ */ jsxs(FieldDescription, {
|
|
283
|
+
className: "text-center",
|
|
284
|
+
children: ["Already have an account? ", /* @__PURE__ */ jsx("a", {
|
|
285
|
+
href: loginHref,
|
|
286
|
+
children: "Sign in"
|
|
287
|
+
})]
|
|
288
|
+
})
|
|
289
|
+
] }),
|
|
248
290
|
...props,
|
|
249
|
-
children:
|
|
250
|
-
/* @__PURE__ */ jsx(Header, {
|
|
251
|
-
title,
|
|
252
|
-
description
|
|
253
|
-
}),
|
|
291
|
+
children: [
|
|
254
292
|
providersFirst && sso,
|
|
255
293
|
providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or sign up with email" }),
|
|
294
|
+
children,
|
|
256
295
|
nameField && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
257
296
|
htmlFor: `${id}-name`,
|
|
258
297
|
children: "Full name"
|
|
@@ -260,66 +299,35 @@ function SignupForm({ title = "Create your account", description = "Enter your d
|
|
|
260
299
|
id: `${id}-name`,
|
|
261
300
|
name: "name",
|
|
262
301
|
autoComplete: "name",
|
|
263
|
-
required: true
|
|
264
|
-
disabled: loading
|
|
265
|
-
})] }),
|
|
266
|
-
/* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
267
|
-
htmlFor: `${id}-email`,
|
|
268
|
-
children: "Email"
|
|
269
|
-
}), /* @__PURE__ */ jsx(Input, {
|
|
270
|
-
id: `${id}-email`,
|
|
271
|
-
name: "email",
|
|
272
|
-
type: "email",
|
|
273
|
-
autoComplete: "email",
|
|
274
|
-
placeholder: "you@example.com",
|
|
275
|
-
required: true,
|
|
276
|
-
disabled: loading
|
|
302
|
+
required: true
|
|
277
303
|
})] }),
|
|
278
|
-
/* @__PURE__ */
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
name: "password",
|
|
286
|
-
type: "password",
|
|
287
|
-
autoComplete: "new-password",
|
|
288
|
-
onInput: (e) => (e.currentTarget.form?.elements.namedItem("confirm"))?.setCustomValidity(""),
|
|
289
|
-
minLength: minPasswordLength,
|
|
290
|
-
required: true,
|
|
291
|
-
disabled: loading
|
|
292
|
-
}),
|
|
293
|
-
/* @__PURE__ */ jsx(FieldDescription, { children: passwordHint ?? `At least ${minPasswordLength} characters.` })
|
|
294
|
-
] }),
|
|
295
|
-
confirmPassword && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
296
|
-
htmlFor: `${id}-confirm`,
|
|
297
|
-
children: "Confirm password"
|
|
298
|
-
}), /* @__PURE__ */ jsx(Input, {
|
|
299
|
-
id: `${id}-confirm`,
|
|
300
|
-
name: "confirm",
|
|
301
|
-
type: "password",
|
|
302
|
-
autoComplete: "new-password",
|
|
303
|
-
required: true,
|
|
304
|
-
disabled: loading,
|
|
305
|
-
onInput: (e) => e.currentTarget.setCustomValidity("")
|
|
306
|
-
})] }),
|
|
307
|
-
error && /* @__PURE__ */ jsx(FieldError, { children: error }),
|
|
308
|
-
/* @__PURE__ */ jsx(Field, { children: /* @__PURE__ */ jsxs(Button, {
|
|
309
|
-
type: "submit",
|
|
310
|
-
disabled: loading,
|
|
311
|
-
children: [loading && /* @__PURE__ */ jsx(Spinner, {}), submitLabel]
|
|
312
|
-
}) }),
|
|
313
|
-
!providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
|
|
314
|
-
!providersFirst && sso,
|
|
315
|
-
loginHref && /* @__PURE__ */ jsxs(FieldDescription, {
|
|
316
|
-
className: "text-center",
|
|
317
|
-
children: ["Already have an account? ", /* @__PURE__ */ jsx("a", {
|
|
318
|
-
href: loginHref,
|
|
319
|
-
children: "Sign in"
|
|
320
|
-
})]
|
|
304
|
+
/* @__PURE__ */ jsx(EmailField, { id }),
|
|
305
|
+
/* @__PURE__ */ jsx(NewPasswordFields, {
|
|
306
|
+
id,
|
|
307
|
+
label: "Password",
|
|
308
|
+
confirm: confirmPassword,
|
|
309
|
+
minLength: minPasswordLength,
|
|
310
|
+
hint: passwordHint
|
|
321
311
|
})
|
|
322
|
-
]
|
|
312
|
+
]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** Password reset or first password: new password + confirm. Pair with AuthLayout. */
|
|
316
|
+
function NewPasswordForm({ title = "Choose a new password", onSubmit, minPasswordLength = 8, passwordHint, submitLabel = "Save password", ...props }) {
|
|
317
|
+
const id = React.useId();
|
|
318
|
+
return /* @__PURE__ */ jsx(AuthForm, {
|
|
319
|
+
"data-slot": "new-password-form",
|
|
320
|
+
title,
|
|
321
|
+
submitLabel,
|
|
322
|
+
onSubmit: onSubmit && ((data) => onSubmit({ password: text(data, "password") })),
|
|
323
|
+
...props,
|
|
324
|
+
children: /* @__PURE__ */ jsx(NewPasswordFields, {
|
|
325
|
+
id,
|
|
326
|
+
label: "New password",
|
|
327
|
+
confirm: true,
|
|
328
|
+
minLength: minPasswordLength,
|
|
329
|
+
hint: passwordHint
|
|
330
|
+
})
|
|
323
331
|
});
|
|
324
332
|
}
|
|
325
|
-
export { AppleIcon, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, SignupForm };
|
|
333
|
+
export { AppleIcon, AuthForm, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, NewPasswordForm, SignupForm };
|
|
@@ -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,27 @@
|
|
|
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
|
+
* Sticks to the top of the page (or `top-*` you pass) while it scrolls, and lands jumped-to days below the strip
|
|
18
|
+
* instead of under it. Works in any scroll container, the document included.
|
|
19
|
+
*/
|
|
20
|
+
sticky?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A row of day buttons (weekday, date, a dot when there are plans) that jump to the element with the day's id.
|
|
24
|
+
* Scrolls inside itself when the days do not fit. Smooth scrolling is off under prefers-reduced-motion.
|
|
25
|
+
*/
|
|
26
|
+
declare function DayStrip({ days, value, onValueChange, sticky, className, "aria-label": ariaLabel, ...props }: DayStripProps): React.JSX.Element;
|
|
27
|
+
export { DayStrip };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { parseLocalDate } from "../../lib/internal/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, sticky = false, 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
|
+
const strip = sticky ? e.currentTarget.closest("[data-slot=day-strip]") : null;
|
|
17
|
+
if (strip) {
|
|
18
|
+
const top = Number.parseFloat(getComputedStyle(strip).top) || 0;
|
|
19
|
+
el.style.scrollMarginTop = `${strip.offsetHeight + top + 8}px`;
|
|
20
|
+
}
|
|
21
|
+
el.scrollIntoView?.({
|
|
22
|
+
behavior: reduce ? "auto" : "smooth",
|
|
23
|
+
block: "start"
|
|
24
|
+
});
|
|
25
|
+
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "-1");
|
|
26
|
+
el.focus({ preventScroll: true });
|
|
27
|
+
}
|
|
28
|
+
onValueChange?.(id);
|
|
29
|
+
};
|
|
30
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
31
|
+
"data-slot": "day-strip",
|
|
32
|
+
"aria-label": ariaLabel ?? "Jump to a day",
|
|
33
|
+
className: cn("min-w-0 overflow-x-auto", sticky && "sticky top-0 z-10 bg-background", className),
|
|
34
|
+
...props,
|
|
35
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
36
|
+
className: "flex gap-2 p-1",
|
|
37
|
+
children: days.map((day) => {
|
|
38
|
+
const selected = day.id === value;
|
|
39
|
+
const date = parseLocalDate(day.date);
|
|
40
|
+
const label = date.toLocaleDateString(void 0, {
|
|
41
|
+
weekday: "long",
|
|
42
|
+
month: "long",
|
|
43
|
+
day: "numeric"
|
|
44
|
+
});
|
|
45
|
+
return /* @__PURE__ */ jsx("li", {
|
|
46
|
+
className: "flex shrink-0 grow basis-18",
|
|
47
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
48
|
+
href: `#${day.id}`,
|
|
49
|
+
"aria-current": selected ? "true" : void 0,
|
|
50
|
+
onClick: (e) => jump(e, day.id),
|
|
51
|
+
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-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none", selected ? "bg-primary text-primary-foreground" : "bg-card text-foreground shadow-card hover:bg-muted"),
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", {
|
|
54
|
+
className: "sr-only",
|
|
55
|
+
children: day.hasPlans ? `${label}, has plans` : label
|
|
56
|
+
}),
|
|
57
|
+
/* @__PURE__ */ jsx("span", {
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
className: cn("text-xs font-medium", selected ? "text-primary-foreground" : "text-muted-foreground"),
|
|
60
|
+
children: date.toLocaleDateString(void 0, { weekday: "short" })
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ jsx("span", {
|
|
63
|
+
"aria-hidden": "true",
|
|
64
|
+
className: "text-[22px] leading-tight font-semibold tracking-tight tabular-nums",
|
|
65
|
+
children: date.getDate()
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ jsx("span", {
|
|
68
|
+
"aria-hidden": "true",
|
|
69
|
+
className: cn("size-1.5 rounded-full", !day.hasPlans && "bg-transparent", day.hasPlans && (selected ? "bg-primary-foreground" : "bg-primary"))
|
|
70
|
+
})
|
|
71
|
+
]
|
|
72
|
+
})
|
|
73
|
+
}, day.id);
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
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 };
|