@meith/theme-raidframe 0.7.0 → 0.8.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 +13 -3
  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,15 +1,18 @@
1
- import type { PanelNavItemModel, PanelNavModel } from '@meith/theme-kit'
1
+ import type { PanelNavItemModel, PanelNavModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { MICRO, NUMERIC, RULE } from '../shared'
4
5
 
5
- const ITEM =
6
- 'flex items-baseline gap-2 border-l-2 px-2.5 py-1.5 text-[0.8125rem] tracking-[0.02em]'
6
+ const ITEM = 'flex items-baseline gap-2 border-l-2 px-2.5 py-1.5 text-[0.8125rem] tracking-[0.02em]'
7
7
 
8
8
  const HERE = 'border-l-primary bg-secondary font-semibold text-foreground'
9
9
  const OPEN = 'border-l-primary/40 font-semibold text-foreground'
10
- const ELSEWHERE = 'border-l-transparent text-muted-foreground hover:border-l-border hover:text-primary'
10
+ const ELSEWHERE =
11
+ 'border-l-transparent text-muted-foreground hover:border-l-border hover:text-primary'
12
+
13
+ function Count({ count, copy }: { count: number; copy: SlotCopy }) {
14
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.panelNav.${key}`)
11
15
 
12
- function Count({ count }: { count: number }) {
13
16
  return (
14
17
  <>
15
18
  <span
@@ -18,16 +21,26 @@ function Count({ count }: { count: number }) {
18
21
  >
19
22
  {count > 99 ? '99+' : count}
20
23
  </span>
21
- <span className="sr-only">({count} waiting)</span>
24
+ <span className="sr-only">
25
+ ({count} {c('waiting')})
26
+ </span>
22
27
  </>
23
28
  )
24
29
  }
25
30
 
26
- function Item({ item, className }: { item: PanelNavItemModel; className: string }) {
31
+ function Item({
32
+ item,
33
+ className,
34
+ copy,
35
+ }: {
36
+ item: PanelNavItemModel
37
+ className: string
38
+ copy: SlotCopy
39
+ }) {
27
40
  const body = (
28
41
  <>
29
42
  <span className="min-w-0 flex-1">{item.title}</span>
30
- {item.count !== null && <Count count={item.count} />}
43
+ {item.count !== null && <Count count={item.count} copy={copy} />}
31
44
  </>
32
45
  )
33
46
 
@@ -52,7 +65,11 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
52
65
  )
53
66
  }
54
67
 
55
- function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>) {
68
+ function Sections({
69
+ label,
70
+ sections,
71
+ copy,
72
+ }: Pick<PanelNavModel, 'label' | 'sections'> & { copy: SlotCopy }) {
56
73
  return (
57
74
  <nav aria-label={label}>
58
75
  <ul className="flex flex-col">
@@ -64,13 +81,18 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
64
81
  <Item
65
82
  item={section}
66
83
  className={section.current === 'here' ? HERE : section.isOpen ? OPEN : ELSEWHERE}
84
+ copy={copy}
67
85
  />
68
86
 
69
87
  {section.isOpen && section.children.length > 0 && (
70
88
  <ul className="my-0.5 ml-3 flex flex-col border-l border-border pl-1">
71
89
  {section.children.map((child) => (
72
90
  <li key={child.href}>
73
- <Item item={child} className={child.current === 'here' ? HERE : ELSEWHERE} />
91
+ <Item
92
+ item={child}
93
+ className={child.current === 'here' ? HERE : ELSEWHERE}
94
+ copy={copy}
95
+ />
74
96
  </li>
75
97
  ))}
76
98
  </ul>
@@ -82,7 +104,12 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
82
104
  )
83
105
  }
84
106
 
85
- export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
107
+ export function PanelNav({
108
+ label,
109
+ sections,
110
+ currentTitle,
111
+ copy,
112
+ }: PanelNavModel & { copy: SlotCopy }) {
86
113
  return (
87
114
  <>
88
115
  <details className="border border-border bg-card lg:hidden">
@@ -92,7 +119,7 @@ export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
92
119
  </summary>
93
120
  <div className={RULE} aria-hidden="true" />
94
121
  <div className="p-2">
95
- <Sections label={label} sections={sections} />
122
+ <Sections label={label} sections={sections} copy={copy} />
96
123
  </div>
97
124
  </details>
98
125
 
@@ -102,7 +129,7 @@ export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
102
129
  </p>
103
130
  <div className={RULE} aria-hidden="true" />
104
131
  <div className="p-2">
105
- <Sections label={label} sections={sections} />
132
+ <Sections label={label} sections={sections} copy={copy} />
106
133
  </div>
107
134
  </div>
108
135
  </>
@@ -1,13 +1,8 @@
1
- import type { PanelKind, PanelPageModel, PanelSectionModel } from '@meith/theme-kit'
1
+ import type { PanelKind, PanelPageModel, PanelSectionModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { HEADING, MICRO, RULE } from '../shared'
4
5
 
5
- const LABEL: Record<PanelKind, string> = {
6
- usercp: 'your control panel',
7
- modcp: 'moderator control panel',
8
- admincp: 'control panel',
9
- }
10
-
11
6
  export function PanelPage({
12
7
  panel,
13
8
  title,
@@ -17,16 +12,22 @@ export function PanelPage({
17
12
  gap,
18
13
  regions,
19
14
  children,
20
- }: PanelPageModel) {
15
+ copy,
16
+ }: PanelPageModel & { copy: SlotCopy }) {
17
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.panelPage.${key}`)
18
+ const LABEL: Record<PanelKind, string> = {
19
+ usercp: c('panelLabel.usercp'),
20
+ modcp: c('panelLabel.modcp'),
21
+ admincp: c('panelLabel.admincp'),
22
+ }
23
+
21
24
  return (
22
25
  <main
23
26
  id="board-content"
24
27
  tabIndex={-1}
25
28
  className={`flex w-full flex-col p-4 ${
26
29
  frame === 'standalone' ? 'mx-auto flex-1' : ''
27
- } ${width === 'wide' ? 'max-w-none' : 'max-w-4xl'} ${
28
- gap === 'loose' ? 'gap-6' : 'gap-4'
29
- }`}
30
+ } ${width === 'wide' ? 'max-w-none' : 'max-w-4xl'} ${gap === 'loose' ? 'gap-6' : 'gap-4'}`}
30
31
  >
31
32
  <div className="border border-border bg-card shadow-elevation">
32
33
  <div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-2 px-4 py-3">
@@ -1,23 +1,30 @@
1
- import type { PostActionsSlotModel } from '@meith/theme-kit'
1
+ import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  const ACTION =
4
5
  'font-mono text-[0.625rem] font-semibold uppercase tracking-[0.14em] text-muted-foreground hover:text-primary'
5
6
 
6
- export function PostActions({ actions, children }: PostActionsSlotModel) {
7
+ export function PostActions({
8
+ actions,
9
+ children,
10
+ copy,
11
+ }: PostActionsSlotModel & { copy: SlotCopy }) {
12
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.postActions.${key}`)
13
+
7
14
  const items = [
8
- { href: actions.quoteHref, label: 'quote' },
9
- { href: actions.editHref, label: 'edit' },
10
- { href: actions.restoreHref, label: 'restore' },
11
- { href: actions.rateHref, label: 'rate' },
12
- { href: actions.reportHref, label: 'report' },
13
- { href: actions.warnHref, label: 'warn' },
14
- { href: actions.moderateHref, label: 'moderate' },
15
+ { href: actions.quoteHref, label: c('quote') },
16
+ { href: actions.editHref, label: c('edit') },
17
+ { href: actions.restoreHref, label: c('restore') },
18
+ { href: actions.rateHref, label: c('rate') },
19
+ { href: actions.reportHref, label: c('report') },
20
+ { href: actions.warnHref, label: c('warn') },
21
+ { href: actions.moderateHref, label: c('moderate') },
15
22
  ].filter((item): item is { href: string; label: string } => item.href !== null)
16
23
 
17
24
  if (items.length === 0 && children === undefined) return null
18
25
 
19
26
  return (
20
- <nav aria-label="Post actions" className="flex flex-wrap items-center gap-x-4 gap-y-1">
27
+ <nav aria-label={c('ariaLabel')} className="flex flex-wrap items-center gap-x-4 gap-y-1">
21
28
  {items.map((item) => (
22
29
  <a key={item.label} href={item.href} className={ACTION}>
23
30
  {item.label}
@@ -1,10 +1,15 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import type { PostBitSlotModel } from '@meith/theme-kit'
3
+ import type { PostBitSlotModel, SlotCopy } from '@meith/theme-kit'
4
+ import { fromSlotCopy } from '@meith/theme-kit'
4
5
 
5
6
  import { HEADING, MICRO, NUMERIC, Stamp, UserRef } from '../shared'
6
7
 
7
- function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['author']['badge']> }) {
8
+ function GroupBadge({
9
+ badge,
10
+ }: {
11
+ badge: NonNullable<PostBitSlotModel['post']['author']['badge']>
12
+ }) {
8
13
  const image = (
9
14
  <img
10
15
  src={badge.src}
@@ -26,18 +31,24 @@ function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['au
26
31
  )
27
32
  }
28
33
 
29
- function StatusBanner({ visibility }: { visibility: PostBitSlotModel['post']['visibility'] }) {
34
+ function StatusBanner({
35
+ visibility,
36
+ copy,
37
+ }: {
38
+ visibility: PostBitSlotModel['post']['visibility']
39
+ copy: SlotCopy
40
+ }) {
30
41
  if (visibility === 'visible') return null
31
42
 
43
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.postBit.${key}`)
44
+
32
45
  return (
33
46
  <p
34
47
  className={`${MICRO} border-b border-border px-3 py-1.5 ${
35
48
  visibility === 'deleted' ? 'bg-thread-deleted/10' : 'bg-post-unapproved'
36
49
  }`}
37
50
  >
38
- {visibility === 'deleted'
39
- ? 'deleted — visible to moderators only'
40
- : 'awaiting approval — visible to moderators only'}
51
+ {visibility === 'deleted' ? c('deletedNotice') : c('awaitingApprovalNotice')}
41
52
  </p>
42
53
  )
43
54
  }
@@ -51,7 +62,9 @@ function StatLine({ label, children }: { label: string; children: ReactNode }) {
51
62
  )
52
63
  }
53
64
 
54
- export function PostBit({ post, select, regions }: PostBitSlotModel) {
65
+ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { copy: SlotCopy }) {
66
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.postBit.${key}`)
67
+
55
68
  return (
56
69
  <article
57
70
  id={`post-${post.number}`}
@@ -67,7 +80,7 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
67
80
  className="pointer-events-none absolute -right-px -bottom-px size-2 border-r-2 border-b-2 border-primary/70"
68
81
  />
69
82
 
70
- <StatusBanner visibility={post.visibility} />
83
+ <StatusBanner visibility={post.visibility} copy={copy} />
71
84
 
72
85
  <div className="grid grid-cols-1 sm:grid-cols-[12rem_1fr]">
73
86
  <div className="border-b border-border bg-surface px-3 py-3 sm:border-r sm:border-b-0">
@@ -115,11 +128,7 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
115
128
  aria-hidden="true"
116
129
  className="size-1.5 bg-moderation-approved shadow-[0_0_6px_var(--color-moderation-approved)]"
117
130
  />
118
- <span
119
- className={`${MICRO} text-moderation-approved`}
120
- >
121
- online
122
- </span>
131
+ <span className={`${MICRO} text-moderation-approved`}>{c('online')}</span>
123
132
  </p>
124
133
  )}
125
134
  </div>
@@ -134,12 +143,12 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
134
143
  )}
135
144
 
136
145
  <dl className="mt-2.5">
137
- <StatLine label="posts">{post.author.postCount}</StatLine>
146
+ <StatLine label={c('posts')}>{post.author.postCount.label}</StatLine>
138
147
  {post.author.reputation != null && (
139
- <StatLine label="rep">{post.author.reputation}</StatLine>
148
+ <StatLine label={c('rep')}>{post.author.reputation.label}</StatLine>
140
149
  )}
141
150
  {post.author.joinedAt !== null && (
142
- <StatLine label="joined">
151
+ <StatLine label={c('joined')}>
143
152
  <Stamp at={post.author.joinedAt} />
144
153
  </StatLine>
145
154
  )}
@@ -164,9 +173,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
164
173
 
165
174
  {post.ignored !== null ? (
166
175
  <p className="px-3 py-3 text-sm text-muted-foreground">
167
- Post from {post.ignored.authorUsername}, who you are ignoring.{' '}
176
+ {c('ignoredPrefix')}
177
+ {post.ignored.authorUsername}
178
+ {c('ignoredSuffix')}{' '}
168
179
  <a href={post.ignored.revealHref} className="text-primary hover:underline">
169
- Show it
180
+ {c('showIt')}
170
181
  </a>
171
182
  </p>
172
183
  ) : (
@@ -1,4 +1,5 @@
1
- import type { PostFormModel } from '@meith/theme-kit'
1
+ import type { PostFormModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { BUTTON, HEADING, MICRO, RULE } from '../shared'
4
5
 
@@ -9,7 +10,10 @@ export function PostForm({
9
10
  cancelLabel,
10
11
  errorMessage,
11
12
  regions,
12
- }: PostFormModel) {
13
+ copy,
14
+ }: PostFormModel & { copy: SlotCopy }) {
15
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.postForm.${key}`)
16
+
13
17
  return (
14
18
  <div className="mx-auto flex w-full max-w-4xl flex-col gap-4 p-4">
15
19
  {errorMessage !== null && (
@@ -17,7 +21,7 @@ export function PostForm({
17
21
  role="alert"
18
22
  className="border border-l-[3px] border-border border-l-destructive bg-card px-3 py-2 text-sm"
19
23
  >
20
- <span className={`${MICRO} mr-2 text-destructive`}>cannot post</span>
24
+ <span className={`${MICRO} mr-2 text-destructive`}>{c('cannotPost')}</span>
21
25
  {errorMessage}
22
26
  </p>
23
27
  )}
@@ -25,7 +29,13 @@ export function PostForm({
25
29
  <section className="border border-border bg-card shadow-elevation">
26
30
  <div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
27
31
  <div>
28
- <p className={MICRO}>{mode === 'edit' ? 'edit' : mode === 'reply' ? 'reply' : 'new thread'}</p>
32
+ <p className={MICRO}>
33
+ {mode === 'edit'
34
+ ? c('mode.edit')
35
+ : mode === 'reply'
36
+ ? c('mode.reply')
37
+ : c('mode.newThread')}
38
+ </p>
29
39
  <h1 className={`${HEADING} text-lg text-foreground`}>{heading}</h1>
30
40
  </div>
31
41
  <a href={cancelHref} className={BUTTON}>
@@ -1,23 +1,31 @@
1
- import type { RedirectNoticeModel } from '@meith/theme-kit'
1
+ import type { RedirectNoticeModel, 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 RedirectNotice({ message, targetHref, delaySeconds }: RedirectNoticeModel) {
6
+ export function RedirectNotice({
7
+ message,
8
+ targetHref,
9
+ delaySeconds,
10
+ copy,
11
+ }: RedirectNoticeModel & { copy: SlotCopy }) {
12
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.redirectNotice.${key}`)
13
+
6
14
  return (
7
15
  <Frame className="w-full max-w-lg">
8
16
  <div className="px-5 py-5">
9
- <p className={`${MICRO} text-primary`}>redirecting</p>
10
- <h1 className={`${HEADING} mt-1 text-2xl text-foreground`}>Please wait</h1>
17
+ <p className={`${MICRO} text-primary`}>{c('kicker')}</p>
18
+ <h1 className={`${HEADING} mt-1 text-2xl text-foreground`}>{c('title')}</h1>
11
19
  <p className="mt-2 text-sm text-muted-foreground">{message}</p>
12
20
 
13
21
  <div className="mt-5 flex flex-wrap items-center gap-3">
14
22
  <a href={targetHref} className={BUTTON_PRIMARY}>
15
- continue now
23
+ {c('continueNow')}
16
24
  </a>
17
25
  <span className={`${MICRO} normal-case`}>
18
- <span className="uppercase">continuing in</span>{' '}
19
- <span className={NUMERIC}>{delaySeconds}</span>
20
- {delaySeconds === 1 ? ' second' : ' seconds'}
26
+ <span className="uppercase">{c('continuingIn')}</span>{' '}
27
+ <span className={NUMERIC}>{delaySeconds}</span>{' '}
28
+ {delaySeconds === 1 ? c('second.one') : c('second.other')}
21
29
  </span>
22
30
  </div>
23
31
  </div>
@@ -1,4 +1,5 @@
1
- import type { OptionModel, SearchAdvancedModel, SearchFormModel } from '@meith/theme-kit'
1
+ import type { OptionModel, SearchAdvancedModel, SearchFormModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { BUTTON_PRIMARY, Frame, MICRO, PanelHead } from '../shared'
4
5
 
@@ -98,7 +99,10 @@ export function SearchForm({
98
99
  hint,
99
100
  errorMessage,
100
101
  advanced,
101
- }: SearchFormModel) {
102
+ copy,
103
+ }: SearchFormModel & { copy: SlotCopy }) {
104
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.searchForm.${key}`)
105
+
102
106
  const described = [
103
107
  hint === null ? null : 'search-hint',
104
108
  errorMessage === null ? null : 'search-error',
@@ -108,12 +112,12 @@ export function SearchForm({
108
112
 
109
113
  return (
110
114
  <Frame aria-labelledby="search-heading">
111
- <PanelHead id="search-heading" title="Search" />
115
+ <PanelHead id="search-heading" title={c('heading')} />
112
116
 
113
117
  <form method="get" action={action} className="flex flex-col gap-4 px-4 py-4">
114
118
  <div>
115
119
  <label htmlFor="search-query" className={`${MICRO} mb-1 block`}>
116
- Search for
120
+ {c('searchForLabel')}
117
121
  </label>
118
122
  <input
119
123
  id="search-query"
@@ -122,7 +126,7 @@ export function SearchForm({
122
126
  defaultValue={query}
123
127
  maxLength={maxQueryLength}
124
128
  autoComplete="off"
125
- placeholder="Words in a subject or a post"
129
+ placeholder={c('placeholder')}
126
130
  aria-invalid={errorMessage === null ? undefined : true}
127
131
  {...(described === '' ? {} : { 'aria-describedby': described })}
128
132
  className={CONTROL}
@@ -142,15 +146,15 @@ export function SearchForm({
142
146
  </div>
143
147
 
144
148
  <div className="grid gap-4 sm:grid-cols-2">
145
- <Choice id="search-forum" label="In" name={fields.forum} options={forums} />
146
- <Choice id="search-sort" label="Sort by" name={fields.sort} options={sorts} />
149
+ <Choice id="search-forum" label={c('inLabel')} name={fields.forum} options={forums} />
150
+ <Choice id="search-sort" label={c('sortByLabel')} name={fields.sort} options={sorts} />
147
151
  </div>
148
152
 
149
153
  {advanced !== undefined && <Advanced {...advanced} />}
150
154
 
151
155
  <div>
152
156
  <button type="submit" className={BUTTON_PRIMARY}>
153
- search
157
+ {c('submit')}
154
158
  </button>
155
159
  </div>
156
160
  </form>
@@ -1,4 +1,5 @@
1
- import type { OptionModel, SearchRefineModel, SearchResultsModel } from '@meith/theme-kit'
1
+ import type { OptionModel, SearchRefineModel, SearchResultsModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { BUTTON_PRIMARY, Frame, HEADING, MICRO, RULE, Stamp } from '../shared'
4
5
 
@@ -15,7 +16,10 @@ export function SearchResults({
15
16
  within,
16
17
  refine,
17
18
  regions,
18
- }: SearchResultsModel) {
19
+ copy,
20
+ }: SearchResultsModel & { copy: SlotCopy }) {
21
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.searchResults.${key}`)
22
+
19
23
  return (
20
24
  <main
21
25
  id="board-content"
@@ -24,34 +28,37 @@ export function SearchResults({
24
28
  >
25
29
  <div className="border border-border bg-card shadow-elevation">
26
30
  <div className="px-4 py-3">
27
- <p className={MICRO}>search</p>
31
+ <p className={MICRO}>{c('kicker')}</p>
28
32
  <h1 className={`${HEADING} text-xl text-foreground`}>
29
- Results for &ldquo;{terms}&rdquo;
33
+ {c('resultsForPrefix')}
34
+ {terms}
35
+ {c('resultsForSuffix')}
30
36
  </h1>
31
37
  <p className={`${MICRO} mt-1 normal-case`}>
32
- <span className="uppercase">run</span> <Stamp at={searchedAt} />
38
+ <span className="uppercase">{c('run')}</span> <Stamp at={searchedAt} />
33
39
  <span className="text-border">{' | '}</span>
34
- <span>checked against your access every time this page is opened</span>
40
+ <span>{c('accessNotice')}</span>
35
41
  </p>
36
42
  </div>
37
43
  <div className={RULE} aria-hidden="true" />
38
44
  </div>
39
45
 
40
- {refine !== undefined && <Refine {...refine} />}
46
+ {refine !== undefined && <Refine {...refine} copy={copy} />}
41
47
 
42
48
  <Frame>
43
49
  {hits.length === 0 ? (
44
50
  <div className="px-4 py-6 text-center">
45
- <p className={`${HEADING} text-sm text-foreground`}>Nothing matched</p>
46
- <p className="mt-1 text-sm text-muted-foreground">
47
- Or nothing you can see does. Try fewer words, or a different spelling.
48
- </p>
51
+ <p className={`${HEADING} text-sm text-foreground`}>{c('nothingMatched')}</p>
52
+ <p className="mt-1 text-sm text-muted-foreground">{c('nothingMatchedHint')}</p>
49
53
  </div>
50
54
  ) : (
51
55
  <ul className="divide-y divide-border">
52
56
  {hits.map((hit) => (
53
57
  <li key={hit.postId} className="flex flex-col gap-1 px-4 py-3 hover:bg-secondary/50">
54
- <a href={hit.href} className="text-sm font-medium text-foreground hover:text-primary">
58
+ <a
59
+ href={hit.href}
60
+ className="text-sm font-medium text-foreground hover:text-primary"
61
+ >
55
62
  {hit.threadTitle}
56
63
  </a>
57
64
  <p
@@ -120,7 +127,7 @@ export function SearchResults({
120
127
 
121
128
  <p>
122
129
  <a href={newSearchHref} className={`${MICRO} hover:text-primary`}>
123
- start a new search
130
+ {c('startNewSearch')}
124
131
  </a>
125
132
  </p>
126
133
  </main>
@@ -138,7 +145,10 @@ function Refine({
138
145
  submitLabel,
139
146
  applied,
140
147
  clearHref,
141
- }: SearchRefineModel) {
148
+ copy,
149
+ }: SearchRefineModel & { copy: SlotCopy }) {
150
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.searchResults.${key}`)
151
+
142
152
  return (
143
153
  <section
144
154
  aria-label={label}
@@ -172,7 +182,7 @@ function Refine({
172
182
  >
173
183
  {chip.label}
174
184
  <span aria-hidden="true">×</span>
175
- <span className="sr-only">— remove this filter</span>
185
+ <span className="sr-only">{c('removeFilter')}</span>
176
186
  </a>
177
187
  ))}
178
188
  </div>
@@ -192,7 +202,7 @@ function Refine({
192
202
 
193
203
  {clearHref !== null && (
194
204
  <a href={clearHref} className={`${MICRO} hover:text-primary`}>
195
- clear filters
205
+ {c('clearFilters')}
196
206
  </a>
197
207
  )}
198
208
  </form>
@@ -1,4 +1,5 @@
1
- import type { ShellModel } from '@meith/theme-kit'
1
+ import type { ShellModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  const APP_REGIONS = `
4
5
  [data-theme-raidframe] .font-heading:not([class*="tracking-"]) {
@@ -7,7 +8,9 @@ const APP_REGIONS = `
7
8
  }
8
9
  `
9
10
 
10
- export function Shell({ viewer, children }: ShellModel) {
11
+ export function Shell({ viewer, children, copy }: ShellModel & { copy: SlotCopy }) {
12
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.shell.${key}`)
13
+
11
14
  return (
12
15
  <div
13
16
  className="relative min-h-dvh bg-background text-foreground"
@@ -29,7 +32,7 @@ export function Shell({ viewer, children }: ShellModel) {
29
32
  href="#board-content"
30
33
  className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:border focus:border-primary focus:bg-card focus:px-3 focus:py-2 focus:font-mono focus:text-xs focus:uppercase"
31
34
  >
32
- Skip to content
35
+ {c('skipToContent')}
33
36
  </a>
34
37
 
35
38
  <div className="relative mx-auto flex min-h-dvh w-full max-w-6xl flex-col border-x border-border bg-background/80">
@@ -1,16 +1,19 @@
1
- import type { SubforumListModel } from '@meith/theme-kit'
1
+ import type { SlotCopy, SubforumListModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { MICRO, NUMERIC } from '../shared'
4
5
 
5
- export function SubforumList({ forums }: SubforumListModel) {
6
+ export function SubforumList({ forums, copy }: SubforumListModel & { copy: SlotCopy }) {
6
7
  if (forums.length === 0) return null
7
8
 
9
+ const c = (key: string) => fromSlotCopy(copy, `raidframe.subforumList.${key}`)
10
+
8
11
  return (
9
12
  <nav
10
- aria-label="Subforums"
13
+ aria-label={c('ariaLabel')}
11
14
  className="flex flex-wrap items-center gap-2 border border-border bg-card px-3 py-2"
12
15
  >
13
- <span className={MICRO}>subforums</span>
16
+ <span className={MICRO}>{c('label')}</span>
14
17
  {forums.map((forum) => (
15
18
  <a
16
19
  key={forum.href}
@@ -18,7 +21,7 @@ export function SubforumList({ forums }: SubforumListModel) {
18
21
  className="inline-flex items-center gap-1.5 border border-border bg-surface px-2 py-0.5 font-mono text-[0.625rem] tracking-[0.1em] text-foreground uppercase hover:border-primary/60 hover:text-primary"
19
22
  >
20
23
  {forum.title}
21
- <span className={`${NUMERIC} text-muted-foreground`}>{forum.threadCount}</span>
24
+ <span className={`${NUMERIC} text-muted-foreground`}>{forum.threadCount.label}</span>
22
25
  </a>
23
26
  ))}
24
27
  </nav>