From e274f9ed0b4d2a6cf67a7a8f6e59b2b0f886ffd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:04:55 +0000 Subject: [PATCH 01/12] docs(layout): add JSDoc for kt.sidebar nesting behavior Add @remarks section documenting: - Nested call support - Sidebar buffer output behavior - Exception safety via try/finally --- src/kt/layout.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/kt/layout.ts b/src/kt/layout.ts index 4a9e28b8..a796eef4 100644 --- a/src/kt/layout.ts +++ b/src/kt/layout.ts @@ -253,20 +253,20 @@ export interface SidebarConfig { * @param content - サイドバー内に表示するコンテンツ * @param config - オプション設定 * + * @remarks + * - ネスト呼び出しがサポートされています + * - 内側の `kt.sidebar()` 内のコンテンツもサイドバーバッファに出力されます + * - コールバック内で例外が発生しても、ターゲットは `try/finally` により正しく復元されます + * * @example * ```typescript * kt.sidebar(() => { * kt.title("Settings"); - * const theme = kt.selectbox("Theme", ["Light", "Dark"]); - * kt.divider(); - * if (kt.button("Reset")) { - * // リセット処理 - * } + * kt.write("Sidebar content"); * }); * * // メインコンテンツ - * kt.title("Main Content"); - * kt.write("This is the main area."); + * kt.title("Main"); * ``` */ export function sidebar(content: () => void, _config?: SidebarConfig): void { From 941059fd01473184b2f81c18d44743a661625768 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:06:16 +0000 Subject: [PATCH 02/12] docs(sidebar): document nested kt.sidebar() call behavior Add comprehensive documentation for nested sidebar calls: - Supported patterns and output behavior - Implementation details with code examples - Non-recommended patterns for code clarity - Error handling guarantees with try/finally --- docs/impl/sidebar-design.md | 89 +++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/docs/impl/sidebar-design.md b/docs/impl/sidebar-design.md index c3024cf1..e8501b8a 100644 --- a/docs/impl/sidebar-design.md +++ b/docs/impl/sidebar-design.md @@ -470,6 +470,95 @@ export interface PageConfig { --- +## ネスト呼び出しの挙動 + +### サポートされるパターン + +`kt.sidebar()` はネストして呼び出すことができます。内側の `kt.sidebar()` 呼び出し内のコンテンツも、すべてサイドバーバッファに出力されます。 + +```typescript +kt.sidebar(() => { + kt.write("Level 1"); + + kt.sidebar(() => { + kt.write("Level 2 - still in sidebar"); + }); + + kt.write("Back to Level 1 - still in sidebar"); +}); + +kt.write("Main content"); +``` + +**出力結果:** +- サイドバー: "Level 1", "Level 2 - still in sidebar", "Back to Level 1 - still in sidebar" +- メイン: "Main content" + +### 実装詳細 + +`kt.sidebar()` は内部で以下の処理を行います: + +1. 現在のターゲット(`main` または `sidebar`)を保存 +2. ターゲットを `sidebar` に切り替え +3. コールバックを実行 +4. ターゲットを元に戻す(`try/finally` で保証) + +```typescript +export function sidebar(content: () => void, config?: SidebarConfig): void { + const ctx = requireRenderContext(); + const previousTarget = ctx.getTarget(); // "main" or "sidebar" + ctx.setTarget("sidebar"); + + try { + content(); + } finally { + ctx.setTarget(previousTarget); // 必ず復元 + } +} +``` + +ネスト呼び出し時: +- 外側の `sidebar()` で `previousTarget = "main"`、`currentTarget = "sidebar"` +- 内側の `sidebar()` で `previousTarget = "sidebar"`、`currentTarget = "sidebar"` +- 内側終了時に `currentTarget = "sidebar"`(変わらず) +- 外側終了時に `currentTarget = "main"`(復元) + +### 非推奨パターン + +以下のパターンは技術的には動作しますが、コードの可読性のため推奨しません: + +```typescript +// 非推奨: 深いネスト +kt.sidebar(() => { + kt.sidebar(() => { + kt.sidebar(() => { + kt.write("Deeply nested"); + }); + }); +}); + +// 推奨: フラットな構造 +kt.sidebar(() => { + kt.write("All sidebar content here"); +}); +``` + +### エラーハンドリング + +コールバック内で例外が発生しても、ターゲットは正しく復元されます: + +```typescript +kt.sidebar(() => { + kt.write("Before error"); + throw new Error("Something went wrong"); + kt.write("After error"); // 実行されない +}); + +kt.write("This goes to main"); // ターゲットは正しくmainに戻っている +``` + +--- + ## アーキテクチャ設計 ### 現状の問題点 From 5d5da32d2a90f1b3f98f33ad91c9eff9fafe9ae1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:07:12 +0000 Subject: [PATCH 03/12] test(e2e): add mobile viewport tests for sidebar Add E2E tests for mobile responsive behavior: - Verify fixed positioning on 375x667 viewport - Verify toggle button size >= 40px on mobile --- e2e/layout.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/e2e/layout.spec.ts b/e2e/layout.spec.ts index f12ac50e..3c536257 100644 --- a/e2e/layout.spec.ts +++ b/e2e/layout.spec.ts @@ -204,3 +204,21 @@ test.describe("Layout API - Expander", () => { await expect(content).toContainText("This content is visible by default"); }); }); + +test.describe("Layout API - Sidebar (Mobile)", () => { + test.use({ viewport: { width: 375, height: 667 } }); + + test("sidebar uses fixed positioning on mobile", async ({ page }) => { + await gotoAndWait(page); + const sidebar = page.locator(".kt-sidebar"); + await expect(sidebar).toHaveCSS("position", "fixed"); + }); + + test("sidebar toggle button is larger on mobile", async ({ page }) => { + await gotoAndWait(page); + const toggle = page.locator(".kt-sidebar-toggle"); + const box = await toggle.boundingBox(); + expect(box?.width).toBeGreaterThanOrEqual(40); + expect(box?.height).toBeGreaterThanOrEqual(40); + }); +}); From 24587e276b24e3181f5bf5005afdaa6e002d6bfe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:08:05 +0000 Subject: [PATCH 04/12] test(e2e): add mobile overlay interaction tests for sidebar Add E2E tests for mobile overlay behavior: - Verify overlay appears when sidebar is expanded - Verify clicking overlay closes the sidebar --- e2e/layout.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/e2e/layout.spec.ts b/e2e/layout.spec.ts index 3c536257..4a5b5a1f 100644 --- a/e2e/layout.spec.ts +++ b/e2e/layout.spec.ts @@ -221,4 +221,22 @@ test.describe("Layout API - Sidebar (Mobile)", () => { expect(box?.width).toBeGreaterThanOrEqual(40); expect(box?.height).toBeGreaterThanOrEqual(40); }); + + test("sidebar overlay appears when expanded on mobile", async ({ page }) => { + await gotoAndWait(page); + const overlay = page.locator(".kt-sidebar-overlay"); + const sidebar = page.locator(".kt-sidebar"); + + await expect(sidebar).toHaveAttribute("data-state", "expanded"); + await expect(overlay).toBeVisible(); + }); + + test("clicking overlay closes sidebar on mobile", async ({ page }) => { + await gotoAndWait(page); + const overlay = page.locator(".kt-sidebar-overlay"); + const sidebar = page.locator(".kt-sidebar"); + + await overlay.click(); + await expect(sidebar).toHaveAttribute("data-state", "collapsed"); + }); }); From 958e36d7d5ff342cf78d53f32cf01017cc2d2bf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:08:42 +0000 Subject: [PATCH 05/12] test(e2e): add tablet and desktop viewport tests for sidebar Add E2E tests for tablet and desktop breakpoints: - Tablet (768px): verify fixed positioning at boundary - Desktop (1280px): verify relative positioning - Desktop: verify overlay is hidden --- e2e/layout.spec.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/e2e/layout.spec.ts b/e2e/layout.spec.ts index 4a5b5a1f..e489545c 100644 --- a/e2e/layout.spec.ts +++ b/e2e/layout.spec.ts @@ -240,3 +240,29 @@ test.describe("Layout API - Sidebar (Mobile)", () => { await expect(sidebar).toHaveAttribute("data-state", "collapsed"); }); }); + +test.describe("Layout API - Sidebar (Tablet 768px)", () => { + test.use({ viewport: { width: 768, height: 1024 } }); + + test("sidebar uses fixed positioning at exactly 768px", async ({ page }) => { + await gotoAndWait(page); + const sidebar = page.locator(".kt-sidebar"); + await expect(sidebar).toHaveCSS("position", "fixed"); + }); +}); + +test.describe("Layout API - Sidebar (Desktop)", () => { + test.use({ viewport: { width: 1280, height: 800 } }); + + test("sidebar uses relative positioning on desktop", async ({ page }) => { + await gotoAndWait(page); + const sidebar = page.locator(".kt-sidebar"); + await expect(sidebar).toHaveCSS("position", "relative"); + }); + + test("sidebar overlay is hidden on desktop", async ({ page }) => { + await gotoAndWait(page); + const overlay = page.locator(".kt-sidebar-overlay"); + await expect(overlay).not.toBeVisible(); + }); +}); From 5a6aa4ab8753ef0108f6d30a045606dcdcac0238 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:09:50 +0000 Subject: [PATCH 06/12] feat(context): add sidebarWidth getter/setter to RenderContext - Add sidebarWidth private property with default "280px" - Add setSidebarWidth() and getSidebarWidth() methods - Reset sidebarWidth in clear() method - Add unit tests for sidebarWidth functionality --- src/kt/context.ts | 16 ++++++++++++++++ tests/unit/kt/context.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/kt/context.ts b/src/kt/context.ts index 6ac94d5d..1d684ea1 100644 --- a/src/kt/context.ts +++ b/src/kt/context.ts @@ -16,6 +16,7 @@ export class RenderContext { private flushCallback: FlushCallback | null = null; private flushThreshold = 0; // 0 = 無効(フラッシュしない) private flushedCount = 0; + private sidebarWidth = "280px"; /** * フラッシュコールバックを設定 @@ -108,6 +109,20 @@ export class RenderContext { return this.sidebarBuffer.length > 0; } + /** + * サイドバーの幅を設定 + */ + setSidebarWidth(width: string): void { + this.sidebarWidth = width; + } + + /** + * サイドバーの幅を取得 + */ + getSidebarWidth(): string { + return this.sidebarWidth; + } + /** * バッファを結合してHTMLを取得(後方互換性のため維持、メインのみ返す) */ @@ -123,6 +138,7 @@ export class RenderContext { this.sidebarBuffer = []; this.currentTarget = "main"; this.flushedCount = 0; + this.sidebarWidth = "280px"; } /** diff --git a/tests/unit/kt/context.test.ts b/tests/unit/kt/context.test.ts index 5b280d12..81b54bf7 100644 --- a/tests/unit/kt/context.test.ts +++ b/tests/unit/kt/context.test.ts @@ -317,3 +317,26 @@ describe("RenderContext dual buffer", () => { expect(ctx.hasSidebar()).toBe(false); }); }); + +describe("RenderContext sidebarWidth", () => { + let ctx: RenderContext; + + beforeEach(() => { + ctx = new RenderContext(); + }); + + it("should have default sidebar width of 280px", () => { + expect(ctx.getSidebarWidth()).toBe("280px"); + }); + + it("should set custom sidebar width", () => { + ctx.setSidebarWidth("350px"); + expect(ctx.getSidebarWidth()).toBe("350px"); + }); + + it("should reset sidebar width on clear", () => { + ctx.setSidebarWidth("400px"); + ctx.clear(); + expect(ctx.getSidebarWidth()).toBe("280px"); + }); +}); From 82aaa54b5b90613d22e845a5fca529d2b3489df2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:10:49 +0000 Subject: [PATCH 07/12] feat(sidebar): implement SidebarConfig.width option - Use config.width to set custom sidebar width via RenderContext - Add unit tests for width configuration --- src/kt/layout.ts | 7 ++++++- tests/unit/kt/layout.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/kt/layout.ts b/src/kt/layout.ts index a796eef4..804ed15d 100644 --- a/src/kt/layout.ts +++ b/src/kt/layout.ts @@ -269,9 +269,14 @@ export interface SidebarConfig { * kt.title("Main"); * ``` */ -export function sidebar(content: () => void, _config?: SidebarConfig): void { +export function sidebar(content: () => void, config?: SidebarConfig): void { const ctx = requireRenderContext(); + // 幅の設定(指定があれば更新) + if (config?.width) { + ctx.setSidebarWidth(config.width); + } + // ターゲットをサイドバーに切り替え const previousTarget = ctx.getTarget(); ctx.setTarget("sidebar"); diff --git a/tests/unit/kt/layout.test.ts b/tests/unit/kt/layout.test.ts index 7b50f9bc..7aefdd48 100644 --- a/tests/unit/kt/layout.test.ts +++ b/tests/unit/kt/layout.test.ts @@ -490,5 +490,35 @@ describe("Layout APIs", () => { }), ).toThrow("RenderContext is not available"); }); + + it("should set custom width via config", () => { + sidebar( + () => { + write("content"); + }, + { width: "350px" }, + ); + + expect(ctx.getSidebarWidth()).toBe("350px"); + }); + + it("should keep default width when not specified", () => { + sidebar(() => { + write("content"); + }); + + expect(ctx.getSidebarWidth()).toBe("280px"); + }); + + it("should keep default width when config is empty object", () => { + sidebar( + () => { + write("content"); + }, + {}, + ); + + expect(ctx.getSidebarWidth()).toBe("280px"); + }); }); }); From 66e0214a90435eb85fa429c000d5e6677f856a8b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:12:12 +0000 Subject: [PATCH 08/12] feat(rerun): include sidebarWidth in RerunResult - Add sidebarWidth property to RerunResult interface - Return sidebarWidth from rerun() function - Add unit tests for sidebarWidth functionality --- src/runtime/rerun.ts | 4 ++++ tests/unit/runtime/rerun.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/runtime/rerun.ts b/src/runtime/rerun.ts index 12c3cf3a..59e7057c 100644 --- a/src/runtime/rerun.ts +++ b/src/runtime/rerun.ts @@ -22,6 +22,8 @@ export interface RerunResult { sidebarHtml: string; /** サイドバーが使用されているか */ hasSidebar: boolean; + /** サイドバーの幅 */ + sidebarWidth: string; } /** @@ -98,6 +100,7 @@ export function rerun( mainHtml: result, sidebarHtml: "", hasSidebar: false, + sidebarWidth: "280px", }; } @@ -105,6 +108,7 @@ export function rerun( mainHtml: renderContext.getMainHtml(), sidebarHtml: renderContext.getSidebarHtml(), hasSidebar: renderContext.hasSidebar(), + sidebarWidth: renderContext.getSidebarWidth(), }; } finally { // コンテキストをクリア diff --git a/tests/unit/runtime/rerun.test.ts b/tests/unit/runtime/rerun.test.ts index d70e1f83..f71ec2ff 100644 --- a/tests/unit/runtime/rerun.test.ts +++ b/tests/unit/runtime/rerun.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "vitest"; +import { sidebar } from "../../../src/kt/layout"; +import { write } from "../../../src/kt/output"; import { AbortError } from "../../../src/runtime/abort"; import { rerun } from "../../../src/runtime/rerun"; @@ -56,3 +58,29 @@ describe("rerun with AbortSignal", () => { expect(result.mainHtml).toBe("
With context
"); }); }); + +describe("rerun sidebarWidth", () => { + it("should return default sidebarWidth", () => { + const result = rerun(() => { + write("test"); + }); + expect(result.sidebarWidth).toBe("280px"); + }); + + it("should return default sidebarWidth for string return type", () => { + const result = rerun(() => "
test
"); + expect(result.sidebarWidth).toBe("280px"); + }); + + it("should return custom sidebarWidth when set", () => { + const result = rerun(() => { + sidebar( + () => { + write("sidebar"); + }, + { width: "400px" }, + ); + }); + expect(result.sidebarWidth).toBe("400px"); + }); +}); From 50e59d12af82a6b26bc63a1464cbe37b4f68511c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 09:13:23 +0000 Subject: [PATCH 09/12] feat(sidebar): apply CSS variable for dynamic width - Add --kt-sidebar-width CSS variable to layout container - Update CSS to use CSS variable with 280px fallback - Enables custom sidebar width via kt.sidebar({ width: "350px" }) --- src/app.ts | 2 +- src/styles/default.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index 455fc205..d18638ad 100644 --- a/src/app.ts +++ b/src/app.ts @@ -135,7 +135,7 @@ export async function createApp(script: Script, options?: KantanAppOptions): Pro // サイドバーがある場合のHTML構造を生成 const bodyContent = initialResult.hasSidebar - ? html`
+ ? html`