@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,40 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Controlled / Uncontrolled dual-mode state hook。
|
|
5
|
+
*
|
|
6
|
+
* 對齊 Radix `useControllableState` 慣例:
|
|
7
|
+
* - 提供 `value` → controlled,setter 純 callback,內部不存 state
|
|
8
|
+
* - 不提供 `value`(or undefined)→ uncontrolled,內部 state + callback 同步
|
|
9
|
+
*
|
|
10
|
+
* 使用情境:Field / Switch / Checkbox / DataTable selection / DropdownMenu open 等
|
|
11
|
+
* 雙模式 prop。
|
|
12
|
+
*/
|
|
13
|
+
export function useControllable<T>({
|
|
14
|
+
value,
|
|
15
|
+
defaultValue,
|
|
16
|
+
onChange,
|
|
17
|
+
}: {
|
|
18
|
+
value?: T
|
|
19
|
+
defaultValue: T
|
|
20
|
+
onChange?: (next: T) => void
|
|
21
|
+
}): [T, (next: T | ((prev: T) => T)) => void] {
|
|
22
|
+
const isControlled = value !== undefined
|
|
23
|
+
const [internal, setInternal] = useState<T>(defaultValue)
|
|
24
|
+
const onChangeRef = useRef(onChange)
|
|
25
|
+
onChangeRef.current = onChange
|
|
26
|
+
|
|
27
|
+
const current = isControlled ? (value as T) : internal
|
|
28
|
+
|
|
29
|
+
const setValue = useCallback(
|
|
30
|
+
(next: T | ((prev: T) => T)) => {
|
|
31
|
+
const computed =
|
|
32
|
+
typeof next === 'function' ? (next as (prev: T) => T)(current) : next
|
|
33
|
+
if (!isControlled) setInternal(computed)
|
|
34
|
+
onChangeRef.current?.(computed)
|
|
35
|
+
},
|
|
36
|
+
[isControlled, current]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return [current, setValue]
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsNarrowViewport() {
|
|
6
|
+
const [isNarrow, setIsNarrow] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsNarrow(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsNarrow(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isNarrow
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useIsMobile — 偵測觸控裝置(mobile / tablet)
|
|
5
|
+
*
|
|
6
|
+
* 使用 `pointer: coarse` media query,正確區分觸控 vs 精確指標裝置。
|
|
7
|
+
* 用途:Select 等元件在 mobile 退回原生 picker。
|
|
8
|
+
*/
|
|
9
|
+
export function useIsTouchDevice() {
|
|
10
|
+
const [isMobile, setIsMobile] = useState(false)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const mq = window.matchMedia('(pointer: coarse)')
|
|
14
|
+
setIsMobile(mq.matches)
|
|
15
|
+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches)
|
|
16
|
+
mq.addEventListener('change', handler)
|
|
17
|
+
return () => mq.removeEventListener('change', handler)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
return isMobile
|
|
21
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 水平溢出追蹤 hooks — 給 Tabs / ChipGroup 等「一排水平 items 塞不下父容器」的元件共用。
|
|
5
|
+
*
|
|
6
|
+
* ── 設計原則 ──
|
|
7
|
+
* Scroll 模式與 Menu 模式是兩種處理策略:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Scroll 模式**(Material / Polaris / Primer / iOS 作法)
|
|
10
|
+
* items 用原生 overflow-x-auto 水平滾動,邊緣 fade mask 指示還有內容。
|
|
11
|
+
* 使用 `useScrollEdges()`:回傳 atStart / atEnd / canScroll,讓消費端
|
|
12
|
+
* 決定 mask-image / scroll arrow 的顯示。
|
|
13
|
+
*
|
|
14
|
+
* 2. **Menu 模式**(Ant Design / Atlassian 作法)
|
|
15
|
+
* 塞不下的 items 收進 DropdownMenu。**所有 items 都渲染在 DOM 中**
|
|
16
|
+
* (只視覺隱藏溢出的)以保留 Radix 的 roving tabindex / roving focus 等
|
|
17
|
+
* a11y 語意。使用 `useOverflowIndices()`:回傳 overflowIndices
|
|
18
|
+
* 供消費端渲染對應的 menu items(通常是 click proxy,觸發同一個
|
|
19
|
+
* onValueChange)。
|
|
20
|
+
*
|
|
21
|
+
* ── 為什麼分兩個 hook,不合一 ──
|
|
22
|
+
* Scroll 的計算依據是 scroll 事件與 client/scroll width;Menu 的計算依據是
|
|
23
|
+
* items 的 offsetLeft/offsetWidth 相對容器 clientWidth。訂閱的事件不同
|
|
24
|
+
* (scroll vs resize),回傳的資料形狀不同。合一會讓 API 有一半是 noise。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// useScrollEdges — 給 scroll 模式
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
// code-quality-allow: dead-export — hook return type — API surface for consumers who want to annotate
|
|
32
|
+
export interface UseScrollEdgesResult<T extends HTMLElement> {
|
|
33
|
+
/** 綁在 scroll container 上的 ref */
|
|
34
|
+
scrollRef: React.RefObject<T>
|
|
35
|
+
/** scroll 位置在最左側(無法再往左)*/
|
|
36
|
+
atStart: boolean
|
|
37
|
+
/** scroll 位置在最右側(無法再往右)*/
|
|
38
|
+
atEnd: boolean
|
|
39
|
+
/** 內容總寬度超過可視寬度,有滾動空間 */
|
|
40
|
+
canScroll: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 追蹤 scroll container 的滾動位置,用來決定左右 fade mask 是否顯示。
|
|
45
|
+
*
|
|
46
|
+
* 典型用法:
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const { scrollRef, atStart, atEnd, canScroll } = useScrollEdges<HTMLDivElement>()
|
|
49
|
+
* const maskImage = canScroll
|
|
50
|
+
* ? `linear-gradient(to right,
|
|
51
|
+
* ${atStart ? 'black' : 'transparent'} 0,
|
|
52
|
+
* black 16px,
|
|
53
|
+
* black calc(100% - 16px),
|
|
54
|
+
* ${atEnd ? 'black' : 'transparent'} 100%)`
|
|
55
|
+
* : undefined
|
|
56
|
+
* return <div ref={scrollRef} className="overflow-x-auto" style={{ maskImage, WebkitMaskImage: maskImage }}>
|
|
57
|
+
* {items}
|
|
58
|
+
* </div>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function useScrollEdges<T extends HTMLElement = HTMLElement>(): UseScrollEdgesResult<T> {
|
|
62
|
+
const scrollRef = React.useRef<T | null>(null)
|
|
63
|
+
const [state, setState] = React.useState({ atStart: true, atEnd: true, canScroll: false })
|
|
64
|
+
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
const el = scrollRef.current
|
|
67
|
+
if (!el) return
|
|
68
|
+
|
|
69
|
+
const update = () => {
|
|
70
|
+
const canScroll = el.scrollWidth > el.clientWidth + 1
|
|
71
|
+
const atStart = el.scrollLeft <= 0
|
|
72
|
+
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1
|
|
73
|
+
setState((prev) =>
|
|
74
|
+
prev.atStart === atStart && prev.atEnd === atEnd && prev.canScroll === canScroll
|
|
75
|
+
? prev
|
|
76
|
+
: { atStart, atEnd, canScroll }
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
update()
|
|
81
|
+
el.addEventListener('scroll', update, { passive: true })
|
|
82
|
+
const ro = new ResizeObserver(update)
|
|
83
|
+
ro.observe(el)
|
|
84
|
+
// 監聽 children 變化(items 增減、字體載入)
|
|
85
|
+
const mo = new MutationObserver(update)
|
|
86
|
+
mo.observe(el, { childList: true, subtree: true, characterData: true })
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
el.removeEventListener('scroll', update)
|
|
90
|
+
ro.disconnect()
|
|
91
|
+
mo.disconnect()
|
|
92
|
+
}
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
return { scrollRef, ...state }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// useOverflowIndices — 給 menu 模式
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export interface UseOverflowIndicesOptions {
|
|
103
|
+
/**
|
|
104
|
+
* 預留給 overflow trigger(如 "More" 按鈕)的右側寬度(px)。
|
|
105
|
+
* 計算 items 是否溢出時會從 container clientWidth 扣掉這個值。
|
|
106
|
+
*/
|
|
107
|
+
reserveTriggerWidth?: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// code-quality-allow: dead-export — hook return type — API surface for consumers who want to annotate
|
|
111
|
+
export interface UseOverflowIndicesResult<C extends HTMLElement> {
|
|
112
|
+
/** 綁在 items 的父容器上 */
|
|
113
|
+
containerRef: React.RefObject<C>
|
|
114
|
+
/** 為每個 item 註冊 ref(回傳 callback ref)*/
|
|
115
|
+
registerItem: (index: number) => (el: HTMLElement | null) => void
|
|
116
|
+
/** DOM 順序上溢出的 item 索引(連續區間,從某個 index 到尾端)*/
|
|
117
|
+
overflowIndices: number[]
|
|
118
|
+
/** 至少有一個 item 溢出 */
|
|
119
|
+
hasOverflow: boolean
|
|
120
|
+
/** 依 index 取得對應 item 的 DOM 元素,供 scrollIntoView 等操作使用 */
|
|
121
|
+
getItemAt: (index: number) => HTMLElement | undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 追蹤一排水平 items 裡哪些「當前不在可視範圍內」。
|
|
126
|
+
*
|
|
127
|
+
* ── 演算法 ──
|
|
128
|
+
* 對每個 item 檢查 `[offsetLeft, offsetLeft+offsetWidth]` 是否完全落在
|
|
129
|
+
* `[scrollLeft, scrollLeft + clientWidth - reserveTriggerWidth]` 範圍內。
|
|
130
|
+
* 沒完全落在內的就算溢出。
|
|
131
|
+
*
|
|
132
|
+
* **關鍵**:overflow 集合跟 scrollLeft 連動,不是靜態「原始佈局不 fit」集合。
|
|
133
|
+
* 這是 Ant Design / Atlassian 世界級作法 —— menu 顯示「當下看不到的 items」,
|
|
134
|
+
* 不是「原始溢出的 items」。
|
|
135
|
+
*
|
|
136
|
+
* 為什麼這樣對:
|
|
137
|
+
* - 使用者點 menu item → `scrollIntoView` → 原本 overflow 的 item 進入視圖,
|
|
138
|
+
* 原本可見的 items 被推出去 → 下次開 menu 看到的是「剛被推出去的 items」,
|
|
139
|
+
* 永遠能找到所有當前看不到的 items,不會卡住。
|
|
140
|
+
* - 靜態 overflow (只算原始佈局) 會造成 scroll 後無法回到前面的 items。
|
|
141
|
+
*
|
|
142
|
+
* ── 觸發重算 ──
|
|
143
|
+
* - ResizeObserver: 容器寬度變化 / item 尺寸變化
|
|
144
|
+
* - scroll event: scroll 位置變化
|
|
145
|
+
*
|
|
146
|
+
* ── 前提 ──
|
|
147
|
+
* items 在 DOM 裡始終存在 (不可 conditional render),並用 `registerItem(i)` 綁 ref。
|
|
148
|
+
* 容器要可以 scroll (overflow-x-auto 或類似),否則 scrollLeft 永遠 0。
|
|
149
|
+
*
|
|
150
|
+
* 典型用法:
|
|
151
|
+
* ```tsx
|
|
152
|
+
* const { containerRef, registerItem, overflowIndices, hasOverflow, getItemAt } =
|
|
153
|
+
* useOverflowIndices<HTMLDivElement>({ reserveTriggerWidth: 0 })
|
|
154
|
+
*
|
|
155
|
+
* const handleMenuSelect = (value: string, index: number) => {
|
|
156
|
+
* onValueChange?.(value)
|
|
157
|
+
* requestAnimationFrame(() => {
|
|
158
|
+
* getItemAt(index)?.scrollIntoView({ behavior: 'smooth', inline: 'center' })
|
|
159
|
+
* })
|
|
160
|
+
* }
|
|
161
|
+
*
|
|
162
|
+
* return (
|
|
163
|
+
* <div className="flex items-center">
|
|
164
|
+
* <div ref={containerRef} className="flex-1 min-w-0 overflow-x-auto">
|
|
165
|
+
* <ItemList>
|
|
166
|
+
* {items.map((item, i) => React.cloneElement(item, { ref: registerItem(i) }))}
|
|
167
|
+
* </ItemList>
|
|
168
|
+
* </div>
|
|
169
|
+
* {hasOverflow && <OverflowMenu items={overflowIndices.map(i => items[i])} />}
|
|
170
|
+
* </div>
|
|
171
|
+
* )
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export function useOverflowIndices<C extends HTMLElement = HTMLElement>(
|
|
175
|
+
options: UseOverflowIndicesOptions = {}
|
|
176
|
+
): UseOverflowIndicesResult<C> {
|
|
177
|
+
const { reserveTriggerWidth = 0 } = options
|
|
178
|
+
const containerRef = React.useRef<C | null>(null)
|
|
179
|
+
const itemRefsMap = React.useRef<Map<number, HTMLElement>>(new Map())
|
|
180
|
+
const [overflowIndices, setOverflowIndices] = React.useState<number[]>([])
|
|
181
|
+
|
|
182
|
+
const registerItem = React.useCallback((index: number) => {
|
|
183
|
+
return (el: HTMLElement | null) => {
|
|
184
|
+
if (el) {
|
|
185
|
+
itemRefsMap.current.set(index, el)
|
|
186
|
+
} else {
|
|
187
|
+
itemRefsMap.current.delete(index)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, [])
|
|
191
|
+
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
const container = containerRef.current
|
|
194
|
+
if (!container) return
|
|
195
|
+
|
|
196
|
+
const compute = () => {
|
|
197
|
+
const containerWidth = container.clientWidth
|
|
198
|
+
if (containerWidth === 0) return
|
|
199
|
+
|
|
200
|
+
const scrollLeft = container.scrollLeft
|
|
201
|
+
const visibleStart = scrollLeft
|
|
202
|
+
const visibleEnd = scrollLeft + containerWidth - reserveTriggerWidth
|
|
203
|
+
|
|
204
|
+
// 容許 1px 的像素取整誤差,避免邊界 item 被誤判為 overflow
|
|
205
|
+
const tolerance = 1
|
|
206
|
+
|
|
207
|
+
const indices = Array.from(itemRefsMap.current.keys()).sort((a, b) => a - b)
|
|
208
|
+
|
|
209
|
+
const overflow: number[] = []
|
|
210
|
+
for (const i of indices) {
|
|
211
|
+
const el = itemRefsMap.current.get(i)
|
|
212
|
+
if (!el) continue
|
|
213
|
+
|
|
214
|
+
const left = el.offsetLeft
|
|
215
|
+
const right = left + el.offsetWidth
|
|
216
|
+
// 完全可見條件: item 的左右邊都在可視窗口內
|
|
217
|
+
const fullyVisible =
|
|
218
|
+
left >= visibleStart - tolerance && right <= visibleEnd + tolerance
|
|
219
|
+
if (!fullyVisible) overflow.push(i)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setOverflowIndices((prev) => {
|
|
223
|
+
if (prev.length === overflow.length && prev.every((v, i) => v === overflow[i])) {
|
|
224
|
+
return prev
|
|
225
|
+
}
|
|
226
|
+
return overflow
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
compute()
|
|
231
|
+
const ro = new ResizeObserver(compute)
|
|
232
|
+
ro.observe(container)
|
|
233
|
+
// 也觀察每個 item 的尺寸變化(字體載入、label 更新等)
|
|
234
|
+
itemRefsMap.current.forEach((el) => ro.observe(el))
|
|
235
|
+
// Scroll event: overflow 集合隨 scroll 位置變化
|
|
236
|
+
container.addEventListener('scroll', compute, { passive: true })
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
ro.disconnect()
|
|
240
|
+
container.removeEventListener('scroll', compute)
|
|
241
|
+
}
|
|
242
|
+
}, [reserveTriggerWidth])
|
|
243
|
+
|
|
244
|
+
const getItemAt = React.useCallback(
|
|
245
|
+
(index: number) => itemRefsMap.current.get(index),
|
|
246
|
+
[]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
containerRef,
|
|
251
|
+
registerItem,
|
|
252
|
+
overflowIndices,
|
|
253
|
+
hasOverflow: overflowIndices.length > 0,
|
|
254
|
+
getItemAt,
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// 2026-05-22 Phase 1 team-distribution-roadmap:barrel auto-generated by scripts/gen-design-system-barrel.mjs
|
|
2
|
+
// Re-export 主元件 / patterns / hooks / lib(consumers can also use subpath imports per package.json exports map)
|
|
3
|
+
|
|
4
|
+
// ─── Components ───────────────────────────────────────────────────────────
|
|
5
|
+
export * from './components/Accordion/accordion'
|
|
6
|
+
export * from './components/Alert/alert'
|
|
7
|
+
export * from './components/AppShell/app-shell'
|
|
8
|
+
export * from './components/AspectRatio/aspect-ratio'
|
|
9
|
+
export * from './components/Avatar/avatar'
|
|
10
|
+
export * from './components/Badge/badge'
|
|
11
|
+
export * from './components/Breadcrumb/breadcrumb'
|
|
12
|
+
export * from './components/BulkActionBar/bulk-action-bar'
|
|
13
|
+
export * from './components/Button/button'
|
|
14
|
+
export * from './components/Calendar/calendar'
|
|
15
|
+
export * from './components/Carousel/carousel'
|
|
16
|
+
export * from './components/Chart/chart'
|
|
17
|
+
export * from './components/Checkbox/checkbox'
|
|
18
|
+
export * from './components/Chip/chip'
|
|
19
|
+
export * from './components/CircularProgress/circular-progress'
|
|
20
|
+
export * from './components/Coachmark/coachmark'
|
|
21
|
+
// Combobox re-exports SelectOption(也在 Select 出口)→ 改 type-explicit re-export 避 collision(2026-05-22)
|
|
22
|
+
export { Combobox } from './components/Combobox/combobox'
|
|
23
|
+
export type { ComboboxProps } from './components/Combobox/combobox'
|
|
24
|
+
export * from './components/Command/command'
|
|
25
|
+
export * from './components/DataTable/data-table'
|
|
26
|
+
export * from './components/DateGrid/date-grid'
|
|
27
|
+
export * from './components/DatePicker/date-picker'
|
|
28
|
+
export * from './components/DescriptionList/description-list'
|
|
29
|
+
export * from './components/Dialog/dialog'
|
|
30
|
+
export * from './components/DropdownMenu/dropdown-menu'
|
|
31
|
+
export * from './components/Empty/empty'
|
|
32
|
+
export * from './components/Field/field'
|
|
33
|
+
export * from './components/FieldControlGroup/field-control-group'
|
|
34
|
+
export * from './components/FileItem/file-item'
|
|
35
|
+
export * from './components/FileUpload/file-upload'
|
|
36
|
+
export * from './components/FileViewer/file-viewer'
|
|
37
|
+
export * from './components/HoverCard/hover-card'
|
|
38
|
+
export * from './components/Input/input'
|
|
39
|
+
export * from './components/LinkInput/link-input'
|
|
40
|
+
export * from './components/Menu/menu-item'
|
|
41
|
+
export * from './components/NameCard/name-card'
|
|
42
|
+
export * from './components/Notice/notice'
|
|
43
|
+
export * from './components/NumberInput/number-input'
|
|
44
|
+
export * from './components/OverflowIndicator/overflow-indicator'
|
|
45
|
+
export * from './components/PeoplePicker/people-picker'
|
|
46
|
+
export * from './components/Popover/popover'
|
|
47
|
+
export * from './components/ProgressBar/progress-bar'
|
|
48
|
+
export * from './components/RadioGroup/radio-group'
|
|
49
|
+
export * from './components/Rating/rating'
|
|
50
|
+
export * from './components/ScrollArea/scroll-area'
|
|
51
|
+
export * from './components/SegmentedControl/segmented-control'
|
|
52
|
+
export * from './components/Select/select'
|
|
53
|
+
export * from './components/SelectMenu/select-menu'
|
|
54
|
+
export * from './components/SelectionControl/selection-item'
|
|
55
|
+
export * from './components/Separator/separator'
|
|
56
|
+
export * from './components/Sheet/sheet'
|
|
57
|
+
export * from './components/Sidebar/sidebar'
|
|
58
|
+
export * from './components/Skeleton/skeleton'
|
|
59
|
+
export * from './components/Slider/slider'
|
|
60
|
+
export * from './components/Steps/steps'
|
|
61
|
+
export * from './components/Switch/switch'
|
|
62
|
+
export * from './components/Tabs/tabs'
|
|
63
|
+
export * from './components/Tag/tag'
|
|
64
|
+
export * from './components/Textarea/textarea'
|
|
65
|
+
export * from './components/TimePicker/time-picker'
|
|
66
|
+
export * from './components/Toast/toast'
|
|
67
|
+
export * from './components/Tooltip/tooltip'
|
|
68
|
+
// TreeView re-exports DropPosition(也在 dnd primitives)→ 改 type-explicit(2026-05-22)
|
|
69
|
+
export { TreeView, TreeItem } from './components/TreeView/tree-view'
|
|
70
|
+
export type { TreeViewProps, TreeItemProps, TreeDragEndEvent } from './components/TreeView/tree-view'
|
|
71
|
+
|
|
72
|
+
// ─── Patterns ─────────────────────────────────────────────────────────────
|
|
73
|
+
export * from './patterns/horizontal-overflow/horizontal-overflow'
|
|
74
|
+
export * from './patterns/overlay-surface/overlay-surface'
|
|
75
|
+
export * from './patterns/resize-handle/resize-handle'
|
|
76
|
+
|
|
77
|
+
// ─── Hooks ────────────────────────────────────────────────────────────────
|
|
78
|
+
export * from './hooks/use-controllable'
|
|
79
|
+
export * from './hooks/use-is-narrow-viewport'
|
|
80
|
+
export * from './hooks/use-is-touch-device'
|
|
81
|
+
export * from './hooks/use-overflow-items'
|
|
82
|
+
|
|
83
|
+
// ─── Lib utilities ────────────────────────────────────────────────────────
|
|
84
|
+
export * from './lib/drag-visual'
|
|
85
|
+
export * from './lib/multi-select-ordering'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# lib/ Charter
|
|
2
|
+
|
|
3
|
+
## 這裡只收:cross-cutting non-visual utility module
|
|
4
|
+
|
|
5
|
+
每個 lib 子模組提供**跨元件共用、無 visual surface 的 infrastructure / utility**:
|
|
6
|
+
|
|
7
|
+
- React Context Provider + hook(non-visual cross-cutting feature)
|
|
8
|
+
- 純運算 / 格式化 helper(date / number / a11y string)
|
|
9
|
+
- TypeScript type module(無 runtime,共用 type-only export)
|
|
10
|
+
- 第三方 library 的 thin wrapper(對齊 DS API 風格)
|
|
11
|
+
|
|
12
|
+
**核心特徵**:沒有 visual surface(不 render UI),但 ≥ 2 個 DS 元件 import 它使用。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 跟 hooks/ / patterns/ / components/ 分權
|
|
17
|
+
|
|
18
|
+
| Home | 收什麼 | 反例(錯誤 home)|
|
|
19
|
+
|------|-------|----------------|
|
|
20
|
+
| `lib/{topic}/` | cross-cutting feature module(Provider + hook + types 集合)| 純 hook 一個檔案 → 應 `hooks/use-*.ts` |
|
|
21
|
+
| `hooks/use-*.ts` | 單純 stateless hook(無 Context / 無 Provider)| 帶 Provider → `lib/{topic}/` |
|
|
22
|
+
| `patterns/{topic}/` | runtime visual primitive(`<Item>` / `<ActionBar>` 等渲染 UI)| 沒 visual surface → `lib/` |
|
|
23
|
+
| `components/{Name}/` | user-facing 元件(public API)| internal cross-cutting → `lib/` |
|
|
24
|
+
|
|
25
|
+
**判斷 flow**:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
有 visual surface(render UI 元素)?
|
|
29
|
+
├─ Yes → 是 user-facing? → components/ : patterns/
|
|
30
|
+
└─ No → 是純 stateless hook 一支?
|
|
31
|
+
├─ Yes → hooks/use-*.ts
|
|
32
|
+
└─ No(Provider + hook + types 一組,或 utility module)→ lib/{topic}/
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 當前居民
|
|
38
|
+
|
|
39
|
+
| Module | 提供什麼 | Consumer | 世界級對齊 |
|
|
40
|
+
|--------|---------|----------|-----------|
|
|
41
|
+
| `i18n/` | `<I18nProvider>` + `useI18n()` hook + `I18nLabels` types(opt-in context-based label catalog,additive 與 prop API 並存)| 全 DS 元件 opt-in consumer | Material `@mui/material/locale` / Ant `<ConfigProvider locale>` / Carbon `<PrefixContext>` 共識:i18n 是 utility/locale module 非 visual pattern |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 命名鐵律
|
|
46
|
+
|
|
47
|
+
- 子目錄名 kebab-case(對齊 components/ / patterns/)
|
|
48
|
+
- 入口 module 名描述功能(`i18n-context.tsx` / `formatters.ts`),非 generic 名(`utils.ts` / `helpers.ts` 違反 — 後者該歸到 `src/lib/utils.ts` 專案級)
|
|
49
|
+
- 違反 → audit Dim 19 home-name-vs-scope 抓
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 這裡**不收**(反例 + 正確去處)
|
|
54
|
+
|
|
55
|
+
| 疑似要放這但其實不是 | 實際應去 | 為什麼 |
|
|
56
|
+
|-------------------|---------|--------|
|
|
57
|
+
| 「`cn()` Tailwind 合併」工具 | `src/lib/utils.ts` | shadcn 慣例 home,專案級非 DS 內部 |
|
|
58
|
+
| 「`useControllable` 雙模式 hook」 | `packages/design-system/src/hooks/use-controllable.ts` | 純 stateless hook,無 Context |
|
|
59
|
+
| 「Toast 行為 primitive(渲染 UI)」 | `packages/design-system/src/patterns/{name}/` | 有 visual surface |
|
|
60
|
+
| 「formik / react-hook-form 整合」 | 不在 DS scope | DS 不耦合 form library;consumer 自己 wire |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 新增 lib module 的 criteria(必須全部通過)
|
|
65
|
+
|
|
66
|
+
1. **無 visual surface**(不 render UI 元素;Context Provider 例外因 wrap children)
|
|
67
|
+
2. **≥ 2 個 DS 元件消費**(spec / README 必列 consumers)
|
|
68
|
+
3. **≥ 3 家世界級對照**(MUI / Polaris / Ant / Carbon / Material 任一以上)
|
|
69
|
+
4. **不適合 hooks/**(超過單一 stateless hook,有 Provider / types / multi-file 結構)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 為什麼新建這個 home(2026-05-01)
|
|
74
|
+
|
|
75
|
+
i18n-context 原放 `patterns/i18n/`,但 patterns/ 定義是「runtime visual primitive」,i18n 無 visual surface(只是 Context + hook + types)→ home-name-vs-scope 不符(audit Dim 19 抓到)。
|
|
76
|
+
|
|
77
|
+
世界級三家共識:**i18n 是 utility / locale module,不是 visual pattern**:
|
|
78
|
+
- Material `@mui/material/locale` 是 utility sub-package
|
|
79
|
+
- Ant `<ConfigProvider locale>` 是 provider config 非 visual element
|
|
80
|
+
- Carbon `<PrefixContext>` 是 DI module 非 visual primitive
|
|
81
|
+
|
|
82
|
+
新建 `lib/` 為這類 cross-cutting non-visual primitive 留 home,patterns/ 純化只收 visual primitive。
|