@meith/web 0.35.1 → 0.36.1

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 (42) 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/install/page.tsx +2 -4
  8. package/app/layout.tsx +2 -0
  9. package/package.json +50 -50
  10. package/src/components/account/active-sessions.tsx +2 -2
  11. package/src/components/account/avatar-form.tsx +3 -2
  12. package/src/components/account/feed-token.tsx +5 -5
  13. package/src/components/account/notification-forms.tsx +4 -6
  14. package/src/components/account/push-device-form.tsx +3 -2
  15. package/src/components/account/reputation-forms.tsx +5 -4
  16. package/src/components/account/sign-in-methods.tsx +2 -2
  17. package/src/components/account/subscription-forms.tsx +6 -8
  18. package/src/components/account/two-factor-forms.tsx +3 -4
  19. package/src/components/account/usercp-forms.tsx +48 -59
  20. package/src/components/admin/admin-forms.tsx +27 -18
  21. package/src/components/admin/form-bits.tsx +2 -3
  22. package/src/components/admin/mail-test-card.tsx +3 -1
  23. package/src/components/admin/theme-forms.tsx +4 -5
  24. package/src/components/auth/form-controls.tsx +22 -11
  25. package/src/components/content/edit-post-form.tsx +3 -2
  26. package/src/components/content/new-thread-form.tsx +4 -4
  27. package/src/components/messages/message-forms.tsx +9 -10
  28. package/src/components/moderation/inline-moderation-form.tsx +3 -2
  29. package/src/components/moderation/queue-form.tsx +3 -3
  30. package/src/components/moderation/report-forms.tsx +3 -3
  31. package/src/components/moderation/thread-surgery-form.tsx +4 -4
  32. package/src/components/moderation/thread-tools-form.tsx +4 -4
  33. package/src/components/moderation/warning-forms.tsx +8 -8
  34. package/src/components/shell/install-action.tsx +3 -2
  35. package/src/components/shell/nav-disclosure-enhancer.tsx +3 -0
  36. package/src/components/shell/scheme-toggle.tsx +1 -1
  37. package/src/components/shell/theme-switcher.tsx +9 -8
  38. package/src/components/shell/view-tabs.tsx +1 -65
  39. package/src/server/upgrade-notice.ts +1 -1
  40. package/src/styles/globals.css +63 -0
  41. package/src/view/copy.ts +2 -0
  42. package/src/view/panel-nav.ts +8 -10
@@ -2,6 +2,8 @@
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 { threadToolAction } from '@/server/thread-tool-actions'
7
9
 
@@ -9,10 +11,8 @@ import { FormError, PendingButton } from '../auth/form-controls'
9
11
  import { ConfirmDialog } from '../shell/confirm-dialog'
10
12
  import { type Copy, fromCopy } from '../shell/copy'
11
13
 
12
- const BUTTON =
13
- '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'
14
- const SELECT =
15
- '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'
14
+ const BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
15
+ const SELECT = controlVariants({ size: 'sm', className: 'w-auto max-w-full' })
16
16
  const SUMMARY =
17
17
  'flex cursor-pointer list-none items-center gap-2 rounded-lg px-4 py-3 text-xs font-medium text-muted-foreground select-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden'
18
18
 
@@ -2,17 +2,17 @@
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 { issueWarningAction, revokeWarningAction } from '@/server/warning-actions'
7
9
 
8
10
  import { FormError, PendingButton } from '../auth/form-controls'
9
11
  import { type Copy, fromCopy } from '../shell/copy'
10
12
 
11
- const FIELD =
12
- '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'
13
+ const FIELD = controlVariants()
13
14
 
14
- const BUTTON =
15
- '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'
15
+ const BUTTON = buttonVariants({ variant: 'primary', size: 'default' })
16
16
 
17
17
  export interface WarningTypeOption {
18
18
  readonly id: number
@@ -45,7 +45,7 @@ export function IssueWarningForm({
45
45
  <input type="hidden" name="userId" value={userId} />
46
46
  {postId !== null && <input type="hidden" name="postId" value={postId} />}
47
47
 
48
- <label className="flex flex-col gap-1 text-sm">
48
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
49
49
  <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.reason')}</span>
50
50
  <select name="typeId" className={FIELD} defaultValue="">
51
51
  <option value="">{fromCopy(copy, 'moderationForm.warn.somethingElse')}</option>
@@ -58,20 +58,20 @@ export function IssueWarningForm({
58
58
  </label>
59
59
 
60
60
  <div className="grid gap-4 sm:grid-cols-[1fr_8rem]">
61
- <label className="flex flex-col gap-1 text-sm">
61
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
62
62
  <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.titleLabel')}</span>
63
63
  <input type="text" name="title" maxLength={150} className={FIELD} />
64
64
  <span className="text-xs text-muted-foreground">
65
65
  {fromCopy(copy, 'moderationForm.warn.titleHint')}
66
66
  </span>
67
67
  </label>
68
- <label className="flex flex-col gap-1 text-sm">
68
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
69
69
  <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.points')}</span>
70
70
  <input type="number" name="points" min={1} max={100} className={FIELD} />
71
71
  </label>
72
72
  </div>
73
73
 
74
- <label className="flex flex-col gap-1 text-sm">
74
+ <label className="flex min-w-0 flex-col gap-2 text-sm">
75
75
  <span className="font-medium">{fromCopy(copy, 'moderationForm.warn.what')}</span>
76
76
  <textarea name="reason" rows={4} maxLength={2000} className={FIELD} required />
77
77
  <span className="text-xs text-muted-foreground">
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
4
 
5
+ import { buttonVariants } from '@meith/ui'
6
+
5
7
  import { INSTALL_BANNER_COOKIE, INSTALL_BANNER_COOKIE_MAX_AGE } from '@/view/install-banner'
6
8
 
7
9
  interface InstallPrompt extends Event {
@@ -23,8 +25,7 @@ function isInstallPrompt(event: Event): event is InstallPrompt {
23
25
  return 'prompt' in event && 'userChoice' in event
24
26
  }
25
27
 
26
- const BUTTON =
27
- 'inline-flex h-8 items-center rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
28
+ const BUTTON = buttonVariants({ variant: 'outline', size: 'sm' })
28
29
 
29
30
  export function InstallAction({ label, how }: { label: string; how: string }) {
30
31
  const [pending, setPending] = useState<InstallPrompt | null>(null)
@@ -19,6 +19,9 @@ export function NavDisclosureEnhancer() {
19
19
  if (event.key !== 'Escape') return
20
20
 
21
21
  for (const details of document.querySelectorAll<HTMLDetailsElement>(OPEN_DISCLOSURES)) {
22
+ if (details.contains(document.activeElement)) {
23
+ details.querySelector('summary')?.focus()
24
+ }
22
25
  details.open = false
23
26
  }
24
27
  }
@@ -55,7 +55,7 @@ export function SchemeToggle({
55
55
  title={labels[option]}
56
56
  aria-pressed={selected}
57
57
  onClick={() => applySchemeImmediately(option)}
58
- className={`inline-flex h-8 items-center justify-center border-border px-2.5 transition-colors [&:not(:first-child)]:border-l focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${
58
+ className={`inline-flex h-8 pointer-coarse:min-h-11 pointer-coarse:min-w-11 items-center justify-center border-border px-2.5 transition-colors [&:not(:first-child)]:border-l focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${
59
59
  selected
60
60
  ? 'bg-primary text-primary-foreground'
61
61
  : 'bg-background hover:bg-accent hover:text-accent-foreground'
@@ -1,3 +1,5 @@
1
+ import { buttonVariants, cn, controlVariants } from '@meith/ui'
2
+
1
3
  import { PendingButton } from '@/components/auth/form-controls'
2
4
  import { MonitorIcon, MoonIcon, SunIcon } from '@/components/shell/appearance-icons'
3
5
  import { SchemeToggle } from '@/components/shell/scheme-toggle'
@@ -13,9 +15,6 @@ const SCHEME_LABEL_KEY: Record<ColourSchemePreference, string> = {
13
15
  dark: 'appearance.scheme.dark',
14
16
  }
15
17
 
16
- const CONTROL =
17
- 'h-8 rounded-md border border-border bg-background px-2 text-xs text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
18
-
19
18
  export async function ThemeSwitcher() {
20
19
  const [{ choices }, theme, scheme, t] = await Promise.all([
21
20
  getBoardThemeStyle(),
@@ -43,17 +42,19 @@ export async function ThemeSwitcher() {
43
42
  <label htmlFor="appearance-theme" className="text-xs text-muted-foreground">
44
43
  {t.t('appearance.theme')}
45
44
  </label>
46
- <select id="appearance-theme" name="theme" defaultValue={theme} className={CONTROL}>
45
+ <select
46
+ id="appearance-theme"
47
+ name="theme"
48
+ defaultValue={theme}
49
+ className={cn(controlVariants({ size: 'sm' }), 'w-auto')}
50
+ >
47
51
  {choices.map((choice) => (
48
52
  <option key={choice.key} value={choice.key}>
49
53
  {choice.title}
50
54
  </option>
51
55
  ))}
52
56
  </select>
53
- <PendingButton
54
- showWorking
55
- className="h-8 rounded-md border border-border px-2.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
56
- >
57
+ <PendingButton showWorking className={buttonVariants({ variant: 'outline', size: 'sm' })}>
57
58
  {t.t('appearance.apply')}
58
59
  </PendingButton>
59
60
  </form>
@@ -1,65 +1 @@
1
- import { cn } from '@meith/ui'
2
-
3
- export interface ViewTab {
4
- readonly href: string
5
- readonly label: string
6
- readonly isCurrent: boolean
7
- readonly count?: number
8
- }
9
-
10
- export function ViewTabs({
11
- label,
12
- tabs,
13
- aside,
14
- className,
15
- }: {
16
- label: string
17
- tabs: readonly ViewTab[]
18
- aside?: React.ReactNode
19
- className?: string
20
- }) {
21
- if (tabs.length === 0) return null
22
-
23
- return (
24
- <nav
25
- aria-label={label}
26
- className={cn('flex flex-wrap items-center gap-x-3 gap-y-2', className)}
27
- >
28
- <ul className="inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border border-border bg-surface p-1">
29
- {tabs.map((tab) => (
30
- <li key={tab.href} className="shrink-0">
31
- <a
32
- href={tab.href}
33
- {...(tab.isCurrent ? { 'aria-current': 'page' as const } : {})}
34
- className={cn(
35
- 'inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-sm whitespace-nowrap transition-colors',
36
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
37
- tab.isCurrent
38
- ? 'bg-card font-semibold text-primary shadow-sm'
39
- : 'font-medium text-muted-foreground hover:text-foreground',
40
- )}
41
- >
42
- {tab.label}
43
- {tab.count !== undefined && tab.count > 0 && (
44
- <span
45
- className={cn(
46
- 'rounded px-1.5 text-xs font-semibold tabular-nums',
47
- tab.isCurrent
48
- ? 'bg-secondary text-secondary-foreground'
49
- : 'bg-muted text-muted-foreground',
50
- )}
51
- >
52
- {tab.count}
53
- </span>
54
- )}
55
- </a>
56
- </li>
57
- ))}
58
- </ul>
59
-
60
- {aside !== undefined && (
61
- <span className="shrink-0 text-xs text-muted-foreground">{aside}</span>
62
- )}
63
- </nav>
64
- )
65
- }
1
+ export { type NavTab as ViewTab, NavTabs as ViewTabs } from '@meith/ui'
@@ -17,7 +17,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
17
17
 
18
18
  import { activeDefinitions } from './plugin-host'
19
19
 
20
- export const CODE_VERSION = '0.35.1'
20
+ export const CODE_VERSION = '0.36.1'
21
21
 
22
22
  export interface UpgradeApplied {
23
23
  readonly plugins: readonly string[]
@@ -436,7 +436,12 @@
436
436
  border-color: var(--border);
437
437
  }
438
438
 
439
+ html {
440
+ scrollbar-gutter: stable;
441
+ }
442
+
439
443
  body {
444
+ -webkit-font-smoothing: antialiased;
440
445
  background-color: var(--background);
441
446
  color: var(--foreground);
442
447
  }
@@ -465,6 +470,7 @@
465
470
  h3,
466
471
  h4 {
467
472
  text-wrap: balance;
473
+ overflow-wrap: anywhere;
468
474
  }
469
475
 
470
476
  /*
@@ -1000,3 +1006,60 @@ select:not([multiple], [size]):disabled {
1000
1006
  font-size: 1rem;
1001
1007
  }
1002
1008
  }
1009
+
1010
+ [data-slot="navigation-drawer"] {
1011
+ translate: 100% 0;
1012
+ transition:
1013
+ translate 180ms ease-out,
1014
+ display 180ms allow-discrete,
1015
+ overlay 180ms allow-discrete;
1016
+ }
1017
+
1018
+ [dir="rtl"] [data-slot="navigation-drawer"] {
1019
+ translate: -100% 0;
1020
+ }
1021
+
1022
+ [data-slot="navigation-drawer"]:popover-open {
1023
+ translate: 0 0;
1024
+ }
1025
+
1026
+ @starting-style {
1027
+ [data-slot="navigation-drawer"]:popover-open {
1028
+ translate: 100% 0;
1029
+ }
1030
+
1031
+ [dir="rtl"] [data-slot="navigation-drawer"]:popover-open {
1032
+ translate: -100% 0;
1033
+ }
1034
+ }
1035
+
1036
+ @media (width < 64rem) {
1037
+ html:has([data-slot="navigation-drawer"]:popover-open) {
1038
+ overflow: hidden;
1039
+ }
1040
+ }
1041
+
1042
+ @media (width >= 64rem) {
1043
+ [data-slot="navigation-drawer"]:popover-open {
1044
+ display: none;
1045
+ }
1046
+ }
1047
+
1048
+ @media (prefers-reduced-motion: reduce) {
1049
+ [data-slot="navigation-drawer"] {
1050
+ transition: none;
1051
+ }
1052
+ }
1053
+
1054
+ [data-panel-nav-trigger] {
1055
+ display: none;
1056
+ }
1057
+
1058
+ body:has(#panel-usercp-navigation) [data-panel-nav-trigger="usercp"],
1059
+ body:has(#panel-modcp-navigation) [data-panel-nav-trigger="modcp"] {
1060
+ display: inline-flex;
1061
+ }
1062
+
1063
+ body:has(#panel-usercp-navigation, #panel-modcp-navigation) [data-main-navigation] > button {
1064
+ display: none;
1065
+ }
package/src/view/copy.ts CHANGED
@@ -21,6 +21,8 @@ export function patternCopy(
21
21
  }
22
22
 
23
23
  const CHROME_KEYS = [
24
+ 'form.showPassword',
25
+ 'form.hidePassword',
24
26
  'form.working',
25
27
  'form.notSaved',
26
28
  'confirm.confirm',
@@ -178,16 +178,14 @@ export function buildPanelNavModel(input: {
178
178
  isRecord: false,
179
179
  isOpen,
180
180
  isOverview: section.href === input.overviewHref,
181
- children: isOpen
182
- ? visibleChildren(section, deepest).map((child) => ({
183
- href: child.href,
184
- title: panelText(t, child),
185
- icon: null,
186
- count: countFor(input.counts, child.href),
187
- current: currentFor(input.location, child.href, deepest),
188
- isRecord: child.record === true,
189
- }))
190
- : [],
181
+ children: visibleChildren(section, deepest).map((child) => ({
182
+ href: child.href,
183
+ title: panelText(t, child),
184
+ icon: null,
185
+ count: countFor(input.counts, child.href),
186
+ current: currentFor(input.location, child.href, deepest),
187
+ isRecord: child.record === true,
188
+ })),
191
189
  }
192
190
  })
193
191