@morphika/andami 0.2.4 → 0.2.6
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/components/blocks/ProjectGridBlockRenderer.tsx +50 -29
- package/components/blocks/TextBlockRenderer.tsx +27 -2
- package/components/blocks/TypewriterRichText.tsx +7 -1
- package/components/builder/CanvasToolbar.tsx +11 -17
- package/components/builder/SectionV2Canvas.tsx +3 -2
- package/components/builder/SectionV2Column.tsx +7 -3
- package/components/builder/SortableBlock.tsx +7 -6
- package/components/builder/SortableRow.tsx +36 -11
- package/components/builder/editors/TextBlockEditor.tsx +2 -6
- package/components/builder/live-preview/LiveTextEditor.tsx +117 -198
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +451 -0
- package/components/builder/live-preview/RichTextEditor.tsx +223 -0
- package/components/builder/live-preview/index.ts +2 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +4 -0
- package/lib/builder/types.ts +10 -0
- package/lib/editor/index.ts +2 -0
- package/lib/editor/portableToTiptap.ts +156 -0
- package/lib/editor/tiptapToPortable.ts +238 -0
- package/package.json +223 -212
- package/sanity/schemas/blocks/textBlock.ts +13 -0
|
@@ -1,198 +1,117 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Resolve
|
|
24
|
-
function
|
|
25
|
-
if (
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
119
|
-
debounceRef.current = setTimeout(() => {
|
|
120
|
-
commitText();
|
|
121
|
-
debounceRef.current = null;
|
|
122
|
-
}, 500);
|
|
123
|
-
}, [commitText]);
|
|
124
|
-
|
|
125
|
-
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
126
|
-
// Stop propagation so builder shortcuts don't fire while typing
|
|
127
|
-
if (!e.metaKey && !e.ctrlKey) {
|
|
128
|
-
e.stopPropagation();
|
|
129
|
-
}
|
|
130
|
-
// Allow Ctrl+S, Ctrl+Z, Ctrl+Shift+Z to pass through
|
|
131
|
-
if ((e.metaKey || e.ctrlKey) && (e.key === "s" || e.key === "z" || e.key === "Z" || e.key === "y")) {
|
|
132
|
-
// Don't stop propagation for these
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
|
-
const cols = block.columns && block.columns > 1 ? block.columns : undefined;
|
|
138
|
-
|
|
139
|
-
const computedStyle: React.CSSProperties = {
|
|
140
|
-
fontSize: resolveTextFontSize(style.fontSize),
|
|
141
|
-
fontWeight: resolveTextFontWeight(style.fontWeight),
|
|
142
|
-
textAlign: style.alignment || "left",
|
|
143
|
-
color: style.color || pageTextColor,
|
|
144
|
-
opacity: style.opacity ?? 1,
|
|
145
|
-
lineHeight: style.lineHeight || "1.6",
|
|
146
|
-
letterSpacing: style.letterSpacing || "normal",
|
|
147
|
-
maxWidth: style.maxWidth || "none",
|
|
148
|
-
textTransform: style.textTransform || "none",
|
|
149
|
-
fontFamily: "inherit",
|
|
150
|
-
outline: "none",
|
|
151
|
-
whiteSpace: "pre-wrap",
|
|
152
|
-
wordBreak: "break-word",
|
|
153
|
-
minHeight: "1em",
|
|
154
|
-
// Multi-column layout: gap inherits global grid gutter
|
|
155
|
-
...(cols ? {
|
|
156
|
-
columnCount: cols,
|
|
157
|
-
columnGap: "var(--grid-gutter, 24px)",
|
|
158
|
-
} : {}),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Preview mode or read-only frame: static render
|
|
162
|
-
if (previewMode || !editable) {
|
|
163
|
-
if (!plainText) {
|
|
164
|
-
return (
|
|
165
|
-
<p className="text-neutral-400 italic text-sm py-4 text-center">
|
|
166
|
-
Empty text block
|
|
167
|
-
</p>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
return (
|
|
171
|
-
<div style={computedStyle}>
|
|
172
|
-
{plainText.split("\n").map((line, i) => (
|
|
173
|
-
<p key={i} className={i > 0 ? "mt-3" : ""}>
|
|
174
|
-
{line || "\u00A0"}
|
|
175
|
-
</p>
|
|
176
|
-
))}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Design mode: contentEditable inline editing
|
|
182
|
-
return (
|
|
183
|
-
<div
|
|
184
|
-
ref={editableRef}
|
|
185
|
-
contentEditable
|
|
186
|
-
suppressContentEditableWarning
|
|
187
|
-
onFocus={handleFocus}
|
|
188
|
-
onBlur={handleBlur}
|
|
189
|
-
onInput={handleInput}
|
|
190
|
-
onKeyDown={handleKeyDown}
|
|
191
|
-
onCompositionStart={() => { isComposingRef.current = true; }}
|
|
192
|
-
onCompositionEnd={() => { isComposingRef.current = false; commitText(); }}
|
|
193
|
-
style={computedStyle}
|
|
194
|
-
data-placeholder="Type something..."
|
|
195
|
-
className={`${!plainText && !isSelected ? "before:content-[attr(data-placeholder)] before:text-neutral-400 before:italic before:pointer-events-none" : ""}`}
|
|
196
|
-
/>
|
|
197
|
-
);
|
|
198
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LiveTextEditor.tsx
|
|
5
|
+
*
|
|
6
|
+
* Entry point for text block editing in the visual builder.
|
|
7
|
+
* Delegates to RichTextEditor (Tiptap-based) for editable mode,
|
|
8
|
+
* and renders a static rich text preview for read-only / preview modes
|
|
9
|
+
* using Sanity's PortableText renderer (preserves bold, italic, links, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Preserves the original API surface (default export + usePageTextColor hook)
|
|
12
|
+
* so existing imports continue to work.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { PortableText, type PortableTextComponents } from "next-sanity";
|
|
16
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
17
|
+
import RichTextEditor, { usePageTextColor } from "./RichTextEditor";
|
|
18
|
+
import type { TextBlock } from "../../../lib/sanity/types";
|
|
19
|
+
|
|
20
|
+
// Re-export the hook so barrel imports don't break
|
|
21
|
+
export { usePageTextColor };
|
|
22
|
+
|
|
23
|
+
/** Resolve fontSize: supports new numeric px and legacy string enum */
|
|
24
|
+
function resolveTextFontSize(fontSize?: number | string): string {
|
|
25
|
+
if (typeof fontSize === "number") return `${fontSize}px`;
|
|
26
|
+
const legacyMap: Record<string, string> = {
|
|
27
|
+
small: "12px", base: "14px", large: "20px",
|
|
28
|
+
xl: "24px", "2xl": "32px", "3xl": "48px",
|
|
29
|
+
};
|
|
30
|
+
return legacyMap[fontSize || "base"] || "14px";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolve fontWeight: supports new string numbers and legacy names */
|
|
34
|
+
function resolveTextFontWeight(fw?: string): number {
|
|
35
|
+
if (!fw) return 400;
|
|
36
|
+
const num = parseInt(fw, 10);
|
|
37
|
+
if (!isNaN(num)) return num;
|
|
38
|
+
if (fw === "bold") return 700;
|
|
39
|
+
if (fw === "medium") return 500;
|
|
40
|
+
return 400;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Portable Text components for builder preview — links non-navigable */
|
|
44
|
+
const previewRichTextComponents: PortableTextComponents = {
|
|
45
|
+
marks: {
|
|
46
|
+
link: ({ value, children }) => {
|
|
47
|
+
const href = value?.href || "#";
|
|
48
|
+
const blank = value?.blank;
|
|
49
|
+
return (
|
|
50
|
+
<a
|
|
51
|
+
href={href}
|
|
52
|
+
target={blank ? "_blank" : undefined}
|
|
53
|
+
rel={blank ? "noopener noreferrer" : undefined}
|
|
54
|
+
onClick={(e) => e.preventDefault()}
|
|
55
|
+
className="underline decoration-current underline-offset-2 cursor-default"
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</a>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
color: ({ value, children }) => {
|
|
62
|
+
const hex = value?.hex;
|
|
63
|
+
if (!hex) return <>{children}</>;
|
|
64
|
+
return <span style={{ color: hex }}>{children}</span>;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default function LiveTextEditor({ block, editable = false }: { block: TextBlock; editable?: boolean }) {
|
|
70
|
+
const pageTextColor = usePageTextColor();
|
|
71
|
+
const previewMode = useBuilderStore((s) => s.previewMode);
|
|
72
|
+
|
|
73
|
+
const style = block.style || {};
|
|
74
|
+
const cols = block.columns && block.columns > 1 ? block.columns : undefined;
|
|
75
|
+
|
|
76
|
+
const hasText = Array.isArray(block.text) && block.text.length > 0 &&
|
|
77
|
+
block.text.some((blk) => blk?.children?.some((c: { text?: string }) => (c.text || "").length > 0));
|
|
78
|
+
|
|
79
|
+
const computedStyle: React.CSSProperties = {
|
|
80
|
+
fontSize: resolveTextFontSize(style.fontSize),
|
|
81
|
+
fontWeight: resolveTextFontWeight(style.fontWeight),
|
|
82
|
+
textAlign: style.alignment || "left",
|
|
83
|
+
color: style.color || pageTextColor,
|
|
84
|
+
opacity: style.opacity ?? 1,
|
|
85
|
+
lineHeight: style.lineHeight || "1.6",
|
|
86
|
+
letterSpacing: style.letterSpacing || "normal",
|
|
87
|
+
maxWidth: style.maxWidth || "none",
|
|
88
|
+
textTransform: style.textTransform || "none",
|
|
89
|
+
fontFamily: "inherit",
|
|
90
|
+
whiteSpace: "pre-wrap",
|
|
91
|
+
wordBreak: "break-word",
|
|
92
|
+
minHeight: "1em",
|
|
93
|
+
...(cols ? {
|
|
94
|
+
columnCount: cols,
|
|
95
|
+
columnGap: "var(--grid-gutter, 24px)",
|
|
96
|
+
} : {}),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Preview mode or read-only frame: static rich text render via PortableText
|
|
100
|
+
if (previewMode || !editable) {
|
|
101
|
+
if (!hasText) {
|
|
102
|
+
return (
|
|
103
|
+
<p className="text-neutral-400 italic text-sm py-4 text-center">
|
|
104
|
+
Empty text block
|
|
105
|
+
</p>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
<div style={computedStyle} className="space-y-[0.75em]">
|
|
110
|
+
<PortableText value={block.text!} components={previewRichTextComponents} />
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Design mode: Tiptap rich text editor
|
|
116
|
+
return <RichTextEditor block={block} editable />;
|
|
117
|
+
}
|