@@ -16,6 +16,7 @@ interface DemoContentFile {
1616 filename : string ;
1717 language : string ;
1818 content : string ;
19+ highlighted ?: boolean ;
1920}
2021
2122interface DemoContent {
@@ -87,7 +88,8 @@ export default function DojoPage() {
8788 integrations [ 0 ] ?. demos [ 0 ] ?. id || "" ,
8889 ) ;
8990 const [ viewMode , setViewMode ] = useState < ViewMode > ( "preview" ) ;
90- const [ selectedFileIndex , setSelectedFileIndex ] = useState ( 0 ) ;
91+ const [ selectedFilename , setSelectedFilename ] = useState < string | null > ( null ) ;
92+ const [ codeViewMode , setCodeViewMode ] = useState < "core" | "all" > ( "core" ) ;
9193 const [ dropdownOpen , setDropdownOpen ] = useState ( false ) ;
9294
9395 const integration = useMemo (
@@ -115,10 +117,38 @@ export default function DojoPage() {
115117 return [ ...content . files , ...content . backend_files ] ;
116118 } , [ content ] ) ;
117119
120+ const hasHighlights = useMemo (
121+ ( ) => allFiles . some ( ( f ) => f . highlighted ) ,
122+ [ allFiles ] ,
123+ ) ;
124+
125+ const visibleFiles = useMemo (
126+ ( ) =>
127+ codeViewMode === "core" && hasHighlights
128+ ? allFiles . filter ( ( f ) => f . highlighted )
129+ : allFiles ,
130+ [ allFiles , codeViewMode , hasHighlights ] ,
131+ ) ;
132+
133+ // When the demo changes: reset code-view mode + selected file to defaults.
134+ // Default mode is "core" if any file is highlighted; otherwise "all" (which
135+ // is identical to the full list since core would be empty).
136+ useEffect ( ( ) => {
137+ setCodeViewMode ( hasHighlights ? "core" : "all" ) ;
138+ const firstHighlighted = allFiles . find ( ( f ) => f . highlighted ) ;
139+ setSelectedFilename (
140+ firstHighlighted ?. filename ?? allFiles [ 0 ] ?. filename ?? null ,
141+ ) ;
142+ } , [ contentKey , hasHighlights , allFiles ] ) ;
143+
144+ const activeFile = useMemo (
145+ ( ) => allFiles . find ( ( f ) => f . filename === selectedFilename ) ?? allFiles [ 0 ] ,
146+ [ allFiles , selectedFilename ] ,
147+ ) ;
148+
118149 const handleIntegrationChange = useCallback (
119150 ( slug : string ) => {
120151 setSelectedSlug ( slug ) ;
121- setSelectedFileIndex ( 0 ) ;
122152 setDropdownOpen ( false ) ;
123153 const newIntegration = integrations . find ( ( i ) => i . slug === slug ) ;
124154 if ( newIntegration ) {
@@ -135,7 +165,6 @@ export default function DojoPage() {
135165
136166 const handleDemoSelect = useCallback ( ( demoId : string ) => {
137167 setSelectedDemoId ( demoId ) ;
138- setSelectedFileIndex ( 0 ) ;
139168 } , [ ] ) ;
140169
141170 // URL ↔ selection sync. On mount we hydrate from ?integration=&demo=; after
@@ -653,10 +682,10 @@ export default function DojoPage() {
653682 overflow : "hidden" ,
654683 } }
655684 >
656- { allFiles [ selectedFileIndex ] && (
685+ { activeFile && (
657686 < CodeBlock
658- code = { allFiles [ selectedFileIndex ] . content }
659- language = { allFiles [ selectedFileIndex ] . language }
687+ code = { activeFile . content }
688+ language = { activeFile . language }
660689 />
661690 ) }
662691 </ div >
@@ -671,9 +700,14 @@ export default function DojoPage() {
671700 } }
672701 >
673702 < FileTree
674- files = { allFiles }
675- selectedFileIndex = { selectedFileIndex }
676- onSelect = { setSelectedFileIndex }
703+ files = { visibleFiles }
704+ activeFilename = { activeFile ?. filename }
705+ onSelect = { setSelectedFilename }
706+ hasHighlights = { hasHighlights }
707+ showAll = { codeViewMode === "all" }
708+ onToggleShowAll = { ( ) =>
709+ setCodeViewMode ( codeViewMode === "all" ? "core" : "all" )
710+ }
677711 />
678712 </ aside >
679713 </ div >
@@ -698,63 +732,86 @@ export default function DojoPage() {
698732 ) ;
699733}
700734
735+ type FileLeaf = { filename : string ; highlighted ?: boolean } ;
736+
701737type FileTreeNode = {
702738 name : string ;
703- fileIndex : number | null ;
739+ file : FileLeaf | null ;
704740 children : Map < string , FileTreeNode > ;
705741} ;
706742
707- function buildFileTree ( files : { filename : string } [ ] ) : FileTreeNode {
743+ function buildFileTree ( files : FileLeaf [ ] ) : FileTreeNode {
708744 const root : FileTreeNode = {
709745 name : "" ,
710- fileIndex : null ,
746+ file : null ,
711747 children : new Map ( ) ,
712748 } ;
713- files . forEach ( ( file , idx ) => {
749+ for ( const file of files ) {
714750 const parts = file . filename . split ( "/" ) . filter ( Boolean ) ;
715751 let current = root ;
716752 parts . forEach ( ( part , i ) => {
717753 const isFile = i === parts . length - 1 ;
718754 const existing = current . children . get ( part ) ;
719755 if ( existing ) {
720- if ( isFile ) existing . fileIndex = idx ;
756+ if ( isFile ) existing . file = file ;
721757 current = existing ;
722758 } else {
723759 const node : FileTreeNode = {
724760 name : part ,
725- fileIndex : isFile ? idx : null ,
761+ file : isFile ? file : null ,
726762 children : new Map ( ) ,
727763 } ;
728764 current . children . set ( part , node ) ;
729765 current = node ;
730766 }
731767 } ) ;
732- } ) ;
768+ }
733769 return root ;
734770}
735771
772+ // Folders before files; within each group, highlighted files float to the top,
773+ // then alphabetical. Matches the standalone shell's code-view ordering.
736774function sortedChildren ( node : FileTreeNode ) : FileTreeNode [ ] {
737775 return Array . from ( node . children . values ( ) ) . sort ( ( a , b ) => {
738- const aIsFolder = a . fileIndex === null ;
739- const bIsFolder = b . fileIndex === null ;
776+ const aIsFolder = a . file === null ;
777+ const bIsFolder = b . file === null ;
740778 if ( aIsFolder !== bIsFolder ) return aIsFolder ? - 1 : 1 ;
779+ if ( a . file && b . file ) {
780+ if ( ! ! a . file . highlighted !== ! ! b . file . highlighted ) {
781+ return a . file . highlighted ? - 1 : 1 ;
782+ }
783+ }
741784 return a . name . localeCompare ( b . name ) ;
742785 } ) ;
743786}
744787
745788function FileTree ( {
746789 files,
747- selectedFileIndex ,
790+ activeFilename ,
748791 onSelect,
792+ hasHighlights,
793+ showAll,
794+ onToggleShowAll,
749795} : {
750- files : { filename : string } [ ] ;
751- selectedFileIndex : number ;
752- onSelect : ( idx : number ) => void ;
796+ files : FileLeaf [ ] ;
797+ activeFilename : string | undefined ;
798+ onSelect : ( filename : string ) => void ;
799+ hasHighlights : boolean ;
800+ showAll : boolean ;
801+ onToggleShowAll : ( ) => void ;
753802} ) {
754803 const root = useMemo ( ( ) => buildFileTree ( files ) , [ files ] ) ;
755804 return (
756805 < div style = { { fontSize : 13 } } >
757- < div style = { { padding : "0 6px 8px" } } >
806+ < div
807+ style = { {
808+ display : "flex" ,
809+ alignItems : "center" ,
810+ justifyContent : "space-between" ,
811+ gap : 8 ,
812+ padding : "0 6px 8px" ,
813+ } }
814+ >
758815 < span
759816 style = { {
760817 fontSize : 11 ,
@@ -766,13 +823,66 @@ function FileTree({
766823 >
767824 Files
768825 </ span >
826+ { hasHighlights && (
827+ < button
828+ type = "button"
829+ role = "switch"
830+ aria-checked = { showAll }
831+ onClick = { onToggleShowAll }
832+ title = "Include scaffolding (configs, lockfiles, etc.)"
833+ style = { {
834+ display : "flex" ,
835+ alignItems : "center" ,
836+ gap : 6 ,
837+ border : "none" ,
838+ background : "transparent" ,
839+ cursor : "pointer" ,
840+ padding : 0 ,
841+ color : "var(--text-secondary)" ,
842+ fontSize : 10 ,
843+ fontWeight : 500 ,
844+ textTransform : "uppercase" ,
845+ letterSpacing : "0.05em" ,
846+ } }
847+ >
848+ < span > show all</ span >
849+ < span
850+ style = { {
851+ position : "relative" ,
852+ display : "inline-block" ,
853+ width : 22 ,
854+ height : 12 ,
855+ borderRadius : 999 ,
856+ background : showAll
857+ ? "var(--text-primary)"
858+ : "rgba(0,0,0,0.15)" ,
859+ transition : "background 0.15s" ,
860+ } }
861+ >
862+ < span
863+ style = { {
864+ position : "absolute" ,
865+ top : 1 ,
866+ left : 1 ,
867+ width : 10 ,
868+ height : 10 ,
869+ borderRadius : "50%" ,
870+ background : "#ffffff" ,
871+ transform : showAll ? "translateX(10px)" : "translateX(0)" ,
872+ transition : "transform 0.15s" ,
873+ boxShadow : "0 1px 2px rgba(0,0,0,0.2)" ,
874+ } }
875+ />
876+ </ span >
877+ </ button >
878+ ) }
769879 </ div >
770880 { sortedChildren ( root ) . map ( ( node ) => (
771881 < FileTreeRow
772882 key = { node . name }
773883 node = { node }
774884 depth = { 0 }
775- selectedFileIndex = { selectedFileIndex }
885+ activeFilename = { activeFilename }
776886 onSelect = { onSelect }
777887 />
778888 ) ) }
@@ -783,18 +893,16 @@ function FileTree({
783893function FileTreeRow ( {
784894 node,
785895 depth,
786- selectedFileIndex ,
896+ activeFilename ,
787897 onSelect,
788898} : {
789899 node : FileTreeNode ;
790900 depth : number ;
791- selectedFileIndex : number ;
792- onSelect : ( idx : number ) => void ;
901+ activeFilename : string | undefined ;
902+ onSelect : ( filename : string ) => void ;
793903} ) {
794- const isFolder = node . fileIndex === null ;
795- const isSelected = ! isFolder && node . fileIndex === selectedFileIndex ;
796904 const padLeft = 8 + depth * 12 ;
797- if ( isFolder ) {
905+ if ( ! node . file ) {
798906 return (
799907 < div >
800908 < div
@@ -819,17 +927,21 @@ function FileTreeRow({
819927 key = { child . name }
820928 node = { child }
821929 depth = { depth + 1 }
822- selectedFileIndex = { selectedFileIndex }
930+ activeFilename = { activeFilename }
823931 onSelect = { onSelect }
824932 />
825933 ) ) }
826934 </ div >
827935 ) ;
828936 }
937+ const file = node . file ;
938+ const isSelected = file . filename === activeFilename ;
939+ const isHighlighted = ! ! file . highlighted ;
829940 return (
830941 < button
831942 type = "button"
832- onClick = { ( ) => onSelect ( node . fileIndex ! ) }
943+ onClick = { ( ) => onSelect ( file . filename ) }
944+ title = { isHighlighted ? "Core file" : undefined }
833945 style = { {
834946 display : "flex" ,
835947 alignItems : "center" ,
@@ -838,8 +950,12 @@ function FileTreeRow({
838950 padding : `2px 6px 2px ${ padLeft + 14 } px` ,
839951 border : "none" ,
840952 background : isSelected ? "rgba(0, 0, 0, 0.05)" : "transparent" ,
841- color : isSelected ? "var(--text-primary)" : "var(--text-secondary)" ,
842- fontWeight : isSelected ? 500 : 400 ,
953+ color : isSelected
954+ ? "var(--text-primary)"
955+ : isHighlighted
956+ ? "var(--text-primary)"
957+ : "var(--text-disabled)" ,
958+ fontWeight : isHighlighted ? 600 : isSelected ? 500 : 400 ,
843959 textAlign : "left" ,
844960 cursor : "pointer" ,
845961 borderRadius : 4 ,
@@ -856,6 +972,19 @@ function FileTreeRow({
856972 if ( ! isSelected ) e . currentTarget . style . background = "transparent" ;
857973 } }
858974 >
975+ { isHighlighted && (
976+ < span
977+ aria-hidden = "true"
978+ style = { {
979+ color : "#f59e0b" ,
980+ fontSize : 11 ,
981+ lineHeight : 1 ,
982+ flexShrink : 0 ,
983+ } }
984+ >
985+ ★
986+ </ span >
987+ ) }
859988 < span
860989 style = { {
861990 whiteSpace : "nowrap" ,
0 commit comments