@@ -3,15 +3,11 @@ 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' ;
13
9
import { EditorPanel } from './EditorPanel.js' ;
14
- import { PreviewPanel , type ImperativePreviewHandle } from './PreviewPanel.js' ;
10
+ import { PreviewPanel } from './PreviewPanel.js' ;
15
11
import { TerminalPanel } from './TerminalPanel.js' ;
16
12
17
13
const DEFAULT_TERMINAL_SIZE = 25 ;
@@ -21,41 +17,80 @@ 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
+ < Editor
45
+ theme = { theme }
46
+ tutorialStore = { tutorialStore }
47
+ hasEditor = { hasEditor }
48
+ hasPreviews = { hasPreviews }
49
+ hideTerminalPanel = { hideTerminalPanel }
50
+ />
40
51
41
- const selectedFile = useStore ( tutorialStore . selectedFile ) ;
42
- const currentDocument = useStore ( tutorialStore . currentDocument ) ;
52
+ < PanelResizeHandle
53
+ className = { resizePanelStyles . PanelResizeHandle }
54
+ hitAreaMargins = { { fine : 5 , coarse : 5 } }
55
+ disabled = { ! hasEditor }
56
+ />
43
57
44
- const lesson = tutorialStore . lesson ! ;
58
+ < Preview
59
+ theme = { theme }
60
+ tutorialStore = { tutorialStore }
61
+ terminalPanelRef = { terminalPanelRef }
62
+ terminalExpanded = { terminalExpanded }
63
+ hideTerminalPanel = { hideTerminalPanel }
64
+ hasPreviews = { hasPreviews }
65
+ hasEditor = { hasEditor }
66
+ />
45
67
46
- const onEditorChange = useCallback < OnEditorChange > ( ( update ) => {
47
- tutorialStore . setCurrentDocumentContent ( update . content ) ;
48
- } , [ ] ) ;
68
+ < PanelResizeHandle
69
+ className = { resizePanelStyles . PanelResizeHandle }
70
+ hitAreaMargins = { { fine : 5 , coarse : 5 } }
71
+ disabled = { hideTerminalPanel || ! hasPreviews }
72
+ />
49
73
50
- const onEditorScroll = useCallback < OnEditorScroll > ( ( position ) => {
51
- tutorialStore . setCurrentDocumentScrollPosition ( position ) ;
52
- } , [ ] ) ;
74
+ < Terminal
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
+ }
53
86
54
- const onFileSelect = useCallback ( ( filePath : string | undefined ) => {
55
- tutorialStore . setSelectedFile ( filePath ) ;
56
- } , [ ] ) ;
87
+ function Editor ( { theme, tutorialStore, hasEditor } : PanelProps ) {
88
+ const [ helpAction , setHelpAction ] = useState < 'solve' | 'reset' > ( 'reset' ) ;
89
+ const selectedFile = useStore ( tutorialStore . selectedFile ) ;
90
+ const currentDocument = useStore ( tutorialStore . currentDocument ) ;
91
+ const lesson = tutorialStore . lesson ! ;
57
92
58
- const onHelpClick = useCallback ( ( ) => {
93
+ function onHelpClick ( ) {
59
94
if ( tutorialStore . hasSolution ( ) ) {
60
95
setHelpAction ( ( action ) => {
61
96
if ( action === 'reset' ) {
@@ -71,157 +106,147 @@ export function WorkspacePanel({ tutorialStore, theme }: Props) {
71
106
} else {
72
107
tutorialStore . reset ( ) ;
73
108
}
74
- } , [ tutorialStore . ref ] ) ;
109
+ }
75
110
76
111
useEffect ( ( ) => {
77
- const lesson = tutorialStore . lesson ! ;
78
-
79
- const unsubscribe = tutorialStore . lessonFullyLoaded . subscribe ( ( loaded ) => {
80
- if ( loaded && lesson . data . autoReload ) {
81
- /**
82
- * @todo This causes some race with the preview where the iframe can show the "wrong" page.
83
- * I think the reason is that when the ports are different then we render new frames which
84
- * races against the reload which will internally reset the `src` attribute.
85
- */
86
- // previewRef.current?.reload();
87
- }
88
- } ) ;
89
-
90
112
if ( tutorialStore . hasSolution ( ) ) {
91
113
setHelpAction ( 'solve' ) ;
92
114
} else {
93
115
setHelpAction ( 'reset' ) ;
94
116
}
95
-
96
- return ( ) => unsubscribe ( ) ;
97
117
} , [ tutorialStore . ref ] ) ;
98
118
99
- useEffect ( ( ) => {
100
- if ( hideTerminalPanel ) {
101
- // force hide the terminal if we don't have any panels to show
102
- hideTerminal ( ) ;
119
+ return (
120
+ < Panel
121
+ id = { hasEditor ? 'editor-opened' : 'editor-closed' }
122
+ defaultSize = { hasEditor ? 50 : 0 }
123
+ minSize = { 10 }
124
+ maxSize = { hasEditor ? 100 : 0 }
125
+ collapsible = { ! hasEditor }
126
+ className = "transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor"
127
+ >
128
+ < EditorPanel
129
+ id = { tutorialStore . ref }
130
+ theme = { theme }
131
+ showFileTree = { tutorialStore . hasFileTree ( ) }
132
+ editorDocument = { currentDocument }
133
+ files = { lesson . files [ 1 ] }
134
+ i18n = { lesson . data . i18n as I18n }
135
+ hideRoot = { lesson . data . hideRoot }
136
+ helpAction = { helpAction }
137
+ onHelpClick = { onHelpClick }
138
+ onFileSelect = { ( filePath ) => tutorialStore . setSelectedFile ( filePath ) }
139
+ selectedFile = { selectedFile }
140
+ onEditorScroll = { ( position ) => tutorialStore . setCurrentDocumentScrollPosition ( position ) }
141
+ onEditorChange = { ( update ) => tutorialStore . setCurrentDocumentContent ( update . content ) }
142
+ />
143
+ </ Panel >
144
+ ) ;
145
+ }
103
146
104
- terminalExpanded . current = false ;
105
- }
106
- } , [ hideTerminalPanel ] ) ;
147
+ function Preview ( {
148
+ tutorialStore,
149
+ terminalPanelRef,
150
+ terminalExpanded,
151
+ hideTerminalPanel,
152
+ hasPreviews,
153
+ hasEditor,
154
+ } : TerminalProps ) {
155
+ const lesson = tutorialStore . lesson ! ;
107
156
108
- const showTerminal = useCallback ( ( ) => {
157
+ const toggleTerminal = useCallback ( ( ) => {
109
158
const { current : terminal } = terminalPanelRef ;
110
159
111
160
if ( ! terminal ) {
112
161
return ;
113
162
}
114
163
115
- if ( ! terminalExpanded . current ) {
116
- terminalExpanded . current = true ;
117
- terminal . resize ( DEFAULT_TERMINAL_SIZE ) ;
164
+ if ( terminalPanelRef . current ?. isCollapsed ( ) ) {
165
+ if ( ! terminalExpanded . current ) {
166
+ terminalExpanded . current = true ;
167
+ terminal . resize ( DEFAULT_TERMINAL_SIZE ) ;
168
+ } else {
169
+ terminal . expand ( ) ;
170
+ }
118
171
} else {
119
- terminal . expand ( ) ;
172
+ terminalPanelRef . current ?. collapse ( ) ;
120
173
}
121
174
} , [ ] ) ;
122
175
123
- const hideTerminal = useCallback ( ( ) => {
124
- terminalPanelRef . current ?. collapse ( ) ;
125
- } , [ ] ) ;
126
-
127
- const toggleTerminal = useCallback ( ( ) => {
128
- const { current : terminal } = terminalPanelRef ;
129
-
130
- if ( ! terminal ) {
131
- return ;
132
- }
176
+ useEffect ( ( ) => {
177
+ if ( hideTerminalPanel ) {
178
+ // force hide the terminal if we don't have any panels to show
179
+ terminalPanelRef . current ?. collapse ( ) ;
133
180
134
- if ( terminalPanelRef . current ?. isCollapsed ( ) ) {
135
- showTerminal ( ) ;
136
- } else {
137
- hideTerminal ( ) ;
181
+ terminalExpanded . current = false ;
138
182
}
139
- } , [ ] ) ;
183
+ } , [ hideTerminalPanel ] ) ;
140
184
141
185
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 = { onHelpClick }
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 }
186
+ < Panel
187
+ id = { hasPreviews ? 'previews-opened' : 'previews-closed' }
188
+ defaultSize = { hasPreviews ? 50 : 0 }
189
+ minSize = { 10 }
190
+ maxSize = { hasPreviews ? 100 : 0 }
191
+ collapsible = { ! hasPreviews }
192
+ className = { classNames ( {
193
+ 'transition-theme border-t border-tk-elements-app-borderColor' : hasEditor ,
194
+ } ) }
195
+ >
196
+ < PreviewPanel
197
+ tutorialStore = { tutorialStore }
198
+ i18n = { lesson . data . i18n as I18n }
199
+ showToggleTerminal = { ! hideTerminalPanel }
200
+ toggleTerminal = { toggleTerminal }
172
201
/>
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 }
196
- />
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 >
202
+ </ Panel >
203
+ ) ;
204
+ }
205
+
206
+ function Terminal ( {
207
+ tutorialStore,
208
+ theme,
209
+ terminalPanelRef,
210
+ terminalExpanded,
211
+ hideTerminalPanel,
212
+ hasEditor,
213
+ hasPreviews,
214
+ } : TerminalProps ) {
215
+ let id = 'terminal-closed' ;
216
+
217
+ if ( hideTerminalPanel ) {
218
+ id = 'terminal-none' ;
219
+ } else if ( ! hasPreviews && ! hasEditor ) {
220
+ id = 'terminal-full' ;
221
+ } else if ( ! hasPreviews ) {
222
+ id = 'terminal-opened' ;
223
+ }
224
+
225
+ let defaultSize = 0 ;
226
+
227
+ if ( hideTerminalPanel ) {
228
+ defaultSize = 0 ;
229
+ } else if ( ! hasPreviews && ! hasEditor ) {
230
+ defaultSize = 100 ;
231
+ } else if ( ! hasPreviews ) {
232
+ defaultSize = DEFAULT_TERMINAL_SIZE ;
233
+ }
234
+
235
+ return (
236
+ < Panel
237
+ id = { id }
238
+ defaultSize = { defaultSize }
239
+ minSize = { hideTerminalPanel ? 0 : 10 }
240
+ collapsible = { hasPreviews }
241
+ ref = { terminalPanelRef }
242
+ onExpand = { ( ) => {
243
+ terminalExpanded . current = true ;
244
+ } }
245
+ className = { classNames ( 'transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor' , {
246
+ 'border-t border-tk-elements-app-borderColor' : hasPreviews ,
247
+ } ) }
248
+ >
249
+ < TerminalPanel tutorialStore = { tutorialStore } theme = { theme } />
250
+ </ Panel >
226
251
) ;
227
252
}
0 commit comments