Skip to content

Commit b7b5d35

Browse files
committed
feat: clarify content ownership and add classic presentation
1 parent 9f271f6 commit b7b5d35

49 files changed

Lines changed: 2024 additions & 365 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ JavaScript data is a graph. Dates, collections, undefined values, symbols and ob
2222
- Controlled expansion and selection; cancellable search through collapsed data.
2323
- Hierarchical ranges and bounded rendering for large values.
2424
- Tree keyboard navigation, separate focus/selection, accessible actions.
25+
- Inspector and classic presentations on the same graph engine.
2526
- CSS variables, targeted slots, custom types and actions, unstyled mode.
2627
- React 18.3/19, SSR, TypeScript, ESM/CJS; no runtime dependencies beyond React peers.
2728

@@ -44,9 +45,11 @@ JavaScript data is a graph. Dates, collections, undefined values, symbols and ob
4445
}
4546
```
4647

48+
Prefer quoted keys and object-viewer syntax? Set `presentation="classic"`; see [presentation modes](https://react-data-inspector.nipesolutions.com/guides/presentation).
49+
4750
See the [compiled customization examples](examples/customization.tsx), [API reference](docs/api.md), [architecture and type matrix](docs/architecture.md), and [performance methodology](benchmarks/README.md).
4851

49-
The first alpha is read-only. Editing will emit immutable change proposals; it will never mutate application data. Getters are not evaluated, promises are not awaited, functions are not executed, and WeakMap/WeakSet contents remain opaque. Collection paths are not presented as JSON Pointers.
52+
The alpha is read-only; editing is not part of the public API. Getters are not evaluated, promises are not awaited, functions are not executed, and WeakMap/WeakSet contents remain opaque. Collection paths are not presented as JSON Pointers.
5053

5154
## Development
5255

‎benchmarks/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ Each synchronous case has one warmup and ten measured runs; raw samples, median
77
`npm run benchmark:memory` runs a forced-GC model-lifetime experiment. It reports observed collection, not a deterministic guarantee or a browser retained-heap audit. `npm run benchmark:browser` records Chromium UI timing samples against the local site, including expansion, collapse, selection reveal and scrolling. Browser fixture state and timings are in its JSON output.
88

99
Do not compare these numbers to a competitor without matching fixtures, expansion state, browser, production mode and work limits. No comparative speed claim is published. Wide-object property enumeration is synchronous and should be measured separately. Search and default rendering intentionally perform different amounts of work.
10+
11+
## Presentation comparison
12+
13+
Run `npx tsx benchmarks/presentation.ts > /tmp/rdi-presentation-results.json` to compare inspector and classic server rendering over identical datasets: a 100k-property object, a 200-level object (subject to the inspection depth limit), 1k records, and a grouped 500k array with ranges closed and open. The harness alternates order, warms up both modes, records ten samples, and asserts identical data-row counts. Output includes Node/React versions and hardware details.
14+
15+
This measures SSR traversal and markup generation, not browser frame rate. Classic emits additional decorative markup; equal row counts do not imply equal rendering cost. Browser tests separately cover uniform row heights, bounded mounted nodes and navigation in both presentations.

‎benchmarks/browser.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ try {
3232
const samples = []
3333
for (let i = 0; i < 5; i++) {
3434
await page.goto('http://127.0.0.1:5174')
35-
await page.locator('.json-disclosure > summary').click()
3635
const tree = page.getByRole('tree', { name: 'Playground inspector' })
3736
const start = performance.now()
3837
await tree.getByRole('button', { name: 'Expand user' }).click()
@@ -52,13 +51,14 @@ try {
5251
.click()
5352
await tree.locator('[data-selected=true]').waitFor()
5453
const searchRevealMs = performance.now() - searchStart
54+
await page.goto('http://127.0.0.1:5174/playground?section=json')
5555
await page
5656
.getByLabel('JSON input', { exact: true })
5757
.fill(JSON.stringify(Array.from({ length: 1000 }, (_, i) => i)))
5858
await page
5959
.getByRole('button', { name: 'Inspect JSON', exact: true })
6060
.click()
61-
const virtual = page.getByRole('tree', { name: 'JSON input inspector' })
61+
const virtual = page.getByRole('tree', { name: 'JSON inspector' })
6262
await virtual.focus()
6363
const scrollStart = performance.now()
6464
await virtual.press('End')

‎benchmarks/presentation.ts‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { performance } from 'node:perf_hooks'
2+
import { arch, cpus, platform } from 'node:os'
3+
import { createElement, version as reactVersion } from 'react'
4+
import { renderToString } from 'react-dom/server'
5+
import { DataInspector, type DataInspectorProps } from '../src'
6+
7+
// Run: npx tsx benchmarks/presentation.ts > presentation-results.json
8+
// Keep local comparison output outside the repository unless publishing evidence.
9+
const samples = 10
10+
let deep: unknown = 'leaf'
11+
for (let index = 0; index < 200; index++) deep = { child: deep }
12+
const array500k = Array.from({ length: 500000 }, (_, index) => index)
13+
const cases: Record<string, Omit<DataInspectorProps, 'presentation'>> = {
14+
wide100k: {
15+
value: Object.fromEntries(
16+
Array.from({ length: 100000 }, (_, i) => [`field${i}`, i]),
17+
),
18+
defaultExpandedDepth: 2,
19+
},
20+
deep200: { value: deep, defaultExpandedDepth: 101 },
21+
array1k: {
22+
value: Array.from({ length: 1000 }, (_, id) => ({ id })),
23+
defaultExpandedDepth: 2,
24+
},
25+
grouped500k: { value: array500k, defaultExpandedDepth: 2 },
26+
grouped500kOpenRange: {
27+
value: array500k,
28+
defaultExpandedDepth: 2,
29+
defaultExpandedPaths: [
30+
[{ kind: 'range', start: 0, end: 10000 }],
31+
[
32+
{ kind: 'range', start: 0, end: 10000 },
33+
{ kind: 'range', start: 0, end: 100 },
34+
],
35+
],
36+
},
37+
}
38+
const results = Object.entries(cases).map(([name, props]) => {
39+
const timings = { inspector: [] as number[], classic: [] as number[] }
40+
const rowCounts = { inspector: 0, classic: 0 }
41+
for (let iteration = -1; iteration < samples; iteration++) {
42+
const order =
43+
iteration % 2 === 0
44+
? (['classic', 'inspector'] as const)
45+
: (['inspector', 'classic'] as const)
46+
for (const presentation of order) {
47+
const start = performance.now()
48+
const html = renderToString(
49+
createElement(DataInspector, { ...props, presentation }),
50+
)
51+
const elapsed = performance.now() - start
52+
rowCounts[presentation] = (html.match(/data-rdi-node=/g) ?? []).length
53+
if (iteration >= 0) timings[presentation].push(elapsed)
54+
}
55+
}
56+
if (rowCounts.inspector !== rowCounts.classic)
57+
throw Error(`Presentation changed data row count for ${name}`)
58+
const summarize = (values: number[]) => {
59+
const sorted = [...values].sort((a, b) => a - b)
60+
return {
61+
samplesMs: values,
62+
medianMs: (sorted[4]! + sorted[5]!) / 2,
63+
p95Ms: sorted[9],
64+
}
65+
}
66+
return {
67+
name,
68+
dataRows: rowCounts.inspector,
69+
inspector: summarize(timings.inspector),
70+
classic: summarize(timings.classic),
71+
}
72+
})
73+
process.stdout.write(
74+
JSON.stringify(
75+
{
76+
date: new Date().toISOString(),
77+
environment: {
78+
node: process.version,
79+
react: reactVersion,
80+
platform: platform(),
81+
arch: arch(),
82+
cpu: cpus()[0]?.model,
83+
},
84+
methodology:
85+
'Paired in-process React SSR; one warmup per mode and fixture, ten measured samples, alternating mode order. Dataset construction excluded. Each render creates the same model and visible rows. No client windowing, scrolling, or browser timings. Deep fixture reaches the default maximum depth (100); wide and 500k cases use default grouping. Open-range fixture explicitly expands the first 10k range and first 100-value subrange. Customization, themes, and actions use defaults. Samples are descriptive, not timing assertions.',
86+
results,
87+
},
88+
null,
89+
2,
90+
) + '\n',
91+
)

‎docs/api.md‎

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The root export provides `DataInspector`, `defineInspectorType`, `formatPath`, `
2525
| actions | readonly InspectorAction[] / [] | Extend the action list. |
2626
| messages | Partial&lt;InspectorMessages&gt; | Replace action labels, instructions, summary/nodeLabel functions and parameterized result/reference phrases. |
2727
| className, style | React root attributes | Styling hooks. |
28+
| presentation | inspector, classic / inspector | Visual notation only; shared inspection model and meaningful rows. |
2829
| theme | light, dark, system / system | CSS-only color preference. |
2930
| density | compact, comfortable / compact | Default 28px or 36px rows; 40px on coarse pointers. |
3031
| unstyled | boolean / false | Omit all default appearance selectors; keep behavior and attributes. |
@@ -41,7 +42,19 @@ The application must rerender when input changes. A parent rerender re-inspects
4142

4243
`inspectionOptions` supports `includeNonEnumerable` (false), `includeSymbols` (true for enumerable own symbols), `sortKeys` (false, true, or string comparator), `maxDepth` (100, maximum 200), `stringLimit` (200 characters, maximum 100,000), and `onInspectionError(error, path)`. Sorting never reorders Map/Set entries. Error detail adapters include non-enumerable own error properties.
4344

44-
`searchOptions` supports `scope: 'keys' | 'values' | 'keys-and-values'` (last by default), `debounce` (150 ms), `maxNodes` (100,000, maximum 1,000,000), `maxResults` (1,000, maximum 10,000), and `stringLimit` (65,536). Work yields between bounded tasks. A search can be limited by depth, collections, nodes, result count or string truncation. Search enumerates hidden branches; normal collapsed rendering does not. Counts indicate incomplete results. Search can revisit a shared subtree at a shallower path to inspect descendants previously hidden by the depth limit. Query changes cancel previous scans. Data updates coalesce without restarting the query debounce; completed results remain usable while the localizable `refreshing` status reports “Updating results…”. Matches can lag live data, and reveal checks the current value. Mutating the same object does not provide an atomic search snapshot.
45+
`searchOptions` supports the following settings:
46+
47+
| Option | Default | Maximum / choices |
48+
| ------------- | ----------------- | ----------------------------------- |
49+
| `scope` | `keys-and-values` | `keys`, `values`, `keys-and-values` |
50+
| `debounce` | 150 ms | Query scheduling delay |
51+
| `maxNodes` | 100,000 | 1,000,000 |
52+
| `maxResults` | 1,000 | 10,000 |
53+
| `stringLimit` | 65,536 characters | 100,000 characters |
54+
55+
Search yields between bounded tasks and visits hidden branches. Depth, collection, node, result-count, and string limits can make counts incomplete. Query changes cancel earlier scans; data updates coalesce into a follow-up scan while completed results remain available.
56+
57+
See [search behavior](https://react-data-inspector.nipesolutions.com/docs/search) for shared-path revisits, refresh state, controlled queries, and reveal against changing data. A scan is not an atomic snapshot, and synchronous application callbacks cannot be preempted.
4558

4659
## Paths
4760

@@ -82,24 +95,10 @@ Use `defineInspectorType<T>` to preserve inference while combining heterogeneous
8295

8396
## Styling contract
8497

85-
Styles are scoped with zero-specificity selectors rooted in `[data-rdi-root]:not([data-rdi-unstyled])`. No global reset or CSS-in-JS runtime. Consumer CSS variables override defaults without specificity escalation.
86-
87-
| Variables | Meaning |
88-
| ------------------------------------------------------------------- | ------------------------------------------ |
89-
| --rdi-font-family, --rdi-font-size, --rdi-line-height | Typography |
90-
| --rdi-background, --rdi-foreground, --rdi-muted | Surface and text |
91-
| --rdi-key-color, --rdi-string-color, --rdi-number-color | Key, string, number/bigint syntax |
92-
| --rdi-boolean-color, --rdi-null-color, --rdi-special-color | Boolean, nullish/accessor, special syntax |
93-
| --rdi-row-height, --rdi-indent | Uniform row height and logical indentation |
94-
| --rdi-radius, --rdi-border-color | Frame treatment |
95-
| --rdi-hover-background, --rdi-selected-background, --rdi-focus-ring | Interaction state |
96-
97-
Stable presence selectors: `data-rdi-root`, `data-rdi-node`, `data-rdi-tree`, `data-rdi-key`, `data-rdi-value`, `data-rdi-toggle`, `data-rdi-actions`, `data-rdi-reference`, `data-rdi-search`, `data-rdi-footer`, `data-rdi-unstyled`, `data-rdi-match` (only matches).
98-
99-
Stable state attributes: `data-type`, `data-depth` (zero-based display depth), `data-expanded`, `data-selected`, `data-focused` (string true/false), `data-theme`, `data-density`. DOM nesting and private CSS class names are not a styling contract. Logical padding supports RTL. Search matches are underlined, focus is outlined, selection has its own fill.
98+
The canonical [styling reference](https://react-data-inspector.nipesolutions.com/reference/styling) lists CSS variables, stable presence/state attributes, theme and density defaults, and unstyled responsibilities. Styles remain scoped with zero-specificity selectors; no global reset or CSS-in-JS runtime is required. DOM nesting and implementation class names are private.
10099

101-
In unstyled mode, provide your own tree overflow, single-line row geometry, indentation, focus and selection appearance. The current virtualizer requires uniform row heights; use `virtualization={false}` for wrapping or variable-height custom rows. Even without virtualization, grouping and the row limit remain active.
100+
## Presentation
102101

103-
## Editing design
102+
`presentation="inspector"` is the default. `presentation="classic"` changes built-in visual notation using quoted property keys and balanced inline container delimiters. It adds no closing-delimiter rows or selectable synthetic punctuation. Both presentations share paths, graph identity, search, selection, expansion, actions, grouping, virtualization, and SSR behavior.
104103

105-
No editing prop is exported in this alpha. The next editing stage proposes a readonly replace/add/remove union with `path`, `value`, and `previousValue` where applicable. The application decides whether to apply a proposal. Initial string/number/boolean/null input will use deterministic parsing and normal form controls; any later JSON text mode uses JSON.parse. JSON Patch conversion will be restricted to compatible paths and values, not used as the internal model.
104+
Classic output is not serialized JSON or executable JavaScript. Non-JSON type summaries and separately addressed Map key/value rows retain their meaning. See [presentation modes](https://react-data-inspector.nipesolutions.com/guides/presentation) for examples and customization boundaries.

0 commit comments

Comments
 (0)