@qijenchen/design-system 0.1.0-beta.75 → 0.1.0-beta.76
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/CLAUDE.md +1 -1
- package/dist/components/AppShell/app-shell.d.ts +2 -2
- package/dist/components/AppShell/app-shell.js.map +1 -1
- package/dist/components/Avatar/avatar.js.map +1 -1
- package/dist/components/Button/button.d.ts.map +1 -1
- package/dist/components/Button/button.js.map +1 -1
- package/dist/components/Chart/chart.d.ts +1 -1
- package/dist/components/Chart/chart.js.map +1 -1
- package/dist/components/Checkbox/checkbox.d.ts +1 -1
- package/dist/components/Checkbox/checkbox.js +1 -1
- package/dist/components/Checkbox/checkbox.js.map +1 -1
- package/dist/components/Combobox/combobox.d.ts +1 -1
- package/dist/components/Combobox/combobox.d.ts.map +1 -1
- package/dist/components/Combobox/combobox.js +13 -10
- package/dist/components/Combobox/combobox.js.map +1 -1
- package/dist/components/Command/command.d.ts +1 -1
- package/dist/components/Command/command.js +3 -3
- package/dist/components/Command/command.js.map +1 -1
- package/dist/components/DataTable/cell-registry.d.ts.map +1 -1
- package/dist/components/DataTable/cell-registry.js +2 -2
- package/dist/components/DataTable/cell-registry.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +2 -2
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/DescriptionList/description-list.d.ts +1 -1
- package/dist/components/DescriptionList/description-list.js +2 -2
- package/dist/components/DescriptionList/description-list.js.map +1 -1
- package/dist/components/Empty/empty.d.ts +2 -0
- package/dist/components/Empty/empty.d.ts.map +1 -1
- package/dist/components/Empty/empty.js.map +1 -1
- package/dist/components/Field/field-wrapper.js +4 -4
- package/dist/components/Field/field-wrapper.js.map +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.d.ts +1 -1
- package/dist/components/OverflowIndicator/overflow-indicator.js +2 -2
- package/dist/components/OverflowIndicator/overflow-indicator.js.map +1 -1
- package/dist/components/PeoplePicker/people-picker.js +2 -2
- package/dist/components/PeoplePicker/people-picker.js.map +1 -1
- package/dist/components/ProfileCard/profile-card.d.ts +1 -1
- package/dist/components/ProfileCard/profile-card.js +2 -1
- package/dist/components/ProfileCard/profile-card.js.map +1 -1
- package/dist/components/Rating/rating.js.map +1 -1
- package/dist/components/Select/select.js +4 -4
- package/dist/components/Select/select.js.map +1 -1
- package/dist/components/Textarea/textarea.d.ts +1 -1
- package/dist/components/Textarea/textarea.js +2 -2
- package/dist/components/Textarea/textarea.js.map +1 -1
- package/dist/components/TimePicker/time-picker.d.ts.map +1 -1
- package/dist/components/TimePicker/time-picker.js +14 -23
- package/dist/components/TimePicker/time-picker.js.map +1 -1
- package/dist/components/TreeView/tree-view.d.ts +1 -1
- package/dist/components/TreeView/tree-view.js +1 -1
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/ds-canonical/fork/governance.lock +1 -1
- package/ds-canonical/fork/preamble.md +2 -2
- package/ds-canonical/hooks/check_field_controls_contracts.sh +30 -3
- package/ds-canonical/references/props-naming.md +15 -1
- package/ds-canonical/rules/ui-development.md +2 -2
- package/llms-full.txt +7 -2
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/Alert/alert.anatomy.stories.tsx +4 -4
- package/src/components/AppShell/app-shell.spec.md +4 -4
- package/src/components/AppShell/app-shell.tsx +2 -2
- package/src/components/Avatar/avatar.tsx +1 -1
- package/src/components/Breadcrumb/breadcrumb.spec.md +11 -1
- package/src/components/Button/button.tsx +0 -10
- package/src/components/Calendar/calendar.anatomy.stories.tsx +1 -1
- package/src/components/Chart/chart.tsx +1 -1
- package/src/components/Checkbox/checkbox.tsx +1 -1
- package/src/components/Coachmark/coachmark.anatomy.stories.tsx +1 -1
- package/src/components/Coachmark/coachmark.spec.md +2 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +12 -12
- package/src/components/Combobox/combobox.principles.stories.tsx +1 -1
- package/src/components/Combobox/combobox.spec.md +1 -1
- package/src/components/Combobox/combobox.tsx +25 -14
- package/src/components/Command/command.anatomy.stories.tsx +2 -0
- package/src/components/Command/command.tsx +2 -2
- package/src/components/DataTable/cell-registry.tsx +6 -2
- package/src/components/DataTable/data-table-filter-panel.tsx +3 -3
- package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.css +1 -1
- package/src/components/DataTable/data-table.spec.md +2 -2
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
- package/src/components/DateGrid/date-grid.spec.md +1 -1
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +15 -11
- package/src/components/DatePicker/date-picker.spec.md +1 -1
- package/src/components/DatePicker/date-picker.tsx +9 -6
- package/src/components/DescriptionList/description-list.tsx +1 -1
- package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
- package/src/components/DropdownMenu/dropdown-menu.spec.md +1 -1
- package/src/components/Empty/empty.tsx +2 -0
- package/src/components/Field/field-controls.spec.md +9 -6
- package/src/components/Field/field-wrapper.tsx +4 -4
- package/src/components/FileItem/file-item.principles.stories.tsx +1 -0
- package/src/components/FileUpload/file-upload.principles.stories.tsx +2 -2
- package/src/components/FileUpload/file-upload.spec.md +1 -1
- package/src/components/HoverCard/hover-card.principles.stories.tsx +1 -1
- package/src/components/Input/input.anatomy.stories.tsx +3 -3
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +3 -3
- package/src/components/Notice/notice.anatomy.stories.tsx +1 -1
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +8 -7
- package/src/components/NumberInput/number-input.spec.md +1 -1
- package/src/components/OverflowIndicator/overflow-indicator.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +3 -3
- package/src/components/PeoplePicker/people-picker.tsx +6 -6
- package/src/components/Popover/popover.principles.stories.tsx +4 -4
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.tsx +1 -1
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/Rating/rating.anatomy.stories.tsx +2 -2
- package/src/components/Rating/rating.spec.md +1 -1
- package/src/components/Rating/rating.tsx +1 -1
- package/src/components/Select/select.anatomy.stories.tsx +18 -18
- package/src/components/Select/select.spec.md +1 -1
- package/src/components/Select/select.tsx +7 -7
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +2 -2
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Steps/steps.spec.md +2 -2
- package/src/components/Tabs/tabs.principles.stories.tsx +1 -1
- package/src/components/Tabs/tabs.spec.md +1 -1
- package/src/components/Textarea/textarea.tsx +3 -3
- package/src/components/TimePicker/time-picker.spec.md +1 -1
- package/src/components/TimePicker/time-picker.tsx +11 -12
- package/src/components/TreeView/tree-view.tsx +1 -1
- package/src/patterns/element-anatomy/item-anatomy.spec.md +1 -1
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +1 -1
- package/src/patterns/overlay-surface/overlay-surface.spec.md +1 -0
- package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
- package/src/tokens/color/semantic.css +1 -1
- package/src/tokens/uiSize/uiSize.css +5 -0
- package/src/tokens/uiSize/uiSize.spec.md +17 -3
|
@@ -33,7 +33,7 @@ const COLOR_MAP: Record<ModeKey, Partial<Record<StateKey, ColorSpec>>> = {
|
|
|
33
33
|
disabled: { bg: '--bg-disabled', text: '--fg-disabled', border: 'transparent', icon: '--fg-disabled' },
|
|
34
34
|
},
|
|
35
35
|
readonly: {
|
|
36
|
-
default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '
|
|
36
|
+
default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent', icon: '—' },
|
|
37
37
|
},
|
|
38
38
|
disabled: {
|
|
39
39
|
default: { bg: '--bg-disabled', text: '--fg-disabled', border: 'transparent', icon: '--fg-disabled' },
|
|
@@ -50,9 +50,9 @@ interface SizeSpec {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
|
|
53
|
-
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-
|
|
54
|
-
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-
|
|
55
|
-
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-
|
|
53
|
+
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, clearHover: 18 },
|
|
54
|
+
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, clearHover: 18 },
|
|
55
|
+
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body-lg', font: '16px', icon: 20, clearHover: 22 },
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const MODE_DESC: Record<ModeKey, string> = {
|
|
@@ -172,7 +172,7 @@ export const Overview = {
|
|
|
172
172
|
<div className="flex flex-col gap-4">
|
|
173
173
|
<div className="flex flex-col gap-1">
|
|
174
174
|
<H3>結構(Anatomy)</H3>
|
|
175
|
-
<Desc>edit 模式:可點擊的觸發列顯示格式化日期文字 + 日曆圖示(固定右側),點任意位置都會展開日期面板。clearable 有值時額外顯示 X 清除按鈕。readonly
|
|
175
|
+
<Desc>edit 模式:可點擊的觸發列顯示格式化日期文字 + 日曆圖示(固定右側),點任意位置都會展開日期面板。clearable 有值時額外顯示 X 清除按鈕。readonly 模式:純格式化文字,無 Calendar icon、無 X。disabled 模式:格式化文字 + Calendar icon(類型身份 indicator,切 fg-disabled),無 X。</Desc>
|
|
176
176
|
</div>
|
|
177
177
|
<div className="flex gap-8">
|
|
178
178
|
{/* Edit layout */}
|
|
@@ -346,7 +346,7 @@ const InspectorInner = () => {
|
|
|
346
346
|
{ c: Z.pad, l: '左右內距' },
|
|
347
347
|
...(isEdit ? [{ c: Z.input, l: 'trigger text' }] : [{ c: Z.input, l: 'formatted text' }]),
|
|
348
348
|
...(showClear ? [{ c: Z.action, l: 'X clear' }] : []),
|
|
349
|
-
{ c: Z.icon, l: 'Calendar' },
|
|
349
|
+
...(mode !== 'readonly' ? [{ c: Z.icon, l: 'Calendar' }] : []),
|
|
350
350
|
].map(({ c, l }) => (
|
|
351
351
|
<span key={l} className="inline-flex items-center gap-1">
|
|
352
352
|
<span className="w-2.5 h-2.5 rounded-md" style={{ background: c.bg, border: `1px dashed ${c.border}` }} />
|
|
@@ -364,8 +364,12 @@ const InspectorInner = () => {
|
|
|
364
364
|
<BpZone w={44} color={Z.action} label={`${s.icon}px`} sub="clear" />
|
|
365
365
|
</>
|
|
366
366
|
)}
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
{mode !== 'readonly' && (
|
|
368
|
+
<>
|
|
369
|
+
<BpZone w={32} color={Z.gap} label={s.gapToken} sub={`${s.gap}px`} />
|
|
370
|
+
<BpZone w={44} color={Z.icon} label={`${s.icon}px`} sub="Calendar" />
|
|
371
|
+
</>
|
|
372
|
+
)}
|
|
369
373
|
<BpZone w={44} color={Z.pad} label={s.pxToken} sub={`${s.px}px`} />
|
|
370
374
|
</div>
|
|
371
375
|
<div className="ml-3 flex items-center" style={{ height: 52 }}>
|
|
@@ -393,7 +397,7 @@ const InspectorInner = () => {
|
|
|
393
397
|
<PropRow label="Fill"><TokenValue value={colors.bg} /></PropRow>
|
|
394
398
|
<PropRow label="Text"><TokenValue value={colors.text} /></PropRow>
|
|
395
399
|
<PropRow label="Border"><TokenValue value={colors.border} /></PropRow>
|
|
396
|
-
{
|
|
400
|
+
{mode !== 'readonly' && (
|
|
397
401
|
<PropRow label="Calendar">
|
|
398
402
|
<TokenValue value={colors.icon} />
|
|
399
403
|
</PropRow>
|
|
@@ -424,7 +428,7 @@ const InspectorInner = () => {
|
|
|
424
428
|
<PropRow label="高度" dot={Z.dim.text}><TkVal token={s.heightToken} value={s.height} /></PropRow>
|
|
425
429
|
<PropRow label="左右內距" dot={Z.pad.text}><TkVal token={s.pxToken} value={`${s.px}px`} /></PropRow>
|
|
426
430
|
<PropRow label="元素間距" dot={Z.gap.text}><TkVal token={s.gapToken} value={`${s.gap}px`} /></PropRow>
|
|
427
|
-
{
|
|
431
|
+
{mode !== 'readonly' && (
|
|
428
432
|
<PropRow label="Calendar" dot={Z.icon.text}>{s.icon}px</PropRow>
|
|
429
433
|
)}
|
|
430
434
|
{showClear && (
|
|
@@ -686,7 +690,7 @@ export const StateBehavior = {
|
|
|
686
690
|
<span className="text-fg-muted text-caption">→</span>
|
|
687
691
|
<DatePicker mode="disabled" value="2026-04-02" className="w-56" />
|
|
688
692
|
</div>
|
|
689
|
-
<span className="text-[11px] text-fg-muted">左:edit(有 X)→ 中:readonly(無 X
|
|
693
|
+
<span className="text-[11px] text-fg-muted">左:edit(有 X)→ 中:readonly(無 X、無 Calendar,純文字)→ 右:disabled(無 X,保留 Calendar,與文字切 fg-disabled)</span>
|
|
690
694
|
</div>
|
|
691
695
|
</div>
|
|
692
696
|
|
|
@@ -310,7 +310,7 @@ DatePicker 套 `React.forwardRef` + `displayName`;`DatePickerProps` extends `Omi
|
|
|
310
310
|
|
|
311
311
|
## 邊界案例
|
|
312
312
|
|
|
313
|
-
- **Disabled**:Field SSOT own;resolvedMode='disabled' 走 readonly / disabled 純 wrapper 分支(無 Popover trigger、`aria-disabled`、不開 picker),文字與 Calendar icon 切 `text-fg-disabled`。顯式 `mode="display"` 永遠最優先(useResolvedFieldMode step 1,見 field-context.ts):display + disabled 同傳走 display 分支不套 disabled token;要 disabled chrome 傳 `disabled` 或 `mode="disabled"`。
|
|
313
|
+
- **Disabled**:Field SSOT own;resolvedMode='disabled' 走 readonly / disabled 純 wrapper 分支(無 Popover trigger、`aria-disabled`、不開 picker),文字與 Calendar icon 切 `text-fg-disabled`。readonly 不顯示 Calendar icon(類型身份 indicator 規則,SSOT field-controls.spec.md L227);disabled 保留並切 `fg-disabled`。顯式 `mode="display"` 永遠最優先(useResolvedFieldMode step 1,見 field-context.ts):display + disabled 同傳走 display 分支不套 disabled token;要 disabled chrome 傳 `disabled` 或 `mode="disabled"`。
|
|
314
314
|
- **Loading(server-rendered grid)**:DatePicker 為 sync UI 不獨立 own loading。若 consumer 場景需 async date constraint fetch(如後端 disabled-dates list),consumer 應先 disable trigger 直到 fetch 完成,或在 popover 開啟後 body 切 `<Empty icon={<CircularProgress/>}/>`(對齊 panel-body loading SSOT)。本 spec scope 內不渲 loading state。
|
|
315
315
|
- **Empty(no value)**:`value=null` → trigger 顯 placeholder(預設 `YYYY/MM/DD`,showTime 時 `YYYY/MM/DD HH:MM`;consumer 可傳 `placeholder` 覆寫)。無導覽目標時鍵盤焦點停留(react-day-picker v9 內建)。
|
|
316
316
|
- **Invalid date input**:Field validation 處理 `aria-invalid="true"` + error border + 下方 error message;DatePicker 本身不 own validation 規則。
|
|
@@ -483,9 +483,9 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
|
|
|
483
483
|
: <span className="text-fg-muted">{EMPTY_DISPLAY}</span>
|
|
484
484
|
}
|
|
485
485
|
</span>
|
|
486
|
-
{/* 2026-06-
|
|
487
|
-
|
|
488
|
-
{(variant === 'naked' ? showDisplayEndIcon :
|
|
486
|
+
{/* 2026-06-26 類型身份 indicator:edit 顯示 / readonly 不顯示(純值、不可開) / disabled 保留(fg-disabled,對齊原生 <select disabled>);
|
|
487
|
+
naked cell 依 showDisplayEndIcon=isEditable(維持 2026-05-10 cell canonical「非可編欄不顯」)*/}
|
|
488
|
+
{(variant === 'naked' ? showDisplayEndIcon : resolvedMode === 'disabled') && (
|
|
489
489
|
<ItemSuffix className="pointer-events-none">
|
|
490
490
|
<CalendarIcon size={iconSize} className={resolvedMode === 'disabled' ? 'text-fg-disabled' : 'text-fg-muted'} aria-hidden />
|
|
491
491
|
</ItemSuffix>
|
|
@@ -897,9 +897,12 @@ const DatePickerRange = React.forwardRef<HTMLDivElement, DatePickerRangeProps>(
|
|
|
897
897
|
<span className={cn('flex-1 min-w-0 truncate', !endIso && 'text-fg-muted', resolvedMode === 'disabled' && 'text-fg-disabled')}>
|
|
898
898
|
{endIso ? formatDateOrDateTime(endIso, showTime, showSeconds, { formatOptions, locale }) : resolvedPlaceholder[1]}
|
|
899
899
|
</span>
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
900
|
+
{/* 2026-06-26 類型身份 indicator:readonly 不顯示 / disabled 保留(fg-disabled)— 對齊單一 DatePicker(:488)+ SSOT;Range 無 naked-cell 變體故 gate = resolvedMode==='disabled' */}
|
|
901
|
+
{resolvedMode === 'disabled' && (
|
|
902
|
+
<ItemSuffix className="pointer-events-none">
|
|
903
|
+
<CalendarIcon size={iconSize} className="text-fg-disabled" aria-hidden />
|
|
904
|
+
</ItemSuffix>
|
|
905
|
+
)}
|
|
903
906
|
</div>
|
|
904
907
|
)
|
|
905
908
|
}
|
|
@@ -123,7 +123,7 @@ DescriptionItem.displayName = 'DescriptionItem'
|
|
|
123
123
|
// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
|
|
124
124
|
export const descriptionListMeta = {
|
|
125
125
|
component: 'DescriptionList',
|
|
126
|
-
family:
|
|
126
|
+
family: 'composite', // 對齊 description-list.spec.md frontmatter family: composite(SSOT)
|
|
127
127
|
variants: {
|
|
128
128
|
|
|
129
129
|
},
|
|
@@ -417,7 +417,7 @@ export const ColorMatrix: Story = {
|
|
|
417
417
|
<div className="flex flex-col gap-8">
|
|
418
418
|
<div>
|
|
419
419
|
<H3>Density</H3>
|
|
420
|
-
<Desc>Dialog
|
|
420
|
+
<Desc>Dialog **不自設任何 density attribute**,layout-space + ui-size 全繼承 page。md page → body px-loose 16 / header py-tight 12 → header 48;lg page → 24 / 16 → header 56,隨 page tier。modal 寬鬆呼吸由 page tier 自動滿足(lg 拿 24 = Material 級;md 16 = Polaris Modal 下限)。世界級:SAP Fiori / AWS Cloudscape 皆讓 modal 跟 page density dial 走,不釘固定 tier。</Desc>
|
|
421
421
|
</div>
|
|
422
422
|
|
|
423
423
|
<div>
|
|
@@ -217,7 +217,7 @@ Item-level default / hover / focused / selected / disabled **色彩**由 MenuIte
|
|
|
217
217
|
- **Disabled item**:`<DropdownMenuItem disabled>`(Radix 內建支援),視覺繼承 MenuItem SSOT(`text-fg-disabled` + `aria-disabled=true` + 鍵盤導覽 skip + 不觸發 onSelect)。
|
|
218
218
|
- **Loading(async submenu / async items)**:DropdownMenu primitive 不獨立 own loading prop。async submenu 場景應由 consumer 在 `<DropdownMenuSub>` 內條件性渲 `<DropdownMenuItem disabled>` + spinner label(如「載入中...」);或 fetch options 前先 disable 整個 menu trigger。完整 loading body 替換 pattern 屬 SelectMenu scope,不在 DropdownMenu scope。
|
|
219
219
|
- **Empty(no items / async fetch result empty)**:consumer 應條件性渲 `<DropdownMenuItem disabled>` 顯示「無可用動作」字樣;不渲空 menu。
|
|
220
|
-
- **Dark mode / density**:走 MenuItem + Popover SSOT 自動 adapt;density
|
|
220
|
+
- **Dark mode / density**:走 MenuItem + Popover SSOT 自動 adapt;density 繼承 page(不鎖;DropdownMenu item 高度 = field-height-{size} 隨 page density 變,鎖 md 會釘死 md-scale 對不上 lg 觸發點),SSOT 見 `tokens/density/density.spec.md`。
|
|
221
221
|
|
|
222
222
|
---
|
|
223
223
|
|
|
@@ -15,6 +15,8 @@ import { useRowSize } from '@/design-system/patterns/element-anatomy/item-anatom
|
|
|
15
15
|
* desc → action = mt-6(24px)
|
|
16
16
|
* title → desc = `var(--item-gap-label-desc-reading-lg)`(reading-lg tier token,item-anatomy SSOT;bare `--item-gap-label-desc` 已 retire)
|
|
17
17
|
*
|
|
18
|
+
* description 字級隨 RowSizeContext 變(sm/md → text-body 14px,lg → text-body-lg 16px)。
|
|
19
|
+
*
|
|
18
20
|
* Outer padding 由 consumer 容器決定(py-12 / py-6 / py-16 等)。
|
|
19
21
|
*/
|
|
20
22
|
|
|
@@ -58,7 +58,9 @@ components/
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
## Mode —
|
|
61
|
+
## Mode — 表單三態 (display 見下方 Display 段)
|
|
62
|
+
|
|
63
|
+
下表涵蓋 Form-context 三態(edit / readonly / disabled);完整 `FieldMode` 為四值(`'edit' | 'display' | 'readonly' | 'disabled'`),`display` 模式於下方 `## Display` 段記載。
|
|
62
64
|
|
|
63
65
|
| Mode | 底色 | 邊框 | 文字色 | 用途 |
|
|
64
66
|
|------|------|------|--------|------|
|
|
@@ -226,14 +228,15 @@ Field 內的具體套用:
|
|
|
226
228
|
|
|
227
229
|
## 下拉箭頭(Select / Combobox)與類型身份 indicator
|
|
228
230
|
|
|
229
|
-
Select / Combobox 的 ChevronDown、DatePicker 的 Calendar、TimePicker 的 Clock = **類型身份 indicator**(「這是什麼欄位」)
|
|
231
|
+
Select / Combobox 的 ChevronDown、DatePicker 的 Calendar、TimePicker 的 Clock = **類型身份 indicator**(「這是什麼欄位」)。**edit 顯示(可互動)/ readonly 不顯示 / disabled 顯示(灰示)**(2026-06-26 user 拍板;readonly = 純值顯示、不可開下拉 → 箭頭會誤導故不顯;disabled 保留對齊原生 `<select disabled>` 灰示箭頭 + MUI #19833 / Carbon read-only「keep icon signifiers de-emphasized」/ Accordion M24 precedent):
|
|
230
232
|
|
|
231
|
-
- edit
|
|
233
|
+
- edit:`fg-muted`;**readonly:不顯示 indicator**;**disabled:`fg-disabled`**(對齊上方 Icon 色彩原則)
|
|
232
234
|
- 不可互動(`pointer-events-none`)——下拉由 select 元素本身觸發
|
|
233
235
|
- **Cell(naked variant)例外**:indicator 依 `showDisplayEndIcon`(= cell 的 isEditable)——非可編欄不顯(2026-05-10 cell canonical「indicator = editable affordance」);**可編欄的 disabled cell 顯示 + fg-disabled**(同表單邏輯)
|
|
234
236
|
- locked(readonly/disabled)wrapper 並設 `aria-disabled`(disabled 時)——styled-disabled 非原生元素需明告 AT inactive,亦使 axe 正確套用 WCAG 1.4.3 inactive-UI 豁免
|
|
235
237
|
- clearable 有值時:clear X 在左,ChevronDown 在右
|
|
236
|
-
-
|
|
238
|
+
- **右側元素(clear / chevron / calendar / clock)右緣水平內距 = `--field-px`(12px,SSOT `tokens/uiSize/uiSize.css`),edit / readonly / disabled / display 全 mode 一致**(跟 Input 一致)。**tag 模式特例**:左側 `tagPadding` 用對稱 px-calc(≈8px)貼齊 tags、會吃掉右緣,故 tag 容器(含 readonly/disabled)**必 re-assert `paddingRight: var(--field-px)`** 對齊 edit;漏接 = chevron 右緣偏移 bug(2026-06-27 修 Select:354 / Combobox ReadonlyMultiSelect)
|
|
239
|
+
- **多行(Combobox tag wrap)垂直對齊**:tags 換行、容器動態變高時,右側 chevron **鎖第一行 tag 中線**(非整體置中)——容器 `items-start` + `ItemSuffix self-start` + `style={{ height: tagHeight }}`(sm 20 / md+lg 24)。對齊 item-anatomy「suffix 永遠 `h-[1lh]` 對齊第一行」canonical;edit / readonly / disabled 全 mode 一致(2026-06-27 補 readonly/disabled wrap 漏接)
|
|
237
240
|
|
|
238
241
|
## Select 顯示模式
|
|
239
242
|
|
|
@@ -246,9 +249,9 @@ Select 支援兩種顯示模式(`display` prop):
|
|
|
246
249
|
|
|
247
250
|
`plain` 模式可搭配 `startIcon`(代表 value 的圖示,如狀態 icon;2026-05-01 由 `text` 改名 `plain`,rationale 見 `select.spec.md`)。
|
|
248
251
|
|
|
249
|
-
`tag` 模式的 edit 用 hidden select overlay(跟 Combobox 同模式),Tag 用 `pointer-events-none`,點擊穿透到 select
|
|
252
|
+
`tag` 模式的 edit 用 hidden select overlay(跟 Combobox 同模式),Tag 用 `pointer-events-none`,點擊穿透到 select。右側元素右緣 = `--field-px`(見上方「右側元素」canonical;tag 模式 readonly/disabled 必 re-assert)。
|
|
250
253
|
|
|
251
|
-
tagPadding 只在有 Tag 時才套用。Placeholder/空值狀態使用 fieldWrapper 的標準 px-
|
|
254
|
+
tagPadding 只在有 Tag 時才套用。Placeholder/空值狀態使用 fieldWrapper 的標準 `--field-px`(`px-[var(--field-px)]`)padding,確保文字與邊框有足夠間距。
|
|
252
255
|
|
|
253
256
|
---
|
|
254
257
|
|
|
@@ -57,9 +57,9 @@ export const fieldWrapperStyles = cva(
|
|
|
57
57
|
naked: '',
|
|
58
58
|
},
|
|
59
59
|
size: {
|
|
60
|
-
sm: 'text-body h-field-sm px-
|
|
61
|
-
md: 'text-body h-field-md px-
|
|
62
|
-
lg: 'text-body-lg h-field-lg px-
|
|
60
|
+
sm: 'text-body h-field-sm px-[var(--field-px)] gap-2',
|
|
61
|
+
md: 'text-body h-field-md px-[var(--field-px)] gap-2',
|
|
62
|
+
lg: 'text-body-lg h-field-lg px-[var(--field-px)] gap-2',
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
// mode x variant 交叉:visual chrome 由 compoundVariants 決定
|
|
@@ -98,7 +98,7 @@ export const fieldWrapperStyles = cva(
|
|
|
98
98
|
mode: 'display',
|
|
99
99
|
variant: 'default',
|
|
100
100
|
// 2026-05-13 Q3 Path Ⅰ(user 拍板 Path Ⅰ 全 zero chrome + codex V2 verdict + field-controls.spec.md (d)):
|
|
101
|
-
// default display = zero chrome — !px-0 !py-0 override size token 的 px-
|
|
101
|
+
// default display = zero chrome — !px-0 !py-0 override size token 的 px-[var(--field-px)],跟 Select / Combobox
|
|
102
102
|
// / DatePicker / TimePicker / LinkInput non-D-path bare-span idiom 一致(Carbon read-only / Stripe
|
|
103
103
|
// display / Notion property / Polaris readonly TextField 全 zero chrome)。
|
|
104
104
|
className: 'bg-transparent border border-transparent !px-0 !py-0',
|
|
@@ -93,10 +93,10 @@ export const UsageGuidance: Story = {
|
|
|
93
93
|
note="drag-over 只用邊框顏色(border-primary,底維持 surface 不變 bg)傳達狀態。加 scale 會讓區塊在拖放瞬間晃動、使用者滑鼠與 drop target 錯位;加 shadow 和元件 elevation 體系衝突。"
|
|
94
94
|
>
|
|
95
95
|
<div className="flex flex-col gap-2">
|
|
96
|
-
<div className="border-2 border-dashed border-primary bg-surface rounded-md
|
|
96
|
+
<div className="border-2 border-dashed border-primary bg-surface rounded-md p-[var(--layout-space-loose)] text-center text-caption text-fg-muted">
|
|
97
97
|
✓ 僅改邊框顏色(dashed primary,底維持 surface)
|
|
98
98
|
</div>
|
|
99
|
-
<div className="border-2 border-dashed border-primary bg-surface rounded-md
|
|
99
|
+
<div className="border-2 border-dashed border-primary bg-surface rounded-md p-[var(--layout-space-loose)] text-center text-caption text-fg-muted scale-105 shadow-[var(--elevation-200)]">
|
|
100
100
|
✗ scale + shadow — 視覺噪音,與 elevation 系統衝突
|
|
101
101
|
</div>
|
|
102
102
|
</div>
|
|
@@ -140,7 +140,7 @@ FileUpload 是**拖放 / 點擊上傳區塊**——可拖曳檔案進入或點
|
|
|
140
140
|
|
|
141
141
|
## A11y 預設
|
|
142
142
|
|
|
143
|
-
- `role="button"` + `tabIndex=0`(disabled 時 `-1`)
|
|
143
|
+
- `role="button"` + `tabIndex=0`(disabled 或 loading 時 `-1`)
|
|
144
144
|
- Enter / Space 鍵觸發檔案選取浮窗(模擬 click)
|
|
145
145
|
- `aria-disabled={true}` 當 disabled
|
|
146
146
|
- `<input type="file">` 以 `className="hidden"`(`display:none`)隱藏 — 移出無障礙樹、不可聚焦;互動由外層 `role="button"` wrapper 承載。Accessible name 來自 wrapper 內 Empty 的 title + description 文字(name-from-content),非 input 本身
|
|
@@ -193,7 +193,7 @@ export const PureBehaviorPrimitiveRule: Story = {
|
|
|
193
193
|
<HoverCardTrigger asChild>
|
|
194
194
|
<Button variant="tertiary" size="sm">深色 Tooltip 樣式</Button>
|
|
195
195
|
</HoverCardTrigger>
|
|
196
|
-
<HoverCardContent className="bg-tooltip text-
|
|
196
|
+
<HoverCardContent className="bg-tooltip text-on-emphasis rounded-md px-3 py-2" data-theme="dark">
|
|
197
197
|
<div className="w-48 text-caption">OverflowIndicator 風格:深底 + compact</div>
|
|
198
198
|
</HoverCardContent>
|
|
199
199
|
</HoverCard>
|
|
@@ -51,9 +51,9 @@ interface SizeSpec {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
|
|
54
|
-
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-
|
|
55
|
-
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-
|
|
56
|
-
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-
|
|
54
|
+
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, actionHover: 18 },
|
|
55
|
+
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, actionHover: 18 },
|
|
56
|
+
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body-lg', font: '16px', icon: 20, actionHover: 22 },
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const MODE_DESC: Record<ModeKey, string> = {
|
|
@@ -92,9 +92,9 @@ interface SizeSpec {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
|
|
95
|
-
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-
|
|
96
|
-
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-
|
|
97
|
-
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-
|
|
95
|
+
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, actionHover: 18 },
|
|
96
|
+
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, actionHover: 18 },
|
|
97
|
+
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body-lg', font: '16px', icon: 20, actionHover: 22 },
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -448,7 +448,7 @@ export const VariantIconMap: Story = {
|
|
|
448
448
|
<H3>Subtle 樣式視覺</H3>
|
|
449
449
|
<Desc>
|
|
450
450
|
Notice 本身不設 bg / border。此處 shell 為 Alert consumer 提供的 subtle container(
|
|
451
|
-
<code className="font-mono text-footnote mx-1">bg-*-subtle + border {`{hue}-
|
|
451
|
+
<code className="font-mono text-footnote mx-1">bg-*-subtle + border {`{hue}-text`}</code>
|
|
452
452
|
),展示 icon 色彩在 subtle surface 上的對比。Solid(dark bg + 白字 icon)對照請查
|
|
453
453
|
<code className="font-mono text-footnote mx-1">alert.anatomy.stories.tsx</code>
|
|
454
454
|
。
|
|
@@ -45,11 +45,12 @@ const TOKEN_MAP: Record<ModeKey, Record<StateKey, ColorSpec>> = {
|
|
|
45
45
|
disabled: { bg: 'transparent', text: '--fg-disabled', border: 'transparent' },
|
|
46
46
|
},
|
|
47
47
|
readonly: {
|
|
48
|
+
// readonly cva = `bg-readonly border border-transparent`(field-wrapper.tsx:109)—— 鎖定,無 hover / focus / error bg 轉換
|
|
48
49
|
default: { bg: '--bg-readonly', text: '--foreground', border: 'transparent' },
|
|
49
|
-
hover: { bg: '--bg-
|
|
50
|
-
focus: { bg: '--bg-
|
|
51
|
-
error: { bg: '--bg-
|
|
52
|
-
disabled: { bg: '--bg-
|
|
50
|
+
hover: { bg: '--bg-readonly', text: '--foreground', border: 'transparent' },
|
|
51
|
+
focus: { bg: '--bg-readonly', text: '--foreground', border: 'transparent' },
|
|
52
|
+
error: { bg: '--bg-readonly', text: '--foreground', border: 'transparent' },
|
|
53
|
+
disabled: { bg: '--bg-readonly', text: '--foreground', border: 'transparent' },
|
|
53
54
|
},
|
|
54
55
|
disabled: {
|
|
55
56
|
default: { bg: '--bg-disabled', text: '--fg-disabled', border: 'transparent' },
|
|
@@ -70,9 +71,9 @@ interface SizeSpec {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const SIZE_SPECS: Record<SizeKey, SizeSpec> = {
|
|
73
|
-
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-
|
|
74
|
-
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-
|
|
75
|
-
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-
|
|
74
|
+
sm: { heightToken: 'h-field-sm', height: '28px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, actionHover: 18 },
|
|
75
|
+
md: { heightToken: 'h-field-md', height: '32px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body', font: '14px', icon: 16, actionHover: 18 },
|
|
76
|
+
lg: { heightToken: 'h-field-lg', height: '36px', pxToken: 'px-[var(--field-px)]', px: 12, gapToken: 'gap-2', gap: 8, fontToken: 'text-body-lg', font: '16px', icon: 20, actionHover: 22 },
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -124,7 +124,7 @@ col.accessor('price', {
|
|
|
124
124
|
|
|
125
125
|
## A11y 預設
|
|
126
126
|
|
|
127
|
-
**ARIA / Pattern**:ARIA / keyboard 機制**僅 edit mode 適用**——edit mode 渲染 native `<input>` element(預設 a11y),且把 `...props`(含 `aria-label` / `aria-describedby` 等)轉發到該 input
|
|
127
|
+
**ARIA / Pattern**:ARIA / keyboard 機制**僅 edit mode 適用**——edit mode 渲染 native `<input>` element(預設 a11y),且把 `...props`(含 `aria-label` / `aria-describedby` 等)轉發到該 input;該 native input 自身消費 `fieldCtx` 設定 `aria-invalid` / `aria-required` / `aria-describedby` / `aria-errormessage`(非由 Field wrapper 注入,對齊 Select / Combobox trigger 自設機制)。`display` / `readonly` / `disabled` 三 mode 渲染的是無互動純展示文字(`<div>` + `<span>`,不轉發 `aria-*`),螢幕報讀僅得格式化後的靜態值(對齊 Carbon read-only / Stripe display 慣例)。
|
|
128
128
|
|
|
129
129
|
**Keyboard 行為**:
|
|
130
130
|
|
|
@@ -151,7 +151,7 @@ OverflowIndicator.displayName = 'OverflowIndicator'
|
|
|
151
151
|
// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
|
|
152
152
|
export const overflowIndicatorMeta = {
|
|
153
153
|
component: 'OverflowIndicator',
|
|
154
|
-
family:
|
|
154
|
+
family: 'self-contained', // 對齊 overflow-indicator.spec.md frontmatter family: self-contained(SSOT)
|
|
155
155
|
variants: {
|
|
156
156
|
|
|
157
157
|
},
|
|
@@ -147,7 +147,7 @@ PeoplePicker 永遠支援搜尋(內部使用 `Command` / cmdk)——因為
|
|
|
147
147
|
|
|
148
148
|
| 共享項 | SSOT owner |
|
|
149
149
|
|---|---|
|
|
150
|
-
| Avatar 左 inset 距 trigger.left = 12px 固定(不隨 size/density 漂移)| 本 spec L94 + `people-picker.tsx:362` `!px-
|
|
150
|
+
| Avatar 左 inset 距 trigger.left = 12px 固定(不隨 size/density 漂移)| 本 spec L94 + `people-picker.tsx:362` `!px-[var(--field-px)]` inject(form context)/ `!px-[var(--table-cell-px)]`(naked variant table cell)|
|
|
151
151
|
| Avatar+人名視覺 | `person-display.tsx:137-152` `PersonDisplay`(共享 renderer)|
|
|
152
152
|
| 人名按空間 ellipsis | `person-display.tsx:150` `truncate flex-1 min-w-0` |
|
|
153
153
|
| Placeholder ellipsis(inline-search active 時)| `field-wrapper.tsx:244` `bareInputStyles` 含 `truncate` |
|
|
@@ -172,7 +172,7 @@ PeoplePicker 永遠支援搜尋(內部使用 `Command` / cmdk)——因為
|
|
|
172
172
|
|
|
173
173
|
**改 PeoplePicker 視覺前必查本表,動 trigger render / placeholder / overflowShape / tagWrapperClassName 前先 cite 對應 row。** Hook `check_peoplepicker_ssot_drift.sh` 機械強制。
|
|
174
174
|
|
|
175
|
-
**Avatar 左 inset SSOT(GitHub people picker idiom)**:single trigger / multi stack 第一個 avatar 距 trigger.left = **12px 固定**(不隨 size / density 漂移)。實作:PeoplePicker form context + 有 tag → `cn(className, '!px-
|
|
175
|
+
**Avatar 左 inset SSOT(GitHub people picker idiom)**:single trigger / multi stack 第一個 avatar 距 trigger.left = **12px 固定**(不隨 size / density 漂移)。實作:PeoplePicker form context + 有 tag → `cn(className, '!px-[var(--field-px)]')` inject 12px override 到 Combobox Field wrapper,**覆蓋** Combobox `tagPadding[size]` density-dependent calc 公式(該公式在 default lg + 全 comfortable 共 4/6 組合漂離 12px,故報廢改固定值;推導史詳 git log 2026-05-13);`tagAreaPaddingLeftPx={undefined}` 不再 +8 magic。Cell context naked variant `!px-[var(--table-cell-px)]` 已是 12px 不 inject。對齊 GitHub picker / Material Autocomplete renderTags / Ant Select tagRender 共享 fixed-inset canonical(不 scale with size)。 <!-- @benchmark-unverified: see frontmatter benchmark list for canonical DS source URL -->
|
|
176
176
|
|
|
177
177
|
**判準**:
|
|
178
178
|
- **選人 → PeoplePicker**(不用 Select / Combobox 代替,失去 Avatar 視覺)
|
|
@@ -249,7 +249,7 @@ PeoplePicker 是 **composite 元件**(內部 wrap `<Select>`(single)/ `<Combobox
|
|
|
249
249
|
|
|
250
250
|
## 邊界案例
|
|
251
251
|
|
|
252
|
-
- **Disabled**:`disabled` → `resolvedMode='disabled'`(`useResolvedFieldMode`),走獨立 static-display 分支(`people-picker.tsx`「readonly / disabled」段)——渲染靜態 `<div>` 包 `MultiPersonDisplay` / `PersonDisplay` + `ItemSuffix` ChevronDown 類型身份 indicator(2026-06-
|
|
252
|
+
- **Disabled**:`disabled` → `resolvedMode='disabled'`(`useResolvedFieldMode`),走獨立 static-display 分支(`people-picker.tsx`「readonly / disabled」段)——渲染靜態 `<div>` 包 `MultiPersonDisplay` / `PersonDisplay` + `ItemSuffix` ChevronDown 類型身份 indicator(2026-06-26:disabled 保留 chevron、readonly 不顯示;naked variant 依 `showDisplayEndIcon`),**不** wrap Select / Combobox、無 dismiss X、無 inline-search input。token 走 M24 state precedence(`text-fg-disabled`,含 chevron)。
|
|
253
253
|
- **Loading(async people fetch)**:目前 PeoplePicker **未暴露 `loading` prop**(`PeoplePickerProps` 無此欄位,內部也未 forward 給 wrapped Select / Combobox)。consumer 若需 loading 態,在 fetch 完成前自行控制 `people=[]`(走 emptyText 空態)。底層 Select / Combobox / SelectMenu 各有自己的 loading 機制,但尚未經 PeoplePicker API 層轉發。
|
|
254
254
|
- **Empty(no search results)**:`emptyText` 預設「沒有符合的人員」(本 spec L72 已 codify);無 creatable mode(人員不可建立)。
|
|
255
255
|
- **Empty(no value selected)**:single mode → trigger 顯 placeholder「請選擇人員」;multi mode `value=[]` → trigger 同 placeholder(無 avatar stack 渲);詳「Trigger display SSOT canonical table」B-D 段。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.
|
|
2
2
|
// @placeholder-vocabulary-allow: 1-cycle backward-compat — `placeholder` 已加(trigger empty SSOT),`emptyPlaceholder={emptyText}` forward 仍保留讓既有 consumer 不被 silent break;Combobox line 760 `placeholder ?? emptyPlaceholder` fallback → placeholder 永遠 takes precedence。Future cycle 移除 emptyPlaceholder forward(per field-controls.spec.md 共享 contract b)。
|
|
3
|
-
// @cell-metric-escape-allow: comment describes RETIRED `tagAreaPaddingLeftPx={8}` magic — current code is surface-guarded (`surface === 'form'` only injects `!px-
|
|
3
|
+
// @cell-metric-escape-allow: comment describes RETIRED `tagAreaPaddingLeftPx={8}` magic — current code is surface-guarded (`surface === 'form'` only injects `!px-[var(--field-px)]`; table-cell context untouched, lets naked `!px-[var(--table-cell-px)]` SSOT take over). Hook regex grep'd the comment word, not the live code path. Per (a) fix 2026-05-13 user-approved Path a.
|
|
4
4
|
import * as React from 'react'
|
|
5
5
|
import { ChevronDown } from 'lucide-react'
|
|
6
6
|
import { cn } from '@/lib/utils'
|
|
@@ -189,8 +189,8 @@ const PeoplePicker = React.forwardRef<HTMLDivElement, PeoplePickerProps>(functio
|
|
|
189
189
|
? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />
|
|
190
190
|
: <PersonDisplay value={value as PersonValue} size={size} />}
|
|
191
191
|
</span>
|
|
192
|
-
{/* 2026-06-
|
|
193
|
-
{(resolvedVariant === 'naked' ? showDisplayEndIcon :
|
|
192
|
+
{/* 2026-06-26 類型身份 indicator:edit 顯示 / readonly 不顯示 / disabled 保留(fg-disabled,對齊原生 <select disabled>);naked cell 依 showDisplayEndIcon */}
|
|
193
|
+
{(resolvedVariant === 'naked' ? showDisplayEndIcon : resolvedMode === 'disabled') && (
|
|
194
194
|
<ItemSuffix className="pointer-events-none">
|
|
195
195
|
<ChevronDown size={ICON_SIZE[size as 'sm' | 'md' | 'lg']} className={cn('shrink-0', resolvedMode === 'disabled' ? 'text-fg-disabled' : 'text-fg-muted')} aria-hidden />
|
|
196
196
|
</ItemSuffix>
|
|
@@ -363,12 +363,12 @@ const PeoplePicker = React.forwardRef<HTMLDivElement, PeoplePickerProps>(functio
|
|
|
363
363
|
// 撤掉 `tagAreaPaddingLeftPx={8}` magic — Combobox `tagPadding[size]` 是 density-dependent
|
|
364
364
|
// calc 公式(`(field-height - icon-size) / 2`),只在 md size + default density 才 = 4px;
|
|
365
365
|
// 其他 size/density 漂 6px / 8px → 4+8=12 spec 公式不成立。
|
|
366
|
-
// (a) fix:form context + 有 tag → 改 inject `!px-
|
|
366
|
+
// (a) fix:form context + 有 tag → 改 inject `!px-[var(--field-px)]`(固定 12px)直接 override `tagPadding[size]`,
|
|
367
367
|
// 達成 GitHub PeoplePicker fixed 12px inset(對齊 cell context 同 13px from cell.left 含 1px border)。
|
|
368
|
-
// - form + 有 tag → `!px-
|
|
368
|
+
// - form + 有 tag → `!px-[var(--field-px)]`(12px 固定 inset)+ tagAreaPaddingLeftPx undefined → field.padL=12 ✓
|
|
369
369
|
// - table-cell + 有 tag → naked variant `!px-[var(--table-cell-px)]` 已是 12px,不 inject ✓
|
|
370
370
|
// - isEmpty → 不 inject,走 Combobox 預設文字 inset(`tagPadding[size]` 公式自然 vertical center)
|
|
371
|
-
className={cn(className, !isEmpty && surface === 'form' && '!px-
|
|
371
|
+
className={cn(className, !isEmpty && surface === 'form' && '!px-[var(--field-px)]')}
|
|
372
372
|
aria-label={ariaLabel}
|
|
373
373
|
// 2026-05-15 Bug 1 fix(Claude+Codex Step 5 比稿 consensus,user verbatim「就 A」):per-length 動態
|
|
374
374
|
// wrapper class — length=1 降階單人視覺需要 width constraint chain(`flex-1 min-w-0 overflow-hidden`),
|
|
@@ -253,7 +253,7 @@ export const VisualAlignmentRule: Story = {
|
|
|
253
253
|
<div>
|
|
254
254
|
<Rule
|
|
255
255
|
title="Popover 與 Dialog 共用 overlay-surface 視覺語言"
|
|
256
|
-
note="bg-surface-raised / border-border / rounded-lg / elevation-200 完全一致。Header / Body / Footer 內 padding 來自 overlay-surface pattern 主檔(px-loose py-tight)。差異:(1) Popover 是 non-modal 無 overlay 遮罩,(2) Popover 鎖 layout-space=md(header 精簡)
|
|
256
|
+
note="bg-surface-raised / border-border / rounded-lg / elevation-200 完全一致。Header / Body / Footer 內 padding 來自 overlay-surface pattern 主檔(px-loose py-tight)。差異:(1) Popover 是 non-modal 無 overlay 遮罩,(2) Popover 鎖 layout-space=md(header 精簡);Dialog 不鎖 layout-space,全繼承 page(md page → 16 / lg page → 24)。兩者 ui-size 都繼承 page"
|
|
257
257
|
>
|
|
258
258
|
<Popover>
|
|
259
259
|
<PopoverTrigger asChild>
|
|
@@ -285,10 +285,10 @@ export const VisualAlignmentRule: Story = {
|
|
|
285
285
|
</Rule>
|
|
286
286
|
|
|
287
287
|
<Rule
|
|
288
|
-
title="為什麼
|
|
289
|
-
note="
|
|
288
|
+
title="為什麼 layout-space 鎖 md"
|
|
289
|
+
note="Popover 只鎖 layout-space=md(header / footer padding 永遠精簡),內部控件的 ui-size 繼承 page(對齊 trigger)。頁面 layout-space 大(lg)時,Popover 若 header / footer 跟著放大會失去「輕量補充 UI」的定位 — 使用者會誤以為是主 modal。鎖 layout-space=md 讓 Popover chrome 永遠比主頁面緊湊,保持「這是補充不是主流程」的視覺暗示"
|
|
290
290
|
>
|
|
291
|
-
<Label>頁面
|
|
291
|
+
<Label>頁面 layout-space = lg,Popover header / footer 仍是 md(內部控件 ui-size 繼承 page)— 輕量感不被放大破壞</Label>
|
|
292
292
|
</Rule>
|
|
293
293
|
</div>
|
|
294
294
|
),
|
|
@@ -498,7 +498,7 @@ export const Accessibility = {
|
|
|
498
498
|
render: () => (
|
|
499
499
|
<div className="max-w-3xl text-body text-fg-secondary">
|
|
500
500
|
<h3 className="text-h5 text-foreground mb-2">無障礙設計</h3>
|
|
501
|
-
<p className="whitespace-pre-line">{"詳 `profile-card.spec.md` 「A11y 預設」段。摘要:\n\n> 命名對齊 DS 設計準則(2026-05-18,per # 命名與語言一致性 )。本節原標題「無障礙」,改「A11y 預設」與其他 spec 一致。\n\n- Trigger 整合 :Avatar 作為 HoverCard trigger 時, onFocus / onBlur 與 mouseenter/leave 同時觸發由 Radix HoverCard 管理——鍵盤使用者 Tab 到 avatar 可自動顯示 card,Escape 關閉\n- Focus 順序 :Radix HoverCard 預設不抓取 focus 進入浮層——HoverCardContent 每次 render 把浮層內所有 tabIndex>=0 節點 set tabindex=-1(@radix-ui/react-hover-card
|
|
501
|
+
<p className="whitespace-pre-line">{"詳 `profile-card.spec.md` 「A11y 預設」段。摘要:\n\n> 命名對齊 DS 設計準則(2026-05-18,per # 命名與語言一致性 )。本節原標題「無障礙」,改「A11y 預設」與其他 spec 一致。\n\n- Trigger 整合 :Avatar 作為 HoverCard trigger 時, onFocus / onBlur 與 mouseenter/leave 同時觸發由 Radix HoverCard 管理——鍵盤使用者 Tab 到 avatar 可自動顯示 card,Escape 關閉\n- Focus 順序 :Radix HoverCard 預設不抓取 focus 進入浮層——HoverCardContent 每次 render 把浮層內所有 tabIndex>=0 節點 set tabindex=-1(@radix-ui/react-hover-card 的 getTabbableNodes + setAttribute('tabindex','-1');dist 行號隨安裝版本浮動,不 pin),刻意把 action / view more 移出 tab order。hover-integrated 路徑下鍵盤使用者 Tab 只停在 trigger(Avatar),無法 Tab 聚焦浮層內 action / view more(與 Popover 的 focus trap 截然不同)。需要鍵盤可達 action 改用 Popover / Dialog\n- Live region 語意 :ProfileCard 是展示內容,非 announcement,不套 aria-live \n- DL 語意 :Info Fields 使用 DescriptionList(<dl>/<dt>/<dd>),screen reader 讀「term X, description Y」(詳 description-list.spec.md「無障礙」段)\n- CTA button aria-label :icon-only action button 必帶 aria-label (「傳訊息給 {name}」),不只 icon 視覺\n- 色彩對比 :Status badge / Avatar status 圓點均過 WCAG AA,不依賴單一色彩載體(搭配文字標籤)"}</p>
|
|
502
502
|
</div>
|
|
503
503
|
),
|
|
504
504
|
}
|
|
@@ -314,7 +314,7 @@ export const nameCardMeta = {
|
|
|
314
314
|
sizes: {
|
|
315
315
|
|
|
316
316
|
},
|
|
317
|
-
states: [
|
|
317
|
+
states: [], // 唯讀 person-info template,無自身互動 state(hover/active/focus/disabled 屬子 Button/Avatar)
|
|
318
318
|
tokens: {
|
|
319
319
|
bg: ['bg-muted'],
|
|
320
320
|
fg: ['text-foreground'],
|
|
@@ -72,7 +72,7 @@ ProgressBar 是**量化 linear 進度** primitive——consumer 必須能回答
|
|
|
72
72
|
| **視覺** | 水平直線從左往右填充 | 圓弧從 12 點順時針填充 / indeterminate 整體旋轉 |
|
|
73
73
|
| **典型情境** | 批次處理 / 表單 wizard / 頁面級大區塊 / quota(檔案上傳 → 走 FileItem) | Button loading / Field loading / cell 局部 / inline 小 % |
|
|
74
74
|
| **可量化時機** | **必須能量化**(100% 結束可預期) | determinate 能量化 / indeterminate 不需要 |
|
|
75
|
-
| **a11y** | `role="progressbar"` + `aria-valuenow` | `role="progressbar"`(有 value) / `role="status"`(無 value) |
|
|
75
|
+
| **a11y** | `role="progressbar"` + `aria-valuenow` | `role="progressbar"`(有 value) / `role="status"`(無 value 且有 label/aria-label;否則 aria-hidden 交父層 aria-busy) |
|
|
76
76
|
| **終止條件** | 到 100% 或 status 變 success / error | 任務完成(卸載或 aria-busy 移除) |
|
|
77
77
|
| **typical 時長** | 秒級到分鐘級(值得顯示比例) | 秒級(短暫等待) / 秒到分鐘(determinate) |
|
|
78
78
|
|
|
@@ -124,7 +124,7 @@ const Tab = ({ active, onClick, children }: { active: boolean; onClick: () => vo
|
|
|
124
124
|
type="button"
|
|
125
125
|
onClick={onClick}
|
|
126
126
|
className={`px-2.5 py-1 text-[12px] font-mono rounded-md cursor-pointer transition-colors ${
|
|
127
|
-
active ? 'bg-primary text-
|
|
127
|
+
active ? 'bg-primary text-on-emphasis font-semibold' : 'bg-neutral-hover text-fg-secondary hover:bg-neutral-active'
|
|
128
128
|
}`}
|
|
129
129
|
>
|
|
130
130
|
{children}
|
|
@@ -237,7 +237,7 @@ const InspectorInner = () => {
|
|
|
237
237
|
<PropRow label="aria-valuemax">{readOnly || disabled ? '—' : '5'}</PropRow>
|
|
238
238
|
<PropRow label="aria-valuetext">{readOnly || disabled ? '—' : `${value} of 5 stars`}</PropRow>
|
|
239
239
|
<PropRow label="aria-label">{readOnly ? '必填' : 'Field 內免填 · standalone 必填'}</PropRow>
|
|
240
|
-
<PropRow label="aria-labelledby">
|
|
240
|
+
<PropRow label="aria-labelledby">Field 內自動指向 FieldLabel(所有模式,含 readonly/disabled)</PropRow>
|
|
241
241
|
</div>
|
|
242
242
|
</div>
|
|
243
243
|
</div>
|
|
@@ -170,7 +170,7 @@ Star icon 渲染時明確設 `stroke="none"`(Lucide Star 預設 `stroke="current
|
|
|
170
170
|
## A11y 預設
|
|
171
171
|
|
|
172
172
|
- **interactive**:`role="slider"` + `aria-valuenow={value}` + `aria-valuemin={0}` + `aria-valuemax={max}` + `aria-valuetext={`{value} of {max} stars`}` + `tabIndex={0}`,鍵盤 Arrow Left/Right/Up/Down ± step(precision=half 時 step=0.5,否則 step=1);Home = 0;End = max(完整 WAI-ARIA slider keyboard pattern)
|
|
173
|
-
- **readOnly**:`role="img"` + `aria-label
|
|
173
|
+
- **readOnly**:`role="img"` + accessible name。`Field` 內由 `aria-labelledby` 自動指向 `FieldLabel`(免填 `aria-label`);standalone(無 Field)時 `aria-label` 必填,例:`aria-label="平均評分 4.7 星,共 5 星"`。無 tabIndex
|
|
174
174
|
- **disabled**:`aria-disabled="true"` + `pointer-events-none`
|
|
175
175
|
- **單顆星** `aria-hidden`:內部點擊目標是 `<span role="presentation" aria-hidden>`(非 interactive element,避免與外層 `role="slider"` 形成 axe nested-interactive 違規,2026-04-25 修正;含 half-precision 的兩個 hover zone)都不干擾螢幕閱讀器,父層 role 獨自表達語意
|
|
176
176
|
|
|
@@ -17,7 +17,7 @@ import { useFieldContext, useResolvedFieldSize, useResolvedFieldDisabled, useRes
|
|
|
17
17
|
* ── 視覺 ──
|
|
18
18
|
* 填色用 `var(--warning)`(yellow-6,世界級黃星 convention;與 warning 語意共用色相
|
|
19
19
|
* 但語境不同,評分 = UX convention color 非 status)。
|
|
20
|
-
* 空色用 `var(--
|
|
20
|
+
* 空色用 `var(--divider)`(neutral-4 借 divider semantic alias;灰色;與 disabled/empty 同級)。
|
|
21
21
|
*
|
|
22
22
|
* ── 互動 ──
|
|
23
23
|
* interactive(預設):hover 預覽、click 設值、keyboard Left/Right 改值
|