@qijenchen/design-system 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +93 -0
- package/src/README.md +32 -0
- package/src/components/Accordion/accordion.tsx +104 -0
- package/src/components/Alert/alert.tsx +188 -0
- package/src/components/AppShell/_demo-helpers.tsx +198 -0
- package/src/components/AppShell/app-shell.tsx +364 -0
- package/src/components/AspectRatio/aspect-ratio.tsx +58 -0
- package/src/components/Avatar/avatar.tsx +368 -0
- package/src/components/Badge/badge.tsx +104 -0
- package/src/components/Breadcrumb/breadcrumb.tsx +609 -0
- package/src/components/BulkActionBar/bulk-action-bar.tsx +156 -0
- package/src/components/Button/button-group.tsx +96 -0
- package/src/components/Button/button.tsx +539 -0
- package/src/components/Calendar/calendar.tsx +411 -0
- package/src/components/Carousel/carousel.tsx +371 -0
- package/src/components/Chart/chart.tsx +376 -0
- package/src/components/Checkbox/checkbox-group.tsx +94 -0
- package/src/components/Checkbox/checkbox.tsx +237 -0
- package/src/components/Chip/chip.tsx +359 -0
- package/src/components/CircularProgress/circular-progress.tsx +204 -0
- package/src/components/Coachmark/coachmark.tsx +255 -0
- package/src/components/Combobox/combobox.tsx +826 -0
- package/src/components/Command/command.tsx +187 -0
- package/src/components/DataTable/active-editor-controller.ts +72 -0
- package/src/components/DataTable/cell-registry.tsx +520 -0
- package/src/components/DataTable/column-types.ts +180 -0
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +261 -0
- package/src/components/DataTable/data-table-filter-panel.tsx +813 -0
- package/src/components/DataTable/data-table-interaction-layer.tsx +483 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +210 -0
- package/src/components/DataTable/data-table.css +165 -0
- package/src/components/DataTable/data-table.tsx +2924 -0
- package/src/components/DataTable/filter-operators.ts +225 -0
- package/src/components/DataTable/filter-tree.ts +313 -0
- package/src/components/DataTable/lib/column-meta.ts +79 -0
- package/src/components/DateGrid/date-grid.tsx +209 -0
- package/src/components/DatePicker/date-picker.tsx +1114 -0
- package/src/components/DescriptionList/description-list.tsx +141 -0
- package/src/components/Dialog/dialog.tsx +267 -0
- package/src/components/DropdownMenu/dropdown-menu.tsx +475 -0
- package/src/components/Empty/empty.tsx +108 -0
- package/src/components/Field/field-context.ts +136 -0
- package/src/components/Field/field-types.ts +52 -0
- package/src/components/Field/field-wrapper.tsx +348 -0
- package/src/components/Field/field.tsx +535 -0
- package/src/components/FieldControlGroup/field-control-group.tsx +136 -0
- package/src/components/FileItem/file-item.tsx +322 -0
- package/src/components/FileUpload/file-upload.tsx +326 -0
- package/src/components/FileViewer/file-viewer-types.ts +76 -0
- package/src/components/FileViewer/file-viewer.tsx +1065 -0
- package/src/components/FileViewer/image-renderer.tsx +256 -0
- package/src/components/HoverCard/hover-card.tsx +79 -0
- package/src/components/Input/input.tsx +233 -0
- package/src/components/LinkInput/link-input.tsx +304 -0
- package/src/components/Menu/menu-item.tsx +334 -0
- package/src/components/NameCard/name-card.tsx +319 -0
- package/src/components/Notice/notice.tsx +196 -0
- package/src/components/NumberInput/number-input.tsx +203 -0
- package/src/components/OverflowIndicator/overflow-indicator.tsx +156 -0
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +100 -0
- package/src/components/PeoplePicker/people-picker-helpers.ts +76 -0
- package/src/components/PeoplePicker/people-picker.tsx +455 -0
- package/src/components/PeoplePicker/person-display.tsx +358 -0
- package/src/components/Popover/popover.tsx +183 -0
- package/src/components/ProgressBar/progress-bar.tsx +157 -0
- package/src/components/README.md +58 -0
- package/src/components/RadioGroup/radio-group.tsx +261 -0
- package/src/components/Rating/rating.tsx +295 -0
- package/src/components/ScrollArea/scroll-area.tsx +110 -0
- package/src/components/SegmentedControl/segmented-control.tsx +304 -0
- package/src/components/Select/select.tsx +658 -0
- package/src/components/SelectMenu/select-menu.tsx +430 -0
- package/src/components/SelectionControl/selection-item.tsx +261 -0
- package/src/components/Separator/separator.tsx +48 -0
- package/src/components/Sheet/sheet.tsx +240 -0
- package/src/components/Sidebar/sidebar.tsx +1280 -0
- package/src/components/Skeleton/skeleton.tsx +35 -0
- package/src/components/Slider/slider.tsx +158 -0
- package/src/components/Steps/steps.tsx +850 -0
- package/src/components/Switch/switch.tsx +285 -0
- package/src/components/Tabs/tabs.tsx +515 -0
- package/src/components/Tag/tag.tsx +246 -0
- package/src/components/Textarea/textarea.tsx +280 -0
- package/src/components/TimePicker/time-columns.tsx +260 -0
- package/src/components/TimePicker/time-picker.tsx +419 -0
- package/src/components/Toast/toast.tsx +129 -0
- package/src/components/Tooltip/tooltip.tsx +68 -0
- package/src/components/TreeView/tree-view.tsx +1031 -0
- package/src/hooks/use-controllable.ts +40 -0
- package/src/hooks/use-is-narrow-viewport.ts +19 -0
- package/src/hooks/use-is-touch-device.ts +21 -0
- package/src/hooks/use-overflow-items.ts +256 -0
- package/src/index.ts +85 -0
- package/src/lib/README.md +82 -0
- package/src/lib/drag-visual.ts +272 -0
- package/src/lib/i18n/README.md +60 -0
- package/src/lib/i18n/i18n-context.tsx +129 -0
- package/src/lib/multi-select-ordering.ts +61 -0
- package/src/lib/utils.ts +93 -0
- package/src/patterns/README.md +67 -0
- package/src/patterns/element-anatomy/item-anatomy.tsx +744 -0
- package/src/patterns/header-canonical/chrome-header.tsx +175 -0
- package/src/patterns/header-canonical/header-canonical.css +27 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +217 -0
- package/src/patterns/overlay-surface/overlay-surface.tsx +191 -0
- package/src/patterns/resize-handle/resize-handle.tsx +188 -0
- package/src/stories-helpers/anatomy/anatomy-utils.tsx +64 -0
- package/src/tokens/README.md +53 -0
- package/src/tokens/color/primitives.css +429 -0
- package/src/tokens/color/semantic.css +539 -0
- package/src/tokens/elevation/overlay-geometry.ts +13 -0
- package/src/tokens/layoutSpace/layoutSpace.css +36 -0
- package/src/tokens/motion/motion.css +30 -0
- package/src/tokens/motion/motion.ts +17 -0
- package/src/tokens/opacity/opacity.css +23 -0
- package/src/tokens/radius/radius.css +19 -0
- package/src/tokens/typography/typography.css +118 -0
- package/src/tokens/uiSize/icon-size.ts +52 -0
- package/src/tokens/uiSize/uiSize.css +125 -0
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qijenchen/design-system",
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src/**/*.{tsx,ts,css}",
|
|
11
|
+
"!src/**/*.stories.tsx",
|
|
12
|
+
"!src/**/*.anatomy.stories.tsx",
|
|
13
|
+
"!src/**/*.principles.stories.tsx",
|
|
14
|
+
"!src/**/*.spec.md",
|
|
15
|
+
"!src/**/*.spec.ts",
|
|
16
|
+
"!src/**/*.test.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"scripts"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./components/*": {
|
|
26
|
+
"types": "./dist/components/*/index.d.ts",
|
|
27
|
+
"import": "./dist/components/*/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./patterns/*": {
|
|
30
|
+
"types": "./dist/patterns/*/index.d.ts",
|
|
31
|
+
"import": "./dist/patterns/*/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./tokens/*": "./dist/tokens/*",
|
|
34
|
+
"./hooks/*": {
|
|
35
|
+
"types": "./dist/hooks/*.d.ts",
|
|
36
|
+
"import": "./dist/hooks/*.js"
|
|
37
|
+
},
|
|
38
|
+
"./styles/globals.css": "./dist/globals.css"
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/index.js",
|
|
41
|
+
"module": "./dist/index.js",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"sideEffects": [
|
|
44
|
+
"**/*.css"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "vite build",
|
|
48
|
+
"build:dts": "tsc -p tsconfig.json",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
53
|
+
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
54
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
55
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
56
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
57
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
58
|
+
"@radix-ui/react-hover-card": "^1.1.15",
|
|
59
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
60
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
61
|
+
"@radix-ui/react-radio-group": "^1.3.8",
|
|
62
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
63
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
64
|
+
"@radix-ui/react-slider": "^1.3.6",
|
|
65
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
66
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
67
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
68
|
+
"@radix-ui/react-toggle": "^1.1.10",
|
|
69
|
+
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
70
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
71
|
+
"@dnd-kit/core": "^6.3.1",
|
|
72
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
73
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
74
|
+
"@tanstack/react-table": "^8.21.3",
|
|
75
|
+
"@tanstack/react-virtual": "^3.13.23",
|
|
76
|
+
"class-variance-authority": "^0.7.1",
|
|
77
|
+
"clsx": "^2.1.1",
|
|
78
|
+
"cmdk": "^1.1.1",
|
|
79
|
+
"date-fns": "^4.1.0",
|
|
80
|
+
"embla-carousel-react": "^8.6.0",
|
|
81
|
+
"lucide-react": "^0.577.0",
|
|
82
|
+
"react-day-picker": "^9.14.0",
|
|
83
|
+
"react-zoom-pan-pinch": "^4.0.3",
|
|
84
|
+
"recharts": "^3.8.1",
|
|
85
|
+
"sonner": "^2.0.7",
|
|
86
|
+
"tailwind-merge": "^3.5.0"
|
|
87
|
+
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"react": ">=18.0.0",
|
|
90
|
+
"react-dom": ">=18.0.0",
|
|
91
|
+
"tailwindcss": ">=4.0.0"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Design System Home
|
|
2
|
+
|
|
3
|
+
本資料夾是 design system 的原始碼 + 設計規格 SSOT。
|
|
4
|
+
|
|
5
|
+
## 子資料夾 charter(建立新檔案前必讀對應子 dir 的 README)
|
|
6
|
+
|
|
7
|
+
| 子 dir | 收什麼 | 不收什麼 | Charter |
|
|
8
|
+
|--------|-------|---------|---------|
|
|
9
|
+
| `tokens/` | CSS 變數定義 + token spec + token stories | 元件 code、文件撰寫指南 | `tokens/README.md` |
|
|
10
|
+
| `components/` | 每個元件一個 PascalCase folder(內含 tsx / spec / stories) | 平坦 `.md` 檔、cross-cutting rule | `components/README.md` |
|
|
11
|
+
| `patterns/` | runtime UI 佈局 / 互動 primitive(.tsx + .spec.md),多元件 consume | 文件撰寫指南、governance meta rule、taxonomy | `patterns/README.md` |
|
|
12
|
+
| `hooks/` | React hooks(跨 DS 元件共用的 `use-*.ts`) | Claude Code hooks(那屬 `.claude/hooks/`) | N/A |
|
|
13
|
+
| `stories-helpers/` | Storybook 共用 helper(非 runtime,僅 `.stories.tsx` / `.anatomy.stories.tsx` 消費的 anatomy 排版 util 等) | 任何 runtime consume(應用 / 元件 / pattern code 不得 import);runtime primitive 應住 `patterns/` | N/A |
|
|
14
|
+
|
|
15
|
+
## 本層級(`packages/design-system/src/` 根)只收 `README.md`
|
|
16
|
+
|
|
17
|
+
所有 DS 內容必屬於某個子 dir(tokens / components / patterns / hooks / stories-helpers)。即使跨 pattern 的 taxonomy(如 4-Family Model)也住在最相關的 pattern topic 資料夾內(`patterns/element-anatomy/element-anatomy.spec.md`)—— 這樣 folder = topic home,不需要頂層 flat 檔案。
|
|
18
|
+
|
|
19
|
+
若未來真有 scope 橫跨 3+ 子 dir 且不屬任一 topic 的純 meta doc,才重新評估是否加頂層檔(屆時更新本 charter)。
|
|
20
|
+
|
|
21
|
+
## 不屬於本資料夾的 DS 相關內容
|
|
22
|
+
|
|
23
|
+
| 內容類型 | 實際位置 |
|
|
24
|
+
|---------|---------|
|
|
25
|
+
| AI 工作流 / workflow guide(寫 story / 做 prototype / audit) | `.claude/skills/<skill>/` |
|
|
26
|
+
| AI 每 session 需要的 signal rule | `CLAUDE.md` |
|
|
27
|
+
| AI session 狀態(audit progress / tech debt) | `~/.claude/projects/.../memory/` |
|
|
28
|
+
| Tool-level 機械檢查 | `.claude/hooks/` |
|
|
29
|
+
|
|
30
|
+
## 建立新檔案決策
|
|
31
|
+
|
|
32
|
+
不確定檔案放這裡還是別處 → **先 Read 該目標 dir 的 README.md 再 Write**。這是 CLAUDE.md 的硬規則。
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
|
3
|
+
import { ChevronDown } from 'lucide-react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Accordion — Radix Accordion + 本 DS token
|
|
8
|
+
*
|
|
9
|
+
* 結構對齊 shadcn/ui accordion(Accordion / AccordionItem / AccordionTrigger /
|
|
10
|
+
* AccordionContent),但視覺全改本 DS token。
|
|
11
|
+
*
|
|
12
|
+
* ── 視覺差異 vs shadcn 預設 ──
|
|
13
|
+
* Shadcn 預設 hover 加底線(web 早期 link style),本 DS 改為文字色 tint
|
|
14
|
+
* (`hover:text-fg-secondary`)——維持現代 SaaS 質感(Notion / Linear / Stripe 皆不用
|
|
15
|
+
* 底線),但保留 hover 顏色變化作為可點擊提示(user 決策 2026-04-20)。
|
|
16
|
+
* Chevron 用 Lucide + 本 DS icon size(16px),rotate 動畫 200ms。
|
|
17
|
+
*
|
|
18
|
+
* ── 使用情境 ──
|
|
19
|
+
* FAQ / settings section 收合 / 多區塊表單分組 / 進階選項可隱藏。
|
|
20
|
+
* 不用於「單純顯示 / 隱藏單一區塊」(那是 Collapsible,本 DS 尚未建立;用 details 或
|
|
21
|
+
* 自組 toggle),Accordion 是「多個 item 可互斥或獨立收合」的 pattern。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const Accordion = AccordionPrimitive.Root
|
|
25
|
+
|
|
26
|
+
const AccordionItem = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
29
|
+
>(({ className, ...props }, ref) => (
|
|
30
|
+
<AccordionPrimitive.Item
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn('border-b border-divider', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
))
|
|
36
|
+
AccordionItem.displayName = 'AccordionItem'
|
|
37
|
+
|
|
38
|
+
const AccordionTrigger = React.forwardRef<
|
|
39
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
40
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
41
|
+
>(({ className, children, ...props }, ref) => (
|
|
42
|
+
<AccordionPrimitive.Header className="flex">
|
|
43
|
+
<AccordionPrimitive.Trigger
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
'flex flex-1 items-center justify-between gap-2',
|
|
47
|
+
'py-4 text-body font-medium text-foreground text-left',
|
|
48
|
+
'transition-colors hover:text-fg-secondary',
|
|
49
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
50
|
+
// AccordionTrigger 單一 text-style 列 → semantic `text-fg-disabled`(非 opacity);Button canonical 對齊
|
|
51
|
+
'disabled:text-fg-disabled disabled:pointer-events-none',
|
|
52
|
+
"[&[data-state=open]>svg]:rotate-180",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
<ChevronDown
|
|
59
|
+
size={16}
|
|
60
|
+
className="shrink-0 text-fg-muted transition-transform duration-200"
|
|
61
|
+
aria-hidden
|
|
62
|
+
/>
|
|
63
|
+
</AccordionPrimitive.Trigger>
|
|
64
|
+
</AccordionPrimitive.Header>
|
|
65
|
+
))
|
|
66
|
+
AccordionTrigger.displayName = 'AccordionTrigger'
|
|
67
|
+
|
|
68
|
+
const AccordionContent = React.forwardRef<
|
|
69
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
70
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
71
|
+
>(({ className, children, ...props }, ref) => (
|
|
72
|
+
<AccordionPrimitive.Content
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn(
|
|
75
|
+
'overflow-hidden text-body text-fg-secondary',
|
|
76
|
+
'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
|
|
77
|
+
)}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<div className={cn('pb-4', className)}>{children}</div>
|
|
81
|
+
</AccordionPrimitive.Content>
|
|
82
|
+
))
|
|
83
|
+
AccordionContent.displayName = 'AccordionContent'
|
|
84
|
+
|
|
85
|
+
// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)
|
|
86
|
+
// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
|
|
87
|
+
export const accordionMeta = {
|
|
88
|
+
component: 'Accordion',
|
|
89
|
+
family: null, // non-family composite / overlay / layout
|
|
90
|
+
variants: {
|
|
91
|
+
|
|
92
|
+
},
|
|
93
|
+
sizes: {
|
|
94
|
+
|
|
95
|
+
},
|
|
96
|
+
states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
|
|
97
|
+
tokens: {
|
|
98
|
+
bg: [],
|
|
99
|
+
fg: ['text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],
|
|
100
|
+
ring: ['ring-ring'],
|
|
101
|
+
},
|
|
102
|
+
} as const
|
|
103
|
+
|
|
104
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { Notice, useInverseTheme, SUBTLE_ICON_COLOR, type NoticeVariant } from '@/design-system/components/Notice/notice'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Alert — inline / fixed 通知
|
|
9
|
+
*
|
|
10
|
+
* ── data-theme 必須搭配 text-foreground ──
|
|
11
|
+
* CSS color 從 body 繼承已解析值,data-theme 只改 --foreground 不改 color。
|
|
12
|
+
* 在 theme boundary 設 text-foreground 強制 re-resolve。
|
|
13
|
+
*
|
|
14
|
+
* ── Appearance ──
|
|
15
|
+
* subtle: 淺底色 + 四邊 1px border(色相 hover 色)。不設 data-theme,跟隨頁面。
|
|
16
|
+
* solid: 跟 Toast 對齊:
|
|
17
|
+
* neutral → data-theme={inverse} + bg-surface-raised(同層翻轉)
|
|
18
|
+
* info/success/error → bg on outer, data-theme="dark" on inner
|
|
19
|
+
* warning → bg on outer, data-theme="light" on inner
|
|
20
|
+
*
|
|
21
|
+
* ── Placement ──
|
|
22
|
+
* inline: rounded-md(card-level 圓角,非 overlay — 因 Alert 在頁面流內,非 floating)
|
|
23
|
+
* fixed: 無圓角,full-width,無 border
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const SUBTLE_CONTAINER: Record<NoticeVariant, string> = {
|
|
27
|
+
neutral: 'bg-muted border border-border',
|
|
28
|
+
info: 'bg-info-subtle border border-[var(--info-hover)]',
|
|
29
|
+
success: 'bg-success-subtle border border-[var(--success-hover)]',
|
|
30
|
+
warning: 'bg-warning-subtle border border-[var(--warning-hover)]',
|
|
31
|
+
error: 'bg-error-subtle border border-[var(--error-hover)]',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SOLID_HUE_BG: Record<string, string> = {
|
|
35
|
+
info: 'bg-info',
|
|
36
|
+
success: 'bg-success',
|
|
37
|
+
warning: 'bg-warning',
|
|
38
|
+
error: 'bg-error',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SOLID_HUE_THEME: Record<string, string> = {
|
|
42
|
+
info: 'dark',
|
|
43
|
+
success: 'dark',
|
|
44
|
+
warning: 'light',
|
|
45
|
+
error: 'dark',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const alertVariants = cva('w-full overflow-hidden', {
|
|
49
|
+
variants: {
|
|
50
|
+
placement: {
|
|
51
|
+
inline: 'rounded-md',
|
|
52
|
+
fixed: 'rounded-none border-none',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultVariants: { placement: 'inline' },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export interface AlertProps
|
|
59
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>,
|
|
60
|
+
VariantProps<typeof alertVariants> {
|
|
61
|
+
variant?: NoticeVariant
|
|
62
|
+
appearance?: 'subtle' | 'solid'
|
|
63
|
+
title: React.ReactNode
|
|
64
|
+
description?: React.ReactNode
|
|
65
|
+
endContent?: React.ReactNode
|
|
66
|
+
dismissible?: boolean
|
|
67
|
+
onDismiss?: () => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
|
|
71
|
+
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
72
|
+
(
|
|
73
|
+
{
|
|
74
|
+
variant = 'neutral',
|
|
75
|
+
appearance = 'subtle',
|
|
76
|
+
placement,
|
|
77
|
+
title,
|
|
78
|
+
description,
|
|
79
|
+
endContent,
|
|
80
|
+
dismissible = true,
|
|
81
|
+
onDismiss,
|
|
82
|
+
className,
|
|
83
|
+
...props
|
|
84
|
+
},
|
|
85
|
+
ref,
|
|
86
|
+
) => {
|
|
87
|
+
const inverseTheme = useInverseTheme()
|
|
88
|
+
const isSolid = appearance === 'solid'
|
|
89
|
+
const iconClassName = !isSolid ? SUBTLE_ICON_COLOR[variant] : undefined
|
|
90
|
+
|
|
91
|
+
// ── Live region 由 wrapping consumer 擁有(WAI-ARIA + Atlassian / Polaris / Material 共識) ──
|
|
92
|
+
// Alert 是 outer host —— 自己擁有 role + aria-live;Notice(inner layout)不再帶 role,
|
|
93
|
+
// 避免 nested live region 造成 screen reader 重複朗讀。
|
|
94
|
+
// - error / warning → role="alert" + aria-live="assertive"(中斷,使用者必須立刻知道)
|
|
95
|
+
// - info / success / neutral → role="status" + aria-live="polite"(空閒朗讀,不打斷)
|
|
96
|
+
const isCritical = variant === 'error' || variant === 'warning'
|
|
97
|
+
const liveRole = isCritical ? 'alert' : 'status'
|
|
98
|
+
const liveLevel = isCritical ? 'assertive' : 'polite'
|
|
99
|
+
|
|
100
|
+
// placement="fixed" 用 loose px(density-aware)讓 Alert 嵌在更大佈局內時跟周圍
|
|
101
|
+
// loose-padding 元素(Toolbar / BulkActionBar / DataTable 等)左右對齊。
|
|
102
|
+
// py 維持 py-3 fixed(notification banner family canonical,垂直不隨 density)。
|
|
103
|
+
const noticeEl = (
|
|
104
|
+
<Notice
|
|
105
|
+
variant={variant}
|
|
106
|
+
title={title}
|
|
107
|
+
description={description}
|
|
108
|
+
endContent={endContent}
|
|
109
|
+
dismissible={dismissible}
|
|
110
|
+
onDismiss={onDismiss}
|
|
111
|
+
iconClassName={iconClassName}
|
|
112
|
+
className={placement === 'fixed' ? 'px-[var(--layout-space-loose)]' : undefined}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// ── Subtle ──
|
|
117
|
+
if (!isSolid) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
ref={ref}
|
|
121
|
+
role={liveRole}
|
|
122
|
+
aria-live={liveLevel}
|
|
123
|
+
className={cn(alertVariants({ placement }), SUBTLE_CONTAINER[variant], className)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
{noticeEl}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Solid neutral (inverse: bg + theme 同層) ──
|
|
132
|
+
if (variant === 'neutral') {
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
ref={ref}
|
|
136
|
+
role={liveRole}
|
|
137
|
+
aria-live={liveLevel}
|
|
138
|
+
data-theme={inverseTheme}
|
|
139
|
+
className={cn(alertVariants({ placement }), 'bg-surface-raised text-foreground', className)}
|
|
140
|
+
{...props}
|
|
141
|
+
>
|
|
142
|
+
{noticeEl}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Solid 有色相: bg outer + data-theme inner ──
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
ref={ref}
|
|
151
|
+
role={liveRole}
|
|
152
|
+
aria-live={liveLevel}
|
|
153
|
+
className={cn(alertVariants({ placement }), SOLID_HUE_BG[variant], className)}
|
|
154
|
+
{...props}
|
|
155
|
+
>
|
|
156
|
+
<div data-theme={SOLID_HUE_THEME[variant]} className="text-foreground">
|
|
157
|
+
{noticeEl}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
Alert.displayName = 'Alert'
|
|
164
|
+
|
|
165
|
+
// Story auto-compile metadata — Phase 2 fill(2026-05-15)
|
|
166
|
+
// Variants = NoticeVariant 5 hues(prop name `variant`,cva 內僅 placement,色相由 SUBTLE_CONTAINER / SOLID_HUE_BG map 控)
|
|
167
|
+
// Sizes = none(Alert 視覺尺寸繼承 Notice primitive,不隨 size 變;見 spec「為何無 SizeMatrix」)
|
|
168
|
+
export const alertMeta = {
|
|
169
|
+
component: 'Alert',
|
|
170
|
+
family: null, // non-family composite / overlay / layout
|
|
171
|
+
variants: {
|
|
172
|
+
neutral: { purpose: '中性提示(系統公告、非緊急說明);無情緒色' },
|
|
173
|
+
info: { purpose: '資訊性提示(版本更新、流程說明);藍色 hue' },
|
|
174
|
+
success: { purpose: '成功狀態的持久性宣告(綁定生效、付款完成需保留確認)' },
|
|
175
|
+
warning: { purpose: '警告但非阻斷(方案到期、需更新付款方式);最高頻' },
|
|
176
|
+
error: { purpose: '錯誤但非阻斷(系統錯誤可重試、API 失敗摘要);aria-live=assertive' },
|
|
177
|
+
},
|
|
178
|
+
sizes: {},
|
|
179
|
+
states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
|
|
180
|
+
tokens: {
|
|
181
|
+
bg: ['bg-error', 'bg-error-subtle', 'bg-info', 'bg-info-subtle', 'bg-muted', 'bg-success', 'bg-success-subtle', 'bg-surface-raised', 'bg-warning', 'bg-warning-subtle'],
|
|
182
|
+
fg: ['text-foreground'],
|
|
183
|
+
ring: [],
|
|
184
|
+
},
|
|
185
|
+
defaultVariant: 'neutral',
|
|
186
|
+
} as const
|
|
187
|
+
|
|
188
|
+
export { Alert, alertVariants }
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// AppShell stories 共用 helper(對齊 sidebar.stories.tsx IconCollapse baseline)
|
|
2
|
+
// @story-baseline: packages/design-system/src/components/Sidebar/sidebar.stories.tsx#IconCollapse
|
|
3
|
+
//
|
|
4
|
+
// **嚴格**對齊既有 production-grade Sidebar story baseline,避免 AppShell stories 跟
|
|
5
|
+
// Sidebar 既有範例視覺偏移(2026-05-20 user 抓 anti-drift)。
|
|
6
|
+
// Showcase + Anatomy stories 全部 consume 此 file 不重發明 simplified mock。
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Inbox,
|
|
10
|
+
Calendar,
|
|
11
|
+
Settings,
|
|
12
|
+
Users,
|
|
13
|
+
BarChart3,
|
|
14
|
+
LayoutDashboard,
|
|
15
|
+
} from 'lucide-react'
|
|
16
|
+
import {
|
|
17
|
+
Sidebar,
|
|
18
|
+
SidebarHeader,
|
|
19
|
+
SidebarContent,
|
|
20
|
+
SidebarGroup,
|
|
21
|
+
SidebarGroupContent,
|
|
22
|
+
SidebarMenu,
|
|
23
|
+
SidebarMenuItem,
|
|
24
|
+
SidebarMenuButton,
|
|
25
|
+
SidebarFooter,
|
|
26
|
+
SidebarTrigger,
|
|
27
|
+
} from '@/design-system/components/Sidebar/sidebar'
|
|
28
|
+
import { ChromeHeader } from '@/design-system/patterns/header-canonical/chrome-header'
|
|
29
|
+
import {
|
|
30
|
+
ItemAvatar,
|
|
31
|
+
} from '@/design-system/patterns/element-anatomy/item-anatomy'
|
|
32
|
+
import { Avatar } from '@/design-system/components/Avatar/avatar'
|
|
33
|
+
import {
|
|
34
|
+
NameCard,
|
|
35
|
+
NameCardDefaultActions,
|
|
36
|
+
} from '@/design-system/components/NameCard/name-card'
|
|
37
|
+
|
|
38
|
+
// ── MAIN_NAV(對齊 sidebar.stories.tsx baseline)────────────────────────
|
|
39
|
+
|
|
40
|
+
export const MAIN_NAV = [
|
|
41
|
+
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
|
42
|
+
{ id: 'inbox', label: 'Inbox', icon: Inbox },
|
|
43
|
+
{ id: 'team', label: 'Team', icon: Users },
|
|
44
|
+
{ id: 'insights', label: 'Insights', icon: BarChart3 },
|
|
45
|
+
{ id: 'calendar', label: 'Calendar', icon: Calendar },
|
|
46
|
+
{ id: 'settings', label: 'Settings', icon: Settings },
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
// ── WorkspaceBrand(對齊 sidebar.stories.tsx)────────────────────────────
|
|
50
|
+
|
|
51
|
+
// 2026-05-21 v15 — Chrome header avatar SSOT, semantic-correct revise(per user 抓 v14
|
|
52
|
+
// RowSizeProvider hijack 是 semantic 漂移 + 「想辦法在語言正確下修到正確」directive):
|
|
53
|
+
//
|
|
54
|
+
// Chrome header **不是 row context**(per `item-anatomy.spec.md:550` 規則 scope 是 row primitive
|
|
55
|
+
// consumer + `header-canonical.spec.md` 4.5 chrome header avatar SSOT)。Chrome header avatar
|
|
56
|
+
// 是 spec-level canonical 24px、density-fixed、row-size-fixed,跟 row-anatomy 的 sm/md/lg
|
|
57
|
+
// lookup 邏輯無關。
|
|
58
|
+
//
|
|
59
|
+
// 因此 chrome header 內 avatar **用 raw `<Avatar size={24}>`,不用 `<ItemAvatar>`**:
|
|
60
|
+
// - ItemAvatar 透過 RowSizeContext lookup 是 row primitive(Sidebar / SelectMenu / TreeView 等)
|
|
61
|
+
// 的 anatomy helper,目的避免 asChild consumer 寫死跨 row size 漂移
|
|
62
|
+
// - Chrome header 沒有 sm/md/lg row size 概念,無 lookup 需求,用 raw Avatar 才語義正確
|
|
63
|
+
// - 父 `<div flex items-center>` 已 provide 縱向對齊,ItemPrefix wrapper(`h-[1lh]`)冗餘
|
|
64
|
+
//
|
|
65
|
+
// SSOT 鎖定:`<Avatar size={24}>` 對應 `--chrome-header-avatar-size: 1.5rem` token
|
|
66
|
+
// (`header-canonical.css`)+ `header-canonical.spec.md` 4.5 canonical「24px raw Avatar」。
|
|
67
|
+
// 公式端透過 var() 連動;JS 端透過 spec authority + comment cite 連動。改 24 → 同步 token + 此 hardcode。
|
|
68
|
+
export const WorkspaceBrand = () => (
|
|
69
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
70
|
+
{/* 24 per header-canonical.spec.md 4.5 chrome header avatar canonical; sync with `--chrome-header-avatar-size` */}
|
|
71
|
+
<Avatar size={24} shape="square" color="blue" solid alt="Acme Inc" />
|
|
72
|
+
<span className="text-body-lg font-medium truncate group-data-[collapsible=icon]:hidden">Acme Inc</span>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// ── UserFooter(對齊 sidebar.stories.tsx)────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export const UserFooter = () => (
|
|
79
|
+
<SidebarMenu>
|
|
80
|
+
<SidebarMenuItem>
|
|
81
|
+
<SidebarMenuButton asChild>
|
|
82
|
+
<div role="group" aria-label="當前使用者">
|
|
83
|
+
<ItemAvatar
|
|
84
|
+
alt="Alan Chen"
|
|
85
|
+
color="blue"
|
|
86
|
+
hoverCard={
|
|
87
|
+
<NameCard
|
|
88
|
+
name="Alan Chen"
|
|
89
|
+
subtitle="Design|D-0042"
|
|
90
|
+
avatar={{ alt: 'Alan Chen', color: 'blue' }}
|
|
91
|
+
status="online"
|
|
92
|
+
actions={<NameCardDefaultActions />}
|
|
93
|
+
/>
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
<span data-sidebar="menu-label" className="min-w-0 flex-1 truncate">Alan Chen</span>
|
|
97
|
+
</div>
|
|
98
|
+
</SidebarMenuButton>
|
|
99
|
+
</SidebarMenuItem>
|
|
100
|
+
</SidebarMenu>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// ── AcmeSidebar(完整 production-grade,對齊 sidebar IconCollapse story)──
|
|
104
|
+
// `includeWorkspaceBrand` default true(primary-sidebar 派 Linear/Notion 慣例:workspace brand 在 sidebar 頂)。
|
|
105
|
+
// `false` 用於 primary-header mode:workspace brand 移到 globalHeader 左側(GitHub logo / Slack workspace bar 慣例)。
|
|
106
|
+
|
|
107
|
+
export function AcmeSidebar({
|
|
108
|
+
viewportInsetTop,
|
|
109
|
+
includeWorkspaceBrand = true,
|
|
110
|
+
}: {
|
|
111
|
+
viewportInsetTop?: string
|
|
112
|
+
includeWorkspaceBrand?: boolean
|
|
113
|
+
} = {}) {
|
|
114
|
+
return (
|
|
115
|
+
<Sidebar collapsible="icon" viewportInsetTop={viewportInsetTop}>
|
|
116
|
+
{includeWorkspaceBrand && (
|
|
117
|
+
<SidebarHeader>
|
|
118
|
+
<WorkspaceBrand />
|
|
119
|
+
</SidebarHeader>
|
|
120
|
+
)}
|
|
121
|
+
<SidebarContent>
|
|
122
|
+
<SidebarGroup>
|
|
123
|
+
<SidebarGroupContent>
|
|
124
|
+
<SidebarMenu>
|
|
125
|
+
{MAIN_NAV.map((item) => (
|
|
126
|
+
<SidebarMenuItem key={item.id}>
|
|
127
|
+
<SidebarMenuButton
|
|
128
|
+
id={item.id}
|
|
129
|
+
startIcon={item.icon}
|
|
130
|
+
tooltip={item.label}
|
|
131
|
+
>
|
|
132
|
+
{item.label}
|
|
133
|
+
</SidebarMenuButton>
|
|
134
|
+
</SidebarMenuItem>
|
|
135
|
+
))}
|
|
136
|
+
</SidebarMenu>
|
|
137
|
+
</SidebarGroupContent>
|
|
138
|
+
</SidebarGroup>
|
|
139
|
+
</SidebarContent>
|
|
140
|
+
<SidebarFooter>
|
|
141
|
+
<UserFooter />
|
|
142
|
+
</SidebarFooter>
|
|
143
|
+
</Sidebar>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── GlobalHeader(primary-header mode 用,跨頁 chrome:WorkspaceBrand 左 + 跨頁 actions 右)──
|
|
148
|
+
// 2026-05-21 加 per user clarification「primary-header = primary-sidebar + 一條 global header」。
|
|
149
|
+
// 對齊 GitHub top nav(logo 左 / search 中 / account 右)+ Slack workspace bar 慣例。
|
|
150
|
+
// 消費 ChromeHeader(per `header-canonical.spec.md` Element + Background ownership 段:
|
|
151
|
+
// top-level chrome → 自畫 bg-surface)。
|
|
152
|
+
|
|
153
|
+
export function GlobalHeader({ rightSlot }: { rightSlot?: React.ReactNode } = {}) {
|
|
154
|
+
// 2026-05-21 v2 ship Option B(per user「primary header toggle 為了與 sidebar menu item
|
|
155
|
+
// startIcon 水平對齊...container 寬度 = sidebar-width-icon」+ Issue 2 geometry formula 落地):
|
|
156
|
+
// 用 ChromeHeader `leadingRail` slot(width = `--sidebar-width-icon` = 2*loose + icon-size)。
|
|
157
|
+
// Toggle center x = rail 寬度中點 = sidebar collapsed icon center x = 完美 vertical 對齊。
|
|
158
|
+
return (
|
|
159
|
+
<ChromeHeader className="bg-surface" leadingRail={<SidebarTrigger />}>
|
|
160
|
+
<WorkspaceBrand />
|
|
161
|
+
<div className="flex-1" />
|
|
162
|
+
{rightSlot}
|
|
163
|
+
</ChromeHeader>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── PageHeader(top-level chrome header,消費 ChromeHeader primitive)──
|
|
168
|
+
// 2026-05-20 migrate 消費 ChromeHeader,撤回自刻 `<header>` + 重複 className
|
|
169
|
+
// (per header-canonical.spec.md「6. Background ownership」段「Top-level chrome
|
|
170
|
+
// header 自畫 bg-surface」+「Element canonical」段「ChromeHeader 內部用 `<header>`」)。
|
|
171
|
+
// global header aside toggle 已撤回(2026-05-20 user「圖二 global header 不該有」)— 由
|
|
172
|
+
// DataTable rowActions Info icon 主入口(row-driven)取代,page header 純 title。
|
|
173
|
+
|
|
174
|
+
export function PageHeader({
|
|
175
|
+
title,
|
|
176
|
+
tabsSlot,
|
|
177
|
+
includeSidebarTrigger = true,
|
|
178
|
+
}: {
|
|
179
|
+
title: string
|
|
180
|
+
/**
|
|
181
|
+
* Optional tabs row(per header-canonical.spec.md W1-W6 + Background ownership 段)。
|
|
182
|
+
* 提供時 ChromeHeader 自動 column mode + suppress border + delegate paint 給 TabsList。
|
|
183
|
+
*/
|
|
184
|
+
tabsSlot?: React.ReactNode
|
|
185
|
+
/**
|
|
186
|
+
* 是否含 SidebarTrigger(2026-05-21 加 per user「primary-header mode 的 sidebar toggle 應該只放在 primary header 才對」)。
|
|
187
|
+
* - `primary-sidebar` mode = true(預設):PageHeader 是 chrome 第一層,trigger 自然在這
|
|
188
|
+
* - `primary-header` mode = false:SidebarTrigger 已在 GlobalHeader,PageHeader 不該重複
|
|
189
|
+
*/
|
|
190
|
+
includeSidebarTrigger?: boolean
|
|
191
|
+
}) {
|
|
192
|
+
return (
|
|
193
|
+
<ChromeHeader className="bg-surface" tabsSlot={tabsSlot}>
|
|
194
|
+
{includeSidebarTrigger && <SidebarTrigger />}
|
|
195
|
+
<h1 className="text-body-lg font-medium flex-1 truncate">{title}</h1>
|
|
196
|
+
</ChromeHeader>
|
|
197
|
+
)
|
|
198
|
+
}
|