@nocobase/portal-template-default 1.0.16 → 1.0.17

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 (150) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-vPRTWiQ5.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DOl_Tss3.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-j08W3KBm.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-Bukw7zEe.js → auto-login-provider-DO5Il6Dp.js} +2 -2
  8. package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
  9. package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
  10. package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
  11. package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
  12. package/dist/assets/calendar-C7xnRgOT.js +1 -0
  13. package/dist/assets/{cas-sign-in-button-BQn3Mttx.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-BT5mL9gW.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-BJoadoA6.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-m8IbnJ1z.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-CYLPurwK.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
  19. package/dist/assets/demo-BPlae2bp.js +1 -0
  20. package/dist/assets/demo-Bj_Y0RrW.js +1 -0
  21. package/dist/assets/demo-CKPAQOib.js +1 -0
  22. package/dist/assets/demo-CXRTPvIx.js +1 -0
  23. package/dist/assets/demo-DJuVDu1n.js +1 -0
  24. package/dist/assets/demo-DUmWU7ik.js +1 -0
  25. package/dist/assets/demo-NhIzFRE1.js +1 -0
  26. package/dist/assets/{dingtalk-sign-in-button-DSluFaP9.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-mRXoEciY.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-CvLr4Slh.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-BC1tvfQ0.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-DhNaV5B8.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-BiioS8t1.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-CAWqYvFh.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-RVbHvIiD.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Dw20Lzpp.js → index-CPzehPGe.js} +3 -3
  36. package/dist/assets/index-Cc6awvdg.js +1064 -0
  37. package/dist/assets/index-ChA0z9ke.js +38 -0
  38. package/dist/assets/{index-BysibBZu.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-C-tdHirk.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DbITmOUW.js → labelHelper-Dk5jkiD-.js} +1 -1
  42. package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
  43. package/dist/assets/{oidc-sign-in-button-Cgpa8Qzj.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-AU6RU0Qi.js → page-context-DAiW_dId.js} +4 -4
  45. package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
  46. package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
  47. package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
  48. package/dist/assets/{sectorHelper-C6L782Ue.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-Bo2WLZuP.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-CoPxsICt.js → static.browser-kzzpH0Xt.js} +1 -1
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-ipgJ_3Ec.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-BzFeh7qe.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
  55. package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
  56. package/dist/assets/{use-wecom-sign-in-PPIMLvGa.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/{wecom-sign-in-button-rUq9RW8R.js → wecom-sign-in-button-Dn_2zDab.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/index.raw.html +2 -2
  60. package/package.json +9 -1
  61. package/src/App.tsx +5 -2
  62. package/src/app/extension.ts +2 -0
  63. package/src/app/extensions.tsx +18 -6
  64. package/src/app/route-access-guard.tsx +22 -0
  65. package/src/app/route-runtime.ts +173 -0
  66. package/src/app/routes.tsx +2 -2
  67. package/src/components/app-shell/header.tsx +56 -1
  68. package/src/components/app-shell/sidebar.tsx +14 -2
  69. package/src/extensions/nocobase-acl/README.md +28 -14
  70. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  71. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  72. package/src/extensions/nocobase-mail/README.md +74 -0
  73. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  74. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  75. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  76. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  77. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  78. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  79. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  80. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  81. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  82. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  83. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  84. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  85. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  86. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  87. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  89. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  90. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  91. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  92. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  93. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  94. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  95. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  96. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  97. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  98. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  99. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  100. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  101. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  102. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  103. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  104. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  108. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  109. package/src/extensions/nocobase-mail/index.ts +1 -0
  110. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  111. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  112. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  113. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  114. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  115. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  116. package/src/extensions/nocobase-users-example/index.ts +1 -0
  117. package/src/lib/nocobase/acl/menu.ts +17 -3
  118. package/src/lib/nocobase/acl/types.ts +4 -0
  119. package/src/locales/en-US.ts +2 -1
  120. package/src/locales/zh-CN.ts +2 -1
  121. package/src/providers/runtime-config.ts +18 -0
  122. package/src/routes.tsx +10 -0
  123. package/src/vite-env.d.ts +3 -0
  124. package/vite.config.ts +8 -0
  125. package/dist/assets/auth-method-demo-B2qcpY8z.js +0 -1
  126. package/dist/assets/auto-login-provider-BMtzvdxG.js +0 -1
  127. package/dist/assets/auto-redirect-provider-C6GNPg5r.js +0 -1
  128. package/dist/assets/auto-redirect-provider-D5c6kN2R.js +0 -1
  129. package/dist/assets/auto-redirect-provider-iTzGEDpU.js +0 -1
  130. package/dist/assets/calendar-Dv2RNnsr.js +0 -1
  131. package/dist/assets/card-C9Hpeq1c.js +0 -1
  132. package/dist/assets/createSeriesDataSimply-DKUV5b5e.js +0 -1
  133. package/dist/assets/demo-BWGOwqtJ.js +0 -1
  134. package/dist/assets/demo-BWbcvkya.js +0 -1
  135. package/dist/assets/demo-DIASSPgn.js +0 -1
  136. package/dist/assets/demo-DuZ6asUU.js +0 -1
  137. package/dist/assets/demo-Dxcy4bDE.js +0 -1
  138. package/dist/assets/demo-YpEC76uw.js +0 -1
  139. package/dist/assets/demo-wzkoR8T2.js +0 -1
  140. package/dist/assets/index-CBZbj45X.js +0 -58
  141. package/dist/assets/index-CgkdZ6Oc.js +0 -686
  142. package/dist/assets/index-NH3aqqQz.css +0 -1
  143. package/dist/assets/index-hSBzW-VA.js +0 -43
  144. package/dist/assets/ldap-sign-in-form-BOMNDTkK.js +0 -1
  145. package/dist/assets/prompt-card-FWugRgi-.js +0 -1
  146. package/dist/assets/prompt-output-DMTuTNab.js +0 -1
  147. package/dist/assets/saml-sign-in-button-qt8bMeoQ.js +0 -1
  148. package/dist/assets/shortcut-Bu7jT_-w.js +0 -47
  149. package/dist/assets/switch-DQkkIE7G.js +0 -1
  150. package/dist/assets/use-dingtalk-sign-in-mrq6K5Yx.js +0 -1
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from "react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export function MailShowcasePage({
6
+ title,
7
+ description,
8
+ badge = "Interactive preview",
9
+ children,
10
+ className,
11
+ }: {
12
+ title: string;
13
+ description: string;
14
+ badge?: string;
15
+ children: ReactNode;
16
+ className?: string;
17
+ }) {
18
+ return (
19
+ <div className={cn("space-y-10 pb-12", className)}>
20
+ <section className="flex flex-wrap items-start justify-between gap-5 border-b pb-8">
21
+ <div>
22
+ <div className="flex items-center gap-2">
23
+ <Badge variant="secondary">Mail Components</Badge>
24
+ <Badge variant="outline">{badge}</Badge>
25
+ </div>
26
+ <h1 className="mt-4 text-3xl font-semibold tracking-[-0.035em]">
27
+ {title}
28
+ </h1>
29
+ <p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
30
+ {description}
31
+ </p>
32
+ </div>
33
+ </section>
34
+ {children}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function MailShowcaseSection({
40
+ eyebrow,
41
+ title,
42
+ description,
43
+ children,
44
+ }: {
45
+ eyebrow: string;
46
+ title: string;
47
+ description: string;
48
+ children: ReactNode;
49
+ }) {
50
+ return (
51
+ <section className="space-y-5">
52
+ <div>
53
+ <p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
54
+ {eyebrow}
55
+ </p>
56
+ <h2 className="mt-2 text-xl font-semibold tracking-tight">{title}</h2>
57
+ <p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
58
+ {description}
59
+ </p>
60
+ </div>
61
+ {children}
62
+ </section>
63
+ );
64
+ }
@@ -0,0 +1,258 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Plus, Star, Trash2 } from "lucide-react";
3
+ import { toast } from "sonner";
4
+ import type { MailSignature } from "./types";
5
+ import type { MailSignatureValues } from "./use-mail-signatures";
6
+ import { MailRichEditor } from "./mail-rich-editor";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Label } from "@/components/ui/label";
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from "@/components/ui/dialog";
16
+ import { cn } from "@/lib/utils";
17
+
18
+ export interface MailSignatureManagerProps {
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ signatures: MailSignature[];
22
+ onCreate: (values: MailSignatureValues) => Promise<MailSignature>;
23
+ onUpdate: (id: string, values: MailSignatureValues) => Promise<unknown>;
24
+ onRemove: (id: string) => Promise<unknown>;
25
+ onSetDefault: (id: string) => Promise<unknown>;
26
+ embedded?: boolean;
27
+ }
28
+
29
+ const EMPTY: MailSignatureValues = { name: "", content: "", isDefault: false };
30
+
31
+ export function MailSignatureManager({
32
+ open,
33
+ onOpenChange,
34
+ signatures,
35
+ onCreate,
36
+ onUpdate,
37
+ onRemove,
38
+ onSetDefault,
39
+ embedded = false,
40
+ }: MailSignatureManagerProps) {
41
+ const [selectedId, setSelectedId] = useState<string | null>(null);
42
+ const [form, setForm] = useState<MailSignatureValues>(EMPTY);
43
+ const [busy, setBusy] = useState(false);
44
+
45
+ useEffect(() => {
46
+ if (!open) return;
47
+ const first = signatures[0];
48
+ if (first) {
49
+ setSelectedId(first.id);
50
+ setForm({
51
+ name: first.name,
52
+ content: first.content,
53
+ isDefault: Boolean(first.isDefault),
54
+ });
55
+ } else {
56
+ setSelectedId(null);
57
+ setForm(EMPTY);
58
+ }
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [open]);
61
+
62
+ const selectSignature = (signature: MailSignature) => {
63
+ setSelectedId(signature.id);
64
+ setForm({
65
+ name: signature.name,
66
+ content: signature.content,
67
+ isDefault: Boolean(signature.isDefault),
68
+ });
69
+ };
70
+
71
+ const startNew = () => {
72
+ setSelectedId(null);
73
+ setForm(EMPTY);
74
+ };
75
+
76
+ const canSave = form.name.trim().length > 0;
77
+ const selectedIsDefault = signatures.find(
78
+ (signature) => signature.id === selectedId
79
+ )?.isDefault;
80
+
81
+ const handleSave = async () => {
82
+ if (!canSave) return;
83
+ setBusy(true);
84
+ try {
85
+ if (selectedId) await onUpdate(selectedId, form);
86
+ else setSelectedId((await onCreate(form)).id);
87
+ toast.success("Signature saved");
88
+ } catch (error) {
89
+ toast.error(
90
+ error instanceof Error ? error.message : "Failed to save signature"
91
+ );
92
+ } finally {
93
+ setBusy(false);
94
+ }
95
+ };
96
+
97
+ const handleDelete = async (id: string) => {
98
+ setBusy(true);
99
+ try {
100
+ await onRemove(id);
101
+ if (selectedId === id) {
102
+ setSelectedId(null);
103
+ setForm(EMPTY);
104
+ }
105
+ toast.success("Signature deleted");
106
+ } catch (error) {
107
+ toast.error(
108
+ error instanceof Error ? error.message : "Failed to delete signature"
109
+ );
110
+ } finally {
111
+ setBusy(false);
112
+ }
113
+ };
114
+
115
+ const handleSetDefault = async () => {
116
+ if (!selectedId) return;
117
+ setBusy(true);
118
+ try {
119
+ await onSetDefault(selectedId);
120
+ setForm((prev) => ({ ...prev, isDefault: !selectedIsDefault }));
121
+ toast.success(
122
+ selectedIsDefault
123
+ ? "Default signature cleared"
124
+ : "Default signature set"
125
+ );
126
+ } catch (error) {
127
+ toast.error(
128
+ error instanceof Error
129
+ ? error.message
130
+ : "Failed to update default signature"
131
+ );
132
+ } finally {
133
+ setBusy(false);
134
+ }
135
+ };
136
+
137
+ const editor = (
138
+ <div className="grid gap-4 sm:grid-cols-[180px_1fr]">
139
+ <div className="flex flex-col gap-2">
140
+ <div className="flex max-h-80 flex-col gap-0.5 overflow-y-auto">
141
+ {signatures.map((signature) => (
142
+ <div
143
+ key={signature.id}
144
+ className={cn(
145
+ "group flex items-center rounded-md text-sm transition-colors",
146
+ selectedId === signature.id
147
+ ? "bg-muted font-medium"
148
+ : "hover:bg-muted/50"
149
+ )}
150
+ >
151
+ <button
152
+ type="button"
153
+ onClick={() => selectSignature(signature)}
154
+ className="flex min-w-0 flex-1 items-center gap-2 px-2.5 py-2 text-left"
155
+ >
156
+ <span className="min-w-0 flex-1 truncate">{signature.name}</span>
157
+ {signature.isDefault && (
158
+ <Star className="size-3.5 shrink-0 fill-amber-400 text-amber-400" />
159
+ )}
160
+ </button>
161
+ <Button
162
+ variant="ghost"
163
+ size="icon-sm"
164
+ title={`Delete ${signature.name}`}
165
+ aria-label={`Delete ${signature.name}`}
166
+ disabled={busy}
167
+ onClick={() => void handleDelete(signature.id)}
168
+ className="mr-1 shrink-0 text-muted-foreground opacity-60 hover:text-destructive sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100"
169
+ >
170
+ <Trash2 />
171
+ </Button>
172
+ </div>
173
+ ))}
174
+ {!signatures.length && (
175
+ <p className="px-2.5 py-2 text-xs text-muted-foreground">
176
+ No signatures yet
177
+ </p>
178
+ )}
179
+ </div>
180
+ <Button variant="outline" size="sm" onClick={startNew}>
181
+ <Plus />
182
+ New
183
+ </Button>
184
+ </div>
185
+
186
+ <div className="flex flex-col gap-3">
187
+ <div className="space-y-1.5">
188
+ <Label className="text-xs text-muted-foreground">Name</Label>
189
+ <Input
190
+ value={form.name}
191
+ onChange={(e) =>
192
+ setForm((prev) => ({ ...prev, name: e.target.value }))
193
+ }
194
+ placeholder="e.g. Work"
195
+ className="h-9"
196
+ />
197
+ </div>
198
+
199
+ <div className="space-y-1.5">
200
+ <Label className="text-xs text-muted-foreground">Content</Label>
201
+ <MailRichEditor
202
+ value={form.content}
203
+ onChange={(content) => setForm((prev) => ({ ...prev, content }))}
204
+ placeholder="Signature content…"
205
+ />
206
+ </div>
207
+
208
+ <label className="flex items-center gap-2 text-sm">
209
+ <input
210
+ type="checkbox"
211
+ checked={Boolean(form.isDefault)}
212
+ onChange={(e) =>
213
+ setForm((prev) => ({ ...prev, isDefault: e.target.checked }))
214
+ }
215
+ className="size-4 accent-primary"
216
+ />
217
+ Default signature (auto-insert on new messages)
218
+ </label>
219
+
220
+ <div className="flex items-center justify-end">
221
+ <div className="flex items-center gap-2">
222
+ {selectedId && (
223
+ <Button
224
+ variant="outline"
225
+ size="sm"
226
+ onClick={() => void handleSetDefault()}
227
+ disabled={busy}
228
+ >
229
+ <Star />
230
+ {selectedIsDefault ? "Unset default" : "Set default"}
231
+ </Button>
232
+ )}
233
+ <Button
234
+ size="sm"
235
+ onClick={() => void handleSave()}
236
+ disabled={!canSave || busy}
237
+ >
238
+ Save
239
+ </Button>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ );
245
+
246
+ if (embedded) return editor;
247
+
248
+ return (
249
+ <Dialog open={open} onOpenChange={onOpenChange}>
250
+ <DialogContent className="sm:max-w-3xl">
251
+ <DialogHeader>
252
+ <DialogTitle>Signatures</DialogTitle>
253
+ </DialogHeader>
254
+ {editor}
255
+ </DialogContent>
256
+ </Dialog>
257
+ );
258
+ }
@@ -0,0 +1,25 @@
1
+ export const SIGNATURE_ATTR = "data-mail-signature";
2
+
3
+ export function createSignatureId() {
4
+ return `sig-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
5
+ }
6
+
7
+ function stripSignature(html: string): string {
8
+ if (!html) return "";
9
+ const doc = new DOMParser().parseFromString(html, "text/html");
10
+ doc.body.querySelectorAll(`[${SIGNATURE_ATTR}]`).forEach((el) => el.remove());
11
+ return doc.body.innerHTML;
12
+ }
13
+
14
+ function wrapSignature(content: string): string {
15
+ return `<div ${SIGNATURE_ATTR}="true" style="margin-top:16px;padding-top:12px;border-top:1px solid #e4e4e7">${content}</div>`;
16
+ }
17
+
18
+ export function applySignature(
19
+ html: string,
20
+ signatureContent: string | null | undefined
21
+ ): string {
22
+ const base = stripSignature(html);
23
+ if (!signatureContent) return base;
24
+ return `${base}${wrapSignature(signatureContent)}`;
25
+ }