@@ -3,10 +3,6 @@ import { TutorialStore } from '@tutorialkit/runtime';
3
3
import type { I18n } from '@tutorialkit/types' ;
4
4
import { useCallback , useEffect , useRef , useState } from 'react' ;
5
5
import { 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' ;
10
6
import type { Theme } from '../core/types.js' ;
11
7
import resizePanelStyles from '../styles/resize-panel.module.css' ;
12
8
import { classNames } from '../utils/classnames.js' ;
@@ -21,42 +17,82 @@ interface Props {
21
17
theme : Theme ;
22
18
}
23
19
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
+
24
31
/**
25
32
* This component is the orchestrator between various interactive components.
26
33
*/
27
34
export function WorkspacePanel ( { tutorialStore, theme } : Props ) {
28
- const fileTree = tutorialStore . hasFileTree ( ) ;
29
35
const hasEditor = tutorialStore . hasEditor ( ) ;
30
36
const hasPreviews = tutorialStore . hasPreviews ( ) ;
31
37
const hideTerminalPanel = ! tutorialStore . hasTerminalPanel ( ) ;
32
38
33
- const editorPanelRef = useRef < ImperativePanelHandle > ( null ) ;
34
- const previewPanelRef = useRef < ImperativePanelHandle > ( null ) ;
35
39
const terminalPanelRef = useRef < ImperativePanelHandle > ( null ) ;
36
- const previewRef = useRef < ImperativePreviewHandle > ( null ) ;
37
40
const terminalExpanded = useRef ( false ) ;
38
41
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
+ }
40
86
87
+ function EditorSection ( { theme, tutorialStore, hasEditor } : PanelProps ) {
88
+ const [ helpAction , setHelpAction ] = useState < 'solve' | 'reset' > ( 'reset' ) ;
41
89
const selectedFile = useStore ( tutorialStore . selectedFile ) ;
42
90
const currentDocument = useStore ( tutorialStore . currentDocument ) ;
43
91
const lessonFullyLoaded = useStore ( tutorialStore . lessonFullyLoaded ) ;
44
92
45
93
const lesson = tutorialStore . lesson ! ;
46
94
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 ( ) {
60
96
if ( tutorialStore . hasSolution ( ) ) {
61
97
setHelpAction ( ( action ) => {
62
98
if ( action === 'reset' ) {
@@ -72,40 +108,57 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
72
108
} else {
73
109
tutorialStore . reset ( ) ;
74
110
}
75
- } , [ tutorialStore . ref ] ) ;
111
+ }
76
112
77
113
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
-
86
114
if ( tutorialStore . hasSolution ( ) ) {
87
115
setHelpAction ( 'solve' ) ;
88
116
} else {
89
117
setHelpAction ( 'reset' ) ;
90
118
}
91
-
92
- if ( tutorialStore . terminalConfig . value ?. defaultOpen ) {
93
- showTerminal ( ) ;
94
- }
95
-
96
- return ( ) => unsubscribe ( ) ;
97
119
} , [ tutorialStore . ref ] ) ;
98
120
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
+ }
103
148
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 ) ;
107
160
108
- const showTerminal = useCallback ( ( ) => {
161
+ function showTerminal ( ) {
109
162
const { current : terminal } = terminalPanelRef ;
110
163
111
164
if ( ! terminal ) {
@@ -118,110 +171,109 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
118
171
} else {
119
172
terminal . expand ( ) ;
120
173
}
121
- } , [ ] ) ;
174
+ }
122
175
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
+ }
125
182
} , [ ] ) ;
126
183
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 ( ) ;
129
188
130
- if ( ! terminal ) {
131
- return ;
189
+ terminalExpanded . current = false ;
132
190
}
191
+ } , [ hideTerminalPanel ] ) ;
133
192
134
- if ( terminalPanelRef . current ?. isCollapsed ( ) ) {
193
+ useEffect ( ( ) => {
194
+ if ( terminalConfig . defaultOpen ) {
135
195
showTerminal ( ) ;
136
- } else {
137
- hideTerminal ( ) ;
138
196
}
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 ] ) ;
140
210
141
211
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 }
196
228
/>
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 >
226
278
) ;
227
279
}
0 commit comments