diff --git a/package.json b/package.json
index 2c58f03a..11c6f7f4 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"@angular/cdk-experimental": "^19.0.0-next.9",
"@angular/common": "^19.0.0-next.10",
"@angular/compiler": "^19.0.0-next.10",
- "@angular/components-examples": "https://github.com/angular/material2-docs-content.git#4334c0c112b4a55c7257a5837ac94f5464938764",
+ "@angular/components-examples": "https://github.com/angular/material2-docs-content.git#3e3187172e1edc005a6669fa214e7b4bdc6a230b",
"@angular/core": "^19.0.0-next.10",
"@angular/forms": "^19.0.0-next.10",
"@angular/google-maps": "^19.0.0-next.9",
diff --git a/src/app/pages/component-sidenav/component-sidenav.ts b/src/app/pages/component-sidenav/component-sidenav.ts
index 6f66f260..4f483289 100644
--- a/src/app/pages/component-sidenav/component-sidenav.ts
+++ b/src/app/pages/component-sidenav/component-sidenav.ts
@@ -51,6 +51,7 @@ import {
ComponentViewer,
ComponentViewerModule
} from '../component-viewer/component-viewer';
+import {ComponentStyling} from '../component-viewer/component-styling';
// These constants are used by the ComponentSidenav for orchestrating the MatSidenav in a responsive
// way. This includes hiding the sidenav, defaulting it to open, changing the mode from over to
@@ -165,7 +166,8 @@ const routes: Routes = [{
{path: '', redirectTo: 'overview', pathMatch: 'full'},
{path: 'overview', component: ComponentOverview, pathMatch: 'full'},
{path: 'api', component: ComponentApi, pathMatch: 'full'},
- {path: 'examples', component: ComponentExamples, pathMatch: 'full'}
+ {path: 'styling', component: ComponentStyling, pathMatch: 'full'},
+ {path: 'examples', component: ComponentExamples, pathMatch: 'full'},
],
},
{path: '**', redirectTo: '/404'}
diff --git a/src/app/pages/component-viewer/component-styling.html b/src/app/pages/component-viewer/component-styling.html
new file mode 100644
index 00000000..5e08edd9
--- /dev/null
+++ b/src/app/pages/component-viewer/component-styling.html
@@ -0,0 +1,34 @@
+@let item = docItem | async;
+@let data = dataStream | async;
+@let example = exampleStream | async;
+@let hasData = hasDataStream | async;
+
+@if (!item || !data) {
+ Loading...
+} @else if (!hasData) {
+ This component does not support style overrides
+} @else {
+
How to style {{item.id}}
+ Styles from the {{item.packageName}}/{{item.id}}
package can be customized using
+ @if (data.length === 1) {
+ the {{data[0].overridesMixin}}
mixin.
+ } @else {
+ the @for (current of data; track current.name) {{{$last ? ' and ' : ($first ? '' : ', ')}}{{current.overridesMixin}}
} mixins.
+ }
+ {{data.length === 1 ? 'This mixin accepts' : 'These mixins accept'}} a set of tokens that control how the components will look, either for the entire app or under a specific selector. {{example ? 'For example:' : ''}}
+
+ @if (example) {
+
+ }
+
+ You can find the full list of supported mixins and tokens below.
+
+
+ @for (current of data; track current.name) {
+
Tokens supported by {{current.overridesMixin}}
+
+ }
+
+}
diff --git a/src/app/pages/component-viewer/component-styling.ts b/src/app/pages/component-viewer/component-styling.ts
new file mode 100644
index 00000000..9b7fcbd5
--- /dev/null
+++ b/src/app/pages/component-viewer/component-styling.ts
@@ -0,0 +1,79 @@
+import {Component, inject, Injectable} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {AsyncPipe} from '@angular/common';
+import {Observable} from 'rxjs';
+import {map, shareReplay, switchMap} from 'rxjs/operators';
+import {ComponentViewer} from './component-viewer';
+import {DocItem} from '../../shared/documentation-items/documentation-items';
+import {Token, TokenTable} from './token-table';
+
+interface StyleOverridesData {
+ name: string;
+ overridesMixin: string;
+ tokens: Token[];
+}
+
+@Injectable({providedIn: 'root'})
+class TokenService {
+ private _cache: Record> = {};
+
+ constructor(private _http: HttpClient) {}
+
+ getTokenData(item: DocItem): Observable {
+ const url = `/docs-content/tokens/${item.packageName}/${item.id}/${item.id}.json`;
+
+ if (this._cache[url]) {
+ return this._cache[url];
+ }
+
+ const stream = this._http.get(url).pipe(shareReplay(1));
+ this._cache[url] = stream;
+ return stream;
+ }
+}
+
+@Component({
+ selector: 'component-styling',
+ templateUrl: './component-styling.html',
+ standalone: true,
+ imports: [AsyncPipe, TokenTable],
+})
+export class ComponentStyling {
+ private componentViewer = inject(ComponentViewer);
+ private tokenService = inject(TokenService);
+ protected docItem = this.componentViewer.componentDocItem;
+ protected dataStream =
+ this.docItem.pipe(switchMap(item => this.tokenService.getTokenData(item)));
+ protected hasDataStream = this.dataStream.pipe(
+ map(data => data.length > 0 && data.some(d => d.tokens.length > 0)));
+
+ protected exampleStream = this.dataStream.pipe(map(data => {
+ const mixin = data.find(d => d.tokens.length > 0);
+
+ if (!mixin) {
+ return null;
+ }
+
+ // Pick out a couple of color tokens to show as examples.
+ const firstToken = mixin.tokens.find(token => token.type === 'color');
+ const secondToken = mixin.tokens.find(token => token.type === 'color' && token !== firstToken);
+
+ if (!firstToken) {
+ return null;
+ }
+
+ const lines = [
+ `@use '@angular/material' as mat;`,
+ ``,
+ `// Customize the entire app. Change :root to your selector if you want to scope the styles.`,
+ `:root {`,
+ ` @include mat.${mixin.overridesMixin}((`,
+ ` ${firstToken.overridesName}: orange,`,
+ ...(secondToken ? [` ${secondToken.overridesName}: red,`] : []),
+ ` ));`,
+ `}`,
+ ];
+
+ return lines.join('\n');
+ }));
+}
diff --git a/src/app/pages/component-viewer/component-viewer.spec.ts b/src/app/pages/component-viewer/component-viewer.spec.ts
index 59aaf606..6fa2097f 100644
--- a/src/app/pages/component-viewer/component-viewer.spec.ts
+++ b/src/app/pages/component-viewer/component-viewer.spec.ts
@@ -44,7 +44,7 @@ describe('ComponentViewer', () => {
throw Error(`Unable to find DocItem: '${docItemsId}' in section: 'material'.`);
}
const expected = `${docItem.name}`;
- expect(component._componentPageTitle.title).toEqual(expected);
+ expect(component.componentPageTitle.title).toEqual(expected);
});
});
diff --git a/src/app/pages/component-viewer/component-viewer.ts b/src/app/pages/component-viewer/component-viewer.ts
index 9c3ea879..b19d6f61 100644
--- a/src/app/pages/component-viewer/component-viewer.ts
+++ b/src/app/pages/component-viewer/component-viewer.ts
@@ -51,32 +51,47 @@ export class ComponentViewer implements OnDestroy {
sections: Set = new Set(['overview', 'api']);
private _destroyed = new Subject();
- constructor(_route: ActivatedRoute, private router: Router,
- public _componentPageTitle: ComponentPageTitle,
- public docItems: DocumentationItems) {
- const routeAndParentParams = [_route.params];
- if (_route.parent) {
- routeAndParentParams.push(_route.parent.params);
+ constructor(
+ route: ActivatedRoute,
+ private router: Router,
+ public componentPageTitle: ComponentPageTitle,
+ readonly docItems: DocumentationItems) {
+ const routeAndParentParams = [route.params];
+ if (route.parent) {
+ routeAndParentParams.push(route.parent.params);
}
// Listen to changes on the current route for the doc id (e.g. button/checkbox) and the
// parent route for the section (material/cdk).
combineLatest(routeAndParentParams).pipe(
- map((params: Params[]) => ({id: params[0]['id'], section: params[1]['section']})),
- map((docIdAndSection: {id: string, section: string}) =>
- ({doc: docItems.getItemById(docIdAndSection.id, docIdAndSection.section),
- section: docIdAndSection.section}), takeUntil(this._destroyed))
- ).subscribe((docItemAndSection: {doc: DocItem | undefined, section: string}) => {
- if (docItemAndSection.doc !== undefined) {
- this.componentDocItem.next(docItemAndSection.doc);
- this._componentPageTitle.title = `${docItemAndSection.doc.name}`;
-
- if (docItemAndSection.doc.examples && docItemAndSection.doc.examples.length) {
- this.sections.add('examples');
- } else {
- this.sections.delete('examples');
- }
+ map((params: Params[]) => {
+ const id = params[0]['id'];
+ const section = params[1]['section'];
+
+ return ({
+ doc: docItems.getItemById(id, section),
+ section: section
+ });
+ },
+ takeUntil(this._destroyed))
+ ).subscribe(({doc, section}) => {
+ if (!doc) {
+ this.router.navigate(['/' + section]);
+ return;
+ }
+
+ this.componentDocItem.next(doc);
+ componentPageTitle.title = `${doc.name}`;
+
+ if (doc.hasStyling) {
+ this.sections.add('styling');
} else {
- this.router.navigate(['/' + docItemAndSection.section]);
+ this.sections.delete('styling');
+ }
+
+ if (doc.examples && doc.examples.length) {
+ this.sections.add('examples');
+ } else {
+ this.sections.delete('examples');
}
});
}
@@ -159,14 +174,6 @@ export class ComponentBaseView implements OnInit, OnDestroy {
],
})
export class ComponentOverview extends ComponentBaseView {
- constructor(
- componentViewer: ComponentViewer,
- breakpointObserver: BreakpointObserver,
- changeDetectorRef: ChangeDetectorRef
- ) {
- super(componentViewer, breakpointObserver, changeDetectorRef);
- }
-
getOverviewDocumentUrl(doc: DocItem) {
// Use the explicit overview path if specified. Otherwise, compute an overview path based
// on the package name and doc item id. Overviews for components are commonly stored in a
@@ -191,14 +198,6 @@ export class ComponentOverview extends ComponentBaseView {
],
})
export class ComponentApi extends ComponentBaseView {
- constructor(
- componentViewer: ComponentViewer,
- breakpointObserver: BreakpointObserver,
- changeDetectorRef: ChangeDetectorRef
- ) {
- super(componentViewer, breakpointObserver, changeDetectorRef);
- }
-
getApiDocumentUrl(doc: DocItem) {
const apiDocId = doc.apiDocId || `${doc.packageName}-${doc.id}`;
return `/docs-content/api-docs/${apiDocId}.html`;
@@ -215,15 +214,7 @@ export class ComponentApi extends ComponentBaseView {
AsyncPipe,
],
})
-export class ComponentExamples extends ComponentBaseView {
- constructor(
- componentViewer: ComponentViewer,
- breakpointObserver: BreakpointObserver,
- changeDetectorRef: ChangeDetectorRef
- ) {
- super(componentViewer, breakpointObserver, changeDetectorRef);
- }
-}
+export class ComponentExamples extends ComponentBaseView {}
@NgModule({
imports: [
diff --git a/src/app/pages/component-viewer/token-name.ts b/src/app/pages/component-viewer/token-name.ts
new file mode 100644
index 00000000..0661a7d8
--- /dev/null
+++ b/src/app/pages/component-viewer/token-name.ts
@@ -0,0 +1,42 @@
+import {Component, input, inject} from '@angular/core';
+import {MatIconButton} from '@angular/material/button';
+import {Clipboard} from '@angular/cdk/clipboard';
+import {MatIcon} from '@angular/material/icon';
+import {MatSnackBar} from '@angular/material/snack-bar';
+import {MatTooltip} from '@angular/material/tooltip';
+
+@Component({
+ selector: 'token-name',
+ standalone: true,
+ template: `
+ {{name()}}
+
+ content_copy
+
+ `,
+ styles: `
+ :host {
+ display: flex;
+ align-items: center;
+
+ button {
+ margin-left: 8px;
+ }
+ }
+ `,
+ imports: [MatIconButton, MatIcon, MatTooltip],
+})
+export class TokenName {
+ private clipboard = inject(Clipboard);
+ private snackbar = inject(MatSnackBar);
+
+ name = input.required();
+
+ protected copy(name: string): void {
+ const message = this.clipboard.copy(name) ? 'Copied token name' : 'Failed to copy token name';
+ this.snackbar.open(message, undefined, {duration: 2500});
+ }
+}
diff --git a/src/app/pages/component-viewer/token-table.html b/src/app/pages/component-viewer/token-table.html
new file mode 100644
index 00000000..3001fa4e
--- /dev/null
+++ b/src/app/pages/component-viewer/token-table.html
@@ -0,0 +1,53 @@
+
+
+ Filter by name
+
+
+
+
+ Filter by type
+
+ @for (type of types; track $index) {
+ {{type | titlecase}}
+ }
+
+
+
+ Reset filters
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+ @for (token of filteredTokens(); track token.overridesName) {
+
+
+ {{token.type | titlecase}}
+
+ @if (token.derivedFrom) {
+
+ } @else {
+ None
+ }
+
+
+ } @empty {
+
+ No tokens match the current set of filters
+
+ }
+
+
+
diff --git a/src/app/pages/component-viewer/token-table.scss b/src/app/pages/component-viewer/token-table.scss
new file mode 100644
index 00000000..0defe3e7
--- /dev/null
+++ b/src/app/pages/component-viewer/token-table.scss
@@ -0,0 +1,39 @@
+:host {
+ display: block;
+}
+
+table {
+ table-layout: fixed;
+}
+
+thead {
+ position: sticky;
+ top: 0;
+ left: 0;
+ background: #fdfbff;
+ z-index: 1;
+}
+
+.filters {
+ display: flex;
+ align-items: center;
+ margin: 24px 0;
+ width: 100%;
+}
+
+.filters mat-form-field {
+ margin-right: 16px;
+}
+
+.name-field {
+ width: 380px;
+ max-width: 100%;
+}
+
+.type-header {
+ width: 10%;
+}
+
+.system-header {
+ width: 30%;
+}
diff --git a/src/app/pages/component-viewer/token-table.ts b/src/app/pages/component-viewer/token-table.ts
new file mode 100644
index 00000000..1e3b4d90
--- /dev/null
+++ b/src/app/pages/component-viewer/token-table.ts
@@ -0,0 +1,53 @@
+import {ChangeDetectionStrategy, Component, computed, input, signal} from '@angular/core';
+import {TitleCasePipe} from '@angular/common';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {TokenName} from './token-name';
+
+type TokenType = 'base' | 'color' | 'typography' | 'density';
+
+export interface Token {
+ name: string;
+ overridesName: string;
+ prefix: string;
+ type: TokenType;
+ derivedFrom?: string;
+}
+
+@Component({
+ selector: 'token-table',
+ templateUrl: './token-table.html',
+ styleUrl: './token-table.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ TokenName,
+ TitleCasePipe,
+ ]
+})
+export class TokenTable {
+ tokens = input.required();
+
+ protected nameFilter = signal('');
+ protected typeFilter = signal(null);
+ protected types: TokenType[] = ['base', 'color', 'typography', 'density'];
+ protected filteredTokens = computed(() => {
+ const name = this.nameFilter().trim();
+ const typeFilter = this.typeFilter();
+
+ return this.tokens().filter(token =>
+ (!name || token.overridesName.includes(name)) &&
+ (!typeFilter || token.type === typeFilter));
+ });
+
+ protected reset() {
+ this.nameFilter.set('');
+ this.typeFilter.set(null);
+ }
+}
diff --git a/src/app/shared/documentation-items/documentation-items.ts b/src/app/shared/documentation-items/documentation-items.ts
index 817a3eb0..399408c0 100644
--- a/src/app/shared/documentation-items/documentation-items.ts
+++ b/src/app/shared/documentation-items/documentation-items.ts
@@ -30,6 +30,8 @@ export interface DocItem {
overviewPath?: string;
/** List of additional API docs. */
additionalApiDocs?: AdditionalApiDoc[];
+ /** Whether the doc item can display styling information. */
+ hasStyling?: boolean;
}
export interface DocSection {
@@ -596,6 +598,7 @@ export class DocumentationItems {
function processDocs(packageName: string, docs: DocItem[]): DocItem[] {
for (const doc of docs) {
doc.packageName = packageName;
+ doc.hasStyling = packageName === 'material';
doc.examples = exampleNames.filter(key =>
key.match(RegExp(`^${doc.exampleSpecs.prefix}`)) &&
!doc.exampleSpecs.exclude?.some(excludeName => key.indexOf(excludeName) === 0));
diff --git a/yarn.lock b/yarn.lock
index 82f3bcf6..b6cbe8d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -482,22 +482,22 @@ __metadata:
languageName: node
linkType: hard
-"@angular/components-examples@https://github.com/angular/material2-docs-content.git#4334c0c112b4a55c7257a5837ac94f5464938764":
- version: 19.0.0-next.9+sha-57d9a2f
- resolution: "@angular/components-examples@https://github.com/angular/material2-docs-content.git#commit=4334c0c112b4a55c7257a5837ac94f5464938764"
+"@angular/components-examples@https://github.com/angular/material2-docs-content.git#3e3187172e1edc005a6669fa214e7b4bdc6a230b":
+ version: 19.1.0-next.0+sha-6b3a371
+ resolution: "@angular/components-examples@https://github.com/angular/material2-docs-content.git#commit=3e3187172e1edc005a6669fa214e7b4bdc6a230b"
dependencies:
tslib: "npm:^2.3.0"
peerDependencies:
- "@angular/cdk": 19.0.0-next.9+sha-57d9a2f
- "@angular/cdk-experimental": 19.0.0-next.9+sha-57d9a2f
+ "@angular/cdk": 19.1.0-next.0+sha-6b3a371
+ "@angular/cdk-experimental": 19.1.0-next.0+sha-6b3a371
"@angular/common": ^19.0.0-0 || ^19.1.0-0 || ^19.2.0-0 || ^19.3.0-0 || ^20.0.0-0
"@angular/core": ^19.0.0-0 || ^19.1.0-0 || ^19.2.0-0 || ^19.3.0-0 || ^20.0.0-0
- "@angular/material": 19.0.0-next.9+sha-57d9a2f
- "@angular/material-date-fns-adapter": 19.0.0-next.9+sha-57d9a2f
- "@angular/material-experimental": 19.0.0-next.9+sha-57d9a2f
- "@angular/material-luxon-adapter": 19.0.0-next.9+sha-57d9a2f
- "@angular/material-moment-adapter": 19.0.0-next.9+sha-57d9a2f
- checksum: 10c0/557b12db31d56a8187b2cda9685732bb6789f7c9fe4c9cdccf692635c316fd7c14c56d25596c96214ae94959ea5895e87db118ec51fc98832936cac0748319e7
+ "@angular/material": 19.1.0-next.0+sha-6b3a371
+ "@angular/material-date-fns-adapter": 19.1.0-next.0+sha-6b3a371
+ "@angular/material-experimental": 19.1.0-next.0+sha-6b3a371
+ "@angular/material-luxon-adapter": 19.1.0-next.0+sha-6b3a371
+ "@angular/material-moment-adapter": 19.1.0-next.0+sha-6b3a371
+ checksum: 10c0/e2f34c111ff87d8be5ab58c3a08f70faaaac713523b9a69acfd76f1831a735ff1a590c399da024c1b7312b420c6d698fd17a971f46a57e95c383a756a683a8af
languageName: node
linkType: hard
@@ -12725,7 +12725,7 @@ __metadata:
"@angular/common": "npm:^19.0.0-next.10"
"@angular/compiler": "npm:^19.0.0-next.10"
"@angular/compiler-cli": "npm:^19.0.0-next.10"
- "@angular/components-examples": "https://github.com/angular/material2-docs-content.git#4334c0c112b4a55c7257a5837ac94f5464938764"
+ "@angular/components-examples": "https://github.com/angular/material2-docs-content.git#3e3187172e1edc005a6669fa214e7b4bdc6a230b"
"@angular/core": "npm:^19.0.0-next.10"
"@angular/forms": "npm:^19.0.0-next.10"
"@angular/google-maps": "npm:^19.0.0-next.9"