@rivinia/table 1.0.0 → 1.0.1

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 (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +41 -264
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Documentation: README restructured into docs/table/ pages; stale pre-1.0 wording removed.
6
+
3
7
  ## 1.0.0
4
8
 
5
9
  - First stable release. Row identity must be unique and stable; ambiguous IDs cannot own selection or other identity-dependent state.
package/README.md CHANGED
@@ -3,290 +3,67 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/table.svg)](https://www.npmjs.com/package/@rivinia/table)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/table.svg)](https://www.npmjs.com/package/@rivinia/table)
5
5
 
6
- <p align="center">
7
- <img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/table/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
8
- </p>
6
+ A headless Angular table controller with typed columns, spans, selection, and optional grid UI.
9
7
 
10
- A headless Angular table with typed columns, real row and column spans,
11
- sorting, filtering, selection and paging — plus an optional ready-made grid.
12
- The core owns the data and the geometry; it never renders a cell, never knows
13
- what an "action" is, and never imports a stylesheet.
14
-
15
- - Package version: **1.0.0**
16
- - Requires: **Angular `^22.0.0`** (`@angular/core` and `@angular/common`).
17
- - Entry points: `@rivinia/table` (headless controller + pure helpers) and
18
- `@rivinia/table/ui` (the `rv-table` grid, div-based, `role="grid"`).
19
- - Side effects: **none** (`sideEffects: false`).
20
-
21
- ## Quick features
22
-
23
- - **Headless first.** The controller is signals and pure functions — no DOM, no
24
- browser needed to test the pipeline. The grid is a component you can throw
25
- away and replace with your own markup.
26
- - **Real spans, not CSS tricks.** `merge: 'equal'` for consecutive equal values,
27
- `colSpan: n`, or a per-cell `span(ctx)` for both axes. The result is a resolved
28
- matrix, so the keyboard, the ARIA and the pinned columns all follow the real
29
- geometry.
30
- - **One pipeline.** Filter → sort → page → selection, in that order, every step
31
- a signal, so a page never recomputes more than it must.
32
- - **Selection by id, never by index.** It survives sorting, filtering and
33
- paging, and an action always receives the row it was rendered for.
34
- `selectAllOnPage()` is page-scoped on purpose.
35
- - **A shareable view.** `table.state` is the serialisable slice (sort, filters,
36
- search, page, page size, selection) — put it in the URL and `setState()`
37
- restores what the user sees.
38
- - **The actions column is yours.** `display('actions')` reserves the column;
39
- `<ng-template rvTableCell="actions">` fills it. Five icons, one link, or a
40
- permission-driven set — the grid passes the row and gets out of the way.
41
- - **Four bodies, not four screens.** Loading skeletons that keep the header
42
- widths, an empty state, an error alert and the data, all inside one grid, all
43
- live regions.
44
- - **Right-to-left ready.** Every inset is logical, the header mirrors with the
45
- body, labels ship in English and Arabic, and each value carries `dir="auto"`
46
- so an English name stays intact in an Arabic table.
47
- - **Themable end to end.** `--rv-table-*` beats `--rv-*` from `@rivinia/theme`,
48
- which beats the built-in default; sizes are `rem`, so the grid follows the
49
- browser's text size.
50
- - **Zero runtime dependencies.** Angular peers only.
51
-
52
- ## Install
53
-
54
- ```bash
55
- npm install @rivinia/table
56
- ```
57
-
58
- or
59
-
60
- ```bash
61
- pnpm add @rivinia/table
62
- ```
63
-
64
- or
65
-
66
- ```bash
67
- yarn add @rivinia/table
68
- ```
69
-
70
- or
71
-
72
- ```bash
73
- bun add @rivinia/table
74
- ```
75
-
76
- ## Why another table
77
-
78
- Most Angular tables couple three things that change at different speeds: the
79
- data pipeline, the DOM, and the styling. This one separates them. The pipeline
80
- is signals and pure functions, so it is testable without a browser; the grid is
81
- a component you can replace; and every colour, size and radius is a CSS custom
82
- property read at the use site.
83
-
84
- ## Quick start
8
+ Package version: **1.0.1**
85
9
 
86
10
  ```ts
11
+ import { Component } from '@angular/core';
87
12
  import { createTable, tableColumn } from '@rivinia/table';
88
-
89
- @Component({/* … */})
13
+ import { RvTable } from '@rivinia/table/ui';
14
+ @Component({ imports: [RvTable], template: '<rv-table aria-label="Invoices" [columns]="table.visibleColumns()" [rows]="table.pageRows()" />' })
90
15
  export class InvoicesPage {
91
- private readonly table = createTable<Invoice>(() => ({
92
- // Columns are a FACTORY too: `options()` runs inside a computed, so a
93
- // column list that reads a signal re-renders. A bare array is not accepted.
94
- columns: () => [
95
- tableColumn<Invoice>().accessor('number', { header: 'Invoice', width: '7rem' }),
96
- tableColumn<Invoice>().accessor('client', { header: 'Client', flex: 1 }),
97
- tableColumn<Invoice>().accessor('amount', {
98
- header: 'Amount',
99
- align: 'end',
100
- format: (value) => new Intl.NumberFormat('en-EG').format(value),
101
- }),
102
- tableColumn<Invoice>().accessor('status', { header: 'Status' }),
103
- ],
16
+ readonly table = createTable<Invoice>(() => ({
17
+ columns: () => [tableColumn<Invoice>().accessor('number', { header: 'Invoice' })],
104
18
  data: () => this.invoices(),
105
- pageSize: 25,
106
- selection: { mode: 'multi' },
107
19
  }));
108
-
109
- readonly rows = this.table.pageRows;
110
- readonly columns = this.table.visibleColumns;
111
20
  }
112
21
  ```
113
22
 
114
- `createTable()` must be called in an injection context: the controller is built
115
- from signals and computeds, so it is either a field initialiser or inside
116
- `inject()`-capable code.
117
-
118
- ## Columns
119
-
120
- A column is data, not a component. `tableColumn<T>()` has two builders:
121
- `accessor(key, def?)` reads a field of the row, `display(id, def?)` has no field
122
- and exists for a template or an actions column.
123
-
124
- | Field | Meaning |
125
- | -------------- | ------------------------------------------------------------------- |
126
- | `header` | Visible header text. Omit for an icon-only column. |
127
- | `width` | A CSS length. Fixed columns take part in `table-layout: fixed`. |
128
- | `flex` | `> 0` absorbs free space; `0` stays fixed. |
129
- | `align` | `'start' \| 'center' \| 'end'` — logical, so RTL needs no override. |
130
- | `sortable` | Adds the three-state sort affordance and `aria-sort`. |
131
- | `format` | `(value, row, locale) => string` for display only. |
132
- | `comparator` | Sort with something other than the default compare. |
133
- | `pinned` | `'start' \| 'end'` — stays visible during horizontal scroll. |
134
- | `textOverflow` | `'nowrap'` (default), `'wrap'`, `'clamp'`. |
135
- | `lines` | Clamp height, default 2. |
136
- | `hidden` | Removed from the visible set without losing its definition. |
137
-
138
- ## Row and column spans
139
-
140
- Spans are part of the core, not an add-on: pinning, keyboard navigation and
141
- selection all need to know the grid's real geometry. Three ways to declare one,
142
- in increasing power:
143
-
144
- ```ts
145
- // 1. Row span by a shared key: consecutive rows with an equal value merge.
146
- tableColumn<Invoice>().accessor('department', { header: 'Department', merge: 'equal' });
147
-
148
- // 2. Column span: this cell covers N columns to its end.
149
- tableColumn<Invoice>().accessor('note', { header: 'Notes', colSpan: 2 });
150
-
151
- // 3. Both axes, computed per cell.
152
- tableColumn<Invoice>().display('summary', {
153
- span: ({ rowIndex }) => (rowIndex === 0 ? { colSpan: 5 } : null),
154
- });
155
- ```
156
-
157
- Rules, enforced at `createTable()` rather than at render time:
158
-
159
- - `span` cannot be combined with `merge` or `colSpan` — the call throws.
160
- - `merge` without an accessor warns and does not merge (there is nothing to
161
- compare).
162
- - `merge` and `colSpan` compose into one rectangle.
163
- - Overlapping spans are shrunk so the anchor wins, and the overlap is reported.
164
-
165
- `table.spans` is a resolved `SpanModel` with `at(row, column)`,
166
- `owner(row, column)` and `move(row, column, dRow, dColumn)`. Every query is
167
- O(1) against a dense matrix built once per `(rows, columns)` pair, so a
168
- 10,000-row page does not re-walk your callbacks on scroll.
169
-
170
- ## Selection
171
-
172
- Ids, not array positions — so a selection survives sorting, filtering and
173
- paging, and an action always receives the row it was rendered for. Turn it on
174
- in the options (`'single'` or `'multi'`); a table without it renders none of the
175
- affordance and every method is a no-op:
176
-
177
- ```ts
178
- createTable<Invoice>(() => ({
179
- columns: () => this.columns,
180
- data: () => this.invoices(),
181
- selection: { mode: 'multi' },
182
- }));
183
- ```
184
-
185
- ```ts
186
- table.selection.toggle(row.id);
187
- table.selection.isSelected(row.id);
188
- table.selection.selectAllOnPage(); // page scope, never "everything by accident"
189
- table.selection.clear();
190
- ```
191
-
192
- ### Row identity contract
23
+ ## Why @rivinia/table
193
24
 
194
- `getRowId` must return a unique, stable, non-empty string or finite number for
195
- every row in the full active data set. Validation runs before filtering,
196
- sorting, paging, or selection state is committed. In development, duplicate IDs
197
- throw an error naming the ID and source row indices; invalid values name their
198
- source index. In production, rows with ambiguous IDs remain visible, but
199
- selection and other identity-dependent actions are disabled for those rows.
200
- The table never substitutes an array index for an invalid ID. If rows have no
201
- stable `id`, provide a `getRowId` backed by a persistent domain key.
25
+ - The signal pipeline is headless and testable without rendering a grid.
26
+ - Typed columns, spans, paging, filters, sorting, and selection share one model.
27
+ - The optional grid renders the model and can be replaced with custom markup.
202
28
 
203
- ## Paging and state
204
-
205
- `page`, `pageSize`, `sorting`, `filters` and `globalSearch` are writable
206
- signals; `rows` (filtered and sorted), `pageRows` (sliced) and `total` are
207
- read-only computeds. `table.state` is the serialisable slice — put it in the URL
208
- and the user can share what they see; `setState()` and `resetState()` restore it.
209
-
210
- ## UI entry point
211
-
212
- ```ts
213
- import { RvTable } from '@rivinia/table/ui';
214
- ```
29
+ ## Install
215
30
 
216
- ```html
217
- <rv-table
218
- aria-label="Invoices"
219
- [columns]="columns()"
220
- [rows]="rows()"
221
- [spans]="table.spans()"
222
- [totalRows]="table.total()"
223
- [(sort)]="sort"
224
- [(selected)]="selected"
225
- [skeletonRows]="'auto'"
226
- [loading]="loading()"
227
- >
228
- <!-- The actions column is YOUR template. Five buttons, one button, or a
229
- link — the table only reserves the column and hands you the row. -->
230
- <ng-template rvTableCell="actions" let-row>
231
- <button type="button" (click)="approve(row)">Approve</button>
232
- </ng-template>
233
- </rv-table>
31
+ ```bash
32
+ npm install @rivinia/table
234
33
  ```
235
34
 
236
- The grid renders a single `role="grid"` container, a sticky header that shares
237
- its column widths with the body, one roving `tabindex`, and cells that respect
238
- `span.move()` for keyboard navigation. It ships no icons and no action
239
- semantics.
240
-
241
- Two details that are easy to miss:
242
-
243
- - `aria-label` written on `<rv-table>` becomes the grid's accessible name; the
244
- `[ariaLabel]` input is the reactive equivalent and wins over it.
245
- - `[totalRows]` is what `aria-rowcount` reports when the page is smaller than
246
- the data set. Without it the grid can only count what it renders, which on a
247
- paged table is the page, not the table.
248
- - A cell can be narrower than its content. The grid does not clip a cell by
249
- default, because a floating menu inside a cell has to be able to leave it —
250
- so an actions cell that does not fit paints over its neighbour. Size the
251
- column (`width`) or let the content wrap; if your cells never float
252
- anything, `overflow: clip` on `.rv-table-cell` is the strict fix.
253
-
254
- ## Theming
255
-
256
- Every visual value is a token read where it is used, so a value set on an
257
- ancestor wins:
35
+ ## Quick start
258
36
 
259
- ```
260
- --rv-table-<property> wins for the table only
261
- --rv-<semantic> from @rivinia/theme
262
- <built-in default> last
263
- ```
37
+ 1. Install the package and import `createTable` and `tableColumn`.
38
+ 2. Create the controller in an Angular injection context.
39
+ 3. Render `<rv-table>` with its columns and rows.
264
40
 
265
- Sizes are in `rem`, so the grid follows the browser's text size; hairlines stay
266
- `1px` and focus rings `2px`.
41
+ ## Two ways to use it
267
42
 
268
- ## Accessibility
43
+ | Ready-made UI | Headless controller |
44
+ | --- | --- |
45
+ | `@rivinia/table/ui` | `@rivinia/table` |
269
46
 
270
- - The grid is `role="grid"` with `aria-rowcount` / `aria-colcount`, `aria-sort`
271
- on headers and `aria-selected` on rows.
272
- - With `selectable`, each row draws a **decorative** box in its first cell and
273
- the state lives in `aria-selected` — one announcement per row, not two, and
274
- no second tab stop inside the cell. Clicking the row or pressing Space
275
- toggles it.
276
- - Merged cells are one cell: a screen reader hears the value once, not once per
277
- covered position.
278
- - Truncated text is available to assistive technology through
279
- `aria-describedby`; a visual tooltip is the consumer's choice.
280
- - Loading, empty and error states are live regions (`role="status"`), never
281
- silent.
47
+ The UI and controller are independent; use either or combine them.
282
48
 
283
- ## Status
49
+ ## Compatibility
284
50
 
285
- `0.1.0` is not published yet. Client-mode sorting, filtering, paging, selection
286
- and spans are the 0.1.0 scope; server mode (`TableRequest`, `reload()`) and
287
- editing (`@rivinia/table/editing`) are planned, additive, and not part of this
288
- version.
51
+ | Angular | Node | Browser verification |
52
+ | --- | --- | --- |
53
+ | `^22.0.0` | `^22.22.3 or ^24.15.0 or >=26.0.0` | Chromium and Firefox; native Safari not verified |
289
54
 
290
- ## Further reading
55
+ ## Go further
291
56
 
292
- [Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.0/docs/specifications/table-design.md).
57
+ | Topic | Read |
58
+ | --- | --- |
59
+ | Overview | [Overview](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/overview.md) |
60
+ | Install | [Install](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/installation.md) |
61
+ | Quick start | [Quick start](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/quick-start.md) |
62
+ | Columns | [Columns](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/columns.md) |
63
+ | Row and column spans | [Row and column spans](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/spans.md) |
64
+ | Selection | [Selection](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/selection.md) |
65
+ | Paging and state | [Paging and state](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/paging-and-state.md) |
66
+ | Ready-made UI | [Ready-made UI](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/ready-made-ui.md) |
67
+ | Theming | [Theming](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/theming.md) |
68
+ | Accessibility | [Accessibility](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/accessibility.md) |
69
+ | Scope and limits | [Scope and limits](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.1/docs/table/scope-and-limits.md) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/table",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Headless Angular table: typed columns, sorting, filtering, pagination, selection and real row/column spans.",
5
5
  "keywords": [
6
6
  "angular",