@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.
- package/dist/{ConnectedDataExplorerComponent-eLHPh6uu.js → ConnectedDataExplorerComponent-CErm132k.js} +25 -26
- package/dist/{ErrorBoundary-DBh-wUT9.js → ErrorBoundary-DmTMuDPR.js} +43 -44
- package/dist/{abnfDiagram-VCTEODGH-hqd0IBtX.js → abnfDiagram-VCTEODGH-d-Qn2fwb.js} +2 -2
- package/dist/{any-language-editor-DnG26QXh.js → any-language-editor-6k6F12Z3.js} +3 -3
- package/dist/{architectureDiagram-5GKGNRK7-BzLAFcc0.js → architectureDiagram-5GKGNRK7-DxragHYv.js} +1 -1
- package/dist/{assertNever-DIU3JJbt.js → assertNever-B-YzWzbL.js} +14 -14
- package/dist/{button-0-RqY1DW.js → button-DZuNnIZO.js} +220 -223
- package/dist/{capabilities-B6yy_CBN.js → capabilities-D9FskwsK.js} +1 -1
- package/dist/{chat-ui-BWIkve2R.js → chat-ui-v1Fdh0NA.js} +332 -333
- package/dist/{check-gUSZbRsl.js → check-XFzZeH-y.js} +1 -1
- package/dist/{chunk-IMKFNOWR-DOeVvuyE.js → chunk-IMKFNOWR-C0x0feFg.js} +1 -1
- package/dist/{chunk-SHT3W25Y-DZoFI8wR.js → chunk-SHT3W25Y-D0ZdASqB.js} +1 -1
- package/dist/{chunk-SVP7TREG-CWu86PSW.js → chunk-SVP7TREG-BHjL96zx.js} +1 -1
- package/dist/{chunk-TICWLB2K-BrM0GoP3.js → chunk-TICWLB2K-CzcrwGfp.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-DYi4V2bB.js → classDiagram-ZZMXUADV-BTqFAZTP.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-tN7FIk-m.js → classDiagram-v2-VYDZK3BY-EPXb0Elt.js} +2 -2
- package/dist/{code-visibility-M_dXDGp6.js → code-visibility-Dw4-TXN0.js} +20 -20
- package/dist/{common-DV0fvw_I.js → common-BXMFi4lr.js} +5399 -5397
- package/dist/{config-BsPruWMR.js → config-C8MFHQ2E.js} +23 -22
- package/dist/{copy-icon-C2I8sw4h.js → copy-icon-CtllOdmg.js} +3 -3
- package/dist/{createReducer-DdtT_7ji.js → createReducer-DRqwmaTc.js} +2 -2
- package/dist/{cynefinDiagram-5FMLGOSQ-D5nCnDc-.js → cynefinDiagram-5FMLGOSQ-CBSD344q.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-BHkd7f_i.js → dagre-GXQ25YYZ-JItjh3Ge.js} +1 -1
- package/dist/{data-grid-overlay-editor-Cd1zGKBt.js → data-grid-overlay-editor-hZzwX7Ia.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-D1ALppT2.js → diagram-S7CK7UJ4-CTD9xTVb.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DLv0b83L.js → diagram-UQ7AKVKN-DTfnFai8.js} +1 -1
- package/dist/{diagram-VX7I27RA-7XJ7J6qg.js → diagram-VX7I27RA-BcSnucV-.js} +1 -1
- package/dist/{diagram-Z3DM3KII-7M7zbHp_.js → diagram-Z3DM3KII-C2ATIE8A.js} +1 -1
- package/dist/{dialog-DzCj1qQv.js → dialog-CaYm5imE.js} +33 -33
- package/dist/{dropdown-menu-CdXE15an.js → dropdown-menu-Bb0bFOIk.js} +149 -149
- package/dist/{ebnfDiagram-PWID7BFC-DHH1qlmV.js → ebnfDiagram-PWID7BFC-B7B9JPxc.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-fI_0dGef.js → erDiagram-RLTQ6QDP-v6hDno1O.js} +1 -1
- package/dist/{error-banner-DPc9eIw5.js → error-banner-CHCdHVFB.js} +44 -45
- package/dist/{error-entries-CxN-Nq3V.js → error-entries-C1S-Rmyt.js} +563 -554
- package/dist/{feedback-button-CE2gPi4-.js → feedback-button-6Ui2TDLR.js} +12 -12
- package/dist/{flowDiagram-HODETNUW-Brg9MA6i.js → flowDiagram-HODETNUW-aIFlAtOy.js} +2 -2
- package/dist/{formats-DbSvAPY-.js → formats-PzBQMUnr.js} +3 -3
- package/dist/{glide-data-editor-BycATbgF.js → glide-data-editor-PZNlrSDI.js} +14 -16
- package/dist/{glide-utils-BSw3SSHF.js → glide-utils-BSQr4d90.js} +51 -59
- package/dist/{globals-CW6OvnI5.js → globals-DX6FPZYq.js} +3 -3
- package/dist/{html-to-image-Bc2AvXHG.js → html-to-image-CqGUd58T.js} +14529 -14531
- package/dist/{infoDiagram-27XIBGKW-E9e403YT.js → infoDiagram-27XIBGKW-D6YHLeCP.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-C-RlYDRo.js → ishikawaDiagram-5VMMS53U-D1ZvprOX.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CLFl_uqK.js → kanban-definition-UXKFOSKX-Bna_3BMr.js} +1 -1
- package/dist/{loader-BVEtQ4F3.js → loader-DZvtzQoD.js} +1 -1
- package/dist/main.js +7357 -7361
- package/dist/{mermaid-rd09e1JL.js → mermaid-A36unKXJ.js} +50 -50
- package/dist/{mindmap-definition-YA3MSWOX-BQ8TWtyB.js → mindmap-definition-YA3MSWOX-CyVCs__G.js} +1 -1
- package/dist/{number-overlay-editor-B_rR-LdM.js → number-overlay-editor-D0CsL3_R.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-CN8UHFPi.js → pegDiagram-XKGWAZYB-9hNBD2wZ.js} +2 -2
- package/dist/{pencil-DcT8AE0h.js → pencil-DYUlSKxZ.js} +1 -1
- package/dist/{pieDiagram-E7YTZNPT-DGuJ0EBd.js → pieDiagram-E7YTZNPT-8tr-NYo7.js} +1 -1
- package/dist/{process-output-qecD7Dk8.js → process-output-DskX9jLa.js} +4 -4
- package/dist/{railroadDiagram-O6MQD6OU-BPDA8wy8.js → railroadDiagram-O6MQD6OU-Dzg1dyyp.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-ChzLGbGW.js → requirementDiagram-BXWQKSXE-B7sNqwGG.js} +1 -1
- package/dist/{reveal-component-CngVwOSy.js → reveal-component-D7xeI1QR.js} +1003 -1003
- package/dist/{select-OxgozOC8.js → select-DsoP1qe8.js} +290 -290
- package/dist/{sequenceDiagram-WJ2MYXX4-DFX-bE6O.js → sequenceDiagram-WJ2MYXX4-oP9shbaG.js} +1 -1
- package/dist/{spec-oJn0RhpQ.js → spec-BZMDxAu7.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-BR7cI3wK.js → stateDiagram-D77RDMKH-CjCv4ogv.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-oGqB5UDI.js → stateDiagram-v2-MP3YSRHH-BW9X162a.js} +2 -2
- package/dist/{static-state-CuV6Pc3a.js → static-state-D-Q-Ajaa.js} +8 -8
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-DwSZl1yC.js → swimlanes-42K2YHIH-Ckfs9Wrn.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js → swimlanesDiagram-VR7AAH4N-DpTNG9YP.js} +2 -2
- package/dist/{swiper-component-Dvj-goaD.js → swiper-component-DQbK_ZWm.js} +7 -7
- package/dist/{timeline-definition-24CTP7MA-DMmP-G2O.js → timeline-definition-24CTP7MA-BXaBlV6U.js} +1 -1
- package/dist/{toast-DbBHmxmB.js → toast-BhjIy3Tq.js} +158 -158
- package/dist/{tooltip-BfcDQd2j.js → tooltip-DMMiOn2f.js} +64 -64
- package/dist/use-restore-focus-CosX7-gx.js +7568 -0
- package/dist/{useAsyncData-BYIAoMqn.js → useAsyncData-Ci4AvJff.js} +4 -4
- package/dist/{useDateFormatter-ButWkdcK.js → useDateFormatter-CPlava52.js} +2 -2
- package/dist/{useDeepCompareMemoize-Bz0c5GN6.js → useDeepCompareMemoize-NaaWoFlX.js} +1 -1
- package/dist/useEvent-BRH-aCqi.js +19 -0
- package/dist/{useIframeCapabilities-BIolGsF1.js → useIframeCapabilities-BXhDDoCG.js} +1 -1
- package/dist/{useLifecycle-JjxCC7Vu.js → useLifecycle-SniivYmC.js} +2 -2
- package/dist/{vega-component-rxIxAAh9.js → vega-component-DIxaDQNs.js} +40 -40
- package/dist/{vennDiagram-4TSXK5OY-Db8GtH1G.js → vennDiagram-4TSXK5OY-C-hQ4iNJ.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-DmIiC3FU.js → wardleyDiagram-VM6X3IG4-C2MQ_Umv.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DS9OPrNq.js → xychartDiagram-S5SC5T6Z-BtTTBn7Q.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/executions.ts +4 -2
- package/src/__mocks__/notebook.ts +0 -2
- package/src/__tests__/CellStatus.test.tsx +6 -6
- package/src/components/editor/__tests__/data-attributes.test.tsx +0 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +20 -5
- package/src/components/editor/cell/CellStatus.tsx +6 -5
- package/src/components/editor/cell/__tests__/useRunCells.test.tsx +169 -0
- package/src/components/editor/cell/collapse.tsx +3 -4
- package/src/components/editor/cell/useRunCells.ts +32 -3
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +2 -3
- package/src/components/editor/notebook-cell.tsx +23 -37
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +4 -10
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +8 -5
- package/src/components/slides/minimap.tsx +3 -2
- package/src/components/slides/reveal-component.tsx +3 -2
- package/src/components/slides/slide-cell-view.tsx +8 -8
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -101
- package/src/core/cells/__tests__/actions.test.ts +0 -1
- package/src/core/cells/__tests__/cell.test.ts +81 -17
- package/src/core/cells/__tests__/cells.test.ts +0 -100
- package/src/core/cells/__tests__/document.test.ts +2 -8
- package/src/core/cells/__tests__/utils.test.ts +93 -24
- package/src/core/cells/actions.ts +0 -1
- package/src/core/cells/cell.ts +37 -14
- package/src/core/cells/cells.ts +10 -99
- package/src/core/cells/document.ts +2 -13
- package/src/core/cells/types.ts +0 -23
- package/src/core/cells/utils.ts +44 -17
- package/src/core/codemirror/cells/extensions.ts +0 -14
- package/src/core/errors/__tests__/error-entries.test.ts +40 -1
- package/src/core/errors/error-entries.ts +2 -3
- package/src/core/executions/__tests__/freshness.test.ts +420 -0
- package/src/core/executions/__tests__/hooks.test.tsx +106 -0
- package/src/core/executions/atoms.ts +22 -0
- package/src/core/executions/freshness.ts +164 -0
- package/src/core/executions/hooks.ts +17 -1
- package/src/core/executions/index.ts +3 -1
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/islands/components/__tests__/web-components.test.tsx +63 -0
- package/src/core/islands/components/web-components.tsx +11 -0
- package/src/core/kernel/handlers.ts +0 -4
- package/src/core/session/useNotebookSession.tsx +2 -7
- package/src/css/globals.css +6 -2
- package/src/plugins/impl/DataEditorPlugin.tsx +0 -4
- package/src/plugins/impl/data-editor/__tests__/column-types.test.ts +5 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +9 -5
- package/src/plugins/impl/data-editor/column-types.ts +3 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +10 -32
- package/src/plugins/impl/data-editor/types.ts +0 -2
- package/src/stories/cell-status.stories.tsx +12 -14
- package/src/stories/cell.stories.tsx +44 -10
- package/src/stories/layout/vertical/one-column.stories.tsx +0 -10
- package/src/theme/__tests__/islands-theme.test.tsx +276 -0
- package/src/theme/namespace.tsx +4 -3
- package/src/theme/useTheme.ts +71 -41
- package/dist/schemas-CdscWNy_.js +0 -4747
- package/dist/use-restore-focus-CmflVLWJ.js +0 -1672
- package/dist/useTheme-DPLobSFs.js +0 -339
- package/dist/utils-EBklsC5R.js +0 -813
- package/src/components/editor/cell/__tests__/useRunCells.test.ts +0 -45
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether each cell's result still reflects the document and the values
|
|
5
|
+
* it read, by the host protocol's rules.
|
|
6
|
+
*
|
|
7
|
+
* A completed execution is current when its code fingerprint equals the
|
|
8
|
+
* cell's, every variable it depends on still holds the recorded revision,
|
|
9
|
+
* every name it depends on that was unbound is still unbound, and the
|
|
10
|
+
* executions that bound those variables are current. See the freshness
|
|
11
|
+
* section of `protocols/host/spec.md`.
|
|
12
|
+
*
|
|
13
|
+
* The protocol also ranks why a result is outdated. The editor asks two
|
|
14
|
+
* questions of its own, so that is what this reports; the ranked reason
|
|
15
|
+
* can come back with a reader. Two editor concerns are folded in: a name
|
|
16
|
+
* bound only by an earlier runtime is unbound in this one, and a queued or
|
|
17
|
+
* running execution for the cell's current source already covers it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
21
|
+
import type { Executions, Results } from "@/core/notebook/replica";
|
|
22
|
+
|
|
23
|
+
export interface CellFreshness {
|
|
24
|
+
/** This cell's source changed since its result. */
|
|
25
|
+
readonly sourceChanged: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* A value the result read changed since, by a rerun or an update, here
|
|
28
|
+
* or upstream. An ancestor that was only edited does not count: its
|
|
29
|
+
* values are still the ones this result read.
|
|
30
|
+
*/
|
|
31
|
+
readonly inputsChanged: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Current, and also a cell with no completed execution to compare. */
|
|
35
|
+
const CURRENT: CellFreshness = { sourceChanged: false, inputsChanged: false };
|
|
36
|
+
|
|
37
|
+
/** What freshness is derived from. */
|
|
38
|
+
export interface CellFreshnessInputs {
|
|
39
|
+
/** The document's cells, for the fingerprint of each cell's source. */
|
|
40
|
+
readonly cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>;
|
|
41
|
+
/** The stream's executions, for the variables currently bound. */
|
|
42
|
+
readonly executions: Executions;
|
|
43
|
+
/** Each cell's last completed record, the result being compared. */
|
|
44
|
+
readonly results: Results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Freshness by cell id, as `freshnessOf` builds it. */
|
|
48
|
+
export type CellFreshnessLookup = (id: Protocol.CellId) => CellFreshness;
|
|
49
|
+
|
|
50
|
+
/** The variables bound in the namespace right now, by name. */
|
|
51
|
+
function boundVariables(
|
|
52
|
+
executions: Executions,
|
|
53
|
+
): ReadonlyMap<string, Protocol.Variable[]> {
|
|
54
|
+
const byName = new Map<string, Protocol.Variable[]>();
|
|
55
|
+
for (const execution of executions.executions.values()) {
|
|
56
|
+
for (const variable of execution.variables.values()) {
|
|
57
|
+
const bound = byName.get(variable.name);
|
|
58
|
+
if (bound === undefined) {
|
|
59
|
+
byName.set(variable.name, [variable]);
|
|
60
|
+
} else {
|
|
61
|
+
bound.push(variable);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return byName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compares results against the document and the namespace.
|
|
70
|
+
*
|
|
71
|
+
* Build one per set of inputs and ask it about each cell, since the
|
|
72
|
+
* bound variables are indexed once and a binding execution's freshness is
|
|
73
|
+
* computed once however many results depend on it.
|
|
74
|
+
*/
|
|
75
|
+
export function freshnessOf(inputs: CellFreshnessInputs): CellFreshnessLookup {
|
|
76
|
+
const bound = boundVariables(inputs.executions);
|
|
77
|
+
const memo = new Map<Protocol.ExecutionId, CellFreshness>();
|
|
78
|
+
const recordOf = (id: Protocol.ExecutionId) =>
|
|
79
|
+
inputs.executions.executions.get(id)?.record;
|
|
80
|
+
|
|
81
|
+
/** Whether a value the record read has changed, or one upstream has. */
|
|
82
|
+
function inputsChanged(
|
|
83
|
+
record: Protocol.Execution,
|
|
84
|
+
visiting: ReadonlySet<Protocol.ExecutionId>,
|
|
85
|
+
): boolean {
|
|
86
|
+
const path = new Set(visiting).add(record.id);
|
|
87
|
+
for (const [name, revision] of Object.entries(record.depends_on)) {
|
|
88
|
+
const variables = bound.get(name) ?? [];
|
|
89
|
+
if (revision === null) {
|
|
90
|
+
// A result retained from an earlier runtime binds nothing here.
|
|
91
|
+
if (
|
|
92
|
+
variables.some(
|
|
93
|
+
(v) => recordOf(v.execution_id)?.runtime_id === record.runtime_id,
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const variable = variables.find(
|
|
101
|
+
(candidate) => candidate.revision.execution === revision.execution,
|
|
102
|
+
);
|
|
103
|
+
if (
|
|
104
|
+
variable === undefined ||
|
|
105
|
+
variable.revision.update !== revision.update
|
|
106
|
+
) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const binder = recordOf(variable.execution_id);
|
|
110
|
+
if (binder === undefined) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (!path.has(binder.id) && ofRecord(binder, path).inputsChanged) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether a newer execution of the cell already runs its current source. */
|
|
121
|
+
function coveredByPending(
|
|
122
|
+
cell: Protocol.Cell,
|
|
123
|
+
record: Protocol.Execution,
|
|
124
|
+
): boolean {
|
|
125
|
+
const latest = inputs.executions.latest.get(cell.id);
|
|
126
|
+
if (latest === undefined || latest === record.id) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const pending = recordOf(latest);
|
|
130
|
+
return (
|
|
131
|
+
pending !== undefined &&
|
|
132
|
+
pending.status !== "completed" &&
|
|
133
|
+
pending.code_fingerprint === cell.code_fingerprint
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ofRecord(
|
|
138
|
+
record: Protocol.Execution,
|
|
139
|
+
visiting: ReadonlySet<Protocol.ExecutionId>,
|
|
140
|
+
): CellFreshness {
|
|
141
|
+
const known = memo.get(record.id);
|
|
142
|
+
if (known !== undefined) {
|
|
143
|
+
return known;
|
|
144
|
+
}
|
|
145
|
+
const cell =
|
|
146
|
+
record.cell_id === null ? undefined : inputs.cells.get(record.cell_id);
|
|
147
|
+
const freshness: CellFreshness = {
|
|
148
|
+
sourceChanged:
|
|
149
|
+
cell === undefined ||
|
|
150
|
+
(cell.code_fingerprint !== record.code_fingerprint &&
|
|
151
|
+
!coveredByPending(cell, record)),
|
|
152
|
+
inputsChanged: inputsChanged(record, visiting),
|
|
153
|
+
};
|
|
154
|
+
memo.set(record.id, freshness);
|
|
155
|
+
return freshness;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (id) => {
|
|
159
|
+
const result = inputs.results.get(id);
|
|
160
|
+
return result === null || result === undefined
|
|
161
|
+
? CURRENT
|
|
162
|
+
: ofRecord(result, new Set());
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -5,7 +5,8 @@ import { dequal } from "dequal";
|
|
|
5
5
|
import { useAtomValue } from "jotai";
|
|
6
6
|
import { selectAtom } from "jotai/utils";
|
|
7
7
|
import { useMemo } from "react";
|
|
8
|
-
import { cellExecutionInputsAtom } from "./atoms";
|
|
8
|
+
import { cellExecutionInputsAtom, cellFreshnessAtom } from "./atoms";
|
|
9
|
+
import type { CellFreshness } from "./freshness";
|
|
9
10
|
import { type CellExecutionView, cellExecutionView } from "./select";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -29,3 +30,18 @@ export function useCellExecution(id: Protocol.CellId): CellExecutionView {
|
|
|
29
30
|
);
|
|
30
31
|
return useAtomValue(cellAtom);
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a cell's result is current, by the host protocol's rules over
|
|
36
|
+
* the confirmed document and the executions mirror.
|
|
37
|
+
*
|
|
38
|
+
* The confirmed document is what the host has accepted, so an edit reads
|
|
39
|
+
* as such once the model has flushed it, not on the keystroke.
|
|
40
|
+
*/
|
|
41
|
+
export function useCellFreshness(id: Protocol.CellId): CellFreshness {
|
|
42
|
+
const cellAtom = useMemo(
|
|
43
|
+
() => selectAtom(cellFreshnessAtom, (freshness) => freshness(id), dequal),
|
|
44
|
+
[id],
|
|
45
|
+
);
|
|
46
|
+
return useAtomValue(cellAtom);
|
|
47
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
export {
|
|
12
12
|
cellExecutionInputsAtom,
|
|
13
|
+
cellFreshnessAtom,
|
|
13
14
|
notebookIsRunningAtom,
|
|
14
15
|
notebookQueuedOrRunningCountAtom,
|
|
15
16
|
observeCellMessage,
|
|
@@ -18,7 +19,8 @@ export {
|
|
|
18
19
|
waitForRuntimeRunning,
|
|
19
20
|
withdrawCells,
|
|
20
21
|
} from "./atoms";
|
|
21
|
-
export {
|
|
22
|
+
export type { CellFreshness, CellFreshnessLookup } from "./freshness";
|
|
23
|
+
export { useCellExecution, useCellFreshness } from "./hooks";
|
|
22
24
|
export {
|
|
23
25
|
type CellExecutionInputs,
|
|
24
26
|
type CellExecutionView,
|
|
@@ -221,7 +221,6 @@ function handleNotification(
|
|
|
221
221
|
handleKernelReady(msg, {
|
|
222
222
|
autoInstantiate: true,
|
|
223
223
|
cells: notebookCells(store.get(notebookAtom)),
|
|
224
|
-
setLastRun: actions.setLastRun,
|
|
225
224
|
resetCellRuntimes: actions.resetCellRuntimes,
|
|
226
225
|
setLayoutData: Functions.NOOP,
|
|
227
226
|
setCapabilities: Functions.NOOP,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
describe,
|
|
10
10
|
expect,
|
|
11
11
|
it,
|
|
12
|
+
vi,
|
|
12
13
|
} from "vitest";
|
|
13
14
|
import { cellId } from "@/__tests__/branded";
|
|
14
15
|
import { MockNotebook } from "@/__mocks__/notebook";
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
|
|
23
24
|
import { requestClientAtom } from "@/core/network/requests";
|
|
24
25
|
import { store } from "@/core/state/jotai";
|
|
26
|
+
import { buildIslandHTML } from "../../__tests__/test-utils";
|
|
25
27
|
import { parseMarimoIslandApps } from "../../parse";
|
|
26
28
|
import { MarimoIslandElement } from "../web-components";
|
|
27
29
|
|
|
@@ -45,9 +47,55 @@ beforeEach(() => {
|
|
|
45
47
|
afterEach(async () => {
|
|
46
48
|
await actAndFlush(() => document.body.replaceChildren());
|
|
47
49
|
store.set(requestClientAtom, null);
|
|
50
|
+
document.body.removeAttribute("style");
|
|
51
|
+
vi.unstubAllEnvs();
|
|
48
52
|
});
|
|
49
53
|
|
|
50
54
|
describe("MarimoIslandElement lifecycle", () => {
|
|
55
|
+
it.each([true, false])(
|
|
56
|
+
"synchronizes the theme of a reactive=%s island across reconnects",
|
|
57
|
+
async (reactive) => {
|
|
58
|
+
vi.stubEnv("VITE_MARIMO_ISLANDS", "true");
|
|
59
|
+
if (reactive) {
|
|
60
|
+
setNotebook("runtime-cell", "runtime output");
|
|
61
|
+
}
|
|
62
|
+
const { container, island } = createThemeIsland(reactive);
|
|
63
|
+
document.body.style.colorScheme = "dark";
|
|
64
|
+
await actAndFlush(() => document.body.append(container));
|
|
65
|
+
expect(island).toHaveClass("marimo", "dark");
|
|
66
|
+
await actAndFlush(() => {
|
|
67
|
+
document.body.style.colorScheme = "light";
|
|
68
|
+
});
|
|
69
|
+
expect(island).toHaveClass("marimo", "light");
|
|
70
|
+
expect(island).not.toHaveClass("dark");
|
|
71
|
+
await actAndFlush(() => island.remove());
|
|
72
|
+
await actAndFlush(() => {
|
|
73
|
+
document.body.style.colorScheme = "dark";
|
|
74
|
+
});
|
|
75
|
+
expect(island).toHaveClass("light");
|
|
76
|
+
await actAndFlush(() => container.append(island));
|
|
77
|
+
expect(island).toHaveClass("dark");
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it("can disconnect before its scheduled render and reconnect with the current theme", async () => {
|
|
82
|
+
vi.stubEnv("VITE_MARIMO_ISLANDS", "true");
|
|
83
|
+
const { container, island } = createThemeIsland(false);
|
|
84
|
+
document.body.style.colorScheme = "dark";
|
|
85
|
+
await actAndFlush(() => {
|
|
86
|
+
document.body.append(container);
|
|
87
|
+
island.remove();
|
|
88
|
+
});
|
|
89
|
+
expect(island.querySelector("marimo-cell-output")).not.toBeNull();
|
|
90
|
+
await actAndFlush(() => {
|
|
91
|
+
document.body.style.colorScheme = "light";
|
|
92
|
+
});
|
|
93
|
+
expect(island).toHaveClass("dark");
|
|
94
|
+
await actAndFlush(() => container.append(island));
|
|
95
|
+
expect(island).toHaveClass("light");
|
|
96
|
+
expect(island.textContent).toContain("static output");
|
|
97
|
+
});
|
|
98
|
+
|
|
51
99
|
it("keeps static output until the initial run completes", async () => {
|
|
52
100
|
store.set(islandsPendingInitialRunsAtom, new Set([1]));
|
|
53
101
|
setNotebook("runtime-cell", "runtime output");
|
|
@@ -182,6 +230,21 @@ describe("MarimoIslandElement lifecycle", () => {
|
|
|
182
230
|
});
|
|
183
231
|
});
|
|
184
232
|
|
|
233
|
+
function createThemeIsland(reactive: boolean) {
|
|
234
|
+
const container = document.createElement("div");
|
|
235
|
+
container.innerHTML = buildIslandHTML([
|
|
236
|
+
{
|
|
237
|
+
appId: "app-1",
|
|
238
|
+
cellId: "runtime-cell",
|
|
239
|
+
reactive,
|
|
240
|
+
code: "x = 1",
|
|
241
|
+
output: "<pre>static output</pre>",
|
|
242
|
+
},
|
|
243
|
+
]);
|
|
244
|
+
const island = container.querySelector<HTMLElement>(ISLAND_TAG_NAMES.ISLAND)!;
|
|
245
|
+
return { container, island };
|
|
246
|
+
}
|
|
247
|
+
|
|
185
248
|
function setNotebook(id: string, output?: string): void {
|
|
186
249
|
const runtimeCellId = cellId(id);
|
|
187
250
|
store.set(
|
|
@@ -10,6 +10,7 @@ import { OBJECT_ID_ATTR } from "@/core/dom/ui-element-constants";
|
|
|
10
10
|
import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
|
|
11
11
|
import { LocaleProvider } from "@/core/i18n/locale-provider";
|
|
12
12
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
13
|
+
import { resolvedThemeAtom } from "@/theme/useTheme";
|
|
13
14
|
import { invariant } from "@/utils/invariant";
|
|
14
15
|
import type { CellId, UIElementId } from "../../cells/ids";
|
|
15
16
|
import { store } from "../../state/jotai";
|
|
@@ -44,6 +45,7 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
44
45
|
private runtimeAppId?: string;
|
|
45
46
|
private runtimeCellId?: CellId;
|
|
46
47
|
private unsubscribeCellIds?: () => void;
|
|
48
|
+
private unsubscribeTheme?: () => void;
|
|
47
49
|
|
|
48
50
|
public static readonly tagName = ISLAND_TAG_NAMES.ISLAND;
|
|
49
51
|
public static readonly outputTagName = ISLAND_TAG_NAMES.CELL_OUTPUT;
|
|
@@ -132,6 +134,13 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
132
134
|
? this.cellId
|
|
133
135
|
: undefined;
|
|
134
136
|
this.syncCellIdsSubscription();
|
|
137
|
+
const syncTheme = () => {
|
|
138
|
+
const theme = store.get(resolvedThemeAtom);
|
|
139
|
+
this.classList.toggle("dark", theme === "dark");
|
|
140
|
+
this.classList.toggle("light", theme === "light");
|
|
141
|
+
};
|
|
142
|
+
this.unsubscribeTheme = store.sub(resolvedThemeAtom, syncTheme);
|
|
143
|
+
syncTheme();
|
|
135
144
|
queueMicrotask(() => {
|
|
136
145
|
// Guard against disconnect between connectedCallback and microtask
|
|
137
146
|
if (
|
|
@@ -297,6 +306,8 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
297
306
|
* Cleanup when element is removed from DOM
|
|
298
307
|
*/
|
|
299
308
|
public disconnectedCallback(): void {
|
|
309
|
+
this.unsubscribeTheme?.();
|
|
310
|
+
this.unsubscribeTheme = undefined;
|
|
300
311
|
this.removeEventListener(
|
|
301
312
|
ISLAND_SOURCE_CHANGED_EVENT,
|
|
302
313
|
this.handleSourceChanged,
|
|
@@ -60,7 +60,6 @@ export function handleKernelReady(
|
|
|
60
60
|
autoInstantiate: boolean;
|
|
61
61
|
/** The notebook's cells, as the host document provided them. */
|
|
62
62
|
cells: CellData[];
|
|
63
|
-
setLastRun: ReducerCellActions["setLastRun"];
|
|
64
63
|
resetCellRuntimes: ReducerCellActions["resetCellRuntimes"];
|
|
65
64
|
setLayoutData: (payload: {
|
|
66
65
|
layoutView: LayoutType;
|
|
@@ -73,7 +72,6 @@ export function handleKernelReady(
|
|
|
73
72
|
const {
|
|
74
73
|
autoInstantiate,
|
|
75
74
|
cells,
|
|
76
|
-
setLastRun,
|
|
77
75
|
resetCellRuntimes,
|
|
78
76
|
setLayoutData,
|
|
79
77
|
setCapabilities,
|
|
@@ -82,8 +80,6 @@ export function handleKernelReady(
|
|
|
82
80
|
const { resumed, ui_values, capabilities, auto_instantiated } = data;
|
|
83
81
|
|
|
84
82
|
resetCellRuntimes();
|
|
85
|
-
// TODO(MO-8403): Delete with `lastCodeRun`.
|
|
86
|
-
setLastRun({ lastCodeRun: data.last_executed_code ?? {} });
|
|
87
83
|
const layoutState = buildLayoutState(data, cells, setLayoutData);
|
|
88
84
|
|
|
89
85
|
setCapabilities(capabilities);
|
|
@@ -92,12 +92,8 @@ export function useNotebookSession(opts: {
|
|
|
92
92
|
const { showBoundary } = useErrorBoundary();
|
|
93
93
|
const store = useStore();
|
|
94
94
|
|
|
95
|
-
const {
|
|
96
|
-
|
|
97
|
-
reconcileDocument,
|
|
98
|
-
setLastRun,
|
|
99
|
-
resetCellRuntimes,
|
|
100
|
-
} = useReducerActions();
|
|
95
|
+
const { handleCellMessage, reconcileDocument, resetCellRuntimes } =
|
|
96
|
+
useReducerActions();
|
|
101
97
|
const { reconcileStagedCells } = useStagedAICellsActions();
|
|
102
98
|
const { addCellNotification } = useRunsActions();
|
|
103
99
|
const setInitialRunCompleted = useSetAtom(initialRunCompletedAtom);
|
|
@@ -154,7 +150,6 @@ export function useNotebookSession(opts: {
|
|
|
154
150
|
const layout = handleKernelReady(msg.data, {
|
|
155
151
|
autoInstantiate,
|
|
156
152
|
cells: notebookCells(store.get(notebookAtom)),
|
|
157
|
-
setLastRun,
|
|
158
153
|
resetCellRuntimes,
|
|
159
154
|
setLayoutData,
|
|
160
155
|
setCapabilities,
|
package/src/css/globals.css
CHANGED
|
@@ -128,14 +128,18 @@
|
|
|
128
128
|
--cm-search-match-selected-background-dark: #6199ff88;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
.marimo.light {
|
|
132
|
+
color-scheme: light;
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
:root:has(body.dark),
|
|
132
136
|
.dark,
|
|
133
|
-
.marimo:is(.dark *) {
|
|
137
|
+
.marimo:not(.light):is(.dark *) {
|
|
134
138
|
color-scheme: dark;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
.dark,
|
|
138
|
-
.marimo:is(.dark *) {
|
|
142
|
+
.marimo:not(.light):is(.dark *) {
|
|
139
143
|
--ring: var(--blue-8, #2870bd);
|
|
140
144
|
--destructive: var(--red-6, #72232d);
|
|
141
145
|
--destructive-hover: var(--red-7, #8c333a);
|
|
@@ -92,13 +92,9 @@ interface Props {
|
|
|
92
92
|
const LoadingDataEditor = (props: Props) => {
|
|
93
93
|
const [editorState, setEditorState] = useState<EditorState | null>(null);
|
|
94
94
|
|
|
95
|
-
// Load the data
|
|
96
95
|
const { data: loadedState, error } = useAsyncData(async () => {
|
|
97
96
|
const withoutExternalTypes = toFieldTypes(props.fieldTypes ?? []);
|
|
98
97
|
|
|
99
|
-
// If we already have the data, return it
|
|
100
|
-
// Otherwise, load the data from the URL. Vega's CSV parser takes a
|
|
101
|
-
// plain `Record`; column order doesn't matter for parsing.
|
|
102
98
|
const localData = Array.isArray(props.data)
|
|
103
99
|
? props.data
|
|
104
100
|
: await vegaLoadData<EditorRow>(
|
|
@@ -10,6 +10,11 @@ describe("normalizeConfiguredColumns", () => {
|
|
|
10
10
|
vi.restoreAllMocks();
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
it("preserves the data reference when no columns are configured", () => {
|
|
14
|
+
const data = [{ label: "01", done: "yes" }];
|
|
15
|
+
expect(normalizeConfiguredColumns(data, new Map())).toBe(data);
|
|
16
|
+
});
|
|
17
|
+
|
|
13
18
|
it("normalizes large integer boolean values from the CSV loader", async () => {
|
|
14
19
|
vi.spyOn(vegaLoader, "load").mockResolvedValueOnce(
|
|
15
20
|
"done,label\n9007199254740992,01\n-9007199254740992,02\n0,03\n",
|
|
@@ -65,7 +65,11 @@ vi.mock("@glideapps/glide-data-grid", async () => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
vi.mock("@glideapps/glide-data-grid-cells", () => ({
|
|
68
|
-
DropdownCell: {
|
|
68
|
+
DropdownCell: {
|
|
69
|
+
kind: "dropdown-renderer",
|
|
70
|
+
isMatch: (cell: { data: { kind: string } }) =>
|
|
71
|
+
cell.data.kind === "dropdown-cell",
|
|
72
|
+
},
|
|
69
73
|
}));
|
|
70
74
|
|
|
71
75
|
vi.mock("@/theme/useTheme", () => ({
|
|
@@ -74,11 +78,11 @@ vi.mock("@/theme/useTheme", () => ({
|
|
|
74
78
|
|
|
75
79
|
const editorProps = {
|
|
76
80
|
data: [{ name: "alice" }],
|
|
77
|
-
columnFields: new Map([["name", "string"]])
|
|
78
|
-
editableColumns: "all"
|
|
81
|
+
columnFields: new Map([["name", "string"]]),
|
|
82
|
+
editableColumns: "all",
|
|
79
83
|
columnTypes: new Map(),
|
|
80
84
|
onAddEdits: vi.fn(),
|
|
81
|
-
}
|
|
85
|
+
} satisfies React.ComponentProps<typeof GlideDataEditor>;
|
|
82
86
|
|
|
83
87
|
describe("GlideDataEditor portal", () => {
|
|
84
88
|
let fullscreenElement: Element | null;
|
|
@@ -148,7 +152,7 @@ describe("GlideDataEditor portal", () => {
|
|
|
148
152
|
},
|
|
149
153
|
});
|
|
150
154
|
expect(capturedDataEditor.customRenderers).toEqual([
|
|
151
|
-
{ kind: "dropdown-renderer" },
|
|
155
|
+
{ kind: "dropdown-renderer", isMatch: expect.any(Function) },
|
|
152
156
|
]);
|
|
153
157
|
});
|
|
154
158
|
|
|
@@ -33,6 +33,9 @@ export function normalizeConfiguredColumns(
|
|
|
33
33
|
data: EditorRow[],
|
|
34
34
|
columnTypes: ColumnTypes,
|
|
35
35
|
): EditorRow[] {
|
|
36
|
+
if (columnTypes.size === 0) {
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
36
39
|
return data.map((row) => {
|
|
37
40
|
const normalizedRow = { ...row };
|
|
38
41
|
for (const [columnName, columnType] of columnTypes) {
|
|
@@ -55,11 +55,13 @@ import {
|
|
|
55
55
|
} from "./types";
|
|
56
56
|
|
|
57
57
|
function getEditedValue(newValue: EditableGridCell): unknown {
|
|
58
|
-
if (
|
|
59
|
-
|
|
58
|
+
if (
|
|
59
|
+
newValue.kind === GridCellKind.Custom &&
|
|
60
|
+
DropdownCellRenderer.isMatch(newValue)
|
|
61
|
+
) {
|
|
62
|
+
return newValue.data.value;
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
return data.kind === "dropdown-cell" ? data.value : newValue.data;
|
|
64
|
+
return newValue.data;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
interface GlideDataEditorProps {
|
|
@@ -190,7 +192,6 @@ export const GlideDataEditor = ({
|
|
|
190
192
|
const key = column.title;
|
|
191
193
|
|
|
192
194
|
// Deletes are not handled by validateCell, so we need to handle them here.
|
|
193
|
-
// Custom dropdown cells carry their selected value inside their data.
|
|
194
195
|
let newData = getEditedValue(newValue);
|
|
195
196
|
if (Array.isArray(column.configuredType)) {
|
|
196
197
|
if (
|
|
@@ -132,7 +132,6 @@ export function pasteCells(options: {
|
|
|
132
132
|
const { range } = selection.current;
|
|
133
133
|
const { x: startCol, y: startRow } = range;
|
|
134
134
|
|
|
135
|
-
// Read clipboard data
|
|
136
135
|
navigator.clipboard
|
|
137
136
|
.readText()
|
|
138
137
|
.then((clipboardText) => {
|
|
@@ -140,43 +139,25 @@ export function pasteCells(options: {
|
|
|
140
139
|
return;
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for (const row of rows) {
|
|
148
|
-
const cells = row.split("\t");
|
|
149
|
-
parsedData.push(cells);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (parsedData.length === 0) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
142
|
+
const parsedData = clipboardText
|
|
143
|
+
.split("\n")
|
|
144
|
+
.filter((row) => row.trim())
|
|
145
|
+
.map((row) => row.split("\t"));
|
|
155
146
|
|
|
156
147
|
const edits: Edits["edits"] = [];
|
|
157
148
|
const editableColumnSet =
|
|
158
149
|
editableColumns === "all" ? null : new Set(editableColumns);
|
|
159
150
|
|
|
160
151
|
for (const [rowIndex, dataRow] of parsedData.entries()) {
|
|
161
|
-
if (!dataRow) {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
152
|
const targetRowIdx = startRow + rowIndex;
|
|
166
153
|
|
|
167
|
-
// Check if we've exceeded the data bounds
|
|
168
154
|
if (targetRowIdx >= data.length) {
|
|
169
155
|
break;
|
|
170
156
|
}
|
|
171
157
|
|
|
172
158
|
for (const [colIndex, cellValue] of dataRow.entries()) {
|
|
173
|
-
if (cellValue === undefined) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
159
|
const targetColIdx = startCol + colIndex;
|
|
178
160
|
|
|
179
|
-
// Check if we've exceeded the column bounds
|
|
180
161
|
if (targetColIdx >= columns.length) {
|
|
181
162
|
break;
|
|
182
163
|
}
|
|
@@ -191,13 +172,13 @@ export function pasteCells(options: {
|
|
|
191
172
|
continue;
|
|
192
173
|
}
|
|
193
174
|
|
|
194
|
-
// Convert the value based on the cell type
|
|
195
175
|
let convertedValue: unknown = cellValue;
|
|
196
176
|
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
177
|
+
if (
|
|
178
|
+
Array.isArray(columnType) &&
|
|
179
|
+
!isValidCellValue(dataType, cellValue, columnType)
|
|
180
|
+
) {
|
|
181
|
+
continue;
|
|
201
182
|
}
|
|
202
183
|
|
|
203
184
|
switch (dataType) {
|
|
@@ -243,12 +224,9 @@ export function pasteCells(options: {
|
|
|
243
224
|
continue;
|
|
244
225
|
}
|
|
245
226
|
|
|
246
|
-
// Get the column ID from the columns array using the title
|
|
247
|
-
const columnId = columns[targetColIdx].title;
|
|
248
|
-
|
|
249
227
|
edits.push({
|
|
250
228
|
rowIdx: targetRowIdx,
|
|
251
|
-
columnId,
|
|
229
|
+
columnId: column.title,
|
|
252
230
|
value: convertedValue,
|
|
253
231
|
});
|
|
254
232
|
}
|
|
@@ -6,8 +6,6 @@ import type { DataType } from "@/core/kernel/messages";
|
|
|
6
6
|
|
|
7
7
|
export type EditorRow = Record<string, unknown>;
|
|
8
8
|
|
|
9
|
-
// Shared transport for specialized column editors: dropdown columns use a
|
|
10
|
-
// string array, while checkbox columns use the boolean discriminator.
|
|
11
9
|
export type ColumnType = "boolean" | string[];
|
|
12
10
|
export type ColumnTypes = Map<string, ColumnType>;
|
|
13
11
|
|