structscript 1.2.0 → 1.4.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.
@@ -0,0 +1,3247 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>StructScript Editor</title>
7
+ <style>
8
+ /* ============================================================
9
+ THEME VARIABLES — StructScript Brand v1.2
10
+ ============================================================ */
11
+ :root {
12
+ /* Dark theme (default) */
13
+ --bg: #0a1614;
14
+ --bg2: #0d1f1d;
15
+ --bg3: #112420;
16
+ --surface: #162b28;
17
+ --surface2: #1e3530;
18
+ --border: #234440;
19
+ --border2: #2e5550;
20
+ --ink: #e0f0ee;
21
+ --ink2: #8ab8b4;
22
+ --muted: #4a7470;
23
+ --accent: #0b7a75;
24
+ --accent-lt: #12a89e;
25
+ --accent-dim: rgba(11,122,117,0.18);
26
+ --lime: #b8f000;
27
+ --lime-dim: rgba(184,240,0,0.12);
28
+ --accent2: #b8f000;
29
+ --accent3: #5b9cf6;
30
+ --err: #f07070;
31
+ --warn: #f0c060;
32
+ --kw-color: #b8f000;
33
+ --str-color: #7ed8c0;
34
+ --num-color: #89b4f8;
35
+ --cmt-color: #3a6460;
36
+ --fn-color: #c4a5f8;
37
+ --op-color: #5bc8c4;
38
+ --bool-color: #f07070;
39
+ --line-h: 22px;
40
+ --font-size: 13px;
41
+ --pad: 16px;
42
+ }
43
+
44
+ [data-theme="light"] {
45
+ --bg: #f2f8f7;
46
+ --bg2: #e8f4f2;
47
+ --bg3: #dceeed;
48
+ --surface: #ffffff;
49
+ --surface2: #f2f8f7;
50
+ --border: #c4dedd;
51
+ --border2: #9ac8c6;
52
+ --ink: #0d1f1e;
53
+ --ink2: #2a5552;
54
+ --muted: #6a9896;
55
+ --accent: #0b7a75;
56
+ --accent-lt: #12a89e;
57
+ --accent-dim: rgba(11,122,117,0.1);
58
+ --lime: #8ab800;
59
+ --lime-dim: rgba(138,184,0,0.12);
60
+ --accent2: #8ab800;
61
+ --accent3: #1d4db0;
62
+ --err: #c0392b;
63
+ --warn: #b07820;
64
+ --kw-color: #8ab800;
65
+ --str-color: #0b7a75;
66
+ --num-color: #1d4db0;
67
+ --cmt-color: #9ac8c6;
68
+ --fn-color: #7c3aed;
69
+ --op-color: #0b7a75;
70
+ --bool-color: #c0392b;
71
+ }
72
+
73
+ * { box-sizing: border-box; margin: 0; padding: 0; }
74
+
75
+ body {
76
+ background: var(--bg);
77
+ color: var(--ink);
78
+ font-family: 'Manrope', sans-serif;
79
+ height: 100vh;
80
+ overflow: hidden;
81
+ transition: background 0.3s, color 0.3s;
82
+ }
83
+
84
+ /* ============================================================
85
+ TOPBAR
86
+ ============================================================ */
87
+ .topbar {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 8px;
91
+ height: 48px;
92
+ padding: 0 16px;
93
+ border-bottom: 1px solid var(--border);
94
+ background: var(--bg2);
95
+ flex-shrink: 0;
96
+ position: relative;
97
+ z-index: 50;
98
+ }
99
+
100
+ .logo { display: flex; align-items: center; gap: 6px; text-decoration: none; }
101
+ .logo-s { font-size: 17px; font-weight: 800; color: var(--ink); letter-spacing: -0.5px; }
102
+ .logo-sc { font-size: 17px; font-weight: 800; color: var(--accent-lt); letter-spacing: -0.5px; }
103
+ .logo-v { font-size: 9px; font-family: 'DM Mono', monospace; color: var(--muted);
104
+ padding: 1px 5px; border: 1px solid var(--border2); border-radius: 3px; margin-left: 2px; }
105
+
106
+ .divider { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
107
+
108
+ .nav-tabs { display: flex; gap: 2px; }
109
+ .nav-tab {
110
+ padding: 5px 12px; border-radius: 5px; font-size: 12px; font-weight: 600;
111
+ cursor: pointer; border: none; background: none; color: var(--ink2);
112
+ transition: all 0.15s; font-family: 'Manrope', sans-serif;
113
+ }
114
+ .nav-tab:hover { background: var(--surface); color: var(--ink); }
115
+ .nav-tab.active { background: var(--accent-dim); color: var(--accent); }
116
+
117
+ .topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
118
+
119
+ .icon-btn {
120
+ width: 30px; height: 30px; border-radius: 6px; border: 1px solid var(--border);
121
+ background: var(--surface); color: var(--ink2); cursor: pointer;
122
+ display: flex; align-items: center; justify-content: center; font-size: 14px;
123
+ transition: all 0.15s;
124
+ }
125
+ .icon-btn:hover { border-color: var(--accent); color: var(--accent); }
126
+
127
+ .file-btn {
128
+ display: flex; align-items: center; gap: 6px;
129
+ padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
130
+ cursor: pointer; border: 1px solid var(--border); background: var(--surface);
131
+ color: var(--ink2); transition: all 0.15s; font-family: 'Manrope', sans-serif;
132
+ }
133
+ .file-btn:hover { border-color: var(--border2); color: var(--ink); }
134
+
135
+ .run-btn {
136
+ display: flex; align-items: center; gap: 6px;
137
+ padding: 6px 16px; background: var(--lime); border: none; border-radius: 6px;
138
+ color: #0d1f1e; font-family: 'Manrope', sans-serif; font-size: 13px; font-weight: 800;
139
+ cursor: pointer; transition: all 0.15s; letter-spacing: 0.2px;
140
+ }
141
+ .run-btn:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 4px 14px rgba(184,240,0,0.3); }
142
+ .run-btn:active { transform: none; }
143
+ .run-kbd { font-size: 10px; opacity: 0.65; font-family: 'DM Mono', monospace; }
144
+
145
+ /* ============================================================
146
+ LAYOUT
147
+ ============================================================ */
148
+ .app-body {
149
+ display: flex;
150
+ flex-direction: column;
151
+ height: calc(100vh - 48px);
152
+ }
153
+
154
+ .page { display: none; flex: 1; overflow: hidden; }
155
+ .page.active { display: flex; flex-direction: column; }
156
+
157
+ /* ============================================================
158
+ PLAYGROUND
159
+ ============================================================ */
160
+ .playground {
161
+ display: grid;
162
+ grid-template-columns: 1fr 1fr;
163
+ flex: 1;
164
+ overflow: hidden;
165
+ min-height: 0;
166
+ }
167
+
168
+ /* Editor */
169
+ .editor-pane {
170
+ display: flex; flex-direction: column;
171
+ border-right: 1px solid var(--border);
172
+ background: var(--bg3);
173
+ min-height: 0;
174
+ position: relative;
175
+ }
176
+
177
+ .pane-bar {
178
+ display: flex; align-items: center; gap: 8px;
179
+ padding: 6px 12px;
180
+ border-bottom: 1px solid var(--border);
181
+ background: var(--bg2);
182
+ font-family: 'DM Mono', monospace; font-size: 10px; color: var(--muted);
183
+ flex-shrink: 0;
184
+ }
185
+ .pane-bar .filename { color: var(--ink2); font-size: 11px; }
186
+ .pane-bar .modified { color: var(--warn); font-size: 10px; }
187
+ .wc-dots { display: flex; gap: 4px; }
188
+ .wc-dot { width: 8px; height: 8px; border-radius: 50%; }
189
+
190
+ .editor-container {
191
+ display: flex;
192
+ flex: 1;
193
+ overflow: hidden;
194
+ position: relative;
195
+ }
196
+
197
+ /* Line numbers */
198
+ .line-numbers {
199
+ width: 48px;
200
+ flex-shrink: 0;
201
+ padding: 12px 0;
202
+ background: var(--bg2);
203
+ border-right: 1px solid var(--border);
204
+ overflow: hidden;
205
+ user-select: none;
206
+ font-family: 'DM Mono', monospace;
207
+ font-size: var(--font-size);
208
+ line-height: var(--line-h);
209
+ color: var(--muted);
210
+ text-align: right;
211
+ }
212
+ .line-num {
213
+ padding-right: 10px;
214
+ display: block;
215
+ height: var(--line-h);
216
+ transition: color 0.1s;
217
+ }
218
+ .line-num.active { color: var(--accent); }
219
+
220
+ /* Code area */
221
+ .code-wrap {
222
+ flex: 1; position: relative; overflow: auto;
223
+ }
224
+
225
+ #code-editor {
226
+ position: absolute; inset: 0;
227
+ width: 100%; height: 100%;
228
+ padding: 12px var(--pad);
229
+ background: transparent;
230
+ border: none; outline: none;
231
+ color: transparent;
232
+ caret-color: var(--accent);
233
+ font-family: 'DM Mono', monospace;
234
+ font-size: var(--font-size);
235
+ line-height: var(--line-h);
236
+ resize: none; tab-size: 2;
237
+ z-index: 2;
238
+ white-space: pre;
239
+ overflow-wrap: normal;
240
+ overflow: auto;
241
+ spellcheck: false;
242
+ }
243
+
244
+ #highlight-layer {
245
+ position: absolute; inset: 0;
246
+ padding: 12px var(--pad);
247
+ font-family: 'DM Mono', monospace;
248
+ font-size: var(--font-size);
249
+ line-height: var(--line-h);
250
+ pointer-events: none;
251
+ white-space: pre;
252
+ overflow-wrap: normal;
253
+ z-index: 1;
254
+ color: var(--ink);
255
+ overflow: hidden;
256
+ }
257
+
258
+ /* Autocomplete */
259
+ #autocomplete {
260
+ position: absolute;
261
+ background: var(--surface);
262
+ border: 1px solid var(--border2);
263
+ border-radius: 8px;
264
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
265
+ z-index: 100;
266
+ min-width: 200px;
267
+ max-height: 200px;
268
+ overflow-y: auto;
269
+ display: none;
270
+ font-family: 'DM Mono', monospace;
271
+ font-size: 12px;
272
+ }
273
+ .ac-item {
274
+ display: flex; align-items: center; gap: 8px;
275
+ padding: 7px 12px; cursor: pointer;
276
+ transition: background 0.1s;
277
+ }
278
+ .ac-item:hover, .ac-item.selected { background: var(--accent-dim); }
279
+ .ac-item .ac-icon { font-size: 11px; opacity: 0.7; width: 14px; }
280
+ .ac-item .ac-name { color: var(--ink); flex: 1; }
281
+ .ac-item .ac-type { font-size: 10px; color: var(--muted); }
282
+
283
+ /* Output */
284
+ .output-pane {
285
+ display: flex; flex-direction: column;
286
+ background: var(--bg);
287
+ min-height: 0;
288
+ }
289
+
290
+ .out-tabs {
291
+ display: flex; gap: 0;
292
+ border-bottom: 1px solid var(--border);
293
+ background: var(--bg2);
294
+ flex-shrink: 0;
295
+ }
296
+ .out-tab {
297
+ padding: 7px 16px; font-size: 11px; font-weight: 600;
298
+ cursor: pointer; border: none; background: none;
299
+ color: var(--muted); transition: all 0.15s;
300
+ font-family: 'Manrope', sans-serif; border-bottom: 2px solid transparent;
301
+ }
302
+ .out-tab:hover { color: var(--ink2); }
303
+ .out-tab.active { color: var(--ink); border-bottom-color: var(--accent); }
304
+
305
+ .out-panel { display: none; flex: 1; overflow-y: auto; min-height: 0; }
306
+ .out-panel.active { display: block; }
307
+
308
+ #output-display {
309
+ padding: 12px var(--pad);
310
+ font-family: 'DM Mono', monospace;
311
+ font-size: 12px; line-height: 1.9;
312
+ }
313
+
314
+ .out-line { display: flex; gap: 10px; }
315
+ .out-prefix { color: var(--muted); user-select: none; flex-shrink: 0; width: 20px; text-align: right; }
316
+ .out-text { color: var(--ink2); word-break: break-all; }
317
+ .out-line.err .out-text { color: var(--err); }
318
+ .out-line.warn .out-text { color: var(--warn); }
319
+ .out-line.info .out-text { color: var(--accent3); }
320
+ .out-line.success .out-text { color: var(--accent2); }
321
+ .out-line.sep { border-top: 1px solid var(--border); margin: 4px 0; padding-top: 4px; }
322
+
323
+ /* Error console */
324
+ #error-display {
325
+ padding: 12px var(--pad);
326
+ font-family: 'DM Mono', monospace;
327
+ font-size: 12px; line-height: 1.9;
328
+ }
329
+
330
+ .err-card {
331
+ background: rgba(240,112,112,0.06);
332
+ border: 1px solid rgba(240,112,112,0.25);
333
+ border-left: 3px solid var(--err);
334
+ border-radius: 6px; padding: 12px 14px; margin-bottom: 10px;
335
+ }
336
+ .err-title { color: var(--err); font-weight: 600; margin-bottom: 4px; }
337
+ .err-detail { color: var(--ink2); font-size: 11px; }
338
+ .err-line-ref { color: var(--muted); font-size: 11px; margin-top: 6px; }
339
+ .err-snippet {
340
+ background: var(--bg3); border-radius: 4px; padding: 6px 10px;
341
+ margin-top: 8px; color: var(--ink); font-size: 11px;
342
+ }
343
+ .err-arrow { color: var(--err); }
344
+
345
+ .no-errors {
346
+ padding: 40px; text-align: center;
347
+ color: var(--muted); font-size: 12px;
348
+ }
349
+ .no-errors-icon { font-size: 28px; margin-bottom: 8px; }
350
+
351
+ /* Examples bar */
352
+ .examples-bar {
353
+ display: flex; align-items: center; gap: 6px;
354
+ padding: 7px 12px;
355
+ border-top: 1px solid var(--border);
356
+ background: var(--bg2);
357
+ overflow-x: auto; flex-shrink: 0;
358
+ }
359
+ .ex-label { font-size: 10px; font-family: 'DM Mono', monospace;
360
+ color: var(--muted); white-space: nowrap; text-transform: uppercase; letter-spacing: 1px; }
361
+ .ex-chip {
362
+ padding: 4px 10px; border-radius: 5px; font-size: 11px;
363
+ font-family: 'DM Mono', monospace; border: 1px solid var(--border);
364
+ background: var(--surface); color: var(--ink2); cursor: pointer; white-space: nowrap;
365
+ transition: all 0.12s;
366
+ }
367
+ .ex-chip:hover { border-color: var(--accent); color: var(--accent); }
368
+
369
+ /* Status bar */
370
+ .statusbar {
371
+ display: flex; align-items: center; gap: 12px;
372
+ height: 24px; padding: 0 12px;
373
+ border-top: 1px solid var(--border);
374
+ background: var(--bg2);
375
+ font-family: 'DM Mono', monospace; font-size: 10px; color: var(--muted);
376
+ flex-shrink: 0;
377
+ }
378
+ .sb-item { display: flex; align-items: center; gap: 4px; }
379
+ .sb-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); }
380
+ .sb-dot.ok { background: var(--accent2); }
381
+ .sb-dot.err { background: var(--err); }
382
+ .sb-dot.run { background: var(--warn); animation: pulse 0.8s infinite; }
383
+ .sb-sep { width: 1px; height: 12px; background: var(--border); }
384
+
385
+ /* ============================================================
386
+ DOCS PAGE
387
+ ============================================================ */
388
+ .docs-page { flex: 1; overflow-y: auto; padding: 48px 32px; max-width: 900px; margin: 0 auto; width: 100%; }
389
+
390
+ .docs-hero { margin-bottom: 48px; }
391
+ .docs-hero h2 { font-size: clamp(28px,5vw,44px); font-weight: 900; letter-spacing: -1.5px; line-height: 1.1; margin-bottom: 12px; }
392
+ .docs-hero h2 em { color: var(--accent); font-style: normal; }
393
+ .docs-hero p { font-size: 15px; color: var(--ink2); line-height: 1.7; max-width: 520px; }
394
+
395
+ .docs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; margin-bottom: 48px; }
396
+ .docs-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 20px; }
397
+ .docs-card h4 { font-size: 14px; font-weight: 700; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
398
+ .docs-card p { font-size: 12px; color: var(--ink2); line-height: 1.6; }
399
+
400
+ .docs-section { margin-bottom: 40px; }
401
+ .docs-section h3 { font-size: 18px; font-weight: 800; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
402
+
403
+ .docs-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 16px; }
404
+ .docs-table th { text-align: left; padding: 8px 12px; background: var(--surface); border: 1px solid var(--border);
405
+ font-family: 'DM Mono', monospace; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
406
+ .docs-table td { padding: 8px 12px; border: 1px solid var(--border); vertical-align: top; color: var(--ink2); }
407
+ .docs-table tr:hover td { background: var(--surface); }
408
+ code { font-family: 'DM Mono', monospace; font-size: 11px; color: var(--accent);
409
+ background: var(--accent-dim); padding: 1px 5px; border-radius: 3px; }
410
+
411
+ .docs-code {
412
+ background: var(--bg3); border: 1px solid var(--border); border-radius: 8px;
413
+ padding: 16px; font-family: 'DM Mono', monospace; font-size: 12px;
414
+ line-height: 1.8; overflow-x: auto; margin: 10px 0;
415
+ }
416
+ .kw { color: var(--kw-color); font-weight: 600; }
417
+ .str { color: var(--str-color); }
418
+ .cmt { color: var(--cmt-color); font-style: italic; }
419
+ .num { color: var(--num-color); }
420
+ .fn { color: var(--fn-color); }
421
+ .op { color: var(--op-color); }
422
+
423
+ /* ============================================================
424
+ ANIMATIONS
425
+ ============================================================ */
426
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
427
+ @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
428
+ .im-field{margin-bottom:14px}
429
+ .im-label{display:block;font-size:11px;font-family:'DM Mono',monospace;color:var(--ink2);margin-bottom:5px}
430
+ .im-prompt-text{color:var(--accent-lt)}
431
+ .im-idx{color:var(--muted);font-size:10px;margin-left:4px}
432
+ .im-input{width:100%;background:var(--bg3);border:1px solid var(--border2);border-radius:6px;padding:8px 12px;color:var(--ink);font-family:'DM Mono',monospace;font-size:12px;outline:none}
433
+ .im-input:focus{border-color:var(--accent)}
434
+ #input-modal-overlay.open{display:flex!important}
435
+
436
+ /* Web preview tab */
437
+ #out-web { padding: 0; overflow: hidden; display: none; flex-direction: column; }
438
+ #out-web.active { display: flex; }
439
+ #web-preview { flex: 1; width: 100%; border: none; background: #fff; }
440
+ .web-toolbar {
441
+ display: flex; align-items: center; gap: 8px;
442
+ padding: 6px 12px; background: var(--bg2);
443
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
444
+ }
445
+ .web-toolbar-label { font-size: 10px; font-family: 'DM Mono',monospace; color: var(--muted); flex: 1; }
446
+ .web-dl-btn {
447
+ padding: 4px 10px; border-radius: 5px; font-size: 11px; font-weight: 600;
448
+ cursor: pointer; border: 1px solid var(--border2); background: var(--surface2);
449
+ color: var(--ink2); font-family: 'Manrope',sans-serif; transition: all 0.15s;
450
+ }
451
+ .web-dl-btn:hover { border-color: var(--lime); color: var(--lime); }
452
+ </style>
453
+ <style>
454
+
455
+ /* ── EDITOR-SPECIFIC OVERRIDES ── */
456
+
457
+ /* Hide docs nav tab in editor mode */
458
+ #tab-docs { display: none !important; }
459
+
460
+ /* Sidebar */
461
+ .sidebar {
462
+ width: 240px;
463
+ min-width: 180px;
464
+ max-width: 320px;
465
+ background: var(--bg2);
466
+ border-right: 1px solid var(--border);
467
+ display: flex;
468
+ flex-direction: column;
469
+ flex-shrink: 0;
470
+ overflow: hidden;
471
+ }
472
+ .sidebar-header {
473
+ padding: 10px 12px 8px;
474
+ border-bottom: 1px solid var(--border);
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 6px;
478
+ flex-shrink: 0;
479
+ }
480
+ .sidebar-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); flex: 1; }
481
+ .sidebar-action { width: 22px; height: 22px; border-radius: 4px; display: flex; align-items: center; justify-content: center;
482
+ cursor: pointer; background: none; border: none; color: var(--muted); font-size: 13px; transition: all 0.15s; }
483
+ .sidebar-action:hover { background: var(--surface); color: var(--ink); }
484
+
485
+ .file-tree { flex: 1; overflow-y: auto; padding: 4px 0; }
486
+ .file-item {
487
+ display: flex; align-items: center; gap: 6px; padding: 5px 10px 5px 12px;
488
+ cursor: pointer; font-size: 12px; color: var(--ink2); border-radius: 0;
489
+ transition: background 0.1s; user-select: none; position: relative;
490
+ }
491
+ .file-item:hover { background: var(--surface); color: var(--ink); }
492
+ .file-item.active { background: var(--accent-dim); color: var(--accent-lt); }
493
+ .file-item.active::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--accent); }
494
+ .file-icon { font-size: 11px; flex-shrink: 0; }
495
+ .file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'DM Mono', monospace; }
496
+ .file-delete { opacity: 0; font-size: 11px; color: var(--muted); padding: 1px 3px; border-radius: 3px; transition: all 0.1s; }
497
+ .file-item:hover .file-delete { opacity: 1; }
498
+ .file-delete:hover { background: var(--err-dim); color: var(--err); }
499
+ .file-section { padding: 6px 12px 2px; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); }
500
+
501
+ .sidebar-new-btn {
502
+ margin: 8px 10px; padding: 7px 10px; border-radius: 7px; font-size: 11px; font-weight: 700;
503
+ cursor: pointer; border: 1px dashed var(--border2); background: transparent;
504
+ color: var(--muted); font-family: 'Manrope', sans-serif; transition: all 0.2s;
505
+ text-align: center;
506
+ }
507
+ .sidebar-new-btn:hover { border-color: var(--lime); color: var(--lime); background: rgba(184,240,0,0.04); }
508
+
509
+ /* File path bar */
510
+ .filepath-bar {
511
+ display: flex; align-items: center; gap: 8px; padding: 0 14px;
512
+ height: 28px; background: var(--bg2); border-bottom: 1px solid var(--border);
513
+ flex-shrink: 0;
514
+ }
515
+ .filepath-text { font-size: 10px; font-family: 'DM Mono', monospace; color: var(--muted);
516
+ flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
517
+ .filepath-text span { color: var(--ink2); }
518
+ .fp-save-badge { font-size: 9px; padding: 1px 6px; border-radius: 3px; background: var(--accent-dim);
519
+ color: var(--accent); font-weight: 700; display: none; }
520
+ .fp-save-badge.show { display: inline; }
521
+
522
+ /* Override playground layout to include sidebar */
523
+ .playground { display: flex; flex-direction: row; height: 100%; overflow: hidden; }
524
+ .editor-column { display: flex; flex-direction: column; flex: 1; min-width: 0; }
525
+ .editor-pane { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
526
+
527
+ /* Save / open buttons in topbar — hide the browser download versions */
528
+ #save-file-btn-dl { display: none !important; }
529
+
530
+ /* Status bar at bottom */
531
+ .status-bar {
532
+ display: flex; align-items: center; gap: 12px; padding: 0 14px;
533
+ height: 22px; background: var(--accent); color: #0d1f1e; font-size: 10px;
534
+ font-family: 'DM Mono', monospace; flex-shrink: 0;
535
+ }
536
+ .status-bar span { opacity: 0.7; }
537
+ .status-bar .sb-sep { opacity: 0.3; }
538
+ #sb-cursor { opacity: 1; font-weight: 600; }
539
+ #sb-lang { font-weight: 700; }
540
+
541
+ /* Open file dialog overlay */
542
+ .browse-overlay {
543
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.65);
544
+ z-index: 999; align-items: center; justify-content: center; backdrop-filter: blur(4px);
545
+ }
546
+ .browse-overlay.open { display: flex; }
547
+ .browse-dialog {
548
+ background: var(--surface); border: 1px solid var(--border2); border-radius: 12px;
549
+ width: 520px; max-width: 95vw; height: 480px; max-height: 80vh;
550
+ display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
551
+ }
552
+ .browse-bar {
553
+ display: flex; align-items: center; gap: 8px; padding: 12px 16px;
554
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
555
+ }
556
+ .browse-path-input {
557
+ flex: 1; background: var(--bg3); border: 1px solid var(--border2); border-radius: 6px;
558
+ padding: 6px 10px; color: var(--ink); font-family: 'DM Mono', monospace; font-size: 11px; outline: none;
559
+ }
560
+ .browse-path-input:focus { border-color: var(--accent); }
561
+ .browse-up-btn { padding: 5px 10px; border-radius: 5px; font-size: 11px; cursor: pointer;
562
+ border: 1px solid var(--border2); background: var(--surface2); color: var(--ink2); font-family: 'Manrope', sans-serif; }
563
+ .browse-up-btn:hover { border-color: var(--accent); color: var(--accent); }
564
+ .browse-list { flex: 1; overflow-y: auto; padding: 4px 0; }
565
+ .browse-entry { display: flex; align-items: center; gap: 10px; padding: 7px 16px; cursor: pointer;
566
+ font-size: 12px; color: var(--ink2); transition: background 0.1s; }
567
+ .browse-entry:hover { background: var(--surface); color: var(--ink); }
568
+ .browse-entry .be-icon { font-size: 13px; flex-shrink: 0; }
569
+ .browse-entry .be-name { font-family: 'DM Mono', monospace; }
570
+ .browse-entry .be-ss { color: var(--accent); }
571
+ .browse-footer { padding: 10px 16px; border-top: 1px solid var(--border);
572
+ display: flex; justify-content: flex-end; gap: 8px; flex-shrink: 0; }
573
+ .browse-cancel-btn { padding: 7px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;
574
+ cursor: pointer; border: 1px solid var(--border2); background: var(--surface2); color: var(--ink2); font-family: 'Manrope', sans-serif; }
575
+
576
+ /* Save As dialog */
577
+ .saveas-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.65);
578
+ z-index: 999; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
579
+ .saveas-overlay.open { display: flex; }
580
+ .saveas-dialog { background: var(--surface); border: 1px solid var(--border2); border-radius: 12px;
581
+ padding: 24px 28px; width: 420px; max-width: 95vw; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
582
+ .saveas-input { width: 100%; background: var(--bg3); border: 1px solid var(--border2); border-radius: 6px;
583
+ padding: 8px 12px; color: var(--ink); font-family: 'DM Mono', monospace; font-size: 13px; outline: none; margin: 8px 0 16px; }
584
+ .saveas-input:focus { border-color: var(--accent); }
585
+
586
+ /* autosave flash */
587
+ @keyframes savedFlash { 0%{opacity:1} 60%{opacity:1} 100%{opacity:0} }
588
+ .saved-toast { position: fixed; bottom: 32px; right: 24px; padding: 6px 14px; border-radius: 6px;
589
+ background: var(--accent); color: #0d1f1e; font-size: 11px; font-weight: 700;
590
+ font-family: 'Manrope', sans-serif; pointer-events: none; animation: savedFlash 1.6s forwards; z-index: 9999; }
591
+
592
+ </style>
593
+ </head>
594
+ <body data-theme="dark">
595
+
596
+ <!-- TOPBAR -->
597
+ <div class="topbar">
598
+ <div class="logo">
599
+ <svg width="26" height="26" viewBox="0 0 52 52" fill="none" style="flex-shrink:0">
600
+ <path d="M26 3 L47 14.5 L47 37.5 L26 49 L5 37.5 L5 14.5 Z" fill="#0b7a75"/>
601
+ <path d="M20 17 L16 20 C14.5 21 14.5 22.5 16 23.5 L16 28.5 C14.5 29.5 14.5 31 16 32 L20 35" stroke="#b8f000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
602
+ <path d="M32 17 L36 20 C37.5 21 37.5 22.5 36 23.5 L36 28.5 C37.5 29.5 37.5 31 36 32 L32 35" stroke="#b8f000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
603
+ <circle cx="26" cy="26" r="2.5" fill="#b8f000"/>
604
+ </svg>
605
+ <span class="logo-s">Struct</span><span class="logo-sc">Script</span>
606
+ <span class="logo-v">Editor</span>
607
+ </div>
608
+ <div class="divider"></div>
609
+ <div class="nav-tabs">
610
+ <button class="nav-tab active" onclick="showPage('playground',this)" id="tab-playground">Editor</button>
611
+ <button class="nav-tab" onclick="showPage('docs',this)" id="tab-docs">Docs</button>
612
+ </div>
613
+ <div class="topbar-right">
614
+ <button class="file-btn" onclick="newFile()">+ New</button>
615
+ <button class="file-btn" onclick="openFile()">📂 Open</button>
616
+ <button class="file-btn" onclick="saveFile()">💾 Save</button>
617
+ <button class="file-btn" onclick="saveFileAs()">💾 Save As…</button>
618
+ <button class="file-btn" onclick="saveHTML()" id="save-html-btn" style="display:none">🌐 Save .html</button>
619
+ <div class="divider"></div>
620
+ <button class="icon-btn" onclick="toggleTheme()" title="Toggle theme" id="theme-btn">🌙</button>
621
+ <button class="run-btn" onclick="runCode()">▶ Run <span class="run-kbd">Ctrl+↵</span></button>
622
+ </div>
623
+ <input type="file" id="file-input" accept=".ss,.txt" style="display:none" onchange="handleFileOpen(event)">
624
+ </div>
625
+
626
+ <div class="app-body">
627
+
628
+ <!-- PLAYGROUND PAGE (editor + sidebar) -->
629
+ <div id="page-playground" class="page active">
630
+ <div class="playground">
631
+
632
+ <!-- Sidebar: file explorer -->
633
+ <div class="sidebar">
634
+ <div class="sidebar-header">
635
+ <span class="sidebar-title">Files</span>
636
+ <button class="sidebar-action" onclick="newFile()" title="New file">+</button>
637
+ <button class="sidebar-action" onclick="openFile()" title="Open file">📂</button>
638
+ </div>
639
+ <button class="sidebar-new-btn" onclick="newFile()">+ New .ss file</button>
640
+ <div class="file-tree" id="file-tree-list">
641
+ <div style="padding:16px 14px;font-size:11px;color:var(--muted)">Loading…</div>
642
+ </div>
643
+ </div>
644
+
645
+ <!-- Editor column -->
646
+ <div class="editor-column">
647
+
648
+ <!-- File path bar -->
649
+ <div class="filepath-bar">
650
+ <span style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">📄</span>
651
+ <span class="filepath-text" id="fp-path" title="Click to reveal in folder" onclick="if(window.electronAPI&&currentFilePath)window.electronAPI.showInFolder({filePath:currentFilePath})" style="cursor:pointer">untitled.ss</span>
652
+ <span class="fp-save-badge" id="fp-unsaved">unsaved</span>
653
+ </div>
654
+
655
+ <!-- Editor pane -->
656
+ <div class="editor-pane">
657
+ <div class="pane-bar">
658
+ <div class="wc-dots">
659
+ <div class="wc-dot" style="background:#f87171"></div>
660
+ <div class="wc-dot" style="background:#fcd34d"></div>
661
+ <div class="wc-dot" style="background:#6ee7b7"></div>
662
+ </div>
663
+ <span class="filename" id="current-filename">untitled.ss</span>
664
+ <span class="modified" id="modified-indicator" style="display:none">●</span>
665
+ </div>
666
+ <div class="editor-container">
667
+ <div class="line-numbers" id="line-numbers"></div>
668
+ <div class="code-wrap" id="code-wrap">
669
+ <div id="highlight-layer"></div>
670
+ <textarea id="code-editor" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
671
+ <div id="autocomplete"></div>
672
+ </div>
673
+ </div>
674
+ </div>
675
+
676
+ <!-- Output pane -->
677
+ <div class="output-pane">
678
+ <div class="out-tabs">
679
+ <button class="out-tab active" onclick="switchOutTab('output',this)" id="outtab-output">Output</button>
680
+ <button class="out-tab" onclick="switchOutTab('errors',this)" id="outtab-errors">Errors <span id="err-badge" style="display:none;background:var(--err);color:#fff;font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px"></span></button>
681
+ <button class="out-tab" onclick="switchOutTab('web',this)" id="outtab-web" style="display:none">🌐 Preview</button>
682
+ </div>
683
+ <div id="out-output" class="out-panel active">
684
+ <div id="output-display">
685
+ <div class="out-line info"><span class="out-prefix">›</span><span class="out-text">StructScript Editor — Ready</span></div>
686
+ <div class="out-line info"><span class="out-prefix">›</span><span class="out-text">Press ▶ Run or Ctrl+Enter to execute</span></div>
687
+ </div>
688
+ </div>
689
+ <div id="out-errors" class="out-panel">
690
+ <div id="error-display">
691
+ <div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>
692
+ </div>
693
+ </div>
694
+ <div id="out-web" class="out-panel">
695
+ <iframe id="web-preview" style="width:100%;height:100%;border:none;background:#fff"></iframe>
696
+ </div>
697
+ </div>
698
+
699
+ <!-- Status bar -->
700
+ <div class="status-bar">
701
+ <span id="sb-lang">StructScript</span>
702
+ <span class="sb-sep">│</span>
703
+ <span id="sb-cursor">Ln 1, Col 1</span>
704
+ <span class="sb-sep">│</span>
705
+ <span id="sb-dot" class="sb-dot ok" style="width:7px;height:7px;border-radius:50%;background:var(--lime);display:inline-block;margin-right:2px"></span>
706
+ <span id="sb-status">Ready</span>
707
+ <span class="sb-sep">│</span>
708
+ <span id="sb-time"></span>
709
+ </div>
710
+
711
+ </div><!-- editor-column -->
712
+
713
+ </div><!-- playground -->
714
+ </div><!-- page-playground -->
715
+
716
+ <!-- DOCS PAGE -->
717
+ <div id="page-docs" class="page">
718
+ <div class="docs-page">
719
+ <div class="docs-hero">
720
+ <h2><em>StructScript</em> v1.1<br>Language Reference</h2>
721
+ <p>A clean, readable language blending natural English with Python-style indentation. Built for beginners, scripters, and systems thinkers.</p>
722
+ </div>
723
+
724
+ <div class="docs-grid">
725
+ <div class="docs-card"><h4>🧱 Structured</h4><p>Indentation-based blocks. No braces needed. Clean and readable at a glance.</p></div>
726
+ <div class="docs-card"><h4>📖 Readable</h4><p>Keywords read like English: <code>say</code>, <code>define</code>, <code>count from to</code>, <code>repeat times</code>.</p></div>
727
+ <div class="docs-card"><h4>⚡ Capable</h4><p>Functions, structs, lists, error handling, string ops, math — all built in.</p></div>
728
+ <div class="docs-card"><h4>🛡️ Safe</h4><p>Clear error messages with line numbers and code snippets. Stack overflow protection.</p></div>
729
+ </div>
730
+
731
+ <div class="docs-section">
732
+ <h3>Variables & Constants</h3>
733
+ <table class="docs-table">
734
+ <tr><th>Syntax</th><th>Example</th><th>Notes</th></tr>
735
+ <tr><td><code>let name = value</code></td><td><code>let x = 42</code></td><td>Mutable variable</td></tr>
736
+ <tr><td><code>const name = value</code></td><td><code>const PI = 3.14159</code></td><td>Immutable constant</td></tr>
737
+ <tr><td><code>set name = value</code></td><td><code>set x = x + 1</code></td><td>Reassign existing variable</td></tr>
738
+ <tr><td><code>set obj.field = val</code></td><td><code>set person.age = 30</code></td><td>Struct field assignment</td></tr>
739
+ </table>
740
+ </div>
741
+
742
+ <div class="docs-section">
743
+ <h3>Types</h3>
744
+ <table class="docs-table">
745
+ <tr><th>Type</th><th>Examples</th><th>Notes</th></tr>
746
+ <tr><td>Number</td><td><code>42</code>, <code>3.14</code>, <code>-7</code></td><td>Integer or float</td></tr>
747
+ <tr><td>String</td><td><code>"hello"</code>, <code>'world'</code></td><td>Single or double quotes</td></tr>
748
+ <tr><td>Boolean</td><td><code>true</code>, <code>false</code></td><td>Logical values</td></tr>
749
+ <tr><td>Nothing</td><td><code>nothing</code></td><td>Null/None equivalent</td></tr>
750
+ <tr><td>List</td><td><code>[1, 2, 3]</code></td><td>Ordered, mutable collection</td></tr>
751
+ <tr><td>Struct</td><td><code>Point()</code></td><td>Custom data type</td></tr>
752
+ </table>
753
+ </div>
754
+
755
+ <div class="docs-section">
756
+ <h3>Operators</h3>
757
+ <table class="docs-table">
758
+ <tr><th>Category</th><th>Operators</th></tr>
759
+ <tr><td>Arithmetic</td><td><code>+ - * / % ** //</code></td></tr>
760
+ <tr><td>Comparison</td><td><code>== != &gt; &lt; &gt;= &lt;=</code></td></tr>
761
+ <tr><td>Logical</td><td><code>and or not</code></td></tr>
762
+ <tr><td>String</td><td><code>+</code> (concat), <code>*</code> (repeat)</td></tr>
763
+ </table>
764
+ </div>
765
+
766
+ <div class="docs-section">
767
+ <h3>Control Flow</h3>
768
+ <div class="docs-code">
769
+ <span class="kw">if</span> x > <span class="num">0</span>:
770
+ <span class="kw">say</span> <span class="str">"positive"</span>
771
+ <span class="kw">else if</span> x < <span class="num">0</span>:
772
+ <span class="kw">say</span> <span class="str">"negative"</span>
773
+ <span class="kw">else</span>:
774
+ <span class="kw">say</span> <span class="str">"zero"</span>
775
+
776
+ <span class="kw">while</span> x < <span class="num">100</span>:
777
+ <span class="kw">set</span> x = x * <span class="num">2</span>
778
+
779
+ <span class="kw">repeat</span> <span class="num">5</span> <span class="kw">times</span>:
780
+ <span class="kw">say</span> <span class="str">"hello"</span>
781
+
782
+ <span class="kw">count</span> i <span class="kw">from</span> <span class="num">1</span> <span class="kw">to</span> <span class="num">10</span>:
783
+ <span class="kw">say</span> i
784
+
785
+ <span class="kw">for each</span> item <span class="kw">in</span> myList:
786
+ <span class="kw">say</span> item</div>
787
+ </div>
788
+
789
+ <div class="docs-section">
790
+ <h3>Functions</h3>
791
+ <div class="docs-code">
792
+ <span class="kw">define</span> <span class="fn">greet</span>(name):
793
+ <span class="kw">say</span> <span class="str">"Hello, "</span> + name
794
+
795
+ <span class="kw">define</span> <span class="fn">add</span>(a, b):
796
+ <span class="kw">return</span> a + b
797
+
798
+ <span class="cmt">// Default parameters</span>
799
+ <span class="kw">define</span> <span class="fn">power</span>(base, exp = <span class="num">2</span>):
800
+ <span class="kw">return</span> base ** exp
801
+
802
+ <span class="fn">greet</span>(<span class="str">"World"</span>)
803
+ <span class="kw">let</span> result = <span class="fn">add</span>(<span class="num">3</span>, <span class="num">4</span>)</div>
804
+ </div>
805
+
806
+ <div class="docs-section">
807
+ <h3>Error Handling</h3>
808
+ <div class="docs-code">
809
+ <span class="kw">try</span>:
810
+ <span class="kw">let</span> x = <span class="num">10</span> / <span class="num">0</span>
811
+ <span class="kw">catch</span> err:
812
+ <span class="kw">say</span> <span class="str">"Caught: "</span> + err
813
+
814
+ <span class="kw">try</span>:
815
+ <span class="fn">riskyFunction</span>()
816
+ <span class="kw">catch</span> e:
817
+ <span class="kw">say</span> <span class="str">"Error: "</span> + e
818
+ <span class="kw">finally</span>:
819
+ <span class="kw">say</span> <span class="str">"Always runs"</span></div>
820
+ </div>
821
+
822
+ <div class="docs-section">
823
+ <h3>Built-in Functions</h3>
824
+ <table class="docs-table">
825
+ <tr><th>Function</th><th>Description</th><th>Example</th></tr>
826
+ <tr><td><code>say(x)</code></td><td>Print to output</td><td><code>say "hi"</code></td></tr>
827
+ <tr><td><code>length(x)</code></td><td>Length of list/string</td><td><code>length([1,2,3])</code> → 3</td></tr>
828
+ <tr><td><code>push(list, val)</code></td><td>Append to list</td><td><code>push(nums, 4)</code></td></tr>
829
+ <tr><td><code>pop(list)</code></td><td>Remove last item</td><td><code>pop(nums)</code></td></tr>
830
+ <tr><td><code>join(list, sep)</code></td><td>Join list to string</td><td><code>join(items, ", ")</code></td></tr>
831
+ <tr><td><code>split(str, sep)</code></td><td>Split string to list</td><td><code>split("a,b", ",")</code></td></tr>
832
+ <tr><td><code>upper(str)</code></td><td>Uppercase string</td><td><code>upper("hi")</code> → "HI"</td></tr>
833
+ <tr><td><code>lower(str)</code></td><td>Lowercase string</td><td><code>lower("HI")</code> → "hi"</td></tr>
834
+ <tr><td><code>trim(str)</code></td><td>Strip whitespace</td><td><code>trim(" hi ")</code></td></tr>
835
+ <tr><td><code>contains(str, sub)</code></td><td>Check substring</td><td><code>contains("hello", "ell")</code></td></tr>
836
+ <tr><td><code>replace(str,old,new)</code></td><td>Replace in string</td><td><code>replace("hi","h","b")</code></td></tr>
837
+ <tr><td><code>slice(list, a, b)</code></td><td>Sub-list or substring</td><td><code>slice(nums, 0, 3)</code></td></tr>
838
+ <tr><td><code>sort(list)</code></td><td>Sort a list</td><td><code>sort([3,1,2])</code></td></tr>
839
+ <tr><td><code>reverse(list)</code></td><td>Reverse a list</td><td><code>reverse([1,2,3])</code></td></tr>
840
+ <tr><td><code>range(n)</code> / <code>range(a,b)</code></td><td>Generate number list</td><td><code>range(5)</code> → [0..4]</td></tr>
841
+ <tr><td><code>abs(n)</code></td><td>Absolute value</td><td><code>abs(-5)</code> → 5</td></tr>
842
+ <tr><td><code>sqrt(n)</code></td><td>Square root</td><td><code>sqrt(16)</code> → 4</td></tr>
843
+ <tr><td><code>floor/ceil/round</code></td><td>Rounding</td><td><code>floor(3.7)</code> → 3</td></tr>
844
+ <tr><td><code>max/min</code></td><td>Max/min of args</td><td><code>max(1,5,3)</code> → 5</td></tr>
845
+ <tr><td><code>random()</code></td><td>Random 0–1 float</td><td><code>random()</code></td></tr>
846
+ <tr><td><code>randint(a,b)</code></td><td>Random integer</td><td><code>randint(1,6)</code></td></tr>
847
+ <tr><td><code>str(x)</code></td><td>Convert to string</td><td><code>str(42)</code> → "42"</td></tr>
848
+ <tr><td><code>num(x)</code></td><td>Convert to number</td><td><code>num("42")</code> → 42</td></tr>
849
+ <tr><td><code>bool(x)</code></td><td>Convert to boolean</td><td><code>bool(0)</code> → false</td></tr>
850
+ <tr><td><code>type(x)</code></td><td>Get type name</td><td><code>type(42)</code> → "number"</td></tr>
851
+ <tr><td><code>isNothing(x)</code></td><td>Check for nothing</td><td><code>isNothing(val)</code></td></tr>
852
+ </table>
853
+ </div>
854
+
855
+ </div>
856
+ </div>
857
+
858
+ <div id="input-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:999;align-items:center;justify-content:center;backdrop-filter:blur(3px)">
859
+ <div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:24px 28px;min-width:360px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)">
860
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
861
+ <div style="width:28px;height:28px;border-radius:6px;background:var(--accent-dim);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px">⌨</div>
862
+ <div>
863
+ <div style="font-size:13px;font-weight:700;color:var(--ink)">Program needs input</div>
864
+ <div style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">Fill in values then click Run</div>
865
+ </div>
866
+ </div>
867
+ <div id="input-modal-fields"></div>
868
+ <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
869
+ <button onclick="cancelInputModal()" style="padding:7px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
870
+ <button onclick="submitInputModal()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">▶ Run</button>
871
+ </div>
872
+ </div>
873
+ </div>
874
+
875
+
876
+ </div>
877
+
878
+ </div><!-- app-body -->
879
+
880
+ <!-- INPUT MODAL -->
881
+ <div id="input-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:999;align-items:center;justify-content:center;backdrop-filter:blur(3px)">
882
+ <div style="background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:24px 28px;min-width:360px;max-width:480px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,0.5)">
883
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:18px">
884
+ <div style="width:28px;height:28px;border-radius:6px;background:var(--accent-dim);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px">⌨</div>
885
+ <div>
886
+ <div style="font-size:13px;font-weight:700;color:var(--ink)">Program needs input</div>
887
+ <div style="font-size:10px;color:var(--muted);font-family:'DM Mono',monospace">Fill in values then click Run</div>
888
+ </div>
889
+ </div>
890
+ <div id="input-modal-fields"></div>
891
+ <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
892
+ <button onclick="cancelInputModal()" style="padding:7px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
893
+ <button onclick="submitInputModal()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">▶ Run</button>
894
+ </div>
895
+ </div>
896
+ </div>
897
+
898
+ <!-- BROWSE DIALOG -->
899
+ <div class="browse-overlay" id="browse-overlay">
900
+ <div class="browse-dialog">
901
+ <div class="browse-bar">
902
+ <button class="browse-up-btn" onclick="browseUp()">↑ Up</button>
903
+ <input class="browse-path-input" id="browse-path-input" placeholder="/path/to/folder"
904
+ onkeydown="if(event.key==='Enter')browseDirFromInput()">
905
+ <button class="browse-up-btn" onclick="browseDirFromInput()">Go</button>
906
+ </div>
907
+ <div class="browse-list" id="browse-list"></div>
908
+ <div class="browse-footer">
909
+ <button class="browse-cancel-btn" onclick="closeBrowseDialog()">Cancel</button>
910
+ </div>
911
+ </div>
912
+ </div>
913
+
914
+ <!-- SAVE AS DIALOG -->
915
+ <div class="saveas-overlay" id="saveas-overlay">
916
+ <div class="saveas-dialog">
917
+ <div style="font-size:13px;font-weight:700;color:var(--ink);margin-bottom:4px">Save As</div>
918
+ <div style="font-size:11px;color:var(--muted);margin-bottom:8px">Enter a full file path (e.g. /home/user/myscript.ss)</div>
919
+ <input class="saveas-input" id="saveas-input" placeholder="/home/you/script.ss"
920
+ onkeydown="if(event.key==='Enter')confirmSaveAs();if(event.key==='Escape')closeSaveAsDialog()">
921
+ <div style="display:flex;justify-content:flex-end;gap:8px">
922
+ <button onclick="closeSaveAsDialog()" style="padding:7px 14px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid var(--border2);background:var(--surface2);color:var(--ink2);font-family:'Manrope',sans-serif">Cancel</button>
923
+ <button onclick="confirmSaveAs()" style="padding:7px 20px;border-radius:6px;font-size:12px;font-weight:800;cursor:pointer;border:none;background:var(--lime);color:#0d1f1e;font-family:'Manrope',sans-serif">Save</button>
924
+ </div>
925
+ </div>
926
+ </div>
927
+
928
+ <script>
929
+ // ============================================================
930
+ // STRUCTSCRIPT INTERPRETER v1.1
931
+ // ============================================================
932
+
933
+ class SSError extends Error {
934
+ constructor(msg, line, snippet) {
935
+ super(msg);
936
+ this.ssLine = line;
937
+ this.snippet = snippet;
938
+ }
939
+ }
940
+
941
+ class ReturnSignal { constructor(v){this.value=v;} }
942
+ class BreakSignal {}
943
+ class ContinueSignal {}
944
+
945
+ class Environment {
946
+ constructor(parent=null) { this.vars={}; this.consts=new Set(); this.parent=parent; }
947
+ get(name) {
948
+ if (name in this.vars) return this.vars[name];
949
+ if (this.parent) return this.parent.get(name);
950
+ throw new SSError(`Undefined variable: "${name}"`);
951
+ }
952
+ set(name, value) {
953
+ if (name in this.vars) {
954
+ if (this.consts.has(name)) throw new SSError(`Cannot reassign constant "${name}"`);
955
+ this.vars[name] = value; return;
956
+ }
957
+ if (this.parent && this.parent.has(name)) { this.parent.set(name,value); return; }
958
+ this.vars[name] = value;
959
+ }
960
+ has(name) { return name in this.vars || (this.parent ? this.parent.has(name) : false); }
961
+ define(name, value, isConst=false) { this.vars[name]=value; if(isConst)this.consts.add(name); }
962
+ }
963
+
964
+ class Interpreter {
965
+ constructor(outputFn, warnFn) {
966
+ this.output = outputFn;
967
+ this.warn = warnFn || (()=>{});
968
+ this.globals = new Environment();
969
+ this.structs = {};
970
+ this.callDepth = 0;
971
+ this.sourceLines = [];
972
+ this._registerBuiltins();
973
+ }
974
+
975
+ _registerBuiltins() {
976
+ const G = this.globals;
977
+ // I/O
978
+ G.define('say', (a) => { this.output(this._str(a[0])); return null; });
979
+ // Math
980
+ G.define('abs', a => Math.abs(a[0]));
981
+ G.define('sqrt', a => Math.sqrt(a[0]));
982
+ G.define('floor', a => Math.floor(a[0]));
983
+ G.define('ceil', a => Math.ceil(a[0]));
984
+ G.define('round', a => Math.round(a[0]));
985
+ G.define('max', a => Math.max(...a));
986
+ G.define('min', a => Math.min(...a));
987
+ G.define('pow', a => Math.pow(a[0],a[1]));
988
+ G.define('log', a => Math.log(a[0]));
989
+ G.define('log10', a => Math.log10(a[0]));
990
+ G.define('sin', a => Math.sin(a[0]));
991
+ G.define('cos', a => Math.cos(a[0]));
992
+ G.define('tan', a => Math.tan(a[0]));
993
+ G.define('random', a => Math.random());
994
+ G.define('randint',a => Math.floor(Math.random()*(a[1]-a[0]+1))+a[0]);
995
+ // Type conversion
996
+ G.define('str', a => String(a[0] ?? ''));
997
+ G.define('num', a => { const n=Number(a[0]); if(isNaN(n)) throw new SSError(`Cannot convert "${a[0]}" to number`); return n; });
998
+ G.define('bool', a => Boolean(a[0]));
999
+ G.define('type', a => { if(Array.isArray(a[0]))return'list'; if(a[0]===null||a[0]===undefined)return'nothing'; return typeof a[0]; });
1000
+ G.define('isNothing', a => a[0]===null||a[0]===undefined);
1001
+ // String
1002
+ G.define('upper', a => String(a[0]).toUpperCase());
1003
+ G.define('lower', a => String(a[0]).toLowerCase());
1004
+ G.define('trim', a => String(a[0]).trim());
1005
+ G.define('length', a => { if(Array.isArray(a[0]))return a[0].length; return String(a[0]).length; });
1006
+ G.define('contains',a => String(a[0]).includes(String(a[1])));
1007
+ G.define('startsWith',a=>String(a[0]).startsWith(String(a[1])));
1008
+ G.define('endsWith', a=>String(a[0]).endsWith(String(a[1])));
1009
+ G.define('replace', a => String(a[0]).split(String(a[1])).join(String(a[2])));
1010
+ G.define('split', a => String(a[0]).split(String(a[1])));
1011
+ G.define('indexOf', a => { const i=Array.isArray(a[0])?a[0].indexOf(a[1]):String(a[0]).indexOf(String(a[1])); return i; });
1012
+ G.define('slice', a => { const s=a[0]; const from=a[1]??0; const to=a[2]; return Array.isArray(s)?s.slice(from,to):String(s).slice(from,to); });
1013
+ G.define('char', a => String.fromCharCode(a[0]));
1014
+ G.define('charCode',a => String(a[0]).charCodeAt(0));
1015
+ G.define('repeat', a => String(a[0]).repeat(a[1]));
1016
+ G.define('format', a => { let s=String(a[0]); for(let i=1;i<a.length;i++) s=s.replace('{}',this._str(a[i])); return s; });
1017
+ // List
1018
+ G.define('push', a => { if(!Array.isArray(a[0]))throw new SSError('push() requires a list'); a[0].push(a[1]); return null; });
1019
+ G.define('pop', a => { if(!Array.isArray(a[0]))throw new SSError('pop() requires a list'); return a[0].pop()??null; });
1020
+ G.define('shift', a => { if(!Array.isArray(a[0]))return null; return a[0].shift()??null; });
1021
+ G.define('unshift', a => { if(Array.isArray(a[0]))a[0].unshift(a[1]); return null; });
1022
+ G.define('join', a => { if(!Array.isArray(a[0]))throw new SSError('join() requires a list'); return a[0].map(x=>this._str(x)).join(a[1]??''); });
1023
+ G.define('sort', a => { if(!Array.isArray(a[0]))throw new SSError('sort() requires a list'); return [...a[0]].sort((x,y)=>x<y?-1:x>y?1:0); });
1024
+ G.define('reverse', a => { if(!Array.isArray(a[0]))throw new SSError('reverse() requires a list'); return [...a[0]].reverse(); });
1025
+ G.define('range', a => { const from=a[1]!==undefined?a[0]:0; const to=a[1]!==undefined?a[1]:a[0]; const r=[]; for(let i=from;i<to;i++)r.push(i); return r; });
1026
+ G.define('flatten', a => { if(!Array.isArray(a[0]))return[a[0]]; return a[0].flat(); });
1027
+ G.define('unique', a => [...new Set(a[0])]);
1028
+ G.define('sum', a => { if(!Array.isArray(a[0]))return 0; return a[0].reduce((acc,v)=>acc+v,0); });
1029
+ G.define('avg', a => { if(!Array.isArray(a[0])||!a[0].length)return 0; return a[0].reduce((acc,v)=>acc+v,0)/a[0].length; });
1030
+ G.define('map', a => { if(!Array.isArray(a[0]))throw new SSError('map() requires a list'); const fn=a[1]; return a[0].map(item=>{ const r=fn([item],null); return r instanceof ReturnSignal?r.value:r; }); });
1031
+ G.define('filter', a => { if(!Array.isArray(a[0]))throw new SSError('filter() requires a list'); const fn=a[1]; return a[0].filter(item=>{ const r=fn([item],null); return r instanceof ReturnSignal?r.value:r; }); });
1032
+ G.define('reduce', a => { if(!Array.isArray(a[0]))throw new SSError('reduce() requires a list'); const fn=a[1]; let acc=a[2]; return a[0].reduce((ac,item)=>{ const r=fn([ac,item],null); return r instanceof ReturnSignal?r.value:r; },acc); });
1033
+ // Utility
1034
+ G.define('print', a => { this.output(a.map(x=>this._str(x)).join(' ')); return null; });
1035
+ G.define('assert', a => { if(!a[0]) throw new SSError('Assertion failed'+(a[1]?': '+a[1]:'')); return null; });
1036
+ G.define('error', a => { throw new SSError(String(a[0]??'Runtime error')); });
1037
+ G.define('input', a => {
1038
+ const prompt = a[0] !== undefined ? this._str(a[0]) : '';
1039
+ if (this._inputQueue && this._inputQueue.length > 0) {
1040
+ const val = this._inputQueue.shift();
1041
+ this.output('» ' + (prompt ? prompt + ' ' : '') + val);
1042
+ return val;
1043
+ }
1044
+ throw new SSError('input() called but no value was provided');
1045
+ });
1046
+ }
1047
+
1048
+ run(source) {
1049
+ this.sourceLines = source.split('\n');
1050
+ this._execBlock(this.sourceLines, 0, this.sourceLines.length, this.globals);
1051
+ }
1052
+
1053
+ _indent(line) { let i=0; while(i<line.length&&line[i]===' ')i++; return i; }
1054
+
1055
+ _findBlock(lines, startLine, baseIndent) {
1056
+ const block=[]; let i=startLine;
1057
+ while(i<lines.length){
1058
+ const line=lines[i], t=line.trim();
1059
+ if(!t||t.startsWith('//')){ i++; continue; }
1060
+ if(this._indent(line)<=baseIndent) break;
1061
+ block.push(i); i++;
1062
+ }
1063
+ return {block, next:i};
1064
+ }
1065
+
1066
+ _execBlock(lines, start, end, env) {
1067
+ let i=start;
1068
+ while(i<end){
1069
+ const raw=lines[i], t=raw.trim();
1070
+ if(!t||t.startsWith('//')){ i++; continue; }
1071
+ const indent=this._indent(raw);
1072
+ const result=this._execLine(t, lines, i, indent, env);
1073
+ if(result instanceof ReturnSignal||result instanceof BreakSignal||result instanceof ContinueSignal) return result;
1074
+ if(result&&result._skip){ i=result._skip; continue; }
1075
+ i++;
1076
+ }
1077
+ }
1078
+
1079
+ _execLine(line, allLines, lineIdx, indent, env) {
1080
+ try { return this._exec(line, allLines, lineIdx, indent, env); }
1081
+ catch(e) {
1082
+ if(e instanceof SSError){ if(e.ssLine===undefined)e.ssLine=lineIdx; if(!e.snippet)e.snippet=allLines[lineIdx]; throw e; }
1083
+ throw new SSError(e.message, lineIdx, allLines[lineIdx]);
1084
+ }
1085
+ }
1086
+
1087
+ _exec(line, allLines, lineIdx, indent, env) {
1088
+ // say (shorthand keyword)
1089
+ if(line.startsWith('say ')){ const v=this._eval(line.slice(4).trim(),env); this.output(this._str(v)); return null; }
1090
+
1091
+ // let / const
1092
+ if(line.startsWith('let ')||line.startsWith('const ')){
1093
+ const isConst=line.startsWith('const ');
1094
+ const rest=line.slice(isConst?6:4);
1095
+ const eq=rest.indexOf('='); if(eq===-1) throw new SSError(`Missing = in declaration`);
1096
+ const name=rest.slice(0,eq).trim();
1097
+ const val=this._eval(rest.slice(eq+1).trim(),env);
1098
+ env.define(name,val,isConst); return null;
1099
+ }
1100
+
1101
+ // set
1102
+ if(line.startsWith('set ')){
1103
+ const rest=line.slice(4);
1104
+ // compound assignment: set x += 1
1105
+ const compMatch=rest.match(/^([a-zA-Z_][\w.]*)\s*(\+|-|\*|\/|%|\*\*|\/\/)=\s*(.+)$/);
1106
+ if(compMatch){
1107
+ const [,target,op,valExpr]=compMatch;
1108
+ const cur=this._getTarget(target,env);
1109
+ const val=this._eval(valExpr.trim(),env);
1110
+ const newVal=this._applyOp(cur,op,val);
1111
+ this._setTarget(target,newVal,env); return null;
1112
+ }
1113
+ const eq=rest.indexOf('='); if(eq===-1) throw new SSError(`Missing = in set`);
1114
+ const target=rest.slice(0,eq).trim(), val=this._eval(rest.slice(eq+1).trim(),env);
1115
+ this._setTarget(target,val,env); return null;
1116
+ }
1117
+
1118
+ // define function
1119
+ if(line.startsWith('define ')){
1120
+ const sig=line.slice(7).trim().replace(/:$/,'');
1121
+ const po=sig.indexOf('('), pc=sig.lastIndexOf(')');
1122
+ const fname=sig.slice(0,po).trim();
1123
+ const rawParams=sig.slice(po+1,pc).trim();
1124
+ const params=rawParams?rawParams.split(',').map(p=>p.trim()).filter(Boolean):[];
1125
+ const defaults={};
1126
+ params.forEach((p,i)=>{ const eq=p.indexOf('='); if(eq!==-1){ defaults[p.slice(0,eq).trim()]=this._eval(p.slice(eq+1).trim(),env); params[i]=p.slice(0,eq).trim(); }});
1127
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1128
+ env.define(fname,(args,callEnv)=>{
1129
+ if(this.callDepth>200) throw new SSError('Stack overflow: too many nested calls');
1130
+ this.callDepth++;
1131
+ const fnEnv=new Environment(env);
1132
+ params.forEach((p,i)=>fnEnv.define(p, args[i]!==undefined?args[i]:(defaults[p]!==undefined?defaults[p]:null)));
1133
+ const blockLines=block.map(b=>allLines[b]);
1134
+ const res=this._execBlock(blockLines,0,blockLines.length,fnEnv);
1135
+ this.callDepth--;
1136
+ if(res instanceof ReturnSignal) return res.value;
1137
+ return null;
1138
+ });
1139
+ return {_skip:next};
1140
+ }
1141
+
1142
+ // struct
1143
+ if(line.startsWith('struct ')){
1144
+ const name=line.slice(7).trim().replace(/:$/,'');
1145
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1146
+ const defs={};
1147
+ block.forEach(bi=>{ const t=allLines[bi].trim(); if(!t||t.startsWith('//'))return; const eq=t.indexOf('='); if(eq!==-1){ defs[t.slice(0,eq).trim()]=this._eval(t.slice(eq+1).trim(),this.globals); }});
1148
+ this.structs[name]=defs;
1149
+ env.define(name,(args)=>{
1150
+ const inst={__struct:name};
1151
+ Object.entries(defs).forEach(([k,v])=>{ inst[k]=Array.isArray(v)?[...v]:v; });
1152
+ return inst;
1153
+ });
1154
+ return {_skip:next};
1155
+ }
1156
+
1157
+ // class (with fields + methods)
1158
+ if(line.startsWith('class ')){
1159
+ const name=line.slice(6).trim().replace(/:$/,'');
1160
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1161
+ const fields={}, methods={};
1162
+ let i2=0;
1163
+ const bLines=block.map(b=>allLines[b]);
1164
+ while(i2<bLines.length){
1165
+ const t=bLines[i2].trim();
1166
+ if(!t||t.startsWith('//')){ i2++; continue; }
1167
+ const bIndent=this._indent(bLines[i2]);
1168
+ if(t.startsWith('define ')){
1169
+ const sig=t.slice(7).replace(/:$/,'');
1170
+ const po=sig.indexOf('('), pc=sig.lastIndexOf(')');
1171
+ const mname=sig.slice(0,po).trim();
1172
+ const rawP=sig.slice(po+1,pc).trim();
1173
+ const params=rawP?rawP.split(',').map(p=>p.trim()).filter(Boolean):[];
1174
+ const defaults2={};
1175
+ params.forEach((p,pi)=>{ const eq=p.indexOf('='); if(eq!==-1){ defaults2[p.slice(0,eq).trim()]=this._eval(p.slice(eq+1).trim(),env); params[pi]=p.slice(0,eq).trim(); }});
1176
+ const {block:mb,next:mn}=this._findBlock(bLines,i2+1,bIndent);
1177
+ methods[mname]={params, defaults:defaults2, body:mb.map(b=>bLines[b])};
1178
+ i2=mn;
1179
+ } else {
1180
+ const eq=t.indexOf('=');
1181
+ if(eq!==-1){ fields[t.slice(0,eq).trim()]=this._eval(t.slice(eq+1).trim(),this.globals); }
1182
+ i2++;
1183
+ }
1184
+ }
1185
+ this.structs[name]={...fields};
1186
+ const classEnv=env;
1187
+ env.define(name,(args)=>{
1188
+ const inst={__struct:name, __class:name};
1189
+ Object.entries(fields).forEach(([k,v])=>{ inst[k]=Array.isArray(v)?[...v]:(typeof v==='object'&&v?Object.assign({},v):v); });
1190
+ // Bind methods
1191
+ Object.entries(methods).forEach(([mname,m])=>{
1192
+ inst[mname]=(callArgs)=>{
1193
+ if(this.callDepth>200) throw new SSError('Stack overflow');
1194
+ this.callDepth++;
1195
+ const fnEnv=new Environment(classEnv);
1196
+ fnEnv.define('self',inst);
1197
+ fnEnv.define('this',inst);
1198
+ m.params.forEach((p,i)=>fnEnv.define(p, callArgs[i]!==undefined?callArgs[i]:(m.defaults[p]!==undefined?m.defaults[p]:null)));
1199
+ const res=this._execBlock(m.body,0,m.body.length,fnEnv);
1200
+ this.callDepth--;
1201
+ if(res instanceof ReturnSignal) return res.value;
1202
+ return null;
1203
+ };
1204
+ });
1205
+ // Call init if exists
1206
+ if(inst.init) inst.init(args);
1207
+ return inst;
1208
+ });
1209
+ return {_skip:next};
1210
+ }
1211
+
1212
+ // return
1213
+ if(line==='return') return new ReturnSignal(null);
1214
+ if(line.startsWith('return ')) return new ReturnSignal(this._eval(line.slice(7).trim(),env));
1215
+
1216
+ // break / continue
1217
+ if(line==='break') return new BreakSignal();
1218
+ if(line==='continue') return new ContinueSignal();
1219
+
1220
+ // try/catch/finally
1221
+ if(line==='try:'){
1222
+ const {block:tryBlock,next:tryNext}=this._findBlock(allLines,lineIdx+1,indent);
1223
+ let catchVar=null, catchBlock=[], finallyBlock=[], cur=tryNext;
1224
+ if(cur<allLines.length){
1225
+ const ct=allLines[cur].trim();
1226
+ if(ct.startsWith('catch')){
1227
+ const m=ct.match(/^catch\s+(\w+):?$/); catchVar=m?m[1]:'_err';
1228
+ const {block:cb,next:cn}=this._findBlock(allLines,cur+1,this._indent(allLines[cur]));
1229
+ catchBlock=cb; cur=cn;
1230
+ }
1231
+ }
1232
+ if(cur<allLines.length&&allLines[cur].trim()==='finally:'){
1233
+ const {block:fb,next:fn}=this._findBlock(allLines,cur+1,this._indent(allLines[cur]));
1234
+ finallyBlock=fb; cur=fn;
1235
+ }
1236
+ let result=null;
1237
+ try {
1238
+ const bLines=tryBlock.map(b=>allLines[b]);
1239
+ result=this._execBlock(bLines,0,bLines.length,new Environment(env));
1240
+ } catch(e) {
1241
+ if(catchBlock.length){
1242
+ const cEnv=new Environment(env);
1243
+ if(catchVar) cEnv.define(catchVar, e.message||String(e));
1244
+ const cLines=catchBlock.map(b=>allLines[b]);
1245
+ result=this._execBlock(cLines,0,cLines.length,cEnv);
1246
+ }
1247
+ } finally {
1248
+ if(finallyBlock.length){
1249
+ const fLines=finallyBlock.map(b=>allLines[b]);
1250
+ this._execBlock(fLines,0,fLines.length,new Environment(env));
1251
+ }
1252
+ }
1253
+ return {_skip:cur};
1254
+ }
1255
+
1256
+ // if/else if/else
1257
+ if((line.startsWith('if ')&&line.endsWith(':'))){
1258
+ return this._handleIf(line,allLines,lineIdx,indent,env);
1259
+ }
1260
+ if(line.startsWith('else if ')||line==='else:') return null; // handled by if
1261
+
1262
+ // while
1263
+ if(line.startsWith('while ')&&line.endsWith(':')){
1264
+ const cond=line.slice(6,-1).trim();
1265
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1266
+ let iters=0;
1267
+ while(this._truthy(this._eval(cond,env))){
1268
+ if(++iters>50000) throw new SSError('Infinite loop detected (>50000 iterations)');
1269
+ const bLines=block.map(b=>allLines[b]);
1270
+ const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
1271
+ if(res instanceof BreakSignal) break;
1272
+ if(res instanceof ReturnSignal) return res;
1273
+ }
1274
+ return {_skip:next};
1275
+ }
1276
+
1277
+ // repeat N times
1278
+ if(line.startsWith('repeat ')){
1279
+ const m=line.match(/^repeat (.+?) times:?$/);
1280
+ if(m){
1281
+ const count=Math.floor(Number(this._eval(m[1].trim(),env)));
1282
+ if(isNaN(count)) throw new SSError(`repeat count must be a number`);
1283
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1284
+ for(let i=0;i<count;i++){
1285
+ const bLines=block.map(b=>allLines[b]);
1286
+ const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
1287
+ if(res instanceof BreakSignal) break;
1288
+ if(res instanceof ReturnSignal) return res;
1289
+ }
1290
+ return {_skip:next};
1291
+ }
1292
+ }
1293
+
1294
+ // count i from X to Y (step Z)?
1295
+ if(line.startsWith('count ')){
1296
+ const m=line.match(/^count (\w+) from (.+?) to (.+?)(?:\s+step\s+(.+?))?:?$/);
1297
+ if(m){
1298
+ const varName=m[1];
1299
+ const from=Number(this._eval(m[2].trim(),env));
1300
+ const to=Number(this._eval(m[3].trim(),env));
1301
+ const step=m[4]?Number(this._eval(m[4].trim(),env)):(from<=to?1:-1);
1302
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1303
+ for(let i=from;step>0?i<=to:i>=to;i+=step){
1304
+ const loopEnv=new Environment(env); loopEnv.define(varName,i);
1305
+ const bLines=block.map(b=>allLines[b]);
1306
+ const res=this._execBlock(bLines,0,bLines.length,loopEnv);
1307
+ if(res instanceof BreakSignal) break;
1308
+ if(res instanceof ReturnSignal) return res;
1309
+ }
1310
+ return {_skip:next};
1311
+ }
1312
+ }
1313
+
1314
+ // for each item in list
1315
+ if(line.startsWith('for each ')){
1316
+ const m=line.match(/^for each (\w+) in (.+?):?$/);
1317
+ if(m){
1318
+ const varName=m[1], listVal=this._eval(m[2].trim(),env);
1319
+ if(!Array.isArray(listVal)&&typeof listVal!=='string') throw new SSError(`"${m[2].trim()}" is not iterable`);
1320
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1321
+ for(const item of listVal){
1322
+ const loopEnv=new Environment(env); loopEnv.define(varName,item);
1323
+ const bLines=block.map(b=>allLines[b]);
1324
+ const res=this._execBlock(bLines,0,bLines.length,loopEnv);
1325
+ if(res instanceof BreakSignal) break;
1326
+ if(res instanceof ReturnSignal) return res;
1327
+ }
1328
+ return {_skip:next};
1329
+ }
1330
+ }
1331
+
1332
+ // bare expression / function call
1333
+ this._eval(line,env);
1334
+ return null;
1335
+ }
1336
+
1337
+ _handleIf(line, allLines, lineIdx, indent, env) {
1338
+ const cond=line.slice(3,-1).trim();
1339
+ const {block,next}=this._findBlock(allLines,lineIdx+1,indent);
1340
+ if(this._truthy(this._eval(cond,env))){
1341
+ const bLines=block.map(b=>allLines[b]);
1342
+ const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
1343
+ if(res instanceof ReturnSignal||res instanceof BreakSignal) return res;
1344
+ // skip else/else if chains
1345
+ let skip=next;
1346
+ while(skip<allLines.length){
1347
+ const t=allLines[skip].trim();
1348
+ if(t.startsWith('else')){
1349
+ const {next:n2}=this._findBlock(allLines,skip+1,this._indent(allLines[skip]));
1350
+ skip=n2;
1351
+ } else break;
1352
+ }
1353
+ return {_skip:skip};
1354
+ } else {
1355
+ let cur=next;
1356
+ while(cur<allLines.length){
1357
+ const t=allLines[cur].trim();
1358
+ const curIndent=this._indent(allLines[cur]);
1359
+ if(t.startsWith('else if ')&&t.endsWith(':')){
1360
+ const cond2=t.slice(8,-1).trim();
1361
+ const {block:b2,next:n2}=this._findBlock(allLines,cur+1,curIndent);
1362
+ if(this._truthy(this._eval(cond2,env))){
1363
+ const bLines=b2.map(b=>allLines[b]);
1364
+ const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
1365
+ if(res instanceof ReturnSignal||res instanceof BreakSignal) return res;
1366
+ // skip remaining else chains
1367
+ let skip2=n2;
1368
+ while(skip2<allLines.length){ const tt=allLines[skip2].trim(); if(tt.startsWith('else')){ const {next:n3}=this._findBlock(allLines,skip2+1,this._indent(allLines[skip2])); skip2=n3; } else break; }
1369
+ return {_skip:skip2};
1370
+ }
1371
+ cur=n2;
1372
+ } else if(t==='else:'){
1373
+ const {block:b3,next:n3}=this._findBlock(allLines,cur+1,curIndent);
1374
+ const bLines=b3.map(b=>allLines[b]);
1375
+ const res=this._execBlock(bLines,0,bLines.length,new Environment(env));
1376
+ if(res instanceof ReturnSignal||res instanceof BreakSignal) return res;
1377
+ return {_skip:n3};
1378
+ } else break;
1379
+ }
1380
+ return {_skip:cur};
1381
+ }
1382
+ }
1383
+
1384
+ _getTarget(target, env) {
1385
+ if(target.includes('.')){
1386
+ const parts=target.split('.');
1387
+ let obj=env.get(parts[0]);
1388
+ for(let i=1;i<parts.length-1;i++) obj=obj[parts[i]];
1389
+ return obj[parts[parts.length-1]];
1390
+ }
1391
+ if(target.includes('[')){ const m=target.match(/^(\w+)\[(.+)\]$/); if(m){ const arr=env.get(m[1]); return arr[this._eval(m[2],env)]; } }
1392
+ return env.get(target);
1393
+ }
1394
+
1395
+ _setTarget(target, value, env) {
1396
+ if(target.includes('.')){
1397
+ const parts=target.split('.');
1398
+ let obj=env.get(parts[0]);
1399
+ for(let i=1;i<parts.length-1;i++) obj=obj[parts[i]];
1400
+ obj[parts[parts.length-1]]=value; return;
1401
+ }
1402
+ if(target.includes('[')){ const m=target.match(/^(\w+)\[(.+)\]$/); if(m){ const arr=env.get(m[1]); arr[this._eval(m[2],env)]=value; return; } }
1403
+ env.set(target, value);
1404
+ }
1405
+
1406
+ _applyOp(a, op, b) {
1407
+ switch(op){
1408
+ case '+': return a+b; case '-': return a-b;
1409
+ case '*': return a*b; case '/': return a/b;
1410
+ case '%': return a%b; case '**': return Math.pow(a,b);
1411
+ case '//': return Math.floor(a/b);
1412
+ }
1413
+ }
1414
+
1415
+ _truthy(v) { return v!==null&&v!==undefined&&v!==false&&v!==0&&v!==''; }
1416
+
1417
+ _eval(expr, env) {
1418
+ expr=expr.trim();
1419
+ if(!expr) return null;
1420
+ if(expr==='true') return true;
1421
+ if(expr==='false') return false;
1422
+ if(expr==='null'||expr==='nothing') return null;
1423
+ // String
1424
+ if((expr[0]==='"'&&expr[expr.length-1]==='"')||(expr[0]==="'"&&expr[expr.length-1]==="'"))
1425
+ return this._parseString(expr.slice(1,-1), env);
1426
+ // Multiline string (triple quote) - simplified
1427
+ // Number
1428
+ if(/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr);
1429
+ // List
1430
+ if(expr[0]==='['&&expr[expr.length-1]===']'){
1431
+ const inner=expr.slice(1,-1).trim();
1432
+ if(!inner) return [];
1433
+ return this._splitArgs(inner).map(a=>this._eval(a,env));
1434
+ }
1435
+ // Parens
1436
+ if(expr[0]==='('&&expr[expr.length-1]===')') return this._eval(expr.slice(1,-1),env);
1437
+ // Ternary: expr if cond else expr
1438
+ const ternIdx=this._findOp(expr,' if ');
1439
+ if(ternIdx!==-1){
1440
+ const elseIdx=this._findOp(expr,' else ');
1441
+ if(elseIdx>ternIdx){
1442
+ const val=expr.slice(0,ternIdx).trim();
1443
+ const cond=expr.slice(ternIdx+4,elseIdx).trim();
1444
+ const alt=expr.slice(elseIdx+6).trim();
1445
+ return this._truthy(this._eval(cond,env))?this._eval(val,env):this._eval(alt,env);
1446
+ }
1447
+ }
1448
+ // Binary ops in precedence order
1449
+ for(const op of [' or ',' and ']){
1450
+ const i=this._findOp(expr,op);
1451
+ if(i!==-1){
1452
+ if(op===' or ') return this._truthy(this._eval(expr.slice(0,i),env))||this._truthy(this._eval(expr.slice(i+4),env));
1453
+ if(op===' and ') return this._truthy(this._eval(expr.slice(0,i),env))&&this._truthy(this._eval(expr.slice(i+5),env));
1454
+ }
1455
+ }
1456
+ if(expr.startsWith('not ')) return !this._truthy(this._eval(expr.slice(4),env));
1457
+ for(const op of ['>=','<=','!=','==','>','<']){
1458
+ const i=this._findOp(expr,op);
1459
+ if(i!==-1){
1460
+ const l=this._eval(expr.slice(0,i).trim(),env),r=this._eval(expr.slice(i+op.length).trim(),env);
1461
+ switch(op){ case'==':return l==r; case'!=':return l!=r; case'>':return l>r; case'<':return l<r; case'>=':return l>=r; case'<=':return l<=r; }
1462
+ }
1463
+ }
1464
+ // Arithmetic
1465
+ const addI=this._findOp(expr,'+');
1466
+ if(addI!==-1){ return this._eval(expr.slice(0,addI).trim(),env)+this._eval(expr.slice(addI+1).trim(),env); }
1467
+ const subI=this._findBinaryMinus(expr);
1468
+ if(subI!==-1){ return this._eval(expr.slice(0,subI).trim(),env)-this._eval(expr.slice(subI+1).trim(),env); }
1469
+ const modI=this._findOp(expr,'%');
1470
+ if(modI!==-1){ return this._eval(expr.slice(0,modI).trim(),env)%this._eval(expr.slice(modI+1).trim(),env); }
1471
+ const powI=this._findOp(expr,'**');
1472
+ if(powI!==-1){ return Math.pow(this._eval(expr.slice(0,powI).trim(),env),this._eval(expr.slice(powI+2).trim(),env)); }
1473
+ const fdivI=this._findOp(expr,'//');
1474
+ if(fdivI!==-1){ return Math.floor(this._eval(expr.slice(0,fdivI).trim(),env)/this._eval(expr.slice(fdivI+2).trim(),env)); }
1475
+ const mulI=this._findOp(expr,'*');
1476
+ if(mulI!==-1){ return this._eval(expr.slice(0,mulI).trim(),env)*this._eval(expr.slice(mulI+1).trim(),env); }
1477
+ const divI=this._findOp(expr,'/');
1478
+ if(divI!==-1){ const d=this._eval(expr.slice(divI+1).trim(),env); return this._eval(expr.slice(0,divI).trim(),env)/d; }
1479
+ // Unary minus
1480
+ if(expr[0]==='-') return -this._eval(expr.slice(1),env);
1481
+ // Method call: obj.method(args)
1482
+ const methodM=expr.match(/^(.+?)\.([a-zA-Z_]\w*)\((.*)\)$/s);
1483
+ if(methodM){
1484
+ const objExpr=methodM[1], mname=methodM[2], argsRaw=methodM[3].trim();
1485
+ const obj=this._eval(objExpr,env);
1486
+ const args=argsRaw?this._splitArgs(argsRaw).map(a=>this._eval(a.trim(),env)):[];
1487
+ if(typeof obj==='object'&&obj!==null&&typeof obj[mname]==='function'){
1488
+ return obj[mname](args);
1489
+ }
1490
+ // Built-in string/array methods via bridge
1491
+ if(typeof obj==='string'){
1492
+ if(mname==='upper') return obj.toUpperCase();
1493
+ if(mname==='lower') return obj.toLowerCase();
1494
+ if(mname==='trim') return obj.trim();
1495
+ if(mname==='length') return obj.length;
1496
+ if(mname==='split') return obj.split(args[0]??'');
1497
+ if(mname==='contains') return obj.includes(String(args[0]));
1498
+ if(mname==='replace') return obj.split(String(args[0])).join(String(args[1]));
1499
+ if(mname==='startsWith') return obj.startsWith(String(args[0]));
1500
+ if(mname==='endsWith') return obj.endsWith(String(args[0]));
1501
+ }
1502
+ if(Array.isArray(obj)){
1503
+ if(mname==='push'){ obj.push(args[0]); return null; }
1504
+ if(mname==='pop') return obj.pop()??null;
1505
+ if(mname==='length') return obj.length;
1506
+ if(mname==='sort') return [...obj].sort();
1507
+ if(mname==='reverse') return [...obj].reverse();
1508
+ if(mname==='join') return obj.map(x=>this._str(x)).join(args[0]??'');
1509
+ if(mname==='contains') return obj.includes(args[0]);
1510
+ }
1511
+ throw new SSError(`"${mname}" is not a method of ${this._str(obj)}`);
1512
+ }
1513
+ // Function call
1514
+ const callM=expr.match(/^([a-zA-Z_][\w]*)\((.*)\)$/s);
1515
+ if(callM){
1516
+ const fname=callM[1], argsRaw=callM[2].trim();
1517
+ const args=argsRaw?this._splitArgs(argsRaw).map(a=>this._eval(a.trim(),env)):[];
1518
+ const fn=env.get(fname);
1519
+ if(typeof fn!=='function') throw new SSError(`"${fname}" is not a function`);
1520
+ this.callDepth++;
1521
+ if(this.callDepth>200){ this.callDepth--; throw new SSError('Stack overflow'); }
1522
+ const res=fn(args,env);
1523
+ this.callDepth--;
1524
+ return res;
1525
+ }
1526
+ // Index access
1527
+ const bracketM=expr.match(/^(.+?)\[(.+)\]$/);
1528
+ if(bracketM){
1529
+ const obj=this._eval(bracketM[1].trim(),env);
1530
+ const idx=this._eval(bracketM[2].trim(),env);
1531
+ if(Array.isArray(obj)) return obj[idx]??null;
1532
+ if(typeof obj==='string') return obj[idx]??null;
1533
+ if(typeof obj==='object'&&obj!==null) return obj[idx]??null;
1534
+ return null;
1535
+ }
1536
+ // Dot access
1537
+ if(expr.includes('.')&&!/^\d/.test(expr)){
1538
+ const dot=expr.lastIndexOf('.');
1539
+ const obj=this._eval(expr.slice(0,dot),env);
1540
+ const key=expr.slice(dot+1);
1541
+ if(typeof obj==='object'&&obj!==null) return obj[key]??null;
1542
+ return null;
1543
+ }
1544
+ // Variable
1545
+ return env.get(expr);
1546
+ }
1547
+
1548
+ _parseString(s, env){
1549
+ s = s.replace(/\\n/g,'\n').replace(/\\t/g,'\t').replace(/\\\\/g,'\\').replace(/\\"/g,'"').replace(/\\'/g,"'");
1550
+ if(!env) return s;
1551
+ let result='', i=0;
1552
+ while(i<s.length){
1553
+ if(s[i]==='{'){
1554
+ let depth=1, j=i+1;
1555
+ while(j<s.length&&depth>0){ if(s[j]==='{')depth++; else if(s[j]==='}')depth--; j++; }
1556
+ const expr=s.slice(i+1,j-1);
1557
+ try{ result+=this._str(this._eval(expr.trim(),env)); } catch(e){ result+='{'+expr+'}'; }
1558
+ i=j;
1559
+ } else { result+=s[i++]; }
1560
+ }
1561
+ return result;
1562
+ }
1563
+
1564
+ _findOp(expr, op) {
1565
+ let depth=0, inStr=false, sc='';
1566
+ for(let i=0;i<expr.length;i++){
1567
+ const c=expr[i];
1568
+ if(inStr){if(c===sc)inStr=false;continue;}
1569
+ if(c==='"'||c==="'"){inStr=true;sc=c;continue;}
1570
+ if(c==='('||c==='[')depth++;
1571
+ else if(c===')'||c===']')depth--;
1572
+ else if(depth===0&&expr.slice(i,i+op.length)===op) return i;
1573
+ }
1574
+ return -1;
1575
+ }
1576
+
1577
+ _findBinaryMinus(expr){
1578
+ let depth=0,inStr=false,sc='';
1579
+ for(let i=expr.length-1;i>0;i--){
1580
+ const c=expr[i];
1581
+ if(c==='"'||c==="'")inStr=!inStr;
1582
+ if(inStr)continue;
1583
+ if(c===')'||c===']')depth++;
1584
+ else if(c==='('||c==='[')depth--;
1585
+ else if(depth===0&&c==='-'){
1586
+ const prev=expr[i-1];
1587
+ if(/[\w\d\)"\'\]]/.test(prev)) return i;
1588
+ }
1589
+ }
1590
+ return -1;
1591
+ }
1592
+
1593
+ _splitArgs(str){
1594
+ const args=[]; let depth=0,inStr=false,sc='',cur='';
1595
+ for(let i=0;i<str.length;i++){
1596
+ const c=str[i];
1597
+ if(inStr){cur+=c;if(c===sc)inStr=false;continue;}
1598
+ if(c==='"'||c==="'"){inStr=true;sc=c;cur+=c;continue;}
1599
+ if(c==='('||c==='['){depth++;cur+=c;continue;}
1600
+ if(c===')'||c===']'){depth--;cur+=c;continue;}
1601
+ if(c===','&&depth===0){args.push(cur.trim());cur='';continue;}
1602
+ cur+=c;
1603
+ }
1604
+ if(cur.trim())args.push(cur.trim());
1605
+ return args;
1606
+ }
1607
+
1608
+ _str(val){
1609
+ if(val===null||val===undefined) return 'nothing';
1610
+ if(val===true) return 'true';
1611
+ if(val===false) return 'false';
1612
+ if(Array.isArray(val)) return '['+val.map(v=>this._str(v)).join(', ')+']';
1613
+ if(typeof val==='object'){
1614
+ const n=val.__struct||'struct';
1615
+ const fields=Object.entries(val).filter(([k])=>k!=='__struct').map(([k,v])=>`${k}: ${this._str(v)}`).join(', ');
1616
+ return `${n}{ ${fields} }`;
1617
+ }
1618
+ return String(val);
1619
+ }
1620
+ }
1621
+
1622
+ // ============================================================
1623
+ // EXAMPLES
1624
+ // ============================================================
1625
+ const EXAMPLES = {
1626
+ hello:`// Hello, World! in StructScript v1.2
1627
+ let name = "World"
1628
+ say "Hello, {name}!"
1629
+ say "Welcome to StructScript v1.2"
1630
+
1631
+ // String interpolation — use {expr} inside any string
1632
+ let version = 1.2
1633
+ let year = 2025
1634
+ say "StructScript v{version} — built in {year}"
1635
+
1636
+ let a = 6
1637
+ let b = 7
1638
+ say "The answer is {a * b}"`,
1639
+
1640
+ interpolation:`// String Interpolation — "Hello {name}!"
1641
+ // Any expression works inside { }
1642
+
1643
+ let name = "Alex"
1644
+ let age = 24
1645
+ let score = 97.5
1646
+
1647
+ say "Hello, {name}! You are {age} years old."
1648
+ say "Score: {score}%"
1649
+
1650
+ // Expressions inside braces
1651
+ let x = 10
1652
+ let y = 3
1653
+ say "{x} + {y} = {x + y}"
1654
+ say "{x} * {y} = {x * y}"
1655
+ say "Is x > y? {x > y}"
1656
+
1657
+ // Works with function calls too
1658
+ let items = ["apple", "banana", "cherry"]
1659
+ say "You have {length(items)} items."
1660
+ say "First: {items[0]}, Last: {items[length(items) - 1]}"
1661
+
1662
+ // Nested values
1663
+ let pi = 3.14159
1664
+ say "Pi rounded: {round(pi)}"
1665
+ say "Pi floored: {floor(pi)}"`,
1666
+
1667
+ classes:`// Classes — objects with fields AND methods
1668
+
1669
+ class Animal:
1670
+ name = "Unknown"
1671
+ sound = "..."
1672
+ legs = 4
1673
+
1674
+ define speak():
1675
+ say "{self.name} says {self.sound}!"
1676
+
1677
+ define describe():
1678
+ say "{self.name} has {self.legs} legs."
1679
+
1680
+ define init(n, s):
1681
+ set self.name = n
1682
+ set self.sound = s
1683
+
1684
+ class Counter:
1685
+ value = 0
1686
+ step = 1
1687
+
1688
+ define increment():
1689
+ set self.value = self.value + self.step
1690
+
1691
+ define reset():
1692
+ set self.value = 0
1693
+
1694
+ define get():
1695
+ return self.value
1696
+
1697
+ // Create instances
1698
+ let dog = Animal()
1699
+ dog.init("Rex", "Woof")
1700
+ dog.speak()
1701
+ dog.describe()
1702
+
1703
+ let cat = Animal()
1704
+ cat.init("Whiskers", "Meow")
1705
+ cat.speak()
1706
+
1707
+ // Counter class
1708
+ let c = Counter()
1709
+ set c.step = 5
1710
+ c.increment()
1711
+ c.increment()
1712
+ c.increment()
1713
+ say "Counter: {c.get()}"
1714
+ c.reset()
1715
+ say "After reset: {c.get()}"`,
1716
+
1717
+ variables:`// Variables, constants, and types
1718
+ let name = "Alex"
1719
+ let age = 24
1720
+ let height = 5.9
1721
+ let active = true
1722
+ const MAX_SCORE = 100
1723
+ const PI = 3.14159
1724
+
1725
+ say "Name: " + name
1726
+ say "Age: " + str(age)
1727
+ say "Height: " + str(height)
1728
+ say "Active: " + str(active)
1729
+ say "Max: " + str(MAX_SCORE)
1730
+
1731
+ // Compound assignment
1732
+ let x = 10
1733
+ set x += 5
1734
+ say "x after += 5: " + str(x)
1735
+ set x *= 2
1736
+ say "x after *= 2: " + str(x)
1737
+
1738
+ // Type checking
1739
+ say "type of age: " + type(age)
1740
+ say "type of name: " + type(name)
1741
+ say "type of active: " + type(active)`,
1742
+
1743
+ strings:`// String operations
1744
+ let s = "Hello, StructScript!"
1745
+
1746
+ say upper(s)
1747
+ say lower(s)
1748
+ say length(s)
1749
+ say contains(s, "Script")
1750
+ say replace(s, "Hello", "Goodbye")
1751
+ say slice(s, 0, 5)
1752
+
1753
+ // String repeat
1754
+ say repeat("ha", 3)
1755
+
1756
+ // Format
1757
+ say format("I am {} years old and {} cm tall", 25, 180)
1758
+
1759
+ // Split and join
1760
+ let csv = "apple,banana,cherry"
1761
+ let fruits = split(csv, ",")
1762
+ say fruits
1763
+ say join(fruits, " | ")
1764
+
1765
+ // Check
1766
+ say startsWith("StructScript", "Struct")
1767
+ say endsWith("main.ss", ".ss")`,
1768
+
1769
+ loops:`// All loop types in StructScript
1770
+
1771
+ // Count loop with step
1772
+ say "Even numbers 0–10:"
1773
+ count i from 0 to 10 step 2:
1774
+ say i
1775
+
1776
+ // Countdown
1777
+ say "Countdown:"
1778
+ count i from 5 to 1 step -1:
1779
+ say i
1780
+ say "Blast off!"
1781
+
1782
+ // While with break
1783
+ say "While with break:"
1784
+ let n = 1
1785
+ while n <= 1000:
1786
+ if n > 16:
1787
+ break
1788
+ say n
1789
+ set n = n * 2
1790
+
1791
+ // For each
1792
+ let langs = ["Python", "JavaScript", "StructScript"]
1793
+ say "Languages:"
1794
+ for each lang in langs:
1795
+ say " - " + lang
1796
+
1797
+ // Range
1798
+ say "Range:"
1799
+ for each i in range(5):
1800
+ say i`,
1801
+
1802
+ functions:`// Functions with defaults, recursion, and higher-order
1803
+
1804
+ define greet(name, greeting = "Hello"):
1805
+ say greeting + ", " + name + "!"
1806
+
1807
+ define clamp(val, lo, hi):
1808
+ if val < lo:
1809
+ return lo
1810
+ if val > hi:
1811
+ return hi
1812
+ return val
1813
+
1814
+ define factorial(n):
1815
+ if n <= 1:
1816
+ return 1
1817
+ return n * factorial(n - 1)
1818
+
1819
+ define isPrime(n):
1820
+ if n < 2:
1821
+ return false
1822
+ count i from 2 to n - 1:
1823
+ if n % i == 0:
1824
+ return false
1825
+ return true
1826
+
1827
+ greet("World")
1828
+ greet("StructScript", "Welcome to")
1829
+
1830
+ say "clamp(15, 0, 10) = " + str(clamp(15, 0, 10))
1831
+ say "5! = " + str(factorial(5))
1832
+ say "7 is prime: " + str(isPrime(7))
1833
+ say "9 is prime: " + str(isPrime(9))
1834
+
1835
+ // Primes up to 20
1836
+ say "Primes up to 20:"
1837
+ count i from 2 to 20:
1838
+ if isPrime(i):
1839
+ say i`,
1840
+
1841
+ lists:`// Lists — the workhorse of StructScript
1842
+
1843
+ let nums = [5, 3, 8, 1, 9, 2, 7, 4, 6]
1844
+ say "Original: " + str(nums)
1845
+ say "Sorted: " + str(sort(nums))
1846
+ say "Reversed: " + str(reverse(nums))
1847
+ say "Sum: " + str(sum(nums))
1848
+ say "Average: " + str(avg(nums))
1849
+ say "Max: " + str(max(5, 3, 8, 1, 9))
1850
+ say "Min: " + str(min(5, 3, 8, 1, 9))
1851
+
1852
+ // Push/pop
1853
+ let stack = []
1854
+ push(stack, "first")
1855
+ push(stack, "second")
1856
+ push(stack, "third")
1857
+ say "Stack: " + str(stack)
1858
+ say "Popped: " + pop(stack)
1859
+ say "Stack after pop: " + str(stack)
1860
+
1861
+ // Slice
1862
+ let letters = ["a","b","c","d","e"]
1863
+ say "Slice [1..3]: " + str(slice(letters, 1, 4))
1864
+
1865
+ // Unique
1866
+ let dupes = [1, 2, 2, 3, 3, 3, 4]
1867
+ say "Unique: " + str(unique(dupes))
1868
+
1869
+ // Length
1870
+ say "Length: " + str(length(nums))`,
1871
+
1872
+ structs:`// Structs — custom data types
1873
+
1874
+ struct Vector:
1875
+ x = 0
1876
+ y = 0
1877
+
1878
+ struct Player:
1879
+ name = "Unknown"
1880
+ health = 100
1881
+ score = 0
1882
+ alive = true
1883
+
1884
+ define magnitude(v):
1885
+ return sqrt(v.x * v.x + v.y * v.y)
1886
+
1887
+ define playerStatus(p):
1888
+ if not p.alive:
1889
+ say p.name + " is defeated."
1890
+ return nothing
1891
+ say p.name + " — HP: " + str(p.health) + " | Score: " + str(p.score)
1892
+
1893
+ let v1 = Vector()
1894
+ set v1.x = 3
1895
+ set v1.y = 4
1896
+ say "Vector: " + str(v1.x) + ", " + str(v1.y)
1897
+ say "Magnitude: " + str(magnitude(v1))
1898
+
1899
+ let hero = Player()
1900
+ set hero.name = "Aria"
1901
+ set hero.score = 2400
1902
+
1903
+ let enemy = Player()
1904
+ set enemy.name = "Shadow"
1905
+ set enemy.health = 60
1906
+
1907
+ playerStatus(hero)
1908
+ playerStatus(enemy)
1909
+
1910
+ set hero.health -= 30
1911
+ playerStatus(hero)`,
1912
+
1913
+ errors:`// Error handling with try/catch/finally
1914
+
1915
+ // Basic error handling
1916
+ try:
1917
+ let result = 10 / 0
1918
+ say "Result: " + str(result)
1919
+ catch err:
1920
+ say "Caught division issue: " + err
1921
+
1922
+ // Catching a bad function call
1923
+ define divide(a, b):
1924
+ if b == 0:
1925
+ error("Cannot divide by zero")
1926
+ return a / b
1927
+
1928
+ try:
1929
+ say str(divide(10, 2))
1930
+ say str(divide(5, 0))
1931
+ catch e:
1932
+ say "Error: " + e
1933
+
1934
+ // Finally always runs
1935
+ say "--- Finally demo ---"
1936
+ try:
1937
+ error("Something went wrong")
1938
+ catch e:
1939
+ say "Handled: " + e
1940
+ finally:
1941
+ say "Cleanup complete."
1942
+
1943
+ // Assert
1944
+ try:
1945
+ let age = -5
1946
+ assert(age >= 0, "Age must be non-negative")
1947
+ catch e:
1948
+ say "Assertion: " + e`,
1949
+
1950
+ fibonacci:`// Fibonacci — iterative and recursive
1951
+
1952
+ define fibRecursive(n):
1953
+ if n <= 1:
1954
+ return n
1955
+ return fibRecursive(n - 1) + fibRecursive(n - 2)
1956
+
1957
+ define fibIterative(n):
1958
+ if n <= 1:
1959
+ return n
1960
+ let a = 0
1961
+ let b = 1
1962
+ count i from 2 to n:
1963
+ let temp = a + b
1964
+ set a = b
1965
+ set b = temp
1966
+ return b
1967
+
1968
+ say "Recursive (first 8):"
1969
+ count i from 0 to 7:
1970
+ say " fib(" + str(i) + ") = " + str(fibRecursive(i))
1971
+
1972
+ say "Iterative (first 15):"
1973
+ let fibs = []
1974
+ count i from 0 to 14:
1975
+ push(fibs, fibIterative(i))
1976
+ say fibs`,
1977
+
1978
+ webpage:`// StructScript Web — build a real webpage!
1979
+
1980
+ page "My Page":
1981
+ style background "#0d1f1e"
1982
+ style fontFamily "Manrope, sans-serif"
1983
+ style padding "40px"
1984
+
1985
+ add div "hero":
1986
+ style textAlign "center"
1987
+ style padding "60px 20px"
1988
+
1989
+ add h1 "title":
1990
+ text "Hello from StructScript!"
1991
+ style color "#b8f000"
1992
+ style fontSize "48px"
1993
+ style fontWeight "800"
1994
+ style marginBottom "16px"
1995
+ animate fadeIn 0.6
1996
+
1997
+ add p "subtitle":
1998
+ text "Building web pages with structured scripting"
1999
+ style color "#8ab8b4"
2000
+ style fontSize "18px"
2001
+ style marginBottom "32px"
2002
+ animate fadeIn 0.9
2003
+
2004
+ add button "cta":
2005
+ text "Click me!"
2006
+ style background "#0b7a75"
2007
+ style color "#b8f000"
2008
+ style border "none"
2009
+ style padding "14px 32px"
2010
+ style borderRadius "8px"
2011
+ style fontSize "16px"
2012
+ style fontWeight "700"
2013
+ style cursor "pointer"
2014
+ animate pop 0.5
2015
+ on click:
2016
+ say "You clicked the button!"
2017
+
2018
+ add div "cards":
2019
+ style display "flex"
2020
+ style gap "20px"
2021
+ style justifyContent "center"
2022
+ style marginTop "40px"
2023
+ style flexWrap "wrap"
2024
+
2025
+ add div "":
2026
+ style background "#162b28"
2027
+ style border "1px solid #234440"
2028
+ style borderRadius "12px"
2029
+ style padding "24px"
2030
+ style width "180px"
2031
+ style textAlign "center"
2032
+ animate slideIn 0.4
2033
+ add h3 "": text "Rocket Fast"
2034
+ style color "#b8f000"
2035
+ add p "": text "Runs in the browser"
2036
+ style color "#8ab8b4"
2037
+ style fontSize "13px"
2038
+
2039
+ add div "":
2040
+ style background "#162b28"
2041
+ style border "1px solid #234440"
2042
+ style borderRadius "12px"
2043
+ style padding "24px"
2044
+ style width "180px"
2045
+ style textAlign "center"
2046
+ animate slideIn 0.65
2047
+ add h3 "": text "Super Simple"
2048
+ style color "#b8f000"
2049
+ add p "": text "English-like syntax"
2050
+ style color "#8ab8b4"
2051
+ style fontSize "13px"
2052
+
2053
+ add div "":
2054
+ style background "#162b28"
2055
+ style border "1px solid #234440"
2056
+ style borderRadius "12px"
2057
+ style padding "24px"
2058
+ style width "180px"
2059
+ style textAlign "center"
2060
+ animate slideIn 0.9
2061
+ add h3 "": text "Fully Styled"
2062
+ style color "#b8f000"
2063
+ add p "": text "CSS control built in"
2064
+ style color "#8ab8b4"
2065
+ style fontSize "13px"`,
2066
+
2067
+ fizzbuzz:`// FizzBuzz — classic interview question
2068
+
2069
+ say "FizzBuzz 1–30:"
2070
+ count i from 1 to 30:
2071
+ if i % 15 == 0:
2072
+ say "FizzBuzz"
2073
+ else if i % 3 == 0:
2074
+ say "Fizz"
2075
+ else if i % 5 == 0:
2076
+ say "Buzz"
2077
+ else:
2078
+ say i`,
2079
+
2080
+ sorting:`// Bubble sort implemented in StructScript
2081
+
2082
+ define bubbleSort(arr):
2083
+ let n = length(arr)
2084
+ let sorted = arr
2085
+ count i from 0 to n - 2:
2086
+ count j from 0 to n - 2 - i:
2087
+ if sorted[j] > sorted[j + 1]:
2088
+ let temp = sorted[j]
2089
+ set sorted[j] = sorted[j + 1]
2090
+ set sorted[j + 1] = temp
2091
+ return sorted
2092
+
2093
+ let data = [64, 34, 25, 12, 22, 11, 90]
2094
+ say "Unsorted: " + str(data)
2095
+ let result = bubbleSort(data)
2096
+ say "Sorted: " + str(result)
2097
+
2098
+ // Also test with strings
2099
+ let words = ["banana", "apple", "cherry", "date"]
2100
+ say "Words sorted: " + str(sort(words))`
2101
+ };
2102
+
2103
+ // ============================================================
2104
+ // UI STATE
2105
+ // ============================================================
2106
+ let currentTheme = 'dark';
2107
+ let currentFile = 'untitled.ss';
2108
+ let isModified = false;
2109
+ let acSelectedIdx = -1;
2110
+ let acItems = [];
2111
+ let lastErrors = [];
2112
+
2113
+ const editor = document.getElementById('code-editor');
2114
+ const highlightLayer = document.getElementById('highlight-layer');
2115
+ const lineNumbers = document.getElementById('line-numbers');
2116
+ const acDiv = document.getElementById('autocomplete');
2117
+ const outputDisplay = document.getElementById('output-display');
2118
+ const errorDisplay = document.getElementById('error-display');
2119
+
2120
+ // ============================================================
2121
+ // KEYWORDS & AUTOCOMPLETE DATA
2122
+ // ============================================================
2123
+ const KEYWORDS = ['say','let','const','set','define','return','if','else if','else',
2124
+ 'while','repeat','times','count','from','to','step','for','each','in','and','or','not',
2125
+ 'true','false','null','nothing','struct','class','self','break','continue','try','catch','finally','error',
2126
+ 'page','add','style','on','animate','text','html','attr','remove','clear'];
2127
+
2128
+ const BUILTINS = ['input','length','push','pop','shift','unshift','join','split','upper','lower','trim',
2129
+ 'contains','startsWith','endsWith','replace','slice','sort','reverse','range','flatten',
2130
+ 'unique','sum','avg','map','filter','reduce','abs','sqrt','floor','ceil','round','max','min',
2131
+ 'pow','log','log10','sin','cos','tan','random','randint','str','num','bool','type',
2132
+ 'isNothing','print','assert','error','format','char','charCode','repeat','indexOf',
2133
+ 'getEl','setStyle','getStyle','addClass','removeClass','toggleClass','setAttr','getAttr',
2134
+ 'setHtml','getText','setText','appendTo','prependTo','removeEl','query','queryAll',
2135
+ 'fadeIn','fadeOut','slideIn','slideOut','moveTo','scaleTo','rotateTo'];
2136
+
2137
+ const SNIPPETS = {
2138
+ 'define': 'define name(params):\n ',
2139
+ 'if': 'if condition:\n ',
2140
+ 'while': 'while condition:\n ',
2141
+ 'for each': 'for each item in list:\n ',
2142
+ 'count': 'count i from 1 to 10:\n ',
2143
+ 'repeat': 'repeat 5 times:\n ',
2144
+ 'try': 'try:\n \ncatch err:\n say err',
2145
+ 'struct': 'struct Name:\n field = value',
2146
+ };
2147
+
2148
+ // Load default example
2149
+ editor.value = EXAMPLES.hello;
2150
+ updateAll();
2151
+
2152
+ // ============================================================
2153
+ // SYNTAX HIGHLIGHT
2154
+ // ============================================================
2155
+ function highlight(code) {
2156
+ return code.split('\n').map(line => highlightLine(line)).join('\n');
2157
+ }
2158
+
2159
+ function highlightLine(line) {
2160
+ const tokens = [];
2161
+ let i = 0;
2162
+
2163
+ while (i < line.length) {
2164
+ // Comment
2165
+ if (line[i]==='/' && line[i+1]==='/') {
2166
+ tokens.push(`<span style="color:var(--cmt-color);font-style:italic">${esc(line.slice(i))}</span>`);
2167
+ break;
2168
+ }
2169
+ // String
2170
+ if (line[i]==='"' || line[i]==="'") {
2171
+ const q=line[i]; let j=i+1;
2172
+ while(j<line.length && line[j]!==q) { if(line[j]==='\\')j++; j++; }
2173
+ tokens.push(`<span style="color:var(--str-color)">${esc(line.slice(i,j+1))}</span>`);
2174
+ i=j+1; continue;
2175
+ }
2176
+ // Number
2177
+ if (/\d/.test(line[i]) && (i===0||/\W/.test(line[i-1]))) {
2178
+ let j=i; while(j<line.length&&/[\d.]/.test(line[j]))j++;
2179
+ tokens.push(`<span style="color:var(--num-color)">${esc(line.slice(i,j))}</span>`);
2180
+ i=j; continue;
2181
+ }
2182
+ // Keywords (longest match first)
2183
+ let matched=false;
2184
+ const sorted=[...KEYWORDS].sort((a,b)=>b.length-a.length);
2185
+ for(const kw of sorted){
2186
+ if(line.slice(i).startsWith(kw)){
2187
+ const after=line[i+kw.length];
2188
+ const before=i===0?null:line[i-1];
2189
+ if((after===undefined||/[\s(:,]/.test(after))&&(before===null||/[\s(]/.test(before))){
2190
+ tokens.push(`<span style="color:var(--kw-color);font-weight:600">${esc(kw)}</span>`);
2191
+ i+=kw.length; matched=true; break;
2192
+ }
2193
+ }
2194
+ }
2195
+ if(matched) continue;
2196
+ // Built-in function call
2197
+ if(/[a-zA-Z_]/.test(line[i])){
2198
+ let j=i; while(j<line.length&&/[\w]/.test(line[j]))j++;
2199
+ const word=line.slice(i,j);
2200
+ if(BUILTINS.includes(word)&&line[j]==='(')
2201
+ tokens.push(`<span style="color:var(--fn-color)">${esc(word)}</span>`);
2202
+ else
2203
+ tokens.push(esc(word));
2204
+ i=j; continue;
2205
+ }
2206
+ // Operators
2207
+ if(/[+\-*\/%=<>!&|]/.test(line[i])){
2208
+ tokens.push(`<span style="color:var(--op-color)">${esc(line[i])}</span>`);
2209
+ i++; continue;
2210
+ }
2211
+ tokens.push(esc(line[i])); i++;
2212
+ }
2213
+ return tokens.join('');
2214
+ }
2215
+
2216
+ function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
2217
+
2218
+ // ============================================================
2219
+ // LINE NUMBERS
2220
+ // ============================================================
2221
+ function updateLineNumbers() {
2222
+ const lines = editor.value.split('\n');
2223
+ const cursorPos = editor.selectionStart;
2224
+ const textBefore = editor.value.slice(0, cursorPos);
2225
+ const curLine = textBefore.split('\n').length;
2226
+
2227
+ lineNumbers.innerHTML = lines.map((_,i)=>
2228
+ `<span class="line-num${i+1===curLine?' active':''}">${i+1}</span>`
2229
+ ).join('');
2230
+
2231
+ // Sync scroll
2232
+ lineNumbers.scrollTop = editor.scrollTop;
2233
+
2234
+ // Status bar
2235
+ const col = textBefore.split('\n').pop().length + 1;
2236
+ document.getElementById('sb-lines').textContent = lines.length + (lines.length===1?' line':' lines');
2237
+ document.getElementById('sb-pos').textContent = `Ln ${curLine}, Col ${col}`;
2238
+ }
2239
+
2240
+ // ============================================================
2241
+ // AUTOCOMPLETE
2242
+ // ============================================================
2243
+ function getWordAtCursor() {
2244
+ const pos = editor.selectionStart;
2245
+ const text = editor.value;
2246
+ let start = pos - 1;
2247
+ while (start >= 0 && /[\w]/.test(text[start])) start--;
2248
+ start++;
2249
+ return { word: text.slice(start, pos), start, end: pos };
2250
+ }
2251
+
2252
+ function showAutocomplete() {
2253
+ const { word } = getWordAtCursor();
2254
+ if (!word || word.length < 1) { hideAutocomplete(); return; }
2255
+
2256
+ const all = [...KEYWORDS, ...BUILTINS];
2257
+ acItems = all.filter(k => k.startsWith(word) && k !== word).slice(0, 10);
2258
+
2259
+ if (!acItems.length) { hideAutocomplete(); return; }
2260
+
2261
+ acSelectedIdx = -1;
2262
+ acDiv.innerHTML = acItems.map((item, i) => {
2263
+ const isKw = KEYWORDS.includes(item);
2264
+ const icon = isKw ? '🔑' : '⚡';
2265
+ const typeLabel = isKw ? 'keyword' : 'builtin';
2266
+ return `<div class="ac-item" data-idx="${i}" onmousedown="applyAC(${i})">
2267
+ <span class="ac-icon">${icon}</span>
2268
+ <span class="ac-name">${item}</span>
2269
+ <span class="ac-type">${typeLabel}</span>
2270
+ </div>`;
2271
+ }).join('');
2272
+
2273
+ // Position near cursor
2274
+ const linesBefore = editor.value.slice(0, editor.selectionStart).split('\n');
2275
+ const lineH = 22, padTop = 12;
2276
+ const top = padTop + linesBefore.length * lineH;
2277
+ const col = linesBefore[linesBefore.length-1].length;
2278
+ const charW = 7.8;
2279
+
2280
+ acDiv.style.display = 'block';
2281
+ acDiv.style.top = (top + lineH) + 'px';
2282
+ acDiv.style.left = (16 + col * charW) + 'px';
2283
+ }
2284
+
2285
+ function hideAutocomplete() {
2286
+ acDiv.style.display = 'none';
2287
+ acItems = [];
2288
+ acSelectedIdx = -1;
2289
+ }
2290
+
2291
+ function applyAC(idx) {
2292
+ const { word, start, end } = getWordAtCursor();
2293
+ const completion = acItems[idx];
2294
+ const snippet = SNIPPETS[completion] || completion;
2295
+ const before = editor.value.slice(0, start);
2296
+ const after = editor.value.slice(end);
2297
+ editor.value = before + snippet + after;
2298
+ const newPos = start + snippet.length;
2299
+ editor.selectionStart = editor.selectionEnd = newPos;
2300
+ hideAutocomplete();
2301
+ updateAll();
2302
+ }
2303
+
2304
+ // ============================================================
2305
+ // MAIN UPDATE
2306
+ // ============================================================
2307
+ function updateAll() {
2308
+ highlightLayer.innerHTML = highlight(editor.value);
2309
+ // Sync scroll
2310
+ highlightLayer.scrollTop = editor.scrollTop;
2311
+ highlightLayer.scrollLeft = editor.scrollLeft;
2312
+ updateLineNumbers();
2313
+ markModified(true);
2314
+ }
2315
+
2316
+ function markModified(v) {
2317
+ isModified = v;
2318
+ document.getElementById('modified-indicator').style.display = v ? 'inline' : 'none';
2319
+ }
2320
+
2321
+ // ============================================================
2322
+ // INPUT MODAL
2323
+ // ============================================================
2324
+ function scanForInputs(code) {
2325
+ const inputs = [];
2326
+ const re = /\binput\s*\(\s*(?:"([^"]*)"|\'([^\']*)\'|([^)]*?))?\s*\)/g;
2327
+ let m;
2328
+ while ((m = re.exec(code)) !== null) {
2329
+ inputs.push((m[1] ?? m[2] ?? m[3] ?? '').trim() || 'Enter a value');
2330
+ }
2331
+ return inputs;
2332
+ }
2333
+
2334
+ let _pendingRunFn = null;
2335
+
2336
+ function cancelInputModal() {
2337
+ document.getElementById('input-modal-overlay').style.display='none';
2338
+ _pendingRunFn = null;
2339
+ }
2340
+
2341
+ function submitInputModal() {
2342
+ const fields = document.querySelectorAll('#input-modal-fields .im-input');
2343
+ const values = Array.from(fields).map(f => f.value);
2344
+ document.getElementById('input-modal-overlay').style.display='none';
2345
+ if (_pendingRunFn) { _pendingRunFn(values); _pendingRunFn = null; }
2346
+ }
2347
+
2348
+ document.addEventListener('keydown', e => {
2349
+ if (e.key === 'Enter' && document.getElementById('input-modal-overlay').style.display!=='none') {
2350
+ const active = document.activeElement;
2351
+ if (active && active.classList.contains('im-input')) {
2352
+ const fields = [...document.querySelectorAll('#input-modal-fields .im-input')];
2353
+ const idx = fields.indexOf(active);
2354
+ if (idx === fields.length - 1) submitInputModal();
2355
+ else if (idx >= 0) fields[idx + 1].focus();
2356
+ e.preventDefault();
2357
+ }
2358
+ }
2359
+ if (e.key === 'Escape' && document.getElementById('input-modal-overlay').style.display!=='none') cancelInputModal();
2360
+ });
2361
+
2362
+
2363
+
2364
+ // ============================================================
2365
+ // RUN
2366
+ // ============================================================
2367
+ function runCode() {
2368
+ const code = editor.value;
2369
+ if (isWebCode(code)) { _execCode(code, []); return; }
2370
+ const inputPrompts = scanForInputs(code);
2371
+ if (inputPrompts.length > 0) {
2372
+ const fieldsDiv = document.getElementById('input-modal-fields');
2373
+ fieldsDiv.innerHTML = inputPrompts.map((prompt, i) => `
2374
+ <div class="im-field">
2375
+ <label class="im-label"><span class="im-prompt-text">${esc(prompt)}</span><span class="im-idx">#${i+1}</span></label>
2376
+ <input class="im-input" type="text" placeholder="type here..." autocomplete="off" spellcheck="false">
2377
+ </div>`).join('');
2378
+ document.getElementById('input-modal-overlay').style.display='flex';
2379
+ const first = fieldsDiv.querySelector('.im-input');
2380
+ if (first) setTimeout(() => first.focus(), 50);
2381
+ _pendingRunFn = (values) => _execCode(code, values);
2382
+ return;
2383
+ }
2384
+ _execCode(code, []);
2385
+ }
2386
+
2387
+ function _execCode(code, inputValues) {
2388
+ const dot = document.getElementById('sb-dot');
2389
+ dot.className = 'sb-dot run';
2390
+ document.getElementById('sb-status').textContent = 'Running...';
2391
+ document.getElementById('sb-time').textContent = '';
2392
+ outputDisplay.innerHTML = '';
2393
+ errorDisplay.innerHTML = '';
2394
+ lastErrors = [];
2395
+
2396
+ // ── Web mode ──────────────────────────────────────────────
2397
+ if (isWebCode(code)) {
2398
+ const webTab = document.getElementById('outtab-web');
2399
+ const saveBtn = document.getElementById('save-html-btn');
2400
+ webTab.style.display = '';
2401
+ saveBtn.style.display = '';
2402
+ try {
2403
+ const html = buildWebDoc(code);
2404
+ _lastWebHTML = html;
2405
+ const iframe = document.getElementById('web-preview');
2406
+ const webPanel = document.getElementById('out-web');
2407
+ if (!webPanel.querySelector('.web-toolbar')) {
2408
+ const bar = document.createElement('div');
2409
+ bar.className = 'web-toolbar';
2410
+ bar.innerHTML = '<span class="web-toolbar-label">🌐 Live Preview</span>' +
2411
+ '<button class="web-dl-btn" onclick="saveHTML()">⬇ Download .html</button>';
2412
+ webPanel.insertBefore(bar, iframe);
2413
+ }
2414
+ iframe.srcdoc = html;
2415
+ switchOutTab('web', document.getElementById('outtab-web'));
2416
+ dot.className = 'sb-dot ok';
2417
+ document.getElementById('sb-status').textContent = 'Rendered';
2418
+ document.getElementById('err-badge').style.display = 'none';
2419
+ errorDisplay.innerHTML = '<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>';
2420
+ } catch(e) {
2421
+ dot.className = 'sb-dot err';
2422
+ document.getElementById('sb-status').textContent = 'Error';
2423
+ outputDisplay.innerHTML = `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Web error: ${esc(e.message)}</span></div>`;
2424
+ switchOutTab('output', document.getElementById('outtab-output'));
2425
+ }
2426
+ return;
2427
+ }
2428
+
2429
+ // ── Script mode ───────────────────────────────────────────
2430
+ const t0 = performance.now();
2431
+ const lines = [];
2432
+ setTimeout(() => {
2433
+ try {
2434
+ const interp = new Interpreter(
2435
+ msg => lines.push({ text: msg, cls: '' }),
2436
+ msg => lines.push({ text: msg, cls: 'warn' })
2437
+ );
2438
+ interp._inputQueue = [...inputValues];
2439
+ interp.run(code);
2440
+ const elapsed = (performance.now() - t0).toFixed(1);
2441
+ dot.className = 'sb-dot ok';
2442
+ document.getElementById('sb-status').textContent = 'Success';
2443
+ document.getElementById('sb-time').textContent = elapsed + 'ms';
2444
+ if (!lines.length) {
2445
+ outputDisplay.innerHTML = '<div class="out-line success"><span class="out-prefix">✓</span><span class="out-text">Program completed — no output</span></div>';
2446
+ } else {
2447
+ outputDisplay.innerHTML = lines.map((l,i) =>
2448
+ `<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
2449
+ ).join('');
2450
+ }
2451
+ errorDisplay.innerHTML = '<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>';
2452
+ document.getElementById('err-badge').style.display = 'none';
2453
+ } catch(e) {
2454
+ const elapsed = (performance.now() - t0).toFixed(1);
2455
+ dot.className = 'sb-dot err';
2456
+ document.getElementById('sb-status').textContent = 'Error';
2457
+ document.getElementById('sb-time').textContent = elapsed + 'ms';
2458
+ if (lines.length) {
2459
+ outputDisplay.innerHTML = lines.map((l,i)=>
2460
+ `<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
2461
+ ).join('') + `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(e.message)}</span></div>`;
2462
+ } else {
2463
+ outputDisplay.innerHTML = `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(e.message)}</span></div>`;
2464
+ }
2465
+ const lineNum = e.ssLine !== undefined ? e.ssLine + 1 : '?';
2466
+ const snippet = e.snippet || (e.ssLine !== undefined ? (editor.value.split('\n')[e.ssLine]||'') : '');
2467
+ errorDisplay.innerHTML = `
2468
+ <div class="err-card">
2469
+ <div class="err-title">⚠ Runtime Error</div>
2470
+ <div class="err-detail">${esc(e.message)}</div>
2471
+ ${lineNum !== '?' ? `<div class="err-line-ref">Line ${lineNum}</div>` : ''}
2472
+ ${snippet ? `<div class="err-snippet"><span class="err-arrow">→ </span>${esc(snippet.trim())}</div>` : ''}
2473
+ </div>`;
2474
+ const badge = document.getElementById('err-badge');
2475
+ badge.textContent = '1'; badge.style.display = 'inline';
2476
+ }
2477
+ outputDisplay.scrollTop = outputDisplay.scrollHeight;
2478
+ }, 30);
2479
+ }
2480
+
2481
+
2482
+
2483
+ // ============================================================
2484
+ // WEB ENGINE
2485
+ // ============================================================
2486
+
2487
+ // Detects if the code uses any web commands
2488
+ function isWebCode(code) {
2489
+ return /^\s*(page|add)\s+/m.test(code);
2490
+ }
2491
+
2492
+ // Build a full HTML document from StructScript web commands
2493
+ function buildWebDoc(code) {
2494
+ // Page-level settings
2495
+ let pageTitle = 'StructScript Page';
2496
+ let pageStyles = {};
2497
+ let bodyClass = '';
2498
+
2499
+ // Element tree
2500
+ const elements = []; // { tag, id, classes, text, html, styles, attrs, events, children, parent }
2501
+ let currentEl = null;
2502
+ let currentIndent = 0;
2503
+ const elStack = []; // stack for nested elements
2504
+
2505
+ const lines = code.split('\n');
2506
+ let i = 0;
2507
+
2508
+ function getIndent(line) {
2509
+ let n = 0; while (n < line.length && line[n] === ' ') n++; return n;
2510
+ }
2511
+
2512
+ function parseStringArg(s) {
2513
+ s = s.trim();
2514
+ if ((s.startsWith('"') && s.endsWith('"')) ||
2515
+ (s.startsWith("'") && s.endsWith("'"))) return s.slice(1,-1);
2516
+ return s;
2517
+ }
2518
+
2519
+ while (i < lines.length) {
2520
+ const raw = lines[i];
2521
+ const t = raw.trim();
2522
+ const ind = getIndent(raw);
2523
+ i++;
2524
+
2525
+ if (!t || t.startsWith('//')) continue;
2526
+
2527
+ // ── page "title": ────────────────────────────────────
2528
+ if (/^page\b/.test(t)) {
2529
+ const m = t.match(/^page\s+"([^"]*)"\s*:?$/) ||
2530
+ t.match(/^page\s+'([^']*)'\s*:?$/);
2531
+ if (m) pageTitle = m[1];
2532
+ currentEl = { _type: 'page', styles: {}, attrs: {} };
2533
+ elStack.length = 0;
2534
+ elStack.push({ el: currentEl, indent: ind });
2535
+ continue;
2536
+ }
2537
+
2538
+ // ── add TAG "id/class": ──────────────────────────────
2539
+ const addM = t.match(/^add\s+(\w+)(?:\s+"([^"]*)")?(?:\s+'([^']*)')?\s*:?$/);
2540
+ if (addM) {
2541
+ const tag = addM[1].toLowerCase();
2542
+ const rawId = addM[2] || addM[3] || '';
2543
+ // Pop stack to find parent at correct indent
2544
+ while (elStack.length > 1 && elStack[elStack.length-1].indent >= ind) elStack.pop();
2545
+ const parent = elStack[elStack.length-1]?.el || null;
2546
+ const el = {
2547
+ tag, id: rawId.startsWith('#') ? rawId.slice(1) : (rawId.includes(' ') ? '' : rawId),
2548
+ classes: rawId.startsWith('.') ? [rawId.slice(1)] : (rawId.includes(' ') ? rawId.split(' ') : []),
2549
+ text: '', html: '', styles: {}, attrs: {}, events: [], children: [],
2550
+ parent, _indent: ind
2551
+ };
2552
+ if (parent && parent._type !== 'page') parent.children.push(el);
2553
+ else elements.push(el);
2554
+ currentEl = el;
2555
+ elStack.push({ el, indent: ind });
2556
+ continue;
2557
+ }
2558
+
2559
+ // ── Properties inside an element block ───────────────
2560
+ if (currentEl && ind > (elStack[elStack.length-1]?.indent ?? -1)) {
2561
+ const el = elStack[elStack.length-1].el;
2562
+
2563
+ // text "content"
2564
+ const textM = t.match(/^text\s+(.+)$/);
2565
+ if (textM) { el.text = parseStringArg(textM[1]); continue; }
2566
+
2567
+ // html "<raw>"
2568
+ const htmlM = t.match(/^html\s+(.+)$/);
2569
+ if (htmlM) { el.html = parseStringArg(htmlM[1]); continue; }
2570
+
2571
+ // style property "value" OR style property: "value"
2572
+ const styleM = t.match(/^style\s+([\w-]+)\s*:?\s+(.+)$/);
2573
+ if (styleM) {
2574
+ const prop = styleM[1].replace(/-([a-z])/g, (_,c) => c.toUpperCase());
2575
+ el.styles[prop] = parseStringArg(styleM[2]);
2576
+ if (el._type === 'page') pageStyles[prop] = parseStringArg(styleM[2]);
2577
+ continue;
2578
+ }
2579
+
2580
+ // attr name "value"
2581
+ const attrM = t.match(/^attr\s+(\w+)\s+(.+)$/);
2582
+ if (attrM) { el.attrs[attrM[1]] = parseStringArg(attrM[2]); continue; }
2583
+
2584
+ // class "name"
2585
+ const classM = t.match(/^class\s+(.+)$/);
2586
+ if (classM) { el.classes.push(parseStringArg(classM[1])); continue; }
2587
+
2588
+ // on EVENT: (collect handler lines)
2589
+ const onM = t.match(/^on\s+(\w+)\s*:?$/);
2590
+ if (onM) {
2591
+ const evName = onM[1];
2592
+ const handlerLines = [];
2593
+ while (i < lines.length) {
2594
+ const nextRaw = lines[i];
2595
+ const nextInd = getIndent(nextRaw);
2596
+ if (!nextRaw.trim() || nextInd <= ind) break;
2597
+ handlerLines.push(nextRaw.slice(nextInd));
2598
+ i++;
2599
+ }
2600
+ el.events.push({ event: evName, code: handlerLines.join('\n') });
2601
+ continue;
2602
+ }
2603
+
2604
+ // animate TYPE DURATION
2605
+ const animM = t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+(\w+))?$/);
2606
+ if (animM) {
2607
+ el._anim = { type: animM[1], dur: parseFloat(animM[2] || 0.4), easing: animM[3] || 'ease' };
2608
+ continue;
2609
+ }
2610
+ }
2611
+ }
2612
+
2613
+ // ── Render elements to HTML ───────────────────────────
2614
+ function renderEl(el) {
2615
+ if (el._type === 'page') return '';
2616
+ const tag = el.tag;
2617
+ const idAttr = el.id ? ` id="${el.id}"` : '';
2618
+ const clsAttr = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
2619
+
2620
+ // Build style string
2621
+ let styleStr = Object.entries(el.styles).map(([k,v]) => {
2622
+ const cssProp = k.replace(/([A-Z])/g, '-$1').toLowerCase();
2623
+ return `${cssProp}:${v}`;
2624
+ }).join(';');
2625
+
2626
+ // Animation keyframe via inline style
2627
+ if (el._anim) {
2628
+ const animMap = {
2629
+ fadeIn: `fadeIn ${el._anim.dur}s ${el._anim.easing} forwards`,
2630
+ fadeOut: `fadeOut ${el._anim.dur}s ${el._anim.easing} forwards`,
2631
+ slideIn: `slideIn ${el._anim.dur}s ${el._anim.easing} forwards`,
2632
+ slideUp: `slideUp ${el._anim.dur}s ${el._anim.easing} forwards`,
2633
+ bounce: `bounce ${el._anim.dur}s ${el._anim.easing} infinite`,
2634
+ pulse: `pulse ${el._anim.dur}s ${el._anim.easing} infinite`,
2635
+ spin: `spin ${el._anim.dur}s linear infinite`,
2636
+ shake: `shake ${el._anim.dur}s ${el._anim.easing}`,
2637
+ pop: `pop ${el._anim.dur}s ${el._anim.easing} forwards`,
2638
+ };
2639
+ const animVal = animMap[el._anim.type] || `${el._anim.type} ${el._anim.dur}s ${el._anim.easing}`;
2640
+ styleStr += (styleStr ? ';' : '') + `animation:${animVal}`;
2641
+ }
2642
+
2643
+ const styleAttr = styleStr ? ` style="${styleStr}"` : '';
2644
+
2645
+ // Extra attributes
2646
+ const extraAttrs = Object.entries(el.attrs)
2647
+ .map(([k,v]) => ` ${k}="${v}"`).join('');
2648
+
2649
+ // Event handlers — inject inline onclick etc.
2650
+ const evAttrs = el.events.map(ev => {
2651
+ const escaped = ev.code.replace(/"/g, '&quot;').replace(/\n/g, ' ');
2652
+ return ` data-ss-on${ev.event}="${escaped}"`;
2653
+ }).join('');
2654
+
2655
+ // Self-closing tags
2656
+ const selfClose = ['img','input','br','hr','meta','link'].includes(tag);
2657
+ if (selfClose) return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}/>\n`;
2658
+
2659
+ const inner = el.html || el.text || el.children.map(renderEl).join('');
2660
+ return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}${evAttrs}>${inner}</${tag}>\n`;
2661
+ }
2662
+
2663
+ const bodyStyleStr = Object.entries(pageStyles)
2664
+ .map(([k,v]) => `${k.replace(/([A-Z])/g,'-$1').toLowerCase()}:${v}`).join(';');
2665
+
2666
+ const bodyHtml = elements.map(renderEl).join('');
2667
+
2668
+ // Animation keyframes
2669
+ const KEYFRAMES = `
2670
+ @keyframes fadeIn { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:none} }
2671
+ @keyframes fadeOut { from{opacity:1} to{opacity:0} }
2672
+ @keyframes slideIn { from{transform:translateX(-40px);opacity:0} to{transform:none;opacity:1} }
2673
+ @keyframes slideUp { from{transform:translateY(40px);opacity:0} to{transform:none;opacity:1} }
2674
+ @keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-16px)} }
2675
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
2676
+ @keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
2677
+ @keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} 75%{transform:translateX(8px)} }
2678
+ @keyframes pop { 0%{transform:scale(0.5);opacity:0} 70%{transform:scale(1.1)} 100%{transform:scale(1);opacity:1} }
2679
+ `;
2680
+
2681
+ // Event-wiring script: wire data-ss-on* attributes to JS event listeners
2682
+ const EVENT_TYPES = ['click','mouseover','mouseout','mouseenter','mouseleave','keydown','keyup','change','focus','blur','dblclick'];
2683
+ const evScript = EVENT_TYPES.map(ev =>
2684
+ `document.querySelectorAll('[data-ss-on${ev}]').forEach(function(el){el.addEventListener('${ev}',function(){try{(new Function(this.getAttribute('data-ss-on${ev}')))()}catch(e){console.error(e)}}.bind(el));});`
2685
+ ).join('\n');
2686
+
2687
+ return `<!DOCTYPE html>
2688
+ <html lang="en">
2689
+ <head>
2690
+ <meta charset="UTF-8">
2691
+ <meta name="viewport" content="width=device-width,initial-scale=1">
2692
+ <title>${pageTitle}</title>
2693
+ <style>
2694
+ *{box-sizing:border-box;margin:0;padding:0}
2695
+ ${KEYFRAMES}
2696
+ </style>
2697
+ </head>
2698
+ <body${bodyStyleStr ? ` style="${bodyStyleStr}"` : ''}>
2699
+ ${bodyHtml}
2700
+ <script>(function(){${evScript}})()</\/script>
2701
+ </body>
2702
+ </html>`;
2703
+ }
2704
+
2705
+ // Saved web HTML for download
2706
+ let _lastWebHTML = '';
2707
+
2708
+ async function saveHTML() {
2709
+ if (!_lastWebHTML) return;
2710
+ if (window.electronAPI) {
2711
+ const suggested = (currentFilePath || currentFile).replace(/\.ss$/i, '.html');
2712
+ const result = await window.electronAPI.dialogSaveHtml({ content: _lastWebHTML, suggested });
2713
+ if (!result.canceled) showToast('\u2713 Saved ' + (result.path||'').split(/[\\/]/).pop());
2714
+ return;
2715
+ }
2716
+ const blob = new Blob([_lastWebHTML], { type: 'text/html' });
2717
+ const a = document.createElement('a');
2718
+ a.href = URL.createObjectURL(blob);
2719
+ a.download = currentFile.replace('.ss','.html') || 'page.html';
2720
+ a.click();
2721
+ URL.revokeObjectURL(a.href);
2722
+ }
2723
+
2724
+
2725
+
2726
+ // ============================================================
2727
+ // FILE OPERATIONS (browser fallback — editor overrides these)
2728
+ // ============================================================
2729
+ function handleFileOpen(event) {
2730
+ const file = event.target.files[0]; if (!file) return;
2731
+ const reader = new FileReader();
2732
+ reader.onload = e => {
2733
+ editor.value = e.target.result;
2734
+ currentFile = file.name;
2735
+ document.getElementById('current-filename').textContent = currentFile;
2736
+ updateHighlight(); updateLineNumbers(); setModified(false);
2737
+ };
2738
+ reader.readAsText(file);
2739
+ }
2740
+ function shareCode() {} // disabled in editor mode
2741
+
2742
+
2743
+ // ============================================================
2744
+ // THEME
2745
+ // ============================================================
2746
+ function toggleTheme() {
2747
+ currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
2748
+ document.body.setAttribute('data-theme', currentTheme);
2749
+ document.getElementById('theme-btn').textContent = currentTheme === 'dark' ? '🌙' : '☀️';
2750
+ }
2751
+
2752
+ // ============================================================
2753
+ // PAGE / TAB SWITCHING
2754
+ // ============================================================
2755
+ function showPage(name, btn) {
2756
+ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
2757
+ document.querySelectorAll('.nav-tab').forEach(b=>b.classList.remove('active'));
2758
+ document.getElementById('page-'+name).classList.add('active');
2759
+ if(btn) btn.classList.add('active');
2760
+ }
2761
+
2762
+ function switchOutTab(name, btn) {
2763
+ document.querySelectorAll('.out-tab').forEach(b=>b.classList.remove('active'));
2764
+ document.querySelectorAll('.out-panel').forEach(p=>p.classList.remove('active'));
2765
+ btn.classList.add('active');
2766
+ document.getElementById('out-'+name).classList.add('active');
2767
+ }
2768
+
2769
+ // ============================================================
2770
+ // LOAD EXAMPLE
2771
+ // ============================================================
2772
+ function loadExample(name) {
2773
+ if (isModified && !confirm('Load example? Unsaved changes will be lost.')) return;
2774
+ editor.value = EXAMPLES[name] || '';
2775
+ currentFile = name + '.ss';
2776
+ document.getElementById('current-filename').textContent = currentFile;
2777
+ markModified(false);
2778
+ updateAll();
2779
+ editor.focus();
2780
+ }
2781
+
2782
+ // ============================================================
2783
+ // EDITOR EVENT LISTENERS
2784
+ // ============================================================
2785
+ editor.addEventListener('input', () => { updateAll(); showAutocomplete(); });
2786
+ editor.addEventListener('scroll', () => {
2787
+ highlightLayer.scrollTop = editor.scrollTop;
2788
+ highlightLayer.scrollLeft = editor.scrollLeft;
2789
+ lineNumbers.scrollTop = editor.scrollTop;
2790
+ });
2791
+ editor.addEventListener('click', () => { updateLineNumbers(); hideAutocomplete(); });
2792
+ editor.addEventListener('blur', () => setTimeout(hideAutocomplete, 150));
2793
+
2794
+ editor.addEventListener('keydown', e => {
2795
+ // Autocomplete navigation
2796
+ if (acDiv.style.display !== 'none') {
2797
+ if (e.key === 'ArrowDown') {
2798
+ e.preventDefault();
2799
+ acSelectedIdx = Math.min(acSelectedIdx+1, acItems.length-1);
2800
+ acDiv.querySelectorAll('.ac-item').forEach((el,i)=>el.classList.toggle('selected',i===acSelectedIdx));
2801
+ return;
2802
+ }
2803
+ if (e.key === 'ArrowUp') {
2804
+ e.preventDefault();
2805
+ acSelectedIdx = Math.max(acSelectedIdx-1, 0);
2806
+ acDiv.querySelectorAll('.ac-item').forEach((el,i)=>el.classList.toggle('selected',i===acSelectedIdx));
2807
+ return;
2808
+ }
2809
+ if (e.key === 'Enter' || e.key === 'Tab') {
2810
+ if (acSelectedIdx >= 0) { e.preventDefault(); applyAC(acSelectedIdx); return; }
2811
+ }
2812
+ if (e.key === 'Escape') { hideAutocomplete(); return; }
2813
+ }
2814
+
2815
+ // Tab = 2 spaces
2816
+ if (e.key === 'Tab') {
2817
+ e.preventDefault();
2818
+ const s = editor.selectionStart, en = editor.selectionEnd;
2819
+ editor.value = editor.value.slice(0,s)+' '+editor.value.slice(en);
2820
+ editor.selectionStart = editor.selectionEnd = s+2;
2821
+ updateAll();
2822
+ }
2823
+ // Run shortcut
2824
+ if (e.key === 'Enter' && (e.ctrlKey||e.metaKey)) { e.preventDefault(); runCode(); }
2825
+ // Save shortcut
2826
+ if (e.key === 's' && (e.ctrlKey||e.metaKey)) { e.preventDefault(); saveFile(); }
2827
+ // Auto-indent after colon
2828
+ if (e.key === 'Enter') {
2829
+ const pos = editor.selectionStart;
2830
+ const lineBefore = editor.value.slice(0, pos).split('\n').pop();
2831
+ if (lineBefore.trimEnd().endsWith(':')) {
2832
+ e.preventDefault();
2833
+ const indent = ' '.repeat(lineBefore.match(/^\s*/)[0].length + 2);
2834
+ const ins = '\n' + indent;
2835
+ editor.value = editor.value.slice(0, pos) + ins + editor.value.slice(pos);
2836
+ editor.selectionStart = editor.selectionEnd = pos + ins.length;
2837
+ updateAll();
2838
+ }
2839
+ }
2840
+ // Auto-close brackets and quotes
2841
+ const pairs = { '(':')', '[':']', '"':'"', "'":"'" };
2842
+ if (pairs[e.key] && !e.ctrlKey && !e.metaKey) {
2843
+ const pos = editor.selectionStart, sel = editor.selectionEnd;
2844
+ if (pos !== sel) {
2845
+ e.preventDefault();
2846
+ const selected = editor.value.slice(pos, sel);
2847
+ const ins = e.key + selected + pairs[e.key];
2848
+ editor.value = editor.value.slice(0,pos) + ins + editor.value.slice(sel);
2849
+ editor.selectionStart = pos+1; editor.selectionEnd = sel+1;
2850
+ updateAll();
2851
+ }
2852
+ }
2853
+ });
2854
+
2855
+ // Initial render
2856
+ loadSharedCode();
2857
+ updateLineNumbers();
2858
+
2859
+
2860
+ // ============================================================
2861
+ // EDITOR FILE I/O (overrides browser-only versions above)
2862
+ // ============================================================
2863
+
2864
+ // ============================================================
2865
+ // EDITOR MODE — all file I/O goes through the local server API
2866
+ // ============================================================
2867
+
2868
+ const IS_EDITOR = true;
2869
+ let currentFilePath = null; // full absolute path on disk
2870
+ let autoSaveTimer = null;
2871
+ let unsavedChanges = false;
2872
+
2873
+ // ── API helpers ─────────────────────────────────────────────
2874
+ async function api(route, method = 'GET', body = null) {
2875
+ if (window.electronAPI) {
2876
+ const b = body || {};
2877
+ if (route === '/api/run') return window.electronAPI.run(b);
2878
+ if (route === '/api/save') return window.electronAPI.save({ filePath: b.path, content: b.content });
2879
+ if (route === '/api/open') return window.electronAPI.open({ filePath: b.path });
2880
+ if (route === '/api/recent') return window.electronAPI.recent();
2881
+ if (route === '/api/new') return window.electronAPI.newFile(b);
2882
+ if (route.startsWith('/api/browse')) {
2883
+ const dir = new URL(route, 'http://x').searchParams.get('dir');
2884
+ return window.electronAPI.browse({ dir });
2885
+ }
2886
+ }
2887
+ const opts = { method, headers: { 'Content-Type': 'application/json' } };
2888
+ if (body) opts.body = JSON.stringify(body);
2889
+ const res = await fetch(route, opts);
2890
+ return res.json();
2891
+ }
2892
+
2893
+ // ── Show saved toast ────────────────────────────────────────
2894
+ function showToast(msg = '✓ Saved') {
2895
+ const t = document.createElement('div');
2896
+ t.className = 'saved-toast';
2897
+ t.textContent = msg;
2898
+ document.body.appendChild(t);
2899
+ setTimeout(() => t.remove(), 1700);
2900
+ }
2901
+
2902
+ // ── Update sidebar + filepath bar ───────────────────────────
2903
+ function setCurrentFile(fullPath, name) {
2904
+ currentFilePath = fullPath;
2905
+ currentFile = name || fullPath.split(/[\\/]/).pop();
2906
+ document.getElementById('current-filename').textContent = currentFile;
2907
+ document.getElementById('fp-path').textContent = fullPath || '';
2908
+ document.getElementById('fp-path').title = fullPath || '';
2909
+ unsavedChanges = false;
2910
+ document.getElementById('modified-indicator').style.display = 'none';
2911
+ document.getElementById('fp-unsaved').classList.remove('show');
2912
+ refreshSidebar();
2913
+ }
2914
+
2915
+ function markUnsaved() {
2916
+ if (!unsavedChanges) {
2917
+ unsavedChanges = true;
2918
+ document.getElementById('modified-indicator').style.display = 'inline';
2919
+ document.getElementById('fp-unsaved').classList.add('show');
2920
+ }
2921
+ clearTimeout(autoSaveTimer);
2922
+ if (currentFilePath) {
2923
+ autoSaveTimer = setTimeout(() => autoSave(), 2000);
2924
+ }
2925
+ }
2926
+
2927
+ async function autoSave() {
2928
+ if (!currentFilePath || !unsavedChanges) return;
2929
+ await doSave(currentFilePath, false);
2930
+ }
2931
+
2932
+ async function doSave(filePath, toast = true) {
2933
+ const content = editor.value;
2934
+ const result = await api('/api/save', 'POST', { path: filePath, content });
2935
+ if (result.ok) {
2936
+ currentFilePath = result.path;
2937
+ unsavedChanges = false;
2938
+ document.getElementById('modified-indicator').style.display = 'none';
2939
+ document.getElementById('fp-unsaved').classList.remove('show');
2940
+ if (toast) showToast('✓ Saved');
2941
+ refreshSidebar();
2942
+ } else {
2943
+ showToast('⚠ Save failed: ' + result.error);
2944
+ }
2945
+ }
2946
+
2947
+ // ── File operations (override playground versions) ──────────
2948
+ async function newFile() {
2949
+ const name = prompt('File name:', 'untitled.ss') || 'untitled.ss';
2950
+ const n = name.endsWith('.ss') ? name : name + '.ss';
2951
+ const result = await api('/api/new', 'POST', { name: n });
2952
+ if (result.ok) {
2953
+ editor.value = result.content;
2954
+ setCurrentFile(result.path, result.name);
2955
+ updateHighlight(); updateLineNumbers();
2956
+ document.getElementById('modified-indicator').style.display = 'none';
2957
+ }
2958
+ }
2959
+
2960
+ async function saveFile() {
2961
+ if (currentFilePath) {
2962
+ await doSave(currentFilePath);
2963
+ } else {
2964
+ await saveFileAs();
2965
+ }
2966
+ }
2967
+
2968
+ async function saveFileAs() {
2969
+ if (window.electronAPI) {
2970
+ const suggested = currentFilePath || (currentFile.endsWith('.ss') ? currentFile : currentFile + '.ss');
2971
+ const result = await window.electronAPI.dialogSave({ suggested, content: editor.value });
2972
+ if (result.canceled) return;
2973
+ setCurrentFile(result.path, result.name);
2974
+ unsavedChanges = false;
2975
+ document.getElementById('modified-indicator').style.display = 'none';
2976
+ document.getElementById('fp-unsaved').classList.remove('show');
2977
+ showToast('\u2713 Saved');
2978
+ refreshSidebar();
2979
+ return;
2980
+ }
2981
+ const suggested = currentFilePath || (currentFile.endsWith('.ss') ? currentFile : currentFile + '.ss');
2982
+ showSaveAsDialog(suggested);
2983
+ }
2984
+
2985
+ async function openFile() {
2986
+ if (window.electronAPI) {
2987
+ const result = await window.electronAPI.dialogOpen();
2988
+ if (result.canceled) return;
2989
+ editor.value = result.content;
2990
+ setCurrentFile(result.path, result.name);
2991
+ updateHighlight(); updateLineNumbers();
2992
+ return;
2993
+ }
2994
+ showBrowseDialog();
2995
+ }
2996
+
2997
+ async function openFilePath(filePath) {
2998
+ const result = await api('/api/open', 'POST', { path: filePath });
2999
+ if (result.ok) {
3000
+ editor.value = result.content;
3001
+ setCurrentFile(result.path, result.name);
3002
+ updateHighlight(); updateLineNumbers();
3003
+ closeBrowseDialog();
3004
+ // Switch to playground view
3005
+ showPage('playground', document.getElementById('tab-playground'));
3006
+ } else {
3007
+ showToast('⚠ ' + result.error);
3008
+ }
3009
+ }
3010
+
3011
+ // ── Run code via server ─────────────────────────────────────
3012
+ // Override the existing runCode to send to server API
3013
+ const _origRunCode = runCode;
3014
+ async function runCode() {
3015
+ const code = editor.value;
3016
+
3017
+ // Web mode: use client-side buildWebDoc (no input needed)
3018
+ if (isWebCode(code)) { _execCode(code, []); return; }
3019
+
3020
+ // Scan for inputs client-side so we can show the modal
3021
+ const inputPrompts = scanForInputs(code);
3022
+ if (inputPrompts.length > 0) {
3023
+ const fieldsDiv = document.getElementById('input-modal-fields');
3024
+ fieldsDiv.innerHTML = inputPrompts.map((prompt, i) => `
3025
+ <div class="im-field">
3026
+ <label class="im-label"><span class="im-prompt-text">${esc(prompt)}</span><span class="im-idx">#${i+1}</span></label>
3027
+ <input class="im-input" type="text" placeholder="type here..." autocomplete="off" spellcheck="false">
3028
+ </div>`).join('');
3029
+ document.getElementById('input-modal-overlay').style.display = 'flex';
3030
+ const first = fieldsDiv.querySelector('.im-input');
3031
+ if (first) setTimeout(() => first.focus(), 50);
3032
+ _pendingRunFn = (values) => _runViaServer(code, values);
3033
+ return;
3034
+ }
3035
+ await _runViaServer(code, []);
3036
+ }
3037
+
3038
+ async function _runViaServer(code, inputs) {
3039
+ const dot = document.getElementById('sb-dot');
3040
+ dot.className = 'sb-dot run';
3041
+ document.getElementById('sb-status').textContent = 'Running...';
3042
+ document.getElementById('sb-time').textContent = '';
3043
+ outputDisplay.innerHTML = '';
3044
+ errorDisplay.innerHTML = '';
3045
+
3046
+ const t0 = performance.now();
3047
+ let result;
3048
+ try {
3049
+ result = await api('/api/run', 'POST', { source: code, inputs });
3050
+ } catch (e) {
3051
+ // fallback to client-side if server unreachable
3052
+ _execCode(code, inputs);
3053
+ return;
3054
+ }
3055
+ const elapsed = (performance.now() - t0).toFixed(1);
3056
+
3057
+ if (result.ok) {
3058
+ dot.className = 'sb-dot ok';
3059
+ document.getElementById('sb-status').textContent = 'Success';
3060
+ document.getElementById('sb-time').textContent = elapsed + 'ms';
3061
+ if (!result.lines.length) {
3062
+ outputDisplay.innerHTML = '<div class="out-line success"><span class="out-prefix">✓</span><span class="out-text">Program completed — no output</span></div>';
3063
+ } else {
3064
+ outputDisplay.innerHTML = result.lines.map((l, i) =>
3065
+ `<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
3066
+ ).join('');
3067
+ }
3068
+ errorDisplay.innerHTML = '<div class="no-errors"><div class="no-errors-icon">✓</div>No errors</div>';
3069
+ document.getElementById('err-badge').style.display = 'none';
3070
+ // Auto-switch to output tab
3071
+ switchOutTab('output', document.getElementById('outtab-output'));
3072
+ } else {
3073
+ dot.className = 'sb-dot err';
3074
+ document.getElementById('sb-status').textContent = 'Error';
3075
+ document.getElementById('sb-time').textContent = elapsed + 'ms';
3076
+ const err = result.error || {};
3077
+ if (result.lines.length) {
3078
+ outputDisplay.innerHTML = result.lines.map((l, i) =>
3079
+ `<div class="out-line ${l.cls}"><span class="out-prefix">${i+1}</span><span class="out-text">${esc(String(l.text))}</span></div>`
3080
+ ).join('') + `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(err.message)}</span></div>`;
3081
+ } else {
3082
+ outputDisplay.innerHTML = `<div class="out-line err"><span class="out-prefix">!</span><span class="out-text">Error: ${esc(err.message||'Unknown error')}</span></div>`;
3083
+ }
3084
+ errorDisplay.innerHTML = `
3085
+ <div class="err-card">
3086
+ <div class="err-title">⚠ Runtime Error</div>
3087
+ <div class="err-detail">${esc(err.message||'')}</div>
3088
+ ${err.line ? `<div class="err-line-ref">Line ${err.line}</div>` : ''}
3089
+ ${err.snippet ? `<div class="err-snippet"><span class="err-arrow">→ </span>${esc(err.snippet.trim())}</div>` : ''}
3090
+ </div>`;
3091
+ const badge = document.getElementById('err-badge');
3092
+ badge.textContent = '1'; badge.style.display = 'inline';
3093
+ }
3094
+ outputDisplay.scrollTop = outputDisplay.scrollHeight;
3095
+ }
3096
+
3097
+ // ── Sidebar ─────────────────────────────────────────────────
3098
+ async function refreshSidebar() {
3099
+ const result = await api('/api/recent');
3100
+ const list = document.getElementById('file-tree-list');
3101
+ if (!result.files || !result.files.length) {
3102
+ list.innerHTML = '<div style="padding:16px 14px;font-size:11px;color:var(--muted)">No recent files</div>';
3103
+ return;
3104
+ }
3105
+ list.innerHTML = result.files.map(f => {
3106
+ const name = f.split(/[\\/]/).pop();
3107
+ const isActive = f === currentFilePath;
3108
+ return `<div class="file-item ${isActive ? 'active' : ''}" onclick="openFilePath(${JSON.stringify(f)})">
3109
+ <span class="file-icon">${isActive ? '▶' : '◦'}</span>
3110
+ <span class="file-name" title="${esc(f)}">${esc(name)}</span>
3111
+ </div>`;
3112
+ }).join('');
3113
+ }
3114
+
3115
+ // ── Browse dialog ───────────────────────────────────────────
3116
+ let _browseDir = '';
3117
+
3118
+ async function showBrowseDialog() {
3119
+ document.getElementById('browse-overlay').classList.add('open');
3120
+ const homeDir = await getHomeDir();
3121
+ _browseDir = homeDir;
3122
+ await loadBrowseDir(_browseDir);
3123
+ }
3124
+
3125
+ async function getHomeDir() {
3126
+ if (window.electronAPI) return window.electronAPI.homedir();
3127
+ const r = await api('/api/recent');
3128
+ if (r.files && r.files.length) {
3129
+ const parts = r.files[0].split(/[\\/]/);
3130
+ parts.pop();
3131
+ return parts.join('/') || '/';
3132
+ }
3133
+ return '/';
3134
+ }
3135
+
3136
+ async function loadBrowseDir(dir) {
3137
+ _browseDir = dir;
3138
+ document.getElementById('browse-path-input').value = dir;
3139
+ const result = await api(`/api/browse?dir=${encodeURIComponent(dir)}`);
3140
+ if (result.error) return;
3141
+ const list = document.getElementById('browse-list');
3142
+ list.innerHTML = result.items.map(e => {
3143
+ const isSS = !e.isDir && e.name.endsWith('.ss');
3144
+ return `<div class="browse-entry" onclick="${e.isDir ? `loadBrowseDir(${JSON.stringify(e.path)})` : `openFilePath(${JSON.stringify(e.path)})`}">
3145
+ <span class="be-icon">${e.isDir ? '📁' : '📄'}</span>
3146
+ <span class="be-name ${isSS ? 'be-ss' : ''}">${esc(e.name)}</span>
3147
+ </div>`;
3148
+ }).join('') || '<div style="padding:16px;font-size:11px;color:var(--muted)">No .ss files here</div>';
3149
+ }
3150
+
3151
+ function closeBrowseDialog() {
3152
+ document.getElementById('browse-overlay').classList.remove('open');
3153
+ }
3154
+
3155
+ function browseUp() {
3156
+ const parts = _browseDir.split(/[\\/]/);
3157
+ parts.pop();
3158
+ const parent = parts.join('/') || '/';
3159
+ if (parent !== _browseDir) loadBrowseDir(parent);
3160
+ }
3161
+
3162
+ function browseDirFromInput() {
3163
+ const val = document.getElementById('browse-path-input').value;
3164
+ if (val) loadBrowseDir(val);
3165
+ }
3166
+
3167
+ // ── Save As dialog ──────────────────────────────────────────
3168
+ function showSaveAsDialog(suggested) {
3169
+ document.getElementById('saveas-input').value = suggested || '';
3170
+ document.getElementById('saveas-overlay').classList.add('open');
3171
+ setTimeout(() => document.getElementById('saveas-input').focus(), 50);
3172
+ }
3173
+
3174
+ function closeSaveAsDialog() {
3175
+ document.getElementById('saveas-overlay').classList.remove('open');
3176
+ }
3177
+
3178
+ async function confirmSaveAs() {
3179
+ const p = document.getElementById('saveas-input').value.trim();
3180
+ if (!p) return;
3181
+ const path = p.endsWith('.ss') ? p : p + '.ss';
3182
+ closeSaveAsDialog();
3183
+ currentFilePath = path;
3184
+ currentFile = path.split(/[\\/]/).pop();
3185
+ document.getElementById('current-filename').textContent = currentFile;
3186
+ document.getElementById('fp-path').textContent = path;
3187
+ await doSave(path);
3188
+ }
3189
+
3190
+ // ── Hook editor change for autosave ────────────────────────
3191
+ function editorChanged() {
3192
+ markUnsaved();
3193
+ updateHighlight();
3194
+ updateLineNumbers();
3195
+ setModified(true);
3196
+ }
3197
+
3198
+ // ── Keyboard shortcuts ──────────────────────────────────────
3199
+ document.addEventListener('keydown', e => {
3200
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
3201
+ e.preventDefault();
3202
+ saveFile();
3203
+ }
3204
+ if ((e.ctrlKey || e.metaKey) && e.key === 'o') {
3205
+ e.preventDefault();
3206
+ openFile();
3207
+ }
3208
+ if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
3209
+ e.preventDefault();
3210
+ newFile();
3211
+ }
3212
+ });
3213
+
3214
+ // ── Init ────────────────────────────────────────────────────
3215
+ window.addEventListener('load', async () => {
3216
+ // Check if a file was passed in the URL
3217
+ const params = new URLSearchParams(location.search);
3218
+ const initFile = params.get('file');
3219
+ if (initFile) {
3220
+ await openFilePath(initFile);
3221
+ } else {
3222
+ // Load most recent file, or create untitled
3223
+ const r = await api('/api/recent');
3224
+ if (r.files && r.files.length) {
3225
+ await openFilePath(r.files[0]);
3226
+ } else {
3227
+ await newFile();
3228
+ }
3229
+ }
3230
+ refreshSidebar();
3231
+ });
3232
+
3233
+ // Hook editor input to markUnsaved (override plain updateEditor)
3234
+ const _origEditorInput = editor.oninput;
3235
+ editor.addEventListener('input', markUnsaved);
3236
+
3237
+ // Warn before closing with unsaved changes
3238
+ window.addEventListener('beforeunload', e => {
3239
+ if (unsavedChanges && currentFilePath) {
3240
+ e.preventDefault(); return '';
3241
+ }
3242
+ });
3243
+
3244
+
3245
+ </script>
3246
+ </body>
3247
+ </html>