sol-components 2.1.0

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 (150) hide show
  1. package/README.md +7 -0
  2. package/core/activate.js +27 -0
  3. package/core/adopt.js +71 -0
  4. package/core/auth-core.js +73 -0
  5. package/core/auth-fetch.js +154 -0
  6. package/core/component-mount.js +110 -0
  7. package/core/defaults.js +48 -0
  8. package/core/define.js +15 -0
  9. package/core/display-target.js +166 -0
  10. package/core/edit-placements.js +28 -0
  11. package/core/editor-self.js +127 -0
  12. package/core/editor.js +162 -0
  13. package/core/events.js +27 -0
  14. package/core/extension-points.js +189 -0
  15. package/core/form-utils.js +210 -0
  16. package/core/from-query.js +138 -0
  17. package/core/from-rdf.js +52 -0
  18. package/core/here.js +33 -0
  19. package/core/include-core.js +73 -0
  20. package/core/inrupt-global.js +18 -0
  21. package/core/menu-consumer.js +41 -0
  22. package/core/menu-rdf.js +154 -0
  23. package/core/pod-ops.js +392 -0
  24. package/core/pod-registry.js +82 -0
  25. package/core/popup-proxy.js +255 -0
  26. package/core/rdf-core.js +280 -0
  27. package/core/rdf-render.js +136 -0
  28. package/core/rdf-utils.js +411 -0
  29. package/core/rdf.js +154 -0
  30. package/core/services.js +106 -0
  31. package/core/shape-to-form.js +741 -0
  32. package/core/sparql-safety.js +20 -0
  33. package/core/utils.js +196 -0
  34. package/dist/importmap-cdn.json +49 -0
  35. package/dist/importmap-local.json +49 -0
  36. package/dist/sol-loader.manifest.json +140 -0
  37. package/dist/vendor/@comunica-query-sparql.js +137851 -0
  38. package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
  39. package/dist/vendor/dompurify.js +1476 -0
  40. package/dist/vendor/ical.js.js +9739 -0
  41. package/dist/vendor/marked.js +85 -0
  42. package/dist/vendor/n3.js +14670 -0
  43. package/dist/vendor/rdf-validate-shacl.js +6970 -0
  44. package/dist/vendor/rdflib.js +35172 -0
  45. package/dist/vendor/solid-logic.js +6819 -0
  46. package/dist/vendor/solid-ui.js +21945 -0
  47. package/node/sol-form.js +133 -0
  48. package/node/sol-include.js +55 -0
  49. package/node/sol-login.js +632 -0
  50. package/node/sol-menu.js +639 -0
  51. package/node/sol-query.js +116 -0
  52. package/package.json +133 -0
  53. package/web/menu-from-rdf.js +23 -0
  54. package/web/scripts/prefs.js +25 -0
  55. package/web/sol-accordion.js +114 -0
  56. package/web/sol-basic.js +50 -0
  57. package/web/sol-breadcrumb.js +131 -0
  58. package/web/sol-button.js +244 -0
  59. package/web/sol-calendar.js +465 -0
  60. package/web/sol-default.js +118 -0
  61. package/web/sol-dropdown-button.js +222 -0
  62. package/web/sol-feed.js +1336 -0
  63. package/web/sol-form.js +949 -0
  64. package/web/sol-full.js +43 -0
  65. package/web/sol-gallery.js +303 -0
  66. package/web/sol-include.js +246 -0
  67. package/web/sol-live-edit.js +415 -0
  68. package/web/sol-login.js +856 -0
  69. package/web/sol-menu.js +593 -0
  70. package/web/sol-modal.js +377 -0
  71. package/web/sol-pod-extras.js +17 -0
  72. package/web/sol-pod-ops.js +680 -0
  73. package/web/sol-pod.js +1039 -0
  74. package/web/sol-query.js +546 -0
  75. package/web/sol-rolodex.js +95 -0
  76. package/web/sol-search.js +402 -0
  77. package/web/sol-settings.js +199 -0
  78. package/web/sol-solidos.js +93 -0
  79. package/web/sol-tabs.js +445 -0
  80. package/web/sol-time.js +194 -0
  81. package/web/sol-tree-edit.js +492 -0
  82. package/web/sol-wac.js +456 -0
  83. package/web/sol-weather.js +337 -0
  84. package/web/sol-window.js +142 -0
  85. package/web/styles/buttons-css.js +108 -0
  86. package/web/styles/help.css +242 -0
  87. package/web/styles/root.css +112 -0
  88. package/web/styles/sol-accordion-css.js +97 -0
  89. package/web/styles/sol-calendar-css.js +154 -0
  90. package/web/styles/sol-feed-css.js +475 -0
  91. package/web/styles/sol-form-css.js +471 -0
  92. package/web/styles/sol-gallery-css.js +181 -0
  93. package/web/styles/sol-include-css.js +95 -0
  94. package/web/styles/sol-live-edit-css.js +84 -0
  95. package/web/styles/sol-live-edit.css +101 -0
  96. package/web/styles/sol-login-css.js +116 -0
  97. package/web/styles/sol-menu-css.js +145 -0
  98. package/web/styles/sol-modal-css.js +134 -0
  99. package/web/styles/sol-pod-css.js +187 -0
  100. package/web/styles/sol-pod-modal-css.js +203 -0
  101. package/web/styles/sol-query-css.js +140 -0
  102. package/web/styles/sol-query-help.css +267 -0
  103. package/web/styles/sol-query-one-pager.css +67 -0
  104. package/web/styles/sol-search-css.js +157 -0
  105. package/web/styles/sol-solidos-css.js +7 -0
  106. package/web/styles/sol-tabs-css.js +114 -0
  107. package/web/styles/sol-time-css.js +30 -0
  108. package/web/styles/sol-wac-css.js +73 -0
  109. package/web/styles/sol-weather-css.js +59 -0
  110. package/web/styles/solid-logo.svg +9 -0
  111. package/web/styles/view-accordion-css.js +66 -0
  112. package/web/styles/view-anchorlist-css.js +22 -0
  113. package/web/styles/view-autocomplete-css.js +59 -0
  114. package/web/styles/view-rolodex-css.js +102 -0
  115. package/web/styles/view-select-css.js +21 -0
  116. package/web/utils/calendar-fetch.js +388 -0
  117. package/web/utils/code-mirror-editor.js +82 -0
  118. package/web/utils/commons-fetch.js +108 -0
  119. package/web/utils/feed-edit.js +159 -0
  120. package/web/utils/feed-edit.smoke.mjs +74 -0
  121. package/web/utils/feed-fetch.js +573 -0
  122. package/web/utils/live-edit-help/csv.js +64 -0
  123. package/web/utils/live-edit-help/graphviz.js +41 -0
  124. package/web/utils/live-edit-help/jsonld.js +55 -0
  125. package/web/utils/live-edit-help/markdown.js +52 -0
  126. package/web/utils/live-edit-help/mermaid.js +48 -0
  127. package/web/utils/live-edit-help/turtle.js +85 -0
  128. package/web/utils/rdf-config.js +125 -0
  129. package/web/utils/renderers/csv.js +124 -0
  130. package/web/utils/renderers/d3-force.js +82 -0
  131. package/web/utils/renderers/graphviz.js +13 -0
  132. package/web/utils/renderers/html.js +10 -0
  133. package/web/utils/renderers/jsonld.js +63 -0
  134. package/web/utils/renderers/markdown.js +19 -0
  135. package/web/utils/renderers/mermaid.js +54 -0
  136. package/web/utils/renderers/turtle.js +51 -0
  137. package/web/utils/sol-query-triple-patterns.js +151 -0
  138. package/web/utils/sol-query-ui.js +250 -0
  139. package/web/utils/sol-query-views.js +32 -0
  140. package/web/views/_helpers.js +34 -0
  141. package/web/views/accordion.js +133 -0
  142. package/web/views/anchorlist.js +59 -0
  143. package/web/views/auto-complete.js +183 -0
  144. package/web/views/dl.js +38 -0
  145. package/web/views/list.js +19 -0
  146. package/web/views/menu.js +56 -0
  147. package/web/views/rolodex.js +126 -0
  148. package/web/views/select.js +79 -0
  149. package/web/views/table.js +73 -0
  150. package/web/views/tabs.js +57 -0
@@ -0,0 +1,492 @@
1
+ /**
2
+ * <sol-tree-edit> — drill-down editor for tree-shaped editable data.
3
+ *
4
+ * Composes <sol-breadcrumb> + <sol-accordion> + <sol-form>'s
5
+ * shape-driven internals into a single editor for a container that has
6
+ * a head (its own properties) plus an ordered list of items (each
7
+ * editable as its own subject). Items can themselves be containers —
8
+ * clicking "Open →" pushes a breadcrumb segment and re-renders at the
9
+ * deeper subject. The page stays visually flat at every depth: one
10
+ * accordion, one breadcrumb, no nested accordions.
11
+ *
12
+ * Two SHACL shapes drive the layout:
13
+ * head-shape — sh:property entries for the container's own fields
14
+ * (e.g. ui:label, ui:orientation on a ui:Menu).
15
+ * item-shape — a SHACL document containing one NodeShape per item
16
+ * type (with sh:targetClass on each). sol-tree-edit
17
+ * picks the matching shape for each item based on the
18
+ * item's rdf:type.
19
+ *
20
+ * Attributes:
21
+ * root — starting subject URI (with #fragment).
22
+ * head-shape — URI of the head SHACL shape file.
23
+ * item-shape — URI of the item shapes file (multiple NodeShapes,
24
+ * each with sh:targetClass).
25
+ * parts — predicate URI linking container → ordered list of
26
+ * items. Default: http://www.w3.org/ns/ui#parts.
27
+ * drill-when-type — rdf:type URI(s), space-separated. Items of these
28
+ * types render an "Open →" affordance instead of an
29
+ * inline form panel. Default: http://www.w3.org/ns/ui#Menu.
30
+ * label-property — predicate used for the accordion summary text on
31
+ * each item. Default: http://www.w3.org/ns/ui#label.
32
+ * root-label — the breadcrumb label for the root subject; falls
33
+ * back to the root's label-property value.
34
+ * head-label — label for the first accordion section (the head
35
+ * form). Default: "Heading".
36
+ * items-label — label for the divider between head and items.
37
+ * Default: "Items".
38
+ *
39
+ * Events (bubbling, composed):
40
+ * sol-tree-navigate — detail: { stack } — after drill / back.
41
+ * sol-form-save — bubbles up from the inner sol-form when an edit
42
+ * persists.
43
+ */
44
+
45
+ import { define } from '../core/define.js';
46
+ import { ensureDocStyle } from '../core/adopt.js';
47
+ import { rdf } from '../core/rdf.js';
48
+ import { parseShape, renderRecordForm, readShapeProperty } from '../core/shape-to-form.js';
49
+
50
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
51
+ const UI_PARTS = 'http://www.w3.org/ns/ui#parts';
52
+ const UI_LABEL = 'http://www.w3.org/ns/ui#label';
53
+ const UI_MENU = 'http://www.w3.org/ns/ui#Menu';
54
+ const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
55
+ const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
56
+ const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
57
+
58
+ const CSS = `
59
+ sol-tree-edit {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 0.8rem;
63
+ font-family: var(--font-ui, system-ui, sans-serif);
64
+ }
65
+ sol-tree-edit .sol-tree-edit-loading,
66
+ sol-tree-edit .sol-tree-edit-error {
67
+ padding: 0.6rem 0.9rem;
68
+ color: var(--text-muted, #4d4d4d);
69
+ font-style: italic;
70
+ }
71
+ sol-tree-edit .sol-tree-edit-error {
72
+ color: var(--error, #c00);
73
+ font-style: normal;
74
+ }
75
+ sol-tree-edit .sol-tree-edit-section {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 0.35rem;
79
+ }
80
+ sol-tree-edit .sol-tree-edit-section-label {
81
+ font-size: 0.75rem;
82
+ letter-spacing: 0.05em;
83
+ text-transform: uppercase;
84
+ color: var(--text-muted, #4d4d4d);
85
+ margin: 0 0.2rem;
86
+ }
87
+ sol-tree-edit .sol-tree-edit-item-row {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: space-between;
91
+ gap: 0.6rem;
92
+ width: 100%;
93
+ }
94
+ sol-tree-edit .sol-tree-edit-open-btn {
95
+ background: none;
96
+ border: 1px solid var(--border, #d0d0d0);
97
+ border-radius: 4px;
98
+ padding: 0.2em 0.7em;
99
+ font: inherit;
100
+ font-size: 0.85em;
101
+ color: var(--accent, #1F618D);
102
+ cursor: pointer;
103
+ }
104
+ sol-tree-edit .sol-tree-edit-open-btn:hover {
105
+ background: var(--hover, #eaf2fb);
106
+ }
107
+ sol-tree-edit .sol-tree-edit-add {
108
+ align-self: flex-start;
109
+ background: none;
110
+ border: 1px dashed var(--border, #d0d0d0);
111
+ border-radius: 4px;
112
+ padding: 0.35em 0.8em;
113
+ font: inherit;
114
+ font-size: 0.85em;
115
+ color: var(--text-muted, #4d4d4d);
116
+ cursor: pointer;
117
+ }
118
+ sol-tree-edit .sol-tree-edit-add:hover {
119
+ color: var(--accent, #1F618D);
120
+ border-color: var(--accent, #1F618D);
121
+ }
122
+ sol-tree-edit .sol-tree-edit-item-controls {
123
+ display: inline-flex;
124
+ gap: 0.2em;
125
+ }
126
+ sol-tree-edit .sol-tree-edit-item-controls button {
127
+ background: none;
128
+ border: 1px solid var(--border, #d0d0d0);
129
+ border-radius: 4px;
130
+ width: 1.6em; height: 1.6em;
131
+ padding: 0; line-height: 1;
132
+ color: var(--text-muted, #4d4d4d);
133
+ cursor: pointer;
134
+ font-size: 0.85em;
135
+ }
136
+ sol-tree-edit .sol-tree-edit-item-controls button:hover {
137
+ border-color: var(--accent, #1F618D);
138
+ color: var(--accent, #1F618D);
139
+ }
140
+ sol-tree-edit .sol-tree-edit-item-controls .sol-tree-edit-remove:hover {
141
+ border-color: var(--error, #c00);
142
+ color: var(--error, #c00);
143
+ }
144
+ sol-tree-edit .sol-tree-edit-items-divider {
145
+ margin: 0.6rem 0 0.2rem;
146
+ padding: 0 0.3rem;
147
+ font-size: 0.72rem;
148
+ letter-spacing: 0.08em;
149
+ text-transform: uppercase;
150
+ color: var(--text-muted, #4d4d4d);
151
+ border-top: 1px solid var(--border-soft, #e0e0e0);
152
+ padding-top: 0.5rem;
153
+ }
154
+ `;
155
+
156
+ class SolTreeEdit extends HTMLElement {
157
+ static get observedAttributes() {
158
+ return ['root', 'head-shape', 'item-shape', 'parts', 'drill-when-type',
159
+ 'label-property', 'root-label', 'head-label', 'items-label'];
160
+ }
161
+
162
+ connectedCallback() {
163
+ ensureDocStyle(this.getRootNode(), 'sol-tree-edit-styles', CSS);
164
+ this._stack = []; // [{ subject: NamedNode, label: string }, ...]
165
+ this._headShapeText = null;
166
+ this._itemShapeText = null;
167
+ this._headParsed = null;
168
+ this._itemShapes = null; // [{ nodeShape, target: NamedNode, properties: ShapeProp[] }]
169
+ this._rendered = false;
170
+
171
+ this._render().catch(err => this._fatal(err));
172
+ }
173
+
174
+ attributeChangedCallback(_name, oldV, newV) {
175
+ if (!this._rendered || oldV === newV) return;
176
+ this._stack = [];
177
+ this._render().catch(err => this._fatal(err));
178
+ }
179
+
180
+ async _render() {
181
+ this.innerHTML = '<div class="sol-tree-edit-loading">Loading…</div>';
182
+
183
+ const rootUri = this.getAttribute('root');
184
+ if (!rootUri) { this._fatal(new Error('sol-tree-edit needs a `root` attribute')); return; }
185
+ const rootAbs = new URL(rootUri, document.baseURI).href;
186
+ const docUrl = rootAbs.split('#')[0];
187
+
188
+ // Load the data document into the singleton store so any other
189
+ // shape-to-form consumer (e.g. dk-settings widgets) shares it.
190
+ await rdf.store.fetcher.load(docUrl);
191
+
192
+ // Parse shape files (cache after first load).
193
+ if (!this._headShapeText) {
194
+ const headUri = new URL(this.getAttribute('head-shape'), document.baseURI).href;
195
+ this._headShapeText = await (await fetch(headUri)).text();
196
+ // Pass the root subject so parseShape selects the container NodeShape by
197
+ // sh:targetClass (e.g. ui:Menu → MenuShape) rather than falling back to
198
+ // the first sh:node-referenced shape — essential when head-shape is a
199
+ // multi-NodeShape file like menu.shacl.
200
+ this._headParsed = await parseShape(this._headShapeText, headUri, {
201
+ subject: rdf.sym(rootAbs), dataStore: rdf.store,
202
+ });
203
+ }
204
+ if (!this._itemShapeText) {
205
+ const itemUri = new URL(this.getAttribute('item-shape'), document.baseURI).href;
206
+ this._itemShapeText = await (await fetch(itemUri)).text();
207
+ this._itemShapes = this._parseItemShapes(this._itemShapeText, itemUri);
208
+ }
209
+
210
+ // Initialise the breadcrumb stack with the root.
211
+ if (this._stack.length === 0) {
212
+ const rootSubj = rdf.sym(rootAbs);
213
+ const labelProp = this._labelProperty();
214
+ const rootLabel = this.getAttribute('root-label')
215
+ || rdf.store.anyValue(rootSubj, labelProp) || lastSegment(rootAbs);
216
+ this._stack.push({ subject: rootSubj, label: rootLabel });
217
+ }
218
+
219
+ this._paint();
220
+ this._rendered = true;
221
+ }
222
+
223
+ // Parse the item-shape file into a list of { nodeShape, target, properties }.
224
+ // sh:targetClass is the discriminator — picks one shape per item type.
225
+ _parseItemShapes(text, base) {
226
+ const SH = 'http://www.w3.org/ns/shacl#';
227
+ const store = rdf.graph();
228
+ rdf.parse(text, store, base, 'text/turtle');
229
+ const out = [];
230
+ for (const ns of store.each(null, rdf.sym(RDF_TYPE), rdf.sym(SH + 'NodeShape'))) {
231
+ const target = store.any(ns, rdf.sym(SH + 'targetClass'));
232
+ if (!target) continue;
233
+ // Reuse parseShape's property walker by feeding the same text +
234
+ // pulling the properties off the matching NodeShape. parseShape
235
+ // returns the FIRST NodeShape's properties; instead, we walk
236
+ // manually here to get a per-shape parse.
237
+ const props = [];
238
+ for (const p of store.each(ns, rdf.sym(SH + 'property'))) {
239
+ const desc = readShapeProperty(store, p);
240
+ if (desc) props.push(desc);
241
+ }
242
+ out.push({ nodeShape: ns, target, properties: props });
243
+ }
244
+ return out;
245
+ }
246
+
247
+ _labelProperty() {
248
+ return rdf.sym(this.getAttribute('label-property') || UI_LABEL);
249
+ }
250
+
251
+ _drillTypes() {
252
+ const raw = this.getAttribute('drill-when-type') || UI_MENU;
253
+ return raw.split(/\s+/).filter(Boolean).map(rdf.sym.bind(rdf));
254
+ }
255
+
256
+ _partsPredicate() {
257
+ return rdf.sym(this.getAttribute('parts') || UI_PARTS);
258
+ }
259
+
260
+ _fatal(err) {
261
+ this.innerHTML = `<div class="sol-tree-edit-error">${err.message}</div>`;
262
+ console.error('[sol-tree-edit]', err);
263
+ }
264
+
265
+ // Render the current level: breadcrumb + accordion containing the
266
+ // head form + one panel per item + an Add row.
267
+ _paint() {
268
+ this.innerHTML = '';
269
+ const current = this._stack[this._stack.length - 1];
270
+ const { subject } = current;
271
+
272
+ // ── Breadcrumb ─────────────────────────────────────────────────
273
+ // Only render when there's somewhere to go back to — a single
274
+ // segment is just a label, not navigation, and dilutes the page.
275
+ if (this._stack.length > 1) {
276
+ const crumb = document.createElement('sol-breadcrumb');
277
+ this._stack.forEach((s, i) => {
278
+ const seg = document.createElement('span');
279
+ seg.dataset.key = String(i);
280
+ seg.textContent = s.label;
281
+ crumb.appendChild(seg);
282
+ });
283
+ crumb.addEventListener('sol-breadcrumb-navigate', (e) => {
284
+ const idx = e.detail.index;
285
+ this._stack = this._stack.slice(0, idx + 1);
286
+ this._paint();
287
+ this.dispatchEvent(new CustomEvent('sol-tree-navigate', {
288
+ bubbles: true, composed: true, detail: { stack: this._stack.slice() },
289
+ }));
290
+ });
291
+ this.appendChild(crumb);
292
+ }
293
+
294
+ // Head and items go into SEPARATE sol-accordion instances so a
295
+ // labelled divider can sit between them. sol-accordion's
296
+ // connectedCallback wipes any non-DIV children of its own light
297
+ // DOM, so a divider can't live inside one accordion — it has to
298
+ // be a sibling between two. Both accordions have their own
299
+ // exclusive group, which means the head panel can stay open while
300
+ // the user expands an item; that's the right UX for "edit one
301
+ // thing at a time within each section."
302
+
303
+ // Head form panel.
304
+ const headLabel = this.getAttribute('head-label') || 'Heading';
305
+ const headAccordion = document.createElement('sol-accordion');
306
+ const headPanel = this._buildAccordionPanel(headLabel, () => {
307
+ const body = document.createElement('div');
308
+ // The items predicate (ui:parts) is the tree's own list, edited as the
309
+ // item panels below — not a scalar head field. Drop it so a combined
310
+ // shape (e.g. menu.shacl, whose ui:Menu NodeShape includes ui:parts)
311
+ // can serve as both head- and item-shape.
312
+ const partsPred = this._partsPredicate().value;
313
+ const headProps = (this._headParsed.properties || [])
314
+ .filter((p) => !(p.path && p.path.value === partsPred));
315
+ renderRecordForm(body, rdf.store, subject, headProps, {
316
+ doc: rdf.sym(this._currentDoc()),
317
+ onChange: () => this._onChange(),
318
+ });
319
+ return body;
320
+ });
321
+ headAccordion.appendChild(headPanel);
322
+ this.appendChild(headAccordion);
323
+
324
+ // Items section: labelled divider + a second accordion containing
325
+ // one panel per item.
326
+ const items = this._currentItems(subject);
327
+ if (items.length > 0) {
328
+ const divider = document.createElement('div');
329
+ divider.className = 'sol-tree-edit-items-divider';
330
+ divider.textContent = this.getAttribute('items-label') || 'Items';
331
+ this.appendChild(divider);
332
+ }
333
+ const accordion = document.createElement('sol-accordion');
334
+ const drillTypes = this._drillTypes().map(t => t.value);
335
+ for (const item of items) {
336
+ const itemLabel = rdf.store.anyValue(item, this._labelProperty()) || lastSegment(item.value);
337
+ const itemType = rdf.store.any(item, rdf.sym(RDF_TYPE));
338
+ const isDrillable = !!itemType && drillTypes.includes(itemType.value);
339
+
340
+ const summary = document.createElement('div');
341
+ summary.className = 'sol-tree-edit-item-row';
342
+ const titleSpan = document.createElement('span');
343
+ titleSpan.textContent = isDrillable ? `${itemLabel} ▸` : itemLabel;
344
+ summary.appendChild(titleSpan);
345
+ const controls = document.createElement('span');
346
+ controls.className = 'sol-tree-edit-item-controls';
347
+ summary.appendChild(controls);
348
+
349
+ const body = document.createElement('div');
350
+ if (isDrillable) {
351
+ // Drillable: body shows "Open →"; expanding the panel doesn't
352
+ // mount a form here — the user drills via the button.
353
+ const open = document.createElement('button');
354
+ open.className = 'sol-tree-edit-open-btn';
355
+ open.type = 'button';
356
+ open.textContent = 'Open →';
357
+ open.addEventListener('click', (e) => {
358
+ e.stopPropagation();
359
+ this._stack.push({ subject: item, label: itemLabel });
360
+ this._paint();
361
+ });
362
+ body.appendChild(open);
363
+ } else {
364
+ // Mount the per-type item form.
365
+ const shape = this._matchItemShape(item);
366
+ if (!shape) {
367
+ body.textContent = '(no shape declared for this item type)';
368
+ } else {
369
+ renderRecordForm(body, rdf.store, item, shape.properties, {
370
+ doc: rdf.sym(this._currentDoc()),
371
+ onChange: () => this._onChange(),
372
+ });
373
+ }
374
+ }
375
+
376
+ const panel = document.createElement('div');
377
+ panel.appendChild(summary);
378
+ panel.appendChild(body);
379
+ accordion.appendChild(panel);
380
+ }
381
+
382
+ // Only attach the items accordion when there's at least one item;
383
+ // otherwise the empty accordion would dangle below the divider.
384
+ if (items.length > 0) this.appendChild(accordion);
385
+
386
+ // Make head + items panels mutually exclusive across the two
387
+ // accordions: opening any details in one closes all open details
388
+ // in the other. sol-accordion's own exclusive grouping is per-
389
+ // instance (each picks a unique <details name=…>), so we wire the
390
+ // cross-accordion coordination ourselves via toggle events.
391
+ const allAccordions = items.length > 0
392
+ ? [headAccordion, accordion]
393
+ : [headAccordion];
394
+ this._bindMutualExclusion(allAccordions);
395
+
396
+ // TODO: wire add / remove / reorder. v0 ships read-render of items.
397
+ }
398
+
399
+ // Listen for `toggle` events on every <details> in each accordion;
400
+ // when one opens, close any open details in the OTHER accordions.
401
+ // Deferred via setTimeout because sol-accordion's connectedCallback
402
+ // runs synchronously after appendChild but before its child
403
+ // <details> are reachable from outside.
404
+ //
405
+ // Also reconciles the INITIAL state: sol-accordion opens its first
406
+ // details on mount, so without intervention the head's first panel
407
+ // and the items' first panel would both start open. Reconcile by
408
+ // keeping the head's first open and closing every other.
409
+ _bindMutualExclusion(accordions) {
410
+ setTimeout(() => {
411
+ const detailsByAccordion = accordions.map(a => Array.from(a.querySelectorAll('details')));
412
+ // Initial-state reconcile: keep the first open panel found across
413
+ // all accordions; close every other open one. (The first
414
+ // accordion is the head, so its initial-open survives — exactly
415
+ // what we want for the "Menu Heading" default-expanded look.)
416
+ let kept = false;
417
+ for (const group of detailsByAccordion) {
418
+ for (const det of group) {
419
+ if (!det.open) continue;
420
+ if (kept) det.open = false;
421
+ else kept = true;
422
+ }
423
+ }
424
+ detailsByAccordion.forEach((group, i) => {
425
+ for (const det of group) {
426
+ det.addEventListener('toggle', () => {
427
+ if (!det.open) return;
428
+ detailsByAccordion.forEach((other, j) => {
429
+ if (j === i) return;
430
+ for (const sibling of other) {
431
+ if (sibling.open) sibling.open = false;
432
+ }
433
+ });
434
+ });
435
+ }
436
+ });
437
+ }, 0);
438
+ }
439
+
440
+ _currentItems(subject) {
441
+ const partsPred = this._partsPredicate();
442
+ const head = rdf.store.any(subject, partsPred);
443
+ if (!head) return [];
444
+ // ui:parts is an rdf:List; rdflib parses turtle list syntax into a
445
+ // Collection node (.elements). For an in-graph first/rest list,
446
+ // walk the chain.
447
+ if (head.termType === 'Collection' && Array.isArray(head.elements)) {
448
+ return head.elements;
449
+ }
450
+ const out = [];
451
+ let node = head;
452
+ while (node && node.value !== RDF_NIL) {
453
+ const first = rdf.store.any(node, rdf.sym(RDF_FIRST));
454
+ if (first) out.push(first);
455
+ node = rdf.store.any(node, rdf.sym(RDF_REST));
456
+ }
457
+ return out;
458
+ }
459
+
460
+ _matchItemShape(item) {
461
+ const types = rdf.store.each(item, rdf.sym(RDF_TYPE)).map(t => t.value);
462
+ for (const sh of this._itemShapes || []) {
463
+ if (types.includes(sh.target.value)) return sh;
464
+ }
465
+ return null;
466
+ }
467
+
468
+ _buildAccordionPanel(label, contentBuilder) {
469
+ const panel = document.createElement('div');
470
+ const head = document.createElement('div');
471
+ head.textContent = label;
472
+ panel.appendChild(head);
473
+ panel.appendChild(contentBuilder());
474
+ return panel;
475
+ }
476
+
477
+ _currentDoc() {
478
+ const rootUri = this.getAttribute('root');
479
+ if (!rootUri) return null;
480
+ return new URL(rootUri, document.baseURI).href.split('#')[0];
481
+ }
482
+
483
+ _onChange() {
484
+ // Auto-save flows through the singleton store; sol-form-save event
485
+ // bubbles from each inner renderRecordForm via the store mutations.
486
+ // We just re-paint the current level to pick up any structural
487
+ // changes (e.g., parts list reordering).
488
+ // For v0 keep it minimal — defer to consumer's listening.
489
+ }
490
+ }
491
+
492
+ define('sol-tree-edit', SolTreeEdit);