diff --git a/skills/commit-workflow/SKILL.md b/.agents/skills/commit-workflow/SKILL.md similarity index 97% rename from skills/commit-workflow/SKILL.md rename to .agents/skills/commit-workflow/SKILL.md index 358dd7b..baae500 100644 --- a/skills/commit-workflow/SKILL.md +++ b/.agents/skills/commit-workflow/SKILL.md @@ -5,6 +5,8 @@ description: Use this skill when creating git commits in this repository. It sta # Commit Workflow +Reread this skill immediately before preparing or creating any commit in this repository. + ## When To Use Use this skill when the user asks to: - create one or more commits; diff --git a/skills/coverage-recovery/SKILL.md b/.agents/skills/coverage-recovery/SKILL.md similarity index 100% rename from skills/coverage-recovery/SKILL.md rename to .agents/skills/coverage-recovery/SKILL.md diff --git a/skills/docs-parity/SKILL.md b/.agents/skills/docs-parity/SKILL.md similarity index 100% rename from skills/docs-parity/SKILL.md rename to .agents/skills/docs-parity/SKILL.md diff --git a/.agents/skills/exploration-workflow/SKILL.md b/.agents/skills/exploration-workflow/SKILL.md new file mode 100644 index 0000000..ce58072 --- /dev/null +++ b/.agents/skills/exploration-workflow/SKILL.md @@ -0,0 +1,282 @@ +--- +name: exploration-workflow +description: Use this skill only when the user explicitly asks for exploration mode: autonomous hypothesis-driven work on uncertain functionality, with timeboxing, milestone logging, and tightly controlled escalation windows. +--- + +# Exploration Workflow + +[reread] Reread this skill immediately before starting any explicit exploration-mode run in this repository. + +## When To Use +Use this skill only when the user explicitly switches the task into exploration mode. + +Typical signals: +- the user wants autonomous long-running work with periodic checkpoints; +- the target is a new or uncertain functional layer; +- the goal is to validate or reject hypotheses, not just implement a known change; +- the user expects structured evidence, logs, and bounded escalation behavior. + +Do not trigger this skill for ordinary implementation, refactoring, bugfixing, or short investigative questions. + +## Entry Handshake +If the user gives a signal that exploration mode may be desired, do not silently start it. +Instead, explicitly ask whether they want to start exploration mode now. + +That confirmation message must: +- mention that this repository has an `exploration-workflow` skill; +- say that exploration starts only after explicit confirmation; +- say that the confirmation reply must include a concrete time budget. + +Accepted confirmation examples: +- `Да, стартуем exploration на 30 минут` +- `Да, запускай, бюджет 60 минут` +- `Start exploration, 120 minutes` + +Once the user gives an affirmative answer with an explicit time budget, take this skill into use immediately and follow it from that point onward. + +## Source Of Truth +- `AGENTS.md` +- `Makefile` +- `recipes/research.mk` +- `recipes/research/fetch.sh` +- `recipes/research/capture.sh` +- `drafts/current.yml` + +## Core Intent +- Reduce uncertainty around a proposed solution or functional layer. +- Let the agent work autonomously for a bounded period without drifting into open-ended activity. +- Keep all risky actions explicit, logged, and reviewable. +- Produce artifacts that make it easy for the user to resume control at any checkpoint. + +## Drafts Layout +- Exploration state in `drafts/` is owned by this skill. +- If `drafts/current.yml` does not exist when exploration starts, create it. +- Every exploration run gets its own dedicated activity directory inside `drafts/`. +- Use a distinct directory name that is easy to differentiate from other runs. +Recommended pattern: + `drafts/explorations/YYYYMMDD-HHMM-/` +- `drafts/current.yml` must point to the current activity directory and contain a short run summary. +- Inside the activity directory, `index.md` is mandatory and acts as the main entry point. +- `index.md` must describe the task in Russian and link to other files relevant to this exploration. +- Other files inside the activity directory are fully at the agent's disposal. +Use them freely for: + notes, plans, milestones, facts, decisions, open questions, artifact inventories, external links, and recovery context. +- Except for `index.md`, there is no fixed file set inside the activity directory. +Choose the structure that best supports efficient autonomous work. +- If screenshots, fetched JSON, or other generated artifacts live outside the activity directory, add links to them from `index.md` or another linked file. + +## `drafts/current.yml` +Maintain `drafts/current.yml` as a short machine-readable pointer to the active exploration. +Minimum fields: +- `mode`: `exploration` +- `slug`: short run identifier +- `path`: relative path to the current activity directory inside `drafts/` +- `goal`: short current goal +- `status`: current state such as `planned`, `running`, `blocked`, `done` +- `started_at`: run start timestamp +- `timeout_minutes`: agreed timeout +- `summary`: short human-readable summary + +Example: +```yaml +mode: exploration +slug: 20260307-2015-surface-layer +path: drafts/explorations/20260307-2015-surface-layer +goal: Проверить новый слой surface/dialog parity и собрать артефакты по расхождениям +status: running +started_at: 2026-03-07T20:15:00+04:00 +timeout_minutes: 60 +summary: Исследуется новый слой surface parity; подготовлен план, открыт escalation budget, собираются локальные факты и артефакты. +``` + +## `index.md` +`index.md` is the human entry point for the current exploration directory. +It must be in Russian and should at least contain: +- short task statement; +- current hypothesis or focus; +- timeout and current status; +- links to notes, logs, plans, and generated artifacts. + +Minimal example: +```md +# Exploration: surface-layer parity + +## Задача +Проверить, можно ли собрать новый слой surface/dialog parity без правок production-компонентов. + +## Статус +- Старт: 2026-03-07 20:15 +04:00 +- Бюджет: 60 минут +- Состояние: running + +## Ссылки +- [План](./plan.md) +- [Лог наблюдений](./notes.md) +- [Решения](./decisions.md) +- [Артефакты](./artifacts.md) +``` + +## Required Rules +- Before execution, prepare a concrete exploration plan with phases, checkpoints, and expected outputs. +- Before execution, estimate which actions require escalation and which can stay local-only. +- Do not begin autonomous exploration until the initial plan and escalation budget are defined. +- Before any non-trivial exploration, ask for a concrete time budget or timeout. +- Record exploration start immediately in the current activity directory and update `drafts/current.yml`. +- Keep a running log of milestones, missing inputs, blocked steps, and unavailable artifacts inside the current activity directory. +- Escalations are forbidden by default. +- Escalations are allowed only after explicit user sanction for the current scope. +- Before any sanctioned escalation phase, define an escalation window: + exact command set, start marker, and freeze condition. +- Log `escalation-needed` before requesting escalation. +- Log `escalation-result` after the sanctioned command set finishes. +- After escalation freeze, do not issue new escalation requests. +- Never request a new escalation in the middle of an autonomous run unless the user explicitly re-opens planning. +- If a needed command was not included in the pre-agreed escalation window, do not request it ad hoc. +- Instead, log `escalation-blocked` and `missing`, continue with local-only work, and report the gap at the next checkpoint or final handoff. +- Periodically check elapsed time against the agreed timeout and record time checks. +- If timeout is reached or likely to be exceeded, stop and ask whether to extend, narrow scope, or finish with current findings. + +## Repository Conventions +- Prefer repository exploration helpers where they exist. + At the moment, this includes `exploration-init`, `exploration-preflight-*`, + `exploration-prepare-batch`, `exploration-bootstrap`, and + `exploration-summarize`. +- Use `make exploration-init slug=` as the quickest way to prepare + the initial activity directory skeleton under `drafts/explorations/`. + This helper only creates directories and draft files; it does not start + exploration mode and must not replace the explicit start sequence from this skill. +- Direct `research-*` commands are allowed only when they are listed in the current escalation window. +- Prefer one-shot privileged bootstrap: + `make exploration-bootstrap activity_dir=drafts/explorations/` +- After the escalation window is closed, prefer local synthesis via: + `make exploration-summarize activity_dir=drafts/explorations/` +- Treat `exploration-*` target names as the current repository command interface for exploration support flows. +- During exploration-driven component development, do not modify current production + components in `src/components` unless explicitly requested. +- For agent-driven exploratory components, create and use `src/experimental` + directories at the same level as `src/components` inside each workspace. +- Exploratory implementation order is mandatory: implement in + `m3-vue/src/experimental` first; port to `m3-react/src/experimental` only after + the exploration outcome is explicitly confirmed as successful. + +## Workflow +1. Confirm exploration mode was explicitly requested. +If it was not, do not use this skill. + +2. If the user only hinted at exploration mode, run the entry handshake first. +Do not start exploration until the user explicitly confirms it and gives a time budget. + +3. Build the initial exploration plan before starting execution. +The plan must include: +- goal and current uncertainty; +- ordered phases; +- checkpoints between phases; +- expected artifacts; +- safe local steps; +- steps that probably require escalation. + +4. Build an escalation budget. +List every command or command family that may need escalation during this run. +For each item, record: +- exact command or bounded command set; +- reason; +- expected output or artifact; +- freeze condition after which no more escalation is allowed. + +5. Ask for a timeout. +Examples: +- `30 minutes` +- `60 minutes` +- `120 minutes` + +6. Create or select the activity directory under `drafts/`. +Use a clear distinct name, for example: +- `drafts/explorations/20260307-2015-surface-layer/` +- `drafts/explorations/20260307-2015-dialog-parity/` +To scaffold this quickly before filling in the run state, you may use: +- `make exploration-init slug=surface-layer` + +7. Initialize `drafts/current.yml`. +Write at least: +- active path; +- short goal; +- current status; +- start timestamp; +- timeout; +- short summary. + +8. Create `index.md` inside the activity directory. +`index.md` must be in Russian and should link to the other files used during the run. + +9. If escalation is likely, ask for sanction on the whole planned escalation window before the run starts. +Prefer one bounded approval phase over incremental requests. +Do not defer obvious escalation planning to later checkpoints. + +10. Create the first run notes inside the activity directory. +At minimum record: +- start timestamp; +- agreed timeout; +- current goal; +- planned checkpoints; +- escalation budget summary; +- the main hypothesis; +- sub-hypotheses if already known; +- known missing inputs. + +11. Choose the lowest-risk path first. +Order of preference: +- local inspection; +- safe local recipes; +- analysis of existing artifacts; +- sanctioned escalation window only if needed. + +12. When escalation is needed, define the window before requesting it. +The window must include: +- exact commands; +- why they are needed; +- what condition ends the window. + +13. During execution, keep the activity directory current. +Log: +- hypothesis start; +- hypothesis completion; +- missing data; +- escalation-needed; +- escalation-result; +- escalation-blocked; +- timeout checks. +Update `drafts/current.yml` whenever run status changes materially. + +14. If unplanned escalation becomes necessary during the run, do not request it immediately. +Instead: +- log the block; +- continue with safe/local analysis; +- collect all remaining non-privileged findings; +- stop at the next checkpoint or end-of-run synthesis. + +15. When sufficient evidence exists, stop exploring and synthesize. +Summarize: +- what was validated; +- what was disproved; +- what remains unknown; +- which next move is safest. + +## Expected Outputs +- Updated `drafts/current.yml` +- A dedicated activity directory under `drafts/` +- `index.md` in Russian inside that activity directory +- Any notes, facts, plans, logs, or inventories that help resume context later +- Links to generated local artifacts such as screenshots, fetched JSON, and derived analysis +- A concise user-facing summary with findings, blockers, and recommended next step + +## Stop Conditions +Stop exploration and hand back control when: +- the agreed timeout is reached; +- the next required action is outside the current escalation window; +- the remaining uncertainty is mostly product or design uncertainty rather than technical uncertainty; +- enough evidence exists to recommend a clear next step. + +Before handoff: +- update `drafts/current.yml` to reflect the latest status; +- ensure `index.md` links to the most important notes and artifacts; +- leave enough written context for the next session to resume without reconstructing the whole run from memory. diff --git a/skills/yarn-lock-conflict-resolution/SKILL.md b/.agents/skills/yarn-lock-conflict-resolution/SKILL.md similarity index 100% rename from skills/yarn-lock-conflict-resolution/SKILL.md rename to .agents/skills/yarn-lock-conflict-resolution/SKILL.md diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index b20f5c9..58c4e75 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -10,7 +10,7 @@ runs: using: composite steps: - name: Use Node.js ${{ inputs.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} @@ -19,7 +19,7 @@ runs: run: make .yarnrc.yml - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .yarn key: ${{ runner.OS }}-node-${{ inputs.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} @@ -29,3 +29,7 @@ runs: - name: Install dependencies shell: bash run: yarn install + + - name: Fix PATH for workspace bins + shell: bash + run: echo "${{ github.workspace }}/node_modules/.bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..acd7979 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,98 @@ +name: Release + +on: + workflow_dispatch: + inputs: + prerelease: + type: choice + options: + - 'alpha' + - 'beta' + - 'rc' + - 'none' + description: Prerelease type + default: 'none' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 90 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node and dependencies + uses: ./.github/actions/setup-env + with: + node-version: '24.x' + + - name: Configure Git + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Run package type checks + run: yarn tsc + + - name: Run test type checks + run: yarn tsc:tests + + - name: Run e2e config type checks + run: yarn tsc:e2e + + - name: Run eslint + run: yarn eslint + + - name: Run tests + run: yarn test + + - name: Install Playwright Chromium + run: yarn playwright install --with-deps chromium + + - name: Run e2e tests + run: yarn test:e2e + + - name: Build packages + run: yarn build + + - name: Build React Storybook + run: yarn workspace @modulify/m3-react storybook:build --test --quiet + + - name: Build Vue Storybook + run: yarn workspace @modulify/m3-vue storybook:build --test --quiet + + - name: Run release [${{ inputs.prerelease }}] + if: ${{ inputs.prerelease != 'none' }} + env: + YARN_ENABLE_IMMUTABLE_INSTALLS: false + run: yarn release:${{ inputs.prerelease }} + + - name: Ensure stable release is running from main + if: ${{ inputs.prerelease == 'none' }} + run: | + if [ "${{ github.ref_name }}" != "main" ]; then + echo "Stable release is allowed only from main. Current ref: ${{ github.ref_name }}" + exit 1 + fi + + - name: Run release + if: ${{ inputs.prerelease == 'none' }} + env: + YARN_ENABLE_IMMUTABLE_INSTALLS: false + run: yarn release + + - name: Push current branch with tags + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: git push --follow-tags origin HEAD:${{ github.ref_name }} + + - name: Publish packages to npm + env: + YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: yarn publish:packages diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 066403a..18fc754 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,18 +51,29 @@ jobs: echo "has_open_pr=false" >> "$GITHUB_OUTPUT" fi - eslint: + workflow-lint: needs: pr-check if: github.event_name != 'push' || github.ref == 'refs/heads/main' || needs.pr-check.outputs.has_open_pr != 'true' runs-on: ubuntu-latest + steps: + - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. + uses: actions/checkout@v6 + + - name: Run actionlint + uses: devops-actions/actionlint@v0.1.10 + + eslint: + needs: workflow-lint + runs-on: ubuntu-latest + strategy: matrix: node-version: [ 22.x, 24.x ] steps: - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node and dependencies uses: ./.github/actions/setup-env @@ -72,11 +83,36 @@ jobs: - name: Run eslint run: yarn eslint - tests: + build: needs: eslint + runs-on: ubuntu-latest - timeout-minutes: 90 + strategy: + matrix: + node-version: [ 22.x, 24.x ] + + steps: + - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. + uses: actions/checkout@v6 + - name: Setup Node and dependencies + uses: ./.github/actions/setup-env + with: + node-version: ${{ matrix.node-version }} + + - name: Run package type checks + run: yarn tsc + + - name: Run e2e config type checks + if: matrix.node-version == '24.x' + run: yarn tsc:e2e + + - name: Build packages + run: yarn build + + tests: + needs: build + timeout-minutes: 90 runs-on: ubuntu-latest strategy: @@ -85,7 +121,7 @@ jobs: steps: - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node and dependencies uses: ./.github/actions/setup-env @@ -104,6 +140,10 @@ jobs: if: matrix.node-version == '24.x' run: yarn playwright install --with-deps chromium + - name: Run e2e tests + if: matrix.node-version == '24.x' + run: yarn test:e2e + - name: Run merged coverage if: matrix.node-version == '24.x' run: | @@ -121,7 +161,7 @@ jobs: - name: Upload Playwright/Vitest artifacts on failure if: failure() && matrix.node-version == '24.x' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: playwright-vitest-artifacts-node-24 path: artifacts @@ -129,7 +169,7 @@ jobs: - name: Upload merged coverage artifacts if: matrix.node-version == '24.x' && github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: coverage-node-24 path: | @@ -140,15 +180,14 @@ jobs: codecov: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: tests - runs-on: ubuntu-latest steps: - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download merged coverage artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: coverage-node-24 path: coverage @@ -164,10 +203,8 @@ jobs: verbose: true storybook-tests: - needs: eslint - + needs: build timeout-minutes: 60 - runs-on: ubuntu-latest strategy: @@ -176,7 +213,7 @@ jobs: steps: - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node and dependencies uses: ./.github/actions/setup-env diff --git a/.gitignore b/.gitignore index 7e65074..18e828f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ node_modules/ .yarnrc.yml *storybook.log .env +drafts/ diff --git a/.yarnrc.yml.dist b/.yarnrc.yml.dist index b66fc11..2ecc46b 100644 --- a/.yarnrc.yml.dist +++ b/.yarnrc.yml.dist @@ -13,19 +13,19 @@ packageExtensions: "@commitlint/load@*": dependencies: "@types/node": "^25.3.0" - typescript: "~5.9.3" + typescript: "~6.0.3" "@storybook/addon-docs@*": dependencies: "@types/react": "^18.3.28" "@storybook/react-vite@*": dependencies: - typescript: "~5.9.3" + typescript: "~6.0.3" "@storybook/vue3-vite@*": dependencies: vue: "^3.5.28" "eslint-plugin-storybook@*": dependencies: - typescript: "~5.9.3" + typescript: "~6.0.3" "storybook@*": dependencies: "@testing-library/dom": "^10.4.1" diff --git a/AGENTS.md b/AGENTS.md index 33ec614..5074796 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 `.agents/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..50547a5 100644 --- a/Makefile +++ b/Makefile @@ -1,236 +1,50 @@ +.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 +DOCKER_COMPOSE ?= docker compose +YARN=@$(DOCKER_COMPOSE) run --rm node yarn +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 + $(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 - yes | docker-compose rm -s -v $(service) && docker-compose up -d $(service) + yes | $(DOCKER_COMPOSE) rm -s -v $(service) && $(DOCKER_COMPOSE) up -d $(service) else - docker-compose stop && docker-compose up -d + $(DOCKER_COMPOSE) stop && $(DOCKER_COMPOSE) up -d 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 + $(DOCKER_COMPOSE) stop -.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/release.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/docker/playwright/Dockerfile b/docker/playwright/Dockerfile index 70bc873..76c02f2 100644 --- a/docker/playwright/Dockerfile +++ b/docker/playwright/Dockerfile @@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN corepack enable \ - && npx -y playwright@1.58.2 install --with-deps chromium \ + && npx -y playwright@1.61.1 install --with-deps chromium \ && chown -R node:node /ms-playwright USER node 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/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\n\n**Contents**\n\n* [Using bottom sheets](#using-bottom-sheets)\n* [Standard bottom sheet](#standard-bottom-sheet)\n* [Modal bottom sheet](#modal-bottom-sheet)\n* [Anatomy and key properties](#anatomy-and-key-properties)\n* [Theming](#theming-bottom-sheets)\n\n## Using bottom sheets\n\nBefore you can use Material bottom sheets, you need to add a dependency to the\nMaterial Components for Android library. For more information, go to the\n[Getting started](/libraries/mdc-android/getting-started)\npage.\n\nStandard bottom sheet basic usage:\n\n```xml\n\n\n \n\n \n\n \n\n\n```\n\nModal bottom sheet basic usage:\n\n```kt\nclass ModalBottomSheet : BottomSheetDialogFragment() {\n\n override fun onCreateView(\n inflater: LayoutInflater,\n container: ViewGroup?,\n savedInstanceState: Bundle?\n ): View? = inflater.inflate(R.layout.modal_bottom_sheet_content, container, false)\n\n companion object {\n const val TAG = \"ModalBottomSheet\"\n }\n}\n\nclass MainActivity : AppCompatActivity() {\n ...\n val modalBottomSheet = ModalBottomSheet()\n modalBottomSheet.show(supportFragmentManager, ModalBottomSheet.TAG)\n ...\n}\n```\n\nMore information on each individual section, below.\n\n### Setting behavior\n\nThere are several attributes that can be used to adjust the behavior of both\nstandard and modal bottom sheets.\n\nBehavior attributes can be applied to standard bottom sheets in xml by setting\nthem on a child `View` set to `app:layout_behavior`, or programmatically:\n\n```kt\nval standardBottomSheetBehavior = BottomSheetBehavior.from(standardBottomSheet)\n// Use this to programmatically apply behavior attributes\n```\n\nBehavior attributes can be applied to modal bottom sheets using app-level theme\nattributes and styles:\n\n```xml\n\n\n\n\n\n```\n\nOr programmatically:\n\n```kt\nval modalBottomSheetBehavior = (modalBottomSheet.dialog as BottomSheetDialog).behavior\n// Use this to programmatically apply behavior attributes\n```\n\nMore information about these attributes and their default values is available in\nthe [behavior attributes](#behavior-attributes) section.\n\n### Retaining behavior on configuration change\n\nIn order to save and restore specific behaviors of the bottom sheet on\nconfiguration change, the following flags can be set (or combined with bitwise\nOR operations):\n\n* `SAVE_PEEK_HEIGHT`: `app:behavior_peekHeight` is preserved.\n* `SAVE_HIDEABLE`: `app:behavior_hideable` is preserved.\n* `SAVE_SKIP_COLLAPSED`: `app:behavior_skipCollapsed` is preserved.\n* `SAVE_FIT_TO_CONTENTS`: `app:behavior_fitToContents` is preserved.\n* `SAVE_ALL`: All aforementioned attributes are preserved.\n* `SAVE_NONE`: No attribute is preserved. This is the default value.\n\nBehaviors can also be set in code:\n\n```kt\nbottomSheetBehavior.saveFlags = BottomSheetBehavior.SAVE_ALL\n```\n\nOr in xml using the `app:behavior_saveFlags` attribute.\n\n### Setting state\n\nStandard and modal bottom sheets have the following states:\n\n* `STATE_COLLAPSED`: The bottom sheet is visible but only showing its peek\n height. This state is usually the 'resting position' of a bottom sheet, and\n should have enough height to indicate there is extra content for the user to\n interact with.\n* `STATE_EXPANDED`: The bottom sheet is visible at its maximum height and it\n is neither dragging nor settling (see below).\n* `STATE_HALF_EXPANDED`: The bottom sheet is half-expanded (only applicable if\n `behavior_fitToContents` has been set to false), and is neither dragging nor\n settling (see below).\n* `STATE_HIDDEN`: The bottom sheet is no longer visible and can only be\n re-shown programmatically.\n* `STATE_DRAGGING`: The user is actively dragging the bottom sheet up or down.\n* `STATE_SETTLING`: The bottom sheet is settling to a specific height after a\n drag/swipe gesture. This will be the peek height, expanded height, or 0, in\n case the user action caused the bottom sheet to hide.\n\nYou can set a state on the bottom sheet:\n\n```kt\nbottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED\n```\n\n**Note:** `STATE_SETTLING` and `STATE_DRAGGING` should not be set programmatically.\n\n### Listening to state and slide changes\n\nA `BottomSheetCallback` can be added to a `BottomSheetBehavior`:\n\n```kt\nval bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {\n\n override fun onStateChanged(bottomSheet: View, newState: Int) {\n // Do something for new state.\n }\n\n override fun onSlide(bottomSheet: View, slideOffset: Float) {\n // Do something for slide offset.\n }\n}\n\n// To add the callback:\nbottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)\n\n// To remove the callback:\nbottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)\n```\n\n### Handling insets and fullscreen\n\n`BottomSheetBehavior` can automatically handle insets (such as for\n[edge to edge](https://developer.android.com/training/gestures/edge-to-edge)) by\nspecifying any of these to true on the view:\n\n* `app:paddingBottomSystemWindowInsets`\n* `app:paddingLeftSystemWindowInsets`\n* `app:paddingRightSystemWindowInsets`\n* `app:paddingTopSystemWindowInsets`\n\nOn API 21 and above the modal bottom sheet will be rendered fullscreen (edge to\nedge) if the navigation bar is transparent and `app:enableEdgeToEdge` is true.\nTo enable edge-to-edge by default for modal bottom sheets, you can override\n`?attr/bottomSheetDialogTheme` like the below example:\n\n```\n\n\n\n```\n\nInsets can be added automatically if any of the padding attributes above are set\nto true in the style, either by updating the style passed to the constructor, or\nby updating the default style specified by the `?attr/bottomSheetDialogTheme`\nattribute in your theme.\n\n`BottomSheetDialog` will also add padding to the top when the bottom sheet\nslides under the status bar, to prevent content from being drawn underneath it.\n\n### Making bottom sheets accessible\n\nThe contents within a bottom sheet should follow their own accessibility\nguidelines, such as setting content descriptions for images.\n\nTo support dragging bottom sheets with accessibility services such as TalkBack,\nVoice Access, Switch Access, etc., we provide a convenient widget\n`BottomSheetDragHandleView` which will automatically receive and handle\naccessibility commands to expand and collapse the attached bottom sheet when\nthe accessibility mode is enabled. To use `BottomSheetDragHandleView`, you can\nadd it to the top of your bottom sheet content. It will show a customizable\nvisual indicator for all users. See the example in the below section for how to\nadd a drag handle to your bottom sheet.\n\n**Note:** `BottomSheetDragHandleView` has a default min width and height of 48dp\nto conform to the minimum touch target requirement. So you will need to preserve\nat least 48dp at the top to place a drag handle.\n\n## Standard bottom sheet\n\nStandard bottom sheets co-exist with the screen\u2019s main UI region and allow for\nsimultaneously viewing and interacting with both regions. They are commonly used\nto keep a feature or secondary content visible on screen when content in the\nmain UI region is frequently scrolled or panned.\n\n`BottomSheetBehavior` is applied to a child of\n[CoordinatorLayout](https://developer.android.com/reference/androidx/coordinatorlayout/widget/CoordinatorLayout)\nto make that child a **persistent bottom sheet**, which is a view that comes up\nfrom the bottom of the screen, elevated over the main content. It can be dragged\nvertically to expose more or less content.\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\n### Standard bottom sheet example\n\nThe following example shows a standard bottom sheet in its collapsed and\nexpanded states:\n\n\n\n`BottomSheetBehavior` works in tandem with `CoordinatorLayout` to let you\ndisplay content on a bottom sheet, perform enter/exit animations, respond to\ndragging/swiping gestures, etc.\n\nApply the `BottomSheetBehavior` to a direct child `View` of `CoordinatorLayout`:\n\n```xml\n\n\n \n\n \n \n\n \n \n\n \n\n \n\n \n\n \n\n\n```\n\nIn this example, the bottom sheet is the `FrameLayout`.\n\n## Modal bottom sheet\n\nModal bottom sheets present a set of choices while blocking interaction with the\nrest of the screen. They are an alternative to inline menus and simple dialogs\non mobile devices, providing additional room for content, iconography, and\nactions.\n\n`BottomSheetDialogFragment` is a thin layer on top of the regular support\nlibrary Fragment that renders your fragment as a **modal bottom sheet**,\nfundamentally acting as a dialog.\n\nModal bottom sheets render a shadow on the content below them, to indicate that\nthey are modal. If the content outside of the dialog is tapped, the bottom sheet\nis dismissed. Modal bottom sheets can be dragged vertically and dismissed by\nsliding them down completely.\n\nAPI and source code:\n\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\n### Modal bottom sheet example\n\nThe following example shows a modal bottom sheet in its collapsed and expanded\nstates:\n\n\n\nFirst, subclass `BottomSheetDialogFragment` and overwrite `onCreateView` to\nprovide a layout for the contents of the sheet (in this example, it's\n`modal_bottom_sheet_content.xml`):\n\n```kt\nclass ModalBottomSheet : BottomSheetDialogFragment() {\n\n override fun onCreateView(\n inflater: LayoutInflater,\n container: ViewGroup?,\n savedInstanceState: Bundle?\n ): View? = inflater.inflate(R.layout.modal_bottom_sheet_content, container, false)\n\n companion object {\n const val TAG = \"ModalBottomSheet\"\n }\n}\n```\n\nThen, inside an `AppCompatActivity`, to show the bottom sheet:\n\n```kt\nval modalBottomSheet = ModalBottomSheet()\nmodalBottomSheet.show(supportFragmentManager, ModalBottomSheet.TAG)\n```\n\n`BottomSheetDialogFragment` is a subclass of `AppCompatFragment`, which means\nyou need to use `Activity.getSupportFragmentManager()`.\n\n**Note:** Don't call `setOnCancelListener` or `setOnDismissListener` on a\n`BottomSheetDialogFragment`. You can override\n`onCancel(DialogInterface)` or `onDismiss(DialogInterface)` if necessary.\n\n## Anatomy and key properties\n\nBottom sheets have a sheet, content, and, if modal, a scrim.\n\n\n\n1. Sheet\n2. Content\n3. Scrim (in modal bottom sheets)\n\n### Sheet attributes\n\nElement | Attribute | Related method(s) | Default value\n-------------- | --------------------- | --------------------------------- | -------------\n**Color** | `app:backgroundTint` | N/A | `?attr/colorSurface`\n**Shape** | `app:shapeAppearance` | N/A | `?attr/shapeAppearanceLargeComponent`\n**Elevation** | `android:elevation` | N/A | `3dp`\n**Max width** | `android:maxWidth` | `setMaxWidth`
`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\n\n#### Implementing bottom sheet theming\n\nSetting the theme attribute `bottomSheetDialogTheme` to your custom\n`ThemeOverlay` will affect all bottom sheets.\n\nIn `res/values/themes.xml`:\n\n```xml\n\n\n\n```\n\nIn `res/values/styles.xml`:\n\n```xml\n\n\n\n```\n\n**Note:** The benefit of using a custom `ThemeOverlay` is that any changes to\nyour main theme, such as updated colors, will be reflected in the bottom sheet,\nas long as they're not overridden in your custom theme overlay. If you use a\ncustom `Theme` instead, by extending from one of the\n`Theme.Material3.*.BottomSheetDialog` variants, you will have more control over\nexactly what attributes are included in each, but it also means you'll have to\nduplicate any changes that you've made in your main theme into your custom\ntheme.\n", "markdown_path": "docs/components/BottomSheet.md", "meta": {"keywords": null, "meta_robots": " ", "schema_structure": "{}", "share_description": null, "share_image": null, "share_title": null}, "title": "Bottom sheets", "up_next_section": {"items": []}, "document_id": "4640185669255168"}, "mdc_flutter": {"banners": [], "hide_generic_not_available_message": false, "hide_intro_block": true, "image": [], "is_beta": false, "markdown_content": null, "markdown_path": null, "meta": {"keywords": null, "meta_robots": " ", "schema_structure": "{}", "share_description": null, "share_image": {}, "share_title": null}, "title": "Bottom Sheets", "up_next_section": {"items": []}, "document_id": "5472253978476544"}, "mdc_web": {}, "metadata": {"keywords": "Material, Material Design, Google, Material Design 3, Material 3, Material You, Material Design Components, Material Components, components, cards", "meta_robots": null, "schema_structure": "{}", "share_description": "Bottom sheets show secondary content anchored to the bottom of the screen. There are two types of bottom sheets: standard and modal.", "share_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/c953bf9b2efdd14a850224135254c8b148a26adbce86d8a93dcf42439deb393f1ebbb519dc761df6c1569e1c08f757a88e4d533cc82dc8c7f4d25b16b781fb79", "height": 560, "url": "https://lh3.googleusercontent.com/anRizsTTKb3ZFDxvaOQnGhnaepLiJtk__kY2eOG1-X9tBEOZ_Efjd8vHU14mujMBUuRZXdKZjW6kZp32adjusmMOSOrgzXKXqvJHUUkEDF9C-tteNDyQ", "width": 1120}, "share_title": "Bottom sheets \u2013 Material Design 3"}, "page_hero": {"foreground_image_1x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/423127aef1c64cb6a4d543d785647510ee752773c1e3730cd2aedeea5ce998522efed9ab837c46053e0b59e94b383b0626daf465340c81b7c98b05e944f9318d", "height": 1120, "url": "https://lh3.googleusercontent.com/LYDT9qYA1q7NLsDc9uG-q6ADc8znPvqUZ78u1oAg28gAsH6KDuUou2jk_OBbds4h3MDfZ0HwStPi5LPeXDGvpm7CkL1me_RlYTWXsHBmtW-FAYY-nXE", "width": 1120}, "foreground_image_1x1_dark_mode": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/2b1aa6610cb93219f521691df9d488f81e51d42cdf8515a454ad22f7f2820ed3c35ec81f42f80b2715a47e0d9cc6cff730ff516220572fa989ad9ccb7f1573e7", "height": 1120, "url": "https://lh3.googleusercontent.com/G9867ICt9b-lncJHHeVCxGxXlAKtIHq2Mzk4exKf7IkmAbj663wW-c4gIUsyeT0JfuNm8rrf1Hmo1wdCDS5AH0zScZNH8SLxn0iIi_58kS--vmVqPg", "width": 1120}, "foreground_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/f27e561a36c757333bc0e1671f30bf024bf5fc42d120495a9f443ce9cd207fa573c01d10b4ea977586470eeca1a134949841971504e59d1d12d34543974f2d72", "height": 1000, "url": "https://lh3.googleusercontent.com/6Xkb4jV_z0luNQskg19qC2PaETvm-JM49LlNOeqzfFiSXSmwae6J93dXBo7Jc442xJdZSENioqa28IsM406PXN_bPxo53-4kw5ZpXyj1lGKzOC2ORfSS", "width": 2000}, "foreground_image_2x1_dark_mode": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/22fc7f4780841efbb4c9acc5c9134eb76d84aec484c21dcc003c134fa90b77495aa6ed31efaaeec851096e2746ca4660aa396a49c45ed1726c6b1552b883d75c", "height": 1000, "url": "https://lh3.googleusercontent.com/XCWmA6moCnvUU5E2-0TNFM3MY2l4DlNkwRzwrjbD_ZHXcP-h2H02Y6UZ9juXVm4O0Eliok1wjM7kc4mZGRfRKEW_lS0DAk_qJdpPkG6gWtKozPpGOnfY", "width": 2000}, "full_width": false, "full_width_content_centered": false, "horizontal_foreground_alignment_override": null, "main_background_image": {}, "main_background_image_dark_mode": {}, "split_background_asset": {}, "split_background_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/6ab37909dc8478dd5e56137ee989c11507af974de96d6b52d7b04c096eaf500eb6cb297ecbc6d68d676634ef0c77bc196b4b50daf9101baf0a393f0d4e4d70c5", "height": 4096, "url": "https://lh3.googleusercontent.com/Zepnyf8JSnn6jKK6Zq6Ppm1oWSVdUMujaWc3pp-TMsk0U5THxsPYusqEIccnUary6eohY1UoVBgB7bQQ9FRgawuvXKDAPy2EPZwj6D9zgkRPV-breddj", "width": 4096}, "split_background_image_dark_mode": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/11266e9deb9db2deaab0114997d42033baa68f7cdc695f7f62dae5722a76b2e4787c5b96f0193b88c26968894dfbc41cc8fb0d07608fb463b21f5afd20ff238b", "height": 4096, "url": "https://lh3.googleusercontent.com/ewqelGlD5dUrp4LyDl4uyaS1aEAT0LqmTGwUblaNx6UlioWljMsus4141fu-waKli5dgu71O2g42Kl8owxFv6ijaFo_HTSnexyGaVHwF1DUqPEfW0Q", "width": 4096}, "vertical_foreground_alignment_override": null}, "title": "Bottom sheets", "document_id": "5317529291587584", "modified_date": "2025-04-25"} \ No newline at end of file diff --git a/evidence/surface/fetch/dialogs.json b/evidence/surface/fetch/dialogs.json new file mode 100644 index 0000000..6696384 --- /dev/null +++ b/evidence/surface/fetch/dialogs.json @@ -0,0 +1 @@ +{"category": "containment", "component_image_1x1": {}, "component_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/1201b10f2aa29cc2b012aedf3d6767fa7b34b9a502457c68e1a6ef87c1b3d8c35e2f631bec3c785a03d0ee6e560e2004e6cf4527dc1faf03206476fde49d502f", "height": 560, "url": "https://lh3.googleusercontent.com/CFJfp5JSKy1CCK9X0jVO7pRomxEXLpvUPlj6biCd8V1zkvQAdNnnhWqE-pMeX8nol5yx9YwMikRapiuiahQLUEufqpxDJxs4w00HrUNUMcxLftTcddA", "width": 1120}, "component_image_3x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/a8ee8e7fdedecf347ab7be12bdbe17ae43a145b06f8b6849806808fe705a329dae84930131c39debda19c5ebc03d60d93b102e96323f1a342e65c42354cb52e8", "height": 560, "url": "https://lh3.googleusercontent.com/vA2EVTxGzbpxA8uKcqA_kUbswA5Ip9NL9iuDq6XXU4DlNyAFNQaXu5H0JslNpWvfo0wqTABMQchMT2toI5YQFByNKhkty8pwj3cZXIXBsw0Hd5PwdBlx", "width": 1680}, "component_image_3x2": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/667710b7bb2a7ea03afea363090125566b582bf944873a5fc55e158fc76d05a18f9784140bef9f4d631fe0392e2386d81aa854e59279c438d36a7e2cda9049c3", "height": 560, "url": "https://lh3.googleusercontent.com/asmiS26l6fx3OYk-LUie65WSh26b_c1zjpLUODiitaz_ICoFGQz-hyrnid0pw4GiCWNGy4incRNik8xuTNWjaMyPjKUQo5njyXuwdlm7A8yAFlCZIg", "width": 840}, "component_image_position": "center", "dark_component_image_1x1": {}, "dark_component_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/994d5fde6374e3edf539c37a962337f22e7ebcecb254b5b8f0b624245e2a34148282576570acac992f31fd10ad56d328f63e06536d38a5dfb0e0112711bcbe73", "height": 560, "url": "https://lh3.googleusercontent.com/8r_6V4X-Rw2Iywz74S0SHRsbe1Dghi2GmPHKHTqQ_AkOKhjWXsiJ9RUGx4l8n5WT17AHdj9E2ycJlIzqqPWJJ0Lii3C-WgXV1em_EEEtl4t0H1UGULg", "width": 1120}, "dark_component_image_3x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/cf8967735ecfa675a864c392e728b94723b3d365ac8e0334c776fa4d2a3ed869e6a7514cd0f0671c753f33d891fd49a4d84450b8c05482c95e261603268075ba", "height": 560, "url": "https://lh3.googleusercontent.com/kVRpL11HJpgVz1WwKXoePGQs50bzRfd_vrk1gntt3VMrQDniwtkEinIYOj7CDeKh4j38EaxuR4OIXHao0ahKelQyT2fvNvB6-qMw7pplTaARrXLP9r-F", "width": 1680}, "dark_component_image_3x2": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/720bedc77eae4ce24f71b551978a503ba999d9518f626b3478efe9166a76765ce767f84b59d5952c9bcca3cdc69fcac897ca20503ed5b0418fd57dee4cdca900", "height": 560, "url": "https://lh3.googleusercontent.com/UxQqwGi4wvijSnl77_1OLF9jpxNnS6zoadsG94JlTwHVusL-jOc5M2EMECCgSpuhp7euZlXdo2dj4s5o23F3puPqaLS6mS91DVS8ANeQGbNsGIrYBw", "width": 840}, "description": null, "hero": {"background_asset": {}, "background_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/374704d69859062037341f8f902b1efd04a623e626051d8249b8cd4ffe237f131bb0259de564e978f96a5ad0e3386721618b3cbb1f01890c63d94109c0f84fcc", "height": 4096, "url": "https://lh3.googleusercontent.com/p5D8lSFRzHHtJeJh9glEIalFFsCBHp0PBPImeTzekk3IE8mtEYxCMNXaj0TffIlcPkdFOjvjLf6Zp7SZDpNOA2e6M6jJNHoA5jRV5V0iSSE8AAljlZ4", "width": 4096}, "foreground_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/96b77eadf1dc3f9f97ed94edfdbe69a7716114344cb29f3a4ab0e9265945e617679c07754f984ed5d53fca0741176f1973d95bd76928ec9dd845487c81db3c1b", "height": 1816, "url": "https://lh3.googleusercontent.com/cD2FTVbrqtc_pZ7IwitArkWuRFGvQ_CHj-cuD76UiDUZZpjY2F0EmeUPmdLdf29NQKITPu540wiWTGIz4CbCZFzE_REolC9FEWXM6_pShckgW2Wg9Q", "width": 856}, "foreground_image_alignment": "center", "foreground_image_svg": null, "foreground_text_light": false}, "ia_title": "Components / Dialogs", "isDisplayed": true, "jetpack": {}, "mdc_android": {"banners": [{"description": "

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:

", "links": [{"aria_label": "Read about implementing Material Design 3 AlertDialogs using Jetpack Compose", "href": "https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#alertdialog", "label": "AlertDialog API reference", "open_new_window": true}], "title": "Jetpack Compose support available"}], "hide_generic_not_available_message": true, "hide_intro_block": true, "image": [{"a11y_description": "Examples of the two types of dialogs.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/4702378141911baecc1e6721afe2722cd953dbcece8c607f27ad431a1d2f3c59255d1b291781cfc8ea2251a4a42395ec674cec71ee85f8eb9712bffc963792d1", "height": 1036, "url": "https://lh3.googleusercontent.com/U3KG3u3LdCQsgAwfJMpOsk_RAZF1qYzBg-IVOQIqZEMmicfZu-1hHHHtfPTORjuMlP6zl5-gBV7ZMGoCxvQpGhL9_605HTMGUmVBBlSc8auT_9lV", "width": 1999}}, {"a11y_description": "\"Dialog with title, and text and purple buttons to cancel, decline or accept\"", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/641d369239c3c8122c7504ec20bb4734d7fa5e82211b760e7a2b353cafe5120bba9117af0a26e7d3726d8c9384f52e5c083ab8cd6df4b7eb8cea49f90703c8ed", "height": 687, "url": "https://lh3.googleusercontent.com/kEgwg9iNd9jboCKEfFrhvyJYod5D4ZALPrNd5Ru63Cqy4OG_FCX7zs8RU5AKBp1KrJxZzhGvu1QWG8dv1Z4Ngc0BYp8CChsqKcV8YFhnddJqRJU50ZQ", "width": 1080}}, {"a11y_description": "anatomy", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/34848ab749d6ff65f57f82b2c5ff5a54fe75ee3e7edafbe09cd860b75f16216d8409c6d0a3e044f13e4de1dd4c0db6b0f2c692c8265af4cc422f2939523b3b2e", "height": 970, "url": "https://lh3.googleusercontent.com/7GNdJqfCwAzo8wn9yzGsmGPeAFU3R3s6_A3VCdKX9u9cpGmQeCgnYni2t6CleU4bMEq01sBxsgQsNRJ2agc6lEdo5-j2angIeg87yqOTOTaaQS8Awbo", "width": 1410}}, {"a11y_description": "Dialog with title and text buttons in brown and selected radio button in pink", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/beb9fd6d806c96a1fc008977850761f1bc11c0e49fac272a021e11785fba46813bd8f6c66e31d1a6df3853f7cab78b7a242871798b0b94a20a09901925a8e0d5", "height": 737, "url": "https://lh3.googleusercontent.com/Ts8rbXjz2Oemc7LixUJrHcsXICUeM2tuHs89gPRfDL9pIfbLsHdu_5UUBCpx6cM8jkiFn3qAtNj3Ue0cpMTTzTprdhwjw-e45G4JrpaX51p4aHU9iOo", "width": 1080}}], "is_beta": false, "markdown_content": "\n\n# Dialogs\n\n[Dialogs](https://material.io/components/dialogs/) inform users about a task and\ncan contain critical information, require decisions, or involve multiple tasks.\n\n![\"Dialog to confirm settings centered in a screen\"](assets/dialogs/dialogs_hero.png)\n\n**Contents**\n\n* [Using dialogs](#using-dialogs)\n* [Basic dialog](#basic-dialog)\n* [Full-screen dialog](#full-screen-dialog)\n* [Theming](#theming-dialogs)\n\n## Using dialogs\n\nBefore you can use Material dialogs, you need to add a dependency to the\nMaterial Components for Android library. For more information, go to the\n[Getting started](/libraries/mdc-android/getting-started)\npage.\n\n```kt\nMaterialAlertDialogBuilder(context)\n // Add customization options here\n .show()\n```\n\n### Making dialogs accessible\n\nThe contents within a dialog should follow their own accessibility guidelines,\nsuch as an icon on a title having a content description via the\n`android:contentDescription` attribute set in the\n`MaterialAlertDialog.Material3.Title.Icon` style or descendant.\n\n### Types\n\nThere are two types of dialogs: 1\\. [Basic dialog](#basic-dialog), 2\\.\n[Full-screen dialog](#full-screen-dialog)\n\n\n\n## Dialogs\n\nA dialog is a type of modal window that appears in front of app content to\nprovide critical information or ask for a decision. Dialogs disable all app\nfunctionality when they appear, and remain on screen until confirmed, dismissed,\nor a required action has been taken.\n\nDialogs are purposefully interruptive, so they should be used sparingly.\n\n### Dialog examples\n\nAPI and source code:\n\n* `MaterialAlertDialogBuilder`\n * [Class description](https://developer.android.com/reference/com/google/android/material/dialog/MaterialAlertDialogBuilder)\n * [Class source](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/MaterialAlertDialogBuilder.java)\n\n## Basic dialog\n\nThe following example shows a basic dialog.\n\n\n\nIn code:\n\n```kt\nMaterialAlertDialogBuilder(context)\n .setTitle(resources.getString(R.string.title))\n .setMessage(resources.getString(R.string.supporting_text))\n .setNeutralButton(resources.getString(R.string.cancel)) { dialog, which ->\n // Respond to neutral button press\n }\n .setNegativeButton(resources.getString(R.string.decline)) { dialog, which ->\n // Respond to negative button press\n }\n .setPositiveButton(resources.getString(R.string.accept)) { dialog, which ->\n // Respond to positive button press\n }\n .show()\n```\n\n## Full-screen dialog\n\nFull-screen dialogs group a series of tasks, such as creating a calendar entry\nwith the event title, date, location, and time. Because they take up the entire\nscreen, full-screen dialogs are the only dialogs over which other dialogs can\nappear.\n\nThere is no specific Material implementation of a full-screen dialog. You can\nimplement it by using a\n[`DialogFragment`](https://developer.android.com/reference/androidx/fragment/app/DialogFragment)\nas explained in the\n[Android Developer guides](https://developer.android.com/guide/topics/ui/dialogs#FullscreenDialog).\n\n### Anatomy and key properties\n\nA dialog has a container, content (either supporting text or a set of items of a\nparticular type), a background scrim, and, optionally, title and buttons.\n\n\n\n1. Container\n2. Icon (optional)\n3. Title (optional)\n4. Content\n5. Buttons (optional)\n6. Scrim\n\n#### Container attributes\n\nElement | **Attribute** | **Related methods** | **Default value**\n----------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ | -----------------\n**Color** | N/A | N/A | `?attr/colorSurface`\n**Shape** | `app:shapeAppearance`
`app:shapeAppearanceOverlay` | N/A | `?attr/shapeAppearanceMediumComponent` with a corner size of `28dp`\n**Background inset start and end** | `app:backgroundInsetStart`
`app:backgroundInsetEnd` | `setBackgroundInsetStart`
`setBackgroundInsetEnd` | `24dp`\n**Background inset top and bottom** | `app:backgroundInsetTop`
`app:backgroundInsetBottom` | `setBackgroundInsetTop`
`setBackgroundInsetBottom` | `80dp`\n\n#### Title attributes\n\nElement | **Attribute** | **Related methods** | **Default value**\n-------------- | ------------------------ | -------------------------------- | -----------------\n**Text label** | N/A | `setTitle`
`setCustomTitle` | `null`\n**Text color** | `android:textColor` | N/A | `?attr/colorOnSurface`\n**Typography** | `android:textAppearance` | N/A | `?attr/textAppearanceHeadlineSmall`\n**Icon** | N/A | `setIcon`
`setIconAttribute` | `null`\n**Icon tint** | `app:tint` | N/A | `?attr/colorSecondary`\n\n#### Content attributes\n\n**Supporting text**\n\nElement | **Attribute** | **Related methods** | **Default value**\n-------------- | ------------------------ | ------------------- | -----------------\n**Text** | N/A | `setMessage` | `null`\n**Color** | `android:textColor` | N/A | `?attr/colorOnSurfaceVariant`\n**Typography** | `android:textAppearance` | N/A | `?attr/textAppearanceBodyMedium`\n\n**List item**\n\nElement | **Attribute** | **Related methods** | **Default value**\n--------------------------------------- | ------------------------------ | ---------------------- | -----------------\n**List item layout** | `app:listItemLayout` | `setItems` | [`@layout/mtrl_alert_select_dialog_item`](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/res/layout/mtrl_alert_select_dialog_item.xml)\n**List item layout style** | N/A | N/A | `?attr/materialAlertDialogBodyTextStyle`\n**List item text color** | `android:textColor` | N/A | `?attr/colorOnSurfaceVariant`\n**List item typography** | `android:textAppearance` | N/A | `?attr/textAppearanceBodyMedium`\n**Multi choice item layout** | `app:multiChoiceItemLayout` | `setMultiChoiceItems` | [`@layout/mtrl_alert_select_dialog_multichoice`](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/res/layout/mtrl_alert_select_dialog_multichoice.xml)\n**Single choice item layout** | `app:singleChoiceItemLayout` | `setSingleChoiceItems` | [`@layout/mtrl_alert_select_dialog_singlechoice`](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/res/layout/mtrl_alert_select_dialog_singlechoice.xml)\n**Multi/single choice item style** | `android:checkedTextViewStyle` | N/A | `@style/Widget.Material3.CheckedTextView`\n**Multi/single choice item text color** | `android:textColor` | N/A | `?attr/colorOnSurfaceVariant`\n**Multi/single choice item typography** | `android:textAppearance` | N/A | `?attr/textAppearanceBodyLarge`\n\n**Note:** You can set any custom view to be the content of your dialog via the\n`setView` method.\n\n#### Buttons attributes\n\nElement | **Attribute** | **Related methods** | **Default value**\n------------------------------------------------ | --------------------------------- | ------------------- | -----------------\n**Buttons theme attributes (negative/positive)** | `app:buttonBar*ButtonStyle` | N/A | `@style/Widget.Material3.Button.TextButton.Dialog`\n**Buttons theme attributes (neutral)** | `app:buttonBarNeutralButtonStyle` | N/A | `@style/Widget.Material3.Button.TextButton.Dialog.Flush`\n**Buttons (neutral/negative/positive)** | N/A | `set*Button` | `null`\n**Icons** | N/A | `set*ButtonIcon` | `null`\n\nFor specific button attributes, see the\n[Buttons documentation](https://github.com/material-components/material-components-android/tree/master/docs/components/Button.md).\n\n#### Scrim attributes\n\nElement | **Attribute** | **Related methods** | **Default value**\n-------------- | ----------------------------- | ------------------- | -----------------\n**Dim amount** | `android:backgroundDimAmount` | N/A | 32%\n\n#### Theme overlays\n\nElement | **Theme overlay**\n-------------------------- | -----------------\n**Default theme overlay** | `ThemeOverlay.Material3.MaterialAlertDialog`\n**Centered theme overlay** | `ThemeOverlay.Material3.MaterialAlertDialog.Centered`\n\nDefault theme overlay attribute: `?attr/materialAlertDialogTheme`\n\n#### Theme attributes\n\nElement | **Theme attribute** | **Default value**\n------------------------- | ----------------------------------------- | -----------------\n**Default style** | `?attr/alertDialogStyle` | `@style/MaterialAlertDialog.Material3`\n**Title text style** | `?attr/materialAlertDialogTitleTextStyle` | `@style/MaterialAlertDialog.Material3.Title.Text`\n**Supporting text style** | `?attr/materialAlertDialogBodyTextStyle` | `@style/MaterialAlertDialog.Material3.Body.Text`\n\nSee full list of\n[styles](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/res/values/styles.xml),\n[attributes](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/res/values/attrs.xml),\nand\n[theme overlays](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/res/values/themes.xml).\n\n## Theming dialogs\n\nA dialog supports\n[Material Theming](https://material.io/components/dialogs/#theming) which can\ncustomize color, typography and shape.\n\n### Dialog theming example\n\nAPI and source code:\n\n* `MaterialAlertDialogBuilder`\n * [Class description](https://developer.android.com/reference/com/google/android/material/dialog/MaterialAlertDialogBuilder)\n * [Class source](https://github.com/material-components/material-components-android/tree/master/lib/java/com/google/android/material/dialog/MaterialAlertDialogBuilder.java)\n\nThe following example shows a dialog with Material Theming.\n\n\n\n#### Implementing dialog theming\n\nSetting the theme attribute `materialAlertDialogTheme` to your custom\n`ThemeOverlay` will affect all dialogs.\n\nIn `res/values/themes.xml`:\n\n```xml\n\n\n\n```\n\nIn `res/values/styles.xml`:\n\n```xml\n\n\n\n\n \n\n\n```\n\nOr if you want to change only one specific dialog, pass the `themeResId` to the\nconstructor:\n\n```kt\nMaterialAlertDialogBuilder(context, R.style.ThemeOverlay_App_MaterialAlertDialog)\n ...\n .show()\n```\n", "markdown_path": "docs/components/Dialog.md", "meta": {"keywords": null, "meta_robots": " ", "schema_structure": "{}", "share_description": null, "share_image": null, "share_title": null}, "title": "Dialogs", "up_next_section": {"items": []}, "document_id": "4969122496249856"}, "mdc_flutter": {"banners": [{"description": "

Dialogs are available in the Flutter Material library using the useMaterial3 flag. For details on how to implement, visit the API documentation.

", "links": [{"aria_label": "Read about implementing Material Design 3 Dialog using Flutter", "href": "https://api.flutter.dev/flutter/material/Dialog-class.html", "label": "Dialog API reference", "open_new_window": true}, {"aria_label": "Read about implementing Material Design 3 AlertDialog using Flutter", "href": "https://api.flutter.dev/flutter/material/AlertDialog-class.html", "label": "AlertDialog API reference", "open_new_window": true}], "title": "Flutter support available"}], "hide_generic_not_available_message": true, "hide_intro_block": true, "image": [], "is_beta": false, "markdown_content": null, "markdown_path": null, "meta": {"keywords": null, "meta_robots": " ", "schema_structure": "{}", "share_description": null, "share_image": {}, "share_title": null}, "title": "Dialogs", "up_next_section": {"items": []}, "document_id": "5240559261974528"}, "mdc_web": {}, "metadata": {"keywords": "Material, Material Design, Google Material, Google, Material Design 3, Material 3, Material You, Material Design Components, Material Components, components, dialogs", "meta_robots": null, "schema_structure": "{}", "share_description": "Dialogs provide important prompts in a user flow. Use dialogs to make sure users act on information", "share_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/645047f1a7e12769b092d3f1746d0a86fc87b52d90a065f64ae8582a5547fa67706ae0c2b4ca6193b7df3172692ae586d3368247bcdb349749ac862e2e7c6bfb", "height": 560, "url": "https://lh3.googleusercontent.com/mfskU6xDOqSsTCcgnGcsTfdCcT35ftTpCJmb4kuchYadG_41OF410XdGQP08L3iqFsq_pwIJ7_Zyf-8e-8J7nEwdNWMB-PqZGdICdnLsmHYfXy08Wrw", "width": 1120}, "share_title": "Dialogs \u2013 Material Design 3"}, "page_hero": {"foreground_image_1x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/c26a0789742bad25a27d711dea8e655b1048515c311e47467b70ce19aada489175aeebb7b779c1dadfe85d429aec3c13715cea872735e5b59951114d5a593bc2", "height": 1120, "url": "https://lh3.googleusercontent.com/q-dfTEhPWF_-_eM4VpJrZA694bAqAO8BJMQMvVrDsqZ1kHyCOOhhnaXWYJw-wP3bGv6CXAKaEOrAxk2OEY47frCMhjGgp4ZZUtFsUCeMn0dthK0T8w", "width": 1120}, "foreground_image_1x1_dark_mode": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/9ed9fa99e47af01d81ae06cb058b307f64fe5ad80f518c46655fab1b66a8eb019aca19b27aaf200320caaaced0560bc0e68888d728194894bdd9710eb1080594", "height": 1120, "url": "https://lh3.googleusercontent.com/NFxYirrIZx8x5PbwPfor383cpP0mjHhJtIeXy-fAbo878fU2IJ5NEiRubwH3xHFVuN8J12QtUD6erRN6YCyx7e1OluYRzs8hYtRFer3K5oNn562hrOo", "width": 1120}, "foreground_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/16ec9ceebf3552311c38177ae3e547df730ad4398714a450983194b622471a59168f0d13e76f271096b27d3e5cf3d309a08de27408b08466dfad4e022ecd995c", "height": 1000, "url": "https://lh3.googleusercontent.com/euzNp7-lR2RlNLgwaPFT0mjcpki6eFepE5yrYjxsGmVn3Mi-3e7nmGiVWBdPKUYHKn9cWZncX_09QVYT_0QWdl8vbo88ajt3Usbeqv-_ypmB0pu43-9x", "width": 2000}, "foreground_image_2x1_dark_mode": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/22d90bf7a02bea312349422e7bc3a1a2b915f0a07b4d25e51fc5bef75cd7a3d2cbfe49fe6851f048ad712861e2ac09844daab502007b649de1103fde29ed466c", "height": 1000, "url": "https://lh3.googleusercontent.com/QfWucnJId1YNG8tBqQdCxL5aDcK5kcDxCo57DWf_e9_Q8y-W1Om1ulxFtF0WZI6V4W-bBxj7spcHt2vJx1LfnWFE_PMmQ-aRYS-kH2COxgwyAgmWJg8", "width": 2000}, "full_width": false, "full_width_content_centered": false, "horizontal_foreground_alignment_override": null, "main_background_image": {}, "main_background_image_dark_mode": {}, "split_background_asset": {}, "split_background_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/06a3ad2112a7a75e2a38793327f4e8e486d6c9419a6767c779d54d7dbbacfbc5490ff86f55d08ab821aa6e89881362465f8332e9bf5eb67b9c7c9bba8b59c6d6", "height": 4096, "url": "https://lh3.googleusercontent.com/ClaZSxMwSB7FD1hlooJt1hkkA2NiWXWIVrCHb1qizShsE2nNht2euUQm6BmS-C6B6nwzB8DUuuUjYy25E9weS5oMxQFkrWgcucbDtXtBobfs_riZootR", "width": 4096}, "split_background_image_dark_mode": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/04794fbf602312f722329bc76020840bdd22adb6c751aba94802088e571ebaff854cfb859c1cf2efc541f99ed8d274386b67b60c8ad4586e3d9a6079a1af0404", "height": 4096, "url": "https://lh3.googleusercontent.com/hORpCLy8XRY7280TZFng7WkrjURav7C3AApKqntCFUZnFZ4_NybzinpEA-HSGL46pPn_m96V4GL9t08BxTkkKmT3FX3SrRsUi2yWdQyzwQmUqzNSSMj7", "width": 4096}, "vertical_foreground_alignment_override": null}, "title": "Dialogs", "document_id": "5732087063379968", "modified_date": "2025-05-01"} \ No newline at end of file diff --git a/evidence/surface/fetch/layout-applying.json b/evidence/surface/fetch/layout-applying.json new file mode 100644 index 0000000..61b87c6 --- /dev/null +++ b/evidence/surface/fetch/layout-applying.json @@ -0,0 +1 @@ +{"category": null, "component_image_1x1": {}, "component_image_2x1": {}, "component_image_3x1": {}, "component_image_3x2": {}, "component_image_position": "center", "dark_component_image_1x1": {}, "dark_component_image_2x1": {}, "dark_component_image_3x1": {}, "dark_component_image_3x2": {}, "description": "Use window size classes to create layouts that scale across most devices and form factors.", "hero": {"background_asset": {}, "background_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_be877ab23edb4a39be610a57a771c69d_/2a62e31378857d6697036e05f7be96efbf388211a4d46eeefaecbe993b06d74be5826e0e9dcc08fc8b450c5b0faef9f061aebc8cce918d965b2aa7338b8833a6", "height": 1200, "url": "https://lh3.googleusercontent.com/CIyB5lCif4nv2PAOf5rXTP3F5QR1FrtGwOfF0nTi1YUu38kd9Q-16uQ4huiiaPFftqAgZqNM8JaymDXcxUb_P_4vASsP8hlP6jMDXXfezufgTRkMC9s", "width": 2400}, "foreground_image": {}, "foreground_image_alignment": "top", "foreground_image_svg": null, "foreground_text_light": false}, "ia_title": "Foundations / Layout / Applying layout", "isDisplayed": true, "jetpack": {}, "mdc_android": {}, "mdc_flutter": {}, "mdc_web": {}, "metadata": {"keywords": "Material, Material Design, Google Material, Google, Material Design 3, Material 3, Material You, adaptive, responsive, layouts", "meta_robots": " ", "schema_structure": "{}", "share_description": "Use window size classes to create layouts that scale across most devices and form factors.", "share_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_be877ab23edb4a39be610a57a771c69d_/a5205fc1d66462945233e8abb82dd1a98633141a775cf9809bdfc8a8dbcd6bc2215cfc3309a640dceb773eca70977b133e878cd2e6b98ade4a377686ef149260", "height": 540, "url": "https://lh3.googleusercontent.com/9IC8KyAT2SkaRgd0oRs6Q9_EThFKVJQomkWlNMfvupqnnafcaoMNDpju-OSuXaKR5pvAwLF1NGQibVeKPdmwuisCTerH_NdiJdpxYi2AmD9QT_Nleg", "width": 960}, "share_title": "Applying layout \u2013 Material Design 3"}, "page_hero": {"foreground_image_1x1": {}, "foreground_image_1x1_dark_mode": {}, "foreground_image_2x1": {}, "foreground_image_2x1_dark_mode": {}, "full_width": false, "full_width_content_centered": false, "horizontal_foreground_alignment_override": null, "main_background_image": {}, "main_background_image_dark_mode": {}, "split_background_asset": {"file_name": "Google_Mio_UnderstandingLayout_1080x1080.mp4", "file_path": "f3eb2abc4e890d68862f4d078fb6c1b39232bc11ccf12df23c1b7dd35eb6b7554fd0c937fd186d6340d5147c096764df5a2075bff03d3b64333cd60d0efd067a", "url": "https://kstatic.googleusercontent.com/files/f3eb2abc4e890d68862f4d078fb6c1b39232bc11ccf12df23c1b7dd35eb6b7554fd0c937fd186d6340d5147c096764df5a2075bff03d3b64333cd60d0efd067a"}, "split_background_image": {}, "split_background_image_dark_mode": {}, "vertical_foreground_alignment_override": null}, "title": "Applying layout", "document_id": "6396979944620032", "modified_date": "2024-05-10"} \ No newline at end of file diff --git a/evidence/surface/fetch/manifest.yaml b/evidence/surface/fetch/manifest.yaml new file mode 100644 index 0000000..a44b7e4 --- /dev/null +++ b/evidence/surface/fetch/manifest.yaml @@ -0,0 +1,17 @@ +schema_version: 1 +package: "surface_fetch_baseline" +updated_at: "2026-02-23" +source_run: "EXP-2026-02-22-longrun-001" +source_path: "drafts/experiment/runs/EXP-2026-02-22-longrun-001/fetch" +notes: + - "This fetch set was selected as the canonical preserved baseline." + - "Equivalent fetch sets existed in other drafts runs and were byte-identical." +files: + - "bottom-sheets.json" + - "dialogs.json" + - "layout-applying.json" + - "menus.json" + - "motion-easing-duration.json" + - "motion-transitions.json" + - "navigation-drawer.json" + - "side-sheets.json" diff --git a/evidence/surface/fetch/menus.json b/evidence/surface/fetch/menus.json new file mode 100644 index 0000000..ad78789 --- /dev/null +++ b/evidence/surface/fetch/menus.json @@ -0,0 +1 @@ +{"category": "selection", "component_image_1x1": {}, "component_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/f3d1d05fa3f98b7ed34577f6bf887203bd162e7b9345e649ff4cbf5fed67d6f6ab856ea9ccd72f468d1470880f1d9dc00bbeb72b9846dbb421bcda0fbc41139c", "height": 560, "url": "https://lh3.googleusercontent.com/k9FOfw5GEcyNWd-Iqc8zhd2qfWULlhXjaEnV_N_twzxIYtR77YiLgLEwGgcK-xdWA4uumfdX-aYKo_V8-Ufqarw-n2YmJqeNiyDcNEqza48A7eipbOg", "width": 1120}, "component_image_3x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/205babd7b20c05185e7b46ac4fae5fb4d132f7ff66544af9160f3f36e424b640f0b0791be257bd8b2e8554e4af9cfc2312ca19b10da7f238ba6c331e917af67a", "height": 560, "url": "https://lh3.googleusercontent.com/xtpBXljEGhJN5JEnvNw2UVR_l_yng6IfOoCQGC6Ls4WxVvoROmN5O8qyvdCoIelP90wzZR3_QN41CDXlHgqZnw5_WjWIs9uPP8iI0K90lFQ_HsCLyh0", "width": 1680}, "component_image_3x2": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/3e17b5ed53619638fe6ece233a0a31848381cf699b3685def701b7d49f8f9e5eb0561a077a3ca478c95d76ef770e8c5f80478a0a49b94522078eb5cf3c481fa5", "height": 560, "url": "https://lh3.googleusercontent.com/VxcZwSoYqM_KdhpHLPeAaM-KScIOFBt4uzxbifNaVrQwQYEw6ZbDh-ehhaBrd_lxWn5mTxUXUKb_foMmV-zYN-rd4vJOEl6cxWdZToarE8edli0mhp4", "width": 840}, "component_image_position": "center", "dark_component_image_1x1": {}, "dark_component_image_2x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/f46cfd9b67bc7fcda8898b95c5f15a6642f8d0227aceb890cb46d730bd4b57f3549972d614c76bfdc1a7ab9b5af623eb8ba675c431f01f1a6c28851962f30c48", "height": 560, "url": "https://lh3.googleusercontent.com/g-nOrn0GXf-nZJ22TwAM8ChICae6oDtnAVPPWnoe0JQ9Mh2OpWkl_qfQ45Q59YUZDv2ZCLnpierG13fE8UinLDi1xFNv1XyyYmiw0vf0wfqEjvadTJI", "width": 1120}, "dark_component_image_3x1": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/89bb3e1f2cd4b1ca5e62e21f9042322a871a424e12dcb6c5b46d1fe57606f5283f9f5b73b9ecd354963a903739e7212af3fb8b950d30c5fbec2975419f26085e", "height": 560, "url": "https://lh3.googleusercontent.com/8qsTRu46sT7aR_EHcDRrbgTHAp_emItoya46tNkjc8u0i5UHbeo3u5kViYcYSXq8v0UeGyFXJeCDyJ8Nm4dqxpMc-gkASRqZDIa94rTnpXe5lCFBFNzQ", "width": 1680}, "dark_component_image_3x2": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/aa0f1e028214700f1e8d3cccf8824090f78360e951eabac3341cd71dbb64778c761d6a2b42f075a87360a70de9a2aaa95682a11bdac4c679a0d2004cd2547daf", "height": 560, "url": "https://lh3.googleusercontent.com/4un3gOBZ2kAujd4tLWLaKBpPgfMyz0TEQzAcdQqRTj0KFe-snSi9L7Sqpe0rdFwruDkRvn4wlQmtqJPNY54F1fQeKkXuGlFHyKjLWB2JsdpAuPIyNN4", "width": 840}, "description": null, "hero": {"background_asset": {}, "background_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/7a8fa848b6b77f42495f75eafac551a09a53d560ca7a9430c7a5056f89ba232f0bb1fd6ef8b4888e8f33871e050a153c2d8eb677e4a8c45a68775a42c305784d", "height": 4096, "url": "https://lh3.googleusercontent.com/2fY0UpxDH_6NCU8n8L7xSnxSokFQqdA8qOWo_NNCU8ikQW5BhiP4Ha5aP_L0oYnFC8wtHdkEtxxC8CcGue6Tj_wrY7-QtEVsYjZziIasgck97JD4pIo", "width": 4096}, "foreground_image": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_95f9980dd12d4f8886c881994eaa6a79_/ec9075bd851c86a3ae9930b62d7fefbc2cc4300973d11813ae6c8f7cbc2da2b00341136ab501bac4e3a8e8e85c144ced8f574fc89a1f0c25afa66c66b9f54cb1", "height": 1120, "url": "https://lh3.googleusercontent.com/1nhfDYUuWn2Y837J6niWOD9td-3BcYN1e83LtvwoWVyMrs16SQcxxWnr1dlxrQPyyTPe08aKzSlxetsVLWX1uBTyvdYRthlhgNrv5Q8UnxJGGbj1Ow", "width": 1120}, "foreground_image_alignment": "top", "foreground_image_svg": null, "foreground_text_light": true}, "ia_title": "Components / Menus", "isDisplayed": true, "jetpack": {}, "mdc_android": {"banners": [{"description": "

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:

", "links": [{"aria_label": "Read about implementing Material Design 3 AlertDialogs using Jetpack Compose", "href": "https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#DropdownMenu(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.ui.unit.DpOffset,androidx.compose.ui.window.PopupProperties,kotlin.Function1)", "label": "DropdownMenu API Reference", "open_new_window": true}, {"aria_label": "Read about implementing Material Design 3 ExposedDropdownMenu using Jetpack Compose", "href": "https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)", "label": "ExposedDropdownMenu API Reference", "open_new_window": true}], "title": "Jetpack Compose support available"}], "hide_generic_not_available_message": true, "hide_intro_block": true, "image": [{"a11y_description": "Menu hero example", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/da9bc72e7ce22d5466d4932275b9af1a2c798a87b47943fc3c8be1e70e12255833c07378041e1cd278e80620c1076ce4929d72b2b37ccb282d449b6ab54f7020", "height": 754, "url": "https://lh3.googleusercontent.com/Kb7vuTWlHrnwuv-8taEKtQZuTzKta54HxDNsUzN864yfInGGIx333w1VErFA8zlmFEW8hRa9JDXi-K6wUy-ReZQHERNZH4CA273evW3urLrys-KsYQ", "width": 1999}}, {"a11y_description": "Composite image of example dropdown and exposed dropdown menu types", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/13e3ee536262916e9edd4ea67762d836c54d571e515ce1234308e96abcede4082375a2aec11cc9c6b82050663ed69a456182b3ff18cc83ad7da8eb17fd944ab5", "height": 940, "url": "https://lh3.googleusercontent.com/X_D_If_smcA-GPD4A-USkWJgj0UgT9YSx2JmTcxI7dfvJiKU7M08lpFsc8-MNb-ZHuh5jVwpx5b6eeMuzlpNVmt0F6MbSVjvL63oGlzeJLFEjBarQeQ", "width": 2048}}, {"a11y_description": "Composite image: purple \"Menus\" bar, and purple \"Menus\" bar with white menu\nand 3 options", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/d5dadc3e2f0d1b341fb784613fcb87fde6231cc9803588b5b56f06415e5106f66cae8dba24edb84ee928dd94aea9f6f1a78e67b9fefa89bf09a46e1fa969c64c", "height": 650, "url": "https://lh3.googleusercontent.com/gh99kXdWMairaR0nsNpVkgE90_dMb74HMzoPfxFIZwIJo2vkJ3wKL8Nd87QdKXqaA1zFImD3ACa0sUbuqpQ1WYU5QapTEB8JOuO18JZLERWbE_d_n9c", "width": 1080}}, {"a11y_description": "White menu window with two options floating over a white background with grey\ntext", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/c308438bedef64d236cf034176a68e6bd5c4758eeb0a6e9e91c4762dfd5da830ac7f340ada2280d0210d57412bf6b0b1886a4e2f05b65e05598079b083683520", "height": 463, "url": "https://lh3.googleusercontent.com/j4Crs8v0RxV8F4W40NQ78y8IANvZfG4ra-hAXxP3qe5WVYycY91eVpyfEQbNZ9vX11qryv-lEK0GO1DoI6laBsoOkxjBHpDGH_Gy0lWx2gzZBVfrjA", "width": 1080}}, {"a11y_description": "White menu container with 3 options open below a purple \"Show menu\" button", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/3a9ece693f83eb50c0810d14bc629f88293aa830fe94487f17bbe440a9a7a3b59cd558f6819e2fb6e38842aca960d9eb7e53b0da8698c6166cee415efb52b071", "height": 655, "url": "https://lh3.googleusercontent.com/n--GNHXqdi6IAWHN4AqGWHN8hWZ1D2Zp2Vzz4H5Hyn13DCUjQtJYxTg8QEA3KVNSJ_32X4SPuwRJrMHveZwvqwxBaKXptGRO3OwUamedoPtLMiVW3nw", "width": 1080}}, {"a11y_description": "White menu window with 3 options open below a purple \"Show menu\" button. Each\noption has an icon.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/243744613f0f55c96c0f809d11beb9598662c3525f4942dfe6c5bf5981be692b55bc48d382d1f6814dff115788a6e93392e5870eb6f6df474faa23f49126c677", "height": 746, "url": "https://lh3.googleusercontent.com/DQC8wOdRISEh_rVPd8uCJryHd1SvjzYkKBSiSkCd6-9Zm1Cpwafo5cRWWqh6xfhby-6xgKNazfOnjDRy0zNkBNYtUkUJ8xMNs_05Cr-bV8QsJ96Zhrkb", "width": 1080}}, {"a11y_description": "White menu container with 3 options below a purple \"Show menu\" button", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/7f7449899e6653b6854d950c1ec540034cb2dbfdccbd5f150950d2cd1bfc2d6065595a02bd2d1a855dd2b24f3c12bcfbddcff952dd54e98cc9419e59b4d9ef69", "height": 851, "url": "https://lh3.googleusercontent.com/dUS2p3rCcRYLLAqbINsDJNG6BS35MCimP6RCHjoEPybt7rCmylcw6umvXSxSbZLPM1_Nw-baGHJxQTRGYoHlevsSK2jvvI2Qmf4BMHyRjyiEcfA2krE", "width": 1080}}, {"a11y_description": "Menus anatomy diagrams", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/3fdf3f958e1e19bdd6e730ab63e3bdea088c222eb1f7ccb8eb5dfc50e30aa17285220a071ded0ee7a7b3cc1404cce78a3ce71194f354b02952bac496eecea132", "height": 1045, "url": "https://lh3.googleusercontent.com/laHuXPSZTM4O_vqnZXda7hziGjy9ASSB-iISC1EyQ3d_7SXZNLp0JXMi795PoYeXC0YO1dfEtUcPh2uNAmXFvLUkW9hwxQqERmpKwN7OqF8GM8OfIx3O", "width": 1999}}, {"a11y_description": "2 menu states with text field element: 1) has \"item 1\", 2) has \"item 1\" and a\n4-item menu container.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/fc19c6475eac357ad6f5095c449c90472faceca82417de3dc23a06ace9264596e4ccacff8d9f6b004cba367cdc43daedf1b1cf6b367962433cb9a2ddd49a96d9", "height": 1088, "url": "https://lh3.googleusercontent.com/wAY6SANH7FSIDjD3unZacLO13lhDGveMKaa0SPRYxXSb2DYIRMlGsZ5_439T_YMs9oCv47QCPxqlSz5avTVR9XfI9Ln_o1OcHxWMuYwyVMl_CgKmiZQ", "width": 1080}}, {"a11y_description": "Menu with purple outlined text field element and 4 items. Item 1 selected.", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/24ebf0ae74d894bdbd3a3866451ee6b4ca89aae5d40a6dff81bc98ca61036daffd5ccc7ae5927fe5170b1bf157955bada93c8a723da4f4c4f1c070f3ae2ed8b5", "height": 806, "url": "https://lh3.googleusercontent.com/50YgYjtYrYt-pt2z0cbeScd46k87_auB_rG8WzXaI-XH6dsdrn1CrI2C6uy9Peff1MxlyO5EEpOU3x2Jvjzb0DJq844Tg34zCimJruqLXIe-jsCQww", "width": 1080}}, {"a11y_description": "Exposed dropdown menu anatomy diagram", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/899b0462ee201be46c303101b746518dcabc60bd3bcf192fc713d74b7f9ac6c47612039c93bbfce86c3c3891ec71d26316d1428db98df0998f3ebc1c1c6442d9", "height": 1070, "url": "https://lh3.googleusercontent.com/-UN5DIcokiLQt7ga1W60Cgn7jOvXipS0dEOoHTIyj_AAs9PvkqbXZZpeHaXHib0jbBoK3nlFNVBRBjQsRCYnqWxkmICedLpAA3EbjvXBYc-34INabOc", "width": 2048}}, {"a11y_description": "White menu container with brown text showing 3 options", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/1764ef3c118e224429b07bf02f93685a5cc7519afda3744d917a3133226e0aeeadef6ca81491b417075e05c2dd39fb499b322f52b6c2edeaa92d1d97e711f32e", "height": 294, "url": "https://lh3.googleusercontent.com/D12ZcYdcTC8C6Opc_SwZbJ5hA5rCeVZo8hkTEtuawp0WZZ8P3bSSpbGWuar3_ehXTGwLI5AE2GVbixeehq3g2CanaIy7SWB5ZWx7awEkmSaD9c38XK4", "width": 400}}, {"a11y_description": "Composite image of \"Show menu\" button and options: gray button with pink\nunderline, and white button with pink\noutline", "bordered": true, "caption": null, "case": null, "file": {"file_path": "_f68a99ffe9144467a917320028f1ce3b_/_293ae6e29e4b420a9648cf846508437a_/2f6fe3ba4e94014212938f293059220eb01ad09b5e8d0493f5f62f19eee576ad0f4b91f1418c3e25ec9e8eae775a23fc9b3ed75bb96463bd1099b05602e46156", "height": 363, "url": "https://lh3.googleusercontent.com/MRNcysCTiR5Z9bsP_I3LkcLnvFTCl5ZIWdQrkjfeMUe4-YuhoI8duuHM7syR5q3m3pWUCi-6HI4Zm2ZSBbznP4A3_9Ylvbx8i4BK4uafeet1ClZat29o", "width": 1000}}], "is_beta": false, "markdown_content": "\n\n# Menus\n\n[Menus](https://material.io/components/menus) display a list of choices on\ntemporary surfaces.\n\n\n\n**Contents**\n\n* [Using menus](#using-menus)\n* [Dropdown menus](#dropdown-menus)\n* [Exposed dropdown menus](#exposed-dropdown-menus)\n* [Theming](#theming-menus)\n\n## Using menus\n\nA menu displays a list of choices on a temporary surface. They appear when users\ninteract with a button, action, or other control.\n\nBefore you can use Material menus, you need to add a dependency to the Material\nComponents for Android library. For more information, go to the\n[Getting started](/libraries/mdc-android/getting-started)\npage.\n\nA typical menu resource looks like this:\n\n```xml\n\n\n \n \n \n\n```\n\nA typical exposed dropdown menu looks like this:\n\n```xml\n\n\n \n\n\n```\n\nSee the [dropdown menus](#dropdown-menus) and\n[exposed dropdown menus](#exposed-dropdown-menus) sections for detailed usage\ninformation.\n\n### Making menus accessible\n\nMenus are readable by most screen readers, such as TalkBack. Text rendered in\nmenus is automatically provided to accessibility services. Additional content\nlabels are usually unnecessary.\n\nAndroid's exposed dropdown menu component APIs support both label text and\nhelper text, which tell the user what information is requested for a menu. While\noptional, their use is strongly encouraged. For more information about this\ncomponent's accessibility, check out\n[the text field's a11y section](https://www.github.com/material-components/material-components-android/tree/master/docs/components/TextField.md#making-text-fields-accessible).\n\n### Types\n\nMenus allow users to make a selection from multiple options. They are less\nprominent and take up less space than selection controls, such as a set of radio\nbuttons.\n\nThere are two types of menus: 1\\. [Dropdown menus](#dropdown-menus) (overflow,\ncontext, popup, and list popup window menus), 2\\.\n[Exposed dropdown menus](#exposed-dropdown-menus).\n\n\n\n## Dropdown menus\n\nDropdown menus display a list of options, triggered by an icon, button, or\naction. Their placement varies based on the element that opens them.\n\n### Dropdown menu examples\n\nAPI and source code:\n\n* `Menu`\n * [Class definition](https://developer.android.com/reference/android/view/Menu)\n* `MenuInflater`\n * [Class definition](https://developer.android.com/reference/android/view/MenuInflater)\n* `ContextMenu`\n * [Class definition](https://developer.android.com/reference/android/view/ContextMenu)\n* `PopupMenu`\n * [Class definition](https://developer.android.com/reference/android/widget/PopupMenu)\n* `ListPopupWindow`\n * [Class definition](https://developer.android.com/reference/android/widget/ListPopupWindow)\n\n#### Overflow menus\n\nThe following example shows an overflow menu.\n\n\n\nIn code:\n\n```kt\noverride fun onCreateOptionsMenu(menu: Menu): Boolean {\n val inflater: MenuInflater = menuInflater\n inflater.inflate(R.menu.overflow_menu, menu)\n return true\n}\n```\n\nIn `res/menu/overflow_menu.xml`:\n\n```xml\n\n \n \n \n\n```\n\n#### Context menus\n\nThe following example shows a context menu that appears when a `TextView` is\npressed for a designated amount of time.\n\n\n\nIn code:\n\n```kt\noverride fun onCreate(savedInstanceState: Bundle?) {\n ...\n val contextMenuTextView = view.findViewById(R.id.context_menu_tv)\n // Register context menu for TextView\n registerForContextMenu(contextMenuTextView)\n}\n\noverride fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {\n val contextMenuTextView = v as TextView\n val context = context\n // Add menu items via menu.add\n menu.add(R.string.option_1)\n .setOnMenuItemClickListener { item: MenuItem? ->\n // Respond to item click.\n }\n menu.add(R.string.option_2)\n .setOnMenuItemClickListener { item: MenuItem? ->\n // Respond to item click.\n }\n}\n\noverride fun onContextMenuClosed(menu: Menu) {\n // Respond to context menu being closed.\n}\n```\n\nIn the layout:\n\n```xml\n\n```\n\nAlternatively, you can inflate a context menu in `onCreateContextMenu` (as with\nthe overflow menu):\n\n```kt\noverride fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {\n super.onCreateContextMenu(menu, v, menuInfo)\n val inflater: MenuInflater = menuInflater\n inflater.inflate(R.menu.context_menu, menu)\n}\n\n// Then, to handle clicks:\noverride fun onContextItemSelected(item: MenuItem): Boolean {\n val info = item.menuInfo as AdapterView.AdapterContextMenuInfo\n return when (item.itemId) {\n R.id.option_1 -> {\n // Respond to context menu item 1 click.\n true\n }\n R.id.option_2 -> {\n // Respond to context menu item 2 click.\n true\n }\n else -> super.onContextItemSelected(item)\n }\n}\n```\n\nwith a `res/menu/context_menu.xml`:\n\n```xml\n\n \n \n\n```\n\n#### Popup menus\n\nThe following example shows a popup menu that displays when a button is clicked.\n\n\n\nIn code:\n\n```kt\noverride fun onCreate(savedInstanceState: Bundle?) {\n ...\n val button = view.findViewById + + {hasDismiss ? ( + + ) : null} + + ) +} + +export default forwardRef(M3Chip) diff --git a/m3-react/src/components/chip/index.ts b/m3-react/src/components/chip/index.ts new file mode 100644 index 0000000..06c82da --- /dev/null +++ b/m3-react/src/components/chip/index.ts @@ -0,0 +1,5 @@ +export { default as M3Chip } from './M3Chip' +export type { + M3ChipMethods, + M3ChipProps, +} from './M3Chip' diff --git a/m3-react/src/components/dialog/M3Dialog.tsx b/m3-react/src/components/dialog/M3Dialog.tsx index 05300f2..4dcbdf7 100644 --- a/m3-react/src/components/dialog/M3Dialog.tsx +++ b/m3-react/src/components/dialog/M3Dialog.tsx @@ -1,16 +1,20 @@ import type { + CSSProperties, FC, HTMLAttributes, } from 'react' -import { createPortal } from 'react-dom' -import { useMemo } from 'react' -import { useTransition } from 'react-transition-state' +import { useEffect, useMemo, useRef, useState } from 'react' + +import { + m3MotionDurations, + m3MotionEasings, +} from '@modulify/m3-foundation/lib/motion' +import { M3Surface } from '@/components/surface' import { - useRecord, - useWatch, -} from '@/hooks' -import { distinct } from '@/utils/content' + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' export interface M3DialogProps extends HTMLAttributes { @@ -19,7 +23,17 @@ export interface M3DialogProps extends HTMLAttributes { onToggle?: (expanded: boolean) => void; } -const Icon: FC> = ({ +const DIALOG_WIDTH = 312 +const DIALOG_MIN_WIDTH = 280 +const DIALOG_MAX_WIDTH = 560 +const DIALOG_ROUNDING = 28 +const DIALOG_ENTRY_OFFSET_PX = 24 +const DIALOG_ELEVATION = 0 +const DIALOG_Z_INDEX = 1000 +const DIALOG_TRANSITION_MS = m3MotionDurations.medium2 +const DIALOG_TRANSITION_TIMING = m3MotionEasings.standard + +const Icon: FC> = defineSlot('M3Dialog.Icon', ({ className = '', children = [], ...attrs @@ -27,9 +41,9 @@ const Icon: FC> = ({
{children}
-) +)) -const Header: FC> = ({ +const Header: FC> = defineSlot('M3Dialog.Header', ({ className = '', children = [], ...attrs @@ -37,9 +51,9 @@ const Header: FC> = ({
{children}
-) +)) -const Footer: FC> = ({ +const Footer: FC> = defineSlot('M3Dialog.Footer', ({ className = '', children = [], ...attrs @@ -47,12 +61,13 @@ const Footer: FC> = ({
{children}
-) +)) const M3Dialog: FC = ({ opened = false, fullscreen = false, className = '', + style, children = [], onToggle = (_: boolean) => {}, ...attrs @@ -62,64 +77,115 @@ const M3Dialog: FC = ({ header: Header, footer: Footer, }), [children]) + const [dialogMounted, setDialogMounted] = useState(opened) + const [dialogVisible, setDialogVisible] = useState(false) + const frameRef = useRef(null) + const timeoutRef = useRef(null) + + useEffect(() => { + if (typeof window === 'undefined') { + setDialogMounted(opened) + setDialogVisible(opened) + return + } + + if (frameRef.current !== null) { + window.cancelAnimationFrame(frameRef.current) + frameRef.current = null + } + + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + + if (opened) { + setDialogMounted(true) + setDialogVisible(false) + + frameRef.current = window.requestAnimationFrame(() => { + frameRef.current = null + setDialogVisible(true) + }) + + return () => { + if (frameRef.current !== null) { + window.cancelAnimationFrame(frameRef.current) + frameRef.current = null + } + } + } + + setDialogVisible(false) + + if (dialogMounted) { + timeoutRef.current = window.setTimeout(() => { + timeoutRef.current = null + setDialogMounted(false) + }, DIALOG_TRANSITION_MS) + } + + return () => { + if (frameRef.current !== null) { + window.cancelAnimationFrame(frameRef.current) + frameRef.current = null + } + + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + } + }, [opened]) + + const dialogStyle: CSSProperties = { + opacity: dialogVisible ? 1 : 0, + transform: fullscreen + ? 'translate3d(0, 0, 0)' + : (dialogVisible + ? 'translate(-50%, -50%)' + : `translate(-50%, calc(-50% + ${DIALOG_ENTRY_OFFSET_PX}px))`), + transition: `opacity ${DIALOG_TRANSITION_MS}ms ${DIALOG_TRANSITION_TIMING}, transform ${DIALOG_TRANSITION_MS}ms ${DIALOG_TRANSITION_TIMING}`, + pointerEvents: dialogVisible ? 'auto' : 'none', + ...style, + } - const handlers = useRecord({ - onToggle, - }) - - useWatch(onToggle, onToggle => handlers.onToggle = onToggle) - - const [transition, toggle] = useTransition({ - timeout: 500, - preEnter: true, - preExit: true, - mountOnEnter: true, - unmountOnExit: true, - }) - - toggle(opened) - - return createPortal( -
onToggle(false)} + {...attrs} > - {fullscreen ? null : ( -
handlers.onToggle(false)} - /> - )} - - {transition.isMounted && ( -
- {slots.icon} - {slots.header} - {content.length ? ( -
- {content} -
- ) : null} - {slots.footer} -
- )} -
, - document.body + {slots.icon} + {slots.header} + {content.length ? ( +
+ {content} +
+ ) : null} + {slots.footer} + ) } diff --git a/m3-react/src/components/icon/M3Icon.tsx b/m3-react/src/components/icon/M3Icon.tsx index a8d8b72..86a8e8d 100644 --- a/m3-react/src/components/icon/M3Icon.tsx +++ b/m3-react/src/components/icon/M3Icon.tsx @@ -25,7 +25,7 @@ const M3Icon: FC = ({ className = '', ...attrs }) => { - const appearanceInherited: Appearance = useContext(M3IconAppearance) + const appearanceInherited = useContext(M3IconAppearance) const appearanceActual: Appearance = appearanceInherited ?? appearance return ( @@ -42,4 +42,4 @@ const M3Icon: FC = ({ ) } -export default M3Icon \ No newline at end of file +export default M3Icon diff --git a/m3-react/src/components/menu/M3MenuItem.tsx b/m3-react/src/components/menu/M3MenuItem.tsx index 672adc1..94c5ad1 100644 --- a/m3-react/src/components/menu/M3MenuItem.tsx +++ b/m3-react/src/components/menu/M3MenuItem.tsx @@ -18,7 +18,10 @@ import { } from 'react' import { compose } from '@/utils/events' -import { distinct } from '@/utils/content' +import { + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' export interface M3MenuItemProps extends HTMLAttributes { @@ -27,8 +30,8 @@ export interface M3MenuItemProps extends HTMLAttributes { disabled?: boolean; } -const Leading: FC<{ children: ReactNode }> = props => <>{props.children} -const Trailing: FC<{ children: ReactNode }> = props => <>{props.children} +const Leading: FC<{ children: ReactNode }> = defineSlot('M3MenuItem.Leading', props => <>{props.children}) +const Trailing: FC<{ children: ReactNode }> = defineSlot('M3MenuItem.Trailing', props => <>{props.children}) const M3MenuItem: FC = ({ href, diff --git a/m3-react/src/components/navigation/M3Navigation.tsx b/m3-react/src/components/navigation/M3Navigation.tsx index c301549..1126550 100644 --- a/m3-react/src/components/navigation/M3Navigation.tsx +++ b/m3-react/src/components/navigation/M3Navigation.tsx @@ -1,7 +1,6 @@ import type { FC, HTMLAttributes, - ReactNode, } from 'react' import type { @@ -15,23 +14,24 @@ import M3NavigationAppearance from '@/components/navigation/M3NavigationAppearan import M3NavigationSection from './M3NavigationSection' import { - Children, - isValidElement, useEffect, useMemo, } from 'react' import { createPortal } from 'react-dom' +import { compose } from '@/utils/events' +import { + defineSlot, + distinct, +} from '@/utils/content' +import { toClassName } from '@/utils/styling' import { useBreakpoint, useRecord, useWatch, } from '@/hooks' -import { compose } from '@/utils/events' -import { toClassName } from '@/utils/styling' - export interface M3NavigationProps extends HTMLAttributes { appearance?: Appearance; alignment?: Alignment; @@ -39,7 +39,7 @@ export interface M3NavigationProps extends HTMLAttributes { onToggle?: (expanded: boolean) => void; } -const Top: FC> = ({ +const Top: FC> = defineSlot('M3Navigation.Top', ({ className = '', children = [], ...attrs @@ -47,9 +47,9 @@ const Top: FC> = ({
{children}
-) +)) -const Header: FC> = ({ +const Header: FC> = defineSlot('M3Navigation.Header', ({ className = '', children = [], ...attrs @@ -57,9 +57,9 @@ const Header: FC> = ({
{children}
-) +)) -const Subheader: FC> = ({ +const Subheader: FC> = defineSlot('M3Navigation.Subheader', ({ className = '', children = [], ...attrs @@ -67,37 +67,7 @@ const Subheader: FC> = ({ {children} -) - -const distinct = (children: ReactNode) => { - let top: ReactNode = null - let header: ReactNode = null - let subheader: ReactNode = null - - const content: ReactNode[] = [] - const sections: ReactNode[] = [] - - Children.forEach(children, child => { - if (isValidElement(child)) { - switch (child.type) { - case Top: - top = child; break - case Header: - header = child; break - case Subheader: - subheader = child; break - case M3NavigationSection: - sections.push(child); break - default: - content.push(child) - } - } else { - content.push(child) - } - }) - - return { top, header, subheader, content, sections } -} +)) const M3Navigation: FC = ({ appearance = 'auto', @@ -116,7 +86,16 @@ const M3Navigation: FC = ({ transitioning: expanded, }, ['appearance', 'transitioning']) - const slots = useMemo(() => distinct(children), [children]) + const parsed = useMemo(() => distinct(children, { + slots: { + top: Top, + header: Header, + subheader: Subheader, + }, + collections: { + sections: M3NavigationSection, + }, + }), [children]) const handlers = useRecord({ onToggle, @@ -155,7 +134,7 @@ const M3Navigation: FC = ({ >
handlers.onToggle(false)} /> @@ -174,14 +153,14 @@ const M3Navigation: FC = ({ }, onTransitionEnd)} {...attrs} > - {slots.top} - {slots.header} + {parsed.slots.top} + {parsed.slots.header} - {slots.subheader} - {slots.content} + {parsed.slots.subheader} + {parsed.content} - {slots.sections} + {parsed.collections.sections} , @@ -193,4 +172,4 @@ export default Object.assign(M3Navigation, { Header, Subheader, Top, -}) \ No newline at end of file +}) diff --git a/m3-react/src/components/navigation/M3NavigationSection.tsx b/m3-react/src/components/navigation/M3NavigationSection.tsx index 4d07776..f66bc91 100644 --- a/m3-react/src/components/navigation/M3NavigationSection.tsx +++ b/m3-react/src/components/navigation/M3NavigationSection.tsx @@ -3,10 +3,13 @@ import type { HTMLAttributes, } from 'react' -import { distinct } from '@/utils/content' +import { + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' -const Header: FC> = ({ +const Header: FC> = defineSlot('M3NavigationSection.Header', ({ className = '', children = [], ...attrs @@ -14,7 +17,7 @@ const Header: FC> = ({
{children}
-) +)) const M3NavigationSection: FC> = ({ role = 'group', @@ -40,4 +43,4 @@ const M3NavigationSection: FC> = ({ export default Object.assign(M3NavigationSection, { Header, -}) \ No newline at end of file +}) diff --git a/m3-react/src/components/navigation/M3NavigationTab.tsx b/m3-react/src/components/navigation/M3NavigationTab.tsx index 518bfe4..af773b8 100644 --- a/m3-react/src/components/navigation/M3NavigationTab.tsx +++ b/m3-react/src/components/navigation/M3NavigationTab.tsx @@ -31,7 +31,10 @@ import { } from '@/hooks' import { compose } from '@/utils/events' -import { distinct } from '@/utils/content' +import { + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' import { useM3NavigationAppearance } from './M3NavigationAppearance' @@ -47,7 +50,7 @@ export interface M3NavigationTabProps extends HTMLAttributes { export interface M3NavigationTabMethods extends Clickable, Focusable {} -const Icon: FC> = ({ +const Icon: FC> = defineSlot('M3NavigationTab.Icon', ({ className = '', children = [], ...attrs @@ -55,10 +58,10 @@ const Icon: FC> = ({ {children} -) +)) -const Label: FC<{ children: ReactNode }> = props => <>{props.children} -const Badge: FC<{ children: ReactNode }> = props => <>{props.children} +const Label: FC<{ children: ReactNode }> = defineSlot('M3NavigationTab.Label', props => <>{props.children}) +const Badge: FC<{ children: ReactNode }> = defineSlot('M3NavigationTab.Badge', props => <>{props.children}) const M3NavigationTab: ForwardRefRenderFunction< M3NavigationTabMethods, @@ -194,4 +197,4 @@ export default Object.assign(forwardRef(M3NavigationTab), { Badge, Icon, Label, -}) \ No newline at end of file +}) diff --git a/m3-react/src/components/popper/hooks.ts b/m3-react/src/components/popper/hooks.ts index 801a034..c0b9c9c 100644 --- a/m3-react/src/components/popper/hooks.ts +++ b/m3-react/src/components/popper/hooks.ts @@ -38,7 +38,7 @@ export const useAutoAdjust = ( }, [adjust]) export const useDelay = (delay: M3PopperProps['delay']) => { - const normalized = useMemo(() => normalizeDelay(delay), [delay]) + const normalized = useMemo(() => normalizeDelay(delay ?? 0), [delay]) return [ useMemo(() => normalized.show, [normalized]), diff --git a/m3-react/src/components/radio/M3Radio.tsx b/m3-react/src/components/radio/M3Radio.tsx new file mode 100644 index 0000000..94495da --- /dev/null +++ b/m3-react/src/components/radio/M3Radio.tsx @@ -0,0 +1,114 @@ +import type { + ForwardRefRenderFunction, + HTMLAttributes, +} from 'react' + +import type { + Clickable, + Focusable, +} from '@modulify/m3-foundation' + +import type { M3RippleMethods } from '@/components/ripple' + +import { M3Ripple } from '@/components/ripple' + +import { + forwardRef, + useCallback, + useImperativeHandle, + useMemo, + useRef, +} from 'react' + +import { + useElementEffect, + useId, + useTarget, +} from '@/hooks' + +import { toClassName } from '@/utils/styling' + +export interface M3RadioProps extends HTMLAttributes { + id?: string; + name?: string; + model?: unknown; + value?: unknown; + invalid?: boolean; + disabled?: boolean; + equalsFn?: (a: unknown, b: unknown) => boolean; + onChange?: (value: unknown) => void; +} + +export interface M3RadioMethods extends Clickable, Focusable {} + +const M3Radio: ForwardRefRenderFunction< + M3RadioMethods, + M3RadioProps +> = ({ + id, + name, + model, + value = true, + invalid = false, + disabled = false, + equalsFn = (a: unknown, b: unknown): boolean => a === b, + className = '', + onChange = (_: unknown) => {}, + ...args +}, ref) => { + const root = useRef(null) + const input = useRef(null) + const ripple = useRef(null) + const [rippleTarget, setRippleTarget] = useTarget() + + useImperativeHandle(ref, () => ({ + click: () => input.current?.click(), + focus: () => input.current?.focus(), + blur: () => input.current?.blur(), + })) + + useElementEffect(root, setRippleTarget) + + const checked = useMemo(() => equalsFn(model, value), [equalsFn, model, value]) + const inputId = useId(id, 'm3-radio') + + const handleChange = useCallback((nextChecked: boolean) => { + if (nextChecked) { + onChange(value) + } + }, [onChange, value]) + + return ( + + + + handleChange(event.currentTarget.checked)} + /> + + + + + ) +} + +export default forwardRef(M3Radio) diff --git a/m3-react/src/components/radio/index.ts b/m3-react/src/components/radio/index.ts new file mode 100644 index 0000000..130f2c3 --- /dev/null +++ b/m3-react/src/components/radio/index.ts @@ -0,0 +1,6 @@ +export type { + M3RadioMethods, + M3RadioProps, +} from './M3Radio' + +export { default as M3Radio } from './M3Radio' diff --git a/m3-react/src/components/rich-tooltip/M3RichTooltip.tsx b/m3-react/src/components/rich-tooltip/M3RichTooltip.tsx index 42767e0..599234c 100644 --- a/m3-react/src/components/rich-tooltip/M3RichTooltip.tsx +++ b/m3-react/src/components/rich-tooltip/M3RichTooltip.tsx @@ -19,14 +19,17 @@ import { useRef, } from 'react' -import { distinct } from '@/utils/content' +import { + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' export interface M3RichTooltipProps extends M3PopperProps {} export interface M3RichTooltipMethods extends M3PopperMethods {} -const Heading: FC<{ children: ReactNode }> = props => <>{props.children} -const Footer: FC<{ children: ReactNode }> = props => <>{props.children} +const Heading: FC<{ children: ReactNode }> = defineSlot('M3RichTooltip.Heading', props => <>{props.children}) +const Footer: FC<{ children: ReactNode }> = defineSlot('M3RichTooltip.Footer', props => <>{props.children}) const M3RichTooltip: ForwardRefRenderFunction< M3RichTooltipMethods, @@ -46,7 +49,7 @@ const M3RichTooltip: ForwardRefRenderFunction< }) useImperativeHandle(ref, () => ({ - adjust: () => popper.current?.adjust(), + adjust: () => popper.current?.adjust() ?? Promise.resolve(), contains: (el: Element | null) => popper.current?.contains(el) ?? false, show: (immediately = false) => popper.current?.show(immediately), hide: (immediately = false, reason: 'generic') => popper.current?.hide(immediately, reason), diff --git a/m3-react/src/components/scroll-rail/M3ScrollRail.tsx b/m3-react/src/components/scroll-rail/M3ScrollRail.tsx index 9b9b423..16e501f 100644 --- a/m3-react/src/components/scroll-rail/M3ScrollRail.tsx +++ b/m3-react/src/components/scroll-rail/M3ScrollRail.tsx @@ -33,8 +33,8 @@ const M3ScrollRail: ForwardRefRenderFunction< M3ScrollRailMethods, M3ScrollRailProps > = ({ - horizontal, - disabled, + horizontal = false, + disabled = false, className = '', ...attrs }, ref) => { diff --git a/m3-react/src/components/select/M3Select.tsx b/m3-react/src/components/select/M3Select.tsx index fa56363..9274be4 100644 --- a/m3-react/src/components/select/M3Select.tsx +++ b/m3-react/src/components/select/M3Select.tsx @@ -27,7 +27,10 @@ import { useId, } from '@/hooks' -import { distinct } from '@/utils/content' +import { + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' export type M3SelectOption = { @@ -68,9 +71,9 @@ const CaretIcon: FC> = (attrs) => ( ) -const Leading: FC<{ children: ReactNode }> = props => <>{props.children} -const OptionLeading: FC<{ children: ReactNode }> = props => <>{props.children} -const OptionContent: FC<{ children: ReactNode }> = props => <>{props.children} +const Leading: FC<{ children: ReactNode }> = defineSlot('M3Select.Leading', props => <>{props.children}) +const OptionLeading: FC<{ children: ReactNode }> = defineSlot('M3Select.OptionLeading', props => <>{props.children}) +const OptionContent: FC<{ children: ReactNode }> = defineSlot('M3Select.OptionContent', props => <>{props.children}) const asRenderProp = (value: unknown): null | ((context: Context) => ReactNode) => { return typeof value === 'function' ? value as (context: Context) => ReactNode : null diff --git a/m3-react/src/components/side-sheet/M3SideSheet.tsx b/m3-react/src/components/side-sheet/M3SideSheet.tsx index ee0dc65..5d12b8d 100644 --- a/m3-react/src/components/side-sheet/M3SideSheet.tsx +++ b/m3-react/src/components/side-sheet/M3SideSheet.tsx @@ -1,42 +1,86 @@ import type { FC, - HTMLAttributes, ReactNode, } from 'react' +import type { M3SurfaceProps } from '@/components/surface' import { M3IconButton } from '@/components/icon-button' import { M3ScrollRail } from '@/components/scroll-rail' +import { M3Surface } from '@/components/surface' +import { m3MotionDurations } from '@modulify/m3-foundation/lib/motion' import { useId, } from '@/hooks' -import { createPortal } from 'react-dom' - import { useMemo } from 'react' +import { useTransition } from 'react-transition-state' -import { distinct } from '@/utils/content' +import { + defineSlot, + distinct, +} from '@/utils/content' import { toClassName } from '@/utils/styling' -export interface M3SideSheetProps extends HTMLAttributes { +export interface M3SideSheetProps extends Omit< + M3SurfaceProps, + | 'anchor' + | 'children' + | 'mode' + | 'onDismiss' + | 'onToggle' + | 'scrim' + | 'shown' + | 'tag' +> { id?: string; shown?: boolean; docked?: boolean; + children?: ReactNode | ReactNode[]; onToggle?: (shown: boolean) => void; + onDismiss?: () => void; } -const Affordance: FC<{ children: ReactNode }> = props => <>{props.children} -const Title: FC<{ children: ReactNode }> = props => <>{props.children} -const CloseIcon: FC<{ children: ReactNode }> = props => <>{props.children} -const Footer: FC<{ children: ReactNode }> = props => <>{props.children} +const Affordance: FC<{ children: ReactNode }> = defineSlot('M3SideSheet.Affordance', props => <>{props.children}) +const Title: FC<{ children: ReactNode }> = defineSlot('M3SideSheet.Title', props => <>{props.children}) +const CloseIcon: FC<{ children: ReactNode }> = defineSlot('M3SideSheet.CloseIcon', props => <>{props.children}) +const Footer: FC<{ children: ReactNode }> = defineSlot('M3SideSheet.Footer', props => <>{props.children}) + +const DOCKED_WIDTH = 256 +const MODAL_MIN_WIDTH = 320 +const MODAL_MAX_WIDTH = 400 +const MODAL_RADIUS = 16 +const SIDE_SHEET_Z_INDEX = 1000 +const SIDE_SHEET_TRANSITION_MS = m3MotionDurations['extra-long2'] const M3SideSheet: FC = ({ id, shown = false, docked = false, className = '', + style, + fillWidth = false, + fillHeight = true, + width = null, + minWidth = null, + maxWidth = null, + minHeight = null, + maxHeight = null, + rounding = 0, + roundingTopLeft = null, + roundingTopRight = 0, + roundingBottomRight = 0, + roundingBottomLeft = null, + transitionMs = 220, + transitionTiming, + overflow = 'hidden', + elevation = 0, + variant = 'surface-container-low', + role = 'dialog', + zIndex = SIDE_SHEET_Z_INDEX, children = [], onToggle = (_: boolean) => {}, + onDismiss = () => {}, ...attrs }) => { const _id = useId(id, 'm3-side-sheet') @@ -52,69 +96,118 @@ const M3SideSheet: FC = ({ ? {} : { 'aria-labelledby': _id + '-title' } - return createPortal( - <> - {docked ? null : ( -
onToggle(false)} - /> - )} - -