@drawnagency/primitives 0.1.60 → 0.1.62

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 (82) hide show
  1. package/dist/chunk-2BHPWO7M.js +558 -0
  2. package/dist/chunk-4E66N6UV.js +10 -0
  3. package/dist/{chunk-RFZNNCAS.js → chunk-53DKVAB6.js} +1 -61
  4. package/dist/chunk-5THS4K5D.js +123 -0
  5. package/dist/chunk-677NTHAZ.js +27 -0
  6. package/dist/{chunk-TG43X7JO.js → chunk-75P6HWPN.js} +29 -5
  7. package/dist/chunk-7EOSDFS3.js +10 -0
  8. package/dist/{chunk-ODCJQOVO.js → chunk-7IMQVDDJ.js} +1 -1
  9. package/dist/chunk-FJBDGYHE.js +782 -0
  10. package/dist/chunk-I5737FJY.js +62 -0
  11. package/dist/chunk-JRT4KWHU.js +1201 -0
  12. package/dist/chunk-QWDOJUUO.js +648 -0
  13. package/dist/{chunk-BT7STGDW.js → chunk-TEYTLQVH.js} +0 -15
  14. package/dist/chunk-WDSVHKAH.js +54 -0
  15. package/dist/{chunk-Q5EEYBMJ.js → chunk-ZYXLNJ5M.js} +6 -30
  16. package/dist/components/brandguide/ColorSwatchSettings.d.ts +1 -1
  17. package/dist/components/brandguide/ColorSwatchSettings.d.ts.map +1 -1
  18. package/dist/components/brandguide/Colors.d.ts +1 -1
  19. package/dist/components/brandguide/Colors.d.ts.map +1 -1
  20. package/dist/components/editor/index.js +642 -0
  21. package/dist/components/primitives/IconPicker.d.ts +1 -7
  22. package/dist/components/primitives/IconPicker.d.ts.map +1 -1
  23. package/dist/components/primitives/index.d.ts +10 -0
  24. package/dist/components/primitives/index.d.ts.map +1 -0
  25. package/dist/components/primitives/index.js +169 -0
  26. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  27. package/dist/components/sections/Colors/schema.d.ts +19 -0
  28. package/dist/components/sections/Colors/schema.d.ts.map +1 -0
  29. package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts +20 -0
  30. package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts.map +1 -0
  31. package/dist/components/sections/DoDontMedia/index.d.ts +45 -0
  32. package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -0
  33. package/dist/components/sections/IconList/DoDontToggle.d.ts +14 -0
  34. package/dist/components/sections/IconList/DoDontToggle.d.ts.map +1 -0
  35. package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
  36. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  37. package/dist/components/sections/Media/MediaBlock.d.ts +1 -3
  38. package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -1
  39. package/dist/components/sections/Media/index.d.ts +0 -1
  40. package/dist/components/sections/Media/index.d.ts.map +1 -1
  41. package/dist/components/sections/all-sections.d.ts +54 -18
  42. package/dist/components/sections/all-sections.d.ts.map +1 -1
  43. package/dist/components/sections/brand-guide.d.ts +84 -0
  44. package/dist/components/sections/brand-guide.d.ts.map +1 -0
  45. package/dist/components/sections/brand-guide.js +21 -0
  46. package/dist/components/sections/core-sections.d.ts +93 -0
  47. package/dist/components/sections/core-sections.d.ts.map +1 -0
  48. package/dist/components/sections/register-schemas.js +240 -3353
  49. package/dist/index.js +48 -46
  50. package/dist/lib/index.js +17 -11
  51. package/dist/lib/loader.d.ts.map +1 -1
  52. package/dist/lib/migrate-sections-transform.d.ts.map +1 -1
  53. package/dist/lib/migrate-sections-transform.js +1 -1
  54. package/dist/lib/registry.d.ts +6 -1
  55. package/dist/lib/registry.d.ts.map +1 -1
  56. package/dist/media/index.js +1 -1
  57. package/dist/schemas/index.js +9 -13
  58. package/dist/schemas/link.js +13 -0
  59. package/dist/schemas/shared.d.ts +0 -17
  60. package/dist/schemas/shared.d.ts.map +1 -1
  61. package/package.json +13 -1
  62. package/src/components/brandguide/ColorSwatchSettings.tsx +1 -1
  63. package/src/components/brandguide/Colors.tsx +1 -1
  64. package/src/components/primitives/IconPicker.tsx +27 -119
  65. package/src/components/primitives/index.ts +11 -0
  66. package/src/components/sections/Colors/index.tsx +2 -1
  67. package/src/components/sections/Colors/schema.ts +24 -0
  68. package/src/components/sections/DoDontMedia/DoDontMediaBlock.tsx +35 -0
  69. package/src/components/sections/DoDontMedia/index.tsx +84 -0
  70. package/src/components/sections/IconList/DoDontToggle.tsx +76 -0
  71. package/src/components/sections/IconList/IconList.tsx +24 -14
  72. package/src/components/sections/IconList/index.tsx +1 -0
  73. package/src/components/sections/Media/MediaBlock.tsx +1 -11
  74. package/src/components/sections/Media/index.tsx +0 -15
  75. package/src/components/sections/all-sections.ts +7 -35
  76. package/src/components/sections/brand-guide.ts +13 -0
  77. package/src/components/sections/core-sections.ts +26 -0
  78. package/src/lib/loader.ts +10 -4
  79. package/src/lib/migrate-sections-transform.ts +33 -5
  80. package/src/lib/registry.ts +6 -1
  81. package/src/schemas/shared.ts +0 -18
  82. /package/dist/{chunk-ZU2MKPTG.js → chunk-3DCG4DNU.js} +0 -0
@@ -0,0 +1,782 @@
1
+ import {
2
+ EditablePlainText
3
+ } from "./chunk-QWDOJUUO.js";
4
+ import {
5
+ usePages
6
+ } from "./chunk-4E66N6UV.js";
7
+ import {
8
+ sanitizeHtml,
9
+ toSectionId
10
+ } from "./chunk-53DKVAB6.js";
11
+ import {
12
+ cn
13
+ } from "./chunk-7EOSDFS3.js";
14
+ import {
15
+ defineSection
16
+ } from "./chunk-VKAGMEKE.js";
17
+
18
+ // src/components/primitives/defineHeadingSection.tsx
19
+ import { z } from "zod";
20
+
21
+ // src/components/primitives/HeadingSection.tsx
22
+ import { jsx } from "react/jsx-runtime";
23
+ function HeadingSection({ heading, tag, placeholder, className, onChange }) {
24
+ return /* @__PURE__ */ jsx(
25
+ EditablePlainText,
26
+ {
27
+ tag,
28
+ value: heading,
29
+ onChange: onChange ?? (() => {
30
+ }),
31
+ isEditMode: !!onChange,
32
+ placeholder,
33
+ id: toSectionId(heading),
34
+ className: cn("scroll-mt-20", className)
35
+ }
36
+ );
37
+ }
38
+
39
+ // src/components/primitives/defineHeadingSection.tsx
40
+ import { jsx as jsx2 } from "react/jsx-runtime";
41
+ function defineHeadingSection(config) {
42
+ const content = config.navExclusion ? z.object({ heading: z.string(), excludeFromNav: z.boolean().optional() }) : z.object({ heading: z.string() });
43
+ const schema = z.object({ type: z.literal(config.type), content });
44
+ return defineSection({
45
+ type: config.type,
46
+ label: config.label,
47
+ icon: config.icon,
48
+ schema,
49
+ navRole: config.navRole,
50
+ component: ({ content: content2, onChange }) => /* @__PURE__ */ jsx2(
51
+ HeadingSection,
52
+ {
53
+ heading: content2.content.heading,
54
+ tag: config.tag,
55
+ placeholder: config.placeholder,
56
+ onChange: onChange ? (heading) => onChange({ ...content2, content: { ...content2.content, heading } }) : void 0
57
+ }
58
+ ),
59
+ defaults: () => ({ type: config.type, content: { heading: config.defaultHeading } }),
60
+ getLabel: (content2) => content2.content.heading,
61
+ ...config.navExclusion ? {
62
+ settings: {
63
+ excludeFromNav: { type: "checkbox", label: "Exclude from navigation", default: false, target: "content" }
64
+ }
65
+ } : {}
66
+ });
67
+ }
68
+
69
+ // src/components/primitives/LinkPopover.tsx
70
+ import { useState } from "react";
71
+
72
+ // src/components/shared/Button.tsx
73
+ import { forwardRef } from "react";
74
+ import { jsx as jsx3 } from "react/jsx-runtime";
75
+ var boxSize = {
76
+ sm: "rounded px-3 py-1.5 text-xs font-medium",
77
+ md: "rounded px-4 py-2 text-sm font-medium"
78
+ };
79
+ var ghostSize = {
80
+ sm: "text-xs font-medium",
81
+ md: "text-sm font-medium"
82
+ };
83
+ var variantClasses = {
84
+ primary: "bg-base-contrast text-base-accent hover:bg-base-contrast/90",
85
+ brand: "bg-primary text-primary-contrast hover:opacity-90",
86
+ secondary: "border border-base-200 text-base-contrast hover:bg-base-accent",
87
+ destructive: "border border-red-600 text-red-600 hover:bg-red-600 hover:text-white"
88
+ };
89
+ var ghostToneClasses = {
90
+ neutral: "text-base-contrast-light hover:text-base-contrast",
91
+ destructive: "text-red-600 hover:text-red-800"
92
+ };
93
+ var Button = forwardRef(function Button2({
94
+ variant = "primary",
95
+ size = "sm",
96
+ tone = "neutral",
97
+ fullWidth = false,
98
+ isLoading = false,
99
+ loadingLabel,
100
+ disabled,
101
+ className,
102
+ children,
103
+ ...rest
104
+ }, ref) {
105
+ const isGhost = variant === "ghost";
106
+ const resolvedDisabled = disabled || isLoading;
107
+ const label = isLoading && loadingLabel ? loadingLabel : children;
108
+ return /* @__PURE__ */ jsx3(
109
+ "button",
110
+ {
111
+ ref,
112
+ disabled: resolvedDisabled,
113
+ className: cn(
114
+ "inline-flex items-center justify-center transition-colors",
115
+ isGhost ? ghostSize[size] : boxSize[size],
116
+ isGhost ? ghostToneClasses[tone] : variantClasses[variant],
117
+ fullWidth && "w-full",
118
+ resolvedDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
119
+ className
120
+ ),
121
+ ...rest,
122
+ children: label
123
+ }
124
+ );
125
+ });
126
+
127
+ // src/lib/links.ts
128
+ function internalHref(pageId, anchorSectionId) {
129
+ return `page://${pageId}${anchorSectionId ? `#${anchorSectionId}` : ""}`;
130
+ }
131
+ function parseInternalHref(href) {
132
+ const m = /^page:\/\/([^#]+)(?:#(.+))?$/.exec(href);
133
+ return m ? { pageId: m[1], anchorSectionId: m[2] ?? null } : null;
134
+ }
135
+
136
+ // src/components/primitives/LinkPopover.tsx
137
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
138
+ var selectClasses = "w-full rounded border border-base-contrast/20 bg-base px-2 py-1 text-sm text-base-contrast focus:outline-none focus:ring-1 focus:ring-primary";
139
+ function LinkPopover({ editor, onClose }) {
140
+ const { pages, getPageHeadings } = usePages();
141
+ const existingAttrs = editor.getAttributes("link");
142
+ const existingInternal = parseInternalHref(existingAttrs.href ?? "");
143
+ const [mode, setMode] = useState(existingInternal ? "internal" : "external");
144
+ const [url, setUrl] = useState(existingInternal ? "" : existingAttrs.href ?? "");
145
+ const [pageId, setPageId] = useState(existingInternal?.pageId ?? "");
146
+ const [anchorId, setAnchorId] = useState(existingInternal?.anchorSectionId ?? "");
147
+ const [openInNewTab, setOpenInNewTab] = useState(
148
+ existingAttrs.target === "_blank"
149
+ );
150
+ const hasLink = Boolean(existingAttrs.href);
151
+ const headings = pageId ? getPageHeadings(pageId) : [];
152
+ const handleApply = () => {
153
+ if (mode === "internal") {
154
+ if (pageId) {
155
+ editor.chain().focus().extendMarkRange("link").setLink({ href: internalHref(pageId, anchorId || null), target: null }).run();
156
+ }
157
+ } else if (url.trim()) {
158
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url.trim(), target: openInNewTab ? "_blank" : null }).run();
159
+ }
160
+ onClose();
161
+ };
162
+ const handleRemove = () => {
163
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
164
+ onClose();
165
+ };
166
+ const handleKeyDown = (e) => {
167
+ if (e.key === "Enter") {
168
+ e.preventDefault();
169
+ handleApply();
170
+ } else if (e.key === "Escape") {
171
+ e.preventDefault();
172
+ onClose();
173
+ }
174
+ };
175
+ return /* @__PURE__ */ jsxs(
176
+ "div",
177
+ {
178
+ className: "flex w-64 flex-col gap-2 rounded bg-base-accent p-3 shadow-lg",
179
+ onMouseDown: (e) => e.stopPropagation(),
180
+ onKeyDown: handleKeyDown,
181
+ children: [
182
+ /* @__PURE__ */ jsx4("div", { className: "flex gap-2", role: "tablist", children: ["external", "internal"].map((kind) => /* @__PURE__ */ jsx4(
183
+ "button",
184
+ {
185
+ type: "button",
186
+ role: "tab",
187
+ "aria-selected": mode === kind,
188
+ onClick: () => setMode(kind),
189
+ className: cn(
190
+ "cursor-pointer rounded border px-2 py-0.5 text-xs",
191
+ mode === kind ? "border-primary bg-primary text-primary-contrast" : "border-base-200 text-base-contrast"
192
+ ),
193
+ children: kind === "external" ? "External URL" : "Internal page"
194
+ },
195
+ kind
196
+ )) }),
197
+ mode === "external" ? /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx4(
199
+ "input",
200
+ {
201
+ autoFocus: true,
202
+ type: "url",
203
+ value: url,
204
+ onChange: (e) => setUrl(e.target.value),
205
+ placeholder: "https://example.com",
206
+ className: "rounded border border-base-contrast/20 bg-base px-2 py-1 text-sm text-base-contrast placeholder:text-base-contrast/40 focus:outline-none focus:ring-1 focus:ring-primary"
207
+ }
208
+ ),
209
+ /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-base-contrast", children: [
210
+ /* @__PURE__ */ jsx4(
211
+ "input",
212
+ {
213
+ type: "checkbox",
214
+ checked: openInNewTab,
215
+ onChange: (e) => setOpenInNewTab(e.target.checked),
216
+ className: "accent-primary"
217
+ }
218
+ ),
219
+ "Open in new tab"
220
+ ] })
221
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
222
+ /* @__PURE__ */ jsxs(
223
+ "select",
224
+ {
225
+ autoFocus: true,
226
+ "aria-label": "Page",
227
+ value: pageId,
228
+ onChange: (e) => {
229
+ setPageId(e.target.value);
230
+ setAnchorId("");
231
+ },
232
+ className: selectClasses,
233
+ children: [
234
+ /* @__PURE__ */ jsx4("option", { value: "", children: "Select a page\u2026" }),
235
+ pages.map((p) => /* @__PURE__ */ jsx4("option", { value: p.id, children: p.title }, p.id))
236
+ ]
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsxs(
240
+ "select",
241
+ {
242
+ "aria-label": "Section",
243
+ value: anchorId,
244
+ disabled: !pageId,
245
+ onChange: (e) => setAnchorId(e.target.value),
246
+ className: cn(selectClasses, !pageId && "cursor-not-allowed opacity-50"),
247
+ children: [
248
+ /* @__PURE__ */ jsx4("option", { value: "", children: "None (top of page)" }),
249
+ headings.map((h) => /* @__PURE__ */ jsx4("option", { value: h.id, children: h.label }, h.id))
250
+ ]
251
+ }
252
+ )
253
+ ] }),
254
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
255
+ /* @__PURE__ */ jsx4(
256
+ Button,
257
+ {
258
+ variant: "brand",
259
+ size: "sm",
260
+ onMouseDown: (e) => {
261
+ e.preventDefault();
262
+ handleApply();
263
+ },
264
+ children: "Apply"
265
+ }
266
+ ),
267
+ hasLink && /* @__PURE__ */ jsx4(
268
+ Button,
269
+ {
270
+ variant: "secondary",
271
+ size: "sm",
272
+ onMouseDown: (e) => {
273
+ e.preventDefault();
274
+ handleRemove();
275
+ },
276
+ children: "Remove"
277
+ }
278
+ ),
279
+ /* @__PURE__ */ jsx4(
280
+ Button,
281
+ {
282
+ variant: "secondary",
283
+ size: "sm",
284
+ onMouseDown: (e) => {
285
+ e.preventDefault();
286
+ onClose();
287
+ },
288
+ children: "Cancel"
289
+ }
290
+ )
291
+ ] })
292
+ ]
293
+ }
294
+ );
295
+ }
296
+
297
+ // src/components/primitives/EditableRichText.tsx
298
+ import { useRef as useRef2, useCallback as useCallback2, useEffect as useEffect2 } from "react";
299
+ import { EditorContent } from "@tiptap/react";
300
+ import { BubbleMenu } from "@tiptap/react/menus";
301
+
302
+ // src/components/primitives/useEditableRichText.ts
303
+ import { useState as useState2, useRef, useCallback, useEffect } from "react";
304
+ import { Editor } from "@tiptap/core";
305
+ import Placeholder from "@tiptap/extension-placeholder";
306
+
307
+ // src/components/primitives/tiptap-presets.ts
308
+ import StarterKit from "@tiptap/starter-kit";
309
+ import Underline from "@tiptap/extension-underline";
310
+ import Link from "@tiptap/extension-link";
311
+
312
+ // ../../node_modules/.pnpm/@tiptap+extension-paragraph@3.22.4_@tiptap+core@3.22.4_@tiptap+pm@3.22.4_/node_modules/@tiptap/extension-paragraph/dist/index.js
313
+ import { mergeAttributes, Node } from "@tiptap/core";
314
+ var EMPTY_PARAGRAPH_MARKDOWN = " ";
315
+ var NBSP_CHAR = "\xA0";
316
+ var Paragraph = Node.create({
317
+ name: "paragraph",
318
+ priority: 1e3,
319
+ addOptions() {
320
+ return {
321
+ HTMLAttributes: {}
322
+ };
323
+ },
324
+ group: "block",
325
+ content: "inline*",
326
+ parseHTML() {
327
+ return [{ tag: "p" }];
328
+ },
329
+ renderHTML({ HTMLAttributes }) {
330
+ return ["p", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
331
+ },
332
+ parseMarkdown: (token, helpers) => {
333
+ const tokens = token.tokens || [];
334
+ if (tokens.length === 1 && tokens[0].type === "image") {
335
+ return helpers.parseChildren([tokens[0]]);
336
+ }
337
+ const content = helpers.parseInline(tokens);
338
+ const hasExplicitEmptyParagraphMarker = tokens.length === 1 && tokens[0].type === "text" && (tokens[0].raw === EMPTY_PARAGRAPH_MARKDOWN || tokens[0].text === EMPTY_PARAGRAPH_MARKDOWN || tokens[0].raw === NBSP_CHAR || tokens[0].text === NBSP_CHAR);
339
+ if (hasExplicitEmptyParagraphMarker && content.length === 1 && content[0].type === "text" && (content[0].text === EMPTY_PARAGRAPH_MARKDOWN || content[0].text === NBSP_CHAR)) {
340
+ return helpers.createNode("paragraph", void 0, []);
341
+ }
342
+ return helpers.createNode("paragraph", void 0, content);
343
+ },
344
+ renderMarkdown: (node, h, ctx) => {
345
+ var _a, _b;
346
+ if (!node) {
347
+ return "";
348
+ }
349
+ const content = Array.isArray(node.content) ? node.content : [];
350
+ if (content.length === 0) {
351
+ const previousContent = Array.isArray((_a = ctx == null ? void 0 : ctx.previousNode) == null ? void 0 : _a.content) ? ctx.previousNode.content : [];
352
+ const previousNodeIsEmptyParagraph = ((_b = ctx == null ? void 0 : ctx.previousNode) == null ? void 0 : _b.type) === "paragraph" && previousContent.length === 0;
353
+ return previousNodeIsEmptyParagraph ? EMPTY_PARAGRAPH_MARKDOWN : "";
354
+ }
355
+ return h.renderChildren(content);
356
+ },
357
+ addCommands() {
358
+ return {
359
+ setParagraph: () => ({ commands }) => {
360
+ return commands.setNode(this.name);
361
+ }
362
+ };
363
+ },
364
+ addKeyboardShortcuts() {
365
+ return {
366
+ "Mod-Alt-0": () => this.editor.commands.setParagraph()
367
+ };
368
+ }
369
+ });
370
+ var index_default = Paragraph;
371
+
372
+ // src/components/primitives/CustomParagraph.ts
373
+ var ALLOWED_CLASSES = ["large", "lead-in"];
374
+ var CustomParagraph = index_default.extend({
375
+ addAttributes() {
376
+ return {
377
+ ...this.parent?.(),
378
+ class: {
379
+ default: null,
380
+ parseHTML: (element) => {
381
+ const cls = element.getAttribute("class");
382
+ if (cls && ALLOWED_CLASSES.includes(cls)) {
383
+ return cls;
384
+ }
385
+ return null;
386
+ },
387
+ renderHTML: (attributes) => {
388
+ if (!attributes.class) return {};
389
+ return { class: attributes.class };
390
+ }
391
+ }
392
+ };
393
+ }
394
+ });
395
+
396
+ // src/components/primitives/tiptap-presets.ts
397
+ var basic = [
398
+ StarterKit.configure({
399
+ paragraph: false,
400
+ heading: false,
401
+ blockquote: false,
402
+ bulletList: false,
403
+ orderedList: false,
404
+ listItem: false,
405
+ codeBlock: false,
406
+ horizontalRule: false,
407
+ code: false,
408
+ strike: false
409
+ }),
410
+ CustomParagraph,
411
+ Underline
412
+ ];
413
+ var rich = [
414
+ StarterKit.configure({
415
+ paragraph: false,
416
+ heading: { levels: [3, 4] },
417
+ blockquote: false,
418
+ codeBlock: false,
419
+ horizontalRule: false,
420
+ code: false,
421
+ strike: false
422
+ }),
423
+ CustomParagraph,
424
+ Underline,
425
+ // `page` protocol: stable internal-page hrefs (page://{pageId}#{sectionId})
426
+ // resolved to real routes at SSR — see lib/links.ts.
427
+ Link.configure({ openOnClick: false, protocols: ["page"] })
428
+ ];
429
+ var presets = { basic, rich };
430
+
431
+ // src/components/primitives/useEditableRichText.ts
432
+ function useEditableRichText({
433
+ value,
434
+ onChange,
435
+ preset,
436
+ placeholder
437
+ }) {
438
+ const editorRef = useRef(null);
439
+ const [editor, setEditor] = useState2(null);
440
+ const valueRef = useRef(value);
441
+ const onChangeRef = useRef(onChange);
442
+ useEffect(() => {
443
+ valueRef.current = value;
444
+ }, [value]);
445
+ useEffect(() => {
446
+ onChangeRef.current = onChange;
447
+ }, [onChange]);
448
+ const activate = useCallback(() => {
449
+ if (editorRef.current) return;
450
+ const extensions = placeholder ? [...presets[preset], Placeholder.configure({ placeholder })] : presets[preset];
451
+ const ed = new Editor({
452
+ extensions,
453
+ content: valueRef.current,
454
+ // Commit edits to onChange on every transaction, not only on deactivate().
455
+ // Without this, content lives only inside TipTap until blur; clicking a
456
+ // Save button while the editor is still focused flushes persistence before
457
+ // the (150ms-delayed) blur->deactivate commit fires, silently dropping the
458
+ // in-progress edit. Live-committing keeps persistence in sync continuously.
459
+ onUpdate: ({ editor: updated }) => {
460
+ const html = updated.getHTML();
461
+ if (html !== valueRef.current) {
462
+ valueRef.current = html;
463
+ onChangeRef.current(html);
464
+ }
465
+ }
466
+ });
467
+ editorRef.current = ed;
468
+ setEditor(ed);
469
+ }, [preset, placeholder]);
470
+ const deactivate = useCallback(() => {
471
+ const ed = editorRef.current;
472
+ if (!ed) return;
473
+ const html = ed.getHTML();
474
+ ed.destroy();
475
+ editorRef.current = null;
476
+ setEditor(null);
477
+ if (html !== valueRef.current) {
478
+ onChangeRef.current(html);
479
+ }
480
+ }, []);
481
+ useEffect(() => {
482
+ return () => {
483
+ editorRef.current?.destroy();
484
+ editorRef.current = null;
485
+ };
486
+ }, []);
487
+ return {
488
+ isEditorActive: editor !== null,
489
+ editor,
490
+ activate,
491
+ deactivate
492
+ };
493
+ }
494
+
495
+ // src/components/primitives/RichTextToolbar.tsx
496
+ import { useState as useState3 } from "react";
497
+ import { useEditorState } from "@tiptap/react";
498
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
499
+ function ToolbarButton({ isActive, onClick, title, children }) {
500
+ return /* @__PURE__ */ jsx5(
501
+ "button",
502
+ {
503
+ type: "button",
504
+ title,
505
+ "aria-label": title,
506
+ "aria-pressed": isActive,
507
+ onMouseDown: (e) => {
508
+ e.preventDefault();
509
+ onClick();
510
+ },
511
+ className: cn(
512
+ "cursor-pointer rounded px-2 py-1 text-sm font-medium transition-colors",
513
+ isActive ? "bg-primary text-primary-contrast" : "bg-base-accent text-base-contrast hover:opacity-80"
514
+ ),
515
+ children
516
+ }
517
+ );
518
+ }
519
+ function Separator() {
520
+ return /* @__PURE__ */ jsx5("div", { className: "mx-1 h-5 w-px bg-base-contrast/20" });
521
+ }
522
+ function RichTextToolbar({ editor, preset }) {
523
+ const [showLinkPopover, setShowLinkPopover] = useState3(false);
524
+ const state = useEditorState({
525
+ editor,
526
+ selector: ({ editor: ed }) => ({
527
+ isBold: ed.isActive("bold"),
528
+ isItalic: ed.isActive("italic"),
529
+ isUnderline: ed.isActive("underline"),
530
+ isLink: ed.isActive("link"),
531
+ isBulletList: ed.isActive("bulletList"),
532
+ isOrderedList: ed.isActive("orderedList"),
533
+ isH3: ed.isActive("heading", { level: 3 }),
534
+ isH4: ed.isActive("heading", { level: 4 }),
535
+ isLarge: ed.isActive("paragraph", { class: "large" }),
536
+ isLeadIn: ed.isActive("paragraph", { class: "lead-in" })
537
+ })
538
+ });
539
+ const toggleParagraphVariant = (cls, isCurrentlyActive) => {
540
+ if (isCurrentlyActive) {
541
+ editor.chain().focus().setNode("paragraph").run();
542
+ } else {
543
+ editor.chain().focus().setNode("paragraph", { class: cls }).run();
544
+ }
545
+ };
546
+ return /* @__PURE__ */ jsxs2(
547
+ "div",
548
+ {
549
+ "data-testid": "rich-text-toolbar",
550
+ className: "flex items-center gap-1 rounded border border-base-contrast/10 bg-base-accent p-1 shadow-md",
551
+ children: [
552
+ /* @__PURE__ */ jsx5(
553
+ ToolbarButton,
554
+ {
555
+ isActive: state.isBold,
556
+ onClick: () => editor.chain().focus().toggleBold().run(),
557
+ title: "Bold",
558
+ children: /* @__PURE__ */ jsx5("strong", { children: "B" })
559
+ }
560
+ ),
561
+ /* @__PURE__ */ jsx5(
562
+ ToolbarButton,
563
+ {
564
+ isActive: state.isItalic,
565
+ onClick: () => editor.chain().focus().toggleItalic().run(),
566
+ title: "Italic",
567
+ children: /* @__PURE__ */ jsx5("em", { children: "I" })
568
+ }
569
+ ),
570
+ /* @__PURE__ */ jsx5(
571
+ ToolbarButton,
572
+ {
573
+ isActive: state.isUnderline,
574
+ onClick: () => editor.chain().focus().toggleUnderline().run(),
575
+ title: "Underline",
576
+ children: /* @__PURE__ */ jsx5("span", { className: "underline", children: "U" })
577
+ }
578
+ ),
579
+ preset === "rich" && /* @__PURE__ */ jsxs2(Fragment2, { children: [
580
+ /* @__PURE__ */ jsx5(Separator, {}),
581
+ /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
582
+ /* @__PURE__ */ jsx5(
583
+ ToolbarButton,
584
+ {
585
+ isActive: state.isLink || showLinkPopover,
586
+ onClick: () => setShowLinkPopover((prev) => !prev),
587
+ title: "Link",
588
+ children: "\u{1F517}"
589
+ }
590
+ ),
591
+ showLinkPopover && /* @__PURE__ */ jsx5("div", { className: "absolute left-0 top-full z-50 mt-1", children: /* @__PURE__ */ jsx5(
592
+ LinkPopover,
593
+ {
594
+ editor,
595
+ onClose: () => setShowLinkPopover(false)
596
+ }
597
+ ) })
598
+ ] }),
599
+ /* @__PURE__ */ jsx5(Separator, {}),
600
+ /* @__PURE__ */ jsx5(
601
+ ToolbarButton,
602
+ {
603
+ isActive: state.isBulletList,
604
+ onClick: () => editor.chain().focus().toggleBulletList().run(),
605
+ title: "Bullet List",
606
+ children: "\u2022\u2261"
607
+ }
608
+ ),
609
+ /* @__PURE__ */ jsx5(
610
+ ToolbarButton,
611
+ {
612
+ isActive: state.isOrderedList,
613
+ onClick: () => editor.chain().focus().toggleOrderedList().run(),
614
+ title: "Ordered List",
615
+ children: "1\u2261"
616
+ }
617
+ ),
618
+ /* @__PURE__ */ jsx5(Separator, {}),
619
+ /* @__PURE__ */ jsx5(
620
+ ToolbarButton,
621
+ {
622
+ isActive: state.isH3,
623
+ onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
624
+ title: "Heading 3",
625
+ children: "H3"
626
+ }
627
+ ),
628
+ /* @__PURE__ */ jsx5(
629
+ ToolbarButton,
630
+ {
631
+ isActive: state.isH4,
632
+ onClick: () => editor.chain().focus().toggleHeading({ level: 4 }).run(),
633
+ title: "Heading 4",
634
+ children: "H4"
635
+ }
636
+ ),
637
+ /* @__PURE__ */ jsx5(
638
+ ToolbarButton,
639
+ {
640
+ isActive: state.isLarge,
641
+ onClick: () => toggleParagraphVariant("large", state.isLarge),
642
+ title: "Large Paragraph",
643
+ children: "Lg"
644
+ }
645
+ ),
646
+ /* @__PURE__ */ jsx5(
647
+ ToolbarButton,
648
+ {
649
+ isActive: state.isLeadIn,
650
+ onClick: () => toggleParagraphVariant("lead-in", state.isLeadIn),
651
+ title: "Lead-In",
652
+ children: "Li"
653
+ }
654
+ )
655
+ ] })
656
+ ]
657
+ }
658
+ );
659
+ }
660
+
661
+ // src/components/primitives/EditableRichText.tsx
662
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
663
+ function isContentEmpty(html) {
664
+ return html.replace(/<[^>]*>/g, "").trim().length === 0;
665
+ }
666
+ var DEFAULT_PLACEHOLDER = "Click to edit\u2026";
667
+ function EditableRichText({
668
+ value,
669
+ onChange,
670
+ isEditMode,
671
+ preset = "rich",
672
+ className,
673
+ placeholder = DEFAULT_PLACEHOLDER
674
+ }) {
675
+ const { isEditorActive, editor, activate, deactivate } = useEditableRichText({ value, onChange, preset, placeholder });
676
+ const wrapperRef = useRef2(null);
677
+ const blurTimeoutRef = useRef2(null);
678
+ const clickCoordsRef = useRef2(null);
679
+ useEffect2(() => {
680
+ return () => {
681
+ if (blurTimeoutRef.current) {
682
+ clearTimeout(blurTimeoutRef.current);
683
+ }
684
+ };
685
+ }, []);
686
+ useEffect2(() => {
687
+ if (!isEditorActive || !editor) return;
688
+ requestAnimationFrame(() => {
689
+ if (editor.isDestroyed) return;
690
+ const coords = clickCoordsRef.current;
691
+ if (coords) {
692
+ try {
693
+ const pos = editor.view.posAtCoords(coords);
694
+ if (pos) {
695
+ editor.commands.focus();
696
+ editor.commands.setTextSelection(pos.pos);
697
+ } else {
698
+ editor.commands.focus("end");
699
+ }
700
+ } catch {
701
+ editor.commands.focus("end");
702
+ } finally {
703
+ clickCoordsRef.current = null;
704
+ }
705
+ } else {
706
+ editor.commands.focus("end");
707
+ }
708
+ });
709
+ }, [isEditorActive, editor]);
710
+ const handleClick = useCallback2((e) => {
711
+ if (isEditMode && !isEditorActive) {
712
+ clickCoordsRef.current = { left: e.clientX, top: e.clientY };
713
+ activate();
714
+ }
715
+ }, [isEditMode, isEditorActive, activate]);
716
+ const handleFocus = useCallback2(() => {
717
+ if (blurTimeoutRef.current) {
718
+ clearTimeout(blurTimeoutRef.current);
719
+ blurTimeoutRef.current = null;
720
+ }
721
+ }, []);
722
+ const handleBlur = useCallback2(() => {
723
+ blurTimeoutRef.current = setTimeout(() => {
724
+ if (wrapperRef.current && !wrapperRef.current.contains(document.activeElement)) {
725
+ deactivate();
726
+ }
727
+ }, 150);
728
+ }, [deactivate]);
729
+ if (!isEditMode) {
730
+ return /* @__PURE__ */ jsx6(
731
+ "div",
732
+ {
733
+ className,
734
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(value) }
735
+ }
736
+ );
737
+ }
738
+ if (!isEditorActive || !editor) {
739
+ const empty = isContentEmpty(value);
740
+ if (empty) {
741
+ return /* @__PURE__ */ jsx6("div", { className, onClick: handleClick, children: /* @__PURE__ */ jsx6("p", { className: "tiptap-placeholder", children: placeholder }) });
742
+ }
743
+ return /* @__PURE__ */ jsx6(
744
+ "div",
745
+ {
746
+ className,
747
+ onClick: handleClick,
748
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(value) }
749
+ }
750
+ );
751
+ }
752
+ return /* @__PURE__ */ jsxs3(
753
+ "div",
754
+ {
755
+ ref: wrapperRef,
756
+ className,
757
+ onFocus: handleFocus,
758
+ onBlur: handleBlur,
759
+ children: [
760
+ /* @__PURE__ */ jsx6(
761
+ BubbleMenu,
762
+ {
763
+ editor,
764
+ className: "z-50",
765
+ shouldShow: ({ editor: ed, state }) => {
766
+ const { from, to } = state.selection;
767
+ return ed.isFocused && from !== to;
768
+ },
769
+ children: /* @__PURE__ */ jsx6(RichTextToolbar, { editor, preset })
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsx6(EditorContent, { editor })
773
+ ]
774
+ }
775
+ );
776
+ }
777
+
778
+ export {
779
+ defineHeadingSection,
780
+ LinkPopover,
781
+ EditableRichText
782
+ };