torch-glare 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/apps/lib/components/BadgeField.tsx +2 -2
  2. package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
  3. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
  4. package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
  5. package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
  6. package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
  7. package/apps/lib/components/DataViews/InboxView.tsx +514 -0
  8. package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
  9. package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
  10. package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
  11. package/apps/lib/components/DataViews/TableView.tsx +232 -0
  12. package/apps/lib/components/DataViews/TreeView.tsx +363 -0
  13. package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
  14. package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
  15. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
  16. package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
  17. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
  18. package/apps/lib/components/DataViews/index.ts +30 -0
  19. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
  20. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
  21. package/apps/lib/components/DataViews/types.ts +177 -0
  22. package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
  23. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
  24. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
  25. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
  26. package/apps/lib/components/TreeFolder/icons.tsx +63 -0
  27. package/apps/lib/components/TreeFolder/index.ts +17 -0
  28. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
  29. package/apps/lib/components/TreeFolder/types.ts +68 -0
  30. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
  31. package/apps/lib/hooks/useDataViewsState.ts +169 -0
  32. package/apps/lib/hooks/useIsMobile.ts +21 -0
  33. package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
  34. package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
  35. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
  36. package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
  37. package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
  38. package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
  39. package/dist/bin/index.js +3 -3
  40. package/dist/bin/index.js.map +1 -1
  41. package/dist/src/commands/add.d.ts.map +1 -1
  42. package/dist/src/commands/add.js +29 -6
  43. package/dist/src/commands/add.js.map +1 -1
  44. package/dist/src/commands/utils.d.ts.map +1 -1
  45. package/dist/src/commands/utils.js +22 -2
  46. package/dist/src/commands/utils.js.map +1 -1
  47. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  48. package/dist/src/shared/copyComponentsRecursively.js +8 -1
  49. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  50. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
  51. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
  52. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
  53. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  54. package/docs/components/form-stepper.md +244 -0
  55. package/docs/components/stepper.md +215 -0
  56. package/docs/components/timeline.md +248 -0
  57. package/package.json +6 -6
  58. package/apps/lib/components/Charts-dev.tsx +0 -365
  59. package/apps/lib/components/Command-dev.tsx +0 -151
  60. package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
  61. /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
  62. /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
@@ -0,0 +1,242 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+ import { useMemo, useState } from "react"
5
+ import { Badge } from "../Badge"
6
+ import { Plus, MoreHorizontal } from "lucide-react"
7
+ import type {
8
+ DynamicRecord,
9
+ ViewConfig,
10
+ DynamicColumnConfig,
11
+ FieldConfig,
12
+ } from "./types"
13
+ import { Button } from "../Button"
14
+ import { Card, CardContent, CardHeader } from "../Card"
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 { resolveBadgeVariant } from "./badgeAdapter"
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
+ }
29
+
30
+ type KanbanColumn = {
31
+ id: string
32
+ title: string
33
+ color: string
34
+ items: DynamicRecord[]
35
+ }
36
+
37
+ const COLUMN_COLORS = [
38
+ "bg-background-presentation-badge-gray-primary",
39
+ "bg-background-presentation-badge-blue-primary",
40
+ "bg-background-presentation-badge-purple-primary",
41
+ "bg-background-presentation-badge-success-primary",
42
+ "bg-background-presentation-badge-yellow-primary",
43
+ "bg-background-presentation-badge-red-primary",
44
+ ]
45
+
46
+ function getId(item: DynamicRecord, fallbackPath: string | undefined, idx: number): any {
47
+ if (item?.id != null) return item.id
48
+ if (fallbackPath) {
49
+ const v = getByPath(item, fallbackPath)
50
+ if (v != null) return v
51
+ }
52
+ return idx
53
+ }
54
+
55
+ export function KanbanView({
56
+ data,
57
+ fields,
58
+ onDataUpdate,
59
+ groupByField = "status",
60
+ }: KanbanViewProps) {
61
+ const isMobile = useIsMobile()
62
+ const [draggedItem, setDraggedItem] = useState<{ item: DynamicRecord; columnId: string } | null>(null)
63
+
64
+ const displayFields = useMemo(
65
+ () => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
66
+ [fields],
67
+ )
68
+
69
+ const groupField = useMemo(
70
+ () => fields.find((f) => f.path === groupByField),
71
+ [fields, groupByField],
72
+ )
73
+
74
+ const kanbanColumns = useMemo<KanbanColumn[]>(() => {
75
+ const groups: Record<string, KanbanColumn> = {}
76
+
77
+ if (groupField?.variants) {
78
+ Object.keys(groupField.variants).forEach((value, index) => {
79
+ groups[value] = {
80
+ id: value,
81
+ title: value,
82
+ color: COLUMN_COLORS[index % COLUMN_COLORS.length],
83
+ items: [],
84
+ }
85
+ })
86
+ }
87
+
88
+ let nextColorIdx = Object.keys(groups).length
89
+ for (const item of data) {
90
+ const value = String(getByPath(item, groupByField) ?? "Uncategorized")
91
+ if (!groups[value]) {
92
+ groups[value] = {
93
+ id: value,
94
+ title: value,
95
+ color: COLUMN_COLORS[nextColorIdx++ % COLUMN_COLORS.length],
96
+ items: [],
97
+ }
98
+ }
99
+ groups[value].items.push(item)
100
+ }
101
+
102
+ return Object.values(groups)
103
+ }, [data, groupByField, groupField])
104
+
105
+ const handleDragStart = (item: DynamicRecord, columnId: string) => {
106
+ setDraggedItem({ item, columnId })
107
+ }
108
+
109
+ const handleDragOver = (e: React.DragEvent) => {
110
+ e.preventDefault()
111
+ }
112
+
113
+ const idPath = displayFields[0]?.path
114
+
115
+ const handleDrop = (targetColumnId: string) => {
116
+ if (!draggedItem) return
117
+ const draggedId = getId(draggedItem.item, idPath, -1)
118
+
119
+ const updatedData = data.map((item, idx) => {
120
+ const itemId = getId(item, idPath, idx)
121
+ if (itemId === draggedId) {
122
+ return setByPath(item, groupByField, targetColumnId)
123
+ }
124
+ return item
125
+ })
126
+
127
+ onDataUpdate?.(updatedData)
128
+ setDraggedItem(null)
129
+ }
130
+
131
+ const renderCard = (item: DynamicRecord, idx: number) => {
132
+ const itemId = getId(item, idPath, idx)
133
+ const titleField = displayFields[0]
134
+ const descField = displayFields[1]
135
+ const titleValue = titleField ? getByPath(item, titleField.path) : ""
136
+ const descValue = descField ? getByPath(item, descField.path) : null
137
+
138
+ return (
139
+ <Card
140
+ key={itemId}
141
+ draggable={!isMobile}
142
+ onDragStart={!isMobile ? () => handleDragStart(item, String(getByPath(item, groupByField) ?? "Uncategorized")) : undefined}
143
+ className={isMobile ? "cursor-pointer" : "cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow"}
144
+ >
145
+ <CardHeader>
146
+ <div className="flex items-start justify-between gap-2">
147
+ <div className="flex-1">
148
+ {titleField && renderField(titleValue, titleField, item)}
149
+ </div>
150
+ <Button variant="BorderStyle" buttonType="icon" className="h-6 w-6 -mt-1 -mr-1">
151
+ <MoreHorizontal className="h-3 w-3" />
152
+ </Button>
153
+ </div>
154
+ {descField && descValue != null && (
155
+ <div className="text-xs text-content-presentation-global-tertiary leading-relaxed">
156
+ {renderField(descValue, descField, item)}
157
+ </div>
158
+ )}
159
+ </CardHeader>
160
+ <CardContent className="space-y-3 pt-0">
161
+ <div className="space-y-2">
162
+ {displayFields.slice(2).map((field) => {
163
+ if (field.path === groupByField) return null
164
+ const value = getByPath(item, field.path)
165
+ if (value == null) return null
166
+ return (
167
+ <div key={field.path} className="flex items-center justify-between text-xs">
168
+ <span className="text-content-presentation-global-tertiary">{field.label}:</span>
169
+ {renderField(value, field, item)}
170
+ </div>
171
+ )
172
+ })}
173
+ </div>
174
+ </CardContent>
175
+ </Card>
176
+ )
177
+ }
178
+
179
+ if (isMobile) {
180
+ return (
181
+ <div className="h-full overflow-y-auto p-4 bg-background-presentation-body-primary">
182
+ <div className="flex flex-col gap-4">
183
+ {kanbanColumns.map((column) => (
184
+ <div key={column.id} className="flex flex-col gap-3">
185
+ <ColumnHeader column={column} />
186
+ <div className="flex flex-col gap-3">
187
+ {column.items.map((item, idx) => renderCard(item, idx))}
188
+ </div>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </div>
193
+ )
194
+ }
195
+
196
+ return (
197
+ <div className="h-full overflow-x-auto p-6 bg-background-presentation-body-primary">
198
+ <div className="flex h-full gap-4 pb-4" style={{ minWidth: "max-content" }}>
199
+ {kanbanColumns.map((column) => (
200
+ <div
201
+ key={column.id}
202
+ className="flex w-80 flex-col gap-3"
203
+ onDragOver={handleDragOver}
204
+ onDrop={() => handleDrop(column.id)}
205
+ >
206
+ <ColumnHeader column={column} />
207
+ <div className="flex flex-col gap-3 overflow-y-auto">
208
+ {column.items.map((item, idx) => renderCard(item, idx))}
209
+ </div>
210
+ </div>
211
+ ))}
212
+ </div>
213
+ </div>
214
+ )
215
+ }
216
+
217
+ function ColumnHeader({ column }: { column: KanbanColumn }) {
218
+ const countBadge = resolveBadgeVariant("gray")
219
+ return (
220
+ <div className="flex items-center justify-between rounded-lg p-3 border border-border-presentation-global-primary bg-background-presentation-body-overlay-primary">
221
+ <div className="flex items-center gap-2">
222
+ <div className={`h-2 w-2 rounded-full ${column.color}`} />
223
+ <h3 className="font-semibold text-content-presentation-global-primary">{column.title}</h3>
224
+ <Badge
225
+ {...countBadge}
226
+ label={String(column.items.length)}
227
+ className="h-5 rounded-full p-0 text-xs"
228
+ size="XS"
229
+
230
+ />
231
+ </div>
232
+ <div className="flex items-center gap-1">
233
+ <Button variant="BorderStyle" buttonType="icon" className="h-7 w-7">
234
+ <Plus className="h-4 w-4" />
235
+ </Button>
236
+ <Button variant="BorderStyle" buttonType="icon" className="h-7 w-7">
237
+ <MoreHorizontal className="h-4 w-4" />
238
+ </Button>
239
+ </div>
240
+ </div>
241
+ )
242
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4
+ import { Switch } from "../Switch";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ /**
8
+ * DataViews config-panel form controls.
9
+ *
10
+ * These are intentionally NOT shared library components. The panel chrome is
11
+ * always-dark (Figma `Cun` = #000000) and hardcodes Figma hex values, which
12
+ * conflicts with the design-system token / `data-theme` convention used by the
13
+ * public components. They live colocated with the panel so the always-dark
14
+ * Figma styling stays internal to DataViews. Not re-exported from index.ts.
15
+ */
16
+
17
+ /**
18
+ * Saved View / Default Sort radio row, built from the raw Radix primitive to
19
+ * match Figma node 1612:30021. The shared <Radio>/<Label> components impose
20
+ * their own circle size, color tokens, line-height reset and wrapper flex
21
+ * layout that fight this panel's always-dark Figma spec, so the row is
22
+ * hand-built here instead.
23
+ *
24
+ * The whole row IS the Radix Item, so the entire area (circle + label +
25
+ * padding) is one click target. The circle/label are non-interactive visuals.
26
+ *
27
+ */
28
+ export function RadioRow({ value, label }: { value: string; label: string }) {
29
+ return (
30
+ <RadioGroupPrimitive.Item
31
+ value={value}
32
+ className={cn(
33
+ "group flex w-full items-center gap-1.5 py-1 pl-2",
34
+ "cursor-pointer rounded-[8px] text-left outline-none transition-colors",
35
+ "hover:bg-white/[0.04] focus-visible:bg-white/[0.04]",
36
+ )}
37
+ >
38
+ <span
39
+ className={cn(
40
+ "flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full",
41
+ "border border-[#626467] bg-[rgba(255,255,255,0.05)] transition-colors",
42
+ "group-data-[state=checked]:border-transparent",
43
+ "group-data-[state=checked]:bg-[#005ECC]",
44
+ )}
45
+ >
46
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
47
+ <span className="h-[6px] w-[6px] rounded-full bg-white" />
48
+ </RadioGroupPrimitive.Indicator>
49
+ </span>
50
+ <span className="text-[14px] font-normal leading-[1.475] text-white">
51
+ {label}
52
+ </span>
53
+ </RadioGroupPrimitive.Item>
54
+ );
55
+ }
56
+
57
+ // Shared <Switch> with the bright-green checked track (#0AC713) from the Figma
58
+ // Switcher-1.0 "On" state, applied regardless of the panel's dark theme scope.
59
+ const SWITCH_GREEN =
60
+ "data-[state=checked]:bg-[#0AC713] data-[state=checked]:border-[#0AC713]";
61
+
62
+ type DataViewsSwitchProps = {
63
+ checked: boolean;
64
+ onCheckedChange: () => void;
65
+ };
66
+
67
+ /** Column show/hide toggle: the library <Switch> pre-styled to the panel's
68
+ * Figma green-checked spec. */
69
+ export function DataViewsSwitch({
70
+ checked,
71
+ onCheckedChange,
72
+ }: DataViewsSwitchProps) {
73
+ return (
74
+ <Switch
75
+ checked={checked}
76
+ onCheckedChange={onCheckedChange}
77
+ className={SWITCH_GREEN}
78
+ />
79
+ );
80
+ }
@@ -0,0 +1,285 @@
1
+ "use client"
2
+
3
+ import { useMemo, useState } from "react"
4
+ import { X, GripVertical, ArrowUp, ArrowDown, Minus } from "lucide-react"
5
+ import type {
6
+ ViewConfig,
7
+ ViewType,
8
+ FieldConfig,
9
+ } from "./types"
10
+ import { Button } from "../Button"
11
+ import { Switch } from "../Switch"
12
+ import { Divider } from "../Divider"
13
+ import { Label } from "../Label"
14
+ import { RadioGroup, Radio } from "../Radio"
15
+
16
+ type SettingsPanelProps = {
17
+ config: ViewConfig
18
+ onConfigChange: (config: Partial<ViewConfig>) => void
19
+ onClose: () => void
20
+ currentView: ViewType
21
+ fields: FieldConfig[]
22
+ }
23
+
24
+ export function SettingsPanel({
25
+ config,
26
+ onConfigChange,
27
+ onClose,
28
+ currentView,
29
+ fields,
30
+ }: SettingsPanelProps) {
31
+ const visibleFields = useMemo(
32
+ () => fields.filter((f) => f.type !== "hidden"),
33
+ [fields],
34
+ )
35
+
36
+ const groupableFields = useMemo(
37
+ () => fields.filter((f) => f.type === "enum-badge"),
38
+ [fields],
39
+ )
40
+
41
+ const visiblePaths = useMemo(
42
+ () => new Set(visibleFields.map((f) => f.path)),
43
+ [visibleFields],
44
+ )
45
+ const fieldByPath = useMemo(
46
+ () => new Map(visibleFields.map((f) => [f.path, f])),
47
+ [visibleFields],
48
+ )
49
+ const orderedColumns = useMemo(
50
+ () =>
51
+ [...config.tableColumns]
52
+ .filter((c) => visiblePaths.has(c.id))
53
+ .sort((a, b) => a.order - b.order),
54
+ [config.tableColumns, visiblePaths],
55
+ )
56
+
57
+ const toggleColumnVisibility = (path: string) => {
58
+ const next = config.tableColumns.map((c) =>
59
+ c.id === path ? { ...c, visible: !c.visible } : c,
60
+ )
61
+ onConfigChange({ tableColumns: next })
62
+ }
63
+
64
+ const [dragPath, setDragPath] = useState<string | null>(null)
65
+ const [dragOverPath, setDragOverPath] = useState<string | null>(null)
66
+
67
+ const reorderColumn = (sourcePath: string, targetPath: string) => {
68
+ if (sourcePath === targetPath) return
69
+ const ids = orderedColumns.map((c) => c.id)
70
+ const from = ids.indexOf(sourcePath)
71
+ const to = ids.indexOf(targetPath)
72
+ if (from === -1 || to === -1) return
73
+ const reordered = [...ids]
74
+ const [moved] = reordered.splice(from, 1)
75
+ reordered.splice(to, 0, moved)
76
+ const orderByPath = new Map(reordered.map((id, i) => [id, i]))
77
+ const next = config.tableColumns.map((c) => {
78
+ const newOrder = orderByPath.get(c.id)
79
+ return newOrder == null ? c : { ...c, order: newOrder }
80
+ })
81
+ onConfigChange({ tableColumns: next })
82
+ }
83
+
84
+ return (
85
+ <div className="w-80 border-l border-border-presentation-global-primary bg-background-presentation-body-overlay-primary overflow-y-auto">
86
+ <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">
87
+ <h2 className="font-semibold text-content-presentation-global-primary">Settings</h2>
88
+ <Button variant="BorderStyle" buttonType="icon" onClick={onClose} className="h-8 w-8">
89
+ <X className="h-4 w-4" />
90
+ </Button>
91
+ </div>
92
+
93
+ <div className="space-y-6 p-4">
94
+ <div className="space-y-3">
95
+ <h3 className="text-sm font-medium text-content-presentation-global-primary">General</h3>
96
+ <div className="space-y-3">
97
+ <div className="flex items-center justify-between">
98
+ <Label htmlFor="show-filters">Show Filters</Label>
99
+ <Switch
100
+ id="show-filters"
101
+ checked={config.showFilters}
102
+ onCheckedChange={(checked) => onConfigChange({ showFilters: checked })}
103
+ />
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <Divider />
109
+
110
+ {currentView === "table" && (
111
+ <>
112
+ <div className="space-y-3">
113
+ <h3 className="text-sm font-medium text-content-presentation-global-primary">Table Columns</h3>
114
+ <p className="text-xs text-content-presentation-global-tertiary">
115
+ Show or hide columns in table view
116
+ </p>
117
+ {orderedColumns.length === 0 ? (
118
+ <p className="text-xs text-content-presentation-global-tertiary">No fields detected.</p>
119
+ ) : (
120
+ <div className="space-y-2">
121
+ {orderedColumns.map((col) => {
122
+ const field = fieldByPath.get(col.id)
123
+ const isDragging = dragPath === col.id
124
+ const isDropTarget = dragOverPath === col.id && dragPath !== col.id
125
+ return (
126
+ <div
127
+ key={col.id}
128
+ draggable
129
+ onDragStart={(e) => {
130
+ setDragPath(col.id)
131
+ e.dataTransfer.effectAllowed = "move"
132
+ e.dataTransfer.setData("text/plain", col.id)
133
+ }}
134
+ onDragOver={(e) => {
135
+ e.preventDefault()
136
+ e.dataTransfer.dropEffect = "move"
137
+ if (dragOverPath !== col.id) setDragOverPath(col.id)
138
+ }}
139
+ onDragLeave={() => {
140
+ if (dragOverPath === col.id) setDragOverPath(null)
141
+ }}
142
+ onDrop={(e) => {
143
+ e.preventDefault()
144
+ if (dragPath) reorderColumn(dragPath, col.id)
145
+ setDragPath(null)
146
+ setDragOverPath(null)
147
+ }}
148
+ onDragEnd={() => {
149
+ setDragPath(null)
150
+ setDragOverPath(null)
151
+ }}
152
+ className={
153
+ "flex items-center gap-2 rounded-lg border p-2 cursor-grab active:cursor-grabbing transition-colors " +
154
+ (isDragging
155
+ ? "opacity-50 border-border-presentation-global-primary "
156
+ : isDropTarget
157
+ ? "border-content-presentation-action-light-primary bg-background-presentation-form-field-primary "
158
+ : "border-border-presentation-global-primary ")
159
+ }
160
+ >
161
+ <GripVertical className="h-4 w-4 text-content-presentation-global-tertiary" />
162
+ <span className="flex-1 text-sm text-content-presentation-global-primary">
163
+ {col.label || field?.label || col.id}
164
+ </span>
165
+ <Switch
166
+ checked={col.visible}
167
+ onCheckedChange={() => toggleColumnVisibility(col.id)}
168
+ />
169
+ </div>
170
+ )
171
+ })}
172
+ </div>
173
+ )}
174
+ </div>
175
+ <Divider />
176
+ </>
177
+ )}
178
+
179
+ {currentView === "kanban" && (
180
+ <>
181
+ <div className="space-y-3">
182
+ <h3 className="text-sm font-medium text-content-presentation-global-primary">Kanban Grouping</h3>
183
+ <p className="text-xs text-content-presentation-global-tertiary">Group cards by field</p>
184
+ {groupableFields.length === 0 ? (
185
+ <p className="text-xs text-content-presentation-global-tertiary">
186
+ No groupable fields detected. Declare a field with type "enum-badge" to enable grouping.
187
+ </p>
188
+ ) : (
189
+ <RadioGroup
190
+ value={config.kanbanGroupBy}
191
+ onValueChange={(value) => onConfigChange({ kanbanGroupBy: value })}
192
+ >
193
+ {groupableFields.map((field) => (
194
+ <div key={field.path} className="flex items-center space-x-2">
195
+ <Radio value={field.path} id={`group-${field.path}`} />
196
+ <Label htmlFor={`group-${field.path}`}>{field.label ?? field.path}</Label>
197
+ </div>
198
+ ))}
199
+ </RadioGroup>
200
+ )}
201
+ </div>
202
+ <Divider />
203
+ </>
204
+ )}
205
+
206
+ {currentView === "inbox" && (
207
+ <>
208
+ <div className="space-y-3">
209
+ <h3 className="text-sm font-medium text-content-presentation-global-primary">Inbox Layout</h3>
210
+ <div className="flex items-center justify-between">
211
+ <Label htmlFor="preview-pane" className="text-sm">
212
+ Show Preview Pane
213
+ </Label>
214
+ <Switch
215
+ id="preview-pane"
216
+ checked={config.showPreviewPane}
217
+ onCheckedChange={(checked) => onConfigChange({ showPreviewPane: checked })}
218
+ />
219
+ </div>
220
+ </div>
221
+ <Divider />
222
+ </>
223
+ )}
224
+
225
+ {orderedColumns.length > 0 && (
226
+ <div className="space-y-3">
227
+ <h3 className="text-sm font-medium text-content-presentation-global-primary">Sort</h3>
228
+ <p className="text-xs text-content-presentation-global-tertiary">
229
+ Pick a column and direction. Only one column sorts at a time.
230
+ </p>
231
+ <div className="space-y-2">
232
+ {orderedColumns.map((col) => {
233
+ const field = fieldByPath.get(col.id)
234
+ const isActive = config.sortBy === col.id
235
+ const dir: "asc" | "desc" | "none" = isActive ? config.sortOrder : "none"
236
+ const setDir = (next: "asc" | "desc" | "none") => {
237
+ if (next === "none") {
238
+ onConfigChange({ sortBy: "" })
239
+ } else {
240
+ onConfigChange({ sortBy: col.id, sortOrder: next })
241
+ }
242
+ }
243
+ const btn = (
244
+ mode: "none" | "asc" | "desc",
245
+ Icon: typeof Minus,
246
+ label: string,
247
+ ) => (
248
+ <button
249
+ type="button"
250
+ aria-label={`${label} ${col.label || col.id}`}
251
+ aria-pressed={dir === mode}
252
+ onClick={() => setDir(mode)}
253
+ className={
254
+ "flex h-7 w-7 items-center justify-center rounded-md border transition-colors " +
255
+ (dir === mode
256
+ ? "border-content-presentation-action-light-primary bg-background-presentation-form-field-primary text-content-presentation-global-primary"
257
+ : "border-border-presentation-global-primary text-content-presentation-global-tertiary hover:text-content-presentation-global-primary")
258
+ }
259
+ >
260
+ <Icon className="h-3.5 w-3.5" />
261
+ </button>
262
+ )
263
+ return (
264
+ <div
265
+ key={col.id}
266
+ className="flex items-center gap-2 rounded-lg border border-border-presentation-global-primary p-2"
267
+ >
268
+ <span className="flex-1 text-sm text-content-presentation-global-primary">
269
+ {col.label || field?.label || col.id}
270
+ </span>
271
+ <div className="flex items-center gap-1">
272
+ {btn("none", Minus, "No sort")}
273
+ {btn("asc", ArrowUp, "Sort ascending")}
274
+ {btn("desc", ArrowDown, "Sort descending")}
275
+ </div>
276
+ </div>
277
+ )
278
+ })}
279
+ </div>
280
+ </div>
281
+ )}
282
+ </div>
283
+ </div>
284
+ )
285
+ }