diff --git a/src/browser/index.ts b/src/browser/index.ts index 3a394a9..86416dc 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -1,11 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ThemePreset, Override, validateOverride } from '../shared/theme'; -import { createOverrideDeclarations } from '../shared/declaration'; +import { createOverrideDeclarations, createFullThemeDeclarations } from '../shared/declaration'; import { getNonce, createStyleNode, appendStyleNode } from './dom'; import { createMultiThemeCustomizer } from '../shared/declaration/customizer'; import { getContexts, getThemeFromPreset } from '../shared/theme/validate'; - export interface GenerateThemeStylesheetParams { override: Override; preset: ThemePreset; @@ -52,6 +51,16 @@ export function applyTheme(params: ApplyThemeParams): ApplyThemeResult { }; } +export interface GenerateFullThemeStylesheetParams { + preset: ThemePreset; + baseThemeId?: string; +} + +export function generateFullThemeStylesheet({ preset, baseThemeId }: GenerateFullThemeStylesheetParams): string { + const theme = getThemeFromPreset(preset, baseThemeId); + return createFullThemeDeclarations(theme, preset.propertiesMap); +} + export { Theme, Override, diff --git a/src/shared/declaration/__tests__/__snapshots__/index.test.ts.snap b/src/shared/declaration/__tests__/__snapshots__/index.test.ts.snap index 474c7c9..c240d5c 100644 --- a/src/shared/declaration/__tests__/__snapshots__/index.test.ts.snap +++ b/src/shared/declaration/__tests__/__snapshots__/index.test.ts.snap @@ -1,13 +1,16 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`renderDeclarations > does not render unnecessary declarations 1`] = ` -":global body{ +"@layer cloudscape-base-theme { +:global body{ --fontFamilyBase-css:"Helvetica Neue", Arial, sans-serif; +} }" `; exports[`renderDeclarations > includes secondary theme 1`] = ` -"body{ +"@layer cloudscape-base-theme { +body{ --fontFamilyBase-css:"Helvetica Neue", Arial, sans-serif; --fontFamilyBody-css:var(--fontFamilyBase-css); --black-css:black; @@ -27,51 +30,44 @@ exports[`renderDeclarations > includes secondary theme 1`] = ` .compact{ --scaledSize-css:var(--small-css); } -@media not print {.dark{ - --shadow-css:var(--black-css); - --boxShadow-css:var(--brown-css); - --lineShadow-css:var(--boxShadow-css); -}} .disabled-motion{ --appear-css:0; } -.navigation{ - --shadow-css:var(--black-css); - --boxShadow-css:purple; - --buttonShadow-css:var(--shadow-css); - --lineShadow-css:var(--buttonShadow-css); -} -@media not print {.dark .navigation{ - --shadow-css:var(--brown-css); - --lineShadow-css:var(--boxShadow-css); -}} -@media not print {.dark.navigation{ - --shadow-css:var(--brown-css); - --lineShadow-css:var(--boxShadow-css); -}} .secondary-theme{ --black-css:purple; --brown-css:black; } -.secondary-theme .navigation{ - --boxShadow-css:var(--shadow-css); -} +.secondary-theme .navigation, .navigation.secondary-theme{ --boxShadow-css:var(--shadow-css); } -@media not print {.dark.secondary-theme .navigation{ +@media not print { +.dark{ + --shadow-css:var(--black-css); + --boxShadow-css:var(--brown-css); + --lineShadow-css:var(--boxShadow-css); +} +.dark .navigation, +.dark.navigation{ + --shadow-css:var(--brown-css); + --lineShadow-css:var(--boxShadow-css); +} +.dark.secondary-theme .navigation{ --shadow-css:var(--grey-css); --boxShadow-css:var(--brown-css); -}} -@media not print {.dark.navigation.secondary-theme{ +} +.dark.navigation.secondary-theme{ --shadow-css:var(--grey-css); --boxShadow-css:var(--brown-css); --lineShadow-css:var(--boxShadow-css); -}}" +} +} +}" `; exports[`renderDeclarations > renders declarations for theme with :root selector and context 1`] = ` -":global body{ +"@layer cloudscape-base-theme { +:global body{ --fontFamilyBase-css:"Helvetica Neue", Arial, sans-serif; --fontFamilyBody-css:var(--fontFamilyBase-css); --black-css:black; @@ -91,32 +87,27 @@ exports[`renderDeclarations > renders declarations for theme with :root selector :global .compact{ --scaledSize-css:var(--small-css); } -@media not print {:global .dark{ - --shadow-css:var(--black-css); - --boxShadow-css:var(--brown-css); - --lineShadow-css:var(--boxShadow-css); -}} :global .disabled-motion{ --appear-css:0; } -:global .navigation{ +@media not print { +:global .dark{ --shadow-css:var(--black-css); - --boxShadow-css:purple; - --buttonShadow-css:var(--shadow-css); - --lineShadow-css:var(--buttonShadow-css); -} -@media not print {:global .dark .navigation{ - --shadow-css:var(--brown-css); + --boxShadow-css:var(--brown-css); --lineShadow-css:var(--boxShadow-css); -}} -@media not print {:global .dark.navigation{ +} +:global .dark .navigation, +:global .dark.navigation{ --shadow-css:var(--brown-css); --lineShadow-css:var(--boxShadow-css); -}}" +} +} +}" `; exports[`renderDeclarations > renders declarations for theme with non :root selector 1`] = ` -".secondary-theme{ +"@layer cloudscape-base-theme { +.secondary-theme{ --fontFamilyBase-css:"Helvetica Neue", Arial, sans-serif; --fontFamilyBody-css:var(--fontFamilyBase-css); --black-css:purple; @@ -136,34 +127,28 @@ exports[`renderDeclarations > renders declarations for theme with non :root sele .compact.secondary-theme{ --scaledSize-css:var(--small-css); } -@media not print {.dark.secondary-theme{ - --shadow-css:var(--black-css); - --boxShadow-css:var(--brown-css); - --lineShadow-css:var(--boxShadow-css); -}} .disabled-motion.secondary-theme{ --appear-css:0; } -.secondary-theme .navigation{ - --shadow-css:var(--black-css); - --buttonShadow-css:var(--shadow-css); - --boxShadow-css:var(--shadow-css); - --lineShadow-css:var(--buttonShadow-css); -} +.secondary-theme .navigation, .navigation.secondary-theme{ --shadow-css:var(--black-css); --buttonShadow-css:var(--shadow-css); --boxShadow-css:var(--shadow-css); --lineShadow-css:var(--buttonShadow-css); } -@media not print {.dark.secondary-theme .navigation{ - --shadow-css:var(--grey-css); +@media not print { +.dark.secondary-theme{ + --shadow-css:var(--black-css); --boxShadow-css:var(--brown-css); --lineShadow-css:var(--boxShadow-css); -}} -@media not print {.dark.navigation.secondary-theme{ +} +.dark.secondary-theme .navigation, +.dark.navigation.secondary-theme{ --shadow-css:var(--grey-css); --boxShadow-css:var(--brown-css); --lineShadow-css:var(--boxShadow-css); -}}" +} +} +}" `; diff --git a/src/shared/declaration/index.ts b/src/shared/declaration/index.ts index 04fccb3..e9ad178 100644 --- a/src/shared/declaration/index.ts +++ b/src/shared/declaration/index.ts @@ -7,8 +7,9 @@ import { RuleCreator } from './rule'; import { SingleThemeCreator } from './single'; import { MultiThemeCreator } from './multi'; import { Selector } from './selector'; -import { UsedPropertyRegistry } from './registry'; -import { MinimalTransformer } from './transformer'; +import { AllPropertyRegistry, UsedPropertyRegistry } from './registry'; +import { MinimalTransformer, SelectorMergeOptimizer } from './transformer'; +import Stylesheet from './stylesheet'; import { cloneDeep, values } from '../utils'; function createMinimalTheme(base: Theme, override: Override): Theme { @@ -66,6 +67,10 @@ function addMissingTokensToTheme(theme: Theme, tokens: string[], sourceTheme: Th }); } +function optimize(stylesheet: Stylesheet): string { + return new SelectorMergeOptimizer().transform(new MinimalTransformer().transform(stylesheet)).toString(); +} + export function createOverrideDeclarations( base: Theme, override: Override, @@ -86,7 +91,7 @@ export function createOverrideDeclarations( new UsedPropertyRegistry(propertiesMap, usedTokens), ); const stylesheet = new SingleThemeCreator(minimalTheme, ruleCreator, base, propertiesMap).create(); - return new MinimalTransformer().transform(stylesheet).toString(); + return optimize(stylesheet); } export function createBuildDeclarations( @@ -105,5 +110,16 @@ export function createBuildDeclarations( new UsedPropertyRegistry(propertiesMap, usedTokens), ); const stylesheet = new MultiThemeCreator(themes, ruleCreator, propertiesMap).create(); - return new MinimalTransformer().transform(stylesheet).toString(); + return `@layer cloudscape-base-theme {\n${optimize(stylesheet)}\n}`; +} + +export function createFullThemeDeclarations( + theme: Theme, + propertiesMap: PropertiesMap, + selectorCustomizer: SelectorCustomizer = (s) => s, +): string { + const ruleCreator = new RuleCreator(new Selector(selectorCustomizer), new AllPropertyRegistry(propertiesMap)); + const stylesheet = new SingleThemeCreator(theme, ruleCreator).create(); + const css = optimize(stylesheet); + return `@layer cloudscape-base-theme, cloudscape-theme;\n@layer cloudscape-theme {\n${css}\n}`; } diff --git a/src/shared/declaration/stylesheet.ts b/src/shared/declaration/stylesheet.ts index 924cdad..a9d9373 100644 --- a/src/shared/declaration/stylesheet.ts +++ b/src/shared/declaration/stylesheet.ts @@ -47,9 +47,26 @@ export default class Stylesheet { * @returns CSS */ toString(): string { - return asValuesArray(this.rulesMap) - .map((rule) => rule.toString()) - .join('\n'); + const rules = asValuesArray(this.rulesMap); + const mediaGroups = new Map(); + const result: string[] = []; + + rules.forEach((rule) => { + if (!rule.media) { + result.push(rule.toString()); + return; + } + const group = mediaGroups.get(rule.media) ?? []; + if (group.length === 0) mediaGroups.set(rule.media, group); + group.push(rule); + }); + + mediaGroups.forEach((group, media) => { + const inner = group.map((r) => r.toRuleString()).join('\n'); + result.push(`@media ${media} {\n${inner}\n}`); + }); + + return result.join('\n'); } } @@ -82,14 +99,18 @@ export class Rule { } toString(): string { - const lines = asValuesArray(this.declarationsMap).map((decl) => decl.toString()); - const rule = `${this.selector}{\n\t${lines.join('\n\t')}\n}`; + const rule = this.toRuleString(); if (this.media) { return `@media ${this.media} {${rule}}`; } return rule; } + toRuleString(): string { + const lines = asValuesArray(this.declarationsMap).map((decl) => decl.toString()); + return `${this.selector}{\n\t${lines.join('\n\t')}\n}`; + } + isModeRule(): boolean { return !!this.media; } diff --git a/src/shared/declaration/transformer.ts b/src/shared/declaration/transformer.ts index 51a9897..27f5e5b 100644 --- a/src/shared/declaration/transformer.ts +++ b/src/shared/declaration/transformer.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { entries } from '../utils'; import type Stylesheet from './stylesheet'; -import { Declaration } from './stylesheet'; +import { Declaration, Rule } from './stylesheet'; import { getFirstSelector, isGlobalSelector } from '../styles/selector'; import { getReferencedVar } from './utils'; @@ -97,6 +97,30 @@ export class MinimalTransformer implements Transformer { } } +export class SelectorMergeOptimizer implements Transformer { + transform(stylesheet: Stylesheet): Stylesheet { + const rules = stylesheet.getAllRules(); + const byDeclarations = new Map(); + rules.forEach((rule) => { + const key = `${rule.media ?? ''}|${rule + .getAllDeclarations() + .map((d) => d.toString()) + .join('')}`; + const group = byDeclarations.get(key) ?? []; + group.push(rule); + byDeclarations.set(key, group); + }); + + byDeclarations.forEach((group) => { + if (group.length < 2) return; + group[0].selector = group.map((r) => r.selector).join(',\n'); + group.slice(1).forEach((r) => stylesheet.removeRule(r)); + }); + + return stylesheet; + } +} + function difference(mapA: Record, mapB: Record): Record { const diff: Record = {};