diff --git a/.gitignore b/.gitignore
index e3009e6..18e828f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ node_modules/
.pnp.loader.mjs
.yarnrc.yml
*storybook.log
+.env
+drafts/
diff --git a/AGENTS.md b/AGENTS.md
index 33ec614..cab7d21 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,12 +11,6 @@
respond in that language.
- If a message is mixed-language, reply in the dominant language unless the
user specifies otherwise.
-- Run `make eslint` before handoff or commit preparation only when changed
- files include code covered by eslint rules (for example `*.js`, `*.ts`,
- and similar source files). Do not run `make eslint` for markdown-only
- changes (for example `*.md`).
-- Getter/helper functions must be side-effect free. Side effects are allowed
- only by prior agreement and only when there are strong, explicit reasons.
## Reporting
- Keep handoff reports natural and outcome-focused: describe what was done.
@@ -26,9 +20,9 @@
affect correctness, safety, or reproducibility.
## Purpose
-This file defines practical instructions for working in the
-`modulify/m3-web` repository, with a focus on test execution and commit
-workflow.
+This file defines practical instructions for day-to-day work in the
+`modulify/m3-web` repository, with a focus on repository conventions,
+test execution, and standard local commands.
## Repository Structure
- This project is a Yarn Workspaces monorepo.
@@ -38,13 +32,27 @@ workflow.
- Vitest workspace targets are declared in `vitest.workspace.ts`:
`m3-react`, `m3-vue`.
-## Local Environment Prerequisites
+## Architecture Rules
+- Getter/helper functions must be side-effect free. Side effects are allowed
+ only by prior agreement and only when there are strong, explicit reasons.
+- Tests inside a workspace must cover only code owned by that workspace. In
+ `m3-foundation`, `m3-react`, `m3-vue`, and any future workspace, do not add
+ tests for implementation that belongs to another workspace.
+- Cross-workspace imports must go through the target workspace package name as
+ declared in `package.json`. Relative or absolute filesystem imports into
+ another workspace are forbidden. For example,
+ `import type { Appearance } from '@modulify/m3-foundation/types/components/button'`
+ is allowed, but
+ `import type { Appearance } from '../../../m3-foundation/types/components/button'`
+ is not.
+
+## Local setup
- Yarn version is `4.6.0` (see `packageManager` in `package.json`).
-- Local `.yarnrc.yml` is generated from `.yarnrc.yml.dist` using:
+- Generate local `.yarnrc.yml` from `.yarnrc.yml.dist`:
```bash
make .yarnrc.yml
```
-- Install dependencies with:
+- Install dependencies:
```bash
make node_modules
# or
@@ -54,16 +62,6 @@ yarn install
## Running Tests
### Local Path
-- Generate local Yarn config:
-```bash
-make .yarnrc.yml
-```
-- Install dependencies:
-```bash
-make node_modules
-# or
-yarn install
-```
- Run all tests:
```bash
make test
@@ -104,44 +102,47 @@ make help
```
## Important Project Rules
-- Commit messages follow Conventional Commits.
-- Commitlint configuration is in `.commitlintrc.json` with:
- `header-max-length=200`, `body-max-line-length=200`,
- `footer-max-line-length=200`, `subject-case=never`.
-- Getter/helper functions must be side-effect free. Side effects are allowed
- only by prior agreement and only when there are strong, explicit reasons.
+- Before performing actions, analyze whether there is a suitable local skill
+ for the task and consult it for detailed instructions.
+- Before performing repeated or operational actions, inspect `make help` and
+ its output to see whether an existing recipe already covers the task.
+- If a suitable recipe exists, prefer it over ad hoc commands to reduce extra
+ work, keep workflows standardized, and avoid unnecessary escalations.
+- The project includes a Playwright container and make recipes for screenshot
+ capture; use them when visual analysis of Storybook pages, component states,
+ or other UI behavior is helpful.
+- The project also includes runtime-analysis research recipes for DOM, styles,
+ layout metrics, a11y snapshots, traces, network/performance logs, token
+ diffs, and screenshot matrices; use them to reduce uncertainty and to
+ understand what is going wrong before guessing at visual or runtime issues.
+ Read `docs/en/runtime-analysis-recipes.md` first when the task involves
+ visual regressions, layout ambiguity, token/theme uncertainty, unclear
+ animation behavior, or other runtime issues where these recipes may help.
+- Run eslint before handoff or commit preparation only when changed files
+ include code covered by eslint rules (for example `*.js`, `*.ts`, and
+ similar source files). Do not run eslint for markdown-only changes.
+- Prefer running eslint with `--fix` when available so autofixable issues are
+ resolved automatically before manual follow-up.
+
+## Skills
+The skills listed below are stored locally in this repository under `skills/`.
+
+If the context was compacted and you see `Context compacted`, reread any skill
+whose description below starts with `[reread]` after the colon before
+continuing.
-## Commit Workflow
-- Default commit message language is English (unless explicitly requested
- otherwise).
-- Commit style is Conventional Commits.
-- Write commit subjects as historical facts (not intentions).
-- Start commit subject description with an uppercase letter.
-- Keep commit subject description concise.
-- Move long details to commit body; lists in body are allowed for enumerations.
-- Use past/perfective wording; prefer passive voice for changelog-friendly phrasing.
-Examples: `Added ...`, `Removed ...`, `Refactored ...`, `Fixed ...`.
-- Respect commitlint limits from `.commitlintrc.json`:
- `header-max-length=200`, `body-max-line-length=200`,
- `footer-max-line-length=200`.
-- For workspace commits, use scope equal to the workspace directory name:
- `m3-foundation`, `m3-react`, `m3-vue`.
-- Split commits by logical change. Workspace-local changes should stay in
- their workspace scope.
-- Changes in `yarn.lock` must always be committed separately from all other files.
-- Commit message for `yarn.lock`-only commit must be exactly:
-`chore: Updated yarn.lock`.
-- Exception for intentional dependency updates:
-if commit purpose is dependency update (`yarn up`, `yarn add`, `yarn remove`, etc.),
-after rebase conflict resolution rerun the original dependency command and recreate separate
-`chore: Updated yarn.lock` commit.
-- Exception: global workspace-level changes can be combined in one commit.
-Global examples: eslint rule updates, shared dependency updates, repository-level infra/config changes.
-- For commit tasks, use the local skill:
-`skills/commit-workflow/SKILL.md`.
-- For `yarn.lock` merge/rebase conflict resolution, use the local skill:
-`skills/yarn-lock-conflict-resolution/SKILL.md`.
-- For coverage deficit analysis and recovery strategy, use the local skill:
-`skills/coverage-recovery/SKILL.md`.
-- For documentation creation or edits under `docs/` with locale parity, use the local skill:
-`skills/docs-parity/SKILL.md`.
+- `commit-workflow`: [reread] Use when creating or splitting git commits in
+ this repository. Reread it before every commit creation; it standardizes
+ commit grouping, Conventional Commits, workspace scopes, and commitlint
+ limits.
+- `coverage-recovery`: Use when coverage is below target or uncovered code must
+ be analyzed and closed without adding artificial tests.
+- `docs-parity`: Use when creating or editing files under `docs/`. Keeps
+ English-first edits, locale parity, and locale index updates aligned.
+- `exploration-workflow`: [reread] Use only when the user explicitly switches
+ the task into exploration mode: autonomous hypothesis-driven work on
+ uncertain functionality, with timeboxing, milestone logging, and tightly
+ controlled pre-agreed escalation windows, plus `drafts/current.yml` and a
+ dedicated `drafts/` activity directory for logs, facts, and artifact links.
+- `yarn-lock-conflict-resolution`: Use when resolving merge or rebase conflicts
+ in `yarn.lock` according to repository policy.
diff --git a/Makefile b/Makefile
index 824cfbf..1276beb 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,18 @@
+.DEFAULT_GOAL := help
+
TARGET_HEADER=@echo -e '===== \e[34m' $@ '\e[0m'
TARGET_OK=@echo -e '\e[32mOK\e[0m'
YARN=@docker-compose run --rm node yarn
-YARN_PLAYWRIGHT=@docker-compose run --rm playwright yarn
-COVERAGE_PARTS_DIR=coverage/.parts
-COVERAGE_UNIT_DIR=coverage/unit
-COVERAGE_E2E_REACT_DIR=coverage/e2e-react
-COVERAGE_E2E_VUE_DIR=coverage/e2e-vue
-NYC_OUTPUT_DIR=.nyc_output
+PLAYWRIGHT_NODE_CMD=docker-compose run --rm playwright node
+M3_UA?=m3-web-research/1.0
.PHONY: up
-up: ## Starts storybook
+up: ## [Dev][docker] Starts storybook
$(TARGET_HEADER)
docker-compose up -d
.PHONY: restart
-restart: ## Restarts all docker services or a particular service, if argument "service" is specified (example: make restart service="storybook").
+restart: ## [Dev][docker] Restarts all docker services or a particular service, if argument "service" is specified (example: make restart service="storybook")
$(TARGET_HEADER)
ifdef service
@@ -24,213 +22,27 @@ else
endif
.PHONY: stop
-stop: ## Stops all docker services
+stop: ## [Dev][docker] Stops all docker services
$(TARGET_HEADER)
docker-compose stop
-.PHONY: .yarnrc.yml
-.yarnrc.yml: ## Creates yarn configuration
- @cp .yarnrc.yml.dist .yarnrc.yml
-
-.PHONY: node_modules
-node_modules: package.json yarn.lock ## Installs dependencies
- $(TARGET_HEADER)
- $(YARN) install
- @touch node_modules || true
- @echo ""
+include recipes/common.mk
.PHONY: build
-build: node_modules ## Creates a dist catalogue with library build
+build: node_modules ## [Build][docker] Creates a dist catalogue with library build
$(TARGET_HEADER)
$(YARN) build
-.PHONY: storybook-build-test
-storybook-build-test: node_modules ## Builds Storybook in --test mode for all UI workspaces
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-react storybook:build --test --quiet
- $(YARN) workspace @modulify/m3-vue storybook:build --test --quiet
-
-.PHONY: storybook-build-test-react
-storybook-build-test-react: node_modules ## Builds Storybook in --test mode for @modulify/m3-react
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-react storybook:build --test --quiet
-
-.PHONY: storybook-build-test-vue
-storybook-build-test-vue: node_modules ## Builds Storybook in --test mode for @modulify/m3-vue
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-vue storybook:build --test --quiet
-
-.PHONY: test-smoke
-test-smoke: node_modules ## Runs smoke tests for all UI workspaces
- $(TARGET_HEADER)
- $(YARN) test:smoke
-
-.PHONY: test-runtime-parity
-test-runtime-parity: ## Checks Node/Yarn parity across docker services (node, storybook, playwright)
- $(TARGET_HEADER)
- ./runtime-parity.test.sh
-
-.PHONY: husky
-husky: node_modules ## Adds husky git hooks with commit content checks
- @docker-compose run --rm node npx husky init
-
-.PHONY: eslint
-eslint: node_modules ## Runs eslint
- $(TARGET_HEADER)
- $(YARN) eslint
-
-.PHONY: tsc
-tsc: node_modules ## Runs type checks in all workspaces
- $(TARGET_HEADER)
- $(YARN) tsc
-
-.PHONY: tsc-foundation
-tsc-foundation: node_modules ## Runs type checks in @modulify/m3-foundation
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-foundation tsc
-
-.PHONY: tsc-react
-tsc-react: node_modules ## Runs type checks in @modulify/m3-react
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-react tsc
-
-.PHONY: tsc-vue
-tsc-vue: node_modules ## Runs type checks in @modulify/m3-vue
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-vue tsc
-
-.PHONY: tsc-tests
-tsc-tests: node_modules ## Runs type checks for tests in all UI workspaces
- $(TARGET_HEADER)
- $(YARN) tsc:tests
-
-.PHONY: tsc-tests-react
-tsc-tests-react: node_modules ## Runs type checks for tests in @modulify/m3-react
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-react tsc:tests
-
-.PHONY: tsc-tests-vue
-tsc-tests-vue: node_modules ## Runs type checks for tests in @modulify/m3-vue
- $(TARGET_HEADER)
- $(YARN) workspace @modulify/m3-vue tsc:tests
-
-.PHONY: tsc-e2e
-tsc-e2e: node_modules ## Runs type checks for Playwright Vitest configs
- $(TARGET_HEADER)
- $(YARN) exec tsc -p tsconfig.e2e.json --skipLibCheck
-
-.PHONY: test
-test: node_modules ## Runs autotests
- $(TARGET_HEADER)
-
-ifdef cli
- @echo "${YARN} test ${cli}"
- $(YARN) test $(cli)
-else
- @echo "${YARN} test"
- $(YARN) test
-endif
-
-.PHONY: test-coverage
-test-coverage: node_modules ## Runs merged coverage for unit and Playwright e2e tests
- $(TARGET_HEADER)
- @rm -rf coverage $(NYC_OUTPUT_DIR) artifacts
- @mkdir -p $(COVERAGE_PARTS_DIR) $(NYC_OUTPUT_DIR)
- $(YARN) test --coverage --coverage.provider=istanbul --coverage.reporter=json --coverage.reportsDirectory=$(COVERAGE_UNIT_DIR)
- $(YARN_PLAYWRIGHT) test:e2e:coverage
- @cp $(COVERAGE_UNIT_DIR)/coverage-final.json $(COVERAGE_PARTS_DIR)/unit.json
- @cp $(COVERAGE_E2E_REACT_DIR)/coverage-final.json $(COVERAGE_PARTS_DIR)/e2e-react.json
- @cp $(COVERAGE_E2E_VUE_DIR)/coverage-final.json $(COVERAGE_PARTS_DIR)/e2e-vue.json
- @$(YARN) nyc merge $(COVERAGE_PARTS_DIR) $(NYC_OUTPUT_DIR)/coverage-final.json >/dev/null
- $(YARN) nyc report
- @$(YARN) nyc report --reporter=json-summary >/dev/null
- @$(YARN) node --experimental-strip-types scripts/show-total-coverage.ts
-
-.PHONY: test-e2e
-test-e2e: node_modules ## Runs Playwright-based e2e tests (Vitest browser mode)
- $(TARGET_HEADER)
-
-ifdef cli
- @$(YARN_PLAYWRIGHT) test:e2e $(cli)
-else
- @$(YARN_PLAYWRIGHT) test:e2e
-endif
-
-.PHONY: test-e2e-react
-test-e2e-react: node_modules ## Runs Playwright-based e2e tests for @modulify/m3-react
- $(TARGET_HEADER)
-
-ifdef cli
- @$(YARN_PLAYWRIGHT) workspace @modulify/m3-react test:e2e $(cli)
-else
- @$(YARN_PLAYWRIGHT) workspace @modulify/m3-react test:e2e
-endif
-
-.PHONY: test-e2e-vue
-test-e2e-vue: node_modules ## Runs Playwright-based e2e tests for @modulify/m3-vue
- $(TARGET_HEADER)
-
-ifdef cli
- @$(YARN_PLAYWRIGHT) workspace @modulify/m3-vue test:e2e $(cli)
-else
- @$(YARN_PLAYWRIGHT) workspace @modulify/m3-vue test:e2e
-endif
-
-.PHONY: test-e2e-stop
-test-e2e-stop: ## Stops stuck Playwright E2E host processes and run containers
- $(TARGET_HEADER)
- @pkill -TERM -f "docker-compose run --rm playwright yarn test:[e]2e" || true
- @pkill -TERM -f "docker-compose run --rm playwright yarn workspace @modulify/m3-react test:[e]2e" || true
- @pkill -TERM -f "docker-compose run --rm playwright yarn workspace @modulify/m3-vue test:[e]2e" || true
- @docker ps -q --filter "name=m3-web-playwright-run" | xargs -r docker rm -f
-
-.PHONY: ci-actionlint
-ci-actionlint: ## Lints GitHub Actions workflows locally (actionlint binary or docker image)
- $(TARGET_HEADER)
- @if command -v actionlint >/dev/null 2>&1; then \
- actionlint; \
- elif command -v docker >/dev/null 2>&1; then \
- docker run --rm -v "$$(pwd):/repo" -w /repo rhysd/actionlint:latest; \
- else \
- echo "actionlint is not installed and docker is unavailable"; \
- exit 1; \
- fi
- $(TARGET_OK)
-
-.PHONY: ci-act-plan
-ci-act-plan: ## Shows act execution plan for tests workflow without running jobs
- $(TARGET_HEADER)
- @if command -v act >/dev/null 2>&1; then \
- act -P ubuntu-latest=catthehacker/ubuntu:act-latest -n pull_request -W .github/workflows/tests.yml; \
- elif command -v docker >/dev/null 2>&1; then \
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$$(pwd):/repo" -w /repo alpine:3.20 sh -lc "apk add --no-cache curl tar >/dev/null && curl -fsSL https://github.com/nektos/act/releases/download/v0.2.84/act_Linux_x86_64.tar.gz | tar -xz -C /tmp && /tmp/act -P ubuntu-latest=catthehacker/ubuntu:act-latest -n pull_request -W .github/workflows/tests.yml"; \
- else \
- echo "act is not installed and docker is unavailable"; \
- exit 1; \
- fi
- $(TARGET_OK)
-
-.PHONY: ci-act-tests
-ci-act-tests: ## Runs tests workflow locally via act (pr-check, eslint, tests, storybook-tests)
- $(TARGET_HEADER)
- @if command -v act >/dev/null 2>&1; then \
- act -P ubuntu-latest=catthehacker/ubuntu:act-latest pull_request -W .github/workflows/tests.yml -j pr-check -j eslint -j tests -j storybook-tests; \
- elif command -v docker >/dev/null 2>&1; then \
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$$(pwd):/repo" -w /repo alpine:3.20 sh -lc "apk add --no-cache curl tar >/dev/null && curl -fsSL https://github.com/nektos/act/releases/download/v0.2.84/act_Linux_x86_64.tar.gz | tar -xz -C /tmp && /tmp/act -P ubuntu-latest=catthehacker/ubuntu:act-latest pull_request -W .github/workflows/tests.yml -j pr-check -j eslint -j tests -j storybook-tests"; \
- else \
- echo "act is not installed and docker is unavailable"; \
- exit 1; \
- fi
- $(TARGET_OK)
-
-.PHONY: ci-check
-ci-check: ci-actionlint ci-act-plan ## Validates CI workflow config and prints local act plan
- $(TARGET_OK)
+include recipes/lint.mk
+include recipes/tests.mk
+include recipes/storybook.mk
+include recipes/ci.mk
+include recipes/research.mk
+include recipes/exploration.mk
.PHONY: help
-help: ## Calls recipes list
- @cat $(MAKEFILE_LIST) | grep -e "^[-a-zA-Z_\.]*: *.*## *" | awk '\
- BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+help: ## [General] Shows grouped command help
+ @HELP_FILTER="$(value filter)" HELP_SHOW_INTERNAL="$(value show_internal)" ./recipes/help.sh $(MAKEFILE_LIST)
# Colors
$(call computable,CC_BLACK,$(shell tput -Txterm setaf 0 2>/dev/null))
diff --git a/docs/en/index.md b/docs/en/index.md
index 3d3428c..012396c 100644
--- a/docs/en/index.md
+++ b/docs/en/index.md
@@ -3,6 +3,7 @@
## Contents
- [Contributing Guide](./contributing.md) - Development workflow, core commands, and CI quality gates.
+- [Runtime Analysis Recipes](./runtime-analysis-recipes.md) - Playwright-based recipes for visual, layout, style, motion, and runtime investigation.
- [TSConfig Layering](./tsconfig-layering.md) - How TypeScript configs are split between editor DX and CI checks.
## Translations
diff --git a/docs/en/runtime-analysis-recipes.md b/docs/en/runtime-analysis-recipes.md
new file mode 100644
index 0000000..7bed121
--- /dev/null
+++ b/docs/en/runtime-analysis-recipes.md
@@ -0,0 +1,103 @@
+# Runtime Analysis Recipes
+
+This document describes the Playwright-based research recipes available in
+`m3-web` for reducing uncertainty when visual or runtime behavior is unclear.
+
+## When To Use
+
+Use these recipes when:
+- a Storybook page looks wrong and screenshots are not enough;
+- a layout shifts, overlaps, or animates unexpectedly;
+- local theming or CSS tokens appear to be ignored;
+- React and Vue parity is unclear;
+- visual regressions need before/after or matrix comparison;
+- runtime errors may be hidden in console, network, or accessibility state.
+
+## Core Principle
+
+Prefer these recipes before guessing. They are intended to turn “something is
+off” into concrete evidence: screenshots, computed styles, layout metrics,
+accessibility trees, traces, and diffs.
+
+## Most Useful Recipes
+
+- `make research-capture url='...'` - Single screenshot with metadata.
+- `make research-capture-batch in=urls.txt out_dir=drafts/screenshots/...` -
+ Batch screenshot capture from a URL list.
+- `make research-style-dump url='...' selector='...'` - Computed styles and CSS
+ custom properties for one element.
+- `make research-layout-metrics url='...' selectors='.a||.b||.c'` - Bounding
+ boxes, scroll metrics, offsets, and layout details for multiple elements.
+- `make research-token-diff url='...' selector='.left' compare_selector='.right'`
+ - CSS variable diff between two scopes.
+- `make research-console-capture url='...'` - Console messages, page errors,
+ and failed requests.
+- `make research-a11y-snapshot url='...' selector='...'` - Accessibility tree
+ for the page or a subtree.
+- `make research-trace url='...' action_selector='...' interaction=click` -
+ Playwright trace for a page and optional interaction.
+- `make research-motion-sample url='...' action_selector='...' interaction=click`
+ - Timed frame sequence for motion and transitions.
+- `make research-capture-diff left=before.png right=after.png out=diff.png` -
+ Pixel diff for two images or directories.
+- `make research-capture-matrix ...` - Capture one URL across multiple themes,
+ globals, args, and viewports.
+- `make research-story-props story_id='...' themes='light,dark' args_sets='...'`
+ - Storybook-oriented matrix capture for story args and globals.
+
+## Typical Workflows
+
+## Investigate A Broken Layout
+
+1. Capture the current page:
+ `make research-capture url='...'`
+2. Dump layout metrics:
+ `make research-layout-metrics url='...' selectors='.host||.panel||.scrim'`
+3. Dump computed styles for the suspicious node:
+ `make research-style-dump url='...' selector='.panel'`
+
+## Investigate A Theme Or Token Problem
+
+1. Capture the page in both themes:
+ `make research-capture-matrix story_id='...' themes='light,dark'`
+2. Compare token scopes:
+ `make research-token-diff url='...' selector='.default-scope' compare_selector='.local-scope'`
+3. Inspect computed variables:
+ `make research-style-dump url='...' selector='.local-scope' var_prefixes='--m3-sys-,--m3-state-layers-'`
+
+## Investigate Animation Or Motion
+
+1. Capture a motion sequence:
+ `make research-motion-sample url='...' action_selector='...' interaction=click`
+2. If behavior is still unclear, capture a trace:
+ `make research-trace url='...' action_selector='...' interaction=click`
+
+## Compare Two Runtime States
+
+1. Capture both states into separate directories.
+2. Run:
+ `make research-capture-diff left=dir-a right=dir-b out_dir=drafts/research/diff`
+
+## Storybook Notes
+
+- Storybook pages can be opened directly with:
+ `http://m3-vue.modulify.test/?path=/story/...&globals=theme:dark`
+- For systematic capture across states, prefer `research-story-props` and
+ `research-capture-matrix` over manually building many URLs.
+
+## Output Conventions
+
+- Screenshots are written under `drafts/screenshots/...` unless overridden.
+- Runtime inspection outputs default to `drafts/research/...`.
+- Most recipes emit machine-readable JSON so results can be inspected or
+ compared later.
+
+## Recommendation
+
+When a bug report is vague, start with:
+1. `research-capture`
+2. `research-style-dump`
+3. `research-layout-metrics`
+
+That combination usually clarifies whether the problem is geometry, styling,
+token inheritance, or runtime state.
diff --git a/docs/ru/index.md b/docs/ru/index.md
index 6a36c7c..e5f7526 100644
--- a/docs/ru/index.md
+++ b/docs/ru/index.md
@@ -3,4 +3,5 @@
## Содержание
- [Руководство по участию](./contributing.md) - Процесс разработки, ключевые команды и проверки в CI.
+- [Рецепты runtime-анализа](./runtime-analysis-recipes.md) - Playwright-рецепты для исследования visual, layout, style, motion и runtime-проблем.
- [Слои TSConfig](./tsconfig-layering.md) - Разделение TypeScript-конфигов для IDE и package-level typecheck.
diff --git a/docs/ru/runtime-analysis-recipes.md b/docs/ru/runtime-analysis-recipes.md
new file mode 100644
index 0000000..32cd720
--- /dev/null
+++ b/docs/ru/runtime-analysis-recipes.md
@@ -0,0 +1,105 @@
+# Рецепты runtime-анализа
+
+Этот документ описывает Playwright-рецепты исследования, доступные в
+`m3-web`, чтобы снимать неопределённость, когда визуальное или runtime-поведение
+непонятно.
+
+## Когда применять
+
+Используйте эти рецепты, когда:
+- Storybook-страница выглядит неправильно, и одного скриншота недостаточно;
+- layout сдвигается, наползает или анимируется неожиданно;
+- локальная тема или CSS-токены как будто игнорируются;
+- неясно, есть ли parity между React и Vue;
+- нужно сравнить состояния до/после или матрицу сценариев;
+- runtime-ошибки могут прятаться в console, network или accessibility-состоянии.
+
+## Базовый принцип
+
+Предпочитайте эти рецепты угадыванию. Их задача — превращать расплывчатое
+«что-то не так» в конкретные артефакты: скриншоты, computed styles, layout
+метрики, accessibility tree, traces и diffs.
+
+## Самые полезные рецепты
+
+- `make research-capture url='...'` - Один скриншот с метаданными.
+- `make research-capture-batch in=urls.txt out_dir=drafts/screenshots/...` -
+ Пакетное снятие скриншотов по списку URL.
+- `make research-style-dump url='...' selector='...'` - Computed styles и CSS
+ custom properties для одного элемента.
+- `make research-layout-metrics url='...' selectors='.a||.b||.c'` - Bounding
+ boxes, scroll-метрики, offsets и layout-детали для нескольких элементов.
+- `make research-token-diff url='...' selector='.left' compare_selector='.right'`
+ - Diff CSS-переменных между двумя scope.
+- `make research-console-capture url='...'` - Console messages, page errors и
+ failed requests.
+- `make research-a11y-snapshot url='...' selector='...'` - Accessibility tree
+ для страницы или subtree.
+- `make research-trace url='...' action_selector='...' interaction=click` -
+ Playwright trace для страницы и опционального взаимодействия.
+- `make research-motion-sample url='...' action_selector='...' interaction=click`
+ - Последовательность кадров для motion и transitions.
+- `make research-capture-diff left=before.png right=after.png out=diff.png` -
+ Pixel diff для двух изображений или каталогов.
+- `make research-capture-matrix ...` - Съёмка одного URL по нескольким themes,
+ globals, args и viewports.
+- `make research-story-props story_id='...' themes='light,dark' args_sets='...'`
+ - Storybook-oriented matrix capture для story args и globals.
+
+## Типовые сценарии
+
+## Разобрать сломанный layout
+
+1. Снять текущую страницу:
+ `make research-capture url='...'`
+2. Снять layout metrics:
+ `make research-layout-metrics url='...' selectors='.host||.panel||.scrim'`
+3. Снять computed styles для подозрительного узла:
+ `make research-style-dump url='...' selector='.panel'`
+
+## Разобрать проблему темы или токенов
+
+1. Снять страницу в обеих темах:
+ `make research-capture-matrix story_id='...' themes='light,dark'`
+2. Сравнить token scope:
+ `make research-token-diff url='...' selector='.default-scope' compare_selector='.local-scope'`
+3. Посмотреть computed variables:
+ `make research-style-dump url='...' selector='.local-scope' var_prefixes='--m3-sys-,--m3-state-layers-'`
+
+## Разобрать анимацию или motion
+
+1. Снять последовательность кадров:
+ `make research-motion-sample url='...' action_selector='...' interaction=click`
+2. Если поведение всё ещё неясно, снять trace:
+ `make research-trace url='...' action_selector='...' interaction=click`
+
+## Сравнить два runtime-состояния
+
+1. Снять оба состояния в разные каталоги.
+2. Запустить:
+ `make research-capture-diff left=dir-a right=dir-b out_dir=drafts/research/diff`
+
+## Примечания по Storybook
+
+- Storybook-страницы можно открывать напрямую так:
+ `http://m3-vue.modulify.test/?path=/story/...&globals=theme:dark`
+- Для систематической съёмки по состояниям лучше использовать
+ `research-story-props` и `research-capture-matrix`, а не собирать множество
+ URL вручную.
+
+## Формат артефактов
+
+- Скриншоты по умолчанию пишутся в `drafts/screenshots/...`.
+- Runtime inspection outputs по умолчанию пишутся в `drafts/research/...`.
+- Большинство рецептов сохраняет machine-readable JSON, чтобы результаты можно
+ было потом изучать и сравнивать.
+
+## Рекомендация
+
+Когда баг-репорт расплывчатый, начинайте с:
+1. `research-capture`
+2. `research-style-dump`
+3. `research-layout-metrics`
+
+Эта комбинация обычно быстро показывает, проблема в геометрии, styling,
+наследовании токенов или runtime-состоянии.
diff --git a/evidence/popper.md b/evidence/popper.md
new file mode 100644
index 0000000..b0468ac
--- /dev/null
+++ b/evidence/popper.md
@@ -0,0 +1,70 @@
+# Popper Animation Evidence
+Date: 2026-02-22
+
+## Sources
+### Primary references
+- User-provided design reference video:
+ - https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Fmhlk1jwz-GM3_Menus_Guidelines%2001_IA_v01.mp4?alt=media&token=2e7dfcc9-2447-4808-8234-83c313d82df8
+- Material 3 motion pages:
+ - https://m3.material.io/styles/motion/overview
+ - https://m3.material.io/foundations/motion/applying-easing-and-duration
+ - https://m3.material.io/styles/motion/easing-and-duration/tokens-specs
+
+### Token/easing references used as practical source of truth
+- Flutter generated motion tokens:
+ - https://flutter.googlesource.com/mirrors/packages/+/refs/tags/google_maps_flutter-v2.7.0/packages/flutter/lib/src/material/motion.dart
+- Flutter API docs for durations and easing:
+ - https://api.flutter.dev/flutter/material/Durations-class.html
+ - https://api.flutter.dev/flutter/material/Easing/standard-constant.html
+ - https://api.flutter.dev/flutter/material/Easing/standardAccelerate-constant.html
+ - https://api.flutter.dev/flutter/material/Easing/standardDecelerate-constant.html
+ - https://api.flutter.dev/flutter/material/Easing/emphasizedAccelerate-constant.html
+ - https://api.flutter.dev/flutter/material/Easing/emphasizedDecelerate-constant.html
+
+### Internal empirical source
+- Implementation and local validation log in `draft.txt`:
+ - wrapper split (`.m3-popper-positioner` + `.m3-popper`)
+ - updated E2E assertions
+ - local check commands and outcomes
+
+## Extracted Facts
+### Direct facts
+1. Positioning and animation transforms conflict if both are applied to the same element.
+2. Splitting responsibilities into two elements is technically viable:
+ - outer element handles geometry (`absolute/top/left/transform` from floating-ui),
+ - inner element handles visual animation (`transform/opacity/visibility`).
+3. Show ordering matters: first position (`await adjust`), then reveal content.
+4. Animation direction must use the effective side after flip (actual placement), not requested placement.
+5. Headless access to `m3.material.io` pages may be limited (JS-required shell), so token tables were taken from official generated/tokenized sources.
+6. Local verification was reported as green after stabilization:
+ - `tsc` for foundation/react/vue,
+ - `eslint`,
+ - unit and e2e checks for popper,
+ - combined coverage run (`80.11%`).
+
+### Motion token facts (from token references)
+1. Duration token scale:
+ - short: 50/100/150/200 ms,
+ - medium: 250/300/350/400 ms,
+ - long: 450/500/550/600 ms,
+ - extra long: 700/800/900/1000 ms.
+2. Easing token curves:
+ - standard: `cubic-bezier(0.2, 0.0, 0.0, 1.0)`,
+ - standardAccelerate: `cubic-bezier(0.3, 0.0, 1.0, 1.0)`,
+ - standardDecelerate: `cubic-bezier(0.0, 0.0, 0.0, 1.0)`,
+ - emphasizedAccelerate: `cubic-bezier(0.3, 0.0, 0.8, 0.15)`,
+ - emphasizedDecelerate: `cubic-bezier(0.05, 0.7, 0.1, 1.0)`,
+ - linear: `cubic-bezier(0.0, 0.0, 1.0, 1.0)`.
+
+### Inferences used for implementation tuning
+1. Enter animation should prefer decelerate-family easing, exit should prefer accelerate-family easing.
+2. Perceived "menu unfolding from anchor point" depends on:
+ - side-aware `transform-origin`,
+ - axis-dominant scale (uncollapse) with small translation offset.
+3. Candidate presets for iterative tuning:
+ - `short3/short1` or `short4/short2`,
+ - decelerate for enter + accelerate for exit.
+
+## Notes for future work
+- Keep parity of popper behavior checks in React and Vue E2E.
+- Freeze chosen duration/easing pair in tests via computed-style assertions to prevent regressions.
diff --git a/evidence/surface/fetch/bottom-sheets.json b/evidence/surface/fetch/bottom-sheets.json
new file mode 100644
index 0000000..3f8310e
--- /dev/null
+++ b/evidence/surface/fetch/bottom-sheets.json
@@ -0,0 +1 @@
+{"category": "containment", "component_image_1x1": {}, "component_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/341c3402df784b026de4b8b6fe23f3eeddcfc45e753bc872f59861c10edde81bfb0410c8b1838a86255f0f66b47581ff3621044502dee57db2fb365ca71021da", "height": 560, "url": "https://lh3.googleusercontent.com/UIUrt9wQoicIdyQ4IjklJLL6-vIR-GmyYF4ErZOfLrFLREEII6mOVuE15ImA8sq6VvneWK0KQvVOFh8nR_39hUdwN2YJMT7OX9Bivo7aPBBPj0zakQQ", "width": 1120}, "component_image_3x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/c2b9b97f5b43d4d8edfb0efa2cae3a4ede8e4e442ea0c5231f5309e37613166abbfb485a7f96a16f06bdd58a0abeebaa7a24aed225d635193aaa2e07fb0de0f1", "height": 560, "url": "https://lh3.googleusercontent.com/2YiNco_ni2-lSwWlJuYpcp0SKqQQzz1FtoDMeqvd5rjvkTenfvcYm3ccSqJh2oowZ_bXvcUnrMvRzLXrR4HT-3iwlA25df97JZBiIAeOfZEoSHeldA", "width": 1680}, "component_image_3x2": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/14d3cfe953f0b131da2966f35d9437ba2065e16f2d584c8da4192c45a2ba1937052ca085c386576e924d0cdb2c454593054b9742c644f4e57a541c1e5eed5a04", "height": 560, "url": "https://lh3.googleusercontent.com/vs0KnopDGwWY_DYPbdNdmkyYL_N4Cf3eOG7StgUi-LFVsgXqBJ0k4Jlgw25QLpCuxUFPk8Rwt3ijxuBgIohNjVQsEAIqvOB7EzObKxSQzbRyNgGVEYXG", "width": 840}, "component_image_position": "top center", "dark_component_image_1x1": {}, "dark_component_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/6442b37617d160c37751d4301b1265a781d2c786aed628fa890892d2e191038e5b987ff74363ccd261904eb0d686dbabd2e356107badfeaf57b6e13b629e0697", "height": 560, "url": "https://lh3.googleusercontent.com/aOb9-hvKVti3KeEn-NfoD-f1IonRWbrz-A26gJ4SSuakHtKHC2Vw_uh1w3E_1JzMAx4CQ0tjpi2Zvq4yzQ60whrjOAzyIq2CcjAzMgx5VcbGpNb1h4cA", "width": 1120}, "dark_component_image_3x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/2e5a55f30d6a8841040c2e2d947f4039c5713cb5144318a02d5a1ef5a2d9b4fafb5f3f89af22ad89876c5f2fdbc5be393b23f2457e0829d2ed877d61301bec50", "height": 560, "url": "https://lh3.googleusercontent.com/2eharb9N0r0D_CQm81EpHY7FtjRm1hfksH7QVW09EsH2Wx6OwToQyFaWpUmqqGqtk3r30XIf42o1GeDOwjmkZtLPivrY0LyV0q2avatZVgKk8U0Gyw", "width": 1680}, "dark_component_image_3x2": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/31f1e3efd779bd9bd956ab312e0107409d00de80ef55beb7a4fcc5e1588da9b88e3d4a5281002ff43de1ee61169a9538a9544015a620d9a2445c2f072c66e094", "height": 560, "url": "https://lh3.googleusercontent.com/3MqGJTtT0PiPqw6-AG2hxNyEw3EN7kfRuPQBbq4jAd5tU2lDf2eBPFzXyEmoe9iPQH1RfQaoql6df_4tuSKQli87ijf1aWl8JqoyarmV1FM36g33Gg", "width": 840}, "description": "Bottom sheets are surfaces containing supplementary content, anchored to the bottom of the screen.", "hero": {"background_asset": {}, "background_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/c9b614e2c6394e633361aedf8dfabee7018abc81fed5e9c2d2b830cfb91cdde77617375d80608ab19e9deaae6e65763ecf7ee7e7f3be1c5cb05eb08a89e09fe0", "height": 4096, "url": "https://lh3.googleusercontent.com/9gfaUBF_jWA_0uIpfK8gwcKthghUalMNBXP1_ILmkMAF8uf8q8xV3FPeiK2qJYfUyoUHf0zrQooP-olwdgm3nEfSIah6Do202yLoBMuFyl4nao29ao8", "width": 4096}, "foreground_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/533e014f36dc2451d394c03188633b3e805049119e1fdac9903be32e3e3163588ccfb610d365c2e1ada576174426cea0101031cbad8245a346d8fc1a88d4fff6", "height": 1816, "url": "https://lh3.googleusercontent.com/xG7HTNDPwS9Dj37Bw-sK9tqCB7RKZkDDYhw6oYHKxmEio1ZdY7ilg1TGGLteoAI14_qj3ZCMj4gbXa8NebMrGyYRKRcQ-5wqcMCdw6BeuTkI_P2gvOU", "width": 856}, "foreground_image_alignment": "center", "foreground_image_svg": null, "foreground_text_light": false}, "ia_title": "Components / Bottom Sheets", "isDisplayed": true, "jetpack": {}, "mdc_android": {"banners": [], "hide_generic_not_available_message": false, "hide_intro_block": true, "image": [{"a11y_description": "Example bottom sheet: modal bottom sheet", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/14fa0f73686498345e6b3d2c162cc038612f0ce475969b6223543977af215f0dd3a5d4473a594d5c4cb7cfb8f79f7fdb5f97f9159c372a2853111143861d1685", "height": 532, "url": "https://lh3.googleusercontent.com/Um-uopzOwBakYytrS-_tys-4Jhe_bwbY-rVuzx9swVBZMcizNX_w985A8GdRQ-FwbEW45mMfDgkiuMMsqi3JI9fqa6FHb0oSlvM4cwFEog1sSlicsYr-", "width": 1064}}, {"a11y_description": "Standard bottom sheet example. Collapsed on the left and expanded on the\nright.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/b08f62e5cc779362c32c8360c3015ea601124ece798a056ab276b565e117d0deaa774e0c476230b7dd796ce8443cbf34f9265ecc0027944f8cfe742a753139e2", "height": 2160, "url": "https://lh3.googleusercontent.com/B7t0IbDHwFQuL7iRuKhTuTpZo8mblYAHg0q-bCowsAn8fpizoxpWnQfK0E9Zb-g_6DownUBiSOA6NdaeCoVEd_DKdR1P8qbeP9dCQFXy0uz9ngGNeg", "width": 2160}}, {"a11y_description": "Modal bottom sheet example. Collapsed on the left and expanded on the right.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/f810ef2649599d0c1993697c41e609a37fb93773f5c7c09d52bb10e728e087c2f28c36b2cfe17bb8aa8c59725ba172f463b1723247f42cdac16d2379db1d52e4", "height": 2160, "url": "https://lh3.googleusercontent.com/ajuNBwFkxERvKgQ6Pk4e_ZOq8CPWj5f7aA_uYD6Qq5Tv1bYy1-jLKIhd_TK5EerJ550N0JnoyQ_fZ6_dEXC9g-hAWxe2GGamCxGEnsG1n_YxNVaqwQ", "width": 2160}}, {"a11y_description": "Bottom sheet anatomy", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/28bf359e28ada5a30a7b335b4ba3860c8a323c572cf1aa1b9d567e2caf99b6f39df87bbe15aa14ad6a4a8632ed0ec78b5e1c1051f34868f47e7ecddc1f3f550d", "height": 760, "url": "https://lh3.googleusercontent.com/UQIEJJ9UEOR3t5g9wtTOGoWy6s8QXbsxKYOdt7RJ6bSmINikBLnZ3XbSpIb9RBtQwY811TZCw9Zs57eg85AjERuRB4bE7LR08qZZsJLD21O4qRIjJQaX", "width": 1520}}, {"a11y_description": "Bottom sheet with pink background color. Collapsed on the left and expanded on\nthe right.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/8f7f2b0ea75fdb81c0988892e73059dd272b71c910b181096c5f963c88e47c780bc83ce2dbb100b550248173a0b64523d8cb29b3ef19c636d117877d24e1a434", "height": 830, "url": "https://lh3.googleusercontent.com/5KpmGZgdsLzmO52VnZGPD5JqniKVgQ1dynaKlS948Igjo0NmW5h1MVQibtW2ZD6-RoSUraRWU1PRlvgie1J1_1Q2A3opYG9gyGsBOzpzwsR4HKJTLZQE", "width": 800}}], "is_beta": false, "markdown_content": "\n\n# Bottom Sheets\n\n[Bottom sheets](https://material.io/components/sheets-bottom) are surfaces\ncontaining supplementary content that are anchored to the bottom of the screen.\n\n
`getMaxWidth` | `640dp`\n**Max height** | `android:maxHeight` | `setMaxHeight`
`getMaxHeight` | N/A\n\n### Behavior attributes\n\nMore info about these attributes and how to use them in the\n[setting behavior](#setting-behavior) section.\n\nBehavior | Related method(s) | Default value\n------------------------------------------- | ------------------------------------------------------------------------- | -------------\n`app:behavior_peekHeight` | `setPeekHeight`
`getPeekHeight` | `auto`\n`app:behavior_hideable` | `setHideable`
`isHideable` | `false` for standard
`true` for modal\n`app:behavior_skipCollapsed` | `setSkipCollapsed`
`getSkipCollapsed` | `false`\n`app:behavior_fitToContents` | `setFitToContents`
`isFitToContents` | `true`\n`app:behavior_draggable` | `setDraggable`
`isDraggable` | `true`\n`app:behavior_halfExpandedRatio` | `setHalfExpandedRatio`
`getHalfExpandedRatio` | `0.5`\n`app:behavior_expandedOffset` | `setExpandedOffset`
`getExpandedOffset` | `0dp`\n`app:behavior_significantVelocityThreshold` | `setSignificantVelocityThreshold`
`getSignificantVelocityThreshold` | `500 pixels/s`\n\nTo save behavior on configuration change:\n\nAttribute | Related method(s) | Default value\n------------------------ | --------------------------------- | -------------\n`app:behavior_saveFlags` | `setSaveFlags`
`getSaveFlags` | `SAVE_NONE`\n\n### Styles\n\n**Element** | **Default value**\n------------------------- | -------------------------------------------\n**Default style (modal)** | `@style/Widget.Material3.BottomSheet.Modal`\n\nDefault style theme attribute:`?attr/bottomSheetStyle`\n\nNote: The `?attr/bottomSheetStyle` default style theme attribute is for modal\nbottom sheets only. There is no default style theme attribute for standard\nbottom sheets, because `BottomSheetBehavior`s don't have a designated associated\n`View`.\n\n### Theme overlays\n\n**Element** | **Theme overlay**\n------------------------- | ------------------------------------------\n**Default theme overlay** | `ThemeOverlay.Material3.BottomSheetDialog`\n\nDefault theme overlay attribute: `?attr/bottomSheetDialogTheme`\n\nSee the full list of\n[styles](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/bottomsheet/res/values/styles.xml),\n[attrs](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/bottomsheet/res/values/attrs.xml),\nand\n[themes and theme overlays](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/bottomsheet/res/values/themes.xml).\n\n## Theming bottom sheets\n\nBottom sheets support\n[Material Theming](https://material.io/components/sheets-bottom#theming), which\ncan customize color and shape.\n\n### Bottom sheet theming example\n\nAPI and source code:\n\n* `BottomSheetBehavior`\n * [Class definition](https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetBehavior)\n * [Class source](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/bottomsheet/BottomSheetBehavior.java)\n* `BottomSheetDialogFragment`\n * [Class definition](https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetDialogFragment)\n * [Class source](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/bottomsheet/BottomSheetDialogFragment.java)\n\nThe following example shows a bottom sheet with Material Theming, in its\ncollapsed and expanded states.\n\n
Dialogs are available in the Material Design 3 library for Jetpack Compose. For implementation details, visit the androidx.compose.material3 library documentation on Android Developers:
Dialogs are available in the Flutter Material library using the useMaterial3 flag. For details on how to implement, visit the API documentation.
Menus are available in the Material Design 3 library for Jetpack Compose. For implementation details, visit the androidx.compose.material3 library documentation on Android Developers: