@meith/theme-default 0.33.1 → 0.33.3

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.
@@ -1,19 +1,31 @@
1
1
  import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy, linkTarget } from '@meith/theme-kit'
3
3
 
4
- import { PAGE } from '../shared'
4
+ import { BELOW_HEADER, HEADER_HEIGHT, PAGE } from '../shared'
5
5
 
6
6
  const DESKTOP_LINK =
7
- 'inline-flex h-10 items-center rounded-md px-3 font-medium text-muted-foreground transition-colors pointer-coarse:h-11 hover:bg-primary/8 hover:text-primary'
7
+ 'inline-flex h-9 items-center rounded-full px-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground'
8
8
 
9
9
  const MOBILE_LINK =
10
- 'flex min-h-11 items-center rounded-md px-3 text-sm font-medium text-muted-foreground hover:bg-primary/8 hover:text-primary'
10
+ 'flex min-h-11 items-center rounded-lg px-3 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground'
11
11
 
12
12
  const SUMMARY =
13
13
  'cursor-default list-none select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none'
14
14
 
15
15
  function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'>) {
16
- if (logo === undefined) return <>{boardTitle}</>
16
+ if (logo === undefined) {
17
+ return (
18
+ <>
19
+ <span
20
+ aria-hidden="true"
21
+ className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-sm font-bold text-primary-foreground shadow-sm"
22
+ >
23
+ {(Array.from(boardTitle.trim())[0] ?? '?').toUpperCase()}
24
+ </span>
25
+ <span className="truncate">{boardTitle}</span>
26
+ </>
27
+ )
28
+ }
17
29
 
18
30
  const image = (
19
31
  <img
@@ -38,13 +50,13 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
38
50
  if (items === undefined || items.length === 0) return null
39
51
 
40
52
  return (
41
- <ul className="invisible absolute top-full left-0 z-30 min-w-48 rounded-md border border-border bg-card p-1 opacity-0 shadow-elevation transition-opacity group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
53
+ <ul className="invisible absolute top-full left-0 z-30 mt-1 min-w-52 rounded-xl border border-border bg-card p-1.5 opacity-0 shadow-elevation transition-opacity group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
42
54
  {items.map((child) => (
43
55
  <li key={child.href}>
44
56
  <a
45
57
  href={child.href}
46
58
  {...linkTarget(child)}
47
- className="flex items-center rounded-sm px-2.5 py-1.5 text-muted-foreground pointer-coarse:min-h-11 hover:bg-primary/8 hover:text-primary"
59
+ className="flex items-center rounded-lg px-2.5 py-1.5 text-sm text-muted-foreground pointer-coarse:min-h-11 hover:bg-muted hover:text-foreground"
48
60
  >
49
61
  {child.label}
50
62
  </a>
@@ -103,7 +115,7 @@ function MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
103
115
  <li>
104
116
  <details data-nav-disclosure name="default-header-submenu" className="group/submenu">
105
117
  <summary
106
- className={`${SUMMARY} flex min-h-11 items-center justify-between gap-2 rounded-md pl-3 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground`}
118
+ className={`${SUMMARY} flex min-h-11 items-center justify-between gap-2 rounded-lg pl-3 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground`}
107
119
  >
108
120
  <a
109
121
  href={item.href}
@@ -141,30 +153,35 @@ export function Header({
141
153
  const c = (key: string) => fromSlotCopy(copy, `default.header.${key}`)
142
154
 
143
155
  const hasNavigation = navigation.length > 0
156
+ const opensMenus = navigation.some(
157
+ (item) => item.submenu !== undefined && item.submenu.length > 0,
158
+ )
144
159
 
145
160
  return (
146
- <header className="relative border-b border-b-border bg-card">
147
- <div className={`${PAGE} flex flex-wrap items-center gap-x-4 gap-y-3 py-3`}>
161
+ <header className="sticky top-0 z-40 border-b border-b-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/85">
162
+ <div className={`${PAGE} flex ${HEADER_HEIGHT} items-center gap-x-3 sm:gap-x-4`}>
148
163
  <a
149
164
  href={homeHref}
150
- className="me-auto inline-flex min-h-9 min-w-0 items-center text-xl font-semibold tracking-tight text-primary transition-colors hover:text-primary-hover"
165
+ className="me-auto flex min-w-0 items-center gap-2.5 text-lg font-semibold tracking-tight text-foreground transition-colors hover:text-primary"
151
166
  >
152
167
  <BoardMark boardTitle={boardTitle} logo={logo} />
153
168
  </a>
154
169
 
155
- {children}
170
+ <div className="flex min-w-0 shrink-0 items-center gap-2 sm:gap-3">{children}</div>
156
171
 
157
172
  {hasNavigation && (
158
173
  <nav aria-label={c('sections')} data-nav-view="mobile" className="lg:hidden">
159
174
  <details data-nav-disclosure className="group/menu">
160
175
  <summary
161
- className={`${SUMMARY} flex size-11 items-center justify-center rounded-md border border-border text-foreground transition-colors hover:border-primary/40 hover:text-primary group-open/menu:border-primary/40 group-open/menu:bg-primary/8 group-open/menu:text-primary`}
176
+ className={`${SUMMARY} flex size-10 items-center justify-center rounded-lg border border-border bg-card text-foreground transition-colors hover:border-primary/40 hover:text-primary group-open/menu:border-primary/40 group-open/menu:bg-primary/8 group-open/menu:text-primary`}
162
177
  >
163
178
  <MenuGlyph />
164
179
  <span className="sr-only">{c('sections')}</span>
165
180
  </summary>
166
181
 
167
- <div className="absolute inset-x-0 top-full z-30 max-h-[calc(100dvh-5rem)] overflow-y-auto border-b border-border bg-card shadow-elevation">
182
+ <div
183
+ className={`absolute inset-x-0 ${BELOW_HEADER} z-30 max-h-[calc(100dvh-3.5rem)] overflow-y-auto border-b border-border bg-card shadow-elevation`}
184
+ >
168
185
  <ul className={`${PAGE} flex flex-col gap-0.5 py-2`}>
169
186
  {navigation.map((item) => (
170
187
  <MobileNavItem key={item.href} item={item} />
@@ -178,10 +195,20 @@ export function Header({
178
195
 
179
196
  {hasNavigation && (
180
197
  <nav aria-label={c('sections')} className="hidden border-t border-border lg:block">
181
- <div className={PAGE}>
198
+ <div
199
+ className={
200
+ opensMenus
201
+ ? PAGE
202
+ : `${PAGE} overflow-x-auto [mask-image:linear-gradient(to_right,black_calc(100%-1.5rem),transparent)] [scrollbar-width:none]`
203
+ }
204
+ >
182
205
  <ul
183
206
  data-nav-view="desktop"
184
- className="-mx-3 flex flex-wrap items-center gap-0.5 py-1 text-sm"
207
+ className={
208
+ opensMenus
209
+ ? '-mx-3 flex flex-wrap items-center gap-0.5 py-1'
210
+ : '-mx-3 flex items-center gap-0.5 py-1 pr-6 whitespace-nowrap'
211
+ }
185
212
  >
186
213
  {navigation.map((item) => (
187
214
  <li key={item.href} className="group relative shrink-0">
@@ -1,6 +1,7 @@
1
1
  import type { LatestPostsModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
3
  import {
4
+ Avatar,
4
5
  Card,
5
6
  CardContent,
6
7
  CardHeader,
@@ -10,15 +11,15 @@ import {
10
11
  EmptyTitle,
11
12
  } from '@meith/ui'
12
13
 
13
- import { LINK, MUTED_LINK, NUMERIC, PRIMARY_HEADER, Stamp, UserRef } from '../shared'
14
+ import { LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
14
15
 
15
16
  export function LatestPosts({ posts, capturedAt, copy }: LatestPostsModel & { copy: SlotCopy }) {
16
17
  const c = (key: string) => fromSlotCopy(copy, `default.latestPosts.${key}`)
17
18
 
18
19
  return (
19
- <Card aria-labelledby="latest-posts-heading">
20
- <CardHeader className={PRIMARY_HEADER}>
21
- <CardTitle id="latest-posts-heading" className="text-primary">
20
+ <Card aria-labelledby="latest-posts-heading" className="rounded-xl">
21
+ <CardHeader className="bg-card">
22
+ <CardTitle id="latest-posts-heading" className="text-sm">
22
23
  {c('heading')}
23
24
  </CardTitle>
24
25
  <p className={`text-xs text-muted-foreground ${NUMERIC}`}>
@@ -35,25 +36,36 @@ export function LatestPosts({ posts, capturedAt, copy }: LatestPostsModel & { co
35
36
  <CardContent className="px-0 py-0">
36
37
  <ul className="divide-y divide-border">
37
38
  {posts.map((post) => (
38
- <li key={post.href} className="flex flex-col gap-0.5 px-4 py-2">
39
- <a
40
- href={post.href}
41
- className={`truncate text-sm font-medium text-foreground ${LINK}`}
42
- >
43
- {post.threadTitle}
44
- </a>
39
+ <li
40
+ key={post.href}
41
+ className="flex gap-3 px-4 py-2.5 transition-colors hover:bg-muted/50"
42
+ >
43
+ <Avatar
44
+ src={null}
45
+ name={post.author.username}
46
+ size={28}
47
+ className="mt-0.5 rounded-full"
48
+ />
49
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
50
+ <a
51
+ href={post.href}
52
+ className={`truncate text-sm font-medium text-foreground ${LINK}`}
53
+ >
54
+ {post.threadTitle}
55
+ </a>
45
56
 
46
- {post.excerpt !== '' && (
47
- <p className="line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
48
- )}
57
+ {post.excerpt !== '' && (
58
+ <p className="line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
59
+ )}
49
60
 
50
- <p className="truncate text-xs text-muted-foreground">
51
- <UserRef user={post.author} className="font-normal" /> {c('in')}{' '}
52
- <a href={post.forum.href} className={MUTED_LINK}>
53
- {post.forum.label}
54
- </a>{' '}
55
- {c('dot')} <Stamp at={post.postedAt} />
56
- </p>
61
+ <p className="truncate text-xs text-muted-foreground">
62
+ <UserRef user={post.author} className="font-normal" /> {c('in')}{' '}
63
+ <a href={post.forum.href} className={MUTED_LINK}>
64
+ {post.forum.label}
65
+ </a>{' '}
66
+ {c('dot')} <Stamp at={post.postedAt} />
67
+ </p>
68
+ </div>
57
69
  </li>
58
70
  ))}
59
71
  </ul>
@@ -10,7 +10,7 @@ import {
10
10
  EmptyTitle,
11
11
  } from '@meith/ui'
12
12
 
13
- import { LINK, MUTED_LINK, NUMERIC, PRIMARY_HEADER, Stamp, UserRef } from '../shared'
13
+ import { LINK, MUTED_LINK, NUMERIC, PILL, Stamp, UserRef } from '../shared'
14
14
 
15
15
  export function LatestThreads({
16
16
  threads,
@@ -20,9 +20,9 @@ export function LatestThreads({
20
20
  const c = (key: string) => fromSlotCopy(copy, `default.latestThreads.${key}`)
21
21
 
22
22
  return (
23
- <Card aria-labelledby="latest-threads-heading">
24
- <CardHeader className={PRIMARY_HEADER}>
25
- <CardTitle id="latest-threads-heading" className="text-primary">
23
+ <Card aria-labelledby="latest-threads-heading" className="rounded-xl">
24
+ <CardHeader className="bg-card">
25
+ <CardTitle id="latest-threads-heading" className="text-sm">
26
26
  {c('heading')}
27
27
  </CardTitle>
28
28
  <p className={`text-xs text-muted-foreground ${NUMERIC}`}>
@@ -39,17 +39,23 @@ export function LatestThreads({
39
39
  <CardContent className="px-0 py-0">
40
40
  <ul className="divide-y divide-border">
41
41
  {threads.map((thread) => (
42
- <li key={thread.href} className="flex flex-col gap-0.5 px-4 py-2">
43
- <div className="flex items-baseline justify-between gap-2">
42
+ <li
43
+ key={thread.href}
44
+ className="flex flex-col gap-0.5 px-4 py-2.5 transition-colors hover:bg-muted/50"
45
+ >
46
+ <div className="flex items-start justify-between gap-2">
44
47
  <a
45
48
  href={thread.href}
46
- className={`truncate text-sm font-medium text-foreground ${LINK}`}
49
+ className={`line-clamp-2 text-sm font-medium text-foreground ${LINK}`}
47
50
  >
48
51
  {thread.title}
49
52
  </a>
50
- <span className={`shrink-0 text-xs text-muted-foreground ${NUMERIC}`}>
51
- <span className="font-medium text-primary">{thread.replyCount.label}</span>{' '}
52
- {thread.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
53
+ <span className={`${PILL} shrink-0 ${NUMERIC}`}>
54
+ {thread.replyCount.label}
55
+ <span className="sr-only">
56
+ {' '}
57
+ {thread.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
58
+ </span>
53
59
  </span>
54
60
  </div>
55
61
 
@@ -22,22 +22,33 @@ export function MemberProfile({
22
22
 
23
23
  return (
24
24
  <div className={PAGE_BODY}>
25
- <Card>
26
- <CardContent className="flex flex-col gap-4 p-5 sm:flex-row sm:items-start sm:justify-between">
25
+ <Card className="rounded-xl">
26
+ <CardContent className="flex flex-col gap-4 px-5 py-5 sm:flex-row sm:items-center sm:justify-between sm:px-6">
27
27
  <div className="flex min-w-0 items-center gap-4">
28
- <Avatar src={avatarUrl} name={user.username} size={72} />
28
+ <Avatar src={avatarUrl} name={user.username} size={80} className="rounded-2xl" />
29
29
 
30
30
  <div className="min-w-0">
31
31
  <h1
32
- className={cn('text-2xl font-semibold tracking-tight break-words', user.nameClass)}
32
+ className={cn(
33
+ 'font-heading text-2xl font-semibold tracking-tight break-words',
34
+ user.nameClass,
35
+ )}
33
36
  >
34
37
  {user.username}
35
38
  </h1>
36
- {groupTags(groups, title).map((group) => (
37
- <p key={group.title} className="mt-0.5 text-sm text-muted-foreground">
38
- <span className={group.nameClass ?? undefined}>{group.title}</span>
39
- </p>
40
- ))}
39
+ <p className="mt-1.5 flex flex-wrap gap-1.5">
40
+ {groupTags(groups, title).map((group) => (
41
+ <span
42
+ key={group.title}
43
+ className={cn(
44
+ 'inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground',
45
+ group.nameClass,
46
+ )}
47
+ >
48
+ {group.title}
49
+ </span>
50
+ ))}
51
+ </p>
41
52
  </div>
42
53
  </div>
43
54
 
@@ -62,18 +73,20 @@ export function MemberProfile({
62
73
  )}
63
74
  </CardContent>
64
75
 
65
- <dl className="grid grid-cols-3 gap-3 border-t border-border px-5 py-4">
66
- <div>
67
- <dd className={`text-lg font-semibold text-primary ${NUMERIC}`}>{postCount.label}</dd>
76
+ <dl className="grid grid-cols-3 divide-x divide-border border-t border-border">
77
+ <div className="px-5 py-3.5 sm:px-6">
78
+ <dd className={`text-lg font-semibold text-foreground ${NUMERIC}`}>
79
+ {postCount.label}
80
+ </dd>
68
81
  <dt className="text-xs text-muted-foreground">{c('postsLabel')}</dt>
69
82
  </div>
70
- <div>
83
+ <div className="px-5 py-3.5 sm:px-6">
71
84
  <dd className="text-sm font-medium text-foreground">
72
85
  <Stamp at={joinedAt} />
73
86
  </dd>
74
87
  <dt className="text-xs text-muted-foreground">{c('joinedLabel')}</dt>
75
88
  </div>
76
- <div>
89
+ <div className="px-5 py-3.5 sm:px-6">
77
90
  <dd className="text-sm font-medium text-foreground">
78
91
  {lastVisitAt === null ? c('never') : <Stamp at={lastVisitAt} />}
79
92
  </dd>
@@ -83,18 +96,18 @@ export function MemberProfile({
83
96
  </Card>
84
97
 
85
98
  {fields.length > 0 && (
86
- <Card aria-labelledby="profile-fields-heading">
87
- <CardHeader>
99
+ <Card aria-labelledby="profile-fields-heading" className="rounded-xl">
100
+ <CardHeader className="bg-card">
88
101
  <CardTitle id="profile-fields-heading" className="text-sm">
89
102
  {c('about')}
90
103
  </CardTitle>
91
104
  </CardHeader>
92
- <CardContent>
93
- <dl className="grid gap-x-6 gap-y-3 sm:grid-cols-2">
105
+ <CardContent className="px-5 py-4">
106
+ <dl className="grid gap-x-6 gap-y-4 sm:grid-cols-2">
94
107
  {fields.map((field) => (
95
108
  <div key={field.label}>
96
109
  <dt className="text-xs text-muted-foreground">{field.label}</dt>
97
- <dd className="text-sm break-words">{field.value}</dd>
110
+ <dd className="mt-0.5 text-sm break-words">{field.value}</dd>
98
111
  </div>
99
112
  ))}
100
113
  </dl>
@@ -103,13 +116,13 @@ export function MemberProfile({
103
116
  )}
104
117
 
105
118
  {signatureHtml !== null && (
106
- <Card aria-labelledby="profile-signature-heading">
107
- <CardHeader>
119
+ <Card aria-labelledby="profile-signature-heading" className="rounded-xl">
120
+ <CardHeader className="bg-card">
108
121
  <CardTitle id="profile-signature-heading" className="text-sm">
109
122
  {c('signature')}
110
123
  </CardTitle>
111
124
  </CardHeader>
112
- <CardContent>
125
+ <CardContent className="px-5 py-4">
113
126
  <div
114
127
  className="prose-md text-sm text-muted-foreground"
115
128
  dangerouslySetInnerHTML={{ __html: signatureHtml }}
@@ -16,21 +16,21 @@ export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }
16
16
  return (
17
17
  <li key={item.href} className="flex shrink-0 items-center">
18
18
  {index > 0 && (
19
- <span aria-hidden="true" className="px-0.5 text-border select-none">
19
+ <span aria-hidden="true" className="px-1 text-muted-foreground/50 select-none">
20
20
  {c('separator')}
21
21
  </span>
22
22
  )}
23
23
  {isLast ? (
24
24
  <span
25
25
  aria-current="page"
26
- className="max-w-[24ch] truncate px-1.5 py-1 font-medium text-foreground sm:max-w-none"
26
+ className="max-w-[28ch] truncate px-1.5 py-1 font-medium text-foreground sm:max-w-none"
27
27
  >
28
28
  {item.label}
29
29
  </span>
30
30
  ) : (
31
31
  <a
32
32
  href={item.href}
33
- className="rounded-md px-1.5 py-1 transition-colors hover:bg-primary/8 hover:text-primary"
33
+ className="rounded-md px-1.5 py-1 transition-colors hover:bg-muted hover:text-foreground"
34
34
  >
35
35
  {item.label}
36
36
  </a>
@@ -17,7 +17,7 @@ export function Pagination({
17
17
 
18
18
  const c = (key: string) => fromSlotCopy(copy, `default.pagination.${key}`)
19
19
 
20
- const step = cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'min-w-20')
20
+ const step = cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'min-w-20 rounded-full')
21
21
  const stepDisabled = cn(step, 'pointer-events-none opacity-40')
22
22
 
23
23
  return (
@@ -53,7 +53,7 @@ export function Pagination({
53
53
  variant: entry.isCurrent ? 'primary' : 'ghost',
54
54
  size: 'sm',
55
55
  }),
56
- 'min-w-8 px-2',
56
+ 'min-w-8 rounded-full px-2',
57
57
  NUMERIC,
58
58
  entry.isCurrent ? '' : 'hidden sm:inline-flex',
59
59
  )}
@@ -3,14 +3,14 @@ import { fromSlotCopy } from '@meith/theme-kit'
3
3
  import { cn, Disclosure } from '@meith/ui'
4
4
 
5
5
  import { PanelIcon } from '../panel-icons'
6
+ import { BELOW_HEADER } from '../shared'
6
7
 
7
8
  const ITEM =
8
- 'relative flex items-center gap-2.5 rounded-lg px-2.5 py-2 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
9
+ 'group/item relative flex items-center gap-3 rounded-lg px-3 py-2 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
9
10
 
10
- const HERE =
11
- 'bg-secondary font-semibold text-foreground before:absolute before:top-1.5 before:bottom-1.5 before:-left-2.5 before:w-0.5 before:rounded-full before:bg-primary'
12
- const OPEN = 'font-semibold text-foreground hover:bg-accent'
13
- const ELSEWHERE = 'text-muted-foreground hover:bg-accent hover:text-foreground'
11
+ const HERE = 'bg-primary/10 font-semibold text-primary [&_svg]:text-primary'
12
+ const OPEN = 'font-semibold text-foreground hover:bg-muted'
13
+ const ELSEWHERE = 'text-muted-foreground hover:bg-muted hover:text-foreground'
14
14
 
15
15
  function Count({ count, copy }: { count: number; copy: SlotCopy }) {
16
16
  const c = (key: string) => fromSlotCopy(copy, `default.panelNav.${key}`)
@@ -19,7 +19,7 @@ function Count({ count, copy }: { count: number; copy: SlotCopy }) {
19
19
  <>
20
20
  <span
21
21
  aria-hidden="true"
22
- className="ml-auto min-w-5 rounded-full bg-primary px-1.5 py-0.5 text-center text-xs font-semibold tabular-nums text-primary-foreground"
22
+ className="ml-auto min-w-5 rounded-full bg-primary px-1.5 py-0.5 text-center text-[0.6875rem] leading-4 font-semibold tabular-nums text-primary-foreground"
23
23
  >
24
24
  {count > 99 ? c('countCap') : count}
25
25
  </span>
@@ -42,7 +42,7 @@ function Item({
42
42
  const body = (
43
43
  <>
44
44
  {item.icon !== null && (
45
- <span className="text-muted-foreground/80">
45
+ <span className="text-muted-foreground/70 transition-colors group-hover/item:text-foreground">
46
46
  <PanelIcon icon={item.icon} />
47
47
  </span>
48
48
  )}
@@ -81,11 +81,11 @@ function Sections({
81
81
  }: Pick<PanelNavModel, 'label' | 'sections'> & { copy: SlotCopy }) {
82
82
  return (
83
83
  <nav aria-label={label} className="text-sm">
84
- <ul className="flex flex-col gap-0.5 pl-2.5">
84
+ <ul className="flex flex-col gap-0.5">
85
85
  {sections.map((section) => (
86
86
  <li
87
87
  key={section.href}
88
- className={cn(section.isOverview && 'mb-1 border-b border-border pb-1')}
88
+ className={cn(section.isOverview && 'mb-1.5 border-b border-border pb-1.5')}
89
89
  >
90
90
  <Item
91
91
  item={section}
@@ -94,12 +94,12 @@ function Sections({
94
94
  />
95
95
 
96
96
  {section.isOpen && section.children.length > 0 && (
97
- <ul className="mt-0.5 ml-4 flex flex-col gap-0.5 border-l border-border pl-2.5">
97
+ <ul className="my-1 ml-5 flex flex-col gap-0.5 border-l-2 border-border pl-2">
98
98
  {section.children.map((child) => (
99
99
  <li key={child.href}>
100
100
  <Item
101
101
  item={child}
102
- className={child.current === 'here' ? HERE : ELSEWHERE}
102
+ className={cn('py-1.5', child.current === 'here' ? HERE : ELSEWHERE)}
103
103
  copy={copy}
104
104
  />
105
105
  </li>
@@ -121,7 +121,9 @@ export function PanelNav({
121
121
  }: PanelNavModel & { copy: SlotCopy }) {
122
122
  return (
123
123
  <>
124
- <div className="sticky top-0 z-20 -mx-4 bg-background/95 px-4 py-2 backdrop-blur-sm sm:-mx-6 sm:px-6 lg:hidden">
124
+ <div
125
+ className={`sticky ${BELOW_HEADER} z-20 -mx-4 bg-background/95 px-4 py-2 backdrop-blur-sm sm:-mx-6 sm:px-6 lg:hidden`}
126
+ >
125
127
  <Disclosure
126
128
  summary={label}
127
129
  contentClassName="p-2"
@@ -131,7 +133,7 @@ export function PanelNav({
131
133
  </Disclosure>
132
134
  </div>
133
135
 
134
- <div className="hidden rounded-lg border border-border bg-card p-2 shadow-elevation lg:block">
136
+ <div className="hidden rounded-xl border border-border bg-card p-2 shadow-elevation lg:block">
135
137
  <Sections label={label} sections={sections} copy={copy} />
136
138
  </div>
137
139
  </>
@@ -2,7 +2,7 @@ import type { PanelPageModel, PanelSectionModel, SlotCopy } from '@meith/theme-k
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
3
  import { cn } from '@meith/ui'
4
4
 
5
- import { MUTED_LINK, PAGE, pageAt } from '../shared'
5
+ import { MUTED_LINK, PAGE, PAGE_TITLE, pageAt, SECTION_TITLE } from '../shared'
6
6
 
7
7
  export function PanelPage({
8
8
  title,
@@ -21,25 +21,28 @@ export function PanelPage({
21
21
  id="board-content"
22
22
  tabIndex={-1}
23
23
  className={cn(
24
- 'flex w-full flex-col py-8',
24
+ 'flex w-full flex-col py-6 lg:py-8',
25
25
  frame === 'standalone'
26
26
  ? `flex-1 ${width === 'wide' ? PAGE : pageAt('max-w-4xl')}`
27
- : `px-6 ${width === 'wide' ? 'max-w-none' : 'max-w-4xl'}`,
27
+ : `px-4 sm:px-6 ${width === 'wide' ? 'max-w-none' : 'max-w-4xl'}`,
28
28
  gap === 'loose' ? 'gap-8' : 'gap-6',
29
29
  )}
30
30
  >
31
- <div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-2">
31
+ <div className="flex flex-wrap items-end justify-between gap-x-6 gap-y-3 border-b border-border pb-5">
32
32
  <div className="flex min-w-0 flex-col gap-1">
33
33
  {back !== null && (
34
- <a href={back.href} className={`text-sm font-medium ${MUTED_LINK}`}>
35
- {c('back')} {back.label}
34
+ <a
35
+ href={back.href}
36
+ className={`mb-1 inline-flex w-fit items-center gap-1 text-sm font-medium ${MUTED_LINK}`}
37
+ >
38
+ <span aria-hidden="true">{c('back')}</span> {back.label}
36
39
  </a>
37
40
  )}
38
41
 
39
- <h1 className="font-heading text-2xl font-semibold text-foreground">{title}</h1>
42
+ <h1 className={PAGE_TITLE}>{title}</h1>
40
43
 
41
44
  {regions.lede !== undefined && (
42
- <p className="text-sm text-muted-foreground">{regions.lede}</p>
45
+ <p className="max-w-prose text-sm text-muted-foreground">{regions.lede}</p>
43
46
  )}
44
47
  {regions.meta !== undefined && (
45
48
  <p className="text-xs text-muted-foreground">{regions.meta}</p>
@@ -61,7 +64,7 @@ export function PanelSection({ title, headingId, regions, children }: PanelSecti
61
64
  <section aria-labelledby={headingId} className="flex flex-col gap-3">
62
65
  <div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-1">
63
66
  <div className="flex min-w-0 flex-col gap-0.5">
64
- <h2 id={headingId} className="font-heading text-lg font-semibold text-foreground">
67
+ <h2 id={headingId} className={SECTION_TITLE}>
65
68
  {title}
66
69
  </h2>
67
70
  {regions.description !== undefined && (
@@ -1,32 +1,48 @@
1
1
  import type { PanelShellModel } from '@meith/theme-kit'
2
2
 
3
- import { MUTED_LINK } from '../shared'
3
+ import { BELOW_DESKTOP_HEADER, EYEBROW, MUTED_LINK } from '../shared'
4
4
 
5
5
  const RAIL =
6
- 'flex flex-col gap-4 px-4 pt-4 sm:px-6 lg:sticky lg:w-60 lg:shrink-0 lg:self-start lg:py-8 lg:pr-0'
6
+ 'flex flex-col gap-4 px-4 pt-5 sm:px-6 lg:sticky lg:w-64 lg:shrink-0 lg:self-start lg:pt-8 lg:pr-0'
7
+
8
+ function SwitchGlyph() {
9
+ return (
10
+ <svg
11
+ aria-hidden="true"
12
+ viewBox="0 0 16 16"
13
+ className="size-3.5 shrink-0 text-muted-foreground/70"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ strokeWidth="1.5"
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ >
20
+ <path d="M6 3.5 9.5 7 6 10.5M3 7h6.5M13 3v8" />
21
+ </svg>
22
+ )
23
+ }
7
24
 
8
25
  export function PanelShell({ panel, links, linksLabel, regions, children }: PanelShellModel) {
9
26
  return (
10
- <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col lg:flex-row">
11
- <aside className={`${RAIL} ${panel === 'admincp' ? 'lg:top-14' : 'lg:top-6'}`}>
27
+ <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col lg:flex-row lg:gap-8">
28
+ <aside className={`${RAIL} ${panel === 'admincp' ? 'lg:top-14' : BELOW_DESKTOP_HEADER}`}>
12
29
  {regions.nav}
13
30
 
14
31
  {links.length > 0 && (
15
32
  <nav
16
33
  aria-label={linksLabel}
17
- className="hidden rounded-lg border border-border bg-card p-2 shadow-elevation lg:block"
34
+ className="hidden rounded-xl border border-border bg-card p-2 shadow-elevation lg:block"
18
35
  >
19
- <p className="px-2.5 pt-1 pb-1.5 text-xs font-medium tracking-wide text-muted-foreground uppercase">
20
- {linksLabel}
21
- </p>
36
+ <p className={`${EYEBROW} px-3 pt-1.5 pb-1.5`}>{linksLabel}</p>
22
37
  <ul className="flex flex-col gap-0.5 text-sm">
23
38
  {links.map((link) => (
24
39
  <li key={link.href}>
25
40
  <a
26
41
  href={link.href}
27
- className={`block rounded-lg px-2.5 py-1.5 ${MUTED_LINK} hover:bg-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`}
42
+ className={`flex items-center gap-2.5 rounded-lg px-3 py-2 ${MUTED_LINK} hover:bg-muted hover:no-underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`}
28
43
  >
29
- {link.label}
44
+ <SwitchGlyph />
45
+ <span className="min-w-0 flex-1 truncate">{link.label}</span>
30
46
  </a>
31
47
  </li>
32
48
  ))}
@@ -40,10 +56,16 @@ export function PanelShell({ panel, links, linksLabel, regions, children }: Pane
40
56
  {links.length > 0 && (
41
57
  <nav
42
58
  aria-label={linksLabel}
43
- className="flex flex-wrap gap-x-4 gap-y-1 px-4 pt-2 text-sm sm:px-6 lg:hidden"
59
+ className="flex flex-wrap items-center gap-x-2 gap-y-2 px-4 pt-2 pb-6 text-sm sm:px-6 lg:hidden"
44
60
  >
61
+ <span className={`${EYEBROW} me-1`}>{linksLabel}</span>
45
62
  {links.map((link) => (
46
- <a key={link.href} href={link.href} className={MUTED_LINK}>
63
+ <a
64
+ key={link.href}
65
+ href={link.href}
66
+ className="inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
67
+ >
68
+ <SwitchGlyph />
47
69
  {link.label}
48
70
  </a>
49
71
  ))}