From 617b931fdd45d3e0701b92808d15587835ba2708 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 14:45:56 +1000 Subject: [PATCH 01/11] refactor(tokens): migrate SCSS variables to CSS custom properties Replace hardcoded SCSS variables with CSS custom properties (--ty-*) across component styles for runtime theming. Remove _tokens.scss in favor of direct custom property usage. Add new theme tokens for color-picker, list, speed-dial, and avatar components. --- apps/docs/guides/customise-theme.md | 78 +++--- apps/docs/guides/customise-theme.zh_CN.md | 78 +++--- .../theme-editor/components/export-dialog.tsx | 6 +- .../theme-editor/constants/default-tokens.ts | 38 +-- .../theme-editor/utils/export-theme.ts | 26 -- packages/charts/src/style/index.scss | 26 +- packages/react/src/avatar/style/_index.scss | 6 +- .../react/src/breadcrumb/style/_index.scss | 2 +- packages/react/src/button/style/_index.scss | 22 +- packages/react/src/calendar/style/_index.scss | 54 ++-- packages/react/src/card/style/_index.scss | 6 +- packages/react/src/cascader/style/_index.scss | 34 +-- packages/react/src/collapse/style/_index.scss | 6 +- .../react/src/color-picker/style/_index.scss | 24 +- .../react/src/descriptions/style/_index.scss | 2 +- .../react/src/input-number/style/_index.scss | 18 +- .../react/src/input-otp/style/_index.scss | 10 +- packages/react/src/input/style/_index.scss | 32 +-- packages/react/src/input/style/_mixin.scss | 2 +- packages/react/src/layout/style/_index.scss | 4 +- packages/react/src/list/style/_index.scss | 24 +- packages/react/src/menu/style/_index.scss | 2 +- .../react/src/native-select/style/_index.scss | 8 +- packages/react/src/popup/style/_index.scss | 2 +- .../react/src/scroll-number/style/_index.scss | 4 +- .../react/src/segmented/style/_index.scss | 8 +- packages/react/src/select/style/_index.scss | 4 +- .../react/src/speed-dial/style/_index.scss | 26 +- .../react/src/statistic/style/_index.scss | 4 +- packages/react/src/table/style/_index.scss | 24 +- packages/react/src/tag/style/_index.scss | 2 +- packages/react/src/tooltip/style/_index.scss | 2 +- packages/react/src/tour/style/_index.scss | 4 +- packages/react/src/tree/style/_index.scss | 2 +- .../react/src/typography/style/_index.scss | 12 +- packages/tokens/.gitignore | 1 + packages/tokens/README.md | 18 +- packages/tokens/css/base.css | 179 ++++++------- packages/tokens/scss/_constants.scss | 80 ++++++ packages/tokens/scss/_normalise.scss | 26 +- packages/tokens/scss/_tokens.scss | 91 ------- packages/tokens/scss/_variables.scss | 252 +----------------- packages/tokens/scss/base.scss | 3 - packages/tokens/scss/themes/_dark.scss | 22 +- packages/tokens/scss/themes/_light.scss | 22 +- 45 files changed, 534 insertions(+), 762 deletions(-) create mode 100644 packages/tokens/.gitignore create mode 100644 packages/tokens/scss/_constants.scss delete mode 100644 packages/tokens/scss/_tokens.scss diff --git a/apps/docs/guides/customise-theme.md b/apps/docs/guides/customise-theme.md index 8acbc816..b3596cca 100755 --- a/apps/docs/guides/customise-theme.md +++ b/apps/docs/guides/customise-theme.md @@ -4,7 +4,7 @@ Tiny UI provides three ways to customise the look and feel: 1. **Theme Editor** — a visual, no-code tool for real-time theming (great for exploration and quick customisation). 2. **Design tokens** — CSS custom properties that power light and dark mode. These are the runtime values every component reads. -3. **SCSS variables** — compile-time variables (sizes, font stacks, border radii, etc.) that can be overridden when you build your own stylesheet. +3. **SCSS constants** — compile-time structural constants (padding, transitions, arrow sizes, etc.) that can be overridden when you build your own stylesheet. ## Theme Editor @@ -14,7 +14,7 @@ The built-in [Theme Editor](/theme/theme-editor) lets you visually customise des - Adjust primary, success, warning, danger, and info colours, background, text, and border colours. - Tweak typography (font size, line height, font weight) and details (border radius, spacing, sizing). - Preview changes live on real components. -- Export your customised tokens as CSS or SCSS to use in your project. +- Export your customised tokens as CSS or JSON to use in your project. Changes are applied instantly via CSS custom properties — no rebuild required. @@ -55,7 +55,7 @@ The hook returns: ## Design tokens (CSS custom properties) -Every colour, shadow, and visual state is exposed as a `--ty-*` CSS custom property on `:root`. You can override any token in your own stylesheet: +Every colour, shadow, and visual state is exposed as a `--ty-*` CSS custom property on `:root`. This is the **primary way** to customise Tiny UI. You can override any token in your own stylesheet: ```css :root { @@ -65,6 +65,16 @@ Every colour, shadow, and visual state is exposed as a `--ty-*` CSS custom prope } ``` +For dark mode overrides, target the dark theme selector: + +```css +html[data-tiny-theme='dark'] { + --ty-color-primary: #3d9bff; + --ty-color-primary-hover: #66b3ff; + --ty-color-primary-active: #007bff; +} +``` + ### Commonly used tokens | Token | Light default | Description | @@ -76,16 +86,21 @@ Every colour, shadow, and visual state is exposed as a `--ty-*` CSS custom prope | `--ty-color-text` | `rgba(0,0,0,0.85)` | Primary text colour | | `--ty-color-text-secondary` | `rgba(0,0,0,0.65)` | Secondary text colour | | `--ty-color-border` | `#d9d9d9` | Default border colour | +| `--ty-border-radius` | `2px` | Global border radius | +| `--ty-font-size-base` | `1rem` | Base font size | +| `--ty-height-sm` | `24px` | Small control height | +| `--ty-height-md` | `32px` | Medium control height | +| `--ty-height-lg` | `42px` | Large control height | -The full list of tokens can be found in the source: +Every component also has its own tokens for fine-grained control. For example, Button uses `--ty-btn-default-bg`, `--ty-btn-default-color`, etc. The full list of tokens can be found in the source: - [Light theme tokens](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_light.scss) - [Dark theme tokens](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_dark.scss) -## SCSS variables +## SCSS constants -If you import Tiny UI's SCSS source instead of the pre-built CSS, you can override compile-time variables such as sizes, spacing, font stacks, and border radii. Every variable uses the `!default` flag, so your overrides take precedence. +If you import Tiny UI's SCSS source instead of the pre-built CSS, you can override compile-time structural constants such as padding, transitions, and arrow sizes. These are values that don't need to change at runtime. -> **What's `!default`?** A Sass variable with `!default` is only assigned if it hasn't already been defined. By declaring your value *before* importing Tiny UI's styles, your value wins. +Every constant uses the `!default` flag, so your overrides take precedence. ### 1. Install Sass @@ -95,13 +110,13 @@ $ npm install sass --save-dev ### 2. Create your overrides file -Create a file, e.g. `theme-variables.scss`. Your overrides **must come before** the Tiny UI import: +Create a file, e.g. `theme-overrides.scss`. Your overrides **must come before** the Tiny UI import: ```scss -// Your overrides -$primary-color: #007bff; -$border-radius: 4px; -$font-size-base: 14px; +// Override structural constants +$btn-padding-md: 0 20px; +$card-body-padding: 20px; +$tooltip-arrow-size: 6px; // Import Tiny UI styles (applies your overrides via !default) @use "@tiny-design/react/es/style/index" as *; @@ -110,32 +125,27 @@ $font-size-base: 14px; ### 3. Import in your entry file ```js -import './theme-variables.scss'; +import './theme-overrides.scss'; ``` -The full list of SCSS variables can be found in [_variables.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_variables.scss). +The full list of SCSS constants can be found in [_constants.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_constants.scss). -Some commonly overridden variables: +Some commonly overridden constants: ```scss -// Color -$primary-color: #6e41bf !default; - -// Font -$font-size-base: 1rem !default; -$font-size-lg: $font-size-base * 1.25 !default; -$font-size-sm: $font-size-base * 0.875 !default; -$font-weight: 400 !default; - -// Border -$border-radius: 2px !default; -$border-width: 1px !default; -$border-color: $gray-300 !default; - -// Component sizes -$height-sm: 24px !default; -$height-md: 32px !default; -$height-lg: 42px !default; +// Button +$btn-padding-sm: 0 10px !default; +$btn-padding-md: 0 15px !default; +$btn-padding-lg: 0 28px !default; + +// Card +$card-header-padding: 13px 16px !default; +$card-body-padding: 16px !default; + +// Notification +$notification-width: 380px !default; ``` -Please report an issue if the existing list of variables is not enough for you. +> **Note:** Colours, font sizes, border radii, shadows, and all other visual tokens should be customised via CSS custom properties (see above), not SCSS variables. SCSS constants are only for structural values like padding and sizing. + +Please report an issue if the existing list of tokens or constants is not enough for you. diff --git a/apps/docs/guides/customise-theme.zh_CN.md b/apps/docs/guides/customise-theme.zh_CN.md index ae343ba0..3799458f 100644 --- a/apps/docs/guides/customise-theme.zh_CN.md +++ b/apps/docs/guides/customise-theme.zh_CN.md @@ -4,7 +4,7 @@ Tiny UI 提供三种方式来定制外观: 1. **主题编辑器** — 一个可视化的实时主题工具,无需编写代码(非常适合探索和快速定制)。 2. **设计令牌(Design tokens)** — 驱动亮色/暗色模式的 CSS 自定义属性,所有组件在运行时读取这些值。 -3. **SCSS 变量** — 编译时变量(尺寸、字体、圆角等),可在构建自定义样式表时覆盖。 +3. **SCSS 常量** — 编译时结构常量(内边距、过渡动画、箭头尺寸等),可在构建自定义样式表时覆盖。 ## 主题编辑器 @@ -14,7 +14,7 @@ Tiny UI 提供三种方式来定制外观: - 调整主色、成功色、警告色、危险色和信息色,以及背景色、文本色和边框色。 - 调整排版(字号、行高、字重)和细节(圆角、间距、尺寸)。 - 在真实组件上实时预览更改效果。 -- 导出自定义的令牌为 CSS 或 SCSS,在你的项目中使用。 +- 导出自定义的令牌为 CSS 或 JSON,在你的项目中使用。 更改通过 CSS 自定义属性即时生效 — 无需重新构建。 @@ -55,7 +55,7 @@ const App = () => { ## 设计令牌(CSS 自定义属性) -所有颜色、阴影和视觉状态都以 `--ty-*` CSS 自定义属性的形式暴露在 `:root` 上。你可以在自己的样式表中覆盖任意令牌: +所有颜色、阴影和视觉状态都以 `--ty-*` CSS 自定义属性的形式暴露在 `:root` 上。这是定制 Tiny UI 的**主要方式**。你可以在自己的样式表中覆盖任意令牌: ```css :root { @@ -65,6 +65,16 @@ const App = () => { } ``` +暗色模式下的覆盖,使用暗色主题选择器: + +```css +html[data-tiny-theme='dark'] { + --ty-color-primary: #3d9bff; + --ty-color-primary-hover: #66b3ff; + --ty-color-primary-active: #007bff; +} +``` + ### 常用令牌 | 令牌 | 亮色默认值 | 说明 | @@ -76,16 +86,21 @@ const App = () => { | `--ty-color-text` | `rgba(0,0,0,0.85)` | 主文本色 | | `--ty-color-text-secondary` | `rgba(0,0,0,0.65)` | 次要文本色 | | `--ty-color-border` | `#d9d9d9` | 默认边框色 | +| `--ty-border-radius` | `2px` | 全局圆角 | +| `--ty-font-size-base` | `1rem` | 基础字号 | +| `--ty-height-sm` | `24px` | 小尺寸控件高度 | +| `--ty-height-md` | `32px` | 中尺寸控件高度 | +| `--ty-height-lg` | `42px` | 大尺寸控件高度 | -完整的令牌列表请参考源码: +每个组件也有自己的令牌,用于细粒度控制。例如,Button 使用 `--ty-btn-default-bg`、`--ty-btn-default-color` 等。完整的令牌列表请参考源码: - [亮色主题令牌](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_light.scss) - [暗色主题令牌](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_dark.scss) -## SCSS 变量 +## SCSS 常量 -如果你引入的是 Tiny UI 的 SCSS 源文件而非预编译的 CSS,可以覆盖编译时变量,如尺寸、间距、字体和圆角等。每个变量都使用了 `!default` 标志,因此你的覆盖值会优先生效。 +如果你引入的是 Tiny UI 的 SCSS 源文件而非预编译的 CSS,可以覆盖编译时结构常量,如内边距、过渡动画和箭头尺寸。这些是不需要在运行时变化的值。 -> **什么是 `!default`?** 带有 `!default` 的 Sass 变量仅在尚未定义时才会赋值。在引入 Tiny UI 样式*之前*声明你的值,你的值就会生效。 +每个常量都使用了 `!default` 标志,因此你的覆盖值会优先生效。 ### 1. 安装 Sass @@ -95,13 +110,13 @@ $ npm install sass --save-dev ### 2. 创建覆盖文件 -创建一个文件,例如 `theme-variables.scss`。你的覆盖值**必须写在** Tiny UI 引入语句之前: +创建一个文件,例如 `theme-overrides.scss`。你的覆盖值**必须写在** Tiny UI 引入语句之前: ```scss -// 你的覆盖值 -$primary-color: #007bff; -$border-radius: 4px; -$font-size-base: 14px; +// 覆盖结构常量 +$btn-padding-md: 0 20px; +$card-body-padding: 20px; +$tooltip-arrow-size: 6px; // 引入 Tiny UI 样式(通过 !default 应用你的覆盖值) @use "@tiny-design/react/es/style/index" as *; @@ -110,32 +125,27 @@ $font-size-base: 14px; ### 3. 在入口文件中引入 ```js -import './theme-variables.scss'; +import './theme-overrides.scss'; ``` -完整的 SCSS 变量列表请参考 [_variables.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_variables.scss)。 +完整的 SCSS 常量列表请参考 [_constants.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_constants.scss)。 -以下是一些常用的可覆盖变量: +以下是一些常用的可覆盖常量: ```scss -// 颜色 -$primary-color: #6e41bf !default; - -// 字体 -$font-size-base: 1rem !default; -$font-size-lg: $font-size-base * 1.25 !default; -$font-size-sm: $font-size-base * 0.875 !default; -$font-weight: 400 !default; - -// 边框 -$border-radius: 2px !default; -$border-width: 1px !default; -$border-color: $gray-300 !default; - -// 组件尺寸 -$height-sm: 24px !default; -$height-md: 32px !default; -$height-lg: 42px !default; +// 按钮 +$btn-padding-sm: 0 10px !default; +$btn-padding-md: 0 15px !default; +$btn-padding-lg: 0 28px !default; + +// 卡片 +$card-header-padding: 13px 16px !default; +$card-body-padding: 16px !default; + +// 通知 +$notification-width: 380px !default; ``` -如果现有的变量列表无法满足你的需求,请提交 issue 反馈。 +> **注意:** 颜色、字号、圆角、阴影等所有视觉令牌应通过 CSS 自定义属性定制(见上方),而非 SCSS 变量。SCSS 常量仅用于内边距、尺寸等结构性值。 + +如果现有的令牌或常量列表无法满足你的需求,请提交 issue 反馈。 diff --git a/apps/docs/src/containers/theme-editor/components/export-dialog.tsx b/apps/docs/src/containers/theme-editor/components/export-dialog.tsx index 31b34883..fcde808d 100644 --- a/apps/docs/src/containers/theme-editor/components/export-dialog.tsx +++ b/apps/docs/src/containers/theme-editor/components/export-dialog.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Button, Modal, Tabs } from '@tiny-design/react'; -import { generateCSS, generateSCSS, generateJSON } from '../utils/export-theme'; +import { generateCSS, generateJSON } from '../utils/export-theme'; interface ExportDialogProps { visible: boolean; @@ -28,7 +28,6 @@ export const ExportDialog = ({ const [copied, setCopied] = useState(false); const cssCode = generateCSS(appliedTokens); - const scssCode = generateSCSS(seeds); const jsonCode = generateJSON(seeds); const handleCopy = (code: string) => { @@ -71,9 +70,6 @@ export const ExportDialog = ({ {renderBlock(cssCode, 'tiny-theme.css', 'text/css')} - - {renderBlock(scssCode, 'tiny-theme.scss', 'text/x-scss')} - {renderBlock(jsonCode, 'tiny-theme.json', 'application/json')} diff --git a/apps/docs/src/containers/theme-editor/constants/default-tokens.ts b/apps/docs/src/containers/theme-editor/constants/default-tokens.ts index ec0b2836..94dd3fcd 100644 --- a/apps/docs/src/containers/theme-editor/constants/default-tokens.ts +++ b/apps/docs/src/containers/theme-editor/constants/default-tokens.ts @@ -6,7 +6,7 @@ export interface TokenDef { labelZh: string; type: TokenType; defaultValue: string; - scssVar?: string; + options?: { label: string; value: string }[]; min?: number; max?: number; @@ -22,7 +22,7 @@ export const COLOR_TOKENS: TokenDef[] = [ labelZh: '主色', type: 'color', defaultValue: '#6e41bf', - scssVar: '$primary-color', + }, { key: 'color-success', @@ -30,7 +30,7 @@ export const COLOR_TOKENS: TokenDef[] = [ labelZh: '成功色', type: 'color', defaultValue: '#52c41a', - scssVar: '$success-color', + }, { key: 'color-warning', @@ -38,7 +38,7 @@ export const COLOR_TOKENS: TokenDef[] = [ labelZh: '警告色', type: 'color', defaultValue: '#ff9800', - scssVar: '$warning-color', + }, { key: 'color-danger', @@ -46,7 +46,7 @@ export const COLOR_TOKENS: TokenDef[] = [ labelZh: '危险色', type: 'color', defaultValue: '#f44336', - scssVar: '$danger-color', + }, { key: 'color-info', @@ -54,7 +54,7 @@ export const COLOR_TOKENS: TokenDef[] = [ labelZh: '信息色', type: 'color', defaultValue: '#1890ff', - scssVar: '$info-color', + }, { key: 'color-bg', @@ -88,7 +88,7 @@ export const FONT_TOKENS: TokenDef[] = [ type: 'font', defaultValue: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif', - scssVar: '$font-family-sans-serif', + }, { key: 'font-family-monospace', @@ -97,7 +97,7 @@ export const FONT_TOKENS: TokenDef[] = [ type: 'font', defaultValue: '"Lucida Console", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace', - scssVar: '$font-family-monospace', + }, ]; @@ -109,7 +109,7 @@ export const TYPOGRAPHY_TOKENS: TokenDef[] = [ labelZh: '基础字号', type: 'size', defaultValue: '1rem', - scssVar: '$font-size-base', + min: 12, max: 20, step: 1, @@ -121,7 +121,7 @@ export const TYPOGRAPHY_TOKENS: TokenDef[] = [ labelZh: '小号字号', type: 'size', defaultValue: '0.875rem', - scssVar: '$font-size-sm', + min: 10, max: 16, step: 1, @@ -133,7 +133,7 @@ export const TYPOGRAPHY_TOKENS: TokenDef[] = [ labelZh: '大号字号', type: 'size', defaultValue: '1.25rem', - scssVar: '$font-size-lg', + min: 14, max: 24, step: 1, @@ -145,7 +145,7 @@ export const TYPOGRAPHY_TOKENS: TokenDef[] = [ labelZh: '字重', type: 'select', defaultValue: '400', - scssVar: '$font-weight', + options: [ { label: 'Light (300)', value: '300' }, { label: 'Regular (400)', value: '400' }, @@ -160,7 +160,7 @@ export const TYPOGRAPHY_TOKENS: TokenDef[] = [ labelZh: '行高', type: 'number', defaultValue: '1.5', - scssVar: '$line-height-base', + min: 1, max: 2.5, step: 0.1, @@ -171,7 +171,7 @@ export const TYPOGRAPHY_TOKENS: TokenDef[] = [ labelZh: '标题字重', type: 'select', defaultValue: '500', - scssVar: '$headings-font-weight', + options: [ { label: 'Regular (400)', value: '400' }, { label: 'Medium (500)', value: '500' }, @@ -200,7 +200,7 @@ export const DETAIL_TOKENS: TokenDef[] = [ labelZh: '圆角', type: 'size', defaultValue: '2px', - scssVar: '$border-radius', + min: 0, max: 20, step: 1, @@ -212,7 +212,7 @@ export const DETAIL_TOKENS: TokenDef[] = [ labelZh: '小尺寸高度', type: 'size', defaultValue: '24px', - scssVar: '$height-sm', + min: 20, max: 36, step: 2, @@ -224,7 +224,7 @@ export const DETAIL_TOKENS: TokenDef[] = [ labelZh: '中尺寸高度', type: 'size', defaultValue: '32px', - scssVar: '$height-md', + min: 28, max: 44, step: 2, @@ -236,7 +236,7 @@ export const DETAIL_TOKENS: TokenDef[] = [ labelZh: '大尺寸高度', type: 'size', defaultValue: '42px', - scssVar: '$height-lg', + min: 36, max: 56, step: 2, @@ -269,7 +269,7 @@ export const SPACING_TOKENS: TokenDef[] = [ labelZh: '基础间距', type: 'size', defaultValue: '16px', - scssVar: '$spacer', + min: 8, max: 24, step: 2, diff --git a/apps/docs/src/containers/theme-editor/utils/export-theme.ts b/apps/docs/src/containers/theme-editor/utils/export-theme.ts index 01099994..1354e99b 100644 --- a/apps/docs/src/containers/theme-editor/utils/export-theme.ts +++ b/apps/docs/src/containers/theme-editor/utils/export-theme.ts @@ -1,5 +1,3 @@ -import { ALL_TOKENS } from '../constants/default-tokens'; - /** Keys that should be excluded from CSS variable export (they are meta-seeds, not real tokens) */ const META_KEYS = new Set(['shadow-intensity']); @@ -12,30 +10,6 @@ export function generateCSS(overrides: Record): string { return `:root {\n${lines}\n}`; } -export function generateSCSS(seeds: Record): string { - const scssMap = new Map(); - for (const def of ALL_TOKENS) { - if (def.scssVar) { - scssMap.set(def.key, def.scssVar); - } - } - - const lines: string[] = []; - for (const [key, value] of Object.entries(seeds)) { - if (META_KEYS.has(key)) continue; - const scssVar = scssMap.get(key); - if (scssVar) { - lines.push(`${scssVar}: ${value};`); - } - } - - if (lines.length === 0) { - return '// No SCSS variable overrides'; - } - - return `// Override these before importing @tiny-design/tokens\n${lines.join('\n')}`; -} - export function generateJSON(seeds: Record): string { const clean: Record = {}; for (const [key, value] of Object.entries(seeds)) { diff --git a/packages/charts/src/style/index.scss b/packages/charts/src/style/index.scss index 61356808..cf5346c4 100644 --- a/packages/charts/src/style/index.scss +++ b/packages/charts/src/style/index.scss @@ -1,5 +1,3 @@ -@use '@tiny-design/tokens/scss/tokens' as *; - // ---- Chart Container ---- .ty-chart { position: relative; @@ -8,12 +6,12 @@ // Recharts text elements inherit chart-friendly styles .recharts-cartesian-axis-tick-value { - font-size: $token-font-size-sm; - fill: $token-color-text-secondary; + font-size: var(--ty-font-size-sm); + fill: var(--ty-color-text-secondary); } .recharts-cartesian-grid line { - stroke: $token-color-border-light; + stroke: var(--ty-color-border-light); } } @@ -31,25 +29,25 @@ } .ty-chart-tooltip { - background-color: $token-color-bg-elevated; + background-color: var(--ty-color-bg-elevated); backdrop-filter: blur(12px); - border: 1px solid $token-color-border-light; + border: 1px solid var(--ty-color-border-light); border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.03); padding: 10px 14px; - font-size: $token-font-size-sm; + font-size: var(--ty-font-size-sm); min-width: 140px; line-height: 1.5; &__label { font-weight: 600; - color: $token-color-text; + color: var(--ty-color-text); margin-bottom: 6px; padding-bottom: 6px; - border-bottom: 1px solid $token-color-border-light; + border-bottom: 1px solid var(--ty-color-border-light); letter-spacing: -0.01em; } @@ -103,14 +101,14 @@ display: flex; align-items: center; gap: 4px; - color: $token-color-text-secondary; + color: var(--ty-color-text-secondary); white-space: nowrap; } &__item-value { font-weight: 600; font-variant-numeric: tabular-nums; - color: $token-color-text; + color: var(--ty-color-text); font-feature-settings: 'tnum'; } } @@ -121,7 +119,7 @@ flex-wrap: wrap; justify-content: center; gap: 16px; - font-size: $token-font-size-sm; + font-size: var(--ty-font-size-sm); &_top { padding-bottom: 8px; @@ -146,6 +144,6 @@ } &__label { - color: $token-color-text-secondary; + color: var(--ty-color-text-secondary); } } diff --git a/packages/react/src/avatar/style/_index.scss b/packages/react/src/avatar/style/_index.scss index 6ac7cb2b..2867ee62 100755 --- a/packages/react/src/avatar/style/_index.scss +++ b/packages/react/src/avatar/style/_index.scss @@ -6,8 +6,8 @@ justify-content: center; align-items: center; text-align: center; - background: $avatar-bg; - color: $avatar-color; + background: var(--ty-avatar-bg); + color: var(--ty-avatar-color); white-space: nowrap; position: relative; vertical-align: middle; @@ -48,7 +48,7 @@ } &_offline { - background-color: $gray-400; + background-color: var(--ty-avatar-offline-color); } } diff --git a/packages/react/src/breadcrumb/style/_index.scss b/packages/react/src/breadcrumb/style/_index.scss index f8f55d30..82000651 100755 --- a/packages/react/src/breadcrumb/style/_index.scss +++ b/packages/react/src/breadcrumb/style/_index.scss @@ -8,7 +8,7 @@ display: flex; align-items: center; color: var(--ty-color-text-tertiary); - font-size: $breadcrumb-font-size; + font-size: var(--ty-font-size-base); } &-item { diff --git a/packages/react/src/button/style/_index.scss b/packages/react/src/button/style/_index.scss index 90ed5dfc..0368810c 100755 --- a/packages/react/src/button/style/_index.scss +++ b/packages/react/src/button/style/_index.scss @@ -19,9 +19,9 @@ $btn-prefix: #{$prefix}-btn; text-decoration: none; white-space: nowrap; user-select: none; - border-radius: $btn-border-radius; + border-radius: var(--ty-border-radius); transition: $btn-transition; - line-height: $btn-line-height; + line-height: var(--ty-line-height-base); &__loader { @include loader; @@ -31,7 +31,7 @@ $btn-prefix: #{$prefix}-btn; display: inline-block; flex-shrink: 0; pointer-events: none; - line-height: $btn-line-height; + line-height: var(--ty-line-height-base); vertical-align: middle; & + span { @@ -117,15 +117,15 @@ $btn-prefix: #{$prefix}-btn; // Sizes &_sm { - @include btn-size($btn-padding-sm, $btn-font-size-sm, $btn-height-sm); + @include btn-size($btn-padding-sm, var(--ty-font-size-sm), var(--ty-height-sm)); } &_md { - @include btn-size($btn-padding-md, $btn-font-size-md, $btn-height-md); + @include btn-size($btn-padding-md, var(--ty-font-size-base), var(--ty-height-md)); } &_lg { - @include btn-size($btn-padding-lg, $btn-font-size-lg, $btn-height-lg); + @include btn-size($btn-padding-lg, var(--ty-font-size-lg), var(--ty-height-lg)); } &_block { @@ -137,7 +137,7 @@ $btn-prefix: #{$prefix}-btn; } &_round { - border-radius: $btn-height-lg; + border-radius: var(--ty-height-lg); } &_loading { @@ -183,13 +183,13 @@ $btn-prefix: #{$prefix}-btn; } &:first-child { - border-top-left-radius: $btn-border-radius; - border-bottom-left-radius: $btn-border-radius; + border-top-left-radius: var(--ty-border-radius); + border-bottom-left-radius: var(--ty-border-radius); } &:last-child { - border-top-right-radius: $btn-border-radius; - border-bottom-right-radius: $btn-border-radius; + border-top-right-radius: var(--ty-border-radius); + border-bottom-right-radius: var(--ty-border-radius); } } diff --git a/packages/react/src/calendar/style/_index.scss b/packages/react/src/calendar/style/_index.scss index f7a0819a..8191de2f 100644 --- a/packages/react/src/calendar/style/_index.scss +++ b/packages/react/src/calendar/style/_index.scss @@ -1,8 +1,8 @@ @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-calendar { - background: var(--ty-calendar-bg, #fff); - border: 1px solid var(--ty-calendar-border, #{$gray-200}); + background: var(--ty-calendar-bg); + border: 1px solid var(--ty-calendar-border); border-radius: var(--ty-border-radius); outline: none; @@ -21,7 +21,7 @@ align-items: center; justify-content: space-between; padding: 8px 12px; - border-bottom: 1px solid var(--ty-calendar-border, #{$gray-200}); + border-bottom: 1px solid var(--ty-calendar-border); } &__header-nav { @@ -69,7 +69,7 @@ font-size: inherit; padding: 2px 6px; border-radius: 4px; - color: var(--ty-color-text, #{$gray-800}); + color: var(--ty-color-text); transition: color 0.2s; &:hover { @@ -96,14 +96,14 @@ text-align: center; font-weight: 500; font-size: var(--ty-font-size-sm); - color: var(--ty-color-text-secondary, #{$gray-600}); + color: var(--ty-color-text-secondary); } // ── Week number ───────────────────────────────────────────────────────── &__week-number-header { width: 32px; - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-quaternary); font-weight: 400; font-size: 12px; } @@ -111,7 +111,7 @@ &__week-number { text-align: center; font-size: 12px; - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-quaternary); padding: 4px 0; user-select: none; } @@ -134,7 +134,7 @@ } &:not(&_disabled):hover .#{$prefix}-calendar__cell-inner { - background: var(--ty-calendar-hover, #{$gray-100}); + background: var(--ty-calendar-hover); } &_selected:not(&_disabled):hover .#{$prefix}-calendar__cell-inner { @@ -142,11 +142,11 @@ } &_in-view:not(&_disabled) { - color: var(--ty-color-text, #{$gray-900}); + color: var(--ty-color-text); } &:not(&_in-view) { - color: var(--ty-color-text-quaternary, #{$gray-300}); + color: var(--ty-color-text-quaternary); .#{$prefix}-calendar__cell-dot { opacity: 0.4; @@ -159,7 +159,7 @@ } &_selected .#{$prefix}-calendar__cell-inner { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); color: #fff; border-radius: var(--ty-border-radius); } @@ -171,27 +171,27 @@ // ── Range selection ───────────────────────────────────────────────── &_in-range { - background: var(--ty-color-primary-bg, rgba($primary-color, 0.08)); + background: var(--ty-color-primary-bg); } &_range-start { border-radius: var(--ty-border-radius) 0 0 var(--ty-border-radius); - background: var(--ty-color-primary-bg, rgba($primary-color, 0.08)); + background: var(--ty-color-primary-bg); } &_range-start .#{$prefix}-calendar__cell-inner { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); color: #fff; border-radius: var(--ty-border-radius); } &_range-end { border-radius: 0 var(--ty-border-radius) var(--ty-border-radius) 0; - background: var(--ty-color-primary-bg, rgba($primary-color, 0.08)); + background: var(--ty-color-primary-bg); } &_range-end .#{$prefix}-calendar__cell-inner { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); color: #fff; border-radius: var(--ty-border-radius); } @@ -204,7 +204,7 @@ &_focused .#{$prefix}-calendar__cell-inner, &:focus-visible .#{$prefix}-calendar__cell-inner { - outline: 2px solid var(--ty-color-primary, #{$primary-color}); + outline: 2px solid var(--ty-color-primary); outline-offset: 1px; } } @@ -250,7 +250,7 @@ width: 6px; height: 6px; border-radius: 50%; - background-color: var(--ty-color-primary, #{$primary-color}); + background-color: var(--ty-color-primary); } // ── Month panel (year mode) ───────────────────────────────────────────── @@ -270,15 +270,15 @@ transition: all 0.2s; &:hover { - background: var(--ty-calendar-hover, #{$gray-100}); + background: var(--ty-calendar-hover); } &_selected { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); color: #fff; &:hover { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); opacity: 0.9; } } @@ -313,21 +313,21 @@ font-size: var(--ty-font-size-base); &:hover { - background: var(--ty-calendar-hover, #{$gray-100}); + background: var(--ty-calendar-hover); } &_selected { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); color: #fff; &:hover { - background: var(--ty-color-primary, #{$primary-color}); + background: var(--ty-color-primary); opacity: 0.9; } } &_out { - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-quaternary); } } @@ -335,7 +335,7 @@ &__footer { padding: 8px 12px; - border-top: 1px solid var(--ty-calendar-border, #{$gray-200}); + border-top: 1px solid var(--ty-calendar-border); text-align: center; } @@ -348,4 +348,4 @@ opacity: 0.8; } } -} \ No newline at end of file +} diff --git a/packages/react/src/card/style/_index.scss b/packages/react/src/card/style/_index.scss index 1a307a3b..34cd32e4 100644 --- a/packages/react/src/card/style/_index.scss +++ b/packages/react/src/card/style/_index.scss @@ -5,12 +5,12 @@ box-sizing: border-box; padding: 0; margin: 0; - border-radius: $card-border-radius; + border-radius: var(--ty-border-radius); transition: all 0.3s; background-color: var(--ty-card-bg); & > img:first-child { - border-radius: $card-border-radius $card-border-radius 0 0; + border-radius: var(--ty-border-radius) var(--ty-border-radius) 0 0; } &_outlined { @@ -47,7 +47,7 @@ font-size: 16px; background: transparent; border-bottom: 1px solid var(--ty-card-border); - border-radius: $card-border-radius $card-border-radius 0 0; + border-radius: var(--ty-border-radius) var(--ty-border-radius) 0 0; } &__body { diff --git a/packages/react/src/cascader/style/_index.scss b/packages/react/src/cascader/style/_index.scss index c2aecc38..fd50c0dd 100644 --- a/packages/react/src/cascader/style/_index.scss +++ b/packages/react/src/cascader/style/_index.scss @@ -32,21 +32,21 @@ display: flex; align-items: center; width: 100%; - border: 1px solid var(--ty-cascader-border, #{$gray-300}); + border: 1px solid var(--ty-cascader-border); border-radius: var(--ty-border-radius); - background: var(--ty-cascader-bg, #fff); + background: var(--ty-cascader-bg); cursor: pointer; transition: all 0.2s; position: relative; &:hover { - border-color: var(--ty-color-primary, #{$primary-color}); + border-color: var(--ty-color-primary); } } &_open &__selector { - border-color: var(--ty-color-primary, #{$primary-color}); - box-shadow: 0 0 0 2px rgba($primary-color, 0.1); + border-color: var(--ty-color-primary); + box-shadow: var(--ty-input-focus-shadow); } &__display { @@ -54,18 +54,18 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: var(--ty-color-text, #{$gray-800}); + color: var(--ty-color-text); } &__placeholder { - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-placeholder); } &__clear { position: absolute; right: 24px; font-size: 12px; - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-tertiary); cursor: pointer; display: none; @@ -74,7 +74,7 @@ } &:hover { - color: var(--ty-color-text, #{$gray-600}); + color: var(--ty-color-text-secondary); } } @@ -82,7 +82,7 @@ position: absolute; right: 8px; font-size: 12px; - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-tertiary); transition: transform 0.2s; display: inline-flex; align-items: center; @@ -94,7 +94,7 @@ } &__dropdown { - background: var(--ty-cascader-dropdown-bg, #fff); + background: var(--ty-cascader-dropdown-bg); border-radius: var(--ty-border-radius); box-shadow: $select-dropdown-shadow; font-size: var(--ty-font-size-base); @@ -113,13 +113,13 @@ overflow-y: auto; &:not(:last-child) { - border-right: 1px solid var(--ty-cascader-border, #{$gray-200}); + border-right: 1px solid var(--ty-cascader-border); } } &__menu-empty { padding: 8px 12px; - color: var(--ty-color-text-secondary, #{$gray-500}); + color: var(--ty-color-text-secondary); text-align: center; } @@ -132,13 +132,13 @@ transition: background 0.15s; &:hover:not(&_disabled) { - background: var(--ty-cascader-hover, #{$gray-100}); + background: var(--ty-cascader-hover); } &_active { - color: var(--ty-color-primary, #{$primary-color}); + color: var(--ty-color-primary); font-weight: $select-selected-font-weight; - background: var(--ty-cascader-selected-bg, rgba($primary-color, 0.06)); + background: var(--ty-cascader-selected-bg); } &_disabled { @@ -157,6 +157,6 @@ &__menu-item-arrow { margin-left: 8px; font-size: 12px; - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-tertiary); } } diff --git a/packages/react/src/collapse/style/_index.scss b/packages/react/src/collapse/style/_index.scss index 609ad677..53aebf48 100755 --- a/packages/react/src/collapse/style/_index.scss +++ b/packages/react/src/collapse/style/_index.scss @@ -2,7 +2,7 @@ .#{$prefix}-collapse { box-sizing: border-box; - border-radius: $collapse-border-radius; + border-radius: var(--ty-border-radius); color: var(--ty-color-text); font-size: 14px; border: 1px solid var(--ty-collapse-border); @@ -15,10 +15,10 @@ border-bottom: 1px solid var(--ty-collapse-border); &:last-child { - border-radius: 0 0 $collapse-border-radius $collapse-border-radius; + border-radius: 0 0 var(--ty-border-radius) var(--ty-border-radius); .#{$prefix}-collapse-item__content { - border-radius: 0 0 $collapse-border-radius $collapse-border-radius; + border-radius: 0 0 var(--ty-border-radius) var(--ty-border-radius); } } diff --git a/packages/react/src/color-picker/style/_index.scss b/packages/react/src/color-picker/style/_index.scss index 45db4054..e62ac489 100644 --- a/packages/react/src/color-picker/style/_index.scss +++ b/packages/react/src/color-picker/style/_index.scss @@ -24,7 +24,7 @@ width: 32px; height: 32px; border-radius: var(--ty-border-radius); - border: 1px solid var(--ty-color-picker-border, #{$gray-300}); + border: 1px solid var(--ty-color-picker-border); padding: 3px; cursor: pointer; } @@ -37,7 +37,7 @@ &__panel { padding: 12px; - background: var(--ty-color-picker-bg, #fff); + background: var(--ty-color-picker-bg); border-radius: 8px; box-shadow: 0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%); width: 240px; @@ -86,7 +86,7 @@ width: 28px; height: 28px; border-radius: 50%; - border: 1px solid var(--ty-color-picker-border, #{$gray-300}); + border: 1px solid var(--ty-color-picker-border); flex-shrink: 0; } @@ -116,7 +116,7 @@ position: absolute; inset: 0; border-radius: 6px; - background: repeating-conic-gradient(#{$gray-300} 0% 25%, transparent 0% 50%) 0 0 / 8px 8px; + background: repeating-conic-gradient(var(--ty-color-border) 0% 25%, transparent 0% 50%) 0 0 / 8px 8px; z-index: -1; } } @@ -142,33 +142,33 @@ } &__format-btn { - border: 1px solid var(--ty-color-picker-border, #{$gray-300}); + border: 1px solid var(--ty-color-picker-border); background: transparent; border-radius: var(--ty-border-radius); padding: 2px 6px; cursor: pointer; font-size: 12px; - color: var(--ty-color-text, #{$gray-700}); + color: var(--ty-color-text); white-space: nowrap; &:hover { - border-color: var(--ty-color-primary, #{$primary-color}); + border-color: var(--ty-color-primary); } } &__hex-input { flex: 1; - border: 1px solid var(--ty-color-picker-border, #{$gray-300}); + border: 1px solid var(--ty-color-picker-border); border-radius: var(--ty-border-radius); padding: 2px 6px; font-size: 12px; font-family: var(--ty-font-family-monospace); - color: var(--ty-color-text, #{$gray-800}); + color: var(--ty-color-text); outline: none; min-width: 0; &:focus { - border-color: var(--ty-color-primary, #{$primary-color}); + border-color: var(--ty-color-primary); } } @@ -178,7 +178,7 @@ gap: 6px; margin-top: 12px; padding-top: 12px; - border-top: 1px solid var(--ty-color-picker-border, #{$gray-200}); + border-top: 1px solid var(--ty-color-picker-border); } &__preset { @@ -186,7 +186,7 @@ height: 20px; border-radius: var(--ty-border-radius); cursor: pointer; - border: 1px solid var(--ty-color-picker-border, #{$gray-300}); + border: 1px solid var(--ty-color-picker-border); transition: transform 0.15s; &:hover { diff --git a/packages/react/src/descriptions/style/_index.scss b/packages/react/src/descriptions/style/_index.scss index a4fc7523..8833125b 100644 --- a/packages/react/src/descriptions/style/_index.scss +++ b/packages/react/src/descriptions/style/_index.scss @@ -46,7 +46,7 @@ .#{$prefix}-descriptions { &__body { border: 1px solid var(--ty-descriptions-border); - border-radius: $description-border-radius; + border-radius: var(--ty-border-radius); } &__row { diff --git a/packages/react/src/input-number/style/_index.scss b/packages/react/src/input-number/style/_index.scss index 68c04650..b10cf528 100755 --- a/packages/react/src/input-number/style/_index.scss +++ b/packages/react/src/input-number/style/_index.scss @@ -84,9 +84,9 @@ &_sm { .#{$prefix}-input-number { &__input { - font-size: $input-sm-font-size; - height: $input-sm-height; - line-height: $input-sm-height; + font-size: var(--ty-font-size-sm); + height: var(--ty-height-sm); + line-height: var(--ty-height-sm); } } } @@ -94,9 +94,9 @@ &_md { .#{$prefix}-input-number { &__input { - font-size: $input-md-font-size; - height: $input-md-height; - line-height: $input-md-height; + font-size: var(--ty-font-size-base); + height: var(--ty-height-md); + line-height: var(--ty-height-md); } } } @@ -104,9 +104,9 @@ &_lg { .#{$prefix}-input-number { &__input { - font-size: $input-lg-font-size; - height: $input-lg-height; - line-height: $input-lg-height; + font-size: var(--ty-font-size-lg); + height: var(--ty-height-lg); + line-height: var(--ty-height-lg); } } } diff --git a/packages/react/src/input-otp/style/_index.scss b/packages/react/src/input-otp/style/_index.scss index b1ea666a..38a9d9fc 100644 --- a/packages/react/src/input-otp/style/_index.scss +++ b/packages/react/src/input-otp/style/_index.scss @@ -13,26 +13,26 @@ height: 36px; text-align: center; padding: 0; - font-size: $input-md-font-size; - border-radius: $input-border-radius; + font-size: var(--ty-font-size-base); + border-radius: var(--ty-border-radius); caret-color: currentcolor; &_sm { width: 28px; height: 28px; - font-size: $input-sm-font-size; + font-size: var(--ty-font-size-sm); } &_md { width: 36px; height: 36px; - font-size: $input-md-font-size; + font-size: var(--ty-font-size-base); } &_lg { width: 44px; height: 44px; - font-size: $input-lg-font-size; + font-size: var(--ty-font-size-lg); } &_disabled { diff --git a/packages/react/src/input/style/_index.scss b/packages/react/src/input/style/_index.scss index 8aacd2e4..9c133647 100755 --- a/packages/react/src/input/style/_index.scss +++ b/packages/react/src/input/style/_index.scss @@ -41,9 +41,9 @@ &_sm { .#{$prefix}-input { &__input { - font-size: $input-sm-font-size; - height: $input-sm-height; - line-height: $input-sm-height; + font-size: var(--ty-font-size-sm); + height: var(--ty-height-sm); + line-height: var(--ty-height-sm); } &__clear-btn { @@ -55,9 +55,9 @@ &_md { .#{$prefix}-input { &__input { - font-size: $input-md-font-size; - height: $input-md-height; - line-height: $input-md-height; + font-size: var(--ty-font-size-base); + height: var(--ty-height-md); + line-height: var(--ty-height-md); } &__clear-btn { @@ -69,9 +69,9 @@ &_lg { .#{$prefix}-input { &__input { - font-size: $input-lg-font-size; - height: $input-lg-height; - line-height: $input-lg-height; + font-size: var(--ty-font-size-lg); + height: var(--ty-height-lg); + line-height: var(--ty-height-lg); } } } @@ -96,15 +96,15 @@ } &_sm { - height: $input-sm-height; + height: var(--ty-height-sm); } &_md { - height: $input-md-height; + height: var(--ty-height-md); } &_lg { - height: $input-lg-height; + height: var(--ty-height-lg); } .#{$prefix}-input { @@ -139,20 +139,20 @@ box-sizing: border-box; text-align: center; line-height: 1; - border-radius: $input-border-radius; + border-radius: var(--ty-border-radius); color: var(--ty-color-text); padding: 0 7px; &_sm { - font-size: $input-sm-font-size; + font-size: var(--ty-font-size-sm); } &_md { - font-size: $input-md-font-size; + font-size: var(--ty-font-size-base); } &_lg { - font-size: $input-lg-font-size; + font-size: var(--ty-font-size-lg); } &:first-child { diff --git a/packages/react/src/input/style/_mixin.scss b/packages/react/src/input/style/_mixin.scss index dff7c129..1556bebd 100755 --- a/packages/react/src/input/style/_mixin.scss +++ b/packages/react/src/input/style/_mixin.scss @@ -9,7 +9,7 @@ border: 1px solid var(--ty-input-border); transition: all 0.3s; outline: 0; - border-radius: $input-border-radius; + border-radius: var(--ty-border-radius); font-size: var(--ty-font-size-base); background-color: var(--ty-input-bg); diff --git a/packages/react/src/layout/style/_index.scss b/packages/react/src/layout/style/_index.scss index cc9affb6..74390b16 100755 --- a/packages/react/src/layout/style/_index.scss +++ b/packages/react/src/layout/style/_index.scss @@ -13,13 +13,13 @@ .#{$prefix}-layout-header { box-sizing: border-box; - height: $layout-header-height; + height: 60px; background-color: var(--ty-color-bg-layout); } .#{$prefix}-layout-footer { box-sizing: border-box; - padding: $layout-footer-padding; + padding: 24px 50px; background-color: var(--ty-color-bg-layout); } diff --git a/packages/react/src/list/style/_index.scss b/packages/react/src/list/style/_index.scss index 1a537493..22e9b52c 100644 --- a/packages/react/src/list/style/_index.scss +++ b/packages/react/src/list/style/_index.scss @@ -1,11 +1,11 @@ @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-list { - color: var(--ty-color-text, #{$gray-800}); + color: var(--ty-color-text); font-size: var(--ty-font-size-base); &_bordered { - border: 1px solid var(--ty-list-border, #{$gray-300}); + border: 1px solid var(--ty-list-border); border-radius: var(--ty-border-radius); } @@ -33,11 +33,11 @@ } &_bordered &__header { - border-bottom: 1px solid var(--ty-list-border, #{$gray-300}); + border-bottom: 1px solid var(--ty-list-border); } &_bordered &__footer { - border-top: 1px solid var(--ty-list-border, #{$gray-300}); + border-top: 1px solid var(--ty-list-border); } &__body_virtual { @@ -45,7 +45,7 @@ will-change: transform; .#{$prefix}-list-item:last-child { - border-bottom: 1px solid var(--ty-list-border, #{$gray-200}); + border-bottom: 1px solid var(--ty-list-border); } } @@ -59,13 +59,13 @@ &__empty { padding: 24px; text-align: center; - color: var(--ty-color-text-secondary, #{$gray-500}); + color: var(--ty-color-text-secondary); } &__loading { padding: 24px; text-align: center; - color: var(--ty-color-text-secondary, #{$gray-500}); + color: var(--ty-color-text-secondary); } } @@ -75,7 +75,7 @@ align-items: flex-start; .#{$prefix}-list_split & { - border-bottom: 1px solid var(--ty-list-border, #{$gray-200}); + border-bottom: 1px solid var(--ty-list-border); &:last-child { border-bottom: none; @@ -106,11 +106,11 @@ } &__action { - color: var(--ty-color-text-secondary, #{$gray-600}); + color: var(--ty-color-text-secondary); cursor: pointer; &:hover { - color: var(--ty-color-primary, #{$primary-color}); + color: var(--ty-color-primary); } } @@ -137,12 +137,12 @@ &__title { font-weight: 500; - color: var(--ty-color-text, #{$gray-900}); + color: var(--ty-color-text); margin-bottom: 4px; } &__description { - color: var(--ty-color-text-secondary, #{$gray-600}); + color: var(--ty-color-text-secondary); font-size: var(--ty-font-size-sm); } } diff --git a/packages/react/src/menu/style/_index.scss b/packages/react/src/menu/style/_index.scss index 4b45baba..857171fb 100644 --- a/packages/react/src/menu/style/_index.scss +++ b/packages/react/src/menu/style/_index.scss @@ -94,7 +94,7 @@ } &_disabled { - color: $gray-700 !important; + color: var(--ty-color-text-secondary) !important; opacity: .5; cursor: not-allowed; } diff --git a/packages/react/src/native-select/style/_index.scss b/packages/react/src/native-select/style/_index.scss index 59767dd5..7960bb3a 100755 --- a/packages/react/src/native-select/style/_index.scss +++ b/packages/react/src/native-select/style/_index.scss @@ -14,7 +14,7 @@ $select-arrow: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcm vertical-align: middle; box-sizing: border-box; border: 1px solid var(--ty-input-border); - border-radius: $native-select-border-radius; + border-radius: var(--ty-border-radius); background-color: var(--ty-native-select-bg); background-image: url($select-arrow); background-repeat: no-repeat, repeat; @@ -52,14 +52,14 @@ $select-arrow: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcm } &_sm { - @include native-size($native-select-sm-padding, $native-select-sm-font-size); + @include native-size($native-select-sm-padding, var(--ty-font-size-sm)); } &_md { - @include native-size($native-select-md-padding, $native-select-md-font-size); + @include native-size($native-select-md-padding, var(--ty-font-size-base)); } &_lg { - @include native-size($native-select-lg-padding, $native-select-lg-font-size); + @include native-size($native-select-lg-padding, var(--ty-font-size-lg)); } } diff --git a/packages/react/src/popup/style/_index.scss b/packages/react/src/popup/style/_index.scss index 96dcdef9..b29e7f8a 100755 --- a/packages/react/src/popup/style/_index.scss +++ b/packages/react/src/popup/style/_index.scss @@ -3,7 +3,7 @@ .#{$prefix}-popup { box-sizing: border-box; - border-radius: $popover-border-radius; + border-radius: var(--ty-border-radius); white-space: nowrap; font-size: var(--ty-font-size-base); text-align: left; diff --git a/packages/react/src/scroll-number/style/_index.scss b/packages/react/src/scroll-number/style/_index.scss index 18378c85..7516aa69 100644 --- a/packages/react/src/scroll-number/style/_index.scss +++ b/packages/react/src/scroll-number/style/_index.scss @@ -6,14 +6,14 @@ &__title { margin-bottom: 4px; - color: var(--ty-color-text-secondary, #{$gray-600}); + color: var(--ty-color-text-secondary); font-size: var(--ty-font-size-sm); } &__content { display: flex; align-items: baseline; - color: var(--ty-color-text, #{$gray-900}); + color: var(--ty-color-text); font-size: 24px; font-weight: 600; font-family: var(--ty-font-family); diff --git a/packages/react/src/segmented/style/_index.scss b/packages/react/src/segmented/style/_index.scss index 9c79bd8b..73dd20bf 100644 --- a/packages/react/src/segmented/style/_index.scss +++ b/packages/react/src/segmented/style/_index.scss @@ -4,7 +4,7 @@ display: inline-flex; align-items: center; padding: 2px; - background: var(--ty-segmented-bg, #{$gray-200}); + background: var(--ty-segmented-bg); border-radius: var(--ty-border-radius); box-sizing: border-box; @@ -58,12 +58,12 @@ white-space: nowrap; &:hover:not(&_active, &_disabled) { - color: var(--ty-color-text, #{$gray-900}); + color: var(--ty-color-text); } &_active { - background: var(--ty-segmented-active-bg, #fff); - color: var(--ty-color-text, #{$gray-900}); + background: var(--ty-segmented-active-bg); + color: var(--ty-color-text); box-shadow: 0 1px 2px 0 rgb(0 0 0 / 6%), 0 1px 3px 0 rgb(0 0 0 / 10%); font-weight: 500; } diff --git a/packages/react/src/select/style/_index.scss b/packages/react/src/select/style/_index.scss index 9a8282e5..081fe4f2 100644 --- a/packages/react/src/select/style/_index.scss +++ b/packages/react/src/select/style/_index.scss @@ -61,7 +61,7 @@ display: flex; align-items: center; width: 100%; - border: $border-width solid var(--ty-input-border); + border: 1px solid var(--ty-input-border); border-radius: var(--ty-border-radius); background-color: var(--ty-input-bg); box-sizing: border-box; @@ -286,7 +286,7 @@ &__title { font-size: var(--ty-font-size-sm); cursor: default; - color: $gray-600; + color: var(--ty-color-text-secondary); padding: 7px 12px; } diff --git a/packages/react/src/speed-dial/style/_index.scss b/packages/react/src/speed-dial/style/_index.scss index d70a53d1..11df0667 100644 --- a/packages/react/src/speed-dial/style/_index.scss +++ b/packages/react/src/speed-dial/style/_index.scss @@ -17,20 +17,20 @@ $speed-dial-actions-gap: 16px; height: $speed-dial-fab-size; border-radius: 50%; border: none; - background-color: var(--ty-speed-dial-bg, var(--ty-color-primary)); - color: var(--ty-speed-dial-color, #fff); + background-color: var(--ty-speed-dial-bg); + color: var(--ty-speed-dial-color); font-size: 24px; cursor: pointer; - box-shadow: $box-shadow; + box-shadow: var(--ty-shadow); transition: background-color 0.2s ease; outline: none; &:hover:not(.#{$prefix}-speed-dial__button_disabled) { - background-color: var(--ty-speed-dial-bg-hover, var(--ty-color-primary-active)); + background-color: var(--ty-speed-dial-bg-hover); } &:focus-visible { - box-shadow: 0 0 0 3px var(--ty-input-focus-shadow, rgb(110 65 191 / 20%)), $box-shadow; + box-shadow: 0 0 0 3px var(--ty-input-focus-shadow), var(--ty-shadow); } &_open { @@ -168,21 +168,21 @@ $speed-dial-actions-gap: 16px; height: $speed-dial-action-size; border-radius: 50%; border: none; - background-color: var(--ty-speed-dial-action-bg, #{$white-color}); - color: var(--ty-speed-dial-action-color, #{$gray-800}); + background-color: var(--ty-speed-dial-action-bg); + color: var(--ty-speed-dial-action-color); font-size: 16px; cursor: pointer; - box-shadow: $box-shadow-sm; + box-shadow: var(--ty-shadow-sm); transition: background-color 0.2s ease, box-shadow 0.2s ease; outline: none; &:hover:not(.#{$prefix}-speed-dial__action_disabled) { - background-color: var(--ty-speed-dial-action-bg-hover, #{$gray-100}); - box-shadow: $box-shadow; + background-color: var(--ty-speed-dial-action-bg-hover); + box-shadow: var(--ty-shadow); } &:focus-visible { - box-shadow: 0 0 0 3px var(--ty-input-focus-shadow, rgb(110 65 191 / 20%)), $box-shadow-sm; + box-shadow: 0 0 0 3px var(--ty-input-focus-shadow), var(--ty-shadow-sm); } &_disabled { @@ -195,8 +195,8 @@ $speed-dial-actions-gap: 16px; &__action-tooltip { position: absolute; white-space: nowrap; - background-color: var(--ty-speed-dial-tooltip-bg, #{$gray-800}); - color: var(--ty-speed-dial-tooltip-color, #{$white-color}); + background-color: var(--ty-speed-dial-tooltip-bg); + color: var(--ty-speed-dial-tooltip-color); font-size: var(--ty-font-size-sm); padding: 4px 8px; border-radius: var(--ty-border-radius); diff --git a/packages/react/src/statistic/style/_index.scss b/packages/react/src/statistic/style/_index.scss index 76dd693b..02c06965 100644 --- a/packages/react/src/statistic/style/_index.scss +++ b/packages/react/src/statistic/style/_index.scss @@ -3,14 +3,14 @@ .#{$prefix}-statistic { &__title { margin-bottom: 4px; - color: var(--ty-color-text-secondary, #{$gray-600}); + color: var(--ty-color-text-secondary); font-size: var(--ty-font-size-sm); } &__content { display: flex; align-items: baseline; - color: var(--ty-color-text, #{$gray-900}); + color: var(--ty-color-text); font-size: 24px; font-weight: 600; font-family: var(--ty-font-family); diff --git a/packages/react/src/table/style/_index.scss b/packages/react/src/table/style/_index.scss index b36084f6..0be3ad7b 100644 --- a/packages/react/src/table/style/_index.scss +++ b/packages/react/src/table/style/_index.scss @@ -1,7 +1,7 @@ @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-table { - color: var(--ty-color-text, #{$gray-800}); + color: var(--ty-color-text); font-size: var(--ty-font-size-base); &__wrapper { @@ -15,11 +15,11 @@ } &_bordered &__table { - border: 1px solid var(--ty-table-border, #{$gray-300}); + border: 1px solid var(--ty-table-border); } &_bordered &__cell { - border: 1px solid var(--ty-table-border, #{$gray-300}); + border: 1px solid var(--ty-table-border); } // Sizes @@ -53,9 +53,9 @@ // Header &__thead { .#{$prefix}-table__cell { - background: var(--ty-table-header-bg, #{$gray-100}); + background: var(--ty-table-header-bg); font-weight: 500; - border-bottom: 1px solid var(--ty-table-border, #{$gray-300}); + border-bottom: 1px solid var(--ty-table-border); } } @@ -69,7 +69,7 @@ user-select: none; &:hover { - background: var(--ty-table-hover, #{$gray-200}); + background: var(--ty-table-hover); } } @@ -91,15 +91,15 @@ // Rows &__tbody &__row { - border-bottom: 1px solid var(--ty-table-border, #{$gray-200}); + border-bottom: 1px solid var(--ty-table-border); transition: background 0.2s; &:hover { - background: var(--ty-table-hover, #{$gray-100}); + background: var(--ty-table-hover); } &_selected { - background: var(--ty-table-selected-bg, rgb(110 65 191 / 6%)); + background: var(--ty-table-selected-bg); } } @@ -125,10 +125,10 @@ &__sorter-icon { font-size: 8px; line-height: 8px; - color: var(--ty-color-text-secondary, #{$gray-400}); + color: var(--ty-color-text-quaternary); &_active { - color: var(--ty-color-primary, #{$primary-color}); + color: var(--ty-color-primary); } } @@ -137,7 +137,7 @@ &__loading-cell { text-align: center; padding: 32px !important; - color: var(--ty-color-text-secondary, #{$gray-500}); + color: var(--ty-color-text-secondary); } } diff --git a/packages/react/src/tag/style/_index.scss b/packages/react/src/tag/style/_index.scss index 9d80c7b4..7a2beb2b 100755 --- a/packages/react/src/tag/style/_index.scss +++ b/packages/react/src/tag/style/_index.scss @@ -11,7 +11,7 @@ $tag-status-colors: success, info, warning, danger; padding: 3px 7px; font-size: 12px; border: 1px solid var(--ty-tag-border); - border-radius: $tag-border-radius; + border-radius: var(--ty-border-radius); color: var(--ty-color-text); background: var(--ty-tag-bg); diff --git a/packages/react/src/tooltip/style/_index.scss b/packages/react/src/tooltip/style/_index.scss index 088d584b..1744fa19 100755 --- a/packages/react/src/tooltip/style/_index.scss +++ b/packages/react/src/tooltip/style/_index.scss @@ -2,7 +2,7 @@ @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-tooltip { - font-size: $tooltip-font-size; + font-size: var(--ty-font-size-sm); &__inner { padding: $tooltip-content-padding; diff --git a/packages/react/src/tour/style/_index.scss b/packages/react/src/tour/style/_index.scss index c216a2b5..dc26a2aa 100644 --- a/packages/react/src/tour/style/_index.scss +++ b/packages/react/src/tour/style/_index.scss @@ -55,7 +55,7 @@ $arrow-size: 8px; // Panel &__panel { position: relative; - border-radius: $popover-border-radius; + border-radius: var(--ty-border-radius); box-shadow: var(--ty-shadow-modal); max-width: 360px; min-width: 260px; @@ -173,7 +173,7 @@ $arrow-size: 8px; img { max-width: 100%; - border-radius: $popover-border-radius $popover-border-radius 0 0; + border-radius: var(--ty-border-radius) var(--ty-border-radius) 0 0; } } diff --git a/packages/react/src/tree/style/_index.scss b/packages/react/src/tree/style/_index.scss index 1ddc61d3..cf7bf973 100644 --- a/packages/react/src/tree/style/_index.scss +++ b/packages/react/src/tree/style/_index.scss @@ -4,7 +4,7 @@ margin: 0; padding: 0; list-style: none; - font-size: $tree-font-size; + font-size: var(--ty-font-size-base); } .#{$prefix}-tree-node { diff --git a/packages/react/src/typography/style/_index.scss b/packages/react/src/typography/style/_index.scss index 05e4223a..22ac937e 100755 --- a/packages/react/src/typography/style/_index.scss +++ b/packages/react/src/typography/style/_index.scss @@ -66,32 +66,32 @@ h6.#{$tp-prefix} { } h1.#{$tp-prefix} { - font-size: $h1-font-size; + font-size: var(--ty-h1-font-size); line-height: 1.23; } h2.#{$tp-prefix} { - font-size: $h2-font-size; + font-size: var(--ty-h2-font-size); line-height: 1.35; } h3.#{$tp-prefix} { - font-size: $h3-font-size; + font-size: var(--ty-h3-font-size); line-height: 1.35; } h4.#{$tp-prefix} { - font-size: $h4-font-size; + font-size: var(--ty-h4-font-size); line-height: 1.4; } h5.#{$tp-prefix} { - font-size: $h5-font-size; + font-size: var(--ty-h5-font-size); line-height: 1.2; } h6.#{$tp-prefix} { - font-size: $h6-font-size; + font-size: var(--ty-h6-font-size); line-height: 1.2; } diff --git a/packages/tokens/.gitignore b/packages/tokens/.gitignore new file mode 100644 index 00000000..9798131c --- /dev/null +++ b/packages/tokens/.gitignore @@ -0,0 +1 @@ +css/ diff --git a/packages/tokens/README.md b/packages/tokens/README.md index a65a5256..6dfa2610 100644 --- a/packages/tokens/README.md +++ b/packages/tokens/README.md @@ -27,8 +27,7 @@ import '@tiny-design/tokens/css/base.css'; Import individual SCSS modules for custom builds: ```scss -@use '@tiny-design/tokens/scss/variables'; -@use '@tiny-design/tokens/scss/tokens'; +@use '@tiny-design/tokens/scss/variables'; // $prefix, breakpoints, structural constants @use '@tiny-design/tokens/scss/animation'; @use '@tiny-design/tokens/scss/mixins'; ``` @@ -37,8 +36,8 @@ Import individual SCSS modules for custom builds: | Module | Description | | --- | --- | -| `_variables.scss` | Core variables — colors, typography, spacing, breakpoints, component dimensions | -| `_tokens.scss` | CSS custom property wrappers (`--ty-*` prefix) | +| `_variables.scss` | `$prefix`, responsive breakpoints, and `@forward` of structural constants | +| `_constants.scss` | Compile-time structural constants — padding, sizing, transitions | | `_theme.scss` | Theme generation (light/dark via `data-tiny-theme` attribute) | | `_normalise.scss` | HTML normalization (based on Normalize.css) | | `_animation.scss` | Keyframe animations (`ty-rotate`, `ty-rotate-reverse`, `ty-processing`) | @@ -46,7 +45,16 @@ Import individual SCSS modules for custom builds: ## Theming -Tokens supports light and dark themes via the `data-tiny-theme` attribute on the document root: +All visual tokens are delivered as CSS custom properties (`--ty-*`). Override them in your stylesheet: + +```css +:root { + --ty-color-primary: #007bff; + --ty-border-radius: 4px; +} +``` + +Light and dark themes are supported via the `data-tiny-theme` attribute on the document root: ```html diff --git a/packages/tokens/css/base.css b/packages/tokens/css/base.css index e6201338..68e1ed03 100644 --- a/packages/tokens/css/base.css +++ b/packages/tokens/css/base.css @@ -53,10 +53,10 @@ --ty-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); --ty-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); --ty-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); - --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.12) , 0 6px 16px 0 rgba(0, 0, 0, 0.08) , 0 9px 28px 8px rgba(0, 0, 0, 0.05); --ty-shadow-card: 0 1px 6px rgba(0, 0, 0, 0.12); --ty-shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.15); - --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.15) , 0 1px 1px rgba(0, 0, 0, 0.075); --ty-color-overlay-bg: rgba(0, 0, 0, 0.55); --ty-color-overlay-inverted: rgba(255, 255, 255, 0.75); --ty-btn-default-color: #32325d; @@ -195,6 +195,17 @@ --ty-picker-cell-selected-hover-bg: #5a30a8; --ty-picker-cell-disabled-bg: #f5f5f5; --ty-picker-clear-bg: #fff; + --ty-color-picker-bg: #fff; + --ty-color-picker-border: #dee2e6; + --ty-list-border: #dee2e6; + --ty-speed-dial-bg: #6e41bf; + --ty-speed-dial-color: #fff; + --ty-speed-dial-bg-hover: #5a30a8; + --ty-speed-dial-action-bg: #fff; + --ty-speed-dial-action-color: #32325d; + --ty-speed-dial-action-bg-hover: #f6f9fc; + --ty-speed-dial-tooltip-bg: #32325d; + --ty-speed-dial-tooltip-color: #fff; --ty-split-bar-bg: #f8f8f9; --ty-split-bar-border: #dcdee2; --ty-split-bar-line: #d5d5d5; @@ -247,8 +258,11 @@ --ty-carousel-dot-bg: rgba(255, 255, 255, 0.3); --ty-carousel-dot-hover-bg: rgba(255, 255, 255, 0.6); --ty-carousel-dot-active-bg: #fff; + --ty-avatar-bg: #ccc; + --ty-avatar-color: #fff; --ty-avatar-border: #fff; --ty-avatar-presence-shadow: 0 0 0 0.1rem #fff; + --ty-avatar-offline-color: #ced4da; --ty-back-top-bg: rgba(0, 0, 0, 0.3); --ty-input-number-control-border: #d9d9d9; --ty-input-number-control-active-bg: #f4f4f4; @@ -351,10 +365,10 @@ html[data-tiny-theme=dark] { --ty-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3); --ty-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); --ty-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.5); - --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.48), 0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.2); + --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.48) , 0 6px 16px 0 rgba(0, 0, 0, 0.32) , 0 9px 28px 8px rgba(0, 0, 0, 0.2); --ty-shadow-card: 0 1px 6px rgba(0, 0, 0, 0.35); --ty-shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.45); - --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 1px 1px rgba(0, 0, 0, 0.2); + --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.05) , 0 1px 1px rgba(0, 0, 0, 0.2); --ty-color-overlay-bg: rgba(0, 0, 0, 0.65); --ty-color-overlay-inverted: rgba(50, 50, 50, 0.75); --ty-btn-default-color: rgba(255, 255, 255, 0.85); @@ -493,6 +507,17 @@ html[data-tiny-theme=dark] { --ty-picker-cell-selected-hover-bg: #7a50bf; --ty-picker-cell-disabled-bg: #2a2a2a; --ty-picker-clear-bg: #1f1f1f; + --ty-color-picker-bg: #1f1f1f; + --ty-color-picker-border: #424242; + --ty-list-border: #363636; + --ty-speed-dial-bg: #9065d0; + --ty-speed-dial-color: #fff; + --ty-speed-dial-bg-hover: #7a50bf; + --ty-speed-dial-action-bg: #1f1f1f; + --ty-speed-dial-action-color: rgba(255, 255, 255, 0.85); + --ty-speed-dial-action-bg-hover: #2a2a2a; + --ty-speed-dial-tooltip-bg: #363636; + --ty-speed-dial-tooltip-color: rgba(255, 255, 255, 0.85); --ty-split-bar-bg: #262626; --ty-split-bar-border: #424242; --ty-split-bar-line: #525252; @@ -545,8 +570,11 @@ html[data-tiny-theme=dark] { --ty-carousel-dot-bg: rgba(255, 255, 255, 0.3); --ty-carousel-dot-hover-bg: rgba(255, 255, 255, 0.6); --ty-carousel-dot-active-bg: #fff; + --ty-avatar-bg: #555; + --ty-avatar-color: #e8e8e8; --ty-avatar-border: #1f1f1f; --ty-avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f; + --ty-avatar-offline-color: #525252; --ty-back-top-bg: rgba(255, 255, 255, 0.2); --ty-input-number-control-border: #424242; --ty-input-number-control-active-bg: #2a2a2a; @@ -650,10 +678,10 @@ html[data-tiny-theme=dark] { --ty-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3); --ty-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); --ty-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.5); - --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.48), 0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.2); + --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.48) , 0 6px 16px 0 rgba(0, 0, 0, 0.32) , 0 9px 28px 8px rgba(0, 0, 0, 0.2); --ty-shadow-card: 0 1px 6px rgba(0, 0, 0, 0.35); --ty-shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.45); - --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 1px 1px rgba(0, 0, 0, 0.2); + --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.05) , 0 1px 1px rgba(0, 0, 0, 0.2); --ty-color-overlay-bg: rgba(0, 0, 0, 0.65); --ty-color-overlay-inverted: rgba(50, 50, 50, 0.75); --ty-btn-default-color: rgba(255, 255, 255, 0.85); @@ -792,6 +820,17 @@ html[data-tiny-theme=dark] { --ty-picker-cell-selected-hover-bg: #7a50bf; --ty-picker-cell-disabled-bg: #2a2a2a; --ty-picker-clear-bg: #1f1f1f; + --ty-color-picker-bg: #1f1f1f; + --ty-color-picker-border: #424242; + --ty-list-border: #363636; + --ty-speed-dial-bg: #9065d0; + --ty-speed-dial-color: #fff; + --ty-speed-dial-bg-hover: #7a50bf; + --ty-speed-dial-action-bg: #1f1f1f; + --ty-speed-dial-action-color: rgba(255, 255, 255, 0.85); + --ty-speed-dial-action-bg-hover: #2a2a2a; + --ty-speed-dial-tooltip-bg: #363636; + --ty-speed-dial-tooltip-color: rgba(255, 255, 255, 0.85); --ty-split-bar-bg: #262626; --ty-split-bar-border: #424242; --ty-split-bar-line: #525252; @@ -844,8 +883,11 @@ html[data-tiny-theme=dark] { --ty-carousel-dot-bg: rgba(255, 255, 255, 0.3); --ty-carousel-dot-hover-bg: rgba(255, 255, 255, 0.6); --ty-carousel-dot-active-bg: #fff; + --ty-avatar-bg: #555; + --ty-avatar-color: #e8e8e8; --ty-avatar-border: #1f1f1f; --ty-avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f; + --ty-avatar-offline-color: #525252; --ty-back-top-bg: rgba(255, 255, 255, 0.2); --ty-input-number-control-border: #424242; --ty-input-number-control-active-bg: #2a2a2a; @@ -893,7 +935,6 @@ html[data-tiny-theme=dark] { --ty-chart-5: #d32029; } } - /* stylelint-disable scss/comment-no-empty */ /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /** @@ -902,12 +943,10 @@ html[data-tiny-theme=dark] { */ html { font-size: 14px; - line-height: 1.15; - /* 1 */ + line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; - -moz-text-size-adjust: 100%; - text-size-adjust: 100%; - /* 2 */ + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; /* 2 */ } /* Sections @@ -931,40 +970,35 @@ main { display: block; } -h6, -h5, -h4, -h3, -h2, -h1 { +h6, h5, h4, h3, h2, h1 { margin-top: 0; margin-bottom: 0.5rem; - font-weight: 500; + font-weight: var(--ty-headings-font-weight); line-height: 1.2; } h1 { - font-size: 2.5rem; + font-size: var(--ty-h1-font-size); } h2 { - font-size: 2rem; + font-size: var(--ty-h2-font-size); } h3 { - font-size: 1.75rem; + font-size: var(--ty-h3-font-size); } h4 { - font-size: 1.5rem; + font-size: var(--ty-h4-font-size); } h5 { - font-size: 1.25rem; + font-size: var(--ty-h5-font-size); } h6 { - font-size: 1rem; + font-size: var(--ty-h6-font-size); } /* Grouping content @@ -974,12 +1008,9 @@ h6 { * 2. Show the overflow in Edge and IE. */ hr { - box-sizing: content-box; - /* 1 */ - height: 0; - /* 1 */ - overflow: visible; - /* 2 */ + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ } /* Text-level semantics @@ -993,14 +1024,12 @@ a { text-decoration: none; cursor: pointer; } - a:hover { color: var(--ty-color-primary-hover); text-decoration: underline; } -a:not([href]), -a:not([href]):hover { +a:not([href]), a:not([href]):hover { text-decoration: none; } @@ -1009,13 +1038,10 @@ a:not([href]):hover { * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ abbr[title] { - border-bottom: none; - /* 1 */ - text-decoration: underline; - /* 2 */ + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - /* 2 */ + text-decoration: underline dotted; /* 2 */ } /** @@ -1034,10 +1060,8 @@ pre, code, kbd, samp { - font-family: var(--ty-font-family-monospace); - /* 1 */ - font-size: 1em; - /* 2 */ + font-family: var(--ty-font-family-monospace); /* 1 */ + font-size: 1em; /* 2 */ } /** @@ -1087,14 +1111,10 @@ input, optgroup, select, textarea { - font-family: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - line-height: 1.15; - /* 1 */ - margin: 0; - /* 2 */ + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ } /** @@ -1102,8 +1122,7 @@ textarea { * 1. Show the overflow in Edge. */ button, -input { - /* 1 */ +input { /* 1 */ overflow: visible; } @@ -1112,8 +1131,7 @@ input { * 1. Remove the inheritance of text transform in Firefox. */ button, -select { - /* 1 */ +select { /* 1 */ text-transform: none; } @@ -1125,8 +1143,8 @@ button, [type=reset], [type=submit] { -webkit-appearance: button; - -moz-appearance: button; - appearance: button; + -moz-appearance: button; + appearance: button; } /** @@ -1164,18 +1182,12 @@ fieldset { * `fieldset` elements in all browsers. */ legend { - box-sizing: border-box; - /* 1 */ - color: inherit; - /* 2 */ - display: table; - /* 1 */ - max-width: 100%; - /* 1 */ - padding: 0; - /* 3 */ - white-space: normal; - /* 1 */ + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ } /** @@ -1198,10 +1210,8 @@ textarea { */ [type=checkbox], [type=radio] { - box-sizing: border-box; - /* 1 */ - padding: 0; - /* 2 */ + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ } /** @@ -1218,11 +1228,9 @@ textarea { */ [type=search] { -webkit-appearance: textfield; - -moz-appearance: textfield; - appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ + -moz-appearance: textfield; + appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ } /** @@ -1230,7 +1238,7 @@ textarea { */ [type=search]::-webkit-search-decoration { -webkit-appearance: none; - appearance: none; + appearance: none; } /** @@ -1239,10 +1247,8 @@ textarea { */ ::-webkit-file-upload-button { -webkit-appearance: button; - appearance: button; - /* 1 */ - font: inherit; - /* 2 */ + appearance: button; /* 1 */ + font: inherit; /* 2 */ } /* Interactive @@ -1281,28 +1287,23 @@ template { 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } } - @keyframes ty-rotate-reverse { from { transform: rotate(0); } - to { transform: rotate(-360deg); } } - @keyframes ty-processing { 0% { transform: scale(0.8); opacity: 0.5; } - 100% { transform: scale(2.8); opacity: 0; diff --git a/packages/tokens/scss/_constants.scss b/packages/tokens/scss/_constants.scss new file mode 100644 index 00000000..9b3f7554 --- /dev/null +++ b/packages/tokens/scss/_constants.scss @@ -0,0 +1,80 @@ +// Static structural constants — compile-time only. +// These values do not change between themes. + +// Alert +$alert-border-radius: 3px !default; + +// Avatar +$avatar-border-radius: 2px !default; + +// Badge +$badge-font-size: 12px !default; +$badge-size: 18px !default; +$badge-dot-size: 6px !default; + +// Button +$btn-padding-sm: 0 10px !default; +$btn-padding-md: 0 15px !default; +$btn-padding-lg: 0 28px !default; +$btn-loading-opacity: 0.35 !default; +$btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !default; + +// Card +$card-header-padding: 13px 16px !default; +$card-body-padding: 16px !default; +$card-footer-padding: 5px 16px 16px !default; + +// Description +$description-sm-padding-vt: 8px !default; +$description-md-padding-vt: 12px !default; +$description-lg-padding-vt: 16px !default; +$description-sm-padding-hr: 16px !default; +$description-md-padding-hr: 24px !default; +$description-lg-padding-hr: 24px !default; + +// Input +$input-sm-padding: 0 4px !default; +$input-md-padding: 0 6px !default; +$input-lg-padding: 0 8px !default; + +// Menu +$menu-item-padding-vertical: 15px 20px !default; + +// Native Select +$native-select-sm-padding: 3px 25px 3px 7px !default; +$native-select-md-padding: 6px 25px 6px 7px !default; +$native-select-lg-padding: 9px 25px 9px 7px !default; + +// Notification +$notification-width: 380px !default; +$notification-margin: 20px !default; + +// Popover +$popover-arrow-size: 8px !default; + +// Select +$select-selected-font-weight: 600 !default; +$select-dropdown-max-height: 300px !default; +$select-dropdown-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%) !default; + +// Slider +$slider-size: 12px !default; +$slider-track-size: 4px !default; + +// Steps +$steps-title-font-size: 16px !default; + +// Strength Indicator +$strength-indicator-border-radius: 99px !default; + +// Switch +$switch-md-font-size: 12px !default; +$switch-sm-font-size: 9px !default; +$switch-lg-font-size: 14px !default; + +// Textarea +$textarea-padding: 5px !default; + +// Tooltip +$tooltip-arrow-size: 4px !default; +$tooltip-content-padding: 5px 8px !default; diff --git a/packages/tokens/scss/_normalise.scss b/packages/tokens/scss/_normalise.scss index bdf4fcf3..6b61e8b8 100644 --- a/packages/tokens/scss/_normalise.scss +++ b/packages/tokens/scss/_normalise.scss @@ -1,5 +1,4 @@ /* stylelint-disable scss/comment-no-empty */ -@use "./variables" as *; /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ @@ -53,48 +52,45 @@ main { %heading { margin-top: 0; // 1 - margin-bottom: $headings-margin-bottom; - font-family: $headings-font-family; - font-style: $headings-font-style; - font-weight: $headings-font-weight; - line-height: $headings-line-height; - color: $headings-color; + margin-bottom: 0.5rem; + font-weight: var(--ty-headings-font-weight); + line-height: 1.2; } h1 { @extend %heading; - font-size: $h1-font-size; + font-size: var(--ty-h1-font-size); } h2 { @extend %heading; - font-size: $h2-font-size; + font-size: var(--ty-h2-font-size); } h3 { @extend %heading; - font-size: $h3-font-size; + font-size: var(--ty-h3-font-size); } h4 { @extend %heading; - font-size: $h4-font-size; + font-size: var(--ty-h4-font-size); } h5 { @extend %heading; - font-size: $h5-font-size; + font-size: var(--ty-h5-font-size); } h6 { @extend %heading; - font-size: $h6-font-size; + font-size: var(--ty-h6-font-size); } /* Grouping content @@ -121,12 +117,12 @@ hr { a { background-color: transparent; color: var(--ty-color-primary); - text-decoration: $link-decoration; + text-decoration: none; cursor: pointer; &:hover { color: var(--ty-color-primary-hover); - text-decoration: $link-hover-decoration; + text-decoration: underline; } } diff --git a/packages/tokens/scss/_tokens.scss b/packages/tokens/scss/_tokens.scss deleted file mode 100644 index e5f7967c..00000000 --- a/packages/tokens/scss/_tokens.scss +++ /dev/null @@ -1,91 +0,0 @@ -// SCSS convenience variables referencing CSS custom properties -// Usage: background: $token-color-bg; - -// ---- Background ---- -$token-color-bg: var(--ty-color-bg); -$token-color-bg-elevated: var(--ty-color-bg-elevated); -$token-color-bg-container: var(--ty-color-bg-container); -$token-color-bg-spotlight: var(--ty-color-bg-spotlight); -$token-color-bg-disabled: var(--ty-color-bg-disabled); -$token-color-bg-layout: var(--ty-color-bg-layout); - -// ---- Text ---- -$token-color-text: var(--ty-color-text); -$token-color-text-secondary: var(--ty-color-text-secondary); -$token-color-text-tertiary: var(--ty-color-text-tertiary); -$token-color-text-quaternary: var(--ty-color-text-quaternary); -$token-color-text-heading: var(--ty-color-text-heading); -$token-color-text-label: var(--ty-color-text-label); -$token-color-text-placeholder: var(--ty-color-text-placeholder); - -// ---- Primary ---- -$token-color-primary: var(--ty-color-primary); -$token-color-primary-hover: var(--ty-color-primary-hover); -$token-color-primary-active: var(--ty-color-primary-active); -$token-color-primary-bg: var(--ty-color-primary-bg); -$token-color-primary-border: var(--ty-color-primary-border); -$token-color-primary-bg-hover: var(--ty-color-primary-bg-hover); -$token-color-primary-text-hover: var(--ty-color-primary-text-hover); - -// ---- Border ---- -$token-color-border: var(--ty-color-border); -$token-color-border-secondary: var(--ty-color-border-secondary); -$token-color-border-light: var(--ty-color-border-light); - -// ---- Fill ---- -$token-color-fill: var(--ty-color-fill); -$token-color-fill-secondary: var(--ty-color-fill-secondary); -$token-color-fill-tertiary: var(--ty-color-fill-tertiary); - -// ---- Status ---- -$token-color-success: var(--ty-color-success); -$token-color-success-bg: var(--ty-color-success-bg); -$token-color-success-border: var(--ty-color-success-border); -$token-color-warning: var(--ty-color-warning); -$token-color-warning-bg: var(--ty-color-warning-bg); -$token-color-warning-border: var(--ty-color-warning-border); -$token-color-danger: var(--ty-color-danger); -$token-color-danger-bg: var(--ty-color-danger-bg); -$token-color-danger-border: var(--ty-color-danger-border); -$token-color-info: var(--ty-color-info); -$token-color-info-bg: var(--ty-color-info-bg); -$token-color-info-border: var(--ty-color-info-border); - -// ---- Shadows ---- -$token-shadow-sm: var(--ty-shadow-sm); -$token-shadow: var(--ty-shadow); -$token-shadow-lg: var(--ty-shadow-lg); -$token-shadow-popup: var(--ty-shadow-popup); - -// ---- Overlay ---- -$token-color-overlay-bg: var(--ty-color-overlay-bg); - -// ---- Typography ---- -$token-font-family: var(--ty-font-family); -$token-font-family-monospace: var(--ty-font-family-monospace); -$token-font-size-base: var(--ty-font-size-base); -$token-font-size-sm: var(--ty-font-size-sm); -$token-font-size-lg: var(--ty-font-size-lg); -$token-font-weight: var(--ty-font-weight); -$token-line-height-base: var(--ty-line-height-base); -$token-headings-font-weight: var(--ty-headings-font-weight); -$token-h1-font-size: var(--ty-h1-font-size); -$token-h2-font-size: var(--ty-h2-font-size); -$token-h3-font-size: var(--ty-h3-font-size); -$token-h4-font-size: var(--ty-h4-font-size); -$token-h5-font-size: var(--ty-h5-font-size); -$token-h6-font-size: var(--ty-h6-font-size); - -// ---- Sizing ---- -$token-border-radius: var(--ty-border-radius); -$token-height-sm: var(--ty-height-sm); -$token-height-md: var(--ty-height-md); -$token-height-lg: var(--ty-height-lg); -$token-spacer: var(--ty-spacer); - -// ---- Chart ---- -$token-chart-1: var(--ty-chart-1); -$token-chart-2: var(--ty-chart-2); -$token-chart-3: var(--ty-chart-3); -$token-chart-4: var(--ty-chart-4); -$token-chart-5: var(--ty-chart-5); diff --git a/packages/tokens/scss/_variables.scss b/packages/tokens/scss/_variables.scss index 50b330cf..2e65e40d 100755 --- a/packages/tokens/scss/_variables.scss +++ b/packages/tokens/scss/_variables.scss @@ -1,259 +1,11 @@ -@use "sass:color"; +@forward './constants'; $prefix: 'ty' !default; -// Color -$primary-color: #6e41bf !default; -$white-color: #fff !default; -$gray-100: #f6f9fc !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #8898aa !default; -$gray-700: #525f7f !default; -$gray-800: #32325d !default; -$gray-900: #212529 !default; -$black-color: #000 !default; -$red-color: #dc3545 !default; -$orange-color: #fd7e14 !default; -$yellow-color: #fadb14 !default; -$green-color: #52c41a !default; -$teal-color: #20c997 !default; -$cyan-color: #17a2b8 !default; -$blue-color: #0d6efd !default; -$indigo-color: #6610f2 !default; -$purple-color: #6f42c1 !default; -$magenta-color: #eb2f96 !default; - -// Info -$info-color: #1890ff !default; -$info-light-color: #91d5ff !default; -$info-lighter-color: #e6f7ff !default; - -// Success -$success-color: #52c41a !default; -$success-light-color: #b7eb8f !default; -$success-lighter-color: #f6ffed !default; - -// Warn -$warning-color: #ff9800 !default; -$warning-light-color: #ffe58f !default; -$warning-lighter-color: #fffbe6 !default; - -// Error -$danger-color: #f44336 !default; -$danger-light-color: #ffa39e !default; -$danger-lighter-color: #fff1f0 !default; - -// Body -$body-bg-color: $white-color !default; -$body-color: $gray-800 !default; - -// Font & Font family -$font-color: $body-color !default; -$font-size-base: 1rem !default; -$font-size-lg: $font-size-base * 1.25 !default; -$font-size-sm: $font-size-base * 0.875 !default; -$font-weight: 400 !default; -$font-family-sans-serif: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; -$font-family-monospace: lucida console,consolas,monaco,andale mono,ubuntu mono,monospace !default; -$font-family: $font-family-sans-serif !default; -$height-sm: 24px !default; -$height-md: 32px !default; -$height-lg: 42px !default; -$line-height-base: 1.5 !default; -$line-height-lg: 2 !default; -$line-height-sm: 1.25 !default; - -// Responsive breakpoints +// Responsive breakpoints (CSS custom properties don't work in @media queries) $size-xs: 480px !default; $size-sm: 600px !default; $size-md: 840px !default; $size-lg: 960px !default; $size-xl: 1280px !default; $size-xxl: 1440px !default; - -// Layout -$layout-body-background: #fff !default; -$layout-header-background: #fff !default; -$layout-header-padding: 0 50px !default; -$layout-header-height: 60px !default; -$layout-footer-background: #fff !default; -$layout-footer-padding: 24px 50px !default; -$layout-sidebar-background: #12131a !default; - -// Spacing -$spacer: 1rem !default; - -// Typography -$h1-font-size: $font-size-base * 2.5 !default; -$h2-font-size: $font-size-base * 2 !default; -$h3-font-size: $font-size-base * 1.75 !default; -$h4-font-size: $font-size-base * 1.5 !default; -$h5-font-size: $font-size-base * 1.25 !default; -$h6-font-size: $font-size-base !default; -$headings-margin-bottom: calc($spacer / 2) !default; -$headings-font-family: null !default; -$headings-font-style: null !default; -$headings-font-weight: 500 !default; -$headings-line-height: 1.2 !default; -$headings-color: null !default; - -// Border & Border Radius -$border-radius: 2px !default; -$border-width: 1px !default; -$border-color: $gray-300 !default; - -// Box shadow -$box-shadow-sm: 0 0.125rem 0.25rem rgba($black-color, 0.075) !default; -$box-shadow: 0 0.5rem 1rem rgba($black-color, 0.15) !default; -$box-shadow-lg: 0 1rem 3rem rgba($black-color, 0.175) !default; -$box-shadow-inset: inset 0 1px 2px rgba($black-color, 0.075) !default; - -// Code -$code-font-size: 0.875em !default; -$code-color: $gray-800 !default; -$pre-color: null !default; - -// Link -$link-color: $primary-color !default; -$link-decoration: none !default; -$link-hover-color: color.adjust($link-color, $lightness: -15%) !default; -$link-hover-decoration: underline !default; - -// Alert -$alert-border-radius: 3px !default; - -// Avatar -$avatar-bg: #ccc !default; -$avatar-color: #fff !default; -$avatar-border-radius: 2px !default; - -// Collapse -$collapse-border-radius: var(--ty-border-radius) !default; - -// Badge -$badge-font-size: 12px !default; -$badge-size: 18px !default; -$badge-dot-size: 6px !default; - -// Button -$btn-font-size-sm: var(--ty-font-size-sm) !default; -$btn-font-size-md: var(--ty-font-size-base) !default; -$btn-font-size-lg: var(--ty-font-size-lg) !default; -$btn-padding-sm: 0 10px !default; -$btn-padding-md: 0 15px !default; -$btn-padding-lg: 0 28px !default; -$btn-height-sm: var(--ty-height-sm) !default; -$btn-height-md: var(--ty-height-md) !default; -$btn-height-lg: var(--ty-height-lg) !default; -$btn-line-height: var(--ty-line-height-base) !default; -$btn-border-radius: var(--ty-border-radius) !default; -$btn-border-width: $border-width !default; -$btn-font-weight: 400 !default; -$btn-font-family: var(--ty-font-family) !default; -$btn-box-shadow: inset 0 1px 0 rgba($white-color, 0.15), 0 1px 1px rgba($black-color, 0.075) !default; -$btn-loading-opacity: 0.35 !default; -$btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !default; - -// Card -$card-border-radius: var(--ty-border-radius) !default; -$card-header-padding: 13px 16px !default; -$card-body-padding: 16px !default; -$card-footer-padding: 5px 16px 16px !default; - -// Divider -$divider-line-color: #e4e4e4 !default; - -// Input -$input-font-color: $font-color !default; -$input-border-radius: var(--ty-border-radius) !default; -$input-sm-padding: 0 4px !default; -$input-sm-height: var(--ty-height-sm) !default; -$input-sm-font-size: var(--ty-font-size-sm) !default; -$input-md-padding: 0 6px !default; -$input-md-font-size: var(--ty-font-size-base) !default; -$input-md-height: var(--ty-height-md) !default; -$input-lg-padding: 0 8px !default; -$input-lg-font-size: var(--ty-font-size-lg) !default; -$input-lg-height: var(--ty-height-lg) !default; - -// Textarea -$textarea-font-size: var(--ty-font-size-base) !default; -$textarea-padding: 5px !default; - -// Native Select -$native-select-sm-padding: 3px 25px 3px 7px !default; -$native-select-sm-font-size: var(--ty-font-size-sm) !default; -$native-select-md-padding: 6px 25px 6px 7px !default; -$native-select-md-font-size: var(--ty-font-size-base) !default; -$native-select-lg-padding: 9px 25px 9px 7px !default; -$native-select-lg-font-size: var(--ty-font-size-lg) !default; -$native-select-border-radius: var(--ty-border-radius) !default; - -// Switch -$switch-md-font-size: 12px !default; -$switch-sm-font-size: 9px !default; -$switch-lg-font-size: 14px !default; - -// Select -$select-selected-font-weight: 600 !default; -$select-dropdown-max-height: 300px !default; -$select-dropdown-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%) !default; - -// Notification -$notification-width: 380px !default; -$notification-margin: 20px !default; - -// Breadcrumb -$breadcrumb-font-size: var(--ty-font-size-base) !default; - -// Tag -$tag-border-radius: var(--ty-border-radius) !default; - -// Popover -$popover-arrow-size: 8px !default; -$popover-border-radius: var(--ty-border-radius) !default; -$popover-box-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%) !default; - -// Tooltip -$tooltip-arrow-size: 4px !default; -$tooltip-font-size: var(--ty-font-size-sm) !default; -$tooltip-content-padding: 5px 8px !default; - -// Menu -$menu-sub-border-radius: var(--ty-border-radius) !default; -$menu-item-padding-vertical: 15px 20px !default; - -// Slider -$slider-size: 12px !default; -$slider-track-size: 4px !default; -$slider-margin-bottom-with-marks: 30px !default; - -// Steps -$steps-title-font-size: 16px !default; -$steps-title-active-color: $primary-color !default; -$steps-title-normal-color: rgb(0 0 0 / 65%) !default; -$steps-title-wait-color: rgb(0 0 0 / 25%) !default; -$steps-title-error-color: #ff4d4f !default; -$steps-desc-active-color: $primary-color !default; -$steps-desc-normal-color: rgb(0 0 0 / 45%) !default; -$steps-desc-wait-color: rgb(0 0 0 / 25%) !default; -$steps-desc-error-color: #ff4d4f !default; - -// Description -$description-border-color: #dfe2e5 !default; -$description-border-radius: var(--ty-border-radius) !default; -$description-sm-padding-vt: 8px !default; -$description-md-padding-vt: 12px !default; -$description-lg-padding-vt: 16px !default; -$description-sm-padding-hr: 16px !default; -$description-md-padding-hr: 24px !default; -$description-lg-padding-hr: 24px !default; - -// Tree -$tree-font-size: var(--ty-font-size-base) !default; - -// StrengthIndicator -$strength-indicator-border-radius: 99px !default; diff --git a/packages/tokens/scss/base.scss b/packages/tokens/scss/base.scss index ef76e49d..60b194f4 100644 --- a/packages/tokens/scss/base.scss +++ b/packages/tokens/scss/base.scss @@ -1,6 +1,3 @@ @use './theme' as *; @use './normalise' as *; -@use './variables' as *; -@use './tokens' as *; @use './animation' as *; -@use './mixins' as *; diff --git a/packages/tokens/scss/themes/_dark.scss b/packages/tokens/scss/themes/_dark.scss index 6134cc2e..9ee49bcf 100644 --- a/packages/tokens/scss/themes/_dark.scss +++ b/packages/tokens/scss/themes/_dark.scss @@ -108,7 +108,7 @@ $dark-theme: ( input-disabled-bg: #2a2a2a, input-disabled-color: rgba(255, 255, 255, 0.25), input-addon-bg: #262626, - input-focus-shadow: 0 0 0 2px rgba(144, 101, 208, 0.2), + input-focus-shadow: 0 0 0 3px rgba(144, 101, 208, 0.2), input-focus-border: rgba(144, 101, 208, 0.8), // ---- Component-specific: Select ---- @@ -262,6 +262,23 @@ $dark-theme: ( picker-cell-disabled-bg: #2a2a2a, picker-clear-bg: #1f1f1f, + // ---- Component-specific: Color Picker ---- + color-picker-bg: #1f1f1f, + color-picker-border: #424242, + + // ---- Component-specific: List ---- + list-border: #363636, + + // ---- Component-specific: Speed Dial ---- + speed-dial-bg: #9065d0, + speed-dial-color: #fff, + speed-dial-bg-hover: #7a50bf, + speed-dial-action-bg: #1f1f1f, + speed-dial-action-color: rgba(255, 255, 255, 0.85), + speed-dial-action-bg-hover: #2a2a2a, + speed-dial-tooltip-bg: #363636, + speed-dial-tooltip-color: rgba(255, 255, 255, 0.85), + // ---- Component-specific: Split ---- split-bar-bg: #262626, split-bar-border: #424242, @@ -345,8 +362,11 @@ $dark-theme: ( carousel-dot-active-bg: #fff, // ---- Component-specific: Avatar ---- + avatar-bg: #555, + avatar-color: #e8e8e8, avatar-border: #1f1f1f, avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f, + avatar-offline-color: #525252, // ---- Component-specific: BackTop ---- back-top-bg: rgba(255, 255, 255, 0.2), diff --git a/packages/tokens/scss/themes/_light.scss b/packages/tokens/scss/themes/_light.scss index 29d47dcd..cc60c0ab 100644 --- a/packages/tokens/scss/themes/_light.scss +++ b/packages/tokens/scss/themes/_light.scss @@ -108,7 +108,7 @@ $light-theme: ( input-disabled-bg: #f4f4f5, input-disabled-color: #999, input-addon-bg: #fafafa, - input-focus-shadow: 0 0 0 2px rgba(110, 65, 191, 0.2), + input-focus-shadow: 0 0 0 3px rgba(110, 65, 191, 0.2), input-focus-border: rgba(110, 65, 191, 0.8), // ---- Component-specific: Select ---- @@ -262,6 +262,23 @@ $light-theme: ( picker-cell-disabled-bg: #f5f5f5, picker-clear-bg: #fff, + // ---- Component-specific: Color Picker ---- + color-picker-bg: #fff, + color-picker-border: #dee2e6, + + // ---- Component-specific: List ---- + list-border: #dee2e6, + + // ---- Component-specific: Speed Dial ---- + speed-dial-bg: #6e41bf, + speed-dial-color: #fff, + speed-dial-bg-hover: #5a30a8, + speed-dial-action-bg: #fff, + speed-dial-action-color: #32325d, + speed-dial-action-bg-hover: #f6f9fc, + speed-dial-tooltip-bg: #32325d, + speed-dial-tooltip-color: #fff, + // ---- Component-specific: Split ---- split-bar-bg: #f8f8f9, split-bar-border: #dcdee2, @@ -345,8 +362,11 @@ $light-theme: ( carousel-dot-active-bg: #fff, // ---- Component-specific: Avatar ---- + avatar-bg: #ccc, + avatar-color: #fff, avatar-border: #fff, avatar-presence-shadow: 0 0 0 0.1rem #fff, + avatar-offline-color: #ced4da, // ---- Component-specific: BackTop ---- back-top-bg: rgba(0, 0, 0, 0.3), From 940a8c5b2a6925d185af492fd1acaa9c1e73be07 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 14:46:04 +1000 Subject: [PATCH 02/11] chore: add changeset for SCSS to CSS custom properties migration --- .changeset/migrate-scss-to-css-custom-properties.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/migrate-scss-to-css-custom-properties.md diff --git a/.changeset/migrate-scss-to-css-custom-properties.md b/.changeset/migrate-scss-to-css-custom-properties.md new file mode 100644 index 00000000..b5cfa24f --- /dev/null +++ b/.changeset/migrate-scss-to-css-custom-properties.md @@ -0,0 +1,6 @@ +--- +"@tiny-design/react": minor +"@tiny-design/tokens": minor +--- + +Migrate component styles from SCSS variables to CSS custom properties (`--ty-*`) for better runtime theming support. Add new theme tokens for color-picker, list, speed-dial, and avatar components. From ba9036e22cdb1dbe66bf2716132a14b6c5ed2d10 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 15:22:32 +1000 Subject: [PATCH 03/11] feat(tokens,react): add fine-grained component token customization - Migrate ~80 structural SCSS constants to CSS custom properties - Tokenize hardcoded values in Button, Input, Card, Select, Notification - Add component-scoped CSS variable fallback chain pattern - Add ConfigProvider ThemeConfig API for programmatic token overrides - Convert SCSS arithmetic to CSS calc() in popup, tooltip, notification - Update ConfigProvider docs (EN + ZH) --- packages/react/src/button/style/_index.scss | 34 ++-- packages/react/src/card/style/_index.scss | 10 +- .../src/config-provider/config-context.tsx | 3 +- .../src/config-provider/config-provider.tsx | 34 +++- packages/react/src/config-provider/index.md | 66 ++++++- packages/react/src/config-provider/index.tsx | 1 + .../react/src/config-provider/index.zh_CN.md | 66 ++++++- .../react/src/config-provider/token-utils.ts | 110 +++++++++++ packages/react/src/form/style/_index.scss | 2 +- packages/react/src/index.ts | 1 + packages/react/src/input/style/_index.scss | 46 ++--- packages/react/src/input/style/_mixin.scss | 6 +- .../react/src/notification/style/_index.scss | 14 +- packages/react/src/popup/style/_index.scss | 9 +- packages/react/src/select/style/_index.scss | 30 +-- packages/react/src/tooltip/style/_index.scss | 9 +- packages/tokens/css/base.css | 178 ++++++++++++++++++ packages/tokens/scss/_constants.scss | 88 ++++----- packages/tokens/scss/_theme.scss | 5 + packages/tokens/scss/themes/_dark.scss | 105 +++++++++++ packages/tokens/scss/themes/_light.scss | 105 +++++++++++ 21 files changed, 784 insertions(+), 138 deletions(-) create mode 100644 packages/react/src/config-provider/token-utils.ts diff --git a/packages/react/src/button/style/_index.scss b/packages/react/src/button/style/_index.scss index 0368810c..09dcb6e5 100755 --- a/packages/react/src/button/style/_index.scss +++ b/packages/react/src/button/style/_index.scss @@ -14,14 +14,14 @@ $btn-prefix: #{$prefix}-btn; display: inline-flex; justify-content: center; align-items: center; - min-width: 50px; + min-width: var(--ty-btn-min-width); vertical-align: middle; text-decoration: none; white-space: nowrap; user-select: none; - border-radius: var(--ty-border-radius); + border-radius: var(--ty-btn-border-radius, var(--ty-border-radius)); transition: $btn-transition; - line-height: var(--ty-line-height-base); + line-height: var(--ty-btn-line-height, var(--ty-line-height-base)); &__loader { @include loader; @@ -31,7 +31,7 @@ $btn-prefix: #{$prefix}-btn; display: inline-block; flex-shrink: 0; pointer-events: none; - line-height: var(--ty-line-height-base); + line-height: var(--ty-btn-line-height, var(--ty-line-height-base)); vertical-align: middle; & + span { @@ -117,15 +117,15 @@ $btn-prefix: #{$prefix}-btn; // Sizes &_sm { - @include btn-size($btn-padding-sm, var(--ty-font-size-sm), var(--ty-height-sm)); + @include btn-size($btn-padding-sm, var(--ty-btn-font-size-sm, var(--ty-font-size-sm)), var(--ty-btn-height-sm, var(--ty-height-sm))); } &_md { - @include btn-size($btn-padding-md, var(--ty-font-size-base), var(--ty-height-md)); + @include btn-size($btn-padding-md, var(--ty-btn-font-size, var(--ty-font-size-base)), var(--ty-btn-height-md, var(--ty-height-md))); } &_lg { - @include btn-size($btn-padding-lg, var(--ty-font-size-lg), var(--ty-height-lg)); + @include btn-size($btn-padding-lg, var(--ty-btn-font-size-lg, var(--ty-font-size-lg)), var(--ty-btn-height-lg, var(--ty-height-lg))); } &_block { @@ -166,7 +166,7 @@ $btn-prefix: #{$prefix}-btn; display: inline-block; & + .#{$btn-prefix}-group { - margin-left: 10px; + margin-left: var(--ty-btn-group-gap); } .#{$btn-prefix} { @@ -183,26 +183,26 @@ $btn-prefix: #{$prefix}-btn; } &:first-child { - border-top-left-radius: var(--ty-border-radius); - border-bottom-left-radius: var(--ty-border-radius); + border-top-left-radius: var(--ty-btn-border-radius, var(--ty-border-radius)); + border-bottom-left-radius: var(--ty-btn-border-radius, var(--ty-border-radius)); } &:last-child { - border-top-right-radius: var(--ty-border-radius); - border-bottom-right-radius: var(--ty-border-radius); + border-top-right-radius: var(--ty-btn-border-radius, var(--ty-border-radius)); + border-bottom-right-radius: var(--ty-btn-border-radius, var(--ty-border-radius)); } } &_round { .#{$btn-prefix} { &:first-child { - border-top-left-radius: 30px; - border-bottom-left-radius: 30px; + border-top-left-radius: var(--ty-btn-round-radius); + border-bottom-left-radius: var(--ty-btn-round-radius); } &:last-child { - border-top-right-radius: 30px; - border-bottom-right-radius: 30px; + border-top-right-radius: var(--ty-btn-round-radius); + border-bottom-right-radius: var(--ty-btn-round-radius); } } } @@ -214,7 +214,7 @@ $btn-prefix: #{$prefix}-btn; &_danger { .#{$btn-prefix} { &:not(:first-child) { - border-left-color: rgb(255 255 255 / 20%); + border-left-color: var(--ty-btn-group-divider-color); } } } diff --git a/packages/react/src/card/style/_index.scss b/packages/react/src/card/style/_index.scss index 34cd32e4..6f99f8ac 100644 --- a/packages/react/src/card/style/_index.scss +++ b/packages/react/src/card/style/_index.scss @@ -5,12 +5,12 @@ box-sizing: border-box; padding: 0; margin: 0; - border-radius: var(--ty-border-radius); + border-radius: var(--ty-card-border-radius, var(--ty-border-radius)); transition: all 0.3s; background-color: var(--ty-card-bg); & > img:first-child { - border-radius: var(--ty-border-radius) var(--ty-border-radius) 0 0; + border-radius: var(--ty-card-border-radius, var(--ty-border-radius)) var(--ty-card-border-radius, var(--ty-border-radius)) 0 0; } &_outlined { @@ -43,11 +43,11 @@ justify-content: space-between; padding: $card-header-padding; color: var(--ty-card-header-color); - font-weight: 500; - font-size: 16px; + font-weight: var(--ty-card-header-font-weight); + font-size: var(--ty-card-header-font-size); background: transparent; border-bottom: 1px solid var(--ty-card-border); - border-radius: var(--ty-border-radius) var(--ty-border-radius) 0 0; + border-radius: var(--ty-card-border-radius, var(--ty-border-radius)) var(--ty-card-border-radius, var(--ty-border-radius)) 0 0; } &__body { diff --git a/packages/react/src/config-provider/config-context.tsx b/packages/react/src/config-provider/config-context.tsx index 507223b4..16e10590 100644 --- a/packages/react/src/config-provider/config-context.tsx +++ b/packages/react/src/config-provider/config-context.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { SizeType } from '../_utils/props'; import { SpaceSize } from '../space/types'; import { Locale } from '../locale/types'; +import { ThemeConfig } from './token-utils'; export type ThemeMode = 'light' | 'dark' | 'system'; @@ -10,7 +11,7 @@ export interface ConfigContextProps { componentSize?: SizeType; shimmer?: boolean; space?: SpaceSize; - theme?: ThemeMode; + theme?: ThemeMode | ThemeConfig; locale?: Locale; } diff --git a/packages/react/src/config-provider/config-provider.tsx b/packages/react/src/config-provider/config-provider.tsx index aadac032..5ebee6de 100644 --- a/packages/react/src/config-provider/config-provider.tsx +++ b/packages/react/src/config-provider/config-provider.tsx @@ -1,16 +1,28 @@ -import { useEffect } from 'react'; -import { ConfigContext } from './config-context'; +import { useEffect, useMemo } from 'react'; +import { ConfigContext, ThemeMode } from './config-context'; import { ConfigProviderProps } from './types'; +import { buildCssVars, ThemeConfig } from './token-utils'; import IntlProvider from '../intl-provider'; +function isThemeConfig(theme: unknown): theme is ThemeConfig { + return typeof theme === 'object' && theme !== null; +} + const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { const { children, theme, locale, ...otherProps } = props; + const themeConfig = isThemeConfig(theme) ? theme : undefined; + const mode = themeConfig ? themeConfig.mode : (theme as ThemeMode | undefined); + const cssVars = useMemo( + () => (themeConfig ? buildCssVars(themeConfig) : undefined), + [themeConfig] + ); + useEffect(() => { - if (!theme) return; + if (!mode) return; const html = document.documentElement; - html.setAttribute('data-tiny-theme', theme); - }, [theme]); + html.setAttribute('data-tiny-theme', mode); + }, [mode]); const content = locale ? ( {children} @@ -18,9 +30,17 @@ const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { children ); - return ( - + const wrapped = cssVars ? ( +
{content} +
+ ) : ( + content + ); + + return ( + + {wrapped} ); }; diff --git a/packages/react/src/config-provider/index.md b/packages/react/src/config-provider/index.md index 50b884a7..71227e1b 100644 --- a/packages/react/src/config-provider/index.md +++ b/packages/react/src/config-provider/index.md @@ -1,6 +1,6 @@ # ConfigProvider -This component provides a universal configuration for components. +This component provides a universal configuration for components, including theme customization with design token overrides. ## Usage @@ -16,6 +16,68 @@ return ( ); ``` +## Theme Customization + +The `theme` prop accepts either a theme mode string or a `ThemeConfig` object for fine-grained token customization. + +### Theme Mode + +```jsx + + + +``` + +### Token Overrides + +Use `ThemeConfig` to customize global tokens and component-level tokens: + +```jsx +import { ConfigProvider } from 'tiny-design'; + + + + +``` + +### ThemeConfig + +| Property | Description | Type | Default | +| ---------- | ------------------------------------------------ | --------------------------------------------- | ------- | +| mode | theme mode | enum: `light` | `dark` | `system` | - | +| token | global design token overrides (camelCase keys) | `Record` | - | +| components | per-component token overrides (camelCase keys) | `Record>` | - | + +Token keys use camelCase and are automatically converted to CSS custom properties. For example, `colorPrimary` becomes `--ty-color-primary`, and `Button.borderRadius` becomes `--ty-btn-border-radius`. + +### CSS Custom Property Overrides + +You can also customize components directly via CSS without using `ThemeConfig`: + +```css +/* Global — affects all components */ +:root { --ty-border-radius: 8px; } + +/* Component-specific — only affects Button */ +:root { --ty-btn-border-radius: 20px; } + +/* Scoped — only affects Buttons inside .my-section */ +.my-section { --ty-btn-border-radius: 0; } +``` + ## Props | Property | Description | Type | Default | @@ -25,6 +87,6 @@ return ( | shimmer | display shimmer effect for [Skeleton](#/components/skeleton). | boolean | false | | space | set Space size, ref [Space](#/components/space). | enum: `sm` | `md` | `lg` or `number`. | `sm` | | locale | set locale for components (e.g. `en_US`, `zh_CN`). | Locale | - | -| theme | set theme mode. | enum: `light` | `dark` | `system` | - | +| theme | set theme mode or theme config with token overrides. | `ThemeMode` | `ThemeConfig` | - | > The `prefixCls` property is useful to solve the classname conflict with other libraries. Please note that this will lose default styles from `ty`. To solve that, also updating the `prefix` variable in the `_variables.scss`. diff --git a/packages/react/src/config-provider/index.tsx b/packages/react/src/config-provider/index.tsx index 414e1236..77147208 100644 --- a/packages/react/src/config-provider/index.tsx +++ b/packages/react/src/config-provider/index.tsx @@ -1,3 +1,4 @@ import ConfigProvider from './config-provider'; +export type { ThemeConfig } from './token-utils'; export default ConfigProvider; diff --git a/packages/react/src/config-provider/index.zh_CN.md b/packages/react/src/config-provider/index.zh_CN.md index cdb090da..f7092277 100644 --- a/packages/react/src/config-provider/index.zh_CN.md +++ b/packages/react/src/config-provider/index.zh_CN.md @@ -1,6 +1,6 @@ # ConfigProvider -为组件提供统一的全局配置。 +为组件提供统一的全局配置,支持通过设计令牌进行主题定制。 ## 使用方式 @@ -16,9 +16,67 @@ return ( ); ``` -# ConfigProvider 全局配置 +## 主题定制 -为组件提供统一的全局配置。 +`theme` 属性既可以接收主题模式字符串,也可以接收 `ThemeConfig` 对象来进行细粒度的令牌定制。 + +### 主题模式 + +```jsx + + + +``` + +### 令牌覆盖 + +使用 `ThemeConfig` 自定义全局令牌和组件级令牌: + +```jsx +import { ConfigProvider } from 'tiny-design'; + + + + +``` + +### ThemeConfig + +| 属性 | 说明 | 类型 | 默认值 | +| ---------- | ------------------------------------------------ | --------------------------------------------- | ------- | +| mode | 主题模式 | enum: `light` | `dark` | `system` | - | +| token | 全局设计令牌覆盖(camelCase 键名) | `Record` | - | +| components | 组件级令牌覆盖(camelCase 键名) | `Record>` | - | + +令牌键名使用 camelCase 格式,会自动转换为 CSS 自定义属性。例如 `colorPrimary` 会转换为 `--ty-color-primary`,`Button.borderRadius` 会转换为 `--ty-btn-border-radius`。 + +### CSS 自定义属性覆盖 + +也可以直接通过 CSS 自定义属性来定制组件,无需使用 `ThemeConfig`: + +```css +/* 全局 — 影响所有组件 */ +:root { --ty-border-radius: 8px; } + +/* 组件级 — 仅影响 Button */ +:root { --ty-btn-border-radius: 20px; } + +/* 作用域 — 仅影响 .my-section 内的 Button */ +.my-section { --ty-btn-border-radius: 0; } +``` ## Props @@ -29,6 +87,6 @@ return ( | shimmer | 为 [Skeleton](#/components/skeleton) 显示微光动画效果 | boolean | false | | space | 设置 Space 间距,参考 [Space](#/components/space) | enum: `sm` | `md` | `lg` or `number`. | `sm` | | locale | 设置组件语言包(如 `en_US`、`zh_CN`) | Locale | - | -| theme | 设置主题模式 | enum: `light` | `dark` | `system` | - | +| theme | 设置主题模式或包含令牌覆盖的主题配置 | `ThemeMode` | `ThemeConfig` | - | > `prefixCls` 属性适用于解决与其他库的类名冲突问题。请注意,这将丢失 `ty` 的默认样式。要解决此问题,还需要同时更新 `_variables.scss` 中的 `prefix` 变量。 diff --git a/packages/react/src/config-provider/token-utils.ts b/packages/react/src/config-provider/token-utils.ts new file mode 100644 index 00000000..313107a1 --- /dev/null +++ b/packages/react/src/config-provider/token-utils.ts @@ -0,0 +1,110 @@ +import React from 'react'; + +/** + * Maps component display names to their CSS class prefix. + * Used to convert ThemeConfig.components keys to --ty-{prefix}-* CSS variables. + */ +const COMPONENT_PREFIX_MAP: Record = { + Alert: 'alert', + Anchor: 'anchor', + AutoComplete: 'auto-complete', + Avatar: 'avatar', + BackTop: 'back-top', + Badge: 'badge', + Breadcrumb: 'breadcrumb', + Button: 'btn', + Calendar: 'calendar', + Card: 'card', + Carousel: 'carousel', + Cascader: 'cascader', + Checkbox: 'checkbox', + Collapse: 'collapse', + ColorPicker: 'color-picker', + DatePicker: 'picker', + Descriptions: 'descriptions', + Divider: 'divider', + Drawer: 'drawer', + Dropdown: 'dropdown', + Empty: 'empty', + Form: 'form', + Input: 'input', + InputNumber: 'input-number', + Keyboard: 'kbd', + Layout: 'layout', + List: 'list', + Menu: 'menu', + Message: 'message', + Modal: 'modal', + NativeSelect: 'native-select', + Notification: 'notification', + Pagination: 'pagination', + Popover: 'popover', + Popup: 'popup', + Progress: 'progress', + Radio: 'radio', + Result: 'result', + Segmented: 'segmented', + Select: 'select', + Skeleton: 'skeleton', + Slider: 'slider', + SpeedDial: 'speed-dial', + Split: 'split', + Steps: 'steps', + Switch: 'switch', + Table: 'table', + Tabs: 'tabs', + Tag: 'tag', + Textarea: 'textarea', + TimePicker: 'picker', + Timeline: 'timeline', + Tooltip: 'tooltip', + Transfer: 'transfer', + Tree: 'tree', + Typography: 'typography', + Upload: 'upload', +}; + +/** Converts a camelCase key to kebab-case. */ +function camelToKebab(str: string): string { + return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); +} + +export interface ThemeConfig { + mode?: 'light' | 'dark' | 'system'; + token?: Record; + components?: Record>; +} + +/** + * Builds a CSSProperties object from a ThemeConfig. + * + * - `token` entries: `colorPrimary: '#1890ff'` → `'--ty-color-primary': '#1890ff'` + * - `components.Button` entries: `borderRadius: '20px'` → `'--ty-btn-border-radius': '20px'` + */ +export function buildCssVars( + theme: ThemeConfig +): React.CSSProperties | undefined { + const { token, components } = theme; + if (!token && !components) return undefined; + + const vars: Record = {}; + + if (token) { + for (const [key, value] of Object.entries(token)) { + vars[`--ty-${camelToKebab(key)}`] = String(value); + } + } + + if (components) { + for (const [componentName, componentTokens] of Object.entries(components)) { + const prefix = COMPONENT_PREFIX_MAP[componentName]; + if (!prefix) continue; + for (const [key, value] of Object.entries(componentTokens)) { + vars[`--ty-${prefix}-${camelToKebab(key)}`] = String(value); + } + } + } + + if (Object.keys(vars).length === 0) return undefined; + return vars as React.CSSProperties; +} diff --git a/packages/react/src/form/style/_index.scss b/packages/react/src/form/style/_index.scss index b88c2f2d..f1f1270f 100755 --- a/packages/react/src/form/style/_index.scss +++ b/packages/react/src/form/style/_index.scss @@ -85,7 +85,7 @@ &:focus{ border-color: var(--ty-form-error-hover); - box-shadow: 0 0 0 2px rgb(255 77 79 / 20%); + box-shadow: 0 0 0 3px rgb(255 77 79 / 20%); } } } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7b64e786..b7b0d9be 100755 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -13,6 +13,7 @@ export { default as Carousel } from './carousel'; export { default as Cascader } from './cascader'; export { default as Checkbox } from './checkbox'; export { default as ConfigProvider } from './config-provider'; +export type { ThemeConfig } from './config-provider'; export { default as CopyToClipboard } from './copy-to-clipboard'; export { default as Col } from './col'; export { default as Collapse } from './collapse'; diff --git a/packages/react/src/input/style/_index.scss b/packages/react/src/input/style/_index.scss index 9c133647..0cba7dae 100755 --- a/packages/react/src/input/style/_index.scss +++ b/packages/react/src/input/style/_index.scss @@ -5,7 +5,7 @@ position: relative; box-sizing: border-box; width: 100%; - color: var(--ty-color-text); + color: var(--ty-input-color, var(--ty-color-text)); &__input { @include input-default; @@ -17,7 +17,7 @@ top: 50%; transform: translateY(-50%); z-index: 1; - margin: 0 8px; + margin: var(--ty-input-affix-margin); } &__prefix { @@ -31,8 +31,8 @@ &__clear-btn { display: inline-block; color: var(--ty-color-text-quaternary); - width: 14px; - height: 14px; + width: var(--ty-input-clear-size); + height: var(--ty-input-clear-size); position: relative; top: 2px; cursor: pointer; @@ -41,9 +41,9 @@ &_sm { .#{$prefix}-input { &__input { - font-size: var(--ty-font-size-sm); - height: var(--ty-height-sm); - line-height: var(--ty-height-sm); + font-size: var(--ty-input-font-size-sm, var(--ty-font-size-sm)); + height: var(--ty-input-height-sm, var(--ty-height-sm)); + line-height: var(--ty-input-height-sm, var(--ty-height-sm)); } &__clear-btn { @@ -55,9 +55,9 @@ &_md { .#{$prefix}-input { &__input { - font-size: var(--ty-font-size-base); - height: var(--ty-height-md); - line-height: var(--ty-height-md); + font-size: var(--ty-input-font-size, var(--ty-font-size-base)); + height: var(--ty-input-height-md, var(--ty-height-md)); + line-height: var(--ty-input-height-md, var(--ty-height-md)); } &__clear-btn { @@ -69,9 +69,9 @@ &_lg { .#{$prefix}-input { &__input { - font-size: var(--ty-font-size-lg); - height: var(--ty-height-lg); - line-height: var(--ty-height-lg); + font-size: var(--ty-input-font-size-lg, var(--ty-font-size-lg)); + height: var(--ty-input-height-lg, var(--ty-height-lg)); + line-height: var(--ty-input-height-lg, var(--ty-height-lg)); } } } @@ -96,15 +96,15 @@ } &_sm { - height: var(--ty-height-sm); + height: var(--ty-input-height-sm, var(--ty-height-sm)); } &_md { - height: var(--ty-height-md); + height: var(--ty-input-height-md, var(--ty-height-md)); } &_lg { - height: var(--ty-height-lg); + height: var(--ty-input-height-lg, var(--ty-height-lg)); } .#{$prefix}-input { @@ -139,20 +139,20 @@ box-sizing: border-box; text-align: center; line-height: 1; - border-radius: var(--ty-border-radius); - color: var(--ty-color-text); - padding: 0 7px; + border-radius: var(--ty-input-border-radius, var(--ty-border-radius)); + color: var(--ty-input-color, var(--ty-color-text)); + padding: var(--ty-input-addon-padding); &_sm { - font-size: var(--ty-font-size-sm); + font-size: var(--ty-input-font-size-sm, var(--ty-font-size-sm)); } &_md { - font-size: var(--ty-font-size-base); + font-size: var(--ty-input-font-size, var(--ty-font-size-base)); } &_lg { - font-size: var(--ty-font-size-lg); + font-size: var(--ty-input-font-size-lg, var(--ty-font-size-lg)); } &:first-child { @@ -171,7 +171,7 @@ border-radius: 0; border-left: 0; border-right: 0; - padding: 0 7px; + padding: var(--ty-input-addon-padding); } &_no-border { diff --git a/packages/react/src/input/style/_mixin.scss b/packages/react/src/input/style/_mixin.scss index 1556bebd..1cba1049 100755 --- a/packages/react/src/input/style/_mixin.scss +++ b/packages/react/src/input/style/_mixin.scss @@ -5,12 +5,12 @@ box-sizing: border-box; width: 100%; margin: 0; - color: var(--ty-color-text); + color: var(--ty-input-color, var(--ty-color-text)); border: 1px solid var(--ty-input-border); transition: all 0.3s; outline: 0; - border-radius: var(--ty-border-radius); - font-size: var(--ty-font-size-base); + border-radius: var(--ty-input-border-radius, var(--ty-border-radius)); + font-size: var(--ty-input-font-size, var(--ty-font-size-base)); background-color: var(--ty-input-bg); &:hover { diff --git a/packages/react/src/notification/style/_index.scss b/packages/react/src/notification/style/_index.scss index cd891959..294188a5 100755 --- a/packages/react/src/notification/style/_index.scss +++ b/packages/react/src/notification/style/_index.scss @@ -2,8 +2,8 @@ .#{$prefix}-notification { position: relative; - padding: 16px 24px; - border-radius: 3px; + padding: var(--ty-notification-padding); + border-radius: var(--ty-notification-border-radius); color: var(--ty-color-text-secondary); font-size: var(--ty-font-size-base); box-shadow: var(--ty-shadow-modal); @@ -21,14 +21,14 @@ &_top-right, &_bottom-right { - right: -($notification-width + $notification-margin); - margin-right: $notification-margin; + right: calc(-1 * (var(--ty-notification-width) + var(--ty-notification-margin))); + margin-right: var(--ty-notification-margin); } &_top-left, &_bottom-left { - left: -($notification-width + $notification-margin); - margin-left: $notification-margin; + left: calc(-1 * (var(--ty-notification-width) + var(--ty-notification-margin))); + margin-left: var(--ty-notification-margin); } } @@ -55,7 +55,7 @@ padding-right: 24px; margin-bottom: 5px; color: var(--ty-color-text); - font-size: 16px; + font-size: var(--ty-notification-title-font-size); line-height: 24px; } diff --git a/packages/react/src/popup/style/_index.scss b/packages/react/src/popup/style/_index.scss index b29e7f8a..040bbad4 100755 --- a/packages/react/src/popup/style/_index.scss +++ b/packages/react/src/popup/style/_index.scss @@ -1,4 +1,3 @@ -@use 'sass:math'; @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-popup { @@ -44,7 +43,7 @@ &[data-popper-placement^='top'] { & > .#{$prefix}-popup__arrow { - bottom: math.div(-$popover-arrow-size, 2); + bottom: calc(var(--ty-popover-arrow-size) / -2); &::before { box-shadow: 3px 3px 7px var(--ty-popup-arrow-shadow); @@ -54,7 +53,7 @@ &[data-popper-placement^='bottom'] { > .#{$prefix}-popup__arrow { - top: math.div(-$popover-arrow-size, 2); + top: calc(var(--ty-popover-arrow-size) / -2); &::before { box-shadow: -2px -2px 5px var(--ty-popup-arrow-shadow); @@ -64,7 +63,7 @@ &[data-popper-placement^='left'] { > .#{$prefix}-popup__arrow { - right: math.div(-$popover-arrow-size, 2); + right: calc(var(--ty-popover-arrow-size) / -2); &::before { box-shadow: 3px -3px 7px var(--ty-popup-arrow-shadow); @@ -74,7 +73,7 @@ &[data-popper-placement^='right'] { > .#{$prefix}-popup__arrow { - left: math.div(-$popover-arrow-size, 2); + left: calc(var(--ty-popover-arrow-size) / -2); &::before { box-shadow: -3px 3px 7px var(--ty-popup-arrow-shadow); diff --git a/packages/react/src/select/style/_index.scss b/packages/react/src/select/style/_index.scss index 081fe4f2..b57478c0 100644 --- a/packages/react/src/select/style/_index.scss +++ b/packages/react/src/select/style/_index.scss @@ -4,7 +4,7 @@ position: relative; display: inline-block; width: 100%; - font-size: var(--ty-font-size-base); + font-size: var(--ty-select-font-size, var(--ty-font-size-base)); cursor: pointer; outline: none; @@ -20,19 +20,19 @@ // Sizes &_sm .#{$prefix}-select__selector { - min-height: var(--ty-height-sm); + min-height: var(--ty-select-height-sm, var(--ty-height-sm)); font-size: var(--ty-font-size-sm); padding: 0 24px 0 8px; } &_md .#{$prefix}-select__selector { - min-height: var(--ty-height-md); - font-size: var(--ty-font-size-base); + min-height: var(--ty-select-height-md, var(--ty-height-md)); + font-size: var(--ty-select-font-size, var(--ty-font-size-base)); padding: 0 28px 0 10px; } &_lg .#{$prefix}-select__selector { - min-height: var(--ty-height-lg); + min-height: var(--ty-select-height-lg, var(--ty-height-lg)); font-size: var(--ty-font-size-lg); padding: 0 32px 0 12px; } @@ -62,7 +62,7 @@ align-items: center; width: 100%; border: 1px solid var(--ty-input-border); - border-radius: var(--ty-border-radius); + border-radius: var(--ty-select-border-radius, var(--ty-border-radius)); background-color: var(--ty-input-bg); box-sizing: border-box; transition: all 0.3s; @@ -126,8 +126,8 @@ display: inline-flex; align-items: center; justify-content: center; - width: 14px; - height: 14px; + width: var(--ty-select-suffix-size); + height: var(--ty-select-suffix-size); color: var(--ty-color-text-quaternary); } @@ -171,9 +171,9 @@ display: inline-flex; align-items: center; max-width: 100%; - height: 22px; + height: var(--ty-select-tag-height); padding: 0 4px 0 8px; - border-radius: var(--ty-border-radius); + border-radius: var(--ty-select-border-radius, var(--ty-border-radius)); background-color: var(--ty-color-fill-secondary); font-size: var(--ty-font-size-sm); line-height: 20px; @@ -215,8 +215,8 @@ overflow: hidden auto; z-index: 10; box-shadow: var(--ty-shadow-popup); - border-radius: var(--ty-border-radius); - font-size: var(--ty-font-size-base); + border-radius: var(--ty-select-border-radius, var(--ty-border-radius)); + font-size: var(--ty-select-font-size, var(--ty-font-size-base)); max-height: $select-dropdown-max-height; } @@ -246,8 +246,8 @@ display: flex; align-items: center; justify-content: space-between; - padding: 7px 12px; - font-size: 14px; + padding: var(--ty-select-option-padding); + font-size: var(--ty-select-option-font-size); line-height: 22px; cursor: pointer; color: var(--ty-color-text); @@ -287,7 +287,7 @@ font-size: var(--ty-font-size-sm); cursor: default; color: var(--ty-color-text-secondary); - padding: 7px 12px; + padding: var(--ty-select-option-padding); } &__list { diff --git a/packages/react/src/tooltip/style/_index.scss b/packages/react/src/tooltip/style/_index.scss index 1744fa19..b88a2140 100755 --- a/packages/react/src/tooltip/style/_index.scss +++ b/packages/react/src/tooltip/style/_index.scss @@ -1,4 +1,3 @@ -@use 'sass:math'; @use '@tiny-design/tokens/scss/variables' as *; .#{$prefix}-tooltip { @@ -17,25 +16,25 @@ &[data-popper-placement^='top'] { & > .#{$prefix}-popup__arrow { - bottom: math.div(-$tooltip-arrow-size, 2); + bottom: calc(var(--ty-tooltip-arrow-size) / -2); } } &[data-popper-placement^='bottom'] { > .#{$prefix}-popup__arrow { - top: math.div(-$tooltip-arrow-size, 2); + top: calc(var(--ty-tooltip-arrow-size) / -2); } } &[data-popper-placement^='left'] { > .#{$prefix}-popup__arrow { - right: math.div(-$tooltip-arrow-size, 2); + right: calc(var(--ty-tooltip-arrow-size) / -2); } } &[data-popper-placement^='right'] { > .#{$prefix}-popup__arrow { - left: math.div(-$tooltip-arrow-size, 2); + left: calc(var(--ty-tooltip-arrow-size) / -2); } } } diff --git a/packages/tokens/css/base.css b/packages/tokens/css/base.css index 68e1ed03..1afeb264 100644 --- a/packages/tokens/css/base.css +++ b/packages/tokens/css/base.css @@ -308,6 +308,64 @@ --ty-chart-3: #52c41a; --ty-chart-4: #ff9800; --ty-chart-5: #f44336; + --ty-btn-min-width: 50px; + --ty-btn-round-radius: 30px; + --ty-btn-group-gap: 10px; + --ty-btn-group-divider-color: rgba(255, 255, 255, 0.2); + --ty-input-affix-margin: 0 8px; + --ty-input-clear-size: 14px; + --ty-input-addon-padding: 0 7px; + --ty-card-header-font-size: 16px; + --ty-card-header-font-weight: 500; + --ty-select-option-padding: 7px 12px; + --ty-select-option-font-size: 14px; + --ty-select-tag-height: 22px; + --ty-select-suffix-size: 14px; + --ty-notification-padding: 16px 24px; + --ty-notification-border-radius: 3px; + --ty-notification-title-font-size: 16px; + --ty-alert-border-radius: 3px; + --ty-avatar-border-radius: 2px; + --ty-badge-font-size: 12px; + --ty-badge-size: 18px; + --ty-badge-dot-size: 6px; + --ty-btn-padding-sm: 0 10px; + --ty-btn-padding-md: 0 15px; + --ty-btn-padding-lg: 0 28px; + --ty-btn-loading-opacity: 0.35; + --ty-btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + --ty-card-header-padding: 13px 16px; + --ty-card-body-padding: 16px; + --ty-card-footer-padding: 5px 16px 16px; + --ty-description-sm-padding-vt: 8px; + --ty-description-md-padding-vt: 12px; + --ty-description-lg-padding-vt: 16px; + --ty-description-sm-padding-hr: 16px; + --ty-description-md-padding-hr: 24px; + --ty-description-lg-padding-hr: 24px; + --ty-input-sm-padding: 0 4px; + --ty-input-md-padding: 0 6px; + --ty-input-lg-padding: 0 8px; + --ty-menu-item-padding-vertical: 15px 20px; + --ty-native-select-sm-padding: 3px 25px 3px 7px; + --ty-native-select-md-padding: 6px 25px 6px 7px; + --ty-native-select-lg-padding: 9px 25px 9px 7px; + --ty-notification-width: 380px; + --ty-notification-margin: 20px; + --ty-popover-arrow-size: 8px; + --ty-select-selected-font-weight: 600; + --ty-select-dropdown-max-height: 300px; + --ty-select-dropdown-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + --ty-slider-size: 12px; + --ty-slider-track-size: 4px; + --ty-steps-title-font-size: 16px; + --ty-strength-indicator-border-radius: 99px; + --ty-switch-md-font-size: 12px; + --ty-switch-sm-font-size: 9px; + --ty-switch-lg-font-size: 14px; + --ty-textarea-padding: 5px; + --ty-tooltip-arrow-size: 4px; + --ty-tooltip-content-padding: 5px 8px; } html[data-tiny-theme=dark] { @@ -620,6 +678,64 @@ html[data-tiny-theme=dark] { --ty-chart-3: #49aa19; --ty-chart-4: #d89614; --ty-chart-5: #d32029; + --ty-btn-min-width: 50px; + --ty-btn-round-radius: 30px; + --ty-btn-group-gap: 10px; + --ty-btn-group-divider-color: rgba(255, 255, 255, 0.2); + --ty-input-affix-margin: 0 8px; + --ty-input-clear-size: 14px; + --ty-input-addon-padding: 0 7px; + --ty-card-header-font-size: 16px; + --ty-card-header-font-weight: 500; + --ty-select-option-padding: 7px 12px; + --ty-select-option-font-size: 14px; + --ty-select-tag-height: 22px; + --ty-select-suffix-size: 14px; + --ty-notification-padding: 16px 24px; + --ty-notification-border-radius: 3px; + --ty-notification-title-font-size: 16px; + --ty-alert-border-radius: 3px; + --ty-avatar-border-radius: 2px; + --ty-badge-font-size: 12px; + --ty-badge-size: 18px; + --ty-badge-dot-size: 6px; + --ty-btn-padding-sm: 0 10px; + --ty-btn-padding-md: 0 15px; + --ty-btn-padding-lg: 0 28px; + --ty-btn-loading-opacity: 0.35; + --ty-btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + --ty-card-header-padding: 13px 16px; + --ty-card-body-padding: 16px; + --ty-card-footer-padding: 5px 16px 16px; + --ty-description-sm-padding-vt: 8px; + --ty-description-md-padding-vt: 12px; + --ty-description-lg-padding-vt: 16px; + --ty-description-sm-padding-hr: 16px; + --ty-description-md-padding-hr: 24px; + --ty-description-lg-padding-hr: 24px; + --ty-input-sm-padding: 0 4px; + --ty-input-md-padding: 0 6px; + --ty-input-lg-padding: 0 8px; + --ty-menu-item-padding-vertical: 15px 20px; + --ty-native-select-sm-padding: 3px 25px 3px 7px; + --ty-native-select-md-padding: 6px 25px 6px 7px; + --ty-native-select-lg-padding: 9px 25px 9px 7px; + --ty-notification-width: 380px; + --ty-notification-margin: 20px; + --ty-popover-arrow-size: 8px; + --ty-select-selected-font-weight: 600; + --ty-select-dropdown-max-height: 300px; + --ty-select-dropdown-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + --ty-slider-size: 12px; + --ty-slider-track-size: 4px; + --ty-steps-title-font-size: 16px; + --ty-strength-indicator-border-radius: 99px; + --ty-switch-md-font-size: 12px; + --ty-switch-sm-font-size: 9px; + --ty-switch-lg-font-size: 14px; + --ty-textarea-padding: 5px; + --ty-tooltip-arrow-size: 4px; + --ty-tooltip-content-padding: 5px 8px; } @media (prefers-color-scheme: dark) { @@ -933,8 +1049,70 @@ html[data-tiny-theme=dark] { --ty-chart-3: #49aa19; --ty-chart-4: #d89614; --ty-chart-5: #d32029; + --ty-btn-min-width: 50px; + --ty-btn-round-radius: 30px; + --ty-btn-group-gap: 10px; + --ty-btn-group-divider-color: rgba(255, 255, 255, 0.2); + --ty-input-affix-margin: 0 8px; + --ty-input-clear-size: 14px; + --ty-input-addon-padding: 0 7px; + --ty-card-header-font-size: 16px; + --ty-card-header-font-weight: 500; + --ty-select-option-padding: 7px 12px; + --ty-select-option-font-size: 14px; + --ty-select-tag-height: 22px; + --ty-select-suffix-size: 14px; + --ty-notification-padding: 16px 24px; + --ty-notification-border-radius: 3px; + --ty-notification-title-font-size: 16px; + --ty-alert-border-radius: 3px; + --ty-avatar-border-radius: 2px; + --ty-badge-font-size: 12px; + --ty-badge-size: 18px; + --ty-badge-dot-size: 6px; + --ty-btn-padding-sm: 0 10px; + --ty-btn-padding-md: 0 15px; + --ty-btn-padding-lg: 0 28px; + --ty-btn-loading-opacity: 0.35; + --ty-btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + --ty-card-header-padding: 13px 16px; + --ty-card-body-padding: 16px; + --ty-card-footer-padding: 5px 16px 16px; + --ty-description-sm-padding-vt: 8px; + --ty-description-md-padding-vt: 12px; + --ty-description-lg-padding-vt: 16px; + --ty-description-sm-padding-hr: 16px; + --ty-description-md-padding-hr: 24px; + --ty-description-lg-padding-hr: 24px; + --ty-input-sm-padding: 0 4px; + --ty-input-md-padding: 0 6px; + --ty-input-lg-padding: 0 8px; + --ty-menu-item-padding-vertical: 15px 20px; + --ty-native-select-sm-padding: 3px 25px 3px 7px; + --ty-native-select-md-padding: 6px 25px 6px 7px; + --ty-native-select-lg-padding: 9px 25px 9px 7px; + --ty-notification-width: 380px; + --ty-notification-margin: 20px; + --ty-popover-arrow-size: 8px; + --ty-select-selected-font-weight: 600; + --ty-select-dropdown-max-height: 300px; + --ty-select-dropdown-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); + --ty-slider-size: 12px; + --ty-slider-track-size: 4px; + --ty-steps-title-font-size: 16px; + --ty-strength-indicator-border-radius: 99px; + --ty-switch-md-font-size: 12px; + --ty-switch-sm-font-size: 9px; + --ty-switch-lg-font-size: 14px; + --ty-textarea-padding: 5px; + --ty-tooltip-arrow-size: 4px; + --ty-tooltip-content-padding: 5px 8px; } } +.ty-config-provider { + display: contents; +} + /* stylelint-disable scss/comment-no-empty */ /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /** diff --git a/packages/tokens/scss/_constants.scss b/packages/tokens/scss/_constants.scss index 9b3f7554..6f793b09 100644 --- a/packages/tokens/scss/_constants.scss +++ b/packages/tokens/scss/_constants.scss @@ -1,80 +1,82 @@ -// Static structural constants — compile-time only. +// Static structural constants — bridged to CSS custom properties. // These values do not change between themes. +// The var() references allow runtime customization while +// maintaining backward compatibility with existing SCSS usage. // Alert -$alert-border-radius: 3px !default; +$alert-border-radius: var(--ty-alert-border-radius) !default; // Avatar -$avatar-border-radius: 2px !default; +$avatar-border-radius: var(--ty-avatar-border-radius) !default; // Badge -$badge-font-size: 12px !default; -$badge-size: 18px !default; -$badge-dot-size: 6px !default; +$badge-font-size: var(--ty-badge-font-size) !default; +$badge-size: var(--ty-badge-size) !default; +$badge-dot-size: var(--ty-badge-dot-size) !default; // Button -$btn-padding-sm: 0 10px !default; -$btn-padding-md: 0 15px !default; -$btn-padding-lg: 0 28px !default; -$btn-loading-opacity: 0.35 !default; -$btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out !default; +$btn-padding-sm: var(--ty-btn-padding-sm) !default; +$btn-padding-md: var(--ty-btn-padding-md) !default; +$btn-padding-lg: var(--ty-btn-padding-lg) !default; +$btn-loading-opacity: var(--ty-btn-loading-opacity) !default; +$btn-transition: var(--ty-btn-transition) !default; // Card -$card-header-padding: 13px 16px !default; -$card-body-padding: 16px !default; -$card-footer-padding: 5px 16px 16px !default; +$card-header-padding: var(--ty-card-header-padding) !default; +$card-body-padding: var(--ty-card-body-padding) !default; +$card-footer-padding: var(--ty-card-footer-padding) !default; // Description -$description-sm-padding-vt: 8px !default; -$description-md-padding-vt: 12px !default; -$description-lg-padding-vt: 16px !default; -$description-sm-padding-hr: 16px !default; -$description-md-padding-hr: 24px !default; -$description-lg-padding-hr: 24px !default; +$description-sm-padding-vt: var(--ty-description-sm-padding-vt) !default; +$description-md-padding-vt: var(--ty-description-md-padding-vt) !default; +$description-lg-padding-vt: var(--ty-description-lg-padding-vt) !default; +$description-sm-padding-hr: var(--ty-description-sm-padding-hr) !default; +$description-md-padding-hr: var(--ty-description-md-padding-hr) !default; +$description-lg-padding-hr: var(--ty-description-lg-padding-hr) !default; // Input -$input-sm-padding: 0 4px !default; -$input-md-padding: 0 6px !default; -$input-lg-padding: 0 8px !default; +$input-sm-padding: var(--ty-input-sm-padding) !default; +$input-md-padding: var(--ty-input-md-padding) !default; +$input-lg-padding: var(--ty-input-lg-padding) !default; // Menu -$menu-item-padding-vertical: 15px 20px !default; +$menu-item-padding-vertical: var(--ty-menu-item-padding-vertical) !default; // Native Select -$native-select-sm-padding: 3px 25px 3px 7px !default; -$native-select-md-padding: 6px 25px 6px 7px !default; -$native-select-lg-padding: 9px 25px 9px 7px !default; +$native-select-sm-padding: var(--ty-native-select-sm-padding) !default; +$native-select-md-padding: var(--ty-native-select-md-padding) !default; +$native-select-lg-padding: var(--ty-native-select-lg-padding) !default; // Notification -$notification-width: 380px !default; -$notification-margin: 20px !default; +$notification-width: var(--ty-notification-width) !default; +$notification-margin: var(--ty-notification-margin) !default; // Popover -$popover-arrow-size: 8px !default; +$popover-arrow-size: var(--ty-popover-arrow-size) !default; // Select -$select-selected-font-weight: 600 !default; -$select-dropdown-max-height: 300px !default; -$select-dropdown-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%) !default; +$select-selected-font-weight: var(--ty-select-selected-font-weight) !default; +$select-dropdown-max-height: var(--ty-select-dropdown-max-height) !default; +$select-dropdown-shadow: var(--ty-select-dropdown-shadow) !default; // Slider -$slider-size: 12px !default; -$slider-track-size: 4px !default; +$slider-size: var(--ty-slider-size) !default; +$slider-track-size: var(--ty-slider-track-size) !default; // Steps -$steps-title-font-size: 16px !default; +$steps-title-font-size: var(--ty-steps-title-font-size) !default; // Strength Indicator -$strength-indicator-border-radius: 99px !default; +$strength-indicator-border-radius: var(--ty-strength-indicator-border-radius) !default; // Switch -$switch-md-font-size: 12px !default; -$switch-sm-font-size: 9px !default; -$switch-lg-font-size: 14px !default; +$switch-md-font-size: var(--ty-switch-md-font-size) !default; +$switch-sm-font-size: var(--ty-switch-sm-font-size) !default; +$switch-lg-font-size: var(--ty-switch-lg-font-size) !default; // Textarea -$textarea-padding: 5px !default; +$textarea-padding: var(--ty-textarea-padding) !default; // Tooltip -$tooltip-arrow-size: 4px !default; -$tooltip-content-padding: 5px 8px !default; +$tooltip-arrow-size: var(--ty-tooltip-arrow-size) !default; +$tooltip-content-padding: var(--ty-tooltip-content-padding) !default; diff --git a/packages/tokens/scss/_theme.scss b/packages/tokens/scss/_theme.scss index 8237e920..30a53a1d 100644 --- a/packages/tokens/scss/_theme.scss +++ b/packages/tokens/scss/_theme.scss @@ -24,3 +24,8 @@ html[data-tiny-theme='dark'] { @include generate-theme-vars($dark-theme); } } + +// ConfigProvider token wrapper — transparent to layout +.ty-config-provider { + display: contents; +} diff --git a/packages/tokens/scss/themes/_dark.scss b/packages/tokens/scss/themes/_dark.scss index 9ee49bcf..6df5a485 100644 --- a/packages/tokens/scss/themes/_dark.scss +++ b/packages/tokens/scss/themes/_dark.scss @@ -406,6 +406,7 @@ $dark-theme: ( calendar-hover: #2a2a2a, // ---- Typography ---- + // stylelint-disable-next-line scss/operator-no-unspaced font-family: #{-apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}, font-family-monospace: #{lucida console, consolas, monaco, andale mono, ubuntu mono, monospace}, font-size-base: 1rem, @@ -434,4 +435,108 @@ $dark-theme: ( chart-3: #49aa19, chart-4: #d89614, chart-5: #d32029, + + // ---- Component-specific: Button (structural) ---- + btn-min-width: 50px, + btn-round-radius: 30px, + btn-group-gap: 10px, + btn-group-divider-color: rgb(255 255 255 / 20%), + + // ---- Component-specific: Input (structural) ---- + input-affix-margin: 0 8px, + input-clear-size: 14px, + input-addon-padding: 0 7px, + + // ---- Component-specific: Card (structural) ---- + card-header-font-size: 16px, + card-header-font-weight: 500, + + // ---- Component-specific: Select (structural) ---- + select-option-padding: 7px 12px, + select-option-font-size: 14px, + select-tag-height: 22px, + select-suffix-size: 14px, + + // ---- Component-specific: Notification (structural) ---- + notification-padding: 16px 24px, + notification-border-radius: 3px, + notification-title-font-size: 16px, + + // ---- Structural: Alert ---- + alert-border-radius: 3px, + + // ---- Structural: Avatar ---- + avatar-border-radius: 2px, + + // ---- Structural: Badge ---- + badge-font-size: 12px, + badge-size: 18px, + badge-dot-size: 6px, + + // ---- Structural: Button ---- + btn-padding-sm: 0 10px, + btn-padding-md: 0 15px, + btn-padding-lg: 0 28px, + btn-loading-opacity: 0.35, + btn-transition: #{color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}, + + // ---- Structural: Card ---- + card-header-padding: 13px 16px, + card-body-padding: 16px, + card-footer-padding: 5px 16px 16px, + + // ---- Structural: Description ---- + description-sm-padding-vt: 8px, + description-md-padding-vt: 12px, + description-lg-padding-vt: 16px, + description-sm-padding-hr: 16px, + description-md-padding-hr: 24px, + description-lg-padding-hr: 24px, + + // ---- Structural: Input ---- + input-sm-padding: 0 4px, + input-md-padding: 0 6px, + input-lg-padding: 0 8px, + + // ---- Structural: Menu ---- + menu-item-padding-vertical: 15px 20px, + + // ---- Structural: Native Select ---- + native-select-sm-padding: 3px 25px 3px 7px, + native-select-md-padding: 6px 25px 6px 7px, + native-select-lg-padding: 9px 25px 9px 7px, + + // ---- Structural: Notification ---- + notification-width: 380px, + notification-margin: 20px, + + // ---- Structural: Popover ---- + popover-arrow-size: 8px, + + // ---- Structural: Select ---- + select-selected-font-weight: 600, + select-dropdown-max-height: 300px, + select-dropdown-shadow: #{0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)}, + + // ---- Structural: Slider ---- + slider-size: 12px, + slider-track-size: 4px, + + // ---- Structural: Steps ---- + steps-title-font-size: 16px, + + // ---- Structural: Strength Indicator ---- + strength-indicator-border-radius: 99px, + + // ---- Structural: Switch ---- + switch-md-font-size: 12px, + switch-sm-font-size: 9px, + switch-lg-font-size: 14px, + + // ---- Structural: Textarea ---- + textarea-padding: 5px, + + // ---- Structural: Tooltip ---- + tooltip-arrow-size: 4px, + tooltip-content-padding: 5px 8px, ); diff --git a/packages/tokens/scss/themes/_light.scss b/packages/tokens/scss/themes/_light.scss index cc60c0ab..27016f60 100644 --- a/packages/tokens/scss/themes/_light.scss +++ b/packages/tokens/scss/themes/_light.scss @@ -406,6 +406,7 @@ $light-theme: ( calendar-hover: #f6f9fc, // ---- Typography ---- + // stylelint-disable-next-line scss/operator-no-unspaced font-family: #{-apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}, font-family-monospace: #{lucida console, consolas, monaco, andale mono, ubuntu mono, monospace}, font-size-base: 1rem, @@ -434,4 +435,108 @@ $light-theme: ( chart-3: #52c41a, chart-4: #ff9800, chart-5: #f44336, + + // ---- Component-specific: Button (structural) ---- + btn-min-width: 50px, + btn-round-radius: 30px, + btn-group-gap: 10px, + btn-group-divider-color: rgb(255 255 255 / 20%), + + // ---- Component-specific: Input (structural) ---- + input-affix-margin: 0 8px, + input-clear-size: 14px, + input-addon-padding: 0 7px, + + // ---- Component-specific: Card (structural) ---- + card-header-font-size: 16px, + card-header-font-weight: 500, + + // ---- Component-specific: Select (structural) ---- + select-option-padding: 7px 12px, + select-option-font-size: 14px, + select-tag-height: 22px, + select-suffix-size: 14px, + + // ---- Component-specific: Notification (structural) ---- + notification-padding: 16px 24px, + notification-border-radius: 3px, + notification-title-font-size: 16px, + + // ---- Structural: Alert ---- + alert-border-radius: 3px, + + // ---- Structural: Avatar ---- + avatar-border-radius: 2px, + + // ---- Structural: Badge ---- + badge-font-size: 12px, + badge-size: 18px, + badge-dot-size: 6px, + + // ---- Structural: Button ---- + btn-padding-sm: 0 10px, + btn-padding-md: 0 15px, + btn-padding-lg: 0 28px, + btn-loading-opacity: 0.35, + btn-transition: #{color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}, + + // ---- Structural: Card ---- + card-header-padding: 13px 16px, + card-body-padding: 16px, + card-footer-padding: 5px 16px 16px, + + // ---- Structural: Description ---- + description-sm-padding-vt: 8px, + description-md-padding-vt: 12px, + description-lg-padding-vt: 16px, + description-sm-padding-hr: 16px, + description-md-padding-hr: 24px, + description-lg-padding-hr: 24px, + + // ---- Structural: Input ---- + input-sm-padding: 0 4px, + input-md-padding: 0 6px, + input-lg-padding: 0 8px, + + // ---- Structural: Menu ---- + menu-item-padding-vertical: 15px 20px, + + // ---- Structural: Native Select ---- + native-select-sm-padding: 3px 25px 3px 7px, + native-select-md-padding: 6px 25px 6px 7px, + native-select-lg-padding: 9px 25px 9px 7px, + + // ---- Structural: Notification ---- + notification-width: 380px, + notification-margin: 20px, + + // ---- Structural: Popover ---- + popover-arrow-size: 8px, + + // ---- Structural: Select ---- + select-selected-font-weight: 600, + select-dropdown-max-height: 300px, + select-dropdown-shadow: #{0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)}, + + // ---- Structural: Slider ---- + slider-size: 12px, + slider-track-size: 4px, + + // ---- Structural: Steps ---- + steps-title-font-size: 16px, + + // ---- Structural: Strength Indicator ---- + strength-indicator-border-radius: 99px, + + // ---- Structural: Switch ---- + switch-md-font-size: 12px, + switch-sm-font-size: 9px, + switch-lg-font-size: 14px, + + // ---- Structural: Textarea ---- + textarea-padding: 5px, + + // ---- Structural: Tooltip ---- + tooltip-arrow-size: 4px, + tooltip-content-padding: 5px 8px, ); From 95f4ac082b7d9335be47d13a942f7895bfd267c8 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 15:22:55 +1000 Subject: [PATCH 04/11] chore: update changeset for component token customization --- .changeset/migrate-scss-to-css-custom-properties.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/migrate-scss-to-css-custom-properties.md b/.changeset/migrate-scss-to-css-custom-properties.md index b5cfa24f..868fe9be 100644 --- a/.changeset/migrate-scss-to-css-custom-properties.md +++ b/.changeset/migrate-scss-to-css-custom-properties.md @@ -3,4 +3,10 @@ "@tiny-design/tokens": minor --- -Migrate component styles from SCSS variables to CSS custom properties (`--ty-*`) for better runtime theming support. Add new theme tokens for color-picker, list, speed-dial, and avatar components. +Migrate component styles from SCSS variables to CSS custom properties (`--ty-*`) for better runtime theming support. + +- Migrate ~80 structural SCSS constants (padding, sizing, transitions) to runtime-customizable CSS custom properties +- Tokenize hardcoded values in Button, Input, Card, Select, and Notification components +- Add component-scoped CSS variable fallback chains (e.g., `--ty-btn-border-radius` falls back to `--ty-border-radius`) +- Add `ThemeConfig` API to `ConfigProvider` for programmatic token and component-level overrides +- Three-level customization: global tokens, component tokens, and scoped instance overrides via CSS From 6fd4c1e99b47c8b4920ccc0eb976f211066dd1f0 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 19:42:07 +1000 Subject: [PATCH 05/11] fix(config-provider): address PR review feedback - Apply token overrides to instead of wrapper div, so portals (modals, tooltips, dropdowns) inherit overrides correctly - Remove wrapper
to avoid invalid DOM in constrained containers - Clean up data-tiny-theme attribute on unmount/mode change - Fix Select dropdown shadow to use $select-dropdown-shadow token - Remove unused .ty-config-provider CSS rule --- .../src/config-provider/config-provider.tsx | 34 +++++++++++++------ packages/react/src/config-provider/index.md | 2 ++ .../react/src/config-provider/index.zh_CN.md | 2 ++ packages/react/src/select/style/_index.scss | 2 +- packages/tokens/scss/_theme.scss | 7 +--- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/react/src/config-provider/config-provider.tsx b/packages/react/src/config-provider/config-provider.tsx index 5ebee6de..6c23eb15 100644 --- a/packages/react/src/config-provider/config-provider.tsx +++ b/packages/react/src/config-provider/config-provider.tsx @@ -18,29 +18,41 @@ const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { [themeConfig] ); + // Apply theme mode to attribute, clean up when mode is removed useEffect(() => { - if (!mode) return; const html = document.documentElement; - html.setAttribute('data-tiny-theme', mode); + if (mode) { + html.setAttribute('data-tiny-theme', mode); + } + return () => { + html.removeAttribute('data-tiny-theme'); + }; }, [mode]); + // Apply token overrides as inline styles on , clean up on unmount/change + useEffect(() => { + if (!cssVars) return; + const html = document.documentElement; + const keys = Object.keys(cssVars); + for (const key of keys) { + html.style.setProperty(key, (cssVars as Record)[key]); + } + return () => { + for (const key of keys) { + html.style.removeProperty(key); + } + }; + }, [cssVars]); + const content = locale ? ( {children} ) : ( children ); - const wrapped = cssVars ? ( -
- {content} -
- ) : ( - content - ); - return ( - {wrapped} + {content} ); }; diff --git a/packages/react/src/config-provider/index.md b/packages/react/src/config-provider/index.md index 71227e1b..67d3c282 100644 --- a/packages/react/src/config-provider/index.md +++ b/packages/react/src/config-provider/index.md @@ -63,6 +63,8 @@ import { ConfigProvider } from 'tiny-design'; Token keys use camelCase and are automatically converted to CSS custom properties. For example, `colorPrimary` becomes `--ty-color-primary`, and `Button.borderRadius` becomes `--ty-btn-border-radius`. +Token overrides are applied as inline styles on the `` element, so they are inherited by all components including portal-rendered UI (modals, tooltips, dropdowns, etc.). Overrides are automatically cleaned up when the ConfigProvider unmounts or when the `theme` prop changes. + ### CSS Custom Property Overrides You can also customize components directly via CSS without using `ThemeConfig`: diff --git a/packages/react/src/config-provider/index.zh_CN.md b/packages/react/src/config-provider/index.zh_CN.md index f7092277..e1615ba8 100644 --- a/packages/react/src/config-provider/index.zh_CN.md +++ b/packages/react/src/config-provider/index.zh_CN.md @@ -63,6 +63,8 @@ import { ConfigProvider } from 'tiny-design'; 令牌键名使用 camelCase 格式,会自动转换为 CSS 自定义属性。例如 `colorPrimary` 会转换为 `--ty-color-primary`,`Button.borderRadius` 会转换为 `--ty-btn-border-radius`。 +令牌覆盖以内联样式的方式应用到 `` 元素上,因此所有组件(包括通过 Portal 渲染的弹窗、提示框、下拉菜单等)都会继承这些覆盖值。当 ConfigProvider 卸载或 `theme` 属性变更时,覆盖值会自动清理。 + ### CSS 自定义属性覆盖 也可以直接通过 CSS 自定义属性来定制组件,无需使用 `ThemeConfig`: diff --git a/packages/react/src/select/style/_index.scss b/packages/react/src/select/style/_index.scss index b57478c0..8bdbaa4f 100644 --- a/packages/react/src/select/style/_index.scss +++ b/packages/react/src/select/style/_index.scss @@ -214,7 +214,7 @@ box-sizing: border-box; overflow: hidden auto; z-index: 10; - box-shadow: var(--ty-shadow-popup); + box-shadow: $select-dropdown-shadow; border-radius: var(--ty-select-border-radius, var(--ty-border-radius)); font-size: var(--ty-select-font-size, var(--ty-font-size-base)); max-height: $select-dropdown-max-height; diff --git a/packages/tokens/scss/_theme.scss b/packages/tokens/scss/_theme.scss index 30a53a1d..e953d28f 100644 --- a/packages/tokens/scss/_theme.scss +++ b/packages/tokens/scss/_theme.scss @@ -23,9 +23,4 @@ html[data-tiny-theme='dark'] { html[data-tiny-theme='system'] { @include generate-theme-vars($dark-theme); } -} - -// ConfigProvider token wrapper — transparent to layout -.ty-config-provider { - display: contents; -} +} \ No newline at end of file From c64204f544fc4a66feb04304c49d08bc99815ed2 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 19:46:40 +1000 Subject: [PATCH 06/11] chore: lint --- packages/tokens/css/base.css | 1489 ---------------------------------- 1 file changed, 1489 deletions(-) delete mode 100644 packages/tokens/css/base.css diff --git a/packages/tokens/css/base.css b/packages/tokens/css/base.css deleted file mode 100644 index 1afeb264..00000000 --- a/packages/tokens/css/base.css +++ /dev/null @@ -1,1489 +0,0 @@ -:root { - --ty-color-bg: #fff; - --ty-color-bg-elevated: #fff; - --ty-color-bg-container: #fff; - --ty-color-bg-spotlight: #f5f5f5; - --ty-color-bg-disabled: #f5f5f5; - --ty-color-bg-layout: #fff; - --ty-color-text: rgba(0, 0, 0, 0.85); - --ty-color-text-secondary: rgba(0, 0, 0, 0.65); - --ty-color-text-tertiary: rgba(0, 0, 0, 0.45); - --ty-color-text-quaternary: rgba(0, 0, 0, 0.25); - --ty-color-text-heading: rgba(0, 0, 0, 0.85); - --ty-color-text-label: rgba(0, 0, 0, 0.85); - --ty-color-text-placeholder: #bfbfbf; - --ty-color-primary: #6e41bf; - --ty-color-primary-hover: #8b62d0; - --ty-color-primary-active: #5a30a8; - --ty-color-primary-bg: #f3eefa; - --ty-color-primary-border: #c4a7e6; - --ty-color-primary-bg-hover: #ece3f7; - --ty-color-primary-text-hover: #8b62d0; - --ty-color-border: #d9d9d9; - --ty-color-border-secondary: #e8e8e8; - --ty-color-border-light: #f0f0f0; - --ty-color-border-btn-default: #d0d0d5; - --ty-color-fill: #fafafa; - --ty-color-fill-secondary: #f5f5f5; - --ty-color-fill-tertiary: #f0f0f0; - --ty-color-success: #52c41a; - --ty-color-success-hover: #73d13d; - --ty-color-success-active: #389e0d; - --ty-color-success-bg: #f6ffed; - --ty-color-success-border: #b7eb8f; - --ty-color-success-text: #49b10e; - --ty-color-warning: #ff9800; - --ty-color-warning-hover: #ffad33; - --ty-color-warning-active: #e68a00; - --ty-color-warning-bg: #fffbe6; - --ty-color-warning-border: #ffe58f; - --ty-color-warning-text: #d48806; - --ty-color-danger: #f44336; - --ty-color-danger-hover: #ff7875; - --ty-color-danger-active: #cf1322; - --ty-color-danger-bg: #fff1f0; - --ty-color-danger-border: #ffa39e; - --ty-color-danger-text: #cf1322; - --ty-color-info: #1890ff; - --ty-color-info-hover: #40a9ff; - --ty-color-info-active: #096dd9; - --ty-color-info-bg: #e6f7ff; - --ty-color-info-border: #91d5ff; - --ty-color-info-text: #096dd9; - --ty-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); - --ty-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - --ty-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); - --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.12) , 0 6px 16px 0 rgba(0, 0, 0, 0.08) , 0 9px 28px 8px rgba(0, 0, 0, 0.05); - --ty-shadow-card: 0 1px 6px rgba(0, 0, 0, 0.12); - --ty-shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.15); - --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.15) , 0 1px 1px rgba(0, 0, 0, 0.075); - --ty-color-overlay-bg: rgba(0, 0, 0, 0.55); - --ty-color-overlay-inverted: rgba(255, 255, 255, 0.75); - --ty-btn-default-color: #32325d; - --ty-btn-default-bg: #fff; - --ty-btn-default-border: #d0d0d5; - --ty-btn-default-hover-bg: #fff; - --ty-btn-default-hover-border: #6e41bf; - --ty-btn-default-hover-color: #6e41bf; - --ty-btn-default-active-bg: #f2f2f2; - --ty-btn-default-active-border: #6e41bf; - --ty-btn-default-active-color: #6e41bf; - --ty-btn-disabled-color: rgba(0, 0, 0, 0.25); - --ty-btn-disabled-bg: #f5f5f5; - --ty-btn-disabled-border: #d9d9d9; - --ty-btn-loading-bg: #fff; - --ty-btn-ghost-hover-bg: #f3eefa; - --ty-btn-ghost-active-bg: #ece3f7; - --ty-btn-outline-hover-bg: #f3eefa; - --ty-btn-outline-active-bg: #ece3f7; - --ty-btn-link-disabled-color: rgba(0, 0, 0, 0.25); - --ty-input-bg: #fff; - --ty-input-border: #d9d9d9; - --ty-input-disabled-bg: #f4f4f5; - --ty-input-disabled-color: #999; - --ty-input-addon-bg: #fafafa; - --ty-input-focus-shadow: 0 0 0 3px rgba(110, 65, 191, 0.2); - --ty-input-focus-border: rgba(110, 65, 191, 0.8); - --ty-select-dropdown-bg: #fff; - --ty-select-option-active-bg: #f5f5f5; - --ty-select-option-selected-bg: #f3eefa; - --ty-select-option-disabled-bg: #fff; - --ty-card-bg: #fff; - --ty-card-border: #e8e8e8; - --ty-card-header-color: rgba(0, 0, 0, 0.85); - --ty-card-shadow-border: rgba(0, 0, 0, 0.07); - --ty-modal-bg: #fff; - --ty-modal-header-bg: #fff; - --ty-modal-header-border: #e8e8e8; - --ty-modal-footer-border: #e8e8e8; - --ty-drawer-bg: #fff; - --ty-drawer-border: #e8e8e8; - --ty-menu-light-bg: #fff; - --ty-menu-light-color: #32325d; - --ty-menu-light-border: #f0f0f0; - --ty-menu-dark-bg: #001529; - --ty-menu-dark-color: rgba(255, 255, 255, 0.65); - --ty-menu-dark-border: #001529; - --ty-menu-divider-color: rgba(0, 0, 0, 0.1); - --ty-menu-group-title-color: rgba(0, 0, 0, 0.45); - --ty-notification-bg: #fff; - --ty-notification-close-color: rgba(0, 0, 0, 0.2); - --ty-notification-close-hover: rgba(0, 0, 0, 0.7); - --ty-message-bg: #fff; - --ty-badge-shadow: 0 0 0 1.5px #fff; - --ty-tag-bg: #fafafa; - --ty-tag-border: #d9d9d9; - --ty-tag-checkable-bg: #fff; - --ty-tag-magenta-color: #eb2f96; - --ty-tag-magenta-bg: #fff0f6; - --ty-tag-magenta-border: #ffadd2; - --ty-tag-red-color: #f5222d; - --ty-tag-red-bg: #fff1f0; - --ty-tag-red-border: #ffa39e; - --ty-tag-volcano-color: #fa541c; - --ty-tag-volcano-bg: #fff2e8; - --ty-tag-volcano-border: #ffbb96; - --ty-tag-orange-color: #fa8c16; - --ty-tag-orange-bg: #fff7e6; - --ty-tag-orange-border: #ffd591; - --ty-tag-gold-color: #faad14; - --ty-tag-gold-bg: #fffbe6; - --ty-tag-gold-border: #ffe58f; - --ty-tag-lime-color: #a0d911; - --ty-tag-lime-bg: #fcffe6; - --ty-tag-lime-border: #eaff8f; - --ty-tag-green-color: #52c41a; - --ty-tag-green-bg: #f6ffed; - --ty-tag-green-border: #b7eb8f; - --ty-tag-cyan-color: #13c2c2; - --ty-tag-cyan-bg: #e6fffb; - --ty-tag-cyan-border: #87e8de; - --ty-tag-blue-color: #1890ff; - --ty-tag-blue-bg: #e6f7ff; - --ty-tag-blue-border: #91d5ff; - --ty-tag-geekblue-color: #2f54eb; - --ty-tag-geekblue-bg: #f0f5ff; - --ty-tag-geekblue-border: #adc6ff; - --ty-tag-purple-color: #722ed1; - --ty-tag-purple-bg: #f9f0ff; - --ty-tag-purple-border: #d3adf7; - --ty-tabs-border: #f0f0f0; - --ty-tabs-card-bg: #fafafa; - --ty-tabs-card-active-bg: #fff; - --ty-collapse-bg: #fafafa; - --ty-collapse-border: #d9d9d9; - --ty-collapse-content-bg: #fff; - --ty-collapse-header-hover-bg: #efefef; - --ty-collapse-borderless-bg: #fff; - --ty-descriptions-label-bg: #fafafa; - --ty-descriptions-border: #dfe2e5; - --ty-steps-tail-color: #dcdcdc; - --ty-steps-icon-bg: #fff; - --ty-timeline-line-color: #e8e8e8; - --ty-timeline-dot-bg: #fff; - --ty-timeline-head-bg: #fff; - --ty-slider-rail-bg: #e4e8f1; - --ty-slider-thumb-bg: rgb(245, 248, 250); - --ty-slider-thumb-border: #9570d4; - --ty-slider-dot-bg: #fff; - --ty-slider-dot-border: #f0f0f0; - --ty-slider-dot-active-border: #9570d4; - --ty-slider-mark-color: rgba(0, 0, 0, 0.4); - --ty-slider-mark-active-color: rgba(0, 0, 0, 0.7); - --ty-progress-trail-bg: #e4e8f1; - --ty-progress-text-color: #48576a; - --ty-progress-circle-trail: #e5e9f2; - --ty-skeleton-bg: #f2f2f2; - --ty-skeleton-shimmer: linear-gradient(to right, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); - --ty-kbd-bg: #f6f6f6; - --ty-kbd-border: #d8d8d8; - --ty-kbd-border-bottom: #ccc; - --ty-kbd-color: #333; - --ty-kbd-shadow: inset 0 -1px 0 #ccc; - --ty-transfer-border: #d9d9d9; - --ty-transfer-header-bg: #fff; - --ty-transfer-item-hover-bg: #f5f5f5; - --ty-transfer-footer-bg: #fff; - --ty-transfer-footer-border: #f0f0f0; - --ty-upload-dragger-bg: #fafafa; - --ty-upload-dragger-border: #d9d9d9; - --ty-upload-dragger-hover-bg: #efefef; - --ty-upload-item-hover-bg: #f5f5f5; - --ty-picker-input-bg: #fff; - --ty-picker-dropdown-bg: #fff; - --ty-picker-cell-hover-bg: #f5f5f5; - --ty-picker-cell-selected-hover-bg: #5a30a8; - --ty-picker-cell-disabled-bg: #f5f5f5; - --ty-picker-clear-bg: #fff; - --ty-color-picker-bg: #fff; - --ty-color-picker-border: #dee2e6; - --ty-list-border: #dee2e6; - --ty-speed-dial-bg: #6e41bf; - --ty-speed-dial-color: #fff; - --ty-speed-dial-bg-hover: #5a30a8; - --ty-speed-dial-action-bg: #fff; - --ty-speed-dial-action-color: #32325d; - --ty-speed-dial-action-bg-hover: #f6f9fc; - --ty-speed-dial-tooltip-bg: #32325d; - --ty-speed-dial-tooltip-color: #fff; - --ty-split-bar-bg: #f8f8f9; - --ty-split-bar-border: #dcdee2; - --ty-split-bar-line: #d5d5d5; - --ty-popup-light-bg: #fff; - --ty-popup-dark-bg: #262626; - --ty-popup-arrow-shadow: rgba(0, 0, 0, 0.07); - --ty-layout-sidebar-bg: #12131a; - --ty-layout-sidebar-trigger-bg: rgb(0, 33, 64); - --ty-layout-sidebar-light-bg: #fff; - --ty-layout-sidebar-light-color: #333; - --ty-layout-sidebar-light-trigger-bg: #efefef; - --ty-layout-sidebar-light-trigger-icon: #bbb; - --ty-anchor-bg: #fff; - --ty-anchor-ink-bg: #f0f0f0; - --ty-anchor-ball-bg: #fff; - --ty-form-notice-bg: #fff7cc; - --ty-form-notice-color: #555; - --ty-form-error-color: #ff4d4f; - --ty-form-error-hover: #ff7875; - --ty-checkbox-bg: #fff; - --ty-checkbox-border: #d9d9d9; - --ty-checkbox-disabled-bg: #f5f5f5; - --ty-checkbox-check-color: #fff; - --ty-radio-bg: #fff; - --ty-radio-disabled-border: #d9d9d9; - --ty-radio-disabled-dot: rgba(0, 0, 0, 0.2); - --ty-switch-bg: rgba(0, 0, 0, 0.25); - --ty-switch-thumb-bg: #fff; - --ty-switch-thumb-border: rgba(0, 0, 0, 0.25); - --ty-switch-thumb-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.2); - --ty-divider-color: #e4e4e4; - --ty-divider-text-color: #333; - --ty-popover-dark-border: #4a4a4a; - --ty-native-select-bg: #fff; - --ty-native-select-disabled-bg: #ddd; - --ty-native-select-disabled-color: #a5a5a5; - --ty-pagination-bg: #fff; - --ty-pagination-disabled-bg: #f5f5f5; - --ty-pagination-disabled-active-bg: #dbdbdb; - --ty-pagination-disabled-color: #d9d9d9; - --ty-typography-heading-color: rgba(0, 0, 0, 0.85); - --ty-typography-body-color: rgba(0, 0, 0, 0.65); - --ty-typography-code-bg: rgba(0, 0, 0, 0.06); - --ty-typography-code-border: rgba(0, 0, 0, 0.06); - --ty-typography-mark-bg: #ffe58f; - --ty-result-content-bg: #fafafa; - --ty-empty-desc-color: rgba(0, 0, 0, 0.35); - --ty-carousel-arrow-bg: rgba(0, 0, 0, 0.25); - --ty-carousel-arrow-hover-bg: rgba(0, 0, 0, 0.45); - --ty-carousel-dot-bg: rgba(255, 255, 255, 0.3); - --ty-carousel-dot-hover-bg: rgba(255, 255, 255, 0.6); - --ty-carousel-dot-active-bg: #fff; - --ty-avatar-bg: #ccc; - --ty-avatar-color: #fff; - --ty-avatar-border: #fff; - --ty-avatar-presence-shadow: 0 0 0 0.1rem #fff; - --ty-avatar-offline-color: #ced4da; - --ty-back-top-bg: rgba(0, 0, 0, 0.3); - --ty-input-number-control-border: #d9d9d9; - --ty-input-number-control-active-bg: #f4f4f4; - --ty-input-number-icon-color: #999; - --ty-tree-arrow-color: #999; - --ty-tree-hover-bg: #f5f5f5; - --ty-textarea-counter-color: #666; - --ty-table-header-bg: #f6f9fc; - --ty-table-border: #e9ecef; - --ty-table-hover: #f6f9fc; - --ty-table-selected-bg: rgba(110, 65, 191, 0.06); - --ty-segmented-bg: #e9ecef; - --ty-segmented-active-bg: #fff; - --ty-cascader-bg: #fff; - --ty-cascader-border: #d9d9d9; - --ty-cascader-dropdown-bg: #fff; - --ty-cascader-hover: #f5f5f5; - --ty-cascader-selected-bg: rgba(110, 65, 191, 0.06); - --ty-calendar-bg: #fff; - --ty-calendar-border: #e9ecef; - --ty-calendar-hover: #f6f9fc; - --ty-font-family: -apple-system, blinkmacsystemfont, Segoe UI, roboto, Helvetica Neue, arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; - --ty-font-family-monospace: lucida console, consolas, monaco, andale mono, ubuntu mono, monospace; - --ty-font-size-base: 1rem; - --ty-font-size-sm: 0.875rem; - --ty-font-size-lg: 1.25rem; - --ty-font-weight: 400; - --ty-line-height-base: 1.5; - --ty-headings-font-weight: 500; - --ty-h1-font-size: 2.5rem; - --ty-h2-font-size: 2rem; - --ty-h3-font-size: 1.75rem; - --ty-h4-font-size: 1.5rem; - --ty-h5-font-size: 1.25rem; - --ty-h6-font-size: 1rem; - --ty-border-radius: 2px; - --ty-height-sm: 24px; - --ty-height-md: 32px; - --ty-height-lg: 42px; - --ty-spacer: 1rem; - --ty-chart-1: #6e41bf; - --ty-chart-2: #1890ff; - --ty-chart-3: #52c41a; - --ty-chart-4: #ff9800; - --ty-chart-5: #f44336; - --ty-btn-min-width: 50px; - --ty-btn-round-radius: 30px; - --ty-btn-group-gap: 10px; - --ty-btn-group-divider-color: rgba(255, 255, 255, 0.2); - --ty-input-affix-margin: 0 8px; - --ty-input-clear-size: 14px; - --ty-input-addon-padding: 0 7px; - --ty-card-header-font-size: 16px; - --ty-card-header-font-weight: 500; - --ty-select-option-padding: 7px 12px; - --ty-select-option-font-size: 14px; - --ty-select-tag-height: 22px; - --ty-select-suffix-size: 14px; - --ty-notification-padding: 16px 24px; - --ty-notification-border-radius: 3px; - --ty-notification-title-font-size: 16px; - --ty-alert-border-radius: 3px; - --ty-avatar-border-radius: 2px; - --ty-badge-font-size: 12px; - --ty-badge-size: 18px; - --ty-badge-dot-size: 6px; - --ty-btn-padding-sm: 0 10px; - --ty-btn-padding-md: 0 15px; - --ty-btn-padding-lg: 0 28px; - --ty-btn-loading-opacity: 0.35; - --ty-btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - --ty-card-header-padding: 13px 16px; - --ty-card-body-padding: 16px; - --ty-card-footer-padding: 5px 16px 16px; - --ty-description-sm-padding-vt: 8px; - --ty-description-md-padding-vt: 12px; - --ty-description-lg-padding-vt: 16px; - --ty-description-sm-padding-hr: 16px; - --ty-description-md-padding-hr: 24px; - --ty-description-lg-padding-hr: 24px; - --ty-input-sm-padding: 0 4px; - --ty-input-md-padding: 0 6px; - --ty-input-lg-padding: 0 8px; - --ty-menu-item-padding-vertical: 15px 20px; - --ty-native-select-sm-padding: 3px 25px 3px 7px; - --ty-native-select-md-padding: 6px 25px 6px 7px; - --ty-native-select-lg-padding: 9px 25px 9px 7px; - --ty-notification-width: 380px; - --ty-notification-margin: 20px; - --ty-popover-arrow-size: 8px; - --ty-select-selected-font-weight: 600; - --ty-select-dropdown-max-height: 300px; - --ty-select-dropdown-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); - --ty-slider-size: 12px; - --ty-slider-track-size: 4px; - --ty-steps-title-font-size: 16px; - --ty-strength-indicator-border-radius: 99px; - --ty-switch-md-font-size: 12px; - --ty-switch-sm-font-size: 9px; - --ty-switch-lg-font-size: 14px; - --ty-textarea-padding: 5px; - --ty-tooltip-arrow-size: 4px; - --ty-tooltip-content-padding: 5px 8px; -} - -html[data-tiny-theme=dark] { - --ty-color-bg: #141414; - --ty-color-bg-elevated: #1f1f1f; - --ty-color-bg-container: #1f1f1f; - --ty-color-bg-spotlight: #2a2a2a; - --ty-color-bg-disabled: #2a2a2a; - --ty-color-bg-layout: #141414; - --ty-color-text: rgba(255, 255, 255, 0.85); - --ty-color-text-secondary: rgba(255, 255, 255, 0.65); - --ty-color-text-tertiary: rgba(255, 255, 255, 0.45); - --ty-color-text-quaternary: rgba(255, 255, 255, 0.25); - --ty-color-text-heading: rgba(255, 255, 255, 0.85); - --ty-color-text-label: rgba(255, 255, 255, 0.85); - --ty-color-text-placeholder: #5c5c5c; - --ty-color-primary: #9065d0; - --ty-color-primary-hover: #a882dc; - --ty-color-primary-active: #7a50bf; - --ty-color-primary-bg: #1a1325; - --ty-color-primary-border: #5b3d8f; - --ty-color-primary-bg-hover: #231a33; - --ty-color-primary-text-hover: #a882dc; - --ty-color-border: #424242; - --ty-color-border-secondary: #363636; - --ty-color-border-light: #303030; - --ty-color-border-btn-default: #424242; - --ty-color-fill: #262626; - --ty-color-fill-secondary: #2a2a2a; - --ty-color-fill-tertiary: #303030; - --ty-color-success: #49aa19; - --ty-color-success-hover: #6abe39; - --ty-color-success-active: #3c8c14; - --ty-color-success-bg: #162312; - --ty-color-success-border: #274916; - --ty-color-success-text: #6abe39; - --ty-color-warning: #d89614; - --ty-color-warning-hover: #e8b339; - --ty-color-warning-active: #b37a10; - --ty-color-warning-bg: #2b2111; - --ty-color-warning-border: #594214; - --ty-color-warning-text: #e8b339; - --ty-color-danger: #d32029; - --ty-color-danger-hover: #e84749; - --ty-color-danger-active: #ab1a20; - --ty-color-danger-bg: #2a1215; - --ty-color-danger-border: #58181c; - --ty-color-danger-text: #e84749; - --ty-color-info: #177ddc; - --ty-color-info-hover: #3c9ae8; - --ty-color-info-active: #1268b3; - --ty-color-info-bg: #111d2c; - --ty-color-info-border: #15395b; - --ty-color-info-text: #3c9ae8; - --ty-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3); - --ty-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); - --ty-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.5); - --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.48) , 0 6px 16px 0 rgba(0, 0, 0, 0.32) , 0 9px 28px 8px rgba(0, 0, 0, 0.2); - --ty-shadow-card: 0 1px 6px rgba(0, 0, 0, 0.35); - --ty-shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.45); - --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.05) , 0 1px 1px rgba(0, 0, 0, 0.2); - --ty-color-overlay-bg: rgba(0, 0, 0, 0.65); - --ty-color-overlay-inverted: rgba(50, 50, 50, 0.75); - --ty-btn-default-color: rgba(255, 255, 255, 0.85); - --ty-btn-default-bg: #1f1f1f; - --ty-btn-default-border: #424242; - --ty-btn-default-hover-bg: #1f1f1f; - --ty-btn-default-hover-border: #9065d0; - --ty-btn-default-hover-color: #9065d0; - --ty-btn-default-active-bg: #2a2a2a; - --ty-btn-default-active-border: #9065d0; - --ty-btn-default-active-color: #9065d0; - --ty-btn-disabled-color: rgba(255, 255, 255, 0.25); - --ty-btn-disabled-bg: #2a2a2a; - --ty-btn-disabled-border: #424242; - --ty-btn-loading-bg: #1f1f1f; - --ty-btn-ghost-hover-bg: #1a1325; - --ty-btn-ghost-active-bg: #231a33; - --ty-btn-outline-hover-bg: #1a1325; - --ty-btn-outline-active-bg: #231a33; - --ty-btn-link-disabled-color: rgba(255, 255, 255, 0.25); - --ty-input-bg: #1f1f1f; - --ty-input-border: #424242; - --ty-input-disabled-bg: #2a2a2a; - --ty-input-disabled-color: rgba(255, 255, 255, 0.25); - --ty-input-addon-bg: #262626; - --ty-input-focus-shadow: 0 0 0 3px rgba(144, 101, 208, 0.2); - --ty-input-focus-border: rgba(144, 101, 208, 0.8); - --ty-select-dropdown-bg: #1f1f1f; - --ty-select-option-active-bg: #2a2a2a; - --ty-select-option-selected-bg: #1a1325; - --ty-select-option-disabled-bg: #1f1f1f; - --ty-card-bg: #1f1f1f; - --ty-card-border: #363636; - --ty-card-header-color: rgba(255, 255, 255, 0.85); - --ty-card-shadow-border: rgba(0, 0, 0, 0.2); - --ty-modal-bg: #1f1f1f; - --ty-modal-header-bg: #1f1f1f; - --ty-modal-header-border: #363636; - --ty-modal-footer-border: #363636; - --ty-drawer-bg: #1f1f1f; - --ty-drawer-border: #363636; - --ty-menu-light-bg: #1f1f1f; - --ty-menu-light-color: rgba(255, 255, 255, 0.85); - --ty-menu-light-border: #303030; - --ty-menu-dark-bg: #001529; - --ty-menu-dark-color: rgba(255, 255, 255, 0.65); - --ty-menu-dark-border: #001529; - --ty-menu-divider-color: rgba(255, 255, 255, 0.1); - --ty-menu-group-title-color: rgba(255, 255, 255, 0.45); - --ty-notification-bg: #1f1f1f; - --ty-notification-close-color: rgba(255, 255, 255, 0.2); - --ty-notification-close-hover: rgba(255, 255, 255, 0.7); - --ty-message-bg: #1f1f1f; - --ty-badge-shadow: 0 0 0 1.5px #1f1f1f; - --ty-tag-bg: #262626; - --ty-tag-border: #424242; - --ty-tag-checkable-bg: #1f1f1f; - --ty-tag-magenta-color: #e0529c; - --ty-tag-magenta-bg: #291321; - --ty-tag-magenta-border: #55162b; - --ty-tag-red-color: #e84749; - --ty-tag-red-bg: #2a1215; - --ty-tag-red-border: #58181c; - --ty-tag-volcano-color: #e87040; - --ty-tag-volcano-bg: #2b1611; - --ty-tag-volcano-border: #592716; - --ty-tag-orange-color: #e89a3c; - --ty-tag-orange-bg: #2b1d11; - --ty-tag-orange-border: #593815; - --ty-tag-gold-color: #e8b339; - --ty-tag-gold-bg: #2b2111; - --ty-tag-gold-border: #594214; - --ty-tag-lime-color: #8bbb11; - --ty-tag-lime-bg: #1a2611; - --ty-tag-lime-border: #3e4f13; - --ty-tag-green-color: #6abe39; - --ty-tag-green-bg: #162312; - --ty-tag-green-border: #274916; - --ty-tag-cyan-color: #33bcb7; - --ty-tag-cyan-bg: #112123; - --ty-tag-cyan-border: #144848; - --ty-tag-blue-color: #3c9ae8; - --ty-tag-blue-bg: #111d2c; - --ty-tag-blue-border: #15395b; - --ty-tag-geekblue-color: #5273e0; - --ty-tag-geekblue-bg: #131a2e; - --ty-tag-geekblue-border: #1c2d57; - --ty-tag-purple-color: #854eca; - --ty-tag-purple-bg: #1a1325; - --ty-tag-purple-border: #301c4d; - --ty-tabs-border: #303030; - --ty-tabs-card-bg: #262626; - --ty-tabs-card-active-bg: #1f1f1f; - --ty-collapse-bg: #262626; - --ty-collapse-border: #424242; - --ty-collapse-content-bg: #1f1f1f; - --ty-collapse-header-hover-bg: #303030; - --ty-collapse-borderless-bg: #1f1f1f; - --ty-descriptions-label-bg: #262626; - --ty-descriptions-border: #363636; - --ty-steps-tail-color: #424242; - --ty-steps-icon-bg: #1f1f1f; - --ty-timeline-line-color: #363636; - --ty-timeline-dot-bg: #1f1f1f; - --ty-timeline-head-bg: #1f1f1f; - --ty-slider-rail-bg: #363636; - --ty-slider-thumb-bg: #1f1f1f; - --ty-slider-thumb-border: #9065d0; - --ty-slider-dot-bg: #1f1f1f; - --ty-slider-dot-border: #424242; - --ty-slider-dot-active-border: #9065d0; - --ty-slider-mark-color: rgba(255, 255, 255, 0.4); - --ty-slider-mark-active-color: rgba(255, 255, 255, 0.7); - --ty-progress-trail-bg: #363636; - --ty-progress-text-color: rgba(255, 255, 255, 0.65); - --ty-progress-circle-trail: #363636; - --ty-skeleton-bg: #303030; - --ty-skeleton-shimmer: linear-gradient(to right, #303030 25%, #3a3a3a 37%, #303030 63%); - --ty-kbd-bg: #2a2a2a; - --ty-kbd-border: #424242; - --ty-kbd-border-bottom: #363636; - --ty-kbd-color: rgba(255, 255, 255, 0.85); - --ty-kbd-shadow: inset 0 -1px 0 #363636; - --ty-transfer-border: #424242; - --ty-transfer-header-bg: #1f1f1f; - --ty-transfer-item-hover-bg: #2a2a2a; - --ty-transfer-footer-bg: #1f1f1f; - --ty-transfer-footer-border: #303030; - --ty-upload-dragger-bg: #262626; - --ty-upload-dragger-border: #424242; - --ty-upload-dragger-hover-bg: #303030; - --ty-upload-item-hover-bg: #2a2a2a; - --ty-picker-input-bg: #1f1f1f; - --ty-picker-dropdown-bg: #1f1f1f; - --ty-picker-cell-hover-bg: #2a2a2a; - --ty-picker-cell-selected-hover-bg: #7a50bf; - --ty-picker-cell-disabled-bg: #2a2a2a; - --ty-picker-clear-bg: #1f1f1f; - --ty-color-picker-bg: #1f1f1f; - --ty-color-picker-border: #424242; - --ty-list-border: #363636; - --ty-speed-dial-bg: #9065d0; - --ty-speed-dial-color: #fff; - --ty-speed-dial-bg-hover: #7a50bf; - --ty-speed-dial-action-bg: #1f1f1f; - --ty-speed-dial-action-color: rgba(255, 255, 255, 0.85); - --ty-speed-dial-action-bg-hover: #2a2a2a; - --ty-speed-dial-tooltip-bg: #363636; - --ty-speed-dial-tooltip-color: rgba(255, 255, 255, 0.85); - --ty-split-bar-bg: #262626; - --ty-split-bar-border: #424242; - --ty-split-bar-line: #525252; - --ty-popup-light-bg: #1f1f1f; - --ty-popup-dark-bg: #363636; - --ty-popup-arrow-shadow: rgba(0, 0, 0, 0.2); - --ty-layout-sidebar-bg: #12131a; - --ty-layout-sidebar-trigger-bg: rgb(0, 33, 64); - --ty-layout-sidebar-light-bg: #1f1f1f; - --ty-layout-sidebar-light-color: rgba(255, 255, 255, 0.85); - --ty-layout-sidebar-light-trigger-bg: #2a2a2a; - --ty-layout-sidebar-light-trigger-icon: #666; - --ty-anchor-bg: #1f1f1f; - --ty-anchor-ink-bg: #303030; - --ty-anchor-ball-bg: #1f1f1f; - --ty-form-notice-bg: #2b2111; - --ty-form-notice-color: rgba(255, 255, 255, 0.65); - --ty-form-error-color: #e84749; - --ty-form-error-hover: #d32029; - --ty-checkbox-bg: #1f1f1f; - --ty-checkbox-border: #424242; - --ty-checkbox-disabled-bg: #2a2a2a; - --ty-checkbox-check-color: #fff; - --ty-radio-bg: #1f1f1f; - --ty-radio-disabled-border: #424242; - --ty-radio-disabled-dot: rgba(255, 255, 255, 0.2); - --ty-switch-bg: rgba(255, 255, 255, 0.25); - --ty-switch-thumb-bg: #e8e8e8; - --ty-switch-thumb-border: rgba(255, 255, 255, 0.25); - --ty-switch-thumb-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.4); - --ty-divider-color: #363636; - --ty-divider-text-color: rgba(255, 255, 255, 0.85); - --ty-popover-dark-border: #525252; - --ty-native-select-bg: #1f1f1f; - --ty-native-select-disabled-bg: #2a2a2a; - --ty-native-select-disabled-color: rgba(255, 255, 255, 0.25); - --ty-pagination-bg: #1f1f1f; - --ty-pagination-disabled-bg: #2a2a2a; - --ty-pagination-disabled-active-bg: #424242; - --ty-pagination-disabled-color: #525252; - --ty-typography-heading-color: rgba(255, 255, 255, 0.85); - --ty-typography-body-color: rgba(255, 255, 255, 0.65); - --ty-typography-code-bg: rgba(255, 255, 255, 0.06); - --ty-typography-code-border: rgba(255, 255, 255, 0.06); - --ty-typography-mark-bg: #594214; - --ty-result-content-bg: #262626; - --ty-empty-desc-color: rgba(255, 255, 255, 0.35); - --ty-carousel-arrow-bg: rgba(255, 255, 255, 0.15); - --ty-carousel-arrow-hover-bg: rgba(255, 255, 255, 0.25); - --ty-carousel-dot-bg: rgba(255, 255, 255, 0.3); - --ty-carousel-dot-hover-bg: rgba(255, 255, 255, 0.6); - --ty-carousel-dot-active-bg: #fff; - --ty-avatar-bg: #555; - --ty-avatar-color: #e8e8e8; - --ty-avatar-border: #1f1f1f; - --ty-avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f; - --ty-avatar-offline-color: #525252; - --ty-back-top-bg: rgba(255, 255, 255, 0.2); - --ty-input-number-control-border: #424242; - --ty-input-number-control-active-bg: #2a2a2a; - --ty-input-number-icon-color: #666; - --ty-tree-arrow-color: #666; - --ty-tree-hover-bg: #2a2a2a; - --ty-textarea-counter-color: rgba(255, 255, 255, 0.45); - --ty-table-header-bg: #262626; - --ty-table-border: #363636; - --ty-table-hover: #2a2a2a; - --ty-table-selected-bg: rgba(144, 101, 208, 0.1); - --ty-segmented-bg: #2a2a2a; - --ty-segmented-active-bg: #1f1f1f; - --ty-cascader-bg: #1f1f1f; - --ty-cascader-border: #424242; - --ty-cascader-dropdown-bg: #1f1f1f; - --ty-cascader-hover: #2a2a2a; - --ty-cascader-selected-bg: rgba(144, 101, 208, 0.1); - --ty-calendar-bg: #1f1f1f; - --ty-calendar-border: #363636; - --ty-calendar-hover: #2a2a2a; - --ty-font-family: -apple-system, blinkmacsystemfont, Segoe UI, roboto, Helvetica Neue, arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; - --ty-font-family-monospace: lucida console, consolas, monaco, andale mono, ubuntu mono, monospace; - --ty-font-size-base: 1rem; - --ty-font-size-sm: 0.875rem; - --ty-font-size-lg: 1.25rem; - --ty-font-weight: 400; - --ty-line-height-base: 1.5; - --ty-headings-font-weight: 500; - --ty-h1-font-size: 2.5rem; - --ty-h2-font-size: 2rem; - --ty-h3-font-size: 1.75rem; - --ty-h4-font-size: 1.5rem; - --ty-h5-font-size: 1.25rem; - --ty-h6-font-size: 1rem; - --ty-border-radius: 2px; - --ty-height-sm: 24px; - --ty-height-md: 32px; - --ty-height-lg: 42px; - --ty-spacer: 1rem; - --ty-chart-1: #9065d0; - --ty-chart-2: #177ddc; - --ty-chart-3: #49aa19; - --ty-chart-4: #d89614; - --ty-chart-5: #d32029; - --ty-btn-min-width: 50px; - --ty-btn-round-radius: 30px; - --ty-btn-group-gap: 10px; - --ty-btn-group-divider-color: rgba(255, 255, 255, 0.2); - --ty-input-affix-margin: 0 8px; - --ty-input-clear-size: 14px; - --ty-input-addon-padding: 0 7px; - --ty-card-header-font-size: 16px; - --ty-card-header-font-weight: 500; - --ty-select-option-padding: 7px 12px; - --ty-select-option-font-size: 14px; - --ty-select-tag-height: 22px; - --ty-select-suffix-size: 14px; - --ty-notification-padding: 16px 24px; - --ty-notification-border-radius: 3px; - --ty-notification-title-font-size: 16px; - --ty-alert-border-radius: 3px; - --ty-avatar-border-radius: 2px; - --ty-badge-font-size: 12px; - --ty-badge-size: 18px; - --ty-badge-dot-size: 6px; - --ty-btn-padding-sm: 0 10px; - --ty-btn-padding-md: 0 15px; - --ty-btn-padding-lg: 0 28px; - --ty-btn-loading-opacity: 0.35; - --ty-btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - --ty-card-header-padding: 13px 16px; - --ty-card-body-padding: 16px; - --ty-card-footer-padding: 5px 16px 16px; - --ty-description-sm-padding-vt: 8px; - --ty-description-md-padding-vt: 12px; - --ty-description-lg-padding-vt: 16px; - --ty-description-sm-padding-hr: 16px; - --ty-description-md-padding-hr: 24px; - --ty-description-lg-padding-hr: 24px; - --ty-input-sm-padding: 0 4px; - --ty-input-md-padding: 0 6px; - --ty-input-lg-padding: 0 8px; - --ty-menu-item-padding-vertical: 15px 20px; - --ty-native-select-sm-padding: 3px 25px 3px 7px; - --ty-native-select-md-padding: 6px 25px 6px 7px; - --ty-native-select-lg-padding: 9px 25px 9px 7px; - --ty-notification-width: 380px; - --ty-notification-margin: 20px; - --ty-popover-arrow-size: 8px; - --ty-select-selected-font-weight: 600; - --ty-select-dropdown-max-height: 300px; - --ty-select-dropdown-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); - --ty-slider-size: 12px; - --ty-slider-track-size: 4px; - --ty-steps-title-font-size: 16px; - --ty-strength-indicator-border-radius: 99px; - --ty-switch-md-font-size: 12px; - --ty-switch-sm-font-size: 9px; - --ty-switch-lg-font-size: 14px; - --ty-textarea-padding: 5px; - --ty-tooltip-arrow-size: 4px; - --ty-tooltip-content-padding: 5px 8px; -} - -@media (prefers-color-scheme: dark) { - html[data-tiny-theme=system] { - --ty-color-bg: #141414; - --ty-color-bg-elevated: #1f1f1f; - --ty-color-bg-container: #1f1f1f; - --ty-color-bg-spotlight: #2a2a2a; - --ty-color-bg-disabled: #2a2a2a; - --ty-color-bg-layout: #141414; - --ty-color-text: rgba(255, 255, 255, 0.85); - --ty-color-text-secondary: rgba(255, 255, 255, 0.65); - --ty-color-text-tertiary: rgba(255, 255, 255, 0.45); - --ty-color-text-quaternary: rgba(255, 255, 255, 0.25); - --ty-color-text-heading: rgba(255, 255, 255, 0.85); - --ty-color-text-label: rgba(255, 255, 255, 0.85); - --ty-color-text-placeholder: #5c5c5c; - --ty-color-primary: #9065d0; - --ty-color-primary-hover: #a882dc; - --ty-color-primary-active: #7a50bf; - --ty-color-primary-bg: #1a1325; - --ty-color-primary-border: #5b3d8f; - --ty-color-primary-bg-hover: #231a33; - --ty-color-primary-text-hover: #a882dc; - --ty-color-border: #424242; - --ty-color-border-secondary: #363636; - --ty-color-border-light: #303030; - --ty-color-border-btn-default: #424242; - --ty-color-fill: #262626; - --ty-color-fill-secondary: #2a2a2a; - --ty-color-fill-tertiary: #303030; - --ty-color-success: #49aa19; - --ty-color-success-hover: #6abe39; - --ty-color-success-active: #3c8c14; - --ty-color-success-bg: #162312; - --ty-color-success-border: #274916; - --ty-color-success-text: #6abe39; - --ty-color-warning: #d89614; - --ty-color-warning-hover: #e8b339; - --ty-color-warning-active: #b37a10; - --ty-color-warning-bg: #2b2111; - --ty-color-warning-border: #594214; - --ty-color-warning-text: #e8b339; - --ty-color-danger: #d32029; - --ty-color-danger-hover: #e84749; - --ty-color-danger-active: #ab1a20; - --ty-color-danger-bg: #2a1215; - --ty-color-danger-border: #58181c; - --ty-color-danger-text: #e84749; - --ty-color-info: #177ddc; - --ty-color-info-hover: #3c9ae8; - --ty-color-info-active: #1268b3; - --ty-color-info-bg: #111d2c; - --ty-color-info-border: #15395b; - --ty-color-info-text: #3c9ae8; - --ty-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3); - --ty-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4); - --ty-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.5); - --ty-shadow-popup: 0 3px 6px -4px rgba(0, 0, 0, 0.48) , 0 6px 16px 0 rgba(0, 0, 0, 0.32) , 0 9px 28px 8px rgba(0, 0, 0, 0.2); - --ty-shadow-card: 0 1px 6px rgba(0, 0, 0, 0.35); - --ty-shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.45); - --ty-shadow-btn: inset 0 1px 0 rgba(255, 255, 255, 0.05) , 0 1px 1px rgba(0, 0, 0, 0.2); - --ty-color-overlay-bg: rgba(0, 0, 0, 0.65); - --ty-color-overlay-inverted: rgba(50, 50, 50, 0.75); - --ty-btn-default-color: rgba(255, 255, 255, 0.85); - --ty-btn-default-bg: #1f1f1f; - --ty-btn-default-border: #424242; - --ty-btn-default-hover-bg: #1f1f1f; - --ty-btn-default-hover-border: #9065d0; - --ty-btn-default-hover-color: #9065d0; - --ty-btn-default-active-bg: #2a2a2a; - --ty-btn-default-active-border: #9065d0; - --ty-btn-default-active-color: #9065d0; - --ty-btn-disabled-color: rgba(255, 255, 255, 0.25); - --ty-btn-disabled-bg: #2a2a2a; - --ty-btn-disabled-border: #424242; - --ty-btn-loading-bg: #1f1f1f; - --ty-btn-ghost-hover-bg: #1a1325; - --ty-btn-ghost-active-bg: #231a33; - --ty-btn-outline-hover-bg: #1a1325; - --ty-btn-outline-active-bg: #231a33; - --ty-btn-link-disabled-color: rgba(255, 255, 255, 0.25); - --ty-input-bg: #1f1f1f; - --ty-input-border: #424242; - --ty-input-disabled-bg: #2a2a2a; - --ty-input-disabled-color: rgba(255, 255, 255, 0.25); - --ty-input-addon-bg: #262626; - --ty-input-focus-shadow: 0 0 0 3px rgba(144, 101, 208, 0.2); - --ty-input-focus-border: rgba(144, 101, 208, 0.8); - --ty-select-dropdown-bg: #1f1f1f; - --ty-select-option-active-bg: #2a2a2a; - --ty-select-option-selected-bg: #1a1325; - --ty-select-option-disabled-bg: #1f1f1f; - --ty-card-bg: #1f1f1f; - --ty-card-border: #363636; - --ty-card-header-color: rgba(255, 255, 255, 0.85); - --ty-card-shadow-border: rgba(0, 0, 0, 0.2); - --ty-modal-bg: #1f1f1f; - --ty-modal-header-bg: #1f1f1f; - --ty-modal-header-border: #363636; - --ty-modal-footer-border: #363636; - --ty-drawer-bg: #1f1f1f; - --ty-drawer-border: #363636; - --ty-menu-light-bg: #1f1f1f; - --ty-menu-light-color: rgba(255, 255, 255, 0.85); - --ty-menu-light-border: #303030; - --ty-menu-dark-bg: #001529; - --ty-menu-dark-color: rgba(255, 255, 255, 0.65); - --ty-menu-dark-border: #001529; - --ty-menu-divider-color: rgba(255, 255, 255, 0.1); - --ty-menu-group-title-color: rgba(255, 255, 255, 0.45); - --ty-notification-bg: #1f1f1f; - --ty-notification-close-color: rgba(255, 255, 255, 0.2); - --ty-notification-close-hover: rgba(255, 255, 255, 0.7); - --ty-message-bg: #1f1f1f; - --ty-badge-shadow: 0 0 0 1.5px #1f1f1f; - --ty-tag-bg: #262626; - --ty-tag-border: #424242; - --ty-tag-checkable-bg: #1f1f1f; - --ty-tag-magenta-color: #e0529c; - --ty-tag-magenta-bg: #291321; - --ty-tag-magenta-border: #55162b; - --ty-tag-red-color: #e84749; - --ty-tag-red-bg: #2a1215; - --ty-tag-red-border: #58181c; - --ty-tag-volcano-color: #e87040; - --ty-tag-volcano-bg: #2b1611; - --ty-tag-volcano-border: #592716; - --ty-tag-orange-color: #e89a3c; - --ty-tag-orange-bg: #2b1d11; - --ty-tag-orange-border: #593815; - --ty-tag-gold-color: #e8b339; - --ty-tag-gold-bg: #2b2111; - --ty-tag-gold-border: #594214; - --ty-tag-lime-color: #8bbb11; - --ty-tag-lime-bg: #1a2611; - --ty-tag-lime-border: #3e4f13; - --ty-tag-green-color: #6abe39; - --ty-tag-green-bg: #162312; - --ty-tag-green-border: #274916; - --ty-tag-cyan-color: #33bcb7; - --ty-tag-cyan-bg: #112123; - --ty-tag-cyan-border: #144848; - --ty-tag-blue-color: #3c9ae8; - --ty-tag-blue-bg: #111d2c; - --ty-tag-blue-border: #15395b; - --ty-tag-geekblue-color: #5273e0; - --ty-tag-geekblue-bg: #131a2e; - --ty-tag-geekblue-border: #1c2d57; - --ty-tag-purple-color: #854eca; - --ty-tag-purple-bg: #1a1325; - --ty-tag-purple-border: #301c4d; - --ty-tabs-border: #303030; - --ty-tabs-card-bg: #262626; - --ty-tabs-card-active-bg: #1f1f1f; - --ty-collapse-bg: #262626; - --ty-collapse-border: #424242; - --ty-collapse-content-bg: #1f1f1f; - --ty-collapse-header-hover-bg: #303030; - --ty-collapse-borderless-bg: #1f1f1f; - --ty-descriptions-label-bg: #262626; - --ty-descriptions-border: #363636; - --ty-steps-tail-color: #424242; - --ty-steps-icon-bg: #1f1f1f; - --ty-timeline-line-color: #363636; - --ty-timeline-dot-bg: #1f1f1f; - --ty-timeline-head-bg: #1f1f1f; - --ty-slider-rail-bg: #363636; - --ty-slider-thumb-bg: #1f1f1f; - --ty-slider-thumb-border: #9065d0; - --ty-slider-dot-bg: #1f1f1f; - --ty-slider-dot-border: #424242; - --ty-slider-dot-active-border: #9065d0; - --ty-slider-mark-color: rgba(255, 255, 255, 0.4); - --ty-slider-mark-active-color: rgba(255, 255, 255, 0.7); - --ty-progress-trail-bg: #363636; - --ty-progress-text-color: rgba(255, 255, 255, 0.65); - --ty-progress-circle-trail: #363636; - --ty-skeleton-bg: #303030; - --ty-skeleton-shimmer: linear-gradient(to right, #303030 25%, #3a3a3a 37%, #303030 63%); - --ty-kbd-bg: #2a2a2a; - --ty-kbd-border: #424242; - --ty-kbd-border-bottom: #363636; - --ty-kbd-color: rgba(255, 255, 255, 0.85); - --ty-kbd-shadow: inset 0 -1px 0 #363636; - --ty-transfer-border: #424242; - --ty-transfer-header-bg: #1f1f1f; - --ty-transfer-item-hover-bg: #2a2a2a; - --ty-transfer-footer-bg: #1f1f1f; - --ty-transfer-footer-border: #303030; - --ty-upload-dragger-bg: #262626; - --ty-upload-dragger-border: #424242; - --ty-upload-dragger-hover-bg: #303030; - --ty-upload-item-hover-bg: #2a2a2a; - --ty-picker-input-bg: #1f1f1f; - --ty-picker-dropdown-bg: #1f1f1f; - --ty-picker-cell-hover-bg: #2a2a2a; - --ty-picker-cell-selected-hover-bg: #7a50bf; - --ty-picker-cell-disabled-bg: #2a2a2a; - --ty-picker-clear-bg: #1f1f1f; - --ty-color-picker-bg: #1f1f1f; - --ty-color-picker-border: #424242; - --ty-list-border: #363636; - --ty-speed-dial-bg: #9065d0; - --ty-speed-dial-color: #fff; - --ty-speed-dial-bg-hover: #7a50bf; - --ty-speed-dial-action-bg: #1f1f1f; - --ty-speed-dial-action-color: rgba(255, 255, 255, 0.85); - --ty-speed-dial-action-bg-hover: #2a2a2a; - --ty-speed-dial-tooltip-bg: #363636; - --ty-speed-dial-tooltip-color: rgba(255, 255, 255, 0.85); - --ty-split-bar-bg: #262626; - --ty-split-bar-border: #424242; - --ty-split-bar-line: #525252; - --ty-popup-light-bg: #1f1f1f; - --ty-popup-dark-bg: #363636; - --ty-popup-arrow-shadow: rgba(0, 0, 0, 0.2); - --ty-layout-sidebar-bg: #12131a; - --ty-layout-sidebar-trigger-bg: rgb(0, 33, 64); - --ty-layout-sidebar-light-bg: #1f1f1f; - --ty-layout-sidebar-light-color: rgba(255, 255, 255, 0.85); - --ty-layout-sidebar-light-trigger-bg: #2a2a2a; - --ty-layout-sidebar-light-trigger-icon: #666; - --ty-anchor-bg: #1f1f1f; - --ty-anchor-ink-bg: #303030; - --ty-anchor-ball-bg: #1f1f1f; - --ty-form-notice-bg: #2b2111; - --ty-form-notice-color: rgba(255, 255, 255, 0.65); - --ty-form-error-color: #e84749; - --ty-form-error-hover: #d32029; - --ty-checkbox-bg: #1f1f1f; - --ty-checkbox-border: #424242; - --ty-checkbox-disabled-bg: #2a2a2a; - --ty-checkbox-check-color: #fff; - --ty-radio-bg: #1f1f1f; - --ty-radio-disabled-border: #424242; - --ty-radio-disabled-dot: rgba(255, 255, 255, 0.2); - --ty-switch-bg: rgba(255, 255, 255, 0.25); - --ty-switch-thumb-bg: #e8e8e8; - --ty-switch-thumb-border: rgba(255, 255, 255, 0.25); - --ty-switch-thumb-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.4); - --ty-divider-color: #363636; - --ty-divider-text-color: rgba(255, 255, 255, 0.85); - --ty-popover-dark-border: #525252; - --ty-native-select-bg: #1f1f1f; - --ty-native-select-disabled-bg: #2a2a2a; - --ty-native-select-disabled-color: rgba(255, 255, 255, 0.25); - --ty-pagination-bg: #1f1f1f; - --ty-pagination-disabled-bg: #2a2a2a; - --ty-pagination-disabled-active-bg: #424242; - --ty-pagination-disabled-color: #525252; - --ty-typography-heading-color: rgba(255, 255, 255, 0.85); - --ty-typography-body-color: rgba(255, 255, 255, 0.65); - --ty-typography-code-bg: rgba(255, 255, 255, 0.06); - --ty-typography-code-border: rgba(255, 255, 255, 0.06); - --ty-typography-mark-bg: #594214; - --ty-result-content-bg: #262626; - --ty-empty-desc-color: rgba(255, 255, 255, 0.35); - --ty-carousel-arrow-bg: rgba(255, 255, 255, 0.15); - --ty-carousel-arrow-hover-bg: rgba(255, 255, 255, 0.25); - --ty-carousel-dot-bg: rgba(255, 255, 255, 0.3); - --ty-carousel-dot-hover-bg: rgba(255, 255, 255, 0.6); - --ty-carousel-dot-active-bg: #fff; - --ty-avatar-bg: #555; - --ty-avatar-color: #e8e8e8; - --ty-avatar-border: #1f1f1f; - --ty-avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f; - --ty-avatar-offline-color: #525252; - --ty-back-top-bg: rgba(255, 255, 255, 0.2); - --ty-input-number-control-border: #424242; - --ty-input-number-control-active-bg: #2a2a2a; - --ty-input-number-icon-color: #666; - --ty-tree-arrow-color: #666; - --ty-tree-hover-bg: #2a2a2a; - --ty-textarea-counter-color: rgba(255, 255, 255, 0.45); - --ty-table-header-bg: #262626; - --ty-table-border: #363636; - --ty-table-hover: #2a2a2a; - --ty-table-selected-bg: rgba(144, 101, 208, 0.1); - --ty-segmented-bg: #2a2a2a; - --ty-segmented-active-bg: #1f1f1f; - --ty-cascader-bg: #1f1f1f; - --ty-cascader-border: #424242; - --ty-cascader-dropdown-bg: #1f1f1f; - --ty-cascader-hover: #2a2a2a; - --ty-cascader-selected-bg: rgba(144, 101, 208, 0.1); - --ty-calendar-bg: #1f1f1f; - --ty-calendar-border: #363636; - --ty-calendar-hover: #2a2a2a; - --ty-font-family: -apple-system, blinkmacsystemfont, Segoe UI, roboto, Helvetica Neue, arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; - --ty-font-family-monospace: lucida console, consolas, monaco, andale mono, ubuntu mono, monospace; - --ty-font-size-base: 1rem; - --ty-font-size-sm: 0.875rem; - --ty-font-size-lg: 1.25rem; - --ty-font-weight: 400; - --ty-line-height-base: 1.5; - --ty-headings-font-weight: 500; - --ty-h1-font-size: 2.5rem; - --ty-h2-font-size: 2rem; - --ty-h3-font-size: 1.75rem; - --ty-h4-font-size: 1.5rem; - --ty-h5-font-size: 1.25rem; - --ty-h6-font-size: 1rem; - --ty-border-radius: 2px; - --ty-height-sm: 24px; - --ty-height-md: 32px; - --ty-height-lg: 42px; - --ty-spacer: 1rem; - --ty-chart-1: #9065d0; - --ty-chart-2: #177ddc; - --ty-chart-3: #49aa19; - --ty-chart-4: #d89614; - --ty-chart-5: #d32029; - --ty-btn-min-width: 50px; - --ty-btn-round-radius: 30px; - --ty-btn-group-gap: 10px; - --ty-btn-group-divider-color: rgba(255, 255, 255, 0.2); - --ty-input-affix-margin: 0 8px; - --ty-input-clear-size: 14px; - --ty-input-addon-padding: 0 7px; - --ty-card-header-font-size: 16px; - --ty-card-header-font-weight: 500; - --ty-select-option-padding: 7px 12px; - --ty-select-option-font-size: 14px; - --ty-select-tag-height: 22px; - --ty-select-suffix-size: 14px; - --ty-notification-padding: 16px 24px; - --ty-notification-border-radius: 3px; - --ty-notification-title-font-size: 16px; - --ty-alert-border-radius: 3px; - --ty-avatar-border-radius: 2px; - --ty-badge-font-size: 12px; - --ty-badge-size: 18px; - --ty-badge-dot-size: 6px; - --ty-btn-padding-sm: 0 10px; - --ty-btn-padding-md: 0 15px; - --ty-btn-padding-lg: 0 28px; - --ty-btn-loading-opacity: 0.35; - --ty-btn-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - --ty-card-header-padding: 13px 16px; - --ty-card-body-padding: 16px; - --ty-card-footer-padding: 5px 16px 16px; - --ty-description-sm-padding-vt: 8px; - --ty-description-md-padding-vt: 12px; - --ty-description-lg-padding-vt: 16px; - --ty-description-sm-padding-hr: 16px; - --ty-description-md-padding-hr: 24px; - --ty-description-lg-padding-hr: 24px; - --ty-input-sm-padding: 0 4px; - --ty-input-md-padding: 0 6px; - --ty-input-lg-padding: 0 8px; - --ty-menu-item-padding-vertical: 15px 20px; - --ty-native-select-sm-padding: 3px 25px 3px 7px; - --ty-native-select-md-padding: 6px 25px 6px 7px; - --ty-native-select-lg-padding: 9px 25px 9px 7px; - --ty-notification-width: 380px; - --ty-notification-margin: 20px; - --ty-popover-arrow-size: 8px; - --ty-select-selected-font-weight: 600; - --ty-select-dropdown-max-height: 300px; - --ty-select-dropdown-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05); - --ty-slider-size: 12px; - --ty-slider-track-size: 4px; - --ty-steps-title-font-size: 16px; - --ty-strength-indicator-border-radius: 99px; - --ty-switch-md-font-size: 12px; - --ty-switch-sm-font-size: 9px; - --ty-switch-lg-font-size: 14px; - --ty-textarea-padding: 5px; - --ty-tooltip-arrow-size: 4px; - --ty-tooltip-content-padding: 5px 8px; - } -} -.ty-config-provider { - display: contents; -} - -/* stylelint-disable scss/comment-no-empty */ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ -html { - font-size: 14px; - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; - -moz-text-size-adjust: 100%; - text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ -/** - * Remove the margin in all browsers. - */ -body { - margin: 0; - font-family: var(--ty-font-family); - font-size: var(--ty-font-size-base); - font-weight: var(--ty-font-weight); - line-height: var(--ty-line-height-base); - color: var(--ty-color-text); -} - -/** - * Render the `main` element consistently in IE. - */ -main { - display: block; -} - -h6, h5, h4, h3, h2, h1 { - margin-top: 0; - margin-bottom: 0.5rem; - font-weight: var(--ty-headings-font-weight); - line-height: 1.2; -} - -h1 { - font-size: var(--ty-h1-font-size); -} - -h2 { - font-size: var(--ty-h2-font-size); -} - -h3 { - font-size: var(--ty-h3-font-size); -} - -h4 { - font-size: var(--ty-h4-font-size); -} - -h5 { - font-size: var(--ty-h5-font-size); -} - -h6 { - font-size: var(--ty-h6-font-size); -} - -/* Grouping content - ========================================================================== */ -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ -/** - * Remove the gray background on active links in IE 10. - */ -a { - background-color: transparent; - color: var(--ty-color-primary); - text-decoration: none; - cursor: pointer; -} -a:hover { - color: var(--ty-color-primary-hover); - text-decoration: underline; -} - -a:not([href]), a:not([href]):hover { - text-decoration: none; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ -pre, -code, -kbd, -samp { - font-family: var(--ty-font-family-monospace); /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: 0; -} - -/* Embedded content - ========================================================================== */ -/** - * Remove the border on images inside links in IE 10. - */ -img { - border-style: none; -} - -/* Forms - ========================================================================== */ -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ -button, -select { /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ -button, -[type=button], -[type=reset], -[type=submit] { - -webkit-appearance: button; - -moz-appearance: button; - appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ -button::-moz-focus-inner, -[type=button]::-moz-focus-inner, -[type=reset]::-moz-focus-inner, -[type=submit]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ -button:-moz-focusring, -[type=button]:-moz-focusring, -[type=reset]:-moz-focusring, -[type=submit]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ -[type=checkbox], -[type=radio] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ -[type=number]::-webkit-inner-spin-button, -[type=number]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ -[type=search] { - -webkit-appearance: textfield; - -moz-appearance: textfield; - appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ -[type=search]::-webkit-search-decoration { - -webkit-appearance: none; - appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ -::-webkit-file-upload-button { - -webkit-appearance: button; - appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ -/** - * Add the correct display in IE 10+. - */ -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ -[hidden] { - display: none; -} - -@keyframes ty-rotate { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} -@keyframes ty-rotate-reverse { - from { - transform: rotate(0); - } - to { - transform: rotate(-360deg); - } -} -@keyframes ty-processing { - 0% { - transform: scale(0.8); - opacity: 0.5; - } - 100% { - transform: scale(2.8); - opacity: 0; - } -} \ No newline at end of file From 4be14715cbb8e89832c0ca7620a2155e7cf5433c Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 19:50:41 +1000 Subject: [PATCH 07/11] fix(config-provider): ref-count global styles for multiple providers Add global-style-registry with acquire/release pattern so multiple concurrent ConfigProvider instances don't clobber each other's theme mode or token overrides on . DOM cleanup only fires when the last provider holding a given property unmounts. --- .../src/config-provider/config-provider.tsx | 23 ++++----- .../config-provider/global-style-registry.ts | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 packages/react/src/config-provider/global-style-registry.ts diff --git a/packages/react/src/config-provider/config-provider.tsx b/packages/react/src/config-provider/config-provider.tsx index 6c23eb15..226c1b39 100644 --- a/packages/react/src/config-provider/config-provider.tsx +++ b/packages/react/src/config-provider/config-provider.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo } from 'react'; import { ConfigContext, ThemeMode } from './config-context'; import { ConfigProviderProps } from './types'; import { buildCssVars, ThemeConfig } from './token-utils'; +import { acquireMode, releaseMode, acquireProps, releaseProps } from './global-style-registry'; import IntlProvider from '../intl-provider'; function isThemeConfig(theme: unknown): theme is ThemeConfig { @@ -18,29 +19,21 @@ const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { [themeConfig] ); - // Apply theme mode to attribute, clean up when mode is removed useEffect(() => { - const html = document.documentElement; - if (mode) { - html.setAttribute('data-tiny-theme', mode); - } + if (!mode) return; + acquireMode(mode); return () => { - html.removeAttribute('data-tiny-theme'); + releaseMode(); }; }, [mode]); - // Apply token overrides as inline styles on , clean up on unmount/change useEffect(() => { if (!cssVars) return; - const html = document.documentElement; - const keys = Object.keys(cssVars); - for (const key of keys) { - html.style.setProperty(key, (cssVars as Record)[key]); - } + const vars = cssVars as Record; + acquireProps(vars); + const keys = Object.keys(vars); return () => { - for (const key of keys) { - html.style.removeProperty(key); - } + releaseProps(keys); }; }, [cssVars]); diff --git a/packages/react/src/config-provider/global-style-registry.ts b/packages/react/src/config-provider/global-style-registry.ts new file mode 100644 index 00000000..33cce78f --- /dev/null +++ b/packages/react/src/config-provider/global-style-registry.ts @@ -0,0 +1,47 @@ +/** + * Ref-counted registry for global styles and attributes on . + * + * Multiple ConfigProvider instances may coexist. Each one calls acquire() + * when it applies a value and release() when it unmounts or its deps change. + * The actual DOM mutation (set / remove) only happens on the first acquire + * and when the last holder releases. + */ + +// theme-mode ref count +let modeCount = 0; + +export function acquireMode(mode: string): void { + modeCount++; + document.documentElement.setAttribute('data-tiny-theme', mode); +} + +export function releaseMode(): void { + modeCount = Math.max(0, modeCount - 1); + if (modeCount === 0) { + document.documentElement.removeAttribute('data-tiny-theme'); + } +} + +// CSS custom-property ref counts (key → count) +const propCounts = new Map(); + +export function acquireProps(vars: Record): void { + const style = document.documentElement.style; + for (const [key, value] of Object.entries(vars)) { + propCounts.set(key, (propCounts.get(key) ?? 0) + 1); + style.setProperty(key, value); + } +} + +export function releaseProps(keys: string[]): void { + const style = document.documentElement.style; + for (const key of keys) { + const count = (propCounts.get(key) ?? 1) - 1; + if (count <= 0) { + propCounts.delete(key); + style.removeProperty(key); + } else { + propCounts.set(key, count); + } + } +} From bbaaf2fba76029e7de4278056e35f0c4bd8ca4e6 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 19:53:51 +1000 Subject: [PATCH 08/11] fix(config-provider): use stack-based registry for nested providers Replace ref-counting with per-key stacks so nested ConfigProviders correctly restore the parent's value on unmount instead of deleting it. Each provider gets a stable Symbol id via useRef. acquireMode/acquireProps push onto the stack; releaseMode/releaseProps pop and restore the previous entry. The topmost (last-registered) entry always wins. --- .../src/config-provider/config-provider.tsx | 13 ++-- .../config-provider/global-style-registry.ts | 63 ++++++++++++------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/packages/react/src/config-provider/config-provider.tsx b/packages/react/src/config-provider/config-provider.tsx index 226c1b39..f94a5b29 100644 --- a/packages/react/src/config-provider/config-provider.tsx +++ b/packages/react/src/config-provider/config-provider.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { ConfigContext, ThemeMode } from './config-context'; import { ConfigProviderProps } from './types'; import { buildCssVars, ThemeConfig } from './token-utils'; @@ -11,6 +11,7 @@ function isThemeConfig(theme: unknown): theme is ThemeConfig { const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { const { children, theme, locale, ...otherProps } = props; + const idRef = useRef(Symbol('ConfigProvider')); const themeConfig = isThemeConfig(theme) ? theme : undefined; const mode = themeConfig ? themeConfig.mode : (theme as ThemeMode | undefined); @@ -21,19 +22,21 @@ const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { useEffect(() => { if (!mode) return; - acquireMode(mode); + const id = idRef.current; + acquireMode(id, mode); return () => { - releaseMode(); + releaseMode(id); }; }, [mode]); useEffect(() => { if (!cssVars) return; + const id = idRef.current; const vars = cssVars as Record; - acquireProps(vars); + acquireProps(id, vars); const keys = Object.keys(vars); return () => { - releaseProps(keys); + releaseProps(id, keys); }; }, [cssVars]); diff --git a/packages/react/src/config-provider/global-style-registry.ts b/packages/react/src/config-provider/global-style-registry.ts index 33cce78f..831d5c80 100644 --- a/packages/react/src/config-provider/global-style-registry.ts +++ b/packages/react/src/config-provider/global-style-registry.ts @@ -1,47 +1,66 @@ /** - * Ref-counted registry for global styles and attributes on . + * Stack-based registry for global styles and attributes on . * - * Multiple ConfigProvider instances may coexist. Each one calls acquire() - * when it applies a value and release() when it unmounts or its deps change. - * The actual DOM mutation (set / remove) only happens on the first acquire - * and when the last holder releases. + * Multiple ConfigProvider instances may coexist and set overlapping values. + * Each provider gets a stable id (Symbol). Values are stacked per-key: + * the topmost (last-registered, not-yet-released) entry wins. + * When a provider unmounts, the previous value is restored automatically. */ -// theme-mode ref count -let modeCount = 0; +type Entry = { id: symbol; value: string }; -export function acquireMode(mode: string): void { - modeCount++; +// ---- theme mode ---- + +const modeStack: Entry[] = []; + +export function acquireMode(id: symbol, mode: string): void { + const idx = modeStack.findIndex((e) => e.id === id); + if (idx !== -1) modeStack.splice(idx, 1); + modeStack.push({ id, value: mode }); document.documentElement.setAttribute('data-tiny-theme', mode); } -export function releaseMode(): void { - modeCount = Math.max(0, modeCount - 1); - if (modeCount === 0) { +export function releaseMode(id: symbol): void { + const idx = modeStack.findIndex((e) => e.id === id); + if (idx !== -1) modeStack.splice(idx, 1); + if (modeStack.length > 0) { + document.documentElement.setAttribute('data-tiny-theme', modeStack[modeStack.length - 1].value); + } else { document.documentElement.removeAttribute('data-tiny-theme'); } } -// CSS custom-property ref counts (key → count) -const propCounts = new Map(); +// ---- CSS custom properties ---- + +const propStacks = new Map(); -export function acquireProps(vars: Record): void { +export function acquireProps(id: symbol, vars: Record): void { const style = document.documentElement.style; for (const [key, value] of Object.entries(vars)) { - propCounts.set(key, (propCounts.get(key) ?? 0) + 1); + let stack = propStacks.get(key); + if (!stack) { + stack = []; + propStacks.set(key, stack); + } + const idx = stack.findIndex((e) => e.id === id); + if (idx !== -1) stack.splice(idx, 1); + stack.push({ id, value }); style.setProperty(key, value); } } -export function releaseProps(keys: string[]): void { +export function releaseProps(id: symbol, keys: string[]): void { const style = document.documentElement.style; for (const key of keys) { - const count = (propCounts.get(key) ?? 1) - 1; - if (count <= 0) { - propCounts.delete(key); - style.removeProperty(key); + const stack = propStacks.get(key); + if (!stack) continue; + const idx = stack.findIndex((e) => e.id === id); + if (idx !== -1) stack.splice(idx, 1); + if (stack.length > 0) { + style.setProperty(key, stack[stack.length - 1].value); } else { - propCounts.set(key, count); + propStacks.delete(key); + style.removeProperty(key); } } } From 5bf1574d3eda51e179ceb119b22f0dfff4e8ec37 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Sun, 5 Apr 2026 21:03:49 +1000 Subject: [PATCH 09/11] refactor: configProvider --- .changeset/config-provider-infra-upgrade.md | 23 ++ .../src/_utils/__tests__/use-theme.test.tsx | 62 +++++ packages/react/src/_utils/dom.ts | 24 ++ packages/react/src/_utils/use-theme.ts | 92 ++++++-- packages/react/src/anchor/anchor.tsx | 17 +- packages/react/src/anchor/types.ts | 2 +- .../src/back-top/__tests__/back-top.test.tsx | 25 +- packages/react/src/back-top/back-top.tsx | 23 +- packages/react/src/back-top/index.md | 4 +- packages/react/src/back-top/index.zh_CN.md | 4 +- .../src/cascader/__tests__/cascader.test.tsx | 22 ++ packages/react/src/cascader/cascader.tsx | 74 ++++-- .../config-provider.test.tsx.snap | 16 -- .../__tests__/config-provider.test.tsx | 219 +++++++++++++++++- .../src/config-provider/config-context.tsx | 11 +- .../src/config-provider/config-provider.tsx | 118 +++++++--- .../src/config-provider/container-utils.ts | 16 ++ .../src/config-provider/demo/DynamicTheme.tsx | 41 ++++ .../src/config-provider/demo/NestedTheme.tsx | 41 ++++ .../src/config-provider/demo/PortalTheme.tsx | 39 ++++ .../config-provider/global-style-registry.ts | 66 ------ packages/react/src/config-provider/index.md | 139 ++++++++++- packages/react/src/config-provider/index.tsx | 3 + .../react/src/config-provider/index.zh_CN.md | 139 ++++++++++- .../react/src/config-provider/scroll-lock.ts | 64 +++++ .../src/config-provider/static-config.tsx | 19 ++ .../react/src/config-provider/static-host.ts | 39 ++++ packages/react/src/config-provider/types.ts | 9 + .../__tests__/copy-to-clipboard.test.tsx | 28 ++- .../react/src/copy-to-clipboard/clipboard.ts | 40 ++++ .../copy-to-clipboard/copy-to-clipboard.tsx | 44 ++-- packages/react/src/copy-to-clipboard/index.md | 3 +- .../src/copy-to-clipboard/index.zh_CN.md | 3 +- packages/react/src/copy-to-clipboard/types.ts | 1 + packages/react/src/loading-bar/index.ts | 24 +- .../react/src/message/message-container.tsx | 23 +- .../react/src/modal/__tests__/modal.test.tsx | 67 +++++- packages/react/src/modal/index.md | 23 +- packages/react/src/modal/index.tsx | 12 +- packages/react/src/modal/index.zh_CN.md | 34 ++- packages/react/src/modal/static-modal.tsx | 68 ++++++ .../notification/notification-container.tsx | 23 +- .../src/overlay/__tests__/overlay.test.tsx | 20 ++ packages/react/src/overlay/overlay.tsx | 22 +- packages/react/src/popup/popup.tsx | 8 +- packages/react/src/portal/portal.tsx | 9 +- packages/react/src/sticky/sticky.tsx | 36 ++- .../react/src/tour/__tests__/tour.test.tsx | 20 ++ packages/react/src/tour/tour.tsx | 17 +- packages/tokens/scss/_theme.scss | 6 +- 50 files changed, 1585 insertions(+), 297 deletions(-) create mode 100644 .changeset/config-provider-infra-upgrade.md create mode 100644 packages/react/src/_utils/__tests__/use-theme.test.tsx delete mode 100644 packages/react/src/config-provider/__tests__/__snapshots__/config-provider.test.tsx.snap create mode 100644 packages/react/src/config-provider/container-utils.ts create mode 100644 packages/react/src/config-provider/demo/DynamicTheme.tsx create mode 100644 packages/react/src/config-provider/demo/NestedTheme.tsx create mode 100644 packages/react/src/config-provider/demo/PortalTheme.tsx delete mode 100644 packages/react/src/config-provider/global-style-registry.ts create mode 100644 packages/react/src/config-provider/scroll-lock.ts create mode 100644 packages/react/src/config-provider/static-config.tsx create mode 100644 packages/react/src/config-provider/static-host.ts create mode 100644 packages/react/src/copy-to-clipboard/clipboard.ts create mode 100644 packages/react/src/modal/static-modal.tsx diff --git a/.changeset/config-provider-infra-upgrade.md b/.changeset/config-provider-infra-upgrade.md new file mode 100644 index 00000000..936a37b9 --- /dev/null +++ b/.changeset/config-provider-infra-upgrade.md @@ -0,0 +1,23 @@ +--- +"@tiny-design/react": minor +--- + +Upgrade the global configuration infrastructure around `ConfigProvider` and align popup, scroll, and static layer behavior across the component library. + +Highlights: + +- Reworked `ConfigProvider` to use provider-scoped theme containers instead of mutating global HTML styles. +- Added `ConfigProvider.useConfig()` and `ConfigProvider.config({ holderRender })` support for a wider set of static APIs. +- Added static `Modal.open()` and `Modal.confirm()` APIs that participate in the shared static host pipeline. +- Unified popup container resolution across `Portal`, `Popup`, and `Cascader`. +- Unified target container resolution across `Anchor`, `Sticky`, `BackTop`, `Overlay`, and `Tour`. +- Improved `Sticky` container observation with `ResizeObserver`. +- Improved `useTheme()` to sync with DOM state, localStorage, system preference changes, and cross-tab storage events. +- Added `onCopy` to `CopyToClipboard` so copy results can be observed by consumers. + +Notes for consumers: + +- `Anchor` and `BackTop` now accept and resolve `Window` as a first-class target container shape. +- `BackTop` now defaults to `ConfigProvider.getTargetContainer()` when present. +- `ConfigProvider` only renders an internal scope node when scoped theme behavior is required. +- Static APIs such as `Message.*`, `Notification.*`, `LoadingBar.*`, and `Modal.open()` can now be wrapped consistently through `ConfigProvider.config({ holderRender })`. diff --git a/packages/react/src/_utils/__tests__/use-theme.test.tsx b/packages/react/src/_utils/__tests__/use-theme.test.tsx new file mode 100644 index 00000000..1313104f --- /dev/null +++ b/packages/react/src/_utils/__tests__/use-theme.test.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { useTheme } from '../use-theme'; + +const ThemeConsumer = () => { + const { mode, resolvedTheme, setMode, toggle } = useTheme(); + + return ( +
+ {mode} + {resolvedTheme} + + +
+ ); +}; + +describe('useTheme', () => { + beforeEach(() => { + localStorage.clear(); + document.documentElement.removeAttribute('data-tiny-theme'); + }); + + it('should initialize from the current DOM theme attribute', () => { + document.documentElement.setAttribute('data-tiny-theme', 'dark'); + + render(); + + expect(screen.getByTestId('mode').textContent).toBe('dark'); + expect(screen.getByTestId('resolved').textContent).toBe('dark'); + }); + + it('should persist theme updates to localStorage and html attribute', () => { + render(); + + act(() => { + screen.getByText('Set dark').click(); + }); + + expect(screen.getByTestId('mode').textContent).toBe('dark'); + expect(localStorage.getItem('ty-theme')).toBe('dark'); + expect(document.documentElement.getAttribute('data-tiny-theme')).toBe('dark'); + }); + + it('should react to storage events from other tabs', () => { + render(); + + act(() => { + window.dispatchEvent(new StorageEvent('storage', { + key: 'ty-theme', + newValue: 'dark', + })); + }); + + expect(screen.getByTestId('mode').textContent).toBe('dark'); + expect(document.documentElement.getAttribute('data-tiny-theme')).toBe('dark'); + }); +}); diff --git a/packages/react/src/_utils/dom.ts b/packages/react/src/_utils/dom.ts index 4d0ee98f..fb8ad9b0 100755 --- a/packages/react/src/_utils/dom.ts +++ b/packages/react/src/_utils/dom.ts @@ -56,3 +56,27 @@ export const getNodeScrollHeight = (node: Target): number => { } return (node as HTMLElement).scrollHeight; }; + +export const getScrollParents = (node: HTMLElement | null): Array => { + if (typeof window === 'undefined' || !node) { + return []; + } + + const parents = new Set(); + let current: HTMLElement | null = node.parentElement; + + while (current) { + const style = window.getComputedStyle(current); + const overflow = `${style.overflow}${style.overflowX}${style.overflowY}`; + + if (/(auto|scroll|overlay)/.test(overflow)) { + parents.add(current); + } + + current = current.parentElement; + } + + parents.add(window); + + return Array.from(parents); +}; diff --git a/packages/react/src/_utils/use-theme.ts b/packages/react/src/_utils/use-theme.ts index cba4640c..56bde0b4 100644 --- a/packages/react/src/_utils/use-theme.ts +++ b/packages/react/src/_utils/use-theme.ts @@ -1,8 +1,8 @@ import { useSyncExternalStore, useCallback } from 'react'; - -export type ThemeMode = 'light' | 'dark' | 'system'; +import type { ThemeMode } from '../config-provider/config-context'; const STORAGE_KEY = 'ty-theme'; +const THEME_ATTR = 'data-tiny-theme'; function getSystemTheme(): 'light' | 'dark' { if (typeof window === 'undefined') return 'light'; @@ -11,7 +11,13 @@ function getSystemTheme(): 'light' | 'dark' { function applyTheme(mode: ThemeMode): void { if (typeof document === 'undefined') return; - document.documentElement.setAttribute('data-tiny-theme', mode); + document.documentElement.setAttribute(THEME_ATTR, mode); +} + +function readDomTheme(): ThemeMode | null { + if (typeof document === 'undefined') return null; + const value = document.documentElement.getAttribute(THEME_ATTR); + return value === 'light' || value === 'dark' || value === 'system' ? value : null; } function readStoredTheme(): ThemeMode { @@ -19,12 +25,16 @@ function readStoredTheme(): ThemeMode { return (localStorage.getItem(STORAGE_KEY) as ThemeMode) || 'light'; } +function readInitialTheme(): ThemeMode { + return readDomTheme() ?? readStoredTheme(); +} + // ---- Shared store ---- -let currentMode: ThemeMode = readStoredTheme(); +let currentMode: ThemeMode = readInitialTheme(); const listeners = new Set<() => void>(); function getSnapshot(): ThemeMode { - return currentMode; + return readDomTheme() ?? currentMode; } function getServerSnapshot(): ThemeMode { @@ -32,27 +42,81 @@ function getServerSnapshot(): ThemeMode { } function subscribe(cb: () => void): () => void { + const syncFromDom = () => { + const domTheme = readDomTheme(); + + if (domTheme && domTheme !== currentMode) { + currentMode = domTheme; + cb(); + } + }; + listeners.add(cb); - return () => listeners.delete(cb); + syncFromDom(); + + const observer = + typeof MutationObserver !== 'undefined' && typeof document !== 'undefined' + ? new MutationObserver(() => { + syncFromDom(); + }) + : null; + + observer?.observe(document.documentElement, { + attributes: true, + attributeFilter: [THEME_ATTR], + }); + + return () => { + listeners.delete(cb); + observer?.disconnect(); + }; } function setThemeMode(next: ThemeMode): void { currentMode = next; - localStorage.setItem(STORAGE_KEY, next); + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_KEY, next); + } applyTheme(next); listeners.forEach((cb) => cb()); } +function emit(): void { + listeners.forEach((cb) => cb()); +} + // Listen for system preference changes at module level -if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { - window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', () => { +if (typeof document !== 'undefined') { + applyTheme(currentMode); +} + +if (typeof window !== 'undefined') { + if (typeof window.matchMedia === 'function') { + const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); + const handleSystemThemeChange = () => { if (currentMode === 'system') { - // Force re-render for all subscribers so resolvedTheme updates - listeners.forEach((cb) => cb()); + emit(); } - }); + }; + + if (typeof mediaQueryList.addEventListener === 'function') { + mediaQueryList.addEventListener('change', handleSystemThemeChange); + } else if (typeof mediaQueryList.addListener === 'function') { + mediaQueryList.addListener(handleSystemThemeChange); + } + } + + window.addEventListener('storage', (event) => { + if (event.key !== STORAGE_KEY) { + return; + } + + currentMode = event.newValue === 'light' || event.newValue === 'dark' || event.newValue === 'system' + ? event.newValue + : 'light'; + applyTheme(currentMode); + emit(); + }); } // ---- Hook ---- diff --git a/packages/react/src/anchor/anchor.tsx b/packages/react/src/anchor/anchor.tsx index 0ab4ca91..64ae2ebb 100755 --- a/packages/react/src/anchor/anchor.tsx +++ b/packages/react/src/anchor/anchor.tsx @@ -1,6 +1,7 @@ import React, { useContext, useState, useCallback, useEffect, useRef, useMemo } from 'react'; import classNames from 'classnames'; import { ConfigContext } from '../config-provider/config-context'; +import { resolveTargetContainer } from '../config-provider/container-utils'; import { getPrefixCls } from '../_utils/general'; import { AnchorLinkProps, AnchorProps } from './types'; import { AnchorContext } from './anchor-context'; @@ -59,16 +60,17 @@ const Anchor = (props: AnchorProps): JSX.Element => { }, [prefixCls, type]); const getScrollContainer = useCallback((): HTMLElement | Window => { - return getContainer ? getContainer() : window; - }, [getContainer]); + return resolveTargetContainer(configContext, getContainer); + }, [configContext, getContainer]); const scrollToAnchor = useCallback( (anchorName: string): void => { const element = document.getElementById(anchorName); if (!element) return; - if (getContainer) { - const container = getContainer(); + const scrollContainer = resolveTargetContainer(configContext, getContainer); + if (scrollContainer && scrollContainer !== window) { + const container = scrollContainer as HTMLElement; const containerRect = container.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); container.scrollTo({ @@ -79,7 +81,7 @@ const Anchor = (props: AnchorProps): JSX.Element => { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, - [getContainer] + [configContext, getContainer] ); const handleLinkClick = (e: React.MouseEvent, anchorName: string) => { @@ -115,7 +117,8 @@ const Anchor = (props: AnchorProps): JSX.Element => { const links = linksRef.current; if (links.size === 0) return; - const container = getContainer?.(); + const targetContainer = resolveTargetContainer(configContext, getContainer); + const container = targetContainer && targetContainer !== window ? (targetContainer as HTMLElement) : undefined; const containerTop = container ? container.getBoundingClientRect().top : 0; @@ -171,7 +174,7 @@ const Anchor = (props: AnchorProps): JSX.Element => { } return newActiveId; }); - }, [onChange, getContainer, offsetTop]); + }, [configContext, onChange, getContainer, offsetTop]); useEffect(() => { updateInk(); diff --git a/packages/react/src/anchor/types.ts b/packages/react/src/anchor/types.ts index d3c840df..41b6216d 100644 --- a/packages/react/src/anchor/types.ts +++ b/packages/react/src/anchor/types.ts @@ -6,7 +6,7 @@ export interface AnchorProps extends BaseProps { type?: 'dot' | 'line'; offsetBottom?: number; offsetTop?: number; - getContainer?: () => HTMLElement; + getContainer?: () => HTMLElement | Window; onChange?: (currentActiveLink: string) => void; onClick?: (e: React.MouseEvent, link: { title: string; href: string }) => void; children?: React.ReactNode; diff --git a/packages/react/src/back-top/__tests__/back-top.test.tsx b/packages/react/src/back-top/__tests__/back-top.test.tsx index 61a9ad89..f4cce974 100644 --- a/packages/react/src/back-top/__tests__/back-top.test.tsx +++ b/packages/react/src/back-top/__tests__/back-top.test.tsx @@ -1,5 +1,6 @@ -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import BackTop from '../index'; +import ConfigProvider from '../../config-provider'; describe('', () => { it('should match the snapshot', () => { @@ -11,4 +12,26 @@ describe('', () => { const { container } = render(); expect(container).toBeEmptyDOMElement(); }); + + it('should use the configured target container by default', () => { + const container = document.createElement('div'); + Object.defineProperty(container, 'scrollTop', { + value: 400, + writable: true, + }); + document.body.appendChild(container); + + const { getByRole } = render( + container}> + + + ); + + expect(getByRole('button', { name: 'Back to top' })).toBeInTheDocument(); + + fireEvent.click(getByRole('button', { name: 'Back to top' })); + expect(container.scrollTop).toBeLessThanOrEqual(400); + + document.body.removeChild(container); + }); }); diff --git a/packages/react/src/back-top/back-top.tsx b/packages/react/src/back-top/back-top.tsx index f4bab5a5..fd7d0a84 100755 --- a/packages/react/src/back-top/back-top.tsx +++ b/packages/react/src/back-top/back-top.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; -import { Target } from '../_utils/dom'; +import { getScroll, Target } from '../_utils/dom'; import { ConfigContext } from '../config-provider/config-context'; +import { resolveTargetContainer } from '../config-provider/container-utils'; import { getPrefixCls } from '../_utils/general'; import { BackTopProps } from './types'; @@ -18,7 +19,7 @@ const easeInOutCubic = (t: number, b: number, c: number, d: number): number => { const BackTop = (props: BackTopProps): JSX.Element | null => { const { visibilityHeight = 300, - target = (): Target => window, + target, prefixCls: customisedCls, onClick, className, @@ -31,17 +32,17 @@ const BackTop = (props: BackTopProps): JSX.Element | null => { [`${prefixCls}_custom`]: !!children, }); const [visible, setVisible] = useState(true); + const resolvedTarget = useCallback( + (): Target => resolveTargetContainer(configContext, target), + [configContext, target] + ); const getDistanceFromTop = useCallback((): number => { - const targetNode = target(); - if (targetNode === window) { - return window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; - } - return (targetNode as HTMLElement).scrollTop; - }, [target]); + return getScroll(resolvedTarget(), true); + }, [resolvedTarget]); const setScrollToTop = (distance: number): void => { - const targetNode = target(); + const targetNode = resolvedTarget(); if (targetNode === window) { document.body.scrollTop = distance; document.documentElement.scrollTop = distance; @@ -76,14 +77,14 @@ const BackTop = (props: BackTopProps): JSX.Element | null => { }, [getDistanceFromTop, visible, visibilityHeight]); useEffect(() => { - const targetNode = target(); + const targetNode = resolvedTarget(); targetNode.addEventListener('scroll', handleOnScroll); handleOnScroll(); return (): void => { targetNode.removeEventListener('scroll', handleOnScroll); }; - }, [target, handleOnScroll]); + }, [resolvedTarget, handleOnScroll]); if (visible) { return ( diff --git a/packages/react/src/back-top/index.md b/packages/react/src/back-top/index.md index 67525982..c5ada677 100644 --- a/packages/react/src/back-top/index.md +++ b/packages/react/src/back-top/index.md @@ -49,8 +49,8 @@ Use custom children to replace the default button. | Property | Description | Type | Default | | ----------------- | ----------------------------------------------------------------------------- | ----------------- | ------------- | -| target | Specifies the scrollable area dom node | () => HTMLElement | () => window | +| target | Specifies the scrollable area dom node. Defaults to `ConfigProvider.getTargetContainer()` when available. | () => HTMLElement | Window | provider target container | | visibilityHeight | The `BackTop` button will not show until the scroll height reaches this value | number | 300 | | onClick | A callback function, which can be executed when you click the button | () => void | - | | style | Style object of container object | CSSProperties | - | -| className | ClassName of container | string | - | \ No newline at end of file +| className | ClassName of container | string | - | diff --git a/packages/react/src/back-top/index.zh_CN.md b/packages/react/src/back-top/index.zh_CN.md index 4777e99c..42d5a560 100644 --- a/packages/react/src/back-top/index.zh_CN.md +++ b/packages/react/src/back-top/index.zh_CN.md @@ -49,8 +49,8 @@ import { BackTop } from 'tiny-design'; | 属性 | 说明 | 类型 | 默认值 | | ----------------- | ------------------------------------------------------------------------- | ----------------- | ------------- | -| target | 指定可滚动区域的 DOM 节点 | () => HTMLElement | () => window | +| target | 指定可滚动区域的 DOM 节点;未设置时会优先使用 `ConfigProvider.getTargetContainer()` | () => HTMLElement | Window | provider target container | | visibilityHeight | 滚动高度达到此值时才出现 `BackTop` 按钮 | number | 300 | | onClick | 点击按钮时的回调 | () => void | - | | style | 容器的样式对象 | CSSProperties | - | -| className | 容器的类名 | string | - | \ No newline at end of file +| className | 容器的类名 | string | - | diff --git a/packages/react/src/cascader/__tests__/cascader.test.tsx b/packages/react/src/cascader/__tests__/cascader.test.tsx index 2acf6ec2..21a2e223 100644 --- a/packages/react/src/cascader/__tests__/cascader.test.tsx +++ b/packages/react/src/cascader/__tests__/cascader.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import Cascader from '../index'; +import ConfigProvider from '../../config-provider'; const options = [ { @@ -81,4 +82,25 @@ describe('', () => { ); expect(getByText('Zhejiang / Hangzhou / West Lake')).toBeInTheDocument(); }); + + it('should respect the configured popup container', () => { + const popupContainer = document.createElement('div'); + document.body.appendChild(popupContainer); + + const { container } = render( + popupContainer}> + + + ); + + const selector = container.querySelector('.ty-cascader__selector'); + fireEvent.click(selector!); + + const dropdown = popupContainer.querySelector('.ty-cascader__dropdown'); + + expect(dropdown).toBeTruthy(); + expect(dropdown?.parentElement).toBe(popupContainer); + + document.body.removeChild(popupContainer); + }); }); diff --git a/packages/react/src/cascader/cascader.tsx b/packages/react/src/cascader/cascader.tsx index eef8af5a..925d68f2 100644 --- a/packages/react/src/cascader/cascader.tsx +++ b/packages/react/src/cascader/cascader.tsx @@ -1,9 +1,11 @@ -import React, { useState, useEffect, useRef, useContext, useMemo } from 'react'; -import { createPortal } from 'react-dom'; +import React, { useState, useEffect, useRef, useContext, useMemo, useCallback } from 'react'; import classNames from 'classnames'; import { ConfigContext } from '../config-provider/config-context'; +import { resolvePopupContainer } from '../config-provider/container-utils'; +import { getScrollParents } from '../_utils/dom'; import { getPrefixCls } from '../_utils/general'; import { ArrowDown } from '../_utils/components'; +import Portal from '../portal'; import { CascaderProps, CascaderOption, CascaderValue } from './types'; const getOptionsByValue = ( @@ -67,6 +69,33 @@ const Cascader = React.forwardRef((props, ref) => } }, [props.open]); + const updateDropdownPosition = useCallback(() => { + if (!open || !wrapperRef.current || !dropdownRef.current) { + return; + } + + const rect = wrapperRef.current.getBoundingClientRect(); + const offsetParent = dropdownRef.current.offsetParent; + + if (offsetParent && offsetParent instanceof HTMLElement) { + const offsetParentRect = offsetParent.getBoundingClientRect(); + setDropdownStyle({ + position: 'absolute', + top: rect.bottom - offsetParentRect.top + offsetParent.scrollTop + 4, + left: rect.left - offsetParentRect.left + offsetParent.scrollLeft, + zIndex: 1050, + }); + return; + } + + setDropdownStyle({ + position: 'absolute', + top: rect.bottom + window.scrollY + 4, + left: rect.left + window.scrollX, + zIndex: 1050, + }); + }, [open]); + // Build columns from selected value on mount useEffect(() => { const cols: CascaderOption[][] = [options]; @@ -84,18 +113,30 @@ const Cascader = React.forwardRef((props, ref) => setActiveColumns(cols); }, [options, selectedValue]); - // Position dropdown below selector useEffect(() => { - if (open && wrapperRef.current) { - const rect = wrapperRef.current.getBoundingClientRect(); - setDropdownStyle({ - position: 'absolute', - top: rect.bottom + 4 + window.scrollY, - left: rect.left + window.scrollX, - zIndex: 1050, - }); + if (!open) { + return undefined; } - }, [open]); + + const popupContainer = resolvePopupContainer(configContext, wrapperRef.current); + const scrollTargets = new Set([ + ...getScrollParents(wrapperRef.current), + ...getScrollParents(popupContainer), + ]); + + updateDropdownPosition(); + window.addEventListener('resize', updateDropdownPosition); + scrollTargets.forEach((target) => { + target.addEventListener('scroll', updateDropdownPosition); + }); + + return () => { + window.removeEventListener('resize', updateDropdownPosition); + scrollTargets.forEach((target) => { + target.removeEventListener('scroll', updateDropdownPosition); + }); + }; + }, [configContext, open, updateDropdownPosition]); // Click outside useEffect(() => { @@ -195,7 +236,8 @@ const Cascader = React.forwardRef((props, ref) => }); const dropdown = open - ? createPortal( + ? ( +
{activeColumns.map((columnOptions, level) => ( @@ -232,9 +274,9 @@ const Cascader = React.forwardRef((props, ref) => ))}
-
, - document.body - ) +
+ + ) : null; return ( diff --git a/packages/react/src/config-provider/__tests__/__snapshots__/config-provider.test.tsx.snap b/packages/react/src/config-provider/__tests__/__snapshots__/config-provider.test.tsx.snap deleted file mode 100644 index 5637e732..00000000 --- a/packages/react/src/config-provider/__tests__/__snapshots__/config-provider.test.tsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should match the snapshot 1`] = ` - - - -`; diff --git a/packages/react/src/config-provider/__tests__/config-provider.test.tsx b/packages/react/src/config-provider/__tests__/config-provider.test.tsx index a28d1339..119bd393 100644 --- a/packages/react/src/config-provider/__tests__/config-provider.test.tsx +++ b/packages/react/src/config-provider/__tests__/config-provider.test.tsx @@ -1,16 +1,41 @@ import React from 'react'; -import { render } from '@testing-library/react'; -import ConfigProvider from '../index'; +import { act, render, screen } from '@testing-library/react'; +import ConfigProvider, { useConfig } from '../index'; import Button from '../../button'; +import Message from '../../message'; +import Notification from '../../notification'; +import LoadingBar from '../../loading-bar'; +import Modal from '../../modal'; describe('', () => { - it('should match the snapshot', () => { - const { asFragment } = render( + afterEach(() => { + document.documentElement.removeAttribute('data-tiny-theme'); + document.documentElement.removeAttribute('style'); + ConfigProvider.config({ holderRender: undefined }); + document.querySelectorAll('.ty-message-container').forEach((node) => node.remove()); + document.querySelectorAll('.ty-notification-container').forEach((node) => node.remove()); + document.querySelectorAll('#ty-loading-bar').forEach((node) => node.parentElement?.remove()); + }); + + it('should render provider scope and popup holder when scoped theme is enabled', () => { + const { container } = render( + + + + ); + expect(container.querySelector('.custom-config-provider')).toBeTruthy(); + expect(container.querySelector('.custom-config-provider__popup-holder')).toBeTruthy(); + }); + + it('should avoid rendering provider scope when scoped theme is not needed', () => { + const { container } = render( ); - expect(asFragment()).toMatchSnapshot(); + + expect(container.querySelector('.custom-config-provider')).toBeFalsy(); + expect(container.querySelector('.custom-config-provider__popup-holder')).toBeFalsy(); }); it('should render children', () => { @@ -30,4 +55,188 @@ describe('', () => { ); expect(container.querySelector('.my-btn')).toBeTruthy(); }); + + it('should restore parent theme mode when nested provider unmounts', () => { + const nested = render( + + +
Content
+
+
+ ); + + const providers = nested.container.querySelectorAll('.ty-config-provider'); + expect(providers[0].getAttribute('data-tiny-theme')).toBe('dark'); + expect(providers[1].getAttribute('data-tiny-theme')).toBe('light'); + + nested.rerender( + +
Content
+
+ ); + + expect(nested.container.querySelector('.ty-config-provider')?.getAttribute('data-tiny-theme')).toBe('dark'); + }); + + it('should restore parent token overrides when nested provider unmounts', () => { + const nested = render( + + +
Content
+
+
+ ); + + const providers = nested.container.querySelectorAll('.ty-config-provider'); + expect((providers[0] as HTMLElement).style.getPropertyValue('--ty-color-primary')).toBe('blue'); + expect((providers[1] as HTMLElement).style.getPropertyValue('--ty-color-primary')).toBe('red'); + + nested.rerender( + +
Content
+
+ ); + + expect( + (nested.container.querySelector('.ty-config-provider') as HTMLElement).style.getPropertyValue( + '--ty-color-primary' + ) + ).toBe('blue'); + }); + + it('should replace old token values when the same provider updates theme config', () => { + const { rerender } = render( + +
Content
+
+ ); + + const getProvider = () => document.querySelector('.ty-config-provider') as HTMLElement; + + expect(getProvider().style.getPropertyValue('--ty-color-primary')).toBe('blue'); + expect(getProvider().style.getPropertyValue('--ty-border-radius')).toBe('8px'); + + rerender( + +
Content
+
+ ); + + expect(getProvider().style.getPropertyValue('--ty-color-success')).toBe('green'); + expect(getProvider().style.getPropertyValue('--ty-color-primary')).toBe(''); + expect(getProvider().style.getPropertyValue('--ty-border-radius')).toBe(''); + }); + + it('should isolate different token keys across multiple providers', () => { + const nested = render( + + +
Content
+
+
+ ); + + const providers = nested.container.querySelectorAll('.ty-config-provider'); + expect((providers[0] as HTMLElement).style.getPropertyValue('--ty-color-primary')).toBe('blue'); + expect((providers[1] as HTMLElement).style.getPropertyValue('--ty-border-radius')).toBe('12px'); + + nested.rerender( + +
Content
+
+ ); + + const provider = nested.container.querySelector('.ty-config-provider') as HTMLElement; + expect(provider.style.getPropertyValue('--ty-color-primary')).toBe('blue'); + expect(provider.style.getPropertyValue('--ty-border-radius')).toBe(''); + }); + + it('should expose merged config via useConfig', () => { + const Consumer = () => { + const config = useConfig(); + return ( +
+ ); + }; + + render( + + + + + + ); + + const node = screen.getByTestId('config'); + expect(node.getAttribute('data-prefix')).toBe('outer'); + expect(node.getAttribute('data-size')).toBe('sm'); + expect(node.getAttribute('data-theme')).toBe('dark'); + }); + + it('should apply holderRender to static message APIs', () => { + ConfigProvider.config({ + holderRender: (children) =>
{children}
, + }); + + act(() => { + Message.info('Hello', 1000, undefined as unknown as () => void, {}); + }); + + expect(document.querySelector('[data-testid="message-holder"]')).toBeTruthy(); + expect(document.body.textContent).toContain('Hello'); + }); + + it('should apply holderRender to static notification APIs', () => { + ConfigProvider.config({ + holderRender: (children) =>
{children}
, + }); + + act(() => { + Notification.open({ title: 'Notice', description: 'Body', duration: 0 }); + }); + + expect(document.querySelector('[data-testid="notification-holder"]')).toBeTruthy(); + expect(document.body.textContent).toContain('Notice'); + }); + + it('should apply holderRender to static loading bar APIs', () => { + ConfigProvider.config({ + holderRender: (children) =>
{children}
, + }); + + act(() => { + LoadingBar.start(); + }); + + expect(document.querySelector('[data-testid="loading-bar-holder"]')).toBeTruthy(); + expect(document.querySelector('#ty-loading-bar')).toBeTruthy(); + + act(() => { + LoadingBar.succeed(); + }); + }); + + it('should apply holderRender to static modal APIs', () => { + ConfigProvider.config({ + holderRender: (children) =>
{children}
, + }); + + let instance!: ReturnType; + + act(() => { + instance = Modal.open({ header: 'Static Modal', children: 'Body' }); + }); + + expect(document.querySelector('[data-testid="modal-holder"]')).toBeTruthy(); + expect(document.body.textContent).toContain('Static Modal'); + + act(() => { + instance.destroy(); + }); + }); }); diff --git a/packages/react/src/config-provider/config-context.tsx b/packages/react/src/config-provider/config-context.tsx index 16e10590..3a608db1 100644 --- a/packages/react/src/config-provider/config-context.tsx +++ b/packages/react/src/config-provider/config-context.tsx @@ -11,8 +11,11 @@ export interface ConfigContextProps { componentSize?: SizeType; shimmer?: boolean; space?: SpaceSize; - theme?: ThemeMode | ThemeConfig; + theme?: ThemeMode; + themeConfig?: ThemeConfig; locale?: Locale; + getPopupContainer?: (trigger?: HTMLElement | null) => HTMLElement; + getTargetContainer?: () => HTMLElement | Window; } export const ConfigContext = React.createContext({ @@ -20,4 +23,10 @@ export const ConfigContext = React.createContext({ componentSize: 'md', shimmer: false, space: 'sm', + getPopupContainer: () => document.body, + getTargetContainer: () => window, }); + +export function useConfig(): ConfigContextProps { + return React.useContext(ConfigContext); +} diff --git a/packages/react/src/config-provider/config-provider.tsx b/packages/react/src/config-provider/config-provider.tsx index f94a5b29..cd31142c 100644 --- a/packages/react/src/config-provider/config-provider.tsx +++ b/packages/react/src/config-provider/config-provider.tsx @@ -1,17 +1,28 @@ -import { useEffect, useMemo, useRef } from 'react'; -import { ConfigContext, ThemeMode } from './config-context'; -import { ConfigProviderProps } from './types'; +import React, { Fragment, useCallback, useContext, useMemo, useState } from 'react'; +import { ConfigContext, ThemeMode, useConfig } from './config-context'; +import { ConfigProviderComponent, ConfigProviderProps } from './types'; import { buildCssVars, ThemeConfig } from './token-utils'; -import { acquireMode, releaseMode, acquireProps, releaseProps } from './global-style-registry'; +import { setStaticConfig } from './static-config'; import IntlProvider from '../intl-provider'; function isThemeConfig(theme: unknown): theme is ThemeConfig { return typeof theme === 'object' && theme !== null; } -const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { - const { children, theme, locale, ...otherProps } = props; - const idRef = useRef(Symbol('ConfigProvider')); +const ConfigProviderImpl = (props: ConfigProviderProps): React.ReactElement => { + const { + children, + theme, + locale, + prefixCls, + componentSize, + shimmer, + space, + getPopupContainer, + getTargetContainer, + } = props; + const parentConfig = useContext(ConfigContext); + const [holderElement, setHolderElement] = useState(null); const themeConfig = isThemeConfig(theme) ? theme : undefined; const mode = themeConfig ? themeConfig.mode : (theme as ThemeMode | undefined); @@ -19,40 +30,83 @@ const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { () => (themeConfig ? buildCssVars(themeConfig) : undefined), [themeConfig] ); + const requiresScope = Boolean(mode || cssVars); - useEffect(() => { - if (!mode) return; - const id = idRef.current; - acquireMode(id, mode); - return () => { - releaseMode(id); - }; - }, [mode]); - - useEffect(() => { - if (!cssVars) return; - const id = idRef.current; - const vars = cssVars as Record; - acquireProps(id, vars); - const keys = Object.keys(vars); - return () => { - releaseProps(id, keys); - }; - }, [cssVars]); - - const content = locale ? ( - {children} + const mergedGetPopupContainer = useCallback( + (trigger?: HTMLElement | null) => + getPopupContainer?.(trigger) ?? + (requiresScope ? holderElement : null) ?? + parentConfig.getPopupContainer?.(trigger) ?? + document.body, + [getPopupContainer, holderElement, parentConfig, requiresScope] + ); + + const popupHolderRef = useCallback((node: HTMLDivElement | null) => { + setHolderElement(node); + }, []); + + const mergedConfig = useMemo( + () => ({ + prefixCls: prefixCls ?? parentConfig.prefixCls, + componentSize: componentSize ?? parentConfig.componentSize, + shimmer: shimmer ?? parentConfig.shimmer, + space: space ?? parentConfig.space, + theme: mode ?? parentConfig.theme, + themeConfig: themeConfig ?? parentConfig.themeConfig, + locale: locale ?? parentConfig.locale, + getPopupContainer: mergedGetPopupContainer, + getTargetContainer: getTargetContainer ?? parentConfig.getTargetContainer, + }), + [ + componentSize, + getTargetContainer, + locale, + mergedGetPopupContainer, + mode, + parentConfig, + prefixCls, + shimmer, + space, + themeConfig, + ] + ); + + const content = mergedConfig.locale ? ( + {children} ) : ( children ); + const popupHolder = requiresScope ? ( +
+ ) : null; + return ( - - {content} + + {requiresScope ? ( +
+ {content} + {popupHolder} +
+ ) : ( + {content} + )}
); }; -ConfigProvider.displayName = 'ConfigProvider'; +ConfigProviderImpl.displayName = 'ConfigProvider'; + +const ConfigProvider = ConfigProviderImpl as ConfigProviderComponent; + +ConfigProvider.config = setStaticConfig; +ConfigProvider.useConfig = useConfig; export default ConfigProvider; diff --git a/packages/react/src/config-provider/container-utils.ts b/packages/react/src/config-provider/container-utils.ts new file mode 100644 index 00000000..260fd3b1 --- /dev/null +++ b/packages/react/src/config-provider/container-utils.ts @@ -0,0 +1,16 @@ +import { ConfigContextProps } from './config-context'; + +export function resolveTargetContainer( + config: ConfigContextProps, + targetContainer?: () => HTMLElement | Window +): HTMLElement | Window { + return targetContainer?.() ?? config.getTargetContainer?.() ?? window; +} + +export function resolvePopupContainer( + config: ConfigContextProps, + trigger?: HTMLElement | null, + popupContainer?: (trigger?: HTMLElement | null) => HTMLElement +): HTMLElement { + return popupContainer?.(trigger) ?? config.getPopupContainer?.(trigger) ?? document.body; +} diff --git a/packages/react/src/config-provider/demo/DynamicTheme.tsx b/packages/react/src/config-provider/demo/DynamicTheme.tsx new file mode 100644 index 00000000..d73eb2d6 --- /dev/null +++ b/packages/react/src/config-provider/demo/DynamicTheme.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import { Button, ConfigProvider, Space } from '@tiny-design/react'; + +export default function DynamicThemeDemo() { + const [danger, setDanger] = useState(false); + + return ( + + + + + + + ); +} diff --git a/packages/react/src/config-provider/demo/NestedTheme.tsx b/packages/react/src/config-provider/demo/NestedTheme.tsx new file mode 100644 index 00000000..8959a16f --- /dev/null +++ b/packages/react/src/config-provider/demo/NestedTheme.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Button, Card, ConfigProvider, Space } from '@tiny-design/react'; + +export default function NestedThemeDemo() { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/react/src/config-provider/demo/PortalTheme.tsx b/packages/react/src/config-provider/demo/PortalTheme.tsx new file mode 100644 index 00000000..f64163c4 --- /dev/null +++ b/packages/react/src/config-provider/demo/PortalTheme.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Button, ConfigProvider, Select, Space, Tooltip } from '@tiny-design/react'; + +const options = [ + { label: 'Ocean', value: 'ocean' }, + { label: 'Forest', value: 'forest' }, + { label: 'Sunset', value: 'sunset' }, +]; + +export default function PortalThemeDemo() { + return ( + + + + + + + +``` + +Even though the Select dropdown and Tooltip popup are rendered through portals, they still inherit the same token values from the current provider scope. + +#### 3. Updating theme config + +When the `theme` prop changes, old overrides are removed and replaced by the new ones. + +```jsx +const [danger, setDanger] = useState(false); + + + + +``` + +In this example, when `danger` switches back to `false`, `borderRadius: '2px'` is removed instead of leaking into the next theme state. ### CSS Custom Property Overrides @@ -80,6 +188,31 @@ You can also customize components directly via CSS without using `ThemeConfig`: .my-section { --ty-btn-border-radius: 0; } ``` +## Static Functions + +Static feedback APIs such as `Message.*`, `Notification.*`, `LoadingBar.*`, and `Modal.open()` do not automatically read the nearest React tree provider. Use `ConfigProvider.config()` to provide a holder wrapper for them. + +```jsx +ConfigProvider.config({ + holderRender: (children) => ( + + {children} + + ), +}); +``` + +## useConfig + +Read the merged parent config inside custom components or hooks. + +```jsx +const { componentSize, getPopupContainer, locale } = ConfigProvider.useConfig(); +``` + ## Props | Property | Description | Type | Default | @@ -89,6 +222,8 @@ You can also customize components directly via CSS without using `ThemeConfig`: | shimmer | display shimmer effect for [Skeleton](#/components/skeleton). | boolean | false | | space | set Space size, ref [Space](#/components/space). | enum: `sm` | `md` | `lg` or `number`. | `sm` | | locale | set locale for components (e.g. `en_US`, `zh_CN`). | Locale | - | +| getPopupContainer | set the container for popup-based components within this provider scope. | `(trigger?: HTMLElement \| null) => HTMLElement` | provider popup holder | +| getTargetContainer | set the default scroll target for components such as `Anchor`, `Sticky`, and `BackTop`, and the scroll-lock target for layers such as `Overlay` and `Tour`. | `() => HTMLElement \| Window` | `() => window` | | theme | set theme mode or theme config with token overrides. | `ThemeMode` | `ThemeConfig` | - | > The `prefixCls` property is useful to solve the classname conflict with other libraries. Please note that this will lose default styles from `ty`. To solve that, also updating the `prefix` variable in the `_variables.scss`. diff --git a/packages/react/src/config-provider/index.tsx b/packages/react/src/config-provider/index.tsx index 77147208..f8bed3c7 100644 --- a/packages/react/src/config-provider/index.tsx +++ b/packages/react/src/config-provider/index.tsx @@ -1,4 +1,7 @@ import ConfigProvider from './config-provider'; +import { useConfig } from './config-context'; export type { ThemeConfig } from './token-utils'; +export type { StaticConfig } from './static-config'; export default ConfigProvider; +export { useConfig }; diff --git a/packages/react/src/config-provider/index.zh_CN.md b/packages/react/src/config-provider/index.zh_CN.md index e1615ba8..a24ef235 100644 --- a/packages/react/src/config-provider/index.zh_CN.md +++ b/packages/react/src/config-provider/index.zh_CN.md @@ -1,3 +1,10 @@ +import NestedThemeDemo from './demo/NestedTheme'; +import NestedThemeSource from './demo/NestedTheme.tsx?raw'; +import PortalThemeDemo from './demo/PortalTheme'; +import PortalThemeSource from './demo/PortalTheme.tsx?raw'; +import DynamicThemeDemo from './demo/DynamicTheme'; +import DynamicThemeSource from './demo/DynamicTheme.tsx?raw'; + # ConfigProvider 为组件提供统一的全局配置,支持通过设计令牌进行主题定制。 @@ -16,6 +23,42 @@ return ( ); ``` +## 示例 + + + + + +### 嵌套 Provider + +内层 provider 会覆盖外层 provider,这也是嵌套挂载和卸载时最需要保证正确的行为。 + + + + + + +### Portal 继承 + +令牌会作用在当前 provider 的作用域节点上,弹层类组件默认也会渲染到这个 provider 对应的 popup holder 中。 + + + + + + + + +### 动态更新 Theme + +当 `theme` 属性更新时,旧 token 会先被清理,再应用新的值。 + + + + + + + ## 主题定制 `theme` 属性既可以接收主题模式字符串,也可以接收 `ThemeConfig` 对象来进行细粒度的令牌定制。 @@ -61,9 +104,74 @@ import { ConfigProvider } from 'tiny-design'; | token | 全局设计令牌覆盖(camelCase 键名) | `Record` | - | | components | 组件级令牌覆盖(camelCase 键名) | `Record>` | - | -令牌键名使用 camelCase 格式,会自动转换为 CSS 自定义属性。例如 `colorPrimary` 会转换为 `--ty-color-primary`,`Button.borderRadius` 会转换为 `--ty-btn-border-radius`。 +令牌键名使用 camelCase 格式,会自动转换为 CSS 自定义属性。例如 `colorPrimary` 会转换为 `--ty-color-primary`,`Button.borderRadius` 会转换为 `--ty-btn-border-radius`。ConfigProvider 会把这些值应用到自己的作用域节点上,而不是直接写到全局 ``。因此嵌套 provider 会形成真正的嵌套作用域,弹层类组件也会通过 provider 的 popup holder 继承相同的值。 -令牌覆盖以内联样式的方式应用到 `` 元素上,因此所有组件(包括通过 Portal 渲染的弹窗、提示框、下拉菜单等)都会继承这些覆盖值。当 ConfigProvider 卸载或 `theme` 属性变更时,覆盖值会自动清理。 +当启用局部主题时,`ConfigProvider` 会渲染一个带 `display: contents` 的内部作用域节点,用来承载局部 CSS 变量和 popup holder。它对布局影响很小,但这个节点仍然存在于 DOM 中,依赖直接父子关系的 DOM 逻辑需要注意这一点。 + +### 示例 + +#### 1. 嵌套 ConfigProvider + +内层 `ConfigProvider` 会覆盖外层配置;当内层卸载时,外层值会自动恢复。 + +```jsx + + + + + + + +``` + +这个例子里: + +- `DangerSection` 外部的 `--ty-color-primary` 是 `#1677ff` +- `DangerSection` 内部的 `--ty-color-primary` 会变成 `#f5222d` +- 当 `DangerSection` 卸载后,值会恢复成 `#1677ff` + +#### 2. Portal 组件也会继承 + +因为弹层类组件默认会渲染到当前 provider 的 holder 中,所以通过 Portal 渲染出来的内容也会继承这些值。 + +```jsx + +