diff --git a/.circleci/config.yml b/.circleci/config.yml index c3c7099bc..dc2fc1a53 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,6 +56,12 @@ jobs: steps: - checkout - install_js + - run: + name: Docs Infra Build + command: pnpm -F './packages/docs-infra' run build + - run: + name: ESLint + command: pnpm eslint:ci - run: name: ESLint command: pnpm eslint:ci diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0d5d57323..32bd807ad 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -127,7 +127,7 @@ All commands are fast in this repository, but network issues or system load can ## Docs Infra Conventions -Follow additional instructions when working in the `@mui/internal-docs-infra` (`packages/docs-infra`) package: +Follow additional instructions when working in the `@mui/internal-docs-infra` (`packages/docs-infra`) package or `docs/app/docs-infra` docs: ### Development Process @@ -165,6 +165,7 @@ Follow additional instructions when working in the `@mui/internal-docs-infra` (` - **4.5** When looking for documentation, start at the `/README.md` and follow links inward. - **4.6** Avoid "breaking the 3rd wall" in code comments and documentation by referring to the instructions provided when working in this repository. Instead, focus on clear, concise explanations of the code itself. - **4.7** When writing code comments, use JSDoc style comments for all functions, but type definitions should be in TypeScript types. Avoid using JSDoc `@typedef` and `@param` tags for types. Use them only for descriptions. +- **4.8** Use progressive disclosure in documentation. Start with simple, common use cases and gradually introduce complexity. Structure docs so readers can stop at their desired depth of understanding. Place advanced sections (like architecture details or performance tuning) at the end of the document after practical content. Follow this pattern: basic usage → configuration → common patterns → reference material → advanced features → implementation details. ### File Organization & Structure @@ -186,7 +187,7 @@ Follow additional instructions when working in the `@mui/internal-docs-infra` (` ### Code Style & Standards -- **7.1** Write type-safe code. Avoid using `any` or `unknown` unless absolutely necessary. Prefer using `unknown` over `any` and always narrow `unknown` to a specific type as soon as possible. User-facing exports should have as strong of typing as possible. +- **7.1** Write type-safe code. Avoid using `any` or `unknown` unless absolutely necessary. Prefer using `unknown` over `any` and always narrow `unknown` to a specific type as soon as possible. Avoid using `as` type assertions except when working with well-known browser APIs or when you have verified the type through runtime checks. Prefer type guards, type predicates, and proper type narrowing over assertions. User-facing exports should have as strong of typing as possible. - **7.2** Use `async/await` for asynchronous code. Avoid using `.then()` and `.catch()`. - **7.3** Use `import { ... } from '...'` syntax for imports. Avoid using `require()`. - **7.4** Use ES modules and `import`/`export` syntax. diff --git a/.lintignore b/.lintignore index 2e75f5d40..a5e0b5c27 100644 --- a/.lintignore +++ b/.lintignore @@ -5,5 +5,7 @@ apps/tools-public/toolpad/**/*.yml build node_modules pnpm-lock.yaml +.next +docs/out __fixtures__ diff --git a/README.md b/README.md index 2b648b917..e2b98b835 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ Mono-repository for the MUI organization with code that can be public. See https://github.com/mui/mui-private for code that needs to be private. +## Documentation + +You can [read the Infra documentation here](./docs/README.md). + ## Applications ### [tools-public.mui.com](https://tools-public.mui.com/) @@ -27,6 +31,11 @@ Internal public Toolpad apps that run the operations of MUI, built using https:/ - Folder: `/packages/docs-infra/` - [Docs](./packages/docs-infra/README.md) +### [code-infra](./packages/code-infra/) + +- Folder: `/packages/code-infra/` +- [Docs](./packages/code-infra/README.md) + ## Versioning Steps: diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..e099e0dec --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +# MUI Infra Docs + +This package contains the documentation for the MUI Infra project, which is responsible for the infrastructure and tooling used in the various MUI documentation sites and libraries. + +[Read in Markdown](<./app/(shared)/page.mdx>) +[Read in Browser](https://infra.mui.com) + +For the most immersive experience, we recommend opening this documentation in VSCode, starting with this README and working deeper into the documentation by navigating through the links provided (ctrl + click). You should have [the MDX extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) installed to view the documentation properly. + +To see demos live, run `pnpm run dev` and open diff --git a/docs/app/(shared)/layout.tsx b/docs/app/(shared)/layout.tsx new file mode 100644 index 000000000..6470235bd --- /dev/null +++ b/docs/app/(shared)/layout.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import styles from '../layout.module.css'; + +export const metadata: Metadata = { + title: 'MUI Infra Documentation', + description: 'How to use the MUI Infra packages', +}; + +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
{children}
+
+ ); +} diff --git a/docs/app/(shared)/page.mdx b/docs/app/(shared)/page.mdx new file mode 100644 index 000000000..40acd7398 --- /dev/null +++ b/docs/app/(shared)/page.mdx @@ -0,0 +1,9 @@ +# MUI Infra + +## Docs Infra + +Read more about `docs-infra` [here](../docs-infra/page.mdx). + +## Code Infra + +Read more about `code-infra` [here](../code-infra/page.mdx). diff --git a/docs/app/code-infra/layout.tsx b/docs/app/code-infra/layout.tsx new file mode 100644 index 000000000..fe4d94238 --- /dev/null +++ b/docs/app/code-infra/layout.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import styles from '../layout.module.css'; + +export const metadata: Metadata = { + title: 'MUI Code Infra Documentation', + description: 'How to use the MUI Code-Infra package', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
+ MUI Code Infra +
+
{children}
+
+ ); +} diff --git a/docs/app/code-infra/page.mdx b/docs/app/code-infra/page.mdx new file mode 100644 index 000000000..8dc058d18 --- /dev/null +++ b/docs/app/code-infra/page.mdx @@ -0,0 +1,21 @@ +# Code Infra + +This is the documentation for the MUI Internal Code Infra package. + +You can install this package using: + +```bash variant=pnpm +pnpm install @mui/internal-code-infra +``` + +```bash variant=yarn +yarn add @mui/internal-code-infra +``` + +```bash variant=npm +npm install @mui/internal-code-infra +``` + +## Usage + +TODO diff --git a/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeController.tsx b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeController.tsx new file mode 100644 index 000000000..bb2436740 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeController.tsx @@ -0,0 +1,15 @@ +'use client'; + +import * as React from 'react'; +import { CodeControllerContext } from '@mui/internal-docs-infra/CodeControllerContext'; +import type { ControlledCode } from '@mui/internal-docs-infra/CodeHighlighter/types'; + +export function CodeController({ children }: { children: React.ReactNode }) { + const [code, setCode] = React.useState(undefined); + + const contextValue = React.useMemo(() => ({ code, setCode }), [code, setCode]); + + return ( + {children} + ); +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditor.tsx b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditor.tsx new file mode 100644 index 000000000..d3dbee261 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditor.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter'; +import { createParseSource } from '@mui/internal-docs-infra/pipeline/parseSource'; + +import { CodeProvider } from '@mui/internal-docs-infra/CodeProvider'; +import { CodeController } from './CodeController'; +import { CodeEditorContent } from './CodeEditorContent'; + +const initialCode = { + Default: { + url: 'file://live-example.js', + fileName: 'live-example.js', + source: `// Welcome to the live code editor! +function greet(name) { + return \`Hello, \${name}!\`; +} +`, + }, +}; + +export function CodeEditor() { + return ( + + + + + + ); +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent.module.css b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent.module.css new file mode 100644 index 000000000..26263573a --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent.module.css @@ -0,0 +1,37 @@ +.container { + border: 1px solid #d0cdd7; + border-radius: 8px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 12px; + border-bottom: 1px solid #d0cdd7; +} + +.name { + color: #65636d; + font-size: 13px; + font-family: var(--font-geist-mono); +} + +.headerActions { + display: flex; + align-items: center; + gap: 8px; +} + +.switchContainer { + display: flex; +} +.code { + padding: 6px; +} + +.codeBlock { + margin: 0; + padding: 6px; + overflow-x: auto; +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent.tsx b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent.tsx new file mode 100644 index 000000000..2f9972650 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; +import { useEditable } from 'use-editable'; +import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types'; +import { useCode } from '@mui/internal-docs-infra/useCode'; +import { LabeledSwitch } from '@/components/LabeledSwitch'; +import styles from './CodeEditorContent.module.css'; + +import '@wooorm/starry-night/style/light'; // load the light theme for syntax highlighting + +export function CodeEditorContent(props: ContentProps) { + const preRef = React.useRef(null); + const code = useCode(props, { preClassName: styles.codeBlock, preRef }); + + const hasJsTransform = code.availableTransforms.includes('js'); + const isJsSelected = code.selectedTransform === 'js'; + const labels = { false: 'TS', true: 'JS' }; + const toggleJs = React.useCallback( + (checked: boolean) => { + code.selectTransform(checked ? 'js' : null); + }, + [code], + ); + + const onInput = React.useCallback( + (text: string) => { + code.setSource?.(text); + }, + [code], + ); + + useEditable(preRef, onInput, { indentation: 2 }); + + return ( +
+
+ {code.selectedFileName} +
+ {hasJsTransform && ( +
+ +
+ )} +
+
+
{code.selectedFile}
+
+ ); +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/code-editor/index.ts b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/index.ts new file mode 100644 index 000000000..be190b299 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/code-editor/index.ts @@ -0,0 +1,7 @@ +import { createDemo } from '@/functions/createDemo'; +import { CodeEditor } from './CodeEditor'; + +export const DemoCodeControllerCodeEditor = createDemo(import.meta.url, CodeEditor, { + name: 'Live Code Editor', + slug: 'live-code-editor', +}); diff --git a/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoController.tsx b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoController.tsx new file mode 100644 index 000000000..b42c1ca94 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoController.tsx @@ -0,0 +1,59 @@ +'use client'; + +import * as React from 'react'; +import { useRunner } from 'react-runner'; +import { CodeControllerContext } from '@mui/internal-docs-infra/CodeControllerContext'; +import type { ControlledCode } from '@mui/internal-docs-infra/CodeHighlighter/types'; +import { useCodeExternals } from '@mui/internal-docs-infra/CodeExternalsContext'; + +function Runner({ code }: { code: string }) { + const externalsContext = useCodeExternals(); + const scope = React.useMemo(() => { + let externals = externalsContext?.externals; + if (!externals) { + externals = { imports: { react: React } }; + } + + return { import: { ...externals } }; + }, [externalsContext]); + + const { element, error } = useRunner({ code, scope }); + + if (error) { + return
{error}
; + } + + return element; +} + +export function DemoController({ children }: { children: React.ReactNode }) { + const [code, setCode] = React.useState(undefined); + + const components = React.useMemo( + () => + code + ? Object.keys(code).reduce( + (acc, cur) => { + const source = code[cur]?.source; + if (!source) { + return acc; + } + + acc[cur] = ; + return acc; + }, + {} as Record, + ) + : undefined, + [code], + ); + + const contextValue = React.useMemo( + () => ({ code, setCode, components }), + [code, setCode, components], + ); + + return ( + {children} + ); +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLive.tsx b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLive.tsx new file mode 100644 index 000000000..e634b8568 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLive.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { CodeProvider } from '@mui/internal-docs-infra/CodeProvider'; +import { DemoController } from './DemoController'; +import { DemoCheckboxBasic } from './demo-basic'; + +export function DemoLive() { + return ( + + + + + + ); +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLiveContent.module.css b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLiveContent.module.css new file mode 100644 index 000000000..dfc66f576 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLiveContent.module.css @@ -0,0 +1,72 @@ +.container { + border: 1px solid #d0cdd7; + border-radius: 8px; +} + +.demoSection { + padding: 24px; +} + +.codeSection { + border-top: 1px solid #d0cdd7; +} + +.header { + border-bottom: 1px solid #d0cdd7; + height: 48px; + position: relative; +} + +.headerContainer { + position: absolute; + width: 100%; + display: flex; + justify-content: space-between; + gap: 8px; +} + +.headerContainer:before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 1px; + margin: -1px; + background-color: #d0cdd7; +} + +.headerActions { + display: flex; + align-items: center; + gap: 8px; + padding-right: 8px; + height: 48px; +} + +.tabContainer { + display: flex; + align-items: center; + gap: 8px; + margin-left: -1px; + padding-bottom: 2px; + overflow-x: auto; +} + +.switchContainer { + display: flex; +} + +.switchContainerHidden { + display: none; +} + +.code { + padding: 10px 6px; +} + +.codeBlock { + margin: 0; + padding: 6px; + overflow-x: auto; +} diff --git a/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLiveContent.tsx b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLiveContent.tsx new file mode 100644 index 000000000..167c91925 --- /dev/null +++ b/docs/app/docs-infra/components/code-controller-context/demos/demo-live/DemoLiveContent.tsx @@ -0,0 +1,88 @@ +'use client'; + +import * as React from 'react'; +import { useEditable } from 'use-editable'; +import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types'; +import { useDemo } from '@mui/internal-docs-infra/useDemo'; +import { LabeledSwitch } from '@/components/LabeledSwitch'; +import { Tabs } from '@/components/Tabs'; +import { Select } from '@/components/Select'; +import styles from './DemoLiveContent.module.css'; + +import '@wooorm/starry-night/style/light'; + +const variantNames: Record = { + CssModules: 'CSS Modules', +}; + +export function DemoLiveContent(props: ContentProps) { + const preRef = React.useRef(null); + const demo = useDemo(props, { preClassName: styles.codeBlock, preRef }); + + const hasJsTransform = demo.availableTransforms.includes('js'); + const isJsSelected = demo.selectedTransform === 'js'; + + const labels = { false: 'TS', true: 'JS' }; + const toggleJs = React.useCallback( + (checked: boolean) => { + demo.selectTransform(checked ? 'js' : null); + }, + [demo], + ); + + const tabs = React.useMemo( + () => demo.files.map(({ name }) => ({ id: name, name })), + [demo.files], + ); + const variants = React.useMemo( + () => + demo.variants.map((variant) => ({ value: variant, label: variantNames[variant] || variant })), + [demo.variants], + ); + + const onChange = React.useCallback( + (text: string) => { + demo.setSource?.(text); + }, + [demo], + ); + useEditable(preRef, onChange, { indentation: 2, disabled: !demo.setSource }); + + return ( +
+
{demo.component}
+
+
+
+
+ +
+
+ {demo.variants.length > 1 && ( + demo.selectVariant(e.target.value)}> + {demo.variants.map((variant) => ( + + ))} + + +