@marimo-team/islands 0.23.10-dev3 → 0.23.10-dev30
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-CyV83R2m.js → ConnectedDataExplorerComponent-DmBropAy.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-DfdpyDv_.js → any-language-editor-DNmoSiWL.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-C1tL1pML.js → chat-ui-D6oraHT2.js} +76 -76
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{code-visibility-DfnO0DcH.js → code-visibility-wR7WSQ4c.js} +2166 -1292
- package/dist/{copy-BuQpJEzp.js → copy-UqRYxiOg.js} +33 -33
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-Cc0I3C9e.js} +1 -1
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-BgdxCfYu.js → extends-D_hDsj6R.js} +4 -4
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-BOmK9ETQ.js → glide-data-editor-Qhu8oCX-.js} +12 -12
- package/dist/{html-to-image-CNa5ok96.js → html-to-image-UEH5lFDZ.js} +2318 -2275
- package/dist/{input-_2sjvfne.js → input-CMYy4hzj.js} +187 -185
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +6941 -6913
- package/dist/{mermaid-lXOw5Py9.js → mermaid-zuLgJ8J8.js} +4 -4
- package/dist/{process-output-DKr4f1di.js → process-output-CyMLTogj.js} +3 -3
- package/dist/{reveal-component-UdMnCK5U.js → reveal-component-BjnkUAZ9.js} +697 -619
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-9h1ACS78.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +3 -3
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +154 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/data-table/hover-tooltip/__tests__/content.test.ts +60 -0
- package/src/components/data-table/hover-tooltip/content.ts +44 -0
- package/src/components/data-table/hover-tooltip/hover-tooltip.tsx +55 -0
- package/src/components/data-table/hover-tooltip/use-table-hover-tooltip.ts +159 -0
- package/src/components/data-table/renderers.tsx +27 -43
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/cell/cell-context-menu.tsx +15 -2
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +53 -0
- package/src/components/storage/storage-inspector.tsx +68 -48
- package/src/components/ui/__tests__/use-toast.test.ts +75 -0
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/use-toast.ts +33 -13
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -28
- package/src/core/cells/__tests__/cell.test.ts +29 -2
- package/src/core/cells/cell.ts +5 -1
- package/src/core/codemirror/go-to-definition/commands.ts +4 -3
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/network/__tests__/requests-static.test.ts +30 -0
- package/src/core/network/requests-static.ts +14 -10
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/layout/DownloadPlugin.tsx +1 -1
- package/src/utils/lazy.ts +6 -1
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-BfhQmZjp.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type { Cell, RowData, Table } from "@tanstack/react-table";
|
|
3
|
+
import {
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import useEvent from "react-use-event-hook";
|
|
12
|
+
import { computeCellTooltipContent } from "./content";
|
|
13
|
+
|
|
14
|
+
// Matches the default TooltipProvider delay (MarimoApp.tsx) for visual parity
|
|
15
|
+
// with the rest of the app's tooltips.
|
|
16
|
+
const TOOLTIP_DELAY_MS = 400;
|
|
17
|
+
|
|
18
|
+
export interface HoverTooltipState {
|
|
19
|
+
rect: { top: number; left: number; width: number; height: number };
|
|
20
|
+
content: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useTableHoverTooltip<TData extends RowData>({
|
|
24
|
+
table,
|
|
25
|
+
}: {
|
|
26
|
+
table: Table<TData>;
|
|
27
|
+
}) {
|
|
28
|
+
const hoverTemplate = table.getState().cellHoverTemplate || null;
|
|
29
|
+
const [tooltipState, setTooltipState] = useState<HoverTooltipState | null>(
|
|
30
|
+
null,
|
|
31
|
+
);
|
|
32
|
+
const timer = useRef<number | null>(null);
|
|
33
|
+
|
|
34
|
+
// Stable id linking the focused/hovered cell to the tooltip content for
|
|
35
|
+
// assistive tech (the radix trigger is an aria-hidden phantom anchor).
|
|
36
|
+
const tooltipContentId = useId();
|
|
37
|
+
const anchorCell = useRef<HTMLElement | null>(null);
|
|
38
|
+
// Focus fires for pointer interactions too; track pointer state so
|
|
39
|
+
// click/drag-select focus doesn't show a tooltip (keyboard focus still does).
|
|
40
|
+
const pointerDown = useRef(false);
|
|
41
|
+
|
|
42
|
+
const clearTimer = () => {
|
|
43
|
+
if (timer.current != null) {
|
|
44
|
+
clearTimeout(timer.current);
|
|
45
|
+
timer.current = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const hideTooltip = useEvent(() => {
|
|
50
|
+
clearTimer();
|
|
51
|
+
setTooltipState(null);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const showFor = (target: HTMLElement, content: ReactNode) => {
|
|
55
|
+
anchorCell.current = target;
|
|
56
|
+
const r = target.getBoundingClientRect();
|
|
57
|
+
setTooltipState({
|
|
58
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
|
|
59
|
+
content,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Point the real cell at the tooltip content while it is shown. Done in a
|
|
64
|
+
// layout effect (after commit) so React's re-render from `setTooltipState`
|
|
65
|
+
// can't clobber an imperatively set attribute; cleanup unlinks the previous
|
|
66
|
+
// cell.
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (!tooltipState) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const cell = anchorCell.current;
|
|
72
|
+
cell?.setAttribute("aria-describedby", tooltipContentId);
|
|
73
|
+
return () => cell?.removeAttribute("aria-describedby");
|
|
74
|
+
}, [tooltipState, tooltipContentId]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const onDown = () => {
|
|
78
|
+
pointerDown.current = true;
|
|
79
|
+
};
|
|
80
|
+
const onUp = () => {
|
|
81
|
+
pointerDown.current = false;
|
|
82
|
+
};
|
|
83
|
+
window.addEventListener("mousedown", onDown, { capture: true });
|
|
84
|
+
window.addEventListener("mouseup", onUp, { capture: true });
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener("mousedown", onDown, { capture: true });
|
|
87
|
+
window.removeEventListener("mouseup", onUp, { capture: true });
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const handleCellMouseOver = useEvent(
|
|
92
|
+
(e: React.MouseEvent, cell: Cell<TData, unknown>) => {
|
|
93
|
+
// Suppress while a mouse button is held (range-select drag).
|
|
94
|
+
if (e.buttons !== 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const target = e.currentTarget as HTMLElement;
|
|
98
|
+
const content = computeCellTooltipContent(cell, hoverTemplate);
|
|
99
|
+
if (content == null || content === "") {
|
|
100
|
+
hideTooltip();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
clearTimer();
|
|
104
|
+
timer.current = window.setTimeout(
|
|
105
|
+
() => showFor(target, content),
|
|
106
|
+
TOOLTIP_DELAY_MS,
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const handleCellMouseLeave = useEvent(() => hideTooltip());
|
|
112
|
+
|
|
113
|
+
// Keyboard parity: cells are tabIndex=0, native `title` showed on focus too.
|
|
114
|
+
const handleCellFocus = useEvent(
|
|
115
|
+
(e: React.FocusEvent, cell: Cell<TData, unknown>) => {
|
|
116
|
+
// Cancel any pending hover-show so a stale timer can't overwrite the
|
|
117
|
+
// focus-triggered tooltip after the delay.
|
|
118
|
+
clearTimer();
|
|
119
|
+
// Focus also fires for click/drag-select; only keyboard focus (no pointer
|
|
120
|
+
// held) should show the tooltip, mirroring the hover drag suppression.
|
|
121
|
+
if (pointerDown.current) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const content = computeCellTooltipContent(cell, hoverTemplate);
|
|
125
|
+
if (content == null || content === "") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
showFor(e.currentTarget as HTMLElement, content);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const handleCellBlur = useEvent(() => hideTooltip());
|
|
133
|
+
|
|
134
|
+
// The anchor rect is captured at hover time, so any scroll or resize leaves
|
|
135
|
+
// it stale; hide instead of tracking. Capture catches scrolls inside the
|
|
136
|
+
// table's own container too (scroll events don't bubble but do fire in
|
|
137
|
+
// capture).
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const opts = { passive: true, capture: true } as const;
|
|
140
|
+
window.addEventListener("scroll", hideTooltip, opts);
|
|
141
|
+
window.addEventListener("resize", hideTooltip);
|
|
142
|
+
return () => {
|
|
143
|
+
window.removeEventListener("scroll", hideTooltip, { capture: true });
|
|
144
|
+
window.removeEventListener("resize", hideTooltip);
|
|
145
|
+
};
|
|
146
|
+
}, [hideTooltip]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => clearTimer, []);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
tooltipState,
|
|
152
|
+
tooltipContentId,
|
|
153
|
+
hideTooltip,
|
|
154
|
+
handleCellMouseOver,
|
|
155
|
+
handleCellMouseLeave,
|
|
156
|
+
handleCellFocus,
|
|
157
|
+
handleCellBlur,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -24,11 +24,12 @@ import { cn } from "@/utils/cn";
|
|
|
24
24
|
import { getCellDomProps } from "./cell-utils";
|
|
25
25
|
import { COLUMN_WRAPPING_STYLES } from "./column-wrapping/feature";
|
|
26
26
|
import { DataTableContextMenu } from "./context-menu";
|
|
27
|
+
import { HoverTooltip } from "./hover-tooltip/hover-tooltip";
|
|
28
|
+
import { useTableHoverTooltip } from "./hover-tooltip/use-table-hover-tooltip";
|
|
27
29
|
import { CellRangeSelectionIndicator } from "./range-focus/cell-selection-indicator";
|
|
28
30
|
import { useCellRangeSelection } from "./range-focus/use-cell-range-selection";
|
|
29
31
|
import { useScrollIntoViewOnFocus } from "./range-focus/use-scroll-into-view";
|
|
30
32
|
import { AUTO_WIDTH_MAX_COLUMNS, TABLE_ROW_HEIGHT_PX } from "./types";
|
|
31
|
-
import { stringifyUnknownValue } from "./utils";
|
|
32
33
|
|
|
33
34
|
export function renderTableHeader<TData>(
|
|
34
35
|
table: Table<TData>,
|
|
@@ -135,24 +136,7 @@ export const DataTableBody = <TData,>({
|
|
|
135
136
|
contextMenuCell.current = cell;
|
|
136
137
|
});
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
template: string,
|
|
140
|
-
cells: Cell<TData, unknown>[],
|
|
141
|
-
): string {
|
|
142
|
-
const variableRegex = /{{(\w+)}}/g;
|
|
143
|
-
// Map column id -> stringified value
|
|
144
|
-
const idToValue = new Map<string, string>();
|
|
145
|
-
for (const c of cells) {
|
|
146
|
-
const v = c.getValue();
|
|
147
|
-
// Prefer empty string for nulls to keep tooltip clean
|
|
148
|
-
const s = stringifyUnknownValue({ value: v, nullAsEmptyString: true });
|
|
149
|
-
idToValue.set(c.column.id, s);
|
|
150
|
-
}
|
|
151
|
-
return template.replaceAll(variableRegex, (_substr, varName: string) => {
|
|
152
|
-
const val = idToValue.get(varName);
|
|
153
|
-
return val === undefined ? `{{${varName}}}` : val;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
139
|
+
const hoverTooltip = useTableHoverTooltip({ table });
|
|
156
140
|
|
|
157
141
|
const renderCells = (cells: Cell<TData, unknown>[]) => {
|
|
158
142
|
return cells.map((cell) => {
|
|
@@ -163,7 +147,6 @@ export const DataTableBody = <TData,>({
|
|
|
163
147
|
pinningstyle,
|
|
164
148
|
);
|
|
165
149
|
|
|
166
|
-
const title = cell.getHoverTitle?.() ?? undefined;
|
|
167
150
|
return (
|
|
168
151
|
<TableCell
|
|
169
152
|
tabIndex={0}
|
|
@@ -178,10 +161,18 @@ export const DataTableBody = <TData,>({
|
|
|
178
161
|
className,
|
|
179
162
|
)}
|
|
180
163
|
style={style}
|
|
181
|
-
|
|
182
|
-
|
|
164
|
+
onMouseDown={(e) => {
|
|
165
|
+
handleCellMouseDown(e, cell);
|
|
166
|
+
hoverTooltip.hideTooltip();
|
|
167
|
+
}}
|
|
183
168
|
onMouseUp={handleCellMouseUp}
|
|
184
|
-
onMouseOver={(e) =>
|
|
169
|
+
onMouseOver={(e) => {
|
|
170
|
+
handleCellMouseOver(e, cell);
|
|
171
|
+
hoverTooltip.handleCellMouseOver(e, cell);
|
|
172
|
+
}}
|
|
173
|
+
onMouseLeave={hoverTooltip.handleCellMouseLeave}
|
|
174
|
+
onFocus={(e) => hoverTooltip.handleCellFocus(e, cell)}
|
|
175
|
+
onBlur={hoverTooltip.handleCellBlur}
|
|
185
176
|
onContextMenu={() => handleContextMenu(cell)}
|
|
186
177
|
>
|
|
187
178
|
<CellRangeSelectionIndicator cellId={cell.id} />
|
|
@@ -200,8 +191,6 @@ export const DataTableBody = <TData,>({
|
|
|
200
191
|
}
|
|
201
192
|
};
|
|
202
193
|
|
|
203
|
-
const hoverTemplate = table.getState().cellHoverTemplate || null;
|
|
204
|
-
|
|
205
194
|
const renderRow = (row: Row<TData>) => {
|
|
206
195
|
// Only find the row index if the row viewer panel is open
|
|
207
196
|
const rowIndex = rowViewerPanelOpen
|
|
@@ -209,22 +198,10 @@ export const DataTableBody = <TData,>({
|
|
|
209
198
|
: undefined;
|
|
210
199
|
const isRowViewedInPanel = rowViewerPanelOpen && viewedRowIdx === rowIndex;
|
|
211
200
|
|
|
212
|
-
// Compute hover title once per row using all visible cells
|
|
213
|
-
let rowTitle: string | undefined;
|
|
214
|
-
if (hoverTemplate) {
|
|
215
|
-
const visibleCells = row.getVisibleCells?.() ?? [
|
|
216
|
-
...row.getLeftVisibleCells(),
|
|
217
|
-
...row.getCenterVisibleCells(),
|
|
218
|
-
...row.getRightVisibleCells(),
|
|
219
|
-
];
|
|
220
|
-
rowTitle = applyHoverTemplate(hoverTemplate, visibleCells);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
201
|
return (
|
|
224
202
|
<TableRow
|
|
225
203
|
key={row.id}
|
|
226
204
|
data-state={row.getIsSelected() && "selected"}
|
|
227
|
-
title={rowTitle}
|
|
228
205
|
// These classes ensure that empty rows (nulls) still render
|
|
229
206
|
className={cn(
|
|
230
207
|
"border-t h-6",
|
|
@@ -296,12 +273,19 @@ export const DataTableBody = <TData,>({
|
|
|
296
273
|
);
|
|
297
274
|
|
|
298
275
|
return (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
276
|
+
<>
|
|
277
|
+
<DataTableContextMenu
|
|
278
|
+
tableBody={tableBody}
|
|
279
|
+
contextMenuRef={contextMenuCell}
|
|
280
|
+
tableRef={tableRef}
|
|
281
|
+
copyAllCells={handleCopyAllCells}
|
|
282
|
+
/>
|
|
283
|
+
<HoverTooltip
|
|
284
|
+
state={hoverTooltip.tooltipState}
|
|
285
|
+
contentId={hoverTooltip.tooltipContentId}
|
|
286
|
+
onClose={hoverTooltip.hideTooltip}
|
|
287
|
+
/>
|
|
288
|
+
</>
|
|
305
289
|
);
|
|
306
290
|
};
|
|
307
291
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import type {
|
|
5
|
+
Database,
|
|
6
|
+
DatabaseSchema,
|
|
7
|
+
DataTable,
|
|
8
|
+
} from "@/core/kernel/messages";
|
|
9
|
+
import { filterEmptyDatabases } from "../datasources";
|
|
10
|
+
|
|
11
|
+
function makeTable(name: string): DataTable {
|
|
12
|
+
return {
|
|
13
|
+
name,
|
|
14
|
+
columns: [],
|
|
15
|
+
source: "memory",
|
|
16
|
+
source_type: "local",
|
|
17
|
+
type: "table",
|
|
18
|
+
engine: null,
|
|
19
|
+
indexes: null,
|
|
20
|
+
num_columns: null,
|
|
21
|
+
num_rows: null,
|
|
22
|
+
variable_name: null,
|
|
23
|
+
primary_keys: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeSchema(opts: {
|
|
28
|
+
name: string;
|
|
29
|
+
tables: DataTable[];
|
|
30
|
+
tables_resolved?: boolean;
|
|
31
|
+
}): DatabaseSchema {
|
|
32
|
+
return {
|
|
33
|
+
name: opts.name,
|
|
34
|
+
tables: opts.tables,
|
|
35
|
+
tables_resolved: opts.tables_resolved ?? true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeDatabase(
|
|
40
|
+
name: string,
|
|
41
|
+
schemas: DatabaseSchema[],
|
|
42
|
+
schemas_resolved = true,
|
|
43
|
+
): Database {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
dialect: "duckdb",
|
|
47
|
+
schemas,
|
|
48
|
+
schemas_resolved,
|
|
49
|
+
engine: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("filterEmptyDatabases", () => {
|
|
54
|
+
it("hides schemas whose tables are resolved and empty", () => {
|
|
55
|
+
const databases = [
|
|
56
|
+
makeDatabase("memory", [
|
|
57
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
58
|
+
makeSchema({ name: "empty_schema", tables: [] }),
|
|
59
|
+
]),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
63
|
+
makeDatabase("memory", [
|
|
64
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
65
|
+
]),
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("preserves databases whose schemas have not been resolved yet (lazy state)", () => {
|
|
70
|
+
const databases = [
|
|
71
|
+
makeDatabase("not_loaded_yet", [], /* schemas_resolved */ false),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
75
|
+
makeDatabase("not_loaded_yet", [], false),
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("hides databases that have been resolved as empty", () => {
|
|
80
|
+
const databases = [
|
|
81
|
+
makeDatabase("really_empty", [], /* schemas_resolved */ true),
|
|
82
|
+
makeDatabase("has_tables", [
|
|
83
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
84
|
+
]),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
88
|
+
makeDatabase("has_tables", [
|
|
89
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
90
|
+
]),
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("hides databases whose schemas all filtered to empty", () => {
|
|
95
|
+
const databases = [
|
|
96
|
+
makeDatabase("only_empty", [
|
|
97
|
+
makeSchema({ name: "a", tables: [] }),
|
|
98
|
+
makeSchema({ name: "b", tables: [] }),
|
|
99
|
+
]),
|
|
100
|
+
makeDatabase("has_tables", [
|
|
101
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
102
|
+
]),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
106
|
+
makeDatabase("has_tables", [
|
|
107
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
108
|
+
]),
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("treats missing schemas_resolved as resolved (backward compatible)", () => {
|
|
113
|
+
const databases = [
|
|
114
|
+
{ name: "memory", dialect: "duckdb", schemas: [], engine: null },
|
|
115
|
+
] as Database[];
|
|
116
|
+
|
|
117
|
+
expect(filterEmptyDatabases(databases)).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("preserves schemas whose tables have not been resolved yet", () => {
|
|
121
|
+
const databases = [
|
|
122
|
+
makeDatabase("snowflake_db", [
|
|
123
|
+
// include_tables=False was used; the schema is not actually empty,
|
|
124
|
+
// tables will be fetched lazily on expand.
|
|
125
|
+
makeSchema({ name: "public", tables: [], tables_resolved: false }),
|
|
126
|
+
makeSchema({ name: "audit", tables: [], tables_resolved: false }),
|
|
127
|
+
makeSchema({
|
|
128
|
+
name: "really_empty",
|
|
129
|
+
tables: [],
|
|
130
|
+
tables_resolved: true,
|
|
131
|
+
}),
|
|
132
|
+
]),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
136
|
+
makeDatabase("snowflake_db", [
|
|
137
|
+
makeSchema({ name: "public", tables: [], tables_resolved: false }),
|
|
138
|
+
makeSchema({ name: "audit", tables: [], tables_resolved: false }),
|
|
139
|
+
]),
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("treats missing tables_resolved as resolved (backward compatible)", () => {
|
|
144
|
+
// Older payloads predating the new flag may omit it; default semantics
|
|
145
|
+
// treat the schema as resolved/authoritative.
|
|
146
|
+
const databases = [
|
|
147
|
+
makeDatabase("memory", [
|
|
148
|
+
{ name: "main", tables: [makeTable("t1")] },
|
|
149
|
+
{ name: "empty_schema", tables: [] },
|
|
150
|
+
] as DatabaseSchema[]),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
154
|
+
makeDatabase("memory", [
|
|
155
|
+
{ name: "main", tables: [makeTable("t1")] },
|
|
156
|
+
] as DatabaseSchema[]),
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns the same reference when nothing was filtered", () => {
|
|
161
|
+
const databases = [
|
|
162
|
+
makeDatabase("memory", [
|
|
163
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
164
|
+
]),
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
expect(filterEmptyDatabases(databases)).toBe(databases);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("does not mutate the input", () => {
|
|
171
|
+
const databases = [
|
|
172
|
+
makeDatabase("memory", [
|
|
173
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
174
|
+
makeSchema({ name: "empty_schema", tables: [] }),
|
|
175
|
+
]),
|
|
176
|
+
];
|
|
177
|
+
const snapshot = JSON.parse(JSON.stringify(databases));
|
|
178
|
+
|
|
179
|
+
filterEmptyDatabases(databases);
|
|
180
|
+
|
|
181
|
+
expect(databases).toEqual(snapshot);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { CommandList } from "cmdk";
|
|
4
|
-
import { atom, useAtomValue, useSetAtom } from "jotai";
|
|
4
|
+
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
5
|
+
import { atomWithStorage } from "jotai/utils";
|
|
5
6
|
import { PlusIcon, PlusSquareIcon, XIcon } from "lucide-react";
|
|
6
7
|
import React from "react";
|
|
7
8
|
import { dbDisplayName } from "@/components/databases/display";
|
|
8
9
|
import { EngineVariable } from "@/components/databases/engine-variable";
|
|
9
10
|
import { DatabaseLogo } from "@/components/databases/icon";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
RefreshIconButton,
|
|
13
|
+
VisibilityToggleButton,
|
|
14
|
+
} from "@/components/editor/file-tree/tree-actions";
|
|
11
15
|
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
12
16
|
import { Button } from "@/components/ui/button";
|
|
13
17
|
import { Command, CommandInput, CommandItem } from "@/components/ui/command";
|
|
@@ -52,6 +56,7 @@ import { sortBy } from "@/utils/arrays";
|
|
|
52
56
|
import { logNever } from "@/utils/assertNever";
|
|
53
57
|
import { cn } from "@/utils/cn";
|
|
54
58
|
import { Events } from "@/utils/events";
|
|
59
|
+
import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
55
60
|
import {
|
|
56
61
|
DatabaseIcon,
|
|
57
62
|
SchemaIcon,
|
|
@@ -116,6 +121,63 @@ const sortedTablesAtom = atom((get) => {
|
|
|
116
121
|
});
|
|
117
122
|
});
|
|
118
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Whether to hide empty schemas and databases (those with no tables) in the
|
|
126
|
+
* datasources panel.
|
|
127
|
+
*/
|
|
128
|
+
export const hideEmptyDatasourcesAtom = atomWithStorage<boolean>(
|
|
129
|
+
"marimo:datasources:hideEmpty",
|
|
130
|
+
false,
|
|
131
|
+
jotaiJsonStorage,
|
|
132
|
+
{ getOnInit: true },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
function isKnownEmptySchema(schema: DatabaseSchema): boolean {
|
|
136
|
+
return schema.tables_resolved !== false && schema.tables.length === 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply the "hide empty" filter to a connection's databases.
|
|
141
|
+
*
|
|
142
|
+
* - Schemas with confirmed-empty table lists are hidden.
|
|
143
|
+
* - Databases are hidden when either (a) their schemas have been enumerated
|
|
144
|
+
* and the list is empty, or (b) every schema in them was hidden by the
|
|
145
|
+
* schema-level filter.
|
|
146
|
+
* - Databases / schemas whose contents haven't been resolved yet (deferred
|
|
147
|
+
* discovery — `schemas_resolved === false` or `tables_resolved === false`)
|
|
148
|
+
* are preserved so the user can expand them to trigger a fetch.
|
|
149
|
+
*/
|
|
150
|
+
export function filterEmptyDatabases(databases: Database[]): Database[] {
|
|
151
|
+
let changed = false;
|
|
152
|
+
const result: Database[] = [];
|
|
153
|
+
for (const database of databases) {
|
|
154
|
+
// Known-empty database: schema list was enumerated and is empty.
|
|
155
|
+
if (database.schemas_resolved !== false && database.schemas.length === 0) {
|
|
156
|
+
changed = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Deferred schema discovery — keep so the user can expand and load.
|
|
160
|
+
if (database.schemas.length === 0) {
|
|
161
|
+
result.push(database);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const visibleSchemas = database.schemas.filter(
|
|
165
|
+
(schema) => !isKnownEmptySchema(schema),
|
|
166
|
+
);
|
|
167
|
+
if (visibleSchemas.length === 0) {
|
|
168
|
+
changed = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (visibleSchemas.length === database.schemas.length) {
|
|
172
|
+
result.push(database);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
changed = true;
|
|
176
|
+
result.push({ ...database, schemas: visibleSchemas });
|
|
177
|
+
}
|
|
178
|
+
return changed ? result : databases;
|
|
179
|
+
}
|
|
180
|
+
|
|
119
181
|
/**
|
|
120
182
|
* This atom is used to get the data connections that are available to the user.
|
|
121
183
|
* It filters out the internal engines if it has no databases or if it has only the in-memory database and no schemas.
|
|
@@ -152,10 +214,27 @@ export const connectionsAtom = atom((get) => {
|
|
|
152
214
|
|
|
153
215
|
export const DataSources: React.FC = () => {
|
|
154
216
|
const [searchValue, setSearchValue] = React.useState<string>("");
|
|
217
|
+
const [hideEmpty, setHideEmpty] = useAtom(hideEmptyDatasourcesAtom);
|
|
155
218
|
|
|
156
219
|
const closeAllColumns = useSetAtom(closeAllColumnsAtom);
|
|
157
220
|
const tables = useAtomValue(sortedTablesAtom);
|
|
158
|
-
const
|
|
221
|
+
const rawConnections = useAtomValue(connectionsAtom);
|
|
222
|
+
|
|
223
|
+
const dataConnections = React.useMemo(() => {
|
|
224
|
+
if (!hideEmpty) {
|
|
225
|
+
return rawConnections;
|
|
226
|
+
}
|
|
227
|
+
let changed = false;
|
|
228
|
+
const filtered = rawConnections.map((connection) => {
|
|
229
|
+
const databases = filterEmptyDatabases(connection.databases);
|
|
230
|
+
if (databases === connection.databases) {
|
|
231
|
+
return connection;
|
|
232
|
+
}
|
|
233
|
+
changed = true;
|
|
234
|
+
return { ...connection, databases };
|
|
235
|
+
});
|
|
236
|
+
return changed ? filtered : rawConnections;
|
|
237
|
+
}, [rawConnections, hideEmpty]);
|
|
159
238
|
|
|
160
239
|
if (tables.length === 0 && dataConnections.length === 0) {
|
|
161
240
|
return (
|
|
@@ -204,6 +283,16 @@ export const DataSources: React.FC = () => {
|
|
|
204
283
|
</button>
|
|
205
284
|
)}
|
|
206
285
|
|
|
286
|
+
<VisibilityToggleButton
|
|
287
|
+
data-testid="datasources-hide-empty-button"
|
|
288
|
+
isVisible={!hideEmpty}
|
|
289
|
+
onToggle={() => setHideEmpty(!hideEmpty)}
|
|
290
|
+
showTooltip="Show empty schemas and databases"
|
|
291
|
+
hideTooltip="Hide empty schemas and databases"
|
|
292
|
+
size="sm"
|
|
293
|
+
className="px-2 rounded-none focus-visible:outline-hidden"
|
|
294
|
+
/>
|
|
295
|
+
|
|
207
296
|
<AddConnectionDialog>
|
|
208
297
|
<Button
|
|
209
298
|
variant="ghost"
|
|
@@ -61,6 +61,7 @@ export const CellActionsContextMenu = ({
|
|
|
61
61
|
});
|
|
62
62
|
const [imageRightClicked, setImageRightClicked] =
|
|
63
63
|
React.useState<HTMLImageElement>();
|
|
64
|
+
const suppressCloseAutoFocus = React.useRef(false);
|
|
64
65
|
|
|
65
66
|
const DEFAULT_CONTEXT_MENU_ITEMS: ActionButton[] = [
|
|
66
67
|
{
|
|
@@ -166,7 +167,10 @@ export const CellActionsContextMenu = ({
|
|
|
166
167
|
handle: () => {
|
|
167
168
|
const editorView = getEditorView();
|
|
168
169
|
if (editorView) {
|
|
169
|
-
|
|
170
|
+
// Only suppress focus restoration when we actually navigated;
|
|
171
|
+
// otherwise let Radix return focus to the trigger cell.
|
|
172
|
+
suppressCloseAutoFocus.current =
|
|
173
|
+
goToDefinitionAtCursorPosition(editorView);
|
|
170
174
|
}
|
|
171
175
|
},
|
|
172
176
|
},
|
|
@@ -194,7 +198,16 @@ export const CellActionsContextMenu = ({
|
|
|
194
198
|
>
|
|
195
199
|
{children}
|
|
196
200
|
</ContextMenuTrigger>
|
|
197
|
-
<ContextMenuContent
|
|
201
|
+
<ContextMenuContent
|
|
202
|
+
className="w-[300px]"
|
|
203
|
+
scrollable={true}
|
|
204
|
+
onCloseAutoFocus={(evt) => {
|
|
205
|
+
if (suppressCloseAutoFocus.current) {
|
|
206
|
+
evt.preventDefault();
|
|
207
|
+
suppressCloseAutoFocus.current = false;
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
198
211
|
{visibleActions.map((group, i) => (
|
|
199
212
|
<Fragment key={i}>
|
|
200
213
|
{group.map((action) => {
|
|
@@ -17,6 +17,11 @@ interface LanguageTogglesProps {
|
|
|
17
17
|
code: string;
|
|
18
18
|
currentLanguageAdapter: LanguageAdapter["type"] | undefined;
|
|
19
19
|
onAfterToggle: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Classes for the wrapper element. Defaults to the absolutely-positioned,
|
|
22
|
+
* hover-revealed placement used inside the notebook cell editor.
|
|
23
|
+
*/
|
|
24
|
+
className?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
@@ -24,6 +29,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
24
29
|
code,
|
|
25
30
|
currentLanguageAdapter,
|
|
26
31
|
onAfterToggle,
|
|
32
|
+
className = "absolute right-3 top-2 z-20 flex hover-action gap-1",
|
|
27
33
|
}) => {
|
|
28
34
|
const canUseMarkdown = useMemo(
|
|
29
35
|
() => LanguageAdapters.markdown.isSupported(code) || code.trim() === "",
|
|
@@ -35,7 +41,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
35
41
|
);
|
|
36
42
|
|
|
37
43
|
return (
|
|
38
|
-
<div className=
|
|
44
|
+
<div className={className}>
|
|
39
45
|
<LanguageToggle
|
|
40
46
|
editorView={editorView}
|
|
41
47
|
currentLanguageAdapter={currentLanguageAdapter}
|