From 195d1bded4e3bccfcdf2c289137a3394ec69267e Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:41:18 +0800 Subject: [PATCH] feat(frontend): refine shadcn UI foundations Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../refine-shadcn-ui-foundation/design.md | 32 +++++ .../refine-shadcn-ui-foundation/proposal.md | 26 ++++ .../specs/shared-ui-foundation/spec.md | 61 +++++++++ .../refine-shadcn-ui-foundation/tasks.md | 14 ++ web/components.json | 7 +- web/e2e/fixtures/ui-foundation.html | 2 + web/e2e/fixtures/ui-foundation.tsx | 44 +++++++ web/e2e/ui-foundation.spec.ts | 91 +++++++++++++ web/src/index.css | 39 +++++- web/src/shared/ui/button.test.ts | 6 +- web/src/shared/ui/button.tsx | 6 +- web/src/shared/ui/card.tsx | 12 +- web/src/shared/ui/input.tsx | 2 +- web/src/shared/ui/select.test.ts | 2 +- web/src/shared/ui/select.tsx | 4 +- web/src/shared/ui/table.tsx | 4 +- web/src/shared/ui/tabs.behavior.test.tsx | 123 ++++++++++++++++++ web/src/shared/ui/tabs.tsx | 101 +++++++++++--- web/src/shared/ui/textarea.tsx | 2 +- web/tailwind.config.ts | 34 +++-- 20 files changed, 545 insertions(+), 67 deletions(-) create mode 100644 openspec/changes/refine-shadcn-ui-foundation/design.md create mode 100644 openspec/changes/refine-shadcn-ui-foundation/proposal.md create mode 100644 openspec/changes/refine-shadcn-ui-foundation/specs/shared-ui-foundation/spec.md create mode 100644 openspec/changes/refine-shadcn-ui-foundation/tasks.md create mode 100644 web/e2e/fixtures/ui-foundation.html create mode 100644 web/e2e/fixtures/ui-foundation.tsx create mode 100644 web/e2e/ui-foundation.spec.ts create mode 100644 web/src/shared/ui/tabs.behavior.test.tsx diff --git a/openspec/changes/refine-shadcn-ui-foundation/design.md b/openspec/changes/refine-shadcn-ui-foundation/design.md new file mode 100644 index 00000000..e7388e8d --- /dev/null +++ b/openspec/changes/refine-shadcn-ui-foundation/design.md @@ -0,0 +1,32 @@ +## Approach + +Keep component source in `web/src/shared/ui` and use explicit shadcn aliases for UI, composition, +utilities and hooks. Compare compatible upstream source before adapting individual components; +existing APIs, variants, native attributes, refs and event behavior remain the compatibility baseline. + +CSS custom properties own semantic colors, fonts and density. Tailwind consumes these properties. +Regular density remains the default; `data-density="compact"` changes control height and surface +spacing without shrinking default control text. Explicit caller classes and styles remain effective. +Table headings and cells share horizontal padding. Portal content must retain usable independent sizing. + +Preserve self-hosted Inter and JetBrains Mono and platform-appropriate fallback fonts. Provide semantic +status colors in both themes, visible focus and invalid styling, and reduced-motion treatment. + +Keep the existing Tabs API and panel mounting behavior. Navigate enabled triggers in their rendered +order with arrow keys and Home/End; respect horizontal direction. A valid enabled selection is the +tab stop, otherwise the first enabled trigger is. Recomputing a tab stop must not change selection +or invoke `onValueChange`. An entirely disabled list has no actionable tab stop. Trigger IDs/aria-controls and panel IDs/aria-labelledby +are reserved for the generated association; other supported native attributes and refs remain available. + +## Validation + +Use component behavior tests and an isolated browser fixture for keyboard, density and validation +contracts. Verify light/dark and regular/compact combinations plus representative page layouts. +Keep test fixtures out of production entry points. Actual backend smoke results are reported separately +from UI fixtures; successful frontend tests alone do not establish backend integration health. + +## Risks + +Shared font and spacing changes affect many callers. Check existing variants and explicit class/style +overrides, table alignment, nested portals and responsive layouts before accepting the change. +No dependency migration or global component replacement is required. diff --git a/openspec/changes/refine-shadcn-ui-foundation/proposal.md b/openspec/changes/refine-shadcn-ui-foundation/proposal.md new file mode 100644 index 00000000..9b87d3f6 --- /dev/null +++ b/openspec/changes/refine-shadcn-ui-foundation/proposal.md @@ -0,0 +1,26 @@ +## Why + +Shared controls need a consistent foundation for future page work. The current shadcn configuration +does not explicitly resolve the UI directory, CSS and Tailwind disagree on the sans-serif font, +and shared controls do not expose a consistent density or validation treatment. Tabs also need +complete basic keyboard navigation while preserving optional controlled and uncontrolled values. + +## What Changes + +- Resolve shadcn aliases to the existing shared directories without replacing component APIs. +- Align self-hosted fonts and semantic light/dark tokens, and add regular/compact container density. +- Apply shared density, focus and invalid states to existing controls, cards and tables. +- Complete Tabs keyboard navigation and retain an entry point when selection is absent or invalid. +- Add behavior-focused regression coverage and verify representative existing pages. + +## Capabilities + +### New Capabilities + +- `shared-ui-foundation`: shared theme, density, component compatibility and keyboard behavior. + +## Impact + +Changes are limited to frontend configuration, theme, shared UI components and regression tests. +React 19, Tailwind 3, existing Radix primitives and current business behavior are retained. +This change does not redesign business pages or add a global shortcut system. diff --git a/openspec/changes/refine-shadcn-ui-foundation/specs/shared-ui-foundation/spec.md b/openspec/changes/refine-shadcn-ui-foundation/specs/shared-ui-foundation/spec.md new file mode 100644 index 00000000..eb665f72 --- /dev/null +++ b/openspec/changes/refine-shadcn-ui-foundation/specs/shared-ui-foundation/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: Component generation uses existing shared directories +The project SHALL configure shadcn aliases to generate UI primitives into `src/shared/ui` and +resolve imports using the existing TypeScript and Vite aliases. + +#### Scenario: Preview a new component +- **WHEN** a representative component is previewed with the compatible CLI +- **THEN** its UI file resolves into `src/shared/ui`, without a nested `ui/ui` directory +- **AND** dependencies and source changes can be reviewed before applying them + +### Requirement: Shared theme and density are consistent +The frontend SHALL use consistent self-hosted font families and semantic light/dark colors. +Shared controls SHALL support regular and compact container density without reducing default text size. + +#### Scenario: Switch density +- **WHEN** a container switches from regular to compact +- **THEN** default controls change from 36px to 32px and card/table spacing follows the shared density +- **AND** table heading and data text remain horizontally aligned +- **AND** caller-provided class and style overrides remain effective + +#### Scenario: Validation and reduced motion +- **WHEN** a form control is invalid or receives keyboard focus +- **THEN** it has visible invalid or focus treatment appropriate to the selected theme +- **WHEN** reduced motion is requested +- **THEN** nonessential shared control motion is suppressed + +### Requirement: Existing component contracts remain compatible +Shared component changes SHALL retain existing variants, native attributes, refs, events, controlled +and uncontrolled values, form semantics, portal behavior and panel mounting behavior. + +#### Scenario: Reuse an existing button variant +- **WHEN** a page renders an existing variant such as `soft` +- **THEN** the variant remains available and existing caller attributes are honored + +### Requirement: Tabs are keyboard accessible +Tabs SHALL support arrow-key navigation, Home/End and disabled-item skipping using the current +horizontal direction. At least one enabled trigger SHALL provide a keyboard entry when available. + +#### Scenario: Navigate enabled tabs +- **WHEN** the user presses a navigation key on a tab +- **THEN** focus and selection follow the intended enabled trigger in rendered order +- **AND** disabled triggers are skipped + +#### Scenario: Missing or invalid selection +- **WHEN** selection is absent, invalid, removed or disabled and an enabled trigger exists +- **THEN** the first enabled trigger remains reachable with Tab +- **AND** establishing that entry does not change selection or invoke the selection callback + +#### Scenario: All triggers are disabled +- **WHEN** every trigger is disabled +- **THEN** no trigger is exposed as an actionable keyboard entry + +### Requirement: Regression evidence is repeatable +The change SHALL include repeatable component regression tests and pass relevant frontend checks. +Browser fixtures SHALL be separate from production entry points. + +#### Scenario: Validate component behavior independently +- **WHEN** the component fixture is tested without a backend +- **THEN** keyboard and density assertions run against actual shared components +- **AND** the result is described as component verification rather than backend acceptance diff --git a/openspec/changes/refine-shadcn-ui-foundation/tasks.md b/openspec/changes/refine-shadcn-ui-foundation/tasks.md new file mode 100644 index 00000000..838220be --- /dev/null +++ b/openspec/changes/refine-shadcn-ui-foundation/tasks.md @@ -0,0 +1,14 @@ +## Implementation + +- [x] 1. Inspect existing components, callers, tokens and compatible shadcn configuration. +- [x] 2. Correct aliases and verify CLI project information and a representative dry-run. +- [x] 3. Align fonts, semantic colors, density and shared validation/focus treatment. +- [x] 4. Complete Tabs navigation and missing-selection keyboard entry behavior. +- [x] 5. Add repeatable behavior and browser regression tests. + +## Verification + +- [x] 6. Pass relevant frontend typecheck, lint, unit tests and production build. +- [x] 7. Verify light/dark, regular/compact and desktop/mobile component behavior. +- [x] 8. Check representative existing pages, overrides and production entry isolation. +- [x] 9. Record actual verification results and any unverified integration layers. diff --git a/web/components.json b/web/components.json index 7d38f61b..290cfae9 100644 --- a/web/components.json +++ b/web/components.json @@ -10,7 +10,10 @@ "cssVariables": true }, "aliases": { - "components": "@/shared/ui", - "utils": "@/shared/lib/utils" + "components": "@/shared/components", + "ui": "@/shared/ui", + "utils": "@/shared/lib/utils", + "lib": "@/shared/lib", + "hooks": "@/shared/hooks" } } diff --git a/web/e2e/fixtures/ui-foundation.html b/web/e2e/fixtures/ui-foundation.html new file mode 100644 index 00000000..125f22d7 --- /dev/null +++ b/web/e2e/fixtures/ui-foundation.html @@ -0,0 +1,2 @@ + +Shared UI regression fixture
diff --git a/web/e2e/fixtures/ui-foundation.tsx b/web/e2e/fixtures/ui-foundation.tsx new file mode 100644 index 00000000..83f793fd --- /dev/null +++ b/web/e2e/fixtures/ui-foundation.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react' +import { createRoot } from 'react-dom/client' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/shared/ui/tabs' +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/shared/ui/table' +import { Button } from '@/shared/ui/button' +import { Input } from '@/shared/ui/input' +import { Textarea } from '@/shared/ui/textarea' +import { Card, CardHeader } from '@/shared/ui/card' +import '@/index.css' + +const params = new URLSearchParams(location.search) +const mode = params.get('mode') +document.documentElement.classList.toggle('dark', params.get('theme') === 'dark') +document.documentElement.dir = params.get('dir') === 'rtl' ? 'rtl' : 'ltr' + +function Fixture() { + const [value, setValue] = useState(mode === 'missing' || mode === 'all-disabled' ? 'missing' : mode === 'empty' || mode === 'none' ? '' : mode === 'selected' ? 'second' : 'first') + const [changes, setChanges] = useState(0) + const [disabled, setDisabled] = useState(mode === 'disabled') + const [removed, setRemoved] = useState(false) + const allDisabled = mode === 'all-disabled' + return
+ + { setValue(next); setChanges(count => count + 1) }}> + First + {!removed && Second} + Third + first contentsecond contentthird content + + + + {changes} + SkillExample
+ +

Enter a valid version.

+