From 6defea35fb9eca6a924e023ab078ab9e3bb28d3c Mon Sep 17 00:00:00 2001 From: Brian Heston <47367562+bheston@users.noreply.github.com> Date: Thu, 12 May 2022 10:33:21 -0700 Subject: [PATCH 1/4] Add adaptive-ui package Update readme, remove jest --- packages/utilities/adaptive-ui/.eslintignore | 10 + packages/utilities/adaptive-ui/.eslintrc.json | 10 + packages/utilities/adaptive-ui/.gitignore | 3 + packages/utilities/adaptive-ui/.mocharc.json | 6 + packages/utilities/adaptive-ui/.npmignore | 10 + packages/utilities/adaptive-ui/.npmrc | 1 + .../utilities/adaptive-ui/.prettierignore | 3 + packages/utilities/adaptive-ui/README.md | 49 + .../utilities/adaptive-ui/api-extractor.json | 4 + .../utilities/adaptive-ui/docs/api-report.md | 664 +++++++++++++ packages/utilities/adaptive-ui/package.json | 50 + .../utilities/adaptive-ui/src/color/README.md | 27 + .../utilities/adaptive-ui/src/color/index.ts | 6 + .../adaptive-ui/src/color/palette-rgb.spec.ts | 15 + .../adaptive-ui/src/color/palette-rgb.ts | 299 ++++++ .../adaptive-ui/src/color/palette.ts | 231 +++++ .../utilities/adaptive-ui/src/color/recipe.ts | 58 ++ .../recipes/black-or-white-by-contrast-set.ts | 49 + .../black-or-white-by-contrast.spec.ts | 21 + .../recipes/black-or-white-by-contrast.ts | 22 + .../recipes/contrast-and-delta-swatch-set.ts | 71 ++ .../src/color/recipes/contrast-swatch.spec.ts | 38 + .../src/color/recipes/contrast-swatch.ts | 22 + .../src/color/recipes/delta-swatch-set.ts | 38 + .../src/color/recipes/delta-swatch.ts | 23 + .../ideal-color-delta-swatch-set.spec.ts | 96 ++ .../recipes/ideal-color-delta-swatch-set.ts | 88 ++ .../adaptive-ui/src/color/recipes/index.ts | 6 + .../utilities/adaptive-ui/src/color/swatch.ts | 98 ++ .../src/color/utilities/binary-search.ts | 31 + .../src/color/utilities/color-constants.ts | 15 + .../color/utilities/direction-by-is-dark.ts | 15 + .../adaptive-ui/src/color/utilities/index.ts | 5 + .../src/color/utilities/is-dark.ts | 22 + .../src/color/utilities/luminance-swatch.ts | 13 + .../src/color/utilities/relative-luminance.ts | 29 + .../src/design-tokens/appearance.ts | 16 + .../adaptive-ui/src/design-tokens/color.ts | 930 ++++++++++++++++++ .../adaptive-ui/src/design-tokens/create.ts | 7 + .../adaptive-ui/src/design-tokens/general.ts | 5 + .../adaptive-ui/src/design-tokens/index.ts | 5 + .../adaptive-ui/src/design-tokens/palette.ts | 40 + .../adaptive-ui/src/design-tokens/type.ts | 166 ++++ packages/utilities/adaptive-ui/src/index.ts | 3 + .../utilities/adaptive-ui/src/type/index.ts | 1 + .../adaptive-ui/src/type/type-ramp.ts | 129 +++ .../utilities/adaptive-ui/tsconfig.build.json | 11 + packages/utilities/adaptive-ui/tsconfig.json | 8 + .../utilities/adaptive-ui/tsconfig.test.json | 8 + 49 files changed, 3477 insertions(+) create mode 100644 packages/utilities/adaptive-ui/.eslintignore create mode 100644 packages/utilities/adaptive-ui/.eslintrc.json create mode 100644 packages/utilities/adaptive-ui/.gitignore create mode 100644 packages/utilities/adaptive-ui/.mocharc.json create mode 100644 packages/utilities/adaptive-ui/.npmignore create mode 100644 packages/utilities/adaptive-ui/.npmrc create mode 100644 packages/utilities/adaptive-ui/.prettierignore create mode 100644 packages/utilities/adaptive-ui/README.md create mode 100644 packages/utilities/adaptive-ui/api-extractor.json create mode 100644 packages/utilities/adaptive-ui/docs/api-report.md create mode 100644 packages/utilities/adaptive-ui/package.json create mode 100644 packages/utilities/adaptive-ui/src/color/README.md create mode 100644 packages/utilities/adaptive-ui/src/color/index.ts create mode 100644 packages/utilities/adaptive-ui/src/color/palette-rgb.spec.ts create mode 100644 packages/utilities/adaptive-ui/src/color/palette-rgb.ts create mode 100644 packages/utilities/adaptive-ui/src/color/palette.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipe.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast-set.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.spec.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/contrast-and-delta-swatch-set.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.spec.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/delta-swatch-set.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/delta-swatch.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.spec.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.ts create mode 100644 packages/utilities/adaptive-ui/src/color/recipes/index.ts create mode 100644 packages/utilities/adaptive-ui/src/color/swatch.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/binary-search.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/color-constants.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/direction-by-is-dark.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/index.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/is-dark.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/luminance-swatch.ts create mode 100644 packages/utilities/adaptive-ui/src/color/utilities/relative-luminance.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/appearance.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/color.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/create.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/general.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/index.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/palette.ts create mode 100644 packages/utilities/adaptive-ui/src/design-tokens/type.ts create mode 100644 packages/utilities/adaptive-ui/src/index.ts create mode 100644 packages/utilities/adaptive-ui/src/type/index.ts create mode 100644 packages/utilities/adaptive-ui/src/type/type-ramp.ts create mode 100644 packages/utilities/adaptive-ui/tsconfig.build.json create mode 100644 packages/utilities/adaptive-ui/tsconfig.json create mode 100644 packages/utilities/adaptive-ui/tsconfig.test.json diff --git a/packages/utilities/adaptive-ui/.eslintignore b/packages/utilities/adaptive-ui/.eslintignore new file mode 100644 index 00000000000..6fde7dc2651 --- /dev/null +++ b/packages/utilities/adaptive-ui/.eslintignore @@ -0,0 +1,10 @@ +# don't ever lint node_modules +node_modules +# don't lint build output (make sure it's set to your correct build folder name) +dist +# don't lint coverage output +coverage +# don't lint www +www +# don't lint test files +__test__ diff --git a/packages/utilities/adaptive-ui/.eslintrc.json b/packages/utilities/adaptive-ui/.eslintrc.json new file mode 100644 index 00000000000..ea40c852980 --- /dev/null +++ b/packages/utilities/adaptive-ui/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["@microsoft/eslint-config-fast-dna", "prettier"], + "rules": { + "@typescript-eslint/no-non-null-assertion": "off", + "import/extensions": [ + "error", + "always" + ] + } +} diff --git a/packages/utilities/adaptive-ui/.gitignore b/packages/utilities/adaptive-ui/.gitignore new file mode 100644 index 00000000000..55e944cd3e6 --- /dev/null +++ b/packages/utilities/adaptive-ui/.gitignore @@ -0,0 +1,3 @@ +tsdoc-metadata.json +temp +test diff --git a/packages/utilities/adaptive-ui/.mocharc.json b/packages/utilities/adaptive-ui/.mocharc.json new file mode 100644 index 00000000000..364e9651b90 --- /dev/null +++ b/packages/utilities/adaptive-ui/.mocharc.json @@ -0,0 +1,6 @@ +{ + "colors": true, + "recursive": true, + "timeout": 5000, + "spec": "test/**/*.spec.js" +} \ No newline at end of file diff --git a/packages/utilities/adaptive-ui/.npmignore b/packages/utilities/adaptive-ui/.npmignore new file mode 100644 index 00000000000..8ae9ed66b33 --- /dev/null +++ b/packages/utilities/adaptive-ui/.npmignore @@ -0,0 +1,10 @@ +# Tests +__test__/ +*.spec.* +*.test.* + +# Source files +src/ + +# babel config +babel.config.js diff --git a/packages/utilities/adaptive-ui/.npmrc b/packages/utilities/adaptive-ui/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/packages/utilities/adaptive-ui/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/utilities/adaptive-ui/.prettierignore b/packages/utilities/adaptive-ui/.prettierignore new file mode 100644 index 00000000000..60aa645faec --- /dev/null +++ b/packages/utilities/adaptive-ui/.prettierignore @@ -0,0 +1,3 @@ +dist/* +test/* +src/__test__/* diff --git a/packages/utilities/adaptive-ui/README.md b/packages/utilities/adaptive-ui/README.md new file mode 100644 index 00000000000..206b9d90661 --- /dev/null +++ b/packages/utilities/adaptive-ui/README.md @@ -0,0 +1,49 @@ +# Adaptive UI + +Adaptive UI is a library for building highly-consistent design systems based around your visual decisions instead of a complex map of color swatches and tokens based on t-shirt sizes. + +This is a core feature of [FAST](https://fast.design) and is incorporated into the [Fluent UI Web Components](https://aka.ms/fluentwebcomponents) and other design systems. + +## Installation + +```shell +npm install --save @microsoft/adaptive-ui +``` + +```shell +yarn add @microsoft/adaptive-ui +``` + +## Getting started + +The most common way to use this library is through the `DesignToken`s. + +For example, in a FAST style sheet: + +```ts +css` + :host { + background: ${neutralFillRest}; + } +` +``` + +This will evaluate the `neutralFillRest` recipe when the component loads and apply the correct color appropriate for the current context within design. + +Most of the color recipes are contextually aware of where they're used so they produce accessible colors based on the configuration of the design, for instance between light mode and dark mode, or when personalization is applied, like a blue tint to the neutral colors or orange accent color. + +To tint the neutral palette, and thus all color recipes derived from it: + +```ts +neutralBaseColor.withDefault("#73818C"); +``` + +To switch to dark mode: + +```ts +fillColor.withDefault("#232323"); +``` + +The layer system for setting content area background colors and improved handling of light and dark mode is evolving and will be added soon. + +See more about the [adaptive color system](./src/color/README.md). diff --git a/packages/utilities/adaptive-ui/api-extractor.json b/packages/utilities/adaptive-ui/api-extractor.json new file mode 100644 index 00000000000..a5c079e97d1 --- /dev/null +++ b/packages/utilities/adaptive-ui/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../../api-extractor.json" +} \ No newline at end of file diff --git a/packages/utilities/adaptive-ui/docs/api-report.md b/packages/utilities/adaptive-ui/docs/api-report.md new file mode 100644 index 00000000000..63d502febf7 --- /dev/null +++ b/packages/utilities/adaptive-ui/docs/api-report.md @@ -0,0 +1,664 @@ +## API Report File for "@microsoft/adaptive-ui" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CSSDesignToken } from '@microsoft/fast-foundation'; +import { CSSDirective } from '@microsoft/fast-element'; +import { DesignToken } from '@microsoft/fast-foundation'; +import { Direction } from '@microsoft/fast-web-utilities'; + +// @public (undocumented) +export const accentBaseColor: CSSDesignToken; + +// @public (undocumented) +export const accentBaseSwatch: DesignToken; + +// @public (undocumented) +export const accentFillActive: CSSDesignToken; + +// @public (undocumented) +export const accentFillActiveDelta: DesignToken; + +// @public (undocumented) +export const accentFillFocus: CSSDesignToken; + +// @public (undocumented) +export const accentFillFocusDelta: DesignToken; + +// @public (undocumented) +export const accentFillHover: CSSDesignToken; + +// @public (undocumented) +export const accentFillHoverDelta: DesignToken; + +// @public (undocumented) +export const accentFillMinContrast: DesignToken; + +// @public (undocumented) +export const accentFillRecipe: DesignToken; + +// @public (undocumented) +export const accentFillRest: CSSDesignToken; + +// @public (undocumented) +export const accentFillRestDelta: DesignToken; + +// @public (undocumented) +export const accentForegroundActive: CSSDesignToken; + +// @public (undocumented) +export const accentForegroundActiveDelta: DesignToken; + +// @public (undocumented) +export const accentForegroundFocus: CSSDesignToken; + +// @public (undocumented) +export const accentForegroundFocusDelta: DesignToken; + +// @public (undocumented) +export const accentForegroundHover: CSSDesignToken; + +// @public (undocumented) +export const accentForegroundHoverDelta: DesignToken; + +// @public (undocumented) +export const accentForegroundMinContrast: DesignToken; + +// @public (undocumented) +export const accentForegroundRecipe: DesignToken; + +// @public (undocumented) +export const accentForegroundRest: CSSDesignToken; + +// @public (undocumented) +export const accentForegroundRestDelta: DesignToken; + +// @public (undocumented) +export const accentPalette: DesignToken>; + +// @public +export class BasePalette implements Palette { + constructor(source: T, swatches: ReadonlyArray); + readonly closestIndexCache: Map; + closestIndexOf(reference: RelativeLuminance): number; + colorContrast(reference: RelativeLuminance, contrastTarget: number, initialSearchIndex?: number, direction?: PaletteDirection): T; + delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T; + get(index: number): T; + readonly lastIndex: number; + readonly reversedSwatches: ReadonlyArray; + readonly source: T; + readonly swatches: ReadonlyArray; +} + +// @internal +export const _black: SwatchRGB; + +// @public +export function blackOrWhiteByContrast(reference: Swatch, minContrast: number, defaultBlack: boolean): Swatch; + +// @public (undocumented) +export const bodyFont: CSSDesignToken; + +// @public +export interface ColorRecipe { + evaluate(element: HTMLElement, reference?: Swatch): Swatch; +} + +// @public +export function contrast(a: RelativeLuminance, b: RelativeLuminance): number; + +// @public +export function contrastAndDeltaSwatchSet(palette: Palette, reference: Swatch, minContrast: number, restDelta: number, hoverDelta: number, activeDelta: number, focusDelta: number, direction?: PaletteDirection): InteractiveSwatchSet; + +// @public +export function contrastSwatch(palette: Palette, reference: Swatch, minContrast: number, direction?: PaletteDirection): Swatch; + +// @public (undocumented) +export const controlCornerRadius: CSSDesignToken; + +// @public +export function deltaSwatch(palette: Palette, reference: Swatch, delta: number, direction?: PaletteDirection): Swatch; + +// @public +export function deltaSwatchSet(palette: Palette, reference: Swatch, restDelta: number, hoverDelta: number, activeDelta: number, focusDelta: number, direction?: PaletteDirection): InteractiveSwatchSet; + +// @public (undocumented) +export const designUnit: CSSDesignToken; + +// @public (undocumented) +export const direction: CSSDesignToken; + +// @public +export function directionByIsDark(color: RelativeLuminance): PaletteDirectionValue; + +// @public (undocumented) +export const fillColor: CSSDesignToken; + +// @public (undocumented) +export const focusStrokeInner: CSSDesignToken; + +// @public (undocumented) +export const focusStrokeInnerRecipe: DesignToken; + +// @public (undocumented) +export const focusStrokeOuter: CSSDesignToken; + +// @public (undocumented) +export const focusStrokeOuterRecipe: DesignToken; + +// @public (undocumented) +export const focusStrokeWidth: CSSDesignToken; + +// @public (undocumented) +export const fontWeight: CSSDesignToken; + +// @public (undocumented) +export const foregroundOnAccentActive: CSSDesignToken; + +// @public (undocumented) +export const foregroundOnAccentFocus: CSSDesignToken; + +// @public (undocumented) +export const foregroundOnAccentHover: CSSDesignToken; + +// @public (undocumented) +export const foregroundOnAccentRecipe: DesignToken; + +// @public (undocumented) +export const foregroundOnAccentRest: CSSDesignToken; + +// @public +export function idealColorDeltaSwatchSet(palette: Palette, reference: Swatch, minContrast: number, idealColor: Swatch, restDelta: number, hoverDelta: number, activeDelta: number, focusDelta: number, direction?: PaletteDirection): InteractiveSwatchSet; + +// @public +export interface InteractiveColorRecipe { + evaluate(element: HTMLElement, reference?: Swatch): InteractiveSwatchSet; +} + +// @public +export interface InteractiveSwatchSet { + active: Swatch; + focus: Swatch; + hover: Swatch; + rest: Swatch; +} + +// @public +export function isDark(color: RelativeLuminance): boolean; + +// @public (undocumented) +export const layerCornerRadius: CSSDesignToken; + +// @public +export function luminanceSwatch(luminance: number): Swatch; + +// @public (undocumented) +export const neutralBaseColor: CSSDesignToken; + +// @public (undocumented) +export const neutralBaseSwatch: DesignToken; + +// @public (undocumented) +export const neutralFillActive: CSSDesignToken; + +// @public (undocumented) +export const neutralFillActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralFillFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralFillFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralFillHover: CSSDesignToken; + +// @public (undocumented) +export const neutralFillHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralFillInputActive: CSSDesignToken; + +// @public (undocumented) +export const neutralFillInputActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralFillInputFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralFillInputFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralFillInputHover: CSSDesignToken; + +// @public (undocumented) +export const neutralFillInputHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralFillInputRecipe: DesignToken; + +// @public (undocumented) +export const neutralFillInputRest: CSSDesignToken; + +// @public (undocumented) +export const neutralFillInputRestDelta: DesignToken; + +// @public (undocumented) +export const neutralFillRecipe: DesignToken; + +// @public (undocumented) +export const neutralFillRest: CSSDesignToken; + +// @public (undocumented) +export const neutralFillRestDelta: DesignToken; + +// @public (undocumented) +export const neutralFillSecondaryActive: CSSDesignToken; + +// @public (undocumented) +export const neutralFillSecondaryActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralFillSecondaryFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralFillSecondaryFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralFillSecondaryHover: CSSDesignToken; + +// @public (undocumented) +export const neutralFillSecondaryHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralFillSecondaryRecipe: DesignToken; + +// @public (undocumented) +export const neutralFillSecondaryRest: CSSDesignToken; + +// @public (undocumented) +export const neutralFillSecondaryRestDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStealthActive: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStealthActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStealthFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStealthFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStealthHover: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStealthHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStealthRecipe: DesignToken; + +// @public (undocumented) +export const neutralFillStealthRest: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStealthRestDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStrongActive: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStrongActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStrongFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStrongFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStrongHover: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStrongHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralFillStrongMinContrast: DesignToken; + +// @public (undocumented) +export const neutralFillStrongRecipe: DesignToken; + +// @public (undocumented) +export const neutralFillStrongRest: CSSDesignToken; + +// @public (undocumented) +export const neutralFillStrongRestDelta: DesignToken; + +// @public (undocumented) +export const neutralForegroundActive: CSSDesignToken; + +// @public (undocumented) +export const neutralForegroundActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralForegroundFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralForegroundFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralForegroundHint: CSSDesignToken; + +// @public (undocumented) +export const neutralForegroundHintRecipe: DesignToken; + +// @public (undocumented) +export const neutralForegroundHover: CSSDesignToken; + +// @public (undocumented) +export const neutralForegroundHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralForegroundMinContrast: DesignToken; + +// @public (undocumented) +export const neutralForegroundRecipe: DesignToken; + +// @public (undocumented) +export const neutralForegroundRest: CSSDesignToken; + +// @public (undocumented) +export const neutralForegroundRestDelta: DesignToken; + +// @public (undocumented) +export const neutralPalette: DesignToken>; + +// @public (undocumented) +export const neutralStrokeActive: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeDividerRecipe: DesignToken; + +// @public (undocumented) +export const neutralStrokeDividerRest: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeDividerRestDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeHover: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeInputActive: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeInputActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeInputFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeInputFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeInputHover: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeInputHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeInputRecipe: DesignToken; + +// @public (undocumented) +export const neutralStrokeInputRest: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeInputRestDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeRecipe: DesignToken; + +// @public (undocumented) +export const neutralStrokeRest: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeRestDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeStrongActive: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeStrongActiveDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeStrongFocus: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeStrongFocusDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeStrongHover: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeStrongHoverDelta: DesignToken; + +// @public (undocumented) +export const neutralStrokeStrongMinContrast: DesignToken; + +// @public (undocumented) +export const neutralStrokeStrongRecipe: DesignToken; + +// @public (undocumented) +export const neutralStrokeStrongRest: CSSDesignToken; + +// @public (undocumented) +export const neutralStrokeStrongRestDelta: DesignToken; + +// @public +export interface Palette { + closestIndexOf(reference: RelativeLuminance): number; + colorContrast(reference: RelativeLuminance, minContrast: number, initialIndex?: number, direction?: PaletteDirection): T; + delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T; + get(index: number): T; + readonly source: T; + readonly swatches: ReadonlyArray; +} + +// @public +export type PaletteDirection = PaletteDirectionValue | (() => PaletteDirectionValue); + +// @public +export enum PaletteDirectionValue { + darker = 1, + lighter = -1 +} + +// @public +export class PaletteRGB extends BasePalette { + static from(source: SwatchRGB, options?: Partial): PaletteRGB; +} + +// @public +export interface PaletteRGBOptions { + preserveSource: boolean; + stepContrast: number; + stepContrastRamp: number; +} + +// @public +export interface RelativeLuminance { + readonly relativeLuminance: number; +} + +// @public +export function resolvePaletteDirection(direction: PaletteDirection): PaletteDirectionValue; + +// @public +export const StandardFontWeight: { + readonly Thin: 100; + readonly ExtraLight: 200; + readonly Light: 300; + readonly Normal: 400; + readonly Medium: 500; + readonly SemiBold: 600; + readonly Bold: 700; + readonly ExtraBold: 800; + readonly Black: 900; +}; + +// @public (undocumented) +export const strokeWidth: CSSDesignToken; + +// @public +export interface Swatch extends RelativeLuminance { + contrast(target: RelativeLuminance): number; + toColorString(): string; +} + +// @public +export class SwatchRGB implements Swatch { + constructor(red: number, green: number, blue: number); + readonly b: number; + contrast: any; + createCSS: () => string; + static from(obj: { + r: number; + g: number; + b: number; + }): SwatchRGB; + readonly g: number; + readonly r: number; + readonly relativeLuminance: number; + toColorString(): string; +} + +// @public (undocumented) +export const typeRampBase: CSSDirective; + +// @public (undocumented) +export const typeRampBaseFontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampBaseFontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampBaseLineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampMinus1: CSSDirective; + +// @public (undocumented) +export const typeRampMinus1FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampMinus1FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampMinus1LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampMinus2: CSSDirective; + +// @public (undocumented) +export const typeRampMinus2FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampMinus2FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampMinus2LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus1: CSSDirective; + +// @public (undocumented) +export const typeRampPlus1FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus1FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus1LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus2: CSSDirective; + +// @public (undocumented) +export const typeRampPlus2FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus2FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus2LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus3: CSSDirective; + +// @public (undocumented) +export const typeRampPlus3FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus3FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus3LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus4: CSSDirective; + +// @public (undocumented) +export const typeRampPlus4FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus4FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus4LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus5: CSSDirective; + +// @public (undocumented) +export const typeRampPlus5FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus5FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus5LineHeight: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus6: CSSDirective; + +// @public (undocumented) +export const typeRampPlus6FontSize: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus6FontVariations: CSSDesignToken; + +// @public (undocumented) +export const typeRampPlus6LineHeight: CSSDesignToken; + +// @internal +export const _white: SwatchRGB; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/utilities/adaptive-ui/package.json b/packages/utilities/adaptive-ui/package.json new file mode 100644 index 00000000000..2e10fee261b --- /dev/null +++ b/packages/utilities/adaptive-ui/package.json @@ -0,0 +1,50 @@ +{ + "name": "@microsoft/adaptive-ui", + "version": "0.1.0", + "description": "A collection of design utilities supporting basic styling and Adaptive UI", + "type": "module", + "main": "./dist/esm/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/fast.git" + }, + "author": { + "name": "Microsoft", + "url": "https://discord.gg/FcSNfg4" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/microsoft/fast/issues/new/choose" + }, + "homepage": "https://fast.design", + "scripts": { + "build": "tsc -p ./tsconfig.build.json && yarn doc", + "build:tests": "tsc -p ./tsconfig.test.json", + "clean:dist": "node ../../../build/clean.js dist", + "doc": "api-extractor run --local", + "doc:ci": "api-extractor run", + "prepare": "yarn clean:dist && yarn build", + "prettier": "prettier --config ../../../.prettierrc --write \"**/*.ts\"", + "prettier:diff": "prettier --config ../../../.prettierrc \"**/*.ts\" --list-different", + "test": "yarn build:tests && yarn eslint && yarn unit-tests && yarn doc", + "eslint": "eslint . --ext .ts", + "eslint:fix": "eslint . --ext .ts --fix", + "unit-tests": "mocha", + "unit-tests:watch": "mocha --watch" + }, + "dependencies": { + "@microsoft/fast-colors": "^5.3.0", + "@microsoft/fast-foundation": "^2.46.2" + }, + "devDependencies": { + "@microsoft/api-extractor": "7.23.1", + "@microsoft/eslint-config-fast-dna": "^2.1.0", + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "chai": "^4.2.0", + "eslint-config-prettier": "^6.10.1", + "mocha": "^7.1.2", + "prettier": "2.0.2", + "typescript": "4.7.0-beta" + } +} diff --git a/packages/utilities/adaptive-ui/src/color/README.md b/packages/utilities/adaptive-ui/src/color/README.md new file mode 100644 index 00000000000..e2c5b5bc8d6 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/README.md @@ -0,0 +1,27 @@ +# Adaptive UI Color Recipes + +Color recipes are algorithmic patterns that produce individual or sets of colors from a variety of inputs. Components can apply these recipes to achieve expressive theming options while maintaining color accessability targets. + +## Swatch +A Swatch is a representation of a color that has a `relativeLuminance` value and a method to convert the swatch to a color string. It is used by recipes to determine which colors to use for UI. + +### SwatchRGB +A concrete implementation of `Swatch`, it is a swatch with red, green, and blue 64bit color channels. + +**Example: Creating a SwatchRGB** +```ts +import { SwatchRGB } from "@microsoft/adaptive-ui"; + +const red = new SwatchRGB(1, 0, 0); +``` + +## Palette +A palette is a collection `Swatch` instances, ordered by relative luminance, and provides mechanisms to safely retrieve swatches by index and by target contrast ratios. It also contains a `source` color, which is the color from which the palette is derived. + +### PaletteRGB +An implementation of `Palette` of `SwatchRGB` instances. + +```ts +// Create a PaletteRGB from a SwatchRGB +const redPalette = PaletteRGB.from(red): +``` diff --git a/packages/utilities/adaptive-ui/src/color/index.ts b/packages/utilities/adaptive-ui/src/color/index.ts new file mode 100644 index 00000000000..a85c7d7976b --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/index.ts @@ -0,0 +1,6 @@ +export * from "./recipes/index.js"; +export * from "./utilities/index.js"; +export * from "./palette-rgb.js"; +export * from "./palette.js"; +export * from "./recipe.js"; +export * from "./swatch.js"; diff --git a/packages/utilities/adaptive-ui/src/color/palette-rgb.spec.ts b/packages/utilities/adaptive-ui/src/color/palette-rgb.spec.ts new file mode 100644 index 00000000000..9432d590f4a --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/palette-rgb.spec.ts @@ -0,0 +1,15 @@ +import chai from "chai"; +import { PaletteRGB } from "./palette-rgb.js"; +import { SwatchRGB } from "./swatch.js"; + +const { expect } = chai; + +const test: SwatchRGB = new SwatchRGB(0, 0, 0); + +describe("PaletteRGB.from", () => { + it("should create a palette from the provided swatch if it matches a SwatchRGB implementation", () => { + const palette = PaletteRGB.from(test); + + expect(palette.source === test).to.be.true; + }); +}); diff --git a/packages/utilities/adaptive-ui/src/color/palette-rgb.ts b/packages/utilities/adaptive-ui/src/color/palette-rgb.ts new file mode 100644 index 00000000000..7d4e449550d --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/palette-rgb.ts @@ -0,0 +1,299 @@ +import { + ColorHSL, + ColorLAB, + ColorRGBA64, + hslToRGB, + interpolateRGB, + labToRGB, + rgbToHSL, + rgbToLAB, + roundToPrecisionSmall, +} from "@microsoft/fast-colors"; +import { BasePalette } from "./palette.js"; +import { SwatchRGB } from "./swatch.js"; +import { contrast } from "./utilities/relative-luminance.js"; + +/** + * A utility Palette that generates many Swatches used for selection in the actual Palette. + * The algorithm uses the LAB color space and keeps the saturation from the source color throughout. + */ +class HighResolutionPaletteRGB extends BasePalette { + /** + * Bump the saturation if it falls below the reference color saturation. + * + * @param reference - Color with target saturation + * @param color - Color to check and bump if below target saturation + * @returns Original or adjusted color + */ + private static saturationBump( + reference: ColorRGBA64, + color: ColorRGBA64 + ): ColorRGBA64 { + const hslReference = rgbToHSL(reference); + const saturationTarget = hslReference.s; + const hslColor = rgbToHSL(color); + if (hslColor.s < saturationTarget) { + const hslNew = new ColorHSL(hslColor.h, saturationTarget, hslColor.l); + return hslToRGB(hslNew); + } + return color; + } + + /** + * Scales input from 0 to 100 to 0 to 0.5. + * + * @param l - Input number, 0 to 100 + * @returns Output number, 0 to 0.5 + */ + private static ramp(l: number) { + const inputval = l / 100; + if (inputval > 0.5) return (inputval - 0.5) / 0.5; //from 0.500001in = 0.00000001out to 1.0in = 1.0out + return 2 * inputval; //from 0in = 0out to 0.5in = 1.0out + } + + /** + * Creates a new high-resolution Palette. + * + * @param source - The source color + * @returns The Palette based on the `source` color + */ + static from(source: SwatchRGB): HighResolutionPaletteRGB { + const swatches: SwatchRGB[] = []; + + const labSource = rgbToLAB(ColorRGBA64.fromObject(source)!.roundToPrecision(4)); + const lab0 = labToRGB(new ColorLAB(0, labSource.a, labSource.b)) + .clamp() + .roundToPrecision(4); + const lab50 = labToRGB(new ColorLAB(50, labSource.a, labSource.b)) + .clamp() + .roundToPrecision(4); + const lab100 = labToRGB(new ColorLAB(100, labSource.a, labSource.b)) + .clamp() + .roundToPrecision(4); + const rgbMin = new ColorRGBA64(0, 0, 0); + const rgbMax = new ColorRGBA64(1, 1, 1); + + const lAbove = lab100.equalValue(rgbMax) ? 0 : 14; + const lBelow = lab0.equalValue(rgbMin) ? 0 : 14; + + // 257 levels max, depending on whether lab0 or lab100 are black or white respectively. + for (let l = 100 + lAbove; l >= 0 - lBelow; l -= 0.5) { + let rgb: ColorRGBA64; + + if (l < 0) { + // For L less than 0, scale from black to L=0 + const percentFromRgbMinToLab0 = l / lBelow + 1; + rgb = interpolateRGB(percentFromRgbMinToLab0, rgbMin, lab0); + } else if (l <= 50) { + // For L less than 50, we scale from L=0 to the base color + rgb = interpolateRGB(HighResolutionPaletteRGB.ramp(l), lab0, lab50); + } else if (l <= 100) { + // For L less than 100, scale from the base color to L=100 + rgb = interpolateRGB(HighResolutionPaletteRGB.ramp(l), lab50, lab100); + } else { + // For L greater than 100, scale from L=100 to white + const percentFromLab100ToRgbMax = (l - 100.0) / lAbove; + rgb = interpolateRGB(percentFromLab100ToRgbMax, lab100, rgbMax); + } + + rgb = HighResolutionPaletteRGB.saturationBump(lab50, rgb).roundToPrecision(4); + + swatches.push(SwatchRGB.from(rgb)); + } + + return new HighResolutionPaletteRGB(source, swatches); + } +} + +/** + * Options to tailor the generation of PaletteRGB. + * + * @public + */ +export interface PaletteRGBOptions { + /** + * The minimum amount of contrast between steps in the palette. Default 1.05. + * Recommended increments by hundredths. + */ + stepContrast: number; + + /** + * Multiplier for increasing step contrast as the swatches darken. Default 0. + * Recommended increments by hundredths. + */ + stepContrastRamp: number; + + /** + * Whether to keep the exact source color in the target palette. Default false. + * Only recommended when the exact color is required and used in stateful interaction recipes like hover. + * Note that custom recipes can still access the source color even if it's not in the ramp, + * but turning this on will potentially increase the contrast between steps toward the ends of the palette. + */ + preserveSource: boolean; +} + +const defaultPaletteRGBOptions: PaletteRGBOptions = { + stepContrast: 1.05, + stepContrastRamp: 0, + preserveSource: false, +}; + +/** + * An implementation of a {@link Palette} that has a consistent minimum contrast value between Swatches. + * This is useful for UI as it means the perception of the difference between colors the same distance + * apart in the Palette will be consistent whether the colors are light yellow or dark red. + * It generates its curve using the LAB color space and maintains the saturation of the source color throughout. + * + * @public + */ +export class PaletteRGB extends BasePalette { + /** + * Adjust one end of the contrast-based palette so it doesn't abruptly fall to black (or white). + * + * @param swatchContrast - Function to get the target contrast for the next swatch + * @param referencePalette - The high resolution palette + * @param targetPalette - The contrast-based palette to adjust + * @param direction - The end to adjust + */ + private static adjustEnd( + swatchContrast: (swatch: SwatchRGB) => number, + referencePalette: HighResolutionPaletteRGB, + targetPalette: SwatchRGB[], + direction: 1 | -1 + ) { + // Careful with the use of referencePalette as only the refSwatches is reversed. + const refSwatches = + direction === -1 + ? referencePalette.swatches + : referencePalette.reversedSwatches; + const refIndex = (swatch: SwatchRGB) => { + const index = referencePalette.closestIndexOf(swatch); + return direction === 1 ? referencePalette.lastIndex - index : index; + }; + + // Only operates on the 'end' end of the array, so flip if we're adjusting the 'beginning' + if (direction === 1) { + targetPalette.reverse(); + } + + const targetContrast = swatchContrast(targetPalette[targetPalette.length - 2]); + const actualContrast = roundToPrecisionSmall( + contrast( + targetPalette[targetPalette.length - 1], + targetPalette[targetPalette.length - 2] + ), + 2 + ); + if (actualContrast < targetContrast) { + // Remove last swatch if not sufficient contrast + targetPalette.pop(); + + // Distribute to the last swatch + const safeSecondSwatch = referencePalette.colorContrast( + refSwatches[referencePalette.lastIndex], + targetContrast, + undefined, + direction + ); + const safeSecondRefIndex = refIndex(safeSecondSwatch); + const targetSwatchCurrentRefIndex = refIndex( + targetPalette[targetPalette.length - 2] + ); + const swatchesToSpace = safeSecondRefIndex - targetSwatchCurrentRefIndex; + let space = 1; + for ( + let i = targetPalette.length - swatchesToSpace - 1; + i < targetPalette.length; + i++ + ) { + const currentRefIndex = refIndex(targetPalette[i]); + const nextRefIndex = + i === targetPalette.length - 1 + ? referencePalette.lastIndex + : currentRefIndex + space; + targetPalette[i] = refSwatches[nextRefIndex]; + space++; + } + } + + if (direction === 1) { + targetPalette.reverse(); + } + } + + /** + * Generate a Palette with consistent minimum contrast between Swatches. + * + * @param source - The source color + * @param options - Palette generation options + * @returns A Palette meeting the requested contrast between Swatches. + */ + private static createColorPaletteByContrast( + source: SwatchRGB, + options: PaletteRGBOptions + ): SwatchRGB[] { + const referencePalette = HighResolutionPaletteRGB.from(source); + + // Ramp function to increase contrast as the swatches get darker + const nextContrast = (swatch: SwatchRGB) => { + const c = + options.stepContrast + + options.stepContrast * + (1 - swatch.relativeLuminance) * + options.stepContrastRamp; + return roundToPrecisionSmall(c, 2); + }; + + const swatches: SwatchRGB[] = []; + + // Start with the source color or the light end color + let ref = options.preserveSource ? source : referencePalette.swatches[0]; + swatches.push(ref); + + // Add swatches with contrast toward dark + do { + const targetContrast = nextContrast(ref); + ref = referencePalette.colorContrast(ref, targetContrast, undefined, 1); + swatches.push(ref); + } while (ref.relativeLuminance > 0); + + // Add swatches with contrast toward light + if (options.preserveSource) { + ref = source; + do { + // This is off from the dark direction because `ref` here is the darker swatch, probably subtle + const targetContrast = nextContrast(ref); + ref = referencePalette.colorContrast(ref, targetContrast, undefined, -1); + swatches.unshift(ref); + } while (ref.relativeLuminance < 1); + } + + // Validate dark end + this.adjustEnd(nextContrast, referencePalette, swatches, -1); + + // Validate light end + if (options.preserveSource) { + this.adjustEnd(nextContrast, referencePalette, swatches, 1); + } + + return swatches; + } + + /** + * Create a color Palette from a provided Swatch. + * + * @param source - The source Swatch to create a Palette from + * @returns The Palette + */ + static from(source: SwatchRGB, options?: Partial): PaletteRGB { + const opts = + options === void 0 || null + ? defaultPaletteRGBOptions + : { ...defaultPaletteRGBOptions, ...options }; + + return new PaletteRGB( + source, + Object.freeze(PaletteRGB.createColorPaletteByContrast(source, opts)) + ); + } +} diff --git a/packages/utilities/adaptive-ui/src/color/palette.ts b/packages/utilities/adaptive-ui/src/color/palette.ts new file mode 100644 index 00000000000..1b2cc7e1392 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/palette.ts @@ -0,0 +1,231 @@ +import { clamp } from "@microsoft/fast-colors"; +import { Swatch } from "./swatch.js"; +import { binarySearch } from "./utilities/binary-search.js"; +import { directionByIsDark } from "./utilities/direction-by-is-dark.js"; +import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js"; + +/** + * Directional values for navigating {@link Swatch}es in {@link Palette}. + * + * @public + */ +export enum PaletteDirectionValue { + /** + * Move darker, or up the Palette. + */ + darker = 1, + + /** + * Move lighter, or down the Palette. + */ + lighter = -1, +} + +// I know we like to avoid enums so I tried to make it an object, but I'm not sure how to make this work with the type and function below. +// export const PaletteDirectionValue = { +// darker: 1, +// lighter: -1 +// } as const; + +/** + * Convenience type to allow a fixed {@link PaletteDirectionValue} or a function that resolves to one. + * + * @public + */ +export type PaletteDirection = PaletteDirectionValue | (() => PaletteDirectionValue); + +/** + * Gets a fixed {@link PaletteDirectionValue} from {@link PaletteDirection} which may be a function that needs to be resolved. + * + * @param direction - A fixed palette direction value or a function that resolves to one + * @returns A fixed palette direction value + * + * @public + */ +export function resolvePaletteDirection( + direction: PaletteDirection +): PaletteDirectionValue { + if (typeof direction === "function") { + return direction(); + } else { + return direction; + } +} + +/** + * A collection of {@link Swatch}es that form a luminance gradient from light (index 0) to dark. + * + * @public + */ +export interface Palette { + /** + * The Swatch used to create the full palette. + */ + readonly source: T; + + /** + * The array of all Swatches from light to dark. + */ + readonly swatches: ReadonlyArray; + + /** + * Returns a Swatch from the Palette that most closely meets + * the `minContrast` ratio for to the `reference`. + * + * @param reference - The relative luminance of the reference + * @param minContrast - The minimum amount of contrast from the `reference` + * @param initialIndex - Optional starting point for the search + * @param direction - Optional control for the direction of the search + * @returns The Swatch that meets the provided contrast + */ + colorContrast( + reference: RelativeLuminance, + minContrast: number, + initialIndex?: number, + direction?: PaletteDirection + ): T; + + /** + * Returns a Swatch from the Palette that's the specified position and direction away from the `reference`. + * + * @param reference - The relative luminance of the reference + * @param delta - The number of Swatches away from `reference` + * @param direction - The direction to go from `reference`, 1 goes darker, -1 goes lighter + */ + delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T; + + /** + * Returns the index of the Palette that most closely matches + * the provided relative luminance. + * + * @param reference - The relative luminance of the reference + * @returns The index + */ + closestIndexOf(reference: RelativeLuminance): number; + + /** + * Gets a Swatch by index. Index is clamped to the limits + * of the Palette so a Swatch will always be returned. + * + * @param index - The index + * @returns The Swatch + */ + get(index: number): T; +} + +/** + * A base {@link Palette} with a common implementation of the interface. Use PaletteRGB for an implementation + * of a palette generation algorithm that is ready to be used directly, or extend this class to generate custom Swatches. + * + * @public + */ +export class BasePalette implements Palette { + /** + * {@inheritdoc Palette.source} + */ + readonly source: T; + /** + * {@inheritdoc Palette.swatches} + */ + readonly swatches: ReadonlyArray; + + /** + * An index pointer to the end of the palette. + */ + readonly lastIndex: number; + + /** + * A copy of the `Swatch`es in reverse order, used for optimized searching. + */ + readonly reversedSwatches: ReadonlyArray; + + /** + * Cache from `relativeLuminance` to `Swatch` index in the `Palette`. + */ + readonly closestIndexCache = new Map(); + + /** + * Creates a new Palette. + * + * @param source - The source color for the Palette + * @param swatches - All Swatches in the Palette + */ + constructor(source: T, swatches: ReadonlyArray) { + this.source = source; + this.swatches = swatches; + + this.reversedSwatches = Object.freeze([...this.swatches].reverse()); + this.lastIndex = this.swatches.length - 1; + } + + /** + * {@inheritdoc Palette.colorContrast} + */ + colorContrast( + reference: RelativeLuminance, + contrastTarget: number, + initialSearchIndex?: number, + direction: PaletteDirection = directionByIsDark(reference) + ): T { + if (initialSearchIndex === undefined) { + initialSearchIndex = this.closestIndexOf(reference); + } + + let source: ReadonlyArray = this.swatches; + const endSearchIndex = this.lastIndex; + let startSearchIndex = initialSearchIndex; + + const condition = (value: T) => contrast(reference, value) >= contrastTarget; + + if (direction === PaletteDirectionValue.lighter) { + source = this.reversedSwatches; + startSearchIndex = endSearchIndex - startSearchIndex; + } + + return binarySearch(source, condition, startSearchIndex, endSearchIndex); + } + + /** + * {@inheritdoc Palette.delta} + */ + delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T { + const dir = resolvePaletteDirection(direction); + return this.get(this.closestIndexOf(reference) + dir * delta); + } + + /** + * {@inheritdoc Palette.closestIndexOf} + */ + closestIndexOf(reference: RelativeLuminance): number { + if (this.closestIndexCache.has(reference.relativeLuminance)) { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return this.closestIndexCache.get(reference.relativeLuminance)!; + } + + let index = this.swatches.indexOf(reference as T); + + if (index !== -1) { + this.closestIndexCache.set(reference.relativeLuminance, index); + return index; + } + + const closest = this.swatches.reduce((previous, next) => + Math.abs(next.relativeLuminance - reference.relativeLuminance) < + Math.abs(previous.relativeLuminance - reference.relativeLuminance) + ? next + : previous + ); + + index = this.swatches.indexOf(closest); + this.closestIndexCache.set(reference.relativeLuminance, index); + + return index; + } + + /** + * {@inheritdoc Palette.get} + */ + get(index: number): T { + return this.swatches[index] || this.swatches[clamp(index, 0, this.lastIndex)]; + } +} diff --git a/packages/utilities/adaptive-ui/src/color/recipe.ts b/packages/utilities/adaptive-ui/src/color/recipe.ts new file mode 100644 index 00000000000..9c7b88dbd1d --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipe.ts @@ -0,0 +1,58 @@ +import { Swatch } from "./swatch.js"; + +/** + * A recipe that evaluates a single color value. + * + * @public + */ +export interface ColorRecipe { + /** + * Evaluate a single color value. + * + * @param element - The element for which to evaluate the color recipe + * @param reference - The reference color, implementation defaults to `fillColor`, but sometimes overridden for nested color recipes + */ + evaluate(element: HTMLElement, reference?: Swatch): Swatch; +} + +/** + * A recipe that evaluates a color value for rest, hover, active, and focus states. + * + * @public + */ +export interface InteractiveColorRecipe { + /** + * Evaluate an interactive color set. + * + * @param element - The element for which to evaluate the color recipe + * @param reference - The reference color, implementation defaults to `fillColor`, but sometimes overridden for nested color recipes + */ + evaluate(element: HTMLElement, reference?: Swatch): InteractiveSwatchSet; +} + +/** + * A set of {@link Swatch}es to use for an interactive control's states. + * + * @public + */ +export interface InteractiveSwatchSet { + /** + * The Swatch to apply to the rest state + */ + rest: Swatch; + + /** + * The Swatch to apply to the hover state + */ + hover: Swatch; + + /** + * The Swatch to apply to the active state + */ + active: Swatch; + + /** + * The Swatch to apply to the focus state + */ + focus: Swatch; +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast-set.ts b/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast-set.ts new file mode 100644 index 00000000000..fb675fc7d2f --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast-set.ts @@ -0,0 +1,49 @@ +import { Swatch } from "../swatch.js"; +import { blackOrWhiteByContrast } from "./black-or-white-by-contrast.js"; + +/** + * Gets an interactive set of black or white Swatches based on the reference color for each state and minimum contrast. + * + * This is commonly used for something like foreground color on an accent-filled Button. + * + * This algorithm ignores the contrast check for active state if rest and hover produce the same color, and uses that + * color directly. This is because many times something like a Button has slight luminance variation between rest, hover, + * and active states, but generally flipping the text color produces a flash and is more unexpected. + * + * @param restReference - The rest state reference color + * @param hoverReference - The hover state reference color + * @param activeReference - The active state reference color + * @param focusReference - The focus state reference color + * @param minContrast - The minimum contrast required for black or white from each reference color + * @param defaultBlack - True to default to black if both black or white meet contrast + * @returns The interactive set of black or white Swatches. + * + * @public + */ +export function blackOrWhiteByContrastSet( + restReference: Swatch, + hoverReference: Swatch, + activeReference: Swatch, + focusReference: Swatch, + minContrast: number, + defaultBlack: boolean +) { + const defaultRule: (reference: Swatch) => Swatch = reference => + blackOrWhiteByContrast(reference, minContrast, defaultBlack); + + const restForeground = defaultRule(restReference); + const hoverForeground = defaultRule(hoverReference); + // Active doe not have contrast requirements, so if rest and hover use the same color, use that for active even if it would not have passed the contrast check. + const activeForeground = + restForeground.relativeLuminance === hoverForeground.relativeLuminance + ? restForeground + : defaultRule(activeReference); + const focusForeground = defaultRule(focusReference); + + return { + rest: restForeground, + hover: hoverForeground, + active: activeForeground, + focus: focusForeground, + }; +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.spec.ts b/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.spec.ts new file mode 100644 index 00000000000..b08f80465f6 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.spec.ts @@ -0,0 +1,21 @@ +import chai from "chai"; +import { SwatchRGB } from "../swatch.js"; +import { _black, _white } from "../utilities/color-constants.js"; +import { blackOrWhiteByContrast } from "./black-or-white-by-contrast.js"; + +const { expect } = chai; + +describe("blackOrWhiteByContrast", (): void => { + it("should return black when background does not meet contrast ratio", (): void => { + const small = blackOrWhiteByContrast(_white, 4.5, false) as SwatchRGB; + const large = blackOrWhiteByContrast(_white, 3, false) as SwatchRGB; + + expect(small.r).to.equal(_black.r); + expect(small.g).to.equal(_black.g); + expect(small.b).to.equal(_black.b); + + expect(large.r).to.equal(_black.r); + expect(large.g).to.equal(_black.g); + expect(large.b).to.equal(_black.b); + }); +}); diff --git a/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.ts b/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.ts new file mode 100644 index 00000000000..b652501ef3f --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/black-or-white-by-contrast.ts @@ -0,0 +1,22 @@ +import { Swatch } from "../swatch.js"; +import { _black, _white } from "../utilities/color-constants.js"; + +/** + * Gets a black or white Swatch based on the reference color and minimum contrast. + * + * @param reference - The reference color + * @param minContrast - The minimum contrast required for black or white from `reference` + * @param defaultBlack - True to default to black if both black and white meet contrast + * @returns A black or white Swatch + * + * @public + */ +export function blackOrWhiteByContrast( + reference: Swatch, + minContrast: number, + defaultBlack: boolean +): Swatch { + const defaultColor = defaultBlack ? _black : _white; + const otherColor = defaultBlack ? _white : _black; + return reference.contrast(defaultColor) >= minContrast ? defaultColor : otherColor; +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/contrast-and-delta-swatch-set.ts b/packages/utilities/adaptive-ui/src/color/recipes/contrast-and-delta-swatch-set.ts new file mode 100644 index 00000000000..4facac39a60 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/contrast-and-delta-swatch-set.ts @@ -0,0 +1,71 @@ +import { + Palette, + PaletteDirection, + PaletteDirectionValue, + resolvePaletteDirection, +} from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * Gets an interactive set of {@link Swatch}es using contrast from the reference color, then deltas for each state. + * + * Since this is based on contrast it tries to do the right thing for accessibility. Ideally the `restDelta` + * and `hoverDelta` should be greater than or equal to zero, because that will ensure those colors meet or + * exceed the `minContrast`. + * + * This algorithm will maintain the difference between the rest and hover deltas, but may slide them on the Palette + * to maintain accessibility. + * + * @param palette - The Palette used to find the Swatches + * @param reference - The reference color + * @param minContrast - The desired minimum contrast from `reference`, which determines the base color + * @param restDelta - The rest state offset from the base color + * @param hoverDelta - The hover state offset from the base color + * @param activeDelta - The active state offset from the base color + * @param focusDelta - The focus state offset from the base color + * @param direction - The direction the deltas move on the `palette`, defaults to {@link directionByIsDark} based on `reference` + * @returns The interactive set of Swatches + * + * @public + */ +export function contrastAndDeltaSwatchSet( + palette: Palette, + reference: Swatch, + minContrast: number, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + direction: PaletteDirection = directionByIsDark(reference) +): InteractiveSwatchSet { + const dir = resolvePaletteDirection(direction); + + const accessibleSwatch = palette.colorContrast(reference, minContrast); + + const accessibleIndex1 = palette.closestIndexOf(accessibleSwatch); + const accessibleIndex2 = accessibleIndex1 + dir * Math.abs(restDelta - hoverDelta); + const indexOneIsRestState = + dir === PaletteDirectionValue.darker + ? restDelta < hoverDelta + : dir * restDelta > dir * hoverDelta; + + let restIndex: number; + let hoverIndex: number; + + if (indexOneIsRestState) { + restIndex = accessibleIndex1; + hoverIndex = accessibleIndex2; + } else { + restIndex = accessibleIndex2; + hoverIndex = accessibleIndex1; + } + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(restIndex + dir * activeDelta), + focus: palette.get(restIndex + dir * focusDelta), + }; +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.spec.ts b/packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.spec.ts new file mode 100644 index 00000000000..69bb31e48a8 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.spec.ts @@ -0,0 +1,38 @@ +import chai from "chai"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { PaletteRGB } from "../palette-rgb.js"; +import { SwatchRGB } from "../swatch.js"; +import { contrastSwatch } from "./contrast-swatch.js"; + +const { expect } = chai; + +const neutralBase = SwatchRGB.from(parseColorHexRGB("#808080")!); +const accentBase = SwatchRGB.from(parseColorHexRGB("#80DEEA")!); + +describe("contrastSwatch", (): void => { + const neutralPalette = PaletteRGB.from(neutralBase); + const accentPalette = PaletteRGB.from(accentBase); + + neutralPalette.swatches.concat(accentPalette.swatches).forEach((swatch): void => { + it(`${swatch.toColorString()} should resolve a color from the neutral palette`, (): void => { + expect( + neutralPalette.swatches.indexOf( + contrastSwatch(neutralPalette, swatch, 4.5) as SwatchRGB + ) + ).not.to.equal(-1); + }); + }); + + neutralPalette.swatches.concat(accentPalette.swatches).forEach((swatch): void => { + it(`${swatch.toColorString()} should always be at least 4.5 : 1 against the background`, (): void => { + expect( + swatch.contrast(contrastSwatch(neutralPalette, swatch, 4.5)) + // Because contrastSwatch follows the direction patterns of neutralForeground, + // a backgroundColor #777777 is impossible to hit 4.5 against. + ).to.be.gte(swatch.toColorString().toUpperCase() === "#777777" ? 4.48 : 4.5); + expect( + swatch.contrast(contrastSwatch(neutralPalette, swatch, 4.5)) + ).to.be.lessThan(5); + }); + }); +}); diff --git a/packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.ts b/packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.ts new file mode 100644 index 00000000000..12f2880b2e2 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/contrast-swatch.ts @@ -0,0 +1,22 @@ +import { Palette, PaletteDirection } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * Gets a Swatch meeting the minimum contrast from the reference color. + * + * @param palette - The Palette used to find the Swatch + * @param reference - The reference color + * @param minContrast - The desired minimum contrast + * @returns The Swatch + * + * @public + */ +export function contrastSwatch( + palette: Palette, + reference: Swatch, + minContrast: number, + direction: PaletteDirection = directionByIsDark(reference) +): Swatch { + return palette.colorContrast(reference, minContrast, undefined, direction); +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/delta-swatch-set.ts b/packages/utilities/adaptive-ui/src/color/recipes/delta-swatch-set.ts new file mode 100644 index 00000000000..c3510268765 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/delta-swatch-set.ts @@ -0,0 +1,38 @@ +import { Palette, PaletteDirection, resolvePaletteDirection } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * Gets an interactive set of Swatches the specified positions away from the reference color. + * + * @param palette - The Palette used to find the Swatches + * @param reference - The reference color + * @param restDelta - The rest state offset from `reference` + * @param hoverDelta - The hover state offset from `reference` + * @param activeDelta - The active state offset from `reference` + * @param focusDelta - The focus state offset from `reference` + * @param direction - The direction the deltas move on the `palette`, defaults to {@link directionByIsDark} based on `reference` + * @returns The interactive set of Swatches + * + * @public + */ +export function deltaSwatchSet( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + direction: PaletteDirection = directionByIsDark(reference) +): InteractiveSwatchSet { + const referenceIndex = palette.closestIndexOf(reference); + const dir = resolvePaletteDirection(direction); + + return { + rest: palette.get(referenceIndex + dir * restDelta), + hover: palette.get(referenceIndex + dir * hoverDelta), + active: palette.get(referenceIndex + dir * activeDelta), + focus: palette.get(referenceIndex + dir * focusDelta), + }; +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/delta-swatch.ts b/packages/utilities/adaptive-ui/src/color/recipes/delta-swatch.ts new file mode 100644 index 00000000000..b9c5148c04a --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/delta-swatch.ts @@ -0,0 +1,23 @@ +import { Swatch } from "../swatch.js"; +import { Palette, PaletteDirection } from "../palette.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * Color algorithm to get the Swatch a specified position away from the reference color. + * + * @param palette - The Palette used to find the Swatch + * @param reference - The reference color + * @param delta - The offset from the `reference` + * @param direction - The direction the delta moves on the `palette`, defaults to {@link directionByIsDark} based on `reference` + * @returns The Swatch + * + * @public + */ +export function deltaSwatch( + palette: Palette, + reference: Swatch, + delta: number, + direction: PaletteDirection = directionByIsDark(reference) +): Swatch { + return palette.delta(reference, delta, direction); +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.spec.ts b/packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.spec.ts new file mode 100644 index 00000000000..a79f601279f --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.spec.ts @@ -0,0 +1,96 @@ +import chai from "chai"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { PaletteRGB } from "../palette-rgb.js"; +import { SwatchRGB } from "../swatch.js"; +import { _black, _white } from "../utilities/color-constants.js"; +import { idealColorDeltaSwatchSet } from "./ideal-color-delta-swatch-set.js"; + +const { expect } = chai; + +const neutralBase = SwatchRGB.from(parseColorHexRGB("#808080")!); +const accentBase = SwatchRGB.from(parseColorHexRGB("#80DEEA")!); + +describe("idealColorDeltaSwatchSet", (): void => { + const neutralPalette = PaletteRGB.from(neutralBase); + const accentPalette = PaletteRGB.from(accentBase); + + it("should increase contrast on hover state and decrease contrast on active state in either mode", (): void => { + const lightModeColors = idealColorDeltaSwatchSet( + accentPalette, + _white, + 4.5, + accentPalette.source, + 0, + 6, + -4, + 0 + ); + const darkModeColors = idealColorDeltaSwatchSet( + accentPalette, + _black, + 4.5, + accentPalette.source, + 0, + 6, + -4, + 0 + ); + + expect(lightModeColors.hover.contrast(_white)).to.be.greaterThan( + lightModeColors.rest.contrast(_white) + ); + expect(darkModeColors.hover.contrast(_black)).to.be.greaterThan( + darkModeColors.rest.contrast(_black) + ); + }); + + it("should have accessible rest and hover colors against the background color", (): void => { + const accentColors = [ + SwatchRGB.from(parseColorHexRGB("#0078D4")!), + SwatchRGB.from(parseColorHexRGB("#107C10")!), + SwatchRGB.from(parseColorHexRGB("#5C2D91")!), + SwatchRGB.from(parseColorHexRGB("#D83B01")!), + SwatchRGB.from(parseColorHexRGB("#F2C812")!), + ]; + + accentColors.forEach( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + (accent): void => { + const accentPalette = PaletteRGB.from(accent); + + neutralPalette.swatches.forEach((swatch): void => { + const smallColors = idealColorDeltaSwatchSet( + accentPalette, + swatch, + 4.5, + accentPalette.source, + 0, + 6, + -4, + 0 + ); + const largeColors = idealColorDeltaSwatchSet( + accentPalette, + swatch, + 3, + accentPalette.source, + 0, + 6, + -4, + 0 + ); + expect( + swatch.contrast(smallColors.rest) + // There are a few states that are impossible to meet contrast on + ).to.be.gte(4.47); + expect( + swatch.contrast(smallColors.hover) + // There are a few states that are impossible to meet contrast on + ).to.be.gte(3.7); + expect(swatch.contrast(largeColors.rest)).to.be.gte(3); + expect(swatch.contrast(largeColors.hover)).to.be.gte(3); + }); + } + ); + }); +}); diff --git a/packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.ts b/packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.ts new file mode 100644 index 00000000000..e2b84189b26 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/ideal-color-delta-swatch-set.ts @@ -0,0 +1,88 @@ +import { + Palette, + PaletteDirection, + PaletteDirectionValue, + resolvePaletteDirection, +} from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * Gets an interactive set of {@link Swatch}es using contrast from the reference color. If the ideal color meets contrast it + * is used for the base color, if not it's adjusted until contrast is met. Then deltas from that color are used for each state. + * + * This algorithm is similar to {@link contrastAndDeltaSwatchSet}, with the addition of `idealColor`. This is often preferable + * for brand colors, like for an accent-filled Button. + * + * Since this is based on contrast it tries to do the right thing for accessibility. Ideally the `restDelta` + * and `hoverDelta` should be greater than or equal to zero, because that will ensure those colors meet or + * exceed the `minContrast`. + * + * This algorithm will maintain the difference between the rest and hover deltas, but may slide them on the Palette + * to maintain accessibility. + * + * @param palette - The Palette used to find the Swatches + * @param reference - The reference color + * @param idealColor - The color to use as the base color if it meets `minContrast` from `reference` + * @param minContrast - The desired minimum contrast from `reference`, which determines the base color + * @param restDelta - The rest state offset from the base color + * @param hoverDelta - The hover state offset from the base color + * @param activeDelta - The active state offset from the base color + * @param focusDelta - The focus state offset from the base color + * @param direction - The direction the deltas move on the `palette`, defaults to {@link directionByIsDark} based on `reference` + * @returns The interactive set of Swatches + * + * @public + */ +export function idealColorDeltaSwatchSet( + palette: Palette, + reference: Swatch, + minContrast: number, + idealColor: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + direction: PaletteDirection = directionByIsDark(reference) +): InteractiveSwatchSet { + const dir = resolvePaletteDirection(direction); + + const idealIndex = palette.closestIndexOf(idealColor); + const startIndex = + idealIndex + + (dir === PaletteDirectionValue.darker + ? Math.min(restDelta, hoverDelta) + : Math.max(dir * restDelta, dir * hoverDelta)); + const accessibleSwatch = palette.colorContrast( + reference, + minContrast, + startIndex, + direction + ); + + const accessibleIndex1 = palette.closestIndexOf(accessibleSwatch); + const accessibleIndex2 = accessibleIndex1 + dir * Math.abs(restDelta - hoverDelta); + const indexOneIsRestState = + dir === PaletteDirectionValue.darker + ? restDelta < hoverDelta + : dir * restDelta > dir * hoverDelta; + + let restIndex: number; + let hoverIndex: number; + + if (indexOneIsRestState) { + restIndex = accessibleIndex1; + hoverIndex = accessibleIndex2; + } else { + restIndex = accessibleIndex2; + hoverIndex = accessibleIndex1; + } + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(restIndex + dir * activeDelta), + focus: palette.get(restIndex + dir * focusDelta), + }; +} diff --git a/packages/utilities/adaptive-ui/src/color/recipes/index.ts b/packages/utilities/adaptive-ui/src/color/recipes/index.ts new file mode 100644 index 00000000000..024e10d8f67 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/recipes/index.ts @@ -0,0 +1,6 @@ +export * from "./black-or-white-by-contrast.js"; +export * from "./contrast-and-delta-swatch-set.js"; +export * from "./contrast-swatch.js"; +export * from "./delta-swatch-set.js"; +export * from "./delta-swatch.js"; +export * from "./ideal-color-delta-swatch-set.js"; diff --git a/packages/utilities/adaptive-ui/src/color/swatch.ts b/packages/utilities/adaptive-ui/src/color/swatch.ts new file mode 100644 index 00000000000..1b112fe7246 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/swatch.ts @@ -0,0 +1,98 @@ +import { ColorRGBA64, rgbToRelativeLuminance } from "@microsoft/fast-colors"; +import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js"; + +/** + * Represents a color in a {@link Palette}. + * + * @public + */ +export interface Swatch extends RelativeLuminance { + /** + * Gets a string representation of the color. + */ + toColorString(): string; + + /** + * Gets the contrast between this Swatch and another one. + * @param target - The relative luminance for comparison + */ + contrast(target: RelativeLuminance): number; +} + +/** + * Represents a color in a {@link Palette} using RGB. + * + * @public + */ +export class SwatchRGB implements Swatch { + /** + * Red channel expressed as a number between 0 and 1. + */ + readonly r: number; + + /** + * Green channel expressed as a number between 0 and 1. + */ + readonly g: number; + + /** + * Blue channel expressed as a number between 0 and 1. + */ + readonly b: number; + + /** + * {@inheritdoc RelativeLuminance.relativeLuminance} + */ + readonly relativeLuminance: number; + + private readonly color: ColorRGBA64; + + /** + * Creates a new SwatchRGB. + * + * @param red - Red channel expressed as a number between 0 and 1 + * @param green - Green channel expressed as a number between 0 and 1 + * @param blue - Blue channel expressed as a number between 0 and 1 + */ + constructor(red: number, green: number, blue: number) { + this.r = red; + this.g = green; + this.b = blue; + this.color = new ColorRGBA64(red, blue, green); + + this.relativeLuminance = rgbToRelativeLuminance(this.color); + } + + /** + * Gets this color value as a string. + * + * @returns The color value in string format + */ + toColorString() { + return this.color.toStringHexRGB(); + } + + /** + * Gets the contrast between this Swatch and another. + * + * @returns The contrast between the two luminance values, for example, 4.54 + */ + contrast = contrast.bind(null, this); + + /** + * Gets this color value as a string for use in css. + * + * @returns The color value in a valid css string format + */ + createCSS = this.toColorString; + + /** + * Creates a new SwatchRGB from and object with R, G, and B values expressed as a number between 0 to 1. + * + * @param obj - An object with `r`, `g`, and `b` values expressed as a number between 0 and 1 + * @returns A new SwatchRGB + */ + static from(obj: { r: number; g: number; b: number }): SwatchRGB { + return new SwatchRGB(obj.r, obj.g, obj.b); + } +} diff --git a/packages/utilities/adaptive-ui/src/color/utilities/binary-search.ts b/packages/utilities/adaptive-ui/src/color/utilities/binary-search.ts new file mode 100644 index 00000000000..25f3b4ccc98 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/binary-search.ts @@ -0,0 +1,31 @@ +/** + * @internal + */ +export function binarySearch( + valuesToSearch: T[] | ReadonlyArray, + searchCondition: (value: T) => boolean, + startIndex: number = 0, + endIndex: number = valuesToSearch.length - 1 +): T { + if (endIndex === startIndex) { + return valuesToSearch[startIndex]; + } + + const middleIndex: number = Math.floor((endIndex - startIndex) / 2) + startIndex; + + // Check to see if this passes on the item in the center of the array + // if it does check the previous values + return searchCondition(valuesToSearch[middleIndex]) + ? binarySearch( + valuesToSearch, + searchCondition, + startIndex, + middleIndex // include this index because it passed the search condition + ) + : binarySearch( + valuesToSearch, + searchCondition, + middleIndex + 1, // exclude this index because it failed the search condition + endIndex + ); +} diff --git a/packages/utilities/adaptive-ui/src/color/utilities/color-constants.ts b/packages/utilities/adaptive-ui/src/color/utilities/color-constants.ts new file mode 100644 index 00000000000..78fbdb5701f --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/color-constants.ts @@ -0,0 +1,15 @@ +import { SwatchRGB } from "../swatch.js"; + +/** + * A {@link SwatchRGB} convenience for white. + * + * @internal + */ +export const _white = new SwatchRGB(1, 1, 1); + +/** + * A {@link SwatchRGB} convenience for black. + * + * @internal + */ +export const _black = new SwatchRGB(0, 0, 0); diff --git a/packages/utilities/adaptive-ui/src/color/utilities/direction-by-is-dark.ts b/packages/utilities/adaptive-ui/src/color/utilities/direction-by-is-dark.ts new file mode 100644 index 00000000000..13a7569b5dd --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/direction-by-is-dark.ts @@ -0,0 +1,15 @@ +import { PaletteDirectionValue } from "../palette.js"; +import { isDark } from "./is-dark.js"; +import { RelativeLuminance } from "./relative-luminance.js"; + +/** + * A convenience for many color recipes. Gets a directional multiplier based on whether the `color` is dark or light. + * + * @param color - The color to check + * @returns `darker` if the `color` is light, `lighter` if the `color` is dark + * + * @public + */ +export function directionByIsDark(color: RelativeLuminance): PaletteDirectionValue { + return isDark(color) ? PaletteDirectionValue.lighter : PaletteDirectionValue.darker; +} diff --git a/packages/utilities/adaptive-ui/src/color/utilities/index.ts b/packages/utilities/adaptive-ui/src/color/utilities/index.ts new file mode 100644 index 00000000000..3c45dc116dd --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/index.ts @@ -0,0 +1,5 @@ +export * from "./color-constants.js"; +export * from "./direction-by-is-dark.js"; +export * from "./is-dark.js"; +export * from "./luminance-swatch.js"; +export * from "./relative-luminance.js"; diff --git a/packages/utilities/adaptive-ui/src/color/utilities/is-dark.ts b/packages/utilities/adaptive-ui/src/color/utilities/is-dark.ts new file mode 100644 index 00000000000..ad29918a1ac --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/is-dark.ts @@ -0,0 +1,22 @@ +import { RelativeLuminance } from "./relative-luminance.js"; + +/* + * A color is "dark" if there is more contrast between #000000 and a reference + * color than #FFFFFF and the reference color. That threshold can be expressed as a relative luminance + * using the contrast formula as (1 + 0.5) / (R + 0.05) === (R + 0.05) / (0 + 0.05), + * which reduces to the following, where 'R' is the relative luminance of the reference color + */ +const target = (-0.1 + Math.sqrt(0.21)) / 2; + +/** + * Determines if a `color` is dark according to relative luminance. That is, from a contrast perspective, + * whether it has more contrast against white than black. In grey, dark is #757575 and darker. + * + * @param color - The color to check + * @returns True when the `color` is dark + * + * @public + */ +export function isDark(color: RelativeLuminance): boolean { + return color.relativeLuminance <= target; +} diff --git a/packages/utilities/adaptive-ui/src/color/utilities/luminance-swatch.ts b/packages/utilities/adaptive-ui/src/color/utilities/luminance-swatch.ts new file mode 100644 index 00000000000..70a1ec14f54 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/luminance-swatch.ts @@ -0,0 +1,13 @@ +import { Swatch, SwatchRGB } from "../swatch.js"; + +/** + * Create a grey swatch for the specified `luminance`. Note this is absolute luminance not 'relative' luminance. + * + * @param luminance - A value between 0 and 1 representing the desired luminance, for example, 0.5 for middle grey + * @returns A swatch for the specified grey value + * + * @public + */ +export function luminanceSwatch(luminance: number): Swatch { + return new SwatchRGB(luminance, luminance, luminance); +} diff --git a/packages/utilities/adaptive-ui/src/color/utilities/relative-luminance.ts b/packages/utilities/adaptive-ui/src/color/utilities/relative-luminance.ts new file mode 100644 index 00000000000..69a3e751503 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/color/utilities/relative-luminance.ts @@ -0,0 +1,29 @@ +/** + * An interface for storing the relative luminance value of a color to aid in contrast calculations. + * + * @public + */ +export interface RelativeLuminance { + /** + * A number between 0 and 1, calculated by {@link https://www.w3.org/WAI/GL/wiki/Relative_luminance} + */ + readonly relativeLuminance: number; +} + +/** + * Gets the contrast ratio between two colors expressed as relative luminance values. + * + * For example, the contrast between #FFFFFF and #767676 is 4.5:1 + * + * @param a - One color relative luminance, for example, 1 for white + * @param b - Another color relative luminance, for example, 0.18116 for #767676 + * @returns The contrast between the two luminance values, for example, 4.54 + * + * @public + */ +export function contrast(a: RelativeLuminance, b: RelativeLuminance): number { + const y1 = a.relativeLuminance > b.relativeLuminance ? a : b; + const y2 = a.relativeLuminance > b.relativeLuminance ? b : a; + + return (y1.relativeLuminance + 0.05) / (y2.relativeLuminance + 0.05); +} diff --git a/packages/utilities/adaptive-ui/src/design-tokens/appearance.ts b/packages/utilities/adaptive-ui/src/design-tokens/appearance.ts new file mode 100644 index 00000000000..daf8b37c696 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/appearance.ts @@ -0,0 +1,16 @@ +import { create } from "./create.js"; + +/** @public */ +export const designUnit = create("design-unit").withDefault(4); + +/** @public */ +export const controlCornerRadius = create("control-corner-radius").withDefault(4); + +/** @public */ +export const layerCornerRadius = create("layer-corner-radius").withDefault(8); + +/** @public */ +export const strokeWidth = create("stroke-width").withDefault(1); + +/** @public */ +export const focusStrokeWidth = create("focus-stroke-width").withDefault(2); diff --git a/packages/utilities/adaptive-ui/src/design-tokens/color.ts b/packages/utilities/adaptive-ui/src/design-tokens/color.ts new file mode 100644 index 00000000000..d40e42aff14 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/color.ts @@ -0,0 +1,930 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { blackOrWhiteByContrast } from "../color/index.js"; +import { + ColorRecipe, + InteractiveColorRecipe, + InteractiveSwatchSet, +} from "../color/recipe.js"; +import { blackOrWhiteByContrastSet } from "../color/recipes/black-or-white-by-contrast-set.js"; +import { contrastAndDeltaSwatchSet } from "../color/recipes/contrast-and-delta-swatch-set.js"; +import { contrastSwatch } from "../color/recipes/contrast-swatch.js"; +import { deltaSwatchSet } from "../color/recipes/delta-swatch-set.js"; +import { deltaSwatch } from "../color/recipes/delta-swatch.js"; +import { Swatch, SwatchRGB } from "../color/swatch.js"; +import { create, createNonCss } from "./create.js"; +import { accentPalette, neutralPalette } from "./palette.js"; + +enum ContrastTarget { + NormalText = 4.5, + LargeText = 3, +} + +/** @public */ +export const fillColor = create("fill-color").withDefault( + SwatchRGB.from(parseColorHexRGB("#FFFFFF")!) +); + +// Accent Fill + +/** @public */ +export const accentFillMinContrast = createNonCss( + "accent-fill-min-contrast" +).withDefault(5.5); + +/** @public */ +export const accentFillRestDelta = createNonCss( + "accent-fill-rest-delta" +).withDefault(0); + +/** @public */ +export const accentFillHoverDelta = createNonCss( + "accent-fill-hover-delta" +).withDefault(-2); + +/** @public */ +export const accentFillActiveDelta = createNonCss( + "accent-fill-active-delta" +).withDefault(-5); + +/** @public */ +export const accentFillFocusDelta = createNonCss( + "accent-fill-focus-delta" +).withDefault(0); + +/** @public */ +export const accentFillRecipe = createNonCss( + "accent-fill-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + contrastAndDeltaSwatchSet( + accentPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + accentFillMinContrast.getValueFor(element), + accentFillRestDelta.getValueFor(element), + accentFillHoverDelta.getValueFor(element), + accentFillActiveDelta.getValueFor(element), + accentFillFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const accentFillRest = create("accent-fill-rest").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).rest; + } +); + +/** @public */ +export const accentFillHover = create("accent-fill-hover").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).hover; + } +); + +/** @public */ +export const accentFillActive = create("accent-fill-active").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).active; + } +); + +/** @public */ +export const accentFillFocus = create("accent-fill-focus").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).focus; + } +); + +// Foreground On Accent + +/** @public */ +export const foregroundOnAccentRecipe = createNonCss( + "foreground-on-accent-recipe" +).withDefault({ + evaluate: (element: HTMLElement): InteractiveSwatchSet => + blackOrWhiteByContrastSet( + accentFillRest.getValueFor(element), + accentFillHover.getValueFor(element), + accentFillActive.getValueFor(element), + accentFillFocus.getValueFor(element), + ContrastTarget.NormalText, + false + ), +}); + +/** @public */ +export const foregroundOnAccentRest = create( + "foreground-on-accent-rest" +).withDefault( + (element: HTMLElement) => + foregroundOnAccentRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const foregroundOnAccentHover = create( + "foreground-on-accent-hover" +).withDefault( + (element: HTMLElement) => + foregroundOnAccentRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const foregroundOnAccentActive = create( + "foreground-on-accent-active" +).withDefault( + (element: HTMLElement) => + foregroundOnAccentRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const foregroundOnAccentFocus = create( + "foreground-on-accent-focus" +).withDefault( + (element: HTMLElement) => + foregroundOnAccentRecipe.getValueFor(element).evaluate(element).focus +); + +// Accent Foreground + +/** @public */ +export const accentForegroundMinContrast = createNonCss( + "accent-foreground-min-contrast" +).withDefault(0); + +/** @public */ +export const accentForegroundRestDelta = createNonCss( + "accent-foreground-rest-delta" +).withDefault(0); + +/** @public */ +export const accentForegroundHoverDelta = createNonCss( + "accent-foreground-hover-delta" +).withDefault(3); + +/** @public */ +export const accentForegroundActiveDelta = createNonCss( + "accent-foreground-active-delta" +).withDefault(-8); + +/** @public */ +export const accentForegroundFocusDelta = createNonCss( + "accent-foreground-focus-delta" +).withDefault(0); + +/** @public */ +export const accentForegroundRecipe = createNonCss( + "accent-foreground-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + contrastAndDeltaSwatchSet( + accentPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + accentForegroundMinContrast.getValueFor(element), + accentForegroundRestDelta.getValueFor(element), + accentForegroundHoverDelta.getValueFor(element), + accentForegroundActiveDelta.getValueFor(element), + accentForegroundFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const accentForegroundRest = create("accent-foreground-rest").withDefault( + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const accentForegroundHover = create( + "accent-foreground-hover" +).withDefault( + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const accentForegroundActive = create( + "accent-foreground-active" +).withDefault( + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const accentForegroundFocus = create( + "accent-foreground-focus" +).withDefault( + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Foreground + +/** @public */ +export const neutralForegroundMinContrast = createNonCss( + "neutral-foreground-min-contrast" +).withDefault(16); + +/** @public */ +export const neutralForegroundRestDelta = createNonCss( + "neutral-foreground-rest-delta" +).withDefault(0); + +/** @public */ +export const neutralForegroundHoverDelta = createNonCss( + "neutral-foreground-hover-delta" +).withDefault(-19); + +/** @public */ +export const neutralForegroundActiveDelta = createNonCss( + "neutral-foreground-active-delta" +).withDefault(-30); + +/** @public */ +export const neutralForegroundFocusDelta = createNonCss( + "neutral-foreground-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralForegroundRecipe = createNonCss( + "neutral-foreground-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + contrastAndDeltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralForegroundMinContrast.getValueFor(element), + neutralForegroundRestDelta.getValueFor(element), + neutralForegroundHoverDelta.getValueFor(element), + neutralForegroundActiveDelta.getValueFor(element), + neutralForegroundFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralForegroundRest = create( + "neutral-foreground-rest" +).withDefault( + (element: HTMLElement) => + neutralForegroundRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralForegroundHover = create( + "neutral-foreground-hover" +).withDefault( + (element: HTMLElement) => + neutralForegroundRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralForegroundActive = create( + "neutral-foreground-active" +).withDefault( + (element: HTMLElement) => + neutralForegroundRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralForegroundFocus = create( + "neutral-foreground-focus" +).withDefault( + (element: HTMLElement) => + neutralForegroundRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Foreground Hint + +/** @public */ +export const neutralForegroundHintRecipe = createNonCss( + "neutral-foreground-hint-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + contrastSwatch( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + ContrastTarget.NormalText + ), +}); + +/** @public */ +export const neutralForegroundHint = create( + "neutral-foreground-hint" +).withDefault((element: HTMLElement) => + neutralForegroundHintRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Fill + +/** @public */ +export const neutralFillRestDelta = createNonCss( + "neutral-fill-rest-delta" +).withDefault(-1); + +/** @public */ +export const neutralFillHoverDelta = createNonCss( + "neutral-fill-hover-delta" +).withDefault(1); + +/** @public */ +export const neutralFillActiveDelta = createNonCss( + "neutral-fill-active-delta" +).withDefault(0); + +/** @public */ +export const neutralFillFocusDelta = createNonCss( + "neutral-fill-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillRecipe = createNonCss( + "neutral-fill-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + deltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element), + neutralFillFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillRest = create("neutral-fill-rest").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralFillHover = create("neutral-fill-hover").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralFillActive = create("neutral-fill-active").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralFillFocus = create("neutral-fill-focus").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Input + +/** @public */ +export const neutralFillInputRestDelta = createNonCss( + "neutral-fill-input-rest-delta" +).withDefault(-1); + +/** @public */ +export const neutralFillInputHoverDelta = createNonCss( + "neutral-fill-input-hover-delta" +).withDefault(1); + +/** @public */ +export const neutralFillInputActiveDelta = createNonCss( + "neutral-fill-input-active-delta" +).withDefault(0); + +/** @public */ +export const neutralFillInputFocusDelta = createNonCss( + "neutral-fill-input-focus-delta" +).withDefault(-2); + +/** @public */ +export const neutralFillInputRecipe = createNonCss( + "neutral-fill-input-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + deltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillInputRestDelta.getValueFor(element), + neutralFillInputHoverDelta.getValueFor(element), + neutralFillInputActiveDelta.getValueFor(element), + neutralFillInputFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillInputRest = create("neutral-fill-input-rest").withDefault( + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralFillInputHover = create( + "neutral-fill-input-hover" +).withDefault( + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralFillInputActive = create( + "neutral-fill-input-active" +).withDefault( + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralFillInputFocus = create( + "neutral-fill-input-focus" +).withDefault( + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Secondary + +/** @public */ +export const neutralFillSecondaryRestDelta = createNonCss( + "neutral-fill-secondary-rest-delta" +).withDefault(3); + +/** @public */ +export const neutralFillSecondaryHoverDelta = createNonCss( + "neutral-fill-secondary-hover-delta" +).withDefault(2); + +/** @public */ +export const neutralFillSecondaryActiveDelta = createNonCss( + "neutral-fill-secondary-active-delta" +).withDefault(1); + +/** @public */ +export const neutralFillSecondaryFocusDelta = createNonCss( + "neutral-fill-secondary-focus-delta" +).withDefault(3); + +/** @public */ +export const neutralFillSecondaryRecipe = createNonCss( + "neutral-fill-secondary-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + deltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillSecondaryRestDelta.getValueFor(element), + neutralFillSecondaryHoverDelta.getValueFor(element), + neutralFillSecondaryActiveDelta.getValueFor(element), + neutralFillSecondaryFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillSecondaryRest = create( + "neutral-fill-secondary-rest" +).withDefault( + (element: HTMLElement) => + neutralFillSecondaryRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralFillSecondaryHover = create( + "neutral-fill-secondary-hover" +).withDefault( + (element: HTMLElement) => + neutralFillSecondaryRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralFillSecondaryActive = create( + "neutral-fill-secondary-active" +).withDefault( + (element: HTMLElement) => + neutralFillSecondaryRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralFillSecondaryFocus = create( + "neutral-fill-secondary-focus" +).withDefault( + (element: HTMLElement) => + neutralFillSecondaryRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Stealth + +/** @public */ +export const neutralFillStealthRestDelta = createNonCss( + "neutral-fill-stealth-rest-delta" +).withDefault(0); + +/** @public */ +export const neutralFillStealthHoverDelta = createNonCss( + "neutral-fill-stealth-hover-delta" +).withDefault(3); + +/** @public */ +export const neutralFillStealthActiveDelta = createNonCss( + "neutral-fill-stealth-active-delta" +).withDefault(2); + +/** @public */ +export const neutralFillStealthFocusDelta = createNonCss( + "neutral-fill-stealth-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillStealthRecipe = createNonCss( + "neutral-fill-stealth-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + deltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillStealthRestDelta.getValueFor(element), + neutralFillStealthHoverDelta.getValueFor(element), + neutralFillStealthActiveDelta.getValueFor(element), + neutralFillStealthFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillStealthRest = create( + "neutral-fill-stealth-rest" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralFillStealthHover = create( + "neutral-fill-stealth-hover" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralFillStealthActive = create( + "neutral-fill-stealth-active" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralFillStealthFocus = create( + "neutral-fill-stealth-focus" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Strong + +/** @public */ +export const neutralFillStrongMinContrast = createNonCss( + "neutral-fill-strong-min-contrast" +).withDefault(0); + +/** @public */ +export const neutralFillStrongRestDelta = createNonCss( + "neutral-fill-strong-rest-delta" +).withDefault(0); + +/** @public */ +export const neutralFillStrongHoverDelta = createNonCss( + "neutral-fill-strong-hover-delta" +).withDefault(8); + +/** @public */ +export const neutralFillStrongActiveDelta = createNonCss( + "neutral-fill-strong-active-delta" +).withDefault(-5); + +/** @public */ +export const neutralFillStrongFocusDelta = createNonCss( + "neutral-fill-strong-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillStrongRecipe = createNonCss( + "neutral-fill-strong-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + contrastAndDeltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillStrongMinContrast.getValueFor(element), + neutralFillStrongRestDelta.getValueFor(element), + neutralFillStrongHoverDelta.getValueFor(element), + neutralFillStrongActiveDelta.getValueFor(element), + neutralFillStrongFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillStrongRest = create( + "neutral-fill-strong-rest" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralFillStrongHover = create( + "neutral-fill-strong-hover" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralFillStrongActive = create( + "neutral-fill-strong-active" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralFillStrongFocus = create( + "neutral-fill-strong-focus" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Stroke + +/** @public */ +export const neutralStrokeRestDelta = createNonCss( + "neutral-stroke-rest-delta" +).withDefault(8); + +/** @public */ +export const neutralStrokeHoverDelta = createNonCss( + "neutral-stroke-hover-delta" +).withDefault(12); + +/** @public */ +export const neutralStrokeActiveDelta = createNonCss( + "neutral-stroke-active-delta" +).withDefault(6); + +/** @public */ +export const neutralStrokeFocusDelta = createNonCss( + "neutral-stroke-focus-delta" +).withDefault(8); + +/** @public */ +export const neutralStrokeRecipe = createNonCss( + "neutral-stroke-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => { + return deltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralStrokeRestDelta.getValueFor(element), + neutralStrokeHoverDelta.getValueFor(element), + neutralStrokeActiveDelta.getValueFor(element), + neutralStrokeFocusDelta.getValueFor(element) + ); + }, +}); + +/** @public */ +export const neutralStrokeRest = create("neutral-stroke-rest").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralStrokeHover = create("neutral-stroke-hover").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralStrokeActive = create("neutral-stroke-active").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralStrokeFocus = create("neutral-stroke-focus").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Stroke Divider + +/** @public */ +export const neutralStrokeDividerRestDelta = createNonCss( + "neutral-stroke-divider-rest-delta" +).withDefault(4); + +/** @public */ +export const neutralStrokeDividerRecipe = createNonCss( + "neutral-stroke-divider-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + deltaSwatch( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralStrokeDividerRestDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralStrokeDividerRest = create( + "neutral-stroke-divider-rest" +).withDefault(element => + neutralStrokeDividerRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Stroke Input + +/** @public */ +export const neutralStrokeInputRestDelta = createNonCss( + "neutral-stroke-input-rest-delta" +).withDefault(3); + +/** @public */ +export const neutralStrokeInputHoverDelta = createNonCss( + "neutral-stroke-input-hover-delta" +).withDefault(5); + +/** @public */ +export const neutralStrokeInputActiveDelta = createNonCss( + "neutral-stroke-input-active-delta" +).withDefault(5); + +/** @public */ +export const neutralStrokeInputFocusDelta = createNonCss( + "neutral-stroke-input-focus-delta" +).withDefault(5); + +/** @public */ +export const neutralStrokeInputRecipe = createNonCss( + "neutral-stroke-input-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => { + return deltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralStrokeInputRestDelta.getValueFor(element), + neutralStrokeInputHoverDelta.getValueFor(element), + neutralStrokeInputActiveDelta.getValueFor(element), + neutralStrokeInputFocusDelta.getValueFor(element) + ); + }, +}); + +/** @public */ +export const neutralStrokeInputRest = create( + "neutral-stroke-input-rest" +).withDefault( + (element: HTMLElement) => + neutralStrokeInputRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralStrokeInputHover = create( + "neutral-stroke-input-hover" +).withDefault( + (element: HTMLElement) => + neutralStrokeInputRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralStrokeInputActive = create( + "neutral-stroke-input-active" +).withDefault( + (element: HTMLElement) => + neutralStrokeInputRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralStrokeInputFocus = create( + "neutral-stroke-input-focus" +).withDefault( + (element: HTMLElement) => + neutralStrokeInputRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Stroke Strong + +/** @public */ +export const neutralStrokeStrongMinContrast = createNonCss( + "neutral-stroke-strong-min-contrast" +).withDefault(5.5); + +/** @public */ +export const neutralStrokeStrongRestDelta = createNonCss( + "neutral-stroke-strong-rest-delta" +).withDefault(0); + +/** @public */ +export const neutralStrokeStrongHoverDelta = createNonCss( + "neutral-stroke-strong-hover-delta" +).withDefault(0); + +/** @public */ +export const neutralStrokeStrongActiveDelta = createNonCss( + "neutral-stroke-strong-active-delta" +).withDefault(0); + +/** @public */ +export const neutralStrokeStrongFocusDelta = createNonCss( + "neutral-stroke-strong-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralStrokeStrongRecipe = createNonCss( + "neutral-stroke-strong-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + contrastAndDeltaSwatchSet( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralStrokeStrongMinContrast.getValueFor(element), + neutralStrokeStrongRestDelta.getValueFor(element), + neutralStrokeStrongHoverDelta.getValueFor(element), + neutralStrokeStrongActiveDelta.getValueFor(element), + neutralStrokeStrongFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralStrokeStrongRest = create( + "neutral-stroke-strong-rest" +).withDefault( + (element: HTMLElement) => + neutralStrokeStrongRecipe.getValueFor(element).evaluate(element).rest +); + +/** @public */ +export const neutralStrokeStrongHover = create( + "neutral-stroke-strong-hover" +).withDefault( + (element: HTMLElement) => + neutralStrokeStrongRecipe.getValueFor(element).evaluate(element).hover +); + +/** @public */ +export const neutralStrokeStrongActive = create( + "neutral-stroke-strong-active" +).withDefault( + (element: HTMLElement) => + neutralStrokeStrongRecipe.getValueFor(element).evaluate(element).active +); + +/** @public */ +export const neutralStrokeStrongFocus = create( + "neutral-stroke-strong-focus" +).withDefault( + (element: HTMLElement) => + neutralStrokeStrongRecipe.getValueFor(element).evaluate(element).focus +); + +// Focus Stroke Outer + +/** @public */ +export const focusStrokeOuterRecipe = createNonCss( + "focus-stroke-outer-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + blackOrWhiteByContrast( + fillColor.getValueFor(element), + ContrastTarget.NormalText, + true + ), +}); + +/** @public */ +export const focusStrokeOuter = create( + "focus-stroke-outer" +).withDefault((element: HTMLElement) => + focusStrokeOuterRecipe.getValueFor(element).evaluate(element) +); + +// Focus Stroke Inner + +/** @public */ +export const focusStrokeInnerRecipe = createNonCss( + "focus-stroke-inner-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + blackOrWhiteByContrast( + focusStrokeOuter.getValueFor(element), + ContrastTarget.NormalText, + false + ), +}); + +/** @public */ +export const focusStrokeInner = create( + "focus-stroke-inner" +).withDefault((element: HTMLElement) => + focusStrokeInnerRecipe.getValueFor(element).evaluate(element) +); diff --git a/packages/utilities/adaptive-ui/src/design-tokens/create.ts b/packages/utilities/adaptive-ui/src/design-tokens/create.ts new file mode 100644 index 00000000000..5aa15157a85 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/create.ts @@ -0,0 +1,7 @@ +import { DesignToken } from "@microsoft/fast-foundation"; + +export const { create } = DesignToken; + +export function createNonCss(name: string): DesignToken { + return DesignToken.create({ name, cssCustomPropertyName: null }); +} diff --git a/packages/utilities/adaptive-ui/src/design-tokens/general.ts b/packages/utilities/adaptive-ui/src/design-tokens/general.ts new file mode 100644 index 00000000000..50813564958 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/general.ts @@ -0,0 +1,5 @@ +import { Direction } from "@microsoft/fast-web-utilities"; +import { create } from "./create.js"; + +/** @public */ +export const direction = create("direction").withDefault(Direction.ltr); diff --git a/packages/utilities/adaptive-ui/src/design-tokens/index.ts b/packages/utilities/adaptive-ui/src/design-tokens/index.ts new file mode 100644 index 00000000000..8388993ee24 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/index.ts @@ -0,0 +1,5 @@ +export * from "./appearance.js"; +export * from "./color.js"; +export * from "./general.js"; +export * from "./palette.js"; +export * from "./type.js"; diff --git a/packages/utilities/adaptive-ui/src/design-tokens/palette.ts b/packages/utilities/adaptive-ui/src/design-tokens/palette.ts new file mode 100644 index 00000000000..099d4ec468f --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/palette.ts @@ -0,0 +1,40 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { Palette, Swatch, SwatchRGB } from "../color/index.js"; +import { PaletteRGB } from "../color/palette-rgb.js"; +import { create, createNonCss } from "./create.js"; + +/** @public */ +export const neutralBaseColor = create("neutral-base-color").withDefault( + "#808080" +); + +/** @public */ +export const neutralBaseSwatch = createNonCss( + "neutral-base-swatch" +).withDefault((element: HTMLElement) => + SwatchRGB.from(parseColorHexRGB(neutralBaseColor.getValueFor(element))!) +); + +/** @public */ +export const neutralPalette = createNonCss( + "neutral-palette" +).withDefault((element: HTMLElement) => + PaletteRGB.from(neutralBaseSwatch.getValueFor(element) as SwatchRGB) +); + +/** @public */ +export const accentBaseColor = create("accent-base-color").withDefault("#0078D4"); + +/** @public */ +export const accentBaseSwatch = createNonCss( + "accent-base-swatch" +).withDefault((element: HTMLElement) => + SwatchRGB.from(parseColorHexRGB(accentBaseColor.getValueFor(element))!) +); + +/** @public */ +export const accentPalette = createNonCss( + "accent-palette" +).withDefault((element: HTMLElement) => + PaletteRGB.from(accentBaseSwatch.getValueFor(element) as SwatchRGB) +); diff --git a/packages/utilities/adaptive-ui/src/design-tokens/type.ts b/packages/utilities/adaptive-ui/src/design-tokens/type.ts new file mode 100644 index 00000000000..81e570c3ac7 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/design-tokens/type.ts @@ -0,0 +1,166 @@ +import { DesignToken } from "@microsoft/fast-foundation"; +import { StandardFontWeight } from "../type/type-ramp.js"; +import { create } from "./create.js"; + +/** @public */ +export const bodyFont = create("body-font").withDefault( + '"Segoe UI Variable", "Segoe UI", sans-serif' +); + +/** @public */ +export const fontWeight = create("font-weight").withDefault( + StandardFontWeight.Normal +); + +function fontVariations( + sizeToken: DesignToken +): (element: HTMLElement) => string { + return (element: HTMLElement): string => { + const size = sizeToken.getValueFor(element); + const weight = fontWeight.getValueFor(element); + if (size.endsWith("px")) { + const px = Number.parseFloat(size.replace("px", "")); + if (px <= 12) { + return `"wght" ${weight}, "opsz" 8`; + } else if (px > 24) { + return `"wght" ${weight}, "opsz" 36`; + } + } + return `"wght" ${weight}, "opsz" 10.5`; + }; +} + +/** @public */ +export const typeRampBaseFontSize = create( + "type-ramp-base-font-size" +).withDefault("14px"); + +/** @public */ +export const typeRampBaseLineHeight = create( + "type-ramp-base-line-height" +).withDefault("20px"); + +/** @public */ +export const typeRampBaseFontVariations = create( + "type-ramp-base-font-variations" +).withDefault(fontVariations(typeRampBaseFontSize)); + +/** @public */ +export const typeRampMinus1FontSize = create( + "type-ramp-minus-1-font-size" +).withDefault("12px"); + +/** @public */ +export const typeRampMinus1LineHeight = create( + "type-ramp-minus-1-line-height" +).withDefault("16px"); + +/** @public */ +export const typeRampMinus1FontVariations = create( + "type-ramp-minus-1-font-variations" +).withDefault(fontVariations(typeRampMinus1FontSize)); + +/** @public */ +export const typeRampMinus2FontSize = create( + "type-ramp-minus-2-font-size" +).withDefault("10px"); + +/** @public */ +export const typeRampMinus2LineHeight = create( + "type-ramp-minus-2-line-height" +).withDefault("14px"); + +/** @public */ +export const typeRampMinus2FontVariations = create( + "type-ramp-minus-2-font-variations" +).withDefault(fontVariations(typeRampMinus2FontSize)); + +/** @public */ +export const typeRampPlus1FontSize = create( + "type-ramp-plus-1-font-size" +).withDefault("16px"); + +/** @public */ +export const typeRampPlus1LineHeight = create( + "type-ramp-plus-1-line-height" +).withDefault("22px"); + +/** @public */ +export const typeRampPlus1FontVariations = create( + "type-ramp-plus-1-font-variations" +).withDefault(fontVariations(typeRampPlus1FontSize)); + +/** @public */ +export const typeRampPlus2FontSize = create( + "type-ramp-plus-2-font-size" +).withDefault("20px"); + +/** @public */ +export const typeRampPlus2LineHeight = create( + "type-ramp-plus-2-line-height" +).withDefault("26px"); + +/** @public */ +export const typeRampPlus2FontVariations = create( + "type-ramp-plus-2-font-variations" +).withDefault(fontVariations(typeRampPlus2FontSize)); + +/** @public */ +export const typeRampPlus3FontSize = create( + "type-ramp-plus-3-font-size" +).withDefault("24px"); + +/** @public */ +export const typeRampPlus3LineHeight = create( + "type-ramp-plus-3-line-height" +).withDefault("32px"); + +/** @public */ +export const typeRampPlus3FontVariations = create( + "type-ramp-plus-3-font-variations" +).withDefault(fontVariations(typeRampPlus3FontSize)); + +/** @public */ +export const typeRampPlus4FontSize = create( + "type-ramp-plus-4-font-size" +).withDefault("28px"); + +/** @public */ +export const typeRampPlus4LineHeight = create( + "type-ramp-plus-4-line-height" +).withDefault("36px"); + +/** @public */ +export const typeRampPlus4FontVariations = create( + "type-ramp-plus-4-font-variations" +).withDefault(fontVariations(typeRampPlus4FontSize)); + +/** @public */ +export const typeRampPlus5FontSize = create( + "type-ramp-plus-5-font-size" +).withDefault("32px"); + +/** @public */ +export const typeRampPlus5LineHeight = create( + "type-ramp-plus-5-line-height" +).withDefault("40px"); + +/** @public */ +export const typeRampPlus5FontVariations = create( + "type-ramp-plus-5-font-variations" +).withDefault(fontVariations(typeRampPlus5FontSize)); + +/** @public */ +export const typeRampPlus6FontSize = create( + "type-ramp-plus-6-font-size" +).withDefault("40px"); + +/** @public */ +export const typeRampPlus6LineHeight = create( + "type-ramp-plus-6-line-height" +).withDefault("52px"); + +/** @public */ +export const typeRampPlus6FontVariations = create( + "type-ramp-plus-6-font-variations" +).withDefault(fontVariations(typeRampPlus6FontSize)); diff --git a/packages/utilities/adaptive-ui/src/index.ts b/packages/utilities/adaptive-ui/src/index.ts new file mode 100644 index 00000000000..8a0068d0685 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/index.ts @@ -0,0 +1,3 @@ +export * from "./color/index.js"; +export * from "./design-tokens/index.js"; +export * from "./type/index.js"; diff --git a/packages/utilities/adaptive-ui/src/type/index.ts b/packages/utilities/adaptive-ui/src/type/index.ts new file mode 100644 index 00000000000..52f257e0aea --- /dev/null +++ b/packages/utilities/adaptive-ui/src/type/index.ts @@ -0,0 +1 @@ +export * from "./type-ramp.js"; diff --git a/packages/utilities/adaptive-ui/src/type/type-ramp.ts b/packages/utilities/adaptive-ui/src/type/type-ramp.ts new file mode 100644 index 00000000000..c2066d7bc02 --- /dev/null +++ b/packages/utilities/adaptive-ui/src/type/type-ramp.ts @@ -0,0 +1,129 @@ +import { cssPartial } from "@microsoft/fast-element"; +import { + bodyFont, + typeRampBaseFontSize, + typeRampBaseFontVariations, + typeRampBaseLineHeight, + typeRampMinus1FontSize, + typeRampMinus1FontVariations, + typeRampMinus1LineHeight, + typeRampMinus2FontSize, + typeRampMinus2FontVariations, + typeRampMinus2LineHeight, + typeRampPlus1FontSize, + typeRampPlus1FontVariations, + typeRampPlus1LineHeight, + typeRampPlus2FontSize, + typeRampPlus2FontVariations, + typeRampPlus2LineHeight, + typeRampPlus3FontSize, + typeRampPlus3FontVariations, + typeRampPlus3LineHeight, + typeRampPlus4FontSize, + typeRampPlus4FontVariations, + typeRampPlus4LineHeight, + typeRampPlus5FontSize, + typeRampPlus5FontVariations, + typeRampPlus5LineHeight, + typeRampPlus6FontSize, + typeRampPlus6FontVariations, + typeRampPlus6LineHeight, +} from "../design-tokens/type.js"; + +/** + * Standard font wights. + * + * @public + */ +export const StandardFontWeight = { + Thin: 100, + ExtraLight: 200, + Light: 300, + Normal: 400, + Medium: 500, + SemiBold: 600, + Bold: 700, + ExtraBold: 800, + Black: 900, +} as const; + +/** @public */ +export const typeRampBase = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampBaseFontVariations}; +`; + +/** @public */ +export const typeRampMinus1 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampMinus1FontSize}; + line-height: ${typeRampMinus1LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampMinus1FontVariations}; +`; + +/** @public */ +export const typeRampMinus2 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampMinus2FontSize}; + line-height: ${typeRampMinus2LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampMinus2FontVariations}; +`; + +/** @public */ +export const typeRampPlus1 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampPlus1FontSize}; + line-height: ${typeRampPlus1LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampPlus1FontVariations}; +`; + +/** @public */ +export const typeRampPlus2 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampPlus2FontSize}; + line-height: ${typeRampPlus2LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampPlus2FontVariations}; +`; + +/** @public */ +export const typeRampPlus3 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampPlus3FontSize}; + line-height: ${typeRampPlus3LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampPlus3FontVariations}; +`; + +/** @public */ +export const typeRampPlus4 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampPlus4FontSize}; + line-height: ${typeRampPlus4LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampPlus4FontVariations}; +`; + +/** @public */ +export const typeRampPlus5 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampPlus5FontSize}; + line-height: ${typeRampPlus5LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampPlus5FontVariations}; +`; + +/** @public */ +export const typeRampPlus6 = cssPartial` + font-family: ${bodyFont}; + font-size: ${typeRampPlus6FontSize}; + line-height: ${typeRampPlus6LineHeight}; + font-weight: initial; + font-variation-settings: ${typeRampPlus6FontVariations}; +`; diff --git a/packages/utilities/adaptive-ui/tsconfig.build.json b/packages/utilities/adaptive-ui/tsconfig.build.json new file mode 100644 index 00000000000..4e340029230 --- /dev/null +++ b/packages/utilities/adaptive-ui/tsconfig.build.json @@ -0,0 +1,11 @@ + +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationDir": "dist/dts", + }, + "exclude": [ + "**/*.spec.ts", + "**/__test__", + ] +} diff --git a/packages/utilities/adaptive-ui/tsconfig.json b/packages/utilities/adaptive-ui/tsconfig.json new file mode 100644 index 00000000000..d5bbaa6f41d --- /dev/null +++ b/packages/utilities/adaptive-ui/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/esm", + "types": ["mocha"] + }, + "include": ["src"] +} diff --git a/packages/utilities/adaptive-ui/tsconfig.test.json b/packages/utilities/adaptive-ui/tsconfig.test.json new file mode 100644 index 00000000000..440bd4efe05 --- /dev/null +++ b/packages/utilities/adaptive-ui/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": "./src", + "outDir": "test", + "declaration": false, + } +} From a35c77ec9961d9dd3fd83b7f8a7efdc9a23797b1 Mon Sep 17 00:00:00 2001 From: Brian Heston <47367562+bheston@users.noreply.github.com> Date: Tue, 17 May 2022 11:35:15 -0700 Subject: [PATCH 2/4] Change files --- ...t-adaptive-ui-63900c6a-7460-40f4-bb44-42cedac1ac3c.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@microsoft-adaptive-ui-63900c6a-7460-40f4-bb44-42cedac1ac3c.json diff --git a/change/@microsoft-adaptive-ui-63900c6a-7460-40f4-bb44-42cedac1ac3c.json b/change/@microsoft-adaptive-ui-63900c6a-7460-40f4-bb44-42cedac1ac3c.json new file mode 100644 index 00000000000..9e8c5334ffd --- /dev/null +++ b/change/@microsoft-adaptive-ui-63900c6a-7460-40f4-bb44-42cedac1ac3c.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add adaptive-ui package", + "packageName": "@microsoft/adaptive-ui", + "email": "47367562+bheston@users.noreply.github.com", + "dependentChangeType": "patch" +} From 77618691516cbbb488b4e076b50ce4fa70f78dac Mon Sep 17 00:00:00 2001 From: Brian Heston <47367562+bheston@users.noreply.github.com> Date: Mon, 23 May 2022 14:47:03 -0700 Subject: [PATCH 3/4] Review comments --- packages/utilities/adaptive-ui/README.md | 14 ++++++++++++-- packages/utilities/adaptive-ui/docs/api-report.md | 11 +++++++---- .../utilities/adaptive-ui/src/color/palette.ts | 15 +++++++++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/utilities/adaptive-ui/README.md b/packages/utilities/adaptive-ui/README.md index 206b9d90661..e072f96ff86 100644 --- a/packages/utilities/adaptive-ui/README.md +++ b/packages/utilities/adaptive-ui/README.md @@ -1,6 +1,6 @@ # Adaptive UI -Adaptive UI is a library for building highly-consistent design systems based around your visual decisions instead of a complex map of color swatches and tokens based on t-shirt sizes. +Adaptive UI is a library for building highly-consistent design systems based around your visual decisions. This represents an evolution of many token implementations that provide end-result values without the means to track how they were derived or easily adjust them. This is a core feature of [FAST](https://fast.design) and is incorporated into the [Fluent UI Web Components](https://aka.ms/fluentwebcomponents) and other design systems. @@ -44,6 +44,16 @@ To switch to dark mode: fillColor.withDefault("#232323"); ``` +See more about the [adaptive color system](./src/color/README.md). + +## Road map + The layer system for setting content area background colors and improved handling of light and dark mode is evolving and will be added soon. -See more about the [adaptive color system](./src/color/README.md). +The latest version of adaptive density, currently in RFC and PR, will come here soon as well. + +The color system is being updated to support opacity in colors, which will enable at least the neutral palette to overlay images or background blur effects like in Windows 11. + +### Further afield + +The fixed design tokens will evolve into a more configurable definition aligning with industry standards in design tokens. diff --git a/packages/utilities/adaptive-ui/docs/api-report.md b/packages/utilities/adaptive-ui/docs/api-report.md index 63d502febf7..ee0b3795961 100644 --- a/packages/utilities/adaptive-ui/docs/api-report.md +++ b/packages/utilities/adaptive-ui/docs/api-report.md @@ -484,10 +484,13 @@ export interface Palette { export type PaletteDirection = PaletteDirectionValue | (() => PaletteDirectionValue); // @public -export enum PaletteDirectionValue { - darker = 1, - lighter = -1 -} +export const PaletteDirectionValue: Readonly<{ + readonly darker: 1; + readonly lighter: -1; +}>; + +// @public +export type PaletteDirectionValue = typeof PaletteDirectionValue[keyof typeof PaletteDirectionValue]; // @public export class PaletteRGB extends BasePalette { diff --git a/packages/utilities/adaptive-ui/src/color/palette.ts b/packages/utilities/adaptive-ui/src/color/palette.ts index 1b2cc7e1392..9a9ebdfdead 100644 --- a/packages/utilities/adaptive-ui/src/color/palette.ts +++ b/packages/utilities/adaptive-ui/src/color/palette.ts @@ -9,17 +9,24 @@ import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js"; * * @public */ -export enum PaletteDirectionValue { +export const PaletteDirectionValue = Object.freeze({ /** * Move darker, or up the Palette. */ - darker = 1, + darker: 1, /** * Move lighter, or down the Palette. */ - lighter = -1, -} + lighter: -1, +} as const); + +/** + * Directional values for navigating {@link Swatch}es in {@link Palette}. + * + * @public + */ +export type PaletteDirectionValue = typeof PaletteDirectionValue[keyof typeof PaletteDirectionValue]; // I know we like to avoid enums so I tried to make it an object, but I'm not sure how to make this work with the type and function below. // export const PaletteDirectionValue = { From ba99a4f1652924eabb52ebf38ad66f99a209edd2 Mon Sep 17 00:00:00 2001 From: Brian Heston <47367562+bheston@users.noreply.github.com> Date: Wed, 25 May 2022 11:59:16 -0700 Subject: [PATCH 4/4] Review comments 2 --- packages/utilities/adaptive-ui/package.json | 15 ++++++++++++--- .../utilities/adaptive-ui/src/color/palette.ts | 4 ++-- .../adaptive-ui/src/design-tokens/create.ts | 2 ++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/utilities/adaptive-ui/package.json b/packages/utilities/adaptive-ui/package.json index 2e10fee261b..27700c61237 100644 --- a/packages/utilities/adaptive-ui/package.json +++ b/packages/utilities/adaptive-ui/package.json @@ -1,12 +1,21 @@ { "name": "@microsoft/adaptive-ui", - "version": "0.1.0", + "version": "1.0.0-alpha", "description": "A collection of design utilities supporting basic styling and Adaptive UI", "type": "module", - "main": "./dist/esm/index.js", + "main": "dist/esm/index.js", + "types": "dist/adaptive-ui.d.ts", + "unpkg": "dist/esm/index.js", + "exports": { + "." : { + "types": "./dist/adaptive-ui.d.ts", + "default": "./dist/esm/index.js" + } + }, "repository": { "type": "git", - "url": "git+https://github.com/microsoft/fast.git" + "url": "git+https://github.com/microsoft/fast.git", + "directory": "packages/utilities/adaptive-ui" }, "author": { "name": "Microsoft", diff --git a/packages/utilities/adaptive-ui/src/color/palette.ts b/packages/utilities/adaptive-ui/src/color/palette.ts index 9a9ebdfdead..7912649cee4 100644 --- a/packages/utilities/adaptive-ui/src/color/palette.ts +++ b/packages/utilities/adaptive-ui/src/color/palette.ts @@ -35,14 +35,14 @@ export type PaletteDirectionValue = typeof PaletteDirectionValue[keyof typeof Pa // } as const; /** - * Convenience type to allow a fixed {@link PaletteDirectionValue} or a function that resolves to one. + * Convenience type to allow a fixed {@link (PaletteDirectionValue:variable)} or a function that resolves to one. * * @public */ export type PaletteDirection = PaletteDirectionValue | (() => PaletteDirectionValue); /** - * Gets a fixed {@link PaletteDirectionValue} from {@link PaletteDirection} which may be a function that needs to be resolved. + * Gets a fixed {@link (PaletteDirectionValue:variable)} from {@link PaletteDirection} which may be a function that needs to be resolved. * * @param direction - A fixed palette direction value or a function that resolves to one * @returns A fixed palette direction value diff --git a/packages/utilities/adaptive-ui/src/design-tokens/create.ts b/packages/utilities/adaptive-ui/src/design-tokens/create.ts index 5aa15157a85..dae16a1d6bb 100644 --- a/packages/utilities/adaptive-ui/src/design-tokens/create.ts +++ b/packages/utilities/adaptive-ui/src/design-tokens/create.ts @@ -1,7 +1,9 @@ import { DesignToken } from "@microsoft/fast-foundation"; +/** @internal */ export const { create } = DesignToken; +/** @internal */ export function createNonCss(name: string): DesignToken { return DesignToken.create({ name, cssCustomPropertyName: null }); }