@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev41
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-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
- package/dist/{code-visibility-Dk8cVOny.js → code-visibility-COokQS5X.js} +946 -864
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
- package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
- package/dist/main.js +1297 -1201
- package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
- package/dist/{reveal-component-B5DXLyO7.js → reveal-component-D3ySPwH5.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/file-tree/download.ts +46 -0
- package/src/components/editor/file-tree/file-explorer.tsx +3 -21
- package/src/components/editor/file-tree/file-viewer.tsx +4 -27
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/components/ui/toast.tsx +11 -3
- package/src/components/ui/toaster.tsx +65 -9
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +6 -0
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/main.test.ts +176 -0
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +116 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +69 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/runtime/__tests__/runtime.test.ts +64 -0
- package/src/core/runtime/runtime.ts +30 -10
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type KeyboardEvent,
|
|
15
15
|
memo,
|
|
16
16
|
useCallback,
|
|
17
|
+
useEffect,
|
|
17
18
|
useImperativeHandle,
|
|
18
19
|
useMemo,
|
|
19
20
|
useRef,
|
|
@@ -54,6 +55,8 @@ import {
|
|
|
54
55
|
cellNeedsRun,
|
|
55
56
|
cellStatusClasses,
|
|
56
57
|
isUninstantiated,
|
|
58
|
+
publishedCellClasses,
|
|
59
|
+
shouldHidePublishedCell,
|
|
57
60
|
} from "../../core/cells/utils";
|
|
58
61
|
import type { UserConfig } from "../../core/config/config-schema";
|
|
59
62
|
import { isAppInteractionDisabled } from "../../core/websocket/connection-utils";
|
|
@@ -303,7 +306,9 @@ const CellComponent = (props: CellProps) => {
|
|
|
303
306
|
);
|
|
304
307
|
}
|
|
305
308
|
|
|
306
|
-
|
|
309
|
+
// Present mode is handled by EditableCellComponent with the edit chrome
|
|
310
|
+
// hidden, so that the output DOM is not remounted when toggling modes.
|
|
311
|
+
if (mode === "edit" || mode === "present") {
|
|
307
312
|
return (
|
|
308
313
|
<EditableCellComponent
|
|
309
314
|
{...props}
|
|
@@ -329,14 +334,14 @@ const ReadonlyCellComponent = forwardRef(
|
|
|
329
334
|
published: true,
|
|
330
335
|
});
|
|
331
336
|
|
|
332
|
-
const outputIsError = isErrorMime(cellRuntime.output?.mimetype);
|
|
333
|
-
|
|
334
337
|
// Hide the output if it's an error or stopped.
|
|
335
|
-
const hidden =
|
|
336
|
-
cellRuntime.errored
|
|
337
|
-
cellRuntime.interrupted
|
|
338
|
-
cellRuntime.stopped
|
|
339
|
-
|
|
338
|
+
const hidden = shouldHidePublishedCell({
|
|
339
|
+
errored: cellRuntime.errored,
|
|
340
|
+
interrupted: cellRuntime.interrupted,
|
|
341
|
+
stopped: cellRuntime.stopped,
|
|
342
|
+
output: cellRuntime.output,
|
|
343
|
+
showErrorTracebacks: false,
|
|
344
|
+
});
|
|
340
345
|
|
|
341
346
|
if (hidden) {
|
|
342
347
|
return null;
|
|
@@ -368,6 +373,7 @@ const EditableCellComponent = ({
|
|
|
368
373
|
theme,
|
|
369
374
|
showPlaceholder,
|
|
370
375
|
cellId,
|
|
376
|
+
mode,
|
|
371
377
|
canDelete,
|
|
372
378
|
userConfig,
|
|
373
379
|
isCollapsed,
|
|
@@ -415,6 +421,28 @@ const EditableCellComponent = ({
|
|
|
415
421
|
|
|
416
422
|
const loading = outputIsLoading(cellRuntime.status);
|
|
417
423
|
|
|
424
|
+
const isPresenting = mode === "present";
|
|
425
|
+
|
|
426
|
+
// While presenting, the cell may be hidden (matching the read view), but it
|
|
427
|
+
// stays mounted (CSS-hidden) so its DOM survives mode toggles.
|
|
428
|
+
const presentHidden =
|
|
429
|
+
isPresenting &&
|
|
430
|
+
shouldHidePublishedCell({
|
|
431
|
+
errored: cellRuntime.errored,
|
|
432
|
+
interrupted: cellRuntime.interrupted,
|
|
433
|
+
stopped: cellRuntime.stopped,
|
|
434
|
+
output: cellRuntime.output,
|
|
435
|
+
showErrorTracebacks: userConfig.runtime.show_tracebacks ?? false,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// CodeMirror cannot measure itself while the tray is display:none, so
|
|
439
|
+
// re-measure when returning to edit mode.
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (!isPresenting) {
|
|
442
|
+
editorView.current?.requestMeasure();
|
|
443
|
+
}
|
|
444
|
+
}, [isPresenting, editorView]);
|
|
445
|
+
|
|
418
446
|
// console output is cleared immediately on run, so check for queued instead
|
|
419
447
|
// of loading to determine staleness
|
|
420
448
|
const consoleOutputStale =
|
|
@@ -454,7 +482,12 @@ const EditableCellComponent = ({
|
|
|
454
482
|
});
|
|
455
483
|
const canCollapse = canCollapseOutline(cellRuntime.outline);
|
|
456
484
|
const hasOutput = !isOutputEmpty(cellRuntime.output);
|
|
457
|
-
|
|
485
|
+
// While presenting, pending edits do not dim the output as stale,
|
|
486
|
+
// matching the read view.
|
|
487
|
+
const isStaleCell = outputIsStale(
|
|
488
|
+
cellRuntime,
|
|
489
|
+
cellData.edited && !isPresenting,
|
|
490
|
+
);
|
|
458
491
|
const hasConsoleOutput = cellRuntime.consoleOutputs.length > 0;
|
|
459
492
|
const cellOutput = userConfig.display.cell_output;
|
|
460
493
|
|
|
@@ -487,7 +520,8 @@ const EditableCellComponent = ({
|
|
|
487
520
|
},
|
|
488
521
|
});
|
|
489
522
|
|
|
490
|
-
const emptyMarkdownPlaceholder =
|
|
523
|
+
const emptyMarkdownPlaceholder = !isPresenting &&
|
|
524
|
+
isMarkdownCodeHidden &&
|
|
491
525
|
isEmptyMarkdownContent &&
|
|
492
526
|
!needsRun && (
|
|
493
527
|
<div
|
|
@@ -509,25 +543,31 @@ const EditableCellComponent = ({
|
|
|
509
543
|
);
|
|
510
544
|
|
|
511
545
|
const outputArea = hasOutput && !isEmptyMarkdownContent && (
|
|
512
|
-
<div
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
546
|
+
<div
|
|
547
|
+
className="relative"
|
|
548
|
+
onDoubleClick={isPresenting ? undefined : showHiddenCodeIfMarkdown}
|
|
549
|
+
>
|
|
550
|
+
{!isPresenting && (
|
|
551
|
+
<div className="absolute top-5 -left-7 z-20 print:hidden">
|
|
552
|
+
<CollapseToggle
|
|
553
|
+
isCollapsed={isCollapsed}
|
|
554
|
+
onClick={() => {
|
|
555
|
+
if (isCollapsed) {
|
|
556
|
+
actions.expandCell({ cellId });
|
|
557
|
+
} else {
|
|
558
|
+
actions.collapseCell({ cellId });
|
|
559
|
+
}
|
|
560
|
+
}}
|
|
561
|
+
canCollapse={canCollapse}
|
|
562
|
+
/>
|
|
563
|
+
</div>
|
|
564
|
+
)}
|
|
526
565
|
<OutputArea
|
|
527
|
-
//
|
|
566
|
+
// `allowExpand` must stay constant across modes: flipping it swaps the
|
|
567
|
+
// output's container component and would remount the output DOM.
|
|
528
568
|
allowExpand={true}
|
|
529
|
-
// Force expand when markdown is hidden
|
|
530
|
-
forceExpand={isMarkdownCodeHidden}
|
|
569
|
+
// Force expand when markdown is hidden or when presenting
|
|
570
|
+
forceExpand={isMarkdownCodeHidden || isPresenting}
|
|
531
571
|
className={CSSClasses.outputArea}
|
|
532
572
|
cellId={cellId}
|
|
533
573
|
output={cellRuntime.output}
|
|
@@ -537,19 +577,32 @@ const EditableCellComponent = ({
|
|
|
537
577
|
</div>
|
|
538
578
|
);
|
|
539
579
|
|
|
540
|
-
const className = clsx(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
580
|
+
const className = clsx(
|
|
581
|
+
"marimo-cell",
|
|
582
|
+
"hover-actions-parent",
|
|
583
|
+
isPresenting
|
|
584
|
+
? // Match the published styling of the read view
|
|
585
|
+
publishedCellClasses({
|
|
586
|
+
errored: cellRuntime.errored,
|
|
587
|
+
stopped: cellRuntime.stopped,
|
|
588
|
+
})
|
|
589
|
+
: {
|
|
590
|
+
"z-10": true,
|
|
591
|
+
interactive: true,
|
|
592
|
+
...cellStatusClasses({
|
|
593
|
+
needsRun,
|
|
594
|
+
errored: cellRuntime.errored,
|
|
595
|
+
stopped: cellRuntime.stopped,
|
|
596
|
+
disabled: cellData.config.disabled,
|
|
597
|
+
status: cellRuntime.status,
|
|
598
|
+
}),
|
|
599
|
+
borderless:
|
|
600
|
+
isMarkdownCodeHidden &&
|
|
601
|
+
hasOutput &&
|
|
602
|
+
!navigationProps["data-selected"],
|
|
603
|
+
"pending-cut": isPendingCut,
|
|
604
|
+
},
|
|
605
|
+
);
|
|
553
606
|
|
|
554
607
|
const handleRefactorWithAI: OnRefactorWithAI = useEvent(
|
|
555
608
|
(opts: { prompt: string; triggerImmediately: boolean }) => {
|
|
@@ -578,7 +631,11 @@ const EditableCellComponent = ({
|
|
|
578
631
|
|
|
579
632
|
return (
|
|
580
633
|
<TooltipProvider>
|
|
581
|
-
<CellActionsContextMenu
|
|
634
|
+
<CellActionsContextMenu
|
|
635
|
+
cellId={cellId}
|
|
636
|
+
getEditorView={getEditorView}
|
|
637
|
+
disabled={isPresenting}
|
|
638
|
+
>
|
|
582
639
|
<SortableCell
|
|
583
640
|
tabIndex={-1}
|
|
584
641
|
ref={cellRef}
|
|
@@ -587,23 +644,34 @@ const EditableCellComponent = ({
|
|
|
587
644
|
onKeyDown={resumeCompletionHandler}
|
|
588
645
|
cellId={cellId}
|
|
589
646
|
canMoveX={canMoveX}
|
|
647
|
+
disabled={isPresenting}
|
|
648
|
+
hidden={presentHidden}
|
|
590
649
|
title={renderCellTitle()}
|
|
591
650
|
>
|
|
592
651
|
<div
|
|
593
652
|
tabIndex={-1}
|
|
594
|
-
{...navigationProps}
|
|
653
|
+
{...(isPresenting ? undefined : navigationProps)}
|
|
595
654
|
className={cn(
|
|
596
655
|
className,
|
|
597
|
-
navigationProps.className,
|
|
598
|
-
|
|
656
|
+
!isPresenting && navigationProps.className,
|
|
657
|
+
!isPresenting &&
|
|
658
|
+
"focus:ring-1 focus:ring-(--slate-8) focus:ring-offset-2",
|
|
599
659
|
)}
|
|
600
660
|
ref={cellContainerRef}
|
|
601
661
|
{...cellDomProps(cellId, cellData.name)}
|
|
602
662
|
>
|
|
603
|
-
|
|
663
|
+
{!isPresenting && (
|
|
664
|
+
<CellLeftSideActions cellId={cellId} actions={actions} />
|
|
665
|
+
)}
|
|
604
666
|
{cellOutput === "above" && (outputArea || emptyMarkdownPlaceholder)}
|
|
667
|
+
{/* The tray is hidden-but-mounted while presenting so CodeMirror
|
|
668
|
+
state survives mode toggles. Hide via the `hidden` attribute:
|
|
669
|
+
preflight's `[hidden] { display: none !important }` beats
|
|
670
|
+
`.tray { display: flex }`, which a display class would not. */}
|
|
605
671
|
<div
|
|
606
|
-
|
|
672
|
+
data-testid="cell-tray"
|
|
673
|
+
className="tray"
|
|
674
|
+
hidden={isPresenting}
|
|
607
675
|
data-has-output-above={hasOutputAbove}
|
|
608
676
|
data-hidden={isMarkdownCodeHidden}
|
|
609
677
|
>
|
|
@@ -643,21 +711,25 @@ const EditableCellComponent = ({
|
|
|
643
711
|
setLanguageAdapter={setLanguageAdapter}
|
|
644
712
|
outputArea={cellOutput}
|
|
645
713
|
/>
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
714
|
+
{/* Unmounted while presenting: the status timer re-renders every
|
|
715
|
+
frame for running cells. */}
|
|
716
|
+
{!isPresenting && (
|
|
717
|
+
<CellRightSideActions
|
|
718
|
+
className={cn(
|
|
719
|
+
isMarkdownCodeHidden && cellOutput === "below" && "top-14",
|
|
720
|
+
)}
|
|
721
|
+
edited={cellData.edited}
|
|
722
|
+
status={cellRuntime.status}
|
|
723
|
+
isCellStatusInline={isCellStatusInline}
|
|
724
|
+
uninstantiated={uninstantiated}
|
|
725
|
+
disabled={cellData.config.disabled}
|
|
726
|
+
runElapsedTimeMs={cellRuntime.runElapsedTimeMs}
|
|
727
|
+
runStartTimestamp={cellRuntime.runStartTimestamp}
|
|
728
|
+
lastRunStartTimestamp={cellRuntime.lastRunStartTimestamp}
|
|
729
|
+
staleInputs={cellRuntime.staleInputs}
|
|
730
|
+
interrupted={cellRuntime.interrupted}
|
|
731
|
+
/>
|
|
732
|
+
)}
|
|
661
733
|
<div className="shoulder-bottom hover-action">
|
|
662
734
|
{canDelete && isCellCodeShown && (
|
|
663
735
|
<DeleteButton
|
|
@@ -675,12 +747,14 @@ const EditableCellComponent = ({
|
|
|
675
747
|
)}
|
|
676
748
|
</div>
|
|
677
749
|
</div>
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
750
|
+
{!isPresenting && (
|
|
751
|
+
<SqlValidationErrorBanner
|
|
752
|
+
cellId={cellId}
|
|
753
|
+
hide={cellRuntime.errored && !isStaleCell}
|
|
754
|
+
/>
|
|
755
|
+
)}
|
|
682
756
|
{cellOutput === "below" && (outputArea || emptyMarkdownPlaceholder)}
|
|
683
|
-
{cellRuntime.serialization && (
|
|
757
|
+
{!isPresenting && cellRuntime.serialization && (
|
|
684
758
|
<div className="py-1 px-2 flex items-center justify-end gap-2 last:rounded-b">
|
|
685
759
|
{isToplevel && (
|
|
686
760
|
<a
|
|
@@ -738,34 +812,36 @@ const EditableCellComponent = ({
|
|
|
738
812
|
</Tooltip>
|
|
739
813
|
</div>
|
|
740
814
|
)}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
815
|
+
{!isPresenting && (
|
|
816
|
+
<ConsoleOutput
|
|
817
|
+
consoleOutputs={cellRuntime.consoleOutputs}
|
|
818
|
+
stale={consoleOutputStale}
|
|
819
|
+
interrupted={cellRuntime.interrupted}
|
|
820
|
+
// Empty name if serialization triggered
|
|
821
|
+
cellName={cellRuntime.serialization ? "_" : cellData.name}
|
|
822
|
+
onRefactorWithAI={handleRefactorWithAI}
|
|
823
|
+
onClear={() => {
|
|
824
|
+
actions.clearCellConsoleOutput({ cellId });
|
|
825
|
+
// The debugger "Clear" (trashcan) also drops this cell's
|
|
826
|
+
// breakpoints; no-op when none are set.
|
|
827
|
+
clearCellBreakpoints(cellId);
|
|
828
|
+
}}
|
|
829
|
+
onSubmitDebugger={(text, index) => {
|
|
830
|
+
actions.setStdinResponse({
|
|
831
|
+
cellId,
|
|
832
|
+
response: text,
|
|
833
|
+
outputIndex: index,
|
|
834
|
+
});
|
|
835
|
+
sendStdin({ text });
|
|
836
|
+
}}
|
|
837
|
+
cellId={cellId}
|
|
838
|
+
debuggerActive={cellRuntime.debuggerActive}
|
|
839
|
+
/>
|
|
840
|
+
)}
|
|
765
841
|
<PendingDeleteConfirmation cellId={cellId} />
|
|
766
842
|
</div>
|
|
767
|
-
<StagedAICellFooter cellId={cellId} />
|
|
768
|
-
{isCollapsed && (
|
|
843
|
+
{!isPresenting && <StagedAICellFooter cellId={cellId} />}
|
|
844
|
+
{isCollapsed && !isPresenting && (
|
|
769
845
|
<CollapsedCellBanner
|
|
770
846
|
onClick={() => actions.expandCell({ cellId })}
|
|
771
847
|
count={collapseCount}
|
|
@@ -970,6 +1046,7 @@ const SetupCellComponent = ({
|
|
|
970
1046
|
theme,
|
|
971
1047
|
showPlaceholder,
|
|
972
1048
|
cellId,
|
|
1049
|
+
mode,
|
|
973
1050
|
canDelete,
|
|
974
1051
|
userConfig,
|
|
975
1052
|
canMoveX,
|
|
@@ -1080,10 +1157,26 @@ const SetupCellComponent = ({
|
|
|
1080
1157
|
return undefined;
|
|
1081
1158
|
};
|
|
1082
1159
|
|
|
1160
|
+
const isPresenting = mode === "present";
|
|
1161
|
+
|
|
1162
|
+
// CodeMirror cannot measure itself while its hidden ancestor is display:none,
|
|
1163
|
+
// so re-measure when returning to edit mode. (Mirrors EditableCellComponent.)
|
|
1164
|
+
useEffect(() => {
|
|
1165
|
+
if (!isPresenting) {
|
|
1166
|
+
editorView.current?.requestMeasure();
|
|
1167
|
+
}
|
|
1168
|
+
}, [isPresenting, editorView]);
|
|
1169
|
+
|
|
1083
1170
|
return (
|
|
1084
1171
|
<TooltipProvider>
|
|
1085
|
-
<CellActionsContextMenu
|
|
1086
|
-
|
|
1172
|
+
<CellActionsContextMenu
|
|
1173
|
+
cellId={cellId}
|
|
1174
|
+
getEditorView={getEditorView}
|
|
1175
|
+
disabled={isPresenting}
|
|
1176
|
+
>
|
|
1177
|
+
{/* The setup cell has no visible output, so it is hidden (but stays
|
|
1178
|
+
mounted) while presenting. */}
|
|
1179
|
+
<div hidden={isPresenting}>
|
|
1087
1180
|
<div
|
|
1088
1181
|
data-status={cellRuntime.status}
|
|
1089
1182
|
ref={cellRef}
|
|
@@ -1145,18 +1238,22 @@ const SetupCellComponent = ({
|
|
|
1145
1238
|
setLanguageAdapter={Functions.NOOP}
|
|
1146
1239
|
showLanguageToggles={false}
|
|
1147
1240
|
/>
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1241
|
+
{/* Unmounted while presenting: the status timer re-renders every
|
|
1242
|
+
frame for running cells. */}
|
|
1243
|
+
{!isPresenting && (
|
|
1244
|
+
<CellRightSideActions
|
|
1245
|
+
edited={cellData.edited}
|
|
1246
|
+
status={cellRuntime.status}
|
|
1247
|
+
isCellStatusInline={false}
|
|
1248
|
+
uninstantiated={uninstantiated}
|
|
1249
|
+
disabled={cellData.config.disabled}
|
|
1250
|
+
runElapsedTimeMs={cellRuntime.runElapsedTimeMs}
|
|
1251
|
+
runStartTimestamp={cellRuntime.runStartTimestamp}
|
|
1252
|
+
lastRunStartTimestamp={cellRuntime.lastRunStartTimestamp}
|
|
1253
|
+
staleInputs={cellRuntime.staleInputs}
|
|
1254
|
+
interrupted={cellRuntime.interrupted}
|
|
1255
|
+
/>
|
|
1256
|
+
)}
|
|
1160
1257
|
<div className="shoulder-bottom hover-action">
|
|
1161
1258
|
{canDelete && (
|
|
1162
1259
|
<DeleteButton
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { createStore, Provider } from "jotai";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
6
|
+
import { parseAppConfig } from "@/core/config/config-schema";
|
|
7
|
+
import { initialLayoutState, layoutStateAtom } from "@/core/layout/layout";
|
|
8
|
+
import { type AppMode, kioskModeAtom } from "@/core/mode";
|
|
9
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
10
|
+
import { CellsRenderer } from "../cells-renderer";
|
|
11
|
+
import type { LayoutType } from "../types";
|
|
12
|
+
|
|
13
|
+
function renderWithStore(
|
|
14
|
+
mode: AppMode,
|
|
15
|
+
{
|
|
16
|
+
kiosk = false,
|
|
17
|
+
layout = "vertical",
|
|
18
|
+
}: { kiosk?: boolean; layout?: LayoutType } = {},
|
|
19
|
+
) {
|
|
20
|
+
const store = createStore();
|
|
21
|
+
store.set(requestClientAtom, MockRequestClient.create());
|
|
22
|
+
store.set(kioskModeAtom, kiosk);
|
|
23
|
+
store.set(layoutStateAtom, {
|
|
24
|
+
...initialLayoutState(),
|
|
25
|
+
selectedLayout: layout,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return render(
|
|
29
|
+
<Provider store={store}>
|
|
30
|
+
<CellsRenderer appConfig={parseAppConfig({})} mode={mode}>
|
|
31
|
+
<div data-testid="notebook-children" />
|
|
32
|
+
</CellsRenderer>
|
|
33
|
+
</Provider>,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("CellsRenderer", () => {
|
|
38
|
+
it("renders children in edit mode", () => {
|
|
39
|
+
const { queryByTestId } = renderWithStore("edit");
|
|
40
|
+
expect(queryByTestId("notebook-children")).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps children mounted in present mode with the vertical layout", () => {
|
|
44
|
+
// This preserves cell output DOM across the edit <-> present toggle;
|
|
45
|
+
// swapping to the layout renderer would remount every output.
|
|
46
|
+
const { queryByTestId } = renderWithStore("present");
|
|
47
|
+
expect(queryByTestId("notebook-children")).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses the layout renderer in present mode with a non-vertical layout", () => {
|
|
51
|
+
// Only present+vertical keeps the editable tree mounted; grid/slides swap
|
|
52
|
+
// to their layout renderer (which remounts outputs) per the toggle logic.
|
|
53
|
+
const { queryByTestId } = renderWithStore("present", { layout: "grid" });
|
|
54
|
+
expect(queryByTestId("notebook-children")).toBeFalsy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("uses the layout renderer in read mode", () => {
|
|
58
|
+
const { queryByTestId } = renderWithStore("read");
|
|
59
|
+
expect(queryByTestId("notebook-children")).toBeFalsy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses the layout renderer in kiosk mode", () => {
|
|
63
|
+
const { queryByTestId } = renderWithStore("edit", { kiosk: true });
|
|
64
|
+
expect(queryByTestId("notebook-children")).toBeFalsy();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -89,22 +89,32 @@ const CellArrayInternal: React.FC<CellArrayProps> = ({
|
|
|
89
89
|
const { theme } = useTheme();
|
|
90
90
|
const { toggleSidebarPanel } = useChromeActions();
|
|
91
91
|
|
|
92
|
+
const isPresenting = mode === "present";
|
|
93
|
+
|
|
92
94
|
// Side-effects
|
|
93
95
|
useFocusFirstEditor();
|
|
94
96
|
|
|
95
97
|
// HOTKEYS
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
// Cell-editing hotkeys are disabled while presenting
|
|
99
|
+
const whilePresenting = { disabled: isPresenting };
|
|
100
|
+
useHotkey("global.focusTop", actions.focusTopCell, whilePresenting);
|
|
101
|
+
useHotkey("global.focusBottom", actions.focusBottomCell, whilePresenting);
|
|
98
102
|
useHotkey("global.toggleSidebar", toggleSidebarPanel);
|
|
99
|
-
useHotkey("global.foldCode", actions.foldAll);
|
|
100
|
-
useHotkey("global.unfoldCode", actions.unfoldAll);
|
|
101
|
-
useHotkey(
|
|
102
|
-
formatAll
|
|
103
|
-
|
|
103
|
+
useHotkey("global.foldCode", actions.foldAll, whilePresenting);
|
|
104
|
+
useHotkey("global.unfoldCode", actions.unfoldAll, whilePresenting);
|
|
105
|
+
useHotkey(
|
|
106
|
+
"global.formatAll",
|
|
107
|
+
() => {
|
|
108
|
+
formatAll();
|
|
109
|
+
},
|
|
110
|
+
whilePresenting,
|
|
111
|
+
);
|
|
104
112
|
// Catch all to avoid native OS behavior
|
|
105
|
-
// Otherwise a user might try to hide a cell and accidentally hide the OS
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
// Otherwise a user might try to hide a cell and accidentally hide the OS
|
|
114
|
+
// window. While presenting, no cell editing is possible, so let the
|
|
115
|
+
// keystrokes reach the browser/OS.
|
|
116
|
+
useHotkey("cell.hideCode", Functions.NOOP, whilePresenting);
|
|
117
|
+
useHotkey("cell.format", Functions.NOOP, whilePresenting);
|
|
108
118
|
|
|
109
119
|
const cellIds = useCellIds();
|
|
110
120
|
const scrollKey = useScrollKey();
|
|
@@ -121,10 +131,11 @@ const CellArrayInternal: React.FC<CellArrayProps> = ({
|
|
|
121
131
|
return (
|
|
122
132
|
<VerticalLayoutWrapper
|
|
123
133
|
// 'pb' allows the user to put the cell in the middle of the screen
|
|
124
|
-
className="pb-[40vh]"
|
|
134
|
+
className={cn(!isPresenting && "pb-[40vh]")}
|
|
125
135
|
invisible={false}
|
|
126
136
|
appConfig={appConfig}
|
|
127
|
-
|
|
137
|
+
// 'pr' makes room for the floating actions
|
|
138
|
+
innerClassName={cn(!isPresenting && "pr-4")}
|
|
128
139
|
>
|
|
129
140
|
<PackageAlert />
|
|
130
141
|
<StartupLogsAlert />
|
|
@@ -186,6 +197,7 @@ const CellColumn: React.FC<{
|
|
|
186
197
|
|
|
187
198
|
const hasOnlyOneCell = cellIds.hasOnlyOneId();
|
|
188
199
|
const hasSetupCell = cellIds.inOrderIds.includes(SETUP_CELL_ID);
|
|
200
|
+
const isPresenting = mode === "present";
|
|
189
201
|
|
|
190
202
|
return (
|
|
191
203
|
<Column
|
|
@@ -195,8 +207,9 @@ const CellColumn: React.FC<{
|
|
|
195
207
|
canMoveRight={index < columnsLength - 1}
|
|
196
208
|
width={appConfig.width}
|
|
197
209
|
canDelete={columnsLength > 1}
|
|
210
|
+
presenting={isPresenting}
|
|
198
211
|
footer={
|
|
199
|
-
hideControls ? null : (
|
|
212
|
+
hideControls || isPresenting ? null : (
|
|
200
213
|
<AddCellButtons
|
|
201
214
|
columnId={columnId}
|
|
202
215
|
className={cn(
|
|
@@ -29,8 +29,14 @@ export const CellsRenderer: React.FC<PropsWithChildren<Props>> = memo(
|
|
|
29
29
|
const { selectedLayout, layoutData } = useLayoutState();
|
|
30
30
|
const kioskMode = useAtomValue(kioskModeAtom);
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
|
|
32
|
+
// Render children (the editable notebook) in edit mode, and in present
|
|
33
|
+
// mode with the vertical layout: keeping the same tree across the
|
|
34
|
+
// edit<->present toggle preserves cell output DOM (iframes, widgets).
|
|
35
|
+
// Grid/slides layouts and kiosk mode swap to their layout renderer.
|
|
36
|
+
if (
|
|
37
|
+
!kioskMode &&
|
|
38
|
+
(mode === "edit" || (mode === "present" && selectedLayout === "vertical"))
|
|
39
|
+
) {
|
|
34
40
|
return children;
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -42,6 +42,23 @@ describe("SlidesLayoutPlugin validator", () => {
|
|
|
42
42
|
}).success,
|
|
43
43
|
).toBe(false);
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it("accepts each valid deck vertical alignment", () => {
|
|
47
|
+
for (const verticalAlign of ["top", "center", "bottom"]) {
|
|
48
|
+
expect(
|
|
49
|
+
SlidesLayoutPlugin.validator.safeParse({ deck: { verticalAlign } })
|
|
50
|
+
.success,
|
|
51
|
+
).toBe(true);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("rejects an unknown deck vertical alignment", () => {
|
|
56
|
+
expect(
|
|
57
|
+
SlidesLayoutPlugin.validator.safeParse({
|
|
58
|
+
deck: { verticalAlign: "middle" },
|
|
59
|
+
}).success,
|
|
60
|
+
).toBe(false);
|
|
61
|
+
});
|
|
45
62
|
});
|
|
46
63
|
|
|
47
64
|
describe("SlidesLayoutPlugin deserializeLayout", () => {
|
|
@@ -268,6 +285,18 @@ const BACKWARDS_COMPAT_SNAPSHOTS: BackwardsCompatCase[] = [
|
|
|
268
285
|
],
|
|
269
286
|
},
|
|
270
287
|
},
|
|
288
|
+
{
|
|
289
|
+
// `verticalAlign` was added to DeckConfig.
|
|
290
|
+
label: "deck.verticalAlign round-trips through validate + (de)serialize",
|
|
291
|
+
input: {
|
|
292
|
+
cells: [{}],
|
|
293
|
+
deck: { transition: "fade", verticalAlign: "top" },
|
|
294
|
+
},
|
|
295
|
+
expected: {
|
|
296
|
+
deck: { transition: "fade", verticalAlign: "top" },
|
|
297
|
+
cellIds: ["a"],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
271
300
|
];
|
|
272
301
|
|
|
273
302
|
describe("SlidesLayoutPlugin backwards compatibility", () => {
|
|
@@ -23,8 +23,12 @@ const DeckTransitionSchema = z.enum([
|
|
|
23
23
|
]);
|
|
24
24
|
export type DeckTransition = z.infer<typeof DeckTransitionSchema>;
|
|
25
25
|
|
|
26
|
+
const DeckVerticalAlignSchema = z.enum(["top", "center", "bottom"]);
|
|
27
|
+
export type DeckVerticalAlign = z.infer<typeof DeckVerticalAlignSchema>;
|
|
28
|
+
|
|
26
29
|
const DeckConfigSchema = z.looseObject({
|
|
27
30
|
transition: DeckTransitionSchema.optional(),
|
|
31
|
+
verticalAlign: DeckVerticalAlignSchema.optional(),
|
|
28
32
|
});
|
|
29
33
|
export type DeckConfig = z.infer<typeof DeckConfigSchema>;
|
|
30
34
|
|