@ripla/godd-mcp 1.0.7-canary.8 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +523 -124
  2. package/dist/godd.cjs +293 -244
  3. package/dist/godd.js +1005 -321
  4. package/dist/index.js +148 -119
  5. package/notes/app/DESIGN.md +5 -2
  6. package/notes/app/e2e/fixtures.ts +39 -0
  7. package/notes/app/e2e/ja-typography-visual.spec.ts +185 -0
  8. package/notes/app/e2e/ja-typography-visual.spec.ts-snapshots/ja-typography-desktop-1280-chromium-linux.png +0 -0
  9. package/notes/app/e2e/ja-typography-visual.spec.ts-snapshots/ja-typography-mobile-360-chromium-linux.png +0 -0
  10. package/notes/app/e2e/rtl-logical-properties.spec.ts +36 -0
  11. package/notes/app/package-lock.json +0 -8
  12. package/notes/app/package.json +0 -1
  13. package/notes/app/playwright.config.ts +7 -0
  14. package/notes/app/pnpm-lock.yaml +0 -8
  15. package/notes/app/src/components/BranchSelectorModal.tsx +8 -8
  16. package/notes/app/src/components/CommentPanel.tsx +5 -5
  17. package/notes/app/src/components/DiffPreview.tsx +4 -4
  18. package/notes/app/src/components/FileContentView.tsx +1 -1
  19. package/notes/app/src/components/FileContextMenu.tsx +1 -1
  20. package/notes/app/src/components/FileInfoPanel.branch-tab.test.tsx +3 -35
  21. package/notes/app/src/components/IssueCommentThread.test.tsx +40 -19
  22. package/notes/app/src/components/IssueDetailView.test.tsx +3 -40
  23. package/notes/app/src/components/IssueList.tsx +3 -3
  24. package/notes/app/src/components/LabelSelector.tsx +1 -1
  25. package/notes/app/src/components/MarkdownEditor.tsx +1 -1
  26. package/notes/app/src/components/Skeleton.tsx +2 -2
  27. package/notes/app/src/components/ToastContainer.tsx +1 -1
  28. package/notes/app/src/components/TreeNode.tsx +6 -6
  29. package/notes/app/src/contexts/ToastContext.tsx +1 -1
  30. package/notes/app/src/hooks/useComments.ts +1 -1
  31. package/notes/app/src/hooks/useEditSession.ts +1 -1
  32. package/notes/app/src/lib/api.ts +1 -13
  33. package/notes/app/src/lib/csv-utils.ts +1 -1
  34. package/notes/app/src/lib/postLoginRedirect.ts +1 -1
  35. package/notes/app/src/lib/rtl-logical-properties.test.ts +59 -0
  36. package/notes/app/src/lib/user-colors.ts +1 -1
  37. package/notes/app/src/pages/MainPage.tsx +2 -2
  38. package/notes/app/src/pages/SettingsPage.test.tsx +40 -10
  39. package/notes/app/src/pages/SettingsPage.tsx +1 -1
  40. package/notes/app/src/pages/UserManagementPage.tsx +7 -7
  41. package/notes/app/src/styles/globals.css +17 -0
  42. package/notes/app/src/test/mockPeriodicSync.ts +68 -0
  43. package/notes/app/src/test/mocks.ts +1 -1
  44. package/package.json +7 -3
  45. package/templates/feedback/feedback-lambda/index.mjs +342 -0
  46. package/templates/feedback/frontend/FeedbackWidget.tsx +574 -0
  47. package/templates/feedback/frontend/api.ts +38 -0
  48. package/templates/feedback/frontend/composePinImage.ts +80 -0
  49. package/templates/feedback/frontend/config.ts +14 -0
  50. package/templates/feedback/frontend/types.ts +41 -0
  51. package/templates/feedback/terraform/Makefile +43 -0
  52. package/templates/feedback/terraform/backend.tf +10 -0
  53. package/templates/feedback/terraform/lambda.tf +95 -0
  54. package/templates/feedback/terraform/local.tf +12 -0
  55. package/templates/feedback/terraform/output.tf +9 -0
  56. package/templates/feedback/terraform/provider.tf +24 -0
  57. package/templates/feedback/terraform/variables.tf +25 -0
  58. package/templates/github-actions/aws/deploy-notes.yml.hbs +193 -10
  59. package/templates/notes-docs/001_project/README.md +0 -2
  60. package/templates/notes-docs/002_business_flow/README.md +19 -0
  61. package/templates/notes-docs/003_requirements/README.md +20 -0
  62. package/templates/notes-docs/003_requirements/questions.csv +1 -0
  63. package/templates/notes-docs/003_requirements/spec/README.md +11 -0
  64. package/templates/notes-docs/004_pages/README.md +22 -0
  65. package/templates/notes-docs/005_architecture/README.md +20 -0
  66. package/templates/notes-docs/006_development_workflow/README.md +67 -0
  67. package/templates/notes-docs/006_development_workflow/daily_flow.template.md +303 -0
  68. package/templates/notes-docs/006_development_workflow/git.template.md +72 -20
  69. package/templates/notes-docs/006_development_workflow/judgment.template.md +182 -0
  70. package/templates/notes-docs/006_development_workflow/workflow.template.md +167 -192
  71. package/templates/notes-docs/007_guides/README.md +85 -0
  72. package/templates/notes-docs/007_guides/bot-token-setup.md +174 -0
  73. package/templates/notes-docs/007_guides/getting_started.md +112 -0
  74. package/templates/notes-docs/007_guides/glossary.md +145 -0
  75. package/templates/notes-docs/007_guides/godd_notes_features.md +105 -0
  76. package/templates/notes-docs/007_guides/godd_notes_usage.md +28 -19
  77. package/templates/notes-docs/007_guides/godd_tools.md +129 -0
  78. package/templates/notes-docs/007_guides/godd_usage.md +88 -95
  79. package/templates/notes-docs/007_guides/infra/README.md +57 -0
  80. package/templates/notes-docs/007_guides/{setup_aws_accounts.md → infra/setup_aws_accounts.md} +3 -2
  81. package/templates/notes-docs/007_guides/{setup_infra.template.md → infra/setup_infra.template.md} +15 -4
  82. package/templates/notes-docs/007_guides/setup_godd.md +73 -22
  83. package/templates/notes-docs/007_guides/setup_godd_notes.md +97 -42
  84. package/templates/notes-docs/007_guides/setup_godd_notes.md.styles.json +2 -2
  85. package/templates/notes-docs/007_guides/setup_local_environment.template.md +81 -20
  86. package/templates/notes-docs/007_guides/troubleshooting.md +225 -0
  87. package/templates/notes-docs/999_ref/README.md +23 -0
  88. package/templates/notes-docs/GENERATED.md +1 -1
  89. package/templates/notes-docs/README.md +64 -34
  90. package/templates/terraform/aws/iam.tf.hbs +361 -4
  91. /package/templates/notes-docs/007_guides/{setup_aws_cost_alert.md → infra/setup_aws_cost_alert.md} +0 -0
  92. /package/templates/notes-docs/007_guides/{setup_aws_guard_duty.md → infra/setup_aws_guard_duty.md} +0 -0
@@ -120,7 +120,7 @@
120
120
  | ホバー | `transition-colors` |
121
121
  | ローディング(スケルトン) | `animate-pulse` |
122
122
  | ローディング(スピナー) | `animate-spin` |
123
- | パネル登場 | `animate-slide-in`(`translateX(100%)→0`, 0.25s ease-out。`globals.css` で定義) |
123
+ | パネル登場 | `animate-slide-in`(LTR は `translateX(100%)→0`、RTL は `translateX(-100%)→0`。0.25s ease-out。`globals.css` で定義) |
124
124
 
125
125
  ---
126
126
 
@@ -196,6 +196,9 @@
196
196
  > 日本語本文・見出し・和欧混植・改行・フォーム密度を AI/人間が壊さないための契約を固定する。
197
197
  > §1–8(TypeUI ベースの色・状態・レイアウト契約)と矛盾せず**補完**する。§9.2–9.3 の recipes は
198
198
  > #1020 で `notes/app/src/styles/globals.css` に実装済み(長文プローズ `max-width` のみ保留 — §9.3 参照。本書はトークン・ルールの正本)。
199
+ > 見出しの語中改行禁止・本文 line-height・長い URL/パスの折り返しは、実ブラウザの視覚回帰 E2E
200
+ > (`notes/app/e2e/ja-typography-visual.spec.ts`:モバイル 360px / デスクトップ 1280px、`toHaveScreenshot` +
201
+ > `Range.getClientRects` による幾何アサーション)で退行を自動検出する(#1065)。
199
202
 
200
203
  ### 9.1 タイポグラフィ役割スケール(日本語前提)
201
204
 
@@ -250,7 +253,7 @@
250
253
  | 本文 `line-height` ≥ 1.5 | ✅ PASS | `globals.css` の `:lang(ja) :is(p,li,dd)` で `line-height: 1.55` を明示(#1020) |
251
254
  | 見出しと本文の行間ロジック分離 | ✅ PASS | 見出し `h1:1.35 / h2:1.4 / h3,h4:1.45` を本文(1.55)と分離して明示(#1020 / §9.1 スケール準拠) |
252
255
  | テーブル / フォームの本文からの分離 | ✅ PASS | 本文レシピは `p,li,dd` にスコープし `table` は非継承。フォームは `label/input/textarea/select/button` に個別ルール(入力系のみ `line-height: 1.5`)を明示(#1020 / §9.3) |
253
- | 和欧混植見出しのモバイル目視検証 | ✅ PASS | 固定 360px 幅(モバイル相当)で Windows/Chromium 実機確認済み(#1020)。H1/H2 の和欧混植(`GoDD Notes` / `API Gateway` / `OAuth 2.0`)が語中改行せず折り返し、長い URL / パスもコンテナ内で折り返し横溢れなし |
256
+ | 和欧混植見出しのモバイル目視検証 | ✅ PASS | 固定 360px 幅(モバイル相当)で Windows/Chromium 実機確認済み(#1020)。H1/H2 の和欧混植(`GoDD Notes` / `API Gateway` / `OAuth 2.0`)が語中改行せず折り返し、長い URL / パスもコンテナ内で折り返し横溢れなし。以降の退行は手動目視ではなく `notes/app/e2e/ja-typography-visual.spec.ts`(モバイル 360px / デスクトップ 1280px、Docker `notes-e2e-test` で自動実行)が検出する(#1065) |
254
257
  | macOS / Windows レンダリング確認 | 🟡 WARN | Windows/Chromium は実機確認済み(#1020)。使用プロパティは標準(`overflow-wrap: anywhere` は全ブラウザ対応、`word-break: auto-phrase` は `@supports` ガードで安全に段階適用、`text-autospace` は未対応環境で無害に無視)。**macOS 実機の目視のみ未実施**(当該環境が手元になく AI では実行不能・UAT 推奨) |
255
258
  | タッチターゲット(44px) | ✅ PASS | §4 / §5 で `min-h-11` を契約・実装済み |
256
259
 
@@ -17,6 +17,45 @@ interface Fixtures {
17
17
  }
18
18
 
19
19
  export const test = base.extend<Fixtures>({
20
+ // The API intentionally returns the mock tree with `mode: "mock"` when no
21
+ // GitHub App credentials are available. The file-view and panel scenarios
22
+ // still exercise the authenticated editor UI against that deterministic
23
+ // mock data; normalize only the UI mode marker so MainPage exposes the
24
+ // editor controls. Write endpoints remain mock-disabled and are not
25
+ // called by these browser scenarios.
26
+ page: async ({ page }, use) => {
27
+ await page.route("**/api/tree**", async (route) => {
28
+ const response = await route.fetch();
29
+ if (!response.ok()) {
30
+ await route.fulfill({ response });
31
+ return;
32
+ }
33
+
34
+ const data: unknown = await response.json();
35
+ if (
36
+ typeof data === "object" &&
37
+ data !== null &&
38
+ "mode" in data &&
39
+ (data as { mode?: unknown }).mode === "mock"
40
+ ) {
41
+ const normalized = { ...(data as Record<string, unknown>), mode: "live" };
42
+ await route.fulfill({
43
+ status: response.status(),
44
+ contentType: "application/json",
45
+ body: JSON.stringify(normalized),
46
+ });
47
+ return;
48
+ }
49
+
50
+ await route.fulfill({
51
+ status: response.status(),
52
+ contentType: "application/json",
53
+ body: JSON.stringify(data),
54
+ });
55
+ });
56
+
57
+ await use(page);
58
+ },
20
59
  loginPage: async ({ page }, use) => {
21
60
  await use(new LoginPage(page));
22
61
  },
@@ -0,0 +1,185 @@
1
+ import { test, expect, type Locator, type Page } from "./fixtures";
2
+
3
+ /**
4
+ * #1065: 日本語組版契約(`notes/app/DESIGN.md` §9.1–9.3 / `src/styles/globals.css`)の
5
+ * 視覚回帰テスト。PR #1064 のレビュー指摘(本 PR スコープ外の Suggestion / #1020 フォローアップ)を追跡する。
6
+ *
7
+ * 対象(§9.1–9.3、issue #1065 のスコープ):
8
+ * - 見出し(h1–h3)の和欧混植(`GoDD Notes` / `API Gateway` / `OAuth 2.0`)が語中で改行しないこと
9
+ * - 本文(p)の line-height が契約範囲 1.5–1.65(既定 1.55)内であること
10
+ * - 長い URL / パスがコンテナ内で折り返し、横溢れしないこと
11
+ * スコープ外: 長文プローズ `max-width: 42em`(§9.3 で実装意図的に保留。要否は 質問リスト.md / #1020 で追跡)。
12
+ *
13
+ * 検証方針(3 段構え):
14
+ * 1. 見出しの `computed style`(`word-break`)が `break-all`(§9.4 の禁止事項)に退行していないことを
15
+ * 直接検証する。短い和欧混植トークンは行幅に余裕があると break-all 下でも偶然折り返し位置が
16
+ * 変わらないことがあるため、レンダリング結果だけに頼らない一次検証として置く。
17
+ * 2. 幾何アサーション(`Range.getClientRects`)で「語中改行しない」ことを実測する。
18
+ * PR #1041(`panel-resize-handle.spec.ts`)の `getBoundingClientRect` 数値比較パターンを踏襲し、
19
+ * クロス OS のフォント描画差に左右されにくい決定的な二次検証とする。
20
+ * 3. Playwright `toHaveScreenshot` でプロース領域のピクセル差分も取得し、issue #1065 の受け入れ基準
21
+ * (ベースライン確定後に `globals.css` の組版 recipe を変更すると視覚回帰テストが fail すること)を満たす。
22
+ *
23
+ * 対象コンテンツは実際の本番レンダリングパス(`FileContentView` → `MarkdownView` の
24
+ * marked + `.prose`)を通す。API 応答のみ `page.route` でモックする(`file-view.spec.ts` と同じ方針)。
25
+ *
26
+ * ベースラインスナップショット(`ja-typography-visual.spec.ts-snapshots/*.png`)は
27
+ * Docker(`notes-e2e-test` / Linux + Playwright 同梱 Chromium 固定)でのみ生成・更新すること。
28
+ * ローカル Windows/macOS で `--update-snapshots` を実行すると、フォントレンダリング差で
29
+ * CI のベースラインと乖離し無効化される(issue #1065 の留意点)。
30
+ *
31
+ * docker compose --profile test run --rm notes-app-test pnpm build
32
+ * docker compose up --build -d postgres notes-api notes-app
33
+ * docker compose --profile e2e run --rm -v "$(pwd)/notes/app/e2e:/app/e2e" notes-e2e-test \
34
+ * npx playwright test e2e/ja-typography-visual.spec.ts --update-snapshots
35
+ */
36
+
37
+ const FIXTURE_PATH = "e2e-fixtures/ja-typography-contract.md";
38
+
39
+ // DESIGN.md §9 の和欧混植サンプル(GoDD Notes / API Gateway / OAuth 2.0)+ 長い URL/パス。
40
+ const MARKDOWN_FIXTURE = `# GoDD Notes と API Gateway 連携
41
+
42
+ ## API Gateway 経由のリクエストフロー
43
+
44
+ ### OAuth 2.0 によるトークン発行
45
+
46
+ このセクションでは、GoDD Notes が API Gateway を経由して外部サービスと連携する際の認証フローを説明します。認証には OAuth 2.0 を使用し、アクセストークンを発行します。
47
+
48
+ トークンエンドポイントの例です。長い URL はコンテナ内で折り返され、横溢れしません。
49
+
50
+ https://api.example.com/v1/oauth/token/exchange?client_id=godd-notes-app&scope=read-write-admin&redirect_uri=https%3A%2F%2Fnotes.example.com%2Fcallback
51
+
52
+ 設定ファイルのパス例: /var/lib/godd/config/notes-api-gateway-oauth-client-credentials.json
53
+ `;
54
+
55
+ async function mockFixtureFile(page: Page): Promise<void> {
56
+ await page.route(`**/api/files/${FIXTURE_PATH}`, async (route) => {
57
+ await route.fulfill({
58
+ status: 200,
59
+ contentType: "application/json",
60
+ body: JSON.stringify({
61
+ path: FIXTURE_PATH,
62
+ content: MARKDOWN_FIXTURE,
63
+ sha: "e2e-fixture-sha",
64
+ size: MARKDOWN_FIXTURE.length,
65
+ encoding: "utf-8",
66
+ styles: null,
67
+ }),
68
+ });
69
+ });
70
+ }
71
+
72
+ /**
73
+ * `heading` 内で `token` が単一行に収まっている(= 語中改行していない)ことを検証する。
74
+ * `Range.getClientRects()` の `top` 座標がすべて同じなら、その文字列は 1 行に収まっている。
75
+ * `word-break: break-all` 等への回帰でトークン内に改行が入ると `top` が複数値に分かれ fail する。
76
+ */
77
+ async function assertNoMidWordWrap(heading: Locator, token: string): Promise<void> {
78
+ const lineCount = await heading.evaluate((el, needle: string) => {
79
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
80
+ let node: Text | null;
81
+ while ((node = walker.nextNode() as Text | null)) {
82
+ const text = node.textContent ?? "";
83
+ const idx = text.indexOf(needle);
84
+ if (idx === -1) continue;
85
+ const range = document.createRange();
86
+ range.setStart(node, idx);
87
+ range.setEnd(node, idx + needle.length);
88
+ const tops = Array.from(range.getClientRects()).map((r) => Math.round(r.top));
89
+ return new Set(tops).size;
90
+ }
91
+ return 0;
92
+ }, token);
93
+
94
+ expect(lineCount, `"${token}" は語中改行せず単一行に収まる必要があります`).toBe(1);
95
+ }
96
+
97
+ async function runTypographyContractAssertions(page: Page): Promise<void> {
98
+ const prose = page.locator("main .prose");
99
+ await expect(prose).toBeVisible({ timeout: 10_000 });
100
+
101
+ const h1 = prose.locator("h1");
102
+ const h2 = prose.locator("h2");
103
+ const h3 = prose.locator("h3");
104
+ await expect(h1).toBeVisible();
105
+ await expect(h2).toBeVisible();
106
+ await expect(h3).toBeVisible();
107
+
108
+ await test.step("見出しに本文へのグローバル break-all が波及していない(DESIGN.md §9.4 禁止事項)", async () => {
109
+ // 短い和欧混植トークン(GoDD / API 等)は現行フォント・ビューポート幅では
110
+ // word-break: break-all に規制退行しても偶然折り返し位置が変わらないことがあるため
111
+ // (語自体が行に収まってしまう)、レンダリング結果だけに頼らず computed style も直接検証する。
112
+ for (const heading of [h1, h2, h3]) {
113
+ const wordBreak = await heading.evaluate((el) => getComputedStyle(el).wordBreak);
114
+ expect(wordBreak, "見出しに word-break: break-all が適用されてはならない").not.toBe(
115
+ "break-all",
116
+ );
117
+ }
118
+ });
119
+
120
+ await test.step("見出しが和欧混植で語中改行しない(DESIGN.md §9.3)", async () => {
121
+ await assertNoMidWordWrap(h1, "GoDD");
122
+ await assertNoMidWordWrap(h1, "Notes");
123
+ await assertNoMidWordWrap(h1, "API");
124
+ await assertNoMidWordWrap(h1, "Gateway");
125
+ await assertNoMidWordWrap(h2, "Gateway");
126
+ await assertNoMidWordWrap(h3, "OAuth");
127
+ await assertNoMidWordWrap(h3, "2.0");
128
+ });
129
+
130
+ await test.step("本文 line-height が契約範囲 1.5–1.65 内(DESIGN.md §9.1)", async () => {
131
+ const metrics = await prose.locator("p").first().evaluate((el) => {
132
+ const style = getComputedStyle(el);
133
+ return {
134
+ lineHeight: Number.parseFloat(style.lineHeight),
135
+ fontSize: Number.parseFloat(style.fontSize),
136
+ };
137
+ });
138
+ const ratio = metrics.lineHeight / metrics.fontSize;
139
+ expect(ratio).toBeGreaterThanOrEqual(1.5);
140
+ expect(ratio).toBeLessThanOrEqual(1.65);
141
+ });
142
+
143
+ await test.step("長い URL / パスがコンテナ内で折り返し横溢れしない(DESIGN.md §9.3)", async () => {
144
+ const overflow = await prose.evaluate((el) => el.scrollWidth - el.clientWidth);
145
+ expect(overflow).toBeLessThanOrEqual(1);
146
+ });
147
+ }
148
+
149
+ test.describe("日本語組版契約の視覚回帰 (#1065)", () => {
150
+ test.describe("モバイル 360px", () => {
151
+ test.use({ viewport: { width: 360, height: 800 } });
152
+
153
+ test("見出し折り返し・本文 line-height・長文 URL 折り返しが契約どおり", async ({
154
+ page,
155
+ mainPage,
156
+ }) => {
157
+ await mockFixtureFile(page);
158
+ await mainPage.goto(FIXTURE_PATH);
159
+
160
+ await runTypographyContractAssertions(page);
161
+
162
+ await expect(page.locator("main .prose")).toHaveScreenshot(
163
+ "ja-typography-mobile-360.png",
164
+ );
165
+ });
166
+ });
167
+
168
+ test.describe("デスクトップ 1280px", () => {
169
+ test.use({ viewport: { width: 1280, height: 800 } });
170
+
171
+ test("見出し折り返し・本文 line-height・長文 URL 折り返しが契約どおり", async ({
172
+ page,
173
+ mainPage,
174
+ }) => {
175
+ await mockFixtureFile(page);
176
+ await mainPage.goto(FIXTURE_PATH);
177
+
178
+ await runTypographyContractAssertions(page);
179
+
180
+ await expect(page.locator("main .prose")).toHaveScreenshot(
181
+ "ja-typography-desktop-1280.png",
182
+ );
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,36 @@
1
+ import { test, expect } from "./fixtures";
2
+
3
+ /**
4
+ * #1043: TreeNode の階層インデントが dir に応じて inline-start 側へ移ることを
5
+ * 実ブラウザで検証する。jsdom は論理プロパティの物理側を計算できないため、
6
+ * paddingInlineStart の反転は E2E でガードする。
7
+ */
8
+ test.describe("RTL 論理プロパティ (#1043)", () => {
9
+ test.use({ viewport: { width: 1280, height: 720 } });
10
+
11
+ test("ファイルツリーの階層インデントが RTL で物理右へ反転する", async ({ page, mainPage }) => {
12
+ await mainPage.goto();
13
+ await page.getByRole("button", { name: /001_project/ }).click();
14
+
15
+ const nestedFile = page.getByRole("button", { name: /features\.csv/ });
16
+ await expect(nestedFile).toBeVisible();
17
+
18
+ const measurePadding = () =>
19
+ nestedFile.evaluate((element) => {
20
+ const style = getComputedStyle(element);
21
+ return {
22
+ left: Number.parseFloat(style.paddingLeft),
23
+ right: Number.parseFloat(style.paddingRight),
24
+ };
25
+ });
26
+
27
+ const ltr = await measurePadding();
28
+ expect(ltr.left).toBeGreaterThan(ltr.right);
29
+
30
+ await page.evaluate(() => document.documentElement.setAttribute("dir", "rtl"));
31
+
32
+ const rtl = await measurePadding();
33
+ expect(rtl.right).toBeGreaterThan(rtl.left);
34
+ expect(rtl.right).toBeCloseTo(ltr.left, 0);
35
+ });
36
+ });
@@ -46,7 +46,6 @@
46
46
  "@playwright/test": "^1.59.1",
47
47
  "@tailwindcss/vite": "^4.1.18",
48
48
  "@testing-library/react": "^16.3.2",
49
- "@types/marked": "^5.0.2",
50
49
  "@types/node": "^24.10.1",
51
50
  "@types/react": "^19.2.7",
52
51
  "@types/react-dom": "^19.2.3",
@@ -3898,13 +3897,6 @@
3898
3897
  "@types/mdurl": "^2"
3899
3898
  }
3900
3899
  },
3901
- "node_modules/@types/marked": {
3902
- "version": "5.0.2",
3903
- "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
3904
- "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==",
3905
- "dev": true,
3906
- "license": "MIT"
3907
- },
3908
3900
  "node_modules/@types/mdurl": {
3909
3901
  "version": "2.0.0",
3910
3902
  "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
@@ -51,7 +51,6 @@
51
51
  "@playwright/test": "^1.59.1",
52
52
  "@tailwindcss/vite": "^4.1.18",
53
53
  "@testing-library/react": "^16.3.2",
54
- "@types/marked": "^5.0.2",
55
54
  "@types/node": "^24.10.1",
56
55
  "@types/react": "^19.2.7",
57
56
  "@types/react-dom": "^19.2.3",
@@ -17,6 +17,13 @@ export default defineConfig({
17
17
  screenshot: "only-on-failure",
18
18
  },
19
19
 
20
+ // #1065: 日本語組版契約の視覚回帰(toHaveScreenshot)向けの既定閾値。
21
+ // Docker(notes-e2e-test / Linux+bundled Chromium)でベースラインを固定する前提で、
22
+ // フォントのアンチエイリアス差を吸収しつつレイアウト崩れ(語中改行・行間変化)は検出する値。
23
+ expect: {
24
+ toHaveScreenshot: { maxDiffPixelRatio: 0.02, animations: "disabled" },
25
+ },
26
+
20
27
  projects: [
21
28
  {
22
29
  name: "setup",
@@ -117,9 +117,6 @@ importers:
117
117
  '@testing-library/react':
118
118
  specifier: ^16.3.2
119
119
  version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
120
- '@types/marked':
121
- specifier: ^5.0.2
122
- version: 5.0.2
123
120
  '@types/node':
124
121
  specifier: ^24.10.1
125
122
  version: 24.10.13
@@ -1535,9 +1532,6 @@ packages:
1535
1532
  '@types/markdown-it@14.1.2':
1536
1533
  resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
1537
1534
 
1538
- '@types/marked@5.0.2':
1539
- resolution: {integrity: sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==}
1540
-
1541
1535
  '@types/mdurl@2.0.0':
1542
1536
  resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
1543
1537
 
@@ -5042,8 +5036,6 @@ snapshots:
5042
5036
  '@types/linkify-it': 5.0.0
5043
5037
  '@types/mdurl': 2.0.0
5044
5038
 
5045
- '@types/marked@5.0.2': {}
5046
-
5047
5039
  '@types/mdurl@2.0.0': {}
5048
5040
 
5049
5041
  '@types/node@24.10.13':
@@ -82,9 +82,9 @@ export default function BranchSelectorModal({
82
82
  <button
83
83
  type="button"
84
84
  onClick={() => { onSelect(null); onClose(); }}
85
- className={`block w-full text-left px-4 py-3 border-b border-gray-700/50 transition-colors ${
85
+ className={`block w-full text-start px-4 py-3 border-b border-gray-700/50 transition-colors ${
86
86
  currentBranch === null
87
- ? "bg-blue-900/40 border-l-2 border-l-blue-400"
87
+ ? "bg-blue-900/40 border-s-2 border-s-blue-400"
88
88
  : "hover:bg-gray-700/30"
89
89
  }`}
90
90
  >
@@ -92,7 +92,7 @@ export default function BranchSelectorModal({
92
92
  <GitBranch className="w-3.5 h-3.5 text-green-400 shrink-0" />
93
93
  <span className="text-sm font-medium">main</span>
94
94
  {currentBranch === null && (
95
- <span className="text-[10px] text-blue-400 ml-auto">表示中</span>
95
+ <span className="text-[10px] text-blue-400 ms-auto">表示中</span>
96
96
  )}
97
97
  </div>
98
98
  </button>
@@ -120,9 +120,9 @@ export default function BranchSelectorModal({
120
120
  key={pr.number}
121
121
  type="button"
122
122
  onClick={() => { onSelect(pr.branch); onClose(); }}
123
- className={`block w-full text-left px-4 py-3 border-b border-gray-700/50 transition-colors ${
123
+ className={`block w-full text-start px-4 py-3 border-b border-gray-700/50 transition-colors ${
124
124
  isSelected
125
- ? "bg-blue-900/40 border-l-2 border-l-blue-400"
125
+ ? "bg-blue-900/40 border-s-2 border-s-blue-400"
126
126
  : "hover:bg-gray-700/30"
127
127
  }`}
128
128
  >
@@ -137,10 +137,10 @@ export default function BranchSelectorModal({
137
137
  <span className="text-[10px] text-blue-400 shrink-0">表示中</span>
138
138
  )}
139
139
  </div>
140
- <div className="text-[10px] text-gray-500 font-mono truncate mb-0.5 pl-4">
140
+ <div className="text-[10px] text-gray-500 font-mono truncate mb-0.5 ps-4">
141
141
  {pr.branch}
142
142
  </div>
143
- <div className="flex items-center gap-2 text-[10px] text-gray-500 pl-4">
143
+ <div className="flex items-center gap-2 text-[10px] text-gray-500 ps-4">
144
144
  <span>#{pr.number}</span>
145
145
  <span>{pr.author}</span>
146
146
  <span>
@@ -156,7 +156,7 @@ export default function BranchSelectorModal({
156
156
  href={pr.url}
157
157
  target="_blank"
158
158
  rel="noopener noreferrer"
159
- className="text-blue-400 hover:text-blue-300 ml-auto"
159
+ className="text-blue-400 hover:text-blue-300 ms-auto"
160
160
  onClick={(e) => e.stopPropagation()}
161
161
  >
162
162
  GitHub
@@ -79,7 +79,7 @@ function CommentItem({
79
79
  )}
80
80
  </div>
81
81
  {comment.anchor_text && (
82
- <blockquote className="mt-1 pl-2 border-l-2 border-blue-600 text-gray-400 truncate">
82
+ <blockquote className="mt-1 ps-2 border-s-2 border-blue-600 text-gray-400 truncate">
83
83
  {comment.anchor_text}
84
84
  </blockquote>
85
85
  )}
@@ -185,7 +185,7 @@ function CommentItem({
185
185
  <button
186
186
  type="button"
187
187
  onClick={() => onDelete(comment.id)}
188
- className="flex items-center gap-0.5 hover:text-red-400 transition-colors ml-auto"
188
+ className="flex items-center gap-0.5 hover:text-red-400 transition-colors ms-auto"
189
189
  title="削除"
190
190
  >
191
191
  <Trash2 className="w-3 h-3" />
@@ -195,7 +195,7 @@ function CommentItem({
195
195
 
196
196
  {/* Replies */}
197
197
  {expanded && replies.length > 0 && (
198
- <div className="border-t border-gray-700 ml-4 space-y-0 divide-y divide-gray-700/50">
198
+ <div className="border-t border-gray-700 ms-4 space-y-0 divide-y divide-gray-700/50">
199
199
  {replies.map((reply) => (
200
200
  <div key={reply.id} className="px-2.5 py-1.5">
201
201
  <div className="flex items-baseline gap-1.5 flex-wrap">
@@ -208,7 +208,7 @@ function CommentItem({
208
208
  <button
209
209
  type="button"
210
210
  onClick={() => onDelete(reply.id)}
211
- className="flex items-center gap-0.5 hover:text-red-400 transition-colors ml-auto"
211
+ className="flex items-center gap-0.5 hover:text-red-400 transition-colors ms-auto"
212
212
  title="削除"
213
213
  >
214
214
  <Trash2 className="w-3 h-3" />
@@ -315,7 +315,7 @@ export default function CommentPanel({
315
315
  <h3 className="text-sm font-semibold text-gray-300">
316
316
  コメント
317
317
  {allComments.length > 0 && (
318
- <span className="ml-1.5 text-xs text-gray-500">({allComments.length})</span>
318
+ <span className="ms-1.5 text-xs text-gray-500">({allComments.length})</span>
319
319
  )}
320
320
  </h3>
321
321
  <div className="flex items-center gap-1">
@@ -30,7 +30,7 @@ export default function DiffPreview({ original, modified, onClose }: Props) {
30
30
  <span className="text-sm font-medium text-gray-300">変更プレビュー</span>
31
31
  <span className="text-xs text-green-400">+{stats.added}</span>
32
32
  <span className="text-xs text-red-400">-{stats.removed}</span>
33
- <div role="toolbar" aria-label="差分表示モード" className="flex items-center gap-0.5 rounded bg-gray-700 p-0.5 ml-2">
33
+ <div role="toolbar" aria-label="差分表示モード" className="flex items-center gap-0.5 rounded bg-gray-700 p-0.5 ms-2">
34
34
  <button
35
35
  type="button"
36
36
  aria-pressed={viewMode === "unified"}
@@ -98,10 +98,10 @@ function UnifiedView({ changes }: { changes: ReturnType<typeof diffLines> }) {
98
98
  const prefix = part.added ? "+" : part.removed ? "-" : " ";
99
99
  return (
100
100
  <div key={`${i}-${j}`} className={`flex ${bg}`}>
101
- <span className="w-12 text-right pr-3 text-gray-600 select-none shrink-0">
101
+ <span className="w-12 text-end pe-3 text-gray-600 select-none shrink-0">
102
102
  {!part.added && !part.removed ? lineNum : ""}
103
103
  </span>
104
- <span className={`pr-2 ${part.added ? "text-green-500" : part.removed ? "text-red-500" : "text-gray-600"} select-none`}>
104
+ <span className={`pe-2 ${part.added ? "text-green-500" : part.removed ? "text-red-500" : "text-gray-600"} select-none`}>
105
105
  {prefix}
106
106
  </span>
107
107
  <span className={textColor}>{line || " "}</span>
@@ -151,7 +151,7 @@ function SideBySideView({ original, modified }: { original: string; modified: st
151
151
 
152
152
  return (
153
153
  <div className="flex min-w-0 overflow-x-auto">
154
- <div className="flex-1 min-w-[200px] border-r border-gray-700 p-4">
154
+ <div className="flex-1 min-w-[200px] border-e border-gray-700 p-4">
155
155
  <div className="text-[10px] text-gray-500 mb-2 uppercase">元のバージョン</div>
156
156
  {Array.from({ length: maxLines }).map((_, i) => {
157
157
  const line = leftLines[i];
@@ -57,7 +57,7 @@ function CsvTable({ content }: { content: string }) {
57
57
  {header.map((cell, i) => (
58
58
  <th
59
59
  key={i}
60
- className="border border-gray-600 bg-gray-700 px-3 py-2 text-left font-semibold text-gray-200 whitespace-nowrap"
60
+ className="border border-gray-600 bg-gray-700 px-3 py-2 text-start font-semibold text-gray-200 whitespace-nowrap"
61
61
  >
62
62
  {cell}
63
63
  </th>
@@ -54,7 +54,7 @@ export default function FileContextMenu({ x, y, isDir, onAction, onClose }: Prop
54
54
  role="menuitem"
55
55
  key={item.action}
56
56
  onClick={() => onAction(item.action)}
57
- className={`w-full text-left px-3 py-1.5 text-sm hover:bg-gray-700 flex items-center gap-2 ${
57
+ className={`w-full text-start px-3 py-1.5 text-sm hover:bg-gray-700 flex items-center gap-2 ${
58
58
  item.action === "delete" ? "text-red-400 hover:text-red-300" : "text-gray-200"
59
59
  }`}
60
60
  >
@@ -5,42 +5,10 @@ import type { FileContent } from "@/types";
5
5
  const triggerPeriodicSync = vi.hoisted(() => vi.fn());
6
6
 
7
7
  vi.mock("@/hooks/usePeriodicSync", () => {
8
+ // vi.mock factory は hoist されるため、共有ヘルパも factory 内で解決する。
8
9
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- mock factory 制約
9
- const { useEffect, useRef } = require("react") as typeof import("react");
10
- return {
11
- // 実フック(usePeriodicSync.ts)の即時 sync 挙動を忠実に再現する:
12
- // onSync は ref 捕捉し、effect 依存は実フックと同じ [enabled, intervalMs, skipInitialSync]。
13
- // enabled 時の effect setup(初回マウント / enabled false→true 遷移)で onSync を発火し、
14
- // 初回マウントは skipInitialSync で抑制する。これにより「mount 直後の foreground load 中に
15
- // 即時 background poll が走る」競合(#934)を再現する。
16
- usePeriodicSync: ({
17
- onSync,
18
- enabled,
19
- intervalMs,
20
- skipInitialSync,
21
- }: {
22
- onSync: () => void;
23
- enabled: boolean;
24
- intervalMs: number;
25
- skipInitialSync?: boolean;
26
- }) => {
27
- const onSyncRef = useRef(onSync);
28
- const isInitialMountRef = useRef(true);
29
- useEffect(() => {
30
- onSyncRef.current = onSync;
31
- }, [onSync]);
32
- useEffect(() => {
33
- triggerPeriodicSync.mockImplementation(() => {
34
- if (enabled) onSyncRef.current();
35
- });
36
- const isInitialMount = isInitialMountRef.current;
37
- isInitialMountRef.current = false;
38
- if (!enabled || intervalMs <= 0) return;
39
- if (skipInitialSync && isInitialMount) return;
40
- onSyncRef.current();
41
- }, [enabled, intervalMs, skipInitialSync]);
42
- },
43
- };
10
+ const { faithfulPeriodicSyncMock } = require("../test/mockPeriodicSync.ts") as typeof import("../test/mockPeriodicSync");
11
+ return faithfulPeriodicSyncMock(triggerPeriodicSync);
44
12
  });
45
13
 
46
14
  vi.mock("@/lib/api", () => ({