torch-glare 2.4.5 → 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 (142) 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 +7 -6
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +1 -1
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +55 -3
  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/TabSwitch.tsx +16 -4
  48. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  49. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  50. package/apps/lib/components/TreeFolder/index.ts +1 -1
  51. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  52. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  53. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  54. package/apps/lib/registry.json +159 -4
  55. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  56. package/apps/lib/utils/dataViews/path.ts +67 -0
  57. package/apps/lib/utils/dataViews/query.ts +73 -0
  58. package/apps/lib/utils/dataViews/types.ts +187 -0
  59. package/docs/components/breadcrumb.md +1 -1
  60. package/docs/components/button-group.md +1 -1
  61. package/docs/components/button.md +1 -1
  62. package/docs/components/card.md +1 -1
  63. package/docs/components/checkbox.md +1 -1
  64. package/docs/components/data-views/backend-response.md +324 -0
  65. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  66. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  67. package/docs/components/data-views/examples/fields.md +362 -0
  68. package/docs/components/data-views/examples/filters.md +308 -0
  69. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  70. package/docs/components/data-views/examples/index.md +29 -0
  71. package/docs/components/data-views/examples/overview.md +244 -0
  72. package/docs/components/data-views/examples/panel.md +212 -0
  73. package/docs/components/data-views/examples/scale.md +231 -0
  74. package/docs/components/data-views/examples/server-side.md +210 -0
  75. package/docs/components/data-views/examples/state.md +250 -0
  76. package/docs/components/data-views/examples/tree-custom.md +388 -0
  77. package/docs/components/data-views/examples/view-registry.md +313 -0
  78. package/docs/components/data-views/examples/views.md +534 -0
  79. package/docs/components/data-views/guide.md +405 -0
  80. package/docs/components/data-views/index.md +1504 -0
  81. package/docs/components/data-views/migration.md +79 -0
  82. package/docs/components/date-picker.md +0 -1
  83. package/docs/components/form-builder.md +3 -2
  84. package/docs/components/form-renderer.md +2 -1
  85. package/docs/components/form.md +1 -1
  86. package/docs/components/input-field.md +1 -1
  87. package/docs/components/input-otp.md +1 -1
  88. package/docs/components/input.md +1 -1
  89. package/docs/components/labeled-check-box.md +1 -1
  90. package/docs/components/labeled-radio.md +1 -1
  91. package/docs/components/radio-card.md +1 -1
  92. package/docs/components/radio.md +1 -1
  93. package/docs/components/search-field.md +1 -1
  94. package/docs/components/section-block.md +1 -1
  95. package/docs/components/select.md +1 -1
  96. package/docs/components/simple-select.md +1 -1
  97. package/docs/components/switch.md +1 -1
  98. package/docs/components/tab-switch.md +1 -1
  99. package/docs/components/table.md +1 -1
  100. package/docs/components/text-editor.md +1 -1
  101. package/docs/components/textarea.md +1 -1
  102. package/docs/components/toggle-button.md +1 -1
  103. package/docs/components/toggle.md +1 -1
  104. package/docs/components/tree-folder.md +110 -0
  105. package/docs/how-to/forms-with-form-builder.md +2 -2
  106. package/docs/reference/components.md +16 -6
  107. package/docs/tutorials/component-composition.md +11 -13
  108. package/package.json +3 -2
  109. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  110. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  111. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  112. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  113. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  114. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  115. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  116. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  117. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  118. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  119. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  120. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  121. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  122. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  123. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  124. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  125. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  126. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  127. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  128. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  129. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  130. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  131. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  132. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  133. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  134. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  135. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  136. package/docs/components/data-views-config-panel.md +0 -208
  137. package/docs/components/data-views-layout.md +0 -291
  138. package/docs/components/inbox-view.md +0 -170
  139. package/docs/components/kanban-view.md +0 -135
  140. package/docs/components/table-view.md +0 -141
  141. package/docs/components/tree-view.md +0 -147
  142. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -1,197 +0,0 @@
1
- import type {
2
- DynamicRecord,
3
- FieldConfig,
4
- FieldType,
5
- DynamicColumnConfig,
6
- InboxConfig,
7
- } from "../../components/DataViews/types";
8
- import { findFirstDefined, formatPathLabel, getByPath } from "./pathUtils";
9
- import { isPlainObject, isCurrencyField, isRatingField } from "./nestedDataUtils";
10
-
11
- const ISO_DATE = /^\d{4}-\d{2}-\d{2}/;
12
-
13
- const KEY_HINTS: Array<[RegExp, FieldType]> = [
14
- [/(^|[._])status$/i, "enum-badge"],
15
- [/(^|[._])priority$/i, "enum-badge"],
16
- [/(^|[._])avatar(url)?$/i, "avatar"],
17
- [/(^|[._])email$/i, "link"],
18
- [/(^|[._])phone$/i, "link"],
19
- [/(^|[._])url$|website$|homepage$/i, "link"],
20
- [/(^|[._])(image|thumbnail|photo|picture)$/i, "image"],
21
- [/(^|[._])tags?$|labels?$/i, "badge-array"],
22
- [/(date|time)$/i, "date-format"],
23
- ];
24
-
25
- export function inferFieldType(path: string, value: unknown): FieldType {
26
- for (const [re, type] of KEY_HINTS) {
27
- if (re.test(path)) return type;
28
- }
29
-
30
- if (isCurrencyField(path)) return "currency";
31
- if (isRatingField(path) && typeof value === "number" && value <= 5) return "star-rating";
32
-
33
- if (value === null || value === undefined) return "text";
34
- if (typeof value === "boolean") return "boolean";
35
- if (typeof value === "number") return "number";
36
- if (Array.isArray(value)) return "badge-array";
37
- if (typeof value === "string") {
38
- if (ISO_DATE.test(value) && !isNaN(Date.parse(value))) return "date-format";
39
- return "text";
40
- }
41
- return "text";
42
- }
43
-
44
- export function detectFields(data: DynamicRecord[]): FieldConfig[] {
45
- if (!data || data.length === 0) return [];
46
-
47
- const allKeys = new Set<string>();
48
- for (const item of data) {
49
- if (item && typeof item === "object") {
50
- for (const k of Object.keys(item)) allKeys.add(k);
51
- }
52
- }
53
-
54
- const fields: FieldConfig[] = [];
55
- let order = 0;
56
-
57
- for (const key of allKeys) {
58
- if (key.startsWith("_")) continue;
59
-
60
- const sample = findFirstDefined(data, key);
61
- if (isPlainObject(sample)) continue;
62
- if (Array.isArray(sample) && sample.length > 0 && isPlainObject(sample[0])) continue;
63
-
64
- fields.push({
65
- path: key,
66
- label: formatPathLabel(key),
67
- type: inferFieldType(key, sample),
68
- visible: true,
69
- order: order++,
70
- });
71
- }
72
-
73
- return fields
74
- .sort((a, b) => {
75
- if (a.path === "id") return -1;
76
- if (b.path === "id") return 1;
77
- return (a.label || "").localeCompare(b.label || "");
78
- })
79
- .map((f, i) => ({ ...f, order: i }));
80
- }
81
-
82
- export function mergeFields(detected: FieldConfig[], custom?: FieldConfig[]): FieldConfig[] {
83
- if (!custom || custom.length === 0) return detected;
84
-
85
- const out = [...detected];
86
- const byPath = new Map(out.map((f, i) => [f.path, i]));
87
-
88
- for (const c of custom) {
89
- if (!c.path) continue;
90
- const existing = byPath.get(c.path);
91
- if (existing != null) {
92
- out[existing] = { ...out[existing], ...c };
93
- } else {
94
- out.push({
95
- ...c,
96
- path: c.path,
97
- label: c.label ?? formatPathLabel(c.path),
98
- visible: c.visible ?? true,
99
- order: c.order ?? out.length,
100
- });
101
- byPath.set(c.path, out.length - 1);
102
- }
103
- }
104
-
105
- return out
106
- .map((f, i) => ({ ...f, order: f.order ?? i }))
107
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
108
- }
109
-
110
- export function visibleFields(fields: FieldConfig[]): FieldConfig[] {
111
- return fields.filter((f) => f.type !== "hidden" && f.visible !== false);
112
- }
113
-
114
- export function fieldToColumn(
115
- field: FieldConfig,
116
- idx = 0,
117
- ): DynamicColumnConfig & {
118
- __field: FieldConfig;
119
- } {
120
- const legacyType = mapFieldTypeToColumnType(field.type);
121
- return {
122
- id: field.path,
123
- label: field.label ?? formatPathLabel(field.path),
124
- visible: field.visible !== false && field.type !== "hidden",
125
- order: field.order ?? idx,
126
- type: legacyType,
127
- render: field.render,
128
- __field: field,
129
- };
130
- }
131
-
132
- function mapFieldTypeToColumnType(t?: FieldType): DynamicColumnConfig["type"] {
133
- switch (t) {
134
- case "number":
135
- case "currency":
136
- case "number-format":
137
- case "progress-bar":
138
- case "star-rating":
139
- return "number";
140
- case "date":
141
- case "date-format":
142
- return "date";
143
- case "enum-badge":
144
- case "icon-text":
145
- case "two-line":
146
- case "link":
147
- return "badge";
148
- case "badge-array":
149
- return "array";
150
- case "boolean":
151
- return "boolean";
152
- default:
153
- return "text";
154
- }
155
- }
156
-
157
- const STARRED_PATTERNS = ["isStarred", "starred", "favorite", "isFavorite", "pinned"];
158
- const READ_PATTERNS = ["isRead", "read", "seen", "viewed"];
159
- const ATTACHMENT_PATTERNS = ["hasAttachment", "hasAttachments", "attachments", "files"];
160
- const PRIORITY_PATTERNS = ["priority", "urgency", "level", "importance"];
161
-
162
- function pickField(sample: DynamicRecord, patterns: string[]): string | null {
163
- for (const p of patterns) {
164
- if (p in sample) return p;
165
- }
166
- return null;
167
- }
168
-
169
- export function resolveInboxConfig(
170
- data: DynamicRecord[],
171
- user?: InboxConfig,
172
- ): Required<Omit<InboxConfig, "titlePath" | "previewPath">> &
173
- Pick<InboxConfig, "titlePath" | "previewPath"> {
174
- const sample = data?.[0] && typeof data[0] === "object" ? data[0] : {};
175
-
176
- const auto = {
177
- starredField: pickField(sample, STARRED_PATTERNS),
178
- readField: pickField(sample, READ_PATTERNS),
179
- attachmentField: pickField(sample, ATTACHMENT_PATTERNS),
180
- priorityField: pickField(sample, PRIORITY_PATTERNS),
181
- };
182
-
183
- return {
184
- starredField: user?.starredField !== undefined ? user.starredField : auto.starredField,
185
- readField: user?.readField !== undefined ? user.readField : auto.readField,
186
- attachmentField:
187
- user?.attachmentField !== undefined ? user.attachmentField : auto.attachmentField,
188
- priorityField: user?.priorityField !== undefined ? user.priorityField : auto.priorityField,
189
- titlePath: user?.titlePath,
190
- previewPath: user?.previewPath,
191
- };
192
- }
193
-
194
- export function readInboxField(item: DynamicRecord, field: string | null | undefined): unknown {
195
- if (!field) return undefined;
196
- return getByPath(item, field);
197
- }
@@ -1,371 +0,0 @@
1
- import type { ReactElement } from "react";
2
- import { Badge } from "../../components/Badge";
3
- import { Divider } from "../../components/Divider";
4
- import { cn } from "../cn";
5
- import type { DynamicRecord, DynamicColumnConfig } from "../../components/DataViews/types";
6
-
7
- export type NestedFieldMetadata = {
8
- key: string;
9
- label: string;
10
- type: "object" | "array" | "primitive";
11
- valueType?: "string" | "number" | "boolean" | "date";
12
- isArrayOfObjects?: boolean;
13
- depth: number;
14
- };
15
-
16
- export function isPlainObject(value: unknown): boolean {
17
- return (
18
- value !== null &&
19
- typeof value === "object" &&
20
- !Array.isArray(value) &&
21
- !(value instanceof Date) &&
22
- Object.keys(value).length > 0
23
- );
24
- }
25
-
26
- export function formatFieldName(fieldName: string): string {
27
- if (fieldName.includes("_")) {
28
- return fieldName
29
- .split("_")
30
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
31
- .join(" ");
32
- }
33
-
34
- return fieldName
35
- .replace(/([A-Z])/g, " $1")
36
- .replace(/^./, (str) => str.toUpperCase())
37
- .trim();
38
- }
39
-
40
- export function analyzeNestedFields(
41
- item: DynamicRecord,
42
- visibleColumnIds: Set<string> = new Set(),
43
- excludeKeys: string[] = ["id", "isRead", "isStarred", "hasAttachment", "priority"],
44
- ): NestedFieldMetadata[] {
45
- const nestedFields: NestedFieldMetadata[] = [];
46
-
47
- Object.entries(item).forEach(([key, value]) => {
48
- if (visibleColumnIds.has(key) || excludeKeys.includes(key)) {
49
- return;
50
- }
51
-
52
- if (isPlainObject(value)) {
53
- nestedFields.push({
54
- key,
55
- label: formatFieldName(key),
56
- type: "object",
57
- depth: 0,
58
- });
59
- } else if (Array.isArray(value) && value.length > 0) {
60
- nestedFields.push({
61
- key,
62
- label: formatFieldName(key),
63
- type: "array",
64
- isArrayOfObjects: isPlainObject(value[0]),
65
- depth: 0,
66
- });
67
- }
68
- });
69
-
70
- return nestedFields;
71
- }
72
-
73
- export function isCurrencyField(key: string): boolean {
74
- const lowerKey = key.toLowerCase();
75
- return (
76
- lowerKey.includes("salary") ||
77
- lowerKey.includes("price") ||
78
- lowerKey.includes("cost") ||
79
- lowerKey.includes("amount") ||
80
- lowerKey.includes("pay") ||
81
- lowerKey.includes("fee")
82
- );
83
- }
84
-
85
- export function isRatingField(key: string): boolean {
86
- const lowerKey = key.toLowerCase();
87
- return lowerKey.includes("rating") || lowerKey.includes("score");
88
- }
89
-
90
- export function renderPrimitiveValue(
91
- key: string,
92
- value: unknown,
93
- options: {
94
- showLabel?: boolean;
95
- labelClassName?: string;
96
- valueClassName?: string;
97
- } = {},
98
- ): ReactElement | null {
99
- const {
100
- showLabel = true,
101
- labelClassName = "text-content-presentation-global-tertiary",
102
- valueClassName = "text-content-presentation-global-primary",
103
- } = options;
104
-
105
- if (value == null) return null;
106
-
107
- const label = showLabel ? formatFieldName(key) : null;
108
-
109
- if (typeof value === "boolean") {
110
- return (
111
- <div className="flex items-center gap-2">
112
- {label && <span className={labelClassName}>{label}:</span>}
113
- <Badge
114
- color={value ? "green" : "gray"}
115
- badgeStyle={value ? "solid" : "subtle"}
116
- label={value ? "Yes" : "No"}
117
- size="XS"
118
- />
119
- </div>
120
- );
121
- }
122
-
123
- if (typeof value === "number") {
124
- const isCurrency = isCurrencyField(key);
125
- const isRating = isRatingField(key);
126
-
127
- if (isRating && value <= 5) {
128
- return (
129
- <div className="flex items-center gap-2">
130
- {label && <span className={labelClassName}>{label}:</span>}
131
- <div className="flex items-center gap-2">
132
- <div className="flex">
133
- {[...Array(5)].map((_, i) => (
134
- <span
135
- key={i}
136
- className={i < Math.floor(value) ? "text-yellow-500" : "text-gray-300"}
137
- >
138
- ⭐
139
- </span>
140
- ))}
141
- </div>
142
- <span className="font-semibold text-sm">{value}</span>
143
- </div>
144
- </div>
145
- );
146
- }
147
-
148
- return (
149
- <div className="flex items-center gap-2">
150
- {label && <span className={labelClassName}>{label}:</span>}
151
- <span className={cn(valueClassName, isCurrency && "font-semibold text-green-600")}>
152
- {isCurrency ? `$${value.toLocaleString()}` : value.toLocaleString()}
153
- </span>
154
- </div>
155
- );
156
- }
157
-
158
- return (
159
- <div className="flex items-center gap-2">
160
- {label && <span className={labelClassName}>{label}:</span>}
161
- <span className={valueClassName}>{String(value)}</span>
162
- </div>
163
- );
164
- }
165
-
166
- export function renderArrayValue(
167
- key: string,
168
- value: unknown[],
169
- options: {
170
- showLabel?: boolean;
171
- maxItems?: number;
172
- renderItem?: (item: unknown, index: number) => ReactElement;
173
- } = {},
174
- ): ReactElement | null {
175
- const { showLabel = true, maxItems, renderItem } = options;
176
-
177
- if (!Array.isArray(value) || value.length === 0) return null;
178
-
179
- const label = showLabel ? formatFieldName(key) : null;
180
- const displayItems = maxItems ? value.slice(0, maxItems) : value;
181
-
182
- if (isPlainObject(value[0])) {
183
- return (
184
- <div className="space-y-2">
185
- {label && (
186
- <div className="text-xs font-semibold text-content-presentation-global-primary uppercase tracking-wide">
187
- {label} ({value.length})
188
- </div>
189
- )}
190
- <div className="space-y-2">
191
- {displayItems.map((item: unknown, idx: number) => {
192
- if (renderItem) {
193
- return renderItem(item, idx);
194
- }
195
- return (
196
- <div key={idx} className="p-2 rounded bg-background-presentation-form-field-primary">
197
- {renderNestedObject(item, 1)}
198
- </div>
199
- );
200
- })}
201
- {maxItems && value.length > maxItems && (
202
- <div className="text-xs text-content-presentation-global-tertiary">
203
- +{value.length - maxItems} more
204
- </div>
205
- )}
206
- </div>
207
- </div>
208
- );
209
- }
210
-
211
- return (
212
- <div className="flex items-start gap-2">
213
- {label && <span className="text-content-presentation-global-tertiary">{label}:</span>}
214
- <div className="flex flex-wrap gap-1">
215
- {displayItems.map((item: unknown, idx: number) => (
216
- <Badge key={idx} color="blue" badgeStyle="solid" label={String(item)} size="XS" />
217
- ))}
218
- {maxItems && value.length > maxItems && (
219
- <Badge color="gray" badgeStyle="subtle" label={`+${value.length - maxItems}`} size="XS" />
220
- )}
221
- </div>
222
- </div>
223
- );
224
- }
225
-
226
- export function renderNestedObject(
227
- obj: unknown,
228
- depth: number = 0,
229
- options: {
230
- maxDepth?: number;
231
- showSeparators?: boolean;
232
- } = {},
233
- ): ReactElement {
234
- const { maxDepth = 3 } = options;
235
-
236
- if (depth >= maxDepth) {
237
- return <div className="text-xs text-content-presentation-global-tertiary">...</div>;
238
- }
239
-
240
- return (
241
- <div className="space-y-2 text-sm">
242
- {Object.entries(obj as Record<string, unknown>).map(([key, value]) => {
243
- if (isPlainObject(value)) {
244
- return (
245
- <div key={key} className="space-y-1">
246
- <div className="text-xs font-semibold text-content-presentation-global-primary uppercase tracking-wide">
247
- {formatFieldName(key)}
248
- </div>
249
- <div className="pl-3 border-l-2 border-border-presentation-global-primary">
250
- {renderNestedObject(value, depth + 1, options)}
251
- </div>
252
- </div>
253
- );
254
- }
255
-
256
- if (Array.isArray(value)) {
257
- return <div key={key}>{renderArrayValue(key, value)}</div>;
258
- }
259
-
260
- return <div key={key}>{renderPrimitiveValue(key, value)}</div>;
261
- })}
262
- </div>
263
- );
264
- }
265
-
266
- export function renderDetailView(
267
- selectedItem: DynamicRecord,
268
- visibleColumns: DynamicColumnConfig[],
269
- renderCellValue: (
270
- value: unknown,
271
- column: DynamicColumnConfig,
272
- row: DynamicRecord,
273
- ) => React.ReactNode,
274
- ): ReactElement {
275
- const visibleColumnIds = new Set(visibleColumns.map((col) => col.id));
276
- const nestedFields = analyzeNestedFields(selectedItem, visibleColumnIds);
277
-
278
- return (
279
- <div className="space-y-6">
280
- {visibleColumns.slice(2).length > 0 && (
281
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
282
- {visibleColumns.slice(2).map((col) => {
283
- const value = selectedItem[col.id];
284
- // Skip empty cells (null/undefined); 0 and false are rendered.
285
- if (value === null || value === undefined) return null;
286
- return (
287
- <div key={col.id} className="space-y-1">
288
- <dt className="text-xs font-medium text-content-presentation-global-tertiary uppercase tracking-wide">
289
- {col.label}
290
- </dt>
291
- <dd className="text-sm text-content-presentation-global-primary">
292
- {renderCellValue(value, col, selectedItem)}
293
- </dd>
294
- </div>
295
- );
296
- })}
297
- </div>
298
- )}
299
-
300
- {nestedFields.map((field, index) => {
301
- const value = selectedItem[field.key];
302
-
303
- if (field.type === "object" && isPlainObject(value)) {
304
- return (
305
- <div key={field.key}>
306
- {index > 0 && <Divider />}
307
- <div>
308
- <h3 className="text-sm font-semibold text-content-presentation-global-primary mb-3">
309
- {field.label}
310
- </h3>
311
- {renderNestedObject(value)}
312
- </div>
313
- </div>
314
- );
315
- }
316
-
317
- if (field.type === "array" && Array.isArray(value)) {
318
- return (
319
- <div key={field.key}>
320
- {index > 0 && <Divider />}
321
- <div>
322
- <h3 className="text-sm font-semibold text-content-presentation-global-primary mb-3">
323
- {field.label}
324
- </h3>
325
- {renderArrayValue(field.key, value, { showLabel: false })}
326
- </div>
327
- </div>
328
- );
329
- }
330
-
331
- return null;
332
- })}
333
- </div>
334
- );
335
- }
336
-
337
- export function getDataStructureSummary(data: DynamicRecord[]): {
338
- totalFields: number;
339
- nestedFields: string[];
340
- arrayFields: string[];
341
- primitiveFields: string[];
342
- } {
343
- if (!data || data.length === 0) {
344
- return { totalFields: 0, nestedFields: [], arrayFields: [], primitiveFields: [] };
345
- }
346
-
347
- const allKeys = new Set<string>();
348
- const nestedKeys = new Set<string>();
349
- const arrayKeys = new Set<string>();
350
- const primitiveKeys = new Set<string>();
351
-
352
- data.forEach((item) => {
353
- Object.entries(item).forEach(([key, value]) => {
354
- allKeys.add(key);
355
- if (isPlainObject(value)) {
356
- nestedKeys.add(key);
357
- } else if (Array.isArray(value)) {
358
- arrayKeys.add(key);
359
- } else {
360
- primitiveKeys.add(key);
361
- }
362
- });
363
- });
364
-
365
- return {
366
- totalFields: allKeys.size,
367
- nestedFields: Array.from(nestedKeys),
368
- arrayFields: Array.from(arrayKeys),
369
- primitiveFields: Array.from(primitiveKeys),
370
- };
371
- }
@@ -1,139 +0,0 @@
1
- export type Path = string;
2
-
3
- const PATH_CACHE = new Map<string, string[]>();
4
-
5
- function splitPath(path: Path): string[] {
6
- const cached = PATH_CACHE.get(path);
7
- if (cached) return cached;
8
- const parts = path.split(".");
9
- PATH_CACHE.set(path, parts);
10
- return parts;
11
- }
12
-
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic path accessor returns arbitrary runtime values
14
- export function getByPath(obj: unknown, path: Path | undefined | null): any {
15
- if (obj == null || path == null || path === "") return undefined;
16
- if (typeof obj !== "object") return undefined;
17
-
18
- const parts = splitPath(path);
19
- let cur: unknown = obj;
20
- for (let i = 0; i < parts.length; i++) {
21
- if (cur == null) return undefined;
22
- cur = (cur as Record<string, unknown>)[parts[i]];
23
- }
24
- return cur;
25
- }
26
-
27
- export function setByPath<T extends Record<string, unknown>>(
28
- obj: T,
29
- path: Path,
30
- value: unknown,
31
- ): T {
32
- if (!path) return obj;
33
- const parts = splitPath(path);
34
- if (parts.length === 0) return obj;
35
-
36
- const root = (Array.isArray(obj) ? [...obj] : { ...obj }) as Record<string, unknown>;
37
- let cur: Record<string, unknown> = root;
38
-
39
- for (let i = 0; i < parts.length - 1; i++) {
40
- const key = parts[i];
41
- const next = cur[key];
42
- const cloned = (
43
- next != null && typeof next === "object" && !Array.isArray(next)
44
- ? { ...next }
45
- : Array.isArray(next)
46
- ? [...next]
47
- : {}
48
- ) as Record<string, unknown>;
49
- cur[key] = cloned;
50
- cur = cloned;
51
- }
52
-
53
- cur[parts[parts.length - 1]] = value;
54
- return root as T;
55
- }
56
-
57
- export function hasPath(obj: unknown, path: Path | undefined | null): boolean {
58
- return getByPath(obj, path) !== undefined;
59
- }
60
-
61
- export function formatPathLabel(path: Path): string {
62
- if (!path) return "";
63
- const tail = path.includes(".") ? path.split(".").pop()! : path;
64
-
65
- if (tail.includes("_")) {
66
- return tail
67
- .split("_")
68
- .filter(Boolean)
69
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
70
- .join(" ");
71
- }
72
-
73
- return tail
74
- .replace(/([A-Z])/g, " $1")
75
- .replace(/^./, (s) => s.toUpperCase())
76
- .trim();
77
- }
78
-
79
- export function findFirstDefined(data: unknown[], path: Path): unknown {
80
- for (const item of data) {
81
- const v = getByPath(item, path);
82
- if (v != null) return v;
83
- }
84
- return undefined;
85
- }
86
-
87
- export function matchesFilterValues(
88
- item: unknown,
89
- path: Path,
90
- filter:
91
- | string[]
92
- | { kind: "number"; min?: number; max?: number }
93
- | { kind: "date"; from?: string; to?: string }
94
- | undefined,
95
- ): boolean {
96
- if (filter == null) return true;
97
-
98
- if (Array.isArray(filter)) {
99
- if (filter.length === 0) return true;
100
- const value = getByPath(item, path);
101
- if (Array.isArray(value)) {
102
- const set = new Set(value.map((v) => String(v)));
103
- return filter.some((s) => set.has(s));
104
- }
105
- if (typeof value === "boolean") {
106
- return filter.includes(value ? "true" : "false");
107
- }
108
- return filter.includes(String(value ?? ""));
109
- }
110
-
111
- if (filter.kind === "number") {
112
- if (filter.min == null && filter.max == null) return true;
113
- const raw = getByPath(item, path);
114
- const n = typeof raw === "number" ? raw : Number(raw);
115
- if (!Number.isFinite(n)) return false;
116
- if (filter.min != null && n < filter.min) return false;
117
- if (filter.max != null && n > filter.max) return false;
118
- return true;
119
- }
120
-
121
- if (filter.kind === "date") {
122
- if (filter.from == null && filter.to == null) return true;
123
- const raw = getByPath(item, path);
124
- if (raw == null) return false;
125
- const ms = raw instanceof Date ? raw.getTime() : Date.parse(String(raw));
126
- if (!Number.isFinite(ms)) return false;
127
- if (filter.from != null) {
128
- const fromMs = Date.parse(filter.from);
129
- if (Number.isFinite(fromMs) && ms < fromMs) return false;
130
- }
131
- if (filter.to != null) {
132
- const toMs = Date.parse(filter.to);
133
- if (Number.isFinite(toMs) && ms > toMs + 24 * 60 * 60 * 1000 - 1) return false;
134
- }
135
- return true;
136
- }
137
-
138
- return true;
139
- }