|
| 1 | +import React from "react" |
| 2 | +import { EditorFrame, TerminalPanel } from "./editor-frame" |
| 3 | +import { InnerTerminal } from "@code-hike/mini-terminal" |
| 4 | +import { Code } from "./code" |
| 5 | +import { |
| 6 | + useBackwardTransitions, |
| 7 | + useForwardTransitions, |
| 8 | +} from "./steps" |
| 9 | +import { Classes } from "@code-hike/classer" |
| 10 | +// import "./theme.css" |
| 11 | + |
| 12 | +export { MiniEditorHike } |
| 13 | + |
| 14 | +type MiniEditorStep = { |
| 15 | + code?: string |
| 16 | + focus?: string |
| 17 | + lang?: string |
| 18 | + file?: string |
| 19 | + tabs?: string[] |
| 20 | + terminal?: string |
| 21 | +} |
| 22 | + |
| 23 | +export type MiniEditorHikeProps = { |
| 24 | + progress?: number |
| 25 | + backward?: boolean |
| 26 | + code?: string |
| 27 | + focus?: string |
| 28 | + lang?: string |
| 29 | + file?: string |
| 30 | + tabs?: string[] |
| 31 | + steps?: MiniEditorStep[] |
| 32 | + height?: number |
| 33 | + minColumns?: number |
| 34 | + minZoom?: number |
| 35 | + maxZoom?: number |
| 36 | + button?: React.ReactNode |
| 37 | + classes?: Classes |
| 38 | +} & React.PropsWithoutRef<JSX.IntrinsicElements["div"]> |
| 39 | + |
| 40 | +function MiniEditorHike(props: MiniEditorHikeProps) { |
| 41 | + const { |
| 42 | + progress = 0, |
| 43 | + backward = false, |
| 44 | + code, |
| 45 | + focus, |
| 46 | + lang, |
| 47 | + file, |
| 48 | + steps: ogSteps, |
| 49 | + tabs: ogTabs, |
| 50 | + minColumns = 50, |
| 51 | + minZoom = 0.2, |
| 52 | + maxZoom = 1, |
| 53 | + height, |
| 54 | + ...rest |
| 55 | + } = props |
| 56 | + const { steps, files, stepsByFile } = useSteps(ogSteps, { |
| 57 | + code, |
| 58 | + focus, |
| 59 | + lang, |
| 60 | + file, |
| 61 | + tabs: ogTabs, |
| 62 | + }) |
| 63 | + |
| 64 | + const activeStepIndex = backward |
| 65 | + ? Math.floor(progress) |
| 66 | + : Math.ceil(progress) |
| 67 | + const activeStep = steps[activeStepIndex] |
| 68 | + const activeFile = (activeStep && activeStep.file) || "" |
| 69 | + |
| 70 | + const activeSteps = stepsByFile[activeFile] || [] |
| 71 | + |
| 72 | + const tabs = activeStep.tabs || files |
| 73 | + |
| 74 | + const terminalHeight = getTerminalHeight(steps, progress) |
| 75 | + |
| 76 | + const terminalSteps = steps.map(s => ({ |
| 77 | + text: (s && s.terminal) || "", |
| 78 | + })) |
| 79 | + |
| 80 | + const contentSteps = useStepsWithDefaults( |
| 81 | + { code, focus, lang, file }, |
| 82 | + ogSteps || [] |
| 83 | + ) |
| 84 | + |
| 85 | + return ( |
| 86 | + <EditorFrame |
| 87 | + files={tabs} |
| 88 | + active={activeFile} |
| 89 | + terminalPanel={ |
| 90 | + <TerminalPanel height={terminalHeight}> |
| 91 | + <InnerTerminal |
| 92 | + steps={terminalSteps} |
| 93 | + progress={progress} |
| 94 | + /> |
| 95 | + </TerminalPanel> |
| 96 | + } |
| 97 | + height={height} |
| 98 | + {...rest} |
| 99 | + > |
| 100 | + {activeSteps.length > 0 && ( |
| 101 | + <EditorContent |
| 102 | + key={activeFile} |
| 103 | + backward={backward} |
| 104 | + progress={progress} |
| 105 | + steps={contentSteps} |
| 106 | + parentHeight={height} |
| 107 | + minColumns={minColumns} |
| 108 | + minZoom={minZoom} |
| 109 | + maxZoom={maxZoom} |
| 110 | + /> |
| 111 | + )} |
| 112 | + </EditorFrame> |
| 113 | + ) |
| 114 | +} |
| 115 | + |
| 116 | +function useStepsWithDefaults( |
| 117 | + defaults: MiniEditorStep, |
| 118 | + steps: MiniEditorStep[] |
| 119 | +): ContentStep[] { |
| 120 | + const files = [ |
| 121 | + ...new Set( |
| 122 | + steps.map(s => coalesce(s.file, defaults.file, "")) |
| 123 | + ), |
| 124 | + ] |
| 125 | + return steps.map(step => { |
| 126 | + return { |
| 127 | + code: coalesce(step.code, defaults.code, ""), |
| 128 | + file: coalesce(step.file, defaults.file, ""), |
| 129 | + focus: coalesce(step.focus, defaults.focus, ""), |
| 130 | + lang: coalesce( |
| 131 | + step.lang, |
| 132 | + defaults.lang, |
| 133 | + "javascript" |
| 134 | + ), |
| 135 | + tabs: coalesce(step.tabs, defaults.tabs, files), |
| 136 | + terminal: step.terminal || defaults.terminal, |
| 137 | + } |
| 138 | + }) |
| 139 | +} |
| 140 | + |
| 141 | +function coalesce<T>( |
| 142 | + a: T | null | undefined, |
| 143 | + b: T | null | undefined, |
| 144 | + c: T |
| 145 | +): T { |
| 146 | + return a != null ? a : b != null ? b : c |
| 147 | +} |
| 148 | + |
| 149 | +type ContentStep = { |
| 150 | + code: string |
| 151 | + focus: string |
| 152 | + lang: string |
| 153 | + file: string |
| 154 | + tabs: string[] |
| 155 | + terminal?: string |
| 156 | +} |
| 157 | + |
| 158 | +type ContentProps = { |
| 159 | + progress: number |
| 160 | + backward: boolean |
| 161 | + steps: ContentStep[] |
| 162 | + parentHeight?: number |
| 163 | + minColumns: number |
| 164 | + minZoom: number |
| 165 | + maxZoom: number |
| 166 | +} |
| 167 | + |
| 168 | +function EditorContent({ |
| 169 | + progress, |
| 170 | + backward, |
| 171 | + steps, |
| 172 | + parentHeight, |
| 173 | + minColumns, |
| 174 | + minZoom, |
| 175 | + maxZoom, |
| 176 | +}: ContentProps) { |
| 177 | + const fwdTransitions = useForwardTransitions(steps) |
| 178 | + const bwdTransitions = useBackwardTransitions(steps) |
| 179 | + |
| 180 | + const transitionIndex = Math.ceil(progress) |
| 181 | + const { |
| 182 | + prevCode, |
| 183 | + nextCode, |
| 184 | + prevFocus, |
| 185 | + nextFocus, |
| 186 | + lang, |
| 187 | + } = backward |
| 188 | + ? bwdTransitions[transitionIndex] |
| 189 | + : fwdTransitions[transitionIndex] |
| 190 | + |
| 191 | + return ( |
| 192 | + <Code |
| 193 | + prevCode={prevCode || nextCode!} |
| 194 | + nextCode={nextCode || prevCode!} |
| 195 | + prevFocus={prevFocus} |
| 196 | + nextFocus={nextFocus} |
| 197 | + language={lang} |
| 198 | + progress={progress - transitionIndex + 1} |
| 199 | + parentHeight={parentHeight} |
| 200 | + minColumns={minColumns} |
| 201 | + minZoom={minZoom} |
| 202 | + maxZoom={maxZoom} |
| 203 | + /> |
| 204 | + ) |
| 205 | +} |
| 206 | + |
| 207 | +function useSteps( |
| 208 | + ogSteps: MiniEditorStep[] | undefined, |
| 209 | + { code = "", focus, lang, file, tabs }: MiniEditorStep |
| 210 | +) { |
| 211 | + return React.useMemo(() => { |
| 212 | + const steps = ogSteps?.map(s => ({ |
| 213 | + code, |
| 214 | + focus, |
| 215 | + lang, |
| 216 | + file, |
| 217 | + tabs, |
| 218 | + ...s, |
| 219 | + })) || [{ code, focus, lang, file, tabs }] |
| 220 | + |
| 221 | + const files = [ |
| 222 | + ...new Set( |
| 223 | + steps |
| 224 | + .map((s: any) => s.file) |
| 225 | + .filter((f: any) => f != null) |
| 226 | + ), |
| 227 | + ] |
| 228 | + |
| 229 | + const stepsByFile: Record<string, MiniEditorStep[]> = {} |
| 230 | + steps.forEach(s => { |
| 231 | + if (s.file == null) return |
| 232 | + if (!stepsByFile[s.file]) { |
| 233 | + stepsByFile[s.file] = [] |
| 234 | + } |
| 235 | + stepsByFile[s.file].push(s) |
| 236 | + }) |
| 237 | + |
| 238 | + return { steps, files, stepsByFile } |
| 239 | + }, [ogSteps, code, focus, lang, file, tabs]) |
| 240 | +} |
| 241 | + |
| 242 | +const MAX_HEIGHT = 150 |
| 243 | +function getTerminalHeight(steps: any, progress: number) { |
| 244 | + if (!steps.length) { |
| 245 | + return 0 |
| 246 | + } |
| 247 | + |
| 248 | + const prevIndex = Math.floor(progress) |
| 249 | + const nextIndex = Math.ceil(progress) |
| 250 | + const prevTerminal = |
| 251 | + steps[prevIndex] && steps[prevIndex].terminal |
| 252 | + const nextTerminal = steps[nextIndex].terminal |
| 253 | + |
| 254 | + if (!prevTerminal && !nextTerminal) return 0 |
| 255 | + |
| 256 | + if (!prevTerminal && nextTerminal) |
| 257 | + return MAX_HEIGHT * Math.min((progress % 1) * 4, 1) |
| 258 | + if (prevTerminal && !nextTerminal) |
| 259 | + return MAX_HEIGHT * Math.max(1 - (progress % 1) * 4, 0) |
| 260 | + |
| 261 | + return MAX_HEIGHT |
| 262 | +} |
0 commit comments