diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c38ed571 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + +jobs: + release: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 10 # 👈 Required to retrieve git history + + - name: Prepare repository + run: git fetch --unshallow --tags + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Cache node modules + uses: actions/cache@v1 + with: + path: node_modules + key: yarn-deps-${{ hashFiles('yarn.lock') }} + restore-keys: | + yarn-deps-${{ hashFiles('yarn.lock') }} + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + yarn install --frozen-lockfile 2>&1 | grep -v '^[warning|info]' + yarn build + yarn release + # - name: Publish to Chromatic + # uses: chromaui/action@v1 + # with: + # token: ${{ secrets.GITHUB_TOKEN }} + # projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + # workingDir: packages/storybook diff --git a/.gitignore b/.gitignore index 3e670f46..2f96e3e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ node_modules .next todo.md +current.md dist yarn-error.log -site/out \ No newline at end of file +.prettierignore +.env +.vscode +storybook-static +packages/playground/out +packages/starter \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f6ee7ecd..00000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "demos/the-x-in-mdx"] - path = demos/the-x-in-mdx - url = https://github.com/pomber/the-x-in-mdx -[submodule "demos/hooks-talk-demo"] - path = demos/hooks-talk-demo - url = https://github.com/code-hike/hooks-talk-demo -[submodule "demos/swr-minidocs-demo"] - path = demos/swr-minidocs-demo - url = https://github.com/code-hike/swr-minidocs-demo -[submodule "demos/nextjs-post-demo"] - path = demos/nextjs-post-demo - url = https://github.com/code-hike/nextjs-post-demo diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000..95ac4cb2 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,9 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) +# and commit this file to your remote git repository to share the goodness with others. + +tasks: + - init: yarn install && yarn run build + command: yarn run playground + + diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 57b8a9c4..00000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -site/src/**/steps \ No newline at end of file diff --git a/demos/hooks-talk-demo b/demos/hooks-talk-demo deleted file mode 160000 index c1e27c34..00000000 --- a/demos/hooks-talk-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c1e27c347805d224f5c7575515ec6ec028117960 diff --git a/demos/nextjs-post-demo b/demos/nextjs-post-demo deleted file mode 160000 index 608a6dae..00000000 --- a/demos/nextjs-post-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 608a6daebf82a2a434f797f5bc2bbb408624bb29 diff --git a/demos/swr-minidocs-demo b/demos/swr-minidocs-demo deleted file mode 160000 index 06b344ea..00000000 --- a/demos/swr-minidocs-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 06b344eab94cbe5a429bb055a63ec1121a57bc35 diff --git a/demos/the-x-in-mdx b/demos/the-x-in-mdx deleted file mode 160000 index d1ee69b1..00000000 --- a/demos/the-x-in-mdx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d1ee69b120fe0be865324acd42bc02ec337081b2 diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..32cc3a12 --- /dev/null +++ b/lerna.json @@ -0,0 +1,11 @@ +{ + "version": "0.3.0-next.0", + "npmClient": "yarn", + "packages": ["packages/*"], + "useWorkspaces": true, + "command": { + "publish": { + "verifyAccess": false + } + } +} diff --git a/LICENSE b/license similarity index 100% rename from LICENSE rename to license diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index cbf1d659..00000000 --- a/netlify.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build] - publish = "site/out/" - command = "yarn build-site" \ No newline at end of file diff --git a/package.json b/package.json index 2ad2ba8c..f1d5a3a7 100644 --- a/package.json +++ b/package.json @@ -3,29 +3,31 @@ "workspaces": { "packages": [ "packages/*", - "demos/*", - "storybook", - "site" + "external/*" ] }, "scripts": { - "dev": "yarn workspace xmdx dev", - "mbrowser": "yarn workspace @code-hike/mini-browser", - "mframe": "yarn workspace @code-hike/mini-frame", - "mterminal": "yarn workspace @code-hike/mini-terminal", - "meditor": "yarn workspace @code-hike/mini-editor", - "scroller": "yarn workspace @code-hike/scroller", - "player": "yarn workspace @code-hike/player", - "sim-user": "yarn workspace @code-hike/sim-user", - "storybook": "yarn workspace storybook", - "site": "yarn workspace site", - "ch-build": "yarn workspace @code-hike/build", - "watch-packages": "yarn mframe watch & yarn mterminal watch & yarn scroller watch & yarn sim-user watch & yarn player watch & yarn mbrowser watch & yarn meditor watch", - "build-packages": "yarn mframe build && yarn mterminal build && yarn scroller build && yarn sim-user build && yarn player build && yarn mbrowser build && yarn meditor build", - "build-site": "yarn build-packages && yarn workspace site export" + "build": "lerna run --stream x -- build", + "watch": "lerna run --since HEAD --exclude-dependents --parallel x -- watch", + "watch-all": "lerna run --parallel x -- watch", + "storybook": "lerna run --scope storybook start --stream", + "playground": "lerna run --scope @*/playground dev --stream", + "docs": "lerna run build-docs --stream", + "build-playground": "lerna run --scope @*/playground build --stream", + "release": "auto shipit" }, - "dependencies": { - "@emotion/core": "10.0.7", - "theme-ui": "0.2.31" + "devDependencies": { + "auto": "^10.18.7", + "lerna": "^4.0.0", + "prettier": "^2.5.1" + }, + "repository": "code-hike/codehike", + "author": "pomber ", + "auto": { + "plugins": [ + "npm", + "released" + ], + "onlyPublishWithReleaseLabel": true } } diff --git a/.prettierrc b/packages/.prettierrc similarity index 100% rename from .prettierrc rename to packages/.prettierrc diff --git a/packages/build/build.js b/packages/build/build.js deleted file mode 100755 index 5a56e3fa..00000000 --- a/packages/build/build.js +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env node - -const path = require("path"); -const spawn = require("child_process").spawn; - -const configPath = path.resolve(__dirname, "rollup.config.js"); - -// console.log({ configPath, cwd: process.cwd(), args: process.argv.slice(2) }); - -spawn("yarn", ["rollup", "-c", configPath, ...process.argv.slice(2)], { - stdio: "inherit", - // cwd: "foo" -}); diff --git a/packages/build/package.json b/packages/build/package.json deleted file mode 100644 index 22e75489..00000000 --- a/packages/build/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@code-hike/build", - "version": "0.0.1", - "bin": { - "ch-build": "./build.js" - }, - "dependencies": { - "autoprefixer": "^9.8.2", - "cssnano": "^4.1.10", - "rollup": "^2.17.0", - "rollup-plugin-styles": "^3.8.2", - "rollup-plugin-typescript2": "^0.27.1", - "typescript": "^3.9.5" - } -} diff --git a/packages/build/rollup.config.js b/packages/build/rollup.config.js deleted file mode 100644 index 3d3e7ff5..00000000 --- a/packages/build/rollup.config.js +++ /dev/null @@ -1,50 +0,0 @@ -import styles from "rollup-plugin-styles"; -import autoprefixer from "autoprefixer"; -// import postcss from "rollup-plugin-postcss"; -// import embedCSS from "rollup-plugin-embed-css"; -import cssnano from "cssnano"; -import typescript from "rollup-plugin-typescript2"; - -const plugins = [ - typescript(), - styles({ - plugins: [autoprefixer(), cssnano()], - mode: [ - "inject", - { - // container: "body", - singleTag: true, - // prepend: true, - // attributes: { id: "global" }, - }, - ], - }), - // // postcss({ - // // plugins: [], - // // }), -]; - -const createConfig = (filename) => ({ - input: `src/${filename}.tsx`, - output: [ - { - file: `./dist/${filename}.js`, - format: "umd", - name: "foo", - }, - { - file: `./dist/${filename}.cjs.js`, - format: "cjs", - name: "foo", - }, - { - file: `./dist/${filename}.esm.js`, - format: "es", - }, - ], - plugins, -}); - -const configs = ["index"].map((filename) => createConfig(filename)); - -export default configs; diff --git a/packages/player/package.json b/packages/classer/package.json similarity index 54% rename from packages/player/package.json rename to packages/classer/package.json index a1975868..92e974d3 100644 --- a/packages/player/package.json +++ b/packages/classer/package.json @@ -1,30 +1,29 @@ { - "name": "@code-hike/player", - "version": "0.2.1", - "main": "dist/index.js", + "name": "@code-hike/classer", + "version": "0.3.0-next.0", + "main": "dist/index.cjs.js", "typings": "dist/index.d.ts", "module": "dist/index.esm.js", "files": [ "dist" ], "scripts": { - "build": "ch-build", - "watch": "ch-build --watch" + "x": "x" }, "devDependencies": { - "@code-hike/build": "0.0.1", - "@types/react": "^16.9.38", - "react": "^16.13.1" + "@code-hike/script": "0.0.1", + "@types/react": "^17.0.2", + "react": "^17.0.2" }, - "dependencies": {}, "peerDependencies": { - "react": ">=16.8" + "react": "^16.8.3 || ^17 || ^18" }, - "keywords": [ - "react" - ], "homepage": "https://codehike.org", "repository": "code-hike/codehike", + "publishConfig": { + "access": "public" + }, + "author": "Rodrigo Pombo", "license": "MIT", "funding": { "type": "opencollective", diff --git a/packages/classer/readme.md b/packages/classer/readme.md new file mode 100644 index 00000000..18d5e65f --- /dev/null +++ b/packages/classer/readme.md @@ -0,0 +1,106 @@ +> Classer is a tool from [Code Hike](https://codehike.org) + +A little package to make React component libraries interoperable with most styling solutions. ([See this twitter thread explaining why this is useful](https://twitter.com/pomber/status/1362125599607820290)) + +You write your library code like this: + +```jsx +// foo-library code +import { useClasser } from "@code-hike/classer" + +export function Foo({ classes }) { + const c = useClasser("foo", classes) + return ( +
+

Hello

+

World

+ +
+ ) +} +``` + +And the library consumers use it like this in their apps: + +```jsx +import { Foo } from "foo-library" + +const classes = { + "foo-title": "my-app-blue", + "foo-img": "rounded-corners some-border", +} + +function MyApp() { + return +} +``` + +`MyApp` renders this HTML: + +```html +
+

Hello

+

World

+ +
+``` + +Examples: + +- [With Emotion](https://codesandbox.io/s/classer-emotion-b7go0) +- [With Tailwind](https://codesandbox.io/s/classer-tailwind-wfs1d) + +## Context + +You can also do this (to avoid passing `classes` to nested components): + +```jsx +// foo-library code +import { + useClasser, + ClasserProvider, +} from "@code-hike/classer" + +export function Foo({ classes }) { + return ( + + + + + ) +} + +function FirstChild() { + const c = useClasser("foo-first") + return

Hi

+} + +function SecondChild() { + const c = useClasser("foo-second") + return

Ho

+} +``` + +```jsx +import { Foo } from "./foo-library" +import styles from "./app.module.css" + +const classes = { + "foo-title": styles.myTitle, + "foo-img": styles.myImage, +} + +function MyApp() { + return +} +``` + +## License + +MIT diff --git a/packages/classer/src/index.tsx b/packages/classer/src/index.tsx new file mode 100644 index 00000000..8ba01fc1 --- /dev/null +++ b/packages/classer/src/index.tsx @@ -0,0 +1,76 @@ +import React, { + createContext, + useContext, + useCallback, + useMemo, +} from "react" + +export { ClasserProvider, useClasser, Classes } + +type AppClassName = string +type LibClassName = string +type Classes = Record + +const ClasserContext = createContext({}) + +interface ClasserProviderProps { + classes: Classes | undefined + children?: React.ReactNode +} + +function ClasserProvider({ + classes, + children, +}: ClasserProviderProps) { + const outer = useContext(ClasserContext) + const mixed = useMerge(outer, classes) + return ( + + ) +} + +function useClasser( + libClassPrefix: string, + innerClasses?: Classes +) { + const outerClasses = useContext(ClasserContext) + const classes = useMerge(outerClasses, innerClasses) + return useCallback(getClasser(libClassPrefix, classes), [ + libClassPrefix, + classes, + ]) +} + +function getClasser( + libClassPrefix: string, + classes: Classes +) { + return function classer(...libClassSuffixList: string[]) { + const libClassList = libClassSuffixList.map( + libClassSuffix => + libClassPrefix + + (libClassPrefix && libClassSuffix ? "-" : "") + + libClassSuffix + ) + const outputList = libClassList.slice() + libClassList.forEach(libClass => { + if (libClass in classes) { + outputList.push(classes[libClass]) + } + }) + return outputList.join(" ") + } +} + +function useMerge( + outer: Classes | undefined, + inner: Classes | undefined +) { + return useMemo(() => ({ ...outer, ...inner }), [ + outer, + inner, + ]) +} diff --git a/packages/player/tsconfig.json b/packages/classer/tsconfig.json similarity index 83% rename from packages/player/tsconfig.json rename to packages/classer/tsconfig.json index 3e7693c2..8b1dd15e 100644 --- a/packages/player/tsconfig.json +++ b/packages/classer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../tsconfig.json", "include": ["src", "types"], "compilerOptions": { "rootDir": "./src", diff --git a/packages/highlighter/package.json b/packages/highlighter/package.json new file mode 100644 index 00000000..5e666342 --- /dev/null +++ b/packages/highlighter/package.json @@ -0,0 +1,33 @@ +{ + "name": "@code-hike/highlighter", + "version": "0.3.0-next.0", + "main": "dist/index.cjs.js", + "typings": "dist/index.d.ts", + "module": "dist/index.esm.js", + "sideEffects": false, + "files": [ + "dist" + ], + "scripts": { + "x": "x" + }, + "devDependencies": { + "@code-hike/script": "0.0.1", + "@types/react": "^17.0.2" + }, + "dependencies": { + "@code-hike/utils": "^0.3.0-next.0", + "shiki": "^0.10.1" + }, + "homepage": "https://codehike.org", + "repository": "code-hike/codehike", + "publishConfig": { + "access": "public" + }, + "author": "Rodrigo Pombo", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/codehike" + } +} diff --git a/packages/player/readme.md b/packages/highlighter/readme.md similarity index 100% rename from packages/player/readme.md rename to packages/highlighter/readme.md diff --git a/packages/highlighter/src/index.tsx b/packages/highlighter/src/index.tsx new file mode 100644 index 00000000..5128f553 --- /dev/null +++ b/packages/highlighter/src/index.tsx @@ -0,0 +1,113 @@ +import { + Highlighter, + setCDN, + getHighlighter, + IShikiTheme, + IThemedToken, + FontStyle, + Lang, +} from "shiki" +import { Code } from "@code-hike/utils" + +let highlighterPromise: Promise | null = null +let highlighter: Highlighter | null = null + +const newlineRe = /\r\n|\r|\n/ + +export async function highlight({ + code, + lang, + theme, +}: { + code: string + lang: string + theme: any // TODO type this +}): Promise { + if (lang === "text") { + const lines = code ? code.split(newlineRe) : [""] + return { + lines: lines.map(line => ({ + tokens: [{ content: line, props: {} }], + })), + lang, + } + } + if (highlighterPromise === null) { + setCDN("https://unpkg.com/shiki/") + highlighterPromise = getHighlighter({ + theme: theme as IShikiTheme, + // langs: [lang as Lang], // TODO change lang from string to Lang + }) + } + + if (highlighter === null) { + highlighter = await highlighterPromise + } + if (missingTheme(highlighter, theme)) { + await highlighter.loadTheme(theme as IShikiTheme) + } + if (missingLang(highlighter, lang)) { + try { + await highlighter.loadLanguage(lang as Lang) + } catch (e) { + console.warn( + "[Code Hike warning]", + `${lang} is not a valid language, no syntax highlighting will be applied.` + ) + return highlight({ code, lang: "text", theme }) + } + } + + const tokenizedLines = highlighter.codeToThemedTokens( + code, + lang, + theme.name, + { + includeExplanation: false, + } + ) + + const lines = tokenizedLines.map(line => ({ + tokens: line.map(token => ({ + content: token.content, + props: { style: getStyle(token) }, + })), + })) + + return { lines, lang } +} + +function missingTheme( + highlighter: Highlighter, + theme: any +) { + return !highlighter + .getLoadedThemes() + .some(t => t === theme.name) +} + +function missingLang( + highlighter: Highlighter, + lang: string +) { + return !highlighter + .getLoadedLanguages() + .some(l => l === lang) +} + +const FONT_STYLE_TO_CSS = { + [FontStyle.NotSet]: {}, + [FontStyle.None]: {}, + [FontStyle.Italic]: { fontStyle: "italic" }, + [FontStyle.Bold]: { fontWeight: "bold" }, + [FontStyle.Underline]: { textDecoration: "underline" }, +} +function getStyle(token: IThemedToken) { + const fontStyle = token.fontStyle + ? FONT_STYLE_TO_CSS[token.fontStyle] + : {} + return { + color: token.color, + ...fontStyle, + } +} diff --git a/packages/highlighter/tsconfig.json b/packages/highlighter/tsconfig.json new file mode 100644 index 00000000..8b1dd15e --- /dev/null +++ b/packages/highlighter/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "include": ["src", "types"], + "compilerOptions": { + "rootDir": "./src", + "baseUrl": "./", + "paths": { + "@": ["./"], + "*": ["src/*", "node_modules/*"] + } + } +} diff --git a/packages/mdx/package.json b/packages/mdx/package.json new file mode 100644 index 00000000..aec680c0 --- /dev/null +++ b/packages/mdx/package.json @@ -0,0 +1,57 @@ +{ + "name": "@code-hike/mdx", + "version": "0.3.0-next.0", + "main": "dist/index.cjs.js", + "typings": "dist/index.d.ts", + "module": "dist/index.esm.js", + "sideEffects": false, + "style": "dist/index.css", + "files": [ + "dist" + ], + "inputs": [ + "index", + "components" + ], + "scripts": { + "x": "x" + }, + "devDependencies": { + "@code-hike/script": "0.0.1", + "@types/react": "^17.0.2", + "react": "^17.0.2" + }, + "dependencies": { + "@code-hike/highlighter": "^0.3.0-next.0", + "@code-hike/mini-browser": "^0.3.0-next.0", + "@code-hike/mini-editor": "^0.3.0-next.0", + "@code-hike/scroller": "^0.3.0-next.0", + "@code-hike/smooth-code": "^0.3.0-next.0", + "@code-hike/utils": "^0.3.0-next.0", + "@codesandbox/sandpack-client": "^0.1.20", + "hast-util-to-estree": "^1.4.0", + "is-plain-obj": "^3.0.0", + "node-fetch": "^2.0.0", + "remark-rehype": "^8.1.0", + "unified": "^9.2.2", + "unist-util-visit": "^2.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "keywords": [ + "react" + ], + "homepage": "https://codehike.org", + "repository": "code-hike/codehike", + "publishConfig": { + "access": "public" + }, + "author": "Rodrigo Pombo", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/codehike" + } +} diff --git a/packages/mdx/readme.md b/packages/mdx/readme.md new file mode 100644 index 00000000..2a3bd5f6 --- /dev/null +++ b/packages/mdx/readme.md @@ -0,0 +1 @@ +See [codehike.org](https://codehike.org) diff --git a/packages/mdx/src/client/annotations.tsx b/packages/mdx/src/client/annotations.tsx new file mode 100644 index 00000000..6162e208 --- /dev/null +++ b/packages/mdx/src/client/annotations.tsx @@ -0,0 +1,220 @@ +import React from "react" +import { CodeAnnotation } from "@code-hike/smooth-code" +import { + getColor, + transparent, + ColorName, +} from "@code-hike/utils" + +export function Annotation() { + return "error: code hike remark plugin not running or annotation isn't at the right place" +} + +export const annotationsMap: Record< + string, + CodeAnnotation["Component"] +> = { + box: Box, + bg: Background, + label: Label, + link: CodeLink, + mark: Mark, +} + +function Mark({ + children, + data, + theme, +}: { + data: any + children: React.ReactNode + theme: any +}) { + const bg = + data && typeof data === "string" + ? data + : tryGuessColor(children) || + transparent( + getColor(theme, ColorName.CodeForeground), + 0.2 + ) + + return ( + + {children} + + ) +} + +function tryGuessColor( + children: React.ReactNode +): string | undefined { + const child = React.Children.toArray(children)[0] as any + + const grandChild = React.Children.toArray( + child?.props?.children || [] + )[0] as any + + const grandGrandChild = React.Children.toArray( + grandChild?.props?.children || [] + )[0] as any + + const { color } = grandGrandChild?.props?.style + + if (color) { + return transparent(color as string, 0.2) + } + + return undefined +} + +function Box({ + children, + data, + theme, +}: { + data: any + children: React.ReactNode + theme: any +}) { + const border = + typeof data === "string" + ? data + : theme.tokenColors.find((tc: any) => + tc.scope?.includes("string") + )?.settings?.foreground || "yellow" + return ( + + {children} + + ) +} + +function Background({ + children, + data, + style, + theme, +}: { + data: string + children: React.ReactNode + style?: React.CSSProperties + theme?: any +}) { + const bg = + data || + (((theme as any).colors[ + "editor.lineHighlightBackground" + ] || + (theme as any).colors[ + "editor.selectionHighlightBackground" + ]) as string) + return ( +
+ + {children} +
+ ) +} + +function Label({ + children, + data, + style, + theme, +}: { + data: any + children: React.ReactNode + style?: React.CSSProperties + theme?: any +}) { + const bg = ((theme as any).colors[ + "editor.lineHighlightBackground" + ] || + (theme as any).colors[ + "editor.selectionHighlightBackground" + ]) as string + const [hover, setHover] = React.useState(false) + + return ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + > + {children} +
+ {data?.children || data} +
+
+ ) +} + +function CodeLink({ + children, + data, +}: { + data: + | { + url: string + title: string | undefined + } + | string + children: React.ReactNode +}) { + const url = (data as any)?.url || data + const title = (data as any)?.title + return ( + + {children} + + ) +} diff --git a/packages/mdx/src/client/code.tsx b/packages/mdx/src/client/code.tsx new file mode 100644 index 00000000..507f7503 --- /dev/null +++ b/packages/mdx/src/client/code.tsx @@ -0,0 +1,78 @@ +import React from "react" +import { CodeSpring } from "@code-hike/smooth-code" +import { + EditorSpring, + EditorProps, + EditorStep, +} from "@code-hike/mini-editor" + +export function Code(props: EditorProps) { + const [step, setStep] = React.useState(props) + + function onTabClick(filename: string) { + const newStep = updateEditorStep(step, filename, null) + setStep({ ...step, ...newStep }) + } + + return +} + +export function InnerCode({ + onTabClick, + ...props +}: EditorProps & { + onTabClick?: (filename: string) => void +}) { + if ( + !props.southPanel && + props.files.length === 1 && + !props.files[0].name + ) { + return ( +
+ +
+ ) + } else { + const frameProps = { + ...props?.frameProps, + onTabClick, + } + return ( +
+ +
+ ) + } +} + +export function updateEditorStep( + step: EditorStep, + filename: string | undefined, + focus: string | null +): EditorStep { + const name = filename || step.northPanel.active + const newFiles = step.files.map((file: any) => + file.name === name + ? { + ...file, + focus: focus === null ? file.focus : focus, + } + : file + ) + + let northPanel = { ...step.northPanel } + let southPanel = step.southPanel && { + ...step.southPanel, + } + if (step.northPanel.tabs.includes(name)) { + northPanel.active = name + } else if (southPanel) { + southPanel.active = name + } + return { files: newFiles, northPanel, southPanel } +} diff --git a/packages/mdx/src/client/inline-code.tsx b/packages/mdx/src/client/inline-code.tsx new file mode 100644 index 00000000..1bbf355f --- /dev/null +++ b/packages/mdx/src/client/inline-code.tsx @@ -0,0 +1,55 @@ +import React from "react" +import { + EditorTheme, + getColor, + transparent, + ColorName, + Code, +} from "@code-hike/utils" + +export function InlineCode({ + className, + codeConfig, + children, + code, + ...rest +}: { + className: string + code: Code + children?: React.ReactNode + codeConfig: { theme: EditorTheme } +}) { + const { theme } = codeConfig + const { lines } = code + const allTokens = lines.flatMap(line => line.tokens) + const foreground = getColor( + theme, + ColorName.CodeForeground + ) + return ( + + + {allTokens.map((token, j) => ( + + {token.content} + + ))} + + + ) +} diff --git a/packages/mdx/src/client/preview.tsx b/packages/mdx/src/client/preview.tsx new file mode 100644 index 00000000..eabeef0b --- /dev/null +++ b/packages/mdx/src/client/preview.tsx @@ -0,0 +1,106 @@ +import React from "react" +import { MiniBrowser } from "@code-hike/mini-browser" +import { + SandpackClient, + SandpackBundlerFiles, + SandboxInfo, +} from "@codesandbox/sandpack-client" +import { EditorStep } from "@code-hike/mini-editor" +import { EditorTheme } from "@code-hike/utils" + +export type PresetConfig = SandboxInfo +export function Preview({ + className, + files, + presetConfig, + show, + children, + codeConfig, + style, + ...rest +}: { + className: string + files: EditorStep["files"] + presetConfig?: PresetConfig + show?: string + style?: React.CSSProperties + children?: React.ReactNode + codeConfig: { theme: EditorTheme } +}) { + return ( +
+ + ) : ( + children + ) + } + /> +
+ ) +} + +function SandpackPreview({ + files, + presetConfig, +}: { + files: EditorStep["files"] + presetConfig: PresetConfig +}) { + const iframeRef = React.useRef(null!) + const clientRef = React.useRef(null!) + + React.useEffect(() => { + clientRef.current = new SandpackClient( + iframeRef.current, + { + ...presetConfig, + files: mergeFiles(presetConfig.files, files), + }, + { + showOpenInCodeSandbox: false, + // showErrorScreen: false, + // showLoadingScreen: false, + } + ) + }, []) + + React.useEffect(() => { + if (clientRef.current) { + clientRef.current.updatePreview({ + ...presetConfig, + files: mergeFiles(presetConfig.files, files), + }) + } + }, [files]) + + return