@startsimpli/ui 0.4.103 → 0.4.105

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 (31) hide show
  1. package/package.json +6 -5
  2. package/src/components/document-editor/DocumentEditor.tsx +11 -2
  3. package/src/components/navigation/__tests__/breadcrumbs.test.tsx +16 -0
  4. package/src/components/navigation/breadcrumbs.tsx +39 -0
  5. package/src/components/navigation/index.ts +1 -0
  6. package/src/components/unified-table/UnifiedTable.tsx +30 -2
  7. package/src/components/unified-table/__tests__/integration/search-and-empty-state.test.tsx +210 -0
  8. package/src/components/unified-table/components/DataTableCore/index.tsx +6 -2
  9. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +53 -3
  10. package/src/components/unified-table/utils/export.ts +5 -37
  11. package/src/foundry/LinkPanel.tsx +394 -0
  12. package/src/foundry/__tests__/LinkPanel.test.tsx +139 -0
  13. package/src/foundry/__tests__/links.test.ts +251 -0
  14. package/src/foundry/__tests__/list-query.test.ts +200 -0
  15. package/src/foundry/__tests__/publication.test.ts +227 -0
  16. package/src/foundry/__tests__/record-validation.test.ts +132 -0
  17. package/src/foundry/__tests__/types.test.ts +53 -0
  18. package/src/foundry/index.ts +21 -0
  19. package/src/foundry/links.ts +227 -0
  20. package/src/foundry/list-query.ts +241 -0
  21. package/src/foundry/publication.ts +260 -0
  22. package/src/foundry/record-validation.ts +109 -0
  23. package/src/foundry/system-attrs.ts +51 -0
  24. package/src/foundry/types.ts +137 -0
  25. package/src/hooks/index.ts +1 -0
  26. package/src/hooks/use-now.ts +46 -0
  27. package/src/index.ts +1 -0
  28. package/src/utils/__tests__/csv.test.ts +67 -0
  29. package/src/utils/__tests__/download-csv.test.ts +40 -0
  30. package/src/utils/csv.ts +77 -0
  31. package/src/utils/index.ts +9 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `downloadCsv` writes the BOM. Without it Excel reads a UTF-8 file in the local
3
+ * ANSI codepage and every accented character arrives mojibaked — which is what
4
+ * the table's exporter did until these primitives replaced its private copy.
5
+ */
6
+ import { downloadCsv } from '../csv';
7
+
8
+ describe('downloadCsv', () => {
9
+ const originalCreate = URL.createObjectURL
10
+ const originalRevoke = URL.revokeObjectURL
11
+
12
+ afterEach(() => {
13
+ URL.createObjectURL = originalCreate
14
+ URL.revokeObjectURL = originalRevoke
15
+ })
16
+
17
+ it('prefixes the UTF-8 BOM and names the file as asked', () => {
18
+ let captured: Blob | null = null
19
+ URL.createObjectURL = ((blob: Blob) => {
20
+ captured = blob
21
+ return 'blob:x'
22
+ }) as typeof URL.createObjectURL
23
+ URL.revokeObjectURL = (() => {}) as typeof URL.revokeObjectURL
24
+
25
+ const clicks: string[] = []
26
+ const realCreateElement = document.createElement.bind(document)
27
+ jest.spyOn(document, 'createElement').mockImplementation((tag: string) => {
28
+ const el = realCreateElement(tag) as HTMLAnchorElement
29
+ if (tag === 'a') el.click = () => clicks.push(el.download)
30
+ return el
31
+ })
32
+
33
+ downloadCsv('patents-2026-08-24.csv', 'Name\r\nCafé\r\n')
34
+
35
+ expect(clicks).toEqual(['patents-2026-08-24.csv'])
36
+ expect(captured).not.toBeNull()
37
+ expect(captured!.type).toBe('text/csv;charset=utf-8;')
38
+ ;(document.createElement as jest.Mock).mockRestore()
39
+ })
40
+ })
@@ -0,0 +1,77 @@
1
+ /**
2
+ * CSV, written the way a spreadsheet actually reads it.
3
+ *
4
+ * These are the primitives `unified-table/utils/export.ts` uses, and they are
5
+ * exported so a page that needs to export something the table does not hold —
6
+ * every matching row rather than the page on screen — writes the SAME file
7
+ * rather than growing a second CSV implementation next to this one.
8
+ *
9
+ * THE FORMULA GUARD IS A SECURITY CONTROL, not a nicety. A cell whose first
10
+ * character is `=`, `+`, `-` or `@` is EXECUTED by Excel and Sheets when the
11
+ * file is opened, so anyone who can name a record can run a formula in whoever
12
+ * exports the list. The leading apostrophe is the standard neutralisation.
13
+ *
14
+ * THE BOM IS WHY AN ACCENT SURVIVES. Without it Excel reads a UTF-8 file in the
15
+ * local ANSI codepage and a title with an accent arrives mojibaked.
16
+ *
17
+ * CRLF is the line ending RFC 4180 specifies and the one Excel does not
18
+ * second-guess when a quoted cell contains a newline of its own.
19
+ */
20
+
21
+ /** Characters that make a spreadsheet treat a cell as a formula. Tab and CR are
22
+ * in the set because they can be used to smuggle a leader past a naive check. */
23
+ const FORMULA_LEADERS = new Set(['=', '+', '-', '@', '\t', '\r'])
24
+
25
+ /** One cell: formula-guarded, then quoted if it needs to be. */
26
+ export function csvCell(raw: unknown): string {
27
+ const value = raw == null ? '' : String(raw)
28
+ const guarded = value.length > 0 && FORMULA_LEADERS.has(value[0]) ? `'${value}` : value
29
+ if (/[",\n\r]/.test(guarded)) return `"${guarded.replace(/"/g, '""')}"`
30
+ return guarded
31
+ }
32
+
33
+ /** Rows of already-stringified cells -> one RFC 4180 document. */
34
+ export function csvFromRows(rows: string[][]): string {
35
+ return `${rows.map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`
36
+ }
37
+
38
+ export interface CsvColumn<T> {
39
+ header: string
40
+ value: (row: T) => string
41
+ }
42
+
43
+ /** Typed rows + a column spec -> one RFC 4180 document. */
44
+ export function toCsv<T>(rows: T[], columns: CsvColumn<T>[]): string {
45
+ const out = [columns.map((c) => c.header)]
46
+ for (const row of rows) out.push(columns.map((c) => c.value(row)))
47
+ return csvFromRows(out)
48
+ }
49
+
50
+ /** `products-2026-08-24.csv` — the subject and the day, so two exports never
51
+ * collide in a downloads folder. */
52
+ export function csvFilename(base: string, now: Date = new Date()): string {
53
+ const slug =
54
+ base.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'export'
55
+ return `${slug}-${now.toISOString().slice(0, 10)}.csv`
56
+ }
57
+
58
+ /**
59
+ * Hand a file to the browser. A Blob + object URL rather than a `data:` URI: a
60
+ * large list exceeds what some browsers accept in a URL, and the object URL is
61
+ * revoked immediately so the rows do not sit in memory after the save.
62
+ */
63
+ export function downloadBlob(filename: string, blob: Blob): void {
64
+ const url = URL.createObjectURL(blob)
65
+ const link = document.createElement('a')
66
+ link.href = url
67
+ link.download = filename
68
+ document.body.appendChild(link)
69
+ link.click()
70
+ document.body.removeChild(link)
71
+ URL.revokeObjectURL(url)
72
+ }
73
+
74
+ /** Save a CSV document, BOM and all. */
75
+ export function downloadCsv(filename: string, csv: string): void {
76
+ downloadBlob(filename, new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' }))
77
+ }
@@ -1,4 +1,13 @@
1
1
  export { cn } from './cn'
2
+ export {
3
+ csvCell,
4
+ csvFromRows,
5
+ csvFilename,
6
+ toCsv,
7
+ downloadCsv,
8
+ downloadBlob,
9
+ type CsvColumn,
10
+ } from './csv'
2
11
 
3
12
  /**
4
13
  * Generate a random ID with optional prefix