@remit/ui 0.0.100 → 0.0.101
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/package.json +1 -1
- package/src/components/compose-mode-toggle.tsx +35 -0
- package/src/components/plain-text-editor.mount.test.ts +238 -0
- package/src/components/plain-text-editor.stories.tsx +227 -0
- package/src/components/plain-text-editor.tsx +178 -0
- package/src/components/rich-text-document.ts +111 -3
- package/src/components/rich-text-editor.stories.tsx +76 -0
- package/src/components/rich-text-editor.tsx +4 -1
- package/src/components/rich-text-formatting.test.ts +153 -0
- package/src/components/rich-text-markdown.test.ts +131 -0
- package/src/components/rich-text-markdown.ts +155 -0
- package/src/components/rich-text-toolbar.tsx +68 -46
- package/src/components/rich-text-value.ts +11 -1
- package/src/rich-text.ts +17 -3
- package/src/tokens.css +4 -0
|
@@ -38,7 +38,7 @@ const ToolbarButton = ({
|
|
|
38
38
|
title={title}
|
|
39
39
|
aria-label={title}
|
|
40
40
|
aria-pressed={isActive}
|
|
41
|
-
className={`
|
|
41
|
+
className={`inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded transition-colors ${
|
|
42
42
|
isActive
|
|
43
43
|
? "text-fg bg-accent-2-soft"
|
|
44
44
|
: "text-fg-muted hover:text-fg hover:bg-surface-raised"
|
|
@@ -66,7 +66,16 @@ const INITIAL_STATE: ToolbarState = {
|
|
|
66
66
|
canRedo: false,
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
export
|
|
69
|
+
export interface RichTextToolbarProps {
|
|
70
|
+
/**
|
|
71
|
+
* Pinned to the right of the strip, outside the part that scrolls. The mode
|
|
72
|
+
* toggle rides here, and it is the last element in the toolbar's DOM so one
|
|
73
|
+
* Shift+Tab out of the body reaches it.
|
|
74
|
+
*/
|
|
75
|
+
trailing?: React.ReactNode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const RichTextToolbar = ({ trailing }: RichTextToolbarProps) => {
|
|
70
79
|
const [editor] = useLexicalComposerContext();
|
|
71
80
|
const [state, setState] = useState<ToolbarState>(INITIAL_STATE);
|
|
72
81
|
const [linkDraft, setLinkDraft] = useState<string | null>(null);
|
|
@@ -146,51 +155,64 @@ export const RichTextToolbar = () => {
|
|
|
146
155
|
};
|
|
147
156
|
|
|
148
157
|
return (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
isActive={state.italic}
|
|
160
|
-
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")}
|
|
161
|
-
title="Italic (Ctrl+I)"
|
|
162
|
-
>
|
|
163
|
-
<Italic className="size-4" />
|
|
164
|
-
</ToolbarButton>
|
|
165
|
-
<ToolbarButton
|
|
166
|
-
isActive={state.link !== null}
|
|
167
|
-
onClick={() => setLinkDraft(state.link ?? "https://")}
|
|
168
|
-
title="Link (Ctrl+K)"
|
|
169
|
-
>
|
|
170
|
-
<Link className="size-4" />
|
|
171
|
-
</ToolbarButton>
|
|
172
|
-
<ToolbarButton
|
|
173
|
-
isActive={state.quote}
|
|
174
|
-
onClick={toggleQuote}
|
|
175
|
-
title="Blockquote"
|
|
176
|
-
>
|
|
177
|
-
<Quote className="size-4" />
|
|
178
|
-
</ToolbarButton>
|
|
179
|
-
<div className="mx-1.5 h-4 w-px bg-line" />
|
|
180
|
-
<ToolbarButton
|
|
181
|
-
isActive={false}
|
|
182
|
-
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
|
|
183
|
-
title="Undo (Ctrl+Z)"
|
|
184
|
-
>
|
|
185
|
-
<Undo2 className={`size-4 ${state.canUndo ? "" : "opacity-40"}`} />
|
|
186
|
-
</ToolbarButton>
|
|
187
|
-
<ToolbarButton
|
|
188
|
-
isActive={false}
|
|
189
|
-
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
|
|
190
|
-
title="Redo (Ctrl+Y)"
|
|
158
|
+
// The toolbar and the body share one scroller, so twenty lines of typing
|
|
159
|
+
// would carry the toolbar off the top of the compose window with them.
|
|
160
|
+
<div className="sticky top-0 z-10 border-b border-line bg-canvas">
|
|
161
|
+
<div className="flex items-center gap-2 px-3 py-1">
|
|
162
|
+
{/* `min-w-0`, without which a flex child refuses to shrink below its
|
|
163
|
+
content and pushes the trailing control off the edge instead of
|
|
164
|
+
scrolling. */}
|
|
165
|
+
<div
|
|
166
|
+
data-testid="compose-format-cluster"
|
|
167
|
+
className="flex min-w-0 items-center gap-0.5 overflow-x-auto"
|
|
191
168
|
>
|
|
192
|
-
<
|
|
193
|
-
|
|
169
|
+
<ToolbarButton
|
|
170
|
+
isActive={state.bold}
|
|
171
|
+
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
|
|
172
|
+
title="Bold (Ctrl+B)"
|
|
173
|
+
>
|
|
174
|
+
<Bold className="size-4" />
|
|
175
|
+
</ToolbarButton>
|
|
176
|
+
<ToolbarButton
|
|
177
|
+
isActive={state.italic}
|
|
178
|
+
onClick={() =>
|
|
179
|
+
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
|
|
180
|
+
}
|
|
181
|
+
title="Italic (Ctrl+I)"
|
|
182
|
+
>
|
|
183
|
+
<Italic className="size-4" />
|
|
184
|
+
</ToolbarButton>
|
|
185
|
+
<ToolbarButton
|
|
186
|
+
isActive={state.link !== null}
|
|
187
|
+
onClick={() => setLinkDraft(state.link ?? "https://")}
|
|
188
|
+
title="Link (Ctrl+K)"
|
|
189
|
+
>
|
|
190
|
+
<Link className="size-4" />
|
|
191
|
+
</ToolbarButton>
|
|
192
|
+
<ToolbarButton
|
|
193
|
+
isActive={state.quote}
|
|
194
|
+
onClick={toggleQuote}
|
|
195
|
+
title="Blockquote"
|
|
196
|
+
>
|
|
197
|
+
<Quote className="size-4" />
|
|
198
|
+
</ToolbarButton>
|
|
199
|
+
<div className="mx-1.5 h-4 w-px shrink-0 bg-line" />
|
|
200
|
+
<ToolbarButton
|
|
201
|
+
isActive={false}
|
|
202
|
+
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
|
|
203
|
+
title="Undo (Ctrl+Z)"
|
|
204
|
+
>
|
|
205
|
+
<Undo2 className={`size-4 ${state.canUndo ? "" : "opacity-40"}`} />
|
|
206
|
+
</ToolbarButton>
|
|
207
|
+
<ToolbarButton
|
|
208
|
+
isActive={false}
|
|
209
|
+
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
|
|
210
|
+
title="Redo (Ctrl+Y)"
|
|
211
|
+
>
|
|
212
|
+
<Redo2 className={`size-4 ${state.canRedo ? "" : "opacity-40"}`} />
|
|
213
|
+
</ToolbarButton>
|
|
214
|
+
</div>
|
|
215
|
+
{trailing && <div className="ml-auto shrink-0">{trailing}</div>}
|
|
194
216
|
</div>
|
|
195
217
|
{linkDraft !== null && (
|
|
196
218
|
<div className="flex items-center gap-2 border-t border-line px-3 py-1.5">
|
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
* not the editor's own JSON, which changes shape between Lexical versions.
|
|
4
4
|
* `text` is the plain alternative, as Markdown over the same document.
|
|
5
5
|
*
|
|
6
|
+
* `formatting` is the inventory of node types and text formats in the document
|
|
7
|
+
* that plain text cannot carry, sorted, empty for a document of ordinary
|
|
8
|
+
* paragraphs. It is reported rather than reduced to a flag so the caller can
|
|
9
|
+
* both decide and say what is at stake.
|
|
10
|
+
*
|
|
6
11
|
* Kept apart from the editor so a caller can name the shape without pulling
|
|
7
12
|
* the editor, and its dependencies, out of their own chunk.
|
|
8
13
|
*/
|
|
9
14
|
export interface RichTextValue {
|
|
10
15
|
html: string;
|
|
11
16
|
text: string;
|
|
17
|
+
formatting: readonly string[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
export const EMPTY_RICH_TEXT: RichTextValue = {
|
|
20
|
+
export const EMPTY_RICH_TEXT: RichTextValue = {
|
|
21
|
+
html: "",
|
|
22
|
+
text: "",
|
|
23
|
+
formatting: [],
|
|
24
|
+
};
|
package/src/rich-text.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
* chunk already imports `@remit/ui`, which is every screen.
|
|
2
|
+
* The compose writing surfaces and nothing else. Their own entry point so an
|
|
3
|
+
* app can load them on demand: reached through the package barrel they would
|
|
4
|
+
* land in whichever chunk already imports `@remit/ui`, which is every screen.
|
|
5
5
|
*/
|
|
6
|
+
export {
|
|
7
|
+
type ComposeBodyMode,
|
|
8
|
+
ComposeModeToggle,
|
|
9
|
+
type ComposeModeToggleProps,
|
|
10
|
+
} from "./components/compose-mode-toggle.js";
|
|
11
|
+
export {
|
|
12
|
+
PlainTextEditor,
|
|
13
|
+
type PlainTextEditorProps,
|
|
14
|
+
} from "./components/plain-text-editor.js";
|
|
15
|
+
export {
|
|
16
|
+
htmlToMarkdown,
|
|
17
|
+
markdownToHtml,
|
|
18
|
+
} from "./components/rich-text-document.js";
|
|
6
19
|
export {
|
|
7
20
|
RichTextEditor,
|
|
8
21
|
type RichTextEditorProps,
|
|
9
22
|
} from "./components/rich-text-editor.js";
|
|
23
|
+
export { COMPOSE_TRANSFORMERS } from "./components/rich-text-markdown.js";
|
|
10
24
|
export {
|
|
11
25
|
EMPTY_RICH_TEXT,
|
|
12
26
|
type RichTextValue,
|
package/src/tokens.css
CHANGED
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
--font-ui: var(--font-geist);
|
|
39
39
|
--font-sans: var(--font-ui);
|
|
40
40
|
|
|
41
|
+
/* The plain compose surface promises the characters that will be sent, and a
|
|
42
|
+
pipe table in a proportional face is unaligned bars. Nothing downloaded. */
|
|
43
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
44
|
+
|
|
41
45
|
/* surfaces & text */
|
|
42
46
|
--color-canvas: var(--canvas);
|
|
43
47
|
--color-surface: var(--surface);
|