@@ -3,10 +3,6 @@ import { TutorialStore } from '@tutorialkit/runtime';
33import type { I18n } from '@tutorialkit/types' ;
44import { useCallback , useEffect , useRef , useState } from 'react' ;
55import { Panel , PanelGroup , PanelResizeHandle , type ImperativePanelHandle } from 'react-resizable-panels' ;
6- import type {
7- OnChangeCallback as OnEditorChange ,
8- OnScrollCallback as OnEditorScroll ,
9- } from '../core/CodeMirrorEditor/index.js' ;
106import type { Theme } from '../core/types.js' ;
117import resizePanelStyles from '../styles/resize-panel.module.css' ;
128import { classNames } from '../utils/classnames.js' ;
@@ -21,42 +17,82 @@ interface Props {
2117 theme : Theme ;
2218}
2319
20+ interface PanelProps extends Props {
21+ hasEditor : boolean ;
22+ hasPreviews : boolean ;
23+ hideTerminalPanel : boolean ;
24+ }
25+
26+ interface TerminalProps extends PanelProps {
27+ terminalPanelRef : React . RefObject < ImperativePanelHandle > ;
28+ terminalExpanded : React . MutableRefObject < boolean > ;
29+ }
30+
2431/**
2532 * This component is the orchestrator between various interactive components.
2633 */
2734export function WorkspacePanel ( { tutorialStore, theme } : Props ) {
28- const fileTree = tutorialStore . hasFileTree ( ) ;
2935 const hasEditor = tutorialStore . hasEditor ( ) ;
3036 const hasPreviews = tutorialStore . hasPreviews ( ) ;
3137 const hideTerminalPanel = ! tutorialStore . hasTerminalPanel ( ) ;
3238
33- const editorPanelRef = useRef < ImperativePanelHandle > ( null ) ;
34- const previewPanelRef = useRef < ImperativePanelHandle > ( null ) ;
3539 const terminalPanelRef = useRef < ImperativePanelHandle > ( null ) ;
36- const previewRef = useRef < ImperativePreviewHandle > ( null ) ;
3740 const terminalExpanded = useRef ( false ) ;
3841
39- const [ helpAction , setHelpAction ] = useState < 'solve' | 'reset' > ( 'reset' ) ;
42+ return (
43+ < PanelGroup className = { resizePanelStyles . PanelGroup } direction = "vertical" >
44+ < EditorSection
45+ theme = { theme }
46+ tutorialStore = { tutorialStore }
47+ hasEditor = { hasEditor }
48+ hasPreviews = { hasPreviews }
49+ hideTerminalPanel = { hideTerminalPanel }
50+ />
51+
52+ < PanelResizeHandle
53+ className = { resizePanelStyles . PanelResizeHandle }
54+ hitAreaMargins = { { fine : 5 , coarse : 5 } }
55+ disabled = { ! hasEditor }
56+ />
57+
58+ < PreviewsSection
59+ theme = { theme }
60+ tutorialStore = { tutorialStore }
61+ terminalPanelRef = { terminalPanelRef }
62+ terminalExpanded = { terminalExpanded }
63+ hideTerminalPanel = { hideTerminalPanel }
64+ hasPreviews = { hasPreviews }
65+ hasEditor = { hasEditor }
66+ />
67+
68+ < PanelResizeHandle
69+ className = { resizePanelStyles . PanelResizeHandle }
70+ hitAreaMargins = { { fine : 5 , coarse : 5 } }
71+ disabled = { hideTerminalPanel || ! hasPreviews }
72+ />
73+
74+ < TerminalSection
75+ tutorialStore = { tutorialStore }
76+ theme = { theme }
77+ terminalPanelRef = { terminalPanelRef }
78+ terminalExpanded = { terminalExpanded }
79+ hideTerminalPanel = { hideTerminalPanel }
80+ hasEditor = { hasEditor }
81+ hasPreviews = { hasPreviews }
82+ />
83+ </ PanelGroup >
84+ ) ;
85+ }
4086
87+ function EditorSection ( { theme, tutorialStore, hasEditor } : PanelProps ) {
88+ const [ helpAction , setHelpAction ] = useState < 'solve' | 'reset' > ( 'reset' ) ;
4189 const selectedFile = useStore ( tutorialStore . selectedFile ) ;
4290 const currentDocument = useStore ( tutorialStore . currentDocument ) ;
4391 const lessonFullyLoaded = useStore ( tutorialStore . lessonFullyLoaded ) ;
4492
4593 const lesson = tutorialStore . lesson ! ;
4694
47- const onEditorChange = useCallback < OnEditorChange > ( ( update ) => {
48- tutorialStore . setCurrentDocumentContent ( update . content ) ;
49- } , [ ] ) ;
50-
51- const onEditorScroll = useCallback < OnEditorScroll > ( ( position ) => {
52- tutorialStore . setCurrentDocumentScrollPosition ( position ) ;
53- } , [ ] ) ;
54-
55- const onFileSelect = useCallback ( ( filePath : string | undefined ) => {
56- tutorialStore . setSelectedFile ( filePath ) ;
57- } , [ ] ) ;
58-
59- const onHelpClick = useCallback ( ( ) => {
95+ function onHelpClick ( ) {
6096 if ( tutorialStore . hasSolution ( ) ) {
6197 setHelpAction ( ( action ) => {
6298 if ( action === 'reset' ) {
@@ -72,40 +108,57 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
72108 } else {
73109 tutorialStore . reset ( ) ;
74110 }
75- } , [ tutorialStore . ref ] ) ;
111+ }
76112
77113 useEffect ( ( ) => {
78- const lesson = tutorialStore . lesson ! ;
79-
80- const unsubscribe = tutorialStore . lessonFullyLoaded . subscribe ( ( loaded ) => {
81- if ( loaded && lesson . data . autoReload ) {
82- previewRef . current ?. reload ( ) ;
83- }
84- } ) ;
85-
86114 if ( tutorialStore . hasSolution ( ) ) {
87115 setHelpAction ( 'solve' ) ;
88116 } else {
89117 setHelpAction ( 'reset' ) ;
90118 }
91-
92- if ( tutorialStore . terminalConfig . value ?. defaultOpen ) {
93- showTerminal ( ) ;
94- }
95-
96- return ( ) => unsubscribe ( ) ;
97119 } , [ tutorialStore . ref ] ) ;
98120
99- useEffect ( ( ) => {
100- if ( hideTerminalPanel ) {
101- // force hide the terminal if we don't have any panels to show
102- hideTerminal ( ) ;
121+ return (
122+ < Panel
123+ id = { hasEditor ? 'editor-opened' : 'editor-closed' }
124+ defaultSize = { hasEditor ? 50 : 0 }
125+ minSize = { 10 }
126+ maxSize = { hasEditor ? 100 : 0 }
127+ collapsible = { ! hasEditor }
128+ className = "transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor"
129+ >
130+ < EditorPanel
131+ id = { tutorialStore . ref }
132+ theme = { theme }
133+ showFileTree = { tutorialStore . hasFileTree ( ) }
134+ editorDocument = { currentDocument }
135+ files = { lesson . files [ 1 ] }
136+ i18n = { lesson . data . i18n as I18n }
137+ hideRoot = { lesson . data . hideRoot }
138+ helpAction = { helpAction }
139+ onHelpClick = { lessonFullyLoaded ? onHelpClick : undefined }
140+ onFileSelect = { ( filePath ) => tutorialStore . setSelectedFile ( filePath ) }
141+ selectedFile = { selectedFile }
142+ onEditorScroll = { ( position ) => tutorialStore . setCurrentDocumentScrollPosition ( position ) }
143+ onEditorChange = { ( update ) => tutorialStore . setCurrentDocumentContent ( update . content ) }
144+ />
145+ </ Panel >
146+ ) ;
147+ }
103148
104- terminalExpanded . current = false ;
105- }
106- } , [ hideTerminalPanel ] ) ;
149+ function PreviewsSection ( {
150+ tutorialStore,
151+ terminalPanelRef,
152+ terminalExpanded,
153+ hideTerminalPanel,
154+ hasPreviews,
155+ hasEditor,
156+ } : TerminalProps ) {
157+ const previewRef = useRef < ImperativePreviewHandle > ( null ) ;
158+ const lesson = tutorialStore . lesson ! ;
159+ const terminalConfig = useStore ( tutorialStore . terminalConfig ) ;
107160
108- const showTerminal = useCallback ( ( ) => {
161+ function showTerminal ( ) {
109162 const { current : terminal } = terminalPanelRef ;
110163
111164 if ( ! terminal ) {
@@ -118,110 +171,109 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
118171 } else {
119172 terminal . expand ( ) ;
120173 }
121- } , [ ] ) ;
174+ }
122175
123- const hideTerminal = useCallback ( ( ) => {
124- terminalPanelRef . current ?. collapse ( ) ;
176+ const toggleTerminal = useCallback ( ( ) => {
177+ if ( terminalPanelRef . current ?. isCollapsed ( ) ) {
178+ showTerminal ( ) ;
179+ } else if ( terminalPanelRef . current ) {
180+ terminalPanelRef . current . collapse ( ) ;
181+ }
125182 } , [ ] ) ;
126183
127- const toggleTerminal = useCallback ( ( ) => {
128- const { current : terminal } = terminalPanelRef ;
184+ useEffect ( ( ) => {
185+ if ( hideTerminalPanel ) {
186+ // force hide the terminal if we don't have any panels to show
187+ terminalPanelRef . current ?. collapse ( ) ;
129188
130- if ( ! terminal ) {
131- return ;
189+ terminalExpanded . current = false ;
132190 }
191+ } , [ hideTerminalPanel ] ) ;
133192
134- if ( terminalPanelRef . current ?. isCollapsed ( ) ) {
193+ useEffect ( ( ) => {
194+ if ( terminalConfig . defaultOpen ) {
135195 showTerminal ( ) ;
136- } else {
137- hideTerminal ( ) ;
138196 }
139- } , [ ] ) ;
197+ } , [ terminalConfig . defaultOpen ] ) ;
198+
199+ useEffect ( ( ) => {
200+ const lesson = tutorialStore . lesson ! ;
201+
202+ const unsubscribe = tutorialStore . lessonFullyLoaded . subscribe ( ( loaded ) => {
203+ if ( loaded && lesson . data . autoReload ) {
204+ previewRef . current ?. reload ( ) ;
205+ }
206+ } ) ;
207+
208+ return ( ) => unsubscribe ( ) ;
209+ } , [ tutorialStore . ref ] ) ;
140210
141211 return (
142- < PanelGroup className = { resizePanelStyles . PanelGroup } direction = "vertical" >
143- < Panel
144- id = { hasEditor ? 'editor-opened' : 'editor-closed' }
145- defaultSize = { hasEditor ? 50 : 0 }
146- minSize = { 10 }
147- maxSize = { hasEditor ? 100 : 0 }
148- collapsible = { ! hasEditor }
149- ref = { editorPanelRef }
150- className = "transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor"
151- >
152- < EditorPanel
153- id = { tutorialStore . ref }
154- theme = { theme }
155- showFileTree = { fileTree }
156- editorDocument = { currentDocument }
157- files = { lesson . files [ 1 ] }
158- i18n = { lesson . data . i18n as I18n }
159- hideRoot = { lesson . data . hideRoot }
160- helpAction = { helpAction }
161- onHelpClick = { lessonFullyLoaded ? onHelpClick : undefined }
162- onFileSelect = { onFileSelect }
163- selectedFile = { selectedFile }
164- onEditorScroll = { onEditorScroll }
165- onEditorChange = { onEditorChange }
166- />
167- </ Panel >
168- < PanelResizeHandle
169- className = { resizePanelStyles . PanelResizeHandle }
170- hitAreaMargins = { { fine : 5 , coarse : 5 } }
171- disabled = { ! hasEditor }
172- />
173- < Panel
174- id = { hasPreviews ? 'previews-opened' : 'previews-closed' }
175- defaultSize = { hasPreviews ? 50 : 0 }
176- minSize = { 10 }
177- maxSize = { hasPreviews ? 100 : 0 }
178- collapsible = { ! hasPreviews }
179- ref = { previewPanelRef }
180- className = { classNames ( {
181- 'transition-theme border-t border-tk-elements-app-borderColor' : hasEditor ,
182- } ) }
183- >
184- < PreviewPanel
185- tutorialStore = { tutorialStore }
186- i18n = { lesson . data . i18n as I18n }
187- ref = { previewRef }
188- showToggleTerminal = { ! hideTerminalPanel }
189- toggleTerminal = { toggleTerminal }
190- />
191- </ Panel >
192- < PanelResizeHandle
193- className = { resizePanelStyles . PanelResizeHandle }
194- hitAreaMargins = { { fine : 5 , coarse : 5 } }
195- disabled = { hideTerminalPanel || ! hasPreviews }
212+ < Panel
213+ id = { hasPreviews ? 'previews-opened' : 'previews-closed' }
214+ defaultSize = { hasPreviews ? 50 : 0 }
215+ minSize = { 10 }
216+ maxSize = { hasPreviews ? 100 : 0 }
217+ collapsible = { ! hasPreviews }
218+ className = { classNames ( {
219+ 'transition-theme border-t border-tk-elements-app-borderColor' : hasEditor ,
220+ } ) }
221+ >
222+ < PreviewPanel
223+ ref = { previewRef }
224+ tutorialStore = { tutorialStore }
225+ i18n = { lesson . data . i18n as I18n }
226+ showToggleTerminal = { ! hideTerminalPanel }
227+ toggleTerminal = { toggleTerminal }
196228 />
197- < Panel
198- id = {
199- hideTerminalPanel
200- ? 'terminal-none'
201- : ! hasPreviews && ! hasEditor
202- ? 'terminal-full'
203- : ! hasPreviews
204- ? 'terminal-opened'
205- : 'terminal-closed'
206- }
207- defaultSize = {
208- hideTerminalPanel ? 0 : ! hasPreviews && ! hasEditor ? 100 : ! hasPreviews ? DEFAULT_TERMINAL_SIZE : 0
209- }
210- minSize = { hideTerminalPanel ? 0 : 10 }
211- collapsible = { hasPreviews }
212- ref = { terminalPanelRef }
213- onExpand = { ( ) => {
214- terminalExpanded . current = true ;
215- } }
216- className = { classNames (
217- 'transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor' ,
218- {
219- 'border-t border-tk-elements-app-borderColor' : hasPreviews ,
220- } ,
221- ) }
222- >
223- < TerminalPanel tutorialStore = { tutorialStore } theme = { theme } />
224- </ Panel >
225- </ PanelGroup >
229+ </ Panel >
230+ ) ;
231+ }
232+
233+ function TerminalSection ( {
234+ tutorialStore,
235+ theme,
236+ terminalPanelRef,
237+ terminalExpanded,
238+ hideTerminalPanel,
239+ hasEditor,
240+ hasPreviews,
241+ } : TerminalProps ) {
242+ let id = 'terminal-closed' ;
243+
244+ if ( hideTerminalPanel ) {
245+ id = 'terminal-none' ;
246+ } else if ( ! hasPreviews && ! hasEditor ) {
247+ id = 'terminal-full' ;
248+ } else if ( ! hasPreviews ) {
249+ id = 'terminal-opened' ;
250+ }
251+
252+ let defaultSize = 0 ;
253+
254+ if ( hideTerminalPanel ) {
255+ defaultSize = 0 ;
256+ } else if ( ! hasPreviews && ! hasEditor ) {
257+ defaultSize = 100 ;
258+ } else if ( ! hasPreviews ) {
259+ defaultSize = DEFAULT_TERMINAL_SIZE ;
260+ }
261+
262+ return (
263+ < Panel
264+ id = { id }
265+ defaultSize = { defaultSize }
266+ minSize = { hideTerminalPanel ? 0 : 10 }
267+ collapsible = { hasPreviews }
268+ ref = { terminalPanelRef }
269+ onExpand = { ( ) => {
270+ terminalExpanded . current = true ;
271+ } }
272+ className = { classNames ( 'transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor' , {
273+ 'border-t border-tk-elements-app-borderColor' : hasPreviews ,
274+ } ) }
275+ >
276+ < TerminalPanel tutorialStore = { tutorialStore } theme = { theme } />
277+ </ Panel >
226278 ) ;
227279}
0 commit comments