Skip to content
Draft
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
27 changes: 27 additions & 0 deletions .changeset/bulk-item-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@tailor-platform/app-shell": minor
---

Add `BulkItemPicker` — a generic tree-select dialog for bulk choosing from a hierarchical list.

Designed for "Bulk add" flows on documents that pick from a catalog with optional sub-items (products with variants, accounts with sub-accounts, categories with tags, etc.). Decoupled from `LineItems` so it can be used anywhere a multi-select-from-tree is needed.

- Generic via render-props: consumer supplies `items[]` + `renderRow` + optional `renderMetric` for the right-aligned column. The picker draws the checkbox column, expand/collapse caret, search, and Add-N-items CTA.
- Selection is leaf-only. Parent rows show a tri-state checkbox derived from descendant leaves; clicking a parent selects/deselects all descendants.
- Built-in case-insensitive search (overridable via `matchesSearch`); parents auto-expand when a descendant matches the query.
- Selection state resets on close so each open starts fresh.
- Exports: `BulkItemPicker`, `BulkItemPickerProps<T>`, `BulkItemPickerNode<T>`.

```tsx
<BulkItemPicker<MyNode>
open={open}
onOpenChange={setOpen}
title="Bulk picker"
rowLabel="Product Name"
metricLabel="Total available"
items={tree}
renderRow={(node) => <span>{node.data.name}</span>}
renderMetric={(node) => node.data.available}
onCommit={(leaves) => addLines(leaves.map(toLine))}
/>
```
11 changes: 11 additions & 0 deletions .changeset/line-items-add-lines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: `useLineItems` now exposes `addLines(items, opts?)` for batched inserts.

Same shape as `addLine` but takes an array and funnels through a single state mutation, so committing N picks from a bulk picker is one render and one logical operation in the change set instead of N. Returns the new `lineRef`s in input order.

```tsx
const refs = lineItems.addLines(selectedVariants.map((v) => ({ sku: v.id, quantity: 1, ...rest })));
```
5 changes: 5 additions & 0 deletions .changeset/line-items-amend-readonly-tint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/app-shell": patch
---

LineItems: in `amend` mode, cells that aren't editable in amend now get a subtle `bg-muted/40` background so users can see at a glance which cells they can touch and which are locked. Selection ring + fill preview still override the tint when active. No effect in `edit` or `display` mode.
5 changes: 5 additions & 0 deletions .changeset/line-items-card-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: new `LineItems.SearchToggle` part — an icon button that expands inline into a search input on click, with a 200ms width transition. Collapses on blur (when the filter is empty) or Escape. Reads/writes the same `filter` state as `LineItems.Search`, so the two are interchangeable.
10 changes: 10 additions & 0 deletions .changeset/line-items-cell-bounds-polish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@tailor-platform/app-shell": patch
---

LineItems: cell bounds and selection ring now align pixel-perfect with the cell box.

- The grid cell shell renders directly inside the `<td>` as `absolute inset-0` instead of being nested under an extra wrapper, so the cell box, the input, and the selection rectangle all share identical bounds.
- Active-cell and fill-preview selection borders are painted via an `inset` box-shadow on the cell shell rather than an absolute-positioned overlay, eliminating any size/rounding mismatch.
- Default virtualized row height bumped from 32px to 36px for slightly more breathing room around input text.
- Fill drag handle is now a smaller, flush 6×6px square seated on the cell's bottom-right corner.
21 changes: 21 additions & 0 deletions .changeset/line-items-dense-sku-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@tailor-platform/app-shell": minor
---

**LineItems:** Default table chrome is denser and card-aligned: `bg-card`, column/row dividers, no header shadow, `32px` virtual row height, zero cell padding, and borderless cell inputs so the grid reads like a spreadsheet.

**LineItems:** `LineItemsField.type` adds `kind: "select"` with `options: { value, label, description? }[]` and optional `placeholder`. Options render in a combobox with a two-line layout when `description` is set.

```tsx
f.field({
key: "sku",
label: "SKU",
type: {
kind: "select",
options: [{ value: "A", label: "A", description: "Line two" }],
placeholder: "Pick…",
},
render: (row) => row.sku,
editable: ["edit"],
});
```
9 changes: 9 additions & 0 deletions .changeset/line-items-equals-normalize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: per-field `equals?` and `normalize?` hooks on `LineItemsField`.

App-supplied callbacks let consumers override dirty-equality and value coercion for fields the component can't normalize on its own — e.g. id-based equality on attribute objects (`equals: (a, b) => a.id === b.id`), deep-equal on JSON blobs, currency-aware money comparison, custom rounding rules. The built-in tolerance-aware numeric equality and string trim still apply when the field doesn't supply its own.

Resolution order: field-level `equals` / `normalize` win over `kind: "custom"` type-level `equals` / `normalize`, which win over the built-in numeric defaults.
11 changes: 11 additions & 0 deletions .changeset/line-items-field-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: three new `LineItemsFieldType` variants.

- `{ kind: "boolean"; trueLabel?; falseLabel? }` — checkbox cell with full keyboard nav. Default alignment is `center`.
- `{ kind: "date"; min?; max? }` — native `<input type="date">` cell using ISO `yyyy-mm-dd` values. Empty value commits as `null`.
- `{ kind: "custom"; renderEditor; normalize?; equals? }` — escape hatch. Apps drop in any React editor (async product picker, attribute selector, currency-pair input, …). The editor receives `{ value, onCommit, onCancel, row, mode, field }` and routes commits through the standard hook so dirty-tracking and the change-set keep working.

Numeric fields now default to `align: "right"` (with `tabular-nums`); boolean fields default to `align: "center"`. Apps that explicitly set `align` keep their value.
8 changes: 8 additions & 0 deletions .changeset/line-items-flex-and-shift-click.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: two related fixes for table interaction and layout consistency.

- **`LineItemsField.flex?: boolean`** — opt-in flag that makes a column absorb leftover horizontal space (typical use: a description / product-name column with the longest content). When at least one field is `flex` (or has no declared `width`), the table drops its trailing spacer and routes leftover space to the flagged column. Tables where every column has an explicit width still get an invisible trailing spacer so column widths stay pixel-exact across pages.
- **Shift-click multi-select fix** — `onCellFocused` no longer overwrites the selection anchor when an input fires its native focus event after a shift-click. Anchor is now owned exclusively by `onCellPointerDown` and the keyboard-nav handlers, so shift-click on a different cell now correctly extends the rectangular selection.
35 changes: 35 additions & 0 deletions .changeset/line-items-floating-dock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: floating-bar UX is now a first-class library pattern.

Three new compound parts hosted at the bottom-center of the viewport when the hook reflects the relevant state. Hook-driven (auto-mount/unmount), composable, no boilerplate per page.

- **`LineItems.FloatingDock`** — fixed bottom-center container that hosts the bars and stacks them vertically. `pointer-events: none` outer / `auto` inner so the surrounding area stays click-through.
- **`LineItems.DirtyBar`** — auto-shows when `useLineItemsRoot().hook.isDirty`. Discard button defaults to `hook.revert()`; Save calls the consumer's `onSave`. Optional `warnOnNav` prop intercepts in-app anchor clicks + browser `beforeunload` so the user can't leave with unsaved changes; bar jiggles to draw attention.
- **`LineItems.SelectionBar<T>`** — render-prop bar that auto-shows when rows are selected. Library renders the dark pill chrome (count label + divider); consumer plugs in domain-specific actions (Delete, Export PDF, Update price, etc.) via render-prop receiving `{selectedIds, bulkUpdate, bulkRemove, clear}`.
- **`lineItemsFloatingBarStyles`** — exported style helpers (`primaryButton`, `secondaryButton`, `divider`, `label`) so consumers can build buttons inside the SelectionBar render-prop that visually match the DirtyBar's defaults.

```tsx
<LineItems.Root value={lineItems}>
<LineItems.Table />
<LineItems.FloatingDock>
<LineItems.DirtyBar warnOnNav onSave={onSave} />
<LineItems.SelectionBar>
{({ bulkRemove, clear }) => (
<>
<button style={lineItemsFloatingBarStyles.primaryButton} onClick={bulkRemove}>
Delete
</button>
<button style={lineItemsFloatingBarStyles.secondaryButton} onClick={clear}>
Clear
</button>
</>
)}
</LineItems.SelectionBar>
</LineItems.FloatingDock>
</LineItems.Root>
```

`LineItems.BulkActions` (the inline-toolbar variant) stays unchanged for apps that don't want the floating dock.
8 changes: 8 additions & 0 deletions .changeset/line-items-fullscreen-card-fill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tailor-platform/app-shell": patch
---

LineItems: fullscreen mode polish.

- Clicking the dimmed backdrop around the card now closes fullscreen (in addition to the existing Escape-to-close).
- When the table is rendered inside a `Card`, fullscreen now stretches the immediate `Card` child to fill the viewport and expands its `Card.Content` so the `LineItems.Table` scrolls inside the card. Layout rules are scoped via descendant selectors on the fullscreen root, so only `data-slot="card"` / `data-slot="card-content"` are affected.
9 changes: 9 additions & 0 deletions .changeset/line-items-fullscreen-shift-paste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tailor-platform/app-shell": patch
---

LineItems: fullscreen open animation + shift-click text-selection fix + single-cell broadcast paste.

- Fullscreen modal now slides + fades in on open (backdrop fade 220ms, card translate-from-below 280ms with a soft cubic-bezier ease-out). Close is instant.
- Shift-click range selection no longer paints the browser's native text-selection across editable cells between the previous focus and the click point. Handler now blurs any focused editor, calls `removeAllRanges()`, and parks focus on the grid container.
- Paste a single copied cell into a multi-cell selection now broadcasts that value to every selected cell (Excel / Sheets parity), skipping read-only columns with the existing toast.
13 changes: 13 additions & 0 deletions .changeset/line-items-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: new `useLineItemsGroup({ a, b, ... })` hook.

Composes multiple `useLineItems` hooks into a single document-level boundary. Use it when one header record owns more than one ordered list of lines — e.g. a Journal Entry with `debits` + `credits` that must balance, or a Work Order with `componentLines` + `operationLines`. Each collection still gets its own table + selection + dirty tracking; the group helper provides:

- `isDirty` — `true` when **any** member is dirty
- `getChangeSet()` — keyed bundle `{ isEmpty, [memberName]: ChangeSet }` so the page-level submit handler dispatches one transactional mutation
- `reset()` / `revert()` — fan out to every member at once

See the new `journal-entry-demo` for a complete worked example with a balance-check header derived from both collections.
104 changes: 104 additions & 0 deletions .changeset/line-items-hooks-rewrite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: rewrite around a public `useLineItems()` hook + compound components.

The line-items module now follows the "hooks own logic, components own layout" pattern. Variants (Journal Entry, Work Order, Purchase Order baseline) compose freely instead of demanding ever-more props.

### What's new

- **`useLineItems<T>()`** is the single source of truth for a document. It owns row order, the dirty baseline, filter, bulk row selection, and every imperative mutation (`addLine`, `updateField`, `updateLines`, `removeLine`, `reorderLine`, `toggleSelect`, `selectAllVisible`, `clearSelection`, `bulkUpdate`, `bulkRemove`, `duplicateLastLine`, `reset`, `getChangeSet`). `isDirty` is reactive — no more `onChangeSet` push callback.
- **`createLineItemHelper<T>().field({...})`** is the typed schema builder. Each field declares `key`, `label`, `render(line)`, `editable: LineItemsMode[]`, `type: { kind: "text" | "number"; decimals? }`, `commit: "document" | "metadata"`, `sort.comparator`, `search`, `align`, `className`. Computed read-only columns just pick a unique `key` and omit `editable` / `type`.
- **`LineItems.*` compound components** replace the monolithic `<LineItems columns={...} />`:
- `<LineItems.Root value={hook}>` — provider + fullscreen container (Esc exits).
- `<LineItems.Table maxBodyHeight={1000} />` — virtualized table with always-on spreadsheet UX (range select, fill-drag, TSV copy/paste, keyboard nav).
- `<LineItems.Search />` — controlled by `hook.filter` / `setFilter`.
- `<LineItems.BulkActions>{({ selectedIds, bulkUpdate, bulkRemove, clear }) => …}</LineItems.BulkActions>` — render-prop, gated on selection.
- `<LineItems.AddRow>` — children-as-slot for an inline empty row beneath the table.
- `<LineItems.FullscreenToggle />` — hoistable expand button (default rendered inside `<LineItems.Table />`).
- `<LineItems.SaveActions onSave={…} />` — Discard + Save, auto-disabled on `!isDirty`.
- The `cellInteraction` mode is dropped — spreadsheet behaviors are always-on.

### BREAKING

The previous `<LineItems columns={…} initialLines={…} onChangeSet={…} />` API is removed entirely. The following types are no longer exported: `LineItemsRootRef`, `LineItemsCellInteractionMode`, `LineItemsColumnDef`, `LineItemsAddSlotRenderArgs`, `LineItemsInlineAddRowRenderArgs`, `LineItemsMutationScope`, `LineItemsCellRendererContext`. The `LineItemsQuickAdd` component is also removed.

### Migration

```tsx
// Before
const ref = React.useRef<LineItemsRootRef>(null);
<LineItems<POLine>
ref={ref}
initialLines={initial}
columns={[
{ id: "sku", header: "SKU", accessorKey: "sku", sortable: true },
{
id: "qty",
header: "Qty",
accessorKey: "qty",
align: "right",
normalize: (v) => Number(v),
equals: (a, b) => Number(a) === Number(b),
},
{ id: "note", header: "Note", accessorKey: "note", mutationScope: "metadata" },
]}
onChangeSet={(cs) => setChangeSet(cs)}
enableBulkActions
cellInteraction="spreadsheet"
/>;

// After
const f = createLineItemHelper<POLine>();
const fields = [
f.field({
key: "sku",
label: "SKU",
render: (l) => l.sku,
editable: ["edit"],
type: { kind: "text" },
sort: { comparator: (a, b) => a.sku.localeCompare(b.sku) },
search: (l, q) => l.sku.toLowerCase().includes(q.toLowerCase()),
}),
f.field({
key: "qty",
label: "Qty",
render: (l) => l.qty,
editable: ["edit", "amend"],
type: { kind: "number", decimals: 0 },
align: "right",
}),
f.field({
key: "note",
label: "Note",
render: (l) => l.note,
editable: ["edit", "amend"],
type: { kind: "text" },
commit: "metadata",
}),
];

const lineItems = useLineItems<POLine>({ fields, data: initial, selection: true });

<LineItems.Root value={lineItems}>
<LineItems.Search />
<LineItems.BulkActions>
{({ bulkRemove, clear }) => (
<>
<Button onClick={bulkRemove}>Delete</Button>
<Button variant="ghost" onClick={clear}>
Clear
</Button>
</>
)}
</LineItems.BulkActions>
<LineItems.Table maxBodyHeight={1000} />
<LineItems.AddRow>
{/* host JSX, e.g. <Combobox onValueChange={(p) => lineItems.addLine(...)} /> */}
</LineItems.AddRow>
<LineItems.SaveActions onSave={() => save(lineItems.getChangeSet())} />
</LineItems.Root>;
```

Imperative ref methods are replaced 1:1 by hook methods: `ref.resetBaseline()` → `lineItems.reset()`, `ref.getChangeSet()` → `lineItems.getChangeSet()`, `ref.hasChanges()` → `lineItems.isDirty`, `ref.duplicateLastLine()` → `lineItems.duplicateLastLine()`. `ref.focusLine()` is no longer provided — focus the cell directly through the DOM if needed.
8 changes: 8 additions & 0 deletions .changeset/line-items-hover-expand-column.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: column-level `width` and `hoverExpandWidth` options.

- New `LineItemsField.width` sets a resting pixel width for the column (otherwise auto-sized).
- New `LineItemsField.hoverExpandWidth` widens the column to that pixel value while the user hovers any cell (header or body) in that column, with a subtle 220ms `width` / `min-width` transition. Useful for dense columns that show truncated content (e.g. a SKU label that includes the product name).
11 changes: 11 additions & 0 deletions .changeset/line-items-pinned-columns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: new `LineItemsField.pinned?: "left" | "right"` per-field option.

Pinned columns stay visible while the user scrolls horizontally — `position: sticky` with offsets accumulated from preceding pinned columns of the same side. Useful for dense tables where the SKU / Product columns should stay anchored as the user scrolls through 8+ data columns.

Pinned columns must declare a `width` so subsequent offsets can be computed. The selection checkbox column (`__select`) is auto-pinned to the left when present.

Demo: `/custom-page/goods-receipt-demo` pins SKU + Product on the left while horizontal-scrolling Condition / Lot / Expiry.
13 changes: 13 additions & 0 deletions .changeset/line-items-polish-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@tailor-platform/app-shell": minor
---

LineItems: polish pass for the spreadsheet experience.

- New `renderInlineAddRow` slot renders an always-visible empty row directly below the scrollable table. Hosts can drop a `Combobox` (or any control) into it to add lines via type-ahead search.
- New `enableFullscreenToggle` prop (default `true`) shows a small expand icon in the top-right corner of the table; clicking it (or pressing `Esc` again) toggles a viewport-filling fullscreen overlay with a dark backdrop. Component state is preserved across the toggle.
- Spreadsheet selection visuals now use the brand `--primary` color: the active cell ring, the fill drag handle, the per-cell ring on every shift-selected cell, and the per-cell ring on every cell of an active fill-drag preview (matching the active cell's border).
- Native up/down spinner buttons are hidden globally on every `<Input type="number">` for a cleaner numeric editing experience.
- The "scroll active cell into view" effect now runs only when the focused coordinate actually changes, fixing a regression where typing in cells (and in the inline-add-row Combobox) could drop focus mid-keystroke. Smooth-scroll animation on cell focus has been removed for the same reason.
- The inline add-row sits as a sibling below the scroll container instead of inside `<tbody>`, so it remains anchored even when the table is virtualized and scrolled. `maxBodyHeight` JSDoc clarifies that the table grows up to this height before scrolling and that the add-row sits below the scroll area.
- BREAKING: `pageSize` and `showDirtyOffPageBanner` props are removed. Line items now always render as a single virtualized list — large lists load on scroll automatically, no pagination UI is rendered. Hosts that were paginating client-side should drop both props; the virtualizer handles thousands of rows efficiently.
15 changes: 15 additions & 0 deletions .changeset/line-items-prd-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@tailor-platform/app-shell": major
---

LineItems: align `getChangeSet()` shape with the platform PRD ("Generalized Line-Item Component").

**Breaking** field-name changes inside `LineItemsLineChange`:

- `add` → `{ tempId, data }` (was `{ lineRef, insertAfterLineRef, patch }`). `tempId` is a client-only id; the server should mint the persistent id on insert.
- `update` / `remove` → keyed on `lineId` (was `lineRef`).
- `move` action renamed to `reorder`; payload is `{ lineId, position: number }` — zero-based final index in document order. Replaces the previous after-cursor model.

`LineItemsChangeSet` now carries a top-level `isEmpty: boolean` flag for ergonomic no-op detection (`if (cs.isEmpty) return`).

Migration: rename your switch arms and field reads (e.g. `change.lineRef` → `change.lineId`; `add.patch` → `add.data`; `move`/`afterLineRef` → `reorder`/`position`). Behavior is unchanged — only field names move.
Loading
Loading