structscript 1.3.0 → 1.4.1

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