@railway/inkwell 1.1.1 → 1.2.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
@@ -1377,6 +1377,7 @@ var InkwellEditorClient = react.forwardRef(
1377
1377
  );
1378
1378
  const overLimit = characterLimit !== void 0 && characterCount > characterLimit;
1379
1379
  const hasCharacterLimit = characterLimit !== void 0;
1380
+ const showCharacterCount = characterLimit !== void 0 && characterCount >= characterLimit * 0.8;
1380
1381
  const getEditorState = react.useCallback(() => {
1381
1382
  const content2 = serializeContent();
1382
1383
  return {
@@ -2026,7 +2027,7 @@ var InkwellEditorClient = react.forwardRef(
2026
2027
  className: `inkwell-editor-wrapper${hasCharacterLimit ? " inkwell-editor-has-character-limit" : ""}${overLimit ? " inkwell-editor-over-limit" : ""}${className ? ` ${className}` : ""}${classNames?.root ? ` ${classNames.root}` : ""}`,
2027
2028
  style: styles?.root,
2028
2029
  children: [
2029
- hasCharacterLimit && /* @__PURE__ */ jsxRuntime.jsx(
2030
+ showCharacterCount && /* @__PURE__ */ jsxRuntime.jsx(
2030
2031
  CharacterCount,
2031
2032
  {
2032
2033
  count: characterCount,
@@ -2185,16 +2186,41 @@ var insertUploadedAttachment = (file, options) => {
2185
2186
  options.onError?.(err, file);
2186
2187
  });
2187
2188
  };
2188
- function createAttachmentsPlugin(options) {
2189
+ function routeFiles(editor, files, options) {
2189
2190
  const { accept } = options;
2191
+ const matching = accept ? files.filter((f) => mimeMatches(f.type, accept)) : files;
2192
+ const handled = matching.filter(
2193
+ (f) => isImageFile(f) || options.onAttachmentAdd !== void 0
2194
+ );
2195
+ for (const file of handled) {
2196
+ if (isImageFile(file)) {
2197
+ insertUploadedImage(editor, file, options);
2198
+ } else {
2199
+ insertUploadedAttachment(file, options);
2200
+ }
2201
+ }
2202
+ const skipped = files.filter((f) => !handled.includes(f));
2203
+ return { handled, skipped };
2204
+ }
2205
+ function createAttachmentsPlugin(options) {
2190
2206
  return {
2191
2207
  name: "attachments",
2208
+ setup(editor) {
2209
+ if (!options.ref) return;
2210
+ const writableRef = options.ref;
2211
+ writableRef.current = {
2212
+ upload: (files) => {
2213
+ if (files.length === 0) return;
2214
+ routeFiles(editor, files, options);
2215
+ }
2216
+ };
2217
+ return () => {
2218
+ writableRef.current = null;
2219
+ };
2220
+ },
2192
2221
  onInsertData(data, { editor, insertData }) {
2193
2222
  const files = extractFiles(data);
2194
- const matching = accept ? files.filter((f) => mimeMatches(f.type, accept)) : files;
2195
- const handled = matching.filter(
2196
- (f) => isImageFile(f) || options.onAttachmentAdd !== void 0
2197
- );
2223
+ const { handled, skipped } = routeFiles(editor, files, options);
2198
2224
  if (handled.length === 0) {
2199
2225
  const htmlImages = extractHtmlImages(data);
2200
2226
  if (htmlImages.length === 0) return false;
@@ -2203,16 +2229,8 @@ function createAttachmentsPlugin(options) {
2203
2229
  }
2204
2230
  return true;
2205
2231
  }
2206
- const unhandled = files.filter((f) => !handled.includes(f));
2207
- if (unhandled.length > 0) {
2208
- insertData(filesOnlyDataTransfer(unhandled));
2209
- }
2210
- for (const file of handled) {
2211
- if (isImageFile(file)) {
2212
- insertUploadedImage(editor, file, options);
2213
- } else {
2214
- insertUploadedAttachment(file, options);
2215
- }
2232
+ if (skipped.length > 0) {
2233
+ insertData(filesOnlyDataTransfer(skipped));
2216
2234
  }
2217
2235
  return true;
2218
2236
  }
package/dist/index.d.cts CHANGED
@@ -280,6 +280,9 @@ interface Attachment {
280
280
  */
281
281
  [key: string]: unknown;
282
282
  }
283
+ interface AttachmentsHandle {
284
+ upload: (files: File[]) => void;
285
+ }
283
286
  interface AttachmentsPluginOptions {
284
287
  /**
285
288
  * Upload a single file and resolve to the public URL, or an object
@@ -292,6 +295,8 @@ interface AttachmentsPluginOptions {
292
295
  * (`image/*`). Files that don't match pass through untouched.
293
296
  */
294
297
  accept?: string;
298
+ /** Populated on editor mount, nulled on unmount. */
299
+ ref?: RefObject<AttachmentsHandle | null>;
295
300
  /**
296
301
  * Placeholder alt text shown on the inserted image element while an
297
302
  * image upload is in flight. Defaults to `"Uploading…"`.
@@ -484,4 +489,4 @@ declare function InkwellRenderer({ content, className, components, rehypePlugins
484
489
  */
485
490
  declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
486
491
 
487
- export { type Attachment, type AttachmentUploadResult, 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -280,6 +280,9 @@ interface Attachment {
280
280
  */
281
281
  [key: string]: unknown;
282
282
  }
283
+ interface AttachmentsHandle {
284
+ upload: (files: File[]) => void;
285
+ }
283
286
  interface AttachmentsPluginOptions {
284
287
  /**
285
288
  * Upload a single file and resolve to the public URL, or an object
@@ -292,6 +295,8 @@ interface AttachmentsPluginOptions {
292
295
  * (`image/*`). Files that don't match pass through untouched.
293
296
  */
294
297
  accept?: string;
298
+ /** Populated on editor mount, nulled on unmount. */
299
+ ref?: RefObject<AttachmentsHandle | null>;
295
300
  /**
296
301
  * Placeholder alt text shown on the inserted image element while an
297
302
  * image upload is in flight. Defaults to `"Uploading…"`.
@@ -484,4 +489,4 @@ declare function InkwellRenderer({ content, className, components, rehypePlugins
484
489
  */
485
490
  declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
486
491
 
487
- export { type Attachment, type AttachmentUploadResult, 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 };
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 };
package/dist/index.js CHANGED
@@ -1362,6 +1362,7 @@ var InkwellEditorClient = forwardRef(
1362
1362
  );
1363
1363
  const overLimit = characterLimit !== void 0 && characterCount > characterLimit;
1364
1364
  const hasCharacterLimit = characterLimit !== void 0;
1365
+ const showCharacterCount = characterLimit !== void 0 && characterCount >= characterLimit * 0.8;
1365
1366
  const getEditorState = useCallback(() => {
1366
1367
  const content2 = serializeContent();
1367
1368
  return {
@@ -2011,7 +2012,7 @@ var InkwellEditorClient = forwardRef(
2011
2012
  className: `inkwell-editor-wrapper${hasCharacterLimit ? " inkwell-editor-has-character-limit" : ""}${overLimit ? " inkwell-editor-over-limit" : ""}${className ? ` ${className}` : ""}${classNames?.root ? ` ${classNames.root}` : ""}`,
2012
2013
  style: styles?.root,
2013
2014
  children: [
2014
- hasCharacterLimit && /* @__PURE__ */ jsx(
2015
+ showCharacterCount && /* @__PURE__ */ jsx(
2015
2016
  CharacterCount,
2016
2017
  {
2017
2018
  count: characterCount,
@@ -2170,16 +2171,41 @@ var insertUploadedAttachment = (file, options) => {
2170
2171
  options.onError?.(err, file);
2171
2172
  });
2172
2173
  };
2173
- function createAttachmentsPlugin(options) {
2174
+ function routeFiles(editor, files, options) {
2174
2175
  const { accept } = options;
2176
+ const matching = accept ? files.filter((f) => mimeMatches(f.type, accept)) : files;
2177
+ const handled = matching.filter(
2178
+ (f) => isImageFile(f) || options.onAttachmentAdd !== void 0
2179
+ );
2180
+ for (const file of handled) {
2181
+ if (isImageFile(file)) {
2182
+ insertUploadedImage(editor, file, options);
2183
+ } else {
2184
+ insertUploadedAttachment(file, options);
2185
+ }
2186
+ }
2187
+ const skipped = files.filter((f) => !handled.includes(f));
2188
+ return { handled, skipped };
2189
+ }
2190
+ function createAttachmentsPlugin(options) {
2175
2191
  return {
2176
2192
  name: "attachments",
2193
+ setup(editor) {
2194
+ if (!options.ref) return;
2195
+ const writableRef = options.ref;
2196
+ writableRef.current = {
2197
+ upload: (files) => {
2198
+ if (files.length === 0) return;
2199
+ routeFiles(editor, files, options);
2200
+ }
2201
+ };
2202
+ return () => {
2203
+ writableRef.current = null;
2204
+ };
2205
+ },
2177
2206
  onInsertData(data, { editor, insertData }) {
2178
2207
  const files = extractFiles(data);
2179
- const matching = accept ? files.filter((f) => mimeMatches(f.type, accept)) : files;
2180
- const handled = matching.filter(
2181
- (f) => isImageFile(f) || options.onAttachmentAdd !== void 0
2182
- );
2208
+ const { handled, skipped } = routeFiles(editor, files, options);
2183
2209
  if (handled.length === 0) {
2184
2210
  const htmlImages = extractHtmlImages(data);
2185
2211
  if (htmlImages.length === 0) return false;
@@ -2188,16 +2214,8 @@ function createAttachmentsPlugin(options) {
2188
2214
  }
2189
2215
  return true;
2190
2216
  }
2191
- const unhandled = files.filter((f) => !handled.includes(f));
2192
- if (unhandled.length > 0) {
2193
- insertData(filesOnlyDataTransfer(unhandled));
2194
- }
2195
- for (const file of handled) {
2196
- if (isImageFile(file)) {
2197
- insertUploadedImage(editor, file, options);
2198
- } else {
2199
- insertUploadedAttachment(file, options);
2200
- }
2217
+ if (skipped.length > 0) {
2218
+ insertData(filesOnlyDataTransfer(skipped));
2201
2219
  }
2202
2220
  return true;
2203
2221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@railway/inkwell",
3
- "version": "1.1.1",
3
+ "version": "1.2.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
@@ -31,6 +31,7 @@
31
31
  --inkwell-accent: hsl(217, 91%, 50%);
32
32
  --inkwell-accent-soft: hsla(217, 91%, 50%, 0.12);
33
33
  --inkwell-danger: hsl(0, 72%, 51%);
34
+ --inkwell-danger-soft: hsla(0, 72%, 51%, 0.14);
34
35
 
35
36
  /* Inline code */
36
37
  --inkwell-code-bg: hsl(220, 14%, 94%);
@@ -61,6 +62,8 @@
61
62
 
62
63
  --inkwell-accent: hsl(217, 91%, 65%);
63
64
  --inkwell-accent-soft: hsla(217, 91%, 65%, 0.16);
65
+ --inkwell-danger: hsl(0, 70%, 65%);
66
+ --inkwell-danger-soft: hsla(0, 70%, 65%, 0.18);
64
67
 
65
68
  --inkwell-code-bg: hsl(220, 13%, 18%);
66
69
  --inkwell-code-fg: hsl(340, 70%, 75%);
@@ -175,36 +178,35 @@
175
178
  height: auto;
176
179
  }
177
180
 
178
- /* Built-in character count. Anchored to the bottom-right of the editor
179
- wrapper. `characterLimit` is a soft hint typing past the limit is
180
- allowed; the count turns red and the wrapper picks up
181
- `.inkwell-editor-over-limit`, which paints a red ring on the editor
182
- surface so it's visually obvious the document is over budget. */
181
+ /* Built-in character count. Overlays the top-right corner of the editor
182
+ wrapper so it never shifts content, and sits on a solid surface tint
183
+ so it visually layers above any text that wraps underneath. Only
184
+ rendered once typing reaches 80% of `characterLimit`, since the
185
+ limit is a soft hint typing past it is allowed; the count then
186
+ turns red and the wrapper picks up `.inkwell-editor-over-limit`,
187
+ which paints a soft red halo on the editor surface so it's visually
188
+ obvious the document is over budget. */
183
189
  .inkwell-editor-character-count {
184
190
  position: absolute;
185
- right: 0.7rem;
186
- bottom: 0.7rem;
191
+ top: 0.5rem;
192
+ right: 0.5rem;
187
193
  z-index: 10;
188
- padding: 0.15rem 0.45rem;
194
+ padding: 0.1rem 0.4rem;
189
195
  font-size: 0.72rem;
190
196
  font-variant-numeric: tabular-nums;
191
197
  color: var(--inkwell-text-dim);
198
+ background: var(--inkwell-bg);
199
+ border-radius: calc(var(--inkwell-radius) - 2px);
192
200
  pointer-events: none;
193
201
  user-select: none;
194
202
  }
195
203
  .inkwell-editor-character-count-over {
196
- color: hsl(0, 75%, 55%);
204
+ color: var(--inkwell-danger);
197
205
  font-weight: 500;
198
206
  }
199
- :where(
200
- .inkwell-editor-wrapper.inkwell-editor-has-character-limit .inkwell-editor
201
- ) {
202
- padding-right: 5.25rem;
203
- padding-bottom: 2.25rem;
204
- }
205
207
  .inkwell-editor-wrapper.inkwell-editor-over-limit .inkwell-editor {
206
- border-color: hsl(0, 75%, 55%);
207
- box-shadow: 0 0 0 1px hsl(0, 75%, 55%);
208
+ border-color: var(--inkwell-danger-soft);
209
+ box-shadow: 0 0 0 3px var(--inkwell-danger-soft);
208
210
  }
209
211
 
210
212
  .inkwell-editor-backtick,