Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/playground/src/react/context/playground-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@ import type { BrowserHost } from "../../types.js";

export interface PlaygroundContext {
readonly host: BrowserHost;
/** @deprecated Use file management functions instead */
readonly setContent: (content: string) => void;
/** All files in the playground. Maps relative path to content. */
readonly files: Record<string, string>;
/** Currently active file path. */
readonly activeFile: string;
/** Switch the active file being edited. */
readonly setActiveFile: (path: string) => void;
/** Update content of a specific file. */
readonly setFileContent: (path: string, content: string) => void;
/** Add a new file to the playground. */
readonly addFile: (path: string, content?: string) => void;
/** Remove a file from the playground. */
readonly removeFile: (path: string) => void;
/** Rename a file. */
readonly renameFile: (oldPath: string, newPath: string) => void;
}

const PlaygroundContext = createContext<PlaygroundContext | undefined>(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,36 @@
}

.panel-tabs-container {
display: flex;
flex-direction: column;
background-color: var(--colorNeutralBackground3);
}

.tree-toggle {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border: none;
background: none;
cursor: pointer;
color: var(--colorNeutralForeground2);
font-size: 16px;
}

.tree-toggle:hover {
color: var(--colorNeutralForeground1);
background: var(--colorNeutralBackground4);
}

.file-tree-panel {
width: 200px;
min-width: 120px;
max-width: 300px;
border-right: 1px solid var(--colorNeutralStroke2);
overflow: hidden;
}

.panel-content {
flex: 1;
min-width: 0;
Expand Down
178 changes: 141 additions & 37 deletions packages/playground/src/react/editor-panel/editor-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Tab, TabList, type SelectTabEventHandler } from "@fluentui/react-components";
import { SettingsRegular } from "@fluentui/react-icons";
import { ChevronRightRegular, DocumentRegular, SettingsRegular } from "@fluentui/react-icons";
import type { CompilerOptions } from "@typespec/compiler";
import { editor } from "monaco-editor";
import { useCallback, useState, type FunctionComponent, type ReactNode } from "react";
import { useCallback, useMemo, useState, type FunctionComponent, type ReactNode } from "react";
import type { BrowserHost } from "../../types.js";
import type { OnMountData } from "../editor.js";
import { InputFileTree } from "../input-file-tree/index.js";
import type { PlaygroundEditorsOptions } from "../playground.js";
import { TypeSpecEditor } from "../typespec-editor.js";
import { ConfigPanel } from "./config-panel.js";
import style from "./editor-panel.module.css";

export type EditorPanelTab = "tsp" | "cfg";
/** Special tab ID for the tspconfig file */
const CONFIG_TAB = "$$config$$";

const TypeSpecIcon = () => (
// icons/raw/tsp-logo-inverted.svg
Expand All @@ -28,6 +30,13 @@
</svg>
);

function getFileIcon(filePath: string) {
if (filePath === "main.tsp") {
return <TypeSpecIcon />;
}
return <DocumentRegular />;
}

export interface EditorPanelProps {
host: BrowserHost;
model: editor.IModel;
Expand All @@ -42,6 +51,19 @@

/** Toolbar content rendered above the editor area */
commandBar?: ReactNode;

/** Input file paths for file tree */
files?: string[];
/** Currently active file */
activeFile?: string;
/** Callback when a file is selected in the tree */
onFileSelect?: (file: string) => void;
/** Callback to add a new file */
onFileAdd?: (path: string, content?: string) => void;
/** Callback to remove a file */
onFileRemove?: (path: string) => void;
/** Callback to rename a file */
onFileRename?: (oldPath: string, newPath: string) => void;
}

export const EditorPanel: FunctionComponent<EditorPanelProps> = ({
Expand All @@ -55,48 +77,130 @@
onCompilerOptionsChange,
onSelectedEmitterChange,
commandBar,
files,
activeFile,
onFileSelect,
onFileAdd,
onFileRemove,
onFileRename,
}) => {
const [selectedTab, setSelectedTab] = useState<EditorPanelTab>("tsp");
const [fileTreeExpanded, setFileTreeExpanded] = useState(false);

const onTabSelect = useCallback<SelectTabEventHandler>((_, data) => {
setSelectedTab(data.value as EditorPanelTab);
const [showConfig, setShowConfig] = useState(false);

// The selected tab in the sidebar: either a file path or CONFIG_TAB
const selectedTab = showConfig ? CONFIG_TAB : (activeFile ?? "main.tsp");

const onTabSelect = useCallback<SelectTabEventHandler>(
(_, data) => {
const value = data.value as string;
if (value === CONFIG_TAB) {
setShowConfig(true);
} else {
setShowConfig(false);
onFileSelect?.(value);
}
},
[onFileSelect],
);

const handleFileTreeSelect = useCallback(

Check warning on line 107 in packages/playground/src/react/editor-panel/editor-panel.tsx

View workflow job for this annotation

GitHub Actions / Lint

'handleFileTreeSelect' is assigned a value but never used. Allowed unused vars must match /^_/u
(file: string) => {
setShowConfig(false);
onFileSelect?.(file);
},
[onFileSelect],
);

const toggleFileTree = useCallback(() => {
setFileTreeExpanded((v) => !v);
}, []);

// Files list including tspconfig for the tree view
const allFiles = useMemo(() => {
const tspFiles = files ?? ["main.tsp"];
return [...tspFiles, "tspconfig.yaml"];
}, [files]);

// Build the tab icons for the sidebar
const fileTabs = useMemo(() => {
const tabs: { id: string; icon: ReactNode; title: string }[] = [];
if (files) {
for (const f of files) {
tabs.push({ id: f, icon: getFileIcon(f), title: f });
}
} else {
tabs.push({ id: "main.tsp", icon: <TypeSpecIcon />, title: "main.tsp" });
}
// Always add config tab at the end
tabs.push({ id: CONFIG_TAB, icon: <SettingsRegular />, title: "tspconfig.yaml" });
return tabs;
}, [files]);

const editorContent = (
<TypeSpecEditor model={model} actions={actions} options={editorOptions} onMount={onMount} />
);

const mainContent = showConfig ? (
<ConfigPanel
host={host}
selectedEmitter={selectedEmitter}
compilerOptions={compilerOptions}
onCompilerOptionsChange={onCompilerOptionsChange}
onSelectedEmitterChange={onSelectedEmitterChange}
editorOptions={editorOptions}
/>
) : (
editorContent
);

const handleTreeFileSelect = useCallback(
(file: string) => {
if (file === "tspconfig.yaml") {
setShowConfig(true);
} else {
setShowConfig(false);
onFileSelect?.(file);
}
},
[onFileSelect],
);

return (
<div className={style["editor-panel"]}>
<div className={style["panel-tabs-container"]}>
<TabList vertical size="large" selectedValue={selectedTab} onTabSelect={onTabSelect}>
<Tab value="tsp">
<span title="TypeSpec">
<TypeSpecIcon />
</span>
</Tab>
<Tab value="cfg">
<span title="Config">
<SettingsRegular />
</span>
</Tab>
</TabList>
</div>
{fileTreeExpanded ? (
<div className={style["file-tree-panel"]}>
<InputFileTree
files={allFiles}
activeFile={showConfig ? "tspconfig.yaml" : (activeFile ?? "main.tsp")}
onFileSelect={handleTreeFileSelect}
onFileAdd={onFileAdd ?? (() => {})}
onFileRemove={onFileRemove ?? (() => {})}
onFileRename={onFileRename ?? (() => {})}
onCollapse={toggleFileTree}
/>
</div>
) : (
<div className={style["panel-tabs-container"]}>
<button
className={style["tree-toggle"]}
onClick={toggleFileTree}
title="Expand file tree"
>
<ChevronRightRegular />
</button>
<TabList vertical size="large" selectedValue={selectedTab} onTabSelect={onTabSelect}>
{fileTabs.map((tab) => (
<Tab key={tab.id} value={tab.id}>
<span title={tab.title}>{tab.icon}</span>
</Tab>
))}
</TabList>
</div>
)}
<div className={style["panel-content"]}>
{commandBar}
{selectedTab === "tsp" ? (
<TypeSpecEditor
model={model}
actions={actions}
options={editorOptions}
onMount={onMount}
/>
) : (
<ConfigPanel
host={host}
selectedEmitter={selectedEmitter}
compilerOptions={compilerOptions}
onCompilerOptionsChange={onCompilerOptionsChange}
onSelectedEmitterChange={onSelectedEmitterChange}
editorOptions={editorOptions}
/>
)}
{mainContent}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/src/react/editor-panel/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { ConfigPanel, type ConfigPanelProps } from "./config-panel.js";
export { EditorPanel, type EditorPanelProps, type EditorPanelTab } from "./editor-panel.js";
export { EditorPanel, type EditorPanelProps } from "./editor-panel.js";
51 changes: 51 additions & 0 deletions packages/playground/src/react/file-tree/file-tree.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,54 @@
background: var(--colorNeutralBackground3);
padding-top: 4px;
}

/* Reduce empty caret column for leaf nodes (no expand arrow) */
.file-tree div[role="treeitem"] > span:first-child:empty {
width: 4px;
flex: 0 0 4px;
}

.file-tree-with-actions {
display: flex;
flex-direction: column;
height: 100%;
background: var(--colorNeutralBackground3);
}

.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 4px 4px 8px;
border-bottom: 1px solid var(--colorNeutralStroke2);
}

.toolbar-actions {
display: flex;
align-items: center;
gap: 2px;
}

.title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--colorNeutralForeground3);
}

.tree-container {
flex: 1;
overflow: hidden;
}

.inline-input {
padding: 4px 8px;
}

.file-actions {
padding: 2px 4px;
border-top: 1px solid var(--colorNeutralStroke2);
display: flex;
align-items: center;
justify-content: flex-end;
}
8 changes: 4 additions & 4 deletions packages/playground/src/react/file-tree/file-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export interface FileTreeExplorerProps {
readonly onSelect: (file: string) => void;
}

interface FileTreeNode extends TreeNode {
export interface FileTreeNode extends TreeNode {
readonly isDirectory: boolean;
}

const FileNodeIcon: FC<{ node: FileTreeNode }> = ({ node }) => {
export const FileNodeIcon: FC<{ node: FileTreeNode }> = ({ node }) => {
if (node.isDirectory) {
return <FolderRegular />;
}
Expand All @@ -24,7 +24,7 @@ const FileNodeIcon: FC<{ node: FileTreeNode }> = ({ node }) => {
/**
* Builds a tree structure from a flat list of file paths.
*/
function buildTree(files: string[]): FileTreeNode {
export function buildFileTree(files: string[]): FileTreeNode {
const root: FileTreeNode = { id: "__root__", name: "root", isDirectory: true, children: [] };
const dirMap = new Map<string, FileTreeNode>();
dirMap.set("", root);
Expand Down Expand Up @@ -91,7 +91,7 @@ export const FileTreeExplorer: FunctionComponent<FileTreeExplorerProps> = ({
selected,
onSelect,
}) => {
const tree = useMemo(() => buildTree(files), [files]);
const tree = useMemo(() => buildFileTree(files), [files]);

return (
<div className={style["file-tree"]}>
Expand Down
8 changes: 7 additions & 1 deletion packages/playground/src/react/file-tree/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { FileTreeExplorer, type FileTreeExplorerProps } from "./file-tree.js";
export {
buildFileTree,
FileNodeIcon,
FileTreeExplorer,
type FileTreeExplorerProps,
type FileTreeNode,
} from "./file-tree.js";
1 change: 1 addition & 0 deletions packages/playground/src/react/input-file-tree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InputFileTree, type InputFileTreeProps } from "./input-file-tree.js";
Loading
Loading