From 40cd5a189407c7efdee1d207ce334ca522382466 Mon Sep 17 00:00:00 2001 From: bre97-web Date: Thu, 29 Aug 2024 20:53:19 +0800 Subject: [PATCH] feat: ColorProvider can customize color, exclude tokens --- .../material-design-theme-token.interface.ts | 59 ++ src/tokens/internal/color.ts | 526 ++++++------------ src/tokens/provide-all.ts | 10 +- src/tokens/provide-color.ts | 15 +- 4 files changed, 253 insertions(+), 357 deletions(-) create mode 100644 src/declaration/material-design-theme-token.interface.ts diff --git a/src/declaration/material-design-theme-token.interface.ts b/src/declaration/material-design-theme-token.interface.ts new file mode 100644 index 0000000..711606a --- /dev/null +++ b/src/declaration/material-design-theme-token.interface.ts @@ -0,0 +1,59 @@ + +export interface IMaterialDesignThemeTokens { + primaryPaletteKeyColor: string + secondaryPaletteKeyColor: string + tertiaryPaletteKeyColor: string + neutralPaletteKeyColor: string + neutralVariantPaletteKeyColor: string + background: string + onBackground: string + surface: string + surfaceDim: string + surfaceBright: string + surfaceContainerLowest: string + surfaceContainerLow: string + surfaceContainer: string + surfaceContainerHigh: string + surfaceContainerHighest: string + onSurface: string + surfaceVariant: string + onSurfaceVariant: string + inverseSurface: string + inverseOnSurface: string + outline: string + outlineVariant: string + shadow: string + scrim: string + surfaceTint: string + primary: string + onPrimary: string + primaryContainer: string + onPrimaryContainer: string + inversePrimary: string + secondary: string + onSecondary: string + secondaryContainer: string + onSecondaryContainer: string + tertiary: string + onTertiary: string + tertiaryContainer: string + onTertiaryContainer: string + error: string + onError: string + errorContainer: string + onErrorContainer: string + primaryFixed: string + primaryFixedDim: string + onPrimaryFixed: string + onPrimaryFixedVariant: string + secondaryFixed: string + secondaryFixedDim: string + onSecondaryFixed: string + onSecondaryFixedVariant: string + tertiaryFixed: string + tertiaryFixedDim: string + onTertiaryFixed: string + onTertiaryFixedVariant: string +} + +// export interface IMaterialDesignThemeToken diff --git a/src/tokens/internal/color.ts b/src/tokens/internal/color.ts index 098e351..fe8a726 100644 --- a/src/tokens/internal/color.ts +++ b/src/tokens/internal/color.ts @@ -1,363 +1,197 @@ import plugin from "tailwindcss/plugin" +import type { CSSRuleObject } from "tailwindcss/types/config" +import type { IMaterialDesignThemeTokens } from "../../declaration/material-design-theme-token.interface" import type { IProvider } from "../../declaration/provider.interface" +import { Strings } from "../../utils/strings" export type TColorProviderConstructorParams = { - readonly prefix?: string + readonly prefix: string + readonly defaultTokens: IMaterialDesignThemeTokens | Record + readonly useDefaultValue: boolean + readonly excludedTokens: Array<(keyof IMaterialDesignThemeTokens) | {}> } -export class ColorProvider implements IProvider { +class DefaultMaterialDesignThemeTokens implements IMaterialDesignThemeTokens { + /** + * Key colors + */ + primaryPaletteKeyColor = '#047aff' + secondaryPaletteKeyColor = '#727598' + tertiaryPaletteKeyColor = '#7b70a3' + neutralPaletteKeyColor = '#737782' + neutralVariantPaletteKeyColor = '#727785' + + /** + * Base colors + */ + background = '#f9f9ff' + onBackground = '#181c25' + surface = '#f9f9ff' + surfaceDim = '#d7d9e6' + surfaceBright = '#f9f9ff' + surfaceContainerLowest = '#ffffff' + surfaceContainerLow = '#f1f3ff' + surfaceContainer = '#ebedfa' + surfaceContainerHigh = '#e5e8f5' + surfaceContainerHighest = '#dfe2ef' + onSurface = '#181c25' + surfaceVariant = '#dee2f2' + onSurfaceVariant = '#424753' + inverseSurface = '#2c303a' + inverseOnSurface = '#eef0fd' + shadow = '#000000' + scrim = '#000000' + surfaceTint = '#005ac1' + primary = '#005ac1' + onPrimary = '#ffffff' + primaryContainer = '#d8e2ff' + onPrimaryContainer = '#001a41' + inversePrimary = '#adc6ff' + secondary = '#595c7e' + onSecondary = '#ffffff' + secondaryContainer = '#dfe0ff' + onSecondaryContainer = '#151937' + tertiary = '#625789' + onTertiary = '#ffffff' + tertiaryContainer = '#e7deff' + onTertiaryContainer = '#1e1341' + error = '#ba1a1a' + onError = '#ffffff' + errorContainer = '#ffdad6' + onErrorContainer = '#410002' + + /** + * Border as bg/text + */ + outline = '#727785' + outlineVariant = '#c2c6d6' + + /** + * Fixed colors + */ + primaryFixed = '#d8e2ff' + primaryFixedDim = '#adc6ff' + onPrimaryFixed = '#001a41' + onPrimaryFixedVariant = '#004494' + secondaryFixed = '#dfe0ff' + secondaryFixedDim = '#c1c4eb' + onSecondaryFixed = '#151937' + onSecondaryFixedVariant = '#414465' + tertiaryFixed = '#e7deff' + tertiaryFixedDim = '#ccbff8' + onTertiaryFixed = '#1e1341' + onTertiaryFixedVariant = '#4a4070' + + public get defaultTokenValues() { + return { + primaryPaletteKeyColor: this.primaryPaletteKeyColor, + secondaryPaletteKeyColor: this.secondaryPaletteKeyColor, + tertiaryPaletteKeyColor: this.tertiaryPaletteKeyColor, + neutralPaletteKeyColor: this.neutralPaletteKeyColor, + neutralVariantPaletteKeyColor: this.neutralVariantPaletteKeyColor, + background: this.background, + onBackground: this.onBackground, + surface: this.surface, + surfaceDim: this.surfaceDim, + surfaceBright: this.surfaceBright, + surfaceContainerLowest: this.surfaceContainerLowest, + surfaceContainerLow: this.surfaceContainerLow, + surfaceContainer: this.surfaceContainer, + surfaceContainerHigh: this.surfaceContainerHigh, + surfaceContainerHighest: this.surfaceContainerHighest, + onSurface: this.onSurface, + surfaceVariant: this.surfaceVariant, + onSurfaceVariant: this.onSurfaceVariant, + inverseSurface: this.inverseSurface, + inverseOnSurface: this.inverseOnSurface, + outline: this.outline, + outlineVariant: this.outlineVariant, + shadow: this.shadow, + scrim: this.scrim, + surfaceTint: this.surfaceTint, + primary: this.primary, + onPrimary: this.onPrimary, + primaryContainer: this.primaryContainer, + onPrimaryContainer: this.onPrimaryContainer, + inversePrimary: this.inversePrimary, + secondary: this.secondary, + onSecondary: this.onSecondary, + secondaryContainer: this.secondaryContainer, + onSecondaryContainer: this.onSecondaryContainer, + tertiary: this.tertiary, + onTertiary: this.onTertiary, + tertiaryContainer: this.tertiaryContainer, + onTertiaryContainer: this.onTertiaryContainer, + error: this.error, + onError: this.onError, + errorContainer: this.errorContainer, + onErrorContainer: this.onErrorContainer, + primaryFixed: this.primaryFixed, + primaryFixedDim: this.primaryFixedDim, + onPrimaryFixed: this.onPrimaryFixed, + onPrimaryFixedVariant: this.onPrimaryFixedVariant, + secondaryFixed: this.secondaryFixed, + secondaryFixedDim: this.secondaryFixedDim, + onSecondaryFixed: this.onSecondaryFixed, + onSecondaryFixedVariant: this.onSecondaryFixedVariant, + tertiaryFixed: this.tertiaryFixed, + tertiaryFixedDim: this.tertiaryFixedDim, + onTertiaryFixed: this.onTertiaryFixed, + onTertiaryFixedVariant: this.onTertiaryFixedVariant, + } + } +} + +export class ColorProvider extends DefaultMaterialDesignThemeTokens implements IProvider { public prefix + public tokens + public useDefaultValue + public excludedTokens - constructor(params: TColorProviderConstructorParams) { - this.prefix = params.prefix + constructor(params: Partial) { + super() + this.prefix = params.prefix ?? 'md-sys-color' + this.useDefaultValue = params.useDefaultValue ?? true + this.excludedTokens = params.excludedTokens ?? [] + this.tokens = this.validate((params.defaultTokens ?? {}) as Record) } - getPlugin() { - return plugin(({ addUtilities }) => { - /** - * Base colors - */ - addUtilities({ - '.bg-background': { - 'background-color': `var(--${this.prefix}-background, #f9f9ff)`, - }, - '.text-background': { - 'color': `var(--${this.prefix}-background, #f9f9ff)`, - }, - '.bg-on-background': { - 'background-color': `var(--${this.prefix}-on-background, #181c25)`, - }, - '.text-on-background': { - 'color': `var(--${this.prefix}-on-background, #181c25)`, - }, - '.bg-surface': { - 'background-color': `var(--${this.prefix}-surface, #f9f9ff)`, - }, - '.text-surface': { - 'color': `var(--${this.prefix}-surface, #f9f9ff)`, - }, - '.bg-surface-dim': { - 'background-color': `var(--${this.prefix}-surface-dim, #d7d9e6)`, - }, - '.text-surface-dim': { - 'color': `var(--${this.prefix}-surface-dim, #d7d9e6)`, - }, - '.bg-surface-bright': { - 'background-color': `var(--${this.prefix}-surface-bright, #f9f9ff)`, - }, - '.text-surface-bright': { - 'color': `var(--${this.prefix}-surface-bright, #f9f9ff)`, - }, - '.bg-on-surface': { - 'background-color': `var(--${this.prefix}-on-surface, #181c25)`, - }, - '.text-on-surface': { - 'color': `var(--${this.prefix}-on-surface, #181c25)`, - }, - '.bg-surface-variant': { - 'background-color': `var(--${this.prefix}-surface-variant, #dee2f2)`, - }, - '.text-surface-variant': { - 'color': `var(--${this.prefix}-surface-variant, #dee2f2)`, - }, - '.bg-on-surface-variant': { - 'background-color': `var(--${this.prefix}-on-surface-variant, #424753)`, - }, - '.text-on-surface-variant': { - 'color': `var(--${this.prefix}-on-surface-variant, #424753)`, - }, - '.bg-inverse-surface': { - 'background-color': `var(--${this.prefix}-inverse-surface, #2c303a)`, - }, - '.text-inverse-surface': { - 'color': `var(--${this.prefix}-inverse-surface, #2c303a)`, - }, - '.bg-inverse-on-surface': { - 'background-color': `var(--${this.prefix}-inverse-on-surface, #eef0fd)`, - }, - '.text-inverse-on-surface': { - 'color': `var(--${this.prefix}-inverse-on-surface, #eef0fd)`, - }, - '.bg-scrim': { - 'background-color': `var(--${this.prefix}-scrim, #000000)`, - }, - '.text-scrim': { - 'color': `var(--${this.prefix}-scrim, #000000)`, - }, - '.bg-surface-tint': { - 'background-color': `var(--${this.prefix}-surface-tint, #005ac1)`, - }, - '.text-surface-tint': { - 'color': `var(--${this.prefix}-surface-tint, #005ac1)`, - }, - '.bg-primary': { - 'background-color': `var(--${this.prefix}-primary, #005ac1)`, - }, - '.text-primary': { - 'color': `var(--${this.prefix}-primary, #005ac1)`, - }, - '.bg-on-primary': { - 'background-color': `var(--${this.prefix}-on-primary, #ffffff)`, - }, - '.text-on-primary': { - 'color': `var(--${this.prefix}-on-primary, #ffffff)`, - }, - '.bg-primary-container': { - 'background-color': `var(--${this.prefix}-primary-container, #d8e2ff)`, - }, - '.text-primary-container': { - 'color': `var(--${this.prefix}-primary-container, #d8e2ff)`, - }, - '.bg-on-primary-container': { - 'background-color': `var(--${this.prefix}-on-primary-container, #001a41)`, - }, - '.text-on-primary-container': { - 'color': `var(--${this.prefix}-on-primary-container, #001a41)`, - }, - '.bg-inverse-primary': { - 'background-color': `var(--${this.prefix}-inverse-primary, #adc6ff)`, - }, - '.text-inverse-primary': { - 'color': `var(--${this.prefix}-inverse-primary, #adc6ff)`, - }, - '.bg-secondary': { - 'background-color': `var(--${this.prefix}-secondary, #595c7e)`, - }, - '.text-secondary': { - 'color': `var(--${this.prefix}-secondary, #595c7e)`, - }, - '.bg-on-secondary': { - 'background-color': `var(--${this.prefix}-on-secondary, #ffffff)`, - }, - '.text-on-secondary': { - 'color': `var(--${this.prefix}-on-secondary, #ffffff)`, - }, - '.bg-secondary-container': { - 'background-color': `var(--${this.prefix}-secondary-container, #dfe0ff)`, - }, - '.text-secondary-container': { - 'color': `var(--${this.prefix}-secondary-container, #dfe0ff)`, - }, - '.bg-on-secondary-container': { - 'background-color': `var(--${this.prefix}-on-secondary-container, #151937)`, - }, - '.text-on-secondary-container': { - 'color': `var(--${this.prefix}-on-secondary-container, #151937)`, - }, - '.bg-tertiary': { - 'background-color': `var(--${this.prefix}-tertiary, #625789)`, - }, - '.text-tertiary': { - 'color': `var(--${this.prefix}-tertiary, #625789)`, - }, - '.bg-on-tertiary': { - 'background-color': `var(--${this.prefix}-on-tertiary, #ffffff)`, - }, - '.text-on-tertiary': { - 'color': `var(--${this.prefix}-on-tertiary, #ffffff)`, - }, - '.bg-tertiary-container': { - 'background-color': `var(--${this.prefix}-tertiary-container, #e7deff)`, - }, - '.text-tertiary-container': { - 'color': `var(--${this.prefix}-tertiary-container, #e7deff)`, - }, - '.bg-on-tertiary-container': { - 'background-color': `var(--${this.prefix}-on-tertiary-container, #1e1341)`, - }, - '.text-on-tertiary-container': { - 'color': `var(--${this.prefix}-on-tertiary-container, #1e1341)`, - }, - '.bg-error': { - 'background-color': `var(--${this.prefix}-error, #ba1a1a)`, - }, - '.text-error': { - 'color': `var(--${this.prefix}-error, #ba1a1a)`, - }, - '.bg-on-error': { - 'background-color': `var(--${this.prefix}-on-error, #ffffff)`, - }, - '.text-on-error': { - 'color': `var(--${this.prefix}-on-error, #ffffff)`, - }, - '.bg-error-container': { - 'background-color': `var(--${this.prefix}-error-container, #ffdad6)`, - }, - '.text-error-container': { - 'color': `var(--${this.prefix}-error-container, #ffdad6)`, - }, - '.bg-on-error-container': { - 'background-color': `var(--${this.prefix}-on-error-container, #410002)`, - }, - '.text-on-error-container': { - 'color': `var(--${this.prefix}-on-error-container, #410002)`, - }, - '.bg-outline': { - 'background-color': `var(--${this.prefix}-outline, #727785)`, - }, - '.text-outline': { - 'color': `var(--${this.prefix}-outline, #727785)`, - }, - '.bg-outline-variant': { - 'background-color': `var(--${this.prefix}-outline-variant, #c2c6d6)`, - }, - '.text-outline-variant': { - 'color': `var(--${this.prefix}-outline-variant, #c2c6d6)`, - }, - '.bg-shadow': { - 'background-color': `var(--${this.prefix}-shadow, #000000)`, - }, - '.text-shadow': { - 'color': `var(--${this.prefix}-shadow, #000000)`, - }, - }) + protected validate(tokens: Record) { + const newTokens = {} as Record + for (const token of Object.entries(this.defaultTokenValues)) { + const tokenName = token[0] + const defaultTokenValue = token[1] - /** - * Border color - */ - addUtilities({ - '.border-outline': { - 'border-color': `var(--${this.prefix}-outline, #727785)`, - }, - '.border-outline-variant': { - 'border-color': `var(--${this.prefix}-outline-variant, #c2c6d6)`, - }, - }) + if (this.excludedTokens.includes(tokenName)) { + continue + } - /** - * Containers, background-color only. - */ - addUtilities({ - '.bg-surface-container-lowest': { - 'background-color': `var(--${this.prefix}-surface-container-lowest, #ffffff)`, - }, - '.bg-surface-container-low': { - 'background-color': `var(--${this.prefix}-surface-container-low, #f1f3ff)`, - }, - '.bg-surface-container': { - 'background-color': `var(--${this.prefix}-surface-container, #ebedfa)`, - }, - '.bg-surface-container-high': { - 'background-color': `var(--${this.prefix}-surface-container-high, #e5e8f5)`, - }, - '.bg-surface-container-highest': { - 'background-color': `var(--${this.prefix}-surface-container-highest, #dfe2ef)`, - }, - }) + if (!Object.hasOwn(tokens, tokenName)) { + newTokens[tokenName] = defaultTokenValue + } else { + newTokens[tokenName] = tokens[tokenName] + } + } + return newTokens + } - /** - * Fixed colors - */ - addUtilities({ - '.bg-primary-fixed': { - 'background-color': `var(--${this.prefix}-primary-fixed, #d8e2ff)`, - }, - '.text-primary-fixed': { - 'color': `var(--${this.prefix}-primary-fixed, #d8e2ff)`, - }, - '.bg-primary-fixed-dim': { - 'background-color': `var(--${this.prefix}-primary-fixed-dim, #adc6ff)`, - }, - '.text-primary-fixed-dim': { - 'color': `var(--${this.prefix}-primary-fixed-dim, #adc6ff)`, - }, - '.bg-on-primary-fixed': { - 'background-color': `var(--${this.prefix}-on-primary-fixed, #001a41)`, - }, - '.text-on-primary-fixed': { - 'color': `var(--${this.prefix}-on-primary-fixed, #001a41)`, - }, - '.bg-on-primary-fixed-variant': { - 'background-color': `var(--${this.prefix}-on-primary-fixed-variant, #004494)`, - }, - '.text-on-primary-fixed-variant': { - 'color': `var(--${this.prefix}-on-primary-fixed-variant, #004494)`, - }, - '.bg-secondary-fixed': { - 'background-color': `var(--${this.prefix}-secondary-fixed, #dfe0ff)`, - }, - '.text-secondary-fixed': { - 'color': `var(--${this.prefix}-secondary-fixed, #dfe0ff)`, - }, - '.bg-secondary-fixed-dim': { - 'background-color': `var(--${this.prefix}-secondary-fixed-dim, #c1c4eb)`, - }, - '.text-secondary-fixed-dim': { - 'color': `var(--${this.prefix}-secondary-fixed-dim, #c1c4eb)`, - }, - '.bg-on-secondary-fixed': { - 'background-color': `var(--${this.prefix}-on-secondary-fixed, #151937)`, - }, - '.text-on-secondary-fixed': { - 'color': `var(--${this.prefix}-on-secondary-fixed, #151937)`, - }, - '.bg-on-secondary-fixed-variant': { - 'background-color': `var(--${this.prefix}-on-secondary-fixed-variant, #414465)`, - }, - '.text-on-secondary-fixed-variant': { - 'color': `var(--${this.prefix}-on-secondary-fixed-variant, #414465)`, - }, - '.bg-tertiary-fixed': { - 'background-color': `var(--${this.prefix}-tertiary-fixed, #e7deff)`, - }, - '.text-tertiary-fixed': { - 'color': `var(--${this.prefix}-tertiary-fixed, #e7deff)`, - }, - '.bg-tertiary-fixed-dim': { - 'background-color': `var(--${this.prefix}-tertiary-fixed-dim, #ccbff8)`, - }, - '.text-tertiary-fixed-dim': { - 'color': `var(--${this.prefix}-tertiary-fixed-dim, #ccbff8)`, - }, - '.bg-on-tertiary-fixed': { - 'background-color': `var(--${this.prefix}-on-tertiary-fixed, #1e1341)`, - }, - '.text-on-tertiary-fixed': { - 'color': `var(--${this.prefix}-on-tertiary-fixed, #1e1341)`, - }, - '.bg-on-tertiary-fixed-variant': { - 'background-color': `var(--${this.prefix}-on-tertiary-fixed-variant, #4a4070)`, - }, - '.text-on-tertiary-fixed-variant': { - 'color': `var(--${this.prefix}-on-tertiary-fixed-variant, #4a4070)`, - }, - }) + protected transformTokensToCssRuleObjectRecord(prefix: string, tokens: Record) { + const newTokens = {} as Record + for (const token in tokens) { + const className = Strings.toKebabCase(token) + const cssVariantToken = `--${prefix}-${Strings.toKebabCase(token)}` + const cssVariantValue = tokens[token] - /** - * Key colors - */ - addUtilities({ - '.bg-primary-palette-key-color': { - 'background-color': `var(--${this.prefix}-primary-palette-key-color, #047aff)`, - }, - '.text-primary-palette-key-color': { - 'color': `var(--${this.prefix}-primary-palette-key-color, #047aff)`, - }, - '.bg-secondary-palette-key-color': { - 'background-color': `var(--${this.prefix}-secondary-palette-key-color, #727598)`, - }, - '.text-secondary-palette-key-color': { - 'color': `var(--${this.prefix}-secondary-palette-key-color, #727598)`, - }, - '.bg-tertiary-palette-key-color': { - 'background-color': `var(--${this.prefix}-tertiary-palette-key-color, #7b70a3)`, - }, - '.text-tertiary-palette-key-color': { - 'color': `var(--${this.prefix}-tertiary-palette-key-color, #7b70a3)`, - }, - '.bg-neutral-palette-key-color': { - 'background-color': `var(--${this.prefix}-neutral-palette-key-color, #737782)`, - }, - '.text-neutral-palette-key-color': { - 'color': `var(--${this.prefix}-neutral-palette-key-color, #737782)`, - }, - '.bg-neutral-variant-palette-key-color': { - 'background-color': `var(--${this.prefix}-neutral-variant-palette-key-color, #727785)`, - }, - '.text-neutral-variant-palette-key-color': { - 'color': `var(--${this.prefix}-neutral-variant-palette-key-color, #727785)`, - }, - }) + newTokens[`.bg-${className}`] = { 'background-color': `var(${cssVariantToken}, ${this.useDefaultValue ? cssVariantValue : ''})` } + newTokens[`.text-${className}`] = { color: `var(${cssVariantToken}, ${this.useDefaultValue ? cssVariantValue : ''})` } + } + return newTokens + } + + getPlugin() { + const tokens = this.transformTokensToCssRuleObjectRecord(this.prefix, this.tokens!) + return plugin(({ addUtilities }) => { + addUtilities(tokens) }) } diff --git a/src/tokens/provide-all.ts b/src/tokens/provide-all.ts index ce03c9a..8bfa1dd 100644 --- a/src/tokens/provide-all.ts +++ b/src/tokens/provide-all.ts @@ -10,18 +10,12 @@ import { provideShape } from "./provide-shape"; import { provideTypography } from "./provide-typography"; export function provideAll(params: { - color?: TColorProviderConstructorParams, + color?: Partial, elevation?: TElevationProviderConstructorParams, motion?: TMotionProviderConstructorParams, shape?: TShapeProviderConstructorParams, typography?: TTypographyProviderConstructorParams, -} = { - color: { prefix: 'md-sys-color' }, - elevation: { shadowToken: 'md-sys-color-shadow' }, - motion: { prefix: 'md-sys-motion' }, - shape: { prefix: 'md-sys-shape', defaultUnit: '1' }, - typography: { prefix: 'md-sys-typescale' } - }) { +}) { return ({ color: provideColor(params.color), elevation: provideElevation(params.elevation), diff --git a/src/tokens/provide-color.ts b/src/tokens/provide-color.ts index 49efa15..3201c3b 100644 --- a/src/tokens/provide-color.ts +++ b/src/tokens/provide-color.ts @@ -15,7 +15,16 @@ import { ColorProvider, type TColorProviderConstructorParams } from "./internal/ * @description * Only colors are provided, with default values. * The developer needs to generate the Material Design theme color matching token into HTML or plug-in. + * + * @example + * ```typescript + * const color = provideColor({ + * useDefaultValue: true, + * defaultTokens: { + * background: '#00ff00', + * onBackground: '#ff00ff', + * }, + * }) + * ``` */ -export const provideColor = (params: TColorProviderConstructorParams = { - prefix: 'md-sys-color', -}) => new ColorProvider(params) +export const provideColor = (params?: Partial) => new ColorProvider(params ?? {})