From 7f5b2cda0f1b26bdd07978b10fe4a3f0a169df4d Mon Sep 17 00:00:00 2001 From: Jenny Zhu Date: Tue, 7 Jul 2026 12:12:29 -0400 Subject: [PATCH 1/2] [FEATURE] create @perses-dev/design-token package Signed-off-by: Jenny Zhu --- design-tokens/.eslintrc.js | 14 ++ design-tokens/jest.config.ts | 23 +++ design-tokens/package.json | 48 +++++ design-tokens/src/colors.ts | 129 ++++++++++++ design-tokens/src/css/index.css | 20 ++ design-tokens/src/css/reset.css | 28 +++ design-tokens/src/css/semantic.css | 228 +++++++++++++++++++++ design-tokens/src/css/tokens.css | 155 ++++++++++++++ design-tokens/src/index.ts | 16 ++ design-tokens/src/test/consistency.test.ts | 60 ++++++ design-tokens/src/test/css.test.ts | 115 +++++++++++ design-tokens/src/test/setup-tests.ts | 12 ++ design-tokens/src/test/tokens.test.ts | 132 ++++++++++++ design-tokens/src/test/type-assertions.ts | 62 ++++++ design-tokens/src/tokens.ts | 132 ++++++++++++ design-tokens/src/types.ts | 87 ++++++++ design-tokens/tsconfig.build.json | 9 + design-tokens/tsconfig.json | 8 + jest.shared.ts | 2 +- package-lock.json | 10 + package.json | 1 + 21 files changed, 1290 insertions(+), 1 deletion(-) create mode 100644 design-tokens/.eslintrc.js create mode 100644 design-tokens/jest.config.ts create mode 100644 design-tokens/package.json create mode 100644 design-tokens/src/colors.ts create mode 100644 design-tokens/src/css/index.css create mode 100644 design-tokens/src/css/reset.css create mode 100644 design-tokens/src/css/semantic.css create mode 100644 design-tokens/src/css/tokens.css create mode 100644 design-tokens/src/index.ts create mode 100644 design-tokens/src/test/consistency.test.ts create mode 100644 design-tokens/src/test/css.test.ts create mode 100644 design-tokens/src/test/setup-tests.ts create mode 100644 design-tokens/src/test/tokens.test.ts create mode 100644 design-tokens/src/test/type-assertions.ts create mode 100644 design-tokens/src/tokens.ts create mode 100644 design-tokens/src/types.ts create mode 100644 design-tokens/tsconfig.build.json create mode 100644 design-tokens/tsconfig.json diff --git a/design-tokens/.eslintrc.js b/design-tokens/.eslintrc.js new file mode 100644 index 00000000..08ac9603 --- /dev/null +++ b/design-tokens/.eslintrc.js @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = require('../.eslintrc.base.js'); diff --git a/design-tokens/jest.config.ts b/design-tokens/jest.config.ts new file mode 100644 index 00000000..095facc3 --- /dev/null +++ b/design-tokens/jest.config.ts @@ -0,0 +1,23 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Config } from '@jest/types'; +import shared from '../jest.shared'; + +const jestConfig: Config.InitialOptions = { + ...shared, + + setupFilesAfterEnv: [...(shared.setupFilesAfterEnv ?? []), '/src/test/setup-tests.ts'], +}; + +export default jestConfig; diff --git a/design-tokens/package.json b/design-tokens/package.json new file mode 100644 index 00000000..6f120719 --- /dev/null +++ b/design-tokens/package.json @@ -0,0 +1,48 @@ +{ + "name": "@perses-dev/design-tokens", + "version": "0.54.0-beta.10", + "description": "Perses design tokens for ui components", + "license": "Apache-2.0", + "homepage": "https://github.com/perses/perses/blob/main/README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/perses/perses.git" + }, + "bugs": { + "url": "https://github.com/perses/perses/issues" + }, + "module": "dist/index.js", + "main": "dist/cjs/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/cjs/index.js", + "types": "./dist/index.d.ts" + }, + "./css": "./dist/css/index.css", + "./css/reset": "./dist/css/reset.css", + "./css/tokens": "./dist/css/tokens.css", + "./css/semantic": "./dist/css/semantic.css" + }, + "sideEffects": [ + "*.css" + ], + "scripts": { + "clean": "rimraf dist/", + "build": "concurrently \"npm:build:*\"", + "build:cjs": "swc ./src -d dist/cjs --strip-leading-paths --config-file ../.cjs.swcrc --ignore '**/test/**'", + "build:esm": "swc ./src -d dist --strip-leading-paths --config-file ../.swcrc --ignore '**/test/**'", + "build:types": "tsc --project tsconfig.build.json", + "build:css": "mkdir -p dist/css && cp -f src/css/*.css dist/css/", + "type-check": "tsc --noEmit", + "start": "concurrently -P \"npm:build:* -- {*}\" -- --watch", + "test": "cross-env TZ=UTC jest", + "test:watch": "npm run test -- --watch", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint --fix src --ext .ts,.tsx" + }, + "files": [ + "dist" + ] +} diff --git a/design-tokens/src/colors.ts b/design-tokens/src/colors.ts new file mode 100644 index 00000000..87380aae --- /dev/null +++ b/design-tokens/src/colors.ts @@ -0,0 +1,129 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export type HexColor = `#${string}`; + +export interface PersesColor { + 50: HexColor; + 100: HexColor; + 150: HexColor; + 200: HexColor; + 300: HexColor; + 400: HexColor; + 500: HexColor; + 600: HexColor; + 700: HexColor; + 800: HexColor; + 850: HexColor; + 900: HexColor; + 950: HexColor; +} + +export const blue: PersesColor = { + 50: '#E7F1FC', + 100: '#D0E3FA', + 150: '#B8D5F7', + 200: '#A1C7F5', + 300: '#72ABF0', + 400: '#438FEB', + 500: '#1473E6', + 600: '#105CB8', + 700: '#0C458A', + 800: '#082E5C', + 850: '#062345', + 900: '#04172E', + 950: '#020C17', +}; + +export const green: PersesColor = { + 50: '#EAF9F1', + 100: '#D5F2E3', + 150: '#C1ECD4', + 200: '#ACE5C6', + 300: '#82D9AA', + 400: '#59CC8D', + 500: '#2FBF71', + 600: '#26995A', + 700: '#1C7344', + 800: '#134C2D', + 850: '#0E3922', + 900: '#092617', + 950: '#05130B', +}; + +export const gray: PersesColor = { + 50: '#F0F1F6', + 100: '#E1E3ED', + 150: '#D2D5E4', + 200: '#C3C7DB', + 300: '#A4ACC8', + 400: '#8690B6', + 500: '#717CA4', + 600: '#535D83', + 700: '#3E4662', + 800: '#2A2E42', + 850: '#1F2331', + 900: '#151721', + 950: '#0A0C10', +}; + +export const orange: PersesColor = { + 50: '#FFF5E8', + 100: '#FFECD2', + 150: '#FFE2BB', + 200: '#FFD9A4', + 300: '#FFC577', + 400: '#FFB249', + 500: '#FF9F1C', + 600: '#CC7F16', + 700: '#995F11', + 800: '#66400B', + 850: '#4D3008', + 900: '#332006', + 950: '#1A1003', +}; + +export const purple: PersesColor = { + 50: '#EFE9FD', + 100: '#E0D2FC', + 150: '#D0BCFA', + 200: '#C1A6F8', + 300: '#A179F5', + 400: '#824DF1', + 500: '#6320EE', + 600: '#4F1ABE', + 700: '#3B138F', + 800: '#280D5F', + 850: '#1E0A47', + 900: '#140630', + 950: '#0A0318', +}; + +export const red: PersesColor = { + 50: '#FDEDED', + 100: '#FBDADA', + 150: '#F9C8C8', + 200: '#F7B5B5', + 300: '#F29191', + 400: '#EE6C6C', + 500: '#EA4747', + 600: '#BD3939', + 700: '#902B2B', + 800: '#621D1D', + 850: '#4C1616', + 900: '#350F0F', + 950: '#1F0808', +}; + +export const white = '#FFFFFF' as HexColor; +export const black = '#000000' as HexColor; diff --git a/design-tokens/src/css/index.css b/design-tokens/src/css/index.css new file mode 100644 index 00000000..8749d048 --- /dev/null +++ b/design-tokens/src/css/index.css @@ -0,0 +1,20 @@ +/* + * Copyright The Perses Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer perses.reset, perses.tokens, perses.semantic; + +@import './reset.css'; +@import './tokens.css'; +@import './semantic.css'; diff --git a/design-tokens/src/css/reset.css b/design-tokens/src/css/reset.css new file mode 100644 index 00000000..d211b250 --- /dev/null +++ b/design-tokens/src/css/reset.css @@ -0,0 +1,28 @@ +/* + * Copyright The Perses Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer perses.reset { + *, + *::before, + *::after { + box-sizing: border-box; + } + + body { + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} diff --git a/design-tokens/src/css/semantic.css b/design-tokens/src/css/semantic.css new file mode 100644 index 00000000..971631df --- /dev/null +++ b/design-tokens/src/css/semantic.css @@ -0,0 +1,228 @@ +/* + * Copyright The Perses Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer perses.semantic { + /* + * Elevation hierarchy (lowest → highest): + * bg-default — page/app background (base level) + * bg-surface — raised surfaces: cards, panels, dialogs + * bg-sunken — recessed areas: code blocks, inset regions + * bg-overlay — floating surfaces: tooltips, popovers, dropdowns + * bg-backdrop — semi-transparent scrim behind modals + * + * Other backgrounds: + * bg-navigation — sidebar/nav area (brand-tinted) + * + * Border: + * border-default — general-purpose border color + */ + + /* ---- Light mode (default) ---- */ + :root { + /* Background: Elevation */ + --perses-bg-default: var(--perses-color-white); + --perses-bg-surface: var(--perses-color-gray-50); + --perses-bg-sunken: var(--perses-color-gray-100); + --perses-bg-overlay: var(--perses-color-gray-100); + --perses-bg-backdrop: rgba(21, 23, 33, 0.75); + --perses-bg-navigation: var(--perses-color-blue-150); + + /* Border */ + --perses-border-default: var(--perses-color-gray-100); + + /* Text */ + --perses-text-primary: var(--perses-color-gray-800); + --perses-text-secondary: var(--perses-color-gray-700); + --perses-text-disabled: var(--perses-color-gray-300); + --perses-text-link: var(--perses-color-blue-500); + --perses-text-link-hover: var(--perses-color-blue-600); + --perses-text-navigation: var(--perses-color-gray-800); + --perses-text-accent: var(--perses-color-gray-300); + + /* Status: Primary */ + --perses-status-bg-primary: var(--perses-color-blue-50); + --perses-status-bg-primary-hover: var(--perses-color-blue-100); + --perses-status-text-primary: var(--perses-color-blue-700); + --perses-status-border-primary: var(--perses-color-blue-200); + --perses-status-icon-primary: var(--perses-color-blue-600); + + /* Status: Secondary */ + --perses-status-bg-secondary: var(--perses-color-gray-50); + --perses-status-bg-secondary-hover: var(--perses-color-gray-100); + --perses-status-text-secondary: var(--perses-color-gray-700); + --perses-status-border-secondary: var(--perses-color-gray-200); + --perses-status-icon-secondary: var(--perses-color-gray-600); + + /* Status: Error */ + --perses-status-bg-error: var(--perses-color-red-50); + --perses-status-bg-error-hover: var(--perses-color-red-100); + --perses-status-text-error: var(--perses-color-red-700); + --perses-status-border-error: var(--perses-color-red-200); + --perses-status-icon-error: var(--perses-color-red-600); + + /* Status: Warning */ + --perses-status-bg-warning: var(--perses-color-orange-50); + --perses-status-bg-warning-hover: var(--perses-color-orange-100); + --perses-status-text-warning: var(--perses-color-orange-700); + --perses-status-border-warning: var(--perses-color-orange-200); + --perses-status-icon-warning: var(--perses-color-orange-600); + + /* Status: Success */ + --perses-status-bg-success: var(--perses-color-green-50); + --perses-status-bg-success-hover: var(--perses-color-green-100); + --perses-status-text-success: var(--perses-color-green-700); + --perses-status-border-success: var(--perses-color-green-200); + --perses-status-icon-success: var(--perses-color-green-600); + + /* Status: Info */ + --perses-status-bg-info: var(--perses-color-blue-50); + --perses-status-bg-info-hover: var(--perses-color-blue-100); + --perses-status-text-info: var(--perses-color-blue-700); + --perses-status-border-info: var(--perses-color-blue-200); + --perses-status-icon-info: var(--perses-color-blue-600); + } + + /* ---- Dark mode: explicit (data attribute) ---- */ + [data-perses-mode='dark'] { + /* Background: Elevation */ + --perses-bg-default: var(--perses-color-gray-900); + --perses-bg-surface: var(--perses-color-gray-850); + --perses-bg-sunken: var(--perses-color-gray-800); + --perses-bg-overlay: var(--perses-color-gray-600); + --perses-bg-backdrop: rgba(10, 12, 16, 0.85); + --perses-bg-navigation: var(--perses-color-gray-850); + + /* Border */ + --perses-border-default: var(--perses-color-gray-600); + + /* Text */ + --perses-text-primary: var(--perses-color-white); + --perses-text-secondary: var(--perses-color-gray-50); + --perses-text-disabled: var(--perses-color-gray-600); + --perses-text-link: var(--perses-color-blue-400); + --perses-text-link-hover: var(--perses-color-blue-500); + --perses-text-navigation: var(--perses-color-white); + --perses-text-accent: var(--perses-color-gray-400); + + /* Status: Primary */ + --perses-status-bg-primary: var(--perses-color-blue-900); + --perses-status-bg-primary-hover: var(--perses-color-blue-850); + --perses-status-text-primary: var(--perses-color-blue-300); + --perses-status-border-primary: var(--perses-color-blue-700); + --perses-status-icon-primary: var(--perses-color-blue-600); + + /* Status: Secondary */ + --perses-status-bg-secondary: var(--perses-color-gray-850); + --perses-status-bg-secondary-hover: var(--perses-color-gray-800); + --perses-status-text-secondary: var(--perses-color-gray-200); + --perses-status-border-secondary: var(--perses-color-gray-700); + --perses-status-icon-secondary: var(--perses-color-gray-400); + + /* Status: Error */ + --perses-status-bg-error: var(--perses-color-red-900); + --perses-status-bg-error-hover: var(--perses-color-red-850); + --perses-status-text-error: var(--perses-color-red-300); + --perses-status-border-error: var(--perses-color-red-700); + --perses-status-icon-error: var(--perses-color-red-400); + + /* Status: Warning */ + --perses-status-bg-warning: var(--perses-color-orange-900); + --perses-status-bg-warning-hover: var(--perses-color-orange-850); + --perses-status-text-warning: var(--perses-color-orange-300); + --perses-status-border-warning: var(--perses-color-orange-700); + --perses-status-icon-warning: var(--perses-color-orange-400); + + /* Status: Success */ + --perses-status-bg-success: var(--perses-color-green-900); + --perses-status-bg-success-hover: var(--perses-color-green-850); + --perses-status-text-success: var(--perses-color-green-300); + --perses-status-border-success: var(--perses-color-green-700); + --perses-status-icon-success: var(--perses-color-green-400); + + /* Status: Info */ + --perses-status-bg-info: var(--perses-color-blue-900); + --perses-status-bg-info-hover: var(--perses-color-blue-850); + --perses-status-text-info: var(--perses-color-blue-300); + --perses-status-border-info: var(--perses-color-blue-700); + --perses-status-icon-info: var(--perses-color-blue-400); + } + + /* ---- Dark mode: system preference fallback ---- */ + @media (prefers-color-scheme: dark) { + :root:where(:not([data-perses-mode='light'])) { + /* Background: Elevation */ + --perses-bg-default: var(--perses-color-gray-900); + --perses-bg-surface: var(--perses-color-gray-850); + --perses-bg-sunken: var(--perses-color-gray-800); + --perses-bg-overlay: var(--perses-color-gray-600); + --perses-bg-backdrop: rgba(10, 12, 16, 0.85); + --perses-bg-navigation: var(--perses-color-gray-850); + + /* Border */ + --perses-border-default: var(--perses-color-gray-600); + + /* Text */ + --perses-text-primary: var(--perses-color-white); + --perses-text-secondary: var(--perses-color-gray-50); + --perses-text-disabled: var(--perses-color-gray-600); + --perses-text-link: var(--perses-color-blue-400); + --perses-text-link-hover: var(--perses-color-blue-500); + --perses-text-navigation: var(--perses-color-white); + --perses-text-accent: var(--perses-color-gray-400); + + /* Status: Primary */ + --perses-status-bg-primary: var(--perses-color-blue-900); + --perses-status-bg-primary-hover: var(--perses-color-blue-850); + --perses-status-text-primary: var(--perses-color-blue-300); + --perses-status-border-primary: var(--perses-color-blue-700); + --perses-status-icon-primary: var(--perses-color-blue-600); + + /* Status: Secondary */ + --perses-status-bg-secondary: var(--perses-color-gray-850); + --perses-status-bg-secondary-hover: var(--perses-color-gray-800); + --perses-status-text-secondary: var(--perses-color-gray-200); + --perses-status-border-secondary: var(--perses-color-gray-700); + --perses-status-icon-secondary: var(--perses-color-gray-400); + + /* Status: Error */ + --perses-status-bg-error: var(--perses-color-red-900); + --perses-status-bg-error-hover: var(--perses-color-red-850); + --perses-status-text-error: var(--perses-color-red-300); + --perses-status-border-error: var(--perses-color-red-700); + --perses-status-icon-error: var(--perses-color-red-400); + + /* Status: Warning */ + --perses-status-bg-warning: var(--perses-color-orange-900); + --perses-status-bg-warning-hover: var(--perses-color-orange-850); + --perses-status-text-warning: var(--perses-color-orange-300); + --perses-status-border-warning: var(--perses-color-orange-700); + --perses-status-icon-warning: var(--perses-color-orange-400); + + /* Status: Success */ + --perses-status-bg-success: var(--perses-color-green-900); + --perses-status-bg-success-hover: var(--perses-color-green-850); + --perses-status-text-success: var(--perses-color-green-300); + --perses-status-border-success: var(--perses-color-green-700); + --perses-status-icon-success: var(--perses-color-green-400); + + /* Status: Info */ + --perses-status-bg-info: var(--perses-color-blue-900); + --perses-status-bg-info-hover: var(--perses-color-blue-850); + --perses-status-text-info: var(--perses-color-blue-300); + --perses-status-border-info: var(--perses-color-blue-700); + --perses-status-icon-info: var(--perses-color-blue-400); + } + } +} diff --git a/design-tokens/src/css/tokens.css b/design-tokens/src/css/tokens.css new file mode 100644 index 00000000..246b3279 --- /dev/null +++ b/design-tokens/src/css/tokens.css @@ -0,0 +1,155 @@ +/* + * Copyright The Perses Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@layer perses.tokens { + :root { + /* ---- Colors: Blue ---- */ + --perses-color-blue-50: #e7f1fc; + --perses-color-blue-100: #d0e3fa; + --perses-color-blue-150: #b8d5f7; + --perses-color-blue-200: #a1c7f5; + --perses-color-blue-300: #72abf0; + --perses-color-blue-400: #438feb; + --perses-color-blue-500: #1473e6; + --perses-color-blue-600: #105cb8; + --perses-color-blue-700: #0c458a; + --perses-color-blue-800: #082e5c; + --perses-color-blue-850: #062345; + --perses-color-blue-900: #04172e; + --perses-color-blue-950: #020c17; + + /* ---- Colors: Green ---- */ + --perses-color-green-50: #eaf9f1; + --perses-color-green-100: #d5f2e3; + --perses-color-green-150: #c1ecd4; + --perses-color-green-200: #ace5c6; + --perses-color-green-300: #82d9aa; + --perses-color-green-400: #59cc8d; + --perses-color-green-500: #2fbf71; + --perses-color-green-600: #26995a; + --perses-color-green-700: #1c7344; + --perses-color-green-800: #134c2d; + --perses-color-green-850: #0e3922; + --perses-color-green-900: #092617; + --perses-color-green-950: #05130b; + + /* ---- Colors: Gray ---- */ + --perses-color-gray-50: #f0f1f6; + --perses-color-gray-100: #e1e3ed; + --perses-color-gray-150: #d2d5e4; + --perses-color-gray-200: #c3c7db; + --perses-color-gray-300: #a4acc8; + --perses-color-gray-400: #8690b6; + --perses-color-gray-500: #717ca4; + --perses-color-gray-600: #535d83; + --perses-color-gray-700: #3e4662; + --perses-color-gray-800: #2a2e42; + --perses-color-gray-850: #1f2331; + --perses-color-gray-900: #151721; + --perses-color-gray-950: #0a0c10; + + /* ---- Colors: Orange ---- */ + --perses-color-orange-50: #fff5e8; + --perses-color-orange-100: #ffecd2; + --perses-color-orange-150: #ffe2bb; + --perses-color-orange-200: #ffd9a4; + --perses-color-orange-300: #ffc577; + --perses-color-orange-400: #ffb249; + --perses-color-orange-500: #ff9f1c; + --perses-color-orange-600: #cc7f16; + --perses-color-orange-700: #995f11; + --perses-color-orange-800: #66400b; + --perses-color-orange-850: #4d3008; + --perses-color-orange-900: #332006; + --perses-color-orange-950: #1a1003; + + /* ---- Colors: Purple ---- */ + --perses-color-purple-50: #efe9fd; + --perses-color-purple-100: #e0d2fc; + --perses-color-purple-150: #d0bcfa; + --perses-color-purple-200: #c1a6f8; + --perses-color-purple-300: #a179f5; + --perses-color-purple-400: #824df1; + --perses-color-purple-500: #6320ee; + --perses-color-purple-600: #4f1abe; + --perses-color-purple-700: #3b138f; + --perses-color-purple-800: #280d5f; + --perses-color-purple-850: #1e0a47; + --perses-color-purple-900: #140630; + --perses-color-purple-950: #0a0318; + + /* ---- Colors: Red ---- */ + --perses-color-red-50: #fdeded; + --perses-color-red-100: #fbdada; + --perses-color-red-150: #f9c8c8; + --perses-color-red-200: #f7b5b5; + --perses-color-red-300: #f29191; + --perses-color-red-400: #ee6c6c; + --perses-color-red-500: #ea4747; + --perses-color-red-600: #bd3939; + --perses-color-red-700: #902b2b; + --perses-color-red-800: #621d1d; + --perses-color-red-850: #4c1616; + --perses-color-red-900: #350f0f; + --perses-color-red-950: #1f0808; + + /* ---- Colors: Common ---- */ + --perses-color-white: #ffffff; + --perses-color-black: #000000; + + /* ---- Spacing (rem) ---- */ + --perses-spacing-0: 0; + --perses-spacing-xs: 0.25rem; + --perses-spacing-sm: 0.5rem; + --perses-spacing-md: 0.75rem; + --perses-spacing-lg: 1rem; + --perses-spacing-xl: 1.25rem; + --perses-spacing-2xl: 1.5rem; + --perses-spacing-3xl: 2rem; + --perses-spacing-4xl: 3rem; + + /* ---- Border Radius ---- */ + --perses-radius-none: 0; + --perses-radius-sm: 2px; + --perses-radius-md: 4px; + --perses-radius-lg: 8px; + --perses-radius-xl: 12px; + --perses-radius-full: 9999px; + + /* ---- Typography ---- */ + --perses-font-family: + Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + + --perses-font-weight-light: 300; + --perses-font-weight-regular: 400; + --perses-font-weight-medium: 600; + --perses-font-weight-bold: 700; + + --perses-font-size-xs: 0.75rem; + --perses-font-size-sm: 0.875rem; + --perses-font-size-md: 1rem; + --perses-font-size-lg: 1.25rem; + --perses-font-size-xl: 1.5rem; + --perses-font-size-2xl: 2rem; + --perses-font-size-3xl: 2.5rem; + --perses-font-size-4xl: 3rem; + + --perses-line-height-tight: 1.2; + --perses-line-height-compact: 1.3; + --perses-line-height-normal: 1.4; + --perses-line-height-relaxed: 1.5; + } +} diff --git a/design-tokens/src/index.ts b/design-tokens/src/index.ts new file mode 100644 index 00000000..d627d721 --- /dev/null +++ b/design-tokens/src/index.ts @@ -0,0 +1,16 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export * from './types'; +export { tokens } from './tokens'; +export { blue, green, gray, orange, purple, red, white, black } from './colors'; diff --git a/design-tokens/src/test/consistency.test.ts b/design-tokens/src/test/consistency.test.ts new file mode 100644 index 00000000..2204fb85 --- /dev/null +++ b/design-tokens/src/test/consistency.test.ts @@ -0,0 +1,60 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { tokens } from '../tokens'; + +const cssDir = resolve(__dirname, '../css'); +const readCss = (f: string): string => readFileSync(resolve(cssDir, f), 'utf-8'); + +function extractCssVarDefinitions(...files: string[]): Set { + const vars = new Set(); + for (const file of files) { + const css = readCss(file); + for (const match of css.matchAll(/^\s*(--perses-[\w-]+)\s*:/gm)) { + vars.add(match[1]!); + } + } + return vars; +} + +function extractTokenVarRefs(obj: Record): Set { + const vars = new Set(); + for (const value of Object.values(obj)) { + if (typeof value === 'string') { + const match = value.match(/^var\((--perses-[\w-]+)\)$/); + if (match) vars.add(match[1]!); + } else if (typeof value === 'object' && value !== null) { + for (const v of extractTokenVarRefs(value as Record)) { + vars.add(v); + } + } + } + return vars; +} + +describe('token ↔ CSS consistency', () => { + const cssVars = extractCssVarDefinitions('tokens.css', 'semantic.css'); + const tokenVars = extractTokenVarRefs(tokens as unknown as Record); + + it('every CSS variable has a corresponding tokens entry', () => { + const missing = [...cssVars].filter((v) => !tokenVars.has(v)).sort(); + expect(missing).toEqual([]); + }); + + it('every tokens entry references a defined CSS variable', () => { + const missing = [...tokenVars].filter((v) => !cssVars.has(v)).sort(); + expect(missing).toEqual([]); + }); +}); diff --git a/design-tokens/src/test/css.test.ts b/design-tokens/src/test/css.test.ts new file mode 100644 index 00000000..c23f0b54 --- /dev/null +++ b/design-tokens/src/test/css.test.ts @@ -0,0 +1,115 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { blue, green, gray, orange, purple, red, white, black, type PersesColor } from '../colors'; + +const cssDir = resolve(__dirname, '../css'); + +const readCss = (filename: string): string => readFileSync(resolve(cssDir, filename), 'utf-8'); + +describe('CSS layer declarations', () => { + it('index.css declares layer order', () => { + const css = readCss('index.css'); + expect(css).toContain('@layer perses.reset, perses.tokens, perses.semantic'); + }); + + it('reset.css uses @layer perses.reset', () => { + const css = readCss('reset.css'); + expect(css).toContain('@layer perses.reset'); + }); + + it('tokens.css uses @layer perses.tokens', () => { + const css = readCss('tokens.css'); + expect(css).toContain('@layer perses.tokens'); + }); + + it('semantic.css uses @layer perses.semantic', () => { + const css = readCss('semantic.css'); + expect(css).toContain('@layer perses.semantic'); + }); +}); + +describe('CSS primitive color variables', () => { + const tokensCss = readCss('tokens.css'); + const tokensCssUpper = tokensCss.toUpperCase(); + + const hues: Array<[string, PersesColor]> = [ + ['blue', blue], + ['green', green], + ['gray', gray], + ['orange', orange], + ['purple', purple], + ['red', red], + ]; + + const stops = [50, 100, 150, 200, 300, 400, 500, 600, 700, 800, 850, 900, 950] as const; + + it.each(hues)('tokens.css defines all %s color variables', (hue, colorObj) => { + for (const stop of stops) { + expect(tokensCss).toContain(`--perses-color-${hue}-${stop}`); + expect(tokensCssUpper).toContain(colorObj[stop].toUpperCase()); + } + }); + + it('tokens.css defines white and black', () => { + expect(tokensCss).toContain('--perses-color-white'); + expect(tokensCss).toContain('--perses-color-black'); + expect(tokensCssUpper).toContain(white.toUpperCase()); + expect(tokensCssUpper).toContain(black.toUpperCase()); + }); +}); + +describe('CSS semantic variables', () => { + const semanticCss = readCss('semantic.css'); + + it('defines light mode defaults on :root', () => { + expect(semanticCss).toContain(':root {'); + expect(semanticCss).toContain('--perses-bg-default'); + expect(semanticCss).toContain('--perses-text-primary'); + }); + + it('defines dark mode via data attribute', () => { + expect(semanticCss).toContain(`[data-perses-mode='dark']`); + }); + + it('has all background semantic tokens', () => { + const bgTokens = ['default', 'surface', 'sunken', 'overlay', 'backdrop', 'navigation']; + for (const name of bgTokens) { + expect(semanticCss).toContain(`--perses-bg-${name}`); + } + }); + + it('has border semantic token', () => { + expect(semanticCss).toContain('--perses-border-default'); + }); + + it('has all text semantic tokens', () => { + const textTokens = ['primary', 'secondary', 'disabled', 'link', 'link-hover', 'navigation', 'accent']; + for (const name of textTokens) { + expect(semanticCss).toContain(`--perses-text-${name}`); + } + }); + + it('has all status tokens with property-scoped naming', () => { + const roles = ['primary', 'secondary', 'error', 'warning', 'success', 'info']; + const properties = ['bg', 'text', 'border', 'icon']; + for (const role of roles) { + for (const prop of properties) { + expect(semanticCss).toContain(`--perses-status-${prop}-${role}`); + } + expect(semanticCss).toContain(`--perses-status-bg-${role}-hover`); + } + }); +}); diff --git a/design-tokens/src/test/setup-tests.ts b/design-tokens/src/test/setup-tests.ts new file mode 100644 index 00000000..7d2273f2 --- /dev/null +++ b/design-tokens/src/test/setup-tests.ts @@ -0,0 +1,12 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/design-tokens/src/test/tokens.test.ts b/design-tokens/src/test/tokens.test.ts new file mode 100644 index 00000000..4caaef09 --- /dev/null +++ b/design-tokens/src/test/tokens.test.ts @@ -0,0 +1,132 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { tokens } from '../tokens'; +import { blue, green, gray, orange, purple, red, white, black } from '../colors'; + +const HEX_PATTERN = /^#[0-9A-Fa-f]{6}$/; + +describe('color constants', () => { + const colorEntries = [ + ['blue', blue], + ['green', green], + ['gray', gray], + ['orange', orange], + ['purple', purple], + ['red', red], + ] as const; + + it.each(colorEntries)('%s has valid hex values for all stops', (_name, color) => { + const stops = [50, 100, 150, 200, 300, 400, 500, 600, 700, 800, 850, 900, 950] as const; + for (const stop of stops) { + expect(color[stop]).toMatch(HEX_PATTERN); + } + }); + + it('white and black are valid hex', () => { + expect(white).toMatch(HEX_PATTERN); + expect(black).toMatch(HEX_PATTERN); + }); +}); + +describe('tokens object', () => { + it('produces correct var() strings for primitive colors', () => { + expect(tokens.color.blue[500]).toBe('var(--perses-color-blue-500)'); + expect(tokens.color.gray[100]).toBe('var(--perses-color-gray-100)'); + expect(tokens.color.red[50]).toBe('var(--perses-color-red-50)'); + expect(tokens.color.white).toBe('var(--perses-color-white)'); + expect(tokens.color.black).toBe('var(--perses-color-black)'); + }); + + it('produces correct var() strings for semantic background tokens', () => { + expect(tokens.bg.default).toBe('var(--perses-bg-default)'); + expect(tokens.bg.surface).toBe('var(--perses-bg-surface)'); + expect(tokens.bg.sunken).toBe('var(--perses-bg-sunken)'); + expect(tokens.bg.overlay).toBe('var(--perses-bg-overlay)'); + expect(tokens.bg.backdrop).toBe('var(--perses-bg-backdrop)'); + expect(tokens.bg.navigation).toBe('var(--perses-bg-navigation)'); + }); + + it('produces correct var() strings for semantic border tokens', () => { + expect(tokens.border.default).toBe('var(--perses-border-default)'); + }); + + it('produces correct var() strings for semantic text tokens', () => { + expect(tokens.text.primary).toBe('var(--perses-text-primary)'); + expect(tokens.text.link).toBe('var(--perses-text-link)'); + expect(tokens.text.disabled).toBe('var(--perses-text-disabled)'); + }); + + it('produces correct var() strings for status tokens', () => { + expect(tokens.status.success.bg).toBe('var(--perses-status-bg-success)'); + expect(tokens.status.success.bgHover).toBe('var(--perses-status-bg-success-hover)'); + expect(tokens.status.success.text).toBe('var(--perses-status-text-success)'); + expect(tokens.status.success.border).toBe('var(--perses-status-border-success)'); + expect(tokens.status.success.icon).toBe('var(--perses-status-icon-success)'); + + expect(tokens.status.error.bg).toBe('var(--perses-status-bg-error)'); + expect(tokens.status.error.text).toBe('var(--perses-status-text-error)'); + expect(tokens.status.warning.border).toBe('var(--perses-status-border-warning)'); + expect(tokens.status.info.icon).toBe('var(--perses-status-icon-info)'); + expect(tokens.status.primary.bg).toBe('var(--perses-status-bg-primary)'); + expect(tokens.status.secondary.bgHover).toBe('var(--perses-status-bg-secondary-hover)'); + }); + + it('has all 6 status roles with 5 properties each', () => { + const roles = ['primary', 'secondary', 'error', 'warning', 'success', 'info'] as const; + const properties = ['bg', 'bgHover', 'text', 'border', 'icon'] as const; + for (const role of roles) { + for (const prop of properties) { + expect(tokens.status[role][prop]).toBeDefined(); + expect(tokens.status[role][prop]).toMatch(/^var\(--perses-status-/); + } + } + }); + + it('produces correct var() strings for spacing tokens', () => { + expect(tokens.spacing[0]).toBe('var(--perses-spacing-0)'); + expect(tokens.spacing.xs).toBe('var(--perses-spacing-xs)'); + expect(tokens.spacing.sm).toBe('var(--perses-spacing-sm)'); + expect(tokens.spacing.md).toBe('var(--perses-spacing-md)'); + expect(tokens.spacing.lg).toBe('var(--perses-spacing-lg)'); + expect(tokens.spacing.xl).toBe('var(--perses-spacing-xl)'); + expect(tokens.spacing['2xl']).toBe('var(--perses-spacing-2xl)'); + expect(tokens.spacing['3xl']).toBe('var(--perses-spacing-3xl)'); + expect(tokens.spacing['4xl']).toBe('var(--perses-spacing-4xl)'); + }); + + it('produces correct var() strings for radius tokens', () => { + expect(tokens.radius.none).toBe('var(--perses-radius-none)'); + expect(tokens.radius.md).toBe('var(--perses-radius-md)'); + expect(tokens.radius.full).toBe('var(--perses-radius-full)'); + }); + + it('produces correct var() strings for typography tokens', () => { + expect(tokens.font.family).toBe('var(--perses-font-family)'); + expect(tokens.font.weight.bold).toBe('var(--perses-font-weight-bold)'); + expect(tokens.font.size.xs).toBe('var(--perses-font-size-xs)'); + expect(tokens.font.size.sm).toBe('var(--perses-font-size-sm)'); + expect(tokens.font.size.md).toBe('var(--perses-font-size-md)'); + expect(tokens.font.size['2xl']).toBe('var(--perses-font-size-2xl)'); + expect(tokens.font.lineHeight.tight).toBe('var(--perses-line-height-tight)'); + expect(tokens.font.lineHeight.compact).toBe('var(--perses-line-height-compact)'); + expect(tokens.font.lineHeight.normal).toBe('var(--perses-line-height-normal)'); + expect(tokens.font.lineHeight.relaxed).toBe('var(--perses-line-height-relaxed)'); + }); + + it('has all expected top-level categories', () => { + expect(Object.keys(tokens)).toEqual( + expect.arrayContaining(['color', 'bg', 'border', 'text', 'status', 'spacing', 'radius', 'font']) + ); + }); +}); diff --git a/design-tokens/src/test/type-assertions.ts b/design-tokens/src/test/type-assertions.ts new file mode 100644 index 00000000..75ab9ff3 --- /dev/null +++ b/design-tokens/src/test/type-assertions.ts @@ -0,0 +1,62 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Compile-time type assertions: if a token var name is missing from +// PersesTokenVar, `npm run type-check` will fail here. +// This file is never executed — it only needs to pass tsc. + +import type { PersesTokenVar } from '../types'; +import { tokens } from '../tokens'; + +type ExtractVar = T extends `var(${infer V})` ? V : never; + +type AssertAssignable = T; + +// Verify that the var() references in the tokens object produce variable names +// that are assignable to PersesTokenVar. If a token references a CSS variable +// not covered by PersesTokenVar, tsc will emit an error on this line. +type _BgVars = AssertAssignable, PersesTokenVar>; +type _BgSurface = AssertAssignable, PersesTokenVar>; +type _BgSunken = AssertAssignable, PersesTokenVar>; +type _BgOverlay = AssertAssignable, PersesTokenVar>; +type _BgBackdrop = AssertAssignable, PersesTokenVar>; +type _BgNav = AssertAssignable, PersesTokenVar>; + +type _BorderDefault = AssertAssignable, PersesTokenVar>; + +type _TextPrimary = AssertAssignable, PersesTokenVar>; +type _TextSecondary = AssertAssignable, PersesTokenVar>; +type _TextDisabled = AssertAssignable, PersesTokenVar>; +type _TextLink = AssertAssignable, PersesTokenVar>; +type _TextLinkHover = AssertAssignable, PersesTokenVar>; +type _TextNav = AssertAssignable, PersesTokenVar>; +type _TextAccent = AssertAssignable, PersesTokenVar>; + +type _SpacingXs = AssertAssignable, PersesTokenVar>; +type _SpacingLg = AssertAssignable, PersesTokenVar>; + +type _RadiusMd = AssertAssignable, PersesTokenVar>; +type _RadiusFull = AssertAssignable, PersesTokenVar>; + +type _FontFamily = AssertAssignable, PersesTokenVar>; +type _FontWeightBold = AssertAssignable, PersesTokenVar>; +type _FontSizeSm = AssertAssignable, PersesTokenVar>; +type _LineHeightTight = AssertAssignable, PersesTokenVar>; + +type _StatusErrorBg = AssertAssignable, PersesTokenVar>; +type _StatusSuccessText = AssertAssignable, PersesTokenVar>; +type _StatusWarningBorder = AssertAssignable, PersesTokenVar>; +type _StatusInfoIcon = AssertAssignable, PersesTokenVar>; + +type _ColorBlue500 = AssertAssignable, PersesTokenVar>; +type _ColorWhite = AssertAssignable, PersesTokenVar>; diff --git a/design-tokens/src/tokens.ts b/design-tokens/src/tokens.ts new file mode 100644 index 00000000..ecfa0d60 --- /dev/null +++ b/design-tokens/src/tokens.ts @@ -0,0 +1,132 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { ColorHue, StatusRole } from './types'; + +const colorScale = (hue: ColorHue) => + ({ + 50: `var(--perses-color-${hue}-50)`, + 100: `var(--perses-color-${hue}-100)`, + 150: `var(--perses-color-${hue}-150)`, + 200: `var(--perses-color-${hue}-200)`, + 300: `var(--perses-color-${hue}-300)`, + 400: `var(--perses-color-${hue}-400)`, + 500: `var(--perses-color-${hue}-500)`, + 600: `var(--perses-color-${hue}-600)`, + 700: `var(--perses-color-${hue}-700)`, + 800: `var(--perses-color-${hue}-800)`, + 850: `var(--perses-color-${hue}-850)`, + 900: `var(--perses-color-${hue}-900)`, + 950: `var(--perses-color-${hue}-950)`, + }) as const; + +const statusRole = (role: StatusRole) => + ({ + bg: `var(--perses-status-bg-${role})`, + bgHover: `var(--perses-status-bg-${role}-hover)`, + text: `var(--perses-status-text-${role})`, + border: `var(--perses-status-border-${role})`, + icon: `var(--perses-status-icon-${role})`, + }) as const; + +export const tokens = { + color: { + blue: colorScale('blue'), + green: colorScale('green'), + gray: colorScale('gray'), + orange: colorScale('orange'), + purple: colorScale('purple'), + red: colorScale('red'), + white: 'var(--perses-color-white)', + black: 'var(--perses-color-black)', + }, + + bg: { + default: 'var(--perses-bg-default)', + surface: 'var(--perses-bg-surface)', + sunken: 'var(--perses-bg-sunken)', + overlay: 'var(--perses-bg-overlay)', + backdrop: 'var(--perses-bg-backdrop)', + navigation: 'var(--perses-bg-navigation)', + }, + + border: { + default: 'var(--perses-border-default)', + }, + + text: { + primary: 'var(--perses-text-primary)', + secondary: 'var(--perses-text-secondary)', + disabled: 'var(--perses-text-disabled)', + link: 'var(--perses-text-link)', + linkHover: 'var(--perses-text-link-hover)', + navigation: 'var(--perses-text-navigation)', + accent: 'var(--perses-text-accent)', + }, + + status: { + primary: statusRole('primary'), + secondary: statusRole('secondary'), + error: statusRole('error'), + warning: statusRole('warning'), + success: statusRole('success'), + info: statusRole('info'), + }, + + spacing: { + 0: 'var(--perses-spacing-0)', + xs: 'var(--perses-spacing-xs)', + sm: 'var(--perses-spacing-sm)', + md: 'var(--perses-spacing-md)', + lg: 'var(--perses-spacing-lg)', + xl: 'var(--perses-spacing-xl)', + '2xl': 'var(--perses-spacing-2xl)', + '3xl': 'var(--perses-spacing-3xl)', + '4xl': 'var(--perses-spacing-4xl)', + }, + + radius: { + none: 'var(--perses-radius-none)', + sm: 'var(--perses-radius-sm)', + md: 'var(--perses-radius-md)', + lg: 'var(--perses-radius-lg)', + xl: 'var(--perses-radius-xl)', + full: 'var(--perses-radius-full)', + }, + + font: { + family: 'var(--perses-font-family)', + weight: { + light: 'var(--perses-font-weight-light)', + regular: 'var(--perses-font-weight-regular)', + medium: 'var(--perses-font-weight-medium)', + bold: 'var(--perses-font-weight-bold)', + }, + size: { + xs: 'var(--perses-font-size-xs)', + sm: 'var(--perses-font-size-sm)', + md: 'var(--perses-font-size-md)', + lg: 'var(--perses-font-size-lg)', + xl: 'var(--perses-font-size-xl)', + '2xl': 'var(--perses-font-size-2xl)', + '3xl': 'var(--perses-font-size-3xl)', + '4xl': 'var(--perses-font-size-4xl)', + }, + lineHeight: { + tight: 'var(--perses-line-height-tight)', + compact: 'var(--perses-line-height-compact)', + normal: 'var(--perses-line-height-normal)', + relaxed: 'var(--perses-line-height-relaxed)', + }, + }, +} as const; diff --git a/design-tokens/src/types.ts b/design-tokens/src/types.ts new file mode 100644 index 00000000..6e633945 --- /dev/null +++ b/design-tokens/src/types.ts @@ -0,0 +1,87 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export type { HexColor, PersesColor } from './colors'; + +export type ColorStop = 50 | 100 | 150 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 850 | 900 | 950; + +export type ColorHue = 'blue' | 'green' | 'gray' | 'orange' | 'purple' | 'red'; + +export type PrimitiveColorVar = `--perses-color-${ColorHue}-${ColorStop}`; + +export type CommonColorVar = '--perses-color-white' | '--perses-color-black'; + +export type SemanticBgVar = + | '--perses-bg-default' + | '--perses-bg-surface' + | '--perses-bg-sunken' + | '--perses-bg-overlay' + | '--perses-bg-backdrop' + | '--perses-bg-navigation'; + +export type SemanticBorderVar = '--perses-border-default'; + +export type SemanticTextVar = + | '--perses-text-primary' + | '--perses-text-secondary' + | '--perses-text-disabled' + | '--perses-text-link' + | '--perses-text-link-hover' + | '--perses-text-navigation' + | '--perses-text-accent'; + +export type StatusRole = 'primary' | 'secondary' | 'error' | 'warning' | 'success' | 'info'; + +export type StatusBgVar = `--perses-status-bg-${StatusRole}` | `--perses-status-bg-${StatusRole}-hover`; + +export type StatusTextVar = `--perses-status-text-${StatusRole}`; + +export type StatusBorderVar = `--perses-status-border-${StatusRole}`; + +export type StatusIconVar = `--perses-status-icon-${StatusRole}`; + +export type SpacingScale = '0' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + +export type SpacingVar = `--perses-spacing-${SpacingScale}`; + +export type RadiusVar = `--perses-radius-${'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'}`; + +export type FontSizeScale = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; + +export type FontSizeVar = `--perses-font-size-${FontSizeScale}`; + +export type LineHeightScale = 'tight' | 'compact' | 'normal' | 'relaxed'; + +export type LineHeightVar = `--perses-line-height-${LineHeightScale}`; + +export type FontVar = + | '--perses-font-family' + | `--perses-font-weight-${'light' | 'regular' | 'medium' | 'bold'}` + | FontSizeVar + | LineHeightVar; + +export type PersesTokenVar = + | PrimitiveColorVar + | CommonColorVar + | SemanticBgVar + | SemanticBorderVar + | SemanticTextVar + | StatusBgVar + | StatusTextVar + | StatusBorderVar + | StatusIconVar + | SpacingVar + | RadiusVar + | FontVar; + +export type PersesMode = 'light' | 'dark'; diff --git a/design-tokens/tsconfig.build.json b/design-tokens/tsconfig.build.json new file mode 100644 index 00000000..e477966e --- /dev/null +++ b/design-tokens/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.stories.*", "**/*.test.*", "**/stories/*", "**/test/*"], + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "preserveWatchOutput": true + } +} diff --git a/design-tokens/tsconfig.json b/design-tokens/tsconfig.json new file mode 100644 index 00000000..806aa79a --- /dev/null +++ b/design-tokens/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + } +} diff --git a/jest.shared.ts b/jest.shared.ts index 3cfbe332..8c6f5e48 100644 --- a/jest.shared.ts +++ b/jest.shared.ts @@ -29,7 +29,7 @@ const config: Config.InitialOptions = { '^use-resize-observer$': 'use-resize-observer/polyfilled', // Tell Jest where other Perses packages live since it doesn't know about project references - '^@perses-dev/(client|components|dashboards|explore|plugin-system)(.*)$': '/../$1/src', + '^@perses-dev/(client|components|dashboards|design-tokens|explore|plugin-system)(.*)$': '/../$1/src', // Configure Jest to handle stylesheets '\\.(css|less)$': '/../stylesMock.js', diff --git a/package-lock.json b/package-lock.json index cf3a1865..b571ef35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "workspaces": [ "components", "dashboards", + "design-tokens", "plugin-system", "explore", "client" @@ -175,6 +176,11 @@ "url": "https://github.com/sponsors/eemeli" } }, + "design-tokens": { + "name": "@perses-dev/design-tokens", + "version": "0.54.0-beta.10", + "license": "Apache-2.0" + }, "explore": { "name": "@perses-dev/explore", "version": "0.54.0-beta.10", @@ -3345,6 +3351,10 @@ "resolved": "dashboards", "link": true }, + "node_modules/@perses-dev/design-tokens": { + "resolved": "design-tokens", + "link": true + }, "node_modules/@perses-dev/explore": { "resolved": "explore", "link": true diff --git a/package.json b/package.json index 5b4acc81..320a5dae 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "workspaces": [ "components", "dashboards", + "design-tokens", "plugin-system", "explore", "client" From 84f3ea9f4df86eda8dcb1fed2f73dbbf495711df Mon Sep 17 00:00:00 2001 From: Jenny Zhu Date: Wed, 8 Jul 2026 16:02:07 -0400 Subject: [PATCH 2/2] [IGNORE] address comments perses/shared pr #198 Signed-off-by: Jenny Zhu --- design-tokens/jest.config.ts | 2 - design-tokens/package.json | 4 +- design-tokens/src/css/semantic.css | 70 +-------------------------- design-tokens/src/test/setup-tests.ts | 12 ----- design-tokens/src/tokens.ts | 2 +- 5 files changed, 5 insertions(+), 85 deletions(-) delete mode 100644 design-tokens/src/test/setup-tests.ts diff --git a/design-tokens/jest.config.ts b/design-tokens/jest.config.ts index 095facc3..02371308 100644 --- a/design-tokens/jest.config.ts +++ b/design-tokens/jest.config.ts @@ -16,8 +16,6 @@ import shared from '../jest.shared'; const jestConfig: Config.InitialOptions = { ...shared, - - setupFilesAfterEnv: [...(shared.setupFilesAfterEnv ?? []), '/src/test/setup-tests.ts'], }; export default jestConfig; diff --git a/design-tokens/package.json b/design-tokens/package.json index 6f120719..fbd18540 100644 --- a/design-tokens/package.json +++ b/design-tokens/package.json @@ -16,9 +16,9 @@ "types": "dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "import": "./dist/index.js", - "require": "./dist/cjs/index.js", - "types": "./dist/index.d.ts" + "require": "./dist/cjs/index.js" }, "./css": "./dist/css/index.css", "./css/reset": "./dist/css/reset.css", diff --git a/design-tokens/src/css/semantic.css b/design-tokens/src/css/semantic.css index 971631df..2f4d37c6 100644 --- a/design-tokens/src/css/semantic.css +++ b/design-tokens/src/css/semantic.css @@ -34,7 +34,7 @@ /* Background: Elevation */ --perses-bg-default: var(--perses-color-white); --perses-bg-surface: var(--perses-color-gray-50); - --perses-bg-sunken: var(--perses-color-gray-100); + --perses-bg-sunken: var(--perses-color-gray-200); --perses-bg-overlay: var(--perses-color-gray-100); --perses-bg-backdrop: rgba(21, 23, 33, 0.75); --perses-bg-navigation: var(--perses-color-blue-150); @@ -121,7 +121,7 @@ --perses-status-bg-primary-hover: var(--perses-color-blue-850); --perses-status-text-primary: var(--perses-color-blue-300); --perses-status-border-primary: var(--perses-color-blue-700); - --perses-status-icon-primary: var(--perses-color-blue-600); + --perses-status-icon-primary: var(--perses-color-blue-400); /* Status: Secondary */ --perses-status-bg-secondary: var(--perses-color-gray-850); @@ -159,70 +159,4 @@ --perses-status-icon-info: var(--perses-color-blue-400); } - /* ---- Dark mode: system preference fallback ---- */ - @media (prefers-color-scheme: dark) { - :root:where(:not([data-perses-mode='light'])) { - /* Background: Elevation */ - --perses-bg-default: var(--perses-color-gray-900); - --perses-bg-surface: var(--perses-color-gray-850); - --perses-bg-sunken: var(--perses-color-gray-800); - --perses-bg-overlay: var(--perses-color-gray-600); - --perses-bg-backdrop: rgba(10, 12, 16, 0.85); - --perses-bg-navigation: var(--perses-color-gray-850); - - /* Border */ - --perses-border-default: var(--perses-color-gray-600); - - /* Text */ - --perses-text-primary: var(--perses-color-white); - --perses-text-secondary: var(--perses-color-gray-50); - --perses-text-disabled: var(--perses-color-gray-600); - --perses-text-link: var(--perses-color-blue-400); - --perses-text-link-hover: var(--perses-color-blue-500); - --perses-text-navigation: var(--perses-color-white); - --perses-text-accent: var(--perses-color-gray-400); - - /* Status: Primary */ - --perses-status-bg-primary: var(--perses-color-blue-900); - --perses-status-bg-primary-hover: var(--perses-color-blue-850); - --perses-status-text-primary: var(--perses-color-blue-300); - --perses-status-border-primary: var(--perses-color-blue-700); - --perses-status-icon-primary: var(--perses-color-blue-600); - - /* Status: Secondary */ - --perses-status-bg-secondary: var(--perses-color-gray-850); - --perses-status-bg-secondary-hover: var(--perses-color-gray-800); - --perses-status-text-secondary: var(--perses-color-gray-200); - --perses-status-border-secondary: var(--perses-color-gray-700); - --perses-status-icon-secondary: var(--perses-color-gray-400); - - /* Status: Error */ - --perses-status-bg-error: var(--perses-color-red-900); - --perses-status-bg-error-hover: var(--perses-color-red-850); - --perses-status-text-error: var(--perses-color-red-300); - --perses-status-border-error: var(--perses-color-red-700); - --perses-status-icon-error: var(--perses-color-red-400); - - /* Status: Warning */ - --perses-status-bg-warning: var(--perses-color-orange-900); - --perses-status-bg-warning-hover: var(--perses-color-orange-850); - --perses-status-text-warning: var(--perses-color-orange-300); - --perses-status-border-warning: var(--perses-color-orange-700); - --perses-status-icon-warning: var(--perses-color-orange-400); - - /* Status: Success */ - --perses-status-bg-success: var(--perses-color-green-900); - --perses-status-bg-success-hover: var(--perses-color-green-850); - --perses-status-text-success: var(--perses-color-green-300); - --perses-status-border-success: var(--perses-color-green-700); - --perses-status-icon-success: var(--perses-color-green-400); - - /* Status: Info */ - --perses-status-bg-info: var(--perses-color-blue-900); - --perses-status-bg-info-hover: var(--perses-color-blue-850); - --perses-status-text-info: var(--perses-color-blue-300); - --perses-status-border-info: var(--perses-color-blue-700); - --perses-status-icon-info: var(--perses-color-blue-400); - } - } } diff --git a/design-tokens/src/test/setup-tests.ts b/design-tokens/src/test/setup-tests.ts deleted file mode 100644 index 7d2273f2..00000000 --- a/design-tokens/src/test/setup-tests.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/design-tokens/src/tokens.ts b/design-tokens/src/tokens.ts index ecfa0d60..1f68128a 100644 --- a/design-tokens/src/tokens.ts +++ b/design-tokens/src/tokens.ts @@ -84,7 +84,7 @@ export const tokens = { }, spacing: { - 0: 'var(--perses-spacing-0)', + '0': 'var(--perses-spacing-0)', xs: 'var(--perses-spacing-xs)', sm: 'var(--perses-spacing-sm)', md: 'var(--perses-spacing-md)',