@qijenchen/design-system 0.1.0-beta.19 → 0.1.0-beta.21
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/TreeView/tree-view.d.ts.map +1 -1
- package/dist/components/TreeView/tree-view.js +11 -10
- package/dist/components/TreeView/tree-view.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Sidebar/sidebar.spec.md +2 -2
- package/src/components/TreeView/tree-view.spec.md +16 -8
- package/src/components/TreeView/tree-view.tsx +19 -12
- package/src/tokens/README.md +2 -0
- package/src/tokens/token-system.spec.md +243 -0
- package/src/tokens/uiSize/uiSize.css +16 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-view.d.ts","sourceRoot":"","sources":["../../../src/components/TreeView/tree-view.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"tree-view.d.ts","sourceRoot":"","sources":["../../../src/components/TreeView/tree-view.tsx"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import { ChevronRight } from "lucide-react";
|
|
|
6
6
|
import { cva } from "class-variance-authority";
|
|
7
7
|
import { dropIndicatorRow, dropIndicatorInside, dragSourceClass } from "../../lib/drag-visual.js";
|
|
8
8
|
import { cn } from "../../lib/utils.js";
|
|
9
|
+
import { Checkbox } from "../Checkbox/checkbox.js";
|
|
9
10
|
import { ICON_SIZE, ItemPrefix, ItemIcon, ItemSuffix, ItemInlineAction, RowSizeProvider, ROW_PADDING_BY_SIZE } from "../../patterns/element-anatomy/item-anatomy.js";
|
|
10
11
|
const CONTEXT_PX_VAR = {
|
|
11
12
|
sidebar: "var(--layout-space-loose)",
|
|
@@ -560,18 +561,18 @@ const TreeItem = React.forwardRef(
|
|
|
560
561
|
className: cn(
|
|
561
562
|
"group/tree-item",
|
|
562
563
|
treeItemVariants({ size }),
|
|
563
|
-
//
|
|
564
|
-
//
|
|
564
|
+
// 2026-05-26 SSOT lock(user explicit「multi 已有 checkbox 強信號,text 不該再變色」):
|
|
565
|
+
// ── Single mode ──
|
|
566
|
+
// - default text 預設 fg-secondary muted(hierarchy navigation 慣例,跟 Sidebar 一致)
|
|
567
|
+
// - selected → text-foreground emphasis + bg-neutral-selected(無 checkbox,需 text+bg 雙信號)
|
|
568
|
+
// ── Multi mode ──
|
|
569
|
+
// - default text 維持 fg-secondary muted(跟 single 對齊 hierarchy)
|
|
570
|
+
// - selected → 視覺信號只在 checkbox(auto-render below),text 不變、bg 不變
|
|
571
|
+
// - 對齊 SelectMenu multi pattern(menu-item.tsx:194-195 selected → bg only;multi → checkbox only)
|
|
565
572
|
!disabled && !isSelected && "text-fg-secondary",
|
|
566
|
-
!disabled && isSelected && "text-foreground",
|
|
567
|
-
// inside: 資料夾背景高亮(Figma 風格),不用 ring/border
|
|
573
|
+
!disabled && isSelected && selectionMode === "single" && "text-foreground",
|
|
568
574
|
isDropTarget && (dropTarget == null ? void 0 : dropTarget.position) === "inside" && dropIndicatorInside,
|
|
569
575
|
!disabled && "hover:bg-neutral-hover hover:text-foreground",
|
|
570
|
-
// 2026-05-26 RESTORE(per DS SSOT M23):bg-neutral-selected 只 single mode 套。
|
|
571
|
-
// 對齊 SelectMenu(select-menu.tsx:352-354)既有 canonical:
|
|
572
|
-
// multi-select = checkbox 表達 selection,row 本身不套 bg highlight
|
|
573
|
-
// 之前一次 fix(2026-05-26 13:00 commit b8843c2b)引世界級對照 macOS Finder 改 bg apply
|
|
574
|
-
// 多選,違反 M23「DS 既有 canonical 優先於外部 benchmark」。User 抓 + revert。
|
|
575
576
|
!disabled && isSelected && selectionMode === "single" && "bg-neutral-selected",
|
|
576
577
|
showRing && "ring-2 ring-ring ring-inset",
|
|
577
578
|
disabled && "pointer-events-none text-fg-disabled cursor-default",
|
|
@@ -586,7 +587,7 @@ const TreeItem = React.forwardRef(
|
|
|
586
587
|
...props,
|
|
587
588
|
children: [
|
|
588
589
|
chevronSlot,
|
|
589
|
-
checkbox && /* @__PURE__ */ jsx(ItemPrefix, { className: "pointer-events-none", children: checkbox }),
|
|
590
|
+
(checkbox || selectionMode === "multiple") && /* @__PURE__ */ jsx(ItemPrefix, { className: "pointer-events-none", children: checkbox || /* @__PURE__ */ jsx(Checkbox, { checked: isSelected, disabled, "aria-hidden": "true" }) }),
|
|
590
591
|
indicator ? /* @__PURE__ */ jsx(ItemPrefix, { style: { width: iconPx }, children: indicator }) : Icon ? /* @__PURE__ */ jsx(ItemIcon, { icon: Icon, className: disabled ? "text-fg-disabled" : void 0 }) : null,
|
|
591
592
|
/* @__PURE__ */ jsx("span", { className: cn("flex-1 min-w-0 truncate", disabled && "text-fg-disabled"), children: label }),
|
|
592
593
|
inlineActionsSlot ? /* @__PURE__ */ jsx(ItemSuffix, { hoverReveal: actionsReveal === "hover", hoverGroup: "tree-item", children: inlineActionsSlot }) : inlineActions && inlineActions.length > 0 ? /* @__PURE__ */ jsx(ItemSuffix, { hoverReveal: actionsReveal === "hover", hoverGroup: "tree-item", children: inlineActions.map((action, i) => /* @__PURE__ */ jsx(ItemInlineAction, { action }, action.label + i)) }) : null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-view.js","sources":["../../../src/components/TreeView/tree-view.tsx"],"sourcesContent":["// code-quality-allow: file-size — foundational composite(TreeView owns tree logic + TreeItem + drag-drop + keyboard;拆 sub-component 會把 register/unregister 跨檔傳 ref 複雜化超過可讀性 gain)\nimport * as React from 'react'\nimport * as CollapsiblePrimitive from '@radix-ui/react-collapsible'\nimport {\n DndContext,\n DragOverlay,\n useDraggable,\n useDroppable,\n PointerSensor,\n useSensor,\n useSensors,\n type DragStartEvent,\n type DragEndEvent,\n type DragOverEvent,\n} from '@dnd-kit/core'\nimport { ChevronRight } from 'lucide-react'\nimport { cva } from 'class-variance-authority'\nimport type { LucideIcon } from 'lucide-react'\nimport { dragSourceClass, dropIndicatorRow, dropIndicatorInside } from '@/design-system/lib/drag-visual'\nimport { cn } from '@/lib/utils'\n// Row primitive 共用常數——單一 source of truth\nimport {\n ICON_SIZE,\n RowSizeProvider,\n ItemIcon,\n ItemPrefix,\n ItemSuffix,\n ItemInlineAction,\n ROW_PADDING_BY_SIZE,\n type InlineActionConfig,\n} from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * TreeView — 階層結構的遞迴元件\n *\n * 一個 TreeItem 就是一個 node——有 children 就可展開,沒有就是 leaf。\n * 沒有第二個概念(沒有 TreeGroup)。\n *\n * TreeView 負責:\n * 1. 遞迴渲染 + indent\n * 2. 展開/收合狀態管理\n * 3. 鍵盤導覽 + ARIA tree\n *\n * 它不管 node 裡面長什麼樣——icon、badge、status indicator 等\n * 由 consumer 透過 props / slots 決定。\n *\n * 詳見 tree-view.spec.md。\n */\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Types\n// ═══════════════════════════════════════════════════════════════════════════\n\ntype SizeKey = 'sm' | 'md' | 'lg'\ntype SelectionMode = 'single' | 'multiple' | 'none'\n/**\n * TreeView 的使用脈絡,決定 item 的水平 padding:\n * - `'sidebar'`:頁面側邊欄,用 `--layout-space-loose` token(md=16px / lg=24px,跟 density 連動)\n * - `'menu'`:浮層選單 / dropdown,px-3(12px),對齊 MenuItem / DropdownMenu\n */\ntype TreeContext = 'sidebar' | 'menu'\n\n// Base horizontal padding per context — 用 CSS variable 注入到 TreeView 容器,\n// TreeItem 用 calc(var(--tree-px) + indent) 算出最終 paddingLeft。\nconst CONTEXT_PX_VAR: Record<TreeContext, string> = {\n sidebar: 'var(--layout-space-loose)', // md=16px, lg=24px(density 連動)\n menu: '12px', // px-3,對齊 MenuItem / DropdownMenu\n}\n\n/** Drag drop position — 拖放目標的三種位置 */\n// code-quality-allow: dead-export — public event/state type — consumer event handler parameter type\nexport type TreeDropPosition = 'before' | 'after' | 'inside'\n\n/** onDragEnd callback 的參數 */\n// code-quality-allow: dead-export — public event/state type — consumer event handler parameter type\nexport interface TreeDragEndEvent {\n /** 被拖曳的 node id */\n sourceId: string\n /** 目標 node id */\n targetId: string\n /** 放置位置:before(同層上方)/ after(同層下方)/ inside(成為子 node) */\n position: TreeDropPosition\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Constants\n// ═══════════════════════════════════════════════════════════════════════════\n\n// Icon / chevron 尺寸——從 item-layout pattern module 引入(在檔頂 import),\n// 這裡本地不再宣告。所有 row primitives 共用同一個常數。\n\n// indentStep = chevronSize + gap-2(8px)— 2026-05-04 升 SSOT 為 token `--tree-indent-{sm,md,lg}`\n// 在 `tokens/uiSize/uiSize.css`。DataTable nested rows 共用此 SSOT,跨元件視覺一致。\n// 結構對齊:子 chevron 對齊父 icon,子 icon 對齊父 label。\n// Numeric value 此處保留(drop indicator JS px 計算需 number),Tailwind class 走 token。\nconst INDENT_STEP: Record<SizeKey, number> = { sm: 24, md: 24, lg: 28 }\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Context\n// ═══════════════════════════════════════════════════════════════════════════\n\ninterface TreeViewContextValue {\n size: SizeKey\n context: TreeContext\n selectionMode: SelectionMode\n expandOnSelect: boolean\n draggable: boolean\n isKeyboardRef: React.RefObject<boolean>\n expandedIds: Set<string>\n selectedIds: Set<string>\n focusedId: string | null\n /** 目前拖曳中的 node id(null = 沒在拖) */\n draggingId: string | null\n /** 目前 drop indicator 的位置 + depth(用於 line indent) */\n dropTarget: { id: string; position: TreeDropPosition; depth: number } | null\n toggleExpand: (id: string) => void\n select: (id: string) => void\n setFocusedId: (id: string | null) => void\n registerNode: (id: string, parentId: string | null, hasChildren: boolean, label?: React.ReactNode, icon?: LucideIcon) => void\n getNodeInfo: (id: string) => NodeInfo | undefined\n unregisterNode: (id: string) => void\n}\n\nconst TreeViewContext = React.createContext<TreeViewContextValue | null>(null)\n\nfunction useTreeView(): TreeViewContextValue {\n const ctx = React.useContext(TreeViewContext)\n if (!ctx) throw new Error('TreeItem must be used within TreeView')\n return ctx\n}\n\n// TreeItem depth context(遞迴 depth tracking)\nconst DepthContext = React.createContext(0)\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Node registry — 追蹤所有 node 的 parent/children 關係,用於鍵盤導覽\n// ═══════════════════════════════════════════════════════════════════════════\n\ninterface NodeInfo {\n id: string\n parentId: string | null\n hasChildren: boolean\n /** 用於 DragOverlay ghost 渲染 */\n label?: React.ReactNode\n icon?: LucideIcon\n}\n\nfunction useNodeRegistry() {\n const nodesRef = React.useRef(new Map<string, NodeInfo>())\n\n const registerNode = React.useCallback(\n (id: string, parentId: string | null, hasChildren: boolean, label?: React.ReactNode, icon?: LucideIcon) => {\n nodesRef.current.set(id, { id, parentId, hasChildren, label, icon })\n },\n []\n )\n\n const unregisterNode = React.useCallback((id: string) => {\n nodesRef.current.delete(id)\n }, [])\n\n const getNodeInfo = React.useCallback((id: string) => nodesRef.current.get(id), [])\n\n return { nodesRef, registerNode, unregisterNode, getNodeInfo }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// TreeView\n// ═══════════════════════════════════════════════════════════════════════════\n\nexport interface TreeViewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onDragEnd'> {\n /** 元件尺寸,影響 node 高度、icon 大小、indent 寬度 */\n size?: SizeKey\n /**\n * 使用脈絡,決定 item 的水平 padding:\n * - `'sidebar'`(預設):頁面側邊欄,px-2(8px)\n * - `'menu'`:浮層選單 / dropdown,px-3(12px),對齊 MenuItem\n */\n context?: TreeContext\n /** 選取模式。預設 'single'(sidebar nav / stepper) */\n selectionMode?: SelectionMode\n /** 點擊 label 時是否同時展開 children。預設 false(chevron 是展開的唯一控件) */\n expandOnSelect?: boolean\n /** 受控:展開的 node id 集合 */\n expandedIds?: Set<string>\n /** 受控:展開狀態變更 callback */\n onExpandedChange?: (ids: Set<string>) => void\n /** 受控:選取的 node id 集合 */\n selectedIds?: Set<string>\n /** 受控:選取狀態變更 callback */\n onSelectedChange?: (ids: Set<string>) => void\n /** 非受控:預設展開的 node id 陣列 */\n defaultExpandedIds?: string[]\n /** 非受控:預設選取的 node id 陣列 */\n defaultSelectedIds?: string[]\n /**\n * 啟用拖曳排序。預設 false。\n * 啟用後每個 TreeItem 左側出現 drag handle(GripVertical icon),\n * 拖曳時顯示 drop indicator(before / after / inside 三種位置)。\n * Consumer 透過 `onDragEnd` callback 接收 reorder 事件,自行更新 data。\n */\n draggable?: boolean\n /** Drag 結束時觸發,提供 sourceId、targetId、position。Consumer 負責 reorder。 */\n onDragEnd?: (event: TreeDragEndEvent) => void\n /** ARIA label */\n 'aria-label'?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(\n (\n {\n size = 'md',\n context = 'sidebar',\n selectionMode = 'single',\n expandOnSelect = false,\n draggable = false,\n onDragEnd: onDragEndProp,\n expandedIds: controlledExpanded,\n onExpandedChange,\n selectedIds: controlledSelected,\n onSelectedChange,\n defaultExpandedIds = [],\n defaultSelectedIds = [],\n className,\n children,\n ...props\n },\n ref\n ) => {\n // ── Expand state(受控 / 非受控) ──\n const [internalExpanded, setInternalExpanded] = React.useState(\n () => new Set(defaultExpandedIds)\n )\n const expandedIds = controlledExpanded ?? internalExpanded\n const setExpandedIds = React.useCallback(\n (updater: (prev: Set<string>) => Set<string>) => {\n const update = (prev: Set<string>) => {\n const next = updater(prev)\n onExpandedChange?.(next)\n return next\n }\n if (controlledExpanded) {\n update(controlledExpanded)\n } else {\n setInternalExpanded(update)\n }\n },\n [controlledExpanded, onExpandedChange]\n )\n\n // ── Selection state(受控 / 非受控) ──\n const [internalSelected, setInternalSelected] = React.useState(\n () => new Set(defaultSelectedIds)\n )\n const selectedIds = controlledSelected ?? internalSelected\n const setSelectedIds = React.useCallback(\n (updater: (prev: Set<string>) => Set<string>) => {\n const update = (prev: Set<string>) => {\n const next = updater(prev)\n onSelectedChange?.(next)\n return next\n }\n if (controlledSelected) {\n update(controlledSelected)\n } else {\n setInternalSelected(update)\n }\n },\n [controlledSelected, onSelectedChange]\n )\n\n // ── Focus state ──\n const [focusedId, setFocusedId] = React.useState<string | null>(null)\n\n // ── Keyboard vs mouse detection ──\n // focus ring 只在鍵盤操作時顯示,滑鼠點擊用 bg-neutral-selected 表達選中,不顯示 ring\n const isKeyboardRef = React.useRef(false)\n\n // ── Drag state ──\n const [draggingId, setDraggingId] = React.useState<string | null>(null)\n const [dropTarget, setDropTarget] = React.useState<{ id: string; position: TreeDropPosition; depth: number } | null>(null)\n const autoExpandTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n // 2026-05-16 audit codex Round 6:unmount cleanup(原 cleanup 只在 dragEnd/dragCancel,unmount-during-drag 漏 cancel)\n React.useEffect(() => () => { if (autoExpandTimerRef.current) clearTimeout(autoExpandTimerRef.current) }, [])\n // Ref for toggleExpand — handleDragOver 定義在 toggleExpand 之前(hook 順序限制),\n // 用 ref 打斷 temporal dead zone。\n const toggleExpandRef = React.useRef<(id: string) => void>(() => {})\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } })\n )\n\n const handleDragStart = React.useCallback((event: DragStartEvent) => {\n setDraggingId(String(event.active.id))\n }, [])\n\n // ── Figma-style drop detection(X + Y 雙軸)──\n //\n // Y 軸:決定在哪個 item 附近\n // - item 上 25% = before\n // - item 中 50% = inside(只有 folder)\n // - item 下 25% = after\n //\n // X 軸:決定 nesting 深度(Figma 核心邏輯)\n // - 滑鼠越左 = 越淺層(放在 parent 層級)\n // - 滑鼠越右 = 越深層(放進 folder)\n // - 用 pointer X 相對於 tree 左邊界計算 indent level\n //\n const handleDragOver = React.useCallback((event: DragOverEvent) => {\n const { over, active } = event\n if (!over || over.id === active.id) {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n setDropTarget(null)\n return\n }\n\n const rowEl = document.querySelector(`[data-tree-row=\"${over.id}\"]`) as HTMLElement | null\n const targetEl = document.querySelector(`[data-tree-id=\"${over.id}\"]`) as HTMLElement | null\n if (!rowEl || !targetEl) { setDropTarget(null); return }\n\n // 實際指標位置\n const startX = (event.activatorEvent as PointerEvent)?.clientX ?? 0\n const startY = (event.activatorEvent as PointerEvent)?.clientY ?? 0\n const currentX = startX + (event.delta?.x ?? 0)\n const currentY = startY + (event.delta?.y ?? 0)\n\n const rect = rowEl.getBoundingClientRect()\n const offsetY = currentY - rect.top\n const height = rect.height || 32\n const ratio = Math.max(0, Math.min(1, offsetY / height))\n\n const hasChildren = targetEl.dataset.treeHasChildren === 'true'\n const targetDepth = Number(targetEl.getAttribute('aria-level') ?? 1) - 1\n\n // ── X 軸:計算指標在哪個 indent level ──\n const treeEl = treeRef.current\n const treeLeft = treeEl?.getBoundingClientRect().left ?? 0\n const indentStep = INDENT_STEP[size]\n const pointerIndentLevel = Math.max(0, Math.floor((currentX - treeLeft) / indentStep))\n\n let position: TreeDropPosition\n let finalDepth = targetDepth\n\n if (hasChildren) {\n // Folder node\n if (ratio < 0.25) {\n position = 'before'\n } else if (ratio > 0.75) {\n // after folder: 如果指標在 folder 層級或更淺 = after(同層)\n // 如果指標更深 = inside(放進 folder)\n position = pointerIndentLevel > targetDepth ? 'inside' : 'after'\n } else {\n position = 'inside'\n }\n } else {\n // Leaf node\n if (ratio < 0.5) {\n position = 'before'\n } else {\n position = 'after'\n // X 軸:如果指標在比 target 更淺的層級,提升 drop depth\n // 例:Contact(depth 1)的 after,如果滑鼠在 depth 0 → 變成「after Pages」\n if (pointerIndentLevel < targetDepth) {\n // 找 parent 來放\n const groupEl = targetEl.closest('[role=\"group\"]')\n const parentTreeItem = groupEl?.parentElement?.closest('[role=\"treeitem\"]')\n const parentId = parentTreeItem?.getAttribute('data-tree-id')\n if (parentId && parentId !== String(active.id)) {\n const parentDepth = Number(parentTreeItem?.getAttribute('aria-level') ?? 1) - 1\n finalDepth = parentDepth\n setDropTarget({ id: parentId, position: 'after', depth: parentDepth })\n return\n }\n }\n }\n }\n\n setDropTarget({ id: String(over.id), position, depth: finalDepth })\n\n // Auto-expand collapsed folder after 500ms hover (Figma behavior)\n if (position === 'inside' && hasChildren && !expandedIds.has(String(over.id))) {\n if (autoExpandTimerRef.current) clearTimeout(autoExpandTimerRef.current)\n autoExpandTimerRef.current = setTimeout(() => {\n toggleExpandRef.current(String(over.id))\n }, 500)\n } else {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n }\n }, [expandedIds])\n\n const dropTargetRef = React.useRef(dropTarget)\n dropTargetRef.current = dropTarget\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n const { active, over } = event\n const dt = dropTargetRef.current\n if (over && active.id !== over.id && dt) {\n onDragEndProp?.({\n sourceId: String(active.id),\n targetId: String(over.id),\n position: dt.position,\n })\n }\n setDraggingId(null)\n setDropTarget(null)\n }, [onDragEndProp])\n\n const handleDragCancel = React.useCallback(() => {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n setDraggingId(null)\n setDropTarget(null)\n }, [])\n\n // ── Node registry ──\n const { registerNode, unregisterNode, getNodeInfo } = useNodeRegistry()\n\n // ── Actions ──\n const toggleExpand = React.useCallback(\n (id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n },\n [setExpandedIds]\n )\n toggleExpandRef.current = toggleExpand\n\n const select = React.useCallback(\n (id: string) => {\n if (selectionMode === 'none') return\n setSelectedIds((prev) => {\n if (selectionMode === 'single') {\n return new Set([id])\n }\n // multiple\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n },\n [selectionMode, setSelectedIds]\n )\n\n // ── Context value ──\n const contextValue = React.useMemo<TreeViewContextValue>(\n () => ({\n size,\n context,\n selectionMode,\n expandOnSelect,\n draggable,\n isKeyboardRef,\n draggingId,\n dropTarget,\n expandedIds,\n selectedIds,\n focusedId,\n toggleExpand,\n select,\n setFocusedId,\n registerNode,\n unregisterNode,\n getNodeInfo,\n }),\n [\n size,\n context,\n selectionMode,\n expandOnSelect,\n draggable,\n isKeyboardRef,\n draggingId,\n dropTarget,\n expandedIds,\n selectedIds,\n focusedId,\n toggleExpand,\n select,\n setFocusedId,\n registerNode,\n unregisterNode,\n getNodeInfo,\n ]\n )\n\n // ── Keyboard handler ──\n const treeRef = React.useRef<HTMLDivElement>(null)\n React.useImperativeHandle(ref, () => treeRef.current!)\n\n const handleMouseDown = React.useCallback(() => {\n isKeyboardRef.current = false\n }, [])\n\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const handleKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n isKeyboardRef.current = true\n if (!treeRef.current) return\n\n // 取得所有可見的 treeitem\n const items = Array.from(\n treeRef.current.querySelectorAll<HTMLElement>('[role=\"treeitem\"]:not([hidden])')\n )\n const currentIndex = items.findIndex(\n (el) => el.dataset.treeId === focusedId\n )\n if (currentIndex < 0 && items.length > 0 && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {\n // 沒有焦點時,任何方向鍵先聚焦第一個\n setFocusedId(items[0].dataset.treeId ?? null)\n e.preventDefault()\n return\n }\n\n const currentEl = items[currentIndex]\n\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault()\n const next = items[currentIndex + 1]\n if (next) setFocusedId(next.dataset.treeId ?? null)\n break\n }\n case 'ArrowUp': {\n e.preventDefault()\n const prev = items[currentIndex - 1]\n if (prev) setFocusedId(prev.dataset.treeId ?? null)\n break\n }\n case 'ArrowRight': {\n e.preventDefault()\n const id = currentEl?.dataset.treeId\n if (!id) break\n const isExpanded = expandedIds.has(id)\n const hasChildren = currentEl?.dataset.treeHasChildren === 'true'\n if (hasChildren && !isExpanded) {\n toggleExpand(id)\n } else if (hasChildren && isExpanded) {\n // 已展開 → 移到第一個 child\n const next = items[currentIndex + 1]\n if (next) setFocusedId(next.dataset.treeId ?? null)\n }\n break\n }\n case 'ArrowLeft': {\n e.preventDefault()\n const id = currentEl?.dataset.treeId\n if (!id) break\n const isExpanded = expandedIds.has(id)\n const hasChildren = currentEl?.dataset.treeHasChildren === 'true'\n if (hasChildren && isExpanded) {\n toggleExpand(id)\n } else {\n // 收合狀態或 leaf → 移到 parent\n const parentId = currentEl?.dataset.treeParentId\n if (parentId) setFocusedId(parentId)\n }\n break\n }\n case 'Home': {\n e.preventDefault()\n if (items[0]) setFocusedId(items[0].dataset.treeId ?? null)\n break\n }\n case 'End': {\n e.preventDefault()\n const last = items[items.length - 1]\n if (last) setFocusedId(last.dataset.treeId ?? null)\n break\n }\n case 'Enter':\n case ' ': {\n e.preventDefault()\n const id = currentEl?.dataset.treeId\n if (id) select(id)\n break\n }\n }\n },\n [focusedId, expandedIds, toggleExpand, select, setFocusedId]\n )\n\n const treeEl = (\n <div\n ref={treeRef}\n role=\"tree\"\n aria-multiselectable={selectionMode === 'multiple' || undefined}\n className={cn(\n // TreeView root 不加任何 py——呼吸空間由外層容器負責:\n // - 在 SidebarGroup 內: SidebarGroup py-2 提供\n // - 在 DropdownMenuContent 內: content py-2 提供\n // - 獨立使用(story demo): consumer 自己加 py-2\n // 這樣才能跟 DropdownMenu / MenuGroup 的結構一致(group 是容器,row 是內容)。\n 'flex flex-col',\n className,\n )}\n style={{\n ['--tree-px' as string]: CONTEXT_PX_VAR[context],\n ...props.style,\n } as React.CSSProperties}\n onKeyDown={handleKeyDown}\n onMouseDown={handleMouseDown}\n tabIndex={0}\n {...props}\n >\n {children}\n </div>\n )\n\n return (\n <TreeViewContext.Provider value={contextValue}>\n {/* RowSizeProvider:讓 TreeView 子樹內任何 <ItemIcon> / <ItemAvatar> /\n <ItemInlineAction> 自動讀到對的 size,跟 SidebarProvider 同一條規則。\n 未來 TreeView 接 inlineActions API 後也吃這個 context。 */}\n <RowSizeProvider value={size}>\n {/* 永遠包 DndContext(hooks 不能 conditional call)。不 draggable 時無 sensors = 不可拖 */}\n <DndContext\n sensors={draggable ? sensors : undefined}\n onDragStart={handleDragStart}\n onDragOver={handleDragOver}\n onDragEnd={handleDragEnd}\n onDragCancel={handleDragCancel}\n >\n {treeEl}\n {draggable && (\n <DragOverlay dropAnimation={null}>\n {draggingId ? (() => {\n const info = getNodeInfo(draggingId)\n const IconComp = info?.icon\n return (\n <div className={cn(\n 'flex items-center gap-2 rounded-lg bg-surface border border-border pointer-events-none',\n 'shadow-[var(--elevation-200)]',\n size === 'lg' ? 'text-body-lg leading-compact px-4 py-2' : 'text-body leading-compact px-3 py-1.5',\n )}>\n {IconComp && <IconComp size={ICON_SIZE[size]} className=\"shrink-0\" aria-hidden />}\n <span className=\"text-foreground truncate max-w-[200px]\">{info?.label ?? draggingId}</span>\n </div>\n )\n })() : null}\n </DragOverlay>\n )}\n </DndContext>\n </RowSizeProvider>\n </TreeViewContext.Provider>\n )\n }\n)\nTreeView.displayName = 'TreeView'\n\n// ═══════════════════════════════════════════════════════════════════════════\n// TreeItem variants\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst treeItemVariants = cva(\n [\n // items-start:多行 label 時 prefix 留在第一行(item-layout 規則)\n 'flex items-start gap-2 w-full',\n 'cursor-pointer select-none',\n 'transition-colors duration-150',\n 'outline-none',\n // Label 字重 500(跟 SidebarMenuButton 一致)\n 'font-medium',\n ],\n {\n variants: {\n // 消費 ROW_PADDING_BY_SIZE SSOT(item-anatomy.tsx)— drift risk 消除\n size: ROW_PADDING_BY_SIZE,\n },\n defaultVariants: {\n size: 'md',\n },\n }\n)\n\n// ═══════════════════════════════════════════════════════════════════════════\n// TreeItem\n// ═══════════════════════════════════════════════════════════════════════════\n\nexport interface TreeItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'id'> {\n /** 唯一 id。必填,用於 expand / select / keyboard 追蹤 */\n id: string\n /** 主要文字 */\n label: React.ReactNode\n /** 左側 icon(chevron 之後)。LucideIcon 型別,尺寸由 TreeView size 決定 */\n icon?: LucideIcon\n /**\n * Checkbox(多選模式,label 前方)。傳入 ReactNode(Checkbox 元件)。\n * 位置:在 icon 之後、label 之前。\n * 單選模式通常不需要(用 bg-neutral-selected 表達選中)。\n */\n checkbox?: React.ReactNode\n /**\n * 右側 inline actions(suffix slot,宣告式 API)。對齊 `uiSize.spec.md`「Inline Action」\n * 與 `SidebarMenuButton.inlineActions` 的同一條規格——TreeItem / SidebarMenuButton /\n * 未來的 row primitive 全部用同一個 declarative API。\n *\n * Consumer 只宣告 intent,TreeItem 用 `<ItemInlineAction>` 自動渲染:\n * - Icon 尺寸 = `ICON_SIZE[treeViewSize]`(自動)\n * - Hover bg、tooltip、aria-label、cursor-pointer 自動處理\n * - **不可以**手刻 button JSX(canonical 實作在 `item-layout.tsx`)\n *\n * ```tsx\n * <TreeItem\n * id=\"inbox\"\n * icon={Inbox}\n * label=\"Inbox\"\n * inlineActions={[\n * { icon: MoreVertical, label: '更多', onClick: handleMore },\n * { icon: Plus, label: '新增', onClick: handleAdd },\n * ]}\n * actionsReveal=\"hover\"\n * />\n * ```\n *\n * 若需要永遠可見的 suffix(如 badge 計數),放在 `label` 內:\n * ```tsx\n * <TreeItem label={<>Inbox <Badge count={3} /></>} />\n * ```\n */\n inlineActions?: InlineActionConfig[]\n /**\n * 右側 actions slot(ReactNode)— escape hatch 供 consumer 放自訂元素\n * (如 DropdownMenu trigger / 自訂 popover / 多 tier 動作)。\n *\n * 跟 `inlineActions` 互斥(同時傳 `inlineActionsSlot` 會優先,`inlineActions` 被忽略)。\n *\n * 規則對齊 Input.endSlot canonical:90% case 用 `inlineActions` 宣告式 API,\n * 10% config 表達不出時走 slot。視覺一致性由 consumer 負責(可使用 host 內部 helper\n * — 但禁止 app-code 直接 import L3 primitive,見 `check_l3_primitive_import.sh`)。\n */\n inlineActionsSlot?: React.ReactNode\n /**\n * Inline actions 的顯示模式:\n * - `\"hover\"`(預設):row hover 或鍵盤 focus(focus-visible)時才淡入\n * - `false`:常駐顯示\n *\n * 對齊 `SidebarMenuButton.actionsReveal`,同一套規則。\n */\n actionsReveal?: false | \"hover\"\n /**\n * 取代 chevron 的位置。用於 stepper 的 status indicator(●/○/✓)。\n * 設定後 chevron 不渲染,改渲染 indicator。\n */\n indicator?: React.ReactNode\n /** 是否停用 */\n disabled?: boolean\n /** 子 TreeItem(有 children = expandable,沒有 = leaf) */\n children?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(\n ({ id, label, icon: Icon, checkbox, inlineActions, inlineActionsSlot, actionsReveal = 'hover', indicator, disabled, children, className, ...props }, ref) => {\n const ctx = useTreeView()\n const depth = React.useContext(DepthContext)\n const {\n size,\n selectionMode,\n expandOnSelect,\n draggable,\n expandedIds,\n selectedIds,\n focusedId,\n draggingId,\n dropTarget,\n toggleExpand,\n select,\n setFocusedId,\n registerNode,\n unregisterNode,\n isKeyboardRef,\n } = ctx\n\n const hasChildren = React.Children.count(children) > 0\n const isExpanded = expandedIds.has(id)\n const isSelected = selectedIds.has(id)\n const isFocused = focusedId === id\n const showRing = isFocused && isKeyboardRef.current\n const isDragging = draggingId === id\n const isDropTarget = dropTarget?.id === id\n\n const iconPx = ICON_SIZE[size]\n const indentPx = depth * INDENT_STEP[size]\n\n // ── Drag hooks ──\n // Figma 風格:整列可拖(不用 grip handle),靠 distance:5 區分 click vs drag\n const { attributes: dragAttrs, listeners: dragListeners, setNodeRef: setDragRef } = useDraggable({\n id, disabled: !draggable || disabled,\n })\n const { setNodeRef: setDropRef } = useDroppable({\n id, disabled: !draggable || disabled,\n })\n\n // ── 找 parent id(from depth context chain)──\n const parentId = React.useContext(ParentIdContext)\n\n // ── Register / unregister ──\n React.useEffect(() => {\n registerNode(id, parentId, hasChildren, label, Icon)\n return () => unregisterNode(id)\n }, [id, parentId, hasChildren, label, Icon, registerNode, unregisterNode])\n\n // ── Focus scroll into view ──\n const itemRef = React.useRef<HTMLDivElement>(null)\n React.useImperativeHandle(ref, () => itemRef.current!)\n\n React.useEffect(() => {\n if (isFocused && itemRef.current) {\n itemRef.current.scrollIntoView({ block: 'nearest' })\n }\n }, [isFocused])\n\n // ── Handlers ──\n const handleRowClick = React.useCallback(\n (e: React.MouseEvent) => {\n if (disabled) return\n e.stopPropagation()\n setFocusedId(id)\n select(id)\n if (expandOnSelect && hasChildren) {\n toggleExpand(id)\n }\n },\n [id, disabled, select, setFocusedId, expandOnSelect, hasChildren, toggleExpand]\n )\n\n const handleChevronClick = React.useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n if (disabled) return\n toggleExpand(id)\n },\n [id, disabled, toggleExpand]\n )\n\n // ── Chevron(永遠存在:expandable = 旋轉箭頭;leaf = placeholder 佔位) ──\n // 消費 `<ItemPrefix>` SSOT — 永遠 h-[1lh] 對齊 label 第一行中線(item-anatomy 對應)。\n // forced width 透過 style 鎖 chevron 槽寬,讓 sibling label 起點水平對齊(無 chevron leaf 佔位同寬)。\n const chevronSlot = (\n <ItemPrefix style={{ width: iconPx }}>\n {hasChildren ? (\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={handleChevronClick}\n className={cn(\n 'flex items-center justify-center rounded-md',\n 'text-fg-muted hover:text-foreground hover:bg-neutral-hover',\n 'transition-all duration-150',\n isExpanded && 'rotate-90',\n disabled && 'text-fg-disabled pointer-events-none',\n )}\n style={{ width: iconPx, height: iconPx }}\n aria-hidden\n >\n <ChevronRight size={iconPx} />\n </button>\n ) : (\n // Leaf placeholder\n <span style={{ width: iconPx }} aria-hidden />\n )}\n </ItemPrefix>\n )\n\n return (\n <ParentIdContext.Provider value={id}>\n <div\n ref={(node) => {\n (itemRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n }}\n role=\"treeitem\"\n aria-expanded={hasChildren ? isExpanded : undefined}\n aria-selected={selectionMode !== 'none' ? isSelected : undefined}\n aria-level={depth + 1}\n aria-disabled={disabled || undefined}\n data-tree-id={id}\n data-tree-parent-id={parentId ?? ''}\n data-tree-has-children={hasChildren}\n tabIndex={-1}\n className={cn('w-full min-w-0 relative', isDragging && dragSourceClass)}\n >\n {/* Drop indicator — before:水平 2px primary line(指 SSOT drag-visual.ts);\n indent 跟隨 depth(left 由 inline style override class 的 left-0)*/}\n {isDropTarget && dropTarget?.position === 'before' && (\n <div\n className={dropIndicatorRow.before}\n style={{ left: `calc(var(--tree-px) + ${indentPx}px)` }}\n />\n )}\n\n {/* Row: draggable + droppable 都在這一行(合併 ref),確保碰撞偵測只看行高 */}\n <div\n ref={(node) => {\n // 合併 drag + drop ref 到同一個 element\n if (draggable) setDragRef(node)\n setDropRef(node)\n }}\n data-tree-row={id}\n className={cn(\n 'group/tree-item',\n treeItemVariants({ size }),\n // 預設文字色 neutral-8 (fg-secondary),選中後變 neutral-9 (foreground)\n // icon 透過 currentColor 繼承,不需要另外設\n !disabled && !isSelected && 'text-fg-secondary',\n !disabled && isSelected && 'text-foreground',\n // inside: 資料夾背景高亮(Figma 風格),不用 ring/border\n isDropTarget && dropTarget?.position === 'inside' && dropIndicatorInside,\n !disabled && 'hover:bg-neutral-hover hover:text-foreground',\n // 2026-05-26 RESTORE(per DS SSOT M23):bg-neutral-selected 只 single mode 套。\n // 對齊 SelectMenu(select-menu.tsx:352-354)既有 canonical:\n // multi-select = checkbox 表達 selection,row 本身不套 bg highlight\n // 之前一次 fix(2026-05-26 13:00 commit b8843c2b)引世界級對照 macOS Finder 改 bg apply\n // 多選,違反 M23「DS 既有 canonical 優先於外部 benchmark」。User 抓 + revert。\n !disabled && isSelected && selectionMode === 'single' && 'bg-neutral-selected',\n showRing && 'ring-2 ring-ring ring-inset',\n disabled && 'pointer-events-none text-fg-disabled cursor-default',\n className,\n )}\n style={{\n paddingLeft: indentPx > 0\n ? `calc(var(--tree-px) + ${indentPx}px)`\n : 'var(--tree-px)',\n paddingRight: 'var(--tree-px)',\n }}\n onClick={handleRowClick}\n {...(draggable ? { ...dragListeners, ...dragAttrs } : {})}\n {...props}\n >\n {chevronSlot}\n\n {/* Checkbox 在 icon 前——消費 `<ItemPrefix>` 對齊第一行 */}\n {checkbox && (\n <ItemPrefix className=\"pointer-events-none\">\n {checkbox}\n </ItemPrefix>\n )}\n\n {/* indicator 取代 icon 的位置;h-[1lh] 對齊第一行\n indicator 是 escape hatch(stepper status dot 等客製內容),消費 `<ItemPrefix>` 鎖 chevron 槽寬;\n Icon 走 canonical `<ItemIcon>` helper——自動標 data-prefix-type=\"icon\",\n 讓 SidebarProvider 的全域 :has() prefix-mix 偵測能命中。 */}\n {indicator ? (\n <ItemPrefix style={{ width: iconPx }}>\n {indicator}\n </ItemPrefix>\n ) : Icon ? (\n <ItemIcon icon={Icon} className={disabled ? 'text-fg-disabled' : undefined} />\n ) : null}\n\n <span className={cn('flex-1 min-w-0 truncate', disabled && 'text-fg-disabled')}>\n {label}\n </span>\n\n {/* Suffix inline actions——宣告式 API,用 `<ItemInlineAction>` 渲染。\n 消費 `<ItemSuffix hoverReveal hoverGroup=\"tree-item\">` SSOT(2026-05-05 v8 group selector 參數化後)。\n actionsReveal=\"hover\"(預設):row hover 或 keyboard focus-visible 才顯示;\n actionsReveal=false:常駐顯示。跟 SidebarMenuButton 共用同一條規則,行為一致。\n inlineActionsSlot escape hatch 優先(consumer 自控 JSX,reveal 一樣套外層 group)。 */}\n {inlineActionsSlot ? (\n <ItemSuffix hoverReveal={actionsReveal === 'hover'} hoverGroup=\"tree-item\">\n {inlineActionsSlot}\n </ItemSuffix>\n ) : inlineActions && inlineActions.length > 0 ? (\n <ItemSuffix hoverReveal={actionsReveal === 'hover'} hoverGroup=\"tree-item\">\n {inlineActions.map((action, i) => (\n <ItemInlineAction key={action.label + i} action={action} />\n ))}\n </ItemSuffix>\n ) : null}\n </div>\n\n {/* Drop indicator — after:同 before mirror 到 bottom edge(SSOT drag-visual.ts)*/}\n {isDropTarget && dropTarget?.position === 'after' && (\n <div\n className={dropIndicatorRow.after}\n style={{ left: `calc(var(--tree-px) + ${indentPx}px)` }}\n />\n )}\n\n {/* Children: Collapsible 展開/收合 */}\n {hasChildren && (\n <CollapsiblePrimitive.Root open={isExpanded}>\n <CollapsiblePrimitive.Content\n className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\"\n >\n <DepthContext.Provider value={depth + 1}>\n <div role=\"group\" className=\"flex flex-col w-full\">\n {children}\n </div>\n </DepthContext.Provider>\n </CollapsiblePrimitive.Content>\n </CollapsiblePrimitive.Root>\n )}\n </div>\n </ParentIdContext.Provider>\n )\n }\n)\nTreeItem.displayName = 'TreeItem'\n\n// Parent ID context for keyboard navigation (← to parent)\nconst ParentIdContext = React.createContext<string | null>(null)\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Exports\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 treeViewMeta = {\n component: 'TreeView',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-neutral-hover', 'bg-primary', 'bg-primary-subtle', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { TreeView, TreeItem, treeItemVariants }\n"],"names":["treeEl"],"mappings":";;;;;;;;;AAgEA,MAAM,iBAA8C;AAAA,EAClD,SAAS;AAAA;AAAA,EACT,MAAM;AAAA;AACR;AA4BA,MAAM,cAAuC,EAAE,IAAI,IAAI,IAAI,IAAI,IAAI,GAAA;AA4BnE,MAAM,kBAAkB,MAAM,cAA2C,IAAI;AAE7E,SAAS,cAAoC;AAC3C,QAAM,MAAM,MAAM,WAAW,eAAe;AAC5C,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uCAAuC;AACjE,SAAO;AACT;AAGA,MAAM,eAAe,MAAM,cAAc,CAAC;AAe1C,SAAS,kBAAkB;AACzB,QAAM,WAAW,MAAM,OAAO,oBAAI,KAAuB;AAEzD,QAAM,eAAe,MAAM;AAAA,IACzB,CAAC,IAAY,UAAyB,aAAsB,OAAyB,SAAsB;AACzG,eAAS,QAAQ,IAAI,IAAI,EAAE,IAAI,UAAU,aAAa,OAAO,MAAM;AAAA,IACrE;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,iBAAiB,MAAM,YAAY,CAAC,OAAe;AACvD,aAAS,QAAQ,OAAO,EAAE;AAAA,EAC5B,GAAG,CAAA,CAAE;AAEL,QAAM,cAAc,MAAM,YAAY,CAAC,OAAe,SAAS,QAAQ,IAAI,EAAE,GAAG,EAAE;AAElF,SAAO,EAAE,UAAU,cAAc,gBAAgB,YAAA;AACnD;AA6CA,MAAM,WAAW,MAAM;AAAA,EACrB,CACE;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA,qBAAqB,CAAA;AAAA,IACrB,qBAAqB,CAAA;AAAA,IACrB;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AAEH,UAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM;AAAA,MACpD,MAAM,IAAI,IAAI,kBAAkB;AAAA,IAAA;AAElC,UAAM,cAAc,sBAAsB;AAC1C,UAAM,iBAAiB,MAAM;AAAA,MAC3B,CAAC,YAAgD;AAC/C,cAAM,SAAS,CAAC,SAAsB;AACpC,gBAAM,OAAO,QAAQ,IAAI;AACzB,+DAAmB;AACnB,iBAAO;AAAA,QACT;AACA,YAAI,oBAAoB;AACtB,iBAAO,kBAAkB;AAAA,QAC3B,OAAO;AACL,8BAAoB,MAAM;AAAA,QAC5B;AAAA,MACF;AAAA,MACA,CAAC,oBAAoB,gBAAgB;AAAA,IAAA;AAIvC,UAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM;AAAA,MACpD,MAAM,IAAI,IAAI,kBAAkB;AAAA,IAAA;AAElC,UAAM,cAAc,sBAAsB;AAC1C,UAAM,iBAAiB,MAAM;AAAA,MAC3B,CAAC,YAAgD;AAC/C,cAAM,SAAS,CAAC,SAAsB;AACpC,gBAAM,OAAO,QAAQ,IAAI;AACzB,+DAAmB;AACnB,iBAAO;AAAA,QACT;AACA,YAAI,oBAAoB;AACtB,iBAAO,kBAAkB;AAAA,QAC3B,OAAO;AACL,8BAAoB,MAAM;AAAA,QAC5B;AAAA,MACF;AAAA,MACA,CAAC,oBAAoB,gBAAgB;AAAA,IAAA;AAIvC,UAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AAIpE,UAAM,gBAAgB,MAAM,OAAO,KAAK;AAGxC,UAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAwB,IAAI;AACtE,UAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA2E,IAAI;AACzH,UAAM,qBAAqB,MAAM,OAA6C,IAAI;AAElF,UAAM,UAAU,MAAM,MAAM;AAAE,UAAI,mBAAmB,QAAS,cAAa,mBAAmB,OAAO;AAAA,IAAE,GAAG,CAAA,CAAE;AAG5G,UAAM,kBAAkB,MAAM,OAA6B,MAAM;AAAA,IAAC,CAAC;AAEnE,UAAM,UAAU;AAAA,MACd,UAAU,eAAe,EAAE,sBAAsB,EAAE,UAAU,EAAA,GAAK;AAAA,IAAA;AAGpE,UAAM,kBAAkB,MAAM,YAAY,CAAC,UAA0B;AACnE,oBAAc,OAAO,MAAM,OAAO,EAAE,CAAC;AAAA,IACvC,GAAG,CAAA,CAAE;AAcL,UAAM,iBAAiB,MAAM,YAAY,CAAC,UAAyB;;AACjE,YAAM,EAAE,MAAM,OAAA,IAAW;AACzB,UAAI,CAAC,QAAQ,KAAK,OAAO,OAAO,IAAI;AAClC,YAAI,mBAAmB,SAAS;AAAE,uBAAa,mBAAmB,OAAO;AAAG,6BAAmB,UAAU;AAAA,QAAK;AAC9G,sBAAc,IAAI;AAClB;AAAA,MACF;AAEA,YAAM,QAAQ,SAAS,cAAc,mBAAmB,KAAK,EAAE,IAAI;AACnE,YAAM,WAAW,SAAS,cAAc,kBAAkB,KAAK,EAAE,IAAI;AACrE,UAAI,CAAC,SAAS,CAAC,UAAU;AAAE,sBAAc,IAAI;AAAG;AAAA,MAAO;AAGvD,YAAM,WAAU,WAAM,mBAAN,mBAAuC,YAAW;AAClE,YAAM,WAAU,WAAM,mBAAN,mBAAuC,YAAW;AAClE,YAAM,WAAW,YAAU,WAAM,UAAN,mBAAa,MAAK;AAC7C,YAAM,WAAW,YAAU,WAAM,UAAN,mBAAa,MAAK;AAE7C,YAAM,OAAO,MAAM,sBAAA;AACnB,YAAM,UAAU,WAAW,KAAK;AAChC,YAAM,SAAS,KAAK,UAAU;AAC9B,YAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,UAAU,MAAM,CAAC;AAEvD,YAAM,cAAc,SAAS,QAAQ,oBAAoB;AACzD,YAAM,cAAc,OAAO,SAAS,aAAa,YAAY,KAAK,CAAC,IAAI;AAGvE,YAAMA,UAAS,QAAQ;AACvB,YAAM,YAAWA,mCAAQ,wBAAwB,SAAQ;AACzD,YAAM,aAAa,YAAY,IAAI;AACnC,YAAM,qBAAqB,KAAK,IAAI,GAAG,KAAK,OAAO,WAAW,YAAY,UAAU,CAAC;AAErF,UAAI;AACJ,UAAI,aAAa;AAEjB,UAAI,aAAa;AAEf,YAAI,QAAQ,MAAM;AAChB,qBAAW;AAAA,QACb,WAAW,QAAQ,MAAM;AAGvB,qBAAW,qBAAqB,cAAc,WAAW;AAAA,QAC3D,OAAO;AACL,qBAAW;AAAA,QACb;AAAA,MACF,OAAO;AAEL,YAAI,QAAQ,KAAK;AACf,qBAAW;AAAA,QACb,OAAO;AACL,qBAAW;AAGX,cAAI,qBAAqB,aAAa;AAEpC,kBAAM,UAAU,SAAS,QAAQ,gBAAgB;AACjD,kBAAM,kBAAiB,wCAAS,kBAAT,mBAAwB,QAAQ;AACvD,kBAAM,WAAW,iDAAgB,aAAa;AAC9C,gBAAI,YAAY,aAAa,OAAO,OAAO,EAAE,GAAG;AAC9C,oBAAM,cAAc,QAAO,iDAAgB,aAAa,kBAAiB,CAAC,IAAI;AAC9E,2BAAa;AACb,4BAAc,EAAE,IAAI,UAAU,UAAU,SAAS,OAAO,aAAa;AACrE;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,oBAAc,EAAE,IAAI,OAAO,KAAK,EAAE,GAAG,UAAU,OAAO,YAAY;AAGlE,UAAI,aAAa,YAAY,eAAe,CAAC,YAAY,IAAI,OAAO,KAAK,EAAE,CAAC,GAAG;AAC7E,YAAI,mBAAmB,QAAS,cAAa,mBAAmB,OAAO;AACvE,2BAAmB,UAAU,WAAW,MAAM;AAC5C,0BAAgB,QAAQ,OAAO,KAAK,EAAE,CAAC;AAAA,QACzC,GAAG,GAAG;AAAA,MACR,OAAO;AACL,YAAI,mBAAmB,SAAS;AAAE,uBAAa,mBAAmB,OAAO;AAAG,6BAAmB,UAAU;AAAA,QAAK;AAAA,MAChH;AAAA,IACF,GAAG,CAAC,WAAW,CAAC;AAEhB,UAAM,gBAAgB,MAAM,OAAO,UAAU;AAC7C,kBAAc,UAAU;AAExB,UAAM,gBAAgB,MAAM,YAAY,CAAC,UAAwB;AAC/D,UAAI,mBAAmB,SAAS;AAAE,qBAAa,mBAAmB,OAAO;AAAG,2BAAmB,UAAU;AAAA,MAAK;AAC9G,YAAM,EAAE,QAAQ,KAAA,IAAS;AACzB,YAAM,KAAK,cAAc;AACzB,UAAI,QAAQ,OAAO,OAAO,KAAK,MAAM,IAAI;AACvC,uDAAgB;AAAA,UACd,UAAU,OAAO,OAAO,EAAE;AAAA,UAC1B,UAAU,OAAO,KAAK,EAAE;AAAA,UACxB,UAAU,GAAG;AAAA,QAAA;AAAA,MAEjB;AACA,oBAAc,IAAI;AAClB,oBAAc,IAAI;AAAA,IACpB,GAAG,CAAC,aAAa,CAAC;AAElB,UAAM,mBAAmB,MAAM,YAAY,MAAM;AAC/C,UAAI,mBAAmB,SAAS;AAAE,qBAAa,mBAAmB,OAAO;AAAG,2BAAmB,UAAU;AAAA,MAAK;AAC9G,oBAAc,IAAI;AAClB,oBAAc,IAAI;AAAA,IACpB,GAAG,CAAA,CAAE;AAGL,UAAM,EAAE,cAAc,gBAAgB,YAAA,IAAgB,gBAAA;AAGtD,UAAM,eAAe,MAAM;AAAA,MACzB,CAAC,OAAe;AACd,uBAAe,CAAC,SAAS;AACvB,gBAAM,OAAO,IAAI,IAAI,IAAI;AACzB,cAAI,KAAK,IAAI,EAAE,EAAG,MAAK,OAAO,EAAE;AAAA,cAC3B,MAAK,IAAI,EAAE;AAChB,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,MACA,CAAC,cAAc;AAAA,IAAA;AAEjB,oBAAgB,UAAU;AAE1B,UAAM,SAAS,MAAM;AAAA,MACnB,CAAC,OAAe;AACd,YAAI,kBAAkB,OAAQ;AAC9B,uBAAe,CAAC,SAAS;AACvB,cAAI,kBAAkB,UAAU;AAC9B,mBAAO,oBAAI,IAAI,CAAC,EAAE,CAAC;AAAA,UACrB;AAEA,gBAAM,OAAO,IAAI,IAAI,IAAI;AACzB,cAAI,KAAK,IAAI,EAAE,EAAG,MAAK,OAAO,EAAE;AAAA,cAC3B,MAAK,IAAI,EAAE;AAChB,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,MACA,CAAC,eAAe,cAAc;AAAA,IAAA;AAIhC,UAAM,eAAe,MAAM;AAAA,MACzB,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAEF;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IACF;AAIF,UAAM,UAAU,MAAM,OAAuB,IAAI;AACjD,UAAM,oBAAoB,KAAK,MAAM,QAAQ,OAAQ;AAErD,UAAM,kBAAkB,MAAM,YAAY,MAAM;AAC9C,oBAAc,UAAU;AAAA,IAC1B,GAAG,CAAA,CAAE;AAGL,UAAM,gBAAgB,MAAM;AAAA,MAC1B,CAAC,MAA2B;AAC1B,sBAAc,UAAU;AACxB,YAAI,CAAC,QAAQ,QAAS;AAGtB,cAAM,QAAQ,MAAM;AAAA,UAClB,QAAQ,QAAQ,iBAA8B,iCAAiC;AAAA,QAAA;AAEjF,cAAM,eAAe,MAAM;AAAA,UACzB,CAAC,OAAO,GAAG,QAAQ,WAAW;AAAA,QAAA;AAEhC,YAAI,eAAe,KAAK,MAAM,SAAS,KAAK,CAAC,aAAa,WAAW,QAAQ,KAAK,EAAE,SAAS,EAAE,GAAG,GAAG;AAEnG,uBAAa,MAAM,CAAC,EAAE,QAAQ,UAAU,IAAI;AAC5C,YAAE,eAAA;AACF;AAAA,QACF;AAEA,cAAM,YAAY,MAAM,YAAY;AAEpC,gBAAQ,EAAE,KAAA;AAAA,UACR,KAAK,aAAa;AAChB,cAAE,eAAA;AACF,kBAAM,OAAO,MAAM,eAAe,CAAC;AACnC,gBAAI,KAAM,cAAa,KAAK,QAAQ,UAAU,IAAI;AAClD;AAAA,UACF;AAAA,UACA,KAAK,WAAW;AACd,cAAE,eAAA;AACF,kBAAM,OAAO,MAAM,eAAe,CAAC;AACnC,gBAAI,KAAM,cAAa,KAAK,QAAQ,UAAU,IAAI;AAClD;AAAA,UACF;AAAA,UACA,KAAK,cAAc;AACjB,cAAE,eAAA;AACF,kBAAM,KAAK,uCAAW,QAAQ;AAC9B,gBAAI,CAAC,GAAI;AACT,kBAAM,aAAa,YAAY,IAAI,EAAE;AACrC,kBAAM,eAAc,uCAAW,QAAQ,qBAAoB;AAC3D,gBAAI,eAAe,CAAC,YAAY;AAC9B,2BAAa,EAAE;AAAA,YACjB,WAAW,eAAe,YAAY;AAEpC,oBAAM,OAAO,MAAM,eAAe,CAAC;AACnC,kBAAI,KAAM,cAAa,KAAK,QAAQ,UAAU,IAAI;AAAA,YACpD;AACA;AAAA,UACF;AAAA,UACA,KAAK,aAAa;AAChB,cAAE,eAAA;AACF,kBAAM,KAAK,uCAAW,QAAQ;AAC9B,gBAAI,CAAC,GAAI;AACT,kBAAM,aAAa,YAAY,IAAI,EAAE;AACrC,kBAAM,eAAc,uCAAW,QAAQ,qBAAoB;AAC3D,gBAAI,eAAe,YAAY;AAC7B,2BAAa,EAAE;AAAA,YACjB,OAAO;AAEL,oBAAM,WAAW,uCAAW,QAAQ;AACpC,kBAAI,uBAAuB,QAAQ;AAAA,YACrC;AACA;AAAA,UACF;AAAA,UACA,KAAK,QAAQ;AACX,cAAE,eAAA;AACF,gBAAI,MAAM,CAAC,EAAG,cAAa,MAAM,CAAC,EAAE,QAAQ,UAAU,IAAI;AAC1D;AAAA,UACF;AAAA,UACA,KAAK,OAAO;AACV,cAAE,eAAA;AACF,kBAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,gBAAI,KAAM,cAAa,KAAK,QAAQ,UAAU,IAAI;AAClD;AAAA,UACF;AAAA,UACA,KAAK;AAAA,UACL,KAAK,KAAK;AACR,cAAE,eAAA;AACF,kBAAM,KAAK,uCAAW,QAAQ;AAC9B,gBAAI,WAAW,EAAE;AACjB;AAAA,UACF;AAAA,QAAA;AAAA,MAEJ;AAAA,MACA,CAAC,WAAW,aAAa,cAAc,QAAQ,YAAY;AAAA,IAAA;AAG7D,UAAM,SACJ;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,wBAAsB,kBAAkB,cAAc;AAAA,QACtD,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMT;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,OAAO;AAAA,UACL,CAAC,WAAqB,GAAG,eAAe,OAAO;AAAA,UAC/C,GAAG,MAAM;AAAA,QAAA;AAAA,QAEX,WAAW;AAAA,QACX,aAAa;AAAA,QACb,UAAU;AAAA,QACT,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA;AAIL,WACE,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,cAI/B,UAAA,oBAAC,iBAAA,EAAgB,OAAO,MAExB,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,SAAS,YAAY,UAAU;AAAA,QAC/B,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,cAAc;AAAA,QAEb,UAAA;AAAA,UAAA;AAAA,UACA,aACC,oBAAC,aAAA,EAAY,eAAe,MACzB,wBAAc,MAAM;AACnB,kBAAM,OAAO,YAAY,UAAU;AACnC,kBAAM,WAAW,6BAAM;AACvB,mBACE,qBAAC,SAAI,WAAW;AAAA,cACd;AAAA,cACA;AAAA,cACA,SAAS,OAAO,2CAA2C;AAAA,YAAA,GAE1D,UAAA;AAAA,cAAA,YAAY,oBAAC,YAAS,MAAM,UAAU,IAAI,GAAG,WAAU,YAAW,eAAW,KAAA,CAAC;AAAA,kCAC9E,QAAA,EAAK,WAAU,0CAA0C,WAAA,6BAAM,UAAS,WAAA,CAAW;AAAA,YAAA,GACtF;AAAA,UAEJ,GAAA,IAAO,KAAA,CACT;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAGJ,EAAA,CACF;AAAA,EAEJ;AACF;AACA,SAAS,cAAc;AAMvB,MAAM,mBAAmB;AAAA,EACvB;AAAA;AAAA,IAEE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA;AAAA,MAER,MAAM;AAAA,IAAA;AAAA,IAER,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AA+EA,MAAM,WAAW,MAAM;AAAA,EACrB,CAAC,EAAE,IAAI,OAAO,MAAM,MAAM,UAAU,eAAe,mBAAmB,gBAAgB,SAAS,WAAW,UAAU,UAAU,WAAW,GAAG,MAAA,GAAS,QAAQ;AAC3J,UAAM,MAAM,YAAA;AACZ,UAAM,QAAQ,MAAM,WAAW,YAAY;AAC3C,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA,IACE;AAEJ,UAAM,cAAc,MAAM,SAAS,MAAM,QAAQ,IAAI;AACrD,UAAM,aAAa,YAAY,IAAI,EAAE;AACrC,UAAM,aAAa,YAAY,IAAI,EAAE;AACrC,UAAM,YAAY,cAAc;AAChC,UAAM,WAAW,aAAa,cAAc;AAC5C,UAAM,aAAa,eAAe;AAClC,UAAM,gBAAe,yCAAY,QAAO;AAExC,UAAM,SAAS,UAAU,IAAI;AAC7B,UAAM,WAAW,QAAQ,YAAY,IAAI;AAIzC,UAAM,EAAE,YAAY,WAAW,WAAW,eAAe,YAAY,WAAA,IAAe,aAAa;AAAA,MAC/F;AAAA,MAAI,UAAU,CAAC,aAAa;AAAA,IAAA,CAC7B;AACD,UAAM,EAAE,YAAY,WAAA,IAAe,aAAa;AAAA,MAC9C;AAAA,MAAI,UAAU,CAAC,aAAa;AAAA,IAAA,CAC7B;AAGD,UAAM,WAAW,MAAM,WAAW,eAAe;AAGjD,UAAM,UAAU,MAAM;AACpB,mBAAa,IAAI,UAAU,aAAa,OAAO,IAAI;AACnD,aAAO,MAAM,eAAe,EAAE;AAAA,IAChC,GAAG,CAAC,IAAI,UAAU,aAAa,OAAO,MAAM,cAAc,cAAc,CAAC;AAGzE,UAAM,UAAU,MAAM,OAAuB,IAAI;AACjD,UAAM,oBAAoB,KAAK,MAAM,QAAQ,OAAQ;AAErD,UAAM,UAAU,MAAM;AACpB,UAAI,aAAa,QAAQ,SAAS;AAChC,gBAAQ,QAAQ,eAAe,EAAE,OAAO,WAAW;AAAA,MACrD;AAAA,IACF,GAAG,CAAC,SAAS,CAAC;AAGd,UAAM,iBAAiB,MAAM;AAAA,MAC3B,CAAC,MAAwB;AACvB,YAAI,SAAU;AACd,UAAE,gBAAA;AACF,qBAAa,EAAE;AACf,eAAO,EAAE;AACT,YAAI,kBAAkB,aAAa;AACjC,uBAAa,EAAE;AAAA,QACjB;AAAA,MACF;AAAA,MACA,CAAC,IAAI,UAAU,QAAQ,cAAc,gBAAgB,aAAa,YAAY;AAAA,IAAA;AAGhF,UAAM,qBAAqB,MAAM;AAAA,MAC/B,CAAC,MAAwB;AACvB,UAAE,gBAAA;AACF,YAAI,SAAU;AACd,qBAAa,EAAE;AAAA,MACjB;AAAA,MACA,CAAC,IAAI,UAAU,YAAY;AAAA,IAAA;AAM7B,UAAM,kCACH,YAAA,EAAW,OAAO,EAAE,OAAO,OAAA,GACzB,UAAA,cACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,UAAU;AAAA,QACV,SAAS;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc;AAAA,UACd,YAAY;AAAA,QAAA;AAAA,QAEd,OAAO,EAAE,OAAO,QAAQ,QAAQ,OAAA;AAAA,QAChC,eAAW;AAAA,QAEX,UAAA,oBAAC,cAAA,EAAa,MAAM,OAAA,CAAQ;AAAA,MAAA;AAAA,IAAA;AAAA;AAAA,MAI9B,oBAAC,UAAK,OAAO,EAAE,OAAO,OAAA,GAAU,eAAW,KAAA,CAAC;AAAA,OAEhD;AAGF,WACE,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,IAC/B,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK,CAAC,SAAS;AACZ,kBAA0D,UAAU;AACrE,cAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,mBAC9B,IAAM,KAAsD,UAAU;AAAA,QACjF;AAAA,QACA,MAAK;AAAA,QACL,iBAAe,cAAc,aAAa;AAAA,QAC1C,iBAAe,kBAAkB,SAAS,aAAa;AAAA,QACvD,cAAY,QAAQ;AAAA,QACpB,iBAAe,YAAY;AAAA,QAC3B,gBAAc;AAAA,QACd,uBAAqB,YAAY;AAAA,QACjC,0BAAwB;AAAA,QACxB,UAAU;AAAA,QACV,WAAW,GAAG,2BAA2B,cAAc,eAAe;AAAA,QAIrE,UAAA;AAAA,UAAA,iBAAgB,yCAAY,cAAa,YACxC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,iBAAiB;AAAA,cAC5B,OAAO,EAAE,MAAM,yBAAyB,QAAQ,MAAA;AAAA,YAAM;AAAA,UAAA;AAAA,UAK1D;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK,CAAC,SAAS;AAEb,oBAAI,sBAAsB,IAAI;AAC9B,2BAAW,IAAI;AAAA,cACjB;AAAA,cACA,iBAAe;AAAA,cACf,WAAW;AAAA,gBACT;AAAA,gBACA,iBAAiB,EAAE,MAAM;AAAA;AAAA;AAAA,gBAGzB,CAAC,YAAY,CAAC,cAAc;AAAA,gBAC5B,CAAC,YAAY,cAAc;AAAA;AAAA,gBAE3B,iBAAgB,yCAAY,cAAa,YAAY;AAAA,gBACrD,CAAC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAMb,CAAC,YAAY,cAAc,kBAAkB,YAAY;AAAA,gBACzD,YAAY;AAAA,gBACZ,YAAY;AAAA,gBACZ;AAAA,cAAA;AAAA,cAEF,OAAO;AAAA,gBACL,aAAa,WAAW,IACpB,yBAAyB,QAAQ,QACjC;AAAA,gBACJ,cAAc;AAAA,cAAA;AAAA,cAEhB,SAAS;AAAA,cACR,GAAI,YAAY,EAAE,GAAG,eAAe,GAAG,UAAA,IAAc,CAAA;AAAA,cACrD,GAAG;AAAA,cAEH,UAAA;AAAA,gBAAA;AAAA,gBAGA,YACC,oBAAC,YAAA,EAAW,WAAU,uBACnB,UAAA,UACH;AAAA,gBAOD,YACC,oBAAC,YAAA,EAAW,OAAO,EAAE,OAAO,UACzB,UAAA,WACH,IACE,OACF,oBAAC,YAAS,MAAM,MAAM,WAAW,WAAW,qBAAqB,QAAW,IAC1E;AAAA,gBAEJ,oBAAC,UAAK,WAAW,GAAG,2BAA2B,YAAY,kBAAkB,GAC1E,UAAA,OACH;AAAA,gBAOC,oBACC,oBAAC,YAAA,EAAW,aAAa,kBAAkB,SAAS,YAAW,aAC5D,UAAA,kBAAA,CACH,IACE,iBAAiB,cAAc,SAAS,wBACzC,YAAA,EAAW,aAAa,kBAAkB,SAAS,YAAW,aAC5D,UAAA,cAAc,IAAI,CAAC,QAAQ,MAC1B,oBAAC,kBAAA,EAAwC,UAAlB,OAAO,QAAQ,CAAmB,CAC1D,GACH,IACE;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAIL,iBAAgB,yCAAY,cAAa,WACxC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,iBAAiB;AAAA,cAC5B,OAAO,EAAE,MAAM,yBAAyB,QAAQ,MAAA;AAAA,YAAM;AAAA,UAAA;AAAA,UAKzD,eACC,oBAAC,qBAAqB,MAArB,EAA0B,MAAM,YAC/B,UAAA;AAAA,YAAC,qBAAqB;AAAA,YAArB;AAAA,cACC,WAAU;AAAA,cAEV,UAAA,oBAAC,aAAa,UAAb,EAAsB,OAAO,QAAQ,GACpC,UAAA,oBAAC,OAAA,EAAI,MAAK,SAAQ,WAAU,wBACzB,UACH,EAAA,CACF;AAAA,YAAA;AAAA,UAAA,EACF,CACF;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAGN;AAAA,EAEJ;AACF;AACA,SAAS,cAAc;AAGvB,MAAM,kBAAkB,MAAM,cAA6B,IAAI;AAQxD,MAAM,eAAe;AAAA,EAC1B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,oBAAoB,cAAc,qBAAqB,YAAY;AAAA,IACxE,IAAI,CAAC,oBAAoB,iBAAiB,qBAAqB,iBAAiB;AAAA,IAChF,MAAM,CAAC,WAAW;AAAA,EAAA;AAAA,EAEpB,aAAa;AACf;"}
|
|
1
|
+
{"version":3,"file":"tree-view.js","sources":["../../../src/components/TreeView/tree-view.tsx"],"sourcesContent":["// code-quality-allow: file-size — foundational composite(TreeView owns tree logic + TreeItem + drag-drop + keyboard;拆 sub-component 會把 register/unregister 跨檔傳 ref 複雜化超過可讀性 gain)\nimport * as React from 'react'\nimport * as CollapsiblePrimitive from '@radix-ui/react-collapsible'\nimport {\n DndContext,\n DragOverlay,\n useDraggable,\n useDroppable,\n PointerSensor,\n useSensor,\n useSensors,\n type DragStartEvent,\n type DragEndEvent,\n type DragOverEvent,\n} from '@dnd-kit/core'\nimport { ChevronRight } from 'lucide-react'\nimport { cva } from 'class-variance-authority'\nimport type { LucideIcon } from 'lucide-react'\nimport { dragSourceClass, dropIndicatorRow, dropIndicatorInside } from '@/design-system/lib/drag-visual'\nimport { cn } from '@/lib/utils'\nimport { Checkbox } from '@/design-system/components/Checkbox/checkbox'\n// Row primitive 共用常數——單一 source of truth\nimport {\n ICON_SIZE,\n RowSizeProvider,\n ItemIcon,\n ItemPrefix,\n ItemSuffix,\n ItemInlineAction,\n ROW_PADDING_BY_SIZE,\n type InlineActionConfig,\n} from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * TreeView — 階層結構的遞迴元件\n *\n * 一個 TreeItem 就是一個 node——有 children 就可展開,沒有就是 leaf。\n * 沒有第二個概念(沒有 TreeGroup)。\n *\n * TreeView 負責:\n * 1. 遞迴渲染 + indent\n * 2. 展開/收合狀態管理\n * 3. 鍵盤導覽 + ARIA tree\n *\n * 它不管 node 裡面長什麼樣——icon、badge、status indicator 等\n * 由 consumer 透過 props / slots 決定。\n *\n * 詳見 tree-view.spec.md。\n */\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Types\n// ═══════════════════════════════════════════════════════════════════════════\n\ntype SizeKey = 'sm' | 'md' | 'lg'\ntype SelectionMode = 'single' | 'multiple' | 'none'\n/**\n * TreeView 的使用脈絡,決定 item 的水平 padding:\n * - `'sidebar'`:頁面側邊欄,用 `--layout-space-loose` token(md=16px / lg=24px,跟 density 連動)\n * - `'menu'`:浮層選單 / dropdown,px-3(12px),對齊 MenuItem / DropdownMenu\n */\ntype TreeContext = 'sidebar' | 'menu'\n\n// Base horizontal padding per context — 用 CSS variable 注入到 TreeView 容器,\n// TreeItem 用 calc(var(--tree-px) + indent) 算出最終 paddingLeft。\nconst CONTEXT_PX_VAR: Record<TreeContext, string> = {\n sidebar: 'var(--layout-space-loose)', // md=16px, lg=24px(density 連動)\n menu: '12px', // px-3,對齊 MenuItem / DropdownMenu\n}\n\n/** Drag drop position — 拖放目標的三種位置 */\n// code-quality-allow: dead-export — public event/state type — consumer event handler parameter type\nexport type TreeDropPosition = 'before' | 'after' | 'inside'\n\n/** onDragEnd callback 的參數 */\n// code-quality-allow: dead-export — public event/state type — consumer event handler parameter type\nexport interface TreeDragEndEvent {\n /** 被拖曳的 node id */\n sourceId: string\n /** 目標 node id */\n targetId: string\n /** 放置位置:before(同層上方)/ after(同層下方)/ inside(成為子 node) */\n position: TreeDropPosition\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Constants\n// ═══════════════════════════════════════════════════════════════════════════\n\n// Icon / chevron 尺寸——從 item-layout pattern module 引入(在檔頂 import),\n// 這裡本地不再宣告。所有 row primitives 共用同一個常數。\n\n// indentStep = chevronSize + gap-2(8px)— 2026-05-04 升 SSOT 為 token `--tree-indent-{sm,md,lg}`\n// 在 `tokens/uiSize/uiSize.css`。DataTable nested rows 共用此 SSOT,跨元件視覺一致。\n// 結構對齊:子 chevron 對齊父 icon,子 icon 對齊父 label。\n// Numeric value 此處保留(drop indicator JS px 計算需 number),Tailwind class 走 token。\nconst INDENT_STEP: Record<SizeKey, number> = { sm: 24, md: 24, lg: 28 }\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Context\n// ═══════════════════════════════════════════════════════════════════════════\n\ninterface TreeViewContextValue {\n size: SizeKey\n context: TreeContext\n selectionMode: SelectionMode\n expandOnSelect: boolean\n draggable: boolean\n isKeyboardRef: React.RefObject<boolean>\n expandedIds: Set<string>\n selectedIds: Set<string>\n focusedId: string | null\n /** 目前拖曳中的 node id(null = 沒在拖) */\n draggingId: string | null\n /** 目前 drop indicator 的位置 + depth(用於 line indent) */\n dropTarget: { id: string; position: TreeDropPosition; depth: number } | null\n toggleExpand: (id: string) => void\n select: (id: string) => void\n setFocusedId: (id: string | null) => void\n registerNode: (id: string, parentId: string | null, hasChildren: boolean, label?: React.ReactNode, icon?: LucideIcon) => void\n getNodeInfo: (id: string) => NodeInfo | undefined\n unregisterNode: (id: string) => void\n}\n\nconst TreeViewContext = React.createContext<TreeViewContextValue | null>(null)\n\nfunction useTreeView(): TreeViewContextValue {\n const ctx = React.useContext(TreeViewContext)\n if (!ctx) throw new Error('TreeItem must be used within TreeView')\n return ctx\n}\n\n// TreeItem depth context(遞迴 depth tracking)\nconst DepthContext = React.createContext(0)\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Node registry — 追蹤所有 node 的 parent/children 關係,用於鍵盤導覽\n// ═══════════════════════════════════════════════════════════════════════════\n\ninterface NodeInfo {\n id: string\n parentId: string | null\n hasChildren: boolean\n /** 用於 DragOverlay ghost 渲染 */\n label?: React.ReactNode\n icon?: LucideIcon\n}\n\nfunction useNodeRegistry() {\n const nodesRef = React.useRef(new Map<string, NodeInfo>())\n\n const registerNode = React.useCallback(\n (id: string, parentId: string | null, hasChildren: boolean, label?: React.ReactNode, icon?: LucideIcon) => {\n nodesRef.current.set(id, { id, parentId, hasChildren, label, icon })\n },\n []\n )\n\n const unregisterNode = React.useCallback((id: string) => {\n nodesRef.current.delete(id)\n }, [])\n\n const getNodeInfo = React.useCallback((id: string) => nodesRef.current.get(id), [])\n\n return { nodesRef, registerNode, unregisterNode, getNodeInfo }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════\n// TreeView\n// ═══════════════════════════════════════════════════════════════════════════\n\nexport interface TreeViewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onDragEnd'> {\n /** 元件尺寸,影響 node 高度、icon 大小、indent 寬度 */\n size?: SizeKey\n /**\n * 使用脈絡,決定 item 的水平 padding:\n * - `'sidebar'`(預設):頁面側邊欄,px-2(8px)\n * - `'menu'`:浮層選單 / dropdown,px-3(12px),對齊 MenuItem\n */\n context?: TreeContext\n /** 選取模式。預設 'single'(sidebar nav / stepper) */\n selectionMode?: SelectionMode\n /** 點擊 label 時是否同時展開 children。預設 false(chevron 是展開的唯一控件) */\n expandOnSelect?: boolean\n /** 受控:展開的 node id 集合 */\n expandedIds?: Set<string>\n /** 受控:展開狀態變更 callback */\n onExpandedChange?: (ids: Set<string>) => void\n /** 受控:選取的 node id 集合 */\n selectedIds?: Set<string>\n /** 受控:選取狀態變更 callback */\n onSelectedChange?: (ids: Set<string>) => void\n /** 非受控:預設展開的 node id 陣列 */\n defaultExpandedIds?: string[]\n /** 非受控:預設選取的 node id 陣列 */\n defaultSelectedIds?: string[]\n /**\n * 啟用拖曳排序。預設 false。\n * 啟用後每個 TreeItem 左側出現 drag handle(GripVertical icon),\n * 拖曳時顯示 drop indicator(before / after / inside 三種位置)。\n * Consumer 透過 `onDragEnd` callback 接收 reorder 事件,自行更新 data。\n */\n draggable?: boolean\n /** Drag 結束時觸發,提供 sourceId、targetId、position。Consumer 負責 reorder。 */\n onDragEnd?: (event: TreeDragEndEvent) => void\n /** ARIA label */\n 'aria-label'?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(\n (\n {\n size = 'md',\n context = 'sidebar',\n selectionMode = 'single',\n expandOnSelect = false,\n draggable = false,\n onDragEnd: onDragEndProp,\n expandedIds: controlledExpanded,\n onExpandedChange,\n selectedIds: controlledSelected,\n onSelectedChange,\n defaultExpandedIds = [],\n defaultSelectedIds = [],\n className,\n children,\n ...props\n },\n ref\n ) => {\n // ── Expand state(受控 / 非受控) ──\n const [internalExpanded, setInternalExpanded] = React.useState(\n () => new Set(defaultExpandedIds)\n )\n const expandedIds = controlledExpanded ?? internalExpanded\n const setExpandedIds = React.useCallback(\n (updater: (prev: Set<string>) => Set<string>) => {\n const update = (prev: Set<string>) => {\n const next = updater(prev)\n onExpandedChange?.(next)\n return next\n }\n if (controlledExpanded) {\n update(controlledExpanded)\n } else {\n setInternalExpanded(update)\n }\n },\n [controlledExpanded, onExpandedChange]\n )\n\n // ── Selection state(受控 / 非受控) ──\n const [internalSelected, setInternalSelected] = React.useState(\n () => new Set(defaultSelectedIds)\n )\n const selectedIds = controlledSelected ?? internalSelected\n const setSelectedIds = React.useCallback(\n (updater: (prev: Set<string>) => Set<string>) => {\n const update = (prev: Set<string>) => {\n const next = updater(prev)\n onSelectedChange?.(next)\n return next\n }\n if (controlledSelected) {\n update(controlledSelected)\n } else {\n setInternalSelected(update)\n }\n },\n [controlledSelected, onSelectedChange]\n )\n\n // ── Focus state ──\n const [focusedId, setFocusedId] = React.useState<string | null>(null)\n\n // ── Keyboard vs mouse detection ──\n // focus ring 只在鍵盤操作時顯示,滑鼠點擊用 bg-neutral-selected 表達選中,不顯示 ring\n const isKeyboardRef = React.useRef(false)\n\n // ── Drag state ──\n const [draggingId, setDraggingId] = React.useState<string | null>(null)\n const [dropTarget, setDropTarget] = React.useState<{ id: string; position: TreeDropPosition; depth: number } | null>(null)\n const autoExpandTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n // 2026-05-16 audit codex Round 6:unmount cleanup(原 cleanup 只在 dragEnd/dragCancel,unmount-during-drag 漏 cancel)\n React.useEffect(() => () => { if (autoExpandTimerRef.current) clearTimeout(autoExpandTimerRef.current) }, [])\n // Ref for toggleExpand — handleDragOver 定義在 toggleExpand 之前(hook 順序限制),\n // 用 ref 打斷 temporal dead zone。\n const toggleExpandRef = React.useRef<(id: string) => void>(() => {})\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } })\n )\n\n const handleDragStart = React.useCallback((event: DragStartEvent) => {\n setDraggingId(String(event.active.id))\n }, [])\n\n // ── Figma-style drop detection(X + Y 雙軸)──\n //\n // Y 軸:決定在哪個 item 附近\n // - item 上 25% = before\n // - item 中 50% = inside(只有 folder)\n // - item 下 25% = after\n //\n // X 軸:決定 nesting 深度(Figma 核心邏輯)\n // - 滑鼠越左 = 越淺層(放在 parent 層級)\n // - 滑鼠越右 = 越深層(放進 folder)\n // - 用 pointer X 相對於 tree 左邊界計算 indent level\n //\n const handleDragOver = React.useCallback((event: DragOverEvent) => {\n const { over, active } = event\n if (!over || over.id === active.id) {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n setDropTarget(null)\n return\n }\n\n const rowEl = document.querySelector(`[data-tree-row=\"${over.id}\"]`) as HTMLElement | null\n const targetEl = document.querySelector(`[data-tree-id=\"${over.id}\"]`) as HTMLElement | null\n if (!rowEl || !targetEl) { setDropTarget(null); return }\n\n // 實際指標位置\n const startX = (event.activatorEvent as PointerEvent)?.clientX ?? 0\n const startY = (event.activatorEvent as PointerEvent)?.clientY ?? 0\n const currentX = startX + (event.delta?.x ?? 0)\n const currentY = startY + (event.delta?.y ?? 0)\n\n const rect = rowEl.getBoundingClientRect()\n const offsetY = currentY - rect.top\n const height = rect.height || 32\n const ratio = Math.max(0, Math.min(1, offsetY / height))\n\n const hasChildren = targetEl.dataset.treeHasChildren === 'true'\n const targetDepth = Number(targetEl.getAttribute('aria-level') ?? 1) - 1\n\n // ── X 軸:計算指標在哪個 indent level ──\n const treeEl = treeRef.current\n const treeLeft = treeEl?.getBoundingClientRect().left ?? 0\n const indentStep = INDENT_STEP[size]\n const pointerIndentLevel = Math.max(0, Math.floor((currentX - treeLeft) / indentStep))\n\n let position: TreeDropPosition\n let finalDepth = targetDepth\n\n if (hasChildren) {\n // Folder node\n if (ratio < 0.25) {\n position = 'before'\n } else if (ratio > 0.75) {\n // after folder: 如果指標在 folder 層級或更淺 = after(同層)\n // 如果指標更深 = inside(放進 folder)\n position = pointerIndentLevel > targetDepth ? 'inside' : 'after'\n } else {\n position = 'inside'\n }\n } else {\n // Leaf node\n if (ratio < 0.5) {\n position = 'before'\n } else {\n position = 'after'\n // X 軸:如果指標在比 target 更淺的層級,提升 drop depth\n // 例:Contact(depth 1)的 after,如果滑鼠在 depth 0 → 變成「after Pages」\n if (pointerIndentLevel < targetDepth) {\n // 找 parent 來放\n const groupEl = targetEl.closest('[role=\"group\"]')\n const parentTreeItem = groupEl?.parentElement?.closest('[role=\"treeitem\"]')\n const parentId = parentTreeItem?.getAttribute('data-tree-id')\n if (parentId && parentId !== String(active.id)) {\n const parentDepth = Number(parentTreeItem?.getAttribute('aria-level') ?? 1) - 1\n finalDepth = parentDepth\n setDropTarget({ id: parentId, position: 'after', depth: parentDepth })\n return\n }\n }\n }\n }\n\n setDropTarget({ id: String(over.id), position, depth: finalDepth })\n\n // Auto-expand collapsed folder after 500ms hover (Figma behavior)\n if (position === 'inside' && hasChildren && !expandedIds.has(String(over.id))) {\n if (autoExpandTimerRef.current) clearTimeout(autoExpandTimerRef.current)\n autoExpandTimerRef.current = setTimeout(() => {\n toggleExpandRef.current(String(over.id))\n }, 500)\n } else {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n }\n }, [expandedIds])\n\n const dropTargetRef = React.useRef(dropTarget)\n dropTargetRef.current = dropTarget\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n const { active, over } = event\n const dt = dropTargetRef.current\n if (over && active.id !== over.id && dt) {\n onDragEndProp?.({\n sourceId: String(active.id),\n targetId: String(over.id),\n position: dt.position,\n })\n }\n setDraggingId(null)\n setDropTarget(null)\n }, [onDragEndProp])\n\n const handleDragCancel = React.useCallback(() => {\n if (autoExpandTimerRef.current) { clearTimeout(autoExpandTimerRef.current); autoExpandTimerRef.current = null }\n setDraggingId(null)\n setDropTarget(null)\n }, [])\n\n // ── Node registry ──\n const { registerNode, unregisterNode, getNodeInfo } = useNodeRegistry()\n\n // ── Actions ──\n const toggleExpand = React.useCallback(\n (id: string) => {\n setExpandedIds((prev) => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n },\n [setExpandedIds]\n )\n toggleExpandRef.current = toggleExpand\n\n const select = React.useCallback(\n (id: string) => {\n if (selectionMode === 'none') return\n setSelectedIds((prev) => {\n if (selectionMode === 'single') {\n return new Set([id])\n }\n // multiple\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n },\n [selectionMode, setSelectedIds]\n )\n\n // ── Context value ──\n const contextValue = React.useMemo<TreeViewContextValue>(\n () => ({\n size,\n context,\n selectionMode,\n expandOnSelect,\n draggable,\n isKeyboardRef,\n draggingId,\n dropTarget,\n expandedIds,\n selectedIds,\n focusedId,\n toggleExpand,\n select,\n setFocusedId,\n registerNode,\n unregisterNode,\n getNodeInfo,\n }),\n [\n size,\n context,\n selectionMode,\n expandOnSelect,\n draggable,\n isKeyboardRef,\n draggingId,\n dropTarget,\n expandedIds,\n selectedIds,\n focusedId,\n toggleExpand,\n select,\n setFocusedId,\n registerNode,\n unregisterNode,\n getNodeInfo,\n ]\n )\n\n // ── Keyboard handler ──\n const treeRef = React.useRef<HTMLDivElement>(null)\n React.useImperativeHandle(ref, () => treeRef.current!)\n\n const handleMouseDown = React.useCallback(() => {\n isKeyboardRef.current = false\n }, [])\n\n // code-quality-allow: long-function — helper fn 結構緊密,拆 sub-fn 會跨 fn 傳 state 反而複雜\n const handleKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n isKeyboardRef.current = true\n if (!treeRef.current) return\n\n // 取得所有可見的 treeitem\n const items = Array.from(\n treeRef.current.querySelectorAll<HTMLElement>('[role=\"treeitem\"]:not([hidden])')\n )\n const currentIndex = items.findIndex(\n (el) => el.dataset.treeId === focusedId\n )\n if (currentIndex < 0 && items.length > 0 && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {\n // 沒有焦點時,任何方向鍵先聚焦第一個\n setFocusedId(items[0].dataset.treeId ?? null)\n e.preventDefault()\n return\n }\n\n const currentEl = items[currentIndex]\n\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault()\n const next = items[currentIndex + 1]\n if (next) setFocusedId(next.dataset.treeId ?? null)\n break\n }\n case 'ArrowUp': {\n e.preventDefault()\n const prev = items[currentIndex - 1]\n if (prev) setFocusedId(prev.dataset.treeId ?? null)\n break\n }\n case 'ArrowRight': {\n e.preventDefault()\n const id = currentEl?.dataset.treeId\n if (!id) break\n const isExpanded = expandedIds.has(id)\n const hasChildren = currentEl?.dataset.treeHasChildren === 'true'\n if (hasChildren && !isExpanded) {\n toggleExpand(id)\n } else if (hasChildren && isExpanded) {\n // 已展開 → 移到第一個 child\n const next = items[currentIndex + 1]\n if (next) setFocusedId(next.dataset.treeId ?? null)\n }\n break\n }\n case 'ArrowLeft': {\n e.preventDefault()\n const id = currentEl?.dataset.treeId\n if (!id) break\n const isExpanded = expandedIds.has(id)\n const hasChildren = currentEl?.dataset.treeHasChildren === 'true'\n if (hasChildren && isExpanded) {\n toggleExpand(id)\n } else {\n // 收合狀態或 leaf → 移到 parent\n const parentId = currentEl?.dataset.treeParentId\n if (parentId) setFocusedId(parentId)\n }\n break\n }\n case 'Home': {\n e.preventDefault()\n if (items[0]) setFocusedId(items[0].dataset.treeId ?? null)\n break\n }\n case 'End': {\n e.preventDefault()\n const last = items[items.length - 1]\n if (last) setFocusedId(last.dataset.treeId ?? null)\n break\n }\n case 'Enter':\n case ' ': {\n e.preventDefault()\n const id = currentEl?.dataset.treeId\n if (id) select(id)\n break\n }\n }\n },\n [focusedId, expandedIds, toggleExpand, select, setFocusedId]\n )\n\n const treeEl = (\n <div\n ref={treeRef}\n role=\"tree\"\n aria-multiselectable={selectionMode === 'multiple' || undefined}\n className={cn(\n // TreeView root 不加任何 py——呼吸空間由外層容器負責:\n // - 在 SidebarGroup 內: SidebarGroup py-2 提供\n // - 在 DropdownMenuContent 內: content py-2 提供\n // - 獨立使用(story demo): consumer 自己加 py-2\n // 這樣才能跟 DropdownMenu / MenuGroup 的結構一致(group 是容器,row 是內容)。\n 'flex flex-col',\n className,\n )}\n style={{\n ['--tree-px' as string]: CONTEXT_PX_VAR[context],\n ...props.style,\n } as React.CSSProperties}\n onKeyDown={handleKeyDown}\n onMouseDown={handleMouseDown}\n tabIndex={0}\n {...props}\n >\n {children}\n </div>\n )\n\n return (\n <TreeViewContext.Provider value={contextValue}>\n {/* RowSizeProvider:讓 TreeView 子樹內任何 <ItemIcon> / <ItemAvatar> /\n <ItemInlineAction> 自動讀到對的 size,跟 SidebarProvider 同一條規則。\n 未來 TreeView 接 inlineActions API 後也吃這個 context。 */}\n <RowSizeProvider value={size}>\n {/* 永遠包 DndContext(hooks 不能 conditional call)。不 draggable 時無 sensors = 不可拖 */}\n <DndContext\n sensors={draggable ? sensors : undefined}\n onDragStart={handleDragStart}\n onDragOver={handleDragOver}\n onDragEnd={handleDragEnd}\n onDragCancel={handleDragCancel}\n >\n {treeEl}\n {draggable && (\n <DragOverlay dropAnimation={null}>\n {draggingId ? (() => {\n const info = getNodeInfo(draggingId)\n const IconComp = info?.icon\n return (\n <div className={cn(\n 'flex items-center gap-2 rounded-lg bg-surface border border-border pointer-events-none',\n 'shadow-[var(--elevation-200)]',\n size === 'lg' ? 'text-body-lg leading-compact px-4 py-2' : 'text-body leading-compact px-3 py-1.5',\n )}>\n {IconComp && <IconComp size={ICON_SIZE[size]} className=\"shrink-0\" aria-hidden />}\n <span className=\"text-foreground truncate max-w-[200px]\">{info?.label ?? draggingId}</span>\n </div>\n )\n })() : null}\n </DragOverlay>\n )}\n </DndContext>\n </RowSizeProvider>\n </TreeViewContext.Provider>\n )\n }\n)\nTreeView.displayName = 'TreeView'\n\n// ═══════════════════════════════════════════════════════════════════════════\n// TreeItem variants\n// ═══════════════════════════════════════════════════════════════════════════\n\nconst treeItemVariants = cva(\n [\n // items-start:多行 label 時 prefix 留在第一行(item-layout 規則)\n 'flex items-start gap-2 w-full',\n 'cursor-pointer select-none',\n 'transition-colors duration-150',\n 'outline-none',\n // Label 字重 500(跟 SidebarMenuButton 一致)\n 'font-medium',\n ],\n {\n variants: {\n // 消費 ROW_PADDING_BY_SIZE SSOT(item-anatomy.tsx)— drift risk 消除\n size: ROW_PADDING_BY_SIZE,\n },\n defaultVariants: {\n size: 'md',\n },\n }\n)\n\n// ═══════════════════════════════════════════════════════════════════════════\n// TreeItem\n// ═══════════════════════════════════════════════════════════════════════════\n\nexport interface TreeItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'id'> {\n /** 唯一 id。必填,用於 expand / select / keyboard 追蹤 */\n id: string\n /** 主要文字 */\n label: React.ReactNode\n /** 左側 icon(chevron 之後)。LucideIcon 型別,尺寸由 TreeView size 決定 */\n icon?: LucideIcon\n /**\n * Checkbox(多選模式,label 前方)。傳入 ReactNode(Checkbox 元件)。\n * 位置:在 icon 之後、label 之前。\n * 單選模式通常不需要(用 bg-neutral-selected 表達選中)。\n */\n checkbox?: React.ReactNode\n /**\n * 右側 inline actions(suffix slot,宣告式 API)。對齊 `uiSize.spec.md`「Inline Action」\n * 與 `SidebarMenuButton.inlineActions` 的同一條規格——TreeItem / SidebarMenuButton /\n * 未來的 row primitive 全部用同一個 declarative API。\n *\n * Consumer 只宣告 intent,TreeItem 用 `<ItemInlineAction>` 自動渲染:\n * - Icon 尺寸 = `ICON_SIZE[treeViewSize]`(自動)\n * - Hover bg、tooltip、aria-label、cursor-pointer 自動處理\n * - **不可以**手刻 button JSX(canonical 實作在 `item-layout.tsx`)\n *\n * ```tsx\n * <TreeItem\n * id=\"inbox\"\n * icon={Inbox}\n * label=\"Inbox\"\n * inlineActions={[\n * { icon: MoreVertical, label: '更多', onClick: handleMore },\n * { icon: Plus, label: '新增', onClick: handleAdd },\n * ]}\n * actionsReveal=\"hover\"\n * />\n * ```\n *\n * 若需要永遠可見的 suffix(如 badge 計數),放在 `label` 內:\n * ```tsx\n * <TreeItem label={<>Inbox <Badge count={3} /></>} />\n * ```\n */\n inlineActions?: InlineActionConfig[]\n /**\n * 右側 actions slot(ReactNode)— escape hatch 供 consumer 放自訂元素\n * (如 DropdownMenu trigger / 自訂 popover / 多 tier 動作)。\n *\n * 跟 `inlineActions` 互斥(同時傳 `inlineActionsSlot` 會優先,`inlineActions` 被忽略)。\n *\n * 規則對齊 Input.endSlot canonical:90% case 用 `inlineActions` 宣告式 API,\n * 10% config 表達不出時走 slot。視覺一致性由 consumer 負責(可使用 host 內部 helper\n * — 但禁止 app-code 直接 import L3 primitive,見 `check_l3_primitive_import.sh`)。\n */\n inlineActionsSlot?: React.ReactNode\n /**\n * Inline actions 的顯示模式:\n * - `\"hover\"`(預設):row hover 或鍵盤 focus(focus-visible)時才淡入\n * - `false`:常駐顯示\n *\n * 對齊 `SidebarMenuButton.actionsReveal`,同一套規則。\n */\n actionsReveal?: false | \"hover\"\n /**\n * 取代 chevron 的位置。用於 stepper 的 status indicator(●/○/✓)。\n * 設定後 chevron 不渲染,改渲染 indicator。\n */\n indicator?: React.ReactNode\n /** 是否停用 */\n disabled?: boolean\n /** 子 TreeItem(有 children = expandable,沒有 = leaf) */\n children?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(\n ({ id, label, icon: Icon, checkbox, inlineActions, inlineActionsSlot, actionsReveal = 'hover', indicator, disabled, children, className, ...props }, ref) => {\n const ctx = useTreeView()\n const depth = React.useContext(DepthContext)\n const {\n size,\n selectionMode,\n expandOnSelect,\n draggable,\n expandedIds,\n selectedIds,\n focusedId,\n draggingId,\n dropTarget,\n toggleExpand,\n select,\n setFocusedId,\n registerNode,\n unregisterNode,\n isKeyboardRef,\n } = ctx\n\n const hasChildren = React.Children.count(children) > 0\n const isExpanded = expandedIds.has(id)\n const isSelected = selectedIds.has(id)\n const isFocused = focusedId === id\n const showRing = isFocused && isKeyboardRef.current\n const isDragging = draggingId === id\n const isDropTarget = dropTarget?.id === id\n\n const iconPx = ICON_SIZE[size]\n const indentPx = depth * INDENT_STEP[size]\n\n // ── Drag hooks ──\n // Figma 風格:整列可拖(不用 grip handle),靠 distance:5 區分 click vs drag\n const { attributes: dragAttrs, listeners: dragListeners, setNodeRef: setDragRef } = useDraggable({\n id, disabled: !draggable || disabled,\n })\n const { setNodeRef: setDropRef } = useDroppable({\n id, disabled: !draggable || disabled,\n })\n\n // ── 找 parent id(from depth context chain)──\n const parentId = React.useContext(ParentIdContext)\n\n // ── Register / unregister ──\n React.useEffect(() => {\n registerNode(id, parentId, hasChildren, label, Icon)\n return () => unregisterNode(id)\n }, [id, parentId, hasChildren, label, Icon, registerNode, unregisterNode])\n\n // ── Focus scroll into view ──\n const itemRef = React.useRef<HTMLDivElement>(null)\n React.useImperativeHandle(ref, () => itemRef.current!)\n\n React.useEffect(() => {\n if (isFocused && itemRef.current) {\n itemRef.current.scrollIntoView({ block: 'nearest' })\n }\n }, [isFocused])\n\n // ── Handlers ──\n const handleRowClick = React.useCallback(\n (e: React.MouseEvent) => {\n if (disabled) return\n e.stopPropagation()\n setFocusedId(id)\n select(id)\n if (expandOnSelect && hasChildren) {\n toggleExpand(id)\n }\n },\n [id, disabled, select, setFocusedId, expandOnSelect, hasChildren, toggleExpand]\n )\n\n const handleChevronClick = React.useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation()\n if (disabled) return\n toggleExpand(id)\n },\n [id, disabled, toggleExpand]\n )\n\n // ── Chevron(永遠存在:expandable = 旋轉箭頭;leaf = placeholder 佔位) ──\n // 消費 `<ItemPrefix>` SSOT — 永遠 h-[1lh] 對齊 label 第一行中線(item-anatomy 對應)。\n // forced width 透過 style 鎖 chevron 槽寬,讓 sibling label 起點水平對齊(無 chevron leaf 佔位同寬)。\n const chevronSlot = (\n <ItemPrefix style={{ width: iconPx }}>\n {hasChildren ? (\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={handleChevronClick}\n className={cn(\n 'flex items-center justify-center rounded-md',\n 'text-fg-muted hover:text-foreground hover:bg-neutral-hover',\n 'transition-all duration-150',\n isExpanded && 'rotate-90',\n disabled && 'text-fg-disabled pointer-events-none',\n )}\n style={{ width: iconPx, height: iconPx }}\n aria-hidden\n >\n <ChevronRight size={iconPx} />\n </button>\n ) : (\n // Leaf placeholder\n <span style={{ width: iconPx }} aria-hidden />\n )}\n </ItemPrefix>\n )\n\n return (\n <ParentIdContext.Provider value={id}>\n <div\n ref={(node) => {\n (itemRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n if (typeof ref === 'function') ref(node)\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n }}\n role=\"treeitem\"\n aria-expanded={hasChildren ? isExpanded : undefined}\n aria-selected={selectionMode !== 'none' ? isSelected : undefined}\n aria-level={depth + 1}\n aria-disabled={disabled || undefined}\n data-tree-id={id}\n data-tree-parent-id={parentId ?? ''}\n data-tree-has-children={hasChildren}\n tabIndex={-1}\n className={cn('w-full min-w-0 relative', isDragging && dragSourceClass)}\n >\n {/* Drop indicator — before:水平 2px primary line(指 SSOT drag-visual.ts);\n indent 跟隨 depth(left 由 inline style override class 的 left-0)*/}\n {isDropTarget && dropTarget?.position === 'before' && (\n <div\n className={dropIndicatorRow.before}\n style={{ left: `calc(var(--tree-px) + ${indentPx}px)` }}\n />\n )}\n\n {/* Row: draggable + droppable 都在這一行(合併 ref),確保碰撞偵測只看行高 */}\n <div\n ref={(node) => {\n // 合併 drag + drop ref 到同一個 element\n if (draggable) setDragRef(node)\n setDropRef(node)\n }}\n data-tree-row={id}\n className={cn(\n 'group/tree-item',\n treeItemVariants({ size }),\n // 2026-05-26 SSOT lock(user explicit「multi 已有 checkbox 強信號,text 不該再變色」):\n // ── Single mode ──\n // - default text 預設 fg-secondary muted(hierarchy navigation 慣例,跟 Sidebar 一致)\n // - selected → text-foreground emphasis + bg-neutral-selected(無 checkbox,需 text+bg 雙信號)\n // ── Multi mode ──\n // - default text 維持 fg-secondary muted(跟 single 對齊 hierarchy)\n // - selected → 視覺信號只在 checkbox(auto-render below),text 不變、bg 不變\n // - 對齊 SelectMenu multi pattern(menu-item.tsx:194-195 selected → bg only;multi → checkbox only)\n !disabled && !isSelected && 'text-fg-secondary',\n !disabled && isSelected && selectionMode === 'single' && 'text-foreground',\n isDropTarget && dropTarget?.position === 'inside' && dropIndicatorInside,\n !disabled && 'hover:bg-neutral-hover hover:text-foreground',\n !disabled && isSelected && selectionMode === 'single' && 'bg-neutral-selected',\n showRing && 'ring-2 ring-ring ring-inset',\n disabled && 'pointer-events-none text-fg-disabled cursor-default',\n className,\n )}\n style={{\n paddingLeft: indentPx > 0\n ? `calc(var(--tree-px) + ${indentPx}px)`\n : 'var(--tree-px)',\n paddingRight: 'var(--tree-px)',\n }}\n onClick={handleRowClick}\n {...(draggable ? { ...dragListeners, ...dragAttrs } : {})}\n {...props}\n >\n {chevronSlot}\n\n {/* Checkbox 在 icon 前——消費 `<ItemPrefix>` 對齊第一行\n * 2026-05-26 SSOT lock(user explicit「多選的方式應該也是要跟 menu 一樣是出現 checkbox」):\n * - selectionMode='multiple' + 無 consumer checkbox prop → auto-render `<Checkbox>` reflect selectedIds\n * (對齊 SelectMenu multi pattern;consumer 不用手寫 checkbox)\n * - selectionMode='multiple' + consumer 傳 checkbox → 用 consumer 的(parent-child cascade 等 advanced)\n * - selectionMode='single' / 'none' → 不 render checkbox(text-foreground + bg 雙信號表 selected)\n * 對齊 cite:menu-item.tsx:194-195(MenuItem selected bg)+ select-menu.tsx:352-354(SelectMenu multi=checkbox) */}\n {(checkbox || selectionMode === 'multiple') && (\n <ItemPrefix className=\"pointer-events-none\">\n {checkbox || <Checkbox checked={isSelected} disabled={disabled} aria-hidden=\"true\" />}\n </ItemPrefix>\n )}\n\n {/* indicator 取代 icon 的位置;h-[1lh] 對齊第一行\n indicator 是 escape hatch(stepper status dot 等客製內容),消費 `<ItemPrefix>` 鎖 chevron 槽寬;\n Icon 走 canonical `<ItemIcon>` helper——自動標 data-prefix-type=\"icon\",\n 讓 SidebarProvider 的全域 :has() prefix-mix 偵測能命中。 */}\n {indicator ? (\n <ItemPrefix style={{ width: iconPx }}>\n {indicator}\n </ItemPrefix>\n ) : Icon ? (\n <ItemIcon icon={Icon} className={disabled ? 'text-fg-disabled' : undefined} />\n ) : null}\n\n <span className={cn('flex-1 min-w-0 truncate', disabled && 'text-fg-disabled')}>\n {label}\n </span>\n\n {/* Suffix inline actions——宣告式 API,用 `<ItemInlineAction>` 渲染。\n 消費 `<ItemSuffix hoverReveal hoverGroup=\"tree-item\">` SSOT(2026-05-05 v8 group selector 參數化後)。\n actionsReveal=\"hover\"(預設):row hover 或 keyboard focus-visible 才顯示;\n actionsReveal=false:常駐顯示。跟 SidebarMenuButton 共用同一條規則,行為一致。\n inlineActionsSlot escape hatch 優先(consumer 自控 JSX,reveal 一樣套外層 group)。 */}\n {inlineActionsSlot ? (\n <ItemSuffix hoverReveal={actionsReveal === 'hover'} hoverGroup=\"tree-item\">\n {inlineActionsSlot}\n </ItemSuffix>\n ) : inlineActions && inlineActions.length > 0 ? (\n <ItemSuffix hoverReveal={actionsReveal === 'hover'} hoverGroup=\"tree-item\">\n {inlineActions.map((action, i) => (\n <ItemInlineAction key={action.label + i} action={action} />\n ))}\n </ItemSuffix>\n ) : null}\n </div>\n\n {/* Drop indicator — after:同 before mirror 到 bottom edge(SSOT drag-visual.ts)*/}\n {isDropTarget && dropTarget?.position === 'after' && (\n <div\n className={dropIndicatorRow.after}\n style={{ left: `calc(var(--tree-px) + ${indentPx}px)` }}\n />\n )}\n\n {/* Children: Collapsible 展開/收合 */}\n {hasChildren && (\n <CollapsiblePrimitive.Root open={isExpanded}>\n <CollapsiblePrimitive.Content\n className=\"overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\"\n >\n <DepthContext.Provider value={depth + 1}>\n <div role=\"group\" className=\"flex flex-col w-full\">\n {children}\n </div>\n </DepthContext.Provider>\n </CollapsiblePrimitive.Content>\n </CollapsiblePrimitive.Root>\n )}\n </div>\n </ParentIdContext.Provider>\n )\n }\n)\nTreeItem.displayName = 'TreeItem'\n\n// Parent ID context for keyboard navigation (← to parent)\nconst ParentIdContext = React.createContext<string | null>(null)\n\n// ═══════════════════════════════════════════════════════════════════════════\n// Exports\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 treeViewMeta = {\n component: 'TreeView',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-neutral-hover', 'bg-primary', 'bg-primary-subtle', 'bg-surface'],\n fg: ['text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { TreeView, TreeItem, treeItemVariants }\n"],"names":["treeEl"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qijenchen/design-system",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
|
|
6
6
|
"type": "module",
|
|
@@ -610,12 +610,12 @@ Sheet 開啟狀態**不持久化**。
|
|
|
610
610
|
|
|
611
611
|
| Token | 預設值 | 用途 |
|
|
612
612
|
|-------|--------|------|
|
|
613
|
-
| `--sidebar-width` | `
|
|
613
|
+
| `--sidebar-width` | `15rem` (240px) | 展開寬度(2026-05-21 v11 user 拍板 240px;對齊 Linear/Notion/Figma/Polaris/Material Drawer 主流 240–256,5/8 家 = 240。也是 `--sidebar-width-min` AppShell Aside floor 共識值)|
|
|
614
614
|
| `--sidebar-width-icon` | `calc(2 * var(--layout-space-loose) + var(--sidebar-menu-icon-size))` | icon 模式寬度(2026-05-21 v3 撤回 `3rem` hardcode,改 geometry formula 保證 icon center x = loose + icon/2 在展開/收合一致;md=48 / lg=64,跟 chrome-header-height 解耦)|
|
|
615
615
|
| `--sidebar-menu-icon-size` | `1rem` (16px) | sidebar menu icon 大小(per ICON_SIZE.sm/md=16);size=lg 罕見 case override `1.25rem` |
|
|
616
616
|
| `--sidebar-width-mobile` | `18rem` | Mobile sheet 寬度 |
|
|
617
617
|
|
|
618
|
-
**用 rem**:跟隨 root font-size,支援 accessibility 等比例縮放。**不用 layoutSpace / uiSize 推導**:sidebar width 是容器維度,不是間距或元件內高度,語意不同;耦合會讓寬度跟間距連動。**
|
|
618
|
+
**用 rem**:跟隨 root font-size,支援 accessibility 等比例縮放。**不用 layoutSpace / uiSize 推導**:sidebar width 是容器維度,不是間距或元件內高度,語意不同;耦合會讓寬度跟間距連動。**15rem / 18rem** 對齊 Linear/Notion/Figma/Polaris/Material Drawer 240–288 主流寬度,`--sidebar-width-icon` 用 geometry formula(2026-05-21 v3)`calc(2*loose + icon-size)` 保證 collapse/expand icon center x 一致。
|
|
619
619
|
|
|
620
620
|
---
|
|
621
621
|
|
|
@@ -131,16 +131,24 @@ Chevron 是**展開/收合控件**,不是 prefix icon:`fg-muted`(指示色,hover
|
|
|
131
131
|
|
|
132
132
|
### 多選(file browser / permission picker)
|
|
133
133
|
|
|
134
|
-
- **視覺 SSOT(
|
|
135
|
-
- **Checkbox
|
|
136
|
-
- **Row
|
|
137
|
-
- `text-foreground
|
|
138
|
-
- 對齊 cite
|
|
139
|
-
- API:`selectionMode="multiple"`
|
|
134
|
+
- **視覺 SSOT(2026-05-26 user explicit lock)**:對齊 `SelectMenu` multi pattern,checkbox 為**唯一** visual signal —
|
|
135
|
+
- **Auto-render `<Checkbox>`**:`selectionMode="multiple"` 時 TreeItem 內建 render checkbox(reflect `selectedIds`),consumer **無需手動傳** `checkbox` prop
|
|
136
|
+
- **Row 不套 `bg-neutral-selected`**(該 token 保留給 single mode 補無 checkbox 的視覺信號)
|
|
137
|
+
- **Text 也不變 `text-foreground`**(已有 checkbox 強信號,text 變色會雙重 noise)→ 維持 `text-fg-secondary` muted
|
|
138
|
+
- 對齊 cite:`menu-item.tsx:194-195`(MenuItem selected → bg only)+ `select-menu.tsx:352-354`(SelectMenu multi → checkbox only)
|
|
139
|
+
- API:`selectionMode="multiple"` 自動 render checkbox;consumer 傳 `checkbox={<Checkbox/>}` 可 override(parent-child cascade 等 advanced 場景)
|
|
140
140
|
- `Shift+Click` 範圍選取 / `Ctrl/Cmd+Click` 切換個別 / `aria-multiselectable="true"` 在 TreeView 上
|
|
141
|
-
- Checkbox state 應反映**內建 `selectedIds`**(consumer 不該繞過 built-in selection 自管 `checked` Record)— 修 `WithCheckbox` story 對齊
|
|
142
141
|
|
|
143
|
-
|
|
142
|
+
### 視覺信號 SSOT 對照表(single vs multi)
|
|
143
|
+
|
|
144
|
+
| Mode | Default text | Selected text | Selected bg | Checkbox |
|
|
145
|
+
|---|---|---|---|---|
|
|
146
|
+
| `single` | `text-fg-secondary`(muted)| `text-foreground` ✅ | `bg-neutral-selected` ✅ | N/A |
|
|
147
|
+
| `multiple` | `text-fg-secondary`(muted)| 不變(維持 muted)| 不變 | auto `<Checkbox checked={isSelected} />` ✅ |
|
|
148
|
+
|
|
149
|
+
設計理由:single 沒 checkbox → text+bg 雙信號補;multi 有 checkbox 強信號 → text+bg 不再變化避 noise。
|
|
150
|
+
|
|
151
|
+
歷史錨例(2026-05-26):本 session 多次 revert + 對齊。User 明確「multi 已有 checkbox,text 不該再變色」→ 鎖 multi mode `text-foreground` apply 改 `selectionMode === 'single'` only。對齊 SelectMenu pattern,無偏移。
|
|
144
152
|
|
|
145
153
|
### 無選取(純展開/收合)
|
|
146
154
|
|
|
@@ -18,6 +18,7 @@ import { cva } from 'class-variance-authority'
|
|
|
18
18
|
import type { LucideIcon } from 'lucide-react'
|
|
19
19
|
import { dragSourceClass, dropIndicatorRow, dropIndicatorInside } from '@/design-system/lib/drag-visual'
|
|
20
20
|
import { cn } from '@/lib/utils'
|
|
21
|
+
import { Checkbox } from '@/design-system/components/Checkbox/checkbox'
|
|
21
22
|
// Row primitive 共用常數——單一 source of truth
|
|
22
23
|
import {
|
|
23
24
|
ICON_SIZE,
|
|
@@ -907,18 +908,18 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
|
|
|
907
908
|
className={cn(
|
|
908
909
|
'group/tree-item',
|
|
909
910
|
treeItemVariants({ size }),
|
|
910
|
-
//
|
|
911
|
-
//
|
|
911
|
+
// 2026-05-26 SSOT lock(user explicit「multi 已有 checkbox 強信號,text 不該再變色」):
|
|
912
|
+
// ── Single mode ──
|
|
913
|
+
// - default text 預設 fg-secondary muted(hierarchy navigation 慣例,跟 Sidebar 一致)
|
|
914
|
+
// - selected → text-foreground emphasis + bg-neutral-selected(無 checkbox,需 text+bg 雙信號)
|
|
915
|
+
// ── Multi mode ──
|
|
916
|
+
// - default text 維持 fg-secondary muted(跟 single 對齊 hierarchy)
|
|
917
|
+
// - selected → 視覺信號只在 checkbox(auto-render below),text 不變、bg 不變
|
|
918
|
+
// - 對齊 SelectMenu multi pattern(menu-item.tsx:194-195 selected → bg only;multi → checkbox only)
|
|
912
919
|
!disabled && !isSelected && 'text-fg-secondary',
|
|
913
|
-
!disabled && isSelected && 'text-foreground',
|
|
914
|
-
// inside: 資料夾背景高亮(Figma 風格),不用 ring/border
|
|
920
|
+
!disabled && isSelected && selectionMode === 'single' && 'text-foreground',
|
|
915
921
|
isDropTarget && dropTarget?.position === 'inside' && dropIndicatorInside,
|
|
916
922
|
!disabled && 'hover:bg-neutral-hover hover:text-foreground',
|
|
917
|
-
// 2026-05-26 RESTORE(per DS SSOT M23):bg-neutral-selected 只 single mode 套。
|
|
918
|
-
// 對齊 SelectMenu(select-menu.tsx:352-354)既有 canonical:
|
|
919
|
-
// multi-select = checkbox 表達 selection,row 本身不套 bg highlight
|
|
920
|
-
// 之前一次 fix(2026-05-26 13:00 commit b8843c2b)引世界級對照 macOS Finder 改 bg apply
|
|
921
|
-
// 多選,違反 M23「DS 既有 canonical 優先於外部 benchmark」。User 抓 + revert。
|
|
922
923
|
!disabled && isSelected && selectionMode === 'single' && 'bg-neutral-selected',
|
|
923
924
|
showRing && 'ring-2 ring-ring ring-inset',
|
|
924
925
|
disabled && 'pointer-events-none text-fg-disabled cursor-default',
|
|
@@ -936,10 +937,16 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
|
|
|
936
937
|
>
|
|
937
938
|
{chevronSlot}
|
|
938
939
|
|
|
939
|
-
{/* Checkbox 在 icon 前——消費 `<ItemPrefix>` 對齊第一行
|
|
940
|
-
|
|
940
|
+
{/* Checkbox 在 icon 前——消費 `<ItemPrefix>` 對齊第一行
|
|
941
|
+
* 2026-05-26 SSOT lock(user explicit「多選的方式應該也是要跟 menu 一樣是出現 checkbox」):
|
|
942
|
+
* - selectionMode='multiple' + 無 consumer checkbox prop → auto-render `<Checkbox>` reflect selectedIds
|
|
943
|
+
* (對齊 SelectMenu multi pattern;consumer 不用手寫 checkbox)
|
|
944
|
+
* - selectionMode='multiple' + consumer 傳 checkbox → 用 consumer 的(parent-child cascade 等 advanced)
|
|
945
|
+
* - selectionMode='single' / 'none' → 不 render checkbox(text-foreground + bg 雙信號表 selected)
|
|
946
|
+
* 對齊 cite:menu-item.tsx:194-195(MenuItem selected bg)+ select-menu.tsx:352-354(SelectMenu multi=checkbox) */}
|
|
947
|
+
{(checkbox || selectionMode === 'multiple') && (
|
|
941
948
|
<ItemPrefix className="pointer-events-none">
|
|
942
|
-
{checkbox}
|
|
949
|
+
{checkbox || <Checkbox checked={isSelected} disabled={disabled} aria-hidden="true" />}
|
|
943
950
|
</ItemPrefix>
|
|
944
951
|
)}
|
|
945
952
|
|
package/src/tokens/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# tokens/ Charter
|
|
2
2
|
|
|
3
|
+
> **跨 family canonical SSOT** → `token-system.spec.md`(5-layer 架構 / 命名 family-scoped 規則 / token vs hardcode 判斷 / co-location rationale)。每個 family spec.md 只 codify 自家具體規則,本 README 列 family 居住地圖,SSOT 集中在 `token-system.spec.md` 避免 drift。
|
|
4
|
+
|
|
3
5
|
## 這裡只收:design token 定義 + spec + stories
|
|
4
6
|
|
|
5
7
|
每個 token 類別一個 folder:
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<!-- @benchmark-cited: world-class layered token architecture — Polaris/Material/Atlassian/Carbon/Apple HIG (URLs in body table) -->
|
|
2
|
+
|
|
3
|
+
# Token System 設計原則(SSOT,跨所有 token family)
|
|
4
|
+
|
|
5
|
+
> **Foundational SSOT rationale**(cap 1200,per CLAUDE.md「foundational SSOT 例外 ≤ 800-1200」):
|
|
6
|
+
> 本 spec 是**所有 token family 共同遵循的上游 canonical**——5-layer 架構、命名 family-scoped 規則、token vs hardcode 判斷、cross-family co-location rationale、SSOT consumer scope。改變本 spec = 影響全 DS 30+ 元件 + 200+ token。改一處不對齊就 drift,所以集中在這裡為 SSOT;個別 family spec(color / typography / uiSize 等)只 codify 自家具體規則,不重述上游架構。
|
|
7
|
+
|
|
8
|
+
## 架構流派定位
|
|
9
|
+
|
|
10
|
+
業界世界級 DS 都採「**分層 token 架構**」(layered token architecture),底層 raw value,上層 semantic intent。各家命名不同但概念一致:
|
|
11
|
+
|
|
12
|
+
| 流派 | 代表系統 | 層命名 | 對應我們 |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| **3-layer reference / system / component** | Material 3 | `md-ref-*` → `md-sys-*` → `md-comp-*` | Primitive / Semantic / Internal |
|
|
15
|
+
| **2-layer base / semantic + token-per-component** | Polaris | `--p-color-blue-500` → `--p-color-bg-primary` | Primitive / Semantic |
|
|
16
|
+
| **3-layer foundation / semantic / specific** | Atlassian DS | `color.text.brand` → 直接 raw oklch in foundation | Primitive / Semantic |
|
|
17
|
+
| **2-layer global / alias + size scale** | Carbon | `$blue-60` → `$button-primary` | Primitive / Semantic |
|
|
18
|
+
| **2-layer semantic + dynamic** | Apple HIG | `systemBlue` → `UIColor.label`(adaptive) | Primitive / Semantic |
|
|
19
|
+
|
|
20
|
+
**我們選 5-layer**(Primitive / Semantic / Family / Layout / Internal),擴充 Material 3 流派加兩層處理結構化常數:
|
|
21
|
+
- **Family**(Material 3 `md-sys-*` 子分支):cross-component 共用的具體尺寸 family(`--field-height-*` / `--table-row-*` / `--tab-height-*`)
|
|
22
|
+
- **Layout**(Material `md-comp-layout` 對應):跨層佈局常數(`--layout-space-loose` / `--sidebar-width` / `--chrome-header-height`)
|
|
23
|
+
- **Internal**(Material `md-comp-*` 對應):單元件內部不對 consumer 公開(目前 0 個——所有跨元件都 promote 到 Family,單元件 hardcode in tsx)
|
|
24
|
+
|
|
25
|
+
`tokens/README.md`「Public vs Internal token」表 cross-reference 本 spec 此段。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 5-Layer 架構
|
|
30
|
+
|
|
31
|
+
| Layer | 抽象度 | 例子 | 居住檔案 | Consumer scope |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| **L1 Primitive** | 原始 raw value(無 mode 知識 / 無 component 知識) | `--color-blue-6` / `--color-neutral-9-opaque` / `--black-a45` / `--elevation-100` | `color/primitives.css` | **Internal**——只供 semantic 層 alias,或 Tag/Avatar 等「按色分類」元件直接消費(per `color.spec.md` 流派定位) |
|
|
34
|
+
| **L2 Semantic** | 表達**意圖** / 封裝 mode swap | `--primary` / `--error` / `--canvas` / `--surface` / `--foreground` / `--fg-disabled` / `--bg-disabled` | `color/semantic.css` | **Public**——consumer 直接 `bg-primary` / `text-foreground` |
|
|
35
|
+
| **L3 Family**(cross-component 共用尺寸) | family-scoped semantic value | `--field-height-md` / `--table-row-md` / `--tab-height-md` / `--tree-indent-md` | `uiSize/uiSize.css` | **Internal**——DS 內部 primitive 消費,consumer 用 `<Button size="sm">` 不直接寫 token |
|
|
36
|
+
| **L4 Layout**(全 app 結構常數) | 全域 layout primitive 尺寸 | `--sidebar-width` / `--sidebar-width-mobile` / `--chrome-header-height` / `--layout-space-loose` | `uiSize/uiSize.css` + `layoutSpace/layoutSpace.css` | **Public**——AppShell / Sidebar / ChromeHeader 等 layout primitive 元件直接消費 |
|
|
37
|
+
| **L5 Internal**(單元件內部) | 單元件 only | (目前 0 個) | — | 不開——若只 1 個 consumer hardcode in tsx 即可,2+ consumer 才升 L3/L4 |
|
|
38
|
+
|
|
39
|
+
**Layer 判斷流程**:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
新增值 / 公式 / 結構常數時 ──→
|
|
43
|
+
是 raw color/shadow/alpha 沒 component 意圖? ─→ L1 Primitive
|
|
44
|
+
是「意圖」/ semantic state? ─────────────────→ L2 Semantic(必經 L1 alias)
|
|
45
|
+
跨 component(Button/Input/Field 等都吃)? ──→ L3 Family
|
|
46
|
+
全 app 結構(AppShell / Header / Sidebar)? ─→ L4 Layout
|
|
47
|
+
單 component 內部? ─────────────────────────→ 不開 token,hardcode in tsx
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Naming Convention(family-scoped,3 條硬規則)
|
|
53
|
+
|
|
54
|
+
### 規則 1:`--<family>-<measurement>[-<size>]` 嚴格 family-scoped
|
|
55
|
+
|
|
56
|
+
每個 token name 一定先帶 family prefix,讓 grep 即知「這 token 屬哪 family / 哪 layer」:
|
|
57
|
+
|
|
58
|
+
| Family prefix | Layer | 範例 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `--color-*` | L1 Primitive | `--color-blue-6` / `--color-neutral-9-opaque` |
|
|
61
|
+
| `--brand` / `--primary` / `--error` / `--info` / `--success` / `--warning` | L2 Semantic(色 family,history 沒寫 `--color-` 前綴是 Atlassian 流派 idiom) | `--primary-hover` / `--error-subtle` |
|
|
62
|
+
| `--canvas` / `--surface*` / `--foreground` / `--fg-*` | L2 Semantic(shadcn idiom 沿用——surface tone family + fg state family 各自獨立) | `--canvas` / `--surface-raised` / `--fg-disabled` |
|
|
63
|
+
| `--bg-*` | L2 Semantic(state pair family,跟 `--fg-*` 配對) | `--bg-disabled`(配 `--fg-disabled`) |
|
|
64
|
+
| `--neutral-hover` / `--neutral-selected*` | L2 Semantic(neutral interaction family) | `--neutral-selected-hover` |
|
|
65
|
+
| `--<hue>-hover` / `--<hue>-active` | L2 Semantic(色相互動 family) | `--blue-hover` / `--red-active` |
|
|
66
|
+
| `--font-<role>-<measurement>` | L3 Family | `--font-h1-size` / `--font-body-size` |
|
|
67
|
+
| `--field-height-<size>` / `--table-row-<size>` / `--tab-height-<size>` / `--tree-indent-<size>` | L3 Family | `--field-height-md` |
|
|
68
|
+
| `--<part>-width` / `--<part>-height` / `--<part>-<measurement>` | L4 Layout | `--sidebar-width` / `--chrome-header-height` |
|
|
69
|
+
| `--layout-space-<role>` | L4 Layout | `--layout-space-loose` / `--layout-space-tight` |
|
|
70
|
+
| `--radius-<size>` | L3 Family(structural) | `--radius-md` |
|
|
71
|
+
| `--elevation-<step>[-hover]` | L1 Primitive(住 primitives.css 跟色一起,理由見下) | `--elevation-100` / `--elevation-200-hover` |
|
|
72
|
+
| `--opacity-<role>` | L1 Primitive(structural,單一值) | `--opacity-disabled` |
|
|
73
|
+
| `--<part>-<measurement>-<size>` | L4 Layout(panel widths 等)| `--data-table-sort-panel-width` |
|
|
74
|
+
|
|
75
|
+
### 規則 2:`--<size>` suffix 跟 family 共用 size vocabulary
|
|
76
|
+
|
|
77
|
+
所有 size suffix 統一 `xs / sm / md / lg`(elevation 例外用 `100 / 200` Material idiom,因 elevation 不跟 density 走)。**禁** 自創 `tiny` / `small` / `regular` / `large` 等同義詞。
|
|
78
|
+
|
|
79
|
+
### 規則 3:Tailwind utility bridge 命名同步
|
|
80
|
+
|
|
81
|
+
每個 token 若提供 utility(`text-h1` / `bg-primary` / `h-field-md`)必經 `@theme inline` 或 `@utility`,utility 名 = token name 去 `--` 前綴 + 必要的 utility prefix(`bg-` / `text-` / `h-`)。**禁** 在 bridge 中改名(`--font-body-size` 不可橋成 `text-paragraph`)。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Token vs Hardcode 判斷
|
|
86
|
+
|
|
87
|
+
**標準:同值在 ≥ 2 處需要保持同步 → 必開 token。** 1 處 = hardcode + 註解 why(若非常識值);2+ 處 = token。
|
|
88
|
+
|
|
89
|
+
### Token 化必過 3 題
|
|
90
|
+
|
|
91
|
+
1. **這值會在 ≥ 2 處同步出現嗎?**(2 處不一定要 sync 也不算——須有「同步義務」,例如「Button 跟 Input 必同高」)
|
|
92
|
+
2. **找得到既有 family 可鏡射嗎?**(命中 → 用既有 family 加 size variant;沒命中 → 走規則 1 命名,別自創孤立 family)
|
|
93
|
+
3. **改值時所有 consumer 都該跟著變嗎?**(YES → token;NO → hardcode 因為各 consumer 各自意圖)
|
|
94
|
+
|
|
95
|
+
任一 NO → hardcode in code + 註解 why。
|
|
96
|
+
|
|
97
|
+
### Hardcode 合法情境
|
|
98
|
+
|
|
99
|
+
- 單一 consumer 的 padding / gap / size(eg. Tooltip arrow `4px`)
|
|
100
|
+
- 跟設計上下文耦合的值(eg. 圖表 axis tick gap)
|
|
101
|
+
- 數學 / 物理常數(eg. `Math.PI`,動畫 `easing` 公式參數)
|
|
102
|
+
|
|
103
|
+
**禁止以「省工」「之後再開」「先寫死試試」為由拒絕開 token。**(違 mindset #1)
|
|
104
|
+
|
|
105
|
+
### Hardcode 必註解(若不是視覺常識值)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// hardcode 24px:Tooltip arrow geometry,只此一處 consumer + 跟 Radix arrow lib 物理綁定
|
|
109
|
+
<TooltipArrow width={24} />
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 跨 family co-location 規則(2026-05-26 codify)
|
|
115
|
+
|
|
116
|
+
某些 token 雖然名前綴看似屬一 family,但實際 co-located 在另一 family 檔案——**有意的依賴關係,不是命名 drift**:
|
|
117
|
+
|
|
118
|
+
### Co-location 1:`--elevation-*` 住 `color/primitives.css`
|
|
119
|
+
|
|
120
|
+
**Why**:elevation 跟 color 共用 light/dark mode 切換 trigger(`[data-theme="dark"]`)。把 elevation 跟 color primitives 放同一個 `[data-theme="dark"]` block,**改 mode 設定一處全聯動**;若拆出去 `elevation/elevation.css` 各有 dark override block → 兩處 mode trigger,容易「改 a 壞 b」。
|
|
121
|
+
|
|
122
|
+
**對齊**:Material 3 `md-sys-elevation` 直接定義在 `theme` 範圍內(跟 color theme 同 root);Atlassian DS 「shadow + color in same theme primitive layer」;Polaris `--p-shadow-*` 與 `--p-color-*` 同住 theme css。
|
|
123
|
+
|
|
124
|
+
**拆分例外條件**:若未來 elevation 需要獨立 dark mode trigger(eg. high-contrast mode 不變色但加重 shadow)→ 才拆。目前無此需求。
|
|
125
|
+
|
|
126
|
+
### Co-location 2:`--canvas` / `--surface` 跟 `--bg-<state>` 是 **2 個 family**(不是 drift)
|
|
127
|
+
|
|
128
|
+
| Family | 命名前綴 | 用途 | Sourced from |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| **Surface tone family** | `--canvas` / `--surface` / `--surface-raised` / `--surface-strong` / `--overlay` / `--tooltip` | 容器層級的**底色 tone**——不同 layer 不同視覺深度 | shadcn idiom(`--background` family),DS 沿用避免破壞 shadcn compat 命名 |
|
|
131
|
+
| **State pair family** | `--bg-<state>` 配 `--fg-<state>` | 互動元件 disabled / hover / active **state 底色**——跟前景文字 state 成對 | DS 自家 idiom,確保 bg + fg 一定一對一 sync(`--bg-disabled` 配 `--fg-disabled`) |
|
|
132
|
+
|
|
133
|
+
**Why 2 個 family**:Surface tone 是「容器深度」概念,跟 state 無關;State pair 是「元件當下互動 state」概念,跟容器深度無關。**強行統一**(eg. 命名全部 `--bg-canvas` / `--bg-disabled`)→ 失去語義邊界,consumer 寫 code 時不知該選哪個。
|
|
134
|
+
|
|
135
|
+
**對齊**:Polaris `--p-color-bg`(tone family)vs `--p-color-bg-disabled`(state pair);Material 3 `md-sys-color-surface*`(tone)vs `md-sys-color-on-disabled`(state);Atlassian `color.background.neutral`(tone)vs `color.background.disabled`(state)— 三家世界級全部 2 family 拆,我們對齊。
|
|
136
|
+
|
|
137
|
+
### Co-location 3:`--layout-space-*`(layoutSpace/)vs `--sidebar-*` / `--chrome-header-*` / `--*-panel-width`(uiSize/)
|
|
138
|
+
|
|
139
|
+
**Layer 同為 L4 Layout**,但兩個 family 拆兩個檔案(`layoutSpace/` vs `uiSize/`):
|
|
140
|
+
|
|
141
|
+
| Family | 檔案 | 用途 |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `--layout-space-*` | `layoutSpace.css` | **抽象** spacing rhythm(loose / tight / bottom)——多 consumer 共用 |
|
|
144
|
+
| `--sidebar-*` / `--chrome-header-*` / `--data-table-*-width` | `uiSize.css` | **具體** layout primitive 尺寸——對應特定 layout primitive 元件 |
|
|
145
|
+
|
|
146
|
+
**Why 拆**:抽象 spacing rhythm 改值 → 整 app rhythm 動;具體 primitive 尺寸改值 → 單個 primitive 動。兩個影響半徑不同,拆檔案讓 grep / blame 直接看出。
|
|
147
|
+
|
|
148
|
+
### Co-location 4:`--opacity-disabled` 是 L1 Primitive 不是 L2 Semantic
|
|
149
|
+
|
|
150
|
+
雖然名含「disabled」看似 semantic,但實際是**單一 structural value**(0.45)沒 mode swap、沒 hover/active variant——當 L1 Primitive 處理。`@utility opacity-disabled` 直接 expose 給 consumer。
|
|
151
|
+
|
|
152
|
+
**對齊**:Atlassian Pragmatic guideline opacity scale 也住 primitive layer。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Internal vs Public token(consumer scope)
|
|
157
|
+
|
|
158
|
+
| Layer | Scope | Consumer who reads token? |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| **L1 Primitive** | Internal | Tag / Avatar(per color.spec.md 混合存取流派);其他禁直接 `--color-blue-6` |
|
|
161
|
+
| **L2 Semantic** | **Public** | 所有 consumer(app / explorations / patterns)直接 `bg-primary` / `text-foreground` |
|
|
162
|
+
| **L3 Family**(`--field-height-*` 等) | Internal | DS 內部 primitive(Button / Input / DataTable)消費;**consumer 必走 component prop**(`<Button size="sm">`)不直接 raw token |
|
|
163
|
+
| **L4 Layout**(`--sidebar-width` / `--layout-space-*`) | **Public** | Layout primitive 元件(AppShell / Sidebar / ChromeHeader)+ consumer 寫自家 layout 直接消費 |
|
|
164
|
+
| **L5 Internal** | Internal | 單 component 內部(目前 0 個) |
|
|
165
|
+
|
|
166
|
+
**Stories 義務**:
|
|
167
|
+
- Public token → **必補 Storybook stories**(色票 / 字級 / 間距對照展示)
|
|
168
|
+
- Internal token → spec.md codify family 即可,**免 stories**(consumer 不直接看)
|
|
169
|
+
|
|
170
|
+
`tokens/README.md`「Public vs Internal token」表 cross-reference 本段。
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 跨 family `@theme inline` bridge(Tailwind v4 spec)
|
|
175
|
+
|
|
176
|
+
Tailwind v4 `@theme inline` 把 CSS variable 升級成 utility class。每 family CSS 檔末段必有 `@theme inline` block 橋接 token → utility:
|
|
177
|
+
|
|
178
|
+
```css
|
|
179
|
+
@theme inline {
|
|
180
|
+
--spacing-field-md: var(--field-height-md); /* `h-field-md` utility */
|
|
181
|
+
--color-primary: var(--primary); /* `bg-primary` utility */
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**禁** 在 bridge 中改 token 值或重新命名(`--spacing-field-md = 32px` 直接 hardcode = drift);**必** 用 `var()` 引用既有 L1/L2 token。
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## SSOT auto-sync 義務(2026-05-23 user 永久 directive)
|
|
190
|
+
|
|
191
|
+
新增 / 改 / 廢 token 時:
|
|
192
|
+
1. **Spec.md** codify rationale(本 spec 上游 + family spec.md 下游)
|
|
193
|
+
2. **CSS file** 寫定義
|
|
194
|
+
3. **Tailwind bridge** `@theme inline` / `@utility` 補完
|
|
195
|
+
4. **Consumer 改寫**:若 rename / retire → grep DS-wide consumer 一次改完(M10 proactive scan)
|
|
196
|
+
5. **Hook / audit** 對齊(若新 family invariant → 補 hook;若新 dim → 補 audit skill)
|
|
197
|
+
6. **Stories**(if Public token)
|
|
198
|
+
|
|
199
|
+
**禁** 留「下個 session 補 stories」「之後再 grep consumer」(M33 retired but principle absorbed by M20 sub-rule)。
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 世界級對照 citation
|
|
204
|
+
|
|
205
|
+
| 概念 | 對應世界級 DS | source |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| 分層 token 架構 | Material 3 reference / system / component | <https://m3.material.io/foundations/design-tokens/overview> |
|
|
208
|
+
| Primitive + Semantic 2-layer | Polaris `--p-color-*` | <https://polaris.shopify.com/tokens/colors> |
|
|
209
|
+
| Semantic state token(hover / active / disabled) | Atlassian DS foundations | <https://atlassian.design/foundations/color-new> |
|
|
210
|
+
| Family scoped naming(`--<family>-<size>`) | Carbon `$button-primary-active` | <https://carbondesignsystem.com/elements/color/tokens/> |
|
|
211
|
+
| Adaptive token(light/dark co-location) | Apple HIG `UIColor.systemBlue` adaptive | <https://developer.apple.com/design/human-interface-guidelines/color> |
|
|
212
|
+
| Surface tone vs state pair 2 family | Polaris `--p-color-bg` vs `--p-color-bg-disabled` | <https://polaris.shopify.com/tokens/colors> |
|
|
213
|
+
| Elevation 跟 theme co-location | Material 3 `md-sys-elevation` in theme | <https://m3.material.io/styles/elevation/tokens> |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 反 pattern(禁)
|
|
218
|
+
|
|
219
|
+
| 禁 | 原因 |
|
|
220
|
+
|---|---|
|
|
221
|
+
| 自創新命名 family 不過 family-scoped 規則 | 1 個 token 屬 N 個 family = consumer 認知衝突 |
|
|
222
|
+
| 1 處 hardcode 立即開 token | <2 處同步 = token 過度抽象,Linear/Polaris 都警告 token bloat |
|
|
223
|
+
| 為 1 consumer 開 L3 Family token | Family 抽象服務 cross-component,1 consumer 該 hardcode in tsx |
|
|
224
|
+
| 拆 elevation 出 primitives.css 另做 dark override | 兩處 mode trigger,「改 a 壞 b」(M10 違反) |
|
|
225
|
+
| L2 Semantic 直接 raw oklch 不 alias L1 | 跳層 = 失去 primitive scale benefit + 改值要動兩處 |
|
|
226
|
+
| L4 Layout token 不 publish 到 npm tokens aggregator | Consumer install DS 拿不到 → 跑版(2026-05-26 AppShell 事件 root cause) |
|
|
227
|
+
| Internal token(`--field-height-*`)出現在 consumer code | 該走 component prop 不直接 raw token |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 被引用(auto-maintained,Dim 3 reciprocal audit)
|
|
232
|
+
|
|
233
|
+
> 本節由 `scripts/add-reciprocal-pointers.mjs` 自動維護。
|
|
234
|
+
|
|
235
|
+
- `tokens/README.md`(Public vs Internal token 表 cross-reference)
|
|
236
|
+
- `color/color.spec.md`(L1/L2 上游 SSOT)
|
|
237
|
+
- `uiSize/uiSize.spec.md`(L3 Family + L4 Layout 上游 SSOT)
|
|
238
|
+
- `layoutSpace/layoutSpace.spec.md`(L4 Layout 上游 SSOT)
|
|
239
|
+
- `elevation/elevation.spec.md`(L1 co-location rationale)
|
|
240
|
+
- `opacity/opacity.spec.md`(L1 classification)
|
|
241
|
+
- `radius/radius.spec.md`(L3 Family classification)
|
|
242
|
+
- `typography/typography.spec.md`(L3 Family classification)
|
|
243
|
+
- `.claude/rules/ui-development.md`「Token 命名 4 條硬規則」(下游 lint-style summary)
|
|
@@ -49,6 +49,19 @@
|
|
|
49
49
|
不寫成 calc() 公式因為 lg 無法完美遵守(54 → 56 為了 8px grid 取整),hardcode 比假公式誠實。 */
|
|
50
50
|
--chrome-header-height: 3rem; /* 48px */
|
|
51
51
|
|
|
52
|
+
/* Sidebar / Layout primitive sizing(2026-05-26 ship 進 published per consumer SSOT gap):
|
|
53
|
+
原本 declared in DS repo `src/globals.css`,沒進 npm-published tokens aggregator
|
|
54
|
+
→ consumer install DS + import `@qijenchen/design-system/styles/tokens` 拿不到
|
|
55
|
+
→ AppShell + Sidebar gap div 用 `var(--sidebar-width)` undefined → 解析 0px → flex 跑版
|
|
56
|
+
對齊 docs:`components/Sidebar/sidebar.spec.md` L613-614 token table
|
|
57
|
+
Why uiSize.css:layout primitive widths 是 ui sizing semantic 範疇(對齊 --field-height / --chrome-header)
|
|
58
|
+
*/
|
|
59
|
+
--sidebar-width: 15rem; /* 240px expanded sidebar */
|
|
60
|
+
--sidebar-width-min: 240px; /* 最小寬,同時是 AppShell Aside floor(ASIDE_WIDTH_MIN) */
|
|
61
|
+
--sidebar-width-mobile: 18rem; /* 288px mobile sheet 抽屜 */
|
|
62
|
+
--sidebar-menu-icon-size: 1rem; /* 16px sm/md row(per ICON_SIZE.sm/md);size=lg 罕見 case 下方 override 1.25rem(20px) */
|
|
63
|
+
--sidebar-width-icon: calc(2 * var(--layout-space-loose, 1rem) + var(--sidebar-menu-icon-size));
|
|
64
|
+
|
|
52
65
|
/* Tree Indent — TreeView + DataTable nested rows(SSOT,2026-05-04)
|
|
53
66
|
公式:chevronSize + gap-2 = 16+8(sm/md)/ 20+8(lg)
|
|
54
67
|
Consumer:TreeView depth indent / DataTable nested row depth indent */
|
|
@@ -67,6 +80,9 @@
|
|
|
67
80
|
|
|
68
81
|
[data-ui-size="lg"],
|
|
69
82
|
[data-density="lg"] {
|
|
83
|
+
/* Sidebar lg override(per src/globals.css 原 declaration:size=lg 罕見 case)*/
|
|
84
|
+
--sidebar-menu-icon-size: 1.25rem; /* 20px(lg)*/
|
|
85
|
+
|
|
70
86
|
--field-height-sm: 2rem; /* 32px */
|
|
71
87
|
--field-height-md: 2.25rem; /* 36px */
|
|
72
88
|
--field-height-lg: 2.5rem; /* 40px */
|