@meith/web 0.35.0 → 0.36.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 (46) hide show
  1. package/app/(board)/member/[id]/reputation/page.tsx +2 -4
  2. package/app/(board)/plugins/[key]/[[...path]]/page.tsx +2 -1
  3. package/app/(board)/search/page.tsx +2 -1
  4. package/app/(board)/unsubscribe/page.tsx +2 -2
  5. package/app/admin/layout.tsx +9 -1
  6. package/app/admin/plugins/[key]/[[...path]]/page.tsx +1 -3
  7. package/app/auth/passkey/verify/route.ts +1 -2
  8. package/app/install/page.tsx +2 -4
  9. package/app/layout.tsx +2 -0
  10. package/package.json +50 -50
  11. package/src/components/account/active-sessions.tsx +2 -2
  12. package/src/components/account/avatar-form.tsx +3 -2
  13. package/src/components/account/feed-token.tsx +5 -5
  14. package/src/components/account/notification-forms.tsx +4 -6
  15. package/src/components/account/push-device-form.tsx +3 -2
  16. package/src/components/account/reputation-forms.tsx +5 -4
  17. package/src/components/account/sign-in-methods.tsx +2 -2
  18. package/src/components/account/subscription-forms.tsx +6 -8
  19. package/src/components/account/two-factor-forms.tsx +3 -4
  20. package/src/components/account/usercp-forms.tsx +48 -59
  21. package/src/components/admin/admin-forms.tsx +27 -18
  22. package/src/components/admin/form-bits.tsx +2 -3
  23. package/src/components/admin/mail-test-card.tsx +3 -1
  24. package/src/components/admin/theme-forms.tsx +4 -5
  25. package/src/components/auth/form-controls.tsx +22 -11
  26. package/src/components/content/edit-post-form.tsx +3 -2
  27. package/src/components/content/new-thread-form.tsx +4 -4
  28. package/src/components/messages/message-forms.tsx +9 -10
  29. package/src/components/moderation/inline-moderation-form.tsx +3 -2
  30. package/src/components/moderation/queue-form.tsx +3 -3
  31. package/src/components/moderation/report-forms.tsx +3 -3
  32. package/src/components/moderation/thread-surgery-form.tsx +4 -4
  33. package/src/components/moderation/thread-tools-form.tsx +4 -4
  34. package/src/components/moderation/warning-forms.tsx +8 -8
  35. package/src/components/shell/install-action.tsx +3 -2
  36. package/src/components/shell/nav-disclosure-enhancer.tsx +3 -0
  37. package/src/components/shell/scheme-toggle.tsx +1 -1
  38. package/src/components/shell/theme-switcher.tsx +9 -8
  39. package/src/components/shell/view-tabs.tsx +1 -65
  40. package/src/server/admin-actions.ts +30 -21
  41. package/src/server/auth-actions.ts +11 -4
  42. package/src/server/federation.ts +3 -1
  43. package/src/server/upgrade-notice.ts +1 -1
  44. package/src/styles/globals.css +63 -0
  45. package/src/view/copy.ts +2 -0
  46. package/src/view/panel-nav.ts +8 -10
@@ -3,6 +3,7 @@
3
3
  import { useActionState } from 'react'
4
4
 
5
5
  import type { SubscriptionMode } from '@meith/subscriptions'
6
+ import { buttonVariants, controlVariants, surfaceVariants } from '@meith/ui'
6
7
 
7
8
  import { EMPTY_STATE } from '@/server/auth-form-state'
8
9
  import {
@@ -14,20 +15,18 @@ import {
14
15
  saveSignatureAction,
15
16
  } from '@/server/usercp-actions'
16
17
 
17
- import { FormError, PendingButton } from '../auth/form-controls'
18
+ import { Field, FormError, PendingButton } from '../auth/form-controls'
18
19
  import { MarkdownEditor } from '../content/markdown-editor'
19
20
  import { CustomField, type CustomFieldInput } from '../profile/custom-field'
20
21
  import { type Copy, fromCopy } from '../shell/copy'
21
22
 
22
23
  export type { CustomFieldInput }
23
24
 
24
- const FIELD =
25
- 'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
25
+ const FIELD = controlVariants()
26
26
 
27
- const BUTTON =
28
- 'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
27
+ const BUTTON = buttonVariants({ variant: 'primary', size: 'default' })
29
28
 
30
- const CARD = 'flex flex-col gap-4 rounded-lg border border-border bg-card p-5'
29
+ const CARD = surfaceVariants({ padded: true })
31
30
 
32
31
  export function ProfileForm({
33
32
  location,
@@ -48,12 +47,12 @@ export function ProfileForm({
48
47
  <form action={action} className={CARD}>
49
48
  <FormError message={state.error} />
50
49
 
51
- <label className="flex flex-col gap-1 text-sm">
50
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
52
51
  <span className="font-medium">{fromCopy(copy, 'accountForm.profile.location')}</span>
53
52
  <input name="location" defaultValue={location} className={FIELD} maxLength={100} />
54
53
  </label>
55
54
 
56
- <label className="flex flex-col gap-1 text-sm">
55
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
57
56
  <span className="font-medium">{fromCopy(copy, 'accountForm.profile.website')}</span>
58
57
  <input
59
58
  name="website"
@@ -64,7 +63,7 @@ export function ProfileForm({
64
63
  />
65
64
  </label>
66
65
 
67
- <label className="flex flex-col gap-1 text-sm">
66
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
68
67
  <span className="font-medium">{fromCopy(copy, 'accountForm.profile.bio')}</span>
69
68
  <textarea name="bio" defaultValue={bio} className={FIELD} rows={5} maxLength={1000} />
70
69
  <span className="text-xs text-muted-foreground">
@@ -103,7 +102,7 @@ export function DisplayGroupForm({
103
102
  </h2>
104
103
  <FormError message={state.error} />
105
104
 
106
- <label className="flex flex-col gap-1 text-sm">
105
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
107
106
  <span className="font-medium">{fromCopy(copy, 'accountForm.displayGroup.label')}</span>
108
107
  <select name="displayGroupId" defaultValue={selected} className={FIELD}>
109
108
  {choices.map((choice) => (
@@ -157,7 +156,7 @@ export function OptionsForm({
157
156
  <form action={action} className={CARD}>
158
157
  <FormError message={state.error} />
159
158
 
160
- <label className="flex flex-col gap-1 text-sm">
159
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
161
160
  <span className="font-medium">{fromCopy(copy, 'accountForm.options.timezone')}</span>
162
161
  <select name="timezone" defaultValue={timezone} className={FIELD}>
163
162
  {timezones.map((zone) => (
@@ -171,7 +170,7 @@ export function OptionsForm({
171
170
  </span>
172
171
  </label>
173
172
 
174
- <label className="flex flex-col gap-1 text-sm">
173
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
175
174
  <span className="font-medium">{fromCopy(copy, 'accountForm.options.locale')}</span>
176
175
  <select name="locale" defaultValue={locale} className={FIELD}>
177
176
  {locales.map((choice) => (
@@ -185,7 +184,7 @@ export function OptionsForm({
185
184
  </span>
186
185
  </label>
187
186
 
188
- <label className="flex flex-col gap-1 text-sm">
187
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
189
188
  <span className="font-medium">{fromCopy(copy, 'accountForm.options.postsPerPage')}</span>
190
189
  <input
191
190
  name="postsPerPage"
@@ -196,7 +195,7 @@ export function OptionsForm({
196
195
  />
197
196
  </label>
198
197
 
199
- <label className="flex flex-col gap-1 text-sm">
198
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
200
199
  <span className="font-medium">{fromCopy(copy, 'accountForm.options.threadsPerPage')}</span>
201
200
  <input
202
201
  name="threadsPerPage"
@@ -221,7 +220,7 @@ export function OptionsForm({
221
220
  </span>
222
221
  </label>
223
222
 
224
- <label className="flex flex-col gap-1 text-sm">
223
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
225
224
  <span className="font-medium">
226
225
  {fromCopy(copy, 'accountForm.options.autoWatchOwnThreads')}
227
226
  </span>
@@ -237,7 +236,7 @@ export function OptionsForm({
237
236
  </span>
238
237
  </label>
239
238
 
240
- <label className="flex flex-col gap-1 text-sm">
239
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
241
240
  <span className="font-medium">
242
241
  {fromCopy(copy, 'accountForm.options.autoWatchRepliedThreads')}
243
242
  </span>
@@ -276,40 +275,32 @@ export function PasswordForm({ minLength, copy }: { minLength: number; copy: Cop
276
275
  </h2>
277
276
  <FormError message={state.error} />
278
277
 
279
- <label className="flex flex-col gap-1 text-sm">
280
- <span className="font-medium">{fromCopy(copy, 'accountForm.password.current')}</span>
281
- <input
282
- type="password"
283
- name="currentPassword"
284
- className={FIELD}
285
- autoComplete="current-password"
286
- required
287
- />
288
- </label>
278
+ <Field
279
+ label={fromCopy(copy, 'accountForm.password.current')}
280
+ type="password"
281
+ name="currentPassword"
282
+ autoComplete="current-password"
283
+ required
284
+ id="password-currentPassword"
285
+ />
289
286
 
290
- <label className="flex flex-col gap-1 text-sm">
291
- <span className="font-medium">{fromCopy(copy, 'accountForm.password.new')}</span>
292
- <input
293
- type="password"
294
- name="newPassword"
295
- className={FIELD}
296
- autoComplete="new-password"
297
- minLength={minLength}
298
- required
299
- />
300
- </label>
287
+ <Field
288
+ label={fromCopy(copy, 'accountForm.password.new')}
289
+ type="password"
290
+ name="newPassword"
291
+ autoComplete="new-password"
292
+ minLength={minLength}
293
+ required
294
+ />
301
295
 
302
- <label className="flex flex-col gap-1 text-sm">
303
- <span className="font-medium">{fromCopy(copy, 'accountForm.password.confirm')}</span>
304
- <input
305
- type="password"
306
- name="confirmPassword"
307
- className={FIELD}
308
- autoComplete="new-password"
309
- minLength={minLength}
310
- required
311
- />
312
- </label>
296
+ <Field
297
+ label={fromCopy(copy, 'accountForm.password.confirm')}
298
+ type="password"
299
+ name="confirmPassword"
300
+ autoComplete="new-password"
301
+ minLength={minLength}
302
+ required
303
+ />
313
304
 
314
305
  <p className="text-xs text-muted-foreground">{fromCopy(copy, 'accountForm.password.note')}</p>
315
306
 
@@ -338,18 +329,16 @@ export function EmailForm({ email, copy }: { email: string; copy: Copy }) {
338
329
  {copy['accountForm.email.currentTail']}
339
330
  </p>
340
331
 
341
- <label className="flex flex-col gap-1 text-sm">
342
- <span className="font-medium">{fromCopy(copy, 'accountForm.password.current')}</span>
343
- <input
344
- type="password"
345
- name="currentPassword"
346
- className={FIELD}
347
- autoComplete="current-password"
348
- required
349
- />
350
- </label>
332
+ <Field
333
+ label={fromCopy(copy, 'accountForm.password.current')}
334
+ type="password"
335
+ name="currentPassword"
336
+ autoComplete="current-password"
337
+ required
338
+ id="email-currentPassword"
339
+ />
351
340
 
352
- <label className="flex flex-col gap-1 text-sm">
341
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
353
342
  <span className="font-medium">{fromCopy(copy, 'accountForm.email.new')}</span>
354
343
  <input type="email" name="newEmail" className={FIELD} autoComplete="email" required />
355
344
  </label>
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
+ import { buttonVariants, cn, controlVariants, PageTitle, surfaceVariants } from '@meith/ui'
6
+
5
7
  import {
6
8
  adminAbandonSecondFactorAction,
7
9
  adminSignInAction,
@@ -30,9 +32,9 @@ export function AdminSignInForm({
30
32
  return (
31
33
  <form
32
34
  action={action}
33
- className="flex w-full max-w-sm flex-col gap-4 rounded-lg border border-border bg-card p-6"
35
+ className={cn(surfaceVariants({ padded: true }), 'w-full max-w-md gap-6 p-6 sm:p-8')}
34
36
  >
35
- <h1 className="text-xl font-semibold tracking-tight">{fromCopy(copy, 'adminPanel.title')}</h1>
37
+ <PageTitle className="sm:text-2xl">{fromCopy(copy, 'adminPanel.title')}</PageTitle>
36
38
 
37
39
  <p className="text-sm text-muted-foreground">
38
40
  {reason === 'reauth'
@@ -52,14 +54,11 @@ export function AdminSignInForm({
52
54
  name="password"
53
55
  autoComplete="current-password"
54
56
  required
55
- className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40"
57
+ className={controlVariants()}
56
58
  />
57
59
  </label>
58
60
 
59
- <PendingButton
60
- showWorking
61
- className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
62
- >
61
+ <PendingButton showWorking className={buttonVariants({ variant: 'primary' })}>
63
62
  {fromCopy(copy, 'adminPanel.enter')}
64
63
  </PendingButton>
65
64
 
@@ -80,10 +79,8 @@ export function AdminSecondFactorForm({
80
79
  const [state, action] = useActionState(adminVerifySecondFactorAction, EMPTY_STATE)
81
80
 
82
81
  return (
83
- <div className="flex w-full max-w-sm flex-col gap-4 rounded-lg border border-border bg-card p-6">
84
- <h1 className="text-xl font-semibold tracking-tight">
85
- {fromCopy(copy, 'adminPanel.twoFactor.title')}
86
- </h1>
82
+ <div className={cn(surfaceVariants({ padded: true }), 'w-full max-w-md gap-6 p-6 sm:p-8')}>
83
+ <PageTitle className="sm:text-2xl">{fromCopy(copy, 'adminPanel.twoFactor.title')}</PageTitle>
87
84
 
88
85
  <p className="text-sm text-muted-foreground">{fromCopy(copy, 'adminPanel.twoFactor.lede')}</p>
89
86
 
@@ -101,10 +98,7 @@ export function AdminSecondFactorForm({
101
98
  recovery={otpRecoveryFromCopy(copy)}
102
99
  />
103
100
 
104
- <PendingButton
105
- showWorking
106
- className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
107
- >
101
+ <PendingButton showWorking className={buttonVariants({ variant: 'primary' })}>
108
102
  {fromCopy(copy, 'adminPanel.twoFactor.submit')}
109
103
  </PendingButton>
110
104
  </form>
@@ -125,10 +119,25 @@ export function AdminSignOutForm({ copy }: { copy: Copy }) {
125
119
  return (
126
120
  <form action={adminSignOutAction}>
127
121
  <PendingButton
128
- showWorking
129
- className="text-sm text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
122
+ title={fromCopy(copy, 'adminPanel.leave')}
123
+ className={cn(
124
+ buttonVariants({ variant: 'ghost', size: 'icon' }),
125
+ 'size-11 sm:w-auto sm:px-3',
126
+ )}
130
127
  >
131
- {fromCopy(copy, 'adminPanel.leave')}
128
+ <svg
129
+ aria-hidden="true"
130
+ viewBox="0 0 24 24"
131
+ fill="none"
132
+ stroke="currentColor"
133
+ strokeWidth="1.5"
134
+ strokeLinecap="round"
135
+ strokeLinejoin="round"
136
+ className="size-5 sm:hidden"
137
+ >
138
+ <path d="M9 5H5v14h4M14 8l4 4-4 4M9 12h9" />
139
+ </svg>
140
+ <span className="sr-only sm:not-sr-only">{fromCopy(copy, 'adminPanel.leave')}</span>
132
141
  </PendingButton>
133
142
  </form>
134
143
  )
@@ -1,7 +1,6 @@
1
- import { Alert, AlertDescription } from '@meith/ui'
1
+ import { Alert, AlertDescription, controlVariants } from '@meith/ui'
2
2
 
3
- export const INPUT =
4
- 'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
3
+ export const INPUT = controlVariants()
5
4
 
6
5
  export function Saved({ when = true, children }: { when?: boolean; children: React.ReactNode }) {
7
6
  if (!when) return null
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
+ import { buttonVariants } from '@meith/ui'
6
+
5
7
  import { PANEL_CARD } from '@/components/shell/panel-list'
6
8
  import { EMPTY_STATE, type FormState } from '@/server/auth-form-state'
7
9
  import { sendTestMailAction } from '@/server/mail-test-actions'
@@ -69,7 +71,7 @@ export function MailTestCard({
69
71
  <div>
70
72
  <PendingButton
71
73
  disabled={pending || !sends}
72
- className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-60"
74
+ className={buttonVariants({ variant: 'primary', size: 'default' })}
73
75
  >
74
76
  {pending
75
77
  ? fromCopy(copy, 'adminPanel.mail.sending')
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useActionState, useEffect, useState } from 'react'
4
4
 
5
- import { textLinkVariants } from '@meith/ui'
5
+ import { buttonVariants, textLinkVariants } from '@meith/ui'
6
6
 
7
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
8
8
  import {
@@ -42,8 +42,7 @@ import { ThemePreview, ValidatedSample } from './theme-preview'
42
42
  const INPUT =
43
43
  'w-full rounded-md border border-input bg-card px-3 py-2 font-mono text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
44
44
 
45
- const GHOST_BUTTON =
46
- 'inline-flex h-8 items-center justify-center rounded-md border border-border px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
45
+ const GHOST_BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
47
46
 
48
47
  const LINK = `${textLinkVariants({ tone: 'inherit', size: 'xs' })} focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`
49
48
 
@@ -437,7 +436,7 @@ export function ThemeEditorForm({
437
436
  )
438
437
  })}
439
438
 
440
- <label className="flex flex-col gap-1 text-sm">
439
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
441
440
  <span className="font-medium">{fromCopy(copy, 'adminTheme.css.label')}</span>
442
441
  <textarea
443
442
  name="customCss"
@@ -552,7 +551,7 @@ export function ImportThemeForm({ themeKey, copy }: { themeKey: string; copy: Co
552
551
  <Saved when={state.notice === 'imported'}>{fromCopy(copy, 'adminTheme.import.done')}</Saved>
553
552
  <input type="hidden" name="key" value={themeKey} />
554
553
 
555
- <label className="flex flex-col gap-1 text-sm">
554
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
556
555
  <span className="font-medium">{fromCopy(copy, 'adminTheme.import.label')}</span>
557
556
  <textarea name="document" rows={8} className={INPUT} required />
558
557
  <span className="text-xs text-muted-foreground">
@@ -5,6 +5,7 @@ import { useFormStatus } from 'react-dom'
5
5
 
6
6
  import { Alert, AlertDescription, AlertTitle, cn, Input, Field as UiField } from '@meith/ui'
7
7
  import { Button } from '@meith/ui/button'
8
+ import { PasswordInput } from '@meith/ui/password-input'
8
9
 
9
10
  import { fromCopy, useCopy } from '../shell/copy'
10
11
 
@@ -115,6 +116,15 @@ export function Field({
115
116
  error,
116
117
  id,
117
118
  }: FieldProps) {
119
+ const copy = useCopy()
120
+ const inputProps = {
121
+ required,
122
+ ...(autoComplete === undefined ? {} : { autoComplete }),
123
+ ...(defaultValue === undefined ? {} : { defaultValue }),
124
+ ...(minLength === undefined ? {} : { minLength }),
125
+ ...(maxLength === undefined ? {} : { maxLength }),
126
+ }
127
+
118
128
  return (
119
129
  <UiField
120
130
  name={name}
@@ -123,17 +133,18 @@ export function Field({
123
133
  {...(id === undefined ? {} : { id })}
124
134
  {...(hint === undefined ? {} : { description: hint })}
125
135
  >
126
- {(control) => (
127
- <Input
128
- {...control}
129
- type={type}
130
- required={required}
131
- {...(autoComplete === undefined ? {} : { autoComplete })}
132
- {...(defaultValue === undefined ? {} : { defaultValue })}
133
- {...(minLength === undefined ? {} : { minLength })}
134
- {...(maxLength === undefined ? {} : { maxLength })}
135
- />
136
- )}
136
+ {(control) =>
137
+ type === 'password' ? (
138
+ <PasswordInput
139
+ {...control}
140
+ {...inputProps}
141
+ showLabel={fromCopy(copy, 'form.showPassword')}
142
+ hideLabel={fromCopy(copy, 'form.hidePassword')}
143
+ />
144
+ ) : (
145
+ <Input {...control} {...inputProps} type={type} />
146
+ )
147
+ }
137
148
  </UiField>
138
149
  )
139
150
  }
@@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
4
4
  import { useActionState } from 'react'
5
5
 
6
6
  import type { UploadLimits } from '@meith/attachments/limits'
7
+ import { buttonVariants } from '@meith/ui'
7
8
 
8
9
  import { EMPTY_STATE } from '@/server/auth-form-state'
9
10
  import { deletePostAction, editPostAction, restorePostAction } from '@/server/content-actions'
@@ -131,7 +132,7 @@ export function EditPostForm({
131
132
  name="intent"
132
133
  value="preview"
133
134
  showWorking
134
- className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm font-medium transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
135
+ className={buttonVariants({ variant: 'outline', size: 'default' })}
135
136
  >
136
137
  {fromCopy(copy, 'composer.preview')}
137
138
  </PendingButton>
@@ -160,7 +161,7 @@ export function DeletePostForm({
160
161
  <div>
161
162
  <PendingButton
162
163
  showWorking
163
- className="inline-flex h-10 items-center justify-center rounded-md border border-destructive/40 px-4 text-sm font-medium text-destructive transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
164
+ className={buttonVariants({ variant: 'destructive', size: 'default' })}
164
165
  >
165
166
  {fromCopy(copy, 'composer.edit.delete')}
166
167
  </PendingButton>
@@ -6,7 +6,7 @@ import { useActionState, useEffect, useState } from 'react'
6
6
  import type { UploadLimits } from '@meith/attachments/limits'
7
7
  import type { Draft } from '@meith/drafts'
8
8
  import { POLL_OPTION_MAX } from '@meith/polls/limits'
9
- import { buttonVariants, Disclosure } from '@meith/ui'
9
+ import { buttonVariants, controlVariants, Disclosure } from '@meith/ui'
10
10
 
11
11
  import type { PollDraftValues } from '@/server/auth-form-state'
12
12
  import { EMPTY_STATE } from '@/server/auth-form-state'
@@ -128,7 +128,7 @@ export function NewThreadForm({
128
128
  <select
129
129
  name="prefixId"
130
130
  defaultValue={state.values?.prefixId ?? draft?.prefixId?.toString() ?? ''}
131
- className="h-10 rounded-md border border-input bg-card px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
131
+ className={controlVariants()}
132
132
  >
133
133
  <option value="">
134
134
  {requiresPrefix
@@ -183,7 +183,7 @@ export function NewThreadForm({
183
183
  min={0}
184
184
  step={1}
185
185
  defaultValue={pollDraft?.maxOptions ?? '1'}
186
- className="h-10 rounded-md border border-input bg-card px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
186
+ className={controlVariants()}
187
187
  />
188
188
  <span className="text-xs text-muted-foreground">
189
189
  {fromCopy(copy, 'composer.newThread.pollChoicesHint')}
@@ -215,7 +215,7 @@ export function NewThreadForm({
215
215
  type="datetime-local"
216
216
  name="pollClosesAt"
217
217
  defaultValue={pollDraft?.closesAt ?? ''}
218
- className="h-10 rounded-md border border-input bg-card px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
218
+ className={controlVariants()}
219
219
  />
220
220
  <span className="text-xs text-muted-foreground">
221
221
  {fromCopy(copy, 'pollEdit.closesAtHint')}
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
+ import { buttonVariants, controlVariants, surfaceVariants } from '@meith/ui'
6
+
5
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
8
  import { messageBulkAction, sendMessageAction } from '@/server/message-actions'
7
9
 
@@ -11,16 +13,13 @@ import { ConfirmDialog } from '../shell/confirm-dialog'
11
13
  import { type Copy, fromCopy } from '../shell/copy'
12
14
  import { RecipientField } from './recipient-field'
13
15
 
14
- const FIELD =
15
- 'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
16
+ const FIELD = controlVariants()
16
17
 
17
- const BUTTON =
18
- 'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
18
+ const BUTTON = buttonVariants({ variant: 'primary', size: 'default' })
19
19
 
20
- const SECONDARY =
21
- 'inline-flex h-8 items-center justify-center rounded-md border border-border px-3 text-xs font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
20
+ const SECONDARY = buttonVariants({ variant: 'outline', size: 'sm' })
22
21
 
23
- const CARD = 'flex flex-col gap-4 rounded-lg border border-border bg-card p-5'
22
+ const CARD = surfaceVariants({ padded: true })
24
23
 
25
24
  export function ComposeForm({
26
25
  to,
@@ -44,7 +43,7 @@ export function ComposeForm({
44
43
  <FormError message={state.error} />
45
44
  {replyToId === null ? null : <input type="hidden" name="replyTo" value={replyToId} />}
46
45
 
47
- <label htmlFor="message-to" className="flex flex-col gap-1 text-sm">
46
+ <label htmlFor="message-to" className="flex min-w-0 flex-col gap-2 text-sm">
48
47
  <span className="font-medium">{fromCopy(copy, 'messageForm.to')}</span>
49
48
  <RecipientField
50
49
  id="message-to"
@@ -58,7 +57,7 @@ export function ComposeForm({
58
57
  </span>
59
58
  </label>
60
59
 
61
- <label htmlFor="message-bcc" className="flex flex-col gap-1 text-sm">
60
+ <label htmlFor="message-bcc" className="flex min-w-0 flex-col gap-2 text-sm">
62
61
  <span className="font-medium">{fromCopy(copy, 'messageForm.bcc')}</span>
63
62
  <RecipientField
64
63
  id="message-bcc"
@@ -71,7 +70,7 @@ export function ComposeForm({
71
70
  </span>
72
71
  </label>
73
72
 
74
- <label className="flex flex-col gap-1 text-sm">
73
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
75
74
  <span className="font-medium">{fromCopy(copy, 'messageForm.subject')}</span>
76
75
  <input
77
76
  name="subject"
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
+ import { buttonVariants } from '@meith/ui'
6
+
5
7
  import { BOARD_MEASURE } from '@/components/shell/measure'
6
8
  import { EMPTY_STATE } from '@/server/auth-form-state'
7
9
  import { inlineModerateAction } from '@/server/inline-moderation-actions'
@@ -11,8 +13,7 @@ import { FormError, PendingButton } from '../auth/form-controls'
11
13
  import { ConfirmDialog } from '../shell/confirm-dialog'
12
14
  import { type Copy, fromCopy } from '../shell/copy'
13
15
 
14
- const BUTTON =
15
- 'inline-flex h-8 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
16
+ const BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
16
17
 
17
18
  export interface InlineMoveOption {
18
19
  readonly id: number
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
- import { TextLink } from '@meith/ui'
5
+ import { buttonVariants, TextLink } from '@meith/ui'
6
6
 
7
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
8
8
  import { moderateQueueAction } from '@/server/moderation-actions'
@@ -74,14 +74,14 @@ export function QueueForm({ rows, copy }: { rows: readonly QueueFormRow[]; copy:
74
74
  <PendingButton
75
75
  name="decision"
76
76
  value="approve"
77
- className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
77
+ className={buttonVariants({ variant: 'primary', size: 'default' })}
78
78
  >
79
79
  {fromCopy(copy, 'moderationForm.queue.approve')}
80
80
  </PendingButton>
81
81
  <PendingButton
82
82
  name="decision"
83
83
  value="reject"
84
- className="inline-flex h-10 items-center justify-center rounded-md border border-destructive/40 px-4 text-sm font-medium text-destructive transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
84
+ className={buttonVariants({ variant: 'destructive', size: 'default' })}
85
85
  >
86
86
  {fromCopy(copy, 'moderationForm.queue.reject')}
87
87
  </PendingButton>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
- import { Card, CardContent, Field, Textarea, textLinkVariants } from '@meith/ui'
5
+ import { buttonVariants, Card, CardContent, Field, Textarea, textLinkVariants } from '@meith/ui'
6
6
 
7
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
8
8
  import { assignReportAction, closeReportAction, fileReportAction } from '@/server/report-actions'
@@ -120,14 +120,14 @@ export function CloseReportForm({ reportId, copy }: { reportId: number; copy: Co
120
120
  <PendingButton
121
121
  name="status"
122
122
  value="resolved"
123
- className="inline-flex h-8 items-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
123
+ className={buttonVariants({ variant: 'primary', size: 'sm' })}
124
124
  >
125
125
  {fromCopy(copy, 'moderationForm.report.resolve')}
126
126
  </PendingButton>
127
127
  <PendingButton
128
128
  name="status"
129
129
  value="rejected"
130
- className="inline-flex h-8 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
130
+ className={buttonVariants({ variant: 'outline', size: 'sm' })}
131
131
  >
132
132
  {fromCopy(copy, 'moderationForm.report.dismiss')}
133
133
  </PendingButton>
@@ -2,16 +2,16 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
+ import { buttonVariants, controlVariants } from '@meith/ui'
6
+
5
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
8
  import { mergeThreadAction, splitThreadAction } from '@/server/surgery-actions'
7
9
 
8
10
  import { FormError, PendingButton } from '../auth/form-controls'
9
11
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
10
12
 
11
- const BUTTON =
12
- 'inline-flex h-8 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
13
- const FIELD =
14
- 'h-8 min-w-0 max-w-full rounded-md border border-border bg-background px-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
13
+ const BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
14
+ const FIELD = controlVariants({ size: 'sm', className: 'w-auto max-w-full' })
15
15
  const LABEL = 'flex min-w-0 max-w-full items-center text-xs'
16
16
 
17
17
  export interface SplitPoint {