@bendyline/gilde 0.1.21 → 0.1.23

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.
Files changed (69) hide show
  1. package/authoring/chat-models/nemotron3.5-lightning-30b-q4.json +3 -2
  2. package/authoring/chat-models/nemotron3.5-lightning-30b-q8.json +3 -2
  3. package/authoring/page-demo-stub.js +85 -0
  4. package/data/chat-models/index.json +1 -1
  5. package/data/chat-models/ne/nemotron3.5-lightning-30b-q4/manifest.json +3 -2
  6. package/data/chat-models/ne/nemotron3.5-lightning-30b-q8/manifest.json +3 -2
  7. package/data/connector-types/bl/bluesky-posts/manifest.json +16 -0
  8. package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +76 -0
  9. package/data/connector-types/ca/calendar-google/versions/1.0.0/manifest.json +14 -0
  10. package/data/connector-types/gi/github-issues/versions/1.0.1/manifest.json +67 -0
  11. package/data/connector-types/in/instagram-media/manifest.json +17 -0
  12. package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +96 -0
  13. package/data/connector-types/index.json +1 -1
  14. package/data/connector-types/li/linkedin-posts/manifest.json +16 -0
  15. package/data/connector-types/li/linkedin-posts/versions/1.0.0/manifest.json +54 -0
  16. package/data/connector-types/ma/mail-gmail/versions/1.0.0/manifest.json +14 -0
  17. package/data/connector-types/ma/mail-microsoft365/versions/1.0.0/manifest.json +14 -1
  18. package/data/connector-types/ma/mail-outlook/versions/1.0.0/manifest.json +14 -1
  19. package/data/connector-types/x-/x-posts/manifest.json +17 -0
  20. package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +92 -0
  21. package/data/craftbook-templates/dr/draft-social-post/manifest.json +24 -0
  22. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +178 -0
  23. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/test.json +161 -0
  24. package/data/craftbook-templates/index.json +1 -1
  25. package/data/craftbook-templates/re/reception-report/manifest.json +21 -0
  26. package/data/craftbook-templates/re/reception-report/versions/1.0.0/craftbook.json +170 -0
  27. package/data/craftbook-templates/re/reception-report/versions/1.0.0/test.json +168 -0
  28. package/data/craftbook-templates/so/social-digest/manifest.json +21 -0
  29. package/data/craftbook-templates/so/social-digest/versions/1.0.0/craftbook.json +133 -0
  30. package/data/craftbook-templates/so/social-digest/versions/1.0.0/test.json +168 -0
  31. package/data/gezel-templates/index.json +1 -1
  32. package/data/gezel-templates/om/omroeper/manifest.json +20 -0
  33. package/data/gezel-templates/om/omroeper/versions/1.0.0/about.md +33 -0
  34. package/data/gezel-templates/om/omroeper/versions/1.0.0/manifest.json +23 -0
  35. package/data/project-types/ca/caregiving-binder/versions/1.1.0/about.md +8 -0
  36. package/data/project-types/ca/caregiving-binder/versions/1.1.0/care.json +4 -0
  37. package/data/project-types/ca/caregiving-binder/versions/1.1.0/manifest.json +178 -0
  38. package/data/project-types/ca/caregiving-binder/versions/1.1.0/meds.json +8 -0
  39. package/data/project-types/ca/caregiving-binder/versions/1.1.0/mission.md +8 -0
  40. package/data/project-types/ca/caregiving-binder/versions/1.1.0/pages/dashboard/index.html +265 -0
  41. package/data/project-types/ch/checkers/versions/1.2.0/about.md +9 -0
  42. package/data/project-types/ch/checkers/versions/1.2.0/game.json +77 -0
  43. package/data/project-types/ch/checkers/versions/1.2.0/manifest.json +167 -0
  44. package/data/project-types/ch/checkers/versions/1.2.0/mission.md +9 -0
  45. package/data/project-types/ch/checkers/versions/1.2.0/pages/board/index.html +1157 -0
  46. package/data/project-types/fl/flashcards/versions/1.1.0/about.md +8 -0
  47. package/data/project-types/fl/flashcards/versions/1.1.0/deck.json +7 -0
  48. package/data/project-types/fl/flashcards/versions/1.1.0/manifest.json +168 -0
  49. package/data/project-types/fl/flashcards/versions/1.1.0/mission.md +6 -0
  50. package/data/project-types/fl/flashcards/versions/1.1.0/pages/review/index.html +476 -0
  51. package/data/project-types/im/image-feed/manifest.json +20 -0
  52. package/data/project-types/im/image-feed/versions/1.0.0/about.md +21 -0
  53. package/data/project-types/im/image-feed/versions/1.0.0/manifest.json +330 -0
  54. package/data/project-types/im/image-feed/versions/1.0.0/mission.md +10 -0
  55. package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +832 -0
  56. package/data/project-types/im/image-feed/versions/1.0.0/posts/index.json +4 -0
  57. package/data/project-types/im/image-feed/versions/1.0.0/style-guide.md +24 -0
  58. package/data/project-types/index.json +1 -1
  59. package/data/project-types/so/social-feed/manifest.json +21 -0
  60. package/data/project-types/so/social-feed/versions/1.0.0/about.md +19 -0
  61. package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +336 -0
  62. package/data/project-types/so/social-feed/versions/1.0.0/mission.md +10 -0
  63. package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +756 -0
  64. package/data/project-types/so/social-feed/versions/1.0.0/posts/index.json +4 -0
  65. package/data/project-types/so/social-feed/versions/1.0.0/voice-guide.md +22 -0
  66. package/package.json +1 -1
  67. package/schemas/connector-type-version.schema.json +6 -0
  68. package/schemas/craftbook-test.schema.json +1 -0
  69. package/schemas/project-type-version.schema.json +4 -0
@@ -0,0 +1,8 @@
1
+ A spaced-repetition practice bench for **{{subject}}**.
2
+
3
+ Your Studiemaat writes the cards — ask for them in chat, or let them propose
4
+ cards when a weak spot shows. You review in the **Output** tab: flip the card,
5
+ mark it got-it or missed, and the deck's schedule decides when each card comes
6
+ back (daily, every three days, weekly, as you prove them).
7
+
8
+ After each session your Studiemaat checks in on how it went.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 1,
3
+ "subject": "{{subject}}",
4
+ "cards": [],
5
+ "dueCardIds": [],
6
+ "updatedAt": "2026-07-18T00:00:00.000Z"
7
+ }
@@ -0,0 +1,168 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.1.0",
4
+ "releasedAt": "2026-08-12T00:00:00Z",
5
+ "meesterManaged": false,
6
+ "params": {
7
+ "type": "object",
8
+ "properties": {
9
+ "subject": {
10
+ "type": "string",
11
+ "title": "What are you learning?",
12
+ "description": "The deck's subject — e.g. Spanish vocabulary, AWS certification, organic chemistry.",
13
+ "default": "Spanish vocabulary"
14
+ }
15
+ },
16
+ "required": [
17
+ "subject"
18
+ ]
19
+ },
20
+ "nameTemplate": "{{subject}} Flashcards",
21
+ "aboutTemplate": "about.md",
22
+ "missionTemplate": "mission.md",
23
+ "gezels": [
24
+ {
25
+ "templateId": "study-buddy",
26
+ "voorman": true
27
+ }
28
+ ],
29
+ "scripts": {
30
+ "deck-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'deck-store',\n description:\n \"Read and update the flashcard deck (deck.json). Call action:'add_cards' with a cards array ([{front, back}, ...]) to add practice cards; action:'state' to see due counts and box distribution; action:'list_deck' to read every card. The review page drives 'record_review' and 'finish_session' itself.\",\n inputs: {\n action: {\n type: 'string',\n description:\n \"One of 'state', 'list_deck', 'add_cards', 'record_review', or 'finish_session'.\",\n required: true,\n },\n cards: {\n type: 'json',\n description: 'For add_cards: an array of {front, back} objects.',\n },\n cardId: { type: 'string', description: \"For record_review: the card id, e.g. 'card-3'.\" },\n verdict: { type: 'string', description: \"For record_review: 'got-it' or 'missed'.\" },\n reviewed: { type: 'number', description: 'For finish_session: how many cards were reviewed.' },\n missedIds: { type: 'json', description: 'For finish_session: array of card ids that were missed.' },\n },\n outputs: {\n total: { type: 'number', description: 'Cards in the deck.' },\n due: { type: 'number', description: 'Cards due for review now.' },\n summary: { type: 'string', description: 'Human-readable deck or session summary.' },\n missedList: { type: 'string', description: 'Fronts of the cards missed this session (finish_session).' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── leitner-rules-start ──\n// Pure Leitner-box scheduling (3 boxes). Box 1 reviews daily, box 2 every\n// 3 days, box 3 weekly. A got-it promotes (capped at 3); a miss goes back\n// to box 1. Never-reviewed cards are always due. Kept self-contained\n// between these sentinels so unit tests execute the shipped bytes.\n\nvar BOX_INTERVAL_DAYS = { 1: 1, 2: 3, 3: 7 };\nvar DAY_MS = 24 * 60 * 60 * 1000;\n\nfunction isDue(card, nowMs) {\n if (!card.lastReviewedAt) return true;\n var interval = BOX_INTERVAL_DAYS[card.box] || 1;\n return nowMs - Date.parse(card.lastReviewedAt) >= interval * DAY_MS;\n}\n\nfunction computeDueIds(cards, nowMs) {\n return cards\n .filter(function (c) {\n return isDue(c, nowMs);\n })\n .sort(function (a, b) {\n if (a.box !== b.box) return a.box - b.box;\n return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;\n })\n .map(function (c) {\n return c.id;\n });\n}\n\nfunction applyReview(card, verdict, nowIso) {\n if (verdict === 'got-it') card.box = Math.min(3, card.box + 1);\n else card.box = 1;\n card.lastReviewedAt = nowIso;\n card.stats = card.stats || { gotIt: 0, missed: 0 };\n if (verdict === 'got-it') card.stats.gotIt += 1;\n else card.stats.missed += 1;\n return card;\n}\n\nfunction nextCardId(cards) {\n var max = 0;\n for (var i = 0; i < cards.length; i += 1) {\n var m = /^card-(\\d+)$/.exec(cards[i].id);\n if (m) max = Math.max(max, Number(m[1]));\n }\n return 'card-' + (max + 1);\n}\n// ── leitner-rules-end ──\n\nconst DECK_FILE = 'deck.json';\n\ninterface Card {\n id: string;\n front: string;\n back: string;\n box: number;\n lastReviewedAt: string | null;\n createdAt: string;\n stats: { gotIt: number; missed: number };\n}\n\ninterface Deck {\n version: number;\n subject: string;\n cards: Card[];\n dueCardIds: string[];\n updatedAt: string;\n}\n\nasync function load(): Promise<Deck> {\n let raw: string;\n try {\n raw = await gezel.fs.read(DECK_FILE);\n } catch {\n return { version: 1, subject: 'your subject', cards: [], dueCardIds: [], updatedAt: new Date().toISOString() };\n }\n const parsed = JSON.parse(raw) as Partial<Deck>;\n return {\n version: 1,\n subject: typeof parsed.subject === 'string' ? parsed.subject : 'your subject',\n cards: Array.isArray(parsed.cards) ? (parsed.cards as Card[]) : [],\n dueCardIds: [],\n updatedAt: new Date().toISOString(),\n };\n}\n\nasync function persist(deck: Deck): Promise<Deck> {\n deck.dueCardIds = computeDueIds(deck.cards, Date.now());\n deck.updatedAt = new Date().toISOString();\n await gezel.fs.write(DECK_FILE, `${JSON.stringify(deck, null, 2)}\\n`);\n return deck;\n}\n\nconst deck = await load();\ndeck.dueCardIds = computeDueIds(deck.cards, Date.now());\nlet summary = '';\nlet missedList = '';\n\nif (input.action === 'add_cards') {\n const raw = Array.isArray(input.cards) ? input.cards : [];\n const added: string[] = [];\n for (const entry of raw) {\n const front = typeof (entry as { front?: unknown }).front === 'string' ? (entry as { front: string }).front.trim() : '';\n const back = typeof (entry as { back?: unknown }).back === 'string' ? (entry as { back: string }).back.trim() : '';\n if (!front || !back) continue;\n const card: Card = {\n id: nextCardId(deck.cards),\n front: front.slice(0, 300),\n back: back.slice(0, 500),\n box: 1,\n lastReviewedAt: null,\n createdAt: new Date().toISOString(),\n stats: { gotIt: 0, missed: 0 },\n };\n deck.cards.push(card);\n added.push(card.id);\n }\n if (added.length === 0) {\n throw new Error(\"add_cards needs a non-empty 'cards' array of {front, back} objects.\");\n }\n await persist(deck);\n summary = `Added ${added.length} card(s). ${deck.cards.length} in the deck, ${deck.dueCardIds.length} due now.`;\n} else if (input.action === 'record_review') {\n if (!input.cardId || (input.verdict !== 'got-it' && input.verdict !== 'missed')) {\n throw new Error(\"record_review needs 'cardId' and verdict 'got-it' or 'missed'.\");\n }\n const card = deck.cards.find((c) => c.id === input.cardId);\n if (!card) throw new Error(`No card '${input.cardId}' in the deck.`);\n applyReview(card, input.verdict, new Date().toISOString());\n await persist(deck);\n summary = `${card.id} marked ${input.verdict} (now box ${card.box}). ${deck.dueCardIds.length} still due.`;\n} else if (input.action === 'finish_session') {\n const reviewed = typeof input.reviewed === 'number' ? input.reviewed : 0;\n const missedIds = Array.isArray(input.missedIds) ? input.missedIds.map(String) : [];\n const missedFronts = missedIds\n .map((id) => deck.cards.find((c) => c.id === id)?.front)\n .filter((f): f is string => typeof f === 'string');\n missedList = missedFronts.length ? missedFronts.join(' | ') : '(none)';\n summary = `Reviewed ${reviewed} card(s): ${reviewed - missedIds.length} got it, ${missedIds.length} missed. ${deck.dueCardIds.length} due next.`;\n} else if (input.action === 'list_deck') {\n summary = deck.cards.length\n ? deck.cards\n .map((c) => `${c.id} [box ${c.box}] ${c.front} -> ${c.back}`)\n .join('\\n')\n : 'The deck is empty — add cards with add_cards.';\n} else if (input.action === 'state') {\n const boxes = { 1: 0, 2: 0, 3: 0 } as Record<number, number>;\n for (const c of deck.cards) boxes[c.box] = (boxes[c.box] ?? 0) + 1;\n summary = deck.cards.length\n ? `${deck.cards.length} card(s) on \"${deck.subject}\": ${deck.dueCardIds.length} due now. Boxes: ${boxes[1]} daily, ${boxes[2]} every-3-days, ${boxes[3]} weekly.`\n : `The deck for \"${deck.subject}\" is empty — offer to write the first cards.`;\n} else {\n throw new Error(\n `Unknown action '${input.action}'. Use state, list_deck, add_cards, record_review, or finish_session.`,\n );\n}\n\ngezel.output({\n total: deck.cards.length,\n due: deck.dueCardIds.length,\n summary,\n missedList,\n});\n"
31
+ },
32
+ "tools": [
33
+ {
34
+ "name": "add_cards",
35
+ "description": "Add flashcards to the deck. Pass a cards array of {front, back} objects — one fact per card.",
36
+ "script": "deck-store",
37
+ "inputs": {
38
+ "type": "object",
39
+ "properties": {
40
+ "cards": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {
45
+ "front": {
46
+ "type": "string"
47
+ },
48
+ "back": {
49
+ "type": "string"
50
+ }
51
+ },
52
+ "required": [
53
+ "front",
54
+ "back"
55
+ ]
56
+ },
57
+ "description": "The cards to add."
58
+ }
59
+ },
60
+ "required": [
61
+ "cards"
62
+ ]
63
+ },
64
+ "bind": {
65
+ "action": "add_cards"
66
+ }
67
+ },
68
+ {
69
+ "name": "deck_status",
70
+ "description": "See the deck: how many cards, how many due, and the box distribution.",
71
+ "script": "deck-store",
72
+ "inputs": {
73
+ "type": "object",
74
+ "properties": {}
75
+ },
76
+ "bind": {
77
+ "action": "state"
78
+ }
79
+ },
80
+ {
81
+ "name": "list_deck",
82
+ "description": "Read every card in the deck (id, box, front, back).",
83
+ "script": "deck-store",
84
+ "inputs": {
85
+ "type": "object",
86
+ "properties": {}
87
+ },
88
+ "bind": {
89
+ "action": "list_deck"
90
+ }
91
+ },
92
+ {
93
+ "name": "record_review",
94
+ "description": "The review page's per-card verdict.",
95
+ "script": "deck-store",
96
+ "inputs": {
97
+ "type": "object",
98
+ "properties": {
99
+ "cardId": {
100
+ "type": "string"
101
+ },
102
+ "verdict": {
103
+ "type": "string",
104
+ "enum": [
105
+ "got-it",
106
+ "missed"
107
+ ]
108
+ }
109
+ },
110
+ "required": [
111
+ "cardId",
112
+ "verdict"
113
+ ]
114
+ },
115
+ "bind": {
116
+ "action": "record_review"
117
+ }
118
+ },
119
+ {
120
+ "name": "finish_session",
121
+ "description": "The review page's end-of-session summary.",
122
+ "script": "deck-store",
123
+ "inputs": {
124
+ "type": "object",
125
+ "properties": {
126
+ "reviewed": {
127
+ "type": "number"
128
+ },
129
+ "missedIds": {
130
+ "type": "array",
131
+ "items": {
132
+ "type": "string"
133
+ }
134
+ }
135
+ },
136
+ "required": [
137
+ "reviewed"
138
+ ]
139
+ },
140
+ "bind": {
141
+ "action": "finish_session"
142
+ },
143
+ "reaction": {
144
+ "gezel": "study-buddy",
145
+ "prompt": "The user finished a flashcard review session. {{output.summary}}\nMissed cards: {{output.missedList}}\nReply with ONE short encouraging line — specific beats generic. If any cards were missed, offer to add fresh practice cards that come at those same ideas from a new angle (add them with your deck capabilities if the user agrees)."
146
+ }
147
+ }
148
+ ],
149
+ "craftbooks": [],
150
+ "schedules": [],
151
+ "pages": {
152
+ "entry": "review/index.html",
153
+ "api": 1,
154
+ "reads": [
155
+ {
156
+ "source": "workspace",
157
+ "path": "deck.json"
158
+ }
159
+ ],
160
+ "tools": [
161
+ "record_review",
162
+ "finish_session"
163
+ ]
164
+ },
165
+ "workspaceSeed": [
166
+ "deck.json"
167
+ ]
168
+ }
@@ -0,0 +1,6 @@
1
+ - A living deck on {{subject}}: a few sharp new cards whenever they're needed,
2
+ never bulk filler.
3
+ - Due cards get reviewed most days; the page's schedule is the source of truth.
4
+ - Every missed card gets another road in — fresh cards on the same idea from a
5
+ new angle, offered after the session.
6
+ - Encouragement always, shame never: a miss is the system working.
@@ -0,0 +1,476 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="gezel-demo" content="standalone" />
7
+ <title>Flashcards</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #faf7f2;
12
+ --card: #fff;
13
+ --ink: #2b2620;
14
+ --muted: #7a7166;
15
+ --accent: #b5651d;
16
+ --track: #ece5da;
17
+ --good: #4a7c59;
18
+ --warn: #a4552e;
19
+ }
20
+ :root[data-theme='dark'] {
21
+ --bg: #1c1a17;
22
+ --card: #262320;
23
+ --ink: #efe9e0;
24
+ --muted: #a59a8c;
25
+ --accent: #e0894a;
26
+ --track: #3a352f;
27
+ --good: #7fae8d;
28
+ --warn: #d98a63;
29
+ }
30
+ @media (prefers-color-scheme: dark) {
31
+ :root:not([data-theme='light']) {
32
+ --bg: #1c1a17;
33
+ --card: #262320;
34
+ --ink: #efe9e0;
35
+ --muted: #a59a8c;
36
+ --accent: #e0894a;
37
+ --track: #3a352f;
38
+ --good: #7fae8d;
39
+ --warn: #d98a63;
40
+ }
41
+ }
42
+ * { box-sizing: border-box; }
43
+ body {
44
+ margin: 0;
45
+ font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
46
+ background: var(--bg);
47
+ color: var(--ink);
48
+ padding: 1.5rem 1.25rem;
49
+ }
50
+ main { max-width: 560px; margin: 0 auto; }
51
+ h1 {
52
+ font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
53
+ font-size: 1.35rem;
54
+ font-weight: 600;
55
+ margin: 0 0 0.15rem;
56
+ }
57
+ .sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
58
+ .dots { display: flex; gap: 0.3rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
59
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--track); }
60
+ .dot.done-good { background: var(--good); }
61
+ .dot.done-miss { background: var(--warn); }
62
+ .dot.current { background: var(--accent); }
63
+ .flashcard {
64
+ background: var(--card);
65
+ border: 1px solid var(--track);
66
+ border-radius: 10px;
67
+ min-height: 200px;
68
+ display: grid;
69
+ place-items: center;
70
+ text-align: center;
71
+ padding: 1.5rem;
72
+ font-size: 1.15rem;
73
+ cursor: pointer;
74
+ user-select: none;
75
+ }
76
+ .flashcard .face-label {
77
+ display: block;
78
+ font-size: 0.7rem;
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.05em;
81
+ color: var(--muted);
82
+ margin-bottom: 0.6rem;
83
+ }
84
+ .actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; justify-content: center; }
85
+ .actions button {
86
+ border: 1px solid var(--track);
87
+ background: var(--card);
88
+ color: var(--ink);
89
+ border-radius: 6px;
90
+ padding: 0.45rem 1.1rem;
91
+ font: inherit;
92
+ cursor: pointer;
93
+ }
94
+ .actions button.got { border-color: var(--good); color: var(--good); }
95
+ .actions button.miss { border-color: var(--warn); color: var(--warn); }
96
+ .actions button:disabled { opacity: 0.5; cursor: default; }
97
+ .empty, .done {
98
+ background: var(--card);
99
+ border: 1px dashed var(--track);
100
+ border-radius: 10px;
101
+ padding: 1.5rem;
102
+ text-align: center;
103
+ color: var(--muted);
104
+ }
105
+ .banner {
106
+ border: 1px solid var(--accent);
107
+ border-radius: 6px;
108
+ padding: 0.45rem 0.7rem;
109
+ font-size: 0.85rem;
110
+ margin-bottom: 0.75rem;
111
+ display: none;
112
+ }
113
+ .banner.show { display: block; }
114
+ .hint { font-size: 0.78rem; color: var(--muted); text-align: center; margin-top: 0.5rem; }
115
+ .note {
116
+ margin: 0 0 0.85rem;
117
+ padding: 0.5rem 0.65rem;
118
+ border: 1px solid var(--track);
119
+ border-radius: 6px;
120
+ background: var(--card);
121
+ color: var(--muted);
122
+ font-size: 0.8rem;
123
+ }
124
+ .demo-banner {
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: space-between;
128
+ gap: 0.75rem;
129
+ margin: 0 0 0.85rem;
130
+ padding: 0.5rem 0.65rem;
131
+ border: 1px solid var(--accent);
132
+ border-radius: 6px;
133
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
134
+ color: var(--ink);
135
+ font-size: 0.8rem;
136
+ }
137
+ .demo-banner[hidden] { display: none; }
138
+ .demo-banner strong { font-weight: 700; }
139
+ .demo-reset {
140
+ flex: none;
141
+ padding: 0.25rem 0.5rem;
142
+ border: 1px solid var(--accent);
143
+ border-radius: 6px;
144
+ background: var(--card);
145
+ color: var(--ink);
146
+ cursor: pointer;
147
+ font: inherit;
148
+ }
149
+ .demo-reset:hover { border-color: var(--ink); }
150
+ </style>
151
+ </head>
152
+ <body>
153
+ <main>
154
+ <div class="demo-banner" data-gezel-demo-banner hidden>
155
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
156
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
157
+ </div>
158
+ <p class="note" id="browser-note" hidden>
159
+ Viewing from a browser tab — the deck stays live, but reviews happen in the app's Output pane.
160
+ </p>
161
+ <div class="banner" id="banner"></div>
162
+ <h1 id="title">Flashcards</h1>
163
+ <p class="sub" id="sub">Loading the deck…</p>
164
+ <div class="dots" id="dots"></div>
165
+ <div id="stage"></div>
166
+ <p class="hint" id="hint"></p>
167
+ </main>
168
+ <script>
169
+ /* gezel-page-demo-stub v1 */
170
+ /*
171
+ * The canonical demo stub for Gezel type pages. MIT License.
172
+ *
173
+ * Paste this file's bytes verbatim into a <script> block ahead of the
174
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
175
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
176
+ * injected and the stub steps aside; opened as a raw file (the live demos
177
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
178
+ * the page-supplied handler maps — one code path in every mode. `tools`
179
+ * and `data` map a tool name / declared read path to a function; data
180
+ * handlers back read() and list(), and url() returns the handler's value
181
+ * or passes a data: URI through. See docs/page-authoring.md.
182
+ */
183
+ (function () {
184
+ 'use strict';
185
+ window.makeDemoGezel = function (opts) {
186
+ if (window.gezel) return window.gezel;
187
+ var page = (opts && opts.page) || {};
188
+ var tools = (opts && opts.tools) || {};
189
+ var data = (opts && opts.data) || {};
190
+ var themeListeners = [];
191
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
192
+ var ui = {
193
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
194
+ onTheme: function (cb) {
195
+ themeListeners.push(cb);
196
+ return function () {
197
+ var at = themeListeners.indexOf(cb);
198
+ if (at >= 0) themeListeners.splice(at, 1);
199
+ };
200
+ },
201
+ };
202
+ if (media && media.addEventListener) {
203
+ media.addEventListener('change', function (ev) {
204
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
205
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
206
+ });
207
+ }
208
+ function fail(code, message) {
209
+ var err = new Error(message);
210
+ err.code = code;
211
+ return Promise.reject(err);
212
+ }
213
+ function run(map, key, arg) {
214
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
215
+ return Promise.resolve().then(function () { return map[key](arg); });
216
+ }
217
+ window.gezel = {
218
+ page: {
219
+ api: 1,
220
+ projectId: 'demo',
221
+ source: 'type',
222
+ entry: page.entry || 'index.html',
223
+ typeName: page.typeName || 'demo',
224
+ params: page.params || {},
225
+ mode: 'demo',
226
+ },
227
+ tools: {
228
+ list: function () { return Promise.resolve(Object.keys(tools)); },
229
+ invoke: function (name, input) {
230
+ var call = run(tools, name, input || {});
231
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
232
+ return call.then(function (output) { return { output: output }; });
233
+ },
234
+ },
235
+ data: {
236
+ read: function (path, o) {
237
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
238
+ },
239
+ list: function (path, o) {
240
+ return run(data, path, o || {}) || Promise.resolve([]);
241
+ },
242
+ watch: function () { return function () {}; },
243
+ url: function (path) {
244
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
245
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
246
+ },
247
+ },
248
+ ui: ui,
249
+ refresh: function () {},
250
+ };
251
+ return window.gezel;
252
+ };
253
+ })();
254
+ </script>
255
+ <script>
256
+ (function () {
257
+ 'use strict';
258
+
259
+ // ── demo deck (used only when the page opens as a raw file) ──
260
+ var demoDeck = {
261
+ subject: 'Dutch for a market morning',
262
+ cards: [
263
+ { id: 'card-1', front: 'Hoeveel kost dit?', back: 'How much does this cost?' },
264
+ { id: 'card-2', front: 'een bos bloemen', back: 'a bunch of flowers' },
265
+ { id: 'card-3', front: 'Mag ik pinnen?', back: 'Can I pay by card?' },
266
+ { id: 'card-4', front: 'Dat is alles, dank u.', back: 'That is all, thank you.' },
267
+ ],
268
+ dueCardIds: ['card-1', 'card-2', 'card-3', 'card-4'],
269
+ };
270
+ function demoCard(id) {
271
+ return demoDeck.cards.find(function (c) { return c.id === id; }) || null;
272
+ }
273
+
274
+ makeDemoGezel({
275
+ page: { entry: 'review/index.html', typeName: 'Flashcards' },
276
+ tools: {
277
+ record_review: function (input) {
278
+ var card = demoCard(input && input.cardId);
279
+ if (!card) throw new Error("No card '" + (input && input.cardId) + "' in the demo deck.");
280
+ // The demo deck stays due so the session can be replayed;
281
+ // Reset demo restores everything else.
282
+ return { summary: card.id + ' marked ' + input.verdict + '.' };
283
+ },
284
+ finish_session: function (input) {
285
+ var reviewed = (input && input.reviewed) || 0;
286
+ var missed = ((input && input.missedIds) || []).length;
287
+ return { summary: 'Reviewed ' + reviewed + ' card(s): ' + (reviewed - missed) + ' got it, ' + missed + ' missed.' };
288
+ },
289
+ },
290
+ data: {
291
+ 'deck.json': function () { return demoDeck; },
292
+ },
293
+ });
294
+
295
+ // ── one code path from here: window.gezel exists in every mode ──
296
+ var demoMode = gezel.page.mode === 'demo';
297
+ var browserMode = gezel.page.mode === 'browser';
298
+
299
+ function applyTheme(mode) {
300
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
301
+ }
302
+ applyTheme(gezel.ui.theme.mode);
303
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
304
+
305
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
306
+ if (browserMode) document.getElementById('browser-note').hidden = false;
307
+
308
+ var deck = null;
309
+ // The session queue is snapshotted from dueCardIds at start so a
310
+ // background change never reshuffles cards mid-session.
311
+ var queue = null;
312
+ var idx = 0;
313
+ var flipped = false;
314
+ var results = [];
315
+ var busy = false;
316
+
317
+ function esc(s) {
318
+ return String(s).replace(/[&<>"]/g, function (c) {
319
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
320
+ });
321
+ }
322
+ function setBanner(text) {
323
+ var el = document.getElementById('banner');
324
+ el.textContent = text || '';
325
+ el.className = text ? 'banner show' : 'banner';
326
+ }
327
+ function invokeErrorText(err) {
328
+ if (err && err.code === 'unavailable') {
329
+ return "Reviews happen in the app's Output pane — this browser tab stays view-only.";
330
+ }
331
+ return String(err && err.message ? err.message : err);
332
+ }
333
+ function cardById(id) {
334
+ return (deck.cards || []).find(function (c) { return c.id === id; }) || null;
335
+ }
336
+
337
+ function render() {
338
+ var stage = document.getElementById('stage');
339
+ var dots = document.getElementById('dots');
340
+ var hint = document.getElementById('hint');
341
+ if (!deck) return;
342
+ if (deck.subject) document.getElementById('title').textContent = 'Flashcards: ' + deck.subject;
343
+
344
+ if (!deck.cards || deck.cards.length === 0) {
345
+ document.getElementById('sub').textContent = 'The deck is empty.';
346
+ dots.innerHTML = '';
347
+ hint.textContent = '';
348
+ stage.innerHTML =
349
+ '<div class="empty">No cards yet — ask your Studiemaat to write the first ones.</div>';
350
+ return;
351
+ }
352
+
353
+ if (queue === null || (idx >= queue.length && queue.length === 0)) {
354
+ document.getElementById('sub').textContent =
355
+ deck.dueCardIds.length > 0
356
+ ? deck.dueCardIds.length + ' card(s) due.'
357
+ : 'Nothing due right now.';
358
+ dots.innerHTML = '';
359
+ hint.textContent = '';
360
+ stage.innerHTML =
361
+ deck.dueCardIds.length > 0
362
+ ? '<div class="actions"><button type="button" id="start">Start review (' + deck.dueCardIds.length + ')</button></div>'
363
+ : '<div class="done">All caught up — nothing due. Your Studiemaat can always add more.</div>';
364
+ var startBtn = document.getElementById('start');
365
+ if (startBtn) startBtn.addEventListener('click', startSession);
366
+ return;
367
+ }
368
+
369
+ if (idx >= queue.length) {
370
+ var missed = results.filter(function (r) { return r.verdict === 'missed'; }).length;
371
+ document.getElementById('sub').textContent =
372
+ 'Session done: ' + (results.length - missed) + ' got it, ' + missed + ' missed.';
373
+ hint.textContent = '';
374
+ stage.innerHTML = '<div class="done">Nice work — your Studiemaat has the recap.</div>';
375
+ return;
376
+ }
377
+
378
+ var card = cardById(queue[idx]);
379
+ if (!card) {
380
+ idx += 1;
381
+ render();
382
+ return;
383
+ }
384
+ document.getElementById('sub').textContent = 'Card ' + (idx + 1) + ' of ' + queue.length;
385
+ dots.innerHTML = queue
386
+ .map(function (id, i) {
387
+ if (i === idx) return '<span class="dot current"></span>';
388
+ var r = results[i];
389
+ if (!r) return '<span class="dot"></span>';
390
+ return '<span class="dot ' + (r.verdict === 'missed' ? 'done-miss' : 'done-good') + '"></span>';
391
+ })
392
+ .join('');
393
+ stage.innerHTML =
394
+ '<div class="flashcard" id="card"><div><span class="face-label">' +
395
+ (flipped ? 'Back' : 'Front') + '</span>' + esc(flipped ? card.back : card.front) +
396
+ '</div></div>' +
397
+ (flipped
398
+ ? '<div class="actions">' +
399
+ '<button type="button" class="got" id="got"' + (busy ? ' disabled' : '') + '>Got it</button>' +
400
+ '<button type="button" class="miss" id="miss"' + (busy ? ' disabled' : '') + '>Missed</button>' +
401
+ '</div>'
402
+ : '');
403
+ hint.textContent = flipped ? '' : 'Tap the card to reveal the answer.';
404
+ document.getElementById('card').addEventListener('click', function () {
405
+ if (!flipped) {
406
+ flipped = true;
407
+ render();
408
+ }
409
+ });
410
+ if (flipped) {
411
+ document.getElementById('got').addEventListener('click', function () { grade('got-it'); });
412
+ document.getElementById('miss').addEventListener('click', function () { grade('missed'); });
413
+ }
414
+ }
415
+
416
+ function startSession() {
417
+ queue = deck.dueCardIds.slice();
418
+ idx = 0;
419
+ flipped = false;
420
+ results = [];
421
+ render();
422
+ }
423
+
424
+ function grade(verdict) {
425
+ if (busy) return;
426
+ busy = true;
427
+ render();
428
+ var cardId = queue[idx];
429
+ gezel.tools
430
+ .invoke('record_review', { cardId: cardId, verdict: verdict })
431
+ .then(function () {
432
+ busy = false;
433
+ results[idx] = { cardId: cardId, verdict: verdict };
434
+ idx += 1;
435
+ flipped = false;
436
+ if (idx >= queue.length) return finishSession();
437
+ render();
438
+ })
439
+ .catch(function (err) {
440
+ busy = false;
441
+ setBanner(invokeErrorText(err));
442
+ render();
443
+ });
444
+ }
445
+
446
+ function finishSession() {
447
+ var missedIds = results
448
+ .filter(function (r) { return r.verdict === 'missed'; })
449
+ .map(function (r) { return r.cardId; });
450
+ return gezel.tools
451
+ .invoke('finish_session', { reviewed: results.length, missedIds: missedIds })
452
+ .catch(function () {})
453
+ .then(function () {
454
+ render();
455
+ return loadDeck();
456
+ });
457
+ }
458
+
459
+ function loadDeck() {
460
+ return gezel.data
461
+ .read('deck.json')
462
+ .then(function (next) {
463
+ deck = next;
464
+ // Only re-render outside an active session — mid-session the
465
+ // snapshot queue drives the UI.
466
+ if (queue === null || idx >= queue.length) render();
467
+ })
468
+ .catch(function () {});
469
+ }
470
+
471
+ loadDeck();
472
+ gezel.data.watch('deck.json', function () { loadDeck(); });
473
+ })();
474
+ </script>
475
+ </body>
476
+ </html>