mirror of
https://github.com/obra/superpowers.git
synced 2026-06-29 05:59:04 +08:00
feat: add Alpine visual companion mockups
This commit is contained in:
@@ -193,6 +193,7 @@
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
<script defer src="/vendor/alpine.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
@@ -207,7 +208,7 @@
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
<span id="indicator-text">Interact with the mockup, then return to the terminal</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
const container = target.closest('.options') || target.closest('.cards');
|
||||
const selected = container ? container.querySelectorAll('.selected') : [];
|
||||
if (selected.length === 0) {
|
||||
indicator.textContent = 'Click an option above, then return to the terminal';
|
||||
indicator.textContent = 'Interact with the mockup, then return to the terminal';
|
||||
} else if (selected.length === 1) {
|
||||
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
|
||||
@@ -101,6 +101,26 @@ h1 { color: #333; } p { color: #666; }</style>
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
const ALPINE_VENDOR_PATH = path.join(__dirname, 'vendor', 'alpine.js');
|
||||
|
||||
function loadVendorFile(filePath, name) {
|
||||
try {
|
||||
return fs.readFileSync(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load vendored ${name} at ${filePath}; ` +
|
||||
'run the refresh command in skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md. ' +
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VENDOR_FILES = new Map([
|
||||
['/vendor/alpine.js', {
|
||||
content: loadVendorFile(ALPINE_VENDOR_PATH, 'Alpine'),
|
||||
contentType: 'application/javascript; charset=utf-8'
|
||||
}]
|
||||
]);
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
@@ -124,11 +144,30 @@ function getNewestScreen() {
|
||||
return files.length > 0 ? files[0].path : null;
|
||||
}
|
||||
|
||||
function parseRequestUrl(req) {
|
||||
// Vendor routing depends on URL normalization before exact pathname allowlist checks.
|
||||
return new URL(req.url, 'http://localhost');
|
||||
}
|
||||
|
||||
function serveVendorFile(requestUrl, res) {
|
||||
const vendorFile = VENDOR_FILES.get(requestUrl.pathname);
|
||||
if (!vendorFile) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': vendorFile.contentType });
|
||||
res.end(vendorFile.content);
|
||||
}
|
||||
|
||||
// ========== HTTP Request Handler ==========
|
||||
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const requestUrl = parseRequestUrl(req);
|
||||
|
||||
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
@@ -142,8 +181,10 @@ function handleRequest(req, res) {
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/vendor/')) {
|
||||
serveVendorFile(requestUrl, res);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/files/')) {
|
||||
const fileName = requestUrl.pathname.slice(7);
|
||||
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
|
||||
48
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md
vendored
Normal file
48
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Third-Party Notices
|
||||
|
||||
## Alpine.js
|
||||
|
||||
- Package: `alpinejs`
|
||||
- Version: `3.15.12`
|
||||
- Source: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- Vendored file: `package/dist/cdn.min.js`
|
||||
- Local path: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- SHA256: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
|
||||
Refresh command:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
License:
|
||||
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright © 2019-2025 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
5
skills/brainstorming/scripts/vendor/alpine.js
vendored
Normal file
5
skills/brainstorming/scripts/vendor/alpine.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
skills/brainstorming/scripts/vendor/alpine.provenance.json
vendored
Normal file
12
skills/brainstorming/scripts/vendor/alpine.provenance.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "alpinejs",
|
||||
"version": "3.15.12",
|
||||
"license": "MIT",
|
||||
"sourceUrl": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
|
||||
"sourcePackagePath": "package/dist/cdn.min.js",
|
||||
"localPath": "skills/brainstorming/scripts/vendor/alpine.js",
|
||||
"npmIntegrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
|
||||
"sha256": "57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f",
|
||||
"approvalArtifact": "SUP-215",
|
||||
"vendoredAt": "2026-05-08"
|
||||
}
|
||||
Reference in New Issue
Block a user