@qijenchen/design-system 0.1.0-beta.105 → 0.1.0-beta.107
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/dist/components/Empty/empty.d.ts +3 -3
- package/dist/components/Empty/empty.d.ts.map +1 -1
- package/dist/components/Empty/empty.js.map +1 -1
- package/dist/components/FileUpload/file-upload.js +4 -7
- package/dist/components/FileUpload/file-upload.js.map +1 -1
- package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
- package/dist/components/RadioGroup/radio-group.js +34 -12
- package/dist/components/RadioGroup/radio-group.js.map +1 -1
- package/dist/components/SelectMenu/select-menu.d.ts +3 -4
- package/dist/components/SelectMenu/select-menu.d.ts.map +1 -1
- package/dist/components/SelectMenu/select-menu.js +2 -2
- package/dist/components/SelectMenu/select-menu.js.map +1 -1
- package/dist/components/Tabs/tabs.d.ts.map +1 -1
- package/dist/components/Tabs/tabs.js +9 -13
- package/dist/components/Tabs/tabs.js.map +1 -1
- package/ds-canonical/fork/AGENTS.md +1 -1
- package/ds-canonical/fork/codex/hooks.json +1 -1
- package/ds-canonical/fork/common/instruction.md +1 -1
- package/ds-canonical/fork/consumer/lock.json +18 -17
- package/ds-canonical/fork/consumer/managed-files/0235cf6b3fd402a3717881dc205b5bf77aed7632e7d7d6f1811a40e3033bd816.blob +1 -1
- package/ds-canonical/fork/consumer/managed-files/24ad71c8613ddcf6fd23818cb3bb477a1fb6d83af4550b0bad43099813088686.blob +1 -1
- package/ds-canonical/fork/consumer/managed-files/46f10bc55cdfdf834d0749eebff42ea0bb8576f361b36d2b6ba1851354c50150.blob +37 -36
- package/ds-canonical/fork/consumer/managed-files/5aeda1df235815f40dd755951f2b868d4e02035653d4536a8dde209cdc75b493.blob +245 -0
- package/ds-canonical/fork/consumer/managed-files/7ab15a8aeca39485d7b9b57234bbc4a69e0b9e70b4a851dfe10f6447127cee78.blob +51 -0
- package/ds-canonical/fork/consumer/managed-files/7eb6cc60abfdabaf6617c336058fad1454ccde08889295767bfd9b7fd153cc6e.blob +186 -64
- package/ds-canonical/fork/consumer/managed-files/884721550b56308812b20917adac7a12f36fdb4ecda6b02460a7f5a219c0c966.blob +8 -1
- package/ds-canonical/fork/consumer/managed-files/e71c65f45c6af7ce12b6b3c0d8028f185da5b719e2050c9b2a6b243d41ae4878.blob +1 -1
- package/ds-canonical/fork/governance.lock +30 -30
- package/ds-canonical/fork/hooks/check_consumer_app_invariants.sh +7 -19
- package/ds-canonical/fork/hooks/check_escape_marker_abuse.sh +1 -1
- package/ds-canonical/fork/launchers/settings-hooks.json +1 -1
- package/ds-canonical/fork/manifest.json +15 -15
- package/ds-canonical/fork/providers/codex/hook-config.json +1 -1
- package/ds-canonical/hooks/README.md +1 -1
- package/ds-canonical/hooks/check_audit_post_report_validator.sh +22 -1
- package/ds-canonical/hooks/check_benchmark_citation.sh +27 -1
- package/ds-canonical/hooks/check_canonical_propagation.sh +4 -2
- package/ds-canonical/hooks/check_consumer_app_invariants.sh +7 -19
- package/ds-canonical/hooks/check_escape_marker_abuse.sh +1 -1
- package/ds-canonical/hooks/check_plugin_fork_health.sh +16 -5
- package/ds-canonical/hooks/check_story_invariants.sh +68 -29
- package/ds-canonical/hooks/stop_self_audit.sh +24 -13
- package/ds-canonical/hooks/tests/test_check_audit_post_report_validator.sh +58 -4
- package/ds-canonical/hooks/tests/test_check_benchmark_citation.sh +39 -7
- package/ds-canonical/hooks/tests/test_check_consumer_app_invariants.sh +1 -1
- package/ds-canonical/hooks/tests/test_check_consumer_ds_primitive_misuse.sh +17 -32
- package/ds-canonical/hooks/tests/test_check_plugin_fork_health.sh +70 -0
- package/ds-canonical/hooks/tests/test_check_story_category.sh +153 -9
- package/ds-canonical/hooks/tests/test_stop_self_audit.sh +53 -15
- package/ds-canonical/references/failure-class-registry.json +7 -0
- package/ds-canonical/references/repository-hygiene-policy.json +16 -2
- package/ds-canonical/references/repository-hygiene.md +22 -3
- package/ds-canonical/rules/meta-patterns.md +1 -1
- package/ds-canonical/rules/story-rules.md +11 -2
- package/ds-canonical/skills/codify-principle/SKILL.md +1 -1
- package/ds-canonical/skills/design-system-audit/SKILL.md +8 -7
- package/ds-canonical/skills/design-system-audit/references/audit-prompts.md +24 -19
- package/ds-canonical/skills/governance-health/SKILL.md +1 -1
- package/ds-canonical/skills/new-component/SKILL.md +1 -1
- package/ds-canonical/skills/story-writing/SKILL.md +6 -0
- package/ds-canonical/skills/story-writing/references/category-templates.md +9 -9
- package/ds-canonical/templates/consumer-governance.md +8 -1
- package/ds-canonical/templates/product-provider-wiring.json +3 -2
- package/ds-canonical/templates/product-provider-wiring.schema.json +5 -4
- package/ds-story-manifest.json +12 -4
- package/package.json +1 -1
- package/src/components/Accordion/accordion.anatomy.stories.tsx +1 -1
- package/src/components/Accordion/accordion.stories.tsx +1 -1
- package/src/components/AccountMenu/account-menu.anatomy.stories.tsx +2 -2
- package/src/components/Alert/alert.anatomy.stories.tsx +1 -1
- package/src/components/Alert/alert.spec.md +1 -2
- package/src/components/Alert/alert.stories.tsx +2 -2
- package/src/components/AppShell/app-shell.anatomy.stories.tsx +4 -4
- package/src/components/AppShell/app-shell.spec.md +1 -2
- package/src/components/AspectRatio/aspect-ratio.anatomy.stories.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.principles.stories.tsx +1 -1
- package/src/components/AspectRatio/aspect-ratio.spec.md +4 -5
- package/src/components/Avatar/avatar.anatomy.stories.tsx +1 -1
- package/src/components/Avatar/avatar.spec.md +2 -3
- package/src/components/Avatar/avatar.stories.tsx +1 -1
- package/src/components/Badge/badge.anatomy.stories.tsx +1 -1
- package/src/components/Badge/badge.spec.md +4 -5
- package/src/components/Breadcrumb/breadcrumb.anatomy.stories.tsx +1 -1
- package/src/components/Breadcrumb/breadcrumb.spec.md +5 -6
- package/src/components/Breadcrumb/breadcrumb.stories.tsx +7 -7
- package/src/components/BulkActionBar/bulk-action-bar.spec.md +10 -11
- package/src/components/BulkActionBar/bulk-action-bar.stories.tsx +1 -1
- package/src/components/Button/button.anatomy.stories.tsx +1 -1
- package/src/components/Button/button.spec.md +2 -3
- package/src/components/Button/button.stories.tsx +5 -5
- package/src/components/Calendar/calendar.anatomy.stories.tsx +1 -1
- package/src/components/Calendar/calendar.spec.md +1 -2
- package/src/components/Carousel/carousel.anatomy.stories.tsx +1 -1
- package/src/components/Carousel/carousel.spec.md +6 -7
- package/src/components/Carousel/carousel.stories.tsx +1 -1
- package/src/components/Chart/chart.anatomy.stories.tsx +1 -1
- package/src/components/Chart/chart.stories.tsx +1 -1
- package/src/components/Checkbox/checkbox.anatomy.stories.tsx +1 -1
- package/src/components/Checkbox/checkbox.spec.md +3 -4
- package/src/components/Checkbox/checkbox.stories.tsx +2 -3
- package/src/components/Chip/chip.anatomy.stories.tsx +1 -1
- package/src/components/Chip/chip.spec.md +2 -3
- package/src/components/Chip/chip.stories.tsx +2 -2
- package/src/components/CircularProgress/circular-progress.anatomy.stories.tsx +1 -1
- package/src/components/CircularProgress/circular-progress.spec.md +5 -6
- package/src/components/Coachmark/coachmark.anatomy.stories.tsx +1 -1
- package/src/components/Coachmark/coachmark.spec.md +5 -6
- package/src/components/Combobox/combobox.anatomy.stories.tsx +1 -1
- package/src/components/Combobox/combobox.spec.md +3 -4
- package/src/components/Combobox/combobox.stories.tsx +25 -3
- package/src/components/Command/command.anatomy.stories.tsx +1 -1
- package/src/components/Command/command.spec.md +5 -6
- package/src/components/DataTable/data-table.anatomy.stories.tsx +1 -1
- package/src/components/DataTable/data-table.spec.md +25 -24
- package/src/components/DataTable/data-table.stories.tsx +3 -3
- package/src/components/DateGrid/date-grid.anatomy.stories.tsx +1 -1
- package/src/components/DateGrid/date-grid.spec.md +7 -8
- package/src/components/DatePicker/date-picker.anatomy.stories.tsx +1 -1
- package/src/components/DatePicker/date-picker.spec.md +5 -6
- package/src/components/DatePicker/date-picker.stories.tsx +1 -2
- package/src/components/DescriptionList/description-list.anatomy.stories.tsx +1 -1
- package/src/components/DescriptionList/description-list.spec.md +4 -5
- package/src/components/DescriptionList/description-list.stories.tsx +2 -2
- package/src/components/Dialog/dialog.anatomy.stories.tsx +1 -1
- package/src/components/Dialog/dialog.principles.stories.tsx +13 -65
- package/src/components/Dialog/dialog.spec.md +2 -3
- package/src/components/Dialog/dialog.stories.tsx +1 -1
- package/src/components/DropdownMenu/dropdown-menu.anatomy.stories.tsx +1 -1
- package/src/components/DropdownMenu/dropdown-menu.spec.md +1 -1
- package/src/components/DropdownMenu/dropdown-menu.stories.tsx +4 -4
- package/src/components/Empty/empty.anatomy.stories.tsx +1 -1
- package/src/components/Empty/empty.spec.md +8 -9
- package/src/components/Empty/empty.stories.tsx +1 -1
- package/src/components/Empty/empty.tsx +3 -3
- package/src/components/Field/field-controls.spec.md +4 -5
- package/src/components/Field/field.anatomy.stories.tsx +1 -1
- package/src/components/Field/field.spec.md +6 -7
- package/src/components/Field/field.stories.tsx +3 -3
- package/src/components/Field/form-validation.spec.md +9 -10
- package/src/components/FieldControlGroup/field-control-group.anatomy.stories.tsx +2 -2
- package/src/components/FieldControlGroup/field-control-group.spec.md +0 -1
- package/src/components/FieldControlGroup/field-control-group.stories.tsx +2 -2
- package/src/components/FileItem/file-item.anatomy.stories.tsx +1 -1
- package/src/components/FileItem/file-item.principles.stories.tsx +15 -46
- package/src/components/FileItem/file-item.spec.md +5 -6
- package/src/components/FileItem/file-item.stories.tsx +1 -1
- package/src/components/FileUpload/file-upload.anatomy.stories.tsx +1 -1
- package/src/components/FileUpload/file-upload.spec.md +4 -6
- package/src/components/FileUpload/file-upload.stories.tsx +2 -2
- package/src/components/FileUpload/file-upload.tsx +4 -4
- package/src/components/FileViewer/file-viewer.anatomy.stories.tsx +1 -1
- package/src/components/FileViewer/file-viewer.spec.md +9 -10
- package/src/components/FileViewer/file-viewer.stories.tsx +1 -1
- package/src/components/HoverCard/hover-card.anatomy.stories.tsx +1 -1
- package/src/components/HoverCard/hover-card.principles.stories.tsx +6 -25
- package/src/components/HoverCard/hover-card.stories.tsx +1 -1
- package/src/components/InlineEdit/inline-edit.anatomy.stories.tsx +68 -2
- package/src/components/InlineEdit/inline-edit.spec.md +1 -1
- package/src/components/Input/input.anatomy.stories.tsx +1 -1
- package/src/components/Input/input.spec.md +4 -5
- package/src/components/Input/input.stories.tsx +2 -2
- package/src/components/LinkInput/link-input.anatomy.stories.tsx +1 -1
- package/src/components/LinkInput/link-input.spec.md +1 -1
- package/src/components/LinkInput/link-input.stories.tsx +3 -3
- package/src/components/Menu/menu-item.anatomy.stories.tsx +1 -1
- package/src/components/Menu/menu-item.spec.md +1 -1
- package/src/components/Menu/menu-item.stories.tsx +2 -2
- package/src/components/Notice/notice.anatomy.stories.tsx +1 -1
- package/src/components/Notice/notice.spec.md +3 -5
- package/src/components/NumberInput/number-input.anatomy.stories.tsx +1 -1
- package/src/components/NumberInput/number-input.principles.stories.tsx +1 -1
- package/src/components/NumberInput/number-input.spec.md +5 -5
- package/src/components/NumberInput/number-input.stories.tsx +17 -2
- package/src/components/OverflowIndicator/overflow-indicator.anatomy.stories.tsx +1 -1
- package/src/components/OverflowIndicator/overflow-indicator.stories.tsx +2 -2
- package/src/components/Pagination/pagination.anatomy.stories.tsx +5 -4
- package/src/components/Pagination/pagination.spec.md +4 -4
- package/src/components/PeoplePicker/people-picker.anatomy.stories.tsx +1 -1
- package/src/components/PeoplePicker/people-picker.spec.md +2 -4
- package/src/components/PeoplePicker/people-picker.stories.tsx +23 -4
- package/src/components/Popover/popover.anatomy.stories.tsx +1 -1
- package/src/components/Popover/popover.spec.md +4 -6
- package/src/components/Popover/popover.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.anatomy.stories.tsx +1 -1
- package/src/components/ProfileCard/profile-card.spec.md +4 -6
- package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +1 -1
- package/src/components/ProgressBar/progress-bar.spec.md +6 -8
- package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +1 -1
- package/src/components/RadioGroup/radio-group.spec.md +1 -3
- package/src/components/RadioGroup/radio-group.stories.tsx +105 -4
- package/src/components/RadioGroup/radio-group.tsx +43 -13
- package/src/components/Rating/rating.anatomy.stories.tsx +1 -1
- package/src/components/Rating/rating.spec.md +7 -9
- package/src/components/Rating/rating.stories.tsx +3 -3
- package/src/components/ScrollArea/scroll-area.anatomy.stories.tsx +1 -1
- package/src/components/ScrollArea/scroll-area.spec.md +4 -6
- package/src/components/SegmentedControl/segmented-control.anatomy.stories.tsx +1 -1
- package/src/components/SegmentedControl/segmented-control.spec.md +3 -5
- package/src/components/SegmentedControl/segmented-control.stories.tsx +1 -1
- package/src/components/Select/select.anatomy.stories.tsx +1 -1
- package/src/components/Select/select.spec.md +2 -4
- package/src/components/Select/select.stories.tsx +28 -4
- package/src/components/SelectMenu/select-menu.anatomy.stories.tsx +1 -1
- package/src/components/SelectMenu/select-menu.spec.md +8 -10
- package/src/components/SelectMenu/select-menu.stories.tsx +4 -4
- package/src/components/SelectMenu/select-menu.tsx +12 -8
- package/src/components/SelectionControl/selection-item.anatomy.stories.tsx +1 -1
- package/src/components/SelectionControl/selection-item.spec.md +1 -1
- package/src/components/SelectionControl/selection-item.stories.tsx +2 -2
- package/src/components/Separator/separator.anatomy.stories.tsx +1 -1
- package/src/components/Separator/separator.stories.tsx +2 -2
- package/src/components/Sheet/sheet.anatomy.stories.tsx +1 -1
- package/src/components/Sheet/sheet.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.anatomy.stories.tsx +1 -1
- package/src/components/Sidebar/sidebar.spec.md +5 -14
- package/src/components/Sidebar/sidebar.stories.tsx +2 -2
- package/src/components/Skeleton/skeleton.anatomy.stories.tsx +1 -1
- package/src/components/Skeleton/skeleton.spec.md +2 -2
- package/src/components/Slider/slider.anatomy.stories.tsx +1 -1
- package/src/components/Slider/slider.spec.md +6 -9
- package/src/components/Slider/slider.stories.tsx +3 -3
- package/src/components/Steps/steps.anatomy.stories.tsx +1 -1
- package/src/components/Steps/steps.spec.md +11 -13
- package/src/components/Steps/steps.stories.tsx +3 -3
- package/src/components/Switch/switch.anatomy.stories.tsx +1 -1
- package/src/components/Switch/switch.spec.md +8 -11
- package/src/components/Switch/switch.stories.tsx +1 -1
- package/src/components/Tabs/tabs.anatomy.stories.tsx +1 -1
- package/src/components/Tabs/tabs.spec.md +9 -11
- package/src/components/Tabs/tabs.stories.tsx +108 -2
- package/src/components/Tabs/tabs.tsx +12 -15
- package/src/components/Tag/tag.anatomy.stories.tsx +1 -1
- package/src/components/Tag/tag.spec.md +3 -5
- package/src/components/Tag/tag.stories.tsx +1 -1
- package/src/components/Textarea/textarea.anatomy.stories.tsx +1 -1
- package/src/components/Textarea/textarea.spec.md +1 -3
- package/src/components/Textarea/textarea.stories.tsx +3 -2
- package/src/components/TimePicker/time-picker.anatomy.stories.tsx +1 -1
- package/src/components/TimePicker/time-picker.spec.md +6 -12
- package/src/components/TimePicker/time-picker.stories.tsx +1 -2
- package/src/components/Toast/toast.anatomy.stories.tsx +1 -1
- package/src/components/Toast/toast.spec.md +2 -2
- package/src/components/Tooltip/tooltip.anatomy.stories.tsx +1 -1
- package/src/components/Tooltip/tooltip.spec.md +4 -6
- package/src/components/TreeView/tree-view.anatomy.stories.tsx +1 -1
- package/src/components/TreeView/tree-view.spec.md +1 -1
- package/src/components/TreeView/tree-view.stories.tsx +4 -4
- package/src/patterns/element-anatomy/element-anatomy.spec.md +3 -3
- package/src/patterns/element-anatomy/item-anatomy.spec.md +3 -3
- package/src/patterns/element-anatomy/item-anatomy.stories.tsx +1 -1
- package/src/patterns/header-canonical/header-canonical.spec.md +2 -2
- package/src/patterns/horizontal-overflow/horizontal-overflow.spec.md +2 -2
- package/src/patterns/overlay-surface/overlay-surface.spec.md +2 -3
- package/src/patterns/resize-handle/resize-handle.spec.md +1 -1
- package/src/tokens/color/color.spec.md +1 -1
- package/src/tokens/token-system.spec.md +1 -1
- package/src/tokens/uiSize/uiSize.spec.md +2 -2
- package/ds-canonical/fork/consumer/managed-files/6929a3c8bb4d69c507720b4754aeb339c6cbb8ff655e0a381d76606d64695e38.blob +0 -393
- package/ds-canonical/fork/consumer/managed-files/e1b11160d6c4043002d2cbff6d9a4ae551698a96137773c4244dadfac152fcb0.blob +0 -222
|
@@ -4,7 +4,7 @@ import type { LucideIcon } from 'lucide-react';
|
|
|
4
4
|
* Empty — 空狀態視覺元件
|
|
5
5
|
*
|
|
6
6
|
* 居中垂直堆疊:icon(Avatar) → title → description → action。
|
|
7
|
-
*
|
|
7
|
+
* description 必填;icon / title / action 可選。
|
|
8
8
|
*
|
|
9
9
|
* 間距固定,不隨 density 變(Empty 是展示性元件,不是工作區域元件):
|
|
10
10
|
* icon → text = mb-4(16px)
|
|
@@ -20,8 +20,8 @@ export interface EmptyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
20
20
|
icon?: LucideIcon | React.ReactElement;
|
|
21
21
|
/** 標題(可選,font-medium,適用於首次引導) */
|
|
22
22
|
title?: string;
|
|
23
|
-
/**
|
|
24
|
-
description
|
|
23
|
+
/** 必填的情境說明;不得用空字串代替載入中或其他狀態。 */
|
|
24
|
+
description: string;
|
|
25
25
|
/** 行動按鈕(可選) */
|
|
26
26
|
action?: React.ReactNode;
|
|
27
27
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empty.d.ts","sourceRoot":"","sources":["../../../src/components/Empty/empty.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAK9C;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,UAAW,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACtE,+DAA+D;IAC/D,IAAI,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC,YAAY,CAAA;IACtC,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,
|
|
1
|
+
{"version":3,"file":"empty.d.ts","sourceRoot":"","sources":["../../../src/components/Empty/empty.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAK9C;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,UAAW,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACtE,+DAA+D;IAC/D,IAAI,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC,YAAY,CAAA;IACtC,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe;IACf,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,QAAA,MAAM,KAAK,mFA0DV,CAAA;AAKD,eAAO,MAAM,SAAS;;;;;;;;;;;CAeZ,CAAA;AAEV,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empty.js","sources":["../../../src/components/Empty/empty.tsx"],"sourcesContent":["import * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar } from '@/design-system/components/Avatar/avatar'\nimport { useRowSize } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * Empty — 空狀態視覺元件\n *\n * 居中垂直堆疊:icon(Avatar) → title → description → action。\n *
|
|
1
|
+
{"version":3,"file":"empty.js","sources":["../../../src/components/Empty/empty.tsx"],"sourcesContent":["import * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Avatar } from '@/design-system/components/Avatar/avatar'\nimport { useRowSize } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * Empty — 空狀態視覺元件\n *\n * 居中垂直堆疊:icon(Avatar) → title → description → action。\n * description 必填;icon / title / action 可選。\n *\n * 間距固定,不隨 density 變(Empty 是展示性元件,不是工作區域元件):\n * icon → text = mb-4(16px)\n * desc → action = mt-6(24px)\n * title → desc = `var(--item-gap-label-desc-reading-lg)`(reading-lg tier token,item-anatomy SSOT;bare `--item-gap-label-desc` 已 retire)\n *\n * description 字級隨 RowSizeContext 變(sm/md → text-body 14px,lg → text-body-lg 16px)。\n *\n * Outer padding 由 consumer 容器決定(py-12 / py-6 / py-16 等)。\n */\n\nexport interface EmptyProps extends React.HTMLAttributes<HTMLDivElement> {\n /** LucideIcon → 自動包 Avatar 48px neutral;ReactElement → 原樣渲染 */\n icon?: LucideIcon | React.ReactElement\n /** 標題(可選,font-medium,適用於首次引導) */\n title?: string\n /** 必填的情境說明;不得用空字串代替載入中或其他狀態。 */\n description: string\n /** 行動按鈕(可選) */\n action?: React.ReactNode\n /**\n * Disabled context(2026-06-03 加 — FileUpload disabled 等情境消費):title / description 轉\n * `text-fg-disabled`(語意 disabled token,非 opacity)。icon glyph 也 → fg-disabled(icon 是文字一環);icon-circle bg 維持 muted。\n * 預設 false,不影響既有 consumer。\n */\n disabled?: boolean\n}\n\nconst Empty = React.forwardRef<HTMLDivElement, EmptyProps>(\n ({ icon, title, description, action, disabled = false, className, ...props }, ref) => {\n // 字體 tier:讀 RowSizeContext(menu 內自動對齊 menu items 的字體)\n // 沒有 context(standalone)→ fallback 'md' → text-body (14px)\n const rowSize = useRowSize('md')\n const descFont = rowSize === 'lg' ? 'text-body-lg' : 'text-body'\n\n // Icon rendering: ReactElement → as-is;LucideIcon(component,包括 forwardRef 物件)→ 包 Avatar\n // 注意:Lucide v0.577+ icons 是 forwardRef 物件(`typeof === 'object'`),不是 function。\n // 必用 React.isValidElement 判斷 element vs component(typeof 會把 forwardRef 物件誤歸 object)。\n let iconElement: React.ReactNode = null\n if (icon) {\n if (React.isValidElement(icon)) {\n // disabled:自訂 element 同樣變淡(2026-07-07 補漏 — docblock/spec 無條件宣稱「icon glyph →\n // fg-disabled」但原僅 LucideIcon 分支生效)。`contents` wrapper 不進 layout,[&_svg] token swap\n // 只染 svg 系自訂 icon;非 svg(img 照片)不強制染色。\n iconElement = disabled ? <span className=\"contents [&_svg]:!text-fg-disabled\">{icon}</span> : icon\n } else {\n const Icon = icon as LucideIcon\n // disabled:glyph → fg-disabled([&_svg]:!… 蓋過 Avatar 內聯 color;circle bg 維持 muted)。icon 是文字一環,隨 disabled 變淡。\n iconElement = <Avatar icon={Icon} size={48} color=\"neutral\" className={disabled ? '[&_svg]:!text-fg-disabled' : undefined} />\n }\n }\n\n return (\n <div\n ref={ref}\n className={cn('flex flex-col items-center text-center', className)}\n {...props}\n >\n {iconElement && (\n <div className=\"mb-4\">{iconElement}</div>\n )}\n {title && (\n <span className={cn('text-body-lg font-medium', disabled ? 'text-fg-disabled' : 'text-foreground')}>\n {title}\n </span>\n )}\n {description && (\n <span\n className={cn(\n // 字體跟 RowSizeContext 對齊:sm/md = text-body (14px),lg = text-body-lg (16px)\n // 在 menu 內自動對齊 menu items;standalone 時 fallback text-body\n descFont,\n disabled ? 'text-fg-disabled' : (title || action) ? 'text-fg-secondary' : 'text-fg-muted',\n // Empty title 永遠 body-lg(16)→ 用 reading-lg token(label tier 決定)\n title && 'mt-[var(--item-gap-label-desc-reading-lg)]',\n )}\n >\n {description}\n </span>\n )}\n {action && (\n <div className=\"mt-6\">{action}</div>\n )}\n </div>\n )\n },\n)\nEmpty.displayName = 'Empty'\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 emptyMeta = {\n component: 'Empty',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'disabled'],\n tokens: {\n bg: [],\n fg: ['text-fg-muted', 'text-fg-secondary', 'text-foreground', 'text-fg-disabled'],\n ring: [],\n },\n} as const\n\nexport { Empty }\n"],"names":[],"mappings":";;;;;AAuCA,MAAM,QAAQ,MAAM;AAAA,EAClB,CAAC,EAAE,MAAM,OAAO,aAAa,QAAQ,WAAW,OAAO,WAAW,GAAG,MAAA,GAAS,QAAQ;AAGpF,UAAM,UAAU,WAAW,IAAI;AAC/B,UAAM,WAAW,YAAY,OAAO,iBAAiB;AAKrD,QAAI,cAA+B;AACnC,QAAI,MAAM;AACR,UAAI,MAAM,eAAe,IAAI,GAAG;AAI9B,sBAAc,WAAW,oBAAC,QAAA,EAAK,WAAU,sCAAsC,gBAAK,IAAU;AAAA,MAChG,OAAO;AACL,cAAM,OAAO;AAEb,sBAAc,oBAAC,QAAA,EAAO,MAAM,MAAM,MAAM,IAAI,OAAM,WAAU,WAAW,WAAW,8BAA8B,OAAA,CAAW;AAAA,MAC7H;AAAA,IACF;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW,GAAG,0CAA0C,SAAS;AAAA,QAChE,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,eACC,oBAAC,OAAA,EAAI,WAAU,QAAQ,UAAA,aAAY;AAAA,UAEpC,SACC,oBAAC,QAAA,EAAK,WAAW,GAAG,4BAA4B,WAAW,qBAAqB,iBAAiB,GAC9F,UAAA,MAAA,CACH;AAAA,UAED,eACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA;AAAA;AAAA,gBAGT;AAAA,gBACA,WAAW,qBAAsB,SAAS,SAAU,sBAAsB;AAAA;AAAA,gBAE1E,SAAS;AAAA,cAAA;AAAA,cAGV,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGJ,UACC,oBAAC,OAAA,EAAI,WAAU,QAAQ,UAAA,OAAA,CAAO;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAItC;AACF;AACA,MAAM,cAAc;AAIb,MAAM,YAAY;AAAA,EACvB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,UAAU;AAAA,EAC9B,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA,IACJ,IAAI,CAAC,iBAAiB,qBAAqB,mBAAmB,kBAAkB;AAAA,IAChF,MAAM,CAAA;AAAA,EAAC;AAEX;"}
|
|
@@ -241,13 +241,10 @@ const FileUpload = React.forwardRef(
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
),
|
|
244
|
-
loading ? /* @__PURE__ */
|
|
245
|
-
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
title: loadingTitle
|
|
249
|
-
}
|
|
250
|
-
) : children ?? /* @__PURE__ */ jsx(
|
|
244
|
+
loading ? /* @__PURE__ */ jsxs("div", { role: "status", "aria-label": loadingTitle, className: "flex flex-col items-center text-center", children: [
|
|
245
|
+
/* @__PURE__ */ jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsx(CircularProgress, { size: 48 }) }),
|
|
246
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-body-lg font-medium text-foreground", children: loadingTitle })
|
|
247
|
+
] }) : children ?? /* @__PURE__ */ jsx(
|
|
251
248
|
Empty,
|
|
252
249
|
{
|
|
253
250
|
icon: Upload,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-upload.js","sources":["../../../src/components/FileUpload/file-upload.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { Upload as UploadIcon, X } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Empty } from '@/design-system/components/Empty/empty'\nimport { CircularProgress } from '@/design-system/components/CircularProgress/circular-progress'\nimport { FileItem } from '@/design-system/components/FileItem/file-item'\nimport { Button } from '@/design-system/components/Button/button'\n\n/**\n * FileUpload — 拖放 / 點擊上傳區塊\n *\n * 世界級對照:Ant Design `Upload.Dragger`、Polaris `DropZone`、Material community MUI-File-Input。\n * 與本 DS 既有 FileItem(顯示已上傳檔案)配對 — 這裡 own「上傳觸發 + 拖放偵測」,\n * 上傳後的檔案清單顯示交給 consumer 用 FileItem 渲染。\n *\n * ── 4 狀態(2026-06-03 修正)──\n * idle (default) — border-dashed border-border bg-surface\n * hover = drag-over — border-primary-hover(統一,純 border-driven,底維持 surface;2026-07-06 對齊 Ant colorPrimaryHover)\n * loading — CircularProgress;cursor-progress(無 pointer-events-none)\n * disabled — bg-disabled + 文字 fg-disabled(語意 token,非 opacity)+ cursor-not-allowed\n *\n * ── variant ──\n * dropzone(預設)大拖放區 + drag;button 緊湊 Button 觸發(form-friendly,click-only)\n *\n * ── children 插槽 ──\n * 預設渲染 `<Empty icon={Upload} title description />` — 重用 Empty 元件 own\n * 的「icon + title + description 垂直居中」SSOT 避免視覺漂移。Empty 改字體 /\n * gap / icon 尺寸時 FileUpload 自動跟進。若 consumer 傳 children 則整個覆寫。\n *\n * ── API ──\n * onUpload: 使用者選取或拖放檔案時觸發,回傳 File[](至少 1 個)。\n * multiple: 允許多檔。預設 false(單檔)。\n * accept: MIME filter(例 \"image/*,.pdf\"),傳給 <input type=file>。\n * maxSize: 單檔最大 bytes;超過靜默忽略(consumer 若需要錯誤提示,走 onReject)。\n * onReject: 被 maxSize / accept 擋下的檔案(提供錯誤訊息顯示機會)。\n */\n\n/**\n * Uploaded / uploading file status item(for `files` prop)。\n * Consumer 持 state(progress / status),FileUpload 只負責渲染。\n */\nexport interface FileUploadStatus {\n id: string\n name: string\n /** bytes */\n size?: number\n /** Upload 進度(0-100)— uploading 時顯示 progress bar */\n progress?: number\n status?: 'uploading' | 'completed' | 'error'\n /** Error 訊息 / size 等 description */\n description?: React.ReactNode\n /** Thumbnail URL for rich mode */\n thumbnailSrc?: string\n}\n\nexport interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onDrop'> {\n onUpload?: (files: File[]) => void\n onReject?: (files: File[], reason: 'size' | 'type') => void\n multiple?: boolean\n accept?: string\n maxSize?: number\n disabled?: boolean\n /**\n * 觸發外觀(2026-06-03 加,M21 prop-variant + Ant `Upload`(button)/`Upload.Dragger`(dropzone)先例):\n * - `dropzone`(預設):大拖放區 + 點擊,支援 drag-and-drop。\n * - `button`:緊湊 Button 觸發(form-friendly,省空間),click-only(無拖放區)。\n * 兩者共用 onUpload / onReject / files 清單渲染;都可放進 Field control slot。\n */\n variant?: 'dropzone' | 'button'\n /** `variant=\"button\"` 的按鈕文字(預設「Choose file」)。 */\n buttonLabel?: string\n /**\n * Loading 狀態(async 上傳 / 伺服器處理中)。\n * - 2026-06-03 **deferred**:其唯一用途(無清單單檔 / 頭像替換)場景尚未定義,故已從 3-state showcase\n * 移除、不作為當前 feature 呈現;prop 保留供未來該場景。有清單的上傳進度走 FileItem 自身 progress bar(status=uploading)。\n * - 顯示 CircularProgress 取代預設 Empty 內容;`cursor-progress`;互動由 handleClick/handlers 的 isBlocked guard 擋\n * - 宣告 `aria-busy=\"true\"` 讓 screen reader 感知處理中\n */\n loading?: boolean\n /** Loading 狀態的文字標題(預設「上傳中…」) */\n loadingTitle?: string\n /** 標題文字(預設「Click or drag file here to upload」) */\n title?: string\n /** 說明文字(預設依 multiple:單檔「Support for a single file upload」、multiple 多檔「Support for a single or bulk upload」) */\n description?: string\n /** 若傳入 children,覆寫預設 Empty 結構 */\n children?: React.ReactNode\n /**\n * Uploaded / uploading 檔案清單。傳入 → FileUpload 在 drop zone 下方渲染列表。\n * 不傳 → 不顯示(consumer 可自行用 FileItem 組成,pre-2026-04-24 行為)。\n * 每項由 FileItem 渲染,status 狀態對應視覺:\n * - `uploading`:linear ProgressBar(via FileItem;rich mode 另顯示 {progress}%)\n * - `completed`:綠色 ✓(success 狀態視覺確認)\n * - `error`:紅色 ✗(+ description 顯示錯誤訊息)\n */\n files?: FileUploadStatus[]\n /** File list 每項顯示模式。Default: 'compact'(單行);'rich' = 含 thumbnail / size / progress bar */\n fileListMode?: 'compact' | 'rich'\n /** File list 移除 callback。有值 → 每項右側顯示 X dismiss button;無 → 不可移除(view-only) */\n onRemove?: (id: string) => void\n /** File list dismiss button ARIA label template。預設 `移除 {name}`。For i18n. */\n removeAriaLabel?: (name: string) => string\n}\n\nfunction focusAfterFileRemoval(current: HTMLButtonElement) {\n const list = current.closest('ul')\n const buttons = list ? Array.from(list.querySelectorAll<HTMLButtonElement>('[data-collection-remove]')) : []\n const index = buttons.indexOf(current)\n const next = buttons[index + 1] ?? buttons[index - 1]\n const owner = list?.parentElement?.querySelector<HTMLElement>('[data-file-upload-owner]')\n ;(next ?? owner)?.focus()\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(\n (\n {\n onUpload,\n onReject,\n multiple = false,\n accept,\n maxSize,\n disabled = false,\n variant = 'dropzone',\n buttonLabel = 'Choose file', // i18n-allow: DS default; consumer override via buttonLabel prop\n loading = false,\n loadingTitle = '上傳中…', // i18n-allow: DS default; consumer override via loadingTitle prop\n title = 'Click or drag file here to upload', // i18n-allow: DS default; consumer override via title prop\n description = multiple ? 'Support for a single or bulk upload' : 'Support for a single file upload', // i18n-allow: DS default; consumer override via description prop\n children,\n files,\n fileListMode = 'compact',\n onRemove,\n removeAriaLabel = (name: string) => `移除 ${name}`, // i18n-allow: DS default; consumer override via removeAriaLabel prop\n className,\n onClick,\n ...props\n },\n ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [isDragOver, setDragOver] = React.useState(false)\n // drag enter/leave 深度計數(react-dropzone / Mantine Dropzone 同款):跨進內部子元素時\n // 瀏覽器發「子 dragenter → 外層 dragleave」成對事件,+1/−1 抵銷後計數仍 > 0 = 沒離開。\n // 不依賴 e.relatedTarget —— Safari/WebKit 的 dragleave relatedTarget 恆 null,guard 法必失效。\n const dragDepth = React.useRef(0)\n\n const filterAndDispatch = (files: FileList | null) => {\n if (!files || files.length === 0) return\n const accepted: File[] = []\n const rejectedBySize: File[] = []\n const rejectedByType: File[] = []\n\n Array.from(files).forEach((f) => {\n if (maxSize != null && f.size > maxSize) {\n rejectedBySize.push(f)\n return\n }\n if (accept && !matchAccept(f, accept)) {\n rejectedByType.push(f)\n return\n }\n accepted.push(f)\n })\n\n if (rejectedBySize.length) onReject?.(rejectedBySize, 'size')\n if (rejectedByType.length) onReject?.(rejectedByType, 'type')\n if (accepted.length) onUpload?.(multiple ? accepted : accepted.slice(0, 1))\n }\n\n const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n // 2026-06-03 Q4:guard `!disabled && !loading`(= !isBlocked)。loading 時也擋點擊防 double-submit\n // (原只 guard !disabled,靠 pointer-events-none 擋 loading;移除 pointer-events-none 後改這裡 guard)。\n if (!disabled && !loading) inputRef.current?.click()\n onClick?.(e)\n }\n\n // State 優先序:disabled > loading > drag-over > idle(disabled 最硬,loading 次之)\n const state = disabled ? 'disabled' : loading ? 'loading' : isDragOver ? 'drag-over' : 'idle'\n const isBlocked = disabled || loading\n\n const hasFiles = Array.isArray(files) && files.length > 0\n\n const fileListNode = hasFiles ? (\n <ul\n className={cn(\n 'flex flex-col w-full',\n // 2026-06-03 gap SSOT:列間 gap + control→list gap(mt)由「item 有無邊框」單一規則決定,\n // 同值貫穿整個垂直堆疊 — rich(form surface = border card)→ 8px;compact(borderless/bg-pill)→ 4px。\n // 消費 FileItem「List wrapper canonical」(file-item.spec.md),取代原硬寫 gap-2 / mt-3(不分 mode)。\n fileListMode === 'rich' ? 'gap-2 mt-2' : 'gap-1 mt-1',\n )}\n aria-label=\"已上傳的檔案\"\n >\n {files!.map((f) => (\n <li key={f.id} className=\"list-none\">\n <FileItem\n mode={fileListMode}\n name={f.name}\n status={f.status}\n progress={f.progress}\n description={f.description ?? (f.size != null ? formatBytes(f.size) : undefined)}\n thumbnailSrc={f.thumbnailSrc}\n actions={\n onRemove ? (\n // Collection remove(per-file)— 不是 dismiss surface,故不套 `dismiss` prop。\n // 視覺與 dismiss 一致(text variant + fg-muted dim)— 對齊 inline-action.spec.md\n // 「Dismiss canonical — X close only」+「dismiss prop 觸發條件」段:onRemove callback 不觸發 dismiss prop。\n <Button\n iconOnly\n variant=\"text\"\n size=\"xs\"\n startIcon={X}\n aria-label={removeAriaLabel(f.name)}\n data-collection-remove\n onClick={(e) => {\n e.stopPropagation()\n focusAfterFileRemoval(e.currentTarget)\n onRemove(f.id)\n }}\n className=\"text-fg-muted hover:text-fg-secondary\"\n />\n ) : undefined\n }\n />\n </li>\n ))}\n </ul>\n ) : null\n\n return (\n <div ref={ref} className={cn('w-full', hasFiles && 'flex flex-col')}>\n {variant === 'button' ? (\n // ── button variant:緊湊觸發(form-friendly,省空間),click-only(無拖放區)──\n // consumer onClick 在此 wrapper div 承載(type = MouseEvent<HTMLDivElement>,對齊 FileUploadProps\n // extends HTMLAttributes<HTMLDivElement>)。Button 觸發開檔會冒泡至此 → 同時觸發 consumer handler。\n // 2026-07-17 修:onClick 於 L128 destructure 後未再 compose,button variant 靜默丟失公開 DOM prop\n // (dropzone variant 經 handleClick 的 onClick?.(e) 正確 compose,button variant 漏接)。\n <div className={className} onClick={onClick} {...props}>\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n onChange={(e) => {\n // 2026-07-05 D4:dispatch 後清空 value — 瀏覽器對同路徑檔案不重發 change,\n // 不清空則「失敗修正後重選同檔 / 刪除後重傳同檔」靜默無反應\n filterAndDispatch(e.target.files)\n e.target.value = ''\n }}\n />\n <Button\n data-file-upload-owner\n variant=\"tertiary\"\n startIcon={UploadIcon}\n loading={loading}\n disabled={disabled}\n aria-busy={loading || undefined}\n onClick={() => {\n if (!disabled && !loading) inputRef.current?.click()\n }}\n >\n {buttonLabel}\n </Button>\n </div>\n ) : (\n <div\n data-file-upload-owner\n role=\"button\"\n tabIndex={isBlocked ? -1 : 0}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n data-state={state}\n onClick={handleClick}\n onKeyDown={(e) => {\n if (isBlocked) return\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n inputRef.current?.click()\n }\n }}\n onDragEnter={(e) => {\n if (isBlocked) return\n e.preventDefault()\n // 2026-07-06 深度計數升級(user 實測抓到「拖入約一秒高亮熄滅」):游標從邊緣飄到中央\n // 跨進 Empty icon/標題那刻,瀏覽器發「子 dragenter → 外層 dragleave」→ 舊版無 guard 直接\n // 熄滅;2026-07-05 的 relatedTarget guard 只救 Chrome(Safari/WebKit dragleave 的\n // relatedTarget 恆 null → guard 失效)。深度計數不依賴 relatedTarget,跨瀏覽器穩定。\n dragDepth.current += 1\n setDragOver(true)\n }}\n onDragLeave={(e) => {\n if (isBlocked) return\n e.preventDefault()\n dragDepth.current = Math.max(0, dragDepth.current - 1)\n if (dragDepth.current === 0) setDragOver(false)\n }}\n onDragOver={(e) => {\n // 2026-07-05 D4:blocked 時仍先 preventDefault — dragover 未取消 = 非合法 drop target,\n // 瀏覽器 default 會在當前 tab 直接開啟被丟檔案(整頁導航,app 狀態全失);吞掉操作而非讓瀏覽器接管\n e.preventDefault()\n }}\n onDrop={(e) => {\n // 2026-07-05 D4:preventDefault 先於 isBlocked guard — 同 dragover,擋瀏覽器開檔導航\n e.preventDefault()\n if (isBlocked) return\n dragDepth.current = 0 // drop 結束整輪拖曳,計數歸零(下一輪從乾淨狀態起算)\n setDragOver(false)\n filterAndDispatch(e.dataTransfer.files)\n }}\n className={cn(\n // 寬度 w-full 填滿 consumer 容器(user 明示「寬度填滿」);高度由 padding + 內容物決定(不固定 h)\n 'flex flex-col items-center justify-center gap-2 text-center w-full',\n // 對稱 padding p-[var(--layout-space-loose)]:四邊等距,density-aware(md=16px / lg=24px),對齊 DS chrome padding canonical。\n // 不再硬寫 px-6 py-10(不對稱+非 token)。內容物(icon + title + description)垂直堆疊由 gap-2 控制\n 'rounded-md border-2 border-dashed p-[var(--layout-space-loose)]',\n 'cursor-pointer transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n // idle:--border(元件邊框,非 --divider 分隔線 — 2026-06-03 Q2 token 修正)+ surface 底\n 'border-border bg-surface',\n // hover = drag-over 統一(2026-06-03 Q2-A 純 border-driven,對齊 Ant Dragger colorPrimaryHover):\n // 兩者都 → primary-hover 邊框(2026-07-06 修:註解與 spec 一直宣稱 colorPrimaryHover、\n // code 卻寫 base = claim-vs-code 反向 drift;瞬時態歸 hover 階,雙向全掃唯一 B 類違規),\n // 底色維持 surface(不變 bg)。state 信號靠邊框,非底色。\n // 2026-06-11 R2(M5 狀態疊加):hover 變 primary 僅限 idle —— disabled/loading 維持原邊框(spec「disabled 邊框不變色」;dropzone 狀態機無 error 態,error 屬 files 清單 FileItem status)\n 'data-[state=idle]:hover:border-primary-hover data-[state=drag-over]:border-primary-hover',\n // loading(2026-06-03 Q4:移除 pointer-events-none — 它會讓 cursor-progress 失效;\n // 互動已由 handleClick + drag/key handlers 的 isBlocked guard 擋,不需 pointer-events-none)\n 'data-[state=loading]:cursor-progress',\n // disabled(2026-06-03 Q3:語意 token 非 opacity — dashed outline surface 走 DS outline-disabled 慣例;\n // bg→disabled,border 不變色,文字/icon 由 Empty disabled 控;cursor-not-allowed 現在生效)\n 'data-[state=disabled]:bg-disabled data-[state=disabled]:cursor-not-allowed',\n className,\n )}\n {...props}\n >\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n onChange={(e) => {\n // 2026-07-05 D4:dispatch 後清空 value — 瀏覽器對同路徑檔案不重發 change,\n // 不清空則「失敗修正後重選同檔 / 刪除後重傳同檔」靜默無反應\n filterAndDispatch(e.target.files)\n e.target.value = ''\n }}\n />\n {loading ? (\n <Empty\n icon={<CircularProgress size={48} />}\n title={loadingTitle}\n />\n ) : (\n children ?? (\n <Empty\n icon={UploadIcon}\n title={title}\n description={description}\n disabled={disabled}\n />\n )\n )}\n </div>\n )}\n {fileListNode}\n </div>\n )\n },\n)\nFileUpload.displayName = 'FileUpload'\n\n// ── helper: bytes formatter ───────────────────────────────────────────────\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`\n if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`\n return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GB`\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────────\n\nfunction matchAccept(file: File, accept: string): boolean {\n const patterns = accept.split(',').map((s) => s.trim().toLowerCase())\n const fileName = file.name.toLowerCase()\n const fileType = file.type.toLowerCase()\n return patterns.some((p) => {\n if (p.startsWith('.')) return fileName.endsWith(p) // 副檔名 e.g. \".pdf\"\n if (p.endsWith('/*')) return fileType.startsWith(p.slice(0, -1)) // e.g. \"image/*\"\n return fileType === p // 完整 MIME\n })\n}\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 fileUploadMeta = {\n component: 'FileUpload',\n family: null, // non-family composite / overlay / layout\n variants: {\n dropzone: { purpose: '大型 dashed 拖放區(預設)— 拖放有價值 + 主要上傳區(drag + click)' },\n button: { purpose: '緊湊 Button 觸發 — form 內單欄 / 空間省(click-only)' },\n },\n sizes: {\n\n },\n // 'active' 移除 — dropzone 僅 hover / drag-over 態,無按壓視覺(2026-07-07 詞彙統一 DS-wide 按壓訊號盤點:檔內 0 active: utility / 0 *-active token)。\n states: ['default', 'hover', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface', 'bg-disabled'],\n fg: [],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { FileUpload }\n"],"names":["files","UploadIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-upload.js","sources":["../../../src/components/FileUpload/file-upload.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { Upload as UploadIcon, X } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Empty } from '@/design-system/components/Empty/empty'\nimport { CircularProgress } from '@/design-system/components/CircularProgress/circular-progress'\nimport { FileItem } from '@/design-system/components/FileItem/file-item'\nimport { Button } from '@/design-system/components/Button/button'\n\n/**\n * FileUpload — 拖放 / 點擊上傳區塊\n *\n * 世界級對照:Ant Design `Upload.Dragger`、Polaris `DropZone`、Material community MUI-File-Input。\n * 與本 DS 既有 FileItem(顯示已上傳檔案)配對 — 這裡 own「上傳觸發 + 拖放偵測」,\n * 上傳後的檔案清單顯示交給 consumer 用 FileItem 渲染。\n *\n * ── 4 狀態(2026-06-03 修正)──\n * idle (default) — border-dashed border-border bg-surface\n * hover = drag-over — border-primary-hover(統一,純 border-driven,底維持 surface;2026-07-06 對齊 Ant colorPrimaryHover)\n * loading — CircularProgress;cursor-progress(無 pointer-events-none)\n * disabled — bg-disabled + 文字 fg-disabled(語意 token,非 opacity)+ cursor-not-allowed\n *\n * ── variant ──\n * dropzone(預設)大拖放區 + drag;button 緊湊 Button 觸發(form-friendly,click-only)\n *\n * ── children 插槽 ──\n * 預設渲染 `<Empty icon={Upload} title description />` — 重用 Empty 元件 own\n * 的「icon + title + description 垂直居中」SSOT 避免視覺漂移。Empty 改字體 /\n * gap / icon 尺寸時 FileUpload 自動跟進。若 consumer 傳 children 則整個覆寫。\n *\n * ── API ──\n * onUpload: 使用者選取或拖放檔案時觸發,回傳 File[](至少 1 個)。\n * multiple: 允許多檔。預設 false(單檔)。\n * accept: MIME filter(例 \"image/*,.pdf\"),傳給 <input type=file>。\n * maxSize: 單檔最大 bytes;超過靜默忽略(consumer 若需要錯誤提示,走 onReject)。\n * onReject: 被 maxSize / accept 擋下的檔案(提供錯誤訊息顯示機會)。\n */\n\n/**\n * Uploaded / uploading file status item(for `files` prop)。\n * Consumer 持 state(progress / status),FileUpload 只負責渲染。\n */\nexport interface FileUploadStatus {\n id: string\n name: string\n /** bytes */\n size?: number\n /** Upload 進度(0-100)— uploading 時顯示 progress bar */\n progress?: number\n status?: 'uploading' | 'completed' | 'error'\n /** Error 訊息 / size 等 description */\n description?: React.ReactNode\n /** Thumbnail URL for rich mode */\n thumbnailSrc?: string\n}\n\nexport interface FileUploadProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onDrop'> {\n onUpload?: (files: File[]) => void\n onReject?: (files: File[], reason: 'size' | 'type') => void\n multiple?: boolean\n accept?: string\n maxSize?: number\n disabled?: boolean\n /**\n * 觸發外觀(2026-06-03 加,M21 prop-variant + Ant `Upload`(button)/`Upload.Dragger`(dropzone)先例):\n * - `dropzone`(預設):大拖放區 + 點擊,支援 drag-and-drop。\n * - `button`:緊湊 Button 觸發(form-friendly,省空間),click-only(無拖放區)。\n * 兩者共用 onUpload / onReject / files 清單渲染;都可放進 Field control slot。\n */\n variant?: 'dropzone' | 'button'\n /** `variant=\"button\"` 的按鈕文字(預設「Choose file」)。 */\n buttonLabel?: string\n /**\n * Loading 狀態(async 上傳 / 伺服器處理中)。\n * - 2026-06-03 **deferred**:其唯一用途(無清單單檔 / 頭像替換)場景尚未定義,故已從 3-state showcase\n * 移除、不作為當前 feature 呈現;prop 保留供未來該場景。有清單的上傳進度走 FileItem 自身 progress bar(status=uploading)。\n * - 顯示 CircularProgress 取代預設 Empty 內容;`cursor-progress`;互動由 handleClick/handlers 的 isBlocked guard 擋\n * - 宣告 `aria-busy=\"true\"` 讓 screen reader 感知處理中\n */\n loading?: boolean\n /** Loading 狀態的文字標題(預設「上傳中…」) */\n loadingTitle?: string\n /** 標題文字(預設「Click or drag file here to upload」) */\n title?: string\n /** 說明文字(預設依 multiple:單檔「Support for a single file upload」、multiple 多檔「Support for a single or bulk upload」) */\n description?: string\n /** 若傳入 children,覆寫預設 Empty 結構 */\n children?: React.ReactNode\n /**\n * Uploaded / uploading 檔案清單。傳入 → FileUpload 在 drop zone 下方渲染列表。\n * 不傳 → 不顯示(consumer 可自行用 FileItem 組成,pre-2026-04-24 行為)。\n * 每項由 FileItem 渲染,status 狀態對應視覺:\n * - `uploading`:linear ProgressBar(via FileItem;rich mode 另顯示 {progress}%)\n * - `completed`:綠色 ✓(success 狀態視覺確認)\n * - `error`:紅色 ✗(+ description 顯示錯誤訊息)\n */\n files?: FileUploadStatus[]\n /** File list 每項顯示模式。Default: 'compact'(單行);'rich' = 含 thumbnail / size / progress bar */\n fileListMode?: 'compact' | 'rich'\n /** File list 移除 callback。有值 → 每項右側顯示 X dismiss button;無 → 不可移除(view-only) */\n onRemove?: (id: string) => void\n /** File list dismiss button ARIA label template。預設 `移除 {name}`。For i18n. */\n removeAriaLabel?: (name: string) => string\n}\n\nfunction focusAfterFileRemoval(current: HTMLButtonElement) {\n const list = current.closest('ul')\n const buttons = list ? Array.from(list.querySelectorAll<HTMLButtonElement>('[data-collection-remove]')) : []\n const index = buttons.indexOf(current)\n const next = buttons[index + 1] ?? buttons[index - 1]\n const owner = list?.parentElement?.querySelector<HTMLElement>('[data-file-upload-owner]')\n ;(next ?? owner)?.focus()\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(\n (\n {\n onUpload,\n onReject,\n multiple = false,\n accept,\n maxSize,\n disabled = false,\n variant = 'dropzone',\n buttonLabel = 'Choose file', // i18n-allow: DS default; consumer override via buttonLabel prop\n loading = false,\n loadingTitle = '上傳中…', // i18n-allow: DS default; consumer override via loadingTitle prop\n title = 'Click or drag file here to upload', // i18n-allow: DS default; consumer override via title prop\n description = multiple ? 'Support for a single or bulk upload' : 'Support for a single file upload', // i18n-allow: DS default; consumer override via description prop\n children,\n files,\n fileListMode = 'compact',\n onRemove,\n removeAriaLabel = (name: string) => `移除 ${name}`, // i18n-allow: DS default; consumer override via removeAriaLabel prop\n className,\n onClick,\n ...props\n },\n ref,\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const [isDragOver, setDragOver] = React.useState(false)\n // drag enter/leave 深度計數(react-dropzone / Mantine Dropzone 同款):跨進內部子元素時\n // 瀏覽器發「子 dragenter → 外層 dragleave」成對事件,+1/−1 抵銷後計數仍 > 0 = 沒離開。\n // 不依賴 e.relatedTarget —— Safari/WebKit 的 dragleave relatedTarget 恆 null,guard 法必失效。\n const dragDepth = React.useRef(0)\n\n const filterAndDispatch = (files: FileList | null) => {\n if (!files || files.length === 0) return\n const accepted: File[] = []\n const rejectedBySize: File[] = []\n const rejectedByType: File[] = []\n\n Array.from(files).forEach((f) => {\n if (maxSize != null && f.size > maxSize) {\n rejectedBySize.push(f)\n return\n }\n if (accept && !matchAccept(f, accept)) {\n rejectedByType.push(f)\n return\n }\n accepted.push(f)\n })\n\n if (rejectedBySize.length) onReject?.(rejectedBySize, 'size')\n if (rejectedByType.length) onReject?.(rejectedByType, 'type')\n if (accepted.length) onUpload?.(multiple ? accepted : accepted.slice(0, 1))\n }\n\n const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n // 2026-06-03 Q4:guard `!disabled && !loading`(= !isBlocked)。loading 時也擋點擊防 double-submit\n // (原只 guard !disabled,靠 pointer-events-none 擋 loading;移除 pointer-events-none 後改這裡 guard)。\n if (!disabled && !loading) inputRef.current?.click()\n onClick?.(e)\n }\n\n // State 優先序:disabled > loading > drag-over > idle(disabled 最硬,loading 次之)\n const state = disabled ? 'disabled' : loading ? 'loading' : isDragOver ? 'drag-over' : 'idle'\n const isBlocked = disabled || loading\n\n const hasFiles = Array.isArray(files) && files.length > 0\n\n const fileListNode = hasFiles ? (\n <ul\n className={cn(\n 'flex flex-col w-full',\n // 2026-06-03 gap SSOT:列間 gap + control→list gap(mt)由「item 有無邊框」單一規則決定,\n // 同值貫穿整個垂直堆疊 — rich(form surface = border card)→ 8px;compact(borderless/bg-pill)→ 4px。\n // 消費 FileItem「List wrapper canonical」(file-item.spec.md),取代原硬寫 gap-2 / mt-3(不分 mode)。\n fileListMode === 'rich' ? 'gap-2 mt-2' : 'gap-1 mt-1',\n )}\n aria-label=\"已上傳的檔案\"\n >\n {files!.map((f) => (\n <li key={f.id} className=\"list-none\">\n <FileItem\n mode={fileListMode}\n name={f.name}\n status={f.status}\n progress={f.progress}\n description={f.description ?? (f.size != null ? formatBytes(f.size) : undefined)}\n thumbnailSrc={f.thumbnailSrc}\n actions={\n onRemove ? (\n // Collection remove(per-file)— 不是 dismiss surface,故不套 `dismiss` prop。\n // 視覺與 dismiss 一致(text variant + fg-muted dim)— 對齊 inline-action.spec.md\n // 「Dismiss canonical — X close only」+「dismiss prop 觸發條件」段:onRemove callback 不觸發 dismiss prop。\n <Button\n iconOnly\n variant=\"text\"\n size=\"xs\"\n startIcon={X}\n aria-label={removeAriaLabel(f.name)}\n data-collection-remove\n onClick={(e) => {\n e.stopPropagation()\n focusAfterFileRemoval(e.currentTarget)\n onRemove(f.id)\n }}\n className=\"text-fg-muted hover:text-fg-secondary\"\n />\n ) : undefined\n }\n />\n </li>\n ))}\n </ul>\n ) : null\n\n return (\n <div ref={ref} className={cn('w-full', hasFiles && 'flex flex-col')}>\n {variant === 'button' ? (\n // ── button variant:緊湊觸發(form-friendly,省空間),click-only(無拖放區)──\n // consumer onClick 在此 wrapper div 承載(type = MouseEvent<HTMLDivElement>,對齊 FileUploadProps\n // extends HTMLAttributes<HTMLDivElement>)。Button 觸發開檔會冒泡至此 → 同時觸發 consumer handler。\n // 2026-07-17 修:onClick 於 L128 destructure 後未再 compose,button variant 靜默丟失公開 DOM prop\n // (dropzone variant 經 handleClick 的 onClick?.(e) 正確 compose,button variant 漏接)。\n <div className={className} onClick={onClick} {...props}>\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n onChange={(e) => {\n // 2026-07-05 D4:dispatch 後清空 value — 瀏覽器對同路徑檔案不重發 change,\n // 不清空則「失敗修正後重選同檔 / 刪除後重傳同檔」靜默無反應\n filterAndDispatch(e.target.files)\n e.target.value = ''\n }}\n />\n <Button\n data-file-upload-owner\n variant=\"tertiary\"\n startIcon={UploadIcon}\n loading={loading}\n disabled={disabled}\n aria-busy={loading || undefined}\n onClick={() => {\n if (!disabled && !loading) inputRef.current?.click()\n }}\n >\n {buttonLabel}\n </Button>\n </div>\n ) : (\n <div\n data-file-upload-owner\n role=\"button\"\n tabIndex={isBlocked ? -1 : 0}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n data-state={state}\n onClick={handleClick}\n onKeyDown={(e) => {\n if (isBlocked) return\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n inputRef.current?.click()\n }\n }}\n onDragEnter={(e) => {\n if (isBlocked) return\n e.preventDefault()\n // 2026-07-06 深度計數升級(user 實測抓到「拖入約一秒高亮熄滅」):游標從邊緣飄到中央\n // 跨進 Empty icon/標題那刻,瀏覽器發「子 dragenter → 外層 dragleave」→ 舊版無 guard 直接\n // 熄滅;2026-07-05 的 relatedTarget guard 只救 Chrome(Safari/WebKit dragleave 的\n // relatedTarget 恆 null → guard 失效)。深度計數不依賴 relatedTarget,跨瀏覽器穩定。\n dragDepth.current += 1\n setDragOver(true)\n }}\n onDragLeave={(e) => {\n if (isBlocked) return\n e.preventDefault()\n dragDepth.current = Math.max(0, dragDepth.current - 1)\n if (dragDepth.current === 0) setDragOver(false)\n }}\n onDragOver={(e) => {\n // 2026-07-05 D4:blocked 時仍先 preventDefault — dragover 未取消 = 非合法 drop target,\n // 瀏覽器 default 會在當前 tab 直接開啟被丟檔案(整頁導航,app 狀態全失);吞掉操作而非讓瀏覽器接管\n e.preventDefault()\n }}\n onDrop={(e) => {\n // 2026-07-05 D4:preventDefault 先於 isBlocked guard — 同 dragover,擋瀏覽器開檔導航\n e.preventDefault()\n if (isBlocked) return\n dragDepth.current = 0 // drop 結束整輪拖曳,計數歸零(下一輪從乾淨狀態起算)\n setDragOver(false)\n filterAndDispatch(e.dataTransfer.files)\n }}\n className={cn(\n // 寬度 w-full 填滿 consumer 容器(user 明示「寬度填滿」);高度由 padding + 內容物決定(不固定 h)\n 'flex flex-col items-center justify-center gap-2 text-center w-full',\n // 對稱 padding p-[var(--layout-space-loose)]:四邊等距,density-aware(md=16px / lg=24px),對齊 DS chrome padding canonical。\n // 不再硬寫 px-6 py-10(不對稱+非 token)。內容物(icon + title + description)垂直堆疊由 gap-2 控制\n 'rounded-md border-2 border-dashed p-[var(--layout-space-loose)]',\n 'cursor-pointer transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n // idle:--border(元件邊框,非 --divider 分隔線 — 2026-06-03 Q2 token 修正)+ surface 底\n 'border-border bg-surface',\n // hover = drag-over 統一(2026-06-03 Q2-A 純 border-driven,對齊 Ant Dragger colorPrimaryHover):\n // 兩者都 → primary-hover 邊框(2026-07-06 修:註解與 spec 一直宣稱 colorPrimaryHover、\n // code 卻寫 base = claim-vs-code 反向 drift;瞬時態歸 hover 階,雙向全掃唯一 B 類違規),\n // 底色維持 surface(不變 bg)。state 信號靠邊框,非底色。\n // 2026-06-11 R2(M5 狀態疊加):hover 變 primary 僅限 idle —— disabled/loading 維持原邊框(spec「disabled 邊框不變色」;dropzone 狀態機無 error 態,error 屬 files 清單 FileItem status)\n 'data-[state=idle]:hover:border-primary-hover data-[state=drag-over]:border-primary-hover',\n // loading(2026-06-03 Q4:移除 pointer-events-none — 它會讓 cursor-progress 失效;\n // 互動已由 handleClick + drag/key handlers 的 isBlocked guard 擋,不需 pointer-events-none)\n 'data-[state=loading]:cursor-progress',\n // disabled(2026-06-03 Q3:語意 token 非 opacity — dashed outline surface 走 DS outline-disabled 慣例;\n // bg→disabled,border 不變色,文字/icon 由 Empty disabled 控;cursor-not-allowed 現在生效)\n 'data-[state=disabled]:bg-disabled data-[state=disabled]:cursor-not-allowed',\n className,\n )}\n {...props}\n >\n <input\n ref={inputRef}\n type=\"file\"\n className=\"hidden\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n onChange={(e) => {\n // 2026-07-05 D4:dispatch 後清空 value — 瀏覽器對同路徑檔案不重發 change,\n // 不清空則「失敗修正後重選同檔 / 刪除後重傳同檔」靜默無反應\n filterAndDispatch(e.target.files)\n e.target.value = ''\n }}\n />\n {loading ? (\n <div role=\"status\" aria-label={loadingTitle} className=\"flex flex-col items-center text-center\">\n <div className=\"mb-4\"><CircularProgress size={48} /></div>\n <span aria-hidden className=\"text-body-lg font-medium text-foreground\">{loadingTitle}</span>\n </div>\n ) : (\n children ?? (\n <Empty\n icon={UploadIcon}\n title={title}\n description={description}\n disabled={disabled}\n />\n )\n )}\n </div>\n )}\n {fileListNode}\n </div>\n )\n },\n)\nFileUpload.displayName = 'FileUpload'\n\n// ── helper: bytes formatter ───────────────────────────────────────────────\nfunction formatBytes(n: number): string {\n if (n < 1024) return `${n} B`\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`\n if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`\n return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GB`\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────────\n\nfunction matchAccept(file: File, accept: string): boolean {\n const patterns = accept.split(',').map((s) => s.trim().toLowerCase())\n const fileName = file.name.toLowerCase()\n const fileType = file.type.toLowerCase()\n return patterns.some((p) => {\n if (p.startsWith('.')) return fileName.endsWith(p) // 副檔名 e.g. \".pdf\"\n if (p.endsWith('/*')) return fileType.startsWith(p.slice(0, -1)) // e.g. \"image/*\"\n return fileType === p // 完整 MIME\n })\n}\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 fileUploadMeta = {\n component: 'FileUpload',\n family: null, // non-family composite / overlay / layout\n variants: {\n dropzone: { purpose: '大型 dashed 拖放區(預設)— 拖放有價值 + 主要上傳區(drag + click)' },\n button: { purpose: '緊湊 Button 觸發 — form 內單欄 / 空間省(click-only)' },\n },\n sizes: {\n\n },\n // 'active' 移除 — dropzone 僅 hover / drag-over 態,無按壓視覺(2026-07-07 詞彙統一 DS-wide 按壓訊號盤點:檔內 0 active: utility / 0 *-active token)。\n states: ['default', 'hover', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface', 'bg-disabled'],\n fg: [],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { FileUpload }\n"],"names":["files","UploadIcon"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.d.ts","sourceRoot":"","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,6BAA6B,CAAA;AAElE,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAG3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;
|
|
1
|
+
{"version":3,"file":"radio-group.d.ts","sourceRoot":"","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,6BAA6B,CAAA;AAElE,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAG3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAc1E,MAAM,WAAW,eAIf,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,mBAAmB,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC;IACxF;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AAkCD,QAAA,MAAM,UAAU,wFAmId,CAAA;AAWF,QAAA,MAAM,iBAAiB;;8EA2BtB,CAAA;AAOD,KAAK,uBAAuB,GAAG,KAAK,CAAC,wBAAwB,CAAC,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAA;AAE9F,MAAM,WAAW,mBAGf,SAAQ,IAAI,CAAC,uBAAuB,EAAE,SAAS,GAAG,UAAU,CAAC,EAC3D,YAAY,CAAC,OAAO,iBAAiB,CAAC;IACxC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,qHAAqH;IACrH,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAID,QAAA,MAAM,cAAc,+FA6FnB,CAAA;AAKD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwBjB,CAAA;AAEV,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,iBAAiB,EAAE,CAAA"}
|
|
@@ -7,6 +7,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
7
7
|
import { useResolvedFieldMode, useFieldEmptyDisplay, useFieldContext, useResolvedFieldSize, fieldEmptyColorClass, useResolvedFieldDisabled } from "../Field/field-context.js";
|
|
8
8
|
import { SelectionItem } from "../SelectionControl/selection-item.js";
|
|
9
9
|
import { fieldWrapperStyles } from "../Field/field-wrapper.js";
|
|
10
|
+
import { useControllable } from "../../hooks/use-controllable.js";
|
|
10
11
|
const RadioGroupReadonlyContext = React.createContext(false);
|
|
11
12
|
const RadioGroupDisabledContext = React.createContext(false);
|
|
12
13
|
function findSelectedRadioLabel(children, selectedValue) {
|
|
@@ -28,11 +29,32 @@ function findSelectedRadioLabel(children, selectedValue) {
|
|
|
28
29
|
});
|
|
29
30
|
return selectedLabel;
|
|
30
31
|
}
|
|
31
|
-
const RadioGroup = React.forwardRef(({ className, mode, variant: _chrome, value, defaultValue, ...props }, ref) => {
|
|
32
|
+
const RadioGroup = React.forwardRef(({ className, mode, variant: _chrome, value, defaultValue, onValueChange, children, ...props }, ref) => {
|
|
32
33
|
const resolvedMode = useResolvedFieldMode({ mode, disabled: props.disabled });
|
|
33
34
|
const emptyDisplay = useFieldEmptyDisplay();
|
|
34
35
|
const fieldCtx = useFieldContext();
|
|
35
36
|
const resolvedBoxSize = useResolvedFieldSize(void 0, "md");
|
|
37
|
+
const isControlled = value !== void 0;
|
|
38
|
+
const [resolvedValue, setResolvedValue] = useControllable({
|
|
39
|
+
value: value === null ? "" : value,
|
|
40
|
+
defaultValue: defaultValue ?? "",
|
|
41
|
+
onChange: onValueChange
|
|
42
|
+
});
|
|
43
|
+
const innerRef = React.useRef(null);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
var _a;
|
|
46
|
+
if (isControlled) return;
|
|
47
|
+
const form = (_a = innerRef.current) == null ? void 0 : _a.closest("form");
|
|
48
|
+
if (!form) return;
|
|
49
|
+
const handleReset = () => setResolvedValue(defaultValue ?? "");
|
|
50
|
+
form.addEventListener("reset", handleReset);
|
|
51
|
+
return () => form.removeEventListener("reset", handleReset);
|
|
52
|
+
}, [defaultValue, isControlled, resolvedMode, setResolvedValue]);
|
|
53
|
+
const setRef = React.useCallback((element) => {
|
|
54
|
+
innerRef.current = element;
|
|
55
|
+
if (typeof ref === "function") ref(element);
|
|
56
|
+
else if (ref) ref.current = element;
|
|
57
|
+
}, [ref]);
|
|
36
58
|
const {
|
|
37
59
|
onValueChange: _onValueChange,
|
|
38
60
|
disabled: _disabled,
|
|
@@ -42,26 +64,25 @@ const RadioGroup = React.forwardRef(({ className, mode, variant: _chrome, value,
|
|
|
42
64
|
dir: _dir,
|
|
43
65
|
loop: _loop,
|
|
44
66
|
asChild: _asChild,
|
|
45
|
-
children: _children,
|
|
46
67
|
...restDomProps
|
|
47
68
|
} = props;
|
|
48
69
|
if (resolvedMode === "view") {
|
|
49
|
-
const selectedValue =
|
|
70
|
+
const selectedValue = resolvedValue || void 0;
|
|
50
71
|
if (!selectedValue) {
|
|
51
|
-
return /* @__PURE__ */ jsx("div", { ...restDomProps, ref, role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: fieldEmptyColorClass(resolvedMode), children: emptyDisplay }) });
|
|
72
|
+
return /* @__PURE__ */ jsx("div", { ...restDomProps, ref: setRef, role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: fieldEmptyColorClass(resolvedMode), children: emptyDisplay }) });
|
|
52
73
|
}
|
|
53
|
-
const selectedLabel = findSelectedRadioLabel(
|
|
54
|
-
return /* @__PURE__ */ jsx("div", { ...restDomProps, ref, role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: "text-foreground", children: selectedLabel ?? selectedValue }) });
|
|
74
|
+
const selectedLabel = findSelectedRadioLabel(children, selectedValue);
|
|
75
|
+
return /* @__PURE__ */ jsx("div", { ...restDomProps, ref: setRef, role: "group", className: cn("grid", className), children: /* @__PURE__ */ jsx("span", { className: "text-foreground", children: selectedLabel ?? selectedValue }) });
|
|
55
76
|
}
|
|
56
77
|
if (resolvedMode === "readonly" && (fieldCtx == null ? void 0 : fieldCtx.hasFieldWrapper) === true) {
|
|
57
|
-
const selectedValue =
|
|
58
|
-
const selectedLabel = selectedValue ? findSelectedRadioLabel(
|
|
78
|
+
const selectedValue = resolvedValue || void 0;
|
|
79
|
+
const selectedLabel = selectedValue ? findSelectedRadioLabel(children, selectedValue) : null;
|
|
59
80
|
const boxSize = resolvedBoxSize;
|
|
60
81
|
return /* @__PURE__ */ jsx(
|
|
61
82
|
"div",
|
|
62
83
|
{
|
|
63
84
|
...restDomProps,
|
|
64
|
-
ref,
|
|
85
|
+
ref: setRef,
|
|
65
86
|
role: "radiogroup",
|
|
66
87
|
"aria-readonly": "true",
|
|
67
88
|
"aria-labelledby": fieldCtx == null ? void 0 : fieldCtx.labelId,
|
|
@@ -81,15 +102,16 @@ const RadioGroup = React.forwardRef(({ className, mode, variant: _chrome, value,
|
|
|
81
102
|
RadioGroupPrimitive.Root,
|
|
82
103
|
{
|
|
83
104
|
className: cn("grid", className),
|
|
84
|
-
value,
|
|
85
|
-
|
|
105
|
+
value: resolvedValue,
|
|
106
|
+
onValueChange: setResolvedValue,
|
|
86
107
|
"aria-labelledby": fieldCtx == null ? void 0 : fieldCtx.labelId,
|
|
87
108
|
"aria-invalid": (fieldCtx == null ? void 0 : fieldCtx.invalid) || void 0,
|
|
88
109
|
"aria-describedby": fieldCtx == null ? void 0 : fieldCtx.descriptionId,
|
|
89
110
|
"aria-readonly": resolvedMode === "readonly" || void 0,
|
|
90
111
|
...props,
|
|
91
112
|
disabled: resolvedMode === "disabled",
|
|
92
|
-
ref
|
|
113
|
+
ref: setRef,
|
|
114
|
+
children
|
|
93
115
|
}
|
|
94
116
|
) }) });
|
|
95
117
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.js","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { Circle } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { FieldMode, FieldVariant } from \"@/design-system/components/Field/field-types\"\nimport { useFieldContext, useResolvedFieldMode, useResolvedFieldDisabled, useResolvedFieldSize } from \"@/design-system/components/Field/field-context\"\nimport { SelectionItem } from \"@/design-system/components/SelectionControl/selection-item\"\nimport type { LucideIcon } from \"lucide-react\"\nimport type { AvatarData } from \"@/design-system/components/Avatar/avatar\"\nimport { fieldWrapperStyles } from \"@/design-system/components/Field/field-wrapper\"\nimport { useFieldEmptyDisplay, fieldEmptyColorClass } from '@/design-system/components/Field/field-context'\n\n// ── RadioGroup view mode ────────────────────────────────────────────────────\n// RadioGroup mode='view' 時:Group 不渲染 Radix primitive(無 radio 視覺),\n// 改由 RadioGroup 本體 walk props.children,找 control.value === selectedValue 的\n// SelectionItem,把它的 label 渲染為單一純文字 span(其他選項不顯示)。\n// 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n// 實作在 RadioGroup forwardRef 內(見下方 mode === 'view' 分支)。\n\n// ── RadioGroup ──────────────────────────────────────────────────────────────\n\nexport interface RadioGroupProps\n // asChild Omit(2026-07-18 決策2):RadioGroup 恆持有多個 radio item → <Root asChild> 的\n // Radix Slot React.Children.only crash;view/readonly 模式更直接渲染固定 <div> 忽略之。\n // children 保留(edit 容器內容 + view/readonly 走訪選中 label,見 body cast @line ~105)。\n extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>, 'asChild'> {\n /**\n * Field mode(2026-05-05 Phase B3 align):\n * edit — 一般可互動 RadioGroup(預設)\n * view — **純展示**:不渲染 Radix Root / 任何 radio 視覺;RadioGroup 本體 walk\n * children,僅 control.value === group.value 那筆把 label 渲染為純文字 span。\n * 對齊 Carbon read-only / DataTable single-select cell read mode。\n * readonly — standalone:ReadonlyContext 傳 items 鎖互動保留視覺;Field 內:灰框 + 選中項 label(不渲染 radio 群組)\n * disabled — 同 RadioGroupPrimitive.Root disabled 屬性\n */\n mode?: FieldMode\n /**\n * Visual chrome — RadioGroup 本體無 input wrapper variant,本 prop 對主體無視覺影響;\n * 為對齊 Field 4-mode + chrome 透傳契約而保留(M19 一致性)。\n */\n variant?: FieldVariant\n}\n\n// RadioGroup mode='readonly' → 透過 context 把 readOnly 傳給所有 child RadioGroupItem\n// (item 已支援 readOnly prop + data-[readonly] 樣式;Radix Root 無 readOnly,故用 context)。\nconst RadioGroupReadonlyContext = React.createContext(false)\n\n// group-level disabled(standalone <RadioGroup disabled>)→ context 傳給 items,讓 SelectionItem\n// label/description 也降 text-fg-disabled(Radix Root disabled 只 propagate 給 radio 圈、不碰 label 色)。\n// 對齊本檔 RadioGroupReadonlyContext 既有模式 + spec「Disabled」邊界案例(Group-level disabled 視覺繼承 SelectionItem SSOT text-fg-disabled)。\nconst RadioGroupDisabledContext = React.createContext(false)\n\n// walk children 找 control.value === selectedValue 的 SelectionItem label(view / readonly-in-Field 共用)\nfunction findSelectedRadioLabel(children: React.ReactNode, selectedValue: string | undefined): React.ReactNode {\n let selectedLabel: React.ReactNode = null\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return\n const cProps = child.props as { control?: unknown; label?: React.ReactNode; value?: unknown }\n // 形狀 1:<RadioGroupItem value label>(主用法)— value/label 直掛 props\n if (cProps.value === selectedValue && cProps.value !== undefined) {\n selectedLabel = cProps.label ?? selectedValue\n return\n }\n // 形狀 2:<SelectionItem control={<RadioGroupItem value/>} label>(組合用法)\n const control = cProps.control\n if (React.isValidElement(control)) {\n const controlValue = (control.props as { value?: unknown }).value\n if (controlValue === selectedValue) {\n selectedLabel = cProps.label ?? selectedValue\n }\n }\n })\n return selectedLabel\n}\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n RadioGroupProps\n>(({ className, mode, variant: _chrome, value, defaultValue, ...props }, ref) => {\n // 2026-06-08 SSOT cascade:resolvedMode 經 resolver hook 讀 fieldCtx(原 root 完全不讀 → <Field disabled>/<Field mode> 失效)\n const resolvedMode = useResolvedFieldMode({ mode, disabled: (props as { disabled?: boolean }).disabled })\n const emptyDisplay = useFieldEmptyDisplay()\n const fieldCtx = useFieldContext()\n // readonly 灰框 size:走 SSOT resolver(RadioGroup 無 size prop → ctx > 'md')\n const resolvedBoxSize = useResolvedFieldSize(undefined, 'md') as 'sm' | 'md' | 'lg'\n // mode='view' — 純展示 selected option 的 label,不渲染任何 radio control 視覺。\n // 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n // 實作:walk children 找 control.value === selectedValue 的 SelectionItem,render label plain text。\n // (不用 context dispatch 給 RadioGroupItem — SelectionItem layout wrapper 仍會渲染所有 item label)\n // 2026-07-04 修:view / readonly-in-Field 分支的 <div> 原丟棄剩餘 props(id/data-*/aria-*)\n // 且不轉發 forwardRef ref → 自家 radio-group.stories.tsx view demo 傳的 aria-label 都靜默失效。\n // 抽出 Radix 專屬 non-DOM props,其餘 DOM props 於兩分支 spread 轉發(與 Checkbox/Switch 同修);\n // edit 主路徑不受影響(Root 仍 spread 完整 props)。\n const {\n onValueChange: _onValueChange,\n disabled: _disabled,\n required: _required,\n name: _name,\n orientation: _orientation,\n dir: _dir,\n loop: _loop,\n asChild: _asChild,\n children: _children,\n ...restDomProps\n } = props as RadioGroupProps & { asChild?: boolean }\n\n if (resolvedMode === 'view') {\n const selectedValue = (value ?? defaultValue) as string | undefined\n if (!selectedValue) {\n return <div {...restDomProps} ref={ref as React.Ref<HTMLDivElement>} role=\"group\" className={cn('grid', className)}><span className={fieldEmptyColorClass(resolvedMode)}>{emptyDisplay}</span></div>\n }\n const selectedLabel = findSelectedRadioLabel(props.children, selectedValue)\n return (\n <div {...restDomProps} ref={ref as React.Ref<HTMLDivElement>} role=\"group\" className={cn('grid', className)}>\n <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n </div>\n )\n }\n\n // ── mode='readonly' in Field(2026-06-12 user 拍板,與 Checkbox/Switch 灰框模型一致)──\n // Field 內 readonly 單選 = fieldWrapperStyles readonly 灰框 + 選中項 label 文字\n // (= Select readonly 同款呈現:同為「單選資料」,鎖定時呈現一致)。\n // standalone readonly(無 Field)維持原樣鎖互動(ReadonlyContext 路徑)。\n if (resolvedMode === 'readonly' && fieldCtx?.hasFieldWrapper === true) {\n const selectedValue = (value ?? defaultValue) as string | undefined\n const selectedLabel = selectedValue ? findSelectedRadioLabel(props.children, selectedValue) : null\n const boxSize = resolvedBoxSize\n return (\n <div\n {...restDomProps}\n ref={ref as React.Ref<HTMLDivElement>}\n role=\"radiogroup\"\n aria-readonly=\"true\"\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n data-readonly=\"true\"\n tabIndex={0}\n className={cn(\n fieldWrapperStyles({ size: boxSize, mode: 'readonly', variant: 'default' }),\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n >\n {selectedValue\n ? <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n : <span className={fieldEmptyColorClass(resolvedMode)}>{emptyDisplay}</span>}\n </div>\n )\n }\n\n // mode='disabled' → Radix Root disabled(原生 propagate 給所有 item);\n // mode='readonly' → context 傳 readOnly 給 items(item 渲染為 data-[readonly] 鎖互動 + aria-readonly)。\n return (\n <RadioGroupReadonlyContext.Provider value={resolvedMode === 'readonly'}>\n <RadioGroupDisabledContext.Provider value={resolvedMode === 'disabled'}>\n <RadioGroupPrimitive.Root\n className={cn(\"grid\", className)}\n value={value}\n defaultValue={defaultValue}\n // Field 內:role=\"radiogroup\" 的 <div> 無法被 <label for> 命名 → 自讀 fieldCtx 接\n // aria-labelledby / aria-invalid / aria-describedby(field-context.ts「下游 control 需自讀\n // labelId」;對齊 slider.tsx / Rating / TimePicker 的 fieldLabelId 接線 + 上方 readonly-in-Field 分支)。\n // standalone(無 Field)時皆 undefined,consumer 的 {...props} aria-* 照舊生效(置於 spread 前故可被覆蓋)。\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n aria-describedby={fieldCtx?.descriptionId}\n // a11y(2026-07-14 dim-10):readonly 語意放 radiogroup(aria-readonly 合法 role;\n // 對齊上方 readonly-in-Field 分支 L133)。item(role=radio)不帶 aria-readonly —\n // axe 實證 radio 不允許該屬性,item 靠 data-readonly + tabIndex + click guard 鎖互動。\n aria-readonly={resolvedMode === 'readonly' || undefined}\n {...props}\n disabled={resolvedMode === 'disabled'}\n ref={ref}\n />\n </RadioGroupDisabledContext.Provider>\n </RadioGroupReadonlyContext.Provider>\n )\n})\nRadioGroup.displayName = 'RadioGroup'\n// Field layout 宣告:RadioGroup 是 block primitive(多項堆疊),\n// 進入 <Field> 時 control area 自動切 items-start(block 分支不設 min-h、不加 padding-top,\n// 高度由內容決定;額外加 paddingTop 會 double padding — field.tsx block control area SSOT)。\n// Convention 詳見 components/Field/field.spec.md「Control area:Inline vs Block」段落。\n;(RadioGroup as unknown as { fieldLayout: 'block' }).fieldLayout = 'block'\n\n// ── RadioGroupItem Variants ─────────────────────────────────────────────────\n// 與 Checkbox 完全對齊:sm/md=16px, lg=20px。差異只有形狀(rounded-full)和指示器(filled dot)。\n\nconst radioItemVariants = cva(\n [\n 'grid place-content-center shrink-0 rounded-full',\n 'border border-border bg-surface',\n 'transition-colors duration-150',\n 'hover:border-border-hover',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'data-[state=checked]:border-primary data-[state=checked]:text-primary',\n 'data-[state=checked]:hover:border-primary-hover data-[state=checked]:hover:text-primary-hover',\n 'disabled:cursor-not-allowed disabled:bg-disabled disabled:border-transparent disabled:hover:border-transparent',\n 'disabled:data-[state=checked]:bg-disabled disabled:data-[state=checked]:border-transparent disabled:data-[state=checked]:text-fg-disabled',\n // readOnly:鎖定互動但維持 checked/unchecked 視覺\n 'data-[readonly=true]:pointer-events-none data-[readonly=true]:cursor-default',\n 'data-[readonly=true]:hover:border-border',\n ],\n {\n variants: {\n size: {\n sm: 'h-4 w-4',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n }\n)\n\n// ── Dot Size ────────────────────────────────────────────────────────────────\nconst dotSize: Record<string, number> = { sm: 8, md: 8, lg: 10 }\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\ntype RadioItemPrimitiveProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n\nexport interface RadioGroupItemProps\n // asChild / children Omit(2026-07-18 決策2):item 固定渲染 Indicator dot(<Circle/>)—\n // children 被固定 Indicator 靜默覆蓋,asChild 破壞 radio 結構。對齊 Checkbox 收窄。label 走 `label` prop。\n extends Omit<RadioItemPrimitiveProps, 'asChild' | 'children'>,\n VariantProps<typeof radioItemVariants> {\n /**\n * Inline label。提供時 RadioGroupItem 自動透過 SelectionItem 包裝,\n * 套用 codified 樣式(text-body / text-foreground / disabled 色)。\n * 在 <Field> context 內時此 prop 仍然生效(Radio 的 label 是每個 item\n * 各自的,不是整組 Field 的;FieldLabel 是 RadioGroup 整體的 label)。\n */\n label?: React.ReactNode\n /**\n * Inline description(secondary 文字)。須與 label 搭配使用。\n * 套用 text-body / text-fg-secondary 樣式。\n */\n description?: React.ReactNode\n /** 可選左側 icon(label 前)— 2026-06-12 M30 修:轉發 SelectionItem 既有 canonical 槽(selection-item.tsx jsDoc SSOT;與 avatar 互斥)*/\n icon?: LucideIcon\n /** 可選左側 avatar(label 前)— 同上 */\n avatar?: AvatarData\n /**\n * readonly 模式:鎖定互動但維持 checked/unchecked 視覺正確。\n * 通常整個 RadioGroup 一起設 readonly(由 parent RadioGroup 的 disabled\n * 或 readonly 行為決定),個別 item 也可設。\n */\n readOnly?: boolean\n}\n\n// ── RadioGroupItem ──────────────────────────────────────────────────────────\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n RadioGroupItemProps\n>(\n (\n {\n className,\n size,\n label,\n icon,\n avatar,\n description,\n readOnly = false,\n disabled,\n id: idProp,\n ...props\n },\n ref\n ) => {\n const sizeKey = size ?? 'md'\n const dotPx = dotSize[sizeKey]\n\n // 注意:RadioGroup mode='view' 的純文字渲染由 RadioGroup 本體 walk-children 處理\n // (見上方 RadioGroup forwardRef 的 mode === 'view' 分支),RadioGroupItem 在 view\n // mode 下不會被獨立 render,故此處無 view 分支。\n\n // 注意:Radio 的 label 語意與 Checkbox/Switch 不同——\n // Checkbox/Switch 的 label 就是該 control 的唯一 label(被 Field context 接管),\n // RadioGroupItem 的 label 是「該選項」的 label(每 item 各自擁有),\n // FieldLabel 則是整個 RadioGroup 的 label。\n // 因此 RadioGroupItem 的 label 不因 Field context 被忽略。\n // group-level disabled(standalone <RadioGroup disabled>)也要降 label/description 色 —— Radix Root\n // disabled 只 propagate 給 radio 圈,label 色由 SelectionItem disabled 決定,故讀 RadioGroupDisabledContext 補上。\n const groupDisabled = React.useContext(RadioGroupDisabledContext)\n const resolvedDisabled = useResolvedFieldDisabled(disabled) || groupDisabled\n // group-level readonly(RadioGroup mode='readonly')或 item-level readOnly,任一 true 即鎖互動。\n const groupReadonly = React.useContext(RadioGroupReadonlyContext)\n const effectiveReadonly = readOnly || groupReadonly\n\n const generatedId = React.useId()\n const inputId = idProp ?? generatedId\n\n const rootEl = (\n <RadioGroupPrimitive.Item\n id={inputId}\n ref={ref}\n disabled={resolvedDisabled}\n // a11y(2026-07-14 dim-10):item(role=radio)不帶 aria-readonly — axe 實證 radio 不允許\n // 該屬性(readonly 語意由 Root role=radiogroup 宣告,見上方 L167-170)。item 靠\n // data-readonly + tabIndex + click guard 鎖互動。\n data-readonly={effectiveReadonly || undefined}\n tabIndex={effectiveReadonly ? -1 : undefined}\n className={cn(radioItemVariants({ size }), className)}\n {...props}\n // 2026-07-05 D4 修:readOnly 鎖互動補 label 繞道 — <label htmlFor> 的 synthetic click\n // 不是 pointer event,cva pointer-events-none 攔不到,點 label 文字仍選中。\n // preventDefault 讓 Radix composeEventHandlers(checkForDefaultPrevented)跳過內部 onCheck;\n // 必列在 {...props} 之後才不被 consumer onClick 覆蓋(與 Checkbox/Switch 同修)。\n // 同時攔 Radix roving-focus 的 arrow-key onFocus → click() 路徑(同為 click event)。\n onClick={(event) => {\n if (effectiveReadonly) {\n event.preventDefault()\n return\n }\n props.onClick?.(event)\n }}\n >\n <RadioGroupPrimitive.Indicator className=\"grid place-content-center\">\n <Circle\n style={{ width: dotPx, height: dotPx }}\n className=\"fill-current text-current\"\n />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n\n // 無 label → 只渲染 radio 本體\n if (label == null) return rootEl\n\n // 有 label → 透過 SelectionItem 包裝,與 Checkbox 一致(disabled 已於上方 useResolvedFieldDisabled 解析)\n return (\n <SelectionItem\n control={rootEl}\n label={label}\n description={description}\n icon={icon}\n avatar={avatar}\n htmlFor={inputId}\n disabled={resolvedDisabled}\n size={sizeKey}\n />\n )\n }\n)\nRadioGroupItem.displayName = 'RadioGroupItem'\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 radioGroupMeta = {\n component: 'RadioGroup',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L31;非 Family 4 — field-controls.spec.md 成員名單不含 RadioGroup)\n variants: {\n\n },\n sizes: {\n // iconSize = 渲染指示器尺寸(對齊 checkbox/switch meta 慣例:checkbox iconSize=真 Check glyph 12/12/16)。\n // Radio 指示器是 filled dot 非 glyph,真值 = 上方 `const dotSize` map 8/8/10;\n // 控件框 16/16/20 與 Checkbox 對齊但那是 box 非「指示器」,不入此鍵(避免 metadata 語意 drift)。\n sm: { fieldHeight: 28, iconSize: 8, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 8, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 10, typography: 'body-lg' },\n },\n // 2026-07-04 修 stale meta:移除 'active' — radioItemVariants cva 無 active-state 樣式,\n // Radix radio 也無 data-state=active(對照 checkboxMeta 正確不含 active)\n // 'checked' = 選定持續態(card 式 = border+text primary;2026-07-07 meta 詞彙統一補列)\n states: ['default', 'hover', 'checked', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-disabled', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { RadioGroup, RadioGroupItem, radioItemVariants }\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"radio-group.js","sources":["../../../src/components/RadioGroup/radio-group.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { Circle } from \"lucide-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { FieldMode, FieldVariant } from \"@/design-system/components/Field/field-types\"\nimport { useFieldContext, useResolvedFieldMode, useResolvedFieldDisabled, useResolvedFieldSize } from \"@/design-system/components/Field/field-context\"\nimport { SelectionItem } from \"@/design-system/components/SelectionControl/selection-item\"\nimport type { LucideIcon } from \"lucide-react\"\nimport type { AvatarData } from \"@/design-system/components/Avatar/avatar\"\nimport { fieldWrapperStyles } from \"@/design-system/components/Field/field-wrapper\"\nimport { useFieldEmptyDisplay, fieldEmptyColorClass } from '@/design-system/components/Field/field-context'\nimport { useControllable } from '@/design-system/hooks/use-controllable'\n\n// ── RadioGroup view mode ────────────────────────────────────────────────────\n// RadioGroup mode='view' 時:Group 不渲染 Radix primitive(無 radio 視覺),\n// 改由 RadioGroup 本體 walk props.children,找 control.value === selectedValue 的\n// SelectionItem,把它的 label 渲染為單一純文字 span(其他選項不顯示)。\n// 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n// 實作在 RadioGroup forwardRef 內(見下方 mode === 'view' 分支)。\n\n// ── RadioGroup ──────────────────────────────────────────────────────────────\n\nexport interface RadioGroupProps\n // asChild Omit(2026-07-18 決策2):RadioGroup 恆持有多個 radio item → <Root asChild> 的\n // Radix Slot React.Children.only crash;view/readonly 模式更直接渲染固定 <div> 忽略之。\n // children 保留(edit 容器內容 + view/readonly 走訪選中 label,見 body cast @line ~105)。\n extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>, 'asChild'> {\n /**\n * Field mode(2026-05-05 Phase B3 align):\n * edit — 一般可互動 RadioGroup(預設)\n * view — **純展示**:不渲染 Radix Root / 任何 radio 視覺;RadioGroup 本體 walk\n * children,僅 control.value === group.value 那筆把 label 渲染為純文字 span。\n * 對齊 Carbon read-only / DataTable single-select cell read mode。\n * readonly — standalone:ReadonlyContext 傳 items 鎖互動保留視覺;Field 內:灰框 + 選中項 label(不渲染 radio 群組)\n * disabled — 同 RadioGroupPrimitive.Root disabled 屬性\n */\n mode?: FieldMode\n /**\n * Visual chrome — RadioGroup 本體無 input wrapper variant,本 prop 對主體無視覺影響;\n * 為對齊 Field 4-mode + chrome 透傳契約而保留(M19 一致性)。\n */\n variant?: FieldVariant\n}\n\n// RadioGroup mode='readonly' → 透過 context 把 readOnly 傳給所有 child RadioGroupItem\n// (item 已支援 readOnly prop + data-[readonly] 樣式;Radix Root 無 readOnly,故用 context)。\nconst RadioGroupReadonlyContext = React.createContext(false)\n\n// group-level disabled(standalone <RadioGroup disabled>)→ context 傳給 items,讓 SelectionItem\n// label/description 也降 text-fg-disabled(Radix Root disabled 只 propagate 給 radio 圈、不碰 label 色)。\n// 對齊本檔 RadioGroupReadonlyContext 既有模式 + spec「Disabled」邊界案例(Group-level disabled 視覺繼承 SelectionItem SSOT text-fg-disabled)。\nconst RadioGroupDisabledContext = React.createContext(false)\n\n// walk children 找 control.value === selectedValue 的 SelectionItem label(view / readonly-in-Field 共用)\nfunction findSelectedRadioLabel(children: React.ReactNode, selectedValue: string | undefined): React.ReactNode {\n let selectedLabel: React.ReactNode = null\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return\n const cProps = child.props as { control?: unknown; label?: React.ReactNode; value?: unknown }\n // 形狀 1:<RadioGroupItem value label>(主用法)— value/label 直掛 props\n if (cProps.value === selectedValue && cProps.value !== undefined) {\n selectedLabel = cProps.label ?? selectedValue\n return\n }\n // 形狀 2:<SelectionItem control={<RadioGroupItem value/>} label>(組合用法)\n const control = cProps.control\n if (React.isValidElement(control)) {\n const controlValue = (control.props as { value?: unknown }).value\n if (controlValue === selectedValue) {\n selectedLabel = cProps.label ?? selectedValue\n }\n }\n })\n return selectedLabel\n}\n\nconst RadioGroup = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Root>,\n RadioGroupProps\n>(({ className, mode, variant: _chrome, value, defaultValue, onValueChange, children, ...props }, ref) => {\n // 2026-06-08 SSOT cascade:resolvedMode 經 resolver hook 讀 fieldCtx(原 root 完全不讀 → <Field disabled>/<Field mode> 失效)\n const resolvedMode = useResolvedFieldMode({ mode, disabled: (props as { disabled?: boolean }).disabled })\n const emptyDisplay = useFieldEmptyDisplay()\n const fieldCtx = useFieldContext()\n // readonly 灰框 size:走 SSOT resolver(RadioGroup 無 size prop → ctx > 'md')\n const resolvedBoxSize = useResolvedFieldSize(undefined, 'md') as 'sm' | 'md' | 'lg'\n\n // Wrapper 持有 value 的單一 resolved state,讓 uncontrolled edit 內的選擇在切換\n // view / readonly 時仍可讀取;否則值只住在 Radix Root,非 edit 分支會退回 defaultValue。\n // Radix 一律收到 controlled value,避免 mode remount 後重建另一份 internal state。\n const isControlled = value !== undefined\n const [resolvedValue, setResolvedValue] = useControllable<string>({\n value: value === null ? '' : value,\n defaultValue: defaultValue ?? '',\n onChange: onValueChange,\n })\n\n // form.reset() 不會觸發 Radix onValueChange;uncontrolled wrapper 取得狀態所有權後,\n // 需把 resolved value 明確歸回 defaultValue。resolvedMode 進 deps,切回 edit 時會\n // 重新綁定目前 Root 所在的 form;view/readonly 分支仍在同一 form 時也能保持 reset 語意。\n const innerRef = React.useRef<HTMLDivElement | null>(null)\n React.useEffect(() => {\n if (isControlled) return\n const form = innerRef.current?.closest('form')\n if (!form) return\n const handleReset = () => setResolvedValue(defaultValue ?? '')\n form.addEventListener('reset', handleReset)\n return () => form.removeEventListener('reset', handleReset)\n }, [defaultValue, isControlled, resolvedMode, setResolvedValue])\n const setRef = React.useCallback((element: HTMLDivElement | null) => {\n innerRef.current = element\n if (typeof ref === 'function') ref(element)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = element\n }, [ref])\n // mode='view' — 純展示 selected option 的 label,不渲染任何 radio control 視覺。\n // 對齊 Carbon read-only single-select(只顯示 selected 內容)+ Airtable / Notion read-only。\n // 實作:walk children 找 control.value === selectedValue 的 SelectionItem,render label plain text。\n // (不用 context dispatch 給 RadioGroupItem — SelectionItem layout wrapper 仍會渲染所有 item label)\n // 2026-07-04 修:view / readonly-in-Field 分支的 <div> 原丟棄剩餘 props(id/data-*/aria-*)\n // 且不轉發 forwardRef ref → 自家 radio-group.stories.tsx view demo 傳的 aria-label 都靜默失效。\n // 抽出 Radix 專屬 non-DOM props,其餘 DOM props 於兩分支 spread 轉發(與 Checkbox/Switch 同修);\n // edit 主路徑不受影響(Root 仍 spread 完整 props)。\n const {\n onValueChange: _onValueChange,\n disabled: _disabled,\n required: _required,\n name: _name,\n orientation: _orientation,\n dir: _dir,\n loop: _loop,\n asChild: _asChild,\n ...restDomProps\n } = props as RadioGroupProps & { asChild?: boolean }\n\n if (resolvedMode === 'view') {\n const selectedValue = resolvedValue || undefined\n if (!selectedValue) {\n return <div {...restDomProps} ref={setRef} role=\"group\" className={cn('grid', className)}><span className={fieldEmptyColorClass(resolvedMode)}>{emptyDisplay}</span></div>\n }\n const selectedLabel = findSelectedRadioLabel(children, selectedValue)\n return (\n <div {...restDomProps} ref={setRef} role=\"group\" className={cn('grid', className)}>\n <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n </div>\n )\n }\n\n // ── mode='readonly' in Field(2026-06-12 user 拍板,與 Checkbox/Switch 灰框模型一致)──\n // Field 內 readonly 單選 = fieldWrapperStyles readonly 灰框 + 選中項 label 文字\n // (= Select readonly 同款呈現:同為「單選資料」,鎖定時呈現一致)。\n // standalone readonly(無 Field)維持原樣鎖互動(ReadonlyContext 路徑)。\n if (resolvedMode === 'readonly' && fieldCtx?.hasFieldWrapper === true) {\n const selectedValue = resolvedValue || undefined\n const selectedLabel = selectedValue ? findSelectedRadioLabel(children, selectedValue) : null\n const boxSize = resolvedBoxSize\n return (\n <div\n {...restDomProps}\n ref={setRef}\n role=\"radiogroup\"\n aria-readonly=\"true\"\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n data-readonly=\"true\"\n tabIndex={0}\n className={cn(\n fieldWrapperStyles({ size: boxSize, mode: 'readonly', variant: 'default' }),\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n >\n {selectedValue\n ? <span className=\"text-foreground\">{selectedLabel ?? selectedValue}</span>\n : <span className={fieldEmptyColorClass(resolvedMode)}>{emptyDisplay}</span>}\n </div>\n )\n }\n\n // mode='disabled' → Radix Root disabled(原生 propagate 給所有 item);\n // mode='readonly' → context 傳 readOnly 給 items(item 渲染為 data-[readonly] 鎖互動 + aria-readonly)。\n return (\n <RadioGroupReadonlyContext.Provider value={resolvedMode === 'readonly'}>\n <RadioGroupDisabledContext.Provider value={resolvedMode === 'disabled'}>\n <RadioGroupPrimitive.Root\n className={cn(\"grid\", className)}\n value={resolvedValue}\n onValueChange={setResolvedValue}\n // Field 內:role=\"radiogroup\" 的 <div> 無法被 <label for> 命名 → 自讀 fieldCtx 接\n // aria-labelledby / aria-invalid / aria-describedby(field-context.ts「下游 control 需自讀\n // labelId」;對齊 slider.tsx / Rating / TimePicker 的 fieldLabelId 接線 + 上方 readonly-in-Field 分支)。\n // standalone(無 Field)時皆 undefined,consumer 的 {...props} aria-* 照舊生效(置於 spread 前故可被覆蓋)。\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n aria-describedby={fieldCtx?.descriptionId}\n // a11y(2026-07-14 dim-10):readonly 語意放 radiogroup(aria-readonly 合法 role;\n // 對齊上方 readonly-in-Field 分支 L133)。item(role=radio)不帶 aria-readonly —\n // axe 實證 radio 不允許該屬性,item 靠 data-readonly + tabIndex + click guard 鎖互動。\n aria-readonly={resolvedMode === 'readonly' || undefined}\n {...props}\n disabled={resolvedMode === 'disabled'}\n ref={setRef}\n >\n {children}\n </RadioGroupPrimitive.Root>\n </RadioGroupDisabledContext.Provider>\n </RadioGroupReadonlyContext.Provider>\n )\n})\nRadioGroup.displayName = 'RadioGroup'\n// Field layout 宣告:RadioGroup 是 block primitive(多項堆疊),\n// 進入 <Field> 時 control area 自動切 items-start(block 分支不設 min-h、不加 padding-top,\n// 高度由內容決定;額外加 paddingTop 會 double padding — field.tsx block control area SSOT)。\n// Convention 詳見 components/Field/field.spec.md「Control area:Inline vs Block」段落。\n;(RadioGroup as unknown as { fieldLayout: 'block' }).fieldLayout = 'block'\n\n// ── RadioGroupItem Variants ─────────────────────────────────────────────────\n// 與 Checkbox 完全對齊:sm/md=16px, lg=20px。差異只有形狀(rounded-full)和指示器(filled dot)。\n\nconst radioItemVariants = cva(\n [\n 'grid place-content-center shrink-0 rounded-full',\n 'border border-border bg-surface',\n 'transition-colors duration-150',\n 'hover:border-border-hover',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'data-[state=checked]:border-primary data-[state=checked]:text-primary',\n 'data-[state=checked]:hover:border-primary-hover data-[state=checked]:hover:text-primary-hover',\n 'disabled:cursor-not-allowed disabled:bg-disabled disabled:border-transparent disabled:hover:border-transparent',\n 'disabled:data-[state=checked]:bg-disabled disabled:data-[state=checked]:border-transparent disabled:data-[state=checked]:text-fg-disabled',\n // readOnly:鎖定互動但維持 checked/unchecked 視覺\n 'data-[readonly=true]:pointer-events-none data-[readonly=true]:cursor-default',\n 'data-[readonly=true]:hover:border-border',\n ],\n {\n variants: {\n size: {\n sm: 'h-4 w-4',\n md: 'h-4 w-4',\n lg: 'h-5 w-5',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n }\n)\n\n// ── Dot Size ────────────────────────────────────────────────────────────────\nconst dotSize: Record<string, number> = { sm: 8, md: 8, lg: 10 }\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\ntype RadioItemPrimitiveProps = React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n\nexport interface RadioGroupItemProps\n // asChild / children Omit(2026-07-18 決策2):item 固定渲染 Indicator dot(<Circle/>)—\n // children 被固定 Indicator 靜默覆蓋,asChild 破壞 radio 結構。對齊 Checkbox 收窄。label 走 `label` prop。\n extends Omit<RadioItemPrimitiveProps, 'asChild' | 'children'>,\n VariantProps<typeof radioItemVariants> {\n /**\n * Inline label。提供時 RadioGroupItem 自動透過 SelectionItem 包裝,\n * 套用 codified 樣式(text-body / text-foreground / disabled 色)。\n * 在 <Field> context 內時此 prop 仍然生效(Radio 的 label 是每個 item\n * 各自的,不是整組 Field 的;FieldLabel 是 RadioGroup 整體的 label)。\n */\n label?: React.ReactNode\n /**\n * Inline description(secondary 文字)。須與 label 搭配使用。\n * 套用 text-body / text-fg-secondary 樣式。\n */\n description?: React.ReactNode\n /** 可選左側 icon(label 前)— 2026-06-12 M30 修:轉發 SelectionItem 既有 canonical 槽(selection-item.tsx jsDoc SSOT;與 avatar 互斥)*/\n icon?: LucideIcon\n /** 可選左側 avatar(label 前)— 同上 */\n avatar?: AvatarData\n /**\n * readonly 模式:鎖定互動但維持 checked/unchecked 視覺正確。\n * 通常整個 RadioGroup 一起設 readonly(由 parent RadioGroup 的 disabled\n * 或 readonly 行為決定),個別 item 也可設。\n */\n readOnly?: boolean\n}\n\n// ── RadioGroupItem ──────────────────────────────────────────────────────────\n\nconst RadioGroupItem = React.forwardRef<\n React.ElementRef<typeof RadioGroupPrimitive.Item>,\n RadioGroupItemProps\n>(\n (\n {\n className,\n size,\n label,\n icon,\n avatar,\n description,\n readOnly = false,\n disabled,\n id: idProp,\n ...props\n },\n ref\n ) => {\n const sizeKey = size ?? 'md'\n const dotPx = dotSize[sizeKey]\n\n // 注意:RadioGroup mode='view' 的純文字渲染由 RadioGroup 本體 walk-children 處理\n // (見上方 RadioGroup forwardRef 的 mode === 'view' 分支),RadioGroupItem 在 view\n // mode 下不會被獨立 render,故此處無 view 分支。\n\n // 注意:Radio 的 label 語意與 Checkbox/Switch 不同——\n // Checkbox/Switch 的 label 就是該 control 的唯一 label(被 Field context 接管),\n // RadioGroupItem 的 label 是「該選項」的 label(每 item 各自擁有),\n // FieldLabel 則是整個 RadioGroup 的 label。\n // 因此 RadioGroupItem 的 label 不因 Field context 被忽略。\n // group-level disabled(standalone <RadioGroup disabled>)也要降 label/description 色 —— Radix Root\n // disabled 只 propagate 給 radio 圈,label 色由 SelectionItem disabled 決定,故讀 RadioGroupDisabledContext 補上。\n const groupDisabled = React.useContext(RadioGroupDisabledContext)\n const resolvedDisabled = useResolvedFieldDisabled(disabled) || groupDisabled\n // group-level readonly(RadioGroup mode='readonly')或 item-level readOnly,任一 true 即鎖互動。\n const groupReadonly = React.useContext(RadioGroupReadonlyContext)\n const effectiveReadonly = readOnly || groupReadonly\n\n const generatedId = React.useId()\n const inputId = idProp ?? generatedId\n\n const rootEl = (\n <RadioGroupPrimitive.Item\n id={inputId}\n ref={ref}\n disabled={resolvedDisabled}\n // a11y(2026-07-14 dim-10):item(role=radio)不帶 aria-readonly — axe 實證 radio 不允許\n // 該屬性(readonly 語意由 Root role=radiogroup 宣告,見上方 L167-170)。item 靠\n // data-readonly + tabIndex + click guard 鎖互動。\n data-readonly={effectiveReadonly || undefined}\n tabIndex={effectiveReadonly ? -1 : undefined}\n className={cn(radioItemVariants({ size }), className)}\n {...props}\n // 2026-07-05 D4 修:readOnly 鎖互動補 label 繞道 — <label htmlFor> 的 synthetic click\n // 不是 pointer event,cva pointer-events-none 攔不到,點 label 文字仍選中。\n // preventDefault 讓 Radix composeEventHandlers(checkForDefaultPrevented)跳過內部 onCheck;\n // 必列在 {...props} 之後才不被 consumer onClick 覆蓋(與 Checkbox/Switch 同修)。\n // 同時攔 Radix roving-focus 的 arrow-key onFocus → click() 路徑(同為 click event)。\n onClick={(event) => {\n if (effectiveReadonly) {\n event.preventDefault()\n return\n }\n props.onClick?.(event)\n }}\n >\n <RadioGroupPrimitive.Indicator className=\"grid place-content-center\">\n <Circle\n style={{ width: dotPx, height: dotPx }}\n className=\"fill-current text-current\"\n />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n\n // 無 label → 只渲染 radio 本體\n if (label == null) return rootEl\n\n // 有 label → 透過 SelectionItem 包裝,與 Checkbox 一致(disabled 已於上方 useResolvedFieldDisabled 解析)\n return (\n <SelectionItem\n control={rootEl}\n label={label}\n description={description}\n icon={icon}\n avatar={avatar}\n htmlFor={inputId}\n disabled={resolvedDisabled}\n size={sizeKey}\n />\n )\n }\n)\nRadioGroupItem.displayName = 'RadioGroupItem'\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 radioGroupMeta = {\n component: 'RadioGroup',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L31;非 Family 4 — field-controls.spec.md 成員名單不含 RadioGroup)\n variants: {\n\n },\n sizes: {\n // iconSize = 渲染指示器尺寸(對齊 checkbox/switch meta 慣例:checkbox iconSize=真 Check glyph 12/12/16)。\n // Radio 指示器是 filled dot 非 glyph,真值 = 上方 `const dotSize` map 8/8/10;\n // 控件框 16/16/20 與 Checkbox 對齊但那是 box 非「指示器」,不入此鍵(避免 metadata 語意 drift)。\n sm: { fieldHeight: 28, iconSize: 8, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 8, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 10, typography: 'body-lg' },\n },\n // 2026-07-04 修 stale meta:移除 'active' — radioItemVariants cva 無 active-state 樣式,\n // Radix radio 也無 data-state=active(對照 checkboxMeta 正確不含 active)\n // 'checked' = 選定持續態(card 式 = border+text primary;2026-07-07 meta 詞彙統一補列)\n states: ['default', 'hover', 'checked', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-disabled', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { RadioGroup, RadioGroupItem, radioItemVariants }\n"],"names":[],"mappings":"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"}
|
|
@@ -75,10 +75,9 @@ export interface SelectMenuProps {
|
|
|
75
75
|
/** 多選 footer 全選列文字(2026-07-05 D4:原「全部」字面 hardcode,無法覆寫也無法 i18n) */
|
|
76
76
|
selectAllLabel?: string;
|
|
77
77
|
/** Loading 狀態(2026-05-15 audit B fix;2026-07-04 Q3 拍板措辭修訂)
|
|
78
|
-
* true → 無可顯示選項時 empty slot(cmdk CommandEmpty)
|
|
79
|
-
*
|
|
80
|
-
* no suggestions」共識)。trigger 不變,user 隨時可開 dropdown。
|
|
81
|
-
* CircularProgress compose」SSOT。
|
|
78
|
+
* true → 無可顯示選項時 empty slot(cmdk CommandEmpty)渲可命名的
|
|
79
|
+
* `role="status"` wrapper + 48px CircularProgress;已有 options 時保留顯示不清空
|
|
80
|
+
* (MUI Autocomplete「only if there are no suggestions」共識)。trigger 不變,user 隨時可開 dropdown。
|
|
82
81
|
*/
|
|
83
82
|
loading?: boolean;
|
|
84
83
|
/** 尺寸 */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-menu.d.ts","sourceRoot":"","sources":["../../../src/components/SelectMenu/select-menu.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAA;AAa1E;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,KAAK,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAIjC,MAAM,WAAW,eAAe;IAC9B,WAAW;IACX,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,gCAAgC;IAChC,MAAM,CAAC,EAAE,qBAAqB,EAAE,CAAA;IAEhC,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAA;IAChC,mBAAmB;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAA;IAElD,WAAW;IACX,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,YAAY;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACvC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAExC,oBAAoB;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,sBAAsB;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,YAAY;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB
|
|
1
|
+
{"version":3,"file":"select-menu.d.ts","sourceRoot":"","sources":["../../../src/components/SelectMenu/select-menu.tsx"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@ import { RowSizeProvider, ICON_SIZE } from "../../patterns/element-anatomy/item-
|
|
|
15
15
|
import { clearSelection, applySelectAll } from "../../lib/multi-select-ordering.js";
|
|
16
16
|
function SelectMenuLiveStatus({ loading, emptyText }) {
|
|
17
17
|
const filteredCount = useCommandState((state) => state.filtered.count);
|
|
18
|
-
return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: filteredCount === 0 ? loading ?
|
|
18
|
+
return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: filteredCount === 0 ? loading ? null : emptyText : null });
|
|
19
19
|
}
|
|
20
20
|
const SelectMenu = React.forwardRef(function SelectMenu2({
|
|
21
21
|
options,
|
|
@@ -201,7 +201,7 @@ const SelectMenu = React.forwardRef(function SelectMenu2({
|
|
|
201
201
|
{
|
|
202
202
|
className: "flex items-center justify-center",
|
|
203
203
|
style: { minHeight: getMenuListMinHeight(size, minRows) },
|
|
204
|
-
children: loading ? /* @__PURE__ */ jsx(
|
|
204
|
+
children: loading ? /* @__PURE__ */ jsx("div", { role: "status", "aria-label": "載入選項中", className: "flex items-center justify-center py-6", children: /* @__PURE__ */ jsx(CircularProgress, { size: 48 }) }) : /* @__PURE__ */ jsx(Empty, { description: emptyText, className: "py-6" })
|
|
205
205
|
}
|
|
206
206
|
),
|
|
207
207
|
groupedOptions.map((group, gi) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|