対象: スマホ (横向き) と iPad (portrait/landscape)。一般 PC ブラウザでも viewport を縮めれば同じ条件で発火する 検証 viewport:
- スマホ横向き: 844×390 (iPhone 14)
- iPad portrait: 768×1024、iPad mini portrait: 744×1133
- iPad landscape: 1024×768
- desktop reference: 1280×800
このドキュメントは、Smalruby を スマホ・タブレット (iPad) で操作可能にする一連の対応 の設計意図と data-testid をまとめたもの。タイトルどおり SP (smartphone) を主目的とするが、iPad の調整も SP 対応の一部として同じドキュメント内で扱う(フォーム要因が同根のため)。
切り替えは viewport ベースで自動。URL パラメータでのオプトインは設けない。useIsNarrowScreen の matchMedia がリアルタイムに反応するので、ブラウザリサイズ・端末回転に追従する。
packages/scratch-gui/src/lib/use-is-narrow-screen.js:
const NARROW_SCREEN_QUERY = '(max-width: 743px), (max-height: 500px)';packages/scratch-gui/src/lib/responsive-gui.jsx がこの hook の結果で <MobileGui> と <GUI> (upstream desktop) を出し分ける。
| Viewport (例) | 短辺幅 | 高さ | 出るモード | スクリーンショット |
|---|---|---|---|---|
| iPhone 14 横 (844×390) | 844 | 390 | MobileGui (高さ ≤ 500 で発火) | 02〜11 |
| iPhone 14 縦 (390×844) | 390 | 844 | MobileGui + 縦持ち警告 | 11 |
| iPad mini portrait (744×1133) | 744 | 1133 | desktop GUI (iPad 調整) | 23 |
| iPad portrait (768×1024) | 768 | 1024 | desktop GUI (iPad 調整) | 20 |
| iPad landscape (1024×768) | 1024 | 768 | desktop GUI (高さ調整あり) | 21、22 |
| Desktop (1280×800) | 1280 | 800 | desktop GUI (upstream 通常) | 24 |
- 幅 743px は iPad mini portrait (744) を MobileGui に落とさない ための境界。MobileGui は横向き専用 UI なので、iPad portrait のような縦長を強制的に MobileGui で見せると逆に使いにくい。
- 高さ 500px はスマホ横持ち (390 高さ) を確実に拾う保険。デスクトップで縦を 500 以下に縮めるのは一般的でないので副作用は小さい。
「desktop GUI に分岐したけど 1024px 未満」のレンジ (= iPad mini/iPad portrait と縦が低めの iPad landscape) には、upstream の固定 1024px 前提を緩めるための CSS メディアクエリ が当たる。これらは Smalruby マーカー付きで upstream ファイル内に置いてある。
| 場所 | 機能 | 効果 |
|---|---|---|
src/playground/index.css |
iPad portrait min-width relax | body の min-width: 1024px を 744〜1023px で解除し、横スクロールを抑止 |
src/components/gui/gui.css |
iPad portrait narrow desktop layout | editor-wrapper の flex-basis を緩めて、stage 列が 250px 程度に収まるようにする |
src/components/gui/gui.css |
iPad portrait legal links cleanup | フィードバックリンク + セパレータを非表示にして上部メニューの幅を確保 |
src/components/gui/gui.jsx |
iPad portrait narrow desktop stage size | stageSize を 'small' に強制 |
src/components/gui/gui.css |
narrow-height vertical chrome compression | max-height: 800px (= iPad landscape など) で body-wrapper を 2.5rem 圧縮、tab-list を 36px に下げる |
src/components/menu-bar/menu-bar.css |
narrow-height menu bar compression | max-height: 800px で menu-bar を 48→40px に圧縮 |
src/playground/index.css |
narrow viewport vertical scroll lock | 狭幅で overflow-y: clip、ピンチ拡大などの縦スクロール暴発を抑止 |
詳細マーカーは .claude/rules/scratch-gui/smalruby-markers.md を参照。
- upstream は 1 行も削らない: SP/iPad 対応はすべてマーカー付きの加筆で実装。upstream 取り込み時の merge コストを最小化するため (
MEMORY.mdの feedback「upstream Scratch 追従コスト最小化」)。 - オプトインフラグを増やさない: ユーザーが flag を知らないと SP で見られないため、viewport 自動判定で完結させる。同様に「PC 表示が崩れている」警告バナーも置かない (リサイズ可能な PC ブラウザでの誤検知が多いため)。
- MobileGui は別コンポーネントに分離: desktop GUI の一部だけを CSS で隠すアプローチでは「右ペインも見えないのに React tree には残る → 余計なリスナーや HMR コスト」が累積するため、MobileGui を独立したコンポーネントにして一度差し替える方式にした。
- iPad は desktop GUI のまま: iPad は横幅 768〜1024px 程度あり、MobileGui の縦タブ集約より desktop の従来 UI のほうが学習コストが低い。なので iPad は desktop GUI に CSS/レイアウト調整を当てる方針。
┌────┬────────────────────────────────────────┐
│ ☰ │ │
│ ▶ │ │
│ ── │ │
│Cd │ Editor area │
│Cm │ (full height = 100vh) │
│Sn │ │
│Rb │ │
│Sp │ │
└────┴────────────────────────────────────────┘
- 左 56px:
MobileSideRail(ハンバーガー + 実行/停止 + 5 タブ) - 右側: 各タブの編集エリア (viewport 縦 100% / 横 100vw - 56px)
MobileGui 自体は upstream の <GUI> を呼ばず、必要な機能だけを再構成して並べる薄いシェル。
| パーツ | data-testid | 役割 |
|---|---|---|
| ハンバーガー (☰) | mobile-side-rail-menu |
ドロワー (MobileDrawer) を開く |
| 実行/停止 (▶ / ⏹) | mobile-side-rail-play |
▶: 全画面ステージ起動 + vm.start() + vm.greenFlag() / ⏹: vm.stopAll() + 全画面解除 |
| コード | mobile-side-rail-code |
upstream BLOCKS_TAB_INDEX に切替 |
| コスチューム | mobile-side-rail-costume |
upstream COSTUMES_TAB_INDEX に切替 |
| 音 | mobile-side-rail-sound |
upstream SOUNDS_TAB_INDEX に切替 |
| ルビー | mobile-side-rail-ruby |
Smalruby RUBY_TAB_INDEX に切替 |
| スプライト | mobile-side-rail-sprite |
スプライトパネルオーバーレイを開閉 (Redux ではなく親 state) |
| バックパック | mobile-side-rail-backpack |
バックパックの開閉 (実装中) |
active 状態は data-active="true" 属性で表現される (CSS 紫ハイライト)。
設計意図: PC では上部 <MenuBar> + <TabList> + 右ペインの三分割だが、横幅が狭い SP では同じ構成は破綻する。横向き 844×390 で確実に親指の届く左 56px に すべて集約 したのが MobileSideRail。実行/停止は緑旗+赤丸の 1 ボタン統合 にして、SP では「編集中ステージは隠して必要なときだけ全画面で動かす」を基本フローにした。
ブロックパレット (左カテゴリリスト + ブロック群) + ワークスペース (右の dotted area) の構成。
| パーツ | data-testid / セレクタ | 役割 |
|---|---|---|
パレットトグル (< 紫ハンドル) |
[class*="palette-toggle_palette-toggle-button"] |
パレット表示/非表示。SP では紫 56×28 に拡大 (タッチ確実化) |
| カテゴリ一覧 | .blocklyToolboxDiv |
upstream Blockly のカテゴリ |
| ブロック一覧 | .blocklyFlyout |
upstream Blockly のフライアウト |
| ワークスペース | .blocklySvg |
ブロック配置用 |
| ズーム +/-/= | upstream の .blocklyZoom |
ワークスペース拡大縮小 |
| 拡張機能追加 (左下 +) | .extension-button-container |
upstream のまま |
desktop 版との違い: gui_tab-list-container を display: none、gui_stage-and-target-wrapper を flex: 0 0 0 で潰し、Mobile では編集に集中した 1 ペインにする。<MenuBar> は MobileSideRail に置き換わる。
< ハンドルをタップしてパレットを左に隠した状態。ワークスペースが viewport 全幅に広がる。
| パーツ | data-testid | 役割 |
|---|---|---|
パレット復活ハンドル (> 紫) |
[class*="palette-toggle_palette-toggle-button"] |
タップで再度パレット表示 |
| ワークスペース | .blocklySvg |
全幅で広がる |
設計意図: SP の横向きでも 844px しかなく、パレット 200px + ワークスペース 644px ではブロック配置が窮屈。トグルで全幅 788px (= 844 - 56) を取り戻すことで、長いプログラムも組める。トグル位置はサイドレールの紫色と揃え、視認性を上げてある。
▶ をタップするとステージが viewport 全画面で起動 (緑旗自動実行)。サイドレールは残るので「⏹」で戻れる。
| パーツ | data-testid | 役割 |
|---|---|---|
| 停止ボタン (⏹ 赤) | mobile-side-rail-play |
サイドレールに残る。タップで全画面解除 + vm.stopAll() |
| ステージ menu の緑旗・赤丸 | upstream .green-flag / .stop-all |
全画面でも使える |
| 全画面解除トグル (右上) | upstream .full-screen-button |
upstream のまま |
横並びの paint editor。上部にツールバー (フロート配置)、左に縦のペイントツール、右に canvas、下部に bitmap 切替 + zoom。
| パーツ | data-testid / セレクタ | 役割 |
|---|---|---|
| 上部ツールバー (フロート) | [class*="paint-editor_editor-container-top"] |
名前 / undo/redo / 反転 / Z 順 / 色 / 線幅 / コピー / 貼付 / 削除 / 上下反転 |
| ▲ トグル (ツールバー下端中央) | mobile-paint-toolbar-toggle |
タップでツールバーを折りたたむ |
| ペイントツール (左の縦アイコン群) | [class*="paint-editor_mode-selector"] 内 .button |
selector / lasso / brush / eraser / fill / text / line / oval / rectangle |
| canvas (中央大エリア) | canvas.paper-canvas_paper-canvas |
描画領域 |
| ビットマップに変換 | upstream のボタン | bitmap/vector 切替 |
| ズーム Q-/=/Q+ | upstream [class*="paint-editor_zoom-controls"] |
canvas 拡大縮小 |
| コスチューム一覧 (左端列) | upstream [class*="selector_wrapper"] |
コスチューム選択 (左列 80px に縮小) |
動作 (タップに応じて自動制御):
- タブ進入時: ツールバー展開状態
- canvas クリック (描画開始): 自動でツールバー折りたたみ
- ペイントツールタップ (mode-selector): 自動でツールバー展開
- ▼/▲ ハンドル: 手動切替
設計意図: PC は固定上部ツールバーで縦スペースを犠牲にする前提だが、SP は縦が 390px しかないので、描画開始時は自動でツールバーを退避 して canvas を最大化する。ペイントツール (ブラシなど) を選び直すときは展開を戻すことで、操作中の指の動きと UI 表示を一致させる。
▼ ハンドルをタップした状態。ツールバー消失で canvas + ペイントツール + bitmap/zoom が縦 100% で見える。
| パーツ | data-testid | 役割 |
|---|---|---|
| ▼ トグル (上端中央) | mobile-paint-toolbar-toggle |
タップで再展開 |
ペイントツールの位置はツールバー有無に関わらず固定 (= 展開時の位置)。指の下から逃げないようにするため。
audioContext.createBuffer is not a function で <SoundEditor> がクラッシュする。これは upstream AudioBufferPlayer の初期化問題で実機 iOS Safari / Android Chrome では正常動作する。 ドキュメントとしての構成は以下を想定。
| パーツ | data-testid / セレクタ | 役割 |
|---|---|---|
| 音名入力 / undo/redo | upstream .sound-editor |
サウンド名編集、操作履歴 |
| 波形表示 | upstream .audio-trimmer |
クロップ / 音量編集 |
| エフェクトボタン (大きく / 小さく / ミュート / フェードイン / フェードアウト / 逆向き / ロボット) | upstream のボタン群 | 音響エフェクト |
| サウンド一覧 (左端列) | upstream [class*="selector_wrapper"] |
サウンド選択 |
Monaco Editor + ルビーツールバー。
| パーツ | data-testid | 役割 |
|---|---|---|
| ▶ 実行 | ruby-toolbar-execute |
実行 (サイドレールの ▶ と機能重複) |
| Undo / Redo | ruby-toolbar-undo / ruby-toolbar-redo |
元に戻す / やり直す |
| 検索 (🔍) | ruby-toolbar-search |
テキスト検索 |
| 自動置換 | ruby-toolbar-auto-correct |
自動置換トグル |
| 前/次のスプライト | ruby-toolbar-prev-sprite / -next-sprite |
スプライト切替 |
| AI ルビティー | ruby-toolbar-rubytee |
Anthropic Claude による AI コード生成 |
| ふりがな (mode tab) | ruby-toolbar-mode-furigana |
ふりがなモード |
| Ruby (mode tab) | ruby-toolbar-mode-ruby |
Ruby モード |
| 日本語 (mode tab) | ruby-toolbar-mode-dncl |
日本語(DNCL)モード |
| ・・・ (もっと) | ruby-toolbar-more-menu |
保存 / クラス挿入 / プレビュー / 自動置換設定 |
PC のルビーツールバーには「スプライトを名前で検索」入力欄があるが、SP では display: none で省略 (横幅不足)。スプライト切替は ← / → ボタンとスプライトタブ経由。
サイドレールの「スプライト」をタップすると編集エリアにオーバーレイで開く。upstream <TargetPane> を再利用しつつ、<SpriteLibrary> モーダルだけは抑止 (二重描画回避: hideSpriteLibrary prop を upstream target-pane.jsx に追加)。
| パーツ | data-testid | 役割 |
|---|---|---|
| パネル全体 | mobile-sprite-panel |
コンテナ |
| スプライト情報 (上部) | upstream <SpriteInfo> |
名前 / x / y / 表示・非表示 / 大きさ / 向き |
| スプライト一覧 (左の大エリア) | upstream <SpriteList> |
スプライト選択 / 削除 (× アイコン) |
| 「+」 FAB (左下) | upstream <ActionMenu> |
スプライト追加 (Choose / Paint / Surprise / Upload) |
| ステージ列 (右 80px) | upstream <StageSelector> |
ステージ背景の管理 + 「+」 で背景追加 |
stageSize="middle" を強制してフルセットの SpriteInfo (大きさ・向き含む) を表示。PC の右ペイン 270px 幅では small で名前 + x/y のみだったので、SP の方がリッチ。
サイドレール左上の ☰ をタップすると左から slide-in。
| パーツ | data-testid | 役割 |
|---|---|---|
| 閉じるボタン (×) | mobile-drawer-close |
ドロワーを閉じる |
| 背景タップ | mobile-drawer-backdrop |
ドロワーを閉じる |
| 新しいプロジェクト | mobile-drawer-new |
requestNewProject(false) を dispatch |
| Google Drive から開く | mobile-drawer-open-google-drive |
Google Drive 選択 |
| パソコンから開く | mobile-drawer-open-device |
upstream SBFileUploaderHOC 起動 |
| Scratch から開く | mobile-drawer-open-scratch |
Scratch URL 入力 |
| 保存 | mobile-drawer-save |
upstream SB3Downloader 起動 |
| 名前を付けて保存 | mobile-drawer-save-as |
別名保存 |
| ターボモード | mobile-drawer-turbo-mode |
turbo mode の on/off |
| クラスルーム | mobile-drawer-classroom |
生徒モードでクラスルーム参加 |
| メッシュ | mobile-drawer-mesh |
Mesh v2 接続 |
| 言語切替 | mobile-drawer-locale-${code} |
各 locale の選択 (ja, ja-Hira, en など) |
| Ruby バージョン切替 | mobile-drawer-ruby-version-${version} |
Ruby v1 / v2 |
| クラス管理 | mobile-drawer-classroom-management |
教師モード (教師が認証済みのとき) |
| トグル系メニュー (アコーディオン) | mobile-drawer-toggle-${key} |
言語 / Ruby version / その他 設定セクションの開閉 |
設計意図: PC <MenuBar> の機能を SP では このドロワーに集約。アコーディオン化することで、横幅 300px 弱でも縦に伸ばさず収まる構成にしてある。
(orientation: portrait) を matchMedia で検知して全画面オーバーレイを Portal で表示。横向きにすると消える。
| パーツ | data-testid | 役割 |
|---|---|---|
| オーバーレイ全体 | mobile-orientation-gate |
縦向き時のみ表示 |
| メッセージ | (テキストのみ) | 「横向きにしてください」 + iOS の画面の向きロック注意書き |
設計意図: SP 縦持ちは MobileGui の前提 (横レイアウト) と合わない。MobileGui の中で頑張るより、「横にしてください」と明示的に止める ほうが操作迷子を減らせる。iPad には出さない (iPad は portrait でも desktop GUI で見られる)。
iPad は useIsNarrowScreen の閾値超え (短辺 ≥ 744 かつ高さ > 500) なので upstream desktop GUI が出る。ただし viewport が狭い (744〜1023px 幅 / もしくは 800px 以下の高さ) ので、上記 §1 の iPad モード調整 が当たる。MobileGui には行かない。
min-width: 1024pxの制約を解除し、横スクロール禁止editor-wrapperの flex を緩めて stage 列 (~250px) に収める- フィードバックリンクなど狭幅で破綻するメニュー上の付帯リンクは隠す
stageSizeは'small'強制 (gui.jsx 内の Smalruby マーカー)
→ ブロック編集 + スプライト + ステージの三分割が、横スクロールなしで成立する。
- 高さが 800px 以下なので
narrow-height vertical chrome compressionが当たる - menu-bar 48→40px、tab-list 44→36px に圧縮
- 縦のワークスペース有効領域が +16px 増える
ルビータブも同じ chrome 圧縮が効く。Monaco エディタの縦が伸びる。
「744〜1023」レンジに iPad mini portrait (744) も含むよう閾値を引き下げてある。min-width: 1024px 解除と stage 列縮小がそのまま効く。
参考。1024px 以上は upstream のまま。Smalruby の追加 CSS は当たらず、見た目は通常の Smalruby/Scratch エディタと完全に一致する。
SP/iPad 対応で確立した運用ルール。今後の改善も以下を踏襲する。
- upstream は触らない、加筆だけする
=== Smalruby: Start of <feature> === / End ===のマーカーで囲む。一覧は.claude/rules/scratch-gui/smalruby-markers.md。SP 対応のために upstream のロジックを書き換えると後の merge で衝突する。 - URL パラメータでのオプトインを増やさない viewport で自動判定する。flag を知らないと UI が出ない構造を作らない。
- 「PC が崩れている」警告バナーを置かない リサイズ可能な PC ブラウザでの誤検知が多い。viewport で MobileGui に切り替わるならバナー不要。
- MobileGui は横向き専用 縦は orientation gate で止める。MobileGui 内に縦レイアウトを抱え込まない。
- 隠すより「再構成」を優先
display: noneで誤魔化すと React tree が無駄に肥大する。専用コンポーネント (MobileSideRail / MobileDrawer など) に分離して、必要な機能だけ並べ直すこと。 - iPad は desktop GUI のまま、CSS で詰める iPad ユーザーは PC 由来の操作慣れがあるので、独自 UI に変えるよりも desktop GUI を縮める方が学習コストが低い。
- タッチ操作のフィッツの法則を意識 SP/iPad のタップ要素は最低 44px × 44px を確保する。Blockly 内のような触れない要素を除き、新規追加要素は data-testid + 44px 以上で作る。
data-testidは必ず付ける Playwright 確認手順で参照される。<component>-<element>のケバブケース。新規要素には漏れなく付与する (.claude/rules/scratch-gui/e2e-test.md)。
docs/mobile-ui/playwright.md— Playwright での確認手順と data-testid 一覧.claude/rules/scratch-gui/mobile-ui.md— SP / iPad 関連 PR のレビュー観点と影響範囲.claude/rules/scratch-gui/smalruby-markers.md— upstream ファイルへのマーカー一覧.claude/rules/scratch-gui/e2e-test.md— data-testid 命名規則と URL parameter 一覧.claude/rules/scratch-gui/development.md— scratch-gui の開発フロー全般













