@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.
Files changed (86) hide show
  1. package/README.md +537 -0
  2. package/package.json +80 -0
  3. package/src/components/index.ts +50 -0
  4. package/src/components/navigation/sidebar.tsx +178 -0
  5. package/src/components/ui/accordion.tsx +58 -0
  6. package/src/components/ui/alert.tsx +59 -0
  7. package/src/components/ui/badge.tsx +36 -0
  8. package/src/components/ui/button.tsx +57 -0
  9. package/src/components/ui/calendar.tsx +70 -0
  10. package/src/components/ui/card.tsx +68 -0
  11. package/src/components/ui/checkbox.tsx +30 -0
  12. package/src/components/ui/collapsible.tsx +12 -0
  13. package/src/components/ui/dialog.tsx +122 -0
  14. package/src/components/ui/dropdown-menu.tsx +200 -0
  15. package/src/components/ui/index.ts +24 -0
  16. package/src/components/ui/input.tsx +25 -0
  17. package/src/components/ui/label.tsx +26 -0
  18. package/src/components/ui/popover.tsx +31 -0
  19. package/src/components/ui/progress.tsx +28 -0
  20. package/src/components/ui/scroll-area.tsx +48 -0
  21. package/src/components/ui/select.tsx +160 -0
  22. package/src/components/ui/separator.tsx +31 -0
  23. package/src/components/ui/skeleton.tsx +15 -0
  24. package/src/components/ui/table.tsx +117 -0
  25. package/src/components/ui/tabs.tsx +55 -0
  26. package/src/components/ui/textarea.tsx +24 -0
  27. package/src/components/ui/tooltip.tsx +30 -0
  28. package/src/components/unified-table/UnifiedTable.tsx +553 -0
  29. package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
  30. package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
  31. package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
  32. package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
  33. package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
  34. package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
  35. package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
  36. package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
  37. package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
  38. package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
  39. package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
  40. package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
  41. package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
  42. package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
  43. package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
  44. package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
  45. package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
  46. package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
  47. package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
  48. package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
  49. package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
  50. package/src/components/unified-table/components/MobileView/README.md +411 -0
  51. package/src/components/unified-table/components/MobileView/index.tsx +77 -0
  52. package/src/components/unified-table/components/MobileView/types.ts +77 -0
  53. package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
  54. package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
  55. package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
  56. package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
  57. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
  58. package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
  59. package/src/components/unified-table/hooks/index.ts +21 -0
  60. package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
  61. package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
  62. package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
  63. package/src/components/unified-table/hooks/useFilters.ts +53 -0
  64. package/src/components/unified-table/hooks/usePagination.ts +120 -0
  65. package/src/components/unified-table/hooks/useResponsive.ts +50 -0
  66. package/src/components/unified-table/hooks/useSelection.ts +152 -0
  67. package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
  68. package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
  69. package/src/components/unified-table/hooks/useTableState.ts +103 -0
  70. package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
  71. package/src/components/unified-table/hooks/useTableURL.ts +301 -0
  72. package/src/components/unified-table/index.ts +16 -0
  73. package/src/components/unified-table/types.ts +393 -0
  74. package/src/components/unified-table/utils/export.ts +236 -0
  75. package/src/components/unified-table/utils/index.ts +4 -0
  76. package/src/components/unified-table/utils/renderers.ts +105 -0
  77. package/src/components/unified-table/utils/themes.ts +87 -0
  78. package/src/components/unified-table/utils/validation.ts +122 -0
  79. package/src/index.ts +6 -0
  80. package/src/lib/utils.ts +1 -0
  81. package/src/theme/contract.ts +46 -0
  82. package/src/theme/index.ts +9 -0
  83. package/src/theme/tailwind.config.js +70 -0
  84. package/src/theme/tailwind.preset.ts +93 -0
  85. package/src/utils/cn.ts +6 -0
  86. 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,4 @@
1
+ export * from './renderers'
2
+ export * from './themes'
3
+ export * from './validation'
4
+ export * from './export'
@@ -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
@@ -0,0 +1,6 @@
1
+ // Main entry point for @startsimpli/ui package
2
+ export * from './components'
3
+ export * from './utils'
4
+ export * from './theme'
5
+ export { THEME_TOKENS } from './theme/contract'
6
+ export type { ThemeColorTokens, ThemeShapeTokens, ThemeToken } from './theme/contract'
@@ -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
+ }