@exxatdesignux/ui 0.2.15 → 0.2.17
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/CHANGELOG.md +23 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +151 -3
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +17 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +3 -3
- package/src/components/ui/banner.tsx +2 -0
- package/src/components/ui/chart.tsx +57 -2
- package/src/components/ui/sidebar.tsx +1 -0
- package/src/globals.css +21 -2
- package/src/theme.css +4 -2
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +23 -18
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/question-bank/layout.tsx +27 -7
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/globals.css +136 -2
- package/template/app/layout.tsx +41 -5
- package/template/components/app-sidebar.tsx +141 -59
- package/template/components/ask-leo-sidebar.tsx +1 -4
- package/template/components/brand-color-picker.tsx +344 -0
- package/template/components/compliance-list-view.tsx +33 -51
- package/template/components/compliance-table.tsx +24 -0
- package/template/components/data-table/index.tsx +68 -24
- package/template/components/data-table/pagination.tsx +0 -1
- package/template/components/data-table/types.ts +4 -1
- package/template/components/data-table/use-table-state.ts +243 -94
- package/template/components/data-views/data-row-list.tsx +183 -0
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +26 -3
- package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/template/components/data-views/os-folder-glyph.tsx +8 -0
- package/template/components/data-views/outline-tree-menu.tsx +157 -0
- package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
- package/template/components/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +173 -379
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +116 -51
- package/template/components/new-placement-form.tsx +4 -2
- package/template/components/new-question-composer.tsx +2208 -0
- package/template/components/page-breadcrumb-trail.tsx +131 -0
- package/template/components/page-header.tsx +21 -11
- package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +1 -1
- package/template/components/placement-detail.tsx +1 -1
- package/template/components/placements-board-view.tsx +1 -1
- package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
- package/template/components/placements-list-view.tsx +18 -132
- package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
- package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
- package/template/components/placements-table-columns.tsx +2 -2
- package/template/components/{data-list-table.tsx → placements-table.tsx} +67 -58
- package/template/components/product-switcher.tsx +26 -11
- package/template/components/product-wordmark.tsx +285 -0
- package/template/components/question-bank-client.tsx +130 -70
- package/template/components/question-bank-hub-client.tsx +108 -115
- package/template/components/question-bank-list-view.tsx +30 -54
- package/template/components/question-bank-new-folder-sheet.tsx +1 -1
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -228
- package/template/components/question-bank-table.tsx +30 -5
- package/template/components/rotations-empty-state.tsx +3 -0
- package/template/components/secondary-panel.tsx +24 -4
- package/template/components/settings-appearance-card.tsx +584 -141
- package/template/components/site-header.tsx +56 -32
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +24 -0
- package/template/components/table-properties/drawer.tsx +1 -1
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +29 -3
- package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
- package/template/components/templates/list-page.tsx +1 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +11 -6
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +51 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/dev-log.test.ts +6 -5
- package/template/lib/exxat-palette.json +1462 -0
- package/template/lib/exxat-palette.ts +136 -0
- package/template/lib/list-page-table-properties.ts +1 -1
- package/template/lib/list-status-badges.ts +1 -1
- package/template/lib/mailto.ts +29 -0
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/placement-board-card-layout.ts +1 -1
- package/template/lib/product-brand.ts +268 -0
- package/template/lib/question-bank-authoring.ts +308 -0
- package/template/lib/question-bank-nav.ts +70 -0
- package/template/lib/raf-throttle.ts +45 -0
- package/template/lib/table-state-lifecycle.ts +474 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +6 -6
- package/template/stores/app-store.ts +46 -1
- package/template/components/command-menu-01.tsx +0 -133
- package/template/components/command-menu-02.tsx +0 -386
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DataRowList — generic vertical-stack list view used by every hub's "list"
|
|
5
|
+
* tab (placements, team, compliance, sites, question-bank, …). Replaces the
|
|
6
|
+
* hand-rolled `<ul …flex-col gap-2 px-4 pb-8 pt-2 lg:px-6> {rows.map(<li>…)}`
|
|
7
|
+
* shell that was duplicated across `*-list-view.tsx` files.
|
|
8
|
+
*
|
|
9
|
+
* Composition over inheritance: callers provide a `renderRow(row)` that
|
|
10
|
+
* returns whatever ListPageBoardCard / link / chip-stack they need — this
|
|
11
|
+
* component owns the chrome (spacing, empty state, virtualization), not the
|
|
12
|
+
* row body.
|
|
13
|
+
*
|
|
14
|
+
* Auto-virtualises with `@tanstack/react-virtual` when the row count meets
|
|
15
|
+
* `virtualizeThreshold` (default 100). Disable by passing `0`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as React from "react"
|
|
19
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
20
|
+
import { cn } from "@/lib/utils"
|
|
21
|
+
|
|
22
|
+
const DEFAULT_VIRTUALIZE_THRESHOLD = 100
|
|
23
|
+
const DEFAULT_ESTIMATED_ROW_HEIGHT = 96
|
|
24
|
+
const DEFAULT_OVERSCAN = 8
|
|
25
|
+
|
|
26
|
+
export interface DataRowListProps<TRow> {
|
|
27
|
+
/** The filtered/sorted rows from `tableState.rows` (or wherever). */
|
|
28
|
+
rows: readonly TRow[]
|
|
29
|
+
/** Stable id used as the React `key` and (for virtualizer) the v-key. */
|
|
30
|
+
getRowId: (row: TRow, index: number) => string | number
|
|
31
|
+
/** Render the body of one row. Wrap with `<ListPageBoardCard layout="row">` etc. */
|
|
32
|
+
renderRow: (row: TRow, index: number) => React.ReactNode
|
|
33
|
+
/**
|
|
34
|
+
* Shown when `rows.length === 0`. Strings render as muted body copy; pass
|
|
35
|
+
* a `ReactNode` for richer empty states (illustration, CTA, etc.).
|
|
36
|
+
*/
|
|
37
|
+
emptyState?: React.ReactNode
|
|
38
|
+
/**
|
|
39
|
+
* Auto-virtualise when `rows.length >= virtualizeThreshold`. Default 100.
|
|
40
|
+
* Pass `0` to never virtualise (preserves predictable layout for short
|
|
41
|
+
* lists like dashboards / pinned tabs).
|
|
42
|
+
*/
|
|
43
|
+
virtualizeThreshold?: number
|
|
44
|
+
/** Hint for the virtualizer; clamps to measured size after first paint. */
|
|
45
|
+
estimatedRowHeight?: number
|
|
46
|
+
/** Override the default container padding / gap if needed. */
|
|
47
|
+
className?: string
|
|
48
|
+
/** Override the per-row `<li>` className (e.g. tighter spacing). */
|
|
49
|
+
rowClassName?: string
|
|
50
|
+
/** `aria-label` for the `<ul>` (screen-reader name for the list). */
|
|
51
|
+
ariaLabel?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEFAULT_OUTER_CLASS = "flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6"
|
|
55
|
+
|
|
56
|
+
export function DataRowList<TRow>(props: DataRowListProps<TRow>) {
|
|
57
|
+
const {
|
|
58
|
+
rows,
|
|
59
|
+
getRowId,
|
|
60
|
+
renderRow,
|
|
61
|
+
emptyState,
|
|
62
|
+
virtualizeThreshold = DEFAULT_VIRTUALIZE_THRESHOLD,
|
|
63
|
+
estimatedRowHeight = DEFAULT_ESTIMATED_ROW_HEIGHT,
|
|
64
|
+
className,
|
|
65
|
+
rowClassName,
|
|
66
|
+
ariaLabel,
|
|
67
|
+
} = props
|
|
68
|
+
|
|
69
|
+
if (rows.length === 0) {
|
|
70
|
+
if (emptyState == null) return null
|
|
71
|
+
if (typeof emptyState === "string") {
|
|
72
|
+
return (
|
|
73
|
+
<div className="px-4 py-16 text-center lg:px-6">
|
|
74
|
+
<p className="text-sm text-muted-foreground">{emptyState}</p>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return <div className="px-4 py-16 text-center lg:px-6">{emptyState}</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (virtualizeThreshold > 0 && rows.length >= virtualizeThreshold) {
|
|
82
|
+
return (
|
|
83
|
+
<DataRowListVirtualized
|
|
84
|
+
rows={rows}
|
|
85
|
+
getRowId={getRowId}
|
|
86
|
+
renderRow={renderRow}
|
|
87
|
+
estimatedRowHeight={estimatedRowHeight}
|
|
88
|
+
className={className}
|
|
89
|
+
rowClassName={rowClassName}
|
|
90
|
+
ariaLabel={ariaLabel}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ul aria-label={ariaLabel} className={cn(DEFAULT_OUTER_CLASS, className)}>
|
|
97
|
+
{rows.map((row, i) => (
|
|
98
|
+
<li key={getRowId(row, i)} className={rowClassName}>
|
|
99
|
+
{renderRow(row, i)}
|
|
100
|
+
</li>
|
|
101
|
+
))}
|
|
102
|
+
</ul>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Virtualised variant — keeps the DOM short on long lists (e.g. 1000+ rows).
|
|
108
|
+
// Uses `useWindowVirtualizer` so the page scroll drives row recycling; this
|
|
109
|
+
// is the right tool for hub-level lists (not nested-scroll containers).
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function DataRowListVirtualized<TRow>({
|
|
113
|
+
rows,
|
|
114
|
+
getRowId,
|
|
115
|
+
renderRow,
|
|
116
|
+
estimatedRowHeight,
|
|
117
|
+
className,
|
|
118
|
+
rowClassName,
|
|
119
|
+
ariaLabel,
|
|
120
|
+
}: {
|
|
121
|
+
rows: readonly TRow[]
|
|
122
|
+
getRowId: (row: TRow, index: number) => string | number
|
|
123
|
+
renderRow: (row: TRow, index: number) => React.ReactNode
|
|
124
|
+
estimatedRowHeight: number
|
|
125
|
+
className?: string
|
|
126
|
+
rowClassName?: string
|
|
127
|
+
ariaLabel?: string
|
|
128
|
+
}) {
|
|
129
|
+
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
|
130
|
+
// `scrollMargin` is read by the virtualizer during render, so it has to
|
|
131
|
+
// be state (not a ref). We measure with `useLayoutEffect` after the first
|
|
132
|
+
// paint and on resize so window-scroll math stays accurate when the page
|
|
133
|
+
// layout shifts (sidebar collapse, banner, etc.).
|
|
134
|
+
const [scrollMargin, setScrollMargin] = React.useState(0)
|
|
135
|
+
|
|
136
|
+
const updateScrollMargin = React.useCallback(() => {
|
|
137
|
+
const el = anchorRef.current
|
|
138
|
+
if (!el) return
|
|
139
|
+
setScrollMargin(el.getBoundingClientRect().top + window.scrollY)
|
|
140
|
+
}, [])
|
|
141
|
+
|
|
142
|
+
React.useLayoutEffect(() => {
|
|
143
|
+
updateScrollMargin()
|
|
144
|
+
window.addEventListener("resize", updateScrollMargin)
|
|
145
|
+
return () => window.removeEventListener("resize", updateScrollMargin)
|
|
146
|
+
}, [updateScrollMargin, rows.length])
|
|
147
|
+
|
|
148
|
+
const virtualizer = useWindowVirtualizer({
|
|
149
|
+
count: rows.length,
|
|
150
|
+
estimateSize: () => estimatedRowHeight,
|
|
151
|
+
overscan: DEFAULT_OVERSCAN,
|
|
152
|
+
scrollMargin,
|
|
153
|
+
getItemKey: i => String(getRowId(rows[i], i)),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const totalSize = virtualizer.getTotalSize()
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div ref={anchorRef} className={cn("px-4 pb-8 pt-2 lg:px-6", className)}>
|
|
160
|
+
<ul
|
|
161
|
+
aria-label={ariaLabel}
|
|
162
|
+
className="relative m-0 w-full list-none p-0"
|
|
163
|
+
style={{ height: `${totalSize}px` }}
|
|
164
|
+
>
|
|
165
|
+
{virtualizer.getVirtualItems().map(vr => {
|
|
166
|
+
const row = rows[vr.index]
|
|
167
|
+
if (!row) return null
|
|
168
|
+
return (
|
|
169
|
+
<li
|
|
170
|
+
key={vr.key}
|
|
171
|
+
data-index={vr.index}
|
|
172
|
+
ref={virtualizer.measureElement}
|
|
173
|
+
className={cn("absolute left-0 top-0 w-full pb-2", rowClassName)}
|
|
174
|
+
style={{ transform: `translateY(${vr.start}px)` }}
|
|
175
|
+
>
|
|
176
|
+
{renderRow(row, vr.index)}
|
|
177
|
+
</li>
|
|
178
|
+
)
|
|
179
|
+
})}
|
|
180
|
+
</ul>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* FinderPanelView — Miller-style 3-column split for list-page hubs.
|
|
5
5
|
*
|
|
6
|
-
* Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `
|
|
6
|
+
* Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS`,
|
|
7
7
|
* shared resizable handles) — see `list-page-split-hub-tokens.ts`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -142,7 +142,7 @@ export function FinderGroupStrip({
|
|
|
142
142
|
<div
|
|
143
143
|
role="toolbar"
|
|
144
144
|
aria-label={ariaLabel}
|
|
145
|
-
className="flex min-h-10 flex-wrap items-center gap-1.5 border-b border-border bg-
|
|
145
|
+
className="flex min-h-10 flex-wrap items-center gap-1.5 border-b border-border bg-card px-2 py-2"
|
|
146
146
|
>
|
|
147
147
|
{groups.map(group => {
|
|
148
148
|
const isSelected = group.id === selectedGroupId
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central exports for list-page data surfaces and shared view chrome.
|
|
3
3
|
*
|
|
4
|
-
* **Pattern:** `ListPageTemplate` + `
|
|
4
|
+
* **Pattern:** `ListPageTemplate` + `PlacementsTable` (or any hub-specific `*-table.tsx`) — one `useTableState`, one toolbar,
|
|
5
5
|
* table | list | board | dashboard from the same component (`AGENTS.md` §4, `docs/data-views-pattern.md`).
|
|
6
6
|
*
|
|
7
7
|
* **View UI:** `ViewSegmentedControl` matches the template’s views toolbar (`bg-muted/60` pills).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
10
|
+
export { PlacementsTable } from "@/components/placements-table"
|
|
11
|
+
export type { PlacementsTableProps, PlacementsTableHandle } from "@/components/placements-table"
|
|
12
12
|
export type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
|
|
13
13
|
export type { DataListViewType } from "@/lib/data-list-view"
|
|
14
14
|
export { DATA_LIST_VIEW_TILES, dataListViewIcon, dataListViewLabel } from "@/lib/data-list-view"
|
|
@@ -47,6 +47,25 @@ export {
|
|
|
47
47
|
type ListPageTreeColumnHeaderProps,
|
|
48
48
|
} from "@/components/data-views/list-page-tree-column-header"
|
|
49
49
|
|
|
50
|
+
/** VS Code–style outline tree chrome — mirrors shadcn `SidebarMenuSub` (see module doc). */
|
|
51
|
+
export {
|
|
52
|
+
OutlineTreeCollapsibleContentRail,
|
|
53
|
+
OutlineTreeLeafButton,
|
|
54
|
+
OutlineTreeMenu,
|
|
55
|
+
OutlineTreeMenuItem,
|
|
56
|
+
OutlineTreeSub,
|
|
57
|
+
OutlineTreeSubItem,
|
|
58
|
+
OUTLINE_TREE_COLLAPSIBLE_CONTENT_RAIL_CLASS,
|
|
59
|
+
OUTLINE_TREE_CHEVRON_GUIDE_SPACER_CLASS,
|
|
60
|
+
OUTLINE_TREE_SUB_ROW_SHIFT_CLASS,
|
|
61
|
+
type OutlineTreeGuideLayout,
|
|
62
|
+
type OutlineTreeLeafButtonProps,
|
|
63
|
+
type OutlineTreeSurface,
|
|
64
|
+
} from "@/components/data-views/outline-tree-menu"
|
|
65
|
+
|
|
66
|
+
export { QuestionBankFolderTreeBranch } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
67
|
+
export type { QuestionBankFolderTreeBranchProps } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
68
|
+
|
|
50
69
|
export {
|
|
51
70
|
LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
|
|
52
71
|
LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS,
|
|
@@ -84,6 +103,10 @@ export {
|
|
|
84
103
|
/** Generic folder icon-grid — reusable across all list hubs. */
|
|
85
104
|
export { FolderGridView, type FolderGridViewProps } from "@/components/data-views/folder-grid-view"
|
|
86
105
|
|
|
106
|
+
/** Generic vertical row list — used by every hub's "list" tab. Composes
|
|
107
|
+
* `ListPageBoardCard layout="row"` via a `renderRow` prop. */
|
|
108
|
+
export { DataRowList, type DataRowListProps } from "@/components/data-views/data-row-list"
|
|
109
|
+
|
|
87
110
|
|
|
88
111
|
/** Unified hub tile + list row surface — see `list-page-board-card.tsx`. */
|
|
89
112
|
export {
|
|
@@ -10,7 +10,7 @@ export interface ListPageSplitDetailsPlaceholderProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Empty right pane for split hubs —
|
|
13
|
+
* Empty right pane for split hubs — flat `bg-card` to match Miller / tree columns.
|
|
14
14
|
*/
|
|
15
15
|
export function ListPageSplitDetailsPlaceholder({
|
|
16
16
|
title = "Nothing selected",
|
|
@@ -20,11 +20,11 @@ export function ListPageSplitDetailsPlaceholder({
|
|
|
20
20
|
return (
|
|
21
21
|
<div
|
|
22
22
|
className={cn(
|
|
23
|
-
"flex h-full min-h-0 flex-col items-center justify-center bg-
|
|
23
|
+
"flex h-full min-h-0 flex-col items-center justify-center bg-card px-6 py-10 text-center",
|
|
24
24
|
className,
|
|
25
25
|
)}
|
|
26
26
|
>
|
|
27
|
-
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-
|
|
27
|
+
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-card">
|
|
28
28
|
<i
|
|
29
29
|
className="fa-light fa-sidebar text-[1.65rem] leading-none text-muted-foreground/70"
|
|
30
30
|
aria-hidden="true"
|
|
@@ -9,7 +9,7 @@ export const LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS =
|
|
|
9
9
|
|
|
10
10
|
/** Primary column stack (scope list, folder list, record list, …). */
|
|
11
11
|
export const LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS =
|
|
12
|
-
"flex min-h-0 min-w-0 flex-col bg-
|
|
12
|
+
"flex min-h-0 min-w-0 flex-col bg-card"
|
|
13
13
|
|
|
14
14
|
/** Right-hand inspector / detail column shell. */
|
|
15
15
|
export const LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS =
|
|
@@ -19,7 +19,7 @@ export function ListPageTreeColumnHeader({
|
|
|
19
19
|
className,
|
|
20
20
|
}: ListPageTreeColumnHeaderProps) {
|
|
21
21
|
return (
|
|
22
|
-
<div className={cn("shrink-0 border-b border-border/50 bg-
|
|
22
|
+
<div className={cn("shrink-0 border-b border-border/50 bg-card px-3 py-2", className)}>
|
|
23
23
|
<div className="flex h-9 items-center justify-between gap-2">
|
|
24
24
|
<h3 className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">{title}</h3>
|
|
25
25
|
{trailing ? (
|
|
@@ -91,12 +91,20 @@ export function OsFolderGlyph({
|
|
|
91
91
|
aria-label={!decorative ? label : undefined}
|
|
92
92
|
aria-hidden={decorative ? true : undefined}
|
|
93
93
|
>
|
|
94
|
+
{/* Static SVG — `next/image` can't optimize SVGs without
|
|
95
|
+
`dangerouslyAllowSVG`, so we stay on plain <img> but add the same
|
|
96
|
+
loading/decoding hints `next/image` would. Folder grids render many
|
|
97
|
+
of these at once; lazy-loading lets the browser skip off-screen
|
|
98
|
+
glyphs until they scroll near the viewport. */}
|
|
99
|
+
{/* eslint-disable-next-line @next/next/no-img-element -- SVG; next/image can't optimize without dangerouslyAllowSVG */}
|
|
94
100
|
<img
|
|
95
101
|
src={OS_FOLDER_GLYPH_SRC}
|
|
96
102
|
alt=""
|
|
97
103
|
width={240}
|
|
98
104
|
height={240}
|
|
99
105
|
draggable={false}
|
|
106
|
+
loading="lazy"
|
|
107
|
+
decoding="async"
|
|
100
108
|
className={cn(
|
|
101
109
|
"h-full w-full object-contain",
|
|
102
110
|
"transition-[filter] duration-200",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Central outline-tree chrome — mirrors shadcn/ui **Sidebar** file-tree structure
|
|
5
|
+
* (`SidebarMenu` → `SidebarMenuItem` + `Collapsible` → `SidebarMenuSub` → rows) without
|
|
6
|
+
* coupling to `useSidebar`.
|
|
7
|
+
*
|
|
8
|
+
* - **`guideLayout="inset"`** — same rhythm as `SidebarMenuSub` (`mx-3.5` + `translate-x-px`).
|
|
9
|
+
* - **`guideLayout="chevronRail"`** — use with **`OutlineTreeCollapsibleContentRail`**: a **`w-6`**
|
|
10
|
+
* spacer lines up the vertical guide with the **horizontal center** of a **`size-8`** chevron
|
|
11
|
+
* when the folder row uses **`px-2`** (8px padding + 16px half of 32px chevron hit target).
|
|
12
|
+
*
|
|
13
|
+
* @see packages/ui/src/components/ui/sidebar.tsx — `SidebarMenuSub`, `SidebarMenuSubItem`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as React from "react"
|
|
17
|
+
import { CollapsibleContent } from "@/components/ui/collapsible"
|
|
18
|
+
import { cn } from "@/lib/utils"
|
|
19
|
+
|
|
20
|
+
export type OutlineTreeSurface = "sidebar" | "panel"
|
|
21
|
+
|
|
22
|
+
export type OutlineTreeGuideLayout = "inset" | "chevronRail"
|
|
23
|
+
|
|
24
|
+
const outlineTreeSubInsetClass: Record<OutlineTreeSurface, string> = {
|
|
25
|
+
sidebar:
|
|
26
|
+
"mx-3.5 flex min-w-0 list-none translate-x-px flex-col gap-1 border-s border-sidebar-border px-2.5 py-0.5 rtl:-translate-x-px",
|
|
27
|
+
panel:
|
|
28
|
+
"mx-3.5 flex min-w-0 list-none translate-x-px flex-col gap-1 border-s border-border/60 px-2.5 py-0.5 rtl:-translate-x-px",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const outlineTreeSubChevronRailClass: Record<OutlineTreeSurface, string> = {
|
|
32
|
+
sidebar:
|
|
33
|
+
"flex min-w-0 flex-1 list-none flex-col gap-1 border-s border-sidebar-border py-0.5 ps-2.5",
|
|
34
|
+
panel: "flex min-w-0 flex-1 list-none flex-col gap-1 border-s border-border/60 py-0.5 ps-2.5",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Pull row content onto the guide line — matches `SidebarMenuSubButton` horizontal nudge (inset layout only). */
|
|
38
|
+
export const OUTLINE_TREE_SUB_ROW_SHIFT_CLASS = "-translate-x-px rtl:translate-x-px"
|
|
39
|
+
|
|
40
|
+
/** `CollapsibleContent` row: spacer width = `px-2` (8px) + half of `size-8` chevron (16px) → guide under chevron center. */
|
|
41
|
+
export const OUTLINE_TREE_COLLAPSIBLE_CONTENT_RAIL_CLASS = "flex min-w-0 w-full"
|
|
42
|
+
|
|
43
|
+
/** Spacer column — keep in sync with folder row `px-2` + `size-8` chevron. */
|
|
44
|
+
export const OUTLINE_TREE_CHEVRON_GUIDE_SPACER_CLASS = "w-6 shrink-0"
|
|
45
|
+
|
|
46
|
+
/** Wrap `OutlineTreeSub` with `guideLayout="chevronRail"` so the vertical border meets the chevron center. */
|
|
47
|
+
export function OutlineTreeCollapsibleContentRail({
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: React.ComponentProps<typeof CollapsibleContent>) {
|
|
52
|
+
return (
|
|
53
|
+
<CollapsibleContent
|
|
54
|
+
className={cn(OUTLINE_TREE_COLLAPSIBLE_CONTENT_RAIL_CLASS, className)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
<div className={OUTLINE_TREE_CHEVRON_GUIDE_SPACER_CLASS} aria-hidden />
|
|
58
|
+
{children}
|
|
59
|
+
</CollapsibleContent>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Nested list under a folder — vertical guide + indent. */
|
|
64
|
+
export function OutlineTreeSub({
|
|
65
|
+
surface = "panel",
|
|
66
|
+
guideLayout = "inset",
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
}: React.ComponentProps<"ul"> & {
|
|
70
|
+
surface?: OutlineTreeSurface
|
|
71
|
+
guideLayout?: OutlineTreeGuideLayout
|
|
72
|
+
}) {
|
|
73
|
+
return (
|
|
74
|
+
<ul
|
|
75
|
+
data-slot="outline-tree-sub"
|
|
76
|
+
data-guide-layout={guideLayout}
|
|
77
|
+
className={cn(
|
|
78
|
+
guideLayout === "inset" && outlineTreeSubInsetClass[surface],
|
|
79
|
+
guideLayout === "chevronRail" && outlineTreeSubChevronRailClass[surface],
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Root or nested branch list — matches `SidebarMenu` spacing. */
|
|
88
|
+
export function OutlineTreeMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|
89
|
+
return (
|
|
90
|
+
<ul
|
|
91
|
+
data-slot="outline-tree-menu"
|
|
92
|
+
className={cn("flex w-full min-w-0 list-none flex-col gap-0", className)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Expandable folder row wrapper — matches `SidebarMenuItem`. */
|
|
99
|
+
export function OutlineTreeMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
100
|
+
return (
|
|
101
|
+
<li
|
|
102
|
+
data-slot="outline-tree-menu-item"
|
|
103
|
+
className={cn("group/menu-item relative min-w-0 w-full list-none", className)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Leaf / nested row inside `OutlineTreeSub` — matches `SidebarMenuSubItem`. */
|
|
110
|
+
export function OutlineTreeSubItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
111
|
+
return (
|
|
112
|
+
<li
|
|
113
|
+
data-slot="outline-tree-sub-item"
|
|
114
|
+
className={cn("group/menu-sub-item relative min-w-0 w-full list-none", className)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface OutlineTreeLeafButtonProps extends React.ComponentProps<"button"> {
|
|
121
|
+
surface?: OutlineTreeSurface
|
|
122
|
+
isActive?: boolean
|
|
123
|
+
/** Inset `OutlineTreeSub` only — nudge like `SidebarMenuSubButton`. Ignored when parent uses `chevronRail`. */
|
|
124
|
+
subGuideAlign?: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Selectable leaf row (file / terminal row) — `SidebarMenuSubButton`–aligned rhythm. */
|
|
128
|
+
export function OutlineTreeLeafButton({
|
|
129
|
+
surface = "panel",
|
|
130
|
+
isActive = false,
|
|
131
|
+
subGuideAlign = false,
|
|
132
|
+
className,
|
|
133
|
+
...props
|
|
134
|
+
}: OutlineTreeLeafButtonProps) {
|
|
135
|
+
return (
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
data-active={isActive || undefined}
|
|
139
|
+
className={cn(
|
|
140
|
+
"flex min-h-8 w-full min-w-0 cursor-pointer select-none items-center gap-2 overflow-hidden rounded-md px-2 text-start text-sm outline-none ring-ring focus-visible:ring-2 focus-visible:ring-inset [&>svg]:size-4 [&>svg]:shrink-0",
|
|
141
|
+
subGuideAlign && OUTLINE_TREE_SUB_ROW_SHIFT_CLASS,
|
|
142
|
+
surface === "panel" &&
|
|
143
|
+
cn(
|
|
144
|
+
"text-foreground hover:bg-muted/50",
|
|
145
|
+
isActive && "bg-accent font-medium text-accent-foreground",
|
|
146
|
+
),
|
|
147
|
+
surface === "sidebar" &&
|
|
148
|
+
cn(
|
|
149
|
+
"text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
150
|
+
isActive && "bg-sidebar-accent font-medium text-sidebar-accent-foreground",
|
|
151
|
+
),
|
|
152
|
+
className,
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|