@emeryld/rrroutes-contract 2.2.7 → 2.2.8

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.
package/dist/index.mjs CHANGED
@@ -422,6 +422,7 @@ var CSS_STYLES = `:root {
422
422
  --bg-card: rgba(15, 23, 42, 0.6);
423
423
  --bg-card-hover: rgba(30, 41, 59, 0.7);
424
424
  --bg-glass: rgba(15, 23, 42, 0.90);
425
+ --bg-panel: #0f172a;
425
426
  --border-subtle: rgba(148, 163, 184, 0.2);
426
427
 
427
428
  --text-main: #f8fafc;
@@ -430,6 +431,8 @@ var CSS_STYLES = `:root {
430
431
 
431
432
  --accent-primary: #a855f7;
432
433
  --accent-glow: rgba(168, 85, 247, 0.25);
434
+ --accent-success: #4ade80;
435
+ --accent-error: #f87171;
433
436
 
434
437
  /* Method Colors */
435
438
  --method-get: #4ade80; --method-get-bg: rgba(34, 197, 94, 0.15);
@@ -440,29 +443,27 @@ var CSS_STYLES = `:root {
440
443
 
441
444
  --radius-card: 12px;
442
445
  --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.4);
446
+ --shadow-panel: -5px 0 30px rgba(0,0,0,0.5);
443
447
 
444
448
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
445
449
 
446
450
  /* Tag Pastel Palette */
447
- --tag-0-bg: rgba(254, 202, 202, 0.2); --tag-0-fg: #fca5a5; /* Red */
448
- --tag-1-bg: rgba(253, 230, 138, 0.2); --tag-1-fg: #fcd34d; /* Amber */
449
- --tag-2-bg: rgba(187, 247, 208, 0.2); --tag-2-fg: #86efac; /* Green */
450
- --tag-3-bg: rgba(165, 243, 252, 0.2); --tag-3-fg: #67e8f9; /* Cyan */
451
- --tag-4-bg: rgba(191, 219, 254, 0.2); --tag-4-fg: #93c5fd; /* Blue */
452
- --tag-5-bg: rgba(233, 213, 255, 0.2); --tag-5-fg: #d8b4fe; /* Purple */
453
- --tag-6-bg: rgba(251, 207, 232, 0.2); --tag-6-fg: #f9a8d4; /* Pink */
451
+ --tag-0-bg: rgba(254, 202, 202, 0.2); --tag-0-fg: #fca5a5;
452
+ --tag-1-bg: rgba(253, 230, 138, 0.2); --tag-1-fg: #fcd34d;
453
+ --tag-2-bg: rgba(187, 247, 208, 0.2); --tag-2-fg: #86efac;
454
+ --tag-3-bg: rgba(165, 243, 252, 0.2); --tag-3-fg: #67e8f9;
455
+ --tag-4-bg: rgba(191, 219, 254, 0.2); --tag-4-fg: #93c5fd;
456
+ --tag-5-bg: rgba(233, 213, 255, 0.2); --tag-5-fg: #d8b4fe;
457
+ --tag-6-bg: rgba(251, 207, 232, 0.2); --tag-6-fg: #f9a8d4;
454
458
  }
455
459
 
456
460
  * { box-sizing: border-box; }
457
461
 
458
462
  html, body {
459
- height: 100%;
460
- margin: 0;
461
- padding: 0;
463
+ height: 100%; margin: 0; padding: 0;
462
464
  background-color: var(--bg-root);
463
465
  color: var(--text-main);
464
466
  font-family: system-ui, -apple-system, sans-serif;
465
- -webkit-font-smoothing: antialiased;
466
467
  }
467
468
 
468
469
  body {
@@ -473,345 +474,91 @@ body {
473
474
  }
474
475
 
475
476
  .page {
476
- min-height: 100vh;
477
- padding: 20px 40px 60px;
478
- max-width: 1600px;
479
- margin: 0 auto;
480
- display: flex;
481
- flex-direction: column;
482
- gap: 24px;
477
+ min-height: 100vh; padding: 20px 40px 60px; max-width: 1600px;
478
+ margin: 0 auto; display: flex; flex-direction: column; gap: 24px;
483
479
  }
484
480
 
485
- /* --- Header --- */
486
- .header {
487
- display: flex;
488
- justify-content: space-between;
489
- align-items: flex-end;
490
- padding-bottom: 10px;
491
- border-bottom: 1px solid var(--border-subtle);
492
- }
481
+ /* Header */
482
+ .header { display: flex; justify-content: space-between; padding-bottom: 10px; border-bottom: 1px solid var(--border-subtle); }
493
483
  .header-title h1 {
494
- font-size: 28px;
495
- margin: 0;
496
- font-weight: 700;
497
- letter-spacing: -0.02em;
498
- background: linear-gradient(to right, #e2e8f0, #a855f7);
499
- -webkit-background-clip: text;
500
- -webkit-text-fill-color: transparent;
484
+ font-size: 28px; margin: 0; font-weight: 700; letter-spacing: -0.02em;
485
+ background: linear-gradient(to right, #e2e8f0, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
501
486
  }
502
487
 
503
- /* --- Sticky Controls & Overview --- */
488
+ /* Controls */
504
489
  .controls-container {
505
- position: sticky;
506
- top: 0;
507
- z-index: 50;
508
- background: var(--bg-glass);
509
- backdrop-filter: blur(16px);
510
- border: 1px solid var(--border-subtle);
511
- border-radius: var(--radius-card);
512
- padding: 12px 16px;
513
- display: flex;
514
- flex-direction: column;
515
- gap: 12px;
490
+ position: sticky; top: 0; z-index: 50;
491
+ background: var(--bg-glass); backdrop-filter: blur(16px);
492
+ border: 1px solid var(--border-subtle); border-radius: var(--radius-card);
493
+ padding: 12px 16px; display: flex; flex-direction: column; gap: 12px;
516
494
  box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
517
495
  }
518
-
519
- .schema-indent {
520
- display: inline-block;
521
- }
522
-
523
- .schema-branch {
524
- opacity: 0.6;
525
- font-family: var(--font-mono);
526
- margin-right: 2px;
527
- }
528
-
529
- .schema-meta code {
530
- font-family: var(--font-mono);
531
- font-size: 11px;
532
- }
533
-
534
- /* Top Row: Filters */
535
- .filters-row {
536
- display: flex;
537
- align-items: flex-start;
538
- gap: 16px;
539
- width: 100%;
540
- }
541
-
542
- /* New Wrapper for Search and Methods */
543
- .left-column {
544
- display: flex;
545
- flex-direction: column;
546
- gap: 16px;
547
- flex-basis: 300px;
548
- min-width: 250px;
549
- }
550
-
551
- .search-box {
552
- width: 100%;
553
- position: relative;
554
- }
555
-
556
- /* Tag Filter Group */
557
- .filter-group.tag-filters-container {
558
- flex: 1;
559
- min-width: 200px;
560
- }
561
-
562
- /* Method Filter Group */
563
- .filter-group.method-filters-container {
564
- }
565
-
496
+ .filters-row { display: flex; align-items: flex-start; gap: 16px; width: 100%; }
497
+ .left-column { display: flex; flex-direction: column; gap: 16px; flex-basis: 300px; min-width: 250px; }
498
+ .search-box { width: 100%; position: relative; }
566
499
  .search-input {
567
- width: 100%;
568
- background: rgba(2, 6, 23, 0.6);
569
- border: 1px solid var(--border-subtle);
570
- color: var(--text-main);
571
- padding: 8px 12px 8px 32px;
572
- border-radius: 6px;
573
- font-size: 13px;
574
- transition: all 0.2s;
575
- }
576
- .search-input:focus {
577
- outline: none;
578
- border-color: var(--accent-primary);
579
- box-shadow: 0 0 0 2px var(--accent-glow);
580
- }
581
- .search-icon {
582
- position: absolute;
583
- left: 10px;
584
- top: 50%;
585
- transform: translateY(-50%);
586
- color: var(--text-muted);
587
- pointer-events: none;
588
- font-size: 12px;
500
+ width: 100%; background: rgba(2, 6, 23, 0.6); border: 1px solid var(--border-subtle);
501
+ color: var(--text-main); padding: 8px 12px 8px 32px; border-radius: 6px; font-size: 13px;
589
502
  }
503
+ .search-input:focus { outline: none; border-color: var(--accent-primary); }
504
+ .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 12px; }
505
+ .filter-group { display: flex; flex-direction: column; gap: 6px; }
506
+ .filter-label { font-size: 10px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; }
507
+ .checkbox-group { display: flex; flex-wrap: wrap; gap: 6px; }
590
508
 
591
- .filter-group {
592
- display: flex;
593
- flex-direction: column;
594
- gap: 6px;
595
- }
596
- .filter-label {
597
- font-size: 10px;
598
- text-transform: uppercase;
599
- letter-spacing: 0.05em;
600
- color: var(--text-muted);
601
- font-weight: 700;
602
- }
603
- .checkbox-group {
604
- display: flex;
605
- flex-wrap: wrap;
606
- gap: 6px;
607
- }
608
-
609
- /* Bottom Row: Group Overview Chips */
610
- .overview-row {
611
- display: flex;
612
- flex-wrap: wrap;
613
- gap: 8px;
614
- padding-top: 8px;
615
- border-top: 1px solid var(--border-subtle);
616
- align-items: center;
617
- }
618
- .overview-label {
619
- font-size: 10px;
620
- text-transform: uppercase;
621
- color: var(--text-muted);
622
- font-weight: 700;
623
- margin-right: 4px;
624
- }
625
- .schema-indent {
626
- display: inline-block;
627
- }
628
-
629
- .schema-branch {
630
- opacity: 0.6;
631
- font-family: var(--font-mono);
632
- margin-right: 2px;
633
- }
634
-
635
- /* Chips/Pills */
636
- .pill-checkbox { cursor: pointer; user-select: none; }
509
+ /* Pills */
637
510
  .pill-checkbox input { display: none; }
638
511
  .pill-checkbox span {
639
- display: inline-block;
640
- padding: 3px 8px;
641
- border-radius: 4px;
642
- font-size: 11px;
643
- font-weight: 600;
644
- background: rgba(30, 41, 59, 0.5);
645
- color: var(--text-muted);
646
- border: 1px solid transparent;
647
- transition: all 0.15s;
648
- }
649
- .pill-checkbox input:checked + span {
650
- background: rgba(168, 85, 247, 0.15);
651
- color: var(--accent-primary);
652
- border-color: var(--accent-primary);
653
- }
654
-
655
- /* Tag filter pills */
656
- .tag-filter-pill {
657
- display: inline-block;
658
- padding: 3px 8px;
659
- border-radius: 4px;
660
- font-size: 11px;
661
- font-weight: 600;
662
- background: rgba(30, 41, 59, 0.5);
663
- color: var(--text-muted);
664
- border: 1px solid transparent;
665
- transition: all 0.15s;
666
- }
667
- .pill-checkbox.colored-tag input:checked + .tag-filter-pill {
668
- border-color: var(--accent-primary);
669
- box-shadow: 0 0 0 1px var(--accent-glow);
512
+ display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 600;
513
+ background: rgba(30, 41, 59, 0.5); color: var(--text-muted); border: 1px solid transparent; cursor: pointer;
670
514
  }
515
+ .pill-checkbox input:checked + span { background: rgba(168, 85, 247, 0.15); color: var(--accent-primary); border-color: var(--accent-primary); }
516
+ .pill-checkbox.colored-tag input:checked + .tag-filter-pill { border-color: var(--accent-primary); box-shadow: 0 0 0 1px var(--accent-glow); }
671
517
 
672
- /* Group Jump Links */
518
+ /* Overview */
519
+ .overview-row { display: flex; flex-wrap: wrap; gap: 8px; padding-top: 8px; border-top: 1px solid var(--border-subtle); align-items: center; }
520
+ .overview-label { font-size: 10px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; }
673
521
  .group-chip {
674
- font-size: 11px;
675
- text-decoration: none;
676
- color: var(--text-accent);
677
- background: rgba(168, 85, 247, 0.08);
678
- padding: 3px 8px;
679
- border-radius: 4px;
680
- transition: all 0.2s;
681
- border: 1px solid transparent;
682
- }
683
- .group-chip:hover {
684
- background: rgba(168, 85, 247, 0.2);
685
- border-color: var(--accent-primary);
686
- color: #fff;
522
+ font-size: 11px; text-decoration: none; color: var(--text-accent); background: rgba(168, 85, 247, 0.08);
523
+ padding: 3px 8px; border-radius: 4px; border: 1px solid transparent;
687
524
  }
525
+ .group-chip:hover { background: rgba(168, 85, 247, 0.2); border-color: var(--accent-primary); color: #fff; }
688
526
 
689
- /* --- Main Content --- */
527
+ /* Endpoint Cards */
690
528
  .api-group { margin-bottom: 40px; scroll-margin-top: 180px; }
691
- .group-header {
692
- display: flex;
693
- align-items: center;
694
- justify-content: space-between;
695
- margin-bottom: 16px;
696
- padding-bottom: 8px;
697
- border-bottom: 1px solid var(--border-subtle);
698
- }
699
- .group-title {
700
- font-size: 20px;
701
- font-weight: 600;
702
- color: var(--text-main);
703
- margin: 0;
704
- }
529
+ .group-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; border-bottom: 1px solid var(--border-subtle); }
530
+ .group-title { font-size: 20px; font-weight: 600; margin: 0; padding-bottom: 8px; }
705
531
  .group-actions button {
706
- background: transparent;
707
- border: 1px solid var(--border-subtle);
708
- color: var(--text-muted);
709
- font-size: 11px;
710
- padding: 4px 10px;
711
- border-radius: 4px;
712
- cursor: pointer;
713
- margin-left: 8px;
714
- transition: all 0.2s;
715
- }
716
- .group-actions button:hover {
717
- color: var(--text-main);
718
- background: rgba(255,255,255,0.05);
532
+ background: transparent; border: 1px solid var(--border-subtle); color: var(--text-muted);
533
+ font-size: 11px; padding: 4px 10px; border-radius: 4px; cursor: pointer; margin-left: 8px;
719
534
  }
720
-
721
- .cards-list { display: flex; flex-direction: column; gap: 12px; }
722
-
723
- /* --- Endpoint Card --- */
724
535
  .endpoint-card {
725
- background: var(--bg-card);
726
- border: 1px solid var(--border-subtle);
727
- border-radius: var(--radius-card);
728
- overflow: hidden;
729
- transition: all 0.2s ease;
730
- cursor: pointer;
731
- backdrop-filter: blur(10px);
732
- }
733
- .endpoint-card:hover {
734
- background: var(--bg-card-hover);
735
- border-color: rgba(148, 163, 184, 0.4);
736
- box-shadow: var(--shadow-card);
737
- }
738
- .endpoint-card[data-expanded="true"] {
739
- background: rgba(30, 41, 59, 0.6);
740
- border-color: var(--accent-primary);
741
- box-shadow: 0 0 0 1px var(--accent-glow), var(--shadow-card);
742
- }
743
-
744
- /* Card Header */
745
- .card-header {
746
- padding: 12px 16px;
747
- display: flex;
748
- align-items: center;
749
- gap: 12px;
750
- flex-wrap: wrap;
536
+ background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-card);
537
+ overflow: hidden; transition: all 0.2s ease; margin-bottom: 12px;
751
538
  }
539
+ .endpoint-card:hover { border-color: rgba(148, 163, 184, 0.4); box-shadow: var(--shadow-card); }
540
+ .card-header { padding: 12px 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; cursor: pointer; }
541
+ .header-spacer { flex: 1; }
542
+ .card-body { display: none; padding: 0 16px 16px; border-top: 1px solid var(--border-subtle); margin-top: 4px; }
543
+ .endpoint-card[data-expanded="true"] .card-body { display: block; animation: slideDown 0.2s ease-out; }
544
+ .endpoint-card[data-expanded="true"] .expand-icon { transform: rotate(180deg); color: var(--text-main); }
545
+ @keyframes slideDown { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
752
546
 
753
- /* Item 1: Method */
754
- .method-badge {
755
- font-size: 11px;
756
- font-weight: 800;
757
- text-transform: uppercase;
758
- padding: 4px 8px;
759
- border-radius: 4px;
760
- min-width: 55px;
761
- text-align: center;
762
- letter-spacing: 0.05em;
763
- flex-shrink: 0;
764
- }
547
+ /* Method Badges */
548
+ .method-badge { font-size: 11px; font-weight: 800; text-transform: uppercase; padding: 4px 8px; border-radius: 4px; min-width: 55px; text-align: center; }
765
549
  .m-GET { background: var(--method-get-bg); color: var(--method-get); border: 1px solid rgba(74, 222, 128, 0.3); }
766
550
  .m-POST { background: var(--method-post-bg); color: var(--method-post); border: 1px solid rgba(96, 165, 250, 0.3); }
767
551
  .m-PUT { background: var(--method-put-bg); color: var(--method-put); border: 1px solid rgba(250, 204, 21, 0.3); }
768
552
  .m-PATCH { background: var(--method-patch-bg); color: var(--method-patch); border: 1px solid rgba(45, 212, 191, 0.3); }
769
553
  .m-DELETE { background: var(--method-delete-bg); color: var(--method-delete); border: 1px solid rgba(248, 113, 113, 0.3); }
770
554
 
771
- /* Item 2: Path */
772
- .path-container {
773
- font-family: var(--font-mono);
774
- font-size: 13px;
775
- color: var(--text-main);
776
- display: inline-flex;
777
- align-items: center;
778
- gap: 8px;
779
- cursor: pointer;
780
- padding: 2px 6px;
781
- border-radius: 4px;
782
- transition: background 0.2s;
783
- }
784
- .path-container:hover {
785
- background: rgba(255,255,255,0.05);
786
- }
787
- .path-text {
788
- font-weight: 500;
789
- }
790
- .copy-icon {
791
- opacity: 0;
792
- color: var(--text-muted);
793
- font-size: 10px;
794
- transition: opacity 0.2s;
795
- }
555
+ .path-container { font-family: var(--font-mono); font-size: 13px; display: inline-flex; align-items: center; gap: 8px; padding: 2px 6px; border-radius: 4px; }
556
+ .path-container:hover { background: rgba(255,255,255,0.05); }
557
+ .copy-icon { opacity: 0; font-size: 10px; }
796
558
  .path-container:hover .copy-icon { opacity: 1; }
797
-
798
- /* Item 3: Tags */
799
- .tags-container {
800
- display: flex;
801
- align-items: center;
802
- gap: 6px;
803
- flex-wrap: wrap;
804
- }
805
- .status-badge {
806
- font-size: 9px;
807
- text-transform: uppercase;
808
- padding: 2px 6px;
809
- border-radius: 3px;
810
- border: 1px solid transparent;
811
- font-weight: 600;
812
- letter-spacing: 0.03em;
813
- }
814
- /* Tag Colors mapped via JS classes */
559
+ .tags-container { display: flex; gap: 6px; }
560
+ .status-badge { font-size: 9px; text-transform: uppercase; padding: 2px 6px; border-radius: 3px; font-weight: 600; }
561
+ /* Tag colors */
815
562
  .tag-0 { background: var(--tag-0-bg); color: var(--tag-0-fg); border-color: var(--tag-0-fg); }
816
563
  .tag-1 { background: var(--tag-1-bg); color: var(--tag-1-fg); border-color: var(--tag-1-fg); }
817
564
  .tag-2 { background: var(--tag-2-bg); color: var(--tag-2-fg); border-color: var(--tag-2-fg); }
@@ -819,651 +566,784 @@ body {
819
566
  .tag-4 { background: var(--tag-4-bg); color: var(--tag-4-fg); border-color: var(--tag-4-fg); }
820
567
  .tag-5 { background: var(--tag-5-bg); color: var(--tag-5-fg); border-color: var(--tag-5-fg); }
821
568
  .tag-6 { background: var(--tag-6-bg); color: var(--tag-6-fg); border-color: var(--tag-6-fg); }
822
-
823
569
  .not-implemented { border-color: var(--method-delete); color: var(--method-delete); background: var(--method-delete-bg); }
824
570
 
825
- /* Spacer to push expand icon to right */
826
- .header-spacer { flex: 1; }
827
-
828
- .expand-icon {
829
- color: var(--text-muted);
830
- transition: transform 0.3s ease;
831
- font-size: 10px;
832
- margin-left: 8px;
833
- }
834
- .endpoint-card[data-expanded="true"] .expand-icon {
835
- transform: rotate(180deg);
836
- color: var(--text-main);
837
- }
838
-
839
- /* Card Body */
840
- .card-body {
841
- display: none;
842
- padding: 0 16px 16px;
843
- border-top: 1px solid var(--border-subtle);
844
- margin-top: 4px;
845
- cursor: default;
846
- }
847
- .endpoint-card[data-expanded="true"] .card-body {
848
- display: block;
849
- animation: slideDown 0.2s ease-out;
850
- }
851
- @keyframes slideDown {
852
- from { opacity: 0; transform: translateY(-5px); }
853
- to { opacity: 1; transform: translateY(0); }
571
+ /* Use Endpoint Button */
572
+ .btn-try-it {
573
+ background: rgba(168, 85, 247, 0.1); border: 1px solid rgba(168, 85, 247, 0.4);
574
+ color: var(--text-accent); font-size: 11px; font-weight: 600; padding: 4px 12px;
575
+ border-radius: 12px; cursor: pointer; transition: all 0.2s; margin-right: 12px;
576
+ display: flex; align-items: center; gap: 6px;
854
577
  }
578
+ .btn-try-it:hover { background: rgba(168, 85, 247, 0.25); color: #fff; box-shadow: 0 0 10px var(--accent-glow); }
579
+ .btn-try-it .play-icon { font-size: 10px; }
855
580
 
581
+ /* Section Titles */
856
582
  .section-block { margin-top: 18px; }
857
- .section-title {
858
- font-size: 11px;
859
- text-transform: uppercase;
860
- letter-spacing: 0.1em;
861
- color: var(--text-muted);
862
- margin-bottom: 8px;
863
- border-bottom: 1px solid var(--border-subtle);
864
- padding-bottom: 4px;
865
- }
866
- .summary-text { font-size: 14px; color: var(--text-main); line-height: 1.5; }
583
+ .section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 8px; border-bottom: 1px solid var(--border-subtle); padding-bottom: 4px; }
867
584
 
868
- .description-text {
869
- font-size: 12px;
870
- opacity: 0.7;
871
- }
872
-
873
- /* Tables */
585
+ /* Tables & Schema */
874
586
  .schema-table { width: 100%; border-collapse: collapse; font-size: 12px; }
875
- .schema-table th {
876
- text-align: left; color: var(--text-muted); font-weight: 600;
877
- padding: 6px 8px; border-bottom: 1px solid var(--border-subtle); background: rgba(0,0,0,0.2);
878
- }
879
- .schema-table td {
880
- padding: 6px 8px; border-bottom: 1px solid var(--border-subtle); vertical-align: top; color: var(--text-muted);
881
- }
882
- .col-name { font-family: var(--font-mono); color: var(--text-accent) !important; width: 20%; }
587
+ .schema-table th { text-align: left; color: var(--text-muted); font-weight: 600; padding: 6px 8px; border-bottom: 1px solid var(--border-subtle); background: rgba(0,0,0,0.2); }
588
+ .schema-table td { padding: 6px 8px; border-bottom: 1px solid var(--border-subtle); vertical-align: top; color: var(--text-muted); }
589
+ .col-name { font-family: var(--font-mono); color: var(--text-accent) !important; width: 25%; }
883
590
  .col-type { font-family: var(--font-mono); color: #93c5fd !important; width: 15%; }
884
591
  .req-badge { font-size: 9px; text-transform: uppercase; padding: 2px 4px; border-radius: 2px; }
885
592
  .req-true { color: #4ade80; background: rgba(74, 222, 128, 0.1); }
886
593
  .req-false { color: #94a3b8; background: rgba(148, 163, 184, 0.1); }
887
-
888
- .schema-subtitle {
889
- font-size: 11px;
890
- font-weight: 600;
891
- margin-bottom: 4px;
892
- color: #64748b;
594
+ .schema-toggle {
595
+ background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 10px;
596
+ padding: 0 4px; margin-right: 4px; width: 16px; text-align: center;
597
+ }
598
+ .schema-toggle:hover { color: var(--text-main); }
599
+ tr[data-hidden="true"] { display: none; }
600
+
601
+ /* --- Playground Panel --- */
602
+ .playground-overlay {
603
+ position: fixed; top: 0; right: 0; bottom: 0; width: 600px; max-width: 90vw;
604
+ background: var(--bg-panel); border-left: 1px solid var(--border-subtle);
605
+ box-shadow: var(--shadow-panel); z-index: 100;
606
+ transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
607
+ display: flex; flex-direction: column;
608
+ }
609
+ .playground-overlay.open { transform: translateX(0); }
610
+
611
+ .pg-header {
612
+ padding: 16px; border-bottom: 1px solid var(--border-subtle);
613
+ display: flex; justify-content: space-between; align-items: center;
614
+ background: var(--bg-glass);
893
615
  }
894
-
895
- .empty-message { text-align: center; padding: 40px; color: var(--text-muted); }
616
+ .pg-title { font-size: 16px; font-weight: 700; color: var(--text-main); }
617
+ .pg-close { background: transparent; border: none; color: var(--text-muted); font-size: 20px; cursor: pointer; }
618
+ .pg-close:hover { color: var(--text-main); }
619
+
620
+ .pg-content { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 24px; }
621
+
622
+ .pg-section { display: flex; flex-direction: column; gap: 8px; }
623
+ .pg-label { font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 700; letter-spacing: 0.05em; }
624
+ .pg-input, .pg-textarea {
625
+ background: rgba(2, 6, 23, 0.4); border: 1px solid var(--border-subtle);
626
+ color: var(--text-main); padding: 8px; border-radius: 6px; font-family: var(--font-mono); font-size: 12px;
627
+ }
628
+ .pg-input:focus, .pg-textarea:focus { outline: none; border-color: var(--accent-primary); }
629
+ .pg-textarea { min-height: 150px; resize: vertical; }
630
+
631
+ /* Param Grid */
632
+ .param-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
633
+ .param-row { display: contents; }
634
+
635
+ .pg-footer {
636
+ padding: 16px; border-top: 1px solid var(--border-subtle); background: var(--bg-glass);
637
+ display: flex; justify-content: flex-end; gap: 12px;
638
+ }
639
+ .btn-send {
640
+ background: var(--accent-primary); color: white; border: none; padding: 8px 24px;
641
+ border-radius: 6px; font-weight: 600; cursor: pointer; transition: opacity 0.2s;
642
+ }
643
+ .btn-send:hover { opacity: 0.9; }
644
+ .btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
645
+
646
+ /* Response View */
647
+ .response-box { background: rgba(0,0,0,0.3); border-radius: 8px; border: 1px solid var(--border-subtle); overflow: hidden; }
648
+ .response-meta {
649
+ background: rgba(255,255,255,0.05); padding: 8px 12px;
650
+ display: flex; gap: 12px; font-size: 11px; font-family: var(--font-mono); border-bottom: 1px solid var(--border-subtle);
651
+ }
652
+ .meta-item { display: flex; align-items: center; gap: 4px; }
653
+ .status-ok { color: var(--accent-success); }
654
+ .status-err { color: var(--accent-error); }
655
+ .response-body { padding: 12px; overflow-x: auto; font-family: var(--font-mono); font-size: 12px; }
656
+
657
+ /* JSON Tree */
658
+ .json-tree { font-family: var(--font-mono); line-height: 1.6; }
659
+ .jt-obj, .jt-arr { margin-left: 14px; }
660
+ .jt-key { color: #93c5fd; margin-right: 4px; }
661
+ .jt-str { color: #86efac; }
662
+ .jt-num { color: #fca5a5; }
663
+ .jt-bool { color: #fcd34d; }
664
+ .jt-null { color: #94a3b8; }
665
+ .jt-toggle {
666
+ display: inline-block; width: 12px; text-align: center; cursor: pointer; color: var(--text-muted);
667
+ user-select: none; margin-left: -14px;
668
+ }
669
+ .jt-toggle:hover { color: var(--text-main); }
670
+ .jt-collapsed .jt-content { display: none; }
671
+ .jt-collapsed::after { content: " ... "; color: var(--text-muted); font-size: 10px; }
896
672
  `;
897
673
  var DOCS_JS = `
898
674
  (function() {
899
675
  let leaves = [];
676
+ let activeLeaf = null;
677
+ let activeRequest = {
678
+ pathParams: {},
679
+ queryParams: {},
680
+ body: ''
681
+ };
682
+
683
+ // --- Initialization ---
900
684
  try {
901
685
  leaves = JSON.parse(document.getElementById('leaf-data').textContent || '[]');
902
686
  } catch(e) { console.error('Failed to parse docs', e); }
903
687
 
904
- // State
905
- const filters = {
906
- search: '',
907
- methods: new Set(),
908
- tags: new Set()
909
- };
688
+ const filters = { search: '', methods: new Set(), tags: new Set() };
910
689
 
911
- // DOM Elements
690
+ // Elements
912
691
  const elRouteList = document.getElementById('routeList');
913
692
  const elOverview = document.getElementById('overviewList');
914
693
  const elSearch = document.getElementById('searchInput');
915
694
  const elMethodFilters = document.getElementById('methodFilters');
916
695
  const elTagFilters = document.getElementById('tagFilters');
917
696
 
918
- // Constants
919
- const PALETTE_SIZE = 7; // matches .tag-0 to .tag-6 CSS
697
+ // Playground Elements
698
+ let elPlayground, elPgTitle, elPgContent, elPgClose, elPgSend, elPgMethod, elPgUrlPreview, elPgBaseUrl;
920
699
 
921
- // Initialization
922
700
  function init() {
701
+ injectPlaygroundHTML();
923
702
  populateFilters();
924
703
  setupGlobalListeners();
704
+ setupPlaygroundListeners();
925
705
  render();
926
706
  }
927
707
 
928
- // Tag Coloring Logic
929
- function getTagColorClass(tagName) {
930
- if (tagName === 'not-implemented') return 'not-implemented';
931
- let hash = 0;
932
- for (let i = 0; i < tagName.length; i++) {
933
- hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
934
- }
935
- const index = Math.abs(hash % PALETTE_SIZE);
936
- return 'tag-' + index;
708
+ function injectPlaygroundHTML() {
709
+ const div = document.createElement('div');
710
+ div.className = 'playground-overlay';
711
+ div.id = 'playgroundPanel';
712
+ div.innerHTML =
713
+ '<div class="pg-header">' +
714
+ '<div class="pg-title"><span id="pgMethod" class="method-badge">GET</span> <span id="pgTitle">Endpoint</span></div>' +
715
+ '<button class="pg-close" id="pgClose">&times;</button>' +
716
+ '</div>' +
717
+ '<div class="pg-content" id="pgContent">' +
718
+ // Dynamic content goes here
719
+ '</div>' +
720
+ '<div class="pg-footer">' +
721
+ '<button class="btn-send" id="pgSend">Send Request</button>' +
722
+ '</div>';
723
+ document.body.appendChild(div);
724
+
725
+ elPlayground = document.getElementById('playgroundPanel');
726
+ elPgTitle = document.getElementById('pgTitle');
727
+ elPgContent = document.getElementById('pgContent');
728
+ elPgClose = document.getElementById('pgClose');
729
+ elPgSend = document.getElementById('pgSend');
730
+ elPgMethod = document.getElementById('pgMethod');
937
731
  }
938
732
 
939
- // Custom Group Sort: Alpha, but UNGROUPED last
940
- function sortGroups(a, b) {
941
- const nameA = a.toUpperCase();
942
- const nameB = b.toUpperCase();
943
- if (nameA === 'UNGROUPED') return 1;
944
- if (nameB === 'UNGROUPED') return -1;
945
- return nameA.localeCompare(nameB);
946
- }
733
+ // --- Filtering & Rendering ---
947
734
 
948
735
  function populateFilters() {
949
736
  if (!elMethodFilters || !elTagFilters) return;
950
-
951
- // Extract unique values
952
737
  const allMethods = new Set(leaves.map(l => (l.method || 'GET').toUpperCase()));
953
738
  const allTags = new Set(leaves.flatMap(l => (l.cfg && l.cfg.tags) ? l.cfg.tags : []));
954
739
 
955
- // Setup Method Checkboxes
956
740
  const sortedMethods = Array.from(allMethods).sort();
957
- elMethodFilters.innerHTML = sortedMethods.map(m => (
958
- '<label class="pill-checkbox">' +
959
- '<input type="checkbox" value="' + escapeAttr(m) + '" checked>' +
960
- '<span>' + escapeHtml(m) + '</span>' +
961
- '</label>'
962
- )).join('');
741
+ elMethodFilters.innerHTML = sortedMethods.map(m =>
742
+ '<label class="pill-checkbox"><input type="checkbox" value="'+escapeAttr(m)+'" checked><span>'+escapeHtml(m)+'</span></label>'
743
+ ).join('');
963
744
  sortedMethods.forEach(m => filters.methods.add(m));
964
745
 
965
- // Attach listeners to method checkboxes
966
- elMethodFilters.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
967
- cb.addEventListener('change', updateFilters);
968
- });
969
-
970
- // Setup Tag Checkboxes
971
746
  if (allTags.size > 0) {
972
- elTagFilters.innerHTML = Array.from(allTags).sort().map(t => {
973
- const colorClass = getTagColorClass(t);
974
- return (
975
- '<label class="pill-checkbox colored-tag">' +
976
- '<input type="checkbox" value="' + escapeAttr(t) + '">' +
977
- '<span class="tag-filter-pill ' + colorClass + '">' + escapeHtml(t) + '</span>' +
978
- '</label>'
979
- );
980
- }).join('');
981
-
982
- // Attach listeners to tag checkboxes
983
- elTagFilters.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
984
- cb.addEventListener('change', updateFilters);
985
- });
747
+ elTagFilters.innerHTML = Array.from(allTags).sort().map(t =>
748
+ '<label class="pill-checkbox colored-tag"><input type="checkbox" value="'+escapeAttr(t)+'"><span class="tag-filter-pill '+getTagColorClass(t)+'">'+escapeHtml(t)+'</span></label>'
749
+ ).join('');
986
750
  }
751
+
752
+ document.querySelectorAll('#methodFilters input, #tagFilters input').forEach(cb =>
753
+ cb.addEventListener('change', updateFilters)
754
+ );
987
755
  }
988
756
 
989
757
  function updateFilters() {
990
- if (elSearch) {
991
- filters.search = elSearch.value.toLowerCase();
992
- }
993
-
758
+ if (elSearch) filters.search = elSearch.value.toLowerCase();
994
759
  filters.methods.clear();
995
- if (elMethodFilters) {
996
- elMethodFilters.querySelectorAll('input[type="checkbox"]:checked').forEach(function(cb) {
997
- filters.methods.add(cb.value);
998
- });
999
- }
1000
-
760
+ elMethodFilters.querySelectorAll('input:checked').forEach(cb => filters.methods.add(cb.value));
1001
761
  filters.tags.clear();
1002
- if (elTagFilters) {
1003
- elTagFilters.querySelectorAll('input[type="checkbox"]:checked').forEach(function(cb) {
1004
- filters.tags.add(cb.value);
1005
- });
1006
- }
1007
-
762
+ elTagFilters.querySelectorAll('input:checked').forEach(cb => filters.tags.add(cb.value));
1008
763
  render();
1009
764
  }
1010
765
 
1011
766
  function setupGlobalListeners() {
1012
- if (elSearch) {
1013
- elSearch.addEventListener('input', updateFilters);
1014
- }
767
+ if (elSearch) elSearch.addEventListener('input', updateFilters);
768
+
769
+ // Event Delegation for Card interactions
770
+ document.body.addEventListener('click', function(e) {
771
+ const target = e.target;
772
+
773
+ // Try It Button
774
+ const btnTry = target.closest('.btn-try-it');
775
+ if (btnTry) {
776
+ e.preventDefault(); e.stopPropagation();
777
+ const index = parseInt(btnTry.getAttribute('data-index'), 10);
778
+ openPlayground(leaves[index]);
779
+ return;
780
+ }
781
+
782
+ // Expand/Collapse Card
783
+ const cardHeader = target.closest('.card-header');
784
+ if (cardHeader) {
785
+ // Don't toggle if clicked on path (copy) or tags
786
+ if (target.closest('.path-container') || target.closest('.tags-container')) return;
787
+ const card = cardHeader.closest('.endpoint-card');
788
+ const expanded = card.getAttribute('data-expanded') === 'true';
789
+ card.setAttribute('data-expanded', String(!expanded));
790
+ return;
791
+ }
792
+
793
+ // Schema Toggles
794
+ const toggleBtn = target.closest('.schema-toggle');
795
+ if (toggleBtn) {
796
+ e.preventDefault(); e.stopPropagation();
797
+ toggleSchemaRow(toggleBtn);
798
+ return;
799
+ }
800
+
801
+ // Schema Expand All / Collapse All
802
+ if (target.matches('.schema-actions button')) {
803
+ e.preventDefault(); e.stopPropagation();
804
+ const table = target.closest('.section-block').querySelector('table');
805
+ const action = target.getAttribute('data-action');
806
+ toggleAllSchema(table, action === 'expand');
807
+ return;
808
+ }
809
+
810
+ // Copy Path
811
+ const pathContainer = target.closest('.path-container');
812
+ if (pathContainer) {
813
+ e.preventDefault(); e.stopPropagation();
814
+ copyText(pathContainer.getAttribute('data-path'));
815
+ return;
816
+ }
817
+
818
+ // Group Chips
819
+ if (target.matches('.group-chip')) {
820
+ e.preventDefault();
821
+ scrollToGroup(target.getAttribute('data-group'));
822
+ return;
823
+ }
824
+
825
+ // Group Expand/Collapse
826
+ if (target.matches('.group-actions button')) {
827
+ toggleGroup(target.getAttribute('data-group'), target.getAttribute('data-action') === 'expand');
828
+ return;
829
+ }
830
+
831
+ // JSON Tree Toggles
832
+ if (target.classList.contains('jt-toggle')) {
833
+ e.stopPropagation();
834
+ target.parentElement.classList.toggle('jt-collapsed');
835
+ target.textContent = target.parentElement.classList.contains('jt-collapsed') ? '\u25B6' : '\u25BC';
836
+ }
837
+ });
1015
838
  }
1016
839
 
1017
- // Core Render Logic
1018
840
  function render() {
1019
841
  if (!elRouteList || !elOverview) return;
1020
842
 
1021
- // 1. Filter Data
1022
- const filtered = leaves.filter(function(leaf) {
843
+ const filtered = leaves.map((leaf, idx) => ({ ...leaf, originalIndex: idx })).filter(leaf => {
1023
844
  const m = (leaf.method || 'GET').toUpperCase();
1024
- const t = (leaf.cfg && leaf.cfg.tags) ? leaf.cfg.tags : [];
845
+ const t = (leaf.cfg.tags || []);
1025
846
  const path = (leaf.path || '').toLowerCase();
1026
- const summary = (leaf.cfg && leaf.cfg.summary || '').toLowerCase();
847
+ const summary = (leaf.cfg.summary || '').toLowerCase();
1027
848
 
1028
849
  if (!filters.methods.has(m)) return false;
1029
- if (filters.tags.size > 0 && !t.some(function(tag) { return filters.tags.has(tag); })) return false;
850
+ if (filters.tags.size > 0 && !t.some(tag => filters.tags.has(tag))) return false;
1030
851
  if (filters.search && !path.includes(filters.search) && !summary.includes(filters.search)) return false;
1031
-
1032
852
  return true;
1033
853
  });
1034
854
 
1035
855
  if (filtered.length === 0) {
1036
- elRouteList.innerHTML = '<div class="empty-message">No endpoints match your filters.</div>';
856
+ elRouteList.innerHTML = '<div style="text-align:center; padding:40px; color:var(--text-muted)">No endpoints match filters.</div>';
1037
857
  elOverview.innerHTML = '';
1038
858
  return;
1039
859
  }
1040
860
 
1041
- // 2. Group Data
1042
861
  const groups = {};
1043
- filtered.forEach(function(leaf) {
1044
- const gName = (leaf.cfg && leaf.cfg.docsGroup) ? leaf.cfg.docsGroup : 'UNGROUPED';
1045
- if (!groups[gName]) groups[gName] = [];
1046
- groups[gName].push(leaf);
862
+ filtered.forEach(leaf => {
863
+ const g = leaf.cfg.docsGroup || 'UNGROUPED';
864
+ if (!groups[g]) groups[g] = [];
865
+ groups[g].push(leaf);
1047
866
  });
1048
867
 
1049
- const sortedGroupNames = Object.keys(groups).sort(sortGroups);
1050
-
1051
- // 3. Render Overview Chips
1052
- elOverview.innerHTML = (
1053
- '<span class="overview-label">JUMP TO:</span>' +
1054
- sortedGroupNames.map(function(gName) {
1055
- return (
1056
- '<a href="#group-' + escapeAttr(gName) + '" ' +
1057
- 'class="group-chip" data-group="' + escapeAttr(gName) + '">' +
1058
- escapeHtml(gName) +
1059
- '</a>'
1060
- );
1061
- }).join('')
1062
- );
1063
-
1064
- // 4. Render Groups & Cards
1065
- const html = sortedGroupNames.map(function(gName) {
1066
- const routes = groups[gName];
1067
- routes.sort(function(a, b) {
1068
- const pA = a.path || '';
1069
- const pB = b.path || '';
1070
- if (pA < pB) return -1;
1071
- if (pA > pB) return 1;
1072
- return (a.method || '').localeCompare(b.method || '');
1073
- });
868
+ const sortedGroups = Object.keys(groups).sort((a,b) => {
869
+ if (a === 'UNGROUPED') return 1; if (b === 'UNGROUPED') return -1;
870
+ return a.localeCompare(b);
871
+ });
1074
872
 
1075
- return (
1076
- '<div class="api-group" id="group-' + escapeAttr(gName) + '">' +
1077
- '<div class="group-header">' +
1078
- '<h2 class="group-title">' + escapeHtml(gName) + '</h2>' +
1079
- '<div class="group-actions">' +
1080
- '<button type="button" data-group="' + escapeAttr(gName) + '" data-action="expand">Expand All</button>' +
1081
- '<button type="button" data-group="' + escapeAttr(gName) + '" data-action="collapse">Collapse All</button>' +
1082
- '</div>' +
1083
- '</div>' +
1084
- '<div class="cards-list">' +
1085
- routes.map(renderCard).join('') +
1086
- '</div>' +
1087
- '</div>'
1088
- );
873
+ // Render Overview
874
+ elOverview.innerHTML = '<span class="overview-label">JUMP TO:</span>' +
875
+ sortedGroups.map(g => '<a href="#group-'+escapeAttr(g)+'" class="group-chip" data-group="'+escapeAttr(g)+'">'+escapeHtml(g)+'</a>').join('');
876
+
877
+ // Render Cards
878
+ elRouteList.innerHTML = sortedGroups.map(g => {
879
+ const routes = groups[g].sort((a,b) => (a.path||'').localeCompare(b.path||''));
880
+ return '<div class="api-group" id="group-'+escapeAttr(g)+'">' +
881
+ '<div class="group-header"><h2 class="group-title">'+escapeHtml(g)+'</h2>' +
882
+ '<div class="group-actions"><button data-group="'+escapeAttr(g)+'" data-action="expand">Expand All</button><button data-group="'+escapeAttr(g)+'" data-action="collapse">Collapse All</button></div></div>' +
883
+ '<div class="cards-list">' + routes.map(renderCard).join('') + '</div></div>';
1089
884
  }).join('');
1090
-
1091
- elRouteList.innerHTML = html;
1092
-
1093
- // Wire up interactions that depend on rendered DOM
1094
- wireOverviewInteractions();
1095
- wireCardInteractions();
1096
885
  }
1097
886
 
1098
- // Card Renderer
1099
887
  function renderCard(leaf) {
1100
- const method = (leaf.method || 'GET').toUpperCase();
1101
- const path = leaf.path || '/';
1102
- const cfg = leaf.cfg || {};
1103
- const summary = cfg.summary || '';
1104
- const description = cfg.description || '';
888
+ const { method, path, cfg, originalIndex } = leaf;
1105
889
  const tags = cfg.tags || [];
890
+ const methodUc = (method || 'GET').toUpperCase();
1106
891
 
1107
- const tagBadges = tags.map(function(t) {
1108
- const cClass = getTagColorClass(t);
1109
- return '<span class="status-badge ' + cClass + '">' + escapeHtml(t) + '</span>';
1110
- }).join('');
892
+ const tagBadges = tags.map(t => '<span class="status-badge '+getTagColorClass(t)+'">'+escapeHtml(t)+'</span>').join('');
1111
893
 
1112
- const headerHtml =
894
+ return '<article class="endpoint-card" data-expanded="false">' +
1113
895
  '<div class="card-header">' +
1114
- '<span class="method-badge m-' + escapeAttr(method) + '">' + escapeHtml(method) + '</span>' +
1115
- '<div class="path-container" data-path="' + escapeAttr(path) + '" title="Click to copy path">' +
1116
- '<span class="path-text">' + escapeHtml(path) + '</span>' +
1117
- '<span class="copy-icon">\u{1F4CB}</span>' +
1118
- '</div>' +
1119
- '<div class="tags-container">' +
1120
- tagBadges +
1121
- '</div>' +
896
+ '<span class="method-badge m-'+escapeAttr(methodUc)+'">'+escapeHtml(methodUc)+'</span>' +
897
+ '<div class="path-container" data-path="'+escapeAttr(path)+'"><span class="path-text">'+escapeHtml(path)+'</span><span class="copy-icon">\u{1F4CB}</span></div>' +
898
+ '<div class="tags-container">'+tagBadges+'</div>' +
1122
899
  '<div class="header-spacer"></div>' +
900
+ '<button class="btn-try-it" data-index="'+originalIndex+'"><span class="play-icon">\u25B6</span> Use endpoint</button>' +
1123
901
  '<div class="expand-icon">\u25BC</div>' +
902
+ '</div>' +
903
+ '<div class="card-body">' +
904
+ '<div class="section-block"><div class="summary-text"><strong>'+escapeHtml(cfg.summary||'')+'</strong><br><span class="description-text">'+escapeHtml(cfg.description||'')+'</span></div></div>' +
905
+ (cfg.paramsSchema || cfg.querySchema ? renderSection('Parameters', cfg.paramsSchema, cfg.querySchema) : '') +
906
+ (cfg.bodySchema ? renderSection('Request Body ' + (cfg.hasBody?'':'(Optional)'), cfg.bodySchema) : '') +
907
+ (cfg.outputSchema ? renderSection('Response Schema', cfg.outputSchema) : '') +
908
+ '</div></article>';
909
+ }
910
+
911
+ function renderSection(title, schema1, schema2) {
912
+ return '<div class="section-block">' +
913
+ '<div style="display:flex; justify-content:space-between; align-items:flex-end; border-bottom:1px solid var(--border-subtle); margin-bottom:8px;">' +
914
+ '<div class="section-title" style="border:none; margin:0;">'+title+'</div>' +
915
+ '<div class="schema-actions" style="margin-bottom:4px;"><button data-action="expand" style="background:none;border:none;color:var(--text-accent);cursor:pointer;font-size:10px;">Exp All</button> <button data-action="collapse" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:10px;">Col All</button></div>' +
916
+ '</div>' +
917
+ (schema1 ? renderSchemaTable(schema1) : '') +
918
+ (schema2 ? renderSchemaTable(schema2) : '') +
1124
919
  '</div>';
920
+ }
1125
921
 
1126
- let contentHtml =
1127
- '<div class="section-block"><div class="summary-text">' +
1128
- '<strong>' + escapeHtml(summary) + '</strong>' +
1129
- (description
1130
- ? '<br><span class="description-text">' + escapeHtml(description) + '</span>'
1131
- : ''
1132
- ) +
1133
- '</div></div>';
922
+ // --- Schema Table Logic (Flattened with Depth) ---
1134
923
 
1135
- if (cfg.paramsSchema || cfg.querySchema) {
1136
- contentHtml += '<div class="section-block"><div class="section-title">Parameters</div>';
1137
- if (cfg.paramsSchema) contentHtml += renderSchemaTable(cfg.paramsSchema, 'Path Parameters');
1138
- if (cfg.querySchema) contentHtml += renderSchemaTable(cfg.querySchema, 'Query Parameters');
1139
- contentHtml += '</div>';
924
+ function renderSchemaTable(node) {
925
+ let rows = '';
926
+ // Start rendering. Root node usually doesn't need a row unless it's not an object.
927
+ if (node.kind === 'object' && node.properties) {
928
+ rows = renderObjectChildren(node, 0);
929
+ } else {
930
+ rows = renderNodeRow('(root)', node, 0);
1140
931
  }
932
+ return '<table class="schema-table">' +
933
+ '<tr><th class="col-name">Field</th><th class="col-type">Type</th><th>Req</th><th>Description</th></tr>' +
934
+ rows + '</table>';
935
+ }
936
+
937
+ // Returns HTML string for rows
938
+ function renderNodeRow(name, node, depth, parentPath) {
939
+ const hasChildren = isComplexNode(node) && ((node.properties && Object.keys(node.properties).length > 0) || (node.element && isComplexNode(node.element)) || (node.union));
940
+ const currentPath = (parentPath ? parentPath + '.' : '') + name;
941
+ const indent = depth * 16;
942
+
943
+ // NEW: Collapse by default. Hide deeply nested rows (depth > 0) initially.
944
+ const isExpanded = false;
945
+ const isHidden = depth > 0;
946
+
947
+ const styleAttr = isHidden ? 'style="display:none"' : '';
948
+ const rowAttrs = 'data-depth="'+depth+'" data-path="'+escapeAttr(currentPath)+'" '+styleAttr + (hasChildren ? ' data-has-children="true" data-expanded="'+isExpanded+'"' : '');
949
+
950
+ const toggleHtml = hasChildren
951
+ ? '<button class="schema-toggle">' + (isExpanded ? '\u25BC' : '\u25B6') + '</button>'
952
+ : '<span style="width:20px; display:inline-block;"></span>';
1141
953
 
1142
- if (cfg.bodySchema) {
1143
- contentHtml +=
1144
- '<div class="section-block">' +
1145
- '<div class="section-title">Request Body ' + (cfg.hasBody ? '' : '(Optional)') + '</div>' +
1146
- renderSchemaTable(cfg.bodySchema) +
1147
- '</div>';
954
+ const nameCell = '<div style="padding-left:'+indent+'px; display:flex; align-items:center;">' +
955
+ toggleHtml + '<span style="opacity:0.8">'+escapeHtml(name)+'</span></div>';
956
+
957
+ let html = '<tr '+rowAttrs+'>' +
958
+ '<td class="col-name">' + nameCell + '</td>' +
959
+ '<td class="col-type">' + escapeHtml(getTypeLabel(node)) + '</td>' +
960
+ '<td><span class="req-badge '+(node.optional?'req-false':'req-true')+'">'+(node.optional?'OPT':'REQ')+'</span></td>' +
961
+ '<td>' + renderNodeDescription(node) + '</td>' +
962
+ '</tr>';
963
+
964
+ if (hasChildren) {
965
+ if (node.kind === 'object' && node.properties) {
966
+ html += renderObjectChildren(node, depth + 1, currentPath);
967
+ }
968
+ else if (node.kind === 'array' && node.element) {
969
+ // ARRAY FIX: If array element is an object, render its properties directly (removing [item] layer)
970
+ if (node.element.kind === 'object' && node.element.properties) {
971
+ html += renderObjectChildren(node.element, depth + 1, currentPath);
972
+ } else {
973
+ html += renderNodeRow('[item]', node.element, depth + 1, currentPath);
974
+ }
975
+ }
976
+ else if (node.kind === 'union' && node.union) {
977
+ node.union.forEach((u, i) => { html += renderNodeRow('OneOf ('+i+')', u, depth + 1, currentPath); });
978
+ }
1148
979
  }
980
+ return html;
981
+ }
1149
982
 
1150
- if (cfg.outputSchema) {
1151
- contentHtml +=
1152
- '<div class="section-block">' +
1153
- '<div class="section-title">Response Schema</div>' +
1154
- renderSchemaTable(cfg.outputSchema) +
1155
- '</div>';
983
+ function renderObjectChildren(node, depth, parentPath) {
984
+ if (!node.properties) return '';
985
+ return Object.keys(node.properties).map(key => renderNodeRow(key, node.properties[key], depth, parentPath)).join('');
986
+ }
987
+
988
+ // Schema Toggling Logic (JS)
989
+ function toggleSchemaRow(btn) {
990
+ const tr = btn.closest('tr');
991
+ const wasExpanded = tr.getAttribute('data-expanded') === 'true';
992
+ const isNowExpanded = !wasExpanded;
993
+ const currentDepth = parseInt(tr.getAttribute('data-depth'), 10);
994
+
995
+ // Update State UI
996
+ tr.setAttribute('data-expanded', String(isNowExpanded));
997
+ btn.textContent = isNowExpanded ? '\u25BC' : '\u25B6';
998
+
999
+ // Process Siblings
1000
+ let sibling = tr.nextElementSibling;
1001
+ while (sibling) {
1002
+ const sibDepth = parseInt(sibling.getAttribute('data-depth'), 10);
1003
+ // Stop if we hit a row with equal or less depth (next sibling or uncle)
1004
+ if (isNaN(sibDepth) || sibDepth <= currentDepth) break;
1005
+
1006
+ if (isNowExpanded) {
1007
+ // EXPANDING: We only show this child if its immediate parent is also expanded.
1008
+ checkVisibility(sibling);
1009
+ } else {
1010
+ // COLLAPSING: Hide all descendants regardless of their state
1011
+ sibling.style.display = 'none';
1012
+ }
1013
+ sibling = sibling.nextElementSibling;
1156
1014
  }
1015
+ }
1157
1016
 
1158
- return (
1159
- '<article class="endpoint-card" data-expanded="false">' +
1160
- headerHtml +
1161
- '<div class="card-body">' +
1162
- contentHtml +
1163
- '</div>' +
1164
- '</article>'
1165
- );
1017
+ function checkVisibility(row) {
1018
+ // Walk up previous siblings to find parent and check its expansion state
1019
+ const myDepth = parseInt(row.getAttribute('data-depth'), 10);
1020
+ let prev = row.previousElementSibling;
1021
+ while (prev) {
1022
+ const pDepth = parseInt(prev.getAttribute('data-depth'), 10);
1023
+ if (pDepth < myDepth) {
1024
+ // Found parent
1025
+ // If parent is expanded AND visible, then I should be visible.
1026
+ if (prev.getAttribute('data-expanded') === 'true' && prev.style.display !== 'none') {
1027
+ row.style.display = '';
1028
+ } else {
1029
+ row.style.display = 'none';
1030
+ }
1031
+ return;
1032
+ }
1033
+ prev = prev.previousElementSibling;
1034
+ }
1035
+ // No parent found (depth 0), always visible
1036
+ row.style.display = '';
1166
1037
  }
1167
1038
 
1168
- // Recursive schema renderer with proper depth-based indentation.
1039
+ function toggleAllSchema(table, expand) {
1040
+ if (!table) return;
1041
+ const rows = table.querySelectorAll('tr[data-has-children="true"]');
1042
+ rows.forEach(row => {
1043
+ row.setAttribute('data-expanded', String(expand));
1044
+ const btn = row.querySelector('.schema-toggle');
1045
+ if(btn) btn.textContent = expand ? '\u25BC' : '\u25B6';
1046
+ });
1047
+ // Update visibility for all rows
1048
+ const allRows = table.querySelectorAll('tr[data-depth]');
1049
+ allRows.forEach(row => {
1050
+ if(parseInt(row.getAttribute('data-depth')) === 0) return;
1051
+ checkVisibility(row);
1052
+ });
1053
+ }
1169
1054
 
1170
- function renderSchemaTable(node, title) {
1171
- let rows = '';
1055
+ // --- Playground Logic ---
1172
1056
 
1173
- if (node.kind === 'object' && node.properties) {
1174
- // Start at depth -1 so first level of properties shows at depth 0
1175
- rows = renderObjectChildren(node, -1);
1176
- } else {
1177
- rows = renderNodeRow('(root)', node, 0);
1057
+ function setupPlaygroundListeners() {
1058
+ if(elPgClose) elPgClose.addEventListener('click', closePlayground);
1059
+ if(elPgSend) elPgSend.addEventListener('click', executeRequest);
1178
1060
  }
1179
1061
 
1180
- return (
1181
- (title
1182
- ? '<div class="schema-subtitle">' + escapeHtml(title) + '</div>'
1183
- : '') +
1184
- '<table class="schema-table">' +
1185
- rows +
1186
- '</table>'
1187
- );
1188
- }
1062
+ function closePlayground() {
1063
+ elPlayground.classList.remove('open');
1064
+ }
1189
1065
 
1190
- function renderObjectChildren(node, depth) {
1191
- if (!node.properties) return '';
1066
+ function openPlayground(leaf) {
1067
+ activeLeaf = leaf;
1068
+ elPgTitle.textContent = leaf.path;
1069
+ elPgMethod.textContent = (leaf.method || 'GET').toUpperCase();
1070
+ elPgMethod.className = 'method-badge m-' + elPgMethod.textContent;
1192
1071
 
1193
- let rows = '';
1194
- Object.keys(node.properties).forEach(function (key) {
1195
- rows += renderNodeRow(key, node.properties[key], depth + 1);
1196
- });
1197
- return rows;
1198
- }
1072
+ // Reset request state
1073
+ activeRequest = { pathParams: {}, queryParams: {}, body: '' };
1074
+
1075
+ renderPlaygroundForm(leaf);
1076
+ elPlayground.classList.add('open');
1077
+ }
1078
+
1079
+ function renderPlaygroundForm(leaf) {
1080
+ // 1. Base URL
1081
+ let html = '<div class="pg-section"><div class="pg-label">Base URL</div>' +
1082
+ '<input class="pg-input" id="pgBaseUrl" value="'+window.location.origin+'"></div>';
1083
+
1084
+ // 2. Path Params
1085
+ // Extract :param or {param} from path
1086
+ const pathParamRegex = /[:{]([a-zA-Z0-9_]+)}?/g;
1087
+ const matches = [...leaf.path.matchAll(pathParamRegex)];
1088
+
1089
+ if (matches.length > 0) {
1090
+ html += '<div class="pg-section"><div class="pg-label">Path Variables</div><div class="param-grid">';
1091
+ matches.forEach(m => {
1092
+ const key = m[1];
1093
+ html += '<div class="param-row">' +
1094
+ '<input class="pg-input" placeholder="'+key+'" disabled value="'+key+'" style="opacity:0.7">' +
1095
+ '<input class="pg-input pg-path-input" data-key="'+key+'" placeholder="value">' +
1096
+ '</div>';
1097
+ });
1098
+ html += '</div></div>';
1099
+ }
1199
1100
 
1200
- function renderNodeRow(name, node, depth) {
1201
- const reqClass = node.optional ? 'req-false' : 'req-true';
1202
- const reqText = node.optional ? 'OPT' : 'REQ';
1101
+ // 3. Query Params
1102
+ // We check leaf.cfg.querySchema or just provide a generic adder
1103
+ html += '<div class="pg-section"><div class="pg-label">Query Parameters</div>' +
1104
+ '<div class="param-grid" id="pgQueryGrid"></div>' +
1105
+ '<button type="button" id="pgAddQuery" style="font-size:11px;background:none;border:1px dashed var(--border-subtle);color:var(--text-muted);padding:4px;cursor:pointer;">+ Add Parameter</button>' +
1106
+ '</div>';
1203
1107
 
1204
- const indentPx = depth * 16; // increase this if you want more spacing per level
1205
- const nameCell =
1206
- '<span class="schema-indent" style="padding-left:' +
1207
- indentPx +
1208
- 'px;"></span>' +
1209
- (depth > 0 ? '<span class="schema-branch">\u2514\u2500 </span>' : '') +
1210
- escapeHtml(name);
1108
+ // 4. Body
1109
+ if (['POST','PUT','PATCH'].includes((leaf.method||'').toUpperCase())) {
1110
+ html += '<div class="pg-section"><div class="pg-label">Request Body (JSON)</div>' +
1111
+ '<textarea class="pg-textarea" id="pgBodyInput" placeholder="{ ... }"></textarea>' +
1112
+ '</div>';
1113
+ }
1211
1114
 
1212
- const typeLabel = getTypeLabel(node);
1213
- const descriptionHtml = renderNodeDescription(node);
1115
+ // 5. Response Container
1116
+ html += '<div class="pg-section" id="pgResponseContainer" style="display:none;">' +
1117
+ '<div class="pg-label">Response</div>' +
1118
+ '<div class="response-box">' +
1119
+ '<div class="response-meta" id="pgResponseMeta"></div>' +
1120
+ '<div class="response-body" id="pgResponseBody"></div>' +
1121
+ '</div></div>';
1214
1122
 
1215
- let rows =
1216
- '<tr>' +
1217
- '<td class="col-name">' + nameCell + '</td>' +
1218
- '<td class="col-type">' + escapeHtml(typeLabel) + '</td>' +
1219
- '<td><span class="req-badge ' + reqClass + '">' + reqText + '</span></td>' +
1220
- '<td>' + descriptionHtml + '</td>' +
1221
- '</tr>';
1123
+ elPgContent.innerHTML = html;
1222
1124
 
1223
- // 1) Object properties
1224
- if (node.kind === 'object' && node.properties) {
1225
- rows += renderObjectChildren(node, depth);
1226
- }
1125
+ // Setup listeners for the dynamic form
1126
+ const btnAddQuery = document.getElementById('pgAddQuery');
1127
+ if(btnAddQuery) btnAddQuery.addEventListener('click', () => addQueryRow());
1227
1128
 
1228
- // 2) Array element type
1229
- if (node.kind === 'array' && node.element) {
1230
- const element = node.element;
1231
- if (isComplexNode(element)) {
1232
- // element rendered one level deeper than the array itself
1233
- rows += renderNodeRow('[items]', element, depth + 1);
1129
+ // Pre-fill query rows if schema exists
1130
+ if (leaf.cfg.querySchema && leaf.cfg.querySchema.properties) {
1131
+ Object.keys(leaf.cfg.querySchema.properties).forEach(k => addQueryRow(k));
1132
+ } else {
1133
+ addQueryRow();
1234
1134
  }
1235
1135
  }
1236
1136
 
1237
- // 3) Union variants
1238
- if (node.kind === 'union' && Array.isArray(node.union)) {
1239
- node.union.forEach(function (variant, index) {
1240
- rows += renderNodeRow('option ' + (index + 1), variant, depth + 1);
1241
- });
1137
+ function addQueryRow(key = '') {
1138
+ const container = document.getElementById('pgQueryGrid');
1139
+ const div = document.createElement('div');
1140
+ div.className = 'param-row';
1141
+ div.innerHTML = '<input class="pg-input pg-query-key" placeholder="Name" value="'+key+'"><input class="pg-input pg-query-val" placeholder="Value">';
1142
+ container.appendChild(div);
1242
1143
  }
1243
1144
 
1244
- return rows;
1245
- }
1145
+ // --- Execution ---
1246
1146
 
1247
- /**
1248
- * Decide if a node is \u201Ccomplex\u201D enough to warrant a nested expansion row.
1249
- */
1250
- function isComplexNode(node) {
1251
- return (
1252
- node.kind === 'object' ||
1253
- node.kind === 'array' ||
1254
- node.kind === 'union' ||
1255
- node.kind === 'record' ||
1256
- node.kind === 'tuple'
1257
- );
1258
- }
1147
+ async function executeRequest() {
1148
+ const btn = document.getElementById('pgSend');
1149
+ const resContainer = document.getElementById('pgResponseContainer');
1150
+ const metaEl = document.getElementById('pgResponseMeta');
1151
+ const bodyEl = document.getElementById('pgResponseBody');
1152
+ const baseUrlInput = document.getElementById('pgBaseUrl');
1259
1153
 
1260
- /**
1261
- * Helper: simple vs complex node, for compact type labels.
1262
- */
1263
- function isSimpleNode(node) {
1264
- return !isComplexNode(node);
1265
- }
1154
+ btn.disabled = true;
1155
+ btn.textContent = 'Sending...';
1156
+ resContainer.style.display = 'none';
1266
1157
 
1267
- /**
1268
- * Type label shown in the "Type" column.
1269
- */
1270
- function getTypeLabel(node) {
1271
- let base;
1272
-
1273
- switch (node.kind) {
1274
- case 'object':
1275
- base = 'object';
1276
- break;
1277
-
1278
- case 'array':
1279
- if (!node.element) {
1280
- base = 'array';
1281
- } else if (isSimpleNode(node.element)) {
1282
- base = 'array<' + getTypeLabel(node.element) + '>';
1283
- } else {
1284
- base = 'array<object>';
1285
- }
1286
- break;
1158
+ try {
1159
+ // Build URL
1160
+ let url = (baseUrlInput.value || window.location.origin).replace(/\\/$/, '') + activeLeaf.path;
1287
1161
 
1288
- case 'union':
1289
- if (node.union && node.union.length) {
1290
- const parts = node.union.map(function (u) {
1291
- return isSimpleNode(u) ? getTypeLabel(u) : 'object';
1292
- });
1293
- base = parts.join(' | ');
1294
- } else {
1295
- base = 'union';
1162
+ // Sub Path Params
1163
+ document.querySelectorAll('.pg-path-input').forEach(input => {
1164
+ const k = input.getAttribute('data-key');
1165
+ const v = input.value.trim();
1166
+ url = url.replace(':'+k, v).replace('{'+k+'}', v);
1167
+ });
1168
+
1169
+ // Build Query
1170
+ const queryParts = [];
1171
+ document.querySelectorAll('.pgQueryGrid .param-row, .param-grid .param-row').forEach(row => {
1172
+ const k = row.querySelector('.pg-query-key');
1173
+ const v = row.querySelector('.pg-query-val');
1174
+ if(k && v && k.value.trim()) {
1175
+ queryParts.push(encodeURIComponent(k.value.trim()) + '=' + encodeURIComponent(v.value.trim()));
1176
+ }
1177
+ });
1178
+ if(queryParts.length) url += '?' + queryParts.join('&');
1179
+
1180
+ // Build Body
1181
+ const method = (activeLeaf.method || 'GET').toUpperCase();
1182
+ const headers = {};
1183
+ let body = null;
1184
+
1185
+ const bodyInput = document.getElementById('pgBodyInput');
1186
+ if (bodyInput && bodyInput.value.trim()) {
1187
+ try {
1188
+ // Validate JSON
1189
+ const j = JSON.parse(bodyInput.value);
1190
+ body = JSON.stringify(j);
1191
+ headers['Content-Type'] = 'application/json';
1192
+ } catch(e) {
1193
+ alert('Invalid JSON in request body');
1194
+ btn.disabled = false; btn.textContent = 'Send Request';
1195
+ return;
1196
+ }
1296
1197
  }
1297
- break;
1298
1198
 
1299
- case 'literal':
1300
- base = 'literal';
1301
- break;
1199
+ const start = performance.now();
1200
+ const res = await fetch(url, { method, headers, body });
1201
+ const time = (performance.now() - start).toFixed(0);
1302
1202
 
1303
- case 'enum':
1304
- base = 'enum';
1305
- break;
1203
+ // Read response
1204
+ const status = res.status;
1205
+ const statusText = res.statusText;
1206
+ let resData;
1207
+ const contentType = res.headers.get('content-type') || '';
1306
1208
 
1307
- case 'record':
1308
- base = 'record';
1309
- break;
1209
+ if (contentType.includes('application/json')) {
1210
+ resData = await res.json();
1211
+ } else {
1212
+ resData = await res.text();
1213
+ }
1310
1214
 
1311
- case 'tuple':
1312
- base = 'tuple';
1313
- break;
1215
+ // Render Response
1216
+ resContainer.style.display = 'block';
1217
+ const statusClass = (status >= 200 && status < 300) ? 'status-ok' : 'status-err';
1218
+ metaEl.innerHTML = '<div class="meta-item '+statusClass+'">\u2B24 '+status+' '+statusText+'</div><div class="meta-item">\u23F1 '+time+'ms</div>';
1314
1219
 
1315
- default:
1316
- base = node.kind || 'unknown';
1317
- break;
1318
- }
1220
+ bodyEl.innerHTML = '';
1221
+ if (typeof resData === 'object' && resData !== null) {
1222
+ bodyEl.appendChild(renderJsonTree(resData));
1223
+ } else {
1224
+ const pre = document.createElement('pre');
1225
+ pre.style.margin = 0;
1226
+ pre.textContent = resData;
1227
+ bodyEl.appendChild(pre);
1228
+ }
1319
1229
 
1320
- if (node.nullable) {
1321
- base = base + ' | null';
1230
+ } catch (err) {
1231
+ resContainer.style.display = 'block';
1232
+ metaEl.innerHTML = '<div class="meta-item status-err">Network Error</div>';
1233
+ bodyEl.textContent = err.message;
1234
+ } finally {
1235
+ btn.disabled = false;
1236
+ btn.textContent = 'Send Request';
1237
+ }
1322
1238
  }
1323
- return base;
1324
- }
1325
1239
 
1326
- /**
1327
- * Description cell content:
1328
- * - main description text
1329
- * - for enum: allowed values
1330
- * - for literal: literal value
1331
- */
1332
- function renderNodeDescription(node) {
1333
- const parts = [];
1334
-
1335
- if (node.description) {
1336
- parts.push(escapeHtml(node.description));
1337
- }
1240
+ // --- JSON Tree Renderer ---
1338
1241
 
1339
- if (node.kind === 'enum' && node.enumValues && node.enumValues.length) {
1340
- const values = node.enumValues
1341
- .map(function (v) {
1342
- return '<code>' + escapeHtml(String(v)) + '</code>';
1343
- })
1344
- .join(' | ');
1345
- parts.push('<div class="schema-meta">Allowed: ' + values + '</div>');
1346
- }
1242
+ function renderJsonTree(data) {
1243
+ // Recursive DOM generator
1244
+ if (data === null) return createSpan('null', 'jt-null');
1245
+ if (typeof data === 'number') return createSpan(String(data), 'jt-num');
1246
+ if (typeof data === 'boolean') return createSpan(String(data), 'jt-bool');
1247
+ if (typeof data === 'string') return createSpan('"'+data+'"', 'jt-str');
1347
1248
 
1348
- if (node.kind === 'literal' && typeof node.literal !== 'undefined') {
1349
- const valueStr = escapeHtml(JSON.stringify(node.literal));
1350
- parts.push(
1351
- '<div class="schema-meta">Literal: <code>' + valueStr + '</code></div>'
1352
- );
1353
- }
1249
+ const isArr = Array.isArray(data);
1250
+ const container = document.createElement('div');
1251
+ container.className = 'json-tree ' + (isArr ? 'jt-arr' : 'jt-obj');
1354
1252
 
1355
- return parts.join('<br>');
1356
- }
1253
+ const toggle = document.createElement('span');
1254
+ toggle.className = 'jt-toggle';
1255
+ toggle.textContent = '\u25BC';
1357
1256
 
1358
- /**
1359
- * Escape HTML helper.
1360
- */
1361
- function escapeHtml(str) {
1362
- return String(str)
1363
- .replace(/&/g, '&amp;')
1364
- .replace(/</g, '&lt;')
1365
- .replace(/>/g, '&gt;')
1366
- .replace(/"/g, '&quot;')
1367
- .replace(/'/g, '&#39;');
1368
- }
1257
+ const content = document.createElement('div');
1258
+ content.className = 'jt-content';
1369
1259
 
1260
+ const keys = Object.keys(data);
1261
+ if (keys.length === 0) {
1262
+ container.textContent = isArr ? '[]' : '{}';
1263
+ return container;
1264
+ }
1370
1265
 
1371
- function getTypeLabel(node) {
1372
- if (!node) return 'any';
1373
- if (node.kind === 'array') return getTypeLabel(node.element) + '[]';
1374
- if (node.enumValues) return 'enum(' + node.enumValues.join('|') + ')';
1375
- return node.kind || 'any';
1376
- }
1266
+ container.appendChild(toggle);
1267
+ container.appendChild(document.createTextNode(isArr ? '[' : '{'));
1268
+ container.appendChild(content);
1377
1269
 
1378
- // Interactions
1270
+ keys.forEach((k, i) => {
1271
+ const line = document.createElement('div');
1272
+ line.style.paddingLeft = '16px';
1379
1273
 
1380
- function wireOverviewInteractions() {
1381
- if (!elOverview || !elRouteList) return;
1274
+ if (!isArr) {
1275
+ const keySpan = createSpan('"'+k+'": ', 'jt-key');
1276
+ line.appendChild(keySpan);
1277
+ }
1382
1278
 
1383
- // group chips
1384
- elOverview.querySelectorAll('.group-chip').forEach(function(chip) {
1385
- chip.addEventListener('click', function(e) {
1386
- e.preventDefault();
1387
- const gName = chip.getAttribute('data-group');
1388
- if (gName) scrollToGroup(gName);
1389
- });
1279
+ line.appendChild(renderJsonTree(data[k]));
1280
+ if (i < keys.length - 1) line.appendChild(document.createTextNode(','));
1281
+ content.appendChild(line);
1390
1282
  });
1391
1283
 
1392
- // expand/collapse buttons
1393
- elRouteList.querySelectorAll('.group-actions button').forEach(function(btn) {
1394
- btn.addEventListener('click', function() {
1395
- const gName = btn.getAttribute('data-group');
1396
- const action = btn.getAttribute('data-action');
1397
- if (!gName || !action) return;
1398
- toggleGroup(gName, action === 'expand');
1399
- });
1400
- });
1284
+ container.appendChild(document.createTextNode(isArr ? ']' : '}'));
1285
+ return container;
1401
1286
  }
1402
1287
 
1403
- function wireCardInteractions() {
1404
- if (!elRouteList) return;
1288
+ function createSpan(text, cls) {
1289
+ const s = document.createElement('span');
1290
+ s.className = cls;
1291
+ s.textContent = text;
1292
+ return s;
1293
+ }
1405
1294
 
1406
- // card expand/collapse
1407
- elRouteList.querySelectorAll('.endpoint-card').forEach(function(card) {
1408
- card.addEventListener('click', function(e) {
1409
- var target = e.target;
1410
- if (target && target.closest && target.closest('.path-container')) {
1411
- // handled separately for copy
1412
- return;
1413
- }
1414
- toggleCard(card);
1415
- });
1416
- });
1295
+ // --- Utilities ---
1417
1296
 
1418
- // copy path
1419
- elRouteList.querySelectorAll('.path-container').forEach(function(pc) {
1420
- pc.addEventListener('click', function(e) {
1421
- e.stopPropagation();
1422
- const text = pc.getAttribute('data-path') || '';
1423
- copyText(text);
1424
- });
1425
- });
1297
+ function getTagColorClass(tagName) {
1298
+ if (tagName === 'not-implemented') return 'not-implemented';
1299
+ let hash = 0;
1300
+ for (let i = 0; i < tagName.length; i++) hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
1301
+ return 'tag-' + Math.abs(hash % 7);
1302
+ }
1303
+
1304
+ function getTypeLabel(node) {
1305
+ if (!node) return 'any';
1306
+ if (node.kind === 'array') {
1307
+ if(!node.element) return 'array';
1308
+ return 'array<' + getTypeLabel(node.element) + '>';
1309
+ }
1310
+ if (node.kind === 'union' && node.union) return node.union.map(getTypeLabel).join(' | ');
1311
+ if (node.kind === 'literal') return 'literal';
1312
+ if (node.kind === 'enum') return 'enum';
1313
+ return node.kind || 'any';
1426
1314
  }
1427
1315
 
1428
- function toggleCard(card) {
1429
- const isExpanded = card.getAttribute('data-expanded') === 'true';
1430
- card.setAttribute('data-expanded', String(!isExpanded));
1316
+ function renderNodeDescription(node) {
1317
+ let desc = [];
1318
+ if (node.description) desc.push(escapeHtml(node.description));
1319
+ if (node.kind === 'enum' && node.enumValues) desc.push('Allowed: ' + node.enumValues.map(v=>'<code>'+v+'</code>').join(' | '));
1320
+ if (node.kind === 'literal' && node.literal !== undefined) desc.push('Value: <code>'+JSON.stringify(node.literal)+'</code>');
1321
+ return desc.join('<br>');
1431
1322
  }
1432
1323
 
1433
- function toggleGroup(gName, expand) {
1434
- const group = document.getElementById('group-' + gName);
1435
- if (!group) return;
1436
- group.querySelectorAll('.endpoint-card').forEach(function(c) {
1437
- c.setAttribute('data-expanded', String(!!expand));
1438
- });
1324
+ function isComplexNode(node) {
1325
+ return ['object','array','union','record','tuple'].includes(node.kind);
1439
1326
  }
1440
1327
 
1441
- function copyText(text) {
1442
- if (!navigator.clipboard || !text) return;
1443
- navigator.clipboard.writeText(text).catch(function() {
1444
- // ignore
1445
- });
1328
+ function toggleGroup(gName, expand) {
1329
+ const g = document.getElementById('group-'+gName);
1330
+ if(g) g.querySelectorAll('.endpoint-card').forEach(c => c.setAttribute('data-expanded', String(!!expand)));
1446
1331
  }
1447
1332
 
1448
1333
  function scrollToGroup(gName) {
1449
- const el = document.getElementById('group-' + gName);
1450
- if (el) window.scrollTo({ top: el.offsetTop - 220, behavior: 'smooth' });
1334
+ const el = document.getElementById('group-'+gName);
1335
+ if (el) window.scrollTo({ top: el.offsetTop - 100, behavior: 'smooth' });
1451
1336
  }
1452
1337
 
1453
- // Escaping helpers
1454
- function escapeHtml(str) {
1455
- if (!str && str !== 0) return '';
1456
- return String(str)
1457
- .replace(/&/g, '&amp;')
1458
- .replace(/</g, '&lt;')
1459
- .replace(/>/g, '&gt;')
1460
- .replace(/"/g, '&quot;')
1461
- .replace(/'/g, '&#39;');
1338
+ function copyText(text) {
1339
+ if (navigator.clipboard && text) navigator.clipboard.writeText(text);
1462
1340
  }
1463
1341
 
1464
- function escapeAttr(str) {
1465
- return escapeHtml(str);
1342
+ function escapeHtml(str) {
1343
+ if (str === null || str === undefined) return '';
1344
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1466
1345
  }
1346
+ function escapeAttr(str) { return escapeHtml(str); }
1467
1347
 
1468
1348
  init();
1469
1349
  })();
@@ -1477,42 +1357,36 @@ function renderLeafDocsHTML(leaves, options = {}) {
1477
1357
  <head>
1478
1358
  <meta charset="UTF-8">
1479
1359
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1480
- <title>API Documentation</title>
1481
-
1360
+ <title>API Reference</title>
1482
1361
  <style${nonceAttr}>${CSS_STYLES}</style>
1483
1362
  ${options.inlineCss ? `<style${nonceAttr}>${options.inlineCss}</style>` : ""}
1484
1363
  </head>
1485
1364
  <body>
1486
1365
  <div class="page">
1487
1366
  <header class="header">
1488
- <div class="header-title">
1489
- <h1>API Reference</h1>
1490
- </div>
1367
+ <div class="header-title"><h1>API Reference</h1></div>
1491
1368
  </header>
1492
1369
 
1493
1370
  <div class="controls-container">
1494
1371
  <div class="filters-row">
1495
1372
  <div class="left-column">
1496
- <div class="filter-group method-filters-container">
1373
+ <div class="filter-group">
1497
1374
  <div class="filter-label">Search</div>
1498
1375
  <div class="search-box">
1499
1376
  <span class="search-icon">\u{1F50D}</span>
1500
1377
  <input type="text" id="searchInput" class="search-input" placeholder="Filter endpoints...">
1501
1378
  </div>
1502
1379
  </div>
1503
-
1504
- <div class="filter-group method-filters-container">
1380
+ <div class="filter-group">
1505
1381
  <div class="filter-label">Methods</div>
1506
1382
  <div class="checkbox-group" id="methodFilters"></div>
1507
1383
  </div>
1508
1384
  </div>
1509
-
1510
- <div class="filter-group tag-filters-container" id="tagFilterContainer">
1385
+ <div class="filter-group tag-filters-container">
1511
1386
  <div class="filter-label">Tags</div>
1512
1387
  <div class="checkbox-group" id="tagFilters"></div>
1513
1388
  </div>
1514
1389
  </div>
1515
-
1516
1390
  <div class="overview-row" id="overviewList"></div>
1517
1391
  </div>
1518
1392