@qijenchen/design-system 0.1.0-beta.5 → 0.1.0-beta.8
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/README.md +154 -0
- package/dist/components/Alert/alert.d.ts +1 -1
- package/dist/components/Breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/components/Breadcrumb/breadcrumb.d.ts.map +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js +16 -21
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts +2 -2
- package/dist/components/DatePicker/date-picker.d.ts +2 -2
- package/dist/components/Dialog/dialog.d.ts +1 -1
- package/dist/components/Dialog/dialog.js.map +1 -1
- package/dist/components/DropdownMenu/dropdown-menu.d.ts +2 -2
- package/dist/components/FieldControlGroup/field-control-group.d.ts +1 -1
- package/dist/components/Input/input.d.ts +3 -3
- package/dist/components/LinkInput/link-input.d.ts +2 -2
- package/dist/components/Menu/menu-item.d.ts +2 -2
- package/dist/components/NameCard/name-card.d.ts +1 -1
- package/dist/components/NumberInput/number-input.d.ts +3 -3
- package/dist/components/PeoplePicker/people-picker-helpers.d.ts +1 -1
- package/dist/components/PeoplePicker/people-picker.d.ts +1 -1
- package/dist/components/Popover/popover.js +2 -1
- package/dist/components/Popover/popover.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts +1 -1
- package/dist/components/Select/select.d.ts +2 -2
- package/dist/components/SelectMenu/select-menu.d.ts +1 -1
- package/dist/components/SelectionControl/selection-item.d.ts +1 -1
- package/dist/components/Sheet/sheet.d.ts +1 -1
- package/dist/components/Sheet/sheet.js.map +1 -1
- package/dist/components/Sidebar/sidebar.d.ts +5 -5
- package/dist/components/Switch/switch.d.ts +1 -1
- package/dist/components/Textarea/textarea.d.ts +1 -1
- package/dist/components/TimePicker/time-picker.d.ts +2 -2
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/Toast/toast.d.ts +1 -1
- package/dist/components/TreeView/tree-view.d.ts +2 -2
- package/dist/lib/i18n/i18n-context.d.ts +3 -3
- package/dist/patterns/element-anatomy/item-anatomy.d.ts +3 -3
- package/dist/patterns/horizontal-overflow/horizontal-overflow.d.ts +1 -1
- package/dist/tokens/uiSize/icon-size.d.ts +3 -3
- package/package.json +12 -17
- package/src/components/Breadcrumb/breadcrumb.tsx +26 -16
- package/src/components/Dialog/dialog.tsx +4 -4
- package/src/components/Popover/popover.tsx +1 -1
- package/src/components/Sheet/sheet.tsx +1 -1
- package/src/components/TimePicker/time-picker.tsx +4 -4
- /package/dist/{style.css → react-day-picker.css} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @qijenchen/design-system
|
|
2
|
+
|
|
3
|
+
World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @qijenchen/design-system
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Tailwind v4 + React 18+ project required. License: UNLICENSED (internal use).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick start(consumer side,完整 4 步驟)
|
|
14
|
+
|
|
15
|
+
新 consumer 跑通 Button + tokens 需要這 4 件,缺一不可:
|
|
16
|
+
|
|
17
|
+
### 1. CSS entry — 載 tokens(必要)
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
/* globals.css (or main.css / src/index.css) */
|
|
21
|
+
@import 'tailwindcss';
|
|
22
|
+
@import '@qijenchen/design-system/styles/tokens';
|
|
23
|
+
|
|
24
|
+
/* 必要:讓 Tailwind v4 掃 DS 元件原始碼產生 utility class */
|
|
25
|
+
@source '../node_modules/@qijenchen/design-system/src/**/*.{js,ts,jsx,tsx}';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> ⚠️ **`@source` 不能省**。Tailwind v4 預設只掃 `src/**`,不掃 `node_modules`。沒這行,DS 元件內部用的 `h-field-md` / `bg-primary-hover` 等 class 不會被產出 → 元件看起來像沒套樣式(P0.2 of common-mistakes)。
|
|
29
|
+
|
|
30
|
+
### 2. App-level Provider(必要)
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// main.tsx / index.tsx
|
|
34
|
+
import { TooltipProvider } from '@qijenchen/design-system'
|
|
35
|
+
|
|
36
|
+
createRoot(document.getElementById('root')!).render(
|
|
37
|
+
<TooltipProvider delayDuration={500} skipDelayDuration={300}>
|
|
38
|
+
<App />
|
|
39
|
+
</TooltipProvider>
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> ⚠️ iconOnly Button 內建自動 tooltip,缺 `<TooltipProvider>` 會 warn + 部分元件 crash。
|
|
44
|
+
|
|
45
|
+
### 3. Sidebar context(僅用 Sidebar 時)
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { SidebarProvider } from '@qijenchen/design-system'
|
|
49
|
+
|
|
50
|
+
<SidebarProvider activeId={currentRouteId} onSelect={setActiveId}>
|
|
51
|
+
<Sidebar>...</Sidebar>
|
|
52
|
+
<main>...</main>
|
|
53
|
+
</SidebarProvider>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> Sidebar 預設 active 樣式由 `SidebarProvider activeId` driven,**不用** `isActive` prop(會破壞 single-selection 行為)。
|
|
57
|
+
|
|
58
|
+
### 4. Dark mode(可選)
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<html data-theme="dark">
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Token system 用 `data-theme="light"|"dark"` attribute 切換,非 class-based。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 使用 component
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Button, Avatar, Dialog } from '@qijenchen/design-system'
|
|
72
|
+
|
|
73
|
+
<Button variant="primary">儲存</Button>
|
|
74
|
+
<Avatar name="Wendy" />
|
|
75
|
+
<Dialog>...</Dialog>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**只用 top barrel import**(對齊 Material UI canonical)。`./components/<Name>` subpath 目前未支援 — v1.0.0 stable 才會開啟(待 cross-component export name collision rename 完成)。
|
|
79
|
+
|
|
80
|
+
Tree-shake 透過 `sideEffects: ["**/*.css"]` 配置自動 work,unused JS 被 bundler 剝除。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 字型(可選)
|
|
85
|
+
|
|
86
|
+
Token `--font-sans` stack = `Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", ...`。Roboto 沒附,如要載:
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
90
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
91
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
無此載入時,fallback 到系統字(macOS = SF Pro,Windows = Segoe UI,Linux = sans-serif default)。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Figma Make 相容性
|
|
99
|
+
|
|
100
|
+
直接走 Quick start 4 步。Figma Make 預設 React 19 + Tailwind v4 + Vite 跟 DS stack 對齊。
|
|
101
|
+
|
|
102
|
+
完整指南 → `docs/figma-make-setup.md`(本 repo)。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Storybook(consumer 看 DS 元件用法)
|
|
107
|
+
|
|
108
|
+
- Production:**https://ajenchen.github.io/design-system/**
|
|
109
|
+
- 看每個元件的「展示」/「設計規格」/「設計原則」3 層 stories
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API contract / 公開 surface
|
|
114
|
+
|
|
115
|
+
| 路徑 | 內容 |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `@qijenchen/design-system` | Top barrel — components / patterns / hooks / lib utilities |
|
|
118
|
+
| `@qijenchen/design-system/styles/tokens` | CSS aggregator(全 token + Tailwind `@theme inline`)|
|
|
119
|
+
| `@qijenchen/design-system/hooks/<name>` | 單一 hook subpath |
|
|
120
|
+
| `@qijenchen/design-system/tokens/<category>` | Token JS 模組(eg `motion`, `icon-size`)— CSS 透過 `styles/tokens` aggregator |
|
|
121
|
+
|
|
122
|
+
**禁** import:
|
|
123
|
+
- `@qijenchen/design-system/src/...`(internal source path,未來 SSOT 結構改會壞)
|
|
124
|
+
- `@qijenchen/design-system/dist/...`(internal build artifact,未來 build pipeline 改會壞)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Compatibility matrix
|
|
129
|
+
|
|
130
|
+
| Dependency | Required version | Notes |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `react` | `>= 18.0.0` | peer |
|
|
133
|
+
| `react-dom` | `>= 18.0.0` | peer |
|
|
134
|
+
| `tailwindcss` | `>= 4.0.0` | peer,Tailwind v4 only |
|
|
135
|
+
| `lucide-react` | `>= 0.400.0` | peer(consumer 已裝,DS 不重複,避免 hoisting 雙裝)|
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Troubleshooting
|
|
140
|
+
|
|
141
|
+
| 症狀 | 原因 / 修法 |
|
|
142
|
+
|---|---|
|
|
143
|
+
| 元件 render 但**完全沒樣式** | 漏 `@source` directive(Tailwind v4 不掃 node_modules),補 globals.css `@source '../node_modules/@qijenchen/design-system/**/*.{js,ts,jsx,tsx}'` |
|
|
144
|
+
| `Failed to resolve "./dist/globals.css"` | beta.6 以前 exports map bug,升 `@beta` latest (beta.7+ 已 fix) |
|
|
145
|
+
| iconOnly Button 卡死 / warn | App root 缺 `<TooltipProvider>` |
|
|
146
|
+
| Sidebar selection 行為怪 | 不用 `isActive={true}`,改用 `<SidebarProvider activeId={...}>` |
|
|
147
|
+
| Dark mode 不切換 | 確認 `<html data-theme="dark">`(attribute,非 class)|
|
|
148
|
+
| TypeScript `FieldMode` type 找不到 | beta.6 以前 .d.ts 有 `@/` alias leak,升 beta.7+(tsc-alias 已修)|
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
UNLICENSED — internal use only.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { type VariantProps } from 'class-variance-authority';
|
|
3
|
-
import { type NoticeVariant } from '
|
|
3
|
+
import { type NoticeVariant } from '../../components/Notice/notice';
|
|
4
4
|
declare const alertVariants: (props?: ({
|
|
5
5
|
placement?: "fixed" | "inline" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { type LucideIcon } from 'lucide-react';
|
|
3
|
-
import { ItemInlineActionButton } from '
|
|
3
|
+
import { ItemInlineActionButton } from '../../patterns/element-anatomy/item-anatomy';
|
|
4
4
|
/**
|
|
5
5
|
* Breadcrumb — 顯示當前頁面在階層中的位置
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breadcrumb.d.ts","sourceRoot":"","sources":["../../../src/components/Breadcrumb/breadcrumb.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAE5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,uDAAuD,CAAA;AA0E9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,KAAK,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AA0BxC,QAAA,MAAM,UAAU,0JAKd,CAAA;AAKF;;;;GAIG;AAEH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,UAAU,CAAA;CACvB;AAED,UAAU,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC1F;;;;;OAKG;IACH,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAGD,QAAA,MAAM,cAAc,8FAgLnB,CAAA;AAKD;;;;;;;;;;;;GAYG;AACH,UAAU,mBAAoB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC;IACxE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;CAClD;AAED,QAAA,MAAM,cAAc,2FA4BnB,CAAA;AAKD,UAAU,mBAAoB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,GAAG,CAAC;IACvE,wCAAwC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,UAAU,CAAA;CACvB;AAED,QAAA,MAAM,cAAc,+
|
|
1
|
+
{"version":3,"file":"breadcrumb.d.ts","sourceRoot":"","sources":["../../../src/components/Breadcrumb/breadcrumb.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAE5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,uDAAuD,CAAA;AA0E9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH,KAAK,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AA0BxC,QAAA,MAAM,UAAU,0JAKd,CAAA;AAKF;;;;GAIG;AAEH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,UAAU,CAAA;CACvB;AAED,UAAU,mBAAoB,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC1F;;;;;OAKG;IACH,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAGD,QAAA,MAAM,cAAc,8FAgLnB,CAAA;AAKD;;;;;;;;;;;;GAYG;AACH,UAAU,mBAAoB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC;IACxE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAA;CAClD;AAED,QAAA,MAAM,cAAc,2FA4BnB,CAAA;AAKD,UAAU,mBAAoB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,GAAG,CAAC;IACvE,wCAAwC;IACxC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,UAAU,CAAA;CACvB;AAED,QAAA,MAAM,cAAc,+FAyCnB,CAAA;AAKD,UAAU,mBAAoB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,MAAM,CAAC;IAC1E,wEAAwE;IACxE,SAAS,CAAC,EAAE,UAAU,CAAA;CACvB;AAED,QAAA,MAAM,cAAc,6FAqBnB,CAAA;AAKD,UAAU,wBAAyB,SAAQ,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC;IAC7E,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAGD,QAAA,MAAM,mBAAmB,gGAgBxB,CAAA;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,KAAK,uBAAuB,GAAG,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,sBAAsB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;AAEnH,QAAA,MAAM,kBAAkB,mGAavB,CAAA;AAKD,eAAO,MAAM,cAAc;;;;;;;;;;;CAejB,CAAA;AAEV,OAAO,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,kBAAkB,GACnB,CAAA;AACD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,CAAA"}
|
|
@@ -197,30 +197,25 @@ const BreadcrumbItem = React.forwardRef(
|
|
|
197
197
|
BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
198
198
|
const BreadcrumbLink = React.forwardRef(
|
|
199
199
|
({ asChild, className, children, startIcon: StartIcon, ...props }, ref) => {
|
|
200
|
-
const Comp = asChild ? Slot : "a";
|
|
201
200
|
const { size } = React.useContext(BreadcrumbContext);
|
|
202
201
|
const wrappedChildren = typeof children === "string" ? /* @__PURE__ */ jsx(TruncatedLabel, { fullText: children, children }) : children;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"transition-colors duration-150",
|
|
213
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
214
|
-
"rounded-md",
|
|
215
|
-
className
|
|
216
|
-
),
|
|
217
|
-
...props,
|
|
218
|
-
children: [
|
|
219
|
-
StartIcon && /* @__PURE__ */ jsx(StartIcon, { size: BREADCRUMB_ICON_SIZE[size], "aria-hidden": true, className: "shrink-0" }),
|
|
220
|
-
wrappedChildren
|
|
221
|
-
]
|
|
222
|
-
}
|
|
202
|
+
const sharedClassName = cn(
|
|
203
|
+
"inline-flex items-center gap-2",
|
|
204
|
+
"min-w-0 max-w-full",
|
|
205
|
+
"text-fg-secondary",
|
|
206
|
+
"hover:text-primary-hover",
|
|
207
|
+
"transition-colors duration-150",
|
|
208
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
209
|
+
"rounded-md",
|
|
210
|
+
className
|
|
223
211
|
);
|
|
212
|
+
if (asChild) {
|
|
213
|
+
return /* @__PURE__ */ jsx(Slot, { ref, className: sharedClassName, ...props, children: wrappedChildren });
|
|
214
|
+
}
|
|
215
|
+
return /* @__PURE__ */ jsxs("a", { ref, className: sharedClassName, ...props, children: [
|
|
216
|
+
StartIcon && /* @__PURE__ */ jsx(StartIcon, { size: BREADCRUMB_ICON_SIZE[size], "aria-hidden": true, className: "shrink-0" }),
|
|
217
|
+
wrappedChildren
|
|
218
|
+
] });
|
|
224
219
|
}
|
|
225
220
|
);
|
|
226
221
|
BreadcrumbLink.displayName = "BreadcrumbLink";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breadcrumb.js","sources":["../../../src/components/Breadcrumb/breadcrumb.tsx"],"sourcesContent":["// @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.\n// code-quality-allow: file-size — Breadcrumb 含 BreadcrumbList(主)+ BreadcrumbItem + BreadcrumbEllipsis + items-collapse logic,split 會破壞 collapse/overflow Tooltip subtree\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { ChevronRight, MoreHorizontal, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { ItemInlineActionButton } from '@/design-system/patterns/element-anatomy/item-anatomy'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '@/design-system/components/Tooltip/tooltip'\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n} from '@/design-system/components/DropdownMenu/dropdown-menu'\n\n// ── TruncatedLabel ────────────────────────────────────────────────────────────\n// 同 `data-table.tsx:339 TruncateCell` + `tag.tsx:138 isTruncated` SSOT pattern\n// (shared ResizeObserver + scrollWidth > clientWidth → wrap Tooltip)。\n// **TODO** future:Rule-of-3 達 → 抽 `patterns/element-anatomy/truncated-text.tsx` 共用\n// (本 component / DataTable TruncateCell / Tag inner 三處同 idiom,符合 M30 SSOT 抽取門檻)。\n\ntype RoCallback = (entry: ResizeObserverEntry) => void\nlet sharedRO: ResizeObserver | null = null\nconst sharedROCallbacks = new WeakMap<Element, RoCallback>()\nfunction getSharedRO(): ResizeObserver {\n if (sharedRO) return sharedRO\n sharedRO = new ResizeObserver((entries) => {\n entries.forEach((e) => {\n const cb = sharedROCallbacks.get(e.target)\n if (cb) cb(e)\n })\n })\n return sharedRO\n}\nfunction observeShared(el: Element, cb: RoCallback): () => void {\n const obs = getSharedRO()\n sharedROCallbacks.set(el, cb)\n obs.observe(el)\n return () => { sharedROCallbacks.delete(el); obs.unobserve(el) }\n}\n\nfunction TruncatedLabel({ children, fullText }: { children: React.ReactNode; fullText?: string }) {\n const ref = React.useRef<HTMLSpanElement>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const check = () => setIsTruncated(el.scrollWidth > el.clientWidth)\n check()\n // 2026-05-11 fix:首幀 layout 未完成 / 字型 async load → scrollWidth=clientWidth 假陰性。\n // RAF + 短延遲再驗一次,捕獲字型 / 容器尺寸後變(對齊 TruncateCell / Tag SSOT pattern)。\n const raf = requestAnimationFrame(check)\n const t = setTimeout(check, 100)\n const cleanup = observeShared(el, check)\n return () => {\n cancelAnimationFrame(raf)\n clearTimeout(t)\n cleanup()\n }\n }, [])\n // Tooltip canonical:per `tooltip.principles.stories.tsx:190`「Tooltip 是資訊補救 — 文字被\n // truncate 時才顯示完整內容。沒被截斷就不該顯示 tooltip」\n //\n // 2026-05-11 fix(playwright tooltip-on-truncate 卡 hover 沒 tooltip):\n // 原本 isTruncated=false 直接 return 裸 span / true 才 wrap Tooltip → JSX 樹結構改變\n // → React 把 span unmount + remount(因為 wrapper component 變),ref 換到新 span,\n // useEffect [] 不重跑(同 component instance)→ 觀察的 DOM 跟實際 DOM 對不上。\n // Fix:**永遠 wrap Tooltip**(同 DOM 節點生命週期);`open` 由 isTruncated 控制 —\n // 沒被 truncate 就 force `open={false}`,有 truncate 就 `undefined`(uncontrolled,\n // hover 走 Radix default behavior)。對齊 canonical「沒被截斷就不該 tooltip」。\n return (\n <Tooltip open={isTruncated ? undefined : false}>\n <TooltipTrigger asChild>\n <span ref={ref} className=\"truncate min-w-0 block\">{children}</span>\n </TooltipTrigger>\n <TooltipContent>{fullText ?? children}</TooltipContent>\n </Tooltip>\n )\n}\n\n/**\n * Breadcrumb — 顯示當前頁面在階層中的位置\n *\n * 基於 shadcn/ui Breadcrumb 結構(純 HTML nav + ol + li + a/span),\n * 橋接設計系統 token。\n *\n * ── 結構 ──\n * <Breadcrumb>\n * <BreadcrumbList size=\"md\">\n * <BreadcrumbItem>\n * <BreadcrumbLink href=\"/projects\">專案</BreadcrumbLink>\n * </BreadcrumbItem>\n * <BreadcrumbSeparator />\n * <BreadcrumbItem>\n * <BreadcrumbPage>目前頁面</BreadcrumbPage>\n * </BreadcrumbItem>\n * </BreadcrumbList>\n * </Breadcrumb>\n *\n * ── Size(跟 page title 配對) ──\n * sm text-body(14) → 建議配 text-h4(20) title —— Dialog / panel / drawer\n * md text-body(14) → 建議配 text-h3(24) title —— 一般頁面 header (預設)\n * lg text-body-lg(16) → 建議配 text-h2(32) title —— Detail page hero / landing\n *\n * ── 視覺 ──\n * Link (預設): text-fg-secondary\n * Link hover: text-primary-hover (canonical「互動高亮」, 跟 Tabs / Chip 用法一致)\n * Page (當前): text-foreground + font-medium\n * Separator: ChevronRight (size 跟 list 一致), text-fg-muted\n *\n * ── 詳見 breadcrumb.spec.md ──\n */\n\n// ── Size context ─────────────────────────────────────────────────────────────\n\ntype BreadcrumbSize = 'sm' | 'md' | 'lg'\n\ninterface BreadcrumbContextValue {\n size: BreadcrumbSize\n}\n\nconst BreadcrumbContext = React.createContext<BreadcrumbContextValue>({ size: 'md' })\n\nconst BREADCRUMB_TEXT_CLASS: Record<BreadcrumbSize, string> = {\n sm: 'text-body',\n md: 'text-body',\n lg: 'text-body-lg',\n}\n\n// Separator / ellipsis icon 尺寸 — 對齊 uiSize.spec.md「Icon Size Tier」(field-height-sm/md→16,lg→20)\n// 2026-05-18 改 per user 拍板「A 先改 16/16/20」+「做完」approval:\n// 撤回 text-flow 例外設計,Breadcrumb chevron 跟其他 chrome icon 同 tier。\n// World-class 對齊:Atlassian Breadcrumb chevron 16 default / Material 3 / Ant Design 同。\nconst BREADCRUMB_ICON_SIZE: Record<BreadcrumbSize, number> = {\n sm: 16,\n md: 16,\n lg: 20,\n}\n\n// ── Breadcrumb (nav root) ────────────────────────────────────────────────────\n\nconst Breadcrumb = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<'nav'>\n>(({ ...props }, ref) => (\n <nav ref={ref} aria-label=\"Breadcrumb\" {...props} />\n))\nBreadcrumb.displayName = 'Breadcrumb'\n\n// ── BreadcrumbList (ol) ──────────────────────────────────────────────────────\n\n/**\n * Phase B(2026-05-10):declarative items mode 啟用 auto-collapse + auto-separator + auto-page-end。\n * 對齊 Material UI source `Breadcrumbs.js renderItemsBeforeAndAfter` mechanism(`maxItems`\n * default 8;本 DS 採 user-tuned 4 — 更積極 collapse 適合 single-line 緊湊 layout)。\n */\n// code-quality-allow: dead-export — public consumer-facing item spec type;對齊 BreadcrumbProps API contract,允許 consumer 構造 items array 外部\nexport interface BreadcrumbItemSpec {\n label: React.ReactNode\n href?: string\n asChild?: boolean\n /**\n * 起始 icon(per `ui-development.md`「icon prop 命名 4 條」:slot 只接 icon → `startIcon`)。\n * 業界慣例:Breadcrumb 首項用 Home icon 強化視覺錨點(Material / Atlassian)。\n * 內部消費 `BREADCRUMB_ICON_SIZE[size]` SSOT(sm/md=16, lg=20,對齊 uiSize.spec.md Icon Size Tier)。\n * Consumer **不傳** size,DS 統一管。\n */\n startIcon?: LucideIcon\n}\n\ninterface BreadcrumbListProps extends Omit<React.ComponentPropsWithoutRef<'ol'>, 'children'> {\n /**\n * 字體尺寸 — 依據與之配對的 page title 選擇:\n * sm → 配 text-h4(20) title (Dialog / panel / drawer)\n * md → 配 text-h3(24) title (一般頁面 header,預設)\n * lg → 配 text-h2(32) title (Detail page hero / landing)\n */\n size?: BreadcrumbSize\n /**\n * Declarative items mode(opt-in)。當 provided 時 `children` 被忽略,List 內部自動:\n * - 插 separator\n * - 末位 spec(無 `href`)自動 BreadcrumbPage(per Title-breadcrumb-end SSOT)\n * - 超 `maxItems` auto-collapse 中段成 BreadcrumbEllipsis + DropdownMenu(對齊 Material UI\n * source `renderItemsBeforeAndAfter`)\n */\n items?: BreadcrumbItemSpec[]\n /**\n * Auto-collapse 閾值。Default 4(user-tuned;Material UI source 預設 8)。`items.length > maxItems`\n * 才 collapse。\n */\n maxItems?: number\n /** Collapse 後保留首 N 個(default 1)。對齊 Material UI source default。 */\n itemsBeforeCollapse?: number\n /** Collapse 後保留末 N 個(default 1)。對齊 Material UI source default。 */\n itemsAfterCollapse?: number\n children?: React.ReactNode\n}\n\n// code-quality-allow: long-function — items props × children narrowing × collapse-with-overflow × tooltip 4 軸組合在 BreadcrumbList,拆 sub-fn 會跨 fn 傳 itemsBeforeCollapse/After collapsed-tooltip refs\nconst BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(\n ({ className, size = 'md', items, maxItems = 4, itemsBeforeCollapse = 1, itemsAfterCollapse = 1, children, ...props }, ref) => {\n // Memoize provider value(2026-04-22 D3 perf audit):單 field wrapper memoize\n const ctxValue = React.useMemo(() => ({ size }), [size])\n\n // Declarative mode(items prop provided):自動 render + auto-collapse\n const declarativeContent = React.useMemo(() => {\n if (!items) return null\n const renderItem = (spec: BreadcrumbItemSpec, role: 'root' | 'middle' | 'current') => (\n <BreadcrumbItem key={`${role}-${typeof spec.label === 'string' ? spec.label : Math.random()}`} role={role}>\n {role === 'current'\n ? <BreadcrumbPage startIcon={spec.startIcon}>\n <TruncatedLabel fullText={typeof spec.label === 'string' ? spec.label : undefined}>{spec.label}</TruncatedLabel>\n </BreadcrumbPage>\n : <BreadcrumbLink href={spec.href} asChild={spec.asChild} startIcon={spec.startIcon}>\n <TruncatedLabel fullText={typeof spec.label === 'string' ? spec.label : undefined}>{spec.label}</TruncatedLabel>\n </BreadcrumbLink>\n }\n </BreadcrumbItem>\n )\n\n const shouldCollapse = items.length > maxItems\n const beforeN = Math.max(0, itemsBeforeCollapse)\n const afterN = Math.max(1, itemsAfterCollapse) // 末位永遠 ≥ 1(current page)\n\n type VisibleItem = { spec: BreadcrumbItemSpec; role: 'root' | 'middle' | 'current' }\n let visible: Array<VisibleItem | { ellipsisOf: BreadcrumbItemSpec[] }>\n if (!shouldCollapse) {\n visible = items.map((spec, i) => ({\n spec,\n role: i === 0 ? 'root' : (i === items.length - 1 ? 'current' : 'middle') as 'root' | 'middle' | 'current',\n }))\n } else {\n const before = items.slice(0, beforeN).map((spec, i) => ({\n spec,\n role: (i === 0 ? 'root' : 'middle') as 'root' | 'middle' | 'current',\n }))\n const collapsed = items.slice(beforeN, items.length - afterN)\n const after = items.slice(items.length - afterN).map((spec, i, arr) => ({\n spec,\n role: (i === arr.length - 1 ? 'current' : 'middle') as 'root' | 'middle' | 'current',\n }))\n visible = [...before, { ellipsisOf: collapsed }, ...after]\n }\n\n // Interleave with separators\n const rendered: React.ReactNode[] = []\n visible.forEach((entry, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-${i}`} />)\n if ('ellipsisOf' in entry) {\n rendered.push(\n <BreadcrumbItem key=\"ellipsis\" role=\"ellipsis\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <BreadcrumbEllipsis />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {entry.ellipsisOf.map((s, j) => (\n <DropdownMenuItem key={j} asChild={!!s.href}>\n {s.href ? <a href={s.href}>{s.label}</a> : <span>{s.label}</span>}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n </BreadcrumbItem>\n )\n } else {\n rendered.push(renderItem(entry.spec, entry.role))\n }\n })\n return rendered\n }, [items, maxItems, itemsBeforeCollapse, itemsAfterCollapse])\n\n // 2026-05-12 fix(user 抓 image 2 Deep story 違反 single-line + max-levels canonical):\n // Compositional path 也走 auto-collapse + flex-shrink hierarchy。Walk children, 找\n // BreadcrumbItem 並按 index 分派 role (first=root / last=current / middle=middle)。\n // > maxItems 自動 collapse 中段成 ellipsis(對齊 declarative path canonical SSOT)。\n const compositionalContent = React.useMemo(() => {\n if (items) return null\n const childArr = React.Children.toArray(children)\n // 抓 BreadcrumbItem children(skip BreadcrumbSeparator — auto re-interleave)\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):type-identity primary path + displayName fallback。\n // 純 `displayName` check 在 HOC / React.memo / consumer alias 場景脆弱(production build / wrap\n // 可能改寫 displayName)。`c.type === BreadcrumbItem` 是 React fiber reference-identity 最穩\n // primary(對齊 Radix children-walk pattern);displayName fallback 給 HOC 場景。\n const itemChildren = childArr.filter((c): c is React.ReactElement<BreadcrumbItemProps> =>\n React.isValidElement(c) && (c.type === BreadcrumbItem ||\n (c.type as React.ComponentType)?.displayName === 'BreadcrumbItem')\n )\n // 無 item 或全是 separator → pass-through(consumer raw children,e.g. spinners)\n if (itemChildren.length === 0) return children\n // Assign role by position; clone with role prop\n const total = itemChildren.length\n const cloneWithRole = (item: React.ReactElement<BreadcrumbItemProps>, idx: number, role: 'root' | 'middle' | 'current') =>\n React.cloneElement(item, { role: item.props.role ?? role, key: `bc-${role}-${idx}` })\n const shouldCollapse = total > maxItems\n const beforeN = Math.max(0, itemsBeforeCollapse)\n const afterN = Math.max(1, itemsAfterCollapse)\n const rendered: React.ReactNode[] = []\n if (!shouldCollapse) {\n itemChildren.forEach((item, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-${i}`} />)\n const role: 'root' | 'middle' | 'current' = i === 0 ? 'root' : (i === total - 1 ? 'current' : 'middle')\n rendered.push(cloneWithRole(item, i, role))\n })\n } else {\n // before(first N) + ellipsis + after(last M)\n const beforeItems = itemChildren.slice(0, beforeN)\n const collapsedItems = itemChildren.slice(beforeN, total - afterN)\n const afterItems = itemChildren.slice(total - afterN)\n beforeItems.forEach((item, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-bef-${i}`} />)\n const role: 'root' | 'middle' = i === 0 ? 'root' : 'middle'\n rendered.push(cloneWithRole(item, i, role))\n })\n if (rendered.length > 0) rendered.push(<BreadcrumbSeparator key=\"sep-ellipsis-before\" />)\n rendered.push(\n <BreadcrumbItem key=\"bc-ellipsis\" role=\"ellipsis\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <BreadcrumbEllipsis />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {collapsedItems.map((item, j) => {\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):consumer BreadcrumbItem children 常包\n // `<BreadcrumbLink href>` = anchor button-like。直接放進 `<DropdownMenuItem>` 會 nested\n // interactive(menuitem within button violates HTML / a11y)。Fix:extract href + label,\n // 用 `asChild` 把 anchor 接到 menuitem 對齊 declarative path line 245 canonical pattern。\n const innerChildren = (item.props as { children?: React.ReactNode }).children\n let href: string | undefined\n let label: React.ReactNode = innerChildren\n React.Children.forEach(innerChildren, (c) => {\n if (React.isValidElement<{ href?: string; children?: React.ReactNode }>(c)) {\n if (c.props.href) href = c.props.href\n if (c.props.children != null) label = c.props.children\n }\n })\n return href\n ? <DropdownMenuItem key={`collapsed-${j}`} asChild><a href={href}>{label}</a></DropdownMenuItem>\n : <DropdownMenuItem key={`collapsed-${j}`}>{label}</DropdownMenuItem>\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n </BreadcrumbItem>\n )\n rendered.push(<BreadcrumbSeparator key=\"sep-ellipsis-after\" />)\n afterItems.forEach((item, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-aft-${i}`} />)\n const role: 'middle' | 'current' = i === afterItems.length - 1 ? 'current' : 'middle'\n rendered.push(cloneWithRole(item, i, role))\n })\n }\n return rendered\n }, [items, children, maxItems, itemsBeforeCollapse, itemsAfterCollapse])\n\n return (\n <BreadcrumbContext.Provider value={ctxValue}>\n <ol\n ref={ref}\n // gap-1 (4px) — separator 與兩邊 items 間距;緊湊節奏,符合 breadcrumb 密集流動感。\n // 2026-05-10 Phase A single-line canonical(per user + Material UI source verified):\n // `flex-nowrap` 不 wrap。長路徑走中段折疊。\n // 2026-05-12 fix:compositional 也走 auto-collapse + role-assignment(`compositionalContent`)\n // → declarative / compositional 兩 path 都符合 single-line + max-levels + width 分配 canonical SSOT。\n className={cn(\n 'flex flex-nowrap items-center gap-1 text-fg-secondary leading-compact min-w-0',\n BREADCRUMB_TEXT_CLASS[size],\n className\n )}\n {...props}\n >\n {items ? declarativeContent : compositionalContent}\n </ol>\n </BreadcrumbContext.Provider>\n )\n },\n)\nBreadcrumbList.displayName = 'BreadcrumbList'\n\n// ── BreadcrumbItem (li) ──────────────────────────────────────────────────────\n\n/**\n * Phase B(2026-05-10):`role` prop emit `data-bc-role` attr → CSS flex-shrink hierarchy。\n * Per BreadcrumbItem 在 row 中的角色決定 shrink 優先級:\n * - `root`(首位)→ shrink:3(縮最積極;root context 可弱化)\n * - `middle`(中段)→ shrink:2\n * - `current`(末位 / page)→ shrink:1(最後縮;a11y current page anchor)\n * - `ellipsis`(BreadcrumbEllipsis 包裝)→ shrink:0(永遠完整 ⋯)\n *\n * 設計回應 user 兩 challenges:\n * (a) Root 也 truncate(shrink:3,不是 shrink-0)\n * (b) 不用 fixed max-width — flex-shrink hierarchy 容器寬時自然展開不浪費空間,\n * 窄時按優先級縮 + TruncatedLabel 內部 CSS truncate + tooltip。\n */\ninterface BreadcrumbItemProps extends React.ComponentPropsWithoutRef<'li'> {\n role?: 'root' | 'middle' | 'current' | 'ellipsis'\n}\n\nconst BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>(\n ({ className, role, style, ...props }, ref) => {\n // 2026-05-20 fix v3(user 抓「專案 後方多 4px 間距 / 我的新專案 沒有」chevron 不對稱):\n // v2 `minWidth: '2rem'`(32px)在寬容器強制 li ≥ 32px → 短 label「專案」(natural ~28px)\n // 被撐 4px,長 label「我的新專案」(natural ~70px)hug content → chevron 兩側不對稱。\n //\n // v3 解法:minWidth `2rem` → `1.5rem`(24px)\n // 數學:中文「X…」最小寬度 = 1 char(~14-16px)+ ellipsis(~6-8px)≈ 22-24px → 24px 剛 cover\n // 結果:\n // - 寬容器:所有自然 label ≥ 24px → li hug content,chevron 緊貼,對稱(本 fix 主目的)\n // - 窄容器 truncate:shrink 不過 24px → 「X…」仍可見 ellipsis 保險\n // - 短英文「OK / ID」(natural ~20px)→ 多 ~4px(原 12px → 縮到 4px,顯著改善)\n // 對齊 user verbatim「minWidth 再調小一點」+ ellipsis 數學最小值。\n const shrinkStyle: React.CSSProperties = role === 'root' ? { flexShrink: 3, minWidth: '1.5rem' }\n : role === 'middle' ? { flexShrink: 2, minWidth: '1.5rem' }\n : role === 'current' ? { flexShrink: 1, minWidth: '1.5rem' }\n : role === 'ellipsis' ? { flexShrink: 0 }\n : {}\n return (\n <li\n ref={ref}\n data-bc-role={role}\n className={cn('inline-flex items-center min-w-0', className)}\n style={{ ...shrinkStyle, ...style }}\n {...props}\n />\n )\n }\n)\nBreadcrumbItem.displayName = 'BreadcrumbItem'\n\n// ── BreadcrumbLink (a) ───────────────────────────────────────────────────────\n\ninterface BreadcrumbLinkProps extends React.ComponentPropsWithoutRef<'a'> {\n /** 將樣式套用至子元件(e.g. React Router Link) */\n asChild?: boolean\n /**\n * 起始 icon(per `ui-development.md`「icon prop 命名 4 條」:slot 只接 icon → `startIcon`)。\n * 內部消費 `BREADCRUMB_ICON_SIZE[size]` SSOT,DS 統一尺寸不允許 consumer override。\n * 對齊 uiSize.spec.md Icon Size Tier(2026-05-18 撤回 14 例外,統一 16/16/20)。\n */\n startIcon?: LucideIcon\n}\n\nconst BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(\n ({ asChild, className, children, startIcon: StartIcon, ...props }, ref) => {\n const Comp = asChild ? Slot : 'a'\n const { size } = React.useContext(BreadcrumbContext)\n // 2026-05-12 fix(user 抓 image 2 Deep story 麵包屑沒符合 single-line + truncate canonical):\n // 純文字 children → auto-wrap TruncatedLabel(canonical「single-line + ellipsis + tooltip\n // on truncate」per spec.md / Polaris breadcrumb)。Non-string children(consumer 自訂 icon+text\n // 結構)→ pass-through 不 force-wrap(consumer own truncate)。\n const wrappedChildren = typeof children === 'string'\n ? <TruncatedLabel fullText={children}>{children}</TruncatedLabel>\n : children\n return (\n <Comp\n ref={ref}\n className={cn(\n 'inline-flex items-center gap-2',\n 'min-w-0 max-w-full',\n 'text-fg-secondary',\n 'hover:text-primary-hover',\n 'transition-colors duration-150',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'rounded-md',\n className\n )}\n {...props}\n >\n {StartIcon && <StartIcon size={BREADCRUMB_ICON_SIZE[size]} aria-hidden className=\"shrink-0\" />}\n {wrappedChildren}\n </Comp>\n )\n }\n)\nBreadcrumbLink.displayName = 'BreadcrumbLink'\n\n// ── BreadcrumbPage (current, non-clickable) ──────────────────────────────────\n\ninterface BreadcrumbPageProps extends React.ComponentPropsWithoutRef<'span'> {\n /** 起始 icon。內部消費 `BREADCRUMB_ICON_SIZE[size]` SSOT。對齊 BreadcrumbLink. */\n startIcon?: LucideIcon\n}\n\nconst BreadcrumbPage = React.forwardRef<HTMLSpanElement, BreadcrumbPageProps>(\n ({ className, children, startIcon: StartIcon, ...props }, ref) => {\n const { size } = React.useContext(BreadcrumbContext)\n // 2026-05-12 fix(同 BreadcrumbLink):純文字 children → auto-wrap TruncatedLabel。\n const wrappedChildren = typeof children === 'string'\n ? <TruncatedLabel fullText={children}>{children}</TruncatedLabel>\n : children\n return (\n <span\n ref={ref}\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn('inline-flex items-center gap-2 min-w-0 max-w-full text-foreground', className)}\n {...props}\n >\n {StartIcon && <StartIcon size={BREADCRUMB_ICON_SIZE[size]} aria-hidden className=\"shrink-0\" />}\n {wrappedChildren}\n </span>\n )\n }\n)\nBreadcrumbPage.displayName = 'BreadcrumbPage'\n\n// ── BreadcrumbSeparator ──────────────────────────────────────────────────────\n\ninterface BreadcrumbSeparatorProps extends React.ComponentPropsWithoutRef<'li'> {\n children?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst BreadcrumbSeparator = React.forwardRef<HTMLLIElement, BreadcrumbSeparatorProps>(\n ({ children, className, ...props }, ref) => {\n const { size } = React.useContext(BreadcrumbContext)\n return (\n <li\n ref={ref}\n role=\"presentation\"\n aria-hidden=\"true\"\n // Phase B(2026-05-10):separator 永遠 shrink-0(必完整顯示,否則 path 視覺斷裂)\n className={cn('inline-flex items-center text-fg-muted shrink-0', className)}\n {...props}\n >\n {children ?? <ChevronRight size={BREADCRUMB_ICON_SIZE[size]} aria-hidden />}\n </li>\n )\n }\n)\nBreadcrumbSeparator.displayName = 'BreadcrumbSeparator'\n\n// ── BreadcrumbEllipsis ───────────────────────────────────────────────────────\n\n/**\n * BreadcrumbEllipsis — 折疊路徑的 \"⋯\" 按鈕\n *\n * 2026-05-10 重寫:消費 `ItemInlineActionButton`(primitive SSOT)取代自刻 `<button>`。\n * Per inline-action.spec.md L106-131 predicate Q1+Q2+Q3 全指向 Inline Action:\n * - Q1 點了要做事嗎?是(展開折疊路徑 dropdown)\n * - Q2 位置?BreadcrumbList row inline flow(host 內)\n * - Q3 row 多大?14-16px text row(compact tier)→ Inline Action\n * + 對齊 M1「視覺決策前必消費 SSOT」+ Mindset #2「優先消費既有」。\n *\n * 配合 DropdownMenuTrigger asChild 使用:\n *\n * ```tsx\n * <DropdownMenu>\n * <DropdownMenuTrigger asChild>\n * <BreadcrumbEllipsis />\n * </DropdownMenuTrigger>\n * <DropdownMenuContent>\n * <DropdownMenuItem asChild><a href=\"/org\">組織</a></DropdownMenuItem>\n * </DropdownMenuContent>\n * </DropdownMenu>\n * ```\n *\n * `overlayTrigger=true` 視覺鎖:DropdownMenu open 期間 button 維持 hover bg(對齊\n * shadcn / Radix Themes / Material 的 overlay trigger canonical,inline-action.spec.md\n * 「Overlay trigger canonical」段)。\n */\ntype BreadcrumbEllipsisProps = Omit<React.ComponentPropsWithoutRef<typeof ItemInlineActionButton>, 'icon' | 'size'>\n\nconst BreadcrumbEllipsis = React.forwardRef<HTMLButtonElement, BreadcrumbEllipsisProps>(\n ({ 'aria-label': ariaLabel = '顯示折疊路徑' /* i18n-allow: DS default; consumer override via aria-label prop */, ...props }, ref) => {\n return (\n <ItemInlineActionButton\n ref={ref}\n icon={MoreHorizontal}\n size=\"md\" // Breadcrumb 不在 RowSizeProvider 樹內,固定 md(16px icon + 18px hover bg,對齊 inline-action.spec.md 尺寸表)\n aria-label={ariaLabel}\n overlayTrigger\n {...props}\n />\n )\n }\n)\nBreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis'\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const breadcrumbMeta = {\n component: 'Breadcrumb',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n} as const\n\nexport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n}\nexport type { BreadcrumbSize, BreadcrumbListProps, BreadcrumbEllipsisProps }\n// BreadcrumbItemSpec 已在上方 `export interface BreadcrumbItemSpec` 直接 export\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"breadcrumb.js","sources":["../../../src/components/Breadcrumb/breadcrumb.tsx"],"sourcesContent":["// @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.\n// code-quality-allow: file-size — Breadcrumb 含 BreadcrumbList(主)+ BreadcrumbItem + BreadcrumbEllipsis + items-collapse logic,split 會破壞 collapse/overflow Tooltip subtree\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { ChevronRight, MoreHorizontal, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { ItemInlineActionButton } from '@/design-system/patterns/element-anatomy/item-anatomy'\nimport { Tooltip, TooltipTrigger, TooltipContent } from '@/design-system/components/Tooltip/tooltip'\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n} from '@/design-system/components/DropdownMenu/dropdown-menu'\n\n// ── TruncatedLabel ────────────────────────────────────────────────────────────\n// 同 `data-table.tsx:339 TruncateCell` + `tag.tsx:138 isTruncated` SSOT pattern\n// (shared ResizeObserver + scrollWidth > clientWidth → wrap Tooltip)。\n// **TODO** future:Rule-of-3 達 → 抽 `patterns/element-anatomy/truncated-text.tsx` 共用\n// (本 component / DataTable TruncateCell / Tag inner 三處同 idiom,符合 M30 SSOT 抽取門檻)。\n\ntype RoCallback = (entry: ResizeObserverEntry) => void\nlet sharedRO: ResizeObserver | null = null\nconst sharedROCallbacks = new WeakMap<Element, RoCallback>()\nfunction getSharedRO(): ResizeObserver {\n if (sharedRO) return sharedRO\n sharedRO = new ResizeObserver((entries) => {\n entries.forEach((e) => {\n const cb = sharedROCallbacks.get(e.target)\n if (cb) cb(e)\n })\n })\n return sharedRO\n}\nfunction observeShared(el: Element, cb: RoCallback): () => void {\n const obs = getSharedRO()\n sharedROCallbacks.set(el, cb)\n obs.observe(el)\n return () => { sharedROCallbacks.delete(el); obs.unobserve(el) }\n}\n\nfunction TruncatedLabel({ children, fullText }: { children: React.ReactNode; fullText?: string }) {\n const ref = React.useRef<HTMLSpanElement>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n React.useEffect(() => {\n const el = ref.current\n if (!el) return\n const check = () => setIsTruncated(el.scrollWidth > el.clientWidth)\n check()\n // 2026-05-11 fix:首幀 layout 未完成 / 字型 async load → scrollWidth=clientWidth 假陰性。\n // RAF + 短延遲再驗一次,捕獲字型 / 容器尺寸後變(對齊 TruncateCell / Tag SSOT pattern)。\n const raf = requestAnimationFrame(check)\n const t = setTimeout(check, 100)\n const cleanup = observeShared(el, check)\n return () => {\n cancelAnimationFrame(raf)\n clearTimeout(t)\n cleanup()\n }\n }, [])\n // Tooltip canonical:per `tooltip.principles.stories.tsx:190`「Tooltip 是資訊補救 — 文字被\n // truncate 時才顯示完整內容。沒被截斷就不該顯示 tooltip」\n //\n // 2026-05-11 fix(playwright tooltip-on-truncate 卡 hover 沒 tooltip):\n // 原本 isTruncated=false 直接 return 裸 span / true 才 wrap Tooltip → JSX 樹結構改變\n // → React 把 span unmount + remount(因為 wrapper component 變),ref 換到新 span,\n // useEffect [] 不重跑(同 component instance)→ 觀察的 DOM 跟實際 DOM 對不上。\n // Fix:**永遠 wrap Tooltip**(同 DOM 節點生命週期);`open` 由 isTruncated 控制 —\n // 沒被 truncate 就 force `open={false}`,有 truncate 就 `undefined`(uncontrolled,\n // hover 走 Radix default behavior)。對齊 canonical「沒被截斷就不該 tooltip」。\n return (\n <Tooltip open={isTruncated ? undefined : false}>\n <TooltipTrigger asChild>\n <span ref={ref} className=\"truncate min-w-0 block\">{children}</span>\n </TooltipTrigger>\n <TooltipContent>{fullText ?? children}</TooltipContent>\n </Tooltip>\n )\n}\n\n/**\n * Breadcrumb — 顯示當前頁面在階層中的位置\n *\n * 基於 shadcn/ui Breadcrumb 結構(純 HTML nav + ol + li + a/span),\n * 橋接設計系統 token。\n *\n * ── 結構 ──\n * <Breadcrumb>\n * <BreadcrumbList size=\"md\">\n * <BreadcrumbItem>\n * <BreadcrumbLink href=\"/projects\">專案</BreadcrumbLink>\n * </BreadcrumbItem>\n * <BreadcrumbSeparator />\n * <BreadcrumbItem>\n * <BreadcrumbPage>目前頁面</BreadcrumbPage>\n * </BreadcrumbItem>\n * </BreadcrumbList>\n * </Breadcrumb>\n *\n * ── Size(跟 page title 配對) ──\n * sm text-body(14) → 建議配 text-h4(20) title —— Dialog / panel / drawer\n * md text-body(14) → 建議配 text-h3(24) title —— 一般頁面 header (預設)\n * lg text-body-lg(16) → 建議配 text-h2(32) title —— Detail page hero / landing\n *\n * ── 視覺 ──\n * Link (預設): text-fg-secondary\n * Link hover: text-primary-hover (canonical「互動高亮」, 跟 Tabs / Chip 用法一致)\n * Page (當前): text-foreground + font-medium\n * Separator: ChevronRight (size 跟 list 一致), text-fg-muted\n *\n * ── 詳見 breadcrumb.spec.md ──\n */\n\n// ── Size context ─────────────────────────────────────────────────────────────\n\ntype BreadcrumbSize = 'sm' | 'md' | 'lg'\n\ninterface BreadcrumbContextValue {\n size: BreadcrumbSize\n}\n\nconst BreadcrumbContext = React.createContext<BreadcrumbContextValue>({ size: 'md' })\n\nconst BREADCRUMB_TEXT_CLASS: Record<BreadcrumbSize, string> = {\n sm: 'text-body',\n md: 'text-body',\n lg: 'text-body-lg',\n}\n\n// Separator / ellipsis icon 尺寸 — 對齊 uiSize.spec.md「Icon Size Tier」(field-height-sm/md→16,lg→20)\n// 2026-05-18 改 per user 拍板「A 先改 16/16/20」+「做完」approval:\n// 撤回 text-flow 例外設計,Breadcrumb chevron 跟其他 chrome icon 同 tier。\n// World-class 對齊:Atlassian Breadcrumb chevron 16 default / Material 3 / Ant Design 同。\nconst BREADCRUMB_ICON_SIZE: Record<BreadcrumbSize, number> = {\n sm: 16,\n md: 16,\n lg: 20,\n}\n\n// ── Breadcrumb (nav root) ────────────────────────────────────────────────────\n\nconst Breadcrumb = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<'nav'>\n>(({ ...props }, ref) => (\n <nav ref={ref} aria-label=\"Breadcrumb\" {...props} />\n))\nBreadcrumb.displayName = 'Breadcrumb'\n\n// ── BreadcrumbList (ol) ──────────────────────────────────────────────────────\n\n/**\n * Phase B(2026-05-10):declarative items mode 啟用 auto-collapse + auto-separator + auto-page-end。\n * 對齊 Material UI source `Breadcrumbs.js renderItemsBeforeAndAfter` mechanism(`maxItems`\n * default 8;本 DS 採 user-tuned 4 — 更積極 collapse 適合 single-line 緊湊 layout)。\n */\n// code-quality-allow: dead-export — public consumer-facing item spec type;對齊 BreadcrumbProps API contract,允許 consumer 構造 items array 外部\nexport interface BreadcrumbItemSpec {\n label: React.ReactNode\n href?: string\n asChild?: boolean\n /**\n * 起始 icon(per `ui-development.md`「icon prop 命名 4 條」:slot 只接 icon → `startIcon`)。\n * 業界慣例:Breadcrumb 首項用 Home icon 強化視覺錨點(Material / Atlassian)。\n * 內部消費 `BREADCRUMB_ICON_SIZE[size]` SSOT(sm/md=16, lg=20,對齊 uiSize.spec.md Icon Size Tier)。\n * Consumer **不傳** size,DS 統一管。\n */\n startIcon?: LucideIcon\n}\n\ninterface BreadcrumbListProps extends Omit<React.ComponentPropsWithoutRef<'ol'>, 'children'> {\n /**\n * 字體尺寸 — 依據與之配對的 page title 選擇:\n * sm → 配 text-h4(20) title (Dialog / panel / drawer)\n * md → 配 text-h3(24) title (一般頁面 header,預設)\n * lg → 配 text-h2(32) title (Detail page hero / landing)\n */\n size?: BreadcrumbSize\n /**\n * Declarative items mode(opt-in)。當 provided 時 `children` 被忽略,List 內部自動:\n * - 插 separator\n * - 末位 spec(無 `href`)自動 BreadcrumbPage(per Title-breadcrumb-end SSOT)\n * - 超 `maxItems` auto-collapse 中段成 BreadcrumbEllipsis + DropdownMenu(對齊 Material UI\n * source `renderItemsBeforeAndAfter`)\n */\n items?: BreadcrumbItemSpec[]\n /**\n * Auto-collapse 閾值。Default 4(user-tuned;Material UI source 預設 8)。`items.length > maxItems`\n * 才 collapse。\n */\n maxItems?: number\n /** Collapse 後保留首 N 個(default 1)。對齊 Material UI source default。 */\n itemsBeforeCollapse?: number\n /** Collapse 後保留末 N 個(default 1)。對齊 Material UI source default。 */\n itemsAfterCollapse?: number\n children?: React.ReactNode\n}\n\n// code-quality-allow: long-function — items props × children narrowing × collapse-with-overflow × tooltip 4 軸組合在 BreadcrumbList,拆 sub-fn 會跨 fn 傳 itemsBeforeCollapse/After collapsed-tooltip refs\nconst BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(\n ({ className, size = 'md', items, maxItems = 4, itemsBeforeCollapse = 1, itemsAfterCollapse = 1, children, ...props }, ref) => {\n // Memoize provider value(2026-04-22 D3 perf audit):單 field wrapper memoize\n const ctxValue = React.useMemo(() => ({ size }), [size])\n\n // Declarative mode(items prop provided):自動 render + auto-collapse\n const declarativeContent = React.useMemo(() => {\n if (!items) return null\n const renderItem = (spec: BreadcrumbItemSpec, role: 'root' | 'middle' | 'current') => (\n <BreadcrumbItem key={`${role}-${typeof spec.label === 'string' ? spec.label : Math.random()}`} role={role}>\n {role === 'current'\n ? <BreadcrumbPage startIcon={spec.startIcon}>\n <TruncatedLabel fullText={typeof spec.label === 'string' ? spec.label : undefined}>{spec.label}</TruncatedLabel>\n </BreadcrumbPage>\n : <BreadcrumbLink href={spec.href} asChild={spec.asChild} startIcon={spec.startIcon}>\n <TruncatedLabel fullText={typeof spec.label === 'string' ? spec.label : undefined}>{spec.label}</TruncatedLabel>\n </BreadcrumbLink>\n }\n </BreadcrumbItem>\n )\n\n const shouldCollapse = items.length > maxItems\n const beforeN = Math.max(0, itemsBeforeCollapse)\n const afterN = Math.max(1, itemsAfterCollapse) // 末位永遠 ≥ 1(current page)\n\n type VisibleItem = { spec: BreadcrumbItemSpec; role: 'root' | 'middle' | 'current' }\n let visible: Array<VisibleItem | { ellipsisOf: BreadcrumbItemSpec[] }>\n if (!shouldCollapse) {\n visible = items.map((spec, i) => ({\n spec,\n role: i === 0 ? 'root' : (i === items.length - 1 ? 'current' : 'middle') as 'root' | 'middle' | 'current',\n }))\n } else {\n const before = items.slice(0, beforeN).map((spec, i) => ({\n spec,\n role: (i === 0 ? 'root' : 'middle') as 'root' | 'middle' | 'current',\n }))\n const collapsed = items.slice(beforeN, items.length - afterN)\n const after = items.slice(items.length - afterN).map((spec, i, arr) => ({\n spec,\n role: (i === arr.length - 1 ? 'current' : 'middle') as 'root' | 'middle' | 'current',\n }))\n visible = [...before, { ellipsisOf: collapsed }, ...after]\n }\n\n // Interleave with separators\n const rendered: React.ReactNode[] = []\n visible.forEach((entry, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-${i}`} />)\n if ('ellipsisOf' in entry) {\n rendered.push(\n <BreadcrumbItem key=\"ellipsis\" role=\"ellipsis\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <BreadcrumbEllipsis />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {entry.ellipsisOf.map((s, j) => (\n <DropdownMenuItem key={j} asChild={!!s.href}>\n {s.href ? <a href={s.href}>{s.label}</a> : <span>{s.label}</span>}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n </BreadcrumbItem>\n )\n } else {\n rendered.push(renderItem(entry.spec, entry.role))\n }\n })\n return rendered\n }, [items, maxItems, itemsBeforeCollapse, itemsAfterCollapse])\n\n // 2026-05-12 fix(user 抓 image 2 Deep story 違反 single-line + max-levels canonical):\n // Compositional path 也走 auto-collapse + flex-shrink hierarchy。Walk children, 找\n // BreadcrumbItem 並按 index 分派 role (first=root / last=current / middle=middle)。\n // > maxItems 自動 collapse 中段成 ellipsis(對齊 declarative path canonical SSOT)。\n const compositionalContent = React.useMemo(() => {\n if (items) return null\n const childArr = React.Children.toArray(children)\n // 抓 BreadcrumbItem children(skip BreadcrumbSeparator — auto re-interleave)\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):type-identity primary path + displayName fallback。\n // 純 `displayName` check 在 HOC / React.memo / consumer alias 場景脆弱(production build / wrap\n // 可能改寫 displayName)。`c.type === BreadcrumbItem` 是 React fiber reference-identity 最穩\n // primary(對齊 Radix children-walk pattern);displayName fallback 給 HOC 場景。\n const itemChildren = childArr.filter((c): c is React.ReactElement<BreadcrumbItemProps> =>\n React.isValidElement(c) && (c.type === BreadcrumbItem ||\n (c.type as React.ComponentType)?.displayName === 'BreadcrumbItem')\n )\n // 無 item 或全是 separator → pass-through(consumer raw children,e.g. spinners)\n if (itemChildren.length === 0) return children\n // Assign role by position; clone with role prop\n const total = itemChildren.length\n const cloneWithRole = (item: React.ReactElement<BreadcrumbItemProps>, idx: number, role: 'root' | 'middle' | 'current') =>\n React.cloneElement(item, { role: item.props.role ?? role, key: `bc-${role}-${idx}` })\n const shouldCollapse = total > maxItems\n const beforeN = Math.max(0, itemsBeforeCollapse)\n const afterN = Math.max(1, itemsAfterCollapse)\n const rendered: React.ReactNode[] = []\n if (!shouldCollapse) {\n itemChildren.forEach((item, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-${i}`} />)\n const role: 'root' | 'middle' | 'current' = i === 0 ? 'root' : (i === total - 1 ? 'current' : 'middle')\n rendered.push(cloneWithRole(item, i, role))\n })\n } else {\n // before(first N) + ellipsis + after(last M)\n const beforeItems = itemChildren.slice(0, beforeN)\n const collapsedItems = itemChildren.slice(beforeN, total - afterN)\n const afterItems = itemChildren.slice(total - afterN)\n beforeItems.forEach((item, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-bef-${i}`} />)\n const role: 'root' | 'middle' = i === 0 ? 'root' : 'middle'\n rendered.push(cloneWithRole(item, i, role))\n })\n if (rendered.length > 0) rendered.push(<BreadcrumbSeparator key=\"sep-ellipsis-before\" />)\n rendered.push(\n <BreadcrumbItem key=\"bc-ellipsis\" role=\"ellipsis\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <BreadcrumbEllipsis />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {collapsedItems.map((item, j) => {\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):consumer BreadcrumbItem children 常包\n // `<BreadcrumbLink href>` = anchor button-like。直接放進 `<DropdownMenuItem>` 會 nested\n // interactive(menuitem within button violates HTML / a11y)。Fix:extract href + label,\n // 用 `asChild` 把 anchor 接到 menuitem 對齊 declarative path line 245 canonical pattern。\n const innerChildren = (item.props as { children?: React.ReactNode }).children\n let href: string | undefined\n let label: React.ReactNode = innerChildren\n React.Children.forEach(innerChildren, (c) => {\n if (React.isValidElement<{ href?: string; children?: React.ReactNode }>(c)) {\n if (c.props.href) href = c.props.href\n if (c.props.children != null) label = c.props.children\n }\n })\n return href\n ? <DropdownMenuItem key={`collapsed-${j}`} asChild><a href={href}>{label}</a></DropdownMenuItem>\n : <DropdownMenuItem key={`collapsed-${j}`}>{label}</DropdownMenuItem>\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n </BreadcrumbItem>\n )\n rendered.push(<BreadcrumbSeparator key=\"sep-ellipsis-after\" />)\n afterItems.forEach((item, i) => {\n if (i > 0) rendered.push(<BreadcrumbSeparator key={`sep-aft-${i}`} />)\n const role: 'middle' | 'current' = i === afterItems.length - 1 ? 'current' : 'middle'\n rendered.push(cloneWithRole(item, i, role))\n })\n }\n return rendered\n }, [items, children, maxItems, itemsBeforeCollapse, itemsAfterCollapse])\n\n return (\n <BreadcrumbContext.Provider value={ctxValue}>\n <ol\n ref={ref}\n // gap-1 (4px) — separator 與兩邊 items 間距;緊湊節奏,符合 breadcrumb 密集流動感。\n // 2026-05-10 Phase A single-line canonical(per user + Material UI source verified):\n // `flex-nowrap` 不 wrap。長路徑走中段折疊。\n // 2026-05-12 fix:compositional 也走 auto-collapse + role-assignment(`compositionalContent`)\n // → declarative / compositional 兩 path 都符合 single-line + max-levels + width 分配 canonical SSOT。\n className={cn(\n 'flex flex-nowrap items-center gap-1 text-fg-secondary leading-compact min-w-0',\n BREADCRUMB_TEXT_CLASS[size],\n className\n )}\n {...props}\n >\n {items ? declarativeContent : compositionalContent}\n </ol>\n </BreadcrumbContext.Provider>\n )\n },\n)\nBreadcrumbList.displayName = 'BreadcrumbList'\n\n// ── BreadcrumbItem (li) ──────────────────────────────────────────────────────\n\n/**\n * Phase B(2026-05-10):`role` prop emit `data-bc-role` attr → CSS flex-shrink hierarchy。\n * Per BreadcrumbItem 在 row 中的角色決定 shrink 優先級:\n * - `root`(首位)→ shrink:3(縮最積極;root context 可弱化)\n * - `middle`(中段)→ shrink:2\n * - `current`(末位 / page)→ shrink:1(最後縮;a11y current page anchor)\n * - `ellipsis`(BreadcrumbEllipsis 包裝)→ shrink:0(永遠完整 ⋯)\n *\n * 設計回應 user 兩 challenges:\n * (a) Root 也 truncate(shrink:3,不是 shrink-0)\n * (b) 不用 fixed max-width — flex-shrink hierarchy 容器寬時自然展開不浪費空間,\n * 窄時按優先級縮 + TruncatedLabel 內部 CSS truncate + tooltip。\n */\ninterface BreadcrumbItemProps extends React.ComponentPropsWithoutRef<'li'> {\n role?: 'root' | 'middle' | 'current' | 'ellipsis'\n}\n\nconst BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>(\n ({ className, role, style, ...props }, ref) => {\n // 2026-05-20 fix v3(user 抓「專案 後方多 4px 間距 / 我的新專案 沒有」chevron 不對稱):\n // v2 `minWidth: '2rem'`(32px)在寬容器強制 li ≥ 32px → 短 label「專案」(natural ~28px)\n // 被撐 4px,長 label「我的新專案」(natural ~70px)hug content → chevron 兩側不對稱。\n //\n // v3 解法:minWidth `2rem` → `1.5rem`(24px)\n // 數學:中文「X…」最小寬度 = 1 char(~14-16px)+ ellipsis(~6-8px)≈ 22-24px → 24px 剛 cover\n // 結果:\n // - 寬容器:所有自然 label ≥ 24px → li hug content,chevron 緊貼,對稱(本 fix 主目的)\n // - 窄容器 truncate:shrink 不過 24px → 「X…」仍可見 ellipsis 保險\n // - 短英文「OK / ID」(natural ~20px)→ 多 ~4px(原 12px → 縮到 4px,顯著改善)\n // 對齊 user verbatim「minWidth 再調小一點」+ ellipsis 數學最小值。\n const shrinkStyle: React.CSSProperties = role === 'root' ? { flexShrink: 3, minWidth: '1.5rem' }\n : role === 'middle' ? { flexShrink: 2, minWidth: '1.5rem' }\n : role === 'current' ? { flexShrink: 1, minWidth: '1.5rem' }\n : role === 'ellipsis' ? { flexShrink: 0 }\n : {}\n return (\n <li\n ref={ref}\n data-bc-role={role}\n className={cn('inline-flex items-center min-w-0', className)}\n style={{ ...shrinkStyle, ...style }}\n {...props}\n />\n )\n }\n)\nBreadcrumbItem.displayName = 'BreadcrumbItem'\n\n// ── BreadcrumbLink (a) ───────────────────────────────────────────────────────\n\ninterface BreadcrumbLinkProps extends React.ComponentPropsWithoutRef<'a'> {\n /** 將樣式套用至子元件(e.g. React Router Link) */\n asChild?: boolean\n /**\n * 起始 icon(per `ui-development.md`「icon prop 命名 4 條」:slot 只接 icon → `startIcon`)。\n * 內部消費 `BREADCRUMB_ICON_SIZE[size]` SSOT,DS 統一尺寸不允許 consumer override。\n * 對齊 uiSize.spec.md Icon Size Tier(2026-05-18 撤回 14 例外,統一 16/16/20)。\n */\n startIcon?: LucideIcon\n}\n\nconst BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(\n ({ asChild, className, children, startIcon: StartIcon, ...props }, ref) => {\n const { size } = React.useContext(BreadcrumbContext)\n // 2026-05-12 fix(user 抓 image 2 Deep story 麵包屑沒符合 single-line + truncate canonical):\n // 純文字 children → auto-wrap TruncatedLabel(canonical「single-line + ellipsis + tooltip\n // on truncate」per spec.md / Polaris breadcrumb)。Non-string children(consumer 自訂 icon+text\n // 結構)→ pass-through 不 force-wrap(consumer own truncate)。\n const wrappedChildren = typeof children === 'string'\n ? <TruncatedLabel fullText={children}>{children}</TruncatedLabel>\n : children\n const sharedClassName = cn(\n 'inline-flex items-center gap-2',\n 'min-w-0 max-w-full',\n 'text-fg-secondary',\n 'hover:text-primary-hover',\n 'transition-colors duration-150',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'rounded-md',\n className\n )\n // 2026-05-25 fix(user 抓 Breadcrumb asChild story React.Children.only runtime fail):\n // Radix Slot 規範 children 必為單 element;原 unified Comp render 在 asChild path 內\n // 仍輸出「{StartIcon && ...} + {wrappedChildren}」雙 JSX expression → Slot 收到 array\n // → React.Children.only(array) throws「expected to receive a single React element child」。\n // 分支 render 解 — asChild path 只傳 consumer-supplied child(icon 由 consumer 自管,\n // 對齊 Radix Slot canonical「single child contract」);非 asChild path 維持原 native\n // <a> + DS-controlled icon + wrapped label。\n if (asChild) {\n return (\n <Slot ref={ref} className={sharedClassName} {...props}>\n {wrappedChildren}\n </Slot>\n )\n }\n return (\n <a ref={ref} className={sharedClassName} {...props}>\n {StartIcon && <StartIcon size={BREADCRUMB_ICON_SIZE[size]} aria-hidden className=\"shrink-0\" />}\n {wrappedChildren}\n </a>\n )\n }\n)\nBreadcrumbLink.displayName = 'BreadcrumbLink'\n\n// ── BreadcrumbPage (current, non-clickable) ──────────────────────────────────\n\ninterface BreadcrumbPageProps extends React.ComponentPropsWithoutRef<'span'> {\n /** 起始 icon。內部消費 `BREADCRUMB_ICON_SIZE[size]` SSOT。對齊 BreadcrumbLink. */\n startIcon?: LucideIcon\n}\n\nconst BreadcrumbPage = React.forwardRef<HTMLSpanElement, BreadcrumbPageProps>(\n ({ className, children, startIcon: StartIcon, ...props }, ref) => {\n const { size } = React.useContext(BreadcrumbContext)\n // 2026-05-12 fix(同 BreadcrumbLink):純文字 children → auto-wrap TruncatedLabel。\n const wrappedChildren = typeof children === 'string'\n ? <TruncatedLabel fullText={children}>{children}</TruncatedLabel>\n : children\n return (\n <span\n ref={ref}\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn('inline-flex items-center gap-2 min-w-0 max-w-full text-foreground', className)}\n {...props}\n >\n {StartIcon && <StartIcon size={BREADCRUMB_ICON_SIZE[size]} aria-hidden className=\"shrink-0\" />}\n {wrappedChildren}\n </span>\n )\n }\n)\nBreadcrumbPage.displayName = 'BreadcrumbPage'\n\n// ── BreadcrumbSeparator ──────────────────────────────────────────────────────\n\ninterface BreadcrumbSeparatorProps extends React.ComponentPropsWithoutRef<'li'> {\n children?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst BreadcrumbSeparator = React.forwardRef<HTMLLIElement, BreadcrumbSeparatorProps>(\n ({ children, className, ...props }, ref) => {\n const { size } = React.useContext(BreadcrumbContext)\n return (\n <li\n ref={ref}\n role=\"presentation\"\n aria-hidden=\"true\"\n // Phase B(2026-05-10):separator 永遠 shrink-0(必完整顯示,否則 path 視覺斷裂)\n className={cn('inline-flex items-center text-fg-muted shrink-0', className)}\n {...props}\n >\n {children ?? <ChevronRight size={BREADCRUMB_ICON_SIZE[size]} aria-hidden />}\n </li>\n )\n }\n)\nBreadcrumbSeparator.displayName = 'BreadcrumbSeparator'\n\n// ── BreadcrumbEllipsis ───────────────────────────────────────────────────────\n\n/**\n * BreadcrumbEllipsis — 折疊路徑的 \"⋯\" 按鈕\n *\n * 2026-05-10 重寫:消費 `ItemInlineActionButton`(primitive SSOT)取代自刻 `<button>`。\n * Per inline-action.spec.md L106-131 predicate Q1+Q2+Q3 全指向 Inline Action:\n * - Q1 點了要做事嗎?是(展開折疊路徑 dropdown)\n * - Q2 位置?BreadcrumbList row inline flow(host 內)\n * - Q3 row 多大?14-16px text row(compact tier)→ Inline Action\n * + 對齊 M1「視覺決策前必消費 SSOT」+ Mindset #2「優先消費既有」。\n *\n * 配合 DropdownMenuTrigger asChild 使用:\n *\n * ```tsx\n * <DropdownMenu>\n * <DropdownMenuTrigger asChild>\n * <BreadcrumbEllipsis />\n * </DropdownMenuTrigger>\n * <DropdownMenuContent>\n * <DropdownMenuItem asChild><a href=\"/org\">組織</a></DropdownMenuItem>\n * </DropdownMenuContent>\n * </DropdownMenu>\n * ```\n *\n * `overlayTrigger=true` 視覺鎖:DropdownMenu open 期間 button 維持 hover bg(對齊\n * shadcn / Radix Themes / Material 的 overlay trigger canonical,inline-action.spec.md\n * 「Overlay trigger canonical」段)。\n */\ntype BreadcrumbEllipsisProps = Omit<React.ComponentPropsWithoutRef<typeof ItemInlineActionButton>, 'icon' | 'size'>\n\nconst BreadcrumbEllipsis = React.forwardRef<HTMLButtonElement, BreadcrumbEllipsisProps>(\n ({ 'aria-label': ariaLabel = '顯示折疊路徑' /* i18n-allow: DS default; consumer override via aria-label prop */, ...props }, ref) => {\n return (\n <ItemInlineActionButton\n ref={ref}\n icon={MoreHorizontal}\n size=\"md\" // Breadcrumb 不在 RowSizeProvider 樹內,固定 md(16px icon + 18px hover bg,對齊 inline-action.spec.md 尺寸表)\n aria-label={ariaLabel}\n overlayTrigger\n {...props}\n />\n )\n }\n)\nBreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis'\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const breadcrumbMeta = {\n component: 'Breadcrumb',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n} as const\n\nexport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n}\nexport type { BreadcrumbSize, BreadcrumbListProps, BreadcrumbEllipsisProps }\n// BreadcrumbItemSpec 已在上方 `export interface BreadcrumbItemSpec` 直接 export\n"],"names":[],"mappings":";;;;;;;;AAsBA,IAAI,WAAkC;AACtC,MAAM,wCAAwB,QAAA;AAC9B,SAAS,cAA8B;AACrC,MAAI,SAAU,QAAO;AACrB,aAAW,IAAI,eAAe,CAAC,YAAY;AACzC,YAAQ,QAAQ,CAAC,MAAM;AACrB,YAAM,KAAK,kBAAkB,IAAI,EAAE,MAAM;AACzC,UAAI,OAAO,CAAC;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AACA,SAAS,cAAc,IAAa,IAA4B;AAC9D,QAAM,MAAM,YAAA;AACZ,oBAAkB,IAAI,IAAI,EAAE;AAC5B,MAAI,QAAQ,EAAE;AACd,SAAO,MAAM;AAAE,sBAAkB,OAAO,EAAE;AAAG,QAAI,UAAU,EAAE;AAAA,EAAE;AACjE;AAEA,SAAS,eAAe,EAAE,UAAU,YAA8D;AAChG,QAAM,MAAM,MAAM,OAAwB,IAAI;AAC9C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAC1D,QAAM,UAAU,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,QAAQ,MAAM,eAAe,GAAG,cAAc,GAAG,WAAW;AAClE,UAAA;AAGA,UAAM,MAAM,sBAAsB,KAAK;AACvC,UAAM,IAAI,WAAW,OAAO,GAAG;AAC/B,UAAM,UAAU,cAAc,IAAI,KAAK;AACvC,WAAO,MAAM;AACX,2BAAqB,GAAG;AACxB,mBAAa,CAAC;AACd,cAAA;AAAA,IACF;AAAA,EACF,GAAG,CAAA,CAAE;AAWL,SACE,qBAAC,SAAA,EAAQ,MAAM,cAAc,SAAY,OACvC,UAAA;AAAA,IAAA,oBAAC,gBAAA,EAAe,SAAO,MACrB,UAAA,oBAAC,UAAK,KAAU,WAAU,0BAA0B,SAAA,CAAS,EAAA,CAC/D;AAAA,IACA,oBAAC,gBAAA,EAAgB,UAAA,YAAY,SAAA,CAAS;AAAA,EAAA,GACxC;AAEJ;AA2CA,MAAM,oBAAoB,MAAM,cAAsC,EAAE,MAAM,MAAM;AAEpF,MAAM,wBAAwD;AAAA,EAC5D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAMA,MAAM,uBAAuD;AAAA,EAC3D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAIA,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,GAAG,MAAA,GAAS,QACf,oBAAC,SAAI,KAAU,cAAW,cAAc,GAAG,OAAO,CACnD;AACD,WAAW,cAAc;AAoDzB,MAAM,iBAAiB,MAAM;AAAA,EAC3B,CAAC,EAAE,WAAW,OAAO,MAAM,OAAO,WAAW,GAAG,sBAAsB,GAAG,qBAAqB,GAAG,UAAU,GAAG,MAAA,GAAS,QAAQ;AAE7H,UAAM,WAAW,MAAM,QAAQ,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC;AAGvD,UAAM,qBAAqB,MAAM,QAAQ,MAAM;AAC7C,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,aAAa,CAAC,MAA0B,SAC5C,oBAAC,gBAAA,EAA8F,MAC5F,UAAA,SAAS,YACN,oBAAC,gBAAA,EAAe,WAAW,KAAK,WAC9B,UAAA,oBAAC,gBAAA,EAAe,UAAU,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,QAAY,UAAA,KAAK,MAAA,CAAM,EAAA,CACjG,IACA,oBAAC,kBAAe,MAAM,KAAK,MAAM,SAAS,KAAK,SAAS,WAAW,KAAK,WACtE,UAAA,oBAAC,gBAAA,EAAe,UAAU,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,QAAY,UAAA,KAAK,OAAM,EAAA,CACjG,EAAA,GAPe,GAAG,IAAI,IAAI,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK,OAAA,CAAQ,EAS3F;AAGF,YAAM,iBAAiB,MAAM,SAAS;AACtC,YAAM,UAAU,KAAK,IAAI,GAAG,mBAAmB;AAC/C,YAAM,SAAS,KAAK,IAAI,GAAG,kBAAkB;AAG7C,UAAI;AACJ,UAAI,CAAC,gBAAgB;AACnB,kBAAU,MAAM,IAAI,CAAC,MAAM,OAAO;AAAA,UAChC;AAAA,UACA,MAAM,MAAM,IAAI,SAAU,MAAM,MAAM,SAAS,IAAI,YAAY;AAAA,QAAA,EAC/D;AAAA,MACJ,OAAO;AACL,cAAM,SAAS,MAAM,MAAM,GAAG,OAAO,EAAE,IAAI,CAAC,MAAM,OAAO;AAAA,UACvD;AAAA,UACA,MAAO,MAAM,IAAI,SAAS;AAAA,QAAA,EAC1B;AACF,cAAM,YAAY,MAAM,MAAM,SAAS,MAAM,SAAS,MAAM;AAC5D,cAAM,QAAQ,MAAM,MAAM,MAAM,SAAS,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,SAAS;AAAA,UACtE;AAAA,UACA,MAAO,MAAM,IAAI,SAAS,IAAI,YAAY;AAAA,QAAA,EAC1C;AACF,kBAAU,CAAC,GAAG,QAAQ,EAAE,YAAY,UAAA,GAAa,GAAG,KAAK;AAAA,MAC3D;AAGA,YAAM,WAA8B,CAAA;AACpC,cAAQ,QAAQ,CAAC,OAAO,MAAM;AAC5B,YAAI,IAAI,EAAG,UAAS,yBAAM,qBAAA,CAAA,GAAyB,OAAO,CAAC,EAAI,CAAE;AACjE,YAAI,gBAAgB,OAAO;AACzB,mBAAS;AAAA,YACP,oBAAC,gBAAA,EAA8B,MAAK,YAClC,+BAAC,cAAA,EACC,UAAA;AAAA,cAAA,oBAAC,qBAAA,EAAoB,SAAO,MAC1B,UAAA,oBAAC,sBAAmB,GACtB;AAAA,cACA,oBAAC,qBAAA,EAAoB,OAAM,SACxB,gBAAM,WAAW,IAAI,CAAC,GAAG,MACxB,oBAAC,kBAAA,EAAyB,SAAS,CAAC,CAAC,EAAE,MACpC,UAAA,EAAE,OAAO,oBAAC,KAAA,EAAE,MAAM,EAAE,MAAO,UAAA,EAAE,MAAA,CAAM,IAAO,oBAAC,UAAM,UAAA,EAAE,OAAM,EAAA,GADrC,CAEvB,CACD,EAAA,CACH;AAAA,YAAA,EAAA,CACF,KAZkB,UAapB;AAAA,UAAA;AAAA,QAEJ,OAAO;AACL,mBAAS,KAAK,WAAW,MAAM,MAAM,MAAM,IAAI,CAAC;AAAA,QAClD;AAAA,MACF,CAAC;AACD,aAAO;AAAA,IACT,GAAG,CAAC,OAAO,UAAU,qBAAqB,kBAAkB,CAAC;AAM7D,UAAM,uBAAuB,MAAM,QAAQ,MAAM;AAC/C,UAAI,MAAO,QAAO;AAClB,YAAM,WAAW,MAAM,SAAS,QAAQ,QAAQ;AAMhD,YAAM,eAAe,SAAS;AAAA,QAAO,CAAC,MAAA;;AACpC,uBAAM,eAAe,CAAC,MAAM,EAAE,SAAS,oBACpC,OAAE,SAAF,mBAAgC,iBAAgB;AAAA;AAAA,MAAA;AAGrD,UAAI,aAAa,WAAW,EAAG,QAAO;AAEtC,YAAM,QAAQ,aAAa;AAC3B,YAAM,gBAAgB,CAAC,MAA+C,KAAa,SACjF,MAAM,aAAa,MAAM,EAAE,MAAM,KAAK,MAAM,QAAQ,MAAM,KAAK,MAAM,IAAI,IAAI,GAAG,IAAI;AACtF,YAAM,iBAAiB,QAAQ;AAC/B,YAAM,UAAU,KAAK,IAAI,GAAG,mBAAmB;AAC/C,YAAM,SAAS,KAAK,IAAI,GAAG,kBAAkB;AAC7C,YAAM,WAA8B,CAAA;AACpC,UAAI,CAAC,gBAAgB;AACnB,qBAAa,QAAQ,CAAC,MAAM,MAAM;AAChC,cAAI,IAAI,EAAG,UAAS,yBAAM,qBAAA,CAAA,GAAyB,OAAO,CAAC,EAAI,CAAE;AACjE,gBAAM,OAAsC,MAAM,IAAI,SAAU,MAAM,QAAQ,IAAI,YAAY;AAC9F,mBAAS,KAAK,cAAc,MAAM,GAAG,IAAI,CAAC;AAAA,QAC5C,CAAC;AAAA,MACH,OAAO;AAEL,cAAM,cAAc,aAAa,MAAM,GAAG,OAAO;AACjD,cAAM,iBAAiB,aAAa,MAAM,SAAS,QAAQ,MAAM;AACjE,cAAM,aAAa,aAAa,MAAM,QAAQ,MAAM;AACpD,oBAAY,QAAQ,CAAC,MAAM,MAAM;AAC/B,cAAI,IAAI,EAAG,UAAS,yBAAM,qBAAA,CAAA,GAAyB,WAAW,CAAC,EAAI,CAAE;AACrE,gBAAM,OAA0B,MAAM,IAAI,SAAS;AACnD,mBAAS,KAAK,cAAc,MAAM,GAAG,IAAI,CAAC;AAAA,QAC5C,CAAC;AACD,YAAI,SAAS,SAAS,EAAG,UAAS,KAAK,oBAAC,qBAAA,IAAwB,qBAAsB,CAAE;AACxF,iBAAS;AAAA,UACP,oBAAC,gBAAA,EAAiC,MAAK,YACrC,+BAAC,cAAA,EACC,UAAA;AAAA,YAAA,oBAAC,qBAAA,EAAoB,SAAO,MAC1B,UAAA,oBAAC,sBAAmB,GACtB;AAAA,YACA,oBAAC,uBAAoB,OAAM,SACxB,yBAAe,IAAI,CAAC,MAAM,MAAM;AAK/B,oBAAM,gBAAiB,KAAK,MAAyC;AACrE,kBAAI;AACJ,kBAAI,QAAyB;AAC7B,oBAAM,SAAS,QAAQ,eAAe,CAAC,MAAM;AAC3C,oBAAI,MAAM,eAA8D,CAAC,GAAG;AAC1E,sBAAI,EAAE,MAAM,KAAM,QAAO,EAAE,MAAM;AACjC,sBAAI,EAAE,MAAM,YAAY,KAAM,SAAQ,EAAE,MAAM;AAAA,gBAChD;AAAA,cACF,CAAC;AACD,qBAAO,OACH,oBAAC,kBAAA,EAAwC,SAAO,MAAC,UAAA,oBAAC,OAAE,MAAa,UAAA,MAAA,CAAM,KAAhD,aAAa,CAAC,EAAsC,IAC3E,oBAAC,oBAAyC,UAAA,MAAA,GAAnB,aAAa,CAAC,EAAW;AAAA,YACtD,CAAC,EAAA,CACH;AAAA,UAAA,EAAA,CACF,KAzBkB,aA0BpB;AAAA,QAAA;AAEF,iBAAS,KAAK,oBAAC,qBAAA,CAAA,GAAwB,oBAAqB,CAAE;AAC9D,mBAAW,QAAQ,CAAC,MAAM,MAAM;AAC9B,cAAI,IAAI,EAAG,UAAS,yBAAM,qBAAA,CAAA,GAAyB,WAAW,CAAC,EAAI,CAAE;AACrE,gBAAM,OAA6B,MAAM,WAAW,SAAS,IAAI,YAAY;AAC7E,mBAAS,KAAK,cAAc,MAAM,GAAG,IAAI,CAAC;AAAA,QAC5C,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT,GAAG,CAAC,OAAO,UAAU,UAAU,qBAAqB,kBAAkB,CAAC;AAEvE,WACA,oBAAC,kBAAkB,UAAlB,EAA2B,OAAO,UACjC,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QAMA,WAAW;AAAA,UACT;AAAA,UACA,sBAAsB,IAAI;AAAA,UAC1B;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,kBAAQ,qBAAqB;AAAA,MAAA;AAAA,IAAA,GAElC;AAAA,EAEF;AACF;AACA,eAAe,cAAc;AAqB7B,MAAM,iBAAiB,MAAM;AAAA,EAC3B,CAAC,EAAE,WAAW,MAAM,OAAO,GAAG,MAAA,GAAS,QAAQ;AAY7C,UAAM,cAAmC,SAAS,SAAS,EAAE,YAAY,GAAG,UAAU,SAAA,IAClF,SAAS,WAAW,EAAE,YAAY,GAAG,UAAU,SAAA,IAC/C,SAAS,YAAY,EAAE,YAAY,GAAG,UAAU,SAAA,IAChD,SAAS,aAAa,EAAE,YAAY,EAAA,IACpC,CAAA;AACJ,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,gBAAc;AAAA,QACd,WAAW,GAAG,oCAAoC,SAAS;AAAA,QAC3D,OAAO,EAAE,GAAG,aAAa,GAAG,MAAA;AAAA,QAC3B,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAGV;AACF;AACA,eAAe,cAAc;AAe7B,MAAM,iBAAiB,MAAM;AAAA,EAC3B,CAAC,EAAE,SAAS,WAAW,UAAU,WAAW,WAAW,GAAG,MAAA,GAAS,QAAQ;AACzE,UAAM,EAAE,KAAA,IAAS,MAAM,WAAW,iBAAiB;AAKnD,UAAM,kBAAkB,OAAO,aAAa,+BACvC,gBAAA,EAAe,UAAU,UAAW,SAAA,CAAS,IAC9C;AACJ,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AASF,QAAI,SAAS;AACX,iCACG,MAAA,EAAK,KAAU,WAAW,iBAAkB,GAAG,OAC7C,UAAA,iBACH;AAAA,IAEJ;AACA,gCACG,KAAA,EAAE,KAAU,WAAW,iBAAkB,GAAG,OAC1C,UAAA;AAAA,MAAA,aAAa,oBAAC,aAAU,MAAM,qBAAqB,IAAI,GAAG,eAAW,MAAC,WAAU,WAAA,CAAW;AAAA,MAC3F;AAAA,IAAA,GACH;AAAA,EAEJ;AACF;AACA,eAAe,cAAc;AAS7B,MAAM,iBAAiB,MAAM;AAAA,EAC3B,CAAC,EAAE,WAAW,UAAU,WAAW,WAAW,GAAG,MAAA,GAAS,QAAQ;AAChE,UAAM,EAAE,KAAA,IAAS,MAAM,WAAW,iBAAiB;AAEnD,UAAM,kBAAkB,OAAO,aAAa,+BACvC,gBAAA,EAAe,UAAU,UAAW,SAAA,CAAS,IAC9C;AACJ,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,iBAAc;AAAA,QACd,gBAAa;AAAA,QACb,WAAW,GAAG,qEAAqE,SAAS;AAAA,QAC3F,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,aAAa,oBAAC,aAAU,MAAM,qBAAqB,IAAI,GAAG,eAAW,MAAC,WAAU,WAAA,CAAW;AAAA,UAC3F;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AACA,eAAe,cAAc;AAS7B,MAAM,sBAAsB,MAAM;AAAA,EAChC,CAAC,EAAE,UAAU,WAAW,GAAG,MAAA,GAAS,QAAQ;AAC1C,UAAM,EAAE,KAAA,IAAS,MAAM,WAAW,iBAAiB;AACnD,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,eAAY;AAAA,QAEZ,WAAW,GAAG,mDAAmD,SAAS;AAAA,QACzE,GAAG;AAAA,QAEH,UAAA,gCAAa,cAAA,EAAa,MAAM,qBAAqB,IAAI,GAAG,eAAW,KAAA,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,EAG/E;AACF;AACA,oBAAoB,cAAc;AAiClC,MAAM,qBAAqB,MAAM;AAAA,EAC/B,CAAC,EAAE,cAAc,YAAY,UAA8E,GAAG,MAAA,GAAS,QAAQ;AAC7H,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAM;AAAA,QACN,MAAK;AAAA,QACL,cAAY;AAAA,QACZ,gBAAc;AAAA,QACb,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAGV;AACF;AACA,mBAAmB,cAAc;AAI1B,MAAM,iBAAiB;AAAA,EAC5B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA,IACJ,IAAI,CAAC,iBAAiB,qBAAqB,iBAAiB;AAAA,IAC5D,MAAM,CAAC,WAAW;AAAA,EAAA;AAEtB;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
3
3
|
import { type VariantProps } from "class-variance-authority";
|
|
4
|
-
import type { FieldMode, FieldVariant } from "
|
|
4
|
+
import type { FieldMode, FieldVariant } from "../../components/Field/field-types";
|
|
5
5
|
declare const checkboxVariants: (props?: ({
|
|
6
6
|
size?: "sm" | "md" | "lg" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type { FieldMode, FieldVariant } from '
|
|
3
|
-
import { type SelectMenuOption } from '
|
|
2
|
+
import type { FieldMode, FieldVariant } from '../../components/Field/field-types';
|
|
3
|
+
import { type SelectMenuOption } from '../../components/SelectMenu/select-menu';
|
|
4
4
|
/**
|
|
5
5
|
* Combobox option schema(2026-05-10 post-Issue-4 audit unify):**explicit extends
|
|
6
6
|
* SelectMenuOption(primitive SSOT)** — 避免重蹈先前 PeoplePicker 改壞的 wrapper schema drift。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type { FieldMode, FieldVariant } from '
|
|
3
|
-
import { type TimeStep } from '
|
|
2
|
+
import type { FieldMode, FieldVariant } from '../../components/Field/field-types';
|
|
3
|
+
import { type TimeStep } from '../../components/TimePicker/time-columns';
|
|
4
4
|
export interface DateFormatOptions {
|
|
5
5
|
/** Intl.DateTimeFormat options(預設 { year: 'numeric', month: '2-digit', day: '2-digit' }) */
|
|
6
6
|
formatOptions?: Intl.DateTimeFormatOptions;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
3
|
-
import { type SurfaceHeaderProps } from "
|
|
3
|
+
import { type SurfaceHeaderProps } from "../../patterns/overlay-surface/overlay-surface";
|
|
4
4
|
/**
|
|
5
5
|
* Dialog (Modal) — Radix Dialog + 設計系統 token
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog.js","sources":["../../../src/components/Dialog/dialog.tsx"],"sourcesContent":["// @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.\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X as XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/design-system/components/Button/button\"\nimport { SurfaceHeader, SurfaceFooter, type SurfaceHeaderProps } from \"@/design-system/patterns/overlay-surface/overlay-surface\"\nimport { ScrollArea } from \"@/design-system/components/ScrollArea/scroll-area\"\n\n/**\n * Dialog (Modal) — Radix Dialog + 設計系統 token\n *\n * ── Layout ──\n * px = layout-space-loose, header/footer py = layout-space-tight。\n * Body pt = layout-space-tight, pb = layout-space-bottom。\n * Density:繼承 page `data-density`(v5 校準,跟 Sheet 對齊;header 自動對齊\n * `--chrome-header-height` 48/56)。詳 dialog.spec.md「Density」節。\n *\n * ── Viewport Inset ──\n * Modal 與 viewport 四邊保持 layout-space-bottom (48px) 最小間距。\n *\n * ── 高度行為 ──\n * 預設:height 填滿 viewport(扣除 inset),body 捲動。防止動態內容跳動。\n * height=\"auto\":高度隨內容,超過 viewport 時 max-height 安全帽。\n */\n\nconst Dialog = DialogPrimitive.Root\nconst DialogTrigger = DialogPrimitive.Trigger\nconst DialogPortal = DialogPrimitive.Portal\nconst DialogClose = DialogPrimitive.Close\n\n// Modal 與 viewport 四邊的最小間距 = layout-space-bottom (48px)\nconst DIALOG_INSET_VAR = 'var(--layout-space-bottom)'\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-overlay\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out motion-reduce:animate-none\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\ninterface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /** 最大寬度。預設 512px。傳 number 視為 px。 */\n maxWidth?: string | number\n /**\n * 高度模式。\n * - 不傳(預設):填滿 viewport(height = 100vh - inset*2),body 捲動。防止內容跳動。\n * - true:高度隨內容,超過 viewport 時捲動(max-height 安全帽)。\n */\n autoHeight?: boolean\n}\n\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(({ className, maxWidth = '512px', autoHeight, children, style, ...props }, ref) => {\n const insetCalc = `${DIALOG_INSET_VAR} * 2`\n const viewportH = `calc(100vh - ${insetCalc})`\n const maxWidthCss = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth\n\n const heightStyle: React.CSSProperties = autoHeight\n ? { maxHeight: viewportH }\n : { height: viewportH }\n\n // AutoFocus canonical(對齊 Material / Polaris / Atlassian)—\n // 開啟時 focus 落在 body 第一個有意義互動元素(input / button),不是 chrome close X。\n // 預設 Radix 會 focus first tabbable = close X → Button iconOnly 的 focus-triggered\n // tooltip 會立即顯示「關閉」,user-hostile。此 callback 攔截:先找 body 第一個\n // input/textarea/select/button(排除 data-dismiss)focus;找不到就 focus container(不 focus X)。\n const handleOpenAutoFocus = (e: Event) => {\n e.preventDefault()\n const content = e.currentTarget as HTMLElement\n const firstBodyTarget = content.querySelector<HTMLElement>(\n '[data-dialog-body] input:not([disabled]),[data-dialog-body] textarea:not([disabled]),[data-dialog-body] select:not([disabled]),[data-dialog-body] button:not([disabled]):not([data-dismiss])'\n )\n const firstFooterButton = content.querySelector<HTMLElement>(\n '[data-dialog-footer] button:not([disabled]):not([data-dismiss])'\n )\n ;(firstBodyTarget ?? firstFooterButton ?? content).focus({ preventScroll: true })\n }\n\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n // Density canonical(2026-04-22 v5 校準):Dialog 繼承 page density(跟 Sheet 對齊\n // sheet.tsx line 111 canonical),不自設 data-layout-space=\"lg\" 或 data-density。\n //\n // 先前曾設 `data-layout-space=\"lg\"` 給 header/body 寬鬆呼吸,但跟 chrome-header-height\n // canonical 衝突(md page dialog header 期望 48,強設 lg 會變 56)。\n // 世界級對照:Polaris Modal horizontal padding 16 / Material M3 24 / Atlassian 24 — 16 是\n // 合理 lower bound;md page 用 16 loose body padding 可接受,lg page 自動 24。\n // 詳 overlay-surface.spec.md「Chrome dismiss size canonical」\n onOpenAutoFocus={handleOpenAutoFocus}\n className={cn(\n \"fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2\",\n \"flex flex-col bg-surface-raised rounded-lg border border-border\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out motion-reduce:animate-none\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]\",\n \"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]\",\n className,\n )}\n style={{\n boxShadow: 'var(--elevation-200)',\n maxWidth: `min(${maxWidthCss}, calc(100vw - ${insetCalc}))`,\n ...heightStyle,\n ...style,\n }}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n})\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\n// DialogHeader: SurfaceHeader + Close 按鈕(Dialog 特有)\n// justify-between 讓 children 與 Close 分左右;Close 用 Radix DialogPrimitive.Close 包裝。\n// 2026-05-18 audit gap fix:type 用 SurfaceHeaderProps 對齊 — DialogHeader 是 SurfaceHeader\n// 薄包裝,withTabs / lockDensity 等 props 透過 spread 已 forward,但 TS type 沒 expose\n// 導致 consumer 不能用 `<DialogHeader withTabs>` 而只能寫 `as any` 繞。Type lift 修\n// per header-canonical.spec.md W1 跨 6 consumer 同契約。\nconst DialogHeader = React.forwardRef<\n HTMLDivElement,\n SurfaceHeaderProps\n>(({ className, children, ...props }, ref) => (\n // 2026-05-18:className 不再硬加 justify-between(冗餘:row 1 是 flex items-center gap-2,\n // 第一 child flex-1 grow 自然 push close X 靠右,跟 justify-between 同視覺)。\n // 並且 column mode(tabsSlot 提供)justify-between 會把 row 1 / row 2 上下推開 = 破裂。\n // tabsSlot via `...props` spread 自動 forward(type 來自 SurfaceHeaderProps)。\n <SurfaceHeader\n ref={ref}\n className={className}\n {...props}\n >\n <div className=\"flex-1 min-w-0\">{children}</div>\n <DialogPrimitive.Close asChild>\n {/* Dismiss X(chrome-slot canonical,v5):Button 本身 native sm(28 md / 32 lg,touch target 亦同),\n 但 `data-dismiss` attribute 讓 SurfaceHeader CSS rule 套負 my 讓 layout 佔位 = 24,\n header = 24 + 2×tight = 48 / 56 chrome-header-height ✓。\n 詳 overlay-surface.spec.md「Chrome dismiss size canonical」*/}\n <Button data-dismiss iconOnly dismiss size=\"sm\" startIcon={XIcon} aria-label=\"關閉\" />\n </DialogPrimitive.Close>\n </SurfaceHeader>\n))\nDialogHeader.displayName = \"DialogHeader\"\n\n// DialogBody: flex-1 ScrollArea + chrome padding(對齊 overlay-surface SSOT + ScrollArea canonical)\n// 捲軸必用 ScrollArea(跨 OS 一致、不吃寬度)— 不自寫 overflow-y-auto。\n// padding 搬進 viewport inner div:px-loose / pt-tight / pb-bottom(Dialog 「大容器」底部多一拍呼吸)。\n// data-dialog-body:讓 DialogContent onOpenAutoFocus 找得到 body 第一個有意義互動元素(避免 focus 到 close X)\n//\n// ── List-as-region 場景(menu group / Cmd+K)──\n// 不再提供 `flush` variant(2026-05-01 移除,先前曾叫 `variant=\"list\"`)。\n// **canonical pattern** = consumer 自管 list outer wrapper + 用 `className` override 撤掉 chrome padding:\n// ```tsx\n// <DialogBody className=\"!px-0 !pt-0 !pb-0\">\n// <div className=\"py-2\"> {/* list outer wrapper 自帶 py-2(menu group canonical)*/}\n// {items.map(item => <MenuItem className=\"px-[var(--layout-space-loose)] rounded-md\" />)}\n// </div>\n// </DialogBody>\n// ```\n// **rationale**:flush 只為 list-only body 省一行 className,但 (a) 多一個 row(search / banner)\n// 就破功 → 保留 chrome padding 反而更穩,(b) 加新 variant 不解決底層脆弱(consumer 仍要管 list py\n// 且 item px-loose),反而把 1 個 surface decision 拆兩 API。世界級主流(Material/Atlassian/Mantine/\n// shadcn)無 universal LayoutBody flush variant,Polaris flush API 只用於極窄 scope。\n// 詳 `tokens/layoutSpace/layoutSpace.spec.md`「List-as-region in overlay body」節\n// `className` forward 到 **inner content div**(非外層 ScrollArea wrapper)——\n// consumer `<DialogBody className=\"flex flex-col gap-X\">` 期望作用於 children 排列;\n// 套在 ScrollArea 上會 0 效果(children 住 inner div),曾造成 modal form field 完全貼邊。\nconst DialogBody = React.forwardRef<\n HTMLDivElement,\n React.ComponentPropsWithoutRef<typeof ScrollArea>\n>(({ className, children, ...props }, ref) => (\n <ScrollArea ref={ref} data-dialog-body className=\"flex-1 min-h-0\" {...props}>\n <div\n className={cn(\n \"px-[var(--layout-space-loose)] pt-[var(--layout-space-tight)] pb-[var(--layout-space-bottom)]\",\n className,\n )}\n >\n {children}\n </div>\n </ScrollArea>\n))\nDialogBody.displayName = \"DialogBody\"\n\n// DialogFooter: SurfaceFooter wrap 加 data-dialog-footer(autoFocus fallback target)\nconst DialogFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ ...props }, ref) => <SurfaceFooter ref={ref} data-dialog-footer {...props} />)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"text-body-lg font-medium truncate\", className)}\n {...props}\n />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n // title → description 間距 canonical:DialogTitle 是 body-lg(16)+ desc body(14)→ reading-lg token\n // (label tier 決定 token 選擇;item-anatomy Family 2 reading-family token 對照表)\n className={cn(\"mt-[var(--item-gap-label-desc-reading-lg)] text-body text-fg-secondary\", className)}\n {...props}\n />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const dialogMeta = {\n component: 'Dialog',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['text-fg-secondary'],\n ring: [],\n },\n} as const\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogBody,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n"],"names":["XIcon"],"mappings":";;;;;;;;AA2BA,MAAM,SAAS,gBAAgB;AAC/B,MAAM,gBAAgB,gBAAgB;AACtC,MAAM,eAAe,gBAAgB;AACrC,MAAM,cAAc,gBAAgB;AAGpC,MAAM,mBAAmB;AAEzB,MAAM,gBAAgB,MAAM,WAG1B,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,EAAA;AACN,CACD;AACD,cAAc,cAAc,gBAAgB,QAAQ;AAapD,MAAM,gBAAgB,MAAM,WAG1B,CAAC,EAAE,WAAW,WAAW,SAAS,YAAY,UAAU,OAAO,GAAG,MAAA,GAAS,QAAQ;AACnF,QAAM,YAAY,GAAG,gBAAgB;AACrC,QAAM,YAAY,gBAAgB,SAAS;AAC3C,QAAM,cAAc,OAAO,aAAa,WAAW,GAAG,QAAQ,OAAO;AAErE,QAAM,cAAmC,aACrC,EAAE,WAAW,cACb,EAAE,QAAQ,UAAA;AAOd,QAAM,sBAAsB,CAAC,MAAa;AACxC,MAAE,eAAA;AACF,UAAM,UAAU,EAAE;AAClB,UAAM,kBAAkB,QAAQ;AAAA,MAC9B;AAAA,IAAA;AAEF,UAAM,oBAAoB,QAAQ;AAAA,MAChC;AAAA,IAAA;AAED,KAAC,mBAAmB,qBAAqB,SAAS,MAAM,EAAE,eAAe,MAAM;AAAA,EAClF;AAEA,8BACG,cAAA,EACC,UAAA;AAAA,IAAA,oBAAC,eAAA,EAAc;AAAA,IACf;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC;AAAA,QASA,iBAAiB;AAAA,QACjB,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,OAAO;AAAA,UACL,WAAW;AAAA,UACX,UAAU,OAAO,WAAW,kBAAkB,SAAS;AAAA,UACvD,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAAA,QAEJ,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA;AAAA,EACH,GACF;AAEJ,CAAC;AACD,cAAc,cAAc,gBAAgB,QAAQ;AAQpD,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,UAAU,GAAG,MAAA,GAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpC;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA;AAAA,MACC,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAI,WAAU,kBAAkB,SAAA,CAAS;AAAA,QAC1C,oBAAC,gBAAgB,OAAhB,EAAsB,SAAO,MAK5B,UAAA,oBAAC,UAAO,gBAAY,MAAC,UAAQ,MAAC,SAAO,MAAC,MAAK,MAAK,WAAWA,GAAO,cAAW,MAAK,EAAA,CACpF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAAA,CAEH;AACD,aAAa,cAAc;AAyB3B,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,WAAW,UAAU,GAAG,MAAA,GAAS,QACpC,oBAAC,cAAW,KAAU,oBAAgB,MAAC,WAAU,kBAAkB,GAAG,OACpE,UAAA;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,WAAW;AAAA,MACT;AAAA,MACA;AAAA,IAAA;AAAA,IAGD;AAAA,EAAA;AACH,GACF,CACD;AACD,WAAW,cAAc;AAGzB,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,GAAG,MAAA,GAAS,QAAQ,oBAAC,iBAAc,KAAU,sBAAkB,MAAE,GAAG,OAAO,CAAE;AAClF,aAAa,cAAc;AAE3B,MAAM,cAAc,MAAM,WAGxB,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,qCAAqC,SAAS;AAAA,IAC3D,GAAG;AAAA,EAAA;AACN,CACD;AACD,YAAY,cAAc,gBAAgB,MAAM;AAEhD,MAAM,oBAAoB,MAAM,WAG9B,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IAGA,WAAW,GAAG,0EAA0E,SAAS;AAAA,IAChG,GAAG;AAAA,EAAA;AACN,CACD;AACD,kBAAkB,cAAc,gBAAgB,YAAY;AAIrD,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,mBAAmB;AAAA,IACxB,IAAI,CAAC,mBAAmB;AAAA,IACxB,MAAM,CAAA;AAAA,EAAC;AAEX;"}
|
|
1
|
+
{"version":3,"file":"dialog.js","sources":["../../../src/components/Dialog/dialog.tsx"],"sourcesContent":["// @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.\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X as XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/design-system/components/Button/button\"\nimport { SurfaceHeader, SurfaceFooter, type SurfaceHeaderProps } from \"@/design-system/patterns/overlay-surface/overlay-surface\"\nimport { ScrollArea } from \"@/design-system/components/ScrollArea/scroll-area\"\n\n/**\n * Dialog (Modal) — Radix Dialog + 設計系統 token\n *\n * ── Layout ──\n * px = layout-space-loose, header/footer py = layout-space-tight。\n * Body pt = layout-space-tight, pb = layout-space-bottom。\n * Density:繼承 page `data-density`(v5 校準,跟 Sheet 對齊;header 自動對齊\n * `--chrome-header-height` 48/56)。詳 dialog.spec.md「Density」節。\n *\n * ── Viewport Inset ──\n * Modal 與 viewport 四邊保持 layout-space-bottom (48px) 最小間距。\n *\n * ── 高度行為 ──\n * 預設:height 填滿 viewport(扣除 inset),body 捲動。防止動態內容跳動。\n * height=\"auto\":高度隨內容,超過 viewport 時 max-height 安全帽。\n */\n\nconst Dialog = DialogPrimitive.Root\nconst DialogTrigger = DialogPrimitive.Trigger\nconst DialogPortal = DialogPrimitive.Portal\nconst DialogClose = DialogPrimitive.Close\n\n// Modal 與 viewport 四邊的最小間距 = layout-space-bottom (48px)\nconst DIALOG_INSET_VAR = 'var(--layout-space-bottom)'\n\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-overlay\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out motion-reduce:animate-none\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\ninterface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {\n /** 最大寬度。預設 512px。傳 number 視為 px。 */\n maxWidth?: string | number\n /**\n * 高度模式。\n * - 不傳(預設):填滿 viewport(height = 100vh - inset*2),body 捲動。防止內容跳動。\n * - true:高度隨內容,超過 viewport 時捲動(max-height 安全帽)。\n */\n autoHeight?: boolean\n}\n\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n DialogContentProps\n>(({ className, maxWidth = '512px', autoHeight, children, style, ...props }, ref) => {\n const insetCalc = `${DIALOG_INSET_VAR} * 2`\n const viewportH = `calc(100vh - ${insetCalc})`\n const maxWidthCss = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth\n\n const heightStyle: React.CSSProperties = autoHeight\n ? { maxHeight: viewportH }\n : { height: viewportH }\n\n // AutoFocus canonical(對齊 Material / Polaris / Atlassian)—\n // 開啟時 focus 落在 body 第一個有意義互動元素(input / button),不是 chrome close X。\n // 預設 Radix 會 focus first tabbable = close X → Button iconOnly 的 focus-triggered\n // tooltip 會立即顯示「關閉」,user-hostile。此 callback 攔截:先找 body 第一個\n // input/textarea/select/button(排除 data-dismiss)focus;找不到就 focus container(不 focus X)。\n const handleOpenAutoFocus = (e: Event) => {\n e.preventDefault()\n const content = e.currentTarget as HTMLElement\n const firstBodyTarget = content.querySelector<HTMLElement>(\n '[data-dialog-body] input:not([disabled]),[data-dialog-body] textarea:not([disabled]),[data-dialog-body] select:not([disabled]),[data-dialog-body] button:not([disabled]):not([data-dismiss])'\n )\n const firstFooterButton = content.querySelector<HTMLElement>(\n '[data-dialog-footer] button:not([disabled]):not([data-dismiss])'\n )\n ;(firstBodyTarget ?? firstFooterButton ?? content).focus({ preventScroll: true })\n }\n\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n // Density canonical(2026-04-22 v5 校準):Dialog 繼承 page density(跟 Sheet 對齊\n // sheet.tsx line 111 canonical),不自設 data-layout-space=\"lg\" 或 data-density。\n //\n // 先前曾設 `data-layout-space=\"lg\"` 給 header/body 寬鬆呼吸,但跟 chrome-header-height\n // canonical 衝突(md page dialog header 期望 48,強設 lg 會變 56)。\n // 世界級對照:Polaris Modal horizontal padding 16 / Material M3 24 / Atlassian 24 — 16 是\n // 合理 lower bound;md page 用 16 loose body padding 可接受,lg page 自動 24。\n // 詳 overlay-surface.spec.md「Chrome dismiss size canonical」\n onOpenAutoFocus={handleOpenAutoFocus}\n className={cn(\n \"fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2\",\n \"flex flex-col bg-surface-raised rounded-lg border border-border\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out motion-reduce:animate-none\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]\",\n \"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]\",\n className,\n )}\n style={{\n boxShadow: 'var(--elevation-200)',\n maxWidth: `min(${maxWidthCss}, calc(100vw - ${insetCalc}))`,\n ...heightStyle,\n ...style,\n }}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n})\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\n// DialogHeader: SurfaceHeader + Close 按鈕(Dialog 特有)\n// justify-between 讓 children 與 Close 分左右;Close 用 Radix DialogPrimitive.Close 包裝。\n// 2026-05-18 audit gap fix:type 用 SurfaceHeaderProps 對齊 — DialogHeader 是 SurfaceHeader\n// 薄包裝,withTabs / lockDensity 等 props 透過 spread 已 forward,但 TS type 沒 expose\n// 導致 consumer 不能用 `<DialogHeader withTabs>` 而只能寫 `as any` 繞。Type lift 修\n// per header-canonical.spec.md W1 跨 6 consumer 同契約。\nconst DialogHeader = React.forwardRef<\n HTMLDivElement,\n SurfaceHeaderProps\n>(({ className, children, ...props }, ref) => (\n // 2026-05-18:className 不再硬加 justify-between(冗餘:row 1 是 flex items-center gap-2,\n // 第一 child flex-1 grow 自然 push close X 靠右,跟 justify-between 同視覺)。\n // 並且 column mode(tabsSlot 提供)justify-between 會把 row 1 / row 2 上下推開 = 破裂。\n // tabsSlot via `...props` spread 自動 forward(type 來自 SurfaceHeaderProps)。\n <SurfaceHeader\n ref={ref}\n className={className}\n {...props}\n >\n <div className=\"flex-1 min-w-0\">{children}</div>\n {/* Dismiss X(chrome-slot canonical,v5):Button 本身 native sm(28 md / 32 lg,touch target 亦同),\n 但 data-dismiss attribute 讓 SurfaceHeader CSS rule 套負 my 讓 layout 佔位 = 24,\n header = 24 + 2×tight = 48 / 56 chrome-header-height ✓。\n 詳 overlay-surface.spec.md「Chrome dismiss size canonical」*/}\n <DialogPrimitive.Close asChild>\n <Button data-dismiss iconOnly dismiss size=\"sm\" startIcon={XIcon} aria-label=\"關閉\" />\n </DialogPrimitive.Close>\n </SurfaceHeader>\n))\nDialogHeader.displayName = \"DialogHeader\"\n\n// DialogBody: flex-1 ScrollArea + chrome padding(對齊 overlay-surface SSOT + ScrollArea canonical)\n// 捲軸必用 ScrollArea(跨 OS 一致、不吃寬度)— 不自寫 overflow-y-auto。\n// padding 搬進 viewport inner div:px-loose / pt-tight / pb-bottom(Dialog 「大容器」底部多一拍呼吸)。\n// data-dialog-body:讓 DialogContent onOpenAutoFocus 找得到 body 第一個有意義互動元素(避免 focus 到 close X)\n//\n// ── List-as-region 場景(menu group / Cmd+K)──\n// 不再提供 `flush` variant(2026-05-01 移除,先前曾叫 `variant=\"list\"`)。\n// **canonical pattern** = consumer 自管 list outer wrapper + 用 `className` override 撤掉 chrome padding:\n// ```tsx\n// <DialogBody className=\"!px-0 !pt-0 !pb-0\">\n// <div className=\"py-2\"> {/* list outer wrapper 自帶 py-2(menu group canonical)*/}\n// {items.map(item => <MenuItem className=\"px-[var(--layout-space-loose)] rounded-md\" />)}\n// </div>\n// </DialogBody>\n// ```\n// **rationale**:flush 只為 list-only body 省一行 className,但 (a) 多一個 row(search / banner)\n// 就破功 → 保留 chrome padding 反而更穩,(b) 加新 variant 不解決底層脆弱(consumer 仍要管 list py\n// 且 item px-loose),反而把 1 個 surface decision 拆兩 API。世界級主流(Material/Atlassian/Mantine/\n// shadcn)無 universal LayoutBody flush variant,Polaris flush API 只用於極窄 scope。\n// 詳 `tokens/layoutSpace/layoutSpace.spec.md`「List-as-region in overlay body」節\n// `className` forward 到 **inner content div**(非外層 ScrollArea wrapper)——\n// consumer `<DialogBody className=\"flex flex-col gap-X\">` 期望作用於 children 排列;\n// 套在 ScrollArea 上會 0 效果(children 住 inner div),曾造成 modal form field 完全貼邊。\nconst DialogBody = React.forwardRef<\n HTMLDivElement,\n React.ComponentPropsWithoutRef<typeof ScrollArea>\n>(({ className, children, ...props }, ref) => (\n <ScrollArea ref={ref} data-dialog-body className=\"flex-1 min-h-0\" {...props}>\n <div\n className={cn(\n \"px-[var(--layout-space-loose)] pt-[var(--layout-space-tight)] pb-[var(--layout-space-bottom)]\",\n className,\n )}\n >\n {children}\n </div>\n </ScrollArea>\n))\nDialogBody.displayName = \"DialogBody\"\n\n// DialogFooter: SurfaceFooter wrap 加 data-dialog-footer(autoFocus fallback target)\nconst DialogFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ ...props }, ref) => <SurfaceFooter ref={ref} data-dialog-footer {...props} />)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\"text-body-lg font-medium truncate\", className)}\n {...props}\n />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n // title → description 間距 canonical:DialogTitle 是 body-lg(16)+ desc body(14)→ reading-lg token\n // (label tier 決定 token 選擇;item-anatomy Family 2 reading-family token 對照表)\n className={cn(\"mt-[var(--item-gap-label-desc-reading-lg)] text-body text-fg-secondary\", className)}\n {...props}\n />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const dialogMeta = {\n component: 'Dialog',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['text-fg-secondary'],\n ring: [],\n },\n} as const\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogBody,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n}\n"],"names":["XIcon"],"mappings":";;;;;;;;AA2BA,MAAM,SAAS,gBAAgB;AAC/B,MAAM,gBAAgB,gBAAgB;AACtC,MAAM,eAAe,gBAAgB;AACrC,MAAM,cAAc,gBAAgB;AAGpC,MAAM,mBAAmB;AAEzB,MAAM,gBAAgB,MAAM,WAG1B,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,EAAA;AACN,CACD;AACD,cAAc,cAAc,gBAAgB,QAAQ;AAapD,MAAM,gBAAgB,MAAM,WAG1B,CAAC,EAAE,WAAW,WAAW,SAAS,YAAY,UAAU,OAAO,GAAG,MAAA,GAAS,QAAQ;AACnF,QAAM,YAAY,GAAG,gBAAgB;AACrC,QAAM,YAAY,gBAAgB,SAAS;AAC3C,QAAM,cAAc,OAAO,aAAa,WAAW,GAAG,QAAQ,OAAO;AAErE,QAAM,cAAmC,aACrC,EAAE,WAAW,cACb,EAAE,QAAQ,UAAA;AAOd,QAAM,sBAAsB,CAAC,MAAa;AACxC,MAAE,eAAA;AACF,UAAM,UAAU,EAAE;AAClB,UAAM,kBAAkB,QAAQ;AAAA,MAC9B;AAAA,IAAA;AAEF,UAAM,oBAAoB,QAAQ;AAAA,MAChC;AAAA,IAAA;AAED,KAAC,mBAAmB,qBAAqB,SAAS,MAAM,EAAE,eAAe,MAAM;AAAA,EAClF;AAEA,8BACG,cAAA,EACC,UAAA;AAAA,IAAA,oBAAC,eAAA,EAAc;AAAA,IACf;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC;AAAA,QASA,iBAAiB;AAAA,QACjB,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,OAAO;AAAA,UACL,WAAW;AAAA,UACX,UAAU,OAAO,WAAW,kBAAkB,SAAS;AAAA,UACvD,GAAG;AAAA,UACH,GAAG;AAAA,QAAA;AAAA,QAEJ,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA;AAAA,EACH,GACF;AAEJ,CAAC;AACD,cAAc,cAAc,gBAAgB,QAAQ;AAQpD,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,UAAU,GAAG,MAAA,GAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpC;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA;AAAA,MACC,GAAG;AAAA,MAEJ,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAI,WAAU,kBAAkB,SAAA,CAAS;AAAA,QAK1C,oBAAC,gBAAgB,OAAhB,EAAsB,SAAO,MAC5B,UAAA,oBAAC,UAAO,gBAAY,MAAC,UAAQ,MAAC,SAAO,MAAC,MAAK,MAAK,WAAWA,GAAO,cAAW,MAAK,EAAA,CACpF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAAA,CAEH;AACD,aAAa,cAAc;AAyB3B,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,WAAW,UAAU,GAAG,MAAA,GAAS,QACpC,oBAAC,cAAW,KAAU,oBAAgB,MAAC,WAAU,kBAAkB,GAAG,OACpE,UAAA;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,WAAW;AAAA,MACT;AAAA,MACA;AAAA,IAAA;AAAA,IAGD;AAAA,EAAA;AACH,GACF,CACD;AACD,WAAW,cAAc;AAGzB,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,GAAG,MAAA,GAAS,QAAQ,oBAAC,iBAAc,KAAU,sBAAkB,MAAE,GAAG,OAAO,CAAE;AAClF,aAAa,cAAc;AAE3B,MAAM,cAAc,MAAM,WAGxB,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,qCAAqC,SAAS;AAAA,IAC3D,GAAG;AAAA,EAAA;AACN,CACD;AACD,YAAY,cAAc,gBAAgB,MAAM;AAEhD,MAAM,oBAAoB,MAAM,WAG9B,CAAC,EAAE,WAAW,GAAG,MAAA,GAAS,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IAGA,WAAW,GAAG,0EAA0E,SAAS;AAAA,IAChG,GAAG;AAAA,EAAA;AACN,CACD;AACD,kBAAkB,cAAc,gBAAgB,YAAY;AAIrD,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,mBAAmB;AAAA,IACxB,IAAI,CAAC,mBAAmB;AAAA,IACxB,MAAM,CAAA;AAAA,EAAC;AAEX;"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
3
3
|
import { type LucideIcon } from "lucide-react";
|
|
4
|
-
import type { AvatarData } from "
|
|
5
|
-
import { type RowSize } from "
|
|
4
|
+
import type { AvatarData } from "../../components/Avatar/avatar";
|
|
5
|
+
import { type RowSize } from "../../patterns/element-anatomy/item-anatomy";
|
|
6
6
|
/**
|
|
7
7
|
* DropdownMenu — Radix DropdownMenu + MenuItem visual layer
|
|
8
8
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { type VariantProps } from 'class-variance-authority';
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
-
import type { FieldMode, FieldVariant } from '
|
|
5
|
-
import { fieldWrapperStyles } from '
|
|
6
|
-
import { type InlineActionConfig } from '
|
|
4
|
+
import type { FieldMode, FieldVariant } from '../../components/Field/field-types';
|
|
5
|
+
import { fieldWrapperStyles } from '../../components/Field/field-wrapper';
|
|
6
|
+
import { type InlineActionConfig } from '../../patterns/element-anatomy/item-anatomy';
|
|
7
7
|
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>, Omit<VariantProps<typeof fieldWrapperStyles>, 'mode' | 'variant'> {
|
|
8
8
|
/** Field display mode */
|
|
9
9
|
mode?: FieldMode;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { VariantProps } from 'class-variance-authority';
|
|
3
|
-
import type { FieldMode, FieldVariant } from '
|
|
4
|
-
import { fieldWrapperStyles } from '
|
|
3
|
+
import type { FieldMode, FieldVariant } from '../../components/Field/field-types';
|
|
4
|
+
import { fieldWrapperStyles } from '../../components/Field/field-wrapper';
|
|
5
5
|
export interface LinkInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'value' | 'onChange'>, Omit<VariantProps<typeof fieldWrapperStyles>, 'mode' | 'variant'> {
|
|
6
6
|
mode?: FieldMode;
|
|
7
7
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { type VariantProps } from 'class-variance-authority';
|
|
3
3
|
import type { LucideIcon } from 'lucide-react';
|
|
4
|
-
import { type AvatarData } from '
|
|
4
|
+
import { type AvatarData } from '../../components/Avatar/avatar';
|
|
5
5
|
declare const menuItemVariants: (props?: ({
|
|
6
|
-
size?: import("
|
|
6
|
+
size?: import("../../patterns/element-anatomy/item-anatomy").RowSize | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
export interface MenuItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>, VariantProps<typeof menuItemVariants> {
|
|
9
9
|
/** Label 文字 */
|