Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/calendar-dates-format-natively.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@pretable/core": minor
"@pretable/react": minor
---

**Breaking:** Make date columns strict RFC 3339 full-date values and add native,
locale-aware date formatting. Applications must project Date, epoch, date-time,
or localized values to `YYYY-MM-DD | null` to retain built-in date processing.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ shared packages and can contend with each other when launched in parallel.
- Getting started: [pretable.ai/docs/getting-started](https://pretable.ai/docs/getting-started)
- Grid API reference: [pretable.ai/docs/grid/api-reference](https://pretable.ai/docs/grid/api-reference)
- Number formatting: [locale-aware decimal, money, and accounting](https://pretable.ai/docs/grid/number-formatting) across cells, aggregates, and clipboard.
- Date formatting: [strict calendar dates with locale-aware display](https://pretable.ai/docs/grid/date-formatting) across cells, aggregates, copy, and CSV.
- Streaming docs: [pretable.ai/docs/streaming](https://pretable.ai/docs/streaming)

## Contributing
Expand Down
72 changes: 40 additions & 32 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Pretable roadmap

Last reviewed: 2026-08-10
Last reviewed: 2026-08-18

Pretable is a batteries-included React data grid built to be the fastest and
best grid for demanding, financial-grade applications. The roadmap uses two
reference workloads: live portfolio-management systems and collaborative
financial planning.
best grid for demanding applications. Financial applications are a proving
ground for performance, precision, dense interaction, and data integrity—not
the product boundary.

## Governing principles

Expand All @@ -15,58 +15,66 @@ financial planning.
- Pretable ships working grid behavior and UI, not interfaces alone.
- One canonical derived model governs rendering, focus, selection, editing,
clipboard, accessibility, and virtualization.
- The grid remains finance-capable but domain-agnostic; applications own
valuation, formulas, permissions, storage endpoints, and retention policy.
- The grid remains domain-agnostic; applications own valuation, formulas,
permissions, storage endpoints, and retention policy.
- Public state and protocols are versioned and migration-aware.

## Current baseline

The aligned public-package baseline is `0.0.14`. The released grid already
includes local sorting and typed filtering, selection, keyboard navigation,
copy and validated bulk paste, typed asynchronous editing, column virtualization
and layout, configurable row grouping and aggregation with an overflow-safe
group panel, number-column alignment with tabular number/date figures,
variable-height rows, and batched streaming transactions. Package changelogs
and generated API reports are the source of truth for shipped behavior.
The latest published aligned-package baseline recorded in the repository is
`0.10.0`. That release includes an incremental typed local row model; sorting,
filtering, grouping, aggregation, distinct values, and transactions;
virtualization and variable-height rows; selection, focus, keyboard navigation,
editing, validated paste, copy, and CSV; native number formatting with money and
accounting presets; cell presentations; stable row identity across entry points;
and server-controlled windowed data with eviction-safe selection, row-height
continuity, and explicit result metadata. Current main additionally contains
unreleased external sort/filter authority suppression. Package changelogs,
pending changesets, and generated API reports distinguish released from
current-main behavior.

See the current [core changelog](packages/core/CHANGELOG.md),
[React changelog](packages/react/CHANGELOG.md),
[core API report](packages/core/core.api.md),
[React API report](packages/react/react.api.md), and
[committed comparative benchmark evidence](status/milestones/2026-05-12-comparator-aware-evaluators.hypotheses.json).
[committed comparative scroll evidence](status/milestones/2026-08-16-s2-comparative-pinned.json)
and [interaction evidence](status/milestones/2026-08-16-s2-mount-and-interaction.json).

## Now — harden and measure
## Now — correct the typed data contract

- Close remaining public API/documentation gaps and make stable row identity
consistent across entry points.
- Add deterministic PMS and financial-planning benchmark profiles without
weakening existing 60 Hz, zero-gap, and interaction-continuity gates.
- Ship canonical calendar-date semantics and native date formatting across the
incremental row model, strict editing and filtering, valid-first sorting,
date extrema, React presentation, copy, and CSV. See the [planned
design](docs/superpowers/specs/2026-08-18-canonical-calendar-dates-row-model-design.md).
- Preserve Pretable's stable scroll and structural efficiency while closing the
measured local sort/filter interaction gap. Add evidence for product work,
not finance-specific workload profiles.
- Keep public APIs, generated reports, migration guidance, and consumer docs
aligned with the breaking typed contract.

## Next — describe and manipulate financial views
## Next — complete reusable analytical workflows

1. Add a financial field schema, nested column bands, field chooser, formatting
descriptors, and view-management surface.
1. Add grand totals and summary rows as domain-neutral companions to grouping
and aggregation.
2. Ship saved-view persistence: a versioned portable view document, migrations,
schema reconciliation, browser-local storage, remote store adapter, and
shared/personal view layers.
3. Add a typed command foundation with atomic edit/paste batches, inverses, and
bounded local undo/redo. Saved views precede command history.

## Later — remote scale, analytics, and collaboration
## Later — remote scale and advanced interaction

1. Add a remote/live row model with bounded caching, typed query plans,
cancellation, partial/stale/error state, and ordered resynchronization.
1. Extend the shipped server-controlled window contract into an optional
remote/live row model with fetching, bounded caching, cancellation, and
ordered resynchronization.
2. Add revisioned mutations with command IDs, base revisions, optimistic and
pessimistic execution, conflicts, rollback, retries, and resync.
3. Develop parallel application tracks:
- PMS analytics: incremental grouping/aggregation, totals, pivot,
drill-through, context actions, and export.
- Financial planning: range editing, fill, row creation/reordering, formula
and provenance surfaces, and time/scenario comparison columns.
3. Add pivoting, range editing, fill, row creation/reordering, and extensible
context actions as general grid capabilities.
4. Add provider-backed durable audit/version history, history UI,
revert-as-new-command, redaction, retention hooks, and collaborative updates.
5. Converge both tracks with shared/personal configuration, capability-aware UI,
tree data, localization, and accessibility hardening.
5. Continue tree data, localization, accessibility, and capability-aware UI
hardening without embedding application-domain policy.

## Not planned

Expand Down
51 changes: 47 additions & 4 deletions apps/website/app/api/docs/rows/__tests__/dataset.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import fs from "node:fs";
import path from "node:path";

import { describe, expect, test } from "vitest";

import {
Expand Down Expand Up @@ -250,6 +253,43 @@ describe("date operators", () => {
expect(row.placedAt <= "2026-04-30").toBe(true);
}
});

test("canonical low-year strings compare without native Date coercion", () => {
const rows = applyDocsQuery(
[
{ ...DOCS_ORDERS[0]!, id: "low", placedAt: "0050-01-02" },
{ ...DOCS_ORDERS[1]!, id: "later", placedAt: "0050-01-03" },
],
{
...EMPTY_DOCS_QUERY,
filters: [
{ columnId: "placedAt", operator: "on", value: "0050-01-02" },
],
},
);

expect(rows.map((row) => row.id)).toEqual(["low"]);
});

test("a semantically invalid string remains active and matches zero rows", () => {
expect(filterBy("placedAt", "on", "2026-02-30")).toEqual([]);
expect(
filterBy("placedAt", "dateBetween", ["2026-01-01", "2026-02-30"]),
).toEqual([]);
});
});

describe("canonical date fixture implementation", () => {
test("does not parse, trim, or coerce date-time values", () => {
const source = fs.readFileSync(
path.resolve(process.cwd(), "app/api/docs/rows/dataset.ts"),
"utf8",
);

expect(source).not.toMatch(
/Date\.parse|ISO_DATETIME_RE|toDayMs|utcDayOf|isoDayMs/,
);
});
});

describe("selection operators", () => {
Expand Down Expand Up @@ -442,13 +482,16 @@ describe("queries this fixture cannot answer", () => {

test("a date range of the wrong length throws", () => {
expect(() => filterBy("placedAt", "dateBetween", ["2026-01-01"])).toThrow(
/exactly two valid ISO dates/,
/exactly two string operands/,
);
});

test("an unparseable date operand throws", () => {
expect(() => filterBy("placedAt", "on", "the first of January")).toThrow(
/valid ISO date operand/,
test("a date operand of the wrong JavaScript type throws", () => {
expect(() => filterBy("placedAt", "on", 1_786_406_400_000)).toThrow(
/string operand/,
);
expect(() => filterBy("placedAt", "on", new Date("2026-08-06Z"))).toThrow(
/string operand/,
);
});

Expand Down
78 changes: 15 additions & 63 deletions apps/website/app/api/docs/rows/dataset.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isValidDateValue } from "@pretable/core";

/** A row of the docs' example order book. */
export interface DocsOrder {
id: string;
Expand Down Expand Up @@ -161,56 +163,6 @@ function isEmptyValue(value: unknown): boolean {
);
}

const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const ISO_DATETIME_RE =
/^(\d{4}-\d{2}-\d{2})[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/i;

function utcDayOf(value: number): number {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return Number.NaN;
date.setUTCHours(0, 0, 0, 0);
return date.getTime();
}

function isoDayMs(value: string): number {
if (!ISO_DATE_RE.test(value)) return Number.NaN;
const [year, month, day] = value.split("-").map(Number) as [
number,
number,
number,
];
const result = Date.UTC(year, month - 1, day);
const roundTrip = new Date(result);

return roundTrip.getUTCFullYear() === year &&
roundTrip.getUTCMonth() === month - 1 &&
roundTrip.getUTCDate() === day
? result
: Number.NaN;
}

/**
* The engine's UTC calendar-day policy (`toDayMs`), minus its pre-year-100
* Gregorian shim — this order book is entirely modern, and a docs fixture that
* carried the shim would be copying code no example can reach.
*/
function toDayMs(value: unknown): number {
if (value instanceof Date) return utcDayOf(value.getTime());
if (typeof value === "number") return utcDayOf(value);
if (typeof value !== "string") return Number.NaN;

const trimmed = value.trim();
const dateOnly = isoDayMs(trimmed);
if (!Number.isNaN(dateOnly)) return dateOnly;

const parts = ISO_DATETIME_RE.exec(trimmed);
if (!parts || Number.isNaN(isoDayMs(parts[1] as string))) return Number.NaN;

return parts[2]
? utcDayOf(Date.parse(trimmed.replace(" ", "T")))
: isoDayMs(parts[1] as string);
}

function columnTypeFor(columnId: string): DocsColumnType {
const type = (DOCS_COLUMN_TYPES as Record<string, DocsColumnType>)[columnId];

Expand Down Expand Up @@ -270,12 +222,12 @@ function assertUsable(
if (
!Array.isArray(operands) ||
(operator === "dateBetween" && operands.length !== 2) ||
operands.some((entry) => Number.isNaN(toDayMs(entry)))
operands.some((entry) => typeof entry !== "string")
) {
throw new DocsQueryError(
operator === "dateBetween"
? `Filter on "${columnId}" needs a range of exactly two valid ISO dates.`
: `Filter on "${columnId}" needs a valid ISO date operand.`,
? `Filter on "${columnId}" needs a range of exactly two string operands.`
: `Filter on "${columnId}" needs a string operand.`,
);
}
return;
Expand Down Expand Up @@ -339,25 +291,25 @@ function matchesDate(
cell: unknown,
operand: unknown,
): boolean {
const day = toDayMs(cell);
if (Number.isNaN(day)) return false;
if (!isValidDateValue(cell)) return false;

if (operator === "dateBetween") {
const range = operand as readonly [unknown, unknown];
const low = toDayMs(range[0]);
const high = toDayMs(range[1]);
return day >= Math.min(low, high) && day <= Math.max(low, high);
const [first, second] = operand as readonly [string, string];
if (!isValidDateValue(first) || !isValidDateValue(second)) return false;
const lower = first <= second ? first : second;
const upper = first <= second ? second : first;
return cell >= lower && cell <= upper;
}

const other = toDayMs(operand);
if (!isValidDateValue(operand)) return false;

switch (operator) {
case "on":
return day === other;
return cell === operand;
case "before":
return day < other;
return cell < operand;
case "after":
return day > other;
return cell > operand;
default:
// Unreachable by construction: `assertUsable` rejects any operator
// outside DOCS_FILTER_OPERATORS before dispatch. It stays as the failure
Expand Down
81 changes: 81 additions & 0 deletions apps/website/app/docs/__tests__/date-formatting.types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
createColumnHelper,
createLocalRowModel,
isValidDateValue,
type PretableDateFormatOptions,
} from "@pretable/core";
import { PretableSurface } from "@pretable/react";

/** Compile-time fixture for every fence on `grid/date-formatting.mdx`. */

interface Invoice {
id: string;
due: string | null;
}

interface Schedule {
id: string;
startsOn: string | null;
}

// docs-fence: grid/date-formatting.mdx#Validate at the boundary
export function calendarDateOrNull(value: unknown): string | null {
if (value === null) return null;
if (!isValidDateValue(value)) throw new Error("Expected YYYY-MM-DD or null");
return value;
}

// docs-fence: grid/date-formatting.mdx#Rows mode
const column = createColumnHelper<Invoice>();
const columns = [
column.accessor("due", {
type: "date",
header: "Due",
aggregate: "max",
dateFormat: { dateStyle: "medium" },
}),
] as const;

export function InvoiceGrid({ rows }: { rows: readonly Invoice[] }) {
return (
<PretableSurface
ariaLabel="Invoices"
columns={columns}
locale="en-US"
rows={rows}
viewportHeight={360}
/>
);
}

// docs-fence: grid/date-formatting.mdx#Explicit-model presentation
const scheduleColumn = createColumnHelper<Schedule>();
const modelColumns = [
scheduleColumn.accessor("startsOn", {
type: "date",
aggregate: "min",
header: "Starts",
dateFormat: { year: "numeric", month: "short", day: "2-digit" },
}),
] as const;
const model = createLocalRowModel({
rows: [{ id: "s1", startsOn: "2026-08-18" }],
columns: modelColumns,
});

export const scheduleGrid = (
<PretableSurface
ariaLabel="Schedule"
columns={modelColumns}
locale="en-GB"
model={model}
viewportHeight={280}
/>
);

const dateOptions = {
year: "numeric",
month: "short",
day: "2-digit",
} satisfies PretableDateFormatOptions;
void dateOptions;
Loading
Loading