@railway/inkwell 1.1.0 → 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/README.md +9 -17
- package/dist/index.cjs +144 -46
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +144 -46
- package/package.json +2 -1
- package/src/styles.css +64 -21
package/README.md
CHANGED
|
@@ -51,26 +51,18 @@ pnpm dev
|
|
|
51
51
|
|
|
52
52
|
## Releases
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
Releases are automated. Every PR carries exactly one label that determines the next version bump:
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
| Label | Effect |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `release/patch` | Bumps the patch version (e.g. `1.1.0` → `1.1.1`) |
|
|
59
|
+
| `release/minor` | Bumps the minor version (e.g. `1.1.0` → `1.2.0`) |
|
|
60
|
+
| `release/major` | Bumps the major version (e.g. `1.1.0` → `2.0.0`) |
|
|
61
|
+
| `release/skip` | No version bump (use for docs-only or chore PRs) |
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
pnpm changeset # pick bump type, write a user-facing summary
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Commit the generated `.changeset/*.md` file alongside your code.
|
|
63
|
-
|
|
64
|
-
To cut a release from `main`:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
pnpm changeset version # bumps packages/inkwell/package.json + writes CHANGELOG.md
|
|
68
|
-
git commit -am "🚀 release: v$(node -p "require('./packages/inkwell/package.json').version")"
|
|
69
|
-
git tag "v$(node -p "require('./packages/inkwell/package.json').version")"
|
|
70
|
-
git push --follow-tags
|
|
71
|
-
```
|
|
63
|
+
When a labeled PR merges to `main`, the `auto-release` workflow batches with any other recently-merged PRs, picks the highest bump across them, tags `vX.Y.Z`, and pushes. The tag push triggers the `publish` workflow, which generates release notes from PR titles since the previous tag, drafts a GitHub Release, publishes to npm, and un-drafts.
|
|
72
64
|
|
|
73
|
-
|
|
65
|
+
Release notes live on the [GitHub Releases page](https://github.com/railwayapp/inkwell/releases). There is no in-repo `CHANGELOG.md`.
|
|
74
66
|
|
|
75
67
|
## License
|
|
76
68
|
|
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 {
|
|
@@ -1436,7 +1437,8 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1436
1437
|
activePluginRef.current = activePlugin;
|
|
1437
1438
|
const pluginPositionRef = react.useRef({
|
|
1438
1439
|
top: 0,
|
|
1439
|
-
left: 0
|
|
1440
|
+
left: 0,
|
|
1441
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1440
1442
|
});
|
|
1441
1443
|
const forwardedKeyListenersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
1442
1444
|
const wrapperRef = react.useRef(null);
|
|
@@ -1547,10 +1549,14 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1547
1549
|
]
|
|
1548
1550
|
);
|
|
1549
1551
|
const getCursorPosition = react.useCallback(() => {
|
|
1552
|
+
const empty = {
|
|
1553
|
+
top: 0,
|
|
1554
|
+
left: 0,
|
|
1555
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1556
|
+
};
|
|
1550
1557
|
try {
|
|
1551
1558
|
const domSelection = window.getSelection();
|
|
1552
|
-
if (!domSelection || domSelection.rangeCount === 0)
|
|
1553
|
-
return { top: 0, left: 0 };
|
|
1559
|
+
if (!domSelection || domSelection.rangeCount === 0) return empty;
|
|
1554
1560
|
const range = domSelection.getRangeAt(0);
|
|
1555
1561
|
let rect = range.getBoundingClientRect();
|
|
1556
1562
|
if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
|
|
@@ -1558,14 +1564,22 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1558
1564
|
if (node) rect = node.getBoundingClientRect();
|
|
1559
1565
|
}
|
|
1560
1566
|
const wrapperEl = wrapperRef.current;
|
|
1561
|
-
if (!wrapperEl) return
|
|
1567
|
+
if (!wrapperEl) return empty;
|
|
1562
1568
|
const wrapperRect = wrapperEl.getBoundingClientRect();
|
|
1569
|
+
const cursorTop = rect.top - wrapperRect.top;
|
|
1570
|
+
const cursorBottom = rect.bottom - wrapperRect.top;
|
|
1571
|
+
const cursorLeft = rect.left - wrapperRect.left;
|
|
1563
1572
|
return {
|
|
1564
|
-
top:
|
|
1565
|
-
left:
|
|
1573
|
+
top: cursorBottom + 4,
|
|
1574
|
+
left: cursorLeft,
|
|
1575
|
+
cursorRect: {
|
|
1576
|
+
top: cursorTop,
|
|
1577
|
+
bottom: cursorBottom,
|
|
1578
|
+
left: cursorLeft
|
|
1579
|
+
}
|
|
1566
1580
|
};
|
|
1567
1581
|
} catch {
|
|
1568
|
-
return
|
|
1582
|
+
return empty;
|
|
1569
1583
|
}
|
|
1570
1584
|
}, []);
|
|
1571
1585
|
const wrapSelection = react.useCallback(
|
|
@@ -1833,7 +1847,11 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1833
1847
|
query: activePlugin === plugin ? activePluginQuery : "",
|
|
1834
1848
|
onSelect: handlePluginSelect,
|
|
1835
1849
|
onDismiss: dismissPlugin,
|
|
1836
|
-
position:
|
|
1850
|
+
position: {
|
|
1851
|
+
top: pluginPositionRef.current.top,
|
|
1852
|
+
left: pluginPositionRef.current.left
|
|
1853
|
+
},
|
|
1854
|
+
cursorRect: pluginPositionRef.current.cursorRect,
|
|
1837
1855
|
editorRef: editorElRef,
|
|
1838
1856
|
editor: pluginEditor,
|
|
1839
1857
|
wrapSelection,
|
|
@@ -2009,7 +2027,7 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
2009
2027
|
className: `inkwell-editor-wrapper${hasCharacterLimit ? " inkwell-editor-has-character-limit" : ""}${overLimit ? " inkwell-editor-over-limit" : ""}${className ? ` ${className}` : ""}${classNames?.root ? ` ${classNames.root}` : ""}`,
|
|
2010
2028
|
style: styles?.root,
|
|
2011
2029
|
children: [
|
|
2012
|
-
|
|
2030
|
+
showCharacterCount && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2013
2031
|
CharacterCount,
|
|
2014
2032
|
{
|
|
2015
2033
|
count: characterCount,
|
|
@@ -2168,16 +2186,41 @@ var insertUploadedAttachment = (file, options) => {
|
|
|
2168
2186
|
options.onError?.(err, file);
|
|
2169
2187
|
});
|
|
2170
2188
|
};
|
|
2171
|
-
function
|
|
2189
|
+
function routeFiles(editor, files, options) {
|
|
2172
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) {
|
|
2173
2206
|
return {
|
|
2174
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
|
+
},
|
|
2175
2221
|
onInsertData(data, { editor, insertData }) {
|
|
2176
2222
|
const files = extractFiles(data);
|
|
2177
|
-
const
|
|
2178
|
-
const handled = matching.filter(
|
|
2179
|
-
(f) => isImageFile(f) || options.onAttachmentAdd !== void 0
|
|
2180
|
-
);
|
|
2223
|
+
const { handled, skipped } = routeFiles(editor, files, options);
|
|
2181
2224
|
if (handled.length === 0) {
|
|
2182
2225
|
const htmlImages = extractHtmlImages(data);
|
|
2183
2226
|
if (htmlImages.length === 0) return false;
|
|
@@ -2186,16 +2229,8 @@ function createAttachmentsPlugin(options) {
|
|
|
2186
2229
|
}
|
|
2187
2230
|
return true;
|
|
2188
2231
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
insertData(filesOnlyDataTransfer(unhandled));
|
|
2192
|
-
}
|
|
2193
|
-
for (const file of handled) {
|
|
2194
|
-
if (isImageFile(file)) {
|
|
2195
|
-
insertUploadedImage(editor, file, options);
|
|
2196
|
-
} else {
|
|
2197
|
-
insertUploadedAttachment(file, options);
|
|
2198
|
-
}
|
|
2232
|
+
if (skipped.length > 0) {
|
|
2233
|
+
insertData(filesOnlyDataTransfer(skipped));
|
|
2199
2234
|
}
|
|
2200
2235
|
return true;
|
|
2201
2236
|
}
|
|
@@ -2273,8 +2308,10 @@ function createCompletionsPlugin({
|
|
|
2273
2308
|
var BASE = "inkwell-plugin-picker";
|
|
2274
2309
|
var pluginPickerClass = {
|
|
2275
2310
|
popup: `${BASE}-popup`,
|
|
2311
|
+
popupFlipped: `${BASE}-popup-flipped`,
|
|
2276
2312
|
picker: `${BASE}`,
|
|
2277
2313
|
search: `${BASE}-search`,
|
|
2314
|
+
list: `${BASE}-list`,
|
|
2278
2315
|
item: `${BASE}-item`,
|
|
2279
2316
|
itemActive: `${BASE}-item-active`,
|
|
2280
2317
|
empty: `${BASE}-empty`,
|
|
@@ -2282,6 +2319,74 @@ var pluginPickerClass = {
|
|
|
2282
2319
|
subtitle: `${BASE}-subtitle`,
|
|
2283
2320
|
preview: `${BASE}-preview`
|
|
2284
2321
|
};
|
|
2322
|
+
var POPUP_GAP = 4;
|
|
2323
|
+
var VIEWPORT_MARGIN = 8;
|
|
2324
|
+
var CURSOR_HEIGHT_FALLBACK = 20;
|
|
2325
|
+
function usePickerPlacement(position, cursorRect) {
|
|
2326
|
+
const [popupEl, setPopupEl] = react.useState(null);
|
|
2327
|
+
const [placement, setPlacement] = react.useState({ flippedAbove: false, leftOverride: null });
|
|
2328
|
+
const cursorTopWrapper = cursorRect?.top ?? Math.max(0, position.top - POPUP_GAP - CURSOR_HEIGHT_FALLBACK);
|
|
2329
|
+
const cursorBottomWrapper = cursorRect?.bottom ?? Math.max(0, position.top - POPUP_GAP);
|
|
2330
|
+
const cursorLeftWrapper = cursorRect?.left ?? position.left;
|
|
2331
|
+
react.useLayoutEffect(() => {
|
|
2332
|
+
if (!popupEl) return;
|
|
2333
|
+
const measure = () => {
|
|
2334
|
+
const offsetParent = popupEl.offsetParent;
|
|
2335
|
+
if (!(offsetParent instanceof HTMLElement)) return;
|
|
2336
|
+
const wrapperRect = offsetParent.getBoundingClientRect();
|
|
2337
|
+
const popupRect = popupEl.getBoundingClientRect();
|
|
2338
|
+
const popupHeight = popupRect.height;
|
|
2339
|
+
const popupWidth = popupRect.width;
|
|
2340
|
+
const cursorTopViewport = wrapperRect.top + cursorTopWrapper;
|
|
2341
|
+
const cursorBottomViewport = wrapperRect.top + cursorBottomWrapper;
|
|
2342
|
+
const cursorLeftViewport = wrapperRect.left + cursorLeftWrapper;
|
|
2343
|
+
const spaceBelowViewport = window.innerHeight - cursorBottomViewport - VIEWPORT_MARGIN;
|
|
2344
|
+
const spaceBelowWrapper = wrapperRect.bottom - cursorBottomViewport;
|
|
2345
|
+
const spaceBelow = Math.min(spaceBelowViewport, spaceBelowWrapper);
|
|
2346
|
+
const spaceAbove = cursorTopViewport - VIEWPORT_MARGIN;
|
|
2347
|
+
const needsFlip = popupHeight + POPUP_GAP > spaceBelow;
|
|
2348
|
+
const fitsAbove = popupHeight + POPUP_GAP <= spaceAbove;
|
|
2349
|
+
const flippedAbove = needsFlip && fitsAbove;
|
|
2350
|
+
const maxRightViewport = window.innerWidth - VIEWPORT_MARGIN;
|
|
2351
|
+
const popupRightIfDefault = cursorLeftViewport + popupWidth;
|
|
2352
|
+
const leftOverride = popupRightIfDefault > maxRightViewport ? Math.max(
|
|
2353
|
+
VIEWPORT_MARGIN - wrapperRect.left,
|
|
2354
|
+
maxRightViewport - popupWidth - wrapperRect.left
|
|
2355
|
+
) : null;
|
|
2356
|
+
setPlacement(
|
|
2357
|
+
(prev) => prev.flippedAbove === flippedAbove && prev.leftOverride === leftOverride ? prev : { flippedAbove, leftOverride }
|
|
2358
|
+
);
|
|
2359
|
+
};
|
|
2360
|
+
measure();
|
|
2361
|
+
const ResizeObserverCtor = typeof window !== "undefined" ? window.ResizeObserver : void 0;
|
|
2362
|
+
const resizeObserver = ResizeObserverCtor ? new ResizeObserverCtor(() => measure()) : null;
|
|
2363
|
+
resizeObserver?.observe(popupEl);
|
|
2364
|
+
window.addEventListener("resize", measure);
|
|
2365
|
+
window.addEventListener("scroll", measure, true);
|
|
2366
|
+
return () => {
|
|
2367
|
+
resizeObserver?.disconnect();
|
|
2368
|
+
window.removeEventListener("resize", measure);
|
|
2369
|
+
window.removeEventListener("scroll", measure, true);
|
|
2370
|
+
};
|
|
2371
|
+
}, [popupEl, cursorTopWrapper, cursorBottomWrapper, cursorLeftWrapper]);
|
|
2372
|
+
const style = {
|
|
2373
|
+
position: "absolute",
|
|
2374
|
+
top: placement.flippedAbove ? cursorTopWrapper - POPUP_GAP : position.top,
|
|
2375
|
+
left: placement.leftOverride ?? position.left,
|
|
2376
|
+
transform: placement.flippedAbove ? "translateY(-100%)" : void 0,
|
|
2377
|
+
zIndex: 1001
|
|
2378
|
+
};
|
|
2379
|
+
const className = placement.flippedAbove ? `${pluginPickerClass.popup} ${pluginPickerClass.popupFlipped}` : pluginPickerClass.popup;
|
|
2380
|
+
return {
|
|
2381
|
+
setPopupEl,
|
|
2382
|
+
style,
|
|
2383
|
+
className,
|
|
2384
|
+
flippedAbove: placement.flippedAbove
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
function usePluginPopupPlacement(position, cursorRect) {
|
|
2388
|
+
return usePickerPlacement(position, cursorRect);
|
|
2389
|
+
}
|
|
2285
2390
|
function PluginMenuPrimitive({
|
|
2286
2391
|
items,
|
|
2287
2392
|
search,
|
|
@@ -2293,6 +2398,7 @@ function PluginMenuPrimitive({
|
|
|
2293
2398
|
onSelect,
|
|
2294
2399
|
onDismiss,
|
|
2295
2400
|
position,
|
|
2401
|
+
cursorRect,
|
|
2296
2402
|
subscribeForwardedKey
|
|
2297
2403
|
}) {
|
|
2298
2404
|
const [selectedIndex, setSelectedIndex] = react.useState(0);
|
|
@@ -2437,16 +2543,13 @@ function PluginMenuPrimitive({
|
|
|
2437
2543
|
updateSelectedIndex
|
|
2438
2544
|
]
|
|
2439
2545
|
);
|
|
2546
|
+
const placement = usePickerPlacement(position, cursorRect);
|
|
2440
2547
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2441
2548
|
"div",
|
|
2442
2549
|
{
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
top: position.top,
|
|
2447
|
-
left: position.left,
|
|
2448
|
-
zIndex: 1001
|
|
2449
|
-
},
|
|
2550
|
+
ref: placement.setPopupEl,
|
|
2551
|
+
className: placement.className,
|
|
2552
|
+
style: placement.style,
|
|
2450
2553
|
onMouseDown: (event) => event.preventDefault(),
|
|
2451
2554
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2452
2555
|
"div",
|
|
@@ -2460,7 +2563,7 @@ function PluginMenuPrimitive({
|
|
|
2460
2563
|
"aria-activedescendant": results.length > 0 ? activeOptionId : void 0,
|
|
2461
2564
|
children: [
|
|
2462
2565
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.search, "aria-label": placeholder, children: query || placeholder }),
|
|
2463
|
-
results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.empty, role: "status", children: emptyMessage }) : /* @__PURE__ */ jsxRuntime.jsx("div", { id: listboxId, role: "listbox", children: renderedResults })
|
|
2566
|
+
results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.empty, role: "status", children: emptyMessage }) : /* @__PURE__ */ jsxRuntime.jsx("div", { id: listboxId, role: "listbox", className: pluginPickerClass.list, children: renderedResults })
|
|
2464
2567
|
]
|
|
2465
2568
|
}
|
|
2466
2569
|
)
|
|
@@ -2622,6 +2725,7 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2622
2725
|
onExecute,
|
|
2623
2726
|
onDismiss,
|
|
2624
2727
|
position,
|
|
2728
|
+
cursorRect,
|
|
2625
2729
|
getEditor
|
|
2626
2730
|
}, ref) {
|
|
2627
2731
|
const [mode, setMode] = react.useState("commands");
|
|
@@ -2785,17 +2889,14 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2785
2889
|
selectedIndex
|
|
2786
2890
|
]
|
|
2787
2891
|
);
|
|
2892
|
+
const placement = usePluginPopupPlacement(position, cursorRect);
|
|
2788
2893
|
if (mode === "ready" && selectedCommand) {
|
|
2789
2894
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2790
2895
|
"div",
|
|
2791
2896
|
{
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
top: position.top,
|
|
2796
|
-
left: position.left,
|
|
2797
|
-
zIndex: 1001
|
|
2798
|
-
},
|
|
2897
|
+
ref: placement.setPopupEl,
|
|
2898
|
+
className: placement.className,
|
|
2899
|
+
style: placement.style,
|
|
2799
2900
|
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.picker, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "inkwell-plugin-slash-commands-execute", children: "Enter to execute \xB7 Esc to cancel" }) })
|
|
2800
2901
|
}
|
|
2801
2902
|
);
|
|
@@ -2803,13 +2904,9 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2803
2904
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2804
2905
|
"div",
|
|
2805
2906
|
{
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
top: position.top,
|
|
2810
|
-
left: position.left,
|
|
2811
|
-
zIndex: 1001
|
|
2812
|
-
},
|
|
2907
|
+
ref: placement.setPopupEl,
|
|
2908
|
+
className: placement.className,
|
|
2909
|
+
style: placement.style,
|
|
2813
2910
|
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: pluginPickerClass.picker, children: [
|
|
2814
2911
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.search, children: mode === "commands" ? `/${query}` : `${selectedCommand ? `/${selectedCommand.name} ` : ""}${query}` }),
|
|
2815
2912
|
loadingArgs && items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.empty, children: "Loading..." }) : items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.empty, children: emptyMessage }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2818,6 +2915,7 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2818
2915
|
id: listboxId,
|
|
2819
2916
|
role: "listbox",
|
|
2820
2917
|
"aria-activedescendant": activeOptionId,
|
|
2918
|
+
className: pluginPickerClass.list,
|
|
2821
2919
|
children: items.map((item, index) => {
|
|
2822
2920
|
const active = index === selectedIndex;
|
|
2823
2921
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
package/dist/index.d.cts
CHANGED
|
@@ -180,11 +180,20 @@ interface PluginRenderProps {
|
|
|
180
180
|
onSelect: (content: string) => void;
|
|
181
181
|
/** Deactivate this plugin. */
|
|
182
182
|
onDismiss: () => void;
|
|
183
|
-
/** Cursor position when the trigger fired.
|
|
183
|
+
/** Cursor position when the trigger fired. Wrapper-relative; sits just
|
|
184
|
+
* below the caret so a popup with `top: position.top` renders below it. */
|
|
184
185
|
position: {
|
|
185
186
|
top: number;
|
|
186
187
|
left: number;
|
|
187
188
|
};
|
|
189
|
+
/** Wrapper-relative caret bounding rect when the trigger fired. Lets pickers
|
|
190
|
+
* flip above the caret when there isn't room below. Optional for backwards
|
|
191
|
+
* compatibility with consumers that pass synthesized props. */
|
|
192
|
+
cursorRect?: {
|
|
193
|
+
top: number;
|
|
194
|
+
bottom: number;
|
|
195
|
+
left: number;
|
|
196
|
+
};
|
|
188
197
|
/** Ref to the editable DOM element. */
|
|
189
198
|
editorRef: RefObject<HTMLDivElement | null>;
|
|
190
199
|
/** Narrow editor controller for plugin actions. */
|
|
@@ -271,6 +280,9 @@ interface Attachment {
|
|
|
271
280
|
*/
|
|
272
281
|
[key: string]: unknown;
|
|
273
282
|
}
|
|
283
|
+
interface AttachmentsHandle {
|
|
284
|
+
upload: (files: File[]) => void;
|
|
285
|
+
}
|
|
274
286
|
interface AttachmentsPluginOptions {
|
|
275
287
|
/**
|
|
276
288
|
* Upload a single file and resolve to the public URL, or an object
|
|
@@ -283,6 +295,8 @@ interface AttachmentsPluginOptions {
|
|
|
283
295
|
* (`image/*`). Files that don't match pass through untouched.
|
|
284
296
|
*/
|
|
285
297
|
accept?: string;
|
|
298
|
+
/** Populated on editor mount, nulled on unmount. */
|
|
299
|
+
ref?: RefObject<AttachmentsHandle | null>;
|
|
286
300
|
/**
|
|
287
301
|
* Placeholder alt text shown on the inserted image element while an
|
|
288
302
|
* image upload is in flight. Defaults to `"Uploading…"`.
|
|
@@ -475,4 +489,4 @@ declare function InkwellRenderer({ content, className, components, rehypePlugins
|
|
|
475
489
|
*/
|
|
476
490
|
declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
|
|
477
491
|
|
|
478
|
-
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
|
@@ -180,11 +180,20 @@ interface PluginRenderProps {
|
|
|
180
180
|
onSelect: (content: string) => void;
|
|
181
181
|
/** Deactivate this plugin. */
|
|
182
182
|
onDismiss: () => void;
|
|
183
|
-
/** Cursor position when the trigger fired.
|
|
183
|
+
/** Cursor position when the trigger fired. Wrapper-relative; sits just
|
|
184
|
+
* below the caret so a popup with `top: position.top` renders below it. */
|
|
184
185
|
position: {
|
|
185
186
|
top: number;
|
|
186
187
|
left: number;
|
|
187
188
|
};
|
|
189
|
+
/** Wrapper-relative caret bounding rect when the trigger fired. Lets pickers
|
|
190
|
+
* flip above the caret when there isn't room below. Optional for backwards
|
|
191
|
+
* compatibility with consumers that pass synthesized props. */
|
|
192
|
+
cursorRect?: {
|
|
193
|
+
top: number;
|
|
194
|
+
bottom: number;
|
|
195
|
+
left: number;
|
|
196
|
+
};
|
|
188
197
|
/** Ref to the editable DOM element. */
|
|
189
198
|
editorRef: RefObject<HTMLDivElement | null>;
|
|
190
199
|
/** Narrow editor controller for plugin actions. */
|
|
@@ -271,6 +280,9 @@ interface Attachment {
|
|
|
271
280
|
*/
|
|
272
281
|
[key: string]: unknown;
|
|
273
282
|
}
|
|
283
|
+
interface AttachmentsHandle {
|
|
284
|
+
upload: (files: File[]) => void;
|
|
285
|
+
}
|
|
274
286
|
interface AttachmentsPluginOptions {
|
|
275
287
|
/**
|
|
276
288
|
* Upload a single file and resolve to the public URL, or an object
|
|
@@ -283,6 +295,8 @@ interface AttachmentsPluginOptions {
|
|
|
283
295
|
* (`image/*`). Files that don't match pass through untouched.
|
|
284
296
|
*/
|
|
285
297
|
accept?: string;
|
|
298
|
+
/** Populated on editor mount, nulled on unmount. */
|
|
299
|
+
ref?: RefObject<AttachmentsHandle | null>;
|
|
286
300
|
/**
|
|
287
301
|
* Placeholder alt text shown on the inserted image element while an
|
|
288
302
|
* image upload is in flight. Defaults to `"Uploading…"`.
|
|
@@ -475,4 +489,4 @@ declare function InkwellRenderer({ content, className, components, rehypePlugins
|
|
|
475
489
|
*/
|
|
476
490
|
declare function parseMarkdown(content: string, options?: ParseMarkdownOptions): ReactNode;
|
|
477
491
|
|
|
478
|
-
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 {
|
|
@@ -1421,7 +1422,8 @@ var InkwellEditorClient = forwardRef(
|
|
|
1421
1422
|
activePluginRef.current = activePlugin;
|
|
1422
1423
|
const pluginPositionRef = useRef({
|
|
1423
1424
|
top: 0,
|
|
1424
|
-
left: 0
|
|
1425
|
+
left: 0,
|
|
1426
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1425
1427
|
});
|
|
1426
1428
|
const forwardedKeyListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
1427
1429
|
const wrapperRef = useRef(null);
|
|
@@ -1532,10 +1534,14 @@ var InkwellEditorClient = forwardRef(
|
|
|
1532
1534
|
]
|
|
1533
1535
|
);
|
|
1534
1536
|
const getCursorPosition = useCallback(() => {
|
|
1537
|
+
const empty = {
|
|
1538
|
+
top: 0,
|
|
1539
|
+
left: 0,
|
|
1540
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1541
|
+
};
|
|
1535
1542
|
try {
|
|
1536
1543
|
const domSelection = window.getSelection();
|
|
1537
|
-
if (!domSelection || domSelection.rangeCount === 0)
|
|
1538
|
-
return { top: 0, left: 0 };
|
|
1544
|
+
if (!domSelection || domSelection.rangeCount === 0) return empty;
|
|
1539
1545
|
const range = domSelection.getRangeAt(0);
|
|
1540
1546
|
let rect = range.getBoundingClientRect();
|
|
1541
1547
|
if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
|
|
@@ -1543,14 +1549,22 @@ var InkwellEditorClient = forwardRef(
|
|
|
1543
1549
|
if (node) rect = node.getBoundingClientRect();
|
|
1544
1550
|
}
|
|
1545
1551
|
const wrapperEl = wrapperRef.current;
|
|
1546
|
-
if (!wrapperEl) return
|
|
1552
|
+
if (!wrapperEl) return empty;
|
|
1547
1553
|
const wrapperRect = wrapperEl.getBoundingClientRect();
|
|
1554
|
+
const cursorTop = rect.top - wrapperRect.top;
|
|
1555
|
+
const cursorBottom = rect.bottom - wrapperRect.top;
|
|
1556
|
+
const cursorLeft = rect.left - wrapperRect.left;
|
|
1548
1557
|
return {
|
|
1549
|
-
top:
|
|
1550
|
-
left:
|
|
1558
|
+
top: cursorBottom + 4,
|
|
1559
|
+
left: cursorLeft,
|
|
1560
|
+
cursorRect: {
|
|
1561
|
+
top: cursorTop,
|
|
1562
|
+
bottom: cursorBottom,
|
|
1563
|
+
left: cursorLeft
|
|
1564
|
+
}
|
|
1551
1565
|
};
|
|
1552
1566
|
} catch {
|
|
1553
|
-
return
|
|
1567
|
+
return empty;
|
|
1554
1568
|
}
|
|
1555
1569
|
}, []);
|
|
1556
1570
|
const wrapSelection = useCallback(
|
|
@@ -1818,7 +1832,11 @@ var InkwellEditorClient = forwardRef(
|
|
|
1818
1832
|
query: activePlugin === plugin ? activePluginQuery : "",
|
|
1819
1833
|
onSelect: handlePluginSelect,
|
|
1820
1834
|
onDismiss: dismissPlugin,
|
|
1821
|
-
position:
|
|
1835
|
+
position: {
|
|
1836
|
+
top: pluginPositionRef.current.top,
|
|
1837
|
+
left: pluginPositionRef.current.left
|
|
1838
|
+
},
|
|
1839
|
+
cursorRect: pluginPositionRef.current.cursorRect,
|
|
1822
1840
|
editorRef: editorElRef,
|
|
1823
1841
|
editor: pluginEditor,
|
|
1824
1842
|
wrapSelection,
|
|
@@ -1994,7 +2012,7 @@ var InkwellEditorClient = forwardRef(
|
|
|
1994
2012
|
className: `inkwell-editor-wrapper${hasCharacterLimit ? " inkwell-editor-has-character-limit" : ""}${overLimit ? " inkwell-editor-over-limit" : ""}${className ? ` ${className}` : ""}${classNames?.root ? ` ${classNames.root}` : ""}`,
|
|
1995
2013
|
style: styles?.root,
|
|
1996
2014
|
children: [
|
|
1997
|
-
|
|
2015
|
+
showCharacterCount && /* @__PURE__ */ jsx(
|
|
1998
2016
|
CharacterCount,
|
|
1999
2017
|
{
|
|
2000
2018
|
count: characterCount,
|
|
@@ -2153,16 +2171,41 @@ var insertUploadedAttachment = (file, options) => {
|
|
|
2153
2171
|
options.onError?.(err, file);
|
|
2154
2172
|
});
|
|
2155
2173
|
};
|
|
2156
|
-
function
|
|
2174
|
+
function routeFiles(editor, files, options) {
|
|
2157
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) {
|
|
2158
2191
|
return {
|
|
2159
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
|
+
},
|
|
2160
2206
|
onInsertData(data, { editor, insertData }) {
|
|
2161
2207
|
const files = extractFiles(data);
|
|
2162
|
-
const
|
|
2163
|
-
const handled = matching.filter(
|
|
2164
|
-
(f) => isImageFile(f) || options.onAttachmentAdd !== void 0
|
|
2165
|
-
);
|
|
2208
|
+
const { handled, skipped } = routeFiles(editor, files, options);
|
|
2166
2209
|
if (handled.length === 0) {
|
|
2167
2210
|
const htmlImages = extractHtmlImages(data);
|
|
2168
2211
|
if (htmlImages.length === 0) return false;
|
|
@@ -2171,16 +2214,8 @@ function createAttachmentsPlugin(options) {
|
|
|
2171
2214
|
}
|
|
2172
2215
|
return true;
|
|
2173
2216
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
insertData(filesOnlyDataTransfer(unhandled));
|
|
2177
|
-
}
|
|
2178
|
-
for (const file of handled) {
|
|
2179
|
-
if (isImageFile(file)) {
|
|
2180
|
-
insertUploadedImage(editor, file, options);
|
|
2181
|
-
} else {
|
|
2182
|
-
insertUploadedAttachment(file, options);
|
|
2183
|
-
}
|
|
2217
|
+
if (skipped.length > 0) {
|
|
2218
|
+
insertData(filesOnlyDataTransfer(skipped));
|
|
2184
2219
|
}
|
|
2185
2220
|
return true;
|
|
2186
2221
|
}
|
|
@@ -2258,8 +2293,10 @@ function createCompletionsPlugin({
|
|
|
2258
2293
|
var BASE = "inkwell-plugin-picker";
|
|
2259
2294
|
var pluginPickerClass = {
|
|
2260
2295
|
popup: `${BASE}-popup`,
|
|
2296
|
+
popupFlipped: `${BASE}-popup-flipped`,
|
|
2261
2297
|
picker: `${BASE}`,
|
|
2262
2298
|
search: `${BASE}-search`,
|
|
2299
|
+
list: `${BASE}-list`,
|
|
2263
2300
|
item: `${BASE}-item`,
|
|
2264
2301
|
itemActive: `${BASE}-item-active`,
|
|
2265
2302
|
empty: `${BASE}-empty`,
|
|
@@ -2267,6 +2304,74 @@ var pluginPickerClass = {
|
|
|
2267
2304
|
subtitle: `${BASE}-subtitle`,
|
|
2268
2305
|
preview: `${BASE}-preview`
|
|
2269
2306
|
};
|
|
2307
|
+
var POPUP_GAP = 4;
|
|
2308
|
+
var VIEWPORT_MARGIN = 8;
|
|
2309
|
+
var CURSOR_HEIGHT_FALLBACK = 20;
|
|
2310
|
+
function usePickerPlacement(position, cursorRect) {
|
|
2311
|
+
const [popupEl, setPopupEl] = useState(null);
|
|
2312
|
+
const [placement, setPlacement] = useState({ flippedAbove: false, leftOverride: null });
|
|
2313
|
+
const cursorTopWrapper = cursorRect?.top ?? Math.max(0, position.top - POPUP_GAP - CURSOR_HEIGHT_FALLBACK);
|
|
2314
|
+
const cursorBottomWrapper = cursorRect?.bottom ?? Math.max(0, position.top - POPUP_GAP);
|
|
2315
|
+
const cursorLeftWrapper = cursorRect?.left ?? position.left;
|
|
2316
|
+
useLayoutEffect(() => {
|
|
2317
|
+
if (!popupEl) return;
|
|
2318
|
+
const measure = () => {
|
|
2319
|
+
const offsetParent = popupEl.offsetParent;
|
|
2320
|
+
if (!(offsetParent instanceof HTMLElement)) return;
|
|
2321
|
+
const wrapperRect = offsetParent.getBoundingClientRect();
|
|
2322
|
+
const popupRect = popupEl.getBoundingClientRect();
|
|
2323
|
+
const popupHeight = popupRect.height;
|
|
2324
|
+
const popupWidth = popupRect.width;
|
|
2325
|
+
const cursorTopViewport = wrapperRect.top + cursorTopWrapper;
|
|
2326
|
+
const cursorBottomViewport = wrapperRect.top + cursorBottomWrapper;
|
|
2327
|
+
const cursorLeftViewport = wrapperRect.left + cursorLeftWrapper;
|
|
2328
|
+
const spaceBelowViewport = window.innerHeight - cursorBottomViewport - VIEWPORT_MARGIN;
|
|
2329
|
+
const spaceBelowWrapper = wrapperRect.bottom - cursorBottomViewport;
|
|
2330
|
+
const spaceBelow = Math.min(spaceBelowViewport, spaceBelowWrapper);
|
|
2331
|
+
const spaceAbove = cursorTopViewport - VIEWPORT_MARGIN;
|
|
2332
|
+
const needsFlip = popupHeight + POPUP_GAP > spaceBelow;
|
|
2333
|
+
const fitsAbove = popupHeight + POPUP_GAP <= spaceAbove;
|
|
2334
|
+
const flippedAbove = needsFlip && fitsAbove;
|
|
2335
|
+
const maxRightViewport = window.innerWidth - VIEWPORT_MARGIN;
|
|
2336
|
+
const popupRightIfDefault = cursorLeftViewport + popupWidth;
|
|
2337
|
+
const leftOverride = popupRightIfDefault > maxRightViewport ? Math.max(
|
|
2338
|
+
VIEWPORT_MARGIN - wrapperRect.left,
|
|
2339
|
+
maxRightViewport - popupWidth - wrapperRect.left
|
|
2340
|
+
) : null;
|
|
2341
|
+
setPlacement(
|
|
2342
|
+
(prev) => prev.flippedAbove === flippedAbove && prev.leftOverride === leftOverride ? prev : { flippedAbove, leftOverride }
|
|
2343
|
+
);
|
|
2344
|
+
};
|
|
2345
|
+
measure();
|
|
2346
|
+
const ResizeObserverCtor = typeof window !== "undefined" ? window.ResizeObserver : void 0;
|
|
2347
|
+
const resizeObserver = ResizeObserverCtor ? new ResizeObserverCtor(() => measure()) : null;
|
|
2348
|
+
resizeObserver?.observe(popupEl);
|
|
2349
|
+
window.addEventListener("resize", measure);
|
|
2350
|
+
window.addEventListener("scroll", measure, true);
|
|
2351
|
+
return () => {
|
|
2352
|
+
resizeObserver?.disconnect();
|
|
2353
|
+
window.removeEventListener("resize", measure);
|
|
2354
|
+
window.removeEventListener("scroll", measure, true);
|
|
2355
|
+
};
|
|
2356
|
+
}, [popupEl, cursorTopWrapper, cursorBottomWrapper, cursorLeftWrapper]);
|
|
2357
|
+
const style = {
|
|
2358
|
+
position: "absolute",
|
|
2359
|
+
top: placement.flippedAbove ? cursorTopWrapper - POPUP_GAP : position.top,
|
|
2360
|
+
left: placement.leftOverride ?? position.left,
|
|
2361
|
+
transform: placement.flippedAbove ? "translateY(-100%)" : void 0,
|
|
2362
|
+
zIndex: 1001
|
|
2363
|
+
};
|
|
2364
|
+
const className = placement.flippedAbove ? `${pluginPickerClass.popup} ${pluginPickerClass.popupFlipped}` : pluginPickerClass.popup;
|
|
2365
|
+
return {
|
|
2366
|
+
setPopupEl,
|
|
2367
|
+
style,
|
|
2368
|
+
className,
|
|
2369
|
+
flippedAbove: placement.flippedAbove
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
function usePluginPopupPlacement(position, cursorRect) {
|
|
2373
|
+
return usePickerPlacement(position, cursorRect);
|
|
2374
|
+
}
|
|
2270
2375
|
function PluginMenuPrimitive({
|
|
2271
2376
|
items,
|
|
2272
2377
|
search,
|
|
@@ -2278,6 +2383,7 @@ function PluginMenuPrimitive({
|
|
|
2278
2383
|
onSelect,
|
|
2279
2384
|
onDismiss,
|
|
2280
2385
|
position,
|
|
2386
|
+
cursorRect,
|
|
2281
2387
|
subscribeForwardedKey
|
|
2282
2388
|
}) {
|
|
2283
2389
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -2422,16 +2528,13 @@ function PluginMenuPrimitive({
|
|
|
2422
2528
|
updateSelectedIndex
|
|
2423
2529
|
]
|
|
2424
2530
|
);
|
|
2531
|
+
const placement = usePickerPlacement(position, cursorRect);
|
|
2425
2532
|
return /* @__PURE__ */ jsx(
|
|
2426
2533
|
"div",
|
|
2427
2534
|
{
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
top: position.top,
|
|
2432
|
-
left: position.left,
|
|
2433
|
-
zIndex: 1001
|
|
2434
|
-
},
|
|
2535
|
+
ref: placement.setPopupEl,
|
|
2536
|
+
className: placement.className,
|
|
2537
|
+
style: placement.style,
|
|
2435
2538
|
onMouseDown: (event) => event.preventDefault(),
|
|
2436
2539
|
children: /* @__PURE__ */ jsxs(
|
|
2437
2540
|
"div",
|
|
@@ -2445,7 +2548,7 @@ function PluginMenuPrimitive({
|
|
|
2445
2548
|
"aria-activedescendant": results.length > 0 ? activeOptionId : void 0,
|
|
2446
2549
|
children: [
|
|
2447
2550
|
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, "aria-label": placeholder, children: query || placeholder }),
|
|
2448
|
-
results.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, role: "status", children: emptyMessage }) : /* @__PURE__ */ jsx("div", { id: listboxId, role: "listbox", children: renderedResults })
|
|
2551
|
+
results.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, role: "status", children: emptyMessage }) : /* @__PURE__ */ jsx("div", { id: listboxId, role: "listbox", className: pluginPickerClass.list, children: renderedResults })
|
|
2449
2552
|
]
|
|
2450
2553
|
}
|
|
2451
2554
|
)
|
|
@@ -2607,6 +2710,7 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2607
2710
|
onExecute,
|
|
2608
2711
|
onDismiss,
|
|
2609
2712
|
position,
|
|
2713
|
+
cursorRect,
|
|
2610
2714
|
getEditor
|
|
2611
2715
|
}, ref) {
|
|
2612
2716
|
const [mode, setMode] = useState("commands");
|
|
@@ -2770,17 +2874,14 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2770
2874
|
selectedIndex
|
|
2771
2875
|
]
|
|
2772
2876
|
);
|
|
2877
|
+
const placement = usePluginPopupPlacement(position, cursorRect);
|
|
2773
2878
|
if (mode === "ready" && selectedCommand) {
|
|
2774
2879
|
return /* @__PURE__ */ jsx(
|
|
2775
2880
|
"div",
|
|
2776
2881
|
{
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
top: position.top,
|
|
2781
|
-
left: position.left,
|
|
2782
|
-
zIndex: 1001
|
|
2783
|
-
},
|
|
2882
|
+
ref: placement.setPopupEl,
|
|
2883
|
+
className: placement.className,
|
|
2884
|
+
style: placement.style,
|
|
2784
2885
|
children: /* @__PURE__ */ jsx("div", { className: pluginPickerClass.picker, children: /* @__PURE__ */ jsx("div", { className: "inkwell-plugin-slash-commands-execute", children: "Enter to execute \xB7 Esc to cancel" }) })
|
|
2785
2886
|
}
|
|
2786
2887
|
);
|
|
@@ -2788,13 +2889,9 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2788
2889
|
return /* @__PURE__ */ jsx(
|
|
2789
2890
|
"div",
|
|
2790
2891
|
{
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
top: position.top,
|
|
2795
|
-
left: position.left,
|
|
2796
|
-
zIndex: 1001
|
|
2797
|
-
},
|
|
2892
|
+
ref: placement.setPopupEl,
|
|
2893
|
+
className: placement.className,
|
|
2894
|
+
style: placement.style,
|
|
2798
2895
|
children: /* @__PURE__ */ jsxs("div", { className: pluginPickerClass.picker, children: [
|
|
2799
2896
|
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, children: mode === "commands" ? `/${query}` : `${selectedCommand ? `/${selectedCommand.name} ` : ""}${query}` }),
|
|
2800
2897
|
loadingArgs && items.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: "Loading..." }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: emptyMessage }) : /* @__PURE__ */ jsx(
|
|
@@ -2803,6 +2900,7 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2803
2900
|
id: listboxId,
|
|
2804
2901
|
role: "listbox",
|
|
2805
2902
|
"aria-activedescendant": activeOptionId,
|
|
2903
|
+
className: pluginPickerClass.list,
|
|
2806
2904
|
children: items.map((item, index) => {
|
|
2807
2905
|
const active = index === selectedIndex;
|
|
2808
2906
|
return /* @__PURE__ */ jsxs(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@railway/inkwell",
|
|
3
|
-
"version": "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",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"@testing-library/jest-dom": "^6.9.1",
|
|
64
64
|
"@testing-library/react": "^16.3.2",
|
|
65
65
|
"@types/mdast": "^4.0.4",
|
|
66
|
+
"@types/node": "^24.0.0",
|
|
66
67
|
"@types/react": "^19.0.0",
|
|
67
68
|
"@types/react-dom": "^19.0.0",
|
|
68
69
|
"@vitejs/plugin-react": "^6.0.1",
|
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%);
|
|
@@ -72,21 +75,34 @@
|
|
|
72
75
|
.inkwell-editor-wrapper {
|
|
73
76
|
position: relative;
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
|
|
79
|
+
/* Visual-chrome defaults (padding, border, background, type) are wrapped in
|
|
80
|
+
`:where()` so they carry 0,0,0 specificity. Any single-class consumer rule
|
|
81
|
+
on `.inkwell-editor` (Tailwind utilities, `classNames.editor`, etc.) wins
|
|
82
|
+
automatically — no `!important` or selector gymnastics needed.
|
|
83
|
+
|
|
84
|
+
Container size (min-height, max-height, height) is deliberately NOT
|
|
85
|
+
defaulted here. Inkwell sits inside the consumer's layout and the right
|
|
86
|
+
size is a consumer decision: a full-page editor wants `min-height: 60vh`,
|
|
87
|
+
a chat composer wants `min-height: 0`, a panel just wants to fill its
|
|
88
|
+
container. Set it on the editor via `styles.editor`, `classNames.editor`,
|
|
89
|
+
or your own CSS. */
|
|
90
|
+
:where(.inkwell-editor) {
|
|
77
91
|
padding: 1rem 1.25rem;
|
|
78
|
-
outline: none;
|
|
79
92
|
border: 1px solid var(--inkwell-border);
|
|
80
93
|
border-radius: var(--inkwell-radius);
|
|
81
94
|
background: var(--inkwell-bg);
|
|
82
|
-
color: var(--inkwell-text);
|
|
83
95
|
line-height: 1.6;
|
|
84
96
|
font-size: 0.95rem;
|
|
85
97
|
transition: border-color 0.15s ease;
|
|
86
98
|
}
|
|
87
|
-
.inkwell-editor:focus-within {
|
|
99
|
+
:where(.inkwell-editor:focus-within) {
|
|
88
100
|
border-color: var(--inkwell-border-strong);
|
|
89
101
|
}
|
|
102
|
+
.inkwell-editor {
|
|
103
|
+
outline: none;
|
|
104
|
+
color: var(--inkwell-text);
|
|
105
|
+
}
|
|
90
106
|
|
|
91
107
|
.inkwell-editor p {
|
|
92
108
|
margin: 0;
|
|
@@ -162,34 +178,35 @@
|
|
|
162
178
|
height: auto;
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
/* Built-in character count.
|
|
166
|
-
wrapper
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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. */
|
|
170
189
|
.inkwell-editor-character-count {
|
|
171
190
|
position: absolute;
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
top: 0.5rem;
|
|
192
|
+
right: 0.5rem;
|
|
174
193
|
z-index: 10;
|
|
175
|
-
padding: 0.
|
|
194
|
+
padding: 0.1rem 0.4rem;
|
|
176
195
|
font-size: 0.72rem;
|
|
177
196
|
font-variant-numeric: tabular-nums;
|
|
178
197
|
color: var(--inkwell-text-dim);
|
|
198
|
+
background: var(--inkwell-bg);
|
|
199
|
+
border-radius: calc(var(--inkwell-radius) - 2px);
|
|
179
200
|
pointer-events: none;
|
|
180
201
|
user-select: none;
|
|
181
202
|
}
|
|
182
203
|
.inkwell-editor-character-count-over {
|
|
183
|
-
color:
|
|
204
|
+
color: var(--inkwell-danger);
|
|
184
205
|
font-weight: 500;
|
|
185
206
|
}
|
|
186
|
-
.inkwell-editor-wrapper.inkwell-editor-has-character-limit .inkwell-editor {
|
|
187
|
-
padding-right: 5.25rem;
|
|
188
|
-
padding-bottom: 2.25rem;
|
|
189
|
-
}
|
|
190
207
|
.inkwell-editor-wrapper.inkwell-editor-over-limit .inkwell-editor {
|
|
191
|
-
border-color:
|
|
192
|
-
box-shadow: 0 0 0
|
|
208
|
+
border-color: var(--inkwell-danger-soft);
|
|
209
|
+
box-shadow: 0 0 0 3px var(--inkwell-danger-soft);
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
.inkwell-editor-backtick,
|
|
@@ -303,6 +320,28 @@
|
|
|
303
320
|
.inkwell-plugin-picker-search::placeholder {
|
|
304
321
|
color: var(--inkwell-text-dim);
|
|
305
322
|
}
|
|
323
|
+
.inkwell-plugin-picker-list {
|
|
324
|
+
max-height: 240px;
|
|
325
|
+
overflow-y: auto;
|
|
326
|
+
overscroll-behavior: contain;
|
|
327
|
+
scrollbar-width: thin;
|
|
328
|
+
scrollbar-color: var(--inkwell-border-strong) transparent;
|
|
329
|
+
}
|
|
330
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar {
|
|
331
|
+
width: 8px;
|
|
332
|
+
}
|
|
333
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar-track {
|
|
334
|
+
background: transparent;
|
|
335
|
+
}
|
|
336
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar-thumb {
|
|
337
|
+
background-color: var(--inkwell-border-strong);
|
|
338
|
+
border-radius: 4px;
|
|
339
|
+
border: 2px solid transparent;
|
|
340
|
+
background-clip: padding-box;
|
|
341
|
+
}
|
|
342
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar-thumb:hover {
|
|
343
|
+
background-color: var(--inkwell-text-dim);
|
|
344
|
+
}
|
|
306
345
|
.inkwell-plugin-picker-item {
|
|
307
346
|
padding: 7px 10px;
|
|
308
347
|
cursor: pointer;
|
|
@@ -348,9 +387,13 @@
|
|
|
348
387
|
|
|
349
388
|
/* ── Renderer ────────────────────────────────────────────────────── */
|
|
350
389
|
|
|
351
|
-
|
|
390
|
+
/* Layout defaults on the renderer follow the same low-specificity pattern
|
|
391
|
+
as the editor — a single-class consumer rule wins automatically. */
|
|
392
|
+
:where(.inkwell-renderer) {
|
|
352
393
|
line-height: 1.65;
|
|
353
394
|
font-size: 0.95rem;
|
|
395
|
+
}
|
|
396
|
+
.inkwell-renderer {
|
|
354
397
|
color: var(--inkwell-text);
|
|
355
398
|
}
|
|
356
399
|
.inkwell-renderer :first-child {
|