Skip to content

Commit 1ce4fcf

Browse files
ataibarkaiclaude
andcommitted
feat(showcase/shell-dojo): mark highlighted "core" files in code tree
Adopt the same highlighted-file pattern the standalone shell already uses at `showcase/shell/src/app/integrations/[slug]/[demo]/code/page.tsx`: - Each demo's `highlight: […]` list in the integration manifest already flows into the bundled `demo-content.json` as `file.highlighted: true`. The dojo file tree now consumes that flag instead of ignoring it. - Core files render bold + ★ in amber and float to the top within their parent directory. Non-core files render dimmer (muted color, normal weight) so the eye lands on the demo's "real" code first. - A "show all" toggle in the tree header switches between the curated core view (highlights only) and the full tree. Default is "core" when any file is highlighted, otherwise "all". The mode resets per demo. - Selection switched from index to filename so it survives the core⇄all toggle without aliasing to the wrong file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a978b0b commit 1ce4fcf

1 file changed

Lines changed: 163 additions & 34 deletions

File tree

showcase/shell-dojo/src/app/page.tsx

Lines changed: 163 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface DemoContentFile {
1616
filename: string;
1717
language: string;
1818
content: string;
19+
highlighted?: boolean;
1920
}
2021

2122
interface 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+
701737
type 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.
736774
function 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

745788
function 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({
783893
function 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

Comments
 (0)