@marimo-team/islands 0.23.17-dev20 → 0.23.17-dev21
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/{common-BdaGjWOY.js → common-DBjHicCy.js} +1 -1
- package/dist/main.js +16 -13
- package/dist/{reveal-component-agKwqBzN.js → reveal-component-DU8M1Awn.js} +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +114 -2
- package/src/core/codemirror/find-replace/search-highlight.ts +8 -3
- package/src/core/codemirror/theme/__tests__/light.test.ts +59 -123
- package/src/core/codemirror/theme/dark.ts +6 -5
- package/src/core/codemirror/theme/light.ts +6 -4
- package/src/css/app/codemirror.css +11 -7
- package/src/css/globals.css +8 -0
- package/src/stories/theme.stories.tsx +71 -1
package/package.json
CHANGED
|
@@ -1,13 +1,61 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { EditorView } from "@codemirror/view";
|
|
4
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
beforeAll,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
it,
|
|
12
|
+
vi,
|
|
13
|
+
} from "vitest";
|
|
5
14
|
import { SetupMocks } from "@/__mocks__/common";
|
|
6
15
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
16
|
import { ReadonlyCode } from "../readonly-python-code";
|
|
8
17
|
|
|
18
|
+
const themeState = vi.hoisted(() => ({
|
|
19
|
+
value: "light" as "light" | "dark",
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
let originalRangeGetClientRects: PropertyDescriptor | undefined;
|
|
23
|
+
|
|
24
|
+
vi.mock("@/theme/useTheme", async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal<typeof import("@/theme/useTheme")>();
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
useTheme: () => ({ theme: themeState.value }),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
9
32
|
beforeAll(() => {
|
|
10
33
|
SetupMocks.resizeObserver();
|
|
34
|
+
originalRangeGetClientRects = Object.getOwnPropertyDescriptor(
|
|
35
|
+
Range.prototype,
|
|
36
|
+
"getClientRects",
|
|
37
|
+
);
|
|
38
|
+
Object.defineProperty(Range.prototype, "getClientRects", {
|
|
39
|
+
configurable: true,
|
|
40
|
+
value: () => [],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
// Keep the shared JSDOM prototype unchanged for other test files.
|
|
46
|
+
if (originalRangeGetClientRects) {
|
|
47
|
+
Object.defineProperty(
|
|
48
|
+
Range.prototype,
|
|
49
|
+
"getClientRects",
|
|
50
|
+
originalRangeGetClientRects,
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
Reflect.deleteProperty(Range.prototype, "getClientRects");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
themeState.value = "light";
|
|
11
59
|
});
|
|
12
60
|
|
|
13
61
|
/**
|
|
@@ -26,7 +74,71 @@ function isCollapsed(root: ParentNode) {
|
|
|
26
74
|
return root.querySelector(".cm")?.classList.contains("opacity-20") ?? false;
|
|
27
75
|
}
|
|
28
76
|
|
|
77
|
+
function getEditorIdentity(view: EditorView) {
|
|
78
|
+
return {
|
|
79
|
+
doc: view.state.doc,
|
|
80
|
+
dom: view.dom,
|
|
81
|
+
contentDOM: view.contentDOM,
|
|
82
|
+
scrollDOM: view.scrollDOM,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function expectEditorIdentity(
|
|
87
|
+
view: EditorView,
|
|
88
|
+
identity: ReturnType<typeof getEditorIdentity>,
|
|
89
|
+
) {
|
|
90
|
+
expect(view.state.doc).toBe(identity.doc);
|
|
91
|
+
expect(view.dom).toBe(identity.dom);
|
|
92
|
+
expect(view.contentDOM).toBe(identity.contentDOM);
|
|
93
|
+
expect(view.scrollDOM).toBe(identity.scrollDOM);
|
|
94
|
+
}
|
|
95
|
+
|
|
29
96
|
describe("ReadonlyCode", () => {
|
|
97
|
+
it("reconfigures the theme without replacing the editor", async () => {
|
|
98
|
+
let createdView: EditorView | undefined;
|
|
99
|
+
const onCreateEditor = (view: EditorView) => {
|
|
100
|
+
createdView = view;
|
|
101
|
+
};
|
|
102
|
+
const renderCode = (id: string) => (
|
|
103
|
+
<TooltipProvider>
|
|
104
|
+
<ReadonlyCode
|
|
105
|
+
id={id}
|
|
106
|
+
code={"line_1 = 1\nline_2 = 2\nline_3 = 3"}
|
|
107
|
+
showCopyCode={false}
|
|
108
|
+
onCreateEditor={onCreateEditor}
|
|
109
|
+
/>
|
|
110
|
+
</TooltipProvider>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const { rerender } = render(renderCode("light"));
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(createdView).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const view = createdView;
|
|
119
|
+
if (!view) {
|
|
120
|
+
throw new Error("ReadonlyCode did not create an editor view");
|
|
121
|
+
}
|
|
122
|
+
const identity = getEditorIdentity(view);
|
|
123
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(false);
|
|
124
|
+
|
|
125
|
+
themeState.value = "dark";
|
|
126
|
+
rerender(renderCode("dark"));
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
expect(createdView).toBe(view);
|
|
131
|
+
expectEditorIdentity(view, identity);
|
|
132
|
+
|
|
133
|
+
themeState.value = "light";
|
|
134
|
+
rerender(renderCode("light-again"));
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
expect(createdView).toBe(view);
|
|
139
|
+
expectEditorIdentity(view, identity);
|
|
140
|
+
});
|
|
141
|
+
|
|
30
142
|
it("starts collapsed when initiallyHideCode is true", () => {
|
|
31
143
|
const { container } = renderReadonly({ initiallyHideCode: true });
|
|
32
144
|
expect(isCollapsed(container)).toBe(true);
|
|
@@ -140,11 +140,16 @@ export const searchHighlighter = ViewPlugin.fromClass(
|
|
|
140
140
|
);
|
|
141
141
|
|
|
142
142
|
export const highlightTheme = EditorView.baseTheme({
|
|
143
|
-
"&light .cm-searchMatch": {
|
|
144
|
-
|
|
143
|
+
"&light .cm-searchMatch": {
|
|
144
|
+
backgroundColor: "var(--cm-search-match-background-light)",
|
|
145
|
+
},
|
|
146
|
+
"&dark .cm-searchMatch": {
|
|
147
|
+
backgroundColor: "var(--cm-search-match-background-dark)",
|
|
148
|
+
},
|
|
145
149
|
|
|
146
150
|
"&light .cm-searchMatch-selected": { backgroundColor: "transparent" },
|
|
147
151
|
"&dark .cm-searchMatch.cm-searchMatch-selected": {
|
|
148
|
-
backgroundColor:
|
|
152
|
+
backgroundColor:
|
|
153
|
+
"var(--cm-search-match-selected-background-dark) !important",
|
|
149
154
|
},
|
|
150
155
|
});
|
|
@@ -1,136 +1,72 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import type { Extension } from "@codemirror/state";
|
|
4
|
+
import { EditorView } from "@codemirror/view";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { darkTheme } from "../dark";
|
|
5
7
|
import { lightTheme } from "../light";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
const getThemeConfig = () => ({
|
|
9
|
-
variant: "light",
|
|
10
|
-
settings: {
|
|
11
|
-
background: "#ffffff",
|
|
12
|
-
foreground: "#000000",
|
|
13
|
-
caret: "#000000",
|
|
14
|
-
selection: "#d7d4f0",
|
|
15
|
-
lineHighlight: "#cceeff44",
|
|
16
|
-
gutterBackground: "var(--color-background)",
|
|
17
|
-
gutterForeground: "var(--gray-10)",
|
|
18
|
-
},
|
|
19
|
-
styles: [
|
|
20
|
-
{ tag: t.comment, color: "#708090" },
|
|
21
|
-
{ tag: t.variableName, color: "#000000" },
|
|
22
|
-
{ tag: [t.string, t.special(t.brace)], color: "#a11" },
|
|
23
|
-
{ tag: t.number, color: "#164" },
|
|
24
|
-
{ tag: t.bool, color: "#219" },
|
|
25
|
-
{ tag: t.null, color: "#219" },
|
|
26
|
-
{ tag: t.keyword, color: "#708", fontWeight: 500 },
|
|
27
|
-
{ tag: t.className, color: "#00f" },
|
|
28
|
-
{ tag: t.definition(t.typeName), color: "#00f" },
|
|
29
|
-
{ tag: t.typeName, color: "#085" },
|
|
30
|
-
{ tag: t.angleBracket, color: "#000000" },
|
|
31
|
-
{ tag: t.tagName, color: "#170" },
|
|
32
|
-
{ tag: t.attributeName, color: "#00c" },
|
|
33
|
-
{ tag: t.operator, color: "#a2f", fontWeight: 500 },
|
|
34
|
-
{ tag: [t.function(t.variableName)], color: "#00c" },
|
|
35
|
-
{ tag: [t.propertyName], color: "#05a" },
|
|
36
|
-
],
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("lightTheme", () => {
|
|
40
|
-
it("should export a theme", () => {
|
|
41
|
-
expect(lightTheme).toBeDefined();
|
|
42
|
-
expect(typeof lightTheme).toBe("object");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe("theme configuration", () => {
|
|
46
|
-
const config = getThemeConfig();
|
|
47
|
-
|
|
48
|
-
it("should have correct basic settings", () => {
|
|
49
|
-
expect(config.variant).toBe("light");
|
|
50
|
-
expect(config.settings).toEqual({
|
|
51
|
-
background: "#ffffff",
|
|
52
|
-
foreground: "#000000",
|
|
53
|
-
caret: "#000000",
|
|
54
|
-
selection: "#d7d4f0",
|
|
55
|
-
lineHighlight: "#cceeff44",
|
|
56
|
-
gutterBackground: "var(--color-background)",
|
|
57
|
-
gutterForeground: "var(--gray-10)",
|
|
58
|
-
});
|
|
59
|
-
});
|
|
9
|
+
const mounted: Array<{ host: HTMLDivElement; view: EditorView }> = [];
|
|
60
10
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
11
|
+
function mountTheme(extension: Extension) {
|
|
12
|
+
const host = document.createElement("div");
|
|
13
|
+
document.body.append(host);
|
|
14
|
+
const root = host.attachShadow({ mode: "open" });
|
|
15
|
+
const parent = document.createElement("div");
|
|
16
|
+
root.append(parent);
|
|
17
|
+
const view = new EditorView({ extensions: extension, parent, root });
|
|
18
|
+
mounted.push({ host, view });
|
|
66
19
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
20
|
+
const style = root.querySelector("style");
|
|
21
|
+
if (!style) {
|
|
22
|
+
throw new Error("CodeMirror did not mount its theme styles");
|
|
23
|
+
}
|
|
24
|
+
return { css: style.textContent ?? "", view };
|
|
25
|
+
}
|
|
73
26
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
expect(style?.tag).toContain(t.special(t.brace));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("should style numbers and boolean values", () => {
|
|
84
|
-
expect(findStyle(t.number)).toEqual({
|
|
85
|
-
tag: t.number,
|
|
86
|
-
color: "#164",
|
|
87
|
-
});
|
|
88
|
-
expect(findStyle(t.bool)).toEqual({
|
|
89
|
-
tag: t.bool,
|
|
90
|
-
color: "#219",
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("should style keywords with emphasis", () => {
|
|
95
|
-
expect(findStyle(t.keyword)).toEqual({
|
|
96
|
-
tag: t.keyword,
|
|
97
|
-
color: "#708",
|
|
98
|
-
fontWeight: 500,
|
|
99
|
-
});
|
|
100
|
-
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
for (const { host, view } of mounted.splice(0)) {
|
|
29
|
+
view.destroy();
|
|
30
|
+
host.remove();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
101
33
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
34
|
+
const themeCases = [
|
|
35
|
+
{
|
|
36
|
+
name: "light",
|
|
37
|
+
extension: lightTheme,
|
|
38
|
+
dark: false,
|
|
39
|
+
selectionAlias: "var(--cm-selection-background-light)",
|
|
40
|
+
reactiveColor: "var(--cm-reactive-reference-color-light)",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "dark",
|
|
44
|
+
extension: darkTheme,
|
|
45
|
+
dark: true,
|
|
46
|
+
selectionAlias: "var(--cm-selection-background-dark)",
|
|
47
|
+
reactiveColor: "var(--cm-reactive-reference-color-dark)",
|
|
48
|
+
},
|
|
49
|
+
] as const;
|
|
109
50
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
color: "#00f",
|
|
114
|
-
});
|
|
115
|
-
expect(findStyle(t.typeName)).toEqual({
|
|
116
|
-
tag: t.typeName,
|
|
117
|
-
color: "#085",
|
|
118
|
-
});
|
|
119
|
-
});
|
|
51
|
+
describe.each(themeCases)("$name theme", (theme) => {
|
|
52
|
+
it("uses the production theme tokens", () => {
|
|
53
|
+
const { css, view } = mountTheme(theme.extension);
|
|
120
54
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
55
|
+
expect(view.state.facet(EditorView.darkTheme)).toBe(theme.dark);
|
|
56
|
+
expect(css).toContain(
|
|
57
|
+
`--cm-selection-background: ${theme.selectionAlias};`,
|
|
58
|
+
);
|
|
59
|
+
expect(css).toContain("background-color: var(--cm-selection-background);");
|
|
60
|
+
expect(css).toContain(`color: ${theme.reactiveColor};`);
|
|
61
|
+
expect(css).toContain(
|
|
62
|
+
"border-bottom: 2px solid var(--cm-reactive-reference-border-color);",
|
|
63
|
+
);
|
|
127
64
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
65
|
+
const hoverRule = css
|
|
66
|
+
.split("\n")
|
|
67
|
+
.find((rule) => rule.includes(".mo-cm-reactive-reference-hover"));
|
|
68
|
+
expect(hoverRule).toBeDefined();
|
|
69
|
+
expect(hoverRule).toContain("cursor: pointer;");
|
|
70
|
+
expect(hoverRule).not.toContain("border-bottom");
|
|
135
71
|
});
|
|
136
72
|
});
|
|
@@ -11,7 +11,7 @@ export const darkTheme = [
|
|
|
11
11
|
background: "var(--cm-background)",
|
|
12
12
|
foreground: "#abb2bf",
|
|
13
13
|
caret: "#528bff",
|
|
14
|
-
selection: "
|
|
14
|
+
selection: "var(--cm-selection-background)",
|
|
15
15
|
lineHighlight: "#2c313c",
|
|
16
16
|
gutterBackground: "var(--color-background)",
|
|
17
17
|
gutterForeground: "var(--gray-10)",
|
|
@@ -36,15 +36,16 @@ export const darkTheme = [
|
|
|
36
36
|
],
|
|
37
37
|
}),
|
|
38
38
|
EditorView.theme({
|
|
39
|
+
"&": {
|
|
40
|
+
"--cm-selection-background": "var(--cm-selection-background-dark)",
|
|
41
|
+
},
|
|
39
42
|
".mo-cm-reactive-reference": {
|
|
40
43
|
fontWeight: "400",
|
|
41
|
-
color: "
|
|
42
|
-
borderBottom: "2px solid
|
|
44
|
+
color: "var(--cm-reactive-reference-color-dark)",
|
|
45
|
+
borderBottom: "2px solid var(--cm-reactive-reference-border-color)",
|
|
43
46
|
},
|
|
44
47
|
".mo-cm-reactive-reference-hover": {
|
|
45
48
|
cursor: "pointer",
|
|
46
|
-
borderBottomWidth: "3px",
|
|
47
|
-
borderBottomColor: "#4a90a5",
|
|
48
49
|
},
|
|
49
50
|
}),
|
|
50
51
|
];
|
|
@@ -11,7 +11,7 @@ export const lightTheme = [
|
|
|
11
11
|
background: "#ffffff",
|
|
12
12
|
foreground: "#000000",
|
|
13
13
|
caret: "#000000",
|
|
14
|
-
selection: "
|
|
14
|
+
selection: "var(--cm-selection-background)",
|
|
15
15
|
lineHighlight: "#cceeff44",
|
|
16
16
|
gutterBackground: "var(--color-background)",
|
|
17
17
|
gutterForeground: "var(--gray-10)",
|
|
@@ -45,14 +45,16 @@ export const lightTheme = [
|
|
|
45
45
|
],
|
|
46
46
|
}),
|
|
47
47
|
EditorView.theme({
|
|
48
|
+
"&": {
|
|
49
|
+
"--cm-selection-background": "var(--cm-selection-background-light)",
|
|
50
|
+
},
|
|
48
51
|
".mo-cm-reactive-reference": {
|
|
49
52
|
fontWeight: "400",
|
|
50
|
-
color: "
|
|
51
|
-
borderBottom: "2px solid
|
|
53
|
+
color: "var(--cm-reactive-reference-color-light)",
|
|
54
|
+
borderBottom: "2px solid var(--cm-reactive-reference-border-color)",
|
|
52
55
|
},
|
|
53
56
|
".mo-cm-reactive-reference-hover": {
|
|
54
57
|
cursor: "pointer",
|
|
55
|
-
borderBottomWidth: "3px",
|
|
56
58
|
},
|
|
57
59
|
}),
|
|
58
60
|
];
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
@reference "../globals.css";
|
|
2
2
|
|
|
3
3
|
.light .cm-selectionBackground {
|
|
4
|
-
/*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/* Keep drawn selections visible when focus moves to find/replace or another editor. */
|
|
5
|
+
background-color: var(
|
|
6
|
+
--cm-selection-background,
|
|
7
|
+
var(--cm-selection-background-light)
|
|
8
|
+
) !important;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
.dark .cm-selectionBackground {
|
|
11
|
-
background-color:
|
|
12
|
+
background-color: var(
|
|
13
|
+
--cm-selection-background,
|
|
14
|
+
var(--cm-selection-background-dark)
|
|
15
|
+
) !important;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
.cm-focused.cm-focused {
|
|
@@ -125,7 +129,7 @@
|
|
|
125
129
|
/* -- Codeium -- */
|
|
126
130
|
|
|
127
131
|
.cm-codeium-cycle {
|
|
128
|
-
font-size:
|
|
132
|
+
font-size: 0.5625rem;
|
|
129
133
|
background-color: var(--sky-3);
|
|
130
134
|
padding: 2px;
|
|
131
135
|
border-radius: 2px;
|
|
@@ -133,7 +137,7 @@
|
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
.cm-codeium-cycle-key {
|
|
136
|
-
font-size:
|
|
140
|
+
font-size: 0.5625rem;
|
|
137
141
|
font-family: monospace;
|
|
138
142
|
display: inline-block;
|
|
139
143
|
padding: 2px;
|
package/src/css/globals.css
CHANGED
|
@@ -107,6 +107,14 @@
|
|
|
107
107
|
/* Codemirror editor */
|
|
108
108
|
--cm-background: light-dark(var(--background), #282c34);
|
|
109
109
|
--cm-comment: light-dark(#708090, #6b7280);
|
|
110
|
+
--cm-selection-background-light: #d7d4f0;
|
|
111
|
+
--cm-selection-background-dark: #1177cc80;
|
|
112
|
+
--cm-reactive-reference-color-light: #005f87;
|
|
113
|
+
--cm-reactive-reference-color-dark: #2a7aa5;
|
|
114
|
+
--cm-reactive-reference-border-color: #bad3de;
|
|
115
|
+
--cm-search-match-background-light: #99ff7780;
|
|
116
|
+
--cm-search-match-background-dark: #22bb0070;
|
|
117
|
+
--cm-search-match-selected-background-dark: #6199ff88;
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
.dark,
|
|
@@ -4,11 +4,14 @@ import { python } from "@codemirror/lang-python";
|
|
|
4
4
|
import { EditorState, type Extension } from "@codemirror/state";
|
|
5
5
|
import { EditorView } from "@codemirror/view";
|
|
6
6
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
7
|
-
import React, { useEffect, useRef } from "react";
|
|
7
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { ReadonlyCode } from "../components/editor/code/readonly-python-code";
|
|
8
9
|
import { basicBundle, type CodeMirrorSetupOpts } from "../core/codemirror/cm";
|
|
10
|
+
import { userConfigAtom } from "../core/config/config";
|
|
9
11
|
import { darkTheme } from "../core/codemirror/theme/dark";
|
|
10
12
|
import { lightTheme } from "../core/codemirror/theme/light";
|
|
11
13
|
import { OverridingHotkeyProvider } from "../core/hotkeys/hotkeys";
|
|
14
|
+
import { store } from "../core/state/jotai";
|
|
12
15
|
|
|
13
16
|
// Partial config for storybook demo
|
|
14
17
|
const demoConfig: Partial<CodeMirrorSetupOpts> = {
|
|
@@ -52,6 +55,11 @@ result = example.get_value()
|
|
|
52
55
|
size = example.data_size
|
|
53
56
|
`.trim();
|
|
54
57
|
|
|
58
|
+
const READONLY_CONTENT = Array.from(
|
|
59
|
+
{ length: 80 },
|
|
60
|
+
(_, index) => `value_${index + 1} = ${index + 1}`,
|
|
61
|
+
).join("\n");
|
|
62
|
+
|
|
55
63
|
const Editor = (opts: { extensions?: Extension[] }): React.ReactNode => {
|
|
56
64
|
const ref = useRef<HTMLDivElement>(null);
|
|
57
65
|
useEffect(() => {
|
|
@@ -110,3 +118,65 @@ export const ThemeComparison: Story = {
|
|
|
110
118
|
</div>
|
|
111
119
|
),
|
|
112
120
|
};
|
|
121
|
+
|
|
122
|
+
const ReadonlyCodeThemeFixture = (): React.ReactNode => {
|
|
123
|
+
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
124
|
+
const [initialTheme] = useState(
|
|
125
|
+
() => store.get(userConfigAtom).display.theme,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
store.set(userConfigAtom, (config) => ({
|
|
130
|
+
...config,
|
|
131
|
+
display: { ...config.display, theme },
|
|
132
|
+
}));
|
|
133
|
+
}, [theme]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
// Leave Storybook's theme in the state it had before this fixture mounted.
|
|
137
|
+
return () => {
|
|
138
|
+
store.set(userConfigAtom, (config) => ({
|
|
139
|
+
...config,
|
|
140
|
+
display: { ...config.display, theme: initialTheme },
|
|
141
|
+
}));
|
|
142
|
+
};
|
|
143
|
+
}, [initialTheme]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="max-w-3xl space-y-3" style={{ colorScheme: theme }}>
|
|
147
|
+
<div className="flex items-center justify-between gap-4">
|
|
148
|
+
<div>
|
|
149
|
+
<h3 className="text-lg font-semibold">ReadonlyCode fixture</h3>
|
|
150
|
+
<p className="text-sm text-muted-foreground">
|
|
151
|
+
Select text, scroll, then switch themes to check that both remain.
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="flex gap-2" role="group" aria-label="Theme">
|
|
155
|
+
{(["light", "dark"] as const).map((nextTheme) => (
|
|
156
|
+
<button
|
|
157
|
+
aria-pressed={theme === nextTheme}
|
|
158
|
+
className="rounded border px-3 py-1 text-sm"
|
|
159
|
+
key={nextTheme}
|
|
160
|
+
onClick={() => setTheme(nextTheme)}
|
|
161
|
+
type="button"
|
|
162
|
+
>
|
|
163
|
+
{nextTheme}
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="h-64 overflow-hidden rounded border">
|
|
169
|
+
<ReadonlyCode
|
|
170
|
+
className="h-full [&_.cm-editor]:h-full [&_.cm-scroller]:h-full [&_.cm-theme-none]:h-full"
|
|
171
|
+
code={READONLY_CONTENT}
|
|
172
|
+
showCopyCode={false}
|
|
173
|
+
showHideCode={false}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const ReadonlyCodeThemeReconfiguration: Story = {
|
|
181
|
+
render: () => <ReadonlyCodeThemeFixture />,
|
|
182
|
+
};
|