rpr-cli 0.1.25__tar.gz → 0.1.26__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/PKG-INFO +8 -4
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/README.md +7 -3
- rpr_cli-0.1.26/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponent.spec.tsx +147 -0
- rpr_cli-0.1.26/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponent.tsx +94 -0
- rpr_cli-0.1.26/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponentList.spec.tsx +277 -0
- rpr_cli-0.1.26/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponentList.tsx +112 -0
- rpr_cli-0.1.26/Sample-dot-github/scaffolds/frontend/virtual-list/index.ts +1 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/code-map.md +6 -6
- rpr_cli-0.1.26/plan-languageAwareTemplateAdd.prompt.md +63 -0
- rpr_cli-0.1.26/src/rpr/__init__.py +1 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/checks/instructions.py +2 -1
- rpr_cli-0.1.26/src/rpr/commands/add.py +298 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/generate/domain.py +2 -2
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/generate/ui.py +5 -2
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/js_special_files/sticky-navigation.md +15 -9
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/sticky-navigation.spec.md +231 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component-list.md +145 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component-list.spec.md +304 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component.md +110 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-component.spec.md +168 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/virtual-list-index.md +9 -0
- rpr_cli-0.1.26/src/rpr/scaffolds/js_special_files/vitest-browser-config.md +24 -0
- rpr_cli-0.1.26/src/rpr/templates/registry.py +232 -0
- rpr_cli-0.1.26/tests/test_add.py +367 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_check.py +7 -0
- rpr_cli-0.1.25/src/rpr/__init__.py +0 -1
- rpr_cli-0.1.25/src/rpr/commands/add.py +0 -166
- rpr_cli-0.1.25/src/rpr/templates/registry.py +0 -81
- rpr_cli-0.1.25/tests/test_add.py +0 -155
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/.github/agents/story.agent.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/.github/workflows/publish-pypi.yml +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/.gitignore +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/CLAUDE.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/GEMINI.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/LICENSE +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/all.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/api.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/domain.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/frontend.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/rust-engine.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/setup-guide.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/tooling-setup.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/instructions/tooling.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/scaffolds/README.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/scaffolds/api/main.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/scaffolds/domain/base-repo.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/scaffolds/domain/base_entity.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/scaffolds/frontend/api.service.ts +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/Sample-dot-github/scaffolds/frontend/useStateNavigation.ts +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/changelogs/mui_v9.txt +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/nx_uv_setup.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/patch_output.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/pyproject.toml +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/Cargo.lock +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/Cargo.toml +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/pyproject.toml +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/rpr_parser.pyi +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/config/container.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/config/mod.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/config/settings.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/graph.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/lib.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/models/dtos/mod.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/models/entities/mod.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/models/entities/users.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/models/mod.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/parse.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/repos/base.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/repos/mod.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/repos/users.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/resolve.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/src/walker.rs +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/rpr-parser/uv.lock +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/special_files.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/approval.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/bootstrap.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/client.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/control.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/mock.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/runtime.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/session.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/tools/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/tools/base.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/tools/mutating.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/tools/readonly.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/agent/tools/registry.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/catalog.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/chat_service.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/checks.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/cli_adapter.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/completer.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/conversation_service.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/prompt_service.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/selector.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/application/shell.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/checks/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/checks/base.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/checks/packages.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/checks/workspace.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/cli.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/chat.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/check.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/generate/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/generate/api.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/generate/engine.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/generate/storybook.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/init.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/map.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/refresh.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/settings.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/commands/sync.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/context.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/dependencies.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/dependency_manifest_data.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/base.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/claude.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/copilot.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/cursor.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/gemini.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/generators/github.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/manifest_refresh.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/architecture.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/chains.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/classifier.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/coverage.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/dependencies.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/extractor.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/graph.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/output.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/responsibility.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/topology.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/map/walker.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/domain/base_entity.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/domain/base_repo.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/domain/container.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/domain/settings.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/all.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/api.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/domain.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/frontend.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/rust-engine.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/setup-guide.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/tooling-setup.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/instructions/tooling.instructions.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/js_special_files/fetch.service.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/special_files/domain_container.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/special_files/domain_settings.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/special_files/dto_util.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/special_files/encrypted_column.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/special_files/mapper_util.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/scaffolds/special_files/partial_update.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/templates/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/ui/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/ui/console.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/ui/markdown.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/ui/prompt_session.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/ui/renderers.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/ui/theme.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/src/rpr/workspace.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/README.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/00-bootstrap-cli.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/01-rpr-init.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/02-rpr-generate-domain.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/03-rpr-generate-api.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/04-rpr-generate-ui.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/05-rpr-generate-engine.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/06-rpr-generate-storybook.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/07-rpr-sync-rules.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/08-rpr-add-template.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/09-rpr-check.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/10-rpr-domain-config.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/18-rpr-terminal-ui-foundation.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/19-rpr-markdown-rendering.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/20-rpr-chat-repl.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/21-rpr-agent-tool-loop.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/22-rpr-chat-tui.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/23-rpr-chat-unified-command-shell.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/24-rpr-async-chat-runtime-and-services.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/25-rpr-prompt-service-and-conversation-lifecycle.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/26-rpr-shared-command-catalog.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/27-rpr-slash-command-selection-panel.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/28-rpr-chat-cancellable-and-multiline-input.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/chat/29-rpr-file-context-selection.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/11-rpr-map-command.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/12-rpr-map-file-walker.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/13-rpr-map-rust-engine.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/13a-rpr-map-gitignore.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/14-rpr-map-graph-builder.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/15-rpr-map-analysis-bridge.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/16-rpr-map-layer-classifier.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/17-rpr-map-output.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/17a-rpr-map-directory-topology.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/17b-rpr-map-architecture-assessment.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/17c-rpr-map-representative-chains.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/completed/map/17d-rpr-map-multi-resolution-output.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/map/30-rpr-map-directory-view-and-cycle-detection.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/map/31-rpr-map-coupling-hotspots-and-verdict-reasoning.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/stories/map/32-rpr-map-dependencies-responsibilities-and-test-coverage.md +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/__init__.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_architecture.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_architecture_unwrapped.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_catalog.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_chat.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_chat_service.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_completer.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_context.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_generate_api.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_generate_domain.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_generate_engine.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_generate_storybook.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_generate_ui.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_init.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_chains.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_classifier.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_coverage.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_dependencies.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_extractor.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_graph.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_output.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_responsibility.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_topology.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_map_walker.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_packaging.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_prompt_session.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_refresh.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_rpr_parser_stub.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_selector.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_settings.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_sync.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/tests/test_ui.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/update_output.py +0 -0
- {rpr_cli-0.1.25 → rpr_cli-0.1.26}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpr-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: RPR CLI -- React Python Rust monorepo scaffolding orchestrator
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -106,11 +106,15 @@ The scaffold version manifest that powers generated dependency specs and the syn
|
|
|
106
106
|
Sync project context to your favorite AI tools and add common code patterns.
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
rpr sync rules
|
|
110
|
-
rpr add template mapper_util
|
|
111
|
-
rpr add template
|
|
109
|
+
rpr sync rules # Sync AI instructions to Cursor, Copilot, Claude Code, and Gemini
|
|
110
|
+
rpr add template python mapper_util # Add a Python code template to your project
|
|
111
|
+
rpr add template node sticky_navigation --include-tests
|
|
112
|
+
rpr add template node -l # List available Node/TSX templates
|
|
113
|
+
rpr add template --list # List all available code templates
|
|
112
114
|
```
|
|
113
115
|
|
|
116
|
+
Use `--output-dir` to write generated files to a different directory while keeping the template filenames. Templates with test companions can use `--include-tests`; Node browser-test templates print the extra npm dev dependencies and any Playwright setup notes they need. Existing config files such as `vitest.config.ts` are not merged automatically; RPR leaves them unchanged unless you rerun with `--force` to replace them.
|
|
117
|
+
|
|
114
118
|
### 💬 Interactive Shell
|
|
115
119
|
|
|
116
120
|
`rpr chat` is the primary interactive shell for the product. It runs a Rich-based session with slash commands for both chat controls and existing CLI workflows.
|
|
@@ -91,11 +91,15 @@ The scaffold version manifest that powers generated dependency specs and the syn
|
|
|
91
91
|
Sync project context to your favorite AI tools and add common code patterns.
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
rpr sync rules
|
|
95
|
-
rpr add template mapper_util
|
|
96
|
-
rpr add template
|
|
94
|
+
rpr sync rules # Sync AI instructions to Cursor, Copilot, Claude Code, and Gemini
|
|
95
|
+
rpr add template python mapper_util # Add a Python code template to your project
|
|
96
|
+
rpr add template node sticky_navigation --include-tests
|
|
97
|
+
rpr add template node -l # List available Node/TSX templates
|
|
98
|
+
rpr add template --list # List all available code templates
|
|
97
99
|
```
|
|
98
100
|
|
|
101
|
+
Use `--output-dir` to write generated files to a different directory while keeping the template filenames. Templates with test companions can use `--include-tests`; Node browser-test templates print the extra npm dev dependencies and any Playwright setup notes they need. Existing config files such as `vitest.config.ts` are not merged automatically; RPR leaves them unchanged unless you rerun with `--force` to replace them.
|
|
102
|
+
|
|
99
103
|
### 💬 Interactive Shell
|
|
100
104
|
|
|
101
105
|
`rpr chat` is the primary interactive shell for the product. It runs a Rich-based session with slash commands for both chat controls and existing CLI workflows.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { render } from 'vitest-browser-react';
|
|
5
|
+
import { VirtualComponent } from './VirtualComponent';
|
|
6
|
+
|
|
7
|
+
const createRect = (height: number) =>
|
|
8
|
+
({
|
|
9
|
+
x: 0,
|
|
10
|
+
y: 0,
|
|
11
|
+
width: 100,
|
|
12
|
+
height,
|
|
13
|
+
top: 0,
|
|
14
|
+
right: 100,
|
|
15
|
+
bottom: height,
|
|
16
|
+
left: 0,
|
|
17
|
+
toJSON: () => ({}),
|
|
18
|
+
}) as DOMRect;
|
|
19
|
+
|
|
20
|
+
class MockResizeObserver implements ResizeObserver {
|
|
21
|
+
static instances: MockResizeObserver[] = [];
|
|
22
|
+
|
|
23
|
+
observe = vi.fn();
|
|
24
|
+
unobserve = vi.fn();
|
|
25
|
+
disconnect = vi.fn();
|
|
26
|
+
|
|
27
|
+
constructor(private readonly callback: ResizeObserverCallback) {
|
|
28
|
+
MockResizeObserver.instances.push(this);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
emit() {
|
|
32
|
+
this.callback([], this);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('VirtualComponent', () => {
|
|
37
|
+
let measuredHeight = 72;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
MockResizeObserver.instances = [];
|
|
41
|
+
measuredHeight = 72;
|
|
42
|
+
vi.stubGlobal('ResizeObserver', MockResizeObserver);
|
|
43
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
|
|
44
|
+
function getBoundingClientRect(this: HTMLElement) {
|
|
45
|
+
return (this as HTMLElement).classList.contains('virtual-component-item')
|
|
46
|
+
? createRect(measuredHeight)
|
|
47
|
+
: createRect(0);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
|
|
51
|
+
configurable: true,
|
|
52
|
+
get() {
|
|
53
|
+
return measuredHeight;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('preserves the measured height when hidden and unmounts children', async () => {
|
|
64
|
+
function Harness() {
|
|
65
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<button onClick={() => setIsVisible((current) => !current)}>
|
|
70
|
+
Toggle
|
|
71
|
+
</button>
|
|
72
|
+
<VirtualComponent isVisible={isVisible}>
|
|
73
|
+
<div>Measured child</div>
|
|
74
|
+
</VirtualComponent>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const screen = await render(<Harness />);
|
|
80
|
+
|
|
81
|
+
const item = document.querySelector<HTMLDivElement>('.virtual-component-item');
|
|
82
|
+
|
|
83
|
+
expect(item).toBeTruthy();
|
|
84
|
+
expect(item?.style.flexShrink).toBe('0');
|
|
85
|
+
await expect.element(screen.getByText('Measured child')).toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
await screen.getByRole('button', { name: 'Toggle' }).click();
|
|
88
|
+
|
|
89
|
+
await expect.element(screen.getByText('Measured child')).not.toBeInTheDocument();
|
|
90
|
+
expect(item?.style.height).toBe('72px');
|
|
91
|
+
expect(item?.style.visibility).toBe('hidden');
|
|
92
|
+
expect(item?.dataset.virtualVisible).toBe('false');
|
|
93
|
+
|
|
94
|
+
await screen.getByRole('button', { name: 'Toggle' }).click();
|
|
95
|
+
|
|
96
|
+
await expect.element(screen.getByText('Measured child')).toBeInTheDocument();
|
|
97
|
+
expect(item?.style.height).toBe('');
|
|
98
|
+
expect(item?.style.flexShrink).toBe('0');
|
|
99
|
+
expect(item?.style.visibility).toBe('');
|
|
100
|
+
expect(item?.dataset.virtualVisible).toBe('true');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('uses the latest ResizeObserver measurement before hiding', async () => {
|
|
104
|
+
function Harness() {
|
|
105
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<button onClick={() => setIsVisible(false)}>Hide</button>
|
|
110
|
+
<VirtualComponent isVisible={isVisible}>
|
|
111
|
+
<div>Resizable child</div>
|
|
112
|
+
</VirtualComponent>
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const screen = await render(<Harness />);
|
|
118
|
+
|
|
119
|
+
measuredHeight = 96;
|
|
120
|
+
flushSync(() => {
|
|
121
|
+
MockResizeObserver.instances[0].emit();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const item = document.querySelector<HTMLDivElement>('.virtual-component-item');
|
|
125
|
+
|
|
126
|
+
await screen.getByRole('button', { name: 'Hide' }).click();
|
|
127
|
+
|
|
128
|
+
await expect.element(screen.getByText('Resizable child')).not.toBeInTheDocument();
|
|
129
|
+
expect(item?.style.height).toBe('96px');
|
|
130
|
+
expect(item?.style.visibility).toBe('hidden');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('observes on mount and disconnects on unmount', async () => {
|
|
134
|
+
const { unmount } = await render(
|
|
135
|
+
<VirtualComponent>
|
|
136
|
+
<div>Observed child</div>
|
|
137
|
+
</VirtualComponent>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(MockResizeObserver.instances).toHaveLength(1);
|
|
141
|
+
expect(MockResizeObserver.instances[0].observe).toHaveBeenCalledTimes(1);
|
|
142
|
+
|
|
143
|
+
await unmount();
|
|
144
|
+
|
|
145
|
+
expect(MockResizeObserver.instances[0].disconnect).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type VirtualComponentProps = React.PropsWithChildren<{
|
|
4
|
+
isVisible?: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
|
|
7
|
+
const setForwardedRef = (
|
|
8
|
+
ref: React.ForwardedRef<HTMLDivElement>,
|
|
9
|
+
node: HTMLDivElement | null
|
|
10
|
+
) => {
|
|
11
|
+
if (typeof ref === 'function') {
|
|
12
|
+
ref(node);
|
|
13
|
+
} else if (ref) {
|
|
14
|
+
ref.current = node;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const VirtualComponent = React.memo(
|
|
19
|
+
React.forwardRef<HTMLDivElement, VirtualComponentProps>(
|
|
20
|
+
({ children, isVisible = true }, forwardedRef) => {
|
|
21
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
22
|
+
const measuredHeight = useRef<number | null>(null);
|
|
23
|
+
const [height, setHeight] = useState<number | null>(null);
|
|
24
|
+
|
|
25
|
+
const setRef = useCallback(
|
|
26
|
+
(node: HTMLDivElement | null) => {
|
|
27
|
+
ref.current = node;
|
|
28
|
+
setForwardedRef(forwardedRef, node);
|
|
29
|
+
},
|
|
30
|
+
[forwardedRef]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const measureHeight = useCallback(() => {
|
|
34
|
+
const node = ref.current;
|
|
35
|
+
if (!node) return;
|
|
36
|
+
|
|
37
|
+
const nextHeight = Math.ceil(
|
|
38
|
+
Math.max(node.getBoundingClientRect().height, node.scrollHeight)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (nextHeight > 0 && measuredHeight.current !== nextHeight) {
|
|
42
|
+
measuredHeight.current = nextHeight;
|
|
43
|
+
setHeight(nextHeight);
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const shouldRenderChildren = isVisible || height === null;
|
|
48
|
+
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
if (shouldRenderChildren) {
|
|
51
|
+
measureHeight();
|
|
52
|
+
}
|
|
53
|
+
}, [measureHeight, shouldRenderChildren]);
|
|
54
|
+
|
|
55
|
+
useLayoutEffect(() => {
|
|
56
|
+
const node = ref.current;
|
|
57
|
+
if (!node || typeof ResizeObserver === 'undefined') return;
|
|
58
|
+
|
|
59
|
+
const observer = new ResizeObserver(() => {
|
|
60
|
+
if (shouldRenderChildren) {
|
|
61
|
+
measureHeight();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
observer.observe(node);
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
observer.disconnect();
|
|
69
|
+
};
|
|
70
|
+
}, [measureHeight, shouldRenderChildren]);
|
|
71
|
+
|
|
72
|
+
const visibleStyle: React.CSSProperties = { flexShrink: 0 };
|
|
73
|
+
const hiddenStyle: React.CSSProperties = height
|
|
74
|
+
? {
|
|
75
|
+
flexShrink: 0,
|
|
76
|
+
height,
|
|
77
|
+
overflow: 'hidden',
|
|
78
|
+
visibility: 'hidden',
|
|
79
|
+
}
|
|
80
|
+
: visibleStyle;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
ref={setRef}
|
|
85
|
+
className="virtual-component-item"
|
|
86
|
+
data-virtual-visible={isVisible ? 'true' : 'false'}
|
|
87
|
+
style={isVisible ? visibleStyle : hiddenStyle}
|
|
88
|
+
>
|
|
89
|
+
{shouldRenderChildren ? children : null}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
);
|
rpr_cli-0.1.26/Sample-dot-github/scaffolds/frontend/virtual-list/VirtualComponentList.spec.tsx
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { flushSync } from 'react-dom';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { render } from 'vitest-browser-react';
|
|
4
|
+
import { VirtualComponentList } from './VirtualComponentList';
|
|
5
|
+
|
|
6
|
+
const createRect = (height: number) =>
|
|
7
|
+
({
|
|
8
|
+
x: 0,
|
|
9
|
+
y: 0,
|
|
10
|
+
width: 100,
|
|
11
|
+
height,
|
|
12
|
+
top: 0,
|
|
13
|
+
right: 100,
|
|
14
|
+
bottom: height,
|
|
15
|
+
left: 0,
|
|
16
|
+
toJSON: () => ({}),
|
|
17
|
+
}) as DOMRect;
|
|
18
|
+
|
|
19
|
+
class MockIntersectionObserver implements IntersectionObserver {
|
|
20
|
+
static instances: MockIntersectionObserver[] = [];
|
|
21
|
+
|
|
22
|
+
readonly root: Element | Document | null;
|
|
23
|
+
readonly rootMargin: string;
|
|
24
|
+
readonly scrollMargin: string;
|
|
25
|
+
readonly thresholds: ReadonlyArray<number>;
|
|
26
|
+
observedElements = new Set<Element>();
|
|
27
|
+
observe = vi.fn((element: Element) => {
|
|
28
|
+
this.observedElements.add(element);
|
|
29
|
+
});
|
|
30
|
+
unobserve = vi.fn((element: Element) => {
|
|
31
|
+
this.observedElements.delete(element);
|
|
32
|
+
});
|
|
33
|
+
disconnect = vi.fn(() => {
|
|
34
|
+
this.observedElements.clear();
|
|
35
|
+
});
|
|
36
|
+
takeRecords = vi.fn(() => []);
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly callback: IntersectionObserverCallback,
|
|
40
|
+
options: IntersectionObserverInit = {}
|
|
41
|
+
) {
|
|
42
|
+
this.root = options.root ?? null;
|
|
43
|
+
this.rootMargin = options.rootMargin ?? '0px';
|
|
44
|
+
this.scrollMargin = '0px';
|
|
45
|
+
this.thresholds = Array.isArray(options.threshold)
|
|
46
|
+
? options.threshold
|
|
47
|
+
: [options.threshold ?? 0];
|
|
48
|
+
MockIntersectionObserver.instances.push(this);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
emit(entries: Array<{ target: Element; isIntersecting: boolean }>) {
|
|
52
|
+
this.callback(
|
|
53
|
+
entries.map(
|
|
54
|
+
(entry) =>
|
|
55
|
+
({
|
|
56
|
+
boundingClientRect: createRect(0),
|
|
57
|
+
intersectionRatio: entry.isIntersecting ? 1 : 0,
|
|
58
|
+
intersectionRect: createRect(0),
|
|
59
|
+
isIntersecting: entry.isIntersecting,
|
|
60
|
+
rootBounds: createRect(0),
|
|
61
|
+
target: entry.target,
|
|
62
|
+
time: 0,
|
|
63
|
+
}) as IntersectionObserverEntry
|
|
64
|
+
),
|
|
65
|
+
this
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class MockResizeObserver implements ResizeObserver {
|
|
71
|
+
observe = vi.fn();
|
|
72
|
+
unobserve = vi.fn();
|
|
73
|
+
disconnect = vi.fn();
|
|
74
|
+
|
|
75
|
+
constructor(private readonly callback: ResizeObserverCallback) {}
|
|
76
|
+
|
|
77
|
+
emit() {
|
|
78
|
+
this.callback([], this);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const renderItems = (count: number) =>
|
|
83
|
+
Array.from({ length: count }, (_, index) => <div key={index}>Item {index}</div>);
|
|
84
|
+
|
|
85
|
+
const paragraphHeights = [56, 104, 148, 92, 180, 128, 72, 156, 116, 204];
|
|
86
|
+
|
|
87
|
+
const renderVariableParagraphs = (count: number) =>
|
|
88
|
+
Array.from({ length: count }, (_, index) => (
|
|
89
|
+
<section key={index} data-paragraph-index={index}>
|
|
90
|
+
<h3>Paragraph container {index + 1}</h3>
|
|
91
|
+
{Array.from({ length: (index % 4) + 1 }, (__, paragraphIndex) => (
|
|
92
|
+
<p key={paragraphIndex}>
|
|
93
|
+
Paragraph {index + 1}.{paragraphIndex + 1} has variable content.
|
|
94
|
+
</p>
|
|
95
|
+
))}
|
|
96
|
+
</section>
|
|
97
|
+
));
|
|
98
|
+
|
|
99
|
+
const getVirtualItems = () =>
|
|
100
|
+
Array.from(document.querySelectorAll<HTMLDivElement>('.virtual-component-item'));
|
|
101
|
+
|
|
102
|
+
describe('VirtualComponentList', () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
MockIntersectionObserver.instances = [];
|
|
105
|
+
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
|
106
|
+
vi.stubGlobal('ResizeObserver', MockResizeObserver);
|
|
107
|
+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
|
|
108
|
+
function getBoundingClientRect(this: HTMLElement) {
|
|
109
|
+
const textContent = (this as HTMLElement).textContent ?? '';
|
|
110
|
+
const itemMatch = /Item (\d+)/.exec(textContent);
|
|
111
|
+
const paragraphMatch = /Paragraph container (\d+)/.exec(textContent);
|
|
112
|
+
const height = itemMatch
|
|
113
|
+
? 40 + Number(itemMatch[1]) * 10
|
|
114
|
+
: paragraphMatch
|
|
115
|
+
? paragraphHeights[Number(paragraphMatch[1]) - 1] ?? 80
|
|
116
|
+
: 0;
|
|
117
|
+
|
|
118
|
+
return createRect(height);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
|
|
122
|
+
configurable: true,
|
|
123
|
+
get() {
|
|
124
|
+
const textContent = (this as HTMLElement).textContent ?? '';
|
|
125
|
+
const itemMatch = /Item (\d+)/.exec(textContent);
|
|
126
|
+
const paragraphMatch = /Paragraph container (\d+)/.exec(textContent);
|
|
127
|
+
|
|
128
|
+
if (itemMatch) return 40 + Number(itemMatch[1]) * 10;
|
|
129
|
+
if (paragraphMatch) {
|
|
130
|
+
return paragraphHeights[Number(paragraphMatch[1]) - 1] ?? 80;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return 0;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
vi.restoreAllMocks();
|
|
140
|
+
vi.unstubAllGlobals();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('observes children on mount and disconnects the observer on unmount', async () => {
|
|
144
|
+
const { unmount } = await render(
|
|
145
|
+
<VirtualComponentList>{renderItems(3)}</VirtualComponentList>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const observer = MockIntersectionObserver.instances[0];
|
|
149
|
+
|
|
150
|
+
expect(observer.root).toBe(document.getElementById('virtual-component-list'));
|
|
151
|
+
expect(observer.rootMargin).toBe('100% 0px');
|
|
152
|
+
expect(observer.observe).toHaveBeenCalledTimes(3);
|
|
153
|
+
expect(observer.observedElements.size).toBe(3);
|
|
154
|
+
|
|
155
|
+
await unmount();
|
|
156
|
+
|
|
157
|
+
expect(observer.disconnect).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(observer.observedElements.size).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('unmounts hidden item content from intersection events while preserving height', async () => {
|
|
162
|
+
const screen = await render(
|
|
163
|
+
<VirtualComponentList>{renderItems(3)}</VirtualComponentList>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const observer = MockIntersectionObserver.instances[0];
|
|
167
|
+
const items = getVirtualItems();
|
|
168
|
+
|
|
169
|
+
flushSync(() => {
|
|
170
|
+
observer.emit([{ target: items[1], isIntersecting: false }]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect.element(screen.locator.getByText('Item 1')).not.toBeInTheDocument();
|
|
174
|
+
expect(items[1].style.height).toBe('50px');
|
|
175
|
+
expect(items[1].style.flexShrink).toBe('0');
|
|
176
|
+
expect(items[1].style.visibility).toBe('hidden');
|
|
177
|
+
expect(items[1].dataset.virtualVisible).toBe('false');
|
|
178
|
+
await expect.element(screen.locator.getByText('Item 0')).toBeInTheDocument();
|
|
179
|
+
await expect.element(screen.locator.getByText('Item 2')).toBeInTheDocument();
|
|
180
|
+
|
|
181
|
+
flushSync(() => {
|
|
182
|
+
observer.emit([{ target: items[1], isIntersecting: true }]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await expect.element(screen.locator.getByText('Item 1')).toBeInTheDocument();
|
|
186
|
+
expect(items[1].style.height).toBe('');
|
|
187
|
+
expect(items[1].style.visibility).toBe('');
|
|
188
|
+
expect(items[1].dataset.virtualVisible).toBe('true');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('preserves each measured height for variable-sized paragraph containers', async () => {
|
|
192
|
+
const screen = await render(
|
|
193
|
+
<VirtualComponentList>{renderVariableParagraphs(10)}</VirtualComponentList>
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const observer = MockIntersectionObserver.instances[0];
|
|
197
|
+
const items = getVirtualItems();
|
|
198
|
+
|
|
199
|
+
flushSync(() => {
|
|
200
|
+
observer.emit([
|
|
201
|
+
{ target: items[1], isIntersecting: false },
|
|
202
|
+
{ target: items[4], isIntersecting: false },
|
|
203
|
+
{ target: items[8], isIntersecting: false },
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await expect
|
|
208
|
+
.element(screen.locator.getByText('Paragraph container 2'))
|
|
209
|
+
.not.toBeInTheDocument();
|
|
210
|
+
await expect
|
|
211
|
+
.element(screen.locator.getByText('Paragraph container 5'))
|
|
212
|
+
.not.toBeInTheDocument();
|
|
213
|
+
await expect
|
|
214
|
+
.element(screen.locator.getByText('Paragraph container 9'))
|
|
215
|
+
.not.toBeInTheDocument();
|
|
216
|
+
expect(items[1].style.height).toBe('104px');
|
|
217
|
+
expect(items[4].style.height).toBe('180px');
|
|
218
|
+
expect(items[8].style.height).toBe('116px');
|
|
219
|
+
expect(items[1].style.flexShrink).toBe('0');
|
|
220
|
+
expect(items[4].style.flexShrink).toBe('0');
|
|
221
|
+
expect(items[8].style.flexShrink).toBe('0');
|
|
222
|
+
|
|
223
|
+
flushSync(() => {
|
|
224
|
+
observer.emit([{ target: items[4], isIntersecting: true }]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await expect
|
|
228
|
+
.element(screen.locator.getByText('Paragraph container 5'))
|
|
229
|
+
.toBeInTheDocument();
|
|
230
|
+
expect(items[4].style.height).toBe('');
|
|
231
|
+
expect(items[1].style.height).toBe('104px');
|
|
232
|
+
expect(items[8].style.height).toBe('116px');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('unobserves removed children without disconnecting the whole list observer', async () => {
|
|
236
|
+
const { rerender } = await render(
|
|
237
|
+
<VirtualComponentList>{renderItems(3)}</VirtualComponentList>
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const observer = MockIntersectionObserver.instances[0];
|
|
241
|
+
const removedItem = getVirtualItems()[2];
|
|
242
|
+
|
|
243
|
+
await rerender(<VirtualComponentList>{renderItems(2)}</VirtualComponentList>);
|
|
244
|
+
|
|
245
|
+
expect(observer.unobserve).toHaveBeenCalledWith(removedItem);
|
|
246
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('does not re-render the parent container when item visibility changes', async () => {
|
|
250
|
+
const onParentRender = vi.fn();
|
|
251
|
+
|
|
252
|
+
function Parent() {
|
|
253
|
+
onParentRender();
|
|
254
|
+
|
|
255
|
+
return <VirtualComponentList>{renderItems(3)}</VirtualComponentList>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await render(<Parent />);
|
|
259
|
+
|
|
260
|
+
const parentRenderCount = onParentRender.mock.calls.length;
|
|
261
|
+
const observer = MockIntersectionObserver.instances[0];
|
|
262
|
+
const items = getVirtualItems();
|
|
263
|
+
|
|
264
|
+
flushSync(() => {
|
|
265
|
+
observer.emit([
|
|
266
|
+
{ target: items[0], isIntersecting: false },
|
|
267
|
+
{ target: items[1], isIntersecting: false },
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
flushSync(() => {
|
|
272
|
+
observer.emit([{ target: items[0], isIntersecting: true }]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(onParentRender).toHaveBeenCalledTimes(parentRenderCount);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Stack, type BoxProps } from '@mui/material';
|
|
2
|
+
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { VirtualComponent } from './VirtualComponent';
|
|
4
|
+
|
|
5
|
+
type VirtualListProps = {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
sx?: BoxProps['sx'];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type VisibilityListener = (isVisible: boolean) => void;
|
|
11
|
+
|
|
12
|
+
type RegisterVirtualComponent = (
|
|
13
|
+
node: HTMLElement,
|
|
14
|
+
onVisibilityChange: VisibilityListener
|
|
15
|
+
) => () => void;
|
|
16
|
+
|
|
17
|
+
type VirtualComponentListItemProps = React.PropsWithChildren<{
|
|
18
|
+
registerVirtualComponent: RegisterVirtualComponent;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
const VirtualComponentListItem = React.memo(
|
|
22
|
+
({ children, registerVirtualComponent }: VirtualComponentListItemProps) => {
|
|
23
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
24
|
+
const nodeRef = useRef<HTMLDivElement | null>(null);
|
|
25
|
+
|
|
26
|
+
const setNodeRef = useCallback((node: HTMLDivElement | null) => {
|
|
27
|
+
nodeRef.current = node;
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const node = nodeRef.current;
|
|
32
|
+
if (!node) return;
|
|
33
|
+
|
|
34
|
+
return registerVirtualComponent(node, (nextIsVisible) => {
|
|
35
|
+
setIsVisible((currentIsVisible) =>
|
|
36
|
+
currentIsVisible === nextIsVisible ? currentIsVisible : nextIsVisible
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}, [registerVirtualComponent]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<VirtualComponent ref={setNodeRef} isVisible={isVisible}>
|
|
43
|
+
{children}
|
|
44
|
+
</VirtualComponent>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export const VirtualComponentList = ({ children, sx }: VirtualListProps) => {
|
|
50
|
+
const stackSx = useMemo(() => [
|
|
51
|
+
{
|
|
52
|
+
overflowY: 'auto',
|
|
53
|
+
height: '100%',
|
|
54
|
+
justifyContent: 'flex-start',
|
|
55
|
+
alignItems: 'stretch',
|
|
56
|
+
},
|
|
57
|
+
...(Array.isArray(sx) ? sx : [sx]),
|
|
58
|
+
], [sx]);
|
|
59
|
+
|
|
60
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
const intersectionObserver = useRef<IntersectionObserver | null>(null);
|
|
62
|
+
const visibilityListeners = useRef(new Map<Element, VisibilityListener>());
|
|
63
|
+
|
|
64
|
+
const registerVirtualComponent = useCallback<RegisterVirtualComponent>(
|
|
65
|
+
(node, onVisibilityChange) => {
|
|
66
|
+
visibilityListeners.current.set(node, onVisibilityChange);
|
|
67
|
+
intersectionObserver.current?.observe(node);
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
intersectionObserver.current?.unobserve(node);
|
|
71
|
+
visibilityListeners.current.delete(node);
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
[]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
const currentVisibilityListeners = visibilityListeners.current;
|
|
79
|
+
const observer = new IntersectionObserver(
|
|
80
|
+
(entries) => {
|
|
81
|
+
entries.forEach((entry) => {
|
|
82
|
+
currentVisibilityListeners.get(entry.target)?.(entry.isIntersecting);
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
root: containerRef.current,
|
|
87
|
+
rootMargin: '100% 0px',
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
intersectionObserver.current = observer;
|
|
92
|
+
currentVisibilityListeners.forEach((_, node) => {
|
|
93
|
+
observer.observe(node);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
observer.disconnect();
|
|
98
|
+
intersectionObserver.current = null;
|
|
99
|
+
currentVisibilityListeners.clear();
|
|
100
|
+
};
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
return (<Stack id='virtual-component-list' sx={stackSx} direction="column" ref={containerRef}>
|
|
104
|
+
{React.Children.map(children, (child) => {
|
|
105
|
+
return (
|
|
106
|
+
<VirtualComponentListItem registerVirtualComponent={registerVirtualComponent}>
|
|
107
|
+
{child}
|
|
108
|
+
</VirtualComponentListItem>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</Stack>)
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './VirtualComponentList';
|