@everygrid/grid 0.4.7

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.

Potentially problematic release.


This version of @everygrid/grid might be problematic. Click here for more details.

Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +680 -0
  3. package/dist/Everygrid.css +3 -0
  4. package/dist/everygrid-config.json +7 -0
  5. package/dist/everygrid.standalone.js +401 -0
  6. package/dist/i18n/en.json.d.ts +86 -0
  7. package/dist/i18n/ko.json.d.ts +86 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +24227 -0
  10. package/dist/src/components/ColumnSelectorComponent.d.ts +8 -0
  11. package/dist/src/components/DiffPopupComponent.d.ts +14 -0
  12. package/dist/src/components/EmptyGridPlaceholderComponent.d.ts +15 -0
  13. package/dist/src/components/EverygridComponent.d.ts +5 -0
  14. package/dist/src/components/ExcelViewComponent.d.ts +20 -0
  15. package/dist/src/components/GridTableComponent.d.ts +28 -0
  16. package/dist/src/components/GridToolbarComponent.d.ts +49 -0
  17. package/dist/src/components/HiddenColumnSelectorComponent.d.ts +7 -0
  18. package/dist/src/components/InsertedRowsComponent.d.ts +21 -0
  19. package/dist/src/components/MobileColumnSelectorComponent.d.ts +8 -0
  20. package/dist/src/components/NestedTableComponent.d.ts +10 -0
  21. package/dist/src/components/PaginationComponent.d.ts +21 -0
  22. package/dist/src/components/PinnedTableComponent.d.ts +26 -0
  23. package/dist/src/components/PopupComponent.d.ts +29 -0
  24. package/dist/src/components/RowCountComponent.d.ts +18 -0
  25. package/dist/src/components/RowDetailComponent.d.ts +8 -0
  26. package/dist/src/components/TableCellComponent.d.ts +18 -0
  27. package/dist/src/components/TextEditorPopupComponent.d.ts +6 -0
  28. package/dist/src/core/Everygrid.d.ts +568 -0
  29. package/dist/src/core/ExcelView.d.ts +28 -0
  30. package/dist/src/core/GridHandle.d.ts +214 -0
  31. package/dist/src/core/GridHandle.test.d.ts +1 -0
  32. package/dist/src/core/ResizeUtils.d.ts +3 -0
  33. package/dist/src/core/highlightUtils.d.ts +4 -0
  34. package/dist/src/core/normalizeOptions.d.ts +13 -0
  35. package/dist/src/core/normalizeOptions.test.d.ts +1 -0
  36. package/dist/src/core/types.d.ts +361 -0
  37. package/dist/src/core/useVirtualWindow.d.ts +89 -0
  38. package/dist/src/core/useVirtualWindow.test.d.ts +1 -0
  39. package/dist/src/core/utils.d.ts +19 -0
  40. package/dist/src/i18n/I18n.d.ts +16 -0
  41. package/dist/src/icons/ChevronDownIcon.d.ts +2 -0
  42. package/dist/src/icons/ColumnWidthIcon.d.ts +3 -0
  43. package/dist/src/icons/ColumnsIcon.d.ts +3 -0
  44. package/dist/src/icons/CommaIcon.d.ts +1 -0
  45. package/dist/src/icons/ConfigIcon.d.ts +4 -0
  46. package/dist/src/icons/DiffIcon.d.ts +4 -0
  47. package/dist/src/icons/DownloadIcon.d.ts +3 -0
  48. package/dist/src/icons/EditIcon.d.ts +1 -0
  49. package/dist/src/icons/ExcelIcon.d.ts +3 -0
  50. package/dist/src/icons/GlobeIcon.d.ts +2 -0
  51. package/dist/src/icons/HideIcon.d.ts +1 -0
  52. package/dist/src/icons/InfoIcon.d.ts +2 -0
  53. package/dist/src/icons/InsertRowIcon.d.ts +4 -0
  54. package/dist/src/icons/MobileColumnsIcon.d.ts +3 -0
  55. package/dist/src/icons/PinEmptyIcon.d.ts +1 -0
  56. package/dist/src/icons/PinFilledIcon.d.ts +1 -0
  57. package/dist/src/icons/ReloadIcon.d.ts +3 -0
  58. package/dist/src/icons/RowDetailIcon.d.ts +3 -0
  59. package/dist/src/icons/SearchIcon.d.ts +3 -0
  60. package/dist/src/icons/SortDownIcon.d.ts +1 -0
  61. package/dist/src/icons/SortResetIcon.d.ts +3 -0
  62. package/dist/src/icons/SortUpIcon.d.ts +1 -0
  63. package/dist/src/icons/TrashIcon.d.ts +3 -0
  64. package/dist/src/icons/UndoIcon.d.ts +3 -0
  65. package/dist/src/index.d.ts +14 -0
  66. package/dist/src/react/index.d.ts +20 -0
  67. package/dist/src/standalone-entry.d.ts +2 -0
  68. package/dist/src/wasm/ExcelExportClient.d.ts +14 -0
  69. package/dist/src/wasm/ExportWorker.d.ts +1 -0
  70. package/dist/src/wasm/GridEngineWasm.d.ts +70 -0
  71. package/dist/src/wasm/GridEngineWorker.d.ts +102 -0
  72. package/dist/wasm/everygrid_wasm.js +422 -0
  73. package/dist/wasm/everygrid_wasm_bg.wasm +0 -0
  74. package/package.json +61 -0
package/README.md ADDED
@@ -0,0 +1,680 @@
1
+ # Everygrid
2
+
3
+ A config-driven React data grid. Filtering, sorting and paging over millions of rows run in a
4
+ **Rust - WASM engine** inside a Web Worker, so the UI never blocks.
5
+
6
+ ## Installation
7
+
8
+ The package is `@everygrid/grid`. It is served from its own CDN rather than the npm registry: every
9
+ deploy publishes an immutable tarball at `/packages/everygrid-grid-<version>-<hash>.tgz`, which is
10
+ what you install and what your lockfile pins.
11
+
12
+ ```bash
13
+ npm install https://d3886c7yrxubj8.cloudfront.net/packages/everygrid-grid-0.3.3-eba737af4404.tgz
14
+ ```
15
+
16
+ React 18+ is a peer dependency. For a plain `<script>` page with no bundler, see [CDN Usage](#cdn-usage).
17
+
18
+ ## Quick Start
19
+
20
+ ### 1. Install the library
21
+
22
+ ```bash
23
+ npm install https://d3886c7yrxubj8.cloudfront.net/packages/everygrid-grid-<version>-<hash>.tgz
24
+ ```
25
+
26
+ ### 2. Import CSS
27
+
28
+ ```ts
29
+ import '@everygrid/grid/css';
30
+ ```
31
+
32
+ ### 3. Create your grid config file
33
+
34
+ Create a JSON file anywhere in your project's `public/` directory, e.g. `public/everygrid-config-users.json`:
35
+
36
+ ```json
37
+ {
38
+ "targets": [
39
+ {
40
+ "id": "user-grid",
41
+ "title": "Users",
42
+ "pagination": { "pageSize": 10, "position": "bottom" }
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ ### 4. Register the config path in `everygrid.config.json`
49
+
50
+ Create `everygrid.config.json` in the same `public/` directory, so it is served at
51
+ `/everygrid.config.json` — the library fetches it from there at runtime, no server setup needed:
52
+
53
+ ```json
54
+ {
55
+ "configs": [
56
+ "/everygrid-config-users.json"
57
+ ]
58
+ }
59
+ ```
60
+
61
+ > Multiple config files are supported. Each file's `targets` arrays are merged automatically.
62
+ > The path is resolved relative to the page, so a sub-app served at `/admin/` reads
63
+ > `/admin/everygrid.config.json`.
64
+
65
+ ### 5. Mount grids in your app
66
+
67
+ Mount each grid after its container element exists — and unmount it when the screen goes away.
68
+ `mount` reads the root config on demand (cached — one fetch app-wide), so there's no separate
69
+ bootstrap: a matching config target customizes the grid, otherwise it renders with defaults. Grid
70
+ lifetime is yours to control; nothing is allocated for a target this screen doesn't render.
71
+
72
+ ```tsx
73
+ import { Everygrid, createGrid } from '@everygrid/grid';
74
+ import '@everygrid/grid/css';
75
+
76
+ // No bootstrap — createGrid loads `everygrid.config.json` from the app root itself (cached; resolved
77
+ // relative to the document, so a sub-app at /vanilla/ auto-loads /vanilla/everygrid.config.json) and
78
+ // renders with defaults if the id isn't in the config. The fetcher is a `() => Promise<rows>` or a
79
+ // URL string, which is streamed straight into the engine.
80
+ await createGrid('user-grid', () => fetch('/api/users').then(r => r.json()));
81
+ // same thing, long form:
82
+ await Everygrid.mount('user-grid', {fetcher: '/api/users'});
83
+
84
+ // …when the screen unmounts
85
+ Everygrid.unmount('user-grid');
86
+ ```
87
+
88
+ Add a container element with the matching `id` in your HTML:
89
+
90
+ ```html
91
+ <div id="user-grid"></div>
92
+ ```
93
+
94
+ In React, the library ships the lifecycle as a hook (`useGrid`) and a component
95
+ (`EverygridGrid`) — React is a `peerDependency`. Each screen registers its own grid, no central
96
+ wiring:
97
+
98
+ ```tsx
99
+ import { useGrid, EverygridGrid } from '@everygrid/grid'; // or '@everygrid/grid/react'
100
+
101
+ // hook — you render the container:
102
+ function UserGrid() {
103
+ useGrid('user-grid', () => fetch('/api/users').then(r => r.json()));
104
+ return <div id="user-grid" />;
105
+ }
106
+
107
+ // or the component, which renders the container for you:
108
+ <EverygridGrid id="user-grid" fetcher={() => fetch('/api/users').then(r => r.json())} />
109
+ ```
110
+
111
+ The hook creates the grid on mount and tears it down on unmount; it coalesces concurrent creates and
112
+ defers teardown, so React StrictMode's double-invoke is safe. The config target for `user-grid` (if
113
+ any) customizes it; otherwise it renders with defaults. Nothing else on the page needs to know the
114
+ grid exists.
115
+
116
+ The fetcher may close over component state — a size picker, a filter — because a reload runs the
117
+ fetcher as *last rendered*, not the one captured at mount:
118
+
119
+ ```tsx
120
+ const [rows, setRows] = useState(1000);
121
+ useGrid('user-grid', () => buildRows(rows));
122
+ // later: setRows(5000); Everygrid.reload('user-grid', {silent: true, discard: true});
123
+ ```
124
+
125
+ ---
126
+
127
+ ## CDN Usage
128
+
129
+ The standalone build is one self-contained file — React, the WASM engine, the worker and the CSS
130
+ are all inlined — exposed as `window.Everygrid`. No stylesheet, no React script tags, no build step.
131
+
132
+ ```html
133
+ <!-- rolling: always the newest build -->
134
+ <script src="https://d3886c7yrxubj8.cloudfront.net/latest/everygrid.standalone.js"></script>
135
+ <!-- or pinned: an immutable, hash-named build, e.g.
136
+ <script src="https://d3886c7yrxubj8.cloudfront.net/packages/everygrid.standalone-0.3.3-964c98a78fd2.js"></script> -->
137
+
138
+ <div id="user-grid"></div>
139
+
140
+ <script>
141
+ Everygrid.createGrid('user-grid', () => fetch('/api/users').then(r => r.json()));
142
+ </script>
143
+ ```
144
+
145
+ > **Note:** `everygrid.config.json` is resolved relative to the page (a page at `/vanilla/` loads
146
+ > `/vanilla/everygrid.config.json`), and each path in its `configs` must be publicly reachable. A
147
+ > page without one still works — every grid then renders with defaults.
148
+
149
+ ---
150
+
151
+ ## Configuration Reference
152
+
153
+ ### `everygrid.config.json`
154
+
155
+ | Field | Type | Description |
156
+ |-------|------|-------------|
157
+ | `configs` | `string[]` | List of config file URLs to load (browser-relative paths) |
158
+
159
+ ### Grid Config File
160
+
161
+ A grid is configured in one place: its entry in `targets`. Root-level fields are the few that
162
+ apply to every grid in the file.
163
+
164
+ | Field | Type | Description |
165
+ |-------|------|-------------|
166
+ | `targets` | `GridTargetConfig[]` | The grids — one entry per grid, carrying all of its options (below) |
167
+ | `columnI18n` | `ColumnI18n` | Labels shared by every grid, under `common` — see [Column i18n](#column-i18n) |
168
+ | `dataCache` | `RequestCache` | `cache` mode for URL data loads. Defaults to `'no-store'` (always re-fetch). Use `'default'` for large, rarely-changing datasets so a reload revalidates instead of re-downloading. |
169
+
170
+ #### `GridTargetConfig`
171
+
172
+ | Field | Type | Description |
173
+ |-------|------|-------------|
174
+ | `id` | `string` | The DOM element id the grid mounts into |
175
+ | `title` | `string` | Heading shown above the grid |
176
+ | `links` | `string[]` | Fields rendered as links |
177
+ | `editableCols` | `string[]` | Fields the reader may edit |
178
+ | `rowKey` | `string \| string[]` | Field (or fields) that identify a row — what `patch()` and change events report as `key`. Without it, the row's data index |
179
+ | `rowActions` | `{insertRow?, deleteRow?}` | A "+ row" toolbar button that adds an empty row above the data rows (kept apart from the loaded data — its rows and their indices stay put until commit), and a delete button on every row (deleted rows stay struck through until commit) |
180
+ | `toolbar` | `{active?, showConfig?}` | `active: false` hides the toolbar (search box and action buttons); `showConfig: true` adds a "config" button that opens this entry in the popup viewer |
181
+ | `checkbox` | `string \| {mapping, active?}` | Adds a checkbox column; the field named is what `checkedValues()` collects |
182
+ | `pagination` | `{pageSize?, active?, position?, rowCount?}` | Pagination — see [Row count placement](#row-count-placement) |
183
+ | `virtualScroll` | `{active?, rowHeight?, overscan?, blockSize?}` | Virtual scrolling (replaces pagination for the grid) — see [Virtual scrolling](#virtual-scrolling) |
184
+ | `dataLimit` | `{maxRows?, active?}` | Cap on rows loaded — a safety net against out-of-memory tab crashes — see [Data limit](#data-limit-memory-guard) |
185
+ | `colors` | `{font?, bg?}` | Header/body colors: `{font: {header, body}, bg: {header, body}}` |
186
+ | `mobileColumns` | `string[]` | Which columns the narrow (mobile) layout shows — see [Mobile layout](#mobile-layout) |
187
+ | `columnI18n` | `Record<locale, Record<field, label>>` | Column labels for this grid — see [Column i18n](#column-i18n) |
188
+
189
+ > The older per-feature form — `"pagination": [{"id": "user-grid", …}]` at the root — is still
190
+ > read, and a target's own entry wins where both name the same grid.
191
+
192
+ ### Lifecycle API
193
+
194
+ | Method | Description |
195
+ |--------|-------------|
196
+ | `loadConfig(entryConfigUrl?, opts?)` | Fetches the entry config and every file it lists, registering their targets. No DOM work, no engines, no data. Cached per URL (concurrent calls share one request); pass `{reload: true}` to bypass. Returns the registered target ids. |
197
+ | `createGrid(id, fetcher?)` | Ergonomic form of `mount` — also a standalone named export (`import { createGrid }`). Loads the root config on demand, applies a matching target or renders with defaults. |
198
+ | `loadEverygridConfig(urls?)` | Preload one or more entry configs (default `/everygrid.config.json`; pass an array for sub-apps / several entries). Standalone export too. Usually unnecessary — `createGrid` loads on demand. |
199
+ | `mount(targetId, opts?)` | Mounts a grid into the element with the same id. Self-sufficient — loads the root config (`/everygrid.config.json`, cached) on demand, so no `loadConfig()` bootstrap is needed; a matching config target customizes the grid, otherwise it renders with defaults. `opts.fetcher` is a URL or a function returning rows. Requires the element to be in the DOM — returns `null` with a warning otherwise. Idempotent. |
200
+ | `unmount(targetId)` | Tears the grid down completely — React root, WASM engine, worker thread, timers — and makes the target mountable again. Returns whether a grid was there. |
201
+ | `invalidateConfig(entryConfigUrl?)` | Drops cached config so the next `loadConfig` re-fetches. Mounted grids keep the config they were built with. |
202
+ | `refreshAll()` | Re-renders mounted grids that are in the DOM (viewport-lazy). Use after a container changes size or visibility. |
203
+ | `resetAutoInit()` | Unmounts every grid and forgets all loaded config. |
204
+
205
+ A target whose fetcher is a URL is loaded straight into the WASM engine; payloads of 50MB or
206
+ more are streamed in chunks so the main thread never holds the whole dataset.
207
+
208
+ Each mounted grid owns a Web Worker and its own WASM heap, released on `unmount`. Because cost
209
+ tracks grids actually mounted — not targets that exist in config — a site with hundreds of
210
+ screens pays only for what the current screen renders.
211
+
212
+ ### `autoInit(apiFetchers?, entryConfigUrl?)` *(deprecated)*
213
+
214
+ Convenience wrapper: `loadConfig()`, then `mount()` for every target whose element is already in
215
+ the DOM. Targets whose container doesn't exist yet are skipped rather than waited for, so a
216
+ screen that renders its container later must mount it itself. Prefer `loadConfig` + `mount`.
217
+
218
+ | Parameter | Type | Default | Description |
219
+ |-----------|------|---------|-------------|
220
+ | `apiFetchers` | `Record<string, string \| (() => Promise<Row[]>)>` | `{}` | Per target id: a URL to fetch, or a function returning the rows |
221
+ | `entryConfigUrl` | `string` | `'/everygrid.config.json'` | Path to the entry config file |
222
+
223
+ ### Row count placement
224
+
225
+ Every grid states what it is showing — "Showing 1–5 of 10" — in a band **above and below the
226
+ table**, always, whether or not it has pagination. `rowCount` only decides who draws that line:
227
+
228
+ ```json
229
+ {
230
+ "targets": [
231
+ { "id": "user-grid", "pagination": { "pageSize": 10, "position": "bottom", "rowCount": "inline" } }
232
+ ]
233
+ }
234
+ ```
235
+
236
+ | Value | Layout |
237
+ |-------|--------|
238
+ | `'strip'` *(default)* | The band draws the count itself; pagination bars hold only the page controls |
239
+ | `'inline'` | The pagination bar draws it beside its controls, as before the band existed |
240
+
241
+ `'inline'` applies only at an end that actually has a pagination bar — where there is none, the
242
+ band draws the count itself, so both ends always carry it. Virtual grids always use `'strip'`,
243
+ having no pagination bar at all.
244
+
245
+ ---
246
+
247
+ ### Data limit (memory guard)
248
+
249
+ Loading a whole large dataset into the browser — once into JS, again into the WASM worker — can
250
+ exceed a device's per-tab memory limit and crash the tab (common on phones: the page opens, then
251
+ reloads itself into an error screen). `dataLimit` caps how many rows a grid loads and shows a
252
+ banner instead of crashing.
253
+
254
+ ```json
255
+ {
256
+ "targets": [
257
+ { "id": "user-grid", "dataLimit": { "maxRows": "auto" } }
258
+ ]
259
+ }
260
+ ```
261
+
262
+ | Field | Type | Description |
263
+ |-------|------|-------------|
264
+ | `maxRows` | `number \| 'auto'` | Hard cap. A number caps at exactly that; `'auto'` derives a device-appropriate cap from reported RAM (`navigator.deviceMemory`) and whether the device looks mobile — a roomy desktop gets no cap. |
265
+ | `active` | `boolean` | Set `false` to disable without removing the entry |
266
+
267
+ It applies on every load path:
268
+
269
+ - **In-memory / fetched arrays** — the array is sliced to the cap; the banner reads *"Showing the
270
+ first N of M rows"*.
271
+ - **Streamed URLs** — ingestion stops once the cap is reached, so the rest of the file is never
272
+ downloaded or held (this is where a phone actually runs out of memory). The full size isn't known
273
+ because the stream was cut short, so the banner reads *"Showing the first N rows"*.
274
+
275
+ **This is a safety net, not the primary tool for large data.** For datasets that are genuinely too
276
+ big for the client, use server-side pagination (`pagination.serverSide` + `serverFetcher`) so only
277
+ one page is ever in memory. `dataLimit` is there for when a full dataset reaches the client anyway.
278
+
279
+ ---
280
+
281
+ ### Virtual scrolling
282
+
283
+ Renders only the rows in view instead of a page at a time, so one continuous scroll covers the
284
+ whole filtered result. Rows are fetched from the engine in blocks as you scroll; a block that
285
+ hasn't arrived yet shows a placeholder row rather than shifting the scrollbar.
286
+
287
+ ```json
288
+ {
289
+ "targets": [
290
+ {
291
+ "id": "user-grid",
292
+ "virtualScroll": { "rowHeight": 34, "overscan": 8 }
293
+ }
294
+ ]
295
+ }
296
+ ```
297
+
298
+ | Field | Type | Default | Description |
299
+ |-------|------|---------|-------------|
300
+ | `active` | `boolean` | `true` | Set `false` to turn it off without removing the entry |
301
+ | `rowHeight` | `number` | `36` | Row height in px. Every row is forced to exactly this |
302
+ | `overscan` | `number` | `6` | Extra rows rendered above and below the viewport |
303
+ | `blockSize` | `number` | `200` | Rows fetched from the engine per request |
304
+
305
+ **Very large results.** Browsers cap how tall a single element can be (Chrome at 16,777,214px), so
306
+ past **200,000 rows** the scroller covers a segment of the result rather than all of it, and slides
307
+ that segment under you as you approach its edge — the anchor and the scroll offset move together, so
308
+ there is nothing to see and no pager to click. Each segment stays laid out 1:1, which is what keeps
309
+ scrolling smooth: a row is exactly `rowHeight` tall and travelling `rowHeight` advances exactly one
310
+ row. The height cap is the backstop rather than the target — an unusually tall row yields a shorter
311
+ segment instead of one the browser would clip.
312
+
313
+ The trade is the scrollbar: past the threshold its thumb describes the current segment, not the
314
+ whole result, and it returns to the middle each time the segment slides. Dragging the thumb halfway
315
+ does not land halfway through the data. Below the threshold nothing changes. Only the rows in view
316
+ plus a bounded block cache are ever held in JS, so memory does not grow with the result.
317
+
318
+ **Constraints.** Enabling it for a grid changes a few behaviours, all of them consequences of
319
+ never having more than a screenful of rows in the DOM:
320
+
321
+ - **Pagination is ignored** for that grid, and its controls are hidden.
322
+ - **Row heights are fixed.** Cells are clipped to `rowHeight`; variable-height rows are not
323
+ supported in this mode.
324
+ - **The header "select all" checkbox is hidden.** It means "every row rendered right now", which
325
+ under virtual scrolling is whatever happens to be in the viewport — not a selection anyone
326
+ asked for. Per-row checkboxes work as usual.
327
+ - **Server-side pagination is not supported** alongside it; `serverSide` pagination wins and
328
+ virtual scrolling stays off for that grid.
329
+ - **Excel view** previews the head of the current result rather than a page.
330
+
331
+ The container must have a bounded height — with an auto height there is nothing to overflow and
332
+ every row would render:
333
+
334
+ ```css
335
+ #user-grid { height: 560px; display: flex; flex-direction: column; }
336
+ ```
337
+
338
+ Browsers cap element height (~33.5M px in Chrome), which at the default 36px row is around
339
+ 930,000 rows before the scrollbar stops tracking exactly. Lower `rowHeight` to raise the ceiling.
340
+
341
+ ---
342
+
343
+ ### Serving large datasets
344
+
345
+ Compress big JSON at rest — it is the single biggest win for load time (a 1GB test set
346
+ gzips ~11x, turning a multi-minute download into seconds). Keep the URL unchanged and let
347
+ the browser decompress transparently:
348
+
349
+ ```bash
350
+ gzip -6 -c large.json > large.json.gz
351
+ aws s3 cp large.json.gz s3://<bucket>/data/large.json \
352
+ --content-type application/json \
353
+ --content-encoding gzip \
354
+ --metadata "uncompressed-length=$(stat -f%z large.json)"
355
+ ```
356
+
357
+ `Content-Length` then reports the *compressed* size, which is not comparable with the
358
+ decoded bytes the grid reads, so the indexing bar falls back to showing rows ingested. To
359
+ get a true percentage, advertise the decoded size via `X-Uncompressed-Length` (or S3 user
360
+ metadata `uncompressed-length`, surfaced as `x-amz-meta-uncompressed-length`). For
361
+ cross-origin loads that header must also be listed in the bucket's CORS `ExposeHeaders`.
362
+
363
+ ---
364
+
365
+ ## Search & filter syntax
366
+
367
+ The search box runs a small query language against the WASM engine (case-insensitive throughout,
368
+ keys included). Everything composes with `&&` / `||`.
369
+
370
+ | Form | Meaning | Example |
371
+ |------|---------|---------|
372
+ | `text` | Free text — matches any field at any depth | `frontend` |
373
+ | `field(expr)` | Scope into a field; nest for depth | `role(engineering(subRole(front)))` |
374
+ | `a(x).b(y)` | Sibling keys ANDed **on the same object/array element** | `subRole(front).years(>=2)` |
375
+ | `field(>n)` … | Comparisons: `>` `>=` `<` `<=` `==`/`=` `!=` | `age(>=30)`, `dept(!=HR)` |
376
+ | `field(in[a,b])` | Membership | `dept(in[HR,Design])` |
377
+ | `field(~re)` | Regex (add `(?i)` for case-insensitive) | `email(~@example\.com$)` |
378
+ | `field op value` | Un-parenthesized comparison (single field) | `age >= 30`, `active == true` |
379
+
380
+ Notes:
381
+
382
+ - A field scope over an **array** matches when *any* element satisfies it; with the `.` chain the
383
+ conditions must hold on the **same** element (`role(engineering(subRole(front).years(=1)))`).
384
+ - Booleans work with the operators: `active(==true)`, `active(!=false)`.
385
+ - The search box **autocompletes keys** for the current scope (top-level fields first, then the
386
+ selected field's sub-keys), colours parenthesis pairs by depth, and highlights the bracket next to
387
+ the caret. Enter runs the search; Shift+Enter inserts a newline.
388
+
389
+ ## Column i18n
390
+
391
+ `columnI18n` gives columns localized display names without touching the data. It maps
392
+ A grid's own labels go on its target as `locale → field → label`; labels shared by every grid in
393
+ the file go at the root under `common`. A grid's entry overrides `common`, and any field with no
394
+ label falls back to its raw key. Locale is the global `I18n` locale, so `I18n.setLocale(l)`
395
+ followed by `Everygrid.refreshAll()` relabels every grid.
396
+
397
+ ```json
398
+ {
399
+ "columnI18n": {
400
+ "ko": { "common": { "id": "아이디", "name": "이름", "age": "나이" } },
401
+ "en": { "common": { "id": "ID", "name": "Name", "age": "Age" } }
402
+ },
403
+ "targets": [
404
+ { "id": "orders", "columnI18n": { "ko": { "name": "주문자" } } }
405
+ ]
406
+ }
407
+ ```
408
+
409
+ Labels are **display-only**. The search box still uses real field keys (`name() && age()`) — the
410
+ suggestion dropdown just annotates each key with its label, e.g. `name(이름)`, and inserts the key.
411
+ So queries, WASM filtering, and highlighting are locale-independent, and switching language never
412
+ invalidates a typed query.
413
+
414
+ ## Mobile layout
415
+
416
+ Below 720px the grid switches to a touch layout: fixed-width columns that **scroll horizontally**,
417
+ taller rows and larger controls, and a per-row **detail button** that opens the whole row in a
418
+ modal. Pinning, resizing and the checkbox column are off.
419
+
420
+ Which columns show, in priority order:
421
+
422
+ 1. The user's in-session pick — the **Columns** action in the header (the same whitelist as
423
+ desktop's "Select Columns"; an empty pick falls back to the first three columns).
424
+ 2. `mobileColumns` config for the grid.
425
+ 3. Every column (default).
426
+
427
+ ```json
428
+ { "id": "orders", "mobileColumns": ["name", "status", "total"] }
429
+ ```
430
+
431
+ The entries are field keys, shown in the grid's column order; any that don't exist are skipped. Column
432
+ labels follow [Column i18n](#column-i18n) like everywhere else.
433
+
434
+ ## Excel export
435
+
436
+ The toolbar's export button downloads the grid as `.xlsx`:
437
+
438
+ - **Filtered vs All** — when a filter is active the button offers both scopes (with row counts);
439
+ otherwise it downloads directly.
440
+ - **Nested arrays → child sheets** — object arrays are exported as normalized child sheets linked to
441
+ the main sheet by `_mainSheetRowNum` (with `_key` / `_idx` for the sub-path and position), so the
442
+ data stays analysable in Excel/Power Query. On those child sheets, nested objects flatten to
443
+ `parent_child` columns to any depth.
444
+ - **Nested objects → one cell** — on the main sheet a nested object stays a single column, its
445
+ contents rendered into the one cell (the same treatment object arrays get), so every nested value
446
+ reads the same way rather than a `{name, division}` object splitting into `_name` / `_division`
447
+ columns while an object holding arrays stays whole.
448
+ - **Large data** — built off the main thread in a worker, streamed and zip-split so even multi-GB
449
+ grids export without freezing the UI; the progress badge shows a percentage and a **Cancel**.
450
+
451
+ ### Excel preview
452
+
453
+ The toolbar's preview button swaps the grid for a spreadsheet-shaped rendering of the same data —
454
+ the nested-column layout the export produces — without downloading anything. It takes over the
455
+ grid's own box, so a grid with a fixed height keeps it and the preview scrolls inside.
456
+
457
+ **It navigates the way the grid does**, so the whole result is reachable either way:
458
+
459
+ | Grid | Preview |
460
+ |------|---------|
461
+ | Paged | Pages, using that grid's own `pageSize` |
462
+ | Virtual scrolling | Keeps scrolling, appending the next chunk as you near the end |
463
+ | Neither | One page of 50 |
464
+
465
+ The row count under the table reports what is on screen against the full result — `Showing
466
+ 1–50 of 2,000`. Leaving and re-entering the preview returns to the page you were on; changing the
467
+ filter or sort starts again from the top.
468
+
469
+ A grid sized by its content is capped at `60vh` so a preview of wide, deeply nested rows cannot
470
+ push the page around; it scrolls within the cap. Override with `.everygrid-excel-body`.
471
+
472
+ ---
473
+
474
+ ## Editing & change tracking
475
+
476
+ `Everygrid.get(id)` is a cursor into a mounted grid's data — grid → row → cell — with the same verbs
477
+ at every level: `get`, `set`, `original`, `changes`, `cancel`. Handles are stateless
478
+ views, so they never go stale; a row that does not exist reports `exists() === false` and its
479
+ writes are no-ops, so chains need no null checks. Row indices are positions in the loaded data and
480
+ hold still under sort and filter; `visibleRow(n)` is the n-th row on screen.
481
+
482
+ ```ts
483
+ const g = Everygrid.get('user-grid'); // null until the grid is mounted
484
+
485
+ // target a row: by index, by key (see rowKey), by predicate, or by position on screen
486
+ g.row(3); g.rowByKey('U-1002'); g.find(r => r.email === 'a@b.c'); g.visibleRow(0);
487
+
488
+ // cells — set() behaves exactly like typing into the cell: tracked, marked, synced to the engine.
489
+ // It honours editableCols like the UI: a column not listed there is refused (false + a warning)
490
+ // unless you pass {force: true}.
491
+ g.row(3).cell('score').get(); g.cell(3, 'score') // same thing
492
+ g.row(3).cell('score').set(90); g.row(3).cell('score').isEditable(); // any cell of an inserted row is editable
493
+ g.row(3).cell('id').set(7, {force: true});
494
+ g.row(3).cell('score').original(); g.row(3).cell('score').modified();
495
+ g.row(3).cell('score').cancel(); // the loaded value back
496
+
497
+ // rows
498
+ g.row(3).set({score: 90, active: false}); // returns how many cells were written
499
+ g.row(3).changes(); // [{field, from, to}]
500
+ g.row(3).original(); g.row(3).cancel(); g.row(3).key(); // key() is null while an inserted row's key field is empty
501
+
502
+ // columns
503
+ g.column('score').changes(); // [{index, key, from, to, row}]
504
+ g.column('score').values(); g.column('score').cancel();
505
+
506
+ // rows in and out (rowActions.insertRow / deleteRow must allow it)
507
+ g.insertRow({name: 'New'}); // a new row above the data, with these values; returns its handle
508
+ g.insertedRow(0); // inserted rows have their own index space (handle.kind === 'inserted')
509
+ g.row(3).delete(); // struck through until commit; g.row(3).cancel() brings it back
510
+ g.row(3).status(); // 'inserted' | 'updated' | 'deleted' | null
511
+ g.row(3).inserted(); g.row(3).updated(); g.row(3).deleted(); g.row(3).checked(); // predicates
512
+
513
+ // lists — plain arrays of row handles; index, filter, map, forEach them as you like
514
+ g.rows(); // every row; g.rows()[0], g.rows().filter(r => r.updated())
515
+ g.insertedRows(); g.updatedRows(); g.deletedRows(); g.checkedRows();
516
+ g.checkedRows().forEach(r => r.delete());
517
+ g.rows().filter(r => r.checked()).map(r => r.get()); // the checked rows as JSON
518
+
519
+ // the grid
520
+ g.hasChanges();
521
+ g.changes(); // [{status, index, key, row, original, cells: [{field, from, to}]}] (key null for an unkeyed new row)
522
+ g.diff(); // {inserted, updated, deleted, cells}
523
+ g.patch(); // {inserted: [rows], updated: [{key, changes}], deleted: [{key, row}]}
524
+ g.cancel(); // every change cancelled: edits undone, inserted rows dropped, deleted rows back
525
+ g.commit(); // after a successful save: current state becomes the baseline (inserted rows join the data at the end)
526
+
527
+ // the checkbox column (needs a `checkbox` config; rows are identified by its `mapping` field)
528
+ g.checkedValues(); // the checked rows' mapping values
529
+ g.check([1, 2]); g.uncheck([1]); g.checkAll(); g.uncheckAll();
530
+
531
+ // events — each returns its unsubscribe function
532
+ g.on('cellChange', ({index, key, field, from, to, row}) => …);
533
+ g.on('change', changes => …); // after every edit, cancel and commit
534
+ g.on('check', ({values, rows, changed, checked}) => …); // any checkbox change, UI or API
535
+ ```
536
+
537
+ ### Handle reference
538
+
539
+ `Everygrid.get(id)` returns a `GridHandle`; `g.row(i)` a `RowHandle`; `row.cell(field)` a `CellHandle`;
540
+ `g.column(field)` a `ColumnHandle`. Handles are stateless views over the grid — make them freely.
541
+ `RowKey` is `string | number`; `RowChange` is `{status, index, key, row, original, cells}`.
542
+
543
+ **GridHandle** — `Everygrid.get(id)`
544
+
545
+ | Method | Returns | What it does |
546
+ |---|---|---|
547
+ | `exists()` | `boolean` | The grid is still mounted |
548
+ | `data()` | `T[]` | The loaded rows, in data order (live objects — read only; write through `set`) |
549
+ | `row(index)` | `RowHandle` | The row at a data index |
550
+ | `rowByKey(key)` | `RowHandle` | The row whose `rowKey` field equals `key` |
551
+ | `find(pred)` | `RowHandle` | The first row matching `pred(row, index)` |
552
+ | `visibleRow(n)` | `RowHandle` | The n-th row on screen (after filter and sort) |
553
+ | `insertedRow(i)` | `RowHandle` | The i-th inserted row (their own index space) |
554
+ | `cell(index, field)` | `CellHandle` | Shorthand for `row(index).cell(field)` |
555
+ | `column(field)` | `ColumnHandle` | One column across all rows |
556
+ | `rows()` | `RowHandle[]` | Every loaded row; a plain array |
557
+ | `insertedRows()` / `updatedRows()` / `deletedRows()` | `RowHandle[]` | The changed rows of one kind |
558
+ | `checkedRows()` | `RowHandle[]` | The rows whose checkbox is checked (never a deleted row) |
559
+ | `checkedValues()` | `unknown[]` | Their `checkbox.mapping` values |
560
+ | `check(values)` / `uncheck(values)` | — | Check / uncheck rows by mapping value |
561
+ | `checkAll()` / `uncheckAll()` | — | Every row / none |
562
+ | `hasChanges()` | `boolean` | Anything inserted, updated or deleted |
563
+ | `changes()` | `RowChange[]` | Every changed row, in data order |
564
+ | `diff()` | `{inserted, updated, deleted, cells}` | `changes()` grouped by kind, with a cell count |
565
+ | `patch()` | `{inserted, updated, deleted}` | What to save: rows whole, `{key, changes}`, `{key, row}` |
566
+ | `insertRow(values?)` | `RowHandle` | A new row above the data (needs `rowActions.insertRow`) |
567
+ | `cancel()` | — | Every change cancelled: edits undone, inserted rows dropped, deleted rows back |
568
+ | `commit()` | — | Current state becomes the baseline; inserted rows join the data at the end, deleted rows go |
569
+ | `on(event, fn)` | `() => void` | Subscribe to `cellChange` / `change` / `check`; returns the unsubscribe |
570
+
571
+ **RowHandle** — `g.row(i)`, `g.rowByKey(k)`, `g.rows()[i]`, …
572
+
573
+ | Method | Returns | What it does |
574
+ |---|---|---|
575
+ | `index` / `kind` / `gridId` | fields | Position; `'data'` or `'inserted'`; the grid's id |
576
+ | `exists()` | `boolean` | There is a row at this index |
577
+ | `get()` | `T \| undefined` | The row as it is now |
578
+ | `original()` | `T \| undefined` | The row as loaded (the row itself if never edited) |
579
+ | `key()` | `RowKey \| null` | The `rowKey` field's value, else the index; null while an inserted row's key is empty |
580
+ | `cell(field)` | `CellHandle` | One cell |
581
+ | `set(values, {force?})` | `number` | Edit several cells; returns how many were written |
582
+ | `changes()` | `CellChange[]` | `[{field, from, to}]` for the cells that differ from the original |
583
+ | `status()` | `'inserted' \| 'updated' \| 'deleted' \| null` | The row's state |
584
+ | `inserted()` / `updated()` / `deleted()` / `changed()` | `boolean` | Predicates, for `rows().filter(...)` |
585
+ | `checked()` | `boolean` | The checkbox is checked |
586
+ | `check(checked = true)` | — | Check / uncheck this row |
587
+ | `delete()` | `boolean` | Mark deleted (needs `rowActions.deleteRow`); an inserted row is simply dropped |
588
+ | `cancel()` | — | Un-edit / un-insert / un-delete this row |
589
+
590
+ **CellHandle** — `g.row(i).cell(field)`, `g.cell(i, field)`
591
+
592
+ | Method | Returns | What it does |
593
+ |---|---|---|
594
+ | `row` / `field` | fields | The row handle; the column name |
595
+ | `exists()` | `boolean` | The row exists |
596
+ | `get()` | `unknown` | The current value |
597
+ | `original()` | `unknown` | The loaded value |
598
+ | `modified()` | `boolean` | Differs from the loaded value |
599
+ | `isEditable()` | `boolean` | Any cell of an inserted row; else what `editableCols` allows |
600
+ | `set(value, {force?})` | `boolean` | Edit exactly as typing would; refused (false + warning) outside `editableCols` unless `force` |
601
+ | `cancel()` | — | The loaded value back |
602
+
603
+ **ColumnHandle** — `g.column(field)`
604
+
605
+ | Method | Returns | What it does |
606
+ |---|---|---|
607
+ | `field` | field | The column name |
608
+ | `values()` | `unknown[]` | Current values down the column, in data order |
609
+ | `changes()` | `ColumnChange[]` | `[{index, key, from, to, row}]` for the rows whose value changed |
610
+ | `cancel()` | — | Every change in this column undone |
611
+
612
+ `patch()` is plain JSON per kind — how it is saved (fetch, a form, a queue) is up to you:
613
+
614
+ ```ts
615
+ const {inserted, updated, deleted} = g.patch();
616
+ // inserted: [{...row}] the new rows, whole
617
+ // updated: [{key, changes: {...}}] per row, its key and only the changed fields
618
+ // deleted: [{key, row}] per row, its key and the row as loaded
619
+ await save(inserted, updated, deleted);
620
+ g.commit();
621
+ ```
622
+
623
+ `rowKey` names the field that identifies a row (`"rowKey": "id"` on the target; an array
624
+ makes a composite key joined with `|`). Without it `key` is the row's data index. In `changes()`
625
+ and `patch()` the key is read from the row's *original*, so a save can still find the record when
626
+ the key field itself was edited.
627
+
628
+ A grid that can change (editable columns or row actions) gets a **diff** button in its toolbar: a
629
+ popup with a second grid whose rows are the changes since load — status, key, row, field, before,
630
+ after — with the usual search, sort, column choice and export.
631
+
632
+ Change tracking costs only what was edited: a row's original is snapshotted on its first edit, so
633
+ `changes()` and `patch()` walk the edited rows, not the dataset.
634
+
635
+ ## Other APIs
636
+
637
+ ```ts
638
+ // Re-render all grids (e.g. after language change)
639
+ Everygrid.refreshAll();
640
+
641
+ // Re-fetch one grid's data from the source it was created with and rebuild its index.
642
+ // The toolbar shows a "Reload Data" button for exactly those grids; grids given their
643
+ // rows inline have no source to re-fetch, so they get no button and this is a no-op.
644
+ // Reloaded rows become the new baseline: pending cell edits are discarded.
645
+ // Options: {silent} skips the button's spinner; {discard} drops the rows on screen first (the
646
+ // incoming result is a different query, not a refresh of this one). A reload requested while
647
+ // one is running is queued behind it — latest wins — and its promise settles when that run ends.
648
+ // The loading UI is painted before the fetcher runs, so a fetcher that builds rows synchronously
649
+ // does not block it.
650
+ grid.reloadData('my-grid-id', {silent: true, discard: true}); // or Everygrid.reload(id, opts)
651
+
652
+ // Internationalization
653
+ import { I18n } from '@everygrid/grid';
654
+ I18n.initFromBrowser(); // auto-detect browser language
655
+ I18n.setLocale('en'); // 'en' | 'ko'
656
+ ```
657
+
658
+ ---
659
+
660
+ ## Versioning
661
+
662
+ Every deploy publishes immutable, hash-named artifacts (`/packages/everygrid-grid-<version>-<hash>.tgz`
663
+ and `/packages/everygrid.standalone-<version>-<hash>.js`), so the exact bytes are always
664
+ addressable; `/latest/everygrid.standalone.js` is a mutable pointer at the newest build. The version
665
+ is a compatibility promise, not a build id — it moves only when the public API does, and several
666
+ deploys may share one version.
667
+
668
+ The public API is the exports of `@everygrid/grid`, the config schema (`types.ts`), the
669
+ `everygrid.config.json` contract and the `window.Everygrid` global. While at `0.x`, npm's caret makes
670
+ **minor the breaking slot** (`^0.4.2` resolves `>=0.4.2 <0.5.0`):
671
+
672
+ | Bump | When |
673
+ |---|---|
674
+ | minor (`0.5.0`) | Breaking: an export or config key removed or renamed, a changed meaning, a changed signature |
675
+ | patch (`0.4.3`) | Everything else — bug fixes **and** backward-compatible additions |
676
+ | `1.0.0` | The API is declared frozen |
677
+
678
+ ## License
679
+
680
+ MIT