@techrox/page-studio-form 1.0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,2723 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.js
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ Callout: () => Callout,
33
+ CiqImage: () => CiqImage,
34
+ CiqTableCell: () => CiqTableCell,
35
+ CiqTableHeader: () => CiqTableHeader,
36
+ Column: () => Column,
37
+ Columns: () => Columns,
38
+ HistoryPanel: () => HistoryPanel,
39
+ PageStudioForm: () => PageStudioForm,
40
+ RichText: () => RichText,
41
+ ShareBlock: () => ShareBlock,
42
+ SubscribeBlock: () => SubscribeBlock
43
+ });
44
+ module.exports = __toCommonJS(index_exports);
45
+
46
+ // src/PageStudioForm.jsx
47
+ var import_react4 = require("react");
48
+ var import_antd3 = require("antd");
49
+ var import_icons3 = require("@ant-design/icons");
50
+ var import_core2 = require("@dnd-kit/core");
51
+ var import_sortable = require("@dnd-kit/sortable");
52
+ var import_utilities = require("@dnd-kit/utilities");
53
+
54
+ // src/RichText.jsx
55
+ var import_react = require("react");
56
+ var import_react2 = require("@tiptap/react");
57
+ var import_starter_kit = __toESM(require("@tiptap/starter-kit"), 1);
58
+ var import_extension_underline = __toESM(require("@tiptap/extension-underline"), 1);
59
+ var import_extension_link = __toESM(require("@tiptap/extension-link"), 1);
60
+ var import_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"), 1);
61
+ var import_extension_text_align = __toESM(require("@tiptap/extension-text-align"), 1);
62
+ var import_extension_text_style = __toESM(require("@tiptap/extension-text-style"), 1);
63
+ var import_extension_color = require("@tiptap/extension-color");
64
+ var import_extension_highlight = __toESM(require("@tiptap/extension-highlight"), 1);
65
+ var import_extension_table = __toESM(require("@tiptap/extension-table"), 1);
66
+ var import_extension_table_row = __toESM(require("@tiptap/extension-table-row"), 1);
67
+ var import_extension_task_list = __toESM(require("@tiptap/extension-task-list"), 1);
68
+ var import_extension_task_item = __toESM(require("@tiptap/extension-task-item"), 1);
69
+ var import_extension_character_count = __toESM(require("@tiptap/extension-character-count"), 1);
70
+ var import_extension_typography = __toESM(require("@tiptap/extension-typography"), 1);
71
+ var import_antd = require("antd");
72
+ var import_icons = require("@ant-design/icons");
73
+
74
+ // src/CustomBlocks.js
75
+ var import_core = require("@tiptap/core");
76
+ var import_extension_image = __toESM(require("@tiptap/extension-image"), 1);
77
+ var import_extension_table_cell = __toESM(require("@tiptap/extension-table-cell"), 1);
78
+ var import_extension_table_header = __toESM(require("@tiptap/extension-table-header"), 1);
79
+ var vAlignAttrs = {
80
+ verticalAlign: {
81
+ default: "top",
82
+ parseHTML: (el) => el.style?.verticalAlign || el.getAttribute("data-valign") || "top",
83
+ renderHTML: (attrs) => ({
84
+ "data-valign": attrs.verticalAlign,
85
+ style: `vertical-align: ${attrs.verticalAlign};`
86
+ })
87
+ }
88
+ };
89
+ var CiqTableCell = import_extension_table_cell.default.extend({
90
+ addAttributes() {
91
+ return { ...this.parent?.(), ...vAlignAttrs };
92
+ }
93
+ });
94
+ var CiqTableHeader = import_extension_table_header.default.extend({
95
+ addAttributes() {
96
+ return { ...this.parent?.(), ...vAlignAttrs };
97
+ }
98
+ });
99
+ var CiqImage = import_extension_image.default.extend({
100
+ name: "ciqImage",
101
+ // Inline images don't get alignment — we use block images so we can wrap
102
+ // them in a container div and apply text-align.
103
+ inline: false,
104
+ group: "block",
105
+ draggable: true,
106
+ addAttributes() {
107
+ return {
108
+ ...this.parent?.(),
109
+ align: {
110
+ default: "center",
111
+ parseHTML: (el) => el.getAttribute("data-align") || "center",
112
+ renderHTML: (attrs) => ({ "data-align": attrs.align })
113
+ },
114
+ width: {
115
+ default: "medium",
116
+ // 'small' | 'medium' | 'large' | 'full'
117
+ parseHTML: (el) => el.getAttribute("data-width") || "medium",
118
+ renderHTML: (attrs) => ({ "data-width": attrs.width })
119
+ }
120
+ };
121
+ },
122
+ renderHTML({ HTMLAttributes }) {
123
+ const { align, width, ...rest } = HTMLAttributes;
124
+ return [
125
+ "div",
126
+ {
127
+ class: `tps-block-image tps-img-${width || "medium"}`,
128
+ "data-align": align || "center"
129
+ },
130
+ ["img", (0, import_core.mergeAttributes)(this.options.HTMLAttributes, rest)]
131
+ ];
132
+ },
133
+ parseHTML() {
134
+ return [
135
+ {
136
+ tag: "div.tps-block-image > img",
137
+ getAttrs: (img) => {
138
+ const wrapper = img.closest(".tps-block-image");
139
+ return {
140
+ src: img.getAttribute("src"),
141
+ alt: img.getAttribute("alt"),
142
+ title: img.getAttribute("title"),
143
+ align: wrapper?.getAttribute("data-align") || "center",
144
+ width: wrapper?.className.match(/tps-img-(small|medium|large|full)/)?.[1] || "medium"
145
+ };
146
+ }
147
+ },
148
+ // Fallback: legacy plain <img> tags
149
+ { tag: 'img[src]:not([src^="data:"])' }
150
+ ];
151
+ }
152
+ });
153
+ var Column = import_core.Node.create({
154
+ name: "column",
155
+ group: "column",
156
+ // Allow any block content inside — paragraphs, headings, lists, images.
157
+ content: "block+",
158
+ isolating: true,
159
+ defining: true,
160
+ parseHTML() {
161
+ return [{ tag: "div.tps-block-column" }];
162
+ },
163
+ renderHTML({ HTMLAttributes }) {
164
+ return [
165
+ "div",
166
+ (0, import_core.mergeAttributes)(HTMLAttributes, { class: "tps-block-column" }),
167
+ 0
168
+ ];
169
+ }
170
+ });
171
+ var Columns = import_core.Node.create({
172
+ name: "columns",
173
+ group: "block",
174
+ content: "column{2,4}",
175
+ // 2–4 columns
176
+ defining: true,
177
+ addAttributes() {
178
+ return {
179
+ cols: {
180
+ default: 2,
181
+ parseHTML: (el) => Number(el.getAttribute("data-cols")) || 2,
182
+ renderHTML: (attrs) => ({ "data-cols": attrs.cols })
183
+ }
184
+ };
185
+ },
186
+ parseHTML() {
187
+ return [{ tag: "div.tps-block-columns" }];
188
+ },
189
+ renderHTML({ HTMLAttributes }) {
190
+ const cols = HTMLAttributes["data-cols"] || HTMLAttributes.cols || 2;
191
+ return [
192
+ "div",
193
+ (0, import_core.mergeAttributes)(HTMLAttributes, {
194
+ class: `tps-block-columns tps-cols-${cols}`
195
+ }),
196
+ 0
197
+ ];
198
+ },
199
+ addCommands() {
200
+ return {
201
+ insertColumns: (cols = 2) => ({ commands }) => {
202
+ const n = Math.max(2, Math.min(4, Number(cols) || 2));
203
+ return commands.insertContent({
204
+ type: this.name,
205
+ attrs: { cols: n },
206
+ content: Array.from({ length: n }, () => ({
207
+ type: "column",
208
+ content: [
209
+ { type: "paragraph" }
210
+ ]
211
+ }))
212
+ });
213
+ }
214
+ };
215
+ }
216
+ });
217
+ var Callout = import_core.Node.create({
218
+ name: "callout",
219
+ group: "block",
220
+ content: "block+",
221
+ defining: true,
222
+ addAttributes() {
223
+ return {
224
+ tone: {
225
+ default: "info",
226
+ // 'info' | 'success' | 'warning' | 'danger'
227
+ parseHTML: (el) => el.getAttribute("data-tone") || "info",
228
+ renderHTML: (attrs) => ({ "data-tone": attrs.tone })
229
+ }
230
+ };
231
+ },
232
+ parseHTML() {
233
+ return [{ tag: "div.tps-block-callout" }];
234
+ },
235
+ renderHTML({ HTMLAttributes }) {
236
+ const tone = HTMLAttributes["data-tone"] || "info";
237
+ return [
238
+ "div",
239
+ (0, import_core.mergeAttributes)(HTMLAttributes, {
240
+ class: `tps-block-callout tps-callout-${tone}`
241
+ }),
242
+ 0
243
+ ];
244
+ },
245
+ addCommands() {
246
+ return {
247
+ insertCallout: (tone = "info") => ({ commands }) => {
248
+ return commands.insertContent({
249
+ type: this.name,
250
+ attrs: { tone },
251
+ content: [
252
+ {
253
+ type: "paragraph",
254
+ content: [{ type: "text", text: "Highlight something important here." }]
255
+ }
256
+ ]
257
+ });
258
+ }
259
+ };
260
+ }
261
+ });
262
+ var ShareBlock = import_core.Node.create({
263
+ name: "shareBlock",
264
+ group: "block",
265
+ atom: true,
266
+ selectable: true,
267
+ draggable: true,
268
+ addAttributes() {
269
+ return {
270
+ label: {
271
+ default: "Share this page",
272
+ parseHTML: (el) => el.getAttribute("data-label") || "Share this page",
273
+ renderHTML: (attrs) => ({ "data-label": attrs.label })
274
+ }
275
+ };
276
+ },
277
+ parseHTML() {
278
+ return [{ tag: "div.tps-block-share" }];
279
+ },
280
+ renderHTML({ HTMLAttributes }) {
281
+ const label = HTMLAttributes["data-label"] || "Share this page";
282
+ return [
283
+ "div",
284
+ (0, import_core.mergeAttributes)(HTMLAttributes, { class: "tps-block-share" }),
285
+ ["div", { class: "tps-block-share-label" }, label],
286
+ [
287
+ "div",
288
+ { class: "tps-block-share-row" },
289
+ [
290
+ "a",
291
+ {
292
+ class: "tps-block-share-btn",
293
+ href: "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fpagestudio.dev",
294
+ target: "_blank",
295
+ rel: "noopener noreferrer"
296
+ },
297
+ "LinkedIn"
298
+ ],
299
+ [
300
+ "a",
301
+ {
302
+ class: "tps-block-share-btn",
303
+ href: "mailto:?subject=Page%20Studio&body=Thought%20you%27d%20find%20this%20interesting%3A%20https%3A%2F%2Fpagestudio.dev"
304
+ },
305
+ "Email"
306
+ ],
307
+ [
308
+ "a",
309
+ {
310
+ class: "tps-block-share-btn",
311
+ href: "https://twitter.com/intent/tweet?url=https%3A%2F%2Fpagestudio.dev",
312
+ target: "_blank",
313
+ rel: "noopener noreferrer"
314
+ },
315
+ "X / Twitter"
316
+ ]
317
+ ]
318
+ ];
319
+ },
320
+ addCommands() {
321
+ return {
322
+ insertShareBlock: () => ({ commands }) => commands.insertContent({ type: this.name })
323
+ };
324
+ }
325
+ });
326
+ var SubscribeBlock = import_core.Node.create({
327
+ name: "subscribeBlock",
328
+ group: "block",
329
+ atom: true,
330
+ selectable: true,
331
+ draggable: true,
332
+ addAttributes() {
333
+ return {
334
+ heading: {
335
+ default: "Stay in the loop",
336
+ parseHTML: (el) => el.getAttribute("data-heading") || "Stay in the loop",
337
+ renderHTML: (attrs) => ({ "data-heading": attrs.heading })
338
+ },
339
+ body: {
340
+ default: "One short email when we publish \u2014 never more.",
341
+ parseHTML: (el) => el.getAttribute("data-body") || "",
342
+ renderHTML: (attrs) => ({ "data-body": attrs.body })
343
+ },
344
+ button: {
345
+ default: "Subscribe",
346
+ parseHTML: (el) => el.getAttribute("data-button") || "Subscribe",
347
+ renderHTML: (attrs) => ({ "data-button": attrs.button })
348
+ }
349
+ };
350
+ },
351
+ parseHTML() {
352
+ return [{ tag: "div.tps-block-subscribe" }];
353
+ },
354
+ renderHTML({ HTMLAttributes }) {
355
+ const heading = HTMLAttributes["data-heading"] || "Stay in the loop";
356
+ const body = HTMLAttributes["data-body"] || "";
357
+ const button = HTMLAttributes["data-button"] || "Subscribe";
358
+ return [
359
+ "div",
360
+ (0, import_core.mergeAttributes)(HTMLAttributes, { class: "tps-block-subscribe" }),
361
+ ["h3", { class: "tps-block-subscribe-heading" }, heading],
362
+ body ? ["p", { class: "tps-block-subscribe-body" }, body] : "",
363
+ [
364
+ "form",
365
+ { class: "tps-block-subscribe-form", action: "/api/leads", method: "post" },
366
+ ["input", { type: "hidden", name: "source", value: "newsletter" }],
367
+ [
368
+ "input",
369
+ {
370
+ type: "email",
371
+ name: "email",
372
+ required: "true",
373
+ placeholder: "you@company.com",
374
+ class: "tps-block-subscribe-input"
375
+ }
376
+ ],
377
+ ["button", { type: "submit", class: "tps-block-subscribe-button" }, button]
378
+ ]
379
+ ];
380
+ },
381
+ addCommands() {
382
+ return {
383
+ insertSubscribeBlock: () => ({ commands }) => commands.insertContent({ type: this.name })
384
+ };
385
+ }
386
+ });
387
+
388
+ // src/RichText.jsx
389
+ var import_jsx_runtime = require("react/jsx-runtime");
390
+ function defaultUploadMedia() {
391
+ throw new Error(
392
+ "@techrox/page-studio-form: RichText was used without an `uploadMedia` prop. Pass a function `(FormData) => Promise<{ url }>` to enable image uploads."
393
+ );
394
+ }
395
+ var { Text } = import_antd.Typography;
396
+ var HEADING_OPTIONS = [
397
+ { key: "p", label: "Paragraph" },
398
+ { key: "h1", label: "Heading 1" },
399
+ { key: "h2", label: "Heading 2" },
400
+ { key: "h3", label: "Heading 3" },
401
+ { key: "h4", label: "Heading 4" }
402
+ ];
403
+ var TEXT_COLORS = [
404
+ "#0F172A",
405
+ "#475569",
406
+ "#0F766E",
407
+ "#0B5550",
408
+ "#F59E0B",
409
+ "#D97706",
410
+ "#DC2626",
411
+ "#2563EB",
412
+ "#7C3AED",
413
+ "#059669"
414
+ ];
415
+ var HIGHLIGHT_COLORS = [
416
+ "#FEF08A",
417
+ "#FECACA",
418
+ "#FED7AA",
419
+ "#A7F3D0",
420
+ "#BFDBFE",
421
+ "#E9D5FF",
422
+ "#FBCFE8",
423
+ "#FDE68A"
424
+ ];
425
+ var IMAGE_WIDTHS = [
426
+ { value: "small", label: "Small (40%)" },
427
+ { value: "medium", label: "Medium (60%)" },
428
+ { value: "large", label: "Large (80%)" },
429
+ { value: "full", label: "Full width" }
430
+ ];
431
+ function RichText({
432
+ value = "",
433
+ onChange,
434
+ placeholder = "Type '/' for blocks, or just start writing\u2026",
435
+ minHeight = 280,
436
+ uploadMedia = defaultUploadMedia
437
+ }) {
438
+ const [imageOpen, setImageOpen] = (0, import_react.useState)(false);
439
+ const [linkOpen, setLinkOpen] = (0, import_react.useState)(false);
440
+ const [editBlock, setEditBlock] = (0, import_react.useState)(null);
441
+ const editor = (0, import_react2.useEditor)({
442
+ extensions: (0, import_react.useMemo)(
443
+ () => [
444
+ import_starter_kit.default.configure({
445
+ heading: { levels: [1, 2, 3, 4] },
446
+ codeBlock: { HTMLAttributes: { class: "tiptap-code" } }
447
+ }),
448
+ import_extension_underline.default,
449
+ import_extension_link.default.configure({
450
+ openOnClick: false,
451
+ autolink: true,
452
+ HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" }
453
+ }),
454
+ CiqImage.configure({ inline: false, allowBase64: false }),
455
+ import_extension_placeholder.default.configure({
456
+ placeholder: ({ node, editor: editor2 }) => {
457
+ if (node.type.name === "paragraph" && editor2.isEmpty) return placeholder;
458
+ return "";
459
+ },
460
+ showOnlyWhenEditable: true
461
+ }),
462
+ import_extension_text_align.default.configure({ types: ["heading", "paragraph"] }),
463
+ import_extension_text_style.default,
464
+ import_extension_color.Color,
465
+ import_extension_highlight.default.configure({ multicolor: true }),
466
+ import_extension_table.default.configure({ resizable: true, HTMLAttributes: { class: "tps-rich-table" } }),
467
+ import_extension_table_row.default,
468
+ CiqTableHeader,
469
+ CiqTableCell,
470
+ import_extension_task_list.default,
471
+ import_extension_task_item.default.configure({ nested: true }),
472
+ import_extension_character_count.default,
473
+ import_extension_typography.default,
474
+ Columns,
475
+ Column,
476
+ Callout,
477
+ ShareBlock,
478
+ SubscribeBlock
479
+ ],
480
+ [placeholder]
481
+ ),
482
+ content: value || "",
483
+ onUpdate: ({ editor: editor2 }) => onChange?.(editor2.getHTML()),
484
+ immediatelyRender: false,
485
+ editorProps: {
486
+ attributes: { class: "tiptap", spellcheck: "true" },
487
+ handleDrop: (view, event) => {
488
+ const files = Array.from(event.dataTransfer?.files || []).filter(
489
+ (f) => f.type.startsWith("image/")
490
+ );
491
+ if (!files.length) return false;
492
+ event.preventDefault();
493
+ files.forEach((f) => uploadAndInsert(f, view, event, uploadMedia));
494
+ return true;
495
+ },
496
+ handlePaste: (view, event) => {
497
+ const files = Array.from(event.clipboardData?.files || []).filter(
498
+ (f) => f.type.startsWith("image/")
499
+ );
500
+ if (!files.length) return false;
501
+ event.preventDefault();
502
+ files.forEach((f) => uploadAndInsert(f, view, null, uploadMedia));
503
+ return true;
504
+ }
505
+ }
506
+ });
507
+ (0, import_react.useEffect)(() => {
508
+ if (!editor) return;
509
+ const current = editor.getHTML();
510
+ const next = value || "";
511
+ if (current === next) return;
512
+ if (current === "<p></p>" && next === "") return;
513
+ editor.commands.setContent(next, false);
514
+ }, [editor, value]);
515
+ if (!editor) {
516
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
517
+ "div",
518
+ {
519
+ style: {
520
+ border: "1px solid var(--tps-line)",
521
+ borderRadius: "var(--tps-radius)",
522
+ minHeight,
523
+ background: "#fff"
524
+ }
525
+ }
526
+ );
527
+ }
528
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
529
+ "div",
530
+ {
531
+ style: {
532
+ border: "1px solid var(--tps-line)",
533
+ borderRadius: "var(--tps-radius)",
534
+ background: "#fff",
535
+ overflow: "hidden"
536
+ },
537
+ children: [
538
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
539
+ Toolbar,
540
+ {
541
+ editor,
542
+ onOpenLink: () => setLinkOpen(true),
543
+ onOpenImage: () => setImageOpen(true)
544
+ }
545
+ ),
546
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
547
+ import_react2.BubbleMenu,
548
+ {
549
+ editor,
550
+ shouldShow: ({ editor: editor2, from, to }) => {
551
+ if (from === to) return false;
552
+ if (editor2.isActive("ciqImage") || editor2.isActive("table") || editor2.isActive("codeBlock") || editor2.isActive("shareBlock") || editor2.isActive("subscribeBlock")) return false;
553
+ return true;
554
+ },
555
+ tippyOptions: { duration: 100, placement: "top" },
556
+ pluginKey: "selectionBubble",
557
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectionFormatBar, { editor, onLink: () => setLinkOpen(true) })
558
+ }
559
+ ),
560
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
561
+ import_react2.BubbleMenu,
562
+ {
563
+ editor,
564
+ shouldShow: ({ editor: editor2 }) => editor2.isActive("ciqImage"),
565
+ tippyOptions: { duration: 100, placement: "top" },
566
+ pluginKey: "imageBubble",
567
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ImageBubble, { editor })
568
+ }
569
+ ),
570
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
571
+ import_react2.BubbleMenu,
572
+ {
573
+ editor,
574
+ shouldShow: ({ editor: editor2 }) => editor2.isActive("table"),
575
+ tippyOptions: { duration: 100, placement: "top" },
576
+ pluginKey: "tableBubble",
577
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableBubble, { editor })
578
+ }
579
+ ),
580
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
581
+ import_react2.BubbleMenu,
582
+ {
583
+ editor,
584
+ shouldShow: ({ editor: editor2 }) => editor2.isActive("shareBlock") || editor2.isActive("subscribeBlock"),
585
+ tippyOptions: { duration: 100, placement: "top" },
586
+ pluginKey: "atomBlockBubble",
587
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
588
+ AtomBlockBubble,
589
+ {
590
+ editor,
591
+ onEdit: (kind) => setEditBlock(kind)
592
+ }
593
+ )
594
+ }
595
+ ),
596
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
597
+ import_react2.FloatingMenu,
598
+ {
599
+ editor,
600
+ shouldShow: ({ editor: editor2, state }) => {
601
+ const { $from } = state.selection;
602
+ const isEmptyParagraph = $from.parent.type.name === "paragraph" && $from.parent.content.size === 0;
603
+ return editor2.isFocused && isEmptyParagraph && !editor2.isActive("table");
604
+ },
605
+ tippyOptions: { duration: 100, placement: "left-start", offset: [0, 8] },
606
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BlockInsertMenu, { editor, onImage: () => setImageOpen(true) })
607
+ }
608
+ ),
609
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: 16, minHeight }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.EditorContent, { editor }) }),
610
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
611
+ "div",
612
+ {
613
+ style: {
614
+ padding: "6px 14px",
615
+ borderTop: "1px solid var(--tps-line)",
616
+ background: "var(--tps-bg-soft)",
617
+ fontSize: 12,
618
+ color: "var(--tps-muted)",
619
+ display: "flex",
620
+ justifyContent: "space-between"
621
+ },
622
+ children: [
623
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
624
+ editor.storage.characterCount.words(),
625
+ " words \xB7",
626
+ " ",
627
+ editor.storage.characterCount.characters(),
628
+ " characters"
629
+ ] }),
630
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "HTML output \xB7 drop or paste images to upload" })
631
+ ]
632
+ }
633
+ ),
634
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkModal, { open: linkOpen, onClose: () => setLinkOpen(false), editor }),
635
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
636
+ ImageModal,
637
+ {
638
+ open: imageOpen,
639
+ onClose: () => setImageOpen(false),
640
+ editor,
641
+ uploadMedia
642
+ }
643
+ ),
644
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
645
+ BlockAttributeModal,
646
+ {
647
+ kind: editBlock,
648
+ open: !!editBlock,
649
+ onClose: () => setEditBlock(null),
650
+ editor
651
+ }
652
+ ),
653
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EditorStyles, {})
654
+ ]
655
+ }
656
+ );
657
+ }
658
+ async function uploadAndInsert(file, view, dropEvent, uploadMedia) {
659
+ try {
660
+ const fd = new FormData();
661
+ fd.append("file", file);
662
+ const { url } = await uploadMedia(fd);
663
+ if (!url) throw new Error("No URL returned");
664
+ const node = view.state.schema.nodes.ciqImage.create({
665
+ src: url,
666
+ alt: file.name,
667
+ align: "center",
668
+ width: "medium"
669
+ });
670
+ let pos;
671
+ if (dropEvent) {
672
+ const coords = view.posAtCoords({ left: dropEvent.clientX, top: dropEvent.clientY });
673
+ pos = coords?.pos ?? view.state.selection.from;
674
+ } else {
675
+ pos = view.state.selection.from;
676
+ }
677
+ const tr = view.state.tr.insert(pos, node);
678
+ view.dispatch(tr);
679
+ } catch (err) {
680
+ import_antd.message.error(err.message || "Upload failed");
681
+ }
682
+ }
683
+ function Toolbar({ editor, onOpenLink, onOpenImage }) {
684
+ const headingValue = (() => {
685
+ if (editor.isActive("heading", { level: 1 })) return "h1";
686
+ if (editor.isActive("heading", { level: 2 })) return "h2";
687
+ if (editor.isActive("heading", { level: 3 })) return "h3";
688
+ if (editor.isActive("heading", { level: 4 })) return "h4";
689
+ return "p";
690
+ })();
691
+ const setHeading = (key) => {
692
+ if (key === "p") editor.chain().focus().setParagraph().run();
693
+ else editor.chain().focus().toggleHeading({ level: Number(key.slice(1)) }).run();
694
+ };
695
+ const insertBlockItems = [
696
+ { key: "image", label: "Image", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.PictureOutlined, {}), run: onOpenImage },
697
+ {
698
+ key: "table",
699
+ label: "Table (3\xD73)",
700
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.TableOutlined, {}),
701
+ run: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
702
+ },
703
+ {
704
+ key: "cols2",
705
+ label: "2 columns",
706
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AppstoreOutlined, {}),
707
+ run: () => editor.chain().focus().insertColumns(2).run()
708
+ },
709
+ {
710
+ key: "cols3",
711
+ label: "3 columns",
712
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AppstoreOutlined, {}),
713
+ run: () => editor.chain().focus().insertColumns(3).run()
714
+ },
715
+ {
716
+ key: "cols4",
717
+ label: "4 columns",
718
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AppstoreOutlined, {}),
719
+ run: () => editor.chain().focus().insertColumns(4).run()
720
+ },
721
+ {
722
+ key: "callout-info",
723
+ label: "Callout (info)",
724
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ExclamationCircleOutlined, {}),
725
+ run: () => editor.chain().focus().insertCallout("info").run()
726
+ },
727
+ {
728
+ key: "callout-success",
729
+ label: "Callout (success)",
730
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ExclamationCircleOutlined, { style: { color: "#059669" } }),
731
+ run: () => editor.chain().focus().insertCallout("success").run()
732
+ },
733
+ {
734
+ key: "callout-warning",
735
+ label: "Callout (warning)",
736
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ExclamationCircleOutlined, { style: { color: "#D97706" } }),
737
+ run: () => editor.chain().focus().insertCallout("warning").run()
738
+ },
739
+ {
740
+ key: "share",
741
+ label: "Share buttons",
742
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ShareAltOutlined, {}),
743
+ run: () => editor.chain().focus().insertShareBlock().run()
744
+ },
745
+ {
746
+ key: "subscribe",
747
+ label: "Subscribe form",
748
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.MailOutlined, {}),
749
+ run: () => editor.chain().focus().insertSubscribeBlock().run()
750
+ }
751
+ ];
752
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
753
+ "div",
754
+ {
755
+ style: {
756
+ display: "flex",
757
+ flexWrap: "wrap",
758
+ gap: 4,
759
+ padding: 8,
760
+ background: "var(--tps-bg-soft)",
761
+ borderBottom: "1px solid var(--tps-line)",
762
+ alignItems: "center",
763
+ position: "sticky",
764
+ top: 0,
765
+ zIndex: 5
766
+ },
767
+ children: [
768
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
769
+ ToolButton,
770
+ {
771
+ title: "Undo (\u2318Z)",
772
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.UndoOutlined, {}),
773
+ onClick: () => editor.chain().focus().undo().run(),
774
+ disabled: !editor.can().undo()
775
+ }
776
+ ),
777
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
778
+ ToolButton,
779
+ {
780
+ title: "Redo (\u2318\u21E7Z)",
781
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.RedoOutlined, {}),
782
+ onClick: () => editor.chain().focus().redo().run(),
783
+ disabled: !editor.can().redo()
784
+ }
785
+ ),
786
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
788
+ import_antd.Dropdown,
789
+ {
790
+ trigger: ["click"],
791
+ menu: {
792
+ items: HEADING_OPTIONS.map((o) => ({ key: o.key, label: o.label })),
793
+ selectedKeys: [headingValue],
794
+ onClick: ({ key }) => setHeading(key)
795
+ },
796
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Button, { size: "small", type: "text", style: { minWidth: 110 }, children: [
797
+ HEADING_OPTIONS.find((o) => o.key === headingValue)?.label,
798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.DownOutlined, { style: { fontSize: 10, marginLeft: 4 } })
799
+ ] })
800
+ }
801
+ ),
802
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
803
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
804
+ ToolButton,
805
+ {
806
+ title: "Bold (\u2318B)",
807
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.BoldOutlined, {}),
808
+ active: editor.isActive("bold"),
809
+ onClick: () => editor.chain().focus().toggleBold().run()
810
+ }
811
+ ),
812
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
813
+ ToolButton,
814
+ {
815
+ title: "Italic (\u2318I)",
816
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ItalicOutlined, {}),
817
+ active: editor.isActive("italic"),
818
+ onClick: () => editor.chain().focus().toggleItalic().run()
819
+ }
820
+ ),
821
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
822
+ ToolButton,
823
+ {
824
+ title: "Underline (\u2318U)",
825
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.UnderlineOutlined, {}),
826
+ active: editor.isActive("underline"),
827
+ onClick: () => editor.chain().focus().toggleUnderline().run()
828
+ }
829
+ ),
830
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
831
+ ToolButton,
832
+ {
833
+ title: "Strikethrough",
834
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.StrikethroughOutlined, {}),
835
+ active: editor.isActive("strike"),
836
+ onClick: () => editor.chain().focus().toggleStrike().run()
837
+ }
838
+ ),
839
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
840
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
841
+ ColorSwatch,
842
+ {
843
+ title: "Text color",
844
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.BgColorsOutlined, {}),
845
+ colors: TEXT_COLORS,
846
+ onPick: (c) => editor.chain().focus().setColor(c).run(),
847
+ onClear: () => editor.chain().focus().unsetColor().run()
848
+ }
849
+ ),
850
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
851
+ ColorSwatch,
852
+ {
853
+ title: "Highlight",
854
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.HighlightOutlined, {}),
855
+ colors: HIGHLIGHT_COLORS,
856
+ onPick: (c) => editor.chain().focus().toggleHighlight({ color: c }).run(),
857
+ onClear: () => editor.chain().focus().unsetHighlight().run()
858
+ }
859
+ ),
860
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
862
+ ToolButton,
863
+ {
864
+ title: "Align left",
865
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AlignLeftOutlined, {}),
866
+ active: editor.isActive({ textAlign: "left" }),
867
+ onClick: () => editor.chain().focus().setTextAlign("left").run()
868
+ }
869
+ ),
870
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
871
+ ToolButton,
872
+ {
873
+ title: "Align center",
874
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AlignCenterOutlined, {}),
875
+ active: editor.isActive({ textAlign: "center" }),
876
+ onClick: () => editor.chain().focus().setTextAlign("center").run()
877
+ }
878
+ ),
879
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
880
+ ToolButton,
881
+ {
882
+ title: "Align right",
883
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AlignRightOutlined, {}),
884
+ active: editor.isActive({ textAlign: "right" }),
885
+ onClick: () => editor.chain().focus().setTextAlign("right").run()
886
+ }
887
+ ),
888
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
889
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
890
+ ToolButton,
891
+ {
892
+ title: "Bulleted list",
893
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.UnorderedListOutlined, {}),
894
+ active: editor.isActive("bulletList"),
895
+ onClick: () => editor.chain().focus().toggleBulletList().run()
896
+ }
897
+ ),
898
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
899
+ ToolButton,
900
+ {
901
+ title: "Numbered list",
902
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.OrderedListOutlined, {}),
903
+ active: editor.isActive("orderedList"),
904
+ onClick: () => editor.chain().focus().toggleOrderedList().run()
905
+ }
906
+ ),
907
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
908
+ ToolButton,
909
+ {
910
+ title: "Task list",
911
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.CheckSquareOutlined, {}),
912
+ active: editor.isActive("taskList"),
913
+ onClick: () => editor.chain().focus().toggleTaskList().run()
914
+ }
915
+ ),
916
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
917
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
918
+ ToolButton,
919
+ {
920
+ title: "Quote",
921
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.BlockOutlined, {}),
922
+ active: editor.isActive("blockquote"),
923
+ onClick: () => editor.chain().focus().toggleBlockquote().run()
924
+ }
925
+ ),
926
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
927
+ ToolButton,
928
+ {
929
+ title: "Code block",
930
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.CodeOutlined, {}),
931
+ active: editor.isActive("codeBlock"),
932
+ onClick: () => editor.chain().focus().toggleCodeBlock().run()
933
+ }
934
+ ),
935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
936
+ ToolButton,
937
+ {
938
+ title: "Horizontal rule",
939
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.MinusOutlined, {}),
940
+ onClick: () => editor.chain().focus().setHorizontalRule().run()
941
+ }
942
+ ),
943
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
944
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
945
+ ToolButton,
946
+ {
947
+ title: "Insert / edit link",
948
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.LinkOutlined, {}),
949
+ active: editor.isActive("link"),
950
+ onClick: onOpenLink
951
+ }
952
+ ),
953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
954
+ import_antd.Dropdown,
955
+ {
956
+ trigger: ["click"],
957
+ placement: "bottomLeft",
958
+ menu: {
959
+ items: insertBlockItems.map((i) => ({ key: i.key, label: i.label, icon: i.icon })),
960
+ onClick: ({ key }) => insertBlockItems.find((i) => i.key === key)?.run()
961
+ },
962
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Button, { size: "small", type: "text", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.PlusOutlined, {}), style: { paddingInline: 8 }, children: [
963
+ "Insert ",
964
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.DownOutlined, { style: { fontSize: 10 } })
965
+ ] })
966
+ }
967
+ ),
968
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolbarDivider, {}),
969
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Clear formatting", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
970
+ import_antd.Button,
971
+ {
972
+ size: "small",
973
+ type: "text",
974
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ClearOutlined, {}),
975
+ onClick: () => editor.chain().focus().clearNodes().unsetAllMarks().run()
976
+ }
977
+ ) })
978
+ ]
979
+ }
980
+ );
981
+ }
982
+ function ToolButton({ title, icon, active, onClick, disabled }) {
983
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
984
+ import_antd.Button,
985
+ {
986
+ size: "small",
987
+ type: active ? "primary" : "text",
988
+ icon,
989
+ disabled,
990
+ onClick
991
+ }
992
+ ) });
993
+ }
994
+ function ToolbarDivider() {
995
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical", style: { height: 22, margin: "0 2px", borderColor: "var(--tps-line)" } });
996
+ }
997
+ function SelectionFormatBar({ editor, onLink }) {
998
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
999
+ import_antd.Space,
1000
+ {
1001
+ size: 2,
1002
+ style: {
1003
+ background: "#0F172A",
1004
+ padding: 4,
1005
+ borderRadius: 6,
1006
+ boxShadow: "0 8px 24px rgba(0,0,0,0.25)"
1007
+ },
1008
+ children: [
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1010
+ BubbleBtn,
1011
+ {
1012
+ title: "Bold",
1013
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.BoldOutlined, {}),
1014
+ active: editor.isActive("bold"),
1015
+ onClick: () => editor.chain().focus().toggleBold().run()
1016
+ }
1017
+ ),
1018
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1019
+ BubbleBtn,
1020
+ {
1021
+ title: "Italic",
1022
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ItalicOutlined, {}),
1023
+ active: editor.isActive("italic"),
1024
+ onClick: () => editor.chain().focus().toggleItalic().run()
1025
+ }
1026
+ ),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1028
+ BubbleBtn,
1029
+ {
1030
+ title: "Underline",
1031
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.UnderlineOutlined, {}),
1032
+ active: editor.isActive("underline"),
1033
+ onClick: () => editor.chain().focus().toggleUnderline().run()
1034
+ }
1035
+ ),
1036
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1037
+ BubbleBtn,
1038
+ {
1039
+ title: "Strike",
1040
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.StrikethroughOutlined, {}),
1041
+ active: editor.isActive("strike"),
1042
+ onClick: () => editor.chain().focus().toggleStrike().run()
1043
+ }
1044
+ ),
1045
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical", style: { background: "rgba(255,255,255,0.18)", height: 18, margin: "0 2px" } }),
1046
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1047
+ BubbleBtn,
1048
+ {
1049
+ title: "Inline code",
1050
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.CodeOutlined, {}),
1051
+ active: editor.isActive("code"),
1052
+ onClick: () => editor.chain().focus().toggleCode().run()
1053
+ }
1054
+ ),
1055
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1056
+ BubbleBtn,
1057
+ {
1058
+ title: "Link",
1059
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.LinkOutlined, {}),
1060
+ active: editor.isActive("link"),
1061
+ onClick: onLink
1062
+ }
1063
+ )
1064
+ ]
1065
+ }
1066
+ );
1067
+ }
1068
+ function BubbleBtn({ title, icon, active, onClick }) {
1069
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1070
+ import_antd.Button,
1071
+ {
1072
+ size: "small",
1073
+ type: "text",
1074
+ icon,
1075
+ onClick,
1076
+ style: { color: active ? "#F59E0B" : "#fff" }
1077
+ }
1078
+ ) });
1079
+ }
1080
+ function ImageBubble({ editor }) {
1081
+ const attrs = editor.getAttributes("ciqImage");
1082
+ const setAlign = (align) => editor.chain().focus().updateAttributes("ciqImage", { align }).run();
1083
+ const setWidth = (width) => editor.chain().focus().updateAttributes("ciqImage", { width }).run();
1084
+ const remove = () => editor.chain().focus().deleteSelection().run();
1085
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1086
+ import_antd.Space,
1087
+ {
1088
+ size: 2,
1089
+ style: {
1090
+ background: "#fff",
1091
+ padding: 4,
1092
+ border: "1px solid var(--tps-line)",
1093
+ borderRadius: 6,
1094
+ boxShadow: "0 8px 24px rgba(0,0,0,0.12)"
1095
+ },
1096
+ children: [
1097
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Align left", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1098
+ import_antd.Button,
1099
+ {
1100
+ size: "small",
1101
+ type: attrs.align === "left" ? "primary" : "text",
1102
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AlignLeftOutlined, {}),
1103
+ onClick: () => setAlign("left")
1104
+ }
1105
+ ) }),
1106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Align center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1107
+ import_antd.Button,
1108
+ {
1109
+ size: "small",
1110
+ type: attrs.align === "center" ? "primary" : "text",
1111
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AlignCenterOutlined, {}),
1112
+ onClick: () => setAlign("center")
1113
+ }
1114
+ ) }),
1115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Align right", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1116
+ import_antd.Button,
1117
+ {
1118
+ size: "small",
1119
+ type: attrs.align === "right" ? "primary" : "text",
1120
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.AlignRightOutlined, {}),
1121
+ onClick: () => setAlign("right")
1122
+ }
1123
+ ) }),
1124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1125
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1126
+ import_antd.Dropdown,
1127
+ {
1128
+ trigger: ["click"],
1129
+ menu: {
1130
+ items: IMAGE_WIDTHS.map((w) => ({ key: w.value, label: w.label })),
1131
+ selectedKeys: [attrs.width || "medium"],
1132
+ onClick: ({ key }) => setWidth(key)
1133
+ },
1134
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Button, { size: "small", type: "text", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ColumnWidthOutlined, {}), children: [
1135
+ IMAGE_WIDTHS.find((w) => w.value === (attrs.width || "medium"))?.label,
1136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.DownOutlined, { style: { fontSize: 10, marginLeft: 4 } })
1137
+ ] })
1138
+ }
1139
+ ),
1140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Edit alt text", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1142
+ import_antd.Button,
1143
+ {
1144
+ size: "small",
1145
+ type: "text",
1146
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.EditOutlined, {}),
1147
+ onClick: () => {
1148
+ const alt = window.prompt("Alt text", attrs.alt || "");
1149
+ if (alt !== null) {
1150
+ editor.chain().focus().updateAttributes("ciqImage", { alt }).run();
1151
+ }
1152
+ }
1153
+ }
1154
+ ) }),
1155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Delete image", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", type: "text", danger: true, icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.DeleteOutlined, {}), onClick: remove }) })
1156
+ ]
1157
+ }
1158
+ );
1159
+ }
1160
+ function TableBubble({ editor }) {
1161
+ const cellAttrs = editor.getAttributes("tableCell");
1162
+ const setVAlign = (verticalAlign) => {
1163
+ editor.chain().focus().updateAttributes("tableCell", { verticalAlign }).run();
1164
+ editor.chain().focus().updateAttributes("tableHeader", { verticalAlign }).run();
1165
+ };
1166
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1167
+ import_antd.Space,
1168
+ {
1169
+ size: 4,
1170
+ wrap: true,
1171
+ style: {
1172
+ background: "#fff",
1173
+ padding: 4,
1174
+ border: "1px solid var(--tps-line)",
1175
+ borderRadius: 6,
1176
+ boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
1177
+ maxWidth: 720
1178
+ },
1179
+ children: [
1180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().addColumnBefore().run(), children: "+ col \u2190" }),
1181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().addColumnAfter().run(), children: "+ col \u2192" }),
1182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().deleteColumn().run(), children: "\u2212 col" }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().addRowBefore().run(), children: "+ row \u2191" }),
1185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().addRowAfter().run(), children: "+ row \u2193" }),
1186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().deleteRow().run(), children: "\u2212 row" }),
1187
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().mergeCells().run(), children: "Merge" }),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().splitCell().run(), children: "Split" }),
1190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Top", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1192
+ import_antd.Button,
1193
+ {
1194
+ size: "small",
1195
+ type: cellAttrs.verticalAlign === "top" ? "primary" : "default",
1196
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.VerticalAlignTopOutlined, {}),
1197
+ onClick: () => setVAlign("top")
1198
+ }
1199
+ ) }),
1200
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Middle", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1201
+ import_antd.Button,
1202
+ {
1203
+ size: "small",
1204
+ type: cellAttrs.verticalAlign === "middle" ? "primary" : "default",
1205
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.VerticalAlignMiddleOutlined, {}),
1206
+ onClick: () => setVAlign("middle")
1207
+ }
1208
+ ) }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Bottom", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1210
+ import_antd.Button,
1211
+ {
1212
+ size: "small",
1213
+ type: cellAttrs.verticalAlign === "bottom" ? "primary" : "default",
1214
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.VerticalAlignBottomOutlined, {}),
1215
+ onClick: () => setVAlign("bottom")
1216
+ }
1217
+ ) }),
1218
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().toggleHeaderRow().run(), children: "Header row" }),
1220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => editor.chain().focus().toggleHeaderColumn().run(), children: "Header col" }),
1221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Divider, { type: "vertical" }),
1222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", danger: true, onClick: () => editor.chain().focus().deleteTable().run(), children: "Delete" })
1223
+ ]
1224
+ }
1225
+ );
1226
+ }
1227
+ function AtomBlockBubble({ editor, onEdit }) {
1228
+ const kind = editor.isActive("shareBlock") ? "share" : editor.isActive("subscribeBlock") ? "subscribe" : null;
1229
+ if (!kind) return null;
1230
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1231
+ import_antd.Space,
1232
+ {
1233
+ size: 4,
1234
+ style: {
1235
+ background: "#fff",
1236
+ padding: 4,
1237
+ border: "1px solid var(--tps-line)",
1238
+ borderRadius: 6,
1239
+ boxShadow: "0 8px 24px rgba(0,0,0,0.12)"
1240
+ },
1241
+ children: [
1242
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tag, { color: kind === "share" ? "cyan" : "gold", style: { margin: 0 }, children: kind === "share" ? "Share block" : "Subscribe block" }),
1243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.EditOutlined, {}), onClick: () => onEdit(kind), children: "Edit" }),
1244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1245
+ import_antd.Button,
1246
+ {
1247
+ size: "small",
1248
+ type: "text",
1249
+ danger: true,
1250
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.DeleteOutlined, {}),
1251
+ onClick: () => editor.chain().focus().deleteSelection().run()
1252
+ }
1253
+ )
1254
+ ]
1255
+ }
1256
+ );
1257
+ }
1258
+ function BlockInsertMenu({ editor, onImage }) {
1259
+ const items = [
1260
+ { key: "h1", label: "Heading 1", run: () => editor.chain().focus().toggleHeading({ level: 1 }).run() },
1261
+ { key: "h2", label: "Heading 2", run: () => editor.chain().focus().toggleHeading({ level: 2 }).run() },
1262
+ { key: "h3", label: "Heading 3", run: () => editor.chain().focus().toggleHeading({ level: 3 }).run() },
1263
+ { key: "ul", label: "Bulleted list", run: () => editor.chain().focus().toggleBulletList().run() },
1264
+ { key: "ol", label: "Numbered list", run: () => editor.chain().focus().toggleOrderedList().run() },
1265
+ { key: "task", label: "Task list", run: () => editor.chain().focus().toggleTaskList().run() },
1266
+ { key: "quote", label: "Quote", run: () => editor.chain().focus().toggleBlockquote().run() },
1267
+ { key: "code", label: "Code block", run: () => editor.chain().focus().toggleCodeBlock().run() },
1268
+ { key: "hr", label: "Divider", run: () => editor.chain().focus().setHorizontalRule().run() },
1269
+ { type: "divider" },
1270
+ { key: "image", label: "Image", run: () => onImage() },
1271
+ { key: "table", label: "Table", run: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
1272
+ { key: "cols2", label: "2 columns", run: () => editor.chain().focus().insertColumns(2).run() },
1273
+ { key: "cols3", label: "3 columns", run: () => editor.chain().focus().insertColumns(3).run() },
1274
+ { key: "callout", label: "Callout", run: () => editor.chain().focus().insertCallout("info").run() },
1275
+ { key: "share", label: "Share buttons", run: () => editor.chain().focus().insertShareBlock().run() },
1276
+ { key: "subscribe", label: "Subscribe form", run: () => editor.chain().focus().insertSubscribeBlock().run() }
1277
+ ];
1278
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1279
+ import_antd.Dropdown,
1280
+ {
1281
+ trigger: ["click"],
1282
+ placement: "bottomLeft",
1283
+ menu: {
1284
+ items: items.map(
1285
+ (i, idx) => i.type === "divider" ? { type: "divider", key: `d${idx}` } : { key: i.key, label: i.label }
1286
+ ),
1287
+ onClick: ({ key }) => items.find((i) => i.key === key)?.run()
1288
+ },
1289
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title: "Insert block", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1290
+ import_antd.Button,
1291
+ {
1292
+ size: "small",
1293
+ shape: "circle",
1294
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.PlusOutlined, {}),
1295
+ style: {
1296
+ background: "#fff",
1297
+ borderColor: "var(--tps-line)",
1298
+ boxShadow: "0 4px 12px rgba(0,0,0,0.08)"
1299
+ }
1300
+ }
1301
+ ) })
1302
+ }
1303
+ );
1304
+ }
1305
+ function ColorSwatch({ title, icon, colors, onPick, onClear }) {
1306
+ const [open, setOpen] = (0, import_react.useState)(false);
1307
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1308
+ import_antd.Popover,
1309
+ {
1310
+ open,
1311
+ onOpenChange: setOpen,
1312
+ trigger: "click",
1313
+ placement: "bottomLeft",
1314
+ content: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Space, { direction: "vertical", size: 8, style: { width: 200 }, children: [
1315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 6 }, children: colors.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1316
+ "button",
1317
+ {
1318
+ onClick: () => {
1319
+ onPick(c);
1320
+ setOpen(false);
1321
+ },
1322
+ style: {
1323
+ width: 28,
1324
+ height: 28,
1325
+ background: c,
1326
+ border: "1px solid var(--tps-line)",
1327
+ borderRadius: 4,
1328
+ cursor: "pointer"
1329
+ },
1330
+ title: c
1331
+ },
1332
+ c
1333
+ )) }),
1334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", block: true, onClick: () => {
1335
+ onClear();
1336
+ setOpen(false);
1337
+ }, children: "Clear" })
1338
+ ] }),
1339
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tooltip, { title, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", type: "text", icon }) })
1340
+ }
1341
+ );
1342
+ }
1343
+ function LinkModal({ open, onClose, editor }) {
1344
+ const [href, setHref] = (0, import_react.useState)("");
1345
+ (0, import_react.useEffect)(() => {
1346
+ if (open) setHref(editor.getAttributes("link").href || "");
1347
+ }, [open, editor]);
1348
+ const apply = () => {
1349
+ if (!href) editor.chain().focus().unsetLink().run();
1350
+ else editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
1351
+ onClose();
1352
+ };
1353
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1354
+ import_antd.Modal,
1355
+ {
1356
+ title: "Link",
1357
+ open,
1358
+ onCancel: onClose,
1359
+ onOk: apply,
1360
+ okText: "Apply",
1361
+ okButtonProps: { disabled: !href && !editor.isActive("link") },
1362
+ destroyOnClose: true,
1363
+ children: [
1364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1365
+ import_antd.Input,
1366
+ {
1367
+ placeholder: "https://example.com",
1368
+ value: href,
1369
+ onChange: (e) => setHref(e.target.value),
1370
+ onPressEnter: apply,
1371
+ autoFocus: true
1372
+ }
1373
+ ),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { type: "secondary", style: { fontSize: 12 }, children: "Leave blank and Apply to remove the link." })
1375
+ ]
1376
+ }
1377
+ );
1378
+ }
1379
+ function ImageModal({ open, onClose, editor, uploadMedia }) {
1380
+ const [tab, setTab] = (0, import_react.useState)("upload");
1381
+ const [src, setSrc] = (0, import_react.useState)("");
1382
+ const [alt, setAlt] = (0, import_react.useState)("");
1383
+ const [uploading, setUploading] = (0, import_react.useState)(false);
1384
+ (0, import_react.useEffect)(() => {
1385
+ if (open) {
1386
+ setTab("upload");
1387
+ setSrc("");
1388
+ setAlt("");
1389
+ }
1390
+ }, [open]);
1391
+ const apply = () => {
1392
+ if (!src) return;
1393
+ editor.chain().focus().insertContent({
1394
+ type: "ciqImage",
1395
+ attrs: { src, alt, align: "center", width: "medium" }
1396
+ }).run();
1397
+ onClose();
1398
+ };
1399
+ const beforeUpload = async (file) => {
1400
+ if (!file.type.startsWith("image/")) {
1401
+ import_antd.message.error("Only image files are supported.");
1402
+ return import_antd.Upload.LIST_IGNORE;
1403
+ }
1404
+ setUploading(true);
1405
+ try {
1406
+ const fd = new FormData();
1407
+ fd.append("file", file);
1408
+ const { url } = await uploadMedia(fd);
1409
+ setSrc(url);
1410
+ if (!alt) setAlt(file.name.replace(/\.[^.]+$/, ""));
1411
+ } catch (err) {
1412
+ import_antd.message.error(err.message || "Upload failed");
1413
+ } finally {
1414
+ setUploading(false);
1415
+ }
1416
+ return import_antd.Upload.LIST_IGNORE;
1417
+ };
1418
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1419
+ import_antd.Modal,
1420
+ {
1421
+ title: "Insert image",
1422
+ open,
1423
+ onCancel: onClose,
1424
+ onOk: apply,
1425
+ okText: "Insert",
1426
+ okButtonProps: { disabled: !src },
1427
+ destroyOnClose: true,
1428
+ width: 560,
1429
+ children: [
1430
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1431
+ import_antd.Tabs,
1432
+ {
1433
+ activeKey: tab,
1434
+ onChange: setTab,
1435
+ items: [
1436
+ {
1437
+ key: "upload",
1438
+ label: "Upload",
1439
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Space, { direction: "vertical", size: 12, style: { width: "100%" }, children: [
1440
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1441
+ import_antd.Upload.Dragger,
1442
+ {
1443
+ multiple: false,
1444
+ beforeUpload,
1445
+ showUploadList: false,
1446
+ disabled: uploading,
1447
+ accept: "image/*",
1448
+ children: [
1449
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 28, margin: 0 }, children: uploading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.LoadingOutlined, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.UploadOutlined, {}) }),
1450
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { marginTop: 4 }, children: uploading ? "Uploading\u2026" : "Click or drag an image" }),
1451
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "var(--tps-muted)", fontSize: 12 }, children: "JPG / PNG / WEBP / GIF / SVG \xB7 max 10MB" })
1452
+ ]
1453
+ }
1454
+ ),
1455
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1456
+ import_antd.Input,
1457
+ {
1458
+ placeholder: "Alt text (for accessibility & SEO)",
1459
+ value: alt,
1460
+ onChange: (e) => setAlt(e.target.value)
1461
+ }
1462
+ ),
1463
+ src && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Tag, { color: "green", style: { width: "fit-content" }, children: "Uploaded" })
1464
+ ] })
1465
+ },
1466
+ {
1467
+ key: "url",
1468
+ label: "From URL",
1469
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Space, { direction: "vertical", size: 12, style: { width: "100%" }, children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1471
+ import_antd.Input,
1472
+ {
1473
+ placeholder: "https://\u2026",
1474
+ value: src,
1475
+ onChange: (e) => setSrc(e.target.value),
1476
+ autoFocus: true
1477
+ }
1478
+ ),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1480
+ import_antd.Input,
1481
+ {
1482
+ placeholder: "Alt text (for accessibility & SEO)",
1483
+ value: alt,
1484
+ onChange: (e) => setAlt(e.target.value)
1485
+ }
1486
+ )
1487
+ ] })
1488
+ }
1489
+ ]
1490
+ }
1491
+ ),
1492
+ src && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 12, border: "1px solid var(--tps-line)", padding: 8, borderRadius: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src, alt, style: { maxWidth: "100%", display: "block" } }) })
1493
+ ]
1494
+ }
1495
+ );
1496
+ }
1497
+ function BlockAttributeModal({ kind, open, onClose, editor }) {
1498
+ const [form] = import_antd.Form.useForm();
1499
+ const isShare = kind === "share";
1500
+ const nodeName = isShare ? "shareBlock" : "subscribeBlock";
1501
+ (0, import_react.useEffect)(() => {
1502
+ if (!open) return;
1503
+ const attrs = editor.getAttributes(nodeName);
1504
+ form.setFieldsValue(attrs);
1505
+ }, [open, editor, nodeName, form]);
1506
+ const apply = async () => {
1507
+ const values = await form.validateFields();
1508
+ editor.chain().focus().updateAttributes(nodeName, values).run();
1509
+ onClose();
1510
+ };
1511
+ if (!kind) return null;
1512
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1513
+ import_antd.Modal,
1514
+ {
1515
+ title: isShare ? "Share block" : "Subscribe block",
1516
+ open,
1517
+ onCancel: onClose,
1518
+ onOk: apply,
1519
+ okText: "Apply",
1520
+ destroyOnClose: true,
1521
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Form, { form, layout: "vertical", preserve: false, children: isShare ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Form.Item, { label: "Label", name: "label", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Input, { placeholder: "Share this page" }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1522
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Form.Item, { label: "Heading", name: "heading", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Input, { placeholder: "Stay in the loop" }) }),
1523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Form.Item, { label: "Body", name: "body", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Input.TextArea, { rows: 2, placeholder: "One short email when we publish \u2014 never more." }) }),
1524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Form.Item, { label: "Button label", name: "button", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Input, { placeholder: "Subscribe" }) })
1525
+ ] }) })
1526
+ }
1527
+ );
1528
+ }
1529
+ function EditorStyles() {
1530
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { jsx: true, global: true, children: `
1531
+ .tiptap {
1532
+ outline: none;
1533
+ font-size: 15px;
1534
+ line-height: 1.65;
1535
+ color: var(--tps-ink);
1536
+ min-height: 100%;
1537
+ }
1538
+ .tiptap > * + * { margin-top: 0.75em; }
1539
+ .tiptap p.is-editor-empty:first-child::before {
1540
+ content: attr(data-placeholder);
1541
+ float: left;
1542
+ color: var(--tps-muted);
1543
+ pointer-events: none;
1544
+ height: 0;
1545
+ }
1546
+ .tiptap h1 { font-size: 28px; font-weight: 800; line-height: 1.2; margin: 1em 0 0.5em; }
1547
+ .tiptap h2 { font-size: 22px; font-weight: 700; line-height: 1.25; margin: 1em 0 0.5em; }
1548
+ .tiptap h3 { font-size: 18px; font-weight: 700; line-height: 1.3; margin: 1em 0 0.4em; }
1549
+ .tiptap h4 { font-size: 16px; font-weight: 700; line-height: 1.35; margin: 1em 0 0.4em; }
1550
+ .tiptap p { margin: 0; }
1551
+ .tiptap ul, .tiptap ol { padding-left: 22px; }
1552
+ .tiptap ul[data-type='taskList'] { list-style: none; padding-left: 0; }
1553
+ .tiptap ul[data-type='taskList'] li { display: flex; gap: 8px; align-items: flex-start; }
1554
+ .tiptap a { color: var(--tps-primary); text-decoration: underline; }
1555
+ .tiptap blockquote {
1556
+ border-left: 3px solid var(--tps-primary);
1557
+ padding-left: 14px;
1558
+ color: var(--tps-muted);
1559
+ font-style: italic;
1560
+ }
1561
+ .tiptap pre.tiptap-code, .tiptap pre {
1562
+ background: #0f172a; color: #e2e8f0; padding: 12px 14px; border-radius: 6px;
1563
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
1564
+ overflow-x: auto;
1565
+ }
1566
+ .tiptap code {
1567
+ background: var(--tps-bg-soft); padding: 2px 6px; border-radius: 4px; font-size: 0.9em;
1568
+ }
1569
+ .tiptap hr { border: none; border-top: 1px solid var(--tps-line); margin: 1.5em 0; }
1570
+ .tiptap mark { padding: 0 2px; border-radius: 3px; }
1571
+
1572
+ /* Image block with align + width. */
1573
+ .tiptap .tps-block-image { margin: 1em 0; display: flex; }
1574
+ .tiptap .tps-block-image[data-align='left'] { justify-content: flex-start; }
1575
+ .tiptap .tps-block-image[data-align='center'] { justify-content: center; }
1576
+ .tiptap .tps-block-image[data-align='right'] { justify-content: flex-end; }
1577
+ .tiptap .tps-block-image img { max-width: 100%; height: auto; border-radius: 8px; display: block; }
1578
+ .tiptap .tps-block-image.tps-img-small img { width: 40%; }
1579
+ .tiptap .tps-block-image.tps-img-medium img { width: 60%; }
1580
+ .tiptap .tps-block-image.tps-img-large img { width: 80%; }
1581
+ .tiptap .tps-block-image.tps-img-full img { width: 100%; }
1582
+ .tiptap .tps-block-image.ProseMirror-selectednode img,
1583
+ .tiptap .tps-block-image:has(img.ProseMirror-selectednode) img {
1584
+ outline: 2px solid var(--tps-primary); outline-offset: 2px;
1585
+ }
1586
+
1587
+ /* Tables \u2014 alternating rows + resize handles. */
1588
+ .tiptap table.tps-rich-table, .tiptap table {
1589
+ border-collapse: collapse; width: 100%; margin: 1em 0; table-layout: fixed;
1590
+ }
1591
+ .tiptap table td, .tiptap table th {
1592
+ border: 1px solid var(--tps-line); padding: 8px 10px; vertical-align: top;
1593
+ position: relative; min-width: 40px;
1594
+ }
1595
+ .tiptap table th { background: var(--tps-bg-soft); font-weight: 700; text-align: left; }
1596
+ .tiptap table tbody tr:nth-child(even) td { background: rgba(15, 118, 110, 0.025); }
1597
+ .tiptap table .selectedCell::after {
1598
+ content: ''; position: absolute; inset: 0;
1599
+ background: rgba(15, 118, 110, 0.12); pointer-events: none;
1600
+ }
1601
+ .tiptap table .column-resize-handle {
1602
+ position: absolute; right: -2px; top: 0; bottom: -2px; width: 4px;
1603
+ background-color: var(--tps-primary); pointer-events: none;
1604
+ }
1605
+ .tiptap table p { margin: 0; }
1606
+
1607
+ /* Columns. */
1608
+ .tiptap .tps-block-columns {
1609
+ display: grid; gap: 16px; margin: 1em 0;
1610
+ border: 1px dashed var(--tps-line); padding: 12px; border-radius: 8px;
1611
+ }
1612
+ .tiptap .tps-block-columns.tps-cols-2 { grid-template-columns: 1fr 1fr; }
1613
+ .tiptap .tps-block-columns.tps-cols-3 { grid-template-columns: repeat(3, 1fr); }
1614
+ .tiptap .tps-block-columns.tps-cols-4 { grid-template-columns: repeat(4, 1fr); }
1615
+ .tiptap .tps-block-column { padding: 8px; min-height: 40px; border-radius: 6px; }
1616
+ .tiptap .tps-block-column:hover { background: var(--tps-bg-soft); }
1617
+
1618
+ /* Callout. */
1619
+ .tiptap .tps-block-callout {
1620
+ border-left: 4px solid var(--tps-primary);
1621
+ background: rgba(15, 118, 110, 0.05);
1622
+ padding: 12px 16px; border-radius: 6px; margin: 1em 0;
1623
+ }
1624
+ .tiptap .tps-block-callout.tps-callout-success {
1625
+ border-color: #059669; background: rgba(5, 150, 105, 0.06);
1626
+ }
1627
+ .tiptap .tps-block-callout.tps-callout-warning {
1628
+ border-color: #D97706; background: rgba(217, 119, 6, 0.07);
1629
+ }
1630
+ .tiptap .tps-block-callout.tps-callout-danger {
1631
+ border-color: #DC2626; background: rgba(220, 38, 38, 0.06);
1632
+ }
1633
+
1634
+ /* Atom blocks (share, subscribe) \u2014 show as cards in the canvas. */
1635
+ .tiptap .tps-block-share, .tiptap .tps-block-subscribe {
1636
+ border: 1px solid var(--tps-line); border-radius: 8px;
1637
+ padding: 16px; margin: 1em 0; background: var(--tps-bg-soft);
1638
+ }
1639
+ .tiptap .tps-block-share-label, .tiptap .tps-block-subscribe-heading {
1640
+ font-weight: 700; margin-bottom: 8px; color: var(--tps-ink);
1641
+ }
1642
+ .tiptap .tps-block-share-row { display: flex; gap: 8px; flex-wrap: wrap; }
1643
+ .tiptap .tps-block-share-btn {
1644
+ padding: 6px 12px; background: #fff; border: 1px solid var(--tps-line);
1645
+ border-radius: 6px; text-decoration: none; color: var(--tps-ink);
1646
+ font-size: 13px; font-weight: 600;
1647
+ }
1648
+ .tiptap .tps-block-share-btn:hover { border-color: var(--tps-primary); color: var(--tps-primary); }
1649
+ .tiptap .tps-block-subscribe-form { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
1650
+ .tiptap .tps-block-subscribe-input {
1651
+ flex: 1; min-width: 200px; padding: 8px 12px;
1652
+ border: 1px solid var(--tps-line); border-radius: 6px; font-size: 14px;
1653
+ }
1654
+ .tiptap .tps-block-subscribe-button {
1655
+ padding: 8px 16px; background: var(--tps-primary); color: #fff;
1656
+ border: 0; border-radius: 6px; font-weight: 600; cursor: pointer;
1657
+ }
1658
+ .tiptap .tps-block-subscribe-button:hover { background: var(--tps-primary-dark); }
1659
+ .tiptap .tps-block-subscribe-body { color: var(--tps-muted); margin: 4px 0 8px; }
1660
+
1661
+ /* Selected atom block highlight. */
1662
+ .tiptap .ProseMirror-selectednode.tps-block-share,
1663
+ .tiptap .ProseMirror-selectednode.tps-block-subscribe {
1664
+ outline: 2px solid var(--tps-primary); outline-offset: 2px;
1665
+ }
1666
+ ` });
1667
+ }
1668
+
1669
+ // src/HistoryPanel.jsx
1670
+ var import_react3 = require("react");
1671
+ var import_antd2 = require("antd");
1672
+ var import_icons2 = require("@ant-design/icons");
1673
+ var import_diff = require("diff");
1674
+ var import_dayjs = __toESM(require("dayjs"), 1);
1675
+ var import_relativeTime = __toESM(require("dayjs/plugin/relativeTime"), 1);
1676
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1677
+ import_dayjs.default.extend(import_relativeTime.default);
1678
+ var { Text: Text2, Paragraph } = import_antd2.Typography;
1679
+ function notConfigured(name) {
1680
+ return () => {
1681
+ throw new Error(
1682
+ `@techrox/page-studio-form: HistoryPanel was used without a \`${name}\` adapter. Pass loadHistory + restoreRevision callbacks.`
1683
+ );
1684
+ };
1685
+ }
1686
+ function HistoryPanel({
1687
+ pageKey,
1688
+ currentPage,
1689
+ onRestored,
1690
+ loadHistory = notConfigured("loadHistory"),
1691
+ restoreRevision = notConfigured("restoreRevision")
1692
+ }) {
1693
+ const { message, modal } = import_antd2.App.useApp();
1694
+ const [pending, startTransition] = (0, import_react3.useTransition)();
1695
+ const [loading, setLoading] = (0, import_react3.useState)(false);
1696
+ const [revisions, setRevisions] = (0, import_react3.useState)([]);
1697
+ const [error, setError] = (0, import_react3.useState)(null);
1698
+ const [selectedId, setSelectedId] = (0, import_react3.useState)(null);
1699
+ const [view, setView] = (0, import_react3.useState)("summary");
1700
+ const load = async () => {
1701
+ setLoading(true);
1702
+ setError(null);
1703
+ try {
1704
+ const { revisions: revisions2 } = await loadHistory(pageKey);
1705
+ setRevisions(revisions2 || []);
1706
+ if (revisions2?.length && !selectedId) {
1707
+ setSelectedId(revisions2[0]._id);
1708
+ }
1709
+ } catch (e) {
1710
+ setError(e.message);
1711
+ } finally {
1712
+ setLoading(false);
1713
+ }
1714
+ };
1715
+ (0, import_react3.useEffect)(() => {
1716
+ load();
1717
+ }, [pageKey]);
1718
+ const selected = (0, import_react3.useMemo)(
1719
+ () => revisions.find((r) => r._id === selectedId) || null,
1720
+ [revisions, selectedId]
1721
+ );
1722
+ const onRestore = (rev) => {
1723
+ modal.confirm({
1724
+ title: `Restore version ${rev.version}?`,
1725
+ content: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
1726
+ "The current page will be replaced with this snapshot from",
1727
+ " ",
1728
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { strong: true, children: (0, import_dayjs.default)(rev.created_at).format("YYYY-MM-DD HH:mm") }),
1729
+ ". A new revision is logged so you can undo this restore."
1730
+ ] }),
1731
+ okText: "Restore",
1732
+ okButtonProps: { danger: true },
1733
+ onOk: () => new Promise((resolve, reject) => {
1734
+ startTransition(async () => {
1735
+ try {
1736
+ await restoreRevision(pageKey, rev._id);
1737
+ message.success(`Restored version ${rev.version}.`);
1738
+ onRestored?.();
1739
+ await load();
1740
+ resolve();
1741
+ } catch (e) {
1742
+ message.error(e.message || "Restore failed.");
1743
+ reject(e);
1744
+ }
1745
+ });
1746
+ })
1747
+ });
1748
+ };
1749
+ if (loading) {
1750
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Card, { size: "small", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { textAlign: "center", padding: 40 }, children: [
1751
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Spin, {}),
1752
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginTop: 12, color: "var(--tps-muted)", fontSize: 13 }, children: "Loading history\u2026" })
1753
+ ] }) });
1754
+ }
1755
+ if (error) {
1756
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Alert, { type: "error", showIcon: true, message: "Could not load history", description: error });
1757
+ }
1758
+ if (!revisions.length) {
1759
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Card, { size: "small", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1760
+ import_antd2.Empty,
1761
+ {
1762
+ image: import_antd2.Empty.PRESENTED_IMAGE_SIMPLE,
1763
+ description: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { direction: "vertical", size: 4, children: [
1764
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { strong: true, children: "No history yet" }),
1765
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { type: "secondary", style: { fontSize: 13 }, children: "Every save creates a revision. They\u2019ll appear here once you save changes." })
1766
+ ] })
1767
+ }
1768
+ ) });
1769
+ }
1770
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1771
+ "div",
1772
+ {
1773
+ style: {
1774
+ display: "grid",
1775
+ gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)",
1776
+ gap: 16
1777
+ },
1778
+ children: [
1779
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1780
+ import_antd2.Card,
1781
+ {
1782
+ size: "small",
1783
+ title: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { children: [
1784
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.HistoryOutlined, {}),
1785
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
1786
+ revisions.length,
1787
+ " revision",
1788
+ revisions.length === 1 ? "" : "s"
1789
+ ] })
1790
+ ] }),
1791
+ extra: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tooltip, { title: "Refresh", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Button, { size: "small", type: "text", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.ReloadOutlined, {}), onClick: load }) }),
1792
+ styles: { body: { padding: 0, maxHeight: "70vh", overflowY: "auto" } },
1793
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1794
+ import_antd2.List,
1795
+ {
1796
+ dataSource: revisions,
1797
+ renderItem: (r, i) => {
1798
+ const isSelected = r._id === selectedId;
1799
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1800
+ import_antd2.List.Item,
1801
+ {
1802
+ onClick: () => setSelectedId(r._id),
1803
+ style: {
1804
+ padding: "12px 16px",
1805
+ cursor: "pointer",
1806
+ background: isSelected ? "rgba(15, 118, 110, 0.08)" : "transparent",
1807
+ borderLeft: isSelected ? "3px solid var(--tps-primary)" : "3px solid transparent"
1808
+ },
1809
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { width: "100%" }, children: [
1810
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { size: 6, wrap: true, children: [
1811
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Tag, { color: r.action === "restore" ? "orange" : "geekblue", children: [
1812
+ "v",
1813
+ r.version
1814
+ ] }),
1815
+ i === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tag, { color: "green", children: "latest" }),
1816
+ r.action === "restore" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tag, { icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.RollbackOutlined, {}), color: "orange", children: "restore" }),
1817
+ r.snapshot?.published === false && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tag, { children: "draft" })
1818
+ ] }),
1819
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 13, marginTop: 6, fontWeight: 500 }, children: r.snapshot?.title || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { type: "secondary", children: "(no title)" }) }),
1820
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { size: 10, style: { fontSize: 12, color: "var(--tps-muted)", marginTop: 4 }, wrap: true, children: [
1821
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tooltip, { title: (0, import_dayjs.default)(r.created_at).format("YYYY-MM-DD HH:mm:ss"), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
1822
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.ClockCircleOutlined, {}),
1823
+ " ",
1824
+ (0, import_dayjs.default)(r.created_at).fromNow()
1825
+ ] }) }),
1826
+ r.created_by_email && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tooltip, { title: r.created_by_email, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
1827
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.UserOutlined, {}),
1828
+ " ",
1829
+ abbreviateEmail(r.created_by_email)
1830
+ ] }) })
1831
+ ] })
1832
+ ] })
1833
+ }
1834
+ );
1835
+ }
1836
+ }
1837
+ )
1838
+ }
1839
+ ),
1840
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1841
+ RevisionDetail,
1842
+ {
1843
+ revision: selected,
1844
+ currentPage,
1845
+ view,
1846
+ setView,
1847
+ onRestore: () => onRestore(selected),
1848
+ restoring: pending
1849
+ }
1850
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Card, { size: "small", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { type: "secondary", children: "Select a revision on the left to see its diff." }) }) })
1851
+ ]
1852
+ }
1853
+ );
1854
+ }
1855
+ function RevisionDetail({ revision, currentPage, view, setView, onRestore, restoring }) {
1856
+ const left = currentPage || {};
1857
+ const right = revision.snapshot || {};
1858
+ const summary = (0, import_react3.useMemo)(() => fieldChangeSummary(left, right), [left, right]);
1859
+ const jsonDiff = (0, import_react3.useMemo)(() => makeJsonDiff(left, right), [left, right]);
1860
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1861
+ import_antd2.Card,
1862
+ {
1863
+ size: "small",
1864
+ title: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { children: [
1865
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Tag, { color: revision.action === "restore" ? "orange" : "geekblue", children: [
1866
+ "Version ",
1867
+ revision.version
1868
+ ] }),
1869
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { type: "secondary", style: { fontSize: 13 }, children: (0, import_dayjs.default)(revision.created_at).format("YYYY-MM-DD HH:mm:ss") }),
1870
+ revision.created_by_email && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text2, { type: "secondary", style: { fontSize: 13 }, children: [
1871
+ "by ",
1872
+ revision.created_by_email
1873
+ ] })
1874
+ ] }),
1875
+ extra: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { children: [
1876
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1877
+ import_antd2.Segmented,
1878
+ {
1879
+ size: "small",
1880
+ value: view,
1881
+ onChange: setView,
1882
+ options: [
1883
+ { label: "Summary", value: "summary" },
1884
+ { label: "JSON diff", value: "json" }
1885
+ ]
1886
+ }
1887
+ ),
1888
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1889
+ import_antd2.Popconfirm,
1890
+ {
1891
+ title: "Restore this version?",
1892
+ description: "A new revision will be logged so you can undo.",
1893
+ onConfirm: onRestore,
1894
+ okText: "Restore",
1895
+ okButtonProps: { danger: true },
1896
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Button, { danger: true, icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.RollbackOutlined, {}), loading: restoring, children: "Restore" })
1897
+ }
1898
+ )
1899
+ ] }),
1900
+ children: [
1901
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Paragraph, { type: "secondary", style: { fontSize: 12, marginBottom: 12 }, children: [
1902
+ "Comparing ",
1903
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { strong: true, children: "current" }),
1904
+ " (left) \u2192 ",
1905
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { strong: true, children: "this revision" }),
1906
+ " (right). Restore replaces the current page with the right side."
1907
+ ] }),
1908
+ view === "summary" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SummaryView, { summary }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(JsonDiffView, { diff: jsonDiff })
1909
+ ]
1910
+ }
1911
+ );
1912
+ }
1913
+ function SummaryView({ summary }) {
1914
+ if (!summary.length) {
1915
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Alert, { type: "info", showIcon: true, message: "No differences \u2014 this revision matches the current page." });
1916
+ }
1917
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Space, { direction: "vertical", size: 10, style: { width: "100%" }, children: summary.map((row) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Card, { size: "small", styles: { body: { padding: 12 } }, children: [
1918
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }, children: [
1919
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_antd2.Space, { children: [
1920
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_icons2.EditOutlined, { style: { color: "var(--tps-primary)" } }),
1921
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text2, { code: true, style: { fontSize: 12 }, children: row.path })
1922
+ ] }),
1923
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_antd2.Tag, { color: row.kind === "changed" ? "gold" : row.kind === "added" ? "green" : "red", children: row.kind })
1924
+ ] }),
1925
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1926
+ "div",
1927
+ {
1928
+ style: {
1929
+ display: "grid",
1930
+ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
1931
+ gap: 10
1932
+ },
1933
+ children: [
1934
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DiffCell, { label: "Current", value: row.current, kind: "current" }),
1935
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DiffCell, { label: "This revision", value: row.next, kind: "next" })
1936
+ ]
1937
+ }
1938
+ )
1939
+ ] }, row.path)) });
1940
+ }
1941
+ function DiffCell({ label, value, kind }) {
1942
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1943
+ "div",
1944
+ {
1945
+ style: {
1946
+ background: kind === "current" ? "rgba(220,38,38,0.06)" : "rgba(5,150,105,0.06)",
1947
+ border: "1px solid var(--tps-line)",
1948
+ borderRadius: 6,
1949
+ padding: 8
1950
+ },
1951
+ children: [
1952
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 11, color: "var(--tps-muted)", marginBottom: 4 }, children: label }),
1953
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1954
+ "pre",
1955
+ {
1956
+ style: {
1957
+ margin: 0,
1958
+ fontSize: 12,
1959
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1960
+ whiteSpace: "pre-wrap",
1961
+ wordBreak: "break-word",
1962
+ color: "var(--tps-ink)"
1963
+ },
1964
+ children: formatScalar(value)
1965
+ }
1966
+ )
1967
+ ]
1968
+ }
1969
+ );
1970
+ }
1971
+ function JsonDiffView({ diff }) {
1972
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1973
+ "div",
1974
+ {
1975
+ style: {
1976
+ background: "#0F172A",
1977
+ borderRadius: 6,
1978
+ fontSize: 12,
1979
+ lineHeight: 1.6,
1980
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1981
+ maxHeight: "60vh",
1982
+ overflow: "auto",
1983
+ padding: 14,
1984
+ // Constrain to parent so long lines wrap instead of forcing the
1985
+ // History tab horizontally (prior bug: the diff blew past the card).
1986
+ maxWidth: "100%"
1987
+ },
1988
+ children: diff.map((part, i) => {
1989
+ const bg = part.added ? "rgba(16,185,129,0.18)" : part.removed ? "rgba(239,68,68,0.20)" : "transparent";
1990
+ const color = part.added ? "#86efac" : part.removed ? "#fca5a5" : "#cbd5e1";
1991
+ const prefix = part.added ? "+ " : part.removed ? "- " : " ";
1992
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { background: bg, color }, children: part.value.split("\n").filter((l, idx, arr) => !(l === "" && idx === arr.length - 1)).map((line, j) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1993
+ "div",
1994
+ {
1995
+ style: {
1996
+ padding: "0 6px",
1997
+ whiteSpace: "pre-wrap",
1998
+ wordBreak: "break-word",
1999
+ overflowWrap: "anywhere"
2000
+ },
2001
+ children: [
2002
+ prefix,
2003
+ line
2004
+ ]
2005
+ },
2006
+ j
2007
+ )) }, i);
2008
+ })
2009
+ }
2010
+ );
2011
+ }
2012
+ function makeJsonDiff(left, right) {
2013
+ const a = JSON.stringify(snapshot(left), null, 2);
2014
+ const b = JSON.stringify(snapshot(right), null, 2);
2015
+ return (0, import_diff.diffLines)(a, b);
2016
+ }
2017
+ function snapshot(p) {
2018
+ return {
2019
+ title: p?.title || "",
2020
+ published: p?.published !== false,
2021
+ seo: p?.seo || {},
2022
+ content: p?.content || {}
2023
+ };
2024
+ }
2025
+ function fieldChangeSummary(left, right) {
2026
+ const out = [];
2027
+ const a = snapshot(left);
2028
+ const b = snapshot(right);
2029
+ if (a.title !== b.title) {
2030
+ out.push({ path: "title", kind: classify(a.title, b.title), current: a.title, next: b.title });
2031
+ }
2032
+ if (a.published !== b.published) {
2033
+ out.push({
2034
+ path: "published",
2035
+ kind: "changed",
2036
+ current: a.published,
2037
+ next: b.published
2038
+ });
2039
+ }
2040
+ for (const k of unionKeys(a.seo, b.seo)) {
2041
+ const av = a.seo[k];
2042
+ const bv = b.seo[k];
2043
+ if (!equal(av, bv)) {
2044
+ out.push({ path: `seo.${k}`, kind: classify(av, bv), current: av, next: bv });
2045
+ }
2046
+ }
2047
+ for (const k of unionKeys(a.content, b.content)) {
2048
+ const av = a.content[k];
2049
+ const bv = b.content[k];
2050
+ if (!equal(av, bv)) {
2051
+ out.push({ path: `content.${k}`, kind: classify(av, bv), current: av, next: bv });
2052
+ }
2053
+ }
2054
+ return out;
2055
+ }
2056
+ function unionKeys(a, b) {
2057
+ const s = /* @__PURE__ */ new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
2058
+ return Array.from(s).sort();
2059
+ }
2060
+ function classify(a, b) {
2061
+ const aEmpty = isEmpty(a);
2062
+ const bEmpty = isEmpty(b);
2063
+ if (aEmpty && !bEmpty) return "added";
2064
+ if (!aEmpty && bEmpty) return "removed";
2065
+ return "changed";
2066
+ }
2067
+ function isEmpty(v) {
2068
+ if (v == null) return true;
2069
+ if (typeof v === "string") return v.trim() === "";
2070
+ if (Array.isArray(v)) return v.length === 0;
2071
+ if (typeof v === "object") return Object.keys(v).length === 0;
2072
+ return false;
2073
+ }
2074
+ function equal(a, b) {
2075
+ if (a === b) return true;
2076
+ if (a == null || b == null) return a === b;
2077
+ if (typeof a !== typeof b) return false;
2078
+ if (Array.isArray(a) || Array.isArray(b) || typeof a === "object") {
2079
+ return JSON.stringify(a) === JSON.stringify(b);
2080
+ }
2081
+ return false;
2082
+ }
2083
+ function formatScalar(v) {
2084
+ if (v === void 0 || v === null) return "\u2014";
2085
+ if (typeof v === "string") return v || "\u2014";
2086
+ if (typeof v === "boolean") return v ? "true" : "false";
2087
+ if (Array.isArray(v) && v.every((x) => typeof x === "string")) {
2088
+ return v.length ? v.join("\n") : "\u2014";
2089
+ }
2090
+ return JSON.stringify(v, null, 2);
2091
+ }
2092
+ function abbreviateEmail(email) {
2093
+ if (!email) return "";
2094
+ const [name, domain] = email.split("@");
2095
+ if (!domain) return email;
2096
+ return `${name}@${domain.split(".")[0]}`;
2097
+ }
2098
+
2099
+ // src/PageStudioForm.jsx
2100
+ var import_jsx_runtime3 = require("react/jsx-runtime");
2101
+ var { Text: Text3, Paragraph: Paragraph2 } = import_antd3.Typography;
2102
+ function DefaultLink({ href, target, children, ...rest }) {
2103
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2104
+ "a",
2105
+ {
2106
+ href,
2107
+ target,
2108
+ rel: target === "_blank" ? "noopener noreferrer" : void 0,
2109
+ ...rest,
2110
+ children
2111
+ }
2112
+ );
2113
+ }
2114
+ function notConfigured2(name) {
2115
+ return () => {
2116
+ throw new Error(
2117
+ `@techrox/page-studio-form: missing required adapter method \`${name}\`.`
2118
+ );
2119
+ };
2120
+ }
2121
+ var FormCtx = (0, import_react4.createContext)({ LinkComponent: DefaultLink, uploadMedia: void 0 });
2122
+ var useFormCtx = () => (0, import_react4.useContext)(FormCtx);
2123
+ function PageStudioForm({
2124
+ pageKey,
2125
+ initialPage,
2126
+ loadError,
2127
+ schema,
2128
+ contentDefaults = {},
2129
+ livePath,
2130
+ homeHref = "/admin/pages",
2131
+ homeLabel = "All pages",
2132
+ LinkComponent = DefaultLink,
2133
+ adapter = {},
2134
+ onSaved,
2135
+ onDeleted,
2136
+ onRestored
2137
+ }) {
2138
+ const {
2139
+ savePage = notConfigured2("savePage"),
2140
+ deletePage = notConfigured2("deletePage"),
2141
+ loadHistory,
2142
+ restoreRevision,
2143
+ uploadMedia
2144
+ } = adapter;
2145
+ const { message } = import_antd3.App.useApp();
2146
+ const [form] = import_antd3.Form.useForm();
2147
+ const [pending, startTransition] = (0, import_react4.useTransition)();
2148
+ const [savedAt, setSavedAt] = (0, import_react4.useState)(initialPage?.updated_at || null);
2149
+ const [previewKey, setPreviewKey] = (0, import_react4.useState)(0);
2150
+ const defaults = contentDefaults;
2151
+ const initialValues = {
2152
+ title: initialPage?.title || "",
2153
+ published: initialPage?.published ?? true,
2154
+ seo_title: initialPage?.seo?.title || "",
2155
+ seo_description: initialPage?.seo?.description || "",
2156
+ seo_og_image: initialPage?.seo?.og_image || "",
2157
+ seo_noindex: initialPage?.seo?.noindex || false,
2158
+ content: { ...defaults, ...initialPage?.content || {} }
2159
+ };
2160
+ (0, import_react4.useEffect)(() => {
2161
+ form.setFieldsValue(initialValues);
2162
+ setSavedAt(initialPage?.updated_at || null);
2163
+ setPreviewKey((k) => k + 1);
2164
+ }, [initialPage?.updated_at]);
2165
+ const onFinish = (values) => {
2166
+ const fallback = (v, prev) => v === void 0 ? prev : v;
2167
+ const payload = {
2168
+ title: (fallback(values.title, initialPage?.title) || "").trim(),
2169
+ seo: {
2170
+ title: fallback(values.seo_title, initialPage?.seo?.title) || "",
2171
+ description: fallback(values.seo_description, initialPage?.seo?.description) || "",
2172
+ og_image: fallback(values.seo_og_image, initialPage?.seo?.og_image) || "",
2173
+ noindex: !!fallback(values.seo_noindex, initialPage?.seo?.noindex)
2174
+ },
2175
+ content: pruneContent(values.content || {}),
2176
+ published: !!fallback(values.published, initialPage?.published ?? true)
2177
+ };
2178
+ startTransition(async () => {
2179
+ try {
2180
+ const result = await savePage(pageKey, payload);
2181
+ const page = result?.page;
2182
+ message.success("Saved. Public page is being revalidated.");
2183
+ if (page?.updated_at) setSavedAt(page.updated_at);
2184
+ onSaved?.(page);
2185
+ } catch (err) {
2186
+ message.error(err.message || "Failed to save.");
2187
+ }
2188
+ });
2189
+ };
2190
+ const onDelete = () => {
2191
+ startTransition(async () => {
2192
+ try {
2193
+ await deletePage(pageKey);
2194
+ message.success("Override removed. Public page now uses the static defaults.");
2195
+ onDeleted?.();
2196
+ } catch (err) {
2197
+ message.error(err.message || "Failed to delete.");
2198
+ }
2199
+ });
2200
+ };
2201
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FormCtx.Provider, { value: { LinkComponent, uploadMedia }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { padding: 24, maxWidth: 1280 }, children: [
2202
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2203
+ LinkComponent,
2204
+ {
2205
+ href: homeHref,
2206
+ style: {
2207
+ display: "inline-flex",
2208
+ alignItems: "center",
2209
+ gap: 6,
2210
+ fontSize: 13,
2211
+ color: "var(--tps-muted)",
2212
+ marginBottom: 16
2213
+ },
2214
+ children: [
2215
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.ArrowLeftOutlined, {}),
2216
+ " ",
2217
+ homeLabel
2218
+ ]
2219
+ }
2220
+ ),
2221
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2222
+ import_antd3.Form,
2223
+ {
2224
+ form,
2225
+ layout: "vertical",
2226
+ onFinish,
2227
+ initialValues,
2228
+ children: [
2229
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2230
+ "div",
2231
+ {
2232
+ style: {
2233
+ display: "flex",
2234
+ justifyContent: "space-between",
2235
+ alignItems: "flex-start",
2236
+ gap: 16,
2237
+ flexWrap: "wrap"
2238
+ },
2239
+ children: [
2240
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
2241
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { className: "tps-h3", style: { marginBottom: 4 }, children: initialPage?.title || pageKey }),
2242
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd3.Space, { size: 8, wrap: true, children: [
2243
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text3, { type: "secondary", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text3, { code: true, children: pageKey }) }),
2244
+ savedAt && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text3, { type: "secondary", style: { fontSize: 12 }, children: [
2245
+ "Last saved ",
2246
+ new Date(savedAt).toLocaleString()
2247
+ ] })
2248
+ ] })
2249
+ ] }),
2250
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd3.Space, { wrap: true, align: "center", children: [
2251
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2252
+ import_antd3.Form.Item,
2253
+ {
2254
+ name: "published",
2255
+ valuePropName: "checked",
2256
+ style: { marginBottom: 0 },
2257
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PublishToggle, {})
2258
+ }
2259
+ ),
2260
+ livePath && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LinkComponent, { href: livePath, target: "_blank", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Button, { icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.EyeOutlined, {}), children: "View live" }) }),
2261
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2262
+ import_antd3.Popconfirm,
2263
+ {
2264
+ title: "Remove this override?",
2265
+ description: "The public page will fall back to the static defaults.",
2266
+ onConfirm: onDelete,
2267
+ okText: "Remove",
2268
+ cancelText: "Cancel",
2269
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Button, { danger: true, icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.DeleteOutlined, {}), disabled: pending, children: "Remove override" })
2270
+ }
2271
+ ),
2272
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2273
+ import_antd3.Button,
2274
+ {
2275
+ type: "primary",
2276
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.SaveOutlined, {}),
2277
+ loading: pending,
2278
+ onClick: () => form.submit(),
2279
+ children: "Save"
2280
+ }
2281
+ )
2282
+ ] })
2283
+ ]
2284
+ }
2285
+ ),
2286
+ loadError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2287
+ import_antd3.Alert,
2288
+ {
2289
+ type: "error",
2290
+ showIcon: true,
2291
+ message: "Could not load page",
2292
+ description: loadError,
2293
+ style: { margin: "16px 0" }
2294
+ }
2295
+ ),
2296
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2297
+ import_antd3.Tabs,
2298
+ {
2299
+ defaultActiveKey: "content",
2300
+ style: { marginTop: 24 },
2301
+ items: [
2302
+ {
2303
+ key: "content",
2304
+ label: "Content",
2305
+ forceRender: true,
2306
+ children: schema ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SchemaEditor, { schema, defaults }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2307
+ import_antd3.Alert,
2308
+ {
2309
+ type: "info",
2310
+ showIcon: true,
2311
+ message: "No structured content for this page key.",
2312
+ description: "SEO can still be edited on the SEO tab."
2313
+ }
2314
+ )
2315
+ },
2316
+ {
2317
+ key: "preview",
2318
+ label: "Live preview",
2319
+ children: livePath ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LivePreview, { path: livePath, previewKey }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Alert, { type: "info", message: "No public path mapped for this key." })
2320
+ },
2321
+ {
2322
+ key: "history",
2323
+ label: "History",
2324
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2325
+ HistoryPanel,
2326
+ {
2327
+ pageKey,
2328
+ currentPage: initialPage,
2329
+ loadHistory,
2330
+ restoreRevision,
2331
+ onRestored
2332
+ }
2333
+ )
2334
+ },
2335
+ {
2336
+ key: "seo",
2337
+ label: "SEO",
2338
+ forceRender: true,
2339
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SeoFields, {})
2340
+ }
2341
+ ]
2342
+ }
2343
+ ),
2344
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Form.Item, { name: "title", hidden: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input, {}) })
2345
+ ]
2346
+ }
2347
+ )
2348
+ ] }) });
2349
+ }
2350
+ function PublishToggle({ value, onChange }) {
2351
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd3.Space, { size: 6, children: [
2352
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Switch, { checked: !!value, onChange }),
2353
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text3, { style: { fontSize: 12, color: "var(--tps-muted)" }, children: value ? "Published" : "Draft" })
2354
+ ] });
2355
+ }
2356
+ function pruneContent(content) {
2357
+ const out = {};
2358
+ for (const [k, v] of Object.entries(content)) {
2359
+ if (v === void 0 || v === null) continue;
2360
+ if (typeof v === "string" && v.trim() === "") continue;
2361
+ if (Array.isArray(v) && v.length === 0) continue;
2362
+ out[k] = v;
2363
+ }
2364
+ return out;
2365
+ }
2366
+ function SeoFields() {
2367
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd3.Card, { size: "small", children: [
2368
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2369
+ import_antd3.Form.Item,
2370
+ {
2371
+ label: "SEO title",
2372
+ name: "seo_title",
2373
+ extra: "Used in <title> and Open Graph. Leave blank to use the static default.",
2374
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input, { maxLength: 70, showCount: true })
2375
+ }
2376
+ ),
2377
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2378
+ import_antd3.Form.Item,
2379
+ {
2380
+ label: "Meta description",
2381
+ name: "seo_description",
2382
+ extra: "Recommended 140\u2013160 characters.",
2383
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input.TextArea, { rows: 3, maxLength: 170, showCount: true })
2384
+ }
2385
+ ),
2386
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Form.Item, { label: "Open Graph image URL", name: "seo_og_image", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input, { placeholder: "https://pagestudio.dev/og-image.png" }) }),
2387
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Form.Item, { label: "Hide from search engines", name: "seo_noindex", valuePropName: "checked", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Switch, {}) })
2388
+ ] });
2389
+ }
2390
+ function SchemaEditor({ schema, defaults }) {
2391
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Space, { direction: "vertical", size: 20, style: { width: "100%" }, children: schema.map((section) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionBlock, { section, defaults }, section.title)) });
2392
+ }
2393
+ function SectionBlock({ section, defaults }) {
2394
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2395
+ import_antd3.Card,
2396
+ {
2397
+ size: "small",
2398
+ title: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2399
+ "span",
2400
+ {
2401
+ style: {
2402
+ fontSize: 11,
2403
+ fontWeight: 700,
2404
+ letterSpacing: 1.5,
2405
+ color: "var(--tps-accent-dark)"
2406
+ },
2407
+ children: section.title.toUpperCase()
2408
+ }
2409
+ ),
2410
+ children: [
2411
+ section.help && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Paragraph2, { type: "secondary", style: { fontSize: 13, marginTop: 0 }, children: section.help }),
2412
+ section.fields.map((f) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldRenderer, { field: f, parent: ["content"], defaults }, f.name))
2413
+ ]
2414
+ }
2415
+ );
2416
+ }
2417
+ function FieldRenderer({ field, parent, defaults }) {
2418
+ const namePath = [...parent, field.name];
2419
+ const placeholder = placeholderFor(field, defaults?.[field.name]);
2420
+ if (field.type === "text") {
2421
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Form.Item, { label: field.label, name: namePath, extra: field.help, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input, { placeholder }) });
2422
+ }
2423
+ if (field.type === "textarea" || field.type === "html-text") {
2424
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Form.Item, { label: field.label, name: namePath, extra: field.help, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input.TextArea, { rows: field.rows || 3, placeholder }) });
2425
+ }
2426
+ if (field.type === "richtext") {
2427
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2428
+ import_antd3.Form.Item,
2429
+ {
2430
+ label: field.label || void 0,
2431
+ name: namePath,
2432
+ extra: field.help,
2433
+ valuePropName: "value",
2434
+ trigger: "onChange",
2435
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RichTextField, { placeholder })
2436
+ }
2437
+ );
2438
+ }
2439
+ if (field.type === "list") {
2440
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2441
+ import_antd3.Form.Item,
2442
+ {
2443
+ label: field.label,
2444
+ name: namePath,
2445
+ extra: field.help,
2446
+ getValueFromEvent: (e) => e.target.value.split("\n").map((s) => s.trim()).filter(Boolean),
2447
+ getValueProps: (v) => ({ value: Array.isArray(v) ? v.join("\n") : v || "" }),
2448
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input.TextArea, { rows: field.rows || 4, placeholder })
2449
+ }
2450
+ );
2451
+ }
2452
+ if (field.type === "list-csv") {
2453
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2454
+ import_antd3.Form.Item,
2455
+ {
2456
+ label: field.label,
2457
+ name: namePath,
2458
+ extra: field.help,
2459
+ getValueFromEvent: (e) => e.target.value.split(",").map((s) => s.trim()).filter(Boolean),
2460
+ getValueProps: (v) => ({ value: Array.isArray(v) ? v.join(", ") : v || "" }),
2461
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Input, { placeholder })
2462
+ }
2463
+ );
2464
+ }
2465
+ if (field.type === "repeater") {
2466
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Repeater, { field, namePath, defaults: defaults?.[field.name] });
2467
+ }
2468
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2469
+ import_antd3.Alert,
2470
+ {
2471
+ type: "warning",
2472
+ showIcon: true,
2473
+ message: `Unknown field type "${field.type}" for ${field.name}`,
2474
+ style: { marginBottom: 12 }
2475
+ }
2476
+ );
2477
+ }
2478
+ function Repeater({ field, namePath, defaults }) {
2479
+ const sensors = (0, import_core2.useSensors)(
2480
+ (0, import_core2.useSensor)(import_core2.PointerSensor, { activationConstraint: { distance: 5 } }),
2481
+ (0, import_core2.useSensor)(import_core2.KeyboardSensor, { coordinateGetter: import_sortable.sortableKeyboardCoordinates })
2482
+ );
2483
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Form.List, { name: namePath, children: (fields, { add, remove, move }) => {
2484
+ const ids = fields.map((f) => f.key);
2485
+ const onDragEnd = (e) => {
2486
+ const { active, over } = e;
2487
+ if (!over || active.id === over.id) return;
2488
+ const from = ids.indexOf(active.id);
2489
+ const to = ids.indexOf(over.id);
2490
+ if (from < 0 || to < 0) return;
2491
+ move(from, to);
2492
+ };
2493
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
2494
+ field.label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text3, { strong: true, style: { display: "block", marginBottom: 8 }, children: field.label }),
2495
+ field.help && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Paragraph2, { type: "secondary", style: { fontSize: 13, marginTop: 0 }, children: field.help }),
2496
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2497
+ import_core2.DndContext,
2498
+ {
2499
+ sensors,
2500
+ collisionDetection: import_core2.closestCenter,
2501
+ onDragEnd,
2502
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sortable.SortableContext, { items: ids, strategy: import_sortable.verticalListSortingStrategy, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Space, { direction: "vertical", size: 12, style: { width: "100%" }, children: fields.map((row, idx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2503
+ SortableRow,
2504
+ {
2505
+ id: row.key,
2506
+ field,
2507
+ row,
2508
+ idx,
2509
+ namePath,
2510
+ defaults: defaults?.[idx] || null,
2511
+ onRemove: () => remove(idx)
2512
+ },
2513
+ row.key
2514
+ )) }) })
2515
+ }
2516
+ ),
2517
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
2518
+ import_antd3.Button,
2519
+ {
2520
+ type: "dashed",
2521
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.PlusOutlined, {}),
2522
+ onClick: () => add(buildEmptyItem(field)),
2523
+ block: true,
2524
+ style: { marginTop: 12 },
2525
+ children: [
2526
+ "Add ",
2527
+ field.itemLabel ? singular(field.itemLabel(fields.length)) : "item"
2528
+ ]
2529
+ }
2530
+ )
2531
+ ] });
2532
+ } });
2533
+ }
2534
+ function SortableRow({ id, field, row, idx, namePath, defaults, onRemove }) {
2535
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = (0, import_sortable.useSortable)({ id });
2536
+ const style = {
2537
+ transform: import_utilities.CSS.Transform.toString(transform),
2538
+ transition,
2539
+ opacity: isDragging ? 0.6 : 1,
2540
+ zIndex: isDragging ? 5 : "auto"
2541
+ };
2542
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: setNodeRef, style, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2543
+ import_antd3.Card,
2544
+ {
2545
+ size: "small",
2546
+ style: {
2547
+ background: "var(--tps-bg-soft)",
2548
+ boxShadow: isDragging ? "0 8px 24px rgba(0,0,0,0.12)" : void 0
2549
+ },
2550
+ title: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd3.Space, { children: [
2551
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2552
+ import_antd3.Button,
2553
+ {
2554
+ size: "small",
2555
+ type: "text",
2556
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.HolderOutlined, {}),
2557
+ style: { cursor: "grab", touchAction: "none" },
2558
+ ...attributes,
2559
+ ...listeners
2560
+ }
2561
+ ),
2562
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text3, { strong: true, style: { fontSize: 13 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RepeaterItemLabel, { field, index: idx, listPath: namePath }) })
2563
+ ] }),
2564
+ extra: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2565
+ import_antd3.Button,
2566
+ {
2567
+ size: "small",
2568
+ type: "text",
2569
+ danger: true,
2570
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.MinusCircleOutlined, {}),
2571
+ onClick: onRemove,
2572
+ children: "Remove"
2573
+ }
2574
+ ),
2575
+ children: field.itemFields.map((sub) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2576
+ FieldRenderer,
2577
+ {
2578
+ field: sub,
2579
+ parent: [...namePath, row.name],
2580
+ defaults
2581
+ },
2582
+ sub.name
2583
+ ))
2584
+ }
2585
+ ) });
2586
+ }
2587
+ function RepeaterItemLabel({ field, index, listPath }) {
2588
+ const item = import_antd3.Form.useWatch([...listPath, index]);
2589
+ const text = field.itemLabel ? field.itemLabel(index, item) : `Item ${index + 1}`;
2590
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: text });
2591
+ }
2592
+ function buildEmptyItem(field) {
2593
+ const empty = {};
2594
+ for (const sub of field.itemFields) {
2595
+ if (sub.type === "list" || sub.type === "list-csv") empty[sub.name] = [];
2596
+ else empty[sub.name] = "";
2597
+ }
2598
+ return empty;
2599
+ }
2600
+ function singular(label) {
2601
+ return String(label).replace(/\s+\d+$/, "").toLowerCase() || "item";
2602
+ }
2603
+ function placeholderFor(field, defaultValue) {
2604
+ if (defaultValue == null) return void 0;
2605
+ if (Array.isArray(defaultValue)) {
2606
+ if (field.type === "list-csv") return defaultValue.join(", ");
2607
+ return defaultValue.join("\n");
2608
+ }
2609
+ if (typeof defaultValue === "string") {
2610
+ return defaultValue.length > 100 ? defaultValue.slice(0, 100) + "\u2026" : defaultValue;
2611
+ }
2612
+ return void 0;
2613
+ }
2614
+ var VIEWPORTS = {
2615
+ desktop: { width: "100%", label: "Desktop" },
2616
+ tablet: { width: 820, label: "Tablet" },
2617
+ mobile: { width: 390, label: "Mobile" }
2618
+ };
2619
+ function LivePreview({ path, previewKey }) {
2620
+ const [device, setDevice] = (0, import_react4.useState)("desktop");
2621
+ const [version, setVersion] = (0, import_react4.useState)(0);
2622
+ const iframeRef = (0, import_react4.useRef)(null);
2623
+ (0, import_react4.useEffect)(() => {
2624
+ setVersion((v) => v + 1);
2625
+ }, [previewKey]);
2626
+ const reload = () => setVersion((v) => v + 1);
2627
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
2628
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12, gap: 12 }, children: [
2629
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2630
+ import_antd3.Segmented,
2631
+ {
2632
+ value: device,
2633
+ onChange: setDevice,
2634
+ options: [
2635
+ { value: "desktop", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.DesktopOutlined, {}), label: "Desktop" },
2636
+ { value: "tablet", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.TabletOutlined, {}), label: "Tablet" },
2637
+ { value: "mobile", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.MobileOutlined, {}), label: "Mobile" }
2638
+ ]
2639
+ }
2640
+ ),
2641
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_antd3.Space, { children: [
2642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text3, { type: "secondary", style: { fontSize: 12 }, children: [
2643
+ "Showing ",
2644
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text3, { code: true, children: path })
2645
+ ] }),
2646
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Button, { size: "small", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.ReloadOutlined, {}), onClick: reload, children: "Reload" }),
2647
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PreviewLink, { href: path })
2648
+ ] })
2649
+ ] }),
2650
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2651
+ "div",
2652
+ {
2653
+ style: {
2654
+ background: "#0F172A",
2655
+ borderRadius: "var(--tps-radius)",
2656
+ padding: 12,
2657
+ display: "flex",
2658
+ justifyContent: "center"
2659
+ },
2660
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2661
+ "div",
2662
+ {
2663
+ style: {
2664
+ width: VIEWPORTS[device].width,
2665
+ maxWidth: "100%",
2666
+ transition: "width 200ms ease",
2667
+ background: "#fff",
2668
+ borderRadius: 8,
2669
+ overflow: "hidden",
2670
+ boxShadow: "0 12px 36px -8px rgba(0,0,0,0.45)"
2671
+ },
2672
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2673
+ "iframe",
2674
+ {
2675
+ ref: iframeRef,
2676
+ src: `${path}?_preview=${version}`,
2677
+ title: "Live preview",
2678
+ style: {
2679
+ width: "100%",
2680
+ height: "70vh",
2681
+ border: 0,
2682
+ display: "block"
2683
+ }
2684
+ },
2685
+ version
2686
+ )
2687
+ }
2688
+ )
2689
+ }
2690
+ )
2691
+ ] });
2692
+ }
2693
+ function RichTextField({ value, onChange, placeholder }) {
2694
+ const { uploadMedia } = useFormCtx();
2695
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2696
+ RichText,
2697
+ {
2698
+ value,
2699
+ onChange,
2700
+ placeholder,
2701
+ uploadMedia
2702
+ }
2703
+ );
2704
+ }
2705
+ function PreviewLink({ href }) {
2706
+ const { LinkComponent } = useFormCtx();
2707
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LinkComponent, { href, target: "_blank", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_antd3.Button, { size: "small", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_icons3.EyeOutlined, {}), children: "Open in new tab" }) });
2708
+ }
2709
+ // Annotate the CommonJS export names for ESM import in node:
2710
+ 0 && (module.exports = {
2711
+ Callout,
2712
+ CiqImage,
2713
+ CiqTableCell,
2714
+ CiqTableHeader,
2715
+ Column,
2716
+ Columns,
2717
+ HistoryPanel,
2718
+ PageStudioForm,
2719
+ RichText,
2720
+ ShareBlock,
2721
+ SubscribeBlock
2722
+ });
2723
+ //# sourceMappingURL=index.cjs.map