Skip to content

Commit dd58392

Browse files
committed
Show unresolved dependency warnings, add URL import, and replace examples with platform guide
- JShellBridge: show "cannot be referenced until" warnings for TypeDeclSnippet and VarSnippet with unresolved dependencies (MethodSnippet already worked). Extract unresolvedList() helper shared by all three cases. - Import notebook from URL via ?url= query parameter. If another browser tab is already open, delegate via BroadcastChannel instead of loading a second instance. - Replace four Java examples with a single platform guide notebook explaining UI features, menus, shortcuts, and settings. - Add markdown table styles (borders, header background, padding).
1 parent 1e478c9 commit dd58392

13 files changed

Lines changed: 495 additions & 907 deletions

CLAUDE.md

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# Java Notebook
22

3-
Notebook de Java 100% client-side que corre en el navegador. Java 17 completo corre en WebAssembly via CheerpJ + JShell — sin servidor, sin backend, sin instalacion.
3+
Notebook de Java 100% client-side que corre en el navegador. Java 17 completo corre en WebAssembly via CheerpJ + JShell. Sin servidor, sin backend, sin instalacion.
44

55
## Arquitectura
66

77
```
88
index.html → Punto de entrada (Vite entry point)
99
css/notebook.css → Estilos responsive mobile-first con dark mode
1010
js/
11-
app.js → Bootstrap, navbar, settings, shortcuts modal, read mode, autosave, ejemplos
11+
app.js → Bootstrap, navbar, settings, shortcuts modal, read mode, autosave, ejemplos, import por URL
1212
tab-manager.js → Multiples notebooks en pestanas, barra de tabs + menu de acciones
1313
cell-manager.js → Ciclo de vida de celdas, seleccion, ejecucion, mutaciones DOM targeted
1414
cell-renderer.js → Creacion del DOM para celdas code/markdown (Bootstrap cards)
@@ -34,33 +34,33 @@ start.bat → Script de inicio para Windows
3434

3535
## Stack tecnico
3636

37-
- **Vite** build tool + dev server, resuelve dependencias desde node_modules/
38-
- **Bootstrap 5** via npm framework CSS + componentes JS (navbar, nav-tabs, cards, modals, dropdowns)
39-
- **Bootstrap Icons** via npm iconos consistentes en toda la UI
40-
- **CodeMirror 6** via npm editor, syntax highlighting Java + Markdown, search
41-
- **marked.js** via npm renderizado de markdown
42-
- **CheerpJ** JVM completa en WebAssembly, cargada desde CDN
43-
- **JShell** REPL de Java 17 para evaluacion interactiva de codigo
44-
- **@codemirror/theme-one-dark** tema oscuro, togglable desde settings (Auto/Claro/Oscuro)
45-
- **@codemirror/lang-markdown** syntax highlighting para editor de celdas markdown
37+
- **Vite**: build tool + dev server, resuelve dependencias desde node_modules/
38+
- **Bootstrap 5** via npm: framework CSS + componentes JS (navbar, nav-tabs, cards, modals, dropdowns)
39+
- **Bootstrap Icons** via npm: iconos consistentes en toda la UI
40+
- **CodeMirror 6** via npm: editor, syntax highlighting Java + Markdown, search
41+
- **marked.js** via npm: renderizado de markdown
42+
- **CheerpJ**: JVM completa en WebAssembly, cargada desde CDN
43+
- **JShell**: REPL de Java 17 para evaluacion interactiva de codigo
44+
- **@codemirror/theme-one-dark**: tema oscuro, togglable desde settings (Auto/Claro/Oscuro)
45+
- **@codemirror/lang-markdown**: syntax highlighting para editor de celdas markdown
4646

4747
## Como funciona la ejecucion
4848

4949
1. CheerpJ se inicializa una vez al cargar la pagina (carga JVM WASM desde CDN)
5050
2. JShellBridge se carga via `cheerpjRunLibrary()` en library mode
5151
3. Cada tab crea una sesion JShell independiente via `JShellBridge.init(sessionId)`
5252
4. Al ejecutar una celda, `jshell-proxy.js` llama a `JShellBridge.eval(sessionId, code)`
53-
5. JShell compila y ejecuta el snippet — variables, clases y metodos persisten en la sesion
53+
5. JShell compila y ejecuta el snippet. Variables, clases y metodos persisten en la sesion
5454
6. La salida se captura de dos fuentes: Java `SwitchOutputStream` buffer + CheerpJ `#console` DOM
5555
7. "Reiniciar y ejecutar todo" hace reset de la sesion y ejecuta todas las celdas en orden
5656

5757
## Sesiones JShell por tab
5858

5959
- Cada tab tiene su propia sesion JShell (Map<String, SessionState> en Java)
60-
- Las sesiones son independientes — variables de un tab no afectan a otro
60+
- Las sesiones son independientes. Variables de un tab no afectan a otro
6161
- "Reiniciar sesion" destruye y recrea la sesion JShell del tab activo
6262
- Al cerrar un tab se destruye su sesion (`JShellBridge.close(sessionId)`)
63-
- Las evaluaciones se serializan via promise chain solo una eval a la vez (output capture compartido)
63+
- Las evaluaciones se serializan via promise chain, solo una eval a la vez (output capture compartido)
6464

6565
## JShellBridge (src/JShellBridge.java)
6666

@@ -71,42 +71,79 @@ Bridge multi-sesion entre JavaScript y JShell en CheerpJ. Estrategia de evaluaci
7171
3. Declaraciones/statements → eval normal con display de valores post-iteracion
7272

7373
Workarounds para CheerpJ:
74-
- `SnippetEvent.value()` siempre retorna null no usable
75-
- `shell.varValue()` retorna defaults (0, null, false) no usable
74+
- `SnippetEvent.value()` siempre retorna null, no usable
75+
- `shell.varValue()` retorna defaults (0, null, false), no usable
7676
- Se usa `shell.eval("println(varName)")` para leer valores reales
77-
- Excepciones del LocalExecutionControl se tragan silenciosamente wrap en try/catch
77+
- Excepciones del LocalExecutionControl se tragan silenciosamente, wrap en try/catch
7878

7979
## Limitaciones de CheerpJ
8080

81-
- `Scanner` / `System.in` NO hay stdin en browser
82-
- `java.io.File` NO hay filesystem
81+
- `Scanner` / `System.in`: NO hay stdin en browser
82+
- `java.io.File`: NO hay filesystem
8383
- Algunas excepciones no se lanzan (ej: `1/0` retorna 0 en vez de ArithmeticException)
8484
- Primera ejecucion lenta (~5-10s) mientras CheerpJ inicializa la JVM
85-
- CheerpJ se carga desde CDN — requiere conexion a internet
85+
- CheerpJ se carga desde CDN, requiere conexion a internet
86+
87+
## UI: menus y controles
88+
89+
**Menu Archivo** (navbar dropdown):
90+
- Importar: abre un .ipynb desde disco (tambien drag-drop)
91+
- Exportar: descarga el notebook actual
92+
- Ejemplos: notebooks de ejemplo cargados desde `public/examples/index.json`
93+
94+
**Configuracion** (engranaje en navbar):
95+
- Tema: Auto, Claro, Oscuro
96+
- Identacion: 2 o 4 espacios
97+
- Modo: Edicion o Lectura (alterna controles de edicion)
98+
- Atajos generales: On u Off
99+
100+
**Menu del notebook** (icono ⋮ a la derecha de la barra de tabs):
101+
- Reiniciar y ejecutar todo
102+
- Reiniciar sesion
103+
- Agregar celda: Codigo, Texto
104+
- Celda seleccionada: Subir, Bajar, Cortar, Copiar, Pegar, Eliminar, Deshacer eliminar
105+
106+
**Pestanas**:
107+
- Boton + para nuevo notebook
108+
- Doble clic en pestana para renombrar (long-press en mobile)
109+
- Boton X para cerrar
110+
111+
**Celdas de codigo**: boton Ejecutar, botones Subir/Bajar/Eliminar, botones Copiar/Limpiar resultado
112+
**Celdas de markdown**: doble clic para editar, botones Editar/Listo, Subir/Bajar/Eliminar
113+
114+
## Import por URL
115+
116+
Se puede abrir un notebook desde una URL con el query parameter `?url=`:
117+
```
118+
https://sitio.com/?url=https://ejemplo.com/notebook.ipynb
119+
```
120+
- Hace fetch del .ipynb, lo parsea y abre como tab nuevo
121+
- Muestra "Notebook importado: nombre" en el loading overlay
122+
- Si ya hay otra pestana del navegador abierta, delega via BroadcastChannel y muestra "Notebook enviado a la pestana abierta"
86123

87124
## Settings
88125

89126
Se guardan en localStorage key `java-notebook-settings`:
90127
- `theme`: system, light o dark (default system)
91128
- `indentSize`: 2 o 4 espacios (default 4)
92-
- `readMode`: true/false (default false) — modo lectura oculta controles de edicion
93-
- `shortcuts`: true/false (default false) — habilita atajos idle (cut/copy/paste/delete/undo celda)
129+
- `readMode`: true/false (default false). Modo lectura oculta controles de edicion
130+
- `shortcuts`: true/false (default false). Habilita atajos idle (cut/copy/paste/delete/undo celda)
94131

95132
Los tabs/notebooks se guardan en localStorage key `java-notebook-autosave` (formato multi-tab).
96133

97134
## Atajos de teclado
98135

99136
Hay dos categorias de atajos globales (Ctrl+key):
100137

101-
**Always-on** funcionan siempre, incluso dentro del editor (bloqueados solo por modals/dropdowns):
138+
**Always-on**, funcionan siempre, incluso dentro del editor (bloqueados solo por modals/dropdowns):
102139

103140
| Atajo | Accion |
104141
|---|---|
105142
| Ctrl+S | Exportar notebook |
106143
| Ctrl+E | Alternar modo lectura/edicion |
107144
| Ctrl+Up/Down | Navegar entre celdas |
108145

109-
**Idle-only** requieren foco fuera del editor + setting "Atajos generales" habilitado:
146+
**Idle-only**, requieren foco fuera del editor + setting "Atajos generales" habilitado:
110147

111148
| Atajo | Accion |
112149
|---|---|
@@ -122,7 +159,7 @@ Hay dos categorias de atajos globales (Ctrl+key):
122159
|---|---|
123160
| Shift+Enter | Ejecutar y quedarse (code) / Salir edicion (markdown) |
124161
| Ctrl+Enter | Ejecutar y avanzar (code) / Salir y avanzar (markdown) |
125-
| Ctrl+Shift+F | Formatear codigo |
162+
| Ctrl+Shift+F | Corregir indentacion |
126163
| Escape | Salir de edicion markdown |
127164
| ? | Mostrar/ocultar modal de atajos (fuera de editor) |
128165

css/notebook.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,21 @@ body {
302302
color: var(--bs-secondary-color);
303303
margin: 8px 0;
304304
}
305+
.cell-rendered-markdown table {
306+
width: auto;
307+
border-collapse: collapse;
308+
margin: 8px 0;
309+
font-size: 0.92em;
310+
}
311+
.cell-rendered-markdown th,
312+
.cell-rendered-markdown td {
313+
border: 1px solid var(--bs-border-color);
314+
padding: 6px 12px;
315+
}
316+
.cell-rendered-markdown th {
317+
background: var(--bs-tertiary-bg);
318+
font-weight: 600;
319+
}
305320
.cell-rendered-markdown ul, .cell-rendered-markdown ol {
306321
padding-left: 1.5rem;
307322
margin: 4px 0;

index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
<div id="loading-overlay" class="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="z-index:1055; background:rgba(0,0,0,0.5)">
7676
<div class="bg-body rounded-3 p-5 text-center shadow">
7777
<div class="spinner-border text-primary mb-3" role="status"></div>
78-
<div id="loading-status" class="fs-6">Inicializando Java...</div>
78+
<div id="loading-import" class="fs-6 fw-bold mb-3 d-none"></div>
79+
<div id="loading-status" class="fs-6 text-body-secondary">Inicializando Java...</div>
7980
</div>
8081
</div>
8182

js/app.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,55 @@ const container = document.getElementById('notebook-container');
2525
const tabActionsBar = document.getElementById('tab-actions-bar');
2626
const loadingOverlay = document.getElementById('loading-overlay');
2727
const loadingStatus = document.getElementById('loading-status');
28+
const loadingImport = document.getElementById('loading-import');
2829
const btnImport = document.getElementById('btn-import');
2930
const btnExport = document.getElementById('btn-export');
3031

32+
// --- Multi-tab: delegate ?url= to existing tab ---
33+
34+
const channel = new BroadcastChannel('java-notebook');
35+
36+
async function tryDelegateURL() {
37+
const url = new URLSearchParams(window.location.search).get('url');
38+
if (!url) return false;
39+
40+
return new Promise(resolve => {
41+
const onMessage = (e) => {
42+
if (e.data?.type === 'ack-import') {
43+
channel.removeEventListener('message', onMessage);
44+
resolve(true);
45+
}
46+
};
47+
channel.addEventListener('message', onMessage);
48+
channel.postMessage({ type: 'import-url', url });
49+
setTimeout(() => {
50+
channel.removeEventListener('message', onMessage);
51+
resolve(false);
52+
}, 500);
53+
});
54+
}
55+
56+
function showDelegatedMessage() {
57+
loadingOverlay.querySelector('.spinner-border')?.remove();
58+
loadingStatus.remove();
59+
loadingImport.textContent = 'Notebook enviado a la pestaña abierta';
60+
loadingImport.classList.remove('d-none');
61+
const btn = document.createElement('button');
62+
btn.className = 'btn btn-sm btn-outline-secondary mt-3';
63+
btn.textContent = 'Cerrar pestaña';
64+
btn.addEventListener('click', () => window.close());
65+
loadingImport.after(btn);
66+
}
67+
3168
let autosaveTimer = null;
3269

3370
async function main() {
71+
// If ?url= and another tab is open, delegate and stop
72+
if (await tryDelegateURL()) {
73+
showDelegatedMessage();
74+
return;
75+
}
76+
3477
// Restore read mode before rendering
3578
initReadMode();
3679

@@ -50,6 +93,9 @@ async function main() {
5093
createTab(nb, 'notebook.ipynb');
5194
}
5295

96+
// Import notebook from ?url= query parameter
97+
await importFromURL();
98+
5399
// Setup drag-drop — opens as new tab
54100
setupDragDrop(container, ({ notebook, filename }) => {
55101
createTab(notebook, filename);
@@ -66,6 +112,23 @@ async function main() {
66112
initHelpButton();
67113
initGlobalShortcuts();
68114

115+
// Listen for ?url= delegated from other tabs
116+
channel.addEventListener('message', async (e) => {
117+
if (e.data?.type !== 'import-url') return;
118+
const url = e.data.url;
119+
try {
120+
const resp = await fetch(url);
121+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
122+
const obj = await resp.json();
123+
const nb = fromJSON(obj);
124+
const filename = url.split('/').pop().split('?')[0] || 'notebook.ipynb';
125+
createTab(nb, filename);
126+
channel.postMessage({ type: 'ack-import' });
127+
} catch (err) {
128+
console.error('Error importing delegated URL:', err);
129+
}
130+
});
131+
69132
// Init JShell (CheerpJ + JShellBridge)
70133
setProgressCallback(updateLoadingStatus);
71134
try {
@@ -130,6 +193,29 @@ function handleExport() {
130193
}
131194
}
132195

196+
// --- Import from URL ---
197+
198+
async function importFromURL() {
199+
const url = new URLSearchParams(window.location.search).get('url');
200+
if (!url) return;
201+
const filename = url.split('/').pop().split('?')[0] || 'notebook.ipynb';
202+
try {
203+
const resp = await fetch(url);
204+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
205+
const obj = await resp.json();
206+
const nb = fromJSON(obj);
207+
createTab(nb, filename);
208+
loadingImport.textContent = 'Notebook importado: ' + filename;
209+
loadingImport.classList.remove('d-none');
210+
// Clean the URL without reloading
211+
window.history.replaceState({}, '', window.location.pathname);
212+
} catch (e) {
213+
loadingImport.textContent = 'Error al importar: ' + e.message;
214+
loadingImport.classList.remove('d-none');
215+
console.error('Error importing from URL:', e);
216+
}
217+
}
218+
133219
// --- Examples ---
134220

135221
async function initExamples() {

0 commit comments

Comments
 (0)