@marimo-team/islands 0.18.2 → 0.18.4

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.
Files changed (43) hide show
  1. package/dist/{constants-DWBOe162.js → constants-D_G8vnDk.js} +5 -4
  2. package/dist/{formats-7RSCCoSI.js → formats-Bi_tbdwB.js} +21 -22
  3. package/dist/{glide-data-editor-D-Ia_Jsv.js → glide-data-editor-DXF8E-QD.js} +2 -2
  4. package/dist/main.js +280 -148
  5. package/dist/style.css +1 -1
  6. package/dist/{types-Dunk85GC.js → types-DclGb0Yh.js} +1 -1
  7. package/dist/{vega-component-kU4hFYYJ.js → vega-component-BFcH2SqR.js} +8 -8
  8. package/package.json +1 -1
  9. package/src/components/app-config/user-config-form.tsx +14 -1
  10. package/src/components/data-table/context-menu.tsx +7 -3
  11. package/src/components/data-table/filter-pills.tsx +2 -1
  12. package/src/components/data-table/filters.ts +11 -2
  13. package/src/components/editor/cell/CreateCellButton.tsx +5 -3
  14. package/src/components/editor/cell/collapse.tsx +2 -2
  15. package/src/components/editor/chrome/components/contribute-snippet-button.tsx +22 -103
  16. package/src/components/editor/controls/duplicate-shortcut-banner.tsx +50 -0
  17. package/src/components/editor/controls/keyboard-shortcuts.tsx +25 -2
  18. package/src/components/editor/notebook-banner.tsx +1 -1
  19. package/src/components/editor/notebook-cell.tsx +4 -3
  20. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +6 -6
  21. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +3 -3
  22. package/src/components/pages/home-page.tsx +6 -0
  23. package/src/components/scratchpad/scratchpad.tsx +2 -1
  24. package/src/core/constants.ts +10 -0
  25. package/src/core/layout/useTogglePresenting.ts +69 -25
  26. package/src/core/state/__mocks__/mocks.ts +1 -0
  27. package/src/hooks/__tests__/useDuplicateShortcuts.test.ts +449 -0
  28. package/src/hooks/useDuplicateShortcuts.ts +145 -0
  29. package/src/plugins/impl/NumberPlugin.tsx +1 -1
  30. package/src/plugins/impl/__tests__/NumberPlugin.test.tsx +1 -1
  31. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +67 -47
  32. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +2 -57
  33. package/src/plugins/impl/anywidget/__tests__/model.test.ts +23 -19
  34. package/src/plugins/impl/anywidget/model.ts +68 -41
  35. package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +2 -0
  36. package/src/plugins/impl/data-frames/utils/operators.ts +1 -0
  37. package/src/plugins/impl/vega/vega.css +5 -0
  38. package/src/plugins/layout/NavigationMenuPlugin.tsx +24 -22
  39. package/src/plugins/layout/StatPlugin.tsx +43 -23
  40. package/src/utils/__tests__/data-views.test.ts +495 -13
  41. package/src/utils/__tests__/json-parser.test.ts +1 -1
  42. package/src/utils/data-views.ts +134 -16
  43. package/src/utils/json/base64.ts +8 -0
@@ -15,7 +15,7 @@ import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
15
15
  import { t as require_react_dom } from "./react-dom-BZdwbVNI.js";
16
16
  import { m as useEvent_default } from "./useTheme-ByTGDerd.js";
17
17
  import { t as toString_default } from "./toString-DBXBHXIe.js";
18
- import { r as debounce_default, t as Constants } from "./constants-DWBOe162.js";
18
+ import { i as debounce_default, n as Constants } from "./constants-D_G8vnDk.js";
19
19
  import { t as memoizeLastValue } from "./once-DjP4Kbhy.js";
20
20
  var ChevronRight = createLucideIcon("chevron-right", [["path", {
21
21
  d: "m9 18 6-6-6-6",
@@ -2,7 +2,7 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-BSzAiXXz.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-CNX0xYDF.js";
4
4
  import "./Combination-DnWHe36P.js";
5
- import { C as CircleQuestionMark, h as asRemoteURL, i as Alert, n as useDeepCompareMemoize, o as AlertTitle, s as isValid, t as arrow } from "./formats-7RSCCoSI.js";
5
+ import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as useDeepCompareMemoize, o as isValid, r as Alert, t as arrow } from "./formats-Bi_tbdwB.js";
6
6
  import "./clsx-D2KVTYnW.js";
7
7
  import { l as Events } from "./button-XnD6ylpt.js";
8
8
  import { o as Objects, s as Logger } from "./hotkeys-CwkyZ6ZF.js";
@@ -18,7 +18,7 @@ import "./_baseUniq-CPOFUArp.js";
18
18
  import "./_baseIsEqual-CwglS7T6.js";
19
19
  import "./merge-DPdZQMPt.js";
20
20
  import "./now-BA1FgVte.js";
21
- import { r as debounce_default } from "./constants-DWBOe162.js";
21
+ import { i as debounce_default } from "./constants-D_G8vnDk.js";
22
22
  import { a as tooltipHandler, n as vegaLoadData } from "./loader-ZngagiXO.js";
23
23
  import { t as uniq_default } from "./uniq-CCIhWjDg.js";
24
24
  import "./zod-Bvx56F8M.js";
@@ -509,8 +509,8 @@ async function resolveVegaSpecData(e) {
509
509
  } catch {
510
510
  return e2;
511
511
  }
512
- let O = await vegaLoadData(E2.href, e2.data.format);
513
- return w[E2.pathname] = O, {
512
+ let D = await vegaLoadData(E2.href, e2.data.format);
513
+ return w[E2.pathname] = D, {
514
514
  ...e2,
515
515
  data: { name: E2.pathname }
516
516
  };
@@ -540,17 +540,17 @@ var VegaComponent = (e) => {
540
540
  spec: P,
541
541
  embedOptions: A
542
542
  }), w[5] = D, w[6] = A, w[7] = O, w[8] = P, w[9] = E, w[10] = T, w[11] = I) : I = w[11], I;
543
- }, LoadedVegaComponent = ({ value: e, setValue: w, chartSelection: T, fieldSelection: D, spec: M, embedOptions: N }) => {
543
+ }, LoadedVegaComponent = ({ value: e, setValue: w, chartSelection: T, fieldSelection: O, spec: A, embedOptions: N }) => {
544
544
  let { theme: L } = useTheme(), R = (0, import_react.useRef)(null), z = (0, import_react.useRef)(void 0), [B, V] = (0, import_react.useState)(), H = (0, import_react.useMemo)(() => N && "actions" in N ? N.actions : {
545
545
  source: false,
546
546
  compiled: false
547
- }, [N]), U = useDeepCompareMemoize(M), W = (0, import_react.useMemo)(() => makeSelectable(fixRelativeUrl(U), {
547
+ }, [N]), U = useDeepCompareMemoize(A), W = (0, import_react.useMemo)(() => makeSelectable(fixRelativeUrl(U), {
548
548
  chartSelection: T,
549
- fieldSelection: D
549
+ fieldSelection: O
550
550
  }), [
551
551
  U,
552
552
  T,
553
- D
553
+ O
554
554
  ]), G = (0, import_react.useMemo)(() => getSelectionParamNames(W), [W]), K = useEvent_default((T2) => {
555
555
  w({
556
556
  ...e,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -41,6 +41,7 @@ import {
41
41
  } from "@/core/config/config-schema";
42
42
  import { getAppWidths } from "@/core/config/widths";
43
43
  import { marimoVersionAtom } from "@/core/meta/state";
44
+ import { viewStateAtom } from "@/core/mode";
44
45
  import { useRequestClient } from "@/core/network/requests";
45
46
  import { isWasm } from "@/core/wasm/utils";
46
47
  import { useDebouncedCallback } from "@/hooks/useDebounce";
@@ -119,7 +120,19 @@ export const UserConfigForm: React.FC = () => {
119
120
  const [activeCategory, setActiveCategory] = useAtom(
120
121
  activeUserConfigCategoryAtom,
121
122
  );
122
- const capabilities = useAtomValue(capabilitiesAtom);
123
+
124
+ let capabilities = useAtomValue(capabilitiesAtom);
125
+ const isHome = useAtomValue(viewStateAtom).mode === "home";
126
+ // The home page does not fetch kernel capabilities, so we just turn them all on
127
+ if (isHome) {
128
+ capabilities = {
129
+ terminal: true,
130
+ pylsp: true,
131
+ ty: true,
132
+ basedpyright: true,
133
+ };
134
+ }
135
+
123
136
  const marimoVersion = useAtomValue(marimoVersionAtom);
124
137
  const { locale } = useLocale();
125
138
  const { saveUserConfig } = useRequestClient();
@@ -95,11 +95,11 @@ export const CellContextMenu = <TData,>({
95
95
  const column = cell.column;
96
96
  const canFilter = column.getCanFilter() && column.columnDef.meta?.filterType;
97
97
 
98
- const handleFilterCell = () => {
98
+ const handleFilterCell = (operator: "in" | "not_in") => {
99
99
  column.setFilterValue(
100
100
  Filter.select({
101
101
  options: [cell.getValue()],
102
- operator: "in",
102
+ operator,
103
103
  }),
104
104
  );
105
105
  };
@@ -119,10 +119,14 @@ export const CellContextMenu = <TData,>({
119
119
  {canFilter && (
120
120
  <>
121
121
  <ContextMenuSeparator />
122
- <ContextMenuItem onClick={handleFilterCell}>
122
+ <ContextMenuItem onClick={() => handleFilterCell("in")}>
123
123
  <FilterIcon className="mo-dropdown-icon h-3 w-3" />
124
124
  Filter by this value
125
125
  </ContextMenuItem>
126
+ <ContextMenuItem onClick={() => handleFilterCell("not_in")}>
127
+ <FilterIcon className="mo-dropdown-icon h-3 w-3" />
128
+ Remove rows with this value
129
+ </ContextMenuItem>
126
130
  </>
127
131
  )}
128
132
  </ContextMenuContent>
@@ -96,7 +96,8 @@ function formatValue(value: ColumnFilterValue, timeFormatter: DateFormatter) {
96
96
  const stringifiedOptions = value.options.map((o) =>
97
97
  stringifyUnknownValue({ value: o }),
98
98
  );
99
- return `is in [${stringifiedOptions.join(", ")}]`;
99
+ const operator = value.operator === "in" ? "is in" : "not in";
100
+ return `${operator} [${stringifiedOptions.join(", ")}]`;
100
101
  }
101
102
  if (value.type === "text") {
102
103
  return `contains "${value.text}"`;
@@ -7,6 +7,7 @@ import type { ConditionType } from "@/plugins/impl/data-frames/schema";
7
7
  import type { ColumnId } from "@/plugins/impl/data-frames/types";
8
8
  import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
9
9
  import { assertNever } from "@/utils/assertNever";
10
+ import { Logger } from "@/utils/Logger";
10
11
 
11
12
  declare module "@tanstack/react-table" {
12
13
  //allows us to define custom properties for our columns
@@ -192,12 +193,20 @@ export function filterToFilterCondition(
192
193
  }
193
194
 
194
195
  return [];
195
- case "select":
196
+ case "select": {
197
+ let operator = filter.operator;
198
+ if (filter.operator !== "in" && filter.operator !== "not_in") {
199
+ Logger.warn("Invalid operator for select filter", {
200
+ operator: filter.operator,
201
+ });
202
+ operator = "in"; // default to in operator
203
+ }
196
204
  return {
197
205
  column_id: columnId,
198
- operator: "in",
206
+ operator,
199
207
  value: filter.options,
200
208
  };
209
+ }
201
210
 
202
211
  default:
203
212
  assertNever(filter);
@@ -46,7 +46,7 @@ export const CreateCellButton = ({
46
46
  <div>{baseTooltipContent}</div>
47
47
  <div className="text-xs text-muted-foreground font-medium pt-1 -mt-2 border-t border-border">
48
48
  {<MinimalHotkeys shortcut={shortcut} className="inline" />}{" "}
49
- <span>to auto insert a cell</span>
49
+ <span>for other cell types</span>
50
50
  </div>
51
51
  </div>
52
52
  );
@@ -81,7 +81,9 @@ export const CreateCellButton = ({
81
81
  };
82
82
 
83
83
  const handleButtonClick = (e: React.MouseEvent) => {
84
- if (oneClickShortcut === "shift" ? e.shiftKey : e.metaKey || e.ctrlKey) {
84
+ const hasModifier =
85
+ oneClickShortcut === "shift" ? e.shiftKey : e.metaKey || e.ctrlKey;
86
+ if (!hasModifier) {
85
87
  e.preventDefault();
86
88
  e.stopPropagation();
87
89
  addPythonCell();
@@ -123,7 +125,7 @@ export const CreateCellButton = ({
123
125
  >
124
126
  <Tooltip content={finalTooltipContent}>
125
127
  <PlusIcon
126
- strokeWidth={4}
128
+ strokeWidth={3}
127
129
  size={14}
128
130
  className="opacity-60 hover:opacity-90"
129
131
  />
@@ -44,9 +44,9 @@ export const CollapseToggle: React.FC<Props> = (props) => {
44
44
 
45
45
  const Arrow = ({ isCollapsed }: { isCollapsed: boolean }) => {
46
46
  return isCollapsed ? (
47
- <ChevronRightIcon className="w-5 h-5 shrink-0" />
47
+ <ChevronRightIcon className="w-5 h-5 shrink-0 opacity-60" strokeWidth={2} />
48
48
  ) : (
49
- <ChevronDownIcon className="w-5 h-5 shrink-0" />
49
+ <ChevronDownIcon className="w-5 h-5 shrink-0 opacity-60" strokeWidth={2} />
50
50
  );
51
51
  };
52
52
 
@@ -1,8 +1,7 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
- import { EditorView } from "@codemirror/view";
4
3
  import { Slot } from "@radix-ui/react-slot";
5
- import React, { type PropsWithChildren, useState } from "react";
4
+ import React, { type PropsWithChildren } from "react";
6
5
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
7
6
  import { Button } from "@/components/ui/button";
8
7
  import {
@@ -12,12 +11,7 @@ import {
12
11
  DialogHeader,
13
12
  DialogTitle,
14
13
  } from "@/components/ui/dialog";
15
- import { Input } from "@/components/ui/input";
16
- import { Textarea } from "@/components/ui/textarea";
17
- import { toast } from "@/components/ui/use-toast";
18
14
  import { Constants } from "@/core/constants";
19
- import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror";
20
- import { useTheme } from "@/theme/useTheme";
21
15
 
22
16
  export const ContributeSnippetButton: React.FC<PropsWithChildren> = ({
23
17
  children,
@@ -33,106 +27,31 @@ export const ContributeSnippetButton: React.FC<PropsWithChildren> = ({
33
27
  );
34
28
  };
35
29
 
36
- const extensions = [EditorView.lineWrapping];
37
-
38
30
  const ContributeSnippetModal: React.FC<{
39
31
  onClose: () => void;
40
32
  }> = ({ onClose }) => {
41
- const [code, setCode] = useState("");
42
- const { theme } = useTheme();
43
-
44
33
  return (
45
- <DialogContent className="w-fit">
46
- <form
47
- onSubmit={async (e) => {
48
- e.preventDefault();
49
-
50
- const formData = new FormData(e.target as HTMLFormElement);
51
- const title = formData.get("title");
52
- const description = formData.get("description");
53
- const code = formData.get("code");
54
-
55
- // Fire-and-forget we don't care about the response
56
- void fetch("https://marimo.io/api/suggest-snippet", {
57
- method: "POST",
58
- headers: {
59
- "Content-Type": "application/json",
60
- },
61
- body: JSON.stringify({
62
- title,
63
- description,
64
- code,
65
- }),
66
- });
67
- onClose();
68
- toast({
69
- title: "Snippet Submitted",
70
- description:
71
- "Thank you for contributing! We will review your snippet shortly.",
72
- });
73
- }}
74
- >
75
- <DialogHeader>
76
- <DialogTitle>Contribute a Snippet</DialogTitle>
77
- <DialogDescription>
78
- Have a useful snippet you want to share with the community? Submit
79
- it here or make a pull request{" "}
80
- <a
81
- href={Constants.githubPage}
82
- target="_blank"
83
- className="underline"
84
- >
85
- on GitHub
86
- </a>
87
- .
88
- </DialogDescription>
89
- </DialogHeader>
90
- <div className="flex flex-col gap-6 py-4">
91
- <Input
92
- id="title"
93
- name="title"
94
- autoFocus={true}
95
- placeholder="Title"
96
- required={true}
97
- autoComplete="off"
98
- />
99
- <Textarea
100
- id="description"
101
- name="description"
102
- autoFocus={true}
103
- placeholder="Description"
104
- rows={5}
105
- required={true}
106
- autoComplete="off"
107
- />
108
- <input type="hidden" name="code" value={code} />
109
- <LazyAnyLanguageCodeMirror
110
- theme={theme === "dark" ? "dark" : "light"}
111
- language="python"
112
- className="cm border rounded overflow-hidden"
113
- extensions={extensions}
114
- value={code}
115
- onChange={setCode}
116
- />
117
- </div>
118
- <DialogFooter>
119
- <Button
120
- data-testid="snippet-cancel-button"
121
- variant="secondary"
122
- onClick={onClose}
123
- >
124
- Cancel
125
- </Button>
126
- <Button
127
- data-testid="snippet-send-button"
128
- aria-label="Save"
129
- variant="default"
130
- type="submit"
131
- >
132
- Send
133
- </Button>
134
- </DialogFooter>
135
- </form>
34
+ <DialogContent className="max-w-md">
35
+ <DialogHeader>
36
+ <DialogTitle>Contribute a Snippet</DialogTitle>
37
+ <DialogDescription>
38
+ Have a useful snippet you want to share with the community? Make a
39
+ pull request{" "}
40
+ <a href={Constants.githubPage} target="_blank" className="underline">
41
+ on GitHub
42
+ </a>
43
+ .
44
+ </DialogDescription>
45
+ </DialogHeader>
46
+ <DialogFooter>
47
+ <Button
48
+ data-testid="snippet-close-button"
49
+ variant="default"
50
+ onClick={onClose}
51
+ >
52
+ Close
53
+ </Button>
54
+ </DialogFooter>
136
55
  </DialogContent>
137
56
  );
138
57
  };
@@ -0,0 +1,50 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { AlertTriangleIcon } from "lucide-react";
4
+ import { KeyboardHotkeys } from "@/components/shortcuts/renderShortcut";
5
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6
+ import type { DuplicateGroup } from "@/hooks/useDuplicateShortcuts";
7
+
8
+ interface DuplicateShortcutBannerProps {
9
+ duplicates: DuplicateGroup[];
10
+ }
11
+
12
+ /**
13
+ * Banner component that warns about duplicate keyboard shortcuts.
14
+ * Displays a warning when multiple actions share the same key binding.
15
+ */
16
+ export const DuplicateShortcutBanner: React.FC<
17
+ DuplicateShortcutBannerProps
18
+ > = ({ duplicates }) => {
19
+ // Don't render if no duplicates
20
+ if (duplicates.length === 0) {
21
+ return null;
22
+ }
23
+
24
+ return (
25
+ <Alert variant="warning" className="mb-4">
26
+ <AlertTriangleIcon className="h-4 w-4" />
27
+ <AlertTitle>Duplicate shortcuts</AlertTitle>
28
+ <AlertDescription>
29
+ <p className="mb-2">
30
+ Multiple actions are assigned to the same keyboard shortcut:
31
+ </p>
32
+ <ul className="space-y-2">
33
+ {duplicates.map(({ key, actions }) => (
34
+ <li key={key} className="text-xs">
35
+ <div className="flex items-center gap-2 mb-1">
36
+ <KeyboardHotkeys shortcut={key} />
37
+ <span className="font-semibold">is used by:</span>
38
+ </div>
39
+ <ul className="ml-6 list-disc">
40
+ {actions.map(({ action, name }) => (
41
+ <li key={action}>{name}</li>
42
+ ))}
43
+ </ul>
44
+ </li>
45
+ ))}
46
+ </ul>
47
+ </AlertDescription>
48
+ </Alert>
49
+ );
50
+ };
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
3
  import { atom, useAtom, useAtomValue } from "jotai";
4
- import { EditIcon, XIcon } from "lucide-react";
4
+ import { AlertTriangleIcon, EditIcon, XIcon } from "lucide-react";
5
5
  import { useState } from "react";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Input } from "@/components/ui/input";
@@ -15,6 +15,7 @@ import {
15
15
  } from "@/core/hotkeys/hotkeys";
16
16
  import { isPlatformMac } from "@/core/hotkeys/shortcuts";
17
17
  import { useRequestClient } from "@/core/network/requests";
18
+ import { useDuplicateShortcuts } from "../../../hooks/useDuplicateShortcuts";
18
19
  import { useHotkey } from "../../../hooks/useHotkey";
19
20
  import { KeyboardHotkeys } from "../../shortcuts/renderShortcut";
20
21
  import {
@@ -25,6 +26,7 @@ import {
25
26
  DialogPortal,
26
27
  DialogTitle,
27
28
  } from "../../ui/dialog";
29
+ import { DuplicateShortcutBanner } from "./duplicate-shortcut-banner";
28
30
 
29
31
  export const keyboardShortcutsAtom = atom(false);
30
32
 
@@ -37,6 +39,10 @@ export const KeyboardShortcuts: React.FC = () => {
37
39
  const [config, setConfig] = useResolvedMarimoConfig();
38
40
  const hotkeys = useAtomValue(hotkeysAtom);
39
41
  const { saveUserConfig } = useRequestClient();
42
+ const { duplicates, hasDuplicate, getDuplicatesFor } = useDuplicateShortcuts(
43
+ hotkeys,
44
+ "Markdown",
45
+ );
40
46
 
41
47
  useHotkey("global.showHelp", () => setIsOpen((v) => !v));
42
48
 
@@ -214,6 +220,9 @@ export const KeyboardShortcuts: React.FC = () => {
214
220
  );
215
221
  }
216
222
 
223
+ const isDuplicate = hasDuplicate(action);
224
+ const duplicateActions = isDuplicate ? getDuplicatesFor(action) : [];
225
+
217
226
  return (
218
227
  <div
219
228
  key={action}
@@ -231,7 +240,20 @@ export const KeyboardShortcuts: React.FC = () => {
231
240
  <div className="w-3 h-3" />
232
241
  )}
233
242
  <KeyboardHotkeys className="justify-end" shortcut={hotkey.key} />
234
- <span>{hotkey.name.toLowerCase()}</span>
243
+ <div className="flex items-center gap-1">
244
+ <span>{hotkey.name.toLowerCase()}</span>
245
+ {isDuplicate && (
246
+ <div className="group relative inline-flex">
247
+ <AlertTriangleIcon className="w-3 h-3 text-(--yellow-11)" />
248
+ <div className="invisible group-hover:visible absolute left-0 top-5 z-10 w-max max-w-xs rounded-md bg-(--yellow-2) border border-(--yellow-7) p-2 text-xs text-(--yellow-11) shadow-md">
249
+ Also used by:{" "}
250
+ {duplicateActions
251
+ .map((a) => hotkeys.getHotkey(a).name.toLowerCase())
252
+ .join(", ")}
253
+ </div>
254
+ </div>
255
+ )}
256
+ </div>
235
257
  </div>
236
258
  );
237
259
  };
@@ -279,6 +301,7 @@ export const KeyboardShortcuts: React.FC = () => {
279
301
  <DialogHeader>
280
302
  <DialogTitle>Shortcuts</DialogTitle>
281
303
  </DialogHeader>
304
+ <DuplicateShortcutBanner duplicates={duplicates} />
282
305
  <div className="flex flex-row gap-3">
283
306
  <div className="w-1/2">
284
307
  {renderGroup("Editing")}
@@ -26,7 +26,7 @@ export const NotebookBanner: React.FC<Props> = ({ width }) => {
26
26
  <div
27
27
  className={cn(
28
28
  "flex flex-col gap-4 mb-5 print:hidden",
29
- width === "columns" && "sticky left-12 w-full max-w-[80vw]",
29
+ width === "columns" && "w-full max-w-[80vw]",
30
30
  )}
31
31
  >
32
32
  {banners.map((banner) => (
@@ -29,6 +29,7 @@ import { outputIsLoading, outputIsStale } from "@/core/cells/cell";
29
29
  import { isOutputEmpty } from "@/core/cells/outputs";
30
30
  import { autocompletionKeymap } from "@/core/codemirror/cm";
31
31
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
32
+ import { CSSClasses } from "@/core/constants";
32
33
  import { canCollapseOutline } from "@/core/dom/outline";
33
34
  import { isErrorMime } from "@/core/mime";
34
35
  import type { AppMode } from "@/core/mode";
@@ -349,7 +350,7 @@ const ReadonlyCellComponent = forwardRef(
349
350
  <OutputArea
350
351
  allowExpand={false}
351
352
  forceExpand={true}
352
- className="output-area"
353
+ className={CSSClasses.outputArea}
353
354
  cellId={cellId}
354
355
  output={cellRuntime.output}
355
356
  stale={outputIsStale(cellRuntime, cellData.edited)}
@@ -508,7 +509,7 @@ const EditableCellComponent = ({
508
509
  allowExpand={true}
509
510
  // Force expand when markdown is hidden
510
511
  forceExpand={isMarkdownCodeHidden}
511
- className="output-area"
512
+ className={CSSClasses.outputArea}
512
513
  cellId={cellId}
513
514
  output={cellRuntime.output}
514
515
  stale={isStaleCell}
@@ -1168,7 +1169,7 @@ const SetupCellComponent = ({
1168
1169
  <OutputArea
1169
1170
  allowExpand={true}
1170
1171
  forceExpand={true}
1171
- className="output-area"
1172
+ className={CSSClasses.outputArea}
1172
1173
  cellId={cellId}
1173
1174
  output={cellRuntime.output}
1174
1175
  stale={false}
@@ -534,13 +534,13 @@ describe("AnsiReducer streaming with append()", () => {
534
534
  describe("AnsiReducer color preservation", () => {
535
535
  const CASES = [
536
536
  // SGR sequences
537
- "\u001b[34mBlue text\u001b[m normal text\u001b[31mRed text\u001b[0m",
537
+ "\u001B[34mBlue text\u001B[m normal text\u001B[31mRed text\u001B[0m",
538
538
  // Complex SGR with parameters
539
- "\u001b[1;31mBold Red\u001b[0m \u001b[48;5;240mGray bg\u001b[0m",
539
+ "\u001B[1;31mBold Red\u001B[0m \u001B[48;5;240mGray bg\u001B[0m",
540
540
  // Character set selection
541
- "Text\u001b(BMore\u001b(0Graphics\u001b(B",
541
+ "Text\u001B(BMore\u001B(0Graphics\u001B(B",
542
542
  // Complex case
543
- "\u001b[34m[D 251201 15:32:24 cell_runner:695]\u001b(B\u001b[m Running post_execution hooks in context\n\u001b[34m[D 251201 15:32:24 hooks_post_execution:65]\u001b(B\u001b[m Acquiring graph lock to update cell import workspace\n\u001b[34m[D 251201 15:32:24 hooks_post_execution:67]\u001b(B\u001b[m Acquired graph lock to update import workspace.\n",
543
+ "\u001B[34m[D 251201 15:32:24 cell_runner:695]\u001B(B\u001B[m Running post_execution hooks in context\n\u001B[34m[D 251201 15:32:24 hooks_post_execution:65]\u001B(B\u001B[m Acquiring graph lock to update cell import workspace\n\u001B[34m[D 251201 15:32:24 hooks_post_execution:67]\u001B(B\u001B[m Acquired graph lock to update import workspace.\n",
544
544
  ];
545
545
 
546
546
  test.each(CASES)("preserves ANSI color codes", (input) => {
@@ -554,12 +554,12 @@ describe("AnsiReducer color preservation", () => {
554
554
  // Test that color codes work alongside cursor movements
555
555
  // Note: when cursor moves up, lines below are discarded (tqdm behavior)
556
556
  const result = reducer.reduce(
557
- "Line1\n\u001b[31mRed\u001b[0m\u001b[1A\u001b[32mGreen\u001b[0m",
557
+ "Line1\n\u001B[31mRed\u001B[0m\u001B[1A\u001B[32mGreen\u001B[0m",
558
558
  );
559
559
  // After moving up from row 1 to row 0, row 1 is discarded
560
560
  // Green is written at the end of row 0
561
561
  expect(result).toMatchInlineSnapshot(
562
- `"Line1 \u001b[32mGreen\u001b[0m"`,
562
+ `"Line1 \u001B[32mGreen\u001B[0m"`,
563
563
  );
564
564
  });
565
565
  });
@@ -29,7 +29,7 @@ import { isOutputEmpty } from "@/core/cells/outputs";
29
29
  import type { CellData, CellRuntimeState } from "@/core/cells/types";
30
30
  import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
31
31
  import { useResolvedMarimoConfig } from "@/core/config/config";
32
- import { KnownQueryParams } from "@/core/constants";
32
+ import { CSSClasses, KnownQueryParams } from "@/core/constants";
33
33
  import type { OutputMessage } from "@/core/kernel/messages";
34
34
  import { showCodeInRunModeAtom } from "@/core/meta/state";
35
35
  import { isErrorMime } from "@/core/mime";
@@ -339,7 +339,7 @@ const VerticalCell = memo(
339
339
  <OutputArea
340
340
  allowExpand={true}
341
341
  output={output}
342
- className="output-area"
342
+ className={CSSClasses.outputArea}
343
343
  cellId={cellId}
344
344
  stale={outputStale}
345
345
  loading={loading}
@@ -394,7 +394,7 @@ const VerticalCell = memo(
394
394
  <OutputArea
395
395
  allowExpand={mode === "edit"}
396
396
  output={output}
397
- className="output-area"
397
+ className={CSSClasses.outputArea}
398
398
  cellId={cellId}
399
399
  stale={outputStale}
400
400
  loading={loading}
@@ -169,6 +169,12 @@ const WorkspaceNotebooks: React.FC = () => {
169
169
  return (
170
170
  <WorkspaceRootContext value={workspace.root}>
171
171
  <div className="flex flex-col gap-2">
172
+ {workspace.hasMore && (
173
+ <Banner kind="warn" className="rounded p-4">
174
+ Showing first {workspace.fileCount} files. Your workspace has more
175
+ files.
176
+ </Banner>
177
+ )}
172
178
  <Header
173
179
  Icon={BookTextIcon}
174
180
  control={
@@ -17,6 +17,7 @@ import { HTMLCellId, SCRATCH_CELL_ID } from "@/core/cells/ids";
17
17
  import { DEFAULT_CELL_NAME } from "@/core/cells/names";
18
18
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
19
19
  import { useResolvedMarimoConfig } from "@/core/config/config";
20
+ import { CSSClasses } from "@/core/constants";
20
21
  import { useRequestClient } from "@/core/network/requests";
21
22
  import type { CellConfig } from "@/core/network/types";
22
23
  import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror";
@@ -149,7 +150,7 @@ export const ScratchPad: React.FC = () => {
149
150
  <OutputArea
150
151
  allowExpand={false}
151
152
  output={output}
152
- className="output-area"
153
+ className={CSSClasses.outputArea}
153
154
  cellId={cellId}
154
155
  stale={false}
155
156
  loading={false}
@@ -50,3 +50,13 @@ export const KnownQueryParams = {
50
50
  */
51
51
  showChrome: "show-chrome",
52
52
  };
53
+
54
+ /**
55
+ * CSS class names used throughout the application
56
+ */
57
+ export const CSSClasses = {
58
+ /**
59
+ * Class name for cell output areas
60
+ */
61
+ outputArea: "output-area",
62
+ };