openvibers/openvibe.shared dock-card.js
dock-card.js
| 1 | /** |
| 2 | * openvibe-shared/dock-card.js: a known OpenVibe link's card under a message in the Dock (4.147.0). |
| 3 | * |
| 4 | * OpenVibe.Chat resolves the card when a message is sent (its server/chat/link-cards.js) and the DM API carries it on |
| 5 | * the message as `card`: { kind, url, title, line, note?, image, action: { label, href } | null }, for a video on |
| 6 | * OpenVibe.Video ("Watch together"), a party invite ("Join the party"), a shared voice or a skill on OpenVibe.Actor. |
| 7 | * dock.js loads this file the first time an open conversation shows a message with a card, so the Dock that every |
| 8 | * signed-in page carries stays small. |
| 9 | * |
| 10 | * OpenVibeDockCard(card, h) → the card's node, or null when it is not a card this can draw |
| 11 | * |
| 12 | * Every field is data: set as text, cut to size, a link only when it is https, a picture only from openvibe.media. |
| 13 | * The card is compact (a 72 px picture beside the words) because the Dock is narrow; the message stays the thing said. |
| 14 | */ |
| 15 | (function (root) { |
| 16 | ; |
| 17 | if (typeof document === 'undefined' || root.OpenVibeDockCard) return; |
| 18 | |
| 19 | const CSS = ` |
| 20 | .ovdk-card { display: flex; align-items: center; gap: 10px; box-sizing: border-box; max-width: min(100%, 300px); margin-top: 4px; padding: 8px 10px 8px 8px; border-radius: 14px; border: 1px solid var(--border, rgba(255,255,255,.1)); background: var(--bg-primary, #12141b); text-align: left; } |
| 21 | .ovdk-card-pic { flex: none; width: 72px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); } |
| 22 | .ovdk-card-body { min-width: 0; display: grid; justify-items: start; gap: 2px; } |
| 23 | .ovdk-card-t { font-size: 13.5px; font-weight: 700; line-height: 1.3; color: var(--text-primary, #ececf1); text-decoration: none; overflow-wrap: anywhere; } |
| 24 | a.ovdk-card-t:hover { text-decoration: underline; } |
| 25 | .ovdk-card-l { font-size: 12.5px; line-height: 1.35; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; } |
| 26 | .ovdk-card-go { display: inline-flex; align-items: center; min-height: 32px; margin-top: 4px; padding: 0 12px; border-radius: 999px; font-size: 13px; font-weight: 700; text-decoration: none; color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); } |
| 27 | .ovdk-card-go:hover { filter: brightness(1.08); } |
| 28 | @media (pointer: coarse) { .ovdk-card-go { min-height: 44px; padding: 0 16px; } } |
| 29 | `; |
| 30 | |
| 31 | const https = (u) => (typeof u === 'string' && /^https:\/\/[^\s"'<>]+$/.test(u) ? u : null); |
| 32 | const cut = (v, n) => Array.from(String(v)).slice(0, n).join(''); |
| 33 | |
| 34 | function styles() { |
| 35 | if (document.getElementById('ovdk-card-styles')) return; |
| 36 | const st = document.createElement('style'); |
| 37 | st.id = 'ovdk-card-styles'; |
| 38 | st.textContent = CSS; |
| 39 | document.head.appendChild(st); |
| 40 | } |
| 41 | |
| 42 | function card(c, h) { |
| 43 | if (!c || typeof c !== 'object' || typeof c.title !== 'string' || !c.title || typeof h !== 'function') return null; |
| 44 | styles(); |
| 45 | const url = https(c.url); |
| 46 | const box = h('div', 'ovdk-card'); |
| 47 | if (c.kind) box.dataset.kind = cut(c.kind, 20); |
| 48 | const pic = typeof c.image === 'string' && /^https:\/\/openvibe\.media\/[^\s"'<>]+$/.test(c.image) ? c.image : null; |
| 49 | if (pic) { |
| 50 | const img = h('img', 'ovdk-card-pic'); |
| 51 | img.src = pic; img.alt = ''; img.loading = 'lazy'; img.decoding = 'async'; |
| 52 | box.append(img); |
| 53 | } |
| 54 | const body = h('div', 'ovdk-card-body'); |
| 55 | const title = h(url ? 'a' : 'b', 'ovdk-card-t', cut(c.title, 120)); |
| 56 | if (url) { title.href = url; title.target = '_blank'; title.rel = 'noopener nofollow'; } |
| 57 | body.append(title); |
| 58 | if (typeof c.line === 'string' && c.line) body.append(h('span', 'ovdk-card-l', cut(c.line, 200))); |
| 59 | if (typeof c.note === 'string' && c.note) body.append(h('span', 'ovdk-card-l', cut(c.note, 160))); |
| 60 | const go = c.action && typeof c.action === 'object' && https(c.action.href) && typeof c.action.label === 'string' && c.action.label ? c.action : null; |
| 61 | if (go) { |
| 62 | const a = h('a', 'ovdk-card-go', cut(go.label, 40)); |
| 63 | a.href = https(go.href); a.target = '_blank'; a.rel = 'noopener nofollow'; |
| 64 | body.append(a); |
| 65 | } |
| 66 | box.append(body); |
| 67 | return box; |
| 68 | } |
| 69 | |
| 70 | root.OpenVibeDockCard = card; |
| 71 | })(typeof window !== 'undefined' ? window : globalThis); |
| 72 |