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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Solid Web Components
2
+
3
+ ## This is a Work In Progress!
4
+
5
+ See the <a href="https://solidos.github.io/sol-components/">help pages</a> for details.
6
+
7
+ (c) 2023-2026, Jeff Zucker, may be freely distributed under an MIT or Apache license.
@@ -0,0 +1,27 @@
1
+ // core/activate.js — run a capability's behavior over every element bearing one
2
+ // of its attributes, now and as elements mount. This is what makes a capability
3
+ // attribute (e.g. data-from-query) work on ANY element, component or not: the
4
+ // behavior is injected by a DOM walk, not implemented by the element's class.
5
+ //
6
+ // activate('[data-from-query]', (el) => …); // called once per matching element
7
+
8
+ /**
9
+ * @param {string} selector — CSS selector for the capability's attribute
10
+ * @param {(el: Element) => void} fn — wiring run once per matching element
11
+ * @returns {() => void} stop the observer
12
+ */
13
+ export function activate(selector, fn) {
14
+ if (typeof document === 'undefined') return () => {};
15
+ const seen = new WeakSet();
16
+ const scan = () => {
17
+ for (const el of document.querySelectorAll(selector)) {
18
+ if (seen.has(el)) continue;
19
+ seen.add(el);
20
+ try { fn(el); } catch (e) { console.error('[sol-loader] activator error for', selector, e); }
21
+ }
22
+ };
23
+ scan();
24
+ const mo = new MutationObserver(scan);
25
+ mo.observe(document.documentElement || document, { childList: true, subtree: true });
26
+ return () => mo.disconnect();
27
+ }
package/core/adopt.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Style helpers for Constructable Stylesheets + adoptedStyleSheets.
3
+ *
4
+ * `sheetFrom(css)` returns a `CSSStyleSheet` on evergreen browsers, or
5
+ * `null` when the constructor is unavailable (e.g. Jest/node env). Callers
6
+ * should keep the raw `CSS` string export alongside the sheet so they can
7
+ * fall back to a `<style>` tag when `sheet` is null.
8
+ *
9
+ * `adopt(root, { sheet, css, extra })` wires a shadow root (or document)
10
+ * with the baseline module sheet (+ any extras). If no sheet is available
11
+ * it appends `<style>${css}</style>` into the root instead.
12
+ */
13
+
14
+ let _supports = null;
15
+ function supports() {
16
+ if (_supports !== null) return _supports;
17
+ try {
18
+ const s = new CSSStyleSheet();
19
+ _supports = typeof s.replaceSync === 'function';
20
+ } catch {
21
+ _supports = false;
22
+ }
23
+ return _supports;
24
+ }
25
+
26
+ export function sheetFrom(css) {
27
+ if (!supports()) return null;
28
+ const s = new CSSStyleSheet();
29
+ s.replaceSync(css);
30
+ return s;
31
+ }
32
+
33
+ // Adopt a CSSStyleSheet into `root` (ShadowRoot or Document). When sheets
34
+ // aren't supported, falls back to inserting a <style> with the given css.
35
+ export function adopt(root, { sheet, css, extra = [] } = {}) {
36
+ const host = root.adoptedStyleSheets !== undefined ? root : null;
37
+ if (host && sheet) {
38
+ const sheets = [sheet];
39
+ const strings = [];
40
+ for (const e of extra) {
41
+ if (e instanceof CSSStyleSheet) sheets.push(e);
42
+ else if (typeof e === 'string') strings.push(e);
43
+ }
44
+ host.adoptedStyleSheets = [...host.adoptedStyleSheets, ...sheets];
45
+ for (const s of strings) appendStyle(root, s);
46
+ return;
47
+ }
48
+ // Fallback path: inline <style> for baseline + extras.
49
+ if (css) appendStyle(root, css);
50
+ for (const e of extra) {
51
+ if (typeof e === 'string') appendStyle(root, e);
52
+ }
53
+ }
54
+
55
+ function appendStyle(root, css) {
56
+ const el = document.createElement('style');
57
+ el.textContent = css;
58
+ root.appendChild(el);
59
+ }
60
+
61
+ // Ensure a named stylesheet exists in the given document/shadow-root head
62
+ // exactly once. Useful for light-DOM components.
63
+ export function ensureDocStyle(root, id, css) {
64
+ const target = root.nodeType === 11 ? root : (root.ownerDocument || document);
65
+ if (!target) return;
66
+ if (target.getElementById?.(id)) return;
67
+ const el = document.createElement('style');
68
+ el.id = id;
69
+ el.textContent = css;
70
+ (target.head || target).appendChild(el);
71
+ }
@@ -0,0 +1,73 @@
1
+ export function originOf(url) {
2
+ return url.match(/^https?:\/\/[^/]+/)?.[0] || '';
3
+ }
4
+
5
+ export function baseDomain(host) {
6
+ const p = host.split('.');
7
+ return p.length >= 2 ? p.slice(-2).join('.') : host;
8
+ }
9
+
10
+ export function sessionCoversOrigin(session, origin) {
11
+ if (!session.info?.isLoggedIn) return false;
12
+ try {
13
+ const rHost = new URL(origin).host;
14
+ const rBase = baseDomain(rHost);
15
+ for (const ref of [session.info.issuer, session.info.webId]) {
16
+ if (!ref) continue;
17
+ const h = new URL(ref).host;
18
+ if (rHost === h || rHost.endsWith('.' + h) || baseDomain(h) === rBase) return true;
19
+ }
20
+ } catch {}
21
+ return false;
22
+ }
23
+
24
+ export function isNoAuth(url, noAuthConfig) {
25
+ try {
26
+ const origin = new URL(url).origin;
27
+ if (!noAuthConfig) return false;
28
+ return Array.isArray(noAuthConfig) ? noAuthConfig.includes(origin) : origin === noAuthConfig;
29
+ } catch { return false; }
30
+ }
31
+
32
+ export function getSessionFor(sessions, url, tag, noAuthConfig) {
33
+ if (!url || isNoAuth(url, noAuthConfig)) return null;
34
+ const own = sessions.get(tag);
35
+ if (own?.info?.isLoggedIn) return own;
36
+ const origin = originOf(url);
37
+ for (const [, s] of sessions) {
38
+ if (sessionCoversOrigin(s, origin)) return s;
39
+ }
40
+ return own || null;
41
+ }
42
+
43
+ export function makeFetchFor(sessions, url, tag, noAuthConfig, defaultFetch) {
44
+ if (isNoAuth(url, noAuthConfig)) return defaultFetch;
45
+ const session = tag
46
+ ? getSessionFor(sessions, url, tag, noAuthConfig)
47
+ : (() => {
48
+ const origin = originOf(url);
49
+ for (const [, s] of sessions) {
50
+ if (sessionCoversOrigin(s, origin)) return s;
51
+ }
52
+ return null;
53
+ })();
54
+ if (!session?.fetch) return defaultFetch;
55
+ return (input, init) => session.fetch(input, init);
56
+ }
57
+
58
+ export function isLoggedInFor(sessions, url, tag, noAuthConfig) {
59
+ if (!url || isNoAuth(url, noAuthConfig)) return true;
60
+ const s = getSessionFor(sessions, url, tag, noAuthConfig);
61
+ return s?.info?.isLoggedIn ?? false;
62
+ }
63
+
64
+ export function getWebId(sessions, tag = 'default') {
65
+ return sessions.get(tag)?.info?.webId || null;
66
+ }
67
+
68
+ export function getFirstLoggedIn(sessions) {
69
+ for (const s of sessions.values()) {
70
+ if (s.info?.isLoggedIn) return s;
71
+ }
72
+ return null;
73
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * core/auth-fetch.js — page-wide authenticated fetch lookup.
3
+ *
4
+ * Components that need to fetch resources (sol-query for SPARQL endpoints,
5
+ * sol-include for documents, the Comunica adapter, …) call getAuthFetch(url)
6
+ * to obtain a fetch function. If a logged-in <sol-login> is on the page,
7
+ * its session.fetch is returned; otherwise the global fetch is.
8
+ *
9
+ * The component-explicit `login` attribute used by sol-pod / sol-pod-ops /
10
+ * sol-wac still wins — pass `opts.element` here to plumb that through.
11
+ *
12
+ * Lookup is light-DOM only: <sol-login> hidden inside another shadow root
13
+ * isn't auto-discovered. That's by design — cross-shadow auth has to be
14
+ * explicit because the host component can't safely guess the user's intent.
15
+ */
16
+
17
+ /**
18
+ * Return a fetch function appropriate for `url`. Always returns a usable
19
+ * fetch (never null) — callers can use the result without null-checking.
20
+ *
21
+ * @param {string} url — the URL the caller is about to fetch
22
+ * @param {object} [opts]
23
+ * @param {Element} [opts.element] — explicit sol-login element (overrides lookup)
24
+ * @param {string} [opts.tag] — session tag; defaults to the targeted
25
+ * element's `side`, else 'default'
26
+ * @returns {(input: RequestInfo, init?: RequestInit) => Promise<Response>}
27
+ */
28
+ export function getAuthFetch(url, opts = {}) {
29
+ const login = opts.element || findFirstSolLogin();
30
+ // Sessions are keyed by tag — a <sol-login>'s `side`. An explicitly
31
+ // targeted element selects its own session by that side; auto-discovered
32
+ // or side-less logins use the 'default' tag.
33
+ const tag = opts.tag
34
+ || (opts.element && typeof opts.element.getAttribute === 'function'
35
+ && opts.element.getAttribute('side'))
36
+ || 'default';
37
+ if (login && typeof login.fetchFor === 'function') {
38
+ try {
39
+ const f = login.fetchFor(url, tag);
40
+ if (typeof f === 'function') return f;
41
+ } catch { /* fall through to adopted / global fetch */ }
42
+ }
43
+ // No <sol-login> (or it declined): a host may have adopted a foreign
44
+ // authenticated fetch via SolidWebComponents.adoptFetch (e.g. PodOS's
45
+ // authenticatedFetch). Prefer it over the unauthenticated global fetch.
46
+ const adopted = (typeof window !== 'undefined') && window.SolidWebComponents?.adoptedFetch;
47
+ if (typeof adopted === 'function') return adopted;
48
+ // `globalThis.fetch` may be missing in some Node test environments —
49
+ // return undefined so callers fall back to their own default (most use
50
+ // `fetchFn = globalThis.fetch` as the parameter default).
51
+ return typeof globalThis.fetch === 'function' ? globalThis.fetch.bind(globalThis) : undefined;
52
+ }
53
+
54
+ /**
55
+ * Find the first <sol-login> in the document, light-DOM only.
56
+ * Returns null if none is present (or in non-browser environments).
57
+ */
58
+ function findFirstSolLogin() {
59
+ if (typeof document === 'undefined') return null;
60
+ return document.querySelector('sol-login');
61
+ }
62
+
63
+ /* ── solFetch: auto-prompt fetch wrapper ──────────────────────────────
64
+ *
65
+ * solFetch(url, opts) wraps fetch and, when the response indicates auth
66
+ * is required, dispatches `sol-auth-needed` on document with a Promise
67
+ * resolver in the detail. A listener (typically <sol-login>) runs the
68
+ * login UI and resolves the promise; solFetch then retries once.
69
+ *
70
+ * Event contract (public API):
71
+ *
72
+ * document.addEventListener('sol-auth-needed', (e) => {
73
+ * const { url, response, resolve, reject } = e.detail;
74
+ * // Run login UI; call resolve(true) when authed, resolve(false)
75
+ * // to give up, reject(err) on error.
76
+ * });
77
+ *
78
+ * When no `<sol-login>` is mounted, solFetch falls through to a plain
79
+ * fetch (so swc widgets work in unauthed contexts). When AuthManager
80
+ * already has a covering session, the first request is authenticated
81
+ * and `sol-auth-needed` is never fired.
82
+ *
83
+ * Frame integration: a frame (dk, etc.) just mounts `<sol-login>` once
84
+ * — the listener is attached automatically by sol-login's
85
+ * connectedCallback. The frame supplies `default-issuer` via
86
+ * `<sol-default default-issuer="…">` or on the sol-login element
87
+ * itself; sol-login uses it as the auto-login target.
88
+ */
89
+
90
+ const AUTH_NEEDED_EVENT = 'sol-auth-needed';
91
+ const AUTH_PROMPT_TIMEOUT_MS = 5 * 60 * 1000;
92
+
93
+ function getAuthManager() {
94
+ if (typeof window === 'undefined') return null;
95
+ return window.SolidWebComponents?.AuthManager?.shared || null;
96
+ }
97
+
98
+ function hasLoginListener() {
99
+ if (typeof document === 'undefined') return false;
100
+ return !!document.querySelector('sol-login');
101
+ }
102
+
103
+ /** 401 → prompt always. 403 → prompt only when no session is active
104
+ * (with an active session, 403 means "logged in but not authorized",
105
+ * re-login won't help, so surface the response as-is). */
106
+ function shouldPrompt(response, am) {
107
+ if (response.status === 401) return true;
108
+ if (response.status === 403) {
109
+ if (!am) return true;
110
+ const anyLoggedIn = [...am.sessions.values()].some(s => s.info?.isLoggedIn);
111
+ return !anyLoggedIn;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ function awaitAuth(url, response, side) {
117
+ return new Promise((resolve, reject) => {
118
+ let settled = false;
119
+ const finish = (ok) => { if (!settled) { settled = true; resolve(!!ok); } };
120
+ const fail = (err) => { if (!settled) { settled = true; reject(err); } };
121
+ document.dispatchEvent(new CustomEvent(AUTH_NEEDED_EVENT, {
122
+ bubbles: false, composed: false,
123
+ detail: { url, response, side, resolve: finish, reject: fail },
124
+ }));
125
+ setTimeout(() => finish(false), AUTH_PROMPT_TIMEOUT_MS);
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Authenticated fetch with auto-prompt on 401 (and unauthenticated 403).
131
+ * @param {string|URL|Request} url
132
+ * @param {RequestInit} [opts]
133
+ * @returns {Promise<Response>}
134
+ */
135
+ export async function solFetch(url, opts) {
136
+ const am = getAuthManager();
137
+ const tag = opts?.authTag;
138
+ const baseFetch = am ? am.fetchFor(url, tag) : (typeof fetch !== 'undefined' ? fetch : null);
139
+ if (!baseFetch) throw new Error('solFetch: no fetch implementation available');
140
+
141
+ const response = await baseFetch(url, opts);
142
+ if (!shouldPrompt(response, am)) return response;
143
+ if (!hasLoginListener()) return response;
144
+
145
+ const ok = await awaitAuth(url, response, tag);
146
+ if (!ok) return response;
147
+
148
+ const retryFetch = (am ?? getAuthManager())?.fetchFor(url, tag) || baseFetch;
149
+ return retryFetch(url, opts);
150
+ }
151
+
152
+ /** Event name listeners subscribe to. Re-exported so callers needn't
153
+ * string-match. */
154
+ export const SOL_AUTH_NEEDED = AUTH_NEEDED_EVENT;
@@ -0,0 +1,110 @@
1
+ // Shared mount-into-target helper used by:
2
+ // - core/rdf-render.js renderComponentItem (menu / tabs item rendering)
3
+ // - web/sol-button.js (declarative launcher in chrome)
4
+ //
5
+ // Both place a custom-element instance inside a per-named wrapper
6
+ // (`<div data-menu-item="<name>" data-keep-alive="true|false">`) under
7
+ // a CSS-selector-addressed `target` element, so multiple consumers can
8
+ // coexist in the same display area and play nicely with keep-alive.
9
+
10
+ /**
11
+ * Locate the wrapper a prior mount left in `target` for the given name.
12
+ * Returns null when none exists.
13
+ * @param {HTMLElement} target
14
+ * @param {string} name
15
+ * @returns {HTMLElement | null}
16
+ */
17
+ export function findItemWrapper(target, name) {
18
+ if (!target) return null;
19
+ const esc = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(name) : name;
20
+ return target.querySelector(`:scope > [data-menu-item="${esc}"]`);
21
+ }
22
+
23
+ /**
24
+ * Hide every other named wrapper inside `target`. The mount model is
25
+ * "always-persistent tabs" — clicking a menu/button just brings its
26
+ * own wrapper to the foreground and parks the others. Components are
27
+ * never torn down on nav-away, so their internal state (login
28
+ * sessions, scroll position, in-flight fetches, open accordion
29
+ * panels, etc.) survives across menu switches.
30
+ */
31
+ export function pruneSiblings(target, activeName) {
32
+ if (!target) return;
33
+ const wraps = target.querySelectorAll(':scope > [data-menu-item]');
34
+ for (const w of wraps) {
35
+ if (w.dataset.menuItem === activeName) continue;
36
+ w.hidden = true;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Mount (or re-show) a component inside `target`, wrapped for
42
+ * coexistence with siblings.
43
+ *
44
+ * Two modes:
45
+ * - **Persistent tab** (default, `replace` falsy): each
46
+ * `(target, name)` pair is created once and reused — clicking
47
+ * back to the same name just unhides the existing wrapper, so
48
+ * internal state (login, scroll, in-flight fetches) survives.
49
+ * - **Shared / overwrite tab** (`replace: true`): the wrapper for
50
+ * `name` is kept across activations but its component is torn
51
+ * down and rebuilt with the latest attrs on every call. Useful
52
+ * for a single "scratch" pane that multiple sources write into
53
+ * (e.g. external menu links, ad-hoc launchers).
54
+ *
55
+ * @param {object} o
56
+ * @param {HTMLElement} o.target Where to mount (typically a region pane).
57
+ * @param {string} o.name Wrapper id (data-menu-item value).
58
+ * @param {string} o.tag Custom-element tag of the component.
59
+ * @param {Iterable<[string, string]>} [o.attrs] Attributes to set on
60
+ * the new component. Re-applied each call in
61
+ * replace mode; ignored when reusing in
62
+ * persistent mode.
63
+ * @param {string} [o.embedClass] Optional CSS class added to the
64
+ * mounted component (e.g. 'sol-menu-embed').
65
+ * @param {boolean} [o.replace] When true, rebuild the inner
66
+ * component on every call (the wrapper itself
67
+ * persists; only its contents are swapped).
68
+ * @returns {HTMLElement} the wrapper (existing or freshly created).
69
+ */
70
+ export function mountInTarget({ target, name, tag, attrs, embedClass, replace }) {
71
+ if (!target || !tag) return null;
72
+
73
+ let wrap = findItemWrapper(target, name);
74
+ if (wrap && !replace) {
75
+ pruneSiblings(target, name);
76
+ wrap.hidden = false;
77
+ fireTabActivate(target, name);
78
+ return wrap;
79
+ }
80
+
81
+ if (wrap) {
82
+ // Replace mode: rebuild contents but keep the wrapper itself.
83
+ wrap.innerHTML = '';
84
+ wrap.hidden = false;
85
+ } else {
86
+ wrap = document.createElement('div');
87
+ wrap.dataset.menuItem = name;
88
+ target.appendChild(wrap);
89
+ }
90
+ if (replace) wrap.dataset.replace = 'true';
91
+
92
+ const el = document.createElement(tag);
93
+ if (attrs) for (const [k, v] of attrs) el.setAttribute(k, v);
94
+ if (embedClass) el.classList.add(embedClass);
95
+ wrap.appendChild(el);
96
+
97
+ pruneSiblings(target, name);
98
+ fireTabActivate(target, name);
99
+ return wrap;
100
+ }
101
+
102
+ // Bubble + composed event so menus / buttons elsewhere on the page
103
+ // can sync their active-state visuals without each consumer wiring
104
+ // its own listener on every possible target.
105
+ function fireTabActivate(target, name) {
106
+ target.dispatchEvent(new CustomEvent('sol-tab-activate', {
107
+ bubbles: true, composed: true,
108
+ detail: { name, target },
109
+ }));
110
+ }
@@ -0,0 +1,48 @@
1
+ // Shared programmatic defaults — values components fall back to when
2
+ // their own attribute isn't set and no RDF source PropertyValue
3
+ // supplies it. Lives in a singleton <sol-default> element in the host
4
+ // page (see ../web/sol-default.js).
5
+ //
6
+ // CSS-driven knobs (theme, font-size) belong on :root as custom
7
+ // properties, not here. This module is for JS-side values like the
8
+ // CORS proxy URL.
9
+
10
+ import { register as registerService } from './services.js';
11
+
12
+ /**
13
+ * Read the current value of a named default. Returns the matching
14
+ * attribute on the first <sol-default> element in the document, or
15
+ * null if no element exists or the attribute isn't set.
16
+ *
17
+ * @param {string} name
18
+ * @returns {string|null}
19
+ */
20
+ export function getDefault(name) {
21
+ const el = document.querySelector('sol-default');
22
+ if (!el) return null;
23
+ const v = el.getAttribute(name);
24
+ return v == null ? null : v;
25
+ }
26
+
27
+ /**
28
+ * Subscribe to changes on <sol-default>. The handler is invoked with
29
+ * (name, newValue, oldValue) for each attribute change. Returns an
30
+ * unsubscribe function — call it on disconnect to remove the listener.
31
+ *
32
+ * The event is dispatched by sol-default's attributeChangedCallback
33
+ * and bubbles up to document, so this works regardless of where the
34
+ * <sol-default> element sits in the tree.
35
+ *
36
+ * @param {(name: string, newValue: string|null, oldValue: string|null) => void} handler
37
+ * @returns {() => void}
38
+ */
39
+ export function onDefaultChange(handler) {
40
+ const fn = (e) => handler(e.detail.name, e.detail.newValue, e.detail.oldValue);
41
+ document.addEventListener('sol-default-change', fn);
42
+ return () => document.removeEventListener('sol-default-change', fn);
43
+ }
44
+
45
+ // Publish shared config as the `defaults` host-service so any component reaches
46
+ // it via window.SolidWebComponents.defaults — no import required. Registered
47
+ // unconditionally; the getters simply return null when no <sol-default> exists.
48
+ registerService('defaults', { get: getDefault, onChange: onDefaultChange });
package/core/define.js ADDED
@@ -0,0 +1,15 @@
1
+ // Idempotent wrapper around customElements.define. Prevents a throw when the
2
+ // same tag is registered twice — e.g. when a page loads both the all-in-one
3
+ // bundle and a per-component UMD build, or when a module is re-evaluated by
4
+ // a hot-reloader.
5
+ export function define(name, klass) {
6
+ if (typeof customElements === 'undefined') return;
7
+ const existing = customElements.get(name);
8
+ if (existing) {
9
+ if (existing !== klass && !window.__SolSuppressDefineWarn) {
10
+ console.warn(`[sol-components] <${name}> already registered; keeping the existing definition.`);
11
+ }
12
+ return;
13
+ }
14
+ customElements.define(name, klass);
15
+ }
@@ -0,0 +1,166 @@
1
+ // Region resolver + mounter for launchers (menu items, sol-button).
2
+ //
3
+ // The model: content/structure lives in Turtle, all display lives in HTML.
4
+ // A launcher decides three things, all resolved here from the DOM:
5
+ //
6
+ // where — `region=` on the launcher / a container / <sol-menu> /
7
+ // <sol-default> (nearest wins). Value is a CSS selector (a
8
+ // persistent pane the author placed) OR a keyword
9
+ // (modal | floating | tab | window) that conjures an ephemeral
10
+ // surface with no author-placed element. A Turtle menu item,
11
+ // which has no HTML element, is routed by a host that claims it
12
+ // by id via `data-for`.
13
+ // how — the content element: a component tag, a <sol-include> of a
14
+ // same-origin href, or an <iframe> for an external href.
15
+ // lifetime — per pane: component → keep-alive wrapper, doc/iframe → replace.
16
+ //
17
+ // No RDF display vocabulary is involved; surfaces survive only as the HTML
18
+ // keyword values above.
19
+
20
+ import { mountInTarget } from './component-mount.js';
21
+
22
+ const SURFACE_KEYWORDS = new Set(['modal', 'floating', 'tab', 'window']);
23
+
24
+ /** True for a cross-origin http(s) URL. */
25
+ export function isExternal(href) {
26
+ if (!href) return false;
27
+ try {
28
+ const u = new URL(href, document.baseURI);
29
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
30
+ return u.origin !== location.origin;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /** Content element for a link href: same-origin → trusted sol-include
37
+ * (keep-alive); external → iframe (replace). */
38
+ export function contentForHref(href) {
39
+ return isExternal(href)
40
+ ? { tag: 'iframe', attrs: [['src', href]], replace: true }
41
+ : { tag: 'sol-include', attrs: [['source', href], ['endpoint', href], ['trusted', 'true']], replace: false };
42
+ }
43
+
44
+ function buildElement(tag, attrs = [], embedClass = null) {
45
+ const el = document.createElement(tag);
46
+ for (const [k, v] of attrs) el.setAttribute(k, v);
47
+ if (embedClass) el.classList.add(embedClass);
48
+ return el;
49
+ }
50
+
51
+ function safeQuery(sel) {
52
+ try { return document.querySelector(sel); } catch { return null; }
53
+ }
54
+
55
+ // Find a region/host element that claims this item id via `data-for`
56
+ // (space-separated list of ids).
57
+ function claimedRegion(id) {
58
+ if (!id) return null;
59
+ for (const el of document.querySelectorAll('[data-for]')) {
60
+ if ((el.getAttribute('data-for') || '').split(/\s+/).includes(id)) return el;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Resolve where a launcher's content should go.
67
+ * @returns {{kind:'element', element:Element} | {kind:'modal'|'floating'|'tab'|'window'} | {kind:null}}
68
+ */
69
+ export function resolveRegion(launcher, id, fallbackEl = null) {
70
+ const claimed = claimedRegion(id);
71
+ if (claimed) return { kind: 'element', element: claimed };
72
+
73
+ let value = null;
74
+ const scope = launcher && launcher.closest ? launcher.closest('[region]') : null;
75
+ if (scope) value = scope.getAttribute('region');
76
+ if (!value) {
77
+ const def = document.querySelector('sol-default[region]');
78
+ if (def) value = def.getAttribute('region');
79
+ }
80
+
81
+ if (!value) return fallbackEl ? { kind: 'element', element: fallbackEl } : { kind: null };
82
+
83
+ const kw = value.toLowerCase();
84
+ if (SURFACE_KEYWORDS.has(kw)) return { kind: kw };
85
+ const el = safeQuery(value);
86
+ if (el) return { kind: 'element', element: el };
87
+ return fallbackEl ? { kind: 'element', element: fallbackEl } : { kind: null };
88
+ }
89
+
90
+ /**
91
+ * Place a launcher's content into its resolved region.
92
+ *
93
+ * @param {object} o
94
+ * @param {Element|null} o.launcher element initiating (for region cascade)
95
+ * @param {string|null} o.id item id (for data-for routing)
96
+ * @param {string} o.name pane wrapper / surface title
97
+ * @param {string|null} o.tag content element tag
98
+ * @param {Array<[string,string]>} [o.attrs]
99
+ * @param {string|null} o.href used for tab/window surfaces
100
+ * @param {string|null} o.contents literal HTML
101
+ * @param {boolean} [o.replace] pane lifetime (true = rebuild each time)
102
+ * @param {string|null} [o.embedClass]
103
+ * @param {Element|null} [o.fallbackEl] where to mount if no region resolves
104
+ * @param {(tag:string)=>void} [o.ensure] lazy-load hook for conjured hosts
105
+ * @returns {Element|Window|null}
106
+ */
107
+ export function displayItem({
108
+ launcher = null, id = null, name = '', tag = null, attrs = [],
109
+ href = null, contents = null, replace = false, embedClass = null,
110
+ fallbackEl = null, ensure = null,
111
+ }) {
112
+ const region = resolveRegion(launcher, id, fallbackEl);
113
+ const mount = (host) => {
114
+ if (contents != null) host.innerHTML = contents;
115
+ else if (tag) host.appendChild(buildElement(tag, attrs, embedClass));
116
+ };
117
+
118
+ switch (region.kind) {
119
+ case 'tab': return href ? window.open(href, '_blank', '') : null;
120
+ case 'window': return href ? window.open(href, '_blank', 'width=900,height=700,menubar=no,toolbar=no') : null;
121
+ case 'modal': return conjure('sol-modal', name, mount, ensure);
122
+ case 'floating': return conjure('sol-window', name, mount, ensure);
123
+ case 'element': return mountInElement(region.element, { tag, attrs, name, replace, contents, embedClass, mount });
124
+ default: return null;
125
+ }
126
+ }
127
+
128
+ function mountInElement(element, { tag, attrs, name, replace, contents, embedClass, mount }) {
129
+ const t = (element.tagName || '').toLowerCase();
130
+
131
+ if (t === 'sol-modal') {
132
+ element.handler = (body) => mount(body);
133
+ element.open();
134
+ return element;
135
+ }
136
+ if (t === 'sol-window') {
137
+ mount(element.body || element);
138
+ return element;
139
+ }
140
+ // A pane: literal HTML replaces its content; otherwise a keep-alive (or
141
+ // replace) named wrapper via the shared mount helper.
142
+ if (contents != null) { element.innerHTML = contents; return element; }
143
+ if (!tag) return null;
144
+ return mountInTarget({ target: element, name, tag, attrs, embedClass, replace });
145
+ }
146
+
147
+ // Conjure an ephemeral host (sol-modal / sol-window) with no author element.
148
+ // Deferred via whenDefined so open()/body exist once the module upgrades.
149
+ function conjure(hostTag, name, mount, ensure) {
150
+ if (ensure) ensure(hostTag);
151
+ const run = () => {
152
+ const host = document.createElement(hostTag);
153
+ if (name) host.setAttribute('title', name);
154
+ if (hostTag === 'sol-modal') {
155
+ host.handler = (body) => mount(body);
156
+ host.open();
157
+ } else {
158
+ document.body.appendChild(host);
159
+ mount(host.body || host);
160
+ }
161
+ return host;
162
+ };
163
+ if (customElements.get(hostTag)) return run();
164
+ if (typeof customElements !== 'undefined') customElements.whenDefined(hostTag).then(run);
165
+ return null;
166
+ }