@elabs-ai/components-data 4.0.0 → 4.2.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.
@@ -3,7 +3,7 @@
3
3
  * (app-shell + DataTable with toolbar). This story is the single source of
4
4
  * truth: `pnpm gen:templates` derives the consumer template source
5
5
  * (`docs/playbooks/templates/data-app.tsx`) from it.
6
- * Verify across all three themes with globals=theme:<slug>.
6
+ * Verify across every theme with globals=theme:<slug>.
7
7
  */
8
8
  import type { Meta, StoryObj } from "@storybook/react-vite";
9
9
  import { useState } from "react";
@@ -52,9 +52,11 @@ const columns: ColumnDef<DataRow>[] = [
52
52
  {
53
53
  accessorKey: "records",
54
54
  header: "Records",
55
- cell: ({ row }) => (
56
- <span className="tabular-nums">{row.original.records.toLocaleString()}</span>
57
- ),
55
+ // #69: `meta.numeric` is the seam — DataTable applies `tabular-nums` +
56
+ // end-alignment on both `<th>` and `<td>`, so the exemplar no longer
57
+ // hand-rolls a wrapper span for it.
58
+ meta: { numeric: true },
59
+ cell: ({ row }) => row.original.records.toLocaleString(),
58
60
  },
59
61
  { accessorKey: "lastRun", header: "Last run" },
60
62
  ];
@@ -82,6 +82,19 @@ describe("toCsv", () => {
82
82
  expect(csv).toContain("'@baz");
83
83
  });
84
84
 
85
+ it("does NOT quote a plain negative number as an injection risk (#review false positive)", () => {
86
+ const rows = [{ val: -5 } as unknown as Row];
87
+ const csv = toCsv(rows);
88
+ expect(csv).toContain("-5");
89
+ expect(csv).not.toContain("'-5");
90
+ });
91
+
92
+ it("does not quote a leading-plus number either", () => {
93
+ const rows = [{ val: "+12.5" } as Row];
94
+ const csv = toCsv(rows);
95
+ expect(csv).not.toContain("'+12.5");
96
+ });
97
+
85
98
  it("null → empty string", () => {
86
99
  const rows = [{ val: null } as Row];
87
100
  const csv = toCsv(rows);
package/src/to-csv.ts CHANGED
@@ -4,10 +4,12 @@
4
4
  * `toCsv` is pure + SSR-safe (no DOM, no deps). `downloadCsv` delegates the
5
5
  * browser save mechanics to `@elabs-ai/components-ui`'s shared `downloadBlob` (one home for
6
6
  * the Blob → `<a download>` dance; @elabs-ai/components-ui is already a peer dep here).
7
- * ChartFrame uses its own local copy of `toCsv` in @elabs-ai/components-charts to avoid a
8
- * cross-sibling dependency (charts data is not allowed per the one-way rule).
7
+ * Value stringification + injection-guarded field quoting live in
8
+ * `@elabs-ai/components-ui`'s `csv` lib (`csvStringifyValue`/`csvQuoteField`) the shared
9
+ * home so `@elabs-ai/components-charts`'s ChartFrame serializer can reuse the same logic
10
+ * without a charts → data dependency (not allowed per the one-way rule).
9
11
  */
10
- import { downloadBlob } from "@elabs-ai/components-ui";
12
+ import { csvQuoteField, csvStringifyValue, downloadBlob } from "@elabs-ai/components-ui";
11
13
 
12
14
  export type CsvColumn<TData> = { key: keyof TData & string; header?: string };
13
15
 
@@ -25,33 +27,8 @@ export interface DownloadCsvOptions<TData> extends ToCsvOptions<TData> {
25
27
  filename?: string;
26
28
  }
27
29
 
28
- /** RFC 4180 injection guard prefixes. */
29
- const INJECTION_PREFIXES = ["=", "+", "-", "@"];
30
-
31
- function stringifyValue(value: unknown): string {
32
- if (value === null || value === undefined) return "";
33
- if (value instanceof Date) return value.toISOString();
34
- if (typeof value === "object") return JSON.stringify(value);
35
- return String(value);
36
- }
37
-
38
- function quoteField(field: string, delimiter: string): string {
39
- // CSV-injection guard: prefix with a single quote if the field starts with a
40
- // formula trigger character.
41
- if (INJECTION_PREFIXES.some((p) => field.startsWith(p))) {
42
- field = "'" + field;
43
- }
44
- // RFC 4180: quote iff the field contains delimiter, double-quote, CR, or LF.
45
- if (
46
- field.includes(delimiter) ||
47
- field.includes('"') ||
48
- field.includes("\n") ||
49
- field.includes("\r")
50
- ) {
51
- return '"' + field.replaceAll('"', '""') + '"';
52
- }
53
- return field;
54
- }
30
+ const stringifyValue = csvStringifyValue;
31
+ const quoteField = csvQuoteField;
55
32
 
56
33
  /**
57
34
  * Serialize rows to a CSV string (no DOM access — safe for SSR / jsdom).