@railway/inkwell 1.3.0 → 2.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 CHANGED
@@ -2020,6 +2020,20 @@ var InkwellEditorClient = react.forwardRef(
2020
2020
  selectEditor,
2021
2021
  stateVersion
2022
2022
  ]);
2023
+ const renderPlaceholder = react.useCallback(
2024
+ ({ attributes, children }) => /* @__PURE__ */ jsxRuntime.jsx(
2025
+ "span",
2026
+ {
2027
+ ...attributes,
2028
+ style: {
2029
+ ...attributes.style,
2030
+ paddingBottom: "2rem"
2031
+ },
2032
+ children
2033
+ }
2034
+ ),
2035
+ []
2036
+ );
2023
2037
  return /* @__PURE__ */ jsxRuntime.jsxs(
2024
2038
  "div",
2025
2039
  {
@@ -2067,6 +2081,7 @@ var InkwellEditorClient = react.forwardRef(
2067
2081
  style: styles?.editor,
2068
2082
  renderElement: RenderElement,
2069
2083
  renderLeaf: RenderLeaf,
2084
+ renderPlaceholder,
2070
2085
  decorate,
2071
2086
  placeholder: resolvedPlaceholder,
2072
2087
  spellCheck: true,
@@ -3148,6 +3163,71 @@ function CopyCodeBlock({
3148
3163
  /* @__PURE__ */ jsxRuntime.jsx("pre", { ref: preRef, ...props, children })
3149
3164
  ] });
3150
3165
  }
3166
+ function splitTextOnNewlines(text) {
3167
+ if (!text.value.includes("\n")) return [text];
3168
+ const parts = text.value.split("\n");
3169
+ const result = [];
3170
+ for (let i = 0; i < parts.length; i++) {
3171
+ if (parts[i] !== "") {
3172
+ result.push({ type: "text", value: parts[i] });
3173
+ }
3174
+ if (i < parts.length - 1) {
3175
+ result.push({ type: "break" });
3176
+ }
3177
+ }
3178
+ return result;
3179
+ }
3180
+ function expandParagraphChildren(children) {
3181
+ let changed = false;
3182
+ const next = [];
3183
+ for (const child of children) {
3184
+ if (child.type === "text" && child.value.includes("\n")) {
3185
+ next.push(...splitTextOnNewlines(child));
3186
+ changed = true;
3187
+ } else {
3188
+ next.push(child);
3189
+ }
3190
+ }
3191
+ return changed ? next : null;
3192
+ }
3193
+ function remarkSoftBreakAsBreak() {
3194
+ return (tree) => {
3195
+ unistUtilVisit.visit(tree, "paragraph", (node) => {
3196
+ const expanded = expandParagraphChildren(node.children);
3197
+ if (expanded) node.children = expanded;
3198
+ });
3199
+ };
3200
+ }
3201
+ function remarkSoftBreakAsParagraph() {
3202
+ return (tree) => {
3203
+ unistUtilVisit.visit(tree, "paragraph", (node, index, parent) => {
3204
+ if (!parent || index == null) return;
3205
+ const expanded = expandParagraphChildren(node.children) ?? node.children.slice();
3206
+ const breakIndices = [];
3207
+ for (let i = 0; i < expanded.length; i++) {
3208
+ if (expanded[i].type === "break") breakIndices.push(i);
3209
+ }
3210
+ if (breakIndices.length === 0) {
3211
+ return;
3212
+ }
3213
+ const newParagraphs = [];
3214
+ let start = 0;
3215
+ for (const breakIdx of [...breakIndices, expanded.length]) {
3216
+ if (breakIdx > start) {
3217
+ newParagraphs.push({
3218
+ type: "paragraph",
3219
+ children: expanded.slice(start, breakIdx)
3220
+ });
3221
+ }
3222
+ start = breakIdx + 1;
3223
+ }
3224
+ parent.children.splice(index, 1, ...newParagraphs);
3225
+ return [unistUtilVisit.SKIP, index + newParagraphs.length];
3226
+ });
3227
+ };
3228
+ }
3229
+
3230
+ // src/renderer/markdown-parser.ts
3151
3231
  var MENTION_TAG_PREFIX = "inkwell-mention-";
3152
3232
  function rehypeMentions(mentions) {
3153
3233
  return () => (tree) => {
@@ -3210,7 +3290,14 @@ function rehypeMentions(mentions) {
3210
3290
  };
3211
3291
  }
3212
3292
  function createProcessor2(options = {}) {
3213
- const proc = unified.unified().use(remarkParse__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype__default.default);
3293
+ const proc = unified.unified().use(remarkParse__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkFlattenBlockquotes);
3294
+ const softBreak = options.softBreak ?? "paragraph";
3295
+ if (softBreak === "br") {
3296
+ proc.use(remarkSoftBreakAsBreak);
3297
+ } else if (softBreak === "paragraph") {
3298
+ proc.use(remarkSoftBreakAsParagraph);
3299
+ }
3300
+ proc.use(remarkRehype__default.default);
3214
3301
  const plugins = options.rehypePlugins ?? [
3215
3302
  [rehypeHighlight__default.default, { detect: true }]
3216
3303
  ];
@@ -3269,7 +3356,8 @@ function InkwellRenderer({
3269
3356
  className,
3270
3357
  components,
3271
3358
  rehypePlugins,
3272
- mentions
3359
+ mentions,
3360
+ softBreak
3273
3361
  }) {
3274
3362
  const mergedComponents = react.useMemo(
3275
3363
  () => ({ pre: CopyCodeBlock, ...components }),
@@ -3279,9 +3367,10 @@ function InkwellRenderer({
3279
3367
  () => parseMarkdown(content, {
3280
3368
  components: mergedComponents,
3281
3369
  rehypePlugins,
3282
- mentions
3370
+ mentions,
3371
+ softBreak
3283
3372
  }),
3284
- [content, mergedComponents, rehypePlugins, mentions]
3373
+ [content, mergedComponents, rehypePlugins, mentions, softBreak]
3285
3374
  );
3286
3375
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
3287
3376
  }
package/dist/index.d.cts CHANGED
@@ -110,6 +110,16 @@ interface InkwellEditorProps {
110
110
  /** Called when submitOnEnter handles Enter. */
111
111
  onSubmit?: (content: string) => void;
112
112
  }
113
+ /**
114
+ * How `InkwellRenderer` handles single-newline soft breaks in source markdown.
115
+ * - `"paragraph"` (default): split the enclosing paragraph at the soft break,
116
+ * producing two `<p>` elements with normal paragraph margins.
117
+ * - `"br"`: emit a `<br />`. Matches GFM-style behavior (and Showdown with
118
+ * `simpleLineBreaks: true`).
119
+ * - `"preserve"`: keep as a literal `\n` text node. The browser collapses it
120
+ * to whitespace per CSS. Matches strict CommonMark.
121
+ */
122
+ type InkwellSoftBreakBehavior = "preserve" | "br" | "paragraph";
113
123
  interface InkwellRendererProps {
114
124
  /** Markdown source content string. */
115
125
  content: string;
@@ -121,11 +131,15 @@ interface InkwellRendererProps {
121
131
  rehypePlugins?: RehypePluginConfig[];
122
132
  /** Mention patterns to expand in rendered text. */
123
133
  mentions?: MentionRenderer[];
134
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
135
+ softBreak?: InkwellSoftBreakBehavior;
124
136
  }
125
137
  interface ParseMarkdownOptions {
126
138
  components?: InkwellComponents;
127
139
  rehypePlugins?: RehypePluginConfig[];
128
140
  mentions?: MentionRenderer[];
141
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
142
+ softBreak?: InkwellSoftBreakBehavior;
129
143
  }
130
144
  interface MentionRenderer {
131
145
  /** Regular expression applied to text-node content. */
@@ -482,11 +496,11 @@ declare function createSnippetsPlugin({ snippets, name, trigger, }: SnippetsPlug
482
496
  */
483
497
  declare function htmlToMarkdown(html: string): string;
484
498
 
485
- declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, }: InkwellRendererProps): ReactNode;
499
+ declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, softBreak, }: InkwellRendererProps): ReactNode;
486
500
 
487
501
  /**
488
502
  * Parse a markdown string into React elements synchronously
489
503
  */
490
504
  declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
491
505
 
492
- export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
506
+ export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type InkwellSoftBreakBehavior, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
package/dist/index.d.ts CHANGED
@@ -110,6 +110,16 @@ interface InkwellEditorProps {
110
110
  /** Called when submitOnEnter handles Enter. */
111
111
  onSubmit?: (content: string) => void;
112
112
  }
113
+ /**
114
+ * How `InkwellRenderer` handles single-newline soft breaks in source markdown.
115
+ * - `"paragraph"` (default): split the enclosing paragraph at the soft break,
116
+ * producing two `<p>` elements with normal paragraph margins.
117
+ * - `"br"`: emit a `<br />`. Matches GFM-style behavior (and Showdown with
118
+ * `simpleLineBreaks: true`).
119
+ * - `"preserve"`: keep as a literal `\n` text node. The browser collapses it
120
+ * to whitespace per CSS. Matches strict CommonMark.
121
+ */
122
+ type InkwellSoftBreakBehavior = "preserve" | "br" | "paragraph";
113
123
  interface InkwellRendererProps {
114
124
  /** Markdown source content string. */
115
125
  content: string;
@@ -121,11 +131,15 @@ interface InkwellRendererProps {
121
131
  rehypePlugins?: RehypePluginConfig[];
122
132
  /** Mention patterns to expand in rendered text. */
123
133
  mentions?: MentionRenderer[];
134
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
135
+ softBreak?: InkwellSoftBreakBehavior;
124
136
  }
125
137
  interface ParseMarkdownOptions {
126
138
  components?: InkwellComponents;
127
139
  rehypePlugins?: RehypePluginConfig[];
128
140
  mentions?: MentionRenderer[];
141
+ /** How to render single-newline soft breaks. Defaults to `"paragraph"`. */
142
+ softBreak?: InkwellSoftBreakBehavior;
129
143
  }
130
144
  interface MentionRenderer {
131
145
  /** Regular expression applied to text-node content. */
@@ -482,11 +496,11 @@ declare function createSnippetsPlugin({ snippets, name, trigger, }: SnippetsPlug
482
496
  */
483
497
  declare function htmlToMarkdown(html: string): string;
484
498
 
485
- declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, }: InkwellRendererProps): ReactNode;
499
+ declare function InkwellRenderer({ content, className, components, rehypePlugins, mentions, softBreak, }: InkwellRendererProps): ReactNode;
486
500
 
487
501
  /**
488
502
  * Parse a markdown string into React elements synchronously
489
503
  */
490
504
  declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
491
505
 
492
- export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
506
+ export { type Attachment, type AttachmentUploadResult, type AttachmentsHandle, type AttachmentsPluginOptions, type BubbleMenuItem, type BubbleMenuItemProps, type BubbleMenuOptions, type CompletionsPluginOptions, type EmojiItem, type EmojiPluginOptions, type InkwellComponents, InkwellEditor, type InkwellEditorClassNames, type InkwellEditorFocusOptions, type InkwellEditorHandle, type InkwellEditorProps, type InkwellEditorState, type InkwellEditorStyles, type InkwellFeatures, type InkwellPlugin, type InkwellPluginActivation, type InkwellPluginEditor, type InkwellPluginPlaceholder, InkwellRenderer, type InkwellRendererProps, type InkwellSoftBreakBehavior, type MentionItem, type MentionRenderer, type MentionsPluginOptions, type ParseMarkdownOptions, type PluginInsertDataContext, type PluginKeyDownContext, type PluginRenderProps, type RehypePluginConfig, type SlashCommandArg, type SlashCommandChoice, type SlashCommandExecution, type SlashCommandItem, type SlashCommandsPluginOptions, type Snippet, type SnippetsPluginOptions, type SubscribeForwardedKey, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };
package/dist/index.js CHANGED
@@ -2005,6 +2005,20 @@ var InkwellEditorClient = forwardRef(
2005
2005
  selectEditor,
2006
2006
  stateVersion
2007
2007
  ]);
2008
+ const renderPlaceholder = useCallback(
2009
+ ({ attributes, children }) => /* @__PURE__ */ jsx(
2010
+ "span",
2011
+ {
2012
+ ...attributes,
2013
+ style: {
2014
+ ...attributes.style,
2015
+ paddingBottom: "2rem"
2016
+ },
2017
+ children
2018
+ }
2019
+ ),
2020
+ []
2021
+ );
2008
2022
  return /* @__PURE__ */ jsxs(
2009
2023
  "div",
2010
2024
  {
@@ -2052,6 +2066,7 @@ var InkwellEditorClient = forwardRef(
2052
2066
  style: styles?.editor,
2053
2067
  renderElement: RenderElement,
2054
2068
  renderLeaf: RenderLeaf,
2069
+ renderPlaceholder,
2055
2070
  decorate,
2056
2071
  placeholder: resolvedPlaceholder,
2057
2072
  spellCheck: true,
@@ -3133,6 +3148,71 @@ function CopyCodeBlock({
3133
3148
  /* @__PURE__ */ jsx("pre", { ref: preRef, ...props, children })
3134
3149
  ] });
3135
3150
  }
3151
+ function splitTextOnNewlines(text) {
3152
+ if (!text.value.includes("\n")) return [text];
3153
+ const parts = text.value.split("\n");
3154
+ const result = [];
3155
+ for (let i = 0; i < parts.length; i++) {
3156
+ if (parts[i] !== "") {
3157
+ result.push({ type: "text", value: parts[i] });
3158
+ }
3159
+ if (i < parts.length - 1) {
3160
+ result.push({ type: "break" });
3161
+ }
3162
+ }
3163
+ return result;
3164
+ }
3165
+ function expandParagraphChildren(children) {
3166
+ let changed = false;
3167
+ const next = [];
3168
+ for (const child of children) {
3169
+ if (child.type === "text" && child.value.includes("\n")) {
3170
+ next.push(...splitTextOnNewlines(child));
3171
+ changed = true;
3172
+ } else {
3173
+ next.push(child);
3174
+ }
3175
+ }
3176
+ return changed ? next : null;
3177
+ }
3178
+ function remarkSoftBreakAsBreak() {
3179
+ return (tree) => {
3180
+ visit(tree, "paragraph", (node) => {
3181
+ const expanded = expandParagraphChildren(node.children);
3182
+ if (expanded) node.children = expanded;
3183
+ });
3184
+ };
3185
+ }
3186
+ function remarkSoftBreakAsParagraph() {
3187
+ return (tree) => {
3188
+ visit(tree, "paragraph", (node, index, parent) => {
3189
+ if (!parent || index == null) return;
3190
+ const expanded = expandParagraphChildren(node.children) ?? node.children.slice();
3191
+ const breakIndices = [];
3192
+ for (let i = 0; i < expanded.length; i++) {
3193
+ if (expanded[i].type === "break") breakIndices.push(i);
3194
+ }
3195
+ if (breakIndices.length === 0) {
3196
+ return;
3197
+ }
3198
+ const newParagraphs = [];
3199
+ let start = 0;
3200
+ for (const breakIdx of [...breakIndices, expanded.length]) {
3201
+ if (breakIdx > start) {
3202
+ newParagraphs.push({
3203
+ type: "paragraph",
3204
+ children: expanded.slice(start, breakIdx)
3205
+ });
3206
+ }
3207
+ start = breakIdx + 1;
3208
+ }
3209
+ parent.children.splice(index, 1, ...newParagraphs);
3210
+ return [SKIP, index + newParagraphs.length];
3211
+ });
3212
+ };
3213
+ }
3214
+
3215
+ // src/renderer/markdown-parser.ts
3136
3216
  var MENTION_TAG_PREFIX = "inkwell-mention-";
3137
3217
  function rehypeMentions(mentions) {
3138
3218
  return () => (tree) => {
@@ -3195,7 +3275,14 @@ function rehypeMentions(mentions) {
3195
3275
  };
3196
3276
  }
3197
3277
  function createProcessor2(options = {}) {
3198
- const proc = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype);
3278
+ const proc = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes);
3279
+ const softBreak = options.softBreak ?? "paragraph";
3280
+ if (softBreak === "br") {
3281
+ proc.use(remarkSoftBreakAsBreak);
3282
+ } else if (softBreak === "paragraph") {
3283
+ proc.use(remarkSoftBreakAsParagraph);
3284
+ }
3285
+ proc.use(remarkRehype);
3199
3286
  const plugins = options.rehypePlugins ?? [
3200
3287
  [rehypeHighlight, { detect: true }]
3201
3288
  ];
@@ -3254,7 +3341,8 @@ function InkwellRenderer({
3254
3341
  className,
3255
3342
  components,
3256
3343
  rehypePlugins,
3257
- mentions
3344
+ mentions,
3345
+ softBreak
3258
3346
  }) {
3259
3347
  const mergedComponents = useMemo(
3260
3348
  () => ({ pre: CopyCodeBlock, ...components }),
@@ -3264,9 +3352,10 @@ function InkwellRenderer({
3264
3352
  () => parseMarkdown(content, {
3265
3353
  components: mergedComponents,
3266
3354
  rehypePlugins,
3267
- mentions
3355
+ mentions,
3356
+ softBreak
3268
3357
  }),
3269
- [content, mergedComponents, rehypePlugins, mentions]
3358
+ [content, mergedComponents, rehypePlugins, mentions, softBreak]
3270
3359
  );
3271
3360
  return /* @__PURE__ */ jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
3272
3361
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@railway/inkwell",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Inkwell is a Markdown editor and renderer for React with an extensible plugin system.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/src/styles.css CHANGED
@@ -22,11 +22,18 @@
22
22
 
23
23
  /* ── Tokens ──────────────────────────────────────────────────────── */
24
24
 
25
- .inkwell-editor,
26
- .inkwell-editor-wrapper,
27
- .inkwell-renderer,
28
- .inkwell-plugin-bubble-menu-container,
29
- .inkwell-plugin-picker-popup {
25
+ /* Token definitions sit at 0,0,0 specificity so a consumer rule with even
26
+ a single class on a parent (e.g. `.dark .inkwell-renderer { --inkwell-text: ... }`
27
+ for class-driven theming that does not rely on `prefers-color-scheme`)
28
+ beats them automatically. The OS-level dark-mode block below uses the
29
+ same `:where()` wrapper for the same reason. */
30
+ :where(
31
+ .inkwell-editor,
32
+ .inkwell-editor-wrapper,
33
+ .inkwell-renderer,
34
+ .inkwell-plugin-bubble-menu-container,
35
+ .inkwell-plugin-picker-popup
36
+ ) {
30
37
  /* Surfaces */
31
38
  --inkwell-bg: hsl(0, 0%, 100%);
32
39
  --inkwell-bg-elevated: hsl(0, 0%, 100%);
@@ -55,14 +62,44 @@
55
62
  "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Menlo,
56
63
  Consolas, monospace;
57
64
  --inkwell-radius: 6px;
65
+
66
+ /* Typography & spacing. Defined once and consumed by both
67
+ `.inkwell-editor` and `.inkwell-renderer` so the two surfaces stay
68
+ WYSIWYG — what you type matches what renders. Override any token
69
+ on either surface to retune both (or use a per-surface selector
70
+ to retune just one). For chat-composer or compact embeds, set
71
+ `--inkwell-space-paragraph: 0` on the editor surface. */
72
+ --inkwell-font-size: 0.95rem;
73
+ --inkwell-line-height: 1.6;
74
+ --inkwell-heading-weight: 600;
75
+ --inkwell-heading-line-height: 1.3;
76
+ --inkwell-h1-size: 1.75em;
77
+ --inkwell-h2-size: 1.4em;
78
+ --inkwell-h3-size: 1.2em;
79
+ --inkwell-h4-size: 1em;
80
+ --inkwell-h5-size: 0.9em;
81
+ --inkwell-h6-size: 0.8em;
82
+ --inkwell-code-font-size: 0.85em;
83
+
84
+ --inkwell-space-paragraph: 0.5em;
85
+ --inkwell-space-heading: 0.75em;
86
+ --inkwell-space-blockquote: 1em;
87
+ --inkwell-space-list: 1em;
88
+ --inkwell-space-list-item: 0.25em;
89
+ --inkwell-list-indent: 1.5em;
90
+ --inkwell-space-code-block: 1em;
91
+ --inkwell-space-image: 1em;
92
+ --inkwell-space-hr: 2em;
58
93
  }
59
94
 
60
95
  @media (prefers-color-scheme: dark) {
61
- .inkwell-editor,
62
- .inkwell-editor-wrapper,
63
- .inkwell-renderer,
64
- .inkwell-plugin-bubble-menu-container,
65
- .inkwell-plugin-picker-popup {
96
+ :where(
97
+ .inkwell-editor,
98
+ .inkwell-editor-wrapper,
99
+ .inkwell-renderer,
100
+ .inkwell-plugin-bubble-menu-container,
101
+ .inkwell-plugin-picker-popup
102
+ ) {
66
103
  --inkwell-bg: hsl(220, 13%, 10%);
67
104
  --inkwell-bg-elevated: hsl(220, 13%, 13%);
68
105
  --inkwell-bg-subtle: hsl(220, 13%, 16%);
@@ -109,8 +146,8 @@
109
146
  border-radius: var(--inkwell-radius);
110
147
  background: var(--inkwell-bg);
111
148
  color: var(--inkwell-text);
112
- line-height: 1.6;
113
- font-size: 0.95rem;
149
+ line-height: var(--inkwell-line-height);
150
+ font-size: var(--inkwell-font-size);
114
151
  transition: border-color 0.15s ease;
115
152
  }
116
153
  :where(.inkwell-editor:focus-within) {
@@ -118,8 +155,20 @@
118
155
  }
119
156
 
120
157
  /* `position: relative` on paragraphs is structural — Slate decorations and
121
- inline children position against it. Margin is theming and goes through
122
- `:where()` so a consumer paragraph-spacing utility wins. */
158
+ inline children position against it.
159
+
160
+ Margin stays at `0` here even though the renderer's paragraphs use
161
+ `--inkwell-space-paragraph`. Reason: the editor's content model emits
162
+ one `<p>` per source line, so a blank line in Markdown becomes an
163
+ empty `<p>` node between two paragraphs (a cursor target, kept for
164
+ round-trip fidelity). With a non-zero paragraph margin, those empty
165
+ paragraphs add their own top/bottom margin on top of the real
166
+ paragraphs' margins — visually multiplying the gap and breaking the
167
+ WYSIWYG promise in the other direction (editor looks more airy than
168
+ the renderer). Until the empty-paragraph encoding is reworked, the
169
+ editor opts out of the shared paragraph-spacing token. Consumers
170
+ who want non-zero spacing in the editor can set the margin
171
+ themselves with a higher-specificity rule. */
123
172
  .inkwell-editor p {
124
173
  position: relative;
125
174
  }
@@ -143,42 +192,43 @@
143
192
  padding: 0.1em 0.35em;
144
193
  border-radius: 4px;
145
194
  font-family: var(--inkwell-font-mono);
146
- font-size: 0.85em;
195
+ font-size: var(--inkwell-code-font-size);
147
196
  }
148
197
 
149
198
  :where(.inkwell-editor-blockquote) {
150
199
  border-left: 3px solid var(--inkwell-border-strong);
151
200
  padding-left: 0.85em;
152
- margin: 0.5em 0;
201
+ margin: var(--inkwell-space-blockquote) 0;
153
202
  color: var(--inkwell-text-muted);
154
203
  }
155
204
 
156
205
  :where(.inkwell-editor-heading) {
157
- font-weight: 600;
158
- line-height: 1.3;
206
+ font-weight: var(--inkwell-heading-weight);
207
+ line-height: var(--inkwell-heading-line-height);
208
+ margin: var(--inkwell-space-heading) 0;
159
209
  color: var(--inkwell-text);
160
210
  }
161
211
  :where(.inkwell-editor-heading-1) {
162
- font-size: 1.75em;
212
+ font-size: var(--inkwell-h1-size);
163
213
  }
164
214
  :where(.inkwell-editor-heading-2) {
165
- font-size: 1.4em;
215
+ font-size: var(--inkwell-h2-size);
166
216
  }
167
217
  :where(.inkwell-editor-heading-3) {
168
- font-size: 1.2em;
218
+ font-size: var(--inkwell-h3-size);
169
219
  }
170
220
  :where(.inkwell-editor-heading-4) {
171
- font-size: 1em;
221
+ font-size: var(--inkwell-h4-size);
172
222
  }
173
223
  :where(.inkwell-editor-heading-5) {
174
- font-size: 0.9em;
224
+ font-size: var(--inkwell-h5-size);
175
225
  }
176
226
  :where(.inkwell-editor-heading-6) {
177
- font-size: 0.8em;
227
+ font-size: var(--inkwell-h6-size);
178
228
  }
179
229
 
180
230
  :where(.inkwell-editor-image) {
181
- margin: 0.75em 0;
231
+ margin: var(--inkwell-space-image) 0;
182
232
  border-radius: var(--inkwell-radius);
183
233
  overflow: hidden;
184
234
  border: 1px solid transparent;
@@ -248,7 +298,7 @@
248
298
  :where(.inkwell-editor .inkwell-editor-code-fence),
249
299
  :where(.inkwell-renderer pre code) {
250
300
  font-family: var(--inkwell-font-mono);
251
- font-size: 0.85em;
301
+ font-size: var(--inkwell-code-font-size);
252
302
  line-height: 1.55;
253
303
  }
254
304
  /* Wrapping behavior for code lines stays structural — Slate emits one
@@ -433,40 +483,61 @@
433
483
  `!important`. */
434
484
  :where(.inkwell-renderer) {
435
485
  color: var(--inkwell-text);
436
- line-height: 1.65;
437
- font-size: 0.95rem;
486
+ line-height: var(--inkwell-line-height);
487
+ font-size: var(--inkwell-font-size);
438
488
  }
439
489
  :where(.inkwell-renderer :first-child) {
440
490
  margin-top: 0;
441
491
  }
442
492
  :where(.inkwell-renderer h1) {
443
- font-size: 1.75em;
444
- font-weight: 600;
445
- margin: 0.67em 0;
493
+ font-size: var(--inkwell-h1-size);
494
+ font-weight: var(--inkwell-heading-weight);
495
+ line-height: var(--inkwell-heading-line-height);
496
+ margin: var(--inkwell-space-heading) 0;
446
497
  }
447
498
  :where(.inkwell-renderer h2) {
448
- font-size: 1.4em;
449
- font-weight: 600;
450
- margin: 0.75em 0;
499
+ font-size: var(--inkwell-h2-size);
500
+ font-weight: var(--inkwell-heading-weight);
501
+ line-height: var(--inkwell-heading-line-height);
502
+ margin: var(--inkwell-space-heading) 0;
451
503
  }
452
504
  :where(.inkwell-renderer h3) {
453
- font-size: 1.2em;
454
- font-weight: 600;
455
- margin: 0.8em 0;
505
+ font-size: var(--inkwell-h3-size);
506
+ font-weight: var(--inkwell-heading-weight);
507
+ line-height: var(--inkwell-heading-line-height);
508
+ margin: var(--inkwell-space-heading) 0;
509
+ }
510
+ :where(.inkwell-renderer h4) {
511
+ font-size: var(--inkwell-h4-size);
512
+ font-weight: var(--inkwell-heading-weight);
513
+ line-height: var(--inkwell-heading-line-height);
514
+ margin: var(--inkwell-space-heading) 0;
515
+ }
516
+ :where(.inkwell-renderer h5) {
517
+ font-size: var(--inkwell-h5-size);
518
+ font-weight: var(--inkwell-heading-weight);
519
+ line-height: var(--inkwell-heading-line-height);
520
+ margin: var(--inkwell-space-heading) 0;
521
+ }
522
+ :where(.inkwell-renderer h6) {
523
+ font-size: var(--inkwell-h6-size);
524
+ font-weight: var(--inkwell-heading-weight);
525
+ line-height: var(--inkwell-heading-line-height);
526
+ margin: var(--inkwell-space-heading) 0;
456
527
  }
457
528
  :where(.inkwell-renderer p) {
458
- margin: 0.5em 0;
529
+ margin: var(--inkwell-space-paragraph) 0;
459
530
  }
460
531
  :where(.inkwell-renderer blockquote) {
461
532
  border-left: 3px solid var(--inkwell-border-strong);
462
533
  padding-left: 0.85em;
463
- margin: 1em 0;
534
+ margin: var(--inkwell-space-blockquote) 0;
464
535
  color: var(--inkwell-text-muted);
465
536
  }
466
537
  :where(.inkwell-renderer ul),
467
538
  :where(.inkwell-renderer ol) {
468
- padding-left: 1.5em;
469
- margin: 1em 0;
539
+ padding-left: var(--inkwell-list-indent);
540
+ margin: var(--inkwell-space-list) 0;
470
541
  }
471
542
  :where(.inkwell-renderer ul) {
472
543
  list-style: disc;
@@ -475,7 +546,7 @@
475
546
  list-style: decimal;
476
547
  }
477
548
  :where(.inkwell-renderer li) {
478
- margin: 0.25em 0;
549
+ margin: var(--inkwell-space-list-item) 0;
479
550
  }
480
551
  :where(.inkwell-renderer code) {
481
552
  background: var(--inkwell-code-bg);
@@ -483,7 +554,7 @@
483
554
  padding: 0.1em 0.35em;
484
555
  border-radius: 4px;
485
556
  font-family: var(--inkwell-font-mono);
486
- font-size: 0.85em;
557
+ font-size: var(--inkwell-code-font-size);
487
558
  }
488
559
  /* Code-block wrapper position is structural — the copy button absolutely
489
560
  positions inside it. */
@@ -524,7 +595,7 @@
524
595
  color: var(--inkwell-text);
525
596
  }
526
597
  :where(.inkwell-renderer pre) {
527
- margin: 1em 0;
598
+ margin: var(--inkwell-space-code-block) 0;
528
599
  border-radius: var(--inkwell-radius);
529
600
  overflow: auto;
530
601
  border: 1px solid var(--inkwell-border);
@@ -535,7 +606,6 @@
535
606
  padding: 0.85em 1em;
536
607
  background: transparent;
537
608
  color: var(--inkwell-text);
538
- font-size: 0.82em;
539
609
  }
540
610
  :where(.inkwell-renderer a) {
541
611
  color: var(--inkwell-accent);
@@ -545,7 +615,7 @@
545
615
  :where(.inkwell-renderer hr) {
546
616
  border: none;
547
617
  border-top: 1px solid var(--inkwell-border);
548
- margin: 2em 0;
618
+ margin: var(--inkwell-space-hr) 0;
549
619
  }
550
620
  :where(.inkwell-renderer strong) {
551
621
  font-weight: 600;
@@ -560,5 +630,5 @@
560
630
  max-width: 100%;
561
631
  height: auto;
562
632
  border-radius: var(--inkwell-radius);
563
- margin: 1em 0;
633
+ margin: var(--inkwell-space-image) 0;
564
634
  }