Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Add styling page to the docs #1283

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/app/pages/component-sidenav/component-sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'}
Expand Down
34 changes: 34 additions & 0 deletions src/app/pages/component-viewer/component-styling.html
Original file line number Diff line number Diff line change
@@ -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 {
<h2 class="cdk-visually-hidden" tabindex="-1">How to style {{item.id}}</h2>
Styles from the <code>{{item.packageName}}/{{item.id}}</code> package can be customized using
@if (data.length === 1) {
the <code>{{data[0].overridesMixin}}</code> mixin.
} @else {
the @for (current of data; track current.name) {{{$last ? ' and ' : ($first ? '' : ', ')}}<code>{{current.overridesMixin}}</code>} 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) {
<div class="docs-markdown">
<pre>{{example}}</pre>
</div>
}

You can find the full list of supported mixins and tokens below.

<div class="docs-markdown">
@for (current of data; track current.name) {
<h3>Tokens supported by <code>{{current.overridesMixin}}</code></h3>
<token-table [tokens]="current.tokens"/>
}
</div>
}
79 changes: 79 additions & 0 deletions src/app/pages/component-viewer/component-styling.ts
Original file line number Diff line number Diff line change
@@ -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<string, Observable<StyleOverridesData[]>> = {};

constructor(private _http: HttpClient) {}

getTokenData(item: DocItem): Observable<StyleOverridesData[]> {
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<StyleOverridesData[]>(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');
}));
}
2 changes: 1 addition & 1 deletion src/app/pages/component-viewer/component-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
83 changes: 37 additions & 46 deletions src/app/pages/component-viewer/component-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,47 @@ export class ComponentViewer implements OnDestroy {
sections: Set<string> = new Set(['overview', 'api']);
private _destroyed = new Subject<void>();

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');
}
});
}
Expand Down Expand Up @@ -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
Expand All @@ -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`;
Expand All @@ -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: [
Expand Down
42 changes: 42 additions & 0 deletions src/app/pages/component-viewer/token-name.ts
Original file line number Diff line number Diff line change
@@ -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: `
<code>{{name()}}</code>
<button
mat-icon-button
matTooltip="Copy name to the clipboard"
(click)="copy(name())">
<mat-icon>content_copy</mat-icon>
</button>
`,
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<string>();

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});
}
}
53 changes: 53 additions & 0 deletions src/app/pages/component-viewer/token-table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<div class="filters">
<mat-form-field class="name-field" subscriptSizing="dynamic" appearance="outline">
<mat-label>Filter by name</mat-label>
<input
#nameInput
matInput
[value]="nameFilter()"
(input)="nameFilter.set(nameInput.value)"/>
</mat-form-field>

<mat-form-field subscriptSizing="dynamic" appearance="outline">
<mat-label>Filter by type</mat-label>
<mat-select (selectionChange)="typeFilter.set($event.value)">
@for (type of types; track $index) {
<mat-option [value]="type">{{type | titlecase}}</mat-option>
}
</mat-select>
</mat-form-field>

<button mat-button (click)="reset()">Reset filters</button>
</div>

<div class="docs-markdown">
<table>
<thead>
<tr>
<th>Name</th>
<th class="type-header">Type</th>
<th class="system-header">Based on system token</th>
</tr>
</thead>

<tbody>
@for (token of filteredTokens(); track token.overridesName) {
<tr>
<td><token-name [name]="token.overridesName"/></td>
<td>{{token.type | titlecase}}</td>
<td>
@if (token.derivedFrom) {
<token-name [name]="token.derivedFrom"/>
} @else {
None
}
</td>
</tr>
} @empty {
<tr>
<td>No tokens match the current set of filters</td>
</tr>
}
</tbody>
</table>
</div>
Loading