@zikojs/notebook 0.8.0 → 0.10.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zikojs/notebook",
3
- "version": "0.8.0",
4
- "description": "",
3
+ "version": "0.10.0",
4
+ "description": "A lightweight, extensible JavaScript notebook component with Markdown, executable cells, persistent state, and pluggable runtimes.",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -44,6 +44,7 @@
44
44
  "@codemirror/state": "^6.7.4",
45
45
  "@codemirror/view": "^6.43.11",
46
46
  "@zikojs/lucide": "^0.2.2",
47
+ "@zikojs/console": "latest",
47
48
  "acorn": "^8.18.0",
48
49
  "codemirror": "^6.0.2",
49
50
  "lucide": "^1.44.0",
@@ -52,7 +53,7 @@
52
53
  "remark-rehype": "^11.1.2",
53
54
  "vanjs-core": "^1.6.1",
54
55
  "zextra": "^1.15.0",
55
- "ziko": "^2.0.0-beta.16.0"
56
+ "ziko": "latest"
56
57
  },
57
58
  "scripts": {
58
59
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -1,6 +1,4 @@
1
- import {
2
- transformImportsAndScope,
3
- } from "./transform.js";
1
+ import { transformImportsAndScope } from "./transform.js";
4
2
 
5
3
  export const evaluateCodeAsync = async (
6
4
  code,
@@ -8,6 +6,7 @@ export const evaluateCodeAsync = async (
8
6
  {
9
7
  importMap = {},
10
8
  plugins = [],
9
+ runtime = null,
11
10
  } = {},
12
11
  scope,
13
12
  ) => {
@@ -19,11 +18,34 @@ export const evaluateCodeAsync = async (
19
18
  scope: validScope,
20
19
  importMap,
21
20
  plugins,
21
+ runtime,
22
22
  };
23
23
 
24
+ const selectedPlugin = plugins.find(
25
+ p => p.name === runtime,
26
+ );
27
+
28
+ // Runtime-specific evaluator
29
+ if (
30
+ selectedPlugin &&
31
+ selectedPlugin.evaluate
32
+ ) {
33
+ return await selectedPlugin.evaluate(
34
+ code,
35
+ context,
36
+ );
37
+ }
38
+
24
39
  let compiledCode = code;
25
40
 
26
41
  for (const plugin of plugins) {
42
+ if (
43
+ selectedPlugin &&
44
+ plugin.name !== runtime
45
+ ) {
46
+ continue;
47
+ }
48
+
27
49
  if (plugin.transform) {
28
50
  compiledCode = await plugin.transform(
29
51
  compiledCode,
@@ -32,20 +54,29 @@ export const evaluateCodeAsync = async (
32
54
  }
33
55
  }
34
56
 
35
- compiledCode = transformImportsAndScope(
36
- compiledCode,
37
- importMap,
38
- );
57
+ compiledCode =
58
+ transformImportsAndScope(
59
+ compiledCode,
60
+ importMap,
61
+ );
39
62
 
40
63
  const runCell = new Function(
41
64
  "TARGET",
42
65
  "__scope__",
43
66
  `
44
- return (async () => {
45
- with (__scope__) {
67
+ // Bind the active TARGET for this cell evaluation
68
+ if (typeof __Ziko__ !== "undefined" && __Ziko__.__Config__) {
69
+ __Ziko__.__Config__.default.target = TARGET;
70
+ __Ziko__.__Config__.default.autoMount = true;
71
+ }
72
+
73
+ return (async () => {
74
+ with (__scope__) {
75
+ return await (async () => {
46
76
  ${compiledCode}
47
- }
48
- })();
77
+ })();
78
+ }
79
+ })();
49
80
  `,
50
81
  );
51
82
 
@@ -55,6 +86,13 @@ export const evaluateCodeAsync = async (
55
86
  );
56
87
 
57
88
  for (const plugin of plugins) {
89
+ if (
90
+ selectedPlugin &&
91
+ plugin.name !== runtime
92
+ ) {
93
+ continue;
94
+ }
95
+
58
96
  if (plugin.afterEvaluate) {
59
97
  result = await plugin.afterEvaluate(
60
98
  result,
@@ -29,13 +29,21 @@ export function CellItem({ cellData, app, store }) {
29
29
  const actions = app._cellActions(cellData);
30
30
  cellData._actions = actions;
31
31
  cellData._editor = createCodeEditor(app, cellData, actions);
32
+
32
33
  /* Check ziko */
33
- const [In, setIn] = useState(0)
34
- globalThis.setIn = setIn
34
+ const [In, setIn] = useState(0);
35
+ globalThis.setIn = setIn;
35
36
  const inPrompt = span({ class: "prompt" }, In);
36
37
  const outPrompt = span({ class: "prompt out" });
37
- const bodySlot = div().style({ display : 'contents'});
38
- const controlsEl = CellControls({ cellData, store, actions });
38
+ const bodySlot = div().style({ display: 'contents' });
39
+
40
+ // Pass runtimePlugins from app into CellControls
41
+ const controlsEl = CellControls({
42
+ cellData,
43
+ store,
44
+ actions,
45
+ runtimePlugins: app.runtimePlugins
46
+ });
39
47
 
40
48
  const outputGrid = div({ class: "cell-output-grid" }, outPrompt, div({ class: "output-wrapper" }, outputRef));
41
49
  const inputGrid = div({ class: "cell-input-grid" }, inPrompt, bodySlot, controlsEl);
@@ -47,13 +55,12 @@ export function CellItem({ cellData, app, store }) {
47
55
  cellData._outPrompt = outPrompt.element;
48
56
  cellData._bodySlot = bodySlot.element;
49
57
  cellData._outputGrid = outputGrid.element;
50
- cellData._setIn = setIn
58
+ cellData._setIn = setIn;
51
59
 
52
60
  app._refreshCell(cellData);
53
61
  return root.element;
54
62
  }
55
-
56
- export function CellControls({ cellData, store, actions }) {
63
+ export function CellControls({ cellData, store, actions, runtimePlugins = [] }) {
57
64
  const controlsContainer = div({ class: "cell-controls" }).element;
58
65
 
59
66
  const renderButtons = () => {
@@ -61,7 +68,38 @@ export function CellControls({ cellData, store, actions }) {
61
68
  const isMaxCellsReached = store.get('cells').length >= store.get('maxCells');
62
69
  const isCopied = cellData._copiedFlag === true;
63
70
 
64
- controlsContainer.replaceChildren(
71
+ // Build the runtime selector dropdown for code cells using native DOM options
72
+ let runtimeSelectEl = null;
73
+ if (cellData.type === "code") {
74
+ const selectElement = document.createElement("select");
75
+ selectElement.className = "runtime-select";
76
+ selectElement.title = "Select Cell Runtime";
77
+ selectElement.disabled = cellData.readonly;
78
+
79
+ selectElement.onchange = (e) => {
80
+ actions.setRuntime(e.target.value === "" ? null : e.target.value);
81
+ };
82
+
83
+ // Default JS option
84
+ const optJs = document.createElement("option");
85
+ optJs.value = "";
86
+ optJs.textContent = "JS";
87
+ selectElement.appendChild(optJs);
88
+
89
+ // Runtime plugin options
90
+ for (const p of runtimePlugins) {
91
+ const opt = document.createElement("option");
92
+ opt.value = p.name;
93
+ opt.textContent = p.name.charAt(0).toUpperCase() + p.name.slice(1);
94
+ selectElement.appendChild(opt);
95
+ }
96
+
97
+ // Explicitly set the active value
98
+ selectElement.value = cellData.runtime || "";
99
+ runtimeSelectEl = selectElement;
100
+ }
101
+
102
+ const children = [
65
103
  Button({
66
104
  class: `btn-icon ${isCopied ? "btn-active" : ""}`,
67
105
  title: isCopied ? "Copied!" : "Copy Cell Content",
@@ -117,7 +155,13 @@ export function CellControls({ cellData, store, actions }) {
117
155
  disabled: isQueueRunning,
118
156
  onclick: actions.deleteCell
119
157
  }, Trash2())
120
- );
158
+ ];
159
+
160
+ if (runtimeSelectEl) {
161
+ children.unshift(runtimeSelectEl);
162
+ }
163
+
164
+ controlsContainer.replaceChildren(...children);
121
165
  };
122
166
 
123
167
  renderButtons();
@@ -1,4 +1,4 @@
1
- import van from "vanjs-core";
1
+ import van from 'vanjs-core';
2
2
 
3
3
  import {
4
4
  FileText,
@@ -16,20 +16,20 @@ import { tags } from 'ziko/dom'
16
16
 
17
17
  const { div, span } = tags
18
18
 
19
- export const Button = ({ class: className = "", title = "", disabled = false, onclick }, ...children) =>
19
+ export const Button = ({ class: className = '', title = '', disabled = false, onclick }, ...children) =>
20
20
  button({ class: `btn ${className}`, title, disabled, onclick }, ...children);
21
21
 
22
22
  export function Header({ store, actions }) {
23
- const statusBadge = span({ class: "status-badge" }, Loader2(), span('Loading ...'));
24
- const runAllBtn = Button({ class: "btn-primary", onclick: actions.runAll }, PlayForward(), "Run All");
25
- const clearBtn = Button({ onclick: actions.clearOutputs }, Eraser(), "Clear Outputs");
26
- const addCodeBtn = Button({ onclick: actions.addCode }, Plus(), "Add Code");
27
- const addMdBtn = Button({ onclick: actions.addMarkdown }, FileText(), "Add Markdown");
28
- const deleteActiveBtn = Button({ onclick: actions.deleteActive }, Trash2(), "Delete Active");
29
- const exportBtn = Button({ onclick: actions.exportData }, Download(), "Export Data");
23
+ const statusBadge = span({ class: 'status-badge' }, Loader2(), span('Loading ...'));
24
+ const runAllBtn = Button({ class: 'btn-primary', onclick: actions.runAll, title : 'Run All' }, PlayForward());
25
+ const clearBtn = Button({ onclick: actions.clearOutputs, title : 'Clear Outputs' }, Eraser());
26
+ const addCodeBtn = Button({ onclick: actions.addCode, title : 'Add Code'}, Plus());
27
+ const addMdBtn = Button({ onclick: actions.addMarkdown, title : 'Add Markdown' }, FileText());
28
+ const deleteActiveBtn = Button({ onclick: actions.deleteActive, title : 'Delete Active cell' }, Trash2());
29
+ const exportBtn = Button({ onclick: actions.exportData, title : 'Export Data' }, Download());
30
30
 
31
31
  store.on('change:executionQueueState', (q) => {
32
- statusBadge.style({display :q.isRunning ? "inline-flex" : "none" })
32
+ statusBadge.style({display :q.isRunning ? 'inline-flex' : 'none' })
33
33
  statusBadge.element.lastChild.textContent = `Executing Queue (${q.currentIndex + 1}/${q.total})`;
34
34
  [runAllBtn, clearBtn, deleteActiveBtn].forEach(b => { b.disabled = q.isRunning; });
35
35
  const maxReached = store.get('cells').length >= store.get('maxCells');
@@ -37,9 +37,9 @@ export function Header({ store, actions }) {
37
37
  addMdBtn.disabled = q.isRunning || maxReached;
38
38
  });
39
39
 
40
- return div({ class: "header" },
41
- div({ class: "brand" }, "</>", statusBadge),
42
- div({ class: "toolbar" },
40
+ return div({ class: 'header' },
41
+ // div({ class: 'brand' }, '</>', statusBadge),
42
+ div({ class: 'toolbar' },
43
43
  runAllBtn, clearBtn, addCodeBtn, addMdBtn, deleteActiveBtn, exportBtn
44
44
  )
45
45
  ).element;
@@ -2,27 +2,132 @@ import { remark } from "remark";
2
2
  import remarkRehype from "remark-rehype";
3
3
  import rehypeStringify from "rehype-stringify";
4
4
 
5
+ import { tags } from "ziko/dom";
5
6
 
6
- import { tags } from 'ziko/dom'
7
+ const { div } = tags;
7
8
 
8
- const { div } = tags
9
-
10
- export function MarkdownView({ cellData, actions }) {
9
+ export function MarkdownView({
10
+ cellData,
11
+ actions,
12
+ remarkPlugins = [],
13
+ rehypePlugins = [],
14
+ }) {
11
15
  const content = div();
12
-
13
- // Process Markdown asynchronously using Remark + Rehype
16
+
14
17
  remark()
18
+ .use(remarkPlugins)
15
19
  .use(remarkRehype, { allowDangerousHtml: true })
20
+ .use(rehypePlugins)
16
21
  .use(rehypeStringify)
17
22
  .process(cellData.code || "*Empty Markdown Cell*")
18
23
  .then((file) => {
19
24
  content.element.innerHTML = String(file);
20
25
  })
21
26
  .catch((err) => {
22
- content.element.innerHTML = `<div class="error-output">Markdown rendering error: ${err.message}</div>`;
27
+ content.element.innerHTML =
28
+ `<div class="error-output">Markdown rendering error: ${err.message}</div>`;
23
29
  });
24
-
25
- const wrap = div({ class: "markdown-rendered-cell markdown-body" }, content);
26
- wrap.onClick(() => actions.editMarkdown())
30
+
31
+ const wrap = div(
32
+ { class: "markdown-rendered-cell markdown-body" },
33
+ content
34
+ );
35
+
36
+ wrap.onClick(() => actions.editMarkdown());
37
+
27
38
  return wrap.element;
28
- }
39
+ }
40
+
41
+
42
+
43
+ // import { remark } from "remark";
44
+ // import remarkRehype from "remark-rehype";
45
+ // import rehypeStringify from "rehype-stringify";
46
+
47
+ // import { tags } from "ziko/dom";
48
+
49
+ // const { div } = tags;
50
+
51
+ // /**
52
+ // * Activate elements inserted through innerHTML.
53
+ // *
54
+ // * Scripts created by innerHTML are inert, so recreate them
55
+ // * as real DOM nodes. This also handles external scripts.
56
+ // */
57
+ // async function activateHTML(container) {
58
+ // const scripts = [
59
+ // ...container.querySelectorAll("script"),
60
+ // ];
61
+
62
+ // for (const oldScript of scripts) {
63
+ // const script = document.createElement("script");
64
+
65
+ // for (const attribute of oldScript.attributes) {
66
+ // script.setAttribute(
67
+ // attribute.name,
68
+ // attribute.value
69
+ // );
70
+ // }
71
+
72
+ // script.textContent = oldScript.textContent;
73
+
74
+ // oldScript.replaceWith(script);
75
+
76
+ // if (script.src) {
77
+ // await new Promise((resolve, reject) => {
78
+ // script.addEventListener(
79
+ // "load",
80
+ // resolve,
81
+ // { once: true }
82
+ // );
83
+
84
+ // script.addEventListener(
85
+ // "error",
86
+ // reject,
87
+ // { once: true }
88
+ // );
89
+ // });
90
+ // }
91
+ // }
92
+ // }
93
+
94
+ // export function MarkdownView({
95
+ // cellData,
96
+ // actions,
97
+ // remarkPlugins = [],
98
+ // rehypePlugins = [],
99
+ // }) {
100
+ // const content = div();
101
+
102
+ // remark()
103
+ // .use(remarkPlugins)
104
+ // .use(remarkRehype, {
105
+ // allowDangerousHtml: true,
106
+ // })
107
+ // .use(rehypePlugins)
108
+ // .use(rehypeStringify)
109
+ // .process(cellData.code || "*Empty Markdown Cell*")
110
+ // .then(async (file) => {
111
+ // content.element.innerHTML = String(file);
112
+
113
+ // await activateHTML(content.element);
114
+ // })
115
+ // .catch((err) => {
116
+ // content.element.innerHTML = `
117
+ // <div class="error-output">
118
+ // Markdown rendering error: ${err.message}
119
+ // </div>
120
+ // `;
121
+ // });
122
+
123
+ // const wrap = div(
124
+ // {
125
+ // class: "markdown-rendered-cell markdown-body",
126
+ // },
127
+ // content
128
+ // );
129
+
130
+ // wrap.onClick(() => actions.editMarkdown());
131
+
132
+ // return wrap.element;
133
+ // }
@@ -1,4 +1,21 @@
1
1
  @import url('./ErrorOutput/index.css');
2
2
  @import url('./Button/index.css');
3
3
  @import url('./Header/index.css');
4
- @import url('./MarkdownView//index.css');
4
+ @import url('./MarkdownView//index.css');
5
+
6
+ .ziko-notebook .runtime-select {
7
+ font-family: inherit;
8
+ font-size: 11px;
9
+ padding: 2px 4px;
10
+ border: 1px solid var(--border-color);
11
+ border-radius: 4px;
12
+ background: #ffffff;
13
+ color: #1e293b;
14
+ cursor: pointer;
15
+ height: 24px;
16
+ }
17
+
18
+ .ziko-notebook .runtime-select:focus {
19
+ outline: none;
20
+ border-color: var(--primary);
21
+ }
package/src/index.css CHANGED
@@ -141,7 +141,7 @@
141
141
 
142
142
  .ziko-notebook .toolbar {
143
143
  width: 100%;
144
- justify-content: flex-start;
144
+ justify-content: center;
145
145
  }
146
146
 
147
147
  .ziko-notebook .cell-input-grid {
package/src/index.d.ts CHANGED
@@ -1,5 +1,97 @@
1
- import type { UIElement } from 'ziko/dom/UIElement'
2
- export declare class UINotebook extends UIElement{
3
1
 
2
+ import type { UIElement } from "ziko/dom/UIElement";
3
+
4
+ export type MaybePromise<T> = T | PromiseLike<T>;
5
+
6
+ export interface NotebookCell {
7
+ id?: number;
8
+ type?: "code" | "markdown";
9
+ code?: MaybePromise<string>;
10
+ readonly?: boolean;
11
+ runtime?: string | null;
12
+ isEditingMarkdown?: boolean;
13
+ execCount?: number | null;
14
+ outputNode?: HTMLElement | null;
15
+ hasDomOutput?: boolean;
16
+ }
17
+
18
+ export type NotebookCells = MaybePromise<NotebookCell[]>;
19
+
20
+ export interface NotebookOptions {
21
+ /**
22
+ * Initial cells, or a Promise resolving to the cells.
23
+ */
24
+ cells?: NotebookCells;
25
+
26
+ importMap?: Record<string, string>;
27
+ runCells?: boolean;
28
+ minLines?: number;
29
+ maxCells?: number;
30
+ showHeader?: boolean;
31
+ codeMirrorConfig?: unknown[] | Record<string, unknown>;
32
+ codeMirrorPlugins?: unknown[];
33
+ runtimePlugins?: Array<{
34
+ name: string;
35
+ [key: string]: unknown;
36
+ }>;
37
+ remarkPlugins?: unknown[];
38
+ rehypePlugins?: unknown[];
39
+ }
40
+
41
+ export declare class UINotebook extends UIElement {
42
+ constructor(options?: NotebookOptions);
43
+
44
+ /**
45
+ * Resolves when initial cells and Promise-based cell code
46
+ * have finished loading.
47
+ *
48
+ * Rejects if the initial cells source fails or does not
49
+ * resolve to an array. Individual code-source failures are
50
+ * displayed in their respective cells.
51
+ */
52
+ readonly ready: Promise<this>;
53
+
54
+ readonly id: string;
55
+
56
+ importMap: Record<string, string>;
57
+ runtimePlugins: NotebookOptions["runtimePlugins"];
58
+ codeMirrorConfig: NotebookOptions["codeMirrorConfig"];
59
+ codeMirrorPlugins: unknown[];
60
+ remarkPlugins: unknown[];
61
+ rehypePlugins: unknown[];
62
+ minLines: number;
63
+ maxCells: number;
64
+
65
+ runCell(id: number): Promise<void>;
66
+ runAllCells(): Promise<void>;
67
+
68
+ addCell(
69
+ type?: "code" | "markdown",
70
+ initialCode?: MaybePromise<string> | null,
71
+ afterId?: number | null,
72
+ readonly?: boolean
73
+ ): number | null;
74
+
75
+ deleteCell(id: number): void;
76
+ setActiveCell(id: number): void;
77
+ focusCell(id: number): void;
78
+ clearOutputs(): void;
79
+ resetScope(): Record<string, unknown>;
80
+ getScope(): Record<string, unknown>;
81
+
82
+ getNotebookData(options?: {
83
+ inputs?: boolean;
84
+ outputs?: boolean;
85
+ }): Array<{
86
+ id: number;
87
+ type: "code" | "markdown";
88
+ readonly: boolean;
89
+ order: number | null;
90
+ code?: string;
91
+ outputHtml?: string | null;
92
+ }>;
4
93
  }
5
- export declare function Notebook() : UINotebook
94
+
95
+ export declare function Notebook(
96
+ options?: NotebookOptions
97
+ ): UINotebook;