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,402 @@
1
+ /**
2
+ * <sol-search> — multi-engine search form, popup or inline.
3
+ *
4
+ * Two layouts, chosen by the `view` attribute:
5
+ *
6
+ * view="button" — (default) an icon trigger that opens a floating
7
+ * panel on click; the panel positions itself flush
8
+ * against the right edge of the trigger. Best for
9
+ * headers / toolbars where space is tight.
10
+ * view="inline" — the search field, Go button, and engine radios are
11
+ * rendered directly with no toggle. Best when you
12
+ * already have a dedicated strip for search.
13
+ *
14
+ * Engine sources, in order of precedence:
15
+ * `source` — URL of a Turtle file with a `schema:ItemList` whose
16
+ * `schema:itemListElement`s are `hydra:IriTemplate`
17
+ * entries (`hydra:template` = RFC 6570 search URL with
18
+ * `{query}`, `dct:title` = display name,
19
+ * `schema:position` = sort order). Parsed through
20
+ * feed-fetch.js#parseEngineList, sharing the single
21
+ * rdflib instance from core/rdf.js.
22
+ * `engines` — JSON array of {id,label,url} on the attribute itself.
23
+ * built-ins — a sensible default list (DuckDuckGo / Google /
24
+ * Wikipedia / prefix.cc / LOV / Etymology / YouTube /
25
+ * Wayback).
26
+ *
27
+ * Submitting opens the result in a shared "reader" window (the same
28
+ * window object is re-used across submissions so it never spawns a new
29
+ * tab per search).
30
+ *
31
+ * Attributes:
32
+ * view "button" | "inline" (default: button)
33
+ * source "file.ttl#TopicName" — RDF engines list
34
+ * engines JSON array of {id,label,url}
35
+ * default-engine id (or url) of the engine that starts selected
36
+ * placeholder input placeholder (default: "Search…")
37
+ *
38
+ * @element sol-search
39
+ *
40
+ * @example
41
+ * <sol-search></sol-search>
42
+ * <sol-search view="inline" default-engine="ddg"></sol-search>
43
+ * <sol-search view="inline" source="data/search-engines.ttl#SearchEngines"></sol-search>
44
+ */
45
+ import { adopt } from '../core/adopt.js';
46
+ import { define } from '../core/define.js';
47
+ import { CSS as SEARCH_CSS, sheet as SEARCH_SHEET } from './styles/sol-search-css.js';
48
+ import { parseEngineList } from './utils/feed-fetch.js';
49
+ import { attachEditorSelfGear } from '../core/editor-self.js';
50
+
51
+ /** Sensible defaults; callers can override via `engines` or `source`. */
52
+ const DEFAULT_ENGINES = [
53
+ { id: 'ddg', label: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=' },
54
+ { id: 'g', label: 'Google', url: 'https://www.google.com/search?q=' },
55
+ { id: 'wp', label: 'Wikipedia', url: 'https://en.wikipedia.org/w/index.php?search=' },
56
+ { id: 'prefix', label: 'prefix.cc', url: 'https://prefix.cc/' },
57
+ { id: 'lov', label: 'LOV', url: 'https://lov.linkeddata.es/dataset/lov/terms?q=' },
58
+ { id: 'ety', label: 'Etymology', url: 'https://www.etymonline.com/search?q=' },
59
+ { id: 'yt', label: 'YouTube', url: 'https://www.youtube.com/results?search_query=' },
60
+ { id: 'wayback', label: 'Wayback', url: 'https://web.archive.org/web/*/' },
61
+ ];
62
+
63
+ /** Shared "reader" window — kept across submissions so the same off-canvas
64
+ * window is re-used instead of spawning a fresh tab every time. Browsers
65
+ * clear a window's name on cross-origin navigation, so we cannot look it
66
+ * up by name later; we keep the handle that window.open() returns. */
67
+ let readerWindow = null;
68
+
69
+ /** A 1024×640 window flush against the right edge, vertically centred. */
70
+ function readerFeatures() {
71
+ const w = 1024, h = 640;
72
+ const left = Math.max(0, window.screen.availWidth - w);
73
+ const top = Math.max(0, Math.round((window.screen.availHeight - h) / 2));
74
+ return `width=${w},height=${h},left=${left},top=${top}`;
75
+ }
76
+
77
+ /** Open `url` in the shared reader window; returns true when handled. */
78
+ function openInReader(url) {
79
+ if (!url) return false;
80
+ if (readerWindow && !readerWindow.closed) {
81
+ readerWindow.location.href = url;
82
+ readerWindow.focus();
83
+ return true;
84
+ }
85
+ readerWindow = window.open(url, 'sol-search-reader', readerFeatures());
86
+ if (readerWindow) { readerWindow.focus(); return true; }
87
+ return false;
88
+ }
89
+
90
+ /** HTML-escape a value for safe interpolation into innerHTML. */
91
+ function esc(s) {
92
+ return String(s).replace(/[&<>"']/g,
93
+ c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));
94
+ }
95
+
96
+ /** Derive a stable engine id from its label (slug-ish, lower-case). */
97
+ function slugify(label) {
98
+ return String(label).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
99
+ }
100
+
101
+ /** Build the result URL for an engine + query. RDF-sourced engines
102
+ * carry `template` with a `{query}` placeholder (RFC 6570-ish);
103
+ * built-in / JSON-attr engines carry `url` as a prefix that the query
104
+ * is appended to. Both paths URI-encode the query. */
105
+ function expandQuery(eng, q) {
106
+ const encoded = encodeURIComponent(q);
107
+ if (eng?.template) return eng.template.replace(/\{query\}/g, encoded);
108
+ if (eng?.url) return eng.url + encoded;
109
+ return 'https://duckduckgo.com/?q=' + encoded;
110
+ }
111
+
112
+ // RDF parsing is delegated to feed-fetch.js#parseSourceList — the same
113
+ // utility sol-feed uses for its bookmark/SKOS source files, which routes
114
+ // through core/rdf.js (the single rdflib instance the suite shares).
115
+
116
+ /**
117
+ * Multi-engine search component.
118
+ *
119
+ * @class SolSearch
120
+ * @extends HTMLElement
121
+ */
122
+ class SolSearch extends HTMLElement {
123
+ static get observedAttributes() {
124
+ return ['view', 'source', 'engines', 'default-engine', 'placeholder'];
125
+ }
126
+
127
+ /** SHACL shape describing the engines list. Discovery via
128
+ * &lt;sol-settings&gt; picks this up; the shape uses a forward
129
+ * `schema:itemListElement` so the editor lists each
130
+ * `hydra:IriTemplate` that's a member of the currently-edited
131
+ * `schema:ItemList`. */
132
+ static get shape() {
133
+ return new URL('../shapes/search-engines.shacl', import.meta.url).href;
134
+ }
135
+
136
+ constructor() {
137
+ super();
138
+ this.attachShadow({ mode: 'open' });
139
+ this._open = false; // only relevant when view=button
140
+ this._engines = DEFAULT_ENGINES;
141
+ this._view = 'button'; // resolved in connectedCallback
142
+ this._built = false; // true once the shadow tree exists
143
+ }
144
+
145
+ async connectedCallback() {
146
+ // Reset for the re-entry case (view change triggers a rebuild).
147
+ this.shadowRoot.adoptedStyleSheets = [];
148
+ this.shadowRoot.innerHTML = '';
149
+ adopt(this.shadowRoot, { sheet: SEARCH_SHEET, css: SEARCH_CSS });
150
+
151
+ this._view = (this.getAttribute('view') || 'button').toLowerCase();
152
+ // Surface the view on the host so external rules / parts can select on it.
153
+ this.dataset.view = this._view;
154
+
155
+ // The form body is shared between layouts — the only difference is
156
+ // whether it's wrapped in an [open]-toggled panel and preceded by a
157
+ // trigger button. The .engines-line wrapper keeps the engines row
158
+ // as a single flex item below the input + Go row; the engines
159
+ // inside flex-wrap onto a second (or third) row as space allows.
160
+ const formHTML = `
161
+ <form class="form" part="form">
162
+ <div class="row">
163
+ <input class="q" type="search" name="q" autocomplete="off" part="input">
164
+ <button class="go" type="submit" part="submit">Go</button>
165
+ </div>
166
+ <div class="engines-line">
167
+ <div class="engines" aria-label="Search engine"></div>
168
+ </div>
169
+ </form>
170
+ `;
171
+
172
+ if (this._view === 'inline') {
173
+ // Render the form directly in the shadow root — no trigger, no
174
+ // floating panel, no document-level listeners. Engines simply
175
+ // wrap onto subsequent rows when the list outgrows the
176
+ // viewport.
177
+ const wrap = document.createElement('div');
178
+ wrap.innerHTML = formHTML;
179
+ while (wrap.firstChild) this.shadowRoot.appendChild(wrap.firstChild);
180
+ } else {
181
+ const wrap = document.createElement('div');
182
+ wrap.innerHTML = `
183
+ <button class="icon" type="button" part="trigger"
184
+ aria-haspopup="dialog" aria-expanded="false" title="Search">⌕</button>
185
+ <div class="panel" role="dialog" aria-modal="false" part="panel">
186
+ ${formHTML}
187
+ <span class="sr-only">Press Escape to close</span>
188
+ </div>
189
+ `;
190
+ while (wrap.firstChild) this.shadowRoot.appendChild(wrap.firstChild);
191
+ this.$btn = this.shadowRoot.querySelector('button.icon');
192
+ this.$panel = this.shadowRoot.querySelector('.panel');
193
+
194
+ this._onDocPointerDown = (e) => {
195
+ const path = e.composedPath?.() ?? [];
196
+ if (!path.includes(this)) this.close();
197
+ };
198
+ this._onDocKeyDown = (e) => { if (e.key === 'Escape') this.close(); };
199
+
200
+ this.$btn.addEventListener('click', () => this.toggle());
201
+ }
202
+
203
+ this.$form = this.shadowRoot.querySelector('form.form');
204
+ this.$q = this.shadowRoot.querySelector('input.q');
205
+ this.$engines = this.shadowRoot.querySelector('.engines');
206
+
207
+ this._loadEngines();
208
+ this._renderEngines();
209
+ this._applyPlaceholder();
210
+ this._built = true;
211
+
212
+ this.$form.addEventListener('submit', (e) => {
213
+ e.preventDefault();
214
+ this._doSearch();
215
+ });
216
+
217
+ // If a `source` is set, fetch + parse it as a schema:ItemList of
218
+ // hydra:IriTemplate engines (see feed-fetch.js#parseEngineList).
219
+ // The default / engines-attr list is shown in the meantime so the
220
+ // UI is never blank, and stays put if the source request fails.
221
+ const source = this.getAttribute('source');
222
+ if (source) {
223
+ try {
224
+ const list = await parseEngineList(source);
225
+ if (list && list.length) {
226
+ this._engines = list.map((item, i) => ({
227
+ id: item.id || slugify(item.label) || `e${i}`,
228
+ label: item.label,
229
+ template: item.template,
230
+ }));
231
+ this._loadEngines();
232
+ this._renderEngines();
233
+ }
234
+ } catch (err) {
235
+ // Source failed — leave the default / engines-attr list in
236
+ // place; surface to the console so the page author sees it.
237
+ console.warn(`[sol-search] source ${source}: ${err.message}`);
238
+ }
239
+ }
240
+
241
+ if (this.hasAttribute('editor-self')) attachEditorSelfGear(this);
242
+ }
243
+
244
+ disconnectedCallback() {
245
+ if (this._onDocPointerDown) {
246
+ document.removeEventListener('pointerdown', this._onDocPointerDown, { capture: true });
247
+ }
248
+ if (this._onDocKeyDown) {
249
+ document.removeEventListener('keydown', this._onDocKeyDown);
250
+ }
251
+ this._built = false;
252
+ }
253
+
254
+ /**
255
+ * Re-read `source` and rebuild the engines panel. Public hook used by
256
+ * external editors (e.g. dk-settings) after the engines TTL changes.
257
+ * sol-search loads its source inline in connectedCallback, so reload
258
+ * tears down and reconnects to walk the same path.
259
+ */
260
+ async reload() {
261
+ this.disconnectedCallback();
262
+ await this.connectedCallback();
263
+ }
264
+
265
+ attributeChangedCallback(name) {
266
+ // Static-attribute changes fire after the element is in the DOM but
267
+ // before our first connectedCallback runs (isConnected is already
268
+ // true). Bailing on `_built` is the correct gate — `connectedCallback`
269
+ // applies the attributes in order anyway, and we re-apply on later
270
+ // changes.
271
+ if (!this._built) return;
272
+
273
+ // Switching `view` requires a full rebuild — disconnect listeners,
274
+ // clear the root, and re-run connectedCallback (which resets the
275
+ // shadow root and re-loads the source if one is set).
276
+ if (name === 'view') {
277
+ this.disconnectedCallback();
278
+ this.connectedCallback();
279
+ return;
280
+ }
281
+ this._loadEngines();
282
+ this._renderEngines();
283
+ this._applyPlaceholder();
284
+ }
285
+
286
+ _loadEngines() {
287
+ // `engines` attribute beats the built-in defaults; an RDF `source`
288
+ // result (resolved in connectedCallback's tail) beats both.
289
+ const enginesAttr = this.getAttribute('engines');
290
+ if (enginesAttr) {
291
+ try {
292
+ const parsed = JSON.parse(enginesAttr);
293
+ if (Array.isArray(parsed) && parsed.length) this._engines = parsed;
294
+ } catch { /* leave whatever was there on bad JSON */ }
295
+ } else if (!this.getAttribute('source')) {
296
+ this._engines = DEFAULT_ENGINES;
297
+ }
298
+ const def = this.getAttribute('default-engine');
299
+ // Allow matching by id or by URL prefix — convenient when defaults
300
+ // are RDF-sourced and ids are slugified labels.
301
+ this._defaultEngine = def
302
+ || this._engines.find(e => /duckduckgo/i.test(e.label))?.id
303
+ || this._engines[0]?.id
304
+ || 'ddg';
305
+ }
306
+
307
+ _applyPlaceholder() {
308
+ if (!this.$q) return;
309
+ this.$q.setAttribute('placeholder', this.getAttribute('placeholder') || 'Search…');
310
+ }
311
+
312
+ _renderEngines() {
313
+ if (!this.$engines) return;
314
+ // Unique radio-group name per render so repeated re-renders don't
315
+ // accidentally cross-link with stale inputs in the same shadow root.
316
+ const name = `engine-${Math.random().toString(36).slice(2, 8)}`;
317
+ // Radios go directly in .engines — the container is a flex-wrap
318
+ // row, so a list that's too wide for one line continues on a
319
+ // second (or third) row beneath. No track wrapper, no carousel.
320
+ this.$engines.innerHTML = this._engines.map(eng => `
321
+ <label class="engine">
322
+ <input type="radio" name="${name}" value="${esc(eng.id)}">
323
+ <span>${esc(eng.label ?? eng.id)}</span>
324
+ </label>
325
+ `).join('');
326
+
327
+ const radios = [...this.shadowRoot.querySelectorAll(`input[name="${name}"]`)];
328
+ const pick = radios.find(r => r.value === this._defaultEngine) || radios[0];
329
+ if (pick) pick.checked = true;
330
+ }
331
+
332
+ /* ── view: button (popup) controls ─────────────────────────────────── */
333
+
334
+ toggle() {
335
+ if (this._view !== 'button') return;
336
+ this._open ? this.close() : this.openAtButton();
337
+ }
338
+
339
+ openAtButton() {
340
+ if (this._view !== 'button') return;
341
+ this._open = true;
342
+ this.$btn.setAttribute('aria-expanded', 'true');
343
+ this.$panel.setAttribute('open', '');
344
+
345
+ // Measure once visible, then position so the panel's right edge lines
346
+ // up with the trigger's right edge (drops down-and-left). Clamps into
347
+ // the viewport with a 10px margin.
348
+ this.$panel.style.left = '0px';
349
+ this.$panel.style.top = '0px';
350
+
351
+ const btn = this.$btn.getBoundingClientRect();
352
+ const panel = this.$panel.getBoundingClientRect();
353
+ const margin = 10;
354
+
355
+ let left = btn.right - panel.width;
356
+ let top = btn.bottom + 4;
357
+ left = Math.max(margin, Math.min(left, window.innerWidth - panel.width - margin));
358
+ top = Math.max(margin, Math.min(top, window.innerHeight - panel.height - margin));
359
+
360
+ this.$panel.style.left = `${left}px`;
361
+ this.$panel.style.top = `${top}px`;
362
+
363
+ document.addEventListener('pointerdown', this._onDocPointerDown, { capture: true });
364
+ document.addEventListener('keydown', this._onDocKeyDown);
365
+ queueMicrotask(() => this.$q.focus());
366
+ }
367
+
368
+ close() {
369
+ if (this._view !== 'button') return;
370
+ if (!this._open) return;
371
+ this._open = false;
372
+ this.$btn.setAttribute('aria-expanded', 'false');
373
+ this.$panel.removeAttribute('open');
374
+ document.removeEventListener('pointerdown', this._onDocPointerDown, { capture: true });
375
+ document.removeEventListener('keydown', this._onDocKeyDown);
376
+ }
377
+
378
+ /* ── shared submit ────────────────────────────────────────────────── */
379
+
380
+ _selectedEngine() {
381
+ const checked = this.shadowRoot.querySelector('input[type="radio"]:checked');
382
+ return this._engines.find(e => e.id === checked?.value) || this._engines[0];
383
+ }
384
+
385
+ _doSearch() {
386
+ const q = (this.$q.value || '').trim();
387
+ if (!q) return;
388
+ const eng = this._selectedEngine();
389
+ const url = expandQuery(eng, q);
390
+ if (!openInReader(url)) {
391
+ // Popup blocked; fall through to a normal new-tab open so the user
392
+ // still gets the search result rather than nothing.
393
+ window.open(url, '_blank', 'noopener,noreferrer');
394
+ }
395
+ if (this._view === 'button') this.close();
396
+ // Inline view: leave the input populated so the user can refine and
397
+ // submit again without retyping.
398
+ }
399
+ }
400
+
401
+ define('sol-search', SolSearch);
402
+ export { SolSearch };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * <sol-settings> — discovery-driven settings page.
3
+ *
4
+ * Walks the current document (crossing into every shadow root) for
5
+ * elements whose custom-element class declares an editor (`static get
6
+ * editor()` or `static get shape()`). For each, builds one accordion
7
+ * panel: summary shows a friendly label, body lazy-mounts the
8
+ * declared editor element on first expand, wired with the host's
9
+ * `source` / `from-rdf` subject. On successful save the host
10
+ * component's `reload()` (if present) is invoked.
11
+ *
12
+ * No configuration: drop a `<sol-settings></sol-settings>` anywhere
13
+ * on the page; widgets elsewhere on the page are picked up
14
+ * automatically. Hosts can render widgets into a keep-alive region pane so
15
+ * they stay mounted (hidden) when the user navigates to the settings page;
16
+ * otherwise discovery only sees widgets currently in the DOM.
17
+ *
18
+ * Attributes:
19
+ * none
20
+ *
21
+ * Methods:
22
+ * refresh() — re-walk and rebuild the accordion if the widget set
23
+ * has changed (signature: tag + subject). Cheap no-op
24
+ * when nothing changed. Use from consumer code when a
25
+ * new editable widget is mounted after sol-settings
26
+ * connected. (Tab activation triggers this automatically
27
+ * via the sol-tab-activate listener.)
28
+ *
29
+ * Events (consumed):
30
+ * sol-form-save — bubbling from any embedded editor; triggers
31
+ * `host.reload()` on the corresponding source widget.
32
+ */
33
+
34
+ import { define } from '../core/define.js';
35
+ import { buildEditorElement, triggerSelfEditor, editPlacement } from '../core/editor.js';
36
+ import { findExtensionPoints } from '../core/extension-points.js';
37
+ import './sol-accordion.js';
38
+
39
+ class SolSettings extends HTMLElement {
40
+ connectedCallback() {
41
+ if (this._wired) return;
42
+ this._wired = true;
43
+ // Defer one microtask so the surrounding DOM (e.g., a sibling
44
+ // keep-alive wrapper that holds the dashboard widgets) is fully
45
+ // attached before discovery walks.
46
+ queueMicrotask(() => this._build());
47
+
48
+ // Re-discover when the editable-component set changes. Generic trigger: a
49
+ // debounced MutationObserver on the whole document — works with any app, no
50
+ // swc-specific navigation needed. `sol-tab-activate` stays as an extra hint
51
+ // for keep-alive tab UIs (harmless if no one fires it). The rebuild only
52
+ // happens when the discovered set actually changed (signature compare).
53
+ this._rebuild = () => {
54
+ if (this.offsetParent === null) return; // we're hidden; ignore
55
+ this._rebuildIfChanged();
56
+ };
57
+ this._mo = new MutationObserver(() => {
58
+ clearTimeout(this._moTimer);
59
+ this._moTimer = setTimeout(this._rebuild, 50);
60
+ });
61
+ this._mo.observe(document.documentElement, { childList: true, subtree: true });
62
+ document.addEventListener('sol-tab-activate', this._rebuild);
63
+ }
64
+
65
+ disconnectedCallback() {
66
+ if (this._mo) { this._mo.disconnect(); this._mo = null; }
67
+ clearTimeout(this._moTimer);
68
+ if (this._rebuild) {
69
+ document.removeEventListener('sol-tab-activate', this._rebuild);
70
+ this._rebuild = null;
71
+ }
72
+ }
73
+
74
+ _build() {
75
+ const widgets = this._discover();
76
+ this._lastSignature = signatureOf(widgets);
77
+ this.innerHTML = '';
78
+ if (!widgets.length) {
79
+ this._empty();
80
+ return;
81
+ }
82
+
83
+ const accordion = document.createElement('sol-accordion');
84
+ accordion.setAttribute('start-closed', '');
85
+ widgets.forEach((w, i) => {
86
+ const panel = document.createElement('div');
87
+ const head = document.createElement('div');
88
+ head.textContent = w.label;
89
+ const body = document.createElement('div');
90
+ body.className = 'sol-settings-slot';
91
+ body.dataset.widgetIdx = String(i);
92
+ panel.append(head, body);
93
+ accordion.appendChild(panel);
94
+ });
95
+ this.appendChild(accordion);
96
+
97
+ // sol-accordion runs synchronously on connect; once it has cloned
98
+ // the author divs into <details>, attach lazy-mount handlers.
99
+ Promise.resolve().then(() => this._wireLazy(accordion, widgets));
100
+ }
101
+
102
+ _rebuildIfChanged() {
103
+ const widgets = this._discover();
104
+ const sig = signatureOf(widgets);
105
+ if (sig === this._lastSignature) return;
106
+ this._build();
107
+ }
108
+
109
+ refresh() { this._rebuildIfChanged(); }
110
+
111
+ _empty() {
112
+ const note = document.createElement('p');
113
+ note.className = 'sol-settings-empty';
114
+ note.textContent = 'No editable widgets found on this page.';
115
+ this.appendChild(note);
116
+ }
117
+
118
+ _wireLazy(accordion, widgets) {
119
+ const detailsList = accordion.querySelectorAll('details');
120
+ detailsList.forEach((det, i) => {
121
+ const widget = widgets[i];
122
+ if (!widget) return;
123
+ const section = det.querySelector('.accordion-content-section');
124
+ if (!section) return;
125
+ let mounted = false;
126
+ const mount = () => {
127
+ if (mounted) return;
128
+ mounted = true;
129
+ section.innerHTML = '';
130
+ // forms:"self" — the component renders its own editor; offer a trigger.
131
+ if (widget.spec && widget.spec.self) {
132
+ const btn = document.createElement('button');
133
+ btn.type = 'button';
134
+ btn.className = 'sol-settings-self-open';
135
+ btn.textContent = `Edit ${widget.label}…`;
136
+ btn.addEventListener('click', () => triggerSelfEditor(widget.el, widget.spec));
137
+ section.appendChild(btn);
138
+ return;
139
+ }
140
+ const editor = buildEditorElement(widget.el, widget.spec);
141
+ if (!editor) {
142
+ section.textContent = 'No editor available.';
143
+ return;
144
+ }
145
+ editor.addEventListener('sol-form-save', () => {
146
+ if (typeof widget.el.reload === 'function') {
147
+ widget.el.reload().catch(() => {});
148
+ }
149
+ });
150
+ section.appendChild(editor);
151
+ };
152
+ if (det.open) mount();
153
+ det.addEventListener('toggle', () => { if (det.open) mount(); });
154
+ });
155
+ }
156
+
157
+ // Editable widgets = every element offering the `edit` extension point. The
158
+ // walk + shadow-crossing + resolution lives in core/extension-points.js; this
159
+ // is just the `edit` case of the general protocol. Opt-out attr stays
160
+ // `data-settings-skip` for back-compat with pages that use it.
161
+ _discover() {
162
+ return findExtensionPoints('edit', { skipAttr: 'data-settings-skip' })
163
+ .filter(({ el }) => el !== this && !this.contains(el))
164
+ // inPlace editors render a gear ON the element itself — sol-settings
165
+ // gathers only the "collected" ones (the default placement).
166
+ .filter(({ el, spec }) => editPlacement(el, spec) === 'collected')
167
+ .map(({ el, spec }) => ({
168
+ el, spec,
169
+ label: el.getAttribute('label') || labelFromTag(el.localName),
170
+ }));
171
+ }
172
+ }
173
+
174
+ // Fallback label when an element has no `label` attribute. Drops the leading
175
+ // vendor-prefix segment (sol-, dk-, my-, …) generically and title-cases the
176
+ // rest — `sol-weather` → "Weather", `my-thing` → "Thing", `sol-dropdown-button`
177
+ // → "Dropdown Button". Any component can override with an explicit `label`.
178
+ function labelFromTag(tag) {
179
+ return tag
180
+ .replace(/^[a-z0-9]+-/, '')
181
+ .split('-')
182
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
183
+ .join(' ');
184
+ }
185
+
186
+ /** Stable identity for a discovered widget set, used to detect when a
187
+ * later re-discovery has actually changed anything. Tag + subject is
188
+ * enough — two instances of the same widget with the same source
189
+ * would render an identical accordion panel. */
190
+ function signatureOf(widgets) {
191
+ return widgets
192
+ .map(w => `${w.el.localName}#${w.el.getAttribute('source') || w.el.getAttribute('from-rdf') || ''}`)
193
+ .sort()
194
+ .join('|');
195
+ }
196
+
197
+ define('sol-settings', SolSettings);
198
+ export { SolSettings };
199
+ export default SolSettings;
@@ -0,0 +1,93 @@
1
+ import { define } from '../core/define.js';
2
+ import { ensureDocStyle } from '../core/adopt.js';
3
+
4
+ function getMashlib() {
5
+ const w = typeof window !== 'undefined' ? window : {};
6
+ const g = typeof globalThis !== 'undefined' ? globalThis : (typeof self !== 'undefined' ? self : w);
7
+ const Mashlib = w.Mashlib || g.Mashlib;
8
+ const SolidLogic = w.SolidLogic || g.SolidLogic;
9
+ const $rdf = w.$rdf || g.$rdf;
10
+ const panes = w.panes || g.panes;
11
+ if (!Mashlib || !panes) return null;
12
+ const initMainPage = Mashlib.initMainPage || Mashlib.default?.initMainPage || Mashlib.default;
13
+ if (!initMainPage) return null;
14
+ return { Mashlib, initMainPage, SolidLogic, $rdf, panes };
15
+ }
16
+
17
+ // Hide mashlib's own header/footer chrome — when sol-solidos is mounted
18
+ // inside a host shell (dk), the host already owns login + help/prefs
19
+ // affordances, so a duplicate login button in mashlib's header is both
20
+ // confusing and broken (popup-mode pod sessions don't reach mashlib's
21
+ // default Inrupt session, so its login button stalls). Mashlib's reads
22
+ // go through rdflib's patched Fetcher → solFetch → sol-auth-needed →
23
+ // the host's <sol-login> chip handles the prompt and retries.
24
+ const HOST_CSS = `
25
+ sol-solidos { display: block; width: 100%; height: 100%; }
26
+ sol-solidos > #PageHeader,
27
+ sol-solidos > #PageFooter { display: none; }
28
+ `;
29
+
30
+ class SolSolidos extends HTMLElement {
31
+ static get observedAttributes() { return ['source']; }
32
+
33
+ constructor() {
34
+ super();
35
+ this._ready = false;
36
+ this._m = null;
37
+ this._outliner = null;
38
+ }
39
+
40
+ connectedCallback() {
41
+ if (this.isConnected && !this._ready) this._init();
42
+ }
43
+
44
+ attributeChangedCallback(name, oldV, newV) {
45
+ if (name === 'source' && oldV !== newV && this._ready) {
46
+ this._goTo(newV);
47
+ }
48
+ }
49
+
50
+ _goTo(uri) {
51
+ if (!uri || !this._outliner) return;
52
+ const subject = this._m.$rdf.sym(uri);
53
+ this._outliner.GotoSubject(subject, true, undefined, true, undefined);
54
+ }
55
+
56
+ _init() {
57
+ const m = getMashlib();
58
+ if (!m) {
59
+ const w = typeof window !== 'undefined' ? window : {};
60
+ const g = typeof globalThis !== 'undefined' ? globalThis : {};
61
+ console.error('[sol-solidos] getMashlib() returned null. window.Mashlib:', w.Mashlib,
62
+ 'globalThis.Mashlib:', g.Mashlib, 'window.panes:', w.panes, 'globalThis.panes:', g.panes);
63
+ this.textContent = 'mashlib not loaded \u2014 add <script src="mashlib.js"> to the page';
64
+ return;
65
+ }
66
+ this._m = m;
67
+ ensureDocStyle(document, 'sol-solidos-style', HOST_CSS);
68
+
69
+ // Build the DOM structure mashlib expects
70
+ this.innerHTML = `
71
+ <header id="PageHeader" role="banner"></header>
72
+ <main id="mainContent" tabindex="-1">
73
+ <div class="TabulatorOutline" id="DummyUUID">
74
+ <table id="outline"></table>
75
+ <div id="GlobalDashboard"></div>
76
+ </div>
77
+ </main>
78
+ <footer id="PageFooter" role="contentinfo"></footer>
79
+ `;
80
+
81
+ const SL = m.SolidLogic?.solidLogicSingleton || m.SolidLogic?.default?.solidLogicSingleton;
82
+ const store = SL?.store;
83
+ const uri = this.getAttribute('source') || window.location.href;
84
+
85
+ this._outliner = m.panes.getOutliner(document);
86
+ m.initMainPage(store, uri);
87
+
88
+ this._ready = true;
89
+ }
90
+ }
91
+
92
+ define('sol-solidos', SolSolidos);
93
+ export default SolSolidos;