lit-rdf 0.3.2 → 0.3.4

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # lit-rdf
2
2
 
3
+ ## 0.3.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 0bd749b: Include the lit-rdf skill in the published package and clarify when to use it.
8
+
9
+ ## 0.3.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 3b452a8: Remove unused direct dependency `rdf-canonize`
14
+ - 3b452a8: Update `@zazuko/env` to `^3.0.4`.
15
+ - 5a504bc: Made `@zazuko/prefixes` a peer dependency
16
+ - 3b452a8: Add a repository AI skill demonstrating lit-rdf data binding, filtering, sorting, and custom component patterns, with an interactive example and contributor guidance.
17
+
3
18
  ## 0.3.2
4
19
 
5
20
  ### Patch Changes
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "lit-rdf",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "scripts": {
6
+ "postinstall": "skills experimental_sync -y",
6
7
  "prepare": "husky && patch-package",
7
8
  "lint": "eslint . --quiet",
9
+ "lint:skills": "skill-check check skills --no-security-scan --fail-on-warning",
8
10
  "storybook": "storybook dev -p 6006",
9
11
  "build-storybook": "storybook build",
10
12
  "test:storybook": "vitest --project=storybook",
@@ -17,6 +19,7 @@
17
19
  "release": "changeset publish"
18
20
  },
19
21
  "files": [
22
+ "skills/lit-rdf/",
20
23
  "CHANGELOG.md",
21
24
  "custom-elements.json",
22
25
  "src/**/*.js",
@@ -41,29 +44,30 @@
41
44
  "@rdfjs/data-model": "^2.1.1",
42
45
  "@rdfjs/dataset": "^2.0.3",
43
46
  "@rdfjs/types": "^2",
44
- "@zazuko/env": "^3.0.1",
45
- "@zazuko/prefixes": "^2.6.1",
47
+ "@zazuko/env": "^3.0.6",
46
48
  "clownface": "^2.0.3",
47
49
  "clownface-shacl-path": "^2.4.0",
48
50
  "is-url": "^1.2.4",
49
51
  "lit": "^3",
50
- "rdf-canonize": "^3.4.0",
51
52
  "rdf-literal": "^2.0.0",
52
53
  "sparql-path-parser": "^0.1.1"
53
54
  },
55
+ "peerDependencies": {
56
+ "@zazuko/prefixes": ">=2.6.1"
57
+ },
54
58
  "devDependencies": {
55
- "@changesets/cli": "^3.0.0-next.11",
59
+ "@changesets/cli": "^3.0.3",
56
60
  "@chromatic-com/storybook": "^5.3.0",
57
61
  "@custom-elements-manifest/analyzer": "^0.11.0",
58
62
  "@eslint/config-helpers": "^0.7.0",
59
63
  "@eslint/js": "^10",
60
64
  "@lit-labs/ssr": "^4.1.0",
61
65
  "@open-wc/testing": "^5.0.0",
62
- "@rollup/plugin-commonjs": "^26",
63
66
  "@rolldown/plugin-node-polyfills": "^1.0.3",
64
- "@storybook/addon-docs": "^10.6.0",
65
- "@storybook/addon-vitest": "^10.6.0",
66
- "@storybook/web-components-vite": "^10.6.0",
67
+ "@rollup/plugin-commonjs": "^29.0.3",
68
+ "@storybook/addon-docs": "^10.6.6",
69
+ "@storybook/addon-vitest": "^10.6.6",
70
+ "@storybook/web-components-vite": "^10.6.6",
67
71
  "@tpluscode/eslint-config": "^4.0.2",
68
72
  "@types/clownface": "^2.0.10",
69
73
  "@types/is-url": "^1.2.32",
@@ -73,8 +77,8 @@
73
77
  "@types/sinon": "^22.0.0",
74
78
  "@typescript-eslint/eslint-plugin": "^8.66.0",
75
79
  "@typescript-eslint/parser": "^8.66.0",
76
- "@vitest/browser-playwright": "^4.1.11",
77
- "@vitest/coverage-v8": "^4.1.11",
80
+ "@vitest/browser-playwright": "^4.1.12",
81
+ "@vitest/coverage-v8": "^4.1.12",
78
82
  "@vocabulary/rdfs": "^1.0.6",
79
83
  "@web/dev-server-esbuild": "^2.0.0",
80
84
  "@web/dev-server-rollup": "^1.0.0",
@@ -86,13 +90,15 @@
86
90
  "husky": "^9.1.7",
87
91
  "lint-staged": "^17.3.0",
88
92
  "patch-package": "^8.0.1",
89
- "playwright": "^1.58.2",
93
+ "playwright": "^1.65.0",
90
94
  "sinon": "^22.1.0",
91
- "storybook": "^10.6.0",
95
+ "skill-check": "^1.2.0",
96
+ "skills": "^1.7.6",
97
+ "storybook": "^10.6.6",
92
98
  "string-to-stream": "^3.0.1",
93
99
  "typescript": "^5",
94
100
  "util": "^0.12.5",
95
- "vitest": "^4.1.11"
101
+ "vitest": "^4.1.12"
96
102
  },
97
103
  "lint-staged": {
98
104
  "*.{js,ts}": [
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: lit-rdf
3
+ description: Use when building or explaining RDF-driven Lit interfaces with lit-rdf, including dataset binding, focus-node context, graph traversal, filtering, sorting, and custom reactive components. Covers lit-rdf examples and integrations; excludes generic RDF processing and unrelated Lit components.
4
+ ---
5
+
6
+ # Build with lit-rdf
7
+
8
+ Use the library's context providers and Clownface pointers to compose views. Preserve RDF terms and datasets through the binding layer instead of converting the graph into unrelated JSON models.
9
+
10
+ ## Choose the example
11
+
12
+ - For initial setup, data sources, selecting resources, labels, links, and traversal, read [binding.md](references/binding.md).
13
+ - For repeated cards or tables, filters, sorting, custom controllers, and application updates, read [patterns.md](references/patterns.md).
14
+ - For a complete starting point, adapt [people-demo.ts](assets/people-demo.ts). It supplies its own RDF/JS fixture and renders a sorted, filtered list with per-person context. Import it from a browser TypeScript entrypoint with bare-module resolution, then place `<rdf-people-demo></rdf-people-demo>` in the page. It needs `lit`, `lit-rdf`, and `@zazuko/env`; it is not a standalone browser script.
15
+
16
+ ## Binding contract
17
+
18
+ The usual hierarchy is `rdf-environment` → `rdf-dataset` → `target-node` → optional `filter-node` → a view consuming `FocusNode`. Contributing `rdf-graph` elements belong beneath the dataset. A custom view iterates `FocusNode.array` and wraps each item in `<focus-node .focusNode=${item}>` so nested resource elements consume that item.
19
+
20
+ `target-node`, `filter-node`, and `focus-node` provide context; they do not stamp one copy of their children per resource. Use an explicit renderer for collections. Traversal offers rendering callbacks, but those callbacks also need per-item `focus-node` wrappers when rendering nested resource elements.
21
+
22
+ Use attributes for serialized IRIs and paths, and Lit property bindings for objects and functions. Examples: `target-class="schema:Person"`, `.targetClass=${rdf.ns.schema.Person}`, `.value=${dataset}`, `.filter=${callback}`, `.focusNode=${pointer}`. Attribute converters do not run for property bindings.
23
+
24
+ Import `lit-rdf` for element registration; import controllers from `lit-rdf/controllers.js` and the environment mixin from `lit-rdf/mixins.js`. The root entrypoint does not export controller classes. Avoid private `src/` imports in consumer examples.
25
+
26
+ ## Demonstrate the binding
27
+
28
+ Use the public exports supported by the installed version of `lit-rdf`.
29
+
30
+ Examples should include data matching their vocabulary, every required import, and a real collection renderer when multiple nodes are selected. Explain the expected visible output and how the dataset, focus, and nested bindings connect. These are display bindings; implement input-to-RDF writes explicitly when requested.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "lit-rdf"
3
+ short_description: "Build RDF views with Lit context and data binding"
4
+ default_prompt: "Use $lit-rdf to build a data-bound RDF view with filtering and sorting."
@@ -0,0 +1,100 @@
1
+ import { html, LitElement } from 'lit'
2
+ import type { GraphPointer } from 'clownface'
3
+ import type { NamedNode } from '@rdfjs/types'
4
+ import rdf from '@zazuko/env/web.js'
5
+ import { FocusNode } from 'lit-rdf/controllers.js'
6
+ import 'lit-rdf'
7
+
8
+ const alice = rdf.namedNode('https://example.org/alice')
9
+ const bob = rdf.namedNode('https://example.org/bob')
10
+ const carol = rdf.namedNode('https://example.org/carol')
11
+ const people: [NamedNode, string, number][] = [
12
+ [alice, 'Alice', 32], [bob, 'Bob', 16], [carol, 'Carol', 24],
13
+ ]
14
+ const graph = rdf.dataset([
15
+ ...people.flatMap(
16
+ ([person, name, age]) => {
17
+ return [
18
+ rdf.quad(person, rdf.ns.rdf.type, rdf.ns.schema.Person),
19
+ rdf.quad(person, rdf.ns.schema.name, rdf.literal(String(name))),
20
+ rdf.quad(person, rdf.ns.foaf.age,
21
+ rdf.literal(String(age), rdf.ns.xsd.integer)),
22
+ ]
23
+ },
24
+ ),
25
+ rdf.quad(alice, rdf.ns.schema.knows, carol),
26
+ ])
27
+
28
+ class RdfPeopleCards extends LitElement {
29
+ private readonly focusController = new FocusNode(this)
30
+
31
+ render() {
32
+ const people = this.focusController.array ?? []
33
+ if (!people.length) return html`<p>No matching people</p>`
34
+
35
+ return people.map(person => html`
36
+ <focus-node .focusNode=${person}>
37
+ <article>
38
+ <h2><resource-label predicate="schema:name"></resource-label></h2>
39
+ <p>Age: ${person.out(rdf.ns.foaf.age).value}</p>
40
+ <resource-link>Resource URI</resource-link>
41
+ <p>Friends: ${person.out(rdf.ns.schema.knows).toArray().map(friend => html`
42
+ <focus-node .focusNode=${friend}>
43
+ <resource-label predicate="schema:name"></resource-label>
44
+ </focus-node>
45
+ `)}</p>
46
+ </article>
47
+ </focus-node>
48
+ `)
49
+ }
50
+ }
51
+ customElements.define('rdf-people-cards', RdfPeopleCards)
52
+
53
+ class RdfPeopleDemo extends LitElement {
54
+ static properties = {
55
+ minimumAge: { type: Number },
56
+ direction: { type: String },
57
+ }
58
+
59
+ declare minimumAge: number
60
+ declare direction: 'asc' | 'desc'
61
+
62
+ constructor() {
63
+ super()
64
+ this.minimumAge = 18
65
+ this.direction = 'asc'
66
+ }
67
+
68
+ render() {
69
+ return html`
70
+ <label>Minimum age
71
+ <input type="number" .value=${String(this.minimumAge)}
72
+ @input=${(event: Event) => {
73
+ this.minimumAge = (event.target as HTMLInputElement).valueAsNumber
74
+ }}>
75
+ </label>
76
+ <button @click=${() => {
77
+ this.direction = this.direction === 'asc' ? 'desc' : 'asc'
78
+ }}>Reverse name order</button>
79
+ <rdf-environment>
80
+ <rdf-dataset>
81
+ <rdf-graph .value=${graph}></rdf-graph>
82
+ <target-node target-class="schema:Person"
83
+ order-by="schema:name" order-dir=${this.direction}>
84
+ <filter-node .filter=${(person: GraphPointer) => {
85
+ const age = person.out(rdf.ns.foaf.age).value
86
+ return age !== undefined && Number(age) >= this.minimumAge
87
+ }}>
88
+ <rdf-people-cards></rdf-people-cards>
89
+ </filter-node>
90
+ </target-node>
91
+ </rdf-dataset>
92
+ </rdf-environment>
93
+ `
94
+ }
95
+ }
96
+ customElements.define('rdf-people-demo', RdfPeopleDemo)
97
+
98
+ // Initially: Alice (32, friend Carol), then Carol (24).
99
+ // Minimum age 0 includes Bob (16); 100 displays “No matching people”.
100
+ // Reversing order changes the initial list to Carol, then Alice.
@@ -0,0 +1,101 @@
1
+ # Declarative binding
2
+
3
+ ## Supply data and select one resource
4
+
5
+ This is a complete Lit render function for a browser application with package imports resolved by its build tool:
6
+
7
+ ```ts
8
+ import { html, render } from 'lit'
9
+ import rdf from '@zazuko/env/web.js'
10
+ import 'lit-rdf'
11
+
12
+ const alice = rdf.namedNode('https://example.org/alice')
13
+ const dataset = rdf.dataset([
14
+ rdf.quad(alice, rdf.ns.rdf.type, rdf.ns.schema.Person),
15
+ rdf.quad(alice, rdf.ns.schema.name, rdf.literal('Alice')),
16
+ rdf.quad(alice, rdf.ns.schema.url, rdf.namedNode('https://alice.example.org/')),
17
+ ])
18
+
19
+ render(html`
20
+ <rdf-environment>
21
+ <rdf-dataset .value=${dataset}>
22
+ <target-node target-node="https://example.org/alice">
23
+ <h2><resource-label predicate="schema:name"></resource-label></h2>
24
+ <resource-link property="schema:url">Homepage</resource-link>
25
+ </target-node>
26
+ </rdf-dataset>
27
+ </rdf-environment>
28
+ `, document.body)
29
+ ```
30
+
31
+ Expected output: a heading “Alice” and a “Homepage” anchor linking to `https://alice.example.org/`. The environment supplies RDF factories; the dataset supplies the graph; the target supplies a Clownface pointer; resource elements consume that pointer.
32
+
33
+ `resource-label` defaults to `rdfs:label`. Specify `predicate="schema:name"` for schema names; there is no automatic fallback across arbitrary label predicates. `resource-link` uses the focus URI when `property` is absent or resolves no value. Supply its visible content in the default slot. `resource-value` displays the current pointer's raw `.value`; it has no predicate lookup or datatype formatting API.
34
+
35
+ ## Compose graph sources
36
+
37
+ Use either a dataset bound directly to `rdf-dataset.value`, as above, or child graph contributions:
38
+
39
+ ```ts
40
+ html`
41
+ <rdf-environment>
42
+ <rdf-dataset>
43
+ <rdf-graph .value=${peopleDataset}></rdf-graph>
44
+ <rdf-graph .value=${labelsDataset}
45
+ graph="https://example.org/graphs/labels"></rdf-graph>
46
+ <!-- Put selection and view components here. -->
47
+ </rdf-dataset>
48
+ </rdf-environment>
49
+ `
50
+ ```
51
+
52
+ Here `peopleDataset` and `labelsDataset` are RDF/JS DatasetCore objects, not strings. Graph contributions are merged; without a `graph` override their existing quad graph terms are preserved. Setting `graph` rewrites all contributed quads to that named graph. Graph contribution updates rebuild `rdf-dataset.value`, so avoid treating its direct value binding as an additional independently merged source.
53
+
54
+ `rdf-graph` has no `src` URL loader or inline Turtle parser. Fetch/parse RDF in application code, then bind the resulting dataset. Its `window.graphs` lookup is an optional preloaded factory mechanism keyed by `data-graph` or `id`, not a network loader. The current update hook watches `value`; changing only `graph` does not resubmit the contribution.
55
+
56
+ ## Select focus nodes
57
+
58
+ | Attribute | Property binding type | Meaning |
59
+ |----------------------|---------------------------------|-----------------------------------|
60
+ | `target-node` | `.targetNode` = NamedNode | Explicit resource |
61
+ | `target-class` | `.targetClass` = NamedNode | Subjects with matching `rdf:type` |
62
+ | `target-subjects-of` | `.targetSubjectsOf` = NamedNode | Subjects of a predicate |
63
+ | `target-objects-of` | `.targetObjectsOf` = NamedNode | Objects of a predicate |
64
+
65
+ Use one selector per element. If several are set, the implementation chooses in the table's order; it does not combine them. Prefixed attributes use the library's known prefixes. Use a full IRI or a NamedNode property binding for application vocabularies; declaring a prefix in Turtle does not register it with attribute converters.
66
+
67
+ ## Traverse from a single resource
68
+
69
+ Inside a per-resource focus, a single-valued property can be displayed as:
70
+
71
+ ```ts
72
+ html`
73
+ <traverse-graph property-path="foaf:age">
74
+ <resource-value></resource-value>
75
+ <span slot="empty">Age unknown</span>
76
+ </traverse-graph>
77
+ `
78
+ ```
79
+
80
+ `property-path` is parsed as a SPARQL/SHACL path. For example, `schema:knows/schema:name` follows friends to their names, and `^schema:knows` follows incoming relationships. A `.propertyPath` binding expects the parsed SHACL path object, not the serialized string.
81
+
82
+ For multiple results, choose a callback rather than relying on the `.value` of a multi-pointer:
83
+
84
+ ```ts
85
+ import type { GraphPointer } from 'clownface'
86
+
87
+ html`
88
+ <traverse-graph property-path="schema:knows"
89
+ .renderObjectNode=${(friend: GraphPointer) => html`
90
+ <focus-node .focusNode=${friend}>
91
+ <p><resource-label predicate="schema:name"></resource-label></p>
92
+ </focus-node>
93
+ `}>
94
+ <p slot="empty">No friends listed</p>
95
+ </traverse-graph>
96
+ `
97
+ ```
98
+
99
+ The default slot receives the entire resulting pointer, not one scoped pointer per child. `.renderObjectNodes` receives the whole multi-pointer and takes precedence over `.renderObjectNode`. Neither callback applies the ambient sort automatically.
100
+
101
+ Current implementation caveat: `traverse-graph.willUpdate` checks a `focusNode` changed-property key, while its controller calls `requestUpdate()` without that key. Initial property-path rendering is supported, but a later focus-only change may leave traversal stale. For dynamic views, derive related values directly from the current `GraphPointer` inside the collection renderer, as the demo does, or verify a library fix before relying on reactive traversal.
@@ -0,0 +1,100 @@
1
+ # Collection and application patterns
2
+
3
+ ## Consume a collection, then scope each item
4
+
5
+ Use [people-demo.ts](../assets/people-demo.ts) for the complete version of this pattern:
6
+
7
+ ```ts
8
+ import { html, LitElement } from 'lit'
9
+ import { FocusNode } from 'lit-rdf/controllers.js'
10
+ import 'lit-rdf'
11
+
12
+ class ResourceCards extends LitElement {
13
+ private readonly focusController = new FocusNode(this)
14
+
15
+ render() {
16
+ const items = this.focusController.array ?? []
17
+ return items.length ? items.map(item => html`
18
+ <focus-node .focusNode=${item}>
19
+ <article>
20
+ <resource-link>
21
+ <resource-label predicate="schema:name"></resource-label>
22
+ </resource-link>
23
+ </article>
24
+ </focus-node>
25
+ `) : html`<p>No matching resources</p>`
26
+ }
27
+ }
28
+ customElements.define('resource-cards', ResourceCards)
29
+ ```
30
+
31
+ Place `<resource-cards>` under a target and optional filter. `FocusNode.pointer` is the entire selection. `FocusNode.array` returns per-node pointers and applies ambient sorting. The controller requests host updates when focus or sorting changes. Handle its initially undefined pointer; context becomes available through element connection.
32
+
33
+ For a table, put a `focus-node` inside each cell that needs a resource component, or scope custom row components internally. Do not insert a `focus-node` wrapper between native `table`/`tbody`/`tr` elements in parsed HTML: table parsing can relocate invalid children. For keyed collections, use a stable key appropriate to the data and ensure relation displays update when that key retains an existing component.
34
+
35
+ ## Filter and sort
36
+
37
+ ```ts
38
+ import type { GraphPointer } from 'clownface'
39
+ import rdf from '@zazuko/env/web.js'
40
+
41
+ const minimumAge = 18
42
+ html`
43
+ <target-node target-class="schema:Person"
44
+ order-by="schema:name" order-dir="asc">
45
+ <filter-node .filter=${(person: GraphPointer) => {
46
+ const age = person.out(rdf.ns.foaf.age).value
47
+ return age !== undefined && Number(age) >= minimumAge
48
+ }}>
49
+ <resource-cards></resource-cards>
50
+ </filter-node>
51
+ </target-node>
52
+ `
53
+ ```
54
+
55
+ The filter is a Clownface callback passed as a property. An omitted callback passes the current selection through. For interactive thresholds, keep the threshold in reactive host state and create a new callback in `render()` so `.filter` changes when the threshold changes. A stable callback closing over mutable state alone does not notify `filter-node` to recompute.
56
+
57
+ `order-by` converts a predicate IRI to a key extractor. For a custom key use `.orderBy=${(person: GraphPointer) => person.out(rdf.ns.schema.name).value?.toLowerCase()}`. Consumers must use `FocusNode.array` to honor that sort context. Merely wrapping arbitrary slotted markup in `target-node` does not reorder it.
58
+
59
+ Sorting converts RDF literals to comparable JS values. Use typed numeric/date literals for numeric/date ordering; plain string literals sort as strings. Missing keys sort last in both directions.
60
+
61
+ ## Supply an existing pointer
62
+
63
+ If the host already has a Clownface pointer, bind it directly with `<focus-node .focusNode=${pointer}>`. Descendant resource elements can consume it without `target-node`. Keep `rdf-environment` and `rdf-dataset` ancestors when other descendants require those contexts. Do not pass a bare IRI or a dataset as `.focusNode`.
64
+
65
+ ## Custom context consumers and providers
66
+
67
+ Public imports:
68
+
69
+ ```ts
70
+ import { Dataset, Environment, FocusNode } from 'lit-rdf/controllers.js'
71
+ import { provideEnvironment } from 'lit-rdf/mixins.js'
72
+ ```
73
+
74
+ - `new Environment(this)` consumes an ancestor environment; `.value` throws if no provider exists. Access it after connection, rather than assuming availability in the host constructor.
75
+ - `new Dataset(this, dataset => { /* update host state */ })` consumes dataset changes and exposes `.value` and a Clownface `.pointer`. Update reactive host state or request an update in the callback if the host renders dataset-derived values.
76
+ - `new FocusNode(this)` consumes focus and sorting, exposes `.pointer` and sorted `.array`, and requests updates automatically. `.onChange` can recompute derived state.
77
+ - `class Host extends provideEnvironment(LitElement)` supplies an environment after assigning its public `rdf` property. `rdf-environment` already provides the default web environment and is simpler for ordinary templates.
78
+
79
+ `lit-rdf/context.js` and `lit-rdf/controllers/FocusNode.js` are not exported package paths in this version. Use the public controller barrel rather than inventing context imports for consumers.
80
+
81
+ ## Updates and editing
82
+
83
+ The library displays RDF data; it does not provide form-to-RDF two-way binding. To implement editing, explicitly add/delete RDF quads in application code and publish a new dataset reference through `rdf-dataset.value` or `rdf-graph.value`. In-place changes to a DatasetCore do not themselves trigger Lit property updates or context delivery. With contributed graphs, update the contribution rather than overwriting the parent's aggregate.
84
+
85
+ Example after removing a value in application code:
86
+
87
+ ```ts
88
+ const next = rdf.dataset([...current])
89
+ next.delete(oldQuad)
90
+ next.add(replacementQuad)
91
+ // Assign next to reactive application state bound as .value=${...}.
92
+ ```
93
+
94
+ Keep these current limits in mind for demos involving live data:
95
+
96
+ - `target-node.setFocusNode()` only publishes selections with at least one term; a transition from matches to no matches can retain the previous focus. Do not promise that switching to an absent class clears the view without checking/fixing this behavior.
97
+ - `filter-node` can publish an empty pointer, so it is suitable for demonstrating a working zero-results state.
98
+ - For focus-only traversal updates, see the caveat in [binding.md](binding.md). The asset reads ages and friends from each current pointer in the custom renderer instead.
99
+
100
+ Prefer showing a limitation precisely to adding a broad workaround or silently changing library behavior as part of an example.