diff --git a/public/views/aiusage.js b/public/views/aiusage.js new file mode 100644 index 0000000..b8d944b --- /dev/null +++ b/public/views/aiusage.js @@ -0,0 +1,6 @@ +// public/views/aiusage.js — #/ai-usage (phuryn claude-usage) +import { embedView } from './embed.js'; +export const render = embedView({ + title: 'AI Usage', sub: 'claude code · token usage', + src: 'https://aiusage.hynesy.com/' +}); diff --git a/public/views/embed.js b/public/views/embed.js new file mode 100644 index 0000000..921270d --- /dev/null +++ b/public/views/embed.js @@ -0,0 +1,21 @@ +// Shared cross-origin app embed: a Void header bar + a full-height iframe. +// Reuses the Terminal tab's themed classes (.term-bar/.term-frame/.ghost). +// The embedded app lives at its own HTTPS origin, so visiting that origin +// directly shows no Void chrome — there is no "back to Void" affordance here. +import { el, mount } from '../dom.js'; + +export function embedView({ title, sub, src, allow }) { + return async function render(main) { + mount(main, + el('div', { class: 'term-bar' }, + el('span', { class: 'term-title' }, '◆ ' + title), + sub ? el('span', { class: 'muted', style: { fontSize: '11px' } }, sub) : null, + el('a', { class: 'ghost', style: { marginLeft: 'auto' }, href: src, target: '_blank', rel: 'noopener' }, '↗ Open'), + el('button', { class: 'ghost', onclick: () => { + const f = document.getElementById('embed-frame'); if (f) f.src = f.src; + } }, '⟳ Reload') + ), + el('iframe', { id: 'embed-frame', src, class: 'term-frame', ...(allow ? { allow } : {}) }) + ); + }; +} diff --git a/public/views/timelapse.js b/public/views/timelapse.js new file mode 100644 index 0000000..6116be3 --- /dev/null +++ b/public/views/timelapse.js @@ -0,0 +1,6 @@ +// public/views/timelapse.js — #/timelapse +import { embedView } from './embed.js'; +export const render = embedView({ + title: 'Timelapse', sub: 'farm · 4K timelapse', + src: 'https://timelapse.hynesy.com/', allow: 'fullscreen' +}); diff --git a/tests/frontend/embed.test.js b/tests/frontend/embed.test.js new file mode 100644 index 0000000..43df36f --- /dev/null +++ b/tests/frontend/embed.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { JSDOM } from 'jsdom'; + +beforeAll(() => { + const dom = new JSDOM('
', { url: 'http://localhost/' }); + global.window = dom.window; global.document = dom.window.document; + global.Node = dom.window.Node; global.location = dom.window.location; +}); +afterAll(() => { delete global.window; delete global.document; delete global.Node; delete global.location; }); + +describe('embedView + wrappers', () => { + it('mounts an iframe with the given src + matching Open link', async () => { + const { embedView } = await import('../../public/views/embed.js'); + const main = document.getElementById('main'); + await embedView({ title: 'Timelapse', sub: 'x', src: 'https://timelapse.hynesy.com/', allow: 'fullscreen' })(main); + const frame = main.querySelector('iframe.term-frame'); + expect(frame.getAttribute('src')).toBe('https://timelapse.hynesy.com/'); + expect(frame.getAttribute('allow')).toBe('fullscreen'); + expect(main.querySelector('a.ghost').getAttribute('href')).toBe('https://timelapse.hynesy.com/'); + expect(main.querySelector('.term-title').textContent).toContain('Timelapse'); + }); + + it('timelapse wrapper points at timelapse.hynesy.com', async () => { + const main = document.getElementById('main'); + await (await import('../../public/views/timelapse.js')).render(main); + expect(main.querySelector('iframe.term-frame').getAttribute('src')).toBe('https://timelapse.hynesy.com/'); + }); + + it('aiusage wrapper points at aiusage.hynesy.com', async () => { + const main = document.getElementById('main'); + await (await import('../../public/views/aiusage.js')).render(main); + expect(main.querySelector('iframe.term-frame').getAttribute('src')).toBe('https://aiusage.hynesy.com/'); + }); +});