Skip to content

Commit 49b4bfc

Browse files
authored
refactor(tokens): migrate SCSS variables to CSS custom properties (#90)
* 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. * chore: add changeset for SCSS to CSS custom properties migration * 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) * chore: update changeset for component token customization * fix(config-provider): address PR review feedback - Apply token overrides to <html> instead of wrapper div, so portals (modals, tooltips, dropdowns) inherit overrides correctly - Remove wrapper <div> 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 * chore: lint * 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 <html>. DOM cleanup only fires when the last provider holding a given property unmounts. * 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. * refactor: configProvider * fix: export types * test: fix tests
1 parent 55ad7a7 commit 49b4bfc

File tree

182 files changed

+2771
-2274
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+2771
-2274
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@tiny-design/react": minor
3+
---
4+
5+
Upgrade the global configuration infrastructure around `ConfigProvider` and align popup, scroll, and static layer behavior across the component library.
6+
7+
Highlights:
8+
9+
- Reworked `ConfigProvider` to use provider-scoped theme containers instead of mutating global HTML styles.
10+
- Added `ConfigProvider.useConfig()` and `ConfigProvider.config({ holderRender })` support for a wider set of static APIs.
11+
- Added static `Modal.open()` and `Modal.confirm()` APIs that participate in the shared static host pipeline.
12+
- Unified popup container resolution across `Portal`, `Popup`, and `Cascader`.
13+
- Unified target container resolution across `Anchor`, `Sticky`, `BackTop`, `Overlay`, and `Tour`.
14+
- Improved `Sticky` container observation with `ResizeObserver`.
15+
- Improved `useTheme()` to sync with DOM state, localStorage, system preference changes, and cross-tab storage events.
16+
- Added `onCopy` to `CopyToClipboard` so copy results can be observed by consumers.
17+
18+
Notes for consumers:
19+
20+
- `Anchor` and `BackTop` now accept and resolve `Window` as a first-class target container shape.
21+
- `BackTop` now defaults to `ConfigProvider.getTargetContainer()` when present.
22+
- `ConfigProvider` only renders an internal scope node when scoped theme behavior is required.
23+
- Static APIs such as `Message.*`, `Notification.*`, `LoadingBar.*`, and `Modal.open()` can now be wrapped consistently through `ConfigProvider.config({ holderRender })`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@tiny-design/react": minor
3+
"@tiny-design/tokens": minor
4+
---
5+
6+
Migrate component styles from SCSS variables to CSS custom properties (`--ty-*`) for better runtime theming support.
7+
8+
- Migrate ~80 structural SCSS constants (padding, sizing, transitions) to runtime-customizable CSS custom properties
9+
- Tokenize hardcoded values in Button, Input, Card, Select, and Notification components
10+
- Add component-scoped CSS variable fallback chains (e.g., `--ty-btn-border-radius` falls back to `--ty-border-radius`)
11+
- Add `ThemeConfig` API to `ConfigProvider` for programmatic token and component-level overrides
12+
- Three-level customization: global tokens, component tokens, and scoped instance overrides via CSS

CONTRIBUTING.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ your-component/
6969
3. Export the component from `packages/react/src/index.ts`
7070
4. Add the route in `apps/docs/src/routers.tsx`
7171

72+
### Public Type Exports
73+
74+
Every component module must export its public TypeScript types from its own `index.tsx`.
75+
76+
- If the component has a `types.ts` file, re-export its public types from `packages/react/src/your-component/index.tsx`
77+
- If the component exposes public types from related files, re-export them from the same module barrel as well
78+
- Keep component-level type exports consistent with the runtime component export instead of creating one-off type exceptions elsewhere
79+
80+
The package root barrel must also re-export those public component types from `packages/react/src/index.ts`.
81+
82+
- Do not add root-level type exports for only one or two special-case components
83+
- Do not rely on consumers importing public component types from internal file paths when the type is part of the supported API surface
84+
7285
## Code Style
7386

7487
- TypeScript strict mode is enabled

apps/docs/guides/customise-theme.md

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Tiny UI provides three ways to customise the look and feel:
44

55
1. **Theme Editor** — a visual, no-code tool for real-time theming (great for exploration and quick customisation).
66
2. **Design tokens** — CSS custom properties that power light and dark mode. These are the runtime values every component reads.
7-
3. **SCSS variables** — compile-time variables (sizes, font stacks, border radii, etc.) that can be overridden when you build your own stylesheet.
7+
3. **SCSS constants** — compile-time structural constants (padding, transitions, arrow sizes, etc.) that can be overridden when you build your own stylesheet.
88

99
## Theme Editor
1010

@@ -14,7 +14,7 @@ The built-in [Theme Editor](/theme/theme-editor) lets you visually customise des
1414
- Adjust primary, success, warning, danger, and info colours, background, text, and border colours.
1515
- Tweak typography (font size, line height, font weight) and details (border radius, spacing, sizing).
1616
- Preview changes live on real components.
17-
- Export your customised tokens as CSS or SCSS to use in your project.
17+
- Export your customised tokens as CSS or JSON to use in your project.
1818

1919
Changes are applied instantly via CSS custom properties — no rebuild required.
2020

@@ -55,7 +55,7 @@ The hook returns:
5555

5656
## Design tokens (CSS custom properties)
5757

58-
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:
58+
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:
5959

6060
```css
6161
:root {
@@ -65,6 +65,16 @@ Every colour, shadow, and visual state is exposed as a `--ty-*` CSS custom prope
6565
}
6666
```
6767

68+
For dark mode overrides, target the dark theme selector:
69+
70+
```css
71+
html[data-tiny-theme='dark'] {
72+
--ty-color-primary: #3d9bff;
73+
--ty-color-primary-hover: #66b3ff;
74+
--ty-color-primary-active: #007bff;
75+
}
76+
```
77+
6878
### Commonly used tokens
6979

7080
| Token | Light default | Description |
@@ -76,16 +86,21 @@ Every colour, shadow, and visual state is exposed as a `--ty-*` CSS custom prope
7686
| `--ty-color-text` | `rgba(0,0,0,0.85)` | Primary text colour |
7787
| `--ty-color-text-secondary` | `rgba(0,0,0,0.65)` | Secondary text colour |
7888
| `--ty-color-border` | `#d9d9d9` | Default border colour |
89+
| `--ty-border-radius` | `2px` | Global border radius |
90+
| `--ty-font-size-base` | `1rem` | Base font size |
91+
| `--ty-height-sm` | `24px` | Small control height |
92+
| `--ty-height-md` | `32px` | Medium control height |
93+
| `--ty-height-lg` | `42px` | Large control height |
7994

80-
The full list of tokens can be found in the source:
95+
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:
8196
- [Light theme tokens](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_light.scss)
8297
- [Dark theme tokens](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_dark.scss)
8398

84-
## SCSS variables
99+
## SCSS constants
85100

86-
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.
101+
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.
87102

88-
> **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.
103+
Every constant uses the `!default` flag, so your overrides take precedence.
89104

90105
### 1. Install Sass
91106

@@ -95,13 +110,13 @@ $ npm install sass --save-dev
95110

96111
### 2. Create your overrides file
97112

98-
Create a file, e.g. `theme-variables.scss`. Your overrides **must come before** the Tiny UI import:
113+
Create a file, e.g. `theme-overrides.scss`. Your overrides **must come before** the Tiny UI import:
99114

100115
```scss
101-
// Your overrides
102-
$primary-color: #007bff;
103-
$border-radius: 4px;
104-
$font-size-base: 14px;
116+
// Override structural constants
117+
$btn-padding-md: 0 20px;
118+
$card-body-padding: 20px;
119+
$tooltip-arrow-size: 6px;
105120

106121
// Import Tiny UI styles (applies your overrides via !default)
107122
@use "@tiny-design/react/es/style/index" as *;
@@ -110,32 +125,27 @@ $font-size-base: 14px;
110125
### 3. Import in your entry file
111126

112127
```js
113-
import './theme-variables.scss';
128+
import './theme-overrides.scss';
114129
```
115130

116-
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).
131+
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).
117132

118-
Some commonly overridden variables:
133+
Some commonly overridden constants:
119134

120135
```scss
121-
// Color
122-
$primary-color: #6e41bf !default;
123-
124-
// Font
125-
$font-size-base: 1rem !default;
126-
$font-size-lg: $font-size-base * 1.25 !default;
127-
$font-size-sm: $font-size-base * 0.875 !default;
128-
$font-weight: 400 !default;
129-
130-
// Border
131-
$border-radius: 2px !default;
132-
$border-width: 1px !default;
133-
$border-color: $gray-300 !default;
134-
135-
// Component sizes
136-
$height-sm: 24px !default;
137-
$height-md: 32px !default;
138-
$height-lg: 42px !default;
136+
// Button
137+
$btn-padding-sm: 0 10px !default;
138+
$btn-padding-md: 0 15px !default;
139+
$btn-padding-lg: 0 28px !default;
140+
141+
// Card
142+
$card-header-padding: 13px 16px !default;
143+
$card-body-padding: 16px !default;
144+
145+
// Notification
146+
$notification-width: 380px !default;
139147
```
140148

141-
Please report an issue if the existing list of variables is not enough for you.
149+
> **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.
150+
151+
Please report an issue if the existing list of tokens or constants is not enough for you.

apps/docs/guides/customise-theme.zh_CN.md

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Tiny UI 提供三种方式来定制外观:
44

55
1. **主题编辑器** — 一个可视化的实时主题工具,无需编写代码(非常适合探索和快速定制)。
66
2. **设计令牌(Design tokens)** — 驱动亮色/暗色模式的 CSS 自定义属性,所有组件在运行时读取这些值。
7-
3. **SCSS 变量**编译时变量(尺寸、字体、圆角等),可在构建自定义样式表时覆盖。
7+
3. **SCSS 常量**编译时结构常量(内边距、过渡动画、箭头尺寸等),可在构建自定义样式表时覆盖。
88

99
## 主题编辑器
1010

@@ -14,7 +14,7 @@ Tiny UI 提供三种方式来定制外观:
1414
- 调整主色、成功色、警告色、危险色和信息色,以及背景色、文本色和边框色。
1515
- 调整排版(字号、行高、字重)和细节(圆角、间距、尺寸)。
1616
- 在真实组件上实时预览更改效果。
17-
- 导出自定义的令牌为 CSS 或 SCSS,在你的项目中使用。
17+
- 导出自定义的令牌为 CSS 或 JSON,在你的项目中使用。
1818

1919
更改通过 CSS 自定义属性即时生效 — 无需重新构建。
2020

@@ -55,7 +55,7 @@ const App = () => {
5555

5656
## 设计令牌(CSS 自定义属性)
5757

58-
所有颜色、阴影和视觉状态都以 `--ty-*` CSS 自定义属性的形式暴露在 `:root` 上。你可以在自己的样式表中覆盖任意令牌:
58+
所有颜色、阴影和视觉状态都以 `--ty-*` CSS 自定义属性的形式暴露在 `:root` 上。这是定制 Tiny UI 的**主要方式**你可以在自己的样式表中覆盖任意令牌:
5959

6060
```css
6161
:root {
@@ -65,6 +65,16 @@ const App = () => {
6565
}
6666
```
6767

68+
暗色模式下的覆盖,使用暗色主题选择器:
69+
70+
```css
71+
html[data-tiny-theme='dark'] {
72+
--ty-color-primary: #3d9bff;
73+
--ty-color-primary-hover: #66b3ff;
74+
--ty-color-primary-active: #007bff;
75+
}
76+
```
77+
6878
### 常用令牌
6979

7080
| 令牌 | 亮色默认值 | 说明 |
@@ -76,16 +86,21 @@ const App = () => {
7686
| `--ty-color-text` | `rgba(0,0,0,0.85)` | 主文本色 |
7787
| `--ty-color-text-secondary` | `rgba(0,0,0,0.65)` | 次要文本色 |
7888
| `--ty-color-border` | `#d9d9d9` | 默认边框色 |
89+
| `--ty-border-radius` | `2px` | 全局圆角 |
90+
| `--ty-font-size-base` | `1rem` | 基础字号 |
91+
| `--ty-height-sm` | `24px` | 小尺寸控件高度 |
92+
| `--ty-height-md` | `32px` | 中尺寸控件高度 |
93+
| `--ty-height-lg` | `42px` | 大尺寸控件高度 |
7994

80-
完整的令牌列表请参考源码:
95+
每个组件也有自己的令牌,用于细粒度控制。例如,Button 使用 `--ty-btn-default-bg``--ty-btn-default-color` 等。完整的令牌列表请参考源码:
8196
- [亮色主题令牌](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_light.scss)
8297
- [暗色主题令牌](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/themes/_dark.scss)
8398

84-
## SCSS 变量
99+
## SCSS 常量
85100

86-
如果你引入的是 Tiny UI 的 SCSS 源文件而非预编译的 CSS,可以覆盖编译时变量,如尺寸、间距、字体和圆角等。每个变量都使用了 `!default` 标志,因此你的覆盖值会优先生效
101+
如果你引入的是 Tiny UI 的 SCSS 源文件而非预编译的 CSS,可以覆盖编译时结构常量,如内边距、过渡动画和箭头尺寸。这些是不需要在运行时变化的值
87102

88-
> **什么是 `!default`** 带有 `!default` 的 Sass 变量仅在尚未定义时才会赋值。在引入 Tiny UI 样式*之前*声明你的值,你的值就会生效
103+
每个常量都使用了 `!default` 标志,因此你的覆盖值会优先生效
89104

90105
### 1. 安装 Sass
91106

@@ -95,13 +110,13 @@ $ npm install sass --save-dev
95110

96111
### 2. 创建覆盖文件
97112

98-
创建一个文件,例如 `theme-variables.scss`。你的覆盖值**必须写在** Tiny UI 引入语句之前:
113+
创建一个文件,例如 `theme-overrides.scss`。你的覆盖值**必须写在** Tiny UI 引入语句之前:
99114

100115
```scss
101-
// 你的覆盖值
102-
$primary-color: #007bff;
103-
$border-radius: 4px;
104-
$font-size-base: 14px;
116+
// 覆盖结构常量
117+
$btn-padding-md: 0 20px;
118+
$card-body-padding: 20px;
119+
$tooltip-arrow-size: 6px;
105120

106121
// 引入 Tiny UI 样式(通过 !default 应用你的覆盖值)
107122
@use "@tiny-design/react/es/style/index" as *;
@@ -110,32 +125,27 @@ $font-size-base: 14px;
110125
### 3. 在入口文件中引入
111126

112127
```js
113-
import './theme-variables.scss';
128+
import './theme-overrides.scss';
114129
```
115130

116-
完整的 SCSS 变量列表请参考 [_variables.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_variables.scss)
131+
完整的 SCSS 常量列表请参考 [_constants.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_constants.scss)
117132

118-
以下是一些常用的可覆盖变量
133+
以下是一些常用的可覆盖常量
119134

120135
```scss
121-
// 颜色
122-
$primary-color: #6e41bf !default;
123-
124-
// 字体
125-
$font-size-base: 1rem !default;
126-
$font-size-lg: $font-size-base * 1.25 !default;
127-
$font-size-sm: $font-size-base * 0.875 !default;
128-
$font-weight: 400 !default;
129-
130-
// 边框
131-
$border-radius: 2px !default;
132-
$border-width: 1px !default;
133-
$border-color: $gray-300 !default;
134-
135-
// 组件尺寸
136-
$height-sm: 24px !default;
137-
$height-md: 32px !default;
138-
$height-lg: 42px !default;
136+
// 按钮
137+
$btn-padding-sm: 0 10px !default;
138+
$btn-padding-md: 0 15px !default;
139+
$btn-padding-lg: 0 28px !default;
140+
141+
// 卡片
142+
$card-header-padding: 13px 16px !default;
143+
$card-body-padding: 16px !default;
144+
145+
// 通知
146+
$notification-width: 380px !default;
139147
```
140148

141-
如果现有的变量列表无法满足你的需求,请提交 issue 反馈。
149+
> **注意:** 颜色、字号、圆角、阴影等所有视觉令牌应通过 CSS 自定义属性定制(见上方),而非 SCSS 变量。SCSS 常量仅用于内边距、尺寸等结构性值。
150+
151+
如果现有的令牌或常量列表无法满足你的需求,请提交 issue 反馈。

apps/docs/src/containers/theme-editor/components/export-dialog.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from 'react';
22
import { Button, Modal, Tabs } from '@tiny-design/react';
3-
import { generateCSS, generateSCSS, generateJSON } from '../utils/export-theme';
3+
import { generateCSS, generateJSON } from '../utils/export-theme';
44

55
interface ExportDialogProps {
66
visible: boolean;
@@ -28,7 +28,6 @@ export const ExportDialog = ({
2828
const [copied, setCopied] = useState(false);
2929

3030
const cssCode = generateCSS(appliedTokens);
31-
const scssCode = generateSCSS(seeds);
3231
const jsonCode = generateJSON(seeds);
3332

3433
const handleCopy = (code: string) => {
@@ -71,9 +70,6 @@ export const ExportDialog = ({
7170
<Tabs.Panel tab="CSS Variables" tabKey="css">
7271
{renderBlock(cssCode, 'tiny-theme.css', 'text/css')}
7372
</Tabs.Panel>
74-
<Tabs.Panel tab="SCSS Variables" tabKey="scss">
75-
{renderBlock(scssCode, 'tiny-theme.scss', 'text/x-scss')}
76-
</Tabs.Panel>
7773
<Tabs.Panel tab="JSON Tokens" tabKey="json">
7874
{renderBlock(jsonCode, 'tiny-theme.json', 'application/json')}
7975
</Tabs.Panel>

0 commit comments

Comments
 (0)