app
Widget Registry
Public Made by Adomby adom
The reviewed registry of widgets renderable from plain markdown on any wiki page
← Commit history
adom-video: take the video as video= (the wiki inlines a src file into the frame)
3 files changed
+11−10
README.md+2−2@@ -45,7 +45,7 @@ Registry caps: 32 widgets, 12 attrs each. A malformed entry is dropped, never br ## Current widgets -Five load from adom/component-widgets and take a JSON `src`; adom-stock and adom-video load from this page (adom-video takes a video `src`); adom-symbol-variants loads from adom/adom-symbol and takes a component page.json `src`:+Five load from adom/component-widgets and take a JSON `src`; adom-stock and adom-video load from this page (adom-video takes `video`, not `src`: the wiki inlines a `src` file into the frame); adom-symbol-variants loads from adom/adom-symbol and takes a component page.json `src`: | tag | shows | |---|---|@@ -59,5 +59,5 @@ Five load from adom/component-widgets and take a JSON `src`; adom-stock and adom | `::adom-stock{src="/api/pages/OWNER/SLUG/stock"}` | live stock and price at Mouser, DigiKey and LCSC for a hardware page, from the wiki's cached vendor lookups (adom/wiki#186); component pages show it in their sidebar by themselves; `heading` | | `::adom-verified{src="..."}` | the "verified by" trust mark: big green check, who verified the page and when, per-variant coverage, and the full verification log with evidence; turns amber when a verified file changes; takes the page's `verification.json` — loads from adom/component-widgets | | `::adom-models{src="..."}` | every 3D file on a component page as a card: thumbnail, STEP or GLB, etched or unmarked and where the part number sits, what makes it different from its siblings, what it was derived from, a download, and whether the page still serves the file that was described (checked against the file listing, nothing downloaded); previous versions fold away; takes the page's `models.json` (adom/models@1) — loads from adom/component-widgets |-| `::adom-video{src="..."}` | a YouTube-style video player: every YouTube keyboard shortcut (Space/K, J/L, arrows, 0 to 9, `,` `.` frames, `<` `>` speed, C captions, F, I, M, ?), a 24px timeline grab zone with drag-to-scrub and a hover frame preview, chapters as bar segments, captions, speed and loop; `title` `poster` `chapters` (JSON `[{"t":12,"title":"..."}]` or WebVTT) `captions` (.vtt or .srt) `start` (`90` or `1m30s`) `loop` `muted` `autoplay`; F opens the player full-window in a new tab (widget frames cannot go fullscreen); loads from this page |+| `::adom-video{video="..."}` | a YouTube-style video player: every YouTube keyboard shortcut (Space/K, J/L, arrows, 0 to 9, `,` `.` frames, `<` `>` speed, C captions, F, I, M, ?), a 24px timeline grab zone with drag-to-scrub and a hover frame preview, chapters as bar segments, captions, speed and loop; `title` `poster` `chapters` (JSON `[{"t":12,"title":"..."}]` or WebVTT) `captions` (.vtt or .srt) `start` (`90` or `1m30s`) `loop` `muted` `autoplay`; F opens the player full-window in a new tab (widget frames cannot go fullscreen); loads from this page | | `::adom-3d{src="..."}` | any GLB in the canonical Adom 3D Viewer, click to load (nothing loads until the reader asks; it unloads offscreen and releases the WebGL context); `poster` `heading` `caption`; publish the GLB without Draco, the frame cannot reach the decoder CDN; loads from adom/component-widgets |
widgets.json+1−1@@ -161,7 +161,7 @@ "script": "https://wiki.adom.inc/adom/widgets/render/widgets/video-widget.js", "element": "adom-video", "attrs": [- "src",+ "video", "title", "poster", "chapters",
widgets/video-widget.js+8−7@@ -1,13 +1,14 @@ // <adom-video>: a YouTube-style video player for wiki pages. //-// ::adom-video{src="/OWNER/SLUG/render/demo.mp4" title="Demo" chapters="/OWNER/SLUG/render/demo.chapters.json"}+// ::adom-video{video="/OWNER/SLUG/render/demo.mp4" title="Demo" chapters="/OWNER/SLUG/render/demo.chapters.json"} // // Every YouTube keyboard shortcut, a timeline you can actually hit (a 24px grab zone around a // 4px bar, drag to scrub, hover for a frame preview and the time), chapters as bar segments, // captions (WebVTT or SRT, drawn by the player so + / - can resize them), speed and loop in a // settings menu, picture-in-picture, fullscreen. Self-contained: Shadow DOM, no dependencies. //-// Attributes: src (required), title, poster, chapters (JSON [{t,title}] or a WebVTT chapter+// Attributes: video (required; `src` also works outside the registry, but the wiki inlines a `src`+// file into the widget frame, so a registered tag must use `video`), title, poster, chapters (JSON [{t,title}] or a WebVTT chapter // file), captions (.vtt or .srt), start (seconds or 1m30s), loop, muted, autoplay. // // Inside a wiki widget frame the browser blocks fullscreen (the frame carries no allow=@@ -206,7 +207,7 @@ let active = null; class AdomVideo extends HTMLElement {- static get observedAttributes() { return ['src', 'title', 'poster', 'chapters', 'captions', 'start', 'loop', 'muted']; }+ static get observedAttributes() { return ['video', 'src', 'title', 'poster', 'chapters', 'captions', 'start', 'loop', 'muted']; } constructor() { super();@@ -265,17 +266,17 @@ this._load(); } disconnectedCallback() { if (active === this) active = null; clearTimeout(this._t.hide); this.v.pause(); }- attributeChangedCallback(n, a, b) { if (a !== b && this.isConnected && this._loaded) { if (n === 'src') this._load(); else if (n === 'chapters') this._chapters(); else if (n === 'captions') this._captions(); else if (n === 'title') this.$('.top').textContent = b || ''; } }+ attributeChangedCallback(n, a, b) { if (a !== b && this.isConnected && this._loaded) { if (n === 'video' || n === 'src') this._load(); else if (n === 'chapters') this._chapters(); else if (n === 'captions') this._captions(); else if (n === 'title') this.$('.top').textContent = b || ''; } } // absolute URL for a wiki path (the frame is opaque-origin, so resolve against the real location) _abs(u) { try { return new URL(u, location.href).href; } catch { return u; } } _load() { this._loaded = true;- const src = this.getAttribute('src');+ const src = this.getAttribute('video') || this.getAttribute('src'); const v = this.v; this.$('.top').textContent = this.getAttribute('title') || '';- if (!src) return this._error('No video: add <code>src="/OWNER/SLUG/render/video.mp4"</code> to the tag.');+ if (!src) return this._error('No video: add <code>video="/OWNER/SLUG/render/video.mp4"</code> to the tag.'); this.$('.err').classList.remove('on'); if (this.getAttribute('poster')) v.poster = this._abs(this.getAttribute('poster')); v.loop = this.hasAttribute('loop');@@ -568,7 +569,7 @@ ['playing', 'canplay', 'seeked', 'pause'].forEach((e) => v.addEventListener(e, () => p.classList.remove('wait'))); v.addEventListener('seeked', () => { this._seekBusy = false; if (this._seekPending != null) { const t = this._seekPending; this._seekPending = null; this._seekBusy = true; v.fastSeek ? v.fastSeek(t) : (v.currentTime = t); } }); v.addEventListener('error', () => {- const s = this.getAttribute('src') || '';+ const s = this.getAttribute('video') || this.getAttribute('src') || ''; this._error(`This video could not be loaded.<br><code>${esc(s)}</code><br><br>Check the path is <code>/OWNER/SLUG/render/FILE.mp4</code> and that the file is on the page. On a private page the widget frame may not be signed in.`); }); document.addEventListener('fullscreenchange', () => this._sync());