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,680 @@
1
+ /**
2
+ * <sol-pod-ops> — standalone pod file/folder operations panel.
3
+ * Renders the same tabbed interface as the sol-pod gear-icon modal,
4
+ * but inline (no modal wrapper).
5
+ *
6
+ * Attributes:
7
+ * source — URL of the file or container to manage
8
+ * login — CSS selector for a <sol-login> element (authenticated fetch)
9
+ *
10
+ * Properties:
11
+ * item — { url, name, isContainer, contentType } override (optional)
12
+ * fetchFn — custom fetch function
13
+ *
14
+ * Events:
15
+ * sol-status({ message, type }) — operation feedback
16
+ * sol-navigate({ url }) — after delete/rename, signals container reload
17
+ */
18
+
19
+ import { CSS as POD_MODAL_CSS, sheet as POD_MODAL_SHEET } from './styles/sol-pod-modal-css.js';
20
+ import { BTN_CSS } from './styles/buttons-css.js';
21
+ import { adopt, sheetFrom } from '../core/adopt.js';
22
+ import { define } from '../core/define.js';
23
+ import { siblingUrl } from '../core/here.js';
24
+ import {
25
+ extOf, contentTypeFor,
26
+ fetchContainer, copyFolder, deleteFolder,
27
+ liveFormatFor, isLiveFormat,
28
+ isEditable, isViewable, isRdf, isImage, isVideo, isAudio, isPDF,
29
+ CT_TO_EXT,
30
+ } from '../core/pod-ops.js';
31
+
32
+ const HOST_CSS = BTN_CSS + `
33
+ :host { display: block; }
34
+ .pod-ops-wrap {
35
+ display: flex; flex-direction: column; height: 100%; overflow: hidden;
36
+ }
37
+ .pod-ops-body {
38
+ flex: 1; min-height: 0; overflow: auto; padding: 12px;
39
+ display: flex; flex-direction: column;
40
+ }
41
+ .pod-ops-footer { padding: 6px 12px; font-size: 0.85em; color: var(--text-muted, #666); }
42
+ `;
43
+
44
+ const hostSheet = sheetFrom(HOST_CSS);
45
+ let _liveEditLoaded = false;
46
+
47
+ /**
48
+ * Standalone pod file/folder operations panel.
49
+ *
50
+ * Renders the same tabbed interface as the sol-pod gear-icon modal,
51
+ * but inline. Tabs: Live Edit, View, Edit, Graph, Download, Rename,
52
+ * Delete, Permissions (files); New File, New Folder, Download, Rename,
53
+ * Delete, Permissions (containers).
54
+ *
55
+ * @class SolPodOps
56
+ * @extends HTMLElement
57
+ * @attr {string} source - URL of the file or container to manage
58
+ * @attr {string} login - CSS selector for a sol-login element
59
+ * @property {Object} item - { url, name, isContainer, contentType } override
60
+ * @property {Function} fetchFn - custom fetch function
61
+ * @fires sol-status - detail: { message, type }
62
+ * @fires sol-navigate - detail: { url }
63
+ */
64
+ class SolPodOps extends HTMLElement {
65
+ static get observedAttributes() { return ['source', 'login']; }
66
+
67
+ constructor() {
68
+ super();
69
+ this.attachShadow({ mode: 'open' });
70
+ this._login = null;
71
+ this._item = null;
72
+ this._blobUrl = null;
73
+ this._initialized = false;
74
+ }
75
+
76
+ get item() { return this._item; }
77
+ set item(v) { this._item = v; if (this.isConnected) this._load(); }
78
+
79
+ get fetchFn() { return this._fetchFn || null; }
80
+ set fetchFn(fn) { this._fetchFn = fn; }
81
+
82
+ connectedCallback() {
83
+ if (!this._initialized) {
84
+ this._initialized = true;
85
+ this._render();
86
+ this._load();
87
+ }
88
+ }
89
+
90
+ attributeChangedCallback(name, oldV, newV) {
91
+ if (oldV === newV) return;
92
+ if (name === 'login') {
93
+ const el = typeof newV === 'string' ? document.querySelector(newV) : newV;
94
+ this._login = el;
95
+ }
96
+ if (name === 'source' && this._initialized) this._load();
97
+ }
98
+
99
+ _fetchFor(url) {
100
+ if (this._fetchFn) return this._fetchFn;
101
+ if (this._login?.fetchFor) return this._login.fetchFor(url);
102
+ return fetch;
103
+ }
104
+
105
+ _render() {
106
+ const s = this.shadowRoot;
107
+ s.innerHTML = `
108
+ <div class="pod-ops-wrap">
109
+ <div class="pod-ops-body"><div class="modal-message">Loading...</div></div>
110
+ <div class="pod-ops-footer"></div>
111
+ </div>`;
112
+ adopt(s, { sheet: hostSheet, css: HOST_CSS });
113
+ if (POD_MODAL_SHEET) {
114
+ s.adoptedStyleSheets = [...s.adoptedStyleSheets, POD_MODAL_SHEET];
115
+ } else {
116
+ const style = document.createElement('style');
117
+ style.textContent = POD_MODAL_CSS;
118
+ s.appendChild(style);
119
+ }
120
+ }
121
+
122
+ async _load() {
123
+ const source = this.getAttribute('source');
124
+ if (!source) return;
125
+
126
+ let item = this._item;
127
+ if (!item) {
128
+ const isContainer = source.endsWith('/');
129
+ const name = isContainer
130
+ ? source.slice(0, -1).split('/').pop()
131
+ : source.split('/').pop();
132
+ item = { url: source, name, isContainer, contentType: '' };
133
+ }
134
+
135
+ // Probe content-type for files
136
+ const displayBase = item.displayName || item.name;
137
+ let effectiveName = displayBase;
138
+ if (!item.isContainer) {
139
+ try {
140
+ const fetchFn = this._fetchFor(item.url);
141
+ const head = await fetchFn(item.url, { method: 'HEAD' });
142
+ const ct = (head.headers.get('Content-Type') || '').split(';')[0].trim();
143
+ if (ct) {
144
+ item.contentType = ct;
145
+ if (!extOf(item.name)) {
146
+ const mapped = CT_TO_EXT[ct];
147
+ if (mapped) effectiveName = displayBase + '.' + mapped;
148
+ }
149
+ }
150
+ } catch {}
151
+ }
152
+
153
+ this._buildTabs(item, effectiveName);
154
+ }
155
+
156
+ async _buildTabs(item, effectiveName) {
157
+ const hasLive = !item.isContainer && isLiveFormat(item.url, item.contentType);
158
+ const fileTabs = hasLive
159
+ ? ['Live Edit', 'Download', 'Rename', 'Delete', 'Permissions']
160
+ : ['View', 'Edit', 'Graph', 'Download', 'Rename', 'Delete', 'Permissions'];
161
+ const tabDefs = item.isContainer
162
+ ? ['New File', 'New Folder', 'Download', 'Rename', 'Delete', 'Permissions']
163
+ : fileTabs;
164
+
165
+ const tabs = tabDefs.filter(name => {
166
+ if (name === 'Edit' && !isEditable(effectiveName)) return false;
167
+ if (name === 'View' && !isViewable(effectiveName)) return false;
168
+ if (name === 'Graph' && !isRdf(effectiveName)) return false;
169
+ return true;
170
+ }).map(name => ({
171
+ name,
172
+ render: (body, footer, actions) => this._renderTab(name.toLowerCase(), item, effectiveName, body, footer, actions)
173
+ }));
174
+
175
+ const defaultTab = item.isContainer ? 'New File'
176
+ : hasLive ? 'Live Edit'
177
+ : isRdf(effectiveName) ? 'Graph'
178
+ : isViewable(effectiveName) ? 'View' : 'Rename';
179
+
180
+ await import('./sol-tabs.js');
181
+ const body = this.shadowRoot.querySelector('.pod-ops-body');
182
+ const footer = this.shadowRoot.querySelector('.pod-ops-footer');
183
+ body.innerHTML = '';
184
+ body.style.padding = '0';
185
+
186
+ // sol-tabs provides its own actions slot between the bar and content;
187
+ // we don't pass actionsEl so toolbar buttons appear flush right at
188
+ // the top of the tab content area.
189
+ const tabsEl = document.createElement('sol-tabs');
190
+ tabsEl.footerEl = footer;
191
+ tabsEl.tabs = tabs;
192
+ body.appendChild(tabsEl);
193
+ tabsEl.switchTab(defaultTab);
194
+ }
195
+
196
+ _renderTab(tabName, item, effectiveName, body, footer, actions) {
197
+ switch (tabName) {
198
+ case 'live edit': return this._tabLive(item, effectiveName, body, footer, actions);
199
+ case 'view': return this._tabView(item, effectiveName, body, footer, actions);
200
+ case 'edit': return this._tabEdit(item, effectiveName, body, footer, actions);
201
+ case 'graph': return this._tabGraph(item, effectiveName, body, footer, actions);
202
+ case 'download': return item.isContainer ? this._tabDownloadFolder(item, body, footer, actions) : this._tabDownloadFile(item, body, footer, actions);
203
+ case 'rename': return this._tabRename(item, body, footer, actions);
204
+ case 'delete': return this._tabDelete(item, body, footer, actions);
205
+ case 'permissions': return this._tabPermissions(item, body, footer, actions);
206
+ case 'new file': return this._tabNewFile(item, body, footer, actions);
207
+ case 'new folder': return this._tabNewFolder(item, body, footer, actions);
208
+ }
209
+ }
210
+
211
+ // ── Live edit tab ───────────────────────────────────────────────────
212
+
213
+ async _tabLive(item, effectiveName, body, footer, actions) {
214
+ body.innerHTML = '<div class="modal-message">Loading...</div>';
215
+ body.style.padding = '0'; body.style.overflow = 'hidden';
216
+
217
+ const fmt = liveFormatFor(item.url, item.contentType);
218
+
219
+ // Save / Settings / Help / Zoom / Statistics are <sol-live-edit>'s
220
+ // own toolbar now \u2014 the modal tab adds nothing here.
221
+ actions.innerHTML = '';
222
+
223
+ const fetchFn = this._fetchFor(item.url);
224
+ const onSave = async (content, url) => {
225
+ try {
226
+ const resp = await fetchFn(url, {
227
+ method: 'PUT', headers: { 'Content-Type': contentTypeFor(effectiveName) },
228
+ body: content,
229
+ });
230
+ if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
231
+ this._emitStatus('Saved.', 'success');
232
+ } catch (e) { this._emitStatus(`Save failed: ${e.message}`, 'error'); }
233
+ };
234
+
235
+ await this._ensureLiveEdit();
236
+ body.innerHTML = '';
237
+ body.style.height = '100%';
238
+
239
+ const el = document.createElement('sol-live-edit');
240
+ el.setAttribute('source', item.url);
241
+ el.setAttribute('format', fmt);
242
+ el.className = 'pod-live-edit';
243
+ if (fetchFn !== fetch) el.fetchFn = fetchFn;
244
+ el.addEventListener('sol-save', async (ev) => {
245
+ await onSave(ev.detail.content, ev.detail.url);
246
+ });
247
+ body.appendChild(el);
248
+
249
+ return () => { body.innerHTML = ''; };
250
+ }
251
+
252
+ async _ensureLiveEdit() {
253
+ if (_liveEditLoaded || customElements.get('sol-live-edit')) { _liveEditLoaded = true; return; }
254
+ const url = siblingUrl('sol-live-edit.js', import.meta.url);
255
+ await import(url);
256
+ _liveEditLoaded = true;
257
+ }
258
+
259
+ // ── View tab ────────────────────────────────────────────────────────
260
+
261
+ async _tabView(item, effectiveName, body, footer, actions) {
262
+ const fetchFn = this._fetchFor(item.url);
263
+ body.innerHTML = '<div class="modal-message">Loading...</div>';
264
+
265
+ try {
266
+ if (isImage(effectiveName)) {
267
+ const blob = await (await fetchFn(item.url)).blob();
268
+ const url = URL.createObjectURL(blob);
269
+ this._blobUrl = url;
270
+ const img = document.createElement('img');
271
+ img.className = 'modal-media'; img.src = url; img.alt = item.displayName || item.name;
272
+ body.innerHTML = ''; body.appendChild(img);
273
+ } else if (isVideo(effectiveName)) {
274
+ const blob = await (await fetchFn(item.url)).blob();
275
+ const url = URL.createObjectURL(blob);
276
+ this._blobUrl = url;
277
+ const vid = document.createElement('video');
278
+ vid.className = 'modal-media'; vid.src = url; vid.controls = true;
279
+ body.innerHTML = ''; body.appendChild(vid);
280
+ } else if (isAudio(effectiveName)) {
281
+ const blob = await (await fetchFn(item.url)).blob();
282
+ const url = URL.createObjectURL(blob);
283
+ this._blobUrl = url;
284
+ const aud = document.createElement('audio');
285
+ aud.className = 'modal-audio'; aud.src = url; aud.controls = true;
286
+ body.innerHTML = ''; body.appendChild(aud);
287
+ } else if (isPDF(effectiveName)) {
288
+ const blob = await (await fetchFn(item.url)).blob();
289
+ const url = URL.createObjectURL(blob);
290
+ this._blobUrl = url;
291
+ const iframe = document.createElement('iframe');
292
+ iframe.className = 'modal-pdf'; iframe.src = url;
293
+ body.innerHTML = ''; body.appendChild(iframe);
294
+ } else if (extOf(effectiveName) === 'md') {
295
+ const text = await (await fetchFn(item.url)).text();
296
+ try {
297
+ const { marked } = await import('https://esm.sh/marked@9');
298
+ const div = document.createElement('div');
299
+ div.className = 'markdown-preview';
300
+ div.innerHTML = marked.parse(text);
301
+ body.innerHTML = ''; body.appendChild(div);
302
+ } catch {
303
+ body.innerHTML = ''; const pre = document.createElement('pre');
304
+ pre.className = 'modal-preview'; pre.textContent = text;
305
+ body.appendChild(pre);
306
+ }
307
+ } else if (extOf(effectiveName) === 'html' || extOf(effectiveName) === 'htm') {
308
+ const text = await (await fetchFn(item.url)).text();
309
+ const iframe = document.createElement('iframe');
310
+ iframe.className = 'modal-pdf'; iframe.sandbox = 'allow-scripts';
311
+ iframe.srcdoc = text;
312
+ body.innerHTML = ''; body.appendChild(iframe);
313
+ } else {
314
+ const text = await (await fetchFn(item.url)).text();
315
+ const pre = document.createElement('pre');
316
+ pre.className = 'modal-preview'; pre.textContent = text;
317
+ body.innerHTML = ''; body.appendChild(pre);
318
+ }
319
+ } catch (e) {
320
+ body.innerHTML = `<div class="modal-message error">Failed to load: ${e.message}</div>`;
321
+ }
322
+ }
323
+
324
+ // ── Edit tab ────────────────────────────────────────────────────────
325
+
326
+ async _tabEdit(item, effectiveName, body, footer, actions) {
327
+ const fetchFn = this._fetchFor(item.url);
328
+ body.innerHTML = '<div class="modal-message">Loading...</div>';
329
+
330
+ try {
331
+ const text = await (await fetchFn(item.url)).text();
332
+ const ta = document.createElement('textarea');
333
+ ta.className = 'modal-editor'; ta.value = text;
334
+ body.innerHTML = ''; body.appendChild(ta);
335
+
336
+ const saveBtn = document.createElement('button');
337
+ saveBtn.className = 'sol-btn sol-btn-sm sol-btn-primary';
338
+ saveBtn.textContent = 'Save';
339
+ saveBtn.onclick = async () => {
340
+ try {
341
+ const resp = await fetchFn(item.url, {
342
+ method: 'PUT',
343
+ headers: { 'Content-Type': contentTypeFor(effectiveName) },
344
+ body: ta.value,
345
+ });
346
+ if (!resp.ok) throw new Error(`${resp.status}`);
347
+ this._emitStatus('Saved.', 'success');
348
+ } catch (e) { this._emitStatus(`Save failed: ${e.message}`, 'error'); }
349
+ };
350
+ if (actions) actions.appendChild(saveBtn);
351
+ } catch (e) {
352
+ body.innerHTML = `<div class="modal-message error">Failed to load: ${e.message}</div>`;
353
+ }
354
+ }
355
+
356
+ // ── Graph tab ───────────────────────────────────────────────────────
357
+
358
+ async _tabGraph(item, effectiveName, body, footer, actions) {
359
+ const fetchFn = this._fetchFor(item.url);
360
+ body.innerHTML = '<div class="modal-message">Loading RDF graph...</div>';
361
+
362
+ try {
363
+ const text = await (await fetchFn(item.url)).text();
364
+ const { rdf } = await import('../core/rdf.js');
365
+ const store = rdf.graph();
366
+ rdf.parse(text, store, item.url, 'text/turtle');
367
+ const triples = store.statements;
368
+
369
+ if (triples.length === 0) {
370
+ body.innerHTML = '<div class="modal-message">No triples found.</div>';
371
+ return;
372
+ }
373
+
374
+ const table = document.createElement('table');
375
+ table.className = 'triple-table';
376
+ const thead = document.createElement('thead');
377
+ thead.innerHTML = '<tr><th>Subject</th><th>Predicate</th><th>Object</th></tr>';
378
+ table.appendChild(thead);
379
+ const tbody = document.createElement('tbody');
380
+ for (const t of triples) {
381
+ const tr = document.createElement('tr');
382
+ const short = (v) => {
383
+ const s = v.value || String(v);
384
+ return s.length > 60 ? s.slice(0, 57) + '...' : s;
385
+ };
386
+ tr.innerHTML = `<td>${short(t.subject)}</td><td>${short(t.predicate)}</td><td>${short(t.object)}</td>`;
387
+ tbody.appendChild(tr);
388
+ }
389
+ table.appendChild(tbody);
390
+ body.innerHTML = '';
391
+ body.appendChild(table);
392
+ footer.innerHTML = `<span class="modal-note">${triples.length} triple(s)</span>`;
393
+ } catch (e) {
394
+ body.innerHTML = `<div class="modal-message error">Parse error: ${e.message}</div>`;
395
+ }
396
+ }
397
+
398
+ // ── Download tabs ───────────────────────────────────────────────────
399
+
400
+ _tabDownloadFile(item, body, footer, actions) {
401
+ body.innerHTML = '';
402
+ const row = document.createElement('div');
403
+ row.className = 'modal-row';
404
+ const display = item.displayName || item.name;
405
+ const msg = document.createElement('div');
406
+ msg.className = 'modal-message';
407
+ msg.append(document.createTextNode('Download '));
408
+ const strong = document.createElement('strong'); strong.textContent = display;
409
+ msg.append(strong);
410
+ const btn = document.createElement('button');
411
+ btn.className = 'sol-btn sol-btn-sm sol-btn-primary';
412
+ btn.textContent = `\u2B07 ${display}`;
413
+ btn.onclick = async () => {
414
+ const fetchFn = this._fetchFor(item.url);
415
+ const resp = await fetchFn(item.url);
416
+ const blob = await resp.blob();
417
+ const url = URL.createObjectURL(blob);
418
+ const a = document.createElement('a');
419
+ a.href = url; a.download = display; a.click();
420
+ URL.revokeObjectURL(url);
421
+ };
422
+ row.append(msg, btn);
423
+ body.appendChild(row);
424
+ }
425
+
426
+ async _tabDownloadFolder(item, body, footer, actions) {
427
+ body.innerHTML = '';
428
+ const row = document.createElement('div');
429
+ row.className = 'modal-row';
430
+ const display = item.displayName || item.name;
431
+ const msg = document.createElement('div');
432
+ msg.className = 'modal-message';
433
+ msg.append(document.createTextNode('Download folder '));
434
+ const strong = document.createElement('strong'); strong.textContent = display;
435
+ msg.append(strong, document.createTextNode(' as ZIP'));
436
+ const btn = document.createElement('button');
437
+ btn.className = 'sol-btn sol-btn-sm sol-btn-primary';
438
+ btn.textContent = `\u2B07 ${display}.zip`;
439
+ btn.onclick = async () => {
440
+ try {
441
+ const JSZip = (await import('https://esm.sh/jszip@3.10.0')).default;
442
+ const zip = new JSZip();
443
+ const addFolder = async (containerUrl, zipFolder) => {
444
+ const fetchFn = this._fetchFor(containerUrl);
445
+ const items = await fetchContainer(containerUrl, fetchFn);
446
+ for (const child of items) {
447
+ const childDisplay = child.displayName || child.name;
448
+ if (child.isContainer) {
449
+ await addFolder(child.url, zipFolder.folder(childDisplay));
450
+ } else {
451
+ const resp = await fetchFn(child.url);
452
+ zipFolder.file(childDisplay, await resp.blob());
453
+ }
454
+ }
455
+ };
456
+ btn.disabled = true; btn.textContent = 'Downloading...';
457
+ await addFolder(item.url, zip.folder(display));
458
+ const blob = await zip.generateAsync({ type: 'blob' });
459
+ const url = URL.createObjectURL(blob);
460
+ const a = document.createElement('a');
461
+ a.href = url; a.download = display + '.zip'; a.click();
462
+ URL.revokeObjectURL(url);
463
+ btn.textContent = 'Done!';
464
+ } catch (e) {
465
+ this._emitStatus(`Download failed: ${e.message}`, 'error');
466
+ btn.disabled = false; btn.textContent = `\u2B07 ${display}.zip`;
467
+ }
468
+ };
469
+ row.append(msg, btn);
470
+ body.appendChild(row);
471
+ }
472
+
473
+ // ── Rename tab ──────────────────────────────────────────────────────
474
+
475
+ _tabRename(item, body, footer, actions) {
476
+ const currentDisplay = item.displayName || item.name;
477
+ const input = document.createElement('input');
478
+ input.className = 'modal-input'; input.type = 'text';
479
+ input.value = currentDisplay;
480
+
481
+ const btn = document.createElement('button');
482
+ btn.className = 'sol-btn sol-btn-sm sol-btn-primary';
483
+ btn.textContent = 'Rename';
484
+ btn.onclick = async () => {
485
+ const newName = input.value.trim();
486
+ if (!newName || newName === currentDisplay) return;
487
+ // User typed the decoded form; URL-encode for the request path so
488
+ // names with spaces / unicode / reserved chars produce valid URLs.
489
+ const encodedNew = encodeURIComponent(newName);
490
+ try {
491
+ const fetchFn = this._fetchFor(item.url);
492
+ if (item.isContainer) {
493
+ const parentUrl = item.url.slice(0, item.url.slice(0, -1).lastIndexOf('/') + 1);
494
+ const fetchFnForUrl = (u) => this._fetchFor(u);
495
+ await copyFolder(item.url, parentUrl, encodedNew, fetchFnForUrl, msg => this._emitStatus(msg, ''));
496
+ await deleteFolder(item.url, fetchFnForUrl);
497
+ } else {
498
+ const containerUrl = item.url.substring(0, item.url.lastIndexOf('/') + 1);
499
+ const newUrl = containerUrl + encodedNew;
500
+ const resp = await fetchFn(item.url);
501
+ if (!resp.ok) throw new Error(`Read failed: ${resp.status}`);
502
+ const blob = await resp.blob();
503
+ await fetchFn(newUrl, { method: 'PUT', headers: { 'Content-Type': contentTypeFor(newName) }, body: blob });
504
+ await fetchFn(item.url, { method: 'DELETE' });
505
+ }
506
+ this._emitStatus('Renamed.', 'success');
507
+ this._emitNavigate(item);
508
+ } catch (e) { this._emitStatus(`Rename failed: ${e.message}`, 'error'); }
509
+ };
510
+ const row = document.createElement('div');
511
+ row.className = 'modal-row';
512
+ row.append(input, btn);
513
+ body.innerHTML = ''; body.appendChild(row);
514
+ }
515
+
516
+ // ── Delete tab ──────────────────────────────────────────────────────
517
+
518
+ _tabDelete(item, body, footer, actions) {
519
+ body.innerHTML = '';
520
+ const row = document.createElement('div');
521
+ row.className = 'modal-row';
522
+ const display = item.displayName || item.name;
523
+ const msg = document.createElement('div');
524
+ msg.className = 'modal-message';
525
+ msg.append(document.createTextNode('Delete '));
526
+ const strong = document.createElement('strong'); strong.textContent = display;
527
+ msg.append(strong, document.createTextNode((item.isContainer ? ' and all its contents' : '') + '?'));
528
+ const btn = document.createElement('button');
529
+ btn.className = 'sol-btn sol-btn-sm sol-btn-danger';
530
+ btn.textContent = 'Delete';
531
+ btn.onclick = async () => {
532
+ try {
533
+ if (item.isContainer) {
534
+ await deleteFolder(item.url, (u) => this._fetchFor(u));
535
+ } else {
536
+ const fetchFn = this._fetchFor(item.url);
537
+ const resp = await fetchFn(item.url, { method: 'DELETE' });
538
+ if (!resp.ok) throw new Error(`${resp.status}`);
539
+ }
540
+ this._emitStatus('Deleted.', 'success');
541
+ this._emitNavigate(item);
542
+ } catch (e) { this._emitStatus(`Delete failed: ${e.message}`, 'error'); }
543
+ };
544
+ row.append(msg, btn);
545
+ body.appendChild(row);
546
+ }
547
+
548
+ // ── New File tab ────────────────────────────────────────────────────
549
+
550
+ _tabNewFile(item, body, footer, actions) {
551
+ body.innerHTML = '';
552
+
553
+ const uploadLabel = document.createElement('div');
554
+ uploadLabel.className = 'modal-label'; uploadLabel.textContent = 'Upload files:';
555
+ const fileInput = document.createElement('input');
556
+ fileInput.type = 'file'; fileInput.multiple = true;
557
+
558
+ const uploadBtn = document.createElement('button');
559
+ uploadBtn.className = 'sol-btn sol-btn-sm sol-btn-primary';
560
+ uploadBtn.textContent = 'Upload';
561
+ uploadBtn.onclick = async () => {
562
+ const files = fileInput.files;
563
+ if (!files.length) return;
564
+ for (const file of files) {
565
+ try {
566
+ const fetchFn = this._fetchFor(item.url);
567
+ await fetchFn(item.url + file.name, {
568
+ method: 'PUT',
569
+ headers: { 'Content-Type': file.type || contentTypeFor(file.name) },
570
+ body: file,
571
+ });
572
+ } catch (e) { this._emitStatus(`Upload failed: ${e.message}`, 'error'); return; }
573
+ }
574
+ this._emitStatus('Uploaded.', 'success');
575
+ this._emitNavigate(item);
576
+ };
577
+ const uploadRow = document.createElement('div');
578
+ uploadRow.className = 'modal-row';
579
+ uploadRow.append(fileInput, uploadBtn);
580
+ body.append(uploadLabel, uploadRow);
581
+
582
+ const hr = document.createElement('hr');
583
+ hr.className = 'modal-hr';
584
+ body.appendChild(hr);
585
+
586
+ const createLabel = document.createElement('div');
587
+ createLabel.className = 'modal-label'; createLabel.textContent = 'Or create a new file:';
588
+ const nameInput = document.createElement('input');
589
+ nameInput.className = 'modal-input'; nameInput.type = 'text';
590
+ nameInput.placeholder = 'filename.ext';
591
+ const contentTA = document.createElement('textarea');
592
+ contentTA.className = 'modal-editor'; contentTA.placeholder = 'File content (optional)';
593
+ contentTA.style.minHeight = '80px';
594
+
595
+ const createBtn = document.createElement('button');
596
+ createBtn.className = 'sol-btn sol-btn-sm sol-btn-primary';
597
+ createBtn.textContent = 'Create File';
598
+ createBtn.onclick = async () => {
599
+ const name = nameInput.value.trim();
600
+ if (!name) return;
601
+ try {
602
+ const fetchFn = this._fetchFor(item.url);
603
+ await fetchFn(item.url + name, {
604
+ method: 'PUT',
605
+ headers: { 'Content-Type': contentTypeFor(name) },
606
+ body: contentTA.value,
607
+ });
608
+ this._emitStatus('Created.', 'success');
609
+ this._emitNavigate(item);
610
+ } catch (e) { this._emitStatus(`Create failed: ${e.message}`, 'error'); }
611
+ };
612
+ const createRow = document.createElement('div');
613
+ createRow.className = 'modal-row';
614
+ createRow.append(nameInput, createBtn);
615
+ body.append(createLabel, createRow, contentTA);
616
+ }
617
+
618
+ // ── New Folder tab ──────────────────────────────────────────────────
619
+
620
+ _tabNewFolder(item, body, footer, actions) {
621
+ const input = document.createElement('input');
622
+ input.className = 'modal-input'; input.type = 'text';
623
+ input.placeholder = 'Folder name';
624
+
625
+ const btn = document.createElement('button');
626
+ btn.className = 'sol-btn sol-btn-sm sol-btn-primary';
627
+ btn.textContent = 'Create Folder';
628
+ btn.onclick = async () => {
629
+ const name = input.value.trim();
630
+ if (!name) return;
631
+ try {
632
+ const fetchFn = this._fetchFor(item.url);
633
+ const url = item.url + name + '/';
634
+ const resp = await fetchFn(url, {
635
+ method: 'PUT', headers: { 'Content-Type': 'text/turtle' }, body: ''
636
+ });
637
+ if (!resp.ok && resp.status !== 409) throw new Error(`${resp.status}`);
638
+ this._emitStatus('Created.', 'success');
639
+ this._emitNavigate(item);
640
+ } catch (e) { this._emitStatus(`Create failed: ${e.message}`, 'error'); }
641
+ };
642
+ const row = document.createElement('div');
643
+ row.className = 'modal-row';
644
+ row.append(input, btn);
645
+ body.innerHTML = ''; body.appendChild(row);
646
+ }
647
+
648
+ // ── Permissions tab ─────────────────────────────────────────────────
649
+
650
+ async _tabPermissions(item, body, footer, actions) {
651
+ body.innerHTML = '';
652
+ await import('./sol-wac.js');
653
+ const wac = document.createElement('sol-wac');
654
+ wac.fetchFn = this._fetchFor(item.url);
655
+ wac.setAttribute('source', item.url);
656
+ wac.addEventListener('sol-status', (e) => this._emitStatus(e.detail.message, e.detail.type));
657
+ body.appendChild(wac);
658
+ }
659
+
660
+ // ── Events ──────────────────────────────────────────────────────────
661
+
662
+ _emitStatus(message, type) {
663
+ this.dispatchEvent(new CustomEvent('sol-status', {
664
+ bubbles: true, composed: true, detail: { message, type }
665
+ }));
666
+ }
667
+
668
+ _emitNavigate(item) {
669
+ const containerUrl = item.isContainer
670
+ ? item.url.slice(0, item.url.slice(0, -1).lastIndexOf('/') + 1)
671
+ : item.url.substring(0, item.url.lastIndexOf('/') + 1);
672
+ this.dispatchEvent(new CustomEvent('sol-navigate', {
673
+ bubbles: true, composed: true, detail: { url: containerUrl }
674
+ }));
675
+ }
676
+ }
677
+
678
+ define('sol-pod-ops', SolPodOps);
679
+ export { SolPodOps };
680
+ export default SolPodOps;