diff --git a/.changeset/dropdown-hover-only.md b/.changeset/dropdown-hover-only.md
new file mode 100644
index 000000000..0331ca3aa
--- /dev/null
+++ b/.changeset/dropdown-hover-only.md
@@ -0,0 +1,5 @@
+---
+'@tiny-design/react': patch
+---
+
+Normalize Dropdown overlay menus to hover-only feedback and align grouped popup item styling with the rest of the dropdown menu.
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..f52a834ad
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,45 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+This repository is a `pnpm` workspace managed with Turborepo. Core packages live under `packages/`:
+
+- `packages/react`: main React component library (`src/`, component styles, demos, and Jest tests)
+- `packages/icons`, `packages/tokens`, `packages/cli`, `packages/mcp`, `packages/charts`: supporting packages
+- `apps/docs`: Vite-powered documentation site and Playwright visual tests
+
+Common root config lives in `turbo.json`, `eslint.config.mjs`, `.prettierrc`, and `.stylelintrc`. Changesets are stored in `.changeset/`.
+
+## Build, Test, and Development Commands
+- `pnpm install`: install workspace dependencies; Node `>=22` is required.
+- `pnpm dev`: run workspace dev tasks through Turbo; primarily used for the docs app.
+- `pnpm build`: build all packages.
+- `pnpm test`: run package test suites through Turbo.
+- `pnpm test:visual`: run Playwright visual tests from `apps/docs/tests/visual`.
+- `pnpm lint`: run ESLint across workspace packages.
+- `pnpm lint:style`: run Stylelint for SCSS sources.
+
+For package-specific work, prefer filtering:
+`pnpm --filter @tiny-design/react test` or `pnpm --filter @tiny-design/react lint`.
+
+## Coding Style & Naming Conventions
+Use TypeScript with 2-space indentation, semicolons, single quotes, and `printWidth: 100` per `.prettierrc`. Run Prettier before submitting changes.
+
+React source lives mainly in `packages/react/src`. Follow existing component patterns:
+- component folders in kebab-case, for example `packages/react/src/date-picker/`
+- public exports in each component `index.tsx` and again in `packages/react/src/index.ts`
+- SCSS class names prefixed with `ty-`
+
+Linting uses ESLint 9 for TypeScript/React and Stylelint for SCSS.
+
+## Testing Guidelines
+Jest is used for unit tests; place tests in `src/__tests__/` or the package’s existing test location and name files `*.test.ts` or `*.test.tsx`. Use Playwright for docs visual regression coverage.
+
+Run:
+- `pnpm test`
+- `pnpm --filter @tiny-design/react test:coverage`
+- `pnpm test:visual`
+
+## Commit & Pull Request Guidelines
+Recent history favors short Conventional Commit-style subjects such as `fix(react): ...`, `feat(button): ...`, or `chore: ...`. Keep commits focused and imperative.
+
+PRs should include a clear summary, linked issue when applicable, and screenshots or visual diffs for UI/docs changes. Add or update tests for behavioral changes. For user-facing package changes, create a changeset with `pnpm changeset` and commit the generated file.
diff --git a/apps/docs/src/containers/home/home.scss b/apps/docs/src/containers/home/home.scss
index 71a74153e..e2bbdbbf2 100755
--- a/apps/docs/src/containers/home/home.scss
+++ b/apps/docs/src/containers/home/home.scss
@@ -11,6 +11,13 @@
}
.home {
+ --home-surface-base: color-mix(in srgb, var(--ty-color-bg-elevated) 90%, var(--ty-color-primary) 10%);
+ --home-surface-soft: color-mix(in srgb, var(--ty-color-fill) 86%, var(--ty-color-primary) 14%);
+ --home-surface-strong: color-mix(in srgb, var(--ty-color-bg-elevated) 76%, var(--ty-color-primary) 24%);
+ --home-border-strong: color-mix(in srgb, var(--ty-color-border-secondary) 68%, var(--ty-color-primary) 32%);
+ --home-border-soft: color-mix(in srgb, var(--ty-color-border-secondary) 84%, var(--ty-color-primary) 16%);
+ --home-shadow-ambient: color-mix(in srgb, var(--ty-color-primary) 18%, transparent);
+ --home-shadow-depth: color-mix(in srgb, var(--ty-color-text) 10%, transparent);
padding-top: $header-height;
position: relative;
overflow: hidden;
@@ -47,9 +54,9 @@
&__hero {
position: relative;
z-index: 1;
- max-width: 960px;
+ max-width: 1200px;
margin: 0 auto;
- padding: 100px 40px 60px;
+ padding: 84px 40px 44px;
text-align: center;
}
@@ -88,7 +95,7 @@
background: linear-gradient(
135deg,
var(--ty-color-primary) 0%,
- color-mix(in srgb, var(--ty-color-primary) 60%, #8B5CF6) 100%
+ color-mix(in srgb, var(--ty-color-primary) 62%, var(--ty-color-info)) 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
@@ -99,13 +106,14 @@
font-size: clamp(16px, 2vw, 20px);
color: var(--ty-color-text-secondary);
line-height: 1.6;
- margin: 20px auto 0;
- max-width: 520px;
+ margin: 18px auto 0;
+ max-width: 660px;
}
&__hero-actions {
- margin-top: 36px;
+ margin-top: 28px;
justify-content: center;
+ flex-wrap: wrap;
}
&__btn-primary {
@@ -126,7 +134,7 @@
display: flex;
justify-content: center;
gap: 48px;
- margin-top: 64px;
+ margin-top: 36px;
animation: fadeInUp 0.6s ease 0.2s both;
}
@@ -188,41 +196,402 @@
line-height: 1.6;
}
- // ─── Component Showcase ───
- &__showcase-grid {
+ &__product-shell {
display: grid;
- grid-template-columns: repeat(3, 1fr);
+ grid-template-columns: 260px minmax(0, 1fr);
+ gap: 18px;
+ margin-top: 28px;
+ padding: 18px;
+ border-radius: 30px;
+ border: 1px solid var(--home-border-strong);
+ background:
+ linear-gradient(180deg, var(--home-surface-base) 0%, var(--home-surface-soft) 100%);
+ box-shadow:
+ 0 32px 120px var(--home-shadow-ambient),
+ 0 16px 40px var(--home-shadow-depth);
+ overflow: hidden;
+ text-align: left;
+ }
+
+ &__product-sidebar,
+ &__product-main {
+ min-width: 0;
+ }
+
+ &__product-sidebar {
+ padding: 18px;
+ border-radius: 22px;
+ background:
+ linear-gradient(180deg, var(--home-surface-strong) 0%, var(--home-surface-base) 100%);
+ border: 1px solid var(--home-border-strong);
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--ty-color-bg-elevated) 60%, transparent);
+ }
+
+ &__product-brand {
+ display: flex;
+ align-items: center;
gap: 12px;
+
+ strong,
+ span {
+ display: block;
+ }
+
+ strong {
+ font-size: 15px;
+ color: var(--ty-color-text);
+ }
+
+ span {
+ margin-top: 2px;
+ font-size: 12px;
+ color: var(--ty-color-text-secondary);
+ }
}
- &__showcase-card {
- padding: 20px;
+ &__product-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 24px;
+ }
+
+ &__product-nav-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid transparent;
border-radius: 14px;
- border: 1px solid var(--ty-color-border-secondary);
- background: var(--ty-color-bg-elevated);
- transition: border-color 0.2s, box-shadow 0.2s;
- animation: fadeInUp 0.5s ease both;
+ background: transparent;
+ color: var(--ty-color-text-secondary);
+ cursor: pointer;
+ font: inherit;
+ transition: all 0.2s ease;
&:hover {
- border-color: var(--ty-color-border);
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
+ background: color-mix(in srgb, var(--ty-color-bg-elevated) 80%, transparent);
+ color: var(--ty-color-text);
}
- &_wide {
- grid-column: span 2;
+ &_active {
+ border-color: color-mix(in srgb, var(--ty-color-primary) 16%, transparent);
+ background: color-mix(in srgb, var(--ty-color-fill) 76%, var(--ty-color-primary) 24%);
+ color: var(--ty-color-text);
}
}
- &__showcase-label {
- display: block;
+ &__product-sidebar-card {
+ margin-top: 22px;
+ border-color: var(--home-border-soft);
+ background: var(--home-surface-soft);
+ box-shadow: none;
+
+ .ty-card__body {
+ padding: 20px;
+ }
+
+ .ty-typography {
+ margin: 12px 0 16px;
+ font-size: 13px;
+ line-height: 1.6;
+ color: var(--ty-color-text-secondary);
+ }
+ }
+
+ &__product-sidebar-kicker {
font-size: 11px;
font-weight: 700;
+ letter-spacing: 0.08em;
text-transform: uppercase;
- letter-spacing: 0.06em;
color: var(--ty-color-text-tertiary);
+ }
+
+ &__product-swatches {
+ display: flex;
+ gap: 10px;
+ margin-top: 14px;
+ }
+
+ &__product-swatch {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ty-color-text) 10%, transparent);
+
+ &_primary { background: var(--ty-color-primary); }
+ &_info { background: var(--ty-color-info); }
+ &_success { background: var(--ty-color-success); }
+ &_warning { background: var(--ty-color-warning); }
+ }
+
+ &__avatar {
+ color: #fff;
+
+ &_primary { background: var(--ty-color-primary); }
+ &_info { background: var(--ty-color-info); }
+ &_success { background: var(--ty-color-success); }
+ }
+
+ &__product-main {
+ padding: 6px;
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 22px;
+ background:
+ radial-gradient(circle at top right, color-mix(in srgb, var(--ty-color-primary) 14%, transparent), transparent 36%);
+ pointer-events: none;
+ }
+ }
+
+ &__product-descriptions {
+ margin-top: 16px;
+ }
+
+ &__product-topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 16px;
+ }
+
+ &__product-search {
+ flex: 1;
+ }
+
+ &__product-metrics {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+ margin-bottom: 12px;
+ }
+
+ &__product-metric,
+ &__product-panel,
+ &__scenario-card,
+ &__coverage-card {
+ border-radius: 20px;
+ box-shadow: none;
+ }
+
+ &__product-metric {
+ border-color: var(--home-border-soft);
+ background: var(--home-surface-soft);
+
+ .ty-card__body {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .ty-statistic__content {
+ font-family: $font-display;
+ font-size: 28px;
+ line-height: 1;
+ }
+ }
+
+ &__product-pills {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ &__product-panels {
+ display: grid;
+ grid-template-columns: 1.55fr 1fr;
+ gap: 12px;
+ }
+
+ &__product-panel {
+ min-width: 0;
+ border-color: var(--home-border-soft);
+ background: var(--home-surface-base);
+
+ .ty-card__header {
+ align-items: center;
+ font-family: $font-display;
+ font-size: 18px;
+ letter-spacing: -0.01em;
+ background: transparent;
+ }
+
+ .ty-card__body {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ }
+
+ &__product-panel_wide {
+ .ty-tabs {
+ margin-bottom: 4px;
+ }
+ }
+
+ &__product-panel_activity {
+ margin-top: 12px;
+
+ .ty-card__body {
+ gap: 0;
+ }
+
+ .ty-timeline {
+ margin: 0;
+ }
+ }
+
+ &__product-activity-item {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding-bottom: 6px;
+
+ strong {
+ font-size: 13px;
+ color: var(--ty-color-text);
+ }
+
+ span {
+ font-size: 12px;
+ line-height: 1.6;
+ color: var(--ty-color-text-secondary);
+ }
+ }
+
+ &__scenario-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+ }
+
+ &__scenario-card {
+ min-height: 280px;
+ border-color: var(--home-border-soft);
+ background:
+ linear-gradient(180deg, var(--home-surface-base) 0%, var(--ty-color-bg-elevated) 100%);
+ transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
+
+ .ty-card__body {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-height: 280px;
+ padding: 24px;
+ }
+
+ &:hover {
+ transform: translateY(-3px);
+ border-color: var(--ty-color-border);
+ box-shadow: 0 18px 40px color-mix(in srgb, var(--ty-color-text) 8%, transparent);
+ }
+ }
+
+ &__scenario-kicker {
+ display: inline-flex;
margin-bottom: 14px;
}
+ &__scenario-title.ty-typography {
+ margin: 0;
+ font-family: $font-display;
+ font-size: 24px;
+ line-height: 1.1;
+ letter-spacing: -0.02em;
+ color: var(--ty-color-text);
+ }
+
+ &__scenario-desc.ty-typography {
+ margin: 12px 0 0;
+ font-size: 14px;
+ line-height: 1.7;
+ color: var(--ty-color-text-secondary);
+ }
+
+ &__scenario-preview {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ margin-top: 24px;
+ padding: 18px;
+ border-radius: 16px;
+ background: var(--home-surface-soft);
+ border: 1px solid var(--home-border-soft);
+ }
+
+ &__scenario-preview {
+ .ty-statistic__content {
+ font-family: $font-display;
+ font-size: 30px;
+ line-height: 1;
+ }
+ }
+
+ &__scenario-bars {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 10px;
+ align-items: end;
+ min-height: 120px;
+
+ span {
+ display: block;
+ border-radius: 999px 999px 10px 10px;
+ background: linear-gradient(180deg, color-mix(in srgb, var(--ty-color-primary) 70%, var(--ty-color-bg-elevated)) 0%, var(--ty-color-primary) 100%);
+ }
+ }
+
+ &__coverage-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+ }
+
+ &__coverage-card {
+ border: 1px solid var(--home-border-soft);
+ background: var(--home-surface-base);
+
+ .ty-card__body {
+ padding: 22px;
+ }
+ }
+
+ &__coverage-title.ty-typography {
+ margin: 0;
+ font-family: $font-display;
+ font-size: 20px;
+ color: var(--ty-color-text);
+ }
+
+ &__coverage-desc.ty-typography {
+ margin: 10px 0 0;
+ font-size: 14px;
+ line-height: 1.7;
+ color: var(--ty-color-text-secondary);
+ }
+
+ &__coverage-summary {
+ grid-column: 1 / -1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 22px 2px 0;
+
+ span {
+ max-width: 720px;
+ font-size: 14px;
+ line-height: 1.7;
+ color: var(--ty-color-text-secondary);
+ }
+ }
+
// ─── Code Example ───
&__code-split {
display: grid;
@@ -449,7 +818,7 @@
&:hover {
border-color: var(--ty-color-border);
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
+ box-shadow: 0 2px 12px color-mix(in srgb, var(--ty-color-text) 8%, transparent);
}
code {
@@ -536,7 +905,7 @@
&:hover {
border-color: var(--ty-color-border);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
+ box-shadow: 0 4px 16px color-mix(in srgb, var(--ty-color-text) 10%, transparent);
transform: translateY(-2px);
}
@@ -549,7 +918,7 @@
width: 20px;
height: 20px;
border-radius: 50%;
- border: 1px solid rgba(0, 0, 0, 0.08);
+ border: 1px solid color-mix(in srgb, var(--ty-color-text) 10%, transparent);
}
&__marquee-name {
@@ -567,6 +936,27 @@
// ─── Responsive ───
@media (max-width: $size-xl) {
+ &__product-shell {
+ grid-template-columns: 1fr;
+ }
+
+ &__product-sidebar {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 18px;
+ align-items: start;
+ }
+
+ &__product-nav,
+ &__product-sidebar-card {
+ margin-top: 0;
+ }
+
+ &__scenario-grid,
+ &__coverage-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
&__features-row {
grid-template-columns: repeat(2, 1fr);
}
@@ -574,7 +964,7 @@
@media (max-width: $size-md) {
&__hero {
- padding: 60px 24px 40px;
+ padding: 56px 24px 32px;
}
&__hero-stats {
@@ -582,19 +972,36 @@
flex-wrap: wrap;
}
- &__hero-stat-value { font-size: 24px; }
-
- &__section { padding: 48px 0; }
- &__section-inner { padding: 0 24px; }
+ &__product-shell {
+ margin-top: 24px;
+ padding: 14px;
+ border-radius: 22px;
+ }
- &__showcase-grid {
+ &__product-sidebar {
grid-template-columns: 1fr;
+ padding: 16px;
+ border-radius: 18px;
}
- &__showcase-card_wide {
- grid-column: span 1;
+ &__product-topbar,
+ &__coverage-summary {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ &__product-metrics,
+ &__product-panels,
+ &__scenario-grid,
+ &__coverage-grid {
+ grid-template-columns: 1fr;
}
+ &__hero-stat-value { font-size: 24px; }
+
+ &__section { padding: 48px 0; }
+ &__section-inner { padding: 0 24px; }
+
&__code-split {
grid-template-columns: 1fr;
}
@@ -608,7 +1015,7 @@
@media (max-width: $size-sm) {
&__hero {
- padding: 40px 20px 30px;
+ padding: 36px 20px 26px;
}
&__hero-badge {
@@ -622,7 +1029,7 @@
&__hero-stats {
gap: 16px 28px;
- margin-top: 40px;
+ margin-top: 28px;
}
&__hero-stat-value { font-size: 22px; }
@@ -634,6 +1041,19 @@
&__section-title { font-size: 22px; }
&__section-subtitle { font-size: 14px; }
+ &__product-metric .ty-statistic__content {
+ font-size: 24px;
+ }
+
+ &__scenario-card,
+ &__coverage-card {
+ border-radius: 16px;
+ }
+
+ &__scenario-title {
+ font-size: 20px;
+ }
+
&__code-block {
border-radius: 10px;
}
@@ -689,7 +1109,7 @@
@media (max-width: $size-xs) {
&__hero {
- padding: 32px 16px 24px;
+ padding: 28px 16px 22px;
}
&__hero-heading { font-size: 28px; }
@@ -713,15 +1133,24 @@
&__section-inner { padding: 0 12px; }
&__section-header { margin-bottom: 24px; }
- &__showcase-card {
- padding: 16px;
- border-radius: 10px;
- }
-
&__features-row {
gap: 10px;
}
+ &__product-shell {
+ padding: 10px;
+ border-radius: 16px;
+ }
+
+ &__product-main {
+ padding: 0;
+ }
+
+ &__scenario-card .ty-card__body {
+ min-height: auto;
+ padding: 18px;
+ }
+
&__cta-install {
flex-direction: column;
gap: 8px;
diff --git a/apps/docs/src/containers/home/index.tsx b/apps/docs/src/containers/home/index.tsx
index c466d24f7..3add76b6c 100755
--- a/apps/docs/src/containers/home/index.tsx
+++ b/apps/docs/src/containers/home/index.tsx
@@ -7,12 +7,14 @@ import { LightCodeTheme, DarkCodeTheme } from '../../components/demo-block/code-
import * as TinyDesign from '@tiny-design/react';
import * as TinyIcons from '@tiny-design/icons';
import {
- Button, Flex, Tag, Switch, Badge, Avatar, Progress, Rate, Input, Tabs,
- Slider, Tooltip, Checkbox, Radio, Keyboard, useTheme,
+ Button, Flex, Tag, Badge, Avatar, Progress, Rate, Input, Tabs,
+ Tooltip, Checkbox, Keyboard, useTheme, Card, Heading, Paragraph,
+ Statistic, Descriptions, Table, Timeline,
} from '@tiny-design/react';
import {
- IconGithub, IconSearch, IconCheckmark,
- IconColorlens, IconCode, IconPuzzle, IconAccessible,
+ IconGithub, IconSearch, IconCheckmark, IconArrowRight,
+ IconColorlens, IconStructure, IconCheckCircle, IconFire, IconStatistics,
+ IconSettings, IconCalendar, IconComment, IconCreditCard,
} from '@tiny-design/icons';
import { Footer } from './footer';
import { ThemeShowcase } from './theme-showcase';
@@ -23,127 +25,269 @@ import logoSvg from '../../assets/logo/logo.svg';
const { repository } = pkg;
-// ─── Component Showcase Section ───
-const ComponentShowcase = (): React.ReactElement => {
- const [switchVal, setSwitchVal] = useState(true);
- const [sliderVal, setSliderVal] = useState(40);
+const productRows = [
+ { key: '1', name: 'Acme Inc.', owner: 'Design', status: Healthy },
+ { key: '2', name: 'Northstar', owner: 'Growth', status: Review },
+ { key: '3', name: 'Helio', owner: 'Ops', status: Live },
+];
+
+const categoryGroups = [
+ { title: 'Foundation', items: 'Button, Icon, Typography, Link' },
+ { title: 'Layout', items: 'Flex, Grid, Split, Space' },
+ { title: 'Navigation', items: 'Tabs, Menu, Breadcrumb, Pagination' },
+ { title: 'Forms', items: 'Input, Select, Date Picker, Checkbox' },
+ { title: 'Data Display', items: 'Card, Table, Timeline, Charts' },
+ { title: 'Feedback', items: 'Modal, Notification, Tooltip, Drawer' },
+];
+
+const activityItems = [
+ { key: '1', color: 'var(--ty-color-success)', title: 'Theme tokens synced', desc: 'Studio Bloom updated across docs and dashboard previews.' },
+ { key: '2', color: 'var(--ty-color-info)', title: 'Checkout flow reviewed', desc: 'Input, validation, and action states passed the latest visual pass.' },
+ { key: '3', color: 'var(--ty-color-primary)', title: 'Release candidate prepared', desc: '86+ components bundled for the next internal release.' },
+];
+
+const ProductPreview = ({ s }: { s: any }): React.ReactElement => {
const [rateVal, setRateVal] = useState(4);
return (
-
- {/* Card 1: Buttons */}
-
- Buttons
-
- Primary
- Outline
- Default
- Ghost
- Success
- Danger
-
-
-
- {/* Card 2: Tags */}
-
-
Tags
-
- Default
- #f50
- Success
- Warning
- Danger
- Info
-
+
+
+
+
+
+ Tiny UI
+ {s.home.preview.workspace}
+
+
+
+
+
+ {s.home.preview.overview}
+
+
+
+ {s.home.preview.components}
+
+
+
+ {s.home.preview.settings}
+
+
+
+
+ {s.home.preview.activeTheme}
+
+
+
+
+
+
+
+ {s.home.preview.themeValue}
+ {s.home.preview.tokensValue}
+
+ {s.home.preview.themeHint}
+ {s.home.preview.editTheme}
+
+
- {/* Card 3: Toggle & Slider */}
-
-
Controls
-
-
-
-
- {switchVal ? 'Enabled' : 'Disabled'}
-
+
+
+
}
+ className="home__product-search"
+ />
+
+
+ } />
+
+ DW
-
setSliderVal(v as number)} />
-
-
-
- {/* Card 4: Avatars & Badge */}
-
-
Avatars
-
-
- A
- B
- C
- D
- E
-
-
- U
-
-
-
-
- {/* Card 5: Input */}
-
- Input
- } />
-
-
- {/* Card 6: Progress & Rate */}
-
+
- {/* Card 7: Tabs */}
-
- Tabs
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Stable
+ Typed
+
+
+
+
+
+
+
+
+
+
- {/* Card 8: Checkbox & Radio */}
-
- Selection
-
- Remember me
-
- Option A
- Option B
-
-
-
+
- {/* Card 9: Keyboard & Tooltip */}
-
-
Misc
-
- ⌘
- K
-
- Hover me
-
-
+
{s.home.preview.activityTag}}
+ variant="outlined">
+
+
+ {activityItems.map((item) => (
+
+
+ {item.title}
+ {item.desc}
+
+
+ ))}
+
+
+
);
};
+const ScenarioShowcase = ({ s }: { s: any }): React.ReactElement => (
+
+
+
+
+
{s.home.scenarios.dashboard.kicker}
+
{s.home.scenarios.dashboard.title}
+
{s.home.scenarios.dashboard.desc}
+
+
+
+
+
+
+
+
+
{s.home.scenarios.forms.kicker}
+
{s.home.scenarios.forms.title}
+
{s.home.scenarios.forms.desc}
+
+
+
+ } />
+
+ Validated
+ Typed
+ Draft
+
+
+
+
+
+
+
+
+
{s.home.scenarios.content.kicker}
+
{s.home.scenarios.content.title}
+
{s.home.scenarios.content.desc}
+
+
+
+
+ A
+ B
+ C
+
+
+
+ ⌘
+ K
+
+
+
+
+
+
+
+
+);
+
+const CategoryCoverage = ({ s }: { s: any }): React.ReactElement => (
+
+ {categoryGroups.map((group) => (
+
+
+ {group.title}
+ {group.items}
+
+
+ ))}
+
+ {s.home.coverage.summary}
+ window.location.assign('/components')}>
+ {s.home.coverage.cta}
+
+
+
+);
+
// ─── Live Code Example Section ───
const INITIAL_CODE = `import { Button, Flex, Tag } from '@tiny-design/react';
@@ -315,11 +459,11 @@ const HomePage = (): React.ReactElement => {
-
Tiny UI for React
+
{s.home.badge}
- Build beautiful interfaces
- with less effort
+ {s.home.heroTitle}
+ {s.home.heroAccent}
{s.home.subtitle}
@@ -330,6 +474,13 @@ const HomePage = (): React.ReactElement => {
onClick={() => navigate('/guide')}>
{s.home.getStarted}
+ navigate('/components')}>
+ {s.home.browseComponents}
+
{
))}
+
- {/* ─── Component Showcase ─── */}
+ {/* ─── Scenario Showcase ─── */}
{s.home.showcase.title}
{s.home.showcase.subtitle}
-
+
@@ -379,25 +531,26 @@ const HomePage = (): React.ReactElement => {
{s.home.designPrinciple}
+
{s.home.designPrincipleDesc}
}
+ icon={ }
title={s.home.features.themeable}
desc={s.home.features.themeableDesc}
/>
}
+ icon={ }
title={s.home.features.elegant}
desc={s.home.features.elegantDesc}
/>
}
+ icon={ }
title={s.home.features.composable}
desc={s.home.features.composableDesc}
/>
}
+ icon={ }
title={s.home.features.accessible}
desc={s.home.features.accessibleDesc}
/>
@@ -405,6 +558,16 @@ const HomePage = (): React.ReactElement => {
+
+
+
+
{s.home.coverage.title}
+
{s.home.coverage.subtitle}
+
+
+
+
+
{/* ─── CTA ─── */}
diff --git a/apps/docs/src/containers/theme-studio/index.tsx b/apps/docs/src/containers/theme-studio/index.tsx
index d3e9bc6ac..7cbe93946 100644
--- a/apps/docs/src/containers/theme-studio/index.tsx
+++ b/apps/docs/src/containers/theme-studio/index.tsx
@@ -148,12 +148,6 @@ const ThemeStudioPage = (): React.ReactElement => {
{
- const query = inputValue.toLowerCase();
- const label = typeof option.label === 'string' ? option.label : '';
- return label.toLowerCase().includes(query) || option.value.toLowerCase().includes(query);
- }}
onChange={(value) => handlePresetChange(value)}
>
{THEME_EDITOR_PRESETS.map((preset) => (
diff --git a/apps/docs/src/containers/theme-studio/preview-components.tsx b/apps/docs/src/containers/theme-studio/preview-components.tsx
index 9c6344cca..3884d2f78 100644
--- a/apps/docs/src/containers/theme-studio/preview-components.tsx
+++ b/apps/docs/src/containers/theme-studio/preview-components.tsx
@@ -133,28 +133,6 @@ const subscriptionsChartConfig: ChartConfig = {
const currYear = new Date().getFullYear()
const currMonth = new Date().getMonth()
-function MetricsStrip({
- items = [
- ['Total Revenue', '$15,231.89', '+20.1% from last month'],
- ['Subscriptions', '+2,350', '+180.1% from last month'],
- ['Active Goal', '350', 'Calories per day'],
- ],
-}: {
- items?: Array<[string, string, string]>;
-}): React.ReactElement {
- return (
-
- {items.map(([label, value, meta]) => (
-
- {label}
- {value}
- {meta}
-
- ))}
-
- );
-}
-
function LiveResponsePanel({ fields }: { fields: ThemeEditorFields }): React.ReactElement {
const colorPairs = [
['Primary', fields.primary, fields.primaryForeground],
@@ -167,8 +145,8 @@ function LiveResponsePanel({ fields }: { fields: ThemeEditorFields }): React.Rea
];
return (
-
-
+
+
Live Colors
{colorPairs.map(([label, background, foreground]) => (
@@ -177,60 +155,60 @@ function LiveResponsePanel({ fields }: { fields: ThemeEditorFields }): React.Rea
))}
-
+
-
+
Charts
-
+
{[fields.chart1, fields.chart2, fields.chart3, fields.chart4, fields.chart5].map((color, index) => (
))}
-
-
+
+
-
+
Typography
-
+
Ag
{fields.fontSans.split(',')[0].replaceAll('"', '')}
{fields.fontSizeBase} / {fields.lineHeightBase}
-
-
+
+
-
+
+
-
-
+
+
+
+
);
}
function CardsPreview(): React.ReactElement {
return (
-
-
-
+
+
+
-
-
+
+
Total Revenue
+20.1% from last month
-
-
+
+
$15,231.89
@@ -251,12 +229,12 @@ function CardsPreview(): React.ReactElement {
-
-
+
+
Subscriptions
+180.1% from last month
-
-
+
+
+2,350
@@ -280,16 +258,16 @@ function CardsPreview(): React.ReactElement {
-
+
-
+
Upgrade your subscription
You are currently on the free plan. Upgrade to the pro plan to get access to all features.
-
+
-
+
Team Members
Invite your team members to collaborate.
-
-
+
+
{[
['Sofia Davis', 'm@example.com', 'Owner'],
['Jackson Lee', 'p@example.com', 'Developer'],
@@ -356,37 +334,37 @@ function CardsPreview(): React.ReactElement {
))}
-
+
-
- Cookie Settings
- Manage your cookie settings here.
-
-
-
-
+
+
+ Cookie Settings
+ Manage your cookie settings here.
+
+
+
Strictly Necessary
These cookies are essential in order to use the website and use its features.
-
+
-
-
-
+
+
+
Functional Cookies
These cookies allow the website to provide personalized functionality.
-
+
-
+
Save preferences
-
+
-
+
@@ -395,18 +373,18 @@ function CardsPreview(): React.ReactElement {
-
+
Move Goal
Set your daily activity goal.
-
-
+
+
-
-
+
350
CALORIES/DAY
-
+
+
-
+
@@ -418,20 +396,20 @@ function CardsPreview(): React.ReactElement {
Set Goal
-
-
+
+
-
+
-
+
Create an account
Enter your email below to create your account
-
-
+
+
GitHub
Google
-
+
OR CONTINUE WITH
@@ -443,14 +421,14 @@ function CardsPreview(): React.ReactElement {
-
+
S
Sofia Davis
m@example.com
-
-
+
+
Hi, how can I help you today?
@@ -463,7 +441,7 @@ function CardsPreview(): React.ReactElement {
I can't log in.
-
+
@@ -478,12 +456,12 @@ function CardsPreview(): React.ReactElement {
-
+
Exercise Minutes
Your exercise minutes are ahead of where you normally are.
-
+
@@ -510,8 +488,8 @@ function CardsPreview(): React.ReactElement {
-
-
+
+
);
}
@@ -533,13 +511,13 @@ function DashboardPreview(): React.ReactElement {
-
+
TD
Tiny Theme
Dashboard
-
+
Workspace
{['Overview', 'Themes', 'Revenue', 'Activity', 'Settings'].map((item, index) => (
-
-
-
+
+
+
Dashboard
Revenue snapshot
-
+
Healthy
Synced
-
-
-
+
+
+
Last 7 days
Last 30 days
Last 90 days
Export
-
-
-
-
+
+
@@ -620,10 +590,10 @@ function DashboardPreview(): React.ReactElement {
-
+
@@ -632,7 +602,7 @@ function DashboardPreview(): React.ReactElement {
-
+
{[
['Direct', '42%', 'Stable'],
['Referral', '26%', '+4.2%'],
@@ -647,29 +617,29 @@ function DashboardPreview(): React.ReactElement {
{value}
))}
-
+
-
+
);
}
function MailPreview(): React.ReactElement {
return (
-
+
-
-
+
+
Mailbox
Studio Mail
-
+
Compose
-
+
-
+
{[
['Inbox', '128'],
['Drafts', '9'],
@@ -686,32 +656,32 @@ function MailPreview(): React.ReactElement {
{count}
))}
-
+
-
+
Labels
Design
Work
Personal
-
+
-
+
Filter
-
-
+
+
All mail
Unread
Assigned
-
+
-
+
{[
['Sofia Davis', 'New message', 'Hi, how can I help you today?', '12m', true],
['Jackson Lee', 'Billing issue', 'I cannot update my card.', '1h', false],
@@ -732,27 +702,27 @@ function MailPreview(): React.ReactElement {
))}
-
+
-
-
+
+
S
Re: New message
support@tiny.design
-
-
+
+
Unread
Archive
-
-
+
+
-
+
Hi, how can I help you today?
@@ -765,36 +735,32 @@ function MailPreview(): React.ReactElement {
I can't log in after resetting my password.
-
+
-
-
-
-
- Save Draft
- Send
-
-
-
+
+
+ Save Draft
+ Send
+
-
+
);
}
function PricingPreview(): React.ReactElement {
return (
-
-
-
+
+
+
Pricing
Simple pricing for modern teams
Choose a plan that scales from solo work to multi-product organizations.
-
+
No setup fee
Cancel anytime
-
-
+
+
-
+
{[
@@ -815,23 +781,23 @@ function PricingPreview(): React.ReactElement {
className={`theme-studio__pricing-card${plan.featured ? ' theme-studio__pricing-card_featured' : ''}`}
>
-
+
{plan.featured ? Popular : null}
{plan.name}
-
-
+
+
{plan.price}
Per workspace / month
-
+
{plan.description}
-
+
{plan.features.map((feature) => (
{feature}
))}
-
-
+
+
{plan.featured ? 'Upgrade plan' : 'Choose plan'}
-
+
))}
@@ -840,34 +806,34 @@ function PricingPreview(): React.ReactElement {
-
-
+
+
Can I cancel anytime?
Yes. Plans can be changed or canceled without lock-in.
-
-
+
+
Do you offer team migration?
Pro and Scale include assisted import and onboarding.
-
-
+
+
-
+
+
-
+
);
}
@@ -885,9 +851,9 @@ export function renderPreview(
:
;
return (
-
+
{(template !== 'cards' && (section === 'colors' || section === 'typography' || section === 'other')) ? : null}
{content}
-
+
);
}
diff --git a/apps/docs/src/containers/theme-studio/theme-studio.scss b/apps/docs/src/containers/theme-studio/theme-studio.scss
index 1ce00fa7e..ff9c824e4 100644
--- a/apps/docs/src/containers/theme-studio/theme-studio.scss
+++ b/apps/docs/src/containers/theme-studio/theme-studio.scss
@@ -123,7 +123,6 @@
.theme-studio__sidebar {
gap: 12px;
position: sticky;
- top: 88px;
}
.theme-studio__sidebar,
@@ -395,7 +394,6 @@
}
.theme-studio__response-panel {
- display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
padding: 7px;
@@ -464,7 +462,6 @@
}
.theme-studio__chart-strip {
- display: flex;
gap: 6px;
min-height: 34px;
align-items: center;
@@ -508,8 +505,6 @@
}
.theme-studio__type-inline {
- display: flex;
- flex-direction: column;
justify-content: center;
gap: 2px;
}
@@ -526,7 +521,6 @@
}
.theme-studio__surface-inline {
- display: flex;
gap: 8px;
}
@@ -590,23 +584,18 @@
}
.theme-studio__cards-scene {
- display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
gap: 14px;
align-items: start;
}
.theme-studio__cards-column {
- display: flex;
- flex-direction: column;
gap: 14px;
min-width: 0;
}
-.theme-studio__cards-top-grid,
.theme-studio__cards-top-pair,
.theme-studio__cards-bottom-pair {
- display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
align-items: stretch;
@@ -654,8 +643,6 @@
.theme-studio__cards-panel-head,
.theme-studio__cards-copy-block {
- display: flex;
- flex-direction: column;
gap: 4px;
}
@@ -693,10 +680,7 @@
}
.theme-studio__cards-actions-end,
-.theme-studio__cards-auth-actions,
-.theme-studio__cards-share-row,
-.theme-studio__cards-goal-actions {
- display: flex;
+.theme-studio__cards-auth-actions {
gap: 10px;
align-items: center;
}
@@ -727,37 +711,10 @@
border: 0;
}
-.theme-studio__preview-grid_cards > * {
- min-height: 188px;
-}
-
-.theme-studio__cards-waterfall {
- width: 100%;
- min-height: 1200px;
-}
-
-.theme-studio__cards-waterfall .ty-waterfall__item {
- min-width: 0;
-}
-
.theme-studio__preview-card {
overflow: hidden;
}
-.theme-studio__card-metric {
- min-height: 132px !important;
-}
-
-.theme-studio__card-tall {
- min-height: 332px !important;
-}
-
-.theme-studio__metric_preview {
- padding: 0;
- border: 0;
- background: transparent;
-}
-
.theme-studio__card-kicker-row {
display: flex;
align-items: center;
@@ -771,8 +728,6 @@
}
.theme-studio__goal-display {
- display: flex;
- flex-direction: column;
margin: 0;
align-items: center;
}
@@ -790,9 +745,6 @@
}
.theme-studio__cards-goal-header {
- display: grid;
- grid-template-columns: 28px minmax(0, 1fr) 28px;
- align-items: center;
gap: 8px;
}
@@ -808,69 +760,6 @@
justify-content: center;
}
-.theme-studio__goal-footer {
- margin-top: 10px;
-}
-
-.theme-studio__cards-table {
- display: flex;
- flex-direction: column;
-}
-
-.theme-studio__cards-table-head,
-.theme-studio__cards-table-row {
- display: grid;
- grid-template-columns: 18px 88px minmax(0, 1fr) auto;
- gap: 10px;
- align-items: center;
-}
-
-.theme-studio__cards-table-head {
- padding: 0 0 8px 0;
- font-size: 11px;
- color: var(--editor-muted-foreground);
-}
-
-.theme-studio__cards-table-row {
- padding: 9px 0;
- border-top: 1px solid var(--editor-border);
-}
-
-.theme-studio__cards-table-row .ty-checkbox {
- justify-self: start;
-}
-
-.theme-studio__cards-table-status,
-.theme-studio__cards-table-email {
- min-width: 0;
-}
-
-.theme-studio__cards-table-email {
- overflow: hidden;
- color: var(--editor-muted-foreground);
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.theme-studio__cards-people-list {
- display: flex;
- flex-direction: column;
-}
-
-.theme-studio__cards-people-list .theme-studio__member-row {
- padding-block: 10px;
- border-top: 1px solid var(--editor-border);
-}
-
-.theme-studio__cards-people-list .theme-studio__member-row:first-child {
- border-top: 0;
- padding-top: 0;
-}
-
-.theme-studio__cards-chat-head {
- margin-bottom: 2px;
-}
-
.theme-studio__member-row_compact {
padding: 0;
}
@@ -889,10 +778,7 @@
flex: 0 0 auto;
}
-.theme-studio__cards-member-select,
-.theme-studio__cards-inline-action,
-.theme-studio__cards-report-select,
-.theme-studio__cards-share-row .ty-btn {
+.theme-studio__cards-member-select {
width: auto !important;
flex: 0 0 auto;
}
@@ -901,15 +787,6 @@
min-width: 86px;
}
-.theme-studio__cards-report-select {
- flex: 1 1 0;
- min-width: 0;
-}
-
-.theme-studio__cards-inline-action.ty-btn {
- min-width: 78px;
-}
-
.theme-studio__cards-panel .ty-radio {
align-items: flex-start;
}
@@ -972,8 +849,7 @@
.theme-studio__dashboard-sidebar.ty-card,
.theme-studio__mail-sidebar.ty-card,
.theme-studio__mail-panel.ty-card,
-.theme-studio__mail-detail.ty-card,
-.theme-studio__mail-compose-card.ty-card {
+.theme-studio__mail-detail.ty-card {
border-radius: var(--theme-studio-panel-radius);
}
@@ -1039,18 +915,17 @@
}
.theme-studio__dashboard-main,
-.theme-studio__mail-list {
+.theme-studio__mail-panel,
+.theme-studio__mail-detail,
+.theme-studio__mail-sidebar {
min-width: 0;
}
.theme-studio__dashboard-main {
- display: flex;
- flex-direction: column;
gap: var(--theme-studio-block-gap);
}
.theme-studio__dashboard-top {
- display: flex;
justify-content: space-between;
gap: 14px;
align-items: center;
@@ -1064,8 +939,6 @@
.theme-studio__mail-folder-list,
.theme-studio__mail-thread-list,
.theme-studio__pricing-feature-list {
- display: flex;
- flex-direction: column;
gap: 10px;
}
@@ -1092,13 +965,6 @@
color: color-mix(in srgb, var(--editor-sidebar-foreground), transparent 28%);
}
-.theme-studio__mail-sidebar-head,
-.theme-studio__mail-panel,
-.theme-studio__mail-detail {
- display: flex;
- flex-direction: column;
-}
-
.theme-studio__mail-sidebar-head {
gap: 14px;
padding-bottom: 2px;
@@ -1148,8 +1014,6 @@
}
.theme-studio__mail-message-meta {
- display: flex;
- align-items: center;
gap: 12px;
min-width: 0;
}
@@ -1195,8 +1059,6 @@
}
.theme-studio__mail-labels {
- display: flex;
- flex-direction: column;
gap: 10px;
padding-top: 8px;
border-top: 1px solid color-mix(in srgb, var(--editor-sidebar-border), transparent 25%);
@@ -1247,7 +1109,6 @@
}
.theme-studio__mail-message-head {
- display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
@@ -1303,27 +1164,6 @@
color: var(--editor-primary-foreground);
}
-.theme-studio__mail-compose-card {
- margin-top: auto;
- padding-top: 18px;
- border-top: 1px solid var(--editor-border);
-}
-
-.theme-studio__mail-compose-card.ty-card {
- padding-top: 0;
- border-top: 0;
- background: color-mix(in srgb, var(--editor-card), var(--editor-base) 10%);
-}
-
-.theme-studio__mail-compose-card.ty-card > .ty-card__body {
- padding: 14px;
-}
-
-.theme-studio__mail-compose-card .ty-textarea,
-.theme-studio__mail-compose-card .ty-textarea__inner {
- margin-bottom: 12px;
-}
-
.theme-studio__pricing-grid,
.theme-studio__palette-grid,
.theme-studio__summary-grid {
@@ -1340,7 +1180,6 @@
}
.theme-studio__pricing-hero {
- display: flex;
justify-content: space-between;
gap: 18px;
align-items: flex-start;
@@ -1382,15 +1221,11 @@
}
.theme-studio__pricing-card-head {
- display: flex;
- align-items: center;
gap: 8px;
min-height: 24px;
}
.theme-studio__pricing-price {
- display: flex;
- flex-direction: column;
gap: 6px;
}
@@ -1399,8 +1234,6 @@
}
.theme-studio__pricing-feature-list {
- display: flex;
- flex-direction: column;
gap: 10px;
margin: 0;
}
@@ -1415,14 +1248,10 @@
}
.theme-studio__faq-list {
- display: flex;
- flex-direction: column;
gap: 14px;
}
.theme-studio__faq-item {
- display: flex;
- flex-direction: column;
gap: 6px;
padding-bottom: 14px;
border-bottom: 1px solid var(--editor-border);
@@ -1467,11 +1296,6 @@
justify-content: space-between;
}
-.theme-studio__chart-dot_large {
- width: 24px;
- height: 24px;
-}
-
.theme-studio__code-head {
justify-content: space-between;
align-items: center;
@@ -1538,7 +1362,8 @@
}
.theme-studio__cards-scene,
- .theme-studio__cards-top-grid {
+ .theme-studio__cards-top-pair,
+ .theme-studio__cards-bottom-pair {
grid-template-columns: 1fr;
}
}
@@ -1551,7 +1376,6 @@
.theme-studio__topbar-primary,
.theme-studio__topbar-actions {
align-items: flex-start;
- flex-direction: column;
}
.theme-studio__group-toolbar,
diff --git a/apps/docs/src/locale/en_US.ts b/apps/docs/src/locale/en_US.ts
index ac78208ab..b81cddc06 100644
--- a/apps/docs/src/locale/en_US.ts
+++ b/apps/docs/src/locale/en_US.ts
@@ -10,24 +10,29 @@ const en_US: SiteLocale = {
pro: 'Pro',
},
home: {
- subtitle: 'A Friendly UI Component Set for React',
+ badge: 'Tiny UI for React products',
+ heroTitle: 'Build polished React products',
+ heroAccent: 'not just component demos',
+ subtitle: 'A React design system for shipping polished products with flexible theming and reliable composability.',
getStarted: 'Get Started',
+ browseComponents: 'Browse Components',
github: 'GitHub',
- designPrinciple: 'Design Principle',
+ designPrinciple: 'Built for teams that ship',
+ designPrincipleDesc: 'Tiny UI is opinionated where consistency helps and flexible where product teams need room to move.',
componentCategories: 'Component Categories',
nComponents: (count) => `${count} components`,
features: {
- themeable: 'Themeable',
+ themeable: 'Ship faster',
themeableDesc:
- 'Quickly and easily reference values from your theme throughout your entire application with any components.',
- elegant: 'Elegant',
- elegantDesc: 'Thanks for React Hook, the source code is more light weight.',
- composable: 'Composable',
+ 'Consistent APIs across forms, navigation, data display, and feedback help teams move from first screen to shipped product with less friction.',
+ elegant: 'Theme visually',
+ elegantDesc: 'Preview presets instantly, adjust tokens in Theme Studio, and carry one visual system across the whole product surface.',
+ composable: 'Compose freely',
composableDesc:
- 'Completely customisable for all components. You can leverage any component to create new things.',
- accessible: 'Accessible',
+ 'Use components as reliable building blocks, then combine them into dashboards, admin flows, onboarding, and content-heavy screens.',
+ accessible: 'Accessible by default',
accessibleDesc:
- 'Strictly follows WAI-ARIA standards. All components come with proper attributes and keyboard interactions.',
+ 'Keyboard support, semantic structure, and sensible interaction defaults are built in so polish does not come at the cost of usability.',
},
stats: {
components: 'Components',
@@ -36,19 +41,79 @@ const en_US: SiteLocale = {
license: 'License',
},
themeShowcase: 'Make It Yours',
- themeShowcaseDesc: 'Choose a preset theme and watch the entire site transform instantly.',
+ themeShowcaseDesc: 'Choose a preset, preview the shift instantly, then fine-tune every token in Theme Studio.',
themeShowcaseCustomize: 'Customize in Theme Editor',
showcase: {
- title: 'Components in Action',
- subtitle: 'Real components, rendered live. What you see is what you ship.',
+ title: 'From primitives to product surfaces',
+ subtitle: 'Tiny UI is strongest when components work together inside real dashboards, forms, and operational screens.',
},
codeExample: {
- title: 'Delightful Developer Experience',
- subtitle: 'Clean APIs. Predictable patterns. Ship in minutes, not hours.',
+ title: 'A workflow that stays out of the way',
+ subtitle: 'Readable APIs, predictable patterns, and live feedback keep the path from install to shipped screen short.',
+ },
+ preview: {
+ workspace: 'Product workspace',
+ overview: 'Overview',
+ components: 'Components',
+ settings: 'Settings',
+ activeTheme: 'Active theme',
+ themeLabel: 'Preset',
+ themeValue: 'Studio Bloom',
+ tokensLabel: 'Tokens',
+ tokensValue: '148 synced',
+ themeHint: 'Switch presets quickly, then refine the details in Theme Studio.',
+ editTheme: 'Edit theme',
+ search: 'Search dashboards, forms, and docs',
+ metrics: {
+ velocity: 'Velocity',
+ velocitySuffix: 'releases',
+ adoption: 'Adoption',
+ satisfaction: 'Satisfaction',
+ },
+ pipeline: 'Release pipeline',
+ live: 'Live',
+ activity: 'Recent activity',
+ activityTag: 'Healthy',
+ analytics: 'Analytics',
+ checkout: 'Checkout flow',
+ table: {
+ project: 'Project',
+ owner: 'Owner',
+ status: 'Status',
+ },
+ form: {
+ name: 'Workspace name',
+ email: 'team@company.com',
+ updates: 'Email product updates',
+ submit: 'Launch workspace',
+ },
+ },
+ scenarios: {
+ dashboard: {
+ kicker: 'Dashboard',
+ title: 'Metrics that feel product-ready',
+ desc: 'Cards, charts, tables, tabs, and badges align into dashboards that look intentional from the first pass.',
+ },
+ forms: {
+ kicker: 'Forms',
+ title: 'Flows that stay consistent',
+ desc: 'Inputs, validation states, actions, and helper text follow the same logic across onboarding, settings, and checkout.',
+ },
+ content: {
+ kicker: 'Content',
+ title: 'Interfaces that still feel alive',
+ desc: 'Use overlays, keyboard patterns, progress, and avatar systems to add polish without sacrificing clarity.',
+ },
+ },
+ coverage: {
+ title: 'Coverage across the product surface',
+ subtitle: 'A broad component set matters most when the pieces share one visual and behavioral language.',
+ summary: 'More than a grab bag of isolated widgets. Tiny UI covers the common product surface area with one cohesive system.',
+ cta: 'Explore all components',
},
cta: {
- title: 'Start Building Today',
- subtitle: 'Install Tiny UI and have your first component on screen in under a minute.',
+ title: 'Start with the basics. Scale into a full design system.',
+ subtitle: 'Install Tiny UI, ship your first screen quickly, and keep growing without rebuilding your visual language.',
install: 'npm install @tiny-design/react',
copied: 'Copied!',
readDocs: 'Read the Docs',
diff --git a/apps/docs/src/locale/zh_CN.ts b/apps/docs/src/locale/zh_CN.ts
index 3b372edd5..45efb8b01 100644
--- a/apps/docs/src/locale/zh_CN.ts
+++ b/apps/docs/src/locale/zh_CN.ts
@@ -10,21 +10,26 @@ const zh_CN: SiteLocale = {
pro: 'Pro',
},
home: {
- subtitle: '一套友好的 React UI 组件库',
+ badge: '面向 React 产品界面的 Tiny UI',
+ heroTitle: '构建更完整的 React 产品界面',
+ heroAccent: '而不只是组件示例',
+ subtitle: '一套面向真实交付的 React 设计系统,兼顾主题定制、稳定组合能力与完成度。',
getStarted: '快速开始',
+ browseComponents: '浏览组件',
github: 'GitHub',
- designPrinciple: '设计理念',
+ designPrinciple: '为真正交付产品而设计',
+ designPrincipleDesc: '该统一的地方保持统一,该灵活的地方保留空间,让团队既能提速,也不会被框死。',
componentCategories: '组件分类',
nComponents: (count) => `${count} 个组件`,
features: {
- themeable: '可定制主题',
- themeableDesc: '轻松引用主题中的值,快速在整个应用中使用任何组件。',
- elegant: '优雅轻量',
- elegantDesc: '得益于 React Hook,源码更加轻量简洁。',
- composable: '灵活组合',
- composableDesc: '所有组件均可完全自定义,你可以利用任何组件创造新事物。',
- accessible: '无障碍',
- accessibleDesc: '严格遵循 WAI-ARIA 标准,所有组件都具备正确的属性和键盘交互。',
+ themeable: '更快交付',
+ themeableDesc: '表单、导航、数据展示与反馈组件拥有一致 API,让团队从首屏到上线都更顺滑。',
+ elegant: '可视化主题定制',
+ elegantDesc: '预设主题可即时预览,也能在 Theme Studio 里继续微调 token,把整套视觉语言统一起来。',
+ composable: '自由组合',
+ composableDesc: '组件不是孤立 demo,而是可以组合成仪表盘、后台流程、引导页和内容型界面的稳定积木。',
+ accessible: '默认无障碍',
+ accessibleDesc: '键盘交互、语义结构与合理的默认行为内建其中,让精致体验不以可用性为代价。',
},
stats: {
components: '组件',
@@ -33,19 +38,79 @@ const zh_CN: SiteLocale = {
license: '开源协议',
},
themeShowcase: '定制你的主题',
- themeShowcaseDesc: '选择一个预设主题,即刻改变整个网站的外观。',
+ themeShowcaseDesc: '选择一个预设主题立即预览变化,再进入 Theme Studio 微调每一个 token。',
themeShowcaseCustomize: '在主题编辑器中自定义',
showcase: {
- title: '组件实战',
- subtitle: '真实组件,实时渲染。所见即所得。',
+ title: '从基础组件到完整产品界面',
+ subtitle: 'Tiny UI 的价值不只在单个组件,而在于它们组合后能快速形成真实、统一的产品界面。',
},
codeExample: {
- title: '愉悦的开发体验',
- subtitle: '简洁的 API,可预测的模式。几分钟内即可交付。',
+ title: '不打断思路的开发体验',
+ subtitle: '清晰的 API、可预测的模式和即时反馈,让你从安装到交付保持顺手。',
+ },
+ preview: {
+ workspace: '产品工作区',
+ overview: '概览',
+ components: '组件',
+ settings: '设置',
+ activeTheme: '当前主题',
+ themeLabel: '预设',
+ themeValue: 'Studio Bloom',
+ tokensLabel: 'Tokens',
+ tokensValue: '148 项已同步',
+ themeHint: '先快速切换预设,再进入 Theme Studio 调整细节。',
+ editTheme: '编辑主题',
+ search: '搜索仪表盘、表单与文档',
+ metrics: {
+ velocity: '交付速度',
+ velocitySuffix: '次发布',
+ adoption: '采用率',
+ satisfaction: '满意度',
+ },
+ pipeline: '发布流程',
+ live: '实时',
+ activity: '最近动态',
+ activityTag: '稳定',
+ analytics: '分析',
+ checkout: '结算流程',
+ table: {
+ project: '项目',
+ owner: '负责人',
+ status: '状态',
+ },
+ form: {
+ name: '工作区名称',
+ email: 'team@company.com',
+ updates: '接收产品更新',
+ submit: '创建工作区',
+ },
+ },
+ scenarios: {
+ dashboard: {
+ kicker: '仪表盘',
+ title: '指标界面更像产品,不像 demo',
+ desc: '卡片、图表、表格、标签页与状态标签可以自然拼成有层次的后台界面。',
+ },
+ forms: {
+ kicker: '表单',
+ title: '流程前后一致',
+ desc: '输入框、校验状态、操作按钮与辅助信息遵循同一套逻辑,适合引导、设置与结算流程。',
+ },
+ content: {
+ kicker: '内容与交互',
+ title: '界面清楚,也足够生动',
+ desc: '覆盖浮层、快捷键、进度和头像等细节能力,在不牺牲清晰度的前提下提升完成度。',
+ },
+ },
+ coverage: {
+ title: '覆盖常见产品界面能力',
+ subtitle: '组件数量重要,但更重要的是它们共享同一套视觉与交互语言。',
+ summary: 'Tiny UI 不是一堆零散小部件,而是一套可以覆盖常见产品表面的统一系统。',
+ cta: '查看全部组件',
},
cta: {
- title: '立即开始构建',
- subtitle: '安装 Tiny UI,一分钟内即可在屏幕上看到你的第一个组件。',
+ title: '从基础开始,扩展成完整设计系统',
+ subtitle: '安装 Tiny UI,快速搭出第一屏,并在后续迭代中持续沿用同一套视觉语言。',
install: 'npm install @tiny-design/react',
copied: '已复制!',
readDocs: '阅读文档',
diff --git a/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-horizontal-popup-path-selected.png b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-horizontal-popup-path-selected.png
new file mode 100644
index 000000000..9d31a898d
Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-horizontal-popup-path-selected.png differ
diff --git a/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-inline-navigation.png b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-inline-navigation.png
new file mode 100644
index 000000000..b707a5203
Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-inline-navigation.png differ
diff --git a/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-local-contrast-theme.png b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-local-contrast-theme.png
new file mode 100644
index 000000000..f78c43f77
Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-local-contrast-theme.png differ
diff --git a/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-variants-default.png b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-variants-default.png
new file mode 100644
index 000000000..2207d8614
Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-variants-default.png differ
diff --git a/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-vertical-popup-selected.png b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-vertical-popup-selected.png
new file mode 100644
index 000000000..5faa74c4f
Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/menu.visual.spec.ts/menu-vertical-popup-selected.png differ
diff --git a/apps/docs/tests/visual/menu.visual.spec.ts b/apps/docs/tests/visual/menu.visual.spec.ts
new file mode 100644
index 000000000..21b56eebd
--- /dev/null
+++ b/apps/docs/tests/visual/menu.visual.spec.ts
@@ -0,0 +1,59 @@
+import { expect, test } from '@playwright/test';
+import { gotoComponent, previewByTitle, scrollDemoIntoView } from './helpers';
+
+test.describe('menu visual states', () => {
+ test('inline navigation preserves leaf selection and parent path highlight', async ({ page }) => {
+ await gotoComponent(page, 'menu');
+ await scrollDemoIntoView(page, 'Inline Navigation');
+ await expect(previewByTitle(page, 'Inline Navigation')).toHaveScreenshot('menu-inline-navigation.png');
+ });
+
+ test('variants demo default state keeps path-selected in vertical mode', async ({ page }) => {
+ await gotoComponent(page, 'menu');
+ await scrollDemoIntoView(page, 'Variants And Selection Styles');
+ await expect(previewByTitle(page, 'Variants And Selection Styles')).toHaveScreenshot('menu-variants-default.png');
+ });
+
+ test('local contrast theme keeps selected contrast inside dark surface', async ({ page }) => {
+ await gotoComponent(page, 'menu');
+ await scrollDemoIntoView(page, 'Local Contrast Theme');
+ await expect(previewByTitle(page, 'Local Contrast Theme')).toHaveScreenshot('menu-local-contrast-theme.png');
+ });
+
+ test('horizontal popup keeps nested path and selected emphasis', async ({ page }) => {
+ await gotoComponent(page, 'menu');
+ const demo = await scrollDemoIntoView(page, 'Top Navigation');
+ const resources = demo.locator('.ty-menu-sub__title').filter({ hasText: 'Resources' });
+ const visiblePopup = () => page.locator('.ty-menu-sub__list_popup:visible');
+ const community = () =>
+ page.locator('.ty-menu-sub__list_popup:visible .ty-menu-sub__title').filter({ hasText: 'Community' }).last();
+ const showcase = () =>
+ page.locator('.ty-menu-sub__list_popup:visible .ty-menu-item').filter({ hasText: 'Showcase' }).last();
+
+ await resources.hover();
+ await expect(visiblePopup().first()).toBeVisible();
+ await community().hover();
+ await expect(showcase()).toBeVisible();
+ await showcase().click();
+
+ await page.mouse.move(8, 8);
+ await expect(visiblePopup()).toHaveCount(0);
+ await resources.hover();
+ await expect(visiblePopup().first()).toBeVisible();
+ await expect(community()).toBeVisible();
+ await community().hover();
+ await expect(showcase()).toBeVisible();
+ await expect(page).toHaveScreenshot('menu-horizontal-popup-path-selected.png');
+ });
+
+ test('vertical popup keeps reopened selected item highlight', async ({ page }) => {
+ await gotoComponent(page, 'menu');
+ const demo = await scrollDemoIntoView(page, 'Vertical Navigation');
+ await demo.locator('.ty-menu-sub__title').filter({ hasText: 'Customers' }).hover();
+ await page.waitForTimeout(200);
+ await page.locator('.ty-menu-sub__list_popup .ty-menu-item').filter({ hasText: 'Segments' }).click();
+ await demo.locator('.ty-menu-sub__title').filter({ hasText: 'Customers' }).hover();
+ await page.waitForTimeout(200);
+ await expect(page).toHaveScreenshot('menu-vertical-popup-selected.png');
+ });
+});
diff --git a/apps/pro/.next/trace b/apps/pro/.next/trace
new file mode 100644
index 000000000..03d64deda
--- /dev/null
+++ b/apps/pro/.next/trace
@@ -0,0 +1 @@
+[{"name":"next-dev","duration":3909621370,"timestamp":1210455661854,"id":1,"tags":{},"startTime":1775818145022,"traceId":"90f9ef1c40658dd9"}]
diff --git a/package.json b/package.json
index 0767032b2..fbe3fb3e4 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"lint-staged": "^15.0.0",
"postcss": "^8.4.0",
"prettier": "^3.0.0",
+ "resolve": "^1.22.11",
"stylelint": "^16.0.0",
"stylelint-config-standard-scss": "^14.0.0",
"turbo": "^2.8.16",
diff --git a/packages/charts/src/__tests__/charts.test.tsx b/packages/charts/src/__tests__/charts.test.tsx
index 1df1ed99f..577925bed 100644
--- a/packages/charts/src/__tests__/charts.test.tsx
+++ b/packages/charts/src/__tests__/charts.test.tsx
@@ -79,7 +79,7 @@ describe('charts package', () => {
);
expect(warn).toHaveBeenCalledWith(
- 'ChartContainer already includes Recharts ResponsiveContainer. Pass the chart element directly instead of nesting another
.'
+ 'ChartContainer manages chart sizing itself. Pass the chart element directly instead of nesting another
.'
);
});
@@ -93,7 +93,7 @@ describe('charts package', () => {
);
expect(warn).toHaveBeenCalledWith(
- 'Chart config key "total revenue" contains characters that are unsafe for CSS custom properties. Use letters, numbers, "_" or "-".'
+ 'Chart config key "total revenue" contains characters that are unsafe for CSS custom properties. Use letters, numbers, "_" or "-". Theme colors for this key will not be injected.'
);
});
@@ -126,6 +126,41 @@ describe('charts package', () => {
expect(screen.getByText('320')).toBeInTheDocument();
});
+ it('maps tooltip series and label keys from the original payload data', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('April 2026')).toBeInTheDocument();
+ expect(screen.getByText('Revenue')).toBeInTheDocument();
+ expect(screen.getByText('320')).toBeInTheDocument();
+ });
+
it('does not forward recharts-only tooltip props onto the DOM', () => {
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
@@ -182,4 +217,97 @@ describe('charts package', () => {
expect(screen.getByText('Mobile Revenue')).toBeInTheDocument();
});
+
+ it('maps legend names from payload data when nameKey is provided', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Chrome Browser')).toBeInTheDocument();
+ });
+
+ it('renders theme-based color styles for dark mode aware configs', () => {
+ render(
+
+ chart
+
+ );
+
+ const styleTag = document.querySelector('style');
+ expect(styleTag?.textContent).toContain('--color-revenue: #111111;');
+ expect(styleTag?.textContent).toContain('html[data-tiny-theme=dark] [data-chart=');
+ expect(styleTag?.textContent).toContain('--color-revenue: #eeeeee;');
+ });
+
+ describe('when ResizeObserver is unavailable', () => {
+ const originalResizeObserver = window.ResizeObserver;
+
+ beforeEach(() => {
+ Object.defineProperty(window, 'ResizeObserver', {
+ writable: true,
+ configurable: true,
+ value: undefined,
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, 'ResizeObserver', {
+ writable: true,
+ configurable: true,
+ value: originalResizeObserver,
+ });
+ });
+
+ it('uses fallbackSize before resize measurement is available', () => {
+ render(
+
+ chart
+
+ );
+
+ expect(screen.getByText('chart')).toBeInTheDocument();
+ });
+
+ it('falls back to window resize events', () => {
+ render(
+
+ chart
+
+ );
+
+ expect(screen.getByText('chart')).toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/charts/src/chart-container.tsx b/packages/charts/src/chart-container.tsx
index aafa70e5f..7616b4c47 100644
--- a/packages/charts/src/chart-container.tsx
+++ b/packages/charts/src/chart-container.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { ChartContextProvider } from './chart-context';
import { ChartStyle } from './chart-style';
@@ -6,8 +6,9 @@ import { ChartConfig } from './types';
const PREFIX = 'ty-chart';
const SAFE_COLOR_KEY = /^[A-Za-z0-9_-]+$/;
-const DEFAULT_MIN_HEIGHT = 200;
const DEFAULT_INITIAL_SIZE = { width: 0, height: 0 };
+const useIsomorphicLayoutEffect =
+ typeof window === 'undefined' ? useEffect : useLayoutEffect;
let hasWarnedAboutResponsiveContainer = false;
let warnedConfigKeys = new Set
();
@@ -15,32 +16,41 @@ let warnedConfigKeys = new Set();
export interface ChartContainerProps extends React.HTMLAttributes {
config: ChartConfig;
children: React.ReactElement;
+ fallbackSize?: {
+ width: number;
+ height: number;
+ };
}
/**
* ChartContainer wraps Recharts charts with:
- * 1. A ResponsiveContainer for auto-sizing
+ * 1. Measured width/height injected into the chart element
* 2. CSS custom properties (--color-KEY) injected via inline styles
* 3. ChartConfig provided via React context for tooltip/legend
*/
const ChartContainer = React.forwardRef(
- ({ config, children, className, style, id, ...props }, ref) => {
+ ({ config, children, className, style, id, fallbackSize, ...props }, ref) => {
const uniqueId = useId();
const chartId = id || `chart-${uniqueId}`;
const containerRef = useRef(null);
- const [size, setSize] = useState(DEFAULT_INITIAL_SIZE);
-
- if (
- process.env.NODE_ENV !== 'production' &&
- !hasWarnedAboutResponsiveContainer &&
- React.isValidElement(children) &&
- typeof children.type !== 'string' &&
- (children.type as { displayName?: string; name?: string }).displayName === 'ResponsiveContainer'
- ) {
- hasWarnedAboutResponsiveContainer = true;
- console.warn(
- 'ChartContainer already includes Recharts ResponsiveContainer. Pass the chart element directly instead of nesting another .'
- );
+ const [size, setSize] = useState(() => fallbackSize || DEFAULT_INITIAL_SIZE);
+
+ if (process.env.NODE_ENV !== 'production' && !hasWarnedAboutResponsiveContainer) {
+ if (
+ React.isValidElement(children) &&
+ typeof children.type !== 'string'
+ ) {
+ const childName =
+ (children.type as { displayName?: string; name?: string }).displayName ||
+ (children.type as { displayName?: string; name?: string }).name ||
+ '';
+ if (childName === 'ResponsiveContainer') {
+ hasWarnedAboutResponsiveContainer = true;
+ console.warn(
+ 'ChartContainer manages chart sizing itself. Pass the chart element directly instead of nesting another .'
+ );
+ }
+ }
}
// Build --color-KEY CSS custom properties from config
@@ -54,7 +64,7 @@ const ChartContainer = React.forwardRef(
) {
warnedConfigKeys.add(key);
console.warn(
- `Chart config key "${key}" contains characters that are unsafe for CSS custom properties. Use letters, numbers, "_" or "-".`
+ `Chart config key "${key}" contains characters that are unsafe for CSS custom properties. Use letters, numbers, "_" or "-". Theme colors for this key will not be injected.`
);
}
@@ -65,30 +75,24 @@ const ChartContainer = React.forwardRef(
return vars;
}, [config]);
- useEffect(() => {
+ const updateSize = useCallback((width: number, height: number) => {
+ setSize((prevSize) => {
+ const nextWidth = Math.max(0, Math.round(width));
+ const nextHeight = Math.max(0, Math.round(height));
+
+ if (prevSize.width === nextWidth && prevSize.height === nextHeight) {
+ return prevSize;
+ }
+
+ return { width: nextWidth, height: nextHeight };
+ });
+ }, []);
+
+ useIsomorphicLayoutEffect(() => {
if (!containerRef.current || typeof ResizeObserver === 'undefined') {
return undefined;
}
- const updateSize = (width: number, height: number) => {
- setSize((prevSize) => {
- const nextWidth = Math.max(0, Math.round(width));
- const nextHeight = Math.max(0, Math.round(height));
-
- if (
- prevSize.width === nextWidth &&
- prevSize.height === nextHeight
- ) {
- return prevSize;
- }
-
- return {
- width: nextWidth,
- height: nextHeight,
- };
- });
- };
-
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) {
@@ -105,7 +109,29 @@ const ChartContainer = React.forwardRef(
return () => {
observer.disconnect();
};
- }, []);
+ }, [updateSize]);
+
+ useEffect(() => {
+ if (!containerRef.current || typeof ResizeObserver !== 'undefined') {
+ return undefined;
+ }
+
+ const updateFromRect = () => {
+ if (!containerRef.current) {
+ return;
+ }
+
+ const rect = containerRef.current.getBoundingClientRect();
+ updateSize(rect.width, rect.height);
+ };
+
+ updateFromRect();
+ window.addEventListener('resize', updateFromRect);
+
+ return () => {
+ window.removeEventListener('resize', updateFromRect);
+ };
+ }, [updateSize]);
const composedRef = (node: HTMLDivElement | null) => {
containerRef.current = node;
diff --git a/packages/charts/src/chart-legend.tsx b/packages/charts/src/chart-legend.tsx
index 6eff1c0a8..2dc55fd94 100644
--- a/packages/charts/src/chart-legend.tsx
+++ b/packages/charts/src/chart-legend.tsx
@@ -3,6 +3,7 @@ import classNames from 'classnames';
import type { LegendPayload, LegendProps as RechartsLegendProps } from 'recharts';
import { Legend as RechartsLegend } from 'recharts';
import { useChart } from './chart-context';
+import { getPayloadValue } from './utils';
const PREFIX = 'ty-chart-legend';
@@ -64,11 +65,10 @@ export const ChartLegendContent = React.forwardRef<
{...props}
>
{payload.map((entry) => {
- const key = nameKey
- ? (entry.dataKey || entry.value)
- : entry.dataKey || entry.value;
- const itemConfig = key ? config[key] : undefined;
- const displayName = itemConfig?.label || entry.value || key;
+ const payloadNameKey = typeof nameKey === 'string' ? getPayloadValue(entry.payload, nameKey) : undefined;
+ const key = payloadNameKey || entry.dataKey || entry.value;
+ const itemConfig = typeof key === 'string' ? config[key] : undefined;
+ const displayName = itemConfig?.label || payloadNameKey || entry.value || key;
const color =
entry.color || (itemConfig?.color ? `var(--color-${key})` : undefined);
diff --git a/packages/charts/src/chart-style.tsx b/packages/charts/src/chart-style.tsx
index a9fcdaeb5..b34ef17bf 100644
--- a/packages/charts/src/chart-style.tsx
+++ b/packages/charts/src/chart-style.tsx
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { ChartConfig } from './types';
const THEMES = { light: '', dark: 'html[data-tiny-theme=dark]' } as const;
+const SAFE_COLOR_KEY = /^[A-Za-z0-9_-]+$/;
/**
* Generates an inline