#
tokens: 47656/50000 13/473 files (page 7/10)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 7 of 10. Use http://codebase.md/push-based/angular-toolkit-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .aiignore
├── .cursor
│   ├── flows
│   │   ├── component-refactoring
│   │   │   ├── 01-review-component.mdc
│   │   │   ├── 02-refactor-component.mdc
│   │   │   ├── 03-validate-component.mdc
│   │   │   └── angular-20.md
│   │   ├── ds-refactoring-flow
│   │   │   ├── 01-find-violations.mdc
│   │   │   ├── 01b-find-all-violations.mdc
│   │   │   ├── 02-plan-refactoring.mdc
│   │   │   ├── 02b-plan-refactoring-for-all-violations.mdc
│   │   │   ├── 03-fix-violations.mdc
│   │   │   ├── 03-non-viable-cases.mdc
│   │   │   ├── 04-validate-changes.mdc
│   │   │   ├── 05-prepare-report.mdc
│   │   │   └── clean-global-styles.mdc
│   │   └── README.md
│   └── mcp.json.example
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── assets
│   ├── entain-logo.png
│   └── entain.png
├── CONTRIBUTING.MD
├── docs
│   ├── architecture-internal-design.md
│   ├── component-refactoring-flow.md
│   ├── contracts.md
│   ├── ds-refactoring-flow.md
│   ├── getting-started.md
│   ├── README.md
│   ├── tools.md
│   └── writing-custom-tools.md
├── eslint.config.mjs
├── jest.config.ts
├── jest.preset.mjs
├── LICENSE
├── nx.json
├── package-lock.json
├── package.json
├── packages
│   ├── .gitkeep
│   ├── angular-mcp
│   │   ├── eslint.config.mjs
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── assets
│   │   │   │   └── .gitkeep
│   │   │   └── main.ts
│   │   ├── tsconfig.app.json
│   │   ├── tsconfig.json
│   │   ├── vitest.config.mts
│   │   └── webpack.config.cjs
│   ├── angular-mcp-server
│   │   ├── eslint.config.mjs
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   └── lib
│   │   │       ├── angular-mcp-server.ts
│   │   │       ├── prompts
│   │   │       │   └── prompt-registry.ts
│   │   │       ├── tools
│   │   │       │   ├── ds
│   │   │       │   │   ├── component
│   │   │       │   │   │   ├── get-deprecated-css-classes.tool.ts
│   │   │       │   │   │   ├── get-ds-component-data.tool.ts
│   │   │       │   │   │   ├── list-ds-components.tool.ts
│   │   │       │   │   │   └── utils
│   │   │       │   │   │       ├── deprecated-css-helpers.ts
│   │   │       │   │   │       ├── doc-helpers.ts
│   │   │       │   │   │       ├── metadata-helpers.ts
│   │   │       │   │   │       └── paths-helpers.ts
│   │   │       │   │   ├── component-contract
│   │   │       │   │   │   ├── builder
│   │   │       │   │   │   │   ├── build-component-contract.tool.ts
│   │   │       │   │   │   │   ├── models
│   │   │       │   │   │   │   │   ├── schema.ts
│   │   │       │   │   │   │   │   └── types.ts
│   │   │       │   │   │   │   ├── spec
│   │   │       │   │   │   │   │   ├── css-match.spec.ts
│   │   │       │   │   │   │   │   ├── dom-slots.extractor.spec.ts
│   │   │       │   │   │   │   │   ├── element-helpers.spec.ts
│   │   │       │   │   │   │   │   ├── inline-styles.collector.spec.ts
│   │   │       │   │   │   │   │   ├── meta.generator.spec.ts
│   │   │       │   │   │   │   │   ├── public-api.extractor.spec.ts
│   │   │       │   │   │   │   │   ├── styles.collector.spec.ts
│   │   │       │   │   │   │   │   └── typescript-analyzer.spec.ts
│   │   │       │   │   │   │   └── utils
│   │   │       │   │   │   │       ├── build-contract.ts
│   │   │       │   │   │   │       ├── css-match.ts
│   │   │       │   │   │   │       ├── dom-slots.extractor.ts
│   │   │       │   │   │   │       ├── element-helpers.ts
│   │   │       │   │   │   │       ├── inline-styles.collector.ts
│   │   │       │   │   │   │       ├── meta.generator.ts
│   │   │       │   │   │   │       ├── public-api.extractor.ts
│   │   │       │   │   │   │       ├── styles.collector.ts
│   │   │       │   │   │   │       └── typescript-analyzer.ts
│   │   │       │   │   │   ├── diff
│   │   │       │   │   │   │   ├── diff-component-contract.tool.ts
│   │   │       │   │   │   │   ├── models
│   │   │       │   │   │   │   │   └── schema.ts
│   │   │       │   │   │   │   ├── spec
│   │   │       │   │   │   │   │   ├── diff-utils.spec.ts
│   │   │       │   │   │   │   │   └── dom-path-utils.spec.ts
│   │   │       │   │   │   │   └── utils
│   │   │       │   │   │   │       ├── diff-utils.ts
│   │   │       │   │   │   │       └── dom-path-utils.ts
│   │   │       │   │   │   ├── index.ts
│   │   │       │   │   │   ├── list
│   │   │       │   │   │   │   ├── list-component-contracts.tool.ts
│   │   │       │   │   │   │   ├── models
│   │   │       │   │   │   │   │   ├── schema.ts
│   │   │       │   │   │   │   │   └── types.ts
│   │   │       │   │   │   │   ├── spec
│   │   │       │   │   │   │   │   └── contract-list-utils.spec.ts
│   │   │       │   │   │   │   └── utils
│   │   │       │   │   │   │       └── contract-list-utils.ts
│   │   │       │   │   │   └── shared
│   │   │       │   │   │       ├── models
│   │   │       │   │   │       │   └── types.ts
│   │   │       │   │   │       ├── spec
│   │   │       │   │   │       │   └── contract-file-ops.spec.ts
│   │   │       │   │   │       └── utils
│   │   │       │   │   │           └── contract-file-ops.ts
│   │   │       │   │   ├── component-usage-graph
│   │   │       │   │   │   ├── build-component-usage-graph.tool.ts
│   │   │       │   │   │   ├── index.ts
│   │   │       │   │   │   ├── models
│   │   │       │   │   │   │   ├── config.ts
│   │   │       │   │   │   │   ├── schema.ts
│   │   │       │   │   │   │   └── types.ts
│   │   │       │   │   │   └── utils
│   │   │       │   │   │       ├── angular-parser.ts
│   │   │       │   │   │       ├── component-helpers.ts
│   │   │       │   │   │       ├── component-usage-graph-builder.ts
│   │   │       │   │   │       ├── path-resolver.ts
│   │   │       │   │   │       └── unified-ast-analyzer.ts
│   │   │       │   │   ├── ds.tools.ts
│   │   │       │   │   ├── project
│   │   │       │   │   │   ├── get-project-dependencies.tool.ts
│   │   │       │   │   │   ├── report-deprecated-css.tool.ts
│   │   │       │   │   │   └── utils
│   │   │       │   │   │       ├── dependencies-helpers.ts
│   │   │       │   │   │       └── styles-report-helpers.ts
│   │   │       │   │   ├── report-violations
│   │   │       │   │   │   ├── index.ts
│   │   │       │   │   │   ├── models
│   │   │       │   │   │   │   ├── schema.ts
│   │   │       │   │   │   │   └── types.ts
│   │   │       │   │   │   ├── report-all-violations.tool.ts
│   │   │       │   │   │   └── report-violations.tool.ts
│   │   │       │   │   ├── shared
│   │   │       │   │   │   ├── index.ts
│   │   │       │   │   │   ├── models
│   │   │       │   │   │   │   ├── input-schemas.model.ts
│   │   │       │   │   │   │   └── schema-helpers.ts
│   │   │       │   │   │   ├── utils
│   │   │       │   │   │   │   ├── component-validation.ts
│   │   │       │   │   │   │   ├── cross-platform-path.ts
│   │   │       │   │   │   │   ├── handler-helpers.ts
│   │   │       │   │   │   │   ├── output.utils.ts
│   │   │       │   │   │   │   └── regex-helpers.ts
│   │   │       │   │   │   └── violation-analysis
│   │   │       │   │   │       ├── base-analyzer.ts
│   │   │       │   │   │       ├── coverage-analyzer.ts
│   │   │       │   │   │       ├── formatters.ts
│   │   │       │   │   │       ├── index.ts
│   │   │       │   │   │       └── types.ts
│   │   │       │   │   └── tools.ts
│   │   │       │   ├── schema.ts
│   │   │       │   ├── tools.ts
│   │   │       │   ├── types.ts
│   │   │       │   └── utils.ts
│   │   │       └── validation
│   │   │           ├── angular-mcp-server-options.schema.ts
│   │   │           ├── ds-components-file-loader.validation.ts
│   │   │           ├── ds-components-file.validation.ts
│   │   │           ├── ds-components.schema.ts
│   │   │           └── file-existence.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.lib.json
│   │   ├── tsconfig.tsbuildinfo
│   │   └── vitest.config.mts
│   ├── minimal-repo
│   │   └── packages
│   │       ├── application
│   │       │   ├── angular.json
│   │       │   ├── code-pushup.config.ts
│   │       │   ├── src
│   │       │   │   ├── app
│   │       │   │   │   ├── app.component.ts
│   │       │   │   │   ├── app.config.ts
│   │       │   │   │   ├── app.routes.ts
│   │       │   │   │   ├── components
│   │       │   │   │   │   ├── refactoring-tests
│   │       │   │   │   │   │   ├── bad-alert-tooltip-input.component.ts
│   │       │   │   │   │   │   ├── bad-alert.component.ts
│   │       │   │   │   │   │   ├── bad-button-dropdown.component.ts
│   │       │   │   │   │   │   ├── bad-document.component.ts
│   │       │   │   │   │   │   ├── bad-global-this.component.ts
│   │       │   │   │   │   │   ├── bad-mixed-external-assets.component.css
│   │       │   │   │   │   │   ├── bad-mixed-external-assets.component.html
│   │       │   │   │   │   │   ├── bad-mixed-external-assets.component.ts
│   │       │   │   │   │   │   ├── bad-mixed-not-standalone.component.ts
│   │       │   │   │   │   │   ├── bad-mixed.component.ts
│   │       │   │   │   │   │   ├── bad-mixed.module.ts
│   │       │   │   │   │   │   ├── bad-modal-progress.component.ts
│   │       │   │   │   │   │   ├── bad-this-window-document.component.ts
│   │       │   │   │   │   │   ├── bad-window.component.ts
│   │       │   │   │   │   │   ├── complex-components
│   │       │   │   │   │   │   │   ├── first-case
│   │       │   │   │   │   │   │   │   ├── dashboard-demo.component.html
│   │       │   │   │   │   │   │   │   ├── dashboard-demo.component.scss
│   │       │   │   │   │   │   │   │   ├── dashboard-demo.component.ts
│   │       │   │   │   │   │   │   │   ├── dashboard-header.component.html
│   │       │   │   │   │   │   │   │   ├── dashboard-header.component.scss
│   │       │   │   │   │   │   │   │   └── dashboard-header.component.ts
│   │       │   │   │   │   │   │   ├── second-case
│   │       │   │   │   │   │   │   │   ├── complex-badge-widget.component.scss
│   │       │   │   │   │   │   │   │   ├── complex-badge-widget.component.ts
│   │       │   │   │   │   │   │   │   └── complex-widget-demo.component.ts
│   │       │   │   │   │   │   │   └── third-case
│   │       │   │   │   │   │   │       ├── product-card.component.scss
│   │       │   │   │   │   │   │       ├── product-card.component.ts
│   │       │   │   │   │   │   │       └── product-showcase.component.ts
│   │       │   │   │   │   │   ├── group-1
│   │       │   │   │   │   │   │   ├── bad-mixed-1.component.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-1.module.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-1.component.css
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-1.component.html
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-1.component.ts
│   │       │   │   │   │   │   │   └── bad-mixed-not-standalone-1.component.ts
│   │       │   │   │   │   │   ├── group-2
│   │       │   │   │   │   │   │   ├── bad-mixed-2.component.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-2.module.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-2.component.css
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-2.component.html
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-2.component.ts
│   │       │   │   │   │   │   │   └── bad-mixed-not-standalone-2.component.ts
│   │       │   │   │   │   │   ├── group-3
│   │       │   │   │   │   │   │   ├── bad-mixed-3.component.spec.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-3.component.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-3.module.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-3.component.css
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-3.component.html
│   │       │   │   │   │   │   │   ├── bad-mixed-external-assets-3.component.ts
│   │       │   │   │   │   │   │   ├── bad-mixed-not-standalone-3.component.ts
│   │       │   │   │   │   │   │   └── lazy-loader-3.component.ts
│   │       │   │   │   │   │   └── group-4
│   │       │   │   │   │   │       ├── multi-violation-test.component.html
│   │       │   │   │   │   │       ├── multi-violation-test.component.scss
│   │       │   │   │   │   │       └── multi-violation-test.component.ts
│   │       │   │   │   │   └── validation-tests
│   │       │   │   │   │       ├── circular-dependency.component.ts
│   │       │   │   │   │       ├── external-files-missing.component.ts
│   │       │   │   │   │       ├── invalid-lifecycle.component.ts
│   │       │   │   │   │       ├── invalid-pipe-usage.component.ts
│   │       │   │   │   │       ├── invalid-template-syntax.component.ts
│   │       │   │   │   │       ├── missing-imports.component.ts
│   │       │   │   │   │       ├── missing-method.component.ts
│   │       │   │   │   │       ├── README.md
│   │       │   │   │   │       ├── standalone-module-conflict.component.ts
│   │       │   │   │   │       ├── standalone-module-conflict.module.ts
│   │       │   │   │   │       ├── template-reference-error.component.ts
│   │       │   │   │   │       ├── type-mismatch.component.ts
│   │       │   │   │   │       ├── valid.component.ts
│   │       │   │   │   │       ├── wrong-decorator-usage.component.ts
│   │       │   │   │   │       └── wrong-property-binding.component.ts
│   │       │   │   │   └── styles
│   │       │   │   │       ├── bad-global-styles.scss
│   │       │   │   │       ├── base
│   │       │   │   │       │   ├── _reset.scss
│   │       │   │   │       │   └── base.scss
│   │       │   │   │       ├── components
│   │       │   │   │       │   └── components.scss
│   │       │   │   │       ├── extended-deprecated-styles.scss
│   │       │   │   │       ├── layout
│   │       │   │   │       │   └── layout.scss
│   │       │   │   │       ├── new-styles-1.scss
│   │       │   │   │       ├── new-styles-10.scss
│   │       │   │   │       ├── new-styles-2.scss
│   │       │   │   │       ├── new-styles-3.scss
│   │       │   │   │       ├── new-styles-4.scss
│   │       │   │   │       ├── new-styles-5.scss
│   │       │   │   │       ├── new-styles-6.scss
│   │       │   │   │       ├── new-styles-7.scss
│   │       │   │   │       ├── new-styles-8.scss
│   │       │   │   │       ├── new-styles-9.scss
│   │       │   │   │       ├── themes
│   │       │   │   │       │   └── themes.scss
│   │       │   │   │       └── utilities
│   │       │   │   │           └── utilities.scss
│   │       │   │   ├── index.html
│   │       │   │   ├── main.ts
│   │       │   │   └── styles.css
│   │       │   ├── tsconfig.app.json
│   │       │   ├── tsconfig.json
│   │       │   └── tsconfig.spec.json
│   │       └── design-system
│   │           ├── component-options.mjs
│   │           ├── storybook
│   │           │   └── card
│   │           │       └── card-tabs
│   │           │           └── overview.mdx
│   │           ├── storybook-host-app
│   │           │   └── src
│   │           │       └── components
│   │           │           ├── badge
│   │           │           │   ├── badge-tabs
│   │           │           │   │   ├── api.mdx
│   │           │           │   │   ├── examples.mdx
│   │           │           │   │   └── overview.mdx
│   │           │           │   ├── badge.component.mdx
│   │           │           │   └── badge.component.stories.ts
│   │           │           ├── modal
│   │           │           │   ├── demo-cdk-dialog-cmp.component.ts
│   │           │           │   ├── demo-modal-cmp.component.ts
│   │           │           │   ├── modal-tabs
│   │           │           │   │   ├── api.mdx
│   │           │           │   │   ├── examples.mdx
│   │           │           │   │   └── overview.mdx
│   │           │           │   ├── modal.component.mdx
│   │           │           │   └── modal.component.stories.ts
│   │           │           └── segmented-control
│   │           │               ├── segmented-control-tabs
│   │           │               │   ├── api.mdx
│   │           │               │   ├── examples.mdx
│   │           │               │   └── overview.mdx
│   │           │               ├── segmented-control.component.mdx
│   │           │               └── segmented-control.component.stories.ts
│   │           └── ui
│   │               ├── badge
│   │               │   ├── package.json
│   │               │   ├── project.json
│   │               │   └── src
│   │               │       └── badge.component.ts
│   │               ├── modal
│   │               │   ├── package.json
│   │               │   ├── project.json
│   │               │   └── src
│   │               │       ├── modal-content.component.ts
│   │               │       ├── modal-header
│   │               │       │   └── modal-header.component.ts
│   │               │       ├── modal-header-drag
│   │               │       │   └── modal-header-drag.component.ts
│   │               │       └── modal.component.ts
│   │               ├── rx-host-listener
│   │               │   ├── package.json
│   │               │   ├── project.json
│   │               │   └── src
│   │               │       └── rx-host-listener.ts
│   │               └── segmented-control
│   │                   ├── package.json
│   │                   ├── project.json
│   │                   └── src
│   │                       ├── segmented-control.component.html
│   │                       ├── segmented-control.component.ts
│   │                       ├── segmented-control.token.ts
│   │                       └── segmented-option.component.ts
│   └── shared
│       ├── angular-ast-utils
│       │   ├── .spec.swcrc
│       │   ├── ai
│       │   │   ├── API.md
│       │   │   ├── EXAMPLES.md
│       │   │   └── FUNCTIONS.md
│       │   ├── docs
│       │   │   └── angular-component-tree.md
│       │   ├── eslint.config.mjs
│       │   ├── jest.config.ts
│       │   ├── package.json
│       │   ├── README.md
│       │   ├── src
│       │   │   ├── index.ts
│       │   │   └── lib
│       │   │       ├── constants.ts
│       │   │       ├── decorator-config.visitor.inline-styles.spec.ts
│       │   │       ├── decorator-config.visitor.spec.ts
│       │   │       ├── decorator-config.visitor.ts
│       │   │       ├── parse-component.ts
│       │   │       ├── schema.ts
│       │   │       ├── styles
│       │   │       │   └── utils.ts
│       │   │       ├── template
│       │   │       │   ├── noop-tmpl-visitor.ts
│       │   │       │   ├── template.walk.ts
│       │   │       │   ├── utils.spec.ts
│       │   │       │   ├── utils.ts
│       │   │       │   └── utils.unit.test.ts
│       │   │       ├── ts.walk.ts
│       │   │       ├── types.ts
│       │   │       └── utils.ts
│       │   ├── tsconfig.json
│       │   ├── tsconfig.lib.json
│       │   ├── tsconfig.spec.json
│       │   └── vitest.config.mts
│       ├── DEPENDENCIES.md
│       ├── ds-component-coverage
│       │   ├── .spec.swcrc
│       │   ├── ai
│       │   │   ├── API.md
│       │   │   ├── EXAMPLES.md
│       │   │   └── FUNCTIONS.md
│       │   ├── docs
│       │   │   ├── examples
│       │   │   │   ├── report.json
│       │   │   │   └── report.md
│       │   │   ├── images
│       │   │   │   └── report-overview.png
│       │   │   └── README.md
│       │   ├── jest.config.ts
│       │   ├── mocks
│       │   │   └── fixtures
│       │   │       └── e2e
│       │   │           ├── asset-location
│       │   │           │   ├── code-pushup.config.ts
│       │   │           │   ├── inl-styl-inl-tmpl
│       │   │           │   │   └── inl-styl-inl-tmpl.component.ts
│       │   │           │   ├── inl-styl-url-tmpl
│       │   │           │   │   ├── inl-styl-url-tmpl.component.html
│       │   │           │   │   └── inl-styl-url-tmpl.component.ts
│       │   │           │   ├── multi-url-styl-inl-tmpl
│       │   │           │   │   ├── multi-url-styl-inl-tmpl-1.component.css
│       │   │           │   │   ├── multi-url-styl-inl-tmpl-2.component.css
│       │   │           │   │   └── multi-url-styl-inl-tmpl.component.ts
│       │   │           │   ├── url-styl-inl-tmpl
│       │   │           │   │   ├── url-styl-inl-tmpl.component.css
│       │   │           │   │   └── url-styl-inl-tmpl.component.ts
│       │   │           │   ├── url-styl-single-inl-tmpl
│       │   │           │   │   ├── url-styl-inl-tmpl.component.ts
│       │   │           │   │   └── url-styl-single-inl-tmpl.component.css
│       │   │           │   └── url-styl-url-tmpl
│       │   │           │       ├── inl-styl-url-tmpl.component.css
│       │   │           │       ├── inl-styl-url-tmpl.component.html
│       │   │           │       └── inl-styl-url-tmpl.component.ts
│       │   │           ├── demo
│       │   │           │   ├── code-pushup.config.ts
│       │   │           │   ├── prompt.md
│       │   │           │   └── src
│       │   │           │       ├── bad-button-dropdown.component.ts
│       │   │           │       ├── bad-modal-progress.component.ts
│       │   │           │       ├── mixed-external-assets.component.css
│       │   │           │       ├── mixed-external-assets.component.html
│       │   │           │       ├── mixed-external-assets.component.ts
│       │   │           │       └── sub-folder-1
│       │   │           │           ├── bad-alert.component.ts
│       │   │           │           ├── button.component.ts
│       │   │           │           └── sub-folder-2
│       │   │           │               ├── bad-alert-tooltip-input.component.ts
│       │   │           │               └── bad-mixed.component.ts
│       │   │           ├── line-number
│       │   │           │   ├── code-pushup.config.ts
│       │   │           │   ├── inl-styl-single.component.ts
│       │   │           │   ├── inl-styl-span.component.ts
│       │   │           │   ├── inl-tmpl-single.component.ts
│       │   │           │   ├── inl-tmpl-span.component.ts
│       │   │           │   ├── url-style
│       │   │           │   │   ├── url-styl-single.component.css
│       │   │           │   │   ├── url-styl-single.component.ts
│       │   │           │   │   ├── url-styl-span.component.css
│       │   │           │   │   └── url-styl-span.component.ts
│       │   │           │   └── url-tmpl
│       │   │           │       ├── url-tmpl-single.component.html
│       │   │           │       ├── url-tmpl-single.component.ts
│       │   │           │       ├── url-tmpl-span.component.html
│       │   │           │       └── url-tmpl-span.component.ts
│       │   │           ├── style-format
│       │   │           │   ├── code-pushup.config.ts
│       │   │           │   ├── inl-css.component.ts
│       │   │           │   ├── inl-scss.component.ts
│       │   │           │   ├── styles.css
│       │   │           │   ├── styles.scss
│       │   │           │   ├── url-css.component.ts
│       │   │           │   └── url-scss.component.ts
│       │   │           └── template-syntax
│       │   │               ├── class-attribute.component.ts
│       │   │               ├── class-binding.component.ts
│       │   │               ├── code-pushup.config.ts
│       │   │               └── ng-class-binding.component.ts
│       │   ├── package.json
│       │   ├── src
│       │   │   ├── core.config.ts
│       │   │   ├── index.ts
│       │   │   └── lib
│       │   │       ├── constants.ts
│       │   │       ├── ds-component-coverage.plugin.ts
│       │   │       ├── runner
│       │   │       │   ├── audits
│       │   │       │   │   └── ds-coverage
│       │   │       │   │       ├── class-definition.utils.ts
│       │   │       │   │       ├── class-definition.visitor.ts
│       │   │       │   │       ├── class-definition.visitor.unit.test.ts
│       │   │       │   │       ├── class-usage.utils.ts
│       │   │       │   │       ├── class-usage.visitor.spec.ts
│       │   │       │   │       ├── class-usage.visitor.ts
│       │   │       │   │       ├── constants.ts
│       │   │       │   │       ├── ds-coverage.audit.ts
│       │   │       │   │       ├── schema.ts
│       │   │       │   │       └── utils.ts
│       │   │       │   ├── create-runner.ts
│       │   │       │   └── schema.ts
│       │   │       └── utils.ts
│       │   ├── tsconfig.json
│       │   ├── tsconfig.lib.json
│       │   ├── tsconfig.spec.json
│       │   └── vitest.config.mts
│       ├── LLMS.md
│       ├── models
│       │   ├── ai
│       │   │   ├── API.md
│       │   │   ├── EXAMPLES.md
│       │   │   └── FUNCTIONS.md
│       │   ├── package.json
│       │   ├── README.md
│       │   ├── src
│       │   │   ├── index.ts
│       │   │   └── lib
│       │   │       ├── cli.ts
│       │   │       ├── diagnostics.ts
│       │   │       └── mcp.ts
│       │   ├── tsconfig.json
│       │   └── tsconfig.lib.json
│       ├── styles-ast-utils
│       │   ├── .spec.swcrc
│       │   ├── ai
│       │   │   ├── API.md
│       │   │   ├── EXAMPLES.md
│       │   │   └── FUNCTIONS.md
│       │   ├── jest.config.ts
│       │   ├── package.json
│       │   ├── README.md
│       │   ├── src
│       │   │   ├── index.ts
│       │   │   └── lib
│       │   │       ├── postcss-safe-parser.d.ts
│       │   │       ├── styles-ast-utils.spec.ts
│       │   │       ├── styles-ast-utils.ts
│       │   │       ├── stylesheet.parse.ts
│       │   │       ├── stylesheet.parse.unit.test.ts
│       │   │       ├── stylesheet.visitor.ts
│       │   │       ├── stylesheet.walk.ts
│       │   │       ├── types.ts
│       │   │       ├── utils.ts
│       │   │       └── utils.unit.test.ts
│       │   ├── tsconfig.json
│       │   ├── tsconfig.lib.json
│       │   ├── tsconfig.spec.json
│       │   └── vitest.config.mts
│       ├── typescript-ast-utils
│       │   ├── .spec.swcrc
│       │   ├── ai
│       │   │   ├── API.md
│       │   │   ├── EXAMPLES.md
│       │   │   └── FUNCTIONS.md
│       │   ├── jest.config.ts
│       │   ├── package.json
│       │   ├── README.md
│       │   ├── src
│       │   │   ├── index.ts
│       │   │   └── lib
│       │   │       ├── constants.ts
│       │   │       └── utils.ts
│       │   ├── tsconfig.json
│       │   ├── tsconfig.lib.json
│       │   ├── tsconfig.spec.json
│       │   └── vitest.config.mts
│       └── utils
│           ├── .spec.swcrc
│           ├── ai
│           │   ├── API.md
│           │   ├── EXAMPLES.md
│           │   └── FUNCTIONS.md
│           ├── package.json
│           ├── README.md
│           ├── src
│           │   ├── index.ts
│           │   └── lib
│           │       ├── execute-process.ts
│           │       ├── execute-process.unit.test.ts
│           │       ├── file
│           │       │   ├── default-export-loader.spec.ts
│           │       │   ├── default-export-loader.ts
│           │       │   ├── file.resolver.ts
│           │       │   └── find-in-file.ts
│           │       ├── format-command-log.integration.test.ts
│           │       ├── format-command-log.ts
│           │       ├── logging.ts
│           │       └── utils.ts
│           ├── tsconfig.json
│           ├── tsconfig.lib.json
│           ├── tsconfig.spec.json
│           ├── vite.config.ts
│           └── vitest.config.mts
├── README.md
├── testing
│   ├── setup
│   │   ├── eslint.config.mjs
│   │   ├── eslint.next.config.mjs
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.d.ts
│   │   │   ├── index.mjs
│   │   │   └── memfs.constants.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.lib.json
│   │   ├── tsconfig.spec.json
│   │   ├── vitest.config.mts
│   │   └── vitest.integration.config.mts
│   ├── utils
│   │   ├── eslint.config.mjs
│   │   ├── eslint.next.config.mjs
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   └── lib
│   │   │       ├── constants.ts
│   │   │       ├── e2e-setup.ts
│   │   │       ├── execute-process-helper.mock.ts
│   │   │       ├── execute-process.mock.mjs
│   │   │       ├── os-agnostic-paths.ts
│   │   │       ├── os-agnostic-paths.unit.test.ts
│   │   │       ├── source-file-from.code.ts
│   │   │       └── string.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.lib.json
│   │   ├── tsconfig.spec.json
│   │   ├── vite.config.ts
│   │   ├── vitest.config.mts
│   │   └── vitest.integration.config.mts
│   └── vitest-setup
│       ├── eslint.config.mjs
│       ├── eslint.next.config.mjs
│       ├── package.json
│       ├── README.md
│       ├── src
│       │   ├── index.ts
│       │   └── lib
│       │       ├── configuration.ts
│       │       └── fs-memfs.setup-file.ts
│       ├── tsconfig.json
│       ├── tsconfig.lib.json
│       ├── tsconfig.spec.json
│       ├── vite.config.ts
│       ├── vitest.config.mts
│       └── vitest.integration.config.mts
├── tools
│   ├── nx-advanced-profile.bin.js
│   ├── nx-advanced-profile.js
│   ├── nx-advanced-profile.postinstall.js
│   └── perf_hooks.patch.js
├── tsconfig.base.json
├── tsconfig.json
└── vitest.workspace.ts
```

# Files

--------------------------------------------------------------------------------
/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-usage.visitor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type {
  2 |   ASTWithSource,
  3 |   TmplAstBoundAttribute,
  4 |   TmplAstBoundEvent,
  5 |   TmplAstBoundText,
  6 |   TmplAstContent,
  7 |   TmplAstDeferredBlock,
  8 |   TmplAstDeferredBlockError,
  9 |   TmplAstDeferredBlockLoading,
 10 |   TmplAstDeferredBlockPlaceholder,
 11 |   TmplAstDeferredTrigger,
 12 |   TmplAstElement,
 13 |   TmplAstForLoopBlock,
 14 |   TmplAstForLoopBlockEmpty,
 15 |   TmplAstIcu,
 16 |   TmplAstIfBlock,
 17 |   TmplAstIfBlockBranch,
 18 |   TmplAstLetDeclaration,
 19 |   TmplAstReference,
 20 |   TmplAstSwitchBlock,
 21 |   TmplAstSwitchBlockCase,
 22 |   TmplAstTemplate,
 23 |   TmplAstText,
 24 |   TmplAstTextAttribute,
 25 |   TmplAstUnknownBlock,
 26 |   TmplAstVariable,
 27 |   TmplAstVisitor,
 28 | } from '@angular/compiler' with { 'resolution-mode': 'import' };
 29 | import { Issue } from '@code-pushup/models';
 30 | import { DiagnosticsAware } from '@push-based/models';
 31 | 
 32 | import {
 33 |   tmplAstElementToSource,
 34 |   parseClassNames,
 35 |   extractClassNamesFromNgClassAST,
 36 | } from '@push-based/angular-ast-utils';
 37 | 
 38 | import {
 39 |   EXTERNAL_ASSET_ICON,
 40 |   INLINE_ASSET_ICON,
 41 |   TEMPLATE_ASSET_ICON,
 42 | } from './constants.js';
 43 | 
 44 | import { ComponentReplacement } from './schema.js';
 45 | 
 46 | function generateClassUsageMessage({
 47 |   element,
 48 |   className,
 49 |   attribute,
 50 |   componentName = 'a DS component',
 51 |   docsUrl,
 52 | }: {
 53 |   element: TmplAstElement;
 54 |   className: string;
 55 |   attribute: string;
 56 | } & Pick<ComponentReplacement, 'docsUrl' | 'componentName'>): string {
 57 |   const elementName = element.name;
 58 |   const isInline = element.sourceSpan.start.file.url.match(/\.ts$/) != null;
 59 |   const iconString = `${
 60 |     isInline ? INLINE_ASSET_ICON : EXTERNAL_ASSET_ICON
 61 |   }${TEMPLATE_ASSET_ICON} `;
 62 |   const docsLink = docsUrl
 63 |     ? ` <a href="${docsUrl}" target="_blank">Learn more</a>.`
 64 |     : '';
 65 |   return `${iconString} Element <code>${elementName}</code> in attribute <code>${attribute}</code> uses deprecated class <code>${className}</code>. Use <code>${componentName}</code> instead.${docsLink}`;
 66 | }
 67 | 
 68 | export class ClassUsageVisitor
 69 |   implements TmplAstVisitor<void>, DiagnosticsAware
 70 | {
 71 |   private issues: Issue[] = [];
 72 |   private currentElement: TmplAstElement | null = null;
 73 | 
 74 |   constructor(
 75 |     private readonly componentReplacement: ComponentReplacement,
 76 |     private readonly startLine = 0,
 77 |   ) {}
 78 | 
 79 |   getIssues(): Issue[] {
 80 |     return this.issues;
 81 |   }
 82 | 
 83 |   clear(): void {
 84 |     this.issues = [];
 85 |   }
 86 | 
 87 |   visitElement(element: TmplAstElement): void {
 88 |     this.currentElement = element;
 89 | 
 90 |     element.attributes.forEach((attr) => attr.visit(this)); // Check `class="..."`
 91 |     element.inputs.forEach((input) => input.visit(this)); // Check `[class.foo]`, `[ngClass]`
 92 | 
 93 |     element.children.forEach((child) => child.visit(this));
 94 | 
 95 |     this.currentElement = null;
 96 |   }
 97 | 
 98 |   visitTextAttribute(attribute: TmplAstTextAttribute): void {
 99 |     const { deprecatedCssClasses, ...compRepl } = this.componentReplacement;
100 |     if (attribute.name === 'class' && this.currentElement) {
101 |       const classNames = parseClassNames(attribute.value);
102 |       const deprecatedClassesFound = classNames.filter((cn) =>
103 |         deprecatedCssClasses.includes(cn),
104 |       );
105 | 
106 |       if (deprecatedClassesFound.length > 0) {
107 |         const isInline =
108 |           attribute.sourceSpan.start.file.url.match(/\.ts$/) != null;
109 |         const startLine = isInline ? this.startLine : 0;
110 | 
111 |         this.issues.push({
112 |           severity: 'error',
113 |           message: generateClassUsageMessage({
114 |             ...compRepl,
115 |             element: this.currentElement,
116 |             className: deprecatedClassesFound.join(', '),
117 |             attribute: `${attribute.name}`,
118 |           }),
119 |           source: tmplAstElementToSource(this.currentElement, startLine),
120 |         });
121 |       }
122 |     }
123 |   }
124 | 
125 |   visitBoundAttribute(attribute: TmplAstBoundAttribute): void {
126 |     if (!this.currentElement) return;
127 | 
128 |     const { deprecatedCssClasses, ...compRepl } = this.componentReplacement;
129 | 
130 |     // Check `[class.foo]`
131 |     // BindingType.Class === 2
132 |     if (attribute.type === 2 && deprecatedCssClasses.includes(attribute.name)) {
133 |       this.issues.push({
134 |         severity: 'error', // @TODO if we consider transformations this needs to be dynamic
135 |         message: generateClassUsageMessage({
136 |           element: this.currentElement,
137 |           className: attribute.name,
138 |           attribute: '[class.*]',
139 |           componentName: this.componentReplacement.componentName,
140 |           docsUrl: this.componentReplacement.docsUrl,
141 |         }),
142 |         source: tmplAstElementToSource(this.currentElement, this.startLine),
143 |       });
144 |     }
145 | 
146 |     // Handle class="..." with interpolation and [ngClass]
147 |     if (attribute.name === 'class' || attribute.name === 'ngClass') {
148 |       const value: ASTWithSource = attribute.value as ASTWithSource;
149 | 
150 |       // Use AST-based parsing for both [class] and [ngClass] to avoid false positives
151 |       // For simple string literals, the AST parsing will still work correctly
152 |       const foundClassNames = extractClassNamesFromNgClassAST(
153 |         value.ast,
154 |         deprecatedCssClasses,
155 |       );
156 | 
157 |       // Create single issue for all found deprecated classes
158 |       if (foundClassNames.length > 0 && this.currentElement) {
159 |         this.issues.push({
160 |           severity: 'error', // @TODO if we consider transformations this needs to be dynamic
161 |           message: generateClassUsageMessage({
162 |             ...compRepl,
163 |             element: this.currentElement,
164 |             className: foundClassNames.join(', '),
165 |             attribute:
166 |               attribute.name === 'ngClass'
167 |                 ? `[${attribute.name}]`
168 |                 : attribute.name,
169 |           }),
170 |           source: tmplAstElementToSource(this.currentElement, this.startLine),
171 |         });
172 |       }
173 |     }
174 |   }
175 | 
176 |   visitTemplate(template: TmplAstTemplate): void {
177 |     template.children.forEach((child) => child.visit(this));
178 |   }
179 | 
180 |   visitContent(content: TmplAstContent): void {
181 |     content.children.forEach((child) => child.visit(this));
182 |   }
183 | 
184 |   visitForLoopBlock(block: TmplAstForLoopBlock): void {
185 |     block.children.forEach((child) => child.visit(this));
186 |     block.empty?.visit(this);
187 |   }
188 | 
189 |   visitForLoopBlockEmpty(block: TmplAstForLoopBlockEmpty): void {
190 |     block.children.forEach((child) => child.visit(this));
191 |   }
192 | 
193 |   visitIfBlock(block: TmplAstIfBlock): void {
194 |     block.branches.forEach((branch) => branch.visit(this));
195 |   }
196 | 
197 |   visitIfBlockBranch(block: TmplAstIfBlockBranch): void {
198 |     block.children.forEach((child) => child.visit(this));
199 |   }
200 | 
201 |   visitSwitchBlock(block: TmplAstSwitchBlock): void {
202 |     block.cases.forEach((caseBlock) => caseBlock.visit(this));
203 |   }
204 | 
205 |   visitSwitchBlockCase(block: TmplAstSwitchBlockCase): void {
206 |     block.children.forEach((child) => child.visit(this));
207 |   }
208 | 
209 |   visitDeferredBlock(deferred: TmplAstDeferredBlock): void {
210 |     deferred.visitAll(this);
211 |   }
212 | 
213 |   visitDeferredBlockError(block: TmplAstDeferredBlockError): void {
214 |     block.children.forEach((child) => child.visit(this));
215 |   }
216 | 
217 |   visitDeferredBlockLoading(block: TmplAstDeferredBlockLoading): void {
218 |     block.children.forEach((child) => child.visit(this));
219 |   }
220 | 
221 |   visitDeferredBlockPlaceholder(block: TmplAstDeferredBlockPlaceholder): void {
222 |     block.children.forEach((child) => child.visit(this));
223 |   }
224 | 
225 |   // -- No-op Methods --
226 |   /* eslint-disable @typescript-eslint/no-empty-function */
227 |   visitVariable(_variable: TmplAstVariable): void {}
228 | 
229 |   visitReference(_reference: TmplAstReference): void {}
230 | 
231 |   visitText(_text: TmplAstText): void {}
232 | 
233 |   visitBoundText(_text: TmplAstBoundText): void {}
234 | 
235 |   visitIcu(_icu: TmplAstIcu): void {}
236 | 
237 |   visitBoundEvent(_event: TmplAstBoundEvent): void {}
238 | 
239 |   visitUnknownBlock(_block: TmplAstUnknownBlock): void {}
240 | 
241 |   visitDeferredTrigger(_trigger: TmplAstDeferredTrigger): void {}
242 | 
243 |   visitLetDeclaration(_decl: TmplAstLetDeclaration): void {}
244 |   /* eslint-enable @typescript-eslint/no-empty-function */
245 | }
246 | 
```

--------------------------------------------------------------------------------
/packages/minimal-repo/packages/design-system/storybook-host-app/src/components/modal/demo-cdk-dialog-cmp.component.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   DIALOG_DATA,
  3 |   Dialog,
  4 |   DialogModule,
  5 |   DialogRef,
  6 | } from '@angular/cdk/dialog';
  7 | import {
  8 |   AfterViewInit,
  9 |   ChangeDetectionStrategy,
 10 |   Component,
 11 |   ElementRef,
 12 |   Inject,
 13 |   Renderer2,
 14 |   ViewChild,
 15 |   booleanAttribute,
 16 |   inject,
 17 |   input,
 18 | } from '@angular/core';
 19 | 
 20 | import { DemoCloseIconComponent } from '@design-system/storybook-demo-cmp-lib';
 21 | import { DsButton } from '@frontend/ui/button';
 22 | import { DsButtonIcon } from '@frontend/ui/button-icon';
 23 | import {
 24 |   DsModal,
 25 |   DsModalContent,
 26 |   DsModalHeader,
 27 |   DsModalHeaderDrag,
 28 |   DsModalHeaderVariant,
 29 |   DsModalVariant,
 30 | } from '@frontend/ui/modal';
 31 | 
 32 | @Component({
 33 |   selector: 'ds-demo-cdk-dialog-cmp',
 34 |   imports: [
 35 |     DialogModule,
 36 |     DsButton,
 37 |     DsModalHeader,
 38 |     DsButtonIcon,
 39 |     DemoCloseIconComponent,
 40 |     DsModal,
 41 |     DsModalContent,
 42 |     DsModalHeaderDrag,
 43 |   ],
 44 |   standalone: true,
 45 |   template: `
 46 |     <ds-modal
 47 |       [inverse]="data.inverse"
 48 |       [bottomSheet]="data.bottomSheet"
 49 |       [variant]="data.variant"
 50 |     >
 51 |       <ds-modal-header [variant]="data.headerVariant">
 52 |         <ds-modal-header-drag #dragHandle />
 53 |         <button slot="end" ds-button-icon size="small" (click)="close()">
 54 |           <ds-demo-close-icon />
 55 |         </button>
 56 |       </ds-modal-header>
 57 |       <!-- eslint-disable-next-line @angular-eslint/template/no-inline-styles -->
 58 |       <div style="height: 400px; width: 400px; overflow: auto">
 59 |         <ds-modal-content>
 60 |           Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam,
 61 |           ducimus, sequi! Ab consequatur earum expedita fugit illo illum in
 62 |           maiores nihil nostrum officiis ratione repellendus temporibus, vel!
 63 |           Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam,
 64 |           ducimus, sequi! Ab consequatur earum expedita fugit illo illum in
 65 |           maiores nihil nostrum officiis ratione repellendus temporibus, vel! Lo
 66 |           rem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam,
 67 |           ducimus, sequi! Ab consequatur earum expedita fugit illo illum in
 68 |           maiores nihil nostrum officiis ratione repellendus temporibus, vel!
 69 |           Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam,
 70 |           ducimus, sequi! Ab consequatur earum expedita fugit illo illum in
 71 |           maiores nihil nostrum officiis ratione repellendus temporibus, vel!
 72 |           Lorem ipsum Lorem ipsum dolor sit amet, consectetur adipisicing elit.
 73 |           Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit illo
 74 |           illum in maiores nihil nostrum officiis ratione repellendus
 75 |           temporibus, vel! dolor sit amet, consectetur adipisicing elit.
 76 |           Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit illo
 77 |           illum in maiores nihil nostrum officiis ratione repellendus
 78 |           temporibus, vel! Lorem ipsum dolor sit amet, consectetur adipisicing
 79 |           elit. Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit
 80 |           illo illum in maiores nihil nostrum officiis ratione repellendus
 81 |           temporibus, vel! Lorem ipsum dolor sit amet, consectetur adipisicing
 82 |           elit. Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit
 83 |           illo illum in maiores nihil nostrum officiis ratione repellendus
 84 |           temporibus, vel!
 85 |           <br />
 86 |           <br />
 87 |           <b>Lorem ipsum dolor sit amet</b>, consectetur adipisicing elit.
 88 |           Aliquam, ducimus, sequi! Ab consequatur earum expedita fugit illo
 89 |           illum in maiores nihil nostrum officiis ratione repellendus
 90 |           temporibus, vel!
 91 |           <br />
 92 |           <br />
 93 |           <div class="footer-buttons">
 94 |             <button
 95 |               ds-button
 96 |               [inverse]="data.inverse"
 97 |               kind="secondary"
 98 |               variant="outline"
 99 |               (click)="close()"
100 |             >
101 |               Outline Button
102 |             </button>
103 |             <button
104 |               ds-button
105 |               [inverse]="data.inverse"
106 |               kind="primary"
107 |               variant="filled"
108 |               (click)="close()"
109 |             >
110 |               Filled Button
111 |             </button>
112 |           </div>
113 |         </ds-modal-content>
114 |       </div>
115 |     </ds-modal>
116 |   `,
117 |   styles: [
118 |     `
119 |       ds-modal {
120 |         width: 400px;
121 |         min-height: 300px;
122 |       }
123 | 
124 |       /* Bottom Sheet styles */
125 |       :host-context(.ds-bottom-sheet-panel) ds-modal {
126 |         position: fixed;
127 |         bottom: 0;
128 |         left: 0;
129 |         right: 0;
130 |         margin-left: auto;
131 |         margin-right: auto;
132 |       }
133 | 
134 |       .footer-buttons {
135 |         display: grid;
136 |         grid-template-columns: 1fr 1fr;
137 |         gap: 10px;
138 |       }
139 |     `,
140 |   ],
141 |   changeDetection: ChangeDetectionStrategy.OnPush,
142 | })
143 | export class DemoCdkModalCmp implements AfterViewInit {
144 |   @ViewChild('dragHandle', { static: true, read: ElementRef })
145 |   dragHandle!: ElementRef<HTMLElement>;
146 |   @ViewChild(DsModal, { static: true, read: ElementRef })
147 |   modalElementRef!: ElementRef<HTMLElement>;
148 | 
149 |   private renderer = inject(Renderer2);
150 |   private isDragging = false;
151 |   private startX = 0;
152 |   private startY = 0;
153 |   private initialLeft = 0;
154 |   private initialTop = 0;
155 |   private moveListener?: () => void;
156 |   private upListener?: () => void;
157 | 
158 |   constructor(
159 |     private dialogRef: DialogRef,
160 |     @Inject(DIALOG_DATA)
161 |     public data: { headerVariant: string; inverse: boolean; variant: string },
162 |   ) {}
163 | 
164 |   ngAfterViewInit() {
165 |     if (this.dragHandle) {
166 |       this.renderer.listen(
167 |         this.dragHandle.nativeElement,
168 |         'mousedown',
169 |         (event: MouseEvent) => this.startDrag(event),
170 |       );
171 |     }
172 |   }
173 | 
174 |   startDrag(event: MouseEvent) {
175 |     event.preventDefault();
176 |     this.isDragging = true;
177 | 
178 |     const dialogEl = this.modalElementRef.nativeElement;
179 | 
180 |     const rect = dialogEl.getBoundingClientRect();
181 |     this.startX = event.clientX;
182 |     this.startY = event.clientY;
183 |     this.initialLeft = rect.left;
184 |     this.initialTop = rect.top;
185 | 
186 |     this.moveListener = this.renderer.listen('document', 'mousemove', (e) =>
187 |       this.onDragMove(e, dialogEl),
188 |     );
189 |     this.upListener = this.renderer.listen('document', 'mouseup', () =>
190 |       this.endDrag(),
191 |     );
192 |   }
193 | 
194 |   private onDragMove(event: MouseEvent, dialogEl: HTMLElement) {
195 |     if (!this.isDragging) return;
196 | 
197 |     const deltaX = event.clientX - this.startX;
198 |     const deltaY = event.clientY - this.startY;
199 | 
200 |     const left = this.initialLeft + deltaX;
201 |     const top = this.initialTop + deltaY;
202 | 
203 |     // Apply updated position
204 |     this.renderer.setStyle(dialogEl, 'position', 'fixed');
205 |     this.renderer.setStyle(dialogEl, 'left', `${left}px`);
206 |     this.renderer.setStyle(dialogEl, 'top', `${top}px`);
207 |     this.renderer.setStyle(dialogEl, 'margin', `0`);
208 |   }
209 | 
210 |   private endDrag() {
211 |     this.isDragging = false;
212 |     this.moveListener?.();
213 |     this.upListener?.();
214 |   }
215 | 
216 |   close() {
217 |     this.dialogRef.close();
218 |   }
219 | }
220 | 
221 | @Component({
222 |   selector: 'ds-demo-cdk-dialog-container',
223 |   imports: [DialogModule, DsButton],
224 |   standalone: true,
225 |   template: `
226 |     <button ds-button (click)="openDialog()">Open with CDK Dialog</button>
227 |   `,
228 |   changeDetection: ChangeDetectionStrategy.OnPush,
229 | })
230 | export class DemoCdkModalContainer {
231 |   dialog = inject(Dialog);
232 | 
233 |   headerVariant = input<DsModalHeaderVariant>();
234 |   inverse = input(false, { transform: booleanAttribute });
235 |   variant = input<DsModalVariant>();
236 |   bottomSheetInput = input(false, { transform: booleanAttribute });
237 | 
238 |   openDialog() {
239 |     const isBottomSheet = this.bottomSheetInput();
240 |     this.dialog.open(DemoCdkModalCmp, {
241 |       panelClass: isBottomSheet ? 'ds-bottom-sheet-panel' : 'ds-dialog-panel',
242 |       data: {
243 |         headerVariant: this.headerVariant(),
244 |         inverse: this.inverse(),
245 |         variant: this.variant(),
246 |         bottomSheet: isBottomSheet,
247 |       },
248 |     });
249 |   }
250 | }
251 | 
```

--------------------------------------------------------------------------------
/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/component-usage-graph-builder.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as path from 'path';
  2 | import { toUnixPath } from '@code-pushup/utils';
  3 | import { findAllFiles } from '@push-based/utils';
  4 | import {
  5 |   BuildComponentUsageGraphOptions,
  6 |   ComponentUsageGraphResult,
  7 |   FileInfo,
  8 | } from '../models/types.js';
  9 | import {
 10 |   DEPENDENCY_ANALYSIS_CONFIG,
 11 |   clearComponentImportRegexCache,
 12 | } from '../models/config.js';
 13 | import {
 14 |   analyzeFileWithUnifiedOptimization,
 15 |   extractComponentImportsUnified,
 16 | } from './unified-ast-analyzer.js';
 17 | import { resolveCrossPlatformPathAndValidateWithContext } from '../../shared/utils/cross-platform-path.js';
 18 | 
 19 | const BATCH_SIZE = 50; // Process files in batches of 50
 20 | 
 21 | export async function buildComponentUsageGraph(
 22 |   options: BuildComponentUsageGraphOptions,
 23 | ): Promise<ComponentUsageGraphResult> {
 24 |   const startTime = performance.now();
 25 | 
 26 |   const targetPath = resolveCrossPlatformPathAndValidateWithContext(
 27 |     options.cwd,
 28 |     options.directory,
 29 |     options.workspaceRoot,
 30 |   );
 31 | 
 32 |   const files: Record<string, FileInfo> = {};
 33 | 
 34 |   // Phase 1: Directory scanning
 35 |   const scanStartTime = performance.now();
 36 |   const allFiles = await scanDirectoryWithUtils(targetPath);
 37 |   const scanTime = performance.now() - scanStartTime;
 38 | 
 39 |   // Phase 2: File analysis with unified AST parsing
 40 |   const analysisStartTime = performance.now();
 41 |   await processFilesInParallel(allFiles, targetPath, files);
 42 |   const analysisTime = performance.now() - analysisStartTime;
 43 | 
 44 |   // Phase 3: Reverse dependency analysis
 45 |   const reverseDepsStartTime = performance.now();
 46 |   await addReverseDependenciesOptimized(files, targetPath);
 47 |   const reverseDepsTime = performance.now() - reverseDepsStartTime;
 48 | 
 49 |   const totalTime = performance.now() - startTime;
 50 | 
 51 |   // Log performance metrics
 52 |   console.log(`🚀 Unified AST Analysis Performance:`);
 53 |   console.log(
 54 |     `  📁 Directory scan: ${scanTime.toFixed(2)}ms (${((scanTime / totalTime) * 100).toFixed(1)}%)`,
 55 |   );
 56 |   console.log(
 57 |     `  🔍 File analysis: ${analysisTime.toFixed(2)}ms (${((analysisTime / totalTime) * 100).toFixed(1)}%)`,
 58 |   );
 59 |   console.log(
 60 |     `  🔗 Reverse deps: ${reverseDepsTime.toFixed(2)}ms (${((reverseDepsTime / totalTime) * 100).toFixed(1)}%)`,
 61 |   );
 62 |   console.log(
 63 |     `  ⚡ Total time: ${totalTime.toFixed(2)}ms for ${Object.keys(files).length} files`,
 64 |   );
 65 |   console.log(
 66 |     `  📊 Avg per file: ${(totalTime / Object.keys(files).length).toFixed(2)}ms`,
 67 |   );
 68 | 
 69 |   return files;
 70 | }
 71 | 
 72 | async function scanDirectoryWithUtils(dirPath: string): Promise<string[]> {
 73 |   const files: string[] = [];
 74 |   const { fileExtensions } = DEPENDENCY_ANALYSIS_CONFIG;
 75 | 
 76 |   try {
 77 |     // Use findAllFiles async generator for better memory efficiency
 78 |     for await (const file of findAllFiles(dirPath, (filePath) => {
 79 |       const ext = path.extname(filePath);
 80 |       return fileExtensions.includes(ext as any);
 81 |     })) {
 82 |       files.push(toUnixPath(file));
 83 |     }
 84 |   } catch (ctx) {
 85 |     throw new Error(
 86 |       `Failed to scan directory ${dirPath}: ${(ctx as Error).message}`,
 87 |     );
 88 |   }
 89 | 
 90 |   return files;
 91 | }
 92 | 
 93 | async function processFilesInParallel(
 94 |   allFiles: string[],
 95 |   targetPath: string,
 96 |   files: Record<string, FileInfo>,
 97 | ): Promise<void> {
 98 |   // Single pass with slice batching (no extra helpers)
 99 |   for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
100 |     const batch = allFiles.slice(i, i + BATCH_SIZE);
101 |     const batchStartTime = performance.now();
102 | 
103 |     const results = await Promise.all(
104 |       batch.map(async (filePath) => {
105 |         try {
106 |           const relativePath = toUnixPath(path.relative(targetPath, filePath));
107 |           const fileInfo = await analyzeFileWithUnifiedOptimization(
108 |             filePath,
109 |             targetPath,
110 |           );
111 |           return { relativePath, fileInfo } as const;
112 |         } catch (ctx) {
113 |           throw new Error(
114 |             `Failed to analyze file ${filePath}: ${(ctx as Error).message}`,
115 |           );
116 |         }
117 |       }),
118 |     );
119 | 
120 |     for (const result of results) {
121 |       if (result) {
122 |         files[result.relativePath] = result.fileInfo;
123 |       }
124 |     }
125 | 
126 |     const batchTime = performance.now() - batchStartTime;
127 |     console.log(
128 |       `  📦 Batch ${Math.ceil((i + batch.length) / BATCH_SIZE)}: ${batch.length} files in ${batchTime.toFixed(2)}ms (${(batchTime / batch.length).toFixed(2)}ms/file)`,
129 |     );
130 |   }
131 | }
132 | 
133 | async function addReverseDependenciesOptimized(
134 |   files: Record<string, FileInfo>,
135 |   basePath: string,
136 | ): Promise<void> {
137 |   // Build component name to file path mapping
138 |   const componentMap = new Map<string, string>();
139 |   const componentNames: string[] = [];
140 | 
141 |   for (const [filePath, fileInfo] of Object.entries(files)) {
142 |     if (fileInfo.componentName) {
143 |       componentMap.set(fileInfo.componentName, filePath);
144 |       componentNames.push(fileInfo.componentName);
145 |     }
146 |   }
147 | 
148 |   if (componentNames.length === 0) {
149 |     console.log(`  ℹ️  No components found for reverse dependency analysis`);
150 |     return; // No components to analyze
151 |   }
152 | 
153 |   console.log(
154 |     `  🔍 Analyzing reverse dependencies for ${componentNames.length} components`,
155 |   );
156 | 
157 |   // Process files in parallel batches for reverse dependency analysis
158 |   const fileEntries = Object.entries(files);
159 |   const batches = createBatches(fileEntries, BATCH_SIZE);
160 |   let processedBatches = 0;
161 | 
162 |   for (const batch of batches) {
163 |     const batchStartTime = performance.now();
164 |     const promises = batch.map(
165 |       async ([filePath, fileInfo]: [string, FileInfo]) => {
166 |         const fullPath = path.resolve(basePath, filePath);
167 | 
168 |         try {
169 |           // Only analyze TypeScript/JavaScript files for component imports
170 |           if (
171 |             fileInfo.type === 'typescript' ||
172 |             fileInfo.type === 'javascript'
173 |           ) {
174 |             // Use unified component import extraction instead of separate file read + AST parsing
175 |             const foundComponents = await extractComponentImportsUnified(
176 |               fullPath,
177 |               componentNames,
178 |             );
179 | 
180 |             // Collect all reverse dependencies for this file
181 |             const reverseDependencies = [];
182 |             for (const componentName of foundComponents) {
183 |               const componentFilePath = componentMap.get(componentName);
184 |               if (componentFilePath && componentFilePath !== filePath) {
185 |                 reverseDependencies.push({
186 |                   componentFilePath,
187 |                   dependency: {
188 |                     path: toUnixPath(filePath),
189 |                     type: 'reverse-dependency' as const,
190 |                     resolved: true,
191 |                     resolvedPath: toUnixPath(filePath),
192 |                     componentName: componentName,
193 |                     sourceFile: filePath,
194 |                   },
195 |                 });
196 |               }
197 |             }
198 | 
199 |             return reverseDependencies;
200 |           }
201 |         } catch (ctx) {
202 |           throw new Error(
203 |             `Failed to analyze reverse dependencies for ${filePath}: ${(ctx as Error).message}`,
204 |           );
205 |         }
206 | 
207 |         return [];
208 |       },
209 |     );
210 | 
211 |     const results = await Promise.all(promises);
212 |     const batchTime = performance.now() - batchStartTime;
213 |     processedBatches++;
214 | 
215 |     // Apply reverse dependencies
216 |     let dependenciesAdded = 0;
217 |     for (const result of results) {
218 |       if (Array.isArray(result)) {
219 |         for (const dependency of result) {
220 |           files[dependency.componentFilePath].dependencies.push(
221 |             dependency.dependency,
222 |           );
223 |           dependenciesAdded++;
224 |         }
225 |       }
226 |     }
227 | 
228 |     console.log(
229 |       `  🔗 Reverse deps batch ${processedBatches}: ${batch.length} files, ${dependenciesAdded} dependencies in ${batchTime.toFixed(2)}ms`,
230 |     );
231 |   }
232 | }
233 | 
234 | // Compact batch helper reused by reverse-dependency phase
235 | function createBatches<T>(items: T[], batchSize: number): T[][] {
236 |   const batches: T[][] = [];
237 |   for (let i = 0; i < items.length; i += batchSize) {
238 |     batches.push(items.slice(i, i + batchSize));
239 |   }
240 |   return batches;
241 | }
242 | 
243 | export function clearAnalysisCache(): void {
244 |   clearComponentImportRegexCache();
245 | }
246 | 
```

--------------------------------------------------------------------------------
/packages/shared/ds-component-coverage/src/lib/runner/audits/ds-coverage/class-definition.visitor.unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it } from 'vitest';
  2 | import { createClassDefinitionVisitor } from './class-definition.visitor';
  3 | import postcss from 'postcss';
  4 | import { visitEachChild } from '@push-based/styles-ast-utils';
  5 | 
  6 | describe('ClassDefinitionVisitor', () => {
  7 |   let cssAstVisitor: ReturnType<typeof createClassDefinitionVisitor>;
  8 | 
  9 |   it('should find deprecated class in CSS selector', () => {
 10 |     const styles = `
 11 |                 /* This comment is here */
 12 |                 .btn {
 13 |                   color: red;
 14 |                 }
 15 |              `;
 16 | 
 17 |     cssAstVisitor = createClassDefinitionVisitor({
 18 |       deprecatedCssClasses: ['btn'],
 19 |       componentName: 'DsButton',
 20 |       docsUrl: 'docs.example.com/DsButton',
 21 |     });
 22 | 
 23 |     const ast = postcss.parse(styles, { from: 'styles.css' });
 24 |     visitEachChild(ast, cssAstVisitor);
 25 | 
 26 |     expect(cssAstVisitor.getIssues()).toHaveLength(1);
 27 |     const message = cssAstVisitor.getIssues()[0].message;
 28 |     expect(message).toContain('btn');
 29 |     expect(message).toContain('DsButton');
 30 |     expect(cssAstVisitor.getIssues()[0]).toEqual(
 31 |       expect.objectContaining({
 32 |         severity: 'error',
 33 |         source: expect.objectContaining({
 34 |           file: 'styles.css',
 35 |           position: expect.any(Object),
 36 |         }),
 37 |       }),
 38 |     );
 39 |   });
 40 | 
 41 |   it('should not find class when it is not deprecated', () => {
 42 |     const styles = `
 43 |                 .safe-class {
 44 |                   color: red;
 45 |                 }
 46 | 
 47 |                 #btn-1 {
 48 |                   color: green;
 49 |                 }
 50 | 
 51 |                 button {
 52 |                   color: blue;
 53 |                 }
 54 |              `;
 55 | 
 56 |     cssAstVisitor = createClassDefinitionVisitor({
 57 |       deprecatedCssClasses: ['btn'],
 58 |       componentName: 'DsButton',
 59 |     });
 60 | 
 61 |     const ast = postcss.parse(styles, { from: 'styles.css' });
 62 |     visitEachChild(ast, cssAstVisitor);
 63 | 
 64 |     expect(cssAstVisitor.getIssues()).toHaveLength(0);
 65 |   });
 66 | 
 67 |   it('should find deprecated class in complex selector', () => {
 68 |     const styles = `
 69 |                 div > button.btn {
 70 |                   color: blue;
 71 |                 }
 72 |              `;
 73 | 
 74 |     cssAstVisitor = createClassDefinitionVisitor({
 75 |       deprecatedCssClasses: ['btn'],
 76 |       componentName: 'DsButton',
 77 |     });
 78 | 
 79 |     const ast = postcss.parse(styles, { from: 'styles.css' });
 80 |     visitEachChild(ast, cssAstVisitor);
 81 | 
 82 |     expect(cssAstVisitor.getIssues()).toHaveLength(1);
 83 |     const message = cssAstVisitor.getIssues()[0].message;
 84 |     expect(message).toContain('btn');
 85 |     expect(message).toContain('DsButton');
 86 |     expect(cssAstVisitor.getIssues()[0]).toEqual(
 87 |       expect.objectContaining({
 88 |         severity: 'error',
 89 |         source: expect.objectContaining({
 90 |           file: 'styles.css',
 91 |           position: expect.any(Object),
 92 |         }),
 93 |       }),
 94 |     );
 95 |   });
 96 | 
 97 |   describe('deduplication', () => {
 98 |     it('should deduplicate multiple deprecated classes in same selector', () => {
 99 |       const styles = `
100 |                   .btn.btn-primary {
101 |                     color: red;
102 |                   }
103 |                `;
104 | 
105 |       cssAstVisitor = createClassDefinitionVisitor({
106 |         deprecatedCssClasses: ['btn', 'btn-primary'],
107 |         componentName: 'DsButton',
108 |         docsUrl: 'docs.example.com/DsButton',
109 |       });
110 | 
111 |       const ast = postcss.parse(styles, { from: 'styles.css' });
112 |       visitEachChild(ast, cssAstVisitor);
113 | 
114 |       expect(cssAstVisitor.getIssues()).toHaveLength(1);
115 |       const message = cssAstVisitor.getIssues()[0].message;
116 |       expect(message).toContain('btn, btn-primary');
117 |       expect(message).toContain('DsButton');
118 |       expect(cssAstVisitor.getIssues()[0]).toEqual(
119 |         expect.objectContaining({
120 |           severity: 'error',
121 |           source: expect.objectContaining({
122 |             file: 'styles.css',
123 |             position: expect.any(Object),
124 |           }),
125 |         }),
126 |       );
127 |     });
128 | 
129 |     it('should deduplicate multiple deprecated classes in comma-separated selectors', () => {
130 |       const styles = `
131 |                   .btn, .btn-primary {
132 |                     color: red;
133 |                   }
134 |                `;
135 | 
136 |       cssAstVisitor = createClassDefinitionVisitor({
137 |         deprecatedCssClasses: ['btn', 'btn-primary'],
138 |         componentName: 'DsButton',
139 |         docsUrl: 'docs.example.com/DsButton',
140 |       });
141 | 
142 |       const ast = postcss.parse(styles, { from: 'styles.css' });
143 |       visitEachChild(ast, cssAstVisitor);
144 | 
145 |       expect(cssAstVisitor.getIssues()).toHaveLength(1);
146 |       const message = cssAstVisitor.getIssues()[0].message;
147 |       expect(message).toContain('btn, btn-primary');
148 |       expect(message).toContain('DsButton');
149 |       expect(cssAstVisitor.getIssues()[0]).toEqual(
150 |         expect.objectContaining({
151 |           severity: 'error',
152 |           source: expect.objectContaining({
153 |             file: 'styles.css',
154 |             position: expect.any(Object),
155 |           }),
156 |         }),
157 |       );
158 |     });
159 | 
160 |     it('should deduplicate three deprecated classes in same selector', () => {
161 |       const styles = `
162 |                   .btn.btn-primary.btn-large {
163 |                     color: red;
164 |                   }
165 |                `;
166 | 
167 |       cssAstVisitor = createClassDefinitionVisitor({
168 |         deprecatedCssClasses: ['btn', 'btn-primary', 'btn-large'],
169 |         componentName: 'DsButton',
170 |         docsUrl: 'docs.example.com/DsButton',
171 |       });
172 | 
173 |       const ast = postcss.parse(styles, { from: 'styles.css' });
174 |       visitEachChild(ast, cssAstVisitor);
175 | 
176 |       expect(cssAstVisitor.getIssues()).toHaveLength(1);
177 |       const message = cssAstVisitor.getIssues()[0].message;
178 |       expect(message).toContain('btn, btn-primary, btn-large');
179 |       expect(message).toContain('DsButton');
180 |       expect(cssAstVisitor.getIssues()[0]).toEqual(
181 |         expect.objectContaining({
182 |           severity: 'error',
183 |           source: expect.objectContaining({
184 |             file: 'styles.css',
185 |             position: expect.any(Object),
186 |           }),
187 |         }),
188 |       );
189 |     });
190 | 
191 |     it('should still create single issue for single deprecated class', () => {
192 |       const styles = `
193 |                   .btn {
194 |                     color: red;
195 |                   }
196 |                `;
197 | 
198 |       cssAstVisitor = createClassDefinitionVisitor({
199 |         deprecatedCssClasses: ['btn', 'btn-primary'],
200 |         componentName: 'DsButton',
201 |         docsUrl: 'docs.example.com/DsButton',
202 |       });
203 | 
204 |       const ast = postcss.parse(styles, { from: 'styles.css' });
205 |       visitEachChild(ast, cssAstVisitor);
206 | 
207 |       expect(cssAstVisitor.getIssues()).toHaveLength(1);
208 |       const message = cssAstVisitor.getIssues()[0].message;
209 |       expect(message).toContain('btn');
210 |       expect(message).not.toContain(',');
211 |       expect(message).toContain('DsButton');
212 |       expect(cssAstVisitor.getIssues()[0]).toEqual(
213 |         expect.objectContaining({
214 |           severity: 'error',
215 |           source: expect.objectContaining({
216 |             file: 'styles.css',
217 |             position: expect.any(Object),
218 |           }),
219 |         }),
220 |       );
221 |     });
222 | 
223 |     it('should handle mixed deprecated and non-deprecated classes', () => {
224 |       const styles = `
225 |                   .btn.safe-class.btn-primary {
226 |                     color: red;
227 |                   }
228 |                `;
229 | 
230 |       cssAstVisitor = createClassDefinitionVisitor({
231 |         deprecatedCssClasses: ['btn', 'btn-primary'],
232 |         componentName: 'DsButton',
233 |         docsUrl: 'docs.example.com/DsButton',
234 |       });
235 | 
236 |       const ast = postcss.parse(styles, { from: 'styles.css' });
237 |       visitEachChild(ast, cssAstVisitor);
238 | 
239 |       expect(cssAstVisitor.getIssues()).toHaveLength(1);
240 |       const message = cssAstVisitor.getIssues()[0].message;
241 |       expect(message).toContain('btn, btn-primary');
242 |       expect(message).not.toContain('safe-class');
243 |       expect(message).toContain('DsButton');
244 |       expect(cssAstVisitor.getIssues()[0]).toEqual(
245 |         expect.objectContaining({
246 |           severity: 'error',
247 |           source: expect.objectContaining({
248 |             file: 'styles.css',
249 |             position: expect.any(Object),
250 |           }),
251 |         }),
252 |       );
253 |     });
254 |   });
255 | });
256 | 
```

--------------------------------------------------------------------------------
/packages/angular-mcp-server/src/lib/tools/ds/component-contract/builder/utils/dom-slots.extractor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   visitComponentTemplate,
  3 |   visitEachTmplChild,
  4 |   NoopTmplVisitor,
  5 |   parseClassNames,
  6 |   ParsedComponent,
  7 | } from '@push-based/angular-ast-utils';
  8 | import {
  9 |   extractBindings,
 10 |   extractAttributes,
 11 |   extractEvents,
 12 | } from './element-helpers.js';
 13 | import type {
 14 |   TmplAstNode,
 15 |   TmplAstElement,
 16 |   TmplAstForLoopBlock,
 17 |   TmplAstIfBlock,
 18 |   TmplAstSwitchBlock,
 19 |   TmplAstDeferredBlock,
 20 |   TmplAstContent,
 21 |   TmplAstTemplate,
 22 | } from '@angular/compiler' with { 'resolution-mode': 'import' };
 23 | import type {
 24 |   Slots,
 25 |   DomStructure,
 26 |   StructuralDirectiveContext,
 27 | } from '../../shared/models/types.js';
 28 | 
 29 | /**
 30 |  * Extract both content projection slots (ng-content) and DOM structure
 31 |  * in a single pass over the component template for better performance
 32 |  */
 33 | export async function extractSlotsAndDom(
 34 |   parsedComponent: ParsedComponent,
 35 | ): Promise<{ slots: Slots; dom: DomStructure }> {
 36 |   const slots: Slots = {};
 37 |   const dom: DomStructure = {};
 38 | 
 39 |   await visitComponentTemplate(
 40 |     parsedComponent,
 41 |     {},
 42 |     async (_, templateAsset) => {
 43 |       const parsedTemplate = await templateAsset.parse();
 44 |       const visitor = new DomAndSlotExtractionVisitor(slots, dom);
 45 |       visitEachTmplChild(parsedTemplate.nodes as TmplAstNode[], visitor);
 46 |       return [];
 47 |     },
 48 |   );
 49 | 
 50 |   return { slots, dom };
 51 | }
 52 | 
 53 | /**
 54 |  * Combined visitor to extract ng-content slots and build DOM structure in a single pass
 55 |  */
 56 | class DomAndSlotExtractionVisitor extends NoopTmplVisitor {
 57 |   private slotCounter = 0;
 58 |   private pathStack: string[] = [];
 59 | 
 60 |   /**
 61 |    * Stack of active structural directive contexts so that nested elements inherit
 62 |    * the directive information of all parent control-flow blocks.
 63 |    */
 64 |   private directiveStack: StructuralDirectiveContext[] = [];
 65 | 
 66 |   constructor(
 67 |     private slots: Slots,
 68 |     private dom: DomStructure,
 69 |   ) {
 70 |     super();
 71 |   }
 72 | 
 73 |   override visitElement(element: TmplAstElement): void {
 74 |     // skip explicit handling of <ng-content> here – it is visited by visitContent
 75 | 
 76 |     const selectorKey = this.generateSelectorKey(element);
 77 |     const parentKey =
 78 |       this.pathStack.length > 0 ? this.pathStack.join(' > ') : null;
 79 | 
 80 |     this.dom[selectorKey] = {
 81 |       tag: element.name,
 82 |       parent: parentKey,
 83 |       children: [],
 84 |       bindings: extractBindings(element),
 85 |       attributes: extractAttributes(element),
 86 |       events: extractEvents(element),
 87 |       structural:
 88 |         this.directiveStack.length > 0
 89 |           ? // spread to detach reference
 90 |             [...this.directiveStack]
 91 |           : undefined,
 92 |     };
 93 | 
 94 |     if (parentKey && this.dom[parentKey]) {
 95 |       this.dom[parentKey].children.push(selectorKey);
 96 |     }
 97 | 
 98 |     // Push only the current element's selector to the stack
 99 |     const currentSelector = this.generateCurrentElementSelector(element);
100 |     this.pathStack.push(currentSelector);
101 |     visitEachTmplChild(element.children as TmplAstNode[], this);
102 |     this.pathStack.pop();
103 |   }
104 | 
105 |   private visitBlockWithChildren(block: { children?: TmplAstNode[] }): void {
106 |     if (block.children) {
107 |       visitEachTmplChild(block.children as TmplAstNode[], this);
108 |     }
109 |   }
110 | 
111 |   override visitForLoopBlock(block: TmplAstForLoopBlock): void {
112 |     const ctx: StructuralDirectiveContext = {
113 |       kind: 'for',
114 |       expression: (block as any).expression?.source ?? undefined,
115 |       alias: (block as any).item?.name ?? undefined,
116 |     };
117 |     this.directiveStack.push(ctx);
118 |     this.visitBlockWithChildren(block);
119 |     block.empty?.visit(this);
120 |     this.directiveStack.pop();
121 |   }
122 | 
123 |   override visitIfBlock(block: TmplAstIfBlock): void {
124 |     const outerCtx: StructuralDirectiveContext = { kind: 'if' };
125 |     this.directiveStack.push(outerCtx);
126 |     block.branches.forEach((branch) => branch.visit(this));
127 |     this.directiveStack.pop();
128 |   }
129 | 
130 |   override visitSwitchBlock(block: TmplAstSwitchBlock): void {
131 |     const ctx: StructuralDirectiveContext = {
132 |       kind: 'switch',
133 |       expression: (block as any).expression?.source ?? undefined,
134 |     };
135 |     this.directiveStack.push(ctx);
136 |     block.cases.forEach((caseBlock) => caseBlock.visit(this));
137 |     this.directiveStack.pop();
138 |   }
139 | 
140 |   override visitDeferredBlock(deferred: TmplAstDeferredBlock): void {
141 |     const ctx: StructuralDirectiveContext = { kind: 'defer' };
142 |     this.directiveStack.push(ctx);
143 |     deferred.visitAll(this);
144 |     this.directiveStack.pop();
145 |   }
146 | 
147 |   /**
148 |    * Handle <ng-content> projection points represented in the Angular template AST as TmplAstContent.
149 |    * Recognises default, attribute-selector ([slot=foo]) and legacy slot=foo syntaxes.
150 |    */
151 |   override visitContent(content: TmplAstContent): void {
152 |     const selectValue = content.selector ?? '';
153 |     const slotName = selectValue ? this.parseSlotName(selectValue) : 'default';
154 | 
155 |     this.slots[slotName] = {
156 |       selector: selectValue
157 |         ? `ng-content[select="${selectValue}"]`
158 |         : 'ng-content',
159 |     };
160 |   }
161 | 
162 |   private parseSlotName(selectValue: string): string {
163 |     // Matches [slot=foo], [slot='foo'], [slot="foo"], slot=foo  (case-insensitive)
164 |     const match = selectValue.match(
165 |       /(?:^\[?)\s*slot\s*=\s*['"]?([^'" \]\]]+)['"]?\]?$/i,
166 |     );
167 |     if (match) {
168 |       return match[1];
169 |     }
170 | 
171 |     if (selectValue.startsWith('.')) {
172 |       return selectValue.substring(1);
173 |     }
174 | 
175 |     return selectValue || `slot-${this.slotCounter++}`;
176 |   }
177 | 
178 |   private generateSelectorKey(element: TmplAstElement): string {
179 |     const currentSelector = this.generateCurrentElementSelector(element);
180 | 
181 |     return this.pathStack.length > 0
182 |       ? `${this.pathStack.join(' > ')} > ${currentSelector}`
183 |       : currentSelector;
184 |   }
185 | 
186 |   private generateCurrentElementSelector(element: TmplAstElement): string {
187 |     const classes = this.extractClasses(element);
188 |     const id = element.attributes.find((attr) => attr.name === 'id')?.value;
189 | 
190 |     let selector = element.name;
191 |     if (id) selector += `#${id}`;
192 |     if (classes.length > 0) selector += '.' + classes.join('.');
193 | 
194 |     return selector;
195 |   }
196 | 
197 |   private extractClasses(element: TmplAstElement): string[] {
198 |     const out = new Set<string>();
199 | 
200 |     const classAttr = element.attributes.find((attr) => attr.name === 'class');
201 |     if (classAttr) {
202 |       parseClassNames(classAttr.value).forEach((cls) => out.add(cls));
203 |     }
204 | 
205 |     element.inputs
206 |       .filter((input) => input.name.startsWith('class.'))
207 |       .forEach((input) => out.add(input.name.substring(6)));
208 | 
209 |     return [...out];
210 |   }
211 | 
212 |   /** Legacy structural directives on <ng-template> (e.g., *ngIf, *ngFor). */
213 |   override visitTemplate(template: TmplAstTemplate): void {
214 |     const dir = template.templateAttrs?.find?.((a: any) =>
215 |       a.name?.startsWith?.('ng'),
216 |     );
217 | 
218 |     if (!dir) {
219 |       // Just traverse children when no structural directive is present
220 |       this.visitBlockWithChildren(template);
221 |       return;
222 |     }
223 | 
224 |     const map: Record<string, StructuralDirectiveContext['kind']> = {
225 |       ngIf: 'if',
226 |       ngForOf: 'for',
227 |       ngSwitch: 'switch',
228 |       ngSwitchCase: 'switchCase',
229 |       ngSwitchDefault: 'switchDefault',
230 |     } as const;
231 | 
232 |     const kind = map[dir.name as keyof typeof map];
233 | 
234 |     // If the directive is not one we're interested in, skip recording
235 |     if (!kind) {
236 |       this.visitBlockWithChildren(template);
237 |       return;
238 |     }
239 | 
240 |     const ctx: StructuralDirectiveContext = {
241 |       kind,
242 |       expression: (dir as any).value as string | undefined,
243 |     };
244 | 
245 |     this.directiveStack.push(ctx);
246 |     this.visitBlockWithChildren(template);
247 |     this.directiveStack.pop();
248 |   }
249 | }
250 | 
251 | /**
252 |  * Attach simple "just traverse children" visitors dynamically to
253 |  * avoid eight nearly identical class methods above.
254 |  */
255 | const SIMPLE_VISIT_METHODS = [
256 |   'visitForLoopBlockEmpty',
257 |   'visitIfBlockBranch',
258 |   'visitSwitchBlockCase',
259 |   'visitDeferredBlockError',
260 |   'visitDeferredBlockLoading',
261 |   'visitDeferredBlockPlaceholder',
262 |   // 'visitTemplate',
263 | ] as const;
264 | 
265 | SIMPLE_VISIT_METHODS.forEach((method) => {
266 |   // eslint-disable-next-line @typescript-eslint/no-explicit-any
267 |   (DomAndSlotExtractionVisitor.prototype as any)[method] = function (
268 |     this: DomAndSlotExtractionVisitor,
269 |     block: { children?: TmplAstNode[] },
270 |   ): void {
271 |     if (block.children) {
272 |       visitEachTmplChild(block.children as TmplAstNode[], this);
273 |     }
274 |   };
275 | });
276 | 
```

--------------------------------------------------------------------------------
/packages/shared/utils/ai/FUNCTIONS.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Public API — Quick Reference
  2 | 
  3 | | Symbol                 | Kind     | Summary                                                        |
  4 | | ---------------------- | -------- | -------------------------------------------------------------- |
  5 | | `ProcessResult`        | type     | Process execution result with stdout, stderr, code, and timing |
  6 | | `ProcessError`         | class    | Error class for process execution failures                     |
  7 | | `ProcessConfig`        | type     | Configuration object for process execution                     |
  8 | | `ProcessObserver`      | type     | Observer interface for process events                          |
  9 | | `LinePosition`         | type     | Position information for text matches within a line            |
 10 | | `SourcePosition`       | type     | Position information with line and column details              |
 11 | | `SourceLocation`       | type     | File location with position information                        |
 12 | | `accessContent`        | function | Generator function to iterate over file content lines          |
 13 | | `calcDuration`         | function | Calculate duration between performance timestamps              |
 14 | | `executeProcess`       | function | Execute a child process with observer pattern                  |
 15 | | `fileResolverCache`    | constant | Map cache for file resolution operations                       |
 16 | | `findAllFiles`         | function | Async generator to find files matching a predicate             |
 17 | | `findFilesWithPattern` | function | Find TypeScript files containing a search pattern              |
 18 | | `findInFile`           | function | Find pattern matches within a specific file                    |
 19 | | `formatCommandLog`     | function | Format command strings with ANSI colors and directory context  |
 20 | | `getLineHits`          | function | Get all pattern matches within a text line                     |
 21 | | `isExcludedDirectory`  | function | Check if a directory should be excluded from searches          |
 22 | | `isVerbose`            | function | Check if verbose logging is enabled via environment variable   |
 23 | | `loadDefaultExport`    | function | Dynamically import ES modules and extract default export       |
 24 | | `objectToCliArgs`      | function | Convert object properties to command-line arguments            |
 25 | | `resolveFile`          | function | Read file content directly without caching                     |
 26 | | `resolveFileCached`    | function | Read file content with caching for performance                 |
 27 | 
 28 | ## Types
 29 | 
 30 | ### `ProcessResult`
 31 | 
 32 | ```ts
 33 | type ProcessResult = {
 34 |   stdout: string;
 35 |   stderr: string;
 36 |   code: number | null;
 37 |   date: string;
 38 |   duration: number;
 39 | };
 40 | ```
 41 | 
 42 | Represents the result of a process execution with output streams, exit code, and timing information.
 43 | 
 44 | ### `ProcessConfig`
 45 | 
 46 | ```ts
 47 | type ProcessConfig = Omit<
 48 |   SpawnOptionsWithStdioTuple<StdioPipe, StdioPipe, StdioPipe>,
 49 |   'stdio'
 50 | > & {
 51 |   command: string;
 52 |   args?: string[];
 53 |   observer?: ProcessObserver;
 54 |   ignoreExitCode?: boolean;
 55 | };
 56 | ```
 57 | 
 58 | Configuration object for process execution, extending Node.js spawn options.
 59 | 
 60 | ### `ProcessObserver`
 61 | 
 62 | ```ts
 63 | type ProcessObserver = {
 64 |   onStdout?: (stdout: string, sourceProcess?: ChildProcess) => void;
 65 |   onStderr?: (stderr: string, sourceProcess?: ChildProcess) => void;
 66 |   onError?: (error: ProcessError) => void;
 67 |   onComplete?: () => void;
 68 | };
 69 | ```
 70 | 
 71 | Observer interface for handling process events during execution.
 72 | 
 73 | ### `LinePosition`
 74 | 
 75 | ```ts
 76 | type LinePosition = {
 77 |   startColumn: number;
 78 |   endColumn?: number;
 79 | };
 80 | ```
 81 | 
 82 | Position information for text matches within a single line.
 83 | 
 84 | ### `SourcePosition`
 85 | 
 86 | ```ts
 87 | type SourcePosition = {
 88 |   startLine: number;
 89 |   endLine?: number;
 90 | } & LinePosition;
 91 | ```
 92 | 
 93 | Extended position information including line numbers.
 94 | 
 95 | ### `SourceLocation`
 96 | 
 97 | ```ts
 98 | type SourceLocation = {
 99 |   file: string;
100 |   position: SourcePosition;
101 | };
102 | ```
103 | 
104 | Complete location information with file path and position details.
105 | 
106 | ## Classes
107 | 
108 | ### `ProcessError extends Error`
109 | 
110 | Error class for process execution failures, containing additional process result information.
111 | 
112 | **Properties:**
113 | 
114 | - `code: number | null` - Process exit code
115 | - `stderr: string` - Process error output
116 | - `stdout: string` - Process standard output
117 | 
118 | ## Functions
119 | 
120 | ### `executeProcess(cfg: ProcessConfig): Promise<ProcessResult>`
121 | 
122 | Executes a child process with comprehensive error handling and observer pattern support.
123 | 
124 | **Parameters:**
125 | 
126 | - `cfg` - Process configuration object
127 | 
128 | **Returns:** Promise resolving to process result
129 | 
130 | ### `findFilesWithPattern(baseDir: string, searchPattern: string): Promise<string[]>`
131 | 
132 | Searches for TypeScript files containing the specified pattern.
133 | 
134 | **Parameters:**
135 | 
136 | - `baseDir` - Directory to search (absolute or resolved by caller)
137 | - `searchPattern` - Pattern to match in file contents
138 | 
139 | **Returns:** Promise resolving to array of file paths
140 | 
141 | ### `findAllFiles(baseDir: string, predicate?: (file: string) => boolean): AsyncGenerator<string>`
142 | 
143 | Async generator that finds all files matching a predicate function.
144 | 
145 | **Parameters:**
146 | 
147 | - `baseDir` - Base directory to search
148 | - `predicate` - Optional file filter function (defaults to `.ts` files)
149 | 
150 | **Returns:** Async generator yielding file paths
151 | 
152 | ### `findInFile(file: string, searchPattern: string, bail?: boolean): Promise<SourceLocation[]>`
153 | 
154 | Finds all occurrences of a pattern within a specific file.
155 | 
156 | **Parameters:**
157 | 
158 | - `file` - File path to search
159 | - `searchPattern` - Pattern to find
160 | - `bail` - Optional flag to stop after first match
161 | 
162 | **Returns:** Promise resolving to array of source locations
163 | 
164 | ### `resolveFileCached(filePath: string): Promise<string>`
165 | 
166 | Resolves file content with caching to avoid reading the same file multiple times.
167 | 
168 | **Parameters:**
169 | 
170 | - `filePath` - Path to the file to read
171 | 
172 | **Returns:** Promise resolving to file content
173 | 
174 | ### `resolveFile(filePath: string): Promise<string>`
175 | 
176 | Resolves file content directly without caching.
177 | 
178 | **Parameters:**
179 | 
180 | - `filePath` - Path to the file to read
181 | 
182 | **Returns:** Promise resolving to file content
183 | 
184 | ### `formatCommandLog(command: string, args?: string[], cwd?: string): string`
185 | 
186 | Formats a command string with ANSI colors and optional directory context.
187 | 
188 | **Parameters:**
189 | 
190 | - `command` - Command to execute
191 | - `args` - Command arguments (optional)
192 | - `cwd` - Current working directory (optional)
193 | 
194 | **Returns:** ANSI-colored formatted command string
195 | 
196 | ### `objectToCliArgs<T extends object>(params?: CliArgsObject<T>): string[]`
197 | 
198 | Converts an object with different value types into command-line arguments array.
199 | 
200 | **Parameters:**
201 | 
202 | - `params` - Object with CLI parameters
203 | 
204 | **Returns:** Array of formatted CLI arguments
205 | 
206 | ### `calcDuration(start: number, stop?: number): number`
207 | 
208 | Calculates duration between performance timestamps.
209 | 
210 | **Parameters:**
211 | 
212 | - `start` - Start timestamp from `performance.now()`
213 | - `stop` - Optional end timestamp (defaults to current time)
214 | 
215 | **Returns:** Duration in milliseconds
216 | 
217 | ### `getLineHits(content: string, pattern: string, bail?: boolean): LinePosition[]`
218 | 
219 | Gets all pattern matches within a text line.
220 | 
221 | **Parameters:**
222 | 
223 | - `content` - Text content to search
224 | - `pattern` - Pattern to find
225 | - `bail` - Optional flag to stop after first match
226 | 
227 | **Returns:** Array of line positions
228 | 
229 | ### `accessContent(content: string): Generator<string>`
230 | 
231 | Generator function to iterate over file content lines.
232 | 
233 | **Parameters:**
234 | 
235 | - `content` - File content string
236 | 
237 | **Returns:** Generator yielding individual lines
238 | 
239 | ### `isExcludedDirectory(fileName: string): boolean`
240 | 
241 | Checks if a directory should be excluded from file searches.
242 | 
243 | **Parameters:**
244 | 
245 | - `fileName` - Directory name to check
246 | 
247 | **Returns:** `true` if directory should be excluded
248 | 
249 | ### `isVerbose(): boolean`
250 | 
251 | Checks if verbose logging is enabled via the `NG_MCP_VERBOSE` environment variable.
252 | 
253 | **Returns:** `true` if verbose logging is enabled
254 | 
255 | ### `loadDefaultExport<T = unknown>(filePath: string): Promise<T>`
256 | 
257 | Dynamically imports an ES Module and extracts the default export. Uses proper file URL conversion for cross-platform compatibility.
258 | 
259 | **Parameters:**
260 | 
261 | - `filePath` - Absolute path to the ES module file to import
262 | 
263 | **Returns:** Promise resolving to the default export from the module
264 | 
265 | **Throws:** Error if the module cannot be loaded or has no default export
266 | 
267 | **Example:**
268 | 
269 | ```typescript
270 | const config = await loadDefaultExport('/path/to/config.js');
271 | const data = await loadDefaultExport<MyDataType>('/path/to/data.mjs');
272 | ```
273 | 
274 | ## Constants
275 | 
276 | ### `fileResolverCache: Map<string, Promise<string>>`
277 | 
278 | Map cache used by `resolveFileCached` to store file reading promises and avoid duplicate file operations.
279 | 
```

--------------------------------------------------------------------------------
/packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/third-case/product-card.component.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   ChangeDetectionStrategy,
  3 |   Component,
  4 |   ViewEncapsulation,
  5 |   computed,
  6 |   input,
  7 |   output,
  8 |   signal,
  9 |   booleanAttribute,
 10 |   OnInit,
 11 | } from '@angular/core';
 12 | import { CommonModule } from '@angular/common';
 13 | import { FormsModule } from '@angular/forms';
 14 | import { DsBadge } from '@frontend/ui/badge';
 15 | 
 16 | export interface Product {
 17 |   id: string;
 18 |   name: string;
 19 |   price: number;
 20 |   originalPrice?: number;
 21 |   category: string;
 22 |   rating: number;
 23 |   reviewCount: number;
 24 |   inStock: boolean;
 25 |   imageUrl: string;
 26 |   tags: string[];
 27 | }
 28 | 
 29 | export const PRODUCT_BADGE_TYPES = ['sale', 'new', 'bestseller', 'limited'] as const;
 30 | export type ProductBadgeType = (typeof PRODUCT_BADGE_TYPES)[number];
 31 | 
 32 | @Component({
 33 |   selector: 'app-product-card',
 34 |   standalone: true,
 35 |   imports: [CommonModule, FormsModule, DsBadge],
 36 |   template: `
 37 |     <div class="product-card" [class.product-card-selected]="selected()">
 38 |       <!-- Product Image with Badge Overlay -->
 39 |       <div class="product-image-container">
 40 |         <img 
 41 |           [src]="product().imageUrl" 
 42 |           [alt]="product().name"
 43 |           class="product-image"
 44 |           (error)="onImageError($event)">
 45 |         
 46 |         <!-- DsBadge Implementation -->
 47 |         @if (showBadge()) {
 48 |           <div class="badge-overlay">
 49 |             <ds-badge 
 50 |               [size]="compact() ? 'xsmall' : 'medium'"
 51 |               [variant]="getBadgeVariant()">
 52 |               
 53 |               <!-- Icon slot (start) -->
 54 |               <span slot="start">
 55 |                 @switch (badgeType()) {
 56 |                   @case ('sale') {
 57 |                     🔥
 58 |                   }
 59 |                   @case ('new') {
 60 |                     ✨
 61 |                   }
 62 |                   @case ('bestseller') {
 63 |                     ⭐
 64 |                   }
 65 |                   @case ('limited') {
 66 |                     ⏰
 67 |                   }
 68 |                 }
 69 |               </span>
 70 |               
 71 |               <!-- Main badge text -->
 72 |               {{ getBadgeText() }}
 73 |               
 74 |               <!-- Optional percentage for sale badges (end slot) -->
 75 |               @if (badgeType() === 'sale' && product().originalPrice) {
 76 |                 <span slot="end">
 77 |                   -{{ getSalePercentage() }}%
 78 |                 </span>
 79 |               }
 80 |             </ds-badge>
 81 |           </div>
 82 |         }
 83 |         
 84 |         <!-- Stock status indicator -->
 85 |         @if (!product().inStock) {
 86 |           <div class="stock-overlay">
 87 |             <span class="stock-badge">Out of Stock</span>
 88 |           </div>
 89 |         }
 90 |       </div>
 91 | 
 92 |       <!-- Product Content -->
 93 |       <div class="product-content">
 94 |         <div class="product-header">
 95 |           <h3 class="product-name">{{ product().name }}</h3>
 96 |           <button 
 97 |             class="favorite-button"
 98 |             [class.favorite-active]="favorited()"
 99 |             (click)="toggleFavorite()"
100 |             [attr.aria-label]="favorited() ? 'Remove from favorites' : 'Add to favorites'">
101 |             <svg width="20" height="20" viewBox="0 0 20 20">
102 |               <path d="M10 15l-5.5 3 1-6L1 7.5l6-.5L10 1l3 6 6 .5-4.5 4.5 1 6z" 
103 |                     [attr.fill]="favorited() ? '#ef4444' : 'none'"
104 |                     [attr.stroke]="favorited() ? '#ef4444' : 'currentColor'"
105 |                     stroke-width="1.5"/>
106 |             </svg>
107 |           </button>
108 |         </div>
109 |         
110 |         <div class="product-category">{{ product().category }}</div>
111 |         
112 |         <!-- Price section with conditional styling -->
113 |         <div class="product-pricing">
114 |           @if (product().originalPrice && product().originalPrice > product().price) {
115 |             <span class="original-price">\${{ product().originalPrice.toFixed(2) }}</span>
116 |           }
117 |           <span class="current-price">\${{ product().price.toFixed(2) }}</span>
118 |         </div>
119 |         
120 |         <!-- Rating and reviews -->
121 |         <div class="product-rating">
122 |           <div class="rating-stars">
123 |             @for (star of getStarArray(); track $index) {
124 |               <span class="star" [class.star-filled]="star">★</span>
125 |             }
126 |           </div>
127 |           <span class="rating-text">{{ product().rating.toFixed(1) }} ({{ product().reviewCount }})</span>
128 |         </div>
129 |         
130 |         <!-- Product tags -->
131 |         @if (product().tags.length > 0) {
132 |           <div class="product-tags">
133 |             @for (tag of product().tags.slice(0, 3); track tag) {
134 |               <span class="product-tag">{{ tag }}</span>
135 |             }
136 |             @if (product().tags.length > 3) {
137 |               <span class="tag-more">+{{ product().tags.length - 3 }} more</span>
138 |             }
139 |           </div>
140 |         }
141 |       </div>
142 | 
143 |       <!-- Product Actions -->
144 |       <div class="product-actions">
145 |         <button 
146 |           class="action-button add-to-cart"
147 |           [disabled]="!product().inStock"
148 |           (click)="addToCart()"
149 |           [attr.aria-label]="'Add ' + product().name + ' to cart'">
150 |           @if (product().inStock) {
151 |             Add to Cart
152 |           } @else {
153 |             Notify When Available
154 |           }
155 |         </button>
156 |         
157 |         <button 
158 |           class="action-button quick-view"
159 |           (click)="quickView()"
160 |           [attr.aria-label]="'Quick view ' + product().name">
161 |           Quick View
162 |         </button>
163 |       </div>
164 |     </div>
165 |   `,
166 |   styleUrls: ['./product-card.component.scss'],
167 |   encapsulation: ViewEncapsulation.None,
168 |   changeDetection: ChangeDetectionStrategy.OnPush,
169 | })
170 | export class ProductCardComponent implements OnInit {
171 |   // Product data
172 |   product = input.required<Product>();
173 |   
174 |   // Badge configuration
175 |   badgeType = input<ProductBadgeType>('sale');
176 |   showBadge = input(true, { transform: booleanAttribute });
177 |   animated = input(true, { transform: booleanAttribute });
178 |   compact = input(false, { transform: booleanAttribute });
179 |   
180 |   // Card state
181 |   selected = input(false, { transform: booleanAttribute });
182 |   favorited = signal(false);
183 |   
184 |   // Outputs
185 |   productSelected = output<Product>();
186 |   favoriteToggled = output<{product: Product, favorited: boolean}>();
187 |   addToCartClicked = output<Product>();
188 |   quickViewClicked = output<Product>();
189 | 
190 |   ngOnInit() {
191 |     // Initialize favorited state from localStorage or API
192 |     const savedFavorites = localStorage.getItem('favoriteProducts');
193 |     if (savedFavorites) {
194 |       const favorites = JSON.parse(savedFavorites) as string[];
195 |       this.favorited.set(favorites.includes(this.product().id));
196 |     }
197 |   }
198 | 
199 |   getBadgeText(): string {
200 |     const typeMap: Record<ProductBadgeType, string> = {
201 |       'sale': 'Sale',
202 |       'new': 'New',
203 |       'bestseller': 'Best Seller',
204 |       'limited': 'Limited Time'
205 |     };
206 |     return typeMap[this.badgeType()];
207 |   }
208 | 
209 |   getBadgeVariant() {
210 |     const variantMap: Record<ProductBadgeType, string> = {
211 |       'sale': 'red-strong',
212 |       'new': 'green-strong',
213 |       'bestseller': 'yellow-strong',
214 |       'limited': 'purple-strong'
215 |     };
216 |     return variantMap[this.badgeType()];
217 |   }
218 | 
219 |   getBadgeAriaLabel(): string {
220 |     const text = this.getBadgeText();
221 |     if (this.badgeType() === 'sale' && this.product().originalPrice) {
222 |       return `${text} badge: ${this.getSalePercentage()}% off`;
223 |     }
224 |     return `${text} badge`;
225 |   }
226 | 
227 |   getSalePercentage(): number {
228 |     const original = this.product().originalPrice;
229 |     const current = this.product().price;
230 |     if (!original || original <= current) return 0;
231 |     return Math.round(((original - current) / original) * 100);
232 |   }
233 | 
234 |   getStarArray(): boolean[] {
235 |     const rating = this.product().rating;
236 |     const stars: boolean[] = [];
237 |     for (let i = 1; i <= 5; i++) {
238 |       stars.push(i <= rating);
239 |     }
240 |     return stars;
241 |   }
242 | 
243 |   toggleFavorite() {
244 |     const newFavorited = !this.favorited();
245 |     this.favorited.set(newFavorited);
246 |     
247 |     // Update localStorage
248 |     const savedFavorites = localStorage.getItem('favoriteProducts');
249 |     const favorites = savedFavorites ? JSON.parse(savedFavorites) as string[] : [];
250 |     
251 |     if (newFavorited) {
252 |       if (!favorites.includes(this.product().id)) {
253 |         favorites.push(this.product().id);
254 |       }
255 |     } else {
256 |       const index = favorites.indexOf(this.product().id);
257 |       if (index > -1) {
258 |         favorites.splice(index, 1);
259 |       }
260 |     }
261 |     
262 |     localStorage.setItem('favoriteProducts', JSON.stringify(favorites));
263 |     this.favoriteToggled.emit({product: this.product(), favorited: newFavorited});
264 |   }
265 | 
266 |   addToCart() {
267 |     if (this.product().inStock) {
268 |       this.addToCartClicked.emit(this.product());
269 |     }
270 |   }
271 | 
272 |   quickView() {
273 |     this.quickViewClicked.emit(this.product());
274 |   }
275 | 
276 |   onImageError(event: Event) {
277 |     const img = event.target as HTMLImageElement;
278 |     img.src = 'https://via.placeholder.com/300x200?text=No+Image';
279 |   }
280 | } 
```

--------------------------------------------------------------------------------
/packages/shared/angular-ast-utils/src/lib/template/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type {
  2 |   AST,
  3 |   ASTWithSource,
  4 |   ParsedTemplate,
  5 |   ParseSourceSpan,
  6 | } from '@angular/compiler' with { 'resolution-mode': 'import' };
  7 | 
  8 | import { Issue } from '@code-pushup/models';
  9 | import { Asset, ParsedComponent } from '../types.js';
 10 | 
 11 | /**
 12 |  * Convert a TmplAstElement to an Issue source object and adjust its position based on startLine.
 13 |  * It creates a "linkable" source object for the issue.
 14 |  * By default, the source location is 0 indexed, so we add 1 to the startLine to make it work in file links.
 15 |  *
 16 |  * @param element The element to convert.
 17 |  * @param startLine The baseline number to adjust positions.
 18 |  */
 19 | export function tmplAstElementToSource(
 20 |   {
 21 |     startSourceSpan,
 22 |     sourceSpan,
 23 |     endSourceSpan,
 24 |   }: {
 25 |     sourceSpan: ParseSourceSpan;
 26 |     startSourceSpan: ParseSourceSpan;
 27 |     endSourceSpan: ParseSourceSpan | null;
 28 |   },
 29 |   startLine = 0,
 30 | ): Issue['source'] {
 31 |   const offset = startLine; // TS Ast is 0 indexed so is work in 0 based index out of the box
 32 |   return {
 33 |     file: sourceSpan.start.file.url,
 34 |     position: {
 35 |       startLine: (startSourceSpan?.start.line ?? 0) + offset + 1,
 36 |       ...(startSourceSpan?.start.col && {
 37 |         startColumn: startSourceSpan?.start.col,
 38 |       }),
 39 |       ...(endSourceSpan?.end.line !== undefined && {
 40 |         endLine: endSourceSpan?.end.line + offset + 1,
 41 |       }),
 42 |       ...(endSourceSpan?.end.col && {
 43 |         endColumn: endSourceSpan?.end.col,
 44 |       }),
 45 |     },
 46 |   };
 47 | }
 48 | 
 49 | export function parseClassNames(classString: string): string[] {
 50 |   return classString.trim().split(/\s+/).filter(Boolean);
 51 | }
 52 | 
 53 | export async function visitComponentTemplate<T>(
 54 |   component: ParsedComponent,
 55 |   visitorArgument: T,
 56 |   getIssues: (
 57 |     tokenReplacement: T,
 58 |     asset: Asset<ParsedTemplate>,
 59 |   ) => Promise<Issue[]>,
 60 | ): Promise<Issue[]> {
 61 |   const { templateUrl, template } = component;
 62 | 
 63 |   if (templateUrl == null && template == null) {
 64 |     return [];
 65 |   }
 66 |   const componentTemplate = templateUrl ?? template;
 67 | 
 68 |   return getIssues(visitorArgument, componentTemplate);
 69 | }
 70 | 
 71 | /**
 72 |  * AST-based ngClass parser that properly detects class usage in Angular expressions
 73 |  * Handles arrays, objects, and ternary expressions to find actual class usage
 74 |  */
 75 | export function extractClassNamesFromNgClassAST(
 76 |   ast: AST,
 77 |   targetClassNames: string[],
 78 | ): string[] {
 79 |   const foundClasses: string[] = [];
 80 |   const targetSet = new Set(targetClassNames);
 81 | 
 82 |   function visitAST(node: AST): void {
 83 |     if (!node) return;
 84 | 
 85 |     // Use duck typing instead of instanceof for better compatibility
 86 |     const nodeType = node.constructor.name;
 87 | 
 88 |     // Handle array literals: ['class1', 'class2', variable]
 89 |     if (nodeType === 'LiteralArray' && 'expressions' in node) {
 90 |       const arrayNode = node as any;
 91 |       arrayNode.expressions.forEach((expr: any) => {
 92 |         if (
 93 |           expr.constructor.name === 'LiteralPrimitive' &&
 94 |           typeof expr.value === 'string'
 95 |         ) {
 96 |           const classNames = parseClassNames(expr.value);
 97 |           classNames.forEach((className: string) => {
 98 |             if (targetSet.has(className)) {
 99 |               foundClasses.push(className);
100 |             }
101 |           });
102 |         }
103 |         visitAST(expr);
104 |       });
105 |     }
106 |     // Handle object literals: { 'class1': true, 'class2': condition }
107 |     else if (nodeType === 'LiteralMap' && 'keys' in node && 'values' in node) {
108 |       const mapNode = node as any;
109 |       mapNode.keys.forEach((key: any, index: number) => {
110 |         // Handle the key structure: { key: "className", quoted: true }
111 |         if (key && typeof key.key === 'string') {
112 |           const classNames = parseClassNames(key.key);
113 |           classNames.forEach((className: string) => {
114 |             if (targetSet.has(className)) {
115 |               foundClasses.push(className);
116 |             }
117 |           });
118 |         }
119 |         // Visit the value expression but don't extract classes from it
120 |         // (e.g., in { 'card': option?.logo?.toLowerCase() === 'card' })
121 |         // we don't want to extract 'card' from the comparison
122 |         visitAST(mapNode.values[index]);
123 |       });
124 |     }
125 |     // Handle string literals: 'class1 class2'
126 |     else if (
127 |       nodeType === 'LiteralPrimitive' &&
128 |       'value' in node &&
129 |       typeof (node as any).value === 'string'
130 |     ) {
131 |       const primitiveNode = node as any;
132 |       const classNames = parseClassNames(primitiveNode.value);
133 |       classNames.forEach((className: string) => {
134 |         if (targetSet.has(className)) {
135 |           foundClasses.push(className);
136 |         }
137 |       });
138 |     }
139 |     // Handle interpolation: "static {{ dynamic }} static"
140 |     else if (
141 |       nodeType === 'Interpolation' &&
142 |       'strings' in node &&
143 |       'expressions' in node
144 |     ) {
145 |       const interpolationNode = node as any;
146 |       // Extract class names from static string parts only
147 |       // Don't process the expressions to avoid false positives
148 |       interpolationNode.strings.forEach((str: string) => {
149 |         if (str && str.trim()) {
150 |           const classNames = parseClassNames(str);
151 |           classNames.forEach((className: string) => {
152 |             if (targetSet.has(className)) {
153 |               foundClasses.push(className);
154 |             }
155 |           });
156 |         }
157 |       });
158 |       // Note: We intentionally don't visit the expressions to avoid false positives
159 |       // from dynamic expressions like {{ someCondition ? 'card' : 'other' }}
160 |     }
161 |     // Handle ternary expressions: condition ? 'class1' : 'class2'
162 |     else if (
163 |       nodeType === 'Conditional' &&
164 |       'trueExp' in node &&
165 |       'falseExp' in node
166 |     ) {
167 |       const conditionalNode = node as any;
168 |       // Don't visit the condition (to avoid false positives from comparisons)
169 |       visitAST(conditionalNode.trueExp);
170 |       visitAST(conditionalNode.falseExp);
171 |     }
172 |     // Handle binary expressions (avoid extracting from comparisons)
173 |     else if (nodeType === 'Binary') {
174 |       // For binary expressions like comparisons, we generally don't want to extract
175 |       // class names from them to avoid false positives like 'card' in "option?.logo === 'card'"
176 |       return;
177 |     }
178 |     // Handle property access: object.property
179 |     else if (
180 |       (nodeType === 'PropertyRead' || nodeType === 'SafePropertyRead') &&
181 |       'receiver' in node
182 |     ) {
183 |       const propertyNode = node as any;
184 |       visitAST(propertyNode.receiver);
185 |       // Don't extract from property names
186 |     }
187 |     // Handle keyed access: object[key]
188 |     else if (
189 |       (nodeType === 'KeyedRead' || nodeType === 'SafeKeyedRead') &&
190 |       'receiver' in node &&
191 |       'key' in node
192 |     ) {
193 |       const keyedNode = node as any;
194 |       visitAST(keyedNode.receiver);
195 |       visitAST(keyedNode.key);
196 |     }
197 |     // Handle function calls: func(args)
198 |     else if (
199 |       (nodeType === 'Call' || nodeType === 'SafeCall') &&
200 |       'receiver' in node &&
201 |       'args' in node
202 |     ) {
203 |       const callNode = node as any;
204 |       visitAST(callNode.receiver);
205 |       callNode.args.forEach((arg: any) => visitAST(arg));
206 |     }
207 |     // Handle prefix not: !expression
208 |     else if (nodeType === 'PrefixNot' && 'expression' in node) {
209 |       const prefixNode = node as any;
210 |       visitAST(prefixNode.expression);
211 |     } else {
212 |       const anyNode = node as any;
213 |       if (anyNode.expressions && Array.isArray(anyNode.expressions)) {
214 |         anyNode.expressions.forEach((expr: any) => visitAST(expr));
215 |       }
216 |       if (anyNode.receiver) {
217 |         visitAST(anyNode.receiver);
218 |       }
219 |       if (anyNode.args && Array.isArray(anyNode.args)) {
220 |         anyNode.args.forEach((arg: any) => visitAST(arg));
221 |       }
222 |       if (anyNode.left) {
223 |         visitAST(anyNode.left);
224 |       }
225 |       if (anyNode.right) {
226 |         visitAST(anyNode.right);
227 |       }
228 |     }
229 |   }
230 | 
231 |   visitAST(ast);
232 |   return Array.from(new Set(foundClasses));
233 | }
234 | 
235 | export function ngClassContainsClass(
236 |   astWithSource: ASTWithSource,
237 |   className: string,
238 | ): boolean {
239 |   const foundClasses = extractClassNamesFromNgClassAST(astWithSource.ast, [
240 |     className,
241 |   ]);
242 |   return foundClasses.includes(className);
243 | }
244 | 
245 | /**
246 |  * Check if a class name exists in an ngClass expression string
247 |  * This is a simplified regex-based implementation for backward compatibility
248 |  * For more accurate AST-based parsing, use extractClassNamesFromNgClassAST directly
249 |  *
250 |  * @param source The ngClass expression source string
251 |  * @param className The class name to search for
252 |  * @returns true if the class name is found in the expression
253 |  */
254 | export function ngClassesIncludeClassName(
255 |   source: string,
256 |   className: string,
257 | ): boolean {
258 |   const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
259 |   const boundary = '[\\w$-]';
260 |   const regex = new RegExp(`(?<!${boundary})${escaped}(?!${boundary})`);
261 | 
262 |   return regex.test(source);
263 | }
264 | 
```

--------------------------------------------------------------------------------
/packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/first-case/dashboard-header.component.scss:
--------------------------------------------------------------------------------

```scss
  1 | // Dashboard Header Component Styles
  2 | .dashboard-header {
  3 |   display: flex;
  4 |   align-items: center;
  5 |   justify-content: space-between;
  6 |   padding: 0.75rem 1.5rem;
  7 |   background: #ffffff;
  8 |   border-bottom: 1px solid #e5e7eb;
  9 |   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
 10 |   position: relative;
 11 |   z-index: 100;
 12 |   min-height: 4rem;
 13 | }
 14 | 
 15 | // Header Brand Section
 16 | .header-brand {
 17 |   display: flex;
 18 |   align-items: center;
 19 |   gap: 1rem;
 20 |   flex-shrink: 0;
 21 | }
 22 | 
 23 | .brand-logo {
 24 |   display: flex;
 25 |   align-items: center;
 26 |   justify-content: center;
 27 | }
 28 | 
 29 | .logo-icon {
 30 |   width: 2rem;
 31 |   height: 2rem;
 32 |   border-radius: 0.375rem;
 33 | }
 34 | 
 35 | .brand-title {
 36 |   font-size: 1.25rem;
 37 |   font-weight: 700;
 38 |   color: #1f2937;
 39 |   margin: 0;
 40 |   line-height: 1.2;
 41 | }
 42 | 
 43 | // DsBadge Component Styling
 44 | .offer-badge-default-icon {
 45 |   font-size: 1em;
 46 |   line-height: 1;
 47 | }
 48 | 
 49 | .offer-badge-dismiss {
 50 |   background: none;
 51 |   border: none;
 52 |   color: currentColor;
 53 |   cursor: pointer;
 54 |   padding: 0;
 55 |   margin-left: 0.25rem;
 56 |   font-size: 1.125rem;
 57 |   line-height: 1;
 58 |   opacity: 0.7;
 59 |   transition: opacity 0.2s ease;
 60 |   
 61 |   &:hover {
 62 |     opacity: 1;
 63 |   }
 64 | }
 65 | 
 66 | // Header Search Section
 67 | .header-search {
 68 |   flex: 1;
 69 |   max-width: 32rem;
 70 |   margin: 0 2rem;
 71 |   position: relative;
 72 | }
 73 | 
 74 | .search-container {
 75 |   position: relative;
 76 |   display: flex;
 77 |   align-items: center;
 78 |   background: #f9fafb;
 79 |   border: 1px solid #d1d5db;
 80 |   border-radius: 0.5rem;
 81 |   padding: 0 0.75rem;
 82 |   transition: all 0.2s ease;
 83 |   
 84 |   &.search-focused {
 85 |     border-color: #3b82f6;
 86 |     box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
 87 |     background: white;
 88 |   }
 89 | }
 90 | 
 91 | .search-icon {
 92 |   color: #6b7280;
 93 |   flex-shrink: 0;
 94 |   margin-right: 0.5rem;
 95 | }
 96 | 
 97 | .search-input {
 98 |   flex: 1;
 99 |   border: none;
100 |   background: transparent;
101 |   padding: 0.75rem 0;
102 |   font-size: 0.875rem;
103 |   color: #1f2937;
104 |   outline: none;
105 |   
106 |   &::placeholder {
107 |     color: #9ca3af;
108 |   }
109 |   
110 |   &:disabled {
111 |     opacity: 0.5;
112 |     cursor: not-allowed;
113 |   }
114 | }
115 | 
116 | .search-clear {
117 |   background: none;
118 |   border: none;
119 |   color: #6b7280;
120 |   cursor: pointer;
121 |   padding: 0.25rem;
122 |   margin-left: 0.5rem;
123 |   font-size: 1.125rem;
124 |   line-height: 1;
125 |   border-radius: 0.25rem;
126 |   transition: all 0.2s ease;
127 |   
128 |   &:hover {
129 |     background: #f3f4f6;
130 |     color: #374151;
131 |   }
132 | }
133 | 
134 | .search-suggestions {
135 |   position: absolute;
136 |   top: 100%;
137 |   left: 0;
138 |   right: 0;
139 |   background: white;
140 |   border: 1px solid #d1d5db;
141 |   border-top: none;
142 |   border-radius: 0 0 0.5rem 0.5rem;
143 |   box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
144 |   z-index: 50;
145 |   max-height: 12rem;
146 |   overflow-y: auto;
147 | }
148 | 
149 | .suggestion-item {
150 |   display: block;
151 |   width: 100%;
152 |   padding: 0.75rem;
153 |   border: none;
154 |   background: none;
155 |   text-align: left;
156 |   font-size: 0.875rem;
157 |   color: #374151;
158 |   cursor: pointer;
159 |   transition: background-color 0.2s ease;
160 |   
161 |   &:hover {
162 |     background: #f9fafb;
163 |   }
164 |   
165 |   &:not(:last-child) {
166 |     border-bottom: 1px solid #f3f4f6;
167 |   }
168 | }
169 | 
170 | // Header Actions Section
171 | .header-actions {
172 |   display: flex;
173 |   align-items: center;
174 |   gap: 0.5rem;
175 |   flex-shrink: 0;
176 | }
177 | 
178 | .action-item {
179 |   position: relative;
180 | }
181 | 
182 | .action-button {
183 |   display: flex;
184 |   align-items: center;
185 |   gap: 0.5rem;
186 |   padding: 0.5rem;
187 |   border: none;
188 |   background: none;
189 |   border-radius: 0.5rem;
190 |   cursor: pointer;
191 |   transition: all 0.2s ease;
192 |   color: #6b7280;
193 |   
194 |   &:hover {
195 |     background: #f3f4f6;
196 |     color: #374151;
197 |   }
198 | }
199 | 
200 | // Notifications
201 | .notification-button {
202 |   position: relative;
203 |   
204 |   &.has-notifications {
205 |     color: #3b82f6;
206 |   }
207 | }
208 | 
209 | .notification-icon {
210 |   width: 1.5rem;
211 |   height: 1.5rem;
212 | }
213 | 
214 | .notification-badge {
215 |   position: absolute;
216 |   top: -0.25rem;
217 |   right: -0.25rem;
218 |   background: #ef4444;
219 |   color: white;
220 |   font-size: 0.625rem;
221 |   font-weight: 600;
222 |   padding: 0.125rem 0.375rem;
223 |   border-radius: 9999px;
224 |   min-width: 1.125rem;
225 |   text-align: center;
226 |   line-height: 1;
227 | }
228 | 
229 | .notifications-dropdown {
230 |   position: absolute;
231 |   top: 100%;
232 |   right: 0;
233 |   width: 20rem;
234 |   background: white;
235 |   border: 1px solid #d1d5db;
236 |   border-radius: 0.5rem;
237 |   box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
238 |   z-index: 50;
239 |   margin-top: 0.5rem;
240 |   max-height: 24rem;
241 |   overflow: hidden;
242 | }
243 | 
244 | .dropdown-header {
245 |   display: flex;
246 |   align-items: center;
247 |   justify-content: space-between;
248 |   padding: 1rem;
249 |   border-bottom: 1px solid #f3f4f6;
250 |   
251 |   h3 {
252 |     margin: 0;
253 |     font-size: 1rem;
254 |     font-weight: 600;
255 |     color: #1f2937;
256 |   }
257 | }
258 | 
259 | .mark-all-read {
260 |   background: none;
261 |   border: none;
262 |   color: #3b82f6;
263 |   font-size: 0.875rem;
264 |   cursor: pointer;
265 |   padding: 0.25rem 0.5rem;
266 |   border-radius: 0.25rem;
267 |   transition: background-color 0.2s ease;
268 |   
269 |   &:hover {
270 |     background: #f3f4f6;
271 |   }
272 | }
273 | 
274 | .notifications-list {
275 |   max-height: 18rem;
276 |   overflow-y: auto;
277 | }
278 | 
279 | .notification-item {
280 |   display: flex;
281 |   align-items: flex-start;
282 |   padding: 0.75rem 1rem;
283 |   border-bottom: 1px solid #f9fafb;
284 |   transition: background-color 0.2s ease;
285 |   
286 |   &:hover {
287 |     background: #f9fafb;
288 |   }
289 |   
290 |   &.notification-unread {
291 |     background: #eff6ff;
292 |     border-left: 3px solid #3b82f6;
293 |   }
294 |   
295 |   &.notification-error {
296 |     border-left-color: #ef4444;
297 |   }
298 |   
299 |   &.notification-warning {
300 |     border-left-color: #f59e0b;
301 |   }
302 |   
303 |   &.notification-success {
304 |     border-left-color: #10b981;
305 |   }
306 | }
307 | 
308 | .notification-content {
309 |   flex: 1;
310 |   margin-right: 0.5rem;
311 | }
312 | 
313 | .notification-title {
314 |   margin: 0 0 0.25rem 0;
315 |   font-size: 0.875rem;
316 |   font-weight: 600;
317 |   color: #1f2937;
318 |   line-height: 1.25;
319 | }
320 | 
321 | .notification-message {
322 |   margin: 0 0 0.5rem 0;
323 |   font-size: 0.75rem;
324 |   color: #6b7280;
325 |   line-height: 1.4;
326 | }
327 | 
328 | .notification-time {
329 |   font-size: 0.625rem;
330 |   color: #9ca3af;
331 | }
332 | 
333 | .notification-dismiss {
334 |   background: none;
335 |   border: none;
336 |   color: #9ca3af;
337 |   cursor: pointer;
338 |   padding: 0.25rem;
339 |   font-size: 1rem;
340 |   line-height: 1;
341 |   border-radius: 0.25rem;
342 |   transition: all 0.2s ease;
343 |   
344 |   &:hover {
345 |     background: #f3f4f6;
346 |     color: #6b7280;
347 |   }
348 | }
349 | 
350 | .no-notifications {
351 |   padding: 2rem 1rem;
352 |   text-align: center;
353 |   color: #9ca3af;
354 |   font-size: 0.875rem;
355 |   
356 |   p {
357 |     margin: 0;
358 |   }
359 | }
360 | 
361 | // User Menu
362 | .user-button {
363 |   padding: 0.375rem 0.75rem;
364 |   gap: 0.5rem;
365 | }
366 | 
367 | .user-avatar {
368 |   width: 2rem;
369 |   height: 2rem;
370 |   border-radius: 50%;
371 |   object-fit: cover;
372 | }
373 | 
374 | .user-avatar-placeholder {
375 |   width: 2rem;
376 |   height: 2rem;
377 |   border-radius: 50%;
378 |   background: #3b82f6;
379 |   color: white;
380 |   display: flex;
381 |   align-items: center;
382 |   justify-content: center;
383 |   font-size: 0.75rem;
384 |   font-weight: 600;
385 | }
386 | 
387 | .dropdown-arrow {
388 |   color: #9ca3af;
389 |   transition: transform 0.2s ease;
390 | }
391 | 
392 | .user-dropdown {
393 |   position: absolute;
394 |   top: 100%;
395 |   right: 0;
396 |   width: 16rem;
397 |   background: white;
398 |   border: 1px solid #d1d5db;
399 |   border-radius: 0.5rem;
400 |   box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
401 |   z-index: 50;
402 |   margin-top: 0.5rem;
403 |   overflow: hidden;
404 | }
405 | 
406 | .user-info {
407 |   padding: 1rem;
408 |   border-bottom: 1px solid #f3f4f6;
409 | }
410 | 
411 | .user-details {
412 |   h4 {
413 |     margin: 0 0 0.25rem 0;
414 |     font-size: 0.875rem;
415 |     font-weight: 600;
416 |     color: #1f2937;
417 |   }
418 |   
419 |   p {
420 |     margin: 0 0 0.5rem 0;
421 |     font-size: 0.75rem;
422 |     color: #6b7280;
423 |   }
424 | }
425 | 
426 | .user-role {
427 |   display: inline-block;
428 |   padding: 0.125rem 0.5rem;
429 |   background: #f3f4f6;
430 |   color: #6b7280;
431 |   font-size: 0.625rem;
432 |   font-weight: 500;
433 |   border-radius: 9999px;
434 |   text-transform: uppercase;
435 |   letter-spacing: 0.05em;
436 | }
437 | 
438 | .user-actions {
439 |   padding: 0.5rem 0;
440 | }
441 | 
442 | .dropdown-item {
443 |   display: block;
444 |   width: 100%;
445 |   padding: 0.5rem 1rem;
446 |   border: none;
447 |   background: none;
448 |   text-align: left;
449 |   font-size: 0.875rem;
450 |   color: #374151;
451 |   cursor: pointer;
452 |   transition: background-color 0.2s ease;
453 |   
454 |   &:hover {
455 |     background: #f9fafb;
456 |   }
457 |   
458 |   &.logout {
459 |     color: #ef4444;
460 |     
461 |     &:hover {
462 |       background: #fef2f2;
463 |     }
464 |   }
465 | }
466 | 
467 | .dropdown-divider {
468 |   margin: 0.5rem 0;
469 |   border: none;
470 |   border-top: 1px solid #f3f4f6;
471 | }
472 | 
473 | 
474 | 
475 | // Dark mode support
476 | @media (prefers-color-scheme: dark) {
477 |   .dashboard-header {
478 |     background: #1f2937;
479 |     border-bottom-color: #374151;
480 |   }
481 |   
482 |   .brand-title {
483 |     color: #f9fafb;
484 |   }
485 |   
486 |   .search-container {
487 |     background: #374151;
488 |     border-color: #4b5563;
489 |     
490 |     &.search-focused {
491 |       background: #4b5563;
492 |       border-color: #3b82f6;
493 |     }
494 |   }
495 |   
496 |   .search-input {
497 |     color: #f9fafb;
498 |     
499 |     &::placeholder {
500 |       color: #9ca3af;
501 |     }
502 |   }
503 |   
504 |   .search-suggestions,
505 |   .notifications-dropdown,
506 |   .user-dropdown {
507 |     background: #374151;
508 |     border-color: #4b5563;
509 |   }
510 |   
511 |   .dropdown-header h3,
512 |   .notification-title,
513 |   .user-details h4 {
514 |     color: #f9fafb;
515 |   }
516 |   
517 |   .dropdown-item {
518 |     color: #d1d5db;
519 |     
520 |     &:hover {
521 |       background: #4b5563;
522 |     }
523 |   }
524 | }
525 | 
526 | // Responsive design
527 | @media (max-width: 768px) {
528 |   .dashboard-header {
529 |     padding: 0.5rem 1rem;
530 |     flex-wrap: wrap;
531 |     gap: 0.5rem;
532 |   }
533 |   
534 |   .header-search {
535 |     order: 3;
536 |     flex-basis: 100%;
537 |     margin: 0;
538 |     max-width: none;
539 |   }
540 |   
541 |   .brand-title {
542 |     font-size: 1rem;
543 |   }
544 |   
545 |   .notifications-dropdown,
546 |   .user-dropdown {
547 |     width: 16rem;
548 |     right: -4rem;
549 |   }
550 | } 
```

--------------------------------------------------------------------------------
/packages/minimal-repo/packages/application/src/app/styles/extended-deprecated-styles.scss:
--------------------------------------------------------------------------------

```scss
  1 | // Extended stylesheet with nested and combined selectors for deprecated classes
  2 | // This file demonstrates various ways deprecated classes might be used in real applications
  3 | 
  4 | // Nested selectors for badge-related deprecated classes
  5 | .container {
  6 |   .pill-with-badge {
  7 |     color: red;
  8 |     border: 1px solid #ccc;
  9 |     padding: 5px 10px;
 10 |     border-radius: 15px;
 11 |     display: inline-block;
 12 | 
 13 |     &:hover {
 14 |       background-color: #f0f0f0;
 15 |     }
 16 | 
 17 |     &.active {
 18 |       background-color: #e0e0e0;
 19 |     }
 20 |   }
 21 | 
 22 |   .pill-with-badge-v2 {
 23 |     color: blue;
 24 |     border: 2px solid #aaa;
 25 |     padding: 6px 12px;
 26 |     border-radius: 20px;
 27 |     display: inline-block;
 28 | 
 29 |     &.large {
 30 |       padding: 8px 16px;
 31 |     }
 32 |   }
 33 | 
 34 |   .sports-pill {
 35 |     color: green;
 36 |     background-color: #f0f0f0;
 37 |     padding: 8px 16px;
 38 |     border-radius: 25px;
 39 |     display: inline-block;
 40 | 
 41 |     &.highlighted {
 42 |       box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
 43 |     }
 44 |   }
 45 | 
 46 |   .offer-badge {
 47 |     color: yellow;
 48 |     background-color: #333;
 49 |     padding: 4px 8px;
 50 |     border-radius: 10px;
 51 |     display: inline-block;
 52 | 
 53 |     &.urgent {
 54 |       animation: pulse 1s infinite;
 55 |     }
 56 |   }
 57 | }
 58 | 
 59 | // Combined selectors for navigation deprecated classes
 60 | .navigation-wrapper {
 61 |   .tab-nav,
 62 |   .nav-tabs {
 63 |     padding: 10px;
 64 |     border-bottom: 2px solid #ddd;
 65 | 
 66 |     .tab-nav-item {
 67 |       color: pink;
 68 |       padding: 10px 15px;
 69 |       border-radius: 5px;
 70 |       display: inline-block;
 71 |       cursor: pointer;
 72 | 
 73 |       &:hover {
 74 |         background-color: #f5f5f5;
 75 |       }
 76 | 
 77 |       &.active {
 78 |         background-color: #007bff;
 79 |         color: white;
 80 |       }
 81 |     }
 82 |   }
 83 | 
 84 |   .tab-nav {
 85 |     color: orange;
 86 |     background-color: #fff;
 87 |   }
 88 | 
 89 |   .nav-tabs {
 90 |     color: purple;
 91 |     background-color: #eee;
 92 |   }
 93 | }
 94 | 
 95 | // Nested button selectors
 96 | .button-group {
 97 |   .btn {
 98 |     color: brown;
 99 |     background-color: #f5f5f5;
100 |     padding: 10px 20px;
101 |     border: none;
102 |     border-radius: 5px;
103 |     cursor: pointer;
104 | 
105 |     &:hover {
106 |       background-color: #e0e0e0;
107 |     }
108 | 
109 |     &:disabled {
110 |       opacity: 0.6;
111 |       cursor: not-allowed;
112 |     }
113 |   }
114 | 
115 |   .btn-primary {
116 |     color: cyan;
117 |     background-color: #007bff;
118 |     padding: 10px 20px;
119 |     border: none;
120 |     border-radius: 5px;
121 |     cursor: pointer;
122 | 
123 |     &:hover {
124 |       background-color: #0056b3;
125 |     }
126 | 
127 |     &.loading {
128 |       position: relative;
129 | 
130 |       &::after {
131 |         content: '';
132 |         position: absolute;
133 |         width: 16px;
134 |         height: 16px;
135 |         border: 2px solid transparent;
136 |         border-top: 2px solid currentColor;
137 |         border-radius: 50%;
138 |         animation: spin 1s linear infinite;
139 |       }
140 |     }
141 |   }
142 | 
143 |   .legacy-button {
144 |     color: magenta;
145 |     background-color: #f8f9fa;
146 |     padding: 10px 20px;
147 |     border: 1px solid #ccc;
148 |     border-radius: 5px;
149 |     cursor: pointer;
150 | 
151 |     &.deprecated-style {
152 |       border-style: dashed;
153 |     }
154 |   }
155 | }
156 | 
157 | // Modal and card combinations
158 | .content-area {
159 |   .modal {
160 |     color: lime;
161 |     background-color: #fff;
162 |     padding: 20px;
163 |     border-radius: 10px;
164 |     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
165 | 
166 |     .card {
167 |       color: olive;
168 |       background-color: #f8f9fa;
169 |       padding: 15px;
170 |       border-radius: 5px;
171 |       box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
172 |       margin-bottom: 15px;
173 | 
174 |       &:last-child {
175 |         margin-bottom: 0;
176 |       }
177 |     }
178 | 
179 |     &.with-loading {
180 |       .loading,
181 |       .loading-v2,
182 |       .loading-v3 {
183 |         display: flex;
184 |         align-items: center;
185 |         justify-content: center;
186 |         margin: 20px 0;
187 |       }
188 | 
189 |       .loading {
190 |         color: teal;
191 |         font-size: 16px;
192 |       }
193 | 
194 |       .loading-v2 {
195 |         color: navy;
196 |         font-size: 18px;
197 |       }
198 | 
199 |       .loading-v3 {
200 |         color: maroon;
201 |         font-size: 20px;
202 |       }
203 |     }
204 |   }
205 | }
206 | 
207 | // Complex nested form controls
208 | .form-section {
209 |   .form-control-tabs-segmented,
210 |   .form-control-tabs-segmented-v2,
211 |   .form-control-tabs-segmented-v3,
212 |   .form-control-tabs-segmented-v4,
213 |   .form-control-tabs-segmented-flex,
214 |   .form-control-tabs-segmented-v2-dark {
215 |     padding: 10px;
216 |     border-radius: 5px;
217 |     display: flex;
218 |     justify-content: space-between;
219 |     margin-bottom: 15px;
220 | 
221 |     &.with-custom-controls {
222 |       .custom-control-checkbox,
223 |       .custom-control-radio,
224 |       .custom-control-switcher {
225 |         display: flex;
226 |         align-items: center;
227 |         margin-right: 15px;
228 | 
229 |         &:last-child {
230 |           margin-right: 0;
231 |         }
232 |       }
233 |     }
234 |   }
235 | 
236 |   .form-control-tabs-segmented {
237 |     color: wheat;
238 |     background-color: #fff;
239 |   }
240 | 
241 |   .form-control-tabs-segmented-v2 {
242 |     color: salmon;
243 |     background-color: #fff;
244 |   }
245 | 
246 |   .form-control-tabs-segmented-v3 {
247 |     color: turquoise;
248 |     background-color: #fff;
249 |   }
250 | 
251 |   .form-control-tabs-segmented-v4 {
252 |     color: violet;
253 |     background-color: #f8f9fa;
254 |   }
255 | 
256 |   .form-control-tabs-segmented-flex {
257 |     color: sienna;
258 |     background-color: #f8f9fa;
259 |   }
260 | 
261 |   .form-control-tabs-segmented-v2-dark {
262 |     color: tan;
263 |     background-color: #333;
264 |   }
265 | }
266 | 
267 | // Utility classes with nested selectors
268 | .utility-section {
269 |   .collapsible-container {
270 |     color: silver;
271 |     background-color: #f0f0f0;
272 |     padding: 10px;
273 |     border-radius: 5px;
274 |     overflow: hidden;
275 | 
276 |     &.expanded {
277 |       max-height: none;
278 |     }
279 | 
280 |     &.collapsed {
281 |       max-height: 50px;
282 |     }
283 | 
284 |     .divider {
285 |       color: gray;
286 |       border-top: 1px solid #ccc;
287 |       margin: 10px 0;
288 | 
289 |       &.thick {
290 |         border-top-width: 2px;
291 |       }
292 |     }
293 |   }
294 | 
295 |   .count,
296 |   .badge-circle {
297 |     padding: 5px 10px;
298 |     border-radius: 50%;
299 |     display: inline-block;
300 | 
301 |     &.small {
302 |       padding: 3px 6px;
303 |       font-size: 12px;
304 |     }
305 | 
306 |     &.large {
307 |       padding: 8px 16px;
308 |       font-size: 18px;
309 |     }
310 |   }
311 | 
312 |   .count {
313 |     color: gold;
314 |     background-color: #333;
315 |   }
316 | 
317 |   .badge-circle {
318 |     color: coral;
319 |     background-color: #f0f0f0;
320 |   }
321 | }
322 | 
323 | // Random classes with various combinations
324 | .random-section {
325 |   @for $i from 1 through 50 {
326 |     .random-class-#{$i} {
327 |       background-color: lighten(#000, $i * 2%);
328 | 
329 |       &:hover {
330 |         background-color: lighten(#000, ($i * 2% + 10%));
331 |       }
332 | 
333 |       &.active {
334 |         background-color: darken(#000, $i * 1%);
335 |       }
336 | 
337 |       // Nested combinations
338 |       .pill-with-badge,
339 |       .offer-badge {
340 |         margin: 5px;
341 | 
342 |         &.inline {
343 |           display: inline-block;
344 |         }
345 |       }
346 | 
347 |       .btn,
348 |       .btn-primary,
349 |       .legacy-button {
350 |         margin-right: 10px;
351 | 
352 |         &:last-child {
353 |           margin-right: 0;
354 |         }
355 |       }
356 |     }
357 |   }
358 | }
359 | 
360 | // Complex multi-level nesting
361 | .complex-layout {
362 |   .header {
363 |     .nav-tabs {
364 |       .tab-nav-item {
365 |         .pill-with-badge {
366 |           font-size: 12px;
367 | 
368 |           &.notification {
369 |             .count {
370 |               position: absolute;
371 |               top: -5px;
372 |               right: -5px;
373 |             }
374 |           }
375 |         }
376 |       }
377 |     }
378 |   }
379 | 
380 |   .main-content {
381 |     .modal {
382 |       .card {
383 |         .form-control-tabs-segmented {
384 |           .custom-control-checkbox {
385 |             .loading {
386 |               margin-left: 10px;
387 |             }
388 |           }
389 |         }
390 |       }
391 |     }
392 |   }
393 | 
394 |   .sidebar {
395 |     .collapsible-container {
396 |       .sports-pill,
397 |       .offer-badge {
398 |         display: block;
399 |         margin-bottom: 5px;
400 | 
401 |         &:hover {
402 |           .badge-circle {
403 |             transform: scale(1.1);
404 |           }
405 |         }
406 |       }
407 |     }
408 |   }
409 | }
410 | 
411 | // Media query combinations
412 | @media (max-width: 768px) {
413 |   .mobile-specific {
414 |     .pill-with-badge,
415 |     .pill-with-badge-v2,
416 |     .sports-pill,
417 |     .offer-badge {
418 |       display: block;
419 |       width: 100%;
420 |       text-align: center;
421 |       margin-bottom: 10px;
422 |     }
423 | 
424 |     .btn,
425 |     .btn-primary,
426 |     .legacy-button {
427 |       width: 100%;
428 |       margin-bottom: 10px;
429 |     }
430 | 
431 |     .form-control-tabs-segmented,
432 |     .form-control-tabs-segmented-v2,
433 |     .form-control-tabs-segmented-v3,
434 |     .form-control-tabs-segmented-v4 {
435 |       flex-direction: column;
436 | 
437 |       .custom-control-checkbox,
438 |       .custom-control-radio,
439 |       .custom-control-switcher {
440 |         margin-bottom: 10px;
441 |       }
442 |     }
443 |   }
444 | }
445 | 
446 | // Keyframe animations for deprecated classes
447 | @keyframes pulse {
448 |   0% {
449 |     transform: scale(1);
450 |   }
451 |   50% {
452 |     transform: scale(1.05);
453 |   }
454 |   100% {
455 |     transform: scale(1);
456 |   }
457 | }
458 | 
459 | @keyframes spin {
460 |   0% {
461 |     transform: rotate(0deg);
462 |   }
463 |   100% {
464 |     transform: rotate(360deg);
465 |   }
466 | }
467 | 
468 | @keyframes fadeIn {
469 |   0% {
470 |     opacity: 0;
471 |   }
472 |   100% {
473 |     opacity: 1;
474 |   }
475 | }
476 | 
477 | // Pseudo-element combinations
478 | .enhanced-deprecated {
479 |   .pill-with-badge::before,
480 |   .sports-pill::before,
481 |   .offer-badge::before {
482 |     content: '⚠️';
483 |     margin-right: 5px;
484 |   }
485 | 
486 |   .btn::after,
487 |   .btn-primary::after,
488 |   .legacy-button::after {
489 |     content: '';
490 |     position: absolute;
491 |     bottom: 0;
492 |     left: 0;
493 |     width: 100%;
494 |     height: 2px;
495 |     background: linear-gradient(90deg, transparent, currentColor, transparent);
496 |     opacity: 0;
497 |     transition: opacity 0.3s;
498 |   }
499 | 
500 |   .btn:hover::after,
501 |   .btn-primary:hover::after,
502 |   .legacy-button:hover::after {
503 |     opacity: 1;
504 |   }
505 | }
506 | 
```

--------------------------------------------------------------------------------
/packages/angular-mcp-server/src/lib/angular-mcp-server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { PROMPTS, PROMPTS_IMPL } from './prompts/prompt-registry.js';
  2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
  3 | import {
  4 |   CallToolRequest,
  5 |   CallToolRequestSchema,
  6 |   GetPromptRequestSchema,
  7 |   GetPromptResult,
  8 |   ListPromptsRequestSchema,
  9 |   ListPromptsResult,
 10 |   ListResourcesRequestSchema,
 11 |   ListResourcesResult,
 12 |   ListToolsRequestSchema,
 13 | } from '@modelcontextprotocol/sdk/types.js';
 14 | import { TOOLS } from './tools/tools.js';
 15 | import { toolNotFound } from './tools/utils.js';
 16 | import * as fs from 'node:fs';
 17 | import * as path from 'node:path';
 18 | import {
 19 |   AngularMcpServerOptionsSchema,
 20 |   AngularMcpServerOptions,
 21 | } from './validation/angular-mcp-server-options.schema.js';
 22 | import { validateAngularMcpServerFilesExist } from './validation/file-existence.js';
 23 | import { validateDeprecatedCssClassesFile } from './validation/ds-components-file.validation.js';
 24 | 
 25 | export class AngularMcpServerWrapper {
 26 |   private readonly mcpServer: McpServer;
 27 |   private readonly workspaceRoot: string;
 28 |   private readonly storybookDocsRoot?: string;
 29 |   private readonly deprecatedCssClassesPath?: string;
 30 |   private readonly uiRoot: string;
 31 | 
 32 |   /**
 33 |    * Private constructor - use AngularMcpServerWrapper.create() instead.
 34 |    * Config is already validated when this constructor is called.
 35 |    */
 36 |   private constructor(config: AngularMcpServerOptions) {
 37 |     // Config is already validated, no need to validate again
 38 |     const { workspaceRoot, ds } = config;
 39 | 
 40 |     this.workspaceRoot = workspaceRoot;
 41 |     this.storybookDocsRoot = ds.storybookDocsRoot;
 42 |     this.deprecatedCssClassesPath = ds.deprecatedCssClassesPath;
 43 |     this.uiRoot = ds.uiRoot;
 44 | 
 45 |     this.mcpServer = new McpServer({
 46 |       name: 'Angular MCP',
 47 |       version: '0.0.0',
 48 |     });
 49 | 
 50 |     this.mcpServer.server.registerCapabilities({
 51 |       prompts: {},
 52 |       tools: {},
 53 |       resources: {},
 54 |     });
 55 |     this.registerPrompts();
 56 |     this.registerTools();
 57 |     this.registerResources();
 58 |   }
 59 | 
 60 |   /**
 61 |    * Creates and validates an AngularMcpServerWrapper instance.
 62 |    * This is the recommended way to create an instance as it performs all necessary validations.
 63 |    *
 64 |    * @param config - The Angular MCP server configuration options
 65 |    * @returns A Promise that resolves to a fully configured AngularMcpServerWrapper instance
 66 |    * @throws {Error} If configuration validation fails or required files don't exist
 67 |    */
 68 |   static async create(
 69 |     config: AngularMcpServerOptions,
 70 |   ): Promise<AngularMcpServerWrapper> {
 71 |     // Validate config using the Zod schema - only once here
 72 |     const validatedConfig = AngularMcpServerOptionsSchema.parse(config);
 73 | 
 74 |     // Validate file existence (optional keys are checked only when provided)
 75 |     validateAngularMcpServerFilesExist(validatedConfig);
 76 | 
 77 |     // Load and validate deprecatedCssClassesPath content only if provided
 78 |     if (validatedConfig.ds.deprecatedCssClassesPath) {
 79 |       await validateDeprecatedCssClassesFile(validatedConfig);
 80 |     }
 81 | 
 82 |     return new AngularMcpServerWrapper(validatedConfig);
 83 |   }
 84 | 
 85 |   getMcpServer(): McpServer {
 86 |     return this.mcpServer;
 87 |   }
 88 | 
 89 |   private registerResources() {
 90 |     this.mcpServer.server.setRequestHandler(
 91 |       ListResourcesRequestSchema,
 92 |       async (): Promise<ListResourcesResult> => {
 93 |         const resources = [];
 94 | 
 95 |         // Try to read the llms.txt file from the package root (optional)
 96 |         try {
 97 |           const filePath = path.resolve(__dirname, '../../llms.txt');
 98 | 
 99 |           // Only attempt to read if file exists
100 |           if (fs.existsSync(filePath)) {
101 |             console.log('Reading llms.txt from:', filePath);
102 |             const content = fs.readFileSync(filePath, 'utf-8');
103 |             const lines = content.split('\n');
104 | 
105 |             let currentSection = '';
106 | 
107 |             for (let i = 0; i < lines.length; i++) {
108 |               const line = lines[i].trim();
109 | 
110 |               // Skip empty lines and comments that don't start with #
111 |               if (!line || (line.startsWith('#') && !line.includes(':'))) {
112 |                 continue;
113 |               }
114 | 
115 |               // Update section if line starts with #
116 |               if (line.startsWith('# ')) {
117 |                 currentSection = line.substring(2).replace(':', '').trim();
118 |                 continue;
119 |               }
120 | 
121 |               // Parse markdown links: [name](url)
122 |               const linkMatch = line.match(/- \[(.*?)\]\((.*?)\):(.*)/);
123 |               if (linkMatch) {
124 |                 const [, name, uri, description = ''] = linkMatch;
125 |                 resources.push({
126 |                   uri,
127 |                   name: name.trim(),
128 |                   type: currentSection.toLowerCase(),
129 |                   content: description.trim() || name.trim(),
130 |                 });
131 |                 continue;
132 |               }
133 | 
134 |               // Parse simple links: - [name](url)
135 |               const simpleLinkMatch = line.match(/- \[(.*?)\]\((.*?)\)/);
136 |               if (simpleLinkMatch) {
137 |                 const [, name, uri] = simpleLinkMatch;
138 |                 resources.push({
139 |                   uri,
140 |                   name: name.trim(),
141 |                   type: currentSection.toLowerCase(),
142 |                   content: name.trim(),
143 |                 });
144 |               }
145 |             }
146 |           } else {
147 |             console.log('llms.txt not found at:', filePath, '(skipping)');
148 |           }
149 |         } catch (ctx: unknown) {
150 |           if (ctx instanceof Error) {
151 |             console.error('Error reading llms.txt (non-fatal):', ctx.message);
152 |           }
153 |         }
154 | 
155 |         // Scan available design system components to add them as discoverable resources
156 |         try {
157 |           if (this.storybookDocsRoot) {
158 |             const dsUiPath = path.resolve(
159 |               process.cwd(),
160 |               this.storybookDocsRoot,
161 |             );
162 |             if (fs.existsSync(dsUiPath)) {
163 |               const componentFolders = fs
164 |                 .readdirSync(dsUiPath, { withFileTypes: true })
165 |                 .filter((dirent) => dirent.isDirectory())
166 |                 .map((dirent) => dirent.name);
167 | 
168 |               for (const folder of componentFolders) {
169 |                 // Convert kebab-case to PascalCase with 'Ds' prefix
170 |                 const componentName =
171 |                   'Ds' +
172 |                   folder
173 |                     .split('-')
174 |                     .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
175 |                     .join('');
176 | 
177 |                 resources.push({
178 |                   uri: `ds-component://${folder}`,
179 |                   name: componentName,
180 |                   type: 'design-system-component',
181 |                   content: `Design System component: ${componentName}`,
182 |                 });
183 |               }
184 |             }
185 |           }
186 |         } catch (ctx: unknown) {
187 |           if (ctx instanceof Error) {
188 |             console.error(
189 |               'Error scanning DS components (non-fatal):',
190 |               ctx.message,
191 |             );
192 |           }
193 |         }
194 | 
195 |         return {
196 |           resources,
197 |         };
198 |       },
199 |     );
200 |   }
201 | 
202 |   private registerPrompts() {
203 |     this.mcpServer.server.setRequestHandler(
204 |       ListPromptsRequestSchema,
205 |       async (): Promise<ListPromptsResult> => {
206 |         return {
207 |           prompts: Object.values(PROMPTS),
208 |         };
209 |       },
210 |     );
211 | 
212 |     this.mcpServer.server.setRequestHandler(
213 |       GetPromptRequestSchema,
214 |       async (request): Promise<GetPromptResult> => {
215 |         const prompt = PROMPTS[request.params.name];
216 |         if (!prompt) {
217 |           throw new Error(`Prompt not found: ${request.params.name}`);
218 |         }
219 | 
220 |         const promptResult = PROMPTS_IMPL[request.params.name];
221 |         // Register all prompts
222 |         if (promptResult && promptResult.text) {
223 |           return {
224 |             messages: [
225 |               {
226 |                 role: 'user',
227 |                 content: {
228 |                   type: 'text',
229 |                   text: promptResult.text(request.params.arguments ?? {}),
230 |                 },
231 |               },
232 |             ],
233 |           };
234 |         }
235 |         throw new Error('Prompt implementation not found');
236 |       },
237 |     );
238 |   }
239 | 
240 |   private registerTools() {
241 |     this.mcpServer.server.setRequestHandler(
242 |       ListToolsRequestSchema,
243 |       async () => {
244 |         return {
245 |           tools: TOOLS.map(({ schema }) => schema),
246 |         };
247 |       },
248 |     );
249 | 
250 |     this.mcpServer.server.setRequestHandler(
251 |       CallToolRequestSchema,
252 |       async (request: CallToolRequest) => {
253 |         const tool = TOOLS.find(
254 |           ({ schema }) => request.params.name === schema.name,
255 |         );
256 | 
257 |         if (tool?.schema && tool.schema.name === request.params.name) {
258 |           return await tool.handler({
259 |             ...request,
260 |             params: {
261 |               ...request.params,
262 |               arguments: {
263 |                 ...request.params.arguments,
264 |                 storybookDocsRoot: this.storybookDocsRoot,
265 |                 deprecatedCssClassesPath: this.deprecatedCssClassesPath,
266 |                 uiRoot: this.uiRoot,
267 |                 cwd: this.workspaceRoot,
268 |                 workspaceRoot: this.workspaceRoot,
269 |               },
270 |             },
271 |           });
272 |         }
273 | 
274 |         return {
275 |           content: [toolNotFound(request)],
276 |           isError: false,
277 |         };
278 |       },
279 |     );
280 |   }
281 | }
282 | 
```

--------------------------------------------------------------------------------
/packages/angular-mcp-server/src/lib/tools/ds/component-usage-graph/utils/unified-ast-analyzer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import * as ts from 'typescript';
  4 | import { toUnixPath } from '@code-pushup/utils';
  5 | 
  6 | import {
  7 |   DependencyInfo,
  8 |   FileInfo,
  9 |   ComponentMetadata,
 10 |   FileExtension,
 11 | } from '../models/types.js';
 12 | import {
 13 |   DEPENDENCY_ANALYSIS_CONFIG,
 14 |   REGEX_PATTERNS,
 15 |   getCombinedComponentImportRegex,
 16 | } from '../models/config.js';
 17 | import { isExternal, resolveDependencyPath } from './path-resolver.js';
 18 | 
 19 | const DEP_REGEX_TABLE: Array<[RegExp, DependencyInfo['type']]> = [
 20 |   [REGEX_PATTERNS.ES6_IMPORT, 'import'],
 21 |   [REGEX_PATTERNS.COMMONJS_REQUIRE, 'require'],
 22 |   [REGEX_PATTERNS.DYNAMIC_IMPORT, 'dynamic-import'],
 23 | ];
 24 | 
 25 | const STYLE_REGEX_TABLE: Array<
 26 |   [RegExp, DependencyInfo['type'], ((p: string) => boolean)?]
 27 | > = [
 28 |   [REGEX_PATTERNS.CSS_IMPORT, 'css-import'],
 29 |   [
 30 |     REGEX_PATTERNS.CSS_URL,
 31 |     'asset',
 32 |     (u) => !u.startsWith('http') && !u.startsWith('data:'),
 33 |   ],
 34 | ];
 35 | 
 36 | export interface UnifiedAnalysisResult {
 37 |   dependencies: DependencyInfo[];
 38 |   componentMetadata?: ComponentMetadata;
 39 |   importedComponentNames: string[];
 40 |   isAngularComponent: boolean;
 41 | }
 42 | 
 43 | export async function analyzeFileWithUnifiedAST(
 44 |   filePath: string,
 45 |   basePath: string,
 46 |   componentNamesForReverseDeps?: string[],
 47 | ): Promise<UnifiedAnalysisResult> {
 48 |   const content = await fs.promises.readFile(filePath, 'utf-8');
 49 | 
 50 |   try {
 51 |     return analyzeContentWithUnifiedAST(
 52 |       content,
 53 |       filePath,
 54 |       basePath,
 55 |       componentNamesForReverseDeps,
 56 |     );
 57 |   } catch {
 58 |     return analyzeContentWithRegexFallback(
 59 |       content,
 60 |       filePath,
 61 |       basePath,
 62 |       componentNamesForReverseDeps,
 63 |     );
 64 |   }
 65 | }
 66 | 
 67 | function analyzeContentWithUnifiedAST(
 68 |   content: string,
 69 |   filePath: string,
 70 |   basePath: string,
 71 |   componentNamesForReverseDeps?: string[],
 72 | ): UnifiedAnalysisResult {
 73 |   const sourceFile = ts.createSourceFile(
 74 |     filePath,
 75 |     content,
 76 |     ts.ScriptTarget.Latest,
 77 |     true,
 78 |   );
 79 | 
 80 |   const result: UnifiedAnalysisResult = {
 81 |     dependencies: [],
 82 |     importedComponentNames: [],
 83 |     isAngularComponent: false,
 84 |   };
 85 | 
 86 |   const componentNameSet = componentNamesForReverseDeps
 87 |     ? new Set(componentNamesForReverseDeps)
 88 |     : new Set<string>();
 89 |   let componentClassName: string | undefined;
 90 | 
 91 |   const visit = (node: ts.Node): void => {
 92 |     if (ts.isImportDeclaration(node) && node.moduleSpecifier) {
 93 |       if (ts.isStringLiteral(node.moduleSpecifier)) {
 94 |         const importPath = node.moduleSpecifier.text;
 95 |         result.dependencies.push(
 96 |           createDependencyInfo(importPath, 'import', filePath, basePath),
 97 |         );
 98 | 
 99 |         if (
100 |           componentNamesForReverseDeps &&
101 |           componentNamesForReverseDeps.length > 0 &&
102 |           node.importClause
103 |         ) {
104 |           extractComponentImportsFromImportNode(
105 |             node,
106 |             componentNameSet,
107 |             result.importedComponentNames,
108 |           );
109 |         }
110 |       }
111 |     } else if (ts.isCallExpression(node)) {
112 |       if (
113 |         ts.isIdentifier(node.expression) &&
114 |         node.expression.text === 'require' &&
115 |         node.arguments.length === 1 &&
116 |         ts.isStringLiteral(node.arguments[0])
117 |       ) {
118 |         const importPath = node.arguments[0].text;
119 |         result.dependencies.push(
120 |           createDependencyInfo(importPath, 'require', filePath, basePath),
121 |         );
122 |       } else if (
123 |         node.expression.kind === ts.SyntaxKind.ImportKeyword &&
124 |         node.arguments.length === 1 &&
125 |         ts.isStringLiteral(node.arguments[0])
126 |       ) {
127 |         const importPath = node.arguments[0].text;
128 |         result.dependencies.push(
129 |           createDependencyInfo(
130 |             importPath,
131 |             'dynamic-import',
132 |             filePath,
133 |             basePath,
134 |           ),
135 |         );
136 |       }
137 |     } else if (ts.isClassDeclaration(node) && node.name) {
138 |       const hasComponentDecorator = ts
139 |         .getDecorators?.(node as ts.HasDecorators)
140 |         ?.some((decorator) => {
141 |           if (ts.isCallExpression(decorator.expression)) {
142 |             return (
143 |               ts.isIdentifier(decorator.expression.expression) &&
144 |               decorator.expression.expression.text === 'Component'
145 |             );
146 |           }
147 |           return (
148 |             ts.isIdentifier(decorator.expression) &&
149 |             decorator.expression.text === 'Component'
150 |           );
151 |         });
152 | 
153 |       if (hasComponentDecorator) {
154 |         result.isAngularComponent = true;
155 |         componentClassName = node.name.text;
156 |       }
157 |     }
158 | 
159 |     ts.forEachChild(node, visit);
160 |   };
161 | 
162 |   visit(sourceFile);
163 | 
164 |   if (result.isAngularComponent && componentClassName) {
165 |     result.componentMetadata = {
166 |       className: componentClassName,
167 |     };
168 |   }
169 | 
170 |   return result;
171 | }
172 | 
173 | function extractComponentImportsFromImportNode(
174 |   importNode: ts.ImportDeclaration,
175 |   componentNameSet: Set<string>,
176 |   foundComponents: string[],
177 | ): void {
178 |   const importClause = importNode.importClause;
179 |   if (!importClause) return;
180 | 
181 |   if (
182 |     importClause.namedBindings &&
183 |     ts.isNamedImports(importClause.namedBindings)
184 |   ) {
185 |     for (const element of importClause.namedBindings.elements) {
186 |       const importName = element.name.text;
187 |       if (componentNameSet.has(importName)) {
188 |         foundComponents.push(importName);
189 |       }
190 |     }
191 |   }
192 | 
193 |   if (importClause.name) {
194 |     const importName = importClause.name.text;
195 |     if (componentNameSet.has(importName)) {
196 |       foundComponents.push(importName);
197 |     }
198 |   }
199 | }
200 | 
201 | function analyzeContentWithRegexFallback(
202 |   content: string,
203 |   filePath: string,
204 |   basePath: string,
205 |   componentNamesForReverseDeps?: string[],
206 | ): UnifiedAnalysisResult {
207 |   const result: UnifiedAnalysisResult = {
208 |     dependencies: [],
209 |     importedComponentNames: [],
210 |     isAngularComponent: false,
211 |   };
212 | 
213 |   DEP_REGEX_TABLE.forEach(([regex, type]) => {
214 |     regex.lastIndex = 0;
215 |     let match: RegExpExecArray | null;
216 |     while ((match = regex.exec(content))) {
217 |       const importPath = match[1] || match[2];
218 |       result.dependencies.push(
219 |         createDependencyInfo(importPath, type, filePath, basePath),
220 |       );
221 |     }
222 |   });
223 | 
224 |   result.isAngularComponent =
225 |     REGEX_PATTERNS.ANGULAR_COMPONENT_DECORATOR.test(content);
226 | 
227 |   if (result.isAngularComponent) {
228 |     const classMatch = content.match(/export\s+class\s+(\w+)/);
229 |     if (classMatch) {
230 |       result.componentMetadata = {
231 |         className: classMatch[1],
232 |       };
233 |     }
234 |   }
235 | 
236 |   if (componentNamesForReverseDeps && componentNamesForReverseDeps.length > 0) {
237 |     const combinedImportRegex = getCombinedComponentImportRegex(
238 |       componentNamesForReverseDeps,
239 |     );
240 |     const matches = Array.from(content.matchAll(combinedImportRegex));
241 |     result.importedComponentNames = matches
242 |       .map((match) => match[1])
243 |       .filter(Boolean);
244 |   }
245 | 
246 |   return result;
247 | }
248 | 
249 | /**
250 |  * Enhanced version of analyzeFileOptimized that uses unified AST analysis
251 |  */
252 | export async function analyzeFileWithUnifiedOptimization(
253 |   filePath: string,
254 |   basePath: string,
255 | ): Promise<FileInfo> {
256 |   const stats = await fs.promises.stat(filePath);
257 |   const ext = path.extname(filePath);
258 | 
259 |   const { stylesExtensions, scriptExtensions } = DEPENDENCY_ANALYSIS_CONFIG;
260 | 
261 |   let dependencies: DependencyInfo[] = [];
262 |   let isAngularComponent = false;
263 |   let componentName: string | undefined;
264 | 
265 |   if (scriptExtensions.includes(ext as any)) {
266 |     const unifiedResult = await analyzeFileWithUnifiedAST(filePath, basePath);
267 |     dependencies = unifiedResult.dependencies;
268 |     isAngularComponent = unifiedResult.isAngularComponent;
269 |     componentName = unifiedResult.componentMetadata?.className;
270 |   } else if (stylesExtensions.includes(ext as any)) {
271 |     dependencies = parseStyleDependencies(
272 |       await fs.promises.readFile(filePath, 'utf-8'),
273 |       filePath,
274 |       basePath,
275 |     );
276 |   }
277 | 
278 |   return {
279 |     type:
280 |       DEPENDENCY_ANALYSIS_CONFIG.fileTypeMap[ext as FileExtension] || 'unknown',
281 |     size: stats.size,
282 |     dependencies,
283 |     lastModified: stats.mtime.getTime(),
284 |     isAngularComponent,
285 |     componentName,
286 |   };
287 | }
288 | 
289 | export async function extractComponentImportsUnified(
290 |   filePath: string,
291 |   componentNames: string[],
292 | ): Promise<string[]> {
293 |   if (componentNames.length === 0) {
294 |     return [];
295 |   }
296 | 
297 |   try {
298 |     const content = await fs.promises.readFile(filePath, 'utf-8');
299 |     const result = analyzeContentWithUnifiedAST(
300 |       content,
301 |       filePath,
302 |       '',
303 |       componentNames,
304 |     );
305 |     return Array.from(new Set(result.importedComponentNames));
306 |   } catch {
307 |     return [];
308 |   }
309 | }
310 | 
311 | function createDependencyInfo(
312 |   importPath: string,
313 |   type: DependencyInfo['type'],
314 |   filePath: string,
315 |   basePath: string,
316 | ): DependencyInfo {
317 |   if (isExternal(importPath)) {
318 |     return {
319 |       path: importPath,
320 |       type: 'external',
321 |       resolved: false,
322 |     };
323 |   }
324 | 
325 |   const resolvedPath = resolveDependencyPath(importPath, filePath, basePath);
326 | 
327 |   return {
328 |     path: importPath,
329 |     type,
330 |     resolved: resolvedPath !== null,
331 |     resolvedPath: resolvedPath ? toUnixPath(resolvedPath) : undefined,
332 |   };
333 | }
334 | 
335 | function parseStyleDependencies(
336 |   content: string,
337 |   filePath: string,
338 |   basePath: string,
339 | ): DependencyInfo[] {
340 |   const dependencies: DependencyInfo[] = [];
341 | 
342 |   STYLE_REGEX_TABLE.forEach(([regex, type, filter]) => {
343 |     regex.lastIndex = 0;
344 |     let match: RegExpExecArray | null;
345 |     while ((match = regex.exec(content))) {
346 |       const importPath = match[1] || match[2];
347 |       if (!filter || filter(importPath)) {
348 |         dependencies.push(
349 |           createDependencyInfo(importPath, type, filePath, basePath),
350 |         );
351 |       }
352 |     }
353 |   });
354 | 
355 |   return dependencies;
356 | }
357 | 
```

--------------------------------------------------------------------------------
/packages/minimal-repo/packages/design-system/storybook-host-app/src/components/segmented-control/segmented-control.component.stories.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { generateStatusBadges } from '@design-system/shared-storybook-utils';
  2 | import {
  3 |   DsSegmentedControl,
  4 |   DsSegmentedOption,
  5 | } from '@frontend/ui/segmented-control';
  6 | import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
  7 | import { expect, fireEvent, within } from '@storybook/test';
  8 | 
  9 | type StoryType = DsSegmentedControl & {
 10 |   roleType: string;
 11 |   itemMaxWidth: string;
 12 |   customLabel: string;
 13 |   twoLineTruncation: boolean;
 14 | };
 15 | 
 16 | export default {
 17 |   title: 'Components/Segmented Control',
 18 |   parameters: {
 19 |     status: generateStatusBadges('UX-2309', ['a11y', 'integration ready']),
 20 |   },
 21 |   component: DsSegmentedControl,
 22 |   args: {
 23 |     fullWidth: false,
 24 |     inverse: false,
 25 |     activeOption: '2',
 26 |     roleType: 'tablist',
 27 |     itemMaxWidth: 'auto',
 28 |     customLabel: 'customLabel',
 29 |     twoLineTruncation: false,
 30 |   },
 31 |   argTypes: {
 32 |     fullWidth: {
 33 |       type: 'boolean',
 34 |       table: {
 35 |         defaultValue: { summary: 'false' },
 36 |       },
 37 |       control: { type: 'boolean' },
 38 |       description:
 39 |         'Whether the segment should take up the full width of the container',
 40 |     },
 41 |     inverse: {
 42 |       type: 'boolean',
 43 |       table: { defaultValue: { summary: 'false' } },
 44 |       control: { type: 'boolean' },
 45 |       description: 'The inverse state of the Segmented Control',
 46 |     },
 47 |     twoLineTruncation: {
 48 |       type: 'boolean',
 49 |       table: { defaultValue: { summary: 'false' } },
 50 |       control: { type: 'boolean' },
 51 |       description: 'Defining if two lines of text should be visible',
 52 |     },
 53 |     activeOption: {
 54 |       type: 'string',
 55 |       control: 'text',
 56 |       description: 'The text/value of name to be active',
 57 |     },
 58 |     roleType: {
 59 |       control: { type: 'select' },
 60 |       table: { defaultValue: { summary: 'tablist' } },
 61 |       options: ['radiogroup', 'tablist'],
 62 |       description: 'Determines the ARIA role applied to the segmented control',
 63 |     },
 64 |     itemMaxWidth: {
 65 |       type: 'string',
 66 |       table: { defaultValue: { summary: 'auto' } },
 67 |       control: 'text',
 68 |       description: 'Max width of the item',
 69 |     },
 70 |     customLabel: {
 71 |       type: 'string',
 72 |       table: { defaultValue: { summary: 'customLabel' } },
 73 |       control: 'text',
 74 |       description: 'Custom text you can use to check the behavior',
 75 |     },
 76 |   },
 77 | 
 78 |   decorators: [
 79 |     moduleMetadata({
 80 |       imports: [DsSegmentedControl, DsSegmentedOption],
 81 |     }),
 82 |   ],
 83 | } as Meta<StoryType>;
 84 | 
 85 | export const Default: StoryObj<StoryType> = {
 86 |   parameters: {
 87 |     name: 'Default',
 88 |     design: {
 89 |       name: 'Whitelabel',
 90 |       type: 'figma',
 91 |       url: 'https://www.figma.com/file/NgrOt8MGJhe0obKFBQgqdT/Component-Tokens-(POC)?type=design&node-id=12596-148225&mode=design&t=fS1qO73SS8lGciLj-4',
 92 |     },
 93 |   },
 94 |   render: (args) => ({
 95 |     props: args,
 96 |     template: `
 97 |     <div style='width: 650px; text-align: center; display: block;'>
 98 |         <ds-segmented-control [twoLineTruncation]="${args.twoLineTruncation}" [roleType]="'${args.roleType}'" [fullWidth]="${args.fullWidth}" [inverse]="${args.inverse}" [activeOption]="'${args.activeOption}'" style="--ds-segment-item-text-max-width: ${args.itemMaxWidth};">
 99 |             <ds-segmented-option name='0' title="Label1 long text support, label long text support label long text support label long text support" />
100 |             <ds-segmented-option name="1" title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s" />
101 |             <ds-segmented-option name='2' title="Label3" />
102 |             <ds-segmented-option name="3" title="${args.customLabel}" />
103 |         </ds-segmented-control>
104 |     </div>
105 |   `,
106 |   }),
107 |   play: async ({ canvasElement, step }) => {
108 |     await step('check Click event is being called', async () => {
109 |       const canvas = within(canvasElement);
110 |       const segments = canvas.getAllByRole('tab');
111 |       await fireEvent.click(segments[0]);
112 |       await expect(segments[0]).toHaveClass('ds-segment-selected');
113 |     });
114 |   },
115 | };
116 | 
117 | export const WithImage: StoryObj<StoryType> = {
118 |   parameters: {
119 |     name: 'Default',
120 |     design: {
121 |       name: 'Whitelabel',
122 |       type: 'figma',
123 |       url: 'https://www.figma.com/file/NgrOt8MGJhe0obKFBQgqdT/Component-Tokens-(POC)?type=design&node-id=12596-148323&mode=design&t=fS1qO73SS8lGciLj-4',
124 |     },
125 |   },
126 |   argTypes: {
127 |     customLabel: {
128 |       table: { disable: true },
129 |     },
130 |     itemMaxWidth: {
131 |       table: { disable: true },
132 |     },
133 |     twoLineTruncation: {
134 |       table: { disable: true },
135 |     },
136 |   },
137 |   render: (args) => ({
138 |     props: args,
139 |     template: `
140 |     <div style='width: 450px; text-align: center; display: block;' >
141 |         <ds-segmented-control [roleType]="'${args.roleType}'" [fullWidth]="${args.fullWidth}" [inverse]="${args.inverse}" [activeOption]="'${args.activeOption}'">
142 |             <ds-segmented-option name="1" title="image1">
143 |                 <ng-template #dsTemplate>
144 |                         <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
145 |     <g clip-path="url(#clip0_12185_5035)">
146 |     <path d="M1.22907 11.123L0.397165 10.2663C0.140073 10.0015 0.140072 9.5722 0.397165 9.30742L9.2418 0.198579C9.49889 -0.0661931 9.91572 -0.0661931 10.1728 0.198579L10.927 0.975275L1.22907 11.123Z" fill="currentColor"/>
147 |     <path fill-rule="evenodd" clip-rule="evenodd" d="M4.02262 14L1.68316 11.5907L11.3811 1.44293L15.3023 5.4813C15.5594 5.74607 15.5594 6.17535 15.3023 6.44013L7.96171 14H4.02262ZM10.6476 6.44004C10.3905 6.17527 10.3905 5.74599 10.6476 5.48122L11.5786 4.52239C11.8357 4.25762 12.2525 4.25762 12.5096 4.52239L13.4406 5.48122C13.6977 5.74599 13.6977 6.17527 13.4406 6.44004L12.5096 7.39887C12.2525 7.66364 11.8357 7.66364 11.5786 7.39887L10.6476 6.44004ZM3.66431 11.7136L7.38836 7.87826L7.85387 8.35767L4.12981 12.193L3.66431 11.7136Z" fill="currentColor"/>
148 |     <path d="M9.76108 14.644H13.4364C13.6182 14.644 13.7655 14.7958 13.7655 14.983V15.661C13.7655 15.8483 13.6182 16 13.4364 16H2.24482C2.06303 16 1.91566 15.8483 1.91566 15.661V14.983C1.91566 14.8388 2.00309 14.7157 2.12634 14.6667H9.76108V14.644Z" fill="currentColor"/>
149 |     </g>
150 |     <defs>
151 |     <clipPath id="clip0_12185_5035">
152 |     <rect width="16" height="16" fill="currentColor"/>
153 |     </clipPath>
154 |     </defs>
155 | </svg>
156 |                 </ng-template>
157 |             </ds-segmented-option>
158 | 
159 |             <ds-segmented-option name="2" title="image2">
160 |                 <ng-template #dsTemplate>
161 |                         <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
162 |     <g clip-path="url(#clip0_12185_5035)">
163 |     <path d="M1.22907 11.123L0.397165 10.2663C0.140073 10.0015 0.140072 9.5722 0.397165 9.30742L9.2418 0.198579C9.49889 -0.0661931 9.91572 -0.0661931 10.1728 0.198579L10.927 0.975275L1.22907 11.123Z" fill="currentColor"/>
164 |     <path fill-rule="evenodd" clip-rule="evenodd" d="M4.02262 14L1.68316 11.5907L11.3811 1.44293L15.3023 5.4813C15.5594 5.74607 15.5594 6.17535 15.3023 6.44013L7.96171 14H4.02262ZM10.6476 6.44004C10.3905 6.17527 10.3905 5.74599 10.6476 5.48122L11.5786 4.52239C11.8357 4.25762 12.2525 4.25762 12.5096 4.52239L13.4406 5.48122C13.6977 5.74599 13.6977 6.17527 13.4406 6.44004L12.5096 7.39887C12.2525 7.66364 11.8357 7.66364 11.5786 7.39887L10.6476 6.44004ZM3.66431 11.7136L7.38836 7.87826L7.85387 8.35767L4.12981 12.193L3.66431 11.7136Z" fill="currentColor"/>
165 |     <path d="M9.76108 14.644H13.4364C13.6182 14.644 13.7655 14.7958 13.7655 14.983V15.661C13.7655 15.8483 13.6182 16 13.4364 16H2.24482C2.06303 16 1.91566 15.8483 1.91566 15.661V14.983C1.91566 14.8388 2.00309 14.7157 2.12634 14.6667H9.76108V14.644Z" fill="currentColor"/>
166 |     </g>
167 |     <defs>
168 |     <clipPath id="clip0_12185_5035">
169 |     <rect width="16" height="16" fill="currentColor"/>
170 |     </clipPath>
171 |     </defs>
172 | </svg>
173 |                 </ng-template>
174 |             </ds-segmented-option>
175 | 
176 |             <ds-segmented-option name="3" title="image3">
177 |                 <ng-template #dsTemplate>
178 |                         <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
179 |     <g clip-path="url(#clip0_12185_5035)">
180 |     <path d="M1.22907 11.123L0.397165 10.2663C0.140073 10.0015 0.140072 9.5722 0.397165 9.30742L9.2418 0.198579C9.49889 -0.0661931 9.91572 -0.0661931 10.1728 0.198579L10.927 0.975275L1.22907 11.123Z" fill="currentColor"/>
181 |     <path fill-rule="evenodd" clip-rule="evenodd" d="M4.02262 14L1.68316 11.5907L11.3811 1.44293L15.3023 5.4813C15.5594 5.74607 15.5594 6.17535 15.3023 6.44013L7.96171 14H4.02262ZM10.6476 6.44004C10.3905 6.17527 10.3905 5.74599 10.6476 5.48122L11.5786 4.52239C11.8357 4.25762 12.2525 4.25762 12.5096 4.52239L13.4406 5.48122C13.6977 5.74599 13.6977 6.17527 13.4406 6.44004L12.5096 7.39887C12.2525 7.66364 11.8357 7.66364 11.5786 7.39887L10.6476 6.44004ZM3.66431 11.7136L7.38836 7.87826L7.85387 8.35767L4.12981 12.193L3.66431 11.7136Z" fill="currentColor"/>
182 |     <path d="M9.76108 14.644H13.4364C13.6182 14.644 13.7655 14.7958 13.7655 14.983V15.661C13.7655 15.8483 13.6182 16 13.4364 16H2.24482C2.06303 16 1.91566 15.8483 1.91566 15.661V14.983C1.91566 14.8388 2.00309 14.7157 2.12634 14.6667H9.76108V14.644Z" fill="currentColor"/>
183 |     </g>
184 |     <defs>
185 |     <clipPath id="clip0_12185_5035">
186 |     <rect width="16" height="16" fill="currentColor"/>
187 |     </clipPath>
188 |     </defs>
189 | </svg>
190 |                 </ng-template>
191 |             </ds-segmented-option>
192 |         </ds-segmented-control>
193 |     </div>
194 |     `,
195 |   }),
196 |   play: async ({ canvasElement, step }) => {
197 |     await step('check Click event is being called', async () => {
198 |       const canvas = within(canvasElement);
199 |       const segments = canvas.getAllByRole('tab');
200 |       await fireEvent.click(segments[0]);
201 |       await expect(segments[0]).toHaveClass('ds-segment-selected');
202 |     });
203 |   },
204 | };
205 | 
```
Page 7/10FirstPrevNextLast