diff --git a/contributing.md b/contributing.md index 31bccc45..da48e509 100644 --- a/contributing.md +++ b/contributing.md @@ -11,17 +11,3 @@ Remove a submodule: ```bash $ ./scripts/rm-submodule.sh react-tutorial-demo ``` - -## change version - -Change all packages versions (will change to `0.0.0+last-commit-sha`): - -```bash -$ yarn new-version -``` - -Or specific version: - -```bash -$ yarn new-version 1.0.0 -``` diff --git a/packages/mini-editor/src/code-walk.tsx b/packages/mini-editor/src/code-walk.tsx deleted file mode 100644 index 7b0f0d08..00000000 --- a/packages/mini-editor/src/code-walk.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react" - -interface CodeWalkStep { - code?: string - focus?: string - // TODO this shoudln't change between steps - lang?: string -} - -interface CodeWalkProps - extends React.PropsWithoutRef< - JSX.IntrinsicElements["div"] - > { - /** A number between 0 and `steps.length - 1`. */ - progress?: number - /** Default code for all steps. */ - code?: string - /** Default focus for all steps. */ - focus?: string - lang?: string - steps?: CodeWalkStep[] - parentHeight?: number - minColumns?: number -} - -export { CodeWalk } - -function CodeWalk({ - steps = [], - ...rest -}: CodeWalkProps): React.ReactNode { - if (steps.length === 0) return null - return
-} diff --git a/packages/mini-editor/src/code.tsx b/packages/mini-editor/src/code.tsx index 8342365c..1d6e37d5 100644 --- a/packages/mini-editor/src/code.tsx +++ b/packages/mini-editor/src/code.tsx @@ -7,7 +7,7 @@ import { getFocusIndexes, } from "./focus-parser" -type CodeProps = { +export type CodeProps = { prevCode: string prevFocus: string | null nextCode: string @@ -15,10 +15,10 @@ type CodeProps = { progress: number language: string parentHeight?: number - minColumns: number - minZoom: number - maxZoom: number - horizontalCenter: boolean + minColumns?: number + minZoom?: number + maxZoom?: number + horizontalCenter?: boolean } export function Code({ @@ -29,10 +29,10 @@ export function Code({ progress, language, parentHeight, - minColumns, - minZoom, - maxZoom, - horizontalCenter, + minColumns = 40, + minZoom = 0.5, + maxZoom = 1.5, + horizontalCenter = false, }: CodeProps) { const { prevLines, diff --git a/packages/mini-editor/src/editor-frame.tsx b/packages/mini-editor/src/editor-frame.tsx index b6e19a0c..a5af1dd3 100644 --- a/packages/mini-editor/src/editor-frame.tsx +++ b/packages/mini-editor/src/editor-frame.tsx @@ -3,77 +3,154 @@ import { MiniFrame, FrameButtons, } from "@code-hike/mini-frame" -import { Classes, useClasser } from "@code-hike/classer" -import "./index.scss" +import { useClasser, Classes } from "@code-hike/classer" -export { EditorFrame, TerminalPanel } +export { + EditorFrameProps, + getPanelStyles, + Snapshot, + OutputPanel, + TabsSnapshot, + Tab, +} -const DEFAULT_HEIGHT = 200 +type Tab = { + title: string + active: boolean + style: React.CSSProperties +} + +type OutputPanel = { + tabs: Tab[] + style: React.CSSProperties + children: React.ReactNode +} type EditorFrameProps = { - files: string[] - active: string + northPanel: OutputPanel + southPanel?: OutputPanel | null + terminalPanel?: React.ReactNode height?: number - terminalPanel: React.ReactNode button?: React.ReactNode classes?: Classes } & React.PropsWithoutRef -function EditorFrame({ - files, - active, - children, - terminalPanel, - height, - style, - button, - classes, - ...rest -}: EditorFrameProps) { +const DEFAULT_HEIGHT = 200 + +export const EditorFrame = React.forwardRef< + HTMLDivElement, + EditorFrameProps +>(function InnerEditorFrame( + { + northPanel, + southPanel, + terminalPanel, + style, + height, + button, + className, + ...rest + }, + ref +) { const c = useClasser("ch-editor") return ( } - classes={classes} - style={{ height: height ?? DEFAULT_HEIGHT, ...style }} {...rest} > -
{children}
+
+ {southPanel && ( +
+
+ +
+
+
+ )} {terminalPanel} ) -} +}) type TabsContainerProps = { - files: string[] - active: string + tabs: Tab[] button?: React.ReactNode + showFrameButtons: boolean + topBorder?: boolean + panel: "north" | "south" } function TabsContainer({ - files, - active, + tabs, button, + showFrameButtons, + topBorder, + panel, }: TabsContainerProps) { const c = useClasser("ch-editor-tab") return ( <> - - {files.map(fileName => ( + {topBorder && ( +
+ )} + {showFrameButtons ? :
} + {tabs.map(({ title, active, style }) => (
-
{fileName}
+
{title}
))}
@@ -82,22 +159,172 @@ function TabsContainer({ ) } -type TerminalPanelProps = { - height?: number - children: React.ReactNode +type TabsSnapshot = Record< + string, + { left: number; active: boolean; width: number } +> +type Snapshot = { + titleBarHeight: number + northKey: any + northHeight: number + northTabs: TabsSnapshot + southKey: any + southHeight: number | null + southTabs: TabsSnapshot | null } -function TerminalPanel({ - height, - children, -}: TerminalPanelProps) { - return !height ? null : ( -
-
- Terminal -
-
- {children} -
-
- ) + +function getPanelStyles( + prev: Snapshot, + next: Snapshot, + t: number +): { + northStyle: React.CSSProperties + southStyle?: React.CSSProperties +} { + // +---+---+ + // | x | x | + // +---+---+ + // | | | + // +---+---+ + if ( + prev.southHeight === null && + next.southHeight === null + ) { + return { + northStyle: { + height: prev.northHeight, + }, + } + } + + // +---+---+ + // | x | x | + // +---+---+ + // | y | | + // +---+---+ + if ( + prev.southHeight !== null && + next.southHeight === null && + next.northKey !== prev.southKey + ) { + return { + northStyle: { + height: tween( + prev.northHeight, + next.northHeight, + t + ), + }, + southStyle: { + height: prev.southHeight, + }, + } + } + + // +---+---+ + // | x | y | + // +---+---+ + // | y | | + // +---+---+ + if ( + prev.southHeight !== null && + next.southHeight === null && + prev.southKey === next.northKey + ) { + return { + northStyle: { + height: prev.northHeight, + }, + southStyle: { + position: "relative", + height: tween( + prev.southHeight, + next.northHeight + next.titleBarHeight, + t + ), + transform: `translateY(${tween( + 0, + -(prev.northHeight + prev.titleBarHeight), + t + )}px)`, + }, + } + } + + // +---+---+ + // | x | x | + // +---+---+ + // | | y | + // +---+---+ + if ( + prev.southHeight === null && + next.southHeight !== null && + prev.northKey !== next.southKey + ) { + return { + northStyle: { + height: tween( + prev.northHeight, + next.northHeight, + t + ), + }, + southStyle: { + position: "relative", + height: next.southHeight!, + }, + } + } + + // +---+---+ + // | y | x | + // +---+---+ + // | | y | + // +---+---+ + if ( + prev.southHeight === null && + next.southHeight !== null && + prev.northKey === next.southKey + ) { + return { + northStyle: { + height: next.northHeight, + }, + southStyle: { + position: "relative", + height: tween( + prev.northHeight + prev.titleBarHeight, + next.southHeight!, + t + ), + transform: `translateY(${tween( + -(next.northHeight + next.titleBarHeight), + 0, + t + )}px)`, + }, + } + } + + // +---+---+ + // | x | x | + // +---+---+ + // | y | y | + // +---+---+ + return { + northStyle: { + height: tween(prev.northHeight, next.northHeight, t), + }, + southStyle: { + height: tween( + prev.southHeight!, + next.southHeight!, + t + ), + }, + } +} + +function tween(a: number, b: number, t: number) { + return a + (b - a) * t } diff --git a/packages/mini-editor/src/index.scss b/packages/mini-editor/src/index.scss index f028da5e..d7099366 100644 --- a/packages/mini-editor/src/index.scss +++ b/packages/mini-editor/src/index.scss @@ -17,12 +17,14 @@ padding-left: 15px; padding-right: 15px; background-color: rgb(45, 45, 45); + // background-color: maroon; color: rgba(255, 255, 255, 0.5); min-width: 0; } .ch-editor-tab-active { background-color: rgb(30, 30, 30); + // background-color: lightseagreen; color: rgb(255, 255, 255); min-width: unset; } @@ -36,16 +38,20 @@ overflow: hidden; } +.ch-editor-frame { + --ch-content-background: rgb(37, 37, 38); +} + /** body */ .ch-editor-body { background-color: rgb(30, 30, 30); - height: 100%; color: #cccccc; font-size: 15px; // padding: 5px 10px; line-height: 1.1rem; box-sizing: border-box; + position: relative; } .ch-editor-body pre { diff --git a/packages/mini-editor/src/index.tsx b/packages/mini-editor/src/index.tsx index 4b025c45..09a24791 100644 --- a/packages/mini-editor/src/index.tsx +++ b/packages/mini-editor/src/index.tsx @@ -1,14 +1,28 @@ import "./index.scss" +import { + MiniEditorTween, + MiniEditorTweenProps, +} from "./mini-editor-tween" import { MiniEditorHike, MiniEditorHikeProps, + EditorStep, } from "./mini-editor-hike" -import { MiniEditor, MiniEditorProps } from "./mini-editor" +import { + MiniEditor, + MiniEditorProps, +} from "./mini-editor-spring" +import { mdxToStep, mdxToSteps } from "./mdx" export { - MiniEditorHike, + mdxToStep, + mdxToSteps, + MiniEditorTween, + MiniEditorTweenProps, MiniEditor, - MiniEditorHikeProps, MiniEditorProps, + MiniEditorHike, + MiniEditorHikeProps, + EditorStep, } diff --git a/packages/mini-editor/src/mdx.tsx b/packages/mini-editor/src/mdx.tsx new file mode 100644 index 00000000..c5380fe0 --- /dev/null +++ b/packages/mini-editor/src/mdx.tsx @@ -0,0 +1,199 @@ +import React from "react" +import { EditorStep, StepFile } from "./use-snapshots" + +export { mdxToStep, mdxToSteps } + +type Settings = { + defaultFileName?: string +} + +function mdxToSteps( + children: any[], + settings: Settings = {} +) { + const steps = [] as EditorStep[] + children.forEach((child, i) => { + steps.push(mdxToStep(child, steps[i - 1], settings)) + }) + return steps +} + +const defaultFileName = "index.js" + +function mdxToStep( + child: any, + prev: EditorStep | undefined, + settings: Settings = {} +): EditorStep { + const stepProps = child?.props || {} + const stepChildren = React.Children.toArray( + stepProps.children + ) + + const separatorIndex = stepChildren.findIndex( + (child: any) => child?.props?.mdxType === "hr" + ) + + const hasTwoPanels = separatorIndex !== -1 + + const northChildren = hasTwoPanels + ? stepChildren.slice(0, separatorIndex) + : stepChildren + const southChildren = hasTwoPanels + ? stepChildren.slice(separatorIndex + 1) + : null + + const northFiles = northChildren.map(pre => + preToFile(pre, prev ? prev.files : [], settings) + ) + const southFiles = southChildren?.map(pre => + preToFile(pre, prev ? prev.files : [], settings) + ) + + const prevFiles = prev?.files || [] + const files = [ + ...prevFiles.filter( + f => + !northFiles.some(nf => nf.name === f.name) && + !southFiles?.some(sf => sf.name === f.name) + ), + ...northFiles, + ...(southFiles || []), + ] + + return { + files, + northPanel: { + tabs: chooseNorthTabs(prev, northFiles, southFiles), + active: chooseActiveFile( + northFiles, + prev?.northPanel.active + ), + heightRatio: 0.5, + }, + southPanel: + southFiles && southFiles.length + ? { + tabs: chooseSouthTabs( + prev, + northFiles, + southFiles + ), + active: chooseActiveFile( + southFiles, + prev?.southPanel?.active + ), + heightRatio: 0.5, + } + : undefined, + } +} + +function chooseNorthTabs( + prev: EditorStep | undefined, + northFiles: FileWithOptions[], + southFiles: FileWithOptions[] | undefined +) { + // old north tabs + new north tabs (except hidden) - new south tabs + const oldNorthTabs = prev?.northPanel.tabs || [] + const newSouthTabs = (southFiles || []).map(f => f.name) + const newNorthTabs = northFiles + .filter( + f => !f.hidden && !oldNorthTabs.includes(f.name) + ) + .map(f => f.name) + + const baseTabs = oldNorthTabs.filter( + tab => !newSouthTabs.includes(tab) + ) + return [...baseTabs, ...newNorthTabs] +} + +function chooseSouthTabs( + prev: EditorStep | undefined, + northFiles: FileWithOptions[], + southFiles: FileWithOptions[] +) { + // old south tabs + new south tabs (except hidden) - new north tabs + const oldSouthTabs = prev?.southPanel?.tabs || [] + const newSouthTabs = (southFiles || []) + .filter( + f => !f.hidden && !oldSouthTabs.includes(f.name) + ) + .map(f => f.name) + const newNorthTabs = northFiles.map(f => f.name) + + const baseTabs = oldSouthTabs.filter( + tab => !newNorthTabs.includes(tab) + ) + return [...baseTabs, ...newSouthTabs] +} + +function chooseActiveFile( + panelFiles: FileWithOptions[], + prev?: string +) { + const active = + panelFiles.find(file => file.active)?.name || + panelFiles[0]?.name || + prev + if (!active) { + throw new Error("Something is wrong with Code Hike") + } + return active +} + +type FileOptions = { + focus?: string + active?: string + hidden?: boolean +} + +type FileWithOptions = StepFile & FileOptions + +function preToFile( + preElement: any, + prevFiles: StepFile[], + settings: Settings +): FileWithOptions { + const codeElementProps = + preElement?.props?.children?.props || {} + const lang = codeElementProps.className?.slice(9) + const { name, ...options } = parseMetastring( + codeElementProps.metastring || "" + ) + const fileName = + name || settings?.defaultFileName || defaultFileName + const code = codeElementProps.children + + const prevFile = prevFiles.find( + file => file.name === fileName + ) + + return { + code: + code.trim() === "" && prevFile ? prevFile.code : code, + lang, + name: fileName, + ...options, + } +} + +function parseMetastring( + metastring: string +): { name: string | null } & FileOptions { + const params = metastring.split(" ") + const options = {} as FileOptions + let name: string | null = null + params.forEach(param => { + const [key, value] = param.split("=") + if (value != null) { + ;(options as any)[key] = value + } else if (name === null) { + name = key + } else { + ;(options as any)[key] = true + } + }) + return { name, ...options } +} diff --git a/packages/mini-editor/src/mini-editor-hike.tsx b/packages/mini-editor/src/mini-editor-hike.tsx index 7d069492..daca4c43 100644 --- a/packages/mini-editor/src/mini-editor-hike.tsx +++ b/packages/mini-editor/src/mini-editor-hike.tsx @@ -1,268 +1,54 @@ import React from "react" -import { EditorFrame, TerminalPanel } from "./editor-frame" -import { InnerTerminal } from "@code-hike/mini-terminal" -import { Code } from "./code" -import { - useBackwardTransitions, - useForwardTransitions, -} from "./steps" -import { Classes } from "@code-hike/classer" -// import "./theme.css" +import { MiniEditorTween } from "./mini-editor-tween" +import { EditorStep } from "./use-snapshots" +import { CodeProps } from "./code" +import { EditorFrameProps } from "./editor-frame" -export { MiniEditorHike } +export { MiniEditorHike, MiniEditorHikeProps, EditorStep } -type MiniEditorStep = { - code?: string - focus?: string - lang?: string - file?: string - tabs?: string[] - terminal?: string -} - -export type MiniEditorHikeProps = { - progress?: number - backward?: boolean - code?: string - focus?: string - lang?: string - file?: string - tabs?: string[] - steps?: MiniEditorStep[] - height?: number - minColumns?: number - minZoom?: number - maxZoom?: number - button?: React.ReactNode - horizontalCenter?: boolean - classes?: Classes -} & React.PropsWithoutRef - -function MiniEditorHike(props: MiniEditorHikeProps) { - const { - progress = 0, - backward = false, - code, - focus, - lang, - file, - steps: ogSteps, - tabs: ogTabs, - minColumns = 50, - minZoom = 0.2, - maxZoom = 1, - height, - horizontalCenter = false, - ...rest - } = props - const { steps, files, stepsByFile } = useSteps(ogSteps, { - code, - focus, - lang, - file, - tabs: ogTabs, - }) - - const activeStepIndex = backward - ? Math.floor(progress) - : Math.ceil(progress) - const activeStep = steps[activeStepIndex] - const activeFile = (activeStep && activeStep.file) || "" - - const activeSteps = stepsByFile[activeFile] || [] - - const tabs = activeStep.tabs || files - - const terminalHeight = getTerminalHeight(steps, progress) - - const terminalSteps = steps.map(s => ({ - text: (s && s.terminal) || "", - })) - - const contentSteps = useStepsWithDefaults( - { code, focus, lang, file }, - ogSteps || [] - ) - - return ( - - - - } - height={height} - {...rest} - > - {activeSteps.length > 0 && ( - - )} - - ) -} - -function useStepsWithDefaults( - defaults: MiniEditorStep, - steps: MiniEditorStep[] -): ContentStep[] { - const files = [ - ...new Set( - steps.map(s => coalesce(s.file, defaults.file, "")) - ), - ] - return steps.map(step => { - return { - code: coalesce(step.code, defaults.code, ""), - file: coalesce(step.file, defaults.file, ""), - focus: coalesce(step.focus, defaults.focus, ""), - lang: coalesce( - step.lang, - defaults.lang, - "javascript" - ), - tabs: coalesce(step.tabs, defaults.tabs, files), - terminal: step.terminal || defaults.terminal, - } - }) -} - -function coalesce( - a: T | null | undefined, - b: T | null | undefined, - c: T -): T { - return a != null ? a : b != null ? b : c -} - -type ContentStep = { - code: string - focus: string - lang: string - file: string - tabs: string[] - terminal?: string -} - -type ContentProps = { +type MiniEditorHikeProps = { + steps: EditorStep[] progress: number backward: boolean - steps: ContentStep[] - parentHeight?: number - minColumns: number - minZoom: number - maxZoom: number - horizontalCenter: boolean -} + frameProps?: Partial + codeProps?: Partial +} + +function MiniEditorHike({ + steps = [], + progress = 0, + backward = false, + frameProps, + codeProps, +}: MiniEditorHikeProps) { + const prevIndex = clamp( + Math.floor(progress), + 0, + steps.length - 1 + ) + const nextIndex = clamp( + prevIndex + 1, + 0, + steps.length - 1 + ) -function EditorContent({ - progress, - backward, - steps, - parentHeight, - minColumns, - minZoom, - maxZoom, - horizontalCenter, -}: ContentProps) { - const fwdTransitions = useForwardTransitions(steps) - const bwdTransitions = useBackwardTransitions(steps) + const prev = steps[prevIndex] + const next = steps[nextIndex] - const transitionIndex = Math.ceil(progress) - const { - prevCode, - nextCode, - prevFocus, - nextFocus, - lang, - } = backward - ? bwdTransitions[transitionIndex] - : fwdTransitions[transitionIndex] + const t = clamp(progress - prevIndex, 0, steps.length - 1) return ( - ) } -function useSteps( - ogSteps: MiniEditorStep[] | undefined, - { code = "", focus, lang, file, tabs }: MiniEditorStep -) { - return React.useMemo(() => { - const steps = ogSteps?.map(s => ({ - code, - focus, - lang, - file, - tabs, - ...s, - })) || [{ code, focus, lang, file, tabs }] - - const files = [ - ...new Set( - steps - .map((s: any) => s.file) - .filter((f: any) => f != null) - ), - ] - - const stepsByFile: Record = {} - steps.forEach(s => { - if (s.file == null) return - if (!stepsByFile[s.file]) { - stepsByFile[s.file] = [] - } - stepsByFile[s.file].push(s) - }) - - return { steps, files, stepsByFile } - }, [ogSteps, code, focus, lang, file, tabs]) -} - -const MAX_HEIGHT = 150 -function getTerminalHeight(steps: any, progress: number) { - if (!steps.length) { - return 0 - } - - const prevIndex = Math.floor(progress) - const nextIndex = Math.ceil(progress) - const prevTerminal = - steps[prevIndex] && steps[prevIndex].terminal - const nextTerminal = steps[nextIndex].terminal - - if (!prevTerminal && !nextTerminal) return 0 - - if (!prevTerminal && nextTerminal) - return MAX_HEIGHT * Math.min((progress % 1) * 4, 1) - if (prevTerminal && !nextTerminal) - return MAX_HEIGHT * Math.max(1 - (progress % 1) * 4, 0) - - return MAX_HEIGHT +function clamp(a: number, min: number, max: number) { + return Math.max(Math.min(a, max), min) } diff --git a/packages/mini-editor/src/mini-editor-spring.tsx b/packages/mini-editor/src/mini-editor-spring.tsx new file mode 100644 index 00000000..74b18cc8 --- /dev/null +++ b/packages/mini-editor/src/mini-editor-spring.tsx @@ -0,0 +1,171 @@ +import React from "react" +import { useSpring } from "use-spring" +import { MiniEditorTween } from "./mini-editor-tween" +import { EditorStep, StepFile } from "./use-snapshots" +import { CodeProps } from "./code" +import { EditorFrameProps } from "./editor-frame" + +export { MiniEditor, MiniEditorProps } + +type SingleFileEditorProps = { + code: string + lang: string + focus?: string + filename?: string + terminal?: string + frameProps?: Partial + codeProps?: Partial +} +type SinglePanelEditorProps = { + files: StepFile[] + active: string + terminal?: string + frameProps?: Partial + codeProps?: Partial +} +type TwoPanelEditorProps = EditorStep & { + frameProps?: Partial + codeProps?: Partial +} +type MiniEditorProps = + | SingleFileEditorProps + | SinglePanelEditorProps + | TwoPanelEditorProps + +function MiniEditor(props: MiniEditorProps) { + if ("northPanel" in props) { + return + } else if ("active" in props) { + return + } else { + return + } +} + +function SingleFileEditor({ + code = "", + lang = "js", + focus, + filename = "", + terminal, + frameProps, + codeProps, +}: SingleFileEditorProps) { + const step = React.useMemo(() => { + const step: EditorStep = { + files: [{ name: filename, code, lang, focus }], + northPanel: { + active: filename, + tabs: [filename], + heightRatio: 1, + }, + terminal, + } + return step + }, [code, lang, focus, filename, terminal]) + + const { prev, next, t } = useStepSpring(step) + return ( + + ) +} +function SinglePanelEditor({ + files, + active, + terminal, + frameProps, + codeProps, +}: SinglePanelEditorProps) { + const step = React.useMemo(() => { + const tabs = files.map(file => file.name) + const step: EditorStep = { + files, + northPanel: { + active, + tabs, + heightRatio: 1, + }, + terminal, + } + return step + }, [files, active, terminal]) + + const { prev, next, t } = useStepSpring(step) + return ( + + ) +} +function TwoPanelEditor({ + frameProps, + codeProps, + northPanel, + southPanel, + files, + terminal, +}: TwoPanelEditorProps) { + const step = React.useMemo(() => { + return { + northPanel, + southPanel, + files, + terminal, + } + }, [northPanel, southPanel, files, terminal]) + + const { prev, next, t } = useStepSpring(step) + return ( + + ) +} + +function useStepSpring(step: EditorStep) { + const [{ target, prev, next }, setState] = React.useState( + { + target: 0, + prev: step, + next: step, + } + ) + + React.useEffect(() => { + if (next != step) { + setState(s => ({ + target: s.target + 1, + prev: next, + next: step, + })) + } + }, [step]) + + const [progress] = useSpring(target, { + stiffness: 256, + damping: 24, + mass: 0.2, + decimals: 3, + }) + + const t = progress % 1 + + return { prev, next, t: t || 1 } +} diff --git a/packages/mini-editor/src/mini-editor-tween.tsx b/packages/mini-editor/src/mini-editor-tween.tsx new file mode 100644 index 00000000..aecdc168 --- /dev/null +++ b/packages/mini-editor/src/mini-editor-tween.tsx @@ -0,0 +1,432 @@ +import React from "react" +import { + EditorFrame, + EditorFrameProps, + getPanelStyles, + OutputPanel, +} from "./editor-frame" +import { Code, CodeProps } from "./code" +import { + EditorStep, + StepFile, + useSnapshots, +} from "./use-snapshots" +import { getTabs } from "./tabs" +import { TerminalPanel } from "./terminal-panel" + +export { + EditorTransition, + EditorTransitionProps, + MiniEditorTween, + MiniEditorTweenProps, +} + +type EditorTransitionProps = { + prev?: EditorStep + next?: EditorStep + t: number + backward: boolean + codeProps?: Partial +} & Omit + +type MiniEditorTweenProps = { + prev?: EditorStep + next?: EditorStep + t: number + backward: boolean + codeProps?: Partial + frameProps?: Partial +} + +const DEFAULT_STEP: EditorStep = { + files: [{ code: "", lang: "js", name: "" }], + northPanel: { active: "", tabs: [""], heightRatio: 1 }, +} + +function MiniEditorTween({ + prev = DEFAULT_STEP, + next = DEFAULT_STEP, + t, + backward, + codeProps = {}, + frameProps = {}, +}: MiniEditorTweenProps) { + const ref = React.createRef() + const { northPanel, southPanel } = useTransition( + ref, + prev, + next, + t, + backward, + codeProps + ) + + const terminalPanel = ( + + ) + return ( + + ) +} + +function EditorTransition({ + prev = DEFAULT_STEP, + next = DEFAULT_STEP, + t, + backward, + codeProps = {}, + ...rest +}: EditorTransitionProps) { + const ref = React.createRef() + const { northPanel, southPanel } = useTransition( + ref, + prev, + next, + t, + backward, + codeProps + ) + + const terminalPanel = ( + + ) + return ( + + ) +} + +type Transition = { + northPanel: OutputPanel + southPanel?: OutputPanel | null +} + +function useTransition( + ref: React.RefObject, + prev: EditorStep, + next: EditorStep, + t: number, + backward: boolean, + codeProps: Partial +): Transition { + const { prevSnapshot, nextSnapshot } = useSnapshots( + ref, + prev, + next + ) + + if (!prevSnapshot) { + return startingPosition(prev, next, codeProps) + } + + if (!nextSnapshot) { + return endingPosition(prev, next, codeProps) + } + + // if (t === 0) { + // return startingPosition(prev, next, codeProps) + // } + + if (t === 1) { + return endingPosition(prev, next, codeProps) + } + const inputSouthPanel = prev.southPanel || next.southPanel + + const { + prevNorthFile, + prevSouthFile, + nextNorthFile, + nextSouthFile, + } = getStepFiles(prev, next, t == 0 || backward) + + const { northStyle, southStyle } = getPanelStyles( + prevSnapshot, + nextSnapshot, + t + ) + const { northTabs, southTabs } = getTabs( + prevSnapshot, + nextSnapshot, + prevNorthFile.name, + prevSouthFile?.name, + t + ) + + return { + northPanel: { + tabs: northTabs, + style: northStyle, + children: ( + + ), + }, + southPanel: inputSouthPanel && { + tabs: southTabs!, + style: southStyle!, + children: ( + + ), + }, + } +} + +// Returns the t=0 state of the transition +function startingPosition( + prev: EditorStep, + next: EditorStep, + codeProps: Partial +): Transition { + const inputNorthPanel = prev.northPanel + const inputSouthPanel = prev.southPanel + + const { + prevNorthFile, + prevSouthFile, + nextNorthFile, + nextSouthFile, + } = getStepFiles(prev, next, true) + + return { + northPanel: { + tabs: inputNorthPanel.tabs.map(title => ({ + title, + active: title === inputNorthPanel.active, + style: {}, + })), + style: { + height: inputSouthPanel + ? `calc((100% - var(--ch-title-bar-height)) * ${inputNorthPanel.heightRatio})` + : "100%", + }, + children: ( + + ), + }, + southPanel: inputSouthPanel && { + tabs: inputSouthPanel.tabs.map(title => ({ + title, + active: title === inputSouthPanel.active, + style: {}, + })), + style: { + height: `calc((100% - var(--ch-title-bar-height)) * ${inputSouthPanel.heightRatio} + var(--ch-title-bar-height))`, + }, + children: ( + + ), + }, + } +} +// Returns the t=1 state of the transition +function endingPosition( + prev: EditorStep, + next: EditorStep, + codeProps: Partial +): Transition { + const inputNorthPanel = next.northPanel + const inputSouthPanel = next.southPanel + + let { + prevNorthFile, + prevSouthFile, + nextNorthFile, + nextSouthFile, + } = getStepFiles(prev, next, false) + + // getStepFiles return the intermediate files, we need to patch the ending state (2to1south) + const isTwoToOneSouth = + !inputSouthPanel && + inputNorthPanel.active === prev?.southPanel?.active + if (isTwoToOneSouth) { + nextNorthFile = nextSouthFile! + } + + return { + northPanel: { + tabs: inputNorthPanel.tabs.map(title => ({ + title, + active: title === inputNorthPanel.active, + style: {}, + })), + style: { + height: inputSouthPanel + ? `calc((100% - var(--ch-title-bar-height)) * ${inputNorthPanel.heightRatio})` + : "100%", + }, + children: ( + + ), + }, + southPanel: inputSouthPanel && { + tabs: inputSouthPanel.tabs.map(title => ({ + title, + active: title === inputSouthPanel.active, + style: {}, + })), + style: { + height: `calc((100% - var(--ch-title-bar-height)) * ${inputSouthPanel.heightRatio} + var(--ch-title-bar-height))`, + }, + children: ( + + ), + }, + } +} + +function CodeTransition({ + prevFile, + nextFile, + t, + codeProps, +}: { + prevFile: StepFile + nextFile: StepFile + t: number + codeProps: Partial +}) { + return ( + + ) +} + +/** + * Get the StepFiles for a transition + * in each panel, if the prev and next active files are the same + * we return the prev and next version of that panel + * if the active files are different, we return the same file twice, + * if backward is true we return the prev active file twice, + * or else the next active file twice + */ +function getStepFiles( + prev: EditorStep, + next: EditorStep, + backward: boolean +) { + // The active file in each panel before and after: + // +----+----+ + // | pn | nn | + // +----+----+ + // | ps | ns | + // +----+----+ + // + const pn = prev.northPanel.active + const nn = next.northPanel.active + const ps = prev.southPanel?.active + const ns = next.southPanel?.active + + const pnFile = prev.files.find(f => f.name === pn)! + const nnFile = next.files.find(f => f.name === nn)! + const psFile = ps + ? prev.files.find(f => f.name === ps) + : null + const nsFile = ns + ? next.files.find(f => f.name === ns) + : null + + const oneToTwoSouth = !ps && pn === ns + if (oneToTwoSouth) { + return { + prevNorthFile: nnFile, + nextNorthFile: nnFile, + prevSouthFile: pnFile, + nextSouthFile: nsFile, + } + } + + const twoToOneSouth = !ns && nn === ps + if (twoToOneSouth) { + return { + prevNorthFile: pnFile, + nextNorthFile: pnFile, + prevSouthFile: psFile, + nextSouthFile: nnFile, + } + } + + const prevNorthFile = + pn === nn ? pnFile : backward ? pnFile : nnFile + + const nextNorthFile = + pn === nn ? nnFile : backward ? pnFile : nnFile + + const prevSouthFile = + ps === ns + ? psFile + : backward + ? psFile || nsFile + : nsFile || psFile + + const nextSouthFile = + ps === ns + ? nsFile + : backward + ? psFile || nsFile + : nsFile || psFile + + return { + prevNorthFile, + nextNorthFile, + prevSouthFile, + nextSouthFile, + } +} diff --git a/packages/mini-editor/src/mini-editor.tsx b/packages/mini-editor/src/mini-editor.tsx deleted file mode 100644 index f3a6fd6e..00000000 --- a/packages/mini-editor/src/mini-editor.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react" -import { useSpring } from "use-spring" -import { - MiniEditorHike, - MiniEditorHikeProps, -} from "./mini-editor-hike" - -export { MiniEditor } - -export type MiniEditorProps = Omit< - MiniEditorHikeProps, - "progress" | "steps" | "backward" -> - -function MiniEditor({ - focus, - code, - ...rest -}: MiniEditorProps) { - const [steps, progress] = usePrevFocus(code, focus) - - return ( - - ) -} - -function usePrevFocus( - code: string | undefined, - focus: string | undefined -) { - const [state, setState] = React.useState({ - target: 0, - steps: [{ focus, code }], - }) - - React.useEffect(() => { - const last = state.steps[state.steps.length - 1] - if (last.focus !== focus || last.code !== code) { - setState(s => ({ - target: s.target + 1, - steps: [...s.steps, { focus, code }], - })) - } - }, [focus, code]) - - const [progress] = useSpring(state.target, { - stiffness: 256, - damping: 24, - mass: 0.2, - decimals: 3, - }) - - return [state.steps, progress] as const -} diff --git a/packages/mini-editor/src/tabs.tsx b/packages/mini-editor/src/tabs.tsx new file mode 100644 index 00000000..a2301718 --- /dev/null +++ b/packages/mini-editor/src/tabs.tsx @@ -0,0 +1,205 @@ +import React from "react" +import { Snapshot, TabsSnapshot, Tab } from "./editor-frame" + +export { getTabs } + +function getTabs( + prevSnapshot: Snapshot, + nextSnapshot: Snapshot, + northActive: string, + southActive: string | undefined, + t: number +) { + // TODO simplify + if ( + !prevSnapshot.southTabs && + isPresent(southActive, prevSnapshot.northTabs) + ) { + /// one to two south + return { + northTabs: getPanelTabs( + nextSnapshot.northTabs, + nextSnapshot.southTabs, + prevSnapshot.southTabs, + prevSnapshot.northTabs, + northActive, + t + )!, + southTabs: getPanelTabs( + nextSnapshot.southTabs, + nextSnapshot.northTabs, + prevSnapshot.northTabs, + prevSnapshot.southTabs, + southActive, + t + ), + } + } + if ( + !nextSnapshot.southTabs && + isPresent(southActive, nextSnapshot.northTabs) + ) { + /// two to one south + return { + northTabs: getPanelTabs( + nextSnapshot.southTabs, + nextSnapshot.northTabs, + prevSnapshot.northTabs, + prevSnapshot.southTabs, + northActive, + t + )!, + southTabs: getPanelTabs( + nextSnapshot.northTabs, + nextSnapshot.southTabs, + prevSnapshot.southTabs, + prevSnapshot.northTabs, + southActive, + t + ), + } + } + + return { + northTabs: getPanelTabs( + nextSnapshot.northTabs, + nextSnapshot.southTabs, + prevSnapshot.northTabs, + prevSnapshot.southTabs, + northActive, + t + )!, + southTabs: getPanelTabs( + nextSnapshot.southTabs, + nextSnapshot.northTabs, + prevSnapshot.southTabs, + prevSnapshot.northTabs, + southActive, + t + ), + } +} + +function getPanelTabs( + nextSnapshot: TabsSnapshot | null, + otherNextSnapshot: TabsSnapshot | null, + prevSnapshot: TabsSnapshot | null, + otherPrevSnapshot: TabsSnapshot | null, + active: string | undefined, + t: number +): Tab[] { + // For each tab bar there are four types of tabs + // - oldTabs: tabs that are present in both prev and next versions of the bar + // - totallyNewTabs: tabs that are totally new (present in next + // but not in any prev) + // - migratingTabs: tabs that are come from the other bar (present + // in next and in otherPrev) + // - disappearingTabs: present in prev but not in next or otherNext + const oldTabs = !nextSnapshot + ? [] + : Object.keys(nextSnapshot) + .filter( + filename => + isPresent(filename, prevSnapshot) || + !prevSnapshot + ) + .map(filename => { + const prev = + prevSnapshot && prevSnapshot[filename] + const next = nextSnapshot![filename] + const dx = prev + ? prev.left + (next.left - prev.left) * t + : next.left + const width = prev + ? prev.width + (next.width - prev.width) * t + : next.width + return { + active: filename === active, + title: filename, + style: { + position: "absolute" as const, + transform: `translateX(${dx}px)`, + width, + }, + } + }) + + const totallyNewTabs = !nextSnapshot + ? [] + : Object.keys(nextSnapshot) + .filter( + filename => + prevSnapshot && + !isPresent(filename, prevSnapshot) + // && !isPresent(filename, otherPrevSnapshot) + ) + .map(filename => { + const next = nextSnapshot[filename] + + return { + active: filename === active, + title: filename, + style: { + position: "absolute" as const, + transform: `translateX(${next.left}px)`, + opacity: t, + width: next.width, + }, + } + }) + + const migratingTabs = !nextSnapshot + ? [] + : Object.keys(nextSnapshot) + .filter(filename => + isPresent(filename, otherPrevSnapshot) + ) + .map(filename => { + const prev = otherPrevSnapshot![filename] + const next = nextSnapshot![filename] + const dx = next.left - prev.left + return { + active: filename === active, + title: filename, + style: { + position: "absolute" as const, + transform: `translateX(${dx}px)`, + }, + } + }) + + const disappearingTabs = !prevSnapshot + ? [] + : Object.keys(prevSnapshot) + .filter( + filename => !isPresent(filename, nextSnapshot) + // && !isPresent(filename, otherNextSnapshot) + ) + .map(filename => { + const prev = prevSnapshot[filename] + return { + active: filename === active, + title: filename, + style: { + position: "absolute" as const, + opacity: 1 - t, + transform: `translateX(${prev.left}px)`, + width: prev.width, + }, + } + }) + + return [ + ...totallyNewTabs, + // ...migratingTabs, + ...oldTabs, + ...disappearingTabs, + ] +} + +function isPresent( + filename: string | undefined, + snapshot: TabsSnapshot | null +) { + return snapshot && filename && filename in snapshot +} diff --git a/packages/mini-editor/src/terminal-panel.tsx b/packages/mini-editor/src/terminal-panel.tsx new file mode 100644 index 00000000..9048df42 --- /dev/null +++ b/packages/mini-editor/src/terminal-panel.tsx @@ -0,0 +1,70 @@ +import { InnerTerminal } from "@code-hike/mini-terminal" +import React from "react" + +type TerminalPanelProps = { + prev: string | undefined + next: string | undefined + t: number + backward: boolean +} + +export function TerminalPanel({ + prev, + next, + t, + backward, +}: TerminalPanelProps) { + const height = getHeight({ prev, next, t, backward }) + return !height ? null : ( +
+
+ Terminal +
+
+ + ) +
+
+ ) +} + +function getHeight({ + prev, + next, + t, + backward, +}: TerminalPanelProps) { + if (!prev && !next) return 0 + if (!prev && next) return MAX_HEIGHT * Math.min(t * 4, 1) + if (prev && !next) + return MAX_HEIGHT * Math.max(1 - t * 4, 0) + return MAX_HEIGHT +} + +const MAX_HEIGHT = 150 +function getTerminalHeight(steps: any, progress: number) { + if (!steps.length) { + return 0 + } + + const prevIndex = Math.floor(progress) + const nextIndex = Math.ceil(progress) + const prevTerminal = + steps[prevIndex] && steps[prevIndex].terminal + const nextTerminal = steps[nextIndex].terminal + + if (!prevTerminal && !nextTerminal) return 0 + + if (!prevTerminal && nextTerminal) + return MAX_HEIGHT * Math.min((progress % 1) * 4, 1) + if (prevTerminal && !nextTerminal) + return MAX_HEIGHT * Math.max(1 - (progress % 1) * 4, 0) + + return MAX_HEIGHT +} diff --git a/packages/mini-editor/src/use-dimensions.tsx b/packages/mini-editor/src/use-dimensions.tsx index 64e28b88..bc89fc52 100644 --- a/packages/mini-editor/src/use-dimensions.tsx +++ b/packages/mini-editor/src/use-dimensions.tsx @@ -82,7 +82,7 @@ function getWidthWithoutPadding(element: HTMLElement) { function getHeightWithoutPadding(element: HTMLElement) { const computedStyle = getComputedStyle(element) return ( - element.clientHeight - + parseFloat(computedStyle.height) - parseFloat(computedStyle.paddingTop) - parseFloat(computedStyle.paddingBottom) ) diff --git a/packages/mini-editor/src/use-snapshots.tsx b/packages/mini-editor/src/use-snapshots.tsx new file mode 100644 index 00000000..404a69e6 --- /dev/null +++ b/packages/mini-editor/src/use-snapshots.tsx @@ -0,0 +1,148 @@ +import React from "react" +import { Snapshot, TabsSnapshot } from "./editor-frame" + +export { EditorStep, StepFile, useSnapshots } + +const useLayoutEffect = + typeof window !== "undefined" + ? React.useLayoutEffect + : React.useEffect + +type StepFile = { + code: string + focus?: string + lang: string + name: string +} + +type EditorPanel = { + tabs: string[] + active: string + heightRatio: number +} + +type EditorStep = { + files: StepFile[] + northPanel: EditorPanel + southPanel?: EditorPanel + terminal?: string +} + +function useSnapshots( + ref: React.RefObject, + prev: EditorStep, + next: EditorStep +) { + const [ + { prevSnapshot, nextSnapshot }, + setState, + ] = React.useState<{ + prevSnapshot: Snapshot | null + nextSnapshot: Snapshot | null + }>({ + prevSnapshot: null, + nextSnapshot: null, + }) + + useLayoutEffect(() => { + if (prevSnapshot || nextSnapshot) { + setState({ + prevSnapshot: null, + nextSnapshot: null, + }) + } + }, [prev, next]) + + useLayoutEffect(() => { + if (!prevSnapshot) { + setState(s => ({ + ...s, + prevSnapshot: { + ...getPanelSnapshot(ref.current!, prev), + ...getTabsSnapshot(ref.current!, prev), + }, + })) + } else if (!nextSnapshot) { + setState(s => ({ + ...s, + nextSnapshot: { + ...getPanelSnapshot(ref.current!, next), + ...getTabsSnapshot(ref.current!, next), + }, + })) + } + }) + + return { prevSnapshot, nextSnapshot } +} + +function getPanelSnapshot( + parent: HTMLDivElement, + step: EditorStep +) { + const northElement = parent.querySelector( + "[data-ch-panel='north']" + ) + const southElement = parent.querySelector( + "[data-ch-panel='south']" + ) + const bar = parent.querySelector(".ch-frame-title-bar") + return { + titleBarHeight: bar!.getBoundingClientRect().height, + northHeight: northElement!.getBoundingClientRect() + .height, + northKey: step.northPanel.active, + southHeight: + southElement?.getBoundingClientRect().height || null, + southKey: step.southPanel?.active, + } +} + +function getTabsSnapshot( + parent: HTMLDivElement, + step: EditorStep +) { + const northTabs = Array.from( + parent.querySelectorAll("[data-ch-tab='north']") + ) + + const southTabs = Array.from( + parent.querySelectorAll("[data-ch-tab='south']") + ) + + return { + northTabs: getTabsDimensions( + northTabs, + step.northPanel.active + )!, + southTabs: getTabsDimensions( + southTabs, + step.southPanel?.active + ), + } +} + +function getTabsDimensions( + tabElements: Element[], + active: string | undefined +) { + if (!tabElements[0]) { + return null + } + + const parent = tabElements[0]!.parentElement! + const parentLeft = parent.getBoundingClientRect().left + + const dimensions = {} as TabsSnapshot + tabElements.forEach(child => { + const filename = child.getAttribute("title")! + const rect = child.getBoundingClientRect() + dimensions[filename] = { + left: rect.left - parentLeft, + width: rect.width, + active: filename === active, + } + }) + + return dimensions +} diff --git a/packages/mini-frame/src/index.scss b/packages/mini-frame/src/index.scss index 014f1130..781fda84 100644 --- a/packages/mini-frame/src/index.scss +++ b/packages/mini-frame/src/index.scss @@ -11,10 +11,11 @@ flex-direction: column; background-color: rgb(37, 37, 38); break-inside: avoid; + --ch-title-bar-height: 30px; } .ch-frame-content { - background-color: #fafafa; + background-color: var(--ch-content-background, #fafafa); flex-grow: 1; flex-shrink: 1; flex-basis: 0; @@ -34,14 +35,15 @@ .ch-frame-title-bar { font-size: 12px; width: 100%; - height: 2.5em; - min-height: 2.5em; + height: var(--ch-title-bar-height); + min-height: var(--ch-title-bar-height); flex-grow: 0; flex-shrink: 0; display: flex; align-items: center; background-color: rgb(37, 37, 38); color: #ebebed; + position: relative; } .ch-frame-middle-bar { diff --git a/packages/mini-frame/src/mini-frame.tsx b/packages/mini-frame/src/mini-frame.tsx index 284494f4..b50cd321 100644 --- a/packages/mini-frame/src/mini-frame.tsx +++ b/packages/mini-frame/src/mini-frame.tsx @@ -10,26 +10,35 @@ type MiniFrameProps = { titleBar?: React.ReactNode zoom?: number classes?: Classes + overflow?: string } & React.PropsWithoutRef -export function MiniFrame({ - title, - children, - titleBar, - classes, - zoom = 1, - ...props -}: MiniFrameProps) { +export const MiniFrame = React.forwardRef< + HTMLDivElement, + MiniFrameProps +>(function ( + { + title, + children, + titleBar, + classes, + zoom = 1, + overflow, + ...props + }, + ref +) { const c = useClasser("ch-frame", classes) const bar = titleBar || const zoomStyle = { "--ch-frame-zoom": zoom, + overflow, } as React.CSSProperties return ( -
+
{bar}
@@ -41,7 +50,7 @@ export function MiniFrame({
) -} +}) function DefaultTitleBar({ title }: { title?: string }) { const c = useClasser("ch-frame") diff --git a/packages/scrollycoding/src/content-column.tsx b/packages/scrollycoding/src/content-column.tsx new file mode 100644 index 00000000..9d16bfa0 --- /dev/null +++ b/packages/scrollycoding/src/content-column.tsx @@ -0,0 +1,87 @@ +import React from "react" +import { useClasser } from "@code-hike/classer" +import { + Scroller, + Step as ScrollerStep, +} from "@code-hike/scroller" +import { useFluidContext, HikeStep } from "./hike-context" +import { EditorStep } from "@code-hike/mini-editor" + +export const StepContext = React.createContext<{ + stepIndex: number + editorStep: EditorStep +} | null>(null) + +export function ContentColumn({ + steps, + onStepChange, +}: { + steps: HikeStep[] + onStepChange: (index: number) => void +}) { + const c = useClasser("ch") + const contentSteps = steps.map(s => s.content) + return ( +
+ + {contentSteps.map((children, index) => ( + + + + ))} + +
+ ) +} + +function StepContent({ + children, + stepIndex, +}: { + children: React.ReactNode + stepIndex: number +}) { + const c = useClasser("ch-hike-step") + const { dispatch, hikeState } = useFluidContext() + const focusStepIndex = + hikeState.focusStepIndex ?? hikeState.scrollStepIndex + const isOn = stepIndex === focusStepIndex + return ( + + dispatch({ + type: "set-focus", + stepIndex, + editorStep: null, + }) + } + className={c("", isOn ? "focused" : "unfocused")} + > + {stepIndex > 0 &&
} +
+ {children} +
+ + ) +} + +export function useStepData() { + return React.useContext(StepContext)! +} diff --git a/packages/scrollycoding/src/demo-context.tsx b/packages/scrollycoding/src/demo-context.tsx deleted file mode 100644 index bde0240e..00000000 --- a/packages/scrollycoding/src/demo-context.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useContext } from "react" -import { - SandpackFiles, - SandpackProvider, -} from "@codesandbox/sandpack-react" -import { - CodeFiles, - CodeProps, - PreviewProps, -} from "./hike-context" -import hash from "object-hash" - -type DemoProviderProps = { - codeProps: CodeProps - previewProps: Partial - children?: React.ReactNode -} - -type DemoContext = { - codeProps: CodeProps - previewProps: PreviewProps -} - -export const DemoContext = React.createContext( - null -) -export function DemoProvider({ - codeProps, - previewProps, - children, -}: DemoProviderProps) { - const { preset } = previewProps - - // TODO useMemo - - const newFiles = { - ...preset?.customSetup?.files, - ...getFiles(codeProps.files!), - } as SandpackFiles - - const newPreset = { - ...preset, - customSetup: { - ...preset?.customSetup, - files: newFiles, - }, - } - - const demo = { - codeProps, - previewProps: { - ...previewProps, - filesHash: hash(newFiles), - }, - } - - return ( - - - {children} - - - ) -} - -export function useCodeProps() { - return useContext(DemoContext)!.codeProps -} -export function usePreviewProps() { - return useContext(DemoContext)!.previewProps -} - -export const StepContext = React.createContext<{ - stepIndex: number -} | null>(null) - -export function useStepIndex() { - return useContext(StepContext)!.stepIndex -} - -function getFiles(codeFiles: CodeFiles): SandpackFiles { - const files = {} as SandpackFiles - const filenames = Object.keys(codeFiles) - filenames.forEach(filename => { - files["/" + filename] = { - code: codeFiles[filename].code, - } - }) - return files -} diff --git a/packages/scrollycoding/src/demo-provider.tsx b/packages/scrollycoding/src/demo-provider.tsx new file mode 100644 index 00000000..f3d1bdaa --- /dev/null +++ b/packages/scrollycoding/src/demo-provider.tsx @@ -0,0 +1,96 @@ +import React from "react" +import { + SandpackProvider, + SandpackPredefinedTemplate, + SandpackProviderProps, + SandpackSetup, + SandpackFile, +} from "@codesandbox/sandpack-react" +import { EditorProps } from "./editor" +import { PreviewProps } from "./preview" + +export { + DemoProvider, + PreviewPreset, + usePreviewProps, + useEditorProps, +} + +interface PreviewPreset { + template?: SandpackPredefinedTemplate + customSetup?: SandpackSetup +} + +type DemoProviderProps = { + editorProps: EditorProps + previewProps: PreviewProps + previewPreset: PreviewPreset + children?: React.ReactNode +} + +type DemoContext = { + editorProps: EditorProps + previewProps: PreviewProps +} + +export const DemoContext = React.createContext( + null +) + +function DemoProvider({ + children, + editorProps, + previewProps, + previewPreset, +}: DemoProviderProps) { + const sandpackProps = useSandpackProps( + previewPreset, + editorProps + ) + const previewAndEditorProps = React.useMemo( + () => ({ + previewProps, + editorProps, + }), + [previewProps, editorProps] + ) + return ( + + + {children} + + + ) +} + +function useSandpackProps( + previewPreset: PreviewPreset, + editorProps: EditorProps +): SandpackProviderProps { + // TODO useMemo + const files = { + ...previewPreset?.customSetup?.files, + } as Record + const codeFiles = editorProps?.contentProps?.files || [] + codeFiles.forEach(file => { + files["/" + file.name] = { + code: file.code, + } + }) + + return { + recompileMode: "immediate", + ...previewPreset, + customSetup: { + ...previewPreset?.customSetup, + files, + }, + } +} + +function useEditorProps() { + return React.useContext(DemoContext)!.editorProps +} +function usePreviewProps() { + return React.useContext(DemoContext)!.previewProps +} diff --git a/packages/scrollycoding/src/code.tsx b/packages/scrollycoding/src/editor.tsx similarity index 58% rename from packages/scrollycoding/src/code.tsx rename to packages/scrollycoding/src/editor.tsx index 3fec13c4..ff0e09d1 100644 --- a/packages/scrollycoding/src/code.tsx +++ b/packages/scrollycoding/src/editor.tsx @@ -1,27 +1,38 @@ import * as React from "react" -import { MiniEditor } from "@code-hike/mini-editor" -import { CodeProps } from "./hike-context" +import { + MiniEditor, + MiniEditorProps, + EditorStep, +} from "@code-hike/mini-editor" import { useCodeSandboxLink } from "@codesandbox/sandpack-react" -export { Code } +export { Editor, EditorProps } -function Code({ files, activeFile, ...props }: CodeProps) { - const file = files[activeFile] - const tabs = Object.keys(files).filter( - filename => !files[filename].hideTab - ) +type EditorProps = { + contentProps: EditorStep + frameProps: MiniEditorProps["frameProps"] + codeProps: MiniEditorProps["codeProps"] +} + +function Editor({ + contentProps, + codeProps, + frameProps, +}: EditorProps) { + const finalFrameProps = { + button: , + ...frameProps, + style: { height: "100%", ...frameProps?.style }, + } + const finalCodeProps = { + minColumns: 46, + ...codeProps, + } return ( - } - file={activeFile} - tabs={tabs} - lang={file.lang} - code={file.code} - {...props} + {...contentProps} + frameProps={finalFrameProps} + codeProps={finalCodeProps} /> ) } diff --git a/packages/scrollycoding/src/fixed-layout.tsx b/packages/scrollycoding/src/fixed-layout.tsx index ddaa2627..e990ecab 100644 --- a/packages/scrollycoding/src/fixed-layout.tsx +++ b/packages/scrollycoding/src/fixed-layout.tsx @@ -1,35 +1,31 @@ import React from "react" -import { useClasser } from "@code-hike/classer" import { - CodeProps, - HikeProps, + HikeStep, HikeProvider, - PreviewProps, useHikeContext, } from "./hike-context" +import { useClasser } from "@code-hike/classer" import { DemoProvider, - useCodeProps, + useEditorProps, usePreviewProps, -} from "./demo-context" -import { Code } from "./code" -import { Preview } from "./preview" +} from "./demo-provider" +import { EditorProps, Editor } from "./editor" +import { PreviewProps, Preview } from "./preview" export { FixedLayout, CodeSlot, PreviewSlot } -function FixedLayout({ steps, ...props }: HikeProps) { +function FixedLayout({ steps }: { steps: HikeStep[] }) { const c = useClasser("ch") return ( -
+
{steps.map((step, index) => (
@@ -42,7 +38,7 @@ function FixedLayout({ steps, ...props }: HikeProps) { ) } -function CodeSlot(props: CodeProps) { +function CodeSlot(props: EditorProps & { style?: any }) { const { layout } = useHikeContext() return layout === "fixed" ? ( @@ -52,22 +48,23 @@ function CodeSlot(props: CodeProps) { function CodeSlotContent({ style, ...slotProps -}: CodeProps) { +}: EditorProps & { style?: any }) { const c = useClasser("ch-hike") - const stepCodeProps = useCodeProps() + const stepEditorProps = useEditorProps() const props = { - minColumns: 46, - ...stepCodeProps, + ...stepEditorProps, ...slotProps, } return (
- +
) } -function PreviewSlot(props: PreviewProps) { +function PreviewSlot( + props: PreviewProps & { style?: any } +) { const { layout } = useHikeContext() return layout === "fixed" ? ( diff --git a/packages/scrollycoding/src/fluid-layout.tsx b/packages/scrollycoding/src/fluid-layout.tsx index 5aa61315..f49bcfec 100644 --- a/packages/scrollycoding/src/fluid-layout.tsx +++ b/packages/scrollycoding/src/fluid-layout.tsx @@ -1,31 +1,22 @@ import React from "react" import { useClasser } from "@code-hike/classer" -import { Code } from "./code" -import { Preview } from "./preview" +import { ContentColumn } from "./content-column" +import { StickerColumn } from "./sticker-column" import { - CodeProps, - HikeAction, - HikeProps, HikeProvider, HikeState, + HikeAction, HikeStep, - PreviewProps, - useFluidContext, } from "./hike-context" -import { Scroller, Step } from "@code-hike/scroller" -import { - DemoProvider, - StepContext, - usePreviewProps, -} from "./demo-context" +import { EditorProps } from "./editor" -export { FluidLayout } - -function FluidLayout({ - noPreview = false, +export function FluidLayout({ steps, - ...props -}: HikeProps) { + noPreview = false, +}: { + steps: HikeStep[] + noPreview: boolean +}) { const c = useClasser("ch") const [state, dispatch] = React.useReducer( reducer, @@ -36,11 +27,14 @@ function FluidLayout({ state.focusStepIndex ?? state.scrollStepIndex const focusStep = steps[focusStepIndex] - const codeProps: CodeProps = { - ...focusStep.codeProps, - ...state.focusCodeProps, + + const editorProps: EditorProps = { + ...focusStep.editorProps, + contentProps: + state.focusEditorStep || + focusStep.editorProps.contentProps, } - const previewProps = focusStep.previewProps + const { previewProps, previewPreset } = focusStep const onStepChange = (newIndex: number) => dispatch({ type: "change-step", newIndex }) @@ -53,17 +47,15 @@ function FluidLayout({ dispatch, }} > -
+
@@ -71,108 +63,10 @@ function FluidLayout({ ) } -function ContentColumn({ - steps, - onStepChange, -}: { - steps: HikeStep[] - onStepChange: (index: number) => void -}) { - const c = useClasser("ch") - return ( -
- - {steps.map((step, index) => ( - - - - ))} - -
- ) -} - -function StickerColumn({ - previewProps, - codeProps, - noPreview, -}: { - previewProps: PreviewProps - codeProps: CodeProps - noPreview: boolean -}) { - const c = useClasser("ch") - return ( - - ) -} - -function PreviewWrapper() { - const previewProps = usePreviewProps() - return -} - -function StepContent({ - step, - stepIndex, -}: { - step: HikeStep - stepIndex: number -}) { - const c = useClasser("ch-hike-step") - const { dispatch, hikeState } = useFluidContext() - const focusStepIndex = - hikeState.focusStepIndex ?? hikeState.scrollStepIndex - const isOn = stepIndex === focusStepIndex - return ( - - dispatch({ - type: "set-focus", - stepIndex, - codeProps: {}, - }) - } - className={c("", isOn ? "focused" : "unfocused")} - > - {stepIndex > 0 &&
} -
- {step.content} -
- - ) -} - const initialState = { scrollStepIndex: 0, focusStepIndex: null, - focusCodeProps: {}, + focusEditorStep: null, } function reducer( @@ -184,19 +78,19 @@ function reducer( return { scrollStepIndex: action.newIndex, focusStepIndex: null, - focusCodeProps: {}, + focusEditorStep: null, } case "set-focus": return { ...state, focusStepIndex: action.stepIndex, - focusCodeProps: action.codeProps, + focusEditorStep: action.editorStep, } case "reset-focus": return { ...state, focusStepIndex: null, - focusCodeProps: {}, + focusEditorStep: null, } default: throw new Error() diff --git a/packages/scrollycoding/src/focus.tsx b/packages/scrollycoding/src/focus.tsx index 5776de9d..90ac5ace 100644 --- a/packages/scrollycoding/src/focus.tsx +++ b/packages/scrollycoding/src/focus.tsx @@ -1,19 +1,21 @@ -import { useClasser } from "@code-hike/classer" -import * as React from "react" -import { useStepIndex } from "./demo-context" +import React from "react" import { useHikeContext, FluidHikeContext, - CodeProps, } from "./hike-context" +import { useClasser } from "@code-hike/classer" +import { useStepData } from "./content-column" +import { EditorStep } from "@code-hike/mini-editor" -export interface FocusProps { +interface FocusProps { children?: React.ReactNode on: string file?: string } -export function Focus({ +export { Focus, withFocusHandler, AnchorOrFocus } + +function Focus({ children, ...props }: FocusProps): JSX.Element { @@ -37,14 +39,13 @@ function FocusButton({ }: FocusProps & { context: FluidHikeContext }) { const c = useClasser("ch-hike") const { dispatch, hikeState } = context - const stepIndex = useStepIndex() - const currentFocus = hikeState.focusCodeProps.focus - const isFocused = currentFocus === focus - const codeProps: Partial = { focus } - if (file) { - codeProps.activeFile = file - } + const [stepIndex, newEditorStep] = useEditorStep( + file, + focus + ) + const oldEditorStep = hikeState.focusEditorStep + const isFocused = newEditorStep === oldEditorStep return ( +
+ ) +} + +const app =

Hello World

+ +ReactDOM.render(app, document.getElementById("root")) +``` + + + +To create a component you only need to write a function with a name that starts with a capital letter. + + + +```jsx focus=4[10:20],12:17 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent() { + return ( +
+ +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +Now you can use that function in JSX. + + + +```jsx focus=14[18:29],15[18:31] +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent() { + return ( +
+ +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +You can assign attributes + + + +```jsx focus=4[22:29],14[18:29],15[18:31] +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + return ( +
+ +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +And React will pass them to the component as parameters + + + +```jsx focus=4[22:29],7 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + return ( +
+ +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +Inside JSX, you use curly braces to wrap dynamic data + + + +```jsx focus=5,9 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const goalCount = 2 + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +In fact you can put any javascript expression. + + + +```jsx focus=7:9,13[15:35] +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const goalCount = 2 + + const handleClick = event => { + // do something + } + + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +To add event listeners you pass a function to the corresponding attribute + + + +```jsx focus=5 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const [goalCount, setCount] = React.useState(2) + + const handleClick = event => { + // do something + } + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +To add state to a component there's the useState function from React. + + + +```jsx focus=5,7:9 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const [goalCount, setCount] = React.useState(2) + + const handleClick = event => { + setCount(goalCount + 1) + } + + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +const app = ( +
+ + +
+) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +It gives you a function to update the state. + + + +```jsx focus=5,7:9,13,14 +``` + + + +When you call it, React will know it needs to re-render the component. + + + +```jsx focus=19:25,28:31 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const [goalCount, setCount] = React.useState(2) + + const handleClick = event => { + setCount(goalCount + 1) + } + + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +function MyBox() { + return ( +
+ // TODO something +
+ ) +} + +const app = ( + + + + +) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +If you want to compose components together + + + +```jsx focus=19[16:27],22,28:31 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const [goalCount, setCount] = React.useState(2) + + const handleClick = event => { + setCount(goalCount + 1) + } + + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +function MyBox({ children }) { + return ( +
+ {children} +
+ ) +} + +const app = ( + + + + +) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +React passes the nested elements inside a special parameter called children. + + + +```jsx focus=27 +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const [goalCount, setCount] = React.useState(2) + + const handleClick = event => { + setCount(goalCount + 1) + } + + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +function MyBox({ children }) { + return ( +
+ {children} +
+ ) +} + +const players = ["Messi", "Ronaldo", "Laspada"] + +const app = ( + + + + +) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +To render a list + + + +```jsx focus=27,31[6:34],32,33[1:6] +import React from "react" +import ReactDOM from "react-dom" + +function MyComponent({ name }) { + const [goalCount, setCount] = React.useState(2) + + const handleClick = event => { + setCount(goalCount + 1) + } + + return ( +
+ + {"⚽".repeat(goalCount)} +
+ ) +} + +function MyBox({ children }) { + return ( +
+ {children} +
+ ) +} + +const players = ["Messi", "Ronaldo", "Laspada"] + +const app = ( + + {players.map(playerName => ( + + ))} + +) + +ReactDOM.render(app, document.getElementById("root")) +``` + +
+ +you can map each list item to an element using javascript. + + + +```jsx focus=32[38:54] +``` + + + +React only needs a unique key for each element, to find out when something changes. diff --git a/packages/storybook/src/assets/steps.svelte.mdx b/packages/storybook/src/assets/steps.svelte.mdx new file mode 100644 index 00000000..3abcc1ef --- /dev/null +++ b/packages/storybook/src/assets/steps.svelte.mdx @@ -0,0 +1,323 @@ + + +```svelte App.svelte +

Svelte

+``` + +
+ +Svelte uses a custom file format, similar to HTML + + + +```svelte App.svelte + + +

Hello {name}

+``` + +
+ +You can use curly braces to render data. + + + +```svelte App.svelte + +``` + +--- + +```svelte MyComponent.svelte +
+ +
+``` + +
+ +Each svelte file is a different component + + + +```svelte App.svelte focus=2,5,6 + + + + +``` + +--- + +```svelte MyComponent.svelte focus=2[1] +
+ +
+``` + +
+ +You can add imports inside the script tag + + + +```svelte App.svelte focus=5[14:26],6[14:28] + + + + +``` + +--- + +```svelte MyComponent.svelte focus=2[1] +
+ +
+``` + +
+ +To share data with children components + + + +```svelte App.svelte focus=5[14:26],6[14:28] + + + + +``` + +--- + +```svelte MyComponent.svelte focus=2,6 + + +
+ +
+``` + +
+ +you can create props using the export keyword + + + +```svelte MyComponent.svelte focus=3,8 + + +
+ + {"⚽".repeat(goalCount)} +
+``` + +
+ +Inside curly braces you can put any javascript expression + + + +```svelte MyComponent.svelte focus=11[11:32] + + +
+ + {"⚽".repeat(goalCount)} +
+``` + +
+ +With the on directive you can listen to events + + + +```svelte MyComponent.svelte focus=5:7,11[11:32] + + +
+ + {"⚽".repeat(goalCount)} +
+``` + +
+ +here, when the goalcount changes after a click, svelte will rerender the component (show) + + + +```svelte MyComponent.svelte focus=2:3,8,10,11 + + +
+ + {"⚽".repeat(goalCount)} +
+``` + +
+ +You can also use a custom event to share data with the parent + + + +```svelte App.svelte focus=9[27:42],10[29:44] + + + + +``` + +--- + +```svelte MyComponent.svelte focus=10 + +``` + + + +and use the on directive again to handle it + + + +```svelte App.svelte focus=4:6,9[27:42],10[29:44] + + + + +``` + + + +and do something with the data + + + +```svelte App.svelte focus=3 + + + + +``` + + + +If you want to render a list + + + +```svelte App.svelte focus=3,9,10[16:28],11 + + +{#each players as player} + +{/each} +``` + + + +you can use the each block. + + + +```svelte App.svelte focus=9:13 + + + + +
+{#each players as player} + +{/each} +
+``` + +
+ +Svelte also support style tags, + + + +```svelte App.svelte focus=10:12,15,19 + +``` + + + +Here the style only applies to the div from the current component and not others. diff --git a/packages/storybook/src/assets/steps.test.mdx b/packages/storybook/src/assets/steps.test.mdx new file mode 100644 index 00000000..198d5c45 --- /dev/null +++ b/packages/storybook/src/assets/steps.test.mdx @@ -0,0 +1,234 @@ + + +```svelte App.svelte +

Hello Svelte

+``` + +
+ +^0 + + + +```svelte App.svelte + + +

{name}

+``` + +
+ +^1 + + + +```svelte App.svelte + + + + +``` + + + +^2 + + + +```svelte App.svelte focus=2 + +``` + +--- + +```svelte MyComponent.svelte + + +
+ +
+``` + +
+ +^3 Each svelte file is a different component + + + +```svelte App.svelte + +``` + +```svelte MyComponent.svelte focus=2 active + + +
+ +
+``` + +
+ +^4 You can declare props with the export keyword. + + + +```svelte MyComponent.svelte focus=3,8 + + +
+ + {"⚽".repeat(goalCount)} +
+``` + +
+ +^5 using js inside curly braces + + + +```svelte MyComponent.svelte +MC6 +``` + + + +^6 adding event handlers + + + +```svelte MyComponent.svelte +MC7 +``` + + + +^7 firing custom events + + + +```svelte App.svelte +APP 8 +``` + +--- + +```svelte MyComponent.svelte +MC8 +``` + + + +^8 handling custom events + + + +```svelte App.svelte +APP 9 +``` + +--- + +```svelte MyBox.svelte +MB 9 +``` + + + +adding style + + + +```svelte App.svelte + + + + + + +``` + + + +... + + + +```svelte MyBox.svelte +
+ +
+ + +``` + +
+ +using slot + + + +```svelte App.svelte + + + + + + +``` + + + +rendering lists + + + +```svelte App.svelte + + + + {#each players as player} + + {/each} + +``` + + + +... diff --git a/packages/storybook/src/assets/styles.css b/packages/storybook/src/assets/styles.css new file mode 100644 index 00000000..24980835 --- /dev/null +++ b/packages/storybook/src/assets/styles.css @@ -0,0 +1,7 @@ +.steps-story .ch-frame { + max-width: none; +} + +.steps-story .ch-editor-tab { + min-width: fit-content; +} diff --git a/packages/storybook/src/mini-editor-hike.story.js b/packages/storybook/src/mini-editor-hike.story.js index b45609d0..408adc3e 100644 --- a/packages/storybook/src/mini-editor-hike.story.js +++ b/packages/storybook/src/mini-editor-hike.story.js @@ -23,21 +23,22 @@ export const empty = () => ( ) -export const justCode = () => ( - - - -) - export const code = () => { - const steps = [{ code: code1 }, { code: code2 }] + const steps = [ + { + files: [{ code: code1, name: "hi.js", lang: "js" }], + northPanel: { active: "hi.js", tabs: ["hi.js"] }, + }, + { + files: [{ code: code2, name: "hi.js", lang: "js" }], + northPanel: { active: "hi.js", tabs: ["hi.js"] }, + }, + ] return ( {(progress, backward) => ( {(progress, backward) => ( )} @@ -94,10 +95,8 @@ console.log(8)` {(progress, backward) => ( @@ -123,13 +122,13 @@ console.log(8)` {(progress, backward) => ( )} @@ -153,10 +152,8 @@ console.log(4)` {(progress, backward) => ( @@ -164,6 +161,7 @@ console.log(4)` ) } + export const files = () => { const steps = [ { code: "log('foo',1)", file: "foo.js" }, @@ -176,11 +174,11 @@ export const files = () => { {(progress, backward) => ( )} @@ -198,22 +196,19 @@ export const minColumns = () => { 1)

minColumns 80

)} @@ -232,10 +227,8 @@ export const terminal = () => { {(progress, backward) => ( @@ -248,93 +241,87 @@ export const withButton = () => { return ( , + }} + steps={toSteps([{ code: code1 }])} progress={0} - button={} /> ) } -function CodeSandboxIcon() { - return ( - - - - - - ) -} - export const manyTabs = () => { return (

Three tabs

With button

, + }} + steps={toSteps( + [{ code: code1 }], + ["index.js", "two.css", "three.html"] + )} progress={0} - tabs={["index.js", "two.css", "three.html"]} - button={} />

Long name

, + }} + steps={toSteps( + [ + { + code: code1, + file: "index-with-long-name.js", + }, + ], + [ + "index-with-long-name.js", + "two.css", + "three.html", + ] + )} progress={0} - tabs={[ - "index-with-long-name.js", - "two.css", - "three.html", - ]} - button={} />

Six tabs

, + }} + steps={toSteps( + [ + { + code: code1, + file: "two.js", + }, + ], + [ + "index.js", + "two.js", + "three.html", + "four.js", + "five.css", + "six.html", + ] + )} progress={0} - tabs={[ - "index.js", - "two.js", - "three.html", - "four.js", - "five.css", - "six.html", - ]} - button={} />
) @@ -342,30 +329,60 @@ export const manyTabs = () => { export const x = () => { return ( - + {(progress, backward) => ( )} ) } -const xprops = { - steps: [ - { - code: `const app =

Hello World

+function toSteps( + codeSteps, + tabs = null, + name = "index.js", + lang = "js" +) { + return codeSteps.map(codeStep => ({ + files: [ + { + name: codeStep.file || name, + lang, + code: codeStep.code, + focus: codeStep.focus, + }, + ], + northPanel: { + active: codeStep.file || name, + tabs: tabs || [codeStep.file || name], + heightRatio: 1, + }, + terminal: codeStep.terminal, + })) +} + +const xsteps = [ + { + code: `const app =

Hello World

ReactDOM.render(app, document.getElementById('root'))`, - focus: "1", - }, - { - code: `function MyComponent() { + focus: "1", + }, + { + code: `function MyComponent() { return (
@@ -380,10 +397,10 @@ ReactDOM.render(app, document.getElementById('root'))`, const app =

Hello World

ReactDOM.render(app, document.getElementById('root'))`, - focus: "1:7", - }, - { - code: `function MyComponent() { + focus: "1:7", + }, + { + code: `function MyComponent() { return (
@@ -398,10 +415,10 @@ ReactDOM.render(app, document.getElementById('root'))`, const app =

Hello World

ReactDOM.render(app, document.getElementById('root'))`, - focus: "1:7", - }, - { - code: `function MyComponent() { + focus: "1:7", + }, + { + code: `function MyComponent() { return (
@@ -414,15 +431,40 @@ ReactDOM.render(app, document.getElementById('root'))`, const app =

Hello World

ReactDOM.render(app, document.getElementById('root'))`, - focus: "7", - }, - { - code: `const app =

Hello World

+ focus: "7", + }, + { + code: `const app =

Hello World

ReactDOM.render(app, document.getElementById('root'))`, - focus: "1", - }, - ], - lang: "jsx", - file: "index.js", + focus: "1", + }, +] + +function CodeSandboxIcon() { + return ( + + + + + + ) } diff --git a/packages/storybook/src/mini-editor-tween.story.js b/packages/storybook/src/mini-editor-tween.story.js new file mode 100644 index 00000000..aeb9eacd --- /dev/null +++ b/packages/storybook/src/mini-editor-tween.story.js @@ -0,0 +1,283 @@ +import React from "react" +import { MiniEditorTween } from "@code-hike/mini-editor" +import { WithProgress } from "./utils" +import "@code-hike/mini-editor/dist/index.css" + +export default { + title: "Mini Editor Tween", +} + +export const oneToOne = () => { + return ( + // prettier-ignore + + ) +} + +export const oneToOneTabs = () => { + return ( + // prettier-ignore + + ) +} + +export const oneToTwoNorth = () => { + return ( + // prettier-ignore + + ) +} + +export const oneToTwoSouth = () => { + return ( + // prettier-ignore + + ) +} + +export const twoToOneNorth = () => { + return ( + // prettier-ignore + + ) +} + +export const twoToOneSouth = () => { + return ( + // prettier-ignore + + ) +} + +export const twoToTwo = () => { + return ( + + ) +} + +const files0 = [ + { + name: "foo.js", + lang: "js", + code: `console.log(foo, 1)`, + }, + { + name: "bar.js", + lang: "js", + code: `console.log(bar, 1)`, + }, + { + name: "x.js", + lang: "js", + code: `console.log(x, 1)`, + }, + { + name: "y.js", + lang: "js", + code: `console.log(y, 1)`, + }, +] +const files1 = [ + { + name: "foo.js", + lang: "js", + code: `console.log(foo, 2)`, + }, + { + name: "bar.js", + lang: "js", + code: `console.log(bar, 2)`, + }, + { + name: "x.js", + lang: "js", + code: `console.log(x, 2)`, + }, + { + name: "y.js", + lang: "js", + code: `console.log(y, 2)`, + }, +] + +function TestTransition({ + tabs, + actives = [[], []], + ratios = [[], []], +}) { + const [ + prevNorthTabs, + nextNorthTabs, + prevSouthTabs, + nextSouthTabs, + ] = tabs + const [ + [prevNorthActive, nextNorthActive], + [prevSouthActive, nextSouthActive], + ] = actives + + const [ + [prevNorthRatio, nextNorthRatio], + [prevSouthRatio, nextSouthRatio], + ] = ratios + + const prev = { + files: files0, + northPanel: { + tabs: prevNorthTabs, + active: prevNorthActive, + heightRatio: prevNorthRatio, + }, + southPanel: prevSouthTabs + ? { + tabs: prevSouthTabs, + active: prevSouthActive, + heightRatio: prevSouthRatio, + } + : undefined, + } + const next = { + files: files1, + northPanel: { + tabs: nextNorthTabs, + active: nextNorthActive, + heightRatio: nextNorthRatio, + }, + southPanel: nextSouthTabs + ? { + tabs: nextSouthTabs, + active: nextSouthActive, + heightRatio: nextSouthRatio, + } + : undefined, + } + + return ( + + {(progress, backward) => ( + <> + + + + + + + + + + +
+ {prevNorthActive} +
+ {JSON.stringify(prevNorthTabs)} +
+ {nextNorthActive} +
+ {JSON.stringify(nextNorthTabs)} +
+ {prevSouthActive} +
+ {JSON.stringify(prevSouthTabs)} +
+ {nextSouthActive} +
+ {JSON.stringify(nextSouthTabs)} +
+ + )} +
+ ) +} diff --git a/packages/storybook/src/mini-editor.story.js b/packages/storybook/src/mini-editor.story.js index 5559b251..95fca6ff 100644 --- a/packages/storybook/src/mini-editor.story.js +++ b/packages/storybook/src/mini-editor.story.js @@ -27,7 +27,12 @@ export const focusEditor = () => { Focus
- + ) } @@ -47,7 +52,153 @@ export const codeEditor = () => { Change
- + + + ) +} + +export const singlePanelEditor = () => { + const [input, setInput] = React.useState(code1) + const [code, setCode] = React.useState(code1) + const files = [ + { name: "index.js", lang: "js", code: "" }, + { name: "app.js", lang: "js", code }, + ] + return ( + +
+