harebear-ui 0.9.0 → 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 (53) hide show
  1. package/dist/components/admin/app-nav.d.ts +11 -1
  2. package/dist/components/admin/app-nav.js +26 -9
  3. package/dist/components/admin/checklist-editor.d.ts +20 -0
  4. package/dist/components/admin/checklist-editor.js +69 -0
  5. package/dist/components/admin/comment-thread.d.ts +26 -0
  6. package/dist/components/admin/comment-thread.js +68 -0
  7. package/dist/components/admin/copy-field.d.ts +3 -1
  8. package/dist/components/admin/copy-field.js +10 -5
  9. package/dist/components/admin/date-time-picker.js +1 -1
  10. package/dist/components/admin/markdown-editor.d.ts +2 -5
  11. package/dist/components/admin/markdown-editor.js +13 -9
  12. package/dist/components/admin/page-header.d.ts +3 -1
  13. package/dist/components/admin/page-header.js +17 -9
  14. package/dist/components/admin/page.d.ts +16 -0
  15. package/dist/components/admin/page.js +42 -0
  16. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  17. package/dist/components/admin/prompt-dialog.js +83 -0
  18. package/dist/components/admin/settings.d.ts +4 -2
  19. package/dist/components/admin/settings.js +37 -30
  20. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  21. package/dist/components/admin/time-zone-picker.js +31 -0
  22. package/dist/components/admin/user-menu.d.ts +3 -1
  23. package/dist/components/admin/user-menu.js +21 -10
  24. package/dist/components/ai/assistant.js +1 -1
  25. package/dist/components/ai/code-block.js +3 -3
  26. package/dist/components/ai/markdown.d.ts +13 -0
  27. package/dist/components/ai/markdown.js +41 -0
  28. package/dist/components/auth/auth.d.ts +41 -5
  29. package/dist/components/auth/auth.js +165 -155
  30. package/dist/components/data/day-strip.d.ts +6 -1
  31. package/dist/components/data/day-strip.js +15 -5
  32. package/dist/components/layout/collapse.js +1 -1
  33. package/dist/components/layout/text.d.ts +3 -3
  34. package/dist/components/layout/text.js +6 -1
  35. package/dist/components/ui/button.js +1 -1
  36. package/dist/components/ui/check-circle.js +1 -1
  37. package/dist/components/ui/code-block.js +2 -2
  38. package/dist/components/ui/combobox.js +1 -0
  39. package/dist/components/ui/input-group.js +1 -1
  40. package/dist/components/ui/input.js +1 -1
  41. package/dist/components/ui/sonner.d.ts +2 -2
  42. package/dist/components/ui/sonner.js +1 -1
  43. package/dist/components/ui/toggle.js +1 -1
  44. package/dist/index.d.ts +9 -3
  45. package/dist/index.js +9 -3
  46. package/dist/lib/{date.js → internal/date.js} +1 -1
  47. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  48. package/dist/styles.css +6 -1
  49. package/docs/COMPONENTS.md +150 -26
  50. package/docs/ROADMAP.md +1 -0
  51. package/llms.txt +7 -6
  52. package/package.json +3 -1
  53. package/skills/harebear-ui/SKILL.md +5 -4
@@ -4,10 +4,9 @@ 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
6
  import { PasswordInput } from "../ui/password-input.js";
7
- import { Spinner } from "../ui/spinner.js";
8
7
  import { cn } from "../../lib/utils.js";
9
8
  import * as React from "react";
10
- import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
10
  // src/components/auth/auth.tsx
12
11
  /** Full-page wrapper for sign-in and sign-up screens. Put LoginForm or SignupForm inside. */
13
12
  function AuthLayout({ variant = "centered", brand, image, footer, className, children }) {
@@ -71,7 +70,7 @@ function AuthLayout({ variant = "centered", brand, image, footer, className, chi
71
70
  })
72
71
  });
73
72
  }
74
- function ProviderButtons({ providers, verb, onProviderClick, disabled }) {
73
+ function ProviderButtons({ providers, verb, onProviderClick }) {
75
74
  const compact = providers.length >= 3 && providers.every((p) => p.icon);
76
75
  return /* @__PURE__ */ jsx(Field, {
77
76
  className: cn(compact && "grid gap-3"),
@@ -79,7 +78,6 @@ function ProviderButtons({ providers, verb, onProviderClick, disabled }) {
79
78
  children: providers.map((p) => /* @__PURE__ */ jsxs(Button, {
80
79
  type: "button",
81
80
  variant: "outline",
82
- disabled,
83
81
  onClick: () => onProviderClick?.(p.id),
84
82
  "aria-label": compact ? `${verb} with ${p.label}` : void 0,
85
83
  children: [p.icon, !compact && `${verb} with ${p.label}`]
@@ -120,64 +118,127 @@ function AppleIcon(props) {
120
118
  ...props
121
119
  });
122
120
  }
123
- function Header({ title, description }) {
124
- return /* @__PURE__ */ jsxs("div", {
125
- className: "flex flex-col items-center gap-1.5 text-center",
126
- children: [/* @__PURE__ */ jsx("h1", {
127
- className: "text-2xl font-semibold tracking-[-0.02em]",
128
- children: title
129
- }), description && /* @__PURE__ */ jsx("p", {
130
- className: "text-sm text-balance text-muted-foreground",
131
- children: description
132
- })]
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
+ })
133
157
  });
134
158
  }
135
- function readForm(form) {
136
- 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
+ })] })] });
137
207
  }
138
208
  /** Email + password sign-in form with optional SSO buttons. Pair with AuthLayout. */
139
- 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 }) {
140
210
  const id = React.useId();
141
211
  const emailOnly = mode === "email-only";
142
- const desc = description ?? (emailOnly ? "Enter your email and we'll send you a sign-in link" : "Sign in to your account");
143
212
  const sso = providers.length > 0 && /* @__PURE__ */ jsx(ProviderButtons, {
144
213
  providers,
145
214
  verb: "Continue",
146
- onProviderClick,
147
- disabled: loading
215
+ onProviderClick
148
216
  });
149
- return /* @__PURE__ */ jsx("form", {
217
+ return /* @__PURE__ */ jsxs(AuthForm, {
150
218
  "data-slot": "login-form",
151
- className: cn("flex flex-col gap-6", className),
152
- onSubmit: onSubmit ? (e) => {
153
- e.preventDefault();
154
- const v = readForm(e.currentTarget);
155
- onSubmit(emailOnly ? { email: v.email ?? "" } : {
156
- email: v.email ?? "",
157
- password: v.password ?? ""
158
- });
159
- } : void 0,
160
- "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
+ ] }),
161
237
  ...props,
162
- children: /* @__PURE__ */ jsxs(FieldGroup, { children: [
163
- /* @__PURE__ */ jsx(Header, {
164
- title,
165
- description: desc
166
- }),
238
+ children: [
167
239
  providersFirst && sso,
168
240
  providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with email" }),
169
- /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
170
- htmlFor: `${id}-email`,
171
- children: "Email"
172
- }), /* @__PURE__ */ jsx(Input, {
173
- id: `${id}-email`,
174
- name: "email",
175
- type: "email",
176
- autoComplete: "email",
177
- placeholder: "you@example.com",
178
- required: true,
179
- disabled: loading
180
- })] }),
241
+ /* @__PURE__ */ jsx(EmailField, { id }),
181
242
  !emailOnly && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsxs("div", {
182
243
  className: "flex items-center",
183
244
  children: [/* @__PURE__ */ jsx(FieldLabel, {
@@ -185,74 +246,52 @@ function LoginForm({ title = "Welcome back", description, onSubmit, mode = "pass
185
246
  children: "Password"
186
247
  }), forgotPasswordHref && /* @__PURE__ */ jsx("a", {
187
248
  href: forgotPasswordHref,
188
- 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",
189
250
  children: "Forgot password?"
190
251
  })]
191
252
  }), /* @__PURE__ */ jsx(PasswordInput, {
192
253
  id: `${id}-password`,
193
254
  name: "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,64 +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(PasswordInput, {
284
- id: `${id}-password`,
285
- name: "password",
286
- autoComplete: "new-password",
287
- onInput: (e) => (e.currentTarget.form?.elements.namedItem("confirm"))?.setCustomValidity(""),
288
- minLength: minPasswordLength,
289
- required: true,
290
- disabled: loading
291
- }),
292
- /* @__PURE__ */ jsx(FieldDescription, { children: passwordHint ?? `At least ${minPasswordLength} characters.` })
293
- ] }),
294
- confirmPassword && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
295
- htmlFor: `${id}-confirm`,
296
- children: "Confirm password"
297
- }), /* @__PURE__ */ jsx(PasswordInput, {
298
- id: `${id}-confirm`,
299
- name: "confirm",
300
- autoComplete: "new-password",
301
- required: true,
302
- disabled: loading,
303
- onInput: (e) => e.currentTarget.setCustomValidity("")
304
- })] }),
305
- error && /* @__PURE__ */ jsx(FieldError, { children: error }),
306
- /* @__PURE__ */ jsx(Field, { children: /* @__PURE__ */ jsxs(Button, {
307
- type: "submit",
308
- disabled: loading,
309
- children: [loading && /* @__PURE__ */ jsx(Spinner, {}), submitLabel]
310
- }) }),
311
- !providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
312
- !providersFirst && sso,
313
- loginHref && /* @__PURE__ */ jsxs(FieldDescription, {
314
- className: "text-center",
315
- children: ["Already have an account? ", /* @__PURE__ */ jsx("a", {
316
- href: loginHref,
317
- children: "Sign in"
318
- })]
304
+ /* @__PURE__ */ jsx(EmailField, { id }),
305
+ /* @__PURE__ */ jsx(NewPasswordFields, {
306
+ id,
307
+ label: "Password",
308
+ confirm: confirmPassword,
309
+ minLength: minPasswordLength,
310
+ hint: passwordHint
319
311
  })
320
- ] })
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
+ })
321
331
  });
322
332
  }
323
- export { AppleIcon, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, SignupForm };
333
+ export { AppleIcon, AuthForm, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, NewPasswordForm, SignupForm };
@@ -13,10 +13,15 @@ export interface DayStripProps extends React.ComponentProps<"nav"> {
13
13
  value?: string;
14
14
  /** Called with the day's id after it scrolls that element into view. */
15
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;
16
21
  }
17
22
  /**
18
23
  * A row of day buttons (weekday, date, a dot when there are plans) that jump to the element with the day's id.
19
24
  * Scrolls inside itself when the days do not fit. Smooth scrolling is off under prefers-reduced-motion.
20
25
  */
21
- declare function DayStrip({ days, value, onValueChange, className, "aria-label": ariaLabel, ...props }: DayStripProps): React.JSX.Element;
26
+ declare function DayStrip({ days, value, onValueChange, sticky, className, "aria-label": ariaLabel, ...props }: DayStripProps): React.JSX.Element;
22
27
  export { DayStrip };
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { parseLocalDate } from "../../lib/date.js";
2
+ import { parseLocalDate } from "../../lib/internal/date.js";
3
3
  import { cn } from "../../lib/utils.js";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  // src/components/data/day-strip.tsx
@@ -7,12 +7,17 @@ import { jsx, jsxs } from "react/jsx-runtime";
7
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
8
  * Scrolls inside itself when the days do not fit. Smooth scrolling is off under prefers-reduced-motion.
9
9
  */
10
- function DayStrip({ days, value, onValueChange, className, "aria-label": ariaLabel, ...props }) {
10
+ function DayStrip({ days, value, onValueChange, sticky = false, className, "aria-label": ariaLabel, ...props }) {
11
11
  const jump = (e, id) => {
12
12
  e.preventDefault();
13
13
  const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
14
14
  const el = document.getElementById(id);
15
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
+ }
16
21
  el.scrollIntoView?.({
17
22
  behavior: reduce ? "auto" : "smooth",
18
23
  block: "start"
@@ -25,7 +30,7 @@ function DayStrip({ days, value, onValueChange, className, "aria-label": ariaLab
25
30
  return /* @__PURE__ */ jsx("nav", {
26
31
  "data-slot": "day-strip",
27
32
  "aria-label": ariaLabel ?? "Jump to a day",
28
- className: cn("min-w-0 overflow-x-auto", className),
33
+ className: cn("min-w-0 overflow-x-auto", sticky && "sticky top-0 z-10 bg-background", className),
29
34
  ...props,
30
35
  children: /* @__PURE__ */ jsx("ul", {
31
36
  className: "flex gap-2 p-1",
@@ -42,15 +47,20 @@ function DayStrip({ days, value, onValueChange, className, "aria-label": ariaLab
42
47
  children: /* @__PURE__ */ jsxs("a", {
43
48
  href: `#${day.id}`,
44
49
  "aria-current": selected ? "true" : void 0,
45
- "aria-label": day.hasPlans ? `${label}, has plans` : label,
46
50
  onClick: (e) => jump(e, day.id),
47
- className: cn("flex min-h-21 flex-1 flex-col items-center justify-center gap-0.5 rounded-xl px-1 py-2 outline-none transition-colors focus-visible:ring-4 focus-visible:ring-ring/20 motion-reduce:transition-none", selected ? "bg-primary text-primary-foreground" : "bg-card text-foreground shadow-card hover:bg-muted"),
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"),
48
52
  children: [
49
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",
50
59
  className: cn("text-xs font-medium", selected ? "text-primary-foreground" : "text-muted-foreground"),
51
60
  children: date.toLocaleDateString(void 0, { weekday: "short" })
52
61
  }),
53
62
  /* @__PURE__ */ jsx("span", {
63
+ "aria-hidden": "true",
54
64
  className: "text-[22px] leading-tight font-semibold tracking-tight tabular-nums",
55
65
  children: date.getDate()
56
66
  }),
@@ -16,7 +16,7 @@ function Collapse({ open, contentClassName, className, children, ...props }) {
16
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
17
  ...props,
18
18
  children: /* @__PURE__ */ jsx("div", {
19
- className: cn("min-h-0 overflow-hidden", contentClassName),
19
+ className: cn("min-h-0 min-w-0 overflow-x-visible overflow-y-clip", contentClassName),
20
20
  children
21
21
  })
22
22
  });
@@ -29,9 +29,9 @@ export interface TextProps extends Omit<React.ComponentProps<"span">, "ref"> {
29
29
  /** Text with props instead of classes: size, tone, weight, mono, truncate, lines. */
30
30
  declare function Text({ size, tone, weight, mono, truncate, lines, tabular, as: As, className, ...props }: TextProps): React.JSX.Element;
31
31
  export interface HeadingProps extends React.ComponentProps<"h3"> {
32
- /** 3: a section title in a card. 4: a small uppercase label over a group. */
33
- level?: 3 | 4;
32
+ /** 2: a page section title under the page's h1. 3: a section title in a card. 4: a small uppercase label over a group. */
33
+ level?: 2 | 3 | 4;
34
34
  }
35
- /** Section heading inside a card or panel. */
35
+ /** Section heading on a page (2) or inside a card or panel (3, 4). */
36
36
  declare function Heading({ level, className, ...props }: HeadingProps): React.JSX.Element;
37
37
  export { Heading, Text };
@@ -36,8 +36,13 @@ function Text({ size = "base", tone = "default", weight = "normal", mono, trunca
36
36
  ...props
37
37
  });
38
38
  }
39
- /** Section heading inside a card or panel. */
39
+ /** Section heading on a page (2) or inside a card or panel (3, 4). */
40
40
  function Heading({ level = 3, className, ...props }) {
41
+ if (level === 2) return /* @__PURE__ */ jsx("h2", {
42
+ "data-slot": "heading",
43
+ className: cn("text-lg leading-snug font-semibold", className),
44
+ ...props
45
+ });
41
46
  if (level === 4) return /* @__PURE__ */ jsx("h4", {
42
47
  "data-slot": "heading",
43
48
  className: cn("text-xs font-medium tracking-wide text-muted-foreground uppercase", className),
@@ -5,7 +5,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { cva } from "class-variance-authority";
6
6
  import { Slot } from "radix-ui";
7
7
  // src/components/ui/button.tsx
8
- const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[background-color,color,box-shadow,border-color] outline-none select-none focus-visible:ring-4 focus-visible:ring-ring/20 focus-visible:border-ring active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[18px]", {
8
+ const buttonVariants = cva("group/button relative inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[background-color,color,box-shadow,border-color] outline-none select-none focus-visible:ring-4 focus-visible:ring-ring/20 focus-visible:border-ring active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[18px] pointer-coarse:after:absolute pointer-coarse:after:inset-[min(0px,calc(50%-1.375rem))]", {
9
9
  variants: {
10
10
  variant: {
11
11
  default: "bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary),black_10%)]",
@@ -24,7 +24,7 @@ function CheckCircle({ tone = "default", className, onCheckedChange, onAnimation
24
24
  setPop(false);
25
25
  onAnimationEnd?.(e);
26
26
  },
27
- className: cn("peer relative grid size-6 shrink-0 place-items-center rounded-full border-2 border-muted-foreground/45 bg-card text-primary-foreground transition-[background-color,border-color,box-shadow] outline-none after:absolute after:-inset-2.5 hover:border-primary focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 data-[tone=important]:border-warning data-[tone=important]:bg-warning/10 data-checked:border-primary data-checked:bg-primary data-[pop]:animate-check-pop", className),
27
+ className: cn("peer relative grid size-6 shrink-0 place-items-center rounded-full border-2 border-muted-foreground/45 bg-card text-primary-foreground transition-[background-color,border-color] outline-none after:absolute after:-inset-3 hover:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary focus-visible:outline-solid motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-50 data-[tone=important]:border-warning data-[tone=important]:bg-warning/10 data-checked:border-primary data-checked:bg-primary motion-safe:data-[pop]:animate-check-pop", className),
28
28
  ...props,
29
29
  children: /* @__PURE__ */ jsx(Checkbox.Indicator, {
30
30
  "data-slot": "check-circle-indicator",
@@ -39,7 +39,7 @@ function CodeBlockTitle({ className, ...props }) {
39
39
  function CodeBlockActions({ className, ...props }) {
40
40
  return /* @__PURE__ */ jsx("div", {
41
41
  "data-slot": "code-block-actions",
42
- className: cn("-my-1 ms-auto -me-1.5 flex shrink-0 items-center gap-1 [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:end-2 [[data-slot=code-block]>&]:top-2 [[data-slot=code-block]>&]:z-10 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:opacity-0 [[data-slot=code-block]>&]:transition-opacity [[data-slot=code-block]>&]:group-focus-within/code-block:opacity-100 [[data-slot=code-block]>&]:group-hover/code-block:opacity-100 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-1.5 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-1.5 [[data-slot=code-block]>&]:pointer-coarse:opacity-100 [[data-slot=code-block]>&]:*:[button]:bg-background [[data-slot=code-block]>&]:*:[button]:ring-1 [[data-slot=code-block]>&]:*:[button]:ring-foreground/10", className),
42
+ className: cn("-my-1 ms-auto -me-1.5 flex shrink-0 items-center gap-1 pointer-coarse:my-0 [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:end-2 [[data-slot=code-block]>&]:top-2 [[data-slot=code-block]>&]:z-10 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:opacity-0 [[data-slot=code-block]>&]:transition-opacity [[data-slot=code-block]>&]:group-focus-within/code-block:opacity-100 [[data-slot=code-block]>&]:group-hover/code-block:opacity-100 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-1.5 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-1.5 [[data-slot=code-block]>&]:pointer-coarse:opacity-100 [[data-slot=code-block]>&]:*:[button]:bg-background [[data-slot=code-block]>&]:*:[button]:ring-1 [[data-slot=code-block]>&]:*:[button]:ring-foreground/10", className),
43
43
  ...props
44
44
  });
45
45
  }
@@ -57,7 +57,7 @@ function CodeBlockCopyButton({ value, variant = "ghost", size = "icon-xs", type
57
57
  variant,
58
58
  size,
59
59
  "aria-label": copied ? "Copied" : "Copy",
60
- className: cn(className),
60
+ className: cn("pointer-coarse:size-11", className),
61
61
  onClick: (event) => {
62
62
  onClick?.(event);
63
63
  navigator.clipboard.writeText(value).then(() => setCopied(true)).catch(() => {});
@@ -49,6 +49,7 @@ function ComboboxInput({ className, children, disabled = false, showTrigger = tr
49
49
  variant: "ghost",
50
50
  asChild: true,
51
51
  "data-slot": "input-group-button",
52
+ "aria-label": "Show options",
52
53
  className: "group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent",
53
54
  disabled,
54
55
  children: /* @__PURE__ */ jsx(ComboboxTrigger, {})
@@ -10,7 +10,7 @@ function InputGroup({ className, ...props }) {
10
10
  return /* @__PURE__ */ jsx("div", {
11
11
  "data-slot": "input-group",
12
12
  role: "group",
13
- className: cn("group/input-group relative flex h-10 w-full min-w-0 items-center rounded-md border border-transparent bg-muted transition-[border-color,box-shadow,background-color] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-primary has-[[data-slot=input-group-control]:focus-visible]:bg-card has-[[data-slot=input-group-control]:focus-visible]:ring-4 has-[[data-slot=input-group-control]:focus-visible]:ring-primary/15 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", className),
13
+ className: cn("group/input-group relative flex h-10 pointer-coarse:h-11 w-full min-w-0 items-center rounded-md border border-transparent bg-muted transition-[border-color,box-shadow,background-color] outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-primary has-[[data-slot=input-group-control]:focus-visible]:bg-card has-[[data-slot=input-group-control]:focus-visible]:ring-4 has-[[data-slot=input-group-control]:focus-visible]:ring-primary/15 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", className),
14
14
  ...props
15
15
  });
16
16
  }
@@ -6,7 +6,7 @@ function Input({ className, type, ...props }) {
6
6
  return /* @__PURE__ */ jsx("input", {
7
7
  type,
8
8
  "data-slot": "input",
9
- className: cn("h-10 w-full min-w-0 py-1 file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground [&::-webkit-search-cancel-button]:hidden rounded-md border border-transparent bg-muted px-3.5 text-base transition-[border-color,box-shadow,background-color] outline-none placeholder:text-muted-foreground focus-visible:border-primary focus-visible:bg-card focus-visible:ring-4 focus-visible:ring-primary/15 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/15 md:text-sm", className),
9
+ className: cn("h-10 pointer-coarse:h-11 w-full min-w-0 py-1 file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground [&::-webkit-search-cancel-button]:hidden rounded-md border border-transparent bg-muted px-3.5 text-base transition-[border-color,box-shadow,background-color] outline-none placeholder:text-muted-foreground focus-visible:border-primary focus-visible:bg-card focus-visible:ring-4 focus-visible:ring-primary/15 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-4 aria-invalid:ring-destructive/15 md:text-sm", className),
10
10
  ...props
11
11
  });
12
12
  }
@@ -1,4 +1,4 @@
1
- import { ExternalToast, ToasterProps } from "sonner";
1
+ import { ExternalToast, ToasterProps, toast } from "sonner";
2
2
  declare const Toaster: ({ ...props }: ToasterProps) => import("react").JSX.Element;
3
3
  export interface ToastUndoOptions extends Omit<ExternalToast, "action"> {
4
4
  /** Called when Undo is pressed. The toast closes. */
@@ -13,4 +13,4 @@ export interface ToastUndoOptions extends Omit<ExternalToast, "action"> {
13
13
  * Uses the mounted <Toaster />. Returns the toast id (pass it to `toast.dismiss` from sonner).
14
14
  */
15
15
  declare function toastUndo(message: React.ReactNode, { onUndo, undoLabel, duration, ...options }: ToastUndoOptions): string | number;
16
- export { Toaster, toastUndo };
16
+ export { Toaster, toast, toastUndo };
@@ -43,4 +43,4 @@ function toastUndo(message, { onUndo, undoLabel = "Undo", duration = 9e3, ...opt
43
43
  }
44
44
  });
45
45
  }
46
- export { Toaster, toastUndo };
46
+ export { Toaster, toast, toastUndo };
@@ -4,7 +4,7 @@ import { jsx } from "react/jsx-runtime";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Toggle as Toggle$1 } from "radix-ui";
6
6
  // src/components/ui/toggle.tsx
7
- const toggleVariants = cva("group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
7
+ const toggleVariants = cva("group/toggle relative inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 pointer-coarse:after:absolute pointer-coarse:after:inset-[min(0px,calc(50%-1.375rem))]", {
8
8
  variants: {
9
9
  variant: {
10
10
  default: "bg-transparent",