@meith/theme-raidframe 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.
Files changed (38) hide show
  1. package/package.json +4 -4
  2. package/src/copy.ts +286 -0
  3. package/src/index.ts +2 -2
  4. package/src/messages/en.json +144 -0
  5. package/src/messages/index.ts +5 -0
  6. package/src/shared.tsx +1 -2
  7. package/src/slots/announcement.tsx +16 -6
  8. package/src/slots/auth-page.tsx +12 -3
  9. package/src/slots/board-index.tsx +10 -3
  10. package/src/slots/board-stats.tsx +18 -12
  11. package/src/slots/category-block.tsx +13 -6
  12. package/src/slots/discovery-view.tsx +17 -8
  13. package/src/slots/error-notice.tsx +17 -5
  14. package/src/slots/footer.tsx +16 -4
  15. package/src/slots/forum-display.tsx +20 -11
  16. package/src/slots/forum-row.tsx +26 -10
  17. package/src/slots/header.tsx +43 -10
  18. package/src/slots/latest-posts.tsx +12 -6
  19. package/src/slots/latest-threads.tsx +18 -8
  20. package/src/slots/member-profile.tsx +14 -10
  21. package/src/slots/navigation.tsx +6 -3
  22. package/src/slots/notice.tsx +7 -4
  23. package/src/slots/pagination.tsx +10 -6
  24. package/src/slots/panel-nav.tsx +40 -13
  25. package/src/slots/panel-page.tsx +12 -11
  26. package/src/slots/post-actions.tsx +17 -10
  27. package/src/slots/post-bit.tsx +29 -18
  28. package/src/slots/post-form.tsx +14 -4
  29. package/src/slots/redirect-notice.tsx +16 -8
  30. package/src/slots/search-form.tsx +12 -8
  31. package/src/slots/search-results.tsx +26 -16
  32. package/src/slots/shell.tsx +6 -3
  33. package/src/slots/subforum-list.tsx +8 -5
  34. package/src/slots/thread-row.tsx +19 -11
  35. package/src/slots/thread-view.tsx +22 -8
  36. package/src/slots/user-panel.tsx +14 -14
  37. package/src/slots/who-is-online.tsx +19 -10
  38. package/src/theme.ts +66 -0
@@ -1,8 +1,15 @@
1
- import type { CategoryBlockModel } from '@meith/theme-kit'
1
+ import type { CategoryBlockModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { Frame, MICRO, PanelHead } from '../shared'
4
5
 
5
- export function CategoryBlock({ category, children }: CategoryBlockModel) {
6
+ export function CategoryBlock({
7
+ category,
8
+ children,
9
+ copy,
10
+ }: CategoryBlockModel & { copy: SlotCopy }) {
11
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.categoryBlock.${key}`)
12
+
6
13
  return (
7
14
  <Frame>
8
15
  <PanelHead title={category.title} href={category.href} />
@@ -17,16 +24,16 @@ export function CategoryBlock({ category, children }: CategoryBlockModel) {
17
24
  <thead>
18
25
  <tr className={`${MICRO} text-left`}>
19
26
  <th scope="col" className="px-3 py-1.5 font-semibold">
20
- Forum
27
+ {c('forum')}
21
28
  </th>
22
29
  <th scope="col" className="w-16 px-2 py-1.5 text-right font-semibold">
23
- Threads
30
+ {c('threads')}
24
31
  </th>
25
32
  <th scope="col" className="w-16 px-2 py-1.5 text-right font-semibold">
26
- Posts
33
+ {c('posts')}
27
34
  </th>
28
35
  <th scope="col" className="hidden w-60 px-3 py-1.5 font-semibold sm:table-cell">
29
- Last post
36
+ {c('lastPost')}
30
37
  </th>
31
38
  </tr>
32
39
  </thead>
@@ -1,4 +1,5 @@
1
- import type { DiscoveryViewModel, TabModel } from '@meith/theme-kit'
1
+ import type { DiscoveryViewModel, SlotCopy, TabModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { Frame, HEADING, MICRO, NUMERIC, RULE, Stamp } from '../shared'
4
5
 
@@ -6,7 +7,8 @@ const TAB =
6
7
  'inline-flex items-center border px-3 py-1.5 font-mono text-[0.6875rem] font-semibold uppercase tracking-[0.14em] whitespace-nowrap'
7
8
 
8
9
  const CURRENT = 'border-primary bg-primary text-primary-foreground'
9
- const OTHER = 'border-border bg-card text-muted-foreground hover:border-primary/60 hover:text-primary'
10
+ const OTHER =
11
+ 'border-border bg-card text-muted-foreground hover:border-primary/60 hover:text-primary'
10
12
 
11
13
  function Tabs({ label, tabs }: { label: string; tabs: readonly TabModel[] }) {
12
14
  if (tabs.length === 0) return null
@@ -40,7 +42,10 @@ export function DiscoveryView({
40
42
  nextLabel,
41
43
  emptyMessage,
42
44
  refusal,
43
- }: DiscoveryViewModel) {
45
+ copy,
46
+ }: DiscoveryViewModel & { copy: SlotCopy }) {
47
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.discoveryView.${key}`)
48
+
44
49
  return (
45
50
  <main
46
51
  id="board-content"
@@ -49,7 +54,7 @@ export function DiscoveryView({
49
54
  >
50
55
  <div className="border border-border bg-card shadow-elevation">
51
56
  <div className="px-4 py-3">
52
- <p className={MICRO}>listing</p>
57
+ <p className={MICRO}>{c('kicker')}</p>
53
58
  <h1 className={`${HEADING} text-xl text-foreground`}>{title}</h1>
54
59
  <p className="mt-1 text-xs text-muted-foreground">{blurb}</p>
55
60
  </div>
@@ -92,16 +97,20 @@ export function DiscoveryView({
92
97
  {row.forum.label}
93
98
  </a>
94
99
  <span className="text-border">{' | '}</span>
95
- <span className="uppercase">started by</span> {row.authorUsername}
100
+ <span className="uppercase">{c('startedBy')}</span> {row.authorUsername}
96
101
  </p>
97
102
  </div>
98
103
 
99
104
  <p className={`${MICRO} shrink-0 normal-case sm:text-right`}>
100
- <span className={`${NUMERIC} text-foreground`}>{row.replyCount}</span>{' '}
101
- <span className="uppercase">{row.replyCount === 1 ? 'reply' : 'replies'}</span>
105
+ <span className={`${NUMERIC} text-foreground`}>{row.replyCount.label}</span>{' '}
106
+ <span className="uppercase">
107
+ {row.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
108
+ </span>
102
109
  <span className="block">
103
110
  <Stamp at={row.lastPostAt} />
104
- {row.lastPostUsername === null ? null : ` by ${row.lastPostUsername}`}
111
+ {row.lastPostUsername === null
112
+ ? null
113
+ : `${c('lastPostByPrefix')}${row.lastPostUsername}`}
105
114
  </span>
106
115
  </p>
107
116
  </li>
@@ -1,23 +1,35 @@
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 { BUTTON_PRIMARY, Frame, HEADING, MICRO, NUMERIC } 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, `raidframe.errorNotice.${key}`)
15
+
6
16
  return (
7
17
  <Frame className="w-full max-w-lg">
8
18
  <div className="px-5 py-5">
9
- <p className={`${MICRO} ${NUMERIC} text-destructive`}>error {status}</p>
19
+ <p className={`${MICRO} ${NUMERIC} text-destructive`}>
20
+ {c('errorLabel')} {status}
21
+ </p>
10
22
  <h1 className={`${HEADING} mt-1 text-2xl text-foreground`}>{title}</h1>
11
23
  <p className="mt-2 text-sm text-muted-foreground">{message}</p>
12
24
 
13
25
  <a href={homeHref} className={`${BUTTON_PRIMARY} mt-5`}>
14
- forum home
26
+ {c('forumHome')}
15
27
  </a>
16
28
  </div>
17
29
 
18
30
  {requestId !== null && (
19
31
  <p className={`${MICRO} border-t border-border px-5 py-2 normal-case`}>
20
- <span className="uppercase">request id</span>{' '}
32
+ <span className="uppercase">{c('requestId')}</span>{' '}
21
33
  <code className={`${NUMERIC} text-foreground select-all`}>{requestId}</code>
22
34
  </p>
23
35
  )}
@@ -1,8 +1,17 @@
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 { RULE } 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, `raidframe.footer.${key}`)
14
+
6
15
  return (
7
16
  <footer className="mt-auto">
8
17
  <div className={RULE} aria-hidden="true" />
@@ -12,7 +21,7 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
12
21
  {boardTitle}
13
22
  </span>
14
23
 
15
- <nav aria-label="Board links" className="flex flex-wrap gap-x-4 gap-y-1">
24
+ <nav aria-label={c('linksAriaLabel')} className="flex flex-wrap gap-x-4 gap-y-1">
16
25
  {links.map((link) => (
17
26
  <a key={link.href} href={link.href} className="hover:text-primary">
18
27
  {link.label}
@@ -21,7 +30,10 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
21
30
  </nav>
22
31
 
23
32
  <span className="flex flex-wrap gap-x-4 gap-y-1">
24
- <span>all times {timezoneLabel}</span>
33
+ <span>
34
+ {c('allTimesPrefix')}
35
+ {timezoneLabel}
36
+ </span>
25
37
  {poweredBy && (
26
38
  <a href={poweredBy.href} className="hover:text-primary">
27
39
  {poweredBy.label}
@@ -1,14 +1,23 @@
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 { BUTTON, BUTTON_PRIMARY, Frame, HEADING, MICRO, NUMERIC, RULE } from '../shared'
4
5
 
5
- export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }: ForumDisplayModel) {
6
+ export function ForumDisplay({
7
+ forum,
8
+ newThreadHref,
9
+ markReadAction,
10
+ regions,
11
+ copy,
12
+ }: ForumDisplayModel & { copy: SlotCopy }) {
13
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.forumDisplay.${key}`)
14
+
6
15
  return (
7
16
  <div className="flex flex-col gap-4 p-4">
8
17
  <div className="border border-border bg-card shadow-elevation">
9
18
  <div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
10
19
  <div className="min-w-0">
11
- <p className={MICRO}>forum</p>
20
+ <p className={MICRO}>{c('kicker')}</p>
12
21
  <h1 className={`${HEADING} text-xl text-foreground`}>{forum.title}</h1>
13
22
  {forum.description !== null && (
14
23
  <p className="mt-1 text-xs text-muted-foreground">{forum.description}</p>
@@ -17,13 +26,13 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
17
26
 
18
27
  <div className="flex flex-wrap items-center gap-3">
19
28
  <span className={`${MICRO} ${NUMERIC} normal-case`}>
20
- <span className="text-foreground">{forum.threadCount}</span> threads
29
+ <span className="text-foreground">{forum.threadCount.label}</span> {c('threads')}
21
30
  <span className="text-border">{' | '}</span>
22
- <span className="text-foreground">{forum.postCount}</span> posts
31
+ <span className="text-foreground">{forum.postCount.label}</span> {c('posts')}
23
32
  </span>
24
33
  {newThreadHref !== null && (
25
34
  <a href={newThreadHref} className={BUTTON_PRIMARY}>
26
- new thread
35
+ {c('newThread')}
27
36
  </a>
28
37
  )}
29
38
  </div>
@@ -46,16 +55,16 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
46
55
  <thead>
47
56
  <tr className={`${MICRO} border-b border-border bg-surface text-left`}>
48
57
  <th scope="col" className="px-3 py-2 font-semibold">
49
- Thread
58
+ {c('threadHeader')}
50
59
  </th>
51
60
  <th scope="col" className="w-16 px-2 py-2 text-right font-semibold">
52
- Replies
61
+ {c('repliesHeader')}
53
62
  </th>
54
63
  <th scope="col" className="w-16 px-2 py-2 text-right font-semibold">
55
- Views
64
+ {c('viewsHeader')}
56
65
  </th>
57
66
  <th scope="col" className="hidden w-52 px-3 py-2 font-semibold sm:table-cell">
58
- Last post
67
+ {c('lastPostHeader')}
59
68
  </th>
60
69
  </tr>
61
70
  </thead>
@@ -72,7 +81,7 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
72
81
  {markReadAction !== null && (
73
82
  <form action={markReadAction} method="post">
74
83
  <button type="submit" className={BUTTON}>
75
- mark this forum read
84
+ {c('markRead')}
76
85
  </button>
77
86
  </form>
78
87
  )}
@@ -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
4
  import { HEADING, MICRO, NUMERIC, ReadPip, 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, `raidframe.forumRow.${key}`)
7
9
 
8
10
  return (
9
11
  <tr className="border-t border-border align-top hover:bg-secondary/50">
@@ -11,11 +13,14 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
11
13
  <div className="flex items-baseline gap-2">
12
14
  <ReadPip unread={forum.isUnread} className="mt-1.5" />
13
15
  <div className="min-w-0">
14
- <a href={forum.href} className={`${HEADING} text-sm text-foreground hover:text-primary`}>
16
+ <a
17
+ href={forum.href}
18
+ className={`${HEADING} text-sm text-foreground hover:text-primary`}
19
+ >
15
20
  {forum.title}
16
21
  </a>
17
- {forum.isUnread && <span className="sr-only">(new posts)</span>}
18
- {isLink && <span className={`${MICRO} ml-2`}>link</span>}
22
+ {forum.isUnread && <span className="sr-only">{c('newPosts')}</span>}
23
+ {isLink && <span className={`${MICRO} ml-2`}>{c('linkBadge')}</span>}
19
24
 
20
25
  {forum.description !== null && (
21
26
  <p className="mt-0.5 text-xs text-muted-foreground">{forum.description}</p>
@@ -39,23 +44,34 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
39
44
  </td>
40
45
 
41
46
  <td className={`w-16 px-2 py-2.5 text-right text-xs ${NUMERIC} text-foreground`}>
42
- {isLink ? <span className="text-muted-foreground">—</span> : forum.threadCount}
47
+ {isLink ? (
48
+ <span className="text-muted-foreground">{c('none')}</span>
49
+ ) : (
50
+ forum.threadCount.label
51
+ )}
43
52
  </td>
44
53
  <td className={`w-16 px-2 py-2.5 text-right text-xs ${NUMERIC} text-foreground`}>
45
- {isLink ? <span className="text-muted-foreground">—</span> : forum.postCount}
54
+ {isLink ? (
55
+ <span className="text-muted-foreground">{c('none')}</span>
56
+ ) : (
57
+ forum.postCount.label
58
+ )}
46
59
  </td>
47
60
 
48
61
  <td className="hidden w-60 px-3 py-2.5 text-xs text-muted-foreground sm:table-cell">
49
62
  {isLink || forum.lastPost === null ? (
50
- <span className={MICRO}>{isLink ? '' : 'no posts yet'}</span>
63
+ <span className={MICRO}>{isLink ? '' : c('noPostsYet')}</span>
51
64
  ) : (
52
65
  <>
53
- <a href={forum.lastPost.href} className="block truncate text-foreground hover:text-primary">
66
+ <a
67
+ href={forum.lastPost.href}
68
+ className="block truncate text-foreground hover:text-primary"
69
+ >
54
70
  {forum.lastPost.threadTitle}
55
71
  </a>
56
72
  <span className={`${MICRO} mt-0.5 block normal-case`}>
57
73
  <UserRef user={forum.lastPost.author} className="hover:text-primary" />
58
- {' · '}
74
+ {c('separator')}
59
75
  <Stamp at={forum.lastPost.at} />
60
76
  </span>
61
77
  </>
@@ -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 { RULE } from '../shared'
4
5
 
@@ -30,7 +31,36 @@ function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'
30
31
  )
31
32
  }
32
33
 
33
- export function Header({ boardTitle, homeHref, navigation, logo, children }: HeaderModel) {
34
+ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'] }) {
35
+ if (items === undefined || items.length === 0) return null
36
+
37
+ return (
38
+ <ul className="invisible absolute top-full left-0 z-30 min-w-44 rounded-b-md 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">
39
+ {items.map((child) => (
40
+ <li key={child.href}>
41
+ <a
42
+ href={child.href}
43
+ {...linkTarget(child)}
44
+ className="block px-3 py-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
45
+ >
46
+ {child.label}
47
+ </a>
48
+ </li>
49
+ ))}
50
+ </ul>
51
+ )
52
+ }
53
+
54
+ export function Header({
55
+ boardTitle,
56
+ homeHref,
57
+ navigation,
58
+ logo,
59
+ children,
60
+ copy,
61
+ }: HeaderModel & { copy: SlotCopy }) {
62
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.header.${key}`)
63
+
34
64
  return (
35
65
  <header>
36
66
  <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-3 border-b border-border bg-surface px-4 py-3">
@@ -48,17 +78,20 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
48
78
 
49
79
  {navigation.length > 0 && (
50
80
  <nav
51
- aria-label="Board sections"
81
+ aria-label={c('sectionsAriaLabel')}
52
82
  className="flex flex-wrap border-b border-border bg-card/60"
53
83
  >
54
84
  {navigation.map((item) => (
55
- <a
56
- key={item.href}
57
- href={item.href}
58
- className="border-t-2 border-r border-t-transparent border-r-border px-3.5 py-2 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase hover:border-t-primary hover:bg-secondary hover:text-primary"
59
- >
60
- {item.label}
61
- </a>
85
+ <span key={item.href} className="group relative">
86
+ <a
87
+ href={item.href}
88
+ {...linkTarget(item)}
89
+ className="block border-t-2 border-r border-t-transparent border-r-border px-3.5 py-2 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase hover:border-t-primary hover:bg-secondary hover:text-primary"
90
+ >
91
+ {item.label}
92
+ </a>
93
+ <Submenu items={item.submenu} />
94
+ </span>
62
95
  ))}
63
96
  </nav>
64
97
  )}
@@ -1,19 +1,25 @@
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 { Frame, MICRO, PanelHead, 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, `raidframe.latestPosts.${key}`)
8
+
6
9
  return (
7
10
  <Frame aria-labelledby="latest-posts-heading">
8
- <PanelHead id="latest-posts-heading" title="Latest posts" />
11
+ <PanelHead id="latest-posts-heading" title={c('heading')} />
9
12
 
10
13
  {posts.length === 0 ? (
11
- <p className={`${MICRO} px-3 py-2.5`}>nothing said yet</p>
14
+ <p className={`${MICRO} px-3 py-2.5`}>{c('empty')}</p>
12
15
  ) : (
13
16
  <ul className="divide-y divide-border">
14
17
  {posts.map((post) => (
15
18
  <li key={post.href} className="px-3 py-2 hover:bg-secondary/50">
16
- <a href={post.href} className="block truncate text-xs text-foreground hover:text-primary">
19
+ <a
20
+ href={post.href}
21
+ className="block truncate text-xs text-foreground hover:text-primary"
22
+ >
17
23
  {post.threadTitle}
18
24
  </a>
19
25
  <p className={`${MICRO} mt-0.5 truncate normal-case`}>
@@ -35,7 +41,7 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
35
41
  )}
36
42
 
37
43
  <p className={`${MICRO} border-t border-border px-3 py-1.5 normal-case`}>
38
- <span className="uppercase">as of</span> <Stamp at={capturedAt} />
44
+ <span className="uppercase">{c('asOf')}</span> <Stamp at={capturedAt} />
39
45
  </p>
40
46
  </Frame>
41
47
  )
@@ -1,19 +1,29 @@
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
4
  import { Frame, MICRO, NUMERIC, PanelHead, Stamp, UserRef } from '../shared'
4
5
 
5
- export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
6
+ export function LatestThreads({
7
+ threads,
8
+ capturedAt,
9
+ copy,
10
+ }: LatestThreadsModel & { copy: SlotCopy }) {
11
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.latestThreads.${key}`)
12
+
6
13
  return (
7
14
  <Frame aria-labelledby="latest-threads-heading">
8
- <PanelHead id="latest-threads-heading" title="Latest threads" />
15
+ <PanelHead id="latest-threads-heading" title={c('heading')} />
9
16
 
10
17
  {threads.length === 0 ? (
11
- <p className={`${MICRO} px-3 py-2.5`}>nothing started yet</p>
18
+ <p className={`${MICRO} px-3 py-2.5`}>{c('empty')}</p>
12
19
  ) : (
13
20
  <ul className="divide-y divide-border">
14
21
  {threads.map((thread) => (
15
22
  <li key={thread.href} className="px-3 py-2 hover:bg-secondary/50">
16
- <a href={thread.href} className="block truncate text-xs text-foreground hover:text-primary">
23
+ <a
24
+ href={thread.href}
25
+ className="block truncate text-xs text-foreground hover:text-primary"
26
+ >
17
27
  {thread.title}
18
28
  </a>
19
29
  <p className={`${MICRO} mt-0.5 truncate normal-case`}>
@@ -26,8 +36,8 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
26
36
  <p className={`${MICRO} truncate normal-case`}>
27
37
  <Stamp at={thread.startedAt} />
28
38
  <span className="text-border">{' | '}</span>
29
- <span className={NUMERIC}>{thread.replyCount}</span>
30
- <span className="uppercase"> replies</span>
39
+ <span className={NUMERIC}>{thread.replyCount.label}</span>
40
+ <span className="uppercase">{c('replies')}</span>
31
41
  </p>
32
42
  </li>
33
43
  ))}
@@ -35,7 +45,7 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
35
45
  )}
36
46
 
37
47
  <p className={`${MICRO} border-t border-border px-3 py-1.5 normal-case`}>
38
- <span className="uppercase">as of</span> <Stamp at={capturedAt} />
48
+ <span className="uppercase">{c('asOf')}</span> <Stamp at={capturedAt} />
39
49
  </p>
40
50
  </Frame>
41
51
  )
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import type { MemberProfileModel } from '@meith/theme-kit'
3
+ import type { MemberProfileModel, SlotCopy } from '@meith/theme-kit'
4
+ import { fromSlotCopy } from '@meith/theme-kit'
4
5
 
5
6
  import { BUTTON, Frame, HEADING, MICRO, NUMERIC, PanelHead, RULE, Stamp } from '../shared'
6
7
 
@@ -24,7 +25,10 @@ export function MemberProfile({
24
25
  fields,
25
26
  actions,
26
27
  regions,
27
- }: MemberProfileModel) {
28
+ copy,
29
+ }: MemberProfileModel & { copy: SlotCopy }) {
30
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.memberProfile.${key}`)
31
+
28
32
  return (
29
33
  <article className="flex flex-col gap-4 p-4">
30
34
  <div className="border border-border bg-card shadow-elevation">
@@ -44,7 +48,7 @@ export function MemberProfile({
44
48
  )}
45
49
 
46
50
  <div className="min-w-0">
47
- <p className={MICRO}>profile</p>
51
+ <p className={MICRO}>{c('kicker')}</p>
48
52
  <h1
49
53
  className={[`${HEADING} text-2xl text-foreground`, user.nameClass]
50
54
  .filter(Boolean)
@@ -59,14 +63,14 @@ export function MemberProfile({
59
63
  </div>
60
64
 
61
65
  <Frame aria-labelledby="member-stats-heading">
62
- <PanelHead id="member-stats-heading" title="Statistics" />
66
+ <PanelHead id="member-stats-heading" title={c('statsHeading')} />
63
67
  <dl className="grid grid-cols-2 gap-1.5 p-3 sm:grid-cols-3">
64
- <Field label="posts">{postCount}</Field>
65
- <Field label="joined">
68
+ <Field label={c('posts')}>{postCount.label}</Field>
69
+ <Field label={c('joined')}>
66
70
  <Stamp at={joinedAt} />
67
71
  </Field>
68
- <Field label="last visit">
69
- {lastVisitAt === null ? 'never' : <Stamp at={lastVisitAt} />}
72
+ <Field label={c('lastVisit')}>
73
+ {lastVisitAt === null ? c('never') : <Stamp at={lastVisitAt} />}
70
74
  </Field>
71
75
  {fields.map((field) => (
72
76
  <Field key={field.label} label={field.label}>
@@ -78,7 +82,7 @@ export function MemberProfile({
78
82
 
79
83
  {signatureHtml !== null && (
80
84
  <Frame aria-labelledby="member-signature-heading">
81
- <PanelHead id="member-signature-heading" title="Signature" />
85
+ <PanelHead id="member-signature-heading" title={c('signatureHeading')} />
82
86
  <div
83
87
  className="prose-md p-3 text-sm"
84
88
  dangerouslySetInnerHTML={{ __html: signatureHtml }}
@@ -89,7 +93,7 @@ export function MemberProfile({
89
93
  {regions?.plugins}
90
94
 
91
95
  {actions.length > 0 && (
92
- <nav aria-label="Member actions" className="flex flex-wrap gap-2">
96
+ <nav aria-label={c('actionsAriaLabel')} className="flex flex-wrap gap-2">
93
97
  {actions.map((action) => (
94
98
  <a key={action.href} href={action.href} className={BUTTON}>
95
99
  {action.label}
@@ -1,8 +1,11 @@
1
- import type { NavigationModel } from '@meith/theme-kit'
1
+ import type { NavigationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }) {
5
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.navigation.${key}`)
2
6
 
3
- export function Navigation({ items }: NavigationModel) {
4
7
  return (
5
- <nav aria-label="Breadcrumb" className="border-b border-border bg-card/40 px-4 py-2">
8
+ <nav aria-label={c('ariaLabel')} className="border-b border-border bg-card/40 px-4 py-2">
6
9
  <ol className="flex flex-wrap items-center gap-1.5 font-mono text-[0.6875rem] tracking-[0.1em] text-muted-foreground uppercase">
7
10
  {items.map((item, index) => {
8
11
  const isLast = index === items.length - 1
@@ -1,4 +1,5 @@
1
- import type { NoticeModel } from '@meith/theme-kit'
1
+ import type { NoticeModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  const TONE: Record<NoticeModel['kind'], string> = {
4
5
  info: 'border-l-forum-unread text-forum-unread',
@@ -7,7 +8,9 @@ const TONE: Record<NoticeModel['kind'], string> = {
7
8
  error: 'border-l-destructive text-destructive',
8
9
  }
9
10
 
10
- export function Notice({ kind, message, dismissHref }: NoticeModel) {
11
+ export function Notice({ kind, message, dismissHref, copy }: NoticeModel & { copy: SlotCopy }) {
12
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.notice.${key}`)
13
+
11
14
  return (
12
15
  <div
13
16
  role={kind === 'error' ? 'alert' : undefined}
@@ -15,7 +18,7 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
15
18
  >
16
19
  <p className="text-foreground">
17
20
  <span className="mr-2 font-mono text-[0.625rem] font-bold tracking-[0.18em] uppercase">
18
- {kind}
21
+ {c(kind)}
19
22
  </span>
20
23
  {message}
21
24
  </p>
@@ -24,7 +27,7 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
24
27
  href={dismissHref}
25
28
  className="font-mono text-[0.625rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase hover:text-primary"
26
29
  >
27
- dismiss
30
+ {c('dismiss')}
28
31
  </a>
29
32
  )}
30
33
  </div>