@meith/theme-phasebook 0.7.0 → 0.9.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.
@@ -1,12 +1,24 @@
1
- import type { ErrorNoticeModel } from '@meith/theme-kit'
1
+ import type { ErrorNoticeModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { NUMERIC, PILL_PRIMARY } from '../shared'
4
5
 
5
- export function ErrorNotice({ status, title, message, homeHref, requestId }: ErrorNoticeModel) {
6
+ export function ErrorNotice({
7
+ status,
8
+ title,
9
+ message,
10
+ homeHref,
11
+ requestId,
12
+ copy,
13
+ }: ErrorNoticeModel & { copy: SlotCopy }) {
14
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.errorNotice.${key}`)
15
+
6
16
  return (
7
17
  <section className="w-full max-w-lg overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-elevation">
8
18
  <div className="px-5 py-6">
9
- <p className={`text-xs font-semibold text-destructive ${NUMERIC}`}>Error {status}</p>
19
+ <p className={`text-xs font-semibold text-destructive ${NUMERIC}`}>
20
+ {c('error')} {status}
21
+ </p>
10
22
 
11
23
  <h1 className="mt-1 text-2xl leading-tight font-bold tracking-tight text-balance">
12
24
  {title}
@@ -14,14 +26,13 @@ export function ErrorNotice({ status, title, message, homeHref, requestId }: Err
14
26
  <p className="mt-2 text-sm text-muted-foreground">{message}</p>
15
27
 
16
28
  <a href={homeHref} className={`mt-5 ${PILL_PRIMARY}`}>
17
- Forum home
29
+ {c('home')}
18
30
  </a>
19
31
  </div>
20
32
 
21
33
  {requestId !== null && (
22
34
  <p className="border-t border-border px-5 py-3 text-xs text-muted-foreground">
23
- Quote this if you report it:{' '}
24
- <code className="font-mono text-foreground select-all">{requestId}</code>
35
+ {c('quote')} <code className="font-mono text-foreground select-all">{requestId}</code>
25
36
  </p>
26
37
  )}
27
38
  </section>
@@ -1,13 +1,22 @@
1
- import type { FooterModel } from '@meith/theme-kit'
1
+ import type { FooterModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { MUTED_LINK, PAGE } from '../shared'
4
5
 
5
- export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterModel) {
6
+ export function Footer({
7
+ boardTitle,
8
+ links,
9
+ timezoneLabel,
10
+ poweredBy,
11
+ copy,
12
+ }: FooterModel & { copy: SlotCopy }) {
13
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.footer.${key}`)
14
+
6
15
  return (
7
16
  <footer className="mt-auto bg-background">
8
17
  <div className={`${PAGE} flex flex-col gap-2 py-6 text-xs text-muted-foreground`}>
9
18
  {links.length > 0 && (
10
- <nav aria-label="Footer" className="flex flex-wrap gap-x-4 gap-y-1">
19
+ <nav aria-label={c('ariaLabel')} className="flex flex-wrap gap-x-4 gap-y-1">
11
20
  {links.map((link) => (
12
21
  <a key={link.href} href={link.href} className={MUTED_LINK}>
13
22
  {link.label}
@@ -18,7 +27,9 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
18
27
 
19
28
  <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
20
29
  <span className="font-semibold text-foreground">{boardTitle}</span>
21
- <span>Times are shown in {timezoneLabel}</span>
30
+ <span>
31
+ {c('timezone')} {timezoneLabel}
32
+ </span>
22
33
  {poweredBy && (
23
34
  <a href={poweredBy.href} className={MUTED_LINK}>
24
35
  {poweredBy.label}
@@ -1,18 +1,27 @@
1
- import type { ForumDisplayModel } from '@meith/theme-kit'
1
+ import type { ForumDisplayModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import {
4
5
  Circle,
6
+ count,
5
7
  FEED,
8
+ isEmptyRegion,
6
9
  NUMERIC,
7
10
  PAGE,
8
11
  PILL,
9
12
  PILL_PRIMARY,
10
- count,
11
- isEmptyRegion,
12
13
  plural,
13
14
  } from '../shared'
14
15
 
15
- export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }: ForumDisplayModel) {
16
+ export function ForumDisplay({
17
+ forum,
18
+ newThreadHref,
19
+ markReadAction,
20
+ regions,
21
+ copy,
22
+ }: ForumDisplayModel & { copy: SlotCopy }) {
23
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.forumDisplay.${key}`)
24
+
16
25
  return (
17
26
  <div className={`${PAGE} py-4 sm:py-6`}>
18
27
  <div className={`${FEED} flex flex-col gap-4`}>
@@ -30,8 +39,10 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
30
39
  </h1>
31
40
  {forum.type !== 'link' && (
32
41
  <p className={`mt-0.5 text-xs text-muted-foreground ${NUMERIC}`}>
33
- {count(forum.threadCount)} {plural(forum.threadCount, 'thread', 'threads')} ·{' '}
34
- {count(forum.postCount)} {plural(forum.postCount, 'post', 'posts')}
42
+ {count(forum.threadCount)}{' '}
43
+ {plural(forum.threadCount, c('thread.one'), c('thread.other'))} ·{' '}
44
+ {count(forum.postCount)}{' '}
45
+ {plural(forum.postCount, c('post.one'), c('post.other'))}
35
46
  </p>
36
47
  )}
37
48
  </div>
@@ -40,7 +51,7 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
40
51
  {markReadAction !== null && (
41
52
  <form action={markReadAction} method="post" className="shrink-0">
42
53
  <button type="submit" className={PILL}>
43
- Mark read
54
+ {c('markRead')}
44
55
  </button>
45
56
  </form>
46
57
  )}
@@ -59,10 +70,10 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
59
70
  href={newThreadHref}
60
71
  className="min-w-0 flex-1 truncate rounded-full bg-secondary px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent"
61
72
  >
62
- Start a thread in {forum.title}…
73
+ {c('startThreadIn')} {forum.title}…
63
74
  </a>
64
75
  <a href={newThreadHref} className={`hidden sm:inline-flex ${PILL_PRIMARY}`}>
65
- New thread
76
+ {c('newThread')}
66
77
  </a>
67
78
  </div>
68
79
  )}
@@ -79,15 +90,13 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
79
90
 
80
91
  {isEmptyRegion(regions.threads) ? (
81
92
  <div className="rounded-lg border border-border bg-card px-4 py-10 text-center shadow-elevation">
82
- <p className="text-base font-semibold">No threads here yet</p>
93
+ <p className="text-base font-semibold">{c('noThreadsYet')}</p>
83
94
  <p className="mt-1 text-sm text-muted-foreground">
84
- {newThreadHref === null
85
- ? 'Nothing has been posted in this forum.'
86
- : 'Nothing has been posted in this forum. Yours would be the first.'}
95
+ {newThreadHref === null ? c('emptyForum') : c('emptyForumInvite')}
87
96
  </p>
88
97
  {newThreadHref !== null && (
89
98
  <a href={newThreadHref} className={`mt-4 ${PILL_PRIMARY}`}>
90
- Start the first thread
99
+ {c('startFirstThread')}
91
100
  </a>
92
101
  )}
93
102
  </div>
@@ -1,9 +1,11 @@
1
- import type { ForumRowSlotModel } from '@meith/theme-kit'
1
+ import type { ForumRowSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
- import { Circle, MUTED_LINK, NUMERIC, Stamp, UserRef, count, plural } from '../shared'
4
+ import { Circle, count, MUTED_LINK, NUMERIC, plural, Stamp, UserRef } from '../shared'
4
5
 
5
- export function ForumRow({ forum }: ForumRowSlotModel) {
6
+ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }) {
6
7
  const isLink = forum.type === 'link'
8
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.forumRow.${key}`)
7
9
 
8
10
  return (
9
11
  <li
@@ -28,12 +30,13 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
28
30
  >
29
31
  {forum.title}
30
32
  </a>
31
- {forum.isUnread && <span className="sr-only"> (new posts)</span>}
33
+ {forum.isUnread && <span className="sr-only"> {c('newPosts')}</span>}
32
34
 
33
35
  {!isLink && (
34
36
  <span className={`shrink-0 text-xs text-muted-foreground ${NUMERIC}`}>
35
- {count(forum.threadCount)} {plural(forum.threadCount, 'thread', 'threads')} ·{' '}
36
- {count(forum.postCount)} {plural(forum.postCount, 'post', 'posts')}
37
+ {count(forum.threadCount)}{' '}
38
+ {plural(forum.threadCount, c('thread.one'), c('thread.other'))} ·{' '}
39
+ {count(forum.postCount)} {plural(forum.postCount, c('post.one'), c('post.other'))}
37
40
  </span>
38
41
  )}
39
42
  </div>
@@ -59,7 +62,7 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
59
62
  {!isLink && (
60
63
  <p className="mt-1.5 truncate text-xs text-muted-foreground">
61
64
  {forum.lastPost === null ? (
62
- <span className="text-forum-read">No posts yet</span>
65
+ <span className="text-forum-read">{c('noPostsYet')}</span>
63
66
  ) : (
64
67
  <>
65
68
  <a href={forum.lastPost.href} className={`font-semibold ${MUTED_LINK}`}>
@@ -1,4 +1,5 @@
1
- import type { HeaderModel } from '@meith/theme-kit'
1
+ import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy, linkTarget } from '@meith/theme-kit'
2
3
 
3
4
  import { PAGE } from '../shared'
4
5
 
@@ -35,7 +36,38 @@ function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'
35
36
  )
36
37
  }
37
38
 
38
- export function Header({ boardTitle, homeHref, navigation, logo, children }: HeaderModel) {
39
+ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'] }) {
40
+ if (items === undefined || items.length === 0) return null
41
+
42
+ return (
43
+ <ul className="invisible absolute top-full left-0 z-30 min-w-44 rounded-xl border border-border bg-card py-1 text-sm opacity-0 shadow-elevation transition-opacity group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
44
+ {items.map((child) => (
45
+ <li key={child.href}>
46
+ <a
47
+ href={child.href}
48
+ {...linkTarget(child)}
49
+ className="block px-4 py-2 text-muted-foreground hover:bg-accent hover:text-foreground"
50
+ >
51
+ {child.label}
52
+ </a>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ )
57
+ }
58
+
59
+ export function Header({
60
+ boardTitle,
61
+ homeHref,
62
+ navigation,
63
+ logo,
64
+ children,
65
+ copy,
66
+ }: HeaderModel & { copy: SlotCopy }) {
67
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.header.${key}`)
68
+
69
+ const opensMenus = navigation.some((item) => item.submenu !== undefined)
70
+
39
71
  return (
40
72
  <header className="sticky top-0 z-30 border-b border-border bg-card shadow-elevation">
41
73
  <div className={`${PAGE} flex h-14 items-center justify-between gap-3`}>
@@ -50,16 +82,21 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
50
82
  </a>
51
83
 
52
84
  {navigation.length > 0 && (
53
- <nav aria-label="Board sections" className="hidden min-w-0 flex-1 justify-center lg:flex">
85
+ <nav
86
+ aria-label={c('sectionsAriaLabel')}
87
+ className="hidden min-w-0 flex-1 justify-center lg:flex"
88
+ >
54
89
  <ul className="flex items-center gap-1">
55
90
  {navigation.map((item) => (
56
- <li key={item.href}>
91
+ <li key={item.href} className="group relative">
57
92
  <a
58
93
  href={item.href}
94
+ {...linkTarget(item)}
59
95
  className="inline-flex h-11 items-center rounded-lg px-6 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
60
96
  >
61
97
  {item.label}
62
98
  </a>
99
+ <Submenu items={item.submenu} />
63
100
  </li>
64
101
  ))}
65
102
  </ul>
@@ -70,16 +107,22 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
70
107
  </div>
71
108
 
72
109
  {navigation.length > 0 && (
73
- <nav aria-label="Board sections" className="border-t border-border lg:hidden">
74
- <ul className={`${PAGE} flex items-center gap-1 overflow-x-auto py-1`}>
110
+ <nav aria-label={c('sectionsAriaLabel')} className="border-t border-border lg:hidden">
111
+ <ul
112
+ className={`${PAGE} flex items-center gap-1 py-1 ${
113
+ opensMenus ? 'flex-wrap' : 'overflow-x-auto'
114
+ }`}
115
+ >
75
116
  {navigation.map((item) => (
76
- <li key={item.href} className="shrink-0">
117
+ <li key={item.href} className="group relative shrink-0">
77
118
  <a
78
119
  href={item.href}
120
+ {...linkTarget(item)}
79
121
  className="inline-flex h-9 items-center rounded-full px-3 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
80
122
  >
81
123
  {item.label}
82
124
  </a>
125
+ <Submenu items={item.submenu} />
83
126
  </li>
84
127
  ))}
85
128
  </ul>
@@ -1,11 +1,14 @@
1
- import type { LatestPostsModel } from '@meith/theme-kit'
1
+ import type { LatestPostsModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { Circle, MUTED_LINK, NUMERIC, Rail, Stamp, UserRef } from '../shared'
4
5
 
5
- export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
6
+ export function LatestPosts({ posts, capturedAt, copy }: LatestPostsModel & { copy: SlotCopy }) {
7
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.latestPosts.${key}`)
8
+
6
9
  return (
7
10
  <Rail
8
- title="Latest posts"
11
+ title={c('title')}
9
12
  titleId="latest-posts-heading"
10
13
  action={
11
14
  <span className={`text-xs text-muted-foreground ${NUMERIC}`}>
@@ -14,13 +17,14 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
14
17
  }
15
18
  >
16
19
  {posts.length === 0 ? (
17
- <p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">
18
- The newest posts you can see will appear here.
19
- </p>
20
+ <p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">{c('empty')}</p>
20
21
  ) : (
21
22
  <ul className="px-1 pt-1 pb-1">
22
23
  {posts.map((post) => (
23
- <li key={post.href} className="rounded-lg px-2 py-1.5 transition-colors hover:bg-accent">
24
+ <li
25
+ key={post.href}
26
+ className="rounded-lg px-2 py-1.5 transition-colors hover:bg-accent"
27
+ >
24
28
  <div className="flex items-start gap-2.5">
25
29
  <Circle name={post.author.username} size={32} className="mt-0.5" />
26
30
 
@@ -40,7 +44,7 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
40
44
 
41
45
  <p className="mt-0.5 truncate text-xs text-muted-foreground">
42
46
  <UserRef user={post.author} className="text-xs font-medium" />
43
- {' in '}
47
+ {` ${c('in')} `}
44
48
  <a href={post.forum.href} className={MUTED_LINK}>
45
49
  {post.forum.label}
46
50
  </a>
@@ -1,11 +1,18 @@
1
- import type { LatestThreadsModel } from '@meith/theme-kit'
1
+ import type { LatestThreadsModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
- import { Circle, MUTED_LINK, NUMERIC, Rail, Stamp, UserRef, count, plural } from '../shared'
4
+ import { Circle, count, MUTED_LINK, NUMERIC, plural, Rail, Stamp, UserRef } from '../shared'
5
+
6
+ export function LatestThreads({
7
+ threads,
8
+ capturedAt,
9
+ copy,
10
+ }: LatestThreadsModel & { copy: SlotCopy }) {
11
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.latestThreads.${key}`)
4
12
 
5
- export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
6
13
  return (
7
14
  <Rail
8
- title="Latest threads"
15
+ title={c('title')}
9
16
  titleId="latest-threads-heading"
10
17
  action={
11
18
  <span className={`text-xs text-muted-foreground ${NUMERIC}`}>
@@ -14,13 +21,14 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
14
21
  }
15
22
  >
16
23
  {threads.length === 0 ? (
17
- <p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">
18
- The newest threads you can see will appear here.
19
- </p>
24
+ <p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">{c('empty')}</p>
20
25
  ) : (
21
26
  <ul className="px-1 pt-1 pb-1">
22
27
  {threads.map((thread) => (
23
- <li key={thread.href} className="rounded-lg px-2 py-1.5 transition-colors hover:bg-accent">
28
+ <li
29
+ key={thread.href}
30
+ className="rounded-lg px-2 py-1.5 transition-colors hover:bg-accent"
31
+ >
24
32
  <div className="flex items-start gap-2.5">
25
33
  <Circle name={thread.author.username} size={32} className="mt-0.5" />
26
34
 
@@ -41,7 +49,8 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
41
49
  </p>
42
50
 
43
51
  <p className={`mt-0.5 truncate text-xs text-muted-foreground ${NUMERIC}`}>
44
- {count(thread.replyCount)} {plural(thread.replyCount, 'reply', 'replies')} ·{' '}
52
+ {count(thread.replyCount)}{' '}
53
+ {plural(thread.replyCount, c('reply.one'), c('reply.other'))} ·{' '}
45
54
  <Stamp at={thread.startedAt} />
46
55
  </p>
47
56
  </div>
@@ -1,7 +1,8 @@
1
+ import type { MemberProfileModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { cn } from '@meith/ui'
2
- import type { MemberProfileModel } from '@meith/theme-kit'
3
4
 
4
- import { Circle, FEED, NUMERIC, PAGE, PILL, PILL_PRIMARY, Stamp, count } from '../shared'
5
+ import { Circle, count, FEED, NUMERIC, PAGE, PILL, PILL_PRIMARY, Stamp } from '../shared'
5
6
 
6
7
  export function MemberProfile({
7
8
  user,
@@ -14,7 +15,10 @@ export function MemberProfile({
14
15
  fields,
15
16
  actions,
16
17
  regions,
17
- }: MemberProfileModel) {
18
+ copy,
19
+ }: MemberProfileModel & { copy: SlotCopy }) {
20
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.memberProfile.${key}`)
21
+
18
22
  return (
19
23
  <div className={`${PAGE} py-4 sm:py-6`}>
20
24
  <div className={`${FEED} flex flex-col gap-4`}>
@@ -45,7 +49,7 @@ export function MemberProfile({
45
49
 
46
50
  {actions.length > 0 && (
47
51
  <nav
48
- aria-label="Member actions"
52
+ aria-label={c('actionsAriaLabel')}
49
53
  className="flex shrink-0 flex-wrap items-center justify-center gap-2"
50
54
  >
51
55
  {actions.map((action, index) => (
@@ -63,22 +67,20 @@ export function MemberProfile({
63
67
 
64
68
  <dl className="grid grid-cols-3 gap-2 border-t border-border px-4 py-3 text-center">
65
69
  <div>
66
- <dd className={`text-lg font-bold text-foreground ${NUMERIC}`}>
67
- {count(postCount)}
68
- </dd>
69
- <dt className="text-xs text-muted-foreground">Posts</dt>
70
+ <dd className={`text-lg font-bold text-foreground ${NUMERIC}`}>{count(postCount)}</dd>
71
+ <dt className="text-xs text-muted-foreground">{c('posts')}</dt>
70
72
  </div>
71
73
  <div>
72
74
  <dd className="text-sm font-semibold text-foreground">
73
75
  <Stamp at={joinedAt} />
74
76
  </dd>
75
- <dt className="text-xs text-muted-foreground">Joined</dt>
77
+ <dt className="text-xs text-muted-foreground">{c('joined')}</dt>
76
78
  </div>
77
79
  <div>
78
80
  <dd className="text-sm font-semibold text-foreground">
79
- {lastVisitAt === null ? 'Never' : <Stamp at={lastVisitAt} />}
81
+ {lastVisitAt === null ? c('never') : <Stamp at={lastVisitAt} />}
80
82
  </dd>
81
- <dt className="text-xs text-muted-foreground">Last visit</dt>
83
+ <dt className="text-xs text-muted-foreground">{c('lastVisit')}</dt>
82
84
  </div>
83
85
  </dl>
84
86
  </section>
@@ -92,7 +94,7 @@ export function MemberProfile({
92
94
  id="profile-fields-heading"
93
95
  className="px-4 pt-3 pb-1 text-[1.0625rem] font-bold tracking-tight"
94
96
  >
95
- About
97
+ {c('about')}
96
98
  </h2>
97
99
  <dl className="grid gap-x-6 gap-y-3 px-4 pt-2 pb-4 sm:grid-cols-2">
98
100
  {fields.map((field) => (
@@ -114,7 +116,7 @@ export function MemberProfile({
114
116
  id="profile-signature-heading"
115
117
  className="px-4 pt-3 pb-1 text-[1.0625rem] font-bold tracking-tight"
116
118
  >
117
- Signature
119
+ {c('signature')}
118
120
  </h2>
119
121
  <div
120
122
  className="prose-md px-4 pt-2 pb-4 text-sm text-muted-foreground"
@@ -1,12 +1,15 @@
1
- import type { NavigationModel } from '@meith/theme-kit'
1
+ import type { NavigationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { PAGE } from '../shared'
4
5
 
5
- export function Navigation({ items }: NavigationModel) {
6
+ export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }) {
6
7
  if (items.length === 0) return null
7
8
 
9
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.navigation.${key}`)
10
+
8
11
  return (
9
- <nav aria-label="Breadcrumb" className="bg-background">
12
+ <nav aria-label={c('ariaLabel')} className="bg-background">
10
13
  <ol
11
14
  className={`${PAGE} flex items-center gap-1 overflow-x-auto pt-3 text-sm whitespace-nowrap text-muted-foreground`}
12
15
  >
@@ -1,31 +1,33 @@
1
+ import type { NoticeModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { cn } from '@meith/ui'
2
- import type { NoticeModel } from '@meith/theme-kit'
3
4
 
4
5
  const KIND = {
5
6
  info: {
6
- label: 'Notice',
7
+ copyKey: 'info',
7
8
  className: 'border-primary/30 bg-primary/8 text-foreground',
8
9
  mark: 'bg-primary text-primary-foreground',
9
10
  },
10
11
  success: {
11
- label: 'Done',
12
+ copyKey: 'success',
12
13
  className: 'border-moderation-approved/30 bg-moderation-approved/8 text-foreground',
13
14
  mark: 'bg-moderation-approved text-card',
14
15
  },
15
16
  warning: {
16
- label: 'Warning',
17
+ copyKey: 'warning',
17
18
  className: 'border-thread-pinned/30 bg-thread-pinned/8 text-foreground',
18
19
  mark: 'bg-thread-pinned text-card',
19
20
  },
20
21
  error: {
21
- label: 'Error',
22
+ copyKey: 'error',
22
23
  className: 'border-destructive/30 bg-destructive/8 text-foreground',
23
24
  mark: 'bg-destructive text-destructive-foreground',
24
25
  },
25
26
  } as const
26
27
 
27
- export function Notice({ kind, message, dismissHref }: NoticeModel) {
28
+ export function Notice({ kind, message, dismissHref, copy }: NoticeModel & { copy: SlotCopy }) {
28
29
  const tone = KIND[kind]
30
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.notice.${key}`)
29
31
 
30
32
  return (
31
33
  <div
@@ -46,7 +48,7 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
46
48
  </span>
47
49
 
48
50
  <p className="min-w-0 flex-1">
49
- <span className="font-semibold">{tone.label}:</span> {message}
51
+ <span className="font-semibold">{c(tone.copyKey)}:</span> {message}
50
52
  </p>
51
53
 
52
54
  {dismissHref !== null && (
@@ -54,7 +56,7 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
54
56
  href={dismissHref}
55
57
  className="shrink-0 rounded-full px-2 py-1 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
56
58
  >
57
- Dismiss
59
+ {c('dismiss')}
58
60
  </a>
59
61
  )}
60
62
  </div>
@@ -1,5 +1,6 @@
1
+ import type { PaginationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { cn } from '@meith/ui'
2
- import type { PaginationModel } from '@meith/theme-kit'
3
4
 
4
5
  import { NUMERIC, PILL } from '../shared'
5
6
 
@@ -10,23 +11,25 @@ export function Pagination({
10
11
  pages,
11
12
  previousHref,
12
13
  nextHref,
13
- }: PaginationModel) {
14
+ copy,
15
+ }: PaginationModel & { copy: SlotCopy }) {
14
16
  if (pageCount <= 1 && previousHref === null && nextHref === null) return null
15
17
 
16
18
  const disabled = cn(PILL, 'pointer-events-none opacity-40')
19
+ const c = (key: string) => fromSlotCopy(copy, `phasebook.pagination.${key}`)
17
20
 
18
21
  return (
19
22
  <nav
20
- aria-label="Pagination"
23
+ aria-label={c('ariaLabel')}
21
24
  className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-card px-3 py-2.5 shadow-elevation"
22
25
  >
23
26
  {previousHref === null ? (
24
27
  <span className={disabled} aria-hidden="true">
25
- Previous
28
+ {c('previous')}
26
29
  </span>
27
30
  ) : (
28
31
  <a href={previousHref} rel="prev" className={PILL}>
29
- Previous
32
+ {c('previous')}
30
33
  </a>
31
34
  )}
32
35
 
@@ -45,7 +48,7 @@ export function Pagination({
45
48
  <a
46
49
  href={entry.href}
47
50
  aria-current={entry.isCurrent ? 'page' : undefined}
48
- aria-label={`Page ${entry.page}`}
51
+ aria-label={`${c('page')} ${entry.page}`}
49
52
  className={cn(
50
53
  'inline-flex size-9 items-center justify-center rounded-full text-sm font-semibold transition-colors',
51
54
  NUMERIC,
@@ -61,18 +64,18 @@ export function Pagination({
61
64
  })}
62
65
 
63
66
  <li className={`ml-1 text-xs whitespace-nowrap text-muted-foreground ${NUMERIC}`}>
64
- <span className="sr-only">Page </span>
65
- {pageCountIsExact ? `${page} of ${pageCount}` : `Page ${page}`}
67
+ <span className="sr-only">{c('page')} </span>
68
+ {pageCountIsExact ? `${page} ${c('of')} ${pageCount}` : `${c('page')} ${page}`}
66
69
  </li>
67
70
  </ol>
68
71
 
69
72
  {nextHref === null ? (
70
73
  <span className={disabled} aria-hidden="true">
71
- Next
74
+ {c('next')}
72
75
  </span>
73
76
  ) : (
74
77
  <a href={nextHref} rel="next" className={PILL}>
75
- Next
78
+ {c('next')}
76
79
  </a>
77
80
  )}
78
81
  </nav>