intellifont-engine 1.0.0 → 2.0.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,759 @@
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>intelliFont Canvas DNA Inspector — Demo</title>
7
+ <meta name="description" content="Identify any font on a webpage by analyzing rendered glyph metrics. Click any text below to inspect it." />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700&family=JetBrains+Mono:wght@400;600&family=Merriweather:wght@400;700&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet" />
10
+ <style>
11
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+
13
+ :root {
14
+ --bg: #080c14;
15
+ --surface: #0e1420;
16
+ --surface2: #131927;
17
+ --border: rgba(255,255,255,0.07);
18
+ --accent: #6366f1;
19
+ --accent2: #8b5cf6;
20
+ --text: #e2e8f0;
21
+ --muted: #64748b;
22
+ --green: #6ee7b7;
23
+ --yellow: #fbbf24;
24
+ --red: #f87171;
25
+ }
26
+
27
+ html, body {
28
+ height: 100%;
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: 'Inter', system-ui, sans-serif;
32
+ font-size: 15px;
33
+ line-height: 1.6;
34
+ }
35
+
36
+ /* ─── Layout ─────────────────────────────────────────────── */
37
+ .app {
38
+ display: grid;
39
+ grid-template-rows: auto 1fr;
40
+ min-height: 100vh;
41
+ }
42
+
43
+ header {
44
+ padding: 18px 32px;
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 14px;
48
+ border-bottom: 1px solid var(--border);
49
+ background: rgba(255,255,255,0.02);
50
+ backdrop-filter: blur(12px);
51
+ position: sticky;
52
+ top: 0;
53
+ z-index: 100;
54
+ }
55
+
56
+ .logo {
57
+ width: 38px; height: 38px;
58
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
59
+ border-radius: 10px;
60
+ display: flex; align-items: center; justify-content: center;
61
+ font-size: 20px; font-weight: 800; color: white;
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ .header-text h1 {
66
+ font-size: 17px;
67
+ font-weight: 700;
68
+ color: #f8fafc;
69
+ }
70
+ .header-text p {
71
+ font-size: 12px;
72
+ color: var(--muted);
73
+ }
74
+
75
+ .server-config {
76
+ margin-left: auto;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 8px;
80
+ }
81
+
82
+ .server-config label {
83
+ font-size: 12px;
84
+ color: var(--muted);
85
+ }
86
+
87
+ .server-config input {
88
+ background: var(--surface2);
89
+ border: 1px solid var(--border);
90
+ border-radius: 8px;
91
+ padding: 5px 10px;
92
+ font-size: 12px;
93
+ color: var(--text);
94
+ width: 210px;
95
+ outline: none;
96
+ }
97
+ .server-config input:focus { border-color: var(--accent); }
98
+
99
+ .main {
100
+ display: grid;
101
+ grid-template-columns: 1fr 360px;
102
+ gap: 0;
103
+ overflow: hidden;
104
+ }
105
+
106
+ /* ─── Font Playground Panel ───────────────────────────────── */
107
+ .playground {
108
+ padding: 36px 40px;
109
+ overflow-y: auto;
110
+ border-right: 1px solid var(--border);
111
+ }
112
+
113
+ .playground-header {
114
+ margin-bottom: 28px;
115
+ }
116
+ .playground-header h2 {
117
+ font-size: 22px;
118
+ font-weight: 600;
119
+ color: #f1f5f9;
120
+ margin-bottom: 6px;
121
+ }
122
+ .playground-header p {
123
+ color: var(--muted);
124
+ font-size: 13px;
125
+ }
126
+
127
+ .font-cards {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 16px;
131
+ }
132
+
133
+ .font-card {
134
+ background: var(--surface);
135
+ border: 1px solid var(--border);
136
+ border-radius: 16px;
137
+ padding: 24px 28px;
138
+ cursor: pointer;
139
+ transition: border-color 0.2s, transform 0.15s, box-shadow 0.2s;
140
+ position: relative;
141
+ overflow: hidden;
142
+ }
143
+ .font-card::before {
144
+ content: '';
145
+ position: absolute;
146
+ inset: 0;
147
+ background: radial-gradient(ellipse at top left, rgba(99,102,241,0.06), transparent 70%);
148
+ pointer-events: none;
149
+ }
150
+ .font-card:hover {
151
+ border-color: rgba(99,102,241,0.5);
152
+ transform: translateY(-2px);
153
+ box-shadow: 0 12px 40px rgba(0,0,0,0.3);
154
+ }
155
+ .font-card.inspecting {
156
+ border-color: var(--accent);
157
+ box-shadow: 0 0 0 3px rgba(99,102,241,0.2);
158
+ }
159
+
160
+ .font-card-label {
161
+ font-size: 11px;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.08em;
164
+ color: var(--muted);
165
+ margin-bottom: 10px;
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 6px;
169
+ }
170
+ .font-card-label .dot {
171
+ width: 6px; height: 6px;
172
+ border-radius: 50%;
173
+ background: var(--accent);
174
+ }
175
+
176
+ .font-card-sample {
177
+ font-size: 32px;
178
+ line-height: 1.3;
179
+ color: #f8fafc;
180
+ margin-bottom: 12px;
181
+ }
182
+
183
+ .font-card-hint {
184
+ font-size: 11px;
185
+ color: var(--muted);
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 5px;
189
+ }
190
+
191
+ /* ─── Results Panel ───────────────────────────────────────── */
192
+ .results-panel {
193
+ background: var(--surface);
194
+ display: flex;
195
+ flex-direction: column;
196
+ overflow: hidden;
197
+ }
198
+
199
+ .results-header {
200
+ padding: 24px 24px 16px;
201
+ border-bottom: 1px solid var(--border);
202
+ }
203
+ .results-header h3 {
204
+ font-size: 14px;
205
+ font-weight: 600;
206
+ color: #f1f5f9;
207
+ margin-bottom: 4px;
208
+ }
209
+ .results-header .status {
210
+ font-size: 12px;
211
+ color: var(--muted);
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 6px;
215
+ }
216
+ .status-dot {
217
+ width: 7px; height: 7px;
218
+ border-radius: 50%;
219
+ background: var(--muted);
220
+ transition: background 0.3s;
221
+ }
222
+ .status-dot.active { background: var(--green); animation: pulse 1.5s infinite; }
223
+
224
+ @keyframes pulse {
225
+ 0%, 100% { opacity: 1; }
226
+ 50% { opacity: 0.4; }
227
+ }
228
+
229
+ .results-body {
230
+ flex: 1;
231
+ overflow-y: auto;
232
+ padding: 16px;
233
+ }
234
+
235
+ .placeholder {
236
+ height: 100%;
237
+ display: flex;
238
+ flex-direction: column;
239
+ align-items: center;
240
+ justify-content: center;
241
+ gap: 12px;
242
+ color: var(--muted);
243
+ text-align: center;
244
+ padding: 40px 20px;
245
+ }
246
+ .placeholder-icon {
247
+ font-size: 48px;
248
+ opacity: 0.3;
249
+ }
250
+ .placeholder p { font-size: 13px; }
251
+
252
+ /* Detected font box */
253
+ .detected-font {
254
+ background: var(--surface2);
255
+ border-radius: 12px;
256
+ padding: 14px 16px;
257
+ margin-bottom: 14px;
258
+ border: 1px solid var(--border);
259
+ }
260
+ .detected-font .label {
261
+ font-size: 10px;
262
+ text-transform: uppercase;
263
+ letter-spacing: 0.08em;
264
+ color: var(--muted);
265
+ margin-bottom: 6px;
266
+ }
267
+ .detected-font .name {
268
+ font-size: 14px;
269
+ font-weight: 600;
270
+ color: #a5b4fc;
271
+ font-family: 'JetBrains Mono', monospace;
272
+ }
273
+
274
+ /* Match items */
275
+ .match-item {
276
+ background: var(--surface2);
277
+ border: 1px solid var(--border);
278
+ border-radius: 12px;
279
+ padding: 12px 14px;
280
+ margin-bottom: 8px;
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: space-between;
284
+ transition: border-color 0.15s, background 0.15s;
285
+ }
286
+ .match-item:hover { border-color: rgba(99,102,241,0.35); background: rgba(99,102,241,0.04); }
287
+ .match-item.top-match { border-color: rgba(99,102,241,0.45); }
288
+
289
+ .match-left .family {
290
+ font-size: 14px;
291
+ font-weight: 600;
292
+ color: #f1f5f9;
293
+ }
294
+ .match-left .sub {
295
+ font-size: 11px;
296
+ color: var(--muted);
297
+ margin-top: 2px;
298
+ }
299
+
300
+ .match-right { text-align: right; }
301
+ .match-right .pct {
302
+ font-size: 16px;
303
+ font-weight: 700;
304
+ }
305
+ .match-right .quality {
306
+ font-size: 10px;
307
+ text-transform: uppercase;
308
+ letter-spacing: 0.05em;
309
+ color: var(--muted);
310
+ }
311
+
312
+ .c-green { color: var(--green); }
313
+ .c-yellow { color: var(--yellow); }
314
+ .c-red { color: var(--red); }
315
+
316
+ /* Metrics debug strip */
317
+ .metrics-strip {
318
+ margin-top: 14px;
319
+ padding-top: 14px;
320
+ border-top: 1px solid var(--border);
321
+ }
322
+ .metrics-strip .label {
323
+ font-size: 10px;
324
+ text-transform: uppercase;
325
+ letter-spacing: 0.08em;
326
+ color: var(--muted);
327
+ margin-bottom: 8px;
328
+ }
329
+ .metric-row {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 8px;
333
+ margin-bottom: 5px;
334
+ font-size: 11px;
335
+ }
336
+ .metric-row .mname {
337
+ color: var(--muted);
338
+ width: 90px;
339
+ flex-shrink: 0;
340
+ }
341
+ .metric-bar-bg {
342
+ flex: 1;
343
+ height: 4px;
344
+ background: rgba(255,255,255,0.07);
345
+ border-radius: 99px;
346
+ overflow: hidden;
347
+ }
348
+ .metric-bar {
349
+ height: 100%;
350
+ background: linear-gradient(90deg, var(--accent), var(--accent2));
351
+ border-radius: 99px;
352
+ transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
353
+ }
354
+ .metric-val {
355
+ width: 28px;
356
+ text-align: right;
357
+ color: #94a3b8;
358
+ }
359
+
360
+ /* Spinner */
361
+ .spinner {
362
+ display: inline-block;
363
+ width: 14px; height: 14px;
364
+ border: 2px solid rgba(255,255,255,0.1);
365
+ border-top-color: var(--accent);
366
+ border-radius: 50%;
367
+ animation: spin 0.7s linear infinite;
368
+ }
369
+ @keyframes spin { to { transform: rotate(360deg); } }
370
+
371
+ /* Bookmarklet section */
372
+ .bookmarklet-section {
373
+ padding: 20px 24px;
374
+ border-top: 1px solid var(--border);
375
+ }
376
+ .bookmarklet-section h4 {
377
+ font-size: 12px;
378
+ text-transform: uppercase;
379
+ letter-spacing: 0.08em;
380
+ color: var(--muted);
381
+ margin-bottom: 10px;
382
+ }
383
+ .bookmarklet-box {
384
+ background: var(--surface2);
385
+ border: 1px solid var(--border);
386
+ border-radius: 10px;
387
+ padding: 10px 14px;
388
+ font-family: 'JetBrains Mono', monospace;
389
+ font-size: 10px;
390
+ color: #93c5fd;
391
+ word-break: break-all;
392
+ line-height: 1.5;
393
+ max-height: 80px;
394
+ overflow-y: auto;
395
+ }
396
+ .copy-btn {
397
+ margin-top: 8px;
398
+ width: 100%;
399
+ background: var(--surface2);
400
+ border: 1px solid var(--border);
401
+ border-radius: 8px;
402
+ padding: 7px;
403
+ color: var(--text);
404
+ font-size: 12px;
405
+ cursor: pointer;
406
+ transition: border-color 0.15s, background 0.15s;
407
+ }
408
+ .copy-btn:hover { border-color: var(--accent); background: rgba(99,102,241,0.08); }
409
+
410
+ /* Error */
411
+ .error-box {
412
+ background: rgba(248, 113, 113, 0.08);
413
+ border: 1px solid rgba(248, 113, 113, 0.25);
414
+ border-radius: 10px;
415
+ padding: 12px 14px;
416
+ font-size: 12px;
417
+ color: var(--red);
418
+ margin-bottom: 12px;
419
+ }
420
+
421
+ /* Responsive */
422
+ @media (max-width: 768px) {
423
+ .main { grid-template-columns: 1fr; }
424
+ .results-panel { border-right: none; border-top: 1px solid var(--border); max-height: 50vh; }
425
+ .font-card-sample { font-size: 24px; }
426
+ }
427
+ </style>
428
+ </head>
429
+ <body>
430
+ <div class="app">
431
+
432
+ <!-- HEADER -->
433
+ <header>
434
+ <div class="logo">𝔽</div>
435
+ <div class="header-text">
436
+ <h1>intelliFont Canvas DNA Inspector</h1>
437
+ <p>Identify any font by analyzing rendered pixel geometry — no file upload needed</p>
438
+ </div>
439
+ <div class="server-config">
440
+ <label for="server-url">Backend URL:</label>
441
+ <input id="server-url" type="text" value="http://localhost:3000" placeholder="http://localhost:3000" />
442
+ </div>
443
+ </header>
444
+
445
+ <!-- MAIN CONTENT -->
446
+ <div class="main">
447
+
448
+ <!-- LEFT: Font Playground -->
449
+ <div class="playground">
450
+ <div class="playground-header">
451
+ <h2>Font Playground</h2>
452
+ <p>Click on any text sample below. The Canvas DNA analyzer will measure the rendered glyph shapes and query the intelliFont engine to identify the font.</p>
453
+ </div>
454
+
455
+ <div class="font-cards" id="font-cards">
456
+
457
+ <div class="font-card" data-font="Inter" data-chars="RQWM">
458
+ <div class="font-card-label"><span class="dot"></span>Sans-Serif · Google Fonts</div>
459
+ <div class="font-card-sample" style="font-family:'Inter',sans-serif;">
460
+ The quick brown fox jumps over the lazy dog.
461
+ </div>
462
+ <div class="font-card-hint">🎯 Click to inspect · Font: Inter</div>
463
+ </div>
464
+
465
+ <div class="font-card" data-font="Playfair Display" data-chars="RQWM">
466
+ <div class="font-card-label"><span class="dot"></span>Serif · Google Fonts</div>
467
+ <div class="font-card-sample" style="font-family:'Playfair Display',serif; font-weight:700;">
468
+ Sphinx of black quartz, judge my vow.
469
+ </div>
470
+ <div class="font-card-hint">🎯 Click to inspect · Font: Playfair Display</div>
471
+ </div>
472
+
473
+ <div class="font-card" data-font="Merriweather" data-chars="RQWM">
474
+ <div class="font-card-label"><span class="dot"></span>Serif · Google Fonts</div>
475
+ <div class="font-card-sample" style="font-family:'Merriweather',serif;">
476
+ Pack my box with five dozen liquor jugs.
477
+ </div>
478
+ <div class="font-card-hint">🎯 Click to inspect · Font: Merriweather</div>
479
+ </div>
480
+
481
+ <div class="font-card" data-font="JetBrains Mono" data-chars="RQWM">
482
+ <div class="font-card-label"><span class="dot"></span>Monospace · Google Fonts</div>
483
+ <div class="font-card-sample" style="font-family:'JetBrains Mono',monospace; font-size:22px;">
484
+ function identify(font) { return dna(font); }
485
+ </div>
486
+ <div class="font-card-hint">🎯 Click to inspect · Font: JetBrains Mono</div>
487
+ </div>
488
+
489
+ <div class="font-card" data-font="Space Grotesk" data-chars="RQWM">
490
+ <div class="font-card-label"><span class="dot"></span>Geometric Sans · Google Fonts</div>
491
+ <div class="font-card-sample" style="font-family:'Space Grotesk',sans-serif; font-weight:600;">
492
+ How vexingly quick daft zebras jump!
493
+ </div>
494
+ <div class="font-card-hint">🎯 Click to inspect · Font: Space Grotesk</div>
495
+ </div>
496
+
497
+ <div class="font-card" data-font="Georgia" data-chars="RQWM">
498
+ <div class="font-card-label"><span class="dot"></span>Serif · System Font</div>
499
+ <div class="font-card-sample" style="font-family:Georgia,serif;">
500
+ Waltz, bad nymph, for quick jigs vex.
501
+ </div>
502
+ <div class="font-card-hint">🎯 Click to inspect · Font: Georgia (system)</div>
503
+ </div>
504
+
505
+ <div class="font-card" data-font="Arial" data-chars="RQWM">
506
+ <div class="font-card-label"><span class="dot"></span>Sans-Serif · System Font</div>
507
+ <div class="font-card-sample" style="font-family:Arial,sans-serif;">
508
+ Mr. Jock, TV quiz PhD, bags few lynx.
509
+ </div>
510
+ <div class="font-card-hint">🎯 Click to inspect · Font: Arial (system)</div>
511
+ </div>
512
+
513
+ </div>
514
+ </div>
515
+
516
+ <!-- RIGHT: Results Panel -->
517
+ <div class="results-panel">
518
+ <div class="results-header">
519
+ <h3>Analysis Results</h3>
520
+ <div class="status">
521
+ <div class="status-dot" id="status-dot"></div>
522
+ <span id="status-text">Waiting for inspection…</span>
523
+ </div>
524
+ </div>
525
+
526
+ <div class="results-body" id="results-body">
527
+ <div class="placeholder">
528
+ <div class="placeholder-icon">🔬</div>
529
+ <p>Click any font sample on the left to run Canvas DNA analysis.</p>
530
+ </div>
531
+ </div>
532
+
533
+ <!-- Bookmarklet -->
534
+ <div class="bookmarklet-section">
535
+ <h4>Bookmarklet — use on any site</h4>
536
+ <div class="bookmarklet-box" id="bookmarklet-code">loading…</div>
537
+ <button class="copy-btn" onclick="copyBookmarklet()">📋 Copy Bookmarklet</button>
538
+ </div>
539
+ </div>
540
+
541
+ </div>
542
+ </div>
543
+
544
+ <!-- Load Canvas DNA (inline for portability) -->
545
+ <script src="canvas-dna.js"></script>
546
+
547
+ <script>
548
+ // ============================================================
549
+ // CONFIGURATION
550
+ // ============================================================
551
+ function getServerUrl() {
552
+ return document.getElementById('server-url').value.trim() || 'http://localhost:3000';
553
+ }
554
+
555
+ // ============================================================
556
+ // FONT CARD CLICK HANDLER
557
+ // ============================================================
558
+ document.getElementById('font-cards').addEventListener('click', async (e) => {
559
+ const card = e.target.closest('.font-card');
560
+ if (!card) return;
561
+
562
+ // Highlight active card
563
+ document.querySelectorAll('.font-card').forEach(c => c.classList.remove('inspecting'));
564
+ card.classList.add('inspecting');
565
+
566
+ const fontName = card.dataset.font;
567
+ const chars = card.dataset.chars || 'RQWM';
568
+
569
+ setStatus('active', `Analyzing "${fontName}"…`);
570
+ showLoading();
571
+
572
+ try {
573
+ // Step 1: Compute canvas metrics
574
+ const metrics = CanvasDNA.analyzeFont(fontName, chars);
575
+
576
+ if (!metrics || metrics.length === 0) {
577
+ showError('Could not extract glyph metrics. Is the font fully loaded?');
578
+ setStatus('', 'Analysis failed.');
579
+ return;
580
+ }
581
+
582
+ // Step 2: Try to call backend
583
+ let matches = [];
584
+ let backendError = null;
585
+ let databaseNotFound = false;
586
+
587
+ try {
588
+ const res = await fetch(`${getServerUrl()}/api/identify-from-metrics`, {
589
+ method: 'POST',
590
+ headers: { 'Content-Type': 'application/json' },
591
+ body: JSON.stringify({ metrics, limit: 8 }),
592
+ signal: AbortSignal.timeout(4000),
593
+ });
594
+ if (res.ok) {
595
+ const json = await res.json();
596
+ matches = json.matches || [];
597
+ databaseNotFound = !!json.databaseNotFound;
598
+ } else {
599
+ backendError = `Server returned ${res.status}`;
600
+ }
601
+ } catch (err) {
602
+ backendError = err.message.includes('timeout') ? 'Request timed out' : 'Backend not reachable';
603
+ }
604
+
605
+ showResults(fontName, metrics, matches, backendError, databaseNotFound);
606
+ const statusMsg = databaseNotFound
607
+ ? 'Metrics computed — build glyph DB to match.'
608
+ : `Done — ${matches.length} match${matches.length !== 1 ? 'es' : ''} found.`;
609
+ setStatus('', statusMsg);
610
+
611
+ } catch (err) {
612
+ showError(err.message);
613
+ setStatus('', 'Error during analysis.');
614
+ card.classList.remove('inspecting');
615
+ }
616
+ });
617
+
618
+ // ============================================================
619
+ // UI HELPERS
620
+ // ============================================================
621
+ function setStatus(state, text) {
622
+ document.getElementById('status-dot').className = 'status-dot' + (state ? ' ' + state : '');
623
+ document.getElementById('status-text').textContent = text;
624
+ }
625
+
626
+ function showLoading() {
627
+ document.getElementById('results-body').innerHTML = `
628
+ <div class="placeholder">
629
+ <div class="spinner"></div>
630
+ <p>Running Canvas DNA analysis…</p>
631
+ </div>`;
632
+ }
633
+
634
+ function showError(msg) {
635
+ document.getElementById('results-body').innerHTML = `
636
+ <div class="error-box">⚠️ ${msg}</div>`;
637
+ }
638
+
639
+ function confidenceColor(score) {
640
+ if (score >= 0.90) return 'c-green';
641
+ if (score >= 0.70) return 'c-yellow';
642
+ return 'c-red';
643
+ }
644
+
645
+ function showResults(fontName, metrics, matches, backendError, databaseNotFound) {
646
+ const body = document.getElementById('results-body');
647
+
648
+ const detectedHtml = `
649
+ <div class="detected-font">
650
+ <div class="label">Detected CSS Font</div>
651
+ <div class="name">"${fontName}"</div>
652
+ </div>`;
653
+
654
+ let infoHtml = '';
655
+ if (backendError) {
656
+ infoHtml = `
657
+ <div class="error-box">
658
+ ⚙️ Backend not connected: ${backendError}<br>
659
+ <small>Start intelliFont server: <code>intellifont serve --port 3000</code></small>
660
+ <br><small>Showing computed pixel metrics only.</small>
661
+ </div>`;
662
+ } else if (databaseNotFound) {
663
+ infoHtml = `
664
+ <div style="
665
+ background: rgba(99,102,241,0.08);
666
+ border: 1px solid rgba(99,102,241,0.25);
667
+ border-radius: 10px; padding: 12px 14px;
668
+ font-size: 12px; color: #a5b4fc; margin-bottom: 12px;
669
+ ">
670
+ 📦 Glyph signature database not yet built.<br>
671
+ <small style="color:var(--muted)">
672
+ Run: <code style="color:#a5b4fc">intellifont build-glyph-db &lt;fonts-dir&gt;</code><br>
673
+ Metrics above are computed correctly — matches will appear once the DB is ready.
674
+ </small>
675
+ </div>`;
676
+ }
677
+
678
+ const matchesHtml = matches.length > 0
679
+ ? matches.map((m, i) => {
680
+ const score = m.confidence || m.score || 0;
681
+ const col = confidenceColor(score);
682
+ return `
683
+ <div class="match-item${i === 0 ? ' top-match' : ''}">
684
+ <div class="match-left">
685
+ <div class="family">${i === 0 ? '🏆 ' : ''}${m.family}</div>
686
+ <div class="sub">${m.subfamily || 'Regular'} · ${m.matchQuality || ''}</div>
687
+ </div>
688
+ <div class="match-right">
689
+ <div class="pct ${col}">${(score * 100).toFixed(0)}%</div>
690
+ <div class="quality">${m.matchQuality || (score >= 0.9 ? 'exact' : score >= 0.75 ? 'high' : 'medium')}</div>
691
+ </div>
692
+ </div>`;
693
+ }).join('')
694
+ : (backendError || databaseNotFound ? '' :
695
+ '<div style="color:var(--muted);font-size:13px;padding:12px 0;">No matches found in database.</div>');
696
+
697
+ // Metrics from first analyzed character
698
+ const m0 = metrics[0];
699
+ const METRIC_FIELDS = [
700
+ ['Aspect Ratio', 'aspectRatio'],
701
+ ['Ink Density', 'density'],
702
+ ['Curve Ratio', 'curveRatio'],
703
+ ['Serif Score', 'serifScore'],
704
+ ['Stroke Width', 'strokeWidth'],
705
+ ['X Balance', 'xBalance'],
706
+ ['Y Balance', 'yBalance'],
707
+ ];
708
+
709
+ const metricsHtml = m0 ? `
710
+ <div class="metrics-strip">
711
+ <div class="label">Canvas DNA Metrics · char "${m0.character}"</div>
712
+ ${METRIC_FIELDS.map(([label, key]) => `
713
+ <div class="metric-row">
714
+ <span class="mname">${label}</span>
715
+ <div class="metric-bar-bg">
716
+ <div class="metric-bar" style="width:${((m0[key] || 0) / 255 * 100).toFixed(1)}%"></div>
717
+ </div>
718
+ <span class="metric-val">${m0[key] || 0}</span>
719
+ </div>`).join('')}
720
+ </div>` : '';
721
+
722
+ body.innerHTML = detectedHtml + infoHtml + matchesHtml + metricsHtml;
723
+ }
724
+
725
+ // ============================================================
726
+ // BOOKMARKLET GENERATOR
727
+ // ============================================================
728
+ const BOOKMARKLET = `javascript:(function(){
729
+ var s=document.createElement('script');
730
+ s.src='http://localhost:3000/browser/canvas-dna.js';
731
+ s.onload=function(){
732
+ var i=document.createElement('script');
733
+ i.src='http://localhost:3000/browser/intellifont-inspector.js';
734
+ i.onload=function(){
735
+ var ins=new IntellifontInspector({serverUrl:'http://localhost:3000',showOverlay:true});
736
+ ins.enableClickInspect();
737
+ var b=document.createElement('div');
738
+ b.style.cssText='position:fixed;bottom:20px;right:20px;background:#6366f1;color:white;padding:10px 18px;border-radius:99px;font-family:Inter,sans-serif;font-size:13px;font-weight:600;z-index:2147483647;box-shadow:0 4px 20px rgba(99,102,241,0.5);cursor:pointer;';
739
+ b.textContent='intelliFont: Click any text to inspect';
740
+ b.onclick=function(){ins.disableClickInspect();b.remove();};
741
+ document.body.appendChild(b);
742
+ };
743
+ document.head.appendChild(i);
744
+ };
745
+ document.head.appendChild(s);
746
+ })();`;
747
+
748
+ document.getElementById('bookmarklet-code').textContent = BOOKMARKLET;
749
+
750
+ function copyBookmarklet() {
751
+ navigator.clipboard.writeText(BOOKMARKLET).then(() => {
752
+ const btn = document.querySelector('.copy-btn');
753
+ btn.textContent = '✅ Copied!';
754
+ setTimeout(() => { btn.textContent = '📋 Copy Bookmarklet'; }, 2000);
755
+ });
756
+ }
757
+ </script>
758
+ </body>
759
+ </html>