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,43 @@
1
+ /**
2
+ * sol-full.js — load every covered component in one tag.
3
+ *
4
+ * Side-effect aggregator: each child module registers its custom element
5
+ * on import. Externals actually imported by the source (rdflib, dompurify,
6
+ * marked) stay bare-specifier and are resolved by the consumer's importmap.
7
+ *
8
+ * Bring-your-own runtime peers (loaded via a `<script>` tag *before* this
9
+ * module so they self-attach to globals the components probe at runtime):
10
+ * - @inrupt/solid-client-authn-browser → `window.solidClientAuthn`
11
+ * (only needed by sol-login)
12
+ * - @comunica/query-sparql → `window.Comunica`
13
+ * (only needed for full SPARQL — LIMIT/OFFSET/ORDER BY/federation —
14
+ * against RDF documents)
15
+ * The corresponding vendored UMDs live in `dist/vendor/`.
16
+ *
17
+ * Usage:
18
+ * <script type="importmap" src="dist/importmap-cdn.json"></script>
19
+ * — or —
20
+ * <script type="importmap" src="dist/importmap-local.json"></script>
21
+ *
22
+ * <script type="module" src="web/sol-full.js"></script>
23
+ */
24
+
25
+ import './sol-menu.js';
26
+ import './menu-from-rdf.js'; // keep sol-menu `from-rdf` working in this bundle
27
+ import './sol-include.js';
28
+ import './sol-query.js';
29
+ import './sol-login.js';
30
+ import './sol-feed.js';
31
+
32
+ // Deliberately NOT in sol-full: sol-weather, sol-time, sol-calendar — chrome
33
+ // widgets with their own heavy deps (e.g. ical). Load them as ESM where
34
+ // needed, e.g. import 'sol-components/sol-weather.js'.
35
+ //
36
+ // TODO: add the rest as their externals get vendored:
37
+ // sol-accordion, sol-modal, sol-tabs, sol-rolodex, sol-live-edit, sol-pod,
38
+ // sol-pod-ops, sol-wac (no extra externals expected — verify and add)
39
+ // sol-form (requires solid-ui — vendor solid-ui first)
40
+ // sol-solidos (requires mashlib)
41
+ // sol-login is in: it only needs `window.solidClientAuthn` (provided by
42
+ // dist/vendor/@inrupt-solid-client-authn-browser.umd.js loaded as a
43
+ // `<script>` tag before this aggregator).
@@ -0,0 +1,303 @@
1
+ /**
2
+ * <sol-gallery> — pure image-grid display (masonry + lightbox).
3
+ *
4
+ * Source-blind: it renders the ImageItem RDF it is handed and emits events. It
5
+ * does NO network, no search, and knows nothing about Commons / Wikidata /
6
+ * files / SKOS / DCAT. A host (see the `sources/` providers) pumps pages of
7
+ * `schema:ImageObject` records in and decides what a selection means.
8
+ *
9
+ * Display contract
10
+ * clear() drop all tiles — a new collection was selected
11
+ * add(store) append one page of schema:ImageObject records ← the seam
12
+ * end() the host signals there are no more pages
13
+ * Events out
14
+ * 'item-opened' {detail:{iri}} a tile's lightbox opened (lazy per-item detail hook)
15
+ * 'load-more' scrolled near the end; the host should pump the next page
16
+ *
17
+ * Records are read with `readImageItems` from the shared contract, so the
18
+ * gallery and every provider agree on the vocab without either re-declaring it.
19
+ *
20
+ * @element sol-gallery
21
+ * @example
22
+ * const g = document.querySelector('sol-gallery');
23
+ * g.addEventListener('load-more', () => pumpNextPage());
24
+ * g.clear(); g.add(pageStore); // …; g.end();
25
+ */
26
+ import { adopt } from '../core/adopt.js';
27
+ import { define } from '../core/define.js';
28
+ import { CSS as GALLERY_CSS, sheet as GALLERY_SHEET } from './styles/sol-gallery-css.js';
29
+ import { readImageItems } from '../sources/contract.js';
30
+
31
+ class SolGallery extends HTMLElement {
32
+ constructor() {
33
+ super();
34
+ this.attachShadow({ mode: 'open' });
35
+ /** @type {Array} flattened, position-ordered items across all pages. */
36
+ this._items = [];
37
+ }
38
+
39
+ connectedCallback() {
40
+ if (this._built) return; // build once; survives re-attach
41
+ this._built = true;
42
+ adopt(this.shadowRoot, { sheet: GALLERY_SHEET, css: GALLERY_CSS });
43
+
44
+ this._main = document.createElement('div');
45
+ this._main.className = 'gallery-main';
46
+ this._status = document.createElement('div');
47
+ this._status.className = 'gallery-status';
48
+ this._status.setAttribute('role', 'status');
49
+ this._status.setAttribute('aria-live', 'polite');
50
+ this._grid = document.createElement('div');
51
+ this._grid.className = 'gallery-grid';
52
+ this._main.append(this._status, this._grid);
53
+ this.shadowRoot.append(this._main);
54
+ this._buildLightbox();
55
+
56
+ this._renderEmpty('Pick a collection to see its images.');
57
+ }
58
+
59
+ disconnectedCallback() {
60
+ if (this._io) { this._io.disconnect(); this._io = null; }
61
+ document.removeEventListener('keydown', this._onKey);
62
+ }
63
+
64
+ setStatus(msg, isError = false) {
65
+ this._status.textContent = msg || '';
66
+ if (isError) this._status.setAttribute('data-error', '');
67
+ else this._status.removeAttribute('data-error');
68
+ }
69
+
70
+ /** Show a centred placeholder line in the grid (cleared by the next page). */
71
+ _renderEmpty(text) {
72
+ const empty = document.createElement('div');
73
+ empty.className = 'gallery-empty';
74
+ empty.textContent = text;
75
+ this._grid.replaceChildren(empty);
76
+ this._emptyEl = empty;
77
+ }
78
+
79
+ /* ── display contract ───────────────────────────────────────────────────── */
80
+
81
+ /** Drop everything for a freshly selected collection; show a loading line. */
82
+ clear() {
83
+ this._items = [];
84
+ this._complete = false;
85
+ this._awaitingPage = false;
86
+ this._removeSentinel();
87
+ this._grid.replaceChildren();
88
+ this._emptyEl = null;
89
+ this.setStatus('Loading images…');
90
+ }
91
+
92
+ /** Append one page (an rdflib store of schema:ImageObject records). */
93
+ add(store) {
94
+ const page = readImageItems(store);
95
+ this._awaitingPage = false;
96
+ if (this._emptyEl) { this._emptyEl.remove(); this._emptyEl = null; }
97
+
98
+ const start = this._items.length;
99
+ this._items = this._items.concat(page);
100
+ for (let i = 0; i < page.length; i++) {
101
+ this._grid.appendChild(this._thumb(this._items[start + i], start + i));
102
+ }
103
+ this.setStatus(this._countLabel());
104
+ this._armSentinel(); // a page arrived → there may be more
105
+ }
106
+
107
+ /** The host has no more pages: remove the sentinel; show empty if needed. */
108
+ end() {
109
+ this._complete = true;
110
+ this._removeSentinel();
111
+ if (!this._items.length) {
112
+ this.setStatus('');
113
+ this._renderEmpty('This collection has no images in its Commons category.');
114
+ } else {
115
+ this.setStatus(this._countLabel());
116
+ }
117
+ }
118
+
119
+ _countLabel() {
120
+ const n = this._items.length;
121
+ return `${n} image${n === 1 ? '' : 's'}`;
122
+ }
123
+
124
+ /* ── lazy paging (the gallery only ASKS; the host fetches) ───────────────── */
125
+
126
+ /** Ensure a sentinel + observer exist so reaching the end emits 'load-more'.
127
+ * A Load-more button is added as a no-IntersectionObserver fallback. */
128
+ _armSentinel() {
129
+ if (this._complete) return;
130
+ if (!this._sentinel) {
131
+ this._sentinel = document.createElement('div');
132
+ this._sentinel.className = 'gallery-sentinel';
133
+ }
134
+ this._grid.appendChild(this._sentinel); // keep it last
135
+
136
+ if ('IntersectionObserver' in window) {
137
+ if (this._io) this._io.disconnect();
138
+ this._io = new IntersectionObserver((entries) => {
139
+ if (entries.some((e) => e.isIntersecting)) this._requestMore();
140
+ }, { root: this._main, rootMargin: '600px' });
141
+ this._io.observe(this._sentinel);
142
+ } else if (!this._moreBtn) {
143
+ this._moreBtn = document.createElement('button');
144
+ this._moreBtn.type = 'button';
145
+ this._moreBtn.className = 'gallery-more';
146
+ this._moreBtn.textContent = 'Load more';
147
+ this._moreBtn.addEventListener('click', () => this._requestMore());
148
+ this._main.appendChild(this._moreBtn);
149
+ }
150
+ }
151
+
152
+ _removeSentinel() {
153
+ if (this._io) { this._io.disconnect(); this._io = null; }
154
+ this._sentinel?.remove();
155
+ this._moreBtn?.remove();
156
+ this._moreBtn = null;
157
+ }
158
+
159
+ /** Ask the host for the next page (once; re-armed when add() lands). */
160
+ _requestMore() {
161
+ if (this._complete || this._awaitingPage) return;
162
+ this._awaitingPage = true;
163
+ if (this._io) { this._io.disconnect(); this._io = null; } // re-armed by add()
164
+ this.dispatchEvent(new CustomEvent('load-more', { bubbles: true, composed: true }));
165
+ }
166
+
167
+ /* ── thumbnails ──────────────────────────────────────────────────────────── */
168
+
169
+ /** One masonry thumbnail button → opens the lightbox at its index. */
170
+ _thumb(item, index) {
171
+ const b = document.createElement('button');
172
+ b.type = 'button';
173
+ b.className = 'gallery-thumb';
174
+ b.dataset.index = String(index);
175
+ b.setAttribute('aria-label', item.caption || 'image');
176
+ const el = document.createElement('img');
177
+ el.src = item.thumb;
178
+ el.alt = item.caption || '';
179
+ el.loading = 'lazy';
180
+ if (item.width && item.height) { el.width = item.width; el.height = item.height; }
181
+ el.addEventListener('error', () => b.remove());
182
+ b.appendChild(el);
183
+ b.addEventListener('click', () => this.openLightbox(index));
184
+ return b;
185
+ }
186
+
187
+ /* ── lightbox ────────────────────────────────────────────────────────────── */
188
+
189
+ _buildLightbox() {
190
+ const lb = document.createElement('div');
191
+ lb.className = 'gallery-lightbox';
192
+ lb.hidden = true;
193
+ lb.setAttribute('role', 'dialog');
194
+ lb.setAttribute('aria-modal', 'true');
195
+
196
+ const img = document.createElement('img');
197
+ const caption = document.createElement('div');
198
+ caption.className = 'gallery-lb-caption';
199
+ const prev = document.createElement('button');
200
+ prev.type = 'button'; prev.className = 'gallery-lb-btn gallery-lb-prev';
201
+ prev.textContent = '‹'; prev.setAttribute('aria-label', 'Previous image');
202
+ const next = document.createElement('button');
203
+ next.type = 'button'; next.className = 'gallery-lb-btn gallery-lb-next';
204
+ next.textContent = '›'; next.setAttribute('aria-label', 'Next image');
205
+ const close = document.createElement('button');
206
+ close.type = 'button'; close.className = 'gallery-lb-close';
207
+ close.textContent = '✕'; close.setAttribute('aria-label', 'Close');
208
+
209
+ lb.append(close, prev, img, next, caption);
210
+ this.shadowRoot.appendChild(lb);
211
+ this._lb = { lb, img, caption, prev, next, close };
212
+
213
+ prev.addEventListener('click', () => this.stepLightbox(-1));
214
+ next.addEventListener('click', () => this.stepLightbox(1));
215
+ close.addEventListener('click', () => this.closeLightbox());
216
+ lb.addEventListener('click', (e) => { if (e.target === lb) this.closeLightbox(); });
217
+ // Click the image to toggle a full-bleed, actual-size (100%) view that pans
218
+ // via scroll; click again (or page / Esc) to return to fit. Only offered
219
+ // when the fit view shows fewer pixels than the image actually has.
220
+ img.addEventListener('click', (e) => { e.stopPropagation(); if (this._canZoom) this.setZoom(!this._lbZoom); });
221
+ img.addEventListener('load', () => this._refreshZoomable());
222
+
223
+ this._onKey = (e) => {
224
+ if (this._lb.lb.hidden) return;
225
+ if (e.key === 'Escape') this.closeLightbox();
226
+ else if (e.key === 'ArrowLeft') this.stepLightbox(-1);
227
+ else if (e.key === 'ArrowRight') this.stepLightbox(1);
228
+ };
229
+ document.addEventListener('keydown', this._onKey);
230
+ }
231
+
232
+ openLightbox(index) {
233
+ this._lbIndex = index;
234
+ this.showLightboxImage();
235
+ this._lb.lb.hidden = false;
236
+ this._lb.close.focus();
237
+ // Lazy per-item detail hook: a host may listen and enrich (e.g. Wikidata).
238
+ const it = this._items[index];
239
+ if (it) this.dispatchEvent(new CustomEvent('item-opened', {
240
+ detail: { iri: it.iri }, bubbles: true, composed: true,
241
+ }));
242
+ }
243
+
244
+ stepLightbox(delta) {
245
+ const imgs = this._items;
246
+ if (!imgs.length) return;
247
+ this._lbIndex = (this._lbIndex + delta + imgs.length) % imgs.length;
248
+ this.showLightboxImage();
249
+ }
250
+
251
+ /** Decide whether the current (fit) image can usefully zoom: only when its
252
+ * natural pixel width exceeds the fit-rendered width. Toggles the no-zoom
253
+ * class (hides the zoom-in cursor) and gates the click handler. */
254
+ _refreshZoomable() {
255
+ const { lb, img } = this._lb;
256
+ const fitW = this._lbZoom ? 0 : img.getBoundingClientRect().width;
257
+ this._canZoom = !this._lbZoom && img.naturalWidth > Math.ceil(fitW) + 1;
258
+ lb.classList.toggle('no-zoom', !this._canZoom);
259
+ }
260
+
261
+ setZoom(on) {
262
+ this._lbZoom = on;
263
+ const { lb, img } = this._lb;
264
+ lb.classList.toggle('zoomed', on);
265
+ if (on) {
266
+ requestAnimationFrame(() => {
267
+ lb.scrollLeft = Math.max(0, (img.scrollWidth - lb.clientWidth) / 2);
268
+ lb.scrollTop = Math.max(0, (img.scrollHeight - lb.clientHeight) / 2);
269
+ });
270
+ }
271
+ }
272
+
273
+ showLightboxImage() {
274
+ const it = this._items[this._lbIndex];
275
+ if (!it) return;
276
+ this.setZoom(false); // each image starts fit-to-screen
277
+ this._lb.img.src = it.full || it.thumb;
278
+ this._lb.img.alt = it.caption || '';
279
+ const bits = [it.caption, it.author, it.license].filter(Boolean);
280
+ this._lb.caption.textContent = bits.join(' · ');
281
+ if (it.detailUrl) {
282
+ this._lb.caption.append(' ');
283
+ const link = document.createElement('a');
284
+ link.href = it.detailUrl; link.target = '_blank'; link.rel = 'noopener';
285
+ link.textContent = 'View on Commons ↗';
286
+ this._lb.caption.appendChild(link);
287
+ }
288
+ const multi = this._items.length > 1;
289
+ this._lb.prev.style.display = multi ? '' : 'none';
290
+ this._lb.next.style.display = multi ? '' : 'none';
291
+ this._refreshZoomable(); // default to no-zoom until the image loads
292
+ }
293
+
294
+ closeLightbox() {
295
+ this.setZoom(false);
296
+ this._lb.lb.hidden = true;
297
+ this._lb.img.removeAttribute('src');
298
+ }
299
+ }
300
+
301
+ define('sol-gallery', SolGallery);
302
+
303
+ export { SolGallery };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * <sol-include> — Fetch and display remote content inline.
3
+ *
4
+ * Supports HTML, Markdown, and plain text. Content is sanitized with
5
+ * DOMPurify by default. An optional CSS selector filters to a section
6
+ * of the fetched document.
7
+ *
8
+ * @element sol-include
9
+ * @attr {string} source - URL of the resource to fetch (required)
10
+ * @attr {string} if-logged-in - alternate source used when a user is logged in
11
+ * (a live Solid session OR the window.SolidKitchen dev flag,
12
+ * treated identically); falls back to `source` otherwise.
13
+ * Re-evaluates on sol-login / sol-logout.
14
+ * @attr {string} selector - CSS selector — show only matching elements
15
+ * @attr {boolean} raw - show source text verbatim without rendering
16
+ * @attr {boolean} trusted - skip DOMPurify sanitization
17
+ *
18
+ * @example
19
+ * <sol-include source="https://example.org/readme.md"></sol-include>
20
+ * <sol-include source="page.html" selector="article"></sol-include>
21
+ * <sol-include source="guest.html" if-logged-in="owner.html"></sol-include>
22
+ */
23
+ import { sanitizeHtml } from '../core/utils.js';
24
+ import { define } from '../core/define.js';
25
+ import { adopt } from '../core/adopt.js';
26
+ import { fetchIncludeContent, filterWithSelector } from '../core/include-core.js';
27
+ import { getAuthFetch } from '../core/auth-fetch.js';
28
+ import { CSS as INCLUDE_CSS, sheet as includeSheet } from './styles/sol-include-css.js';
29
+
30
+ function browserContainer(html) {
31
+ const div = document.createElement('div');
32
+ div.innerHTML = html;
33
+ return div;
34
+ }
35
+
36
+ /**
37
+ * Is there an authenticated user? True for a live Solid session (a logged-in
38
+ * <sol-login>, or any logged-in session in the shared AuthManager), and for the
39
+ * dev "kitchen" flag — declared as `solid-kitchen` on <sol-default> (or the
40
+ * legacy `window.SolidKitchen` global) — treated as exactly equivalent to being
41
+ * logged in. Used by the `if-logged-in` source switch.
42
+ */
43
+ function isLoggedIn() {
44
+ try { if (typeof window !== 'undefined' && window.SolidKitchen === true) return true; } catch { /* ignore */ }
45
+ if (typeof document === 'undefined') return false;
46
+ if (document.querySelector('sol-default')?.hasAttribute('solid-kitchen')) return true;
47
+ const login = document.querySelector('sol-login');
48
+ if (login && login.isLoggedIn) return true;
49
+ try {
50
+ const am = window.SolidWebComponents?.AuthManager?.shared;
51
+ if (am && [...am.sessions.values()].some((s) => s.info?.isLoggedIn)) return true;
52
+ } catch { /* ignore */ }
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Fetch and display remote content inline.
58
+ *
59
+ * Supports HTML, Markdown, and plain text. Content is sanitized with
60
+ * DOMPurify by default.
61
+ *
62
+ * @class SolInclude
63
+ * @extends HTMLElement
64
+ * @attr {string} source - URL to fetch (required)
65
+ * @attr {string} selector - CSS selector — show only matching elements
66
+ * @attr {boolean} raw - show source text verbatim
67
+ * @attr {boolean} trusted - skip DOMPurify sanitization, render into LIGHT
68
+ * DOM (via a shadow <slot>) so host CSS reaches the content
69
+ *
70
+ * Layout: `:host` is a flex column with `flex: 1 1 auto; min-height: 0` and
71
+ * the `.si-content` wrapper gets the same treatment (matched in both the
72
+ * shadow and trusted-slotted modes), so a definite parent height propagates
73
+ * down to components placed inside (sol-pod, sol-menu, etc.). They can then
74
+ * fill and scroll on their own — sol-include itself never scrolls.
75
+ */
76
+ class SolInclude extends HTMLElement {
77
+ static get observedAttributes() {
78
+ return ['source', 'if-logged-in', 'selector', 'raw', 'trusted'];
79
+ }
80
+
81
+ constructor() {
82
+ super();
83
+ this.attachShadow({ mode: 'open' });
84
+ this._abortCtl = null;
85
+ this._authListener = null;
86
+ }
87
+
88
+ connectedCallback() {
89
+ if (!this.isConnected) return;
90
+ // Only watch auth state when an `if-logged-in` alternate is declared, so the
91
+ // displayed source follows login / logout.
92
+ if (this.hasAttribute('if-logged-in') && !this._authListener) {
93
+ this._authListener = () => { if (this.isConnected) this._load(); };
94
+ document.addEventListener('sol-login', this._authListener);
95
+ document.addEventListener('sol-logout', this._authListener);
96
+ }
97
+ this._load();
98
+ }
99
+ attributeChangedCallback(n, oldV, newV) { if (oldV !== newV && this.isConnected) this._load(); }
100
+ disconnectedCallback() {
101
+ this._abortCtl?.abort(); this._abortCtl = null;
102
+ if (this._authListener) {
103
+ document.removeEventListener('sol-login', this._authListener);
104
+ document.removeEventListener('sol-logout', this._authListener);
105
+ this._authListener = null;
106
+ }
107
+ }
108
+
109
+ /** The source to show now: the `if-logged-in` alternate when authenticated,
110
+ * otherwise the plain `source`. */
111
+ _effectiveSource() {
112
+ const alt = this.getAttribute('if-logged-in');
113
+ if (alt && isLoggedIn()) return alt;
114
+ return this.getAttribute('source');
115
+ }
116
+
117
+ // ── Main load ─────────────────────────────────────────────────────────────────
118
+ async _load() {
119
+ const source = this._effectiveSource();
120
+ const selector = this.getAttribute('selector') || '';
121
+ const raw = this.hasAttribute('raw');
122
+ const trusted = this.hasAttribute('trusted');
123
+
124
+ if (!source) { this._show('error', 'No source provided'); return; }
125
+
126
+ // Cancel any in-flight load triggered by an earlier attribute change.
127
+ this._abortCtl?.abort();
128
+ const ctl = new AbortController();
129
+ this._abortCtl = ctl;
130
+
131
+ this._show('loading', 'Loading…');
132
+
133
+ // `login` attribute (CSS selector for a sol-login) overrides auto-discovery.
134
+ const loginSel = this.getAttribute('login');
135
+ const loginEl = loginSel ? document.querySelector(loginSel) : null;
136
+ const fetchFn = getAuthFetch(source, { element: loginEl || undefined });
137
+
138
+ try {
139
+ // When a selector is present, defer sanitization so the selector can
140
+ // match attributes (e.g. RDFa typeof/rel) that DOMPurify would strip.
141
+ const { type, content } = await fetchIncludeContent(source, {
142
+ raw,
143
+ trusted: trusted || !!selector,
144
+ sanitize: sanitizeHtml,
145
+ signal: ctl.signal,
146
+ fetchFn,
147
+ });
148
+
149
+ if (ctl.signal.aborted) return;
150
+
151
+ if (type === 'raw') {
152
+ this._showRaw(content);
153
+ return;
154
+ }
155
+
156
+ if (selector) {
157
+ const filtered = filterWithSelector(content, selector, browserContainer);
158
+ if (ctl.signal.aborted) return;
159
+ if (filtered === null) {
160
+ this._show('empty', 'No elements matched selector');
161
+ return;
162
+ }
163
+ this._showHtml(trusted ? filtered : await sanitizeHtml(filtered));
164
+ } else {
165
+ this._showHtml(content);
166
+ }
167
+ } catch (err) {
168
+ if (err.name === 'AbortError' || ctl.signal.aborted) return;
169
+ this._show('error', err.message);
170
+ this._fireError('load', err.message);
171
+ }
172
+ }
173
+
174
+ _fireError(kind, message) {
175
+ this.dispatchEvent(new CustomEvent('sol-error', {
176
+ bubbles: true, composed: true,
177
+ detail: { source: 'sol-include', kind, message },
178
+ }));
179
+ }
180
+
181
+ // ── Display helpers ───────────────────────────────────────────────────────────
182
+ _showHtml(html) {
183
+ this._initShadow();
184
+ const div = document.createElement('div');
185
+ div.className = 'si-content';
186
+ div.innerHTML = html;
187
+ // When the consumer marked the source as `trusted`, render into
188
+ // LIGHT DOM rather than shadow DOM. Trusted content is, by
189
+ // definition, page-authored — it should inherit the host's
190
+ // stylesheets so .my-class rules apply, custom-element CSS lands,
191
+ // etc. The shadow root contains a <slot> in trusted mode so the
192
+ // light-DOM child still gets projected into the host's box.
193
+ // Untrusted content stays in shadow for isolation.
194
+ if (this.hasAttribute('trusted')) {
195
+ this.appendChild(div);
196
+ } else {
197
+ this.shadowRoot.appendChild(div);
198
+ }
199
+ }
200
+
201
+ // Remove any prior trusted-mode light-DOM render so reload swaps
202
+ // cleanly without piling stale content next to the new. Called from
203
+ // _initShadow on every state transition; the trusted append path
204
+ // can assume it starts clean.
205
+ _clearLightContent() {
206
+ for (const child of Array.from(this.children)) {
207
+ if (child.classList?.contains('si-content')) child.remove();
208
+ }
209
+ }
210
+
211
+ _showRaw(text) {
212
+ this._initShadow();
213
+ const pre = document.createElement('pre');
214
+ pre.className = 'si-raw';
215
+ pre.textContent = text;
216
+ this.shadowRoot.appendChild(pre);
217
+ }
218
+
219
+ _show(type, message) {
220
+ this._initShadow();
221
+ const div = document.createElement('div');
222
+ div.className = `si-${type}`;
223
+ if (type === 'error') div.setAttribute('role', 'alert');
224
+ else if (type === 'loading' || type === 'empty') {
225
+ div.setAttribute('role', 'status');
226
+ div.setAttribute('aria-live', 'polite');
227
+ }
228
+ div.textContent = message;
229
+ this.shadowRoot.appendChild(div);
230
+ }
231
+
232
+ // Reset for a fresh render. Drops any prior light-DOM content so
233
+ // reload doesn't double-stack, then resets the shadow root. In
234
+ // `trusted` mode the shadow holds a single <slot> so a subsequent
235
+ // light-DOM append shows through; in untrusted mode there's no
236
+ // slot, so any light children stay hidden as before.
237
+ _initShadow() {
238
+ this._clearLightContent();
239
+ this.shadowRoot.innerHTML = this.hasAttribute('trusted') ? '<slot></slot>' : '';
240
+ this.shadowRoot.adoptedStyleSheets = [];
241
+ adopt(this.shadowRoot, { sheet: includeSheet, css: INCLUDE_CSS });
242
+ }
243
+ }
244
+
245
+ define('sol-include', SolInclude);
246
+ export { SolInclude };