@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.
Files changed (38) hide show
  1. package/app/(board)/[slug]/new/page.tsx +7 -5
  2. package/app/(board)/thread/[slug]/edit/page.tsx +7 -5
  3. package/app/(board)/thread/[slug]/page.tsx +19 -1
  4. package/app/(board)/thread/[slug]/reply/page.tsx +7 -5
  5. package/app/(board)/usercp/security/verify/page.tsx +3 -0
  6. package/app/admin/layout.tsx +4 -4
  7. package/package.json +48 -48
  8. package/src/components/account/credential-proof-form.tsx +14 -4
  9. package/src/components/account/two-factor-forms.tsx +4 -4
  10. package/src/components/admin/admin-forms.tsx +8 -13
  11. package/src/components/admin/badge-forms.tsx +6 -7
  12. package/src/components/admin/branding-forms.tsx +5 -10
  13. package/src/components/admin/form-bits.tsx +6 -4
  14. package/src/components/admin/forum-forms.tsx +180 -78
  15. package/src/components/admin/mail-test-card.tsx +2 -5
  16. package/src/components/admin/marketplace-forms.tsx +4 -5
  17. package/src/components/admin/marketplace-listing.tsx +1 -1
  18. package/src/components/admin/plugin-forms.tsx +2 -6
  19. package/src/components/admin/settings-form.tsx +2 -6
  20. package/src/components/admin/system-forms.tsx +2 -5
  21. package/src/components/auth/otp-field.tsx +142 -0
  22. package/src/components/auth/second-factor-form.tsx +4 -3
  23. package/src/components/content/composer-recovery.tsx +19 -1
  24. package/src/components/content/edit-post-form.tsx +3 -2
  25. package/src/components/content/markdown-editor.tsx +123 -104
  26. package/src/components/content/new-thread-form.tsx +3 -2
  27. package/src/components/content/reply-form.tsx +3 -2
  28. package/src/components/moderation/thread-surgery-form.tsx +51 -47
  29. package/src/components/moderation/thread-tools-form.tsx +67 -87
  30. package/src/server/marketplace-admin.ts +2 -2
  31. package/src/server/upgrade-notice.ts +1 -1
  32. package/src/theme/contract.fixture.ts +2 -2
  33. package/src/view/account-copy.ts +2 -0
  34. package/src/view/admin-copy.ts +9 -0
  35. package/src/view/admin-panel-copy.ts +2 -0
  36. package/src/view/auth-copy.ts +17 -10
  37. package/src/view/content-copy.ts +11 -6
  38. 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="external"
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="external"
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={canAttach(actor, replyTarget) ? attachmentLimits(replyTarget) : null}
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="external"
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}
@@ -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.1",
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.1",
47
- "@meith/admin": "0.23.1",
48
- "@meith/antispam": "0.23.1",
49
- "@meith/api": "0.23.1",
50
- "@meith/attachments": "0.23.1",
51
- "@meith/authorization": "0.23.1",
52
- "@meith/avatars": "0.23.1",
53
- "@meith/core": "0.23.1",
54
- "@meith/db": "0.23.1",
55
- "@meith/demo": "0.23.1",
56
- "@meith/drafts": "0.23.1",
57
- "@meith/drivers": "0.23.1",
58
- "@meith/events": "0.23.1",
59
- "@meith/forums": "0.23.1",
60
- "@meith/groups": "0.23.1",
61
- "@meith/i18n": "0.23.1",
62
- "@meith/import": "0.23.1",
63
- "@meith/install": "0.23.1",
64
- "@meith/mail": "0.23.1",
65
- "@meith/markdown": "0.23.1",
66
- "@meith/marketplace": "0.23.1",
67
- "@meith/messages": "0.23.1",
68
- "@meith/moderation": "0.23.1",
69
- "@meith/notifications": "0.23.1",
70
- "@meith/plugin-calendar": "0.23.1",
71
- "@meith/plugin-dues": "0.23.1",
72
- "@meith/plugin-kit": "0.23.1",
73
- "@meith/polls": "0.23.1",
74
- "@meith/posts": "0.23.1",
75
- "@meith/profile-fields": "0.23.1",
76
- "@meith/relations": "0.23.1",
77
- "@meith/reputation": "0.23.1",
78
- "@meith/runtime": "0.23.1",
79
- "@meith/search": "0.23.1",
80
- "@meith/settings": "0.23.1",
81
- "@meith/signatures": "0.23.1",
82
- "@meith/subscriptions": "0.23.1",
83
- "@meith/tasks": "0.23.1",
84
- "@meith/theme-clubhouse": "0.23.1",
85
- "@meith/theme-default": "0.23.1",
86
- "@meith/theme-kit": "0.23.1",
87
- "@meith/theme-midnight": "0.23.1",
88
- "@meith/theme-phasebook": "0.23.1",
89
- "@meith/theme-raidframe": "0.23.1",
90
- "@meith/threads": "0.23.1",
91
- "@meith/ui": "0.23.1",
92
- "@meith/upgrade": "0.23.1"
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
- <label className="flex flex-col gap-1 text-sm">
63
- <span className="font-medium">{copy.codeLabel}</span>
64
- <Input name="code" inputMode="numeric" autoComplete="one-time-code" required />
65
- </label>
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
- <Field
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
- <Field
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
- <label className="flex flex-col gap-1 text-sm">
54
- <span className="font-medium">{fromCopy(copy, 'adminPanel.twoFactorCode')}</span>
55
- <input
56
- type="text"
57
- name="code"
58
- inputMode="numeric"
59
- autoComplete="one-time-code"
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
- {(saved.notice === 'saved' || removed.notice === 'removed') && (
43
- <p role="status" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">
44
- {saved.notice === 'saved'
45
- ? fromCopy(copy, 'admin.saved')
46
- : fromCopy(copy, 'adminPanel.badge.removed')}
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
- <p role="status" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">
46
- {fromCopy(copy, 'adminPanel.branding.saved')}
47
- </p>
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: boolean; children: React.ReactNode }) {
6
+ export function Saved({ when = true, children }: { when?: boolean; children: React.ReactNode }) {
5
7
  if (!when) return null
6
8
  return (
7
- <p role="status" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">
8
- {children}
9
- </p>
9
+ <Alert tone="success">
10
+ <AlertDescription>{children}</AlertDescription>
11
+ </Alert>
10
12
  )
11
13
  }