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/.changeset/migrate-scss-to-css-custom-properties.md b/.changeset/migrate-scss-to-css-custom-properties.md new file mode 100644 index 00000000..868fe9be --- /dev/null +++ b/.changeset/migrate-scss-to-css-custom-properties.md @@ -0,0 +1,12 @@ +--- +"@tiny-design/react": minor +"@tiny-design/tokens": minor +--- + +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 943d4a37..5ebb2de9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,6 +69,19 @@ your-component/ 3. Export the component from `packages/react/src/index.ts` 4. Add the route in `apps/docs/src/routers.tsx` +### Public Type Exports + +Every component module must export its public TypeScript types from its own `index.tsx`. + +- If the component has a `types.ts` file, re-export its public types from `packages/react/src/your-component/index.tsx` +- If the component exposes public types from related files, re-export them from the same module barrel as well +- Keep component-level type exports consistent with the runtime component export instead of creating one-off type exceptions elsewhere + +The package root barrel must also re-export those public component types from `packages/react/src/index.ts`. + +- Do not add root-level type exports for only one or two special-case components +- Do not rely on consumers importing public component types from internal file paths when the type is part of the supported API surface + ## Code Style - TypeScript strict mode is enabled 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/extract/src/extract-tokens.ts b/packages/extract/src/extract-tokens.ts index e2698c05..83adfcbb 100644 --- a/packages/extract/src/extract-tokens.ts +++ b/packages/extract/src/extract-tokens.ts @@ -1,17 +1,17 @@ import * as fs from 'node:fs'; +import * as path from 'node:path'; import type { TokenData, ExtractTokensOptions } from './types.js'; // Map variable name prefixes to categories const CATEGORY_RULES: Array<{ test: (name: string) => boolean; category: string }> = [ - { test: (n) => /^(box-)?shadow/.test(n), category: 'shadows' }, + { test: (n) => /^shadow/.test(n), category: 'shadows' }, { test: (n) => /^size-(xs|sm|md|lg|xl|xxl)$/.test(n), category: 'breakpoints' }, { test: (n) => /^(font|line-height|heading|h\d-)/.test(n), category: 'typography' }, { test: (n) => /^(spacer|height-)/.test(n), category: 'spacing' }, { test: (n) => - /color$/.test(n) || + /^color-/.test(n) || /^(white|black|gray|red|orange|yellow|green|teal|cyan|blue|indigo|purple|magenta)-/.test(n) || - /^(info|success|warning|danger|primary)-/.test(n) || /^(body-bg|body-color)/.test(n), category: 'colors', }, @@ -25,7 +25,6 @@ function categorize(name: string): string | null { } export function extractTokens(options: ExtractTokensOptions): TokenData { - const content = fs.readFileSync(options.variablesPath, 'utf-8'); const result: TokenData = { colors: {}, typography: {}, @@ -34,11 +33,13 @@ export function extractTokens(options: ExtractTokensOptions): TokenData { shadows: {}, }; - // Match SCSS variable declarations: $name: value !default; + const variablesContent = fs.readFileSync(options.variablesPath, 'utf-8'); + + // Parse SCSS variable declarations: $name: value !default; const varRegex = /^\$([a-z0-9-]+):\s*(.+?)\s*!default\s*;/gm; let match: RegExpExecArray | null; - while ((match = varRegex.exec(content)) !== null) { + while ((match = varRegex.exec(variablesContent)) !== null) { const name = match[1]; const value = match[2]; const category = categorize(name); @@ -51,5 +52,30 @@ export function extractTokens(options: ExtractTokensOptions): TokenData { } } + // Also parse theme map files in the themes/ directory next to _variables.scss + const themesDir = path.join(path.dirname(options.variablesPath), 'themes'); + const lightThemePath = path.join(themesDir, '_light.scss'); + + if (fs.existsSync(lightThemePath)) { + const themeContent = fs.readFileSync(lightThemePath, 'utf-8'); + + // Match map entries: key: value, + // Handles multi-value entries like shadows by matching up to the trailing comma + const mapEntryRegex = /^\s+([a-z0-9-]+):\s*(.+?),?\s*$/gm; + + while ((match = mapEntryRegex.exec(themeContent)) !== null) { + const name = match[1]; + const value = match[2].replace(/,\s*$/, ''); + const category = categorize(name); + + if (category && !result[category][name]) { + result[category][name] = { + variable: `--ty-${name}`, + value, + }; + } + } + } + return result; } diff --git a/packages/mcp/__tests__/extract-tokens.test.ts b/packages/mcp/__tests__/extract-tokens.test.ts index 1c739870..df698cd4 100644 --- a/packages/mcp/__tests__/extract-tokens.test.ts +++ b/packages/mcp/__tests__/extract-tokens.test.ts @@ -18,13 +18,13 @@ describe('extractTokens', () => { it('extracts color tokens', () => { const result = extractTokens({ variablesPath: VARIABLES_PATH }); - expect(result.colors['primary-color']).toEqual({ - variable: '$primary-color', + expect(result.colors['color-primary']).toEqual({ + variable: '--ty-color-primary', value: '#6e41bf', }); - expect(result.colors['info-color']).toEqual({ - variable: '$info-color', + expect(result.colors['color-info']).toEqual({ + variable: '--ty-color-info', value: '#1890ff', }); }); @@ -33,7 +33,7 @@ describe('extractTokens', () => { const result = extractTokens({ variablesPath: VARIABLES_PATH }); expect(result.typography['font-size-base']).toEqual({ - variable: '$font-size-base', + variable: '--ty-font-size-base', value: '1rem', }); }); @@ -51,6 +51,6 @@ describe('extractTokens', () => { const result = extractTokens({ variablesPath: VARIABLES_PATH }); expect(result.shadows).toBeDefined(); - expect(result.shadows['box-shadow-sm']).toBeDefined(); + expect(result.shadows['shadow-sm']).toBeDefined(); }); }); 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} + setMode('dark')}> + Set dark + + + Toggle + + + ); +}; + +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/alert/index.tsx b/packages/react/src/alert/index.tsx index 15db282c..81e1f061 100755 --- a/packages/react/src/alert/index.tsx +++ b/packages/react/src/alert/index.tsx @@ -1,3 +1,4 @@ import Alert from './alert'; export default Alert; +export type * from './types'; 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/index.tsx b/packages/react/src/anchor/index.tsx index 4b9527a8..dff9c002 100755 --- a/packages/react/src/anchor/index.tsx +++ b/packages/react/src/anchor/index.tsx @@ -9,3 +9,4 @@ const DefaultAnchor = Anchor as IAnchor; DefaultAnchor.Link = AnchorLink; export default DefaultAnchor; +export type * from './types'; 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/aspect-ratio/index.tsx b/packages/react/src/aspect-ratio/index.tsx index 152797a0..dd9e3928 100755 --- a/packages/react/src/aspect-ratio/index.tsx +++ b/packages/react/src/aspect-ratio/index.tsx @@ -1,3 +1,4 @@ import AspectRatio from './aspect-ratio'; export default AspectRatio; +export type * from './types'; diff --git a/packages/react/src/auto-complete/index.tsx b/packages/react/src/auto-complete/index.tsx index 364f5c7a..30407043 100755 --- a/packages/react/src/auto-complete/index.tsx +++ b/packages/react/src/auto-complete/index.tsx @@ -1,4 +1,4 @@ import AutoComplete from './auto-complete'; export default AutoComplete; -export type { AutoCompleteProps, AutoCompleteOption } from './types'; +export type * from './types'; diff --git a/packages/react/src/avatar/index.tsx b/packages/react/src/avatar/index.tsx index eefdefa8..9abad82c 100755 --- a/packages/react/src/avatar/index.tsx +++ b/packages/react/src/avatar/index.tsx @@ -9,3 +9,4 @@ const DefaultAvatar = Avatar as IAvatar; DefaultAvatar.Group = AvatarGroup; export default DefaultAvatar; +export type * from './types'; 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/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.tsx b/packages/react/src/back-top/index.tsx index 3d0dcfbe..bdd70377 100755 --- a/packages/react/src/back-top/index.tsx +++ b/packages/react/src/back-top/index.tsx @@ -1,3 +1,4 @@ import BackTop from './back-top'; export default BackTop; +export type * from './types'; 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/badge/index.tsx b/packages/react/src/badge/index.tsx index c3a5ecd5..826d54f0 100755 --- a/packages/react/src/badge/index.tsx +++ b/packages/react/src/badge/index.tsx @@ -1,3 +1,4 @@ import Badge from './badge'; export default Badge; +export type * from './types'; diff --git a/packages/react/src/breadcrumb/index.tsx b/packages/react/src/breadcrumb/index.tsx index 77670551..7979647e 100755 --- a/packages/react/src/breadcrumb/index.tsx +++ b/packages/react/src/breadcrumb/index.tsx @@ -9,3 +9,4 @@ const DefaultBreadcrumb = Breadcrumb as IBreadcrumb; DefaultBreadcrumb.Item = BreadcrumbItem; export default DefaultBreadcrumb; +export type * from './types'; 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/index.tsx b/packages/react/src/button/index.tsx index 2876c127..a136aaa6 100755 --- a/packages/react/src/button/index.tsx +++ b/packages/react/src/button/index.tsx @@ -9,3 +9,4 @@ const DefaultButton = Button as IButton; DefaultButton.Group = ButtonGroup; export default DefaultButton; +export type * from './types'; diff --git a/packages/react/src/button/style/_index.scss b/packages/react/src/button/style/_index.scss index 90ed5dfc..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: $btn-border-radius; + border-radius: var(--ty-btn-border-radius, var(--ty-border-radius)); transition: $btn-transition; - line-height: $btn-line-height; + 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: $btn-line-height; + 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, $btn-font-size-sm, $btn-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, $btn-font-size-md, $btn-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, $btn-font-size-lg, $btn-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 { @@ -137,7 +137,7 @@ $btn-prefix: #{$prefix}-btn; } &_round { - border-radius: $btn-height-lg; + border-radius: var(--ty-height-lg); } &_loading { @@ -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: $btn-border-radius; - border-bottom-left-radius: $btn-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: $btn-border-radius; - border-bottom-right-radius: $btn-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/calendar/index.tsx b/packages/react/src/calendar/index.tsx index 278ac9d2..9e3618ac 100755 --- a/packages/react/src/calendar/index.tsx +++ b/packages/react/src/calendar/index.tsx @@ -1,3 +1,4 @@ import Calendar from './calendar'; export default Calendar; +export type * from './types'; 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/index.tsx b/packages/react/src/card/index.tsx index 8a9a485c..16060e29 100755 --- a/packages/react/src/card/index.tsx +++ b/packages/react/src/card/index.tsx @@ -1,8 +1,6 @@ import Card from './card'; import CardContent from './card-content'; -export type { CardVariant, CardProps } from './types'; - type ICard = typeof Card & { Content: typeof CardContent; }; @@ -11,3 +9,4 @@ const DefaultCard = Card as ICard; DefaultCard.Content = CardContent; export default DefaultCard; +export type * from './types'; diff --git a/packages/react/src/card/style/_index.scss b/packages/react/src/card/style/_index.scss index 1a307a3b..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: $card-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: $card-border-radius $card-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: $card-border-radius $card-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/carousel/index.tsx b/packages/react/src/carousel/index.tsx index 31b41c7c..c5907ab6 100755 --- a/packages/react/src/carousel/index.tsx +++ b/packages/react/src/carousel/index.tsx @@ -1,7 +1,7 @@ import Carousel from './carousel'; import CarouselItem from './carousel-item'; -export type { CarouselProps, CarouselRef, DotPlacement, CarouselEffect } from './types'; +export type * from './types'; export type { CarouselItemProps } from './carousel-item'; type ICarousel = typeof Carousel & { 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/cascader/index.tsx b/packages/react/src/cascader/index.tsx index 72c46dd6..67fba37a 100644 --- a/packages/react/src/cascader/index.tsx +++ b/packages/react/src/cascader/index.tsx @@ -1,3 +1,4 @@ import Cascader from './cascader'; export default Cascader; +export type * from './types'; 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/checkbox/index.tsx b/packages/react/src/checkbox/index.tsx index 19ea4ccd..e688b1ac 100755 --- a/packages/react/src/checkbox/index.tsx +++ b/packages/react/src/checkbox/index.tsx @@ -9,3 +9,4 @@ const DefaultCheckbox = Checkbox as ICheckbox; DefaultCheckbox.Group = CheckboxGroup; export default DefaultCheckbox; +export type * from './types'; diff --git a/packages/react/src/col/index.tsx b/packages/react/src/col/index.tsx index 0c0af4de..f2f3904a 100755 --- a/packages/react/src/col/index.tsx +++ b/packages/react/src/col/index.tsx @@ -1,3 +1,4 @@ import Col from '../grid/col'; export default Col; +export type { ColProps, ColSize } from '../grid/types'; 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/index.tsx b/packages/react/src/color-picker/index.tsx index 9d00b562..e6c556fa 100644 --- a/packages/react/src/color-picker/index.tsx +++ b/packages/react/src/color-picker/index.tsx @@ -1,3 +1,4 @@ import ColorPicker from './color-picker'; export default ColorPicker; +export type * from './types'; 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/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`] = ` - - - - Button - - - -`; 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( + + Button + + ); + 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( Button ); - 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 507223b4..3a608db1 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'; @@ -11,7 +12,10 @@ export interface ConfigContextProps { shimmer?: boolean; space?: SpaceSize; theme?: ThemeMode; + themeConfig?: ThemeConfig; locale?: Locale; + getPopupContainer?: (trigger?: HTMLElement | null) => HTMLElement; + getTargetContainer?: () => HTMLElement | Window; } export const ConfigContext = React.createContext({ @@ -19,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 aadac032..cd31142c 100644 --- a/packages/react/src/config-provider/config-provider.tsx +++ b/packages/react/src/config-provider/config-provider.tsx @@ -1,30 +1,112 @@ -import { useEffect } from 'react'; -import { ConfigContext } 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 { setStaticConfig } from './static-config'; import IntlProvider from '../intl-provider'; -const ConfigProvider = (props: ConfigProviderProps): JSX.Element => { - const { children, theme, locale, ...otherProps } = props; +function isThemeConfig(theme: unknown): theme is ThemeConfig { + return typeof theme === 'object' && theme !== null; +} - useEffect(() => { - if (!theme) return; - const html = document.documentElement; - html.setAttribute('data-tiny-theme', theme); - }, [theme]); +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 content = locale ? ( - {children} + const themeConfig = isThemeConfig(theme) ? theme : undefined; + const mode = themeConfig ? themeConfig.mode : (theme as ThemeMode | undefined); + const cssVars = useMemo( + () => (themeConfig ? buildCssVars(themeConfig) : undefined), + [themeConfig] + ); + const requiresScope = Boolean(mode || cssVars); + + 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 ( + + + setDanger((prev) => !prev)}> + Toggle Theme + + {danger ? 'Danger Mode' : 'Default Mode'} + + + ); +} 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 ( + + + + Outer Primary + Outer Default + + + + + + + Inner Primary + Inner Default + + + + + ); +} 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 ( + + + + + Hover For Tooltip + + + + ); +} diff --git a/packages/react/src/config-provider/index.md b/packages/react/src/config-provider/index.md index 50b884a7..7e555f24 100644 --- a/packages/react/src/config-provider/index.md +++ b/packages/react/src/config-provider/index.md @@ -1,6 +1,13 @@ +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 -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 +23,196 @@ return ( ); ``` +## Examples + + + + + +### Nested Providers + +The inner provider overrides the outer provider. This is the behavior that needs to be preserved when nested providers mount and unmount. + + + + + + +### Portal Inheritance + +Token overrides are scoped by the provider itself, and popup-based components render into that provider's popup holder by default. + + + + + + + + +### Dynamic Theme Updates + +When the `theme` prop changes, old token values are removed before the new ones are applied. + + + + + + + +## 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`. The provider applies them to its own scope node, not to the global `` element. Nested providers therefore behave like nested scopes, and popup-based components inherit the same values through the provider popup holder. + +When scoped theming is active, `ConfigProvider` renders an internal scope node with `display: contents` so local CSS variables and popup holders have a stable attachment point. This keeps layout impact low, but the node still exists in the DOM and may affect code that relies on direct parent-child DOM relationships. + +### Examples + +#### 1. Nested providers + +The inner `ConfigProvider` overrides the outer one. When the inner provider unmounts, the outer values are restored. + +```jsx + + + + + + + +``` + +In this example: + +- outside `DangerSection`, `--ty-color-primary` is `#1677ff` +- inside `DangerSection`, `--ty-color-primary` becomes `#f5222d` +- if `DangerSection` unmounts, the value goes back to `#1677ff` + +#### 2. Portal-rendered components + +Because popup-based components render into the current provider holder, they inherit the same scoped token values. + +```jsx + + + + Hover me + + +``` + +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); + + + setDanger((prev) => !prev)}>Toggle theme + +``` + +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 + +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; } +``` + +## 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 | @@ -25,6 +222,8 @@ 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` | - | +| 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 414e1236..e1f96c63 100644 --- a/packages/react/src/config-provider/index.tsx +++ b/packages/react/src/config-provider/index.tsx @@ -1,3 +1,8 @@ import ConfigProvider from './config-provider'; +import { useConfig } from './config-context'; +export type * from './types'; +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 cdb090da..a24ef235 100644 --- a/packages/react/src/config-provider/index.zh_CN.md +++ b/packages/react/src/config-provider/index.zh_CN.md @@ -1,6 +1,13 @@ +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,9 +23,195 @@ return ( ); ``` -# ConfigProvider 全局配置 +## 示例 + + + + + +### 嵌套 Provider + +内层 provider 会覆盖外层 provider,这也是嵌套挂载和卸载时最需要保证正确的行为。 + + + + + + +### Portal 继承 + +令牌会作用在当前 provider 的作用域节点上,弹层类组件默认也会渲染到这个 provider 对应的 popup holder 中。 + + + + + + + + +### 动态更新 Theme + +当 `theme` 属性更新时,旧 token 会先被清理,再应用新的值。 + + + + + + + +## 主题定制 + +`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`。ConfigProvider 会把这些值应用到自己的作用域节点上,而不是直接写到全局 ``。因此嵌套 provider 会形成真正的嵌套作用域,弹层类组件也会通过 provider 的 popup holder 继承相同的值。 + +当启用局部主题时,`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 + + + + Hover me + + +``` + +即使 Select 下拉框和 Tooltip 弹层是通过 Portal 渲染的,它们仍然会继承当前 provider 作用域下的 token 值。 + +#### 3. theme 更新时会回收旧值 + +当 `theme` 属性变化时,旧覆盖值会被清掉,再应用新的值。 + +```jsx +const [danger, setDanger] = useState(false); + + + setDanger((prev) => !prev)}>Toggle theme + +``` -为组件提供统一的全局配置。 +这个例子里,当 `danger` 切回 `false` 时,`borderRadius: '2px'` 会被移除,而不是残留到下一次主题状态中。 + +### 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; } +``` + +## 静态方法 + +像 `Message.*`、`Notification.*`、`LoadingBar.*`、`Modal.open()` 这类静态反馈 API 不会自动读取最近的 React 树 provider。可以通过 `ConfigProvider.config()` 提供一个统一的 holder 包裹器。 + +```jsx +ConfigProvider.config({ + holderRender: (children) => ( + + {children} + + ), +}); +``` + +## useConfig + +在自定义组件或 hooks 中读取合并后的上层配置。 + +```jsx +const { componentSize, getPopupContainer, locale } = ConfigProvider.useConfig(); +``` ## Props @@ -29,6 +222,8 @@ 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` | - | +| getPopupContainer | 为当前 provider 作用域内的弹层组件指定挂载容器 | `(trigger?: HTMLElement \| null) => HTMLElement` | provider popup holder | +| getTargetContainer | 为 `Anchor`、`Sticky`、`BackTop` 等组件设置默认滚动容器,同时为 `Overlay`、`Tour` 这类层级组件设置滚动锁目标 | `() => HTMLElement \| Window` | `() => window` | +| theme | 设置主题模式或包含令牌覆盖的主题配置 | `ThemeMode` | `ThemeConfig` | - | > `prefixCls` 属性适用于解决与其他库的类名冲突问题。请注意,这将丢失 `ty` 的默认样式。要解决此问题,还需要同时更新 `_variables.scss` 中的 `prefix` 变量。 diff --git a/packages/react/src/config-provider/scroll-lock.ts b/packages/react/src/config-provider/scroll-lock.ts new file mode 100644 index 00000000..d32efcca --- /dev/null +++ b/packages/react/src/config-provider/scroll-lock.ts @@ -0,0 +1,64 @@ +type ScrollLockEntry = { + count: number; + overflow: string; +}; + +const scrollLockMap = new WeakMap(); + +function resolveScrollLockTarget(container?: HTMLElement | Window | null): HTMLElement | null { + if (typeof document === 'undefined') { + return null; + } + + if (!container || container === window) { + return document.body; + } + + return container as HTMLElement; +} + +export function acquireScrollLock(container?: HTMLElement | Window | null): () => void { + const target = resolveScrollLockTarget(container); + + if (!target) { + return () => undefined; + } + + const current = scrollLockMap.get(target); + + if (current) { + current.count += 1; + } else { + scrollLockMap.set(target, { + count: 1, + overflow: target.style.overflow, + }); + target.style.overflow = 'hidden'; + } + + let released = false; + + return () => { + if (released) { + return; + } + + released = true; + + const entry = scrollLockMap.get(target); + + if (!entry) { + return; + } + + entry.count -= 1; + + if (entry.count <= 0) { + target.style.overflow = entry.overflow; + scrollLockMap.delete(target); + return; + } + + scrollLockMap.set(target, entry); + }; +} diff --git a/packages/react/src/config-provider/static-config.tsx b/packages/react/src/config-provider/static-config.tsx new file mode 100644 index 00000000..f55f0a96 --- /dev/null +++ b/packages/react/src/config-provider/static-config.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export interface StaticConfig { + holderRender?: (children: React.ReactNode) => React.ReactElement; +} + +let staticConfig: StaticConfig = {}; + +export function setStaticConfig(config: StaticConfig): void { + staticConfig = { ...staticConfig, ...config }; +} + +export function getStaticConfig(): StaticConfig { + return staticConfig; +} + +export function renderStaticNode(node: React.ReactElement): React.ReactElement { + return staticConfig.holderRender ? staticConfig.holderRender(node) : node; +} diff --git a/packages/react/src/config-provider/static-host.ts b/packages/react/src/config-provider/static-host.ts new file mode 100644 index 00000000..33a9d85d --- /dev/null +++ b/packages/react/src/config-provider/static-host.ts @@ -0,0 +1,39 @@ +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; +import { renderStaticNode } from './static-config'; + +const rootMap = new Map(); + +export function createStaticHost(className?: string): HTMLElement { + const container = document.createElement('div'); + + if (className) { + container.className = className; + } + + document.body.appendChild(container); + + return container; +} + +export function renderStaticHost(container: HTMLElement, node: React.ReactElement): Root { + const root = rootMap.get(container) ?? createRoot(container); + + root.render(renderStaticNode(node)); + rootMap.set(container, root); + + return root; +} + +export function destroyStaticHost(container: HTMLElement): void { + const root = rootMap.get(container); + + if (root) { + root.unmount(); + rootMap.delete(container); + } + + if (container.parentNode) { + container.parentNode.removeChild(container); + } +} 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/config-provider/types.ts b/packages/react/src/config-provider/types.ts index 7621786e..4155af9a 100644 --- a/packages/react/src/config-provider/types.ts +++ b/packages/react/src/config-provider/types.ts @@ -1,6 +1,15 @@ import React from 'react'; +import { StaticConfig } from './static-config'; import { ConfigContextProps } from './config-context'; -export interface ConfigProviderProps extends ConfigContextProps { +export interface ConfigProviderProps extends Omit { + // Props accept both ThemeMode and ThemeConfig so callers can provide token overrides. + // Context keeps `theme` normalized as ThemeMode and exposes ThemeConfig separately. + theme?: ConfigContextProps['theme'] | import('./token-utils').ThemeConfig; children: React.ReactNode; } + +export interface ConfigProviderComponent extends React.FC { + config: (config: StaticConfig) => void; + useConfig: typeof import('./config-context').useConfig; +} diff --git a/packages/react/src/copy-to-clipboard/__tests__/copy-to-clipboard.test.tsx b/packages/react/src/copy-to-clipboard/__tests__/copy-to-clipboard.test.tsx index 73e2a81a..94e6a8d0 100644 --- a/packages/react/src/copy-to-clipboard/__tests__/copy-to-clipboard.test.tsx +++ b/packages/react/src/copy-to-clipboard/__tests__/copy-to-clipboard.test.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { act, render, fireEvent } from '@testing-library/react'; import CopyToClipboard from '../index'; describe('', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should match the snapshot', () => { const { asFragment } = render( @@ -20,4 +24,26 @@ describe('', () => { ); expect(getByText('Copy')).toBeInTheDocument(); }); + + it('should notify copy result', async () => { + const onCopy = jest.fn(); + + Object.assign(navigator, { + clipboard: { + writeText: jest.fn().mockResolvedValue(undefined), + }, + }); + + const { getByText } = render( + + Copy + + ); + + await act(async () => { + fireEvent.click(getByText('Copy')); + }); + + expect(onCopy).toHaveBeenCalledWith(true, 'Hello'); + }); }); diff --git a/packages/react/src/copy-to-clipboard/clipboard.ts b/packages/react/src/copy-to-clipboard/clipboard.ts new file mode 100644 index 00000000..8561dcc4 --- /dev/null +++ b/packages/react/src/copy-to-clipboard/clipboard.ts @@ -0,0 +1,40 @@ +function fallbackCopy(value: string): boolean { + if (typeof document === 'undefined') { + return false; + } + + const textArea = document.createElement('textarea'); + textArea.value = value; + textArea.setAttribute('readonly', 'true'); + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.opacity = '0'; + textArea.style.pointerEvents = 'none'; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + textArea.setSelectionRange(0, textArea.value.length); + + try { + return document.execCommand('copy'); + } catch { + return false; + } finally { + textArea.remove(); + } +} + +export async function copyText(value: string): Promise { + if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(value); + return true; + } catch { + return fallbackCopy(value); + } + } + + return fallbackCopy(value); +} diff --git a/packages/react/src/copy-to-clipboard/copy-to-clipboard.tsx b/packages/react/src/copy-to-clipboard/copy-to-clipboard.tsx index 8e9cef74..9664ddf4 100755 --- a/packages/react/src/copy-to-clipboard/copy-to-clipboard.tsx +++ b/packages/react/src/copy-to-clipboard/copy-to-clipboard.tsx @@ -3,42 +3,26 @@ import classNames from 'classnames'; import { ConfigContext } from '../config-provider/config-context'; import { getPrefixCls } from '../_utils/general'; import { CopyToClipboardProps } from './types'; - -const copy = async (value: string) => { - if (navigator.clipboard) { - try { - await navigator.clipboard.writeText(value); - } catch { - fallbackCopy(value); - } - } else { - fallbackCopy(value); - } -}; - -const fallbackCopy = (value: string) => { - const textArea = document.createElement('textarea'); - textArea.style.position = 'fixed'; - textArea.style.opacity = '0'; - textArea.value = value; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand('copy'); - } catch { - // copy failed silently - } - document.body.removeChild(textArea); -}; +import { copyText } from './clipboard'; const CopyToClipboard = (props: CopyToClipboardProps): React.ReactElement => { - const { prefixCls: customisedCls, text, className, children, onClick, ...otherProps } = props; + const { + prefixCls: customisedCls, + text, + className, + children, + onClick, + onCopy, + ...otherProps + } = props; const configContext = useContext(ConfigContext); const prefixCls = getPrefixCls('copy', configContext.prefixCls, customisedCls); const cls = classNames(prefixCls, className); - const btnOnClick = (e: React.MouseEvent): void => { - copy(text); + const btnOnClick = async (e: React.MouseEvent): Promise => { + const copied = await copyText(text); + + onCopy?.(copied, text); onClick && onClick(e); }; diff --git a/packages/react/src/copy-to-clipboard/index.md b/packages/react/src/copy-to-clipboard/index.md index 32f20fd1..cd882f41 100644 --- a/packages/react/src/copy-to-clipboard/index.md +++ b/packages/react/src/copy-to-clipboard/index.md @@ -44,5 +44,6 @@ import { CopyToClipboard } from 'tiny-design'; | --------- | ------------------------------------- | ----------------- | --------- | | text | copied contents | string | - | | onClick | callback when clicking the contents | React.MouseEvent | - | +| onCopy | callback after the copy attempt, receives whether the copy succeeded | `(copied: boolean, text: string) => 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/copy-to-clipboard/index.tsx b/packages/react/src/copy-to-clipboard/index.tsx index 9b32e421..bab6a1d8 100755 --- a/packages/react/src/copy-to-clipboard/index.tsx +++ b/packages/react/src/copy-to-clipboard/index.tsx @@ -1,3 +1,4 @@ import CopyToClipboard from './copy-to-clipboard'; export default CopyToClipboard; +export type * from './types'; diff --git a/packages/react/src/copy-to-clipboard/index.zh_CN.md b/packages/react/src/copy-to-clipboard/index.zh_CN.md index 63e5fea9..83929bb7 100644 --- a/packages/react/src/copy-to-clipboard/index.zh_CN.md +++ b/packages/react/src/copy-to-clipboard/index.zh_CN.md @@ -44,5 +44,6 @@ import { CopyToClipboard } from 'tiny-design'; | --------- | ----------------------------- | ----------------- | --------- | | text | 复制的内容 | string | - | | onClick | 点击内容时的回调 | React.MouseEvent | - | +| onCopy | 复制尝试结束后的回调,返回是否复制成功和复制文本 | `(copied: boolean, text: string) => void` | - | | style | 容器的样式对象 | CSSProperties | - | -| className | 容器的类名 | string | - | \ No newline at end of file +| className | 容器的类名 | string | - | diff --git a/packages/react/src/copy-to-clipboard/types.ts b/packages/react/src/copy-to-clipboard/types.ts index d8d4d8ba..ed7a378a 100644 --- a/packages/react/src/copy-to-clipboard/types.ts +++ b/packages/react/src/copy-to-clipboard/types.ts @@ -3,7 +3,8 @@ import { BaseProps } from '../_utils/props'; export interface CopyToClipboardProps extends BaseProps, - React.PropsWithoutRef { + Omit, 'onCopy'> { text: string; + onCopy?: (copied: boolean, text: string) => void; children?: React.ReactNode; } diff --git a/packages/react/src/countdown/index.tsx b/packages/react/src/countdown/index.tsx index 95a997bc..f15d6d10 100755 --- a/packages/react/src/countdown/index.tsx +++ b/packages/react/src/countdown/index.tsx @@ -1,3 +1,4 @@ import Countdown from './countdown'; export default Countdown; +export type * from './types'; diff --git a/packages/react/src/date-picker/index.tsx b/packages/react/src/date-picker/index.tsx index 0c4651bf..76eb7562 100755 --- a/packages/react/src/date-picker/index.tsx +++ b/packages/react/src/date-picker/index.tsx @@ -1,4 +1,4 @@ import DatePicker from './date-picker'; -export type { DatePickerProps, PanelMode, PickerType } from './types'; +export type * from './types'; export default DatePicker; diff --git a/packages/react/src/descriptions/index.tsx b/packages/react/src/descriptions/index.tsx index 114a42e2..7876b0ab 100755 --- a/packages/react/src/descriptions/index.tsx +++ b/packages/react/src/descriptions/index.tsx @@ -9,3 +9,4 @@ const DefaultDesc = Descriptions as IDesc; DefaultDesc.Item = DescriptionsItem; export default DefaultDesc; +export type * from './types'; 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/divider/index.tsx b/packages/react/src/divider/index.tsx index eeb1444f..d45ece7f 100755 --- a/packages/react/src/divider/index.tsx +++ b/packages/react/src/divider/index.tsx @@ -1,3 +1,4 @@ import Divider from './divider'; export default Divider; +export type * from './types'; diff --git a/packages/react/src/drawer/index.tsx b/packages/react/src/drawer/index.tsx index 7f65a833..623aafd5 100755 --- a/packages/react/src/drawer/index.tsx +++ b/packages/react/src/drawer/index.tsx @@ -1,3 +1,4 @@ import Drawer from './drawer'; export default Drawer; +export type * from './types'; diff --git a/packages/react/src/dropdown/index.tsx b/packages/react/src/dropdown/index.tsx index e55f5586..9e0c4a2f 100755 --- a/packages/react/src/dropdown/index.tsx +++ b/packages/react/src/dropdown/index.tsx @@ -1,3 +1,4 @@ import Dropdown from './dropdown'; export default Dropdown; +export type * from './types'; diff --git a/packages/react/src/empty/index.tsx b/packages/react/src/empty/index.tsx index 607ffca6..f147ecbc 100755 --- a/packages/react/src/empty/index.tsx +++ b/packages/react/src/empty/index.tsx @@ -1,3 +1,4 @@ import Empty from './empty'; export default Empty; +export type * from './types'; diff --git a/packages/react/src/flex/index.tsx b/packages/react/src/flex/index.tsx index 298a2806..1c79c207 100644 --- a/packages/react/src/flex/index.tsx +++ b/packages/react/src/flex/index.tsx @@ -1,3 +1,4 @@ import Flex from './flex'; export default Flex; +export type * from './types'; diff --git a/packages/react/src/flip/index.tsx b/packages/react/src/flip/index.tsx index cb5acbb8..a0a3358e 100755 --- a/packages/react/src/flip/index.tsx +++ b/packages/react/src/flip/index.tsx @@ -9,3 +9,4 @@ const DefaultFlip = Flip as IFlip; DefaultFlip.Item = FlipItem; export default DefaultFlip; +export type * from './types'; diff --git a/packages/react/src/form/index.tsx b/packages/react/src/form/index.tsx index cad727d0..b1ff5ef4 100755 --- a/packages/react/src/form/index.tsx +++ b/packages/react/src/form/index.tsx @@ -15,3 +15,4 @@ DefaultForm.useForm = useForm; DefaultForm.FormInstance = FormInstance; export default DefaultForm; +export type * from './types'; 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/image/index.tsx b/packages/react/src/image/index.tsx index a707bc34..88d2c122 100755 --- a/packages/react/src/image/index.tsx +++ b/packages/react/src/image/index.tsx @@ -1,3 +1,4 @@ import Image from './image'; export default Image; +export type * from './types'; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 7b64e786..63badd91 100755 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -90,7 +90,89 @@ export { withSpin } from './with-spin'; export { en_US, zh_CN } from './locale'; export type { Locale } from './locale'; +export type * from './alert'; +export type * from './anchor'; +export type * from './aspect-ratio'; +export type * from './auto-complete'; +export type * from './avatar'; +export type * from './back-top'; +export type * from './badge'; +export type * from './breadcrumb'; +export type * from './button'; +export type * from './calendar'; +export type * from './card'; +export type * from './carousel'; +export type * from './cascader'; +export type * from './checkbox'; +export type * from './col'; +export type * from './config-provider'; +export type * from './copy-to-clipboard'; +export type * from './countdown'; +export type * from './date-picker'; +export type * from './descriptions'; +export type * from './divider'; +export type * from './drawer'; +export type * from './dropdown'; +export type * from './empty'; +export type * from './flex'; +export type * from './flip'; +export type * from './form'; +export type * from './image'; +export type * from './input'; +export type * from './input-number'; +export type * from './input-otp'; +export type * from './input-password'; +export type * from './keyboard'; +export type * from './link'; +export type * from './list'; +export type * from './loader'; +export type * from './loading-bar'; +export type * from './marquee'; +export type * from './message'; +export type * from './modal'; +export type * from './native-select'; +export type * from './notification'; +export type * from './overlay'; +export type * from './pagination'; +export type * from './pop-confirm'; +export type * from './popover'; +export type * from './popup'; +export type * from './progress'; +export type * from './radio'; +export type * from './rate'; +export type * from './result'; +export type * from './row'; +export type * from './scroll-indicator'; +export type * from './scroll-number'; +export type * from './segmented'; +export type * from './select'; +export type * from './skeleton'; +export type * from './slider'; +export type * from './space'; +export type * from './speed-dial'; +export type * from './split'; +export type * from './split-button'; +export type * from './statistic'; +export type * from './steps'; +export type * from './sticky'; +export type * from './strength-indicator'; +export type * from './switch'; +export type * from './table'; +export type * from './tabs'; +export type * from './tag'; +export type * from './text-loop'; +export type * from './textarea'; +export type * from './time-picker'; +export type * from './timeline'; +export type * from './tooltip'; +export type * from './tour'; +export type * from './transfer'; +export type * from './transition'; +export type * from './tree'; +export type * from './typography'; +export type * from './upload'; +export type * from './waterfall'; + export { useLocale } from './_utils/use-locale'; export { useTheme } from './_utils/use-theme'; -export type { ThemeMode } from './_utils/use-theme'; -export type { TourProps, TourStepProps } from './tour'; +export type { ThemeMode } from './config-provider/config-context'; diff --git a/packages/react/src/input-number/index.tsx b/packages/react/src/input-number/index.tsx index ec494d98..7f2ea53d 100755 --- a/packages/react/src/input-number/index.tsx +++ b/packages/react/src/input-number/index.tsx @@ -1,3 +1,4 @@ import InputNumber from './input-number'; export default InputNumber; +export type * from './types'; 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/index.tsx b/packages/react/src/input-otp/index.tsx index 05680dc6..33e063ef 100644 --- a/packages/react/src/input-otp/index.tsx +++ b/packages/react/src/input-otp/index.tsx @@ -1,3 +1,4 @@ import InputOTP from './input-otp'; export default InputOTP; +export type * from './types'; 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-password/index.tsx b/packages/react/src/input-password/index.tsx index 985e0b16..b3995910 100755 --- a/packages/react/src/input-password/index.tsx +++ b/packages/react/src/input-password/index.tsx @@ -1,3 +1,4 @@ import InputPassword from './input-password'; export default InputPassword; +export type * from './types'; diff --git a/packages/react/src/input/index.tsx b/packages/react/src/input/index.tsx index 3e5ba147..3f0eace7 100755 --- a/packages/react/src/input/index.tsx +++ b/packages/react/src/input/index.tsx @@ -12,3 +12,4 @@ DefaultInput.Group = InputGroup; DefaultInput.Addon = InputGroupAddon; export default DefaultInput; +export type * from './types'; diff --git a/packages/react/src/input/style/_index.scss b/packages/react/src/input/style/_index.scss index 8aacd2e4..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: $input-sm-font-size; - height: $input-sm-height; - line-height: $input-sm-height; + 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: $input-md-font-size; - height: $input-md-height; - line-height: $input-md-height; + 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: $input-lg-font-size; - height: $input-lg-height; - line-height: $input-lg-height; + 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: $input-sm-height; + height: var(--ty-input-height-sm, var(--ty-height-sm)); } &_md { - height: $input-md-height; + height: var(--ty-input-height-md, var(--ty-height-md)); } &_lg { - height: $input-lg-height; + 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: $input-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: $input-sm-font-size; + font-size: var(--ty-input-font-size-sm, var(--ty-font-size-sm)); } &_md { - font-size: $input-md-font-size; + font-size: var(--ty-input-font-size, var(--ty-font-size-base)); } &_lg { - font-size: $input-lg-font-size; + 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 dff7c129..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: $input-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/keyboard/index.tsx b/packages/react/src/keyboard/index.tsx index 29f0a54f..49931a92 100644 --- a/packages/react/src/keyboard/index.tsx +++ b/packages/react/src/keyboard/index.tsx @@ -1,3 +1,4 @@ import Keyboard from './keyboard'; export default Keyboard; +export type * from './types'; 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/link/index.tsx b/packages/react/src/link/index.tsx index 45641fab..0420caf8 100755 --- a/packages/react/src/link/index.tsx +++ b/packages/react/src/link/index.tsx @@ -1,3 +1,4 @@ import Link from './link'; export default Link; +export type * from './types'; diff --git a/packages/react/src/list/index.tsx b/packages/react/src/list/index.tsx index 43503268..cf8356de 100644 --- a/packages/react/src/list/index.tsx +++ b/packages/react/src/list/index.tsx @@ -17,3 +17,4 @@ DefaultList.Item = ListItem; DefaultList.ItemMeta = ListItemMeta; export default DefaultList; +export type * from './types'; 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/loader/index.tsx b/packages/react/src/loader/index.tsx index c16a91f7..5961b1c3 100755 --- a/packages/react/src/loader/index.tsx +++ b/packages/react/src/loader/index.tsx @@ -1,3 +1,4 @@ import Loader from './loader'; export default Loader; +export type * from './types'; diff --git a/packages/react/src/loading-bar/index.ts b/packages/react/src/loading-bar/index.ts index 934b4199..25923a9a 100755 --- a/packages/react/src/loading-bar/index.ts +++ b/packages/react/src/loading-bar/index.ts @@ -1,20 +1,19 @@ import React from 'react'; -import { createRoot, Root } from 'react-dom/client'; import LoadingBar from './loading-bar'; +import { + createStaticHost, + destroyStaticHost, + renderStaticHost, +} from '../config-provider/static-host'; let rafId: number | null = null; let loadingBar: HTMLElement | null = null; let outerDiv: HTMLElement | null = null; -let root: Root | null = null; let width = 0; const reset = (): void => { - if (root) { - root.unmount(); - root = null; - } if (outerDiv) { - document.body.removeChild(outerDiv); + destroyStaticHost(outerDiv); } loadingBar = null; outerDiv = null; @@ -34,18 +33,13 @@ const move = (): void => { }; const createComponent = (): void => { - outerDiv = document.createElement('div'); - document.body.appendChild(outerDiv); - - const component = React.createElement(LoadingBar, { + outerDiv = createStaticHost(); + renderStaticHost(outerDiv, React.createElement(LoadingBar, { didMount: () => { loadingBar = document.getElementById('ty-loading-bar'); rafId = requestAnimationFrame(move); }, - }); - - root = createRoot(outerDiv); - root.render(component); + })); }; const unmountDom = (): void => { @@ -88,3 +82,5 @@ export default { succeed, fail, }; + +export type * from './types'; diff --git a/packages/react/src/marquee/index.tsx b/packages/react/src/marquee/index.tsx index 17289a9c..9b8e3c8e 100644 --- a/packages/react/src/marquee/index.tsx +++ b/packages/react/src/marquee/index.tsx @@ -1,3 +1,4 @@ import Marquee from './marquee'; export default Marquee; +export type * from './types'; diff --git a/packages/react/src/menu/index.tsx b/packages/react/src/menu/index.tsx index b3ef4dd6..21165336 100755 --- a/packages/react/src/menu/index.tsx +++ b/packages/react/src/menu/index.tsx @@ -18,3 +18,4 @@ DefaultMenu.ItemGroup = MenuItemGroup; DefaultMenu.Divider = MenuDivider; export default DefaultMenu; +export type * from './types'; 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/message/index.tsx b/packages/react/src/message/index.tsx index cab458de..221d52c4 100755 --- a/packages/react/src/message/index.tsx +++ b/packages/react/src/message/index.tsx @@ -1,4 +1,5 @@ import messageContainer from './message-container'; +export type * from './types'; export type { Options } from './message-container'; export default messageContainer; diff --git a/packages/react/src/message/message-container.tsx b/packages/react/src/message/message-container.tsx index c8a48255..0f157924 100755 --- a/packages/react/src/message/message-container.tsx +++ b/packages/react/src/message/message-container.tsx @@ -1,10 +1,13 @@ import React, { ReactNode } from 'react'; -import { createRoot, Root } from 'react-dom/client'; import Message from './message'; import { MessageProps, MessageType } from './types'; +import { + createStaticHost, + destroyStaticHost, + renderStaticHost, +} from '../config-provider/static-host'; const className = '.ty-message-container'; -const rootMap = new Map(); export type Options = { top?: number; @@ -32,12 +35,7 @@ type UnmountDom = ( let offset: number; const unmountDom: UnmountDom = (containerDiv, top, height, onClose) => { - const root = rootMap.get(containerDiv); - if (root) { - root.unmount(); - rootMap.delete(containerDiv); - } - document.body.removeChild(containerDiv); + destroyStaticHost(containerDiv); requestAnimationFrame(() => { const containers = document.querySelectorAll(className); const len = containers.length; @@ -66,9 +64,7 @@ const createComponent: CreateComponent = ( ? parseInt(lastContainer.style.top || '0', 10) + lastContainer.offsetHeight + offset : options.top || 15; - const div = document.createElement('div'); - div.className = 'ty-message-container'; - document.body.appendChild(div); + const div = createStaticHost('ty-message-container'); div.style.top = `${top}px`; const props: MessageProps = { @@ -83,10 +79,7 @@ const createComponent: CreateComponent = ( unmountDom(div, updatedTop, height, onClose); }, }; - const component = React.createElement(Message, props); - const root = createRoot(div); - rootMap.set(div, root); - root.render(component); + renderStaticHost(div, React.createElement(Message, props)); }; const messageContainer: any = ( diff --git a/packages/react/src/modal/__tests__/modal.test.tsx b/packages/react/src/modal/__tests__/modal.test.tsx index b779474d..6531e2c9 100644 --- a/packages/react/src/modal/__tests__/modal.test.tsx +++ b/packages/react/src/modal/__tests__/modal.test.tsx @@ -1,8 +1,13 @@ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { act, render, fireEvent } from '@testing-library/react'; import Modal from '../index'; +import ConfigProvider from '../../config-provider'; describe('', () => { + afterEach(() => { + ConfigProvider.config({ holderRender: undefined }); + }); + it('should match the snapshot', () => { const { asFragment } = render(Content); expect(asFragment()).toMatchSnapshot(); @@ -39,4 +44,64 @@ describe('', () => { const { container } = render(Content); expect(container.querySelector('.ty-modal__footer')).toBeFalsy(); }); + + it('should support static modal open', () => { + let instance!: ReturnType; + + act(() => { + instance = Modal.open({ + header: 'Static Modal', + children: 'Static Content', + }); + }); + + expect(document.body.textContent).toContain('Static Modal'); + expect(document.body.textContent).toContain('Static Content'); + + act(() => { + instance.destroy(); + }); + }); + + it('should support static modal confirm', () => { + let instance!: ReturnType; + + act(() => { + instance = Modal.confirm({ + header: 'Confirm Modal', + children: 'Confirm Content', + }); + }); + + expect(document.body.textContent).toContain('Confirm Modal'); + expect(document.body.textContent).toContain('Confirm Content'); + + act(() => { + instance.destroy(); + }); + }); + + it('should apply holderRender to static modal APIs', () => { + ConfigProvider.config({ + holderRender: (children) => ( + {children} + ), + }); + + let instance!: ReturnType; + + act(() => { + instance = Modal.open({ + header: 'Static Holder Modal', + children: 'Holder Content', + }); + }); + + expect(document.body.querySelector('[data-testid="modal-holder"]')).toBeTruthy(); + expect(document.body.textContent).toContain('Static Holder Modal'); + + act(() => { + instance.destroy(); + }); + }); }); diff --git a/packages/react/src/modal/index.md b/packages/react/src/modal/index.md index b51281cb..e94482f2 100755 --- a/packages/react/src/modal/index.md +++ b/packages/react/src/modal/index.md @@ -23,6 +23,23 @@ When requiring users to interact with the application, but without jumping to a import { Modal } from 'tiny-design'; ``` +## Static Methods + +Use `Modal.open()` or `Modal.confirm()` when the dialog needs to be triggered imperatively outside the local React tree. + +```jsx +const instance = Modal.open({ + header: 'Delete item', + children: 'This action cannot be undone.', +}); + +instance.update({ + confirmLoading: true, +}); + +instance.destroy(); +``` + ## Examples @@ -108,4 +125,8 @@ Manage multiple modals by ID with `Modal.Provider` and `Modal.useModal`. | footerStyle | inline style of the footer | CSSProperties | - | | maskStyle | inline style of the mask | CSSProperties | - | | style | style object of container | CSSProperties | - | -| className | className of container | string | - | \ No newline at end of file +| className | className of container | string | - | + +## StaticModalProps + +`Modal.open()` and `Modal.confirm()` accept the same props as `Modal`, except `visible` is managed internally. diff --git a/packages/react/src/modal/index.tsx b/packages/react/src/modal/index.tsx index c1bf66d4..1aec3f03 100755 --- a/packages/react/src/modal/index.tsx +++ b/packages/react/src/modal/index.tsx @@ -1,15 +1,26 @@ import Modal from './modal'; import { ModalProvider, useModal } from './modal-context'; import type { ModalProviderProps, UseModalReturn } from './modal-context'; +import { + confirmStaticModal, + openStaticModal, + StaticModalInstance, + StaticModalProps, +} from './static-modal'; type ModalComponent = typeof Modal & { Provider: typeof ModalProvider; useModal: typeof useModal; + open: (config: StaticModalProps) => StaticModalInstance; + confirm: (config: StaticModalProps) => StaticModalInstance; }; const ModalWithContext = Modal as ModalComponent; ModalWithContext.Provider = ModalProvider; ModalWithContext.useModal = useModal; +ModalWithContext.open = openStaticModal; +ModalWithContext.confirm = confirmStaticModal; export default ModalWithContext; -export type { ModalProviderProps, UseModalReturn }; +export type * from './types'; +export type { ModalProviderProps, UseModalReturn, StaticModalInstance, StaticModalProps }; diff --git a/packages/react/src/modal/index.zh_CN.md b/packages/react/src/modal/index.zh_CN.md index 05e98db4..b23a49ce 100644 --- a/packages/react/src/modal/index.zh_CN.md +++ b/packages/react/src/modal/index.zh_CN.md @@ -6,6 +6,8 @@ import PositionDemo from './demo/Position'; import PositionSource from './demo/Position.tsx?raw'; import AnimationDemo from './demo/Animation'; import AnimationSource from './demo/Animation.tsx?raw'; +import ContextDemo from './demo/Context'; +import ContextSource from './demo/Context.tsx?raw'; # Modal 模态对话框 @@ -21,6 +23,23 @@ import AnimationSource from './demo/Animation.tsx?raw'; import { Modal } from 'tiny-design'; ``` +## 静态方法 + +当需要在当前 React 树之外以命令式方式触发对话框时,可以使用 `Modal.open()` 或 `Modal.confirm()`。 + +```jsx +const instance = Modal.open({ + header: '删除项目', + children: '这个操作不可撤销。', +}); + +instance.update({ + confirmLoading: true, +}); + +instance.destroy(); +``` + ## 代码示例 @@ -63,6 +82,15 @@ import { Modal } from 'tiny-design'; + + + +### 上下文 + +使用 `Modal.Provider` 和 `Modal.useModal` 通过 ID 管理多个对话框。 + + + @@ -97,4 +125,8 @@ import { Modal } from 'tiny-design'; | footerStyle | 底部的内联样式 | CSSProperties | - | | maskStyle | 遮罩层的内联样式 | CSSProperties | - | | style | 容器的样式对象 | CSSProperties | - | -| className | 容器的类名 | string | - | \ No newline at end of file +| className | 容器的类名 | string | - | + +## StaticModalProps + +`Modal.open()` 和 `Modal.confirm()` 接收与 `Modal` 相同的参数,但 `visible` 由内部管理。 diff --git a/packages/react/src/modal/static-modal.tsx b/packages/react/src/modal/static-modal.tsx new file mode 100644 index 00000000..2c67159f --- /dev/null +++ b/packages/react/src/modal/static-modal.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import Modal from './modal'; +import { ModalProps } from './types'; +import { + createStaticHost, + destroyStaticHost, + renderStaticHost, +} from '../config-provider/static-host'; + +export type StaticModalProps = Omit; + +export interface StaticModalInstance { + destroy: () => void; + update: (config: StaticModalProps) => void; +} + +function createStaticModal(config: StaticModalProps): StaticModalInstance { + const container = createStaticHost(); + let currentConfig = config; + let visible = true; + + const render = (): void => { + renderStaticHost( + container, + ( + { + currentConfig.onClose?.(event); + currentConfig.onCancel?.(event); + visible = false; + render(); + }} + afterClose={() => { + currentConfig.afterClose?.(); + destroyStaticHost(container); + }} + /> + ) + ); + }; + + const destroy = (): void => { + visible = false; + render(); + }; + + const update = (nextConfig: StaticModalProps): void => { + currentConfig = { ...currentConfig, ...nextConfig }; + render(); + }; + + render(); + + return { + destroy, + update, + }; +} + +export function openStaticModal(config: StaticModalProps): StaticModalInstance { + return createStaticModal(config); +} + +export function confirmStaticModal(config: StaticModalProps): StaticModalInstance { + return createStaticModal(config); +} diff --git a/packages/react/src/native-select/index.tsx b/packages/react/src/native-select/index.tsx index f8a7e28a..860fddf5 100755 --- a/packages/react/src/native-select/index.tsx +++ b/packages/react/src/native-select/index.tsx @@ -12,3 +12,4 @@ DefaultSelect.Option = NativeOption; DefaultSelect.OptGroup = NativeOptGroup; export default DefaultSelect; +export type * from './types'; 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/notification/index.tsx b/packages/react/src/notification/index.tsx index 128321a2..1bbf09bf 100755 --- a/packages/react/src/notification/index.tsx +++ b/packages/react/src/notification/index.tsx @@ -1,3 +1,4 @@ import notificationContainer from './notification-container'; export default notificationContainer; +export type * from './types'; diff --git a/packages/react/src/notification/notification-container.tsx b/packages/react/src/notification/notification-container.tsx index 87cbe9fc..61becbad 100755 --- a/packages/react/src/notification/notification-container.tsx +++ b/packages/react/src/notification/notification-container.tsx @@ -1,11 +1,14 @@ import React, { ReactNode, MouseEventHandler } from 'react'; -import { createRoot, Root } from 'react-dom/client'; import Notification from './notification'; import { camelCaseToDash } from '../_utils/general'; import { NotificationProps, NotificationType } from './types'; +import { + createStaticHost, + destroyStaticHost, + renderStaticHost, +} from '../config-provider/static-host'; const className = 'ty-notification-container'; -const rootMap = new Map(); type Direction = 'top' | 'bottom'; type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; @@ -32,12 +35,7 @@ type UnmountDom = ( let offset: number; const unmountDom: UnmountDom = (queryName, containerDiv, position, height, direction) => { - const root = rootMap.get(containerDiv); - if (root) { - root.unmount(); - rootMap.delete(containerDiv); - } - document.body.removeChild(containerDiv); + destroyStaticHost(containerDiv); requestAnimationFrame(() => { const containers = document.querySelectorAll(`.${queryName}`); const len = containers.length; @@ -57,9 +55,7 @@ const createComponent = (options: Options, type: NotificationType) => { const lastContainer = containers.length > 0 ? (containers[containers.length - 1] as HTMLElement) : null; - const div = document.createElement('div'); - div.className = `${className} ${queryName}`; - document.body.appendChild(div); + const div = createStaticHost(`${className} ${queryName}`); offset = options.offset || 24; const direction: Direction = placement.includes('top') ? 'top' : 'bottom'; @@ -85,10 +81,7 @@ const createComponent = (options: Options, type: NotificationType) => { unmountDom(queryName, div, updatedPosition, height, direction); }, }; - const element = React.createElement(Notification, props); - const root = createRoot(div); - rootMap.set(div, root); - root.render(element); + renderStaticHost(div, React.createElement(Notification, props)); }; const open = (options: Options) => { 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/overlay/__tests__/overlay.test.tsx b/packages/react/src/overlay/__tests__/overlay.test.tsx index df83a9b3..69e101e4 100644 --- a/packages/react/src/overlay/__tests__/overlay.test.tsx +++ b/packages/react/src/overlay/__tests__/overlay.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import Overlay from '../index'; +import ConfigProvider from '../../config-provider'; describe('', () => { it('should match the snapshot', () => { @@ -17,4 +18,23 @@ describe('', () => { const { baseElement } = render(Content); expect(baseElement.querySelector('.ty-overlay_blurred')).toBeInTheDocument(); }); + + it('should lock and restore the configured target container', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const { unmount } = render( + container}> + Content + + ); + + expect(container.style.overflow).toBe('hidden'); + expect(document.body.style.overflow).toBe(''); + + unmount(); + + expect(container.style.overflow).toBe(''); + document.body.removeChild(container); + }); }); diff --git a/packages/react/src/overlay/index.tsx b/packages/react/src/overlay/index.tsx index 90fd532b..bfd85433 100755 --- a/packages/react/src/overlay/index.tsx +++ b/packages/react/src/overlay/index.tsx @@ -1,3 +1,4 @@ import Overlay from './overlay'; export default Overlay; +export type * from './types'; diff --git a/packages/react/src/overlay/overlay.tsx b/packages/react/src/overlay/overlay.tsx index 15a29277..9830f074 100755 --- a/packages/react/src/overlay/overlay.tsx +++ b/packages/react/src/overlay/overlay.tsx @@ -3,11 +3,11 @@ import classNames from 'classnames'; import Portal from '../portal'; import Transition from '../transition'; import { ConfigContext } from '../config-provider/config-context'; +import { resolveTargetContainer } from '../config-provider/container-utils'; +import { acquireScrollLock } from '../config-provider/scroll-lock'; import { getPrefixCls } from '../_utils/general'; import { OverlayProps } from './types'; -let scrollLockCount = 0; - const Overlay = (props: OverlayProps): JSX.Element => { const { isShow = false, @@ -30,20 +30,12 @@ const Overlay = (props: OverlayProps): JSX.Element => { const nodeRef = useRef(null); useEffect(() => { - if (isShow) { - scrollLockCount++; - document.body.style.overflow = 'hidden'; + if (!isShow) { + return undefined; } - return () => { - if (isShow) { - scrollLockCount--; - if (scrollLockCount <= 0) { - scrollLockCount = 0; - document.body.style.overflow = ''; - } - } - }; - }, [isShow]); + + return acquireScrollLock(resolveTargetContainer(configContext)); + }, [configContext, isShow]); return ( diff --git a/packages/react/src/pagination/index.tsx b/packages/react/src/pagination/index.tsx index 02606a67..b84432a8 100755 --- a/packages/react/src/pagination/index.tsx +++ b/packages/react/src/pagination/index.tsx @@ -1,3 +1,4 @@ import Pagination from './pagination'; export default Pagination; +export type * from './types'; diff --git a/packages/react/src/pop-confirm/index.tsx b/packages/react/src/pop-confirm/index.tsx index 96a8b809..d5064c1e 100755 --- a/packages/react/src/pop-confirm/index.tsx +++ b/packages/react/src/pop-confirm/index.tsx @@ -1,3 +1,4 @@ import PopConfirm from './pop-confirm'; export default PopConfirm; +export type * from './types'; diff --git a/packages/react/src/popover/index.tsx b/packages/react/src/popover/index.tsx index d5fced26..42d11a8b 100755 --- a/packages/react/src/popover/index.tsx +++ b/packages/react/src/popover/index.tsx @@ -1,3 +1,4 @@ import Popover from './popover'; export default Popover; +export type * from './types'; diff --git a/packages/react/src/popup/index.tsx b/packages/react/src/popup/index.tsx index 3ff84083..d620e40b 100644 --- a/packages/react/src/popup/index.tsx +++ b/packages/react/src/popup/index.tsx @@ -1,3 +1,4 @@ import Popup from './popup'; export default Popup; +export type * from './types'; diff --git a/packages/react/src/popup/popup.tsx b/packages/react/src/popup/popup.tsx index bb4c7f6e..9fa6e7b3 100644 --- a/packages/react/src/popup/popup.tsx +++ b/packages/react/src/popup/popup.tsx @@ -266,7 +266,13 @@ const Popup = (props: PopupProps): JSX.Element => { return ( <> {React.cloneElement(children, elementProps)} - {usePortal ? {renderContent()} : renderContent()} + {usePortal ? ( + + {renderContent()} + + ) : ( + renderContent() + )} > ); }; diff --git a/packages/react/src/popup/style/_index.scss b/packages/react/src/popup/style/_index.scss index 96dcdef9..040bbad4 100755 --- a/packages/react/src/popup/style/_index.scss +++ b/packages/react/src/popup/style/_index.scss @@ -1,9 +1,8 @@ -@use 'sass:math'; @use '@tiny-design/tokens/scss/variables' as *; .#{$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; @@ -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/portal/portal.tsx b/packages/react/src/portal/portal.tsx index 24391ed6..23ec6ca6 100755 --- a/packages/react/src/portal/portal.tsx +++ b/packages/react/src/portal/portal.tsx @@ -1,14 +1,17 @@ -import React from 'react'; +import React, { useContext } from 'react'; import ReactDOM from 'react-dom'; +import { ConfigContext } from '../config-provider/config-context'; +import { resolvePopupContainer } from '../config-provider/container-utils'; export interface PortalProps { - container?: HTMLElement; + container?: HTMLElement | null; children?: React.ReactNode; } const Portal = (props: PortalProps): React.ReactPortal | null => { + const configContext = useContext(ConfigContext); const { container, children } = props; - const target = container ?? (typeof document !== 'undefined' ? document.body : null); + const target = container ?? (typeof document !== 'undefined' ? resolvePopupContainer(configContext) : null); if (!target) return null; return ReactDOM.createPortal(children, target); }; diff --git a/packages/react/src/progress/index.tsx b/packages/react/src/progress/index.tsx index 4fa9a512..1c06f22b 100755 --- a/packages/react/src/progress/index.tsx +++ b/packages/react/src/progress/index.tsx @@ -7,3 +7,4 @@ const Progress = { }; export default Progress; +export type * from './types'; diff --git a/packages/react/src/radio/index.tsx b/packages/react/src/radio/index.tsx index 489ba0f2..296b067f 100755 --- a/packages/react/src/radio/index.tsx +++ b/packages/react/src/radio/index.tsx @@ -9,3 +9,4 @@ const DefaultRadio = Radio as IRadio; DefaultRadio.Group = RadioGroup; export default DefaultRadio; +export type * from './types'; diff --git a/packages/react/src/rate/index.tsx b/packages/react/src/rate/index.tsx index 428f42c0..653c0fe3 100755 --- a/packages/react/src/rate/index.tsx +++ b/packages/react/src/rate/index.tsx @@ -1,3 +1,4 @@ import Rate from './rate'; export default Rate; +export type * from './types'; diff --git a/packages/react/src/result/index.tsx b/packages/react/src/result/index.tsx index 2cdcb443..ccb316ee 100755 --- a/packages/react/src/result/index.tsx +++ b/packages/react/src/result/index.tsx @@ -1,3 +1,4 @@ import Result from './result'; export default Result; +export type * from './types'; diff --git a/packages/react/src/row/index.tsx b/packages/react/src/row/index.tsx index 7debfb82..e2852c02 100755 --- a/packages/react/src/row/index.tsx +++ b/packages/react/src/row/index.tsx @@ -1,3 +1,4 @@ import Row from '../grid/row'; export default Row; +export type { RowProps, RowAlign, RowJustify } from '../grid/types'; diff --git a/packages/react/src/scroll-indicator/index.tsx b/packages/react/src/scroll-indicator/index.tsx index c7d08665..6126df46 100755 --- a/packages/react/src/scroll-indicator/index.tsx +++ b/packages/react/src/scroll-indicator/index.tsx @@ -1,3 +1,4 @@ import ScrollIndicator from './scroll-indicator'; export default ScrollIndicator; +export type * from './types'; diff --git a/packages/react/src/scroll-number/index.tsx b/packages/react/src/scroll-number/index.tsx index 4d3ac8a0..6f2b3d3e 100644 --- a/packages/react/src/scroll-number/index.tsx +++ b/packages/react/src/scroll-number/index.tsx @@ -1,3 +1,4 @@ import ScrollNumber from './scroll-number'; export default ScrollNumber; +export type * from './types'; 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/index.tsx b/packages/react/src/segmented/index.tsx index aaf9ba44..44cd7603 100644 --- a/packages/react/src/segmented/index.tsx +++ b/packages/react/src/segmented/index.tsx @@ -1,3 +1,4 @@ import Segmented from './segmented'; export default Segmented; +export type * from './types'; 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/index.tsx b/packages/react/src/select/index.tsx index 8d9bc448..20911395 100755 --- a/packages/react/src/select/index.tsx +++ b/packages/react/src/select/index.tsx @@ -12,3 +12,4 @@ DefaultSelect.Option = SelectOption; DefaultSelect.OptGroup = SelectOptGroup; export default DefaultSelect; +export type * from './types'; diff --git a/packages/react/src/select/style/_index.scss b/packages/react/src/select/style/_index.scss index 9a8282e5..8bdbaa4f 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; } @@ -61,8 +61,8 @@ display: flex; align-items: center; width: 100%; - border: $border-width solid var(--ty-input-border); - border-radius: var(--ty-border-radius); + border: 1px solid var(--ty-input-border); + 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; @@ -214,9 +214,9 @@ box-sizing: border-box; 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); + 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; } @@ -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); @@ -286,8 +286,8 @@ &__title { font-size: var(--ty-font-size-sm); cursor: default; - color: $gray-600; - padding: 7px 12px; + color: var(--ty-color-text-secondary); + padding: var(--ty-select-option-padding); } &__list { diff --git a/packages/react/src/skeleton/index.tsx b/packages/react/src/skeleton/index.tsx index fb7d3a31..87722b8f 100755 --- a/packages/react/src/skeleton/index.tsx +++ b/packages/react/src/skeleton/index.tsx @@ -1,3 +1,4 @@ import Skeleton from './skeleton'; export default Skeleton; +export type * from './types'; diff --git a/packages/react/src/slider/index.tsx b/packages/react/src/slider/index.tsx index c176cbfd..24cdd9a4 100755 --- a/packages/react/src/slider/index.tsx +++ b/packages/react/src/slider/index.tsx @@ -1,3 +1,4 @@ import Slider from './slider'; export default Slider; +export type * from './types'; diff --git a/packages/react/src/space/index.tsx b/packages/react/src/space/index.tsx index 180366b9..3c4f78c4 100755 --- a/packages/react/src/space/index.tsx +++ b/packages/react/src/space/index.tsx @@ -1,3 +1,4 @@ import Space from './space'; export default Space; +export type * from './types'; diff --git a/packages/react/src/speed-dial/index.tsx b/packages/react/src/speed-dial/index.tsx index 6079d16a..b591582f 100644 --- a/packages/react/src/speed-dial/index.tsx +++ b/packages/react/src/speed-dial/index.tsx @@ -9,3 +9,4 @@ const DefaultSpeedDial = SpeedDial as ISpeedDial; DefaultSpeedDial.Action = SpeedDialAction; export default DefaultSpeedDial; +export type * from './types'; 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/split-button/index.tsx b/packages/react/src/split-button/index.tsx index 770e0a09..6feb9f31 100755 --- a/packages/react/src/split-button/index.tsx +++ b/packages/react/src/split-button/index.tsx @@ -1,3 +1,4 @@ import SplitButton from './split-button'; export default SplitButton; +export type * from './types'; diff --git a/packages/react/src/split/index.tsx b/packages/react/src/split/index.tsx index 6064f2cc..7e014d1b 100755 --- a/packages/react/src/split/index.tsx +++ b/packages/react/src/split/index.tsx @@ -1,3 +1,4 @@ import Split from './split'; export default Split; +export type * from './types'; diff --git a/packages/react/src/statistic/index.tsx b/packages/react/src/statistic/index.tsx index 60b97b6e..2b13737b 100644 --- a/packages/react/src/statistic/index.tsx +++ b/packages/react/src/statistic/index.tsx @@ -1,3 +1,4 @@ import Statistic from './statistic'; export default Statistic; +export type * from './types'; 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/steps/index.tsx b/packages/react/src/steps/index.tsx index 969beea7..b5bea3da 100755 --- a/packages/react/src/steps/index.tsx +++ b/packages/react/src/steps/index.tsx @@ -9,3 +9,4 @@ const DefaultSteps = Steps as ISteps; DefaultSteps.Step = StepsItem; export default DefaultSteps; +export type * from './types'; diff --git a/packages/react/src/sticky/index.tsx b/packages/react/src/sticky/index.tsx index 3cc87661..ce1f1d34 100755 --- a/packages/react/src/sticky/index.tsx +++ b/packages/react/src/sticky/index.tsx @@ -1,3 +1,4 @@ import Sticky from './sticky'; export default Sticky; +export type * from './types'; diff --git a/packages/react/src/sticky/sticky.tsx b/packages/react/src/sticky/sticky.tsx index 5df1b56f..4661258d 100755 --- a/packages/react/src/sticky/sticky.tsx +++ b/packages/react/src/sticky/sticky.tsx @@ -1,6 +1,7 @@ import { CSSProperties, useCallback, useContext, useEffect, useRef, useState } 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 { getRect, getScroll, getNodeHeight } from '../_utils/dom'; import { StickyProps } from './types'; @@ -9,7 +10,7 @@ const Sticky = (props: StickyProps): JSX.Element => { const { offsetTop, offsetBottom, - container = () => window, + container, onChange, className, style, @@ -24,7 +25,11 @@ const Sticky = (props: StickyProps): JSX.Element => { const stickyRef = useRef(null); const [stickyStyle, setStickyStyle] = useState({}); const [placeholderStyle, setPlaceholderStyle] = useState({}); - const [stickyContainer, setStickyContainer] = useState(container()); + const resolvedContainer = useCallback( + () => resolveTargetContainer(configContext, container), + [configContext, container] + ); + const [stickyContainer, setStickyContainer] = useState(resolvedContainer()); const getStickyMode = () => { const mode = { @@ -134,20 +139,39 @@ const Sticky = (props: StickyProps): JSX.Element => { * If the container is changed, update the listeners */ useEffect(() => { - const stickyContainer = container(); + const stickyContainer = resolvedContainer(); if (!stickyContainer) { return; } setStickyContainer(stickyContainer); + const placeholderNode = placeholderRef.current; + const stickyNode = stickyRef.current; + const resizeObserver = + typeof ResizeObserver !== 'undefined' + ? new ResizeObserver(() => { + updateNodePosition(); + }) + : null; + stickyContainer.addEventListener('scroll', updateNodePosition); - stickyContainer.addEventListener('resize', updateNodePosition); + window.addEventListener('resize', updateNodePosition); + + if (resizeObserver) { + placeholderNode && resizeObserver.observe(placeholderNode); + stickyNode && resizeObserver.observe(stickyNode); + + if (stickyContainer !== window) { + resizeObserver.observe(stickyContainer as HTMLElement); + } + } return () => { stickyContainer.removeEventListener('scroll', updateNodePosition); - stickyContainer.removeEventListener('resize', updateNodePosition); + window.removeEventListener('resize', updateNodePosition); + resizeObserver?.disconnect(); }; - }, [updateNodePosition, container]); + }, [updateNodePosition, resolvedContainer]); useEffect(() => { updateNodePosition(); diff --git a/packages/react/src/strength-indicator/index.tsx b/packages/react/src/strength-indicator/index.tsx index 476be6a9..836a6384 100755 --- a/packages/react/src/strength-indicator/index.tsx +++ b/packages/react/src/strength-indicator/index.tsx @@ -1,3 +1,4 @@ import StrengthIndicator from './strength-indicator'; export default StrengthIndicator; +export type * from './types'; diff --git a/packages/react/src/switch/index.tsx b/packages/react/src/switch/index.tsx index ce9b367c..eeb1fb9e 100755 --- a/packages/react/src/switch/index.tsx +++ b/packages/react/src/switch/index.tsx @@ -1,3 +1,4 @@ import Switch from './switch'; export default Switch; +export type * from './types'; diff --git a/packages/react/src/table/index.tsx b/packages/react/src/table/index.tsx index 434070a4..15d7a27c 100644 --- a/packages/react/src/table/index.tsx +++ b/packages/react/src/table/index.tsx @@ -1,3 +1,4 @@ import Table from './table'; export default Table; +export type * from './types'; 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/tabs/index.tsx b/packages/react/src/tabs/index.tsx index 08be5dea..a79c3a43 100755 --- a/packages/react/src/tabs/index.tsx +++ b/packages/react/src/tabs/index.tsx @@ -1,7 +1,7 @@ import Tabs from './tabs'; import TabPanel from './tab-panel'; -export type { TabsProps, TabItem, TabType, TabPosition, TabPanelProps } from './types'; +export type * from './types'; type ITabs = typeof Tabs & { Panel: typeof TabPanel; diff --git a/packages/react/src/tag/index.tsx b/packages/react/src/tag/index.tsx index 551475b7..882953f1 100755 --- a/packages/react/src/tag/index.tsx +++ b/packages/react/src/tag/index.tsx @@ -9,3 +9,4 @@ const DefaultTag = Tag as ITag; DefaultTag.CheckableTag = CheckableTag; export default DefaultTag; +export type * from './types'; 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/text-loop/index.tsx b/packages/react/src/text-loop/index.tsx index c96d905c..61ee10e5 100644 --- a/packages/react/src/text-loop/index.tsx +++ b/packages/react/src/text-loop/index.tsx @@ -1,3 +1,4 @@ import TextLoop from './text-loop'; export default TextLoop; +export type * from './types'; diff --git a/packages/react/src/textarea/index.tsx b/packages/react/src/textarea/index.tsx index b7ef4161..d0a12de8 100755 --- a/packages/react/src/textarea/index.tsx +++ b/packages/react/src/textarea/index.tsx @@ -1,3 +1,4 @@ import Textarea from './textarea'; export default Textarea; +export type * from './types'; diff --git a/packages/react/src/time-picker/index.tsx b/packages/react/src/time-picker/index.tsx index 314a6cfe..b1704e0b 100755 --- a/packages/react/src/time-picker/index.tsx +++ b/packages/react/src/time-picker/index.tsx @@ -1,6 +1,6 @@ import TimePicker from './time-picker'; -export type { TimePickerProps, DisabledTime } from './types'; +export type * from './types'; export type { TimePanelProps } from './time-panel'; export default TimePicker; diff --git a/packages/react/src/timeline/index.tsx b/packages/react/src/timeline/index.tsx index c32d4aba..9e190701 100755 --- a/packages/react/src/timeline/index.tsx +++ b/packages/react/src/timeline/index.tsx @@ -9,3 +9,4 @@ const DefaultTimeline = Timeline as ITimeline; DefaultTimeline.Item = TimelineItem; export default DefaultTimeline; +export type * from './types'; diff --git a/packages/react/src/tooltip/index.tsx b/packages/react/src/tooltip/index.tsx index 88ed4419..51229233 100755 --- a/packages/react/src/tooltip/index.tsx +++ b/packages/react/src/tooltip/index.tsx @@ -1,3 +1,4 @@ import Tooltip from './tooltip'; export default Tooltip; +export type * from './types'; diff --git a/packages/react/src/tooltip/style/_index.scss b/packages/react/src/tooltip/style/_index.scss index 088d584b..b88a2140 100755 --- a/packages/react/src/tooltip/style/_index.scss +++ b/packages/react/src/tooltip/style/_index.scss @@ -1,8 +1,7 @@ -@use 'sass:math'; @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; @@ -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/react/src/tour/__tests__/tour.test.tsx b/packages/react/src/tour/__tests__/tour.test.tsx index 52f278be..c61359fd 100644 --- a/packages/react/src/tour/__tests__/tour.test.tsx +++ b/packages/react/src/tour/__tests__/tour.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import Tour from '../index'; import { TourStepProps } from '../types'; +import ConfigProvider from '../../config-provider'; const steps: TourStepProps[] = [ { title: 'Step 1', description: 'Description 1' }, @@ -121,4 +122,23 @@ describe('', () => { ); expect(getByText('1/3')).toBeInTheDocument(); }); + + it('should lock and restore the configured target container', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const { unmount } = render( + container}> + + + ); + + expect(container.style.overflow).toBe('hidden'); + expect(document.body.style.overflow).toBe(''); + + unmount(); + + expect(container.style.overflow).toBe(''); + document.body.removeChild(container); + }); }); diff --git a/packages/react/src/tour/index.tsx b/packages/react/src/tour/index.tsx index f7c369f6..5bdf9944 100644 --- a/packages/react/src/tour/index.tsx +++ b/packages/react/src/tour/index.tsx @@ -1,4 +1,4 @@ import Tour from './tour'; export default Tour; -export type { TourProps, TourStepProps } from './types'; +export type * from './types'; 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/tour/tour.tsx b/packages/react/src/tour/tour.tsx index 24be4636..244e763f 100644 --- a/packages/react/src/tour/tour.tsx +++ b/packages/react/src/tour/tour.tsx @@ -4,6 +4,8 @@ import { createPopper, Instance } from '@popperjs/core'; import Portal from '../portal'; import Transition from '../transition'; import { ConfigContext } from '../config-provider/config-context'; +import { resolveTargetContainer } from '../config-provider/container-utils'; +import { acquireScrollLock } from '../config-provider/scroll-lock'; import { getPrefixCls } from '../_utils/general'; import { useLocale } from '../_utils/use-locale'; import { Placement } from '../popup/types'; @@ -149,17 +151,18 @@ const Tour = React.forwardRef((props, ref) => { useEffect(() => { if (!open) return undefined; + const targetContainer = resolveTargetContainer(configContext); const handleUpdate = () => { updateTargetRect(); popperRef.current?.update(); }; - window.addEventListener('scroll', handleUpdate, true); + targetContainer.addEventListener('scroll', handleUpdate, true); window.addEventListener('resize', handleUpdate); return () => { - window.removeEventListener('scroll', handleUpdate, true); + targetContainer.removeEventListener('scroll', handleUpdate, true); window.removeEventListener('resize', handleUpdate); }; - }, [open, updateTargetRect]); + }, [configContext, open, updateTargetRect]); const handleTransitionExited = useCallback(() => { destroyPopper(); @@ -185,12 +188,8 @@ const Tour = React.forwardRef((props, ref) => { // Scroll lock useEffect(() => { if (!open) return undefined; - const prev = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = prev; - }; - }, [open]); + return acquireScrollLock(resolveTargetContainer(configContext)); + }, [configContext, open]); // Keyboard navigation useEffect(() => { diff --git a/packages/react/src/transfer/index.tsx b/packages/react/src/transfer/index.tsx index f8161a4c..ab6df62a 100755 --- a/packages/react/src/transfer/index.tsx +++ b/packages/react/src/transfer/index.tsx @@ -1,3 +1,4 @@ import Transfer from './transfer'; export default Transfer; +export type * from './types'; diff --git a/packages/react/src/tree/index.tsx b/packages/react/src/tree/index.tsx index 1ec50edf..cb9b8ce5 100755 --- a/packages/react/src/tree/index.tsx +++ b/packages/react/src/tree/index.tsx @@ -1,3 +1,4 @@ import Tree from './tree'; export default Tree; +export type * from './types'; 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/index.tsx b/packages/react/src/typography/index.tsx index e3a06487..ed26a4b2 100755 --- a/packages/react/src/typography/index.tsx +++ b/packages/react/src/typography/index.tsx @@ -15,3 +15,4 @@ DefaultTypo.Paragraph = Paragraph; DefaultTypo.Text = Text; export default DefaultTypo; +export type * from './types'; 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/react/src/upload/index.tsx b/packages/react/src/upload/index.tsx index 4478a8fc..83639821 100755 --- a/packages/react/src/upload/index.tsx +++ b/packages/react/src/upload/index.tsx @@ -1,3 +1,4 @@ import Upload from './upload'; export default Upload; +export type * from './types'; diff --git a/packages/react/src/waterfall/index.tsx b/packages/react/src/waterfall/index.tsx index 64002311..4d83e5b8 100644 --- a/packages/react/src/waterfall/index.tsx +++ b/packages/react/src/waterfall/index.tsx @@ -1,5 +1,5 @@ import Waterfall from './waterfall'; -export type { WaterfallProps, WaterfallItem, Breakpoint } from './types'; +export type * from './types'; export default Waterfall; 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 deleted file mode 100644 index e6201338..00000000 --- a/packages/tokens/css/base.css +++ /dev/null @@ -1,1310 +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-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-border: #fff; - --ty-avatar-presence-shadow: 0 0 0 0.1rem #fff; - --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; -} - -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-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-border: #1f1f1f; - --ty-avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f; - --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; -} - -@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-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-border: #1f1f1f; - --ty-avatar-presence-shadow: 0 0 0 0.1rem #1f1f1f; - --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; - } -} - -/* 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: 500; - line-height: 1.2; -} - -h1 { - font-size: 2.5rem; -} - -h2 { - font-size: 2rem; -} - -h3 { - font-size: 1.75rem; -} - -h4 { - font-size: 1.5rem; -} - -h5 { - font-size: 1.25rem; -} - -h6 { - font-size: 1rem; -} - -/* 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 diff --git a/packages/tokens/scss/_constants.scss b/packages/tokens/scss/_constants.scss new file mode 100644 index 00000000..6f793b09 --- /dev/null +++ b/packages/tokens/scss/_constants.scss @@ -0,0 +1,82 @@ +// 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: var(--ty-alert-border-radius) !default; + +// Avatar +$avatar-border-radius: var(--ty-avatar-border-radius) !default; + +// Badge +$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: 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: 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: 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: 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: var(--ty-menu-item-padding-vertical) !default; + +// Native Select +$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: var(--ty-notification-width) !default; +$notification-margin: var(--ty-notification-margin) !default; + +// Popover +$popover-arrow-size: var(--ty-popover-arrow-size) !default; + +// Select +$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: var(--ty-slider-size) !default; +$slider-track-size: var(--ty-slider-track-size) !default; + +// Steps +$steps-title-font-size: var(--ty-steps-title-font-size) !default; + +// Strength Indicator +$strength-indicator-border-radius: var(--ty-strength-indicator-border-radius) !default; + +// Switch +$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: var(--ty-textarea-padding) !default; + +// Tooltip +$tooltip-arrow-size: var(--ty-tooltip-arrow-size) !default; +$tooltip-content-padding: var(--ty-tooltip-content-padding) !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/_theme.scss b/packages/tokens/scss/_theme.scss index 8237e920..aefa4c64 100644 --- a/packages/tokens/scss/_theme.scss +++ b/packages/tokens/scss/_theme.scss @@ -14,13 +14,13 @@ } // Dark theme (explicit) -html[data-tiny-theme='dark'] { +[data-tiny-theme='dark'] { @include generate-theme-vars($dark-theme); } // System preference (opt-in via data-tiny-theme="system") @media (prefers-color-scheme: dark) { - html[data-tiny-theme='system'] { + [data-tiny-theme='system'] { @include generate-theme-vars($dark-theme); } } 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..6df5a485 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), @@ -386,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, @@ -414,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 29d47dcd..27016f60 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), @@ -386,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, @@ -414,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, );