@railway/inkwell 1.0.0 → 1.1.1
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 +111 -31
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +111 -31
- package/package.json +2 -1
- package/src/styles.css +49 -8
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
|
@@ -148,7 +148,7 @@ function BubbleMenuWidget({
|
|
|
148
148
|
left: position.left,
|
|
149
149
|
transform: position.placement === "above" ? "translateX(-50%) translateY(-100%)" : "translateX(-50%)",
|
|
150
150
|
marginTop: position.placement === "above" ? -8 : 8,
|
|
151
|
-
zIndex:
|
|
151
|
+
zIndex: 9999
|
|
152
152
|
},
|
|
153
153
|
onMouseDown: (e) => e.preventDefault(),
|
|
154
154
|
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls("inner"), children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(item.render, { wrapSelection }, item.key)) })
|
|
@@ -1436,7 +1436,8 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1436
1436
|
activePluginRef.current = activePlugin;
|
|
1437
1437
|
const pluginPositionRef = react.useRef({
|
|
1438
1438
|
top: 0,
|
|
1439
|
-
left: 0
|
|
1439
|
+
left: 0,
|
|
1440
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1440
1441
|
});
|
|
1441
1442
|
const forwardedKeyListenersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
1442
1443
|
const wrapperRef = react.useRef(null);
|
|
@@ -1547,10 +1548,14 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1547
1548
|
]
|
|
1548
1549
|
);
|
|
1549
1550
|
const getCursorPosition = react.useCallback(() => {
|
|
1551
|
+
const empty = {
|
|
1552
|
+
top: 0,
|
|
1553
|
+
left: 0,
|
|
1554
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1555
|
+
};
|
|
1550
1556
|
try {
|
|
1551
1557
|
const domSelection = window.getSelection();
|
|
1552
|
-
if (!domSelection || domSelection.rangeCount === 0)
|
|
1553
|
-
return { top: 0, left: 0 };
|
|
1558
|
+
if (!domSelection || domSelection.rangeCount === 0) return empty;
|
|
1554
1559
|
const range = domSelection.getRangeAt(0);
|
|
1555
1560
|
let rect = range.getBoundingClientRect();
|
|
1556
1561
|
if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
|
|
@@ -1558,14 +1563,22 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1558
1563
|
if (node) rect = node.getBoundingClientRect();
|
|
1559
1564
|
}
|
|
1560
1565
|
const wrapperEl = wrapperRef.current;
|
|
1561
|
-
if (!wrapperEl) return
|
|
1566
|
+
if (!wrapperEl) return empty;
|
|
1562
1567
|
const wrapperRect = wrapperEl.getBoundingClientRect();
|
|
1568
|
+
const cursorTop = rect.top - wrapperRect.top;
|
|
1569
|
+
const cursorBottom = rect.bottom - wrapperRect.top;
|
|
1570
|
+
const cursorLeft = rect.left - wrapperRect.left;
|
|
1563
1571
|
return {
|
|
1564
|
-
top:
|
|
1565
|
-
left:
|
|
1572
|
+
top: cursorBottom + 4,
|
|
1573
|
+
left: cursorLeft,
|
|
1574
|
+
cursorRect: {
|
|
1575
|
+
top: cursorTop,
|
|
1576
|
+
bottom: cursorBottom,
|
|
1577
|
+
left: cursorLeft
|
|
1578
|
+
}
|
|
1566
1579
|
};
|
|
1567
1580
|
} catch {
|
|
1568
|
-
return
|
|
1581
|
+
return empty;
|
|
1569
1582
|
}
|
|
1570
1583
|
}, []);
|
|
1571
1584
|
const wrapSelection = react.useCallback(
|
|
@@ -1833,7 +1846,11 @@ var InkwellEditorClient = react.forwardRef(
|
|
|
1833
1846
|
query: activePlugin === plugin ? activePluginQuery : "",
|
|
1834
1847
|
onSelect: handlePluginSelect,
|
|
1835
1848
|
onDismiss: dismissPlugin,
|
|
1836
|
-
position:
|
|
1849
|
+
position: {
|
|
1850
|
+
top: pluginPositionRef.current.top,
|
|
1851
|
+
left: pluginPositionRef.current.left
|
|
1852
|
+
},
|
|
1853
|
+
cursorRect: pluginPositionRef.current.cursorRect,
|
|
1837
1854
|
editorRef: editorElRef,
|
|
1838
1855
|
editor: pluginEditor,
|
|
1839
1856
|
wrapSelection,
|
|
@@ -2273,8 +2290,10 @@ function createCompletionsPlugin({
|
|
|
2273
2290
|
var BASE = "inkwell-plugin-picker";
|
|
2274
2291
|
var pluginPickerClass = {
|
|
2275
2292
|
popup: `${BASE}-popup`,
|
|
2293
|
+
popupFlipped: `${BASE}-popup-flipped`,
|
|
2276
2294
|
picker: `${BASE}`,
|
|
2277
2295
|
search: `${BASE}-search`,
|
|
2296
|
+
list: `${BASE}-list`,
|
|
2278
2297
|
item: `${BASE}-item`,
|
|
2279
2298
|
itemActive: `${BASE}-item-active`,
|
|
2280
2299
|
empty: `${BASE}-empty`,
|
|
@@ -2282,6 +2301,74 @@ var pluginPickerClass = {
|
|
|
2282
2301
|
subtitle: `${BASE}-subtitle`,
|
|
2283
2302
|
preview: `${BASE}-preview`
|
|
2284
2303
|
};
|
|
2304
|
+
var POPUP_GAP = 4;
|
|
2305
|
+
var VIEWPORT_MARGIN = 8;
|
|
2306
|
+
var CURSOR_HEIGHT_FALLBACK = 20;
|
|
2307
|
+
function usePickerPlacement(position, cursorRect) {
|
|
2308
|
+
const [popupEl, setPopupEl] = react.useState(null);
|
|
2309
|
+
const [placement, setPlacement] = react.useState({ flippedAbove: false, leftOverride: null });
|
|
2310
|
+
const cursorTopWrapper = cursorRect?.top ?? Math.max(0, position.top - POPUP_GAP - CURSOR_HEIGHT_FALLBACK);
|
|
2311
|
+
const cursorBottomWrapper = cursorRect?.bottom ?? Math.max(0, position.top - POPUP_GAP);
|
|
2312
|
+
const cursorLeftWrapper = cursorRect?.left ?? position.left;
|
|
2313
|
+
react.useLayoutEffect(() => {
|
|
2314
|
+
if (!popupEl) return;
|
|
2315
|
+
const measure = () => {
|
|
2316
|
+
const offsetParent = popupEl.offsetParent;
|
|
2317
|
+
if (!(offsetParent instanceof HTMLElement)) return;
|
|
2318
|
+
const wrapperRect = offsetParent.getBoundingClientRect();
|
|
2319
|
+
const popupRect = popupEl.getBoundingClientRect();
|
|
2320
|
+
const popupHeight = popupRect.height;
|
|
2321
|
+
const popupWidth = popupRect.width;
|
|
2322
|
+
const cursorTopViewport = wrapperRect.top + cursorTopWrapper;
|
|
2323
|
+
const cursorBottomViewport = wrapperRect.top + cursorBottomWrapper;
|
|
2324
|
+
const cursorLeftViewport = wrapperRect.left + cursorLeftWrapper;
|
|
2325
|
+
const spaceBelowViewport = window.innerHeight - cursorBottomViewport - VIEWPORT_MARGIN;
|
|
2326
|
+
const spaceBelowWrapper = wrapperRect.bottom - cursorBottomViewport;
|
|
2327
|
+
const spaceBelow = Math.min(spaceBelowViewport, spaceBelowWrapper);
|
|
2328
|
+
const spaceAbove = cursorTopViewport - VIEWPORT_MARGIN;
|
|
2329
|
+
const needsFlip = popupHeight + POPUP_GAP > spaceBelow;
|
|
2330
|
+
const fitsAbove = popupHeight + POPUP_GAP <= spaceAbove;
|
|
2331
|
+
const flippedAbove = needsFlip && fitsAbove;
|
|
2332
|
+
const maxRightViewport = window.innerWidth - VIEWPORT_MARGIN;
|
|
2333
|
+
const popupRightIfDefault = cursorLeftViewport + popupWidth;
|
|
2334
|
+
const leftOverride = popupRightIfDefault > maxRightViewport ? Math.max(
|
|
2335
|
+
VIEWPORT_MARGIN - wrapperRect.left,
|
|
2336
|
+
maxRightViewport - popupWidth - wrapperRect.left
|
|
2337
|
+
) : null;
|
|
2338
|
+
setPlacement(
|
|
2339
|
+
(prev) => prev.flippedAbove === flippedAbove && prev.leftOverride === leftOverride ? prev : { flippedAbove, leftOverride }
|
|
2340
|
+
);
|
|
2341
|
+
};
|
|
2342
|
+
measure();
|
|
2343
|
+
const ResizeObserverCtor = typeof window !== "undefined" ? window.ResizeObserver : void 0;
|
|
2344
|
+
const resizeObserver = ResizeObserverCtor ? new ResizeObserverCtor(() => measure()) : null;
|
|
2345
|
+
resizeObserver?.observe(popupEl);
|
|
2346
|
+
window.addEventListener("resize", measure);
|
|
2347
|
+
window.addEventListener("scroll", measure, true);
|
|
2348
|
+
return () => {
|
|
2349
|
+
resizeObserver?.disconnect();
|
|
2350
|
+
window.removeEventListener("resize", measure);
|
|
2351
|
+
window.removeEventListener("scroll", measure, true);
|
|
2352
|
+
};
|
|
2353
|
+
}, [popupEl, cursorTopWrapper, cursorBottomWrapper, cursorLeftWrapper]);
|
|
2354
|
+
const style = {
|
|
2355
|
+
position: "absolute",
|
|
2356
|
+
top: placement.flippedAbove ? cursorTopWrapper - POPUP_GAP : position.top,
|
|
2357
|
+
left: placement.leftOverride ?? position.left,
|
|
2358
|
+
transform: placement.flippedAbove ? "translateY(-100%)" : void 0,
|
|
2359
|
+
zIndex: 1001
|
|
2360
|
+
};
|
|
2361
|
+
const className = placement.flippedAbove ? `${pluginPickerClass.popup} ${pluginPickerClass.popupFlipped}` : pluginPickerClass.popup;
|
|
2362
|
+
return {
|
|
2363
|
+
setPopupEl,
|
|
2364
|
+
style,
|
|
2365
|
+
className,
|
|
2366
|
+
flippedAbove: placement.flippedAbove
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
function usePluginPopupPlacement(position, cursorRect) {
|
|
2370
|
+
return usePickerPlacement(position, cursorRect);
|
|
2371
|
+
}
|
|
2285
2372
|
function PluginMenuPrimitive({
|
|
2286
2373
|
items,
|
|
2287
2374
|
search,
|
|
@@ -2293,6 +2380,7 @@ function PluginMenuPrimitive({
|
|
|
2293
2380
|
onSelect,
|
|
2294
2381
|
onDismiss,
|
|
2295
2382
|
position,
|
|
2383
|
+
cursorRect,
|
|
2296
2384
|
subscribeForwardedKey
|
|
2297
2385
|
}) {
|
|
2298
2386
|
const [selectedIndex, setSelectedIndex] = react.useState(0);
|
|
@@ -2437,16 +2525,13 @@ function PluginMenuPrimitive({
|
|
|
2437
2525
|
updateSelectedIndex
|
|
2438
2526
|
]
|
|
2439
2527
|
);
|
|
2528
|
+
const placement = usePickerPlacement(position, cursorRect);
|
|
2440
2529
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2441
2530
|
"div",
|
|
2442
2531
|
{
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
top: position.top,
|
|
2447
|
-
left: position.left,
|
|
2448
|
-
zIndex: 1001
|
|
2449
|
-
},
|
|
2532
|
+
ref: placement.setPopupEl,
|
|
2533
|
+
className: placement.className,
|
|
2534
|
+
style: placement.style,
|
|
2450
2535
|
onMouseDown: (event) => event.preventDefault(),
|
|
2451
2536
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2452
2537
|
"div",
|
|
@@ -2460,7 +2545,7 @@ function PluginMenuPrimitive({
|
|
|
2460
2545
|
"aria-activedescendant": results.length > 0 ? activeOptionId : void 0,
|
|
2461
2546
|
children: [
|
|
2462
2547
|
/* @__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 })
|
|
2548
|
+
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
2549
|
]
|
|
2465
2550
|
}
|
|
2466
2551
|
)
|
|
@@ -2622,6 +2707,7 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2622
2707
|
onExecute,
|
|
2623
2708
|
onDismiss,
|
|
2624
2709
|
position,
|
|
2710
|
+
cursorRect,
|
|
2625
2711
|
getEditor
|
|
2626
2712
|
}, ref) {
|
|
2627
2713
|
const [mode, setMode] = react.useState("commands");
|
|
@@ -2785,17 +2871,14 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2785
2871
|
selectedIndex
|
|
2786
2872
|
]
|
|
2787
2873
|
);
|
|
2874
|
+
const placement = usePluginPopupPlacement(position, cursorRect);
|
|
2788
2875
|
if (mode === "ready" && selectedCommand) {
|
|
2789
2876
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2790
2877
|
"div",
|
|
2791
2878
|
{
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
top: position.top,
|
|
2796
|
-
left: position.left,
|
|
2797
|
-
zIndex: 1001
|
|
2798
|
-
},
|
|
2879
|
+
ref: placement.setPopupEl,
|
|
2880
|
+
className: placement.className,
|
|
2881
|
+
style: placement.style,
|
|
2799
2882
|
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
2883
|
}
|
|
2801
2884
|
);
|
|
@@ -2803,13 +2886,9 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2803
2886
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2804
2887
|
"div",
|
|
2805
2888
|
{
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
top: position.top,
|
|
2810
|
-
left: position.left,
|
|
2811
|
-
zIndex: 1001
|
|
2812
|
-
},
|
|
2889
|
+
ref: placement.setPopupEl,
|
|
2890
|
+
className: placement.className,
|
|
2891
|
+
style: placement.style,
|
|
2813
2892
|
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: pluginPickerClass.picker, children: [
|
|
2814
2893
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: pluginPickerClass.search, children: mode === "commands" ? `/${query}` : `${selectedCommand ? `/${selectedCommand.name} ` : ""}${query}` }),
|
|
2815
2894
|
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 +2897,7 @@ var SlashCommandMenuInner = react.forwardRef(function SlashCommandMenuInner2({
|
|
|
2818
2897
|
id: listboxId,
|
|
2819
2898
|
role: "listbox",
|
|
2820
2899
|
"aria-activedescendant": activeOptionId,
|
|
2900
|
+
className: pluginPickerClass.list,
|
|
2821
2901
|
children: items.map((item, index) => {
|
|
2822
2902
|
const active = index === selectedIndex;
|
|
2823
2903
|
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. */
|
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. */
|
package/dist/index.js
CHANGED
|
@@ -133,7 +133,7 @@ function BubbleMenuWidget({
|
|
|
133
133
|
left: position.left,
|
|
134
134
|
transform: position.placement === "above" ? "translateX(-50%) translateY(-100%)" : "translateX(-50%)",
|
|
135
135
|
marginTop: position.placement === "above" ? -8 : 8,
|
|
136
|
-
zIndex:
|
|
136
|
+
zIndex: 9999
|
|
137
137
|
},
|
|
138
138
|
onMouseDown: (e) => e.preventDefault(),
|
|
139
139
|
children: /* @__PURE__ */ jsx("div", { className: cls("inner"), children: items.map((item) => /* @__PURE__ */ jsx(item.render, { wrapSelection }, item.key)) })
|
|
@@ -1421,7 +1421,8 @@ var InkwellEditorClient = forwardRef(
|
|
|
1421
1421
|
activePluginRef.current = activePlugin;
|
|
1422
1422
|
const pluginPositionRef = useRef({
|
|
1423
1423
|
top: 0,
|
|
1424
|
-
left: 0
|
|
1424
|
+
left: 0,
|
|
1425
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1425
1426
|
});
|
|
1426
1427
|
const forwardedKeyListenersRef = useRef(/* @__PURE__ */ new Map());
|
|
1427
1428
|
const wrapperRef = useRef(null);
|
|
@@ -1532,10 +1533,14 @@ var InkwellEditorClient = forwardRef(
|
|
|
1532
1533
|
]
|
|
1533
1534
|
);
|
|
1534
1535
|
const getCursorPosition = useCallback(() => {
|
|
1536
|
+
const empty = {
|
|
1537
|
+
top: 0,
|
|
1538
|
+
left: 0,
|
|
1539
|
+
cursorRect: { top: 0, bottom: 0, left: 0 }
|
|
1540
|
+
};
|
|
1535
1541
|
try {
|
|
1536
1542
|
const domSelection = window.getSelection();
|
|
1537
|
-
if (!domSelection || domSelection.rangeCount === 0)
|
|
1538
|
-
return { top: 0, left: 0 };
|
|
1543
|
+
if (!domSelection || domSelection.rangeCount === 0) return empty;
|
|
1539
1544
|
const range = domSelection.getRangeAt(0);
|
|
1540
1545
|
let rect = range.getBoundingClientRect();
|
|
1541
1546
|
if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
|
|
@@ -1543,14 +1548,22 @@ var InkwellEditorClient = forwardRef(
|
|
|
1543
1548
|
if (node) rect = node.getBoundingClientRect();
|
|
1544
1549
|
}
|
|
1545
1550
|
const wrapperEl = wrapperRef.current;
|
|
1546
|
-
if (!wrapperEl) return
|
|
1551
|
+
if (!wrapperEl) return empty;
|
|
1547
1552
|
const wrapperRect = wrapperEl.getBoundingClientRect();
|
|
1553
|
+
const cursorTop = rect.top - wrapperRect.top;
|
|
1554
|
+
const cursorBottom = rect.bottom - wrapperRect.top;
|
|
1555
|
+
const cursorLeft = rect.left - wrapperRect.left;
|
|
1548
1556
|
return {
|
|
1549
|
-
top:
|
|
1550
|
-
left:
|
|
1557
|
+
top: cursorBottom + 4,
|
|
1558
|
+
left: cursorLeft,
|
|
1559
|
+
cursorRect: {
|
|
1560
|
+
top: cursorTop,
|
|
1561
|
+
bottom: cursorBottom,
|
|
1562
|
+
left: cursorLeft
|
|
1563
|
+
}
|
|
1551
1564
|
};
|
|
1552
1565
|
} catch {
|
|
1553
|
-
return
|
|
1566
|
+
return empty;
|
|
1554
1567
|
}
|
|
1555
1568
|
}, []);
|
|
1556
1569
|
const wrapSelection = useCallback(
|
|
@@ -1818,7 +1831,11 @@ var InkwellEditorClient = forwardRef(
|
|
|
1818
1831
|
query: activePlugin === plugin ? activePluginQuery : "",
|
|
1819
1832
|
onSelect: handlePluginSelect,
|
|
1820
1833
|
onDismiss: dismissPlugin,
|
|
1821
|
-
position:
|
|
1834
|
+
position: {
|
|
1835
|
+
top: pluginPositionRef.current.top,
|
|
1836
|
+
left: pluginPositionRef.current.left
|
|
1837
|
+
},
|
|
1838
|
+
cursorRect: pluginPositionRef.current.cursorRect,
|
|
1822
1839
|
editorRef: editorElRef,
|
|
1823
1840
|
editor: pluginEditor,
|
|
1824
1841
|
wrapSelection,
|
|
@@ -2258,8 +2275,10 @@ function createCompletionsPlugin({
|
|
|
2258
2275
|
var BASE = "inkwell-plugin-picker";
|
|
2259
2276
|
var pluginPickerClass = {
|
|
2260
2277
|
popup: `${BASE}-popup`,
|
|
2278
|
+
popupFlipped: `${BASE}-popup-flipped`,
|
|
2261
2279
|
picker: `${BASE}`,
|
|
2262
2280
|
search: `${BASE}-search`,
|
|
2281
|
+
list: `${BASE}-list`,
|
|
2263
2282
|
item: `${BASE}-item`,
|
|
2264
2283
|
itemActive: `${BASE}-item-active`,
|
|
2265
2284
|
empty: `${BASE}-empty`,
|
|
@@ -2267,6 +2286,74 @@ var pluginPickerClass = {
|
|
|
2267
2286
|
subtitle: `${BASE}-subtitle`,
|
|
2268
2287
|
preview: `${BASE}-preview`
|
|
2269
2288
|
};
|
|
2289
|
+
var POPUP_GAP = 4;
|
|
2290
|
+
var VIEWPORT_MARGIN = 8;
|
|
2291
|
+
var CURSOR_HEIGHT_FALLBACK = 20;
|
|
2292
|
+
function usePickerPlacement(position, cursorRect) {
|
|
2293
|
+
const [popupEl, setPopupEl] = useState(null);
|
|
2294
|
+
const [placement, setPlacement] = useState({ flippedAbove: false, leftOverride: null });
|
|
2295
|
+
const cursorTopWrapper = cursorRect?.top ?? Math.max(0, position.top - POPUP_GAP - CURSOR_HEIGHT_FALLBACK);
|
|
2296
|
+
const cursorBottomWrapper = cursorRect?.bottom ?? Math.max(0, position.top - POPUP_GAP);
|
|
2297
|
+
const cursorLeftWrapper = cursorRect?.left ?? position.left;
|
|
2298
|
+
useLayoutEffect(() => {
|
|
2299
|
+
if (!popupEl) return;
|
|
2300
|
+
const measure = () => {
|
|
2301
|
+
const offsetParent = popupEl.offsetParent;
|
|
2302
|
+
if (!(offsetParent instanceof HTMLElement)) return;
|
|
2303
|
+
const wrapperRect = offsetParent.getBoundingClientRect();
|
|
2304
|
+
const popupRect = popupEl.getBoundingClientRect();
|
|
2305
|
+
const popupHeight = popupRect.height;
|
|
2306
|
+
const popupWidth = popupRect.width;
|
|
2307
|
+
const cursorTopViewport = wrapperRect.top + cursorTopWrapper;
|
|
2308
|
+
const cursorBottomViewport = wrapperRect.top + cursorBottomWrapper;
|
|
2309
|
+
const cursorLeftViewport = wrapperRect.left + cursorLeftWrapper;
|
|
2310
|
+
const spaceBelowViewport = window.innerHeight - cursorBottomViewport - VIEWPORT_MARGIN;
|
|
2311
|
+
const spaceBelowWrapper = wrapperRect.bottom - cursorBottomViewport;
|
|
2312
|
+
const spaceBelow = Math.min(spaceBelowViewport, spaceBelowWrapper);
|
|
2313
|
+
const spaceAbove = cursorTopViewport - VIEWPORT_MARGIN;
|
|
2314
|
+
const needsFlip = popupHeight + POPUP_GAP > spaceBelow;
|
|
2315
|
+
const fitsAbove = popupHeight + POPUP_GAP <= spaceAbove;
|
|
2316
|
+
const flippedAbove = needsFlip && fitsAbove;
|
|
2317
|
+
const maxRightViewport = window.innerWidth - VIEWPORT_MARGIN;
|
|
2318
|
+
const popupRightIfDefault = cursorLeftViewport + popupWidth;
|
|
2319
|
+
const leftOverride = popupRightIfDefault > maxRightViewport ? Math.max(
|
|
2320
|
+
VIEWPORT_MARGIN - wrapperRect.left,
|
|
2321
|
+
maxRightViewport - popupWidth - wrapperRect.left
|
|
2322
|
+
) : null;
|
|
2323
|
+
setPlacement(
|
|
2324
|
+
(prev) => prev.flippedAbove === flippedAbove && prev.leftOverride === leftOverride ? prev : { flippedAbove, leftOverride }
|
|
2325
|
+
);
|
|
2326
|
+
};
|
|
2327
|
+
measure();
|
|
2328
|
+
const ResizeObserverCtor = typeof window !== "undefined" ? window.ResizeObserver : void 0;
|
|
2329
|
+
const resizeObserver = ResizeObserverCtor ? new ResizeObserverCtor(() => measure()) : null;
|
|
2330
|
+
resizeObserver?.observe(popupEl);
|
|
2331
|
+
window.addEventListener("resize", measure);
|
|
2332
|
+
window.addEventListener("scroll", measure, true);
|
|
2333
|
+
return () => {
|
|
2334
|
+
resizeObserver?.disconnect();
|
|
2335
|
+
window.removeEventListener("resize", measure);
|
|
2336
|
+
window.removeEventListener("scroll", measure, true);
|
|
2337
|
+
};
|
|
2338
|
+
}, [popupEl, cursorTopWrapper, cursorBottomWrapper, cursorLeftWrapper]);
|
|
2339
|
+
const style = {
|
|
2340
|
+
position: "absolute",
|
|
2341
|
+
top: placement.flippedAbove ? cursorTopWrapper - POPUP_GAP : position.top,
|
|
2342
|
+
left: placement.leftOverride ?? position.left,
|
|
2343
|
+
transform: placement.flippedAbove ? "translateY(-100%)" : void 0,
|
|
2344
|
+
zIndex: 1001
|
|
2345
|
+
};
|
|
2346
|
+
const className = placement.flippedAbove ? `${pluginPickerClass.popup} ${pluginPickerClass.popupFlipped}` : pluginPickerClass.popup;
|
|
2347
|
+
return {
|
|
2348
|
+
setPopupEl,
|
|
2349
|
+
style,
|
|
2350
|
+
className,
|
|
2351
|
+
flippedAbove: placement.flippedAbove
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
function usePluginPopupPlacement(position, cursorRect) {
|
|
2355
|
+
return usePickerPlacement(position, cursorRect);
|
|
2356
|
+
}
|
|
2270
2357
|
function PluginMenuPrimitive({
|
|
2271
2358
|
items,
|
|
2272
2359
|
search,
|
|
@@ -2278,6 +2365,7 @@ function PluginMenuPrimitive({
|
|
|
2278
2365
|
onSelect,
|
|
2279
2366
|
onDismiss,
|
|
2280
2367
|
position,
|
|
2368
|
+
cursorRect,
|
|
2281
2369
|
subscribeForwardedKey
|
|
2282
2370
|
}) {
|
|
2283
2371
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -2422,16 +2510,13 @@ function PluginMenuPrimitive({
|
|
|
2422
2510
|
updateSelectedIndex
|
|
2423
2511
|
]
|
|
2424
2512
|
);
|
|
2513
|
+
const placement = usePickerPlacement(position, cursorRect);
|
|
2425
2514
|
return /* @__PURE__ */ jsx(
|
|
2426
2515
|
"div",
|
|
2427
2516
|
{
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
top: position.top,
|
|
2432
|
-
left: position.left,
|
|
2433
|
-
zIndex: 1001
|
|
2434
|
-
},
|
|
2517
|
+
ref: placement.setPopupEl,
|
|
2518
|
+
className: placement.className,
|
|
2519
|
+
style: placement.style,
|
|
2435
2520
|
onMouseDown: (event) => event.preventDefault(),
|
|
2436
2521
|
children: /* @__PURE__ */ jsxs(
|
|
2437
2522
|
"div",
|
|
@@ -2445,7 +2530,7 @@ function PluginMenuPrimitive({
|
|
|
2445
2530
|
"aria-activedescendant": results.length > 0 ? activeOptionId : void 0,
|
|
2446
2531
|
children: [
|
|
2447
2532
|
/* @__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 })
|
|
2533
|
+
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
2534
|
]
|
|
2450
2535
|
}
|
|
2451
2536
|
)
|
|
@@ -2607,6 +2692,7 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2607
2692
|
onExecute,
|
|
2608
2693
|
onDismiss,
|
|
2609
2694
|
position,
|
|
2695
|
+
cursorRect,
|
|
2610
2696
|
getEditor
|
|
2611
2697
|
}, ref) {
|
|
2612
2698
|
const [mode, setMode] = useState("commands");
|
|
@@ -2770,17 +2856,14 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2770
2856
|
selectedIndex
|
|
2771
2857
|
]
|
|
2772
2858
|
);
|
|
2859
|
+
const placement = usePluginPopupPlacement(position, cursorRect);
|
|
2773
2860
|
if (mode === "ready" && selectedCommand) {
|
|
2774
2861
|
return /* @__PURE__ */ jsx(
|
|
2775
2862
|
"div",
|
|
2776
2863
|
{
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
top: position.top,
|
|
2781
|
-
left: position.left,
|
|
2782
|
-
zIndex: 1001
|
|
2783
|
-
},
|
|
2864
|
+
ref: placement.setPopupEl,
|
|
2865
|
+
className: placement.className,
|
|
2866
|
+
style: placement.style,
|
|
2784
2867
|
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
2868
|
}
|
|
2786
2869
|
);
|
|
@@ -2788,13 +2871,9 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2788
2871
|
return /* @__PURE__ */ jsx(
|
|
2789
2872
|
"div",
|
|
2790
2873
|
{
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
top: position.top,
|
|
2795
|
-
left: position.left,
|
|
2796
|
-
zIndex: 1001
|
|
2797
|
-
},
|
|
2874
|
+
ref: placement.setPopupEl,
|
|
2875
|
+
className: placement.className,
|
|
2876
|
+
style: placement.style,
|
|
2798
2877
|
children: /* @__PURE__ */ jsxs("div", { className: pluginPickerClass.picker, children: [
|
|
2799
2878
|
/* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, children: mode === "commands" ? `/${query}` : `${selectedCommand ? `/${selectedCommand.name} ` : ""}${query}` }),
|
|
2800
2879
|
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 +2882,7 @@ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
|
|
|
2803
2882
|
id: listboxId,
|
|
2804
2883
|
role: "listbox",
|
|
2805
2884
|
"aria-activedescendant": activeOptionId,
|
|
2885
|
+
className: pluginPickerClass.list,
|
|
2806
2886
|
children: items.map((item, index) => {
|
|
2807
2887
|
const active = index === selectedIndex;
|
|
2808
2888
|
return /* @__PURE__ */ jsxs(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@railway/inkwell",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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
|
@@ -72,21 +72,34 @@
|
|
|
72
72
|
.inkwell-editor-wrapper {
|
|
73
73
|
position: relative;
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
|
|
76
|
+
/* Visual-chrome defaults (padding, border, background, type) are wrapped in
|
|
77
|
+
`:where()` so they carry 0,0,0 specificity. Any single-class consumer rule
|
|
78
|
+
on `.inkwell-editor` (Tailwind utilities, `classNames.editor`, etc.) wins
|
|
79
|
+
automatically — no `!important` or selector gymnastics needed.
|
|
80
|
+
|
|
81
|
+
Container size (min-height, max-height, height) is deliberately NOT
|
|
82
|
+
defaulted here. Inkwell sits inside the consumer's layout and the right
|
|
83
|
+
size is a consumer decision: a full-page editor wants `min-height: 60vh`,
|
|
84
|
+
a chat composer wants `min-height: 0`, a panel just wants to fill its
|
|
85
|
+
container. Set it on the editor via `styles.editor`, `classNames.editor`,
|
|
86
|
+
or your own CSS. */
|
|
87
|
+
:where(.inkwell-editor) {
|
|
77
88
|
padding: 1rem 1.25rem;
|
|
78
|
-
outline: none;
|
|
79
89
|
border: 1px solid var(--inkwell-border);
|
|
80
90
|
border-radius: var(--inkwell-radius);
|
|
81
91
|
background: var(--inkwell-bg);
|
|
82
|
-
color: var(--inkwell-text);
|
|
83
92
|
line-height: 1.6;
|
|
84
93
|
font-size: 0.95rem;
|
|
85
94
|
transition: border-color 0.15s ease;
|
|
86
95
|
}
|
|
87
|
-
.inkwell-editor:focus-within {
|
|
96
|
+
:where(.inkwell-editor:focus-within) {
|
|
88
97
|
border-color: var(--inkwell-border-strong);
|
|
89
98
|
}
|
|
99
|
+
.inkwell-editor {
|
|
100
|
+
outline: none;
|
|
101
|
+
color: var(--inkwell-text);
|
|
102
|
+
}
|
|
90
103
|
|
|
91
104
|
.inkwell-editor p {
|
|
92
105
|
margin: 0;
|
|
@@ -183,7 +196,9 @@
|
|
|
183
196
|
color: hsl(0, 75%, 55%);
|
|
184
197
|
font-weight: 500;
|
|
185
198
|
}
|
|
186
|
-
|
|
199
|
+
:where(
|
|
200
|
+
.inkwell-editor-wrapper.inkwell-editor-has-character-limit .inkwell-editor
|
|
201
|
+
) {
|
|
187
202
|
padding-right: 5.25rem;
|
|
188
203
|
padding-bottom: 2.25rem;
|
|
189
204
|
}
|
|
@@ -226,7 +241,7 @@
|
|
|
226
241
|
|
|
227
242
|
.inkwell-plugin-bubble-menu-container {
|
|
228
243
|
position: absolute;
|
|
229
|
-
z-index:
|
|
244
|
+
z-index: 9999;
|
|
230
245
|
}
|
|
231
246
|
.inkwell-plugin-bubble-menu-inner {
|
|
232
247
|
display: flex;
|
|
@@ -303,6 +318,28 @@
|
|
|
303
318
|
.inkwell-plugin-picker-search::placeholder {
|
|
304
319
|
color: var(--inkwell-text-dim);
|
|
305
320
|
}
|
|
321
|
+
.inkwell-plugin-picker-list {
|
|
322
|
+
max-height: 240px;
|
|
323
|
+
overflow-y: auto;
|
|
324
|
+
overscroll-behavior: contain;
|
|
325
|
+
scrollbar-width: thin;
|
|
326
|
+
scrollbar-color: var(--inkwell-border-strong) transparent;
|
|
327
|
+
}
|
|
328
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar {
|
|
329
|
+
width: 8px;
|
|
330
|
+
}
|
|
331
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar-track {
|
|
332
|
+
background: transparent;
|
|
333
|
+
}
|
|
334
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar-thumb {
|
|
335
|
+
background-color: var(--inkwell-border-strong);
|
|
336
|
+
border-radius: 4px;
|
|
337
|
+
border: 2px solid transparent;
|
|
338
|
+
background-clip: padding-box;
|
|
339
|
+
}
|
|
340
|
+
.inkwell-plugin-picker-list::-webkit-scrollbar-thumb:hover {
|
|
341
|
+
background-color: var(--inkwell-text-dim);
|
|
342
|
+
}
|
|
306
343
|
.inkwell-plugin-picker-item {
|
|
307
344
|
padding: 7px 10px;
|
|
308
345
|
cursor: pointer;
|
|
@@ -348,9 +385,13 @@
|
|
|
348
385
|
|
|
349
386
|
/* ── Renderer ────────────────────────────────────────────────────── */
|
|
350
387
|
|
|
351
|
-
|
|
388
|
+
/* Layout defaults on the renderer follow the same low-specificity pattern
|
|
389
|
+
as the editor — a single-class consumer rule wins automatically. */
|
|
390
|
+
:where(.inkwell-renderer) {
|
|
352
391
|
line-height: 1.65;
|
|
353
392
|
font-size: 0.95rem;
|
|
393
|
+
}
|
|
394
|
+
.inkwell-renderer {
|
|
354
395
|
color: var(--inkwell-text);
|
|
355
396
|
}
|
|
356
397
|
.inkwell-renderer :first-child {
|