codeclone 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl

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.
codeclone/templates.py ADDED
@@ -0,0 +1,1262 @@
1
+ """
2
+ CodeClone — AST and CFG-based code clone detector for Python
3
+ focused on architectural duplication.
4
+
5
+ Copyright (c) 2026 Den Rozhnovskiy
6
+ Licensed under the MIT License.
7
+ """
8
+
9
+ from string import Template
10
+
11
+ FONT_CSS_URL = (
12
+ "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700"
13
+ "&family=JetBrains+Mono:wght@400;500;600&display=swap"
14
+ )
15
+
16
+ REPORT_TEMPLATE = Template(r"""
17
+ <!doctype html>
18
+ <html lang="en" data-theme="dark">
19
+ <head>
20
+ <meta charset="utf-8">
21
+ <meta name="viewport" content="width=device-width, initial-scale=1">
22
+ <title>${title}</title>
23
+
24
+ <!-- Fonts -->
25
+ <link rel="preconnect" href="https://fonts.googleapis.com">
26
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
27
+ <link href="${font_css_url}" rel="stylesheet">
28
+
29
+ <style>
30
+ /* ============================
31
+ CodeClone UI/UX
32
+ ============================ */
33
+
34
+ /* ========== Design Tokens ========== */
35
+ :root {
36
+ /* Brand Colors - Purple/Cyan Identity */
37
+ --brand-purple: #8B5CF6;
38
+ --brand-cyan: #06B6D4;
39
+ --brand-pink: #EC4899;
40
+ --brand-amber: #F59E0B;
41
+
42
+ /* Surface Hierarchy */
43
+ --surface-0: #0A0A0F;
44
+ --surface-1: #141419;
45
+ --surface-2: #1E1E24;
46
+ --surface-3: #28282F;
47
+ --surface-4: #32323A;
48
+
49
+ /* Text */
50
+ --text-primary: #F9FAFB;
51
+ --text-secondary: #D1D5DB;
52
+ --text-tertiary: #9CA3AF;
53
+ --text-muted: #6B7280;
54
+
55
+ /* Borders */
56
+ --border-subtle: #2D2D35;
57
+ --border-default: #3F3F46;
58
+ --border-strong: #52525B;
59
+
60
+ /* Semantic */
61
+ --success: #10B981;
62
+ --warning: #F59E0B;
63
+ --error: #EF4444;
64
+ --info: #3B82F6;
65
+
66
+ /* Gradients */
67
+ --gradient-primary: linear-gradient(
68
+ 135deg,
69
+ var(--brand-purple) 0%,
70
+ var(--brand-cyan) 100%
71
+ );
72
+ --gradient-accent: linear-gradient(
73
+ 135deg,
74
+ var(--brand-pink) 0%,
75
+ var(--brand-amber) 100%
76
+ );
77
+ --gradient-subtle: linear-gradient(
78
+ 180deg,
79
+ transparent 0%,
80
+ rgba(139, 92, 246, 0.05) 100%
81
+ );
82
+ --gradient-mesh:
83
+ radial-gradient(at 0% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%),
84
+ radial-gradient(at 100% 100%, rgba(6, 182, 212, 0.15) 0px, transparent 50%);
85
+
86
+ /* Elevation */
87
+ --elevation-0: none;
88
+ --elevation-1: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4);
89
+ --elevation-2: 0 3px 6px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.3);
90
+ --elevation-3: 0 10px 20px rgba(0, 0, 0, 0.4), 0 3px 6px rgba(0, 0, 0, 0.3);
91
+ --elevation-4: 0 15px 25px rgba(0, 0, 0, 0.45), 0 5px 10px rgba(0, 0, 0, 0.25);
92
+ --elevation-glow: 0 0 20px rgba(139, 92, 246, 0.3);
93
+
94
+ /* Glassmorphism */
95
+ --glass-bg: rgba(20, 20, 25, 0.7);
96
+ --glass-border: rgba(255, 255, 255, 0.1);
97
+ --glass-blur: blur(20px);
98
+
99
+ /* Typography Scale (1.25 ratio) */
100
+ --text-xs: 0.75rem; /* 12px */
101
+ --text-sm: 0.875rem; /* 14px */
102
+ --text-base: 1rem; /* 16px */
103
+ --text-lg: 1.125rem; /* 18px */
104
+ --text-xl: 1.25rem; /* 20px */
105
+ --text-2xl: 1.563rem; /* 25px */
106
+ --text-3xl: 1.953rem; /* 31px */
107
+
108
+ /* Font Families */
109
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
110
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono',
111
+ Menlo, Consolas, monospace;
112
+
113
+ /* Line Heights */
114
+ --leading-tight: 1.25;
115
+ --leading-normal: 1.5;
116
+ --leading-relaxed: 1.75;
117
+
118
+ /* Border Radius */
119
+ --radius-sm: 4px;
120
+ --radius: 8px;
121
+ --radius-lg: 12px;
122
+ --radius-xl: 16px;
123
+ --radius-full: 9999px;
124
+
125
+ /* Transitions */
126
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
127
+ --transition-base: 300ms cubic-bezier(0.4, 0, 0.2, 1);
128
+ --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
129
+ --transition-spring: 600ms cubic-bezier(0.34, 1.56, 0.64, 1);
130
+ }
131
+
132
+ html[data-theme="light"] {
133
+ /* Surface Hierarchy */
134
+ --surface-0: #FFFFFF;
135
+ --surface-1: #F9FAFB;
136
+ --surface-2: #F3F4F6;
137
+ --surface-3: #E5E7EB;
138
+ --surface-4: #D1D5DB;
139
+
140
+ /* Text */
141
+ --text-primary: #111827;
142
+ --text-secondary: #374151;
143
+ --text-tertiary: #6B7280;
144
+ --text-muted: #9CA3AF;
145
+
146
+ /* Borders */
147
+ --border-subtle: #E5E7EB;
148
+ --border-default: #D1D5DB;
149
+ --border-strong: #9CA3AF;
150
+
151
+ /* Elevation */
152
+ --elevation-1: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
153
+ --elevation-2: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
154
+ --elevation-3: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
155
+ --elevation-4: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);
156
+ --elevation-glow: 0 0 20px rgba(139, 92, 246, 0.2);
157
+
158
+ /* Glassmorphism */
159
+ --glass-bg: rgba(249, 250, 251, 0.8);
160
+ --glass-border: rgba(0, 0, 0, 0.1);
161
+ }
162
+
163
+ /* ========== Global Styles ========== */
164
+ * {
165
+ box-sizing: border-box;
166
+ margin: 0;
167
+ padding: 0;
168
+ }
169
+
170
+ html {
171
+ scroll-behavior: smooth;
172
+ }
173
+
174
+ body {
175
+ background: var(--surface-0);
176
+ background-image: var(--gradient-mesh);
177
+ color: var(--text-primary);
178
+ font-family: var(--font-sans);
179
+ font-size: var(--text-base);
180
+ line-height: var(--leading-normal);
181
+ -webkit-font-smoothing: antialiased;
182
+ -moz-osx-font-smoothing: grayscale;
183
+ overflow-x: hidden;
184
+ }
185
+
186
+ ::selection {
187
+ background: rgba(139, 92, 246, 0.3);
188
+ color: var(--text-primary);
189
+ }
190
+
191
+ /* ========== Layout ========== */
192
+ .container {
193
+ max-width: 1400px;
194
+ margin: 0 auto;
195
+ padding: 20px 20px 80px;
196
+ }
197
+
198
+ /* ========== Topbar ========== */
199
+ .topbar {
200
+ position: sticky;
201
+ top: 0;
202
+ z-index: 100;
203
+ background: var(--glass-bg);
204
+ backdrop-filter: var(--glass-blur);
205
+ -webkit-backdrop-filter: var(--glass-blur);
206
+ border-bottom: 1px solid var(--glass-border);
207
+ box-shadow: var(--elevation-2);
208
+ }
209
+
210
+ .topbar-inner {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ height: 64px;
215
+ padding: 0 24px;
216
+ max-width: 1400px;
217
+ margin: 0 auto;
218
+ }
219
+
220
+ .brand {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 12px;
224
+ }
225
+
226
+ .brand h1 {
227
+ font-size: var(--text-xl);
228
+ font-weight: 700;
229
+ background: var(--gradient-primary);
230
+ -webkit-background-clip: text;
231
+ -webkit-text-fill-color: transparent;
232
+ background-clip: text;
233
+ letter-spacing: -0.02em;
234
+ }
235
+
236
+ .brand .sub {
237
+ color: var(--text-tertiary);
238
+ font-size: var(--text-sm);
239
+ background: var(--surface-2);
240
+ padding: 3px 10px;
241
+ border-radius: var(--radius-full);
242
+ font-weight: 600;
243
+ border: 1px solid var(--border-subtle);
244
+ }
245
+
246
+ .top-actions {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 8px;
250
+ }
251
+
252
+ /* ========== Buttons ========== */
253
+ .btn {
254
+ position: relative;
255
+ display: inline-flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ gap: 8px;
259
+ padding: 8px 16px;
260
+ border-radius: var(--radius);
261
+ border: 1px solid var(--border-default);
262
+ background: var(--surface-2);
263
+ color: var(--text-primary);
264
+ cursor: pointer;
265
+ font-size: var(--text-sm);
266
+ font-weight: 500;
267
+ font-family: var(--font-sans);
268
+ transition: all var(--transition-base);
269
+ overflow: hidden;
270
+ white-space: nowrap;
271
+ user-select: none;
272
+ }
273
+
274
+ .btn::before {
275
+ content: '';
276
+ position: absolute;
277
+ inset: 0;
278
+ background: linear-gradient(
279
+ 90deg,
280
+ transparent,
281
+ rgba(255, 255, 255, 0.1),
282
+ transparent
283
+ );
284
+ transform: translateX(-100%);
285
+ transition: transform var(--transition-slow);
286
+ }
287
+
288
+ .btn:hover {
289
+ transform: translateY(-2px);
290
+ box-shadow: var(--elevation-2);
291
+ border-color: var(--border-strong);
292
+ background: var(--surface-3);
293
+ }
294
+
295
+ .btn:hover::before {
296
+ transform: translateX(100%);
297
+ }
298
+
299
+ .btn:active {
300
+ transform: translateY(0);
301
+ box-shadow: var(--elevation-1);
302
+ }
303
+
304
+ .btn:focus-visible {
305
+ outline: 2px solid var(--brand-purple);
306
+ outline-offset: 2px;
307
+ }
308
+
309
+ .btn.ghost {
310
+ background: transparent;
311
+ border-color: transparent;
312
+ padding: 8px;
313
+ }
314
+
315
+ .btn.ghost:hover {
316
+ background: var(--surface-2);
317
+ transform: scale(1.05);
318
+ }
319
+
320
+ .btn.primary {
321
+ background: var(--gradient-primary);
322
+ border-color: transparent;
323
+ color: white;
324
+ font-weight: 600;
325
+ }
326
+
327
+ .btn.primary:hover {
328
+ box-shadow: var(--elevation-glow);
329
+ }
330
+
331
+ /* ========== Form Elements ========== */
332
+ .select {
333
+ padding: 8px 32px 8px 12px;
334
+ height: 36px;
335
+ border-radius: var(--radius);
336
+ border: 1px solid var(--border-default);
337
+ background: var(--surface-2);
338
+ color: var(--text-primary);
339
+ font-size: var(--text-sm);
340
+ font-family: var(--font-sans);
341
+ cursor: pointer;
342
+ transition: all var(--transition-base);
343
+ appearance: none;
344
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' \
345
+ width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239CA3AF' \
346
+ d='M6 8L2 4h8z'/%3E%3C/svg%3E");
347
+ background-repeat: no-repeat;
348
+ background-position: right 12px center;
349
+ }
350
+
351
+ .select:hover {
352
+ border-color: var(--border-strong);
353
+ background-color: var(--surface-3);
354
+ }
355
+
356
+ .select:focus {
357
+ outline: 2px solid var(--brand-purple);
358
+ outline-offset: 2px;
359
+ }
360
+
361
+ /* ========== Section ========== */
362
+ .section {
363
+ margin-top: 48px;
364
+ animation: fadeInUp 0.6s var(--transition-spring);
365
+ }
366
+
367
+ @keyframes fadeInUp {
368
+ from {
369
+ opacity: 0;
370
+ transform: translateY(20px);
371
+ }
372
+ to {
373
+ opacity: 1;
374
+ transform: translateY(0);
375
+ }
376
+ }
377
+
378
+ .section-head {
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 20px;
382
+ margin-bottom: 24px;
383
+ }
384
+
385
+ .section-head h2 {
386
+ font-size: var(--text-2xl);
387
+ font-weight: 700;
388
+ display: flex;
389
+ align-items: center;
390
+ gap: 12px;
391
+ letter-spacing: -0.02em;
392
+ }
393
+
394
+ /* ========== Toolbar ========== */
395
+ .section-toolbar {
396
+ display: flex;
397
+ justify-content: space-between;
398
+ align-items: center;
399
+ gap: 16px;
400
+ padding: 16px;
401
+ background: var(--surface-1);
402
+ border: 1px solid var(--border-subtle);
403
+ border-radius: var(--radius-lg);
404
+ box-shadow: var(--elevation-1);
405
+ }
406
+
407
+ .toolbar-left {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 12px;
411
+ flex: 1;
412
+ }
413
+
414
+ .toolbar-right {
415
+ display: flex;
416
+ align-items: center;
417
+ gap: 12px;
418
+ }
419
+
420
+ @media (max-width: 768px) {
421
+ .section-toolbar {
422
+ flex-direction: column;
423
+ align-items: stretch;
424
+ }
425
+
426
+ .toolbar-left,
427
+ .toolbar-right {
428
+ width: 100%;
429
+ justify-content: space-between;
430
+ }
431
+
432
+ .search-wrap {
433
+ min-width: 0;
434
+ flex: 1;
435
+ }
436
+ }
437
+
438
+ /* ========== Search ========== */
439
+ .search-wrap {
440
+ position: relative;
441
+ display: flex;
442
+ align-items: center;
443
+ gap: 8px;
444
+ padding: 8px 12px;
445
+ border-radius: var(--radius);
446
+ border: 1px solid var(--border-default);
447
+ background: var(--surface-0);
448
+ min-width: 300px;
449
+ transition: all var(--transition-base);
450
+ }
451
+
452
+ .search-wrap:focus-within {
453
+ border-color: var(--brand-purple);
454
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
455
+ background: var(--surface-1);
456
+ }
457
+
458
+ .search-ico {
459
+ color: var(--text-muted);
460
+ display: flex;
461
+ flex-shrink: 0;
462
+ }
463
+
464
+ .search {
465
+ width: 100%;
466
+ border: none;
467
+ outline: none;
468
+ background: transparent;
469
+ color: var(--text-primary);
470
+ font-size: var(--text-sm);
471
+ font-family: var(--font-sans);
472
+ }
473
+
474
+ .search::placeholder {
475
+ color: var(--text-muted);
476
+ }
477
+
478
+ .segmented {
479
+ display: inline-flex;
480
+ background: var(--surface-2);
481
+ padding: 3px;
482
+ border-radius: var(--radius);
483
+ border: 1px solid var(--border-subtle);
484
+ }
485
+
486
+ .btn.seg {
487
+ border: none;
488
+ background: transparent;
489
+ height: 32px;
490
+ font-size: var(--text-sm);
491
+ border-radius: calc(var(--radius) - 3px);
492
+ }
493
+
494
+ .btn.seg:hover {
495
+ background: var(--surface-0);
496
+ box-shadow: var(--elevation-1);
497
+ }
498
+
499
+ /* ========== Pager ========== */
500
+ .pager {
501
+ display: inline-flex;
502
+ align-items: center;
503
+ gap: 8px;
504
+ font-size: var(--text-sm);
505
+ }
506
+
507
+ .page-meta {
508
+ color: var(--text-secondary);
509
+ font-size: var(--text-sm);
510
+ white-space: nowrap;
511
+ min-width: 120px;
512
+ text-align: center;
513
+ font-variant-numeric: tabular-nums;
514
+ }
515
+
516
+ /* ========== Pills/Badges ========== */
517
+ .pill {
518
+ display: inline-flex;
519
+ align-items: center;
520
+ padding: 4px 12px;
521
+ border-radius: var(--radius-full);
522
+ font-size: var(--text-xs);
523
+ font-weight: 600;
524
+ line-height: 1;
525
+ letter-spacing: 0.02em;
526
+ text-transform: uppercase;
527
+ }
528
+
529
+ .pill.small {
530
+ padding: 2px 8px;
531
+ font-size: 10px;
532
+ }
533
+
534
+ .pill-func {
535
+ color: var(--brand-purple);
536
+ background: rgba(139, 92, 246, 0.15);
537
+ border: 1px solid rgba(139, 92, 246, 0.3);
538
+ }
539
+
540
+ .pill-block {
541
+ color: var(--success);
542
+ background: rgba(16, 185, 129, 0.15);
543
+ border: 1px solid rgba(16, 185, 129, 0.3);
544
+ }
545
+
546
+ /* ========== Groups/Cards ========== */
547
+ .group {
548
+ margin-bottom: 20px;
549
+ border: 1px solid var(--border-subtle);
550
+ border-radius: var(--radius-lg);
551
+ background: var(--surface-1);
552
+ box-shadow: var(--elevation-1);
553
+ overflow: hidden;
554
+ transition: all var(--transition-base);
555
+ }
556
+
557
+ .group:hover {
558
+ transform: translateY(-2px);
559
+ box-shadow: var(--elevation-3);
560
+ border-color: var(--brand-purple);
561
+ }
562
+
563
+ .group-head {
564
+ display: flex;
565
+ justify-content: space-between;
566
+ align-items: center;
567
+ padding: 16px 20px;
568
+ background: var(--surface-2);
569
+ border-bottom: 1px solid var(--border-subtle);
570
+ cursor: pointer;
571
+ transition: all var(--transition-fast);
572
+ }
573
+
574
+ .group:hover .group-head {
575
+ background: var(--gradient-subtle);
576
+ }
577
+
578
+ .group-left {
579
+ display: flex;
580
+ align-items: center;
581
+ gap: 12px;
582
+ }
583
+
584
+ .group-title {
585
+ font-weight: 600;
586
+ font-size: var(--text-base);
587
+ color: var(--text-primary);
588
+ }
589
+
590
+ .group-right {
591
+ display: flex;
592
+ align-items: center;
593
+ gap: 12px;
594
+ }
595
+
596
+ .gkey {
597
+ font-family: var(--font-mono);
598
+ font-size: var(--text-xs);
599
+ color: var(--text-tertiary);
600
+ background: var(--surface-0);
601
+ padding: 4px 8px;
602
+ border-radius: var(--radius-sm);
603
+ border: 1px solid var(--border-subtle);
604
+ }
605
+
606
+ /* ========== Chevron Button ========== */
607
+ .chev {
608
+ display: flex;
609
+ align-items: center;
610
+ justify-content: center;
611
+ width: 28px;
612
+ height: 28px;
613
+ border-radius: var(--radius);
614
+ border: 1px solid var(--border-default);
615
+ background: var(--surface-1);
616
+ color: var(--text-muted);
617
+ padding: 0;
618
+ transition: all var(--transition-fast);
619
+ cursor: pointer;
620
+ }
621
+
622
+ .chev:hover {
623
+ color: var(--text-primary);
624
+ border-color: var(--brand-purple);
625
+ background: var(--surface-2);
626
+ transform: scale(1.1);
627
+ }
628
+
629
+ .chev svg {
630
+ transition: transform var(--transition-base);
631
+ }
632
+
633
+ /* ========== Items Container ========== */
634
+ .items {
635
+ padding: 20px;
636
+ background: var(--surface-0);
637
+ }
638
+
639
+ .item-pair {
640
+ display: grid;
641
+ grid-template-columns: 1fr 1fr;
642
+ gap: 20px;
643
+ margin-bottom: 20px;
644
+ min-width: 0;
645
+ }
646
+
647
+ .item-pair:last-child {
648
+ margin-bottom: 0;
649
+ }
650
+
651
+ @media (max-width: 1200px) {
652
+ .item-pair {
653
+ grid-template-columns: 1fr;
654
+ }
655
+ }
656
+
657
+ /* ========== Item Card ========== */
658
+ .item {
659
+ border: 1px solid var(--border-subtle);
660
+ border-radius: var(--radius-lg);
661
+ overflow: hidden;
662
+ display: flex;
663
+ flex-direction: column;
664
+ min-width: 0;
665
+ background: var(--surface-1);
666
+ transition: all var(--transition-base);
667
+ }
668
+
669
+ .item:hover {
670
+ border-color: var(--brand-cyan);
671
+ box-shadow: var(--elevation-2);
672
+ }
673
+
674
+ .item-head {
675
+ padding: 12px 16px;
676
+ background: var(--surface-2);
677
+ border-bottom: 1px solid var(--border-subtle);
678
+ font-size: var(--text-sm);
679
+ font-weight: 600;
680
+ color: var(--brand-purple);
681
+ font-family: var(--font-mono);
682
+ }
683
+
684
+ .item-file {
685
+ padding: 8px 16px;
686
+ background: var(--surface-3);
687
+ border-bottom: 1px solid var(--border-subtle);
688
+ font-family: var(--font-mono);
689
+ font-size: var(--text-xs);
690
+ color: var(--text-tertiary);
691
+ }
692
+
693
+ /* ========== Code Display ========== */
694
+ .codebox {
695
+ position: relative;
696
+ margin: 0;
697
+ padding: 0;
698
+ font-family: var(--font-mono);
699
+ font-size: 13px;
700
+ line-height: 1.6;
701
+ overflow-x: auto;
702
+ overflow-y: auto;
703
+ background: var(--surface-0);
704
+ flex: 1;
705
+ max-width: 100%;
706
+ max-height: 600px;
707
+ }
708
+
709
+ .codebox pre {
710
+ margin: 0;
711
+ padding: 16px;
712
+ white-space: pre;
713
+ word-wrap: normal;
714
+ overflow-wrap: normal;
715
+ min-width: max-content;
716
+ }
717
+
718
+ .codebox code {
719
+ display: block;
720
+ white-space: pre;
721
+ word-wrap: normal;
722
+ overflow-wrap: normal;
723
+ font-family: inherit;
724
+ font-size: inherit;
725
+ color: var(--text-secondary);
726
+ }
727
+
728
+ /* Copy button for code blocks */
729
+ .copy-btn {
730
+ position: absolute;
731
+ top: 12px;
732
+ right: 12px;
733
+ display: flex;
734
+ align-items: center;
735
+ gap: 6px;
736
+ padding: 6px 12px;
737
+ background: var(--surface-2);
738
+ border: 1px solid var(--border-default);
739
+ border-radius: var(--radius);
740
+ color: var(--text-secondary);
741
+ font-size: var(--text-xs);
742
+ font-weight: 500;
743
+ cursor: pointer;
744
+ opacity: 0;
745
+ transition: all var(--transition-base);
746
+ z-index: 10;
747
+ }
748
+
749
+ .codebox:hover .copy-btn {
750
+ opacity: 1;
751
+ }
752
+
753
+ .copy-btn:hover {
754
+ background: var(--surface-3);
755
+ border-color: var(--brand-purple);
756
+ color: var(--text-primary);
757
+ }
758
+
759
+ .copy-btn.copied {
760
+ background: var(--success);
761
+ border-color: var(--success);
762
+ color: white;
763
+ }
764
+
765
+ /* ========== Empty State ========== */
766
+ .empty {
767
+ padding: 80px 20px;
768
+ display: flex;
769
+ justify-content: center;
770
+ align-items: center;
771
+ }
772
+
773
+ .empty-card {
774
+ text-align: center;
775
+ padding: 48px;
776
+ background: var(--surface-1);
777
+ border: 1px solid var(--border-subtle);
778
+ border-radius: var(--radius-xl);
779
+ max-width: 500px;
780
+ box-shadow: var(--elevation-2);
781
+ }
782
+
783
+ .empty-icon {
784
+ color: var(--success);
785
+ margin-bottom: 20px;
786
+ display: flex;
787
+ justify-content: center;
788
+ font-size: 48px;
789
+ }
790
+
791
+ .empty-card h2 {
792
+ font-size: var(--text-xl);
793
+ margin-bottom: 12px;
794
+ color: var(--text-primary);
795
+ }
796
+
797
+ .empty-card p {
798
+ color: var(--text-secondary);
799
+ line-height: var(--leading-relaxed);
800
+ margin-bottom: 8px;
801
+ }
802
+
803
+ .empty-card .muted {
804
+ color: var(--text-muted);
805
+ font-size: var(--text-sm);
806
+ }
807
+
808
+ /* ========== Footer ========== */
809
+ .footer {
810
+ margin-top: 80px;
811
+ text-align: center;
812
+ color: var(--text-muted);
813
+ font-size: var(--text-sm);
814
+ border-top: 1px solid var(--border-subtle);
815
+ padding-top: 32px;
816
+ }
817
+
818
+ /* ========== Toast Notifications ========== */
819
+ .toast-container {
820
+ position: fixed;
821
+ top: 80px;
822
+ right: 20px;
823
+ z-index: 1000;
824
+ display: flex;
825
+ flex-direction: column;
826
+ gap: 12px;
827
+ pointer-events: none;
828
+ }
829
+
830
+ .toast {
831
+ display: flex;
832
+ align-items: center;
833
+ gap: 12px;
834
+ padding: 12px 16px;
835
+ background: var(--glass-bg);
836
+ backdrop-filter: var(--glass-blur);
837
+ border: 1px solid var(--glass-border);
838
+ border-radius: var(--radius);
839
+ box-shadow: var(--elevation-3);
840
+ min-width: 300px;
841
+ transform: translateX(400px);
842
+ opacity: 0;
843
+ transition: all var(--transition-spring);
844
+ pointer-events: auto;
845
+ }
846
+
847
+ .toast.toast-show {
848
+ transform: translateX(0);
849
+ opacity: 1;
850
+ }
851
+
852
+ .toast-icon {
853
+ font-size: var(--text-lg);
854
+ flex-shrink: 0;
855
+ }
856
+
857
+ .toast-message {
858
+ flex: 1;
859
+ font-size: var(--text-sm);
860
+ color: var(--text-primary);
861
+ }
862
+
863
+ .toast-close {
864
+ background: transparent;
865
+ border: none;
866
+ color: var(--text-muted);
867
+ cursor: pointer;
868
+ font-size: var(--text-lg);
869
+ padding: 0;
870
+ width: 24px;
871
+ height: 24px;
872
+ display: flex;
873
+ align-items: center;
874
+ justify-content: center;
875
+ border-radius: var(--radius-sm);
876
+ transition: all var(--transition-fast);
877
+ }
878
+
879
+ .toast-close:hover {
880
+ background: var(--surface-2);
881
+ color: var(--text-primary);
882
+ }
883
+
884
+ .toast-info { border-left: 3px solid var(--info); }
885
+ .toast-success { border-left: 3px solid var(--success); }
886
+ .toast-warning { border-left: 3px solid var(--warning); }
887
+ .toast-error { border-left: 3px solid var(--error); }
888
+
889
+ /* ========== Keyboard Shortcuts Hint ========== */
890
+ .kbd {
891
+ display: inline-flex;
892
+ align-items: center;
893
+ justify-content: center;
894
+ padding: 2px 6px;
895
+ background: var(--surface-2);
896
+ border: 1px solid var(--border-default);
897
+ border-radius: var(--radius-sm);
898
+ font-family: var(--font-mono);
899
+ font-size: var(--text-xs);
900
+ color: var(--text-tertiary);
901
+ box-shadow: 0 1px 0 var(--border-subtle);
902
+ }
903
+
904
+ /* ========== Accessibility ========== */
905
+ @media (prefers-reduced-motion: reduce) {
906
+ *,
907
+ *::before,
908
+ *::after {
909
+ animation-duration: 0.01ms !important;
910
+ animation-iteration-count: 1 !important;
911
+ transition-duration: 0.01ms !important;
912
+ }
913
+ }
914
+
915
+ :focus-visible {
916
+ outline: 2px solid var(--brand-purple);
917
+ outline-offset: 2px;
918
+ }
919
+
920
+ /* ========== Scrollbar ========== */
921
+ ::-webkit-scrollbar {
922
+ width: 10px;
923
+ height: 10px;
924
+ }
925
+
926
+ ::-webkit-scrollbar-track {
927
+ background: var(--surface-1);
928
+ }
929
+
930
+ ::-webkit-scrollbar-thumb {
931
+ background: var(--surface-3);
932
+ border-radius: var(--radius);
933
+ }
934
+
935
+ ::-webkit-scrollbar-thumb:hover {
936
+ background: var(--surface-4);
937
+ }
938
+
939
+ /* ========== Syntax Highlighting (Pygments Override) ========== */
940
+ ${pyg_dark}
941
+ ${pyg_light}
942
+
943
+ /* Custom syntax highlighting enhancements */
944
+ html[data-theme="dark"] .codebox .k,
945
+ html[data-theme="dark"] .codebox .kd,
946
+ html[data-theme="dark"] .codebox .kn { color: #C792EA; } /* Keywords */
947
+ html[data-theme="dark"] .codebox .s,
948
+ html[data-theme="dark"] .codebox .s1,
949
+ html[data-theme="dark"] .codebox .s2 { color: #C3E88D; } /* Strings */
950
+ html[data-theme="dark"] .codebox .nf { color: #82AAFF; } /* Functions */
951
+ html[data-theme="dark"] .codebox .nb { color: #FFCB6B; } /* Builtins */
952
+ html[data-theme="dark"] .codebox .c,
953
+ html[data-theme="dark"] .codebox .c1 {
954
+ color: #546E7A;
955
+ font-style: italic;
956
+ } /* Comments */
957
+
958
+ </style>
959
+ </head>
960
+
961
+ <body>
962
+ <!-- Toast Container -->
963
+ <div class="toast-container"></div>
964
+
965
+ <!-- Topbar -->
966
+ <div class="topbar">
967
+ <div class="topbar-inner">
968
+ <div class="brand">
969
+ <h1>${title}</h1>
970
+ <div class="sub">v${version}</div>
971
+ </div>
972
+ <div class="top-actions">
973
+ <button class="btn" type="button" id="theme-toggle" title="Toggle theme (T)">
974
+ ${icon_theme} Theme
975
+ </button>
976
+ <button class="btn primary" type="button" id="export-btn" title="Export report">
977
+ <svg
978
+ width="16"
979
+ height="16"
980
+ viewBox="0 0 24 24"
981
+ fill="none"
982
+ stroke="currentColor"
983
+ stroke-width="2"
984
+ >
985
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
986
+ <polyline points="7 10 12 15 17 10"></polyline>
987
+ <line x1="12" y1="15" x2="12" y2="3"></line>
988
+ </svg>
989
+ Export
990
+ </button>
991
+ </div>
992
+ </div>
993
+ </div>
994
+
995
+ <!-- Main Content -->
996
+ <div class="container">
997
+ ${empty_state_html}
998
+
999
+ ${func_section}
1000
+ ${block_section}
1001
+
1002
+ <div class="footer">
1003
+ Generated by CodeClone v${version} • Press <kbd class="kbd">/</kbd> to search •
1004
+ <kbd class="kbd">T</kbd> to toggle theme
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <script>
1009
+ (() => {
1010
+ 'use strict';
1011
+
1012
+ // ========== Theme Management ==========
1013
+ const htmlEl = document.documentElement;
1014
+ const btnTheme = document.getElementById('theme-toggle');
1015
+
1016
+ function initTheme() {
1017
+ const stored = localStorage.getItem('codeclone_theme');
1018
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1019
+ const hour = new Date().getHours();
1020
+ const isNight = hour < 7 || hour > 19;
1021
+
1022
+ const theme = stored || (prefersDark || isNight ? 'dark' : 'light');
1023
+ htmlEl.setAttribute('data-theme', theme);
1024
+ }
1025
+
1026
+ function toggleTheme() {
1027
+ const cur = htmlEl.getAttribute('data-theme') || 'dark';
1028
+ const next = cur === 'dark' ? 'light' : 'dark';
1029
+ htmlEl.setAttribute('data-theme', next);
1030
+ localStorage.setItem('codeclone_theme', next);
1031
+ showToast(`Switched to $${next} theme`, 'info');
1032
+ }
1033
+
1034
+ initTheme();
1035
+ btnTheme?.addEventListener('click', toggleTheme);
1036
+
1037
+ // ========== Toast Notifications ==========
1038
+ function showToast(message, type = 'info') {
1039
+ const icons = {
1040
+ info: 'i',
1041
+ success: 'ok',
1042
+ warning: '!',
1043
+ error: 'x'
1044
+ };
1045
+
1046
+ const toast = document.createElement('div');
1047
+ toast.className = `toast toast-$${type}`;
1048
+ toast.innerHTML = `
1049
+ <span class="toast-icon">$${icons[type]}</span>
1050
+ <span class="toast-message">$${message}</span>
1051
+ <button class="toast-close" aria-label="Close">x</button>
1052
+ `;
1053
+
1054
+ const container = document.querySelector('.toast-container');
1055
+ container.appendChild(toast);
1056
+
1057
+ // Trigger animation
1058
+ setTimeout(() => toast.classList.add('toast-show'), 10);
1059
+
1060
+ // Close button
1061
+ toast.querySelector('.toast-close').addEventListener('click', () => {
1062
+ toast.classList.remove('toast-show');
1063
+ setTimeout(() => toast.remove(), 300);
1064
+ });
1065
+
1066
+ // Auto-remove
1067
+ setTimeout(() => {
1068
+ toast.classList.remove('toast-show');
1069
+ setTimeout(() => toast.remove(), 300);
1070
+ }, 4000);
1071
+ }
1072
+
1073
+ // Make showToast global for use in other scripts
1074
+ window.showToast = showToast;
1075
+
1076
+ // ========== Keyboard Shortcuts ==========
1077
+ document.addEventListener('keydown', (e) => {
1078
+ // / - Focus search
1079
+ if (e.key === '/' && !e.metaKey && !e.ctrlKey) {
1080
+ e.preventDefault();
1081
+ const search = document.querySelector('.search');
1082
+ if (search) {
1083
+ search.focus();
1084
+ search.select();
1085
+ }
1086
+ }
1087
+
1088
+ // T - Toggle theme
1089
+ if (e.key === 't' || e.key === 'T') {
1090
+ if (!e.target.matches('input, textarea')) {
1091
+ e.preventDefault();
1092
+ toggleTheme();
1093
+ }
1094
+ }
1095
+
1096
+ // Escape - Clear search / close modals
1097
+ if (e.key === 'Escape') {
1098
+ const search = document.querySelector('.search');
1099
+ if (search && search.value) {
1100
+ search.value = '';
1101
+ search.dispatchEvent(new Event('input', { bubbles: true }));
1102
+ }
1103
+ }
1104
+ });
1105
+
1106
+ // ========== Group Toggle ==========
1107
+ document.querySelectorAll('.group-head').forEach((head) => {
1108
+ head.addEventListener('click', (e) => {
1109
+ if (e.target.closest('button')) return;
1110
+ const btn = head.querySelector('[data-toggle-group]');
1111
+ if (btn) btn.click();
1112
+ });
1113
+ });
1114
+
1115
+ document.querySelectorAll('[data-toggle-group]').forEach((btn) => {
1116
+ btn.addEventListener('click', (e) => {
1117
+ e.stopPropagation();
1118
+ const id = btn.getAttribute('data-toggle-group');
1119
+ const body = document.getElementById('group-body-' + id);
1120
+ if (!body) return;
1121
+
1122
+ const isHidden = body.style.display === 'none';
1123
+ body.style.display = isHidden ? '' : 'none';
1124
+ btn.style.transform = isHidden ? 'rotate(0deg)' : 'rotate(-90deg)';
1125
+ });
1126
+ });
1127
+
1128
+ // ========== Section Management ==========
1129
+ function initSection(sectionId) {
1130
+ const section = document.querySelector(`section[data-section='$${sectionId}']`);
1131
+ if (!section) return;
1132
+
1133
+ const groups = Array.from(
1134
+ section.querySelectorAll(`.group[data-group='$${sectionId}']`)
1135
+ );
1136
+ const searchInput = document.getElementById(`search-$${sectionId}`);
1137
+ const btnPrev = section.querySelector(`[data-prev='$${sectionId}']`);
1138
+ const btnNext = section.querySelector(`[data-next='$${sectionId}']`);
1139
+ const meta = section.querySelector(`[data-page-meta='$${sectionId}']`);
1140
+ const selPageSize = section.querySelector(`[data-pagesize='$${sectionId}']`);
1141
+ const btnClear = section.querySelector(`[data-clear='$${sectionId}']`);
1142
+ const btnCollapseAll = section.querySelector(`[data-collapse-all='$${sectionId}']`);
1143
+ const btnExpandAll = section.querySelector(`[data-expand-all='$${sectionId}']`);
1144
+ const pill = section.querySelector(`[data-count-pill='$${sectionId}']`);
1145
+
1146
+ const state = {
1147
+ q: '',
1148
+ page: 1,
1149
+ pageSize: parseInt(selPageSize?.value || '10', 10),
1150
+ filtered: groups
1151
+ };
1152
+
1153
+ function setGroupVisible(el, yes) {
1154
+ el.style.display = yes ? '' : 'none';
1155
+ }
1156
+
1157
+ function render() {
1158
+ const total = state.filtered.length;
1159
+ const pageSize = Math.max(1, state.pageSize);
1160
+ const pages = Math.max(1, Math.ceil(total / pageSize));
1161
+ state.page = Math.min(Math.max(1, state.page), pages);
1162
+
1163
+ const start = (state.page - 1) * pageSize;
1164
+ const end = Math.min(total, start + pageSize);
1165
+
1166
+ groups.forEach(g => setGroupVisible(g, false));
1167
+ state.filtered.slice(start, end).forEach(g => setGroupVisible(g, true));
1168
+
1169
+ if (meta) meta.textContent = `Page $${state.page} / $${pages} • $${total} groups`;
1170
+ if (pill) pill.textContent = `$${total} groups`;
1171
+
1172
+ if (btnPrev) btnPrev.disabled = state.page <= 1;
1173
+ if (btnNext) btnNext.disabled = state.page >= pages;
1174
+ }
1175
+
1176
+ function applyFilter() {
1177
+ const q = (state.q || '').trim().toLowerCase();
1178
+ if (!q) {
1179
+ state.filtered = groups;
1180
+ } else {
1181
+ state.filtered = groups.filter(g => {
1182
+ const blob = g.getAttribute('data-search') || '';
1183
+ return blob.indexOf(q) !== -1;
1184
+ });
1185
+ }
1186
+ state.page = 1;
1187
+ render();
1188
+ }
1189
+
1190
+ searchInput?.addEventListener('input', (e) => {
1191
+ state.q = e.target.value || '';
1192
+ applyFilter();
1193
+ });
1194
+
1195
+ btnClear?.addEventListener('click', () => {
1196
+ if (searchInput) searchInput.value = '';
1197
+ state.q = '';
1198
+ applyFilter();
1199
+ });
1200
+
1201
+ selPageSize?.addEventListener('change', () => {
1202
+ state.pageSize = parseInt(selPageSize.value || '10', 10);
1203
+ state.page = 1;
1204
+ render();
1205
+ });
1206
+
1207
+ btnPrev?.addEventListener('click', () => {
1208
+ state.page -= 1;
1209
+ render();
1210
+ });
1211
+
1212
+ btnNext?.addEventListener('click', () => {
1213
+ state.page += 1;
1214
+ render();
1215
+ });
1216
+
1217
+ btnCollapseAll?.addEventListener('click', () => {
1218
+ section.querySelectorAll('.items').forEach(b => {
1219
+ b.style.display = 'none';
1220
+ });
1221
+ section.querySelectorAll('[data-toggle-group]').forEach(c => {
1222
+ c.style.transform = 'rotate(-90deg)';
1223
+ });
1224
+ });
1225
+
1226
+ btnExpandAll?.addEventListener('click', () => {
1227
+ section.querySelectorAll('.items').forEach(b => {
1228
+ b.style.display = '';
1229
+ });
1230
+ section.querySelectorAll('[data-toggle-group]').forEach(c => {
1231
+ c.style.transform = 'rotate(0deg)';
1232
+ });
1233
+ });
1234
+
1235
+ render();
1236
+ }
1237
+
1238
+ initSection('functions');
1239
+ initSection('blocks');
1240
+
1241
+ // ========== Export Functionality ==========
1242
+ document.getElementById('export-btn')?.addEventListener('click', () => {
1243
+ showToast('Export functionality coming soon!', 'info');
1244
+ });
1245
+
1246
+ // ========== Page Load Animation ==========
1247
+ document.querySelectorAll('.section').forEach((section, index) => {
1248
+ section.style.animationDelay = `$${index * 0.1}s`;
1249
+ });
1250
+
1251
+ // Show welcome toast
1252
+ setTimeout(() => {
1253
+ const groupCount = document.querySelectorAll('.group').length;
1254
+ if (groupCount > 0) {
1255
+ showToast(`Found $${groupCount} clone groups`, 'success');
1256
+ }
1257
+ }, 500);
1258
+ })();
1259
+ </script>
1260
+ </body>
1261
+ </html>
1262
+ """)