@burojs/ui-shadcn 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +26 -7
  2. package/dist/bulk-summary-CwSd6Yjb.cjs +108 -0
  3. package/dist/bulk-summary-CwSd6Yjb.cjs.map +1 -0
  4. package/dist/bulk-summary-GxMDP4bw.js +108 -0
  5. package/dist/bulk-summary-GxMDP4bw.js.map +1 -0
  6. package/dist/button-BxnnE28H.cjs +55 -0
  7. package/dist/button-BxnnE28H.cjs.map +1 -0
  8. package/dist/button-MWVeZaqj.js +44 -0
  9. package/dist/button-MWVeZaqj.js.map +1 -0
  10. package/dist/candidates-gate-CX2PBu6l.js +63 -0
  11. package/dist/candidates-gate-CX2PBu6l.js.map +1 -0
  12. package/dist/candidates-gate-DiFI52r7.cjs +70 -0
  13. package/dist/candidates-gate-DiFI52r7.cjs.map +1 -0
  14. package/dist/candidates-relation-picker-CBsVcnPR.js +82 -0
  15. package/dist/candidates-relation-picker-CBsVcnPR.js.map +1 -0
  16. package/dist/candidates-relation-picker-Dd_GVLlu.cjs +84 -0
  17. package/dist/candidates-relation-picker-Dd_GVLlu.cjs.map +1 -0
  18. package/dist/check-confirm-dialog-BRByTFpg.cjs +1534 -0
  19. package/dist/check-confirm-dialog-BRByTFpg.cjs.map +1 -0
  20. package/dist/check-confirm-dialog-CkaVTPT9.js +1232 -0
  21. package/dist/check-confirm-dialog-CkaVTPT9.js.map +1 -0
  22. package/dist/dialog-B_rt9S1c.js +63 -0
  23. package/dist/dialog-B_rt9S1c.js.map +1 -0
  24. package/dist/dialog-DRNUq9EA.cjs +112 -0
  25. package/dist/dialog-DRNUq9EA.cjs.map +1 -0
  26. package/dist/filter-builder-B3mZFL-K.cjs +1761 -0
  27. package/dist/filter-builder-B3mZFL-K.cjs.map +1 -0
  28. package/dist/filter-builder-DOnaXNfP.js +1605 -0
  29. package/dist/filter-builder-DOnaXNfP.js.map +1 -0
  30. package/dist/flow-runtime-B6zOHYqI.cjs +255 -0
  31. package/dist/flow-runtime-B6zOHYqI.cjs.map +1 -0
  32. package/dist/flow-runtime-D8h99FSg.js +252 -0
  33. package/dist/flow-runtime-D8h99FSg.js.map +1 -0
  34. package/dist/flow-screen-CX05b3G9.cjs +705 -0
  35. package/dist/flow-screen-CX05b3G9.cjs.map +1 -0
  36. package/dist/flow-screen-CmbARSqg.js +701 -0
  37. package/dist/flow-screen-CmbARSqg.js.map +1 -0
  38. package/dist/index.cjs +6453 -17494
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +3754 -3621
  41. package/dist/index.d.ts +3754 -3621
  42. package/dist/index.js +6217 -17219
  43. package/dist/index.js.map +1 -1
  44. package/dist/inline-flow-host-B7U1OJUe.cjs +112 -0
  45. package/dist/inline-flow-host-B7U1OJUe.cjs.map +1 -0
  46. package/dist/inline-flow-host-C3H2UpSA.js +112 -0
  47. package/dist/inline-flow-host-C3H2UpSA.js.map +1 -0
  48. package/dist/input-BI0czGyI.cjs +23 -0
  49. package/dist/input-BI0czGyI.cjs.map +1 -0
  50. package/dist/input-CUPStzZO.js +17 -0
  51. package/dist/input-CUPStzZO.js.map +1 -0
  52. package/dist/label-66W1qbke.js +29 -0
  53. package/dist/label-66W1qbke.js.map +1 -0
  54. package/dist/label-w6JYf2cF.cjs +36 -0
  55. package/dist/label-w6JYf2cF.cjs.map +1 -0
  56. package/dist/layout/theme-storage-key.cjs +5 -10
  57. package/dist/layout/theme-storage-key.cjs.map +1 -1
  58. package/dist/layout/theme-storage-key.d.cts +2 -23
  59. package/dist/layout/theme-storage-key.d.ts +2 -23
  60. package/dist/layout/theme-storage-key.js +5 -2
  61. package/dist/layout/theme-storage-key.js.map +1 -1
  62. package/dist/lazy-relation-picker-dialog-2oxnA6IR.js +14 -0
  63. package/dist/lazy-relation-picker-dialog-2oxnA6IR.js.map +1 -0
  64. package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs +19 -0
  65. package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs.map +1 -0
  66. package/dist/markdown/index.cjs +424 -511
  67. package/dist/markdown/index.cjs.map +1 -1
  68. package/dist/markdown/index.d.cts +1072 -34
  69. package/dist/markdown/index.d.ts +1072 -34
  70. package/dist/markdown/index.js +411 -459
  71. package/dist/markdown/index.js.map +1 -1
  72. package/dist/page-flow-route-CQ7HsbBm.cjs +277 -0
  73. package/dist/page-flow-route-CQ7HsbBm.cjs.map +1 -0
  74. package/dist/page-flow-route-CyXSvXSE.js +277 -0
  75. package/dist/page-flow-route-CyXSvXSE.js.map +1 -0
  76. package/dist/peek-filter-CnqHwTMU.cjs +51 -0
  77. package/dist/peek-filter-CnqHwTMU.cjs.map +1 -0
  78. package/dist/peek-filter-ErLfO153.js +28 -0
  79. package/dist/peek-filter-ErLfO153.js.map +1 -0
  80. package/dist/place-node--GdsP688.js +7315 -0
  81. package/dist/place-node--GdsP688.js.map +1 -0
  82. package/dist/place-node-SIp4XYQH.cjs +7833 -0
  83. package/dist/place-node-SIp4XYQH.cjs.map +1 -0
  84. package/dist/related-list-cards-B6x9HQQp.js +42 -0
  85. package/dist/related-list-cards-B6x9HQQp.js.map +1 -0
  86. package/dist/related-list-cards-nxuODlIA.cjs +42 -0
  87. package/dist/related-list-cards-nxuODlIA.cjs.map +1 -0
  88. package/dist/related-list-timeline-BYJKEbmA.cjs +142 -0
  89. package/dist/related-list-timeline-BYJKEbmA.cjs.map +1 -0
  90. package/dist/related-list-timeline-DEdfd-rr.js +142 -0
  91. package/dist/related-list-timeline-DEdfd-rr.js.map +1 -0
  92. package/dist/related-rows-B4levHcR.js +186 -0
  93. package/dist/related-rows-B4levHcR.js.map +1 -0
  94. package/dist/related-rows-zh5f_4QR.cjs +209 -0
  95. package/dist/related-rows-zh5f_4QR.cjs.map +1 -0
  96. package/dist/relation-create-BOaUe12G.cjs +362 -0
  97. package/dist/relation-create-BOaUe12G.cjs.map +1 -0
  98. package/dist/relation-create-Bg-eEJLs.js +337 -0
  99. package/dist/relation-create-Bg-eEJLs.js.map +1 -0
  100. package/dist/relation-many-picker-B-NCEUgX.cjs +465 -0
  101. package/dist/relation-many-picker-B-NCEUgX.cjs.map +1 -0
  102. package/dist/relation-many-picker-gdAOD-qK.js +464 -0
  103. package/dist/relation-many-picker-gdAOD-qK.js.map +1 -0
  104. package/dist/relation-picker-BAY77ivO.js +462 -0
  105. package/dist/relation-picker-BAY77ivO.js.map +1 -0
  106. package/dist/relation-picker-Da_uhFvA.cjs +479 -0
  107. package/dist/relation-picker-Da_uhFvA.cjs.map +1 -0
  108. package/dist/relation-picker-dialog-M0pQaWlp.cjs +300 -0
  109. package/dist/relation-picker-dialog-M0pQaWlp.cjs.map +1 -0
  110. package/dist/relation-picker-dialog-hA9rYrZB.js +299 -0
  111. package/dist/relation-picker-dialog-hA9rYrZB.js.map +1 -0
  112. package/dist/resource-page-router-OSQyRA5b.cjs +1779 -0
  113. package/dist/resource-page-router-OSQyRA5b.cjs.map +1 -0
  114. package/dist/resource-page-router-T4vS5gbJ.js +1582 -0
  115. package/dist/resource-page-router-T4vS5gbJ.js.map +1 -0
  116. package/dist/rich-text-DFerywgz.js +457 -0
  117. package/dist/rich-text-DFerywgz.js.map +1 -0
  118. package/dist/rich-text-vTyVjDoa.cjs +460 -0
  119. package/dist/rich-text-vTyVjDoa.cjs.map +1 -0
  120. package/dist/search-access-B7G5oR8n.cjs +812 -0
  121. package/dist/search-access-B7G5oR8n.cjs.map +1 -0
  122. package/dist/search-access-CURsWn-U.js +669 -0
  123. package/dist/search-access-CURsWn-U.js.map +1 -0
  124. package/dist/shell-BPlk8hEa.js +205 -0
  125. package/dist/shell-BPlk8hEa.js.map +1 -0
  126. package/dist/shell-DxhlsPwm.cjs +271 -0
  127. package/dist/shell-DxhlsPwm.cjs.map +1 -0
  128. package/dist/skeleton-U2noefj5.js +13 -0
  129. package/dist/skeleton-U2noefj5.js.map +1 -0
  130. package/dist/skeleton-_1gP7wan.cjs +18 -0
  131. package/dist/skeleton-_1gP7wan.cjs.map +1 -0
  132. package/dist/styles.css +255 -17
  133. package/dist/theme/theme-css.cjs +95 -96
  134. package/dist/theme/theme-css.cjs.map +1 -1
  135. package/dist/theme/theme-css.d.cts +10 -8
  136. package/dist/theme/theme-css.d.ts +10 -8
  137. package/dist/theme/theme-css.js +94 -94
  138. package/dist/theme/theme-css.js.map +1 -1
  139. package/dist/theme/theme-storage.cjs +34 -39
  140. package/dist/theme/theme-storage.cjs.map +1 -1
  141. package/dist/theme/theme-storage.d.cts +3 -1
  142. package/dist/theme/theme-storage.d.ts +3 -1
  143. package/dist/theme/theme-storage.js +33 -37
  144. package/dist/theme/theme-storage.js.map +1 -1
  145. package/dist/theme-storage-key-pYlp4aDS.d.cts +31 -0
  146. package/dist/theme-storage-key-pYlp4aDS.d.ts +31 -0
  147. package/dist/toaster-C7CiYA4Z.cjs +248 -0
  148. package/dist/toaster-C7CiYA4Z.cjs.map +1 -0
  149. package/dist/toaster-CfCRLgR-.js +164 -0
  150. package/dist/toaster-CfCRLgR-.js.map +1 -0
  151. package/dist/use-dialog-filter-state-BBCC0OEK.cjs +92 -0
  152. package/dist/use-dialog-filter-state-BBCC0OEK.cjs.map +1 -0
  153. package/dist/use-dialog-filter-state-BDcgS5Oz.js +87 -0
  154. package/dist/use-dialog-filter-state-BDcgS5Oz.js.map +1 -0
  155. package/dist/use-theme-DFlpXils.cjs +257 -0
  156. package/dist/use-theme-DFlpXils.cjs.map +1 -0
  157. package/dist/use-theme-dnE3Od3I.js +175 -0
  158. package/dist/use-theme-dnE3Od3I.js.map +1 -0
  159. package/package.json +18 -17
  160. package/dist/chunk-ABUQPOOE.js +0 -11
  161. package/dist/chunk-ABUQPOOE.js.map +0 -1
  162. package/dist/chunk-DBGGWBFG.cjs +0 -199
  163. package/dist/chunk-DBGGWBFG.cjs.map +0 -1
  164. package/dist/chunk-FMX4GLCQ.js +0 -6
  165. package/dist/chunk-FMX4GLCQ.js.map +0 -1
  166. package/dist/chunk-UC57Z243.js +0 -143
  167. package/dist/chunk-UC57Z243.js.map +0 -1
  168. package/dist/chunk-UPGPO75B.js +0 -164
  169. package/dist/chunk-UPGPO75B.js.map +0 -1
  170. package/dist/chunk-VXJAXJ3U.cjs +0 -8
  171. package/dist/chunk-VXJAXJ3U.cjs.map +0 -1
  172. package/dist/chunk-W7AEYE7A.cjs +0 -13
  173. package/dist/chunk-W7AEYE7A.cjs.map +0 -1
  174. package/dist/chunk-YF5IW525.cjs +0 -155
  175. package/dist/chunk-YF5IW525.cjs.map +0 -1
  176. package/dist/rich-text-6V7WZ3WV.cjs +0 -518
  177. package/dist/rich-text-6V7WZ3WV.cjs.map +0 -1
  178. package/dist/rich-text-N2FZEJJV.js +0 -491
  179. package/dist/rich-text-N2FZEJJV.js.map +0 -1
@@ -1,491 +0,0 @@
1
- import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Label, Input, DialogFooter } from './chunk-UPGPO75B.js';
2
- import { cn } from './chunk-ABUQPOOE.js';
3
- import * as React from 'react';
4
- import { IconBold, IconItalic, IconPilcrow, IconH1, IconH2, IconH3, IconList, IconListNumbers, IconLink } from '@tabler/icons-react';
5
- import { useOptionalT } from '@burojs/react';
6
- import { useEditor, EditorContent } from '@tiptap/react';
7
- import { generateJSON, generateHTML } from '@tiptap/core';
8
- import StarterKit from '@tiptap/starter-kit';
9
- import { jsx, jsxs } from 'react/jsx-runtime';
10
-
11
- function isAllowedLinkScheme(url) {
12
- if (typeof url !== "string") return false;
13
- const trimmed = url.trim();
14
- if (trimmed.length === 0) return false;
15
- const lowered = trimmed.toLowerCase();
16
- if (lowered.startsWith("javascript:") || lowered.startsWith("data:") || lowered.startsWith("vbscript:")) {
17
- return false;
18
- }
19
- const allowed = ["http:", "https:", "mailto:", "tel:"];
20
- try {
21
- const base = typeof window !== "undefined" && window.location?.origin ? window.location.origin : "http://localhost";
22
- const parsed = new URL(trimmed, base);
23
- return allowed.includes(parsed.protocol);
24
- } catch {
25
- return false;
26
- }
27
- }
28
- function sanitizeRichTextHtml(value) {
29
- if (!value || typeof value !== "string") return "";
30
- const extensions = [
31
- StarterKit.configure({
32
- link: {
33
- openOnClick: false,
34
- HTMLAttributes: {
35
- rel: "noopener noreferrer",
36
- target: "_blank"
37
- }
38
- }
39
- })
40
- ];
41
- try {
42
- const json = generateJSON(value, extensions);
43
- let html = generateHTML(json, extensions);
44
- html = ensureSafeAnchorAttrs(html);
45
- return html;
46
- } catch {
47
- return "";
48
- }
49
- }
50
- function ensureSafeAnchorAttrs(html) {
51
- return html.replace(/<a\b([^>]*)>/gi, (match, attrs) => {
52
- const hasRel = /\brel\s*=/.test(attrs);
53
- const hasTarget = /\btarget\s*=/.test(attrs);
54
- let next = attrs;
55
- if (!hasRel) next += ' rel="noopener noreferrer"';
56
- if (!hasTarget) next += ' target="_blank"';
57
- return `<a${next}>`;
58
- });
59
- }
60
- function Toolbar({ editor, testid, onOpenLinkDialog }) {
61
- const t = useOptionalT();
62
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
63
- React.useEffect(() => {
64
- const handler = () => forceUpdate();
65
- editor.on("selectionUpdate", handler);
66
- editor.on("transaction", handler);
67
- return () => {
68
- editor.off("selectionUpdate", handler);
69
- editor.off("transaction", handler);
70
- };
71
- }, [editor]);
72
- const isBold = editor.isActive("bold");
73
- const isItalic = editor.isActive("italic");
74
- const isBulletList = editor.isActive("bulletList");
75
- const isOrderedList = editor.isActive("orderedList");
76
- const isH1 = editor.isActive("heading", { level: 1 });
77
- const isH2 = editor.isActive("heading", { level: 2 });
78
- const isH3 = editor.isActive("heading", { level: 3 });
79
- const isLink = editor.isActive("link");
80
- const isParagraph = editor.isActive("paragraph") && !isH1 && !isH2 && !isH3;
81
- return /* @__PURE__ */ jsxs(
82
- "div",
83
- {
84
- className: "flex flex-wrap items-center gap-1 border-b bg-muted/30 px-2 py-1.5",
85
- role: "toolbar",
86
- "aria-label": t("richText.toolbar", { defaultValue: "Rich text formatting" }),
87
- "data-testid": `${testid}:toolbar`,
88
- children: [
89
- /* @__PURE__ */ jsx(
90
- Button,
91
- {
92
- type: "button",
93
- variant: "ghost",
94
- size: "sm",
95
- "aria-pressed": isBold,
96
- "aria-label": t("richText.bold", { defaultValue: "Bold (Ctrl+B)" }),
97
- "data-testid": `${testid}:bold`,
98
- "data-active": isBold || void 0,
99
- onClick: () => editor.chain().focus().toggleBold().run(),
100
- className: cn("h-7 w-7 p-0", isBold && "bg-accent"),
101
- children: /* @__PURE__ */ jsx(IconBold, { className: "h-3.5 w-3.5", "aria-hidden": true })
102
- }
103
- ),
104
- /* @__PURE__ */ jsx(
105
- Button,
106
- {
107
- type: "button",
108
- variant: "ghost",
109
- size: "sm",
110
- "aria-pressed": isItalic,
111
- "aria-label": t("richText.italic", { defaultValue: "Italic (Ctrl+I)" }),
112
- "data-testid": `${testid}:italic`,
113
- "data-active": isItalic || void 0,
114
- onClick: () => editor.chain().focus().toggleItalic().run(),
115
- className: cn("h-7 w-7 p-0", isItalic && "bg-accent"),
116
- children: /* @__PURE__ */ jsx(IconItalic, { className: "h-3.5 w-3.5", "aria-hidden": true })
117
- }
118
- ),
119
- /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }),
120
- /* @__PURE__ */ jsx(
121
- Button,
122
- {
123
- type: "button",
124
- variant: "ghost",
125
- size: "sm",
126
- "aria-pressed": isParagraph,
127
- "aria-label": t("richText.paragraph", { defaultValue: "Paragraph (Ctrl+Alt+0)" }),
128
- "data-testid": `${testid}:paragraph`,
129
- "data-active": isParagraph || void 0,
130
- onClick: () => editor.chain().focus().setParagraph().run(),
131
- className: cn("h-7 w-7 p-0", isParagraph && "bg-accent"),
132
- children: /* @__PURE__ */ jsx(IconPilcrow, { className: "h-3.5 w-3.5", "aria-hidden": true })
133
- }
134
- ),
135
- /* @__PURE__ */ jsx(
136
- Button,
137
- {
138
- type: "button",
139
- variant: "ghost",
140
- size: "sm",
141
- "aria-pressed": isH1,
142
- "aria-label": t("richText.heading1", { defaultValue: "Heading 1 (Ctrl+Alt+1)" }),
143
- "data-testid": `${testid}:h1`,
144
- "data-active": isH1 || void 0,
145
- onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
146
- className: cn("h-7 w-7 p-0", isH1 && "bg-accent"),
147
- children: /* @__PURE__ */ jsx(IconH1, { className: "h-3.5 w-3.5", "aria-hidden": true })
148
- }
149
- ),
150
- /* @__PURE__ */ jsx(
151
- Button,
152
- {
153
- type: "button",
154
- variant: "ghost",
155
- size: "sm",
156
- "aria-pressed": isH2,
157
- "aria-label": t("richText.heading2", { defaultValue: "Heading 2 (Ctrl+Alt+2)" }),
158
- "data-testid": `${testid}:h2`,
159
- "data-active": isH2 || void 0,
160
- onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
161
- className: cn("h-7 w-7 p-0", isH2 && "bg-accent"),
162
- children: /* @__PURE__ */ jsx(IconH2, { className: "h-3.5 w-3.5", "aria-hidden": true })
163
- }
164
- ),
165
- /* @__PURE__ */ jsx(
166
- Button,
167
- {
168
- type: "button",
169
- variant: "ghost",
170
- size: "sm",
171
- "aria-pressed": isH3,
172
- "aria-label": t("richText.heading3", { defaultValue: "Heading 3 (Ctrl+Alt+3)" }),
173
- "data-testid": `${testid}:h3`,
174
- "data-active": isH3 || void 0,
175
- onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
176
- className: cn("h-7 w-7 p-0", isH3 && "bg-accent"),
177
- children: /* @__PURE__ */ jsx(IconH3, { className: "h-3.5 w-3.5", "aria-hidden": true })
178
- }
179
- ),
180
- /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }),
181
- /* @__PURE__ */ jsx(
182
- Button,
183
- {
184
- type: "button",
185
- variant: "ghost",
186
- size: "sm",
187
- "aria-pressed": isBulletList,
188
- "aria-label": t("richText.bulletList", { defaultValue: "Bullet list (Ctrl+Shift+8)" }),
189
- "data-testid": `${testid}:bullet-list`,
190
- "data-active": isBulletList || void 0,
191
- onClick: () => editor.chain().focus().toggleBulletList().run(),
192
- className: cn("h-7 w-7 p-0", isBulletList && "bg-accent"),
193
- children: /* @__PURE__ */ jsx(IconList, { className: "h-3.5 w-3.5", "aria-hidden": true })
194
- }
195
- ),
196
- /* @__PURE__ */ jsx(
197
- Button,
198
- {
199
- type: "button",
200
- variant: "ghost",
201
- size: "sm",
202
- "aria-pressed": isOrderedList,
203
- "aria-label": t("richText.orderedList", { defaultValue: "Ordered list (Ctrl+Shift+7)" }),
204
- "data-testid": `${testid}:ordered-list`,
205
- "data-active": isOrderedList || void 0,
206
- onClick: () => editor.chain().focus().toggleOrderedList().run(),
207
- className: cn("h-7 w-7 p-0", isOrderedList && "bg-accent"),
208
- children: /* @__PURE__ */ jsx(IconListNumbers, { className: "h-3.5 w-3.5", "aria-hidden": true })
209
- }
210
- ),
211
- /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": true }),
212
- /* @__PURE__ */ jsx(
213
- Button,
214
- {
215
- type: "button",
216
- variant: "ghost",
217
- size: "sm",
218
- "aria-pressed": isLink,
219
- "aria-label": isLink ? t("richText.editLink", { defaultValue: "Edit link (Ctrl+K)" }) : t("richText.insertLink", { defaultValue: "Insert link (Ctrl+K)" }),
220
- "data-testid": `${testid}:link`,
221
- "data-active": isLink || void 0,
222
- onClick: onOpenLinkDialog,
223
- className: cn("h-7 w-7 p-0", isLink && "bg-accent"),
224
- children: /* @__PURE__ */ jsx(IconLink, { className: "h-3.5 w-3.5", "aria-hidden": true })
225
- }
226
- )
227
- ]
228
- }
229
- );
230
- }
231
- function RichTextDisplay({ value }) {
232
- if (value == null || value === "") {
233
- return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
234
- }
235
- const html = sanitizeRichTextHtml(String(value));
236
- if (html === "") {
237
- return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
238
- }
239
- return /* @__PURE__ */ jsx(
240
- "div",
241
- {
242
- className: "prose prose-sm max-w-none dark:prose-invert",
243
- dangerouslySetInnerHTML: { __html: html }
244
- }
245
- );
246
- }
247
- function LinkDialog({
248
- open,
249
- initialUrl,
250
- onOpenChange,
251
- onSubmit,
252
- onRemove,
253
- testid
254
- }) {
255
- const t = useOptionalT();
256
- const [url, setUrl] = React.useState(initialUrl);
257
- const [error, setError] = React.useState(null);
258
- React.useEffect(() => {
259
- if (open) {
260
- setUrl(initialUrl);
261
- setError(null);
262
- }
263
- }, [open, initialUrl]);
264
- function handleSubmit() {
265
- if (url.trim() === "") {
266
- onRemove();
267
- onOpenChange(false);
268
- return;
269
- }
270
- if (!isAllowedLinkScheme(url)) {
271
- setError("URL scheme not allowed (use http, https, mailto, or tel).");
272
- console.warn("[RichTextInput] Link URL rejected: scheme not in allowlist");
273
- return;
274
- }
275
- onSubmit(url);
276
- onOpenChange(false);
277
- }
278
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { "data-testid": `${testid}:link-dialog`, children: [
279
- /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: t("richText.addLinkTitle", { defaultValue: "Add link" }) }) }),
280
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
281
- /* @__PURE__ */ jsx(Label, { htmlFor: `${testid}:link-url`, children: "URL" }),
282
- /* @__PURE__ */ jsx(
283
- Input,
284
- {
285
- id: `${testid}:link-url`,
286
- "data-testid": `${testid}:link-url`,
287
- type: "url",
288
- value: url,
289
- onChange: (e) => setUrl(e.target.value),
290
- placeholder: "https://example.com",
291
- "aria-label": t("richText.url", { defaultValue: "URL" }),
292
- autoFocus: true,
293
- onKeyDown: (e) => {
294
- if (e.key === "Enter") {
295
- e.preventDefault();
296
- handleSubmit();
297
- }
298
- }
299
- }
300
- ),
301
- error ? /* @__PURE__ */ jsx(
302
- "p",
303
- {
304
- className: "text-sm text-destructive",
305
- "data-testid": `${testid}:link-error`,
306
- role: "alert",
307
- children: error
308
- }
309
- ) : null
310
- ] }),
311
- /* @__PURE__ */ jsxs(DialogFooter, { children: [
312
- /* @__PURE__ */ jsx(
313
- Button,
314
- {
315
- type: "button",
316
- variant: "ghost",
317
- onClick: () => onOpenChange(false),
318
- "data-testid": `${testid}:link-cancel`,
319
- children: "Cancel"
320
- }
321
- ),
322
- /* @__PURE__ */ jsx(
323
- Button,
324
- {
325
- type: "button",
326
- variant: "ghost",
327
- onClick: () => {
328
- onRemove();
329
- onOpenChange(false);
330
- },
331
- "data-testid": `${testid}:link-remove`,
332
- children: "Remove"
333
- }
334
- ),
335
- /* @__PURE__ */ jsx(Button, { type: "button", onClick: handleSubmit, "data-testid": `${testid}:link-save`, children: "Save" })
336
- ] })
337
- ] }) });
338
- }
339
- function RichTextInput({
340
- name,
341
- id,
342
- value,
343
- onChange,
344
- testid,
345
- disabled,
346
- required,
347
- ariaInvalid,
348
- ariaDescribedBy,
349
- ariaErrorMessage,
350
- field,
351
- format: _formatProp
352
- }) {
353
- const ui = field?.ui ?? {};
354
- const showToolbar = ui.toolbar !== false;
355
- const valueStr = typeof value === "string" ? value : "";
356
- const editorTestid = testid ?? `rich-text-input:${name}`;
357
- const [linkDialogOpen, setLinkDialogOpen] = React.useState(false);
358
- const [linkDialogInitial, setLinkDialogInitial] = React.useState("");
359
- const editor = useEditor({
360
- extensions: [
361
- StarterKit.configure({
362
- // StarterKit's bundled link extension. Pass options here instead
363
- // of registering @tiptap/extension-link separately to avoid the
364
- // "Duplicate extension names" warning.
365
- link: {
366
- openOnClick: false,
367
- autolink: true,
368
- // HTMLAttributes lands on every <a> rendered by the editor.
369
- // `rel=noopener noreferrer` is a security-best-practice for
370
- // user-authored content; `target=_blank` opens in a new tab.
371
- HTMLAttributes: {
372
- rel: "noopener noreferrer",
373
- target: "_blank"
374
- }
375
- }
376
- })
377
- ],
378
- content: valueStr,
379
- editable: !disabled,
380
- // SSR safety: TipTap recommends setting this to false to avoid
381
- // hydration mismatches when the server-rendered HTML differs from
382
- // the editor's prosemirror DOM. We render the editor only on
383
- // client mount via the `if (!editor) return null` branch below.
384
- immediatelyRender: false,
385
- editorProps: {
386
- attributes: {
387
- role: "textbox",
388
- "aria-multiline": "true",
389
- "aria-required": required ? "true" : "false",
390
- ...ariaInvalid ? { "aria-invalid": "true" } : {},
391
- ...ariaDescribedBy ? { "aria-describedby": ariaDescribedBy } : {},
392
- ...ariaErrorMessage ? { "aria-errormessage": ariaErrorMessage } : {},
393
- ...id ? { id } : {},
394
- "data-testid": `${editorTestid}:editor`,
395
- // Tailwind classes on the contenteditable element for typography.
396
- // `prose-sm` matches the display variant so the editor and the
397
- // saved view look consistent.
398
- class: "prose prose-sm max-w-none dark:prose-invert px-3 py-2 min-h-[6rem] focus:outline-hidden"
399
- }
400
- },
401
- onUpdate: ({ editor: ed }) => {
402
- const html = ed.getHTML();
403
- onChange(html === "<p></p>" ? "" : html);
404
- }
405
- });
406
- const openLinkDialog = React.useCallback(() => {
407
- if (!editor) return;
408
- const existingHref = editor.getAttributes("link").href ?? "";
409
- setLinkDialogInitial(existingHref);
410
- setLinkDialogOpen(true);
411
- }, [editor]);
412
- const submitLink = React.useCallback(
413
- (href) => {
414
- if (!editor) return;
415
- editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
416
- },
417
- [editor]
418
- );
419
- const removeLink = React.useCallback(() => {
420
- if (!editor) return;
421
- editor.chain().focus().unsetLink().run();
422
- }, [editor]);
423
- React.useEffect(() => {
424
- if (!editor) return;
425
- const current = editor.getHTML();
426
- const incoming = valueStr === "" ? "<p></p>" : valueStr;
427
- if (current === incoming) return;
428
- editor.commands.setContent(valueStr, { emitUpdate: false });
429
- }, [editor, valueStr]);
430
- React.useEffect(() => {
431
- if (!editor) return;
432
- editor.setEditable(!disabled);
433
- }, [editor, disabled]);
434
- if (!editor) {
435
- return /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-background", "data-testid": `${editorTestid}:root`, children: [
436
- /* @__PURE__ */ jsx(
437
- "div",
438
- {
439
- className: "flex h-9 items-center border-b bg-muted/30 px-2 text-xs text-muted-foreground",
440
- "aria-hidden": true,
441
- children: "Loading editor\u2026"
442
- }
443
- ),
444
- /* @__PURE__ */ jsx(
445
- "div",
446
- {
447
- id,
448
- "data-testid": `${editorTestid}:editor`,
449
- className: "min-h-[6rem] px-3 py-2 text-sm text-muted-foreground",
450
- role: "textbox",
451
- "aria-multiline": "true",
452
- "aria-busy": "true",
453
- children: valueStr ? "" : "Rich text editor loading\u2026"
454
- }
455
- ),
456
- /* @__PURE__ */ jsx("input", { type: "hidden", name, value: valueStr })
457
- ] });
458
- }
459
- return /* @__PURE__ */ jsxs(
460
- "div",
461
- {
462
- className: cn(
463
- "rounded-md border bg-background text-foreground focus-within:ring-2 focus-within:ring-ring",
464
- ariaInvalid && "border-destructive focus-within:ring-destructive",
465
- disabled && "opacity-60"
466
- ),
467
- "data-testid": `${editorTestid}:root`,
468
- "data-invalid": ariaInvalid || void 0,
469
- children: [
470
- showToolbar ? /* @__PURE__ */ jsx(Toolbar, { editor, testid: editorTestid, onOpenLinkDialog: openLinkDialog }) : null,
471
- /* @__PURE__ */ jsx(EditorContent, { editor }),
472
- /* @__PURE__ */ jsx(
473
- LinkDialog,
474
- {
475
- open: linkDialogOpen,
476
- initialUrl: linkDialogInitial,
477
- onOpenChange: setLinkDialogOpen,
478
- onSubmit: submitLink,
479
- onRemove: removeLink,
480
- testid: editorTestid
481
- }
482
- ),
483
- /* @__PURE__ */ jsx("input", { type: "hidden", name, value: valueStr })
484
- ]
485
- }
486
- );
487
- }
488
-
489
- export { RichTextDisplay, RichTextInput, sanitizeRichTextHtml };
490
- //# sourceMappingURL=rich-text-N2FZEJJV.js.map
491
- //# sourceMappingURL=rich-text-N2FZEJJV.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fields/rich-text.tsx"],"names":["LinkIcon"],"mappings":"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","file":"rich-text-N2FZEJJV.js","sourcesContent":["'use client';\nimport * as React from 'react';\nimport {\n IconBold,\n IconH1,\n IconH2,\n IconH3,\n IconItalic,\n IconLink as LinkIcon,\n IconList,\n IconListNumbers,\n IconPilcrow,\n} from '@tabler/icons-react';\nimport { useOptionalT } from '@burojs/react';\nimport { useEditor, EditorContent, type Editor } from '@tiptap/react';\nimport { generateHTML, generateJSON } from '@tiptap/core';\nimport StarterKit from '@tiptap/starter-kit';\n// NB: StarterKit already bundles `@tiptap/extension-link` in v3 — passing\n// `link: { ... }` configures the bundled instance rather than adding a\n// duplicate extension. Listing `@tiptap/extension-link` as a direct\n// runtime dep keeps the package's intent explicit and gives us a stable\n// import path for any future override.\nimport { Button } from '../primitives/button.js';\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../primitives/dialog.js';\nimport { Input } from '../primitives/input.js';\nimport { Label } from '../primitives/label.js';\nimport { cn } from '../lib/cn.js';\nimport type { FieldInputProps, FieldRendererProps } from './registry.js';\n\n/**\n * `<RichTextInput>` — TipTap-backed rich-text editor renderer (B5).\n *\n * Editor: TipTap v3 (`@tiptap/react` + `@tiptap/starter-kit` +\n * `@tiptap/extension-link`). See spec\n * `docs/superpowers/specs/2026-05-07-forms-rich-text.md` for the\n * editor + output-format decision rationale.\n *\n * Output format: HTML (`editor.getHTML()`). v0.2 ships only `'html'`;\n * markdown serialization is deferred to v0.3.\n *\n * Toolbar buttons:\n * - Bold / Italic — toggle marks\n * - Heading dropdown — H1 / H2 / H3 / Paragraph\n * - Bullet list / Ordered list — toggle node\n * - Link — opens an accessible Radix Dialog for URL entry\n *\n * Each toolbar button surfaces `aria-pressed` reflecting the current\n * mark/node active state so screen readers announce toggle state.\n *\n * SSR safety: `useEditor` returns null during the first server render\n * because TipTap's ProseMirror view depends on the DOM. The component\n * renders a placeholder dropzone-style div while editor is null so the\n * layout doesn't shift on hydration.\n *\n * aria-* propagation: `ariaInvalid` / `ariaDescribedBy` /\n * `ariaErrorMessage` land on the contenteditable wrapper via\n * `editorProps.attributes` so the form's error association follows the\n * focused control. The wrapper carries `role=\"textbox\"` +\n * `aria-multiline=\"true\"` so AT users hear the right control type.\n */\n\nexport interface RichTextInputProps extends FieldInputProps {\n /** Output format. v0.2 ships only `'html'`. */\n format?: 'html';\n}\n\ninterface ToolbarProps {\n editor: Editor;\n testid: string;\n onOpenLinkDialog: () => void;\n}\n\n/** Allowed link URL schemes — defends against `javascript:` and `data:`\n * smuggling in user-supplied URLs from the link dialog. */\nfunction isAllowedLinkScheme(url: string): boolean {\n if (typeof url !== 'string') return false;\n const trimmed = url.trim();\n if (trimmed.length === 0) return false;\n // Reject explicitly dangerous schemes regardless of casing/whitespace.\n const lowered = trimmed.toLowerCase();\n if (\n lowered.startsWith('javascript:') ||\n lowered.startsWith('data:') ||\n lowered.startsWith('vbscript:')\n ) {\n return false;\n }\n const allowed = ['http:', 'https:', 'mailto:', 'tel:'];\n try {\n const base =\n typeof window !== 'undefined' && window.location?.origin\n ? window.location.origin\n : 'http://localhost';\n const parsed = new URL(trimmed, base);\n return allowed.includes(parsed.protocol);\n } catch {\n // Allow relative URLs (no protocol) — the URL constructor would\n // have parsed them against the base above; if we land here the\n // string is genuinely unparseable.\n return false;\n }\n}\n\n/** Sanitize stored HTML by round-tripping through TipTap's schema.\n * Drops `<script>`, `<iframe>`, unknown nodes, and event-handler\n * attributes (`onerror=`, `onclick=`, …). Also normalizes any\n * `<a>` element to carry `rel=\"noopener noreferrer\"` and\n * `target=\"_blank\"` via the link extension's HTMLAttributes. */\nexport function sanitizeRichTextHtml(value: string): string {\n if (!value || typeof value !== 'string') return '';\n const extensions = [\n StarterKit.configure({\n link: {\n openOnClick: false,\n HTMLAttributes: {\n rel: 'noopener noreferrer',\n target: '_blank',\n },\n },\n }),\n ];\n try {\n const json = generateJSON(value, extensions);\n let html = generateHTML(json, extensions);\n // Belt-and-braces: TipTap's link extension applies rel/target to\n // links it parses, but we double-check via a regex pass so any\n // `<a>` that slipped through (e.g. inline anchors generated\n // outside the link extension) is still hardened.\n html = ensureSafeAnchorAttrs(html);\n return html;\n } catch {\n // If parse fails (malformed HTML the schema rejects outright),\n // return empty string rather than rendering raw markup.\n return '';\n }\n}\n\n/** Add `rel=\"noopener noreferrer\"` and `target=\"_blank\"` to every\n * `<a>` tag if missing. Idempotent — safe to call on already-safe\n * HTML. */\nfunction ensureSafeAnchorAttrs(html: string): string {\n return html.replace(/<a\\b([^>]*)>/gi, (match, attrs: string) => {\n const hasRel = /\\brel\\s*=/.test(attrs);\n const hasTarget = /\\btarget\\s*=/.test(attrs);\n let next = attrs;\n if (!hasRel) next += ' rel=\"noopener noreferrer\"';\n if (!hasTarget) next += ' target=\"_blank\"';\n return `<a${next}>`;\n });\n}\n\nfunction Toolbar({ editor, testid, onOpenLinkDialog }: ToolbarProps): React.ReactElement {\n const t = useOptionalT();\n // Subscribe to editor selection/transaction events so the toolbar\n // re-renders when the active mark/node state changes (toggles\n // aria-pressed correctly). useEditor already triggers re-render on\n // state change in TipTap v3 — this state mirror is a belt-and-braces\n // for testing-library's snapshot model.\n const [, forceUpdate] = React.useReducer((x: number) => x + 1, 0);\n React.useEffect(() => {\n const handler = () => forceUpdate();\n editor.on('selectionUpdate', handler);\n editor.on('transaction', handler);\n return () => {\n editor.off('selectionUpdate', handler);\n editor.off('transaction', handler);\n };\n }, [editor]);\n\n // Active-state probes. `editor.isActive('bold')` etc. is TipTap's\n // canonical predicate; it works with selection ranges + cursor\n // positions and accounts for marks already applied.\n const isBold = editor.isActive('bold');\n const isItalic = editor.isActive('italic');\n const isBulletList = editor.isActive('bulletList');\n const isOrderedList = editor.isActive('orderedList');\n const isH1 = editor.isActive('heading', { level: 1 });\n const isH2 = editor.isActive('heading', { level: 2 });\n const isH3 = editor.isActive('heading', { level: 3 });\n const isLink = editor.isActive('link');\n const isParagraph = editor.isActive('paragraph') && !isH1 && !isH2 && !isH3;\n\n return (\n <div\n className=\"flex flex-wrap items-center gap-1 border-b bg-muted/30 px-2 py-1.5\"\n role=\"toolbar\"\n aria-label={t('richText.toolbar', { defaultValue: 'Rich text formatting' })}\n data-testid={`${testid}:toolbar`}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isBold}\n aria-label={t('richText.bold', { defaultValue: 'Bold (Ctrl+B)' })}\n data-testid={`${testid}:bold`}\n data-active={isBold || undefined}\n onClick={() => editor.chain().focus().toggleBold().run()}\n className={cn('h-7 w-7 p-0', isBold && 'bg-accent')}\n >\n <IconBold className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isItalic}\n aria-label={t('richText.italic', { defaultValue: 'Italic (Ctrl+I)' })}\n data-testid={`${testid}:italic`}\n data-active={isItalic || undefined}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n className={cn('h-7 w-7 p-0', isItalic && 'bg-accent')}\n >\n <IconItalic className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <span className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isParagraph}\n aria-label={t('richText.paragraph', { defaultValue: 'Paragraph (Ctrl+Alt+0)' })}\n data-testid={`${testid}:paragraph`}\n data-active={isParagraph || undefined}\n onClick={() => editor.chain().focus().setParagraph().run()}\n className={cn('h-7 w-7 p-0', isParagraph && 'bg-accent')}\n >\n <IconPilcrow className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isH1}\n aria-label={t('richText.heading1', { defaultValue: 'Heading 1 (Ctrl+Alt+1)' })}\n data-testid={`${testid}:h1`}\n data-active={isH1 || undefined}\n onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}\n className={cn('h-7 w-7 p-0', isH1 && 'bg-accent')}\n >\n <IconH1 className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isH2}\n aria-label={t('richText.heading2', { defaultValue: 'Heading 2 (Ctrl+Alt+2)' })}\n data-testid={`${testid}:h2`}\n data-active={isH2 || undefined}\n onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}\n className={cn('h-7 w-7 p-0', isH2 && 'bg-accent')}\n >\n <IconH2 className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isH3}\n aria-label={t('richText.heading3', { defaultValue: 'Heading 3 (Ctrl+Alt+3)' })}\n data-testid={`${testid}:h3`}\n data-active={isH3 || undefined}\n onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}\n className={cn('h-7 w-7 p-0', isH3 && 'bg-accent')}\n >\n <IconH3 className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <span className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isBulletList}\n aria-label={t('richText.bulletList', { defaultValue: 'Bullet list (Ctrl+Shift+8)' })}\n data-testid={`${testid}:bullet-list`}\n data-active={isBulletList || undefined}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n className={cn('h-7 w-7 p-0', isBulletList && 'bg-accent')}\n >\n <IconList className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isOrderedList}\n aria-label={t('richText.orderedList', { defaultValue: 'Ordered list (Ctrl+Shift+7)' })}\n data-testid={`${testid}:ordered-list`}\n data-active={isOrderedList || undefined}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n className={cn('h-7 w-7 p-0', isOrderedList && 'bg-accent')}\n >\n <IconListNumbers className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n <span className=\"mx-1 h-4 w-px bg-border\" aria-hidden />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-pressed={isLink}\n aria-label={\n isLink\n ? t('richText.editLink', { defaultValue: 'Edit link (Ctrl+K)' })\n : t('richText.insertLink', { defaultValue: 'Insert link (Ctrl+K)' })\n }\n data-testid={`${testid}:link`}\n data-active={isLink || undefined}\n onClick={onOpenLinkDialog}\n className={cn('h-7 w-7 p-0', isLink && 'bg-accent')}\n >\n <LinkIcon className=\"h-3.5 w-3.5\" aria-hidden />\n </Button>\n </div>\n );\n}\n\n/** Display variant: render the saved HTML through TipTap's sanitize\n * pass before injection. Falls back to em-dash when empty.\n * Wrapped in a `prose` container so saved HTML inherits typographic\n * defaults consistent with other display renderers.\n *\n * Security: legacy / adversarial HTML (e.g. records imported from a\n * spreadsheet, payloads from a compromised collaborator) may carry\n * `<script>` / `<iframe>` / `onerror=` smuggling attempts. The\n * sanitize pass round-trips the value through TipTap's schema,\n * dropping nodes outside the editor's allow-list and stripping\n * event-handler attributes. */\nexport function RichTextDisplay({ value }: FieldRendererProps): React.ReactElement {\n if (value == null || value === '') {\n return <span className=\"text-muted-foreground\">—</span>;\n }\n const html = sanitizeRichTextHtml(String(value));\n if (html === '') {\n return <span className=\"text-muted-foreground\">—</span>;\n }\n return (\n <div\n className=\"prose prose-sm max-w-none dark:prose-invert\"\n // biome-ignore lint/security/noDangerouslySetInnerHtml: sanitized via TipTap schema round-trip\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n\ninterface LinkDialogProps {\n open: boolean;\n initialUrl: string;\n onOpenChange: (open: boolean) => void;\n onSubmit: (url: string) => void;\n onRemove: () => void;\n testid: string;\n}\n\nfunction LinkDialog({\n open,\n initialUrl,\n onOpenChange,\n onSubmit,\n onRemove,\n testid,\n}: LinkDialogProps): React.ReactElement {\n const t = useOptionalT();\n const [url, setUrl] = React.useState(initialUrl);\n const [error, setError] = React.useState<string | null>(null);\n\n // Reset internal state when the dialog opens with a fresh URL.\n React.useEffect(() => {\n if (open) {\n setUrl(initialUrl);\n setError(null);\n }\n }, [open, initialUrl]);\n\n function handleSubmit() {\n if (url.trim() === '') {\n // Empty URL = remove link mark.\n onRemove();\n onOpenChange(false);\n return;\n }\n if (!isAllowedLinkScheme(url)) {\n setError('URL scheme not allowed (use http, https, mailto, or tel).');\n // eslint-disable-next-line no-console\n console.warn('[RichTextInput] Link URL rejected: scheme not in allowlist');\n return;\n }\n onSubmit(url);\n onOpenChange(false);\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent data-testid={`${testid}:link-dialog`}>\n <DialogHeader>\n <DialogTitle>{t('richText.addLinkTitle', { defaultValue: 'Add link' })}</DialogTitle>\n </DialogHeader>\n <div className=\"space-y-2\">\n <Label htmlFor={`${testid}:link-url`}>URL</Label>\n <Input\n id={`${testid}:link-url`}\n data-testid={`${testid}:link-url`}\n type=\"url\"\n value={url}\n onChange={(e) => setUrl(e.target.value)}\n placeholder=\"https://example.com\"\n aria-label={t('richText.url', { defaultValue: 'URL' })}\n autoFocus\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n {error ? (\n <p\n className=\"text-sm text-destructive\"\n data-testid={`${testid}:link-error`}\n role=\"alert\"\n >\n {error}\n </p>\n ) : null}\n </div>\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => onOpenChange(false)}\n data-testid={`${testid}:link-cancel`}\n >\n Cancel\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => {\n onRemove();\n onOpenChange(false);\n }}\n data-testid={`${testid}:link-remove`}\n >\n Remove\n </Button>\n <Button type=\"button\" onClick={handleSubmit} data-testid={`${testid}:link-save`}>\n Save\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport function RichTextInput({\n name,\n id,\n value,\n onChange,\n testid,\n disabled,\n required,\n ariaInvalid,\n ariaDescribedBy,\n ariaErrorMessage,\n field,\n format: _formatProp,\n}: RichTextInputProps): React.ReactElement | null {\n const ui = (field?.ui ?? {}) as { format?: string; toolbar?: false };\n // v0.2 ships HTML output only — `format` is accepted (and may be\n // forwarded as a string e.g. `\"html\"`) but the renderer always\n // returns HTML. `_formatProp` is intentionally unused; kept on the\n // props type for forward-compat with v0.3 markdown.\n const showToolbar = ui.toolbar !== false;\n\n const valueStr = typeof value === 'string' ? value : '';\n const editorTestid = testid ?? `rich-text-input:${name}`;\n\n const [linkDialogOpen, setLinkDialogOpen] = React.useState(false);\n const [linkDialogInitial, setLinkDialogInitial] = React.useState('');\n\n const editor = useEditor({\n extensions: [\n StarterKit.configure({\n // StarterKit's bundled link extension. Pass options here instead\n // of registering @tiptap/extension-link separately to avoid the\n // \"Duplicate extension names\" warning.\n link: {\n openOnClick: false,\n autolink: true,\n // HTMLAttributes lands on every <a> rendered by the editor.\n // `rel=noopener noreferrer` is a security-best-practice for\n // user-authored content; `target=_blank` opens in a new tab.\n HTMLAttributes: {\n rel: 'noopener noreferrer',\n target: '_blank',\n },\n },\n }),\n ],\n content: valueStr,\n editable: !disabled,\n // SSR safety: TipTap recommends setting this to false to avoid\n // hydration mismatches when the server-rendered HTML differs from\n // the editor's prosemirror DOM. We render the editor only on\n // client mount via the `if (!editor) return null` branch below.\n immediatelyRender: false,\n editorProps: {\n attributes: {\n role: 'textbox',\n 'aria-multiline': 'true',\n 'aria-required': required ? 'true' : 'false',\n ...(ariaInvalid ? { 'aria-invalid': 'true' } : {}),\n ...(ariaDescribedBy ? { 'aria-describedby': ariaDescribedBy } : {}),\n ...(ariaErrorMessage ? { 'aria-errormessage': ariaErrorMessage } : {}),\n ...(id ? { id } : {}),\n 'data-testid': `${editorTestid}:editor`,\n // Tailwind classes on the contenteditable element for typography.\n // `prose-sm` matches the display variant so the editor and the\n // saved view look consistent.\n class:\n 'prose prose-sm max-w-none dark:prose-invert px-3 py-2 min-h-[6rem] focus:outline-hidden',\n },\n },\n onUpdate: ({ editor: ed }) => {\n // TipTap returns `<p></p>` for an empty editor — normalize that\n // to '' so the form treats \"no content\" identically regardless\n // of whether the user typed-then-deleted or never typed at all.\n const html = ed.getHTML();\n onChange(html === '<p></p>' ? '' : html);\n },\n });\n\n const openLinkDialog = React.useCallback(() => {\n if (!editor) return;\n const existingHref = (editor.getAttributes('link').href as string | undefined) ?? '';\n setLinkDialogInitial(existingHref);\n setLinkDialogOpen(true);\n }, [editor]);\n\n const submitLink = React.useCallback(\n (href: string) => {\n if (!editor) return;\n editor.chain().focus().extendMarkRange('link').setLink({ href }).run();\n },\n [editor],\n );\n\n const removeLink = React.useCallback(() => {\n if (!editor) return;\n editor.chain().focus().unsetLink().run();\n }, [editor]);\n\n // Sync external value changes (e.g. record loaded after first paint,\n // or programmatic clear) into the editor without triggering onUpdate\n // recursion. TipTap exposes `setContent` with `emitUpdate: false`.\n React.useEffect(() => {\n if (!editor) return;\n const current = editor.getHTML();\n const incoming = valueStr === '' ? '<p></p>' : valueStr;\n if (current === incoming) return;\n editor.commands.setContent(valueStr, { emitUpdate: false });\n }, [editor, valueStr]);\n\n // Reflect `disabled` prop changes into TipTap's editable state.\n React.useEffect(() => {\n if (!editor) return;\n editor.setEditable(!disabled);\n }, [editor, disabled]);\n\n // SSR / first-render safety: `useEditor` returns null on initial\n // server-side render (immediatelyRender: false). Render a stable\n // placeholder so the layout doesn't shift on hydration. The\n // placeholder carries the same id/data-testid as the eventual\n // contenteditable so test selectors resolve consistently.\n if (!editor) {\n return (\n <div className=\"rounded-md border bg-background\" data-testid={`${editorTestid}:root`}>\n <div\n className=\"flex h-9 items-center border-b bg-muted/30 px-2 text-xs text-muted-foreground\"\n aria-hidden\n >\n Loading editor…\n </div>\n <div\n id={id}\n data-testid={`${editorTestid}:editor`}\n className=\"min-h-[6rem] px-3 py-2 text-sm text-muted-foreground\"\n role=\"textbox\"\n aria-multiline=\"true\"\n aria-busy=\"true\"\n >\n {valueStr ? '' : 'Rich text editor loading…'}\n </div>\n {/* Hidden form input so the `name` is still submittable in\n uncontrolled-form scenarios; the form reads from state, but\n this keeps the DOM well-formed. */}\n <input type=\"hidden\" name={name} value={valueStr} />\n </div>\n );\n }\n\n return (\n <div\n className={cn(\n 'rounded-md border bg-background text-foreground focus-within:ring-2 focus-within:ring-ring',\n ariaInvalid && 'border-destructive focus-within:ring-destructive',\n disabled && 'opacity-60',\n )}\n data-testid={`${editorTestid}:root`}\n data-invalid={ariaInvalid || undefined}\n >\n {showToolbar ? (\n <Toolbar editor={editor} testid={editorTestid} onOpenLinkDialog={openLinkDialog} />\n ) : null}\n <EditorContent editor={editor} />\n <LinkDialog\n open={linkDialogOpen}\n initialUrl={linkDialogInitial}\n onOpenChange={setLinkDialogOpen}\n onSubmit={submitLink}\n onRemove={removeLink}\n testid={editorTestid}\n />\n {/* Hidden field carrying the canonical name for forms that read\n via the DOM (rare; the form controller reads from state). */}\n <input type=\"hidden\" name={name} value={valueStr} />\n </div>\n );\n}\n"]}