@toonstore/torm 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,663 @@
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.0">
6
+ <title>TORM Studio - ToonStore Database Manager</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
16
+ background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
17
+ color: #c9d1d9;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .header {
22
+ background: rgba(22, 27, 34, 0.95);
23
+ backdrop-filter: blur(10px);
24
+ border-bottom: 1px solid #30363d;
25
+ padding: 1rem 2rem;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
30
+ animation: slideDown 0.5s ease-out;
31
+ }
32
+
33
+ @keyframes slideDown {
34
+ from {
35
+ transform: translateY(-100%);
36
+ opacity: 0;
37
+ }
38
+ to {
39
+ transform: translateY(0);
40
+ opacity: 1;
41
+ }
42
+ }
43
+
44
+ .header h1 {
45
+ font-size: 1.5rem;
46
+ color: #58a6ff;
47
+ }
48
+
49
+ .stats {
50
+ display: flex;
51
+ gap: 2rem;
52
+ font-size: 0.9rem;
53
+ }
54
+
55
+ .stat-item {
56
+ display: flex;
57
+ flex-direction: column;
58
+ }
59
+
60
+ .stat-label {
61
+ color: #8b949e;
62
+ font-size: 0.75rem;
63
+ }
64
+
65
+ .stat-value {
66
+ color: #58a6ff;
67
+ font-weight: 600;
68
+ text-shadow: 0 0 10px rgba(88, 166, 255, 0.5);
69
+ transition: all 0.3s ease;
70
+ }
71
+
72
+ .stat-item:hover .stat-value {
73
+ transform: scale(1.1);
74
+ text-shadow: 0 0 20px rgba(88, 166, 255, 0.8);
75
+ }
76
+
77
+ .container {
78
+ display: flex;
79
+ height: calc(100vh - 60px);
80
+ }
81
+
82
+ .sidebar {
83
+ width: 250px;
84
+ background: rgba(22, 27, 34, 0.95);
85
+ backdrop-filter: blur(10px);
86
+ border-right: 1px solid #30363d;
87
+ padding: 1rem;
88
+ overflow-y: auto;
89
+ animation: slideInLeft 0.5s ease-out;
90
+ }
91
+
92
+ @keyframes slideInLeft {
93
+ from {
94
+ transform: translateX(-100%);
95
+ opacity: 0;
96
+ }
97
+ to {
98
+ transform: translateX(0);
99
+ opacity: 1;
100
+ }
101
+ }
102
+
103
+ .sidebar h3 {
104
+ font-size: 0.875rem;
105
+ color: #8b949e;
106
+ text-transform: uppercase;
107
+ margin-bottom: 1rem;
108
+ }
109
+
110
+ .collection-list {
111
+ list-style: none;
112
+ }
113
+
114
+ .collection-item {
115
+ padding: 0.5rem;
116
+ margin-bottom: 0.25rem;
117
+ border-radius: 6px;
118
+ cursor: pointer;
119
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
120
+ position: relative;
121
+ overflow: hidden;
122
+ }
123
+
124
+ .collection-item::before {
125
+ content: '';
126
+ position: absolute;
127
+ left: 0;
128
+ top: 0;
129
+ height: 100%;
130
+ width: 3px;
131
+ background: #58a6ff;
132
+ transform: scaleY(0);
133
+ transition: transform 0.3s ease;
134
+ }
135
+
136
+ .collection-item:hover {
137
+ background: #21262d;
138
+ transform: translateX(5px);
139
+ box-shadow: 0 2px 8px rgba(88, 166, 255, 0.2);
140
+ }
141
+
142
+ .collection-item:hover::before {
143
+ transform: scaleY(1);
144
+ }
145
+
146
+ .collection-item.active {
147
+ background: linear-gradient(90deg, #1f6feb 0%, #1a56db 100%);
148
+ color: white;
149
+ transform: translateX(5px);
150
+ box-shadow: 0 4px 12px rgba(31, 111, 235, 0.4);
151
+ }
152
+
153
+ .collection-item.active::before {
154
+ transform: scaleY(1);
155
+ }
156
+
157
+ .main-content {
158
+ flex: 1;
159
+ padding: 2rem;
160
+ overflow-y: auto;
161
+ animation: fadeIn 0.6s ease-out;
162
+ }
163
+
164
+ @keyframes fadeIn {
165
+ from {
166
+ opacity: 0;
167
+ transform: translateY(20px);
168
+ }
169
+ to {
170
+ opacity: 1;
171
+ transform: translateY(0);
172
+ }
173
+ }
174
+
175
+ .toolbar {
176
+ display: flex;
177
+ gap: 1rem;
178
+ margin-bottom: 1.5rem;
179
+ align-items: center;
180
+ }
181
+
182
+ input[type="text"] {
183
+ flex: 1;
184
+ padding: 0.5rem;
185
+ background: #0d1117;
186
+ border: 1px solid #30363d;
187
+ border-radius: 6px;
188
+ color: #c9d1d9;
189
+ }
190
+
191
+ button {
192
+ padding: 0.5rem 1rem;
193
+ background: #238636;
194
+ border: none;
195
+ border-radius: 6px;
196
+ color: white;
197
+ cursor: pointer;
198
+ font-weight: 600;
199
+ transition: background 0.2s;
200
+ }
201
+
202
+ button:hover {
203
+ background: #2ea043;
204
+ }
205
+
206
+ button.secondary {
207
+ background: #21262d;
208
+ }
209
+
210
+ button.secondary:hover {
211
+ background: #30363d;
212
+ }
213
+
214
+ button.danger {
215
+ background: #da3633;
216
+ }
217
+
218
+ button.danger:hover {
219
+ background: #f85149;
220
+ }
221
+
222
+ .data-table {
223
+ background: #161b22;
224
+ border: 1px solid #30363d;
225
+ border-radius: 6px;
226
+ overflow: hidden;
227
+ }
228
+
229
+ .table-row {
230
+ display: flex;
231
+ padding: 1rem;
232
+ border-bottom: 1px solid #30363d;
233
+ align-items: center;
234
+ transition: background 0.2s;
235
+ }
236
+
237
+ .table-row:hover {
238
+ background: #21262d;
239
+ }
240
+
241
+ .table-row:last-child {
242
+ border-bottom: none;
243
+ }
244
+
245
+ .table-header {
246
+ background: #0d1117;
247
+ font-weight: 600;
248
+ }
249
+
250
+ .table-cell {
251
+ flex: 1;
252
+ padding: 0 0.5rem;
253
+ }
254
+
255
+ .table-cell.key {
256
+ width: 30%;
257
+ font-family: 'Monaco', 'Menlo', monospace;
258
+ color: #58a6ff;
259
+ }
260
+
261
+ .table-cell.value {
262
+ width: 50%;
263
+ font-family: 'Monaco', 'Menlo', monospace;
264
+ font-size: 0.875rem;
265
+ }
266
+
267
+ .table-cell.actions {
268
+ width: 20%;
269
+ display: flex;
270
+ gap: 0.5rem;
271
+ justify-content: flex-end;
272
+ }
273
+
274
+ .btn-small {
275
+ padding: 0.25rem 0.75rem;
276
+ font-size: 0.75rem;
277
+ }
278
+
279
+ .modal {
280
+ display: none;
281
+ position: fixed;
282
+ top: 0;
283
+ left: 0;
284
+ width: 100%;
285
+ height: 100%;
286
+ background: rgba(0, 0, 0, 0.8);
287
+ align-items: center;
288
+ justify-content: center;
289
+ z-index: 1000;
290
+ backdrop-filter: blur(5px);
291
+ }
292
+
293
+ .modal.active {
294
+ display: flex;
295
+ animation: fadeInModal 0.3s ease-out;
296
+ }
297
+
298
+ @keyframes fadeInModal {
299
+ from {
300
+ opacity: 0;
301
+ }
302
+ to {
303
+ opacity: 1;
304
+ }
305
+ }
306
+
307
+ .modal-content {
308
+ background: #161b22;
309
+ border: 1px solid #30363d;
310
+ border-radius: 12px;
311
+ padding: 2rem;
312
+ max-width: 600px;
313
+ width: 90%;
314
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
315
+ animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
316
+ }
317
+
318
+ @keyframes scaleIn {
319
+ from {
320
+ transform: scale(0.9);
321
+ opacity: 0;
322
+ }
323
+ to {
324
+ transform: scale(1);
325
+ opacity: 1;
326
+ }
327
+ }
328
+
329
+ .modal-header {
330
+ display: flex;
331
+ justify-content: space-between;
332
+ align-items: center;
333
+ margin-bottom: 1.5rem;
334
+ }
335
+
336
+ .modal-header h2 {
337
+ font-size: 1.25rem;
338
+ }
339
+
340
+ .close-btn {
341
+ background: none;
342
+ border: none;
343
+ color: #8b949e;
344
+ font-size: 1.5rem;
345
+ cursor: pointer;
346
+ padding: 0;
347
+ }
348
+
349
+ .form-group {
350
+ margin-bottom: 1rem;
351
+ }
352
+
353
+ .form-group label {
354
+ display: block;
355
+ margin-bottom: 0.5rem;
356
+ color: #8b949e;
357
+ font-size: 0.875rem;
358
+ }
359
+
360
+ textarea {
361
+ width: 100%;
362
+ min-height: 150px;
363
+ padding: 0.5rem;
364
+ background: #0d1117;
365
+ border: 1px solid #30363d;
366
+ border-radius: 6px;
367
+ color: #c9d1d9;
368
+ font-family: 'Monaco', 'Menlo', monospace;
369
+ resize: vertical;
370
+ transition: all 0.3s ease;
371
+ }
372
+
373
+ textarea:focus {
374
+ outline: none;
375
+ border-color: #58a6ff;
376
+ box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.2);
377
+ }
378
+
379
+ .empty-state {
380
+ text-align: center;
381
+ padding: 4rem 2rem;
382
+ color: #8b949e;
383
+ }
384
+
385
+ .empty-state h3 {
386
+ font-size: 1.25rem;
387
+ margin-bottom: 0.5rem;
388
+ }
389
+
390
+ @keyframes spin {
391
+ to { transform: rotate(360deg); }
392
+ }
393
+
394
+ .loading {
395
+ display: inline-block;
396
+ width: 16px;
397
+ height: 16px;
398
+ border: 2px solid #30363d;
399
+ border-top-color: #58a6ff;
400
+ border-radius: 50%;
401
+ animation: spin 0.6s linear infinite;
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div class="header">
407
+ <h1>🎨 TORM Studio</h1>
408
+ <div class="stats">
409
+ <div class="stat-item">
410
+ <span class="stat-label">Total Keys</span>
411
+ <span class="stat-value" id="totalKeys">0</span>
412
+ </div>
413
+ <div class="stat-item">
414
+ <span class="stat-label">Collections</span>
415
+ <span class="stat-value" id="totalCollections">0</span>
416
+ </div>
417
+ </div>
418
+ </div>
419
+
420
+ <div class="container">
421
+ <div class="sidebar">
422
+ <h3>Collections</h3>
423
+ <ul class="collection-list" id="collectionList"></ul>
424
+ </div>
425
+
426
+ <div class="main-content">
427
+ <div class="toolbar">
428
+ <input type="text" id="searchInput" placeholder="Search keys..." />
429
+ <button id="refreshBtn" class="secondary">🔄 Refresh</button>
430
+ <button id="createBtn">+ Create</button>
431
+ </div>
432
+
433
+ <div id="dataContainer"></div>
434
+ </div>
435
+ </div>
436
+
437
+ <!-- Edit Modal -->
438
+ <div class="modal" id="editModal">
439
+ <div class="modal-content">
440
+ <div class="modal-header">
441
+ <h2 id="modalTitle">Edit Key</h2>
442
+ <button class="close-btn" onclick="closeModal()">&times;</button>
443
+ </div>
444
+ <div class="form-group">
445
+ <label>Key</label>
446
+ <input type="text" id="modalKey" />
447
+ </div>
448
+ <div class="form-group">
449
+ <label>Value (JSON)</label>
450
+ <textarea id="modalValue"></textarea>
451
+ </div>
452
+ <div style="display: flex; gap: 1rem; justify-content: flex-end;">
453
+ <button class="secondary" onclick="closeModal()">Cancel</button>
454
+ <button id="saveBtn">Save</button>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
459
+ <script>
460
+ let currentCollection = null;
461
+ let allKeys = [];
462
+
463
+ // Load collections on startup
464
+ async function loadCollections() {
465
+ try {
466
+ const response = await fetch('/api/keys');
467
+ const data = await response.json();
468
+ const list = document.getElementById('collectionList');
469
+ list.innerHTML = '';
470
+
471
+ data.collections.forEach(collection => {
472
+ const li = document.createElement('li');
473
+ li.className = 'collection-item';
474
+ li.textContent = `${collection.name} (${collection.count})`;
475
+ li.addEventListener('click', function() {
476
+ loadCollection(collection.name);
477
+ });
478
+ list.appendChild(li);
479
+ });
480
+
481
+ document.getElementById('totalCollections').textContent = data.collections.length;
482
+ } catch (error) {
483
+ console.error('Failed to load collections:', error);
484
+ }
485
+ }
486
+
487
+ // Load collection data
488
+ async function loadCollection(collection) {
489
+ console.log('Loading collection:', collection);
490
+ currentCollection = collection;
491
+
492
+ // Update active state
493
+ document.querySelectorAll('.collection-item').forEach(el => el.classList.remove('active'));
494
+
495
+ try {
496
+ const response = await fetch(`/api/collection/${collection}`);
497
+ console.log('API Response status:', response.status);
498
+
499
+ const data = await response.json();
500
+ console.log('Received data:', data);
501
+
502
+ // Transform documents to the format expected by renderData
503
+ allKeys = data.documents.map(doc => ({
504
+ key: `toonstore:${collection}:${doc._id}`,
505
+ value: doc
506
+ }));
507
+
508
+ console.log('Transformed data:', allKeys);
509
+ renderData(allKeys);
510
+ } catch (error) {
511
+ console.error('Failed to load collection data:', error);
512
+ document.getElementById('dataContainer').innerHTML = `
513
+ <div class="empty-state">
514
+ <h3>Error loading data</h3>
515
+ <p>${error.message}</p>
516
+ </div>
517
+ `;
518
+ }
519
+ }
520
+
521
+ // Render data table
522
+ function renderData(data) {
523
+ console.log('renderData called with:', data);
524
+ const container = document.getElementById('dataContainer');
525
+
526
+ if (!data || data.length === 0) {
527
+ console.log('No data to render');
528
+ container.innerHTML = `
529
+ <div class="empty-state">
530
+ <h3>No data found</h3>
531
+ <p>Create your first key to get started</p>
532
+ </div>
533
+ `;
534
+ return;
535
+ }
536
+
537
+ console.log('Rendering', data.length, 'items');
538
+
539
+ const rows = data.map(item => {
540
+ const valueStr = JSON.stringify(item.value).substring(0, 100);
541
+ const itemJson = JSON.stringify(item).replace(/'/g, "\\'");
542
+ return `
543
+ <div class="table-row">
544
+ <div class="table-cell key">${item.key}</div>
545
+ <div class="table-cell value">${valueStr}...</div>
546
+ <div class="table-cell actions">
547
+ <button class="btn-small secondary" onclick='editKey(${itemJson})'>Edit</button>
548
+ <button class="btn-small danger" onclick='deleteKey("${item.key}")'>Delete</button>
549
+ </div>
550
+ </div>
551
+ `;
552
+ }).join('');
553
+
554
+ container.innerHTML = `
555
+ <div class="data-table">
556
+ <div class="table-row table-header">
557
+ <div class="table-cell key">Key</div>
558
+ <div class="table-cell value">Value</div>
559
+ <div class="table-cell actions">Actions</div>
560
+ </div>
561
+ ${rows}
562
+ </div>
563
+ `;
564
+
565
+ console.log('Render complete');
566
+ }
567
+
568
+ // Load stats
569
+ async function loadStats() {
570
+ try {
571
+ const response = await fetch('/api/stats');
572
+ const data = await response.json();
573
+ document.getElementById('totalKeys').textContent = data.total_keys;
574
+ } catch (error) {
575
+ console.error('Failed to load stats:', error);
576
+ }
577
+ }
578
+
579
+ // Edit key
580
+ function editKey(item) {
581
+ document.getElementById('modalTitle').textContent = 'Edit Key';
582
+ document.getElementById('modalKey').value = item.key;
583
+ document.getElementById('modalKey').disabled = true;
584
+ document.getElementById('modalValue').value = JSON.stringify(item.value, null, 2);
585
+ document.getElementById('editModal').classList.add('active');
586
+
587
+ document.getElementById('saveBtn').onclick = async () => {
588
+ const key = document.getElementById('modalKey').value;
589
+ const value = JSON.parse(document.getElementById('modalValue').value);
590
+
591
+ await fetch(`/api/key/${encodeURIComponent(key)}`, {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/json' },
594
+ body: JSON.stringify({ value })
595
+ });
596
+
597
+ closeModal();
598
+ if (currentCollection) loadCollection(currentCollection);
599
+ };
600
+ }
601
+
602
+ // Create key
603
+ document.getElementById('createBtn').onclick = () => {
604
+ document.getElementById('modalTitle').textContent = 'Create Key';
605
+ document.getElementById('modalKey').value = '';
606
+ document.getElementById('modalKey').disabled = false;
607
+ document.getElementById('modalValue').value = '{}';
608
+ document.getElementById('editModal').classList.add('active');
609
+
610
+ document.getElementById('saveBtn').onclick = async () => {
611
+ const key = document.getElementById('modalKey').value;
612
+ const value = JSON.parse(document.getElementById('modalValue').value);
613
+
614
+ await fetch(`/api/key/${encodeURIComponent(key)}`, {
615
+ method: 'POST',
616
+ headers: { 'Content-Type': 'application/json' },
617
+ body: JSON.stringify({ key, value })
618
+ });
619
+
620
+ closeModal();
621
+ loadCollections();
622
+ if (currentCollection) loadCollection(currentCollection);
623
+ };
624
+ };
625
+
626
+ // Delete key
627
+ async function deleteKey(key) {
628
+ if (!confirm(`Delete key "${key}"?`)) return;
629
+
630
+ await fetch(`/api/key/${encodeURIComponent(key)}`, {
631
+ method: 'DELETE'
632
+ });
633
+
634
+ if (currentCollection) loadCollection(currentCollection);
635
+ }
636
+
637
+ // Close modal
638
+ function closeModal() {
639
+ document.getElementById('editModal').classList.remove('active');
640
+ }
641
+
642
+ // Search
643
+ document.getElementById('searchInput').oninput = (e) => {
644
+ const search = e.target.value.toLowerCase();
645
+ const filtered = allKeys.filter(item =>
646
+ item.key.toLowerCase().includes(search)
647
+ );
648
+ renderData(filtered);
649
+ };
650
+
651
+ // Refresh
652
+ document.getElementById('refreshBtn').onclick = () => {
653
+ loadCollections();
654
+ loadStats();
655
+ if (currentCollection) loadCollection(currentCollection);
656
+ };
657
+
658
+ // Initialize
659
+ loadCollections();
660
+ loadStats();
661
+ </script>
662
+ </body>
663
+ </html>