torch-glare 2.4.4 → 2.5.0
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/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
- package/apps/lib/components/DataViews/cell.tsx +324 -0
- package/apps/lib/components/DataViews/context.ts +144 -0
- package/apps/lib/components/DataViews/data-views.tsx +383 -0
- package/apps/lib/components/DataViews/filters/children.tsx +98 -0
- package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
- package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
- package/apps/lib/components/DataViews/filters/index.ts +4 -0
- package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
- package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
- package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
- package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
- package/apps/lib/components/DataViews/filters/values.ts +173 -0
- package/apps/lib/components/DataViews/header.tsx +217 -0
- package/apps/lib/components/DataViews/hooks/index.ts +5 -0
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
- package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
- package/apps/lib/components/DataViews/index.ts +74 -26
- package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
- package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
- package/apps/lib/components/DataViews/panel/index.ts +3 -0
- package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
- package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
- package/apps/lib/components/DataViews/panel/section.tsx +79 -0
- package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
- package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
- package/apps/lib/components/DataViews/slots.ts +63 -0
- package/apps/lib/components/DataViews/states.tsx +38 -0
- package/apps/lib/components/DataViews/types.ts +485 -178
- package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
- package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
- package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
- package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
- package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
- package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
- package/apps/lib/components/FormBuilder/context.ts +20 -6
- package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
- package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
- package/apps/lib/components/FormBuilder/index.ts +3 -1
- package/apps/lib/components/FormBuilder/types.ts +40 -0
- package/apps/lib/components/Input.tsx +3 -0
- package/apps/lib/components/SearchableTable.tsx +5 -4
- package/apps/lib/components/SectionBlock.tsx +58 -11
- package/apps/lib/components/Select.tsx +3 -1
- package/apps/lib/components/TabSwitch.tsx +16 -4
- package/apps/lib/components/Table.tsx +265 -67
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
- package/apps/lib/components/TreeFolder/index.ts +1 -1
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
- package/apps/lib/hooks/useDragDrop.tsx +365 -0
- package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
- package/apps/lib/registry.json +159 -4
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/apps/lib/utils/dataViews/path.ts +67 -0
- package/apps/lib/utils/dataViews/query.ts +73 -0
- package/apps/lib/utils/dataViews/types.ts +187 -0
- package/docs/components/breadcrumb.md +1 -1
- package/docs/components/button-group.md +1 -1
- package/docs/components/button.md +1 -1
- package/docs/components/card.md +1 -1
- package/docs/components/checkbox.md +1 -1
- package/docs/components/data-views/backend-response.md +324 -0
- package/docs/components/data-views/examples/a11y-rtl.md +250 -0
- package/docs/components/data-views/examples/api-orders-route.md +130 -0
- package/docs/components/data-views/examples/fields.md +362 -0
- package/docs/components/data-views/examples/filters.md +308 -0
- package/docs/components/data-views/examples/inbox-routing.md +218 -0
- package/docs/components/data-views/examples/index.md +29 -0
- package/docs/components/data-views/examples/overview.md +244 -0
- package/docs/components/data-views/examples/panel.md +212 -0
- package/docs/components/data-views/examples/scale.md +231 -0
- package/docs/components/data-views/examples/server-side.md +210 -0
- package/docs/components/data-views/examples/state.md +250 -0
- package/docs/components/data-views/examples/tree-custom.md +388 -0
- package/docs/components/data-views/examples/view-registry.md +313 -0
- package/docs/components/data-views/examples/views.md +534 -0
- package/docs/components/data-views/guide.md +405 -0
- package/docs/components/data-views/index.md +1504 -0
- package/docs/components/data-views/migration.md +79 -0
- package/docs/components/date-picker.md +0 -1
- package/docs/components/form-builder.md +19 -8
- package/docs/components/form-renderer.md +2 -1
- package/docs/components/form.md +1 -1
- package/docs/components/input-field.md +1 -1
- package/docs/components/input-otp.md +1 -1
- package/docs/components/input.md +1 -1
- package/docs/components/labeled-check-box.md +1 -1
- package/docs/components/labeled-radio.md +1 -1
- package/docs/components/radio-card.md +1 -1
- package/docs/components/radio.md +1 -1
- package/docs/components/search-field.md +1 -1
- package/docs/components/section-block.md +79 -3
- package/docs/components/select.md +1 -1
- package/docs/components/simple-select.md +1 -1
- package/docs/components/switch.md +1 -1
- package/docs/components/tab-switch.md +1 -1
- package/docs/components/table.md +45 -8
- package/docs/components/text-editor.md +1 -1
- package/docs/components/textarea.md +1 -1
- package/docs/components/toggle-button.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/components/tree-folder.md +110 -0
- package/docs/how-to/forms-with-form-builder.md +6 -4
- package/docs/reference/components.md +16 -6
- package/docs/tutorials/component-composition.md +11 -13
- package/package.json +3 -2
- package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
- package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
- package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
- package/apps/lib/components/DataViews/InboxView.tsx +0 -463
- package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
- package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
- package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
- package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
- package/apps/lib/components/DataViews/TableView.tsx +0 -212
- package/apps/lib/components/DataViews/TreeView.tsx +0 -364
- package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
- package/apps/lib/hooks/useDataViewsState.ts +0 -175
- package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
- package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
- package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
- package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
- package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
- package/docs/components/data-views-config-panel.md +0 -208
- package/docs/components/data-views-layout.md +0 -291
- package/docs/components/inbox-view.md +0 -170
- package/docs/components/kanban-view.md +0 -135
- package/docs/components/table-view.md +0 -141
- package/docs/components/tree-view.md +0 -147
- package/docs/how-to/data-views-from-backend-response.md +0 -194
|
@@ -1,36 +1,84 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
// Enumerated, never `export *` — the same convention as `FormBuilder/index.ts`, so what this
|
|
2
|
+
// folder offers is readable in one screen.
|
|
3
|
+
//
|
|
4
|
+
// Almost everything is reached through the compound root: `DataViews.Table`, `DataViews.Panel`,
|
|
5
|
+
// `DataViews.Filters`, and so on. The loose exports below are the pieces you need when writing a
|
|
6
|
+
// part of your own.
|
|
3
7
|
|
|
4
|
-
export {
|
|
5
|
-
export type { TableViewProps } from "./TableView";
|
|
8
|
+
export { DataViews } from "./data-views";
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
// The query's wire format, both directions. A page needs the first two; a route handler that
|
|
11
|
+
// serves one of these lists needs `parseQuery`.
|
|
12
|
+
export { emptyQuery, parseQuery, queryToParams } from "../../utils/dataViews/query";
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
// Reach into the component's state from inside a custom child — a detail pane, a bespoke filter,
|
|
15
|
+
// a toolbar button that clears the selection.
|
|
16
|
+
export {
|
|
17
|
+
useDataViewsData,
|
|
18
|
+
useDataViewsFilters,
|
|
19
|
+
useDataViewsPanel,
|
|
20
|
+
useDataViewsPanelTabs,
|
|
21
|
+
useDataViewsView,
|
|
22
|
+
} from "./context";
|
|
23
|
+
export { useActiveRow } from "./hooks";
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
export
|
|
25
|
+
// Paint one field the way the views paint it, in your own layout.
|
|
26
|
+
export { Cell } from "./cell";
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
// A view of your own gets the loading state the built-in four get: read `loading` from
|
|
29
|
+
// `useDataViewsData()` and lay these out in your own shape. There is no `Empty` counterpart —
|
|
30
|
+
// nothing to show is shown as nothing.
|
|
31
|
+
export { SkeletonBar, skeletonKeys } from "./states";
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
// Wrapping a part in a component of your own — a preset panel, a project-standard header — hides
|
|
34
|
+
// the marker the root recognises it by, so the wrapper has to carry the marker itself:
|
|
35
|
+
//
|
|
36
|
+
// ```tsx
|
|
37
|
+
// const AppPanel = markPanel(function AppPanel() { return <DataViews.Panel>…</DataViews.Panel>; });
|
|
38
|
+
// ```
|
|
39
|
+
export { markHeader, markPanel, markView } from "./slots";
|
|
40
|
+
export { resolveBadgeVariant } from "./badge";
|
|
21
41
|
|
|
22
|
-
export {
|
|
23
|
-
export {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
export type { ResolvedBadgeProps } from "./badge";
|
|
43
|
+
export type {
|
|
44
|
+
DataContextValue,
|
|
45
|
+
FiltersContextValue,
|
|
46
|
+
PanelContextValue,
|
|
47
|
+
PanelTab,
|
|
48
|
+
PanelTabsContextValue,
|
|
49
|
+
RegisteredView,
|
|
50
|
+
ViewContextValue,
|
|
51
|
+
} from "./context";
|
|
52
|
+
export type * from "./types";
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
// The vocabulary — the shapes **you** construct and hand in: a row, a field, the query, a tree of
|
|
55
|
+
// nodes, a board's groups. They live in `utils/dataViews` because the route handler that serves
|
|
56
|
+
// these lists imports them too, and are re-exported here so that one import line is enough:
|
|
57
|
+
// every prop above is typed in terms of them.
|
|
58
|
+
export type {
|
|
59
|
+
BadgeVariant,
|
|
60
|
+
ColumnColor,
|
|
61
|
+
ColumnState,
|
|
62
|
+
CurrencyOptions,
|
|
63
|
+
DataViewsQuery,
|
|
64
|
+
FieldConfig,
|
|
65
|
+
FieldType,
|
|
66
|
+
FilterFieldDescriptor,
|
|
67
|
+
FilterState,
|
|
68
|
+
FilterValue,
|
|
69
|
+
MoveIntent,
|
|
70
|
+
Path,
|
|
71
|
+
Preset,
|
|
72
|
+
Row,
|
|
73
|
+
RowGroup,
|
|
74
|
+
Sort,
|
|
75
|
+
TreeNode,
|
|
76
|
+
} from "../../utils/dataViews/types";
|
|
32
77
|
|
|
33
|
-
|
|
78
|
+
// Reading a value the way `Cell` reads it — dotted paths included — and the id rule the component
|
|
79
|
+
// falls back to. A custom view or a `renderCell` that has to reach a second field needs these.
|
|
80
|
+
export { defaultGetRowId, formatPathLabel, getByPath, getString } from "../../utils/dataViews/path";
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
|
|
82
|
+
// The board's card body, so a card of your own — in the board or the tree's pane — is built from
|
|
83
|
+
// the same pairs the built-in one uses.
|
|
84
|
+
export { buildCardRows } from "./views/card-rows";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { cn } from "../../../utils/cn";
|
|
5
|
+
import { DragList, useDragDrop, useDragItem } from "../../../hooks/useDragDrop";
|
|
6
|
+
import { formatPathLabel } from "../../../utils/dataViews/path";
|
|
7
|
+
import type { ColumnState } from "../../../utils/dataViews/types";
|
|
8
|
+
import { useDataViewsData, useDataViewsView } from "../context";
|
|
9
|
+
import { DataViewsSwitch } from "./controls";
|
|
10
|
+
import { Section } from "./section";
|
|
11
|
+
import type { PanelColumnsProps } from "../types";
|
|
12
|
+
|
|
13
|
+
/** The 2×3 grip dots on a draggable column row. Hand-drawn — there is no icon for this shape. */
|
|
14
|
+
function GripDots() {
|
|
15
|
+
return (
|
|
16
|
+
<svg aria-hidden width="16" height="16" viewBox="0 0 16 16" className="text-white/60" fill="currentColor">
|
|
17
|
+
{[5.33, 9.33].flatMap((cx) =>
|
|
18
|
+
[3.33, 8, 12.67].map((cy) => <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="1" />),
|
|
19
|
+
)}
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The insertion line shown between rows while dragging one. */
|
|
25
|
+
function DropLine() {
|
|
26
|
+
return (
|
|
27
|
+
<div className="pointer-events-none relative h-0">
|
|
28
|
+
<div className="absolute -top-[1px] left-0 right-0 h-[2px] rounded-full bg-[#005ECC]" />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `DataViews.Panel.Columns` — show/hide and reorder the columns.
|
|
35
|
+
*
|
|
36
|
+
* The column list is the root's, read and written through `useDataViewsView()` — this part takes
|
|
37
|
+
* no `value`/`onValueChange` of its own. The root is what turns `columns` into the ordered
|
|
38
|
+
* `visibleFields` every view paints from, so hiding a column here also retitles the board's cards. When `columns` has never been set, the list is seeded from
|
|
39
|
+
* `fields` on first edit, so you are not forced to declare a redundant array to get this working.
|
|
40
|
+
*/
|
|
41
|
+
export function Columns({
|
|
42
|
+
title = "Table Columns",
|
|
43
|
+
// Figma writes this line under the header; it is the section's own words, so it is the default
|
|
44
|
+
// rather than something every caller has to remember to pass.
|
|
45
|
+
description = "Show or hide columns in table view",
|
|
46
|
+
className,
|
|
47
|
+
}: PanelColumnsProps) {
|
|
48
|
+
const { fields } = useDataViewsData();
|
|
49
|
+
const { columns, setColumns } = useDataViewsView();
|
|
50
|
+
|
|
51
|
+
// What the panel shows: the user's arrangement if there is one, otherwise the declared fields.
|
|
52
|
+
const current: ColumnState[] =
|
|
53
|
+
columns.length > 0
|
|
54
|
+
? columns.slice()
|
|
55
|
+
: fields
|
|
56
|
+
.filter((f) => f.type !== "hidden")
|
|
57
|
+
.map((f) => ({
|
|
58
|
+
path: f.path,
|
|
59
|
+
label: f.label ?? formatPathLabel(f.path),
|
|
60
|
+
visible: f.visible !== false,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// Rows are identified by path — the same key the table and every other view uses.
|
|
64
|
+
const ids = current.map((c) => c.path);
|
|
65
|
+
|
|
66
|
+
const { DndContext, contextProps, activeId, overId } = useDragDrop({
|
|
67
|
+
mode: "list",
|
|
68
|
+
onMove: ({ id, to }) => {
|
|
69
|
+
const from = ids.indexOf(id);
|
|
70
|
+
const target = ids.indexOf(String(to));
|
|
71
|
+
if (from < 0 || target < 0 || from === target) return;
|
|
72
|
+
const next = current.slice();
|
|
73
|
+
const [moved] = next.splice(from, 1);
|
|
74
|
+
next.splice(target, 0, moved);
|
|
75
|
+
setColumns(next);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const toggle = (path: string) =>
|
|
80
|
+
setColumns(current.map((c) => (c.path === path ? { ...c, visible: !c.visible } : c)));
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Section title={title} description={description} className={className}>
|
|
84
|
+
<DndContext {...contextProps}>
|
|
85
|
+
<DragList ids={ids}>
|
|
86
|
+
<div data-theme="dark" className="flex flex-col gap-2">
|
|
87
|
+
{current.map((column, index) => (
|
|
88
|
+
<ColumnRow
|
|
89
|
+
key={`${column.path}-${index}`}
|
|
90
|
+
column={column}
|
|
91
|
+
onToggle={() => toggle(column.path)}
|
|
92
|
+
showDropLine={overId === column.path && activeId !== null && activeId !== column.path}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</DragList>
|
|
97
|
+
</DndContext>
|
|
98
|
+
</Section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One column row.
|
|
104
|
+
*
|
|
105
|
+
* A separate component because `useDragItem` is a hook, and a hook cannot be called inside the
|
|
106
|
+
* `.map()` that paints the list.
|
|
107
|
+
*/
|
|
108
|
+
function ColumnRow({
|
|
109
|
+
column,
|
|
110
|
+
onToggle,
|
|
111
|
+
showDropLine,
|
|
112
|
+
}: {
|
|
113
|
+
column: ColumnState;
|
|
114
|
+
onToggle: () => void;
|
|
115
|
+
showDropLine: boolean;
|
|
116
|
+
}) {
|
|
117
|
+
const { ref, handleProps, style, isDragging } = useDragItem(column.path);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<React.Fragment>
|
|
121
|
+
{showDropLine && <DropLine />}
|
|
122
|
+
<div
|
|
123
|
+
ref={ref}
|
|
124
|
+
style={style}
|
|
125
|
+
{...handleProps}
|
|
126
|
+
className={cn(
|
|
127
|
+
// SB-Column-Item: a standalone #1C1D1F pill with a #252729 border, rounded hard
|
|
128
|
+
// on the trailing edge and softly on the leading one.
|
|
129
|
+
"flex cursor-grab items-center gap-2 rounded-e-[99px] rounded-s-[60px] border",
|
|
130
|
+
"border-[#252729] bg-[#1C1D1F] p-[8.8px] transition-colors active:cursor-grabbing",
|
|
131
|
+
isDragging ? "opacity-50" : "hover:bg-[#252729]",
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<span className="flex shrink-0 items-center justify-center">
|
|
135
|
+
<GripDots />
|
|
136
|
+
</span>
|
|
137
|
+
<span className="flex-1 text-[14px] text-white">{column.label}</span>
|
|
138
|
+
{/* Stop the switch from starting a drag: the row is the handle, and a press on the
|
|
139
|
+
toggle is not a press on the row. */}
|
|
140
|
+
<span
|
|
141
|
+
className="flex shrink-0 items-center"
|
|
142
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
143
|
+
>
|
|
144
|
+
<DataViewsSwitch
|
|
145
|
+
checked={column.visible}
|
|
146
|
+
onCheckedChange={onToggle}
|
|
147
|
+
aria-label={`Show ${column.label}`}
|
|
148
|
+
/>
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
</React.Fragment>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { cn } from "../../../utils/cn";
|
|
6
|
+
import { Switch } from "../../Switch";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The form controls the settings rail is built from.
|
|
10
|
+
*
|
|
11
|
+
* The rail is wrapped in `data-theme="dark"`, so themed components resolve dark tokens on their
|
|
12
|
+
* own. The literals below are the exceptions — values chosen against pure black in Figma that
|
|
13
|
+
* sit outside the theme system entirely.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The bright-green checked track from the Figma Switcher-1.0 "On" state. */
|
|
17
|
+
const SWITCH_GREEN = "data-[state=checked]:bg-[#0AC713] data-[state=checked]:border-[#0AC713]";
|
|
18
|
+
|
|
19
|
+
export function DataViewsSwitch(props: React.ComponentProps<typeof Switch>) {
|
|
20
|
+
return <Switch {...props} className={cn(SWITCH_GREEN, props.className)} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A radio row. The whole 32px row is the Radix item, so the label is part of the click target
|
|
25
|
+
* rather than something you have to hit the 14px ring for.
|
|
26
|
+
*/
|
|
27
|
+
export function DataViewRadio({
|
|
28
|
+
value,
|
|
29
|
+
label,
|
|
30
|
+
children,
|
|
31
|
+
className,
|
|
32
|
+
}: {
|
|
33
|
+
value: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
children?: React.ReactNode;
|
|
36
|
+
className?: string;
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<RadioGroupPrimitive.Item
|
|
40
|
+
value={value}
|
|
41
|
+
className={cn(
|
|
42
|
+
"group flex h-[32px] w-full items-center gap-1.5 py-1 ps-2",
|
|
43
|
+
"cursor-pointer rounded-[8px] text-left outline-none transition-colors",
|
|
44
|
+
"hover:bg-background-presentation-action-contstyle-hover focus-visible:bg-background-presentation-action-contstyle-hover",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
<span
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full",
|
|
51
|
+
// Literals so the indicator stays dark inside the rail regardless of the host theme,
|
|
52
|
+
// matching the always-dark panel chrome. Unselected ring is the CheckBox-Primary border
|
|
53
|
+
// (#626467) over the BorderStyle fill; selected is the focus blue.
|
|
54
|
+
"border border-[#626467] bg-white/5 transition-colors",
|
|
55
|
+
"group-data-[state=checked]:border-transparent",
|
|
56
|
+
"group-data-[state=checked]:bg-[#0075FF]",
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
60
|
+
<span className="h-[6px] w-[6px] rounded-full bg-white" />
|
|
61
|
+
</RadioGroupPrimitive.Indicator>
|
|
62
|
+
</span>
|
|
63
|
+
<span className="typography-body-medium-regular text-content-presentation-global-primary flex-1">
|
|
64
|
+
{children ?? label}
|
|
65
|
+
</span>
|
|
66
|
+
</RadioGroupPrimitive.Item>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A `#1C1D1F` pill holding a list of radio rows, with hairlines between them.
|
|
72
|
+
*
|
|
73
|
+
* The two `:has()` rules hide the hairline above and below the row being hovered, so the hover
|
|
74
|
+
* highlight reads as one continuous block instead of being sliced by a line.
|
|
75
|
+
*/
|
|
76
|
+
export function RadioGroup({
|
|
77
|
+
value,
|
|
78
|
+
onValueChange,
|
|
79
|
+
items,
|
|
80
|
+
className,
|
|
81
|
+
}: {
|
|
82
|
+
value: string;
|
|
83
|
+
onValueChange: (value: string) => void;
|
|
84
|
+
items: readonly { value: string; label: string }[];
|
|
85
|
+
className?: string;
|
|
86
|
+
}) {
|
|
87
|
+
return (
|
|
88
|
+
<RadioGroupPrimitive.Root
|
|
89
|
+
value={value}
|
|
90
|
+
onValueChange={onValueChange}
|
|
91
|
+
className={cn(
|
|
92
|
+
"flex flex-col space-y-0 rounded-[12px] bg-[#1C1D1F] p-1",
|
|
93
|
+
"[&>div:has(>[role=radio]:hover)>.dv-divider]:opacity-0",
|
|
94
|
+
"[&>div:has(>[role=radio]:hover)+div>.dv-divider]:opacity-0",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{items.map((item, i) => (
|
|
99
|
+
<div key={item.value}>
|
|
100
|
+
{i > 0 && <div className="dv-divider h-px bg-[#2C2D2E]" />}
|
|
101
|
+
<DataViewRadio value={item.value} label={item.label} />
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</RadioGroupPrimitive.Root>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { Children, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../utils/cn";
|
|
6
|
+
import { PanelTabsContext, useDataViewsPanel, type PanelTab } from "../context";
|
|
7
|
+
import { markPanel } from "../slots";
|
|
8
|
+
import { Columns } from "./columns";
|
|
9
|
+
import { SavedViews } from "./saved-views";
|
|
10
|
+
import { Section } from "./section";
|
|
11
|
+
import { Sort } from "./sort";
|
|
12
|
+
import { Tab, isTabElement } from "./tab";
|
|
13
|
+
import type { PanelProps } from "../types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `DataViews.Panel` — the settings rail, opened by `DataViews.PanelToggle`.
|
|
17
|
+
*
|
|
18
|
+
* Always dark: `data-theme="dark"` on the root makes the Buttons, Switches and Radios inside
|
|
19
|
+
* resolve dark tokens against the black, even when the host app runs light. The width animates
|
|
20
|
+
* from 0 so the content beside it reflows rather than being covered.
|
|
21
|
+
*
|
|
22
|
+
* Tabs, like views, exist because you rendered them: the strip is built from the `Panel.Tab`
|
|
23
|
+
* children found here and disappears when there is only one.
|
|
24
|
+
*
|
|
25
|
+
* Open/closed lives on the root, because the toggle that drives it is in the header — this
|
|
26
|
+
* component's sibling. Which tab is showing lives here, because nothing outside this rail cares.
|
|
27
|
+
*/
|
|
28
|
+
function PanelRoot({ children, defaultTab, title, className }: PanelProps) {
|
|
29
|
+
const { open, setOpen } = useDataViewsPanel();
|
|
30
|
+
|
|
31
|
+
// Mount at width 0, then flip to the open width on the next frame so the transition has two
|
|
32
|
+
// values to animate between; on close, keep it mounted until the animation finishes.
|
|
33
|
+
const [mounted, setMounted] = useState(open);
|
|
34
|
+
const [expanded, setExpanded] = useState(false);
|
|
35
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (open) {
|
|
39
|
+
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
40
|
+
setMounted(true);
|
|
41
|
+
const raf = requestAnimationFrame(() => requestAnimationFrame(() => setExpanded(true)));
|
|
42
|
+
return () => cancelAnimationFrame(raf);
|
|
43
|
+
}
|
|
44
|
+
setExpanded(false);
|
|
45
|
+
closeTimer.current = setTimeout(() => setMounted(false), 300);
|
|
46
|
+
return () => {
|
|
47
|
+
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
48
|
+
};
|
|
49
|
+
}, [open]);
|
|
50
|
+
|
|
51
|
+
const childArray = Children.toArray(children);
|
|
52
|
+
const tabElements = childArray.filter(isTabElement);
|
|
53
|
+
const loose = childArray.filter((n) => !isTabElement(n));
|
|
54
|
+
|
|
55
|
+
// Keyed on the tab identities rather than the (always-new) child array — same reasoning as the
|
|
56
|
+
// view registry in `data-views.tsx`.
|
|
57
|
+
const tabKey = tabElements.map((el) => `${el.props.value}|${el.props.label}`).join(",");
|
|
58
|
+
const tabs = useMemo<PanelTab[]>(
|
|
59
|
+
() =>
|
|
60
|
+
tabElements.map((el) => ({ value: el.props.value, label: el.props.label, icon: el.props.icon })),
|
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on `tabKey`, see above
|
|
62
|
+
[tabKey],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const [currentTab, setTab] = useState(defaultTab ?? "");
|
|
66
|
+
const known = tabs.some((t) => t.value === currentTab);
|
|
67
|
+
const fallbackTab = tabs[0]?.value ?? "";
|
|
68
|
+
// A tab that stops being rendered falls back to the first, which also covers the first render
|
|
69
|
+
// when no `defaultTab` was given.
|
|
70
|
+
const activeTab = known ? currentTab : fallbackTab;
|
|
71
|
+
|
|
72
|
+
const value = useMemo(() => ({ tab: activeTab, setTab, tabs }), [activeTab, setTab, tabs]);
|
|
73
|
+
|
|
74
|
+
if (!mounted) return null;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<PanelTabsContext.Provider value={value}>
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"shrink-0 overflow-hidden transition-[width] duration-300 ease-in-out",
|
|
81
|
+
expanded ? "w-[260px]" : "w-0",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<aside
|
|
85
|
+
data-theme="dark"
|
|
86
|
+
data-state={expanded ? "open" : "closed"}
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex h-full w-[260px] flex-col overflow-hidden rounded-[16px] bg-black",
|
|
89
|
+
"transition-opacity duration-200 ease-in-out",
|
|
90
|
+
expanded ? "opacity-100" : "opacity-0",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<div className="flex items-center gap-2 px-3 py-3">
|
|
95
|
+
{tabs.length > 1 ? (
|
|
96
|
+
<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)]">
|
|
97
|
+
{tabs.map((t) => {
|
|
98
|
+
const active = t.value === activeTab;
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
key={t.value}
|
|
102
|
+
type="button"
|
|
103
|
+
role="tab"
|
|
104
|
+
aria-selected={active}
|
|
105
|
+
onClick={() => setTab(t.value)}
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex h-6 flex-1 items-center justify-center gap-1 rounded-[8px] px-3",
|
|
108
|
+
"text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
|
|
109
|
+
active
|
|
110
|
+
? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
|
|
111
|
+
: "bg-transparent text-white hover:bg-white/5",
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{t.icon && (
|
|
115
|
+
<span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
|
|
116
|
+
{t.icon}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
{t.label}
|
|
120
|
+
</button>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
<span className="flex-1 text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
|
|
126
|
+
{title ?? tabs[0]?.label ?? ""}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
aria-label="Close settings panel"
|
|
133
|
+
onClick={() => setOpen(false)}
|
|
134
|
+
className="hover:bg-background-presentation-state-negative-primary flex h-7 w-7 shrink-0 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:text-white"
|
|
135
|
+
>
|
|
136
|
+
<X className="h-[18px] w-[18px]" />
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="h-px w-full bg-[#2C2D2E]" />
|
|
141
|
+
|
|
142
|
+
<div className="flex flex-1 flex-col gap-6 overflow-y-auto px-3 py-4">
|
|
143
|
+
{tabElements}
|
|
144
|
+
{loose}
|
|
145
|
+
</div>
|
|
146
|
+
</aside>
|
|
147
|
+
</div>
|
|
148
|
+
</PanelTabsContext.Provider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
markPanel(PanelRoot);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The rail's compound surface, assembled in one place so what it offers is readable at a glance.
|
|
157
|
+
*/
|
|
158
|
+
export const Panel = Object.assign(PanelRoot, {
|
|
159
|
+
Tab,
|
|
160
|
+
Section,
|
|
161
|
+
Columns,
|
|
162
|
+
Sort,
|
|
163
|
+
SavedViews,
|
|
164
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Plus } from "lucide-react";
|
|
5
|
+
import { Button } from "../../Button";
|
|
6
|
+
import { useDataViewsFilters, useDataViewsView } from "../context";
|
|
7
|
+
import { RadioGroup } from "./controls";
|
|
8
|
+
import { Section } from "./section";
|
|
9
|
+
import type { PanelSavedViewsProps } from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `DataViews.Panel.SavedViews` — pick a saved arrangement, or save the current one.
|
|
13
|
+
*
|
|
14
|
+
* Persistence is yours. `onSave` is handed the full snapshot — filters, sort and columns — which
|
|
15
|
+
* is the part the old panel got wrong: its save callback took no arguments, so there was nothing
|
|
16
|
+
* a host could actually store.
|
|
17
|
+
*/
|
|
18
|
+
export function SavedViews({
|
|
19
|
+
title = "Saved View",
|
|
20
|
+
views = [],
|
|
21
|
+
onValueChange,
|
|
22
|
+
onSave,
|
|
23
|
+
saveLabel = "Save a New View",
|
|
24
|
+
className,
|
|
25
|
+
}: PanelSavedViewsProps) {
|
|
26
|
+
const { sort, setSort, columns, setColumns } = useDataViewsView();
|
|
27
|
+
const { filters, setFilters } = useDataViewsFilters();
|
|
28
|
+
// Which view is selected is UI state like any other — the caller stores the snapshots, not the
|
|
29
|
+
// cursor into them.
|
|
30
|
+
const [active, setActive] = useState("");
|
|
31
|
+
|
|
32
|
+
/** Selecting a view is the other half of saving one: put back exactly what was stored. */
|
|
33
|
+
const restore = (id: string) => {
|
|
34
|
+
setActive(id);
|
|
35
|
+
const snapshot = views.find((v) => v.id === id)?.snapshot;
|
|
36
|
+
if (snapshot) {
|
|
37
|
+
setColumns(snapshot.columns);
|
|
38
|
+
setSort(snapshot.sort);
|
|
39
|
+
setFilters(snapshot.filters);
|
|
40
|
+
}
|
|
41
|
+
onValueChange?.(id);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Section title={title} className={className}>
|
|
46
|
+
{views.length > 0 && (
|
|
47
|
+
<RadioGroup
|
|
48
|
+
value={active}
|
|
49
|
+
onValueChange={restore}
|
|
50
|
+
items={views.map((v) => ({ value: v.id, label: v.label }))}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
{onSave && (
|
|
54
|
+
<Button
|
|
55
|
+
type="button"
|
|
56
|
+
variant="BorderStyle"
|
|
57
|
+
size="M"
|
|
58
|
+
className="w-full"
|
|
59
|
+
onClick={() => onSave({ filters, sort, columns })}
|
|
60
|
+
>
|
|
61
|
+
<Plus className="h-4 w-4" />
|
|
62
|
+
{saveLabel}
|
|
63
|
+
</Button>
|
|
64
|
+
)}
|
|
65
|
+
</Section>
|
|
66
|
+
);
|
|
67
|
+
}
|