Nymbo commited on
Commit
242cc3a
·
verified ·
1 Parent(s): d877c22

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1034 -18
index.html CHANGED
@@ -1,19 +1,1035 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <!--
3
+ File: micro-apps/templates/general_app_template.html
4
+ Purpose: A self-contained "Micro-App" starter you can copy/paste to begin any app or game.
5
+ Rules this template follows (per your spec):
6
+ - Single HTML file with <!DOCTYPE html>.
7
+ - Only HTML + CSS + JS. No downloaded assets.
8
+ - Optional libraries may be loaded via CDN (commented examples included).
9
+ - When you modify this Micro-App, rewrite the entire file inside a code block (no truncation).
10
+
11
+ Notes for Developers:
12
+ - Search for "TODO:" to see common extension points.
13
+ - Everything is namespaced under window.App to avoid globals.
14
+ - Includes: layout, theme toggle, settings modal, toast notifications, keyboard shortcuts,
15
+ a minimal state store with persistence, a command palette, and example views (Dashboard, Canvas Demo).
16
+ - Three.js bootstrap is included but commented out; un-comment if you need 3D.
17
+ -->
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="UTF-8" />
21
+ <!-- Basic metadata so the app behaves nicely on desktop & mobile -->
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <title>Vibe App Template</title>
24
+
25
+ <!-- Optional CDN Libraries (kept commented until you need them) -->
26
+ <!-- three.js (uncomment to enable 3D): -->
27
+ <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> -->
28
+
29
+ <style>
30
+ /* ====== Design System: CSS variables for easy theming ====== */
31
+ :root {
32
+ --bg: #0f1115; /* main background (dark) */
33
+ --panel: #151924; /* panels/cards */
34
+ --panel-2: #0c0f16; /* deeper panels */
35
+ --text: #e5e7eb; /* primary text */
36
+ --text-muted: #a3aab8; /* secondary text */
37
+ --primary: #5b9cff; /* accent color */
38
+ --primary-2: #2f6fe6; /* accent hover */
39
+ --border: #232836; /* subtle borders */
40
+ --success: #22c55e;
41
+ --warning: #fbbf24;
42
+ --danger: #ef4444;
43
+ --shadow: rgba(0,0,0,0.4);
44
+ --radius: 14px; /* rounded corners standard */
45
+ --radius-sm: 10px; /* smaller radius */
46
+ --radius-lg: 22px; /* larger radius */
47
+ --pad: 14px; /* standard spacing */
48
+ --pad-sm: 10px;
49
+ --pad-lg: 18px;
50
+ }
51
+
52
+ /* Light theme (toggled via [data-theme="light"]) */
53
+ [data-theme="light"] {
54
+ --bg: #f5f7fb;
55
+ --panel: #ffffff;
56
+ --panel-2: #f3f5fa;
57
+ --text: #0f172a;
58
+ --text-muted: #475569;
59
+ --primary: #2563eb;
60
+ --primary-2: #1e40af;
61
+ --border: #e5e7eb;
62
+ --shadow: rgba(0,0,0,0.08);
63
+ }
64
+
65
+ /* ====== Global Resets & Typography ====== */
66
+ * { box-sizing: border-box; }
67
+ html, body {
68
+ height: 100%;
69
+ margin: 0;
70
+ padding: 0;
71
+ background: var(--bg);
72
+ color: var(--text);
73
+ font-family: Inter, ui-sans-serif, system-ui, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
74
+ -webkit-font-smoothing: antialiased;
75
+ -moz-osx-font-smoothing: grayscale;
76
+ }
77
+
78
+ /* ====== App Shell Layout ======
79
+ Layout uses a header + main grid (sidebar + content). */
80
+ .app {
81
+ display: grid;
82
+ grid-template-rows: auto 1fr;
83
+ min-height: 100%;
84
+ }
85
+
86
+ /* Top bar with app title, theme toggle, and actions */
87
+ .topbar {
88
+ position: sticky;
89
+ top: 0;
90
+ z-index: 50;
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 10px;
94
+ padding: var(--pad);
95
+ border-bottom: 1px solid var(--border);
96
+ background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0)) , var(--bg);
97
+ -webkit-backdrop-filter: blur(6px);
98
+ backdrop-filter: blur(6px);
99
+ }
100
+
101
+ .brand {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 10px;
105
+ padding: 8px 12px;
106
+ border-radius: var(--radius-sm);
107
+ background: var(--panel);
108
+ border: 1px solid var(--border);
109
+ box-shadow: 0 6px 20px var(--shadow);
110
+ font-weight: 700;
111
+ letter-spacing: 0.2px;
112
+ }
113
+
114
+ .topbar-actions {
115
+ margin-left: auto;
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ }
120
+
121
+ /* Sidebar + Main Content Grid */
122
+ .main {
123
+ display: grid;
124
+ grid-template-columns: 280px 1fr;
125
+ gap: var(--pad);
126
+ padding: var(--pad);
127
+ }
128
+
129
+ /* Sidebar styling */
130
+ .sidebar {
131
+ background: var(--panel);
132
+ border: 1px solid var(--border);
133
+ border-radius: var(--radius);
134
+ padding: var(--pad);
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: var(--pad);
138
+ box-shadow: 0 10px 30px var(--shadow);
139
+ }
140
+
141
+ .nav-section h3 {
142
+ margin: 0 0 8px;
143
+ font-size: 12px;
144
+ text-transform: uppercase;
145
+ letter-spacing: 0.1em;
146
+ color: var(--text-muted);
147
+ }
148
+
149
+ .nav {
150
+ display: grid;
151
+ gap: 8px;
152
+ }
153
+
154
+ .nav button {
155
+ text-align: left;
156
+ padding: 10px 12px;
157
+ border-radius: var(--radius-sm);
158
+ border: 1px solid var(--border);
159
+ background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0));
160
+ color: var(--text);
161
+ cursor: pointer;
162
+ transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
163
+ }
164
+ .nav button:hover {
165
+ transform: translateY(-1px);
166
+ border-color: var(--primary);
167
+ }
168
+ .nav button.active {
169
+ border-color: var(--primary);
170
+ background: linear-gradient(180deg, rgba(91,156,255,0.18), rgba(91,156,255,0.04));
171
+ }
172
+
173
+ /* Main content card */
174
+ .content {
175
+ background: var(--panel);
176
+ border: 1px solid var(--border);
177
+ border-radius: var(--radius);
178
+ padding: 0;
179
+ min-height: 60vh;
180
+ box-shadow: 0 18px 50px var(--shadow);
181
+ /* Fallback for iOS Safari <16 */
182
+ overflow: hidden;
183
+ display: grid;
184
+ grid-template-rows: auto 1fr;
185
+ }
186
+ /* Preferred where supported */
187
+ @supports (overflow: clip) {
188
+ .content { overflow: clip; }
189
+ }
190
+
191
+ .content-header {
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: space-between;
195
+ padding: var(--pad);
196
+ border-bottom: 1px solid var(--border);
197
+ background: var(--panel-2);
198
+ }
199
+
200
+ .content-body {
201
+ padding: var(--pad);
202
+ overflow: auto;
203
+ }
204
+
205
+ /* Reusable button styles */
206
+ .btn {
207
+ appearance: none;
208
+ border: 1px solid var(--border);
209
+ background: var(--panel);
210
+ color: var(--text);
211
+ border-radius: var(--radius-sm);
212
+ padding: 10px 12px;
213
+ cursor: pointer;
214
+ transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
215
+ }
216
+ .btn:hover { transform: translateY(-1px); border-color: var(--primary); }
217
+ .btn.primary { background: var(--primary); border-color: var(--primary); color: white; }
218
+ .btn.primary:hover { background: var(--primary-2); border-color: var(--primary-2); }
219
+
220
+ /* Chips/pills for small indicators */
221
+ .pill {
222
+ display: inline-flex; align-items: center; gap: 6px;
223
+ padding: 6px 10px;
224
+ border-radius: 999px;
225
+ border: 1px solid var(--border);
226
+ background: var(--panel);
227
+ font-size: 12px;
228
+ color: var(--text-muted);
229
+ }
230
+
231
+ /* Cards */
232
+ .card {
233
+ background: var(--panel);
234
+ border: 1px solid var(--border);
235
+ border-radius: var(--radius);
236
+ padding: var(--pad);
237
+ box-shadow: 0 10px 30px var(--shadow);
238
+ }
239
+
240
+ /* Grid helper */
241
+ .grid {
242
+ display: grid;
243
+ gap: var(--pad);
244
+ }
245
+ .grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
246
+ .grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
247
+ @media (max-width: 1024px) {
248
+ .main { grid-template-columns: 1fr; }
249
+ .grid.two, .grid.three { grid-template-columns: 1fr; }
250
+ }
251
+
252
+ /* ====== Modal & Toasts ====== */
253
+ .modal-backdrop {
254
+ position: fixed; inset: 0; display: none;
255
+ background: rgba(0,0,0,0.5);
256
+ -webkit-backdrop-filter: blur(3px);
257
+ backdrop-filter: blur(3px);
258
+ z-index: 80;
259
+ align-items: center; justify-content: center;
260
+ padding: var(--pad);
261
+ }
262
+ .modal {
263
+ width: min(640px, 96vw);
264
+ background: var(--panel);
265
+ border: 1px solid var(--border);
266
+ border-radius: var(--radius);
267
+ box-shadow: 0 18px 60px var(--shadow);
268
+ overflow: hidden;
269
+ display: grid; grid-template-rows: auto 1fr auto;
270
+ }
271
+ .modal-header, .modal-footer {
272
+ padding: var(--pad);
273
+ border-bottom: 1px solid var(--border);
274
+ background: var(--panel-2);
275
+ }
276
+ .modal-footer { border-top: 1px solid var(--border); border-bottom: 0; display: flex; justify-content: flex-end; gap: 8px; }
277
+ .modal-body { padding: var(--pad); }
278
+
279
+ .toasts {
280
+ position: fixed; bottom: 16px; right: 16px; z-index: 90;
281
+ display: grid; gap: 10px; width: min(420px, 92vw);
282
+ }
283
+ .toast {
284
+ background: var(--panel);
285
+ border: 1px solid var(--border);
286
+ border-left: 6px solid var(--primary);
287
+ border-radius: var(--radius-sm);
288
+ padding: 10px 12px;
289
+ box-shadow: 0 12px 32px var(--shadow);
290
+ display: flex; align-items: start; gap: 10px;
291
+ }
292
+ .toast.success { border-left-color: var(--success); }
293
+ .toast.warning { border-left-color: var(--warning); }
294
+ .toast.danger { border-left-color: var(--danger); }
295
+
296
+ /* ====== Command Palette ====== */
297
+ .palette {
298
+ position: fixed; inset: 0; display: none; z-index: 85;
299
+ align-items: start; justify-content: center;
300
+ padding-top: 10vh;
301
+ background: rgba(0,0,0,0.35);
302
+ -webkit-backdrop-filter: blur(2px);
303
+ backdrop-filter: blur(2px);
304
+ }
305
+ .palette-panel {
306
+ width: min(720px, 96vw);
307
+ background: var(--panel);
308
+ border: 1px solid var(--border);
309
+ border-radius: var(--radius);
310
+ box-shadow: 0 18px 60px var(--shadow);
311
+ overflow: hidden;
312
+ display: grid; grid-template-rows: auto 1fr;
313
+ }
314
+ .palette input {
315
+ width: 100%; padding: 14px;
316
+ background: var(--panel-2);
317
+ color: var(--text);
318
+ border: 0; outline: none;
319
+ border-bottom: 1px solid var(--border);
320
+ font-size: 15px;
321
+ }
322
+ .palette-list { max-height: 50vh; overflow: auto; }
323
+ .palette-item {
324
+ padding: 12px 14px; cursor: pointer;
325
+ border-bottom: 1px solid var(--border);
326
+ }
327
+ .palette-item:hover { background: rgba(255,255,255,0.05); }
328
+
329
+ /* ====== Utility helpers ====== */
330
+ .hidden { display: none !important; }
331
+ .muted { color: var(--text-muted); }
332
+ .spacer { flex: 1; }
333
+ .kbd {
334
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
335
+ font-size: 12px;
336
+ padding: 2px 6px;
337
+ border: 1px solid var(--border);
338
+ border-bottom-width: 3px;
339
+ border-radius: 6px;
340
+ background: var(--panel-2);
341
+ color: var(--text);
342
+ }
343
+
344
+ /* ====== Utilities (to remove inline styles) ====== */
345
+ .mt0 { margin-top: 0; }
346
+ .mt-10 { margin-top: 10px; }
347
+ .row { display: flex; }
348
+ .items-center { align-items: center; }
349
+ .justify-center { justify-content: center; }
350
+ .gap-8 { gap: 8px; }
351
+ .gap-10 { gap: 10px; }
352
+ .w-100 { width: 100%; }
353
+ .flex-1 { flex: 1; }
354
+
355
+ .canvas-surface {
356
+ width: 100%;
357
+ border: 1px solid var(--border);
358
+ border-radius: var(--radius-sm);
359
+ background: var(--panel-2);
360
+ }
361
+ .preblock {
362
+ white-space: pre-wrap;
363
+ -webkit-user-select: text;
364
+ user-select: text;
365
+ background: var(--panel-2);
366
+ padding: 10px;
367
+ border-radius: var(--radius-sm);
368
+ border: 1px solid var(--border);
369
+ }
370
+ .three-mount {
371
+ height: 240px;
372
+ border: 1px dashed var(--border);
373
+ border-radius: var(--radius-sm);
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ color: var(--text-muted);
378
+ }
379
+ .textarea {
380
+ width: 100%;
381
+ min-height: 260px;
382
+ border-radius: var(--radius-sm);
383
+ border: 1px solid var(--border);
384
+ background: var(--panel-2);
385
+ color: var(--text);
386
+ padding: 10px;
387
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
388
+ }
389
+ /* Small element tweaks moved from inline styles */
390
+ .brand svg { color: var(--primary); }
391
+ .count-display { min-width: 64px; text-align: center; font-size: 24px; }
392
+ </style>
393
+ </head>
394
+
395
+ <body>
396
+ <!-- ====== App Shell (Header + Main) ====== -->
397
+ <div class="app" id="app" data-theme="dark">
398
+ <!-- Top bar: branding and quick actions -->
399
+ <header class="topbar">
400
+ <!-- App title badge -->
401
+ <div class="brand" role="img" aria-label="App brand">
402
+ <!-- Simple logo dot -->
403
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
404
+ <circle cx="12" cy="12" r="8" fill="currentColor"></circle>
405
+ </svg>
406
+ Micro-App Template
407
+ </div>
408
+
409
+ <!-- Topbar hint chips -->
410
+ <span class="pill">Press <span class="kbd">Ctrl</span> + <span class="kbd">K</span> for Commands</span>
411
+ <span class="pill">Press <span class="kbd">?</span> for Help</span>
412
+
413
+ <div class="spacer"></div>
414
+
415
+ <!-- Theme toggle and settings -->
416
+ <div class="topbar-actions">
417
+ <button id="themeToggle" class="btn" title="Toggle theme (dark/light)">
418
+ Toggle Theme
419
+ </button>
420
+ <button id="openSettings" class="btn" title="Open Settings">
421
+ Settings
422
+ </button>
423
+ <button id="showAbout" class="btn" title="About this micro-app">
424
+ About
425
+ </button>
426
+ </div>
427
+ </header>
428
+
429
+ <!-- Main area: sidebar navigation + content surface -->
430
+ <main class="main">
431
+ <!-- Sidebar: simple nav between views -->
432
+ <aside class="sidebar" aria-label="Sidebar Navigation">
433
+ <section class="nav-section">
434
+ <h3>Navigation</h3>
435
+ <div class="nav">
436
+ <button class="active" data-route="dashboard">Dashboard</button>
437
+ <button data-route="canvas-demo">Canvas Demo</button>
438
+ <button data-route="storage">Storage</button>
439
+ </div>
440
+ </section>
441
+
442
+ <section class="nav-section">
443
+ <h3>Actions</h3>
444
+ <div class="nav">
445
+ <button id="notifySuccess">Show Success Toast</button>
446
+ <button id="notifyWarning">Show Warning Toast</button>
447
+ <button id="notifyDanger">Show Danger Toast</button>
448
+ </div>
449
+ </section>
450
+
451
+ <section class="nav-section">
452
+ <h3>Shortcuts</h3>
453
+ <div class="card">
454
+ <div class="grid">
455
+ <div><span class="kbd">Ctrl</span> + <span class="kbd">K</span> — Command Palette</div>
456
+ <div><span class="kbd">T</span> — Toggle Theme</div>
457
+ <div><span class="kbd">S</span> — Open Settings</div>
458
+ <div><span class="kbd">?</span> — Help/About</div>
459
+ </div>
460
+ </div>
461
+ </section>
462
+ </aside>
463
+
464
+ <!-- Content: routed views appear here -->
465
+ <section class="content" aria-live="polite">
466
+ <div class="content-header">
467
+ <div id="viewTitle">Dashboard</div>
468
+ <div>
469
+ <button class="btn" id="exportState">Export State</button>
470
+ <button class="btn" id="importState">Import State</button>
471
+ </div>
472
+ </div>
473
+ <div class="content-body">
474
+ <!-- View: Dashboard -->
475
+ <div data-view="dashboard">
476
+ <div class="grid two">
477
+ <div class="card">
478
+ <h2 class="mt0">Welcome</h2>
479
+ <p class="muted">
480
+ This is your starting point. Use the sidebar to explore, the command palette for quick actions, and the settings modal to tweak behavior.
481
+ </p>
482
+ <ul>
483
+ <li>Single-file app shell (no build step).</li>
484
+ <li>Dark/light themes, keyboard shortcuts, toasts, settings.</li>
485
+ <li>State persisted to localStorage.</li>
486
+ <li>Canvas playground + optional Three.js (commented).</li>
487
+ </ul>
488
+ </div>
489
+
490
+ <div class="card">
491
+ <h3 class="mt0">Quick Demo: Counter Component</h3>
492
+ <p class="muted">A tiny stateful widget to show how to read/write from the store.</p>
493
+ <div class="row items-center gap-10">
494
+ <button class="btn" id="dec">−</button>
495
+ <div id="count" class="count-display">0</div>
496
+ <button class="btn" id="inc">+</button>
497
+ <button class="btn" id="reset">Reset</button>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ </div>
502
+
503
+ <!-- View: Canvas Demo (2D canvas animation) -->
504
+ <div class="hidden" data-view="canvas-demo">
505
+ <div class="grid two">
506
+ <div class="card">
507
+ <h3 class="mt0">Canvas Playground</h3>
508
+ <p class="muted">A simple bouncing ball animation on a 2D canvas. Great for quick visuals and prototyping.</p>
509
+ <canvas id="demoCanvas" width="640" height="360" class="canvas-surface"></canvas>
510
+ <div class="mt-10 row gap-8 items-center">
511
+ <button class="btn" id="canvasPause">Pause</button>
512
+ <button class="btn" id="canvasResume">Resume</button>
513
+ <span class="muted">Tip: Resize the window to see the app respond.</span>
514
+ </div>
515
+ </div>
516
+
517
+ <div class="card">
518
+ <h3 class="mt0">Three.js (Optional)</h3>
519
+ <p class="muted">
520
+ If you want 3D, un-comment the three.js CDN at the top and the initThreeDemo() call below.
521
+ </p>
522
+ <pre class="muted preblock">
523
+ <!-- Example bootstrap (uncomment to use)
524
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
525
+ <script>
526
+ function initThreeDemo(mountEl) {
527
+ const scene = new THREE.Scene();
528
+ scene.background = new THREE.Color(0x0e0f14);
529
+ const camera = new THREE.PerspectiveCamera(70, mountEl.clientWidth/mountEl.clientHeight, 0.1, 1000);
530
+ camera.position.set(0,1.4,3);
531
+ const renderer = new THREE.WebGLRenderer({antialias:true});
532
+ renderer.setSize(mountEl.clientWidth, mountEl.clientHeight);
533
+ mountEl.innerHTML = "";
534
+ mountEl.appendChild(renderer.domElement);
535
+
536
+ const light = new THREE.DirectionalLight(0xffffff, 1);
537
+ light.position.set(2,3,4);
538
+ scene.add(light);
539
+
540
+ const geo = new THREE.BoxGeometry(1,1,1);
541
+ const mat = new THREE.MeshPhongMaterial({color: 0x5b9cff});
542
+ const cube = new THREE.Mesh(geo, mat);
543
+ scene.add(cube);
544
+
545
+ function onResize(){
546
+ const {clientWidth:w, clientHeight:h} = mountEl;
547
+ renderer.setSize(w,h);
548
+ camera.aspect = w/h; camera.updateProjectionMatrix();
549
+ }
550
+ window.addEventListener('resize', onResize);
551
+ onResize();
552
+
553
+ (function loop(){
554
+ cube.rotation.y += 0.01;
555
+ renderer.render(scene, camera);
556
+ requestAnimationFrame(loop);
557
+ })();
558
+ }
559
+ </script>
560
+ -->
561
+ </pre>
562
+ <div id="threeMount" class="three-mount">
563
+ Three.js mount target
564
+ </div>
565
+ </div>
566
+ </div>
567
+ </div>
568
+
569
+ <!-- View: Storage (inspect and edit persisted state) -->
570
+ <div class="hidden" data-view="storage">
571
+ <div class="card">
572
+ <h3 class="mt0">App State (localStorage)</h3>
573
+ <p class="muted">Inspect and modify your persisted state. Changes save automatically.</p>
574
+ <textarea id="stateEditor" class="textarea"></textarea>
575
+ <div class="row gap-8 mt-10">
576
+ <button class="btn primary" id="applyState">Apply State</button>
577
+ <button class="btn" id="reloadState">Reload</button>
578
+ </div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </section>
583
+ </main>
584
+ </div>
585
+
586
+ <!-- ====== Settings Modal ====== -->
587
+ <div class="modal-backdrop" id="settingsModal" role="dialog" aria-modal="true" aria-label="Settings">
588
+ <div class="modal">
589
+ <div class="modal-header">
590
+ <strong>Settings</strong>
591
+ </div>
592
+ <div class="modal-body">
593
+ <div class="grid two">
594
+ <div class="card">
595
+ <h4 class="mt0">Theme</h4>
596
+ <p class="muted">Choose dark or light theme.</p>
597
+ <div class="row gap-8">
598
+ <button class="btn" data-theme-choice="dark">Dark</button>
599
+ <button class="btn" data-theme-choice="light">Light</button>
600
+ </div>
601
+ </div>
602
+
603
+ <div class="card">
604
+ <h4 class="mt0">App Options</h4>
605
+ <label class="row items-center gap-8">
606
+ <input type="checkbox" id="optAnimations" />
607
+ Enable subtle animations
608
+ </label>
609
+ <label class="row items-center gap-8 mt-10">
610
+ <input type="checkbox" id="optHints" />
611
+ Show helper hints
612
+ </label>
613
+ </div>
614
+ </div>
615
+ </div>
616
+ <div class="modal-footer">
617
+ <button class="btn" id="closeSettings">Close</button>
618
+ <button class="btn primary" id="saveSettings">Save</button>
619
+ </div>
620
+ </div>
621
+ </div>
622
+
623
+ <!-- ====== Command Palette ====== -->
624
+ <div class="palette" id="palette" role="dialog" aria-modal="true" aria-label="Command Palette">
625
+ <div class="palette-panel">
626
+ <input id="paletteInput" placeholder="Type a command… (e.g., 'Go: Dashboard', 'Theme: Light')" />
627
+ <div class="palette-list" id="paletteList" role="listbox" aria-label="Command Results"></div>
628
+ </div>
629
+ </div>
630
+
631
+ <!-- ====== Toast Container (notifications) ====== -->
632
+ <div class="toasts" id="toasts" aria-live="polite" aria-atomic="true"></div>
633
+
634
+ <script>
635
+ // ====== Tiny helper utilities (in layman's terms) ======
636
+ // qs/qsa: quick ways to grab elements on the page
637
+ const qs = (sel, el=document) => el.querySelector(sel);
638
+ const qsa = (sel, el=document) => [...el.querySelectorAll(sel)];
639
+ // on: attach an event listener in a short way
640
+ const on = (el, ev, fn, opts) => el.addEventListener(ev, fn, opts);
641
+ // clamp: keep a number between min and max
642
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
643
+
644
+ // ====== App namespace to avoid global clutter ======
645
+ window.App = {
646
+ version: '1.0.0',
647
+ storageKey: 'microAppState.v1',
648
+ state: {
649
+ // Default values shown the very first time the app loads
650
+ theme: 'dark',
651
+ options: { animations: true, hints: true },
652
+ counter: 0
653
+ },
654
+ routes: [
655
+ { id: 'dashboard', title: 'Dashboard' },
656
+ { id: 'canvas-demo', title: 'Canvas Demo' },
657
+ { id: 'storage', title: 'Storage' }
658
+ ],
659
+ // List of commands shown in the command palette
660
+ commands: [
661
+ { label: 'Go: Dashboard', run: () => App.navigate('dashboard') },
662
+ { label: 'Go: Canvas Demo', run: () => App.navigate('canvas-demo') },
663
+ { label: 'Go: Storage', run: () => App.navigate('storage') },
664
+ { label: 'Theme: Dark', run: () => App.setTheme('dark') },
665
+ { label: 'Theme: Light', run: () => App.setTheme('light') },
666
+ { label: 'Open: Settings', run: () => App.openSettings() },
667
+ { label: 'About: Show', run: () => App.showAbout() },
668
+ ],
669
+ };
670
+
671
+ // ====== Persistence layer (saves and loads from localStorage) ======
672
+ App.load = function load() {
673
+ // Try to load existing state; if none found, use defaults above
674
+ try {
675
+ const raw = localStorage.getItem(App.storageKey);
676
+ if (raw) {
677
+ const parsed = JSON.parse(raw);
678
+ // Merge to keep backward compatibility if you add new fields later
679
+ App.state = Object.assign({}, App.state, parsed);
680
+ }
681
+ } catch (e) {
682
+ console.warn('State load failed, using defaults', e);
683
+ }
684
+ };
685
+
686
+ App.save = function save() {
687
+ // Save current state to localStorage so it persists across refreshes
688
+ try {
689
+ localStorage.setItem(App.storageKey, JSON.stringify(App.state));
690
+ } catch (e) {
691
+ console.warn('State save failed', e);
692
+ App.toast('Saving to localStorage failed.', 'danger');
693
+ }
694
+ };
695
+
696
+ // ====== Routing (switches which "view" is visible) ======
697
+ App.navigate = function navigate(id) {
698
+ // Find the corresponding route metadata to update title
699
+ const route = App.routes.find(r => r.id === id) || App.routes[0];
700
+ // Show matching view, hide others
701
+ qsa('[data-view]').forEach(v => v.classList.toggle('hidden', v.getAttribute('data-view') !== route.id));
702
+ // Update active button styles in the sidebar
703
+ qsa('.nav button').forEach(b => b.classList.toggle('active', b.getAttribute('data-route') === route.id));
704
+ // Set content header title
705
+ qs('#viewTitle').textContent = route.title;
706
+ // Update URL hash for basic deep-linking (optional)
707
+ location.hash = route.id;
708
+
709
+ // If we entered the canvas view, ensure canvas fits / resumes
710
+ if (route.id === 'canvas-demo') {
711
+ App.canvas && App.canvas.onResize();
712
+ }
713
+ };
714
+
715
+ // ====== Theme handling (dark/light) ======
716
+ App.setTheme = function setTheme(theme) {
717
+ // Update the data attribute; CSS variables do the rest
718
+ const root = qs('#app');
719
+ root.setAttribute('data-theme', theme);
720
+ App.state.theme = theme;
721
+ App.save();
722
+ };
723
+
724
+ // ====== Toast notifications (little popups for feedback) ======
725
+ App.toast = function toast(message, kind='info', timeout=2600) {
726
+ const wrap = qs('#toasts');
727
+ const el = document.createElement('div');
728
+ el.className = `toast ${kind}`;
729
+ el.innerHTML = `<div class="flex-1"></div><button class="btn" aria-label="Dismiss">Dismiss</button>`;
730
+ el.querySelector('.flex-1').textContent = message;
731
+ wrap.appendChild(el);
732
+ const remove = () => el.remove();
733
+ on(el.querySelector('button'), 'click', remove);
734
+ setTimeout(remove, timeout);
735
+ };
736
+
737
+ // ====== Modal controls (for Settings) ======
738
+ App.openSettings = function openSettings() {
739
+ // Fill current values into the modal inputs
740
+ qs('#optAnimations').checked = !!App.state.options.animations;
741
+ qs('#optHints').checked = !!App.state.options.hints;
742
+ qs('#settingsModal').style.display = 'flex';
743
+ };
744
+ App.closeSettings = function closeSettings() {
745
+ qs('#settingsModal').style.display = 'none';
746
+ };
747
+ App.saveSettings = function saveSettings() {
748
+ // Grab values from the modal and store them
749
+ App.state.options.animations = !!qs('#optAnimations').checked;
750
+ App.state.options.hints = !!qs('#optHints').checked;
751
+ App.save();
752
+ App.closeSettings();
753
+ App.toast('Settings saved.', 'success');
754
+ };
755
+
756
+ // ====== About helper (simple alert for now; could be a modal) ======
757
+ App.showAbout = function showAbout() {
758
+ const about = `
759
+ Micro-App Template v${App.version}
760
+ – Single-file, self-contained HTML app shell
761
+ – Dark/Light theme, keyboard shortcuts, toasts
762
+ – LocalStorage persistence
763
+ – Canvas playground + optional Three.js
764
+ `;
765
+ alert(about);
766
+ };
767
+
768
+ // ====== Command Palette (Ctrl/Cmd + K) ======
769
+ App.openPalette = function openPalette() {
770
+ const pal = qs('#palette');
771
+ const input = qs('#paletteInput');
772
+ const list = qs('#paletteList');
773
+ // Populate the list with all commands initially
774
+ list.innerHTML = '';
775
+ App.commands.forEach((cmd, i) => {
776
+ const item = document.createElement('div');
777
+ item.className = 'palette-item';
778
+ item.textContent = cmd.label;
779
+ item.setAttribute('role', 'option');
780
+ item.dataset.index = i;
781
+ list.appendChild(item);
782
+ });
783
+ pal.style.display = 'flex';
784
+ input.value = '';
785
+ input.focus();
786
+ };
787
+ App.closePalette = function closePalette() {
788
+ qs('#palette').style.display = 'none';
789
+ };
790
+
791
+ // Filters palette items live based on input text
792
+ App.filterPalette = function filterPalette(text) {
793
+ const items = qsa('.palette-item', qs('#paletteList'));
794
+ const t = text.trim().toLowerCase();
795
+ items.forEach(item => {
796
+ const match = item.textContent.toLowerCase().includes(t);
797
+ item.style.display = match ? '' : 'none';
798
+ });
799
+ };
800
+
801
+ // Executes a selected command
802
+ App.runPaletteItem = function runPaletteItem(index) {
803
+ const cmd = App.commands[index];
804
+ if (cmd) {
805
+ cmd.run();
806
+ App.closePalette();
807
+ }
808
+ };
809
+
810
+ // ====== Example: Small counter component ======
811
+ App.counter = {
812
+ init() {
813
+ // Show current value
814
+ qs('#count').textContent = App.state.counter;
815
+ // Wire buttons
816
+ on(qs('#inc'), 'click', () => {
817
+ App.state.counter = clamp((App.state.counter || 0) + 1, -9999, 9999);
818
+ qs('#count').textContent = App.state.counter;
819
+ App.save();
820
+ });
821
+ on(qs('#dec'), 'click', () => {
822
+ App.state.counter = clamp((App.state.counter || 0) - 1, -9999, 9999);
823
+ qs('#count').textContent = App.state.counter;
824
+ App.save();
825
+ });
826
+ on(qs('#reset'), 'click', () => {
827
+ App.state.counter = 0;
828
+ qs('#count').textContent = App.state.counter;
829
+ App.save();
830
+ });
831
+ }
832
+ };
833
+
834
+ // ====== Canvas demo (simple 2D bouncing ball) ======
835
+ App.canvas = (function(){
836
+ // Private variables for the animation
837
+ let ctx, canvas, raf, running = true;
838
+ let x = 80, y = 60, vx = 2.6, vy = 2.1, r = 14;
839
+
840
+ function draw() {
841
+ if (!ctx) return;
842
+ // Clear the canvas
843
+ ctx.clearRect(0,0,canvas.width, canvas.height);
844
+ // Draw the ball
845
+ ctx.beginPath();
846
+ ctx.arc(x, y, r, 0, Math.PI*2);
847
+ ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary') || '#5b9cff';
848
+ ctx.fill();
849
+ // Update position and bounce on edges
850
+ x += vx; y += vy;
851
+ if (x - r < 0 || x + r > canvas.width) vx *= -1;
852
+ if (y - r < 0 || y + r > canvas.height) vy *= -1;
853
+ // Continue loop if running
854
+ if (running) raf = requestAnimationFrame(draw);
855
+ }
856
+
857
+ function start() { if (!running) { running = true; draw(); } }
858
+ function stop() { running = false; if (raf) cancelAnimationFrame(raf); }
859
+
860
+ function onResize() {
861
+ if (!canvas) return;
862
+ // Keep a 16:9 feel while filling container width
863
+ const rect = canvas.getBoundingClientRect();
864
+ canvas.width = Math.floor(rect.width * devicePixelRatio);
865
+ canvas.height = Math.floor(rect.width * 9/16 * devicePixelRatio);
866
+ canvas.style.height = `${Math.floor(rect.width * 9/16)}px`;
867
+ }
868
+
869
+ function init() {
870
+ canvas = qs('#demoCanvas');
871
+ if (!canvas) return;
872
+ ctx = canvas.getContext('2d');
873
+ on(window, 'resize', onResize);
874
+ onResize();
875
+ draw();
876
+ on(qs('#canvasPause'), 'click', stop);
877
+ on(qs('#canvasResume'), 'click', start);
878
+ }
879
+
880
+ return { init, onResize, start, stop };
881
+ })();
882
+
883
+ // ====== Import/Export State helpers ======
884
+ App.exportState = function exportState() {
885
+ // Turn current state into a downloadable JSON file
886
+ const data = new Blob([JSON.stringify(App.state, null, 2)], { type: 'application/json' });
887
+ const url = URL.createObjectURL(data);
888
+ const a = document.createElement('a');
889
+ a.href = url;
890
+ a.download = 'micro-app-state.json';
891
+ a.click();
892
+ URL.revokeObjectURL(url);
893
+ App.toast('State exported.', 'success');
894
+ };
895
+
896
+ App.importState = function importState() {
897
+ // Let the user pick a JSON file and merge it into state
898
+ const inp = document.createElement('input');
899
+ inp.type = 'file';
900
+ inp.accept = 'application/json';
901
+ on(inp, 'change', () => {
902
+ const file = inp.files && inp.files[0];
903
+ if (!file) return;
904
+ const reader = new FileReader();
905
+ reader.onload = () => {
906
+ try {
907
+ const obj = JSON.parse(String(reader.result || '{}'));
908
+ App.state = Object.assign({}, App.state, obj);
909
+ App.save();
910
+ App.applyStateToUI();
911
+ App.toast('State imported.', 'success');
912
+ } catch (e) {
913
+ App.toast('Invalid JSON.', 'danger');
914
+ }
915
+ };
916
+ reader.readAsText(file);
917
+ });
918
+ inp.click();
919
+ };
920
+
921
+ // ====== Apply current state to visible UI ======
922
+ App.applyStateToUI = function applyStateToUI() {
923
+ // Theme
924
+ App.setTheme(App.state.theme || 'dark');
925
+ // Counter
926
+ const countEl = qs('#count');
927
+ if (countEl) countEl.textContent = App.state.counter || 0;
928
+ // Storage editor
929
+ const ed = qs('#stateEditor');
930
+ if (ed) ed.value = JSON.stringify(App.state, null, 2);
931
+ };
932
+
933
+ // ====== Bootstrapping the app (runs once at load) ======
934
+ (function init() {
935
+ // 1) Load saved state (if any)
936
+ App.load();
937
+
938
+ // 2) Wire topbar buttons
939
+ on(qs('#themeToggle'), 'click', () => App.setTheme(App.state.theme === 'dark' ? 'light' : 'dark'));
940
+ on(qs('#openSettings'), 'click', App.openSettings);
941
+ on(qs('#showAbout'), 'click', App.showAbout);
942
+
943
+ // 3) Wire sidebar navigation
944
+ qsa('.nav button').forEach(btn => {
945
+ on(btn, 'click', () => App.navigate(btn.getAttribute('data-route')));
946
+ });
947
+
948
+ // 4) Wire toasts for demonstration
949
+ on(qs('#notifySuccess'), 'click', () => App.toast('Operation completed successfully.', 'success'));
950
+ on(qs('#notifyWarning'), 'click', () => App.toast('Heads up! Check your inputs.', 'warning'));
951
+ on(qs('#notifyDanger'), 'click', () => App.toast('Something went wrong.', 'danger'));
952
+
953
+ // 5) Settings modal
954
+ on(qs('#closeSettings'), 'click', App.closeSettings);
955
+ on(qs('#saveSettings'), 'click', App.saveSettings);
956
+ qsa('[data-theme-choice]').forEach(b => on(b, 'click', () => App.setTheme(b.dataset.themeChoice)));
957
+
958
+ // 6) Command palette events
959
+ on(document, 'keydown', (e) => {
960
+ // Ctrl/Cmd + K to open
961
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
962
+ e.preventDefault();
963
+ App.openPalette();
964
+ }
965
+ // '?' to open About
966
+ if (!e.ctrlKey && !e.metaKey && e.key === '?') {
967
+ e.preventDefault();
968
+ App.showAbout();
969
+ }
970
+ // 't' to toggle theme
971
+ if (!e.ctrlKey && !e.metaKey && (e.key === 't' || e.key === 'T')) {
972
+ e.preventDefault();
973
+ App.setTheme(App.state.theme === 'dark' ? 'light' : 'dark');
974
+ }
975
+ // 's' to open settings
976
+ if (!e.ctrlKey && !e.metaKey && (e.key === 's' || e.key === 'S')) {
977
+ e.preventDefault();
978
+ App.openSettings();
979
+ }
980
+ });
981
+ on(qs('#paletteInput'), 'input', (e) => App.filterPalette(e.target.value));
982
+ on(qs('#paletteList'), 'click', (e) => {
983
+ const item = e.target.closest('.palette-item');
984
+ if (!item) return;
985
+ App.runPaletteItem(Number(item.dataset.index));
986
+ });
987
+ on(qs('#palette'), 'click', (e) => {
988
+ if (e.target.id === 'palette') App.closePalette();
989
+ });
990
+ on(document, 'keydown', (e) => {
991
+ if (e.key === 'Escape') {
992
+ App.closePalette();
993
+ App.closeSettings();
994
+ }
995
+ });
996
+
997
+ // 7) Counter demo
998
+ App.counter.init();
999
+
1000
+ // 8) Canvas demo
1001
+ App.canvas.init();
1002
+
1003
+ // 9) Storage view controls
1004
+ on(qs('#reloadState'), 'click', () => {
1005
+ App.load();
1006
+ App.applyStateToUI();
1007
+ App.toast('State reloaded.', 'success');
1008
+ });
1009
+ on(qs('#applyState'), 'click', () => {
1010
+ try {
1011
+ const next = JSON.parse(qs('#stateEditor').value || '{}');
1012
+ App.state = Object.assign({}, App.state, next);
1013
+ App.save();
1014
+ App.applyStateToUI();
1015
+ App.toast('State applied.', 'success');
1016
+ } catch (e) {
1017
+ App.toast('Invalid JSON.', 'danger');
1018
+ }
1019
+ });
1020
+ on(qs('#exportState'), 'click', App.exportState);
1021
+ on(qs('#importState'), 'click', App.importState);
1022
+
1023
+ // 10) Start on hash route or default
1024
+ const startRoute = (location.hash || '').replace('#','') || 'dashboard';
1025
+ App.navigate(startRoute);
1026
+
1027
+ // 11) Apply state to UI once everything is wired
1028
+ App.applyStateToUI();
1029
+
1030
+ // 12) If you un-comment the three.js CDN, you can also un-comment this:
1031
+ // initThreeDemo(qs('#threeMount'));
1032
+ })();
1033
+ </script>
1034
+ </body>
1035
  </html>