@marimo-team/islands 0.23.14-dev16 → 0.23.14-dev19
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/{chat-ui-C6y6xLcn.js → chat-ui-9wA4feLE.js} +3034 -3034
- package/dist/{code-visibility-Dc1Ap0mZ.js → code-visibility-CxDvzm2S.js} +214 -201
- package/dist/{html-to-image-DcTkcjmm.js → html-to-image-DBHfshED.js} +63 -62
- package/dist/main.js +1154 -1100
- package/dist/{process-output-2FbS8KgW.js → process-output-Dw6We6B6.js} +1 -1
- package/dist/{reveal-component-CskA0jHe.js → reveal-component-tJhA77Am.js} +13 -13
- package/package.json +1 -1
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/code/readonly-python-code.tsx +16 -2
- package/src/components/editor/renderers/vertical-layout/__tests__/vertical-layout.test.ts +1 -27
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -19
- package/src/components/slides/slide-cell-view.tsx +7 -6
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
2
2
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
3
|
-
import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-
|
|
3
|
+
import { at as parseHtmlContent, it as ansiToPlainText } from "./html-to-image-DBHfshED.js";
|
|
4
4
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
5
5
|
import { t as Strings } from "./strings-Dq_j3Rxw.js";
|
|
6
6
|
import { t as require_jsx_runtime } from "./jsx-runtime-DebpN0FN.js";
|
|
@@ -6,10 +6,10 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
|
6
6
|
import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-BacYv-bE.js";
|
|
7
7
|
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
8
8
|
import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
9
|
-
import { lt as kioskModeAtom } from "./html-to-image-
|
|
9
|
+
import { lt as kioskModeAtom } from "./html-to-image-DBHfshED.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-BbqSm5gU.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist--2Bqjvs0.js";
|
|
12
|
-
import { a as DEFAULT_SLIDE_TYPE,
|
|
12
|
+
import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelGroup, i as DEFAULT_DECK_TRANSITION, lt as PanelResizeHandle, on as EyeOff, s as SlideSidebar, sn as Expand, st as Panel, t as useNotebookCodeAvailable, un as Code } from "./code-visibility-CxDvzm2S.js";
|
|
13
13
|
import { X as useDebouncedCallback } from "./input-BSdZp5Ng.js";
|
|
14
14
|
import "./toDate-D1Z7ZXWh.js";
|
|
15
15
|
import "./react-dom-BTJzcVJ9.js";
|
|
@@ -7245,12 +7245,12 @@ function _temp(e5) {
|
|
|
7245
7245
|
};
|
|
7246
7246
|
}
|
|
7247
7247
|
var SubslideView = (e5) => {
|
|
7248
|
-
let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a } = e5, o, s, c, l, u,
|
|
7248
|
+
let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a } = e5, o, s, c, l, u, ro;
|
|
7249
7249
|
if (t[0] !== i || t[1] !== r || t[2] !== a || t[3] !== n) {
|
|
7250
7250
|
let { slideLevel: e6, cumulativeByBlock: io2 } = buildSubslideNotes(n, a);
|
|
7251
7251
|
s = e6;
|
|
7252
7252
|
let p2 = n.blocks.some((e7) => e7.cells.some((e8) => r(e8.id)));
|
|
7253
|
-
o = w,
|
|
7253
|
+
o = w, ro = "h-full w-full overflow-auto flex", c = p2 ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", t[10] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (l = { margin: "auto 20px" }, t[10] = l) : l = t[10], u = n.blocks.map((e7, t2) => {
|
|
7254
7254
|
let n2 = e7.cells.map((e8) => r(e8.id) ? i ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellReadOnlyView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slide, {
|
|
7255
7255
|
cellId: e8.id,
|
|
7256
7256
|
status: e8.status,
|
|
@@ -7264,8 +7264,8 @@ var SubslideView = (e5) => {
|
|
|
7264
7264
|
}, t2);
|
|
7265
7265
|
}
|
|
7266
7266
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: n2 }, t2);
|
|
7267
|
-
}), t[0] = i, t[1] = r, t[2] = a, t[3] = n, t[4] = o, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] =
|
|
7268
|
-
} else o = t[4], s = t[5], c = t[6], l = t[7], u = t[8],
|
|
7267
|
+
}), t[0] = i, t[1] = r, t[2] = a, t[3] = n, t[4] = o, t[5] = s, t[6] = c, t[7] = l, t[8] = u, t[9] = ro;
|
|
7268
|
+
} else o = t[4], s = t[5], c = t[6], l = t[7], u = t[8], ro = t[9];
|
|
7269
7269
|
let io;
|
|
7270
7270
|
t[11] !== c || t[12] !== l || t[13] !== u ? (io = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7271
7271
|
className: c,
|
|
@@ -7273,10 +7273,10 @@ var SubslideView = (e5) => {
|
|
|
7273
7273
|
children: u
|
|
7274
7274
|
}), t[11] = c, t[12] = l, t[13] = u, t[14] = io) : io = t[14];
|
|
7275
7275
|
let p;
|
|
7276
|
-
t[15] !==
|
|
7277
|
-
className:
|
|
7276
|
+
t[15] !== ro || t[16] !== io ? (p = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7277
|
+
className: ro,
|
|
7278
7278
|
children: io
|
|
7279
|
-
}), t[15] =
|
|
7279
|
+
}), t[15] = ro, t[16] = io, t[17] = p) : p = t[17];
|
|
7280
7280
|
let ao;
|
|
7281
7281
|
t[18] === s ? ao = t[19] : (ao = s && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: s }), t[18] = s, t[19] = ao);
|
|
7282
7282
|
let oo;
|
|
@@ -7302,7 +7302,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7302
7302
|
status: n.status,
|
|
7303
7303
|
output: n.output
|
|
7304
7304
|
}), t[3] = n.id, t[4] = n.output, t[5] = n.status, t[6] = o) : o = t[6], o;
|
|
7305
|
-
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex: u, onSlideChange:
|
|
7305
|
+
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: l, activeIndex: u, onSlideChange: f, mode: io, configWidth: fo, isEditable: po = false }) => {
|
|
7306
7306
|
var _a2, _b2;
|
|
7307
7307
|
let yo = (0, import_react.useRef)(null), bo = (0, import_react.useRef)(null), { width: So, height: Co } = useSlideDimensions(yo), wo = useFullScreenElement() != null, To = useAtomValue(kioskModeAtom), Eo = (0, import_react.useMemo)(() => To ? [] : [ce], [To]), [Do, Oo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), ko = useNotebookCodeAvailable(e5), Ao = !isIslands() && ko, jo = u == null ? void 0 : e5[u], Mo = jo ?? e5.at(0), { parkedPreviewCell: No, isHeldEdit: Po, isNoOutputPreview: Fo, heldEditCellId: Io, heldShowsCode: Lo, toggleHeldShowsCode: Ro } = useParkedPreview({
|
|
7308
7308
|
activeCell: jo,
|
|
@@ -7391,14 +7391,14 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7391
7391
|
let e6 = bo.current;
|
|
7392
7392
|
if (!e6) return;
|
|
7393
7393
|
let t = resolveActiveCellIndex(Go, e6.getIndices());
|
|
7394
|
-
t != null && (
|
|
7394
|
+
t != null && (f == null ? void 0 : f(t));
|
|
7395
7395
|
}), $o = useEvent_default((t) => {
|
|
7396
7396
|
if (!No || u == null || Events.fromInput(t)) return;
|
|
7397
7397
|
let n = classifyNavKey(t);
|
|
7398
7398
|
if (n === 0) return;
|
|
7399
7399
|
t.preventDefault(), t.stopPropagation();
|
|
7400
7400
|
let i = u + n;
|
|
7401
|
-
i < 0 || i >= e5.length || (
|
|
7401
|
+
i < 0 || i >= e5.length || (f == null ? void 0 : f(i));
|
|
7402
7402
|
}), ns = useEvent_default(() => {
|
|
7403
7403
|
Qo(), triggerResize(bo.current);
|
|
7404
7404
|
});
|
|
@@ -7494,7 +7494,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7494
7494
|
]
|
|
7495
7495
|
})
|
|
7496
7496
|
});
|
|
7497
|
-
return
|
|
7497
|
+
return io === "read" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7498
7498
|
className: "flex-1 min-w-0 flex flex-row gap-3",
|
|
7499
7499
|
children: os
|
|
7500
7500
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
package/package.json
CHANGED
|
@@ -126,3 +126,28 @@ describe("ColumnExplorerPanel header counts", () => {
|
|
|
126
126
|
expect(screen.queryByText(/hidden/)).not.toBeInTheDocument();
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
describe("ColumnExplorerPanel visibility actions", () => {
|
|
131
|
+
const showAll = () => screen.getByRole("button", { name: /Show all/ });
|
|
132
|
+
const hideAll = () => screen.getByRole("button", { name: /Hide all/ });
|
|
133
|
+
|
|
134
|
+
it("disables 'Show all' when every column is visible", () => {
|
|
135
|
+
renderPanel();
|
|
136
|
+
expect(showAll()).toBeDisabled();
|
|
137
|
+
expect(hideAll()).toBeEnabled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("disables 'Hide all' when every column is hidden", () => {
|
|
141
|
+
renderPanel({
|
|
142
|
+
initiallyHidden: ["customer_name", "cust_age", "order_total"],
|
|
143
|
+
});
|
|
144
|
+
expect(showAll()).toBeEnabled();
|
|
145
|
+
expect(hideAll()).toBeDisabled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("enables both actions when some columns are hidden", () => {
|
|
149
|
+
renderPanel({ initiallyHidden: ["cust_age"] });
|
|
150
|
+
expect(showAll()).toBeEnabled();
|
|
151
|
+
expect(hideAll()).toBeEnabled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -21,10 +21,17 @@ beforeAll(() => {
|
|
|
21
21
|
|
|
22
22
|
type Row = Record<string, unknown>;
|
|
23
23
|
|
|
24
|
+
// Select, index, and nameless columns are non-hideable in production
|
|
25
|
+
// (see columns.tsx), so the harness mirrors that to keep visibility counts
|
|
26
|
+
// aligned with the real table.
|
|
24
27
|
const TEST_COLUMNS: ColumnDef<Row>[] = [
|
|
25
|
-
{ id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID },
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
+
{ id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID, enableHiding: false },
|
|
29
|
+
{
|
|
30
|
+
id: INDEX_COLUMN_NAME,
|
|
31
|
+
accessorKey: INDEX_COLUMN_NAME,
|
|
32
|
+
enableHiding: false,
|
|
33
|
+
},
|
|
34
|
+
{ id: "__m_column__0", accessorKey: "__m_column__0", enableHiding: false },
|
|
28
35
|
{
|
|
29
36
|
id: "customer_name",
|
|
30
37
|
accessorKey: "customer_name",
|
|
@@ -101,6 +108,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
101
108
|
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
102
109
|
expect(getOptionTexts()).toEqual([
|
|
103
110
|
"Show all",
|
|
111
|
+
"Hide all",
|
|
104
112
|
"cust_age",
|
|
105
113
|
"customer_name",
|
|
106
114
|
"order_total",
|
|
@@ -117,6 +125,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
117
125
|
|
|
118
126
|
expect(getOptionTexts()).toEqual([
|
|
119
127
|
"Show all",
|
|
128
|
+
"Hide all",
|
|
120
129
|
"cust_age",
|
|
121
130
|
"customer_name",
|
|
122
131
|
"order_total",
|
|
@@ -132,6 +141,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
132
141
|
fireEvent.click(getColumnOption("customer_name"));
|
|
133
142
|
expect(getOptionTexts()).toEqual([
|
|
134
143
|
"Show all",
|
|
144
|
+
"Hide all",
|
|
135
145
|
"cust_age",
|
|
136
146
|
"customer_name",
|
|
137
147
|
"order_total",
|
|
@@ -144,6 +154,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
144
154
|
// Reopen sorts both hidden columns first, preserving table order.
|
|
145
155
|
expect(getOptionTexts()).toEqual([
|
|
146
156
|
"Show all",
|
|
157
|
+
"Hide all",
|
|
147
158
|
"customer_name",
|
|
148
159
|
"cust_age",
|
|
149
160
|
"order_total",
|
|
@@ -189,6 +200,52 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
189
200
|
);
|
|
190
201
|
});
|
|
191
202
|
|
|
203
|
+
it("'Hide all' hides every hideable column", () => {
|
|
204
|
+
renderAndOpen();
|
|
205
|
+
fireEvent.click(getColumnOption("Hide all"));
|
|
206
|
+
|
|
207
|
+
expect(
|
|
208
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
209
|
+
).not.toBeNull();
|
|
210
|
+
expect(
|
|
211
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
212
|
+
).not.toBeNull();
|
|
213
|
+
expect(
|
|
214
|
+
getColumnOption("order_total").querySelector(".lucide-eye-off"),
|
|
215
|
+
).not.toBeNull();
|
|
216
|
+
expect(getColumnOption("Hide all")).toHaveAttribute(
|
|
217
|
+
"aria-disabled",
|
|
218
|
+
"true",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("disables 'Hide all' when every column is already hidden", () => {
|
|
223
|
+
renderAndOpen({
|
|
224
|
+
initiallyHidden: ["customer_name", "cust_age", "order_total"],
|
|
225
|
+
});
|
|
226
|
+
expect(getColumnOption("Hide all")).toHaveAttribute(
|
|
227
|
+
"aria-disabled",
|
|
228
|
+
"true",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("'Hide all' leaves non-hideable columns visible", () => {
|
|
233
|
+
renderAndOpen({ nonHideable: ["customer_name"] });
|
|
234
|
+
fireEvent.click(getColumnOption("Hide all"));
|
|
235
|
+
|
|
236
|
+
expect(
|
|
237
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
238
|
+
).not.toBeNull();
|
|
239
|
+
const nonHideable = getColumnOption("customer_name");
|
|
240
|
+
expect(nonHideable).toHaveAttribute("aria-disabled", "true");
|
|
241
|
+
expect(nonHideable.querySelector(".lucide-eye-off")).toBeNull();
|
|
242
|
+
// Every hideable column is now hidden, so the action gates off.
|
|
243
|
+
expect(getColumnOption("Hide all")).toHaveAttribute(
|
|
244
|
+
"aria-disabled",
|
|
245
|
+
"true",
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
192
249
|
it("hides and shows matching columns via bulk actions while searching", () => {
|
|
193
250
|
renderAndOpen();
|
|
194
251
|
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
@@ -50,7 +50,10 @@ import {
|
|
|
50
50
|
import { smartMatch } from "@/utils/smartMatch";
|
|
51
51
|
import type { Column, Table } from "@tanstack/react-table";
|
|
52
52
|
import { cn } from "@/utils/cn";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
getColumnCountForDisplay,
|
|
55
|
+
getUserColumnVisibilityCounts,
|
|
56
|
+
} from "../hooks/use-column-visibility";
|
|
54
57
|
|
|
55
58
|
interface ColumnExplorerPanelProps<TData> {
|
|
56
59
|
previewColumn: PreviewColumn;
|
|
@@ -90,6 +93,7 @@ export function ColumnExplorerPanel<TData>({
|
|
|
90
93
|
totalColumns: effectiveTotalColumns,
|
|
91
94
|
hiddenColumns: hiddenColumnCount,
|
|
92
95
|
} = getColumnCountForDisplay(table, totalColumns);
|
|
96
|
+
const { visible: visibleColumnCount } = getUserColumnVisibilityCounts(table);
|
|
93
97
|
|
|
94
98
|
const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
|
|
95
99
|
numRows: totalRows,
|
|
@@ -108,17 +112,34 @@ export function ColumnExplorerPanel<TData>({
|
|
|
108
112
|
value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
|
|
109
113
|
className="h-3 w-3"
|
|
110
114
|
/>
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
<div className="ml-auto flex items-center gap-1">
|
|
116
|
+
<Tooltip content="Show all columns" delayDuration={400}>
|
|
117
|
+
<Button
|
|
118
|
+
type="button"
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="xs"
|
|
121
|
+
className="gap-1"
|
|
122
|
+
disabled={hiddenColumnCount === 0}
|
|
123
|
+
onClick={() => table.toggleAllColumnsVisible(true)}
|
|
124
|
+
>
|
|
125
|
+
<EyeIcon className="w-3 h-3" />
|
|
126
|
+
Show all
|
|
127
|
+
</Button>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
<Tooltip content="Hide all columns" delayDuration={400}>
|
|
130
|
+
<Button
|
|
131
|
+
type="button"
|
|
132
|
+
variant="ghost"
|
|
133
|
+
size="xs"
|
|
134
|
+
className="gap-1"
|
|
135
|
+
disabled={visibleColumnCount === 0}
|
|
136
|
+
onClick={() => table.toggleAllColumnsVisible(false)}
|
|
137
|
+
>
|
|
138
|
+
<EyeOffIcon className="w-3 h-3" />
|
|
139
|
+
Hide all
|
|
140
|
+
</Button>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
</div>
|
|
122
143
|
</div>
|
|
123
144
|
<Command className="h-5/6 bg-background" shouldFilter={false}>
|
|
124
145
|
<CommandInput
|
|
@@ -59,6 +59,12 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
59
59
|
const hiddenIds = userColumns
|
|
60
60
|
.filter((column) => !column.getIsVisible())
|
|
61
61
|
.map((column) => column.id);
|
|
62
|
+
const hideableIds = userColumns
|
|
63
|
+
.filter((column) => column.getCanHide())
|
|
64
|
+
.map((column) => column.id);
|
|
65
|
+
const visibleHideableCount = hideableIds.filter(
|
|
66
|
+
(id) => !hiddenIds.includes(id),
|
|
67
|
+
).length;
|
|
62
68
|
|
|
63
69
|
const applyHidden = (next: string[] | string | null) => {
|
|
64
70
|
const hidden = new Set(Array.isArray(next) ? next : []);
|
|
@@ -127,6 +133,15 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
127
133
|
<EyeIcon className="w-3 h-3 mr-1.5" />
|
|
128
134
|
Show all
|
|
129
135
|
</CommandItem>
|
|
136
|
+
<CommandItem
|
|
137
|
+
value="__hide_all__"
|
|
138
|
+
disabled={visibleHideableCount === 0}
|
|
139
|
+
onSelect={() => applyHidden(hideableIds)}
|
|
140
|
+
className="cursor-pointer"
|
|
141
|
+
>
|
|
142
|
+
<EyeOffIcon className="w-3 h-3 mr-1.5" />
|
|
143
|
+
Hide all
|
|
144
|
+
</CommandItem>
|
|
130
145
|
<CommandSeparator />
|
|
131
146
|
</>
|
|
132
147
|
) : (
|
|
@@ -31,7 +31,7 @@ import { cn } from "@/utils/cn";
|
|
|
31
31
|
import { HideInKioskMode } from "../../kiosk-mode";
|
|
32
32
|
import { ContributeSnippetButton } from "../components/contribute-snippet-button";
|
|
33
33
|
import { usePanelOrientation, usePanelSection } from "./panel-context";
|
|
34
|
-
import {
|
|
34
|
+
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
35
35
|
|
|
36
36
|
const extensions = [EditorView.lineWrapping];
|
|
37
37
|
|
|
@@ -188,7 +188,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
|
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
const { language,
|
|
191
|
+
const { language, code: displayCode } = getReadonlyCodeDisplay(code);
|
|
192
192
|
|
|
193
193
|
return (
|
|
194
194
|
<div
|
|
@@ -216,7 +216,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
|
|
|
216
216
|
language={language}
|
|
217
217
|
className="cm border rounded overflow-hidden"
|
|
218
218
|
extensions={extensions}
|
|
219
|
-
value={
|
|
219
|
+
value={displayCode}
|
|
220
220
|
readOnly={true}
|
|
221
221
|
/>
|
|
222
222
|
</Suspense>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
3
4
|
import { sql } from "@codemirror/lang-sql";
|
|
4
5
|
import CodeMirror, {
|
|
5
6
|
EditorView,
|
|
@@ -11,6 +12,7 @@ import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
|
|
|
11
12
|
import { Button } from "@/components/ui/button";
|
|
12
13
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
13
14
|
import { toast } from "@/components/ui/use-toast";
|
|
15
|
+
import type { LanguageAdapterType } from "@/core/codemirror/language/types";
|
|
14
16
|
import { customPythonLanguageSupport } from "@/core/codemirror/language/languages/python";
|
|
15
17
|
import { useTheme } from "@/theme/useTheme";
|
|
16
18
|
import { cn } from "@/utils/cn";
|
|
@@ -22,6 +24,18 @@ const pythonExtensions = [
|
|
|
22
24
|
EditorView.lineWrapping,
|
|
23
25
|
];
|
|
24
26
|
const sqlExtensions = [sql(), EditorView.lineWrapping];
|
|
27
|
+
const markdownExtensions = [markdown(), EditorView.lineWrapping];
|
|
28
|
+
|
|
29
|
+
function readonlyCodeExtensions(language: LanguageAdapterType) {
|
|
30
|
+
switch (language) {
|
|
31
|
+
case "sql":
|
|
32
|
+
return sqlExtensions;
|
|
33
|
+
case "markdown":
|
|
34
|
+
return markdownExtensions;
|
|
35
|
+
default:
|
|
36
|
+
return pythonExtensions;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
27
41
|
* A readonly code component that can be used to display code in a readonly state.
|
|
@@ -42,7 +56,7 @@ export const ReadonlyCode = memo(
|
|
|
42
56
|
showHideCode?: boolean;
|
|
43
57
|
showCopyCode?: boolean;
|
|
44
58
|
insertNewCell?: boolean;
|
|
45
|
-
language?:
|
|
59
|
+
language?: LanguageAdapterType;
|
|
46
60
|
} & ReactCodeMirrorProps,
|
|
47
61
|
) => {
|
|
48
62
|
const { theme } = useTheme();
|
|
@@ -84,7 +98,7 @@ export const ReadonlyCode = memo(
|
|
|
84
98
|
theme={theme === "dark" ? "dark" : "light"}
|
|
85
99
|
height="100%"
|
|
86
100
|
editable={!hideCode}
|
|
87
|
-
extensions={language
|
|
101
|
+
extensions={readonlyCodeExtensions(language)}
|
|
88
102
|
value={code}
|
|
89
103
|
readOnly={true}
|
|
90
104
|
/>
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
/* oxlint-disable typescript/no-explicit-any */
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import type { OutputMessage } from "@/core/kernel/messages";
|
|
5
|
-
import {
|
|
6
|
-
groupCellsByColumn,
|
|
7
|
-
readonlyDisplay,
|
|
8
|
-
shouldHideCode,
|
|
9
|
-
} from "../vertical-layout";
|
|
5
|
+
import { groupCellsByColumn, shouldHideCode } from "../vertical-layout";
|
|
10
6
|
|
|
11
7
|
describe("groupCellsByColumn", () => {
|
|
12
8
|
it("should group cells by column and maintain order", () => {
|
|
@@ -175,25 +171,3 @@ describe("shouldHideCode", () => {
|
|
|
175
171
|
});
|
|
176
172
|
});
|
|
177
173
|
});
|
|
178
|
-
|
|
179
|
-
describe("readonlyDisplay", () => {
|
|
180
|
-
it("unwraps SQL cells to their inner query and marks them as sql", () => {
|
|
181
|
-
const result = readonlyDisplay('my_table = mo.sql("""SELECT 1 AS id""")');
|
|
182
|
-
expect(result.language).toBe("sql");
|
|
183
|
-
expect(result.code).toBe("SELECT 1 AS id");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("leaves plain Python cells untouched", () => {
|
|
187
|
-
const code = "x = 1 + 2";
|
|
188
|
-
const result = readonlyDisplay(code);
|
|
189
|
-
expect(result.language).toBe("python");
|
|
190
|
-
expect(result.code).toBe(code);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("leaves non-SQL cells (e.g. markdown) unchanged as python", () => {
|
|
194
|
-
const code = 'mo.md("""## Heading""")';
|
|
195
|
-
const result = readonlyDisplay(code);
|
|
196
|
-
expect(result.language).toBe("python");
|
|
197
|
-
expect(result.code).toBe(code);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
Loader2Icon,
|
|
11
11
|
MoreHorizontalIcon,
|
|
12
12
|
} from "lucide-react";
|
|
13
|
-
import { SQLParser } from "@marimo-team/smart-cells";
|
|
14
13
|
import type React from "react";
|
|
15
14
|
import { memo, useRef, useState } from "react";
|
|
16
15
|
import { z } from "zod";
|
|
@@ -30,6 +29,7 @@ import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
|
|
|
30
29
|
import type { CellId } from "@/core/cells/ids";
|
|
31
30
|
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
32
31
|
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
32
|
+
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
33
33
|
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
|
|
34
34
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
35
35
|
import { CSSClasses, KnownQueryParams } from "@/core/constants";
|
|
@@ -386,7 +386,7 @@ const VerticalCell = memo(
|
|
|
386
386
|
// Hide the code if it's pure markdown and there's an output, or if the code is empty
|
|
387
387
|
const hideCode = shouldHideCode(code, output);
|
|
388
388
|
// Only unwrap SQL when the code will actually be rendered.
|
|
389
|
-
const display = hideCode ? null :
|
|
389
|
+
const display = hideCode ? null : getReadonlyCodeDisplay(code);
|
|
390
390
|
|
|
391
391
|
return (
|
|
392
392
|
<div
|
|
@@ -500,20 +500,3 @@ export function shouldHideCode(code: string, output: OutputMessage | null) {
|
|
|
500
500
|
const hasOutput = output !== null && !isOutputEmpty(output);
|
|
501
501
|
return (isPureMarkdown && hasOutput) || code.trim() === "";
|
|
502
502
|
}
|
|
503
|
-
|
|
504
|
-
const sqlParser = new SQLParser();
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Unwrap SQL cells to their inner query so read-only views highlight them as
|
|
508
|
-
* SQL instead of showing the raw `mo.sql(...)` wrapper.
|
|
509
|
-
*/
|
|
510
|
-
export function readonlyDisplay(code: string): {
|
|
511
|
-
code: string;
|
|
512
|
-
language: "python" | "sql";
|
|
513
|
-
} {
|
|
514
|
-
const trimmed = code.trim();
|
|
515
|
-
if (sqlParser.isSupported(trimmed)) {
|
|
516
|
-
return { code: sqlParser.transformIn(trimmed).code, language: "sql" };
|
|
517
|
-
}
|
|
518
|
-
return { code, language: "python" };
|
|
519
|
-
}
|
|
@@ -26,7 +26,7 @@ import { connectionAtom } from "@/core/network/connection";
|
|
|
26
26
|
import { useTheme } from "@/theme/useTheme";
|
|
27
27
|
import { cn } from "@/utils/cn";
|
|
28
28
|
import { ReadonlyCode } from "../editor/code/readonly-python-code";
|
|
29
|
-
import {
|
|
29
|
+
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
30
30
|
|
|
31
31
|
type RuntimeCell = CellRuntimeState & CellData;
|
|
32
32
|
|
|
@@ -188,10 +188,7 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
|
|
|
188
188
|
const [userConfig] = useUserConfig();
|
|
189
189
|
const cellOutputPosition = userConfig.display.cell_output;
|
|
190
190
|
|
|
191
|
-
const
|
|
192
|
-
const adapter = languageAdapterFromCode(cell.code.trim());
|
|
193
|
-
return adapter.type === "sql" ? "sql" : "python";
|
|
194
|
-
}, [cell.code]);
|
|
191
|
+
const display = useMemo(() => getReadonlyCodeDisplay(cell.code), [cell.code]);
|
|
195
192
|
|
|
196
193
|
const output = (
|
|
197
194
|
<CellOutputSlide
|
|
@@ -203,7 +200,11 @@ export const SlideCellReadOnlyView = ({ cell }: { cell: RuntimeCell }) => {
|
|
|
203
200
|
|
|
204
201
|
const editor = (
|
|
205
202
|
<div className="marimo-cell">
|
|
206
|
-
<ReadonlyCode
|
|
203
|
+
<ReadonlyCode
|
|
204
|
+
code={display.code}
|
|
205
|
+
language={display.language}
|
|
206
|
+
showHideCode={false}
|
|
207
|
+
/>
|
|
207
208
|
</div>
|
|
208
209
|
);
|
|
209
210
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { getReadonlyCodeDisplay } from "../readonly-code-display";
|
|
5
|
+
|
|
6
|
+
describe("getReadonlyCodeDisplay", () => {
|
|
7
|
+
it("unwraps SQL cells to their inner query and marks them as sql", () => {
|
|
8
|
+
const result = getReadonlyCodeDisplay(
|
|
9
|
+
'my_table = mo.sql("""SELECT 1 AS id""")',
|
|
10
|
+
);
|
|
11
|
+
expect(result.language).toBe("sql");
|
|
12
|
+
expect(result.code).toBe("SELECT 1 AS id");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("unwraps f-string SQL cells", () => {
|
|
16
|
+
const result = getReadonlyCodeDisplay(
|
|
17
|
+
'my_table = mo.sql(f"""SELECT 1 AS id""")',
|
|
18
|
+
);
|
|
19
|
+
expect(result.language).toBe("sql");
|
|
20
|
+
expect(result.code).toBe("SELECT 1 AS id");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("leaves plain Python cells untouched", () => {
|
|
24
|
+
const code = "x = 1 + 2\nprint(x)";
|
|
25
|
+
const result = getReadonlyCodeDisplay(code);
|
|
26
|
+
expect(result.language).toBe("python");
|
|
27
|
+
expect(result.code).toBe(code);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("unwraps markdown cells to their inner content", () => {
|
|
31
|
+
const result = getReadonlyCodeDisplay('mo.md("""## Heading""")');
|
|
32
|
+
expect(result.language).toBe("markdown");
|
|
33
|
+
expect(result.code).toBe("## Heading");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("treats empty or whitespace-only code as python", () => {
|
|
37
|
+
expect(getReadonlyCodeDisplay("")).toEqual({
|
|
38
|
+
code: "",
|
|
39
|
+
language: "python",
|
|
40
|
+
});
|
|
41
|
+
expect(getReadonlyCodeDisplay(" \n\t ")).toEqual({
|
|
42
|
+
code: " \n\t ",
|
|
43
|
+
language: "python",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { MarkdownParser, SQLParser } from "@marimo-team/smart-cells";
|
|
4
|
+
import type { LanguageAdapterType } from "@/core/codemirror/language/types";
|
|
5
|
+
|
|
6
|
+
export interface ReadonlyCodeDisplay {
|
|
7
|
+
code: string;
|
|
8
|
+
language: LanguageAdapterType;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const markdownParser = new MarkdownParser();
|
|
12
|
+
const sqlParser = new SQLParser();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Unwrap SQL and markdown cells so read-only views show inner content with the
|
|
16
|
+
* correct syntax highlighting instead of the raw Python wrapper.
|
|
17
|
+
*/
|
|
18
|
+
export function getReadonlyCodeDisplay(code: string): ReadonlyCodeDisplay {
|
|
19
|
+
const trimmed = code.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return { code, language: "python" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (markdownParser.isSupported(trimmed)) {
|
|
25
|
+
return {
|
|
26
|
+
code: markdownParser.transformIn(trimmed).code,
|
|
27
|
+
language: "markdown",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (sqlParser.isSupported(trimmed)) {
|
|
32
|
+
return { code: sqlParser.transformIn(trimmed).code, language: "sql" };
|
|
33
|
+
}
|
|
34
|
+
return { code, language: "python" };
|
|
35
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { getSnippetDisplay } from "../snippet-display";
|
|
5
|
-
|
|
6
|
-
describe("getSnippetDisplay", () => {
|
|
7
|
-
it("shows sql cells as the sql query", () => {
|
|
8
|
-
const { language, value } = getSnippetDisplay(
|
|
9
|
-
'df = mo.sql("""SELECT * FROM users LIMIT 5""")',
|
|
10
|
-
);
|
|
11
|
-
expect(language).toBe("sql");
|
|
12
|
-
expect(value).toBe("SELECT * FROM users LIMIT 5");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("keeps plain python cells as python", () => {
|
|
16
|
-
const code = "x = 1 + 2\nprint(x)";
|
|
17
|
-
expect(getSnippetDisplay(code)).toEqual({
|
|
18
|
-
language: "python",
|
|
19
|
-
value: code,
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
});
|