diff --git a/.changeset/config.json b/.changeset/config.json index 085f2894..20fb4565 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -14,5 +14,5 @@ "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", - "ignore": ["@tiny-design/docs", "@tiny-design/extract", "@tiny-design/pro"] + "ignore": ["@tiny-design/docs", "@tiny-design/extract"] } diff --git a/.changeset/fix-scss-imports-and-popup.md b/.changeset/fix-scss-imports-and-popup.md new file mode 100644 index 00000000..810863ee --- /dev/null +++ b/.changeset/fix-scss-imports-and-popup.md @@ -0,0 +1,6 @@ +--- +"@tiny-design/react": patch +"@tiny-design/tokens": patch +--- + +Migrate component SCSS imports from @tiny-design/tokens to local style/variables and fix Popup positioning sync diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 2f15da6c..4a1f1c96 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -46,14 +46,6 @@ jobs: - name: Copy 404.html for SPA routing run: cp apps/docs/build/index.html apps/docs/build/404.html - - name: Build pro site - run: pnpm --filter @tiny-design/pro build - env: - NEXT_PUBLIC_BASE_PATH: /tiny-design/pro - - - name: Merge pro into docs output - run: cp -r apps/pro/out/ apps/docs/build/pro/ - - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/.gitignore b/.gitignore index bfcf5c6b..b948a336 100755 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ es lib coverage apps/docs/build +apps/docs/test-results +apps/docs/playwright-report # Turbo .turbo diff --git a/CLAUDE.md b/CLAUDE.md index 481821b2..769786dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Tiny Design is a React component UI library (80+ components) published as `@tiny ``` packages/react/ → @tiny-design/react (main component library) -packages/tokens/ → @tiny-design/tokens (design tokens, SCSS variables) +packages/tokens/ → @tiny-design/tokens (v2 design tokens and theme runtime) packages/icons/ → @tiny-design/icons (SVG icon components) apps/docs/ → documentation site (Vite + MDX) ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ebb2de9..70996481 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ The package root barrel must also re-export those public component types from `p - TypeScript strict mode is enabled - Components use `React.forwardRef` for ref forwarding - CSS class names follow the `ty-component-name` convention -- SCSS variables are in `packages/react/src/style/_variables.scss` +- Shared Sass helpers for React component styles live in `packages/react/src/style/` ## Running Tests diff --git a/README.md b/README.md index 235278ea..733a91eb 100755 --- a/README.md +++ b/README.md @@ -104,24 +104,14 @@ Every visual token is a `--ty-*` CSS custom property. Override any of them: } ``` -### SCSS variables - -For compile-time control, override `!default` SCSS variables before importing: - -```scss -$primary-color: #3b82f6; -$border-radius: 8px; -@use "@tiny-design/react/es/style/index" as *; -``` - -See the [Theming Guide](https://wangdicoder.github.io/tiny-design/theme/customise-theme) for the full list of tokens and variables. +See the [Theming Guide](https://wangdicoder.github.io/tiny-design/theme/customise-theme) for the full token list and Theme Studio workflow. ## Packages | Package | Description | | ------- | ----------- | | [@tiny-design/react](./packages/react) | Core component library | -| [@tiny-design/tokens](./packages/tokens) | Design tokens and foundational styles | +| [@tiny-design/tokens](./packages/tokens) | V2 design tokens and theme runtime | | [@tiny-design/icons](./packages/icons) | SVG icon components | | [@tiny-design/cli](./packages/cli) | CLI for the Tiny Design component library | | [@tiny-design/mcp](./packages/mcp) | MCP server for AI assistants to access the component library | diff --git a/_template/style/_index.scss b/_template/style/_index.scss index 3fd5e4ee..222751ba 100644 --- a/_template/style/_index.scss +++ b/_template/style/_index.scss @@ -1 +1 @@ -@use '@tiny-design/tokens/scss/variables' as *; +@use '../../style/variables' as *; diff --git a/apps/docs/guides/customise-theme.md b/apps/docs/guides/customise-theme.md index bb9e1017..996d13d3 100755 --- a/apps/docs/guides/customise-theme.md +++ b/apps/docs/guides/customise-theme.md @@ -1,13 +1,11 @@ # Customise Theme -Tiny UI now uses a v2 token system with one primary runtime model: +Tiny UI uses a token-driven runtime theme model: 1. **CSS custom properties** for direct runtime overrides. 2. **ThemeDocument** for portable theme JSON. 3. **ConfigProvider `theme.tokens`** for React-scoped theming. -SCSS constants still exist, but they are only for compile-time structural overrides. - ## Theme Editor The built-in [Theme Editor](/theme/theme-studio) lets you visually customise design tokens in real time. You can: @@ -20,7 +18,7 @@ The built-in [Theme Editor](/theme/theme-studio) lets you visually customise des Changes are applied instantly via CSS custom properties — no rebuild required. -The editor exports the same v2 token model used by the runtime, so the result can be applied as CSS variables, stored as a `ThemeDocument`, or passed into `ConfigProvider`. +The editor exports the same token model used by the runtime, so the result can be applied as CSS variables, stored as a `ThemeDocument`, or passed into `ConfigProvider`. ## Dark mode @@ -102,13 +100,13 @@ html[data-tiny-theme='dark'] { | `--ty-color-text` | `rgba(0,0,0,0.85)` | Primary text colour | | `--ty-color-text-secondary` | `rgba(0,0,0,0.65)` | Secondary text colour | | `--ty-color-border` | `#d9d9d9` | Default border colour | -| `--ty-border-radius` | `2px` | Global border radius | -| `--ty-font-size-base` | `1rem` | Base font size | +| `--ty-border-radius` | `6px` | Global border radius | +| `--ty-font-size-base` | `14px` | Base font size | | `--ty-height-sm` | `24px` | Small control height | -| `--ty-height-md` | `32px` | Medium control height | -| `--ty-height-lg` | `42px` | Large control height | +| `--ty-height-md` | `35px` | Medium control height | +| `--ty-height-lg` | `44px` | Large control height | -Every component also has its own tokens for fine-grained control. For example, Button uses `--ty-button-bg-default`, `--ty-button-text-default`, and `--ty-button-radius`. The full list of supported tokens is generated from the v2 registry and component sources: +Every component also has its own tokens for fine-grained control. For example, Button uses `--ty-button-bg-default`, `--ty-button-text-default`, and `--ty-button-radius`. The full list of supported tokens is generated from the token registry and component sources: - [Token registry](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/dist/registry.json) - [Component token sources](https://github.com/wangdicoder/tiny-design/tree/master/packages/tokens/source/components) @@ -178,65 +176,21 @@ Use this when you want: - nested theme overrides - popup / portal content to inherit the same token scope -`ConfigProvider` no longer uses the old `theme.token` or `theme.components` API. Use `theme.tokens.semantic` and `theme.tokens.components` only. - -## SCSS constants - -If you import Tiny UI's SCSS source instead of the pre-built CSS, you can override compile-time structural constants such as padding, transitions, and arrow sizes. These are values that don't need to change at runtime. - -Every constant uses the `!default` flag, so your overrides take precedence. - -### 1. Install Sass - -```bash -$ npm install sass --save-dev -``` - -### 2. Create your overrides file - -Create a file, e.g. `theme-overrides.scss`. Your overrides **must come before** the Tiny UI import: +Use `theme.tokens.semantic` and `theme.tokens.components` as the supported React theme shape. -```scss -// Override structural constants -$btn-padding-md: 0 20px; -$card-body-padding: 20px; -$tooltip-arrow-size: 6px; +## Sass source styles -// Import Tiny UI styles (applies your overrides via !default) -@use "@tiny-design/react/es/style/index" as *; -``` - -### 3. Import in your entry file - -```js -import './theme-overrides.scss'; -``` - -The full list of SCSS constants can be found in [_constants.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_constants.scss). +Tiny UI still ships Sass source files for bundlers that compile library styles directly, but Sass variables are not a theme API. Treat `@tiny-design/react/es/style/*.scss` and component `style/*.scss` files as implementation source, not as supported customisation contracts. -Some commonly overridden constants: - -```scss -// Button -$btn-padding-sm: 0 10px !default; -$btn-padding-md: 0 15px !default; -$btn-padding-lg: 0 28px !default; - -// Card -$card-header-padding: 13px 16px !default; -$card-body-padding: 16px !default; - -// Notification -$notification-width: 380px !default; -``` +Use tokens for visual changes such as colour, typography, radii, shadows, spacing, sizing, and component states. If a value is not exposed as a token yet, prefer adding it to the token registry instead of adding a new public Sass variable. -> **Note:** Colours, typography, radii, shadows, and all other visual tokens should be customised through v2 tokens, not SCSS variables. SCSS constants are only for compile-time structural values like padding and sizing. +The full token list is generated from `packages/tokens/dist/registry.json`. ## Recommended approach - Use CSS variables when you want the simplest runtime override. - Use `ThemeDocument` when you need a portable JSON theme format. - Use `ConfigProvider` when you need scoped theming in React. -- Use SCSS constants only when a value must be decided at build time. +- Avoid treating Sass variables as public theme configuration; add missing customisation points as tokens. -Please report an issue if the existing list of tokens or constants is not enough for you. +Please report an issue if the existing list of tokens is not enough for you. diff --git a/apps/docs/guides/customise-theme.zh_CN.md b/apps/docs/guides/customise-theme.zh_CN.md index 0269abdb..92205a2f 100644 --- a/apps/docs/guides/customise-theme.zh_CN.md +++ b/apps/docs/guides/customise-theme.zh_CN.md @@ -1,13 +1,11 @@ # 自定义主题 -Tiny UI 现在使用 v2 token 系统,推荐的主题自定义方式收敛为一套主模型: +Tiny UI 使用由 token 驱动的运行时主题模型: 1. **CSS 自定义属性**,用于直接做运行时覆盖。 2. **ThemeDocument**,用于可移植的主题 JSON。 3. **ConfigProvider `theme.tokens`**,用于 React 作用域主题。 -SCSS 常量仍然保留,但它只用于编译期的结构性覆盖。 - ## 主题编辑器 内置的[主题编辑器](/theme/theme-studio)让你可以实时可视化定制设计令牌。你可以: @@ -20,7 +18,7 @@ SCSS 常量仍然保留,但它只用于编译期的结构性覆盖。 更改通过 CSS 自定义属性即时生效 — 无需重新构建。 -主题编辑器导出的也是同一套 v2 token 模型,因此结果既可以作为 CSS 变量使用,也可以保存为 `ThemeDocument`,或者传给 `ConfigProvider`。 +主题编辑器导出的也是同一套运行时 token 模型,因此结果既可以作为 CSS 变量使用,也可以保存为 `ThemeDocument`,或者传给 `ConfigProvider`。 ## 暗色模式 @@ -102,13 +100,13 @@ html[data-tiny-theme='dark'] { | `--ty-color-text` | `rgba(0,0,0,0.85)` | 主文本色 | | `--ty-color-text-secondary` | `rgba(0,0,0,0.65)` | 次要文本色 | | `--ty-color-border` | `#d9d9d9` | 默认边框色 | -| `--ty-border-radius` | `2px` | 全局圆角 | -| `--ty-font-size-base` | `1rem` | 基础字号 | +| `--ty-border-radius` | `6px` | 全局圆角 | +| `--ty-font-size-base` | `14px` | 基础字号 | | `--ty-height-sm` | `24px` | 小尺寸控件高度 | -| `--ty-height-md` | `32px` | 中尺寸控件高度 | -| `--ty-height-lg` | `42px` | 大尺寸控件高度 | +| `--ty-height-md` | `35px` | 中尺寸控件高度 | +| `--ty-height-lg` | `44px` | 大尺寸控件高度 | -每个组件也有自己的令牌,用于细粒度控制。例如,Button 使用 `--ty-button-bg-default`、`--ty-button-text-default`、`--ty-button-radius`。完整的受支持令牌列表来自 v2 registry 和组件 source: +每个组件也有自己的令牌,用于细粒度控制。例如,Button 使用 `--ty-button-bg-default`、`--ty-button-text-default`、`--ty-button-radius`。完整的受支持令牌列表来自 token registry 和组件 source: - [Token registry](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/dist/registry.json) - [组件 token 源文件](https://github.com/wangdicoder/tiny-design/tree/master/packages/tokens/source/components) @@ -178,65 +176,21 @@ import { ConfigProvider } from '@tiny-design/react'; - 需要嵌套主题覆盖 - 需要让弹层 / portal 内容继承同一个 token 作用域 -`ConfigProvider` 已经不再使用旧的 `theme.token` 或 `theme.components` API。现在只使用 `theme.tokens.semantic` 和 `theme.tokens.components`。 - -## SCSS 常量 - -如果你引入的是 Tiny UI 的 SCSS 源文件而非预编译的 CSS,可以覆盖编译时结构常量,如内边距、过渡动画和箭头尺寸。这些是不需要在运行时变化的值。 - -每个常量都使用了 `!default` 标志,因此你的覆盖值会优先生效。 - -### 1. 安装 Sass - -```bash -$ npm install sass --save-dev -``` - -### 2. 创建覆盖文件 - -创建一个文件,例如 `theme-overrides.scss`。你的覆盖值**必须写在** Tiny UI 引入语句之前: +`theme.tokens.semantic` 和 `theme.tokens.components` 是受支持的 React 主题结构。 -```scss -// 覆盖结构常量 -$btn-padding-md: 0 20px; -$card-body-padding: 20px; -$tooltip-arrow-size: 6px; +## Sass 源码样式 -// 引入 Tiny UI 样式(通过 !default 应用你的覆盖值) -@use "@tiny-design/react/es/style/index" as *; -``` - -### 3. 在入口文件中引入 - -```js -import './theme-overrides.scss'; -``` - -完整的 SCSS 常量列表请参考 [_constants.scss](https://github.com/wangdicoder/tiny-design/blob/master/packages/tokens/scss/_constants.scss)。 +Tiny UI 仍然发布 Sass 源文件,方便需要直接编译库样式的构建工具使用;但 Sass 变量不再是主题 API。请把 `@tiny-design/react/es/style/*.scss` 和组件 `style/*.scss` 当作实现源码,而不是受支持的主题定制契约。 -以下是一些常用的可覆盖常量: - -```scss -// 按钮 -$btn-padding-sm: 0 10px !default; -$btn-padding-md: 0 15px !default; -$btn-padding-lg: 0 28px !default; - -// 卡片 -$card-header-padding: 13px 16px !default; -$card-body-padding: 16px !default; - -// 通知 -$notification-width: 380px !default; -``` +颜色、排版、圆角、阴影、间距、尺寸和组件状态等视觉定制都应该通过 token 完成。如果某个值还没有对应 token,优先把它补进 token registry,而不是新增公开 Sass 变量。 -> **注意:** 颜色、排版、圆角、阴影等所有视觉令牌,都应该通过 v2 token 定制,而不是通过 SCSS 变量。SCSS 常量只适用于内边距、尺寸等编译期结构值。 +完整 token 列表由 `packages/tokens/dist/registry.json` 生成。 ## 推荐用法 - 想最快做运行时覆盖,用 CSS 变量 - 需要可移植的 JSON 主题格式,用 `ThemeDocument` - 需要 React 局部主题,用 `ConfigProvider` -- 只有在值必须在构建期决定时,才使用 SCSS 常量 +- 不要把 Sass 变量当作公开主题配置;缺失的定制点应该补成 token -如果现有的令牌或常量列表无法满足你的需求,请提交 issue 反馈。 +如果现有的令牌列表无法满足你的需求,请提交 issue 反馈。 diff --git a/apps/docs/guides/mcp-server.md b/apps/docs/guides/mcp-server.md index e8aa713e..9cd4cc54 100644 --- a/apps/docs/guides/mcp-server.md +++ b/apps/docs/guides/mcp-server.md @@ -15,7 +15,7 @@ The Model Context Protocol (MCP) is an open standard that lets AI assistants con | `list_components` | List all 80+ components. Filter by category: Foundation, Layout, Navigation, Data Display, Form, Feedback, Miscellany. | | `get_component_props` | Get the full props interface for a component — types, required flags, descriptions. | | `get_component_example` | Get usage examples (demo code) for a component. | -| `get_design_tokens` | Get design tokens (SCSS variables) — colors, typography, spacing, breakpoints, shadows. | +| `get_design_tokens` | Get v2 design tokens from the token registry — colors, typography, spacing, breakpoints, shadows. | | `list_icons` | List all 240+ icon names. Filter by search term. | | `get_icon` | Get details and usage example for a specific icon. | diff --git a/apps/docs/playwright.config.ts b/apps/docs/playwright.config.ts new file mode 100644 index 00000000..ced009fe --- /dev/null +++ b/apps/docs/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/visual', + outputDir: './test-results', + snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['html'], ['list']] : 'list', + expect: { + toHaveScreenshot: { + maxDiffPixels: 100, + }, + }, + webServer: { + command: 'pnpm exec vite serve --host 127.0.0.1 --port 3004', + url: 'http://127.0.0.1:3004', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + use: { + ...devices['Desktop Chrome'], + baseURL: 'http://127.0.0.1:3004', + channel: 'chrome', + colorScheme: 'light', + deviceScaleFactor: 1, + locale: 'en-US', + timezoneId: 'UTC', + viewport: { width: 1280, height: 900 }, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + }, +}); diff --git a/apps/docs/src/_variables.scss b/apps/docs/src/_variables.scss index fb42ccdd..dcd80ce3 100755 --- a/apps/docs/src/_variables.scss +++ b/apps/docs/src/_variables.scss @@ -1,4 +1,4 @@ -@forward "../../../packages/tokens/scss/variables"; +@forward "../../../packages/react/src/style/variables"; $main-color: #0D9488; $header-height: 64px; diff --git a/apps/docs/src/containers/theme-studio/editor-draft.ts b/apps/docs/src/containers/theme-studio/editor-draft.ts index ac88092a..ab512113 100644 --- a/apps/docs/src/containers/theme-studio/editor-draft.ts +++ b/apps/docs/src/containers/theme-studio/editor-draft.ts @@ -47,12 +47,20 @@ export function loadInitialDraft(): ThemeEditorDraft { const pendingTheme = loadPendingThemeDocument(); if (pendingTheme) { clearPendingThemeDocument(); - return buildDraftFromThemeDocument(pendingTheme); + try { + return buildDraftFromThemeDocument(pendingTheme); + } catch { + // Ignore invalid cached documents and continue with the next source. + } } const storedTheme = loadStoredThemeDocument(); if (storedTheme) { - return buildDraftFromThemeDocument(storedTheme); + try { + return buildDraftFromThemeDocument(storedTheme); + } catch { + // Ignore invalid cached documents and fall back to the editor draft/default. + } } try { @@ -78,7 +86,6 @@ export function buildPreviewVars(fields: ThemeEditorFields): React.CSSProperties '--ty-font-family-monospace': fields.fontMono, '--ty-font-size-base': fields.fontSizeBase, '--ty-line-height-base': fields.lineHeightBase, - '--ty-letter-spacing': fields.letterSpacing, '--ty-h1-font-size': fields.h1Size, '--ty-h2-font-size': fields.h2Size, '--editor-primary': fields.primary, diff --git a/apps/docs/src/containers/theme-studio/index.tsx b/apps/docs/src/containers/theme-studio/index.tsx index bd06bf93..d3e9bc6a 100644 --- a/apps/docs/src/containers/theme-studio/index.tsx +++ b/apps/docs/src/containers/theme-studio/index.tsx @@ -117,7 +117,7 @@ const ThemeStudioPage = (): React.ReactElement => { const handleImport = () => { try { const parsed = JSON.parse(importText) as ThemeDocument; - const validation = validateThemeDocument(parsed); + const validation = validateThemeDocument(parsed, { strict: true }); if (!validation.valid) { setImportError(validation.errors.join('\n')); return; @@ -144,15 +144,7 @@ const ThemeStudioPage = (): React.ReactElement => { style={buildPreviewVars(draft.fields)} >
-
- Theme Editor -
- {activePreset.name} - {THEME_EDITOR_PRESETS.length} presets -
-
- -
+
+
+ +
diff --git a/apps/docs/src/containers/theme-studio/preview-components.tsx b/apps/docs/src/containers/theme-studio/preview-components.tsx index 1dffbe93..9c6344cc 100644 --- a/apps/docs/src/containers/theme-studio/preview-components.tsx +++ b/apps/docs/src/containers/theme-studio/preview-components.tsx @@ -658,7 +658,7 @@ function DashboardPreview(): React.ReactElement { function MailPreview(): React.ReactElement { return ( - +
@@ -778,7 +778,7 @@ function MailPreview(): React.ReactElement { - +
); } 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 9e346e63..26a99ba8 100644 --- a/apps/docs/src/containers/theme-studio/theme-document-adapter.ts +++ b/apps/docs/src/containers/theme-studio/theme-document-adapter.ts @@ -23,7 +23,10 @@ export function inferPresetIdFromThemeDocument(theme: ThemeDocument): string { } function normalizeImportedThemeDocument(theme: ThemeDocument): ThemeDocument { - const validation = validateThemeDocument(theme); + const validation = validateThemeDocument(theme, { strict: true }); + if (!validation.valid) { + throw new Error(validation.errors.join('\n')); + } return validation.normalizedDocument as ThemeDocument; } @@ -113,42 +116,15 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'line-height-base': fields.lineHeightBase, 'h1-font-size': fields.h1Size, 'h2-font-size': fields.h2Size, - 'letter-spacing': fields.letterSpacing, 'border-radius': fields.radius, 'shadow-card': fields.shadowCard, 'shadow-focus': fields.shadowFocus, - 'editor-primary-foreground': fields.primaryForeground, - 'editor-secondary': fields.secondary, - 'editor-secondary-foreground': fields.secondaryForeground, - 'editor-accent': fields.accent, - 'editor-accent-foreground': fields.accentForeground, - 'editor-success': fields.success, - 'editor-success-foreground': fields.successForeground, - 'editor-info': fields.info, - 'editor-info-foreground': fields.infoForeground, - 'editor-warning': fields.warning, - 'editor-warning-foreground': fields.warningForeground, - 'editor-danger': fields.danger, - 'editor-danger-foreground': fields.dangerForeground, - 'editor-base': fields.base, - 'editor-base-foreground': fields.baseForeground, - 'editor-card': fields.card, - 'editor-card-foreground': fields.cardForeground, - 'editor-popover': fields.popover, - 'editor-popover-foreground': fields.popoverForeground, - 'editor-muted': fields.muted, - 'editor-muted-foreground': fields.mutedForeground, - 'editor-border': fields.border, - 'editor-input': fields.input, - 'editor-ring': fields.ring, - 'editor-sidebar': fields.sidebar, - 'editor-sidebar-foreground': fields.sidebarForeground, - 'editor-sidebar-primary': fields.sidebarPrimary, - 'editor-sidebar-primary-foreground': fields.sidebarPrimaryForeground, - 'editor-sidebar-accent': fields.sidebarAccent, - 'editor-sidebar-accent-foreground': fields.sidebarAccentForeground, - 'editor-sidebar-border': fields.sidebarBorder, - 'editor-sidebar-ring': fields.sidebarRing, + 'control.height.sm': fields.fieldHeightSm, + 'control.height.md': fields.fieldHeightMd, + 'control.height.lg': fields.fieldHeightLg, + 'control.padding-inline.sm': fields.fieldPaddingSm, + 'control.padding-inline.md': fields.fieldPaddingMd, + 'control.padding-inline.lg': fields.fieldPaddingLg, }, components: { 'button.bg.primary': fields.primary, @@ -164,12 +140,6 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'button.text.default': fields.baseForeground, 'button.text.default-hover': fields.baseForeground, 'button.text.default-active': fields.baseForeground, - 'control.height.sm': fields.fieldHeightSm, - 'control.height.md': fields.fieldHeightMd, - 'control.height.lg': fields.fieldHeightLg, - 'control.padding-inline.sm': fields.fieldPaddingSm, - 'control.padding-inline.md': fields.fieldPaddingMd, - 'control.padding-inline.lg': fields.fieldPaddingLg, 'button.radius': fields.buttonRadius, 'button.height.sm': fields.buttonHeightSm, 'button.height.md': fields.buttonHeightMd, @@ -230,25 +200,25 @@ export function buildThemeDocumentFromDraft(draft: ThemeEditorDraft): ThemeDocum 'picker.input-padding.lg': `0 ${fields.fieldPaddingLg}`, 'picker.dropdown-bg': fields.popover, 'picker.dropdown-radius': fields.cardRadius, - 'picker.header-border': fields.border, - 'picker.header-button-color': fields.mutedForeground, - 'picker.header-button-color-hover': fields.primary, - 'picker.today-color': fields.primary, - 'picker.today-color-hover': fields.primary, + 'date-picker.header-border': fields.border, + 'date-picker.header-button-color': fields.mutedForeground, + 'date-picker.header-button-color-hover': fields.primary, + 'date-picker.today-color': fields.primary, + 'date-picker.today-color-hover': fields.primary, 'picker.cell-hover-bg': fields.muted, - 'picker.cell-selected-bg': fields.primary, - 'picker.cell-selected-color': fields.primaryForeground, - 'picker.cell-selected-hover-bg': fields.primary, - 'picker.cell-today-border': fields.primary, - 'picker.cell-radius': fields.inputRadius, - 'picker.range-bg': fields.accent, - 'picker.time-column-border': fields.border, - 'picker.time-cell-bg-selected': fields.primary, - 'picker.time-cell-bg-selected-hover': fields.primary, - 'picker.ok-button-bg': fields.primary, - 'picker.ok-button-bg-hover': fields.primary, - 'picker.ok-button-color': fields.primaryForeground, - 'picker.ok-button-radius': fields.buttonRadius, + 'date-picker.cell-selected-bg': fields.primary, + 'date-picker.cell-selected-color': fields.primaryForeground, + 'date-picker.cell-selected-hover-bg': fields.primary, + 'date-picker.cell-today-border': fields.primary, + 'date-picker.cell-radius': fields.inputRadius, + 'date-picker.range-bg': fields.accent, + 'time-picker.column-border': fields.border, + 'time-picker.cell-bg-selected': fields.primary, + 'time-picker.cell-bg-selected-hover': fields.primary, + 'time-picker.ok-button-bg': fields.primary, + 'time-picker.ok-button-bg-hover': fields.primary, + 'time-picker.ok-button-color': fields.primaryForeground, + 'time-picker.ok-button-radius': fields.buttonRadius, 'calendar.bg': fields.card, 'calendar.border': fields.border, 'calendar.radius': fields.cardRadius, @@ -377,50 +347,37 @@ export function buildDraftFromThemeDocument(theme: ThemeDocument): ThemeEditorDr fields: { ...baseFields, primary: readToken(normalizedTheme, 'color-primary') ?? baseFields.primary, - primaryForeground: readToken(normalizedTheme, 'editor-primary-foreground', 'button.text.primary') ?? baseFields.primaryForeground, - secondary: readToken(normalizedTheme, 'editor-secondary') ?? readToken(normalizedTheme, 'color-fill', 'button.bg.default') ?? baseFields.secondary, - secondaryForeground: readToken(normalizedTheme, 'editor-secondary-foreground', 'button.text.default') ?? baseFields.secondaryForeground, - accent: readToken(normalizedTheme, 'editor-accent') ?? readToken(normalizedTheme, 'color-primary-bg') ?? baseFields.accent, - accentForeground: readToken(normalizedTheme, 'editor-accent-foreground') ?? baseFields.accentForeground, - success: readToken(normalizedTheme, 'editor-success') ?? readToken(normalizedTheme, 'color-success') ?? baseFields.success, - successForeground: readToken(normalizedTheme, 'editor-success-foreground') ?? baseFields.successForeground, - info: readToken(normalizedTheme, 'editor-info') ?? readToken(normalizedTheme, 'color-info') ?? baseFields.info, - infoForeground: readToken(normalizedTheme, 'editor-info-foreground') ?? baseFields.infoForeground, - warning: readToken(normalizedTheme, 'editor-warning') ?? readToken(normalizedTheme, 'color-warning') ?? baseFields.warning, - warningForeground: readToken(normalizedTheme, 'editor-warning-foreground') ?? baseFields.warningForeground, - danger: readToken(normalizedTheme, 'editor-danger') ?? readToken(normalizedTheme, 'color-danger') ?? baseFields.danger, - dangerForeground: readToken(normalizedTheme, 'editor-danger-foreground') ?? baseFields.dangerForeground, - base: readToken(normalizedTheme, 'editor-base') ?? readToken(normalizedTheme, 'color-bg') ?? baseFields.base, - baseForeground: readToken(normalizedTheme, 'editor-base-foreground') ?? readToken(normalizedTheme, 'color-text') ?? baseFields.baseForeground, - card: readToken(normalizedTheme, 'editor-card') ?? readToken(normalizedTheme, 'color-bg-container', 'card.bg') ?? baseFields.card, - cardForeground: readToken(normalizedTheme, 'editor-card-foreground', 'card.header-color') ?? baseFields.cardForeground, - popover: readToken(normalizedTheme, 'editor-popover') ?? readToken(normalizedTheme, 'color-bg-elevated') ?? baseFields.popover, - popoverForeground: readToken(normalizedTheme, 'editor-popover-foreground') ?? baseFields.popoverForeground, - muted: readToken(normalizedTheme, 'editor-muted') ?? readToken(normalizedTheme, 'color-bg-spotlight') ?? baseFields.muted, - mutedForeground: readToken(normalizedTheme, 'editor-muted-foreground') ?? readToken(normalizedTheme, 'color-text-secondary') ?? baseFields.mutedForeground, - border: readToken(normalizedTheme, 'editor-border') ?? readToken(normalizedTheme, 'color-border') ?? baseFields.border, - input: readToken(normalizedTheme, 'editor-input') ?? readToken(normalizedTheme, 'color-border', 'input.border') ?? baseFields.input, - ring: readToken(normalizedTheme, 'editor-ring', 'input.border.focus') ?? baseFields.ring, + primaryForeground: readComponentFirst(normalizedTheme, 'button.text.primary') ?? baseFields.primaryForeground, + secondary: readToken(normalizedTheme, 'color-fill', 'button.bg.default') ?? baseFields.secondary, + secondaryForeground: readToken(normalizedTheme, 'color-text', 'button.text.default') ?? baseFields.secondaryForeground, + accent: readToken(normalizedTheme, 'color-primary-bg') ?? baseFields.accent, + success: readToken(normalizedTheme, 'color-success') ?? baseFields.success, + info: readToken(normalizedTheme, 'color-info') ?? baseFields.info, + warning: readToken(normalizedTheme, 'color-warning') ?? baseFields.warning, + danger: readToken(normalizedTheme, 'color-danger') ?? baseFields.danger, + base: readToken(normalizedTheme, 'color-bg') ?? baseFields.base, + baseForeground: readToken(normalizedTheme, 'color-text') ?? baseFields.baseForeground, + card: readToken(normalizedTheme, 'color-bg-container', 'card.bg') ?? baseFields.card, + cardForeground: readToken(normalizedTheme, 'color-text-heading', 'card.header-color') ?? baseFields.cardForeground, + popover: readToken(normalizedTheme, 'color-bg-elevated') ?? baseFields.popover, + muted: readToken(normalizedTheme, 'color-bg-spotlight') ?? baseFields.muted, + mutedForeground: readToken(normalizedTheme, 'color-text-secondary') ?? baseFields.mutedForeground, + border: readToken(normalizedTheme, 'color-border') ?? baseFields.border, + input: readToken(normalizedTheme, 'color-border', 'input.border') ?? baseFields.input, + ring: readToken(normalizedTheme, 'color-primary', 'input.border.focus') ?? baseFields.ring, chart1: readToken(normalizedTheme, 'chart-1') ?? baseFields.chart1, chart2: readToken(normalizedTheme, 'chart-2') ?? baseFields.chart2, chart3: readToken(normalizedTheme, 'chart-3') ?? baseFields.chart3, chart4: readToken(normalizedTheme, 'chart-4') ?? baseFields.chart4, chart5: readToken(normalizedTheme, 'chart-5') ?? baseFields.chart5, - sidebar: readToken(normalizedTheme, 'editor-sidebar', 'layout.sidebar-bg') ?? baseFields.sidebar, - sidebarForeground: readToken(normalizedTheme, 'editor-sidebar-foreground', 'layout.sidebar-color') ?? baseFields.sidebarForeground, - sidebarPrimary: readToken(normalizedTheme, 'editor-sidebar-primary') ?? baseFields.sidebarPrimary, - sidebarPrimaryForeground: readToken(normalizedTheme, 'editor-sidebar-primary-foreground') ?? baseFields.sidebarPrimaryForeground, - sidebarAccent: readToken(normalizedTheme, 'editor-sidebar-accent') ?? baseFields.sidebarAccent, - sidebarAccentForeground: readToken(normalizedTheme, 'editor-sidebar-accent-foreground') ?? baseFields.sidebarAccentForeground, - sidebarBorder: readToken(normalizedTheme, 'editor-sidebar-border') ?? baseFields.sidebarBorder, - sidebarRing: readToken(normalizedTheme, 'editor-sidebar-ring') ?? baseFields.sidebarRing, + sidebar: readToken(normalizedTheme, 'color-bg-layout', 'layout.sidebar-bg') ?? baseFields.sidebar, + sidebarForeground: readToken(normalizedTheme, 'color-text', 'layout.sidebar-color') ?? baseFields.sidebarForeground, fontSans: readToken(normalizedTheme, 'font-family') ?? baseFields.fontSans, fontMono: readToken(normalizedTheme, 'font-family-monospace') ?? baseFields.fontMono, fontSizeBase: readToken(normalizedTheme, 'font-size-base') ?? baseFields.fontSizeBase, lineHeightBase: readToken(normalizedTheme, 'line-height-base') ?? baseFields.lineHeightBase, h1Size: readToken(normalizedTheme, 'h1-font-size') ?? baseFields.h1Size, h2Size: readToken(normalizedTheme, 'h2-font-size') ?? baseFields.h2Size, - letterSpacing: readToken(normalizedTheme, 'letter-spacing') ?? baseFields.letterSpacing, radius: readToken(normalizedTheme, 'border-radius') ?? baseFields.radius, shadowCard: readToken(normalizedTheme, 'shadow-card', 'card.shadow') ?? baseFields.shadowCard, shadowFocus: readToken(normalizedTheme, 'shadow-focus', 'input.shadow.focus') ?? toRgba(baseFields.primary, 0.22), diff --git a/apps/docs/src/containers/theme-studio/theme-studio.scss b/apps/docs/src/containers/theme-studio/theme-studio.scss index b248d5ba..1ce00fa7 100644 --- a/apps/docs/src/containers/theme-studio/theme-studio.scss +++ b/apps/docs/src/containers/theme-studio/theme-studio.scss @@ -11,7 +11,7 @@ .theme-studio__topbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 8px; + gap: 12px; padding: 8px; border: 1px solid var(--ty-color-border-secondary, var(--ty-color-border)); border-radius: 12px; @@ -21,13 +21,11 @@ box-shadow: 0 6px 16px rgba(17, 24, 39, 0.03); } -.theme-studio__topbar-copy { +.theme-studio__topbar-primary { min-width: 0; display: flex; align-items: center; justify-content: flex-start; - gap: 10px; - flex-direction: row; } .theme-studio__topbar-actions, @@ -91,8 +89,8 @@ min-width: auto; } -.theme-studio__topbar-actions .theme-studio__select { - width: 220px; +.theme-studio__topbar-primary .theme-studio__select { + width: min(360px, 100%); } .theme-studio__eyebrow { @@ -103,23 +101,6 @@ white-space: nowrap; } -.theme-studio__topbar-meta { - display: flex; - gap: 6px; - flex-wrap: wrap; - justify-content: flex-start; -} - -.theme-studio__topbar-meta span { - padding: 4px 8px; - border-radius: 999px; - background: color-mix(in srgb, var(--editor-primary), transparent 94%); - border: 1px solid color-mix(in srgb, var(--editor-primary), transparent 88%); - font-size: 10px; - line-height: 1; - text-transform: capitalize; -} - .theme-studio__topbar-utility { display: flex; align-items: center; @@ -977,8 +958,9 @@ } .theme-studio__mail-shell { - grid-template-columns: 220px 320px minmax(0, 1fr); - gap: 16px; + grid-template-columns: minmax(190px, 0.72fr) minmax(260px, 1fr) minmax(380px, 1.55fr); + gap: 14px; + align-items: stretch; } .theme-studio__dashboard-sidebar { @@ -1091,9 +1073,11 @@ display: flex; flex-direction: column; gap: 18px; + min-width: 0; background: color-mix(in srgb, var(--editor-sidebar), var(--editor-base) 8%); color: var(--editor-sidebar-foreground); border: 1px solid var(--editor-sidebar-border); + overflow: hidden; } .theme-studio__mail-sidebar .theme-studio__eyebrow, @@ -1120,9 +1104,15 @@ padding-bottom: 2px; } +.theme-studio__mail-sidebar-head .ty-typography-heading { + margin: 0; + overflow-wrap: anywhere; +} + .theme-studio__mail-panel, .theme-studio__mail-detail { min-width: 0; + overflow: hidden; } .theme-studio__mail-panel.ty-card > .ty-card__body, @@ -1149,12 +1139,30 @@ .theme-studio__mail-panel-head { margin-bottom: 12px; + flex-wrap: wrap; +} + +.theme-studio__mail-panel-head .ty-input { + flex: 1 1 180px; + min-width: 0; } .theme-studio__mail-message-meta { display: flex; align-items: center; gap: 12px; + min-width: 0; +} + +.theme-studio__mail-message-meta > div { + min-width: 0; +} + +.theme-studio__mail-message-meta .ty-typography-text { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .theme-studio__mail-nav-item { @@ -1164,6 +1172,14 @@ gap: 12px; color: inherit; text-align: left; + overflow: hidden; +} + +.theme-studio__mail-nav-item span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .theme-studio__mail-nav-item small { @@ -1208,6 +1224,7 @@ margin-left: auto; font-size: 12px; color: var(--editor-muted-foreground); + white-space: nowrap; } .theme-studio__mail-item-subject { @@ -1235,6 +1252,12 @@ gap: 12px; align-items: flex-start; margin-bottom: 14px; + flex-wrap: wrap; +} + +.theme-studio__mail-message-actions { + flex-wrap: wrap; + justify-content: flex-end; } .theme-studio__mail-thread-list { @@ -1251,6 +1274,7 @@ gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--editor-border); + flex-wrap: wrap; } .theme-studio__chat-bubble { @@ -1264,6 +1288,7 @@ .theme-studio__chat-thread { gap: 10px; + padding: 2px 0 4px; } .theme-studio__chat-bubble_in { @@ -1287,6 +1312,7 @@ .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 { @@ -1522,7 +1548,7 @@ grid-template-columns: 1fr; } - .theme-studio__topbar-copy, + .theme-studio__topbar-primary, .theme-studio__topbar-actions { align-items: flex-start; flex-direction: column; diff --git a/apps/docs/tests/visual/README.md b/apps/docs/tests/visual/README.md new file mode 100644 index 00000000..3bc40dc2 --- /dev/null +++ b/apps/docs/tests/visual/README.md @@ -0,0 +1,19 @@ +# Visual Regression Tests + +These Playwright tests cover the highest-risk docs examples for popup positioning, +component token styling, and dialog overlays. + +Run the comparison: + +```sh +pnpm test:visual +``` + +Update baselines intentionally: + +```sh +pnpm test:visual:update +``` + +The suite targets the docs app and uses the local Chrome channel to avoid storing +browser binaries in the repository workflow. diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/button-types.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/button-types.png new file mode 100644 index 00000000..189c36fc Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/button-types.png differ diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/drawer-open.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/drawer-open.png new file mode 100644 index 00000000..1202734f Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/drawer-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/modal-open.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/modal-open.png new file mode 100644 index 00000000..f62afec2 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/modal-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/table-basic.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/table-basic.png new file mode 100644 index 00000000..8e129b23 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/table-basic.png differ diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/tour-open.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/tour-open.png new file mode 100644 index 00000000..e44398d4 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/tour-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/transfer-search.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/transfer-search.png new file mode 100644 index 00000000..cdeb26f0 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/transfer-search.png differ diff --git a/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/upload-list.png b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/upload-list.png new file mode 100644 index 00000000..d349d2df Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/feedback.visual.spec.ts/upload-list.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/button-disabled.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/button-disabled.png new file mode 100644 index 00000000..ca3d6c66 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/button-disabled.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/button-more-types.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/button-more-types.png new file mode 100644 index 00000000..abb53a29 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/button-more-types.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/cascader-default-value.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/cascader-default-value.png new file mode 100644 index 00000000..e8b9e716 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/cascader-default-value.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/cascader-disabled-options.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/cascader-disabled-options.png new file mode 100644 index 00000000..08871424 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/cascader-disabled-options.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/date-picker-disabled.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/date-picker-disabled.png new file mode 100644 index 00000000..17f56664 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/date-picker-disabled.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/date-picker-extra-footer-open.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/date-picker-extra-footer-open.png new file mode 100644 index 00000000..f1298609 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/date-picker-extra-footer-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/dropdown-click-open.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/dropdown-click-open.png new file mode 100644 index 00000000..0c5e3f72 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/dropdown-click-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/form-size-alignment.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/form-size-alignment.png new file mode 100644 index 00000000..86d6d15e Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/form-size-alignment.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/pop-confirm-basic-open.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/pop-confirm-basic-open.png new file mode 100644 index 00000000..556cb861 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/pop-confirm-basic-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/popover-basic-open.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/popover-basic-open.png new file mode 100644 index 00000000..d0396607 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/popover-basic-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/table-row-selection.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/table-row-selection.png new file mode 100644 index 00000000..fb2fccfb Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/table-row-selection.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/time-picker-controlled.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/time-picker-controlled.png new file mode 100644 index 00000000..e7ce3eaa Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/time-picker-controlled.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/tooltip-basic-open.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/tooltip-basic-open.png new file mode 100644 index 00000000..67fb4ef8 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/tooltip-basic-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/tree-selectable.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/tree-selectable.png new file mode 100644 index 00000000..604c906a Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/tree-selectable.png differ diff --git a/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/upload-dragger.png b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/upload-dragger.png new file mode 100644 index 00000000..0614b360 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/interaction.visual.spec.ts/upload-dragger.png differ diff --git a/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/cascader-change-on-select-open.png b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/cascader-change-on-select-open.png new file mode 100644 index 00000000..24407d67 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/cascader-change-on-select-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/color-picker-open.png b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/color-picker-open.png new file mode 100644 index 00000000..0e1ffa11 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/color-picker-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/date-picker-range-open.png b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/date-picker-range-open.png new file mode 100644 index 00000000..83def81b Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/date-picker-range-open.png differ diff --git a/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/select-multiple.png b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/select-multiple.png new file mode 100644 index 00000000..cd6908ba Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/select-multiple.png differ diff --git a/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/select-search-empty.png b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/select-search-empty.png new file mode 100644 index 00000000..16895fb7 Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/select-search-empty.png differ diff --git a/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/time-picker-open.png b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/time-picker-open.png new file mode 100644 index 00000000..deca0c4b Binary files /dev/null and b/apps/docs/tests/visual/__screenshots__/selectors.visual.spec.ts/time-picker-open.png differ diff --git a/apps/docs/tests/visual/feedback.visual.spec.ts b/apps/docs/tests/visual/feedback.visual.spec.ts new file mode 100644 index 00000000..134f8cf9 --- /dev/null +++ b/apps/docs/tests/visual/feedback.visual.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { gotoComponent, openFromDemo, previewByTitle, scrollDemoIntoView } from './helpers'; + +test.describe('feedback and data visuals', () => { + test('button types', async ({ page }) => { + await gotoComponent(page, 'button'); + await scrollDemoIntoView(page, 'Type'); + await expect(previewByTitle(page, 'Type')).toHaveScreenshot('button-types.png'); + }); + + test('modal open state', async ({ page }) => { + await gotoComponent(page, 'modal'); + await openFromDemo(page, 'Basic', 'button'); + await expect(page.locator('.ty-modal__content')).toBeVisible(); + await expect(page).toHaveScreenshot('modal-open.png'); + }); + + test('drawer open state', async ({ page }) => { + await gotoComponent(page, 'drawer'); + await openFromDemo(page, 'Basic', 'button'); + await expect(page.locator('.ty-drawer__content')).toBeVisible(); + await expect(page).toHaveScreenshot('drawer-open.png'); + }); + + test('tour open state', async ({ page }) => { + await gotoComponent(page, 'tour'); + await openFromDemo(page, 'Basic', 'button:has-text("Start Tour")'); + await expect(page.locator('.ty-tour')).toBeVisible(); + await expect(page).toHaveScreenshot('tour-open.png'); + }); + + test('upload list', async ({ page }) => { + await gotoComponent(page, 'upload'); + await scrollDemoIntoView(page, 'Default Files'); + await expect(previewByTitle(page, 'Default Files')).toHaveScreenshot('upload-list.png'); + }); + + test('table basic', async ({ page }) => { + await gotoComponent(page, 'table'); + await scrollDemoIntoView(page, 'Basic'); + await expect(previewByTitle(page, 'Basic')).toHaveScreenshot('table-basic.png'); + }); + + test('transfer search', async ({ page }) => { + await gotoComponent(page, 'transfer'); + const demo = await scrollDemoIntoView(page, 'Search'); + await demo.locator('.ty-input__input').first().fill('1'); + await expect(previewByTitle(page, 'Search')).toHaveScreenshot('transfer-search.png'); + }); +}); diff --git a/apps/docs/tests/visual/helpers.ts b/apps/docs/tests/visual/helpers.ts new file mode 100644 index 00000000..5cc26d87 --- /dev/null +++ b/apps/docs/tests/visual/helpers.ts @@ -0,0 +1,48 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +export const prepareVisualPage = async (page: Page) => { + await page.addStyleTag({ + content: ` + *, *::before, *::after { + animation-delay: 0s !important; + animation-duration: 0s !important; + caret-color: transparent !important; + scroll-behavior: auto !important; + transition-delay: 0s !important; + transition-duration: 0s !important; + } + `, + }); +}; + +export const gotoComponent = async (page: Page, route: string) => { + await page.goto(`/components/${route}`); + await page.waitForLoadState('networkidle'); + await prepareVisualPage(page); +}; + +export const demoByTitle = (page: Page, title: string): Locator => { + return page + .locator('.markdown__demo') + .filter({ has: page.locator('.markdown__heading-3').filter({ hasText: title }) }) + .first(); +}; + +export const previewByTitle = (page: Page, title: string): Locator => { + return demoByTitle(page, title).locator('.demo-block__previewer').first(); +}; + +export const scrollDemoIntoView = async (page: Page, title: string) => { + const demo = demoByTitle(page, title); + await expect(demo).toBeVisible(); + await demo.scrollIntoViewIfNeeded(); + await page.waitForTimeout(100); + return demo; +}; + +export const openFromDemo = async (page: Page, title: string, selector: string) => { + const demo = await scrollDemoIntoView(page, title); + await demo.locator(selector).first().click(); + await page.waitForTimeout(150); + return demo; +}; diff --git a/apps/docs/tests/visual/interaction.visual.spec.ts b/apps/docs/tests/visual/interaction.visual.spec.ts new file mode 100644 index 00000000..b87009dc --- /dev/null +++ b/apps/docs/tests/visual/interaction.visual.spec.ts @@ -0,0 +1,104 @@ +import { expect, test } from '@playwright/test'; +import { gotoComponent, openFromDemo, previewByTitle, scrollDemoIntoView } from './helpers'; + +test.describe('popup and interaction visuals', () => { + test('button more types', async ({ page }) => { + await gotoComponent(page, 'button'); + await scrollDemoIntoView(page, 'More Types'); + await expect(previewByTitle(page, 'More Types')).toHaveScreenshot('button-more-types.png'); + }); + + test('button disabled states', async ({ page }) => { + await gotoComponent(page, 'button'); + await scrollDemoIntoView(page, 'Disabled'); + await expect(previewByTitle(page, 'Disabled')).toHaveScreenshot('button-disabled.png'); + }); + + test('dropdown click menu', async ({ page }) => { + await gotoComponent(page, 'dropdown'); + await scrollDemoIntoView(page, 'Trigger mode'); + await previewByTitle(page, 'Trigger mode').locator('a').first().click(); + await expect(page.locator('.ty-dropdown .ty-menu')).toBeVisible(); + await expect(page).toHaveScreenshot('dropdown-click-open.png'); + }); + + test('tooltip basic hover', async ({ page }) => { + await gotoComponent(page, 'tooltip'); + await scrollDemoIntoView(page, 'Basic'); + await previewByTitle(page, 'Basic').getByText('Tooltip will show on mouse enter.').hover(); + await expect(page.locator('.ty-tooltip')).toBeVisible(); + await expect(page).toHaveScreenshot('tooltip-basic-open.png'); + }); + + test('popover basic hover', async ({ page }) => { + await gotoComponent(page, 'popover'); + const demo = await scrollDemoIntoView(page, 'Basic'); + await demo.locator('button:has-text("Hover me")').hover(); + await expect(page.locator('.ty-popover')).toBeVisible(); + await expect(page).toHaveScreenshot('popover-basic-open.png'); + }); + + test('popconfirm basic open', async ({ page }) => { + await gotoComponent(page, 'pop-confirm'); + await openFromDemo(page, 'Basic', 'button:has-text("Delete")'); + await expect(page.locator('.ty-pop-confirm')).toBeVisible(); + await expect(page).toHaveScreenshot('pop-confirm-basic-open.png'); + }); + + test('date picker disabled demo', async ({ page }) => { + await gotoComponent(page, 'date-picker'); + await scrollDemoIntoView(page, 'Disabled'); + await expect(previewByTitle(page, 'Disabled')).toHaveScreenshot('date-picker-disabled.png'); + }); + + test('date picker extra footer panel', async ({ page }) => { + await gotoComponent(page, 'date-picker'); + await openFromDemo(page, 'Extra Footer', '.ty-date-picker__input'); + await expect(page.locator('.ty-date-picker__dropdown')).toBeVisible(); + await expect(page).toHaveScreenshot('date-picker-extra-footer-open.png'); + }); + + test('upload dragger', async ({ page }) => { + await gotoComponent(page, 'upload'); + await scrollDemoIntoView(page, 'Drag and Drop'); + await expect(previewByTitle(page, 'Drag and Drop')).toHaveScreenshot('upload-dragger.png'); + }); + + test('table row selection checked', async ({ page }) => { + await gotoComponent(page, 'table'); + const demo = await scrollDemoIntoView(page, 'Row Selection'); + await demo.locator('.ty-table__tbody .ty-checkbox').first().click(); + await expect(demo.getByText('Selected: 1')).toBeVisible(); + await expect(previewByTitle(page, 'Row Selection')).toHaveScreenshot('table-row-selection.png'); + }); + + test('tree selectable', async ({ page }) => { + await gotoComponent(page, 'tree'); + await scrollDemoIntoView(page, 'Selectable'); + await expect(previewByTitle(page, 'Selectable')).toHaveScreenshot('tree-selectable.png'); + }); + + test('form size alignment', async ({ page }) => { + await gotoComponent(page, 'form'); + await scrollDemoIntoView(page, 'Size Alignment'); + await expect(previewByTitle(page, 'Size Alignment')).toHaveScreenshot('form-size-alignment.png'); + }); + + test('cascader default value', async ({ page }) => { + await gotoComponent(page, 'cascader'); + await scrollDemoIntoView(page, 'Default Value'); + await expect(previewByTitle(page, 'Default Value')).toHaveScreenshot('cascader-default-value.png'); + }); + + test('cascader disabled options', async ({ page }) => { + await gotoComponent(page, 'cascader'); + await scrollDemoIntoView(page, 'Disabled Options'); + await expect(previewByTitle(page, 'Disabled Options')).toHaveScreenshot('cascader-disabled-options.png'); + }); + + test('time picker controlled', async ({ page }) => { + await gotoComponent(page, 'time-picker'); + await scrollDemoIntoView(page, 'Controlled'); + await expect(previewByTitle(page, 'Controlled')).toHaveScreenshot('time-picker-controlled.png'); + }); +}); diff --git a/apps/docs/tests/visual/selectors.visual.spec.ts b/apps/docs/tests/visual/selectors.visual.spec.ts new file mode 100644 index 00000000..a6b31c1c --- /dev/null +++ b/apps/docs/tests/visual/selectors.visual.spec.ts @@ -0,0 +1,52 @@ +import { expect, test } from '@playwright/test'; +import { gotoComponent, openFromDemo, previewByTitle, scrollDemoIntoView } from './helpers'; + +test.describe('selection and picker visuals', () => { + test('select search empty dropdown', async ({ page }) => { + await gotoComponent(page, 'select'); + const demo = await openFromDemo(page, 'Search', '.ty-select__selector'); + await demo.locator('.ty-select__search').fill('zzzz'); + const empty = page.locator('.ty-select__empty'); + await expect(empty).toBeVisible(); + const selectorBox = await demo.locator('.ty-select__selector').boundingBox(); + const emptyBox = await empty.boundingBox(); + expect(selectorBox).not.toBeNull(); + expect(emptyBox).not.toBeNull(); + expect(emptyBox!.y).toBeGreaterThan(selectorBox!.y + selectorBox!.height - 1); + await expect(page).toHaveScreenshot('select-search-empty.png'); + }); + + test('select multiple tags', async ({ page }) => { + await gotoComponent(page, 'select'); + await scrollDemoIntoView(page, 'Multiple'); + await expect(previewByTitle(page, 'Multiple')).toHaveScreenshot('select-multiple.png'); + }); + + test('cascader change-on-select dropdown', async ({ page }) => { + await gotoComponent(page, 'cascader'); + await openFromDemo(page, 'Change On Select', '.ty-cascader__selector'); + await expect(page.locator('.ty-cascader__dropdown')).toBeVisible(); + await expect(page).toHaveScreenshot('cascader-change-on-select-open.png'); + }); + + test('date picker range panel', async ({ page }) => { + await gotoComponent(page, 'date-picker'); + await openFromDemo(page, 'Date Range', '.ty-date-picker__input'); + await expect(page.locator('.ty-date-picker__dropdown')).toBeVisible(); + await expect(page).toHaveScreenshot('date-picker-range-open.png'); + }); + + test('time picker panel', async ({ page }) => { + await gotoComponent(page, 'time-picker'); + await openFromDemo(page, 'Controlled', '.ty-time-picker__input'); + await expect(page.locator('.ty-time-picker__dropdown')).toBeVisible(); + await expect(page).toHaveScreenshot('time-picker-open.png'); + }); + + test('color picker panel', async ({ page }) => { + await gotoComponent(page, 'color-picker'); + await openFromDemo(page, 'Basic', '.ty-color-picker__trigger'); + await expect(page.locator('.ty-color-picker__panel')).toBeVisible(); + await expect(page).toHaveScreenshot('color-picker-open.png'); + }); +}); diff --git a/apps/pro/.gitignore b/apps/pro/.gitignore deleted file mode 100644 index 50fc4bdb..00000000 --- a/apps/pro/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.next/ -out/ \ No newline at end of file diff --git a/apps/pro/next-env.d.ts b/apps/pro/next-env.d.ts deleted file mode 100644 index 830fb594..00000000 --- a/apps/pro/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/pro/next.config.ts b/apps/pro/next.config.ts deleted file mode 100644 index f8129d02..00000000 --- a/apps/pro/next.config.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { NextConfig } from 'next'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ''; - -const nextConfig: NextConfig = { - output: 'export', - basePath, - trailingSlash: true, - images: { unoptimized: true }, - sassOptions: { - includePaths: [ - path.resolve(__dirname, 'node_modules'), - path.resolve(__dirname, '../../node_modules'), - path.resolve(__dirname, '../../packages/react/src'), - ], - }, - webpack(config) { - // Source alias: resolve to package source (same pattern as docs app) - config.resolve.alias['@tiny-design/react'] = path.resolve( - __dirname, - '../../packages/react/src' - ); - config.resolve.alias['@tiny-design/icons'] = path.resolve( - __dirname, - '../../packages/icons/src' - ); - - // ?raw imports: embed original file contents as strings at build time. - // We must exclude ?raw from existing oneOf rules so Next.js/SWC doesn't - // compile the TSX before we read it as plain text. - const rawQuery = /raw/; - - for (const rule of config.module.rules) { - if (rule && typeof rule === 'object' && rule.oneOf) { - for (const oneOfRule of rule.oneOf) { - if (oneOfRule && typeof oneOfRule === 'object') { - // Add resourceQuery exclusion to each sub-rule - if (!oneOfRule.resourceQuery) { - oneOfRule.resourceQuery = { not: [rawQuery] }; - } else if (oneOfRule.resourceQuery instanceof RegExp) { - oneOfRule.resourceQuery = { - and: [oneOfRule.resourceQuery], - not: [rawQuery], - }; - } - } - } - } - } - - config.module.rules.push({ - resourceQuery: rawQuery, - type: 'asset/source', - }); - - return config; - }, -}; - -export default nextConfig; diff --git a/apps/pro/package.json b/apps/pro/package.json deleted file mode 100644 index 71512579..00000000 --- a/apps/pro/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@tiny-design/pro", - "version": "0.0.0", - "private": true, - "scripts": { - "dev": "next dev --port 3001", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "@tiny-design/icons": "workspace:*", - "@tiny-design/react": "workspace:*", - "@tiny-design/tokens": "workspace:*", - "classnames": "^2.5.0", - "next": "^15.3.4", - "prism-react-renderer": "^2.3.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-runner": "^1.0.5", - "sass": "^1.49.9" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "typescript": "^5.4.0" - } -} diff --git a/apps/pro/postcss.config.js b/apps/pro/postcss.config.js deleted file mode 100644 index 9361eff3..00000000 --- a/apps/pro/postcss.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - plugins: {}, -}; diff --git a/apps/pro/src/app/blocks/[category]/category-page-client.tsx b/apps/pro/src/app/blocks/[category]/category-page-client.tsx deleted file mode 100644 index 9c4dfbcf..00000000 --- a/apps/pro/src/app/blocks/[category]/category-page-client.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { CategoryNav } from '@/components/layout/category-nav'; -import { BlockPreview } from '@/components/block-preview'; -import { getCategory } from '../../../utils/blocks'; -import styles from './category-page.module.scss'; - -interface CategoryPageClientProps { - slug: string; - label: string; -} - -export function CategoryPageClient({ slug, label }: CategoryPageClientProps) { - const category = getCategory(slug); - const blocks = category?.blocks ?? []; - - return ( -
- -
-
-

{label}

-

- {blocks.length} {blocks.length === 1 ? 'block' : 'blocks'} -

-
- {blocks.map((block) => ( - - ))} -
-
- ); -} diff --git a/apps/pro/src/app/blocks/[category]/category-page.module.scss b/apps/pro/src/app/blocks/[category]/category-page.module.scss deleted file mode 100644 index 80da192d..00000000 --- a/apps/pro/src/app/blocks/[category]/category-page.module.scss +++ /dev/null @@ -1,35 +0,0 @@ -.layout { - display: flex; - padding-top: 60px; - min-height: 100vh; -} - -.content { - flex: 1; - margin-left: 240px; - padding: 32px 40px 80px; - max-width: calc(100% - 240px); -} - -.pageHeader { - margin-bottom: 28px; - padding-bottom: 20px; - border-bottom: 1px solid var(--ty-color-border); -} - -.title { - font-size: 30px; - font-weight: 750; - letter-spacing: -0.03em; - margin: 0 0 4px; - color: var(--ty-color-text); -} - -.count { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--ty-color-text-tertiary); - margin: 0; - font-weight: 700; -} diff --git a/apps/pro/src/app/blocks/[category]/page.tsx b/apps/pro/src/app/blocks/[category]/page.tsx deleted file mode 100644 index a3f19450..00000000 --- a/apps/pro/src/app/blocks/[category]/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { notFound } from 'next/navigation'; -import { getCategorySlugs, getCategoryInfo } from '../../../utils/blocks'; -import { CategoryPageClient } from './category-page-client'; - -export function generateStaticParams() { - return getCategorySlugs().map((slug) => ({ category: slug })); -} - -interface PageProps { - params: Promise<{ category: string }>; -} - -export default async function CategoryPage({ params }: PageProps) { - const { category: slug } = await params; - const info = getCategoryInfo(slug); - if (!info) notFound(); - return ; -} diff --git a/apps/pro/src/app/globals.scss b/apps/pro/src/app/globals.scss deleted file mode 100644 index 95bb9c30..00000000 --- a/apps/pro/src/app/globals.scss +++ /dev/null @@ -1,29 +0,0 @@ -@use '@tiny-design/tokens/scss/base' as *; -@use 'style/component' as *; - -:root { - --font-heading: - 'Avenir Next', avenir, montserrat, 'Segoe UI', 'Helvetica Neue', arial, sans-serif; - --font-body: - 'Aptos', 'Avenir Next', 'Segoe UI', inter, roboto, 'Helvetica Neue', arial, sans-serif; -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -body { - margin: 0; - padding: 0; - font-family: var(--font-body), -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, - 'Helvetica Neue', arial, 'Noto Sans', sans-serif; - background: var(--ty-color-bg); - color: var(--ty-color-text); - transition: - background-color 0.2s, - color 0.2s; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} diff --git a/apps/pro/src/app/layout.tsx b/apps/pro/src/app/layout.tsx deleted file mode 100644 index 549eb9bf..00000000 --- a/apps/pro/src/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next'; -import { ThemeScript } from '../components/theme-script'; -import { SiteHeader } from '../components/layout/site-header'; -import './globals.scss'; - -export const metadata: Metadata = { - title: 'Tiny Design Pro', - description: 'Beautiful, ready-to-use UI blocks built with Tiny Design components.', -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - - - {children} - - - ); -} diff --git a/apps/pro/src/app/page.module.scss b/apps/pro/src/app/page.module.scss deleted file mode 100644 index 4c9d1bf7..00000000 --- a/apps/pro/src/app/page.module.scss +++ /dev/null @@ -1,439 +0,0 @@ -/* ─── Animations ─── */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(24px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes pulseGlow { - 0%, - 100% { - opacity: 0.5; - } - 50% { - opacity: 0.8; - } -} - -@keyframes dotPulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.4; - } -} - -/* ─── Layout ─── */ -.main { - padding-top: 60px; -} - -/* ─── Hero ─── */ -.hero { - position: relative; - overflow: hidden; - padding: 100px 24px 80px; - display: flex; - justify-content: center; - background: var(--ty-color-bg); -} - -.heroGlow { - position: absolute; - top: -200px; - left: 50%; - transform: translateX(-50%); - width: 800px; - height: 600px; - background: radial-gradient( - ellipse at center, - color-mix(in srgb, var(--ty-color-primary) 12%, transparent) 0%, - transparent 70% - ); - pointer-events: none; - animation: pulseGlow 6s ease-in-out infinite; -} - -.heroDots { - position: absolute; - inset: 0; - background-image: radial-gradient(color-mix(in srgb, var(--ty-color-text) 8%, transparent) 1px, transparent 1px); - background-size: 24px 24px; - mask-image: radial-gradient(ellipse at 50% 30%, black 20%, transparent 70%); - -webkit-mask-image: radial-gradient(ellipse at 50% 30%, black 20%, transparent 70%); - pointer-events: none; -} - -.heroContent { - position: relative; - z-index: 1; - max-width: 720px; - text-align: center; - animation: fadeInUp 0.7s ease both; -} - -.badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 16px; - border-radius: 100px; - font-size: 13px; - font-weight: 500; - letter-spacing: 0.01em; - color: var(--ty-color-text-secondary); - background: color-mix(in srgb, var(--ty-color-primary) 8%, transparent); - border: 1px solid color-mix(in srgb, var(--ty-color-primary) 15%, transparent); - margin-bottom: 28px; -} - -.badgeDot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--ty-color-primary); - animation: dotPulse 2s ease-in-out infinite; -} - -.title { - font-family: var(--font-heading), system-ui, sans-serif; - font-size: clamp(42px, 6.5vw, 72px); - font-weight: 800; - line-height: 1.05; - letter-spacing: -0.04em; - margin: 0 0 20px; - color: var(--ty-color-text); -} - -.titleAccent { - background: linear-gradient( - 135deg, - var(--ty-color-primary) 0%, - color-mix(in srgb, var(--ty-color-primary) 70%, #f59e0b) 50%, - #f59e0b 100% - ); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - font-size: 18px; - line-height: 1.7; - color: var(--ty-color-text-secondary); - margin: 0 auto 36px; - max-width: 520px; -} - -.heroActions { - display: flex; - gap: 12px; - justify-content: center; - flex-wrap: wrap; -} - -/* ─── Buttons ─── */ -.btnPrimary { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 28px; - border-radius: 10px; - font-size: 15px; - font-weight: 600; - text-decoration: none; - color: #fff; - background: var(--ty-color-primary); - box-shadow: 0 2px 8px color-mix(in srgb, var(--ty-color-primary) 30%, transparent); - transition: transform 0.2s, box-shadow 0.2s, filter 0.2s; - - &:hover { - transform: translateY(-1px); - box-shadow: 0 4px 16px color-mix(in srgb, var(--ty-color-primary) 40%, transparent); - filter: brightness(1.08); - } - - &:active { - transform: translateY(0); - } -} - -.btnArrow { - transition: transform 0.2s; - - .btnPrimary:hover & { - transform: translateX(3px); - } -} - -.btnSecondary { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 28px; - border-radius: 10px; - font-size: 15px; - font-weight: 600; - text-decoration: none; - color: var(--ty-color-text); - background: transparent; - border: 1px solid var(--ty-color-border); - transition: border-color 0.2s, background 0.2s, transform 0.2s; - - &:hover { - background: var(--ty-color-fill-secondary); - border-color: var(--ty-color-border-secondary); - transform: translateY(-1px); - } -} - -/* ─── Stats Ribbon ─── */ -.stats { - display: flex; - justify-content: center; - gap: 60px; - flex-wrap: wrap; - padding: 48px 24px; - border-top: 1px solid var(--ty-color-border-secondary); - border-bottom: 1px solid var(--ty-color-border-secondary); - background: color-mix(in srgb, var(--ty-color-primary) 3%, transparent); -} - -.stat { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - animation: fadeInUp 0.5s ease both; -} - -.statValue { - font-family: var(--font-heading), system-ui, sans-serif; - font-size: 36px; - font-weight: 700; - letter-spacing: -0.03em; - color: var(--ty-color-primary); -} - -.statLabel { - font-size: 13px; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--ty-color-text-tertiary); -} - -/* ─── Categories Section ─── */ -.categories { - max-width: 1080px; - margin: 0 auto; - padding: 80px 24px; -} - -.sectionHeader { - text-align: center; - margin-bottom: 48px; -} - -.sectionTitle { - font-family: var(--font-heading), system-ui, sans-serif; - font-size: 32px; - font-weight: 700; - letter-spacing: -0.03em; - margin: 0 0 12px; - color: var(--ty-color-text); -} - -.sectionDesc { - font-size: 16px; - color: var(--ty-color-text-secondary); - margin: 0; - line-height: 1.6; -} - -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - gap: 14px; -} - -.card { - display: flex; - align-items: center; - gap: 16px; - padding: 18px 20px; - border-radius: 12px; - text-decoration: none; - color: var(--ty-color-text); - background: var(--ty-color-bg-container); - border: 1px solid var(--ty-color-border-secondary); - transition: border-color 0.2s, box-shadow 0.25s, transform 0.2s; - animation: fadeInUp 0.5s ease both; - - &:hover { - border-color: var(--ty-color-primary); - transform: translateY(-3px); - box-shadow: - 0 4px 12px color-mix(in srgb, var(--ty-color-primary) 8%, transparent), - 0 12px 32px rgba(0, 0, 0, 0.06); - - .cardArrow { - opacity: 1; - transform: translateX(0); - } - } -} - -.cardIcon { - display: flex; - align-items: center; - justify-content: center; - width: 44px; - height: 44px; - border-radius: 11px; - font-size: 20px; - flex-shrink: 0; - background: color-mix(in srgb, var(--ty-color-primary) 8%, transparent); -} - -.cardTitle { - margin: 0 0 2px; - font-size: 15px; - font-weight: 600; - letter-spacing: -0.01em; -} - -.cardCount { - margin: 0; - font-size: 13px; - color: var(--ty-color-text-tertiary); -} - -.cardArrow { - margin-left: auto; - font-size: 16px; - color: var(--ty-color-primary); - opacity: 0; - transform: translateX(-6px); - transition: opacity 0.2s, transform 0.2s; -} - -/* ─── CTA Section ─── */ -.cta { - position: relative; - text-align: center; - padding: 80px 24px; - border-top: 1px solid var(--ty-color-border-secondary); - background: color-mix(in srgb, var(--ty-color-primary) 3%, transparent); - overflow: hidden; -} - -.ctaTitle { - font-family: var(--font-heading), system-ui, sans-serif; - font-size: clamp(26px, 4vw, 36px); - font-weight: 700; - letter-spacing: -0.03em; - margin: 0 0 12px; - color: var(--ty-color-text); -} - -.ctaDesc { - font-size: 16px; - color: var(--ty-color-text-secondary); - line-height: 1.7; - margin: 0 auto 28px; - max-width: 480px; -} - -.ctaActions { - display: flex; - justify-content: center; -} - -/* ─── Footer ─── */ -.footer { - padding: 32px 24px; - text-align: center; - border-top: 1px solid var(--ty-color-border-secondary); - - p { - margin: 0; - font-size: 13px; - color: var(--ty-color-text-tertiary); - } - - a { - color: var(--ty-color-primary); - text-decoration: none; - font-weight: 500; - - &:hover { - text-decoration: underline; - } - } -} - -/* ─── Responsive ─── */ -@media (max-width: 768px) { - .hero { - padding: 70px 20px 60px; - } - - .subtitle { - font-size: 16px; - } - - .stats { - gap: 32px 48px; - padding: 36px 20px; - } - - .statValue { - font-size: 28px; - } - - .categories { - padding: 56px 20px; - } - - .grid { - grid-template-columns: 1fr; - } - - .cta { - padding: 56px 20px; - } -} - -@media (max-width: 480px) { - .hero { - padding: 56px 16px 48px; - } - - .heroActions { - flex-direction: column; - align-items: center; - } - - .btnPrimary, - .btnSecondary { - width: 100%; - max-width: 280px; - justify-content: center; - } - - .stats { - gap: 24px 36px; - } - - .statValue { - font-size: 24px; - } -} diff --git a/apps/pro/src/app/page.tsx b/apps/pro/src/app/page.tsx deleted file mode 100644 index 146474c5..00000000 --- a/apps/pro/src/app/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import Link from 'next/link'; -import { getCategories } from '../utils/blocks'; -import styles from './page.module.scss'; - -const CATEGORY_ICONS: Record = { - authentication: '🔐', - banners: '📢', - cards: '🃏', - 'form-layouts': '📝', - lists: '📋', - navbars: '🧭', - notifications: '🔔', - 'page-headers': '📄', - 'page-shells': '🏗', - 'progress-steps': '📊', - sidebars: '📑', - stats: '📈', - tables: '🗃', - 'user-cards': '👤', -}; - -export default function HomePage() { - const categories = getCategories(); - const totalBlocks = categories.reduce((sum, cat) => sum + cat.blocks.length, 0); - - return ( -
- {/* Hero section */} -
-
-
-
-
- - Open Source · Copy & Paste · Free -
-

- Production-Ready -
- UI Blocks -

-

- Beautifully crafted, ready-to-use interface blocks built entirely with - Tiny Design components. Browse, preview, and copy into your projects. -

-
- - Browse Blocks - - - - View on GitHub - -
-
-
- - {/* Stats ribbon */} -
- {[ - { value: String(totalBlocks), label: 'UI Blocks' }, - { value: String(categories.length), label: 'Categories' }, - { value: '80+', label: 'Components Used' }, - { value: '100%', label: 'TypeScript' }, - ].map((stat, i) => ( -
- {stat.value} - {stat.label} -
- ))} -
- - {/* Categories section */} -
-
-

Explore by Category

-

- Each block is a self-contained, composable UI pattern you can drop into any React project. -

-
-
- {categories.map((cat, i) => ( - - - {CATEGORY_ICONS[cat.slug] || '📦'} - -
-

{cat.label}

-

- {cat.blocks.length} {cat.blocks.length === 1 ? 'block' : 'blocks'} -

-
- - - ))} -
-
- - {/* CTA section */} -
-

Start Building Beautiful Interfaces

-

- Every block uses only Tiny Design components — no extra dependencies. - Just copy, paste, and customize. -

-
- - Get Started - - -
-
- - {/* Footer */} - -
- ); -} diff --git a/apps/pro/src/blocks/authentication/sign-in-simple.tsx b/apps/pro/src/blocks/authentication/sign-in-simple.tsx deleted file mode 100644 index eb7dc2bb..00000000 --- a/apps/pro/src/blocks/authentication/sign-in-simple.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Button, Card, Checkbox, Divider, Flex, Form, Input, InputPassword, Typography } from '@tiny-design/react'; -import { IconLock, IconGoogle, IconGithub } from '@tiny-design/icons'; - -const { Heading, Text } = Typography; - -export default function SignInSimple() { - return ( - - -
- -
- -
- Welcome back - - Sign in to access your team workspace - -
- - - - - - - or continue with email - -
- - - - - - - - Remember me - Forgot password? - - -
- - - Don't have an account? Create one free - -
-
-
- ); -} diff --git a/apps/pro/src/blocks/authentication/sign-up-simple.tsx b/apps/pro/src/blocks/authentication/sign-up-simple.tsx deleted file mode 100644 index 4549e813..00000000 --- a/apps/pro/src/blocks/authentication/sign-up-simple.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Button, Card, Checkbox, Flex, Form, Input, InputPassword, Progress, Typography } from '@tiny-design/react'; -import { IconAddUser } from '@tiny-design/icons'; - -const { Heading, Text } = Typography; - -export default function SignUpSimple() { - return ( - - -
- -
- -
- Create your account - - Start your 14-day trial with a clean setup flow - -
- -
- - - - - - - - - - - - - - -
- - Password strength - Strong - - -
- - - I agree to the Terms of Service and Privacy Policy - - - -
- - - Already have an account? Sign in - -
-
-
- ); -} diff --git a/apps/pro/src/blocks/banners/alert-banner.tsx b/apps/pro/src/blocks/banners/alert-banner.tsx deleted file mode 100644 index ef12be5d..00000000 --- a/apps/pro/src/blocks/banners/alert-banner.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Alert, Flex, Tag, Typography } from '@tiny-design/react'; -import { - IconCheckCircle, - IconCalendar, - IconWarning, - IconCreditCard, - IconArrowRight, -} from '@tiny-design/icons'; - -const { Text } = Typography; - -const iconBox = (gradient: string, shadow: string): React.CSSProperties => ({ - width: 36, - height: 36, - borderRadius: 10, - marginRight: 12, - background: gradient, - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - boxShadow: `0 3px 10px ${shadow}`, - flexShrink: 0, -}); - -const linkStyle = (color: string): React.CSSProperties => ({ - color, - fontWeight: 600, - fontSize: 13, - cursor: 'pointer', - display: 'inline-flex', - alignItems: 'center', - gap: 3, - textDecoration: 'none', -}); - -export default function AlertBanner() { - return ( - - - - - } - title={ - - Deployment successful - - Live - - - } - > - - - All 24 checks passed — your app is now live at production. - - - View logs - - - - - - - - } - title={ - Scheduled maintenance - } - > - - - Systems will be briefly unavailable on March 30, 2:00–4:00 AM UTC. - - - View details - - - - - - - - } - title={ - - Usage limit approaching - - 89% - - - } - > - - - You've consumed 89% of your monthly API quota. Upgrade to avoid interruption. - - - Upgrade plan - - - - - - - - } - title={ - - Payment failed - - Action required - - - } - > - - - We couldn't charge your Visa ending in 4242. Please update your billing info. - - - Update card - - - - - ); -} diff --git a/apps/pro/src/blocks/banners/promo-banner.tsx b/apps/pro/src/blocks/banners/promo-banner.tsx deleted file mode 100644 index 30a5a4ad..00000000 --- a/apps/pro/src/blocks/banners/promo-banner.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Button, Flex, Tag, Typography } from '@tiny-design/react'; -import { IconFire } from '@tiny-design/icons'; - -const { Text } = Typography; - -export default function PromoBanner() { - return ( -
-
-
- - - - - New - - - - Tiny Design v2.0 is here — redesigned components, dark mode, and 40+ new blocks. - - - -
- ); -} diff --git a/apps/pro/src/blocks/cards/profile-card.tsx b/apps/pro/src/blocks/cards/profile-card.tsx deleted file mode 100644 index f81409d7..00000000 --- a/apps/pro/src/blocks/cards/profile-card.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Avatar, Button, Card, Divider, Flex, Tag, Typography } from '@tiny-design/react'; -import { IconBriefcase } from '@tiny-design/icons'; - -const { Heading, Text } = Typography; - -export default function ProfileCard() { - return ( - - -
- -
- - - JD - -
- Jane Doe - - - Software Engineer - -
- - Building beautiful UIs with Tiny Design. Open-source enthusiast and design systems advocate. - - - React - TypeScript - Design Systems - -
- - - - - - 128 - Projects - - - 1.2k - Followers - - - 384 - Following - - - - - - - -
- - - ); -} diff --git a/apps/pro/src/blocks/cards/stats-card.tsx b/apps/pro/src/blocks/cards/stats-card.tsx deleted file mode 100644 index 7b0d84af..00000000 --- a/apps/pro/src/blocks/cards/stats-card.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Card, Flex, Statistic, Tag, Typography } from '@tiny-design/react'; -import { IconWallet, IconTeam, IconPieChart, IconBullish } from '@tiny-design/icons'; - -const { Text } = Typography; - -const stats = [ - { title: 'Total Revenue', value: 45231.89, prefix: '$', change: '+20.1%', up: true, icon: IconWallet, color: '#1d4ed8', bg: '#eff6ff' }, - { title: 'Subscribers', value: 2350, change: '+180', up: true, icon: IconTeam, color: '#0f766e', bg: '#ecfeff' }, - { title: 'Conversion', value: 12.5, suffix: '%', change: '+4.3%', up: true, icon: IconBullish, color: '#15803d', bg: '#f0fdf4' }, - { title: 'Bounce Rate', value: 24.5, suffix: '%', change: '-2.1%', up: false, icon: IconPieChart, color: '#b45309', bg: '#fffbeb' }, -]; - -export default function StatsCard() { - return ( -
- - {stats.map((s) => { - const Icon = s.icon; - return ( - -
- -
- - {s.title} - - -
-
- -
-
-
- - - {s.change} - - vs last month - -
-
-
- ); - })} -
-
- ); -} diff --git a/apps/pro/src/blocks/form-layouts/contact-form.tsx b/apps/pro/src/blocks/form-layouts/contact-form.tsx deleted file mode 100644 index f5fc2ebb..00000000 --- a/apps/pro/src/blocks/form-layouts/contact-form.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Button, Card, Flex, Form, Input, NativeSelect, Textarea, Typography } from '@tiny-design/react'; -import { IconComment, IconCustomerSupport } from '@tiny-design/icons'; - -const { Heading, Text } = Typography; - -export default function ContactForm() { - return ( -
- -
- -
- -
- Get in touch - - Have a question or need help? Fill out the form below and our team will get back to you within 24 hours. - -
- - -
-
- - - - - - - - - - - - - - Select a topic... - General Inquiry - Technical Support - Billing Question - Enterprise Sales - - - -