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.
- package/README.md +7 -0
- package/core/activate.js +27 -0
- package/core/adopt.js +71 -0
- package/core/auth-core.js +73 -0
- package/core/auth-fetch.js +154 -0
- package/core/component-mount.js +110 -0
- package/core/defaults.js +48 -0
- package/core/define.js +15 -0
- package/core/display-target.js +166 -0
- package/core/edit-placements.js +28 -0
- package/core/editor-self.js +127 -0
- package/core/editor.js +162 -0
- package/core/events.js +27 -0
- package/core/extension-points.js +189 -0
- package/core/form-utils.js +210 -0
- package/core/from-query.js +138 -0
- package/core/from-rdf.js +52 -0
- package/core/here.js +33 -0
- package/core/include-core.js +73 -0
- package/core/inrupt-global.js +18 -0
- package/core/menu-consumer.js +41 -0
- package/core/menu-rdf.js +154 -0
- package/core/pod-ops.js +392 -0
- package/core/pod-registry.js +82 -0
- package/core/popup-proxy.js +255 -0
- package/core/rdf-core.js +280 -0
- package/core/rdf-render.js +136 -0
- package/core/rdf-utils.js +411 -0
- package/core/rdf.js +154 -0
- package/core/services.js +106 -0
- package/core/shape-to-form.js +741 -0
- package/core/sparql-safety.js +20 -0
- package/core/utils.js +196 -0
- package/dist/importmap-cdn.json +49 -0
- package/dist/importmap-local.json +49 -0
- package/dist/sol-loader.manifest.json +140 -0
- package/dist/vendor/@comunica-query-sparql.js +137851 -0
- package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
- package/dist/vendor/dompurify.js +1476 -0
- package/dist/vendor/ical.js.js +9739 -0
- package/dist/vendor/marked.js +85 -0
- package/dist/vendor/n3.js +14670 -0
- package/dist/vendor/rdf-validate-shacl.js +6970 -0
- package/dist/vendor/rdflib.js +35172 -0
- package/dist/vendor/solid-logic.js +6819 -0
- package/dist/vendor/solid-ui.js +21945 -0
- package/node/sol-form.js +133 -0
- package/node/sol-include.js +55 -0
- package/node/sol-login.js +632 -0
- package/node/sol-menu.js +639 -0
- package/node/sol-query.js +116 -0
- package/package.json +133 -0
- package/web/menu-from-rdf.js +23 -0
- package/web/scripts/prefs.js +25 -0
- package/web/sol-accordion.js +114 -0
- package/web/sol-basic.js +50 -0
- package/web/sol-breadcrumb.js +131 -0
- package/web/sol-button.js +244 -0
- package/web/sol-calendar.js +465 -0
- package/web/sol-default.js +118 -0
- package/web/sol-dropdown-button.js +222 -0
- package/web/sol-feed.js +1336 -0
- package/web/sol-form.js +949 -0
- package/web/sol-full.js +43 -0
- package/web/sol-gallery.js +303 -0
- package/web/sol-include.js +246 -0
- package/web/sol-live-edit.js +415 -0
- package/web/sol-login.js +856 -0
- package/web/sol-menu.js +593 -0
- package/web/sol-modal.js +377 -0
- package/web/sol-pod-extras.js +17 -0
- package/web/sol-pod-ops.js +680 -0
- package/web/sol-pod.js +1039 -0
- package/web/sol-query.js +546 -0
- package/web/sol-rolodex.js +95 -0
- package/web/sol-search.js +402 -0
- package/web/sol-settings.js +199 -0
- package/web/sol-solidos.js +93 -0
- package/web/sol-tabs.js +445 -0
- package/web/sol-time.js +194 -0
- package/web/sol-tree-edit.js +492 -0
- package/web/sol-wac.js +456 -0
- package/web/sol-weather.js +337 -0
- package/web/sol-window.js +142 -0
- package/web/styles/buttons-css.js +108 -0
- package/web/styles/help.css +242 -0
- package/web/styles/root.css +112 -0
- package/web/styles/sol-accordion-css.js +97 -0
- package/web/styles/sol-calendar-css.js +154 -0
- package/web/styles/sol-feed-css.js +475 -0
- package/web/styles/sol-form-css.js +471 -0
- package/web/styles/sol-gallery-css.js +181 -0
- package/web/styles/sol-include-css.js +95 -0
- package/web/styles/sol-live-edit-css.js +84 -0
- package/web/styles/sol-live-edit.css +101 -0
- package/web/styles/sol-login-css.js +116 -0
- package/web/styles/sol-menu-css.js +145 -0
- package/web/styles/sol-modal-css.js +134 -0
- package/web/styles/sol-pod-css.js +187 -0
- package/web/styles/sol-pod-modal-css.js +203 -0
- package/web/styles/sol-query-css.js +140 -0
- package/web/styles/sol-query-help.css +267 -0
- package/web/styles/sol-query-one-pager.css +67 -0
- package/web/styles/sol-search-css.js +157 -0
- package/web/styles/sol-solidos-css.js +7 -0
- package/web/styles/sol-tabs-css.js +114 -0
- package/web/styles/sol-time-css.js +30 -0
- package/web/styles/sol-wac-css.js +73 -0
- package/web/styles/sol-weather-css.js +59 -0
- package/web/styles/solid-logo.svg +9 -0
- package/web/styles/view-accordion-css.js +66 -0
- package/web/styles/view-anchorlist-css.js +22 -0
- package/web/styles/view-autocomplete-css.js +59 -0
- package/web/styles/view-rolodex-css.js +102 -0
- package/web/styles/view-select-css.js +21 -0
- package/web/utils/calendar-fetch.js +388 -0
- package/web/utils/code-mirror-editor.js +82 -0
- package/web/utils/commons-fetch.js +108 -0
- package/web/utils/feed-edit.js +159 -0
- package/web/utils/feed-edit.smoke.mjs +74 -0
- package/web/utils/feed-fetch.js +573 -0
- package/web/utils/live-edit-help/csv.js +64 -0
- package/web/utils/live-edit-help/graphviz.js +41 -0
- package/web/utils/live-edit-help/jsonld.js +55 -0
- package/web/utils/live-edit-help/markdown.js +52 -0
- package/web/utils/live-edit-help/mermaid.js +48 -0
- package/web/utils/live-edit-help/turtle.js +85 -0
- package/web/utils/rdf-config.js +125 -0
- package/web/utils/renderers/csv.js +124 -0
- package/web/utils/renderers/d3-force.js +82 -0
- package/web/utils/renderers/graphviz.js +13 -0
- package/web/utils/renderers/html.js +10 -0
- package/web/utils/renderers/jsonld.js +63 -0
- package/web/utils/renderers/markdown.js +19 -0
- package/web/utils/renderers/mermaid.js +54 -0
- package/web/utils/renderers/turtle.js +51 -0
- package/web/utils/sol-query-triple-patterns.js +151 -0
- package/web/utils/sol-query-ui.js +250 -0
- package/web/utils/sol-query-views.js +32 -0
- package/web/views/_helpers.js +34 -0
- package/web/views/accordion.js +133 -0
- package/web/views/anchorlist.js +59 -0
- package/web/views/auto-complete.js +183 -0
- package/web/views/dl.js +38 -0
- package/web/views/list.js +19 -0
- package/web/views/menu.js +56 -0
- package/web/views/rolodex.js +126 -0
- package/web/views/select.js +79 -0
- package/web/views/table.js +73 -0
- package/web/views/tabs.js +57 -0
package/web/sol-tabs.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-tabs> — Tabbed content container.
|
|
3
|
+
*
|
|
4
|
+
* Light-DOM element so the hosting context's styles (e.g. the modal's
|
|
5
|
+
* shadow-scoped `.modal-*` classes) reach the tab content.
|
|
6
|
+
*
|
|
7
|
+
* Imperative usage:
|
|
8
|
+
* const t = document.createElement('sol-tabs');
|
|
9
|
+
* t.tabs = [
|
|
10
|
+
* { name: 'View', render(body, footer, actions) { ... } },
|
|
11
|
+
* { name: 'Edit', render(body, footer, actions) { ... } },
|
|
12
|
+
* ];
|
|
13
|
+
* t.footerEl = someFooterEl;
|
|
14
|
+
* t.actionsEl = someActionsEl;
|
|
15
|
+
* parent.appendChild(t);
|
|
16
|
+
* t.switchTab('View');
|
|
17
|
+
*
|
|
18
|
+
* Declarative usage: fill the element with <a href="...">Label</a> anchors.
|
|
19
|
+
* Each anchor becomes a tab — label = text, content URL = href. Contents
|
|
20
|
+
* render lazily on first switch. Set `handler="sol-*"` on the anchor (or
|
|
21
|
+
* on <sol-tabs> as a default) to wrap the URL in that component; otherwise
|
|
22
|
+
* <sol-include> is used. The href is forwarded as both `source` and
|
|
23
|
+
* `endpoint`, and all other anchor attributes pass through — so e.g.
|
|
24
|
+
* `wanted="? ? ?"` on an anchor with `handler="sol-query"` just works.
|
|
25
|
+
*
|
|
26
|
+
* `handler` and the forwarded attributes may be written `data-*` to keep a
|
|
27
|
+
* standard <a> HTML-valid; the `data-` prefix is stripped when forwarding
|
|
28
|
+
* (`data-handler` picks the tag, `data-src` → `src`, `data-view` → `view`, …).
|
|
29
|
+
*
|
|
30
|
+
* <sol-tabs>
|
|
31
|
+
* <a href="notes.md">Notes</a>
|
|
32
|
+
* <a href="data.ttl" handler="sol-query" wanted="? ? ?">Table</a>
|
|
33
|
+
* <a href="lib.ttl" data-handler="ia-player" data-src="lib.ttl">Music</a>
|
|
34
|
+
* </sol-tabs>
|
|
35
|
+
*
|
|
36
|
+
* <sol-tabs handler="sol-live-edit">
|
|
37
|
+
* <a href="readme.md">Readme</a>
|
|
38
|
+
* </sol-tabs>
|
|
39
|
+
*
|
|
40
|
+
* Action launchers: tabs are the `<a href>` children; ANY OTHER element child
|
|
41
|
+
* (a button, a custom control) is treated as a toolbar action — re-homed into
|
|
42
|
+
* the tab bar's actions row (next to the tabs) and otherwise left as-is, so
|
|
43
|
+
* toolbar controls live in the same markup with no marker. `slot="actions"` is
|
|
44
|
+
* an explicit escape hatch (force an <a> to be an action, or be explicit). An
|
|
45
|
+
* inline <sol-button> action is auto-wired to this tabs' content area (no `for=`):
|
|
46
|
+
*
|
|
47
|
+
* <sol-tabs>
|
|
48
|
+
* <a href="a.html">A</a>
|
|
49
|
+
* <sol-button inline handler="sol-include" source="help.html">?</sol-button>
|
|
50
|
+
* </sol-tabs>
|
|
51
|
+
*
|
|
52
|
+
* RDF usage (opt-in): point `from-rdf` at a ui:Menu document — the same RDF
|
|
53
|
+
* shape <sol-menu> consumes. Each ui:Link / ui:Component part becomes a tab; a
|
|
54
|
+
* nested ui:Menu becomes a tab whose content is a slimmer
|
|
55
|
+
* <sol-tabs variant="sub"> strip of that group's children. `from-rdf` is inert
|
|
56
|
+
* until the `web/menu-from-rdf.js` add-on is imported (the lone rdflib pull —
|
|
57
|
+
* it keeps the declarative path dependency-free); without it this element stays
|
|
58
|
+
* declarative-only and waits for the add-on if one arrives later.
|
|
59
|
+
*
|
|
60
|
+
* import 'sol-components/menu-from-rdf.js'; // activation
|
|
61
|
+
* <sol-tabs from-rdf="./demo-tabs.ttl#MainTabs"></sol-tabs>
|
|
62
|
+
*
|
|
63
|
+
* The tab bar is hidden when only one tab is supplied. Set attribute
|
|
64
|
+
* `variant="sub"` for the slimmer nested subtab styling.
|
|
65
|
+
*
|
|
66
|
+
* Events (bubbling, composed):
|
|
67
|
+
* sol-tab-change — detail: { name }
|
|
68
|
+
* sol-error — detail: { source, kind, ... } on RDF / handler load failure
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
import { define } from '../core/define.js';
|
|
72
|
+
import { ensureDocStyle } from '../core/adopt.js';
|
|
73
|
+
import { CSS as TABS_CSS } from './styles/sol-tabs-css.js';
|
|
74
|
+
import { attachEditorSelfGear } from '../core/editor-self.js';
|
|
75
|
+
import { registerMenuConsumer, deferUntilLoader } from '../core/menu-consumer.js';
|
|
76
|
+
import { renderComponentItem, renderLinkItem, ensureHandler, isCommandName } from '../core/rdf-render.js';
|
|
77
|
+
|
|
78
|
+
// For auto-wiring an inline action launcher to this tabs' content area we need
|
|
79
|
+
// a stable selector; mint an id for any <sol-tabs> that lacks one.
|
|
80
|
+
let _solTabsUid = 0;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tabbed content container.
|
|
84
|
+
*
|
|
85
|
+
* Light-DOM element. Fill with anchor children (declarative) or set
|
|
86
|
+
* the `.tabs` property (imperative). Tab bar is hidden for a single tab.
|
|
87
|
+
*
|
|
88
|
+
* @class SolTabs
|
|
89
|
+
* @extends HTMLElement
|
|
90
|
+
* @attr {string} orientation - "horizontal" (default) or "vertical"
|
|
91
|
+
* @attr {string} handler - default sol-* component tag for all tabs
|
|
92
|
+
* @attr {string} variant - "sub" for slimmer nested subtab styling
|
|
93
|
+
* @attr {string} from-rdf - URL of a ui:Menu RDF document to build tabs from
|
|
94
|
+
* @fires sol-tab-change - detail: { name }
|
|
95
|
+
* @fires sol-error - detail: { source, kind } on RDF / handler load failure
|
|
96
|
+
*/
|
|
97
|
+
class SolTabs extends HTMLElement {
|
|
98
|
+
constructor() {
|
|
99
|
+
super();
|
|
100
|
+
this._tabs = [];
|
|
101
|
+
this._btns = {};
|
|
102
|
+
this._active = null;
|
|
103
|
+
this._cleanup = null;
|
|
104
|
+
this._footerEl = null;
|
|
105
|
+
this._actionsEl = null;
|
|
106
|
+
this._launchers = null;
|
|
107
|
+
this._rendered = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static get observedAttributes() { return ['from-rdf']; }
|
|
111
|
+
|
|
112
|
+
// `from-rdf` rendering is an opt-in capability: importing `web/menu-from-rdf.js`
|
|
113
|
+
// installs the rdflib-backed loader here. Null → this component is declarative-
|
|
114
|
+
// only and carries no rdflib (see core/menu-consumer.js).
|
|
115
|
+
static fromRdfLoader = null;
|
|
116
|
+
|
|
117
|
+
// Keep-alive: render every tab once into its own persistent pane and
|
|
118
|
+
// switch by toggling visibility, so components are never torn down —
|
|
119
|
+
// audio keeps playing, scroll / login / in-flight state survive.
|
|
120
|
+
get _keepAlive() { return this.hasAttribute('keep-alive'); }
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Form TTL describing how to edit this tabs' `from-rdf` subject.
|
|
124
|
+
* sol-tabs and sol-menu share the same `ui:Menu` shape, so they
|
|
125
|
+
* also share the same editor.
|
|
126
|
+
*/
|
|
127
|
+
static get editor() {
|
|
128
|
+
return new URL('../data/menu-form.ttl', import.meta.url).href;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
132
|
+
if (name === 'from-rdf' && oldValue !== newValue && this._rendered) {
|
|
133
|
+
this._loadFromRdf(newValue);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
connectedCallback() {
|
|
138
|
+
ensureDocStyle(this.getRootNode(), 'sol-tabs-styles', TABS_CSS);
|
|
139
|
+
if (this._rendered) return;
|
|
140
|
+
|
|
141
|
+
const fromRdf = this.getAttribute('from-rdf');
|
|
142
|
+
|
|
143
|
+
// Harvest declarative anchors before we overwrite innerHTML.
|
|
144
|
+
const declared = (!fromRdf && this._tabs.length === 0)
|
|
145
|
+
? this._harvestAnchors() : null;
|
|
146
|
+
|
|
147
|
+
// Declarative PAGE-LEVEL action launchers (e.g. a <sol-button> toolbar
|
|
148
|
+
// control). A child is an action — not a tab — when it's NOT an `<a href>`
|
|
149
|
+
// tab anchor; `slot="actions"` stays as an explicit escape hatch (e.g. to
|
|
150
|
+
// mark an <a> as an action, or force the classification). They're detached
|
|
151
|
+
// so they survive the innerHTML reset; _renderBar re-homes them onto the bar
|
|
152
|
+
// (right side). Unlike the per-tab `.sol-tabs-actions` row — which switchTab
|
|
153
|
+
// clears on every switch — these persist across tabs. An inline <sol-button>
|
|
154
|
+
// is auto-wired to this tabs' content area (no `for=` needed).
|
|
155
|
+
this._launchers = Array.from(this.children).filter(
|
|
156
|
+
(el) => el.matches('[slot="actions"]') || !el.matches('a[href]'));
|
|
157
|
+
for (const el of this._launchers) { el.remove(); this._wireInlineAction(el); }
|
|
158
|
+
|
|
159
|
+
this.innerHTML = `
|
|
160
|
+
<div class="sol-tabs-bar" role="tablist"></div>
|
|
161
|
+
<div class="sol-tabs-actions"></div>
|
|
162
|
+
<div class="sol-tabs-content"></div>`;
|
|
163
|
+
this._rendered = true;
|
|
164
|
+
|
|
165
|
+
// Default actions slot sits between the bar and the content. Tabs
|
|
166
|
+
// that want toolbar buttons (save / zoom / settings / help, etc.)
|
|
167
|
+
// can append into actionsEl. Callers may still override via
|
|
168
|
+
// `tabsEl.actionsEl = someExternalEl` before switchTab.
|
|
169
|
+
if (!this._actionsEl) {
|
|
170
|
+
this._actionsEl = this.querySelector(':scope > .sol-tabs-actions');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (fromRdf) {
|
|
174
|
+
this._loadFromRdf(fromRdf);
|
|
175
|
+
} else {
|
|
176
|
+
if (declared?.length) {
|
|
177
|
+
this._tabs = declared;
|
|
178
|
+
}
|
|
179
|
+
this._renderBar();
|
|
180
|
+
|
|
181
|
+
if (declared?.length) this._activateInitial();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.hasAttribute('editor-self')) attachEditorSelfGear(this);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fetch a ui:Menu RDF document and render its parts as tabs. This is the
|
|
188
|
+
// exact shape <sol-menu> consumes — ui:parts of ui:Link / ui:Component
|
|
189
|
+
// with ui:label / ui:href / ui:contents / ui:name — so a single RDF
|
|
190
|
+
// document can drive either element. A nested ui:Menu becomes a tab whose
|
|
191
|
+
// body holds a slimmer <sol-tabs variant="sub"> strip of its children.
|
|
192
|
+
async _loadFromRdf(uri) {
|
|
193
|
+
const load = this.constructor.fromRdfLoader;
|
|
194
|
+
if (!load) { deferUntilLoader(this); return; } // wait for the menu-from-rdf add-on
|
|
195
|
+
try {
|
|
196
|
+
const result = await load(uri, document.baseURI);
|
|
197
|
+
if (!result) return;
|
|
198
|
+
if (result.orientation && !this.hasAttribute('orientation')) {
|
|
199
|
+
this.setAttribute('orientation', result.orientation);
|
|
200
|
+
}
|
|
201
|
+
// A part marked slot="actions" is a toolbar launcher, not a tab — build it
|
|
202
|
+
// as an element on the bar's action row (mirrors the inline non-anchor
|
|
203
|
+
// launchers). In RDF mode these REPLACE any inline launchers (the
|
|
204
|
+
// completeness principle: everything comes from RDF).
|
|
205
|
+
const isAction = (d) => d.type === 'component'
|
|
206
|
+
&& (d.params || []).some(([k, v]) => k === 'slot' && v === 'actions');
|
|
207
|
+
const items = result.items || [];
|
|
208
|
+
const actionItems = items.filter(isAction);
|
|
209
|
+
this._tabs = this._wrapRdfItems(items.filter((d) => !isAction(d)));
|
|
210
|
+
if (actionItems.length) {
|
|
211
|
+
this._launchers = actionItems.map((d) => this._buildLauncher(d));
|
|
212
|
+
for (const el of this._launchers) this._wireInlineAction(el);
|
|
213
|
+
}
|
|
214
|
+
this._renderBar();
|
|
215
|
+
if (this._tabs.length) this._activateInitial();
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('<sol-tabs> from-rdf load failed:', err);
|
|
218
|
+
this.dispatchEvent(new CustomEvent('sol-error', {
|
|
219
|
+
bubbles: true, composed: true,
|
|
220
|
+
detail: { source: 'sol-tabs', kind: 'rdf-load', uri, message: err.message },
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build a toolbar launcher element from an RDF action descriptor (ui:name =
|
|
226
|
+
// tag, ui:label → text, ui:attribute → attributes; the slot="actions" marker
|
|
227
|
+
// is dropped). Mirrors an inline non-anchor launcher.
|
|
228
|
+
_buildLauncher(desc) {
|
|
229
|
+
const el = document.createElement(desc.tag);
|
|
230
|
+
for (const [k, v] of desc.params || []) {
|
|
231
|
+
if (k === 'slot' && v === 'actions') continue;
|
|
232
|
+
el.setAttribute(k, v);
|
|
233
|
+
}
|
|
234
|
+
if (desc.name) el.textContent = desc.name; // ?, A, 🌙 — empty for login/dropdown
|
|
235
|
+
return el;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Wrap the plain item descriptions from core/menu-rdf.js with render
|
|
239
|
+
// closures. Leaf links/components use the shared factory in
|
|
240
|
+
// core/rdf-render.js; a nested ui:Menu becomes a tab whose body is a
|
|
241
|
+
// <sol-tabs variant="sub"> holding the group's own children.
|
|
242
|
+
_wrapRdfItems(descriptions) {
|
|
243
|
+
const ctx = {
|
|
244
|
+
host: this, baseUrl: import.meta.url,
|
|
245
|
+
sourceName: 'sol-tabs', embedClass: 'sol-tab-embed',
|
|
246
|
+
};
|
|
247
|
+
return descriptions.map(desc => {
|
|
248
|
+
if (desc.type === 'submenu') {
|
|
249
|
+
const children = this._wrapRdfItems(desc.children);
|
|
250
|
+
return {
|
|
251
|
+
name: desc.name,
|
|
252
|
+
id: desc.id,
|
|
253
|
+
render: (body) => {
|
|
254
|
+
const sub = document.createElement('sol-tabs');
|
|
255
|
+
sub.setAttribute('variant', 'sub');
|
|
256
|
+
sub.tabs = children;
|
|
257
|
+
body.appendChild(sub);
|
|
258
|
+
if (children.length) sub.switchTab(children[0].name);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (desc.type === 'component') {
|
|
263
|
+
// Command items (ui:name is a registry key, not a tag) are a menu
|
|
264
|
+
// affordance, not content — a tab can't "run" something. Skip them.
|
|
265
|
+
if (isCommandName(desc.tag)) return null;
|
|
266
|
+
return { name: desc.name, id: desc.id, render: renderComponentItem(desc, ctx) };
|
|
267
|
+
}
|
|
268
|
+
return { name: desc.name, id: desc.id, render: renderLinkItem(desc, ctx) };
|
|
269
|
+
}).filter(Boolean);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Parse <a href="url" [handler="tag"] [attr=val ...]>Label</a> children
|
|
273
|
+
// into tab descriptors. Each tab's render() creates the component named
|
|
274
|
+
// by the anchor's `handler` attribute (falling back to the sol-tabs-level
|
|
275
|
+
// `handler` attribute, finally to <sol-include>). The href is passed to
|
|
276
|
+
// the created element as both `source` and `endpoint` so components that
|
|
277
|
+
// use either convention (sol-include / sol-live-edit use source, sol-query
|
|
278
|
+
// uses endpoint) pick it up. All other anchor attributes are forwarded.
|
|
279
|
+
// Auto-wire an inline action launcher (<sol-button inline>) to this tabs'
|
|
280
|
+
// content area, so the author needn't repeat a `for=` selector. No-op when it
|
|
281
|
+
// already has `for=` or isn't an inline sol-button.
|
|
282
|
+
_wireInlineAction(el) {
|
|
283
|
+
if (!el.tagName || el.tagName.toLowerCase() !== 'sol-button') return;
|
|
284
|
+
if (!el.hasAttribute('inline') || el.hasAttribute('for')) return;
|
|
285
|
+
if (!this.id) this.id = `sol-tabs-${++_solTabsUid}`;
|
|
286
|
+
el.setAttribute('for', `#${this.id} > .sol-tabs-content`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_harvestAnchors() {
|
|
290
|
+
// Anchors marked slot="actions" are launchers, not tabs — skip them here.
|
|
291
|
+
const anchors = Array.from(this.querySelectorAll(':scope > a[href]:not([slot="actions"])'));
|
|
292
|
+
if (!anchors.length) return [];
|
|
293
|
+
// `handler` may be written plain or as `data-handler` (the latter keeps a
|
|
294
|
+
// standard <a> HTML-valid). Same for the forwarded attributes below.
|
|
295
|
+
const parentHandler = (this.getAttribute('data-handler') || this.getAttribute('handler') || '').trim();
|
|
296
|
+
const SKIP = new Set(['href', 'handler', 'data-handler', 'data-tab-id', 'target', 'rel', 'download', 'hreflang', 'type', 'referrerpolicy']);
|
|
297
|
+
return anchors.map((a, i) => {
|
|
298
|
+
const label = (a.textContent || '').trim() || `Tab ${i + 1}`;
|
|
299
|
+
const url = a.getAttribute('href');
|
|
300
|
+
const handlerTag = (a.getAttribute('data-handler') || a.getAttribute('handler') || parentHandler || 'sol-include').trim();
|
|
301
|
+
return {
|
|
302
|
+
name: label,
|
|
303
|
+
// The tab id (→ button data-tab-id, for styling/selection) can be set
|
|
304
|
+
// explicitly with data-tab-id, independent of the anchor's id — the
|
|
305
|
+
// latter is forwarded to become the content element's id.
|
|
306
|
+
id: a.dataset.tabId || a.id || undefined,
|
|
307
|
+
render: (body) => {
|
|
308
|
+
ensureHandler(handlerTag, this, import.meta.url, 'sol-tabs');
|
|
309
|
+
const el = document.createElement(handlerTag);
|
|
310
|
+
el.setAttribute('source', url);
|
|
311
|
+
el.setAttribute('endpoint', url);
|
|
312
|
+
for (const attr of a.attributes) {
|
|
313
|
+
if (SKIP.has(attr.name)) continue;
|
|
314
|
+
// `data-*` author attributes forward with the prefix stripped, so a
|
|
315
|
+
// standard <a> stays HTML-valid: data-src → src, data-view → view.
|
|
316
|
+
const name = attr.name.startsWith('data-') ? attr.name.slice(5) : attr.name;
|
|
317
|
+
el.setAttribute(name, attr.value);
|
|
318
|
+
}
|
|
319
|
+
el.classList.add('sol-tab-embed');
|
|
320
|
+
body.appendChild(el);
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
get tabs() { return this._tabs; }
|
|
327
|
+
set tabs(arr) {
|
|
328
|
+
this._tabs = arr || [];
|
|
329
|
+
if (this._rendered) this._renderBar();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
get footerEl() { return this._footerEl; }
|
|
333
|
+
set footerEl(el) { this._footerEl = el; }
|
|
334
|
+
|
|
335
|
+
get actionsEl() { return this._actionsEl; }
|
|
336
|
+
set actionsEl(el) { this._actionsEl = el; }
|
|
337
|
+
|
|
338
|
+
get activeTab() { return this._active; }
|
|
339
|
+
get body() { return this.querySelector(':scope > .sol-tabs-content'); }
|
|
340
|
+
|
|
341
|
+
_renderBar() {
|
|
342
|
+
const bar = this.querySelector(':scope > .sol-tabs-bar');
|
|
343
|
+
if (!bar) return;
|
|
344
|
+
bar.innerHTML = '';
|
|
345
|
+
this._btns = {};
|
|
346
|
+
const launchers = this._launchers || [];
|
|
347
|
+
// Hide the bar only when there's nothing to show — a lone tab AND no
|
|
348
|
+
// page-level launchers. Launchers alone keep the bar visible.
|
|
349
|
+
if (this._tabs.length <= 1 && !launchers.length) { bar.style.display = 'none'; return; }
|
|
350
|
+
bar.style.display = '';
|
|
351
|
+
this._tabs.forEach(tab => {
|
|
352
|
+
const btn = document.createElement('button');
|
|
353
|
+
btn.type = 'button';
|
|
354
|
+
btn.setAttribute('role', 'tab');
|
|
355
|
+
btn.textContent = tab.name;
|
|
356
|
+
if (tab.id) btn.dataset.tabId = tab.id;
|
|
357
|
+
btn.onclick = () => this.switchTab(tab.name);
|
|
358
|
+
bar.appendChild(btn);
|
|
359
|
+
this._btns[tab.name] = btn;
|
|
360
|
+
});
|
|
361
|
+
// Page-level action launchers, grouped on the right of the bar. Re-appended
|
|
362
|
+
// on every bar render (so they survive a tabs reload); persist across switches.
|
|
363
|
+
if (launchers.length) {
|
|
364
|
+
const group = document.createElement('span');
|
|
365
|
+
group.className = 'sol-tabs-launch';
|
|
366
|
+
for (const el of launchers) group.appendChild(el);
|
|
367
|
+
bar.appendChild(group);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Render every tab once (keep-alive) then show the first, else just
|
|
372
|
+
// show the first (lazy default path).
|
|
373
|
+
_activateInitial() {
|
|
374
|
+
if (!this._tabs.length) return;
|
|
375
|
+
if (this._keepAlive) {
|
|
376
|
+
this.body.innerHTML = ''; // drop any panes from a prior load (reload)
|
|
377
|
+
for (const t of this._tabs) this._ensurePane(t);
|
|
378
|
+
}
|
|
379
|
+
this.switchTab(this._tabs[0].name);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Build (once) a persistent pane for a tab and render its content into it.
|
|
383
|
+
_ensurePane(tab) {
|
|
384
|
+
if (tab._pane) return tab._pane;
|
|
385
|
+
const pane = document.createElement('div');
|
|
386
|
+
pane.className = 'sol-tabs-pane';
|
|
387
|
+
if (tab.id) pane.dataset.tabId = tab.id;
|
|
388
|
+
pane.dataset.tabName = tab.name;
|
|
389
|
+
pane.hidden = true;
|
|
390
|
+
this.body.appendChild(pane);
|
|
391
|
+
tab._pane = pane;
|
|
392
|
+
tab.render(pane, this._footerEl, this._actionsEl);
|
|
393
|
+
return pane;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
switchTab(name) {
|
|
397
|
+
const tab = this._tabs.find(t => t.name.toLowerCase() === name.toLowerCase());
|
|
398
|
+
if (!tab) return;
|
|
399
|
+
this._active = tab.name;
|
|
400
|
+
|
|
401
|
+
Object.values(this._btns).forEach(b => b.classList.remove('active'));
|
|
402
|
+
if (this._btns[tab.name]) this._btns[tab.name].classList.add('active');
|
|
403
|
+
|
|
404
|
+
if (this._keepAlive) {
|
|
405
|
+
// No teardown: ensure this tab's pane exists, then park the others.
|
|
406
|
+
this._ensurePane(tab);
|
|
407
|
+
for (const t of this._tabs) if (t._pane) t._pane.hidden = (t !== tab);
|
|
408
|
+
} else {
|
|
409
|
+
if (typeof this._cleanup === 'function') { this._cleanup(); this._cleanup = null; }
|
|
410
|
+
|
|
411
|
+
const body = this.body;
|
|
412
|
+
body.innerHTML = '';
|
|
413
|
+
body.style.padding = ''; body.style.overflow = ''; body.style.height = '';
|
|
414
|
+
if (this._footerEl) this._footerEl.innerHTML = '';
|
|
415
|
+
if (this._actionsEl) this._actionsEl.innerHTML = '';
|
|
416
|
+
|
|
417
|
+
const cleanup = tab.render(body, this._footerEl, this._actionsEl);
|
|
418
|
+
if (typeof cleanup === 'function') this._cleanup = cleanup;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.dispatchEvent(new CustomEvent('sol-tab-change', {
|
|
422
|
+
bubbles: true, composed: true, detail: { name: tab.name },
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Re-read `from-rdf` and rebuild the tab bar. Public hook used by
|
|
428
|
+
* external editors (e.g. dk-settings) after the tabs TTL changes.
|
|
429
|
+
* Tabs declared via light-DOM anchors have no source to re-read;
|
|
430
|
+
* reload is a no-op in that case.
|
|
431
|
+
*/
|
|
432
|
+
async reload() {
|
|
433
|
+
const uri = this.getAttribute('from-rdf');
|
|
434
|
+
if (uri) await this._loadFromRdf(uri);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
disconnectedCallback() {
|
|
438
|
+
if (typeof this._cleanup === 'function') { this._cleanup(); this._cleanup = null; }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
define('sol-tabs', SolTabs);
|
|
443
|
+
registerMenuConsumer(SolTabs);
|
|
444
|
+
export { SolTabs };
|
|
445
|
+
export default SolTabs;
|
package/web/sol-time.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-time> — clock display web component.
|
|
3
|
+
*
|
|
4
|
+
* Always shows local and UTC. An optional third timezone (label + hour
|
|
5
|
+
* offset from UTC) appears when either attribute is set. Updates once
|
|
6
|
+
* a minute.
|
|
7
|
+
*
|
|
8
|
+
* Attributes:
|
|
9
|
+
* time-label — short label for an extra timezone (e.g. "tokyo")
|
|
10
|
+
* time-offset — that timezone's offset from UTC in hours (e.g. "9")
|
|
11
|
+
* source — "file.ttl#Subject" Turtle config in schema.org
|
|
12
|
+
* PropertyValue form. Setting names map to the
|
|
13
|
+
* matching HTML attributes:
|
|
14
|
+
* "timezone" → time-label
|
|
15
|
+
* "timezone-offset" → time-offset
|
|
16
|
+
* HTML attributes override the TTL.
|
|
17
|
+
*
|
|
18
|
+
* @element sol-time
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <sol-time></sol-time>
|
|
22
|
+
* <sol-time time-label="tokyo" time-offset="9"></sol-time>
|
|
23
|
+
* <sol-time source="data/time-settings.ttl#Settings"></sol-time>
|
|
24
|
+
*/
|
|
25
|
+
import { adopt } from '../core/adopt.js';
|
|
26
|
+
import { define } from '../core/define.js';
|
|
27
|
+
import { attachEditorSelfGear } from '../core/editor-self.js';
|
|
28
|
+
import { CSS as TIME_CSS, sheet as TIME_SHEET } from './styles/sol-time-css.js';
|
|
29
|
+
import { loadConfig } from './utils/rdf-config.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Derive the current UTC offset (in hours, possibly fractional) for an
|
|
33
|
+
* IANA timezone name. Returns null if the name is unrecognised.
|
|
34
|
+
*
|
|
35
|
+
* Uses Intl.DateTimeFormat's "shortOffset" timezone name — emitted as
|
|
36
|
+
* strings like `"GMT+5:30"`, `"GMT-04:00"`, or just `"GMT"`. Parses
|
|
37
|
+
* the suffix back to a decimal hours value. Honours DST automatically
|
|
38
|
+
* because the formatter consults the OS / browser's IANA database.
|
|
39
|
+
*/
|
|
40
|
+
function ianaOffsetHours(iana) {
|
|
41
|
+
try {
|
|
42
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
43
|
+
timeZone: iana,
|
|
44
|
+
timeZoneName: 'shortOffset',
|
|
45
|
+
});
|
|
46
|
+
const parts = fmt.formatToParts(new Date());
|
|
47
|
+
const tz = parts.find(p => p.type === 'timeZoneName')?.value;
|
|
48
|
+
if (!tz) return null;
|
|
49
|
+
// tz looks like "GMT", "GMT+5:30", "GMT-04:00", "UTC+11", …
|
|
50
|
+
if (tz === 'GMT' || tz === 'UTC') return 0;
|
|
51
|
+
const m = tz.match(/^(?:GMT|UTC)([+-])(\d{1,2})(?::(\d{2}))?$/);
|
|
52
|
+
if (!m) return null;
|
|
53
|
+
const sign = m[1] === '-' ? -1 : 1;
|
|
54
|
+
const hours = parseInt(m[2], 10) + (parseInt(m[3] || '0', 10) / 60);
|
|
55
|
+
return sign * hours;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Zero-pad a one- or two-digit clock value to two chars. */
|
|
62
|
+
function pad2(n) { return n < 10 ? '0' + n : String(n); }
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clock display web component.
|
|
66
|
+
*
|
|
67
|
+
* @class SolTime
|
|
68
|
+
* @extends HTMLElement
|
|
69
|
+
*/
|
|
70
|
+
class SolTime extends HTMLElement {
|
|
71
|
+
static get observedAttributes() {
|
|
72
|
+
return ['time-label', 'time-offset', 'source'];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** SHACL shape declaring the fixed schema (predicates + datatypes +
|
|
76
|
+
* cardinalities). sol-form's shape-driven mode generates a labelled
|
|
77
|
+
* field per property; dk-settings discovery picks this up. The
|
|
78
|
+
* legacy `editor` (ui:Form TTL) getter was dropped in the
|
|
79
|
+
* direct-predicate vocab migration — see
|
|
80
|
+
* swc/claude/plans/PLAN-vocab-migration.md. */
|
|
81
|
+
static get shape() {
|
|
82
|
+
return new URL('../shapes/time-settings.shacl', import.meta.url).href;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
constructor() {
|
|
86
|
+
super();
|
|
87
|
+
this.attachShadow({ mode: 'open' });
|
|
88
|
+
this._root = document.createElement('div');
|
|
89
|
+
this._root.className = 'sol-time';
|
|
90
|
+
this._timer = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async connectedCallback() {
|
|
94
|
+
adopt(this.shadowRoot, { sheet: TIME_SHEET, css: TIME_CSS });
|
|
95
|
+
this.shadowRoot.appendChild(this._root);
|
|
96
|
+
|
|
97
|
+
// Pull defaults from the configured RDF source; explicit HTML
|
|
98
|
+
// attributes win, so the TTL is a baseline rather than a forced
|
|
99
|
+
// setting. Render once synchronously so the clock isn't blank
|
|
100
|
+
// during the async fetch.
|
|
101
|
+
this._render();
|
|
102
|
+
await this._applySource();
|
|
103
|
+
this._render();
|
|
104
|
+
// Tick once a minute — the seconds are not shown so a finer tick
|
|
105
|
+
// would be pure busywork.
|
|
106
|
+
this._timer = setInterval(() => this._render(), 60_000);
|
|
107
|
+
|
|
108
|
+
if (this.hasAttribute('editor-self')) attachEditorSelfGear(this);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Apply config from `source` to attributes the component already
|
|
113
|
+
* observes. Mapping (predicate URI → HTML attribute):
|
|
114
|
+
* schema:timezone → time-label (display label = last IANA segment)
|
|
115
|
+
* → time-offset (UTC offset in hours, derived)
|
|
116
|
+
*
|
|
117
|
+
* The IANA name in schema:timezone fully determines both the label
|
|
118
|
+
* (`"Asia/Kolkata"` → `"Kolkata"`) and the UTC offset (computed via
|
|
119
|
+
* `Intl.DateTimeFormat`, which honors DST). See
|
|
120
|
+
* claude/plans/PLAN-vocab-migration.md for the predicate choice and
|
|
121
|
+
* rationale.
|
|
122
|
+
*/
|
|
123
|
+
async _applySource() {
|
|
124
|
+
const source = this.getAttribute('source');
|
|
125
|
+
if (!source) return;
|
|
126
|
+
const SCHEMA = 'http://schema.org/';
|
|
127
|
+
try {
|
|
128
|
+
const cfg = await loadConfig(source);
|
|
129
|
+
const iana = cfg[SCHEMA + 'timezone'];
|
|
130
|
+
if (!iana) return;
|
|
131
|
+
if (!this.hasAttribute('time-label')) {
|
|
132
|
+
// Display label = last path segment (the city/place part).
|
|
133
|
+
const label = String(iana).split('/').pop() || String(iana);
|
|
134
|
+
this.setAttribute('time-label', label);
|
|
135
|
+
}
|
|
136
|
+
if (!this.hasAttribute('time-offset')) {
|
|
137
|
+
const offset = ianaOffsetHours(String(iana));
|
|
138
|
+
if (offset != null) this.setAttribute('time-offset', String(offset));
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.warn(`[sol-time] source ${source}: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
disconnectedCallback() {
|
|
146
|
+
if (this._timer) { clearInterval(this._timer); this._timer = null; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Re-read `source` and re-render. Public hook used by external
|
|
151
|
+
* editors (e.g. dk-settings) after a configuration file changes.
|
|
152
|
+
*/
|
|
153
|
+
async reload() {
|
|
154
|
+
await this._applySource();
|
|
155
|
+
this._render();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
attributeChangedCallback() {
|
|
159
|
+
if (this.isConnected) this._render();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_render() {
|
|
163
|
+
const now = new Date();
|
|
164
|
+
const local = pad2(now.getHours()) + ':' + pad2(now.getMinutes());
|
|
165
|
+
const utc = pad2(now.getUTCHours()) + ':' + pad2(now.getUTCMinutes());
|
|
166
|
+
|
|
167
|
+
// local + gmt are always shown — the three label-value pairs read
|
|
168
|
+
// uniformly (label + value triplets).
|
|
169
|
+
const parts = [
|
|
170
|
+
'<span class="label" part="local-label">local</span>',
|
|
171
|
+
`<span class="value" part="local-time">${local}</span>`,
|
|
172
|
+
'<span class="sep" part="sep">·</span>',
|
|
173
|
+
'<span class="label" part="utc-label">gmt</span>',
|
|
174
|
+
`<span class="value" part="utc-time">${utc}</span>`,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const label = this.getAttribute('time-label');
|
|
178
|
+
const offset = Number(this.getAttribute('time-offset'));
|
|
179
|
+
if (label && Number.isFinite(offset)) {
|
|
180
|
+
const extra = new Date(now.getTime() + offset * 3_600_000);
|
|
181
|
+
const t = pad2(extra.getUTCHours()) + ':' + pad2(extra.getUTCMinutes());
|
|
182
|
+
parts.push(
|
|
183
|
+
'<span class="sep" part="sep">·</span>',
|
|
184
|
+
`<span class="label" part="extra-label">${label}</span>`,
|
|
185
|
+
`<span class="value" part="extra-time">${t}</span>`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this._root.innerHTML = parts.join('');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
define('sol-time', SolTime);
|
|
194
|
+
export { SolTime };
|