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,465 @@
1
+ /**
2
+ * <sol-calendar> — inline calendar viewer web component.
3
+ *
4
+ * Fetches a public iCalendar (ICS) feed from any provider that exports
5
+ * one — Google Calendar, Apple iCloud, Outlook, Proton Calendar, or a
6
+ * Solid pod — and renders the events as an agenda list that fits
7
+ * whatever container the host page gives it.
8
+ *
9
+ * v1 ships the agenda view only. `mini` (today-only card) and `month`
10
+ * (grid + day popover) are planned and will land as a follow-up; the
11
+ * view dispatch in `connectedCallback` is a switch so adding them is
12
+ * local.
13
+ *
14
+ * Attributes:
15
+ * source One or more ICS URLs (whitespace-separated for >1),
16
+ * **or** `file.ttl#Subject` PropertyValue config. The
17
+ * RDF source may itself declare repeated `"source"`
18
+ * values (multi-calendar / amalgamated view), in
19
+ * which case events from every feed are fetched in
20
+ * parallel and merged into one sorted agenda.
21
+ * provider google | apple | outlook | proton | ics (default: ics)
22
+ * calendar-id For provider="google", the calendar email/id; URL is built
23
+ * via the public-ICS template. Other providers ignore it.
24
+ * view agenda (only value supported in v1)
25
+ * start ISO date YYYY-MM-DD (default: today)
26
+ * window-days Agenda lookahead in days (default: 30)
27
+ * max-events Cap on rendered events (default: 100)
28
+ * proxy CORS proxy pattern — supports `{url}` token or appended
29
+ * time-zone IANA TZ override (default: browser's resolved TZ)
30
+ * locale BCP-47 (default: browser locale)
31
+ * hide-header Boolean — when present, the title + provider strip
32
+ * above the agenda is omitted. Useful when the host page
33
+ * already labels the slot (dashboards, sidebars).
34
+ *
35
+ * The HTML attribute always wins over the same-named PropertyValue in
36
+ * the RDF `source`, matching the `sol-time` / `sol-weather` convention.
37
+ *
38
+ * @element sol-calendar
39
+ *
40
+ * @example
41
+ * <!-- Direct URL -->
42
+ * <sol-calendar
43
+ * source="https://calendar.google.com/calendar/ical/.../public/basic.ics"
44
+ * proxy="http://localhost:3002/proxy?uri="></sol-calendar>
45
+ *
46
+ * <!-- Provider helper builds the URL -->
47
+ * <sol-calendar provider="google" calendar-id="alice@example.org"></sol-calendar>
48
+ *
49
+ * <!-- Pull every setting from a PropertyValue TTL -->
50
+ * <sol-calendar source="data/calendar-settings.ttl#Settings"></sol-calendar>
51
+ */
52
+ import { adopt } from '../core/adopt.js';
53
+ import { define } from '../core/define.js';
54
+ import { CSS as CAL_CSS, sheet as CAL_SHEET } from './styles/sol-calendar-css.js';
55
+ import { getCalendarEvents, getMergedCalendarEvents, buildProviderUrl }
56
+ from './utils/calendar-fetch.js';
57
+ import { loadConfig } from './utils/rdf-config.js';
58
+ import { getDefault, onDefaultChange } from '../core/defaults.js';
59
+ import { attachEditorSelfGear } from '../core/editor-self.js';
60
+
61
+ /** Predicate URI → HTML attribute name. After the vocab migration
62
+ * (see swc/claude/plans/PLAN-vocab-migration.md) calendar settings
63
+ * use direct predicates from Dublin Core / Schema.org / OWL-Time / UI.
64
+ * `dct:source` is multi-valued; everything else is single. */
65
+ const DCT = 'http://purl.org/dc/terms/';
66
+ const SCHEMA = 'http://schema.org/';
67
+ const TIME_NS = 'http://www.w3.org/2006/time#';
68
+ const UI_NS = 'http://www.w3.org/ns/ui#';
69
+ const CONFIG_MAP = [
70
+ [DCT + 'format', 'provider'],
71
+ [UI_NS + 'view', 'view'],
72
+ [TIME_NS + 'days', 'window-days'],
73
+ [SCHEMA + 'numberOfItems', 'max-events'],
74
+ ];
75
+
76
+ /** True iff `source` is a `something.ttl#Subject` PropertyValue pointer
77
+ * rather than a direct calendar URL. The presence of `#` plus the
78
+ * `.ttl`/`.shacl` extension is the disambiguator — an ICS URL with a
79
+ * fragment is exotic enough to leave for later. */
80
+ function isRdfConfigSource(source) {
81
+ if (!source || !source.includes('#')) return false;
82
+ const path = source.split('#', 1)[0].toLowerCase();
83
+ return path.endsWith('.ttl') || path.endsWith('.shacl');
84
+ }
85
+
86
+ /** Format a Date as the day label used in the agenda's date column,
87
+ * e.g. "Wed, May 28". Honours the component's `locale` attribute. */
88
+ function formatDate(d, locale) {
89
+ return d.toLocaleDateString(locale || undefined, {
90
+ weekday: 'short', month: 'short', day: 'numeric',
91
+ });
92
+ }
93
+
94
+ /** Compare two Dates for same-day (local TZ). Used to detect repeat
95
+ * dates in the agenda so the date column can blank-out without
96
+ * losing its layout slot. */
97
+ function sameYMD(a, b) {
98
+ return !!a && !!b
99
+ && a.getFullYear() === b.getFullYear()
100
+ && a.getMonth() === b.getMonth()
101
+ && a.getDate() === b.getDate();
102
+ }
103
+
104
+ /** Two-digit zero-padded number — used for the agenda time column so
105
+ * `09:30` aligns under `14:00`. */
106
+ function pad2(n) { return n < 10 ? '0' + n : String(n); }
107
+
108
+ /** Format the time half of an agenda row: "09:30–10:15", "14:00" if
109
+ * the event has no end time / a same-instant end, or "All day" when
110
+ * the event was a DATE-only DTSTART. */
111
+ function formatEventTime(ev, locale) {
112
+ if (ev.allDay) return 'All day';
113
+ const start = `${pad2(ev.start.getHours())}:${pad2(ev.start.getMinutes())}`;
114
+ if (!ev.end || ev.end.getTime() === ev.start.getTime()) return start;
115
+ // Don't show the end time if it's the same minute as the start
116
+ // (some ICS sources use zero-duration events as bookmarks).
117
+ const endSameMinute =
118
+ ev.end.getFullYear() === ev.start.getFullYear() &&
119
+ ev.end.getMonth() === ev.start.getMonth() &&
120
+ ev.end.getDate() === ev.start.getDate() &&
121
+ ev.end.getHours() === ev.start.getHours() &&
122
+ ev.end.getMinutes() === ev.start.getMinutes();
123
+ if (endSameMinute) return start;
124
+ const end = `${pad2(ev.end.getHours())}:${pad2(ev.end.getMinutes())}`;
125
+ return `${start}–${end}`;
126
+ }
127
+
128
+ /** Pretty header label for the title strip. We don't have the calendar's
129
+ * own X-WR-CALNAME yet (could be added) so this falls back to a clean
130
+ * rendering of the calendar-id or the URL host. */
131
+ function deriveTitle({ source, calendarId }) {
132
+ if (calendarId) return calendarId;
133
+ if (!source) return 'Calendar';
134
+ try { return new URL(source, document.baseURI).hostname; }
135
+ catch { return 'Calendar'; }
136
+ }
137
+
138
+ /**
139
+ * Inline calendar viewer.
140
+ *
141
+ * @class SolCalendar
142
+ * @extends HTMLElement
143
+ */
144
+ class SolCalendar extends HTMLElement {
145
+ static get observedAttributes() {
146
+ return [
147
+ 'source', 'provider', 'calendar-id', 'view',
148
+ 'start', 'window-days', 'max-events', 'proxy',
149
+ 'time-zone', 'locale', 'hide-header',
150
+ ];
151
+ }
152
+
153
+ /** SHACL shape declaring the fixed schema (predicates + datatypes +
154
+ * cardinalities). sol-form's shape-driven mode generates a labelled
155
+ * field per property; only `dct:source` is multi-valued. dk-settings
156
+ * discovery picks this up. The legacy `editor` (ui:Form TTL) getter
157
+ * was dropped in the direct-predicate vocab migration — see
158
+ * swc/claude/plans/PLAN-vocab-migration.md. */
159
+ static get shape() {
160
+ return new URL('../shapes/calendar-settings.shacl', import.meta.url).href;
161
+ }
162
+
163
+ constructor() {
164
+ super();
165
+ this.attachShadow({ mode: 'open' });
166
+ this._controller = null; // AbortController for the active fetch
167
+ this._refreshMs = 10 * 60 * 1000;
168
+ this._timer = null;
169
+ }
170
+
171
+ async connectedCallback() {
172
+ adopt(this.shadowRoot, { sheet: CAL_SHEET, css: CAL_CSS });
173
+
174
+ this._status = document.createElement('div');
175
+ this._status.className = 'sol-calendar-status';
176
+ this._status.setAttribute('role', 'status');
177
+ this._status.setAttribute('aria-live', 'polite');
178
+ this._status.style.display = 'none';
179
+
180
+ this._root = document.createElement('div');
181
+ this._root.className = 'sol-calendar';
182
+
183
+ this.shadowRoot.append(this._status, this._root);
184
+
185
+ // PropertyValue config first (HTML attributes already win because
186
+ // _applySource only sets attributes that aren't already there).
187
+ await this._applySource();
188
+
189
+ try {
190
+ await this._update();
191
+ } catch (e) {
192
+ this._setStatus(e.message || String(e), true);
193
+ }
194
+ this._timer = setInterval(() => this._update().catch(() => {}), this._refreshMs);
195
+
196
+ // Re-fetch when <sol-default> changes the proxy at runtime.
197
+ this._unsubDefaults = onDefaultChange((name) => {
198
+ if (name === 'proxy') this.reload().catch(() => {});
199
+ });
200
+
201
+ if (this.hasAttribute('editor-self')) attachEditorSelfGear(this);
202
+ }
203
+
204
+ disconnectedCallback() {
205
+ if (this._controller) this._controller.abort();
206
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
207
+ if (this._unsubDefaults) { this._unsubDefaults(); this._unsubDefaults = null; }
208
+ }
209
+
210
+ /**
211
+ * Re-read `source` and re-fetch calendar events. Public hook used by
212
+ * external editors (e.g. dk-settings) after a configuration file
213
+ * changes.
214
+ */
215
+ async reload() {
216
+ await this._applySource();
217
+ await this._update();
218
+ }
219
+
220
+ attributeChangedCallback(_name, oldV, newV) {
221
+ if (oldV !== newV && this.isConnected && this._root) {
222
+ this._update().catch(() => {});
223
+ }
224
+ }
225
+
226
+ /** If `source` points at an RDF config file, pull PropertyValue
227
+ * settings into attributes that aren't already explicitly set on
228
+ * the HTML element. Skipped when source is empty or looks like a
229
+ * direct ICS URL. */
230
+ async _applySource() {
231
+ const source = this.getAttribute('source');
232
+ if (!isRdfConfigSource(source)) return;
233
+ try {
234
+ const cfg = await loadConfig(source);
235
+ for (const [predicate, attr] of CONFIG_MAP) {
236
+ if (cfg[predicate] != null && !this.hasAttribute(attr)) {
237
+ this.setAttribute(attr, String(cfg[predicate]));
238
+ }
239
+ }
240
+ // dct:source provides the actual feed URLs (the element's `source`
241
+ // attribute was a config-doc pointer). Stash them on the instance
242
+ // instead of overwriting the attribute — sol-settings discovery
243
+ // and the editor need `source` to keep pointing at the config TTL.
244
+ const dctSource = cfg[DCT + 'source'];
245
+ if (dctSource != null) {
246
+ this._feedUrls = Array.isArray(dctSource)
247
+ ? dctSource.map(String).filter(Boolean)
248
+ : [String(dctSource)].filter(Boolean);
249
+ }
250
+ } catch (err) {
251
+ // Bad TTL or missing rdflib — surface in the status strip but
252
+ // keep going; explicit HTML attributes can still drive a render.
253
+ this._setStatus(`Config: ${err.message}`, true);
254
+ }
255
+ }
256
+
257
+ /** ICS feed URLs to fetch. When `source` is an RDF-config pointer,
258
+ * `_applySource` populates `this._feedUrls` from dct:source — those
259
+ * win. Otherwise `source` is treated as a direct ICS URL (or a
260
+ * whitespace-separated list of them), per the documented inline
261
+ * usage. Empty result before _applySource has run on a config
262
+ * pointer; the post-load render fills it in. */
263
+ _sourceUrls() {
264
+ if (Array.isArray(this._feedUrls) && this._feedUrls.length) return this._feedUrls;
265
+ const raw = this.source;
266
+ if (!raw || isRdfConfigSource(raw)) return [];
267
+ return raw.split(/\s+/).filter(Boolean);
268
+ }
269
+
270
+ /** Update the polite live region. Pass `isError` to colour it red. */
271
+ _setStatus(msg, isError = false) {
272
+ if (!this._status) return;
273
+ this._status.textContent = msg || '';
274
+ this._status.style.display = msg ? '' : 'none';
275
+ if (isError) this._status.setAttribute('data-error', '');
276
+ else this._status.removeAttribute('data-error');
277
+ }
278
+
279
+ /* ── attribute readers ───────────────────────────────────────────── */
280
+
281
+ get source() { return this.getAttribute('source') || ''; }
282
+ get provider() { return (this.getAttribute('provider') || 'ics').toLowerCase(); }
283
+ get calendarId() { return this.getAttribute('calendar-id') || ''; }
284
+ get view() { return (this.getAttribute('view') || 'agenda').toLowerCase(); }
285
+ get proxy() { return this.getAttribute('proxy') || getDefault('proxy') || ''; }
286
+ get locale() { return this.getAttribute('locale') || ''; }
287
+ get windowDays() { return Math.max(1, Number(this.getAttribute('window-days')) || 30); }
288
+ get maxEvents() { return Math.max(1, Number(this.getAttribute('max-events')) || 100); }
289
+ get startDate() {
290
+ const raw = this.getAttribute('start');
291
+ if (!raw) {
292
+ // Start at midnight local — same-day events that started a few
293
+ // minutes ago still appear, instead of being trimmed by the
294
+ // window cutoff.
295
+ const d = new Date();
296
+ d.setHours(0, 0, 0, 0);
297
+ return d;
298
+ }
299
+ // YYYY-MM-DD parses as UTC midnight; pin to local midnight so the
300
+ // agenda's day-grouping matches what the user expects.
301
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
302
+ if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
303
+ return new Date(raw);
304
+ }
305
+
306
+ /* ── fetch + render dispatch ─────────────────────────────────────── */
307
+
308
+ async _update() {
309
+ if (!this._root) return;
310
+
311
+ // After _applySource, `source` holds either zero URLs (caller is
312
+ // using provider+calendar-id only), one URL, or N whitespace-
313
+ // separated URLs (multi-calendar amalgamation).
314
+ const urls = this._sourceUrls();
315
+ if (!urls.length && !this.calendarId) {
316
+ this._setStatus('No calendar source — set source= or provider= + calendar-id=', true);
317
+ return;
318
+ }
319
+
320
+ if (this._controller) this._controller.abort();
321
+ this._controller = new AbortController();
322
+ const signal = this._controller.signal;
323
+
324
+ this._setStatus(urls.length > 1
325
+ ? `Loading ${urls.length} calendars…`
326
+ : 'Loading calendar…');
327
+
328
+ const opts = {
329
+ provider: this.provider,
330
+ calendarId: this.calendarId,
331
+ proxy: this.proxy,
332
+ start: this.startDate,
333
+ windowDays: this.windowDays,
334
+ maxEvents: this.maxEvents,
335
+ signal,
336
+ };
337
+
338
+ try {
339
+ if (urls.length > 1) {
340
+ // Amalgamated calendar — Promise.allSettled inside, so a single
341
+ // dead feed doesn't blank the rest. Surface the count of
342
+ // failures in the status strip without overriding the events.
343
+ const { events, errors } = await getMergedCalendarEvents(urls, opts);
344
+ this._renderAgenda(events);
345
+ if (errors.length) {
346
+ this._setStatus(
347
+ `Loaded ${urls.length - errors.length} of ${urls.length} calendars — ${errors.length} failed`,
348
+ true);
349
+ // Skip the per-feed warn when the failure is just our own
350
+ // AbortController firing (e.g. a later _update cancelled an
351
+ // in-flight one) — that's expected, not a real failure.
352
+ for (const e of errors) {
353
+ if (/aborted/i.test(e.message)) continue;
354
+ console.warn(`[sol-calendar] ${e.url}: ${e.message}`);
355
+ }
356
+ } else {
357
+ this._setStatus('');
358
+ }
359
+ } else {
360
+ const events = await getCalendarEvents(urls[0] || '', opts);
361
+ this._renderAgenda(events);
362
+ this._setStatus('');
363
+ }
364
+ } catch (e) {
365
+ if (e.name === 'AbortError') return;
366
+ this._renderEmpty(`Couldn't load calendar: ${e.message}`);
367
+ this._setStatus(e.message || String(e), true);
368
+ }
369
+ }
370
+
371
+ _renderEmpty(msg) {
372
+ const wrap = document.createElement('div');
373
+ wrap.className = 'sol-calendar-empty';
374
+ wrap.textContent = msg;
375
+ this._root.replaceChildren(wrap);
376
+ }
377
+
378
+ _renderAgenda(events) {
379
+ // hide-header: skip the title + provider strip entirely. Common for
380
+ // dashboards / sidebars that already label the slot themselves.
381
+ const showHeader = !this.hasAttribute('hide-header');
382
+ let header = null;
383
+ if (showHeader) {
384
+ header = document.createElement('div');
385
+ header.className = 'cal-header';
386
+ const title = document.createElement('span');
387
+ title.className = 'cal-title';
388
+ title.textContent = deriveTitle({ source: this.source, calendarId: this.calendarId });
389
+ const prov = document.createElement('span');
390
+ prov.className = 'cal-provider';
391
+ prov.textContent = this.provider;
392
+ header.append(title, prov);
393
+ }
394
+
395
+ const list = document.createElement('div');
396
+ list.className = 'cal-agenda';
397
+ list.setAttribute('aria-label', 'Upcoming events');
398
+
399
+ if (!events.length) {
400
+ const empty = document.createElement('div');
401
+ empty.className = 'sol-calendar-empty';
402
+ empty.textContent = `No events in the next ${this.windowDays} days.`;
403
+ list.appendChild(empty);
404
+ this._root.replaceChildren(...(header ? [header, list] : [list]));
405
+ return;
406
+ }
407
+
408
+ const today = new Date();
409
+ const ul = document.createElement('ul');
410
+ ul.className = 'cal-rows';
411
+
412
+ // Flat list — one row per event, with date / time / event columns.
413
+ // The date is rendered on every row (preserving the grid alignment)
414
+ // but visually blanked when the previous row was the same day, so a
415
+ // run of same-day events reads cleanly without losing the column.
416
+ let prevDate = null;
417
+ for (const ev of events) {
418
+ const li = document.createElement('li');
419
+ li.className = 'cal-row' + (sameYMD(ev.start, today) ? ' today' : '');
420
+
421
+ const date = document.createElement('span');
422
+ date.className = 'cal-row-date' + (sameYMD(ev.start, prevDate) ? ' repeat' : '');
423
+ date.textContent = formatDate(ev.start, this.locale);
424
+
425
+ const time = document.createElement('span');
426
+ time.className = 'cal-row-time';
427
+ time.textContent = formatEventTime(ev, this.locale);
428
+
429
+ const body = document.createElement('div');
430
+ body.className = 'cal-row-body';
431
+
432
+ const summary = document.createElement('span');
433
+ summary.className = 'cal-row-summary';
434
+ if (ev.url) {
435
+ const a = document.createElement('a');
436
+ a.href = ev.url;
437
+ a.target = '_blank';
438
+ a.rel = 'noopener noreferrer';
439
+ a.textContent = ev.summary || '(untitled)';
440
+ summary.appendChild(a);
441
+ } else {
442
+ summary.textContent = ev.summary || '(untitled)';
443
+ }
444
+ body.appendChild(summary);
445
+
446
+ if (ev.location) {
447
+ const loc = document.createElement('span');
448
+ loc.className = 'cal-row-location';
449
+ loc.textContent = ev.location;
450
+ body.appendChild(loc);
451
+ }
452
+
453
+ li.append(date, time, body);
454
+ ul.appendChild(li);
455
+ prevDate = ev.start;
456
+ }
457
+
458
+ list.appendChild(ul);
459
+ this._root.replaceChildren(...(header ? [header, list] : [list]));
460
+ }
461
+ }
462
+
463
+ define('sol-calendar', SolCalendar);
464
+
465
+ export { SolCalendar, buildProviderUrl };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * <sol-default> — Singleton holder for shared programmatic defaults.
3
+ *
4
+ * Place once in the host page; sol-* components consult its
5
+ * attributes as the last fallback for knobs they take (e.g. `proxy`,
6
+ * default issuers list, default endpoint) before reaching for a
7
+ * hard-coded value.
8
+ *
9
+ * Resolution order in each consumer:
10
+ * 1. The component's own HTML attribute
11
+ * 2. The component's RDF source PropertyValue, if applicable
12
+ * 3. This element's matching attribute (via core/defaults.js getDefault)
13
+ * 4. Hard-coded fallback in the component
14
+ *
15
+ * Reactivity: every attribute change re-dispatches `sol-default-change`
16
+ * (bubbling, composed) with detail `{ name, newValue, oldValue }`. The
17
+ * helper `onDefaultChange` in core/defaults.js wraps that listener.
18
+ *
19
+ * The element renders nothing — it's a configuration record, not UI.
20
+ *
21
+ * Usage:
22
+ * <sol-default proxy="http://localhost:3002/proxy?uri="></sol-default>
23
+ *
24
+ * @class SolDefault
25
+ * @extends HTMLElement
26
+ * @fires sol-default-change - detail: { name, newValue, oldValue }
27
+ */
28
+
29
+ import { define } from '../core/define.js';
30
+ import { loadConfig } from './utils/rdf-config.js';
31
+
32
+ class SolDefault extends HTMLElement {
33
+ // observedAttributes is intentionally empty: a MutationObserver
34
+ // watches every attribute on the element, so consumers don't have to
35
+ // declare which knobs they care about in advance. (If we listed any
36
+ // name here, attributeChangedCallback would double-fire alongside
37
+ // the observer.)
38
+ static get observedAttributes() { return []; }
39
+
40
+ // No hardcoded shape: which knobs <sol-default> holds is app-specific, so the
41
+ // editable SHACL shape comes from a per-instance `shape="…"` attribute
42
+ // (resolved by core/editor.js → sol-form). e.g.
43
+ // <sol-default shape="./shapes/app-settings.shacl" theme="dark" …>
44
+ // Without a `shape`, sol-default is a non-editable defaults record.
45
+
46
+ constructor() {
47
+ super();
48
+ this.style.display = 'none';
49
+ }
50
+
51
+ async connectedCallback() {
52
+ if (this._observer) return;
53
+ this._observer = new MutationObserver((mutations) => {
54
+ for (const m of mutations) {
55
+ if (m.type !== 'attributes' || !m.attributeName) continue;
56
+ const name = m.attributeName;
57
+ const newValue = this.getAttribute(name);
58
+ const oldValue = m.oldValue;
59
+ if (newValue === oldValue) continue;
60
+ this._fire(name, newValue, oldValue);
61
+ }
62
+ });
63
+ this._observer.observe(this, { attributes: true, attributeOldValue: true });
64
+
65
+ // RDF source: pulls each predicate's value into a matching HTML
66
+ // attribute (only when not already set explicitly on the element,
67
+ // so an inline override wins). camelCase predicates kebab-case
68
+ // their attribute name (`ui:defaultIssuers` → `default-issuers`).
69
+ const source = this.getAttribute('source');
70
+ if (source) await this._applySource(source);
71
+ }
72
+
73
+ async _applySource(source) {
74
+ try {
75
+ const cfg = await loadConfig(source);
76
+ for (const [predUri, value] of Object.entries(cfg)) {
77
+ const attr = attrFromPredicate(predUri);
78
+ if (!attr) continue;
79
+ if (this.hasAttribute(attr)) continue; // HTML override wins
80
+ this.setAttribute(attr, Array.isArray(value) ? value.join(' ') : String(value));
81
+ }
82
+ } catch (err) {
83
+ console.warn(`[sol-default] source ${source}: ${err.message}`);
84
+ }
85
+ }
86
+
87
+ /** Public hook used by &lt;sol-settings&gt; after a save: re-read the RDF
88
+ * and re-emit change events for downstream consumers. */
89
+ async reload() {
90
+ const source = this.getAttribute('source');
91
+ if (source) await this._applySource(source);
92
+ }
93
+
94
+ disconnectedCallback() {
95
+ if (this._observer) { this._observer.disconnect(); this._observer = null; }
96
+ }
97
+
98
+ _fire(name, newValue, oldValue) {
99
+ this.dispatchEvent(new CustomEvent('sol-default-change', {
100
+ bubbles: true, composed: true,
101
+ detail: { name, newValue, oldValue },
102
+ }));
103
+ }
104
+ }
105
+
106
+ // Map a predicate URI's local name to a kebab-case HTML attribute.
107
+ // `http://www.w3.org/ns/ui#proxy` → `proxy`
108
+ // `http://www.w3.org/ns/ui#defaultIssuers` → `default-issuers`
109
+ function attrFromPredicate(uri) {
110
+ const i = Math.max(uri.lastIndexOf('#'), uri.lastIndexOf('/'));
111
+ const local = i === -1 ? uri : uri.slice(i + 1);
112
+ if (!local || local === 'type') return null;
113
+ return local.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
114
+ }
115
+
116
+ define('sol-default', SolDefault);
117
+ export { SolDefault };
118
+ export default SolDefault;