@meith/theme-default 0.35.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/theme-default",
3
- "version": "0.35.1",
3
+ "version": "0.36.0",
4
4
  "description": "Meith's default theme — every slot and every token, and the reference for what a complete theme fills.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@meith/theme-kit": "^0.35.1",
24
- "@meith/ui": "^0.35.1"
23
+ "@meith/theme-kit": "^0.36.0",
24
+ "@meith/ui": "^0.36.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^19.2.0"
package/src/copy.ts CHANGED
@@ -29,7 +29,12 @@ export function announcementCopy(t: Translator): SlotCopy {
29
29
  }
30
30
 
31
31
  export function boardIndexCopy(t: Translator): SlotCopy {
32
- return copyFor(t, ['default.boardIndex.boardActivity', 'default.boardIndex.markAllRead'])
32
+ return copyFor(t, [
33
+ 'default.boardIndex.boardActivity',
34
+ 'default.boardIndex.markAllRead',
35
+ 'default.boardIndex.heading',
36
+ 'default.boardIndex.description',
37
+ ])
33
38
  }
34
39
 
35
40
  export function boardStatsCopy(t: Translator): SlotCopy {
@@ -103,7 +108,13 @@ export function forumRowCopy(t: Translator): SlotCopy {
103
108
  }
104
109
 
105
110
  export function headerCopy(t: Translator): SlotCopy {
106
- return copyFor(t, ['default.header.sections'])
111
+ return copyFor(t, [
112
+ 'default.header.sections',
113
+ 'default.header.openMenu',
114
+ 'default.header.closeMenu',
115
+ 'default.header.overview',
116
+ 'default.panelNav.open',
117
+ ])
107
118
  }
108
119
 
109
120
  export function latestPostsCopy(t: Translator): SlotCopy {
@@ -168,7 +179,16 @@ export function paginationCopy(t: Translator): SlotCopy {
168
179
  }
169
180
 
170
181
  export function panelNavCopy(t: Translator): SlotCopy {
171
- return copyFor(t, ['default.panelNav.countCap', 'default.panelNav.waiting'])
182
+ return copyFor(t, [
183
+ 'default.panelNav.countCap',
184
+ 'default.panelNav.waiting',
185
+ 'default.panelNav.open',
186
+ 'default.panelNav.close',
187
+ 'default.panelNav.overview',
188
+ 'default.panelNav.title.admincp',
189
+ 'default.panelNav.title.usercp',
190
+ 'default.panelNav.title.modcp',
191
+ ])
172
192
  }
173
193
 
174
194
  export function panelPageCopy(t: Translator): SlotCopy {
package/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
+ export { headerCopy as mobileHeaderNavCopy, panelNavCopy as mobilePanelNavCopy } from './copy'
1
2
  export { defaultMessages } from './messages'
3
+ export { MobileHeaderNav } from './slots/mobile-header-nav'
4
+ export { MobilePanelNav } from './slots/panel-nav'
2
5
  export { defaultTheme } from './theme'
3
6
  export {
4
7
  BROWSER_THEME_COLOR,
@@ -2,6 +2,8 @@
2
2
  "default.announcement.posted": "Posted",
3
3
  "default.announcement.postedBy": "Posted by",
4
4
  "default.boardIndex.boardActivity": "Board activity",
5
+ "default.boardIndex.description": "Find your next conversation.",
6
+ "default.boardIndex.heading": "Community",
5
7
  "default.boardIndex.markAllRead": "Mark all forums read",
6
8
  "default.boardStats.counted": "Counted",
7
9
  "default.boardStats.heading": "Board statistics",
@@ -46,6 +48,9 @@
46
48
  "default.forumRow.thread.one": "thread",
47
49
  "default.forumRow.thread.other": "threads",
48
50
  "default.forumRow.threadsLabel": "Threads",
51
+ "default.header.closeMenu": "Close navigation",
52
+ "default.header.openMenu": "Open navigation",
53
+ "default.header.overview": "Overview",
49
54
  "default.header.sections": "Board sections",
50
55
  "default.latestPosts.asOf": "As of",
51
56
  "default.latestPosts.dot": "·",
@@ -81,7 +86,13 @@
81
86
  "default.pagination.of": "of",
82
87
  "default.pagination.page": "Page",
83
88
  "default.pagination.previous": "Previous",
89
+ "default.panelNav.close": "Close panel navigation",
84
90
  "default.panelNav.countCap": "99+",
91
+ "default.panelNav.open": "Open panel navigation",
92
+ "default.panelNav.overview": "Overview",
93
+ "default.panelNav.title.admincp": "Administration",
94
+ "default.panelNav.title.modcp": "Moderation",
95
+ "default.panelNav.title.usercp": "Your account",
85
96
  "default.panelNav.waiting": "waiting",
86
97
  "default.panelPage.back": "←",
87
98
  "default.postActions.edit": "Edit",
package/src/shared.tsx CHANGED
@@ -7,11 +7,11 @@ import type {
7
7
  } from '@meith/theme-kit'
8
8
  import { Badge, cn } from '@meith/ui'
9
9
 
10
- const PAGE_WIDTH = 'max-w-6xl'
10
+ const PAGE_WIDTH = 'max-w-7xl'
11
11
 
12
12
  export const PAGE = `mx-auto w-full ${PAGE_WIDTH} px-4 sm:px-6`
13
13
 
14
- export const PAGE_BODY = `${PAGE} flex w-full flex-col gap-5 py-5 sm:py-6`
14
+ export const PAGE_BODY = `${PAGE} flex w-full flex-col gap-6 py-6 sm:py-8`
15
15
 
16
16
  export function pageAt(width: 'max-w-3xl' | 'max-w-4xl'): string {
17
17
  return `mx-auto w-full ${width} px-4 sm:px-6`
@@ -26,9 +26,6 @@ export const MUTED_LINK = `text-muted-foreground ${LINK}`
26
26
 
27
27
  export const BELOW_HEADER = 'top-14'
28
28
 
29
- export const PAGE_TITLE =
30
- 'font-heading text-xl font-semibold tracking-tight text-balance sm:text-2xl'
31
-
32
29
  export const SECTION_TITLE = 'font-heading text-lg font-semibold tracking-tight text-foreground'
33
30
 
34
31
  export const EYEBROW = 'text-xs font-semibold tracking-wide text-muted-foreground uppercase'
@@ -163,17 +160,20 @@ export function Figures({ items, className }: { items: readonly CountItem[]; cla
163
160
  return (
164
161
  <dl
165
162
  className={cn(
166
- 'flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-muted-foreground md:grid md:grid-cols-2 md:gap-x-5 md:text-center',
163
+ 'flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-muted-foreground @3xl/card:grid @3xl/card:grid-cols-2 @3xl/card:gap-x-5 @3xl/card:text-center',
167
164
  className,
168
165
  )}
169
166
  >
170
167
  {items.map((item) => (
171
- <div key={item.label} className="flex items-baseline gap-x-1 md:flex-col md:gap-0">
168
+ <div
169
+ key={item.label}
170
+ className="flex items-baseline gap-x-1 @3xl/card:flex-col @3xl/card:gap-0"
171
+ >
172
172
  <dt className="sr-only">{item.label}</dt>
173
- <dd className={cn('font-semibold text-foreground md:text-sm', NUMERIC)}>
173
+ <dd className={cn('font-semibold text-foreground @3xl/card:text-sm', NUMERIC)}>
174
174
  {item.value.label}
175
175
  </dd>
176
- <dd className="md:text-[0.6875rem] md:leading-4">
176
+ <dd className="@3xl/card:text-[0.6875rem] @3xl/card:leading-4">
177
177
  {item.value.value === 1 ? item.one : item.many}
178
178
  </dd>
179
179
  </div>
@@ -1,4 +1,5 @@
1
1
  import type { AuthPageModel } from '@meith/theme-kit'
2
+ import { PageDescription, PageHeader, PageHeaderContent, PageTitle } from '@meith/ui'
2
3
 
3
4
  import { MUTED_LINK } from '../shared'
4
5
 
@@ -9,15 +10,13 @@ export function AuthPage({ title, alert, links, regions }: AuthPageModel) {
9
10
  tabIndex={-1}
10
11
  className="flex flex-1 flex-col items-center justify-center px-4 py-10 sm:px-6 sm:py-14"
11
12
  >
12
- <div className="flex w-full max-w-sm flex-col gap-6 rounded-2xl border border-border bg-card p-6 text-card-foreground shadow-elevation sm:p-8">
13
- <div className="flex flex-col gap-1">
14
- <h1 className="font-heading text-2xl font-semibold tracking-tight text-foreground">
15
- {title}
16
- </h1>
17
- {regions.lede !== undefined && (
18
- <p className="text-sm text-muted-foreground">{regions.lede}</p>
19
- )}
20
- </div>
13
+ <div className="flex w-full max-w-md flex-col gap-6 rounded-2xl border border-border bg-card p-6 text-card-foreground shadow-elevation sm:p-10">
14
+ <PageHeader>
15
+ <PageHeaderContent>
16
+ <PageTitle className="sm:text-2xl">{title}</PageTitle>
17
+ {regions.lede !== undefined && <PageDescription>{regions.lede}</PageDescription>}
18
+ </PageHeaderContent>
19
+ </PageHeader>
21
20
 
22
21
  {alert !== null && (
23
22
  <p
@@ -1,6 +1,12 @@
1
1
  import type { BoardIndexModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { buttonVariants } from '@meith/ui'
3
+ import {
4
+ buttonVariants,
5
+ PageDescription,
6
+ PageHeader,
7
+ PageHeaderContent,
8
+ PageTitle,
9
+ } from '@meith/ui'
4
10
 
5
11
  import { PAGE_BODY } from '../shared'
6
12
 
@@ -21,13 +27,19 @@ export function BoardIndex({
21
27
  <div className="flex flex-col gap-3">{regions.announcements}</div>
22
28
  )}
23
29
 
24
- {markAllReadAction !== null && (
25
- <form action={markAllReadAction} method="post" className="-mb-3 self-end">
26
- <button type="submit" className={buttonVariants({ variant: 'ghost', size: 'sm' })}>
27
- {c('markAllRead')}
28
- </button>
29
- </form>
30
- )}
30
+ <PageHeader>
31
+ <PageHeaderContent>
32
+ <PageTitle>{c('heading')}</PageTitle>
33
+ <PageDescription>{c('description')}</PageDescription>
34
+ </PageHeaderContent>
35
+ {markAllReadAction !== null && (
36
+ <form action={markAllReadAction} method="post">
37
+ <button type="submit" className={buttonVariants({ variant: 'ghost', size: 'sm' })}>
38
+ {c('markAllRead')}
39
+ </button>
40
+ </form>
41
+ )}
42
+ </PageHeader>
31
43
 
32
44
  <div
33
45
  className={
@@ -8,7 +8,7 @@ export function CategoryBlock({ category, children }: CategoryBlockModel) {
8
8
 
9
9
  return (
10
10
  <Card aria-labelledby={headingId} className="rounded-xl">
11
- <CardHeader className="flex-col items-start gap-0 bg-card px-5 py-3">
11
+ <CardHeader className="flex-col items-start gap-1 bg-surface/50 px-5 py-4">
12
12
  <CardTitle id={headingId} className="flex items-center gap-2.5 text-base">
13
13
  <span aria-hidden="true" className="h-4 w-1 rounded-full bg-primary" />
14
14
  <a href={category.href} className={`${LINK} text-foreground`}>
@@ -1,38 +1,21 @@
1
- import type { DiscoveryViewModel, SlotCopy, TabModel } from '@meith/theme-kit'
1
+ import type { DiscoveryViewModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { Avatar, buttonVariants, Card, CardRows, cn, Empty, EmptyDescription } from '@meith/ui'
3
+ import {
4
+ Avatar,
5
+ buttonVariants,
6
+ Card,
7
+ CardRows,
8
+ cn,
9
+ Empty,
10
+ EmptyDescription,
11
+ NavTabs,
12
+ PageDescription,
13
+ PageHeader,
14
+ PageHeaderContent,
15
+ PageTitle,
16
+ } from '@meith/ui'
4
17
 
5
- import { LINK, NUMERIC, PAGE_TITLE, PILL, pageAt, Stamp } from '../shared'
6
-
7
- const TAB =
8
- 'inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-sm whitespace-nowrap transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
9
-
10
- function Tabs({ label, tabs }: { label: string; tabs: readonly TabModel[] }) {
11
- if (tabs.length === 0) return null
12
-
13
- return (
14
- <nav aria-label={label} className="flex flex-wrap items-center gap-x-3 gap-y-2">
15
- <ul className="inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border border-border bg-surface p-1">
16
- {tabs.map((tab) => (
17
- <li key={tab.href} className="shrink-0">
18
- <a
19
- href={tab.href}
20
- {...(tab.isCurrent ? { 'aria-current': 'page' as const } : {})}
21
- className={cn(
22
- TAB,
23
- tab.isCurrent
24
- ? 'bg-card font-semibold text-primary shadow-sm'
25
- : 'font-medium text-muted-foreground hover:text-foreground',
26
- )}
27
- >
28
- {tab.label}
29
- </a>
30
- </li>
31
- ))}
32
- </ul>
33
- </nav>
34
- )
35
- }
18
+ import { LINK, NUMERIC, PILL, pageAt, Stamp } from '../shared'
36
19
 
37
20
  export function DiscoveryView({
38
21
  title,
@@ -54,12 +37,14 @@ export function DiscoveryView({
54
37
  tabIndex={-1}
55
38
  className={`${pageAt('max-w-4xl')} flex flex-1 flex-col gap-6 py-6 sm:py-8`}
56
39
  >
57
- <div className="flex flex-col gap-1">
58
- <h1 className={PAGE_TITLE}>{title}</h1>
59
- <p className="text-sm text-muted-foreground">{blurb}</p>
60
- </div>
40
+ <PageHeader>
41
+ <PageHeaderContent>
42
+ <PageTitle>{title}</PageTitle>
43
+ <PageDescription>{blurb}</PageDescription>
44
+ </PageHeaderContent>
45
+ </PageHeader>
61
46
 
62
- <Tabs label={tabsLabel} tabs={tabs} />
47
+ <NavTabs label={tabsLabel} tabs={tabs} />
63
48
 
64
49
  {refusal !== null ? (
65
50
  <p role="alert" className="rounded-lg border border-border bg-card px-4 py-3 text-sm">
@@ -88,7 +73,7 @@ export function DiscoveryView({
88
73
  <div className="flex min-w-0 flex-col gap-0.5">
89
74
  <a
90
75
  href={row.href}
91
- className={`truncate text-[0.9375rem] font-medium text-foreground ${LINK}`}
76
+ className={`line-clamp-2 [overflow-wrap:anywhere] text-[0.9375rem] font-medium text-foreground ${LINK}`}
92
77
  >
93
78
  {row.title}
94
79
  </a>
@@ -1,6 +1,6 @@
1
1
  import type { ErrorNoticeModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { buttonVariants, Card, CardContent, CardFooter } from '@meith/ui'
3
+ import { buttonVariants, Card, CardContent, CardFooter, PageTitle } from '@meith/ui'
4
4
 
5
5
  import { NUMERIC } from '../shared'
6
6
 
@@ -20,7 +20,7 @@ export function ErrorNotice({
20
20
  <p className={`text-xs font-medium tracking-wide text-destructive uppercase ${NUMERIC}`}>
21
21
  {c('error')} {status}
22
22
  </p>
23
- <h1 className="mt-1 text-2xl font-semibold tracking-tight text-balance">{title}</h1>
23
+ <PageTitle className="mt-1 sm:text-2xl">{title}</PageTitle>
24
24
  <p className="mt-2 text-sm text-muted-foreground">{message}</p>
25
25
 
26
26
  <a href={homeHref} className={`mt-5 ${buttonVariants({ variant: 'primary' })}`}>
@@ -17,7 +17,7 @@ export function Footer({
17
17
  <footer className="mt-auto border-t border-border bg-card">
18
18
  {regions?.controls && (
19
19
  <div
20
- className={`${PAGE} flex flex-wrap items-center justify-end gap-x-6 gap-y-3 border-b border-border py-3`}
20
+ className={`${PAGE} flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-b border-border py-3`}
21
21
  >
22
22
  {regions.controls}
23
23
  </div>
@@ -9,9 +9,13 @@ import {
9
9
  EmptyAction,
10
10
  EmptyDescription,
11
11
  EmptyTitle,
12
+ PageHeader,
13
+ PageHeaderActions,
14
+ PageHeaderContent,
15
+ PageTitle,
12
16
  } from '@meith/ui'
13
17
 
14
- import { Counts, isEmptyRegion, PAGE_BODY, PAGE_TITLE } from '../shared'
18
+ import { Counts, isEmptyRegion, PAGE_BODY } from '../shared'
15
19
 
16
20
  export function ForumDisplay({
17
21
  forum,
@@ -24,9 +28,9 @@ export function ForumDisplay({
24
28
 
25
29
  return (
26
30
  <div className={PAGE_BODY}>
27
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
28
- <div className="min-w-0">
29
- <h1 className={PAGE_TITLE}>{forum.title}</h1>
31
+ <PageHeader>
32
+ <PageHeaderContent>
33
+ <PageTitle>{forum.title}</PageTitle>
30
34
  <div className="mt-1 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm text-muted-foreground">
31
35
  {forum.description !== null && <p className="max-w-prose">{forum.description}</p>}
32
36
  {forum.type !== 'link' && (
@@ -48,9 +52,9 @@ export function ForumDisplay({
48
52
  />
49
53
  )}
50
54
  </div>
51
- </div>
55
+ </PageHeaderContent>
52
56
 
53
- <div className="flex shrink-0 flex-wrap items-center gap-2">
57
+ <PageHeaderActions>
54
58
  {markReadAction !== null && (
55
59
  <form action={markReadAction} method="post">
56
60
  <button type="submit" className={buttonVariants({ variant: 'ghost' })}>
@@ -66,8 +70,8 @@ export function ForumDisplay({
66
70
  {c('newThread')}
67
71
  </a>
68
72
  )}
69
- </div>
70
- </div>
73
+ </PageHeaderActions>
74
+ </PageHeader>
71
75
 
72
76
  {regions.announcements !== undefined && (
73
77
  <div className="flex flex-col gap-3">{regions.announcements}</div>
@@ -13,10 +13,11 @@ export function ForumJump({ action, field, forums, submitLabel, label }: ForumJu
13
13
  </Label>
14
14
 
15
15
  <NativeSelect
16
+ controlSize="sm"
16
17
  id={id}
17
18
  name={field}
18
19
  defaultValue={forums.find((forum) => forum.isSelected)?.value ?? ''}
19
- className="h-8 w-auto min-w-48 flex-1 text-xs sm:flex-none"
20
+ className="h-8 w-auto min-w-0 max-w-full flex-1 sm:w-48 text-xs sm:flex-none"
20
21
  >
21
22
  {forums.map((forum) => (
22
23
  <option
@@ -29,15 +29,17 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
29
29
  <li
30
30
  data-unread={forum.isUnread ? '' : undefined}
31
31
  className={
32
- 'grid grid-cols-[auto_minmax(0,1fr)] gap-x-3.5 gap-y-2 px-4 py-3.5 transition-colors hover:bg-muted/50 sm:px-5' +
33
- (isLink ? '' : ' md:grid-cols-[auto_minmax(0,1fr)_9rem_16rem] md:items-center md:gap-x-5')
32
+ 'grid grid-cols-[auto_minmax(0,1fr)] gap-x-3.5 gap-y-2 px-4 py-4 transition-colors hover:bg-muted/50 sm:px-5' +
33
+ (isLink
34
+ ? ''
35
+ : ' @3xl/card:grid-cols-[auto_minmax(0,1fr)_6rem_13rem] @3xl/card:items-center @3xl/card:gap-x-5')
34
36
  }
35
37
  >
36
- <Tile label={forum.title} unread={forum.isUnread} className="mt-0.5 md:mt-0">
38
+ <Tile label={forum.title} unread={forum.isUnread} className="mt-0.5 @3xl/card:mt-0">
37
39
  {isLink ? <LinkGlyph /> : undefined}
38
40
  </Tile>
39
41
 
40
- <div className="min-w-0">
42
+ <div className="min-w-0 [overflow-wrap:anywhere]">
41
43
  <a
42
44
  href={forum.href}
43
45
  className={
@@ -70,9 +72,9 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
70
72
  </div>
71
73
 
72
74
  {!isLink && (
73
- <div className="col-start-2 flex min-w-0 flex-col gap-y-1 text-xs text-muted-foreground md:contents">
75
+ <div className="col-start-2 flex min-w-0 flex-col gap-y-1 text-xs text-muted-foreground @3xl/card:contents">
74
76
  <Figures
75
- className="md:col-start-3 md:justify-self-end"
77
+ className="@3xl/card:col-start-3 @3xl/card:justify-self-end"
76
78
  items={[
77
79
  {
78
80
  label: c('threadsLabel'),
@@ -89,18 +91,18 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
89
91
  ]}
90
92
  />
91
93
 
92
- <div className="order-first flex min-w-0 max-w-full flex-wrap gap-x-1 md:order-none md:col-start-4 md:block md:border-l md:border-border md:pl-5">
94
+ <div className="order-first flex min-w-0 max-w-full flex-wrap gap-x-1 @3xl/card:order-none @3xl/card:col-start-4 @3xl/card:block @3xl/card:border-l @3xl/card:border-border @3xl/card:pl-5">
93
95
  {forum.lastPost === null ? (
94
96
  <span className="text-forum-read">{c('noPostsYet')}</span>
95
97
  ) : (
96
98
  <>
97
99
  <a
98
100
  href={forum.lastPost.href}
99
- className={`max-w-full truncate font-medium text-foreground md:block ${LINK}`}
101
+ className={`max-w-full truncate font-medium text-foreground @3xl/card:block ${LINK}`}
100
102
  >
101
103
  {forum.lastPost.threadTitle}
102
104
  </a>
103
- <span className="md:mt-0.5 md:block md:truncate">
105
+ <span className="@3xl/card:mt-0.5 @3xl/card:block @3xl/card:truncate">
104
106
  {c('by')} <UserRef user={forum.lastPost.author} className="font-normal" />{' '}
105
107
  {c('dot')} <Stamp at={forum.lastPost.at} />
106
108
  </span>
@@ -1,17 +1,12 @@
1
1
  import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy, linkTarget } from '@meith/theme-kit'
3
3
 
4
- import { BELOW_HEADER, PAGE } from '../shared'
4
+ import { PAGE } from '../shared'
5
+ import { MobileHeaderNav } from './mobile-header-nav'
5
6
 
6
7
  const DESKTOP_LINK =
7
8
  '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
9
 
9
- const MOBILE_LINK =
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
-
12
- const SUMMARY =
13
- 'cursor-default list-none select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none'
14
-
15
10
  function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'>) {
16
11
  if (logo === undefined) {
17
12
  return (
@@ -50,7 +45,7 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
50
45
  if (items === undefined || items.length === 0) return null
51
46
 
52
47
  return (
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">
48
+ <ul className="invisible absolute top-full left-0 z-30 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">
54
49
  {items.map((child) => (
55
50
  <li key={child.href}>
56
51
  <a
@@ -66,82 +61,6 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
66
61
  )
67
62
  }
68
63
 
69
- function Chevron({ className }: { className: string }) {
70
- return (
71
- <svg
72
- aria-hidden="true"
73
- viewBox="0 0 12 12"
74
- className={className}
75
- fill="none"
76
- stroke="currentColor"
77
- strokeWidth="1.5"
78
- strokeLinecap="round"
79
- strokeLinejoin="round"
80
- >
81
- <path d="m4.5 3 3 3-3 3" />
82
- </svg>
83
- )
84
- }
85
-
86
- function MenuGlyph() {
87
- return (
88
- <svg
89
- aria-hidden="true"
90
- viewBox="0 0 20 20"
91
- className="size-5"
92
- fill="none"
93
- stroke="currentColor"
94
- strokeWidth="1.75"
95
- strokeLinecap="round"
96
- >
97
- <path className="group-open/menu:hidden" d="M3 5.5h14M3 10h14M3 14.5h14" />
98
- <path className="hidden group-open/menu:block" d="m5 5 10 10M15 5 5 15" />
99
- </svg>
100
- )
101
- }
102
-
103
- function MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
104
- if (item.submenu === undefined || item.submenu.length === 0) {
105
- return (
106
- <li>
107
- <a href={item.href} {...linkTarget(item)} className={MOBILE_LINK}>
108
- {item.label}
109
- </a>
110
- </li>
111
- )
112
- }
113
-
114
- return (
115
- <li>
116
- <details data-nav-disclosure name="default-header-submenu" className="group/submenu">
117
- <summary
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`}
119
- >
120
- <a
121
- href={item.href}
122
- {...linkTarget(item)}
123
- className="min-w-0 flex-1 truncate hover:underline"
124
- >
125
- {item.label}
126
- </a>
127
- <span className="flex size-11 shrink-0 items-center justify-center">
128
- <Chevron className="size-3 text-muted-foreground transition-transform group-open/submenu:rotate-90" />
129
- </span>
130
- </summary>
131
- <ul className="mb-1 ml-3 flex flex-col gap-0.5 border-l border-border py-1 pl-2">
132
- {item.submenu.map((child) => (
133
- <li key={child.href}>
134
- <a href={child.href} {...linkTarget(child)} className={MOBILE_LINK}>
135
- {child.label}
136
- </a>
137
- </li>
138
- ))}
139
- </ul>
140
- </details>
141
- </li>
142
- )
143
- }
144
-
145
64
  export function Header({
146
65
  boardTitle,
147
66
  homeHref,
@@ -165,7 +84,7 @@ export function Header({
165
84
  <div className={`${PAGE} flex min-h-14 items-center gap-x-3 sm:gap-x-4`}>
166
85
  <a
167
86
  href={homeHref}
168
- className="flex min-w-0 shrink-0 items-center gap-2.5 text-lg font-semibold tracking-tight text-foreground transition-colors hover:text-primary"
87
+ className="flex min-w-0 items-center gap-2.5 text-lg font-semibold tracking-tight text-foreground transition-colors hover:text-primary"
169
88
  >
170
89
  <BoardMark boardTitle={boardTitle} logo={logo} />
171
90
  </a>
@@ -201,28 +120,7 @@ export function Header({
201
120
 
202
121
  <div className="ms-auto flex min-w-0 shrink-0 items-center gap-2 sm:gap-3">{children}</div>
203
122
 
204
- {hasNavigation && (
205
- <nav aria-label={c('sections')} data-nav-view="mobile" className="lg:hidden">
206
- <details data-nav-disclosure className="group/menu">
207
- <summary
208
- 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`}
209
- >
210
- <MenuGlyph />
211
- <span className="sr-only">{c('sections')}</span>
212
- </summary>
213
-
214
- <div
215
- 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`}
216
- >
217
- <ul className={`${PAGE} flex flex-col gap-0.5 py-2`}>
218
- {navigation.map((item) => (
219
- <MobileNavItem key={item.href} item={item} />
220
- ))}
221
- </ul>
222
- </div>
223
- </details>
224
- </nav>
225
- )}
123
+ <MobileHeaderNav boardTitle={boardTitle} navigation={navigation} copy={copy} />
226
124
  </div>
227
125
  </header>
228
126
  )
@@ -0,0 +1,80 @@
1
+ import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy, linkTarget } from '@meith/theme-kit'
3
+ import { NavigationDrawer, NavigationDrawerTrigger } from '@meith/ui'
4
+
5
+ const LINK =
6
+ 'flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-foreground hover:bg-muted'
7
+
8
+ export function MobileHeaderNav({
9
+ boardTitle,
10
+ navigation,
11
+ copy,
12
+ }: Pick<HeaderModel, 'boardTitle' | 'navigation'> & { copy: SlotCopy }) {
13
+ const c = (key: string) => fromSlotCopy(copy, `default.header.${key}`)
14
+ return (
15
+ <nav data-nav-view="mobile" aria-label={c('sections')} className="lg:hidden">
16
+ {(['usercp', 'modcp'] as const).map((panel) => (
17
+ <NavigationDrawerTrigger
18
+ key={panel}
19
+ target={`panel-${panel}-navigation`}
20
+ data-panel-nav-trigger={panel}
21
+ label={fromSlotCopy(copy, 'default.panelNav.open')}
22
+ />
23
+ ))}
24
+ {navigation.length > 0 && (
25
+ <div data-main-navigation>
26
+ <NavigationDrawer
27
+ id="board-mobile-navigation"
28
+ title={boardTitle}
29
+ openLabel={c('openMenu')}
30
+ closeLabel={c('closeMenu')}
31
+ >
32
+ <ul className="flex flex-col gap-1">
33
+ {navigation.map((item) => (
34
+ <li key={item.href}>
35
+ {item.submenu !== undefined && item.submenu.length > 0 ? (
36
+ <details className="group/section">
37
+ <summary
38
+ className={`${LINK} cursor-pointer list-none justify-between gap-3 select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none`}
39
+ >
40
+ <span className="min-w-0 break-words">{item.label}</span>
41
+ <svg
42
+ aria-hidden="true"
43
+ viewBox="0 0 16 16"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ strokeWidth="1.5"
47
+ className="size-4 shrink-0 text-muted-foreground transition-transform group-open/section:rotate-90"
48
+ >
49
+ <path d="m6 4 4 4-4 4" />
50
+ </svg>
51
+ </summary>
52
+ <ul className="my-1 ms-3 flex flex-col gap-1 border-s border-border ps-2">
53
+ <li>
54
+ <a href={item.href} {...linkTarget(item)} className={LINK}>
55
+ {c('overview')}
56
+ </a>
57
+ </li>
58
+ {item.submenu.map((child) => (
59
+ <li key={child.href}>
60
+ <a href={child.href} {...linkTarget(child)} className={LINK}>
61
+ {child.label}
62
+ </a>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ </details>
67
+ ) : (
68
+ <a href={item.href} {...linkTarget(item)} className={LINK}>
69
+ {item.label}
70
+ </a>
71
+ )}
72
+ </li>
73
+ ))}
74
+ </ul>
75
+ </NavigationDrawer>
76
+ </div>
77
+ )}
78
+ </nav>
79
+ )
80
+ }
@@ -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 rounded-full')
20
+ const step = cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'min-w-20')
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 rounded-full px-2',
56
+ 'min-w-8 px-2 pointer-coarse:min-w-11',
57
57
  NUMERIC,
58
58
  entry.isCurrent ? '' : 'hidden sm:inline-flex',
59
59
  )}
@@ -1,12 +1,11 @@
1
1
  import type { PanelNavItemModel, PanelNavModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { cn, Disclosure } from '@meith/ui'
3
+ import { cn, NavigationDrawer } from '@meith/ui'
4
4
 
5
5
  import { PanelIcon } from '../panel-icons'
6
- import { BELOW_HEADER } from '../shared'
7
6
 
8
7
  const ITEM =
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'
8
+ 'group/item relative flex items-center gap-3 rounded-lg px-3 py-2 pointer-coarse:min-h-11 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
10
9
 
11
10
  const HERE = 'bg-primary/10 font-semibold text-primary [&_svg]:text-primary'
12
11
  const OPEN = 'font-semibold text-foreground hover:bg-muted'
@@ -30,26 +29,30 @@ function Count({ count, copy }: { count: number; copy: SlotCopy }) {
30
29
  )
31
30
  }
32
31
 
33
- function Item({
34
- item,
35
- className,
36
- copy,
37
- }: {
38
- item: PanelNavItemModel
39
- className: string
40
- copy: SlotCopy
41
- }) {
42
- const body = (
32
+ function ItemContent({ item, copy }: { item: PanelNavItemModel; copy: SlotCopy }) {
33
+ return (
43
34
  <>
44
35
  {item.icon !== null && (
45
36
  <span className="text-muted-foreground/70 transition-colors group-hover/item:text-foreground">
46
37
  <PanelIcon icon={item.icon} />
47
38
  </span>
48
39
  )}
49
- <span className="min-w-0 flex-1 truncate">{item.title}</span>
40
+ <span className="min-w-0 flex-1 text-start [overflow-wrap:anywhere]">{item.title}</span>
50
41
  {item.count !== null && <Count count={item.count} copy={copy} />}
51
42
  </>
52
43
  )
44
+ }
45
+
46
+ function Item({
47
+ item,
48
+ className,
49
+ copy,
50
+ }: {
51
+ item: PanelNavItemModel
52
+ className: string
53
+ copy: SlotCopy
54
+ }) {
55
+ const body = <ItemContent item={item} copy={copy} />
53
56
 
54
57
  if (item.isRecord) {
55
58
  return (
@@ -113,28 +116,94 @@ function Sections({
113
116
  )
114
117
  }
115
118
 
116
- export function PanelNav({
119
+ export function MobilePanelNav({
120
+ panel,
117
121
  label,
118
122
  sections,
119
- currentTitle,
120
123
  copy,
121
124
  }: PanelNavModel & { copy: SlotCopy }) {
125
+ const c = (key: string) => fromSlotCopy(copy, `default.panelNav.${key}`)
126
+
122
127
  return (
123
- <>
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`}
128
+ <div className="lg:hidden">
129
+ <NavigationDrawer
130
+ showTrigger={false}
131
+ id={`panel-${panel}-navigation`}
132
+ title={c(`title.${panel}`)}
133
+ openLabel={c('open')}
134
+ closeLabel={c('close')}
126
135
  >
127
- <Disclosure
128
- summary={label}
129
- contentClassName="p-2"
130
- {...(currentTitle === null ? {} : { aside: currentTitle })}
131
- >
132
- <Sections label={label} sections={sections} copy={copy} />
133
- </Disclosure>
134
- </div>
136
+ <nav aria-label={label} className="text-sm">
137
+ <ul className="flex flex-col gap-1">
138
+ {sections.map((section) => (
139
+ <li
140
+ key={section.href}
141
+ className={cn(section.isOverview && 'mb-2 border-b border-border pb-2')}
142
+ >
143
+ {section.children.length > 0 ? (
144
+ <details open={section.isOpen} className="group/section">
145
+ <summary
146
+ className={cn(
147
+ ITEM,
148
+ 'min-h-11 cursor-pointer list-none [&::-webkit-details-marker]:hidden',
149
+ section.isOpen ? OPEN : ELSEWHERE,
150
+ )}
151
+ >
152
+ <ItemContent item={section} copy={copy} />
153
+ <svg
154
+ aria-hidden="true"
155
+ viewBox="0 0 16 16"
156
+ className="size-4 shrink-0 transition-transform group-open/section:rotate-90"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ strokeWidth="1.5"
160
+ strokeLinecap="round"
161
+ strokeLinejoin="round"
162
+ >
163
+ <path d="m6 4 4 4-4 4" />
164
+ </svg>
165
+ </summary>
166
+ <ul className="my-1 ms-5 flex flex-col gap-1 border-s-2 border-border ps-2">
167
+ <li>
168
+ <Item
169
+ item={{ ...section, title: c('overview'), icon: null, count: null }}
170
+ className={cn('min-h-11', section.current === 'here' ? HERE : ELSEWHERE)}
171
+ copy={copy}
172
+ />
173
+ </li>
174
+ {section.children.map((child) => (
175
+ <li key={child.href}>
176
+ <Item
177
+ item={child}
178
+ className={cn('min-h-11', child.current === 'here' ? HERE : ELSEWHERE)}
179
+ copy={copy}
180
+ />
181
+ </li>
182
+ ))}
183
+ </ul>
184
+ </details>
185
+ ) : (
186
+ <Item
187
+ item={section}
188
+ className={cn('min-h-11', section.current === 'here' ? HERE : ELSEWHERE)}
189
+ copy={copy}
190
+ />
191
+ )}
192
+ </li>
193
+ ))}
194
+ </ul>
195
+ </nav>
196
+ </NavigationDrawer>
197
+ </div>
198
+ )
199
+ }
135
200
 
201
+ export function PanelNav(props: PanelNavModel & { copy: SlotCopy }) {
202
+ return (
203
+ <>
204
+ <MobilePanelNav {...props} />
136
205
  <div className="hidden rounded-xl border border-border bg-card p-2 shadow-elevation lg:block">
137
- <Sections label={label} sections={sections} copy={copy} />
206
+ <Sections label={props.label} sections={props.sections} copy={props.copy} />
138
207
  </div>
139
208
  </>
140
209
  )
@@ -1,8 +1,8 @@
1
1
  import type { PanelPageModel, PanelSectionModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { cn } from '@meith/ui'
3
+ import { cn, PageHeader, PageHeaderActions, PageHeaderContent, PageTitle } from '@meith/ui'
4
4
 
5
- import { MUTED_LINK, PAGE, PAGE_TITLE, pageAt, SECTION_TITLE } from '../shared'
5
+ import { MUTED_LINK, PAGE, pageAt, SECTION_TITLE } from '../shared'
6
6
 
7
7
  export function PanelPage({
8
8
  title,
@@ -28,8 +28,8 @@ export function PanelPage({
28
28
  gap === 'loose' ? 'gap-8' : 'gap-6',
29
29
  )}
30
30
  >
31
- <div className="flex flex-wrap items-end justify-between gap-x-6 gap-y-3 border-b border-border pb-5">
32
- <div className="flex min-w-0 flex-col gap-1">
31
+ <PageHeader className="border-b border-border pb-5">
32
+ <PageHeaderContent>
33
33
  {back !== null && (
34
34
  <a
35
35
  href={back.href}
@@ -39,7 +39,7 @@ export function PanelPage({
39
39
  </a>
40
40
  )}
41
41
 
42
- <h1 className={PAGE_TITLE}>{title}</h1>
42
+ <PageTitle>{title}</PageTitle>
43
43
 
44
44
  {regions.lede !== undefined && (
45
45
  <p className="max-w-prose text-sm text-muted-foreground">{regions.lede}</p>
@@ -47,12 +47,10 @@ export function PanelPage({
47
47
  {regions.meta !== undefined && (
48
48
  <p className="text-xs text-muted-foreground">{regions.meta}</p>
49
49
  )}
50
- </div>
50
+ </PageHeaderContent>
51
51
 
52
- {regions.actions !== undefined && (
53
- <div className="flex shrink-0 flex-wrap items-center gap-2">{regions.actions}</div>
54
- )}
55
- </div>
52
+ {regions.actions !== undefined && <PageHeaderActions>{regions.actions}</PageHeaderActions>}
53
+ </PageHeader>
56
54
 
57
55
  {children}
58
56
  </main>
@@ -3,7 +3,7 @@ import type { PanelShellModel } from '@meith/theme-kit'
3
3
  import { EYEBROW, MUTED_LINK } from '../shared'
4
4
 
5
5
  const RAIL =
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'
6
+ 'flex flex-col gap-4 lg:px-6 lg:sticky lg:w-64 lg:shrink-0 lg:self-start lg:pt-8 lg:pr-0'
7
7
 
8
8
  function SwitchGlyph() {
9
9
  return (
@@ -24,7 +24,7 @@ function SwitchGlyph() {
24
24
 
25
25
  export function PanelShell({ links, linksLabel, regions, children }: PanelShellModel) {
26
26
  return (
27
- <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col lg:flex-row lg:gap-8">
27
+ <div className="mx-auto flex w-full max-w-7xl flex-1 flex-col lg:flex-row lg:gap-8">
28
28
  <aside className={`${RAIL} lg:top-14`}>
29
29
  {regions.nav}
30
30
 
@@ -237,7 +237,7 @@ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { co
237
237
  ) : (
238
238
  <div className={`flex-1 py-4 sm:py-5 ${BODY_X}`}>
239
239
  <div
240
- className="prose-md text-[0.9375rem] sm:text-base"
240
+ className="prose-md max-w-prose text-[0.9375rem] sm:text-base"
241
241
  dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
242
242
  />
243
243
 
@@ -1,8 +1,8 @@
1
1
  import type { PostFormModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { Alert, AlertDescription, AlertTitle, buttonVariants, Card, cn } from '@meith/ui'
3
+ import { Alert, AlertDescription, AlertTitle, buttonVariants, Card, cn, PageTitle } from '@meith/ui'
4
4
 
5
- import { PAGE_TITLE, pageAt } from '../shared'
5
+ import { pageAt } from '../shared'
6
6
 
7
7
  export function PostForm({
8
8
  heading,
@@ -17,7 +17,7 @@ export function PostForm({
17
17
  return (
18
18
  <div className={`${pageAt('max-w-3xl')} flex w-full flex-col gap-5 py-6 sm:py-8`}>
19
19
  <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
20
- <h1 className={PAGE_TITLE}>{heading}</h1>
20
+ <PageTitle>{heading}</PageTitle>
21
21
  <a href={cancelHref} className={cn(buttonVariants({ variant: 'ghost', size: 'sm' }))}>
22
22
  {cancelLabel}
23
23
  </a>
@@ -1,6 +1,6 @@
1
1
  import type { RedirectNoticeModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { buttonVariants, Card, CardContent } from '@meith/ui'
3
+ import { buttonVariants, Card, CardContent, PageTitle } from '@meith/ui'
4
4
 
5
5
  export function RedirectNotice({
6
6
  message,
@@ -16,7 +16,7 @@ export function RedirectNotice({
16
16
  <p className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
17
17
  {c('redirecting')}
18
18
  </p>
19
- <h1 className="mt-1 text-2xl font-semibold tracking-tight">{c('pleaseWait')}</h1>
19
+ <PageTitle className="mt-1 sm:text-2xl">{c('pleaseWait')}</PageTitle>
20
20
  <p className="mt-2 text-sm text-muted-foreground">{message}</p>
21
21
 
22
22
  <div className="mt-5 flex flex-wrap items-center gap-3">
@@ -13,6 +13,11 @@ import {
13
13
  Field,
14
14
  Input,
15
15
  NativeSelect,
16
+ NavTabs,
17
+ PageDescription,
18
+ PageHeader,
19
+ PageHeaderContent,
20
+ PageTitle,
16
21
  } from '@meith/ui'
17
22
 
18
23
  import { LINK, pageAt, Stamp } from '../shared'
@@ -37,16 +42,18 @@ export function SearchResults({
37
42
  tabIndex={-1}
38
43
  className={`${pageAt('max-w-3xl')} flex flex-1 flex-col gap-6 py-8`}
39
44
  >
40
- <div className="flex flex-col gap-1">
41
- <h1 className="font-heading text-2xl font-semibold">
42
- {c('resultsFor')} {c('openQuote')}
43
- {terms}
44
- {c('closeQuote')}
45
- </h1>
46
- <p className="text-sm text-muted-foreground">
47
- {c('searched')} <Stamp at={searchedAt} />. {c('searchedNote')}
48
- </p>
49
- </div>
45
+ <PageHeader>
46
+ <PageHeaderContent>
47
+ <PageTitle>
48
+ {c('resultsFor')} {c('openQuote')}
49
+ {terms}
50
+ {c('closeQuote')}
51
+ </PageTitle>
52
+ <PageDescription>
53
+ {c('searched')} <Stamp at={searchedAt} />. {c('searchedNote')}
54
+ </PageDescription>
55
+ </PageHeaderContent>
56
+ </PageHeader>
50
57
 
51
58
  {refine !== undefined && <Refine {...refine} copy={copy} />}
52
59
 
@@ -147,32 +154,13 @@ function Refine({
147
154
  <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
148
155
  <p className="text-sm font-medium text-foreground">{summary}</p>
149
156
 
150
- <nav
151
- aria-label={sortsLabel}
152
- className="inline-flex items-center gap-1 rounded-lg border border-border bg-surface p-1"
153
- >
154
- {sorts.map((sort) => (
155
- <a
156
- key={sort.label}
157
- href={sort.href}
158
- {...(sort.isCurrent ? { 'aria-current': 'true' as const } : {})}
159
- className={cn(
160
- 'rounded-md px-2.5 py-1 text-xs whitespace-nowrap transition-colors',
161
- sort.isCurrent
162
- ? 'bg-card font-semibold text-foreground shadow-sm'
163
- : 'font-medium text-muted-foreground hover:text-foreground',
164
- )}
165
- >
166
- {sort.label}
167
- </a>
168
- ))}
169
- </nav>
157
+ <NavTabs label={sortsLabel} tabs={sorts} />
170
158
 
171
159
  {applied.map((chip) => (
172
160
  <a
173
161
  key={chip.label}
174
162
  href={chip.removeHref}
175
- className="inline-flex items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-2.5 py-1 text-xs font-medium text-foreground hover:bg-primary/20"
163
+ className="inline-flex min-h-8 pointer-coarse:min-h-11 items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-2.5 py-1 text-xs font-medium text-foreground hover:bg-primary/20"
176
164
  >
177
165
  {chip.label}
178
166
  <span aria-hidden="true">{c('removeFilterX')}</span>
@@ -184,7 +172,7 @@ function Refine({
184
172
  <form
185
173
  method="get"
186
174
  action={action}
187
- className="flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-border pt-2.5"
175
+ className="grid grid-cols-1 items-end gap-3 border-t border-border pt-3 sm:grid-cols-2"
188
176
  >
189
177
  {choices.map((choice) => (
190
178
  <Filter
@@ -197,7 +185,7 @@ function Refine({
197
185
 
198
186
  <button
199
187
  type="submit"
200
- className={cn(buttonVariants({ variant: 'secondary', size: 'sm' }), 'h-8')}
188
+ className={cn(buttonVariants({ variant: 'secondary' }), 'justify-self-start')}
201
189
  >
202
190
  {submitLabel}
203
191
  </button>
@@ -227,16 +215,11 @@ function Filter({
227
215
  const id = `filter-${name}`
228
216
 
229
217
  return (
230
- <span className="inline-flex items-center gap-1.5">
218
+ <span className="flex min-w-0 flex-col gap-2">
231
219
  <label htmlFor={id} className="text-xs font-medium text-muted-foreground">
232
220
  {label}
233
221
  </label>
234
- <NativeSelect
235
- id={id}
236
- name={name}
237
- defaultValue={selected?.value ?? ''}
238
- className="h-8 w-auto min-w-0 py-0 text-xs"
239
- >
222
+ <NativeSelect id={id} name={name} defaultValue={selected?.value ?? ''} className="w-full">
240
223
  {options.map((option) => (
241
224
  <option key={option.value} value={option.value}>
242
225
  {option.label}
@@ -19,7 +19,7 @@ export function ThreadRow({
19
19
  <li
20
20
  data-unread={thread.isUnread ? '' : undefined}
21
21
  data-visibility={hidden ? thread.visibility : undefined}
22
- className={`grid grid-cols-[auto_minmax(0,1fr)] gap-x-3.5 gap-y-2 px-4 py-3.5 transition-colors hover:bg-muted/50 sm:px-5 md:grid-cols-[auto_minmax(0,1fr)_9rem_15rem] md:items-center md:gap-x-5 ${hidden ? tint : ''}`}
22
+ className={`grid grid-cols-[auto_minmax(0,1fr)] gap-x-3.5 gap-y-2 px-4 py-4 transition-colors hover:bg-muted/50 sm:px-5 @3xl/card:grid-cols-[auto_minmax(0,1fr)_6rem_13rem] @3xl/card:items-center @3xl/card:gap-x-5 ${hidden ? tint : ''}`}
23
23
  >
24
24
  <span className="flex items-center gap-2.5">
25
25
  {select !== null && (
@@ -41,7 +41,7 @@ export function ThreadRow({
41
41
  </span>
42
42
  </span>
43
43
 
44
- <div className="min-w-0">
44
+ <div className="min-w-0 [overflow-wrap:anywhere]">
45
45
  <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
46
46
  {thread.prefix !== null && <Prefix prefix={thread.prefix} />}
47
47
  {thread.isSticky && <Badge tone="pinned">{c('pinned')}</Badge>}
@@ -68,9 +68,9 @@ export function ThreadRow({
68
68
  </p>
69
69
  </div>
70
70
 
71
- <div className="col-start-2 flex min-w-0 flex-col gap-y-1 text-xs text-muted-foreground md:contents">
71
+ <div className="col-start-2 flex min-w-0 flex-col gap-y-1 text-xs text-muted-foreground @3xl/card:contents">
72
72
  <Figures
73
- className="md:col-start-3 md:justify-self-end"
73
+ className="@3xl/card:col-start-3 @3xl/card:justify-self-end"
74
74
  items={[
75
75
  {
76
76
  label: c('repliesLabel'),
@@ -87,18 +87,18 @@ export function ThreadRow({
87
87
  ]}
88
88
  />
89
89
 
90
- <div className="order-first flex min-w-0 max-w-full flex-wrap gap-x-1 md:order-none md:col-start-4 md:block md:border-l md:border-border md:pl-5">
90
+ <div className="order-first flex min-w-0 max-w-full flex-wrap gap-x-1 @3xl/card:order-none @3xl/card:col-start-4 @3xl/card:block @3xl/card:border-l @3xl/card:border-border @3xl/card:pl-5">
91
91
  {thread.lastPost === null ? (
92
92
  <span className="text-thread-moved">{c('noRepliesYet')}</span>
93
93
  ) : (
94
94
  <>
95
95
  <a
96
96
  href={thread.lastPost.href}
97
- className={`font-medium text-foreground md:block ${LINK}`}
97
+ className={`font-medium text-foreground @3xl/card:block ${LINK}`}
98
98
  >
99
99
  {c('latestReply')}
100
100
  </a>
101
- <span className="md:mt-0.5 md:block md:truncate">
101
+ <span className="@3xl/card:mt-0.5 @3xl/card:block @3xl/card:truncate">
102
102
  {c('by')} <UserRef user={thread.lastPost.author} className="font-normal" />{' '}
103
103
  {c('dot')} <Stamp at={thread.lastPost.at} />
104
104
  </span>
@@ -1,8 +1,16 @@
1
1
  import type { SlotCopy, ThreadViewModel } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { Badge, buttonVariants, cn } from '@meith/ui'
3
+ import {
4
+ Badge,
5
+ buttonVariants,
6
+ cn,
7
+ PageHeader,
8
+ PageHeaderActions,
9
+ PageHeaderContent,
10
+ PageTitle,
11
+ } from '@meith/ui'
4
12
 
5
- import { Counts, PAGE_BODY, PAGE_TITLE, Prefix } from '../shared'
13
+ import { Counts, PAGE_BODY, Prefix } from '../shared'
6
14
 
7
15
  export function ThreadView({
8
16
  thread,
@@ -16,9 +24,9 @@ export function ThreadView({
16
24
 
17
25
  return (
18
26
  <div className={PAGE_BODY}>
19
- <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
20
- <div className="min-w-0">
21
- <h1 className={PAGE_TITLE}>{thread.title}</h1>
27
+ <PageHeader>
28
+ <PageHeaderContent>
29
+ <PageTitle>{thread.title}</PageTitle>
22
30
 
23
31
  <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
24
32
  {thread.prefix !== null && <Prefix prefix={thread.prefix} />}
@@ -43,9 +51,9 @@ export function ThreadView({
43
51
  ]}
44
52
  />
45
53
  </div>
46
- </div>
54
+ </PageHeaderContent>
47
55
 
48
- <div className="flex shrink-0 flex-wrap items-center gap-2">
56
+ <PageHeaderActions>
49
57
  {watch != null && (
50
58
  <form action={watch.action} method="post">
51
59
  <button type="submit" className={buttonVariants({ variant: 'outline' })}>
@@ -74,8 +82,8 @@ export function ThreadView({
74
82
  {c('replyAction')}
75
83
  </a>
76
84
  )}
77
- </div>
78
- </div>
85
+ </PageHeaderActions>
86
+ </PageHeader>
79
87
 
80
88
  {regions.tools !== undefined && (
81
89
  <div className="-mb-1 flex flex-col gap-3 empty:hidden">{regions.tools}</div>
package/src/theme.ts CHANGED
@@ -70,7 +70,7 @@ import { WhoIsOnline } from './slots/who-is-online'
70
70
  export const defaultTheme = defineTheme({
71
71
  key: 'default',
72
72
  title: 'Default',
73
- version: '0.35.1',
73
+ version: '0.36.0',
74
74
  slots: {
75
75
  Shell,
76
76
  Header,