diff --git a/apps/docs/src/containers/theme-studio/editor-fields.tsx b/apps/docs/src/containers/theme-studio/editor-fields.tsx index 008d82fb..b8c06040 100644 --- a/apps/docs/src/containers/theme-studio/editor-fields.tsx +++ b/apps/docs/src/containers/theme-studio/editor-fields.tsx @@ -10,22 +10,30 @@ export function swatchTextStyle(background: string, foreground: string): React.C } function parseSliderValue(value: string, unit?: 'px' | 'em' | 'rem'): number | undefined { + const trimmed = value.trim(); + + // CSS zero is unit-agnostic. Presets may serialize it as `0px` while the + // editor slider expects `rem`, so normalize all zero forms to numeric 0. + if (/^-?0(?:\.0+)?(?:px|em|rem)?$/.test(trimmed)) { + return 0; + } + if (unit === 'px') { - const match = /^(-?\d+(?:\.\d+)?)px$/.exec(value.trim()); + const match = /^(-?\d+(?:\.\d+)?)px$/.exec(trimmed); return match ? Number(match[1]) : undefined; } if (unit === 'em') { - const match = /^(-?\d+(?:\.\d+)?)em$/.exec(value.trim()); + const match = /^(-?\d+(?:\.\d+)?)em$/.exec(trimmed); return match ? Number(match[1]) : undefined; } if (unit === 'rem') { - const match = /^(-?\d+(?:\.\d+)?)rem$/.exec(value.trim()); + const match = /^(-?\d+(?:\.\d+)?)rem$/.exec(trimmed); return match ? Number(match[1]) : undefined; } - const parsed = Number(value); + const parsed = Number(trimmed); return Number.isNaN(parsed) ? undefined : parsed; } diff --git a/apps/docs/src/containers/theme-studio/theme-document-adapter.ts b/apps/docs/src/containers/theme-studio/theme-document-adapter.ts index 26a99ba8..78c44e46 100644 --- a/apps/docs/src/containers/theme-studio/theme-document-adapter.ts +++ b/apps/docs/src/containers/theme-studio/theme-document-adapter.ts @@ -245,9 +245,21 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'cascader.height.sm': fields.fieldHeightSm, 'cascader.height.md': fields.fieldHeightMd, 'cascader.height.lg': fields.fieldHeightLg, + 'cascader.bg': fields.base, + 'cascader.border': fields.input, + 'cascader.border-hover': fields.ring, + 'cascader.border-focus': fields.ring, + 'cascader.shadow-focus': fields.shadowFocus, + 'cascader.radius': fields.inputRadius, 'cascader.padding.sm': `0 calc(${fields.fieldPaddingSm} + 20px) 0 ${fields.fieldPaddingSm}`, 'cascader.padding.md': `0 calc(${fields.fieldPaddingMd} + 20px) 0 ${fields.fieldPaddingMd}`, 'cascader.padding.lg': `0 calc(${fields.fieldPaddingLg} + 20px) 0 ${fields.fieldPaddingLg}`, + 'native-select.bg': fields.base, + 'native-select.border': fields.input, + 'native-select.border-hover': fields.ring, + 'native-select.border-focus': fields.ring, + 'native-select.shadow-focus': fields.shadowFocus, + 'native-select.radius': fields.inputRadius, 'checkbox.bg': fields.base, 'checkbox.border': fields.input, 'checkbox.border.hover': fields.ring, diff --git a/packages/react/src/cascader/style/index.scss b/packages/react/src/cascader/style/index.scss index 589ea7f1..2de773b2 100644 --- a/packages/react/src/cascader/style/index.scss +++ b/packages/react/src/cascader/style/index.scss @@ -3,7 +3,8 @@ .#{$prefix}-cascader { position: relative; display: inline-block; - min-width: var(--ty-cascader-min-width); + width: 100%; + min-width: 0; &_disabled { opacity: var(--ty-cascader-opacity-disabled); diff --git a/packages/react/src/date-picker/style/index.scss b/packages/react/src/date-picker/style/index.scss index 6b7c1c42..1ecb5b74 100755 --- a/packages/react/src/date-picker/style/index.scss +++ b/packages/react/src/date-picker/style/index.scss @@ -5,6 +5,7 @@ $dp: #{$prefix}-date-picker; .#{$dp} { display: inline-flex; position: relative; + width: 100%; font-size: var(--ty-picker-input-font-size); // ---- Input ---- diff --git a/packages/react/src/grid/demo/AlignmentGrid.tsx b/packages/react/src/grid/demo/AlignmentGrid.tsx index 083029aa..49592921 100644 --- a/packages/react/src/grid/demo/AlignmentGrid.tsx +++ b/packages/react/src/grid/demo/AlignmentGrid.tsx @@ -1,28 +1,72 @@ import React from 'react'; -import { Button, Grid } from '@tiny-design/react'; +import { Grid, Radio } from '@tiny-design/react'; +import { DemoControlLabel, DemoControls, getDemoBlockStyle } from './shared'; + +type AlignmentValue = 'start' | 'center' | 'end' | 'stretch'; export default function AlignmentGridDemo() { + const [justify, setJustify] = React.useState('stretch'); + const [align, setAlign] = React.useState('stretch'); + return ( - - - - - +
+ +
+ Justify + setJustify(val as AlignmentValue)}> + start + center + end + stretch + +
+
+ Align + setAlign(val as AlignmentValue)}> + start + center + end + stretch + +
+
+ +
+ {justify} +
+
+ {align} +
+
+ items +
+
+
); } diff --git a/packages/react/src/grid/demo/AutoFit.tsx b/packages/react/src/grid/demo/AutoFit.tsx index 44f3495d..d8143852 100644 --- a/packages/react/src/grid/demo/AutoFit.tsx +++ b/packages/react/src/grid/demo/AutoFit.tsx @@ -1,17 +1,72 @@ import React from 'react'; -import { Card, Grid } from '@tiny-design/react'; +import { Checkbox, Grid, Slider } from '@tiny-design/react'; +import type { SliderValue } from '@tiny-design/react'; +import { DemoBlock, DemoControlLabel, DemoControls } from './shared'; export default function AutoFitDemo() { + const [minColumnWidth, setMinColumnWidth] = React.useState(180); + const [autoFit, setAutoFit] = React.useState(true); + const [count, setCount] = React.useState(5); + return ( - - {['Analytics', 'Revenue', 'Orders', 'Retention', 'Conversion'].map((title) => ( - - - {title} -
Auto-fit cards without manual breakpoints.
-
-
- ))} -
+
+ +
+ Min column width: {minColumnWidth}px + { + if (typeof val === 'number') { + setMinColumnWidth(val); + } + }} + /> +
+
+ Item count: {count} + { + if (typeof val === 'number') { + setCount(val); + } + }} + /> +
+
+ ) => setAutoFit(e.currentTarget.checked)}> + Use auto-fit + +
+
+ + {[ + ['Analytics', `${autoFit ? 'auto-fit' : 'auto-fill'} / ${minColumnWidth}px`], + ['Revenue', 'responsive tracks'], + ['Orders', 'reflow'], + ['Retention', 'no breakpoints'], + ['Conversion', 'fluid blocks'], + ['Traffic', 'auto placement'], + ['Pipeline', 'repeat tracks'], + ['Forecast', 'adaptive cells'], + ].slice(0, count).map(([title, detail], index) => ( + + ))} + +
); } diff --git a/packages/react/src/grid/demo/DashboardShell.tsx b/packages/react/src/grid/demo/DashboardShell.tsx index 7d6dd81e..c521e66f 100644 --- a/packages/react/src/grid/demo/DashboardShell.tsx +++ b/packages/react/src/grid/demo/DashboardShell.tsx @@ -1,198 +1,100 @@ import React from 'react'; -import { Button, Card, Divider, Grid, Progress, Tag, Text } from '@tiny-design/react'; - -const shellCardStyle: React.CSSProperties = { - padding: 16, - minHeight: 96, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', -}; - -const metricStyle: React.CSSProperties = { - ...shellCardStyle, - minHeight: 144, - background: - 'linear-gradient(180deg, color-mix(in srgb, var(--ty-color-primary) 10%, transparent), color-mix(in srgb, var(--ty-color-primary-bg) 70%, transparent))', -}; - -const sectionLabelStyle: React.CSSProperties = { - display: 'inline-flex', - alignItems: 'center', - gap: 6, -}; +import { Checkbox, Grid, Slider } from '@tiny-design/react'; +import type { SliderValue } from '@tiny-design/react'; +import { DemoBlock, DemoControlLabel, DemoControls, getDemoBlockStyle } from './shared'; export default function DashboardShellDemo() { + const [gap, setGap] = React.useState(16); + const [showFilters, setShowFilters] = React.useState(true); + return ( - - - -
-
- Dashboard Header -
- Top-level shell area using `grid-template-areas`. -
-
- -
-
- - Healthy - - - 24h window - - - 3 alerts - -
-
-
+
+ +
+ Desktop gap: {gap}px + { + if (typeof val === 'number') { + setGap(val); + } + }} + /> +
+
+ ) => setShowFilters(e.currentTarget.checked)}> + Show filters area + +
+
+ + + + - - - - -
- Revenue - - +12% - -
-
- $182,400 -
- -
-
-
-
- - -
- Conversion - - +1.8% - -
-
- 18.4% -
- -
-
-
-
-
-
+ + + + + + + + + + - - - Filters -
- Region: APAC - Plan: Pro - Channel: Web -
-
-
+ {showFilters ? ( + + + + ) : null} - - -
- Chart Area - Last 7 days + +
+ Chart + + {[38, 60, 50, 82, 56, 72].map((height, index) => ( +
+ ))} + + wide area spanning two columns
- -
-
-
-
-
-
- - Wide content region spanning two columns on desktop. - - + - - - Activity -
-
- New signups - 128 -
- -
- Trial started - 42 -
- -
- Churn risk - 9 -
-
-
-
- + + + + +
); } diff --git a/packages/react/src/grid/demo/ExplicitColumns.tsx b/packages/react/src/grid/demo/ExplicitColumns.tsx index af1d27b3..0f67175f 100644 --- a/packages/react/src/grid/demo/ExplicitColumns.tsx +++ b/packages/react/src/grid/demo/ExplicitColumns.tsx @@ -1,30 +1,49 @@ import React from 'react'; -import { Card, Grid, Text } from '@tiny-design/react'; - -const panelStyle: React.CSSProperties = { - minHeight: 104, - padding: 16, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - background: 'linear-gradient(180deg, color-mix(in srgb, var(--ty-color-primary) 12%, transparent), color-mix(in srgb, var(--ty-color-primary-bg) 70%, transparent))', -}; +import { Grid, Slider } from '@tiny-design/react'; +import type { SliderValue } from '@tiny-design/react'; +import { DemoBlock, DemoControlLabel, DemoControls } from './shared'; export default function ExplicitColumnsDemo() { + const [sidebarWidth, setSidebarWidth] = React.useState(220); + const [inspectorWidth, setInspectorWidth] = React.useState(180); + return ( - - - Sidebar - Fixed 220px track - - - Main content - Fluid `minmax(0, 1fr)` track - - - Inspector - Fixed 180px track - - +
+ +
+ Sidebar: {sidebarWidth}px + { + if (typeof val === 'number') { + setSidebarWidth(val); + } + }} + /> +
+
+ Inspector: {inspectorWidth}px + { + if (typeof val === 'number') { + setInspectorWidth(val); + } + }} + /> +
+
+ + + + + +
); } diff --git a/packages/react/src/grid/demo/NamedAreas.tsx b/packages/react/src/grid/demo/NamedAreas.tsx index 09796f96..1f899ecb 100644 --- a/packages/react/src/grid/demo/NamedAreas.tsx +++ b/packages/react/src/grid/demo/NamedAreas.tsx @@ -1,41 +1,59 @@ import React from 'react'; -import { Card, Grid, Text } from '@tiny-design/react'; - -const panelStyle: React.CSSProperties = { - padding: 16, - minHeight: 96, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', -}; +import { Grid, Radio } from '@tiny-design/react'; +import { DemoBlock, DemoControlLabel, DemoControls } from './shared'; export default function NamedAreasDemo() { - return ( - ('right-rail'); + + const templates = { + 'right-rail': { + columns: { xs: 1, md: 3 }, + areas: { xs: ['hero', 'side', 'content'], md: ['hero hero side', 'content content side'], - }} - columns={{ xs: 1, md: 3 }} - gap="md"> - - - Hero - grid-area: hero - - - - - Sidebar - grid-area: side - - - - - Content - grid-area: content - - - + }, + }, + 'header-band': { + columns: { xs: 1, md: 3 }, + areas: { + xs: ['hero', 'content', 'side'], + md: ['hero hero hero', 'content content side'], + }, + }, + stacked: { + columns: { xs: 1, md: 2 }, + areas: { + xs: ['hero', 'content', 'side'], + md: ['hero hero', 'content content', 'side side'], + }, + }, + } as const; + + const current = templates[layout]; + + return ( +
+ +
+ Area template + setLayout(val as 'right-rail' | 'header-band' | 'stacked')}> + right-rail + header-band + stacked + +
+
+ + + + + + + + + + + +
); } diff --git a/packages/react/src/grid/demo/OffsetAuto.tsx b/packages/react/src/grid/demo/OffsetAuto.tsx index 7a869933..3d940c57 100644 --- a/packages/react/src/grid/demo/OffsetAuto.tsx +++ b/packages/react/src/grid/demo/OffsetAuto.tsx @@ -1,30 +1,64 @@ import React from 'react'; -import { Card, Grid, Text } from '@tiny-design/react'; - -const Item = ({ - title, - desc, -}: { - title: string; - desc: string; -}) => ( - - {title} -
- {desc} -
-
-); +import { Checkbox, Grid, Slider } from '@tiny-design/react'; +import type { SliderValue } from '@tiny-design/react'; +import { DemoBlock, DemoControlLabel, DemoControls } from './shared'; export default function OffsetAutoDemo() { + const [leftSize, setLeftSize] = React.useState(3); + const [rightSize, setRightSize] = React.useState(3); + const [useAutoOffset, setUseAutoOffset] = React.useState(true); + return ( - - - - - - - - +
+ +
+ Left size: {leftSize} + { + if (typeof val === 'number') { + setLeftSize(val); + } + }} + /> +
+
+ Right size: {rightSize} + { + if (typeof val === 'number') { + setRightSize(val); + } + }} + /> +
+
+ ) => setUseAutoOffset(e.currentTarget.checked)}> + Use `offset="auto"` + +
+
+ + + + + + + + +
); } diff --git a/packages/react/src/grid/demo/ResponsiveLayout.tsx b/packages/react/src/grid/demo/ResponsiveLayout.tsx index 57f0a6ca..709c97d9 100644 --- a/packages/react/src/grid/demo/ResponsiveLayout.tsx +++ b/packages/react/src/grid/demo/ResponsiveLayout.tsx @@ -1,46 +1,81 @@ import React from 'react'; -import { Card, Grid, Text } from '@tiny-design/react'; - -const Item = ({ - title, - desc, - minHeight = 88, -}: { - title: string; - desc: string; - minHeight?: number; -}) => ( - - {title} - {desc} - -); +import { Grid, Radio, Slider } from '@tiny-design/react'; +import type { SliderValue } from '@tiny-design/react'; +import { DemoBlock, DemoControlLabel, DemoControls } from './shared'; export default function ResponsiveLayoutDemo() { + const [preset, setPreset] = React.useState<'balanced' | 'hero' | 'sidebar'>('hero'); + const [desktopGap, setDesktopGap] = React.useState(24); + + const presets = { + balanced: { + hero: 6, + sidebar: 6, + cardA: 3, + cardB: 3, + content: 6, + }, + hero: { + hero: 8, + sidebar: 4, + cardA: 3, + cardB: 3, + content: 6, + }, + sidebar: { + hero: 7, + sidebar: 5, + cardA: 4, + cardB: 4, + content: 4, + }, + } as const; + + const current = presets[preset]; + return ( - - - - - - - - - - - - - - - - - +
+ +
+ Desktop layout + setPreset(val as 'balanced' | 'hero' | 'sidebar')}> + hero-first + balanced + sidebar-heavy + +
+
+ Desktop gap: {desktopGap}px + { + if (typeof val === 'number') { + setDesktopGap(val); + } + }} + /> +
+
+ + + + + + + + + + + + + + + + + +
); } diff --git a/packages/react/src/grid/demo/shared.tsx b/packages/react/src/grid/demo/shared.tsx new file mode 100644 index 00000000..97788ae7 --- /dev/null +++ b/packages/react/src/grid/demo/shared.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +const toneMix = { + strong: 96, + base: 88, + soft: 76, + subtle: 64, +} as const; + +type DemoTone = keyof typeof toneMix; + +export function getDemoBlockStyle( + tone: DemoTone = 'base', + minHeight = 88, + extraStyle?: React.CSSProperties, +): React.CSSProperties { + return { + minHeight, + padding: 16, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + gap: 8, + color: '#fff', + background: `color-mix(in srgb, var(--ty-color-primary) ${toneMix[tone]}%, transparent)`, + ...extraStyle, + }; +} + +export function DemoBlock({ + title, + detail, + tone = 'base', + minHeight = 88, + style, +}: { + title: string; + detail?: string; + tone?: DemoTone; + minHeight?: number; + style?: React.CSSProperties; +}) { + return ( +
+ {title} + {detail ? {detail} : null} +
+ ); +} + +export function DemoControls({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); +} + +export function DemoControlLabel({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/packages/react/src/grid/index.md b/packages/react/src/grid/index.md index 337b5172..55c8b12a 100644 --- a/packages/react/src/grid/index.md +++ b/packages/react/src/grid/index.md @@ -28,9 +28,9 @@ import { Grid } from 'tiny-design'; Grid is a two-dimensional layout component built on CSS Grid. It excels when you need control over both rows **and** columns simultaneously. - **Dashboard shells** — define named `areas` like `"sidebar header" "sidebar main"` for application-level page structure. -- **Card walls / galleries** — use `minColumnWidth` with `autoFit` to create responsive card grids that reflow without manual breakpoints. +- **Adaptive tile layouts** — use `minColumnWidth` with `autoFit` to create responsive blocks that reflow without manual breakpoints. - **Data-dense UIs** — use explicit `columns` and `rows` to build spreadsheet-like or calendar-like layouts where items span multiple rows or columns. -- **Asymmetric layouts** — use `Grid.Item colSpan` and `rowSpan` to create hero sections or featured cards that span across tracks. +- **Asymmetric layouts** — use `Grid.Item colSpan` and `rowSpan` to create prominent regions that span across tracks. - **Responsive rearrangement** — pass responsive objects to `columns`, `gap`, and `areas` to radically change the layout across breakpoints (e.g. stacking sidebar below content on mobile). ### Grid vs Grid System (Row / Col) @@ -39,7 +39,7 @@ Grid is a two-dimensional layout component built on CSS Grid. It excels when you |---|---|---| | CSS technology | CSS Grid | Flexbox + 24-column math | | Dimensions | Two-dimensional (rows + columns) | One-dimensional (columns only) | -| Best for | Dashboards, card walls, named areas, row spanning | Classic page columns, form layouts, marketing pages | +| Best for | Dashboards, adaptive blocks, named areas, row spanning | Classic page columns, form layouts, marketing pages | | Responsive | Responsive props on any value | Breakpoint props (`xs`–`xxl`) on Col | - If you are migrating from **MUI Grid**, start with `spacing`, `rowSpacing`, `columnSpacing`, `size`, and `offset`, then adopt `areas` or `rowSpan` when you need layouts that flex grids cannot express. @@ -50,7 +50,7 @@ Grid is a two-dimensional layout component built on CSS Grid. It excels when you ### Explicit Columns -Define tracks directly with `columns`. +Define fixed and fluid tracks directly with `columns`. @@ -59,7 +59,7 @@ Define tracks directly with `columns`. ### Auto Fit -Use `minColumnWidth` to create responsive cards without manual breakpoints. +Use `minColumnWidth` to create responsive blocks without manual breakpoints. @@ -77,7 +77,7 @@ Use `justify` and `align` to control placement inside each cell. ### Responsive Layout -Use responsive `columns`, `gap`, and `Grid.Item size` values, similar to MUI's breakpoint-driven grid. +Use responsive `columns`, `gap`, and `Grid.Item size` values to show how spans change across breakpoints. @@ -86,7 +86,7 @@ Use responsive `columns`, `gap`, and `Grid.Item size` values, similar to MUI's b ### Named Areas -Use `areas` and `Grid.Item area` for dashboard-style shells that MUI's flex grid cannot express directly. +Use `areas` and `Grid.Item area` to make region relationships visible at a glance. @@ -104,7 +104,7 @@ Use `offset="auto"` to push an item to the far right, similar to MUI Grid. ### Dashboard Shell -Use `areas` plus nested `Grid` blocks to build an application shell instead of a simple stacked demo. +Use `areas` plus nested `Grid` blocks to demonstrate shell structure with clear block regions. diff --git a/packages/react/src/grid/index.zh_CN.md b/packages/react/src/grid/index.zh_CN.md index bbd1fa69..cf48251d 100644 --- a/packages/react/src/grid/index.zh_CN.md +++ b/packages/react/src/grid/index.zh_CN.md @@ -28,9 +28,9 @@ import { Grid } from 'tiny-design'; Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和**列的场景。 - **Dashboard 壳层** — 使用 `areas` 定义命名区域,如 `"sidebar header" "sidebar main"`,构建应用级页面骨架。 -- **卡片墙 / 图库** — 使用 `minColumnWidth` 配合 `autoFit`,创建无需手动断点即可自动回流的响应式卡片网格。 +- **自适应块布局** — 使用 `minColumnWidth` 配合 `autoFit`,创建无需手动断点即可自动回流的响应式区块网格。 - **数据密集型界面** — 使用显式 `columns` 和 `rows`,构建类似表格或日历的布局,子项可跨越多行多列。 -- **非对称布局** — 使用 `Grid.Item` 的 `colSpan` 和 `rowSpan`,创建跨轨道的 Hero 区域或特色卡片。 +- **非对称布局** — 使用 `Grid.Item` 的 `colSpan` 和 `rowSpan`,创建跨轨道的重点区域。 - **响应式重排** — 向 `columns`、`gap`、`areas` 传入响应式对象,在不同断点下彻底改变布局(如移动端将侧边栏堆叠到内容下方)。 ### Grid 与 Grid System(Row / Col)对比 @@ -39,7 +39,7 @@ Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和 |---|---|---| | CSS 技术 | CSS Grid | Flexbox + 24 栏数学计算 | | 布局维度 | 二维(行 + 列) | 一维(仅列) | -| 最佳场景 | Dashboard、卡片墙、命名区域、跨行布局 | 经典页面分栏、表单布局、营销页 | +| 最佳场景 | Dashboard、自适应区块、命名区域、跨行布局 | 经典页面分栏、表单布局、营销页 | | 响应式方式 | 任意属性均支持响应式对象 | Col 上的断点属性(`xs`–`xxl`) | - 如果你是从 **MUI Grid** 迁移过来,可以先使用 `spacing`、`rowSpacing`、`columnSpacing`、`size`、`offset`,再逐步使用 `areas` 和 `rowSpan` 这些更强的 CSS Grid 能力。 @@ -50,7 +50,7 @@ Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和 ### 显式列定义 -使用 `columns` 直接定义网格轨道。 +使用 `columns` 直接定义固定轨道和弹性轨道。 @@ -59,7 +59,7 @@ Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和 ### 自动适配列 -使用 `minColumnWidth` 创建无需手动断点的自适应卡片布局。 +使用 `minColumnWidth` 创建无需手动断点的自适应区块布局。 @@ -77,7 +77,7 @@ Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和 ### 响应式布局 -像 MUI Grid 一样使用断点驱动的 `columns`、`gap` 和 `Grid.Item size`。 +使用断点驱动的 `columns`、`gap` 和 `Grid.Item size`,展示不同屏宽下的跨度变化。 @@ -86,7 +86,7 @@ Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和 ### 命名区域 -使用 `areas` 和 `Grid.Item area` 构建 dashboard 壳层,这也是 CSS Grid 相比 MUI Flex Grid 更强的地方。 +使用 `areas` 和 `Grid.Item area`,直接展示不同区域之间的布局关系。 @@ -104,7 +104,7 @@ Grid 是一个基于 CSS Grid 的二维布局组件,适合同时控制行**和 ### Dashboard 壳层 -使用 `areas` 配合嵌套 `Grid` 构建真实应用壳层,而不是简单的堆叠示例。 +使用 `areas` 配合嵌套 `Grid`,用更清晰的区块结构展示壳层布局。 diff --git a/packages/react/src/input-number/__tests__/input-number.test.tsx b/packages/react/src/input-number/__tests__/input-number.test.tsx index 89ccb7db..c97ef0e7 100644 --- a/packages/react/src/input-number/__tests__/input-number.test.tsx +++ b/packages/react/src/input-number/__tests__/input-number.test.tsx @@ -31,4 +31,9 @@ describe('', () => { expect(container.querySelector('.ty-input-number__up')).toBeInTheDocument(); expect(container.querySelector('.ty-input-number__down')).toBeInTheDocument(); }); + + it('should not crash when controlled value is undefined', () => { + const { container } = render(); + expect(container.querySelector('input')).toHaveValue(null); + }); }); diff --git a/packages/react/src/input-number/input-number.tsx b/packages/react/src/input-number/input-number.tsx index a764f7b7..2bde16c8 100755 --- a/packages/react/src/input-number/input-number.tsx +++ b/packages/react/src/input-number/input-number.tsx @@ -13,6 +13,10 @@ const isValid = (val: string | number): boolean => { return !isNaN(+val); }; +const isFiniteNumber = (val: unknown): val is number => { + return typeof val === 'number' && Number.isFinite(val); +}; + const getDecimalPrecision = (num: number): number => { const str = String(num); const dotIndex = str.indexOf('.'); @@ -46,9 +50,13 @@ const InputNumber = React.forwardRef((props, r [`${prefixCls}_always-controls`]: controls, }); const resolvedPrecision = precision ?? Math.max(getDecimalPrecision(step), getDecimalPrecision(defaultValue)); - const [value, setValue] = useState( + const [value, setValue] = useState( 'value' in props ? (props.value as number) : defaultValue ); + const hasNumericValue = isFiniteNumber(value); + const displayValue = hasNumericValue + ? (resolvedPrecision > 0 ? value.toFixed(resolvedPrecision) : String(value)) + : ''; const inputOnChange = (e: React.ChangeEvent): void => { const raw = Number(e.target.value.trim()); @@ -60,7 +68,8 @@ const InputNumber = React.forwardRef((props, r const plusOnClick = (e: MouseEvent): void => { e.stopPropagation(); if (!disabled && isValid(step)) { - const val = toPrecision(+value + +step, resolvedPrecision); + const nextBase = hasNumericValue ? value : 0; + const val = toPrecision(nextBase + +step, resolvedPrecision); if (val <= max) { !('value' in props) && setValue(val); onChange && onChange(val, e); @@ -71,7 +80,8 @@ const InputNumber = React.forwardRef((props, r const minusOnClick = (e: MouseEvent): void => { e.stopPropagation(); if (!disabled && isValid(step)) { - const val = toPrecision(+value - +step, resolvedPrecision); + const nextBase = hasNumericValue ? value : 0; + const val = toPrecision(nextBase - +step, resolvedPrecision); if (val >= min) { !('value' in props) && setValue(val); onChange && onChange(val, e); @@ -88,14 +98,14 @@ const InputNumber = React.forwardRef((props, r 0 ? value.toFixed(resolvedPrecision) : value} + value={displayValue} type="number" className={`${prefixCls}__input`} max={max} min={min} step={step} onChange={inputOnChange} - aria-valuenow={value} + aria-valuenow={hasNumericValue ? value : undefined} aria-valuemax={max} aria-valuemin={min} aria-disabled={disabled} diff --git a/packages/react/src/menu/demo/MegaNavigation.tsx b/packages/react/src/menu/demo/MegaNavigation.tsx new file mode 100644 index 00000000..cdd0d003 --- /dev/null +++ b/packages/react/src/menu/demo/MegaNavigation.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { Menu, Tag } from '@tiny-design/react'; +import './mega-navigation.scss'; + +type FeatureItem = { + key: string; + title: string; + description: string; + badge?: string; +}; + +const componentItems: FeatureItem[] = [ + { + key: 'alert-dialog', + title: 'Alert Dialog', + description: 'A focused modal for urgent decisions that require a clear response.', + }, + { + key: 'hover-card', + title: 'Hover Card', + description: 'Preview linked content, people, or metadata without leaving the page.', + }, + { + key: 'progress', + title: 'Progress', + description: 'Show task, upload, or workflow completion with clear visual feedback.', + badge: 'Updated', + }, + { + key: 'scroll-area', + title: 'Scroll Area', + description: 'Create more intentional scrolling regions for dense panels and inspectors.', + }, + { + key: 'tabs', + title: 'Tabs', + description: 'Split complex views into clear parallel sections for dashboards and settings.', + }, + { + key: 'tooltip', + title: 'Tooltip', + description: 'Add lightweight explanations and hints without interrupting the main flow.', + }, +]; + +const resourceItems: FeatureItem[] = [ + { + key: 'guides', + title: 'Design Guides', + description: 'Patterns for information architecture, interaction semantics, and theme strategy.', + }, + { + key: 'templates', + title: 'Page Templates', + description: 'Ready-made shells for authentication, product consoles, and marketing surfaces.', + badge: 'New', + }, + { + key: 'tokens', + title: 'Token System', + description: 'A shared language for color, radius, elevation, spacing, and typography.', + }, + { + key: 'cli', + title: 'CLI & MCP', + description: 'Scaffolding, theme export, and AI workflow integration for faster delivery.', + }, +]; + +function FeatureCard({ item }: { item: FeatureItem }) { + return ( +
+
+ {item.title} + {item.badge ? ( + + {item.badge} + + ) : null} +
+

{item.description}

+
+ ); +} + +export default function MegaNavigationDemo() { + return ( +
+ + New}> + Installation + Theme Setup + Layout Principles + Design Assets + + + 28}> + {componentItems.map((item) => ( + + + + ))} + + + + {resourceItems.map((item) => ( + + + + ))} + + +
+ ); +} diff --git a/packages/react/src/menu/demo/mega-navigation.scss b/packages/react/src/menu/demo/mega-navigation.scss new file mode 100644 index 00000000..40ecde3c --- /dev/null +++ b/packages/react/src/menu/demo/mega-navigation.scss @@ -0,0 +1,207 @@ +@use "../../style/variables" as *; + +.menu-mega-nav { + --mega-nav-shell-bg: rgb(255 255 255); + --mega-nav-shell-border: rgb(15 23 42 / 8%); + --mega-nav-shell-shadow: 0 8px 24px rgb(15 23 42 / 4%); + --mega-nav-bar-bg: rgb(248 250 252); + --mega-nav-bar-border: rgb(15 23 42 / 6%); + --mega-nav-pill-bg: rgb(241 245 249); + --mega-nav-pill-color: rgb(71 85 105); + --mega-nav-panel-bg: rgb(255 255 255); + --mega-nav-panel-border: rgb(15 23 42 / 8%); + --mega-nav-panel-shadow: 0 16px 40px rgb(15 23 42 / 8%); + --mega-nav-card-bg: rgb(255 255 255); + --mega-nav-card-border: rgb(15 23 42 / 8%); + --mega-nav-card-hover-bg: rgb(248 250 252); + --mega-nav-card-hover-border: rgb(59 130 246 / 22%); + --mega-nav-card-hover-shadow: 0 6px 14px rgb(15 23 42 / 4%); + --mega-nav-title-color: rgb(15 23 42); + --mega-nav-description-color: rgb(100 116 139); + + padding: 18px; + border-radius: 18px; + background: var(--mega-nav-shell-bg); + border: 1px solid var(--mega-nav-shell-border); + box-shadow: var(--mega-nav-shell-shadow); + + &__bar { + align-items: center; + gap: 8px; + min-height: 52px; + padding: 5px; + border: 1px solid var(--mega-nav-bar-border); + border-radius: 14px; + background: var(--mega-nav-bar-bg); + box-shadow: none; + } + + &__bar.#{$prefix}-menu { + border: 0; + } + + &__bar .#{$prefix}-menu-sub__title, + &__bar .#{$prefix}-menu-item { + min-height: 40px; + padding: 0 14px; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + } + + &__bar .#{$prefix}-menu-item__extra { + gap: 6px; + } + + &__nav-pill { + padding: 2px 8px; + border-radius: 999px; + background: var(--mega-nav-pill-bg); + color: var(--mega-nav-pill-color); + font-size: 12px; + font-weight: 600; + line-height: 18px; + } + + &__popup { + --ty-menu-sub-list-popup-min-width: 720px; + --ty-popup-light-bg: var(--mega-nav-panel-bg); + --ty-popup-dark-bg: var(--mega-nav-panel-bg); + --ty-popup-shadow: 0 16px 40px rgb(15 23 42 / 12%); + } + + &__popup.#{$prefix}-popup { + border-radius: 18px; + overflow: hidden; + } + + &__popup .#{$prefix}-menu-sub__list_popup { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + padding: 12px; + border: 1px solid var(--mega-nav-panel-border); + border-radius: 18px; + background: var(--mega-nav-panel-bg); + box-shadow: var(--mega-nav-panel-shadow); + overflow: hidden; + } + + &__popup .#{$prefix}-menu-item { + align-items: stretch; + min-height: 0; + padding: 0; + overflow: visible; + border: 0; + background: transparent; + box-shadow: none; + + &::after { + display: none; + } + } + + &__popup .#{$prefix}-menu-item:hover, + &__popup .#{$prefix}-menu-item.#{$prefix}-menu-item_selected { + background: transparent; + } + + &__popup .#{$prefix}-menu-item__main { + display: block; + width: 100%; + overflow: visible; + } + + &__popup .#{$prefix}-menu-item__label { + display: block; + overflow: visible; + } + + &__card { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 92px; + padding: 14px 16px; + border: 1px solid var(--mega-nav-card-border); + border-radius: 14px; + background: var(--mega-nav-card-bg); + box-shadow: none; + max-width: 300px; + transition: + background-color 160ms ease, + border-color 160ms ease, + box-shadow 160ms ease; + } + + &__popup .#{$prefix}-menu-item:hover &__card, + &__popup .#{$prefix}-menu-item.#{$prefix}-menu-item_selected &__card { + border-color: var(--mega-nav-card-hover-border); + background: var(--mega-nav-card-hover-bg); + box-shadow: var(--mega-nav-card-hover-shadow); + } + + &__card-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + color: var(--mega-nav-title-color); + font-size: 15px; + font-weight: 700; + line-height: 1.3; + } + + &__card-title { + min-width: 0; + flex: 1 1 auto; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; + } + + &__card-description { + margin: 0; + color: var(--mega-nav-description-color); + font-size: 13px; + line-height: 1.5; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; + } +} + +[data-tiny-theme='dark'] .menu-mega-nav { + --mega-nav-shell-bg: var(--ty-color-bg-container); + --mega-nav-shell-border: rgb(255 255 255 / 10%); + --mega-nav-shell-shadow: 0 10px 30px rgb(0 0 0 / 24%); + --mega-nav-bar-bg: rgb(255 255 255 / 4%); + --mega-nav-bar-border: rgb(255 255 255 / 8%); + --mega-nav-pill-bg: rgb(255 255 255 / 8%); + --mega-nav-pill-color: var(--ty-color-text-secondary); + --mega-nav-panel-bg: var(--ty-color-bg-container); + --mega-nav-panel-border: rgb(255 255 255 / 10%); + --mega-nav-panel-shadow: 0 18px 44px rgb(0 0 0 / 32%); + --mega-nav-card-bg: rgb(255 255 255 / 2%); + --mega-nav-card-border: rgb(255 255 255 / 10%); + --mega-nav-card-hover-bg: rgb(255 255 255 / 5%); + --mega-nav-card-hover-border: rgb(139 98 208 / 34%); + --mega-nav-card-hover-shadow: 0 8px 18px rgb(0 0 0 / 22%); + --mega-nav-title-color: var(--ty-color-text-heading); + --mega-nav-description-color: var(--ty-color-text-secondary); + --ty-popup-shadow: 0 18px 44px rgb(0 0 0 / 40%); +} + +@media (width <= 900px) { + .menu-mega-nav { + padding: 16px; + + &__popup { + --ty-menu-sub-list-popup-min-width: min(88vw, 640px); + } + + &__popup .#{$prefix}-menu-sub__list_popup { + grid-template-columns: 1fr; + } + } +} diff --git a/packages/react/src/menu/index.md b/packages/react/src/menu/index.md index b167681a..2d5ca42d 100644 --- a/packages/react/src/menu/index.md +++ b/packages/react/src/menu/index.md @@ -1,3 +1,5 @@ +import MegaNavigationDemo from './demo/MegaNavigation'; +import MegaNavigationSource from './demo/MegaNavigation.tsx?raw'; import HorizontalDemo from './demo/Horizontal'; import HorizontalSource from './demo/Horizontal.tsx?raw'; import VerticalDemo from './demo/Vertical'; @@ -31,6 +33,15 @@ const { Item, SubMenu, ItemGroup, Divider } = Menu; +### Rich Dropdown Navigation + +A mega-menu style top navigation suited for option-rich dropdowns with titles and supporting copy. + + + + + + ### Top Navigation Horizontal top navigation menu. diff --git a/packages/react/src/menu/index.zh_CN.md b/packages/react/src/menu/index.zh_CN.md index 8723eb2b..88391df2 100644 --- a/packages/react/src/menu/index.zh_CN.md +++ b/packages/react/src/menu/index.zh_CN.md @@ -1,3 +1,5 @@ +import MegaNavigationDemo from './demo/MegaNavigation'; +import MegaNavigationSource from './demo/MegaNavigation.tsx?raw'; import HorizontalDemo from './demo/Horizontal'; import HorizontalSource from './demo/Horizontal.tsx?raw'; import VerticalDemo from './demo/Vertical'; @@ -31,6 +33,15 @@ const { Item, SubMenu, ItemGroup, Divider } = Menu; +### 丰富下拉导航 + +接近产品官网顶部导航的 mega menu 形式,适合承载带说明文案的可选菜单。 + + + + + + ### 顶部导航 水平顶部导航菜单。 diff --git a/packages/react/src/native-select/style/index.scss b/packages/react/src/native-select/style/index.scss index 38b8e12d..32ed40c1 100755 --- a/packages/react/src/native-select/style/index.scss +++ b/packages/react/src/native-select/style/index.scss @@ -9,7 +9,7 @@ $select-arrow: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcm cursor: pointer; color: var(--ty-native-select-color); width: 100%; - min-width: 200px; + min-width: 0; margin: 0; vertical-align: middle; box-sizing: border-box; diff --git a/packages/react/src/grid/demo/Alignment.tsx b/packages/react/src/row/demo/Alignment.tsx similarity index 100% rename from packages/react/src/grid/demo/Alignment.tsx rename to packages/react/src/row/demo/Alignment.tsx diff --git a/packages/react/src/grid/demo/Basic.tsx b/packages/react/src/row/demo/Basic.tsx similarity index 100% rename from packages/react/src/grid/demo/Basic.tsx rename to packages/react/src/row/demo/Basic.tsx diff --git a/packages/react/src/grid/demo/Gutter.tsx b/packages/react/src/row/demo/Gutter.tsx similarity index 96% rename from packages/react/src/grid/demo/Gutter.tsx rename to packages/react/src/row/demo/Gutter.tsx index 37c7a743..9664e7cb 100644 --- a/packages/react/src/grid/demo/Gutter.tsx +++ b/packages/react/src/row/demo/Gutter.tsx @@ -41,7 +41,7 @@ export default function GutterDemo() { setGutter(val); } }} - style={{ width: 300 }} + style={{ width: 300, marginBottom: 40 }} />
col-6
diff --git a/packages/react/src/grid/demo/Offset.tsx b/packages/react/src/row/demo/Offset.tsx similarity index 100% rename from packages/react/src/grid/demo/Offset.tsx rename to packages/react/src/row/demo/Offset.tsx diff --git a/packages/react/src/grid/demo/Order.tsx b/packages/react/src/row/demo/Order.tsx similarity index 100% rename from packages/react/src/grid/demo/Order.tsx rename to packages/react/src/row/demo/Order.tsx diff --git a/packages/react/src/grid/demo/Responsive.tsx b/packages/react/src/row/demo/Responsive.tsx similarity index 100% rename from packages/react/src/grid/demo/Responsive.tsx rename to packages/react/src/row/demo/Responsive.tsx diff --git a/packages/react/src/row/index.md b/packages/react/src/row/index.md index 5aba9887..21027ee3 100644 --- a/packages/react/src/row/index.md +++ b/packages/react/src/row/index.md @@ -1,15 +1,15 @@ -import BasicDemo from '../grid/demo/Basic'; -import BasicSource from '../grid/demo/Basic.tsx?raw'; -import GutterDemo from '../grid/demo/Gutter'; -import GutterSource from '../grid/demo/Gutter.tsx?raw'; -import OffsetDemo from '../grid/demo/Offset'; -import OffsetSource from '../grid/demo/Offset.tsx?raw'; -import OrderDemo from '../grid/demo/Order'; -import OrderSource from '../grid/demo/Order.tsx?raw'; -import AlignmentDemo from '../grid/demo/Alignment'; -import AlignmentSource from '../grid/demo/Alignment.tsx?raw'; -import ResponsiveDemo from '../grid/demo/Responsive'; -import ResponsiveSource from '../grid/demo/Responsive.tsx?raw'; +import BasicDemo from './demo/Basic'; +import BasicSource from './demo/Basic.tsx?raw'; +import GutterDemo from './demo/Gutter'; +import GutterSource from './demo/Gutter.tsx?raw'; +import OffsetDemo from './demo/Offset'; +import OffsetSource from './demo/Offset.tsx?raw'; +import OrderDemo from './demo/Order'; +import OrderSource from './demo/Order.tsx?raw'; +import AlignmentDemo from './demo/Alignment'; +import AlignmentSource from './demo/Alignment.tsx?raw'; +import ResponsiveDemo from './demo/Responsive'; +import ResponsiveSource from './demo/Responsive.tsx?raw'; # Grid System diff --git a/packages/react/src/row/index.zh_CN.md b/packages/react/src/row/index.zh_CN.md index 72692236..ffe549ab 100644 --- a/packages/react/src/row/index.zh_CN.md +++ b/packages/react/src/row/index.zh_CN.md @@ -1,15 +1,15 @@ -import BasicDemo from '../grid/demo/Basic'; -import BasicSource from '../grid/demo/Basic.tsx?raw'; -import GutterDemo from '../grid/demo/Gutter'; -import GutterSource from '../grid/demo/Gutter.tsx?raw'; -import OffsetDemo from '../grid/demo/Offset'; -import OffsetSource from '../grid/demo/Offset.tsx?raw'; -import OrderDemo from '../grid/demo/Order'; -import OrderSource from '../grid/demo/Order.tsx?raw'; -import AlignmentDemo from '../grid/demo/Alignment'; -import AlignmentSource from '../grid/demo/Alignment.tsx?raw'; -import ResponsiveDemo from '../grid/demo/Responsive'; -import ResponsiveSource from '../grid/demo/Responsive.tsx?raw'; +import BasicDemo from './demo/Basic'; +import BasicSource from './demo/Basic.tsx?raw'; +import GutterDemo from './demo/Gutter'; +import GutterSource from './demo/Gutter.tsx?raw'; +import OffsetDemo from './demo/Offset'; +import OffsetSource from './demo/Offset.tsx?raw'; +import OrderDemo from './demo/Order'; +import OrderSource from './demo/Order.tsx?raw'; +import AlignmentDemo from './demo/Alignment'; +import AlignmentSource from './demo/Alignment.tsx?raw'; +import ResponsiveDemo from './demo/Responsive'; +import ResponsiveSource from './demo/Responsive.tsx?raw'; # Grid System 栅格系统 diff --git a/packages/react/src/time-picker/style/index.scss b/packages/react/src/time-picker/style/index.scss index 93c1c4a1..031c4831 100755 --- a/packages/react/src/time-picker/style/index.scss +++ b/packages/react/src/time-picker/style/index.scss @@ -5,6 +5,7 @@ $tp: #{$prefix}-time-picker; .#{$tp} { display: inline-flex; position: relative; + width: 100%; font-size: var(--ty-picker-input-font-size); // ---- Input ----