torch-glare 2.1.0 → 2.1.2
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/apps/lib/components/Badge.tsx +34 -137
- package/apps/lib/components/BadgeField.tsx +4 -4
- package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
- package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
- package/apps/lib/components/DataViews/InboxView.tsx +514 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
- package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
- package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
- package/apps/lib/components/DataViews/TableView.tsx +232 -0
- package/apps/lib/components/DataViews/TreeView.tsx +363 -0
- package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
- package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
- package/apps/lib/components/DataViews/index.ts +30 -0
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
- package/apps/lib/components/DataViews/types.ts +177 -0
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
- package/apps/lib/components/TreeFolder/icons.tsx +63 -0
- package/apps/lib/components/TreeFolder/index.ts +17 -0
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
- package/apps/lib/components/TreeFolder/types.ts +68 -0
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
- package/apps/lib/hooks/useDataViewsState.ts +169 -0
- package/apps/lib/hooks/useIsMobile.ts +21 -0
- package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
- package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
- package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
- package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
- package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
- package/dist/bin/index.js +3 -3
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/add.d.ts.map +1 -1
- package/dist/src/commands/add.js +29 -6
- package/dist/src/commands/add.js.map +1 -1
- package/dist/src/commands/utils.d.ts.map +1 -1
- package/dist/src/commands/utils.js +22 -2
- package/dist/src/commands/utils.js.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.js +8 -1
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
- package/docs/components/badge-field.md +21 -21
- package/docs/components/badge.md +156 -483
- package/docs/components/form-stepper.md +244 -0
- package/docs/components/stepper.md +215 -0
- package/docs/components/timeline.md +248 -0
- package/docs/reference/components.md +8 -7
- package/docs/reference/types.md +34 -26
- package/docs/tutorials/theming-basics.md +30 -27
- package/package.json +1 -1
- /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
- /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
X,
|
|
6
|
+
Settings as SettingsIcon,
|
|
7
|
+
Filter as FilterIcon,
|
|
8
|
+
Plus,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import type {
|
|
11
|
+
ViewConfig,
|
|
12
|
+
ViewType,
|
|
13
|
+
FieldConfig,
|
|
14
|
+
DynamicRecord,
|
|
15
|
+
DynamicFilterConfig,
|
|
16
|
+
FilterState,
|
|
17
|
+
FilterValue,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
import { RadioGroup } from "../Radio";
|
|
21
|
+
import { FilterPanel } from "./FilterPanel";
|
|
22
|
+
import { RadioRow, DataViewsSwitch } from "./PanelControls";
|
|
23
|
+
import { cn } from "../../utils/cn";
|
|
24
|
+
|
|
25
|
+
type ConfigTab = "config" | "filters";
|
|
26
|
+
|
|
27
|
+
type SavedView = { id: string; label: string };
|
|
28
|
+
|
|
29
|
+
export type DataViewsConfigPanelProps = {
|
|
30
|
+
config: ViewConfig;
|
|
31
|
+
onConfigChange: (config: Partial<ViewConfig>) => void;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
currentView: ViewType;
|
|
34
|
+
fields: FieldConfig[];
|
|
35
|
+
|
|
36
|
+
// Filters tab
|
|
37
|
+
data: DynamicRecord[];
|
|
38
|
+
filterState: FilterState;
|
|
39
|
+
onFilterChange: (filters: FilterState) => void;
|
|
40
|
+
filterConfig?: DynamicFilterConfig[];
|
|
41
|
+
|
|
42
|
+
// Saved views (presentational shell — wire to persistence when available)
|
|
43
|
+
savedViews?: SavedView[];
|
|
44
|
+
activeSavedView?: string;
|
|
45
|
+
onSavedViewChange?: (id: string) => void;
|
|
46
|
+
onSaveNewView?: () => void;
|
|
47
|
+
|
|
48
|
+
// Animation: drives slide-in/out. Parent keeps the panel mounted through
|
|
49
|
+
// the close animation, then unmounts.
|
|
50
|
+
state?: "open" | "closed";
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DEFAULT_SAVED_VIEWS: SavedView[] = [
|
|
54
|
+
{ id: "default", label: "Default View" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function SectionHeader({
|
|
58
|
+
title,
|
|
59
|
+
action,
|
|
60
|
+
}: {
|
|
61
|
+
title: string;
|
|
62
|
+
action?: React.ReactNode;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex items-center justify-between">
|
|
66
|
+
<h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
|
|
67
|
+
{title}
|
|
68
|
+
</h3>
|
|
69
|
+
{action}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 2×3 dot drag handle, matching the Figma SB-Column-Item grip (16×16 box,
|
|
75
|
+
* compact ~1.5px dots, tight spacing — drawn as an SVG for pixel accuracy). */
|
|
76
|
+
function GripDots() {
|
|
77
|
+
return (
|
|
78
|
+
<svg
|
|
79
|
+
aria-hidden
|
|
80
|
+
width="16"
|
|
81
|
+
height="16"
|
|
82
|
+
viewBox="0 0 16 16"
|
|
83
|
+
// Panel is always-dark chrome (like the hardcoded white label text):
|
|
84
|
+
// the grip stays white-on-dark regardless of host theme.
|
|
85
|
+
className="text-white/60"
|
|
86
|
+
fill="currentColor"
|
|
87
|
+
>
|
|
88
|
+
{[5.33, 9.33].flatMap((cx) =>
|
|
89
|
+
[3.33, 8, 12.67].map((cy) => (
|
|
90
|
+
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="1" />
|
|
91
|
+
)),
|
|
92
|
+
)}
|
|
93
|
+
</svg>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 2px blue insertion line shown between rows during a drag-reorder. */
|
|
98
|
+
function DropLine() {
|
|
99
|
+
return (
|
|
100
|
+
<div className="pointer-events-none relative h-0">
|
|
101
|
+
<div className="absolute -top-[1px] left-0 right-0 h-[2px] rounded-full bg-[#005ECC]" />
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
107
|
+
const {
|
|
108
|
+
config,
|
|
109
|
+
onConfigChange,
|
|
110
|
+
onClose,
|
|
111
|
+
fields,
|
|
112
|
+
data,
|
|
113
|
+
filterState,
|
|
114
|
+
onFilterChange,
|
|
115
|
+
filterConfig,
|
|
116
|
+
savedViews = DEFAULT_SAVED_VIEWS,
|
|
117
|
+
activeSavedView,
|
|
118
|
+
onSavedViewChange,
|
|
119
|
+
onSaveNewView,
|
|
120
|
+
state = "open",
|
|
121
|
+
} = props;
|
|
122
|
+
|
|
123
|
+
const [tab, setTab] = useState<ConfigTab>("config");
|
|
124
|
+
|
|
125
|
+
// Saved View is controlled by the parent when `activeSavedView` /
|
|
126
|
+
// `onSavedViewChange` are supplied; otherwise fall back to local state so the
|
|
127
|
+
// radios are still interactive (DataViewsLayout doesn't thread these props
|
|
128
|
+
// yet — without this the selection would snap back every click).
|
|
129
|
+
const [internalSavedView, setInternalSavedView] = useState(
|
|
130
|
+
() => savedViews[0]?.id,
|
|
131
|
+
);
|
|
132
|
+
const selectedSavedView = activeSavedView ?? internalSavedView;
|
|
133
|
+
const handleSavedViewChange = (id: string) => {
|
|
134
|
+
if (onSavedViewChange) onSavedViewChange(id);
|
|
135
|
+
else setInternalSavedView(id);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const visibleFields = useMemo(
|
|
139
|
+
() => fields.filter((f) => f.type !== "hidden"),
|
|
140
|
+
[fields],
|
|
141
|
+
);
|
|
142
|
+
const visiblePaths = useMemo(
|
|
143
|
+
() => new Set(visibleFields.map((f) => f.path)),
|
|
144
|
+
[visibleFields],
|
|
145
|
+
);
|
|
146
|
+
const fieldByPath = useMemo(
|
|
147
|
+
() => new Map(visibleFields.map((f) => [f.path, f])),
|
|
148
|
+
[visibleFields],
|
|
149
|
+
);
|
|
150
|
+
const orderedColumns = useMemo(
|
|
151
|
+
() =>
|
|
152
|
+
[...config.tableColumns]
|
|
153
|
+
.filter((c) => visiblePaths.has(c.id))
|
|
154
|
+
.sort((a, b) => a.order - b.order),
|
|
155
|
+
[config.tableColumns, visiblePaths],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const toggleColumnVisibility = (path: string) => {
|
|
159
|
+
const next = config.tableColumns.map((c) =>
|
|
160
|
+
c.id === path ? { ...c, visible: !c.visible } : c,
|
|
161
|
+
);
|
|
162
|
+
onConfigChange({ tableColumns: next });
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const [dragPath, setDragPath] = useState<string | null>(null);
|
|
166
|
+
const [dragOverPath, setDragOverPath] = useState<string | null>(null);
|
|
167
|
+
// Whether the drop will land before (true) or after (false) dragOverPath.
|
|
168
|
+
const [dropBefore, setDropBefore] = useState(true);
|
|
169
|
+
|
|
170
|
+
const reorderColumn = (
|
|
171
|
+
sourcePath: string,
|
|
172
|
+
targetPath: string,
|
|
173
|
+
before: boolean,
|
|
174
|
+
) => {
|
|
175
|
+
if (sourcePath === targetPath) return;
|
|
176
|
+
const ids = orderedColumns.map((c) => c.id);
|
|
177
|
+
const from = ids.indexOf(sourcePath);
|
|
178
|
+
let to = ids.indexOf(targetPath);
|
|
179
|
+
if (from === -1 || to === -1) return;
|
|
180
|
+
const reordered = [...ids];
|
|
181
|
+
reordered.splice(from, 1);
|
|
182
|
+
// Recompute the target index after removal, then offset for before/after.
|
|
183
|
+
to = reordered.indexOf(targetPath);
|
|
184
|
+
const insertAt = before ? to : to + 1;
|
|
185
|
+
reordered.splice(insertAt, 0, sourcePath);
|
|
186
|
+
const orderByPath = new Map(reordered.map((id, i) => [id, i]));
|
|
187
|
+
const next = config.tableColumns.map((c) => {
|
|
188
|
+
const newOrder = orderByPath.get(c.id);
|
|
189
|
+
return newOrder == null ? c : { ...c, order: newOrder };
|
|
190
|
+
});
|
|
191
|
+
onConfigChange({ tableColumns: next });
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const sortableColumns = orderedColumns;
|
|
195
|
+
|
|
196
|
+
const tabBtn = (id: ConfigTab, icon: React.ReactNode, label: string) => {
|
|
197
|
+
const active = tab === id;
|
|
198
|
+
return (
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
aria-pressed={active}
|
|
202
|
+
onClick={() => setTab(id)}
|
|
203
|
+
className={cn(
|
|
204
|
+
"flex h-6 flex-1 items-center justify-center gap-1 rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
|
|
205
|
+
active
|
|
206
|
+
? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
|
|
207
|
+
: "bg-transparent text-white hover:bg-white/5",
|
|
208
|
+
)}
|
|
209
|
+
>
|
|
210
|
+
<span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
|
|
211
|
+
{icon}
|
|
212
|
+
</span>
|
|
213
|
+
{label}
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div
|
|
220
|
+
data-state={state}
|
|
221
|
+
// Panel is always dark (Figma `Cun` = #000000). data-theme="dark" makes
|
|
222
|
+
// child themed components (Button, Switch, Radio, FilterPanel) resolve
|
|
223
|
+
// dark tokens even when the host app runs in default/light theme.
|
|
224
|
+
data-theme="dark"
|
|
225
|
+
className={cn(
|
|
226
|
+
"flex h-full w-[260px] flex-col overflow-hidden rounded-[16px] bg-black",
|
|
227
|
+
"transition-opacity duration-200 ease-in-out",
|
|
228
|
+
state === "open" ? "opacity-100" : "opacity-0",
|
|
229
|
+
)}
|
|
230
|
+
>
|
|
231
|
+
{/* Header: tab switcher + close */}
|
|
232
|
+
<div className="flex items-center gap-2 px-3 py-3">
|
|
233
|
+
<div className="flex flex-1 items-center gap-[2px] rounded-[10px] bg-[#252729] p-[2px] shadow-[inset_0_0_5px_0_rgba(0,0,0,0.16)]">
|
|
234
|
+
{tabBtn("config", <SettingsIcon />, "Config.")}
|
|
235
|
+
{tabBtn("filters", <FilterIcon />, "Filters")}
|
|
236
|
+
</div>
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={onClose}
|
|
240
|
+
aria-label="Close panel"
|
|
241
|
+
className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-white/25"
|
|
242
|
+
>
|
|
243
|
+
<X className="h-[18px] w-[18px]" />
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className="h-px w-full bg-[#2C2D2E]" />
|
|
248
|
+
|
|
249
|
+
<div className="flex-1 overflow-y-auto">
|
|
250
|
+
{tab === "config" ? (
|
|
251
|
+
<div className="flex flex-col gap-6 px-3 py-4">
|
|
252
|
+
{/* Saved View */}
|
|
253
|
+
<div className="space-y-3">
|
|
254
|
+
<SectionHeader title="Saved View" />
|
|
255
|
+
<RadioGroup
|
|
256
|
+
value={selectedSavedView}
|
|
257
|
+
onValueChange={handleSavedViewChange}
|
|
258
|
+
className="flex flex-col gap-1 space-y-0 rounded-[12px] bg-[#1C1D1F] p-1"
|
|
259
|
+
>
|
|
260
|
+
{savedViews.map((sv, i) => (
|
|
261
|
+
<div key={sv.id}>
|
|
262
|
+
{/* Divider spans edge-to-edge (Figma: no horizontal
|
|
263
|
+
inset). */}
|
|
264
|
+
{i > 0 && <div className="h-px bg-[#2C2D2E]" />}
|
|
265
|
+
<RadioRow value={sv.id} label={sv.label} />
|
|
266
|
+
</div>
|
|
267
|
+
))}
|
|
268
|
+
</RadioGroup>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onClick={onSaveNewView}
|
|
272
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-[4px] bg-white/[0.15] px-1.5 py-0.5 text-[12px] font-[510] text-white transition-colors hover:bg-white/25"
|
|
273
|
+
>
|
|
274
|
+
<Plus className="h-3 w-3" />
|
|
275
|
+
Save a New View
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="h-px w-full bg-[#2C2D2E]" />
|
|
280
|
+
|
|
281
|
+
{/* Table Columns */}
|
|
282
|
+
<div className="space-y-3">
|
|
283
|
+
<SectionHeader title="Table Columns" />
|
|
284
|
+
<p className="text-[12px] leading-[1.475] text-content-presentation-global-tertiary">
|
|
285
|
+
Show or hide columns in table view
|
|
286
|
+
</p>
|
|
287
|
+
{orderedColumns.length === 0 ? (
|
|
288
|
+
<p className="text-xs text-content-presentation-global-tertiary">
|
|
289
|
+
No fields detected.
|
|
290
|
+
</p>
|
|
291
|
+
) : (
|
|
292
|
+
<div data-theme="dark" className="flex flex-col gap-2">
|
|
293
|
+
{orderedColumns.map((col) => {
|
|
294
|
+
const field = fieldByPath.get(col.id);
|
|
295
|
+
const isDragging = dragPath === col.id;
|
|
296
|
+
const isTarget =
|
|
297
|
+
dragOverPath === col.id && dragPath !== col.id;
|
|
298
|
+
return (
|
|
299
|
+
<div key={col.id}>
|
|
300
|
+
{isTarget && dropBefore && <DropLine />}
|
|
301
|
+
<div
|
|
302
|
+
draggable
|
|
303
|
+
onDragStart={(e) => {
|
|
304
|
+
setDragPath(col.id);
|
|
305
|
+
e.dataTransfer.effectAllowed = "move";
|
|
306
|
+
e.dataTransfer.setData("text/plain", col.id);
|
|
307
|
+
}}
|
|
308
|
+
onDragOver={(e) => {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
e.dataTransfer.dropEffect = "move";
|
|
311
|
+
const rect =
|
|
312
|
+
e.currentTarget.getBoundingClientRect();
|
|
313
|
+
const before =
|
|
314
|
+
e.clientY < rect.top + rect.height / 2;
|
|
315
|
+
if (dragOverPath !== col.id)
|
|
316
|
+
setDragOverPath(col.id);
|
|
317
|
+
if (dropBefore !== before) setDropBefore(before);
|
|
318
|
+
}}
|
|
319
|
+
onDragLeave={() => {
|
|
320
|
+
if (dragOverPath === col.id) setDragOverPath(null);
|
|
321
|
+
}}
|
|
322
|
+
onDrop={(e) => {
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
if (dragPath)
|
|
325
|
+
reorderColumn(dragPath, col.id, dropBefore);
|
|
326
|
+
setDragPath(null);
|
|
327
|
+
setDragOverPath(null);
|
|
328
|
+
}}
|
|
329
|
+
onDragEnd={() => {
|
|
330
|
+
setDragPath(null);
|
|
331
|
+
setDragOverPath(null);
|
|
332
|
+
}}
|
|
333
|
+
className={cn(
|
|
334
|
+
// SB-Column-Item: standalone #1C1D1F pill, #252729
|
|
335
|
+
// border. Figma container spec: 8px radius, 8.8px
|
|
336
|
+
// padding, 8px gap between grip / label / switch.
|
|
337
|
+
"flex items-center gap-2 rounded-r-[99px] rounded-l-[60px] border border-[#252729] bg-[#1C1D1F] p-[8.8px] transition-colors cursor-grab active:cursor-grabbing",
|
|
338
|
+
isDragging ? "opacity-50" : "hover:bg-[#252729]",
|
|
339
|
+
)}
|
|
340
|
+
>
|
|
341
|
+
<span className="flex shrink-0 items-center justify-center">
|
|
342
|
+
<GripDots />
|
|
343
|
+
</span>
|
|
344
|
+
<span className="flex-1 text-[14px] text-white">
|
|
345
|
+
{col.label || field?.label || col.id}
|
|
346
|
+
</span>
|
|
347
|
+
<span className="flex shrink-0 items-center">
|
|
348
|
+
<DataViewsSwitch
|
|
349
|
+
checked={col.visible}
|
|
350
|
+
onCheckedChange={() =>
|
|
351
|
+
toggleColumnVisibility(col.id)
|
|
352
|
+
}
|
|
353
|
+
/>
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
{isTarget && !dropBefore && <DropLine />}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
})}
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<div className="h-px w-full bg-[#2C2D2E]" />
|
|
365
|
+
|
|
366
|
+
{/* Default Sort */}
|
|
367
|
+
<div className="space-y-3">
|
|
368
|
+
<SectionHeader title="Default Sort" />
|
|
369
|
+
{sortableColumns.length === 0 ? (
|
|
370
|
+
<p className="text-xs text-content-presentation-global-tertiary">
|
|
371
|
+
No sortable columns.
|
|
372
|
+
</p>
|
|
373
|
+
) : (
|
|
374
|
+
// Single-choice radio list (Figma 1612:30016): selecting a
|
|
375
|
+
// column sets config.sortBy; direction keeps config.sortOrder.
|
|
376
|
+
<RadioGroup
|
|
377
|
+
value={config.sortBy || undefined}
|
|
378
|
+
onValueChange={(v) => onConfigChange({ sortBy: v })}
|
|
379
|
+
className="flex flex-col gap-1 space-y-0 rounded-[12px] bg-[#1C1D1F] p-1"
|
|
380
|
+
>
|
|
381
|
+
{sortableColumns.map((col, i) => {
|
|
382
|
+
const field = fieldByPath.get(col.id);
|
|
383
|
+
return (
|
|
384
|
+
<div key={col.id}>
|
|
385
|
+
{/* Edge-to-edge divider (Figma: no horizontal
|
|
386
|
+
inset). */}
|
|
387
|
+
{i > 0 && <div className="h-px bg-[#2C2D2E]" />}
|
|
388
|
+
<RadioRow
|
|
389
|
+
value={col.id}
|
|
390
|
+
label={col.label || field?.label || col.id}
|
|
391
|
+
/>
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
})}
|
|
395
|
+
</RadioGroup>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
) : (
|
|
400
|
+
<div className="[&>div]:w-full [&>div]:border-r-0 [&>div]:bg-transparent">
|
|
401
|
+
<FilterPanel
|
|
402
|
+
data={data}
|
|
403
|
+
fields={fields}
|
|
404
|
+
filters={filterState}
|
|
405
|
+
onFilterChange={(path: string, value: FilterValue) =>
|
|
406
|
+
onFilterChange({ ...filterState, [path]: value })
|
|
407
|
+
}
|
|
408
|
+
onClearAll={() => onFilterChange({})}
|
|
409
|
+
filterConfig={filterConfig}
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Settings } from "lucide-react"
|
|
4
|
+
import type { ReactNode } from "react"
|
|
5
|
+
import type { ViewType } from "./types"
|
|
6
|
+
import { Button } from "../Button"
|
|
7
|
+
import { cn } from "../../utils/cn"
|
|
8
|
+
|
|
9
|
+
export type DataViewsHeaderView = {
|
|
10
|
+
id: ViewType
|
|
11
|
+
label: string
|
|
12
|
+
icon: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type DataViewsHeaderProps = {
|
|
16
|
+
title: string
|
|
17
|
+
views: DataViewsHeaderView[]
|
|
18
|
+
currentView: ViewType
|
|
19
|
+
onViewChange: (view: ViewType) => void
|
|
20
|
+
showSettings: boolean
|
|
21
|
+
settingsOpen: boolean
|
|
22
|
+
onToggleSettings: () => void
|
|
23
|
+
onAddNew?: () => void
|
|
24
|
+
addNewLabel?: string
|
|
25
|
+
className?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function DataViewsHeader({
|
|
29
|
+
title,
|
|
30
|
+
views,
|
|
31
|
+
currentView,
|
|
32
|
+
onViewChange,
|
|
33
|
+
showSettings,
|
|
34
|
+
settingsOpen,
|
|
35
|
+
onToggleSettings,
|
|
36
|
+
onAddNew,
|
|
37
|
+
addNewLabel = "Add New",
|
|
38
|
+
className,
|
|
39
|
+
}: DataViewsHeaderProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
// Header is always dark. data-theme="dark" makes the child Button
|
|
43
|
+
// components resolve dark-theme tokens (correct against the black bar)
|
|
44
|
+
// even when the host app runs in default/light theme.
|
|
45
|
+
data-theme="dark"
|
|
46
|
+
className={cn(
|
|
47
|
+
"flex h-[52px] w-full items-center gap-2 rounded-[12px] bg-black px-2",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{/* Title pill */}
|
|
52
|
+
<div className="flex h-9 shrink-0 items-center gap-2 rounded-[12px] border border-[#434446] bg-[#252729] px-[10px]">
|
|
53
|
+
<span className="text-[28px] font-[510] uppercase leading-[1.19] text-white">
|
|
54
|
+
{title}
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Divider */}
|
|
59
|
+
<div className="h-7 w-px shrink-0 bg-[#434446]" />
|
|
60
|
+
|
|
61
|
+
{/* Segmented view switcher */}
|
|
62
|
+
<div className="flex flex-1 items-center">
|
|
63
|
+
<div className="flex items-center gap-[2px] rounded-[10px] bg-[#252729] p-[2px] shadow-[inset_0_0_4px_0_rgba(0,0,0,0.08)]">
|
|
64
|
+
{views.map((view, idx) => {
|
|
65
|
+
const active = view.id === currentView
|
|
66
|
+
const prevActive = idx > 0 && views[idx - 1].id === currentView
|
|
67
|
+
// Separator sits between two inactive tabs only; the active white
|
|
68
|
+
// pill never has a flanking divider (matches Figma).
|
|
69
|
+
const showDivider = idx > 0 && !active && !prevActive
|
|
70
|
+
return (
|
|
71
|
+
<div key={view.id} className="flex items-center">
|
|
72
|
+
{showDivider && (
|
|
73
|
+
<div className="mx-[3px] h-3 w-px bg-[#434446]" />
|
|
74
|
+
)}
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
aria-pressed={active}
|
|
78
|
+
onClick={() => onViewChange(view.id)}
|
|
79
|
+
className={cn(
|
|
80
|
+
"flex h-6 items-center gap-[6px] rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
|
|
81
|
+
active
|
|
82
|
+
? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
|
|
83
|
+
: "bg-transparent text-white hover:bg-white/5",
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
<span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
|
|
87
|
+
{view.icon}
|
|
88
|
+
</span>
|
|
89
|
+
{view.label}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Action bar */}
|
|
98
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
99
|
+
{onAddNew && (
|
|
100
|
+
<Button
|
|
101
|
+
variant="PrimeStyle"
|
|
102
|
+
size="M"
|
|
103
|
+
onClick={onAddNew}
|
|
104
|
+
className="rounded-[6px] bg-[#005ECC] px-[14px] text-[16px] font-[510] text-white hover:bg-[#005ECC]/90"
|
|
105
|
+
>
|
|
106
|
+
{addNewLabel}
|
|
107
|
+
</Button>
|
|
108
|
+
)}
|
|
109
|
+
{/* Hidden while the panel is open — the panel has its own close
|
|
110
|
+
control, so the header trigger would be redundant. */}
|
|
111
|
+
{showSettings && !settingsOpen && (
|
|
112
|
+
<Button
|
|
113
|
+
variant="BluContStyle"
|
|
114
|
+
size="M"
|
|
115
|
+
onClick={onToggleSettings}
|
|
116
|
+
aria-pressed={settingsOpen}
|
|
117
|
+
className="gap-[6px] rounded-[6px] px-[14px] text-[16px] font-[510]"
|
|
118
|
+
>
|
|
119
|
+
<Settings className="h-[18px] w-[18px]" />
|
|
120
|
+
Filter & Config.
|
|
121
|
+
</Button>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|