diff --git a/.gitignore b/.gitignore index 024ac00..5d1935e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,12 @@ playwright/.cache/ # Generated store-listing copy (source of truth: store-assets/STORE_LISTING.md) store-assets/description.md +store-assets/captions.json +store-assets/shotkit-manifest.json +store-assets/shotkit-approval.json +store-assets/storyboard.json +store-assets/schemas/ +store-assets/*-thumbnail.png # Stray files in root /content.css diff --git a/scripts/store-assets/shorts-demo.js b/scripts/store-assets/shorts-demo.js new file mode 100644 index 0000000..ed60f7c --- /dev/null +++ b/scripts/store-assets/shorts-demo.js @@ -0,0 +1,184 @@ +const FOCUS_COLUMN_CSS = ` + html, body { background: #f1f3f5 !important; } + body { overflow-x: hidden !important; } + .site-header { + position: relative !important; + height: 100px !important; + padding: 38px 42px 14px !important; + } + .site-header .logo { gap: 12px !important; font-size: 20px !important; } + .site-header .logo .dot { width: 28px !important; height: 28px !important; } + #header-right { gap: 0 !important; margin-right: 108px !important; } + .header-links-container, #si18n-dark-toggle { display: none !important; } + #si18n-header-lang { + display: flex !important; + align-items: center !important; + gap: 12px !important; + } + #si18n-header-lang::before { + content: 'SkillBridge'; + color: #f2cc8f; + font: 800 14px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + } + #si18n-header-lang select { + min-width: 132px !important; + height: 42px !important; + padding: 7px 34px 7px 14px !important; + border-radius: 6px !important; + font-size: 16px !important; + font-weight: 700 !important; + } + .layout { + display: block !important; + width: 100% !important; + max-width: none !important; + margin: 0 !important; + } + .layout > aside { display: none !important; } + #lesson-main { + min-height: 1180px !important; + padding: 44px 52px 510px !important; + background: #f8f9fa !important; + } + #lesson-main .eyebrow { font-size: 15px !important; } + #lesson-main h1 { + margin: 8px 0 26px !important; + font-size: 42px !important; + line-height: 1.13 !important; + } + #lesson-main h2 { + margin: 28px 0 12px !important; + font-size: 28px !important; + line-height: 1.25 !important; + } + #lesson-main p { + max-width: none !important; + margin-bottom: 18px !important; + font-size: 22px !important; + line-height: 1.58 !important; + } + #lesson-main ul { + max-width: none !important; + padding-left: 28px !important; + font-size: 20px !important; + line-height: 1.7 !important; + } + #lesson-main pre, + #lesson-main .progress, + #skillbridge-root, + #si18n-term-preview, + #si18n-welcome-banner { display: none !important; } + #__shotkit_badge__ { + top: 8px !important; + left: 42px !important; + max-width: none !important; + padding: 2px 0 !important; + border-radius: 0 !important; + background: transparent !important; + color: rgba(255,255,255,.82) !important; + box-shadow: none !important; + font-size: 14px !important; + } +`; + +const SHORTS_LAYOUTS = Object.freeze({ + 'focus-column': FOCUS_COLUMN_CSS, + 'compact-column': `${FOCUS_COLUMN_CSS} + #lesson-main { padding: 32px 44px 430px !important; } + #lesson-main h1 { margin-bottom: 20px !important; font-size: 36px !important; } + #lesson-main h2 { margin: 22px 0 9px !important; font-size: 24px !important; } + #lesson-main p { margin-bottom: 14px !important; font-size: 20px !important; line-height: 1.46 !important; } + #lesson-main ul { font-size: 18px !important; line-height: 1.55 !important; } + `, +}); + +function resolveShortsLayout(layoutPreset = 'focus-column') { + return SHORTS_LAYOUTS[layoutPreset] || SHORTS_LAYOUTS['focus-column']; +} + +function installVerticalLayout(css) { + const install = () => { + if (document.getElementById('__skillbridge_shorts_layout__')) return; + const style = document.createElement('style'); + style.id = '__skillbridge_shorts_layout__'; + style.textContent = css; + document.head.appendChild(style); + }; + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', install, { once: true }); + } else { + install(); + } +} + +async function waitUntil(page, startedAt, atMs) { + const remaining = atMs - (Date.now() - startedAt); + if (remaining > 0) await page.waitForTimeout(remaining); +} + +function createShortsDemo({ evalInContentWorld, lessonUrl }) { + return { + name: 'demo-skillbridge', + targets: ['youtube-shorts'], + disclaimer: 'Unofficial · independent project', + trim: { start: 2.3, duration: 21.7 }, + thumbnail: { at: 3.5 }, + captionOptions: { + mode: 'focus', + appearance: 'outline', + position: 'bottom-left', + bottomOffset: 425, + wordsPerChunk: 2, + wordMs: 390, + activeColor: '#ffd43b', + }, + captions: [ + { at: 2.5, text: 'Translate AI lessons' }, + { at: 6.2, text: 'Korean in one click' }, + { at: 8, text: 'AI terms stay intact' }, + { at: 11, text: 'Restore anytime' }, + { at: 17, text: 'Original stays untouched' }, + { at: 20, text: 'SkillBridge removes language barriers' }, + ], + async run({ page, context, baseUrl, demo, calibration }) { + const startedAt = Date.now(); + await context.addInitScript(installVerticalLayout, resolveShortsLayout(calibration && calibration.layoutPreset)); + await page.goto(`${baseUrl}${lessonUrl}`, { waitUntil: 'networkidle' }); + await evalInContentWorld(context, 'suppressOnboarding'); + + await waitUntil(page, startedAt, 3_800); + await demo.select('#si18n-header-lang-select', 'ko', { + moveMs: 360, + beforeMs: 80, + openMs: 650, + holdMs: 450, + maxOptions: 7, + }); + await page.waitForFunction( + () => /[\uac00-\ud7a3]/.test(document.querySelector('#p-1')?.textContent || ''), + null, + { timeout: 15_000 }, + ); + await evalInContentWorld(context, 'cleanForCapture'); + + await waitUntil(page, startedAt, 12_200); + await demo.select('#si18n-header-lang-select', 'en', { + moveMs: 360, + beforeMs: 80, + openMs: 650, + holdMs: 450, + maxOptions: 7, + }); + await page.waitForFunction( + () => /This lesson covers prompt engineering/.test(document.querySelector('#p-1')?.textContent || ''), + null, + { timeout: 15_000 }, + ); + await evalInContentWorld(context, 'cleanForCapture'); + await waitUntil(page, startedAt, 24_200); + await demo.hidePointer(); + }, + }; +} + +module.exports = { SHORTS_LAYOUTS, createShortsDemo, resolveShortsLayout }; diff --git a/shotkit.calibration.json b/shotkit.calibration.json new file mode 100644 index 0000000..3d1d612 --- /dev/null +++ b/shotkit.calibration.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "profiles": { + "demo-skillbridge": { + "youtube-shorts": { + "layoutPreset": "focus-column", + "framing": { + "scale": 1, + "focusX": 0.5, + "focusY": 0.5 + }, + "captionOptions": { + "position": "bottom-left", + "appearance": "outline", + "bottomOffset": 425 + }, + "protectedRegions": [ + { + "id": "lesson-result", + "label": "Lesson result", + "x": 36, + "y": 110, + "width": 648, + "height": 610 + }, + { + "id": "language-control", + "label": "Language control", + "x": 400, + "y": 28, + "width": 220, + "height": 82 + } + ], + "verification": { + "profileHash": "c59ceba30749fd64fa29bdd701cf4dd42e8f60ba7bcaa2c3a8b6cd8680d7f8da", + "status": "publish-ready", + "verifiedAt": "2026-07-10T14:47:57.139Z" + } + } + } + } +} diff --git a/store.config.js b/store.config.js index df06a9e..406d5e4 100644 --- a/store.config.js +++ b/store.config.js @@ -29,6 +29,7 @@ const path = require('path'); const { evalInContentWorld, makePatchedExtension } = require('./tests/e2e/helpers/extension'); const { registerStubs, GT_KO, buildGTResponse } = require('./tests/e2e/helpers/network-stubs'); +const { createShortsDemo } = require('./scripts/store-assets/shorts-demo'); const FIXTURES = path.join(__dirname, 'store-assets', 'fixtures'); const TEMPLATES = path.join(__dirname, 'store-assets', 'templates'); @@ -82,6 +83,10 @@ module.exports = { outDir: 'store-assets', disclaimer: DISCLAIMER, description: { from: 'store-assets/STORE_LISTING.md' }, + calibration: { + from: 'shotkit.calibration.json', + layouts: ['focus-column', 'compact-column'], + }, // dist/bundled (copied + manifest widened to localhost + streaming Puter stub). prepareExtension: () => makePatchedExtension(), @@ -263,4 +268,6 @@ module.exports = { await page.waitForTimeout(1800); }, }, + + demos: [createShortsDemo({ evalInContentWorld, lessonUrl: LESSON_URL })], }; diff --git a/tests/store-config.test.js b/tests/store-config.test.js new file mode 100644 index 0000000..cfe04f1 --- /dev/null +++ b/tests/store-config.test.js @@ -0,0 +1,34 @@ +const config = require('../store.config'); +const { resolveShortsLayout } = require('../scripts/store-assets/shorts-demo'); + +describe('store asset config', () => { + test('keeps a dedicated publish-ready Shorts story', () => { + const shorts = config.demos.find((demo) => demo.name === 'demo-skillbridge'); + + expect(shorts).toMatchObject({ + targets: ['youtube-shorts'], + disclaimer: 'Unofficial · independent project', + trim: { start: 2.3, duration: 21.7 }, + thumbnail: { at: 3.5 }, + captionOptions: { + mode: 'focus', + appearance: 'outline', + position: 'bottom-left', + bottomOffset: 425, + }, + }); + expect(shorts.captions[0]).toEqual({ at: 2.5, text: 'Translate AI lessons' }); + expect(shorts.captions.map((caption) => caption.text).join(' ')).toMatch(/Restore|Original/); + expect(shorts.run).toEqual(expect.any(Function)); + }); + + test('declares constrained calibration layouts consumed by the Shorts story', () => { + expect(config.calibration).toEqual({ + from: 'shotkit.calibration.json', + layouts: ['focus-column', 'compact-column'], + }); + expect(resolveShortsLayout('focus-column')).toContain('font-size: 42px'); + expect(resolveShortsLayout('compact-column')).toContain('font-size: 36px'); + expect(resolveShortsLayout('unknown')).toBe(resolveShortsLayout('focus-column')); + }); +});