From bdda34caec7c521159794b730ec3e8d645914512 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Thu, 7 Nov 2024 09:46:07 -0700 Subject: [PATCH] add system variables guide (#1287) * add system variables guide * use static style bindings --------- Co-authored-by: Andrew Seguin --- .../pages/component-viewer/component-api.html | 4 +- .../component-viewer/component-overview.html | 2 +- src/app/pages/guide-viewer/guide-viewer.html | 2 +- src/app/pages/system-variables/index.ts | 1 + .../system-variables/system-variables.html | 242 ++++++++++++++++++ .../system-variables/system-variables.scss | 196 ++++++++++++++ .../system-variables/system-variables.ts | 158 ++++++++++++ src/app/shared/doc-viewer/doc-viewer.spec.ts | 27 +- src/app/shared/doc-viewer/doc-viewer.ts | 35 ++- .../shared/example-viewer/code-snippet.html | 2 +- src/app/shared/guide-items/guide-items.ts | 12 +- .../table-of-contents/table-of-contents.ts | 2 +- src/styles.scss | 3 + 13 files changed, 671 insertions(+), 15 deletions(-) create mode 100644 src/app/pages/system-variables/index.ts create mode 100644 src/app/pages/system-variables/system-variables.html create mode 100644 src/app/pages/system-variables/system-variables.scss create mode 100644 src/app/pages/system-variables/system-variables.ts diff --git a/src/app/pages/component-viewer/component-api.html b/src/app/pages/component-viewer/component-api.html index dc6c1ba4..32263d1e 100644 --- a/src/app/pages/component-viewer/component-api.html +++ b/src/app/pages/component-viewer/component-api.html @@ -9,14 +9,14 @@ same container so that they display one after another. -->
- @for (additionalApiDoc of docItem.additionalApiDocs; track additionalApiDoc) { } diff --git a/src/app/pages/component-viewer/component-overview.html b/src/app/pages/component-viewer/component-overview.html index a962a3a6..aa152f39 100644 --- a/src/app/pages/component-viewer/component-overview.html +++ b/src/app/pages/component-viewer/component-overview.html @@ -2,7 +2,7 @@

Overview for {{docItem.id}}

- diff --git a/src/app/pages/guide-viewer/guide-viewer.html b/src/app/pages/guide-viewer/guide-viewer.html index 2e504ead..cb827c69 100644 --- a/src/app/pages/guide-viewer/guide-viewer.html +++ b/src/app/pages/guide-viewer/guide-viewer.html @@ -2,7 +2,7 @@
diff --git a/src/app/pages/system-variables/index.ts b/src/app/pages/system-variables/index.ts new file mode 100644 index 00000000..3a226db5 --- /dev/null +++ b/src/app/pages/system-variables/index.ts @@ -0,0 +1 @@ +export * from './system-variables'; diff --git a/src/app/pages/system-variables/system-variables.html b/src/app/pages/system-variables/system-variables.html new file mode 100644 index 00000000..a3174523 --- /dev/null +++ b/src/app/pages/system-variables/system-variables.html @@ -0,0 +1,242 @@ +

+ Angular Material components depend on system variables defined as CSS variables through the + material.theme + Sass mixin. This page provides guidance and documentation for using these variables to + customize components. +

+ +

Colors

+ +

+ Material Design uses color to create accessible, personal color schemes + that communicate your product's hierarchy, state, and brand. See Material + Design's Color System + page to learn more about its use and purpose. +

+

+ The following colors are the most often used in Angular Material components. Use these + colors and follow their uses to add theme colors to your application's custom components. +

+ +
+
+
+
Primary
+
--mat-sys-primary
+
+
+

+ The most common color used by Angular Material components to + participate in the application theme. +

+

+ Examples include the background color + of filled buttons, the icon color of selected radio buttons, and the + outline color of form fields. +

+

+ Use the color --mat-sys-on-primary for + icons, text, and other visual elements placed on a primary background. This + color is calculated to be optimal for accessibility and legibility. +

+
+
+ +
+
+
Surface
+
--mat-sys-surface
+
+
+

+ A low-emphasis background color that provides a clear contrast for + both light and dark themes and their varied theme colors. +

+

+ Examples include the background color of the application and most + components such as the dialog, card, table, and more. +

+

+ Use the color --mat-sys-on-surface for + icons, text, and other visual elements placed on a surface background. This + color is calculated to be optimal for accessibility and legibility. +

+
+
+ +
+
+
Error
+
--mat-sys-error
+
+
+

+ High-contrast color meant to alert the user to attract immediate attention. +

+

+ Examples include the background color of the badge and the text color of invalid + form fields inputs. +

+

+ Use the color --mat-sys-on-error for + icons, text, and other visual elements placed on an error background. This + color is calculated to be optimal for accessibility and legibility. +

+
+
+ +
+
+
Outline
+
--mat-sys-outline
+
+
+

+ Used for borders and dividers to help provide visual separation between + and around elements. +

+

+ Examples include the color of the divider and border color of an outlined + form field. +

+

+ Use the color --mat-sys-outline-variant for a less + prominent outline. +

+
+
+
+ + + Other available colors + +

+ These colors are less commonly used in Angular Material components but + are available for adding color variety and creating additional emphasis + to components. +

+

+ Colors may be paired with a --mat-sys-on- variable + that should be used for text and icons placed within a filled container. +

+ +

Alternative Theme Colors

+ + + +

Surface Colors

+ +

+ The following colors should be used for backgrounds and large, + low-emphasis areas of the screen. +

+ +

+ Containers filled with a surface color should apply the + --mat-sys-on-surface color to text + and icons placed within. +

+ + + +

Fixed Colors

+ +

+ These colors are the same for both light and dark themes. They are unused + by any Angular Material components. +

+ + + +
+ +

Typography

+ +

+ There are five categories of font types defined by Material Design: body, display, headline, + label, and title. Each category has three sizes: small, medium, and large. +

+

+ Learn more about how these categories and their sizes should be used in your application by + visiting Material Design's + Typography documentation. +

+ + +@for (category of ['body', 'display', 'headline', 'label', 'title']; track $index) { +
+
{{category}}
+ @for (size of ['small', 'medium', 'large']; track $index) { +
+
+
--mat-sys-{{category}}-{{size}}
+
+
Lorem ipsum dolor
+
+ } +
+} + +

+ Each system variable can be applied to the "font" CSS style. Additionally, the parts of the variable definition + can be accessed individually by appending the keywords "font", "line-height", "size", "tracking", and "weight". +

+

+ For example, the values for medium body text may be defined as follows: +

+
+--mat-sys-body-medium: 400 0.875rem / 1.25rem Roboto, sans-serif;
+--mat-sys-body-medium-font: Roboto, sans-serif;
+--mat-sys-body-medium-line-height: 1.25rem;
+--mat-sys-body-medium-size: 0.875rem;
+--mat-sys-body-medium-tracking: 0.016rem;
+--mat-sys-body-medium-weight: 400;
+
+ +

Elevation

+ +

+ Material Design provides six levels of elevation that can be used to provide + a sense of depth and organization to an application's UI. Learn more at Material Design's + Elevation guide. +

+ +

+ These levels are defined as CSS box-shadow values that can be styled to an element. +

+ +@for (level of [0, 1, 2, 3, 4, 5]; track $index) { +
+ box-shadow: var(--mat-sys-level{{level}}) +
+} + +

Overrides

+ +

+ The mat.theme-overrides mixin + can be included to emit different definitions for the system variables and + override the definitions emitted from mat.theme. +

+ +
+ This example container has several system variables overridden by including the + following Sass code: + +
+  @include mat.theme-overrides((
+    primary: #ebdcff,
+    on-primary: #230f46,
+    body-medium: 500 1.15rem/1.3rem Arial,
+    corner-large: 32px,
+    level3: 0 4px 6px 1px var(--mat-sys-surface-dim),
+  ));
+
diff --git a/src/app/pages/system-variables/system-variables.scss b/src/app/pages/system-variables/system-variables.scss new file mode 100644 index 00000000..77a04674 --- /dev/null +++ b/src/app/pages/system-variables/system-variables.scss @@ -0,0 +1,196 @@ +@use '@angular/material' as mat; + +:host { + display: block; + max-width: 1000px; +} + +h1 { + font: var(--mat-sys-title-large); + font-size: 28px; + padding-top: 32px; +} + +h2 { + font: var(--mat-sys-title-large); +} + +a { + color: var(--mat-sys-primary); +} + +.demo-warn { + background: var(--mat-sys-error-container); + color: var(--mat-sys-on-error-container); + border: 1px solid var(--mat-sys-outline-variant); + border-radius: var(--mat-sys-corner-extra-small); + padding: 8px; +} + +.demo-group { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 24px; + margin-top: 24px; +} + +@media (max-width: 1000px) { + .demo-group { grid-template-columns: auto;} +} + +.demo-color-container { + border-radius: var(--mat-sys-corner-small); + display: inline-block; + font: var(--mat-sys-body-medium); + vertical-align: top; +} + +.demo-heading { + color: var(--mat-sys-on-primary); + background: var(--mat-sys-primary); + border: 1px solid var(--mat-sys-outline); + border-top-right-radius: var(--mat-sys-corner-small); + border-top-left-radius: var(--mat-sys-corner-small); + border-bottom: none; + padding: 16px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.demo-name { + font: var(--mat-sys-title-medium); +} + +.demo-variable { + font: var(--mat-sys-title-small); + font-family: monospace; + text-align: right; +} + +.demo-description { + border: 1px solid var(--mat-sys-outline); + border-bottom-right-radius: var(--mat-sys-corner-small); + border-bottom-left-radius: var(--mat-sys-corner-small); + padding: 0 16px; +} + +.demo-code { + font-family: monospace; +} + +.demo-surface-variable { + display: inline-block; + font-family: monospace; + background: var(--mat-sys-primary-container); + color: var(--mat-sys-on-primary-container); + padding: 2px 6px; + margin: 0 2px; + border-radius: 4px; +} + +mat-expansion-panel { + margin-top: 24px; + overflow: visible; + @include mat.expansion-overrides(( + 'container-text-font': var(--mat-sys-body-medium-font), + 'container-text-size': var(--mat-sys-body-medium-size), + 'container-text-weight': var(--mat-sys-body-medium-weight), + 'container-text-line-height': var(--mat-sys-body-medium-line-height), + 'container-text-tracking': var(--mat-sys-body-medium-tracking), + )); +} + +.demo-compact-color-container { + border-radius: var(--mat-sys-corner-small); + border: 1px solid var(--mat-sys-outline); + overflow: hidden; // Hide child heading background color + margin-top: 24px; + + .demo-heading { + border: none; + border-radius: 0; + + &:not(:nth-child(1)) { + border-top: 1px solid var(--mat-sys-outline); + } + } + + .demo-variables { + text-align: end; + } +} + +.demo-typography-group { + border: 1px solid var(--mat-sys-outline); + border-radius: var(--mat-sys-corner-small); + margin-top: 40px; + overflow: hidden; +} + +.demo-typography-title { + text-transform: capitalize; + font: var(--mat-sys-title-medium); + padding: 16px; + border-bottom: 1px solid var(--mat-sys-outline); + background: var(--mat-sys-primary-container); + color: var(--mat-sys-on-primary-container); +} + +.demo-typography-variable { + min-width: 240px; +} + +.demo-typography-example { + padding: 16px; + display: flex; + align-items: baseline; + border-top: 1px solid var(--mat-sys-outline-variant); + + &:nth-child(1) { + border: none; + } + .demo-surface-variable { + margin-right: 16px; + } +} + +.demo-typography-text { + display: inline-block; +} + +.demo-elevation { + height: 40px; + width: 300px; + margin: 32px; + display: flex; + align-items: center; + justify-content: center; + background: var(--mat-sys-surface-container); + color: var(--mat-sys-on-surface); + border-radius: var(--mat-sys-corner-extra-small); +} + +.demo-code-block { + background: var(--mat-sys-surface-container-low); + padding: 16px; + border-radius: var(--mat-sys-corner-small); + border: 1px solid var(--mat-sys-outline); +} + +.demo-overrides { + background-color: var(--mat-sys-primary); + color: var(--mat-sys-on-primary); + font: var(--mat-sys-body-medium); + border-radius: var(--mat-sys-corner-large); + box-shadow: var(--mat-sys-level3); + padding: 16px; + + @include mat.theme-overrides(( + primary: #ebdcff, + on-primary: #230f46, + body-medium: 500 1.15rem/1.3rem Arial, + corner-large: 32px, + level3: 0 4px 6px 1px var(--mat-sys-surface-dim), + )); +} diff --git a/src/app/pages/system-variables/system-variables.ts b/src/app/pages/system-variables/system-variables.ts new file mode 100644 index 00000000..5f2f66fa --- /dev/null +++ b/src/app/pages/system-variables/system-variables.ts @@ -0,0 +1,158 @@ +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; +import {MatCardModule} from '@angular/material/card'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatIconModule} from '@angular/material/icon'; + +interface Color { + name: string; + background: string; + text: string; + hideText?: boolean; +} + +@Component({ + selector: 'theme-demo-colors', + template: ` +
+ @for (color of colors(); track $index) { +
+
{{color.name}}
+
+
{{color.background}}
+ @if (!color.hideText) { +
{{color.text}}
+ } +
+
+ } +
+ `, + styleUrl: 'system-variables.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class ThemeDemoColors { + colors = input(); +} + +@Component({ + selector: 'app-system-variables', + templateUrl: './system-variables.html', + styleUrls: ['./system-variables.scss'], + imports: [MatCardModule, MatExpansionModule, MatIconModule, ThemeDemoColors], + standalone: true, +}) +export class SystemVariables { + alternativeThemeColors: Color[] = [ + { + name: 'Primary Container', + background: '--mat-sys-primary-container', + text: '--mat-sys-on-primary-container', + }, + { + name: 'Secondary', + background: '--mat-sys-secondary', + text: '--mat-sys-on-secondary', + }, + { + name: 'Secondary Container', + background: '--mat-sys-secondary-container', + text: '--mat-sys-on-secondary-container', + }, + { + name: 'Tertiary', + background: '--mat-sys-tertiary', + text: '--mat-sys-on-tertiary', + }, + { + name: 'Tertiary Container', + background: '--mat-sys-tertiary-container', + text: '--mat-sys-on-tertiary-container', + }, + { + name: 'Error Container', + background: '--mat-sys-error-container', + text: '--mat-sys-on-error-container', + }, + ]; + + surfaceColors: Color[] = [ + { + name: 'Surface Dim', + background: '--mat-sys-surface-dim', + text: '--mat-sys-on-surface', + hideText: true, + }, + { + name: 'Surface Bright', + background: '--mat-sys-surface-bright', + text: '--mat-sys-on-surface', + hideText: true, + }, + { + name: 'Surface Container Lowest', + background: '--mat-sys-surface-container-lowest', + text: '--mat-sys-on-surface', + hideText: true, + }, + { + name: 'Surface Container Low', + background: '--mat-sys-surface-container-low', + text: '--mat-sys-on-surface', + hideText: true, + }, + { + name: 'Surface Container', + background: '--mat-sys-surface-container', + text: '--mat-sys-on-surface', + hideText: true, + }, + { + name: 'Surface Container High', + background: '--mat-sys-surface-container-high', + text: '--mat-sys-on-surface', + hideText: true, + }, + { + name: 'Surface Container Highest', + background: '--mat-sys-surface-container-highest', + text: '--mat-sys-on-surface', + hideText: true, + }, + ]; + + fixedColors: Color[] = [ + { + name: 'Primary Fixed', + background: '--mat-sys-primary-fixed', + text: '--mat-sys-on-primary-fixed', + }, + { + name: 'Primary Fixed Dim', + background: '--mat-sys-primary-fixed-dim', + text: '--mat-sys-on-primary-fixed', + }, + { + name: 'Secondary Fixed', + background: '--mat-sys-secondary-fixed', + text: '--mat-sys-on-secondary-fixed', + }, + { + name: 'Secondary Fixed Dim', + background: '--mat-sys-secondary-fixed-dim', + text: '--mat-sys-on-secondary-fixed', + }, + { + name: 'Tertiary Fixed', + background: '--mat-sys-tertiary-fixed', + text: '--mat-sys-on-tertiary-fixed', + }, + { + name: 'Tertiary Fixed Dim', + background: '--mat-sys-tertiary-fixed-dim', + text: '--mat-sys-on-tertiary-fixed', + }, + ]; +} diff --git a/src/app/shared/doc-viewer/doc-viewer.spec.ts b/src/app/shared/doc-viewer/doc-viewer.spec.ts index 7ddf865e..c8b0c290 100644 --- a/src/app/shared/doc-viewer/doc-viewer.spec.ts +++ b/src/app/shared/doc-viewer/doc-viewer.spec.ts @@ -33,6 +33,15 @@ describe('DocViewer', () => { expect(docViewer.nativeElement.innerHTML).toBe('
my docs page
'); }); + it('should load component', () => { + const fixture = TestBed.createComponent(DocViewerWithCompTestComponent); + fixture.detectChanges(); + + const docViewer = fixture.debugElement.query(By.directive(DocViewer)); + expect(docViewer).not.toBeNull(); + expect(docViewer.nativeElement.innerHTML).toContain(`TEST_COMPONENT_GUIDE`); + }); + it('should save textContent of the doc', () => { const fixture = TestBed.createComponent(DocViewerTestComponent); fixture.detectChanges(); @@ -145,7 +154,7 @@ describe('DocViewer', () => { @Component({ selector: 'test', - template: ``, + template: ``, standalone: true, imports: [DocViewerModule, DocsAppTestingModule], }) @@ -170,3 +179,19 @@ const FAKE_DOCS: {[key: string]: string} = { '
', /* eslint-enable @typescript-eslint/naming-convention */ }; + +@Component({ + template: `TEST_COMPONENT_GUIDE`, + standalone: true, +}) +class TestComponent {} + +@Component({ + selector: 'test', + template: ``, + standalone: true, + imports: [DocViewerModule, DocsAppTestingModule, TestComponent], +}) +class DocViewerWithCompTestComponent { + component = TestComponent; +} diff --git a/src/app/shared/doc-viewer/doc-viewer.ts b/src/app/shared/doc-viewer/doc-viewer.ts index 8690fad8..a4e23d64 100644 --- a/src/app/shared/doc-viewer/doc-viewer.ts +++ b/src/app/shared/doc-viewer/doc-viewer.ts @@ -1,4 +1,10 @@ -import {ComponentPortal, DomPortalOutlet} from '@angular/cdk/portal'; +import { + ComponentType, + ComponentPortal, + DomPortalOutlet, + Portal, + PortalModule +} from '@angular/cdk/portal'; import {HttpClient, HttpErrorResponse} from '@angular/common/http'; import {DomSanitizer} from '@angular/platform-browser'; import { @@ -40,20 +46,37 @@ class DocFetcher { @Component({ selector: 'doc-viewer', - template: 'Loading document...', + template: ` + @if (portal) { + + } @else { + Loading document... + } + `, standalone: true, + imports: [PortalModule] }) export class DocViewer implements OnDestroy { private _portalHosts: DomPortalOutlet[] = []; private _documentFetchSubscription: Subscription | undefined; + protected portal: Portal | undefined; readonly name = input(); - /** The URL of the document to display. */ + /** The document to display, either as a URL to a markdown file or a component to create. */ @Input() - set documentUrl(url: string | undefined) { - if (url !== undefined) { - this._fetchDocument(url); + set document(document: string | ComponentType | undefined) { + if (typeof document === 'string') { + this._fetchDocument(document); + } else if (document) { + this.portal = new ComponentPortal(document); + + // Resolving and creating components dynamically in Angular happens synchronously, but since + // we want to emit the output if the components are actually rendered completely, we wait + // until the Angular zone becomes stable. + this._ngZone.onStable + .pipe(take(1)) + .subscribe(() => this.contentRendered.next(this._elementRef.nativeElement)); } } diff --git a/src/app/shared/example-viewer/code-snippet.html b/src/app/shared/example-viewer/code-snippet.html index 09b54722..61de6b25 100644 --- a/src/app/shared/example-viewer/code-snippet.html +++ b/src/app/shared/example-viewer/code-snippet.html @@ -1,3 +1,3 @@
-
+
diff --git a/src/app/shared/guide-items/guide-items.ts b/src/app/shared/guide-items/guide-items.ts index 43563532..4790b901 100644 --- a/src/app/shared/guide-items/guide-items.ts +++ b/src/app/shared/guide-items/guide-items.ts @@ -1,13 +1,15 @@ import {Injectable} from '@angular/core'; +import {SystemVariables} from '../../pages/system-variables'; +import {ComponentType} from '@angular/cdk/portal'; export interface GuideItem { id: string; name: string; - document: string; overview: string; + document: string | ComponentType; } -const GUIDES = [ +const GUIDES: GuideItem[] = [ { id: 'getting-started', name: 'Getting started', @@ -26,6 +28,12 @@ const GUIDES = [ document: '/docs-content/guides/theming.html', overview: 'Customize your application with Angular Material\'s theming system.' }, + { + id: 'system-variables', + name: 'System Variables', + document: SystemVariables, + overview: 'Understand the system variables available to use in your application.' + }, { id: 'theming-your-components', name: 'Theming your own components', diff --git a/src/app/shared/table-of-contents/table-of-contents.ts b/src/app/shared/table-of-contents/table-of-contents.ts index 9bdc7814..5a9c4d60 100644 --- a/src/app/shared/table-of-contents/table-of-contents.ts +++ b/src/app/shared/table-of-contents/table-of-contents.ts @@ -130,7 +130,7 @@ export class TableOfContents implements OnInit, AfterViewInit, OnDestroy { id: header.id, active: false }; - }); + }).filter(link => link.id); this._linkSections[sectionIndex] = {name: sectionName, links}; this._links.push(...links); diff --git a/src/styles.scss b/src/styles.scss index e4c7791e..6febe002 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -6,6 +6,9 @@ @use './styles/general'; html { + background-color: var(--mat-sys-surface); + color: var(--mat-sys-on-surface); + @include mat.theme(( color: ( theme-type: light,