@sava-info-systems/api-maker-with-extensions 2.2.1 → 2.3.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,1189 @@
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>Entity Diagram Viewer</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"/>
10
+ <style>
11
+ *, *::before, *::after {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ :root {
18
+ --bg: #0d1117;
19
+ --surface: #161b22;
20
+ --surface2: #1c2128;
21
+ --border: #30363d;
22
+ --border2: #21262d;
23
+ --text: #e6edf3;
24
+ --text-muted: #8b949e;
25
+ --text-dim: #484f58;
26
+ --accent: #a8171e;
27
+ --accent2: #a60911;
28
+ --radius: 10px;
29
+ --toolbar-h: 40px;
30
+ }
31
+
32
+ body {
33
+ background: var(--bg);
34
+ color: var(--text);
35
+ font-family: 'Inter', system-ui, sans-serif;
36
+ overflow: hidden;
37
+ height: 100vh;
38
+ width: 100vw;
39
+ }
40
+
41
+ /* ── Toolbar ─────────────────────────────────── */
42
+ #toolbar {
43
+ position: fixed;
44
+ top: 0;
45
+ left: 0;
46
+ right: 0;
47
+ height: var(--toolbar-h);
48
+ background: rgba(13, 17, 23, 0.92);
49
+ backdrop-filter: blur(12px);
50
+ border-bottom: 1px solid var(--border);
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 4px;
54
+ padding: 0 16px;
55
+ z-index: 100;
56
+ user-select: none;
57
+ }
58
+
59
+ #toolbar .logo {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 10px;
63
+ margin-right: 12px;
64
+ }
65
+
66
+ #toolbar .logo svg {
67
+ flex-shrink: 0;
68
+ }
69
+
70
+ #toolbar .logo-text {
71
+ font-family: 'JetBrains Mono', monospace;
72
+ font-weight: 700;
73
+ font-size: 14px;
74
+ color: var(--text);
75
+ letter-spacing: 0.04em;
76
+ }
77
+
78
+ #toolbar .logo-sub {
79
+ font-size: 10px;
80
+ color: var(--text-muted);
81
+ letter-spacing: 0.08em;
82
+ }
83
+
84
+ .tb-sep {
85
+ width: 1px;
86
+ height: 18px;
87
+ background: var(--border);
88
+ margin: 0 6px;
89
+ flex-shrink: 0;
90
+ }
91
+
92
+ .tb-group {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 2px;
96
+ }
97
+
98
+ .tb-btn {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 6px;
102
+ padding: 4px 8px;
103
+ border: 1px solid transparent;
104
+ border-radius: 7px;
105
+ background: transparent;
106
+ color: var(--text-muted);
107
+ font-family: 'Inter', sans-serif;
108
+ font-size: 12px;
109
+ font-weight: 500;
110
+ cursor: pointer;
111
+ transition: all 0.15s;
112
+ white-space: nowrap;
113
+ }
114
+
115
+ .tb-btn:hover {
116
+ background: var(--surface);
117
+ border-color: var(--border);
118
+ color: var(--text);
119
+ }
120
+
121
+ .tb-btn:active {
122
+ transform: scale(0.96);
123
+ }
124
+
125
+ .tb-btn svg {
126
+ flex-shrink: 0;
127
+ }
128
+
129
+ .tb-btn.primary {
130
+ background: #3a3a3a;
131
+ border-color: black;
132
+ color: #fff;
133
+ }
134
+
135
+ .tb-btn.primary:hover {
136
+ background: #7c060a;
137
+ }
138
+
139
+ .tb-btn.export-png {
140
+ color: #34d399;
141
+ }
142
+
143
+ .tb-btn.export-svg {
144
+ color: #fb923c;
145
+ }
146
+
147
+ .tb-btn.export-json {
148
+ color: #a78bfa;
149
+ }
150
+
151
+ .tb-btn.export-png:hover {
152
+ border-color: #34d399;
153
+ background: rgba(52, 211, 153, .1);
154
+ }
155
+
156
+ .tb-btn.export-svg:hover {
157
+ border-color: #fb923c;
158
+ background: rgba(251, 146, 60, .1);
159
+ }
160
+
161
+ .tb-btn.export-json:hover {
162
+ border-color: #a78bfa;
163
+ background: rgba(167, 139, 250, .1);
164
+ }
165
+
166
+ #zoom-display {
167
+ font-family: 'JetBrains Mono', monospace;
168
+ font-size: 12px;
169
+ color: var(--text-muted);
170
+ min-width: 44px;
171
+ text-align: center;
172
+ }
173
+
174
+ .tb-spacer {
175
+ flex: 1;
176
+ }
177
+
178
+ /* ── Search trigger ──────────────────────────── */
179
+ #btn-search {
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 8px;
183
+ padding: 4px 10px 4px 8px;
184
+ border: 1px solid var(--border);
185
+ border-radius: 7px;
186
+ background: var(--surface);
187
+ color: var(--text-dim);
188
+ font-size: 12px;
189
+ cursor: pointer;
190
+ transition: all 0.15s;
191
+ min-width: 160px;
192
+ font-family: 'Inter', sans-serif;
193
+ }
194
+
195
+ #btn-search:hover {
196
+ border-color: rgba(59, 130, 246, 0.4);
197
+ color: var(--text-muted);
198
+ }
199
+
200
+ #btn-search .search-hint {
201
+ margin-left: auto;
202
+ font-family: 'JetBrains Mono', monospace;
203
+ font-size: 10px;
204
+ background: rgba(255, 255, 255, 0.05);
205
+ border: 1px solid rgba(255, 255, 255, 0.08);
206
+ border-radius: 4px;
207
+ padding: 1px 5px;
208
+ color: var(--text-dim);
209
+ }
210
+
211
+ /* ── Command Palette ─────────────────────────── */
212
+ #cmd-overlay {
213
+ position: fixed;
214
+ inset: 0;
215
+ background: rgba(0, 0, 0, 0.55);
216
+ backdrop-filter: blur(6px);
217
+ z-index: 500;
218
+ display: none;
219
+ align-items: flex-start;
220
+ justify-content: center;
221
+ padding-top: 72px;
222
+ }
223
+
224
+ #cmd-overlay.open {
225
+ display: flex;
226
+ }
227
+
228
+ #cmd-palette {
229
+ width: 580px;
230
+ max-width: calc(100vw - 32px);
231
+ background: rgba(14, 18, 26, 0.98);
232
+ border: 1px solid rgba(59, 130, 246, 0.25);
233
+ border-radius: 14px;
234
+ box-shadow: 0 32px 80px rgba(0, 0, 0, 0.75),
235
+ 0 0 0 1px rgba(255, 255, 255, 0.03) inset,
236
+ 0 0 40px rgba(59, 130, 246, 0.06);
237
+ overflow: hidden;
238
+ animation: cmd-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
239
+ }
240
+
241
+ @keyframes cmd-in {
242
+ from { opacity: 0; transform: scale(0.96) translateY(-10px); }
243
+ to { opacity: 1; transform: scale(1) translateY(0); }
244
+ }
245
+
246
+ #cmd-input-wrap {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 10px;
250
+ padding: 12px 14px;
251
+ border-bottom: 1px solid rgba(48, 54, 61, 0.7);
252
+ }
253
+
254
+ #cmd-input-wrap .cmd-search-icon { color: #484f58; flex-shrink: 0; }
255
+
256
+ #cmd-input {
257
+ flex: 1;
258
+ background: transparent;
259
+ border: none;
260
+ outline: none;
261
+ font-family: 'Inter', sans-serif;
262
+ font-size: 14px;
263
+ color: var(--text);
264
+ caret-color: #60a5fa;
265
+ }
266
+
267
+ #cmd-input::placeholder { color: #484f58; }
268
+
269
+ #cmd-results {
270
+ max-height: 340px;
271
+ overflow-y: auto;
272
+ padding: 4px 6px;
273
+ scrollbar-width: thin;
274
+ scrollbar-color: rgba(255,255,255,0.08) transparent;
275
+ }
276
+
277
+ #cmd-results::-webkit-scrollbar { width: 4px; }
278
+ #cmd-results::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
279
+
280
+ .cmd-section-label {
281
+ padding: 8px 8px 3px;
282
+ font-size: 10px;
283
+ font-weight: 600;
284
+ letter-spacing: 0.1em;
285
+ color: #484f58;
286
+ text-transform: uppercase;
287
+ font-family: 'JetBrains Mono', monospace;
288
+ }
289
+
290
+ .cmd-item {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 10px;
294
+ padding: 7px 8px;
295
+ border-radius: 8px;
296
+ cursor: pointer;
297
+ transition: background 0.1s;
298
+ }
299
+
300
+ .cmd-item:hover { background: rgba(59, 130, 246, 0.1); }
301
+
302
+ .cmd-item.cmd-active {
303
+ background: rgba(59, 130, 246, 0.16);
304
+ outline: 1px solid rgba(59, 130, 246, 0.25);
305
+ }
306
+
307
+ .cmd-icon {
308
+ width: 28px;
309
+ height: 28px;
310
+ border-radius: 7px;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ flex-shrink: 0;
315
+ }
316
+
317
+ .cmd-icon.t-table { background: rgba(59,130,246,0.14); color: #60a5fa; }
318
+ .cmd-icon.t-column { background: rgba(167,139,250,0.14); color: #a78bfa; }
319
+ .cmd-icon.t-database { background: rgba(34,211,238,0.14); color: #22d3ee; }
320
+ .cmd-icon.t-instance { background: rgba(251,191,36,0.14); color: #fbbf24; }
321
+
322
+ .cmd-info { flex: 1; min-width: 0; }
323
+
324
+ .cmd-name {
325
+ font-size: 13px;
326
+ font-weight: 500;
327
+ color: var(--text);
328
+ white-space: nowrap;
329
+ overflow: hidden;
330
+ text-overflow: ellipsis;
331
+ }
332
+
333
+ .cmd-name mark {
334
+ background: transparent;
335
+ color: #60a5fa;
336
+ font-weight: 700;
337
+ }
338
+
339
+ .cmd-sub {
340
+ font-size: 11px;
341
+ color: #484f58;
342
+ white-space: nowrap;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ margin-top: 1px;
346
+ font-family: 'JetBrains Mono', monospace;
347
+ }
348
+
349
+ .cmd-meta {
350
+ display: flex;
351
+ gap: 4px;
352
+ flex-shrink: 0;
353
+ }
354
+
355
+ .cmd-badge {
356
+ font-family: 'JetBrains Mono', monospace;
357
+ font-size: 9px;
358
+ padding: 2px 5px;
359
+ border-radius: 4px;
360
+ }
361
+
362
+ .cmd-badge.pk { background: rgba(251,191,36,0.13); color: #fbbf24; border: 1px solid rgba(251,191,36,0.25); }
363
+ .cmd-badge.fk { background: rgba(34,211,238,0.13); color: #22d3ee; border: 1px solid rgba(34,211,238,0.25); }
364
+ .cmd-badge.col-type { background: rgba(139,148,158,0.08); color: #6e7681; border: 1px solid rgba(139,148,158,0.15); }
365
+
366
+ #cmd-empty {
367
+ display: none;
368
+ flex-direction: column;
369
+ align-items: center;
370
+ justify-content: center;
371
+ padding: 40px 20px;
372
+ color: #484f58;
373
+ font-size: 13px;
374
+ gap: 12px;
375
+ text-align: center;
376
+ }
377
+
378
+ #cmd-empty svg { opacity: 0.25; }
379
+
380
+ #cmd-empty p { line-height: 1.5; }
381
+ #cmd-empty strong { color: var(--text-muted); }
382
+
383
+ #cmd-footer {
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 14px;
387
+ padding: 7px 14px;
388
+ border-top: 1px solid rgba(48, 54, 61, 0.6);
389
+ font-size: 11px;
390
+ color: #484f58;
391
+ }
392
+
393
+ .cmd-key {
394
+ display: inline-flex;
395
+ align-items: center;
396
+ background: rgba(255, 255, 255, 0.05);
397
+ border: 1px solid rgba(255, 255, 255, 0.07);
398
+ border-radius: 3px;
399
+ padding: 1px 5px;
400
+ font-family: 'JetBrains Mono', monospace;
401
+ font-size: 10px;
402
+ color: #6e7681;
403
+ margin-right: 3px;
404
+ }
405
+
406
+ /* ── Canvas ──────────────────────────────────── */
407
+ #canvas {
408
+ position: fixed;
409
+ top: var(--toolbar-h);
410
+ left: 0;
411
+ cursor: default;
412
+ display: block;
413
+ }
414
+
415
+ /* ── Tooltip ─────────────────────────────────── */
416
+ #tooltip {
417
+ position: fixed;
418
+ display: none;
419
+ background: rgba(22, 27, 34, 0.97);
420
+ border: 1px solid var(--border);
421
+ border-radius: var(--radius);
422
+ padding: 10px 14px;
423
+ max-width: 280px;
424
+ pointer-events: none;
425
+ z-index: 200;
426
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
427
+ backdrop-filter: blur(8px);
428
+ }
429
+
430
+ .tt-name {
431
+ font-family: 'JetBrains Mono', monospace;
432
+ font-weight: 700;
433
+ font-size: 13px;
434
+ color: var(--text);
435
+ margin-bottom: 3px;
436
+ }
437
+
438
+ .tt-type {
439
+ font-family: 'JetBrains Mono', monospace;
440
+ font-size: 11px;
441
+ color: #60a5fa;
442
+ margin-bottom: 8px;
443
+ }
444
+
445
+ .tt-section {
446
+ font-size: 10px;
447
+ color: var(--text-muted);
448
+ margin-top: 6px;
449
+ line-height: 1.6;
450
+ }
451
+
452
+ .tt-section.tt-fk {
453
+ color: #22d3ee;
454
+ font-family: 'JetBrains Mono', monospace;
455
+ font-size: 11px;
456
+ }
457
+
458
+ .tt-badge {
459
+ display: inline-block;
460
+ padding: 1px 6px;
461
+ border-radius: 4px;
462
+ font-size: 10px;
463
+ background: var(--surface2);
464
+ border: 1px solid var(--border);
465
+ color: var(--text);
466
+ margin: 1px 2px;
467
+ font-family: 'JetBrains Mono', monospace;
468
+ }
469
+
470
+ .tt-badge.req {
471
+ background: rgba(248, 113, 113, .15);
472
+ border-color: #f87171;
473
+ color: #f87171;
474
+ }
475
+
476
+ .tt-badge.uniq {
477
+ background: rgba(96, 165, 250, .15);
478
+ border-color: #60a5fa;
479
+ color: #60a5fa;
480
+ }
481
+
482
+ .tt-badge.email {
483
+ background: rgba(96, 165, 250, .15);
484
+ border-color: #60a5fa;
485
+ color: #60a5fa;
486
+ }
487
+
488
+ .tt-badge.enum {
489
+ background: rgba(167, 139, 250, .15);
490
+ border-color: #a78bfa;
491
+ color: #a78bfa;
492
+ }
493
+
494
+ .tt-badge.enc {
495
+ background: rgba(251, 191, 36, .15);
496
+ border-color: #fbbf24;
497
+ color: #fbbf24;
498
+ }
499
+
500
+ .tt-badge.hash {
501
+ background: rgba(245, 158, 11, .15);
502
+ border-color: #f59e0b;
503
+ color: #f59e0b;
504
+ }
505
+
506
+ .tt-badge.def {
507
+ background: rgba(110, 231, 183, .15);
508
+ border-color: #6ee7b7;
509
+ color: #6ee7b7;
510
+ }
511
+
512
+ /* ── Export Dropdown ─────────────────────────── */
513
+ .tb-dropdown {
514
+ position: relative;
515
+ }
516
+
517
+ .tb-dropdown-menu {
518
+ display: none;
519
+ position: absolute;
520
+ top: calc(100% + 6px);
521
+ right: 0;
522
+ background: rgba(22, 27, 34, 0.97);
523
+ border: 1px solid var(--border);
524
+ border-radius: 8px;
525
+ padding: 4px;
526
+ min-width: 130px;
527
+ z-index: 200;
528
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
529
+ backdrop-filter: blur(12px);
530
+ }
531
+
532
+ .tb-dropdown-menu.open {
533
+ display: block;
534
+ }
535
+
536
+ .tb-dropdown-item {
537
+ display: flex;
538
+ align-items: center;
539
+ gap: 8px;
540
+ padding: 6px 10px;
541
+ border-radius: 5px;
542
+ font-size: 12px;
543
+ font-weight: 500;
544
+ cursor: pointer;
545
+ border: none;
546
+ background: transparent;
547
+ width: 100%;
548
+ text-align: left;
549
+ font-family: 'Inter', sans-serif;
550
+ transition: background 0.12s;
551
+ }
552
+
553
+ .tb-dropdown-item.export-png { color: #34d399; }
554
+ .tb-dropdown-item.export-svg { color: #fb923c; }
555
+
556
+ .tb-dropdown-item:hover {
557
+ background: var(--surface2);
558
+ }
559
+
560
+ .tb-dropdown-caret {
561
+ margin-left: 2px;
562
+ opacity: 0.6;
563
+ }
564
+
565
+ /* ── Legend ──────────────────────────────────── */
566
+ #legend {
567
+ display: none;
568
+ position: fixed;
569
+ bottom: 20px;
570
+ left: 20px;
571
+ background: rgba(22, 27, 34, 0.92);
572
+ border: 1px solid var(--border);
573
+ border-radius: var(--radius);
574
+ padding: 12px 16px;
575
+ z-index: 100;
576
+ backdrop-filter: blur(8px);
577
+ min-width: 190px;
578
+ }
579
+
580
+ .legend-title {
581
+ font-size: 10px;
582
+ font-weight: 600;
583
+ letter-spacing: 0.1em;
584
+ color: var(--text-dim);
585
+ text-transform: uppercase;
586
+ margin-bottom: 8px;
587
+ }
588
+
589
+ .legend-row {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 8px;
593
+ margin-bottom: 5px;
594
+ font-size: 11px;
595
+ color: var(--text-muted);
596
+ }
597
+
598
+ .legend-swatch {
599
+ width: 12px;
600
+ height: 12px;
601
+ border-radius: 3px;
602
+ flex-shrink: 0;
603
+ }
604
+
605
+ .legend-dot {
606
+ width: 8px;
607
+ height: 8px;
608
+ border-radius: 50%;
609
+ flex-shrink: 0;
610
+ }
611
+
612
+ .legend-line {
613
+ width: 20px;
614
+ height: 2px;
615
+ border-radius: 1px;
616
+ flex-shrink: 0;
617
+ }
618
+
619
+ /* ── Hints ───────────────────────────────────── */
620
+ #hints {
621
+ display: none;
622
+ position: fixed;
623
+ bottom: 20px;
624
+ right: 20px;
625
+ background: rgba(22, 27, 34, 0.92);
626
+ border: 1px solid var(--border);
627
+ border-radius: var(--radius);
628
+ padding: 10px 14px;
629
+ z-index: 100;
630
+ backdrop-filter: blur(8px);
631
+ font-size: 11px;
632
+ color: var(--text-muted);
633
+ line-height: 1.8;
634
+ }
635
+
636
+ .hint-key {
637
+ display: inline-block;
638
+ background: var(--surface2);
639
+ border: 1px solid var(--border);
640
+ border-radius: 4px;
641
+ padding: 0 5px;
642
+ font-family: 'JetBrains Mono', monospace;
643
+ font-size: 10px;
644
+ color: var(--text);
645
+ }
646
+
647
+ /* ── Loader ──────────────────────────────────── */
648
+ #loader {
649
+ position: fixed;
650
+ inset: 0;
651
+ display: flex;
652
+ flex-direction: column;
653
+ align-items: center;
654
+ justify-content: center;
655
+ background: var(--bg);
656
+ z-index: 999;
657
+ gap: 16px;
658
+ transition: opacity 0.4s;
659
+ }
660
+
661
+ #loader.hidden {
662
+ opacity: 0;
663
+ pointer-events: none;
664
+ }
665
+
666
+ .loader-spinner {
667
+ width: 40px;
668
+ height: 40px;
669
+ border: 3px solid var(--border);
670
+ border-top-color: var(--accent);
671
+ border-radius: 50%;
672
+ animation: spin 0.8s linear infinite;
673
+ }
674
+
675
+ .loader-text {
676
+ font-family: 'JetBrains Mono', monospace;
677
+ font-size: 13px;
678
+ color: var(--text-muted);
679
+ }
680
+
681
+ @keyframes spin {
682
+ to {
683
+ transform: rotate(360deg);
684
+ }
685
+ }
686
+ </style>
687
+ </head>
688
+ <body>
689
+
690
+ <!-- Loader -->
691
+ <div id="loader">
692
+ <div class="loader-spinner"></div>
693
+ <div class="loader-text">Loading diagram…</div>
694
+ </div>
695
+
696
+ <!-- Toolbar -->
697
+ <div id="toolbar">
698
+ <!--<div class="logo">
699
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
700
+ <rect width="28" height="28" rx="7" fill="#1f6feb"/>
701
+ <rect x="5" y="7" width="18" height="4" rx="1.5" fill="white" opacity="0.9"/>
702
+ <rect x="5" y="13" width="18" height="4" rx="1.5" fill="white" opacity="0.7"/>
703
+ <rect x="5" y="19" width="10" height="4" rx="1.5" fill="white" opacity="0.5"/>
704
+ <circle cx="22" cy="21" r="3" fill="#34d399"/>
705
+ </svg>
706
+ <div>
707
+ <div class="logo-text">EntityViz</div>
708
+ <div class="logo-sub">API MAKER</div>
709
+ </div>
710
+ </div>
711
+
712
+ <div class="tb-sep"></div>-->
713
+
714
+ <div class="tb-group">
715
+ <button class="tb-btn primary" id="btn-arrange" title="Auto-arrange tables">
716
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
717
+ <rect x="1" y="1" width="4" height="4" rx="1" stroke="currentColor" stroke-width="1.3"/>
718
+ <rect x="9" y="1" width="4" height="4" rx="1" stroke="currentColor" stroke-width="1.3"/>
719
+ <rect x="1" y="9" width="4" height="4" rx="1" stroke="currentColor" stroke-width="1.3"/>
720
+ <rect x="9" y="9" width="4" height="4" rx="1" stroke="currentColor" stroke-width="1.3"/>
721
+ <line x1="3" y1="5" x2="3" y2="9" stroke="currentColor" stroke-width="1.3"/>
722
+ <line x1="11" y1="5" x2="11" y2="9" stroke="currentColor" stroke-width="1.3"/>
723
+ <line x1="5" y1="3" x2="9" y2="3" stroke="currentColor" stroke-width="1.3"/>
724
+ <line x1="5" y1="11" x2="9" y2="11" stroke="currentColor" stroke-width="1.3"/>
725
+ </svg>
726
+ Auto-Arrange
727
+ </button>
728
+ </div>
729
+
730
+ <div class="tb-sep"></div>
731
+
732
+ <div class="tb-group">
733
+ <button class="tb-btn" id="btn-zoom-out" title="Zoom out">
734
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
735
+ <circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.3"/>
736
+ <line x1="3.5" y1="6" x2="8.5" y2="6" stroke="currentColor" stroke-width="1.3"/>
737
+ <line x1="10" y1="10" x2="13" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
738
+ </svg>
739
+ </button>
740
+ <span id="zoom-display">100%</span>
741
+ <button class="tb-btn" id="btn-zoom-in" title="Zoom in">
742
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
743
+ <circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.3"/>
744
+ <line x1="6" y1="3.5" x2="6" y2="8.5" stroke="currentColor" stroke-width="1.3"/>
745
+ <line x1="3.5" y1="6" x2="8.5" y2="6" stroke="currentColor" stroke-width="1.3"/>
746
+ <line x1="10" y1="10" x2="13" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
747
+ </svg>
748
+ </button>
749
+ <button class="tb-btn" id="btn-fit" title="Fit to screen">
750
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
751
+ <circle cx="7" cy="7" r="4" stroke="currentColor" stroke-width="1.3"/>
752
+ <circle cx="7" cy="7" r="1" fill="currentColor"/>
753
+ <line x1="7" y1="1" x2="7" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
754
+ <line x1="7" y1="11" x2="7" y2="13" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
755
+ <line x1="1" y1="7" x2="3" y2="7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
756
+ <line x1="11" y1="7" x2="13" y2="7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
757
+ </svg>
758
+ Fit
759
+ </button>
760
+ </div>
761
+
762
+ <div class="tb-sep"></div>
763
+
764
+ <div class="tb-group">
765
+ <button class="tb-btn" id="btn-deselect" title="Deselect all (Esc)">
766
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
767
+ <circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3"/>
768
+ <line x1="4" y1="4" x2="10" y2="10" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
769
+ </svg>
770
+ Clear
771
+ </button>
772
+ </div>
773
+
774
+ <div class="tb-sep"></div>
775
+
776
+ <button id="btn-search" title="Search (⌘K)">
777
+ <svg width="13" height="13" viewBox="0 0 14 14" fill="none" class="cmd-search-icon">
778
+ <circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.3"/>
779
+ <line x1="9.5" y1="9.5" x2="13" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
780
+ </svg>
781
+ Search…
782
+ <span class="search-hint">⌘K</span>
783
+ </button>
784
+
785
+ <div class="tb-spacer"></div>
786
+
787
+ <div class="tb-group">
788
+ <button class="tb-btn" id="btn-legend" title="Toggle legend">
789
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
790
+ <rect x="1" y="3" width="5" height="5" rx="1.5" stroke="currentColor" stroke-width="1.3"/>
791
+ <line x1="8" y1="5.5" x2="13" y2="5.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
792
+ <line x1="8" y1="8.5" x2="11" y2="8.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
793
+ </svg>
794
+ Legend
795
+ </button>
796
+ </div>
797
+
798
+ <div class="tb-sep"></div>
799
+
800
+ <div class="tb-group">
801
+ <button class="tb-btn" id="btn-fullscreen" title="Toggle fullscreen">
802
+ <svg id="icon-expand" width="14" height="14" viewBox="0 0 14 14" fill="none">
803
+ <polyline points="1,4 1,1 4,1" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
804
+ <polyline points="10,1 13,1 13,4" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
805
+ <polyline points="13,10 13,13 10,13" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
806
+ <polyline points="4,13 1,13 1,10" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
807
+ </svg>
808
+ <svg id="icon-compress" width="14" height="14" viewBox="0 0 14 14" fill="none" style="display:none">
809
+ <polyline points="4,1 4,4 1,4" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
810
+ <polyline points="10,4 13,4 13,1" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
811
+ <polyline points="13,10 10,10 10,13" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
812
+ <polyline points="4,10 1,10 1,13" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
813
+ </svg>
814
+ </button>
815
+ </div>
816
+
817
+ <div class="tb-sep"></div>
818
+
819
+ <div class="tb-group tb-dropdown">
820
+ <button class="tb-btn" id="btn-export" title="Export diagram">
821
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
822
+ <path d="M7 1v8M4 6l3 3 3-3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
823
+ <path d="M2 10v1.5A1.5 1.5 0 003.5 13h7a1.5 1.5 0 001.5-1.5V10" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
824
+ </svg>
825
+ Export
826
+ <svg class="tb-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" fill="none">
827
+ <path d="M2.5 3.5L5 6.5L7.5 3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
828
+ </svg>
829
+ </button>
830
+ <div class="tb-dropdown-menu" id="export-menu">
831
+ <button class="tb-dropdown-item export-png" id="btn-png">
832
+ <svg width="13" height="13" viewBox="0 0 14 14" fill="none">
833
+ <rect x="1" y="1" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.3"/>
834
+ <circle cx="5" cy="5" r="1.5" stroke="currentColor" stroke-width="1.1"/>
835
+ <path d="M1 10l3-3 2 2 3-3.5 4 4.5" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
836
+ </svg>
837
+ Export PNG
838
+ </button>
839
+ <button class="tb-dropdown-item export-svg" id="btn-svg">
840
+ <svg width="13" height="13" viewBox="0 0 14 14" fill="none">
841
+ <path d="M2 4C2 2.9 2.9 2 4 2h6l2 2v8c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4z" stroke="currentColor" stroke-width="1.3"/>
842
+ <path d="M8 2v3h4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
843
+ <text x="4" y="10" font-size="4" fill="currentColor" font-family="monospace">SVG</text>
844
+ </svg>
845
+ Export SVG
846
+ </button>
847
+ </div>
848
+ </div>
849
+ </div>
850
+
851
+ <!-- Command Palette -->
852
+ <div id="cmd-overlay">
853
+ <div id="cmd-palette" role="dialog" aria-label="Search">
854
+ <div id="cmd-input-wrap">
855
+ <svg class="cmd-search-icon" width="16" height="16" viewBox="0 0 14 14" fill="none">
856
+ <circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.4"/>
857
+ <line x1="9.5" y1="9.5" x2="13" y2="13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
858
+ </svg>
859
+ <input id="cmd-input" placeholder="Search tables, columns, databases…" autocomplete="off" spellcheck="false"/>
860
+ </div>
861
+ <div id="cmd-results"></div>
862
+ <div id="cmd-empty">
863
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
864
+ <circle cx="18" cy="18" r="12" stroke="#484f58" stroke-width="2"/>
865
+ <line x1="27" y1="27" x2="37" y2="37" stroke="#484f58" stroke-width="2.5" stroke-linecap="round"/>
866
+ <line x1="13" y1="18" x2="23" y2="18" stroke="#484f58" stroke-width="2" stroke-linecap="round"/>
867
+ <line x1="18" y1="13" x2="18" y2="23" stroke="#484f58" stroke-width="2" stroke-linecap="round"/>
868
+ </svg>
869
+ <p>No results for <strong id="cmd-empty-query"></strong></p>
870
+ </div>
871
+ <div id="cmd-footer">
872
+ <span><span class="cmd-key">↑↓</span> navigate</span>
873
+ <span><span class="cmd-key">↵</span> select</span>
874
+ <span><span class="cmd-key">esc</span> close</span>
875
+ <span style="margin-left:auto;font-size:10px;opacity:0.5" id="cmd-count"></span>
876
+ </div>
877
+ </div>
878
+ </div>
879
+
880
+ <!-- Canvas -->
881
+ <canvas id="canvas"></canvas>
882
+
883
+ <!-- Tooltip -->
884
+ <div id="tooltip"></div>
885
+
886
+ <!-- Legend -->
887
+ <div id="legend">
888
+ <div class="legend-title">Legend</div>
889
+ <div class="legend-row">
890
+ <div class="legend-dot" style="background:#fbbf24"></div>
891
+ <span>Primary Key</span>
892
+ </div>
893
+ <div class="legend-row">
894
+ <div class="legend-dot" style="background:#22d3ee"></div>
895
+ <span>Foreign Key</span>
896
+ </div>
897
+ <div class="legend-row">
898
+ <div class="legend-dot" style="background:#f87171"></div>
899
+ <span>Required field</span>
900
+ </div>
901
+ <div class="legend-row">
902
+ <div class="legend-line" style="background:linear-gradient(90deg,#60a5fa,#22d3ee)"></div>
903
+ <span>Relation</span>
904
+ </div>
905
+ <div class="legend-row">
906
+ <div class="legend-swatch" style="background:rgba(88,166,255,.15);border:1px solid #58a6ff"></div>
907
+ <span>Instance</span>
908
+ </div>
909
+ <div class="legend-row">
910
+ <div class="legend-swatch" style="background:rgba(61,68,77,.4);border:1px solid #3d444d"></div>
911
+ <span>Database</span>
912
+ </div>
913
+ </div>
914
+
915
+ <!-- Hints -->
916
+ <div id="hints">
917
+ <div><span class="hint-key">⌘/Ctrl</span> + Scroll — Zoom</div>
918
+ <div><span class="hint-key">Drag</span> — Pan canvas or move table</div>
919
+ <div><span class="hint-key">Click</span> — Select &amp; highlight relations</div>
920
+ <div><span class="hint-key">Esc</span> — Deselect</div>
921
+ </div>
922
+
923
+ <script src="index.js"></script>
924
+ <script>
925
+ // ── Fullscreen ──────────────────────────────────────────────────
926
+ (function () {
927
+ let fullscreen = false;
928
+
929
+ function setFullscreen(value) {
930
+ fullscreen = value;
931
+ document.getElementById('icon-expand').style.display = fullscreen ? 'none' : '';
932
+ document.getElementById('icon-compress').style.display = fullscreen ? '' : 'none';
933
+ }
934
+
935
+ document.getElementById('btn-fullscreen').addEventListener('click', () => {
936
+ setFullscreen(!fullscreen);
937
+ window.parent.postMessage({ type: 'DIAGRAM_FULLSCREEN', value: fullscreen }, '*');
938
+ });
939
+
940
+ window.addEventListener('message', e => {
941
+ if (e.data?.type === 'DIAGRAM_FULLSCREEN_ACK') {
942
+ setFullscreen(e.data.value);
943
+ // Let the browser apply the new iframe dimensions before refitting
944
+ setTimeout(() => { resize(); centerView(); scheduleRender(); }, 60);
945
+ }
946
+ });
947
+ })();
948
+
949
+ // ── Export dropdown ─────────────────────────────────────────────
950
+ const exportBtn = document.getElementById('btn-export');
951
+ const exportMenu = document.getElementById('export-menu');
952
+ exportBtn.addEventListener('click', e => { e.stopPropagation(); exportMenu.classList.toggle('open'); });
953
+ document.addEventListener('click', () => exportMenu.classList.remove('open'));
954
+ exportMenu.addEventListener('click', e => e.stopPropagation());
955
+
956
+ // ── Legend + Hints toggle ────────────────────────────────────────
957
+ document.getElementById('btn-legend').addEventListener('click', () => {
958
+ const visible = document.getElementById('legend').style.display === 'block';
959
+ document.getElementById('legend').style.display = visible ? 'none' : 'block';
960
+ document.getElementById('hints').style.display = visible ? 'none' : 'block';
961
+ });
962
+
963
+ // ── Command Palette Search ───────────────────────────────────────
964
+ (function () {
965
+ let index = []; // flat search entries
966
+ let results = []; // current filtered results
967
+ let cursor = -1; // keyboard-selected row index
968
+
969
+ const overlay = document.getElementById('cmd-overlay');
970
+ const input = document.getElementById('cmd-input');
971
+ const resultsEl = document.getElementById('cmd-results');
972
+ const emptyEl = document.getElementById('cmd-empty');
973
+ const emptyQ = document.getElementById('cmd-empty-query');
974
+ const countEl = document.getElementById('cmd-count');
975
+
976
+ // ── Icons ────────────────────────────────────────────────
977
+ const ICONS = {
978
+ table: `<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
979
+ <rect x="1" y="1" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.3"/>
980
+ <line x1="1" y1="5" x2="13" y2="5" stroke="currentColor" stroke-width="1.1"/>
981
+ <line x1="5" y1="5" x2="5" y2="13" stroke="currentColor" stroke-width="1.1"/>
982
+ </svg>`,
983
+ column: `<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
984
+ <line x1="2" y1="4" x2="12" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
985
+ <line x1="2" y1="7" x2="9" y2="7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
986
+ <line x1="2" y1="10" x2="11" y2="10" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
987
+ </svg>`,
988
+ database: `<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
989
+ <ellipse cx="7" cy="4" rx="5" ry="2" stroke="currentColor" stroke-width="1.3"/>
990
+ <path d="M2 4v6c0 1.1 2.2 2 5 2s5-.9 5-2V4" stroke="currentColor" stroke-width="1.3"/>
991
+ <path d="M2 7c0 1.1 2.2 2 5 2s5-.9 5-2" stroke="currentColor" stroke-width="1.3"/>
992
+ </svg>`,
993
+ instance: `<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
994
+ <rect x="1" y="3" width="12" height="8" rx="2" stroke="currentColor" stroke-width="1.3"/>
995
+ <line x1="4" y1="3" x2="4" y2="11" stroke="currentColor" stroke-width="1.1"/>
996
+ <circle cx="2.5" cy="6" r="0.8" fill="currentColor"/>
997
+ </svg>`,
998
+ };
999
+
1000
+ // ── Build search index after diagram data loads ──────────
1001
+ window.onDiagramLoaded = function () {
1002
+ index = [];
1003
+ Object.values(S.tables).forEach(t => {
1004
+ index.push({ type: 'table', label: t.coll, sub: t.db + ' · ' + t.inst, tableKey: t.key });
1005
+ t.columns.forEach(col => {
1006
+ if (col.isGroupHeader) return;
1007
+ index.push({
1008
+ type: 'column',
1009
+ label: col.name,
1010
+ sub: t.coll,
1011
+ tableKey: t.key,
1012
+ colType: col.type || '?',
1013
+ isPK: col.isPK,
1014
+ isFK: col.isFK,
1015
+ });
1016
+ });
1017
+ });
1018
+ Object.values(S.databases).forEach(db => {
1019
+ index.push({ type: 'database', label: db.name, sub: db.inst, dbKey: db.key });
1020
+ });
1021
+ Object.values(S.instances).forEach(inst => {
1022
+ index.push({ type: 'instance', label: inst.name, sub: inst.databases.length + ' database(s)', instName: inst.name });
1023
+ });
1024
+ };
1025
+
1026
+ // ── Open / close ─────────────────────────────────────────
1027
+ function open() {
1028
+ overlay.classList.add('open');
1029
+ input.value = '';
1030
+ cursor = -1;
1031
+ renderResults('');
1032
+ requestAnimationFrame(() => input.focus());
1033
+ }
1034
+
1035
+ function close() {
1036
+ overlay.classList.remove('open');
1037
+ input.blur();
1038
+ }
1039
+
1040
+ document.getElementById('btn-search').addEventListener('click', open);
1041
+
1042
+ document.addEventListener('keydown', e => {
1043
+ const hotkey = (e.metaKey || e.ctrlKey) && e.key === 'k';
1044
+ if (hotkey) { e.preventDefault(); overlay.classList.contains('open') ? close() : open(); return; }
1045
+ if (!overlay.classList.contains('open')) return;
1046
+ if (e.key === 'Escape') { close(); return; }
1047
+ if (e.key === 'ArrowDown') { e.preventDefault(); moveCursor(1); return; }
1048
+ if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1); return; }
1049
+ if (e.key === 'Enter') { e.preventDefault(); selectCurrent(); return; }
1050
+ });
1051
+
1052
+ overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
1053
+ input.addEventListener('input', () => { cursor = -1; renderResults(input.value.trim()); });
1054
+
1055
+ // ── Helpers ──────────────────────────────────────────────
1056
+ function escHtml(s) {
1057
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1058
+ }
1059
+
1060
+ function highlight(text, q) {
1061
+ if (!q) return escHtml(text);
1062
+ const i = text.toLowerCase().indexOf(q.toLowerCase());
1063
+ if (i === -1) return escHtml(text);
1064
+ return escHtml(text.slice(0, i)) +
1065
+ '<mark>' + escHtml(text.slice(i, i + q.length)) + '</mark>' +
1066
+ escHtml(text.slice(i + q.length));
1067
+ }
1068
+
1069
+ function score(entry, q) {
1070
+ const lq = q.toLowerCase();
1071
+ const ll = entry.label.toLowerCase();
1072
+ const ls = (entry.sub || '').toLowerCase();
1073
+ if (ll === lq) return 100;
1074
+ if (ll.startsWith(lq)) return 80;
1075
+ if (ll.includes(lq)) return 60;
1076
+ if (ls.includes(lq)) return 30;
1077
+ return -1;
1078
+ }
1079
+
1080
+ // ── Render results list ───────────────────────────────────
1081
+ function renderResults(q) {
1082
+ const typeOrder = { table: 0, column: 1, database: 2, instance: 3 };
1083
+
1084
+ if (!q) {
1085
+ // Default: show all tables up to 8
1086
+ results = Object.values(S.tables).slice(0, 8).map(t => ({
1087
+ type: 'table', label: t.coll, sub: t.db + ' · ' + t.inst, tableKey: t.key,
1088
+ }));
1089
+ } else {
1090
+ results = index
1091
+ .map(e => ({ ...e, _score: score(e, q) }))
1092
+ .filter(e => e._score > 0)
1093
+ .sort((a, b) => b._score - a._score || typeOrder[a.type] - typeOrder[b.type])
1094
+ .slice(0, 10);
1095
+ }
1096
+
1097
+ emptyEl.style.display = 'none';
1098
+ resultsEl.innerHTML = '';
1099
+
1100
+ if (results.length === 0) {
1101
+ emptyQ.textContent = '"' + q + '"';
1102
+ emptyEl.style.display = 'flex';
1103
+ countEl.textContent = '';
1104
+ return;
1105
+ }
1106
+
1107
+ countEl.textContent = results.length + ' result' + (results.length !== 1 ? 's' : '');
1108
+
1109
+ // Group by type
1110
+ const groups = {};
1111
+ results.forEach((r, i) => { (groups[r.type] = groups[r.type] || []).push({ ...r, _i: i }); });
1112
+
1113
+ const groupLabels = { table: 'Tables', column: 'Columns', database: 'Databases', instance: 'Instances' };
1114
+
1115
+ Object.entries(groups).forEach(([type, items]) => {
1116
+ const sec = document.createElement('div');
1117
+ sec.className = 'cmd-section-label';
1118
+ sec.textContent = groupLabels[type];
1119
+ resultsEl.appendChild(sec);
1120
+
1121
+ items.forEach(r => {
1122
+ const el = document.createElement('div');
1123
+ el.className = 'cmd-item';
1124
+ el.dataset.idx = r._i;
1125
+
1126
+ let meta = '';
1127
+ if (r.type === 'column') {
1128
+ if (r.isPK) meta += `<span class="cmd-badge pk">PK</span>`;
1129
+ if (r.isFK) meta += `<span class="cmd-badge fk">FK</span>`;
1130
+ meta += `<span class="cmd-badge col-type">${escHtml(r.colType)}</span>`;
1131
+ }
1132
+
1133
+ el.innerHTML = `
1134
+ <div class="cmd-icon t-${r.type}">${ICONS[r.type]}</div>
1135
+ <div class="cmd-info">
1136
+ <div class="cmd-name">${highlight(r.label, q)}</div>
1137
+ <div class="cmd-sub">${escHtml(r.sub || '')}</div>
1138
+ </div>
1139
+ <div class="cmd-meta">${meta}</div>`;
1140
+
1141
+ el.addEventListener('click', () => { cursor = r._i; selectCurrent(); });
1142
+ el.addEventListener('mouseenter', () => { cursor = r._i; updateCursorUI(); });
1143
+ resultsEl.appendChild(el);
1144
+ });
1145
+ });
1146
+
1147
+ updateCursorUI();
1148
+ }
1149
+
1150
+ // ── Keyboard cursor ──────────────────────────────────────
1151
+ function moveCursor(dir) {
1152
+ cursor = Math.max(0, Math.min(results.length - 1, cursor + dir));
1153
+ updateCursorUI(true);
1154
+ }
1155
+
1156
+ function updateCursorUI(scroll) {
1157
+ resultsEl.querySelectorAll('.cmd-item').forEach(el => {
1158
+ const active = parseInt(el.dataset.idx) === cursor;
1159
+ el.classList.toggle('cmd-active', active);
1160
+ if (active && scroll) el.scrollIntoView({ block: 'nearest' });
1161
+ });
1162
+ }
1163
+
1164
+ // ── Navigate to table on canvas ──────────────────────────
1165
+ function selectCurrent() {
1166
+ const r = results[cursor];
1167
+ if (!r) return;
1168
+ close();
1169
+
1170
+ const tableKey = r.tableKey;
1171
+ if (!tableKey || !S.tables[tableKey]) return;
1172
+
1173
+ const t = S.tables[tableKey];
1174
+ S.selected = tableKey;
1175
+
1176
+ const cx = t.x + t.w / 2;
1177
+ const cy = t.y + t.h / 2;
1178
+ const targetZoom = Math.min(Math.max(S.zoom, 0.75), 1.5);
1179
+ S.zoom = targetZoom;
1180
+ S.panX = S.vw / 2 - cx * S.zoom;
1181
+ S.panY = S.vh / 2 - cy * S.zoom;
1182
+ S.ripples.push({ x: cx, y: cy, r: 0, a: 1 });
1183
+ scheduleRender();
1184
+ }
1185
+
1186
+ })();
1187
+ </script>
1188
+ </body>
1189
+ </html>