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,336 +0,0 @@
1
- "use client";
2
-
3
- import type React from "react";
4
- import { Fragment, useMemo, useState } from "react";
5
- import { MoreHorizontal } from "lucide-react";
6
- import type {
7
- DynamicRecord,
8
- ViewConfig,
9
- DynamicColumnConfig,
10
- FieldConfig,
11
- KanbanColumnColor,
12
- } from "./types";
13
- import { Button } from "../Button";
14
- import { DataViewCard, type DataViewCardRow } from "../../layouts/DataViewCard";
15
- import { getByPath, setByPath } from "../../utils/dataViews/pathUtils";
16
- import { renderField } from "./fieldRenderers";
17
- import { visibleFields } from "../../utils/dataViews/fieldUtils";
18
- import { useIsMobile } from "../../hooks/useIsMobile";
19
- import { cn } from "../../utils/cn";
20
-
21
- export type KanbanViewProps = {
22
- data: DynamicRecord[];
23
- columns?: DynamicColumnConfig[];
24
- fields: FieldConfig[];
25
- config: ViewConfig;
26
- onDataUpdate?: (data: DynamicRecord[]) => void;
27
- groupByField?: string;
28
- // Path of the field to render as the card title. Defaults to the first
29
- // visible non-group-by field. Use this to opt out of the "first field wins"
30
- // heuristic when consumers want a specific field (e.g. "name", "id").
31
- titleField?: string;
32
- // Click handler for the column header's overflow button. When omitted the
33
- // button is hidden so app-less columns stay clean.
34
- onColumnAction?: (columnId: string) => void;
35
- };
36
-
37
- const COLUMN_PALETTE: readonly KanbanColumnColor[] = [
38
- "gray",
39
- "purple",
40
- "orange",
41
- "blue",
42
- "green",
43
- "red",
44
- ] as const;
45
- type ColumnColor = KanbanColumnColor;
46
-
47
- // Figma kanban header pills use deeply saturated dark fills (#131415, #330C69,
48
- // #532200, #002F66). We match each to the closest existing raw-color token in
49
- // `glare-torch-mode`. Purple has no presentation-layer match close enough, so
50
- // we use the exact Figma hex inline.
51
- const COLUMN_BG: Record<ColumnColor, string> = {
52
- gray: "bg-black-900",
53
- purple: "bg-[#330C69]",
54
- orange: "bg-orange-900",
55
- blue: "bg-blue-sparkle-900",
56
- green: "bg-green-cyan-900",
57
- red: "bg-red-orange-900",
58
- };
59
-
60
- const colorIndexFor = (key: string) => {
61
- let h = 0;
62
- for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0;
63
- return Math.abs(h) % COLUMN_PALETTE.length;
64
- };
65
-
66
- type KanbanColumn = {
67
- id: string;
68
- title: string;
69
- color: ColumnColor;
70
- items: DynamicRecord[];
71
- };
72
-
73
- function getId(item: DynamicRecord, fallbackPath: string | undefined, idx: number): unknown {
74
- if (item?.id != null) return item.id;
75
- if (fallbackPath) {
76
- const v = getByPath(item, fallbackPath);
77
- if (v != null) return v;
78
- }
79
- return idx;
80
- }
81
-
82
- export function KanbanView({
83
- data,
84
- fields,
85
- onDataUpdate,
86
- groupByField = "status",
87
- titleField,
88
- onColumnAction,
89
- }: KanbanViewProps) {
90
- const isMobile = useIsMobile();
91
- const [draggedItem, setDraggedItem] = useState<{
92
- item: DynamicRecord;
93
- columnId: string;
94
- } | null>(null);
95
- const [dragOverColumnId, setDragOverColumnId] = useState<string | null>(null);
96
-
97
- const displayFields = useMemo(
98
- () => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
99
- [fields],
100
- );
101
-
102
- const groupField = useMemo(
103
- () => fields.find((f) => f.path === groupByField),
104
- [fields, groupByField],
105
- );
106
-
107
- const kanbanColumns = useMemo<KanbanColumn[]>(() => {
108
- const groups: Record<string, KanbanColumn> = {};
109
- const overrides = groupField?.kanbanVariants;
110
-
111
- // Resolve a column's visible title + pill color. Consumer-supplied
112
- // `kanbanVariants[key]` wins; otherwise fall back to the raw key and the
113
- // palette rotation.
114
- const resolve = (key: string, paletteIdx: number) => ({
115
- title: overrides?.[key]?.label ?? key,
116
- color: overrides?.[key]?.color ?? COLUMN_PALETTE[paletteIdx % COLUMN_PALETTE.length],
117
- });
118
-
119
- if (groupField?.variants) {
120
- Object.keys(groupField.variants).forEach((value, index) => {
121
- groups[value] = {
122
- id: value,
123
- ...resolve(value, index),
124
- items: [],
125
- };
126
- });
127
- }
128
-
129
- for (const item of data) {
130
- const value = String(getByPath(item, groupByField) ?? "Uncategorized");
131
- if (!groups[value]) {
132
- groups[value] = {
133
- id: value,
134
- ...resolve(value, colorIndexFor(value)),
135
- items: [],
136
- };
137
- }
138
- groups[value].items.push(item);
139
- }
140
-
141
- return Object.values(groups);
142
- }, [data, groupByField, groupField]);
143
-
144
- const handleDragStart = (item: DynamicRecord, columnId: string) => {
145
- setDraggedItem({ item, columnId });
146
- };
147
-
148
- const handleDragOver = (e: React.DragEvent, columnId: string) => {
149
- e.preventDefault();
150
- e.dataTransfer.dropEffect = "move";
151
- if (dragOverColumnId !== columnId) setDragOverColumnId(columnId);
152
- };
153
-
154
- const handleDragLeave = (e: React.DragEvent, columnId: string) => {
155
- // Only clear when the pointer actually exits this column — moving over a
156
- // child element fires dragleave on the parent before dragenter on the child.
157
- if (e.currentTarget.contains(e.relatedTarget as Node)) return;
158
- if (dragOverColumnId === columnId) setDragOverColumnId(null);
159
- };
160
-
161
- const handleDragEnd = () => {
162
- setDraggedItem(null);
163
- setDragOverColumnId(null);
164
- };
165
-
166
- const idPath = displayFields[0]?.path;
167
-
168
- const handleDrop = (targetColumnId: string) => {
169
- if (!draggedItem) {
170
- setDragOverColumnId(null);
171
- return;
172
- }
173
- const draggedId = getId(draggedItem.item, idPath, -1);
174
-
175
- const updatedData = data.map((item, idx) => {
176
- const itemId = getId(item, idPath, idx);
177
- if (itemId === draggedId) {
178
- return setByPath(item, groupByField, targetColumnId);
179
- }
180
- return item;
181
- });
182
-
183
- onDataUpdate?.(updatedData);
184
- setDraggedItem(null);
185
- setDragOverColumnId(null);
186
- };
187
-
188
- // Resolve the title field: consumer-supplied `titleField` wins, else fall
189
- // back to the first visible non-group-by field.
190
- const resolvedTitleField = useMemo(() => {
191
- if (titleField) return displayFields.find((f) => f.path === titleField);
192
- return displayFields.find((f) => f.path !== groupByField);
193
- }, [displayFields, titleField, groupByField]);
194
-
195
- const renderCard = (item: DynamicRecord, idx: number) => {
196
- const itemId = getId(item, idPath, idx);
197
- const isDraggingThis = draggedItem != null && getId(draggedItem.item, idPath, -1) === itemId;
198
- const titleFieldResolved = resolvedTitleField;
199
- const titleValue = titleFieldResolved ? getByPath(item, titleFieldResolved.path) : "";
200
- const bodyFields = displayFields.filter(
201
- (f) => f.path !== groupByField && f.path !== titleFieldResolved?.path,
202
- );
203
-
204
- // Pair body fields two-per-row so the grid keeps its alternating rhythm
205
- // even when one side is missing. If a pair has only one non-null value, the
206
- // surviving cell spans both columns. Fully empty pairs are dropped so we
207
- // don't render a phantom row with only hairlines.
208
- const rows: DataViewCardRow[] = [];
209
- for (let i = 0; i < bodyFields.length; i += 2) {
210
- const left = bodyFields[i];
211
- const right = bodyFields[i + 1];
212
- const leftValue = left ? getByPath(item, left.path) : null;
213
- const rightValue = right ? getByPath(item, right.path) : null;
214
- const cells: DataViewCardRow = [];
215
- if (left && leftValue != null) {
216
- cells.push({
217
- key: left.path,
218
- label: left.label,
219
- value: renderField(leftValue, left, item),
220
- });
221
- }
222
- if (right && rightValue != null) {
223
- cells.push({
224
- key: right.path,
225
- label: right.label,
226
- value: renderField(rightValue, right, item),
227
- });
228
- }
229
- if (cells.length > 0) rows.push(cells);
230
- }
231
-
232
- return (
233
- <DataViewCard
234
- key={itemId as string | number}
235
- draggable={!isMobile}
236
- onDragStart={
237
- !isMobile
238
- ? () => handleDragStart(item, String(getByPath(item, groupByField) ?? "Uncategorized"))
239
- : undefined
240
- }
241
- onDragEnd={!isMobile ? handleDragEnd : undefined}
242
- className={cn(
243
- isMobile
244
- ? "cursor-pointer"
245
- : "cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow",
246
- !isMobile && isDraggingThis && "opacity-40",
247
- )}
248
- title={titleFieldResolved && renderField(titleValue, titleFieldResolved, item)}
249
- rows={rows}
250
- />
251
- );
252
- };
253
-
254
- if (isMobile) {
255
- return (
256
- <div className="h-full overflow-y-auto p-4 bg-background-presentation-body-primary">
257
- <div className="flex flex-col gap-4">
258
- {kanbanColumns.map((column) => (
259
- <div key={column.id} className="flex flex-col gap-3">
260
- <ColumnHeader column={column} onAction={onColumnAction} />
261
- <div className="flex flex-col gap-3">
262
- {column.items.map((item, idx) => renderCard(item, idx))}
263
- </div>
264
- </div>
265
- ))}
266
- </div>
267
- </div>
268
- );
269
- }
270
-
271
- return (
272
- <div className="h-full overflow-x-auto p-2 bg-background-presentation-body-primary">
273
- <div className="flex h-full gap-4" style={{ minWidth: "max-content" }}>
274
- {kanbanColumns.map((column, i) => {
275
- const isDropTarget = draggedItem != null && dragOverColumnId === column.id;
276
- return (
277
- <Fragment key={column.id}>
278
- <div
279
- className={cn(
280
- "flex w-[279px] flex-col gap-2 rounded-[12px] p-1 transition-colors duration-150 ease-in-out border-2 border-transparent",
281
- isDropTarget &&
282
- "bg-background-presentation-cardbutton-blue-hover border-dashed border-border-presentation-state-focus",
283
- )}
284
- onDragOver={(e) => handleDragOver(e, column.id)}
285
- onDragLeave={(e) => handleDragLeave(e, column.id)}
286
- onDrop={() => handleDrop(column.id)}
287
- >
288
- <ColumnHeader column={column} onAction={onColumnAction} />
289
- <div className="flex flex-col gap-2 overflow-y-auto py-1">
290
- {column.items.map((item, idx) => renderCard(item, idx))}
291
- </div>
292
- </div>
293
- {i < kanbanColumns.length - 1 && (
294
- <div
295
- aria-hidden
296
- className="self-stretch mt-[42px] border-dashed border-l-[2px] border-border-presentation-global-primary"
297
- />
298
- )}
299
- </Fragment>
300
- );
301
- })}
302
- </div>
303
- </div>
304
- );
305
- }
306
-
307
- function ColumnHeader({
308
- column,
309
- onAction,
310
- }: {
311
- column: KanbanColumn;
312
- onAction?: (columnId: string) => void;
313
- }) {
314
- return (
315
- <div
316
- className={cn(
317
- "flex items-center justify-between rounded-[8px] px-[6px] py-[4px]",
318
- COLUMN_BG[column.color],
319
- )}
320
- >
321
- <h3 className="typography-headers-small-medium text-content-presentation-global-primary-light">
322
- {column.title}
323
- </h3>
324
- {onAction && (
325
- <Button
326
- variant="BorderStyle"
327
- buttonType="icon"
328
- className="h-5 w-5 border-0 bg-transparent text-content-presentation-global-primary-light hover:bg-white/10"
329
- onClick={() => onAction(column.id)}
330
- >
331
- <MoreHorizontal className="h-3.5 w-3.5" />
332
- </Button>
333
- )}
334
- </div>
335
- );
336
- }
@@ -1,39 +0,0 @@
1
- "use client";
2
-
3
- import { Switch } from "../Switch";
4
- import { DataViewRadio } from "./DataViewRadio";
5
-
6
- /**
7
- * DataViews config-panel form controls.
8
- *
9
- * The config panel is wrapped in `data-theme="dark"`, so theme-aware
10
- * components (DataViewRadio, Switch) render in dark mode automatically. The
11
- * green-checked Switch hex is the only thing this panel still hardcodes
12
- * because it sits outside the theme system.
13
- */
14
-
15
- /**
16
- * Saved View / Default Sort radio row.
17
- *
18
- * Thin wrapper around the reusable {@link DataViewRadio} so the config panel
19
- * keeps a stable name. The whole row IS the Radix Item, so the entire area
20
- * (circle + label + padding) is one click target.
21
- */
22
- export function RadioRow({ value, label }: { value: string; label: string }) {
23
- return <DataViewRadio value={value} label={label} />;
24
- }
25
-
26
- // Shared <Switch> with the bright-green checked track (#0AC713) from the Figma
27
- // Switcher-1.0 "On" state, applied regardless of the panel's dark theme scope.
28
- const SWITCH_GREEN = "data-[state=checked]:bg-[#0AC713] data-[state=checked]:border-[#0AC713]";
29
-
30
- type DataViewsSwitchProps = {
31
- checked: boolean;
32
- onCheckedChange: () => void;
33
- };
34
-
35
- /** Column show/hide toggle: the library <Switch> pre-styled to the panel's
36
- * Figma green-checked spec. */
37
- export function DataViewsSwitch({ checked, onCheckedChange }: DataViewsSwitchProps) {
38
- return <Switch checked={checked} onCheckedChange={onCheckedChange} className={SWITCH_GREEN} />;
39
- }
@@ -1,279 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
- import { X, GripVertical, ArrowUp, ArrowDown, Minus } from "lucide-react";
5
- import type { ViewConfig, ViewType, FieldConfig } from "./types";
6
- import { Button } from "../Button";
7
- import { Switch } from "../Switch";
8
- import { Divider } from "../Divider";
9
- import { Label } from "../Label";
10
- import { RadioGroup, Radio } from "../Radio";
11
-
12
- type SettingsPanelProps = {
13
- config: ViewConfig;
14
- onConfigChange: (config: Partial<ViewConfig>) => void;
15
- onClose: () => void;
16
- currentView: ViewType;
17
- fields: FieldConfig[];
18
- };
19
-
20
- export function SettingsPanel({
21
- config,
22
- onConfigChange,
23
- onClose,
24
- currentView,
25
- fields,
26
- }: SettingsPanelProps) {
27
- const visibleFields = useMemo(() => fields.filter((f) => f.type !== "hidden"), [fields]);
28
-
29
- const groupableFields = useMemo(() => fields.filter((f) => f.type === "enum-badge"), [fields]);
30
-
31
- const visiblePaths = useMemo(() => new Set(visibleFields.map((f) => f.path)), [visibleFields]);
32
- const fieldByPath = useMemo(
33
- () => new Map(visibleFields.map((f) => [f.path, f])),
34
- [visibleFields],
35
- );
36
- const orderedColumns = useMemo(
37
- () =>
38
- [...config.tableColumns]
39
- .filter((c) => visiblePaths.has(c.id))
40
- .sort((a, b) => a.order - b.order),
41
- [config.tableColumns, visiblePaths],
42
- );
43
-
44
- const toggleColumnVisibility = (path: string) => {
45
- const next = config.tableColumns.map((c) =>
46
- c.id === path ? { ...c, visible: !c.visible } : c,
47
- );
48
- onConfigChange({ tableColumns: next });
49
- };
50
-
51
- const [dragPath, setDragPath] = useState<string | null>(null);
52
- const [dragOverPath, setDragOverPath] = useState<string | null>(null);
53
-
54
- const reorderColumn = (sourcePath: string, targetPath: string) => {
55
- if (sourcePath === targetPath) return;
56
- const ids = orderedColumns.map((c) => c.id);
57
- const from = ids.indexOf(sourcePath);
58
- const to = ids.indexOf(targetPath);
59
- if (from === -1 || to === -1) return;
60
- const reordered = [...ids];
61
- const [moved] = reordered.splice(from, 1);
62
- reordered.splice(to, 0, moved);
63
- const orderByPath = new Map(reordered.map((id, i) => [id, i]));
64
- const next = config.tableColumns.map((c) => {
65
- const newOrder = orderByPath.get(c.id);
66
- return newOrder == null ? c : { ...c, order: newOrder };
67
- });
68
- onConfigChange({ tableColumns: next });
69
- };
70
-
71
- return (
72
- <div className="w-80 border-l border-border-presentation-global-primary bg-background-presentation-body-overlay-primary overflow-y-auto">
73
- <div className="sticky top-0 z-10 flex items-center justify-between border-b border-border-presentation-global-primary bg-background-presentation-body-overlay-primary p-4">
74
- <h2 className="font-semibold text-content-presentation-global-primary">Settings</h2>
75
- <Button variant="BorderStyle" buttonType="icon" onClick={onClose} className="h-8 w-8">
76
- <X className="h-4 w-4" />
77
- </Button>
78
- </div>
79
-
80
- <div className="space-y-6 p-4">
81
- <div className="space-y-3">
82
- <h3 className="text-sm font-medium text-content-presentation-global-primary">General</h3>
83
- <div className="space-y-3">
84
- <div className="flex items-center justify-between">
85
- <Label htmlFor="show-filters">Show Filters</Label>
86
- <Switch
87
- id="show-filters"
88
- checked={config.showFilters}
89
- onCheckedChange={(checked) => onConfigChange({ showFilters: checked })}
90
- />
91
- </div>
92
- </div>
93
- </div>
94
-
95
- <Divider />
96
-
97
- {currentView === "table" && (
98
- <>
99
- <div className="space-y-3">
100
- <h3 className="text-sm font-medium text-content-presentation-global-primary">
101
- Table Columns
102
- </h3>
103
- <p className="text-xs text-content-presentation-global-tertiary">
104
- Show or hide columns in table view
105
- </p>
106
- {orderedColumns.length === 0 ? (
107
- <p className="text-xs text-content-presentation-global-tertiary">
108
- No fields detected.
109
- </p>
110
- ) : (
111
- <div className="space-y-2">
112
- {orderedColumns.map((col) => {
113
- const field = fieldByPath.get(col.id);
114
- const isDragging = dragPath === col.id;
115
- const isDropTarget = dragOverPath === col.id && dragPath !== col.id;
116
- return (
117
- <div
118
- key={col.id}
119
- draggable
120
- onDragStart={(e) => {
121
- setDragPath(col.id);
122
- e.dataTransfer.effectAllowed = "move";
123
- e.dataTransfer.setData("text/plain", col.id);
124
- }}
125
- onDragOver={(e) => {
126
- e.preventDefault();
127
- e.dataTransfer.dropEffect = "move";
128
- if (dragOverPath !== col.id) setDragOverPath(col.id);
129
- }}
130
- onDragLeave={() => {
131
- if (dragOverPath === col.id) setDragOverPath(null);
132
- }}
133
- onDrop={(e) => {
134
- e.preventDefault();
135
- if (dragPath) reorderColumn(dragPath, col.id);
136
- setDragPath(null);
137
- setDragOverPath(null);
138
- }}
139
- onDragEnd={() => {
140
- setDragPath(null);
141
- setDragOverPath(null);
142
- }}
143
- className={
144
- "flex items-center gap-2 rounded-lg border p-2 cursor-grab active:cursor-grabbing transition-colors " +
145
- (isDragging
146
- ? "opacity-50 border-border-presentation-global-primary "
147
- : isDropTarget
148
- ? "border-content-presentation-action-light-primary bg-background-presentation-form-field-primary "
149
- : "border-border-presentation-global-primary ")
150
- }
151
- >
152
- <GripVertical className="h-4 w-4 text-content-presentation-global-tertiary" />
153
- <span className="flex-1 text-sm text-content-presentation-global-primary">
154
- {col.label || field?.label || col.id}
155
- </span>
156
- <Switch
157
- checked={col.visible}
158
- onCheckedChange={() => toggleColumnVisibility(col.id)}
159
- />
160
- </div>
161
- );
162
- })}
163
- </div>
164
- )}
165
- </div>
166
- <Divider />
167
- </>
168
- )}
169
-
170
- {currentView === "kanban" && (
171
- <>
172
- <div className="space-y-3">
173
- <h3 className="text-sm font-medium text-content-presentation-global-primary">
174
- Kanban Grouping
175
- </h3>
176
- <p className="text-xs text-content-presentation-global-tertiary">
177
- Group cards by field
178
- </p>
179
- {groupableFields.length === 0 ? (
180
- <p className="text-xs text-content-presentation-global-tertiary">
181
- No groupable fields detected. Declare a field with type &quot;enum-badge&quot; to
182
- enable grouping.
183
- </p>
184
- ) : (
185
- <RadioGroup
186
- value={config.kanbanGroupBy}
187
- onValueChange={(value) => onConfigChange({ kanbanGroupBy: value })}
188
- >
189
- {groupableFields.map((field) => (
190
- <div key={field.path} className="flex items-center space-x-2">
191
- <Radio value={field.path} id={`group-${field.path}`} />
192
- <Label htmlFor={`group-${field.path}`}>{field.label ?? field.path}</Label>
193
- </div>
194
- ))}
195
- </RadioGroup>
196
- )}
197
- </div>
198
- <Divider />
199
- </>
200
- )}
201
-
202
- {currentView === "inbox" && (
203
- <>
204
- <div className="space-y-3">
205
- <h3 className="text-sm font-medium text-content-presentation-global-primary">
206
- Inbox Layout
207
- </h3>
208
- <div className="flex items-center justify-between">
209
- <Label htmlFor="preview-pane" className="text-sm">
210
- Show Preview Pane
211
- </Label>
212
- <Switch
213
- id="preview-pane"
214
- checked={config.showPreviewPane}
215
- onCheckedChange={(checked) => onConfigChange({ showPreviewPane: checked })}
216
- />
217
- </div>
218
- </div>
219
- <Divider />
220
- </>
221
- )}
222
-
223
- {orderedColumns.length > 0 && (
224
- <div className="space-y-3">
225
- <h3 className="text-sm font-medium text-content-presentation-global-primary">Sort</h3>
226
- <p className="text-xs text-content-presentation-global-tertiary">
227
- Pick a column and direction. Only one column sorts at a time.
228
- </p>
229
- <div className="space-y-2">
230
- {orderedColumns.map((col) => {
231
- const field = fieldByPath.get(col.id);
232
- const isActive = config.sortBy === col.id;
233
- const dir: "asc" | "desc" | "none" = isActive ? config.sortOrder : "none";
234
- const setDir = (next: "asc" | "desc" | "none") => {
235
- if (next === "none") {
236
- onConfigChange({ sortBy: "" });
237
- } else {
238
- onConfigChange({ sortBy: col.id, sortOrder: next });
239
- }
240
- };
241
- const btn = (mode: "none" | "asc" | "desc", Icon: typeof Minus, label: string) => (
242
- <button
243
- type="button"
244
- aria-label={`${label} ${col.label || col.id}`}
245
- aria-pressed={dir === mode}
246
- onClick={() => setDir(mode)}
247
- className={
248
- "flex h-7 w-7 items-center justify-center rounded-md border transition-colors " +
249
- (dir === mode
250
- ? "border-content-presentation-action-light-primary bg-background-presentation-form-field-primary text-content-presentation-global-primary"
251
- : "border-border-presentation-global-primary text-content-presentation-global-tertiary hover:text-content-presentation-global-primary")
252
- }
253
- >
254
- <Icon className="h-3.5 w-3.5" />
255
- </button>
256
- );
257
- return (
258
- <div
259
- key={col.id}
260
- className="flex items-center gap-2 rounded-lg border border-border-presentation-global-primary p-2"
261
- >
262
- <span className="flex-1 text-sm text-content-presentation-global-primary">
263
- {col.label || field?.label || col.id}
264
- </span>
265
- <div className="flex items-center gap-1">
266
- {btn("none", Minus, "No sort")}
267
- {btn("asc", ArrowUp, "Sort ascending")}
268
- {btn("desc", ArrowDown, "Sort descending")}
269
- </div>
270
- </div>
271
- );
272
- })}
273
- </div>
274
- </div>
275
- )}
276
- </div>
277
- </div>
278
- );
279
- }