Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
70c6630
docs: audit component kit and specify lifecycle repairs
blove Sep 8, 2026
108c09c
docs: plan approved lifecycle safety repairs
blove Sep 8, 2026
8f4268a
fix(grid-core): preserve pending edit drafts
blove Sep 8, 2026
c8e94e4
fix(react): enforce cell edit lifecycle admission
blove Sep 8, 2026
b70e663
fix(react): await parser results within edit lifecycle
blove Sep 8, 2026
bc6c306
fix(react): keep pending cell editors inert
blove Sep 8, 2026
c1cc829
docs: verify and document edit lifecycle repairs
blove Sep 8, 2026
d02ca88
docs: specify shared control state repairs
blove Sep 8, 2026
8b6a1bf
fix(react): compose control refs with cleanup support
blove Sep 8, 2026
e0a648d
fix(react): reconcile select options and typeahead sessions
blove Sep 8, 2026
356e447
fix(react): integrate and verify shared control repairs
blove Sep 8, 2026
bedfecf
docs: specify overlay ownership and state repairs
blove Sep 9, 2026
21421a7
fix(ui): distinguish disabled and forced-color kit states
blove Sep 9, 2026
19cd1ba
fix(react): preserve overlay ownership across scoped portals
blove Sep 9, 2026
bd5fe3c
fix(react): reanchor open popups after scope layout changes
blove Sep 9, 2026
04dc1b5
fix(react): integrate and verify scoped overlay repairs
blove Sep 9, 2026
f309b88
docs: specify final component kit editor repairs
blove Sep 9, 2026
45f9e07
fix(react): preserve editor identity and adopt native field slots
blove Sep 9, 2026
71c4c13
fix(react): follow commit direction after controlled row acknowledgement
blove Sep 9, 2026
e55d531
fix: complete and verify component kit editor integration
blove Sep 9, 2026
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
25 changes: 25 additions & 0 deletions .changeset/finish-editor-kit-correctness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@pretable/react": minor
"@pretable/ui": minor
---

Finish editor adoption of the component kit. Text, number, enum and date
fields resolve TextInput; multiline fields resolve the new native Textarea
slot. Number steppers and date month buttons resolve IconButton. Preserve
forwarded refs, native props, handlers and ARIA/data hooks in replacements.

Correct enum identity and clearing. Explicit choices retain their canonical
value even with duplicate labels; ambiguous typed labels/values require a
choice. Empty queries can commit null. Custom parsers receive canonical value
text for an explicit choice and query text for typed input.

Composition keys retain native behavior. Shift+Tab commits left and Shift+Enter
up, including when controlled rows acknowledge a save later; cancelled or
replaced sessions ignore stale acknowledgements. Multiline plain Enter remains a newline. Calendar navigation now browses
without changing the draft: Enter accepts an intentionally navigated date,
while Tab/blur commit the typed/current value. Date inputs expose a complete
grid-popup combobox contract.

Preserve editor error/focus/pending styling after kit adoption, prevent error
messages from squeezing fields sideways, and complete disabled and forced-color
calendar/action states. Upgrade React and UI together for the new site hooks.
14 changes: 14 additions & 0 deletions .changeset/guard-pending-edit-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@pretable/react": patch
"@pretable/core": patch
---

Prevent cell edits from committing before permission resolves or dispatching
duplicate validation/save operations. Recover from permission, parsing,
validation, and save callback failures without leaving a pending editor stuck,
and ignore stale results after an edit session is replaced or cancelled.

Draft writes during checking, validating, and saving are now ignored, including
writes from custom editors. This intentionally removes the previous behavior
where a draft write could unlock a pending edit. Enum label normalization waits
for edit permission, and pending editor commit keys do not submit another write.
21 changes: 21 additions & 0 deletions .changeset/repair-overlay-scope-and-states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@pretable/react": minor
"@pretable/ui": minor
---

Add PretableOverlayProvider for an explicit popup container. Place the grid
and portal host in the same CSS scope to preserve custom tokens, direction
and live theme changes while escaping the grid's clipping viewport. The
provider does not copy trigger styles. Without a provider, popups still use
body; a provider with a null target defers popup content until attachment.

Track nested portal ownership for outside presses. Sibling Selects close one
another, clicks inside nested lists preserve the parent dialog, and trigger
toggles work without hiding outside pointer events.
Keep Select and header popups anchored when live scope or layout changes move
their trigger, including delayed portal attachment.

Rename the listbox active marker from data-active to data-pretable-active.
Update custom selectors and upgrade the React/UI packages together. Complete
disabled option styles and forced-colors active-option/checkbox state pairs,
including selected-disabled options and checked/mixed disabled checkboxes.
17 changes: 17 additions & 0 deletions .changeset/repair-shared-control-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@pretable/react": minor
---

Keep Select highlights attached to option values when options reorder, and
choose an enabled fallback when an active option is removed or disabled.
Empty rosters close the Select; all-disabled lists have no active option.
Typeahead starts fresh each time the list opens.

Rich Select option labels now require a plain `textValue` for typeahead.
String and number labels continue to infer their text. This tightens
`PretableSelectOption` from an interface to a union: add `textValue` to rich
labels and replace interfaces extending the old type with type intersections.

Preserve callback-ref cleanup in Select, Checkbox and TextInput, including
ref replacement and StrictMode attachment cycles. Object refs and callbacks
without cleanup still receive their usual null detach.
132 changes: 132 additions & 0 deletions apps/website/app/fixtures/editor-kit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use client";

import { forwardRef, useState } from "react";
import {
PretableSurface,
PretableOverlayProvider,
PretableTextInput,
PretableTextarea,
PretableIconButton,
type PretableColumn,
type PretableTextInputComponent,
type PretableTextareaComponent,
type PretableIconButtonComponent,
} from "@pretable/react";

interface Row {
id: string;
name: string;
quantity: number | null;
status: string | null;
date: string | null;
notes: string;
}
const INITIAL: Row[] = [
{
id: "a",
name: "Alpha",
quantity: 2,
status: "b",
date: "2026-08-18",
notes: "First line",
},
{
id: "b",
name: "Beta",
quantity: 3,
status: "a",
date: "2026-08-22",
notes: "Second line",
},
];
const COLUMNS: PretableColumn<Row>[] = [
{ id: "name", header: "Name", editable: true, widthPx: 140 },
{
id: "quantity",
header: "Quantity",
type: "number",
editable: true,
widthPx: 150,
validate: (value) =>
typeof value === "number" && value < 0 ? "Use a positive quantity" : true,
},
{
id: "status",
header: "Status",
type: "enum",
editable: true,
widthPx: 160,
formatEditValue: () => "Same",
options: [
{ value: "a", label: "Same" },
{ value: "b", label: "Same" },
{ value: "Same", label: "Collision" },
],
},
{ id: "date", header: "Date", type: "date", editable: true, widthPx: 160 },
{ id: "notes", header: "Notes", editable: true, wrap: true, widthPx: 230 },
];
const getRowId = (row: Row) => row.id;
const Input: PretableTextInputComponent = forwardRef(
function Input(props, ref) {
return <PretableTextInput {...props} ref={ref} data-fixture-input="" />;
},
);
const Textarea: PretableTextareaComponent = forwardRef(
function Textarea(props, ref) {
return <PretableTextarea {...props} ref={ref} data-fixture-textarea="" />;
},
);
const IconButton: PretableIconButtonComponent = forwardRef(
function IconButton(props, ref) {
return (
<PretableIconButton {...props} ref={ref} data-fixture-icon-button="" />
);
},
);

export default function EditorKitFixture() {
const [rows, setRows] = useState(INITIAL);
const [saved, setSaved] = useState(0);
const [dark, setDark] = useState(false);
const [compact, setCompact] = useState(false);
const [host, setHost] = useState<HTMLDivElement | null>(null);
return (
<main
style={{ padding: 24 }}
data-theme={dark ? "dark" : "light"}
data-density={compact ? "compact" : "standard"}
>
<h1>Editor kit verification</h1>
<button type="button" onClick={() => setCompact((value) => !value)}>
Toggle compact
</button>
<button type="button" onClick={() => setDark((value) => !value)}>
Toggle theme
</button>{" "}
<button type="button">Outside editor</button>
<div style={{ marginTop: 16 }}>
<PretableOverlayProvider container={host}>
<PretableSurface
key={String(compact)}
ariaLabel="Editor kit"
rows={rows}
columns={COLUMNS}
getRowId={getRowId}
viewportHeight={260}
components={{ TextInput: Input, Textarea, IconButton }}
onRowChange={({ rowId, row }) => {
setRows((previous) =>
previous.map((item) => (item.id === rowId ? row : item)),
);
setSaved((count) => count + 1);
}}
/>
</PretableOverlayProvider>
</div>
<div ref={setHost} data-editor-overlay-host="" />
<output data-saved-count="">{saved}</output>
<pre data-saved-rows="">{JSON.stringify(rows, null, 2)}</pre>
</main>
);
}
119 changes: 119 additions & 0 deletions apps/website/app/fixtures/overlay-scope/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import {
PretableOverlayProvider,
PretableSelect,
PretableSurface,
type PretableColumn,
} from "@pretable/react";
import { useState, type CSSProperties } from "react";

const OPTIONS = [
{ value: "alpha", label: "Alpha" },
{ value: "beta", label: "Beta" },
{ value: "gamma", label: "Gamma" },
{ value: "disabled", label: "Unavailable", disabled: true },
];
const ROWS = [
{ id: "one", name: "Alpha" },
{ id: "two", name: "Beta" },
];
const getRowId = (row: (typeof ROWS)[number]) => row.id;
const COLUMNS: PretableColumn<(typeof ROWS)[number]>[] = [
{ id: "name", header: "Name", type: "text", widthPx: 300 },
];

function Scope({
name,
initiallyDark,
}: {
name: string;
initiallyDark: boolean;
}) {
const [host, setHost] = useState<HTMLDivElement | null>(null);
const [dark, setDark] = useState(initiallyDark);
const [rtl, setRtl] = useState(initiallyDark);
const [first, setFirst] = useState("alpha");
const [second, setSecond] = useState("beta");
const scopeStyle = {
"--pretable-bg-grid": dark ? "rgb(17, 34, 51)" : "rgb(240, 248, 255)",
"--pretable-bg-grid-alt": dark ? "rgb(17, 34, 51)" : "rgb(240, 248, 255)",
"--pretable-text-cell": dark ? "rgb(255, 248, 220)" : "rgb(20, 40, 60)",
"--pretable-rule-strong": dark ? "rgb(150, 180, 210)" : "rgb(50, 70, 90)",
colorScheme: dark ? "dark" : "light",
padding: 16,
border: "1px solid #888",
} as CSSProperties;
return (
<section
data-overlay-scope={name}
data-theme={dark ? "dark" : "light"}
dir={rtl ? "rtl" : "ltr"}
style={scopeStyle}
>
<h2>{name}</h2>
<button type="button" onClick={() => setDark((value) => !value)}>
Toggle {name} theme
</button>{" "}
<button type="button" onClick={() => setRtl((value) => !value)}>
Toggle {name} direction
</button>
<PretableOverlayProvider container={host}>
<div
data-scope-clip=""
style={{
contain: "content",
overflow: "hidden",
height: 190,
marginTop: 12,
}}
>
<PretableSurface
ariaLabel={`${name} grid`}
columns={COLUMNS}
rows={ROWS}
getRowId={getRowId}
viewportHeight={132}
/>
<div style={{ display: "flex", gap: 12, marginTop: 12 }}>
<PretableSelect
aria-label={`${name} first`}
options={OPTIONS}
value={first}
onChange={setFirst}
/>
<PretableSelect
aria-label={`${name} second`}
options={OPTIONS}
value={second}
onChange={setSecond}
/>
</div>
</div>
</PretableOverlayProvider>
<div ref={setHost} data-overlay-host={name} />
</section>
);
}

export default function OverlayScopeFixture() {
return (
<main style={{ padding: 24 }}>
<h1>Scoped overlay fixture</h1>
<button type="button" onPointerDown={(event) => event.stopPropagation()}>
Outside with stopped bubbling
</button>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: 24,
marginTop: 24,
}}
>
<Scope name="Morning" initiallyDark={false} />
<Scope name="Evening" initiallyDark />
</div>
</main>
);
}
Loading