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.
Files changed (147) hide show
  1. package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
  2. package/apps/lib/components/DataViews/cell.tsx +324 -0
  3. package/apps/lib/components/DataViews/context.ts +144 -0
  4. package/apps/lib/components/DataViews/data-views.tsx +383 -0
  5. package/apps/lib/components/DataViews/filters/children.tsx +98 -0
  6. package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
  7. package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
  8. package/apps/lib/components/DataViews/filters/index.ts +4 -0
  9. package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
  10. package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
  11. package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
  12. package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
  13. package/apps/lib/components/DataViews/filters/values.ts +173 -0
  14. package/apps/lib/components/DataViews/header.tsx +217 -0
  15. package/apps/lib/components/DataViews/hooks/index.ts +5 -0
  16. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
  17. package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
  18. package/apps/lib/components/DataViews/index.ts +74 -26
  19. package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
  20. package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
  21. package/apps/lib/components/DataViews/panel/index.ts +3 -0
  22. package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
  23. package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
  24. package/apps/lib/components/DataViews/panel/section.tsx +79 -0
  25. package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
  26. package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
  27. package/apps/lib/components/DataViews/slots.ts +63 -0
  28. package/apps/lib/components/DataViews/states.tsx +38 -0
  29. package/apps/lib/components/DataViews/types.ts +485 -178
  30. package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
  31. package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
  32. package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
  33. package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
  34. package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
  35. package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
  36. package/apps/lib/components/FormBuilder/context.ts +20 -6
  37. package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
  38. package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
  39. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
  40. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
  45. package/apps/lib/components/FormBuilder/index.ts +3 -1
  46. package/apps/lib/components/FormBuilder/types.ts +40 -0
  47. package/apps/lib/components/Input.tsx +3 -0
  48. package/apps/lib/components/SearchableTable.tsx +5 -4
  49. package/apps/lib/components/SectionBlock.tsx +58 -11
  50. package/apps/lib/components/Select.tsx +3 -1
  51. package/apps/lib/components/TabSwitch.tsx +16 -4
  52. package/apps/lib/components/Table.tsx +265 -67
  53. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  54. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  55. package/apps/lib/components/TreeFolder/index.ts +1 -1
  56. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  57. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  58. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  59. package/apps/lib/registry.json +159 -4
  60. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  61. package/apps/lib/utils/dataViews/path.ts +67 -0
  62. package/apps/lib/utils/dataViews/query.ts +73 -0
  63. package/apps/lib/utils/dataViews/types.ts +187 -0
  64. package/docs/components/breadcrumb.md +1 -1
  65. package/docs/components/button-group.md +1 -1
  66. package/docs/components/button.md +1 -1
  67. package/docs/components/card.md +1 -1
  68. package/docs/components/checkbox.md +1 -1
  69. package/docs/components/data-views/backend-response.md +324 -0
  70. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  71. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  72. package/docs/components/data-views/examples/fields.md +362 -0
  73. package/docs/components/data-views/examples/filters.md +308 -0
  74. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  75. package/docs/components/data-views/examples/index.md +29 -0
  76. package/docs/components/data-views/examples/overview.md +244 -0
  77. package/docs/components/data-views/examples/panel.md +212 -0
  78. package/docs/components/data-views/examples/scale.md +231 -0
  79. package/docs/components/data-views/examples/server-side.md +210 -0
  80. package/docs/components/data-views/examples/state.md +250 -0
  81. package/docs/components/data-views/examples/tree-custom.md +388 -0
  82. package/docs/components/data-views/examples/view-registry.md +313 -0
  83. package/docs/components/data-views/examples/views.md +534 -0
  84. package/docs/components/data-views/guide.md +405 -0
  85. package/docs/components/data-views/index.md +1504 -0
  86. package/docs/components/data-views/migration.md +79 -0
  87. package/docs/components/date-picker.md +0 -1
  88. package/docs/components/form-builder.md +19 -8
  89. package/docs/components/form-renderer.md +2 -1
  90. package/docs/components/form.md +1 -1
  91. package/docs/components/input-field.md +1 -1
  92. package/docs/components/input-otp.md +1 -1
  93. package/docs/components/input.md +1 -1
  94. package/docs/components/labeled-check-box.md +1 -1
  95. package/docs/components/labeled-radio.md +1 -1
  96. package/docs/components/radio-card.md +1 -1
  97. package/docs/components/radio.md +1 -1
  98. package/docs/components/search-field.md +1 -1
  99. package/docs/components/section-block.md +79 -3
  100. package/docs/components/select.md +1 -1
  101. package/docs/components/simple-select.md +1 -1
  102. package/docs/components/switch.md +1 -1
  103. package/docs/components/tab-switch.md +1 -1
  104. package/docs/components/table.md +45 -8
  105. package/docs/components/text-editor.md +1 -1
  106. package/docs/components/textarea.md +1 -1
  107. package/docs/components/toggle-button.md +1 -1
  108. package/docs/components/toggle.md +1 -1
  109. package/docs/components/tree-folder.md +110 -0
  110. package/docs/how-to/forms-with-form-builder.md +6 -4
  111. package/docs/reference/components.md +16 -6
  112. package/docs/tutorials/component-composition.md +11 -13
  113. package/package.json +3 -2
  114. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  115. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  116. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  117. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  118. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  119. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  120. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  121. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  122. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  123. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  124. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  125. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  126. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  127. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  128. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  129. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  130. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  131. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  132. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  133. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  134. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  135. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  136. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  137. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  138. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  139. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  140. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  141. package/docs/components/data-views-config-panel.md +0 -208
  142. package/docs/components/data-views-layout.md +0 -291
  143. package/docs/components/inbox-view.md +0 -170
  144. package/docs/components/kanban-view.md +0 -135
  145. package/docs/components/table-view.md +0 -141
  146. package/docs/components/tree-view.md +0 -147
  147. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -1,36 +1,84 @@
1
- export { DataViewsLayout } from "./DataViewsLayout";
2
- export type { DataViewsLayoutProps } from "./DataViewsLayout";
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 { TableView } from "./TableView";
5
- export type { TableViewProps } from "./TableView";
8
+ export { DataViews } from "./data-views";
6
9
 
7
- export { KanbanView } from "./KanbanView";
8
- export type { KanbanViewProps } from "./KanbanView";
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
- export { InboxView } from "./InboxView";
11
- export type { InboxViewProps } from "./InboxView";
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
- export { InboxViewCard } from "./InboxViewCard";
14
- export type { InboxViewCardProps } from "./InboxViewCard";
25
+ // Paint one field the way the views paint it, in your own layout.
26
+ export { Cell } from "./cell";
15
27
 
16
- export { TreeView } from "./TreeView";
17
- export type { TreeViewProps } from "./TreeView";
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
- export { DataViewRadio } from "./DataViewRadio";
20
- export type { DataViewRadioProps } from "./DataViewRadio";
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 { FilterPanel } from "./FilterPanel";
23
- export { SettingsPanel } from "./SettingsPanel";
24
- export { DataViewsHeader } from "./DataViewsHeader";
25
- export type { DataViewsHeaderView } from "./DataViewsHeader";
26
- export { DataViewsConfigPanel } from "./DataViewsConfigPanel";
27
- export type { DataViewsConfigPanelProps } from "./DataViewsConfigPanel";
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
- export { renderField } from "./fieldRenderers";
30
- export { resolveBadgeVariant } from "./badgeAdapter";
31
- export type { ResolvedBadgeProps } from "./badgeAdapter";
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
- export * from "./types";
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
- export { useDataViewsState } from "../../hooks/useDataViewsState";
36
- export type { UseDataViewsStateOptions } from "../../hooks/useDataViewsState";
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,3 @@
1
+ // The rail. `panel.tsx` assembles the compound surface; the rest is one component per file, the
2
+ // same shape as `views/`.
3
+ export { Panel } from "./panel";
@@ -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
+ }