@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
@@ -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'
@@ -55,28 +55,33 @@ export async function adminSignInAction(_prev: FormState, form: FormData): Promi
55
55
  const config = await boardAuthConfig()
56
56
  const attemptBucket = await adminReauthenticationBucket(actor.userId)
57
57
  const attemptSince = new Date(Date.now() - config.lockoutMinutes * 60_000)
58
- if (
59
- config.maxLoginAttempts > 0 &&
60
- (await accountStore.loginAttempts.countFailuresSince(attemptBucket, attemptSince)) >=
61
- config.maxLoginAttempts
62
- ) {
63
- throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
58
+
59
+ let attemptId: number | null = null
60
+ if (config.maxLoginAttempts > 0) {
61
+ const spent = await accountStore.loginAttempts.recordFailureAndCount(
62
+ attemptBucket,
63
+ attemptSince,
64
+ new Date(),
65
+ )
66
+ attemptId = spent.id
67
+ if (spent.count > config.maxLoginAttempts) {
68
+ throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
69
+ }
64
70
  }
65
71
 
66
- const failAttempt = async (): Promise<void> => {
67
- await accountStore.loginAttempts.record(attemptBucket, false, new Date())
72
+ const auditFailure = async (): Promise<void> => {
68
73
  await recordAdminAction({ action: 'admin.signin_failed' })
69
74
  }
70
75
 
71
76
  const password = text(form, 'password')
72
77
  if (password === '') {
73
- await failAttempt()
78
+ await auditFailure()
74
79
  throw new ValidationError(msg('error.app.enter-password'))
75
80
  }
76
81
 
77
82
  const ok = await verifyPassword(password, account.passwordHash)
78
83
  if (!ok) {
79
- await failAttempt()
84
+ await auditFailure()
80
85
  throw new ForbiddenError(msg('error.app.password-right'))
81
86
  }
82
87
 
@@ -84,11 +89,12 @@ export async function adminSignInAction(_prev: FormState, form: FormData): Promi
84
89
  const twoFactor = twoFactorService()
85
90
 
86
91
  if (twoFactor !== null && (await twoFactor.isEnrolled(actor.userId))) {
92
+ if (attemptId !== null) await accountStore.loginAttempts.removeAttempt(attemptId)
87
93
  await holdAdminSecondFactor(actor.userId, next)
88
94
  target = '/admin'
89
95
  } else {
90
96
  if (twoFactor !== null && (await twoFactorRequiredForStaff())) {
91
- await failAttempt()
97
+ await auditFailure()
92
98
  throw new ForbiddenError(msg('adminAction.twoFactorRequired'))
93
99
  }
94
100
 
@@ -133,28 +139,31 @@ export async function adminVerifySecondFactorAction(
133
139
  const config = await boardAuthConfig()
134
140
  const attemptBucket = await adminReauthenticationBucket(actor.userId)
135
141
  const attemptSince = new Date(Date.now() - config.lockoutMinutes * 60_000)
136
- if (
137
- config.maxLoginAttempts > 0 &&
138
- (await accountStore.loginAttempts.countFailuresSince(attemptBucket, attemptSince)) >=
139
- config.maxLoginAttempts
140
- ) {
141
- throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
142
+
143
+ if (config.maxLoginAttempts > 0) {
144
+ const spent = await accountStore.loginAttempts.recordFailureAndCount(
145
+ attemptBucket,
146
+ attemptSince,
147
+ new Date(),
148
+ )
149
+ if (spent.count > config.maxLoginAttempts) {
150
+ throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
151
+ }
142
152
  }
143
153
 
144
- const failAttempt = async (): Promise<void> => {
145
- await accountStore.loginAttempts.record(attemptBucket, false, new Date())
154
+ const auditFailure = async (): Promise<void> => {
146
155
  await recordAdminAction({ action: 'admin.signin_failed' })
147
156
  }
148
157
 
149
158
  const code = text(form, 'code')
150
159
  if (code === '') {
151
- await failAttempt()
160
+ await auditFailure()
152
161
  throw new ValidationError(msg('error.app.enter-code-from-authenticator-app'))
153
162
  }
154
163
 
155
164
  const outcome = await twoFactor.verify({ userId: pending.userId, code })
156
165
  if (outcome.status !== 'ok') {
157
- await failAttempt()
166
+ await auditFailure()
158
167
  throw new ForbiddenError(
159
168
  msg(outcome.status === 'replayed' ? 'adminAction.codeReplayed' : 'adminAction.codeWrong'),
160
169
  )
@@ -1,6 +1,7 @@
1
1
  'use server'
2
2
 
3
3
  import { redirect } from 'next/navigation'
4
+ import { after } from 'next/server'
4
5
 
5
6
  import { type AuthConfig, foldIdentifier, hashToken, type LoginBucket } from '@meith/accounts'
6
7
  import { env, logger } from '@meith/core'
@@ -344,12 +345,11 @@ export async function verifySecondFactorAction(
344
345
  if (service === null) return { error: await tr('authAction.secondFactorExpired') }
345
346
 
346
347
  try {
347
- await identity.assertSecondFactorAttemptsLeft(pending.userId)
348
+ await identity.spendSecondFactorAttempt(pending.userId)
348
349
 
349
350
  const outcome = await service.verify({ userId: pending.userId, code })
350
351
 
351
352
  if (outcome.status !== 'ok') {
352
- await identity.recordSecondFactorFailure(pending.userId)
353
353
  await recordAuthEvent({ userId: pending.userId, kind: 'second_factor_failed' })
354
354
  return {
355
355
  error: await tr(
@@ -440,9 +440,10 @@ export async function requestResetAction(_prev: FormState, form: FormData): Prom
440
440
  const { token, userId } = await identity.requestPasswordReset(email)
441
441
 
442
442
  if (token !== null && userId !== null) {
443
- const account = await getContainer().accountStore.accounts.findById(userId)
444
- if (account !== null) {
443
+ const deliver = async (): Promise<void> => {
445
444
  try {
445
+ const account = await getContainer().accountStore.accounts.findById(userId)
446
+ if (account === null) return
446
447
  await sendPasswordResetEmail({
447
448
  token,
448
449
  email: account.email,
@@ -456,6 +457,12 @@ export async function requestResetAction(_prev: FormState, form: FormData): Prom
456
457
  )
457
458
  }
458
459
  }
460
+
461
+ try {
462
+ after(deliver)
463
+ } catch {
464
+ await deliver()
465
+ }
459
466
  }
460
467
 
461
468
  if (token && env.NODE_ENV === 'development') {
@@ -54,7 +54,9 @@ export async function federationProvider(id: string): Promise<IdentityProvider |
54
54
  if (!isProviderKind(id)) return null
55
55
  if (getContainer().dataSource !== 'postgres') return null
56
56
 
57
- return providerFor(id, federationOptions(await getSettingsUncached()))
57
+ return providerFor(id, federationOptions(await getSettingsUncached()), {
58
+ allowPrivateHosts: env.OIDC_ALLOW_PRIVATE_HOSTS,
59
+ })
58
60
  }
59
61
 
60
62
  export async function signInProviders(): Promise<readonly ProviderButton[]> {
@@ -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.0'
20
+ export const CODE_VERSION = '0.36.0'
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