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
package/web/sol-wac.js ADDED
@@ -0,0 +1,456 @@
1
+ /**
2
+ * <sol-wac> — Web Access Control (WAC) editor.
3
+ *
4
+ * Displays and edits the ACL for a Solid resource. Renders two subtabs:
5
+ * - Form: one row per role (viewer/poster/editor/owner) with a grant
6
+ * select (nobody / specific / authenticated / public) and an inline
7
+ * WebID+group panel for the "specific" case. A container-only checkbox
8
+ * toggles acl:default (apply to contents).
9
+ * - RDF: raw Turtle in a textarea.
10
+ *
11
+ * Attributes:
12
+ * source — resource URL whose ACL should be loaded
13
+ *
14
+ * Properties:
15
+ * fetchFn — authenticated fetch (defaults to window.fetch)
16
+ * source — mirror of the `source` attribute
17
+ *
18
+ * Methods:
19
+ * load() — (re)load the ACL from the server
20
+ * save() — PUT the current Turtle to the resolved ACL URL
21
+ *
22
+ * Events (bubbling, composed):
23
+ * sol-wac-save — ACL saved successfully ({ aclUrl })
24
+ * sol-wac-error — load or save failed ({ phase, error })
25
+ * sol-status — human-readable status ({ message, type })
26
+ */
27
+
28
+ import { define } from '../core/define.js';
29
+ import { ensureDocStyle } from '../core/adopt.js';
30
+ import { CSS as WAC_CSS } from './styles/sol-wac-css.js';
31
+ import { rdf } from '../core/rdf.js';
32
+ import './sol-tabs.js';
33
+
34
+ // ── ACL constants ─────────────────────────────────────────────────────
35
+
36
+ const ACL = 'http://www.w3.org/ns/auth/acl#';
37
+ const FOAF = 'http://xmlns.com/foaf/0.1/';
38
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
39
+
40
+ export const ROLES = [
41
+ { key: 'viewer', label: 'Viewer', modes: [ACL+'Read'] },
42
+ { key: 'poster', label: 'Poster', modes: [ACL+'Read', ACL+'Append'] },
43
+ { key: 'editor', label: 'Editor', modes: [ACL+'Read', ACL+'Write', ACL+'Append'] },
44
+ { key: 'owner', label: 'Owner', modes: [ACL+'Read', ACL+'Write', ACL+'Append', ACL+'Control'] },
45
+ ];
46
+
47
+ export const GRANT_OPTIONS = [
48
+ { value: 'nobody', label: 'Nobody' },
49
+ { value: 'specific', label: 'Specific people/groups' },
50
+ { value: 'authenticated', label: 'Anyone logged in' },
51
+ { value: 'public', label: 'Everyone (public)' },
52
+ ];
53
+
54
+ // ── ACL discovery ─────────────────────────────────────────────────────
55
+
56
+ export async function getAclUrl(resourceUrl, fetchFn) {
57
+ try {
58
+ const resp = await fetchFn(resourceUrl, { method: 'HEAD', headers: { 'Cache-Control': 'no-cache' } });
59
+ const link = resp.headers.get('Link') || '';
60
+ const match = link.match(/<([^>]+)>\s*;\s*rel="acl"/);
61
+ if (match) return new URL(match[1], resourceUrl).href;
62
+ } catch (e) {}
63
+ return resourceUrl + '.acl';
64
+ }
65
+
66
+ export async function getPermissions(url, fetchFn) {
67
+ const aclUrl = await getAclUrl(url, fetchFn);
68
+ const ownResp = await fetchFn(aclUrl, { headers: { 'Cache-Control': 'no-cache' } });
69
+ if (ownResp.ok) {
70
+ return { own: await ownResp.text(), aclUrl, inherited: null, inheritedFrom: null };
71
+ }
72
+
73
+ const urlObj = new URL(url);
74
+ let parts = urlObj.pathname.replace(/\/$/, '').split('/').filter(Boolean);
75
+ while (parts.length > 0) {
76
+ parts = parts.slice(0, -1);
77
+ const parentUrl = urlObj.origin + '/' + parts.join('/') + (parts.length ? '/' : '');
78
+ const parentAclUrl = await getAclUrl(parentUrl, fetchFn);
79
+ const parentResp = await fetchFn(parentAclUrl, { headers: { 'Cache-Control': 'no-cache' } });
80
+ if (parentResp.ok) {
81
+ return { own: null, aclUrl, inherited: await parentResp.text(), inheritedFrom: parentUrl };
82
+ }
83
+ }
84
+ return { own: null, aclUrl, inherited: null, inheritedFrom: null };
85
+ }
86
+
87
+ // ── ACL parsing ───────────────────────────────────────────────────────
88
+
89
+ export function parseAcl(turtleText, baseUrl) {
90
+ const store = rdf.graph();
91
+ try {
92
+ rdf.parse(turtleText, store, baseUrl, 'text/turtle');
93
+ } catch (e) {
94
+ return [];
95
+ }
96
+
97
+ const authSubjects = store
98
+ .each(null, rdf.sym(RDF_TYPE), rdf.sym(ACL + 'Authorization'), null)
99
+ .map(s => s.value);
100
+
101
+ const vals = (subject, pred) =>
102
+ store.each(rdf.sym(subject), rdf.sym(pred), null, null).map(n => n.value);
103
+
104
+ const auths = [];
105
+ for (const subject of new Set(authSubjects)) {
106
+ const auth = {
107
+ subject,
108
+ modes: vals(subject, ACL + 'mode'),
109
+ agents: vals(subject, ACL + 'agent'),
110
+ agentClasses: vals(subject, ACL + 'agentClass'),
111
+ agentGroups: vals(subject, ACL + 'agentGroup'),
112
+ accessTo: vals(subject, ACL + 'accessTo'),
113
+ default: vals(subject, ACL + 'default'),
114
+ };
115
+ if (auth.modes.length > 0) auths.push(auth);
116
+ }
117
+ return auths;
118
+ }
119
+
120
+ function _bestRoleForModes(modes) {
121
+ const has = m => modes.includes(ACL + m);
122
+ if (has('Control')) return 'owner';
123
+ if (has('Write')) return 'editor';
124
+ if (has('Append')) return 'poster';
125
+ if (has('Read')) return 'viewer';
126
+ return null;
127
+ }
128
+
129
+ export function authsToRoleModel(auths) {
130
+ const model = {};
131
+ ROLES.forEach(r => {
132
+ model[r.key] = { grant: 'nobody', webids: [], groups: [], applyToContents: false };
133
+ });
134
+
135
+ for (const auth of auths) {
136
+ const roleKey = _bestRoleForModes(auth.modes);
137
+ if (!roleKey) continue;
138
+ const rm = model[roleKey];
139
+ if (auth.agentClasses.includes(FOAF+'Agent')) rm.grant = 'public';
140
+ else if (auth.agentClasses.includes(ACL+'AuthenticatedAgent')) {
141
+ if (rm.grant !== 'public') rm.grant = 'authenticated';
142
+ }
143
+ else if (auth.agents.length > 0 || auth.agentGroups.length > 0) {
144
+ if (rm.grant === 'nobody') rm.grant = 'specific';
145
+ rm.webids = [...new Set([...rm.webids, ...auth.agents])];
146
+ rm.groups = [...new Set([...rm.groups, ...auth.agentGroups])];
147
+ }
148
+ if (auth.default.length > 0) rm.applyToContents = true;
149
+ }
150
+ return model;
151
+ }
152
+
153
+ export function roleModelToTurtle(model, resourceUrl) {
154
+ let turtle = '@prefix acl: <http://www.w3.org/ns/auth/acl#>.\n@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n\n';
155
+ let idx = 0;
156
+ const isContainer = resourceUrl.endsWith('/');
157
+
158
+ for (const role of ROLES) {
159
+ const rm = model[role.key];
160
+ if (rm.grant === 'nobody') continue;
161
+ idx++;
162
+ turtle += `<#auth${idx}>\n a acl:Authorization;\n`;
163
+ turtle += ` acl:accessTo <${resourceUrl}>;\n`;
164
+ if (isContainer && rm.applyToContents) turtle += ` acl:default <${resourceUrl}>;\n`;
165
+ turtle += ` acl:mode ${role.modes.map(m => 'acl:' + m.split('#')[1]).join(', ')};\n`;
166
+
167
+ if (rm.grant === 'public') turtle += ' acl:agentClass foaf:Agent.\n\n';
168
+ else if (rm.grant === 'authenticated') turtle += ' acl:agentClass acl:AuthenticatedAgent.\n\n';
169
+ else {
170
+ const parts = [];
171
+ rm.webids.forEach(w => parts.push(`acl:agent <${w}>`));
172
+ rm.groups.forEach(g => parts.push(`acl:agentGroup <${g}>`));
173
+ turtle += ' ' + parts.join(';\n ') + '.\n\n';
174
+ }
175
+ }
176
+ return turtle;
177
+ }
178
+
179
+ export function adaptInheritedAcl(inheritedTurtle, parentUrl, resourceUrl) {
180
+ const blocks = inheritedTurtle.split(/\n\s*\n/);
181
+ const kept = blocks.filter(b => {
182
+ const t = b.trim();
183
+ if (!t) return false;
184
+ if (/^@(prefix|base)\b/.test(t)) return true;
185
+ return /\bacl:default\b/.test(b);
186
+ });
187
+ let out = kept.join('\n\n');
188
+ const escaped = parentUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
189
+ out = out.replace(new RegExp(`<${escaped}>`, 'g'), `<${resourceUrl}>`);
190
+ return out;
191
+ }
192
+
193
+ // ── Component ─────────────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Web Access Control (WAC) editor.
197
+ *
198
+ * Displays and edits the ACL for a Solid resource. Renders a form
199
+ * subtab (role-based) and an RDF subtab (raw Turtle).
200
+ *
201
+ * @class SolWac
202
+ * @extends HTMLElement
203
+ * @attr {string} source - resource URL whose ACL should be loaded
204
+ * @property {Function} fetchFn - authenticated fetch (defaults to window.fetch)
205
+ * @fires sol-wac-save - detail: { aclUrl }; ACL saved
206
+ * @fires sol-wac-error - detail: { phase, error }; load or save failed
207
+ * @fires sol-status - detail: { message, type }
208
+ */
209
+ class SolWac extends HTMLElement {
210
+ static get observedAttributes() { return ['source']; }
211
+
212
+ constructor() {
213
+ super();
214
+ this._fetchFn = null;
215
+ this._aclUrl = null;
216
+ this._turtle = '';
217
+ this._model = null;
218
+ this._isContainer = false;
219
+ this._inherited = null;
220
+ this._inheritedFrom = null;
221
+ this._rendered = false;
222
+ }
223
+
224
+ get source() { return this.getAttribute('source') || ''; }
225
+ set source(v) { if (v) this.setAttribute('source', v); else this.removeAttribute('source'); }
226
+
227
+ get fetchFn() { return this._fetchFn || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null); }
228
+ set fetchFn(fn) { this._fetchFn = fn; if (this._rendered) this.load(); }
229
+
230
+ connectedCallback() {
231
+ ensureDocStyle(this.getRootNode(), 'sol-wac-styles', WAC_CSS);
232
+ if (this._rendered) return;
233
+ this._rendered = true;
234
+ if (this.source) this.load();
235
+ }
236
+
237
+ attributeChangedCallback(name, _old, _new) {
238
+ if (name === 'source' && this._rendered) this.load();
239
+ }
240
+
241
+ async load() {
242
+ const url = this.source;
243
+ if (!url) return;
244
+ this._isContainer = url.endsWith('/');
245
+ this.innerHTML = '<div class="acl-banner">Loading permissions…</div>';
246
+
247
+ let perms;
248
+ try {
249
+ perms = await getPermissions(url, this.fetchFn);
250
+ } catch (e) {
251
+ this.innerHTML = `<div class="acl-error">Failed to load ACL: ${e.message}</div>`;
252
+ this._emit('sol-wac-error', { phase: 'load', error: e });
253
+ return;
254
+ }
255
+
256
+ this._aclUrl = perms.aclUrl;
257
+ this._inherited = perms.inherited || null;
258
+ this._inheritedFrom = perms.inheritedFrom || null;
259
+
260
+ if (perms.own) {
261
+ this._turtle = perms.own;
262
+ } else if (perms.inherited) {
263
+ this._turtle = adaptInheritedAcl(perms.inherited, perms.inheritedFrom, url) || perms.inherited;
264
+ } else {
265
+ this._turtle = '';
266
+ }
267
+
268
+ this._model = authsToRoleModel(this._turtle ? parseAcl(this._turtle, url) : []);
269
+ this._render();
270
+ }
271
+
272
+ async save() {
273
+ if (!this._aclUrl) return;
274
+ if (this._model) this._turtle = roleModelToTurtle(this._model, this.source);
275
+ try {
276
+ const resp = await this.fetchFn(this._aclUrl, {
277
+ method: 'PUT',
278
+ headers: { 'Content-Type': 'text/turtle' },
279
+ body: this._turtle,
280
+ });
281
+ if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
282
+ this._emit('sol-wac-save', { aclUrl: this._aclUrl });
283
+ this._emit('sol-status', { message: 'ACL saved.', type: 'success' });
284
+ this._inherited = null;
285
+ this._inheritedFrom = null;
286
+ this._renderBanner();
287
+ } catch (e) {
288
+ this._emit('sol-wac-error', { phase: 'save', error: e });
289
+ this._emit('sol-status', { message: `ACL save failed: ${e.message}`, type: 'error' });
290
+ }
291
+ }
292
+
293
+ // ── Rendering ───────────────────────────────────────────────────────
294
+
295
+ _render() {
296
+ this.innerHTML = '';
297
+
298
+ this._bannerEl = document.createElement('div');
299
+ this._renderBanner();
300
+ this.appendChild(this._bannerEl);
301
+
302
+ const subtabs = document.createElement('sol-tabs');
303
+ subtabs.setAttribute('variant', 'sub');
304
+ subtabs.tabs = [
305
+ { name: 'Form', render: (body) => this._renderForm(body) },
306
+ { name: 'RDF', render: (body) => this._renderRdf(body) },
307
+ ];
308
+ this.appendChild(subtabs);
309
+
310
+ // sol-tabs renders lazily; dispatch once it's ready
311
+ queueMicrotask(() => {
312
+ subtabs.switchTab('Form');
313
+ const bar = subtabs.querySelector(':scope > .sol-tabs-bar');
314
+ if (bar && !bar.querySelector('.acl-save-btn')) {
315
+ const saveBtn = document.createElement('button');
316
+ saveBtn.className = 'sol-btn sol-btn-sm sol-btn-primary acl-save-btn';
317
+ saveBtn.textContent = 'Save ACL';
318
+ saveBtn.onclick = () => this.save();
319
+ bar.appendChild(saveBtn);
320
+ }
321
+ });
322
+
323
+ }
324
+
325
+ _renderBanner() {
326
+ if (!this._bannerEl) return;
327
+ if (this._inherited && this._inheritedFrom) {
328
+ this._bannerEl.className = 'acl-banner';
329
+ this._bannerEl.textContent =
330
+ `Inheriting permissions from ${this._inheritedFrom}. Save to create a resource-specific ACL.`;
331
+ this._bannerEl.style.display = '';
332
+ } else {
333
+ this._bannerEl.textContent = '';
334
+ this._bannerEl.style.display = 'none';
335
+ }
336
+ }
337
+
338
+ _renderForm(body) {
339
+ body.innerHTML = '';
340
+ body.appendChild(this._buildRoleForm(this._model, this._isContainer, () => {
341
+ this._turtle = roleModelToTurtle(this._model, this.source);
342
+ }));
343
+ }
344
+
345
+ _renderRdf(body) {
346
+ body.innerHTML = '';
347
+ const ta = document.createElement('textarea');
348
+ ta.className = 'acl-rdf-editor';
349
+ ta.spellcheck = false;
350
+ ta.value = this._turtle;
351
+ ta.addEventListener('input', () => {
352
+ this._turtle = ta.value;
353
+ // Re-parse so the form view reflects manual edits on next switch.
354
+ try {
355
+ this._model = authsToRoleModel(parseAcl(ta.value, this.source));
356
+ } catch (_) { /* leave model as-is on parse error */ }
357
+ });
358
+ body.appendChild(ta);
359
+ }
360
+
361
+ _buildRoleForm(model, isContainer, onChange) {
362
+ const wrap = document.createElement('div');
363
+ wrap.className = 'acl-role-form';
364
+
365
+ ROLES.forEach(role => {
366
+ const rm = model[role.key];
367
+ const row = document.createElement('div');
368
+ row.className = 'acl-role-row';
369
+
370
+ const nameEl = document.createElement('span');
371
+ nameEl.className = 'acl-role-name';
372
+ nameEl.textContent = role.label;
373
+
374
+ const select = document.createElement('select');
375
+ select.className = 'acl-grant-select';
376
+ GRANT_OPTIONS.forEach(opt => {
377
+ const o = document.createElement('option');
378
+ o.value = opt.value; o.textContent = opt.label;
379
+ select.appendChild(o);
380
+ });
381
+ select.value = rm.grant;
382
+
383
+ const countBadge = document.createElement('span');
384
+ countBadge.className = 'acl-specific-count';
385
+ const updateCount = () => {
386
+ const c = rm.webids.length + rm.groups.length;
387
+ countBadge.textContent = c;
388
+ countBadge.style.display = c > 0 ? '' : 'none';
389
+ };
390
+ updateCount();
391
+
392
+ const panel = this._buildSpecificPanel(rm, onChange, updateCount);
393
+ const showPanel = () => { panel.style.display = ''; };
394
+ const hidePanel = () => { panel.style.display = 'none'; };
395
+ if (rm.grant === 'specific') showPanel(); else hidePanel();
396
+
397
+ select.addEventListener('change', () => {
398
+ rm.grant = select.value;
399
+ if (select.value === 'specific') showPanel(); else hidePanel();
400
+ onChange();
401
+ });
402
+
403
+ row.append(nameEl, select, countBadge);
404
+ wrap.appendChild(row);
405
+ wrap.appendChild(panel);
406
+ });
407
+
408
+ if (isContainer) {
409
+ const cbWrap = document.createElement('label');
410
+ cbWrap.className = 'acl-default-wrap acl-default-global';
411
+ const cb = document.createElement('input');
412
+ cb.type = 'checkbox';
413
+ cb.checked = ROLES.some(r => model[r.key].applyToContents);
414
+ cb.onchange = () => {
415
+ ROLES.forEach(r => { model[r.key].applyToContents = cb.checked; });
416
+ onChange();
417
+ };
418
+ cbWrap.appendChild(cb);
419
+ cbWrap.appendChild(document.createTextNode('\u00A0Apply to folder contents (acl:default)'));
420
+ wrap.appendChild(cbWrap);
421
+ }
422
+ return wrap;
423
+ }
424
+
425
+ _buildSpecificPanel(rm, onChange, onUpdate) {
426
+ const panel = document.createElement('div');
427
+ panel.className = 'acl-specific-panel';
428
+ panel.innerHTML = `
429
+ <div class="acl-section-label">WebIDs (one per line):</div>
430
+ <textarea class="acl-agents-input" rows="3" placeholder="https://example.solidcommunity.net/profile/card#me"></textarea>
431
+ <div class="acl-section-label">vCard group URLs (one per line):</div>
432
+ <textarea class="acl-agents-input" rows="2" placeholder="https://example.solidcommunity.net/contacts/Group/friends#this"></textarea>`;
433
+
434
+ const [webidTA, groupTA] = panel.querySelectorAll('textarea');
435
+ webidTA.value = rm.webids.join('\n');
436
+ groupTA.value = rm.groups.join('\n');
437
+
438
+ const sync = () => {
439
+ rm.webids = webidTA.value.split('\n').map(s => s.trim()).filter(Boolean);
440
+ rm.groups = groupTA.value.split('\n').map(s => s.trim()).filter(Boolean);
441
+ onChange();
442
+ if (onUpdate) onUpdate();
443
+ };
444
+ webidTA.addEventListener('input', sync);
445
+ groupTA.addEventListener('input', sync);
446
+ return panel;
447
+ }
448
+
449
+ _emit(type, detail) {
450
+ this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail }));
451
+ }
452
+ }
453
+
454
+ define('sol-wac', SolWac);
455
+ export { SolWac };
456
+ export default SolWac;