@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.
@@ -38,7 +38,7 @@ const ToolbarButton = ({
38
38
  title={title}
39
39
  aria-label={title}
40
40
  aria-pressed={isActive}
41
- className={`p-1.5 rounded transition-colors ${
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 const RichTextToolbar = () => {
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
- <div className="border-b border-line">
150
- <div className="flex items-center gap-0.5 px-3 py-1">
151
- <ToolbarButton
152
- isActive={state.bold}
153
- onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
154
- title="Bold (Ctrl+B)"
155
- >
156
- <Bold className="size-4" />
157
- </ToolbarButton>
158
- <ToolbarButton
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
- <Redo2 className={`size-4 ${state.canRedo ? "" : "opacity-40"}`} />
193
- </ToolbarButton>
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 = { html: "", text: "" };
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 rich-text editor and nothing else. Its own entry point so an app can load
3
- * it on demand: reached through the package barrel it would land in whichever
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);