playable-lessons 0.2.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * AI-collaboration task export formats, shared by the renderer and the CLI.
4
+ *
5
+ * Standalone (no runtime imports from sibling shared modules) — only the
6
+ * AiTaskResult *type* is imported, which is erased at compile time, so it
7
+ * resolves under both bundler (renderer) and NodeNext (CLI) module resolution.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.toPlainText = toPlainText;
11
+ exports.toStandaloneHTML = toStandaloneHTML;
12
+ const favicon_js_1 = require("./favicon.js");
13
+ /**
14
+ * A printable task sheet: for each task, the scenario, student brief,
15
+ * deliverable, load-bearing specifics (with why a generic answer misses them),
16
+ * the engagement-anchored rubric, and why a delegating learner loses.
17
+ */
18
+ function toPlainText(result, title) {
19
+ const heading = result.title || title || 'AI-Collaboration Tasks';
20
+ const lines = [heading, ''];
21
+ result.tasks.forEach((t, i) => {
22
+ lines.push(`Task ${i + 1}`);
23
+ lines.push(`Scenario: ${t.scenario}`);
24
+ lines.push(`Brief: ${t.brief}`);
25
+ lines.push(`Deliverable: ${t.deliverable}`);
26
+ lines.push('Load-bearing specifics:');
27
+ t.loadBearingSpecifics.forEach((s) => {
28
+ lines.push(` - ${s.detail} — ${s.whyGenericAnswersMissIt}`);
29
+ });
30
+ lines.push('Rubric (engagement-anchored):');
31
+ t.rubric.forEach((r) => {
32
+ lines.push(` - ${r.criterion}: ${r.description}`);
33
+ });
34
+ lines.push(`Why it works: ${t.whyItWorks}`);
35
+ lines.push('');
36
+ });
37
+ return lines.join('\n') + '\n';
38
+ }
39
+ /**
40
+ * A self-contained, offline HTML task sheet. All text is rendered via
41
+ * textContent (no innerHTML); the embedded JSON is guarded against breaking out
42
+ * of its <script> by escaping '<' to '\u003c'.
43
+ */
44
+ function toStandaloneHTML(result, title) {
45
+ const pageTitle = result.title || title;
46
+ const dataJson = JSON.stringify(result).replace(/</g, '\\u003c');
47
+ const safeTitle = String(pageTitle)
48
+ .replace(/&/g, '&amp;')
49
+ .replace(/</g, '&lt;')
50
+ .replace(/>/g, '&gt;')
51
+ .replace(/"/g, '&quot;')
52
+ .replace(/'/g, '&#39;');
53
+ return `<!DOCTYPE html>
54
+ <html lang="en">
55
+ <head>
56
+ <meta charset="UTF-8">
57
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
58
+ ${favicon_js_1.FAVICON_LINK}
59
+ <title>${safeTitle} — AI-Collaboration Tasks</title>
60
+ <style>
61
+ * { margin: 0; padding: 0; box-sizing: border-box; }
62
+ body { font-family: Georgia, 'Times New Roman', serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; justify-content: center; padding: 40px 20px; line-height: 1.7; }
63
+ #sheet { max-width: 760px; width: 100%; }
64
+ h1 { font-size: 26px; color: #a78bfa; margin-bottom: 8px; }
65
+ .lede { font-size: 14px; color: #888; margin-bottom: 28px; }
66
+ .task { background: #252547; border: 1px solid #3a3a5c; border-radius: 14px; padding: 24px 26px; margin-bottom: 22px; }
67
+ .task-no { font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; color: #a78bfa; font-weight: 700; margin-bottom: 6px; }
68
+ h2 { font-size: 16px; color: #a78bfa; margin: 18px 0 6px; text-transform: uppercase; letter-spacing: 0.04em; }
69
+ p { font-size: 16px; color: #cfcfe6; }
70
+ .brief { font-size: 17px; background: rgba(167,139,250,0.08); border-left: 3px solid #a78bfa; padding: 12px 14px; border-radius: 0 6px 6px 0; }
71
+ ul { list-style: none; }
72
+ ul li { padding: 6px 0 6px 18px; position: relative; font-size: 15px; color: #cfcfe6; }
73
+ ul li::before { content: '•'; position: absolute; left: 0; color: #a78bfa; }
74
+ .why { font-size: 14px; color: #6b8; font-style: italic; }
75
+ .specifics li .why, .rubric li .why { display: block; color: #9aa; font-size: 14px; }
76
+ .works { font-size: 15px; color: #c4b5fd; font-style: italic; margin-top: 6px; }
77
+ .meta { text-align: center; color: #555; font-size: 12px; margin-top: 32px; }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <div id="sheet">
82
+ <h1 id="title"></h1>
83
+ <p class="lede">The learner works iteratively with a chatbot; interrogating it — not pasting its first answer — is what produces a good result.</p>
84
+ <div id="tasks"></div>
85
+ </div>
86
+ <script>
87
+ (function () {
88
+ var r = ${dataJson};
89
+ document.getElementById('title').textContent = r.title || 'AI-Collaboration Tasks';
90
+ var host = document.getElementById('tasks');
91
+ (r.tasks || []).forEach(function (t, i) {
92
+ var card = document.createElement('div');
93
+ card.className = 'task';
94
+
95
+ var no = document.createElement('div');
96
+ no.className = 'task-no';
97
+ no.textContent = 'Task ' + (i + 1);
98
+ card.appendChild(no);
99
+
100
+ var h = function (label) { var e = document.createElement('h2'); e.textContent = label; return e; };
101
+ var para = function (txt, cls) { var e = document.createElement('p'); e.textContent = txt; if (cls) e.className = cls; return e; };
102
+
103
+ card.appendChild(h('Scenario'));
104
+ card.appendChild(para(t.scenario));
105
+ card.appendChild(h('Your brief'));
106
+ card.appendChild(para(t.brief, 'brief'));
107
+ card.appendChild(h('Deliverable'));
108
+ card.appendChild(para(t.deliverable));
109
+
110
+ if (t.loadBearingSpecifics && t.loadBearingSpecifics.length) {
111
+ card.appendChild(h('Load-bearing specifics'));
112
+ var ul = document.createElement('ul');
113
+ ul.className = 'specifics';
114
+ t.loadBearingSpecifics.forEach(function (s) {
115
+ var li = document.createElement('li');
116
+ var main = document.createElement('span');
117
+ main.textContent = s.detail;
118
+ var why = document.createElement('span');
119
+ why.className = 'why';
120
+ why.textContent = 'Generic answers miss it: ' + s.whyGenericAnswersMissIt;
121
+ li.appendChild(main);
122
+ li.appendChild(why);
123
+ ul.appendChild(li);
124
+ });
125
+ card.appendChild(ul);
126
+ }
127
+
128
+ if (t.rubric && t.rubric.length) {
129
+ card.appendChild(h('Rubric (engagement-anchored)'));
130
+ var rl = document.createElement('ul');
131
+ rl.className = 'rubric';
132
+ t.rubric.forEach(function (rb) {
133
+ var li = document.createElement('li');
134
+ var main = document.createElement('span');
135
+ main.textContent = rb.criterion;
136
+ var why = document.createElement('span');
137
+ why.className = 'why';
138
+ why.textContent = rb.description;
139
+ li.appendChild(main);
140
+ li.appendChild(why);
141
+ rl.appendChild(li);
142
+ });
143
+ card.appendChild(rl);
144
+ }
145
+
146
+ card.appendChild(h('Why it works'));
147
+ var w = document.createElement('p');
148
+ w.className = 'works';
149
+ w.textContent = t.whyItWorks;
150
+ card.appendChild(w);
151
+
152
+ host.appendChild(card);
153
+ });
154
+
155
+ var meta = document.createElement('div');
156
+ meta.className = 'meta';
157
+ meta.textContent = 'Created with Playable Lessons';
158
+ document.getElementById('sheet').appendChild(meta);
159
+ })();
160
+ </script>
161
+ </body>
162
+ </html>`;
163
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Case-study export formats, shared by the renderer and the CLI.
4
+ *
5
+ * Standalone (no runtime imports from sibling shared modules) — only the
6
+ * CaseStudyResult *type* is imported, which is erased at compile time, so it
7
+ * resolves under both bundler (renderer) and NodeNext (CLI) module resolution.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.toPlainText = toPlainText;
11
+ exports.toStandaloneHTML = toStandaloneHTML;
12
+ const favicon_js_1 = require("./favicon.js");
13
+ /** A printable case study: narrative (if present) then the structured fields. */
14
+ function toPlainText(result, title) {
15
+ const cs = result.caseStudy;
16
+ const heading = result.title || title || 'Case Study';
17
+ const lines = [heading, ''];
18
+ if (cs.narrative) {
19
+ lines.push(cs.narrative, '');
20
+ }
21
+ if (cs.protagonist)
22
+ lines.push(`Protagonist: ${cs.protagonist}`);
23
+ if (cs.situation)
24
+ lines.push(`Situation: ${cs.situation}`);
25
+ if (cs.keyFacts.length) {
26
+ lines.push('Key facts:');
27
+ cs.keyFacts.forEach((f) => lines.push(` - ${f}`));
28
+ }
29
+ if (cs.conflict)
30
+ lines.push(`Conflict: ${cs.conflict}`);
31
+ if (cs.decisionPoints.length) {
32
+ lines.push('Decision points:');
33
+ cs.decisionPoints.forEach((d) => lines.push(` - ${d}`));
34
+ }
35
+ if (cs.discussionQuestions.length) {
36
+ lines.push('Discussion questions:');
37
+ cs.discussionQuestions.forEach((q) => lines.push(` - ${q}`));
38
+ }
39
+ return lines.join('\n') + '\n';
40
+ }
41
+ /**
42
+ * A self-contained, offline HTML case study. Renders the narrative (if present)
43
+ * as prose, then structured sections. Text via textContent (no innerHTML); the
44
+ * embedded JSON is guarded against breaking out of its <script> by escaping
45
+ * '<' to '\u003c'.
46
+ */
47
+ function toStandaloneHTML(result, title) {
48
+ const pageTitle = result.title || title;
49
+ const dataJson = JSON.stringify(result.caseStudy).replace(/</g, '\\u003c');
50
+ const safeTitle = String(pageTitle)
51
+ .replace(/&/g, '&amp;')
52
+ .replace(/</g, '&lt;')
53
+ .replace(/>/g, '&gt;')
54
+ .replace(/"/g, '&quot;')
55
+ .replace(/'/g, '&#39;');
56
+ return `<!DOCTYPE html>
57
+ <html lang="en">
58
+ <head>
59
+ <meta charset="UTF-8">
60
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
61
+ ${favicon_js_1.FAVICON_LINK}
62
+ <title>${safeTitle} — Case Study</title>
63
+ <style>
64
+ * { margin: 0; padding: 0; box-sizing: border-box; }
65
+ body { font-family: Georgia, 'Times New Roman', serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; justify-content: center; padding: 40px 20px; line-height: 1.7; }
66
+ #sheet { max-width: 740px; width: 100%; }
67
+ h1 { font-size: 28px; color: #a78bfa; margin-bottom: 18px; }
68
+ .narrative { font-size: 17px; color: #cfcfe6; margin-bottom: 24px; white-space: pre-wrap; }
69
+ h2 { font-size: 15px; color: #a78bfa; margin: 22px 0 8px; text-transform: uppercase; letter-spacing: 0.05em; }
70
+ .field { font-size: 16px; color: #cfcfe6; margin-bottom: 10px; }
71
+ ul { list-style: none; }
72
+ ul li { padding: 5px 0 5px 18px; position: relative; font-size: 16px; color: #cfcfe6; }
73
+ ul li::before { content: '•'; position: absolute; left: 0; color: #a78bfa; }
74
+ .meta { text-align: center; color: #555; font-size: 12px; margin-top: 36px; }
75
+ </style>
76
+ </head>
77
+ <body>
78
+ <div id="sheet">
79
+ <h1 id="title"></h1>
80
+ <div id="body"></div>
81
+ </div>
82
+ <script>
83
+ (function () {
84
+ var cs = ${dataJson};
85
+ document.getElementById('title').textContent = cs.title || 'Case Study';
86
+ var body = document.getElementById('body');
87
+
88
+ function para(txt, cls) { var e = document.createElement('p'); e.textContent = txt; if (cls) e.className = cls; return e; }
89
+ function heading(label) { var e = document.createElement('h2'); e.textContent = label; return e; }
90
+ function list(items) {
91
+ var ul = document.createElement('ul');
92
+ items.forEach(function (it) { var li = document.createElement('li'); li.textContent = it; ul.appendChild(li); });
93
+ return ul;
94
+ }
95
+
96
+ if (cs.narrative) {
97
+ var n = document.createElement('div');
98
+ n.className = 'narrative';
99
+ n.textContent = cs.narrative;
100
+ body.appendChild(n);
101
+ }
102
+ if (cs.protagonist) { body.appendChild(heading('Protagonist')); body.appendChild(para(cs.protagonist, 'field')); }
103
+ if (cs.situation) { body.appendChild(heading('Situation')); body.appendChild(para(cs.situation, 'field')); }
104
+ if (cs.keyFacts && cs.keyFacts.length) { body.appendChild(heading('Key facts')); body.appendChild(list(cs.keyFacts)); }
105
+ if (cs.conflict) { body.appendChild(heading('The conflict')); body.appendChild(para(cs.conflict, 'field')); }
106
+ if (cs.decisionPoints && cs.decisionPoints.length) { body.appendChild(heading('Decision points')); body.appendChild(list(cs.decisionPoints)); }
107
+ if (cs.discussionQuestions && cs.discussionQuestions.length) { body.appendChild(heading('Discussion questions')); body.appendChild(list(cs.discussionQuestions)); }
108
+
109
+ var meta = document.createElement('div');
110
+ meta.className = 'meta';
111
+ meta.textContent = 'Created with Playable Lessons';
112
+ document.getElementById('sheet').appendChild(meta);
113
+ })();
114
+ </script>
115
+ </body>
116
+ </html>`;
117
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FAVICON_LINK = void 0;
4
+ // App icon as an inline SVG, embedded as a data-URI favicon link for every
5
+ // standalone HTML export — so downloaded story/quiz/deck files show the app
6
+ // icon in the browser tab even when opened offline from disk.
7
+ const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#2b2b52"/><stop offset="1" stop-color="#17172b"/></linearGradient><linearGradient id="pg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f2f0ff"/><stop offset="1" stop-color="#c9c2f2"/></linearGradient></defs><rect width="512" height="512" rx="116" fill="url(#bg)"/><path d="M256 148 C 214 116 150 108 104 122 L 104 372 C 150 358 214 366 256 398 Z" fill="url(#pg)"/><path d="M256 148 C 298 116 362 108 408 122 L 408 372 C 362 358 298 366 256 398 Z" fill="url(#pg)" opacity="0.92"/><rect x="249" y="140" width="14" height="264" rx="7" fill="#7c6ef0"/><path d="M219 210 L219 338 L331 274 Z" fill="#7c6ef0" stroke="#17172b" stroke-width="10" stroke-linejoin="round" paint-order="stroke"/></svg>`;
8
+ exports.FAVICON_LINK = `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}">`;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Flashcard export formats, shared by the renderer and the CLI.
4
+ *
5
+ * Standalone (no runtime imports from sibling shared modules) — it only needs
6
+ * the FlashcardResult *type*, which is erased at compile time, so it resolves
7
+ * cleanly under both bundler (renderer) and NodeNext (CLI) module resolution.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.toCSV = toCSV;
11
+ exports.toAnkiTSV = toAnkiTSV;
12
+ exports.toStandaloneHTML = toStandaloneHTML;
13
+ const favicon_js_1 = require("./favicon.js");
14
+ /** Quote a CSV field per RFC 4180 when it contains a comma, quote, or newline. */
15
+ function csvField(value) {
16
+ const needsQuote = /[",\n\r]/.test(value);
17
+ const escaped = value.replace(/"/g, '""');
18
+ return needsQuote ? `"${escaped}"` : escaped;
19
+ }
20
+ /** CSV with a header row: front,back,hint,tag (imports into Anki, Quizlet, Excel). */
21
+ function toCSV(result) {
22
+ const header = 'front,back,hint,tag';
23
+ const rows = result.cards.map((c) => [csvField(c.front), csvField(c.back), csvField(c.hint ?? ''), csvField(c.tag ?? '')].join(','));
24
+ return [header, ...rows].join('\n') + '\n';
25
+ }
26
+ /**
27
+ * Anki-importable TSV: three columns — front <TAB> back <TAB> tag (empty when a
28
+ * card has no tag). Anki renders fields as HTML, so newlines become <br> and
29
+ * tabs (which would split columns) are collapsed to spaces. Hints are omitted
30
+ * here — use CSV or the HTML deck for hints.
31
+ */
32
+ function toAnkiTSV(result) {
33
+ const clean = (s) => s.replace(/\t/g, ' ').replace(/\r?\n/g, '<br>');
34
+ return (result.cards
35
+ .map((c) => [clean(c.front), clean(c.back), clean(c.tag ?? '')].join('\t'))
36
+ .join('\n') + '\n');
37
+ }
38
+ /**
39
+ * A self-contained, offline HTML flip-card deck: click/Space to flip, arrow keys
40
+ * to navigate, plus shuffle and restart. Card text is rendered via textContent
41
+ * (no innerHTML), and the embedded card JSON is guarded against breaking out of
42
+ * its <script> by escaping '<' to '\u003c'.
43
+ */
44
+ function toStandaloneHTML(result, title) {
45
+ const deckTitle = result.deckTitle || title;
46
+ const cardsJson = JSON.stringify(result.cards).replace(/</g, '\\u003c');
47
+ const safeTitle = String(deckTitle)
48
+ .replace(/&/g, '&amp;')
49
+ .replace(/</g, '&lt;')
50
+ .replace(/>/g, '&gt;')
51
+ .replace(/"/g, '&quot;')
52
+ .replace(/'/g, '&#39;');
53
+ return `<!DOCTYPE html>
54
+ <html lang="en">
55
+ <head>
56
+ <meta charset="UTF-8">
57
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
58
+ ${favicon_js_1.FAVICON_LINK}
59
+ <title>${safeTitle} \u2014 Flashcards</title>
60
+ <style>
61
+ * { margin: 0; padding: 0; box-sizing: border-box; }
62
+ body { font-family: Georgia, 'Times New Roman', serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; }
63
+ #deck { max-width: 640px; width: 100%; }
64
+ h1 { font-size: 26px; margin-bottom: 8px; color: #a78bfa; text-align: center; }
65
+ .progress { text-align: center; color: #888; font-size: 14px; margin-bottom: 8px; }
66
+ .progress-bar { height: 4px; background: #252547; border-radius: 2px; margin-bottom: 20px; overflow: hidden; }
67
+ .progress-fill { height: 100%; background: #a78bfa; border-radius: 2px; transition: width 0.3s ease; }
68
+ .card { background: #252547; border: 1px solid #3a3a5c; border-radius: 14px; min-height: 220px; padding: 32px 28px; display: flex; flex-direction: column; justify-content: center; cursor: pointer; transition: border-color 0.15s, transform 0.1s; user-select: none; outline: none; }
69
+ .card:hover, .card:focus { border-color: #a78bfa; }
70
+ .card:active { transform: scale(0.99); }
71
+ .face-label { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: #6b6b8f; margin-bottom: 12px; }
72
+ .face-text { font-size: 20px; line-height: 1.6; }
73
+ .hint { margin-top: 16px; font-size: 15px; color: #a78bfa; font-style: italic; }
74
+ .controls { display: flex; gap: 10px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }
75
+ button { padding: 12px 22px; background: #252547; border: 1px solid #3a3a5c; border-radius: 8px; color: #e0e0e0; cursor: pointer; font-size: 15px; font-family: inherit; transition: all 0.15s; }
76
+ button:hover:not(:disabled) { border-color: #a78bfa; background: rgba(167,139,250,0.1); }
77
+ button:disabled { opacity: 0.4; cursor: default; }
78
+ button.primary { background: #a78bfa; color: #1a1a2e; border-color: #a78bfa; font-weight: 600; }
79
+ button.primary:hover:not(:disabled) { background: #8b6fdf; }
80
+ .keys { text-align: center; color: #555; font-size: 12px; margin-top: 20px; }
81
+ .meta { text-align: center; color: #555; font-size: 12px; margin-top: 32px; }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div id="deck">
86
+ <h1>${safeTitle}</h1>
87
+ <div class="progress" id="progress"></div>
88
+ <div class="progress-bar"><div class="progress-fill" id="pfill" style="width:0%"></div></div>
89
+ <div class="card" id="card" tabindex="0" role="button" aria-label="Flashcard \u2014 click or press Space to flip"></div>
90
+ <div class="controls">
91
+ <button id="prev">&lsaquo; Prev</button>
92
+ <button id="flip" class="primary">Flip</button>
93
+ <button id="next">Next &rsaquo;</button>
94
+ <button id="shuffle">Shuffle</button>
95
+ <button id="newdeck" class="primary">New deck</button>
96
+ </div>
97
+ <div class="keys">Space = flip &middot; &larr; &rarr; = navigate</div>
98
+ </div>
99
+ <script>
100
+ (function () {
101
+ var ALL = ${cardsJson};
102
+ var DECK_SIZE = 5;
103
+ var deck = [], idx = 0, flipped = false;
104
+ var cardEl = document.getElementById('card');
105
+ var progressEl = document.getElementById('progress');
106
+ var fillEl = document.getElementById('pfill');
107
+
108
+ function shuffleArr(arr) {
109
+ var a = arr.slice();
110
+ for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random()*(i+1)); var t = a[i]; a[i]=a[j]; a[j]=t; }
111
+ return a;
112
+ }
113
+
114
+ function newDeck() {
115
+ deck = ALL.length <= DECK_SIZE ? shuffleArr(ALL) : shuffleArr(ALL).slice(0, DECK_SIZE);
116
+ idx = 0; flipped = false; render();
117
+ }
118
+
119
+ function render() {
120
+ var c = deck[idx]; if (!c) return;
121
+ cardEl.innerHTML = '';
122
+ var label = document.createElement('div');
123
+ label.className = 'face-label';
124
+ label.textContent = flipped ? 'Back' : 'Front';
125
+ var text = document.createElement('div');
126
+ text.className = 'face-text';
127
+ text.textContent = flipped ? c.back : c.front;
128
+ cardEl.appendChild(label); cardEl.appendChild(text);
129
+ if (flipped && c.hint) {
130
+ var hint = document.createElement('div'); hint.className = 'hint';
131
+ hint.textContent = 'Hint: ' + c.hint; cardEl.appendChild(hint);
132
+ }
133
+ progressEl.textContent = 'Card ' + (idx + 1) + ' of ' + deck.length;
134
+ fillEl.style.width = ((idx + 1) / deck.length * 100) + '%';
135
+ document.getElementById('prev').disabled = idx === 0;
136
+ document.getElementById('next').disabled = idx === deck.length - 1;
137
+ }
138
+
139
+ function flip() { flipped = !flipped; render(); }
140
+ function go(n) { var j = idx + n; if (j < 0 || j >= deck.length) return; idx = j; flipped = false; render(); }
141
+ function shuffleDeck() { deck = shuffleArr(deck); idx = 0; flipped = false; render(); }
142
+
143
+ cardEl.addEventListener('click', flip);
144
+ document.getElementById('flip').addEventListener('click', flip);
145
+ document.getElementById('prev').addEventListener('click', function () { go(-1); });
146
+ document.getElementById('next').addEventListener('click', function () { go(1); });
147
+ document.getElementById('shuffle').addEventListener('click', shuffleDeck);
148
+ document.getElementById('newdeck').addEventListener('click', newDeck);
149
+ document.addEventListener('keydown', function (e) {
150
+ if (e.key === ' ') { e.preventDefault(); flip(); }
151
+ else if (e.key === 'ArrowLeft') { go(-1); }
152
+ else if (e.key === 'ArrowRight') { go(1); }
153
+ });
154
+
155
+ newDeck();
156
+ var meta = document.createElement('div'); meta.className = 'meta';
157
+ meta.textContent = 'Created with Playable Lessons';
158
+ document.getElementById('deck').appendChild(meta);
159
+ })();
160
+ </script>
161
+ </body>
162
+ </html>`;
163
+ }