@marimo-team/islands 0.23.16-dev76 → 0.23.16-dev8
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/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
- package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
- package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
- package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
- package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
- package/dist/{chat-ui-BdSDiVjk.js → chat-ui-JeekqoMH.js} +16 -16
- package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
- package/dist/{code-visibility-BQmXTtK5.js → code-visibility-z1wi6InA.js} +734 -711
- package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
- package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
- package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
- package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
- package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
- package/dist/{formats-BFhkZJwd.js → formats-CO8cn5_K.js} +3 -3
- package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
- package/dist/{html-to-image-DDENewl2.js → html-to-image-Bif4utQZ.js} +15 -15
- package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
- package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
- package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
- package/dist/main.js +1000 -1010
- package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
- package/dist/{process-output-2QPcgBQg.js → process-output-TAzy-GUk.js} +3 -3
- package/dist/{reveal-component-B-AxUPCM.js → reveal-component-kTeHzMBu.js} +692 -618
- package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
- package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
- package/dist/{toDate-B3IRfHhw.js → toDate-CmR_xR3P.js} +3 -3
- package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
- package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
- package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
- package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
- package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
- package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
- package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
- package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
- package/dist/{vega-component-CrFoZT1S.js → vega-component-DphdxaJ_.js} +10 -10
- package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
- package/package.json +1 -1
- package/src/components/data-table/column-formatting/feature.ts +2 -1
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/editor/output/CalloutOutput.tsx +17 -3
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +3 -2
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/i18n/__tests__/locale-provider.test.tsx +54 -118
- package/src/core/i18n/__tests__/locale.test.ts +113 -0
- package/src/core/i18n/locale-provider.tsx +1 -19
- package/src/core/i18n/locale.ts +71 -0
- package/src/core/network/types.ts +3 -1
- package/src/css/admonition.css +52 -71
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/plugins/layout/CalloutPlugin.tsx +8 -1
- package/src/stories/callout.stories.tsx +12 -0
- package/src/components/editor/output/CalloutOutput.styles.ts +0 -22
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { memo } from "react";
|
|
3
3
|
import type { Intent } from "@/plugins/impl/common/intent";
|
|
4
|
-
import {
|
|
4
|
+
import { cn } from "@/utils/cn";
|
|
5
5
|
import { HtmlOutput } from "./HtmlOutput";
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
html: string;
|
|
9
9
|
kind: Intent;
|
|
10
|
+
title?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// Callouts share the flat admonition style of markdown admonitions
|
|
14
|
+
// (css/admonition.css); each intent maps onto an admonition kind.
|
|
15
|
+
const KIND_CLASS: Record<Intent, string> = {
|
|
16
|
+
neutral: "neutral",
|
|
17
|
+
info: "info",
|
|
18
|
+
warn: "warning",
|
|
19
|
+
success: "success",
|
|
20
|
+
danger: "danger",
|
|
21
|
+
// 'alert' is deprecated; render as danger
|
|
22
|
+
alert: "danger",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const CalloutOutput: React.FC<Props> = memo(({ html, kind, title }) => {
|
|
13
26
|
return (
|
|
14
|
-
<div className={
|
|
27
|
+
<div className={cn("admonition", KIND_CLASS[kind])}>
|
|
28
|
+
{title && <span className="admonition-title">{title}</span>}
|
|
15
29
|
<HtmlOutput html={html} alwaysSanitizeHtml={true} />
|
|
16
30
|
</div>
|
|
17
31
|
);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, render, screen } from "@testing-library/react";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
shouldShowScrollHint,
|
|
7
|
+
SlideScrollContainer,
|
|
8
|
+
} from "../slide-scroll-hint";
|
|
9
|
+
|
|
10
|
+
describe("shouldShowScrollHint", () => {
|
|
11
|
+
it("shows when content overflows and scroll is at the top", () => {
|
|
12
|
+
expect(
|
|
13
|
+
shouldShowScrollHint({
|
|
14
|
+
scrollHeight: 800,
|
|
15
|
+
clientHeight: 400,
|
|
16
|
+
scrollTop: 0,
|
|
17
|
+
}),
|
|
18
|
+
).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("hides when content fits", () => {
|
|
22
|
+
expect(
|
|
23
|
+
shouldShowScrollHint({
|
|
24
|
+
scrollHeight: 300,
|
|
25
|
+
clientHeight: 400,
|
|
26
|
+
scrollTop: 0,
|
|
27
|
+
}),
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("hides after the user scrolls down", () => {
|
|
32
|
+
expect(
|
|
33
|
+
shouldShowScrollHint({
|
|
34
|
+
scrollHeight: 800,
|
|
35
|
+
clientHeight: 400,
|
|
36
|
+
scrollTop: 40,
|
|
37
|
+
}),
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("ignores sub-pixel overflow", () => {
|
|
42
|
+
expect(
|
|
43
|
+
shouldShowScrollHint({
|
|
44
|
+
scrollHeight: 401,
|
|
45
|
+
clientHeight: 400,
|
|
46
|
+
scrollTop: 0,
|
|
47
|
+
}),
|
|
48
|
+
).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("still shows within the top threshold", () => {
|
|
52
|
+
expect(
|
|
53
|
+
shouldShowScrollHint({
|
|
54
|
+
scrollHeight: 800,
|
|
55
|
+
clientHeight: 400,
|
|
56
|
+
scrollTop: 8,
|
|
57
|
+
}),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("SlideScrollContainer", () => {
|
|
63
|
+
const resizeObservers: Array<{
|
|
64
|
+
callback: ResizeObserverCallback;
|
|
65
|
+
observe: ReturnType<typeof vi.fn>;
|
|
66
|
+
disconnect: ReturnType<typeof vi.fn>;
|
|
67
|
+
}> = [];
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
resizeObservers.length = 0;
|
|
71
|
+
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
72
|
+
cb(0);
|
|
73
|
+
return 0;
|
|
74
|
+
});
|
|
75
|
+
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
76
|
+
vi.stubGlobal(
|
|
77
|
+
"ResizeObserver",
|
|
78
|
+
class MockResizeObserver {
|
|
79
|
+
callback: ResizeObserverCallback;
|
|
80
|
+
observe = vi.fn();
|
|
81
|
+
unobserve = vi.fn();
|
|
82
|
+
disconnect = vi.fn();
|
|
83
|
+
|
|
84
|
+
constructor(callback: ResizeObserverCallback) {
|
|
85
|
+
this.callback = callback;
|
|
86
|
+
resizeObservers.push(this);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
vi.stubGlobal(
|
|
91
|
+
"MutationObserver",
|
|
92
|
+
class MockMutationObserver {
|
|
93
|
+
observe = vi.fn();
|
|
94
|
+
disconnect = vi.fn();
|
|
95
|
+
takeRecords = vi.fn(() => []);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vi.unstubAllGlobals();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function stubOverflow(
|
|
105
|
+
el: HTMLElement,
|
|
106
|
+
metrics: {
|
|
107
|
+
scrollHeight: number;
|
|
108
|
+
clientHeight: number;
|
|
109
|
+
scrollTop?: number;
|
|
110
|
+
},
|
|
111
|
+
) {
|
|
112
|
+
Object.defineProperty(el, "scrollHeight", {
|
|
113
|
+
configurable: true,
|
|
114
|
+
get: () => metrics.scrollHeight,
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(el, "clientHeight", {
|
|
117
|
+
configurable: true,
|
|
118
|
+
get: () => metrics.clientHeight,
|
|
119
|
+
});
|
|
120
|
+
Object.defineProperty(el, "scrollTop", {
|
|
121
|
+
configurable: true,
|
|
122
|
+
get: () => metrics.scrollTop ?? 0,
|
|
123
|
+
set: (value: number) => {
|
|
124
|
+
metrics.scrollTop = value;
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it("renders a scroll hint when content overflows", () => {
|
|
130
|
+
render(
|
|
131
|
+
<SlideScrollContainer>
|
|
132
|
+
<div>tall content</div>
|
|
133
|
+
</SlideScrollContainer>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const container = screen.getByTestId("slide-scroll-container");
|
|
137
|
+
stubOverflow(container, { scrollHeight: 900, clientHeight: 400 });
|
|
138
|
+
|
|
139
|
+
act(() => {
|
|
140
|
+
for (const observer of resizeObservers) {
|
|
141
|
+
observer.callback([], observer as unknown as ResizeObserver);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(screen.getByTestId("slide-scroll-hint")).toBeTruthy();
|
|
146
|
+
expect(screen.getByText("Scroll for more")).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("hides the hint after scrolling", () => {
|
|
150
|
+
const metrics = {
|
|
151
|
+
scrollHeight: 900,
|
|
152
|
+
clientHeight: 400,
|
|
153
|
+
scrollTop: 0,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
render(
|
|
157
|
+
<SlideScrollContainer>
|
|
158
|
+
<div>tall content</div>
|
|
159
|
+
</SlideScrollContainer>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const container = screen.getByTestId("slide-scroll-container");
|
|
163
|
+
stubOverflow(container, metrics);
|
|
164
|
+
|
|
165
|
+
act(() => {
|
|
166
|
+
for (const observer of resizeObservers) {
|
|
167
|
+
observer.callback([], observer as unknown as ResizeObserver);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
expect(screen.getByTestId("slide-scroll-hint")).toBeTruthy();
|
|
171
|
+
|
|
172
|
+
act(() => {
|
|
173
|
+
metrics.scrollTop = 50;
|
|
174
|
+
container.dispatchEvent(new Event("scroll"));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(screen.queryByTestId("slide-scroll-hint")).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not show a hint when content fits", () => {
|
|
181
|
+
render(
|
|
182
|
+
<SlideScrollContainer>
|
|
183
|
+
<div>short content</div>
|
|
184
|
+
</SlideScrollContainer>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const container = screen.getByTestId("slide-scroll-container");
|
|
188
|
+
stubOverflow(container, { scrollHeight: 200, clientHeight: 400 });
|
|
189
|
+
|
|
190
|
+
act(() => {
|
|
191
|
+
for (const observer of resizeObservers) {
|
|
192
|
+
observer.callback([], observer as unknown as ResizeObserver);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(screen.queryByTestId("slide-scroll-hint")).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -14,6 +14,7 @@ import { CodeIcon, ExpandIcon, EyeOffIcon } from "lucide-react";
|
|
|
14
14
|
import { Deck, Fragment, Slide, Stack } from "@revealjs/react";
|
|
15
15
|
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
|
|
16
16
|
import { Slide as CellOutputSlide } from "@/components/slides/slide";
|
|
17
|
+
import { SlideScrollContainer } from "@/components/slides/slide-scroll-hint";
|
|
17
18
|
import { Button } from "@/components/ui/button";
|
|
18
19
|
import { useFullScreenElement } from "@/components/ui/fullscreen";
|
|
19
20
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
@@ -327,7 +328,7 @@ const SubslideView = ({
|
|
|
327
328
|
|
|
328
329
|
return (
|
|
329
330
|
<Slide>
|
|
330
|
-
<
|
|
331
|
+
<SlideScrollContainer>
|
|
331
332
|
<div
|
|
332
333
|
className={
|
|
333
334
|
anyCodeShown
|
|
@@ -367,7 +368,7 @@ const SubslideView = ({
|
|
|
367
368
|
return <ReactFragment key={i}>{rendered}</ReactFragment>;
|
|
368
369
|
})}
|
|
369
370
|
</div>
|
|
370
|
-
</
|
|
371
|
+
</SlideScrollContainer>
|
|
371
372
|
{/* Outside any `.fragment`: shown only before any fragment is revealed. */}
|
|
372
373
|
{slideLevel && <NotesAside text={slideLevel} />}
|
|
373
374
|
</Slide>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type RefObject,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "@/utils/cn";
|
|
12
|
+
|
|
13
|
+
/** Pixels of scroll before the hint is considered dismissed. */
|
|
14
|
+
const TOP_THRESHOLD_PX = 8;
|
|
15
|
+
/** Ignore sub-pixel overflow from rounding / borders. */
|
|
16
|
+
const OVERFLOW_TOLERANCE_PX = 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a scroll container should show a "more content below" affordance.
|
|
20
|
+
* Visible only when content overflows and the viewport is still near the top.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldShowScrollHint(options: {
|
|
23
|
+
scrollHeight: number;
|
|
24
|
+
clientHeight: number;
|
|
25
|
+
scrollTop: number;
|
|
26
|
+
}): boolean {
|
|
27
|
+
const { scrollHeight, clientHeight, scrollTop } = options;
|
|
28
|
+
const overflowing = scrollHeight > clientHeight + OVERFLOW_TOLERANCE_PX;
|
|
29
|
+
const atTop = scrollTop <= TOP_THRESHOLD_PX;
|
|
30
|
+
return overflowing && atTop;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Measure only up to the first unrevealed fragment so
|
|
35
|
+
* the hint reflects currently revealed content, not future fragment steps.
|
|
36
|
+
*/
|
|
37
|
+
function measureRevealedHeight(el: HTMLElement): number {
|
|
38
|
+
const firstUnrevealed = el.querySelector<HTMLElement>(
|
|
39
|
+
".fragment:not(.visible)",
|
|
40
|
+
);
|
|
41
|
+
if (!firstUnrevealed) {
|
|
42
|
+
return el.scrollHeight;
|
|
43
|
+
}
|
|
44
|
+
const fragmentTop = firstUnrevealed.getBoundingClientRect().top;
|
|
45
|
+
return fragmentTop - el.getBoundingClientRect().top + el.scrollTop;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tracks whether a scroll container is overflowing at the top, so we can
|
|
50
|
+
* surface a scroll affordance.
|
|
51
|
+
*/
|
|
52
|
+
function useScrollHint(
|
|
53
|
+
scrollRef: RefObject<HTMLElement | null>,
|
|
54
|
+
contentRef: RefObject<HTMLElement | null>,
|
|
55
|
+
): boolean {
|
|
56
|
+
const [showHint, setShowHint] = useState(false);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const el = scrollRef.current;
|
|
60
|
+
const content = contentRef.current;
|
|
61
|
+
if (!el || !content) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let frame = 0;
|
|
66
|
+
const update = () => {
|
|
67
|
+
cancelAnimationFrame(frame);
|
|
68
|
+
frame = requestAnimationFrame(() => {
|
|
69
|
+
setShowHint(
|
|
70
|
+
shouldShowScrollHint({
|
|
71
|
+
scrollHeight: measureRevealedHeight(el),
|
|
72
|
+
clientHeight: el.clientHeight,
|
|
73
|
+
scrollTop: el.scrollTop,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
update();
|
|
80
|
+
|
|
81
|
+
const resizeObserver = new ResizeObserver(update);
|
|
82
|
+
resizeObserver.observe(el);
|
|
83
|
+
resizeObserver.observe(content);
|
|
84
|
+
|
|
85
|
+
// Fragments reveal via class changes (opacity/visibility, no layout shift),
|
|
86
|
+
// which the ResizeObserver can't see, so re-check on class mutations. Only
|
|
87
|
+
// attach on slides that have fragments to avoid reacting to unrelated class
|
|
88
|
+
// churn (e.g. animating widgets) on the common non-fragment slide.
|
|
89
|
+
const fragmentObserver = content.querySelector(".fragment")
|
|
90
|
+
? new MutationObserver(update)
|
|
91
|
+
: null;
|
|
92
|
+
fragmentObserver?.observe(content, {
|
|
93
|
+
subtree: true,
|
|
94
|
+
attributes: true,
|
|
95
|
+
attributeFilter: ["class"],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
cancelAnimationFrame(frame);
|
|
102
|
+
resizeObserver.disconnect();
|
|
103
|
+
fragmentObserver?.disconnect();
|
|
104
|
+
el.removeEventListener("scroll", update);
|
|
105
|
+
};
|
|
106
|
+
}, [scrollRef, contentRef]);
|
|
107
|
+
|
|
108
|
+
return showHint;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ScrollHintOverlay = ({ className }: { className?: string }) => {
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
className={cn(
|
|
115
|
+
"pointer-events-none absolute inset-x-0 bottom-0 z-10 flex flex-col items-center",
|
|
116
|
+
"animate-in fade-in-0 duration-200",
|
|
117
|
+
className,
|
|
118
|
+
)}
|
|
119
|
+
data-testid="slide-scroll-hint"
|
|
120
|
+
aria-hidden={true}
|
|
121
|
+
>
|
|
122
|
+
<div className="h-16 w-full bg-linear-to-t from-background via-background/80 to-transparent" />
|
|
123
|
+
<div className="absolute bottom-3 flex items-center gap-1 rounded-full border border-border/60 bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur-sm">
|
|
124
|
+
<ChevronDownIcon className="h-3.5 w-3.5" />
|
|
125
|
+
<span>Scroll for more</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Full-height scroll container for a slide. Shows a dismissible "Scroll for
|
|
133
|
+
* more" cue when content overflows the frame (easy to miss in fullscreen when
|
|
134
|
+
* OS overlay scrollbars are hidden).
|
|
135
|
+
*/
|
|
136
|
+
export const SlideScrollContainer = ({
|
|
137
|
+
children,
|
|
138
|
+
className,
|
|
139
|
+
}: {
|
|
140
|
+
children: ReactNode;
|
|
141
|
+
className?: string;
|
|
142
|
+
}) => {
|
|
143
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
144
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
145
|
+
const showHint = useScrollHint(scrollRef, contentRef);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={cn("relative h-full w-full", className)}>
|
|
149
|
+
<div
|
|
150
|
+
ref={scrollRef}
|
|
151
|
+
className="h-full w-full overflow-auto"
|
|
152
|
+
data-testid="slide-scroll-container"
|
|
153
|
+
>
|
|
154
|
+
<div ref={contentRef} className="flex w-full min-h-full">
|
|
155
|
+
{children}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
{showHint && <ScrollHintOverlay />}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
@@ -195,7 +195,7 @@ export const OnBlurredInput = React.forwardRef<
|
|
|
195
195
|
onChange={(event) => setInternalValue(event.target.value)}
|
|
196
196
|
onBlur={() => setValue(internalValue || "")}
|
|
197
197
|
onKeyDown={(event) => {
|
|
198
|
-
if (event.key !== "Enter") {
|
|
198
|
+
if (event.key !== "Enter" || event.shiftKey) {
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
setValue(internalValue || "");
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
copyLineDown,
|
|
5
|
+
copyLineUp,
|
|
6
|
+
defaultKeymap as originalDefaultKeymap,
|
|
7
|
+
} from "@codemirror/commands";
|
|
4
8
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
HotkeyProvider,
|
|
11
|
+
OverridingHotkeyProvider,
|
|
12
|
+
} from "@/core/hotkeys/hotkeys";
|
|
6
13
|
import { visibleForTesting } from "../keymaps";
|
|
7
14
|
|
|
8
15
|
const { defaultKeymap, defaultVimKeymap, overrideKeymap, OVERRIDDEN_COMMANDS } =
|
|
@@ -40,4 +47,26 @@ describe("keymaps", () => {
|
|
|
40
47
|
expect(keys.some((k) => k.run === command)).toBe(true);
|
|
41
48
|
}
|
|
42
49
|
});
|
|
50
|
+
|
|
51
|
+
it.each([
|
|
52
|
+
["cell.copyLineUp", copyLineUp, "Alt-Shift-ArrowUp"],
|
|
53
|
+
["cell.copyLineDown", copyLineDown, "Alt-Shift-ArrowDown"],
|
|
54
|
+
] as const)(
|
|
55
|
+
"%s is bound to the configured hotkey, not CodeMirror's default",
|
|
56
|
+
(action, command, defaultKey) => {
|
|
57
|
+
// Without an override, the binding matches CodeMirror's default key.
|
|
58
|
+
expect(
|
|
59
|
+
overrideKeymap(HotkeyProvider.create()).find((k) => k.run === command)
|
|
60
|
+
?.key,
|
|
61
|
+
).toBe(defaultKey);
|
|
62
|
+
|
|
63
|
+
// With an override, the binding follows the user's configured key.
|
|
64
|
+
// This is what `editable: true` on these hotkeys promises.
|
|
65
|
+
expect(
|
|
66
|
+
overrideKeymap(
|
|
67
|
+
new OverridingHotkeyProvider({ [action]: "Ctrl-d" }),
|
|
68
|
+
).find((k) => k.run === command)?.key,
|
|
69
|
+
).toBe("Ctrl-d");
|
|
70
|
+
},
|
|
71
|
+
);
|
|
43
72
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
copyLineDown,
|
|
5
|
+
copyLineUp,
|
|
4
6
|
cursorCharLeft,
|
|
5
7
|
cursorCharRight,
|
|
6
8
|
cursorLineDown,
|
|
@@ -94,6 +96,8 @@ export function keymapBundle(
|
|
|
94
96
|
const OVERRIDDEN_COMMANDS = new Set<Command | undefined>([
|
|
95
97
|
toggleComment,
|
|
96
98
|
toggleBlockComment,
|
|
99
|
+
copyLineUp,
|
|
100
|
+
copyLineDown,
|
|
97
101
|
]);
|
|
98
102
|
|
|
99
103
|
const defaultKeymap = once(() => {
|
|
@@ -111,6 +115,14 @@ const overrideKeymap = (keymap: HotkeyProvider): readonly KeyBinding[] => {
|
|
|
111
115
|
key: keymap.getHotkey("cell.toggleBlockComment").key,
|
|
112
116
|
run: toggleBlockComment,
|
|
113
117
|
},
|
|
118
|
+
{
|
|
119
|
+
key: keymap.getHotkey("cell.copyLineUp").key,
|
|
120
|
+
run: copyLineUp,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: keymap.getHotkey("cell.copyLineDown").key,
|
|
124
|
+
run: copyLineDown,
|
|
125
|
+
},
|
|
114
126
|
];
|
|
115
127
|
};
|
|
116
128
|
|
|
@@ -256,13 +256,11 @@ const DEFAULT_HOT_KEY = {
|
|
|
256
256
|
name: "Copy line(s) up",
|
|
257
257
|
group: "Editing",
|
|
258
258
|
key: "Alt-Shift-ArrowUp",
|
|
259
|
-
editable: false,
|
|
260
259
|
},
|
|
261
260
|
"cell.copyLineDown": {
|
|
262
261
|
name: "Copy line(s) down",
|
|
263
262
|
group: "Editing",
|
|
264
263
|
key: "Alt-Shift-ArrowDown",
|
|
265
|
-
editable: false,
|
|
266
264
|
},
|
|
267
265
|
|
|
268
266
|
// Markdown
|