@qijenchen/design-system 0.1.0-beta.74 → 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/BulkActionBar/bulk-action-bar.d.ts.map +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.js +1 -1
- package/dist/components/BulkActionBar/bulk-action-bar.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/DataTable/data-table.d.ts +27 -6
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js +57 -34
- package/dist/components/DataTable/data-table.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/hooks/check_story_invariants.sh +26 -0
- package/ds-canonical/hooks/tests/test_check_story_invariants.sh +30 -0
- 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/Accordion/accordion.principles.stories.tsx +3 -3
- package/src/components/Alert/alert.anatomy.stories.tsx +4 -4
- package/src/components/Alert/alert.principles.stories.tsx +5 -5
- package/src/components/AppShell/app-shell.principles.stories.tsx +6 -6
- package/src/components/AppShell/app-shell.spec.md +4 -4
- package/src/components/AppShell/app-shell.tsx +2 -2
- package/src/components/AspectRatio/aspect-ratio.principles.stories.tsx +1 -1
- package/src/components/Avatar/avatar.principles.stories.tsx +3 -3
- package/src/components/Avatar/avatar.tsx +1 -1
- package/src/components/Badge/badge.principles.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.principles.stories.tsx +3 -3
- package/src/components/Breadcrumb/breadcrumb.spec.md +11 -1
- package/src/components/BulkActionBar/bulk-action-bar.anatomy.stories.tsx +1 -1
- package/src/components/BulkActionBar/bulk-action-bar.principles.stories.tsx +3 -3
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +4 -2
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +2 -2
- package/src/components/BulkActionBar/bulk-action-bar.tsx +3 -2
- package/src/components/Button/button.principles.stories.tsx +3 -3
- package/src/components/Button/button.tsx +0 -10
- package/src/components/Calendar/calendar.anatomy.stories.tsx +1 -1
- package/src/components/Calendar/calendar.principles.stories.tsx +3 -3
- package/src/components/Carousel/carousel.principles.stories.tsx +2 -2
- package/src/components/Chart/chart.principles.stories.tsx +4 -4
- package/src/components/Chart/chart.tsx +1 -1
- package/src/components/Checkbox/checkbox.principles.stories.tsx +2 -2
- package/src/components/Checkbox/checkbox.tsx +1 -1
- package/src/components/Chip/chip.principles.stories.tsx +3 -3
- package/src/components/Coachmark/coachmark.anatomy.stories.tsx +1 -1
- package/src/components/Coachmark/coachmark.principles.stories.tsx +3 -3
- package/src/components/Coachmark/coachmark.spec.md +2 -2
- package/src/components/Combobox/combobox.anatomy.stories.tsx +14 -14
- package/src/components/Combobox/combobox.principles.stories.tsx +6 -6
- 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.principles.stories.tsx +7 -7
- 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.principles.stories.tsx +3 -3
- package/src/components/DataTable/data-table.spec.md +25 -17
- package/src/components/DataTable/data-table.stories.tsx +29 -25
- package/src/components/DataTable/data-table.tsx +92 -44
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
- package/src/components/DateGrid/date-grid.principles.stories.tsx +4 -4
- 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.principles.stories.tsx +5 -5
- 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.principles.stories.tsx +5 -5
- package/src/components/DescriptionList/description-list.tsx +1 -1
- package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
- package/src/components/Dialog/dialog.principles.stories.tsx +4 -4
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +1 -1
- package/src/components/DropdownMenu/dropdown-menu.principles.stories.tsx +5 -5
- package/src/components/DropdownMenu/dropdown-menu.spec.md +1 -1
- package/src/components/Empty/empty.principles.stories.tsx +2 -2
- 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/Field/field.principles.stories.tsx +4 -4
- package/src/components/FileItem/file-item.principles.stories.tsx +6 -5
- package/src/components/FileUpload/file-upload.principles.stories.tsx +6 -6
- package/src/components/FileUpload/file-upload.spec.md +1 -1
- package/src/components/FileViewer/file-viewer.principles.stories.tsx +5 -5
- package/src/components/HoverCard/hover-card.principles.stories.tsx +6 -6
- package/src/components/Input/input.anatomy.stories.tsx +3 -3
- package/src/components/Input/input.principles.stories.tsx +4 -4
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +3 -3
- package/src/components/LinkInput/link-input.principles.stories.tsx +5 -5
- package/src/components/Menu/menu-item.principles.stories.tsx +7 -7
- package/src/components/Notice/notice.anatomy.stories.tsx +1 -1
- package/src/components/Notice/notice.principles.stories.tsx +7 -7
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +8 -7
- package/src/components/NumberInput/number-input.principles.stories.tsx +4 -4
- package/src/components/NumberInput/number-input.spec.md +1 -1
- package/src/components/OverflowIndicator/overflow-indicator.principles.stories.tsx +5 -5
- package/src/components/OverflowIndicator/overflow-indicator.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.principles.stories.tsx +3 -3
- 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 +5 -5
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.principles.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.tsx +1 -1
- package/src/components/ProgressBar/progress-bar.principles.stories.tsx +2 -2
- package/src/components/ProgressBar/progress-bar.spec.md +1 -1
- package/src/components/RadioGroup/radio-group.principles.stories.tsx +2 -2
- package/src/components/Rating/rating.anatomy.stories.tsx +2 -2
- package/src/components/Rating/rating.principles.stories.tsx +3 -3
- package/src/components/Rating/rating.spec.md +1 -1
- package/src/components/Rating/rating.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.principles.stories.tsx +4 -4
- package/src/components/Select/select.anatomy.stories.tsx +18 -18
- package/src/components/Select/select.principles.stories.tsx +5 -5
- 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/SelectMenu/select-menu.principles.stories.tsx +8 -8
- package/src/components/SelectionControl/selection-item.principles.stories.tsx +7 -7
- package/src/components/Separator/separator.principles.stories.tsx +4 -4
- package/src/components/Sheet/sheet.principles.stories.tsx +2 -2
- package/src/components/Sidebar/sidebar.principles.stories.tsx +4 -4
- package/src/components/Sidebar/sidebar.spec.md +2 -2
- package/src/components/Skeleton/skeleton.principles.stories.tsx +5 -5
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.principles.stories.tsx +3 -3
- package/src/components/Steps/steps.principles.stories.tsx +4 -4
- package/src/components/Steps/steps.spec.md +2 -2
- package/src/components/Switch/switch.principles.stories.tsx +1 -1
- package/src/components/Tabs/tabs.principles.stories.tsx +3 -3
- package/src/components/Tabs/tabs.spec.md +1 -1
- package/src/components/Tag/tag.principles.stories.tsx +3 -3
- package/src/components/Textarea/textarea.principles.stories.tsx +2 -2
- package/src/components/Textarea/textarea.tsx +3 -3
- package/src/components/TimePicker/time-picker.principles.stories.tsx +5 -5
- package/src/components/TimePicker/time-picker.spec.md +1 -1
- package/src/components/TimePicker/time-picker.tsx +11 -12
- package/src/components/Toast/toast.principles.stories.tsx +2 -2
- package/src/components/Tooltip/tooltip.principles.stories.tsx +3 -3
- package/src/components/TreeView/tree-view.principles.stories.tsx +5 -5
- package/src/components/TreeView/tree-view.stories.tsx +1 -1
- 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/color.spec.md +2 -0
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-wrapper.js","sources":["../../../src/components/Field/field-wrapper.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 { cva } from 'class-variance-authority'\n\n// ── Field Wrapper Styles ────────────────────────────────────────────────────\n// 所有 Field 元件共用的 input wrapper 樣式。\n//\n// 4 種模式(2026-05-05 expand):\n// edit — bg-surface, border, hover/focus 回饋(可編輯 input)\n// display — 純展示(無 input chrome、無 affordance);語意「read-only 內容,展示給人看」。\n// 對齊 Carbon read-only / PatternFly inline-edit hidden-input。\n// readonly — bg-readonly(neutral-2), 無邊框, 文字正常色(input chrome 但鎖定;token 獨立於 disabled)\n// disabled — bg-disabled(neutral-2), 無邊框, 文字灰化\n//\n// 2 種視覺外殼(variant):\n// default — 完整 chrome(form input 場景)\n// bare — 透明 chrome(cell-as-input substrate / Toolbar inline editing)\n//\n// 高度:固定 h = field-height token(rem),與 Button 共用同一組 token。\n\nexport const fieldWrapperStyles = cva(\n [\n // K10 fix(2026-05-04):`group/field` 讓 inner placeholder/text 可透過 `group-data-[field-mode=...]/field:` 變體\n // 各 Field 元件 wrapper 同時加 `data-field-mode={resolvedMode}` 屬性,bareInputStyles 即可\n // 依 mode 切 placeholder color。User canonical:disabled 顯著性優於 muted。\n 'group/field',\n // 2026-05-15 H1 root cause fix(user #1 verbatim 拍板「照你跟codex有共識的最佳建議做」+ codex round 1 verify cite 5/5):\n // 加 `min-w-0` 於 base — Field wrapper 為 cell-as-input substrate(DataTable / Form 上下文),\n // parent grid/flex cell 限寬時 wrapper 子 flex children 需 min-w-0 才能縮 + truncate。\n // 之前 SSOT 缺 → `selectedItemRenderer` / value text / Multi tag area 在 narrow cell 無法\n // truncate-with-ellipsis(`Alexander Hamilton Zhang` 直接被 cell overflow-hidden 硬裁無 `...`\n // 甚至蓋住相鄰 cell `—` indicator,圖二 user round 2 直接抓 trigger 越界證據)。\n // 修一處全 Field family 跟動(Input/Select/Combobox/DatePicker/TimePicker/LinkInput/\n // Textarea/NumberInput/PeoplePicker)— 對齊 M17/M19/M23 一處 SSOT + data-table.spec.md:233\n // 「禁硬裁無 ellipsis」DS canonical + MUI X DataGrid / Ant Table column.ellipsis 共識。\n 'inline-flex items-center w-full min-w-0 rounded-md',\n 'text-foreground font-normal',\n 'transition-colors duration-150',\n ],\n {\n variants: {\n mode: {\n edit: '',\n display: '',\n readonly: '',\n disabled: '',\n },\n variant: {\n // default — 完整 Field wrapper chrome(bg-surface、明顯 border、hover/focus 回饋)\n default: '',\n // bare — 透明 variant,hover / focus 才出現 border。適用 Toolbar inline editing\n // (FileViewer zoom input / chart config / rich text toolbar number input 等)。\n // 世界級對照:VS Code settings / Figma toolbar number / Notion prop input。\n bare: '',\n // naked — 完全無 variant,hover/focus 也不出 border。適用 cell-as-input\n // (host cell 自管 border + focus visual,內部 input 純文字承載)。\n // 世界級對照:Airtable / Notion / Excel / Google Sheets cell editing。\n naked: '',\n },\n size: {\n sm: 'text-body h-field-sm px-3 gap-2',\n md: 'text-body h-field-md px-3 gap-2',\n lg: 'text-body-lg h-field-lg px-3 gap-2',\n },\n },\n // mode x variant 交叉:visual chrome 由 compoundVariants 決定\n //\n // Overlay trigger active state(canonical 2026-05-02):當 Field 是 Popover/DropdownMenu/\n // Combobox trigger 用 asChild,Radix 自動 set `data-state=\"open\"` on trigger root → trigger\n // 視覺維持 hover 樣式直到浮層關閉(對齊 inline-action.spec.md「狀態極簡派」)。\n compoundVariants: [\n // default variant chrome by mode\n {\n mode: 'edit',\n variant: 'default',\n className: [\n 'bg-surface border border-border',\n 'hover:border-border-hover',\n // 2026-05-06 v13.3 SSOT canonical:focus-within `!important` 強制勝過 data-state attribute\n // selector(specificity tie at 0,2,0;source order 後者勝)。\n //\n // 設計原則:**focus dominates everything**(M11 fix「focus-dominates-hover」延伸成\n // 「focus-dominates-{hover,open,error-rest}」)。Cursor 在輸入框 = user 編輯中 = 必藍。\n //\n // 對齊世界級三家共識:\n // - Material Design 3:focus → primary line color\n // - Polaris(Shopify):focus state border-focus(藍)overrides hover/open\n // - Ant Design 5:`.ant-select-focused` blue,popover open + select option close 後\n // trigger 仍 focused → blue stays(focus return canonical via Radix `onCloseAutoFocus`)\n //\n // 副作用 — Ant 風「選後藍 / 取消灰」自動達成:\n // - 選 option close popover → Radix focus return to trigger → focus-within fires → 藍\n // - 點外取消 close popover → focus 移外 → focus-within 不 fire → 灰\n 'focus-within:!border-primary focus-within:hover:!border-primary',\n 'data-[state=open]:border-border-hover',\n ],\n },\n {\n mode: 'display',\n variant: 'default',\n // 2026-05-13 Q3 Path Ⅰ(user 拍板 Path Ⅰ 全 zero chrome + codex V2 verdict + field-controls.spec.md (d)):\n // default display = zero chrome — !px-0 !py-0 override size token 的 px-3,跟 Select / Combobox\n // / DatePicker / TimePicker / LinkInput non-D-path bare-span idiom 一致(Carbon read-only / Stripe\n // display / Notion property / Polaris readonly TextField 全 zero chrome)。\n className: 'bg-transparent border border-transparent !px-0 !py-0',\n },\n {\n mode: 'readonly',\n variant: 'default',\n className: 'bg-readonly border border-transparent',\n },\n {\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // 移除 `opacity-disabled` blanket — Avatar 已 fieldCtx-aware self-dim(avatar.tsx self-managed\n // via `isDisabledInField` derivation)。Field wrapper 不再 host-control Avatar opacity。\n // Inner content(text-fg-disabled / Avatar self-opacity)走具體 disabled token per color.spec.md:729。\n mode: 'disabled',\n variant: 'default',\n className: 'bg-disabled border border-transparent cursor-not-allowed',\n },\n // bare variant chrome by mode\n {\n mode: 'edit',\n variant: 'bare',\n className: [\n 'bg-transparent border border-transparent',\n 'hover:border-border',\n // 同 default chrome v13.3 SSOT:focus-within !important 強制勝過 data-state\n 'focus-within:!border-primary focus-within:hover:!border-primary',\n 'data-[state=open]:border-border',\n ],\n },\n {\n mode: 'display',\n variant: 'bare',\n // bare + display:cell-as-input default state(無 variant,完全融入 cell;hover/focus 才有 affordance 等 user 點下去切 edit mode)\n className: 'bg-transparent border border-transparent',\n },\n {\n mode: 'readonly',\n variant: 'bare',\n className: 'bg-transparent border border-transparent',\n },\n {\n // 2026-05-13 R3.5:移除 `opacity-disabled` blanket(per Avatar self-dim canonical)\n mode: 'disabled',\n variant: 'bare',\n className: 'bg-transparent border border-transparent cursor-not-allowed',\n },\n // naked variant — cell-as-input substrate(Notion / Airtable / Excel canonical)\n //\n // ── 2026-05-06 v14:revert v12 → v9 baseline + keep v13.3 ──\n // v12 `!absolute -inset-px` autoRowHeight 不相容(Field 抽 layout flow → cell 塌 42px;\n // user production 報「Field 沒撐滿 cell, 比沒改之前還糟糕一百萬倍」)→ revert。\n //\n // v14 = v9 baseline border-based state machine + v13.3 focus !important。\n // 暫接受視覺:Field.border-l 跟 prev cell.border-r 視覺 2px 雙線(待另案研究 seamless\n // 方案,約束:SSOT 留 Field state machine + ring 顏色自動跟 border state 同步)。\n // Phase 9 Issue 5 fix(2026-05-10 user 撞 + codex 重比稿 verdict ADOPT):\n // Field naked variant 之前全寫 `!gap-0` strip Field family slot gap → indicator(chevron /\n // calendar / clock)緊連 value 沒間距。違反 item-anatomy slot SSOT。\n // Codex verdict:「naked 把 chrome stripping 跟 slot anatomy stripping 混在一起。\n // chrome stripping 合理(去 padding / border / rounded);slot anatomy stripping 不合理\n // (prefix / content / suffix gap 仍是 item anatomy slot canonical `gap-2`)」\n // Fix:移除 `!gap-0`,讓 Field family base `gap-2`(field-wrapper.tsx:50)透出來。\n // Cite:item-anatomy.spec.md L46-50 / L113-122 partial consumer canonical;\n // field-controls.spec.md L22 Field Controls 視覺對齊 Family 1 Menu item layout。\n // 特殊 stack / multi-pill 場景若需 zero-gap,該 component spec 明文例外,不全域 strip。\n {\n mode: 'edit',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none !h-full',\n '!px-[var(--table-cell-px)] !py-[var(--table-cell-py)]',\n 'border border-border',\n 'hover:border-border-hover',\n // v13.3 SSOT canonical:focus-within !important(同 default + bare)\n 'focus-within:!border-primary focus-within:hover:!border-primary',\n 'data-[state=open]:border-border-hover',\n 'group-data-[row-mode=auto]/cell:!items-start',\n ],\n },\n {\n // 2026-05-12 fix v2(M32 root invariant audit):\n // Q1 root invariant?:cell-as-input display 視覺位置 = `cell.items-{X}` × `Field.height`\n // 兩變數函數;canonical = autoRow → Field intrinsic + cell.items-start\n // (text 在 cell top + cellPadding-y),fixed → Field h-field-md +\n // cell.items-center(text 在 cell vertical center)。\n // Q2 symptom?:`h-field-md` 32px 在 autoRow tall cell + cell items-start → Field 32px\n // sitting at top,inside items-center default → text center = top+15 ≠ top。\n // 即使 `group-data-[row-mode=auto]/cell:!items-start` 加進來,Field height\n // 還是 32 → text 在 Field 內 top,但 Field 自己 height ≠ 0,offset 13~32px。\n // Q3 fix layer?:root-layer fix = Field 在 autoRow context 必 `h-auto` 才能讓 text 真正\n // flush 到 cell.top + padding。前 v1 只 remove `!h-full` 是 surface-fix\n // (沒解決 h-field-md 32px persistence)。v2 真根因 fix:加 `!h-auto`\n // override h-field-md → Field intrinsic line-height → cell items-start\n // 真實 anchor text at cell.top + padding。\n // Edit mode 不動(`!h-full` 保留 — border 必滿格對齊 cell border)。\n mode: 'display',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none !px-0 !py-0 !h-auto',\n 'border border-transparent',\n ],\n },\n {\n mode: 'readonly',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none !px-0 !py-0 !h-auto',\n 'border border-transparent',\n ],\n },\n {\n // 2026-05-13 Q3 fix(per codex Q3 verdict + color.spec.md:729 逃生艙 rule):\n // 移除 `opacity-disabled` blanket — naked wrapper 只負責 substrate(透明 border + 抑制 cursor),\n // 內部 content(text / icon / avatar)各自 disabled context 處理(text-fg-disabled / Avatar opacity 等具體 token)。\n // 對齊 DataTable cell-disabled TD 加 `bg-disabled` 表達 cell-level disabled state 的 SSOT 分權。\n // **default/bare disabled variant(line 107, 135)deferred R3.5** — 仍依賴 wrapper opacity-disabled\n // for Avatar dim(Avatar 尚未 fieldCtx-aware self-dim);Avatar self-dim 實作後再連帶移除。\n mode: 'disabled',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none cursor-not-allowed !px-0 !py-0 !h-auto',\n 'border border-transparent',\n ],\n },\n ],\n defaultVariants: {\n mode: 'edit',\n variant: 'default',\n size: 'md',\n },\n }\n)\n\n// ── Bare Input Styles ───────────────────────────────────────────────────────\n\nexport const bareInputStyles = [\n // 2026-05-15 Q1 真 root cause fix(per codex Round 4 cite-based verdict):\n // 加 `truncate` — 原 bareInputStyles 含 `flex-1 min-w-0` 但無 `truncate / text-overflow` policy。\n // 當 Select `searchable && open` 切 raw `<input placeholder=...>` branch(select.tsx:178),\n // bareInputStyles 套上,placeholder 無 ellipsis → narrow cell(<160px)硬裁無 `...`(user round 3\n // 圖一證據)。加 truncate 後 `<input>` `text-overflow: ellipsis` 啟動,符合 user 「placeholder 直接被截掉沒有變...」\n // 該顯 ellipsis 的 SSOT。對齊 data-table.spec.md:233「禁硬裁無 ellipsis」+ field-controls.spec.md:286\n // 共享 contract(a)「display/readonly/disabled/edit 4 mode 共享同一 renderer」semantic 對齊。\n 'flex-1 min-w-0 truncate bg-transparent',\n 'outline-none border-none p-0',\n 'text-[inherit] font-[inherit] leading-[inherit]',\n // A3 fix(2026-05-05):`<input>` UA stylesheet 強制 `text-align: start`,阻斷 parent 的\n // `text-right`/`text-center` 繼承。顯式 `text-align: inherit` 復原(對齊 NumberCell /\n // CurrencyCell right-align canonical:column meta.align='right' → cell text-right →\n // input 跟著 right-align)。\n '[text-align:inherit]',\n 'placeholder:text-fg-muted',\n // K10 fix(2026-05-04):wrapper data-field-mode=disabled 時,placeholder/text 都切 fg-disabled\n // 依賴 fieldWrapperStyles 的 `group/field` + 各 Field 元件 wrapper 加 `data-field-mode={resolvedMode}`\n // User canonical:disabled state 顯著性優於 muted(neutral-6 > neutral-7)\n 'group-data-[field-mode=disabled]/field:placeholder:text-fg-disabled',\n 'group-data-[field-mode=disabled]/field:text-fg-disabled',\n].join(' ')\n\n// ── Naked Variant Cell Row-Mode Alignment Propagation ──────────────────────\n// SSOT canonical(M19 / 2026-05-05):cell-as-input naked variant 元件**所有內部\n// wrapper**(`<span>` 包 Avatar+name 等)必 import + apply 此 SSOT,host cell\n// `data-row-mode` 屬性自動 propagate alignment(autoRow → items-start / fixed → items-center)。\n//\n// 不 propagate 的後果:autoRow 場景下 People / Select / Combobox 內部用\n// `inline-flex items-center` hardcode → 視覺垂直置中於 wrapper 自身高度,**沒**頂對齊\n// → 跟其他純文字 cell baseline 視覺漂移。\n//\n// 世界級對照:\n// - HTML <td> default `vertical-align: baseline`(瀏覽器自動 first-baseline align)\n// - AG Grid `cellStyle` + `cellRendererSelector`,row context 共享(closed source 部分)\n// - Material X-Grid `gridClasses.cell` wrapper 不允許 cell content override alignment\n// - Notion / Airtable cell content 從 host 繼承,不 hardcode self alignment\n//\n// Hook:`check_naked_row_mode_propagation.sh`(write-time BLOCKER)\n// Audit:design-system-audit Group N M27(periodic batch verify)\nexport const nakedCellRowModeAlign = 'group-data-[row-mode=auto]/cell:items-start'\n\n// ── Cell-as-input Display Hover Ring(2026-05-05 v9 — sole remaining ring const)─\n// editable cell **display mode hover 提示**(「這 cell 可編,點 → 進 edit」affordance 信號)。\n// 對齊 Notion / Airtable hover-cell-shows-border canonical。\n//\n// **為何只剩這一個**:Field naked **edit/focus/open state ring 已下沉到 Field default state\n// machine**(border-based,2026-05-05 v9 architectural rewrite),不需 outline 平行系統;\n// 但 display mode 沒 Field state(display = 純展示無互動),hover 提示需 cell wrapper 自加。\n//\n// **2026-05-09 v15.17 revert v15.16(user 未同意 ship)— 維持 v15.13 outline+offset:-1**\n//\n// User 2026-05-09 後續 message:「設計決策的東西你應該要先問過我讓我決策吧?為什麼就直接開跑」\n// 我 commit 698ff58 ship v15.16(box-shadow inset Spec 2 不蓋 grid)= 設計決策結論未 user 同意\n// 直接 ship = workflow 違反。Revert 回 v15.13 outline+offset:-1(原 user-accepted 路徑),\n// 等 user 拍板 4-邊覆蓋路徑才 ship。\n//\n// User 並指出我視覺分析又錯:「我就是只有看到只有右邊被蓋掉,上面下面左邊都會露出 cell 的邊框」\n// 我之前錯說「right + bottom 都覆蓋」,bottom 真實 row border-b 在 cell.outer 外 1px,outline 蓋不到。\n//\n// 真實 4-邊狀態(re-verified):\n// - right: outline 199-200 = cell own border-r 199-200 → 蓋 1 條線 ✓\n// - top: outline 0-1,前一 row border-b 在 cell.outer 外 → 露 2 條線\n// - bottom: outline 38-39 在 cell 內,row border-b 在 row.outer 內 = cell.outer 外 1px → 露 2 條線\n// - left: outline 0-1,前一 cell border-r 在 cell.outer 外 → 露 2 條線\n//\n// User 想要 4 邊都覆蓋 = 等 codex unframed brief + user 拍板,本次不 ship。\n//\n// 之前 v15.13 / 14 / 16 / 17 緣由 → tsc comment 之前版本 + planning RFC\n// (跑錯方向 4 次 = 沒 1 次 verify 全面向 + 沒 user 拍板 set design)。\n//\n// Color `--border-hover` 對齊 Field default hover state token。\n//\n// **2026-05-10 Slice D Step 2 — Cell host CSS variable suppression**:\n// outline color 改用 `var(--cell-hover-outline-color, var(--border-hover))`,\n// allow DataTable cell host(spreadsheet overlay enabled 時)set `--cell-hover-outline-color: transparent`\n// 抑制 outline,讓 overlay layer 接管 hover ring paint(per RFC Contract 8 「one geometry owner, two paint owners」)。\n// Backward-compat:flag 關時 default `--border-hover`,既有行為不變。\nexport const nakedCellEditableDisplayHover = 'hover:outline hover:outline-1 hover:outline-offset-[-1px] hover:outline-[var(--cell-hover-outline-color,var(--border-hover))]'\n\n// ── Cell-as-input Edge Slot SSOT(2026-05-05 v8 — retire 平行 SSOT,改 L1 消費)───\n//\n// 前身 `nakedCellPrefixSlot` / `nakedCellSuffixSlot` 是 M1+M17 違反:平行 SSOT 跟\n// `patterns/element-anatomy` 的 `<ItemPrefix>` / `<ItemSuffix>` primitive 撞 home。\n// 已 retire — Field naked variant 內 prefix / suffix slot 直接消費 L1 primitive:\n//\n// import { ItemPrefix, ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'\n// <ItemPrefix><StartIcon /></ItemPrefix> // 對 Input.startIcon / Select.startIcon\n// <ItemSuffix>{chevron}</ItemSuffix> // 對 Combobox / DatePicker / PeoplePicker chevron\n//\n// **`h-[1lh]` 普世正確**(item-anatomy.spec.md:190-191 verbatim):\n// - 單行 wrapper items-center → slot 1lh 在 cell 高度中心 = 第一行中線(視覺 = items-center)\n// - 多行 wrapper items-start → slot 1lh 鎖頂 = 第一行中線\n// 不需 conditional `group-data-[row-mode=auto]/cell:` — 我前 v4 自加的 conditional 是過度設計。\n//\n// State ring 3 const 仍留(下方)— 是 Field naked 專屬,MenuItem / TreeView 用 bg hover 不用 outline。\n// `nakedCellRowModeAlign`(wrapper 級)仍留 — 是 cell-context row-mode → wrapper alignment 適配,正交 slot 級。\n\n// ── Empty Value Display ─────────────────────────────────────────────────────\n\nexport const EMPTY_DISPLAY = '—'\n\n/**\n * 2026-05-14 I2 fix(per field-controls.spec.md contract (e) display typography canonical):\n * Field family display path bare-span helper — `sm/md → text-body` / `lg → text-body-lg`,\n * 跨 9 元件 display 視覺尺寸統一(user 抓 LinkInput 字體跟其他 Field 不一致 = SSOT 違反)。\n * Consumer:LinkInput / Select / Combobox / DatePicker / TimePicker non-D-path bare-span 套此 class。\n */\nexport const fieldDisplayTextClass = (size: 'sm' | 'md' | 'lg'): string =>\n size === 'lg' ? 'text-body-lg' : 'text-body'\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"field-wrapper.js","sources":["../../../src/components/Field/field-wrapper.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 { cva } from 'class-variance-authority'\n\n// ── Field Wrapper Styles ────────────────────────────────────────────────────\n// 所有 Field 元件共用的 input wrapper 樣式。\n//\n// 4 種模式(2026-05-05 expand):\n// edit — bg-surface, border, hover/focus 回饋(可編輯 input)\n// display — 純展示(無 input chrome、無 affordance);語意「read-only 內容,展示給人看」。\n// 對齊 Carbon read-only / PatternFly inline-edit hidden-input。\n// readonly — bg-readonly(neutral-2), 無邊框, 文字正常色(input chrome 但鎖定;token 獨立於 disabled)\n// disabled — bg-disabled(neutral-2), 無邊框, 文字灰化\n//\n// 2 種視覺外殼(variant):\n// default — 完整 chrome(form input 場景)\n// bare — 透明 chrome(cell-as-input substrate / Toolbar inline editing)\n//\n// 高度:固定 h = field-height token(rem),與 Button 共用同一組 token。\n\nexport const fieldWrapperStyles = cva(\n [\n // K10 fix(2026-05-04):`group/field` 讓 inner placeholder/text 可透過 `group-data-[field-mode=...]/field:` 變體\n // 各 Field 元件 wrapper 同時加 `data-field-mode={resolvedMode}` 屬性,bareInputStyles 即可\n // 依 mode 切 placeholder color。User canonical:disabled 顯著性優於 muted。\n 'group/field',\n // 2026-05-15 H1 root cause fix(user #1 verbatim 拍板「照你跟codex有共識的最佳建議做」+ codex round 1 verify cite 5/5):\n // 加 `min-w-0` 於 base — Field wrapper 為 cell-as-input substrate(DataTable / Form 上下文),\n // parent grid/flex cell 限寬時 wrapper 子 flex children 需 min-w-0 才能縮 + truncate。\n // 之前 SSOT 缺 → `selectedItemRenderer` / value text / Multi tag area 在 narrow cell 無法\n // truncate-with-ellipsis(`Alexander Hamilton Zhang` 直接被 cell overflow-hidden 硬裁無 `...`\n // 甚至蓋住相鄰 cell `—` indicator,圖二 user round 2 直接抓 trigger 越界證據)。\n // 修一處全 Field family 跟動(Input/Select/Combobox/DatePicker/TimePicker/LinkInput/\n // Textarea/NumberInput/PeoplePicker)— 對齊 M17/M19/M23 一處 SSOT + data-table.spec.md:233\n // 「禁硬裁無 ellipsis」DS canonical + MUI X DataGrid / Ant Table column.ellipsis 共識。\n 'inline-flex items-center w-full min-w-0 rounded-md',\n 'text-foreground font-normal',\n 'transition-colors duration-150',\n ],\n {\n variants: {\n mode: {\n edit: '',\n display: '',\n readonly: '',\n disabled: '',\n },\n variant: {\n // default — 完整 Field wrapper chrome(bg-surface、明顯 border、hover/focus 回饋)\n default: '',\n // bare — 透明 variant,hover / focus 才出現 border。適用 Toolbar inline editing\n // (FileViewer zoom input / chart config / rich text toolbar number input 等)。\n // 世界級對照:VS Code settings / Figma toolbar number / Notion prop input。\n bare: '',\n // naked — 完全無 variant,hover/focus 也不出 border。適用 cell-as-input\n // (host cell 自管 border + focus visual,內部 input 純文字承載)。\n // 世界級對照:Airtable / Notion / Excel / Google Sheets cell editing。\n naked: '',\n },\n size: {\n sm: 'text-body h-field-sm px-[var(--field-px)] gap-2',\n md: 'text-body h-field-md px-[var(--field-px)] gap-2',\n lg: 'text-body-lg h-field-lg px-[var(--field-px)] gap-2',\n },\n },\n // mode x variant 交叉:visual chrome 由 compoundVariants 決定\n //\n // Overlay trigger active state(canonical 2026-05-02):當 Field 是 Popover/DropdownMenu/\n // Combobox trigger 用 asChild,Radix 自動 set `data-state=\"open\"` on trigger root → trigger\n // 視覺維持 hover 樣式直到浮層關閉(對齊 inline-action.spec.md「狀態極簡派」)。\n compoundVariants: [\n // default variant chrome by mode\n {\n mode: 'edit',\n variant: 'default',\n className: [\n 'bg-surface border border-border',\n 'hover:border-border-hover',\n // 2026-05-06 v13.3 SSOT canonical:focus-within `!important` 強制勝過 data-state attribute\n // selector(specificity tie at 0,2,0;source order 後者勝)。\n //\n // 設計原則:**focus dominates everything**(M11 fix「focus-dominates-hover」延伸成\n // 「focus-dominates-{hover,open,error-rest}」)。Cursor 在輸入框 = user 編輯中 = 必藍。\n //\n // 對齊世界級三家共識:\n // - Material Design 3:focus → primary line color\n // - Polaris(Shopify):focus state border-focus(藍)overrides hover/open\n // - Ant Design 5:`.ant-select-focused` blue,popover open + select option close 後\n // trigger 仍 focused → blue stays(focus return canonical via Radix `onCloseAutoFocus`)\n //\n // 副作用 — Ant 風「選後藍 / 取消灰」自動達成:\n // - 選 option close popover → Radix focus return to trigger → focus-within fires → 藍\n // - 點外取消 close popover → focus 移外 → focus-within 不 fire → 灰\n 'focus-within:!border-primary focus-within:hover:!border-primary',\n 'data-[state=open]:border-border-hover',\n ],\n },\n {\n mode: 'display',\n variant: 'default',\n // 2026-05-13 Q3 Path Ⅰ(user 拍板 Path Ⅰ 全 zero chrome + codex V2 verdict + field-controls.spec.md (d)):\n // default display = zero chrome — !px-0 !py-0 override size token 的 px-[var(--field-px)],跟 Select / Combobox\n // / DatePicker / TimePicker / LinkInput non-D-path bare-span idiom 一致(Carbon read-only / Stripe\n // display / Notion property / Polaris readonly TextField 全 zero chrome)。\n className: 'bg-transparent border border-transparent !px-0 !py-0',\n },\n {\n mode: 'readonly',\n variant: 'default',\n className: 'bg-readonly border border-transparent',\n },\n {\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // 移除 `opacity-disabled` blanket — Avatar 已 fieldCtx-aware self-dim(avatar.tsx self-managed\n // via `isDisabledInField` derivation)。Field wrapper 不再 host-control Avatar opacity。\n // Inner content(text-fg-disabled / Avatar self-opacity)走具體 disabled token per color.spec.md:729。\n mode: 'disabled',\n variant: 'default',\n className: 'bg-disabled border border-transparent cursor-not-allowed',\n },\n // bare variant chrome by mode\n {\n mode: 'edit',\n variant: 'bare',\n className: [\n 'bg-transparent border border-transparent',\n 'hover:border-border',\n // 同 default chrome v13.3 SSOT:focus-within !important 強制勝過 data-state\n 'focus-within:!border-primary focus-within:hover:!border-primary',\n 'data-[state=open]:border-border',\n ],\n },\n {\n mode: 'display',\n variant: 'bare',\n // bare + display:cell-as-input default state(無 variant,完全融入 cell;hover/focus 才有 affordance 等 user 點下去切 edit mode)\n className: 'bg-transparent border border-transparent',\n },\n {\n mode: 'readonly',\n variant: 'bare',\n className: 'bg-transparent border border-transparent',\n },\n {\n // 2026-05-13 R3.5:移除 `opacity-disabled` blanket(per Avatar self-dim canonical)\n mode: 'disabled',\n variant: 'bare',\n className: 'bg-transparent border border-transparent cursor-not-allowed',\n },\n // naked variant — cell-as-input substrate(Notion / Airtable / Excel canonical)\n //\n // ── 2026-05-06 v14:revert v12 → v9 baseline + keep v13.3 ──\n // v12 `!absolute -inset-px` autoRowHeight 不相容(Field 抽 layout flow → cell 塌 42px;\n // user production 報「Field 沒撐滿 cell, 比沒改之前還糟糕一百萬倍」)→ revert。\n //\n // v14 = v9 baseline border-based state machine + v13.3 focus !important。\n // 暫接受視覺:Field.border-l 跟 prev cell.border-r 視覺 2px 雙線(待另案研究 seamless\n // 方案,約束:SSOT 留 Field state machine + ring 顏色自動跟 border state 同步)。\n // Phase 9 Issue 5 fix(2026-05-10 user 撞 + codex 重比稿 verdict ADOPT):\n // Field naked variant 之前全寫 `!gap-0` strip Field family slot gap → indicator(chevron /\n // calendar / clock)緊連 value 沒間距。違反 item-anatomy slot SSOT。\n // Codex verdict:「naked 把 chrome stripping 跟 slot anatomy stripping 混在一起。\n // chrome stripping 合理(去 padding / border / rounded);slot anatomy stripping 不合理\n // (prefix / content / suffix gap 仍是 item anatomy slot canonical `gap-2`)」\n // Fix:移除 `!gap-0`,讓 Field family base `gap-2`(field-wrapper.tsx:50)透出來。\n // Cite:item-anatomy.spec.md L46-50 / L113-122 partial consumer canonical;\n // field-controls.spec.md L22 Field Controls 視覺對齊 Family 1 Menu item layout。\n // 特殊 stack / multi-pill 場景若需 zero-gap,該 component spec 明文例外,不全域 strip。\n {\n mode: 'edit',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none !h-full',\n '!px-[var(--table-cell-px)] !py-[var(--table-cell-py)]',\n 'border border-border',\n 'hover:border-border-hover',\n // v13.3 SSOT canonical:focus-within !important(同 default + bare)\n 'focus-within:!border-primary focus-within:hover:!border-primary',\n 'data-[state=open]:border-border-hover',\n 'group-data-[row-mode=auto]/cell:!items-start',\n ],\n },\n {\n // 2026-05-12 fix v2(M32 root invariant audit):\n // Q1 root invariant?:cell-as-input display 視覺位置 = `cell.items-{X}` × `Field.height`\n // 兩變數函數;canonical = autoRow → Field intrinsic + cell.items-start\n // (text 在 cell top + cellPadding-y),fixed → Field h-field-md +\n // cell.items-center(text 在 cell vertical center)。\n // Q2 symptom?:`h-field-md` 32px 在 autoRow tall cell + cell items-start → Field 32px\n // sitting at top,inside items-center default → text center = top+15 ≠ top。\n // 即使 `group-data-[row-mode=auto]/cell:!items-start` 加進來,Field height\n // 還是 32 → text 在 Field 內 top,但 Field 自己 height ≠ 0,offset 13~32px。\n // Q3 fix layer?:root-layer fix = Field 在 autoRow context 必 `h-auto` 才能讓 text 真正\n // flush 到 cell.top + padding。前 v1 只 remove `!h-full` 是 surface-fix\n // (沒解決 h-field-md 32px persistence)。v2 真根因 fix:加 `!h-auto`\n // override h-field-md → Field intrinsic line-height → cell items-start\n // 真實 anchor text at cell.top + padding。\n // Edit mode 不動(`!h-full` 保留 — border 必滿格對齊 cell border)。\n mode: 'display',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none !px-0 !py-0 !h-auto',\n 'border border-transparent',\n ],\n },\n {\n mode: 'readonly',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none !px-0 !py-0 !h-auto',\n 'border border-transparent',\n ],\n },\n {\n // 2026-05-13 Q3 fix(per codex Q3 verdict + color.spec.md:729 逃生艙 rule):\n // 移除 `opacity-disabled` blanket — naked wrapper 只負責 substrate(透明 border + 抑制 cursor),\n // 內部 content(text / icon / avatar)各自 disabled context 處理(text-fg-disabled / Avatar opacity 等具體 token)。\n // 對齊 DataTable cell-disabled TD 加 `bg-disabled` 表達 cell-level disabled state 的 SSOT 分權。\n // **default/bare disabled variant(line 107, 135)deferred R3.5** — 仍依賴 wrapper opacity-disabled\n // for Avatar dim(Avatar 尚未 fieldCtx-aware self-dim);Avatar self-dim 實作後再連帶移除。\n mode: 'disabled',\n variant: 'naked',\n className: [\n 'bg-transparent !rounded-none cursor-not-allowed !px-0 !py-0 !h-auto',\n 'border border-transparent',\n ],\n },\n ],\n defaultVariants: {\n mode: 'edit',\n variant: 'default',\n size: 'md',\n },\n }\n)\n\n// ── Bare Input Styles ───────────────────────────────────────────────────────\n\nexport const bareInputStyles = [\n // 2026-05-15 Q1 真 root cause fix(per codex Round 4 cite-based verdict):\n // 加 `truncate` — 原 bareInputStyles 含 `flex-1 min-w-0` 但無 `truncate / text-overflow` policy。\n // 當 Select `searchable && open` 切 raw `<input placeholder=...>` branch(select.tsx:178),\n // bareInputStyles 套上,placeholder 無 ellipsis → narrow cell(<160px)硬裁無 `...`(user round 3\n // 圖一證據)。加 truncate 後 `<input>` `text-overflow: ellipsis` 啟動,符合 user 「placeholder 直接被截掉沒有變...」\n // 該顯 ellipsis 的 SSOT。對齊 data-table.spec.md:233「禁硬裁無 ellipsis」+ field-controls.spec.md:286\n // 共享 contract(a)「display/readonly/disabled/edit 4 mode 共享同一 renderer」semantic 對齊。\n 'flex-1 min-w-0 truncate bg-transparent',\n 'outline-none border-none p-0',\n 'text-[inherit] font-[inherit] leading-[inherit]',\n // A3 fix(2026-05-05):`<input>` UA stylesheet 強制 `text-align: start`,阻斷 parent 的\n // `text-right`/`text-center` 繼承。顯式 `text-align: inherit` 復原(對齊 NumberCell /\n // CurrencyCell right-align canonical:column meta.align='right' → cell text-right →\n // input 跟著 right-align)。\n '[text-align:inherit]',\n 'placeholder:text-fg-muted',\n // K10 fix(2026-05-04):wrapper data-field-mode=disabled 時,placeholder/text 都切 fg-disabled\n // 依賴 fieldWrapperStyles 的 `group/field` + 各 Field 元件 wrapper 加 `data-field-mode={resolvedMode}`\n // User canonical:disabled state 顯著性優於 muted(neutral-6 > neutral-7)\n 'group-data-[field-mode=disabled]/field:placeholder:text-fg-disabled',\n 'group-data-[field-mode=disabled]/field:text-fg-disabled',\n].join(' ')\n\n// ── Naked Variant Cell Row-Mode Alignment Propagation ──────────────────────\n// SSOT canonical(M19 / 2026-05-05):cell-as-input naked variant 元件**所有內部\n// wrapper**(`<span>` 包 Avatar+name 等)必 import + apply 此 SSOT,host cell\n// `data-row-mode` 屬性自動 propagate alignment(autoRow → items-start / fixed → items-center)。\n//\n// 不 propagate 的後果:autoRow 場景下 People / Select / Combobox 內部用\n// `inline-flex items-center` hardcode → 視覺垂直置中於 wrapper 自身高度,**沒**頂對齊\n// → 跟其他純文字 cell baseline 視覺漂移。\n//\n// 世界級對照:\n// - HTML <td> default `vertical-align: baseline`(瀏覽器自動 first-baseline align)\n// - AG Grid `cellStyle` + `cellRendererSelector`,row context 共享(closed source 部分)\n// - Material X-Grid `gridClasses.cell` wrapper 不允許 cell content override alignment\n// - Notion / Airtable cell content 從 host 繼承,不 hardcode self alignment\n//\n// Hook:`check_naked_row_mode_propagation.sh`(write-time BLOCKER)\n// Audit:design-system-audit Group N M27(periodic batch verify)\nexport const nakedCellRowModeAlign = 'group-data-[row-mode=auto]/cell:items-start'\n\n// ── Cell-as-input Display Hover Ring(2026-05-05 v9 — sole remaining ring const)─\n// editable cell **display mode hover 提示**(「這 cell 可編,點 → 進 edit」affordance 信號)。\n// 對齊 Notion / Airtable hover-cell-shows-border canonical。\n//\n// **為何只剩這一個**:Field naked **edit/focus/open state ring 已下沉到 Field default state\n// machine**(border-based,2026-05-05 v9 architectural rewrite),不需 outline 平行系統;\n// 但 display mode 沒 Field state(display = 純展示無互動),hover 提示需 cell wrapper 自加。\n//\n// **2026-05-09 v15.17 revert v15.16(user 未同意 ship)— 維持 v15.13 outline+offset:-1**\n//\n// User 2026-05-09 後續 message:「設計決策的東西你應該要先問過我讓我決策吧?為什麼就直接開跑」\n// 我 commit 698ff58 ship v15.16(box-shadow inset Spec 2 不蓋 grid)= 設計決策結論未 user 同意\n// 直接 ship = workflow 違反。Revert 回 v15.13 outline+offset:-1(原 user-accepted 路徑),\n// 等 user 拍板 4-邊覆蓋路徑才 ship。\n//\n// User 並指出我視覺分析又錯:「我就是只有看到只有右邊被蓋掉,上面下面左邊都會露出 cell 的邊框」\n// 我之前錯說「right + bottom 都覆蓋」,bottom 真實 row border-b 在 cell.outer 外 1px,outline 蓋不到。\n//\n// 真實 4-邊狀態(re-verified):\n// - right: outline 199-200 = cell own border-r 199-200 → 蓋 1 條線 ✓\n// - top: outline 0-1,前一 row border-b 在 cell.outer 外 → 露 2 條線\n// - bottom: outline 38-39 在 cell 內,row border-b 在 row.outer 內 = cell.outer 外 1px → 露 2 條線\n// - left: outline 0-1,前一 cell border-r 在 cell.outer 外 → 露 2 條線\n//\n// User 想要 4 邊都覆蓋 = 等 codex unframed brief + user 拍板,本次不 ship。\n//\n// 之前 v15.13 / 14 / 16 / 17 緣由 → tsc comment 之前版本 + planning RFC\n// (跑錯方向 4 次 = 沒 1 次 verify 全面向 + 沒 user 拍板 set design)。\n//\n// Color `--border-hover` 對齊 Field default hover state token。\n//\n// **2026-05-10 Slice D Step 2 — Cell host CSS variable suppression**:\n// outline color 改用 `var(--cell-hover-outline-color, var(--border-hover))`,\n// allow DataTable cell host(spreadsheet overlay enabled 時)set `--cell-hover-outline-color: transparent`\n// 抑制 outline,讓 overlay layer 接管 hover ring paint(per RFC Contract 8 「one geometry owner, two paint owners」)。\n// Backward-compat:flag 關時 default `--border-hover`,既有行為不變。\nexport const nakedCellEditableDisplayHover = 'hover:outline hover:outline-1 hover:outline-offset-[-1px] hover:outline-[var(--cell-hover-outline-color,var(--border-hover))]'\n\n// ── Cell-as-input Edge Slot SSOT(2026-05-05 v8 — retire 平行 SSOT,改 L1 消費)───\n//\n// 前身 `nakedCellPrefixSlot` / `nakedCellSuffixSlot` 是 M1+M17 違反:平行 SSOT 跟\n// `patterns/element-anatomy` 的 `<ItemPrefix>` / `<ItemSuffix>` primitive 撞 home。\n// 已 retire — Field naked variant 內 prefix / suffix slot 直接消費 L1 primitive:\n//\n// import { ItemPrefix, ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'\n// <ItemPrefix><StartIcon /></ItemPrefix> // 對 Input.startIcon / Select.startIcon\n// <ItemSuffix>{chevron}</ItemSuffix> // 對 Combobox / DatePicker / PeoplePicker chevron\n//\n// **`h-[1lh]` 普世正確**(item-anatomy.spec.md:190-191 verbatim):\n// - 單行 wrapper items-center → slot 1lh 在 cell 高度中心 = 第一行中線(視覺 = items-center)\n// - 多行 wrapper items-start → slot 1lh 鎖頂 = 第一行中線\n// 不需 conditional `group-data-[row-mode=auto]/cell:` — 我前 v4 自加的 conditional 是過度設計。\n//\n// State ring 3 const 仍留(下方)— 是 Field naked 專屬,MenuItem / TreeView 用 bg hover 不用 outline。\n// `nakedCellRowModeAlign`(wrapper 級)仍留 — 是 cell-context row-mode → wrapper alignment 適配,正交 slot 級。\n\n// ── Empty Value Display ─────────────────────────────────────────────────────\n\nexport const EMPTY_DISPLAY = '—'\n\n/**\n * 2026-05-14 I2 fix(per field-controls.spec.md contract (e) display typography canonical):\n * Field family display path bare-span helper — `sm/md → text-body` / `lg → text-body-lg`,\n * 跨 9 元件 display 視覺尺寸統一(user 抓 LinkInput 字體跟其他 Field 不一致 = SSOT 違反)。\n * Consumer:LinkInput / Select / Combobox / DatePicker / TimePicker non-D-path bare-span 套此 class。\n */\nexport const fieldDisplayTextClass = (size: 'sm' | 'md' | 'lg'): string =>\n size === 'lg' ? 'text-body-lg' : 'text-body'\n"],"names":[],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ export interface OverflowIndicatorProps extends Omit<React.HTMLAttributes<HTMLSp
|
|
|
13
13
|
declare const OverflowIndicator: React.ForwardRefExoticComponent<OverflowIndicatorProps & React.RefAttributes<HTMLSpanElement>>;
|
|
14
14
|
export declare const overflowIndicatorMeta: {
|
|
15
15
|
readonly component: "OverflowIndicator";
|
|
16
|
-
readonly family:
|
|
16
|
+
readonly family: "self-contained";
|
|
17
17
|
readonly variants: {};
|
|
18
18
|
readonly sizes: {};
|
|
19
19
|
readonly states: readonly ["default", "hover", "active", "focus-visible", "disabled"];
|
|
@@ -100,8 +100,8 @@ const OverflowIndicator = React.forwardRef(
|
|
|
100
100
|
OverflowIndicator.displayName = "OverflowIndicator";
|
|
101
101
|
const overflowIndicatorMeta = {
|
|
102
102
|
component: "OverflowIndicator",
|
|
103
|
-
family:
|
|
104
|
-
//
|
|
103
|
+
family: "self-contained",
|
|
104
|
+
// 對齊 overflow-indicator.spec.md frontmatter family: self-contained(SSOT)
|
|
105
105
|
variants: {},
|
|
106
106
|
sizes: {},
|
|
107
107
|
states: ["default", "hover", "active", "focus-visible", "disabled"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overflow-indicator.js","sources":["../../../src/components/OverflowIndicator/overflow-indicator.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 Combobox / PeoplePicker 等 DS 元件 wrap 消費,end-user app 請用 wrapper 元件。\n */\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { tagVariants } from '@/design-system/components/Tag/tag'\nimport { HOVER_DELAY_PLAIN_MS, HOVER_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\n\n/**\n * OverflowIndicator — +N 觸發器 + HoverCard 顯示溢出內容\n *\n * 統一用 HoverCard(不用 Tooltip)——溢出內容可能需要互動:\n * - 人員 +N:tag dismiss + hover name card\n * - 一般 +N:穩定顯示溢出項目\n *\n * trigger 不用 Tag 元件(Tag 有內建 truncation Tooltip 會跟 HoverCard 衝突),\n * 改用 tagVariants 直接套樣式。\n */\n\nconst triggerSize: Record<string, string> = {\n sm: 'h-5 min-w-5',\n md: 'h-6 min-w-6',\n lg: 'h-6 min-w-6',\n}\n\nconst triggerText: Record<string, string> = {\n sm: 'text-[10px]',\n md: 'text-caption',\n lg: 'text-caption',\n}\n\nexport interface OverflowIndicatorProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'> {\n count: number\n shape?: 'circle' | 'tag'\n size?: 'sm' | 'md' | 'lg'\n children: React.ReactNode\n className?: string\n}\n\nfunction ShrinkWrapList({ children }: { children: React.ReactNode }) {\n const containerRef = React.useRef<HTMLDivElement | null>(null)\n\n // 2026-05-16 audit codex Round 6:rAF capture + cancel on unmount/re-run(defensive hygiene)。\n // 原 callback ref `requestAnimationFrame(() => ...)` 沒 cancel,unmount-during-rAF 可能 fire 後\n // mutate detached element.style — no-op but pattern hygiene 應對齊 DS-wide rAF cancel canonical。\n React.useLayoutEffect(() => {\n const container = containerRef.current\n if (!container) return\n let rafId = 0\n rafId = requestAnimationFrame(() => {\n rafId = 0\n const cs = getComputedStyle(container)\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const gap = parseFloat(cs.gap) || parseFloat(cs.columnGap) || 0\n const available = container.offsetWidth - padL - padR\n\n const items = Array.from(container.children) as HTMLElement[]\n if (items.length === 0) return\n\n let currentRow = 0\n let maxRow = 0\n\n items.forEach(item => {\n const w = item.offsetWidth\n const needed = currentRow > 0 ? currentRow + gap + w : w\n\n if (needed > available && currentRow > 0) {\n maxRow = Math.max(maxRow, currentRow)\n currentRow = w\n } else {\n currentRow = needed\n }\n })\n maxRow = Math.max(maxRow, currentRow)\n\n container.style.maxWidth = `${Math.ceil(maxRow) + padL + padR + 1}px`\n })\n return () => { if (rafId) cancelAnimationFrame(rafId) }\n }, [children])\n\n return (\n <div ref={containerRef} className=\"flex flex-wrap gap-1 p-2 max-w-[280px]\">\n {children}\n </div>\n )\n}\n\nconst OverflowIndicator = React.forwardRef<HTMLSpanElement, OverflowIndicatorProps>(\n function OverflowIndicator(\n { count, shape = 'circle', size = 'md', children, className, ...props },\n ref,\n ) {\n if (count <= 0) return null\n\n const trigger = shape === 'tag' ? (\n <span\n ref={ref}\n data-overflow-indicator=\"\"\n tabIndex={0}\n role=\"button\"\n aria-haspopup=\"dialog\"\n className={cn(tagVariants({ color: 'neutral', size }), 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', className)}\n {...props}\n >\n <span className=\"px-1\">+{count}</span>\n </span>\n ) : (\n <span\n ref={ref}\n data-overflow-indicator=\"\"\n tabIndex={0}\n role=\"button\"\n aria-haspopup=\"dialog\"\n className={cn(\n 'shrink-0 rounded-full inline-grid place-content-center',\n 'bg-muted text-foreground font-medium leading-none cursor-pointer',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n triggerSize[size],\n triggerText[size],\n className,\n )}\n {...props}\n >\n +{count}\n </span>\n )\n\n // 2026-05-18 fix(per user audit「所有 hovercard 應消費 hover delay token」+ motion.spec.md SSOT):\n // plain tier(純列表展開、無 fetch)= HOVER_DELAY_PLAIN_MS,per motion.spec.md 對照表 row;\n // close = HOVER_DELAY_CLOSE_MS。2026-06-11 修:2026-05-18 token 遷移誤挑 rich(原 hardcode 200/300\n // 兩 tier 皆非,遷移未對照 spec 表)— popup 可互動性由 close 緩衝保障,與 open tier 無關。\n return (\n <HoverCard openDelay={HOVER_DELAY_PLAIN_MS} closeDelay={HOVER_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {trigger}\n </HoverCardTrigger>\n <HoverCardContent className=\"bg-tooltip rounded-lg\" data-theme=\"dark\">\n <ShrinkWrapList>{children}</ShrinkWrapList>\n </HoverCardContent>\n </HoverCard>\n )\n },\n)\nOverflowIndicator.displayName = 'OverflowIndicator'\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 overflowIndicatorMeta = {\n component: 'OverflowIndicator',\n family:
|
|
1
|
+
{"version":3,"file":"overflow-indicator.js","sources":["../../../src/components/OverflowIndicator/overflow-indicator.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 Combobox / PeoplePicker 等 DS 元件 wrap 消費,end-user app 請用 wrapper 元件。\n */\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { tagVariants } from '@/design-system/components/Tag/tag'\nimport { HOVER_DELAY_PLAIN_MS, HOVER_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\n\n/**\n * OverflowIndicator — +N 觸發器 + HoverCard 顯示溢出內容\n *\n * 統一用 HoverCard(不用 Tooltip)——溢出內容可能需要互動:\n * - 人員 +N:tag dismiss + hover name card\n * - 一般 +N:穩定顯示溢出項目\n *\n * trigger 不用 Tag 元件(Tag 有內建 truncation Tooltip 會跟 HoverCard 衝突),\n * 改用 tagVariants 直接套樣式。\n */\n\nconst triggerSize: Record<string, string> = {\n sm: 'h-5 min-w-5',\n md: 'h-6 min-w-6',\n lg: 'h-6 min-w-6',\n}\n\nconst triggerText: Record<string, string> = {\n sm: 'text-[10px]',\n md: 'text-caption',\n lg: 'text-caption',\n}\n\nexport interface OverflowIndicatorProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'> {\n count: number\n shape?: 'circle' | 'tag'\n size?: 'sm' | 'md' | 'lg'\n children: React.ReactNode\n className?: string\n}\n\nfunction ShrinkWrapList({ children }: { children: React.ReactNode }) {\n const containerRef = React.useRef<HTMLDivElement | null>(null)\n\n // 2026-05-16 audit codex Round 6:rAF capture + cancel on unmount/re-run(defensive hygiene)。\n // 原 callback ref `requestAnimationFrame(() => ...)` 沒 cancel,unmount-during-rAF 可能 fire 後\n // mutate detached element.style — no-op but pattern hygiene 應對齊 DS-wide rAF cancel canonical。\n React.useLayoutEffect(() => {\n const container = containerRef.current\n if (!container) return\n let rafId = 0\n rafId = requestAnimationFrame(() => {\n rafId = 0\n const cs = getComputedStyle(container)\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const gap = parseFloat(cs.gap) || parseFloat(cs.columnGap) || 0\n const available = container.offsetWidth - padL - padR\n\n const items = Array.from(container.children) as HTMLElement[]\n if (items.length === 0) return\n\n let currentRow = 0\n let maxRow = 0\n\n items.forEach(item => {\n const w = item.offsetWidth\n const needed = currentRow > 0 ? currentRow + gap + w : w\n\n if (needed > available && currentRow > 0) {\n maxRow = Math.max(maxRow, currentRow)\n currentRow = w\n } else {\n currentRow = needed\n }\n })\n maxRow = Math.max(maxRow, currentRow)\n\n container.style.maxWidth = `${Math.ceil(maxRow) + padL + padR + 1}px`\n })\n return () => { if (rafId) cancelAnimationFrame(rafId) }\n }, [children])\n\n return (\n <div ref={containerRef} className=\"flex flex-wrap gap-1 p-2 max-w-[280px]\">\n {children}\n </div>\n )\n}\n\nconst OverflowIndicator = React.forwardRef<HTMLSpanElement, OverflowIndicatorProps>(\n function OverflowIndicator(\n { count, shape = 'circle', size = 'md', children, className, ...props },\n ref,\n ) {\n if (count <= 0) return null\n\n const trigger = shape === 'tag' ? (\n <span\n ref={ref}\n data-overflow-indicator=\"\"\n tabIndex={0}\n role=\"button\"\n aria-haspopup=\"dialog\"\n className={cn(tagVariants({ color: 'neutral', size }), 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', className)}\n {...props}\n >\n <span className=\"px-1\">+{count}</span>\n </span>\n ) : (\n <span\n ref={ref}\n data-overflow-indicator=\"\"\n tabIndex={0}\n role=\"button\"\n aria-haspopup=\"dialog\"\n className={cn(\n 'shrink-0 rounded-full inline-grid place-content-center',\n 'bg-muted text-foreground font-medium leading-none cursor-pointer',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n triggerSize[size],\n triggerText[size],\n className,\n )}\n {...props}\n >\n +{count}\n </span>\n )\n\n // 2026-05-18 fix(per user audit「所有 hovercard 應消費 hover delay token」+ motion.spec.md SSOT):\n // plain tier(純列表展開、無 fetch)= HOVER_DELAY_PLAIN_MS,per motion.spec.md 對照表 row;\n // close = HOVER_DELAY_CLOSE_MS。2026-06-11 修:2026-05-18 token 遷移誤挑 rich(原 hardcode 200/300\n // 兩 tier 皆非,遷移未對照 spec 表)— popup 可互動性由 close 緩衝保障,與 open tier 無關。\n return (\n <HoverCard openDelay={HOVER_DELAY_PLAIN_MS} closeDelay={HOVER_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {trigger}\n </HoverCardTrigger>\n <HoverCardContent className=\"bg-tooltip rounded-lg\" data-theme=\"dark\">\n <ShrinkWrapList>{children}</ShrinkWrapList>\n </HoverCardContent>\n </HoverCard>\n )\n },\n)\nOverflowIndicator.displayName = 'OverflowIndicator'\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 overflowIndicatorMeta = {\n component: 'OverflowIndicator',\n family: 'self-contained', // 對齊 overflow-indicator.spec.md frontmatter family: self-contained(SSOT)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-muted'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { OverflowIndicator }\n"],"names":["OverflowIndicator"],"mappings":"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"}
|
|
@@ -75,7 +75,7 @@ const PeoplePicker = React.forwardRef(function PeoplePicker2({
|
|
|
75
75
|
...rest,
|
|
76
76
|
children: [
|
|
77
77
|
/* @__PURE__ */ jsx("span", { className: cn("flex-1 min-w-0 inline-flex items-center", nakedCellRowModeAlign, resolvedMode === "disabled" && "text-fg-disabled"), children: isEmpty ? /* @__PURE__ */ jsx("span", { className: "text-fg-muted", children: EMPTY_DISPLAY }) : isMulti ? /* @__PURE__ */ jsx(MultiPersonDisplay, { value, size, measured: true }) : /* @__PURE__ */ jsx(PersonDisplay, { value, size }) }),
|
|
78
|
-
(resolvedVariant === "naked" ? showDisplayEndIcon :
|
|
78
|
+
(resolvedVariant === "naked" ? showDisplayEndIcon : resolvedMode === "disabled") && /* @__PURE__ */ jsx(ItemSuffix, { className: "pointer-events-none", children: /* @__PURE__ */ jsx(ChevronDown, { size: ICON_SIZE[size], className: cn("shrink-0", resolvedMode === "disabled" ? "text-fg-disabled" : "text-fg-muted"), "aria-hidden": true }) })
|
|
79
79
|
]
|
|
80
80
|
}
|
|
81
81
|
);
|
|
@@ -194,7 +194,7 @@ const PeoplePicker = React.forwardRef(function PeoplePicker2({
|
|
|
194
194
|
wrap: false,
|
|
195
195
|
defaultOpen,
|
|
196
196
|
onOpenChange,
|
|
197
|
-
className: cn(className, !isEmpty && surface === "form" && "!px-
|
|
197
|
+
className: cn(className, !isEmpty && surface === "form" && "!px-[var(--field-px)]"),
|
|
198
198
|
"aria-label": ariaLabel,
|
|
199
199
|
tagWrapperClassName: getPeoplePickerTagWrapperClass(selectedNames.length),
|
|
200
200
|
overflowWrapperClassName: "-ml-0.5 first:ml-0 relative inline-flex",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"people-picker.js","sources":["../../../src/components/PeoplePicker/people-picker.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// @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)。\n// @cell-metric-escape-allow: comment describes RETIRED `tagAreaPaddingLeftPx={8}` magic — current code is surface-guarded (`surface === 'form'` only injects `!px-3`; 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.\nimport * as React from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport type { FieldMode, FieldVariant } from '@/design-system/components/Field/field-types'\nimport { fieldWrapperStyles, EMPTY_DISPLAY, nakedCellRowModeAlign } from '@/design-system/components/Field/field-wrapper'\nimport { ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'\nimport { useFieldSurface, useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldMode, useResolvedFieldVariant } from '@/design-system/components/Field/field-context'\nimport { Avatar } from '@/design-system/components/Avatar/avatar'\nimport { Tag } from '@/design-system/components/Tag/tag'\nimport { Select } from '@/design-system/components/Select/select'\nimport { Combobox } from '@/design-system/components/Combobox/combobox'\nimport { PersonDisplay, MultiPersonDisplay, PersonAvatarTag, buildPersonProfileCard, resolvePerson, type PersonValue } from './person-display'\nimport {\n getAvatarStackVisibleCount,\n AVATAR_STACK_AVATAR_PX,\n AVATAR_STACK_OVERFLOW_CHIP_PX,\n} from './avatar-stack-overflow'\n// Pure helpers extracted to sibling for file-size budget(2026-05-18,P1 ≤ 500 lines)。\n// 不消費 component closure 的純 constant / 純 mapping function 全部搬走,主檔保留 SSOT-bearing\n// render logic(消費 Combobox / Select / state 等 closure 的部分)。\n// SSOT primitive re-export(backward-compat 對外 import 路徑保持 `./people-picker`)。\nimport {\n PEOPLE_PICKER_LENGTH1_WRAPPER_CLASS,\n getPeoplePickerTagWrapperClass,\n personToSelectOption,\n findPerson,\n} from './people-picker-helpers'\nimport { ICON_SIZE } from '@/design-system/tokens/uiSize/icon-size'\nexport { PEOPLE_PICKER_LENGTH1_WRAPPER_CLASS, getPeoplePickerTagWrapperClass }\n\n// ── PeoplePicker ────────────────────────────────────────────────────────────\n// **2026-05-07 v15.6 SSOT 重構 v2**:\n//\n// - **single mode** wraps `<Select searchable selectedItemRenderer>`\n// - **multi mode** 兩種 displayMode(consumer 自選),**皆 wrap `<Combobox>`**(同 SSOT,\n// 差別在 tagRenderer 視覺):\n// - **'stack'**(default,baseline 既有視覺)— Avatar 疊合 + `+N` overflow indicator,\n// 不可 wrap。tagRenderer 渲染 avatar stack(visible count 走 shared `avatar-stack-overflow`\n// primitive deterministic formula,2026-05-15 Bug 3 fix;display 路徑 `<MultiPersonDisplay>` 同 primitive)。\n// 對齊 Notion / Linear / Atlassian / Slack 多人 quick-glance idiom。\n// - **'pill'**(opt-in)— 每人 Tag pill,可 wrap。Wrap `<Combobox tagRenderer>`,\n// tagRenderer 用 Tag 元件 `avatar` prop SSOT(不塞 children)。\n// `pillShowAvatar` 控 pill 內是否顯 avatar prefix(default true,false → 純文字 pill)。\n// 對齊 GitHub Reviewers / Combobox tag-input idiom。\n\n// **codex P2 fix(2026-05-07 v15.10)**:`extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>`\n// 讓 consumer 可傳 `id` / `data-testid` / `onBlur` / `onFocus` / `aria-*` 等 HTML root props,\n// component 內部 `...rest` forward 到 trigger 容器(對齊 DS 既有 Combobox / Select 慣例)。\n// `onChange` 衝突走 Omit(本 component 用 PersonValue[] custom signature)。\nexport interface PeoplePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** Field mode(edit / display / readonly / disabled),默認 inherit Field context 或 'edit' */\n mode?: FieldMode\n /** Field chrome variant(對齊 Select / Combobox)*/\n variant?: FieldVariant\n size?: 'sm' | 'md' | 'lg'\n /** 當前已選的人(單選 PersonValue,多選 PersonValue[])*/\n value?: PersonValue | PersonValue[] | null\n /** 值變更 callback(永遠 emit array — single mode 取 [0] 即 single value)*/\n onChange?: (value: PersonValue[]) => void\n /** 可選人員清單(edit mode 下拉顯示)*/\n people?: PersonValue[]\n /** 2026-05-12 Stream C Issue 4 fix(codex Q3 Cluster C):trigger empty placeholder。\n * Default '請選擇人員'。**禁** 將 `emptyText`(search-empty)當 trigger placeholder 傳。 */\n placeholder?: string\n /** 搜尋框 placeholder */\n searchPlaceholder?: string\n /** 搜尋無結果訊息(filtered menu empty)。**僅**用於 SelectMenu noResultsText,\n * 不再 silent 轉 trigger placeholder(2026-05-12 Issue 4 semantic fix)。 */\n emptyText?: string\n className?: string\n disabled?: boolean\n /** Initial open state(uncontrolled)*/\n defaultOpen?: boolean\n /** open state 變更 callback */\n onOpenChange?: (open: boolean) => void\n /**\n * Multi mode 顯示樣式(default 'stack')。Single mode 此 prop 忽略。\n * - 'stack' — Avatar 疊合 + `+N`(空間省、不可 wrap;default)\n * - 'pill' — 每人 Tag pill(可 wrap)\n */\n multiDisplay?: 'stack' | 'pill'\n /**\n * `multiDisplay='pill'` 模式下是否顯示 avatar prefix(default true)。\n * 設 false → 純文字 pill,進一步節省空間。對齊 Tag 元件 `avatar` prop SSOT。\n */\n pillShowAvatar?: boolean\n /** Pill 模式下是否允許 wrap(default true)— 對齊 Combobox `wrap` prop */\n pillWrap?: boolean\n /**\n * 搜尋型態(2026-05-12 規則 3 ship,3-mode SSOT 對齊 A1-A5 spec):\n * - `'menu'`(default,backward-compat)— 浮層內搜尋(panel-top search)\n * - `'trigger'`(multi 模式 opt-in)— inline 搜尋(浮層開時 name 拿掉,avatar 後接 input cursor,\n * 類 Combobox inline-trigger idiom)\n * Single mode 永遠 inline-trigger(wrap Select searchable 直接走 inline),此 prop multi 才有意義。\n */\n searchIn?: 'menu' | 'trigger'\n /**\n * Display 是否渲 ChevronDown + Field naked wrapper(D-path opt-in,2026-05-08)\n * — DataTable cell display↔edit 像素級對齊用。預設 false(裸 PersonDisplay,backward compat)。\n * 設 true 時 display 走 fieldWrapperStyles(naked variant)+ ItemSuffix ChevronDown,\n * 與 edit (Select / Combobox wrapped) 同 DOM 結構,消除 Layer-B padding mismatch。\n */\n showDisplayEndIcon?: boolean\n /** a11y label */\n 'aria-label'?: string\n}\n\nconst PeoplePicker = React.forwardRef<HTMLDivElement, PeoplePickerProps>(function PeoplePicker({\n mode: modeProp,\n variant: variantProp,\n size: sizeProp,\n value,\n onChange,\n people = [],\n placeholder = '請選擇人員', // i18n-allow: DS default(2026-05-12 Stream C Issue 4)\n searchPlaceholder = '搜尋人員…', // i18n-allow: DS default\n emptyText = '沒有符合的人員', // i18n-allow: DS default — only for SelectMenu noResultsText\n className,\n disabled: disabledProp,\n defaultOpen = false,\n onOpenChange,\n multiDisplay = 'stack',\n pillShowAvatar = true,\n pillWrap = true,\n searchIn = 'menu',\n showDisplayEndIcon = false,\n 'aria-label': ariaLabel,\n ...rest\n}, ref) {\n const surface = useFieldSurface()\n const size = useResolvedFieldSize(sizeProp) // B 組 cascade fix\n const disabled = useResolvedFieldDisabled(disabledProp)\n // 2026-06-08 SSOT:mode/disabled/variant 統一經 helper;修 <Field disabled> 漏 cascade(原只讀 fieldCtx.mode)\n const resolvedMode: FieldMode = useResolvedFieldMode({ mode: modeProp, disabled })\n const resolvedVariant: FieldVariant = useResolvedFieldVariant(variantProp)\n const isMulti = Array.isArray(value)\n const isEmpty = !value || (isMulti && value.length === 0)\n\n // ── mode='display' ────────────────────────────────────────────────────────\n // Default(showDisplayEndIcon=false):裸 PersonDisplay / MultiPersonDisplay — backward compat。\n // Opt-in(showDisplayEndIcon=true,2026-05-08 D-path):Field naked wrapper + ItemSuffix ChevronDown,\n // 與 edit (Select / Combobox wrapped) 同 DOM 結構消除 cell display↔edit 像素偏移。\n if (resolvedMode === 'display') {\n if (!showDisplayEndIcon) {\n if (isEmpty) return <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>\n return isMulti\n ? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />\n : <PersonDisplay value={value as PersonValue} size={size} />\n }\n // 2026-05-18 改 import ICON_SIZE SSOT(per user『做完』approval,消除 M17 違反 7+ 重複 ternary)\n const iconSize = ICON_SIZE[size as 'sm' | 'md' | 'lg']\n return (\n <div\n className={cn(fieldWrapperStyles({ mode: 'display', variant: resolvedVariant, size }), className)}\n data-field-mode=\"display\"\n >\n <span className={cn('flex-1 min-w-0 inline-flex items-center', nakedCellRowModeAlign)}>\n {isEmpty\n ? <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>\n : isMulti\n ? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />\n : <PersonDisplay value={value as PersonValue} size={size} />}\n </span>\n <ItemSuffix className=\"pointer-events-none\">\n <ChevronDown size={iconSize} className=\"shrink-0 text-fg-muted\" aria-hidden />\n </ItemSuffix>\n </div>\n )\n }\n\n // ── readonly / disabled — Field wrapper chrome,Avatar 視覺保留 ───────────\n if (resolvedMode !== 'edit') {\n return (\n <div\n ref={ref}\n className={cn(fieldWrapperStyles({ mode: resolvedMode, variant: resolvedVariant, size }), className)}\n data-field-mode={resolvedMode}\n aria-disabled={resolvedMode === 'disabled' ? true : undefined}\n aria-label={ariaLabel}\n {...rest}\n >\n <span className={cn('flex-1 min-w-0 inline-flex items-center', nakedCellRowModeAlign, resolvedMode === 'disabled' && 'text-fg-disabled')}>\n {isEmpty\n ? <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>\n : isMulti\n ? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />\n : <PersonDisplay value={value as PersonValue} size={size} />}\n </span>\n {/* 2026-06-10 類型身份 indicator:readonly/disabled 保留 chevron(naked cell 依 showDisplayEndIcon);disabled → fg-disabled */}\n {(resolvedVariant === 'naked' ? showDisplayEndIcon : true) && (\n <ItemSuffix className=\"pointer-events-none\">\n <ChevronDown size={ICON_SIZE[size as 'sm' | 'md' | 'lg']} className={cn('shrink-0', resolvedMode === 'disabled' ? 'text-fg-disabled' : 'text-fg-muted')} aria-hidden />\n </ItemSuffix>\n )}\n </div>\n )\n }\n\n // ── edit mode ─────────────────────────────────────────────────────────────\n const selectedNames: string[] = !value\n ? []\n : Array.isArray(value)\n ? value.map(v => resolvePerson(v).name)\n : [resolvePerson(value).name]\n\n // ── single mode → wraps Select ────────────────────────────────────────────\n if (!isMulti) {\n const handleSingleChange = (name: string) => onChange?.([findPerson(people, name)])\n return (\n <Select\n ref={ref as React.Ref<HTMLDivElement>}\n size={size}\n variant={resolvedVariant}\n options={people.map(personToSelectOption)}\n value={selectedNames[0] ?? null}\n onChange={handleSingleChange}\n searchable\n placeholder={placeholder}\n // 2026-05-12 Stream C Issue 4 fix(codex Q3):傳 `placeholder` 給 Select trigger empty。\n // 不再傳 `emptyText`(search-empty semantic 跟 trigger-empty 分離,canonical SSOT)。\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n className={className}\n aria-label={ariaLabel}\n selectedItemRenderer={(opt) => <PersonDisplay value={findPerson(people, opt.value)} size={size} />}\n // **codex P2 forward**:Select extends `SelectHTMLAttributes<HTMLSelectElement>`,\n // event handler element 型別跟 PeoplePicker `HTMLAttributes<HTMLDivElement>` 不一致\n // (`onCopy` / `onChange` 等)。Runtime spread 等效 — DOM 收到 attrs 不挑剔。\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // any-allow: rest 含 `onChange: FormEventHandler` 跟 Select onChange signature 衝突 — DOM runtime spread 安全(per codex P2 forward)\n {...(rest as any)}\n />\n )\n }\n\n // ── multi 'pill' → wraps Combobox(對齊 GitHub Reviewers / Combobox idiom)────\n if (multiDisplay === 'pill') {\n const handleMultiChange = (next: string[]) => {\n onChange?.(next.map(name => findPerson(people, name)))\n }\n return (\n <Combobox\n ref={ref as React.Ref<HTMLDivElement>}\n size={size}\n variant={resolvedVariant}\n options={people.map(personToSelectOption)}\n value={selectedNames}\n onChange={handleMultiChange}\n searchable\n searchPlaceholder={searchPlaceholder}\n // 2026-05-12 Stream C Issue 4(codex Q3):placeholder = trigger empty hint('請選擇人員')\n // — semantic clean separation;emptyText 不再 silent 轉 trigger placeholder。\n // emptyPlaceholder backward-compat forward(Combobox line 760 `placeholder ?? emptyPlaceholder` fallback)\n // 1 cycle:future 移除 emptyPlaceholder forward,emptyText 改傳 SelectMenu noResultsText。\n placeholder={placeholder}\n emptyPlaceholder={emptyText}\n wrap={pillWrap}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n className={className}\n aria-label={ariaLabel}\n // codex P2 forward(see Select branch comment for type-cast rationale)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // any-allow: rest 含 `onChange: FormEventHandler` 跟 Combobox onChange signature 衝突 — DOM runtime spread 安全(per codex P2 forward)\n {...(rest as any)}\n // **Tag SSOT canonical**:用 `avatar` prop(不塞 children),Tag 內部統一\n // wrap 進 16×16 圓形 mask container(per Tag tsx line 175)。\n tagRenderer={(item, onRemove) => {\n const p = resolvePerson(findPerson(people, item.value))\n return (\n <Tag\n key={item.value}\n size={size}\n color=\"neutral\"\n // 2026-05-18 7B' fix(per user 拍板「執行」+ Codex Round 3 共識,paired with Combobox L286):\n // 拿掉 `unbounded` 對齊 Tag canonical max-w-40 cap + 內建 ellipsis。PeoplePicker pill 走\n // Combobox tagRenderer slot,SSOT = Tag primitive 視覺(per codex Round 3 verdict)。\n // 人名 99% < 25 chars 不觸發 cap;極端長名(複數姓 + middle name)觸發 ellipsis 是合理 UX。\n avatar={pillShowAvatar\n ? <Avatar src={p.avatarUrl} alt={p.name} size={16} hoverCard={buildPersonProfileCard(p)} />\n : undefined}\n onRemove={onRemove}\n >\n {p.name}\n </Tag>\n )\n }}\n />\n )\n }\n\n // ── multi 'stack' (default) → wraps Combobox 跟 pill mode 同 SSOT,差別在 tagRenderer 視覺。\n //\n // **2026-05-15 Bug 3 fix(Claude+Codex Step 5 比稿 consensus)**:visible count 走 shared\n // `avatar-stack-overflow` primitive deterministic formula(取代 Combobox DOM offsetWidth-based\n // useOverflowCount + 60px fallback 不 deterministic),pass override 給 Combobox bypass internal\n // measurement。`MultiPersonDisplay`(display path)同 primitive,display + edit 結果一致。\n // 對齊 user verbatim SSOT「同 cell width 同 overflow 判斷」+ codex Q3 consensus shared primitive。\n const handleMultiChange = (next: string[]) => {\n onChange?.(next.map(name => findPerson(people, name)))\n }\n // SSOT visible count compute via formula primitive + ResizeObserver\n const stackContainerRef = React.useRef<HTMLDivElement | null>(null)\n const [stackVisibleCount, setStackVisibleCount] = React.useState<number | undefined>(undefined)\n React.useLayoutEffect(() => {\n // 注:此 effect 只在 multi stack mode 跑(early return if not stack);length<=1 不需 override\n if (!isMulti || selectedNames.length <= 1) {\n setStackVisibleCount(undefined); return\n }\n const root = stackContainerRef.current\n if (!root) return\n // 2026-05-15 ROOT CAUSE FIX(user 抓「之前說的問題都還是存在」):\n // stackContainerRef 透過 mergedStackRef 接 Combobox forwarded ref → root **就是** [role=combobox]\n // div 自己。原 `root.querySelector('[role=combobox]')` 不找 self 永遠 null → trigger=null →\n // tagArea=null → available=0 → setStackVisibleCount(0) → 整 stack 全 overflow → fallback 到\n // Combobox DOM-based useOverflowCount(非 deterministic 那個算法)。修:用 root 自己當 trigger,\n // 從 root 內找 tagArea(flex-1 min-w-0 div)。\n const calc = () => {\n const trigger = root.matches('[role=\"combobox\"]') ? root : root.querySelector<HTMLElement>('[role=\"combobox\"]')\n const tagArea = trigger?.querySelector<HTMLElement>('div[class*=\"flex-1\"][class*=\"min-w-0\"]')\n const available = tagArea?.clientWidth ?? trigger?.clientWidth ?? 0\n const visible = getAvatarStackVisibleCount({\n availablePx: available,\n total: selectedNames.length,\n avatarPx: AVATAR_STACK_AVATAR_PX[size],\n overflowChipPx: AVATAR_STACK_OVERFLOW_CHIP_PX[size],\n })\n setStackVisibleCount(visible)\n }\n calc()\n const ro = new ResizeObserver(calc)\n ro.observe(root)\n return () => ro.disconnect()\n }, [isMulti, multiDisplay, selectedNames.length, size])\n // Merge ref:forward to parent + capture for ResizeObserver\n const mergedStackRef = React.useCallback((el: HTMLDivElement | null) => {\n stackContainerRef.current = el\n if (typeof ref === 'function') ref(el)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el\n }, [ref])\n\n return (\n <Combobox\n ref={mergedStackRef}\n size={size}\n variant={resolvedVariant}\n options={people.map(personToSelectOption)}\n value={selectedNames}\n onChange={handleMultiChange}\n searchable\n searchIn={searchIn}\n searchPlaceholder={searchPlaceholder}\n // 2026-05-12 Stream C Issue 4(codex Q3):placeholder = trigger empty('請選擇人員');emptyText = search-empty(僅 backward-compat forward 1 cycle)\n placeholder={placeholder}\n emptyPlaceholder={emptyText}\n wrap={false}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n // 2026-05-13 (a) fix(user 拍 Path a + Layer A density-drift root-cause):\n // 撤掉 `tagAreaPaddingLeftPx={8}` magic — Combobox `tagPadding[size]` 是 density-dependent\n // calc 公式(`(field-height - icon-size) / 2`),只在 md size + default density 才 = 4px;\n // 其他 size/density 漂 6px / 8px → 4+8=12 spec 公式不成立。\n // (a) fix:form context + 有 tag → 改 inject `!px-3`(固定 12px)直接 override `tagPadding[size]`,\n // 達成 GitHub PeoplePicker fixed 12px inset(對齊 cell context 同 13px from cell.left 含 1px border)。\n // - form + 有 tag → `!px-3`(12px 固定 inset)+ tagAreaPaddingLeftPx undefined → field.padL=12 ✓\n // - table-cell + 有 tag → naked variant `!px-[var(--table-cell-px)]` 已是 12px,不 inject ✓\n // - isEmpty → 不 inject,走 Combobox 預設文字 inset(`tagPadding[size]` 公式自然 vertical center)\n className={cn(className, !isEmpty && surface === 'form' && '!px-3')}\n aria-label={ariaLabel}\n // 2026-05-15 Bug 1 fix(Claude+Codex Step 5 比稿 consensus,user verbatim「就 A」):per-length 動態\n // wrapper class — length=1 降階單人視覺需要 width constraint chain(`flex-1 min-w-0 overflow-hidden`),\n // length>=2 stack 視覺保留 overlap(`-ml-0.5 first:ml-0 relative inline-flex group/avatar`)。對齊\n // spec.md §C row 1(length=1 = avatar+人名+ellipsis)+ §D row 1(length>=2 = stack overlap)。\n // 真根因:Combobox `OverflowTagList` 把所有 tagRenderer 結果包 `shrink-0`,加 `inline-flex` 後\n // wrapper 變 intrinsic content-width → PersonDisplay `w-full` resolves to intrinsic → truncate 無效。\n // 修法:length=1 wrapper 改 `flex-1 min-w-0 overflow-hidden` 提供 width constraint 給 PersonDisplay。\n // SSOT helper `getPeoplePickerTagWrapperClass(count)` 集中,future 改 wrapper 行為改一處。\n tagWrapperClassName={getPeoplePickerTagWrapperClass(selectedNames.length)}\n // 2026-05-16 真 root cause fix:overflow chip wrapper 套同 `-ml-0.5` 讓 chip 物理上\n // 跟 avatar 同 slot(等寬同 step,non-overlapping 多 24px 區塊不再 saw)。對齊 user\n // 「avatars 和 +N 都是同尺寸圓形,空間最多容固定數量圓形」物理模型 directive +\n // MUI AvatarGroup / Primer AvatarStack 共識(`AvatarGroup.js` L54-59 same negative margin)。\n overflowWrapperClassName=\"-ml-0.5 first:ml-0 relative inline-flex\"\n tagAreaGapPx={0}\n tagAreaPaddingLeftPx={undefined}\n // 2026-05-12 Round 7 fix(user 抓 image 2「+N tag 應該圓形不是矩形」+ 對齊 GitHub picker idiom):\n // Combobox default `overflowShape='tag'`(矩形 chip,文字 Combobox 慣例);PeoplePicker stack\n // mode pass `'circle'`(圓形 avatar-shape,跟 avatar 一樣大)。對齊 MultiPersonDisplay readonly path\n // 既有 OverflowIndicator default 'circle' SSOT。\n overflowShape=\"circle\"\n // 2026-05-15 Bug 3 fix:formula-based visible count override(避免 Combobox DOM measurement +\n // 60px fallback 不 deterministic)。SSOT in `./avatar-stack-overflow.ts`,display + edit 共用。\n visibleCountOverride={stackVisibleCount}\n // 2026-05-14 I4 fix(per codex+Layer A 共識):hidden items 在 `+N` overflow popover 顯\n // Tag with avatar(對齊 display MultiPersonDisplay popover SSOT,user 抓 display vs edit\n // overflow 視覺不一致)。\n renderHiddenTag={(item) => {\n const p = resolvePerson(findPerson(people, item.value))\n return (\n <Tag\n key={item.value}\n color=\"neutral\"\n size=\"sm\"\n avatar={\n <Avatar\n src={p.avatarUrl}\n alt={p.name}\n size={16}\n hoverCard={buildPersonProfileCard(p)}\n />\n }\n onRemove={() => {\n onChange?.(selectedNames.filter(n => n !== item.value).map(n => findPerson(people, n)))\n }}\n >\n {p.name}\n </Tag>\n )\n }}\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // any-allow: rest 含 `onChange: FormEventHandler` 跟 Combobox onChange signature 衝突 — DOM runtime spread 安全(per codex P2 forward)\n {...(rest as any)}\n tagRenderer={(item, onRemove) => {\n const p = resolvePerson(findPerson(people, item.value))\n // 2026-05-12 Q2 fix(user 拍板「multi 只選 1 人時 trigger = avatar + name,跟 single mode 同」):\n // selectedNames.length === 1 → PersonDisplay(avatar + name)代替 PersonAvatarTag(avatar only)。\n // SSOT 對齊 PeoplePicker single mode line 201 selectedItemRenderer。多選 1 人時視覺等同單選,\n // 只在 length > 1 才走 stack(各 avatar 純 chip)。多選 + inline 搜尋場景拿掉 name 改 cursor\n // 走 `searchIn='trigger'` opt-in(2026-05-12 規則 3 ship,已轉傳 Combobox;default 'menu' 走 panel-top search)。\n if (selectedNames.length === 1) {\n return <PersonDisplay key={item.value} value={p} size={size} />\n }\n return (\n <PersonAvatarTag\n key={item.value}\n person={p}\n size={size}\n onRemove={onRemove}\n />\n )\n }}\n />\n )\n})\nPeoplePicker.displayName = 'PeoplePicker'\n\n// Story auto-compile metadata\nexport const peoplePickerMeta = {\n component: 'PeoplePicker',\n family: 4,\n variants: {},\n sizes: {},\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-fg-disabled', 'text-fg-muted'],\n ring: [],\n },\n} as const\n\nexport { PeoplePicker }\n"],"names":["PeoplePicker","handleMultiChange"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"people-picker.js","sources":["../../../src/components/PeoplePicker/people-picker.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// @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)。\n// @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.\nimport * as React from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport type { FieldMode, FieldVariant } from '@/design-system/components/Field/field-types'\nimport { fieldWrapperStyles, EMPTY_DISPLAY, nakedCellRowModeAlign } from '@/design-system/components/Field/field-wrapper'\nimport { ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'\nimport { useFieldSurface, useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldMode, useResolvedFieldVariant } from '@/design-system/components/Field/field-context'\nimport { Avatar } from '@/design-system/components/Avatar/avatar'\nimport { Tag } from '@/design-system/components/Tag/tag'\nimport { Select } from '@/design-system/components/Select/select'\nimport { Combobox } from '@/design-system/components/Combobox/combobox'\nimport { PersonDisplay, MultiPersonDisplay, PersonAvatarTag, buildPersonProfileCard, resolvePerson, type PersonValue } from './person-display'\nimport {\n getAvatarStackVisibleCount,\n AVATAR_STACK_AVATAR_PX,\n AVATAR_STACK_OVERFLOW_CHIP_PX,\n} from './avatar-stack-overflow'\n// Pure helpers extracted to sibling for file-size budget(2026-05-18,P1 ≤ 500 lines)。\n// 不消費 component closure 的純 constant / 純 mapping function 全部搬走,主檔保留 SSOT-bearing\n// render logic(消費 Combobox / Select / state 等 closure 的部分)。\n// SSOT primitive re-export(backward-compat 對外 import 路徑保持 `./people-picker`)。\nimport {\n PEOPLE_PICKER_LENGTH1_WRAPPER_CLASS,\n getPeoplePickerTagWrapperClass,\n personToSelectOption,\n findPerson,\n} from './people-picker-helpers'\nimport { ICON_SIZE } from '@/design-system/tokens/uiSize/icon-size'\nexport { PEOPLE_PICKER_LENGTH1_WRAPPER_CLASS, getPeoplePickerTagWrapperClass }\n\n// ── PeoplePicker ────────────────────────────────────────────────────────────\n// **2026-05-07 v15.6 SSOT 重構 v2**:\n//\n// - **single mode** wraps `<Select searchable selectedItemRenderer>`\n// - **multi mode** 兩種 displayMode(consumer 自選),**皆 wrap `<Combobox>`**(同 SSOT,\n// 差別在 tagRenderer 視覺):\n// - **'stack'**(default,baseline 既有視覺)— Avatar 疊合 + `+N` overflow indicator,\n// 不可 wrap。tagRenderer 渲染 avatar stack(visible count 走 shared `avatar-stack-overflow`\n// primitive deterministic formula,2026-05-15 Bug 3 fix;display 路徑 `<MultiPersonDisplay>` 同 primitive)。\n// 對齊 Notion / Linear / Atlassian / Slack 多人 quick-glance idiom。\n// - **'pill'**(opt-in)— 每人 Tag pill,可 wrap。Wrap `<Combobox tagRenderer>`,\n// tagRenderer 用 Tag 元件 `avatar` prop SSOT(不塞 children)。\n// `pillShowAvatar` 控 pill 內是否顯 avatar prefix(default true,false → 純文字 pill)。\n// 對齊 GitHub Reviewers / Combobox tag-input idiom。\n\n// **codex P2 fix(2026-05-07 v15.10)**:`extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>`\n// 讓 consumer 可傳 `id` / `data-testid` / `onBlur` / `onFocus` / `aria-*` 等 HTML root props,\n// component 內部 `...rest` forward 到 trigger 容器(對齊 DS 既有 Combobox / Select 慣例)。\n// `onChange` 衝突走 Omit(本 component 用 PersonValue[] custom signature)。\nexport interface PeoplePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** Field mode(edit / display / readonly / disabled),默認 inherit Field context 或 'edit' */\n mode?: FieldMode\n /** Field chrome variant(對齊 Select / Combobox)*/\n variant?: FieldVariant\n size?: 'sm' | 'md' | 'lg'\n /** 當前已選的人(單選 PersonValue,多選 PersonValue[])*/\n value?: PersonValue | PersonValue[] | null\n /** 值變更 callback(永遠 emit array — single mode 取 [0] 即 single value)*/\n onChange?: (value: PersonValue[]) => void\n /** 可選人員清單(edit mode 下拉顯示)*/\n people?: PersonValue[]\n /** 2026-05-12 Stream C Issue 4 fix(codex Q3 Cluster C):trigger empty placeholder。\n * Default '請選擇人員'。**禁** 將 `emptyText`(search-empty)當 trigger placeholder 傳。 */\n placeholder?: string\n /** 搜尋框 placeholder */\n searchPlaceholder?: string\n /** 搜尋無結果訊息(filtered menu empty)。**僅**用於 SelectMenu noResultsText,\n * 不再 silent 轉 trigger placeholder(2026-05-12 Issue 4 semantic fix)。 */\n emptyText?: string\n className?: string\n disabled?: boolean\n /** Initial open state(uncontrolled)*/\n defaultOpen?: boolean\n /** open state 變更 callback */\n onOpenChange?: (open: boolean) => void\n /**\n * Multi mode 顯示樣式(default 'stack')。Single mode 此 prop 忽略。\n * - 'stack' — Avatar 疊合 + `+N`(空間省、不可 wrap;default)\n * - 'pill' — 每人 Tag pill(可 wrap)\n */\n multiDisplay?: 'stack' | 'pill'\n /**\n * `multiDisplay='pill'` 模式下是否顯示 avatar prefix(default true)。\n * 設 false → 純文字 pill,進一步節省空間。對齊 Tag 元件 `avatar` prop SSOT。\n */\n pillShowAvatar?: boolean\n /** Pill 模式下是否允許 wrap(default true)— 對齊 Combobox `wrap` prop */\n pillWrap?: boolean\n /**\n * 搜尋型態(2026-05-12 規則 3 ship,3-mode SSOT 對齊 A1-A5 spec):\n * - `'menu'`(default,backward-compat)— 浮層內搜尋(panel-top search)\n * - `'trigger'`(multi 模式 opt-in)— inline 搜尋(浮層開時 name 拿掉,avatar 後接 input cursor,\n * 類 Combobox inline-trigger idiom)\n * Single mode 永遠 inline-trigger(wrap Select searchable 直接走 inline),此 prop multi 才有意義。\n */\n searchIn?: 'menu' | 'trigger'\n /**\n * Display 是否渲 ChevronDown + Field naked wrapper(D-path opt-in,2026-05-08)\n * — DataTable cell display↔edit 像素級對齊用。預設 false(裸 PersonDisplay,backward compat)。\n * 設 true 時 display 走 fieldWrapperStyles(naked variant)+ ItemSuffix ChevronDown,\n * 與 edit (Select / Combobox wrapped) 同 DOM 結構,消除 Layer-B padding mismatch。\n */\n showDisplayEndIcon?: boolean\n /** a11y label */\n 'aria-label'?: string\n}\n\nconst PeoplePicker = React.forwardRef<HTMLDivElement, PeoplePickerProps>(function PeoplePicker({\n mode: modeProp,\n variant: variantProp,\n size: sizeProp,\n value,\n onChange,\n people = [],\n placeholder = '請選擇人員', // i18n-allow: DS default(2026-05-12 Stream C Issue 4)\n searchPlaceholder = '搜尋人員…', // i18n-allow: DS default\n emptyText = '沒有符合的人員', // i18n-allow: DS default — only for SelectMenu noResultsText\n className,\n disabled: disabledProp,\n defaultOpen = false,\n onOpenChange,\n multiDisplay = 'stack',\n pillShowAvatar = true,\n pillWrap = true,\n searchIn = 'menu',\n showDisplayEndIcon = false,\n 'aria-label': ariaLabel,\n ...rest\n}, ref) {\n const surface = useFieldSurface()\n const size = useResolvedFieldSize(sizeProp) // B 組 cascade fix\n const disabled = useResolvedFieldDisabled(disabledProp)\n // 2026-06-08 SSOT:mode/disabled/variant 統一經 helper;修 <Field disabled> 漏 cascade(原只讀 fieldCtx.mode)\n const resolvedMode: FieldMode = useResolvedFieldMode({ mode: modeProp, disabled })\n const resolvedVariant: FieldVariant = useResolvedFieldVariant(variantProp)\n const isMulti = Array.isArray(value)\n const isEmpty = !value || (isMulti && value.length === 0)\n\n // ── mode='display' ────────────────────────────────────────────────────────\n // Default(showDisplayEndIcon=false):裸 PersonDisplay / MultiPersonDisplay — backward compat。\n // Opt-in(showDisplayEndIcon=true,2026-05-08 D-path):Field naked wrapper + ItemSuffix ChevronDown,\n // 與 edit (Select / Combobox wrapped) 同 DOM 結構消除 cell display↔edit 像素偏移。\n if (resolvedMode === 'display') {\n if (!showDisplayEndIcon) {\n if (isEmpty) return <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>\n return isMulti\n ? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />\n : <PersonDisplay value={value as PersonValue} size={size} />\n }\n // 2026-05-18 改 import ICON_SIZE SSOT(per user『做完』approval,消除 M17 違反 7+ 重複 ternary)\n const iconSize = ICON_SIZE[size as 'sm' | 'md' | 'lg']\n return (\n <div\n className={cn(fieldWrapperStyles({ mode: 'display', variant: resolvedVariant, size }), className)}\n data-field-mode=\"display\"\n >\n <span className={cn('flex-1 min-w-0 inline-flex items-center', nakedCellRowModeAlign)}>\n {isEmpty\n ? <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>\n : isMulti\n ? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />\n : <PersonDisplay value={value as PersonValue} size={size} />}\n </span>\n <ItemSuffix className=\"pointer-events-none\">\n <ChevronDown size={iconSize} className=\"shrink-0 text-fg-muted\" aria-hidden />\n </ItemSuffix>\n </div>\n )\n }\n\n // ── readonly / disabled — Field wrapper chrome,Avatar 視覺保留 ───────────\n if (resolvedMode !== 'edit') {\n return (\n <div\n ref={ref}\n className={cn(fieldWrapperStyles({ mode: resolvedMode, variant: resolvedVariant, size }), className)}\n data-field-mode={resolvedMode}\n aria-disabled={resolvedMode === 'disabled' ? true : undefined}\n aria-label={ariaLabel}\n {...rest}\n >\n <span className={cn('flex-1 min-w-0 inline-flex items-center', nakedCellRowModeAlign, resolvedMode === 'disabled' && 'text-fg-disabled')}>\n {isEmpty\n ? <span className=\"text-fg-muted\">{EMPTY_DISPLAY}</span>\n : isMulti\n ? <MultiPersonDisplay value={value as PersonValue[]} size={size} measured />\n : <PersonDisplay value={value as PersonValue} size={size} />}\n </span>\n {/* 2026-06-26 類型身份 indicator:edit 顯示 / readonly 不顯示 / disabled 保留(fg-disabled,對齊原生 <select disabled>);naked cell 依 showDisplayEndIcon */}\n {(resolvedVariant === 'naked' ? showDisplayEndIcon : resolvedMode === 'disabled') && (\n <ItemSuffix className=\"pointer-events-none\">\n <ChevronDown size={ICON_SIZE[size as 'sm' | 'md' | 'lg']} className={cn('shrink-0', resolvedMode === 'disabled' ? 'text-fg-disabled' : 'text-fg-muted')} aria-hidden />\n </ItemSuffix>\n )}\n </div>\n )\n }\n\n // ── edit mode ─────────────────────────────────────────────────────────────\n const selectedNames: string[] = !value\n ? []\n : Array.isArray(value)\n ? value.map(v => resolvePerson(v).name)\n : [resolvePerson(value).name]\n\n // ── single mode → wraps Select ────────────────────────────────────────────\n if (!isMulti) {\n const handleSingleChange = (name: string) => onChange?.([findPerson(people, name)])\n return (\n <Select\n ref={ref as React.Ref<HTMLDivElement>}\n size={size}\n variant={resolvedVariant}\n options={people.map(personToSelectOption)}\n value={selectedNames[0] ?? null}\n onChange={handleSingleChange}\n searchable\n placeholder={placeholder}\n // 2026-05-12 Stream C Issue 4 fix(codex Q3):傳 `placeholder` 給 Select trigger empty。\n // 不再傳 `emptyText`(search-empty semantic 跟 trigger-empty 分離,canonical SSOT)。\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n className={className}\n aria-label={ariaLabel}\n selectedItemRenderer={(opt) => <PersonDisplay value={findPerson(people, opt.value)} size={size} />}\n // **codex P2 forward**:Select extends `SelectHTMLAttributes<HTMLSelectElement>`,\n // event handler element 型別跟 PeoplePicker `HTMLAttributes<HTMLDivElement>` 不一致\n // (`onCopy` / `onChange` 等)。Runtime spread 等效 — DOM 收到 attrs 不挑剔。\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // any-allow: rest 含 `onChange: FormEventHandler` 跟 Select onChange signature 衝突 — DOM runtime spread 安全(per codex P2 forward)\n {...(rest as any)}\n />\n )\n }\n\n // ── multi 'pill' → wraps Combobox(對齊 GitHub Reviewers / Combobox idiom)────\n if (multiDisplay === 'pill') {\n const handleMultiChange = (next: string[]) => {\n onChange?.(next.map(name => findPerson(people, name)))\n }\n return (\n <Combobox\n ref={ref as React.Ref<HTMLDivElement>}\n size={size}\n variant={resolvedVariant}\n options={people.map(personToSelectOption)}\n value={selectedNames}\n onChange={handleMultiChange}\n searchable\n searchPlaceholder={searchPlaceholder}\n // 2026-05-12 Stream C Issue 4(codex Q3):placeholder = trigger empty hint('請選擇人員')\n // — semantic clean separation;emptyText 不再 silent 轉 trigger placeholder。\n // emptyPlaceholder backward-compat forward(Combobox line 760 `placeholder ?? emptyPlaceholder` fallback)\n // 1 cycle:future 移除 emptyPlaceholder forward,emptyText 改傳 SelectMenu noResultsText。\n placeholder={placeholder}\n emptyPlaceholder={emptyText}\n wrap={pillWrap}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n className={className}\n aria-label={ariaLabel}\n // codex P2 forward(see Select branch comment for type-cast rationale)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // any-allow: rest 含 `onChange: FormEventHandler` 跟 Combobox onChange signature 衝突 — DOM runtime spread 安全(per codex P2 forward)\n {...(rest as any)}\n // **Tag SSOT canonical**:用 `avatar` prop(不塞 children),Tag 內部統一\n // wrap 進 16×16 圓形 mask container(per Tag tsx line 175)。\n tagRenderer={(item, onRemove) => {\n const p = resolvePerson(findPerson(people, item.value))\n return (\n <Tag\n key={item.value}\n size={size}\n color=\"neutral\"\n // 2026-05-18 7B' fix(per user 拍板「執行」+ Codex Round 3 共識,paired with Combobox L286):\n // 拿掉 `unbounded` 對齊 Tag canonical max-w-40 cap + 內建 ellipsis。PeoplePicker pill 走\n // Combobox tagRenderer slot,SSOT = Tag primitive 視覺(per codex Round 3 verdict)。\n // 人名 99% < 25 chars 不觸發 cap;極端長名(複數姓 + middle name)觸發 ellipsis 是合理 UX。\n avatar={pillShowAvatar\n ? <Avatar src={p.avatarUrl} alt={p.name} size={16} hoverCard={buildPersonProfileCard(p)} />\n : undefined}\n onRemove={onRemove}\n >\n {p.name}\n </Tag>\n )\n }}\n />\n )\n }\n\n // ── multi 'stack' (default) → wraps Combobox 跟 pill mode 同 SSOT,差別在 tagRenderer 視覺。\n //\n // **2026-05-15 Bug 3 fix(Claude+Codex Step 5 比稿 consensus)**:visible count 走 shared\n // `avatar-stack-overflow` primitive deterministic formula(取代 Combobox DOM offsetWidth-based\n // useOverflowCount + 60px fallback 不 deterministic),pass override 給 Combobox bypass internal\n // measurement。`MultiPersonDisplay`(display path)同 primitive,display + edit 結果一致。\n // 對齊 user verbatim SSOT「同 cell width 同 overflow 判斷」+ codex Q3 consensus shared primitive。\n const handleMultiChange = (next: string[]) => {\n onChange?.(next.map(name => findPerson(people, name)))\n }\n // SSOT visible count compute via formula primitive + ResizeObserver\n const stackContainerRef = React.useRef<HTMLDivElement | null>(null)\n const [stackVisibleCount, setStackVisibleCount] = React.useState<number | undefined>(undefined)\n React.useLayoutEffect(() => {\n // 注:此 effect 只在 multi stack mode 跑(early return if not stack);length<=1 不需 override\n if (!isMulti || selectedNames.length <= 1) {\n setStackVisibleCount(undefined); return\n }\n const root = stackContainerRef.current\n if (!root) return\n // 2026-05-15 ROOT CAUSE FIX(user 抓「之前說的問題都還是存在」):\n // stackContainerRef 透過 mergedStackRef 接 Combobox forwarded ref → root **就是** [role=combobox]\n // div 自己。原 `root.querySelector('[role=combobox]')` 不找 self 永遠 null → trigger=null →\n // tagArea=null → available=0 → setStackVisibleCount(0) → 整 stack 全 overflow → fallback 到\n // Combobox DOM-based useOverflowCount(非 deterministic 那個算法)。修:用 root 自己當 trigger,\n // 從 root 內找 tagArea(flex-1 min-w-0 div)。\n const calc = () => {\n const trigger = root.matches('[role=\"combobox\"]') ? root : root.querySelector<HTMLElement>('[role=\"combobox\"]')\n const tagArea = trigger?.querySelector<HTMLElement>('div[class*=\"flex-1\"][class*=\"min-w-0\"]')\n const available = tagArea?.clientWidth ?? trigger?.clientWidth ?? 0\n const visible = getAvatarStackVisibleCount({\n availablePx: available,\n total: selectedNames.length,\n avatarPx: AVATAR_STACK_AVATAR_PX[size],\n overflowChipPx: AVATAR_STACK_OVERFLOW_CHIP_PX[size],\n })\n setStackVisibleCount(visible)\n }\n calc()\n const ro = new ResizeObserver(calc)\n ro.observe(root)\n return () => ro.disconnect()\n }, [isMulti, multiDisplay, selectedNames.length, size])\n // Merge ref:forward to parent + capture for ResizeObserver\n const mergedStackRef = React.useCallback((el: HTMLDivElement | null) => {\n stackContainerRef.current = el\n if (typeof ref === 'function') ref(el)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el\n }, [ref])\n\n return (\n <Combobox\n ref={mergedStackRef}\n size={size}\n variant={resolvedVariant}\n options={people.map(personToSelectOption)}\n value={selectedNames}\n onChange={handleMultiChange}\n searchable\n searchIn={searchIn}\n searchPlaceholder={searchPlaceholder}\n // 2026-05-12 Stream C Issue 4(codex Q3):placeholder = trigger empty('請選擇人員');emptyText = search-empty(僅 backward-compat forward 1 cycle)\n placeholder={placeholder}\n emptyPlaceholder={emptyText}\n wrap={false}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n // 2026-05-13 (a) fix(user 拍 Path a + Layer A density-drift root-cause):\n // 撤掉 `tagAreaPaddingLeftPx={8}` magic — Combobox `tagPadding[size]` 是 density-dependent\n // calc 公式(`(field-height - icon-size) / 2`),只在 md size + default density 才 = 4px;\n // 其他 size/density 漂 6px / 8px → 4+8=12 spec 公式不成立。\n // (a) fix:form context + 有 tag → 改 inject `!px-[var(--field-px)]`(固定 12px)直接 override `tagPadding[size]`,\n // 達成 GitHub PeoplePicker fixed 12px inset(對齊 cell context 同 13px from cell.left 含 1px border)。\n // - form + 有 tag → `!px-[var(--field-px)]`(12px 固定 inset)+ tagAreaPaddingLeftPx undefined → field.padL=12 ✓\n // - table-cell + 有 tag → naked variant `!px-[var(--table-cell-px)]` 已是 12px,不 inject ✓\n // - isEmpty → 不 inject,走 Combobox 預設文字 inset(`tagPadding[size]` 公式自然 vertical center)\n className={cn(className, !isEmpty && surface === 'form' && '!px-[var(--field-px)]')}\n aria-label={ariaLabel}\n // 2026-05-15 Bug 1 fix(Claude+Codex Step 5 比稿 consensus,user verbatim「就 A」):per-length 動態\n // wrapper class — length=1 降階單人視覺需要 width constraint chain(`flex-1 min-w-0 overflow-hidden`),\n // length>=2 stack 視覺保留 overlap(`-ml-0.5 first:ml-0 relative inline-flex group/avatar`)。對齊\n // spec.md §C row 1(length=1 = avatar+人名+ellipsis)+ §D row 1(length>=2 = stack overlap)。\n // 真根因:Combobox `OverflowTagList` 把所有 tagRenderer 結果包 `shrink-0`,加 `inline-flex` 後\n // wrapper 變 intrinsic content-width → PersonDisplay `w-full` resolves to intrinsic → truncate 無效。\n // 修法:length=1 wrapper 改 `flex-1 min-w-0 overflow-hidden` 提供 width constraint 給 PersonDisplay。\n // SSOT helper `getPeoplePickerTagWrapperClass(count)` 集中,future 改 wrapper 行為改一處。\n tagWrapperClassName={getPeoplePickerTagWrapperClass(selectedNames.length)}\n // 2026-05-16 真 root cause fix:overflow chip wrapper 套同 `-ml-0.5` 讓 chip 物理上\n // 跟 avatar 同 slot(等寬同 step,non-overlapping 多 24px 區塊不再 saw)。對齊 user\n // 「avatars 和 +N 都是同尺寸圓形,空間最多容固定數量圓形」物理模型 directive +\n // MUI AvatarGroup / Primer AvatarStack 共識(`AvatarGroup.js` L54-59 same negative margin)。\n overflowWrapperClassName=\"-ml-0.5 first:ml-0 relative inline-flex\"\n tagAreaGapPx={0}\n tagAreaPaddingLeftPx={undefined}\n // 2026-05-12 Round 7 fix(user 抓 image 2「+N tag 應該圓形不是矩形」+ 對齊 GitHub picker idiom):\n // Combobox default `overflowShape='tag'`(矩形 chip,文字 Combobox 慣例);PeoplePicker stack\n // mode pass `'circle'`(圓形 avatar-shape,跟 avatar 一樣大)。對齊 MultiPersonDisplay readonly path\n // 既有 OverflowIndicator default 'circle' SSOT。\n overflowShape=\"circle\"\n // 2026-05-15 Bug 3 fix:formula-based visible count override(避免 Combobox DOM measurement +\n // 60px fallback 不 deterministic)。SSOT in `./avatar-stack-overflow.ts`,display + edit 共用。\n visibleCountOverride={stackVisibleCount}\n // 2026-05-14 I4 fix(per codex+Layer A 共識):hidden items 在 `+N` overflow popover 顯\n // Tag with avatar(對齊 display MultiPersonDisplay popover SSOT,user 抓 display vs edit\n // overflow 視覺不一致)。\n renderHiddenTag={(item) => {\n const p = resolvePerson(findPerson(people, item.value))\n return (\n <Tag\n key={item.value}\n color=\"neutral\"\n size=\"sm\"\n avatar={\n <Avatar\n src={p.avatarUrl}\n alt={p.name}\n size={16}\n hoverCard={buildPersonProfileCard(p)}\n />\n }\n onRemove={() => {\n onChange?.(selectedNames.filter(n => n !== item.value).map(n => findPerson(people, n)))\n }}\n >\n {p.name}\n </Tag>\n )\n }}\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // any-allow: rest 含 `onChange: FormEventHandler` 跟 Combobox onChange signature 衝突 — DOM runtime spread 安全(per codex P2 forward)\n {...(rest as any)}\n tagRenderer={(item, onRemove) => {\n const p = resolvePerson(findPerson(people, item.value))\n // 2026-05-12 Q2 fix(user 拍板「multi 只選 1 人時 trigger = avatar + name,跟 single mode 同」):\n // selectedNames.length === 1 → PersonDisplay(avatar + name)代替 PersonAvatarTag(avatar only)。\n // SSOT 對齊 PeoplePicker single mode line 201 selectedItemRenderer。多選 1 人時視覺等同單選,\n // 只在 length > 1 才走 stack(各 avatar 純 chip)。多選 + inline 搜尋場景拿掉 name 改 cursor\n // 走 `searchIn='trigger'` opt-in(2026-05-12 規則 3 ship,已轉傳 Combobox;default 'menu' 走 panel-top search)。\n if (selectedNames.length === 1) {\n return <PersonDisplay key={item.value} value={p} size={size} />\n }\n return (\n <PersonAvatarTag\n key={item.value}\n person={p}\n size={size}\n onRemove={onRemove}\n />\n )\n }}\n />\n )\n})\nPeoplePicker.displayName = 'PeoplePicker'\n\n// Story auto-compile metadata\nexport const peoplePickerMeta = {\n component: 'PeoplePicker',\n family: 4,\n variants: {},\n sizes: {},\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-fg-disabled', 'text-fg-muted'],\n ring: [],\n },\n} as const\n\nexport { PeoplePicker }\n"],"names":["PeoplePicker","handleMultiChange"],"mappings":"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"}
|
|
@@ -80,7 +80,7 @@ export declare const nameCardMeta: {
|
|
|
80
80
|
readonly family: null;
|
|
81
81
|
readonly variants: {};
|
|
82
82
|
readonly sizes: {};
|
|
83
|
-
readonly states: readonly [
|
|
83
|
+
readonly states: readonly [];
|
|
84
84
|
readonly tokens: {
|
|
85
85
|
readonly bg: readonly ["bg-muted"];
|
|
86
86
|
readonly fg: readonly ["text-foreground"];
|
|
@@ -137,7 +137,8 @@ const nameCardMeta = {
|
|
|
137
137
|
// non-family composite / overlay / layout
|
|
138
138
|
variants: {},
|
|
139
139
|
sizes: {},
|
|
140
|
-
states: [
|
|
140
|
+
states: [],
|
|
141
|
+
// 唯讀 person-info template,無自身互動 state(hover/active/focus/disabled 屬子 Button/Avatar)
|
|
141
142
|
tokens: {
|
|
142
143
|
bg: ["bg-muted"],
|
|
143
144
|
fg: ["text-foreground"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-card.js","sources":["../../../src/components/ProfileCard/profile-card.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 PeoplePicker(person-display)wrap 消費,end-user app 請用 wrapper 元件。\n */\n// @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 { MessageCircle, Phone, ChevronDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar, type AvatarData } from '@/design-system/components/Avatar/avatar'\nimport { Button } from '@/design-system/components/Button/button'\nimport { DescriptionList, DescriptionItem } from '@/design-system/components/DescriptionList/description-list'\nimport { ScrollArea } from '@/design-system/components/ScrollArea/scroll-area'\nimport { ItemContent } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * ProfileCardDefaultActions — canonical 預設 action 組合\n *\n * ── 為什麼需要 export ──\n * ProfileCard 的 action 列在世界級 chat/contact app 是「關係型快速動作」的 canonical\n * (Slack / iMessage / LinkedIn / Figma 皆如此):**Chat / Message + Audio call**。\n * 跨 consumer(avatar.principles / profile-card.stories / future product code)應該用\n * 同一組預設,避免每個範例各自發明 action 組合,讓 reader 誤以為 action 會隨情境自動變。\n *\n * ── 使用方式(hover context 必含 onViewMore,見 profile-card.spec.md 「View more」節)──\n * <ProfileCard name=\"...\" actions={<ProfileCardDefaultActions />} onViewMore={...} />\n *\n * ── 何時要換成自訂 action ──\n * - Single-action 情境(只要「傳訊息」)→ consumer 傳 `<Button>傳訊息</Button>`\n * - 特定情境的 action(管理員「撤銷邀請」/ HR「離職管理」)→ consumer 自訂\n * - 非人員關係動作(「訂購此商品」)→ 根本不應該用 ProfileCard\n *\n * Chat + Audio call 是 **default**,不是 **only**——consumer 可覆寫,但需有明確理由。\n */\nexport const ProfileCardDefaultActions = () => (\n <>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={MessageCircle}>Chat</Button>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={Phone} endIcon={ChevronDown}>Audio call</Button>\n </>\n)\n\n/**\n * ProfileCard — 人員 HoverCard 的內容元件\n *\n * ── Status 對齊 Avatar presence canonical(2026-04-20) ──\n * status type = `online | away | busy | offline`,跟 Avatar 的 presence prop 對齊\n * (世界級 Slack / Teams / Discord term);dot 色走 `--status-*` semantic token\n * (獨立於 success/error/warning,避免語義衝突)。\n *\n * ── Avatar 對齊 ──\n * 跟 FileItem rich 統一:右側 text column 用 justify-center + minHeight=avatar。\n * 短文字置中於 avatar,長文字(多行名字)自然撐高。\n *\n * ── Info fields ──\n * Status message / ID / Employee number 等唯讀屬性全部用 DescriptionList 家族\n * 承載(不手刻 dt/dd),canonical 由 DS primitive own。\n */\n\nconst AVATAR_SIZE = 64\n\ntype StatusType = 'online' | 'away' | 'busy' | 'offline'\n\n// Presence semantic tokens(見 color/semantic.css)——跟 Avatar status dot 共用\nconst STATUS_DOT_COLOR: Record<StatusType, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\nconst STATUS_LABEL: Record<StatusType, string> = {\n online: 'Online',\n away: 'Away',\n busy: 'Busy',\n offline: 'Offline',\n}\n\n/**\n * ProfileCard SSOT — 預設 field keys(現行 v15.7:default 只 `id` + `employeeNumber`;\n * v11 always-render 為歷史脈絡,已退役 — 見下方 2026-05-07 註解 + render 處 v12 註解)\n *\n * ── 為什麼 SSOT ──\n * User explicit rule:「所有 namecard 預設顯示的資訊都是要一樣完整的」。前 v10 props\n * 全 optional + body conditional render → consumer 漏傳 fields 視覺缺 section,每個範例\n * 各自不一致(同 person 在 DataTable / PeoplePicker / avatar.principles 看起來不同)。\n *\n * v11 canonical(歷史):ProfileCard 當時 always renders 4 default sections — 缺 data → 顯\n * `EMPTY_PLACEHOLDER`(\"—\")。現行:default fields 縮為 2(v15.7)+ status 條件 render(v12),\n * 「視覺結構 SSOT 在此元件,不依賴 consumer」原則不變。\n *\n * ── 對齊 world-class ──\n * Slack profile card / Linear member card / Notion person card / GitHub user card / Figma user card\n * 都是 fixed schema(role / email / location / department / department / pronouns 等),\n * 不會因為某 user 沒填 phone 整個 phone field 不見 — 缺 = 顯 `Not set` 或留白。\n *\n * ── 為什麼 placeholder 不 hide ──\n * Hide → consumer 不知道少傳 → 視覺漂移;Placeholder → 永遠看到「該欄該有」+ dev-warn 提示\n * consumer 補資料,自動防漂移(M19 ensure-canonical 對齊)。\n */\n// **2026-05-07 v15.7 user directive**:default render 只 `id` + `employeeNumber` 兩個。\n// Email / Phone / Department / Location 等其他 description 一律 opt-in by consumer 透過\n// `fields` array prop。對齊 user 明確「應該確保所有都只有這兩個,因為我並沒有要求你要選其他的」。\nexport const NAMECARD_DEFAULT_FIELD_KEYS = ['id', 'employeeNumber'] as const\ntype ProfileCardDefaultFieldKey = typeof NAMECARD_DEFAULT_FIELD_KEYS[number]\n\nconst DEFAULT_FIELD_LABEL: Record<ProfileCardDefaultFieldKey, string> = {\n id: 'ID',\n employeeNumber: 'Employee number',\n}\n\nconst FIELD_PLACEHOLDER = '—'\n\nexport interface ProfileCardProps extends React.HTMLAttributes<HTMLDivElement> {\n name: string\n avatar?: AvatarData\n subtitle?: string\n status?: StatusType\n statusMessage?: React.ReactNode\n actions?: React.ReactNode\n /**\n * Consumer 傳的 field 資料(partial)。預設 keys 走 `NAMECARD_DEFAULT_FIELD_KEYS` —\n * 只有 id / employeeNumber 兩個 default field 永遠 render(缺資料顯 `—`);email / phone /\n * department / location 等其他欄位一律 opt-in by consumer 透過本 prop 傳入。Consumer 想新增\n * 自訂 field 直接傳入(在 default 之後 append),想 override default key value 也直接傳。\n */\n fields?: { label: string; value: React.ReactNode }[]\n /**\n * Default field 的真實值。Object key = NAMECARD_DEFAULT_FIELD_KEYS 之一。\n * 缺 key → render placeholder。Dev mode 會 console.warn 提醒消費者補資料。\n */\n defaultFieldValues?: Partial<Record<ProfileCardDefaultFieldKey, React.ReactNode>>\n onViewMore?: () => void\n viewMoreLabel?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst ProfileCard = React.forwardRef<HTMLDivElement, ProfileCardProps>(\n (\n {\n name,\n avatar,\n subtitle,\n status,\n statusMessage,\n actions,\n fields,\n defaultFieldValues,\n onViewMore,\n viewMoreLabel = 'View more',\n className,\n ...props\n },\n ref,\n ) => {\n // v12 canonical:default fields 永遠 render(缺資料顯 placeholder),consumer 自訂\n // fields 在 default 之後 append。Status section 為條件 render(status undefined →\n // 整 block 隱藏,見下方 render 處)— v11「永遠 render + placeholder」已退役。\n //\n // **Dedup canonical(2026-05-07 v15.8 fix Bug E)**:consumer 的 `fields` array 若含\n // label 撞 default(eg. 「ID」「Employee number」),consumer 值 win — defaults 那一行\n // 跳過(否則 same label 連 render 兩次,如 default placeholder `—` + consumer 真值)。\n // 這是遷移期 forgiving 行為:DEV warn 提示應改用 `defaultFieldValues`,但 production\n // 不破壞既有 consumer。對齊 React `key` 唯一性 + Linear / Slack profile card 一 label\n // 一 row idiom。\n const allFields = React.useMemo(() => {\n const consumerLabels = new Set((fields ?? []).map((f) => f.label))\n const defaults = NAMECARD_DEFAULT_FIELD_KEYS\n .map((key) => ({\n label: DEFAULT_FIELD_LABEL[key],\n value: defaultFieldValues?.[key] ?? FIELD_PLACEHOLDER,\n }))\n .filter((d) => !consumerLabels.has(d.label))\n return fields && fields.length > 0 ? [...defaults, ...fields] : defaults\n }, [defaultFieldValues, fields])\n\n // Dev warn:consumer 透過 `fields` 傳 default key label(legacy pattern)→ 應改 `defaultFieldValues`\n if (process.env.NODE_ENV !== 'production' && fields) {\n const legacyEntry = fields.find((f) =>\n Object.values(DEFAULT_FIELD_LABEL).includes(f.label as string),\n )\n if (legacyEntry) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":legacy pattern — fields[].label=\"${legacyEntry.label}\" ` +\n `is a default field. Migrate to defaultFieldValues={{ id, employeeNumber }} prop ` +\n `to align with NAMECARD_DEFAULT_FIELD_KEYS canonical.`,\n )\n }\n }\n\n // Dev mode warn:consumer 沒傳 default field 任何 key → 提示補完(避免漂移成 placeholder-only)\n if (process.env.NODE_ENV !== 'production' && !defaultFieldValues) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":no defaultFieldValues passed — sections will render placeholders. ` +\n `Pass at least { id, employeeNumber } via defaultFieldValues prop. ` +\n `For other description items (email/phone/department/location etc),use \\`fields\\` prop array.`,\n )\n }\n\n // Layout canonical(2026-04-23):Header + Actions 固定上,Body(status + fields)可捲動,\n // View more 固定下。**ProfileCard 自己約束高度**,不依賴 consumer HoverCardContent 設 flex:\n // - `max-h-[var(--radix-hover-card-content-available-height,...)]`:HoverCard / Popover\n // context 自動繼承 Radix viewport-aware 變數;standalone 落到 100vh fallback\n // - 內部 `flex flex-col + overflow-hidden`:Header(shrink-0)+ Body(flex-1 min-h-0 ScrollArea)\n // + Footer(shrink-0)三層 chrome\n // 世界級對照:Slack / Linear / GitHub / Notion hover-profile popover 皆此 chrome pattern。\n return (\n <div\n ref={ref}\n className={cn(\n 'w-[320px] flex flex-col overflow-hidden',\n 'max-h-[var(--radix-hover-card-content-available-height,var(--radix-popover-content-available-height,100vh))]',\n className,\n )}\n {...props}\n >\n {/* ── HEADER(固定): profile + actions ── */}\n <div className=\"shrink-0 flex flex-col\">\n <div className=\"flex items-start gap-3 px-4 py-3\">\n <Avatar\n src={avatar?.src}\n alt={avatar?.alt ?? name}\n color={avatar?.color}\n size={AVATAR_SIZE}\n status={status}\n className=\"shrink-0\"\n />\n {/* ProfileCard typography:label body-lg(16/1.5) + desc body(14/1.5) = reading mode + size=\"lg\"。\n labelClassName escape hatch 加 font-medium(card context 語意)+ labelTruncate=false 允許 wrap。 */}\n <ItemContent\n label={name}\n description={subtitle}\n mode=\"reading\"\n size=\"lg\"\n labelTruncate={false}\n labelClassName=\"text-body-lg font-medium text-foreground\"\n className=\"justify-center\"\n style={{ minHeight: AVATAR_SIZE }}\n />\n </div>\n\n {/* Action buttons — 均分空間 + 填滿格子(canonical):多個 action 等寬瓜分容器,\n 單一 action 也撐滿容器。`grid grid-flow-col auto-cols-fr` + `[&>*]:w-full`。\n 世界級對照:iOS contact card / macOS contact / LinkedIn profile card 的 action row。 */}\n {actions && (\n <div className=\"grid grid-flow-col auto-cols-fr gap-2 px-4 pb-3 [&>*]:w-full\">\n {actions}\n </div>\n )}\n </div>\n\n {/* ── BODY(可捲動,v12 status-conditional 2026-05-14):status + fields ──\n **v12 rule**(per user 拍板「不應該顯示『狀態沒有被設定』,production 每 user 一定有\n presence state,undefined 頂多是 loading transient 還沒讀到」):status undefined →\n 隱藏整 status badge + status message block(loading 期間 skip),禁 render「Status not\n set」這種 placeholder(語義錯,user presence 不會「沒設定」)。**ProfileCard-specific 不外推\n 至 DS 其他元件**(FileItem / DescriptionList / DataTable cell 各自 placeholder 邏輯\n unrelated)。Fields section 仍 always-render(info schema 性質)。 */}\n <ScrollArea className=\"flex-1 min-h-0 border-t border-divider\">\n {/* Status section:`status` defined 才 render(v12 conditional canonical) */}\n {status && (\n <div className=\"px-4 py-3 flex flex-col gap-3\">\n <div className=\"flex items-center gap-2 px-3 py-2 bg-muted rounded-md\">\n <span\n className=\"w-2.5 h-2.5 rounded-full shrink-0\"\n style={{ backgroundColor: STATUS_DOT_COLOR[status] }}\n />\n <span className=\"text-body\">{STATUS_LABEL[status]}</span>\n </div>\n {/* Status message — 只在 status defined 才 render(語意配對:status badge + status\n message 是一組;沒 status 就沒 status message)。缺 statusMessage 顯 placeholder。 */}\n <DescriptionList>\n <DescriptionItem label=\"Status message\">\n {statusMessage ?? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>}\n </DescriptionItem>\n </DescriptionList>\n </div>\n )}\n\n {/* Fields section:status defined 才有 border-t separator;無 status section 時去除 border-t(因 ScrollArea 起點已有上方 border-t,不重疊) */}\n <div className={cn('px-4 py-3', status && 'border-t border-divider')}>\n <DescriptionList cols={2}>\n {allFields.map((f) => (\n <DescriptionItem key={f.label} label={f.label}>\n {f.value === FIELD_PLACEHOLDER\n ? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>\n : f.value}\n </DescriptionItem>\n ))}\n </DescriptionList>\n </div>\n </ScrollArea>\n\n {/* ── FOOTER(固定): View more,py-3 canonical(12px,比一般 link 按鈕多呼吸) ── */}\n {onViewMore && (\n <div className=\"shrink-0 border-t border-divider px-4 py-3\">\n <Button variant=\"link\" size=\"sm\" onClick={onViewMore} className=\"w-full\">{viewMoreLabel}</Button>\n </div>\n )}\n </div>\n )\n },\n)\nProfileCard.displayName = 'ProfileCard'\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 nameCardMeta = {\n component: 'ProfileCard',\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-muted'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { ProfileCard }\n"],"names":[],"mappings":";;;;;;;;;AAiCO,MAAM,4BAA4B,MACvC,qBAAA,UAAA,EACE,UAAA;AAAA,EAAA,oBAAC,UAAO,SAAQ,YAAW,MAAK,MAAK,WAAW,eAAe,UAAA,OAAA,CAAI;AAAA,EACnE,oBAAC,QAAA,EAAO,SAAQ,YAAW,MAAK,MAAK,WAAW,OAAO,SAAS,aAAa,UAAA,aAAA,CAAU;AAAA,EAAA,CACzF;AAoBF,MAAM,cAAc;AAKpB,MAAM,mBAA+C;AAAA,EACnD,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AACX;AAEA,MAAM,eAA2C;AAAA,EAC/C,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AACX;AA2BO,MAAM,8BAA8B,CAAC,MAAM,gBAAgB;AAGlE,MAAM,sBAAkE;AAAA,EACtE,IAAI;AAAA,EACJ,gBAAgB;AAClB;AAEA,MAAM,oBAAoB;AA0B1B,MAAM,cAAc,MAAM;AAAA,EACxB,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AAWH,UAAM,YAAY,MAAM,QAAQ,MAAM;AACpC,YAAM,iBAAiB,IAAI,KAAK,UAAU,CAAA,GAAI,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACjE,YAAM,WAAW,4BACd,IAAI,CAAC,SAAS;AAAA,QACb,OAAO,oBAAoB,GAAG;AAAA,QAC9B,QAAO,yDAAqB,SAAQ;AAAA,MAAA,EACpC,EACD,OAAO,CAAC,MAAM,CAAC,eAAe,IAAI,EAAE,KAAK,CAAC;AAC7C,aAAO,UAAU,OAAO,SAAS,IAAI,CAAC,GAAG,UAAU,GAAG,MAAM,IAAI;AAAA,IAClE,GAAG,CAAC,oBAAoB,MAAM,CAAC;AAG/B,QAAI,QAAQ,IAAI,aAAa,gBAAgB,QAAQ;AACnD,YAAM,cAAc,OAAO;AAAA,QAAK,CAAC,MAC/B,OAAO,OAAO,mBAAmB,EAAE,SAAS,EAAE,KAAe;AAAA,MAAA;AAE/D,UAAI,aAAa;AAEf,gBAAQ;AAAA,UACN,kBAAkB,IAAI,sCAAsC,YAAY,KAAK;AAAA,QAAA;AAAA,MAIjF;AAAA,IACF;AAGA,QAAI,QAAQ,IAAI,aAAa,gBAAgB,CAAC,oBAAoB;AAEhE,cAAQ;AAAA,QACN,kBAAkB,IAAI;AAAA,MAAA;AAAA,IAI1B;AASA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAGJ,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,WAAU,oCACb,UAAA;AAAA,cAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,KAAK,iCAAQ;AAAA,kBACb,MAAK,iCAAQ,QAAO;AAAA,kBACpB,OAAO,iCAAQ;AAAA,kBACf,MAAM;AAAA,kBACN;AAAA,kBACA,WAAU;AAAA,gBAAA;AAAA,cAAA;AAAA,cAIZ;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,OAAO;AAAA,kBACP,aAAa;AAAA,kBACb,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,eAAe;AAAA,kBACf,gBAAe;AAAA,kBACf,WAAU;AAAA,kBACV,OAAO,EAAE,WAAW,YAAA;AAAA,gBAAY;AAAA,cAAA;AAAA,YAClC,GACF;AAAA,YAKC,WACC,oBAAC,OAAA,EAAI,WAAU,gEACZ,UAAA,QAAA,CACH;AAAA,UAAA,GAEJ;AAAA,UASA,qBAAC,YAAA,EAAW,WAAU,0CAEnB,UAAA;AAAA,YAAA,UACC,qBAAC,OAAA,EAAI,WAAU,iCACb,UAAA;AAAA,cAAA,qBAAC,OAAA,EAAI,WAAU,yDACb,UAAA;AAAA,gBAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,WAAU;AAAA,oBACV,OAAO,EAAE,iBAAiB,iBAAiB,MAAM,EAAA;AAAA,kBAAE;AAAA,gBAAA;AAAA,oCAEpD,QAAA,EAAK,WAAU,aAAa,UAAA,aAAa,MAAM,EAAA,CAAE;AAAA,cAAA,GACpD;AAAA,cAGA,oBAAC,iBAAA,EACC,UAAA,oBAAC,iBAAA,EAAgB,OAAM,kBACpB,UAAA,iBAAiB,oBAAC,QAAA,EAAK,WAAU,iBAAiB,UAAA,kBAAA,CAAkB,GACvE,EAAA,CACF;AAAA,YAAA,GACF;AAAA,gCAID,OAAA,EAAI,WAAW,GAAG,aAAa,UAAU,yBAAyB,GACjE,UAAA,oBAAC,iBAAA,EAAgB,MAAM,GACpB,UAAA,UAAU,IAAI,CAAC,MACd,oBAAC,iBAAA,EAA8B,OAAO,EAAE,OACrC,UAAA,EAAE,UAAU,oBACT,oBAAC,UAAK,WAAU,iBAAiB,UAAA,mBAAkB,IACnD,EAAE,MAAA,GAHc,EAAE,KAIxB,CACD,GACH,EAAA,CACF;AAAA,UAAA,GACF;AAAA,UAGC,cACC,oBAAC,OAAA,EAAI,WAAU,8CACb,8BAAC,QAAA,EAAO,SAAQ,QAAO,MAAK,MAAK,SAAS,YAAY,WAAU,UAAU,yBAAc,EAAA,CAC1F;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AACA,YAAY,cAAc;AAInB,MAAM,eAAe;AAAA,EAC1B,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,UAAU;AAAA,IACf,IAAI,CAAC,iBAAiB;AAAA,IACtB,MAAM,CAAA;AAAA,EAAC;AAEX;"}
|
|
1
|
+
{"version":3,"file":"profile-card.js","sources":["../../../src/components/ProfileCard/profile-card.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 PeoplePicker(person-display)wrap 消費,end-user app 請用 wrapper 元件。\n */\n// @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 { MessageCircle, Phone, ChevronDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar, type AvatarData } from '@/design-system/components/Avatar/avatar'\nimport { Button } from '@/design-system/components/Button/button'\nimport { DescriptionList, DescriptionItem } from '@/design-system/components/DescriptionList/description-list'\nimport { ScrollArea } from '@/design-system/components/ScrollArea/scroll-area'\nimport { ItemContent } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * ProfileCardDefaultActions — canonical 預設 action 組合\n *\n * ── 為什麼需要 export ──\n * ProfileCard 的 action 列在世界級 chat/contact app 是「關係型快速動作」的 canonical\n * (Slack / iMessage / LinkedIn / Figma 皆如此):**Chat / Message + Audio call**。\n * 跨 consumer(avatar.principles / profile-card.stories / future product code)應該用\n * 同一組預設,避免每個範例各自發明 action 組合,讓 reader 誤以為 action 會隨情境自動變。\n *\n * ── 使用方式(hover context 必含 onViewMore,見 profile-card.spec.md 「View more」節)──\n * <ProfileCard name=\"...\" actions={<ProfileCardDefaultActions />} onViewMore={...} />\n *\n * ── 何時要換成自訂 action ──\n * - Single-action 情境(只要「傳訊息」)→ consumer 傳 `<Button>傳訊息</Button>`\n * - 特定情境的 action(管理員「撤銷邀請」/ HR「離職管理」)→ consumer 自訂\n * - 非人員關係動作(「訂購此商品」)→ 根本不應該用 ProfileCard\n *\n * Chat + Audio call 是 **default**,不是 **only**——consumer 可覆寫,但需有明確理由。\n */\nexport const ProfileCardDefaultActions = () => (\n <>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={MessageCircle}>Chat</Button>\n <Button variant=\"tertiary\" size=\"sm\" startIcon={Phone} endIcon={ChevronDown}>Audio call</Button>\n </>\n)\n\n/**\n * ProfileCard — 人員 HoverCard 的內容元件\n *\n * ── Status 對齊 Avatar presence canonical(2026-04-20) ──\n * status type = `online | away | busy | offline`,跟 Avatar 的 presence prop 對齊\n * (世界級 Slack / Teams / Discord term);dot 色走 `--status-*` semantic token\n * (獨立於 success/error/warning,避免語義衝突)。\n *\n * ── Avatar 對齊 ──\n * 跟 FileItem rich 統一:右側 text column 用 justify-center + minHeight=avatar。\n * 短文字置中於 avatar,長文字(多行名字)自然撐高。\n *\n * ── Info fields ──\n * Status message / ID / Employee number 等唯讀屬性全部用 DescriptionList 家族\n * 承載(不手刻 dt/dd),canonical 由 DS primitive own。\n */\n\nconst AVATAR_SIZE = 64\n\ntype StatusType = 'online' | 'away' | 'busy' | 'offline'\n\n// Presence semantic tokens(見 color/semantic.css)——跟 Avatar status dot 共用\nconst STATUS_DOT_COLOR: Record<StatusType, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\nconst STATUS_LABEL: Record<StatusType, string> = {\n online: 'Online',\n away: 'Away',\n busy: 'Busy',\n offline: 'Offline',\n}\n\n/**\n * ProfileCard SSOT — 預設 field keys(現行 v15.7:default 只 `id` + `employeeNumber`;\n * v11 always-render 為歷史脈絡,已退役 — 見下方 2026-05-07 註解 + render 處 v12 註解)\n *\n * ── 為什麼 SSOT ──\n * User explicit rule:「所有 namecard 預設顯示的資訊都是要一樣完整的」。前 v10 props\n * 全 optional + body conditional render → consumer 漏傳 fields 視覺缺 section,每個範例\n * 各自不一致(同 person 在 DataTable / PeoplePicker / avatar.principles 看起來不同)。\n *\n * v11 canonical(歷史):ProfileCard 當時 always renders 4 default sections — 缺 data → 顯\n * `EMPTY_PLACEHOLDER`(\"—\")。現行:default fields 縮為 2(v15.7)+ status 條件 render(v12),\n * 「視覺結構 SSOT 在此元件,不依賴 consumer」原則不變。\n *\n * ── 對齊 world-class ──\n * Slack profile card / Linear member card / Notion person card / GitHub user card / Figma user card\n * 都是 fixed schema(role / email / location / department / department / pronouns 等),\n * 不會因為某 user 沒填 phone 整個 phone field 不見 — 缺 = 顯 `Not set` 或留白。\n *\n * ── 為什麼 placeholder 不 hide ──\n * Hide → consumer 不知道少傳 → 視覺漂移;Placeholder → 永遠看到「該欄該有」+ dev-warn 提示\n * consumer 補資料,自動防漂移(M19 ensure-canonical 對齊)。\n */\n// **2026-05-07 v15.7 user directive**:default render 只 `id` + `employeeNumber` 兩個。\n// Email / Phone / Department / Location 等其他 description 一律 opt-in by consumer 透過\n// `fields` array prop。對齊 user 明確「應該確保所有都只有這兩個,因為我並沒有要求你要選其他的」。\nexport const NAMECARD_DEFAULT_FIELD_KEYS = ['id', 'employeeNumber'] as const\ntype ProfileCardDefaultFieldKey = typeof NAMECARD_DEFAULT_FIELD_KEYS[number]\n\nconst DEFAULT_FIELD_LABEL: Record<ProfileCardDefaultFieldKey, string> = {\n id: 'ID',\n employeeNumber: 'Employee number',\n}\n\nconst FIELD_PLACEHOLDER = '—'\n\nexport interface ProfileCardProps extends React.HTMLAttributes<HTMLDivElement> {\n name: string\n avatar?: AvatarData\n subtitle?: string\n status?: StatusType\n statusMessage?: React.ReactNode\n actions?: React.ReactNode\n /**\n * Consumer 傳的 field 資料(partial)。預設 keys 走 `NAMECARD_DEFAULT_FIELD_KEYS` —\n * 只有 id / employeeNumber 兩個 default field 永遠 render(缺資料顯 `—`);email / phone /\n * department / location 等其他欄位一律 opt-in by consumer 透過本 prop 傳入。Consumer 想新增\n * 自訂 field 直接傳入(在 default 之後 append),想 override default key value 也直接傳。\n */\n fields?: { label: string; value: React.ReactNode }[]\n /**\n * Default field 的真實值。Object key = NAMECARD_DEFAULT_FIELD_KEYS 之一。\n * 缺 key → render placeholder。Dev mode 會 console.warn 提醒消費者補資料。\n */\n defaultFieldValues?: Partial<Record<ProfileCardDefaultFieldKey, React.ReactNode>>\n onViewMore?: () => void\n viewMoreLabel?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst ProfileCard = React.forwardRef<HTMLDivElement, ProfileCardProps>(\n (\n {\n name,\n avatar,\n subtitle,\n status,\n statusMessage,\n actions,\n fields,\n defaultFieldValues,\n onViewMore,\n viewMoreLabel = 'View more',\n className,\n ...props\n },\n ref,\n ) => {\n // v12 canonical:default fields 永遠 render(缺資料顯 placeholder),consumer 自訂\n // fields 在 default 之後 append。Status section 為條件 render(status undefined →\n // 整 block 隱藏,見下方 render 處)— v11「永遠 render + placeholder」已退役。\n //\n // **Dedup canonical(2026-05-07 v15.8 fix Bug E)**:consumer 的 `fields` array 若含\n // label 撞 default(eg. 「ID」「Employee number」),consumer 值 win — defaults 那一行\n // 跳過(否則 same label 連 render 兩次,如 default placeholder `—` + consumer 真值)。\n // 這是遷移期 forgiving 行為:DEV warn 提示應改用 `defaultFieldValues`,但 production\n // 不破壞既有 consumer。對齊 React `key` 唯一性 + Linear / Slack profile card 一 label\n // 一 row idiom。\n const allFields = React.useMemo(() => {\n const consumerLabels = new Set((fields ?? []).map((f) => f.label))\n const defaults = NAMECARD_DEFAULT_FIELD_KEYS\n .map((key) => ({\n label: DEFAULT_FIELD_LABEL[key],\n value: defaultFieldValues?.[key] ?? FIELD_PLACEHOLDER,\n }))\n .filter((d) => !consumerLabels.has(d.label))\n return fields && fields.length > 0 ? [...defaults, ...fields] : defaults\n }, [defaultFieldValues, fields])\n\n // Dev warn:consumer 透過 `fields` 傳 default key label(legacy pattern)→ 應改 `defaultFieldValues`\n if (process.env.NODE_ENV !== 'production' && fields) {\n const legacyEntry = fields.find((f) =>\n Object.values(DEFAULT_FIELD_LABEL).includes(f.label as string),\n )\n if (legacyEntry) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":legacy pattern — fields[].label=\"${legacyEntry.label}\" ` +\n `is a default field. Migrate to defaultFieldValues={{ id, employeeNumber }} prop ` +\n `to align with NAMECARD_DEFAULT_FIELD_KEYS canonical.`,\n )\n }\n }\n\n // Dev mode warn:consumer 沒傳 default field 任何 key → 提示補完(避免漂移成 placeholder-only)\n if (process.env.NODE_ENV !== 'production' && !defaultFieldValues) {\n // eslint-disable-next-line no-console\n console.warn(\n `[ProfileCard] \"${name}\":no defaultFieldValues passed — sections will render placeholders. ` +\n `Pass at least { id, employeeNumber } via defaultFieldValues prop. ` +\n `For other description items (email/phone/department/location etc),use \\`fields\\` prop array.`,\n )\n }\n\n // Layout canonical(2026-04-23):Header + Actions 固定上,Body(status + fields)可捲動,\n // View more 固定下。**ProfileCard 自己約束高度**,不依賴 consumer HoverCardContent 設 flex:\n // - `max-h-[var(--radix-hover-card-content-available-height,...)]`:HoverCard / Popover\n // context 自動繼承 Radix viewport-aware 變數;standalone 落到 100vh fallback\n // - 內部 `flex flex-col + overflow-hidden`:Header(shrink-0)+ Body(flex-1 min-h-0 ScrollArea)\n // + Footer(shrink-0)三層 chrome\n // 世界級對照:Slack / Linear / GitHub / Notion hover-profile popover 皆此 chrome pattern。\n return (\n <div\n ref={ref}\n className={cn(\n 'w-[320px] flex flex-col overflow-hidden',\n 'max-h-[var(--radix-hover-card-content-available-height,var(--radix-popover-content-available-height,100vh))]',\n className,\n )}\n {...props}\n >\n {/* ── HEADER(固定): profile + actions ── */}\n <div className=\"shrink-0 flex flex-col\">\n <div className=\"flex items-start gap-3 px-4 py-3\">\n <Avatar\n src={avatar?.src}\n alt={avatar?.alt ?? name}\n color={avatar?.color}\n size={AVATAR_SIZE}\n status={status}\n className=\"shrink-0\"\n />\n {/* ProfileCard typography:label body-lg(16/1.5) + desc body(14/1.5) = reading mode + size=\"lg\"。\n labelClassName escape hatch 加 font-medium(card context 語意)+ labelTruncate=false 允許 wrap。 */}\n <ItemContent\n label={name}\n description={subtitle}\n mode=\"reading\"\n size=\"lg\"\n labelTruncate={false}\n labelClassName=\"text-body-lg font-medium text-foreground\"\n className=\"justify-center\"\n style={{ minHeight: AVATAR_SIZE }}\n />\n </div>\n\n {/* Action buttons — 均分空間 + 填滿格子(canonical):多個 action 等寬瓜分容器,\n 單一 action 也撐滿容器。`grid grid-flow-col auto-cols-fr` + `[&>*]:w-full`。\n 世界級對照:iOS contact card / macOS contact / LinkedIn profile card 的 action row。 */}\n {actions && (\n <div className=\"grid grid-flow-col auto-cols-fr gap-2 px-4 pb-3 [&>*]:w-full\">\n {actions}\n </div>\n )}\n </div>\n\n {/* ── BODY(可捲動,v12 status-conditional 2026-05-14):status + fields ──\n **v12 rule**(per user 拍板「不應該顯示『狀態沒有被設定』,production 每 user 一定有\n presence state,undefined 頂多是 loading transient 還沒讀到」):status undefined →\n 隱藏整 status badge + status message block(loading 期間 skip),禁 render「Status not\n set」這種 placeholder(語義錯,user presence 不會「沒設定」)。**ProfileCard-specific 不外推\n 至 DS 其他元件**(FileItem / DescriptionList / DataTable cell 各自 placeholder 邏輯\n unrelated)。Fields section 仍 always-render(info schema 性質)。 */}\n <ScrollArea className=\"flex-1 min-h-0 border-t border-divider\">\n {/* Status section:`status` defined 才 render(v12 conditional canonical) */}\n {status && (\n <div className=\"px-4 py-3 flex flex-col gap-3\">\n <div className=\"flex items-center gap-2 px-3 py-2 bg-muted rounded-md\">\n <span\n className=\"w-2.5 h-2.5 rounded-full shrink-0\"\n style={{ backgroundColor: STATUS_DOT_COLOR[status] }}\n />\n <span className=\"text-body\">{STATUS_LABEL[status]}</span>\n </div>\n {/* Status message — 只在 status defined 才 render(語意配對:status badge + status\n message 是一組;沒 status 就沒 status message)。缺 statusMessage 顯 placeholder。 */}\n <DescriptionList>\n <DescriptionItem label=\"Status message\">\n {statusMessage ?? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>}\n </DescriptionItem>\n </DescriptionList>\n </div>\n )}\n\n {/* Fields section:status defined 才有 border-t separator;無 status section 時去除 border-t(因 ScrollArea 起點已有上方 border-t,不重疊) */}\n <div className={cn('px-4 py-3', status && 'border-t border-divider')}>\n <DescriptionList cols={2}>\n {allFields.map((f) => (\n <DescriptionItem key={f.label} label={f.label}>\n {f.value === FIELD_PLACEHOLDER\n ? <span className=\"text-fg-muted\">{FIELD_PLACEHOLDER}</span>\n : f.value}\n </DescriptionItem>\n ))}\n </DescriptionList>\n </div>\n </ScrollArea>\n\n {/* ── FOOTER(固定): View more,py-3 canonical(12px,比一般 link 按鈕多呼吸) ── */}\n {onViewMore && (\n <div className=\"shrink-0 border-t border-divider px-4 py-3\">\n <Button variant=\"link\" size=\"sm\" onClick={onViewMore} className=\"w-full\">{viewMoreLabel}</Button>\n </div>\n )}\n </div>\n )\n },\n)\nProfileCard.displayName = 'ProfileCard'\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 nameCardMeta = {\n component: 'ProfileCard',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: [], // 唯讀 person-info template,無自身互動 state(hover/active/focus/disabled 屬子 Button/Avatar)\n tokens: {\n bg: ['bg-muted'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { ProfileCard }\n"],"names":[],"mappings":"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"}
|