@@ -38,6 +38,11 @@ export function SandboxContent(props: SandboxContentProps) {
3838 const [ userFiles , setUserFiles ] = useState < string [ ] > ( [ ] ) ;
3939 const [ newFilename , setNewFilename ] = useState ( "" ) ;
4040 const [ filenameError , setFilenameError ] = useState < string | null > ( null ) ;
41+
42+ const [ userOutputFiles , setUserOutputFiles ] = useState < string [ ] > ( [ ] ) ;
43+ const [ newOutputFilename , setNewOutputFilename ] = useState ( "" ) ;
44+ const [ outputFilenameError , setOutputFilenameError ] = useState < string | null > ( null ) ;
45+
4146 const [ isFormVisible , setIsFormVisible ] = useState ( false ) ;
4247
4348 // サイドバーの目次用セクション定義
@@ -52,7 +57,10 @@ export function SandboxContent(props: SandboxContentProps) {
5257 level : 2 ,
5358 } ) ;
5459 list . push ( { id : "sandbox-exec" as SectionId , title : "実行" , level : 2 } ) ;
55- if ( language . readonlyFiles && language . readonlyFiles . length > 0 ) {
60+ if (
61+ ( language . readonlyFiles && language . readonlyFiles . length > 0 ) ||
62+ language . supportsFileOutput
63+ ) {
5664 list . push ( {
5765 id : "sandbox-readonly" as SectionId ,
5866 title : "出力ファイル" ,
@@ -119,7 +127,8 @@ export function SandboxContent(props: SandboxContentProps) {
119127 if (
120128 defaultFiles . includes ( name ) ||
121129 readonlyFiles . includes ( name ) ||
122- userFiles . includes ( name )
130+ userFiles . includes ( name ) ||
131+ userOutputFiles . includes ( name )
123132 ) {
124133 setFilenameError ( "同名のファイルがすでに存在します。" ) ;
125134 return ;
@@ -135,6 +144,36 @@ export function SandboxContent(props: SandboxContentProps) {
135144 setUserFiles ( ( prev ) => prev . filter ( ( f ) => f !== filename ) ) ;
136145 } ;
137146
147+ const handleAddOutputFile = ( e : FormEvent ) => {
148+ e . preventDefault ( ) ;
149+ const name = newOutputFilename . trim ( ) ;
150+ if ( ! name ) return ;
151+
152+ // 既存ファイルチェック
153+ const defaultFiles = language . sampleFiles
154+ ? Object . keys ( language . sampleFiles )
155+ : [ ] ;
156+ const readonlyFiles = language . readonlyFiles ?? [ ] ;
157+ if (
158+ defaultFiles . includes ( name ) ||
159+ readonlyFiles . includes ( name ) ||
160+ userFiles . includes ( name ) ||
161+ userOutputFiles . includes ( name )
162+ ) {
163+ setOutputFilenameError ( "同名のファイルがすでに存在します。" ) ;
164+ return ;
165+ }
166+
167+ setOutputFilenameError ( null ) ;
168+ setUserOutputFiles ( ( prev ) => [ ...prev , name ] ) ;
169+ writeFile ( { [ name ] : "" } ) ;
170+ setNewOutputFilename ( "" ) ;
171+ } ;
172+
173+ const handleRemoveOutputFile = ( filename : string ) => {
174+ setUserOutputFiles ( ( prev ) => prev . filter ( ( f ) => f !== filename ) ) ;
175+ } ;
176+
138177 return (
139178 < div className = "flex-1 p-4 pb-16 flex flex-col max-w-docs mx-auto w-full" >
140179 < Heading level = { 1 } > { langEntry ?. name ?? langId } Sandbox</ Heading >
@@ -298,23 +337,64 @@ export function SandboxContent(props: SandboxContentProps) {
298337 </ section >
299338
300339 { /* 4. 出力ファイル */ }
301- { language . readonlyFiles && language . readonlyFiles . length > 0 && (
340+ { ( ( language . readonlyFiles && language . readonlyFiles . length > 0 ) ||
341+ language . supportsFileOutput ) && (
302342 < section
303343 id = "sandbox-readonly"
304344 ref = { ( el ) => {
305345 sectionRefs . current . set ( "sandbox-readonly" , el ) ;
306346 } }
307347 >
308348 < Heading level = { 2 } > 出力ファイル</ Heading >
309- { language . readonlyFiles . map ( ( filename ) => (
349+ { language . readonlyFiles &&
350+ language . readonlyFiles . map ( ( filename ) => (
351+ < EditorComponent
352+ key = { filename }
353+ language = { langConstants ( runtimeLang ) }
354+ filename = { filename }
355+ initContent = ""
356+ readonly
357+ />
358+ ) ) }
359+ { userOutputFiles . map ( ( filename ) => (
310360 < EditorComponent
311361 key = { filename }
312362 language = { langConstants ( runtimeLang ) }
313363 filename = { filename }
314364 initContent = ""
315365 readonly
366+ onDelete = { ( ) => handleRemoveOutputFile ( filename ) }
316367 />
317368 ) ) }
369+
370+ { language . supportsFileOutput && (
371+ < div className = "mx-2 my-2 mt-4" >
372+ < form
373+ onSubmit = { handleAddOutputFile }
374+ className = "flex items-center gap-2"
375+ >
376+ 出力ファイルを追加:
377+ < input
378+ type = "text"
379+ className = "input input-bordered input-sm flex-1 font-mono"
380+ placeholder = "追加するファイル名を入力"
381+ value = { newOutputFilename }
382+ onChange = { ( e ) => {
383+ setNewOutputFilename ( e . target . value ) ;
384+ setOutputFilenameError ( null ) ;
385+ } }
386+ />
387+ < button type = "submit" className = "btn btn-sm btn-accent" >
388+ 出力ファイルを追加
389+ </ button >
390+ </ form >
391+ { outputFilenameError && (
392+ < p className = "text-error text-sm mt-1" >
393+ { outputFilenameError }
394+ </ p >
395+ ) }
396+ </ div >
397+ ) }
318398 </ section >
319399 ) }
320400
0 commit comments