@meith/web 0.23.1 → 0.23.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)/[slug]/new/page.tsx +7 -5
- package/app/(board)/thread/[slug]/edit/page.tsx +7 -5
- package/app/(board)/thread/[slug]/page.tsx +19 -1
- package/app/(board)/thread/[slug]/reply/page.tsx +7 -5
- package/app/(board)/usercp/security/verify/page.tsx +3 -0
- package/app/admin/layout.tsx +4 -4
- package/package.json +48 -48
- package/src/components/account/credential-proof-form.tsx +14 -4
- package/src/components/account/two-factor-forms.tsx +4 -4
- package/src/components/admin/admin-forms.tsx +8 -13
- package/src/components/admin/badge-forms.tsx +6 -7
- package/src/components/admin/branding-forms.tsx +5 -10
- package/src/components/admin/form-bits.tsx +6 -4
- package/src/components/admin/forum-forms.tsx +180 -78
- package/src/components/admin/mail-test-card.tsx +2 -5
- package/src/components/admin/marketplace-forms.tsx +4 -5
- package/src/components/admin/marketplace-listing.tsx +1 -1
- package/src/components/admin/plugin-forms.tsx +2 -6
- package/src/components/admin/settings-form.tsx +2 -6
- package/src/components/admin/system-forms.tsx +2 -5
- package/src/components/auth/otp-field.tsx +142 -0
- package/src/components/auth/second-factor-form.tsx +4 -3
- package/src/components/content/composer-recovery.tsx +19 -1
- package/src/components/content/edit-post-form.tsx +3 -2
- package/src/components/content/markdown-editor.tsx +123 -104
- package/src/components/content/new-thread-form.tsx +3 -2
- package/src/components/content/reply-form.tsx +3 -2
- package/src/components/moderation/thread-surgery-form.tsx +51 -47
- package/src/components/moderation/thread-tools-form.tsx +67 -87
- package/src/server/marketplace-admin.ts +2 -2
- package/src/server/upgrade-notice.ts +1 -1
- package/src/theme/contract.fixture.ts +2 -2
- package/src/view/account-copy.ts +2 -0
- package/src/view/admin-copy.ts +9 -0
- package/src/view/admin-panel-copy.ts +2 -0
- package/src/view/auth-copy.ts +17 -10
- package/src/view/content-copy.ts +11 -6
- package/src/view/forum-display.ts +1 -0
|
@@ -70,6 +70,11 @@ export default async function NewThreadPage({ params }: { params: Promise<{ slug
|
|
|
70
70
|
viewerRef(actor),
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
+
const editorToolbar =
|
|
74
|
+
toolbarModel === null ? undefined : (
|
|
75
|
+
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
76
|
+
)
|
|
77
|
+
|
|
73
78
|
const formModel = await filterView(
|
|
74
79
|
'view.post-form',
|
|
75
80
|
{
|
|
@@ -89,13 +94,10 @@ export default async function NewThreadPage({ params }: { params: Promise<{ slug
|
|
|
89
94
|
? null
|
|
90
95
|
: await drafts.find(actor.userId, id, null)
|
|
91
96
|
}
|
|
92
|
-
toolbar=
|
|
97
|
+
toolbar={editorToolbar}
|
|
93
98
|
/>
|
|
94
99
|
) : null,
|
|
95
|
-
toolbar:
|
|
96
|
-
toolbarModel === null ? null : (
|
|
97
|
-
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
98
|
-
),
|
|
100
|
+
toolbar: null,
|
|
99
101
|
},
|
|
100
102
|
},
|
|
101
103
|
viewerRef(actor),
|
|
@@ -68,6 +68,11 @@ export default async function EditPostPage({
|
|
|
68
68
|
viewerRef(await getActor()),
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
+
const editorToolbar =
|
|
72
|
+
toolbarModel === null ? undefined : (
|
|
73
|
+
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
74
|
+
)
|
|
75
|
+
|
|
71
76
|
const formModel = await filterView(
|
|
72
77
|
'view.post-form',
|
|
73
78
|
{
|
|
@@ -84,16 +89,13 @@ export default async function EditPostPage({
|
|
|
84
89
|
postId={post}
|
|
85
90
|
message={scope.target.post.message}
|
|
86
91
|
reason={null}
|
|
87
|
-
toolbar=
|
|
92
|
+
toolbar={editorToolbar}
|
|
88
93
|
/>
|
|
89
94
|
)}
|
|
90
95
|
{scope.mayDelete && <DeletePostForm copy={formCopy} threadId={thread} postId={post} />}
|
|
91
96
|
</>
|
|
92
97
|
),
|
|
93
|
-
toolbar:
|
|
94
|
-
toolbarModel === null ? null : (
|
|
95
|
-
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
96
|
-
),
|
|
98
|
+
toolbar: null,
|
|
97
99
|
},
|
|
98
100
|
},
|
|
99
101
|
viewerRef(await getActor()),
|
|
@@ -41,6 +41,7 @@ import { followFormCopy } from '@/view/account-copy'
|
|
|
41
41
|
import { attachmentsByPost } from '@/view/attachments'
|
|
42
42
|
import { buildBreadcrumb } from '@/view/breadcrumb'
|
|
43
43
|
import { replyFormCopy, threadRatingCopy } from '@/view/content-copy'
|
|
44
|
+
import { buildEditorToolbarModel } from '@/view/editor-toolbar'
|
|
44
45
|
import {
|
|
45
46
|
anyInlineTool,
|
|
46
47
|
INLINE_FORM_ID,
|
|
@@ -402,6 +403,7 @@ export default async function ThreadPage({
|
|
|
402
403
|
const PostActions = requireSlot(theme, 'PostActions')
|
|
403
404
|
const Pagination = requireSlot(theme, 'Pagination')
|
|
404
405
|
const QuickReply = requireSlot(theme, 'QuickReply')
|
|
406
|
+
const EditorToolbar = requireSlot(theme, 'EditorToolbar')
|
|
405
407
|
const translator = await getTranslator()
|
|
406
408
|
const toolNotice = query.tool === undefined ? undefined : TOOL_NOTICE_KEYS[query.tool]
|
|
407
409
|
|
|
@@ -528,6 +530,14 @@ export default async function ThreadPage({
|
|
|
528
530
|
forum: matrix,
|
|
529
531
|
allowsAttachments: replyRules?.allowAttachments === true,
|
|
530
532
|
}
|
|
533
|
+
const quickReplyAttachable = canAttach(actor, replyTarget)
|
|
534
|
+
const quickReplyToolbar = !canReply
|
|
535
|
+
? null
|
|
536
|
+
: await filterView(
|
|
537
|
+
'view.editor-toolbar',
|
|
538
|
+
buildEditorToolbarModel({ attachments: quickReplyAttachable, t: translator }),
|
|
539
|
+
viewerRef(actor),
|
|
540
|
+
)
|
|
531
541
|
const quickReplyForm = !canReply ? null : (
|
|
532
542
|
<>
|
|
533
543
|
<QuoteInPlace threadId={thread.id} />
|
|
@@ -538,12 +548,20 @@ export default async function ThreadPage({
|
|
|
538
548
|
seenLastPostId={thread.lastPost?.postId ?? null}
|
|
539
549
|
prefill=""
|
|
540
550
|
canSubscribe={authorizer.can(actor, 'forum.subscribe', replyTarget)}
|
|
541
|
-
attachmentLimits={
|
|
551
|
+
attachmentLimits={quickReplyAttachable ? attachmentLimits(replyTarget) : null}
|
|
542
552
|
draft={
|
|
543
553
|
actor.userId === null || drafts === null
|
|
544
554
|
? null
|
|
545
555
|
: await drafts.find(actor.userId, forum.id, thread.id)
|
|
546
556
|
}
|
|
557
|
+
toolbar={
|
|
558
|
+
quickReplyToolbar === null ? undefined : (
|
|
559
|
+
<EditorToolbar
|
|
560
|
+
{...quickReplyToolbar}
|
|
561
|
+
copy={slotCopy(theme, 'EditorToolbar', translator)}
|
|
562
|
+
/>
|
|
563
|
+
)
|
|
564
|
+
}
|
|
547
565
|
collapsible
|
|
548
566
|
/>
|
|
549
567
|
</>
|
|
@@ -94,6 +94,11 @@ export default async function ReplyPage({
|
|
|
94
94
|
viewerRef(actor),
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
+
const editorToolbar =
|
|
98
|
+
toolbarModel === null ? undefined : (
|
|
99
|
+
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
100
|
+
)
|
|
101
|
+
|
|
97
102
|
const formModel = await filterView(
|
|
98
103
|
'view.post-form',
|
|
99
104
|
{
|
|
@@ -114,14 +119,11 @@ export default async function ReplyPage({
|
|
|
114
119
|
? null
|
|
115
120
|
: await drafts.find(actor.userId, target.forum.id, target.threadId)
|
|
116
121
|
}
|
|
117
|
-
toolbar=
|
|
122
|
+
toolbar={editorToolbar}
|
|
118
123
|
/>
|
|
119
124
|
</>
|
|
120
125
|
),
|
|
121
|
-
toolbar:
|
|
122
|
-
toolbarModel === null ? null : (
|
|
123
|
-
<EditorToolbar {...toolbarModel} copy={slotCopy(theme, 'EditorToolbar', translator)} />
|
|
124
|
-
),
|
|
126
|
+
toolbar: null,
|
|
125
127
|
},
|
|
126
128
|
},
|
|
127
129
|
viewerRef(actor),
|
|
@@ -47,6 +47,9 @@ export default async function VerifySecurityPage({
|
|
|
47
47
|
codeSubmit: tr.t('credentialProof.codeSubmit'),
|
|
48
48
|
passwordLabel: tr.t('credentialProof.passwordLabel'),
|
|
49
49
|
passwordSubmit: tr.t('credentialProof.passwordSubmit'),
|
|
50
|
+
recoveryLabel: tr.t('otp.recoveryLabel'),
|
|
51
|
+
useRecovery: tr.t('otp.useRecovery'),
|
|
52
|
+
useApp: tr.t('otp.useApp'),
|
|
50
53
|
}}
|
|
51
54
|
hasPassword={account?.passwordHash !== null && account !== null}
|
|
52
55
|
hasSecondFactor={factor.enrolled}
|
package/app/admin/layout.tsx
CHANGED
|
@@ -26,7 +26,7 @@ export default async function AdminLayout({ children }: { children: React.ReactN
|
|
|
26
26
|
<main
|
|
27
27
|
id="board-content"
|
|
28
28
|
tabIndex={-1}
|
|
29
|
-
className="flex min-h-screen items-center justify-center px-6 py-12"
|
|
29
|
+
className="flex min-h-screen flex-col items-center justify-center gap-4 px-6 py-12"
|
|
30
30
|
>
|
|
31
31
|
<AdminSignInForm
|
|
32
32
|
next="/admin"
|
|
@@ -34,6 +34,9 @@ export default async function AdminLayout({ children }: { children: React.ReactN
|
|
|
34
34
|
idleMinutes={ADMIN_IDLE_MINUTES}
|
|
35
35
|
copy={adminFormsCopy(await getTranslator())}
|
|
36
36
|
/>
|
|
37
|
+
<a href="/" className="text-sm text-muted-foreground hover:text-foreground">
|
|
38
|
+
{await tr('adminPanel.cancel')}
|
|
39
|
+
</a>
|
|
37
40
|
</main>
|
|
38
41
|
)
|
|
39
42
|
}
|
|
@@ -64,9 +67,6 @@ export default async function AdminLayout({ children }: { children: React.ReactN
|
|
|
64
67
|
{await tr('page.control-panel')}
|
|
65
68
|
</a>
|
|
66
69
|
<div className="ml-auto flex shrink-0 items-center gap-3 text-sm whitespace-nowrap sm:gap-4">
|
|
67
|
-
<a href="/" className="text-muted-foreground hover:text-foreground">
|
|
68
|
-
{await tr('page.board')}
|
|
69
|
-
</a>
|
|
70
70
|
<AdminSignOutForm copy={adminFormsCopy(t)} />
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/web",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.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,53 +43,53 @@
|
|
|
43
43
|
"react-dom": "19.2.8",
|
|
44
44
|
"tailwindcss": "^4.3.3",
|
|
45
45
|
"typescript": "7.0.2",
|
|
46
|
-
"@meith/accounts": "0.23.
|
|
47
|
-
"@meith/admin": "0.23.
|
|
48
|
-
"@meith/antispam": "0.23.
|
|
49
|
-
"@meith/api": "0.23.
|
|
50
|
-
"@meith/attachments": "0.23.
|
|
51
|
-
"@meith/authorization": "0.23.
|
|
52
|
-
"@meith/avatars": "0.23.
|
|
53
|
-
"@meith/core": "0.23.
|
|
54
|
-
"@meith/db": "0.23.
|
|
55
|
-
"@meith/demo": "0.23.
|
|
56
|
-
"@meith/drafts": "0.23.
|
|
57
|
-
"@meith/drivers": "0.23.
|
|
58
|
-
"@meith/events": "0.23.
|
|
59
|
-
"@meith/forums": "0.23.
|
|
60
|
-
"@meith/groups": "0.23.
|
|
61
|
-
"@meith/i18n": "0.23.
|
|
62
|
-
"@meith/import": "0.23.
|
|
63
|
-
"@meith/install": "0.23.
|
|
64
|
-
"@meith/mail": "0.23.
|
|
65
|
-
"@meith/markdown": "0.23.
|
|
66
|
-
"@meith/marketplace": "0.23.
|
|
67
|
-
"@meith/messages": "0.23.
|
|
68
|
-
"@meith/moderation": "0.23.
|
|
69
|
-
"@meith/notifications": "0.23.
|
|
70
|
-
"@meith/plugin-calendar": "0.23.
|
|
71
|
-
"@meith/plugin-dues": "0.23.
|
|
72
|
-
"@meith/plugin-kit": "0.23.
|
|
73
|
-
"@meith/polls": "0.23.
|
|
74
|
-
"@meith/posts": "0.23.
|
|
75
|
-
"@meith/profile-fields": "0.23.
|
|
76
|
-
"@meith/relations": "0.23.
|
|
77
|
-
"@meith/reputation": "0.23.
|
|
78
|
-
"@meith/runtime": "0.23.
|
|
79
|
-
"@meith/search": "0.23.
|
|
80
|
-
"@meith/settings": "0.23.
|
|
81
|
-
"@meith/signatures": "0.23.
|
|
82
|
-
"@meith/subscriptions": "0.23.
|
|
83
|
-
"@meith/tasks": "0.23.
|
|
84
|
-
"@meith/theme-clubhouse": "0.23.
|
|
85
|
-
"@meith/theme-default": "0.23.
|
|
86
|
-
"@meith/theme-kit": "0.23.
|
|
87
|
-
"@meith/theme-midnight": "0.23.
|
|
88
|
-
"@meith/theme-phasebook": "0.23.
|
|
89
|
-
"@meith/theme-raidframe": "0.23.
|
|
90
|
-
"@meith/threads": "0.23.
|
|
91
|
-
"@meith/ui": "0.23.
|
|
92
|
-
"@meith/upgrade": "0.23.
|
|
46
|
+
"@meith/accounts": "0.23.3",
|
|
47
|
+
"@meith/admin": "0.23.3",
|
|
48
|
+
"@meith/antispam": "0.23.3",
|
|
49
|
+
"@meith/api": "0.23.3",
|
|
50
|
+
"@meith/attachments": "0.23.3",
|
|
51
|
+
"@meith/authorization": "0.23.3",
|
|
52
|
+
"@meith/avatars": "0.23.3",
|
|
53
|
+
"@meith/core": "0.23.3",
|
|
54
|
+
"@meith/db": "0.23.3",
|
|
55
|
+
"@meith/demo": "0.23.3",
|
|
56
|
+
"@meith/drafts": "0.23.3",
|
|
57
|
+
"@meith/drivers": "0.23.3",
|
|
58
|
+
"@meith/events": "0.23.3",
|
|
59
|
+
"@meith/forums": "0.23.3",
|
|
60
|
+
"@meith/groups": "0.23.3",
|
|
61
|
+
"@meith/i18n": "0.23.3",
|
|
62
|
+
"@meith/import": "0.23.3",
|
|
63
|
+
"@meith/install": "0.23.3",
|
|
64
|
+
"@meith/mail": "0.23.3",
|
|
65
|
+
"@meith/markdown": "0.23.3",
|
|
66
|
+
"@meith/marketplace": "0.23.3",
|
|
67
|
+
"@meith/messages": "0.23.3",
|
|
68
|
+
"@meith/moderation": "0.23.3",
|
|
69
|
+
"@meith/notifications": "0.23.3",
|
|
70
|
+
"@meith/plugin-calendar": "0.23.3",
|
|
71
|
+
"@meith/plugin-dues": "0.23.3",
|
|
72
|
+
"@meith/plugin-kit": "0.23.3",
|
|
73
|
+
"@meith/polls": "0.23.3",
|
|
74
|
+
"@meith/posts": "0.23.3",
|
|
75
|
+
"@meith/profile-fields": "0.23.3",
|
|
76
|
+
"@meith/relations": "0.23.3",
|
|
77
|
+
"@meith/reputation": "0.23.3",
|
|
78
|
+
"@meith/runtime": "0.23.3",
|
|
79
|
+
"@meith/search": "0.23.3",
|
|
80
|
+
"@meith/settings": "0.23.3",
|
|
81
|
+
"@meith/signatures": "0.23.3",
|
|
82
|
+
"@meith/subscriptions": "0.23.3",
|
|
83
|
+
"@meith/tasks": "0.23.3",
|
|
84
|
+
"@meith/theme-clubhouse": "0.23.3",
|
|
85
|
+
"@meith/theme-default": "0.23.3",
|
|
86
|
+
"@meith/theme-kit": "0.23.3",
|
|
87
|
+
"@meith/theme-midnight": "0.23.3",
|
|
88
|
+
"@meith/theme-phasebook": "0.23.3",
|
|
89
|
+
"@meith/theme-raidframe": "0.23.3",
|
|
90
|
+
"@meith/threads": "0.23.3",
|
|
91
|
+
"@meith/ui": "0.23.3",
|
|
92
|
+
"@meith/upgrade": "0.23.3"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
95
|
"dev": "next dev",
|
|
@@ -9,6 +9,7 @@ import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
|
9
9
|
import { proveCredentialAction } from '@/server/credential-proof-actions'
|
|
10
10
|
|
|
11
11
|
import { FormError } from '../auth/form-controls'
|
|
12
|
+
import { OtpField } from '../auth/otp-field'
|
|
12
13
|
|
|
13
14
|
export interface CredentialProofCopy {
|
|
14
15
|
readonly codeConsumed: string
|
|
@@ -16,6 +17,9 @@ export interface CredentialProofCopy {
|
|
|
16
17
|
readonly codeSubmit: string
|
|
17
18
|
readonly passwordLabel: string
|
|
18
19
|
readonly passwordSubmit: string
|
|
20
|
+
readonly recoveryLabel: string
|
|
21
|
+
readonly useRecovery: string
|
|
22
|
+
readonly useApp: string
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export function CredentialProofForm({
|
|
@@ -59,10 +63,16 @@ export function CredentialProofForm({
|
|
|
59
63
|
>
|
|
60
64
|
<input type="hidden" name="method" value="code" />
|
|
61
65
|
<input type="hidden" name="next" value={next} />
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
<OtpField
|
|
67
|
+
label={copy.codeLabel}
|
|
68
|
+
name="code"
|
|
69
|
+
recovery={{
|
|
70
|
+
label: copy.recoveryLabel,
|
|
71
|
+
toRecovery: copy.useRecovery,
|
|
72
|
+
toApp: copy.useApp,
|
|
73
|
+
hint: copy.codeConsumed,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
66
76
|
<p className="text-sm text-muted-foreground">{copy.codeConsumed}</p>
|
|
67
77
|
<div>
|
|
68
78
|
<Button type="submit" variant="primary">
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { RECOVERY_CODES_FIELD } from '@/view/two-factor'
|
|
16
16
|
|
|
17
17
|
import { Field, FormError } from '../auth/form-controls'
|
|
18
|
+
import { OtpField, otpRecoveryFromCopy } from '../auth/otp-field'
|
|
18
19
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
19
20
|
|
|
20
21
|
const CARD = 'flex flex-col gap-4 rounded-lg border border-border bg-card p-5'
|
|
@@ -96,10 +97,9 @@ export function TwoFactorSetup({
|
|
|
96
97
|
|
|
97
98
|
<form action={action} className="flex flex-col gap-4">
|
|
98
99
|
<FormError message={state.error} />
|
|
99
|
-
<
|
|
100
|
+
<OtpField
|
|
100
101
|
label={fromCopy(copy, 'accountForm.twoFactor.codeLabel')}
|
|
101
102
|
name="code"
|
|
102
|
-
autoComplete="one-time-code"
|
|
103
103
|
hint={fromCopy(copy, 'accountForm.twoFactor.codeHint')}
|
|
104
104
|
/>
|
|
105
105
|
<div className="flex flex-wrap gap-2">
|
|
@@ -166,12 +166,12 @@ export function TwoFactorPanel({
|
|
|
166
166
|
autoComplete="current-password"
|
|
167
167
|
/>
|
|
168
168
|
) : (
|
|
169
|
-
<
|
|
169
|
+
<OtpField
|
|
170
170
|
id={`${which}-code`}
|
|
171
171
|
label={fromCopy(copy, 'accountForm.twoFactor.codeProofLabel')}
|
|
172
172
|
name="code"
|
|
173
|
-
autoComplete="one-time-code"
|
|
174
173
|
hint={fromCopy(copy, 'accountForm.twoFactor.codeProofHint')}
|
|
174
|
+
recovery={otpRecoveryFromCopy(copy)}
|
|
175
175
|
/>
|
|
176
176
|
)
|
|
177
177
|
|
|
@@ -6,6 +6,7 @@ import { adminSignInAction, adminSignOutAction } from '@/server/admin-actions'
|
|
|
6
6
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
7
7
|
|
|
8
8
|
import { FormError } from '../auth/form-controls'
|
|
9
|
+
import { OtpField, otpRecoveryFromCopy } from '../auth/otp-field'
|
|
9
10
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
10
11
|
|
|
11
12
|
export function AdminSignInForm({
|
|
@@ -50,19 +51,13 @@ export function AdminSignInForm({
|
|
|
50
51
|
/>
|
|
51
52
|
</label>
|
|
52
53
|
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
61
|
-
/>
|
|
62
|
-
<span className="text-xs text-muted-foreground">
|
|
63
|
-
{fromCopy(copy, 'adminPanel.twoFactorCodeHint')}
|
|
64
|
-
</span>
|
|
65
|
-
</label>
|
|
54
|
+
<OtpField
|
|
55
|
+
label={fromCopy(copy, 'adminPanel.twoFactorCode')}
|
|
56
|
+
name="code"
|
|
57
|
+
required={false}
|
|
58
|
+
hint={fromCopy(copy, 'adminPanel.twoFactorCodeHint')}
|
|
59
|
+
recovery={otpRecoveryFromCopy(copy)}
|
|
60
|
+
/>
|
|
66
61
|
|
|
67
62
|
<button
|
|
68
63
|
type="submit"
|
|
@@ -10,6 +10,7 @@ import { removeBadgeAction, saveBadgeAction } from '@/server/group-badge-actions
|
|
|
10
10
|
|
|
11
11
|
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
12
12
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
13
|
+
import { Saved } from './form-bits'
|
|
13
14
|
|
|
14
15
|
const GHOST =
|
|
15
16
|
'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'
|
|
@@ -39,13 +40,11 @@ export function BadgeUploadForm({
|
|
|
39
40
|
<h3 className="text-sm font-medium">{label}</h3>
|
|
40
41
|
|
|
41
42
|
<FormError message={saved.error ?? removed.error} />
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</p>
|
|
48
|
-
)}
|
|
43
|
+
<Saved when={saved.notice === 'saved' || removed.notice === 'removed'}>
|
|
44
|
+
{saved.notice === 'saved'
|
|
45
|
+
? fromCopy(copy, 'admin.saved')
|
|
46
|
+
: fromCopy(copy, 'adminPanel.badge.removed')}
|
|
47
|
+
</Saved>
|
|
49
48
|
|
|
50
49
|
<div
|
|
51
50
|
className={`flex min-h-12 items-center justify-center rounded-md border border-border bg-card p-2 ${
|
|
@@ -8,6 +8,7 @@ import { removeLogoAction, saveLogoAction } from '@/server/branding-actions'
|
|
|
8
8
|
|
|
9
9
|
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
10
10
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
11
|
+
import { Saved } from './form-bits'
|
|
11
12
|
|
|
12
13
|
export interface LogoSlot {
|
|
13
14
|
readonly scheme: 'light' | 'dark'
|
|
@@ -41,16 +42,10 @@ export function LogoUploadForm({
|
|
|
41
42
|
</div>
|
|
42
43
|
|
|
43
44
|
<FormError message={saved.error ?? removed.error} />
|
|
44
|
-
{saved.notice === 'saved'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)}
|
|
49
|
-
{removed.notice === 'removed' && (
|
|
50
|
-
<p role="status" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">
|
|
51
|
-
{fromCopy(copy, 'adminPanel.branding.removed')}
|
|
52
|
-
</p>
|
|
53
|
-
)}
|
|
45
|
+
<Saved when={saved.notice === 'saved'}>{fromCopy(copy, 'adminPanel.branding.saved')}</Saved>
|
|
46
|
+
<Saved when={removed.notice === 'removed'}>
|
|
47
|
+
{fromCopy(copy, 'adminPanel.branding.removed')}
|
|
48
|
+
</Saved>
|
|
54
49
|
|
|
55
50
|
<div
|
|
56
51
|
className={`flex min-h-16 items-center justify-center rounded-md border border-border bg-card p-3 ${
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { Alert, AlertDescription } from '@meith/ui'
|
|
2
|
+
|
|
1
3
|
export const INPUT =
|
|
2
4
|
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
3
5
|
|
|
4
|
-
export function Saved({ when, children }: { when
|
|
6
|
+
export function Saved({ when = true, children }: { when?: boolean; children: React.ReactNode }) {
|
|
5
7
|
if (!when) return null
|
|
6
8
|
return (
|
|
7
|
-
<
|
|
8
|
-
{children}
|
|
9
|
-
</
|
|
9
|
+
<Alert tone="success">
|
|
10
|
+
<AlertDescription>{children}</AlertDescription>
|
|
11
|
+
</Alert>
|
|
10
12
|
)
|
|
11
13
|
}
|