Skip to content

feat: upstream merge 2026-05 (v13.7.2 — 284 commits, scratch-blocks v2)#630

Merged
takaokouji merged 339 commits into
developfrom
feat/upstream-merge-2026-05
May 5, 2026
Merged

feat: upstream merge 2026-05 (v13.7.2 — 284 commits, scratch-blocks v2)#630
takaokouji merged 339 commits into
developfrom
feat/upstream-merge-2026-05

Conversation

@takaokouji
Copy link
Copy Markdown

@takaokouji takaokouji commented May 3, 2026

Summary

Merged 284 upstream commits from scratchfoundation/scratch-editor v13.7.2 (the production release currently deployed at scratch.mit.edu).

Upstream Commit Range: 42ea882750 (previous merge) → 352a334b4a (v13.7.2)
Production deploy: scratch-www 3572d5ad8a (2026-04-30)

Major Upstream Changes

Conflict Resolution Strategy

postMergeReverts guidance was 方針A (accept upstream) because the original argument-drag regression is fixed in v2.1.19. All ScratchBlocks v1 → v2 API call sites were rewritten:

Before (v1) After (v2)
ScratchBlocks.Xml.textToDom ScratchBlocks.utils.xml.textToDom
ScratchBlocks.Xml.domToText ScratchBlocks.utils.xml.domToText
ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml ScratchBlocks.clearWorkspaceAndLoadFromXml
ScratchBlocks.Variables.createVariable ScratchBlocks.ScratchVariables.createVariable
ScratchBlocks.Procedures.createProcedureDefCallback_ ScratchBlocks.ScratchProcedures.createProcedureDefCallback
ScratchBlocks.Procedures.externalProcedureDefCallback ScratchBlocks.ScratchProcedures.externalProcedureDefCallback
ScratchBlocks.statusButtonCallback ScratchBlocks.StatusIndicatorLabel.statusButtonCallback
ScratchBlocks.prompt = fn ScratchBlocks.dialog.setPrompt(fn)
workspace.reportValue ScratchBlocks.reportValue
workspace.toolbox_.getSelectedCategoryId() workspace.getToolbox().getSelectedItem().getName()
inject({ colours }) inject({ theme: new ScratchBlocks.Theme(...) })
import Blockly from 'scratch-blocks' import * as Blockly from 'scratch-blocks'

color-mode/{default,dark,high-contrast,blockHelpers}.js were re-synced from upstream (key rename primarycolourPrimary etc.). lib/blocks.js was replaced wholesale with upstream's v2 version, with only the Smalruby gesture-recovery import / call re-applied.

Smalruby Markers Added (upstream files)

Conflicts Resolved (highlights)

File Disposition
package*.json (root + 5 packages) Smalruby names + 13.7.2 version, took upstream dep bumps; added redux and react/react-dom as direct deps so webpack resolves them (peer-only is no longer enough)
package-lock.json Regenerated via npm install
containers/blocks.jsx Took v2 API, kept Smalruby Ruby-converted block positioning + palette visibility; reset componentWillUnmount to be defensive
containers/backpack.jsx Took upstream's backpackStorage.save/list/delete flow; localStorage path now lives in LegacyBackpackStorage
lib/legacy-storage.ts Took upstream; localStorage logic moved into LegacyBackpackStorage
lib/alerts/index.jsx Kept Smalruby Ruby/classroom alerts AND took upstream thumbnail success/error alerts
serialization/sb3.js Took upstream's shadow-block at top-level + comment-id sanitization
engine/blocks.js Took upstream's shadow-restoration on parent change
13 VM extensions, debug-modal messages, virtual-machine.js test, etc. Took upstream (only had // eslint-disable-next-line @stylistic/max-len comments to add)
eslint.config.mjs (vm) Kept Smalruby ignores + added upstream's tap-snapshots/**/*
webpack.config.js (gui + vm) Replaced require.resolve('scratch-blocks/package.json') with '../../node_modules/scratch-blocks/media'; scratch-blocks/dist/vertical.jsscratch-blocks for expose-loader

Test Results

  • npm run lint — clean (errors 0, warnings 0, prettier OK)
  • npm run build:dev — succeeds (only non-blocking jsdom / node-fetch optional-native warnings remain)
  • ✅ Sample unit tests pass
  • ✅ Editor loads in browser with no fatal runtime errors

Manual DoD verification (Playwright, 1280×900)

Feature Result
Initial load ✅ Editor renders; Hatti sprite on stage; 9 categories in palette
Block palette (Motion) ✅ All blocks render with v2 colours in Japanese
File menu ✅ All Smalruby items present (新規 / Scratchから読み込む / Google ドライブ系)
Ruby tab ✅ Monaco editor + Ruby toolbar + ふりがな button
Extension library ✅ All extensions show in Japanese (音楽 / ペン / ビデオモーションセンサー / 顔認識 / 音声合成 / 翻訳 / micro:bit など)
Stage header ✅ 3-button toggle (small/middle/large) preserved

Remaining console messages (non-fatal):

  • MISSING_TRANSLATION for gui.stageHeader.thumbnailTooltipTitle and gui.stageHeader.saveThumbnailMessage — new upstream strings not yet in ja.js
  • React key warning inside upstream ConfirmationPrompt — upstream-side issue, not blocking

High-Risk Areas (for code review)

scratch-blocks v1 → v2 の API 移行は 441 ファイルに渡るため、レビュー時は以下の 挙動リスクが高い領域 を優先して spot-check してください:

  1. Toolbox refresh / category click

    • make-toolbox-xml.jsid="motion"toolboxitemid="motion" に変更 (v2 の category lookup が toolboxitemid を期待)
    • containers/blocks.jsxhandleCategorySelected が v1 setSelectedCategoryById から v2 getToolbox().setSelectedItem(getToolboxItemById(...)) に変更
    • 影響: 全カテゴリの初回表示 / 切り替え
  2. Flyout block click → 値レポートツールチップ

    • scratch-vm/engine/blocks.js で v1 e.element === 'stackclick' → v2 case 'click' (e.targetType, e.blockId) に置き換え
    • integration test の xpath を data-id から scopeForFlyoutBlock(opcode) に変更 (v2 では data-id は unique block id)
    • 影響: 演算/変数の値プレビュー、Make-a-Variable / Make-a-List の作成 dialog
  3. Custom procedures (My Blocks)

    • ScratchBlocks.ProceduresScratchBlocks.ScratchProcedures への rename
    • 影響: ブロック定義モーダルの起動と保存
  4. Workspace comments / sb3 deserialization

    • block_comment_* 系のイベント名を v2 が削除し comment_* に統一
    • sb3 deserialize で block-attached comment id 衝突を再書き換え
    • 影響: コメント付きプロジェクトの読み込み、エディタ/ステージ切り替え後のコメント表示
  5. Drag / palette toggle

注意: AudioContext autoplay policy 関連は ?tab=sounds 直接アクセスで触らないこと。詳細は .claude/rules/scratch-gui/e2e-test.md 参照。

TODO After Merge

  • Add Japanese translations for gui.stageHeader.thumbnailTooltipTitle / saveThumbnailMessage in src/locales/ja.js and ja-Hira.js
  • Full DoD verification using docs/<feature>/screenshots/ after preview deploy:
    • Mesh v2 connection flow (5 screenshots)
    • Classroom modal flow (15 screenshots)
    • Mobile UI / iPad portrait (16 screenshots)
    • Rubytee modal (3 screenshots)
    • Backpack save/load (7 screenshots)
  • Verify scratch-blocks v2.1.19 specifically does not regress custom-block-argument drag (the original revert reason)
  • Update .claude/rules/scratch-gui/smalruby-markers.md to list the new markers in legacy-backpack-storage.ts

Related

🤖 Generated with /upstream-merge workflow

kbangelov and others added 30 commits March 19, 2026 14:35
chore: Update dependency of scratch-storage to v6.2.0
…king-of-packages

feat: add symlinks:false to webpack config
Apparently, `-F` (or `-f`) causes `gh api` to switch to a `POST`, which
then causes the request to fail with a 404. Forcing `GET` makes it work.
takaokouji added 9 commits May 4, 2026 16:44
ConfirmationPrompt の cancel/confirm ボタンを配列で render する際、React の
unique key prop が無いため warning が発生していた。
StageHeaderComponent → ConfirmationPrompt を経由して console error として
記録されていたが、key を付与することで解消。

仕様変更ではなく React のベストプラクティス対応。
Smalruby 固有コードが Blockly の trailing-underscore (private) フィールドや
internal API に新規に依存していないかを CI で自動検出するユニットテストを
追加する。

upstream merge のたびに private フィールドはリネーム/削除されうるので、
本テストで早期検知して公開 API への migrate を促す。

実装方針:
- src/ 配下を再帰スキャンし、Blockly オブジェクト名 (workspace, flyout,
  toolbox, ScratchBlocks, Blockly, ...) からの `<obj>.<name>_` パターンを
  正規表現で検出
- 既知で公開 API が無い箇所は ALLOWLIST に理由付きで登録
- ALLOWLIST の stale エントリも検出する 2nd test を追加

現在の ALLOWLIST:
- src/lib/blocks-gesture-recovery.js: `workspace?.currentGesture_`
  (drag 中状態取得の公開 API が v12 にも無い)
- src/containers/blocks.jsx: `flyout.svgGroup_`
  (palette toggle 用、flyout.hide() 単独では再表示されてしまう)
- src/containers/blocks.jsx: `ScratchBlocks.FieldColourSlider.activateEyedropper_`
  (scratch-blocks の eyedropper 拡張ポイント、upstream 利用)
- src/lib/blocks.js: `ScratchBlocks.FieldNote.playNote_`
  (scratch-blocks の音符再生コールバック拡張ポイント、upstream 利用)
upstream の PlayButton container では getDerivedStateFromProps が
インスタンスメソッドとして定義されているが、React 16.3+ では static でないと
完全に無視され、開発モードで warning が発生する。

dev mode (Smalruby ローカル開発) では PlayButton を含む音ライブラリモーダル
を開いた際に warning が出ていた。production build では DEV warning が
strip されるため scratch.mit.edu では見えなかった。

修正後は touchStarted の派生 state が正しく React によって反映される
(これまでは無視されていたため意図せず handleMouseEnter/Leave で再生が
止まらない潜在バグもあった)。
PlayButton で getDerivedStateFromProps が static でないバグが production
build (DEV warning strip) のため scratch.mit.edu で見えず、長期間放置されて
いた。同種のバグ (getDerivedStateFromError 等) を lint で systematic に検出
できるよう react/no-typos ルールを error として有効化する。

verification: 一時的に static を外して lint 実行 → "Lifecycle method
should be static: getDerivedStateFromProps" で正しく検出されることを確認。

併せて、これまで markers ドキュメントに記載漏れだった eslint.config.mjs の
prettier integration マーカーも追記。
…ks v2

Ruby → blocks 変換で生成される `@ruby:method:*` などの内部メタデータ
コメントが、scratch-blocks v2 (Blockly v12) では minimize 表示されず、
全コメントが同じ workspace 座標 (200, 0) にスタックして workspace の
別の場所に浮いていた。

これは PR #271 の v1 ダウングレードで対応していなかった v2 仕様変更:

1. v1 の `comment.setMinimized(true)` API は v2 で廃止され、ブロック付随
   コメントは block の **comment icon** として実装された
   (`block.getIcons()` から `IconType.comment` を取得して操作)
2. icon の `setBubbleVisible(false)` で minimize できるが、Events.disable
   中に呼ぶと no-op、また onWorkspaceUpdate 直後の同期呼び出しは Blockly
   の post-load render に上書きされる → 100ms 遅延 + block ID から再取得
   する必要がある
3. v2 は collapsed (minimized) 状態でも comment.x/y を保持するため、
   converter が常に (200, 0) を割り当てると複数の bubble が完全に重なる
   → block ごとに `setBubbleLocation(blockX + 220, blockY)` で再配置

加えて scratch-vm 側で `block.x` が undefined の topLevel block の XML
出力が `x="undefined"` になり、scratch-blocks v2 がこれを 0 として読み込み
move event を発火、`fromRuby` 検出 (`typeof topBlock.x === 'undefined'`)
が効かなくなっていた問題も修正。`Number.isFinite` で実値があるときだけ
属性を出力するようガードを追加。

修正後:
- `puts "hello"` 1 行 → 該当 say block 横に minimize されたバッジ表示
- `puts "a"` + `puts "b"` 複数 → 各 block にバッジが正しく分離して表示
- `def/return` を含むコードでも `@ruby:return` マーカー付き block は
  既存の getTopComments 経路で処理され従来通り動作
前回の修正で各ブロック直下に bubble を配置したが、ネストされた input
ブロック (例: \`puts a\` の \`a\` 変数式) ではコメントが内側ブロックの x を
基準に置かれ、トップレベルブロックの comment と左端が揃わなかった。

\`getParent()\` を辿って top-level (statement chain の根) の x を取得し、
スクリプト内のすべての \`@ruby:*\` comment がその左側 (rootX - 220) で
左端を共有するよう修正。
\`@ruby:class:...\` のような workspace-level comment (blockId=null) は
scratch-blocks v2 で WorkspaceComment として保持され、v1 の
\`comment.setMinimized(true)\` API は廃止された (\`setCollapsed(true)\` に
リネーム)。前回の修正で setMinimized 呼び出しを削除した結果、クラスコメント
が大きな黄色いバブルとして expanded 状態で表示されていた。

修正:
- workspace.getTopComments() の \`@ruby:*\` コメントに対して
  \`setCollapsed(true)\` を呼び出して collapse 表示
- 位置を first top block の x - 220 に揃える (block-attached コメントと
  同じ左端アライメント)

これで \`class Sprite1 < ::Smalruby3::Sprite ... end\` のような Ruby から
変換した script の \`@ruby:class:\` メタデータコメントが、他の \`@ruby:*\`
コメントと同じく左側に小さなバッジとして表示される。
\`@ruby:class:...\` の WorkspaceComment は前回の修正で setCollapsed(true)
を呼ぶようにしたが、これは Events.disable() 中なので no-op、また
onWorkspaceUpdate 直後の同期呼び出しは scratch-blocks v2 の
post-load render に上書きされる (block 付き comment icon と同じパターン)。

修正:
- 同期ブロックでは comment ID だけ集める
- finally の Events.enable() 後 setTimeout 100ms で再 fetch して
  setCollapsed(true) を実行

block-attached comment と完全に同じ deferred apply パターンに統一。
これで class コメントも他の \`@ruby:*\` コメントと同様に小さなバッジ
として表示される。
…ispose error

Ruby converted scripts that introduce list variables (e.g.
\`a = [1, 2, 3]; a.each do |i| puts i end\`) crashed the editor with
infinite "Cannot read properties of undefined (reading '2')" errors at
\`disposeItem\`/\`clearOldBlocks\` inside scratch-blocks v2's flyout
recycling path:

  TypeError: Cannot read properties of undefined (reading '2')
      at u (main.mjs:1:1)
      at DS.dispose (main.mjs:1:1)
      at pA.disposeItem (main.mjs:1:1)
      at DE.clearOldBlocks (main.mjs:1:1)
      at DE.show (main.mjs:1:1)
      at gE.forceRerender (main.mjs:1:1)
      at Blocks.updateToolbox (blocks.jsx:439)

Root cause: when forceRerender() throws, \`_renderedToolboxXML\` is
never assigned, so the next componentDidUpdate sees toolboxXML still
diverging from the rendered state and calls requestToolboxUpdate()
again, which re-throws, ad infinitum (the console flooded with the
same trace until the page was unusable).

修正:
- updateToolbox の \`_renderedToolboxXML = props.toolboxXML\` を
  forceRerender の **前** に移動 (例外で skip されないように)
- forceRerender を try/catch + setRecyclingEnabled(false) で囲む
  (recycling 経路を回避し、v2 内部の dispose クラッシュを抑止)
- 失敗時は \`log.error\` で 1 回だけ通知し、エディタは継続動作

これで a.each など配列リテラルを含む Ruby が無限エラーを起こさず、
ブロックも正しく描画されるようになる (動作確認済み)。
takaokouji added 2 commits May 4, 2026 21:35
…` literals

Issue #634 (under feat/upstream-merge-2026-05).

Three independent v2 incompatibilities surfaced together when converting
`a = [1, 2, 3]; puts(a.max)` and similar Ruby code:

1. **`ScratchBlocks.FieldVerticalSeparator` removed in v2.** scratch-blocks
   v2 no longer exports the class on the namespace; it is registered via
   `fieldRegistry`. The old `new ScratchBlocks.FieldVerticalSeparator()`
   call in `defineDynamicBlock.createAllInputs` threw mid-flight in
   `domToMutation`, which made the surrounding XML parser drop the rest of
   the block's `<next>` chain — every Ruby-converted script that touched
   an extension block with an icon ended up as detached top-level blocks.
   Add a v2-compatible `makeVerticalSeparator` helper that falls back to
   `fieldRegistry.fromJson({type: 'field_vertical_separator'})`.

2. **Eager scalar created by `_onVasgn`.** `assignments.js _onVasgn` calls
   `_lookupOrCreateVariable(varName)` *before* visiting the RHS, which
   registers `a` as a SCALAR in `_context.localVariables`. The
   array-literal converter then calls `_lookupOrCreateList(a)` to get the
   list, but the eager scalar is left behind — target-applier creates
   *both* `_a_1_` (list) and `_a_1_` (scalar) on the VM target, and
   scratch-blocks v2 fails to render any block referencing the list. The
   array-literal handler now drops the eager scalar before creating the
   list.

3. **Cross-store re-creation at top level.** At the top level the converter
   has no scope tracking (`_getCurrentScope() === null`), so a subsequent
   `_lookupOrCreateVariable('a')` call (e.g. from
   `visitLocalVariableReadNode` for `a.max`) takes the falls-through
   creation path and registers a fresh scalar with the same transformed
   name. Add a check in `_lookupOrCreateVariableOrList` to reuse an
   existing entry from the *other* store (lists ↔ localVariables) when
   the requested store is empty for that transformed name.

Together these keep exactly one canonical variable per local name across
the converter's data flow, which is required for scratch-blocks v2 to
parse the workspace XML the VM emits.

Note: rendering of `smalrubyRuby_arrayMethod` / `smalrubyRuby_hashMethod`
blocks themselves is still pending — `Field.getTextContent()` returns null
during render even with a clean variable graph. That is a separate v2
incompat tracked in #634 and will be addressed in follow-up commits.
…h on v2

Issue #634 — final fix.

Smalruby's `defineDynamicBlock.updateBlockDisplay` followed a v1-style
pattern that toggled `block.rendered = false` around `appendField` and
re-issued `block.initSvg()` + `block.render()` at the end:

    const wasRendered = block.rendered;
    block.rendered = false;
    ...
    createAllInputs(block, ...);   // → appendField(new FieldDropdown(...))
    ...
    block.rendered = wasRendered;
    if (wasRendered) { block.initSvg(); block.render(); }

In Blockly v12 (the runtime under scratch-blocks v2.1.19) this dance
silently drops field initialisation:

  Input.appendField(field, name) {
    field.setSourceBlock(this.sourceBlock);
    this.sourceBlock.initialized && this.initField(field);
    this.sourceBlock.rendered && this.sourceBlock.queueRender();
  }
  Input.initField(field) {
    this.sourceBlock.rendered ? field.init() : field.initModel();
  }

For a block constructed via `domToMutation`:

* `block.initialized` is `false` until `initSvg()` runs the one-shot guard.
* `block.rendered` is `true` (BlockSvg constructor default).

Smalruby was forcing `rendered = false` during `appendField`, so:
* `appendField` saw `initialized=false` → skipped `initField` entirely
  → field's SVG `<text>` element never created.
* The follow-up `block.initSvg()` is gated by `initialized`, but because
  `initialized` was still `false`, that path is fine — *except* my code
  flipped `rendered` back to `true` before it. With `rendered=true`,
  `initSvg`'s field-init would run `field.init()` (which creates the SVG
  text). Either path *should* work.
* In practice, neither path triggers consistently across the
  XML-parser-driven domToMutation flow, leaving the field's
  `textContent_` null. The next `block.render()` invokes the block
  renderer, which calls `Field.getSize` → `Field.render_` →
  `Field.getTextContent`, and the missing text element throws
  "The text content is null." Stack:

    at Field.getTextContent
    at Field.render_
    at Field.getSize
    at new BlockRenderRow
    at BlockRenderer.createRows_
    at BlockRenderer.measure
    at Block.render

The error aborts the parser mid-tree, drops the rest of the block's
`<next>` chain, and leaves Smalruby's flyout/workspace unable to render
any script that references `smalrubyRuby_arrayMethod`,
`smalrubyRuby_hashMethod`, or `smalrubyRuby_stringMethod`.

Fix: stop toggling `block.rendered`. Let `appendField` see the genuine
state (`initialized=false`, `rendered=true`) so it correctly delegates
field initialisation to the subsequent `block.initSvg()` call we make at
the end. `block.initSvg()` walks `inputList` and calls `field.init()`
for every field, which creates each field's SVG `<text>` element. We
also keep `block.render()` to re-measure the block with its new inputs.

This mirrors what scratch-blocks v2's own `procedures.ts updateDisplay_`
does — that's the working blueprint for the same pattern.

After the fix:

* `puts(a.max)` round-trips correctly (`a = [1, 2, 3]` block chain
  followed by `Array (a) . max` connected to `戻り値 と 1 秒言う`).
* `Field.getTextContent` no longer throws.
* Toolbox refresh no longer fails.

Verified manually with Playwright on `?tab=ruby&ruby_version=2`.
takaokouji added 9 commits May 4, 2026 23:08
… on v2

Three coordinated fixes for the array/hash method flyout crash on
scratch-blocks v2:

1. define-dynamic-block.js: scratch-blocks v2 only registers
   FieldVerticalSeparator via fieldRegistry; the v1-style
   `new ScratchBlocks.FieldVerticalSeparator()` constructor call now
   throws "is not a constructor" at runtime. Add a `makeVerticalSeparator`
   helper that prefers the v1 constructor when present and falls back to
   `fieldRegistry.fromJson({type: 'field_vertical_separator'})` for v2.

2. target-applier.js: the converter eagerly creates a SCALAR entry in
   `_context.localVariables` for `t = [...]` before visiting the RHS.
   The array-literal handler then creates a LIST entry in `_context.lists`
   under the same transformed name. Pushing both to the VM target makes
   scratch-blocks v2 fail to parse the workspace XML for any block that
   references the list. Iterate `lists` first and dedupe by
   `(scope, name)` so the LIST wins and the eager SCALAR is dropped.

3. smalruby-ruby.js (Ruby generator): the bang-method generator
   (`sort!`, `reverse!`) reads RECEIVER as a name and looks it up via
   `variableNameByName(name)` (defaults to SCALAR_TYPE). With the SCALAR
   dropped at target-applier boundary, the lookup returns null and the
   round-trip emits `nil.sort!`. Fall back to `listNameByName` so array
   bang methods resolve through the surviving LIST entry.

Closes #634
…-flyout-crash

fix: resolve scratch-blocks v2 render crash for arrayMethod/hashMethod (#634)
`blocks-screenshot.js` was already migrated to the v2 method-style API
(`workspace.getCanvas()` / `workspace.getBubbleCanvas()`), but the unit
test mock still exposed the v1 direct-property surface
(`svgBlockCanvas_` / `svgBubbleCanvas_`). 9 tests failed with
`TypeError: workspace.getBubbleCanvas is not a function` on
feat/upstream-merge-2026-05 CI.

Update `makeMockWorkspace` to return jest.fn-wrapped getters, and
replace the one inline `svgBubbleCanvas_` access with the equivalent
method call.

Closes #636
…th comments

Three coordinated fixes that together stop `toBlob()` from throwing
`SecurityError: Tainted canvases may not be exported.` when the workspace
contains the `@ruby:*` comments produced by the Ruby→Blocks converter:

1. Strip `<foreignObject>` from the BLOCK canvas clone, not only the
   bubble canvas. In scratch-blocks v2 (Blockly v12), block-attached
   comments are nested inside the block canvas and contain a
   `<foreignObject>` for the editing UI. Once the export SVG is loaded
   via `blob://`, those foreign objects taint the canvas.

2. Strip external `url(...)` references from injected `<style>` blocks
   via a new `stripExternalCssUrls` helper. The blocky stylesheet that
   carries the `blocklyText` color rules also contains
   `url(./static/blocks-media/.../sprites.png)` and friends — they're
   only used for cursors and toolbox sprite chrome, but the relative
   refs taint the rasteriser.

3. Drop hidden `<image>` elements that have an empty `href` (used as
   placeholders inside dynamic-block dropdowns, `display:none`). An
   empty href falls back to the document URL and tainted canvas under
   blob origin.

Adds unit tests for each piece and exports `stripExternalCssUrls` for
isolated testing.

Verified live: project with `puts(a.max)` / `t.sort!` etc. that emit
multiple `@ruby:*` comments now exports a PNG without errors.
…t its joined string

`executeArrayMethod` (non-bang) reconstructed `items` from
`String(args.RECEIVER).split(' ')`. Scratch's `data_listcontents`
joins single-character items with NO separator (e.g. `[3,1,4,1,5,9,2,6]`
becomes `"31415926"`), so the split returned a single token and every
method (max/min/first/last/sort/join/reverse) returned that joined
string back rather than the computed value.

Walk the block tree from `util.thread.peekStack()` to find the
RECEIVER input block; if it's a `data_listcontents`, look up the list
directly via `target.lookupVariableById` / `lookupVariableByNameAndType`
and operate on `list.value`. Falls back to the space-split heuristic
for non-list receivers (e.g. literal strings).

Verified live: `arr = [3,1,4,1,5,9,2,6]` now reports
- arr.max = "9", arr.min = "1", arr.first = "3", arr.last = "6"
- arr.sort.join(",") = "1,1,2,3,4,5,6,9"
- arr.sort! mutates the list to [1,1,2,3,4,5,6,9]
- arr.join("-") = "1-1-2-3-4-5-6-9"
…canvas

test(blocks-screenshot): align mock workspace with scratch-blocks v2 API
…-blocks v2

scratch-blocks v2's `ScratchZoomControls` uses MARGIN_VERTICAL =
MARGIN_HORIZONTAL = 20 (vs v1's 12) inside the workspace's view metrics
(which exclude the 11px scratch-blocks scrollbar). The Code tab camera
button was hard-coded at right=22/bottom=154 — values tuned for v1 — so
it drifted off the v2 zoom column on desktop and shifted further on
mobile / window resize because scrollbar width is platform dependent.

The Ruby tab's zoom column was at right=22/bottom=22, which sat 11px
inboard of the Code tab's actual on-screen position, producing a visible
jump when switching tabs.

Fixes:

- `BlocksScreenshotButton` now measures the live `.blocklyZoomIn`
  position via `useLayoutEffect` + `ResizeObserver` and pins the camera
  button 8px above its top edge, with right edges flush. Survives
  scrollbar variance, mobile orientation, and any v2 zoom-controls
  reflow. CSS keeps right=20/bottom=152 as a one-frame fallback that
  matches the v2 layout (MARGIN(20) + ZOOM_HEIGHT(124) + GAP(8)).

- `ruby-tab.css` `.zoomControlsWrapper` moves to right=31/bottom=31
  (= MARGIN(20) + scratch-blocks scrollbar(11)) so the Ruby tab column
  lands at the same screen X/Y as the Code tab's zoom controls.
  Verified with Playwright at desktop and 800/812-wide viewports —
  camera buttons match within 1px.

Removes orphan `.downloadButton` / `.downloadIcon` / `.downloadWrapper`
rules from ruby-tab.css; ruby-tab.jsx hasn't used them since the camera
moved into `zoomControlsWrapper`.
…osition

fix(zoom-ui): align screenshot button + Ruby zoom column with scratch-blocks v2
@takaokouji takaokouji merged commit dedebb4 into develop May 5, 2026
9 checks passed
@takaokouji takaokouji deleted the feat/upstream-merge-2026-05 branch May 5, 2026 11:35
github-actions Bot pushed a commit that referenced this pull request May 5, 2026
…-merge-2026-05

feat: upstream merge 2026-05 (v13.7.2 — 284 commits, scratch-blocks v2)
takaokouji added a commit that referenced this pull request May 13, 2026
…tream merge

The Shimaraby and Shimacat sprite/costume entries were silently overwritten
during upstream merges (most recently in PR #630, and also in the v13.7.2
merge before that). The asset PNGs in static/smalruby-assets/ remained but
the library entries pointing to them were gone, so the sprites disappeared
from the editor.

- Re-add Shimaraby and Shimacat sprite entries to sprites.json (between Shark 2 and Shirt)
- Re-add Shimaraby-a/b and Shimacat-a/b costume entries to costumes.json
- Add smalruby-original-sprites.test.js as a regression guard so that future
  upstream merges fail loudly if these entries are wiped again

Refs #688

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
takaokouji added a commit that referenced this pull request May 13, 2026
…sprites

Adds a section to phase2-conflicts.md warning that sprites.json /
costumes.json upstream changes do not produce git conflicts but can
silently wipe Smalruby-original entries (Shimaraby, Shimacat). Includes
the verification commands and the trademark-sprite checklist that
must be run after every upstream merge.

This is the recurrence-prevention measure for the issue caught in #688
where two consecutive upstream merges (#630 and the v13.7.2 merge before
it) wiped Shimaraby/Shimacat without anyone noticing.

Refs #688

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] cannot drag argument reporter from prototype

6 participants