@startsimpli/ui 0.1.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.
- package/README.md +537 -0
- package/package.json +80 -0
- package/src/components/index.ts +50 -0
- package/src/components/navigation/sidebar.tsx +178 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +68 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/unified-table/UnifiedTable.tsx +553 -0
- package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
- package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
- package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
- package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
- package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
- package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
- package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
- package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
- package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
- package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
- package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
- package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
- package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
- package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
- package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
- package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
- package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
- package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
- package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
- package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
- package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
- package/src/components/unified-table/components/MobileView/README.md +411 -0
- package/src/components/unified-table/components/MobileView/index.tsx +77 -0
- package/src/components/unified-table/components/MobileView/types.ts +77 -0
- package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
- package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
- package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
- package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
- package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
- package/src/components/unified-table/hooks/index.ts +21 -0
- package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
- package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
- package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
- package/src/components/unified-table/hooks/useFilters.ts +53 -0
- package/src/components/unified-table/hooks/usePagination.ts +120 -0
- package/src/components/unified-table/hooks/useResponsive.ts +50 -0
- package/src/components/unified-table/hooks/useSelection.ts +152 -0
- package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
- package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
- package/src/components/unified-table/hooks/useTableState.ts +103 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
- package/src/components/unified-table/hooks/useTableURL.ts +301 -0
- package/src/components/unified-table/index.ts +16 -0
- package/src/components/unified-table/types.ts +393 -0
- package/src/components/unified-table/utils/export.ts +236 -0
- package/src/components/unified-table/utils/index.ts +4 -0
- package/src/components/unified-table/utils/renderers.ts +105 -0
- package/src/components/unified-table/utils/themes.ts +87 -0
- package/src/components/unified-table/utils/validation.ts +122 -0
- package/src/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/theme/contract.ts +46 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/tailwind.config.js +70 -0
- package/src/theme/tailwind.preset.ts +93 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +91 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as XLSX from 'xlsx'
|
|
2
|
+
import { ColumnConfig } from '../types'
|
|
3
|
+
|
|
4
|
+
export type ExportFormat = 'csv' | 'excel'
|
|
5
|
+
|
|
6
|
+
export interface ExportOptions {
|
|
7
|
+
filename: string
|
|
8
|
+
format: ExportFormat
|
|
9
|
+
includeHeaders?: boolean
|
|
10
|
+
dateFormat?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the value from a row using column configuration
|
|
15
|
+
*/
|
|
16
|
+
function getColumnValue<TData>(row: TData, column: ColumnConfig<TData>): any {
|
|
17
|
+
if (column.accessorFn) {
|
|
18
|
+
return column.accessorFn(row)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (column.accessorKey) {
|
|
22
|
+
const keys = column.accessorKey.split('.')
|
|
23
|
+
let value: any = row
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
value = value?.[key]
|
|
26
|
+
}
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Formats a value for export (handles dates, objects, arrays)
|
|
35
|
+
*/
|
|
36
|
+
function formatValueForExport(value: any): string {
|
|
37
|
+
if (value == null) {
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (value instanceof Date) {
|
|
42
|
+
return value.toISOString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof value === 'object') {
|
|
46
|
+
return JSON.stringify(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'boolean') {
|
|
50
|
+
return value ? 'Yes' : 'No'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return String(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets the header text for a column
|
|
58
|
+
*/
|
|
59
|
+
function getColumnHeader<TData>(column: ColumnConfig<TData>): string {
|
|
60
|
+
if (typeof column.header === 'string') {
|
|
61
|
+
return column.header
|
|
62
|
+
}
|
|
63
|
+
return column.id
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Prepares data for export based on visible columns
|
|
68
|
+
*/
|
|
69
|
+
function prepareExportData<TData>(
|
|
70
|
+
data: TData[],
|
|
71
|
+
columns: ColumnConfig<TData>[],
|
|
72
|
+
includeHeaders: boolean = true
|
|
73
|
+
): string[][] {
|
|
74
|
+
const rows: string[][] = []
|
|
75
|
+
|
|
76
|
+
// Add headers
|
|
77
|
+
if (includeHeaders) {
|
|
78
|
+
rows.push(columns.map(getColumnHeader))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add data rows
|
|
82
|
+
for (const row of data) {
|
|
83
|
+
const rowData: string[] = []
|
|
84
|
+
for (const column of columns) {
|
|
85
|
+
const value = getColumnValue(row, column)
|
|
86
|
+
rowData.push(formatValueForExport(value))
|
|
87
|
+
}
|
|
88
|
+
rows.push(rowData)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return rows
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Converts array data to CSV string
|
|
96
|
+
*/
|
|
97
|
+
function arrayToCSV(data: string[][]): string {
|
|
98
|
+
return data
|
|
99
|
+
.map(row =>
|
|
100
|
+
row
|
|
101
|
+
.map(cell => {
|
|
102
|
+
// Escape quotes and wrap in quotes if contains comma, quote, or newline
|
|
103
|
+
const needsQuotes = /[,"\n\r]/.test(cell)
|
|
104
|
+
if (needsQuotes) {
|
|
105
|
+
return `"${cell.replace(/"/g, '""')}"`
|
|
106
|
+
}
|
|
107
|
+
return cell
|
|
108
|
+
})
|
|
109
|
+
.join(',')
|
|
110
|
+
)
|
|
111
|
+
.join('\n')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Triggers a download of the given content
|
|
116
|
+
*/
|
|
117
|
+
function downloadFile(content: string | Blob, filename: string, mimeType: string): void {
|
|
118
|
+
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
|
|
119
|
+
const url = URL.createObjectURL(blob)
|
|
120
|
+
const link = document.createElement('a')
|
|
121
|
+
link.href = url
|
|
122
|
+
link.download = filename
|
|
123
|
+
document.body.appendChild(link)
|
|
124
|
+
link.click()
|
|
125
|
+
document.body.removeChild(link)
|
|
126
|
+
URL.revokeObjectURL(url)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Exports data to CSV format
|
|
131
|
+
* @param data Array of data to export
|
|
132
|
+
* @param columns Visible columns configuration
|
|
133
|
+
* @param filename Name of the file to download (without extension)
|
|
134
|
+
* @param includeHeaders Whether to include header row (default: true)
|
|
135
|
+
*/
|
|
136
|
+
export function exportToCSV<TData>(
|
|
137
|
+
data: TData[],
|
|
138
|
+
columns: ColumnConfig<TData>[],
|
|
139
|
+
filename: string,
|
|
140
|
+
includeHeaders: boolean = true
|
|
141
|
+
): void {
|
|
142
|
+
if (!data || data.length === 0) {
|
|
143
|
+
throw new Error('No data to export')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!columns || columns.length === 0) {
|
|
147
|
+
throw new Error('No columns specified for export')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const exportData = prepareExportData(data, columns, includeHeaders)
|
|
151
|
+
const csv = arrayToCSV(exportData)
|
|
152
|
+
|
|
153
|
+
downloadFile(csv, `${filename}.csv`, 'text/csv;charset=utf-8;')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Exports data to Excel format (XLSX)
|
|
158
|
+
* @param data Array of data to export
|
|
159
|
+
* @param columns Visible columns configuration
|
|
160
|
+
* @param filename Name of the file to download (without extension)
|
|
161
|
+
* @param includeHeaders Whether to include header row (default: true)
|
|
162
|
+
*/
|
|
163
|
+
export function exportToExcel<TData>(
|
|
164
|
+
data: TData[],
|
|
165
|
+
columns: ColumnConfig<TData>[],
|
|
166
|
+
filename: string,
|
|
167
|
+
includeHeaders: boolean = true
|
|
168
|
+
): void {
|
|
169
|
+
if (!data || data.length === 0) {
|
|
170
|
+
throw new Error('No data to export')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!columns || columns.length === 0) {
|
|
174
|
+
throw new Error('No columns specified for export')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const exportData = prepareExportData(data, columns, includeHeaders)
|
|
178
|
+
|
|
179
|
+
// Create workbook and worksheet
|
|
180
|
+
const ws = XLSX.utils.aoa_to_sheet(exportData)
|
|
181
|
+
const wb = XLSX.utils.book_new()
|
|
182
|
+
XLSX.utils.book_append_sheet(wb, ws, 'Data')
|
|
183
|
+
|
|
184
|
+
// Auto-size columns
|
|
185
|
+
const columnWidths = columns.map((column, idx) => {
|
|
186
|
+
const header = getColumnHeader(column)
|
|
187
|
+
const maxWidth = exportData.reduce((max, row) => {
|
|
188
|
+
const cellValue = row[idx] || ''
|
|
189
|
+
return Math.max(max, cellValue.length)
|
|
190
|
+
}, header.length)
|
|
191
|
+
return { wch: Math.min(maxWidth + 2, 50) } // Max width of 50 chars
|
|
192
|
+
})
|
|
193
|
+
ws['!cols'] = columnWidths
|
|
194
|
+
|
|
195
|
+
// Generate Excel file and trigger download
|
|
196
|
+
const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' })
|
|
197
|
+
const blob = new Blob([excelBuffer], {
|
|
198
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
downloadFile(blob, `${filename}.xlsx`, blob.type)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generic export function that routes to CSV or Excel based on format
|
|
206
|
+
* @param data Array of data to export
|
|
207
|
+
* @param columns Visible columns configuration
|
|
208
|
+
* @param options Export options including format and filename
|
|
209
|
+
*/
|
|
210
|
+
export function exportData<TData>(
|
|
211
|
+
data: TData[],
|
|
212
|
+
columns: ColumnConfig<TData>[],
|
|
213
|
+
options: ExportOptions
|
|
214
|
+
): void {
|
|
215
|
+
const { format, filename, includeHeaders = true } = options
|
|
216
|
+
|
|
217
|
+
switch (format) {
|
|
218
|
+
case 'csv':
|
|
219
|
+
exportToCSV(data, columns, filename, includeHeaders)
|
|
220
|
+
break
|
|
221
|
+
case 'excel':
|
|
222
|
+
exportToExcel(data, columns, filename, includeHeaders)
|
|
223
|
+
break
|
|
224
|
+
default:
|
|
225
|
+
throw new Error(`Unsupported export format: ${format}`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Helper to generate a filename with timestamp
|
|
231
|
+
*/
|
|
232
|
+
export function generateExportFilename(baseFilename: string): string {
|
|
233
|
+
const now = new Date()
|
|
234
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
|
235
|
+
return `${baseFilename}_${timestamp}`
|
|
236
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type CellRenderer<TData = any> = (value: any, row: TData) => ReactNode
|
|
4
|
+
|
|
5
|
+
export interface RendererConfig {
|
|
6
|
+
[columnId: string]: CellRenderer
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createCellRenderer<TData = any>(
|
|
10
|
+
render: (value: any, row: TData) => ReactNode
|
|
11
|
+
): CellRenderer<TData> {
|
|
12
|
+
return render
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getNestedValue(obj: any, path: string): any {
|
|
16
|
+
if (!path || !obj) return obj
|
|
17
|
+
|
|
18
|
+
const keys = path.split('.')
|
|
19
|
+
let value = obj
|
|
20
|
+
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
if (value === null || value === undefined) return undefined
|
|
23
|
+
value = value[key]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const commonRenderers = {
|
|
30
|
+
text: createCellRenderer((value) => String(value || '-')),
|
|
31
|
+
|
|
32
|
+
number: createCellRenderer((value) => {
|
|
33
|
+
if (value === null || value === undefined) return '-'
|
|
34
|
+
return new Intl.NumberFormat().format(Number(value))
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
currency: createCellRenderer((value, row, options?: { currency?: string; locale?: string }) => {
|
|
38
|
+
if (value === null || value === undefined) return '-'
|
|
39
|
+
return new Intl.NumberFormat(options?.locale || 'en-US', {
|
|
40
|
+
style: 'currency',
|
|
41
|
+
currency: options?.currency || 'USD',
|
|
42
|
+
}).format(Number(value))
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
percentage: createCellRenderer((value) => {
|
|
46
|
+
if (value === null || value === undefined) return '-'
|
|
47
|
+
return `${Number(value).toFixed(2)}%`
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
date: createCellRenderer((value) => {
|
|
51
|
+
if (!value) return '-'
|
|
52
|
+
const date = new Date(value)
|
|
53
|
+
return date.toLocaleDateString()
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
datetime: createCellRenderer((value) => {
|
|
57
|
+
if (!value) return '-'
|
|
58
|
+
const date = new Date(value)
|
|
59
|
+
return date.toLocaleString()
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
boolean: createCellRenderer((value) => {
|
|
63
|
+
if (value === null || value === undefined) return '-'
|
|
64
|
+
return value ? 'Yes' : 'No'
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
badge: createCellRenderer((value, row, options?: { variant?: 'default' | 'success' | 'warning' | 'error' }) => {
|
|
68
|
+
if (!value) return null
|
|
69
|
+
return value
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
truncate: createCellRenderer((value, row, options?: { maxLength?: number }) => {
|
|
73
|
+
if (!value) return '-'
|
|
74
|
+
const str = String(value)
|
|
75
|
+
const maxLength = options?.maxLength || 50
|
|
76
|
+
if (str.length <= maxLength) return str
|
|
77
|
+
return `${str.substring(0, maxLength)}...`
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
array: createCellRenderer((value, row, options?: { separator?: string; maxItems?: number }) => {
|
|
81
|
+
if (!Array.isArray(value) || value.length === 0) return '-'
|
|
82
|
+
const maxItems = options?.maxItems || value.length
|
|
83
|
+
const items = value.slice(0, maxItems)
|
|
84
|
+
const separator = options?.separator || ', '
|
|
85
|
+
const display = items.join(separator)
|
|
86
|
+
if (value.length > maxItems) {
|
|
87
|
+
return `${display} +${value.length - maxItems} more`
|
|
88
|
+
}
|
|
89
|
+
return display
|
|
90
|
+
}),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function combineRenderers<TData = any>(
|
|
94
|
+
...renderers: CellRenderer<TData>[]
|
|
95
|
+
): CellRenderer<TData> {
|
|
96
|
+
return (value, row) => {
|
|
97
|
+
for (const renderer of renderers) {
|
|
98
|
+
const result = renderer(value, row)
|
|
99
|
+
if (result !== null && result !== undefined) {
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return value
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export interface TableTheme {
|
|
2
|
+
name: string
|
|
3
|
+
container: string
|
|
4
|
+
header: string
|
|
5
|
+
headerCell: string
|
|
6
|
+
body: string
|
|
7
|
+
row: string
|
|
8
|
+
rowHover: string
|
|
9
|
+
rowSelected: string
|
|
10
|
+
cell: string
|
|
11
|
+
pagination: string
|
|
12
|
+
emptyState: string
|
|
13
|
+
mobileCard: string
|
|
14
|
+
mobileCardHeader: string
|
|
15
|
+
mobileCardContent: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const defaultTheme: TableTheme = {
|
|
19
|
+
name: 'default',
|
|
20
|
+
container: 'w-full',
|
|
21
|
+
header: 'bg-muted/50',
|
|
22
|
+
headerCell: 'px-4 py-3 text-left text-sm font-medium text-muted-foreground',
|
|
23
|
+
body: '',
|
|
24
|
+
row: 'border-b transition-colors',
|
|
25
|
+
rowHover: 'hover:bg-muted/50',
|
|
26
|
+
rowSelected: 'bg-primary/10 border-primary',
|
|
27
|
+
cell: 'px-4 py-3 text-sm',
|
|
28
|
+
pagination: 'flex items-center justify-between px-4 py-3 border-t',
|
|
29
|
+
emptyState: 'flex items-center justify-center py-12 text-muted-foreground',
|
|
30
|
+
mobileCard: 'rounded-lg border bg-card shadow-sm',
|
|
31
|
+
mobileCardHeader: 'p-4 pb-2',
|
|
32
|
+
mobileCardContent: 'p-4 pt-2',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const compactTheme: TableTheme = {
|
|
36
|
+
...defaultTheme,
|
|
37
|
+
name: 'compact',
|
|
38
|
+
headerCell: 'px-2 py-2 text-left text-xs font-medium text-muted-foreground',
|
|
39
|
+
cell: 'px-2 py-2 text-xs',
|
|
40
|
+
pagination: 'flex items-center justify-between px-2 py-2 border-t',
|
|
41
|
+
mobileCardHeader: 'p-3 pb-1',
|
|
42
|
+
mobileCardContent: 'p-3 pt-1',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const spaciousTheme: TableTheme = {
|
|
46
|
+
...defaultTheme,
|
|
47
|
+
name: 'spacious',
|
|
48
|
+
headerCell: 'px-6 py-4 text-left text-sm font-medium text-muted-foreground',
|
|
49
|
+
cell: 'px-6 py-4 text-sm',
|
|
50
|
+
pagination: 'flex items-center justify-between px-6 py-4 border-t',
|
|
51
|
+
mobileCardHeader: 'p-6 pb-3',
|
|
52
|
+
mobileCardContent: 'p-6 pt-3',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const minimalTheme: TableTheme = {
|
|
56
|
+
...defaultTheme,
|
|
57
|
+
name: 'minimal',
|
|
58
|
+
header: '',
|
|
59
|
+
headerCell: 'px-4 py-3 text-left text-sm font-semibold',
|
|
60
|
+
row: 'border-b border-border/50',
|
|
61
|
+
rowHover: 'hover:bg-muted/30',
|
|
62
|
+
mobileCard: 'rounded-lg border border-border/50 bg-card',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const themes = {
|
|
66
|
+
default: defaultTheme,
|
|
67
|
+
compact: compactTheme,
|
|
68
|
+
spacious: spaciousTheme,
|
|
69
|
+
minimal: minimalTheme,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ThemeName = keyof typeof themes
|
|
73
|
+
|
|
74
|
+
export function getTheme(themeName: ThemeName = 'default'): TableTheme {
|
|
75
|
+
return themes[themeName] || defaultTheme
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createCustomTheme(
|
|
79
|
+
baseTheme: ThemeName = 'default',
|
|
80
|
+
overrides: Partial<TableTheme>
|
|
81
|
+
): TableTheme {
|
|
82
|
+
return {
|
|
83
|
+
...themes[baseTheme],
|
|
84
|
+
...overrides,
|
|
85
|
+
name: overrides.name || `custom-${baseTheme}`,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { MobileCardConfig } from '../components/MobileView/types'
|
|
2
|
+
|
|
3
|
+
export interface ValidationResult {
|
|
4
|
+
valid: boolean
|
|
5
|
+
errors: string[]
|
|
6
|
+
warnings: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateMobileCardConfig<TData = any>(
|
|
10
|
+
config: MobileCardConfig<TData>
|
|
11
|
+
): ValidationResult {
|
|
12
|
+
const errors: string[] = []
|
|
13
|
+
const warnings: string[] = []
|
|
14
|
+
|
|
15
|
+
if (!config.titleKey && !config.titleRender) {
|
|
16
|
+
errors.push('Mobile card config must have either titleKey or titleRender')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!config.primaryFields || config.primaryFields.length === 0) {
|
|
20
|
+
warnings.push('Mobile card config has no primary fields defined')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (config.primaryFields) {
|
|
24
|
+
config.primaryFields.forEach((field, index) => {
|
|
25
|
+
if (!field.key && !field.render) {
|
|
26
|
+
errors.push(`Primary field at index ${index} must have either key or render function`)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (config.secondaryFields) {
|
|
32
|
+
config.secondaryFields.forEach((field, index) => {
|
|
33
|
+
if (!field.key && !field.render) {
|
|
34
|
+
errors.push(`Secondary field at index ${index} must have either key or render function`)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (config.actions) {
|
|
40
|
+
config.actions.forEach((action, index) => {
|
|
41
|
+
if (!action.id) {
|
|
42
|
+
errors.push(`Action at index ${index} must have an id`)
|
|
43
|
+
}
|
|
44
|
+
if (!action.onClick) {
|
|
45
|
+
errors.push(`Action '${action.id || index}' must have an onClick handler`)
|
|
46
|
+
}
|
|
47
|
+
if (!action.label && !action.icon) {
|
|
48
|
+
warnings.push(`Action '${action.id || index}' should have either a label or icon`)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (config.showSelection && !config.onSelectionChange) {
|
|
54
|
+
warnings.push('showSelection is enabled but onSelectionChange handler is not provided')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
valid: errors.length === 0,
|
|
59
|
+
errors,
|
|
60
|
+
warnings,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateColumnConfig(columns: any[]): ValidationResult {
|
|
65
|
+
const errors: string[] = []
|
|
66
|
+
const warnings: string[] = []
|
|
67
|
+
|
|
68
|
+
if (!columns || columns.length === 0) {
|
|
69
|
+
errors.push('At least one column must be defined')
|
|
70
|
+
return { valid: false, errors, warnings }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const columnIds = new Set<string>()
|
|
74
|
+
|
|
75
|
+
columns.forEach((column, index) => {
|
|
76
|
+
if (!column.id && !column.accessorKey) {
|
|
77
|
+
errors.push(`Column at index ${index} must have either id or accessorKey`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const columnId = column.id || column.accessorKey
|
|
81
|
+
|
|
82
|
+
if (columnIds.has(columnId)) {
|
|
83
|
+
errors.push(`Duplicate column id: '${columnId}'`)
|
|
84
|
+
}
|
|
85
|
+
columnIds.add(columnId)
|
|
86
|
+
|
|
87
|
+
if (!column.header && !column.hideHeader) {
|
|
88
|
+
warnings.push(`Column '${columnId}' has no header defined`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!column.cell && !column.accessorKey && !column.accessorFn) {
|
|
92
|
+
warnings.push(`Column '${columnId}' has no way to access data (missing cell, accessorKey, or accessorFn)`)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
valid: errors.length === 0,
|
|
98
|
+
errors,
|
|
99
|
+
warnings,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function logValidationResults(
|
|
104
|
+
configName: string,
|
|
105
|
+
result: ValidationResult,
|
|
106
|
+
throwOnError: boolean = false
|
|
107
|
+
): void {
|
|
108
|
+
if (result.errors.length > 0) {
|
|
109
|
+
console.error(`[${configName}] Validation Errors:`, result.errors)
|
|
110
|
+
if (throwOnError) {
|
|
111
|
+
throw new Error(`${configName} validation failed: ${result.errors.join(', ')}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (result.warnings.length > 0) {
|
|
116
|
+
console.warn(`[${configName}] Validation Warnings:`, result.warnings)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result.valid && result.warnings.length === 0) {
|
|
120
|
+
console.log(`[${configName}] Configuration is valid`)
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/index.ts
ADDED
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from '../utils/cn'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** All CSS custom property names that apps must define in their theme */
|
|
2
|
+
export const THEME_TOKENS = {
|
|
3
|
+
colors: [
|
|
4
|
+
'--background',
|
|
5
|
+
'--foreground',
|
|
6
|
+
'--card',
|
|
7
|
+
'--card-foreground',
|
|
8
|
+
'--popover',
|
|
9
|
+
'--popover-foreground',
|
|
10
|
+
'--primary',
|
|
11
|
+
'--primary-foreground',
|
|
12
|
+
'--secondary',
|
|
13
|
+
'--secondary-foreground',
|
|
14
|
+
'--muted',
|
|
15
|
+
'--muted-foreground',
|
|
16
|
+
'--accent',
|
|
17
|
+
'--accent-foreground',
|
|
18
|
+
'--destructive',
|
|
19
|
+
'--destructive-foreground',
|
|
20
|
+
'--border',
|
|
21
|
+
'--input',
|
|
22
|
+
'--ring',
|
|
23
|
+
],
|
|
24
|
+
shape: [
|
|
25
|
+
'--radius',
|
|
26
|
+
'--radius-sm',
|
|
27
|
+
'--radius-md',
|
|
28
|
+
'--radius-lg',
|
|
29
|
+
'--radius-xl',
|
|
30
|
+
'--radius-full',
|
|
31
|
+
],
|
|
32
|
+
shadows: ['--shadow-sm', '--shadow-md', '--shadow-lg', '--shadow-xl'],
|
|
33
|
+
typography: ['--font-sans', '--font-mono', '--font-heading'],
|
|
34
|
+
animation: ['--duration-fast', '--duration-normal', '--duration-slow', '--easing-default'],
|
|
35
|
+
borders: ['--border-width'],
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
export type ThemeColorTokens = (typeof THEME_TOKENS.colors)[number]
|
|
39
|
+
export type ThemeShapeTokens = (typeof THEME_TOKENS.shape)[number]
|
|
40
|
+
export type ThemeToken =
|
|
41
|
+
| ThemeColorTokens
|
|
42
|
+
| ThemeShapeTokens
|
|
43
|
+
| (typeof THEME_TOKENS.shadows)[number]
|
|
44
|
+
| (typeof THEME_TOKENS.typography)[number]
|
|
45
|
+
| (typeof THEME_TOKENS.animation)[number]
|
|
46
|
+
| (typeof THEME_TOKENS.borders)[number]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Export Tailwind config for apps to extend (legacy CJS config)
|
|
2
|
+
export { default as tailwindConfig } from './tailwind.config'
|
|
3
|
+
|
|
4
|
+
// Export TypeScript preset — the canonical way for apps to extend the design system
|
|
5
|
+
export { default as tailwindPreset } from './tailwind.preset'
|
|
6
|
+
|
|
7
|
+
// Design token contract — all CSS variables apps must define
|
|
8
|
+
export { THEME_TOKENS } from './contract'
|
|
9
|
+
export type { ThemeColorTokens, ThemeShapeTokens, ThemeToken } from './contract'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
darkMode: ["class"],
|
|
4
|
+
theme: {
|
|
5
|
+
container: {
|
|
6
|
+
center: true,
|
|
7
|
+
padding: "2rem",
|
|
8
|
+
screens: {
|
|
9
|
+
"2xl": "1400px",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
border: "hsl(var(--border))",
|
|
15
|
+
input: "hsl(var(--input))",
|
|
16
|
+
ring: "hsl(var(--ring))",
|
|
17
|
+
background: "hsl(var(--background))",
|
|
18
|
+
foreground: "hsl(var(--foreground))",
|
|
19
|
+
primary: {
|
|
20
|
+
DEFAULT: "hsl(var(--primary))",
|
|
21
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
22
|
+
},
|
|
23
|
+
secondary: {
|
|
24
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
25
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
26
|
+
},
|
|
27
|
+
destructive: {
|
|
28
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
29
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
30
|
+
},
|
|
31
|
+
muted: {
|
|
32
|
+
DEFAULT: "hsl(var(--muted))",
|
|
33
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
34
|
+
},
|
|
35
|
+
accent: {
|
|
36
|
+
DEFAULT: "hsl(var(--accent))",
|
|
37
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
38
|
+
},
|
|
39
|
+
popover: {
|
|
40
|
+
DEFAULT: "hsl(var(--popover))",
|
|
41
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
42
|
+
},
|
|
43
|
+
card: {
|
|
44
|
+
DEFAULT: "hsl(var(--card))",
|
|
45
|
+
foreground: "hsl(var(--card-foreground))",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
borderRadius: {
|
|
49
|
+
lg: "var(--radius)",
|
|
50
|
+
md: "calc(var(--radius) - 2px)",
|
|
51
|
+
sm: "calc(var(--radius) - 4px)",
|
|
52
|
+
},
|
|
53
|
+
keyframes: {
|
|
54
|
+
"accordion-down": {
|
|
55
|
+
from: { height: 0 },
|
|
56
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
57
|
+
},
|
|
58
|
+
"accordion-up": {
|
|
59
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
60
|
+
to: { height: 0 },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
animation: {
|
|
64
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
65
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
plugins: [require("tailwindcss-animate")],
|
|
70
|
+
}
|