@marimo-team/islands 0.25.3-dev273 → 0.25.3-dev279

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 (143) hide show
  1. package/dist/{ConnectedDataExplorerComponent-eLHPh6uu.js → ConnectedDataExplorerComponent-CErm132k.js} +25 -26
  2. package/dist/{ErrorBoundary-DBh-wUT9.js → ErrorBoundary-DmTMuDPR.js} +43 -44
  3. package/dist/{abnfDiagram-VCTEODGH-hqd0IBtX.js → abnfDiagram-VCTEODGH-d-Qn2fwb.js} +2 -2
  4. package/dist/{any-language-editor-DnG26QXh.js → any-language-editor-6k6F12Z3.js} +3 -3
  5. package/dist/{architectureDiagram-5GKGNRK7-BzLAFcc0.js → architectureDiagram-5GKGNRK7-DxragHYv.js} +1 -1
  6. package/dist/{assertNever-DIU3JJbt.js → assertNever-B-YzWzbL.js} +14 -14
  7. package/dist/{button-0-RqY1DW.js → button-DZuNnIZO.js} +220 -223
  8. package/dist/{capabilities-B6yy_CBN.js → capabilities-D9FskwsK.js} +1 -1
  9. package/dist/{chat-ui-BWIkve2R.js → chat-ui-v1Fdh0NA.js} +332 -333
  10. package/dist/{check-gUSZbRsl.js → check-XFzZeH-y.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DOeVvuyE.js → chunk-IMKFNOWR-C0x0feFg.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-DZoFI8wR.js → chunk-SHT3W25Y-D0ZdASqB.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-CWu86PSW.js → chunk-SVP7TREG-BHjL96zx.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-BrM0GoP3.js → chunk-TICWLB2K-CzcrwGfp.js} +1 -1
  15. package/dist/{classDiagram-ZZMXUADV-DYi4V2bB.js → classDiagram-ZZMXUADV-BTqFAZTP.js} +2 -2
  16. package/dist/{classDiagram-v2-VYDZK3BY-tN7FIk-m.js → classDiagram-v2-VYDZK3BY-EPXb0Elt.js} +2 -2
  17. package/dist/{code-visibility-M_dXDGp6.js → code-visibility-Dw4-TXN0.js} +20 -20
  18. package/dist/{common-DV0fvw_I.js → common-BXMFi4lr.js} +5399 -5397
  19. package/dist/{config-BsPruWMR.js → config-C8MFHQ2E.js} +23 -22
  20. package/dist/{copy-icon-C2I8sw4h.js → copy-icon-CtllOdmg.js} +3 -3
  21. package/dist/{createReducer-DdtT_7ji.js → createReducer-DRqwmaTc.js} +2 -2
  22. package/dist/{cynefinDiagram-5FMLGOSQ-D5nCnDc-.js → cynefinDiagram-5FMLGOSQ-CBSD344q.js} +1 -1
  23. package/dist/{dagre-GXQ25YYZ-BHkd7f_i.js → dagre-GXQ25YYZ-JItjh3Ge.js} +1 -1
  24. package/dist/{data-grid-overlay-editor-Cd1zGKBt.js → data-grid-overlay-editor-hZzwX7Ia.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-D1ALppT2.js → diagram-S7CK7UJ4-CTD9xTVb.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-DLv0b83L.js → diagram-UQ7AKVKN-DTfnFai8.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-7XJ7J6qg.js → diagram-VX7I27RA-BcSnucV-.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-7M7zbHp_.js → diagram-Z3DM3KII-C2ATIE8A.js} +1 -1
  29. package/dist/{dialog-DzCj1qQv.js → dialog-CaYm5imE.js} +33 -33
  30. package/dist/{dropdown-menu-CdXE15an.js → dropdown-menu-Bb0bFOIk.js} +149 -149
  31. package/dist/{ebnfDiagram-PWID7BFC-DHH1qlmV.js → ebnfDiagram-PWID7BFC-B7B9JPxc.js} +2 -2
  32. package/dist/{erDiagram-RLTQ6QDP-fI_0dGef.js → erDiagram-RLTQ6QDP-v6hDno1O.js} +1 -1
  33. package/dist/{error-banner-DPc9eIw5.js → error-banner-CHCdHVFB.js} +44 -45
  34. package/dist/{error-entries-CxN-Nq3V.js → error-entries-C1S-Rmyt.js} +563 -554
  35. package/dist/{feedback-button-CE2gPi4-.js → feedback-button-6Ui2TDLR.js} +12 -12
  36. package/dist/{flowDiagram-HODETNUW-Brg9MA6i.js → flowDiagram-HODETNUW-aIFlAtOy.js} +2 -2
  37. package/dist/{formats-DbSvAPY-.js → formats-PzBQMUnr.js} +3 -3
  38. package/dist/{glide-data-editor-BycATbgF.js → glide-data-editor-PZNlrSDI.js} +14 -16
  39. package/dist/{glide-utils-BSw3SSHF.js → glide-utils-BSQr4d90.js} +51 -59
  40. package/dist/{globals-CW6OvnI5.js → globals-DX6FPZYq.js} +3 -3
  41. package/dist/{html-to-image-Bc2AvXHG.js → html-to-image-CqGUd58T.js} +14529 -14531
  42. package/dist/{infoDiagram-27XIBGKW-E9e403YT.js → infoDiagram-27XIBGKW-D6YHLeCP.js} +1 -1
  43. package/dist/{ishikawaDiagram-5VMMS53U-C-RlYDRo.js → ishikawaDiagram-5VMMS53U-D1ZvprOX.js} +1 -1
  44. package/dist/{kanban-definition-UXKFOSKX-CLFl_uqK.js → kanban-definition-UXKFOSKX-Bna_3BMr.js} +1 -1
  45. package/dist/{loader-BVEtQ4F3.js → loader-DZvtzQoD.js} +1 -1
  46. package/dist/main.js +7357 -7361
  47. package/dist/{mermaid-rd09e1JL.js → mermaid-A36unKXJ.js} +50 -50
  48. package/dist/{mindmap-definition-YA3MSWOX-BQ8TWtyB.js → mindmap-definition-YA3MSWOX-CyVCs__G.js} +1 -1
  49. package/dist/{number-overlay-editor-B_rR-LdM.js → number-overlay-editor-D0CsL3_R.js} +1 -1
  50. package/dist/{pegDiagram-XKGWAZYB-CN8UHFPi.js → pegDiagram-XKGWAZYB-9hNBD2wZ.js} +2 -2
  51. package/dist/{pencil-DcT8AE0h.js → pencil-DYUlSKxZ.js} +1 -1
  52. package/dist/{pieDiagram-E7YTZNPT-DGuJ0EBd.js → pieDiagram-E7YTZNPT-8tr-NYo7.js} +1 -1
  53. package/dist/{process-output-qecD7Dk8.js → process-output-DskX9jLa.js} +4 -4
  54. package/dist/{railroadDiagram-O6MQD6OU-BPDA8wy8.js → railroadDiagram-O6MQD6OU-Dzg1dyyp.js} +2 -2
  55. package/dist/{requirementDiagram-BXWQKSXE-ChzLGbGW.js → requirementDiagram-BXWQKSXE-B7sNqwGG.js} +1 -1
  56. package/dist/{reveal-component-CngVwOSy.js → reveal-component-D7xeI1QR.js} +1003 -1003
  57. package/dist/{select-OxgozOC8.js → select-DsoP1qe8.js} +290 -290
  58. package/dist/{sequenceDiagram-WJ2MYXX4-DFX-bE6O.js → sequenceDiagram-WJ2MYXX4-oP9shbaG.js} +1 -1
  59. package/dist/{spec-oJn0RhpQ.js → spec-BZMDxAu7.js} +4 -4
  60. package/dist/{stateDiagram-D77RDMKH-BR7cI3wK.js → stateDiagram-D77RDMKH-CjCv4ogv.js} +2 -2
  61. package/dist/{stateDiagram-v2-MP3YSRHH-oGqB5UDI.js → stateDiagram-v2-MP3YSRHH-BW9X162a.js} +2 -2
  62. package/dist/{static-state-CuV6Pc3a.js → static-state-D-Q-Ajaa.js} +8 -8
  63. package/dist/style.css +1 -1
  64. package/dist/{swimlanes-42K2YHIH-DwSZl1yC.js → swimlanes-42K2YHIH-Ckfs9Wrn.js} +1 -1
  65. package/dist/{swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js → swimlanesDiagram-VR7AAH4N-DpTNG9YP.js} +2 -2
  66. package/dist/{swiper-component-Dvj-goaD.js → swiper-component-DQbK_ZWm.js} +7 -7
  67. package/dist/{timeline-definition-24CTP7MA-DMmP-G2O.js → timeline-definition-24CTP7MA-BXaBlV6U.js} +1 -1
  68. package/dist/{toast-DbBHmxmB.js → toast-BhjIy3Tq.js} +158 -158
  69. package/dist/{tooltip-BfcDQd2j.js → tooltip-DMMiOn2f.js} +64 -64
  70. package/dist/use-restore-focus-CosX7-gx.js +7568 -0
  71. package/dist/{useAsyncData-BYIAoMqn.js → useAsyncData-Ci4AvJff.js} +4 -4
  72. package/dist/{useDateFormatter-ButWkdcK.js → useDateFormatter-CPlava52.js} +2 -2
  73. package/dist/{useDeepCompareMemoize-Bz0c5GN6.js → useDeepCompareMemoize-NaaWoFlX.js} +1 -1
  74. package/dist/useEvent-BRH-aCqi.js +19 -0
  75. package/dist/{useIframeCapabilities-BIolGsF1.js → useIframeCapabilities-BXhDDoCG.js} +1 -1
  76. package/dist/{useLifecycle-JjxCC7Vu.js → useLifecycle-SniivYmC.js} +2 -2
  77. package/dist/{vega-component-rxIxAAh9.js → vega-component-DIxaDQNs.js} +40 -40
  78. package/dist/{vennDiagram-4TSXK5OY-Db8GtH1G.js → vennDiagram-4TSXK5OY-C-hQ4iNJ.js} +1 -1
  79. package/dist/{wardleyDiagram-VM6X3IG4-DmIiC3FU.js → wardleyDiagram-VM6X3IG4-C2MQ_Umv.js} +1 -1
  80. package/dist/{xychartDiagram-S5SC5T6Z-DS9OPrNq.js → xychartDiagram-S5SC5T6Z-BtTTBn7Q.js} +1 -1
  81. package/package.json +1 -1
  82. package/src/__mocks__/executions.ts +4 -2
  83. package/src/__mocks__/notebook.ts +0 -2
  84. package/src/__tests__/CellStatus.test.tsx +6 -6
  85. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -2
  86. package/src/components/editor/__tests__/output-persistence.test.tsx +20 -5
  87. package/src/components/editor/cell/CellStatus.tsx +6 -5
  88. package/src/components/editor/cell/__tests__/useRunCells.test.tsx +169 -0
  89. package/src/components/editor/cell/collapse.tsx +3 -4
  90. package/src/components/editor/cell/useRunCells.ts +32 -3
  91. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +2 -3
  92. package/src/components/editor/notebook-cell.tsx +23 -37
  93. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +4 -10
  94. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -2
  95. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +8 -5
  96. package/src/components/slides/minimap.tsx +3 -2
  97. package/src/components/slides/reveal-component.tsx +3 -2
  98. package/src/components/slides/slide-cell-view.tsx +8 -8
  99. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -101
  100. package/src/core/cells/__tests__/actions.test.ts +0 -1
  101. package/src/core/cells/__tests__/cell.test.ts +81 -17
  102. package/src/core/cells/__tests__/cells.test.ts +0 -100
  103. package/src/core/cells/__tests__/document.test.ts +2 -8
  104. package/src/core/cells/__tests__/utils.test.ts +93 -24
  105. package/src/core/cells/actions.ts +0 -1
  106. package/src/core/cells/cell.ts +37 -14
  107. package/src/core/cells/cells.ts +10 -99
  108. package/src/core/cells/document.ts +2 -13
  109. package/src/core/cells/types.ts +0 -23
  110. package/src/core/cells/utils.ts +44 -17
  111. package/src/core/codemirror/cells/extensions.ts +0 -14
  112. package/src/core/errors/__tests__/error-entries.test.ts +40 -1
  113. package/src/core/errors/error-entries.ts +2 -3
  114. package/src/core/executions/__tests__/freshness.test.ts +420 -0
  115. package/src/core/executions/__tests__/hooks.test.tsx +106 -0
  116. package/src/core/executions/atoms.ts +22 -0
  117. package/src/core/executions/freshness.ts +164 -0
  118. package/src/core/executions/hooks.ts +17 -1
  119. package/src/core/executions/index.ts +3 -1
  120. package/src/core/islands/bootstrap.ts +0 -1
  121. package/src/core/islands/components/__tests__/web-components.test.tsx +63 -0
  122. package/src/core/islands/components/web-components.tsx +11 -0
  123. package/src/core/kernel/handlers.ts +0 -4
  124. package/src/core/session/useNotebookSession.tsx +2 -7
  125. package/src/css/globals.css +6 -2
  126. package/src/plugins/impl/DataEditorPlugin.tsx +0 -4
  127. package/src/plugins/impl/data-editor/__tests__/column-types.test.ts +5 -0
  128. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +9 -5
  129. package/src/plugins/impl/data-editor/column-types.ts +3 -0
  130. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -5
  131. package/src/plugins/impl/data-editor/glide-utils.ts +10 -32
  132. package/src/plugins/impl/data-editor/types.ts +0 -2
  133. package/src/stories/cell-status.stories.tsx +12 -14
  134. package/src/stories/cell.stories.tsx +44 -10
  135. package/src/stories/layout/vertical/one-column.stories.tsx +0 -10
  136. package/src/theme/__tests__/islands-theme.test.tsx +276 -0
  137. package/src/theme/namespace.tsx +4 -3
  138. package/src/theme/useTheme.ts +71 -41
  139. package/dist/schemas-CdscWNy_.js +0 -4747
  140. package/dist/use-restore-focus-CmflVLWJ.js +0 -1672
  141. package/dist/useTheme-DPLobSFs.js +0 -339
  142. package/dist/utils-EBklsC5R.js +0 -813
  143. package/src/components/editor/cell/__tests__/useRunCells.test.ts +0 -45
@@ -27,7 +27,7 @@ export const Idle = Template.bind({});
27
27
  Idle.args = {
28
28
  editing: true,
29
29
  status: "idle",
30
- edited: false,
30
+ freshness: { sourceChanged: false, inputsChanged: false },
31
31
  interrupted: false,
32
32
  disabled: false,
33
33
  elapsedTime: null,
@@ -37,7 +37,7 @@ export const Edited = Template.bind({});
37
37
  Edited.args = {
38
38
  editing: true,
39
39
  status: "idle",
40
- edited: true,
40
+ freshness: { sourceChanged: true, inputsChanged: false },
41
41
  interrupted: false,
42
42
  disabled: false,
43
43
  elapsedTime: 100 as Milliseconds,
@@ -47,7 +47,7 @@ export const Interrupted = Template.bind({});
47
47
  Interrupted.args = {
48
48
  editing: true,
49
49
  status: "idle",
50
- edited: false,
50
+ freshness: { sourceChanged: false, inputsChanged: false },
51
51
  interrupted: true,
52
52
  disabled: false,
53
53
  elapsedTime: 50 as Milliseconds,
@@ -57,7 +57,7 @@ export const Running = Template.bind({});
57
57
  Running.args = {
58
58
  editing: true,
59
59
  status: "running",
60
- edited: false,
60
+ freshness: { sourceChanged: false, inputsChanged: false },
61
61
  interrupted: true,
62
62
  disabled: false,
63
63
  elapsedTime: 50 as Milliseconds,
@@ -67,7 +67,7 @@ export const Queued = Template.bind({});
67
67
  Queued.args = {
68
68
  editing: true,
69
69
  status: "queued",
70
- edited: false,
70
+ freshness: { sourceChanged: false, inputsChanged: false },
71
71
  interrupted: true,
72
72
  disabled: false,
73
73
  elapsedTime: 50 as Milliseconds,
@@ -77,7 +77,7 @@ export const Disabled = Template.bind({});
77
77
  Disabled.args = {
78
78
  editing: true,
79
79
  status: "idle",
80
- edited: false,
80
+ freshness: { sourceChanged: false, inputsChanged: false },
81
81
  interrupted: false,
82
82
  disabled: true,
83
83
  elapsedTime: null,
@@ -88,7 +88,7 @@ DisabledTransitively.args = {
88
88
  editing: true,
89
89
  status: "idle",
90
90
  blocked: true,
91
- edited: false,
91
+ freshness: { sourceChanged: false, inputsChanged: false },
92
92
  interrupted: false,
93
93
  disabled: false,
94
94
  elapsedTime: null,
@@ -99,7 +99,7 @@ DisabledTransitivelyAndEdited.args = {
99
99
  editing: true,
100
100
  status: "idle",
101
101
  blocked: true,
102
- edited: true,
102
+ freshness: { sourceChanged: true, inputsChanged: false },
103
103
  interrupted: false,
104
104
  disabled: false,
105
105
  elapsedTime: null,
@@ -110,8 +110,7 @@ Stale.args = {
110
110
  editing: true,
111
111
  status: "idle",
112
112
  blocked: true,
113
- staleInputs: true,
114
- edited: false,
113
+ freshness: { sourceChanged: false, inputsChanged: true },
115
114
  interrupted: false,
116
115
  disabled: false,
117
116
  elapsedTime: null,
@@ -122,8 +121,7 @@ StaleAndDisabled.args = {
122
121
  editing: true,
123
122
  status: "idle",
124
123
  blocked: true,
125
- staleInputs: true,
126
- edited: false,
124
+ freshness: { sourceChanged: false, inputsChanged: true },
127
125
  interrupted: false,
128
126
  disabled: true,
129
127
  elapsedTime: null,
@@ -134,7 +132,7 @@ EditedStaleAndDisabled.args = {
134
132
  editing: true,
135
133
  status: "idle",
136
134
  blocked: true,
137
- staleInputs: true,
135
+ freshness: { sourceChanged: false, inputsChanged: true },
138
136
  interrupted: false,
139
137
  disabled: true,
140
138
  elapsedTime: null,
@@ -145,7 +143,7 @@ EditedAndStale.args = {
145
143
  editing: true,
146
144
  status: "idle",
147
145
  blocked: true,
148
- edited: true,
146
+ freshness: { sourceChanged: true, inputsChanged: false },
149
147
  interrupted: false,
150
148
  disabled: false,
151
149
  elapsedTime: null,
@@ -3,7 +3,14 @@
3
3
  import type { Meta, StoryObj } from "@storybook/react-vite";
4
4
  import { createStore, Provider } from "jotai";
5
5
  import { createRef } from "react";
6
+ import { MockExecutions } from "@/__mocks__/executions";
6
7
  import { cellId } from "@/__tests__/branded";
8
+ import type { CellId } from "@/core/cells/ids";
9
+ import {
10
+ cell,
11
+ document as documentOf,
12
+ withCells,
13
+ } from "@/core/notebook/document/__tests__/fixtures";
7
14
  import { type NotebookState, notebookAtom } from "@/core/cells/cells";
8
15
  import {
9
16
  type CellRuntimeState,
@@ -22,13 +29,41 @@ type Story = StoryObj<typeof Cell>;
22
29
 
23
30
  interface CellOverrides {
24
31
  output?: CellRuntimeState["output"];
25
- edited?: boolean;
26
- staleInputs?: boolean;
32
+ /** How the cell's result is outdated, as the host would report it. */
33
+ freshness?: "edited" | "replaced" | "both";
27
34
  config?: CellConfig;
28
35
  }
29
36
 
30
37
  const EMPTY_OVERRIDES: Readonly<CellOverrides> = {};
31
38
 
39
+ /**
40
+ * Give the replica a completed execution whose source was edited since,
41
+ * whose input a newer execution replaced, or both.
42
+ */
43
+ function seedFreshness(
44
+ store: ReturnType<typeof createStore>,
45
+ cid: CellId,
46
+ freshness: NonNullable<CellOverrides["freshness"]>,
47
+ ) {
48
+ const ran = MockExecutions.completed(cid, {
49
+ code_fingerprint: "ran",
50
+ depends_on:
51
+ freshness === "edited" ? {} : { x: { execution: "gone", update: 0 } },
52
+ });
53
+ const document = withCells(
54
+ documentOf(),
55
+ {
56
+ c1: null,
57
+ c2: null,
58
+ [cid]: cell(cid, "import marimo as mo", {
59
+ code_fingerprint: freshness === "replaced" ? "ran" : "edited",
60
+ }),
61
+ },
62
+ [cid],
63
+ );
64
+ MockExecutions.seed(store, [ran], null, document);
65
+ }
66
+
32
67
  const Cell: React.FC<{
33
68
  overrides?: CellOverrides;
34
69
  }> = ({ overrides = EMPTY_OVERRIDES }) => {
@@ -39,13 +74,11 @@ const Cell: React.FC<{
39
74
  id: cid,
40
75
  name: "cell_1",
41
76
  code: "import marimo as mo",
42
- edited: overrides.edited ?? false,
43
77
  config: {
44
78
  hide_code: overrides.config?.hide_code ?? false,
45
79
  disabled: overrides.config?.disabled ?? false,
46
80
  column: null,
47
81
  },
48
- lastCodeRun: null,
49
82
  },
50
83
  },
51
84
  cellIds: [cid],
@@ -53,7 +86,6 @@ const Cell: React.FC<{
53
86
  [cid]: createCellRuntimeState({
54
87
  output: overrides.output ?? null,
55
88
  consoleOutputs: [],
56
- staleInputs: overrides.staleInputs ?? false,
57
89
  debuggerActive: false,
58
90
  outline: null,
59
91
  }),
@@ -68,6 +100,9 @@ const Cell: React.FC<{
68
100
 
69
101
  const store = createStore();
70
102
  store.set(notebookAtom, notebook);
103
+ if (overrides.freshness !== undefined) {
104
+ seedFreshness(store, cid, overrides.freshness);
105
+ }
71
106
  store.set(connectionAtom, { state: ConnectionState.OPEN });
72
107
  store.set(requestClientAtom, resolveRequestClient());
73
108
  return (
@@ -145,7 +180,7 @@ export const UnsavedEditsOutput: Story = {
145
180
  <TooltipProvider>
146
181
  <Cell
147
182
  overrides={{
148
- edited: true,
183
+ freshness: "edited",
149
184
  output: {
150
185
  channel: "output",
151
186
  data: "<span class='markdown'><h1>Layout</h1>\n<p><code>marimo</code> provides functions to help you lay out your output, such as\nin rows and columns, accordions, tabs, and callouts. This tutorial\nshows some examples.</p></span>",
@@ -239,7 +274,7 @@ export const StaleStatus: Story = {
239
274
  <div className="p-20 max-w-4xl">
240
275
  <Cell
241
276
  overrides={{
242
- staleInputs: true,
277
+ freshness: "replaced",
243
278
  output: {
244
279
  channel: "output",
245
280
  data: "This data is stale because a parent is disabled",
@@ -257,8 +292,7 @@ export const StaleAndEditedStatus: Story = {
257
292
  <div className="p-20 max-w-4xl">
258
293
  <Cell
259
294
  overrides={{
260
- staleInputs: true,
261
- edited: true,
295
+ freshness: "both",
262
296
  output: {
263
297
  channel: "output",
264
298
  data: "This data is stale because a parent is disabled, but this cell has been edited since.",
@@ -281,7 +315,7 @@ export const DisabledAndStaleStatus: Story = {
281
315
  disabled: true,
282
316
  hide_code: false,
283
317
  },
284
- staleInputs: true,
318
+ freshness: "replaced",
285
319
  output: {
286
320
  channel: "output",
287
321
  data: "<span class='markdown'><h1>Layout</h1>\n<p><code>marimo</code> provides functions to help you lay out your output, such as\nin rows and columns, accordions, tabs, and callouts. This tutorial\nshows some examples.</p></span>",
@@ -35,20 +35,11 @@ const createLongReprNotebook = (
35
35
 
36
36
  print('hello')
37
37
  LongReprA()`,
38
- edited: false,
39
38
  config: {
40
39
  hide_code: hideCode,
41
40
  disabled: false,
42
41
  column: null,
43
42
  },
44
- lastCodeRun: `class LongReprA:
45
- def __repr__(self):
46
- return "<dlt.Relation(dataset='" + ("jaffle_ingest_dataset_" * 80) + \\
47
- "', destination='<dlt.destinations.duckdb(destination_type=\\'duckdb\\'," + \\
48
- " credentials=\\'local_jaffle.duckdb\\')>')>"
49
-
50
- print('hello')
51
- LongReprA()`,
52
43
  },
53
44
  },
54
45
  cellIds: [cellId],
@@ -68,7 +59,6 @@ LongReprA()`,
68
59
  timestamp: 1_686_863_705,
69
60
  },
70
61
  ],
71
- staleInputs: false,
72
62
  debuggerActive: false,
73
63
  outline: null,
74
64
  }),
@@ -0,0 +1,276 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render, waitFor } from "@testing-library/react";
4
+ import { createStore } from "jotai";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { JsonOutput } from "@/components/editor/output/JsonOutput";
7
+ import {
8
+ marimoDarkTheme,
9
+ marimoLightTheme,
10
+ } from "@/components/editor/output/json-output/themes";
11
+ import { userConfigAtom } from "@/core/config/config";
12
+ import { defaultUserConfig } from "@/core/config/config-schema";
13
+ import { StyleNamespace } from "../namespace";
14
+ import { resolvedThemeAtom } from "../useTheme";
15
+
16
+ let store: ReturnType<typeof createStore>;
17
+ let unsubscribe: (() => void) | undefined;
18
+ let media: EventTarget & { matches: boolean };
19
+
20
+ beforeEach(() => {
21
+ vi.stubEnv("VITE_MARIMO_ISLANDS", "true");
22
+ media = Object.assign(new EventTarget(), { matches: false });
23
+ vi.stubGlobal(
24
+ "matchMedia",
25
+ vi.fn(() => media),
26
+ );
27
+ store = createStore();
28
+ });
29
+
30
+ afterEach(() => {
31
+ stopObserving();
32
+ for (const element of [document.body, document.documentElement]) {
33
+ for (const attribute of [
34
+ "class",
35
+ "style",
36
+ "data-theme",
37
+ "data-mode",
38
+ "data-vscode-theme-kind",
39
+ ]) {
40
+ element.removeAttribute(attribute);
41
+ }
42
+ }
43
+ vi.unstubAllEnvs();
44
+ vi.unstubAllGlobals();
45
+ vi.restoreAllMocks();
46
+ });
47
+
48
+ function subscribe() {
49
+ unsubscribe = store.sub(resolvedThemeAtom, vi.fn());
50
+ }
51
+
52
+ function stopObserving() {
53
+ unsubscribe?.();
54
+ unsubscribe = undefined;
55
+ }
56
+
57
+ function getTheme() {
58
+ return store.get(resolvedThemeAtom);
59
+ }
60
+
61
+ function setSystemTheme(matches: boolean) {
62
+ media.matches = matches;
63
+ media.dispatchEvent(new Event("change"));
64
+ }
65
+
66
+ describe("islands host theme", () => {
67
+ it("defaults to light on a transparent page even if the system is dark", () => {
68
+ media.matches = true;
69
+ subscribe();
70
+ expect(getTheme()).toBe("light");
71
+ });
72
+
73
+ it.each(["body", "html"])(
74
+ "follows %s theme attributes in both directions",
75
+ async (tag) => {
76
+ const host = document.querySelector(tag)!;
77
+ subscribe();
78
+ for (const [attribute, dark, light] of [
79
+ ["class", "dark", "light"],
80
+ ["class", "dark-mode", "light-mode"],
81
+ ["class", "dark-theme", "light-theme"],
82
+ ["data-theme", "dark", "light"],
83
+ ["data-mode", "dark", "light"],
84
+ ]) {
85
+ host.setAttribute(attribute, dark);
86
+ await expect.poll(getTheme).toBe("dark");
87
+ host.setAttribute(attribute, light);
88
+ await expect.poll(getTheme).toBe("light");
89
+ host.removeAttribute(attribute);
90
+ }
91
+ },
92
+ );
93
+
94
+ it("reads the initial host theme and follows color-scheme changes", async () => {
95
+ document.body.style.colorScheme = "dark";
96
+ subscribe();
97
+ expect(getTheme()).toBe("dark");
98
+ document.body.style.colorScheme = "light";
99
+ await expect.poll(getTheme).toBe("light");
100
+ document.body.style.colorScheme = "dark";
101
+ await expect.poll(getTheme).toBe("dark");
102
+ });
103
+
104
+ it("uses the background when color-scheme is normal", async () => {
105
+ document.body.style.colorScheme = "normal";
106
+ document.body.style.backgroundColor = "rgb(30, 30, 30)";
107
+ subscribe();
108
+ expect(getTheme()).toBe("dark");
109
+ document.body.style.backgroundColor = "white";
110
+ await expect.poll(getTheme).toBe("light");
111
+ });
112
+
113
+ it("resolves a host supporting both color schemes when the system changes", async () => {
114
+ document.body.style.colorScheme = "light dark";
115
+ subscribe();
116
+ expect(getTheme()).toBe("light");
117
+ for (const matches of [true, false]) {
118
+ setSystemTheme(matches);
119
+ await expect.poll(getTheme).toBe(matches ? "dark" : "light");
120
+ }
121
+ });
122
+
123
+ it("shares a subscription, skips unchanged themes, and cleans up on unmount", async () => {
124
+ const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect");
125
+ const removeListener = vi.spyOn(media, "removeEventListener");
126
+ const listener = vi.fn();
127
+ subscribe();
128
+ const unsubscribeSecond = store.sub(resolvedThemeAtom, listener);
129
+ document.body.classList.add("unrelated");
130
+ await new Promise((resolve) => setTimeout(resolve, 0));
131
+ expect(listener).not.toHaveBeenCalled();
132
+ unsubscribeSecond();
133
+ expect(disconnect).not.toHaveBeenCalled();
134
+ stopObserving();
135
+ expect(disconnect).toHaveBeenCalledOnce();
136
+ expect(removeListener).toHaveBeenCalledWith("change", expect.any(Function));
137
+ document.body.classList.add("dark");
138
+ subscribe();
139
+ expect(getTheme()).toBe("dark");
140
+ });
141
+
142
+ it("ignores unrecognized theme attributes and transparent body backgrounds", async () => {
143
+ document.body.dataset.theme = "custom-theme";
144
+ document.body.dataset.mode = "auto";
145
+ document.body.style.backgroundColor = "rgba(0, 0, 0, 0)";
146
+ document.documentElement.style.backgroundColor = "white";
147
+ subscribe();
148
+ expect(getTheme()).toBe("light");
149
+ document.documentElement.style.backgroundColor = "black";
150
+ await expect.poll(getTheme).toBe("dark");
151
+ document.documentElement.style.backgroundColor = "transparent";
152
+ await expect.poll(getTheme).toBe("light");
153
+ });
154
+
155
+ it("falls back after a theme attribute is removed", async () => {
156
+ document.body.dataset.theme = "dark";
157
+ document.body.style.colorScheme = "light";
158
+ subscribe();
159
+ expect(getTheme()).toBe("dark");
160
+ delete document.body.dataset.theme;
161
+ await expect.poll(getTheme).toBe("light");
162
+ });
163
+
164
+ it("continues to observe the host without matchMedia", async () => {
165
+ vi.stubGlobal("matchMedia", undefined);
166
+ document.body.style.colorScheme = "light dark";
167
+ subscribe();
168
+ expect(getTheme()).toBe("light");
169
+ document.body.style.colorScheme = "only dark";
170
+ await expect.poll(getTheme).toBe("dark");
171
+ });
172
+
173
+ it.each(["light", "dark"] as const)(
174
+ "does not override an explicit %s host when system preferences change",
175
+ (theme) => {
176
+ document.body.style.colorScheme = theme;
177
+ subscribe();
178
+ for (const matches of [true, false]) {
179
+ setSystemTheme(matches);
180
+ expect(getTheme()).toBe(theme);
181
+ }
182
+ },
183
+ );
184
+
185
+ it("re-reads host CSS when a media query changes its color-scheme", () => {
186
+ const computedStyle = window.getComputedStyle(document.body);
187
+ const scheme = vi.spyOn(computedStyle, "colorScheme", "get");
188
+ vi.spyOn(window, "getComputedStyle").mockReturnValue(computedStyle);
189
+ scheme.mockReturnValue("light");
190
+ subscribe();
191
+ expect(getTheme()).toBe("light");
192
+ scheme.mockReturnValue("dark");
193
+ media.dispatchEvent(new Event("change"));
194
+ expect(getTheme()).toBe("dark");
195
+ });
196
+
197
+ it("discards queued mutations and media events after the last unsubscribe", async () => {
198
+ const readStyle = vi.spyOn(window, "getComputedStyle");
199
+ subscribe();
200
+ document.body.style.colorScheme = "dark";
201
+ stopObserving();
202
+ readStyle.mockClear();
203
+ media.dispatchEvent(new Event("change"));
204
+ await new Promise((resolve) => setTimeout(resolve, 0));
205
+ expect(readStyle).not.toHaveBeenCalled();
206
+ subscribe();
207
+ expect(getTheme()).toBe("dark");
208
+ });
209
+
210
+ it("keeps the VS Code override and returns to the host after it is removed", async () => {
211
+ document.body.classList.add("dark");
212
+ const { container, unmount } = render(
213
+ <StyleNamespace>Widget</StyleNamespace>,
214
+ );
215
+ for (const [kind, theme] of [
216
+ ["vscode-light", "light"],
217
+ ["vscode-high-contrast", "dark"],
218
+ ["vscode-light", "light"],
219
+ ]) {
220
+ document.body.dataset.vscodeThemeKind = kind;
221
+ await waitFor(() => expect(container.firstChild).toHaveClass(theme));
222
+ }
223
+ delete document.body.dataset.vscodeThemeKind;
224
+ await waitFor(() => expect(container.firstChild).toHaveClass("dark"));
225
+ unmount();
226
+ });
227
+
228
+ it("does not subscribe to host themes outside Islands", async () => {
229
+ vi.stubEnv("VITE_MARIMO_ISLANDS", "false");
230
+ const config = defaultUserConfig();
231
+ store.set(userConfigAtom, {
232
+ ...config,
233
+ display: { ...config.display, theme: "light" },
234
+ });
235
+ const observe = vi.spyOn(MutationObserver.prototype, "observe");
236
+ const addListener = vi.spyOn(media, "addEventListener");
237
+ subscribe();
238
+ document.body.classList.add("dark");
239
+ await new Promise((resolve) => setTimeout(resolve, 0));
240
+ expect(getTheme()).toBe("light");
241
+ expect(observe).not.toHaveBeenCalled();
242
+ expect(addListener).not.toHaveBeenCalled();
243
+ });
244
+
245
+ it("updates the namespace root used by shadow DOM widgets", async () => {
246
+ const { container } = render(<StyleNamespace>Widget</StyleNamespace>);
247
+ expect(container.firstChild).toHaveClass("marimo", "light");
248
+ document.body.classList.add("dark");
249
+ await waitFor(() =>
250
+ expect(container.firstChild).toHaveClass("marimo", "dark"),
251
+ );
252
+ document.body.classList.remove("dark");
253
+ await waitFor(() =>
254
+ expect(container.firstChild).toHaveClass("marimo", "light"),
255
+ );
256
+ });
257
+
258
+ it("updates the dictionary viewer palette without remounting", async () => {
259
+ const { container } = render(
260
+ <JsonOutput data={{ relu: 0, note: "text" }} />,
261
+ );
262
+ const viewer = container.querySelector<HTMLElement>(
263
+ ".w-json-view-container",
264
+ );
265
+ expect(viewer).not.toBeNull();
266
+ for (const theme of ["dark", "light"] as const) {
267
+ document.body.dataset.theme = theme;
268
+ const palette = theme === "dark" ? marimoDarkTheme : marimoLightTheme;
269
+ await waitFor(() =>
270
+ expect(viewer?.style.getPropertyValue("--w-rjv-color")).toBe(
271
+ palette["--w-rjv-color"],
272
+ ),
273
+ );
274
+ }
275
+ });
276
+ });
@@ -1,20 +1,21 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import React, { type PropsWithChildren } from "react";
3
+ import { useTheme } from "./useTheme";
3
4
 
4
5
  const style = {
5
6
  display: "contents",
6
7
  };
7
8
 
8
9
  /**
9
- * Adds the 'marimo' className which serves as a namespace to its children,
10
- * so that the styles are scoped to the DOM subtree.
10
+ * Scopes styles and applies the theme before descendants inherit text colors.
11
11
  */
12
12
  export const StyleNamespace = React.forwardRef<
13
13
  HTMLDivElement,
14
14
  PropsWithChildren<{}>
15
15
  >(({ children }, ref) => {
16
+ const { theme } = useTheme();
16
17
  return (
17
- <div ref={ref} className="marimo" style={style}>
18
+ <div ref={ref} className={`marimo ${theme}`} style={style}>
18
19
  {children}
19
20
  </div>
20
21
  );