@meith/theme-default 0.33.2 → 0.33.4

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.33.2",
3
+ "version": "0.33.4",
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.33.2",
24
- "@meith/ui": "^0.33.2"
23
+ "@meith/theme-kit": "^0.33.4",
24
+ "@meith/ui": "^0.33.4"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^19.2.0"
package/src/shared.tsx CHANGED
@@ -11,7 +11,7 @@ const PAGE_WIDTH = 'max-w-6xl'
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-6 sm:py-8`
14
+ export const PAGE_BODY = `${PAGE} flex w-full flex-col gap-5 py-5 sm:py-6`
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`
@@ -24,10 +24,22 @@ export const PRIMARY_HEADER = 'border-b-primary/15 bg-primary/6'
24
24
 
25
25
  export const MUTED_LINK = `text-muted-foreground ${LINK}`
26
26
 
27
+ export const BELOW_HEADER = 'top-14'
28
+
29
+ export const PAGE_TITLE =
30
+ 'font-heading text-xl font-semibold tracking-tight text-balance sm:text-2xl'
31
+
32
+ export const SECTION_TITLE = 'font-heading text-lg font-semibold tracking-tight text-foreground'
33
+
34
+ export const EYEBROW = 'text-xs font-semibold tracking-wide text-muted-foreground uppercase'
35
+
36
+ export const PILL =
37
+ 'inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground'
38
+
27
39
  export function UserRef({ user, className }: { user: UserRefModel; className?: string }) {
28
40
  const classes = cn(
29
41
  'font-medium',
30
- user.nameClass == null && 'text-primary',
42
+ user.nameClass == null && 'text-foreground',
31
43
  className,
32
44
  user.nameClass,
33
45
  )
@@ -49,14 +61,48 @@ export function Stamp({ at, className }: { at: TimeModel; className?: string })
49
61
  )
50
62
  }
51
63
 
52
- export function UnreadDot() {
53
- return <span aria-hidden="true" className="mt-1.5 size-2 shrink-0 rounded-full bg-forum-unread" />
64
+ export function UnreadDot({ className }: { className?: string }) {
65
+ return (
66
+ <span
67
+ aria-hidden="true"
68
+ className={cn('size-2 shrink-0 rounded-full bg-primary ring-4 ring-primary/15', className)}
69
+ />
70
+ )
54
71
  }
55
72
 
56
73
  export function ReadSpacer() {
57
74
  return <span aria-hidden="true" className="mt-1.5 size-2 shrink-0" />
58
75
  }
59
76
 
77
+ export function Tile({
78
+ label,
79
+ unread,
80
+ className,
81
+ children,
82
+ }: {
83
+ label: string
84
+ unread?: boolean
85
+ className?: string
86
+ children?: React.ReactNode
87
+ }) {
88
+ const initial = (Array.from(label.trim())[0] ?? '?').toUpperCase()
89
+
90
+ return (
91
+ <span
92
+ aria-hidden="true"
93
+ className={cn(
94
+ 'flex size-10 shrink-0 items-center justify-center rounded-lg text-base font-semibold select-none',
95
+ unread
96
+ ? 'bg-primary text-primary-foreground shadow-sm'
97
+ : 'bg-primary/10 text-primary ring-1 ring-primary/15 ring-inset',
98
+ className,
99
+ )}
100
+ >
101
+ {children ?? initial}
102
+ </span>
103
+ )
104
+ }
105
+
60
106
  export function isEmptyRegion(node: React.ReactNode): boolean {
61
107
  if (node === null || node === undefined || node === false || node === '') return true
62
108
  return Array.isArray(node) && node.every((child) => isEmptyRegion(child))
@@ -104,7 +150,30 @@ export function Counts({ items, className }: { items: readonly CountItem[]; clas
104
150
  <div key={item.label}>
105
151
  <dt className="sr-only">{item.label}</dt>
106
152
  <dd>
107
- <span className={cn('font-semibold text-primary', NUMERIC)}>{item.value.label}</span>{' '}
153
+ <span className={cn('font-semibold text-foreground', NUMERIC)}>{item.value.label}</span>{' '}
154
+ {item.value.value === 1 ? item.one : item.many}
155
+ </dd>
156
+ </div>
157
+ ))}
158
+ </dl>
159
+ )
160
+ }
161
+
162
+ export function Figures({ items, className }: { items: readonly CountItem[]; className?: string }) {
163
+ return (
164
+ <dl
165
+ 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',
167
+ className,
168
+ )}
169
+ >
170
+ {items.map((item) => (
171
+ <div key={item.label} className="flex items-baseline gap-x-1 md:flex-col md:gap-0">
172
+ <dt className="sr-only">{item.label}</dt>
173
+ <dd className={cn('font-semibold text-foreground md:text-sm', NUMERIC)}>
174
+ {item.value.label}
175
+ </dd>
176
+ <dd className="md:text-[0.6875rem] md:leading-4">
108
177
  {item.value.value === 1 ? item.one : item.many}
109
178
  </dd>
110
179
  </div>
@@ -15,14 +15,14 @@ export function Announcement({
15
15
  const c = (key: string) => fromSlotCopy(copy, `default.announcement.${key}`)
16
16
 
17
17
  return (
18
- <Card as="article" className="border-l-4 border-l-primary">
19
- <CardContent className="flex flex-col gap-2 p-4">
20
- <h2 className="text-lg font-semibold tracking-tight text-balance">{title}</h2>
18
+ <Card as="article" className="rounded-xl border-l-4 border-l-primary">
19
+ <CardContent className="flex flex-col gap-2 p-5">
20
+ <h2 className="font-heading text-lg font-semibold tracking-tight text-balance">{title}</h2>
21
21
 
22
22
  <div className="prose-md text-sm" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
23
23
  </CardContent>
24
24
 
25
- <CardFooter>
25
+ <CardFooter className="px-5">
26
26
  <span>
27
27
  {postedBy === null ? (
28
28
  c('posted')
@@ -7,11 +7,13 @@ export function AuthPage({ title, alert, links, regions }: AuthPageModel) {
7
7
  <main
8
8
  id="board-content"
9
9
  tabIndex={-1}
10
- className="flex flex-1 flex-col items-center justify-center px-6 py-12"
10
+ className="flex flex-1 flex-col items-center justify-center px-4 py-10 sm:px-6 sm:py-14"
11
11
  >
12
- <div className="flex w-full max-w-sm flex-col gap-6 rounded-lg border border-border bg-card p-6 text-card-foreground shadow-elevation">
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
13
  <div className="flex flex-col gap-1">
14
- <h1 className="font-heading text-2xl font-semibold text-foreground">{title}</h1>
14
+ <h1 className="font-heading text-2xl font-semibold tracking-tight text-foreground">
15
+ {title}
16
+ </h1>
15
17
  {regions.lede !== undefined && (
16
18
  <p className="text-sm text-muted-foreground">{regions.lede}</p>
17
19
  )}
@@ -20,7 +22,7 @@ export function AuthPage({ title, alert, links, regions }: AuthPageModel) {
20
22
  {alert !== null && (
21
23
  <p
22
24
  role="alert"
23
- className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm"
25
+ className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm"
24
26
  >
25
27
  {alert}
26
28
  </p>
@@ -33,7 +35,7 @@ export function AuthPage({ title, alert, links, regions }: AuthPageModel) {
33
35
  {regions.form}
34
36
 
35
37
  {links.length > 0 && (
36
- <div className="flex flex-col gap-1 text-sm text-muted-foreground">
38
+ <div className="flex flex-col gap-1.5 border-t border-border pt-5 text-sm text-muted-foreground">
37
39
  {links.map((link) => (
38
40
  <span key={link.href}>
39
41
  {link.lead === null ? null : `${link.lead} `}
@@ -22,8 +22,8 @@ export function BoardIndex({
22
22
  )}
23
23
 
24
24
  {markAllReadAction !== null && (
25
- <form action={markAllReadAction} method="post" className="-mb-2 self-end">
26
- <button type="submit" className={buttonVariants({ variant: 'outline', size: 'sm' })}>
25
+ <form action={markAllReadAction} method="post" className="-mb-3 self-end">
26
+ <button type="submit" className={buttonVariants({ variant: 'ghost', size: 'sm' })}>
27
27
  {c('markAllRead')}
28
28
  </button>
29
29
  </form>
@@ -32,14 +32,14 @@ export function BoardIndex({
32
32
  <div
33
33
  className={
34
34
  rail
35
- ? 'grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem] lg:items-start'
36
- : 'flex min-w-0 flex-col gap-4'
35
+ ? 'grid gap-6 lg:grid-cols-[minmax(0,1fr)_19rem] lg:items-start'
36
+ : 'flex min-w-0 flex-col gap-5'
37
37
  }
38
38
  >
39
- <div className="flex min-w-0 flex-col gap-4">{regions.categories}</div>
39
+ <div className="flex min-w-0 flex-col gap-5">{regions.categories}</div>
40
40
 
41
41
  {rail && (
42
- <aside aria-label={c('boardActivity')} className="flex min-w-0 flex-col gap-4">
42
+ <aside aria-label={c('boardActivity')} className="flex min-w-0 flex-col gap-5">
43
43
  {latest}
44
44
  {regions.stats}
45
45
  </aside>
@@ -47,7 +47,7 @@ export function BoardIndex({
47
47
  </div>
48
48
 
49
49
  {footer && (
50
- <div className="flex flex-col gap-1.5 border-t border-border pt-4 text-xs text-muted-foreground">
50
+ <div className="flex flex-col gap-1.5 rounded-xl border border-border bg-card px-4 py-3 text-xs text-muted-foreground shadow-elevation">
51
51
  {regions.online}
52
52
  </div>
53
53
  )}
@@ -2,7 +2,7 @@ import type { BoardStatsModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
3
  import { Card, CardContent, CardHeader, CardTitle } from '@meith/ui'
4
4
 
5
- import { NUMERIC, PRIMARY_HEADER, Stamp, UserRef } from '../shared'
5
+ import { NUMERIC, Stamp, UserRef } from '../shared'
6
6
 
7
7
  export function BoardStats({
8
8
  threadCount,
@@ -15,14 +15,14 @@ export function BoardStats({
15
15
  const c = (key: string) => fromSlotCopy(copy, `default.boardStats.${key}`)
16
16
 
17
17
  return (
18
- <Card aria-labelledby="board-stats-heading">
19
- <CardHeader className={PRIMARY_HEADER}>
20
- <CardTitle id="board-stats-heading" className="text-primary">
18
+ <Card aria-labelledby="board-stats-heading" className="rounded-xl">
19
+ <CardHeader className="bg-card">
20
+ <CardTitle id="board-stats-heading" className="text-sm">
21
21
  {c('heading')}
22
22
  </CardTitle>
23
23
  </CardHeader>
24
24
 
25
- <CardContent className="flex flex-col gap-3">
25
+ <CardContent className="flex flex-col gap-3 px-5 py-4">
26
26
  {computedAt === null ? (
27
27
  <p className="text-xs text-muted-foreground">{c('notComputed')}</p>
28
28
  ) : (
@@ -33,11 +33,13 @@ export function BoardStats({
33
33
  { label: c('posts'), value: postCount },
34
34
  { label: c('members'), value: memberCount },
35
35
  ].map((figure) => (
36
- <div key={figure.label}>
37
- <dd className={`text-sm font-medium text-foreground ${NUMERIC}`}>
36
+ <div key={figure.label} className="rounded-lg bg-muted/60 px-2 py-2.5">
37
+ <dd className={`text-lg font-semibold text-foreground ${NUMERIC}`}>
38
38
  {figure.value.label}
39
39
  </dd>
40
- <dt className="text-xs text-muted-foreground">{figure.label}</dt>
40
+ <dt className="text-[0.6875rem] text-muted-foreground uppercase">
41
+ {figure.label}
42
+ </dt>
41
43
  </div>
42
44
  ))}
43
45
  </dl>
@@ -1,21 +1,22 @@
1
1
  import type { CategoryBlockModel } from '@meith/theme-kit'
2
2
  import { Card, CardDescription, CardHeader, CardRows, CardTitle } from '@meith/ui'
3
3
 
4
- import { LINK, PRIMARY_HEADER } from '../shared'
4
+ import { LINK } from '../shared'
5
5
 
6
6
  export function CategoryBlock({ category, children }: CategoryBlockModel) {
7
7
  const headingId = `category-${category.id}`
8
8
 
9
9
  return (
10
- <Card aria-labelledby={headingId}>
11
- <CardHeader className={`flex-col items-start gap-0.5 ${PRIMARY_HEADER}`}>
12
- <CardTitle id={headingId} className="text-primary">
13
- <a href={category.href} className={`${LINK} hover:text-primary-hover`}>
10
+ <Card aria-labelledby={headingId} className="rounded-xl">
11
+ <CardHeader className="flex-col items-start gap-0 bg-card px-5 py-3">
12
+ <CardTitle id={headingId} className="flex items-center gap-2.5 text-base">
13
+ <span aria-hidden="true" className="h-4 w-1 rounded-full bg-primary" />
14
+ <a href={category.href} className={`${LINK} text-foreground`}>
14
15
  {category.title}
15
16
  </a>
16
17
  </CardTitle>
17
18
  {category.description !== null && (
18
- <CardDescription className="text-xs">{category.description}</CardDescription>
19
+ <CardDescription className="pl-3.5 text-xs">{category.description}</CardDescription>
19
20
  )}
20
21
  </CardHeader>
21
22
 
@@ -1,8 +1,8 @@
1
1
  import type { DiscoveryViewModel, SlotCopy, TabModel } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
- import { Card, CardRows, cn, Empty, EmptyDescription } from '@meith/ui'
3
+ import { Avatar, buttonVariants, Card, CardRows, cn, Empty, EmptyDescription } from '@meith/ui'
4
4
 
5
- import { LINK, NUMERIC, pageAt, Stamp } from '../shared'
5
+ import { LINK, NUMERIC, PAGE_TITLE, PILL, pageAt, Stamp } from '../shared'
6
6
 
7
7
  const TAB =
8
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'
@@ -52,62 +52,65 @@ export function DiscoveryView({
52
52
  <main
53
53
  id="board-content"
54
54
  tabIndex={-1}
55
- className={`${pageAt('max-w-4xl')} flex flex-1 flex-col gap-6 py-8`}
55
+ className={`${pageAt('max-w-4xl')} flex flex-1 flex-col gap-6 py-6 sm:py-8`}
56
56
  >
57
57
  <div className="flex flex-col gap-1">
58
- <h1 className="font-heading text-2xl font-semibold">{title}</h1>
58
+ <h1 className={PAGE_TITLE}>{title}</h1>
59
59
  <p className="text-sm text-muted-foreground">{blurb}</p>
60
60
  </div>
61
61
 
62
62
  <Tabs label={tabsLabel} tabs={tabs} />
63
63
 
64
64
  {refusal !== null ? (
65
- <p role="alert" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">
65
+ <p role="alert" className="rounded-lg border border-border bg-card px-4 py-3 text-sm">
66
66
  {refusal.message}{' '}
67
67
  <a href={refusal.signInHref} className={`font-medium text-foreground ${LINK}`}>
68
68
  {refusal.signInLabel}
69
69
  </a>
70
70
  </p>
71
71
  ) : rows.length === 0 ? (
72
- <Card>
72
+ <Card className="rounded-xl">
73
73
  <Empty>
74
74
  <EmptyDescription>{emptyMessage}</EmptyDescription>
75
75
  </Empty>
76
76
  </Card>
77
77
  ) : (
78
- <Card>
78
+ <Card className="rounded-xl">
79
79
  <CardRows>
80
80
  {rows.map((row) => (
81
81
  <li
82
82
  key={row.threadId}
83
- className="flex flex-col gap-1.5 px-4 py-3.5 transition-colors hover:bg-muted/60 sm:flex-row sm:items-center sm:justify-between sm:gap-6"
83
+ className="flex gap-3.5 px-4 py-3.5 transition-colors hover:bg-muted/50 sm:items-center sm:px-5"
84
84
  >
85
- <div className="flex min-w-0 flex-col gap-1">
86
- <a
87
- href={row.href}
88
- className="truncate text-[0.9375rem] font-semibold text-foreground hover:underline"
89
- >
90
- {row.title}
91
- </a>
92
- <p className="truncate text-xs text-muted-foreground">
93
- {c('in')}{' '}
94
- <a href={row.forum.href} className="font-medium hover:underline">
95
- {row.forum.label}
96
- </a>{' '}
97
- {c('dot')} {c('startedBy')} {row.authorUsername}
85
+ <Avatar src={null} name={row.authorUsername} size={36} className="rounded-full" />
86
+
87
+ <div className="flex min-w-0 flex-1 flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
88
+ <div className="flex min-w-0 flex-col gap-0.5">
89
+ <a
90
+ href={row.href}
91
+ className={`truncate text-[0.9375rem] font-medium text-foreground ${LINK}`}
92
+ >
93
+ {row.title}
94
+ </a>
95
+ <p className="truncate text-xs text-muted-foreground">
96
+ {c('startedBy')} {row.authorUsername} {c('in')}{' '}
97
+ <a href={row.forum.href} className="font-medium hover:underline">
98
+ {row.forum.label}
99
+ </a>
100
+ </p>
101
+ </div>
102
+
103
+ <p className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground sm:flex-col sm:items-end sm:gap-0.5">
104
+ <span className={`${PILL} ${NUMERIC}`}>
105
+ {row.replyCount.label}{' '}
106
+ {row.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
107
+ </span>
108
+ <span>
109
+ {c('lastPost')} <Stamp at={row.lastPostAt} />
110
+ {row.lastPostUsername === null ? null : ` ${c('by')} ${row.lastPostUsername}`}
111
+ </span>
98
112
  </p>
99
113
  </div>
100
-
101
- <p className="shrink-0 text-xs text-muted-foreground sm:text-right">
102
- <span className={cn('font-semibold text-foreground', NUMERIC)}>
103
- {row.replyCount.label}
104
- </span>{' '}
105
- {row.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
106
- <span className="block">
107
- {c('lastPost')} <Stamp at={row.lastPostAt} />
108
- {row.lastPostUsername === null ? null : ` ${c('by')} ${row.lastPostUsername}`}
109
- </span>
110
- </p>
111
114
  </li>
112
115
  ))}
113
116
  </CardRows>
@@ -117,7 +120,7 @@ export function DiscoveryView({
117
120
  {nextHref !== null && (
118
121
  <a
119
122
  href={nextHref}
120
- className="inline-flex h-9 w-fit items-center rounded-full bg-secondary px-4 text-sm font-semibold text-secondary-foreground transition-colors hover:bg-accent"
123
+ className={cn(buttonVariants({ variant: 'secondary' }), 'w-fit rounded-full')}
121
124
  >
122
125
  {nextLabel} {c('nextArrow')}
123
126
  </a>
@@ -26,7 +26,7 @@ const GLYPHS: Readonly<Record<EditorTag, string>> = {
26
26
  }
27
27
 
28
28
  const BUTTON =
29
- 'min-w-8 rounded px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
29
+ 'min-w-8 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-card hover:text-foreground hover:shadow-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
30
30
 
31
31
  function runButton(textareaId: string, button: EditorToolbarButtonModel): void {
32
32
  const field = document.getElementById(textareaId)
@@ -54,7 +54,7 @@ export function EditorToolbar({
54
54
  <div
55
55
  role="group"
56
56
  aria-label={groupLabel}
57
- className="flex flex-wrap gap-0.5 border-b border-border bg-muted/40 p-2"
57
+ className="flex flex-wrap gap-0.5 border-b border-border bg-surface/60 p-2"
58
58
  >
59
59
  {buttons.map((button, index) => (
60
60
  <button
@@ -23,9 +23,9 @@ export function Footer({
23
23
  </div>
24
24
  )}
25
25
  <div
26
- className={`${PAGE} flex flex-col gap-3 py-5 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:gap-6`}
26
+ className={`${PAGE} flex flex-col gap-3 py-6 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:gap-6`}
27
27
  >
28
- <span className="font-semibold text-primary">{boardTitle}</span>
28
+ <span className="font-semibold text-foreground">{boardTitle}</span>
29
29
 
30
30
  {links.length > 0 && (
31
31
  <nav aria-label={c('nav')} className="flex flex-wrap gap-x-4 gap-y-1">
@@ -11,7 +11,7 @@ import {
11
11
  EmptyTitle,
12
12
  } from '@meith/ui'
13
13
 
14
- import { Counts, isEmptyRegion, PAGE_BODY } from '../shared'
14
+ import { Counts, isEmptyRegion, PAGE_BODY, PAGE_TITLE } from '../shared'
15
15
 
16
16
  export function ForumDisplay({
17
17
  forum,
@@ -24,37 +24,36 @@ export function ForumDisplay({
24
24
 
25
25
  return (
26
26
  <div className={PAGE_BODY}>
27
- <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
27
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
28
28
  <div className="min-w-0">
29
- <h1 className="text-2xl font-semibold tracking-tight text-balance">{forum.title}</h1>
30
- {forum.description !== null && (
31
- <p className="mt-1 max-w-prose text-sm text-muted-foreground">{forum.description}</p>
32
- )}
33
- {forum.type !== 'link' && (
34
- <Counts
35
- className="mt-2"
36
- items={[
37
- {
38
- label: c('threadsLabel'),
39
- value: forum.threadCount,
40
- one: c('thread.one'),
41
- many: c('thread.other'),
42
- },
43
- {
44
- label: c('postsLabel'),
45
- value: forum.postCount,
46
- one: c('post.one'),
47
- many: c('post.other'),
48
- },
49
- ]}
50
- />
51
- )}
29
+ <h1 className={PAGE_TITLE}>{forum.title}</h1>
30
+ <div className="mt-1 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm text-muted-foreground">
31
+ {forum.description !== null && <p className="max-w-prose">{forum.description}</p>}
32
+ {forum.type !== 'link' && (
33
+ <Counts
34
+ items={[
35
+ {
36
+ label: c('threadsLabel'),
37
+ value: forum.threadCount,
38
+ one: c('thread.one'),
39
+ many: c('thread.other'),
40
+ },
41
+ {
42
+ label: c('postsLabel'),
43
+ value: forum.postCount,
44
+ one: c('post.one'),
45
+ many: c('post.other'),
46
+ },
47
+ ]}
48
+ />
49
+ )}
50
+ </div>
52
51
  </div>
53
52
 
54
53
  <div className="flex shrink-0 flex-wrap items-center gap-2">
55
54
  {markReadAction !== null && (
56
55
  <form action={markReadAction} method="post">
57
- <button type="submit" className={buttonVariants({ variant: 'outline' })}>
56
+ <button type="submit" className={buttonVariants({ variant: 'ghost' })}>
58
57
  {c('markRead')}
59
58
  </button>
60
59
  </form>
@@ -70,17 +69,17 @@ export function ForumDisplay({
70
69
  </div>
71
70
  </div>
72
71
 
73
- {regions.tools !== undefined && (
74
- <div className="flex flex-col gap-3 empty:hidden">{regions.tools}</div>
75
- )}
76
-
77
72
  {regions.announcements !== undefined && (
78
73
  <div className="flex flex-col gap-3">{regions.announcements}</div>
79
74
  )}
80
75
 
81
76
  {regions.subforums}
82
77
 
83
- <Card>
78
+ {regions.tools !== undefined && (
79
+ <div className="-mb-2 flex flex-col gap-3 empty:hidden">{regions.tools}</div>
80
+ )}
81
+
82
+ <Card className="rounded-xl">
84
83
  {isEmptyRegion(regions.threads) ? (
85
84
  <Empty>
86
85
  <EmptyTitle>{c('noThreadsYet')}</EmptyTitle>
@@ -1,7 +1,24 @@
1
1
  import type { ForumRowSlotModel, SlotCopy } from '@meith/theme-kit'
2
2
  import { fromSlotCopy } from '@meith/theme-kit'
3
3
 
4
- import { Counts, LINK, MUTED_LINK, ReadSpacer, Stamp, UnreadDot, UserRef } from '../shared'
4
+ import { Figures, LINK, Stamp, Tile, UserRef } from '../shared'
5
+
6
+ function LinkGlyph() {
7
+ return (
8
+ <svg
9
+ aria-hidden="true"
10
+ viewBox="0 0 16 16"
11
+ className="size-4"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth="1.5"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ >
18
+ <path d="M6.5 3.5h6v6M12.5 3.5 4 12" />
19
+ </svg>
20
+ )
21
+ }
5
22
 
6
23
  export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }) {
7
24
  const isLink = forum.type === 'link'
@@ -12,18 +29,20 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
12
29
  <li
13
30
  data-unread={forum.isUnread ? '' : undefined}
14
31
  className={
15
- 'grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-1.5 px-4 py-3 transition-colors hover:bg-muted/60' +
16
- (isLink ? '' : ' md:grid-cols-[auto_minmax(0,1fr)_9rem_15rem] md:items-center md:gap-x-4')
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')
17
34
  }
18
35
  >
19
- {forum.isUnread ? <UnreadDot /> : <ReadSpacer />}
36
+ <Tile label={forum.title} unread={forum.isUnread} className="mt-0.5 md:mt-0">
37
+ {isLink ? <LinkGlyph /> : undefined}
38
+ </Tile>
20
39
 
21
40
  <div className="min-w-0">
22
41
  <a
23
42
  href={forum.href}
24
43
  className={
25
44
  (forum.isUnread ? 'font-semibold text-foreground' : 'font-medium text-foreground') +
26
- ` ${LINK}`
45
+ ` text-[0.9375rem] ${LINK}`
27
46
  }
28
47
  >
29
48
  {forum.title}
@@ -35,10 +54,14 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
35
54
  )}
36
55
 
37
56
  {forum.subforums.length > 0 && (
38
- <p className="mt-1.5 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
39
- <span className="font-medium text-foreground">{c('subforums')}</span>
57
+ <p className="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
58
+ <span className="sr-only">{c('subforums')}</span>
40
59
  {forum.subforums.map((sub) => (
41
- <a key={sub.href} href={sub.href} className={MUTED_LINK}>
60
+ <a
61
+ key={sub.href}
62
+ href={sub.href}
63
+ className="rounded-md bg-muted px-2 py-0.5 font-medium text-muted-foreground transition-colors hover:bg-primary/10 hover:text-primary"
64
+ >
42
65
  {sub.label}
43
66
  </a>
44
67
  ))}
@@ -47,9 +70,9 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
47
70
  </div>
48
71
 
49
72
  {!isLink && (
50
- <div className="col-start-2 flex min-w-0 flex-wrap items-baseline gap-x-4 gap-y-1 text-xs text-muted-foreground md:contents">
51
- <Counts
52
- className="md:col-start-3 md:justify-end"
73
+ <div className="col-start-2 flex min-w-0 flex-col gap-y-1 text-xs text-muted-foreground md:contents">
74
+ <Figures
75
+ className="md:col-start-3 md:justify-self-end"
53
76
  items={[
54
77
  {
55
78
  label: c('threadsLabel'),
@@ -66,7 +89,7 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
66
89
  ]}
67
90
  />
68
91
 
69
- <div className="flex min-w-0 max-w-full flex-wrap gap-x-1 md:col-start-4 md:block">
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">
70
93
  {forum.lastPost === null ? (
71
94
  <span className="text-forum-read">{c('noPostsYet')}</span>
72
95
  ) : (