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.
- package/apps/lib/components/BadgeField.tsx +2 -2
- package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
- package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
- package/apps/lib/components/DataViews/InboxView.tsx +514 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
- package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
- package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
- package/apps/lib/components/DataViews/TableView.tsx +232 -0
- package/apps/lib/components/DataViews/TreeView.tsx +363 -0
- package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
- package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
- package/apps/lib/components/DataViews/index.ts +30 -0
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
- package/apps/lib/components/DataViews/types.ts +177 -0
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
- package/apps/lib/components/TreeFolder/icons.tsx +63 -0
- package/apps/lib/components/TreeFolder/index.ts +17 -0
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
- package/apps/lib/components/TreeFolder/types.ts +68 -0
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
- package/apps/lib/hooks/useDataViewsState.ts +169 -0
- package/apps/lib/hooks/useIsMobile.ts +21 -0
- package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
- package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
- package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
- package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
- package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
- package/dist/bin/index.js +3 -3
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/add.d.ts.map +1 -1
- package/dist/src/commands/add.js +29 -6
- package/dist/src/commands/add.js.map +1 -1
- package/dist/src/commands/utils.d.ts.map +1 -1
- package/dist/src/commands/utils.js +22 -2
- package/dist/src/commands/utils.js.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.js +8 -1
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
- package/docs/components/form-stepper.md +244 -0
- package/docs/components/stepper.md +215 -0
- package/docs/components/timeline.md +248 -0
- package/package.json +6 -6
- package/apps/lib/components/Charts-dev.tsx +0 -365
- package/apps/lib/components/Command-dev.tsx +0 -151
- package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
- /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
- /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
|
@@ -0,0 +1,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
|
+
}
|