diff --git a/README.md b/README.md index 689338ff3..1a5c372c1 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,96 @@ npm run lint # Check code rules npm run dist # Build distribution files ``` +## Styling + +Components are styled with plain CSS in a sibling `.css` file, imported for side effects. + +### Theme tokens + +Theme values are defined once, in JavaScript, and projected into CSS custom properties by +`src/theme/theme.css`. That file is **generated** from `src/theme/palette.ts` and +`src/theme.ts` — never edit it by hand. `npm run build` regenerates it as its first step, so +a published package can never ship a stale one. To refresh it without a full build: + +``` +npm run generate:theme-css +``` + +It is committed as well as generated, because jest and ladle read `src/` directly and never +run the build — `src/theme/tokens.spec.ts` fails when the committed copy is stale, which is +what makes CI (which runs lint and test, not build) catch it. + +**Never write a theme value as a literal in a component stylesheet** — reference the token +instead: + +```css +/* no */ +.thing { border-color: #d5d5d5; } + +/* yes */ +.thing { border-color: var(--ox-color-pale); } +``` + +Tokens are `--ox-`-prefixed, so they will not collide with a consuming app's own variables. +Colour tokens are the kebab-case form of the `src/theme/palette.ts` key +(`palette.neutralLighter` → `--ox-color-neutral-lighter`); there are also `--ox-color-link`, +`--ox-color-link-hover`, `--ox-z-index-*` and `--ox-padding-navbar-*`. + +Any component whose CSS uses a token must import the token file alongside its own stylesheet: + +```ts +import './MyComponent.css'; +import '../theme/theme.css'; +``` + +Consumers need do nothing — bundlers deduplicate the import, and `sideEffects` in +`package.json` keeps it from being tree-shaken. + +### Component override hooks and when to bind in JS + +A component may expose its own `--component-*` custom property so consumers can restyle it. +Declare the **default in the stylesheet** with the token as the fallback, and do not bind it +from JavaScript: + +```css +.tabs [role="tab"] { border-color: var(--tabs-active-border-color, var(--ox-color-dark-green)); } +``` + +Bind a custom property from JavaScript only when its value genuinely varies at runtime — a +variant lookup, a numeric prop, a disabled state. A static colour pushed through an inline +style is duplication with extra steps, and it wins over the cascade in ways callers do not +expect. + +Widen the component's `style` prop to `CSSPropertiesWithVariables` (from `src/types`) so +callers can set these without a cast. + +### What CI enforces + +`src/theme/tokens.spec.ts` fails the build if: + +- the committed `theme.css` is not what the generator produces from the JS theme (the build + regenerates it; this is what stops a stale copy reaching jest, ladle or a reviewer) +- a component stylesheet writes a colour literal that duplicates a theme value +- a component stylesheet introduces a colour that is not a theme value and not in the + `KNOWN_OFF_PALETTE` allowlist +- a component stylesheet reads an `--ox-*` token that does not exist + +The colour check parses declarations, so it covers every syntax a colour can be written in — +hex, `rgb()`/`hsl()`/`oklch()`/`color()`, and bare named colours wherever they appear, +including inside shorthands and gradient stops. Functions that merely *contain* colours +(`var()`, `color-mix()`, the gradients) are descended into rather than treated as literals, +so building a value out of tokens stays clean. A translucent colour is accepted when its +opaque channels are a theme value — `rgba(0, 0, 0, 0.2)` is black at 20% and there is no +token form for it — which still refuses a new hue smuggled in through `rgba()`. + +The check has its own tests, so the guarantee is a tested one rather than an asserted one. + +Adding a genuinely new colour is therefore a deliberate act: put it in `palette.ts` if it is +part of the design, or in the allowlist with a reason if it is a one-off we are keeping. + +Breakpoints are the known gap — `@media (min-width: var(--x))` is not valid CSS, so +breakpoint values are still repeated in media queries and are not covered by the tests. + ## Testing Across Projects To test changes before tagging: diff --git a/package.json b/package.json index 057c6370b..31d2eedb6 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ ], "scripts": { "build": "./scripts/build.bash", + "generate:theme-css": "./scripts/generate-theme-css.bash", "clean": "rm -rf ./dist", "build:clean": "npm-run-all clean build", "dist": "npm-run-all clean build", diff --git a/scripts/build.bash b/scripts/build.bash index c86b3997a..ec96d6725 100755 --- a/scripts/build.bash +++ b/scripts/build.bash @@ -6,6 +6,11 @@ project_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/dev/null 2>&1 && pwd cd "$project_dir" +# theme.css is generated from the JS theme, so regenerate before anything copies it. +# Committed too, because jest and ladle read src/ directly and never run this script; +# src/theme/tokens.spec.ts fails when the committed file is stale. +./scripts/generate-theme-css.bash + tsc_args=(--noEmit false --declaration) npx tsc --project tsconfig.without-specs.esm.json "${tsc_args[@]}" diff --git a/scripts/generate-theme-css.bash b/scripts/generate-theme-css.bash new file mode 100755 index 000000000..818103f1c --- /dev/null +++ b/scripts/generate-theme-css.bash @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# spell-checker: ignore pipefail outdir +# +# Regenerates src/theme/theme.css from the JavaScript theme. The committed file is checked +# against this output by src/theme/tokens.spec.ts, so CI fails if palette.ts or theme.ts +# changes without this being re-run. +set -euo pipefail; if [ -n "${DEBUG-}" ]; then set -x; fi + +project_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/dev/null 2>&1 && pwd )" +cd "$project_dir" + +# The theme is TypeScript and the repo has no TS runner outside jest, so compile the +# renderer and its (small, pure) import graph to a throwaway directory and run that. +outdir="$(mktemp -d)" +trap 'rm -rf "$outdir"' EXIT + +npx tsc src/theme/themeCss.ts \ + --outDir "$outdir" --rootDir . \ + --module commonjs --target es2019 --moduleResolution node --esModuleInterop --skipLibCheck + +node -e " + const fs = require('fs'); + const { renderThemeCss } = require('$outdir/src/theme/themeCss'); + fs.writeFileSync('src/theme/theme.css', renderThemeCss()); + console.log('wrote src/theme/theme.css'); +" diff --git a/src/components/Button.css b/src/components/Button.css index 3521a9cfe..5e9250509 100644 --- a/src/components/Button.css +++ b/src/components/Button.css @@ -19,8 +19,8 @@ white-space: nowrap; /* Variant-specific styles using CSS custom properties with fallback defaults (primary variant) */ - background-color: var(--button-bg, #d4450c); - color: var(--button-color, #ffffff); + background-color: var(--button-bg, var(--ox-color-orange)); + color: var(--button-color, var(--ox-color-white)); font-weight: var(--button-font-weight, 700); } @@ -37,16 +37,16 @@ } .button-base:not([disabled]):hover { - background: var(--button-bg-hover, #be3c08); + background: var(--button-bg-hover, var(--ox-color-orange-hover)); } .button-base:not([disabled]):active { - background: var(--button-bg-active, #b03808); + background: var(--button-bg-active, var(--ox-color-orange-active)); } .button-base:focus { - outline: solid var(--button-outline, #ffffff); - box-shadow: inset 0 0 0 0.3rem var(--button-shadow, #424242); + outline: solid var(--button-outline, var(--ox-color-white)); + box-shadow: inset 0 0 0 0.3rem var(--button-shadow, var(--ox-color-black)); } /* Plain button - minimal styling */ @@ -66,12 +66,12 @@ margin: 0; padding: 0; background: none; - color: var(--link-color, #026AA1); + color: var(--link-color, var(--ox-color-link)); text-decoration: none; } .button-link:hover, .button-link:focus { text-decoration: underline; - color: var(--link-hover-color, #005481); + color: var(--link-hover-color, var(--ox-color-link-hover)); } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index d70995797..619796486 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import theme from '../theme'; import { getButtonVariantStyles, ButtonVariant } from "../theme/buttons"; import './Button.css'; +import '../theme/theme.css'; // Re-export for backwards compatibility export { applyButtonVariantStyles } from "../theme/buttons"; @@ -140,18 +141,12 @@ export const ButtonLink = React.forwardRef { const { className, style, children, ...otherProps } = props; - const linkStyleVars = { - '--link-color': theme.colors.link.color, - '--link-hover-color': theme.colors.link.hover, - ...style - } as React.CSSProperties; - return ( diff --git a/src/components/ButtonBar.css b/src/components/ButtonBar.css index d95c4e26b..c3f60232b 100644 --- a/src/components/ButtonBar.css +++ b/src/components/ButtonBar.css @@ -4,7 +4,7 @@ overscroll-behavior: contain; display: flex; flex-direction: row; - border: 0.1rem solid var(--button-bar-border-color, #d5d5d5); + border: 0.1rem solid var(--button-bar-border-color, var(--ox-color-pale)); border-radius: 0.5rem; } @@ -17,8 +17,8 @@ outline-offset: -0.1rem; white-space: nowrap; padding: 0 1.6rem; - background: #fff; - border-right: 0.1rem solid var(--button-bar-border-color, #d5d5d5); + background: var(--ox-color-white); + border-right: 0.1rem solid var(--button-bar-border-color, var(--ox-color-pale)); } .button-bar > *:hover { @@ -59,12 +59,12 @@ } .button-bar > *[data-selected=true] { - background: var(--button-bar-selected-bg, #e5e5e5); - box-shadow: inset 0 0 0 0.1rem var(--button-bar-border-color, #d5d5d5); + background: var(--button-bar-selected-bg, var(--ox-color-neutral-light)); + box-shadow: inset 0 0 0 0.1rem var(--button-bar-border-color, var(--ox-color-pale)); } .button-bar > *:hover:not([data-selected=true]) { - background: var(--button-bar-hover-bg, #f1f1f1); + background: var(--button-bar-hover-bg, var(--ox-color-neutral-lighter)); } @media (forced-colors: active) { diff --git a/src/components/ButtonBar.spec.tsx b/src/components/ButtonBar.spec.tsx index b8877cfd5..2f2d2449b 100644 --- a/src/components/ButtonBar.spec.tsx +++ b/src/components/ButtonBar.spec.tsx @@ -1,6 +1,5 @@ import { render } from "@testing-library/react"; import { ButtonBar } from "./ButtonBar"; -import { palette } from "../theme/palette"; describe("ButtonBar", () => { it("renders", () => { @@ -43,8 +42,10 @@ describe("ButtonBar", () => { const el = container.querySelector("div") as HTMLElement; expect(el.style.getPropertyValue("--button-bar-border-color")).toBe("hotpink"); - // the variables the caller did not override are still bound - expect(el.style.getPropertyValue("--button-bar-selected-bg")).toBe(palette.neutralLight); + // Defaults for the variables the caller did not override live in the stylesheet + // as var(--x, var(--ox-color-*)), so they are deliberately absent from the inline + // style. tokens.spec.ts is what keeps those defaults honest. + expect(el.style.getPropertyValue("--button-bar-selected-bg")).toBe(""); }); it("still accepts ordinary css properties", () => { diff --git a/src/components/ButtonBar.tsx b/src/components/ButtonBar.tsx index 96e5b3495..b3122d614 100644 --- a/src/components/ButtonBar.tsx +++ b/src/components/ButtonBar.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { palette } from "../theme/palette"; import classNames from "classnames"; import { CSSPropertiesWithVariables } from "../types"; import './ButtonBar.css'; +import '../theme/theme.css'; type ButtonBarProps = { size?: "large" | "medium" | "small"; @@ -16,7 +16,7 @@ export const ButtonBar = ({ size = "medium", children, className, - style: customStyle, + style, ...restProps }: ButtonBarProps) => { const buttonBarClass = classNames('button-bar', { @@ -25,13 +25,6 @@ export const ButtonBar = ({ 'button-bar-large': size === 'large', }, className); - const style: CSSPropertiesWithVariables = { - '--button-bar-border-color': palette.pale, - '--button-bar-selected-bg': palette.neutralLight, - '--button-bar-hover-bg': palette.neutralLighter, - ...customStyle, - }; - return (
{children} diff --git a/src/components/Checkbox/Checkbox.css b/src/components/Checkbox/Checkbox.css index 8397cb454..39e4d2d05 100644 --- a/src/components/Checkbox/Checkbox.css +++ b/src/components/Checkbox/Checkbox.css @@ -8,15 +8,15 @@ } .checkbox-label--disabled { - color: var(--checkbox-disabled-color, #e5e5e5); + color: var(--checkbox-disabled-color, var(--ox-color-neutral-light)); } /* Checkbox input/selection styles */ .checkbox-input { appearance: none; - background-color: var(--checkbox-bg-unchecked, #ffffff); + background-color: var(--checkbox-bg-unchecked, var(--ox-color-white)); opacity: var(--checkbox-opacity, 1); - border: var(--checkbox-border-unchecked, 1px solid #6f6f6f); + border: var(--checkbox-border-unchecked, 1px solid var(--ox-color-neutral-thin)); border-radius: 0.2rem; transform: translateY(-0.075em); width: var(--checkbox-size, 1.6rem); @@ -31,9 +31,9 @@ border-radius: 0.2rem; width: var(--checkbox-size, 1.6rem); height: var(--checkbox-size, 1.6rem); - border: var(--checkbox-border-checked, 1px solid #026AA1); + border: var(--checkbox-border-checked, 1px solid var(--ox-color-medium-blue)); transform: scale(0); - background-color: var(--checkbox-bg, #026AA1); + background-color: var(--checkbox-bg, var(--ox-color-medium-blue)); background-image: var(--checkbox-checkmark, none); background-size: 80%; background-position: center; @@ -47,7 +47,7 @@ .checkbox-input--disabled { opacity: 0.4; - border: var(--checkbox-disabled-border, 1px solid #d5d5d5); + border: var(--checkbox-disabled-border, 1px solid var(--ox-color-pale)); } .checkbox-input--disabled:checked::before { @@ -57,9 +57,9 @@ /* TreeCheckbox specific styles */ .checkbox-label [data-slot="selection"] { appearance: none; - background-color: var(--checkbox-bg-unchecked, #ffffff); + background-color: var(--checkbox-bg-unchecked, var(--ox-color-white)); opacity: var(--checkbox-opacity, 1); - border: var(--checkbox-border-unchecked, 1px solid #6f6f6f); + border: var(--checkbox-border-unchecked, 1px solid var(--ox-color-neutral-thin)); border-radius: 0.2rem; transform: translateY(-0.075em); width: var(--checkbox-size, 1.6rem); @@ -74,9 +74,9 @@ border-radius: 0.2rem; width: var(--checkbox-size, 1.6rem); height: var(--checkbox-size, 1.6rem); - border: var(--checkbox-border-checked, 1px solid #026AA1); + border: var(--checkbox-border-checked, 1px solid var(--ox-color-medium-blue)); transform: scale(0); - background-color: var(--checkbox-bg, #026AA1); + background-color: var(--checkbox-bg, var(--ox-color-medium-blue)); background-image: var(--checkbox-checkmark, none); background-size: 80%; background-position: center; @@ -92,14 +92,14 @@ content: ""; position: relative; transform: scale(1); - background-color: var(--checkbox-indeterminate-bg, #026AA1); + background-color: var(--checkbox-indeterminate-bg, var(--ox-color-medium-blue)); border: none; background-image: var(--checkbox-indeterminate-icon, none); } .checkbox-label--disabled [data-slot="selection"] { opacity: 0.4; - border: var(--checkbox-disabled-border, 1px solid #d5d5d5); + border: var(--checkbox-disabled-border, 1px solid var(--ox-color-pale)); } .checkbox-label--disabled[data-selected] [data-slot="selection"]::before { diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 8ee85167e..203e35887 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -2,9 +2,9 @@ import type React from "react"; import { LabelHTMLAttributes, PropsWithChildren } from "react"; import { checkboxVariants, CheckboxVariant, CheckboxSize } from "./sharedCheckboxStyles"; import { InputHTMLAttributes } from "react"; -import { colors } from "../../theme"; import classNames from "classnames"; import "./Checkbox.css"; +import '../../theme/theme.css'; type CheckboxProps = PropsWithChildren< Omit, 'type'> & { @@ -29,7 +29,6 @@ export const Checkbox = ({ children, disabled, variant = 'primary', bold = false const labelStyle = { '--checkbox-font-weight': bold ? 700 : 400, '--checkbox-color': variantStyles.color, - '--checkbox-disabled-color': colors.palette.neutralLight, ...labelProps?.style } as unknown as React.CSSProperties; @@ -43,13 +42,11 @@ export const Checkbox = ({ children, disabled, variant = 'primary', bold = false // Merge input style with our CSS variables const inputStyle = { '--checkbox-size': `${size}rem`, - '--checkbox-bg-unchecked': colors.palette.white, '--checkbox-bg': variantStyles.backgroundColor, '--checkbox-border-unchecked': variantStyles.unCheckedBorder, '--checkbox-border-checked': variantStyles.checkedBorder, '--checkbox-checkmark': variantStyles.backgroundImage === 'none' ? 'none' : `url('${variantStyles.backgroundImage}')`, '--checkbox-opacity': disabled ? '0.4' : '1', - '--checkbox-disabled-border': `1px solid ${colors.palette.pale}`, ...style } as unknown as React.CSSProperties; diff --git a/src/components/Checkbox/__snapshots__/Checkbox.spec.tsx.snap b/src/components/Checkbox/__snapshots__/Checkbox.spec.tsx.snap index 68f573e5b..d116f7508 100644 --- a/src/components/Checkbox/__snapshots__/Checkbox.spec.tsx.snap +++ b/src/components/Checkbox/__snapshots__/Checkbox.spec.tsx.snap @@ -7,7 +7,6 @@ exports[`Checkbox allows setting props on label 1`] = ` style={ Object { "--checkbox-color": "inherit", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 700, } } @@ -17,11 +16,9 @@ exports[`Checkbox allows setting props on label 1`] = ` style={ Object { "--checkbox-bg": "#ffffff", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #d5d5d5", "--checkbox-border-unchecked": "1px solid #d5d5d5", "--checkbox-checkmark": "url('data:image/svg+xml,')", - "--checkbox-disabled-border": "1px solid #d5d5d5", "--checkbox-opacity": "1", "--checkbox-size": "2rem", } @@ -38,7 +35,6 @@ exports[`Checkbox handles disabled state 1`] = ` style={ Object { "--checkbox-color": "inherit", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 400, } } @@ -49,11 +45,9 @@ exports[`Checkbox handles disabled state 1`] = ` style={ Object { "--checkbox-bg": "#ffffff", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #d5d5d5", "--checkbox-border-unchecked": "1px solid #d5d5d5", "--checkbox-checkmark": "none", - "--checkbox-disabled-border": "1px solid #d5d5d5", "--checkbox-opacity": "0.4", "--checkbox-size": "1.6rem", } @@ -70,7 +64,6 @@ exports[`Checkbox handles options 1`] = ` style={ Object { "--checkbox-color": "inherit", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 700, } } @@ -80,11 +73,9 @@ exports[`Checkbox handles options 1`] = ` style={ Object { "--checkbox-bg": "#ffffff", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #d5d5d5", "--checkbox-border-unchecked": "1px solid #d5d5d5", "--checkbox-checkmark": "url('data:image/svg+xml,')", - "--checkbox-disabled-border": "1px solid #d5d5d5", "--checkbox-opacity": "1", "--checkbox-size": "2rem", } @@ -101,7 +92,6 @@ exports[`Checkbox matches snapshot 1`] = ` style={ Object { "--checkbox-color": "inherit", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 400, } } @@ -111,11 +101,9 @@ exports[`Checkbox matches snapshot 1`] = ` style={ Object { "--checkbox-bg": "#026AA1", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #026AA1", "--checkbox-border-unchecked": "1px solid #6f6f6f", "--checkbox-checkmark": "url('data:image/svg+xml,')", - "--checkbox-disabled-border": "1px solid #d5d5d5", "--checkbox-opacity": "1", "--checkbox-size": "1.6rem", } diff --git a/src/components/CloseModalButton.css b/src/components/CloseModalButton.css index 99c8446f0..1e8a3ab82 100644 --- a/src/components/CloseModalButton.css +++ b/src/components/CloseModalButton.css @@ -12,22 +12,22 @@ justify-content: center; /* Default variant colors using CSS custom properties with fallbacks */ - color: var(--close-button-color, #a0a0a0); + color: var(--close-button-color, var(--ox-color-neutral-medium)); } .close-modal-button:hover { - color: var(--close-button-hover-color, #5f6163); + color: var(--close-button-hover-color, var(--ox-color-neutral-dark)); } /* Error variant */ .close-modal-button.error { - --close-button-color: #c22032; + --close-button-color: var(--ox-color-dark-red); } /* Inverted circle variant */ .close-modal-button.inverted-circle { - color: #ffffff; - border: 0.1rem solid #ffffff; + color: var(--ox-color-white); + border: 0.1rem solid var(--ox-color-white); padding: 0; display: flex; align-items: center; @@ -37,7 +37,7 @@ .close-modal-button.inverted-circle:hover, .close-modal-button.inverted-circle:focus { - color: #000000; - background-color: #ffffff; - border-color: #ffffff; + color: var(--ox-color-black); + background-color: var(--ox-color-white); + border-color: var(--ox-color-white); } diff --git a/src/components/CloseModalButton.tsx b/src/components/CloseModalButton.tsx index 5a9dd2659..97f997a43 100644 --- a/src/components/CloseModalButton.tsx +++ b/src/components/CloseModalButton.tsx @@ -2,6 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import { Times } from "./svgs/Times"; import './CloseModalButton.css'; +import '../theme/theme.css'; export interface CloseModalButtonProps extends Omit, 'type' | 'aria-label'> { diff --git a/src/components/DropdownMenu.css b/src/components/DropdownMenu.css index 3918dcdf2..dac50df1b 100644 --- a/src/components/DropdownMenu.css +++ b/src/components/DropdownMenu.css @@ -20,8 +20,8 @@ -webkit-font-smoothing: antialiased; /* Variant-specific styles using CSS custom properties (shared with Button.css) */ - background-color: var(--button-bg, #d4450c); - color: var(--button-color, #ffffff); + background-color: var(--button-bg, var(--ox-color-orange)); + color: var(--button-color, var(--ox-color-white)); font-weight: var(--button-font-weight, 700); } @@ -34,20 +34,20 @@ } .dropdown-menu-button:not([disabled]):hover { - background: var(--button-bg-hover, #be3c08); + background: var(--button-bg-hover, var(--ox-color-orange-hover)); } .dropdown-menu-button:not([disabled]):active { - background: var(--button-bg-active, #b03808); + background: var(--button-bg-active, var(--ox-color-orange-active)); } .dropdown-menu-button:focus { - outline: solid var(--button-outline, #ffffff); - box-shadow: inset 0 0 0 0.3rem var(--button-shadow, #000000); + outline: solid var(--button-outline, var(--ox-color-white)); + box-shadow: inset 0 0 0 0.3rem var(--button-shadow, var(--ox-color-black)); } .dropdown-menu-button::after { - background: var(--dropdown-caret-color, #ffffff); + background: var(--dropdown-caret-color, var(--ox-color-white)); clip-path: polygon(0 0, 100% 100%, 100% 0); content: " "; display: block; @@ -62,11 +62,11 @@ /* Dropdown menu popover */ .dropdown-menu { margin-top: -0.6rem; - background-color: #ffffff; - border: 0.1rem solid #d5d5d5; + background-color: var(--ox-color-white); + border: 0.1rem solid var(--ox-color-pale); padding: 0; cursor: pointer; - color: #000000; + color: var(--ox-color-black); } .dropdown-menu [role="menuitem"] { @@ -84,5 +84,5 @@ } .dropdown-menu [role="menuitem"]:hover { - background-color: #f1f1f1; + background-color: var(--ox-color-neutral-lighter); } diff --git a/src/components/DropdownMenu.tsx b/src/components/DropdownMenu.tsx index 1b262aecc..7f9ff8521 100644 --- a/src/components/DropdownMenu.tsx +++ b/src/components/DropdownMenu.tsx @@ -3,6 +3,7 @@ import { Button, Menu, MenuItem, MenuProps, MenuTrigger, MenuTriggerProps, Popov import { ButtonVariant, getButtonVariantStyles } from '../theme/buttons'; import { palette } from '../theme/palette'; import './DropdownMenu.css'; +import '../theme/theme.css'; interface DropdownMenuButtonProps extends MenuProps, Omit { text?: string; diff --git a/src/components/Modal.css b/src/components/Modal.css index 4e2d29e77..b750dd985 100644 --- a/src/components/Modal.css +++ b/src/components/Modal.css @@ -6,9 +6,9 @@ margin: auto; overflow: hidden; width: 40rem; - background-color: white; + background-color: var(--ox-color-white); box-shadow: 0 0 2rem rgba(0, 0, 0, 0.05), 0 0 4rem rgba(0, 0, 0, 0.08); - color: var(--modal-text-color, #424242); + color: var(--modal-text-color, var(--ox-color-neutral-darker)); font-size: 1.6rem; line-height: 2.5rem; outline: none; @@ -19,15 +19,16 @@ align-items: center; margin-bottom: 1.5rem; padding: 1.5rem 3rem; - background: var(--modal-header-bg, #f1f1f1); + background: var(--modal-header-bg, var(--ox-color-neutral-lighter)); + /* #ddd is not a palette value; see KNOWN_OFF_PALETTE in src/theme/tokens.spec.ts */ border-bottom: solid 0.1rem #ddd; justify-content: space-between; color: var(--modal-header-color, inherit); } .modal-header.error { - --modal-header-bg: #FBE7EA; - --modal-header-color: #c22032; + --modal-header-bg: var(--ox-color-pale-red); + --modal-header-color: var(--ox-color-dark-red); } .modal-heading { @@ -59,7 +60,7 @@ background-color: rgba(0, 0, 0, 0.3); justify-content: center; align-items: center; - z-index: var(--modal-z-index, 30); + z-index: var(--modal-z-index, var(--ox-z-index-modals)); } .card-wrapper { diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 29c91fe2a..e767c23a3 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -3,6 +3,7 @@ import { CloseModalButton } from "./CloseModalButton"; import * as RAC from "react-aria-components"; import React from "react"; import './Modal.css'; +import '../theme/theme.css'; export const ModalCard = React.forwardRef( ({ className, ...props }, ref) => ( diff --git a/src/components/NavBar.css b/src/components/NavBar.css index fb3033ce2..7df3e6630 100644 --- a/src/components/NavBar.css +++ b/src/components/NavBar.css @@ -1,17 +1,17 @@ /* NavBar wrapper - portals to 'nav' slot */ .navbar-wrapper { overflow: visible; - z-index: var(--navbar-z-index, 10); - background: #ffffff; + z-index: var(--navbar-z-index, var(--ox-z-index-navbar)); + background: var(--ox-color-white); position: relative; - padding: 0 var(--navbar-padding-mobile, 1.6rem); + padding: 0 var(--navbar-padding-mobile, var(--ox-padding-navbar-mobile)); box-shadow: 0 0.2rem 0.2rem 0 rgba(0, 0, 0, 0.1); min-width: 0; } @media screen and (min-width: 75.0625em) { .navbar-wrapper { - padding: 0 var(--navbar-padding-desktop, 3.2rem); + padding: 0 var(--navbar-padding-desktop, var(--ox-padding-navbar-desktop)); } } diff --git a/src/components/NavBar.stories.css b/src/components/NavBar.stories.css index 0279da0c6..8ad910ca7 100644 --- a/src/components/NavBar.stories.css +++ b/src/components/NavBar.stories.css @@ -12,7 +12,7 @@ } .story-styled-menu-item { - color: #d4450c; /* colors.palette.orange */ + color: var(--ox-color-orange); } .info-menu-button:hover svg path { diff --git a/src/components/NavBar.stories.tsx b/src/components/NavBar.stories.tsx index 7c8ad6ef1..c85ec2943 100644 --- a/src/components/NavBar.stories.tsx +++ b/src/components/NavBar.stories.tsx @@ -7,6 +7,7 @@ import { PopoverContainer, NavBarPopoverButton, NavBarMenuButton, NavBarMenuItem import { Info } from "./svgs/Info"; import { Tab, Tabs, TabList, TabPanel } from "./Tabs"; import "./NavBar.stories.css"; +import "../theme/theme.css"; const dotsBase64 = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNTYiIHZpZXdCb3g9IjAgMCAxMCA1NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iNSIgZmlsbD0iIzAwMCIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSIyOCIgcj0iNSIgZmlsbD0iIzAwMCIvPgogIDxjaXJjbGUgY3g9IjUiIGN5PSI1MSIgcj0iNSIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4K"; diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index b54580b72..feda425e3 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { CSSPropertiesWithVariables } from '../types'; import classNames from 'classnames'; import * as Constants from '../constants'; -import theme from '../theme'; import { BodyPortal } from './BodyPortal'; import { NavBarLogo as OpenstaxLogo } from './NavBarLogo'; import './NavBar.css'; +import '../theme/theme.css'; type Logo = React.HTMLProps & { alt?: string }; @@ -36,13 +36,6 @@ export const NavBar = ({ const {alt = 'OpenStax Logo', ...anchorProps} = logoIsObject ? logo : {}; const logoComponent = logo ? : null; - const wrapperStyle: CSSPropertiesWithVariables = { - '--navbar-z-index': theme.zIndex.navbar, - '--navbar-padding-mobile': `${theme.padding.navbar.mobile}rem`, - '--navbar-padding-desktop': `${theme.padding.navbar.desktop}rem`, - ...style - }; - const barStyle: CSSPropertiesWithVariables = { '--navbar-max-width': maxWidth ? `${maxWidth}rem` : undefined, '--navbar-justify-content': justifyContent, @@ -56,7 +49,7 @@ export const NavBar = ({ ariaLabel={ariaLabel} slot='nav' className={classNames('navbar-wrapper', className)} - style={wrapperStyle} + style={style} {...props} >
diff --git a/src/components/NavBarButton.tsx b/src/components/NavBarButton.tsx index 715ed2f21..e21b0e358 100644 --- a/src/components/NavBarButton.tsx +++ b/src/components/NavBarButton.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Button, ButtonProps, composeRenderProps } from "react-aria-components"; import classNames from "classnames"; import "./NavBarButton.css"; +import "../theme/theme.css"; // className is deliberately not re-declared here: ButtonProps types it as // `string | ((values: ButtonRenderProps) => string)`, and re-declaring it as `string` diff --git a/src/components/NavBarMenuButtons.css b/src/components/NavBarMenuButtons.css index 83ba3c822..cbc413b2d 100644 --- a/src/components/NavBarMenuButtons.css +++ b/src/components/NavBarMenuButtons.css @@ -6,9 +6,10 @@ /* NavBar Popover */ .navbar-popover { margin-top: -1rem; - border-top: 0.4rem solid var(--navbar-popover-border-color, #63a524); + border-top: 0.4rem solid var(--navbar-popover-border-color, var(--ox-color-dark-green)); + /* black at 20% alpha; no token form without color-mix() */ box-shadow: 0 0.4rem 0.4rem 0 #00000033; - background: #fff; + background: var(--ox-color-white); } /* NavBar Menu Item */ @@ -24,7 +25,7 @@ .navbar-menu-item:hover, .navbar-menu-item[data-hovered], .navbar-menu-item[data-focused] { - background: var(--navbar-menu-item-hover-bg, #f1f1f1); + background: var(--navbar-menu-item-hover-bg, var(--ox-color-neutral-lighter)); } .navbar-menu-item:focus-visible { @@ -38,5 +39,5 @@ } .navbar-menu-item:not(:last-child) { - border-bottom: 0.1rem solid var(--navbar-menu-item-border-color, #f5f5f5); + border-bottom: 0.1rem solid var(--navbar-menu-item-border-color, var(--ox-color-neutral-bright)); } diff --git a/src/components/NavBarMenuButtons.spec.tsx b/src/components/NavBarMenuButtons.spec.tsx index 72c8ce745..d810d8f68 100644 --- a/src/components/NavBarMenuButtons.spec.tsx +++ b/src/components/NavBarMenuButtons.spec.tsx @@ -84,9 +84,11 @@ describe("NavBarMenuItem", () => { , ); + // The component no longer sets --navbar-menu-item-* inline; those are defaults in + // NavBarMenuButtons.css, guarded by src/theme/tokens.spec.ts. What matters here is + // that a render-callback style still reaches the element rather than being dropped. const item = document.querySelector(".navbar-menu-item") as HTMLElement; expect(item.style.color).toBe("rgb(255, 0, 0)"); - expect(item.style.getPropertyValue("--navbar-menu-item-hover-bg")).toBeTruthy(); }); it("lets a render-callback style override the wrapper variables", () => { @@ -118,7 +120,6 @@ describe("NavBarMenuItem", () => { const item = document.querySelector(".navbar-menu-item") as HTMLElement; expect(item.style.color).toBe("rgb(0, 0, 255)"); expect(item.style.getPropertyValue("--navbar-menu-item-hover-bg")).toBe("rebeccapurple"); - expect(item.style.getPropertyValue("--navbar-menu-item-border-color")).toBeTruthy(); }); }); @@ -153,8 +154,8 @@ describe("NavBarPopover", () => { it("merges a render-callback style", () => { const popover = renderPopover({ style: () => ({ color: "rgb(255, 0, 0)" }) }); + // See the note in NavBarMenuItem: --navbar-popover-border-color is a CSS default now. expect(popover.style.color).toBe("rgb(255, 0, 0)"); - expect(popover.style.getPropertyValue("--navbar-popover-border-color")).toBeTruthy(); }); it("lets a render-callback style override the wrapper variables", () => { diff --git a/src/components/NavBarMenuButtons.tsx b/src/components/NavBarMenuButtons.tsx index 44aab02f9..dff7d8fda 100644 --- a/src/components/NavBarMenuButtons.tsx +++ b/src/components/NavBarMenuButtons.tsx @@ -10,36 +10,25 @@ import { Popover, PopoverProps, } from "react-aria-components"; -import { colors } from "../theme"; import { NavBarButton, NavBarButtonProps } from "./NavBarButton"; -import { CSSPropertiesWithVariables } from "../types"; import "./NavBarMenuButtons.css"; +import "../theme/theme.css"; export const NavBarMenuItem = React.forwardRef< HTMLDivElement, React.ComponentProps ->(({ className, style, ...props }, ref) => { - // composeRenderProps normalises the object and render-callback forms of style so a - // caller-supplied callback is merged rather than dropped. The caller still spreads last - // and can override the CSS variables set here. - const menuItemStyle = composeRenderProps( - style, - (resolvedStyle): CSSPropertiesWithVariables => ({ - '--navbar-menu-item-hover-bg': colors.palette.neutralLighter, - '--navbar-menu-item-border-color': colors.palette.neutralBright, - ...resolvedStyle - }) - ); - - return ( - classNames("navbar-menu-item", resolved))} - style={menuItemStyle} - {...props} - /> - ); -}); +>(({ className, ...props }, ref) => ( + // style is deliberately not destructured: with the theme defaults moved into + // NavBarMenuButtons.css there is nothing left to merge it with, so it passes straight + // through in ...props and react-aria handles both the object and render-callback forms. + // That is why this needs no composeRenderProps for style (cf. CORE-2710) — the bug that + // one guards against was us overwriting the caller's style, which we no longer do. + classNames("navbar-menu-item", resolved))} + {...props} + /> +)); NavBarMenuItem.displayName = "NavBarMenuItem"; export const PopoverContainer = React.forwardRef< @@ -53,24 +42,14 @@ PopoverContainer.displayName = "PopoverContainer"; export const NavBarPopover = React.forwardRef< HTMLDivElement, PopoverProps ->(({ className, style, ...props }, ref) => { - const popoverStyle = composeRenderProps( - style, - (resolvedStyle): CSSPropertiesWithVariables => ({ - '--navbar-popover-border-color': colors.palette.darkGreen, - ...resolvedStyle - }) - ); - - return ( - classNames("navbar-popover", resolved))} - style={popoverStyle} - {...props} - /> - ); -}); +>(({ className, ...props }, ref) => ( + // style passes through in ...props — see the note on NavBarMenuItem above. + classNames("navbar-popover", resolved))} + {...props} + /> +)); NavBarPopover.displayName = "NavBarPopover"; export type NavBarBaseButtonProps = React.PropsWithChildren<{ diff --git a/src/components/Overlay.css b/src/components/Overlay.css index a99a04314..73f04e376 100644 --- a/src/components/Overlay.css +++ b/src/components/Overlay.css @@ -15,7 +15,7 @@ } .overlay-wrapper { - color: #fff; + color: var(--ox-color-white); } .overlay-body { diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx index a93240d90..afdaaebc4 100644 --- a/src/components/Overlay.tsx +++ b/src/components/Overlay.tsx @@ -4,6 +4,7 @@ import { Mask, ModalWrapper } from "./Modal"; import * as RAC from "react-aria-components"; import React from "react"; import './Overlay.css'; +import '../theme/theme.css'; export const OverlayMask = React.forwardRef( ({ className, ...props }, ref) => ( diff --git a/src/components/Radio.css b/src/components/Radio.css index ef6e7ccee..8fe78a1d3 100644 --- a/src/components/Radio.css +++ b/src/components/Radio.css @@ -9,20 +9,20 @@ } .radio-label--disabled { - color: var(--radio-disabled-color, #d5d5d5); + color: var(--radio-disabled-color, var(--ox-color-pale)); } /* Radio input styles */ .radio-input { appearance: none; /* For iOS < 15 to remove gradient background */ - background-color: var(--radio-bg, #ffffff); + background-color: var(--radio-bg, var(--ox-color-white)); opacity: var(--radio-opacity, 1); font: inherit; - color: var(--radio-border-color, #d5d5d5); + color: var(--radio-border-color, var(--ox-color-pale)); width: 2rem; height: 2rem; - border: var(--radio-border, 1px solid #6f6f6f); + border: var(--radio-border, 1px solid var(--ox-color-neutral-thin)); border-radius: 50%; transform: translateY(-0.075em); margin: 0 1.6rem 0 0; @@ -36,7 +36,7 @@ height: 1.6rem; border-radius: 50%; opacity: 0; - box-shadow: inset 1em 1em var(--radio-checked, #026AA1); + box-shadow: inset 1em 1em var(--radio-checked, var(--ox-color-medium-blue)); } .radio-input:checked::before { diff --git a/src/components/Radio.tsx b/src/components/Radio.tsx index c5525a53d..b6ac39961 100644 --- a/src/components/Radio.tsx +++ b/src/components/Radio.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { PropsWithChildren } from "react"; -import { colors } from "../theme"; import { InputHTMLAttributes } from "react"; import {useTooltipTriggerState} from 'react-stately'; import {useTooltipTrigger} from 'react-aria'; import { CustomTooltip } from './Tooltip'; import classNames from 'classnames'; import "./Radio.css"; +import '../theme/theme.css'; type RadioProps = PropsWithChildren< Omit, 'type'>>; @@ -26,12 +26,6 @@ export const Radio = ({ children, disabled, labelAs, className, style, tooltipTe { 'radio-label--disabled': disabled } ); - // Label style with CSS variables - const labelStyle = { - '--radio-label-color': 'inherit', - '--radio-disabled-color': colors.palette.pale, - } as unknown as React.CSSProperties; - // Input className const inputClassName = classNames( 'radio-input', @@ -39,12 +33,9 @@ export const Radio = ({ children, disabled, labelAs, className, style, tooltipTe className ); - // Input style with CSS variables + // Input style: only the values that vary at runtime. Static colours come from the + // token defaults in Radio.css. const inputStyle = { - '--radio-bg': colors.palette.white, - '--radio-border': `1px solid ${colors.palette.neutralThin}`, - '--radio-border-color': colors.palette.pale, - '--radio-checked': colors.palette.mediumBlue, '--radio-opacity': disabled ? '0.4' : '1', '--radio-checked-opacity': disabled ? '0' : '1', ...style @@ -89,7 +80,6 @@ export const Radio = ({ children, disabled, labelAs, className, style, tooltipTe { ref, className: labelClassName, - style: labelStyle, ...tPropsWithUpdatedOnFocus, }, labelWithTooltip @@ -98,10 +88,7 @@ export const Radio = ({ children, disabled, labelAs, className, style, tooltipTe
: React.createElement( labelElement, - { - className: labelClassName, - style: labelStyle, - }, + { className: labelClassName }, labelContent ); }; diff --git a/src/components/Tabs.css b/src/components/Tabs.css index 8834b35ae..0f4579657 100644 --- a/src/components/Tabs.css +++ b/src/components/Tabs.css @@ -34,7 +34,7 @@ /* Default tabs variant (horizontal with underline) */ .tabs:not(.tabs-button-bar)[data-orientation="horizontal"] [role="tablist"] { - border-bottom: 0.1rem solid var(--tabs-border-color, #d5d5d5); + border-bottom: 0.1rem solid var(--tabs-border-color, var(--ox-color-pale)); } .tabs:not(.tabs-button-bar) [role="tab"] { @@ -53,19 +53,19 @@ .tabs:not(.tabs-button-bar) [role="tab"][data-selected=true], .tabs:not(.tabs-button-bar) [role="tab"]:hover { - border-color: var(--tabs-active-border-color, #63a524); + border-color: var(--tabs-active-border-color, var(--ox-color-dark-green)); } /* Button bar variant */ .tabs.tabs-button-bar [role="tablist"] { - border: 0.1rem solid var(--tabs-border-color, #d5d5d5); + border: 0.1rem solid var(--tabs-border-color, var(--ox-color-pale)); border-radius: 0.5rem; } .tabs.tabs-button-bar [role="tab"] { padding: 0 1.6rem; - background: #fff; - border-right: 0.1rem solid var(--tabs-border-color, #d5d5d5); + background: var(--ox-color-white); + border-right: 0.1rem solid var(--tabs-border-color, var(--ox-color-pale)); } .tabs.tabs-button-bar.tabs-small [role="tab"] { @@ -93,12 +93,12 @@ } .tabs.tabs-button-bar [role="tab"][data-selected=true] { - background: var(--tabs-button-selected-bg, #e5e5e5); - box-shadow: inset 0 0 0 0.1rem var(--tabs-border-color, #d5d5d5); + background: var(--tabs-button-selected-bg, var(--ox-color-neutral-light)); + box-shadow: inset 0 0 0 0.1rem var(--tabs-border-color, var(--ox-color-pale)); } .tabs.tabs-button-bar [role="tab"]:hover:not([data-selected=true]) { - background: var(--tabs-button-hover-bg, #f1f1f1); + background: var(--tabs-button-hover-bg, var(--ox-color-neutral-lighter)); } @media (forced-colors: active) { diff --git a/src/components/Tabs.spec.tsx b/src/components/Tabs.spec.tsx index 66f0d9979..4f9e4ce49 100644 --- a/src/components/Tabs.spec.tsx +++ b/src/components/Tabs.spec.tsx @@ -1,6 +1,5 @@ import { render } from "@testing-library/react"; import { Tabs, Tab, TabList, TabPanel } from "./Tabs"; -import { palette } from "../theme/palette"; import type { ComponentProps } from "react"; describe("Tabs component", () => { @@ -121,7 +120,6 @@ describe("Tabs component", () => { expect(el.classList.contains("tabs-medium")).toBe(true); expect(el.classList.contains("custom")).toBe(true); expect(el.style.color).toBe("red"); - expect(el.style.getPropertyValue("--tabs-border-color")).toBe(palette.pale); }); // No `as CSSPropertiesWithVariables` cast below: the point of these is that the @@ -131,8 +129,10 @@ describe("Tabs component", () => { const el = container.querySelector('[data-orientation]') as HTMLElement; expect(el.style.getPropertyValue("--tabs-border-color")).toBe("hotpink"); - // the variables the caller did not override are still bound - expect(el.style.getPropertyValue("--tabs-active-border-color")).toBe(palette.darkGreen); + // Defaults for the variables the caller did not override live in Tabs.css as + // var(--tabs-*, var(--ox-color-*)), so they are deliberately absent from the + // inline style. tokens.spec.ts is what keeps those defaults honest. + expect(el.style.getPropertyValue("--tabs-active-border-color")).toBe(""); }); it("lets a style render callback return css variables without a cast", () => { @@ -156,7 +156,6 @@ describe("Tabs component", () => { expect(el.classList.contains("tabs")).toBe(true); expect(el.classList.contains("custom-horizontal")).toBe(true); expect(el.style.color).toBe("red"); - expect(el.style.getPropertyValue("--tabs-active-border-color")).toBe(palette.darkGreen); }); }); }); diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index aa1067db8..05826eae8 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -1,9 +1,9 @@ import type { CSSProperties } from "react"; import * as RAC from "react-aria-components"; -import { palette } from "../theme/palette"; import classNames from "classnames"; import { CSSPropertiesWithVariables } from "../types"; import './Tabs.css'; +import '../theme/theme.css'; // style is widened to CSSPropertiesWithVariables so callers can override the documented // --tabs-* custom properties without casting. Note the Omit: intersecting a narrower @@ -30,13 +30,6 @@ export const Tabs = ({ 'tabs-large': size === 'large', }); - const cssVariables: CSSPropertiesWithVariables = { - '--tabs-border-color': palette.pale, - '--tabs-active-border-color': palette.darkGreen, - '--tabs-button-selected-bg': palette.neutralLight, - '--tabs-button-hover-bg': palette.neutralLighter, - }; - // className and style may each be a render callback, so resolve them against the // render props before merging. RAC folds its own defaultStyle in for us. return ( @@ -46,10 +39,7 @@ export const Tabs = ({ variantClassName, typeof className === 'function' ? className(values) : className )} - style={(values) => ({ - ...cssVariables, - ...(typeof style === 'function' ? style(values) : style), - })} + style={(values) => (typeof style === 'function' ? style(values) : style)} /> ); }; diff --git a/src/components/Text.css b/src/components/Text.css index bba575f3f..a860c2541 100644 --- a/src/components/Text.css +++ b/src/components/Text.css @@ -1,17 +1,17 @@ .text-h2 { - color: #424242; + color: var(--ox-color-neutral-darker); font-size: 3.6rem; font-weight: 700; } .text-h3 { - color: #424242; + color: var(--ox-color-neutral-darker); font-size: 1.6rem; font-weight: 700; text-transform: uppercase; } .text-paragraph { - color: #424242; + color: var(--ox-color-neutral-darker); font-size: 1.8rem; } diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 7381aa8d1..9c37606b3 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,6 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import './Text.css'; +import '../theme/theme.css'; export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; diff --git a/src/components/Toast.css b/src/components/Toast.css index ee43a94ec..b993fdf10 100644 --- a/src/components/Toast.css +++ b/src/components/Toast.css @@ -1,7 +1,7 @@ /* Toast component styles */ .toast { min-height: 5rem; - background-color: white; + background-color: var(--ox-color-white); box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2); display: flex; align-items: center; @@ -56,22 +56,22 @@ } .toast .success .title { - color: var(--toast-success-title-color, #4e7226); - background-color: var(--toast-success-bg, #e0edd3); + color: var(--toast-success-title-color, var(--ox-color-darker-green)); + background-color: var(--toast-success-bg, var(--ox-color-pale-green)); } .toast .neutral .title { - color: var(--toast-neutral-title-color, #424242); - background-color: var(--toast-neutral-bg, #f1f1f1); + color: var(--toast-neutral-title-color, var(--ox-color-neutral-darker)); + background-color: var(--toast-neutral-bg, var(--ox-color-neutral-lighter)); } .toast .failure .title { - color: var(--toast-failure-title-color, #c22032); - background-color: var(--toast-failure-bg, #fbe7ea); + color: var(--toast-failure-title-color, var(--ox-color-dark-red)); + background-color: var(--toast-failure-bg, var(--ox-color-pale-red)); } .toast .failure .title .openstax-icon[type="close"] { - color: var(--toast-failure-icon-color, #5f6163); + color: var(--toast-failure-icon-color, var(--ox-color-neutral-dark)); margin-left: 2rem; align-self: flex-start; } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 0c61223f2..15eb9ccb0 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { palette } from '../theme/palette'; import classNames from 'classnames'; import { CSSPropertiesWithVariables, ToastData } from '../types'; import './Toast.css'; +import '../theme/theme.css'; const ANIMATION_TIME_MS = 500; const DISMISS_AFTER_MS_FLOOR = 1000; @@ -64,13 +64,6 @@ export const Toast = ({ const style: CSSPropertiesWithVariables = { '--toast-animation-duration': `${ANIMATION_TIME_MS}ms`, - '--toast-success-title-color': palette.darkerGreen, - '--toast-success-bg': palette.paleGreen, - '--toast-neutral-title-color': palette.neutralDarker, - '--toast-neutral-bg': palette.neutralLighter, - '--toast-failure-title-color': palette.darkRed, - '--toast-failure-bg': palette.paleRed, - '--toast-failure-icon-color': palette.neutralDark, ...(dismissAfterMs ? { animationDelay: `${dismissAfterMs - ANIMATION_TIME_MS}ms` } : {}), }; diff --git a/src/components/ToastContainer.css b/src/components/ToastContainer.css index b0c38449d..60d9a2ef9 100644 --- a/src/components/ToastContainer.css +++ b/src/components/ToastContainer.css @@ -1,6 +1,6 @@ /* Toast container styles */ .toast-container { - z-index: var(--toast-container-z-index, 40); + z-index: var(--toast-container-z-index, var(--ox-z-index-toasts)); display: grid; justify-items: center; justify-content: center; diff --git a/src/components/ToastContainer.tsx b/src/components/ToastContainer.tsx index 6d349fbf8..43292193a 100644 --- a/src/components/ToastContainer.tsx +++ b/src/components/ToastContainer.tsx @@ -1,9 +1,9 @@ import { BodyPortal } from './BodyPortal'; import { Toast } from './Toast'; -import { zIndex } from '../theme'; -import { CSSPropertiesWithVariables, ToastData } from '../types'; +import { ToastData } from '../types'; import classNames from 'classnames'; import './ToastContainer.css'; +import '../theme/theme.css'; export type ToastContainerParams = { toasts: ToastData[]; @@ -24,17 +24,13 @@ const makeToasts = (toasts: ToastData[], inline: boolean, onDismissToast?: (id: >{toast.message} ) ); -const zIndexStyle: CSSPropertiesWithVariables = { - '--toast-container-z-index': zIndex.toasts, -}; - export const ToastContainer: ToastContainerComponent = ({ toasts, onDismissToast, inline = false, className }) => { const containerClass = classNames('toast-container', { 'toast-container-inline': inline, }, className); return ( -
+
{makeToasts(toasts, inline, onDismissToast)}
); @@ -46,7 +42,7 @@ export const BodyPortalToastContainer: ToastContainerComponent = ({ toasts, onDi }, className); return ( - + {makeToasts(toasts, inline, onDismissToast)} ); diff --git a/src/components/Tooltip.css b/src/components/Tooltip.css index 55164f253..6b65a3a40 100644 --- a/src/components/Tooltip.css +++ b/src/components/Tooltip.css @@ -2,9 +2,10 @@ .tooltip { box-shadow: 0 0.8rem 2rem rgba(0, 0, 0, 0.1); border-radius: 0.3rem; + /* #ccc is not a palette value; see KNOWN_OFF_PALETTE in src/theme/tokens.spec.ts */ border: 1px solid var(--tooltip-border-color, #ccc); - background: var(--tooltip-bg, #ffffff); - color: var(--tooltip-color, #6f6f6f); + background: var(--tooltip-bg, var(--ox-color-white)); + color: var(--tooltip-color, var(--ox-color-neutral-thin)); outline: none; padding: 1rem; /* fixes FF gap */ @@ -54,7 +55,7 @@ .tooltip .react-aria-OverlayArrow svg { display: block; - fill: var(--tooltip-bg, #ffffff); + fill: var(--tooltip-bg, var(--ox-color-white)); } /* Tooltip trigger button */ diff --git a/src/components/Tooltip.spec.tsx b/src/components/Tooltip.spec.tsx index 01e52dfdf..78612c8f3 100644 --- a/src/components/Tooltip.spec.tsx +++ b/src/components/Tooltip.spec.tsx @@ -4,7 +4,6 @@ import { render } from '@testing-library/react'; import ReactDOM from 'react-dom'; import { TooltipTrigger } from 'react-aria-components'; import { StyledTooltip, StyledTrigger, TooltipGroup } from './Tooltip'; -import { palette } from '../theme/palette'; describe('Tooltip', () => { beforeAll(() => { @@ -62,7 +61,6 @@ describe('Tooltip', () => { expect(tooltip).toBeTruthy(); expect(tooltip.classList.contains('tooltip')).toBe(true); expect(tooltip.classList.contains('generated-class')).toBe(true); - expect(tooltip.style.getPropertyValue('--tooltip-bg')).toBe(palette.white); }); }); describe('ref forwarding', () => { @@ -102,8 +100,10 @@ describe('Tooltip', () => { const tooltip = document.body.querySelector('[role="tooltip"]') as HTMLElement; expect(tooltip.style.getPropertyValue('--tooltip-bg')).toBe('hotpink'); - // the variables the caller did not override are still bound - expect(tooltip.style.getPropertyValue('--tooltip-color')).toBe(palette.neutralThin); + // Defaults for the variables the caller did not override live in the stylesheet + // as var(--x, var(--ox-color-*)), so they are deliberately absent from the inline + // style. tokens.spec.ts is what keeps those defaults honest. + expect(tooltip.style.getPropertyValue('--tooltip-color')).toBe(''); }); }); }); diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index f4bea6fe1..82a43bd8b 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -9,16 +9,10 @@ import { } from 'react-aria-components'; import { Info } from './svgs/Info'; import { mergeProps, Placement, useTooltip } from 'react-aria'; -import { palette } from '../theme/palette'; import { CSSPropertiesWithVariables } from '../types'; import classNames from 'classnames'; import './Tooltip.css'; - -const tooltipCssVariables: CSSPropertiesWithVariables = { - '--tooltip-bg': palette.white, - '--tooltip-color': palette.neutralThin, - '--tooltip-border-color': '#ccc', -}; +import '../theme/theme.css'; // The styled-components versions of these accepted a plain className/style and merged // them, so the replacements narrow away the react-aria render-callback forms rather @@ -51,7 +45,7 @@ export const StyledTooltip = React.forwardRef< )); @@ -98,15 +92,10 @@ export const TooltipGroup = ({icon, ariaLabel, ...props}: React.PropsWithChildre export const CustomTooltip = ({ state, ...props }: any) => { const { tooltipProps } = useTooltip(props, state); - // mergeProps combines className with clsx, but style is last-wins, so merge it explicitly const mergedProps = mergeProps(props, tooltipProps, { className: 'tooltip' }); return ( -
+
{props.children} diff --git a/src/components/Tree/TreeCheckbox.tsx b/src/components/Tree/TreeCheckbox.tsx index d77b5f968..7a15584d2 100644 --- a/src/components/Tree/TreeCheckbox.tsx +++ b/src/components/Tree/TreeCheckbox.tsx @@ -10,10 +10,10 @@ import { CheckboxVariant } from "../Checkbox/sharedCheckboxStyles"; import { checkedMixIcon } from "../svgs/checkmarksvgs"; -import { colors } from '../../theme'; import { CSSPropertiesWithVariables } from "../../types"; import classNames from "classnames"; import "../Checkbox/Checkbox.css"; +import '../../theme/theme.css'; export interface TreeCheckboxProps extends PropsWithChildren> { @@ -46,22 +46,22 @@ export const TreeCheckbox = ({ // Build style with CSS variables. composeRenderProps normalises the object and // render-callback forms of style so a caller-supplied callback is merged rather than // dropped. The caller still spreads last and can override the variables set here. + // + // Only genuinely dynamic bindings live here. The static palette values that used to sit + // alongside them are defaults in Checkbox.css now, as var(--checkbox-*, var(--ox-*)); + // src/theme/tokens.spec.ts is what guards them. const checkboxStyle = composeRenderProps( style, (resolvedStyle): CSSPropertiesWithVariables => ({ '--checkbox-font-weight': bold ? 700 : 400, '--checkbox-color': variantStyles.color, - '--checkbox-disabled-color': colors.palette.neutralLight, '--checkbox-size': `${size}rem`, - '--checkbox-bg-unchecked': colors.palette.white, '--checkbox-bg': variantStyles.backgroundColor, '--checkbox-border-unchecked': variantStyles.unCheckedBorder, '--checkbox-border-checked': variantStyles.checkedBorder, '--checkbox-checkmark': variantStyles.backgroundImage === 'none' ? 'none' : `url('${variantStyles.backgroundImage}')`, '--checkbox-opacity': isDisabled ? '0.4' : '1', '--checkbox-checked-opacity': isDisabled ? '0' : '1', - '--checkbox-disabled-border': `1px solid ${colors.palette.pale}`, - '--checkbox-indeterminate-bg': colors.palette.mediumBlue, '--checkbox-indeterminate-icon': `url('${checkedMixIcon}')`, ...resolvedStyle, }) diff --git a/src/components/Tree/__snapshots__/Tree.spec.tsx.snap b/src/components/Tree/__snapshots__/Tree.spec.tsx.snap index 3fdf342e3..7be0d1f49 100644 --- a/src/components/Tree/__snapshots__/Tree.spec.tsx.snap +++ b/src/components/Tree/__snapshots__/Tree.spec.tsx.snap @@ -42,12 +42,12 @@ exports[`Tree matches snapshot 1`] = ` > '); --checkbox-opacity: 1; --checkbox-disabled-border: 1px solid #d5d5d5;" + style="--checkbox-size: 1.4rem; --checkbox-bg: #026AA1; --checkbox-border-unchecked: 1px solid #6f6f6f; --checkbox-border-checked: 1px solid #026AA1; --checkbox-checkmark: url('data:image/svg+xml,'); --checkbox-opacity: 1;" type="checkbox" value="one" /> diff --git a/src/components/Tree/__snapshots__/TreeCheckbox.spec.tsx.snap b/src/components/Tree/__snapshots__/TreeCheckbox.spec.tsx.snap index 2abf5c1bd..51ab14575 100644 --- a/src/components/Tree/__snapshots__/TreeCheckbox.spec.tsx.snap +++ b/src/components/Tree/__snapshots__/TreeCheckbox.spec.tsx.snap @@ -20,16 +20,12 @@ exports[`TreeCheckbox handles disabled state 1`] = ` style={ Object { "--checkbox-bg": "#ffffff", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #d5d5d5", "--checkbox-border-unchecked": "1px solid #d5d5d5", "--checkbox-checked-opacity": "0", "--checkbox-checkmark": "none", "--checkbox-color": "inherit", - "--checkbox-disabled-border": "1px solid #d5d5d5", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 400, - "--checkbox-indeterminate-bg": "#026AA1", "--checkbox-indeterminate-icon": "url('data:image/svg+xml;utf8,')", "--checkbox-opacity": "0.4", "--checkbox-size": "1.6rem", @@ -100,16 +96,12 @@ exports[`TreeCheckbox handles indeterminate state 1`] = ` style={ Object { "--checkbox-bg": "#026AA1", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #026AA1", "--checkbox-border-unchecked": "1px solid #6f6f6f", "--checkbox-checked-opacity": "1", "--checkbox-checkmark": "url('data:image/svg+xml,')", "--checkbox-color": "inherit", - "--checkbox-disabled-border": "1px solid #d5d5d5", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 400, - "--checkbox-indeterminate-bg": "#026AA1", "--checkbox-indeterminate-icon": "url('data:image/svg+xml;utf8,')", "--checkbox-opacity": "1", "--checkbox-size": "1.6rem", @@ -180,16 +172,12 @@ exports[`TreeCheckbox handles options 1`] = ` style={ Object { "--checkbox-bg": "#ffffff", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #d5d5d5", "--checkbox-border-unchecked": "1px solid #d5d5d5", "--checkbox-checked-opacity": "1", "--checkbox-checkmark": "url('data:image/svg+xml,')", "--checkbox-color": "inherit", - "--checkbox-disabled-border": "1px solid #d5d5d5", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 700, - "--checkbox-indeterminate-bg": "#026AA1", "--checkbox-indeterminate-icon": "url('data:image/svg+xml;utf8,')", "--checkbox-opacity": "1", "--checkbox-size": "2rem", @@ -260,16 +248,12 @@ exports[`TreeCheckbox matches snapshot 1`] = ` style={ Object { "--checkbox-bg": "#026AA1", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #026AA1", "--checkbox-border-unchecked": "1px solid #6f6f6f", "--checkbox-checked-opacity": "1", "--checkbox-checkmark": "url('data:image/svg+xml,')", "--checkbox-color": "inherit", - "--checkbox-disabled-border": "1px solid #d5d5d5", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 400, - "--checkbox-indeterminate-bg": "#026AA1", "--checkbox-indeterminate-icon": "url('data:image/svg+xml;utf8,')", "--checkbox-opacity": "1", "--checkbox-size": "1.6rem", @@ -341,16 +325,12 @@ exports[`TreeCheckbox supports slot="selection" 1`] = ` style={ Object { "--checkbox-bg": "#026AA1", - "--checkbox-bg-unchecked": "#ffffff", "--checkbox-border-checked": "1px solid #026AA1", "--checkbox-border-unchecked": "1px solid #6f6f6f", "--checkbox-checked-opacity": "1", "--checkbox-checkmark": "url('data:image/svg+xml,')", "--checkbox-color": "inherit", - "--checkbox-disabled-border": "1px solid #d5d5d5", - "--checkbox-disabled-color": "#e5e5e5", "--checkbox-font-weight": 400, - "--checkbox-indeterminate-bg": "#026AA1", "--checkbox-indeterminate-icon": "url('data:image/svg+xml;utf8,')", "--checkbox-opacity": "1", "--checkbox-size": "1.6rem", diff --git a/src/components/__snapshots__/Button.spec.tsx.snap b/src/components/__snapshots__/Button.spec.tsx.snap index f7012aa73..917afc8de 100644 --- a/src/components/__snapshots__/Button.spec.tsx.snap +++ b/src/components/__snapshots__/Button.spec.tsx.snap @@ -99,12 +99,6 @@ exports[`Button renders as a tag variant 1`] = ` exports[`Button renders button that looks like a link 1`] = ` diff --git a/src/components/__snapshots__/ButtonBar.spec.tsx.snap b/src/components/__snapshots__/ButtonBar.spec.tsx.snap index 3f604d571..7cb0898a5 100644 --- a/src/components/__snapshots__/ButtonBar.spec.tsx.snap +++ b/src/components/__snapshots__/ButtonBar.spec.tsx.snap @@ -4,7 +4,6 @@ exports[`ButtonBar renders 1`] = `
One @@ -20,7 +19,6 @@ exports[`ButtonBar renders large size 1`] = `
@@ -20,12 +14,6 @@ exports[`ManageCookies when CookieYes loads renders button in wrapper 1`] = ` @@ -36,12 +24,6 @@ exports[`ManageCookies when CookieYes loads renders button with className 1`] = @@ -51,12 +33,6 @@ exports[`ManageCookies when CookieYes loads renders button with content and corr @@ -66,12 +42,6 @@ exports[`ManageCookies with CookieYes already loaded renders button 1`] = ` @@ -82,12 +52,6 @@ exports[`ManageCookies with CookieYes already loaded renders button in wrapper 1 @@ -98,12 +62,6 @@ exports[`ManageCookies with CookieYes already loaded renders button with content diff --git a/src/components/__snapshots__/NavBar.spec.tsx.snap b/src/components/__snapshots__/NavBar.spec.tsx.snap index bd1a332da..b33569fe8 100644 --- a/src/components/__snapshots__/NavBar.spec.tsx.snap +++ b/src/components/__snapshots__/NavBar.spec.tsx.snap @@ -8,7 +8,6 @@ exports[`NavBar matches snapshot 1`] = `