Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
184 changes: 184 additions & 0 deletions scripts/store-assets/shorts-demo.js
Original file line number Diff line number Diff line change
@@ -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 };
43 changes: 43 additions & 0 deletions shotkit.calibration.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
7 changes: 7 additions & 0 deletions store.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -263,4 +268,6 @@ module.exports = {
await page.waitForTimeout(1800);
},
},

demos: [createShortsDemo({ evalInContentWorld, lessonUrl: LESSON_URL })],
};
34 changes: 34 additions & 0 deletions tests/store-config.test.js
Original file line number Diff line number Diff line change
@@ -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'));
});
});