@meith/web 0.33.1 → 0.33.3
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/app/(board)/members/page.tsx +1 -1
- package/app/(board)/thread/[slug]/page.tsx +3 -7
- package/app/admin/content/attachments/page.tsx +2 -2
- package/app/admin/layout.tsx +21 -4
- package/app/admin/users/[id]/merge/page.tsx +1 -1
- package/app/admin/users/page.tsx +1 -1
- package/app/admin/users/prune/page.tsx +1 -1
- package/package.json +49 -49
- package/src/components/account/avatar-form.tsx +1 -1
- package/src/components/account/notification-forms.tsx +1 -1
- package/src/components/account/reputation-forms.tsx +1 -1
- package/src/components/account/usercp-forms.tsx +1 -1
- package/src/components/admin/form-bits.tsx +1 -1
- package/src/components/admin/forum-forms.tsx +4 -1
- package/src/components/admin/oklch-picker.tsx +1 -1
- package/src/components/admin/system-forms.tsx +1 -1
- package/src/components/admin/theme-forms.tsx +2 -2
- package/src/components/content/edit-post-form.tsx +1 -1
- package/src/components/content/new-thread-form.tsx +3 -3
- package/src/components/messages/message-forms.tsx +1 -1
- package/src/components/moderation/warning-forms.tsx +1 -1
- package/src/components/shell/panel-list.tsx +4 -4
- package/src/components/shell/panel-overview.tsx +48 -15
- package/src/server/upgrade-notice.ts +33 -4
- package/src/styles/globals.css +32 -2
|
@@ -22,7 +22,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const INPUT =
|
|
25
|
-
'rounded-md border border-
|
|
25
|
+
'rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
26
26
|
|
|
27
27
|
export default async function MembersPage({
|
|
28
28
|
searchParams,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
2
|
import { notFound, redirect } from 'next/navigation'
|
|
3
|
-
import { after as scheduleAfterResponse } from 'next/server'
|
|
4
3
|
|
|
5
4
|
import { currentRequestId } from '@meith/core/logger'
|
|
6
5
|
import { acceptsThreads, canHoldThreads } from '@meith/forums'
|
|
@@ -268,14 +267,11 @@ export default async function ThreadPage({
|
|
|
268
267
|
})
|
|
269
268
|
|
|
270
269
|
if (readState !== null && actor.userId !== null) {
|
|
271
|
-
const readerId = actor.userId
|
|
272
270
|
const lastRenderedPostId = postPage.rows.at(-1)?.id
|
|
273
271
|
if (lastRenderedPostId !== undefined) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.catch(() => undefined),
|
|
278
|
-
)
|
|
272
|
+
await readState
|
|
273
|
+
.markThreadRead(actor.userId, thread.id, lastRenderedPostId, new Date())
|
|
274
|
+
.catch(() => undefined)
|
|
279
275
|
}
|
|
280
276
|
}
|
|
281
277
|
|
|
@@ -111,7 +111,7 @@ export default async function AdminAttachmentsPage({
|
|
|
111
111
|
<input
|
|
112
112
|
name="filename"
|
|
113
113
|
defaultValue={filename ?? ''}
|
|
114
|
-
className="w-full rounded-md border border-
|
|
114
|
+
className="w-full rounded-md border border-input bg-card px-3 py-2 text-sm"
|
|
115
115
|
/>
|
|
116
116
|
</label>
|
|
117
117
|
|
|
@@ -120,7 +120,7 @@ export default async function AdminAttachmentsPage({
|
|
|
120
120
|
<select
|
|
121
121
|
name="status"
|
|
122
122
|
defaultValue={status ?? ''}
|
|
123
|
-
className="rounded-md border border-
|
|
123
|
+
className="rounded-md border border-input bg-card px-3 py-2 text-sm"
|
|
124
124
|
>
|
|
125
125
|
<option value="">{translator.t('adminAttachments.any')}</option>
|
|
126
126
|
<option value="ready">{translator.t('adminAttachments.ready')}</option>
|
package/app/admin/layout.tsx
CHANGED
|
@@ -73,13 +73,30 @@ export default async function AdminLayout({ children }: { children: React.ReactN
|
|
|
73
73
|
{await tr('page.skip-content')}
|
|
74
74
|
</a>
|
|
75
75
|
|
|
76
|
-
<header className="sticky top-0 z-30 border-b border-border bg-card">
|
|
77
|
-
<div className="mx-auto flex
|
|
76
|
+
<header className="sticky top-0 z-30 border-b border-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/85">
|
|
77
|
+
<div className="mx-auto flex h-14 w-full max-w-6xl items-center gap-x-3 px-4 sm:gap-x-4 sm:px-6">
|
|
78
78
|
<a
|
|
79
79
|
href="/admin"
|
|
80
|
-
className="min-w-0
|
|
80
|
+
className="flex min-w-0 items-center gap-2.5 font-heading text-base font-semibold whitespace-nowrap text-foreground sm:text-lg"
|
|
81
81
|
>
|
|
82
|
-
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm"
|
|
85
|
+
>
|
|
86
|
+
<svg
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
viewBox="0 0 16 16"
|
|
89
|
+
className="size-4"
|
|
90
|
+
fill="none"
|
|
91
|
+
stroke="currentColor"
|
|
92
|
+
strokeWidth="1.5"
|
|
93
|
+
strokeLinecap="round"
|
|
94
|
+
strokeLinejoin="round"
|
|
95
|
+
>
|
|
96
|
+
<path d="M3 4.5h10M3 8h10M3 11.5h10M6 3v3M10.5 6.5v3M5 10v3" />
|
|
97
|
+
</svg>
|
|
98
|
+
</span>
|
|
99
|
+
<span className="truncate">{await tr('page.control-panel')}</span>
|
|
83
100
|
</a>
|
|
84
101
|
<div className="ml-auto flex shrink-0 items-center gap-3 text-sm whitespace-nowrap sm:gap-4">
|
|
85
102
|
<AdminSignOutForm copy={adminFormsCopy(t)} />
|
|
@@ -17,7 +17,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const INPUT =
|
|
20
|
-
'w-full rounded-md border border-
|
|
20
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
21
21
|
|
|
22
22
|
export default async function AdminMergePage({
|
|
23
23
|
params,
|
package/app/admin/users/page.tsx
CHANGED
|
@@ -24,7 +24,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const INPUT =
|
|
27
|
-
'w-full rounded-md border border-
|
|
27
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
28
28
|
|
|
29
29
|
export default async function AdminUsersPage({
|
|
30
30
|
searchParams,
|
|
@@ -16,7 +16,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const INPUT =
|
|
19
|
-
'w-full rounded-md border border-
|
|
19
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
20
20
|
|
|
21
21
|
export default async function AdminPrunePage({
|
|
22
22
|
searchParams,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.3",
|
|
4
4
|
"description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,54 +43,54 @@
|
|
|
43
43
|
"react-dom": "19.2.8",
|
|
44
44
|
"tailwindcss": "^4.3.3",
|
|
45
45
|
"typescript": "7.0.2",
|
|
46
|
-
"@meith/accounts": "0.33.
|
|
47
|
-
"@meith/admin": "0.33.
|
|
48
|
-
"@meith/antispam": "0.33.
|
|
49
|
-
"@meith/api": "0.33.
|
|
50
|
-
"@meith/attachments": "0.33.
|
|
51
|
-
"@meith/authorization": "0.33.
|
|
52
|
-
"@meith/avatars": "0.33.
|
|
53
|
-
"@meith/board-digest": "0.33.
|
|
54
|
-
"@meith/core": "0.33.
|
|
55
|
-
"@meith/db": "0.33.
|
|
56
|
-
"@meith/demo": "0.33.
|
|
57
|
-
"@meith/drafts": "0.33.
|
|
58
|
-
"@meith/drivers": "0.33.
|
|
59
|
-
"@meith/events": "0.33.
|
|
60
|
-
"@meith/forums": "0.33.
|
|
61
|
-
"@meith/groups": "0.33.
|
|
62
|
-
"@meith/i18n": "0.33.
|
|
63
|
-
"@meith/import": "0.33.
|
|
64
|
-
"@meith/install": "0.33.
|
|
65
|
-
"@meith/mail": "0.33.
|
|
66
|
-
"@meith/markdown": "0.33.
|
|
67
|
-
"@meith/marketplace": "0.33.
|
|
68
|
-
"@meith/messages": "0.33.
|
|
69
|
-
"@meith/moderation": "0.33.
|
|
70
|
-
"@meith/notifications": "0.33.
|
|
71
|
-
"@meith/plugin-calendar": "0.33.
|
|
72
|
-
"@meith/plugin-dues": "0.33.
|
|
73
|
-
"@meith/plugin-kit": "0.33.
|
|
74
|
-
"@meith/polls": "0.33.
|
|
75
|
-
"@meith/posts": "0.33.
|
|
76
|
-
"@meith/profile-fields": "0.33.
|
|
77
|
-
"@meith/
|
|
78
|
-
"@meith/
|
|
79
|
-
"@meith/runtime": "0.33.
|
|
80
|
-
"@meith/search": "0.33.
|
|
81
|
-
"@meith/settings": "0.33.
|
|
82
|
-
"@meith/signatures": "0.33.
|
|
83
|
-
"@meith/subscriptions": "0.33.
|
|
84
|
-
"@meith/tasks": "0.33.
|
|
85
|
-
"@meith/theme-
|
|
86
|
-
"@meith/theme-
|
|
87
|
-
"@meith/theme-kit": "0.33.
|
|
88
|
-
"@meith/theme-midnight": "0.33.
|
|
89
|
-
"@meith/theme-phasebook": "0.33.
|
|
90
|
-
"@meith/theme-raidframe": "0.33.
|
|
91
|
-
"@meith/threads": "0.33.
|
|
92
|
-
"@meith/ui": "0.33.
|
|
93
|
-
"@meith/upgrade": "0.33.
|
|
46
|
+
"@meith/accounts": "0.33.3",
|
|
47
|
+
"@meith/admin": "0.33.3",
|
|
48
|
+
"@meith/antispam": "0.33.3",
|
|
49
|
+
"@meith/api": "0.33.3",
|
|
50
|
+
"@meith/attachments": "0.33.3",
|
|
51
|
+
"@meith/authorization": "0.33.3",
|
|
52
|
+
"@meith/avatars": "0.33.3",
|
|
53
|
+
"@meith/board-digest": "0.33.3",
|
|
54
|
+
"@meith/core": "0.33.3",
|
|
55
|
+
"@meith/db": "0.33.3",
|
|
56
|
+
"@meith/demo": "0.33.3",
|
|
57
|
+
"@meith/drafts": "0.33.3",
|
|
58
|
+
"@meith/drivers": "0.33.3",
|
|
59
|
+
"@meith/events": "0.33.3",
|
|
60
|
+
"@meith/forums": "0.33.3",
|
|
61
|
+
"@meith/groups": "0.33.3",
|
|
62
|
+
"@meith/i18n": "0.33.3",
|
|
63
|
+
"@meith/import": "0.33.3",
|
|
64
|
+
"@meith/install": "0.33.3",
|
|
65
|
+
"@meith/mail": "0.33.3",
|
|
66
|
+
"@meith/markdown": "0.33.3",
|
|
67
|
+
"@meith/marketplace": "0.33.3",
|
|
68
|
+
"@meith/messages": "0.33.3",
|
|
69
|
+
"@meith/moderation": "0.33.3",
|
|
70
|
+
"@meith/notifications": "0.33.3",
|
|
71
|
+
"@meith/plugin-calendar": "0.33.3",
|
|
72
|
+
"@meith/plugin-dues": "0.33.3",
|
|
73
|
+
"@meith/plugin-kit": "0.33.3",
|
|
74
|
+
"@meith/polls": "0.33.3",
|
|
75
|
+
"@meith/posts": "0.33.3",
|
|
76
|
+
"@meith/profile-fields": "0.33.3",
|
|
77
|
+
"@meith/reputation": "0.33.3",
|
|
78
|
+
"@meith/relations": "0.33.3",
|
|
79
|
+
"@meith/runtime": "0.33.3",
|
|
80
|
+
"@meith/search": "0.33.3",
|
|
81
|
+
"@meith/settings": "0.33.3",
|
|
82
|
+
"@meith/signatures": "0.33.3",
|
|
83
|
+
"@meith/subscriptions": "0.33.3",
|
|
84
|
+
"@meith/tasks": "0.33.3",
|
|
85
|
+
"@meith/theme-default": "0.33.3",
|
|
86
|
+
"@meith/theme-clubhouse": "0.33.3",
|
|
87
|
+
"@meith/theme-kit": "0.33.3",
|
|
88
|
+
"@meith/theme-midnight": "0.33.3",
|
|
89
|
+
"@meith/theme-phasebook": "0.33.3",
|
|
90
|
+
"@meith/theme-raidframe": "0.33.3",
|
|
91
|
+
"@meith/threads": "0.33.3",
|
|
92
|
+
"@meith/ui": "0.33.3",
|
|
93
|
+
"@meith/upgrade": "0.33.3"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
96
|
"dev": "next dev",
|
|
@@ -92,7 +92,7 @@ export function AvatarForm({
|
|
|
92
92
|
type="file"
|
|
93
93
|
name="avatar"
|
|
94
94
|
accept=".png,.jpg,.jpeg"
|
|
95
|
-
className="rounded-md border border-
|
|
95
|
+
className="rounded-md border border-input bg-card px-3 py-2 text-sm file:mr-3 file:rounded file:border-0 file:bg-muted file:px-3 file:py-1 file:text-sm"
|
|
96
96
|
/>
|
|
97
97
|
<span className="text-xs text-muted-foreground">
|
|
98
98
|
{fromCopy(copy, 'accountForm.avatar.hint')}
|
|
@@ -204,7 +204,7 @@ export function AnnouncementsOptInForm({ optedIn, copy }: { optedIn: boolean; co
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
const FIELD =
|
|
207
|
-
'w-full max-w-xs rounded-md border border-
|
|
207
|
+
'w-full max-w-xs rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
208
208
|
|
|
209
209
|
export function BoardDigestCadenceForm({ cadence, copy }: { cadence: string; copy: Copy }) {
|
|
210
210
|
const [state, action] = useActionState(saveBoardDigestCadenceAction, EMPTY_STATE)
|
|
@@ -9,7 +9,7 @@ import { FormError, PendingButton } from '../auth/form-controls'
|
|
|
9
9
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
10
10
|
|
|
11
11
|
const FIELD =
|
|
12
|
-
'w-full rounded-md border border-
|
|
12
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
13
13
|
|
|
14
14
|
const CARD = 'flex flex-col gap-3 rounded-lg border border-border bg-card p-5'
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ import { type Copy, fromCopy } from '../shell/copy'
|
|
|
22
22
|
export type { CustomFieldInput }
|
|
23
23
|
|
|
24
24
|
const FIELD =
|
|
25
|
-
'w-full rounded-md border border-
|
|
25
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
26
26
|
|
|
27
27
|
const BUTTON =
|
|
28
28
|
'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Alert, AlertDescription } from '@meith/ui'
|
|
2
2
|
|
|
3
3
|
export const INPUT =
|
|
4
|
-
'w-full rounded-md border border-
|
|
4
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
5
5
|
|
|
6
6
|
export function Saved({ when = true, children }: { when?: boolean; children: React.ReactNode }) {
|
|
7
7
|
if (!when) return null
|
|
@@ -368,7 +368,10 @@ function MatrixSection({
|
|
|
368
368
|
</thead>
|
|
369
369
|
<tbody>
|
|
370
370
|
{rows.map((row) => (
|
|
371
|
-
<tr
|
|
371
|
+
<tr
|
|
372
|
+
key={row.key}
|
|
373
|
+
className="scroll-mt-16 scroll-mb-24 border-b border-border/60 last:border-b-0"
|
|
374
|
+
>
|
|
372
375
|
<th
|
|
373
376
|
scope="row"
|
|
374
377
|
title={row.description}
|
|
@@ -120,7 +120,7 @@ export function OklchPicker({
|
|
|
120
120
|
value={value}
|
|
121
121
|
onChange={(event) => onChange(event.target.value)}
|
|
122
122
|
placeholder={shipped}
|
|
123
|
-
className="w-full rounded-md border border-
|
|
123
|
+
className="w-full rounded-md border border-input bg-card px-3 py-2 font-mono text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
124
124
|
/>
|
|
125
125
|
</div>
|
|
126
126
|
|
|
@@ -135,7 +135,7 @@ export function RetryJobForm({ copy }: { copy: Copy }) {
|
|
|
135
135
|
<span className="font-medium">{fromCopy(copy, 'adminPanel.system.jobId')}</span>
|
|
136
136
|
<input
|
|
137
137
|
name="jobId"
|
|
138
|
-
className="w-full rounded-md border border-
|
|
138
|
+
className="w-full rounded-md border border-input bg-card px-3 py-2 font-mono text-xs"
|
|
139
139
|
required
|
|
140
140
|
/>
|
|
141
141
|
<span className="text-xs text-muted-foreground">
|
|
@@ -40,7 +40,7 @@ import { type CellState, PaletteGrid } from './theme-palette'
|
|
|
40
40
|
import { ThemePreview, ValidatedSample } from './theme-preview'
|
|
41
41
|
|
|
42
42
|
const INPUT =
|
|
43
|
-
'w-full rounded-md border border-
|
|
43
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 font-mono text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
44
44
|
|
|
45
45
|
const GHOST_BUTTON =
|
|
46
46
|
'inline-flex h-8 items-center justify-center rounded-md border border-border px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
@@ -348,7 +348,7 @@ export function ThemeEditorForm({
|
|
|
348
348
|
value={query}
|
|
349
349
|
onChange={(event) => setQuery(event.target.value)}
|
|
350
350
|
placeholder={fromCopy(copy, 'adminTheme.find.placeholder')}
|
|
351
|
-
className="w-full rounded-md border border-
|
|
351
|
+
className="w-full rounded-md border border-input bg-card px-3 py-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
352
352
|
/>
|
|
353
353
|
</label>
|
|
354
354
|
|
|
@@ -117,7 +117,7 @@ export function EditPostForm({
|
|
|
117
117
|
name="reason"
|
|
118
118
|
maxLength={200}
|
|
119
119
|
defaultValue={state.values?.reason ?? reason ?? ''}
|
|
120
|
-
className="rounded-md border border-
|
|
120
|
+
className="rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
121
121
|
/>
|
|
122
122
|
</label>
|
|
123
123
|
|
|
@@ -128,7 +128,7 @@ export function NewThreadForm({
|
|
|
128
128
|
<select
|
|
129
129
|
name="prefixId"
|
|
130
130
|
defaultValue={state.values?.prefixId ?? draft?.prefixId?.toString() ?? ''}
|
|
131
|
-
className="h-10 rounded-md border border-
|
|
131
|
+
className="h-10 rounded-md border border-input bg-card px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
132
132
|
>
|
|
133
133
|
<option value="">
|
|
134
134
|
{requiresPrefix
|
|
@@ -183,7 +183,7 @@ export function NewThreadForm({
|
|
|
183
183
|
min={0}
|
|
184
184
|
step={1}
|
|
185
185
|
defaultValue={pollDraft?.maxOptions ?? '1'}
|
|
186
|
-
className="h-10 rounded-md border border-
|
|
186
|
+
className="h-10 rounded-md border border-input bg-card px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
187
187
|
/>
|
|
188
188
|
<span className="text-xs text-muted-foreground">
|
|
189
189
|
{fromCopy(copy, 'composer.newThread.pollChoicesHint')}
|
|
@@ -215,7 +215,7 @@ export function NewThreadForm({
|
|
|
215
215
|
type="datetime-local"
|
|
216
216
|
name="pollClosesAt"
|
|
217
217
|
defaultValue={pollDraft?.closesAt ?? ''}
|
|
218
|
-
className="h-10 rounded-md border border-
|
|
218
|
+
className="h-10 rounded-md border border-input bg-card px-3 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
219
219
|
/>
|
|
220
220
|
<span className="text-xs text-muted-foreground">
|
|
221
221
|
{fromCopy(copy, 'pollEdit.closesAtHint')}
|
|
@@ -12,7 +12,7 @@ import { type Copy, fromCopy } from '../shell/copy'
|
|
|
12
12
|
import { RecipientField } from './recipient-field'
|
|
13
13
|
|
|
14
14
|
const FIELD =
|
|
15
|
-
'w-full rounded-md border border-
|
|
15
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
16
16
|
|
|
17
17
|
const BUTTON =
|
|
18
18
|
'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
@@ -9,7 +9,7 @@ import { FormError, PendingButton } from '../auth/form-controls'
|
|
|
9
9
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
10
10
|
|
|
11
11
|
const FIELD =
|
|
12
|
-
'w-full rounded-md border border-
|
|
12
|
+
'w-full rounded-md border border-input bg-card px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
13
13
|
|
|
14
14
|
const BUTTON =
|
|
15
15
|
'inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
@@ -4,13 +4,13 @@ export const PANEL_LIST =
|
|
|
4
4
|
'flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border bg-card shadow-elevation'
|
|
5
5
|
|
|
6
6
|
export const PANEL_ROW =
|
|
7
|
-
'flex flex-wrap items-center justify-between gap-x-3 gap-y-2 px-4 py-3 transition-colors hover:bg-muted/40'
|
|
7
|
+
'flex flex-wrap items-center justify-between gap-x-3 gap-y-2 px-4 py-3 transition-colors hover:bg-muted/40 sm:px-5'
|
|
8
8
|
|
|
9
9
|
export const PANEL_CARD =
|
|
10
|
-
'flex flex-col gap-
|
|
10
|
+
'flex flex-col gap-4 rounded-xl border border-border bg-card p-5 shadow-elevation sm:p-6'
|
|
11
11
|
|
|
12
12
|
export const PANEL_NOTE =
|
|
13
|
-
'rounded-xl border border-border bg-card
|
|
13
|
+
'rounded-xl border border-border bg-card px-5 py-4 text-sm text-muted-foreground shadow-elevation'
|
|
14
14
|
|
|
15
15
|
export function PanelList({
|
|
16
16
|
children,
|
|
@@ -85,7 +85,7 @@ export function PanelEmpty({
|
|
|
85
85
|
readonly description?: React.ReactNode
|
|
86
86
|
}) {
|
|
87
87
|
return (
|
|
88
|
-
<div className="rounded-xl border border-border bg-card
|
|
88
|
+
<div className="rounded-xl border border-dashed border-border bg-card/60">
|
|
89
89
|
<Empty className="py-10">
|
|
90
90
|
<EmptyTitle>{title}</EmptyTitle>
|
|
91
91
|
{description !== undefined && <EmptyDescription>{description}</EmptyDescription>}
|
|
@@ -12,13 +12,15 @@ export interface WaitingItem {
|
|
|
12
12
|
|
|
13
13
|
function PanelWaiting({ item }: { item: WaitingItem }) {
|
|
14
14
|
return (
|
|
15
|
-
<Card className="flex-1 border-
|
|
16
|
-
<CardContent className="flex items-center justify-between gap-4 p-
|
|
15
|
+
<Card className="flex-1 rounded-xl border-l-4 border-l-thread-pinned">
|
|
16
|
+
<CardContent className="flex items-center justify-between gap-4 p-5">
|
|
17
17
|
<div>
|
|
18
|
-
<p className="text-
|
|
18
|
+
<p className="text-3xl font-semibold tracking-tight tabular-nums text-foreground">
|
|
19
|
+
{item.count}
|
|
20
|
+
</p>
|
|
19
21
|
<p className="text-sm text-muted-foreground">{item.count === 1 ? item.one : item.many}</p>
|
|
20
22
|
</div>
|
|
21
|
-
<a href={item.href} className={buttonVariants({ variant: '
|
|
23
|
+
<a href={item.href} className={buttonVariants({ variant: 'primary', size: 'sm' })}>
|
|
22
24
|
{item.action}
|
|
23
25
|
</a>
|
|
24
26
|
</CardContent>
|
|
@@ -39,12 +41,29 @@ export function PanelWaitingList({
|
|
|
39
41
|
|
|
40
42
|
if (waiting.length === 0) {
|
|
41
43
|
return (
|
|
42
|
-
<
|
|
44
|
+
<div className="rounded-xl border border-dashed border-border bg-card/60">
|
|
43
45
|
<Empty className="py-8">
|
|
46
|
+
<span
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
className="mb-1 flex size-9 items-center justify-center rounded-full bg-moderation-approved/10 text-moderation-approved"
|
|
49
|
+
>
|
|
50
|
+
<svg
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
viewBox="0 0 16 16"
|
|
53
|
+
className="size-4"
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
strokeWidth="1.75"
|
|
57
|
+
strokeLinecap="round"
|
|
58
|
+
strokeLinejoin="round"
|
|
59
|
+
>
|
|
60
|
+
<path d="m3.5 8.5 3 3 6-7" />
|
|
61
|
+
</svg>
|
|
62
|
+
</span>
|
|
44
63
|
<EmptyTitle>{emptyTitle}</EmptyTitle>
|
|
45
64
|
<EmptyDescription>{emptyDescription}</EmptyDescription>
|
|
46
65
|
</Empty>
|
|
47
|
-
</
|
|
66
|
+
</div>
|
|
48
67
|
)
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -62,15 +81,29 @@ export function PanelSectionGrid({ sections }: { sections: readonly PanelSection
|
|
|
62
81
|
<ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
63
82
|
{sections.map((section) => (
|
|
64
83
|
<li key={section.href}>
|
|
65
|
-
<Card className="relative h-full transition-
|
|
66
|
-
<CardContent className="flex flex-col gap-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
<Card className="group relative h-full rounded-xl transition-[border-color,box-shadow] hover:border-primary/40 hover:shadow-lg">
|
|
85
|
+
<CardContent className="flex h-full flex-col gap-1 p-4">
|
|
86
|
+
<span className="flex items-center justify-between gap-2">
|
|
87
|
+
<a
|
|
88
|
+
href={section.href}
|
|
89
|
+
className="text-sm font-semibold text-foreground underline-offset-2 group-hover:text-primary"
|
|
90
|
+
>
|
|
91
|
+
<span className="absolute inset-0" />
|
|
92
|
+
{section.title}
|
|
93
|
+
</a>
|
|
94
|
+
<svg
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
viewBox="0 0 16 16"
|
|
97
|
+
className="size-4 shrink-0 text-muted-foreground/60 transition-[color,transform] group-hover:translate-x-0.5 group-hover:text-primary"
|
|
98
|
+
fill="none"
|
|
99
|
+
stroke="currentColor"
|
|
100
|
+
strokeWidth="1.5"
|
|
101
|
+
strokeLinecap="round"
|
|
102
|
+
strokeLinejoin="round"
|
|
103
|
+
>
|
|
104
|
+
<path d="M3.5 8h9M8.5 4l4 4-4 4" />
|
|
105
|
+
</svg>
|
|
106
|
+
</span>
|
|
74
107
|
<p className="text-xs text-muted-foreground">{section.blurb}</p>
|
|
75
108
|
</CardContent>
|
|
76
109
|
</Card>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import 'server-only'
|
|
2
2
|
|
|
3
|
-
import { env, logger, ValidationError } from '@meith/core'
|
|
3
|
+
import { ConfigurationError, env, logger, ValidationError } from '@meith/core'
|
|
4
4
|
import {
|
|
5
5
|
appliedPluginMigrations,
|
|
6
6
|
applyPluginMigration,
|
|
7
7
|
getDb,
|
|
8
8
|
PostgresNavigationRepository,
|
|
9
|
+
pendingCoreMigrations,
|
|
9
10
|
readVersion,
|
|
10
11
|
recordVersion,
|
|
11
12
|
} from '@meith/db'
|
|
@@ -16,12 +17,29 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
|
|
|
16
17
|
|
|
17
18
|
import { activeDefinitions } from './plugin-host'
|
|
18
19
|
|
|
19
|
-
export const CODE_VERSION = '0.33.
|
|
20
|
+
export const CODE_VERSION = '0.33.3'
|
|
20
21
|
|
|
21
22
|
export interface UpgradeApplied {
|
|
22
23
|
readonly plugins: readonly string[]
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
let coreMigrationsUnreadable = false
|
|
27
|
+
|
|
28
|
+
async function missingCoreMigrations(db: ReturnType<typeof getDb>): Promise<readonly string[]> {
|
|
29
|
+
if (coreMigrationsUnreadable) return []
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return await pendingCoreMigrations(db)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof ConfigurationError) coreMigrationsUnreadable = true
|
|
35
|
+
logger().warn(
|
|
36
|
+
{ err: String(error) },
|
|
37
|
+
'could not read the pending core migrations, comparing versions only',
|
|
38
|
+
)
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export async function applyPendingUpgrade(): Promise<UpgradeApplied> {
|
|
26
44
|
const db = getDb()
|
|
27
45
|
const definitions = activeDefinitions()
|
|
@@ -31,10 +49,12 @@ export async function applyPendingUpgrade(): Promise<UpgradeApplied> {
|
|
|
31
49
|
applied[plugin.key] = await appliedPluginMigrations(db, plugin.key)
|
|
32
50
|
}
|
|
33
51
|
|
|
52
|
+
const pendingCore = await missingCoreMigrations(db)
|
|
53
|
+
|
|
34
54
|
const state: UpgradeState = {
|
|
35
55
|
recordedVersion: (await readVersion(db, 'core')) ?? CODE_VERSION,
|
|
36
56
|
codeVersion: CODE_VERSION,
|
|
37
|
-
pendingCoreMigrations:
|
|
57
|
+
pendingCoreMigrations: pendingCore,
|
|
38
58
|
plugins: definitions.map((plugin) => ({
|
|
39
59
|
key: plugin.key,
|
|
40
60
|
version: plugin.version,
|
|
@@ -52,6 +72,15 @@ export async function applyPendingUpgrade(): Promise<UpgradeApplied> {
|
|
|
52
72
|
: new ValidationError(notice)
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
if (pendingCore.length > 0) {
|
|
76
|
+
throw new ValidationError(
|
|
77
|
+
msg('error.app.core-migrations-pending', {
|
|
78
|
+
count: pendingCore.length,
|
|
79
|
+
migrations: pendingCore.join(', '),
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
const fresh = new Set<string>()
|
|
56
85
|
for (const plugin of definitions) {
|
|
57
86
|
if ((await readVersion(db, `plugin:${plugin.key}`)) === null) fresh.add(plugin.key)
|
|
@@ -101,7 +130,7 @@ export async function pendingUpgradeNotice(): Promise<string | null> {
|
|
|
101
130
|
const state: UpgradeState = {
|
|
102
131
|
recordedVersion: (await readVersion(db, 'core')) ?? CODE_VERSION,
|
|
103
132
|
codeVersion: CODE_VERSION,
|
|
104
|
-
pendingCoreMigrations:
|
|
133
|
+
pendingCoreMigrations: await missingCoreMigrations(db),
|
|
105
134
|
plugins: plugins.map((plugin) => ({
|
|
106
135
|
key: plugin.key,
|
|
107
136
|
version: plugin.version,
|
package/src/styles/globals.css
CHANGED
|
@@ -485,10 +485,12 @@
|
|
|
485
485
|
/*
|
|
486
486
|
* A permalink to `#post-12` otherwise lands the post flush against the top
|
|
487
487
|
* of the viewport, with its own header cropped and the previous post's footer
|
|
488
|
-
* nowhere — the reader cannot tell they arrived at the right one.
|
|
488
|
+
* nowhere — the reader cannot tell they arrived at the right one. The
|
|
489
|
+
* default theme's header is sticky — 3.5rem on a phone, 6.25rem with its
|
|
490
|
+
* navigation row on a desktop — so the margin clears it with room to spare.
|
|
489
491
|
*/
|
|
490
492
|
:target {
|
|
491
|
-
scroll-margin-block-start:
|
|
493
|
+
scroll-margin-block-start: 7.5rem;
|
|
492
494
|
}
|
|
493
495
|
|
|
494
496
|
::selection {
|
|
@@ -954,6 +956,34 @@ div.md-directive {
|
|
|
954
956
|
margin-block-end: 0;
|
|
955
957
|
}
|
|
956
958
|
|
|
959
|
+
/*
|
|
960
|
+
* A themed arrow on every single-choice <select>.
|
|
961
|
+
*
|
|
962
|
+
* The browser's own arrow ignores the palette — it stays the platform grey in
|
|
963
|
+
* both schemes and sits hard against the field's edge. `appearance: none`
|
|
964
|
+
* removes it, and two small gradient squares draw a chevron in
|
|
965
|
+
* `--muted-foreground` instead, so it follows a runtime token override like
|
|
966
|
+
* everything else. Gradients rather than an SVG data URI because a URI cannot
|
|
967
|
+
* read a custom property, and this file never hardcodes a colour twice.
|
|
968
|
+
* Multi-selects and list boxes keep their native rendering: they have no arrow.
|
|
969
|
+
*/
|
|
970
|
+
select:not([multiple], [size]) {
|
|
971
|
+
appearance: none;
|
|
972
|
+
padding-inline-end: 2.25rem;
|
|
973
|
+
background-image:
|
|
974
|
+
linear-gradient(45deg, transparent 50%, var(--muted-foreground) 50%),
|
|
975
|
+
linear-gradient(135deg, var(--muted-foreground) 50%, transparent 50%);
|
|
976
|
+
background-position:
|
|
977
|
+
calc(100% - 1.05rem) 50%,
|
|
978
|
+
calc(100% - 0.75rem) 50%;
|
|
979
|
+
background-size: 0.3rem 0.3rem;
|
|
980
|
+
background-repeat: no-repeat;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
select:not([multiple], [size]):disabled {
|
|
984
|
+
opacity: 0.6;
|
|
985
|
+
}
|
|
986
|
+
|
|
957
987
|
@media (pointer: coarse) {
|
|
958
988
|
input:not(
|
|
959
989
|
[type="checkbox"],
|