From 203bab381b91be7a872c066f4c460b4ef3f84e59 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:09:27 +0900 Subject: [PATCH 01/15] =?UTF-8?q?=E3=82=A8=E3=83=87=E3=82=A3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=82=B5=E3=82=A4=E3=82=BA=E3=82=92max50%?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=80=81=E6=9C=80=E5=A4=A7=E5=8C=96=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=99=E3=82=8B=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/editor.tsx | 56 +++++++++++++----- app/terminal/modal.tsx | 125 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 app/terminal/modal.tsx diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 4def9eda..d257d54a 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -6,6 +6,7 @@ import { useChangeTheme } from "@/themeToggle"; import { useEmbedContext } from "./embedContext"; import { langConstants } from "./runtime"; import { MarkdownLang } from "@/[lang]/[pageId]/styledSyntaxHighlighter"; +import { MinMaxButton, Modal } from "./modal"; // https://github.com/securingsincity/react-ace/issues/27 により普通のimportができない const AceEditor = lazy(async () => { @@ -104,18 +105,42 @@ export function EditorComponent(props: EditorProps) { }, [files, props.filename, props.initContent, writeFile]); const [fontSize, setFontSize] = useState(); + const [windowHeight, setWindowHeight] = useState(1000); const [initAce, setInitAce] = useState(false); useEffect(() => { - setFontSize( - parseFloat(getComputedStyle(document.documentElement).fontSize) - ); // 1rem - setInitAce(true); + const update = () => { + setFontSize( + parseFloat(getComputedStyle(document.documentElement).fontSize) + ); // 1rem + setWindowHeight(window.innerHeight); + setInitAce(true); + }; + update(); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); }, []); - // 最小8行 or 初期内容+1行 - const editorHeight = Math.max(props.initContent.split("\n").length + 1, 8); + // 現在の内容の行数、最小8行、最大50vh + const editorHeight = Math.max( + Math.min( + code.split("\n").length, + Math.floor((windowHeight * 0.5) / ((fontSize || 16) + 1)) + ), + 8 + ); + + const [isModal, setIsModal] = useState(false); return ( -
+ setIsModal(false)} + >
@@ -128,8 +153,6 @@ export function EditorComponent(props: EditorProps) { +
+
{fontSize !== undefined && initAce ? ( ) : ( - {code} + + {code} + )} -
+
); } function FallbackPre({ children, editorHeight, + isModal, }: { children: string; editorHeight: number; + isModal?: boolean; }) { // AceEditorはなぜかline-heightが小さい // fontSize + 1px になるっぽい? @@ -201,7 +231,7 @@ function FallbackPre({
diff --git a/app/terminal/modal.tsx b/app/terminal/modal.tsx
new file mode 100644
index 00000000..c0b9057d
--- /dev/null
+++ b/app/terminal/modal.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import clsx from "clsx";
+import { ReactNode, useEffect, useState } from "react";
+
+interface Props {
+  className?: string;
+  classNameModal?: string;
+  classNameNonModal?: string;
+  open: boolean;
+  onClose: () => void;
+  children: ReactNode;
+}
+export function Modal(props: Props) {
+  const [daisyModalEnabled, setDaisyModalEnabled] = useState(false);
+  const [daisyModalOpen, setDaisyModalOpen] = useState(false);
+  useEffect(() => {
+    if (props.open) {
+      // daisyuiのmodalモードにする → modalを開くアニメーションをする
+      setDaisyModalEnabled(true);
+      requestAnimationFrame(() => setDaisyModalOpen(true));
+    } else {
+      // 逆順
+      setDaisyModalOpen(false);
+      // アニメーションが終わった後にmodalモードを解除する
+      const timeout = setTimeout(() => setDaisyModalEnabled(false), 300);
+      return () => clearTimeout(timeout);
+    }
+  }, [props.open]);
+
+  return (
+    <>
+      
+      
+
+ {props.children} +
+
+ +
+
+ + ); +} + +export function MinMaxButton(props: { + open: boolean; + setOpen: (open: boolean) => void; +}) { + return ( + + ); +} From 41a08a89b1e6cd0a7cf73adfe2e32d8e05ba605e Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:21:54 +0900 Subject: [PATCH 02/15] =?UTF-8?q?exec=E3=82=BF=E3=83=BC=E3=83=9F=E3=83=8A?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E6=9C=80=E5=A4=A7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/editor.tsx | 1 - app/terminal/exec.tsx | 57 +++++++++++++++++++++++++++++++++------ app/terminal/modal.tsx | 25 ++++++++--------- app/terminal/repl.tsx | 21 ++++++++++----- app/terminal/terminal.tsx | 43 ++++++++++++++++------------- 5 files changed, 101 insertions(+), 46 deletions(-) diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index d257d54a..ee722948 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -133,7 +133,6 @@ export function EditorComponent(props: EditorProps) { return ( (); + const [windowHeight, setWindowHeight] = useState(1000); + useEffect(() => { + const update = () => { + setFontSize( + parseFloat(getComputedStyle(document.documentElement).fontSize) + ); // 1rem + setWindowHeight(window.innerHeight); + }; + update(); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + }, []); + const getRows = useCallback( + (cols: number) => + isModal + ? "fit" + : Math.min( + calculateRows(contents, cols), + Math.floor((windowHeight * 0.2) / ((fontSize || 16) * 1.2)) + ), + [contents, isModal, fontSize, windowHeight] + ); const { terminalRef, terminalInstanceRef, termReady } = useTerminal({ - getRows: (cols: number) => getRows(props.content, cols) + 1, + getRows, onReady: () => { hideCursor(terminalInstanceRef.current!); for (const line of props.content.split("\n")) { @@ -32,7 +59,8 @@ export function ExecFile(props: ExecProps) { } }, }); - const { files, clearExecResult, addExecOutput, writeFile } = useEmbedContext(); + const { files, clearExecResult, addExecOutput, writeFile } = + useEmbedContext(); const { ready, runFiles, getCommandlineStr, runtimeInfo, interrupt } = useRuntime(props.language); @@ -50,6 +78,7 @@ export function ExecFile(props: ExecProps) { // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる const filenameKey = props.filenames.join(","); clearExecResult(filenameKey); + setContents(""); let isFirstOutput = true; await runFiles(props.filenames, files, (output) => { if (output.type === "file") { @@ -70,6 +99,7 @@ export function ExecFile(props: ExecProps) { null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない props.language ); + setContents((prev) => prev + output.message + "\n"); }); setExecutionState("idle"); })(); @@ -88,7 +118,15 @@ export function ExecFile(props: ExecProps) { ]); return ( -
+ setIsModal(false)} + >
+
+
-
+
{/* ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。 可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある @@ -176,7 +216,8 @@ export function ExecFile(props: ExecProps) { className={clsx( !termReady && /* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/ - "absolute invisible" + "absolute invisible", + "h-full" )} ref={terminalRef} /> @@ -184,6 +225,6 @@ export function ExecFile(props: ExecProps) {
)}
-
+ ); } diff --git a/app/terminal/modal.tsx b/app/terminal/modal.tsx index c0b9057d..39b7cc29 100644 --- a/app/terminal/modal.tsx +++ b/app/terminal/modal.tsx @@ -45,6 +45,7 @@ export function Modal(props: Props) { daisyModalEnabled ? clsx( "modal-box", + "max-w-300 p-0", "size-[calc(100%-1rem)]", "md:size-[calc(100%-2rem)]", "lg:size-[calc(100%-4rem)]", @@ -85,15 +86,15 @@ export function MinMaxButton(props: { > ) : ( @@ -107,15 +108,15 @@ export function MinMaxButton(props: { > )} diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index af245043..964ff60e 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -1,12 +1,12 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { highlightCodeToAnsi, importPrism } from "./highlight"; import chalk from "chalk"; chalk.level = 3; import { + calculateRows, clearTerminal, - getRows, hideCursor, showCursor, systemMessageColor, @@ -109,23 +109,30 @@ export function ReplTerminal({ throw new Error(`runCommand not available for language: ${language}`); } - const initCommand = splitReplExamples?.(initContent || ""); + const initCommand = useMemo( + () => splitReplExamples?.(initContent || ""), + [splitReplExamples, initContent] + ); - const { terminalRef, terminalInstanceRef, termReady } = useTerminal({ - getRows: (cols: number) => { + const getRows = useCallback( + (cols: number) => { let rows = 0; for (const cmd of initCommand || []) { // コマンドの行数をカウント for (const line of cmd.command.split("\n")) { - rows += getRows(prompt + line, cols); + rows += calculateRows(prompt + line, cols); } // 出力の行数をカウント for (const out of cmd.output) { - rows += getRows(out.message, cols); + rows += calculateRows(out.message, cols); } } return rows + 2; // 最後のプロンプト行を含める }, + [initCommand, prompt] + ); + const { terminalRef, terminalInstanceRef, termReady } = useTerminal({ + getRows, }); // REPLのユーザー入力 diff --git a/app/terminal/terminal.tsx b/app/terminal/terminal.tsx index 79d614d2..a0959841 100644 --- a/app/terminal/terminal.tsx +++ b/app/terminal/terminal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { Terminal } from "@xterm/xterm"; import type { FitAddon } from "@xterm/addon-fit"; import "@xterm/xterm/css/xterm.css"; @@ -31,7 +31,7 @@ export function strWidth(str: string): number { /** * contentsがちょうど収まるターミナルの高さを計算する */ -export function getRows(contents: string, cols: number): number { +export function calculateRows(contents: string, cols: number): number { return contents .split("\n") .reduce( @@ -85,7 +85,7 @@ function computeTerminalTheme() { } interface TerminalProps { - getRows?: (cols: number) => number; + getRows?: (cols: number) => number | "fit"; onReady?: () => void; } export function useTerminal(props: TerminalProps) { @@ -94,23 +94,34 @@ export function useTerminal(props: TerminalProps) { const fitAddonRef = useRef(null); const [termReady, setTermReady] = useState(false); const theme = useChangeTheme(); - const getRowsRef = useRef<(cols: number) => number>(undefined); - getRowsRef.current = props.getRows; const onReadyRef = useRef<() => void>(undefined); onReadyRef.current = props.onReady; + const { getRows } = props; + const resizeTerminal = useCallback(() => { + // fitAddon.fit(); + const dims = fitAddonRef.current?.proposeDimensions(); + if (dims && !isNaN(dims.cols)) { + let rows = getRows?.(dims.cols) ?? 0; + if (rows === "fit") { + rows = dims.rows; + } + console.log(dims) + terminalInstanceRef.current?.resize(dims.cols, Math.max(5, rows)); + } + }, [getRows]); + useEffect(() => { + const observer = new ResizeObserver(resizeTerminal); + observer.observe(terminalRef.current); + return () => observer.disconnect(); + }, [resizeTerminal]); + const resizeTerminalRef = useRef(resizeTerminal); + resizeTerminalRef.current = resizeTerminal; // ターミナルの初期化処理 + // 初期化が完了した瞬間にその時点のresizeTerminalとonReadyを呼び出す必要があるので、refに入れている useEffect(() => { if (typeof window !== "undefined") { const abortController = new AbortController(); - const resizeTerminal = () => { - // fitAddon.fit(); - const dims = fitAddonRef.current?.proposeDimensions(); - if (dims && !isNaN(dims.cols)) { - const rows = Math.max(5, getRowsRef.current?.(dims.cols) ?? 0); - terminalInstanceRef.current?.resize(dims.cols, rows); - } - } /* globals.cssでフォントを指定し読み込んでいるが、 それが読み込まれる前にterminalを初期化してしまうとバグるので、 @@ -142,7 +153,7 @@ export function useTerminal(props: TerminalProps) { fitAddonRef.current = new FitAddon(); term.loadAddon(fitAddonRef.current); // fitAddonRef.current.fit(); - resizeTerminal(); + resizeTerminalRef.current(); term.open(terminalRef.current); @@ -171,12 +182,8 @@ export function useTerminal(props: TerminalProps) { } }); - const observer = new ResizeObserver(resizeTerminal); - observer.observe(terminalRef.current); - return () => { abortController.abort("terminal component dismount"); - observer.disconnect(); if (fitAddonRef.current) { fitAddonRef.current.dispose(); fitAddonRef.current = null; From 19b00502f2c28e1209dd98fea9abb73307b9ffe2 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:46:57 +0900 Subject: [PATCH 03/15] =?UTF-8?q?repl=E3=81=AE=E6=9C=80=E5=A4=A7=E5=8C=96?= =?UTF-8?q?=E3=80=81=E5=81=9C=E6=AD=A2=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=AE?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/exec.tsx | 38 +++++++++++++++++- app/terminal/repl.tsx | 93 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 16 deletions(-) diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index fdbeb774..d32a4faf 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -133,7 +133,8 @@ export function ExecFile(props: ExecProps) { ここでは最小でそのサイズ、ただし親コンテナがそれより大きい場合に大きくしたい → heightを解除し、min-heightをデフォルトのサイズと同じにする */ className={clsx( - "btn btn-soft btn-accent h-[unset]! min-h-(--size) self-stretch", + "btn btn-soft h-[unset]! min-h-(--size) self-stretch", + executionState === "idle" ? "btn-accent" : "btn-error", "rounded-none rounded-tl-[calc(var(--radius-box)-2px)]" )} onClick={() => { @@ -162,7 +163,11 @@ export function ExecFile(props: ExecProps) { ) } > - {executionState === "idle" ? "▶ 実行" : "■ 停止"} + {executionState === "idle" ? ( + + ) : ( + + )} {getCommandlineStr?.(props.filenames)} @@ -228,3 +233,32 @@ export function ExecFile(props: ExecProps) { ); } + +export function StartButtonContent() { + return "▶ 実行"; +} +export function StopButtonContent() { + /**/ + return ( + <> + + + + + 停止 + + ); +} diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index 964ff60e..c1c14043 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -17,6 +17,8 @@ import { useEmbedContext } from "./embedContext"; import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime"; import clsx from "clsx"; import { InlineCode } from "@/[lang]/[pageId]/markdown"; +import { MinMaxButton, Modal } from "./modal"; +import { StopButtonContent } from "./exec"; export type ReplOutputType = | "stdout" @@ -114,6 +116,25 @@ export function ReplTerminal({ [splitReplExamples, initContent] ); + // REPLのユーザー入力 + const inputBuffer = useRef([]); + + // ターミナルの行数を計算するためのstate + const [newContents, setNewContents] = useState("\n"); + const [isModal, setIsModal] = useState(false); + const [fontSize, setFontSize] = useState(); + const [windowHeight, setWindowHeight] = useState(1000); + useEffect(() => { + const update = () => { + setFontSize( + parseFloat(getComputedStyle(document.documentElement).fontSize) + ); // 1rem + setWindowHeight(window.innerHeight); + }; + update(); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + }, []); const getRows = useCallback( (cols: number) => { let rows = 0; @@ -127,17 +148,30 @@ export function ReplTerminal({ rows += calculateRows(out.message, cols); } } - return rows + 2; // 最後のプロンプト行を含める + for (const line of newContents.trim().split("\n")) { + rows += calculateRows(line, cols); + } + // 最後のプロンプト行を含める + rows += Math.max( + 2, + inputBuffer.current.reduce( + (sum, line) => sum + calculateRows(prompt + line, cols), + 0 + ) + ); + return isModal + ? "fit" + : Math.min( + rows, + Math.floor((windowHeight * 0.5) / ((fontSize || 16) * 1.2)) + ); }, - [initCommand, prompt] + [initCommand, prompt, newContents, fontSize, isModal, windowHeight] ); const { terminalRef, terminalInstanceRef, termReady } = useTerminal({ getRows, }); - // REPLのユーザー入力 - const inputBuffer = useRef([]); - const [executionState, setExecutionState] = useState<"idle" | "executing">( "idle" ); @@ -231,6 +265,7 @@ export function ReplTerminal({ terminalInstanceRef.current.writeln(""); const command = inputBuffer.current.join("\n").trim(); inputBuffer.current = []; + setNewContents((contents) => contents + prompt + command + "\n"); const commandId = addReplCommand(terminalId, command); setExecutionState("executing"); let executionDone = false; @@ -248,6 +283,9 @@ export function ReplTerminal({ } else { handleOutput(output); } + setNewContents( + (contents) => contents + output.message + "\n" + ); addReplOutput(terminalId, commandId, output); }); }); @@ -302,6 +340,7 @@ export function ReplTerminal({ addReplOutput, terminalId, terminalInstanceRef, + prompt, ] ); useEffect(() => { @@ -402,16 +441,39 @@ export function ReplTerminal({ Prism, ]); + const [showStopButton, setShowStopButton] = useState(false); + useEffect(() => { + if ( + !termReady || + initCommandState !== "done" || + executionState !== "executing" + ) { + setShowStopButton(false); + } else { + // 一瞬で実行が完了するなら表示しない + const timeout = setTimeout(() => setShowStopButton(true), 300); + return () => clearTimeout(timeout); + } + }, [termReady, initCommandState, executionState]); return ( -
+ setIsModal(false)} + >
{runtimeInfo?.prettyLangName || language} 実行環境 @@ -459,12 +521,14 @@ export function ReplTerminal({ ?
+
+
{/* ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。 可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある */} -
+
       
-
+
); } From f8c738662713c09c9e61e55f06dc15e101fd4a6c Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:56:29 +0900 Subject: [PATCH 04/15] =?UTF-8?q?=E7=94=BB=E9=9D=A2=E5=B9=85=E3=81=8C100vw?= =?UTF-8?q?=E3=82=92=E8=B6=85=E3=81=88=E3=82=8B=E3=83=90=E3=82=B0=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E3=80=81android=E3=81=A7100dvh=E3=81=8C?= =?UTF-8?q?=E3=82=AD=E3=83=BC=E3=83=9C=E3=83=BC=E3=83=89=E3=82=92=E9=99=A4?= =?UTF-8?q?=E3=81=84=E3=81=9F=E9=AB=98=E3=81=95=E3=81=AB=E3=81=AA=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86viewport=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lang]/[pageId]/styledSyntaxHighlighter.tsx | 8 +++++++- app/layout.tsx | 8 +++++++- app/terminal/exec.tsx | 9 +++++---- app/terminal/modal.tsx | 2 +- app/terminal/repl.tsx | 9 +++++---- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/[lang]/[pageId]/styledSyntaxHighlighter.tsx b/app/[lang]/[pageId]/styledSyntaxHighlighter.tsx index 95dcfe5d..663c29af 100644 --- a/app/[lang]/[pageId]/styledSyntaxHighlighter.tsx +++ b/app/[lang]/[pageId]/styledSyntaxHighlighter.tsx @@ -6,6 +6,7 @@ import { tomorrowNight, } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { lazy, Suspense, useEffect, useState } from "react"; +import clsx from "clsx"; // SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する const SyntaxHighlighter = lazy(() => { @@ -136,7 +137,12 @@ export function StyledSyntaxHighlighter(props: { } function FallbackPre({ children }: { children: string }) { return ( -
+    
       {children}
     
); diff --git a/app/layout.tsx b/app/layout.tsx index 8139b106..24a17c5e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import "@fontsource-variable/inconsolata"; // import "@fontsource/m-plus-rounded-1c/400.css"; // import "@fontsource/m-plus-rounded-1c/700.css"; @@ -14,6 +14,12 @@ import { SidebarMdProvider } from "./sidebar"; import { RuntimeProvider } from "./terminal/runtime"; import { getPagesList } from "@/lib/docs"; +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + interactiveWidget: "resizes-content", +}; + export const metadata: Metadata = { title: { template: "%s - my.code();", diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index d32a4faf..05002750 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -127,7 +127,7 @@ export function ExecFile(props: ExecProps) { open={isModal} onClose={() => setIsModal(false)} > -
+
-
+
{/* ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。 可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある */}
diff --git a/app/terminal/modal.tsx b/app/terminal/modal.tsx
index 39b7cc29..88e35460 100644
--- a/app/terminal/modal.tsx
+++ b/app/terminal/modal.tsx
@@ -37,7 +37,7 @@ export function Modal(props: Props) {
         readOnly
       />
       
setIsModal(false)} > -
+
diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 79899982..f274e672 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -119,16 +119,11 @@ export function ExecFile(props: ExecProps) { return ( setIsModal(false)} > -
+
- +
{fontSize !== undefined && initAce ? ( setIsModal(false)} + setOpen={setIsModal} >
- +
{/* diff --git a/app/terminal/modal.tsx b/app/terminal/modal.tsx index 90357ab1..34f65be7 100644 --- a/app/terminal/modal.tsx +++ b/app/terminal/modal.tsx @@ -1,35 +1,41 @@ "use client"; import clsx from "clsx"; -import { ReactNode, useEffect, useRef, useState } from "react"; +import { ReactNode, useEffect, useRef } from "react"; + +/* +https://daisyui.com/components/modal/ の3の方法を採用している。 +ただし、modalのコンテナ自体を条件付きでレンダリングするので、aタグでの開閉だけでなくstateが必要になる。 +urlハッシュとstateを両方同時にtrueにしてもモーダルを開くアニメーションはするっぽい +*/ interface Props { + id: string; className?: string; classNameModal?: string; classNameNonModal?: string; open: boolean; - onClose: () => void; + setOpen: (open: boolean) => void; children: ReactNode; } export function Modal(props: Props) { - const [daisyModalEnabled, setDaisyModalEnabled] = useState(false); - const [daisyModalOpen, setDaisyModalOpen] = useState(false); const modalDivRef = useRef(null); + const { id, open, setOpen } = props; useEffect(() => { - if (props.open) { - // daisyuiのmodalモードにする → modalを開くアニメーションをする - setDaisyModalEnabled(true); - requestAnimationFrame(() => setDaisyModalOpen(true)); - } else { - // 逆順 - setDaisyModalOpen(false); - // アニメーションが終わった後にmodalモードを解除する - const timeout = setTimeout(() => setDaisyModalEnabled(false), 300); - return () => clearTimeout(timeout); - } - }, [props.open]); + const onHashChange = () => { + if (location.hash === `#${id}`) { + setOpen(true); + } else { + // アニメーションが終わった後にmodalモードを解除する + setTimeout(() => setOpen(false), 300); + } + }; + onHashChange(); + window.addEventListener("hashchange", onHashChange); + return () => window.removeEventListener("hashchange", onHashChange); + }, [id, setOpen]); useEffect(() => { - if (daisyModalEnabled) { + if (open) { const updateHeight = () => { if (modalDivRef.current && window.visualViewport) { modalDivRef.current.style.height = `${window.visualViewport.height}px`; @@ -44,61 +50,58 @@ export function Modal(props: Props) { modalDivRef.current.style.height = ""; } } - }, [daisyModalEnabled]); + }, [open]); return ( - <> - +
-
- {props.children} -
-
- -
+ {props.children} +
+
+
- +
); } -export function MinMaxButton(props: { - open: boolean; - setOpen: (open: boolean) => void; -}) { +export function MinMaxButton(props: { open: boolean; id: string }) { return (
- +
{/* ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。 From 4363c86b0b8dba78dd0e5ac7e173fc47fffcbecd Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:04:13 +0900 Subject: [PATCH 12/15] =?UTF-8?q?repl=E3=81=AE1=E8=A1=8C=E7=9B=AE=E3=81=8C?= =?UTF-8?q?=E6=B6=88=E3=81=88=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lang]/[pageId]/pageContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[lang]/[pageId]/pageContent.tsx b/app/[lang]/[pageId]/pageContent.tsx index fd8ac655..ac91c2bd 100644 --- a/app/[lang]/[pageId]/pageContent.tsx +++ b/app/[lang]/[pageId]/pageContent.tsx @@ -107,7 +107,7 @@ export function PageContent(props: PageContentProps) {
From 89fc5524c4f0f1272a4a701db43b5e6e34aaf28a Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:06:31 +0900 Subject: [PATCH 13/15] =?UTF-8?q?terminal=E3=81=AE=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E3=81=AAlog=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/terminal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/terminal/terminal.tsx b/app/terminal/terminal.tsx index 96971c04..a7a790ba 100644 --- a/app/terminal/terminal.tsx +++ b/app/terminal/terminal.tsx @@ -105,7 +105,6 @@ export function useTerminal(props: TerminalProps) { if (rows === "fit") { rows = dims.rows; } - console.log(dims) terminalInstanceRef.current?.resize(dims.cols, Math.max(5, rows)); } }, [getRows]); From a163f3a60a409023f342f475545be2896f6d8afd Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:08:10 +0900 Subject: [PATCH 14/15] =?UTF-8?q?repl=E3=81=AE=E5=88=9D=E6=9C=9F=E5=8C=96?= =?UTF-8?q?=E3=81=8C=E5=AE=8C=E4=BA=86=E3=81=97=E3=81=9F=E3=82=89focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/repl.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index c1f40c0c..3f419350 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -425,6 +425,7 @@ export function ReplTerminal({ // なぜかそのままscrollToTop()を呼ぶとスクロールせず、setTimeoutを入れるとscrollする(治安bad) setTimeout(() => terminalInstanceRef.current!.scrollToTop()); setInitCommandState("done"); + terminalInstanceRef.current?.focus(); })(); } }, [ @@ -458,11 +459,7 @@ export function ReplTerminal({ return ( From 09251bd43c5368be90b1297e68f71aa010878778 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:09:21 +0900 Subject: [PATCH 15/15] =?UTF-8?q?=E5=AE=9F=E8=A1=8C=E5=AE=8C=E4=BA=86?= =?UTF-8?q?=E6=99=82=E3=81=AB=E5=AE=9F=E8=A1=8C=E4=B8=AD=E3=81=A7=E3=81=99?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/exec.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index c1849d0f..4b01e724 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -102,6 +102,10 @@ export function ExecFile(props: ExecProps) { setContents((prev) => prev + output.message + "\n"); }); setExecutionState("idle"); + if (isFirstOutput) { + // If there was no output, clear the "実行中です..." message + clearTerminal(terminalInstanceRef.current!); + } })(); } }, [