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.
Files changed (83) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/admin/app-nav.d.ts +15 -1
  3. package/dist/components/admin/app-nav.js +28 -11
  4. package/dist/components/admin/avatar-upload.d.ts +32 -0
  5. package/dist/components/admin/avatar-upload.js +121 -0
  6. package/dist/components/admin/category.d.ts +17 -0
  7. package/dist/components/admin/category.js +35 -0
  8. package/dist/components/admin/celebration.d.ts +7 -0
  9. package/dist/components/admin/celebration.js +61 -0
  10. package/dist/components/admin/checklist-editor.d.ts +20 -0
  11. package/dist/components/admin/checklist-editor.js +69 -0
  12. package/dist/components/admin/color-swatch-picker.d.ts +15 -0
  13. package/dist/components/admin/color-swatch-picker.js +68 -0
  14. package/dist/components/admin/comment-thread.d.ts +26 -0
  15. package/dist/components/admin/comment-thread.js +68 -0
  16. package/dist/components/admin/copy-field.d.ts +3 -1
  17. package/dist/components/admin/copy-field.js +10 -5
  18. package/dist/components/admin/date-time-picker.d.ts +20 -0
  19. package/dist/components/admin/date-time-picker.js +97 -0
  20. package/dist/components/admin/detail-sheet.d.ts +18 -0
  21. package/dist/components/admin/detail-sheet.js +80 -0
  22. package/dist/components/admin/markdown-editor.d.ts +12 -0
  23. package/dist/components/admin/markdown-editor.js +46 -0
  24. package/dist/components/admin/page-header.d.ts +3 -1
  25. package/dist/components/admin/page-header.js +17 -9
  26. package/dist/components/admin/page.d.ts +16 -0
  27. package/dist/components/admin/page.js +42 -0
  28. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  29. package/dist/components/admin/prompt-dialog.js +83 -0
  30. package/dist/components/admin/settings.d.ts +4 -2
  31. package/dist/components/admin/settings.js +37 -30
  32. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  33. package/dist/components/admin/time-zone-picker.js +31 -0
  34. package/dist/components/admin/user-chip.d.ts +34 -3
  35. package/dist/components/admin/user-chip.js +66 -7
  36. package/dist/components/admin/user-menu.d.ts +6 -1
  37. package/dist/components/admin/user-menu.js +22 -10
  38. package/dist/components/ai/ask-box.d.ts +14 -2
  39. package/dist/components/ai/ask-box.js +90 -54
  40. package/dist/components/ai/assistant.d.ts +13 -3
  41. package/dist/components/ai/assistant.js +36 -8
  42. package/dist/components/ai/code-block.d.ts +10 -3
  43. package/dist/components/ai/code-block.js +57 -8
  44. package/dist/components/ai/conversation-list.d.ts +13 -3
  45. package/dist/components/ai/conversation-list.js +15 -26
  46. package/dist/components/ai/markdown.d.ts +13 -0
  47. package/dist/components/ai/markdown.js +41 -0
  48. package/dist/components/auth/auth.d.ts +41 -5
  49. package/dist/components/auth/auth.js +167 -159
  50. package/dist/components/data/agenda-row.d.ts +26 -0
  51. package/dist/components/data/agenda-row.js +62 -0
  52. package/dist/components/data/day-strip.d.ts +27 -0
  53. package/dist/components/data/day-strip.js +78 -0
  54. package/dist/components/data/sectioned-list.d.ts +21 -6
  55. package/dist/components/data/sectioned-list.js +44 -21
  56. package/dist/components/data/task-row.d.ts +3 -0
  57. package/dist/components/data/task-row.js +6 -14
  58. package/dist/components/layout/collapse.js +1 -1
  59. package/dist/components/layout/text.d.ts +3 -3
  60. package/dist/components/layout/text.js +6 -1
  61. package/dist/components/ui/avatar.js +1 -1
  62. package/dist/components/ui/button.js +1 -1
  63. package/dist/components/ui/check-circle.js +1 -1
  64. package/dist/components/ui/code-block.js +2 -2
  65. package/dist/components/ui/combobox.js +1 -0
  66. package/dist/components/ui/input-group.js +1 -1
  67. package/dist/components/ui/input.js +1 -1
  68. package/dist/components/ui/kbd.js +1 -1
  69. package/dist/components/ui/password-input.d.ts +8 -0
  70. package/dist/components/ui/password-input.js +32 -0
  71. package/dist/components/ui/sonner.d.ts +2 -2
  72. package/dist/components/ui/sonner.js +1 -1
  73. package/dist/components/ui/toggle.js +1 -1
  74. package/dist/index.d.ts +23 -7
  75. package/dist/index.js +21 -5
  76. package/dist/lib/internal/date.js +8 -0
  77. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  78. package/dist/styles.css +31 -1
  79. package/docs/COMPONENTS.md +302 -28
  80. package/docs/ROADMAP.md +13 -11
  81. package/llms.txt +10 -8
  82. package/package.json +3 -1
  83. 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 { Spinner } from "../ui/spinner.js";
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, disabled }) {
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
- function Header({ title, description }) {
123
- return /* @__PURE__ */ jsxs("div", {
124
- className: "flex flex-col items-center gap-1.5 text-center",
125
- children: [/* @__PURE__ */ jsx("h1", {
126
- className: "text-2xl font-semibold tracking-[-0.02em]",
127
- children: title
128
- }), description && /* @__PURE__ */ jsx("p", {
129
- className: "text-sm text-balance text-muted-foreground",
130
- children: description
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
- function readForm(form) {
135
- return Object.fromEntries([...new FormData(form).entries()].map(([k, v]) => [k, typeof v === "string" ? v : ""]));
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, loading, error, className, ...props }) {
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__ */ jsx("form", {
217
+ return /* @__PURE__ */ jsxs(AuthForm, {
149
218
  "data-slot": "login-form",
150
- className: cn("flex flex-col gap-6", className),
151
- onSubmit: onSubmit ? (e) => {
152
- e.preventDefault();
153
- const v = readForm(e.currentTarget);
154
- onSubmit(emailOnly ? { email: v.email ?? "" } : {
155
- email: v.email ?? "",
156
- password: v.password ?? ""
157
- });
158
- } : void 0,
159
- "aria-busy": loading || void 0,
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: /* @__PURE__ */ jsxs(FieldGroup, { 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__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
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(Input, {
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
- disabled: loading
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", loading, error, className, ...props }) {
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__ */ jsx("form", {
269
+ return /* @__PURE__ */ jsxs(AuthForm, {
226
270
  "data-slot": "signup-form",
227
- className: cn("flex flex-col gap-6", className),
228
- onSubmit: (e) => {
229
- const form = e.currentTarget;
230
- const v = readForm(form);
231
- const confirm = form.elements.namedItem("confirm");
232
- if (confirm) {
233
- confirm.setCustomValidity(v.confirm === v.password ? "" : "Passwords do not match");
234
- if (!confirm.reportValidity()) {
235
- e.preventDefault();
236
- return;
237
- }
238
- }
239
- if (!onSubmit) return;
240
- e.preventDefault();
241
- onSubmit({
242
- ...nameField ? { name: v.name ?? "" } : {},
243
- email: v.email ?? "",
244
- password: v.password ?? ""
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: /* @__PURE__ */ jsxs(FieldGroup, { 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__ */ jsxs(Field, { children: [
279
- /* @__PURE__ */ jsx(FieldLabel, {
280
- htmlFor: `${id}-password`,
281
- children: "Password"
282
- }),
283
- /* @__PURE__ */ jsx(Input, {
284
- id: `${id}-password`,
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
- items: SectionedListItem[];
16
+ /** At the right of the heading, e.g. a small ghost Button "Move to today". */
17
+ action?: React.ReactNode;
18
+ /** Keeps the section shown with this text when it has no open item, e.g. "Nothing planned". */
19
+ empty?: React.ReactNode;
20
+ items: T[];
16
21
  }
17
- export interface SectionedListProps extends Omit<React.ComponentProps<"div">, "onToggle"> {
18
- sections: SectionedListSection[];
22
+ export interface SectionedListProps<T extends SectionedListItem = SectionedListItem> extends Omit<React.ComponentProps<"div">, "onToggle"> {
23
+ sections: SectionedListSection<T>[];
19
24
  /** Called when an item's check changes. Update `done` in your data; the row folds away by itself. */
20
25
  onToggle: (id: string, done: boolean) => void;
26
+ /**
27
+ * Draws a row instead of TaskRow, e.g. an AgendaRow. Call `toggle(done)` from its check so the row lingers and
28
+ * folds away like a TaskRow. Items without `done` (events) always stay.
29
+ */
30
+ renderItem?: (item: T, toggle: (done: boolean) => void) => React.ReactNode;
31
+ /** How long a ticked row stays struck through before it folds away, e.g. about the length of an Undo toast. Default 600. */
32
+ lingerMs?: number;
21
33
  /** Makes item titles buttons, e.g. to open a DetailSheet. */
22
34
  onOpen?: (id: string) => void;
23
35
  /** Shows one add box at the top. Called with the trimmed text on Enter. */
24
36
  onAdd?: (text: string) => void;
25
37
  /** Add box placeholder and label. Default "Add a to-do". */
26
38
  addPlaceholder?: string;
39
+ /** Chips at the right of the add box while it has text, e.g. the date or person parsed from it. Like AskBox `chips`. */
40
+ addChips?: (text: string) => React.ReactNode;
27
41
  /** Shown when no item is open, e.g. <Empty /> or a line of text. */
28
42
  empty?: React.ReactNode;
29
43
  /** Start with the done items shown. Default false. */
@@ -31,7 +45,8 @@ export interface SectionedListProps extends Omit<React.ComponentProps<"div">, "o
31
45
  }
32
46
  /**
33
47
  * A to-do list: one add box, sections with headings, done items folded into a "Show N done" toggle, an empty
34
- * state. Rows fold in and out with Collapse, so nothing jumps.
48
+ * state. Rows fold in and out with Collapse, so nothing jumps. `renderItem` draws other rows (AgendaRow), so a week
49
+ * agenda is one SectionedList with a section per day.
35
50
  */
36
- declare function SectionedList({ sections, onToggle, onOpen, onAdd, addPlaceholder, empty, defaultShowDone, className, ...props }: SectionedListProps): React.JSX.Element;
51
+ declare function SectionedList<T extends SectionedListItem = SectionedListItem>({ sections, onToggle, renderItem, lingerMs, onOpen, onAdd, addPlaceholder, addChips, empty, defaultShowDone, className, ...props }: SectionedListProps<T>): React.JSX.Element;
37
52
  export { SectionedList };