@qijenchen/design-system 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +93 -0
- package/src/README.md +32 -0
- package/src/components/Accordion/accordion.tsx +104 -0
- package/src/components/Alert/alert.tsx +188 -0
- package/src/components/AppShell/_demo-helpers.tsx +198 -0
- package/src/components/AppShell/app-shell.tsx +364 -0
- package/src/components/AspectRatio/aspect-ratio.tsx +58 -0
- package/src/components/Avatar/avatar.tsx +368 -0
- package/src/components/Badge/badge.tsx +104 -0
- package/src/components/Breadcrumb/breadcrumb.tsx +609 -0
- package/src/components/BulkActionBar/bulk-action-bar.tsx +156 -0
- package/src/components/Button/button-group.tsx +96 -0
- package/src/components/Button/button.tsx +539 -0
- package/src/components/Calendar/calendar.tsx +411 -0
- package/src/components/Carousel/carousel.tsx +371 -0
- package/src/components/Chart/chart.tsx +376 -0
- package/src/components/Checkbox/checkbox-group.tsx +94 -0
- package/src/components/Checkbox/checkbox.tsx +237 -0
- package/src/components/Chip/chip.tsx +359 -0
- package/src/components/CircularProgress/circular-progress.tsx +204 -0
- package/src/components/Coachmark/coachmark.tsx +255 -0
- package/src/components/Combobox/combobox.tsx +826 -0
- package/src/components/Command/command.tsx +187 -0
- package/src/components/DataTable/active-editor-controller.ts +72 -0
- package/src/components/DataTable/cell-registry.tsx +520 -0
- package/src/components/DataTable/column-types.ts +180 -0
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +261 -0
- package/src/components/DataTable/data-table-filter-panel.tsx +813 -0
- package/src/components/DataTable/data-table-interaction-layer.tsx +483 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +210 -0
- package/src/components/DataTable/data-table.css +165 -0
- package/src/components/DataTable/data-table.tsx +2924 -0
- package/src/components/DataTable/filter-operators.ts +225 -0
- package/src/components/DataTable/filter-tree.ts +313 -0
- package/src/components/DataTable/lib/column-meta.ts +79 -0
- package/src/components/DateGrid/date-grid.tsx +209 -0
- package/src/components/DatePicker/date-picker.tsx +1114 -0
- package/src/components/DescriptionList/description-list.tsx +141 -0
- package/src/components/Dialog/dialog.tsx +267 -0
- package/src/components/DropdownMenu/dropdown-menu.tsx +475 -0
- package/src/components/Empty/empty.tsx +108 -0
- package/src/components/Field/field-context.ts +136 -0
- package/src/components/Field/field-types.ts +52 -0
- package/src/components/Field/field-wrapper.tsx +348 -0
- package/src/components/Field/field.tsx +535 -0
- package/src/components/FieldControlGroup/field-control-group.tsx +136 -0
- package/src/components/FileItem/file-item.tsx +322 -0
- package/src/components/FileUpload/file-upload.tsx +326 -0
- package/src/components/FileViewer/file-viewer-types.ts +76 -0
- package/src/components/FileViewer/file-viewer.tsx +1065 -0
- package/src/components/FileViewer/image-renderer.tsx +256 -0
- package/src/components/HoverCard/hover-card.tsx +79 -0
- package/src/components/Input/input.tsx +233 -0
- package/src/components/LinkInput/link-input.tsx +304 -0
- package/src/components/Menu/menu-item.tsx +334 -0
- package/src/components/NameCard/name-card.tsx +319 -0
- package/src/components/Notice/notice.tsx +196 -0
- package/src/components/NumberInput/number-input.tsx +203 -0
- package/src/components/OverflowIndicator/overflow-indicator.tsx +156 -0
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +100 -0
- package/src/components/PeoplePicker/people-picker-helpers.ts +76 -0
- package/src/components/PeoplePicker/people-picker.tsx +455 -0
- package/src/components/PeoplePicker/person-display.tsx +358 -0
- package/src/components/Popover/popover.tsx +183 -0
- package/src/components/ProgressBar/progress-bar.tsx +157 -0
- package/src/components/README.md +58 -0
- package/src/components/RadioGroup/radio-group.tsx +261 -0
- package/src/components/Rating/rating.tsx +295 -0
- package/src/components/ScrollArea/scroll-area.tsx +110 -0
- package/src/components/SegmentedControl/segmented-control.tsx +304 -0
- package/src/components/Select/select.tsx +658 -0
- package/src/components/SelectMenu/select-menu.tsx +430 -0
- package/src/components/SelectionControl/selection-item.tsx +261 -0
- package/src/components/Separator/separator.tsx +48 -0
- package/src/components/Sheet/sheet.tsx +240 -0
- package/src/components/Sidebar/sidebar.tsx +1280 -0
- package/src/components/Skeleton/skeleton.tsx +35 -0
- package/src/components/Slider/slider.tsx +158 -0
- package/src/components/Steps/steps.tsx +850 -0
- package/src/components/Switch/switch.tsx +285 -0
- package/src/components/Tabs/tabs.tsx +515 -0
- package/src/components/Tag/tag.tsx +246 -0
- package/src/components/Textarea/textarea.tsx +280 -0
- package/src/components/TimePicker/time-columns.tsx +260 -0
- package/src/components/TimePicker/time-picker.tsx +419 -0
- package/src/components/Toast/toast.tsx +129 -0
- package/src/components/Tooltip/tooltip.tsx +68 -0
- package/src/components/TreeView/tree-view.tsx +1031 -0
- package/src/hooks/use-controllable.ts +40 -0
- package/src/hooks/use-is-narrow-viewport.ts +19 -0
- package/src/hooks/use-is-touch-device.ts +21 -0
- package/src/hooks/use-overflow-items.ts +256 -0
- package/src/index.ts +85 -0
- package/src/lib/README.md +82 -0
- package/src/lib/drag-visual.ts +272 -0
- package/src/lib/i18n/README.md +60 -0
- package/src/lib/i18n/i18n-context.tsx +129 -0
- package/src/lib/multi-select-ordering.ts +61 -0
- package/src/lib/utils.ts +93 -0
- package/src/patterns/README.md +67 -0
- package/src/patterns/element-anatomy/item-anatomy.tsx +744 -0
- package/src/patterns/header-canonical/chrome-header.tsx +175 -0
- package/src/patterns/header-canonical/header-canonical.css +27 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +217 -0
- package/src/patterns/overlay-surface/overlay-surface.tsx +191 -0
- package/src/patterns/resize-handle/resize-handle.tsx +188 -0
- package/src/stories-helpers/anatomy/anatomy-utils.tsx +64 -0
- package/src/tokens/README.md +53 -0
- package/src/tokens/color/primitives.css +429 -0
- package/src/tokens/color/semantic.css +539 -0
- package/src/tokens/elevation/overlay-geometry.ts +13 -0
- package/src/tokens/layoutSpace/layoutSpace.css +36 -0
- package/src/tokens/motion/motion.css +30 -0
- package/src/tokens/motion/motion.ts +17 -0
- package/src/tokens/opacity/opacity.css +23 -0
- package/src/tokens/radius/radius.css +19 -0
- package/src/tokens/typography/typography.css +118 -0
- package/src/tokens/uiSize/icon-size.ts +52 -0
- package/src/tokens/uiSize/uiSize.css +125 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { X } from 'lucide-react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { Button } from '@/design-system/components/Button/button'
|
|
5
|
+
import { ButtonDivider } from '@/design-system/components/Button/button-group'
|
|
6
|
+
|
|
7
|
+
// ── 消費的 SSOT ───────────────────────────────────────────────────────────────
|
|
8
|
+
// - bulk-action-bar.spec.md(本元件 SSOT)
|
|
9
|
+
// - DataTable/data-table.spec.md「L2 選取」(整合方式)
|
|
10
|
+
// - button.spec.md + button-group.tsx(action variant=tertiary,size=sm,
|
|
11
|
+
// gap-2 + ButtonDivider 自帶 mx-1 = 12px 視覺距離)
|
|
12
|
+
// - inline-action.spec.md「same-row consistency rule」(close X 同尺寸 sm)
|
|
13
|
+
// - tokens/layoutSpace/layoutSpace.spec.md(footer 用 px-loose py-tight)
|
|
14
|
+
// - patterns/overlay-surface/overlay-surface.spec.md SurfaceFooter canonical
|
|
15
|
+
// - Alert(banner)用 description ReactNode 帶 inline link CTA
|
|
16
|
+
|
|
17
|
+
// ── i18n labels ─────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
// code-quality-allow: dead-export — public API per spec.md(consumer i18n override hook)
|
|
20
|
+
export interface BulkActionBarLabels {
|
|
21
|
+
count: (n: number) => string
|
|
22
|
+
clear: string
|
|
23
|
+
hiddenSuffix: (hidden: number) => string
|
|
24
|
+
toolbarAriaLabel: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// code-quality-allow: dead-export — public API per spec.md(consumer spread + override)
|
|
28
|
+
export const BULK_ACTION_BAR_DEFAULT_LABELS: BulkActionBarLabels = {
|
|
29
|
+
count: (n) => `已選 ${n} 項`,
|
|
30
|
+
clear: '清除選取',
|
|
31
|
+
hiddenSuffix: (hidden) => `· ${hidden} 個被 filter 隱藏`,
|
|
32
|
+
toolbarAriaLabel: '批次操作',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Props ───────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface BulkActionBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
38
|
+
/** 已選 ID,length === 0 時自動隱藏(回傳 null) */
|
|
39
|
+
selection: readonly string[]
|
|
40
|
+
/** Clear 觸發,user 點 X icon 或 Esc(consumer 在 page-level 監聽) */
|
|
41
|
+
onClear?: () => void
|
|
42
|
+
/** 批次 actions(consumer 提供 sm Button,variant=tertiary 或 tertiary+danger;不用 primary) */
|
|
43
|
+
actions?: React.ReactNode
|
|
44
|
+
/** Filter 模式:hidden 數量,顯示在 count 區 inline「{N} 已選 · {M} 個被 filter 隱藏」 */
|
|
45
|
+
hiddenByFilter?: number
|
|
46
|
+
/**
|
|
47
|
+
* 「擴選整個 dataset」狀態(2026-05-13 ship,per user 抓 Alert「已選 5370」但 BulkActionBar
|
|
48
|
+
* 仍顯「已選 50 項」regression):
|
|
49
|
+
* - undefined / null(default):count 走 `selection.length`(page-level 視覺選取)
|
|
50
|
+
* - number:count 走此數值(整個 dataset 擴選後 user 已選的真總數)
|
|
51
|
+
*
|
|
52
|
+
* Canonical pattern:consumer 把 BulkActionBar 跟「Alert info banner(提示擴選 dataset)」
|
|
53
|
+
* 一起 mount,Alert 點「點此選取全部 N 個」→ setTotalSelected(N) → BulkActionBar count 同步。
|
|
54
|
+
* 對齊 Gmail / Linear / Notion 全選 dataset hint pattern。
|
|
55
|
+
* 詳 `bulk-action-bar.spec.md`「Extend dataset pattern」段。
|
|
56
|
+
*/
|
|
57
|
+
totalSelected?: number | null
|
|
58
|
+
/** i18n labels(Partial,merge with default) */
|
|
59
|
+
labels?: Partial<BulkActionBarLabels>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Component ───────────────────────────────────────────────────────────────
|
|
63
|
+
// 視覺結構(同 SurfaceFooter / DataTable toolbar canonical):
|
|
64
|
+
// px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]
|
|
65
|
+
// gap-2 between elements
|
|
66
|
+
// 全 sm Buttons(same-row consistency)
|
|
67
|
+
// <ButtonDivider /> 自帶 mx-1 = 12px 視覺距離
|
|
68
|
+
//
|
|
69
|
+
// Hint banner(擴 dataset 提示)完全外包給 Alert 元件 — consumer 視 ref 圖
|
|
70
|
+
// 黏在 BulkActionBar 上方/下方,用 Alert variant="info" placement="fixed"
|
|
71
|
+
// description={inline link JSX}。BulkActionBar 自己不再有 hint banner slot。
|
|
72
|
+
//
|
|
73
|
+
// 浮起 / fixed positioning 由 consumer wrap 決定(BulkActionBar 不限定 placement)。
|
|
74
|
+
|
|
75
|
+
const BulkActionBar = React.forwardRef<HTMLDivElement, BulkActionBarProps>(
|
|
76
|
+
function BulkActionBar(
|
|
77
|
+
{ selection, onClear, actions, hiddenByFilter, totalSelected, labels: labelsOverride, className, ...props },
|
|
78
|
+
ref
|
|
79
|
+
) {
|
|
80
|
+
const labels: BulkActionBarLabels = React.useMemo(
|
|
81
|
+
() => ({ ...BULK_ACTION_BAR_DEFAULT_LABELS, ...labelsOverride }),
|
|
82
|
+
[labelsOverride]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// selection.length === 0 自動藏(對齊 spec 禁止事項 #3)
|
|
86
|
+
if (selection.length === 0) return null
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={ref}
|
|
91
|
+
role="toolbar"
|
|
92
|
+
aria-label={labels.toolbarAriaLabel}
|
|
93
|
+
className={cn(
|
|
94
|
+
'flex items-center gap-2',
|
|
95
|
+
'px-[var(--layout-space-loose)] py-[var(--layout-space-tight)]',
|
|
96
|
+
// 上分隔線:bottom toolbar canonical(Linear / Apple Mail / Notion 共識)— 視覺從上方內容收尾
|
|
97
|
+
'border-t border-divider',
|
|
98
|
+
className
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
102
|
+
{/* X close — md dismiss(2026-05-04 spec update:default placement = footer variant,
|
|
103
|
+
visual weight 對齊 Dialog footer commitment buttons md;same-row consistency 維持)
|
|
104
|
+
未來若有 top-toolbar variant(覆蓋 sm-density toolbar)→ 該 variant override sm */}
|
|
105
|
+
{onClear && (
|
|
106
|
+
<Button
|
|
107
|
+
variant="text"
|
|
108
|
+
size="md"
|
|
109
|
+
iconOnly
|
|
110
|
+
dismiss
|
|
111
|
+
startIcon={X}
|
|
112
|
+
aria-label={labels.clear}
|
|
113
|
+
onClick={onClear}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* count + filter hidden inline
|
|
118
|
+
color canonical(2026-05-04):count = primary foreground + medium weight
|
|
119
|
+
理由:count 是 state-bearing 主資訊(「你在 selection mode + N items」),非裝飾
|
|
120
|
+
World-class 共識:Linear/Notion/Carbon/Polaris 均用 primary foreground;muted 化弱化 state signal
|
|
121
|
+
hiddenByFilter suffix 維持 muted(這是次資訊,視覺層次正確) */}
|
|
122
|
+
<span className="text-body text-foreground tabular-nums">
|
|
123
|
+
{/* 2026-05-13:totalSelected override 走 dataset 擴選後真總數,否則 fallback page-level selection.length */}
|
|
124
|
+
{labels.count(typeof totalSelected === 'number' ? totalSelected : selection.length)}
|
|
125
|
+
{hiddenByFilter !== undefined && hiddenByFilter > 0 && (
|
|
126
|
+
<span className="text-fg-muted font-normal"> {labels.hiddenSuffix(hiddenByFilter)}</span>
|
|
127
|
+
)}
|
|
128
|
+
</span>
|
|
129
|
+
|
|
130
|
+
{/* divider */}
|
|
131
|
+
{actions && <ButtonDivider />}
|
|
132
|
+
|
|
133
|
+
{/* batch actions slot(consumer 提供 sm Buttons) */}
|
|
134
|
+
{actions && (
|
|
135
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">{actions}</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
BulkActionBar.displayName = 'BulkActionBar'
|
|
142
|
+
|
|
143
|
+
// Story auto-compile metadata
|
|
144
|
+
export const bulkActionBarMeta = {
|
|
145
|
+
component: 'BulkActionBar',
|
|
146
|
+
family: null,
|
|
147
|
+
variants: {},
|
|
148
|
+
sizes: {},
|
|
149
|
+
states: ['default'],
|
|
150
|
+
tokens: {
|
|
151
|
+
fg: ['text-fg-secondary', 'text-fg-muted'],
|
|
152
|
+
border: ['border-divider'],
|
|
153
|
+
},
|
|
154
|
+
} as const
|
|
155
|
+
|
|
156
|
+
export { BulkActionBar }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { ButtonGroupContext } from './button'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ButtonGroup — 按鈕群組容器
|
|
7
|
+
*
|
|
8
|
+
* 負責群組內的間距(gap 8px)與對齊方式。
|
|
9
|
+
* 垂直排列時透過 ButtonGroupContext 讓所有直接子 Button 自動套用 fullWidth,
|
|
10
|
+
* 無需 cloneElement — 符合 shadcn 的 Context 慣例。
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // 水平排列(預設)
|
|
14
|
+
* <ButtonGroup>
|
|
15
|
+
* <Button variant="primary">確認</Button>
|
|
16
|
+
* <Button variant="tertiary">取消</Button>
|
|
17
|
+
* </ButtonGroup>
|
|
18
|
+
*
|
|
19
|
+
* // 靠右對齊(primary 放右側)
|
|
20
|
+
* <ButtonGroup align="end">
|
|
21
|
+
* <Button variant="tertiary">取消</Button>
|
|
22
|
+
* <Button variant="primary">確認</Button>
|
|
23
|
+
* </ButtonGroup>
|
|
24
|
+
*
|
|
25
|
+
* // 垂直排列(fullWidth 自動套用)
|
|
26
|
+
* <ButtonGroup direction="vertical">
|
|
27
|
+
* <Button variant="primary">確認</Button>
|
|
28
|
+
* <Button variant="tertiary">取消</Button>
|
|
29
|
+
* </ButtonGroup>
|
|
30
|
+
*/
|
|
31
|
+
interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
32
|
+
/** 排列方向,預設 horizontal */
|
|
33
|
+
direction?: 'horizontal' | 'vertical'
|
|
34
|
+
/** 水平對齊,預設 start */
|
|
35
|
+
align?: 'start' | 'center' | 'end'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Module-level 常數(2026-04-22 D3 perf audit):provider value 為 2 狀態 boolean,hoist 避免 render 重建
|
|
39
|
+
const BUTTON_GROUP_CTX_VERTICAL = { fullWidth: true } as const
|
|
40
|
+
const BUTTON_GROUP_CTX_HORIZONTAL = { fullWidth: false } as const
|
|
41
|
+
|
|
42
|
+
const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
43
|
+
({ direction = 'horizontal', align = 'start', className, children, ...props }, ref) => {
|
|
44
|
+
const isVertical = direction === 'vertical'
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ButtonGroupContext.Provider value={isVertical ? BUTTON_GROUP_CTX_VERTICAL : BUTTON_GROUP_CTX_HORIZONTAL}>
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(
|
|
51
|
+
'flex gap-2',
|
|
52
|
+
isVertical ? 'flex-col items-stretch' : 'flex-row items-center flex-wrap',
|
|
53
|
+
!isVertical && align === 'center' && 'justify-center',
|
|
54
|
+
!isVertical && align === 'end' && 'justify-end',
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
</ButtonGroupContext.Provider>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
ButtonGroup.displayName = 'ButtonGroup'
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* ButtonDivider — 按鈕群組內的分隔線
|
|
69
|
+
*
|
|
70
|
+
* 自身左右各 4px,與相鄰按鈕形成 8px(gap)+ 4px(自身)= 12px 的視覺距離。
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* <ButtonGroup>
|
|
74
|
+
* <Button variant="text" size="sm" iconOnly startIcon={Settings} aria-label="設定" />
|
|
75
|
+
* <ButtonDivider />
|
|
76
|
+
* <Button variant="primary" danger size="sm" iconOnly startIcon={Trash2} aria-label="刪除" />
|
|
77
|
+
* </ButtonGroup>
|
|
78
|
+
*/
|
|
79
|
+
const ButtonDivider = React.forwardRef<
|
|
80
|
+
HTMLDivElement,
|
|
81
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
82
|
+
>(({ className, ...props }, ref) => (
|
|
83
|
+
<div
|
|
84
|
+
ref={ref}
|
|
85
|
+
role="separator"
|
|
86
|
+
className={cn(
|
|
87
|
+
'self-stretch w-px mx-1 my-1',
|
|
88
|
+
'bg-divider',
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
))
|
|
94
|
+
ButtonDivider.displayName = 'ButtonDivider'
|
|
95
|
+
|
|
96
|
+
export { ButtonGroup, ButtonDivider }
|