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