@_linked/react 0.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 (58) hide show
  1. package/.context/jest-repro-bundler.config.js +20 -0
  2. package/.context/jest-repro.config.js +20 -0
  3. package/.context/notes.md +0 -0
  4. package/.context/todos.md +0 -0
  5. package/.context/tsconfig-repro-bundler.json +14 -0
  6. package/.context/tsconfig-repro-no-paths.json +12 -0
  7. package/.context/tsconfig-repro-node-modules-paths.json +16 -0
  8. package/.context/tsconfig-repro-node16.json +14 -0
  9. package/AGENTS.md +59 -0
  10. package/LICENSE +21 -0
  11. package/README.md +250 -0
  12. package/docs/001-react-extraction.md +361 -0
  13. package/jest.config.js +20 -0
  14. package/lib/cjs/index.d.ts +4 -0
  15. package/lib/cjs/index.js +21 -0
  16. package/lib/cjs/index.js.map +1 -0
  17. package/lib/cjs/package.d.ts +10 -0
  18. package/lib/cjs/package.js +33 -0
  19. package/lib/cjs/package.js.map +1 -0
  20. package/lib/cjs/package.json +3 -0
  21. package/lib/cjs/utils/Hooks.d.ts +5 -0
  22. package/lib/cjs/utils/Hooks.js +54 -0
  23. package/lib/cjs/utils/Hooks.js.map +1 -0
  24. package/lib/cjs/utils/LinkedComponent.d.ts +52 -0
  25. package/lib/cjs/utils/LinkedComponent.js +322 -0
  26. package/lib/cjs/utils/LinkedComponent.js.map +1 -0
  27. package/lib/cjs/utils/LinkedComponentClass.d.ts +11 -0
  28. package/lib/cjs/utils/LinkedComponentClass.js +34 -0
  29. package/lib/cjs/utils/LinkedComponentClass.js.map +1 -0
  30. package/lib/esm/index.d.ts +4 -0
  31. package/lib/esm/index.js +5 -0
  32. package/lib/esm/index.js.map +1 -0
  33. package/lib/esm/package.d.ts +10 -0
  34. package/lib/esm/package.js +22 -0
  35. package/lib/esm/package.js.map +1 -0
  36. package/lib/esm/package.json +3 -0
  37. package/lib/esm/utils/Hooks.d.ts +5 -0
  38. package/lib/esm/utils/Hooks.js +50 -0
  39. package/lib/esm/utils/Hooks.js.map +1 -0
  40. package/lib/esm/utils/LinkedComponent.d.ts +52 -0
  41. package/lib/esm/utils/LinkedComponent.js +284 -0
  42. package/lib/esm/utils/LinkedComponent.js.map +1 -0
  43. package/lib/esm/utils/LinkedComponentClass.d.ts +11 -0
  44. package/lib/esm/utils/LinkedComponentClass.js +27 -0
  45. package/lib/esm/utils/LinkedComponentClass.js.map +1 -0
  46. package/package.json +57 -0
  47. package/scripts/dual-package.js +25 -0
  48. package/src/index.ts +4 -0
  49. package/src/package.ts +62 -0
  50. package/src/tests/react-component-behavior.test.tsx +578 -0
  51. package/src/tests/react-component-integration.test.tsx +378 -0
  52. package/src/utils/Hooks.ts +56 -0
  53. package/src/utils/LinkedComponent.ts +545 -0
  54. package/src/utils/LinkedComponentClass.tsx +37 -0
  55. package/tsconfig-cjs.json +8 -0
  56. package/tsconfig-esm.json +8 -0
  57. package/tsconfig-test.json +15 -0
  58. package/tsconfig.json +29 -0
@@ -0,0 +1,20 @@
1
+ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'jsdom',
5
+ rootDir: '../src/tests',
6
+ testMatch: ['**/*.test.ts', '**/*.test.tsx'],
7
+ transform: {
8
+ '^.+\\.(ts|tsx)$': [
9
+ 'ts-jest',
10
+ {
11
+ tsconfig: '<rootDir>/../../.context/tsconfig-repro-bundler.json',
12
+ },
13
+ ],
14
+ },
15
+ moduleNameMapper: {
16
+ '^(\\.{1,2}/.*)\\.js$': '$1',
17
+ '^@_linked/react/(.*)$': '<rootDir>/../$1',
18
+ '^@_linked/react$': '<rootDir>/../index',
19
+ },
20
+ };
@@ -0,0 +1,20 @@
1
+ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'jsdom',
5
+ rootDir: '../src/tests',
6
+ testMatch: ['**/*.test.ts', '**/*.test.tsx'],
7
+ transform: {
8
+ '^.+\\.(ts|tsx)$': [
9
+ 'ts-jest',
10
+ {
11
+ tsconfig: '<rootDir>/../../.context/tsconfig-repro-node-modules-paths.json',
12
+ },
13
+ ],
14
+ },
15
+ moduleNameMapper: {
16
+ '^(\\.{1,2}/.*)\\.js$': '$1',
17
+ '^@_linked/react/(.*)$': '<rootDir>/../$1',
18
+ '^@_linked/react$': '<rootDir>/../index',
19
+ },
20
+ };
File without changes
File without changes
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "../",
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "paths": {
8
+ "@_linked/react": ["./src/index"],
9
+ "@_linked/react/*": ["./src/*"]
10
+ }
11
+ },
12
+ "include": ["../src/**/*.ts", "../src/**/*.tsx"],
13
+ "exclude": []
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "../",
5
+ "paths": {
6
+ "@_linked/react": ["./src/index"],
7
+ "@_linked/react/*": ["./src/*"]
8
+ }
9
+ },
10
+ "include": ["../src/tests/react-component-integration.test.tsx"],
11
+ "exclude": []
12
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "../",
5
+ "paths": {
6
+ "@_linked/core": ["./node_modules/@_linked/core/lib/esm/index.d.ts"],
7
+ "@_linked/core/*": ["./node_modules/@_linked/core/lib/esm/*"],
8
+ "@_linked/rdf-mem-store": ["./node_modules/@_linked/rdf-mem-store/lib/esm/index.d.ts"],
9
+ "@_linked/rdf-mem-store/*": ["./node_modules/@_linked/rdf-mem-store/lib/esm/*"],
10
+ "@_linked/react": ["./src/index"],
11
+ "@_linked/react/*": ["./src/*"]
12
+ }
13
+ },
14
+ "include": ["../src/tests/react-component-integration.test.tsx"],
15
+ "exclude": []
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "../",
5
+ "module": "Node16",
6
+ "moduleResolution": "Node16",
7
+ "paths": {
8
+ "@_linked/react": ["./src/index"],
9
+ "@_linked/react/*": ["./src/*"]
10
+ }
11
+ },
12
+ "include": ["../src/tests/react-component-integration.test.tsx"],
13
+ "exclude": []
14
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,59 @@
1
+ # AGENTS.md — @_linked/react repository
2
+
3
+ ## Repository structure
4
+
5
+ Single-package repository for `@_linked/react` (React bindings such as `linkedComponent` and `linkedSetComponent`). Runtime dependency target is `@_linked/core`; tests may also use `@_linked/rdf-mem-store`.
6
+
7
+ Tests: `npm test`
8
+
9
+ ## Agent docs (`docs/`)
10
+
11
+ Files are numbered with a 3-digit prefix for ordering. Names should be explicit about contents (lowercase-dash format). Every file starts with YAML frontmatter:
12
+
13
+ ```yaml
14
+ ---
15
+ summary: One-line description of what this document covers
16
+ packages: [core, react]
17
+ ---
18
+ ```
19
+
20
+ ```bash
21
+ ls docs/ # list all docs
22
+ head -4 docs/*.md # get summaries (or replace * with a specific file)
23
+ ```
24
+
25
+ Each package also has a `README.md` with API docs and a `## Changelog` at the bottom.
26
+
27
+ ## Planning and implementation workflow
28
+
29
+ ### When to plan
30
+
31
+ Any task that requires significant code changes requires a plan. Simple checks, info gathering, and discussions do not. If it's not clear (small code changes), ask the user if he wants to plan first.
32
+
33
+ ### Creating a plan
34
+
35
+ 1. **Inspect the relevant code thoroughly** before writing anything. Read the source files, tests, and existing docs that relate to the task.
36
+ 2. Create a new doc in `docs/` with the next 3-digit prefix (e.g. `005-add-filter-support.md`). Start with YAML frontmatter.
37
+ 3. Write the plan with these sections:
38
+ - **Key considerations and choices** — tradeoffs, open questions, alternatives
39
+ - **Potential problems** — what could go wrong, edge cases
40
+ - **Phases** — ordered list of implementation steps. Each phase has a clear scope and describes how it will be validated. Small tasks: 1-2 phases. Larger tasks: more.
41
+ 4. **Ask the user to review the plan before implementing.**
42
+
43
+ ### Implementing phases
44
+
45
+ - **One commit per phase.** Include the plan doc update (marking the phase complete) in the same commit.
46
+ - **Every phase must be validated** — at minimum one relevant passing test.
47
+ - **After each phase, report to the user:**
48
+ - What was done
49
+ - Any deviations from the plan
50
+ - Problems encountered
51
+ - Validation results (pass/fail counts and what was tested)
52
+ - What you plan to do next
53
+
54
+ ### Wrapping up
55
+
56
+ Before committing final changes or preparing a PR:
57
+
58
+ 1. **Consolidate the plan doc** — collapse alternatives into the choices that were made, summarize implementation details and breaking changes, keep a brief problems section if relevant, remove anything redundant for future readers.
59
+ 2. **Update `## Changelog`** in each affected package's `README.md` — user-facing entry covering behavior changes, new APIs, breaking changes, and migration steps.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Semantu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # @_linked/react
2
+
3
+ React bindings for `@_linked/core`.
4
+
5
+ `@_linked/react` takes a Linked query from `@_linked/core`'s [Schema-Parameterized Query DSL](../core/README.md#schema-parameterized-query-dsl) and maps the top-level query result keys to props for a React component.
6
+
7
+ This package provides:
8
+ - `linkedComponent(...)`
9
+ - `linkedSetComponent(...)`
10
+ - `LinkedComponentClass`
11
+ - `useStyles(...)`
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @_linked/react @_linked/core react react-dom
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Setup package exports
22
+
23
+ ```tsx
24
+ import {
25
+ linkedComponent,
26
+ linkedSetComponent,
27
+ linkedShape,
28
+ } from '@_linked/react';
29
+ ```
30
+
31
+ ### `linkedComponent(...)`
32
+
33
+ `linkedComponent(...)` wraps a React component with a Linked query. You pass a query built with `Shape.query(...)` (which prepares query execution), not `Shape.select(...)` (which executes immediately). At render time, when you pass `of={{id: ...}}`, the wrapper applies the prepared query to that subject and injects the query result keys as props into your component.
34
+
35
+ ```tsx
36
+ const PersonCard = linkedComponent(
37
+ Person.query((p) => p.name),
38
+ ({name, source, _refresh}) => (
39
+ <article>
40
+ <h3>{name}</h3>
41
+ <small>{source.id}</small>
42
+ <button onClick={() => _refresh()}>Reload</button>
43
+ </article>
44
+ ),
45
+ );
46
+
47
+ // External API: pass `of` as a node reference (`{id: string}`), Shape, or QResult.
48
+ <PersonCard of={{id: 'https://example.org/p1'}} />;
49
+ ```
50
+
51
+ Props received by the wrapped component:
52
+ - Query result props: all top-level keys from the query result become direct props (for example `name`).
53
+ - `source`: the resolved shape instance for the input `of` subject.
54
+ - `_refresh(updatedProps?)`: rerun the query (`_refresh()`) or patch local query-result props before rerender (`_refresh({...})`).
55
+ - Custom props: any additional props you pass to the linked component are forwarded as normal.
56
+
57
+ #### `_refresh(updatedProps?)` on linked components
58
+
59
+ `_refresh` is injected into wrapped `linkedComponent(...)` render functions.
60
+
61
+ - `_refresh()` reruns the query and rerenders when results return.
62
+ - `_refresh(updatedProps)` merges `updatedProps` into current query result state and rerenders immediately (without fetching first).
63
+ - `updatedProps` is for query result keys only (for example `name`, `active` from your query), not regular additional props passed by parents.
64
+
65
+ Example use case: optimistic UI after a mutation.
66
+
67
+ ```tsx
68
+ const PersonCard = linkedComponent(
69
+ Person.query((p) => [p.name, p.active]),
70
+ ({id, name, active, _refresh, title}) => (
71
+ <div>
72
+ <h4>{title}</h4>
73
+ <span>{name}</span>
74
+ <button
75
+ onClick={async () => {
76
+ // Patch query-result keys immediately (name/active/id/etc.)
77
+ _refresh({active: !active}); // optimistic local query-result update
78
+ await saveActiveFlag(id, !active); // your write call
79
+ _refresh(); // optional: sync with store response
80
+ // Not for parent custom props like `title`; those come from parent rerender.
81
+ }}
82
+ >
83
+ Toggle active
84
+ </button>
85
+ </div>
86
+ ),
87
+ );
88
+ ```
89
+
90
+ ### `linkedSetComponent(...)`
91
+
92
+ Use `linkedSetComponent(...)` when you want to render a list of sources.
93
+
94
+ ### `linkedSetComponent(...)` (direct query format)
95
+
96
+ ```tsx
97
+ const NameList = linkedSetComponent(
98
+ Person.query((p) => p.name),
99
+ ({linkedData}) => (
100
+ <ul>
101
+ {(linkedData || []).map((person) => (
102
+ <li key={person.id}>{person.name}</li>
103
+ ))}
104
+ </ul>
105
+ ),
106
+ );
107
+ ```
108
+
109
+ ### `linkedSetComponent(...)` (named data-prop format)
110
+
111
+ ```tsx
112
+ const personQuery = Person.query((p) => [p.name, p.hobby]);
113
+
114
+ const NameList = linkedSetComponent({persons: personQuery}, ({persons}) => (
115
+ <ul>
116
+ {persons.map((person) => (
117
+ <li key={person.id}>{person.name}</li>
118
+ ))}
119
+ </ul>
120
+ ));
121
+ ```
122
+
123
+ Both formats are supported. For linked-set wrappers, the external API is also `of` (optional). Internally this becomes `sources` for the wrapped component.
124
+
125
+ ## Render lifecycle and loading state
126
+
127
+ When `LinkedStorage` is initialized and data is not already preloaded in `of`:
128
+ - First render: returns a loading element.
129
+ - Query resolves: component rerenders with mapped query result props.
130
+ - Source changes (`of` changes): prior query result is cleared and query runs again.
131
+
132
+ Loading fallback is currently fixed to:
133
+
134
+ ```html
135
+ <div class="ld-loader" role="status" aria-label="Loading"></div>
136
+ ```
137
+
138
+ There is no API prop to replace this element today. You can style it via CSS class `.ld-loader`.
139
+
140
+ ## Linked set pagination API
141
+
142
+ When `linkedSetComponent(...)` has a limit (explicit query limit or default limit), wrapped props include:
143
+ - `query.nextPage()`
144
+ - `query.previousPage()`
145
+ - `query.setPage(pageIndex)`
146
+ - `query.setLimit(limit)`
147
+
148
+ There is no public `setOffset(...)` in the React query controller; use `setPage`, `nextPage`, or `previousPage`.
149
+
150
+ Example:
151
+
152
+ ```tsx
153
+ import React from 'react';
154
+
155
+ const PeopleList = linkedSetComponent(
156
+ Person.query((p) => [p.name]).limit(5),
157
+ ({linkedData = [], query}) => {
158
+ const [page, setPage] = React.useState(0);
159
+
160
+ return (
161
+ <section>
162
+ <ul>
163
+ {linkedData.map((person) => (
164
+ <li key={person.id}>{person.name}</li>
165
+ ))}
166
+ </ul>
167
+
168
+ <div>
169
+ <button
170
+ onClick={() => {
171
+ query?.previousPage();
172
+ setPage((p) => Math.max(0, p - 1));
173
+ }}
174
+ >
175
+ Previous
176
+ </button>
177
+
178
+ <span>Page {page + 1}</span>
179
+
180
+ <button
181
+ onClick={() => {
182
+ query?.nextPage();
183
+ setPage((p) => p + 1);
184
+ }}
185
+ >
186
+ Next
187
+ </button>
188
+
189
+ <label>
190
+ Page size
191
+ <select
192
+ defaultValue="5"
193
+ onChange={(e) => {
194
+ const nextLimit = Number(e.target.value);
195
+ query?.setLimit(nextLimit);
196
+ query?.setPage(0);
197
+ setPage(0);
198
+ }}
199
+ >
200
+ <option value="5">5</option>
201
+ <option value="10">10</option>
202
+ <option value="25">25</option>
203
+ </select>
204
+ </label>
205
+ </div>
206
+ </section>
207
+ );
208
+ },
209
+ );
210
+ ```
211
+
212
+ ## Notes
213
+
214
+ - This package depends on `@_linked/core` query APIs and `preloadFor(...)` / `BoundComponent` behavior from core.
215
+ - `@_linked/react` itself does not provide RDF storage; use a store package and set a default store in `LinkedStorage` (for example `@_linked/rdf-mem-store`).
216
+
217
+ ## Storage setup (example: `@_linked/rdf-mem-store`)
218
+
219
+ For local in-memory setup, register `@_linked/rdf-mem-store` as the default store:
220
+
221
+ ```tsx
222
+ import {LinkedStorage} from '@_linked/core';
223
+ import {InMemoryStore} from '@_linked/rdf-mem-store';
224
+
225
+ LinkedStorage.setDefaultStore(new InMemoryStore());
226
+ ```
227
+
228
+ ## TODO
229
+
230
+ - Add `setOffset` to `linkedSetComponent` query controller.
231
+ - Make loader configurable and/or switch to passing a loading-state prop.
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ npm run build
237
+ npm test
238
+ ```
239
+
240
+ ## Changelog
241
+
242
+ ### 1.0.0 (from LINCD.js)
243
+
244
+ Initial extraction from the LINCD monolith. Moves React-specific linked component wrappers into a standalone package.
245
+
246
+ - `linkedComponent(...)` and `linkedSetComponent(...)` extracted from `lincd`.
247
+ - `LinkedComponentClass` base class for class-based linked components.
248
+ - `useStyles(...)` hook for component styling.
249
+ - Pagination API (`nextPage`, `previousPage`, `setPage`, `setLimit`) on linked set components.
250
+ - `_refresh(updatedProps?)` for optimistic UI updates on linked components.