From 796fcb8f292707645020e11da8a13ae1ef0323f1 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 11 May 2026 10:03:17 +0900 Subject: [PATCH 1/2] feat(tutorial): split mesh tutorials into 3 step categories (Phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mesh チュートリアル 9 deck を「メッセージを送ってみよう」「ふたりで会話しよう」 「みんなで会話しよう」の 3 カテゴリ × 3 lv 構成に再分類し、ステップ軸と Lv 軸の 2 軸構造を tipsLibrary 上で分かりやすく表示できるようにする。 - tutorial-tags.js: CATEGORIES.chatApp を削除、meshStep1/2/3 を追加 - library.jsx: local messages mapping を新カテゴリ 3 つに更新 - decks/index.jsx: 9 deck の category 参照を新キーに変更 (deck ID は維持) - locales/{en,ja,ja-Hira}.js: gui.library.meshStep1/2/3 の翻訳を追加、 廃止した gui.library.chatApp を削除 docs/tutorial/improvement-plan.md でチュートリアル全体の拡充計画 (Phase 1〜4: Mesh / Ruby / Block / DNCL) を文書化し、Phase 3 では書籍 プロモーション戦略 (Lv0/Lv2/Lv3 番号付けで Lv1 を「書籍を読んで理解する段階」 として空ける)、横断的な setup プロパティによるタブ・モード・拡張機能の 自動セットアップ機構などを設計判断として記録した。 docs/tutorial/progress.md は進捗トラッカー (全 Phase 完了時に削除予定)。 tools/playwright-verify/verify-tutorial-mesh-categories.mjs で tipsLibrary に 3 カテゴリが想定通り表示されることを自動確認できる。 Refs: #678, #682 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/tutorial/improvement-plan.md | 480 ++++++++++++++++++ docs/tutorial/progress.md | 67 +++ .../src/components/library/library.jsx | 18 +- .../src/lib/libraries/decks/index.jsx | 18 +- .../src/lib/libraries/tutorial-tags.js | 7 +- packages/scratch-gui/src/locales/en.js | 4 +- packages/scratch-gui/src/locales/ja-Hira.js | 4 +- packages/scratch-gui/src/locales/ja.js | 4 +- .../verify-tutorial-mesh-categories.mjs | 74 +++ 9 files changed, 659 insertions(+), 17 deletions(-) create mode 100644 docs/tutorial/improvement-plan.md create mode 100644 docs/tutorial/progress.md create mode 100644 tools/playwright-verify/verify-tutorial-mesh-categories.mjs diff --git a/docs/tutorial/improvement-plan.md b/docs/tutorial/improvement-plan.md new file mode 100644 index 00000000000..594acddc5f0 --- /dev/null +++ b/docs/tutorial/improvement-plan.md @@ -0,0 +1,480 @@ +# チュートリアル拡充プラン + +> **🆕 計画文書** — 既存チュートリアル機能 (`docs/tutorial/README.md` 参照) を、Welcome モーダルが提示する 3 つの軸 (Block / Ruby / Mesh) + 高校向け DNCL に沿って体系的に拡充するためのロードマップ。 + +## ゴール + +Welcome モーダルの `cardBlocks` / `cardRuby` / `cardMesh` が提示する 3 つの学習軸に、それぞれ段階的なチュートリアル群を用意する。さらに高校生向けの **DNCL チュートリアル** を新設し、共通テスト「情報Ⅰ」の対策に直結する基礎技能とアルゴリズムを提供する。 + +ユーザーが Welcome → チュートリアル一覧 → カテゴリで学習軸を選ぶ → 同じ題材を Lv1 (コード挿入) → Lv2 (ブロック手組み) → Lv3 (Ruby) と難度別に進めるという既存パターンを、全軸で踏襲する。 + +## 現状サマリー (2026-05 時点) + +### 既存 deck 一覧 (`packages/scratch-gui/src/lib/libraries/decks/index.jsx`) + +| Category | Deck ID | Lv | 内容 | +|---|---|---|---| +| `gettingStarted` | `intro-getting-started` | — | ネコがバウンスする最初の Ruby プログラム | +| `chatApp` | `chat-1-basic-1` | Lv1 | メッセージを送ってみよう (コード挿入) | +| `chatApp` | `chat-1-basic-2` | Lv2 | 同じプログラムをブロック手組み | +| `chatApp` | `chat-1-basic-3` | Lv3 | 同じプログラムを Ruby で | +| `chatApp` | `chat-2-sprites-1` | Lv1 | ネコとペンギンで会話しよう | +| `chatApp` | `chat-2-sprites-2` | Lv2 | (Lv2) | +| `chatApp` | `chat-2-sprites-3` | Lv3 | (Lv3) | +| `chatApp` | `chat-3-mesh-1` | Lv1 | メッシュ拡張機能でつながろう | +| `chatApp` | `chat-3-mesh-2` | Lv2 | (Lv2) | +| `chatApp` | `chat-3-mesh-3` | Lv3 | (Lv3) | + +合計 10 deck。`gettingStarted` と `chatApp` の 2 カテゴリ。 + +### 課題 + +- `chatApp` 1 カテゴリ内に「3 ステップ × 3 lv = 9 deck」が並んでおり、**ステップ軸と lv 軸の 2 軸**が UI 上で見分けにくい (`library.jsx` の `withCategories` は flat list 表示)。 +- **Block 軸の独立したチュートリアルが存在しない**。`gettingStarted` の唯一の deck は Ruby 中心。Welcome モーダルが「ブロックで作る」を最初に提示しているのに、ブロック中心の deck が無い。 +- **Ruby 軸も 1 deck のみ**で、Smalruby を「Ruby を学ぶ場」として使いたいユーザーには学習素材が不足。 +- **DNCL 軸は 0 deck**。Smalruby は DNCL モードを実装済みなのに、共通テスト対策に使える deck が無い。 + +## 新タクソノミー + +### Category キー (改訂後) + +`packages/scratch-gui/src/lib/libraries/tutorial-tags.js` の `CATEGORIES` を以下に更新する: + +```javascript +export const CATEGORIES = { + // 既存 + gettingStarted: 'gettingStarted', + + // Phase 1: chatApp を 3 つに分割 (ストーリー型) + meshStep1: 'meshStep1', // メッセージを送ってみよう + meshStep2: 'meshStep2', // ふたりで会話しよう + meshStep3: 'meshStep3', // みんなで会話しよう + + // Phase 3: Block 軸 (教科ラベル型) + blockBasics: 'blockBasics', // はじめての操作 + blockGames: 'blockGames', // ゲームを作ろう + blockMath: 'blockMath', // 算数: 幾何学模様 + blockScience: 'blockScience', // 理科: マイクロビット + + // Phase 2: Ruby 軸 + rubyBasics: 'rubyBasics', // Ruby のきほん + + // Phase 4: DNCL 軸 + dnclBasics: 'dnclBasics', // DNCL のきほん + dnclAlgorithms: 'dnclAlgorithms', // アルゴリズム +}; +``` + +`chatApp` キーは削除する (置き換え)。後方互換性のための残置は不要 — 既存 9 deck の `category` 参照を新キーに更新するため。 + +### `tag-messages.js` に追加する category ラベル + +各キーに対応する `gui.libraryCategories.` メッセージを追加する。`defaultMessage` (日本語ベース) と `description` を明記。後段で `ja.js` / `ja-Hira.js` / `en.js` にも翻訳を追加。 + +### 並び順 + +`library.jsx` 側でカテゴリの表示順をどう制御するか確認が必要 (現状はオブジェクトのキー順か、deck の出現順に依存する可能性)。新タクソノミーは以下の順を意図: + +1. `gettingStarted` — はじめての Ruby +2. `blockBasics` → `blockGames` → `blockMath` → `blockScience` (Block 軸 4 系) +3. `rubyBasics` (Ruby 軸) +4. `meshStep1` → `meshStep2` → `meshStep3` (Mesh 軸 3 ステップ) +5. `dnclBasics` → `dnclAlgorithms` (DNCL 軸 2 区分) + +実装時、`library.jsx` で `withCategories` 表示の順序が `CATEGORIES` オブジェクトのキー順に依存しないなら、明示的な順序配列を導入する。 + +## Phase 1: Mesh 再カテゴリ化 + +**目標**: 既存 9 deck を 3 カテゴリ × 3 lv に再分類。新規 deck・画像は無し。 + +**ファイル変更**: + +1. `tutorial-tags.js` — `chatApp` 削除、`meshStep1/2/3` を追加。 +2. `tag-messages.js` — `gui.libraryCategories.meshStep1/2/3` を追加。 + - `meshStep1`: 「メッセージを送ってみよう」 + - `meshStep2`: 「ふたりで会話しよう」 + - `meshStep3`: 「みんなで会話しよう」 +3. `decks/index.jsx` — 9 deck の `category` 参照を以下にマップ: + - `chat-1-basic-{1,2,3}` → `CATEGORIES.meshStep1` + - `chat-2-sprites-{1,2,3}` → `CATEGORIES.meshStep2` + - `chat-3-mesh-{1,2,3}` → `CATEGORIES.meshStep3` +4. `locales/{ja,ja-Hira,en}.js` — 新 category キーの翻訳を追加。 +5. `library.jsx` — カテゴリ並び順の制御を確認。必要なら順序配列を追加。 + +**確認項目**: +- チュートリアル一覧でカテゴリ見出しが 3 段階に分かれ、各見出し下に Lv1/Lv2/Lv3 が並ぶこと。 +- カテゴリ名で「ステップになっていること」が一目で分かること (「メッセージを送ってみよう」→「ふたりで会話」→「みんなで会話」の進行)。 +- 既存 deck の挙動 (allowedBlocks、urlId、deckIds ナビゲーション) に副作用が無いこと。 + +**画像**: 既存 deck の画像を流用するため新規撮影なし。 + +**規模見積もり**: 1 PR、変更 5 ファイル前後、~30 分。 + +## Phase 2: Ruby 軸 — TryRuby ベース拡充 + +**目標**: `rubyBasics` カテゴリに 6〜7 deck を追加。最終 deck で TryRuby (https://try.ruby-lang.org/) への導線を提供。 + +### `puts` を採用して「本物の Ruby」感を出す + +Smalruby の Ruby → ブロック変換は **`puts` / `print` / `p` を `looks_say` / `looks_sayforsecs` にマップ**する (`packages/scratch-gui/src/lib/ruby-to-blocks-converter/looks.js`、`packages/scratch-gui/src/lib/ruby-generator/looks.js`)。つまり同じ `puts "Hello"` というコードが: + +- Smalruby では **スプライトが「Hello」としゃべる** (可視的フィードバック) +- 標準 Ruby / TryRuby では **コンソールに「Hello」と出力** (テキストフィードバック) + +として動く。Phase 2 のコード例は `say(...)` ではなく **`puts ...` を一貫して使う**。これにより: + +- 教科書・参考書・TryRuby・irb と **同じコード** が Smalruby でも書ける +- 最終 deck で TryRuby に移行した際、学んだコードがそのまま流用できる +- `puts` の引数は **改行不要・複数文字列 OK** という Ruby の標準仕様を最初から体験できる +- Smalruby は可視成果 (スプライトがしゃべる) で、TryRuby はテキスト出力で同じコードを試せる、という対比が学習者に伝わる + +### deck 候補 (TryRuby との対応) + +TryRuby 日本語訳 56 レッスン (`~/ghq/github.com/ruby/TryRuby/translations/ja/try_ruby_*.md`) から、Smalruby のスプライト世界に翻案しやすいものを抽出する。Smalruby 翻案では **`puts` で結果をスプライトにしゃべらせる**: + +| Deck ID | カテゴリ | Lv1 内容 | TryRuby 対応 | Smalruby 翻案 (puts ベース) | +|---|---|---|---|---| +| `ruby-basics-1-numbers` | rubyBasics | 計算してみよう (`4 * 10`, `5 - 12`) | try_ruby_20〜40 | `puts 4 * 10` でネコが「40」としゃべる | +| `ruby-basics-2-strings` | rubyBasics | 文字列を逆さに (`"スモウルビー".reverse`) | try_ruby_50〜100 | `puts "スモウルビー".reverse` | +| `ruby-basics-3-variables` | rubyBasics | 変数で覚える (`name = "ネコ"`) | try_ruby_110〜140 | `name = "ネコ"; puts "こんにちは " + name` | +| `ruby-basics-4-arrays` | rubyBasics | リストを使う (`ticket = [12, 47, 35]`) | try_ruby_150〜200 | `ticket = [12, 47, 35]; puts ticket.reverse` | +| `ruby-basics-5-blocks` | rubyBasics | ブロックで繰り返す (`5.times { |i| ... }`) | try_ruby_290〜310 | `5.times { |i| puts i }` で 0〜4 を順にしゃべる | +| `ruby-basics-6-methods` | rubyBasics | メソッドを作る (`def`) | try_ruby_350〜420 | `def hello(name); puts "Hi " + name; end` | +| `ruby-basics-7-next` | rubyBasics | 次に進もう (TryRuby 導線) | try_ruby_560 (まとめ) | 「ここまでで学んだ `puts` のコードは TryRuby でもそのまま動くよ」+ 外部リンクボタン | + +各 deck は Lv1 のみ (= `intro-getting-started` 同様、コード挿入で動くものを体験するスタイル) でスタートし、必要に応じて Lv2 (ブロック組み立て) / Lv3 (応用) を追加できる構造にする。**Lv2/Lv3 は Phase 2 のスコープ外** とし、まずは Lv1 のみで「Ruby だけで一通り学べる」体験を提供する。 + +### Smalruby 固有命令との関係 + +`puts` を中核に据えるが、deck の発展ステップでは **`move`, `turn_right`, `bounce_if_on_edge` などの Smalruby スプライト命令** を組み合わせる例も入れて、「Ruby の文法 + Smalruby ならではの絵が動く」という両面を見せる。たとえば `ruby-basics-5-blocks` の発展ステップで: + +```ruby +5.times do |i| + puts "step #{i}" + move(20) +end +``` + +のように `puts` で進捗をしゃべりながらネコを動かす、といった構成を取る。 + +### TryRuby 導線の実装 + +`ruby-basics-7-next` の最終ステップで: +- 「Smalruby で書いた `puts` のコードは、TryRuby や本物の Ruby でもそのまま動くよ」と橋渡しメッセージ +- 「TryRuby を開く」ボタン (新規タブで https://try.ruby-lang.org/ja/ を開く) +- step 構造は既存の `deckIds` ナビゲーション拡張で実装するか、`code` フィールドを使わず外部 URL を持つ新しい step プロパティ (`externalUrl`) を追加するかは実装時に判断。後者の場合 `cards.jsx` の対応も必要。 + +### 画像戦略 + +- Lv1 のみなので 1 deck あたり 5〜7 ステップ × 7 deck ≒ **35〜50 枚**。 +- Playwright MCP + Monaco エディタの自動キャプチャで生成 (`tutorial.md` の手順)。 +- 出力は ASCII アート的なコンソール出力ではなく、**スプライトが結果をしゃべる/動く** 形にして Smalruby らしさを出す。 + +**規模見積もり**: 2〜3 PR (deck 構造 → 画像 → 翻訳)、合計 1〜2 週間規模。 + +### 開放されている設計判断 + +- TryRuby 由来の課題を **どこまで忠実に再現**するか (TryRuby の正解判定 `answer` regex の概念は Smalruby には無いので、ゴールの示し方をどうするか)。 +- 既存 `intro-getting-started` と `ruby-basics-1-numbers` の関係 (前者を Phase 2 で削除して `ruby-basics-0-intro` にリネームするか、共存させるか)。実装時に決定する。 + +## Phase 3: Block 軸 — 書籍プロモーションを兼ねた抜粋チュートリアル + +**位置づけ**: Phase 3 のチュートリアルは、藤村健吾氏の Smalruby 書籍 (`tmp/kirakiraruby/0[1456]_*.docx` がドラフト) の **抜粋プロモーション** として設計する。書籍そのものの代替ではなく、書籍購入と組み合わせて使うことを前提にした「試食」体験。 + +### 目的と意図 + +1. **書籍販売数の増加に貢献する**。Smalruby は無料 OSS でも、関連書籍が売れることで著者の継続的なコンテンツ制作・コミュニティ支援につながる。 +2. **理想形は「先生が書籍を購入 → 児童に配布 → 書籍とチュートリアルを並行して進める」**。チュートリアルだけでも完結はするが、深い理解には書籍が必要、という設計にする。 +3. **書籍コンテンツの引用であることを明示**。各 deck の冒頭・末尾で書誌情報を表示し、書籍ページへの導線を置く。 + +### スコープ削減ルール (重要) + +各章 (= 各カテゴリ) のチュートリアル対象は、**書籍の「前半部分の一部」だけ** に絞る。章全体や 【発展】 セクションをカバーしない: + +- **【基本】 セクションの中で、最初の数小節だけ** を Lv0 / Lv2 / Lv3 で扱う。途中で「続きは書籍の第n章で」と打ち切る。 +- **【発展】 セクションは Lv0 のみ** を用意する (= 完成コードを `code` で挿入して「こう動くよ」と見せるだけ)。Lv2 / Lv3 の解説は提供しない。「詳しい作り方は書籍を見てね」で誘導。 + +### Lv 体系 (Mesh の Lv1/Lv2/Lv3 とは別の番号付け) + +書籍ベース系列は意図的に **Lv0 / Lv2 / Lv3** という番号で運用し、**Lv1 を「書籍を読んで理解する段階」として明示的に空ける**: + +| Lv | 場所 | 内容 | +|---|---|---| +| **Lv0** | チュートリアル | コード挿入で動くものを体験 (最も簡単・最初の入り口) | +| Lv1 | (書籍) | コードの意味・なぜ動くかの解説 — チュートリアルでは扱わず、書籍で学ぶ | +| **Lv2** | チュートリアル | ブロックを自分で組み立てる (書籍を読んだ前提) | +| **Lv3** | チュートリアル | Ruby で書く (書籍を読んだ前提) | + +この欠番構造によって「次の段階の理解は書籍にある」というメッセージを **deck の構造そのもの**で表現する。既存の Mesh `chat-*` 系列の Lv1/Lv2/Lv3 とは独立した番号付けで、ユーザーが「Mesh の Lv1」と「Block の Lv0」を混同しないよう各 deck のタイトルで明示する。 + +### deck マッピング + +各章は **基本 (一部抜粋) 3 deck + 発展 1 deck = 4 deck**、合計 4 章 × 4 deck = **16 deck**: + +| カテゴリ | 章 | 基本 Lv0 | 基本 Lv2 | 基本 Lv3 | 発展 Lv0 | 題材 | +|---|---|---|---|---|---|---| +| `blockBasics` | 第1章 | `block-basics-lv0` | `block-basics-lv2` | `block-basics-lv3` | `block-basics-advanced` | 【基本】ネコからにげるゲーム冒頭の数小節 / 【発展】ネズミ追加 (動くデモのみ) | +| `blockGames` | 第4章 | `block-shooting-lv0` | `block-shooting-lv2` | `block-shooting-lv3` | `block-shooting-advanced` | 【基本】「ネコを上下に動かす」「ネコがタマをうつ」あたりまで / 【発展】クローンで敵増殖 (動くデモのみ) | +| `blockMath` | 第5章 | `block-math-lv0` | `block-math-lv2` | `block-math-lv3` | `block-math-advanced` | 【基本】正三角形・正方形をかく節まで / 【発展】幾何学模様・色変更 (動くデモのみ) | +| `blockScience` | 第6章 | `block-science-lv0` | `block-science-lv2` | `block-science-lv3` | `block-science-advanced` | 【基本】文字表示・加速度センサーで「動いた」検出まで / 【発展】だるまさんが転んだ (動くデモのみ) | + +各 deck の最終ステップで **「ここまでの解説は書籍 第n章の冒頭部分です。続きは書籍をご覧ください」** のメッセージ + 書籍へのリンクを表示する。 + +### deck 内に組み込む書籍プロモーション要素 + +#### 1. オープニングステップ (deck 開始時) + +``` +📖 「キラキラRuby」(藤村健吾 著) 第n章 より + このチュートリアルでは本書 第n章の冒頭部分を体験できます +``` + +書誌情報を表示 (タイトル仮称: 「キラキラRuby」、著者、章番号)。`step` に `book` プロパティを追加するか、`title` 内にこの情報を含めるかは実装時に決定。 + +#### 2. クロージングステップ (deck 終了時、共通) + +``` +ここまでで体験したのは本書 第n章の最初の数ページの内容です。 +このあとは「○○」「△△」と続いていきますが、詳しい解説は書籍をご覧ください。 + +📖 書籍を購入する → [リンク] +👨‍🏫 先生・保護者の方へ: + 本書を購入して児童・お子さんに配布し、チュートリアルと並行して + 進めると、より深く理解できます。 +``` + +#### 3. 発展 deck の Lv0 のみクロージング + +``` +ここで動かしたプログラムは本書 第n章 【発展】 セクションのものです。 +このプログラムをゼロから自分で作る手順は、本書をご覧ください。 + +📖 書籍を購入する → [リンク] +``` + +### 第6章 (理科) の特殊事情 + +- **microbitMore 拡張** に依存。チュートリアル開始時に拡張を自動ロード (`extensionId` プロパティを step に持たせる) するか、利用者に手動でロードしてもらうかは実装時に判断。`allowedBlocks` に microbitMore のブロックを含める。 +- **Scratch Link / Bluetooth** が必要なステップは「画面で説明するだけで、実機なしでも学習が成立する」設計にする。 +- 既存の `docs/extension-microbit-more/README.md` と整合させる。 + +### 第5章 (算数) の特殊事情 + +- **ペン拡張**に依存。同上で `allowedBlocks` にペンブロックを含める。 +- 正多角形の角度計算 (`360 / n`) を扱う step は **算数の学習指導要領との接続** を意識した解説を入れる (ただし詳細は書籍委ね)。 + +### 画像戦略 (段階的に作る) — スコープ削減版 + +書籍前半の一部に絞ったことと、発展 deck が Lv0 のみになったことで画像数を大幅圧縮できる: + +- **基本 Lv0** (4 deck × ~5 ステップ) = 20 枚 — 動かしてみるだけのシンプル構成 +- **基本 Lv2** (4 deck × ~6 ステップ) = 24 枚 — `blocks-screenshot.js` でブロック画像生成 +- **基本 Lv3** (4 deck × ~5 ステップ) = 20 枚 — Monaco エディタスクリーンショット +- **発展 Lv0** (4 deck × ~3 ステップ) = 12 枚 — 完成デモを見せる程度 + +合計 **約 76 枚**。最初の見積もり (~100 枚) から 25% 削減。 + +**規模見積もり**: 1 章 (基本 3 deck + 発展 1 deck = 4 deck) で 1 PR、合計 4 PR。書籍引用文言の調整も含めて 3〜5 週間規模。 + +### 開放されている設計判断 + +- **書籍の正式タイトル・出版社・URL** — `tmp/kirakiraruby/` のドラフトしか手元になく、書誌情報が未確定。Phase 3 着手前にユーザーから書誌情報を提供してもらう必要がある (タイトル、著者、出版社、ISBN、購入リンクまたは公式案内ページ)。 +- **書籍引用の権利関係** — チュートリアルに書籍のサンプルコード・章タイトル・引用文を載せることについて、著者本人 (藤村健吾氏) の許諾を得る必要がある。藤村氏との合意フローを Phase 3 着手前に通す。 +- **書籍リンクの保守** — 書籍ページ URL が変わると 4 deck × 2 ステップ = 8 箇所の修正が必要。共通定数化 (例: `BOOK_URL` を 1 箇所で定義) して保守しやすくする。 +- **「先生向け」「保護者向け」メッセージの粒度** — クロージングステップに含めるか、別経路 (Welcome モーダルなど) で出すか。 +- **`intro-getting-started` (既存) と `block-basics-lv0` (新) の前後関係** — Welcome モーダルの「Build with blocks」CTA をクリックしたら `block-basics-lv0` が開くようにしたい (現状は tipsLibrary を開くだけ)。Welcome → 特定 deck 直接起動の機構を導入するかは別議論。 + +## Phase 4: DNCL 軸 — 高校生向け + +**目標**: `dnclBasics` (5〜6 deck) + `dnclAlgorithms` (5〜7 deck) を追加。共通テスト「情報Ⅰ」対策の入り口になる。 + +### 学習者像 + +- **大学入学共通テスト「情報Ⅰ」を受験する高校生** +- 大半は **初めてテキストでプログラミングをする** 想定 +- 既に DNCL モードでサンプルプログラムを読めるが、自力で書き上げる経験が乏しい + +### `dnclBasics` (基礎技能) + +| Deck ID | 内容 | DNCL 機能 | +|---|---|---| +| `dncl-basics-1-display` | 文字や数字を表示する | `表示する` | +| `dncl-basics-2-variables` | 変数を使う | `← 代入` / `=` 比較 | +| `dncl-basics-3-conditionals` | もし〜ならば | `もし ... ならば:` / `そうでなければ` | +| `dncl-basics-4-loops` | 繰り返す | `を繰り返す` / `の間繰り返す:` / `増やしながら繰り返す:` | +| `dncl-basics-5-arrays` | 配列を使う | 添字、ループとの組み合わせ | +| `dncl-basics-6-functions` | 関数を定義する | `NAME(ARGS) を定義する` | + +**進め方**: Lv 分けはせず、各 deck で「DNCL コードを Ruby タブに貼り付け → 実行 → 一部を変更して挙動を確認」のワンパスとする。Lv 分けは将来の課題。 + +### `dnclAlgorithms` (アルゴリズム + 試作問題) + +[nodai2hitc.github.io/ictl_example](https://nodai2hitc.github.io/ictl_example/) の DNCLv2 サンプル 19 題から、共通テスト頻出パターンを抽出: + +| Deck ID | 題材 | 元サンプル | アルゴリズム概念 | +|---|---|---|---| +| `dncl-alg-1-fizzbuzz` | FizzBuzz | サンプル 13 | 複合条件分岐、剰余 | +| `dncl-alg-2-frequency` | 出現頻度を数える | サンプル 10 | カウンタ配列、線形探索 | +| `dncl-alg-3-caesar` | シーザー暗号復号 | サンプル 11, 12 | モジュロ演算、文字配列 | +| `dncl-alg-4-binsearch` | 二分探索 | サンプル 19 | 分割探索 (重要: 試作問題で頻出) | +| `dncl-alg-5-greedy-coins` | 硬貨枚数 (欲張り法) | サンプル 17 | 貪欲法、降順反復 | +| `dncl-alg-6-trial-exam` | 試作問題 第2問 (得票配分) | サンプル 14〜16 | 比例配分、最大剰余法、制約付き集計 | + +### 試作問題対応 + +[文部科学省 試作問題 (R3.10)](https://www.mext.go.jp/content/20211014-mxt_daigakuc02-000018441_9.pdf) **第2問** をゴールに設定し、`dncl-alg-6-trial-exam` で完答できることを目標とする。デザイン時に PDF の問題文を解析して、必要な前提知識を `dnclBasics` + `dnclAlgorithms` の前半 deck でカバーできているか確認する。 + +### 共通テスト対策方針 ([四谷学院解説](https://www.yotsuyagakuin.com/b_geneki/kyotsu-test-jouhou/) より) + +- 試験は 60 分 / 100 点で 40 題程度を解く → **時間内に読解 + 実行を回せる** 力を養う +- プログラミング/データ活用で配点 50% → アルゴリズム deck の比重を上げる +- 「丸暗記より体系的理解」 → 各 deck で 「**なぜ動くか**」 を必ず説明する step を入れる + +### 教育研究との接続 + +[IPSJ DP64-U06 (CFRP を基準とした分析)](https://www.ipsj.or.jp/dp/contents/publication/64/DP64-U06.html) を参考に、教育的に必要な概念を deck の説明文に反映する。具体的な抽出は実装フェーズで行う。 + +### 画像戦略 + +- DNCL は文字主体なので、ブロック画像は不要 (Ruby タブのエディタスクリーンショット中心)。 +- Monaco エディタの DNCL モードハイライト + 実行結果を並べた 2 ペイン構成を想定。 +- 1 deck あたり 5〜6 ステップ × 12 deck = **約 70 枚**。 + +**規模見積もり**: `dnclBasics` で 1 PR、`dnclAlgorithms` で 1〜2 PR、合計 3〜4 週間規模。 + +### 開放されている設計判断 + +- DNCL モードへの **自動切替** をチュートリアル開始時に行うか、利用者に手動で切り替えてもらうか。前者なら `tab` + `rubyMode` URL パラメータと同等の仕組みを step に持たせる必要がある。 +- 試作問題第2問の **完全な解答を deck で示す** か、誘導しつつ最終解答は学習者に委ねるか。教育的には後者だが、自学自習用ツールとしては前者の方が完結する。 + +## 共通の実装方針 + +### チュートリアル起動時の環境セットアップ (横断機能) + +各 Phase のチュートリアルは「Ruby タブを開いた状態」「DNCL モードで」「ペン拡張を有効化した上で」など、**特定の環境前提**を必要とする。利用者が手動で正しい状態に持っていく負担をなくすため、**deck 起動時に必要なモード遷移と拡張機能ロードを自動実行**できる仕組みを導入する。 + +#### 新規 deck プロパティ: `setup` + +deck 定義に `setup` オブジェクトを追加し、起動時の前提条件を宣言する: + +```javascript +'deck-id': { + setup: { + tab: 'ruby', // 'code' | 'costumes' | 'sounds' | 'ruby' + rubyMode: 'dncl', // 'ruby' | 'furigana' | 'dncl' (tab='ruby' のときのみ有効) + extensions: ['pen', 'microbitMore'], // 拡張機能 ID 配列、未ロードのものを自動ロード + rubyVersion: 2, // 1 | 2 (省略時は現状維持) + }, + name: , + steps: [ ... ] +} +``` + +`setup` は **deck 単位** で 1 回適用される (step 単位ではない)。チュートリアル中で別タブに移動するなどの操作は step の `animationTarget` で別途扱う。 + +#### 実装方針 + +`tips-library.jsx` の `handleItemSelect` で `onActivateDeck` を dispatch する直前に `setup` を適用するヘルパーを呼ぶ。または `activateDeck` action 自体を thunk 化して setup を内包させる。具体的には: + +1. **`activateTab(setup.tab)` を dispatch** (editor-tab reducer) — `setup.tab` 指定時のみ。 +2. **Ruby モード切替**: + - `setup.rubyMode === 'dncl'` → `dispatch(setDnclMode(true))` + - `setup.rubyMode === 'furigana'` → `dispatch(setDnclMode(false))` + ふりがな enabled flag を true に + - `setup.rubyMode === 'ruby'` → `dispatch(setDnclMode(false))` + ふりがな enabled flag を false に +3. **拡張機能ロード**: + - `vm.extensionManager.isExtensionLoaded(extId)` で現在状態を確認 + - 未ロードなら `await vm.extensionManager.loadExtensionURL(extId)` を順次実行 (Promise 直列) + - 全拡張のロードが終わってから 1 ステップ目を表示 +4. **Ruby バージョン切替** (オプション、`setup.rubyVersion` 指定時のみ): + - 既存の `ruby_version` URL パラメータと同じ reducer を利用 (`settings.js` の該当 action) + +#### 設計上の考慮点 + +- **冪等性**: 既に正しい状態のときは何もしない (拡張が既にロード済み、タブが既に同じ、など)。これにより同じ deck を再起動しても安全。 +- **エラーハンドリング**: 拡張ロードが失敗した場合は、deck を開始しつつ「拡張機能のロードに失敗しました。手動でロードしてから再度お試しください」と step 内で警告する (拡張不要の deck は影響を受けない)。 +- **ロード中の UX**: 拡張ロードに数秒かかる場合、deck カードを表示する前にローディングインジケータを出す。 +- **既存 deck への影響**: 既存 10 deck は `setup` を持たないため、フィールド未定義時は **何も実行しない** (現状動作を維持)。後方互換あり。 +- **Welcome モーダル CTA との接続**: `setup` を持つ deck を welcome モーダルの 3 つの CTA に紐付ければ、「Build with blocks → Code タブで `block-basics-lv0` 起動」が 1 クリックで成立する。 + +#### Phase 別の具体例 + +| Phase | deck 例 | 必要な setup | +|---|---|---| +| Phase 1 (Mesh 再分類) | 既存 `chat-*` | 変更なし (setup なし) | +| Phase 2 (Ruby) | `ruby-basics-1-numbers` | `tab: 'ruby', rubyMode: 'ruby'` | +| Phase 3 (Block 第5章) | `block-math-lv0` | `tab: 'code', extensions: ['pen']` | +| Phase 3 (Block 第6章) | `block-science-lv0` | `tab: 'code', extensions: ['microbitMore']` | +| Phase 3 (Lv3 全般) | `block-*-lv3` | `tab: 'ruby', rubyMode: 'ruby'` | +| Phase 4 (DNCL) | `dncl-basics-1-display` | `tab: 'ruby', rubyMode: 'dncl'` | + +#### 実装順序 + +この基盤は **Phase 1 完了直後 (= Phase 2 着手前)** に独立 PR として実装する。Phase 2 以降のすべての新規 deck がこれに依存する。 + +#### 開放されている設計判断 + +- **「furigana」モードの内部状態**: 既存 `dncl-mode` reducer は二値 (dncl on/off) のみ。furigana 表示は別のフラグ (`smalruby:furiganaEnabled` localStorage) で管理されているため、`setup.rubyMode = 'furigana'` を実現するには両方の state を同時に更新する必要がある。 +- **拡張ロードに失敗した deck の振る舞い**: deck 開始を中止するか、警告付きで開始するか。教育用途では「とにかく動かしてもらう」優先で後者を採用する想定。 +- **`setup` を deck から step 単位に拡張するか**: 将来「途中で拡張を追加で有効化したい」ケースが出たら検討。当面は不要。 + +### Deck 命名規約 + +- ID: `--` (例: `mesh-step1-lv1`, `block-basics-lv2`, `dncl-alg-binsearch`)。 +- 既存の `chat-1-basic-1` などは Phase 1 では **リネームせず**、category 参照のみ更新する (URL / 履歴互換性のため)。 +- 新規 deck では上記命名規約を採用。 + +### message ID 規約 + +- カテゴリ: `gui.libraryCategories.` (例: `gui.libraryCategories.meshStep1`) +- deck 名: `gui.howtos..name` +- step タイトル: `gui.howtos..stepN.title` + +### locale 更新 + +すべての変更で `ja.js` / `ja-Hira.js` / `en.js` の 3 ファイルを必ず更新する (`smalruby-prettier-files.md` 記載のファイル)。 + +### Prettier ホワイトリスト + +`decks/index.jsx`, `tutorial-tags.js`, `tag-messages.js`, `tips-library.jsx` は既に Smalruby ホワイトリストの一部ではない (upstream ファイル)。新規 step 画像ディレクトリは Prettier 対象外なので追加不要。 + +### Smalruby マーカー + +`decks/index.jsx` / `tutorial-tags.js` / `tag-messages.js` は upstream ファイルなので、新規追加部分のうち **Smalruby 独自カテゴリ・deck** はマーカーで囲む。`.claude/rules/scratch-gui/smalruby-markers.md` に追記する。 + +### テスト + +- ユニット: 既存 `test/unit/lib/libraries/decks/` (もしあれば) を確認しつつ、新 deck の `name` / `steps` / `category` が undefined にならない smoke test を追加。 +- インテグレーション: 各 Phase の最後に Playwright MCP で「カテゴリ表示が意図通り」「deck を開いて 1 step 進める」「Lv1 → Lv2 ナビゲーションが繋がる」を確認。 + +## Phase 進行順とリリース粒度 + +1. **Phase 1: Mesh 再分類** — 1 PR、画像なし、リスク低。最初に commit する。 +2. **基盤 PR: `setup` プロパティ導入** — deck 起動時のタブ・モード・拡張機能の自動セットアップ機構を実装。新規 deck 無し、共通基盤のみ。Phase 2 以降の前提となる。 +3. **Phase 2: Ruby 拡充** — `ruby-basics-1-numbers` 単体 PR で構造を確立 → 残り 5 deck を deck 数本ずつ PR。`setup.tab='ruby'` を使用。 +4. **Phase 3: Block 4 系列** — 1 章 (基本 3 deck + 発展 1 deck = 4 deck) で 1 PR、合計 4 PR。`blockBasics` → `blockGames` → `blockMath` → `blockScience` の順。`blockMath` 以降で `setup.extensions` を活用 (ペン / microbitMore)。書籍引用文言と書籍リンクの統一処理も含む。 +5. **Phase 4: DNCL** — `dnclBasics` 全 6 deck で 1 PR、`dnclAlgorithms` を 2〜3 PR に分割。`setup.rubyMode='dncl'` で起動時に DNCL モードへ自動遷移。試作問題 deck は最後。 + +各 Phase の完了時に **Welcome モーダルの該当 CTA が新しい deck を開くように接続**する (現状は tipsLibrary を開くだけ → 特定 deck を直接開く改修が必要かは Phase 2 開始時に判断)。 + +## リスクと open question + +- **画像メンテナンス負荷**: 約 200 枚の step 画像。UI 改修のたびに大量の撮り直しが発生する。Lv1 を増やしすぎないようにスコープ管理する。 +- **`library.jsx` のカテゴリ並び順制御**: 現状の挙動 (オブジェクトキー順 or 出現順) を Phase 1 で確認し、明示的な順序配列を入れるかを決定する。 +- **Welcome モーダル ↔ 特定 deck の連携**: 現在 `onStartTutorial` は tipsLibrary を開くだけ。3 つの CTA (Block / Ruby / Mesh) からそれぞれ対応する Lv1 deck を直接開けるように改修するかは、Phase 1 完了後に検討。 +- **試作問題の解答提示の妥当性**: Phase 4 で文科省 PDF の解答を直接 deck に書くことの教育的妥当性 (出典明示で問題ないか) を確認する。 +- **TryRuby との重複**: Ruby 軸を充実させすぎると TryRuby と内容が被る。Smalruby は **`puts` を共通言語にしつつ「絵が動く Ruby」** に振り、TryRuby はコンソール出力で同じコードを試せる場として導線で繋げる。同じコードが両環境で動くという連続性を学習者に体験させる。 + +## 参考資料 + +- [Welcome モーダル実装](../../packages/scratch-gui/src/components/welcome-modal/welcome-modal.jsx) +- [チュートリアル機能の説明](README.md) (本ドキュメントと同ディレクトリ) +- [`.claude/rules/scratch-gui/tutorial.md`](../../.claude/rules/scratch-gui/tutorial.md) — deck 構造、画像撮影フロー、Lv1〜Lv3 のパターン +- [docs/dncl/README.md](../dncl/README.md) — DNCL モード仕様 +- [docs/extension-mesh-v2/README.md](../extension-mesh-v2/README.md) — Mesh v2 拡張 +- [docs/extension-microbit-more/README.md](../extension-microbit-more/README.md) — Phase 3 第6章で参照 +- TryRuby 日本語訳: `~/ghq/github.com/ruby/TryRuby/translations/ja/try_ruby_*.md` +- 書籍原稿: `tmp/kirakiraruby/0[1456]_*.docx` +- DNCLv2 サンプル: +- 共通テスト試作問題: +- 共通テスト「情報」対策方針: +- IPSJ 教育研究 (DP64-U06): diff --git a/docs/tutorial/progress.md b/docs/tutorial/progress.md new file mode 100644 index 00000000000..dac84d7ff3d --- /dev/null +++ b/docs/tutorial/progress.md @@ -0,0 +1,67 @@ +# チュートリアル拡充 進捗トラッカー + +> **🚧 一時ドキュメント** — チュートリアル拡充 4 Phase 完了時に削除予定。Issue [#682](https://github.com/smalruby/smalruby3-editor/issues/682) の進捗をリアルタイムに反映するためのワーキングドキュメント。 +> +> Phase の最終形・設計判断は [`improvement-plan.md`](improvement-plan.md) を参照。 + +## サマリー + +| Phase | Issue | 状態 | 規模 | 画像 | +|---|---|---|---|---| +| Phase 1 — Mesh 再分類 | [#678](https://github.com/smalruby/smalruby3-editor/issues/678) | 🟢 実装完了 (レビュー待ち) | 1 PR | 不要 | +| 基盤 — `setup` プロパティ | (Phase 2 sub-issue 内) | ⚪️ 未着手 | 1 PR | 不要 | +| Phase 2 — Ruby 拡充 | [#679](https://github.com/smalruby/smalruby3-editor/issues/679) | ⚪️ 未着手 | 2〜3 PR | ~50 枚 | +| Phase 3 — Block 4 シリーズ | [#680](https://github.com/smalruby/smalruby3-editor/issues/680) | ⚪️ 未着手 (書誌情報待ち) | 4 PR | ~76 枚 | +| Phase 4 — DNCL | [#681](https://github.com/smalruby/smalruby3-editor/issues/681) | ⚪️ 未着手 | 3〜4 PR | ~70 枚 | + +凡例: ⚪️ 未着手 / 🟡 進行中 / 🟢 完了 (PR レビュー待ち含む) / ✅ マージ済み / ❌ 中断 + +## Phase 1: Mesh チュートリアルを 3 カテゴリ × 3 lv に再分類 + +**Issue**: [#678](https://github.com/smalruby/smalruby3-editor/issues/678) / **ブランチ**: `feature/tutorial-mesh-recategorize` + +### 完了項目 + +- [x] `tutorial-tags.js` — `CATEGORIES.chatApp` を削除、`meshStep1/2/3` を追加 +- [x] `library.jsx` — local messages 定義から `[CATEGORIES.chatApp]` を削除、`[CATEGORIES.meshStep1/2/3]` を追加 +- [x] `decks/index.jsx` — 9 deck の `category:` 参照を新キーに更新 + - `chat-1-basic-{1,2,3}` → `meshStep1` + - `chat-2-sprites-{1,2,3}` → `meshStep2` + - `chat-3-mesh-{1,2,3}` → `meshStep3` +- [x] `locales/en.js` — `gui.library.chatApp` を削除、`gui.library.meshStep1/2/3` を追加 +- [x] `locales/ja.js` — 同上 (日本語ベース) +- [x] `locales/ja-Hira.js` — 同上 (ひらがなベース) +- [x] `docs/tutorial/improvement-plan.md` — Phase 1〜4 の設計を集約 +- [x] `docs/tutorial/progress.md` — 本ドキュメント新規作成 + +### 残項目 (本 PR で対応) + +- [ ] `npm run lint` でエラー・警告ゼロを確認 +- [ ] Playwright で `tipsLibrary` を開き、3 カテゴリが想定順序で表示されることを確認 +- [ ] 各カテゴリ配下に Lv1/Lv2/Lv3 が並ぶことを確認 +- [ ] PR 作成 + +### 設計判断メモ + +- **`tag-messages.js` は更新しない** — `gui.libraryCategories.*` のメッセージ ID は library.jsx の rendering path から参照されておらず、`tag-messages.js` の `gettingStarted` / `chatApp` エントリは vestigial。新カテゴリも追加しない。本物の rendering 経路 (`gui.library.*` を持つ local messages in library.jsx) のみを更新する。 +- **deck ID はリネームしない** — `chat-1-basic-1` 等の既存 ID は URL 互換性のためそのまま維持。`category:` 参照のみ更新。 +- **`tags: ['mesh']` も維持** — タグフィルタは引き続き "Mesh" 1 つで全 9 deck をまとめて絞り込めることを期待する利用者がいる可能性があるため、新カテゴリ追加と並行してタグは維持する。 + +## Phase 2 以降の TODO 抜粋 + +### 基盤 (Phase 2 前半): `setup` プロパティ + +- [ ] deck 定義の type 拡張 (`{ tab, rubyMode, extensions, rubyVersion }`) +- [ ] `tips-library.jsx` または `cards` reducer で deck 起動時に setup を適用 +- [ ] `activateTab` / `setDnclMode` / `vm.extensionManager.loadExtensionURL` の冪等な呼び出し +- [ ] ロード失敗時のグレースフルデグレード +- [ ] ふりがなフラグも考慮した rubyMode の動作 (`smalruby:furiganaEnabled` との同期) + +### Phase 3 着手前に必要 (外部要因) + +- [ ] 書籍「キラキラRuby」(仮称) の正式タイトル・出版社・ISBN・購入リンク確定 +- [ ] 著者 (藤村健吾氏) からの書籍引用許諾 + +## 削除タイミング + +全 4 Phase の Issue (#678/679/680/681) が close され、`improvement-plan.md` の内容が `docs/tutorial/README.md` 等の正規ドキュメントに統合されたら本ファイル (`progress.md`) は削除する。`improvement-plan.md` も同タイミングで削除予定。 diff --git a/packages/scratch-gui/src/components/library/library.jsx b/packages/scratch-gui/src/components/library/library.jsx index 224d614cef5..29babf3f259 100644 --- a/packages/scratch-gui/src/components/library/library.jsx +++ b/packages/scratch-gui/src/components/library/library.jsx @@ -35,10 +35,20 @@ const messages = defineMessages({ defaultMessage: 'Getting Started', description: 'Label for getting started category' }, - [CATEGORIES.chatApp]: { - id: `gui.library.chatApp`, - defaultMessage: 'Build a Chat App', - description: 'Label for chat app tutorial category' + [CATEGORIES.meshStep1]: { + id: `gui.library.meshStep1`, + defaultMessage: 'メッセージを送ってみよう', + description: 'Label for Mesh tutorial step 1 — send a message between same-sprite scripts' + }, + [CATEGORIES.meshStep2]: { + id: `gui.library.meshStep2`, + defaultMessage: 'ふたりで会話しよう', + description: 'Label for Mesh tutorial step 2 — chat between two sprites' + }, + [CATEGORIES.meshStep3]: { + id: `gui.library.meshStep3`, + defaultMessage: 'みんなで会話しよう', + description: 'Label for Mesh tutorial step 3 — chat across devices via Mesh extension' }, membershipTag: { defaultMessage: 'Membership', diff --git a/packages/scratch-gui/src/lib/libraries/decks/index.jsx b/packages/scratch-gui/src/lib/libraries/decks/index.jsx index ba015cd9efe..f8704008833 100644 --- a/packages/scratch-gui/src/lib/libraries/decks/index.jsx +++ b/packages/scratch-gui/src/lib/libraries/decks/index.jsx @@ -112,7 +112,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep1, img: libraryChat1Basic1, nameMessageId: 'gui.howtos.chat-1-basic-1.name', allowedBlocks: { @@ -242,7 +242,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep1, img: libraryChat1Basic2, nameMessageId: 'gui.howtos.chat-1-basic-2.name', allowedBlocks: { @@ -355,7 +355,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep1, img: libraryChat1Basic3, nameMessageId: 'gui.howtos.chat-1-basic-3.name', allowedBlocks: { @@ -474,7 +474,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep2, img: libraryChat2Sprites1, nameMessageId: 'gui.howtos.chat-2-sprites-1.name', allowedBlocks: { @@ -607,7 +607,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep2, img: libraryChat2Sprites2, nameMessageId: 'gui.howtos.chat-2-sprites-2.name', allowedBlocks: { @@ -729,7 +729,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep2, img: libraryChat2Sprites3, nameMessageId: 'gui.howtos.chat-2-sprites-3.name', allowedBlocks: { @@ -871,7 +871,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep3, img: libraryChat3Mesh1, nameMessageId: 'gui.howtos.chat-3-mesh-1.name', allowedBlocks: { @@ -1012,7 +1012,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep3, img: libraryChat3Mesh2, nameMessageId: 'gui.howtos.chat-3-mesh-2.name', allowedBlocks: { @@ -1131,7 +1131,7 @@ end`, /> ), tags: ['mesh'], - category: CATEGORIES.chatApp, + category: CATEGORIES.meshStep3, img: libraryChat3Mesh3, nameMessageId: 'gui.howtos.chat-3-mesh-3.name', allowedBlocks: { diff --git a/packages/scratch-gui/src/lib/libraries/tutorial-tags.js b/packages/scratch-gui/src/lib/libraries/tutorial-tags.js index c5ac361bd67..183fcf08835 100644 --- a/packages/scratch-gui/src/lib/libraries/tutorial-tags.js +++ b/packages/scratch-gui/src/lib/libraries/tutorial-tags.js @@ -2,7 +2,12 @@ import messages from './tag-messages.js'; export const CATEGORIES = { gettingStarted: 'gettingStarted', - chatApp: 'chatApp' // チャットアプリをつくろう / Build a Chat App + // Mesh tutorial series — split from the former single `chatApp` category + // into three story-themed steps so that the progression is visible in the + // tutorial library (see docs/tutorial/improvement-plan.md Phase 1). + meshStep1: 'meshStep1', // メッセージを送ってみよう + meshStep2: 'meshStep2', // ふたりで会話しよう + meshStep3: 'meshStep3' // みんなで会話しよう }; export default [ diff --git a/packages/scratch-gui/src/locales/en.js b/packages/scratch-gui/src/locales/en.js index f4aacd3b95b..fc8cffca3e4 100644 --- a/packages/scratch-gui/src/locales/en.js +++ b/packages/scratch-gui/src/locales/en.js @@ -526,7 +526,9 @@ export default { 'gui.libraryTags.mesh': 'Mesh', 'gui.libraryTags.firstTime': 'First Time', 'gui.libraryCategories.gettingStarted': 'Getting Started', - 'gui.library.chatApp': 'Build a Chat App', + 'gui.library.meshStep1': 'Send a Message', + 'gui.library.meshStep2': 'Chat Between Two Sprites', + 'gui.library.meshStep3': 'Chat Across Devices (Mesh)', 'gui.howtos.chat-1-basic-1.name': "Let's Send a Message!", 'gui.howtos.chat-1-basic-1.step1.title': "Let's send a message to a remote block!", 'gui.howtos.chat-1-basic-1.step2.title': 'First, insert the code and run the program', diff --git a/packages/scratch-gui/src/locales/ja-Hira.js b/packages/scratch-gui/src/locales/ja-Hira.js index d8138090387..542d94aa4c2 100644 --- a/packages/scratch-gui/src/locales/ja-Hira.js +++ b/packages/scratch-gui/src/locales/ja-Hira.js @@ -966,7 +966,9 @@ export default { 'gui.libraryTags.ruby': 'ルビー', 'gui.libraryTags.firstTime': 'はじめて', 'gui.libraryCategories.gettingStarted': 'はじめましょう', - 'gui.library.chatApp': 'チャットアプリをつくろう', + 'gui.library.meshStep1': 'メッセージをおくってみよう', + 'gui.library.meshStep2': 'ふたりでかいわしよう', + 'gui.library.meshStep3': 'みんなでかいわしよう', 'gui.menuBar.updateTooltip': 'あたらしいスモウルビーをつかってみよう!', 'gui.menuBar.updateConfirm': 'あたらしいバージョンのスモウルビーがりようかのうです。いますぐこうしんするばあいは「OK」を、あとにするばあいは「キャンセル」をおしてください。', diff --git a/packages/scratch-gui/src/locales/ja.js b/packages/scratch-gui/src/locales/ja.js index cac4f519b47..52fab46bdb5 100644 --- a/packages/scratch-gui/src/locales/ja.js +++ b/packages/scratch-gui/src/locales/ja.js @@ -941,7 +941,9 @@ export default { 'gui.libraryTags.ruby': 'ルビー', 'gui.libraryTags.firstTime': 'はじめて', 'gui.libraryCategories.gettingStarted': '始めましょう', - 'gui.library.chatApp': 'チャットアプリをつくろう', + 'gui.library.meshStep1': 'メッセージを送ってみよう', + 'gui.library.meshStep2': 'ふたりで会話しよう', + 'gui.library.meshStep3': 'みんなで会話しよう', 'gui.menuBar.updateTooltip': '新しいスモウルビーを使ってみよう!', 'gui.menuBar.updateConfirm': '新しいバージョンのスモウルビーが利用可能です。いますぐ更新する場合は「OK」を、あとにする場合は「キャンセル」を押してください。', diff --git a/tools/playwright-verify/verify-tutorial-mesh-categories.mjs b/tools/playwright-verify/verify-tutorial-mesh-categories.mjs new file mode 100644 index 00000000000..e095b37059a --- /dev/null +++ b/tools/playwright-verify/verify-tutorial-mesh-categories.mjs @@ -0,0 +1,74 @@ +import { chromium } from 'playwright'; + +const URL = 'http://localhost:8601/?no_beforeunload=1&welcome=1'; +const browser = await chromium.launch({ headless: true }); +const context = await browser.newContext({ + viewport: { width: 1280, height: 800 }, + locale: 'ja-JP', +}); +const page = await context.newPage(); +const log = (...args) => console.log('[verify]', ...args); + +page.on('pageerror', (err) => console.error('[pageerror]', err.message)); + +await page.goto(URL); +await page.evaluate(() => localStorage.clear()); +await page.reload(); +await page.waitForTimeout(800); + +// Welcome modal exposes a 'Start the first tutorial' CTA that opens tipsLibrary. +// First close any welcome tooltip balloon, then click the modal's start CTA. +const tooltipDismiss = page.locator('[data-testid="welcome-tooltip"] [aria-label*="閉じる"], [data-testid="welcome-tooltip"] [aria-label*="dismiss"]'); +if (await tooltipDismiss.count()) { + try { await tooltipDismiss.first().click({ force: true }); } catch {} +} + +const startBtn = page.locator('[data-testid="welcome-modal-start-tutorial"]'); +if (await startBtn.count()) { + log('clicking welcome-modal-start-tutorial'); + await startBtn.click(); +} else { + log('welcome modal CTA not found, dismiss it and use menu bar'); + const laterBtn = page.locator('[data-testid="welcome-modal-later"]'); + if (await laterBtn.count()) { + await laterBtn.click(); + await page.waitForTimeout(300); + } + // Click About menu (?) → use Tutorials in dropdown, or directly find tutorials button. + // Smalruby has a question-mark button that opens the About menu containing Tutorials. + const aboutBtn = page.locator('[aria-label="About"], [aria-label="アバウト"], [class*=menuBarItem][class*=about]').first(); + if (await aboutBtn.count()) { + await aboutBtn.click(); + await page.waitForTimeout(300); + const tutItems = await page.locator('[role=menuitem]').allTextContents(); + log('about menu items:', JSON.stringify(tutItems)); + } +} + +await page.waitForTimeout(3000); + +// Take screenshot regardless for debugging +await page.screenshot({ path: 'tmp/tutorial-mesh-categories-debug.png', fullPage: true }); + +// CSS Modules emits classes like `library_library-category-title_`. +// Find by partial match on `library-category-title`. +const titles = await page.evaluate(() => { + const nodes = document.querySelectorAll('[class*="library-category-title"]'); + return Array.from(nodes).map((n) => n.textContent.trim()); +}); +log('category titles =', JSON.stringify(titles)); + +await page.screenshot({ path: 'tmp/tutorial-mesh-categories.png', fullPage: true }); + +const expected = ['メッセージを送ってみよう', 'ふたりで会話しよう', 'みんなで会話しよう']; +const missing = expected.filter((t) => !titles.includes(t)); +if (missing.length) { + console.error('MISSING titles:', missing); + await page.screenshot({ path: 'tmp/tutorial-mesh-categories-failed.png', fullPage: true }); + await browser.close(); + process.exit(1); +} + +log('OK: all 3 mesh-step categories visible'); +await page.screenshot({ path: 'tmp/tutorial-mesh-categories.png', fullPage: true }); +await browser.close(); From 5e12ab2b0c485e82f82ccee6728c93d88434bac9 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Mon, 11 May 2026 10:21:22 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(tutorial):=20rename=20mesh=20categories?= =?UTF-8?q?=20to=20=E3=80=8C=E9=80=9A=E4=BF=A1=E5=85=A5=E9=96=80=20?= =?UTF-8?q?=E2=91=A0=20=E2=91=A1=20=E2=91=A2=E3=80=8D=20series?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR レビューでカテゴリの連続性が伝わらない指摘を受け、ストーリー型単独名から 共通プレフィックス「通信入門」+ 番号付きシリーズ名に変更。3 カテゴリが同じ シリーズの段階的なステップであることを名前から一目で識別できるようにする。 カテゴリ表記: - meshStep1: 通信入門 ① メッセージを送ってみよう - meshStep2: 通信入門 ② ふたりで会話しよう - meshStep3: 通信入門 ③ みんなで会話しよう (メッシュ) 合わせて Step 3 の Lv1 deck 名を「メッシュ拡張機能でつながろう」から 「メッシュでつながろう」に短縮し、Lv2 / Lv3 の表記と揃える。 - library.jsx, locales/{en,ja,ja-Hira}.js: 新カテゴリラベル / Lv1 deck 名 - tutorial-tags.js, decks/index.jsx: コメント表記の整合 - docs/tutorial/improvement-plan.md, progress.md: 命名理由を追記 - verify-tutorial-mesh-categories.mjs: 期待値を新表記に更新 Refs: #678, #682, #683 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/tutorial/improvement-plan.md | 16 ++++++++-------- docs/tutorial/progress.md | 1 + .../src/components/library/library.jsx | 6 +++--- .../src/lib/libraries/decks/index.jsx | 6 +++--- .../src/lib/libraries/tutorial-tags.js | 11 ++++++----- packages/scratch-gui/src/locales/en.js | 8 ++++---- packages/scratch-gui/src/locales/ja-Hira.js | 8 ++++---- packages/scratch-gui/src/locales/ja.js | 8 ++++---- .../verify-tutorial-mesh-categories.mjs | 6 +++++- 9 files changed, 38 insertions(+), 32 deletions(-) diff --git a/docs/tutorial/improvement-plan.md b/docs/tutorial/improvement-plan.md index 594acddc5f0..db565dec944 100644 --- a/docs/tutorial/improvement-plan.md +++ b/docs/tutorial/improvement-plan.md @@ -45,10 +45,10 @@ export const CATEGORIES = { // 既存 gettingStarted: 'gettingStarted', - // Phase 1: chatApp を 3 つに分割 (ストーリー型) - meshStep1: 'meshStep1', // メッセージを送ってみよう - meshStep2: 'meshStep2', // ふたりで会話しよう - meshStep3: 'meshStep3', // みんなで会話しよう + // Phase 1: chatApp を 3 つに分割 (「通信入門」シリーズ番号付け型) + meshStep1: 'meshStep1', // 通信入門 ① メッセージを送ってみよう + meshStep2: 'meshStep2', // 通信入門 ② ふたりで会話しよう + meshStep3: 'meshStep3', // 通信入門 ③ みんなで会話しよう (メッシュ) // Phase 3: Block 軸 (教科ラベル型) blockBasics: 'blockBasics', // はじめての操作 @@ -90,10 +90,10 @@ export const CATEGORIES = { **ファイル変更**: 1. `tutorial-tags.js` — `chatApp` 削除、`meshStep1/2/3` を追加。 -2. `tag-messages.js` — `gui.libraryCategories.meshStep1/2/3` を追加。 - - `meshStep1`: 「メッセージを送ってみよう」 - - `meshStep2`: 「ふたりで会話しよう」 - - `meshStep3`: 「みんなで会話しよう」 +2. `tag-messages.js` — `gui.libraryCategories.meshStep1/2/3` を追加。3 カテゴリは「通信入門」シリーズとして番号付けし、連続性を可視化する: + - `meshStep1`: 「通信入門 ① メッセージを送ってみよう」(同じスプライト内で broadcast) + - `meshStep2`: 「通信入門 ② ふたりで会話しよう」(2 スプライト間で broadcast) + - `meshStep3`: 「通信入門 ③ みんなで会話しよう (メッシュ)」(複数デバイス間で Mesh 通信) 3. `decks/index.jsx` — 9 deck の `category` 参照を以下にマップ: - `chat-1-basic-{1,2,3}` → `CATEGORIES.meshStep1` - `chat-2-sprites-{1,2,3}` → `CATEGORIES.meshStep2` diff --git a/docs/tutorial/progress.md b/docs/tutorial/progress.md index dac84d7ff3d..1d2784ed0ec 100644 --- a/docs/tutorial/progress.md +++ b/docs/tutorial/progress.md @@ -46,6 +46,7 @@ - **`tag-messages.js` は更新しない** — `gui.libraryCategories.*` のメッセージ ID は library.jsx の rendering path から参照されておらず、`tag-messages.js` の `gettingStarted` / `chatApp` エントリは vestigial。新カテゴリも追加しない。本物の rendering 経路 (`gui.library.*` を持つ local messages in library.jsx) のみを更新する。 - **deck ID はリネームしない** — `chat-1-basic-1` 等の既存 ID は URL 互換性のためそのまま維持。`category:` 参照のみ更新。 - **`tags: ['mesh']` も維持** — タグフィルタは引き続き "Mesh" 1 つで全 9 deck をまとめて絞り込めることを期待する利用者がいる可能性があるため、新カテゴリ追加と並行してタグは維持する。 +- **カテゴリ名は「通信入門」共通プレフィックス + ① ② ③ 番号付け** — PR レビューで「3 カテゴリの関係性が伝わらない」と指摘を受け、ストーリー型単独名から番号付きシリーズ名に変更。Step 3 の Lv1 deck 名も「メッシュ拡張機能でつながろう」→「メッシュでつながろう」に短縮し Lv2/Lv3 と表記揃え。 ## Phase 2 以降の TODO 抜粋 diff --git a/packages/scratch-gui/src/components/library/library.jsx b/packages/scratch-gui/src/components/library/library.jsx index 29babf3f259..49bee118e5f 100644 --- a/packages/scratch-gui/src/components/library/library.jsx +++ b/packages/scratch-gui/src/components/library/library.jsx @@ -37,17 +37,17 @@ const messages = defineMessages({ }, [CATEGORIES.meshStep1]: { id: `gui.library.meshStep1`, - defaultMessage: 'メッセージを送ってみよう', + defaultMessage: '通信入門 ① メッセージを送ってみよう', description: 'Label for Mesh tutorial step 1 — send a message between same-sprite scripts' }, [CATEGORIES.meshStep2]: { id: `gui.library.meshStep2`, - defaultMessage: 'ふたりで会話しよう', + defaultMessage: '通信入門 ② ふたりで会話しよう', description: 'Label for Mesh tutorial step 2 — chat between two sprites' }, [CATEGORIES.meshStep3]: { id: `gui.library.meshStep3`, - defaultMessage: 'みんなで会話しよう', + defaultMessage: '通信入門 ③ みんなで会話しよう (メッシュ)', description: 'Label for Mesh tutorial step 3 — chat across devices via Mesh extension' }, membershipTag: { diff --git a/packages/scratch-gui/src/lib/libraries/decks/index.jsx b/packages/scratch-gui/src/lib/libraries/decks/index.jsx index f8704008833..032ca97e5f1 100644 --- a/packages/scratch-gui/src/lib/libraries/decks/index.jsx +++ b/packages/scratch-gui/src/lib/libraries/decks/index.jsx @@ -12,7 +12,7 @@ import libraryChat1Basic3 from './thumbnails/chat-1-basic-3.jpg'; import libraryChat2Sprites1 from './thumbnails/chat-2-sprites-1.jpg'; import libraryChat2Sprites2 from './thumbnails/chat-2-sprites-2.jpg'; import libraryChat2Sprites3 from './thumbnails/chat-2-sprites-3.jpg'; -// Chat Tutorial 3 Mesh 1: メッシュ拡張機能でつながろう +// Chat Tutorial 3 Mesh 1: メッシュでつながろう import libraryChat3Mesh1 from './thumbnails/chat-3-mesh-1.jpg'; import libraryChat3Mesh2 from './thumbnails/chat-3-mesh-2.jpg'; import libraryChat3Mesh3 from './thumbnails/chat-3-mesh-3.jpg'; @@ -861,11 +861,11 @@ end`, urlId: 'chat2Sprites3' }, - // ─── Chat Tutorial 3 Mesh 1: メッシュ拡張機能でつながろう ─────────────── + // ─── Chat Tutorial 3 Mesh 1: メッシュでつながろう ─────────────── 'chat-3-mesh-1': { name: ( diff --git a/packages/scratch-gui/src/lib/libraries/tutorial-tags.js b/packages/scratch-gui/src/lib/libraries/tutorial-tags.js index 183fcf08835..2cbe31e1b42 100644 --- a/packages/scratch-gui/src/lib/libraries/tutorial-tags.js +++ b/packages/scratch-gui/src/lib/libraries/tutorial-tags.js @@ -3,11 +3,12 @@ import messages from './tag-messages.js'; export const CATEGORIES = { gettingStarted: 'gettingStarted', // Mesh tutorial series — split from the former single `chatApp` category - // into three story-themed steps so that the progression is visible in the - // tutorial library (see docs/tutorial/improvement-plan.md Phase 1). - meshStep1: 'meshStep1', // メッセージを送ってみよう - meshStep2: 'meshStep2', // ふたりで会話しよう - meshStep3: 'meshStep3' // みんなで会話しよう + // into three numbered "通信入門" steps so that the sequential nature is + // visible in the tutorial library (see docs/tutorial/improvement-plan.md + // Phase 1). + meshStep1: 'meshStep1', // 通信入門 ① メッセージを送ってみよう + meshStep2: 'meshStep2', // 通信入門 ② ふたりで会話しよう + meshStep3: 'meshStep3' // 通信入門 ③ みんなで会話しよう (メッシュ) }; export default [ diff --git a/packages/scratch-gui/src/locales/en.js b/packages/scratch-gui/src/locales/en.js index fc8cffca3e4..1a1d3d9a320 100644 --- a/packages/scratch-gui/src/locales/en.js +++ b/packages/scratch-gui/src/locales/en.js @@ -526,9 +526,9 @@ export default { 'gui.libraryTags.mesh': 'Mesh', 'gui.libraryTags.firstTime': 'First Time', 'gui.libraryCategories.gettingStarted': 'Getting Started', - 'gui.library.meshStep1': 'Send a Message', - 'gui.library.meshStep2': 'Chat Between Two Sprites', - 'gui.library.meshStep3': 'Chat Across Devices (Mesh)', + 'gui.library.meshStep1': 'Intro to Communication ① Send a Message', + 'gui.library.meshStep2': 'Intro to Communication ② Chat Between Two Sprites', + 'gui.library.meshStep3': 'Intro to Communication ③ Chat Across Devices (Mesh)', 'gui.howtos.chat-1-basic-1.name': "Let's Send a Message!", 'gui.howtos.chat-1-basic-1.step1.title': "Let's send a message to a remote block!", 'gui.howtos.chat-1-basic-1.step2.title': 'First, insert the code and run the program', @@ -596,7 +596,7 @@ export default { 'gui.howtos.chat-2-sprites-3.step7.title': 'Press "Insert Ruby code" for Penguin — message names are reversed!', 'gui.howtos.chat-2-sprites-3.step8.title': 'Click Cat or Penguin to run!', // Chat Tutorial 3 Mesh 1 - 'gui.howtos.chat-3-mesh-1.name': "Let's Connect with the Mesh Extension!", + 'gui.howtos.chat-3-mesh-1.name': "Let's Connect with Mesh!", 'gui.howtos.chat-3-mesh-1.step1.title': "Connect to other people's Smalruby using the Mesh extension", 'gui.howtos.chat-3-mesh-1.step2.title': 'Form a group and select the Mesh extension (even one person can do it with two Smalruby windows)', diff --git a/packages/scratch-gui/src/locales/ja-Hira.js b/packages/scratch-gui/src/locales/ja-Hira.js index 542d94aa4c2..157c44a179c 100644 --- a/packages/scratch-gui/src/locales/ja-Hira.js +++ b/packages/scratch-gui/src/locales/ja-Hira.js @@ -912,7 +912,7 @@ export default { 'ペンギンのRubyコードを「ルビーをにゅうりょくする」でにゅうりょくしよう — メッセージめいがネコとぎゃくになっているよ', 'gui.howtos.chat-2-sprites-3.step8.title': 'ネコやペンギンをおしてじっこうしよう!', // Chat Tutorial 3 Mesh 1 - 'gui.howtos.chat-3-mesh-1.name': 'メッシュかくちょうきのうでつながろう', + 'gui.howtos.chat-3-mesh-1.name': 'メッシュでつながろう', 'gui.howtos.chat-3-mesh-1.step1.title': 'メッシュかくちょうきのうをつかってほかのひとのスモウルビーとつながろう', 'gui.howtos.chat-3-mesh-1.step2.title': '2にんいじょうのグループをつくってメッシュかくちょうきのうをえらぶ(1にんでも2つのスモウルビーをつかえばできる)', @@ -966,9 +966,9 @@ export default { 'gui.libraryTags.ruby': 'ルビー', 'gui.libraryTags.firstTime': 'はじめて', 'gui.libraryCategories.gettingStarted': 'はじめましょう', - 'gui.library.meshStep1': 'メッセージをおくってみよう', - 'gui.library.meshStep2': 'ふたりでかいわしよう', - 'gui.library.meshStep3': 'みんなでかいわしよう', + 'gui.library.meshStep1': 'つうしんにゅうもん ① メッセージをおくってみよう', + 'gui.library.meshStep2': 'つうしんにゅうもん ② ふたりでかいわしよう', + 'gui.library.meshStep3': 'つうしんにゅうもん ③ みんなでかいわしよう (メッシュ)', 'gui.menuBar.updateTooltip': 'あたらしいスモウルビーをつかってみよう!', 'gui.menuBar.updateConfirm': 'あたらしいバージョンのスモウルビーがりようかのうです。いますぐこうしんするばあいは「OK」を、あとにするばあいは「キャンセル」をおしてください。', diff --git a/packages/scratch-gui/src/locales/ja.js b/packages/scratch-gui/src/locales/ja.js index 52fab46bdb5..80d5953f648 100644 --- a/packages/scratch-gui/src/locales/ja.js +++ b/packages/scratch-gui/src/locales/ja.js @@ -885,7 +885,7 @@ export default { 'ペンギンのRubyコードを「ルビーを入力する」で入力しよう — メッセージ名がネコと逆になっているよ', 'gui.howtos.chat-2-sprites-3.step8.title': 'ネコやペンギンを押して実行しよう!', // Chat Tutorial 3 Mesh 1 - 'gui.howtos.chat-3-mesh-1.name': 'メッシュ拡張機能でつながろう', + 'gui.howtos.chat-3-mesh-1.name': 'メッシュでつながろう', 'gui.howtos.chat-3-mesh-1.step1.title': 'メッシュ拡張機能を使って他の人のスモウルビーとつながろう', 'gui.howtos.chat-3-mesh-1.step2.title': '2人以上のグループをつくってメッシュ拡張機能を選ぶ(1人でも2つのスモウルビーを使えばできる)', @@ -941,9 +941,9 @@ export default { 'gui.libraryTags.ruby': 'ルビー', 'gui.libraryTags.firstTime': 'はじめて', 'gui.libraryCategories.gettingStarted': '始めましょう', - 'gui.library.meshStep1': 'メッセージを送ってみよう', - 'gui.library.meshStep2': 'ふたりで会話しよう', - 'gui.library.meshStep3': 'みんなで会話しよう', + 'gui.library.meshStep1': '通信入門 ① メッセージを送ってみよう', + 'gui.library.meshStep2': '通信入門 ② ふたりで会話しよう', + 'gui.library.meshStep3': '通信入門 ③ みんなで会話しよう (メッシュ)', 'gui.menuBar.updateTooltip': '新しいスモウルビーを使ってみよう!', 'gui.menuBar.updateConfirm': '新しいバージョンのスモウルビーが利用可能です。いますぐ更新する場合は「OK」を、あとにする場合は「キャンセル」を押してください。', diff --git a/tools/playwright-verify/verify-tutorial-mesh-categories.mjs b/tools/playwright-verify/verify-tutorial-mesh-categories.mjs index e095b37059a..ba2c9a4e7c4 100644 --- a/tools/playwright-verify/verify-tutorial-mesh-categories.mjs +++ b/tools/playwright-verify/verify-tutorial-mesh-categories.mjs @@ -60,7 +60,11 @@ log('category titles =', JSON.stringify(titles)); await page.screenshot({ path: 'tmp/tutorial-mesh-categories.png', fullPage: true }); -const expected = ['メッセージを送ってみよう', 'ふたりで会話しよう', 'みんなで会話しよう']; +const expected = [ + '通信入門 ① メッセージを送ってみよう', + '通信入門 ② ふたりで会話しよう', + '通信入門 ③ みんなで会話しよう (メッシュ)', +]; const missing = expected.filter((t) => !titles.includes(t)); if (missing.length) { console.error('MISSING titles:', missing);