This is page 6 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/minimal-repo/packages/application/src/app/styles/bad-global-styles.scss: -------------------------------------------------------------------------------- ```scss 1 | // Extended and more meaningful classes 2 | .pill-with-badge { 3 | color: red; 4 | border: 1px solid #ccc; 5 | padding: 5px 10px; 6 | border-radius: 15px; 7 | display: inline-block; 8 | } 9 | 10 | .pill-with-badge-v2 { 11 | color: blue; 12 | border: 2px solid #aaa; 13 | padding: 6px 12px; 14 | border-radius: 20px; 15 | display: inline-block; 16 | } 17 | 18 | .sports-pill { 19 | color: green; 20 | background-color: #f0f0f0; 21 | padding: 8px 16px; 22 | border-radius: 25px; 23 | display: inline-block; 24 | } 25 | 26 | .offer-badge { 27 | color: yellow; 28 | background-color: #333; 29 | padding: 4px 8px; 30 | border-radius: 10px; 31 | display: inline-block; 32 | } 33 | 34 | .tab-nav { 35 | color: orange; 36 | background-color: #fff; 37 | padding: 10px; 38 | border-bottom: 2px solid #ddd; 39 | } 40 | 41 | .nav-tabs { 42 | color: purple; 43 | background-color: #eee; 44 | padding: 10px; 45 | border-bottom: 2px solid #ccc; 46 | } 47 | 48 | .tab-nav-item { 49 | color: pink; 50 | padding: 10px 15px; 51 | border-radius: 5px; 52 | display: inline-block; 53 | cursor: pointer; 54 | } 55 | 56 | .btn { 57 | color: brown; 58 | background-color: #f5f5f5; 59 | padding: 10px 20px; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | 65 | .btn-primary { 66 | color: cyan; 67 | background-color: #007bff; 68 | padding: 10px 20px; 69 | border: none; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | .legacy-button { 75 | color: magenta; 76 | background-color: #f8f9fa; 77 | padding: 10px 20px; 78 | border: 1px solid #ccc; 79 | border-radius: 5px; 80 | cursor: pointer; 81 | } 82 | 83 | .modal { 84 | color: lime; 85 | background-color: #fff; 86 | padding: 20px; 87 | border-radius: 10px; 88 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .card { 92 | color: olive; 93 | background-color: #f8f9fa; 94 | padding: 15px; 95 | border-radius: 5px; 96 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | .loading { 100 | color: teal; 101 | font-size: 16px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | } 106 | 107 | .loading-v2 { 108 | color: navy; 109 | font-size: 18px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .loading-v3 { 116 | color: maroon; 117 | font-size: 20px; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | } 122 | 123 | .collapsible-container { 124 | color: silver; 125 | background-color: #f0f0f0; 126 | padding: 10px; 127 | border-radius: 5px; 128 | overflow: hidden; 129 | } 130 | 131 | .divider { 132 | color: gray; 133 | border-top: 1px solid #ccc; 134 | margin: 10px 0; 135 | } 136 | 137 | .count { 138 | color: gold; 139 | background-color: #333; 140 | padding: 5px 10px; 141 | border-radius: 50%; 142 | display: inline-block; 143 | } 144 | 145 | .badge-circle { 146 | color: coral; 147 | background-color: #f0f0f0; 148 | padding: 5px 10px; 149 | border-radius: 50%; 150 | display: inline-block; 151 | } 152 | 153 | .custom-control-checkbox { 154 | color: khaki; 155 | display: flex; 156 | align-items: center; 157 | } 158 | 159 | .custom-control-radio { 160 | color: lavender; 161 | display: flex; 162 | align-items: center; 163 | } 164 | 165 | .form-control-tabs-segmented-v2 { 166 | color: salmon; 167 | background-color: #fff; 168 | padding: 10px; 169 | border-radius: 5px; 170 | display: flex; 171 | justify-content: space-between; 172 | } 173 | 174 | .form-control-tabs-segmented-flex { 175 | color: sienna; 176 | background-color: #f8f9fa; 177 | padding: 10px; 178 | border-radius: 5px; 179 | display: flex; 180 | justify-content: space-between; 181 | } 182 | 183 | .form-control-tabs-segmented-v2-dark { 184 | color: tan; 185 | background-color: #333; 186 | padding: 10px; 187 | border-radius: 5px; 188 | display: flex; 189 | justify-content: space-between; 190 | } 191 | 192 | .form-control-tabs-segmented-v3 { 193 | color: turquoise; 194 | background-color: #fff; 195 | padding: 10px; 196 | border-radius: 5px; 197 | display: flex; 198 | justify-content: space-between; 199 | } 200 | 201 | .form-control-tabs-segmented-v4 { 202 | color: violet; 203 | background-color: #f8f9fa; 204 | padding: 10px; 205 | border-radius: 5px; 206 | display: flex; 207 | justify-content: space-between; 208 | } 209 | 210 | .form-control-tabs-segmented { 211 | color: wheat; 212 | background-color: #fff; 213 | padding: 10px; 214 | border-radius: 5px; 215 | display: flex; 216 | justify-content: space-between; 217 | } 218 | 219 | .custom-control-switcher { 220 | color: azure; 221 | display: flex; 222 | align-items: center; 223 | } 224 | 225 | // 50 more random classes 226 | .random-class-1 { 227 | background-color: #f0f0f0; 228 | } 229 | .random-class-2 { 230 | background-color: #e0e0e0; 231 | } 232 | .random-class-3 { 233 | background-color: #d0d0d0; 234 | } 235 | .random-class-4 { 236 | background-color: #c0c0c0; 237 | } 238 | .random-class-5 { 239 | background-color: #b0b0b0; 240 | } 241 | .random-class-6 { 242 | background-color: #a0a0a0; 243 | } 244 | .random-class-7 { 245 | background-color: #909090; 246 | } 247 | .random-class-8 { 248 | background-color: #808080; 249 | } 250 | .random-class-9 { 251 | background-color: #707070; 252 | } 253 | .random-class-10 { 254 | background-color: #606060; 255 | } 256 | .random-class-11 { 257 | background-color: #505050; 258 | } 259 | .random-class-12 { 260 | background-color: #404040; 261 | } 262 | .random-class-13 { 263 | background-color: #303030; 264 | } 265 | .random-class-14 { 266 | background-color: #202020; 267 | } 268 | .random-class-15 { 269 | background-color: #101010; 270 | } 271 | .random-class-16 { 272 | background-color: #f8f8f8; 273 | } 274 | .random-class-17 { 275 | background-color: #e8e8e8; 276 | } 277 | .random-class-18 { 278 | background-color: #d8d8d8; 279 | } 280 | .random-class-19 { 281 | background-color: #c8c8c8; 282 | } 283 | .random-class-20 { 284 | background-color: #b8b8b8; 285 | } 286 | .random-class-21 { 287 | background-color: #a8a8a8; 288 | } 289 | .random-class-22 { 290 | background-color: #989898; 291 | } 292 | .random-class-23 { 293 | background-color: #888888; 294 | } 295 | .random-class-24 { 296 | background-color: #787878; 297 | } 298 | .random-class-25 { 299 | background-color: #686868; 300 | } 301 | .random-class-26 { 302 | background-color: #585858; 303 | } 304 | .random-class-27 { 305 | background-color: #484848; 306 | } 307 | .random-class-28 { 308 | background-color: #383838; 309 | } 310 | .random-class-29 { 311 | background-color: #282828; 312 | } 313 | .random-class-30 { 314 | background-color: #181818; 315 | } 316 | .random-class-31 { 317 | background-color: #080808; 318 | } 319 | .random-class-32 { 320 | background-color: #fefefe; 321 | } 322 | .random-class-33 { 323 | background-color: #ededed; 324 | } 325 | .random-class-34 { 326 | background-color: #dcdcdc; 327 | } 328 | .random-class-35 { 329 | background-color: #cbcbcb; 330 | } 331 | .random-class-36 { 332 | background-color: #bababa; 333 | } 334 | .random-class-37 { 335 | background-color: #a9a9a9; 336 | } 337 | .random-class-38 { 338 | background-color: #989898; 339 | } 340 | .random-class-39 { 341 | background-color: #878787; 342 | } 343 | .random-class-40 { 344 | background-color: #767676; 345 | } 346 | .random-class-41 { 347 | background-color: #656565; 348 | } 349 | .random-class-42 { 350 | background-color: #545454; 351 | } 352 | .random-class-43 { 353 | background-color: #434343; 354 | } 355 | .random-class-44 { 356 | background-color: #323232; 357 | } 358 | .random-class-45 { 359 | background-color: #212121; 360 | } 361 | .random-class-46 { 362 | background-color: #101010; 363 | } 364 | .random-class-47 { 365 | background-color: #f7f7f7; 366 | } 367 | .random-class-48 { 368 | background-color: #e6e6e6; 369 | } 370 | .random-class-49 { 371 | background-color: #d5d5d5; 372 | } 373 | .random-class-50 { 374 | background-color: #c4c4c4; 375 | } 376 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/base/base.scss: -------------------------------------------------------------------------------- ```scss 1 | // Extended and more meaningful classes 2 | .pill-with-badge { 3 | color: red; 4 | border: 1px solid #ccc; 5 | padding: 5px 10px; 6 | border-radius: 15px; 7 | display: inline-block; 8 | } 9 | 10 | .pill-with-badge-v2 { 11 | color: blue; 12 | border: 2px solid #aaa; 13 | padding: 6px 12px; 14 | border-radius: 20px; 15 | display: inline-block; 16 | } 17 | 18 | .sports-pill { 19 | color: green; 20 | background-color: #f0f0f0; 21 | padding: 8px 16px; 22 | border-radius: 25px; 23 | display: inline-block; 24 | } 25 | 26 | .offer-badge { 27 | color: yellow; 28 | background-color: #333; 29 | padding: 4px 8px; 30 | border-radius: 10px; 31 | display: inline-block; 32 | } 33 | 34 | .tab-nav { 35 | color: orange; 36 | background-color: #fff; 37 | padding: 10px; 38 | border-bottom: 2px solid #ddd; 39 | } 40 | 41 | .nav-tabs { 42 | color: purple; 43 | background-color: #eee; 44 | padding: 10px; 45 | border-bottom: 2px solid #ccc; 46 | } 47 | 48 | .tab-nav-item { 49 | color: pink; 50 | padding: 10px 15px; 51 | border-radius: 5px; 52 | display: inline-block; 53 | cursor: pointer; 54 | } 55 | 56 | .btn { 57 | color: brown; 58 | background-color: #f5f5f5; 59 | padding: 10px 20px; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | 65 | .btn-primary { 66 | color: cyan; 67 | background-color: #007bff; 68 | padding: 10px 20px; 69 | border: none; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | .legacy-button { 75 | color: magenta; 76 | background-color: #f8f9fa; 77 | padding: 10px 20px; 78 | border: 1px solid #ccc; 79 | border-radius: 5px; 80 | cursor: pointer; 81 | } 82 | 83 | .modal { 84 | color: lime; 85 | background-color: #fff; 86 | padding: 20px; 87 | border-radius: 10px; 88 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .card { 92 | color: olive; 93 | background-color: #f8f9fa; 94 | padding: 15px; 95 | border-radius: 5px; 96 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | .loading { 100 | color: teal; 101 | font-size: 16px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | } 106 | 107 | .loading-v2 { 108 | color: navy; 109 | font-size: 18px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .loading-v3 { 116 | color: maroon; 117 | font-size: 20px; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | } 122 | 123 | .collapsible-container { 124 | color: silver; 125 | background-color: #f0f0f0; 126 | padding: 10px; 127 | border-radius: 5px; 128 | overflow: hidden; 129 | } 130 | 131 | .divider { 132 | color: gray; 133 | border-top: 1px solid #ccc; 134 | margin: 10px 0; 135 | } 136 | 137 | .count { 138 | color: gold; 139 | background-color: #333; 140 | padding: 5px 10px; 141 | border-radius: 50%; 142 | display: inline-block; 143 | } 144 | 145 | .badge-circle { 146 | color: coral; 147 | background-color: #f0f0f0; 148 | padding: 5px 10px; 149 | border-radius: 50%; 150 | display: inline-block; 151 | } 152 | 153 | .custom-control-checkbox { 154 | color: khaki; 155 | display: flex; 156 | align-items: center; 157 | } 158 | 159 | .custom-control-radio { 160 | color: lavender; 161 | display: flex; 162 | align-items: center; 163 | } 164 | 165 | .form-control-tabs-segmented-v2 { 166 | color: salmon; 167 | background-color: #fff; 168 | padding: 10px; 169 | border-radius: 5px; 170 | display: flex; 171 | justify-content: space-between; 172 | } 173 | 174 | .form-control-tabs-segmented-flex { 175 | color: sienna; 176 | background-color: #f8f9fa; 177 | padding: 10px; 178 | border-radius: 5px; 179 | display: flex; 180 | justify-content: space-between; 181 | } 182 | 183 | .form-control-tabs-segmented-v2-dark { 184 | color: tan; 185 | background-color: #333; 186 | padding: 10px; 187 | border-radius: 5px; 188 | display: flex; 189 | justify-content: space-between; 190 | } 191 | 192 | .form-control-tabs-segmented-v3 { 193 | color: turquoise; 194 | background-color: #fff; 195 | padding: 10px; 196 | border-radius: 5px; 197 | display: flex; 198 | justify-content: space-between; 199 | } 200 | 201 | .form-control-tabs-segmented-v4 { 202 | color: violet; 203 | background-color: #f8f9fa; 204 | padding: 10px; 205 | border-radius: 5px; 206 | display: flex; 207 | justify-content: space-between; 208 | } 209 | 210 | .form-control-tabs-segmented { 211 | color: wheat; 212 | background-color: #fff; 213 | padding: 10px; 214 | border-radius: 5px; 215 | display: flex; 216 | justify-content: space-between; 217 | } 218 | 219 | .custom-control-switcher { 220 | color: azure; 221 | display: flex; 222 | align-items: center; 223 | } 224 | 225 | // 50 more random classes 226 | .random-class-1 { 227 | background-color: #f0f0f0; 228 | } 229 | .random-class-2 { 230 | background-color: #e0e0e0; 231 | } 232 | .random-class-3 { 233 | background-color: #d0d0d0; 234 | } 235 | .random-class-4 { 236 | background-color: #c0c0c0; 237 | } 238 | .random-class-5 { 239 | background-color: #b0b0b0; 240 | } 241 | .random-class-6 { 242 | background-color: #a0a0a0; 243 | } 244 | .random-class-7 { 245 | background-color: #909090; 246 | } 247 | .random-class-8 { 248 | background-color: #808080; 249 | } 250 | .random-class-9 { 251 | background-color: #707070; 252 | } 253 | .random-class-10 { 254 | background-color: #606060; 255 | } 256 | .random-class-11 { 257 | background-color: #505050; 258 | } 259 | .random-class-12 { 260 | background-color: #404040; 261 | } 262 | .random-class-13 { 263 | background-color: #303030; 264 | } 265 | .random-class-14 { 266 | background-color: #202020; 267 | } 268 | .random-class-15 { 269 | background-color: #101010; 270 | } 271 | .random-class-16 { 272 | background-color: #f8f8f8; 273 | } 274 | .random-class-17 { 275 | background-color: #e8e8e8; 276 | } 277 | .random-class-18 { 278 | background-color: #d8d8d8; 279 | } 280 | .random-class-19 { 281 | background-color: #c8c8c8; 282 | } 283 | .random-class-20 { 284 | background-color: #b8b8b8; 285 | } 286 | .random-class-21 { 287 | background-color: #a8a8a8; 288 | } 289 | .random-class-22 { 290 | background-color: #989898; 291 | } 292 | .random-class-23 { 293 | background-color: #888888; 294 | } 295 | .random-class-24 { 296 | background-color: #787878; 297 | } 298 | .random-class-25 { 299 | background-color: #686868; 300 | } 301 | .random-class-26 { 302 | background-color: #585858; 303 | } 304 | .random-class-27 { 305 | background-color: #484848; 306 | } 307 | .random-class-28 { 308 | background-color: #383838; 309 | } 310 | .random-class-29 { 311 | background-color: #282828; 312 | } 313 | .random-class-30 { 314 | background-color: #181818; 315 | } 316 | .random-class-31 { 317 | background-color: #080808; 318 | } 319 | .random-class-32 { 320 | background-color: #fefefe; 321 | } 322 | .random-class-33 { 323 | background-color: #ededed; 324 | } 325 | .random-class-34 { 326 | background-color: #dcdcdc; 327 | } 328 | .random-class-35 { 329 | background-color: #cbcbcb; 330 | } 331 | .random-class-36 { 332 | background-color: #bababa; 333 | } 334 | .random-class-37 { 335 | background-color: #a9a9a9; 336 | } 337 | .random-class-38 { 338 | background-color: #989898; 339 | } 340 | .random-class-39 { 341 | background-color: #878787; 342 | } 343 | .random-class-40 { 344 | background-color: #767676; 345 | } 346 | .random-class-41 { 347 | background-color: #656565; 348 | } 349 | .random-class-42 { 350 | background-color: #545454; 351 | } 352 | .random-class-43 { 353 | background-color: #434343; 354 | } 355 | .random-class-44 { 356 | background-color: #323232; 357 | } 358 | .random-class-45 { 359 | background-color: #212121; 360 | } 361 | .random-class-46 { 362 | background-color: #101010; 363 | } 364 | .random-class-47 { 365 | background-color: #f7f7f7; 366 | } 367 | .random-class-48 { 368 | background-color: #e6e6e6; 369 | } 370 | .random-class-49 { 371 | background-color: #d5d5d5; 372 | } 373 | .random-class-50 { 374 | background-color: #c4c4c4; 375 | } 376 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/components/components.scss: -------------------------------------------------------------------------------- ```scss 1 | // Extended and more meaningful classes 2 | .pill-with-badge { 3 | color: red; 4 | border: 1px solid #ccc; 5 | padding: 5px 10px; 6 | border-radius: 15px; 7 | display: inline-block; 8 | } 9 | 10 | .pill-with-badge-v2 { 11 | color: blue; 12 | border: 2px solid #aaa; 13 | padding: 6px 12px; 14 | border-radius: 20px; 15 | display: inline-block; 16 | } 17 | 18 | .sports-pill { 19 | color: green; 20 | background-color: #f0f0f0; 21 | padding: 8px 16px; 22 | border-radius: 25px; 23 | display: inline-block; 24 | } 25 | 26 | .offer-badge { 27 | color: yellow; 28 | background-color: #333; 29 | padding: 4px 8px; 30 | border-radius: 10px; 31 | display: inline-block; 32 | } 33 | 34 | .tab-nav { 35 | color: orange; 36 | background-color: #fff; 37 | padding: 10px; 38 | border-bottom: 2px solid #ddd; 39 | } 40 | 41 | .nav-tabs { 42 | color: purple; 43 | background-color: #eee; 44 | padding: 10px; 45 | border-bottom: 2px solid #ccc; 46 | } 47 | 48 | .tab-nav-item { 49 | color: pink; 50 | padding: 10px 15px; 51 | border-radius: 5px; 52 | display: inline-block; 53 | cursor: pointer; 54 | } 55 | 56 | .btn { 57 | color: brown; 58 | background-color: #f5f5f5; 59 | padding: 10px 20px; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | 65 | .btn-primary { 66 | color: cyan; 67 | background-color: #007bff; 68 | padding: 10px 20px; 69 | border: none; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | .legacy-button { 75 | color: magenta; 76 | background-color: #f8f9fa; 77 | padding: 10px 20px; 78 | border: 1px solid #ccc; 79 | border-radius: 5px; 80 | cursor: pointer; 81 | } 82 | 83 | .modal { 84 | color: lime; 85 | background-color: #fff; 86 | padding: 20px; 87 | border-radius: 10px; 88 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .card { 92 | color: olive; 93 | background-color: #f8f9fa; 94 | padding: 15px; 95 | border-radius: 5px; 96 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | .loading { 100 | color: teal; 101 | font-size: 16px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | } 106 | 107 | .loading-v2 { 108 | color: navy; 109 | font-size: 18px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .loading-v3 { 116 | color: maroon; 117 | font-size: 20px; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | } 122 | 123 | .collapsible-container { 124 | color: silver; 125 | background-color: #f0f0f0; 126 | padding: 10px; 127 | border-radius: 5px; 128 | overflow: hidden; 129 | } 130 | 131 | .divider { 132 | color: gray; 133 | border-top: 1px solid #ccc; 134 | margin: 10px 0; 135 | } 136 | 137 | .count { 138 | color: gold; 139 | background-color: #333; 140 | padding: 5px 10px; 141 | border-radius: 50%; 142 | display: inline-block; 143 | } 144 | 145 | .badge-circle { 146 | color: coral; 147 | background-color: #f0f0f0; 148 | padding: 5px 10px; 149 | border-radius: 50%; 150 | display: inline-block; 151 | } 152 | 153 | .custom-control-checkbox { 154 | color: khaki; 155 | display: flex; 156 | align-items: center; 157 | } 158 | 159 | .custom-control-radio { 160 | color: lavender; 161 | display: flex; 162 | align-items: center; 163 | } 164 | 165 | .form-control-tabs-segmented-v2 { 166 | color: salmon; 167 | background-color: #fff; 168 | padding: 10px; 169 | border-radius: 5px; 170 | display: flex; 171 | justify-content: space-between; 172 | } 173 | 174 | .form-control-tabs-segmented-flex { 175 | color: sienna; 176 | background-color: #f8f9fa; 177 | padding: 10px; 178 | border-radius: 5px; 179 | display: flex; 180 | justify-content: space-between; 181 | } 182 | 183 | .form-control-tabs-segmented-v2-dark { 184 | color: tan; 185 | background-color: #333; 186 | padding: 10px; 187 | border-radius: 5px; 188 | display: flex; 189 | justify-content: space-between; 190 | } 191 | 192 | .form-control-tabs-segmented-v3 { 193 | color: turquoise; 194 | background-color: #fff; 195 | padding: 10px; 196 | border-radius: 5px; 197 | display: flex; 198 | justify-content: space-between; 199 | } 200 | 201 | .form-control-tabs-segmented-v4 { 202 | color: violet; 203 | background-color: #f8f9fa; 204 | padding: 10px; 205 | border-radius: 5px; 206 | display: flex; 207 | justify-content: space-between; 208 | } 209 | 210 | .form-control-tabs-segmented { 211 | color: wheat; 212 | background-color: #fff; 213 | padding: 10px; 214 | border-radius: 5px; 215 | display: flex; 216 | justify-content: space-between; 217 | } 218 | 219 | .custom-control-switcher { 220 | color: azure; 221 | display: flex; 222 | align-items: center; 223 | } 224 | 225 | // 50 more random classes 226 | .random-class-1 { 227 | background-color: #f0f0f0; 228 | } 229 | .random-class-2 { 230 | background-color: #e0e0e0; 231 | } 232 | .random-class-3 { 233 | background-color: #d0d0d0; 234 | } 235 | .random-class-4 { 236 | background-color: #c0c0c0; 237 | } 238 | .random-class-5 { 239 | background-color: #b0b0b0; 240 | } 241 | .random-class-6 { 242 | background-color: #a0a0a0; 243 | } 244 | .random-class-7 { 245 | background-color: #909090; 246 | } 247 | .random-class-8 { 248 | background-color: #808080; 249 | } 250 | .random-class-9 { 251 | background-color: #707070; 252 | } 253 | .random-class-10 { 254 | background-color: #606060; 255 | } 256 | .random-class-11 { 257 | background-color: #505050; 258 | } 259 | .random-class-12 { 260 | background-color: #404040; 261 | } 262 | .random-class-13 { 263 | background-color: #303030; 264 | } 265 | .random-class-14 { 266 | background-color: #202020; 267 | } 268 | .random-class-15 { 269 | background-color: #101010; 270 | } 271 | .random-class-16 { 272 | background-color: #f8f8f8; 273 | } 274 | .random-class-17 { 275 | background-color: #e8e8e8; 276 | } 277 | .random-class-18 { 278 | background-color: #d8d8d8; 279 | } 280 | .random-class-19 { 281 | background-color: #c8c8c8; 282 | } 283 | .random-class-20 { 284 | background-color: #b8b8b8; 285 | } 286 | .random-class-21 { 287 | background-color: #a8a8a8; 288 | } 289 | .random-class-22 { 290 | background-color: #989898; 291 | } 292 | .random-class-23 { 293 | background-color: #888888; 294 | } 295 | .random-class-24 { 296 | background-color: #787878; 297 | } 298 | .random-class-25 { 299 | background-color: #686868; 300 | } 301 | .random-class-26 { 302 | background-color: #585858; 303 | } 304 | .random-class-27 { 305 | background-color: #484848; 306 | } 307 | .random-class-28 { 308 | background-color: #383838; 309 | } 310 | .random-class-29 { 311 | background-color: #282828; 312 | } 313 | .random-class-30 { 314 | background-color: #181818; 315 | } 316 | .random-class-31 { 317 | background-color: #080808; 318 | } 319 | .random-class-32 { 320 | background-color: #fefefe; 321 | } 322 | .random-class-33 { 323 | background-color: #ededed; 324 | } 325 | .random-class-34 { 326 | background-color: #dcdcdc; 327 | } 328 | .random-class-35 { 329 | background-color: #cbcbcb; 330 | } 331 | .random-class-36 { 332 | background-color: #bababa; 333 | } 334 | .random-class-37 { 335 | background-color: #a9a9a9; 336 | } 337 | .random-class-38 { 338 | background-color: #989898; 339 | } 340 | .random-class-39 { 341 | background-color: #878787; 342 | } 343 | .random-class-40 { 344 | background-color: #767676; 345 | } 346 | .random-class-41 { 347 | background-color: #656565; 348 | } 349 | .random-class-42 { 350 | background-color: #545454; 351 | } 352 | .random-class-43 { 353 | background-color: #434343; 354 | } 355 | .random-class-44 { 356 | background-color: #323232; 357 | } 358 | .random-class-45 { 359 | background-color: #212121; 360 | } 361 | .random-class-46 { 362 | background-color: #101010; 363 | } 364 | .random-class-47 { 365 | background-color: #f7f7f7; 366 | } 367 | .random-class-48 { 368 | background-color: #e6e6e6; 369 | } 370 | .random-class-49 { 371 | background-color: #d5d5d5; 372 | } 373 | .random-class-50 { 374 | background-color: #c4c4c4; 375 | } 376 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/layout/layout.scss: -------------------------------------------------------------------------------- ```scss 1 | // Extended and more meaningful classes 2 | .pill-with-badge { 3 | color: red; 4 | border: 1px solid #ccc; 5 | padding: 5px 10px; 6 | border-radius: 15px; 7 | display: inline-block; 8 | } 9 | 10 | .pill-with-badge-v2 { 11 | color: blue; 12 | border: 2px solid #aaa; 13 | padding: 6px 12px; 14 | border-radius: 20px; 15 | display: inline-block; 16 | } 17 | 18 | .sports-pill { 19 | color: green; 20 | background-color: #f0f0f0; 21 | padding: 8px 16px; 22 | border-radius: 25px; 23 | display: inline-block; 24 | } 25 | 26 | .offer-badge { 27 | color: yellow; 28 | background-color: #333; 29 | padding: 4px 8px; 30 | border-radius: 10px; 31 | display: inline-block; 32 | } 33 | 34 | .tab-nav { 35 | color: orange; 36 | background-color: #fff; 37 | padding: 10px; 38 | border-bottom: 2px solid #ddd; 39 | } 40 | 41 | .nav-tabs { 42 | color: purple; 43 | background-color: #eee; 44 | padding: 10px; 45 | border-bottom: 2px solid #ccc; 46 | } 47 | 48 | .tab-nav-item { 49 | color: pink; 50 | padding: 10px 15px; 51 | border-radius: 5px; 52 | display: inline-block; 53 | cursor: pointer; 54 | } 55 | 56 | .btn { 57 | color: brown; 58 | background-color: #f5f5f5; 59 | padding: 10px 20px; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | 65 | .btn-primary { 66 | color: cyan; 67 | background-color: #007bff; 68 | padding: 10px 20px; 69 | border: none; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | .legacy-button { 75 | color: magenta; 76 | background-color: #f8f9fa; 77 | padding: 10px 20px; 78 | border: 1px solid #ccc; 79 | border-radius: 5px; 80 | cursor: pointer; 81 | } 82 | 83 | .modal { 84 | color: lime; 85 | background-color: #fff; 86 | padding: 20px; 87 | border-radius: 10px; 88 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .card { 92 | color: olive; 93 | background-color: #f8f9fa; 94 | padding: 15px; 95 | border-radius: 5px; 96 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | .loading { 100 | color: teal; 101 | font-size: 16px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | } 106 | 107 | .loading-v2 { 108 | color: navy; 109 | font-size: 18px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .loading-v3 { 116 | color: maroon; 117 | font-size: 20px; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | } 122 | 123 | .collapsible-container { 124 | color: silver; 125 | background-color: #f0f0f0; 126 | padding: 10px; 127 | border-radius: 5px; 128 | overflow: hidden; 129 | } 130 | 131 | .divider { 132 | color: gray; 133 | border-top: 1px solid #ccc; 134 | margin: 10px 0; 135 | } 136 | 137 | .count { 138 | color: gold; 139 | background-color: #333; 140 | padding: 5px 10px; 141 | border-radius: 50%; 142 | display: inline-block; 143 | } 144 | 145 | .badge-circle { 146 | color: coral; 147 | background-color: #f0f0f0; 148 | padding: 5px 10px; 149 | border-radius: 50%; 150 | display: inline-block; 151 | } 152 | 153 | .custom-control-checkbox { 154 | color: khaki; 155 | display: flex; 156 | align-items: center; 157 | } 158 | 159 | .custom-control-radio { 160 | color: lavender; 161 | display: flex; 162 | align-items: center; 163 | } 164 | 165 | .form-control-tabs-segmented-v2 { 166 | color: salmon; 167 | background-color: #fff; 168 | padding: 10px; 169 | border-radius: 5px; 170 | display: flex; 171 | justify-content: space-between; 172 | } 173 | 174 | .form-control-tabs-segmented-flex { 175 | color: sienna; 176 | background-color: #f8f9fa; 177 | padding: 10px; 178 | border-radius: 5px; 179 | display: flex; 180 | justify-content: space-between; 181 | } 182 | 183 | .form-control-tabs-segmented-v2-dark { 184 | color: tan; 185 | background-color: #333; 186 | padding: 10px; 187 | border-radius: 5px; 188 | display: flex; 189 | justify-content: space-between; 190 | } 191 | 192 | .form-control-tabs-segmented-v3 { 193 | color: turquoise; 194 | background-color: #fff; 195 | padding: 10px; 196 | border-radius: 5px; 197 | display: flex; 198 | justify-content: space-between; 199 | } 200 | 201 | .form-control-tabs-segmented-v4 { 202 | color: violet; 203 | background-color: #f8f9fa; 204 | padding: 10px; 205 | border-radius: 5px; 206 | display: flex; 207 | justify-content: space-between; 208 | } 209 | 210 | .form-control-tabs-segmented { 211 | color: wheat; 212 | background-color: #fff; 213 | padding: 10px; 214 | border-radius: 5px; 215 | display: flex; 216 | justify-content: space-between; 217 | } 218 | 219 | .custom-control-switcher { 220 | color: azure; 221 | display: flex; 222 | align-items: center; 223 | } 224 | 225 | // 50 more random classes 226 | .random-class-1 { 227 | background-color: #f0f0f0; 228 | } 229 | .random-class-2 { 230 | background-color: #e0e0e0; 231 | } 232 | .random-class-3 { 233 | background-color: #d0d0d0; 234 | } 235 | .random-class-4 { 236 | background-color: #c0c0c0; 237 | } 238 | .random-class-5 { 239 | background-color: #b0b0b0; 240 | } 241 | .random-class-6 { 242 | background-color: #a0a0a0; 243 | } 244 | .random-class-7 { 245 | background-color: #909090; 246 | } 247 | .random-class-8 { 248 | background-color: #808080; 249 | } 250 | .random-class-9 { 251 | background-color: #707070; 252 | } 253 | .random-class-10 { 254 | background-color: #606060; 255 | } 256 | .random-class-11 { 257 | background-color: #505050; 258 | } 259 | .random-class-12 { 260 | background-color: #404040; 261 | } 262 | .random-class-13 { 263 | background-color: #303030; 264 | } 265 | .random-class-14 { 266 | background-color: #202020; 267 | } 268 | .random-class-15 { 269 | background-color: #101010; 270 | } 271 | .random-class-16 { 272 | background-color: #f8f8f8; 273 | } 274 | .random-class-17 { 275 | background-color: #e8e8e8; 276 | } 277 | .random-class-18 { 278 | background-color: #d8d8d8; 279 | } 280 | .random-class-19 { 281 | background-color: #c8c8c8; 282 | } 283 | .random-class-20 { 284 | background-color: #b8b8b8; 285 | } 286 | .random-class-21 { 287 | background-color: #a8a8a8; 288 | } 289 | .random-class-22 { 290 | background-color: #989898; 291 | } 292 | .random-class-23 { 293 | background-color: #888888; 294 | } 295 | .random-class-24 { 296 | background-color: #787878; 297 | } 298 | .random-class-25 { 299 | background-color: #686868; 300 | } 301 | .random-class-26 { 302 | background-color: #585858; 303 | } 304 | .random-class-27 { 305 | background-color: #484848; 306 | } 307 | .random-class-28 { 308 | background-color: #383838; 309 | } 310 | .random-class-29 { 311 | background-color: #282828; 312 | } 313 | .random-class-30 { 314 | background-color: #181818; 315 | } 316 | .random-class-31 { 317 | background-color: #080808; 318 | } 319 | .random-class-32 { 320 | background-color: #fefefe; 321 | } 322 | .random-class-33 { 323 | background-color: #ededed; 324 | } 325 | .random-class-34 { 326 | background-color: #dcdcdc; 327 | } 328 | .random-class-35 { 329 | background-color: #cbcbcb; 330 | } 331 | .random-class-36 { 332 | background-color: #bababa; 333 | } 334 | .random-class-37 { 335 | background-color: #a9a9a9; 336 | } 337 | .random-class-38 { 338 | background-color: #989898; 339 | } 340 | .random-class-39 { 341 | background-color: #878787; 342 | } 343 | .random-class-40 { 344 | background-color: #767676; 345 | } 346 | .random-class-41 { 347 | background-color: #656565; 348 | } 349 | .random-class-42 { 350 | background-color: #545454; 351 | } 352 | .random-class-43 { 353 | background-color: #434343; 354 | } 355 | .random-class-44 { 356 | background-color: #323232; 357 | } 358 | .random-class-45 { 359 | background-color: #212121; 360 | } 361 | .random-class-46 { 362 | background-color: #101010; 363 | } 364 | .random-class-47 { 365 | background-color: #f7f7f7; 366 | } 367 | .random-class-48 { 368 | background-color: #e6e6e6; 369 | } 370 | .random-class-49 { 371 | background-color: #d5d5d5; 372 | } 373 | .random-class-50 { 374 | background-color: #c4c4c4; 375 | } 376 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/themes/themes.scss: -------------------------------------------------------------------------------- ```scss 1 | // Extended and more meaningful classes 2 | .pill-with-badge { 3 | color: red; 4 | border: 1px solid #ccc; 5 | padding: 5px 10px; 6 | border-radius: 15px; 7 | display: inline-block; 8 | } 9 | 10 | .pill-with-badge-v2 { 11 | color: blue; 12 | border: 2px solid #aaa; 13 | padding: 6px 12px; 14 | border-radius: 20px; 15 | display: inline-block; 16 | } 17 | 18 | .sports-pill { 19 | color: green; 20 | background-color: #f0f0f0; 21 | padding: 8px 16px; 22 | border-radius: 25px; 23 | display: inline-block; 24 | } 25 | 26 | .offer-badge { 27 | color: yellow; 28 | background-color: #333; 29 | padding: 4px 8px; 30 | border-radius: 10px; 31 | display: inline-block; 32 | } 33 | 34 | .tab-nav { 35 | color: orange; 36 | background-color: #fff; 37 | padding: 10px; 38 | border-bottom: 2px solid #ddd; 39 | } 40 | 41 | .nav-tabs { 42 | color: purple; 43 | background-color: #eee; 44 | padding: 10px; 45 | border-bottom: 2px solid #ccc; 46 | } 47 | 48 | .tab-nav-item { 49 | color: pink; 50 | padding: 10px 15px; 51 | border-radius: 5px; 52 | display: inline-block; 53 | cursor: pointer; 54 | } 55 | 56 | .btn { 57 | color: brown; 58 | background-color: #f5f5f5; 59 | padding: 10px 20px; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | 65 | .btn-primary { 66 | color: cyan; 67 | background-color: #007bff; 68 | padding: 10px 20px; 69 | border: none; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | .legacy-button { 75 | color: magenta; 76 | background-color: #f8f9fa; 77 | padding: 10px 20px; 78 | border: 1px solid #ccc; 79 | border-radius: 5px; 80 | cursor: pointer; 81 | } 82 | 83 | .modal { 84 | color: lime; 85 | background-color: #fff; 86 | padding: 20px; 87 | border-radius: 10px; 88 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .card { 92 | color: olive; 93 | background-color: #f8f9fa; 94 | padding: 15px; 95 | border-radius: 5px; 96 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | .loading { 100 | color: teal; 101 | font-size: 16px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | } 106 | 107 | .loading-v2 { 108 | color: navy; 109 | font-size: 18px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .loading-v3 { 116 | color: maroon; 117 | font-size: 20px; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | } 122 | 123 | .collapsible-container { 124 | color: silver; 125 | background-color: #f0f0f0; 126 | padding: 10px; 127 | border-radius: 5px; 128 | overflow: hidden; 129 | } 130 | 131 | .divider { 132 | color: gray; 133 | border-top: 1px solid #ccc; 134 | margin: 10px 0; 135 | } 136 | 137 | .count { 138 | color: gold; 139 | background-color: #333; 140 | padding: 5px 10px; 141 | border-radius: 50%; 142 | display: inline-block; 143 | } 144 | 145 | .badge-circle { 146 | color: coral; 147 | background-color: #f0f0f0; 148 | padding: 5px 10px; 149 | border-radius: 50%; 150 | display: inline-block; 151 | } 152 | 153 | .custom-control-checkbox { 154 | color: khaki; 155 | display: flex; 156 | align-items: center; 157 | } 158 | 159 | .custom-control-radio { 160 | color: lavender; 161 | display: flex; 162 | align-items: center; 163 | } 164 | 165 | .form-control-tabs-segmented-v2 { 166 | color: salmon; 167 | background-color: #fff; 168 | padding: 10px; 169 | border-radius: 5px; 170 | display: flex; 171 | justify-content: space-between; 172 | } 173 | 174 | .form-control-tabs-segmented-flex { 175 | color: sienna; 176 | background-color: #f8f9fa; 177 | padding: 10px; 178 | border-radius: 5px; 179 | display: flex; 180 | justify-content: space-between; 181 | } 182 | 183 | .form-control-tabs-segmented-v2-dark { 184 | color: tan; 185 | background-color: #333; 186 | padding: 10px; 187 | border-radius: 5px; 188 | display: flex; 189 | justify-content: space-between; 190 | } 191 | 192 | .form-control-tabs-segmented-v3 { 193 | color: turquoise; 194 | background-color: #fff; 195 | padding: 10px; 196 | border-radius: 5px; 197 | display: flex; 198 | justify-content: space-between; 199 | } 200 | 201 | .form-control-tabs-segmented-v4 { 202 | color: violet; 203 | background-color: #f8f9fa; 204 | padding: 10px; 205 | border-radius: 5px; 206 | display: flex; 207 | justify-content: space-between; 208 | } 209 | 210 | .form-control-tabs-segmented { 211 | color: wheat; 212 | background-color: #fff; 213 | padding: 10px; 214 | border-radius: 5px; 215 | display: flex; 216 | justify-content: space-between; 217 | } 218 | 219 | .custom-control-switcher { 220 | color: azure; 221 | display: flex; 222 | align-items: center; 223 | } 224 | 225 | // 50 more random classes 226 | .random-class-1 { 227 | background-color: #f0f0f0; 228 | } 229 | .random-class-2 { 230 | background-color: #e0e0e0; 231 | } 232 | .random-class-3 { 233 | background-color: #d0d0d0; 234 | } 235 | .random-class-4 { 236 | background-color: #c0c0c0; 237 | } 238 | .random-class-5 { 239 | background-color: #b0b0b0; 240 | } 241 | .random-class-6 { 242 | background-color: #a0a0a0; 243 | } 244 | .random-class-7 { 245 | background-color: #909090; 246 | } 247 | .random-class-8 { 248 | background-color: #808080; 249 | } 250 | .random-class-9 { 251 | background-color: #707070; 252 | } 253 | .random-class-10 { 254 | background-color: #606060; 255 | } 256 | .random-class-11 { 257 | background-color: #505050; 258 | } 259 | .random-class-12 { 260 | background-color: #404040; 261 | } 262 | .random-class-13 { 263 | background-color: #303030; 264 | } 265 | .random-class-14 { 266 | background-color: #202020; 267 | } 268 | .random-class-15 { 269 | background-color: #101010; 270 | } 271 | .random-class-16 { 272 | background-color: #f8f8f8; 273 | } 274 | .random-class-17 { 275 | background-color: #e8e8e8; 276 | } 277 | .random-class-18 { 278 | background-color: #d8d8d8; 279 | } 280 | .random-class-19 { 281 | background-color: #c8c8c8; 282 | } 283 | .random-class-20 { 284 | background-color: #b8b8b8; 285 | } 286 | .random-class-21 { 287 | background-color: #a8a8a8; 288 | } 289 | .random-class-22 { 290 | background-color: #989898; 291 | } 292 | .random-class-23 { 293 | background-color: #888888; 294 | } 295 | .random-class-24 { 296 | background-color: #787878; 297 | } 298 | .random-class-25 { 299 | background-color: #686868; 300 | } 301 | .random-class-26 { 302 | background-color: #585858; 303 | } 304 | .random-class-27 { 305 | background-color: #484848; 306 | } 307 | .random-class-28 { 308 | background-color: #383838; 309 | } 310 | .random-class-29 { 311 | background-color: #282828; 312 | } 313 | .random-class-30 { 314 | background-color: #181818; 315 | } 316 | .random-class-31 { 317 | background-color: #080808; 318 | } 319 | .random-class-32 { 320 | background-color: #fefefe; 321 | } 322 | .random-class-33 { 323 | background-color: #ededed; 324 | } 325 | .random-class-34 { 326 | background-color: #dcdcdc; 327 | } 328 | .random-class-35 { 329 | background-color: #cbcbcb; 330 | } 331 | .random-class-36 { 332 | background-color: #bababa; 333 | } 334 | .random-class-37 { 335 | background-color: #a9a9a9; 336 | } 337 | .random-class-38 { 338 | background-color: #989898; 339 | } 340 | .random-class-39 { 341 | background-color: #878787; 342 | } 343 | .random-class-40 { 344 | background-color: #767676; 345 | } 346 | .random-class-41 { 347 | background-color: #656565; 348 | } 349 | .random-class-42 { 350 | background-color: #545454; 351 | } 352 | .random-class-43 { 353 | background-color: #434343; 354 | } 355 | .random-class-44 { 356 | background-color: #323232; 357 | } 358 | .random-class-45 { 359 | background-color: #212121; 360 | } 361 | .random-class-46 { 362 | background-color: #101010; 363 | } 364 | .random-class-47 { 365 | background-color: #f7f7f7; 366 | } 367 | .random-class-48 { 368 | background-color: #e6e6e6; 369 | } 370 | .random-class-49 { 371 | background-color: #d5d5d5; 372 | } 373 | .random-class-50 { 374 | background-color: #c4c4c4; 375 | } 376 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/styles/utilities/utilities.scss: -------------------------------------------------------------------------------- ```scss 1 | // Extended and more meaningful classes 2 | .pill-with-badge { 3 | color: red; 4 | border: 1px solid #ccc; 5 | padding: 5px 10px; 6 | border-radius: 15px; 7 | display: inline-block; 8 | } 9 | 10 | .pill-with-badge-v2 { 11 | color: blue; 12 | border: 2px solid #aaa; 13 | padding: 6px 12px; 14 | border-radius: 20px; 15 | display: inline-block; 16 | } 17 | 18 | .sports-pill { 19 | color: green; 20 | background-color: #f0f0f0; 21 | padding: 8px 16px; 22 | border-radius: 25px; 23 | display: inline-block; 24 | } 25 | 26 | .offer-badge { 27 | color: yellow; 28 | background-color: #333; 29 | padding: 4px 8px; 30 | border-radius: 10px; 31 | display: inline-block; 32 | } 33 | 34 | .tab-nav { 35 | color: orange; 36 | background-color: #fff; 37 | padding: 10px; 38 | border-bottom: 2px solid #ddd; 39 | } 40 | 41 | .nav-tabs { 42 | color: purple; 43 | background-color: #eee; 44 | padding: 10px; 45 | border-bottom: 2px solid #ccc; 46 | } 47 | 48 | .tab-nav-item { 49 | color: pink; 50 | padding: 10px 15px; 51 | border-radius: 5px; 52 | display: inline-block; 53 | cursor: pointer; 54 | } 55 | 56 | .btn { 57 | color: brown; 58 | background-color: #f5f5f5; 59 | padding: 10px 20px; 60 | border: none; 61 | border-radius: 5px; 62 | cursor: pointer; 63 | } 64 | 65 | .btn-primary { 66 | color: cyan; 67 | background-color: #007bff; 68 | padding: 10px 20px; 69 | border: none; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | .legacy-button { 75 | color: magenta; 76 | background-color: #f8f9fa; 77 | padding: 10px 20px; 78 | border: 1px solid #ccc; 79 | border-radius: 5px; 80 | cursor: pointer; 81 | } 82 | 83 | .modal { 84 | color: lime; 85 | background-color: #fff; 86 | padding: 20px; 87 | border-radius: 10px; 88 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .card { 92 | color: olive; 93 | background-color: #f8f9fa; 94 | padding: 15px; 95 | border-radius: 5px; 96 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | .loading { 100 | color: teal; 101 | font-size: 16px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | } 106 | 107 | .loading-v2 { 108 | color: navy; 109 | font-size: 18px; 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | } 114 | 115 | .loading-v3 { 116 | color: maroon; 117 | font-size: 20px; 118 | display: flex; 119 | align-items: center; 120 | justify-content: center; 121 | } 122 | 123 | .collapsible-container { 124 | color: silver; 125 | background-color: #f0f0f0; 126 | padding: 10px; 127 | border-radius: 5px; 128 | overflow: hidden; 129 | } 130 | 131 | .divider { 132 | color: gray; 133 | border-top: 1px solid #ccc; 134 | margin: 10px 0; 135 | } 136 | 137 | .count { 138 | color: gold; 139 | background-color: #333; 140 | padding: 5px 10px; 141 | border-radius: 50%; 142 | display: inline-block; 143 | } 144 | 145 | .badge-circle { 146 | color: coral; 147 | background-color: #f0f0f0; 148 | padding: 5px 10px; 149 | border-radius: 50%; 150 | display: inline-block; 151 | } 152 | 153 | .custom-control-checkbox { 154 | color: khaki; 155 | display: flex; 156 | align-items: center; 157 | } 158 | 159 | .custom-control-radio { 160 | color: lavender; 161 | display: flex; 162 | align-items: center; 163 | } 164 | 165 | .form-control-tabs-segmented-v2 { 166 | color: salmon; 167 | background-color: #fff; 168 | padding: 10px; 169 | border-radius: 5px; 170 | display: flex; 171 | justify-content: space-between; 172 | } 173 | 174 | .form-control-tabs-segmented-flex { 175 | color: sienna; 176 | background-color: #f8f9fa; 177 | padding: 10px; 178 | border-radius: 5px; 179 | display: flex; 180 | justify-content: space-between; 181 | } 182 | 183 | .form-control-tabs-segmented-v2-dark { 184 | color: tan; 185 | background-color: #333; 186 | padding: 10px; 187 | border-radius: 5px; 188 | display: flex; 189 | justify-content: space-between; 190 | } 191 | 192 | .form-control-tabs-segmented-v3 { 193 | color: turquoise; 194 | background-color: #fff; 195 | padding: 10px; 196 | border-radius: 5px; 197 | display: flex; 198 | justify-content: space-between; 199 | } 200 | 201 | .form-control-tabs-segmented-v4 { 202 | color: violet; 203 | background-color: #f8f9fa; 204 | padding: 10px; 205 | border-radius: 5px; 206 | display: flex; 207 | justify-content: space-between; 208 | } 209 | 210 | .form-control-tabs-segmented { 211 | color: wheat; 212 | background-color: #fff; 213 | padding: 10px; 214 | border-radius: 5px; 215 | display: flex; 216 | justify-content: space-between; 217 | } 218 | 219 | .custom-control-switcher { 220 | color: azure; 221 | display: flex; 222 | align-items: center; 223 | } 224 | 225 | // 50 more random classes 226 | .random-class-1 { 227 | background-color: #f0f0f0; 228 | } 229 | .random-class-2 { 230 | background-color: #e0e0e0; 231 | } 232 | .random-class-3 { 233 | background-color: #d0d0d0; 234 | } 235 | .random-class-4 { 236 | background-color: #c0c0c0; 237 | } 238 | .random-class-5 { 239 | background-color: #b0b0b0; 240 | } 241 | .random-class-6 { 242 | background-color: #a0a0a0; 243 | } 244 | .random-class-7 { 245 | background-color: #909090; 246 | } 247 | .random-class-8 { 248 | background-color: #808080; 249 | } 250 | .random-class-9 { 251 | background-color: #707070; 252 | } 253 | .random-class-10 { 254 | background-color: #606060; 255 | } 256 | .random-class-11 { 257 | background-color: #505050; 258 | } 259 | .random-class-12 { 260 | background-color: #404040; 261 | } 262 | .random-class-13 { 263 | background-color: #303030; 264 | } 265 | .random-class-14 { 266 | background-color: #202020; 267 | } 268 | .random-class-15 { 269 | background-color: #101010; 270 | } 271 | .random-class-16 { 272 | background-color: #f8f8f8; 273 | } 274 | .random-class-17 { 275 | background-color: #e8e8e8; 276 | } 277 | .random-class-18 { 278 | background-color: #d8d8d8; 279 | } 280 | .random-class-19 { 281 | background-color: #c8c8c8; 282 | } 283 | .random-class-20 { 284 | background-color: #b8b8b8; 285 | } 286 | .random-class-21 { 287 | background-color: #a8a8a8; 288 | } 289 | .random-class-22 { 290 | background-color: #989898; 291 | } 292 | .random-class-23 { 293 | background-color: #888888; 294 | } 295 | .random-class-24 { 296 | background-color: #787878; 297 | } 298 | .random-class-25 { 299 | background-color: #686868; 300 | } 301 | .random-class-26 { 302 | background-color: #585858; 303 | } 304 | .random-class-27 { 305 | background-color: #484848; 306 | } 307 | .random-class-28 { 308 | background-color: #383838; 309 | } 310 | .random-class-29 { 311 | background-color: #282828; 312 | } 313 | .random-class-30 { 314 | background-color: #181818; 315 | } 316 | .random-class-31 { 317 | background-color: #080808; 318 | } 319 | .random-class-32 { 320 | background-color: #fefefe; 321 | } 322 | .random-class-33 { 323 | background-color: #ededed; 324 | } 325 | .random-class-34 { 326 | background-color: #dcdcdc; 327 | } 328 | .random-class-35 { 329 | background-color: #cbcbcb; 330 | } 331 | .random-class-36 { 332 | background-color: #bababa; 333 | } 334 | .random-class-37 { 335 | background-color: #a9a9a9; 336 | } 337 | .random-class-38 { 338 | background-color: #989898; 339 | } 340 | .random-class-39 { 341 | background-color: #878787; 342 | } 343 | .random-class-40 { 344 | background-color: #767676; 345 | } 346 | .random-class-41 { 347 | background-color: #656565; 348 | } 349 | .random-class-42 { 350 | background-color: #545454; 351 | } 352 | .random-class-43 { 353 | background-color: #434343; 354 | } 355 | .random-class-44 { 356 | background-color: #323232; 357 | } 358 | .random-class-45 { 359 | background-color: #212121; 360 | } 361 | .random-class-46 { 362 | background-color: #101010; 363 | } 364 | .random-class-47 { 365 | background-color: #f7f7f7; 366 | } 367 | .random-class-48 { 368 | background-color: #e6e6e6; 369 | } 370 | .random-class-49 { 371 | background-color: #d5d5d5; 372 | } 373 | .random-class-50 { 374 | background-color: #c4c4c4; 375 | } 376 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/get-ds-component-data.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolSchemaOptions } from '@push-based/models'; 2 | import { 3 | createHandler, 4 | BaseHandlerOptions, 5 | } from '../shared/utils/handler-helpers.js'; 6 | import { COMMON_ANNOTATIONS } from '../shared/models/schema-helpers.js'; 7 | import { getComponentPathsInfo } from './utils/paths-helpers.js'; 8 | import { getComponentDocPathsForName } from './utils/doc-helpers.js'; 9 | import { 10 | validateComponentName, 11 | componentNameToKebabCase, 12 | } from '../shared/utils/component-validation.js'; 13 | import { resolveCrossPlatformPath } from '../shared/utils/cross-platform-path.js'; 14 | import * as fs from 'fs'; 15 | import * as path from 'path'; 16 | 17 | interface DsComponentDataOptions extends BaseHandlerOptions { 18 | componentName: string; 19 | sections?: string[]; 20 | } 21 | 22 | interface DsComponentData { 23 | componentName: string; 24 | implementation: string[]; 25 | documentation: string[]; 26 | stories: string[]; 27 | importPath: string; 28 | } 29 | 30 | export const getDsComponentDataToolSchema: ToolSchemaOptions = { 31 | name: 'get-ds-component-data', 32 | description: `Return comprehensive data for a DS component including implementation files, documentation files, stories files, and import path.`, 33 | inputSchema: { 34 | type: 'object', 35 | properties: { 36 | componentName: { 37 | type: 'string', 38 | description: 39 | 'The class name of the component to get data for (e.g., DsBadge)', 40 | }, 41 | sections: { 42 | type: 'array', 43 | items: { 44 | type: 'string', 45 | enum: ['implementation', 'documentation', 'stories', 'all'], 46 | }, 47 | description: 48 | 'Sections to include in the response. Options: "implementation", "documentation", "stories", "all". Defaults to ["all"] if not specified.', 49 | default: ['all'], 50 | }, 51 | }, 52 | required: ['componentName'], 53 | }, 54 | annotations: { 55 | title: 'Get Design System Component Data', 56 | ...COMMON_ANNOTATIONS.readOnly, 57 | }, 58 | }; 59 | 60 | function getAllFilesInDirectory(dirPath: string): string[] { 61 | const files: string[] = []; 62 | 63 | function walkDirectory(currentPath: string) { 64 | try { 65 | const items = fs.readdirSync(currentPath); 66 | 67 | for (const item of items) { 68 | const fullPath = path.join(currentPath, item); 69 | const stat = fs.statSync(fullPath); 70 | 71 | if (stat.isDirectory()) { 72 | walkDirectory(fullPath); 73 | } else { 74 | files.push(fullPath); 75 | } 76 | } 77 | } catch { 78 | return; 79 | } 80 | } 81 | 82 | if (fs.existsSync(dirPath)) { 83 | walkDirectory(dirPath); 84 | } 85 | 86 | return files; 87 | } 88 | 89 | function findStoriesFiles(componentPath: string): string[] { 90 | const storiesFiles: string[] = []; 91 | 92 | try { 93 | if (fs.existsSync(componentPath)) { 94 | const items = fs.readdirSync(componentPath); 95 | 96 | for (const item of items) { 97 | const fullPath = path.join(componentPath, item); 98 | const stat = fs.statSync(fullPath); 99 | 100 | if (stat.isFile() && item.endsWith('.stories.ts')) { 101 | storiesFiles.push(fullPath); 102 | } 103 | } 104 | } 105 | } catch { 106 | return storiesFiles; 107 | } 108 | 109 | return storiesFiles; 110 | } 111 | 112 | export const getDsComponentDataHandler = createHandler< 113 | DsComponentDataOptions, 114 | DsComponentData 115 | >( 116 | getDsComponentDataToolSchema.name, 117 | async ( 118 | { componentName, sections = ['all'] }, 119 | { cwd, uiRoot, storybookDocsRoot }, 120 | ) => { 121 | try { 122 | validateComponentName(componentName); 123 | 124 | const includeAll = sections.includes('all'); 125 | const includeImplementation = 126 | includeAll || sections.includes('implementation'); 127 | const includeDocumentation = 128 | includeAll || sections.includes('documentation'); 129 | const includeStories = includeAll || sections.includes('stories'); 130 | 131 | const pathsInfo = getComponentPathsInfo(componentName, uiRoot, cwd); 132 | 133 | let implementationFiles: string[] = []; 134 | if (includeImplementation) { 135 | const srcFiles = getAllFilesInDirectory(pathsInfo.srcPath); 136 | implementationFiles = srcFiles.map((file) => `file://${file}`); 137 | } 138 | 139 | const documentationFiles: string[] = []; 140 | 141 | let storiesFilePaths: string[] = []; 142 | if (storybookDocsRoot) { 143 | const docsBasePath = resolveCrossPlatformPath(cwd, storybookDocsRoot); 144 | 145 | if (includeDocumentation) { 146 | const docPaths = getComponentDocPathsForName( 147 | docsBasePath, 148 | componentName, 149 | ); 150 | 151 | if (fs.existsSync(docPaths.paths.api)) { 152 | documentationFiles.push(`file://${docPaths.paths.api}`); 153 | } 154 | if (fs.existsSync(docPaths.paths.overview)) { 155 | documentationFiles.push(`file://${docPaths.paths.overview}`); 156 | } 157 | } 158 | 159 | if (includeStories) { 160 | const componentFolderName = componentNameToKebabCase(componentName); 161 | const storiesComponentFolderPath = path.join( 162 | docsBasePath, 163 | componentFolderName, 164 | ); 165 | const storiesFiles = findStoriesFiles(storiesComponentFolderPath); 166 | storiesFilePaths = storiesFiles.map((file) => `file://${file}`); 167 | } 168 | } 169 | 170 | return { 171 | componentName, 172 | implementation: implementationFiles, 173 | documentation: documentationFiles, 174 | stories: storiesFilePaths, 175 | importPath: pathsInfo.importPath, 176 | }; 177 | } catch (ctx) { 178 | throw new Error( 179 | `Error retrieving component data: ${(ctx as Error).message}`, 180 | ); 181 | } 182 | }, 183 | (result) => { 184 | const messages: string[] = []; 185 | 186 | if (result.implementation && result.implementation.length > 0) { 187 | messages.push('Implementation'); 188 | messages.push(''); 189 | result.implementation.forEach((file: string) => { 190 | messages.push(file); 191 | }); 192 | messages.push(''); 193 | } 194 | 195 | if (result.documentation && result.documentation.length > 0) { 196 | messages.push('Documentation'); 197 | messages.push(''); 198 | result.documentation.forEach((file: string) => { 199 | messages.push(file); 200 | }); 201 | messages.push(''); 202 | } 203 | 204 | if (result.stories && result.stories.length > 0) { 205 | messages.push('Stories'); 206 | messages.push(''); 207 | result.stories.forEach((file: string) => { 208 | messages.push(file); 209 | }); 210 | messages.push(''); 211 | } 212 | 213 | if (result.importPath) { 214 | messages.push('Import path'); 215 | messages.push(''); 216 | messages.push(result.importPath); 217 | } 218 | 219 | return messages; 220 | }, 221 | ); 222 | 223 | export const getDsComponentDataTools = [ 224 | { 225 | schema: getDsComponentDataToolSchema, 226 | handler: getDsComponentDataHandler, 227 | }, 228 | ]; 229 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/project/utils/dependencies-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import process from 'node:process'; 4 | import { getComponentPathsInfo } from '../../component/utils/paths-helpers.js'; 5 | import { resolveCrossPlatformPathAndValidateWithContext } from '../../shared/utils/cross-platform-path.js'; 6 | 7 | // Type definitions 8 | export interface PackageJsonPeerDependencies { 9 | [packageName: string]: string; 10 | } 11 | 12 | export interface PackageJson { 13 | name?: string; 14 | version?: string; 15 | peerDependencies?: PackageJsonPeerDependencies; 16 | dependencies?: Record<string, string>; 17 | devDependencies?: Record<string, string>; 18 | [key: string]: unknown; 19 | } 20 | 21 | export interface ProjectAnalysisResult { 22 | packageJsonFound: true; 23 | packageJsonPath: string; 24 | projectJsonPath: string | null; 25 | isPublishable: boolean; 26 | peerDependencies: PackageJsonPeerDependencies; 27 | peerDependencyMissing: boolean; 28 | importPath?: string; 29 | message?: string; 30 | suggestedChange?: { 31 | importPath: string; 32 | message: string; 33 | }; 34 | } 35 | 36 | export interface ProjectAnalysisNotFoundResult { 37 | packageJsonFound: false; 38 | searchedPath: string; 39 | message: string; 40 | } 41 | 42 | export type ProjectAnalysisResponse = 43 | | ProjectAnalysisResult 44 | | ProjectAnalysisNotFoundResult; 45 | 46 | export interface ComponentMetadata { 47 | importPath?: string; 48 | [key: string]: unknown; 49 | } 50 | 51 | /** 52 | * Finds package.json by traversing up the directory tree 53 | */ 54 | export function findPackageJson(startPath: string): string | null { 55 | let currentPath = path.resolve(startPath); 56 | const rootPath = path.parse(currentPath).root; 57 | 58 | while (currentPath !== rootPath) { 59 | const packageJsonPath = path.join(currentPath, 'package.json'); 60 | 61 | if (fs.existsSync(packageJsonPath)) { 62 | return packageJsonPath; 63 | } 64 | 65 | currentPath = path.dirname(currentPath); 66 | } 67 | 68 | return null; 69 | } 70 | 71 | /** 72 | * Finds project.json in the same directory as package.json or nearby 73 | */ 74 | export function findProjectJson(packageJsonDir: string): string | null { 75 | const projectJsonPath = path.join(packageJsonDir, 'project.json'); 76 | 77 | if (fs.existsSync(projectJsonPath)) { 78 | return projectJsonPath; 79 | } 80 | 81 | return null; 82 | } 83 | 84 | /** 85 | * Reads and parses package.json file 86 | */ 87 | export function readPackageJson(packageJsonPath: string): PackageJson { 88 | try { 89 | const content = fs.readFileSync(packageJsonPath, 'utf-8'); 90 | return JSON.parse(content) as PackageJson; 91 | } catch (ctx) { 92 | throw new Error( 93 | `Failed to read or parse package.json at ${packageJsonPath}: ${ 94 | (ctx as Error).message 95 | }`, 96 | ); 97 | } 98 | } 99 | 100 | /** 101 | * Checks if the library has peer dependencies (indicating it's buildable/publishable) 102 | */ 103 | export function hasPeerDependencies(packageJson: PackageJson): boolean { 104 | return ( 105 | packageJson.peerDependencies !== undefined && 106 | typeof packageJson.peerDependencies === 'object' && 107 | Object.keys(packageJson.peerDependencies).length > 0 108 | ); 109 | } 110 | 111 | /** 112 | * Gets import path from component metadata 113 | */ 114 | export async function getComponentImportPath( 115 | componentName: string, 116 | cwd: string, 117 | uiRoot: string, 118 | ): Promise<string | null> { 119 | try { 120 | const pathsInfo = getComponentPathsInfo(componentName, uiRoot, cwd); 121 | return pathsInfo.importPath || null; 122 | } catch { 123 | return null; 124 | } 125 | } 126 | 127 | /** 128 | * Builds the base project analysis result from package.json and project.json 129 | */ 130 | export function buildProjectAnalysisResult( 131 | cwd: string, 132 | packageJsonPath: string, 133 | packageJson: PackageJson, 134 | ): ProjectAnalysisResult { 135 | const packageJsonDir = path.dirname(packageJsonPath); 136 | const hasPackagePeerDeps = hasPeerDependencies(packageJson); 137 | const projectJsonPath = findProjectJson(packageJsonDir); 138 | 139 | return { 140 | packageJsonFound: true, 141 | packageJsonPath: path.relative(cwd, packageJsonPath), 142 | projectJsonPath: projectJsonPath 143 | ? path.relative(cwd, projectJsonPath) 144 | : null, 145 | isPublishable: hasPackagePeerDeps, 146 | peerDependencies: packageJson.peerDependencies || {}, 147 | peerDependencyMissing: false, 148 | }; 149 | } 150 | 151 | /** 152 | * Handles peer dependencies analysis and component import path validation 153 | */ 154 | export async function handlePeerDependenciesAnalysis( 155 | result: ProjectAnalysisResult, 156 | componentName?: string, 157 | cwd?: string, 158 | uiRoot?: string, 159 | ): Promise<void> { 160 | if (!result.isPublishable) { 161 | result.message = 162 | 'Library has no peer dependencies - appears to be a normal library'; 163 | return; 164 | } 165 | 166 | if (!componentName) { 167 | result.message = 168 | 'Library has peer dependencies (publishable/buildable). Provide componentName to validate import path.'; 169 | return; 170 | } 171 | 172 | if (!cwd) { 173 | result.message = 'CWD is required for component import path validation.'; 174 | return; 175 | } 176 | 177 | if (!uiRoot) { 178 | result.message = 179 | 'UI root is required for component import path validation.'; 180 | return; 181 | } 182 | 183 | // Try to get import path for the component 184 | const importPath = await getComponentImportPath(componentName, cwd, uiRoot); 185 | 186 | if (!importPath || importPath.trim() === '') { 187 | result.peerDependencyMissing = true; 188 | result.suggestedChange = { 189 | importPath: '*', 190 | message: 191 | 'Component import path is missing or empty. This is required for publishable libraries.', 192 | }; 193 | } else { 194 | result.importPath = importPath; 195 | result.message = 196 | 'Component import path found - library appears properly configured'; 197 | } 198 | } 199 | 200 | /** 201 | * Analyzes project dependencies and determines if library is buildable/publishable 202 | */ 203 | export async function analyzeProjectDependencies( 204 | cwd: string, 205 | directory: string, 206 | componentName?: string, 207 | workspaceRoot?: string, 208 | uiRoot?: string, 209 | ): Promise<ProjectAnalysisResponse> { 210 | // Parameter validation 211 | if (!directory || typeof directory !== 'string') { 212 | throw new Error('Directory parameter is required and must be a string'); 213 | } 214 | 215 | // Set working directory 216 | process.chdir(cwd); 217 | 218 | // Validate target path exists 219 | const targetPath = resolveCrossPlatformPathAndValidateWithContext( 220 | cwd, 221 | directory, 222 | workspaceRoot, 223 | ); 224 | 225 | // Find package.json 226 | const packageJsonPath = findPackageJson(targetPath); 227 | 228 | if (!packageJsonPath) { 229 | return { 230 | packageJsonFound: false, 231 | searchedPath: targetPath, 232 | message: 'No package.json found in the directory tree', 233 | }; 234 | } 235 | 236 | // Read and parse package.json 237 | const packageJson = readPackageJson(packageJsonPath); 238 | 239 | // Build base result 240 | const result = buildProjectAnalysisResult(cwd, packageJsonPath, packageJson); 241 | 242 | // Handle peer dependencies analysis 243 | await handlePeerDependenciesAnalysis(result, componentName, cwd, uiRoot); 244 | 245 | return result; 246 | } 247 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/utils/cross-platform-path.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { toUnixPath } from '@code-pushup/utils'; 4 | 5 | /** 6 | * Enhanced path resolution with workspace root context for better debugging 7 | */ 8 | export function resolveCrossPlatformPathWithContext( 9 | basePath: string, 10 | relativePath: string, 11 | workspaceRoot?: string, 12 | ): { 13 | resolved: string; 14 | context: { 15 | basePath: string; 16 | relativePath: string; 17 | workspaceRoot?: string; 18 | isBasePathSameAsWorkspaceRoot: boolean; 19 | relativeToWorkspaceRoot?: string; 20 | }; 21 | } { 22 | const normalizedRelative = relativePath.replace(/\\/g, '/'); 23 | const resolved = path.resolve(basePath, normalizedRelative); 24 | const unixResolved = toUnixPath(resolved); 25 | 26 | const context = { 27 | basePath: toUnixPath(basePath), 28 | relativePath: normalizedRelative, 29 | workspaceRoot: workspaceRoot ? toUnixPath(workspaceRoot) : undefined, 30 | isBasePathSameAsWorkspaceRoot: workspaceRoot 31 | ? toUnixPath(basePath) === toUnixPath(workspaceRoot) 32 | : false, 33 | relativeToWorkspaceRoot: workspaceRoot 34 | ? path.relative(workspaceRoot, unixResolved) 35 | : undefined, 36 | }; 37 | 38 | return { resolved: unixResolved, context }; 39 | } 40 | 41 | /** 42 | * Enhanced validation with comprehensive workspace root context 43 | */ 44 | export function resolveCrossPlatformPathAndValidateWithContext( 45 | basePath: string, 46 | relativePath: string, 47 | workspaceRoot?: string, 48 | ): string { 49 | const { resolved, context } = resolveCrossPlatformPathWithContext( 50 | basePath, 51 | relativePath, 52 | workspaceRoot, 53 | ); 54 | 55 | // Convert to platform-specific for fs operations 56 | const fsPath = resolved.replace(/\//g, path.sep); 57 | 58 | if (!fs.existsSync(fsPath)) { 59 | let errorMessage = 60 | `Directory does not exist: ${relativePath}\n` + 61 | `Resolved to: ${resolved}\n` + 62 | `Base path: ${basePath}`; 63 | 64 | if (workspaceRoot) { 65 | errorMessage += 66 | `\n` + 67 | `Workspace root: ${workspaceRoot}\n` + 68 | `Base path same as workspace root: ${context.isBasePathSameAsWorkspaceRoot}\n` + 69 | `Path relative to workspace root: ${context.relativeToWorkspaceRoot}`; 70 | 71 | if (!context.isBasePathSameAsWorkspaceRoot) { 72 | errorMessage += 73 | `\n` + 74 | `⚠️ WARNING: Base path differs from workspace root!\n` + 75 | ` This might indicate a configuration issue.`; 76 | } 77 | } 78 | 79 | throw new Error(errorMessage); 80 | } 81 | 82 | return resolved; 83 | } 84 | 85 | /** 86 | * Resolves a relative path against a base path with cross-platform normalization. 87 | * Handles mixed path separators and ensures consistent Unix-style output. 88 | * 89 | * @param basePath - The base directory (usually absolute) 90 | * @param relativePath - The relative path to resolve 91 | * @returns Normalized absolute path with Unix-style separators 92 | */ 93 | export function resolveCrossPlatformPath( 94 | basePath: string, 95 | relativePath: string, 96 | ): string { 97 | return resolveCrossPlatformPathWithContext(basePath, relativePath).resolved; 98 | } 99 | 100 | /** 101 | * Resolves a relative path against a base path and validates that it exists. 102 | * Provides helpful error messages for debugging path issues. 103 | * 104 | * @param basePath - The base directory (usually absolute) 105 | * @param relativePath - The relative path to resolve 106 | * @returns Normalized absolute path with Unix-style separators 107 | * @throws Error if the resolved path does not exist 108 | */ 109 | export function resolveCrossPlatformPathAndValidate( 110 | basePath: string, 111 | relativePath: string, 112 | ): string { 113 | return resolveCrossPlatformPathAndValidateWithContext(basePath, relativePath); 114 | } 115 | 116 | /** 117 | * Legacy replacement for validateTargetPath function. 118 | * Enhanced version that includes workspace root context when available. 119 | * 120 | * @param cwd - Current working directory (base path) 121 | * @param directory - Relative directory path to validate 122 | * @param workspaceRoot - Optional workspace root for enhanced error context 123 | * @returns Normalized absolute path with Unix-style separators 124 | * @throws Error if the resolved path does not exist 125 | */ 126 | export function validateTargetPath( 127 | cwd: string, 128 | directory: string, 129 | workspaceRoot?: string, 130 | ): string { 131 | return resolveCrossPlatformPathAndValidateWithContext( 132 | cwd, 133 | directory, 134 | workspaceRoot, 135 | ); 136 | } 137 | 138 | /** 139 | * Converts absolute paths to relative paths based on a workspace root. 140 | * Handles file paths with line/column annotations (e.g., "file.html@44:9") 141 | * 142 | * @param absolutePath - The absolute path to normalize 143 | * @param workspaceRoot - The workspace root to make paths relative to 144 | * @returns The normalized relative path 145 | */ 146 | export function normalizeAbsolutePathToRelative( 147 | absolutePath: string, 148 | workspaceRoot: string, 149 | ): string { 150 | // Handle paths with line/column annotations (e.g., "file.html@44:9") 151 | const [filePath, annotation] = absolutePath.split('@'); 152 | 153 | // Convert to Unix-style paths for consistent processing 154 | const normalizedFilePath = toUnixPath(filePath); 155 | const normalizedWorkspaceRoot = toUnixPath(workspaceRoot); 156 | 157 | // If the path is already relative or doesn't start with workspace root, return as-is 158 | if ( 159 | !path.isAbsolute(normalizedFilePath) || 160 | !normalizedFilePath.startsWith(normalizedWorkspaceRoot) 161 | ) { 162 | return absolutePath; 163 | } 164 | 165 | // Calculate relative path 166 | const relativePath = path.relative( 167 | normalizedWorkspaceRoot, 168 | normalizedFilePath, 169 | ); 170 | 171 | // Reconstruct with annotation if it existed 172 | return annotation ? `${relativePath}@${annotation}` : relativePath; 173 | } 174 | 175 | /** 176 | * Recursively normalizes all absolute paths in an object to relative paths 177 | * 178 | * @param obj - The object to process 179 | * @param workspaceRoot - The workspace root to make paths relative to 180 | * @returns The object with normalized paths 181 | */ 182 | export function normalizePathsInObject<T>(obj: T, workspaceRoot: string): T { 183 | if (obj === null || obj === undefined) { 184 | return obj; 185 | } 186 | 187 | if (typeof obj === 'string') { 188 | // Check if this looks like an absolute path that starts with workspace root 189 | const normalizedWorkspaceRoot = toUnixPath(workspaceRoot); 190 | const normalizedObj = toUnixPath(obj); 191 | 192 | if ( 193 | path.isAbsolute(normalizedObj) && 194 | normalizedObj.startsWith(normalizedWorkspaceRoot) 195 | ) { 196 | return normalizeAbsolutePathToRelative(obj, workspaceRoot) as T; 197 | } 198 | return obj; 199 | } 200 | 201 | if (Array.isArray(obj)) { 202 | return obj.map((item) => normalizePathsInObject(item, workspaceRoot)) as T; 203 | } 204 | 205 | if (typeof obj === 'object') { 206 | const result = {} as T; 207 | for (const [key, value] of Object.entries(obj)) { 208 | (result as any)[key] = normalizePathsInObject(value, workspaceRoot); 209 | } 210 | return result; 211 | } 212 | 213 | return obj; 214 | } 215 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/first-case/dashboard-header.component.html: -------------------------------------------------------------------------------- ```html 1 | <header class="dashboard-header"> 2 | <!-- Logo and Title Section --> 3 | <div class="header-brand"> 4 | <div class="brand-logo"> 5 | <svg width="32" height="32" viewBox="0 0 32 32" class="logo-icon"> 6 | <circle cx="16" cy="16" r="14" fill="#4F46E5"/> 7 | <path d="M12 16l4 4 8-8" stroke="white" stroke-width="2" fill="none"/> 8 | </svg> 9 | </div> 10 | <h1 class="brand-title">Dashboard Pro</h1> 11 | 12 | <!-- DsBadge Component --> 13 | <ds-badge 14 | [size]="badgeSize() === 'large' ? 'medium' : badgeSize() === 'medium' ? 'medium' : 'xsmall'" 15 | [variant]="getBadgeVariant()"> 16 | <!-- Start slot for icon --> 17 | <span slot="start"> 18 | @if (!hasIconSlot()) { 19 | <span class="offer-badge-default-icon">🎯</span> 20 | } 21 | <ng-content select="[slot=start]" /> 22 | </span> 23 | <!-- Main content --> 24 | <ng-content /> 25 | @if (!hasContent()) { 26 | {{ defaultBadgeText() }} 27 | } 28 | <!-- End slot for dismiss button --> 29 | <span slot="end"> 30 | @if (dismissible()) { 31 | <button 32 | class="offer-badge-dismiss" 33 | (click)="dismissBadge()" 34 | aria-label="Dismiss offer"> 35 | × 36 | </button> 37 | } 38 | <ng-content select="[slot=end]" /> 39 | </span> 40 | </ds-badge> 41 | </div> 42 | 43 | <!-- Search Section --> 44 | <div class="header-search"> 45 | <div *ngIf="true" class="search-container" [class.search-focused]="searchFocused()"> 46 | <svg class="search-icon" width="20" height="20" viewBox="0 0 20 20"> 47 | <path d="M9 2a7 7 0 1 1 0 14 7 7 0 0 1 0-14zM2 9a7 7 0 1 0 14 0 7 7 0 0 0-14 0z" fill="currentColor"/> 48 | <path d="m13 13 4 4-1.5 1.5-4-4" fill="currentColor"/> 49 | </svg> 50 | <input 51 | type="text" 52 | class="search-input" 53 | placeholder="Search dashboard..." 54 | [(ngModel)]="searchQuery" 55 | (focus)="searchFocused.set(true)" 56 | (blur)="searchFocused.set(false)" 57 | (keyup.enter)="performSearch()" 58 | [disabled]="searchDisabled()"> 59 | @if (searchQuery()) { 60 | <button 61 | class="search-clear" 62 | (click)="clearSearch()" 63 | aria-label="Clear search"> 64 | × 65 | </button> 66 | } 67 | </div> 68 | @if (searchSuggestions().length > 0 && searchFocused()) { 69 | <div class="search-suggestions"> 70 | @for (suggestion of searchSuggestions(); track suggestion.id) { 71 | <button 72 | class="suggestion-item" 73 | (click)="selectSuggestion(suggestion)"> 74 | {{ suggestion.text }} 75 | </button> 76 | } 77 | </div> 78 | } 79 | </div> 80 | 81 | <!-- Actions Section --> 82 | <div class="header-actions"> 83 | <!-- Notifications --> 84 | <div class="action-item notification-container"> 85 | <button 86 | class="action-button notification-button" 87 | [class.has-notifications]="unreadNotifications() > 0" 88 | (click)="toggleNotifications()" 89 | [attr.aria-label]="'Notifications (' + unreadNotifications() + ' unread)'"> 90 | <svg width="24" height="24" viewBox="0 0 24 24" class="notification-icon"> 91 | <path d="M12 2a7 7 0 0 1 7 7v4.29l1.71 1.71a1 1 0 0 1-.71 1.71H4a1 1 0 0 1-.71-1.71L5 13.29V9a7 7 0 0 1 7-7z" fill="currentColor"/> 92 | <path d="M10 20a2 2 0 1 0 4 0" fill="currentColor"/> 93 | </svg> 94 | @if (unreadNotifications() > 0) { 95 | <span class="notification-badge">{{ unreadNotifications() }}</span> 96 | } 97 | </button> 98 | 99 | @if (showNotifications()) { 100 | <div class="notifications-dropdown"> 101 | <div class="dropdown-header"> 102 | <h3>Notifications</h3> 103 | @if (unreadNotifications() > 0) { 104 | <button 105 | class="mark-all-read" 106 | (click)="markAllNotificationsRead()"> 107 | Mark all read 108 | </button> 109 | } 110 | </div> 111 | <div class="notifications-list"> 112 | @for (notification of notifications(); track notification.id) { 113 | <div 114 | class="notification-item" 115 | [class.notification-unread]="!notification.read" 116 | [class.notification-{{ notification.type }}]="true"> 117 | <div class="notification-content"> 118 | <h4 class="notification-title">{{ notification.title }}</h4> 119 | <p class="notification-message">{{ notification.message }}</p> 120 | <span class="notification-time">{{ formatTime(notification.timestamp) }}</span> 121 | </div> 122 | <button 123 | class="notification-dismiss" 124 | (click)="dismissNotification(notification.id)" 125 | aria-label="Dismiss notification"> 126 | × 127 | </button> 128 | </div> 129 | } @empty { 130 | <div class="no-notifications"> 131 | <p>No notifications</p> 132 | </div> 133 | } 134 | </div> 135 | </div> 136 | } 137 | </div> 138 | 139 | <!-- User Menu --> 140 | <div class="action-item user-container"> 141 | <button 142 | class="action-button user-button" 143 | (click)="toggleUserMenu()" 144 | [attr.aria-label]="'User menu for ' + userProfile()?.name"> 145 | @if (userProfile()?.avatar) { 146 | <img 147 | [src]="userProfile()!.avatar" 148 | [alt]="userProfile()!.name" 149 | class="user-avatar"> 150 | } @else { 151 | <div class="user-avatar-placeholder"> 152 | {{ getUserInitials() }} 153 | </div> 154 | } 155 | <svg class="dropdown-arrow" width="12" height="12" viewBox="0 0 12 12"> 156 | <path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none"/> 157 | </svg> 158 | </button> 159 | 160 | @if (showUserMenu()) { 161 | <div class="user-dropdown"> 162 | <div class="user-info"> 163 | <div class="user-details"> 164 | <h4>{{ userProfile()?.name }}</h4> 165 | <p>{{ userProfile()?.email }}</p> 166 | <span class="user-role">{{ userProfile()?.role }}</span> 167 | </div> 168 | </div> 169 | <div class="user-actions"> 170 | <button class="dropdown-item" (click)="navigateToProfile()"> 171 | Profile Settings 172 | </button> 173 | <button class="dropdown-item" (click)="navigateToPreferences()"> 174 | Preferences 175 | </button> 176 | <button class="dropdown-item" (click)="toggleTheme()"> 177 | {{ darkMode() ? 'Light Mode' : 'Dark Mode' }} 178 | </button> 179 | <hr class="dropdown-divider"> 180 | <button class="dropdown-item logout" (click)="logout()"> 181 | Sign Out 182 | </button> 183 | </div> 184 | </div> 185 | } 186 | </div> 187 | </div> 188 | </header> ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/component/list-ds-components.tool.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolSchemaOptions } from '@push-based/models'; 2 | import { 3 | createHandler, 4 | BaseHandlerOptions, 5 | } from '../shared/utils/handler-helpers.js'; 6 | import { COMMON_ANNOTATIONS } from '../shared/models/schema-helpers.js'; 7 | import { getComponentPathsInfo } from './utils/paths-helpers.js'; 8 | import { getComponentDocPathsForName } from './utils/doc-helpers.js'; 9 | import { resolveCrossPlatformPath } from '../shared/utils/cross-platform-path.js'; 10 | import * as fs from 'fs'; 11 | import * as path from 'path'; 12 | 13 | interface DsComponentInfo { 14 | componentName: string; 15 | folderName: string; 16 | implementation: string[]; 17 | documentation: string[]; 18 | stories: string[]; 19 | importPath: string; 20 | } 21 | 22 | interface ListDsComponentsOptions extends BaseHandlerOptions { 23 | sections?: string[]; 24 | } 25 | 26 | export const listDsComponentsToolSchema: ToolSchemaOptions = { 27 | name: 'list-ds-components', 28 | description: `List all available Design System components in the project. Returns component names, folder structures, import paths, implementation files, documentation files, and stories files.`, 29 | inputSchema: { 30 | type: 'object', 31 | properties: { 32 | sections: { 33 | type: 'array', 34 | items: { 35 | type: 'string', 36 | enum: ['implementation', 'documentation', 'stories', 'all'], 37 | }, 38 | description: 39 | 'Sections to include in the response. Options: "implementation", "documentation", "stories", "all". Defaults to ["all"] if not specified.', 40 | default: ['all'], 41 | }, 42 | }, 43 | required: [], 44 | }, 45 | annotations: { 46 | title: 'List Design System Components', 47 | ...COMMON_ANNOTATIONS.readOnly, 48 | }, 49 | }; 50 | 51 | function getAllFilesInDirectory(dirPath: string): string[] { 52 | const files: string[] = []; 53 | 54 | function walkDirectory(currentPath: string) { 55 | try { 56 | const items = fs.readdirSync(currentPath); 57 | 58 | for (const item of items) { 59 | const fullPath = path.join(currentPath, item); 60 | const stat = fs.statSync(fullPath); 61 | 62 | if (stat.isDirectory()) { 63 | walkDirectory(fullPath); 64 | } else { 65 | files.push(fullPath); 66 | } 67 | } 68 | } catch { 69 | return; 70 | } 71 | } 72 | 73 | if (fs.existsSync(dirPath)) { 74 | walkDirectory(dirPath); 75 | } 76 | 77 | return files; 78 | } 79 | 80 | function kebabCaseToPascalCase(kebabCase: string): string { 81 | return ( 82 | 'Ds' + 83 | kebabCase 84 | .split('-') 85 | .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) 86 | .join('') 87 | ); 88 | } 89 | 90 | function isValidComponentFolder(folderPath: string): boolean { 91 | const packageJsonPath = path.join(folderPath, 'package.json'); 92 | const srcPath = path.join(folderPath, 'src'); 93 | 94 | return ( 95 | fs.existsSync(packageJsonPath) && 96 | fs.existsSync(srcPath) && 97 | fs.statSync(srcPath).isDirectory() 98 | ); 99 | } 100 | 101 | function findStoriesFiles(componentPath: string): string[] { 102 | const storiesFiles: string[] = []; 103 | 104 | try { 105 | if (fs.existsSync(componentPath)) { 106 | const items = fs.readdirSync(componentPath); 107 | 108 | for (const item of items) { 109 | const fullPath = path.join(componentPath, item); 110 | const stat = fs.statSync(fullPath); 111 | 112 | if (stat.isFile() && item.endsWith('.stories.ts')) { 113 | storiesFiles.push(fullPath); 114 | } 115 | } 116 | } 117 | } catch { 118 | return storiesFiles; 119 | } 120 | 121 | return storiesFiles; 122 | } 123 | 124 | export const listDsComponentsHandler = createHandler< 125 | ListDsComponentsOptions, 126 | DsComponentInfo[] 127 | >( 128 | listDsComponentsToolSchema.name, 129 | async ({ sections = ['all'] }, { cwd, uiRoot, storybookDocsRoot }) => { 130 | try { 131 | if (!uiRoot || typeof uiRoot !== 'string') { 132 | throw new Error('uiRoot must be provided and be a string path.'); 133 | } 134 | 135 | const componentsBasePath = resolveCrossPlatformPath(cwd, uiRoot); 136 | 137 | if (!fs.existsSync(componentsBasePath)) { 138 | throw new Error(`Components directory not found: ${uiRoot}`); 139 | } 140 | 141 | const entries = fs.readdirSync(componentsBasePath, { 142 | withFileTypes: true, 143 | }); 144 | const componentFolders = entries 145 | .filter((entry) => entry.isDirectory()) 146 | .map((entry) => entry.name) 147 | .filter((folderName) => { 148 | const folderPath = path.join(componentsBasePath, folderName); 149 | return isValidComponentFolder(folderPath); 150 | }); 151 | 152 | const components: DsComponentInfo[] = []; 153 | 154 | const includeAll = sections.includes('all'); 155 | const includeImplementation = 156 | includeAll || sections.includes('implementation'); 157 | const includeDocumentation = 158 | includeAll || sections.includes('documentation'); 159 | const includeStories = includeAll || sections.includes('stories'); 160 | 161 | for (const folderName of componentFolders) { 162 | try { 163 | const componentName = kebabCaseToPascalCase(folderName); 164 | 165 | const pathsInfo = getComponentPathsInfo(componentName, uiRoot, cwd); 166 | 167 | let implementationFiles: string[] = []; 168 | if (includeImplementation) { 169 | const srcFiles = getAllFilesInDirectory(pathsInfo.srcPath); 170 | implementationFiles = srcFiles.map((file) => `file://${file}`); 171 | } 172 | 173 | const documentationFiles: string[] = []; 174 | 175 | let storiesFilePaths: string[] = []; 176 | if (storybookDocsRoot) { 177 | const docsBasePath = resolveCrossPlatformPath( 178 | cwd, 179 | storybookDocsRoot, 180 | ); 181 | 182 | if (includeDocumentation) { 183 | const docPaths = getComponentDocPathsForName( 184 | docsBasePath, 185 | componentName, 186 | ); 187 | 188 | if (fs.existsSync(docPaths.paths.api)) { 189 | documentationFiles.push(`file://${docPaths.paths.api}`); 190 | } 191 | if (fs.existsSync(docPaths.paths.overview)) { 192 | documentationFiles.push(`file://${docPaths.paths.overview}`); 193 | } 194 | } 195 | 196 | if (includeStories) { 197 | const storiesComponentFolderPath = path.join( 198 | docsBasePath, 199 | folderName, 200 | ); 201 | const storiesFiles = findStoriesFiles(storiesComponentFolderPath); 202 | storiesFilePaths = storiesFiles.map((file) => `file://${file}`); 203 | } 204 | } 205 | 206 | components.push({ 207 | componentName, 208 | folderName, 209 | implementation: implementationFiles, 210 | documentation: documentationFiles, 211 | stories: storiesFilePaths, 212 | importPath: pathsInfo.importPath, 213 | }); 214 | } catch (ctx) { 215 | console.warn( 216 | `Warning: Skipped component '${folderName}': ${(ctx as Error).message}`, 217 | ); 218 | } 219 | } 220 | 221 | return components; 222 | } catch (ctx) { 223 | throw new Error(`Error listing DS components: ${(ctx as Error).message}`); 224 | } 225 | }, 226 | (components) => { 227 | const response = { 228 | totalComponents: components?.length || 0, 229 | components: components || [], 230 | }; 231 | 232 | return [JSON.stringify(response, null, 2)]; 233 | }, 234 | ); 235 | 236 | export const listDsComponentsTools = [ 237 | { 238 | schema: listDsComponentsToolSchema, 239 | handler: listDsComponentsHandler, 240 | }, 241 | ]; 242 | ``` -------------------------------------------------------------------------------- /packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/formatters.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; 2 | import { buildText } from '../utils/output.utils.js'; 3 | import { 4 | BaseViolationResult, 5 | BaseViolationAudit, 6 | BaseViolationIssue, 7 | FileGroups, 8 | PathCache, 9 | } from './types.js'; 10 | 11 | // Performance-optimized path cache 12 | const pathCache: PathCache = {}; 13 | 14 | /** 15 | * Filters audits to only include those with violations (score < 1) 16 | */ 17 | export function filterFailedAudits( 18 | result: BaseViolationResult, 19 | ): BaseViolationAudit[] { 20 | return result.audits.filter(({ score }) => score < 1); 21 | } 22 | 23 | /** 24 | * Creates standard "no violations found" content 25 | */ 26 | export function createNoViolationsContent(): CallToolResult['content'] { 27 | return [ 28 | buildText( 29 | '✅ No violations found! All files are compliant with the design system.', 30 | ), 31 | ]; 32 | } 33 | 34 | /** 35 | * Extracts all issues from failed audits 36 | */ 37 | export function extractIssuesFromAudits( 38 | audits: BaseViolationAudit[], 39 | ): BaseViolationIssue[] { 40 | return audits.flatMap(({ details }) => details?.issues ?? []); 41 | } 42 | 43 | /** 44 | * Checks if a violation result has any failures 45 | */ 46 | export function hasViolations(result: BaseViolationResult): boolean { 47 | return filterFailedAudits(result).length > 0; 48 | } 49 | 50 | /** 51 | * Performance-optimized file path normalization with caching 52 | */ 53 | export function normalizeFilePath(filePath: string, directory: string): string { 54 | const cacheKey = `${filePath}::${directory}`; 55 | 56 | if (pathCache[cacheKey]) { 57 | return pathCache[cacheKey]; 58 | } 59 | 60 | // Normalize both paths to use consistent separators 61 | const normalizedFilePath = filePath.replace(/\\/g, '/'); 62 | const normalizedDirectory = directory.replace(/\\/g, '/'); 63 | 64 | // Remove leading './' from directory if present for comparison 65 | const cleanDirectory = normalizedDirectory.startsWith('./') 66 | ? normalizedDirectory.slice(2) 67 | : normalizedDirectory; 68 | 69 | let normalized: string; 70 | 71 | // The file path from the coverage plugin is absolute, but we need to extract the relative part 72 | // Look for the directory pattern in the file path and extract everything after it 73 | const directoryPattern = cleanDirectory; 74 | const directoryIndex = normalizedFilePath.indexOf(directoryPattern); 75 | 76 | if (directoryIndex !== -1) { 77 | // Found the directory in the path, extract the part after it 78 | const afterDirectoryIndex = directoryIndex + directoryPattern.length; 79 | const afterDirectory = normalizedFilePath.slice(afterDirectoryIndex); 80 | 81 | // Remove leading slash if present 82 | normalized = afterDirectory.startsWith('/') 83 | ? afterDirectory.slice(1) 84 | : afterDirectory; 85 | } else { 86 | // Fallback: try with directory prefix approach 87 | const directoryPrefix = normalizedDirectory.endsWith('/') 88 | ? normalizedDirectory 89 | : normalizedDirectory + '/'; 90 | normalized = normalizedFilePath.startsWith(directoryPrefix) 91 | ? normalizedFilePath.slice(directoryPrefix.length) 92 | : normalizedFilePath; 93 | } 94 | 95 | pathCache[cacheKey] = normalized; 96 | return normalized; 97 | } 98 | 99 | /** 100 | * Performance-optimized message normalization with caching 101 | */ 102 | export function normalizeMessage(message: string, directory: string): string { 103 | const cacheKey = `msg::${message}::${directory}`; 104 | 105 | if (pathCache[cacheKey]) { 106 | return pathCache[cacheKey]; 107 | } 108 | 109 | const directoryPrefix = directory.endsWith('/') ? directory : directory + '/'; 110 | const normalized = message.includes(directoryPrefix) 111 | ? message.replace(directoryPrefix, '') 112 | : message; 113 | 114 | pathCache[cacheKey] = normalized; 115 | return normalized; 116 | } 117 | 118 | /** 119 | * Groups violation issues by file name - consolidated from multiple modules 120 | * Performance optimized with Set for duplicate checking and cached normalizations 121 | */ 122 | export function groupIssuesByFile( 123 | issues: BaseViolationIssue[], 124 | directory: string, 125 | ): FileGroups { 126 | const fileGroups: FileGroups = {}; 127 | const processedFiles = new Set<string>(); // O(1) lookup instead of includes() 128 | 129 | for (const { message, source } of issues) { 130 | if (!source?.file) continue; 131 | 132 | const fileName = normalizeFilePath(source.file, directory); 133 | const lineNumber = source.position?.startLine || 0; 134 | 135 | if (!fileGroups[fileName]) { 136 | fileGroups[fileName] = { 137 | message: normalizeMessage(message, directory), 138 | lines: [], 139 | }; 140 | processedFiles.add(fileName); 141 | } 142 | 143 | fileGroups[fileName].lines.push(lineNumber); 144 | } 145 | 146 | return fileGroups; 147 | } 148 | 149 | /** 150 | * Extracts unique file paths from violation issues - performance optimized 151 | */ 152 | export function extractUniqueFilePaths( 153 | issues: BaseViolationIssue[], 154 | directory: string, 155 | ): string[] { 156 | const filePathSet = new Set<string>(); // Eliminate O(n) includes() calls 157 | 158 | for (const { source } of issues) { 159 | if (source?.file) { 160 | filePathSet.add(normalizeFilePath(source.file, directory)); 161 | } 162 | } 163 | 164 | return Array.from(filePathSet); 165 | } 166 | 167 | /** 168 | * Clears the path cache - useful for testing or memory management 169 | */ 170 | export function clearPathCache(): void { 171 | Object.keys(pathCache).forEach((key) => delete pathCache[key]); 172 | } 173 | 174 | /** 175 | * Unified formatter for violations - supports both file and folder grouping with minimal output 176 | */ 177 | export function formatViolations( 178 | result: BaseViolationResult, 179 | directory: string, 180 | options: { 181 | groupBy: 'file' | 'folder'; 182 | } = { groupBy: 'file' }, 183 | ): CallToolResult['content'] { 184 | const failedAudits = filterFailedAudits(result); 185 | 186 | if (failedAudits.length === 0) { 187 | return [buildText('No violations found.')]; 188 | } 189 | 190 | const allIssues = extractIssuesFromAudits(failedAudits); 191 | const content: CallToolResult['content'] = []; 192 | 193 | if (options.groupBy === 'file') { 194 | // Group by individual files - minimal format 195 | const fileGroups = groupIssuesByFile(allIssues, directory); 196 | 197 | for (const [fileName, { message, lines }] of Object.entries(fileGroups)) { 198 | const sortedLines = lines.sort((a, b) => a - b); 199 | const lineInfo = 200 | sortedLines.length > 1 201 | ? `lines ${sortedLines.join(', ')}` 202 | : `line ${sortedLines[0]}`; 203 | 204 | content.push(buildText(`${fileName} (${lineInfo}): ${message}`)); 205 | } 206 | } else { 207 | // Group by folders - minimal format 208 | const folderGroups: Record< 209 | string, 210 | { violations: number; files: Set<string> } 211 | > = {}; 212 | 213 | for (const { source } of allIssues) { 214 | if (!source?.file) continue; 215 | 216 | const normalizedPath = normalizeFilePath(source.file, directory); 217 | const folderPath = normalizedPath.includes('/') 218 | ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) 219 | : '.'; 220 | 221 | if (!folderGroups[folderPath]) { 222 | folderGroups[folderPath] = { violations: 0, files: new Set() }; 223 | } 224 | 225 | folderGroups[folderPath].violations++; 226 | folderGroups[folderPath].files.add(normalizedPath); 227 | } 228 | 229 | // Sort folders for consistent output 230 | for (const [folder, { violations, files }] of Object.entries( 231 | folderGroups, 232 | ).sort()) { 233 | const displayPath = folder === '.' ? directory : `${directory}/${folder}`; 234 | content.push( 235 | buildText( 236 | `${displayPath}: ${violations} violations in ${files.size} files`, 237 | ), 238 | ); 239 | } 240 | } 241 | 242 | return content; 243 | } 244 | ``` -------------------------------------------------------------------------------- /packages/shared/angular-ast-utils/src/lib/decorator-config.visitor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as path from 'node:path'; 2 | import * as ts from 'typescript'; 3 | 4 | import { ParsedComponent } from './types.js'; 5 | import { parseStylesheet } from '@push-based/styles-ast-utils'; 6 | import { resolveFile } from '@push-based/utils'; 7 | import { 8 | visitAngularDecoratorProperties, 9 | visitAngularDecorators, 10 | } from './ts.walk.js'; 11 | import { 12 | assetFromPropertyArrayInitializer, 13 | assetFromPropertyValueInitializer, 14 | } from './utils.js'; 15 | import { 16 | isComponentDecorator, 17 | removeQuotes, 18 | } from '@push-based/typescript-ast-utils'; 19 | import type { 20 | ParsedTemplate, 21 | ParseTemplateOptions, 22 | } from '@angular/compiler' with { 'resolution-mode': 'import' }; 23 | 24 | const DEBUG = false; 25 | const debug = ({ 26 | step, 27 | title, 28 | info, 29 | }: { 30 | step: string; 31 | title: string; 32 | info: string | null | undefined; 33 | }) => 34 | DEBUG && 35 | console.log( 36 | `─── 📌 [${step}]: ${title} ${info ? '-' + info : ''}──────────────────────────────`, 37 | ); 38 | 39 | export async function classDecoratorVisitor({ 40 | sourceFile, 41 | }: { 42 | sourceFile: ts.SourceFile; 43 | }) { 44 | // @TODO: rethink module resolution 45 | const { parseTemplate } = await import('@angular/compiler'); 46 | const components: ParsedComponent[] = []; 47 | let activeComponent: ParsedComponent | null = null; 48 | let currentClassName: string | null = null; 49 | 50 | const visitor = (node: ts.Node): ts.VisitResult<ts.Node> => { 51 | // ─── 📌 ENTER: Class Declaration ────────────────────────────── 52 | if (ts.isClassDeclaration(node)) { 53 | currentClassName = node.name?.text ?? 'Unknown'; // Capture class name immediately 54 | debug({ 55 | step: 'ENTER', 56 | title: 'ClassDeclaration', 57 | info: `class ${currentClassName}`, 58 | }); 59 | 60 | activeComponent = { 61 | startLine: ts.getLineAndCharacterOfPosition( 62 | sourceFile, 63 | node.getStart(sourceFile), 64 | ).line, 65 | className: currentClassName, 66 | fileName: sourceFile.fileName, 67 | } as ParsedComponent; 68 | 69 | visitAngularDecorators(node, (decorator: ts.Decorator) => { 70 | debug({ 71 | step: 'ENTER', 72 | title: 'ClassDecorators', 73 | info: 'of ' + currentClassName, 74 | }); 75 | if (isComponentDecorator(decorator)) { 76 | debug({ 77 | step: 'ENTER', 78 | title: 'ClassDecorator', 79 | info: '@Component', 80 | }); 81 | visitAngularDecoratorProperties( 82 | decorator, 83 | (prop: ts.PropertyAssignment) => { 84 | if ( 85 | !ts.isPropertyAssignment(prop) || 86 | !ts.isIdentifier(prop.name) 87 | ) { 88 | return; 89 | } 90 | 91 | const propName = prop.name.escapedText as string; 92 | const getPropValue = getPropValueFactory(parseTemplate); 93 | const propValue = getPropValue( 94 | prop, 95 | sourceFile, 96 | currentClassName ?? 'undefined-class', 97 | ); 98 | if (activeComponent) { 99 | (activeComponent as ParsedComponent)[propName] = 100 | propValue as unknown as string; 101 | 102 | debug({ 103 | step: 'Update', 104 | title: 'ParsedComponent', 105 | info: `add ${propName}`, 106 | }); 107 | } 108 | }, 109 | ); 110 | 111 | if (activeComponent) { 112 | debug({ 113 | step: 'PUSH', 114 | title: 'ParsedComponent', 115 | info: `add ${activeComponent.className}`, 116 | }); 117 | components.push(activeComponent); 118 | activeComponent = null; 119 | } 120 | } 121 | }); 122 | 123 | debug({ 124 | step: 'EXIT', 125 | title: 'ClassDeclaration', 126 | info: `class ${currentClassName}`, 127 | }); 128 | currentClassName = null; 129 | } 130 | return node; 131 | }; 132 | 133 | visitor.components = components; 134 | return visitor; 135 | } 136 | 137 | function getPropValueFactory( 138 | parseTemplate: ( 139 | template: string, 140 | templateUrl: string, 141 | options?: ParseTemplateOptions, 142 | ) => ParsedTemplate, 143 | ) { 144 | return ( 145 | prop: ts.PropertyAssignment, 146 | sourceFile: ts.SourceFile, 147 | currentClassName: string, 148 | ): string | unknown => { 149 | let propName = ''; 150 | if (ts.isIdentifier(prop.name)) { 151 | propName = prop.name.escapedText as string; 152 | } else { 153 | throw new Error('Property name is not an identifier'); 154 | } 155 | switch (propName) { 156 | case 'templateUrl': 157 | case 'template': 158 | return assetFromPropertyValueInitializer({ 159 | prop, 160 | sourceFile, 161 | textParser: async (text: string) => { 162 | const filePath = 163 | propName === 'templateUrl' 164 | ? path.join(path.dirname(sourceFile.fileName), text) 165 | : sourceFile.fileName; 166 | const content = 167 | propName === 'templateUrl' ? await resolveFile(filePath) : text; 168 | 169 | debug({ 170 | step: 'RESOLVE', 171 | title: 'Template', 172 | info: `${currentClassName}; file ${filePath}`, 173 | }); 174 | 175 | return parseTemplate(content, filePath, { 176 | preserveWhitespaces: true, 177 | preserveLineEndings: true, 178 | // preserveSignificantWhitespace: true, 179 | }); 180 | }, 181 | }); 182 | case 'styleUrl': 183 | return assetFromPropertyValueInitializer({ 184 | prop, 185 | sourceFile, 186 | textParser: async (text: string) => { 187 | const filePath = path.join(path.dirname(sourceFile.fileName), text); 188 | const content = await resolveFile(filePath); 189 | 190 | debug({ 191 | step: 'RESOLVE', 192 | title: 'styleUrl', 193 | info: `${currentClassName}; file ${filePath}`, 194 | }); 195 | return parseStylesheet(content, filePath); 196 | }, 197 | }); 198 | case 'styles': 199 | if (ts.isArrayLiteralExpression(prop.initializer)) { 200 | return assetFromPropertyArrayInitializer( 201 | prop, 202 | sourceFile, 203 | async (text: string) => { 204 | debug({ 205 | step: 'RESOLVE', 206 | title: 'Styles', 207 | info: `${currentClassName}; inline-array`, 208 | }); 209 | return parseStylesheet(text, sourceFile.fileName); 210 | }, 211 | ); 212 | } 213 | 214 | return [ 215 | assetFromPropertyValueInitializer({ 216 | prop, 217 | sourceFile, 218 | textParser: async (text: string) => { 219 | debug({ 220 | step: 'RESOLVE', 221 | title: 'Styles', 222 | info: `${currentClassName}; inline-single`, 223 | }); 224 | return parseStylesheet(text, sourceFile.fileName); 225 | }, 226 | }), 227 | ]; 228 | case 'styleUrls': 229 | return assetFromPropertyArrayInitializer( 230 | prop, 231 | sourceFile, 232 | async (text: string) => { 233 | const filePath = path.join(path.dirname(sourceFile.fileName), text); 234 | const content = await resolveFile(filePath); 235 | 236 | debug({ 237 | step: 'RESOLVE', 238 | title: 'styleUrls', 239 | info: `${currentClassName}; file ${filePath}`, 240 | }); 241 | return parseStylesheet(content, filePath); 242 | }, 243 | ); 244 | default: 245 | // @TODO: Implement all of the decorator props 246 | return removeQuotes(prop.initializer, sourceFile); 247 | } 248 | }; 249 | } 250 | ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/application/src/app/components/refactoring-tests/complex-components/first-case/dashboard-header.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | ElementRef, 5 | ViewEncapsulation, 6 | computed, 7 | input, 8 | output, 9 | signal, 10 | booleanAttribute, 11 | OnInit, 12 | OnDestroy, 13 | inject, 14 | } from '@angular/core'; 15 | import { CommonModule } from '@angular/common'; 16 | import { FormsModule } from '@angular/forms'; 17 | import { DsBadge, DsBadgeVariant } from '@frontend/ui/badge'; 18 | import { Subject, interval, takeUntil } from 'rxjs'; 19 | 20 | export const OFFER_BADGE_TYPES = ['limited', 'premium', 'new', 'hot', 'sale'] as const; 21 | export type OfferBadgeType = (typeof OFFER_BADGE_TYPES)[number]; 22 | 23 | export const OFFER_BADGE_SIZES = ['small', 'medium', 'large'] as const; 24 | export type OfferBadgeSize = (typeof OFFER_BADGE_SIZES)[number]; 25 | 26 | export interface NotificationItem { 27 | id: string; 28 | title: string; 29 | message: string; 30 | timestamp: Date; 31 | read: boolean; 32 | type: 'info' | 'warning' | 'error' | 'success'; 33 | } 34 | 35 | export interface UserProfile { 36 | id: string; 37 | name: string; 38 | email: string; 39 | avatar?: string; 40 | role: string; 41 | lastLogin: Date; 42 | } 43 | 44 | @Component({ 45 | selector: 'app-dashboard-header', 46 | standalone: true, 47 | imports: [CommonModule, FormsModule, DsBadge], 48 | templateUrl: './dashboard-header.component.html', 49 | styleUrls: ['./dashboard-header.component.scss'], 50 | encapsulation: ViewEncapsulation.None, 51 | changeDetection: ChangeDetectionStrategy.OnPush, 52 | }) 53 | export class DashboardHeaderComponent implements OnInit, OnDestroy { 54 | // Offer Badge Inputs 55 | badgeType = input<OfferBadgeType>('premium'); 56 | badgeSize = input<OfferBadgeSize>('medium'); 57 | animated = input(true, { transform: booleanAttribute }); 58 | pulsing = input(false, { transform: booleanAttribute }); 59 | dismissible = input(true, { transform: booleanAttribute }); 60 | 61 | // General Inputs 62 | searchDisabled = input(false, { transform: booleanAttribute }); 63 | darkMode = input(false, { transform: booleanAttribute }); 64 | userProfile = input<UserProfile | null>(null); 65 | 66 | // Outputs 67 | searchPerformed = output<string>(); 68 | badgeDismissed = output<void>(); 69 | notificationClicked = output<NotificationItem>(); 70 | userActionClicked = output<string>(); 71 | themeToggled = output<boolean>(); 72 | 73 | // Internal State 74 | searchQuery = signal(''); 75 | searchFocused = signal(false); 76 | showNotifications = signal(false); 77 | showUserMenu = signal(false); 78 | notifications = signal<NotificationItem[]>([]); 79 | searchSuggestions = signal<{id: string, text: string}[]>([]); 80 | 81 | private destroy$ = new Subject<void>(); 82 | private elementRef = inject(ElementRef); 83 | 84 | // Computed values 85 | unreadNotifications = computed(() => 86 | this.notifications().filter(n => !n.read).length 87 | ); 88 | 89 | defaultBadgeText = computed(() => { 90 | const typeMap: Record<OfferBadgeType, string> = { 91 | 'limited': 'Limited Time', 92 | 'premium': 'Premium', 93 | 'new': 'New Feature', 94 | 'hot': 'Hot Deal', 95 | 'sale': 'On Sale' 96 | }; 97 | return typeMap[this.badgeType()]; 98 | }); 99 | 100 | getBadgeVariant = computed((): DsBadgeVariant => { 101 | const variantMap: Record<OfferBadgeType, DsBadgeVariant> = { 102 | 'premium': 'purple-strong', 103 | 'limited': 'red-strong', 104 | 'new': 'green-strong', 105 | 'hot': 'orange-strong', 106 | 'sale': 'blue-strong' 107 | }; 108 | return variantMap[this.badgeType()]; 109 | }); 110 | 111 | ngOnInit() { 112 | this.initializeMockData(); 113 | this.setupAutoRefresh(); 114 | this.setupClickOutsideHandlers(); 115 | } 116 | 117 | ngOnDestroy() { 118 | this.destroy$.next(); 119 | this.destroy$.complete(); 120 | } 121 | 122 | private initializeMockData() { 123 | // Mock notifications 124 | this.notifications.set([ 125 | { 126 | id: '1', 127 | title: 'System Update', 128 | message: 'New dashboard features are now available', 129 | timestamp: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago 130 | read: false, 131 | type: 'info' 132 | }, 133 | { 134 | id: '2', 135 | title: 'Payment Processed', 136 | message: 'Your monthly subscription has been renewed', 137 | timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago 138 | read: true, 139 | type: 'success' 140 | }, 141 | { 142 | id: '3', 143 | title: 'Storage Warning', 144 | message: 'You are running low on storage space', 145 | timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago 146 | read: false, 147 | type: 'warning' 148 | } 149 | ]); 150 | 151 | // Mock search suggestions 152 | this.searchSuggestions.set([ 153 | { id: '1', text: 'Analytics Dashboard' }, 154 | { id: '2', text: 'User Management' }, 155 | { id: '3', text: 'Settings & Preferences' }, 156 | { id: '4', text: 'Reports & Export' } 157 | ]); 158 | } 159 | 160 | private setupAutoRefresh() { 161 | // Refresh notifications every 30 seconds 162 | interval(30000) 163 | .pipe(takeUntil(this.destroy$)) 164 | .subscribe(() => { 165 | // In a real app, this would fetch new notifications 166 | console.log('Refreshing notifications...'); 167 | }); 168 | } 169 | 170 | private setupClickOutsideHandlers() { 171 | document.addEventListener('click', this.handleClickOutside.bind(this)); 172 | } 173 | 174 | private handleClickOutside(event: Event) { 175 | const target = event.target as HTMLElement; 176 | if (!this.elementRef.nativeElement.contains(target)) { 177 | this.showNotifications.set(false); 178 | this.showUserMenu.set(false); 179 | this.searchFocused.set(false); 180 | } 181 | } 182 | 183 | // Badge Methods 184 | 185 | dismissBadge() { 186 | this.badgeDismissed.emit(); 187 | } 188 | 189 | hasIconSlot(): boolean { 190 | return !!this.elementRef.nativeElement.querySelector('[slot=start]'); 191 | } 192 | 193 | hasContent(): boolean { 194 | const textContent = this.elementRef.nativeElement 195 | .querySelector('.ds-badge-text') 196 | ?.textContent?.trim(); 197 | return !!textContent; 198 | } 199 | 200 | // Search Methods 201 | performSearch() { 202 | if (this.searchQuery().trim()) { 203 | this.searchPerformed.emit(this.searchQuery()); 204 | } 205 | } 206 | 207 | clearSearch() { 208 | this.searchQuery.set(''); 209 | } 210 | 211 | selectSuggestion(suggestion: {id: string, text: string}) { 212 | this.searchQuery.set(suggestion.text); 213 | this.searchFocused.set(false); 214 | this.performSearch(); 215 | } 216 | 217 | // Notification Methods 218 | toggleNotifications() { 219 | this.showNotifications.update(show => !show); 220 | this.showUserMenu.set(false); 221 | } 222 | 223 | markAllNotificationsRead() { 224 | this.notifications.update(notifications => 225 | notifications.map(n => ({ ...n, read: true })) 226 | ); 227 | } 228 | 229 | dismissNotification(id: string) { 230 | this.notifications.update(notifications => 231 | notifications.filter(n => n.id !== id) 232 | ); 233 | } 234 | 235 | formatTime(timestamp: Date): string { 236 | const now = new Date(); 237 | const diff = now.getTime() - timestamp.getTime(); 238 | const minutes = Math.floor(diff / (1000 * 60)); 239 | const hours = Math.floor(diff / (1000 * 60 * 60)); 240 | const days = Math.floor(diff / (1000 * 60 * 60 * 24)); 241 | 242 | if (minutes < 1) return 'Just now'; 243 | if (minutes < 60) return `${minutes}m ago`; 244 | if (hours < 24) return `${hours}h ago`; 245 | return `${days}d ago`; 246 | } 247 | 248 | // User Menu Methods 249 | toggleUserMenu() { 250 | this.showUserMenu.update(show => !show); 251 | this.showNotifications.set(false); 252 | } 253 | 254 | getUserInitials(): string { 255 | const name = this.userProfile()?.name || 'User'; 256 | return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); 257 | } 258 | 259 | navigateToProfile() { 260 | this.userActionClicked.emit('profile'); 261 | this.showUserMenu.set(false); 262 | } 263 | 264 | navigateToPreferences() { 265 | this.userActionClicked.emit('preferences'); 266 | this.showUserMenu.set(false); 267 | } 268 | 269 | toggleTheme() { 270 | const newTheme = !this.darkMode(); 271 | this.themeToggled.emit(newTheme); 272 | this.showUserMenu.set(false); 273 | } 274 | 275 | logout() { 276 | this.userActionClicked.emit('logout'); 277 | this.showUserMenu.set(false); 278 | } 279 | } ``` -------------------------------------------------------------------------------- /packages/minimal-repo/packages/design-system/ui/segmented-control/src/segmented-control.component.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { FocusMonitor } from '@angular/cdk/a11y'; 2 | import { NgTemplateOutlet } from '@angular/common'; 3 | import { 4 | AfterViewInit, 5 | ChangeDetectionStrategy, 6 | Component, 7 | ElementRef, 8 | NgZone, 9 | OnDestroy, 10 | Renderer2, 11 | ViewEncapsulation, 12 | WritableSignal, 13 | booleanAttribute, 14 | contentChildren, 15 | effect, 16 | inject, 17 | input, 18 | model, 19 | signal, 20 | untracked, 21 | viewChild, 22 | viewChildren, 23 | } from '@angular/core'; 24 | 25 | import { SEGMENTED_CONTROL_OPTIONS_TOKEN } from './segmented-control.token'; 26 | import { DsSegmentedOption } from './segmented-option.component'; 27 | 28 | @Component({ 29 | selector: 'ds-segmented-control', 30 | templateUrl: './segmented-control.component.html', 31 | host: { 32 | class: `ds-segmented-control`, 33 | }, 34 | imports: [NgTemplateOutlet], 35 | encapsulation: ViewEncapsulation.None, 36 | changeDetection: ChangeDetectionStrategy.OnPush, 37 | }) 38 | export class DsSegmentedControl implements AfterViewInit, OnDestroy { 39 | private controlOptions = inject(SEGMENTED_CONTROL_OPTIONS_TOKEN); 40 | private renderer = inject(Renderer2); 41 | private ngZone = inject(NgZone); 42 | private focusMonitor = inject(FocusMonitor); 43 | 44 | readonly activeOption = model(''); 45 | readonly fullWidth = input(this.controlOptions.fullWidth, { 46 | transform: booleanAttribute, 47 | }); 48 | readonly roleType = input<'radiogroup' | 'tablist'>('tablist'); 49 | readonly twoLineTruncation = input(false, { transform: booleanAttribute }); 50 | readonly inverse = model<boolean>(false); 51 | 52 | protected readonly scContainer = 53 | viewChild.required<ElementRef<HTMLDivElement>>('scContainer'); 54 | protected readonly segmentedOptions = contentChildren(DsSegmentedOption); 55 | protected readonly tabLabels = 56 | viewChildren<ElementRef<HTMLDivElement>>('tabOption'); 57 | 58 | protected isReady = signal(false); 59 | 60 | readonly selectedOption: WritableSignal<DsSegmentedOption | null> = 61 | signal<DsSegmentedOption | null>(this.segmentedOptions()[0] ?? null); 62 | readonly focusedOption: WritableSignal<DsSegmentedOption | null> = 63 | signal<DsSegmentedOption | null>(null); 64 | readonly focusVisibleOption: WritableSignal<DsSegmentedOption | null> = 65 | signal<DsSegmentedOption | null>(null); 66 | private readonly resizeObserver = new ResizeObserver((entries) => { 67 | for (const entry of entries) { 68 | this._setIndicatorCSSVars(entry.target as HTMLDivElement); 69 | } 70 | }); 71 | 72 | constructor() { 73 | effect(() => { 74 | const isReady = this.isReady(); 75 | const options = this.segmentedOptions(); 76 | const activeOption = this.activeOption(); 77 | 78 | // activate the option that comes from the input 79 | untracked(() => { 80 | if (isReady) { 81 | const selectedOption = this.selectedOption(); 82 | if (selectedOption) { 83 | this._setHighlightWidthAndXPost(selectedOption); 84 | } 85 | 86 | if (options.length === 0) { 87 | throw new Error('Please provide segmented options!'); 88 | } 89 | this.selectOption(activeOption); 90 | } 91 | }); 92 | }); 93 | 94 | effect(() => { 95 | const isReady = this.isReady(); 96 | 97 | untracked(() => { 98 | if (isReady) { 99 | const selectedOption = this.selectedOption(); 100 | if (selectedOption) { 101 | this._setHighlightWidthAndXPost(selectedOption); 102 | } 103 | } 104 | }); 105 | }); 106 | } 107 | 108 | ngAfterViewInit(): void { 109 | this.tabLabels().forEach((option, index) => { 110 | this.focusMonitor 111 | .monitor(option.nativeElement, true) 112 | .subscribe((focusOrigin) => { 113 | const isFocused = 114 | focusOrigin === 'keyboard' || focusOrigin === 'program'; 115 | if (isFocused) { 116 | this.focusedOption.set(this.segmentedOptions()[index] ?? null); 117 | this.focusVisibleOption.set(this.segmentedOptions()[index] ?? null); 118 | } 119 | }); 120 | }); 121 | 122 | // we don't want to show the initial animation, but only the subsequent ones 123 | this.ngZone.runOutsideAngular(() => 124 | setTimeout(() => this.isReady.set(true)), 125 | ); 126 | this.selectOption(this.activeOption()); 127 | } 128 | 129 | /** 130 | * The method which will update the `selected` signal in `ds-segment-option` based on the selected name. 131 | * @param name Name of the selected option 132 | * @param event 133 | */ 134 | selectOption(name: string, event?: Event): void { 135 | if ( 136 | ((event && 137 | this.activeOption() === name && 138 | this.selectedOption()?.name() === name) || 139 | name === undefined) && 140 | this.activeOption() != null 141 | ) { 142 | return; // do nothing if the same option is clicked again 143 | } 144 | 145 | const option = this.segmentedOptions().find((x) => x.name() === name); 146 | if (option) { 147 | this.selectedOption.set(option); 148 | 149 | if (this.isReady()) { 150 | this._setHighlightWidthAndXPost(option); 151 | } 152 | this.activeOption.set(name); 153 | } else { 154 | // if no option can be found, we select the first one by default 155 | this.selectFirstOption(); 156 | } 157 | } 158 | 159 | /** 160 | * Select first segment option. This is useful when the activeOption is not provided. 161 | * @private 162 | */ 163 | private selectFirstOption() { 164 | const options = this.segmentedOptions(); 165 | 166 | if (options.length === 0) { 167 | return; 168 | } 169 | 170 | const firstOption = options[0] ?? null; 171 | 172 | this.selectedOption.set(firstOption); 173 | this.activeOption.set(firstOption?.name() ?? ''); 174 | } 175 | 176 | /** 177 | * Will get the active segment position in order to show the indicator on the background. 178 | * @private 179 | */ 180 | private _setHighlightWidthAndXPost(option: DsSegmentedOption) { 181 | for (const item of this.tabLabels()) { 182 | this.resizeObserver.unobserve(item.nativeElement); 183 | } 184 | 185 | const element = this.tabLabels().find( 186 | (item) => item.nativeElement.id === `ds-segment-item-${option.name()}`, 187 | ); 188 | if (element) { 189 | this._setIndicatorCSSVars(element.nativeElement); 190 | this.resizeObserver.observe(element.nativeElement); 191 | } 192 | } 193 | 194 | /** 195 | * Will set the active element indicator related css variables 196 | * @private 197 | */ 198 | private _setIndicatorCSSVars(element: HTMLDivElement) { 199 | const { offsetWidth, offsetLeft } = element; 200 | // We update the DOM directly, so we don't have to go through Angular Change Detection 201 | this.renderer.setProperty( 202 | this.scContainer().nativeElement, 203 | 'style', 204 | `--ds-sc-highlight-width: ${offsetWidth}px; --ds-sc-highlight-x-pos: ${offsetLeft}px`, 205 | ); 206 | } 207 | 208 | onKeydown(event: KeyboardEvent) { 209 | const { key } = event; 210 | const options = this.segmentedOptions(); 211 | const currentIndex = options.findIndex((option) => option.focused()); 212 | let newIndex: number | undefined; 213 | 214 | if (key === 'ArrowRight') { 215 | newIndex = (currentIndex + 1) % options.length; 216 | } else if (key === 'ArrowLeft') { 217 | newIndex = (currentIndex - 1 + options.length) % options.length; 218 | } else if ( 219 | (key === ' ' || key === 'Enter') && 220 | currentIndex !== -1 && 221 | options[currentIndex] 222 | ) { 223 | this.selectOption(options[currentIndex].name(), event); 224 | } 225 | 226 | if (newIndex !== undefined) { 227 | event.preventDefault(); 228 | const newOption = options[newIndex]; 229 | if (newOption) { 230 | this.focusOption(newOption, newIndex); 231 | } 232 | } 233 | } 234 | 235 | private focusOption(option: DsSegmentedOption, index: number) { 236 | const focusOption = this.tabLabels()[index]; 237 | if (focusOption) { 238 | this.focusedOption.set(option); 239 | this.focusVisibleOption.set(option); 240 | this.focusMonitor.focusVia(focusOption.nativeElement, 'keyboard'); 241 | } 242 | } 243 | 244 | ngOnDestroy() { 245 | if (this.tabLabels()) { 246 | this.tabLabels().forEach((option) => 247 | this.focusMonitor.stopMonitoring(option), 248 | ); 249 | } 250 | 251 | this.resizeObserver.disconnect(); 252 | } 253 | } 254 | ``` -------------------------------------------------------------------------------- /packages/shared/styles-ast-utils/ai/EXAMPLES.md: -------------------------------------------------------------------------------- ```markdown 1 | # Examples 2 | 3 | ## 1 — Basic stylesheet parsing 4 | 5 | > Parse CSS content and access the AST structure. 6 | 7 | ```ts 8 | import { parseStylesheet } from '@push-based/styles-ast-utils'; 9 | 10 | const cssContent = ` 11 | .btn { 12 | color: red; 13 | background: blue; 14 | } 15 | 16 | .card { 17 | padding: 1rem; 18 | } 19 | `; 20 | 21 | const result = parseStylesheet(cssContent, 'styles.css'); 22 | const root = result.root; 23 | 24 | console.log(`Parsed ${root.nodes.length} top-level nodes`); // → 'Parsed 2 top-level nodes' 25 | console.log(root.nodes[0].type); // → 'rule' 26 | console.log(root.nodes[0].selector); // → '.btn' 27 | ``` 28 | 29 | --- 30 | 31 | ## 2 — Using the visitor pattern 32 | 33 | > Traverse CSS AST using the visitor pattern to collect information. 34 | 35 | ```ts 36 | import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; 37 | 38 | const cssContent = ` 39 | /* Main styles */ 40 | .btn { 41 | color: red; 42 | font-size: 14px; 43 | } 44 | 45 | @media (max-width: 768px) { 46 | .btn { 47 | font-size: 12px; 48 | } 49 | } 50 | `; 51 | 52 | const result = parseStylesheet(cssContent, 'styles.css'); 53 | const selectors: string[] = []; 54 | const properties: string[] = []; 55 | const mediaQueries: string[] = []; 56 | 57 | const visitor = { 58 | visitRule: (rule) => { 59 | selectors.push(rule.selector); 60 | }, 61 | visitDecl: (decl) => { 62 | properties.push(`${decl.prop}: ${decl.value}`); 63 | }, 64 | visitAtRule: (atRule) => { 65 | if (atRule.name === 'media') { 66 | mediaQueries.push(atRule.params); 67 | } 68 | }, 69 | visitComment: (comment) => { 70 | console.log(`Found comment: ${comment.text}`); 71 | }, 72 | }; 73 | 74 | visitEachChild(result.root, visitor); 75 | 76 | console.log('Selectors:', selectors); 77 | // → ['Selectors:', ['.btn', '.btn']] 78 | 79 | console.log('Properties:', properties); 80 | // → ['Properties:', ['color: red', 'font-size: 14px', 'font-size: 12px']] 81 | 82 | console.log('Media queries:', mediaQueries); 83 | // → ['Media queries:', ['(max-width: 768px)']] 84 | ``` 85 | 86 | --- 87 | 88 | ## 3 — Converting AST nodes to source locations 89 | 90 | > Convert CSS rules to linkable source locations for error reporting. 91 | 92 | ```ts 93 | import { 94 | parseStylesheet, 95 | styleAstRuleToSource, 96 | } from '@push-based/styles-ast-utils'; 97 | import { Rule } from 'postcss'; 98 | 99 | const cssContent = ` 100 | .header { 101 | background: linear-gradient(to right, #ff0000, #00ff00); 102 | padding: 2rem; 103 | } 104 | 105 | .footer { 106 | margin-top: auto; 107 | } 108 | `; 109 | 110 | const result = parseStylesheet(cssContent, 'components/layout.css'); 111 | const rules = result.root.nodes.filter( 112 | (node) => node.type === 'rule' 113 | ) as Rule[]; 114 | 115 | rules.forEach((rule, index) => { 116 | const source = styleAstRuleToSource(rule); 117 | console.log(`Rule ${index + 1}:`, { 118 | selector: rule.selector, 119 | location: `${source.file}:${source.position.startLine}:${source.position.startColumn}`, 120 | span: `${source.position.startLine}-${source.position.endLine}`, 121 | }); 122 | }); 123 | 124 | // Output: 125 | // Rule 1: { 126 | // selector: '.header', 127 | // location: 'components/layout.css:2:1', 128 | // span: '2-4' 129 | // } 130 | // Rule 2: { 131 | // selector: '.footer', 132 | // location: 'components/layout.css:6:1', 133 | // span: '6-8' 134 | // } 135 | ``` 136 | 137 | --- 138 | 139 | ## 4 — Handling inline styles with line offset 140 | 141 | > Parse inline styles and adjust line numbers for accurate source mapping. 142 | 143 | ```ts 144 | import { 145 | parseStylesheet, 146 | styleAstRuleToSource, 147 | } from '@push-based/styles-ast-utils'; 148 | import { Rule } from 'postcss'; 149 | 150 | // Simulate inline styles starting at line 15 in a component file 151 | const inlineStyles = `.component-btn { color: blue; }`; 152 | const componentFilePath = 'src/app/button.component.ts'; 153 | const styleStartLine = 15; // 0-indexed line where styles begin 154 | 155 | const result = parseStylesheet(inlineStyles, componentFilePath); 156 | const rule = result.root.nodes[0] as Rule; 157 | 158 | // Convert with line offset 159 | const source = styleAstRuleToSource(rule, styleStartLine); 160 | 161 | console.log('Inline style location:', { 162 | file: source.file, 163 | line: source.position.startLine, // → 16 (adjusted for file position) 164 | selector: rule.selector, // → '.component-btn' 165 | }); 166 | ``` 167 | 168 | --- 169 | 170 | ## 5 — Recursive node traversal 171 | 172 | > Use recursive traversal to process nested CSS structures. 173 | 174 | ```ts 175 | import { 176 | parseStylesheet, 177 | visitEachStyleNode, 178 | } from '@push-based/styles-ast-utils'; 179 | 180 | const cssContent = ` 181 | .container { 182 | display: flex; 183 | 184 | .item { 185 | flex: 1; 186 | 187 | &:hover { 188 | opacity: 0.8; 189 | } 190 | } 191 | } 192 | 193 | @supports (display: grid) { 194 | .grid-container { 195 | display: grid; 196 | grid-template-columns: repeat(3, 1fr); 197 | } 198 | } 199 | `; 200 | 201 | const result = parseStylesheet(cssContent, 'nested-styles.scss'); 202 | let depth = 0; 203 | 204 | const visitor = { 205 | visitRule: (rule) => { 206 | console.log(`${' '.repeat(depth)}Rule: ${rule.selector}`); 207 | depth++; 208 | if (rule.nodes) { 209 | visitEachStyleNode(rule.nodes, visitor); 210 | } 211 | depth--; 212 | }, 213 | visitAtRule: (atRule) => { 214 | console.log(`${' '.repeat(depth)}@${atRule.name}: ${atRule.params}`); 215 | depth++; 216 | if (atRule.nodes) { 217 | visitEachStyleNode(atRule.nodes, visitor); 218 | } 219 | depth--; 220 | }, 221 | visitDecl: (decl) => { 222 | console.log(`${' '.repeat(depth)}${decl.prop}: ${decl.value}`); 223 | }, 224 | }; 225 | 226 | visitEachStyleNode(result.root.nodes, visitor); 227 | 228 | // Output shows nested structure: 229 | // Rule: .container 230 | // display: flex 231 | // Rule: .item 232 | // flex: 1 233 | // Rule: &:hover 234 | // opacity: 0.8 235 | // @supports: (display: grid) 236 | // Rule: .grid-container 237 | // display: grid 238 | // grid-template-columns: repeat(3, 1fr) 239 | ``` 240 | 241 | --- 242 | 243 | ## 6 — Safe parsing with malformed CSS 244 | 245 | > Handle malformed CSS gracefully using the safe parser. 246 | 247 | ```ts 248 | import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; 249 | 250 | // Malformed CSS with missing closing braces and invalid syntax 251 | const malformedCss = ` 252 | .btn { 253 | color: red 254 | background: blue; 255 | /* missing closing brace */ 256 | 257 | .card 258 | padding: 1rem; 259 | margin: invalid-value; 260 | } 261 | 262 | /* unclosed comment 263 | .footer { 264 | text-align: center; 265 | `; 266 | 267 | const result = parseStylesheet(malformedCss, 'malformed.css'); 268 | const issues: string[] = []; 269 | 270 | const visitor = { 271 | visitRule: (rule) => { 272 | console.log(`Successfully parsed rule: ${rule.selector}`); 273 | }, 274 | visitDecl: (decl) => { 275 | if (!decl.value || decl.value.includes('invalid')) { 276 | issues.push(`Invalid declaration: ${decl.prop}: ${decl.value}`); 277 | } 278 | }, 279 | }; 280 | 281 | visitEachChild(result.root, visitor); 282 | 283 | console.log(`Parsed ${result.root.nodes.length} nodes despite malformed CSS`); 284 | console.log('Issues found:', issues); 285 | 286 | // The safe parser recovers from errors and continues parsing 287 | // Output: 288 | // Successfully parsed rule: .btn 289 | // Successfully parsed rule: .card 290 | // Successfully parsed rule: .footer 291 | // Parsed 3 nodes despite malformed CSS 292 | // Issues found: ['Invalid declaration: margin: invalid-value'] 293 | ``` 294 | 295 | --- 296 | 297 | ## 7 — Collecting CSS class names 298 | 299 | > Extract all CSS class selectors from a stylesheet. 300 | 301 | ```ts 302 | import { parseStylesheet, visitEachChild } from '@push-based/styles-ast-utils'; 303 | 304 | const cssContent = ` 305 | .btn, .button { 306 | padding: 0.5rem 1rem; 307 | } 308 | 309 | .btn-primary { 310 | background: blue; 311 | } 312 | 313 | .card .header { 314 | font-weight: bold; 315 | } 316 | 317 | #main .sidebar .nav-item { 318 | list-style: none; 319 | } 320 | 321 | [data-theme="dark"] .btn { 322 | color: white; 323 | } 324 | `; 325 | 326 | const result = parseStylesheet(cssContent, 'components.css'); 327 | const classNames = new Set<string>(); 328 | 329 | const visitor = { 330 | visitRule: (rule) => { 331 | // Extract class names from selectors using regex 332 | const matches = rule.selector.match(/\.([a-zA-Z0-9_-]+)/g); 333 | if (matches) { 334 | matches.forEach((match) => { 335 | classNames.add(match.substring(1)); // Remove the dot 336 | }); 337 | } 338 | }, 339 | }; 340 | 341 | visitEachChild(result.root, visitor); 342 | 343 | console.log('Found CSS classes:', Array.from(classNames).sort()); 344 | // → ['Found CSS classes:', ['btn', 'btn-primary', 'button', 'card', 'header', 'nav-item', 'sidebar']] 345 | ``` 346 | 347 | These examples demonstrate the comprehensive CSS parsing and analysis capabilities of the `@push-based/styles-ast-utils` library for various stylesheet processing scenarios. 348 | ```