Skip to content

Commit 0e9afd8

Browse files
author
hatimhtm
committed
v2 overhaul: PWA, GitHub Pages deploy, custom dialog, markdown export
- Live demo: GitHub Pages auto-deploy via CI on every push to main. vite.config.js switched to base: './' so dist/ ships unchanged to any subdirectory deploy (Pages, S3, Netlify, Vercel). - PWA: manifest.webmanifest + offline-first service worker (cache-first for same-origin GET, network fallback). Installable to home screen on iPad. - Custom <dialog> modal system (src/dialog.js + dialog.css) replaces native confirm()/alert(). Focus-trapped, Esc-closes, themed to match. - Save-status indicator pill in toolbar ("Saved · 3s ago") with onSave listeners + 15s refresh ticker. - Markdown export of full deck (steps + cards + checklist) alongside the existing JSON export. HTML-to-MD conversion preserves headings, lists, blockquotes, and highlight spans. - Brutalist house-style README with light/dark hero banners, OG + Twitter cards, theme-color meta, inline SVG favicon, CI badge. - CI workflow: vite build + dist sanity check + Pages artifact + deploy job, all gated on main pushes.
1 parent f3283b8 commit 0e9afd8

12 files changed

Lines changed: 798 additions & 107 deletions

File tree

.github/workflows/ci.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
pages: write
16+
id-token: write
17+
18+
jobs:
19+
build:
20+
name: Vite build
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 10
23+
24+
steps:
25+
- uses: actions/checkout@v5
26+
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version: "20"
30+
cache: npm
31+
32+
- name: Install
33+
run: npm ci
34+
35+
- name: Build
36+
run: npm run build
37+
38+
- name: Confirm artifacts
39+
run: |
40+
test -f dist/index.html
41+
test -f dist/manifest.webmanifest
42+
test -f dist/sw.js
43+
test -d dist/assets
44+
echo "Bundle size: $(du -sh dist | cut -f1)"
45+
46+
- uses: actions/upload-artifact@v4
47+
with:
48+
name: dist
49+
path: dist/
50+
retention-days: 14
51+
52+
- name: Upload Pages artifact
53+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
54+
uses: actions/upload-pages-artifact@v3
55+
with:
56+
path: dist
57+
58+
deploy:
59+
name: Deploy to GitHub Pages
60+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
61+
needs: build
62+
runs-on: ubuntu-latest
63+
environment:
64+
name: github-pages
65+
url: ${{ steps.deployment.outputs.page_url }}
66+
steps:
67+
- name: Deploy
68+
id: deployment
69+
uses: actions/deploy-pages@v4

README.md

Lines changed: 121 additions & 95 deletions
Large diffs are not rendered by default.

assets-readme/hero-banner-dark.svg

Lines changed: 36 additions & 0 deletions
Loading

assets-readme/hero-banner.svg

Lines changed: 36 additions & 0 deletions
Loading

index.html

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,23 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
77
<title>ScriptDeck | Conversation Script Runner</title>
88
<meta name="description"
9-
content="Configurable step-by-step conversation script runner with response cards, rebuttals, and session analytics.">
9+
content="Keyboard-first conversation script runner — multi-step decks, categorised response cards, call timer, session analytics. Offline-first PWA.">
10+
<meta name="theme-color" content="#2563EB">
11+
12+
<!-- Open Graph -->
13+
<meta property="og:type" content="website">
14+
<meta property="og:title" content="ScriptDeck — Conversation Script Runner">
15+
<meta property="og:description" content="Step-by-step script runner with response cards, timer, and session analytics. Offline-first.">
16+
<!-- Twitter -->
17+
<meta name="twitter:card" content="summary">
18+
<meta name="twitter:title" content="ScriptDeck — Conversation Script Runner">
19+
<meta name="twitter:description" content="Step-by-step script runner with response cards, timer, and session analytics.">
20+
21+
<!-- Favicon — inline SVG gradient S -->
22+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' x2='1' y1='0' y2='1'%3E%3Cstop offset='0' stop-color='%232563EB'/%3E%3Cstop offset='1' stop-color='%2393C5FD'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Ctext x='32' y='44' font-family='Inter,sans-serif' font-size='34' font-weight='800' fill='white' text-anchor='middle'%3ES%3C/text%3E%3C/svg%3E">
23+
24+
<!-- PWA -->
25+
<link rel="manifest" href="manifest.webmanifest">
1026

1127
<!-- Fonts -->
1228
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -15,7 +31,7 @@
1531
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500&display=swap"
1632
rel="stylesheet">
1733

18-
<!-- Icons -->
34+
<!-- Icons — pinned for reproducibility -->
1935
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
2036

2137
<!-- Styles -->
@@ -25,6 +41,7 @@
2541
<link rel="stylesheet" href="/src/styles/script.css">
2642
<link rel="stylesheet" href="/src/styles/rebuttals.css">
2743
<link rel="stylesheet" href="/src/styles/sidebar.css">
44+
<link rel="stylesheet" href="/src/styles/dialog.css">
2845
</head>
2946

3047
<body data-theme="light">
@@ -48,14 +65,18 @@
4865
</div>
4966

5067
<div class="toolbar-right">
51-
<button id="editBtn" class="icon-btn" title="Edit Script">
68+
<span id="saveIndicator" class="save-indicator" aria-live="polite" title="Auto-save status">
69+
<i class="fa-solid fa-cloud-check"></i><span id="saveLabel">Saved</span>
70+
</span>
71+
<div class="toolbar-divider"></div>
72+
<button id="editBtn" class="icon-btn" title="Edit Script (E)">
5273
<i class="fa-solid fa-pen"></i>
5374
<span>Edit Script</span>
5475
</button>
55-
<button id="undoBtn" class="icon-btn disabled" title="Undo">
76+
<button id="undoBtn" class="icon-btn disabled" title="Undo (⌘Z)">
5677
<i class="fa-solid fa-rotate-left"></i>
5778
</button>
58-
<button id="redoBtn" class="icon-btn disabled" title="Redo">
79+
<button id="redoBtn" class="icon-btn disabled" title="Redo (⌘⇧Z)">
5980
<i class="fa-solid fa-rotate-right"></i>
6081
</button>
6182
<div class="toolbar-divider"></div>
@@ -69,9 +90,12 @@
6990
<button id="importBtn" class="icon-btn" title="Load Script JSON">
7091
<i class="fa-solid fa-file-import"></i>
7192
</button>
72-
<button id="exportBtn" class="icon-btn" title="Export Script JSON">
93+
<button id="exportBtn" class="icon-btn" title="Export as JSON">
7394
<i class="fa-solid fa-file-export"></i>
7495
</button>
96+
<button id="exportMdBtn" class="icon-btn" title="Export as Markdown">
97+
<i class="fa-solid fa-file-lines"></i>
98+
</button>
7599
<button id="themeBtn" class="icon-btn" title="Toggle Theme">
76100
<i class="fa-solid fa-moon"></i>
77101
</button>
@@ -165,6 +189,15 @@ <h3><i class="fa-solid fa-clock-rotate-left"></i> Session History</h3>
165189

166190
<!-- App Entry -->
167191
<script type="module" src="/src/main.js"></script>
192+
193+
<!-- Service Worker (offline-first) -->
194+
<script>
195+
if ('serviceWorker' in navigator) {
196+
window.addEventListener('load', () => {
197+
navigator.serviceWorker.register('sw.js').catch(() => { });
198+
});
199+
}
200+
</script>
168201
</body>
169202

170-
</html>
203+
</html>

public/manifest.webmanifest

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "ScriptDeck — Conversation Script Runner",
3+
"short_name": "ScriptDeck",
4+
"description": "Keyboard-first conversation script runner — steps, response cards, session analytics, all offline.",
5+
"start_url": "./",
6+
"scope": "./",
7+
"display": "standalone",
8+
"orientation": "any",
9+
"background_color": "#F8FAFC",
10+
"theme_color": "#2563EB",
11+
"icons": [
12+
{
13+
"src": "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 192'><defs><linearGradient id='g' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%232563EB'/><stop offset='1' stop-color='%2393C5FD'/></linearGradient></defs><rect width='192' height='192' rx='40' fill='url(%23g)'/><text x='96' y='130' font-family='Helvetica' font-size='110' font-weight='900' fill='white' text-anchor='middle'>S</text></svg>",
14+
"sizes": "192x192",
15+
"type": "image/svg+xml",
16+
"purpose": "any maskable"
17+
},
18+
{
19+
"src": "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><defs><linearGradient id='g' x1='0' x2='1' y1='0' y2='1'><stop offset='0' stop-color='%232563EB'/><stop offset='1' stop-color='%2393C5FD'/></linearGradient></defs><rect width='512' height='512' rx='110' fill='url(%23g)'/><text x='256' y='350' font-family='Helvetica' font-size='300' font-weight='900' fill='white' text-anchor='middle'>S</text></svg>",
20+
"sizes": "512x512",
21+
"type": "image/svg+xml",
22+
"purpose": "any maskable"
23+
}
24+
]
25+
}

public/sw.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* ScriptDeck — Service Worker
2+
* Caches the app shell so the deck works without network after first load.
3+
* Strategy: cache-first for same-origin GET, network-fallback, ignore others.
4+
*/
5+
6+
const CACHE = 'scriptdeck-v1';
7+
8+
self.addEventListener('install', (e) => {
9+
self.skipWaiting();
10+
});
11+
12+
self.addEventListener('activate', (e) => {
13+
e.waitUntil(
14+
caches.keys().then((keys) =>
15+
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
16+
)
17+
);
18+
self.clients.claim();
19+
});
20+
21+
self.addEventListener('fetch', (e) => {
22+
const req = e.request;
23+
if (req.method !== 'GET') return;
24+
const url = new URL(req.url);
25+
if (url.origin !== self.location.origin) return;
26+
27+
e.respondWith(
28+
caches.match(req).then((cached) => {
29+
const network = fetch(req)
30+
.then((res) => {
31+
if (res && res.ok && res.type === 'basic') {
32+
const copy = res.clone();
33+
caches.open(CACHE).then((c) => c.put(req, copy));
34+
}
35+
return res;
36+
})
37+
.catch(() => cached);
38+
return cached || network;
39+
})
40+
);
41+
});

0 commit comments

Comments
 (0)