@bendyline/gilde 0.1.91 → 0.1.93

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 (119) hide show
  1. package/README.md +6 -4
  2. package/data/craftbook-templates/fr/freeze-scope/versions/1.4.0/craftbook.json +94 -0
  3. package/data/craftbook-templates/fr/freeze-scope/versions/1.4.0/test.json +131 -0
  4. package/data/craftbook-templates/index.json +1 -1
  5. package/data/craftbook-templates/na/narrated-slideshow/versions/1.1.7/craftbook.json +315 -0
  6. package/data/craftbook-templates/na/narrated-slideshow/versions/1.1.7/test.json +89 -0
  7. package/data/craftbook-templates/ph/photo-library-nightly/manifest.json +22 -0
  8. package/data/craftbook-templates/ph/photo-library-nightly/versions/1.0.0/craftbook.json +186 -0
  9. package/data/craftbook-templates/ph/photo-library-nightly/versions/1.0.0/test.json +69 -0
  10. package/data/craftbook-templates/po/powerpoint-deck/versions/1.7.17/craftbook.json +486 -0
  11. package/data/craftbook-templates/po/powerpoint-deck/versions/1.7.17/test.json +265 -0
  12. package/data/craftbook-templates/raw-index.json +1 -1
  13. package/data/craftbook-templates/re/report-pdf/versions/1.1.7/craftbook.json +316 -0
  14. package/data/craftbook-templates/re/report-pdf/versions/1.1.7/test.json +101 -0
  15. package/data/craftbook-templates/re/research-to-document/versions/1.2.7/craftbook.json +387 -0
  16. package/data/craftbook-templates/re/research-to-document/versions/1.2.7/test.json +242 -0
  17. package/data/gezel-templates/cu/curator/manifest.json +20 -0
  18. package/data/gezel-templates/cu/curator/versions/1.0.0/about.md +22 -0
  19. package/data/gezel-templates/cu/curator/versions/1.0.0/manifest.json +23 -0
  20. package/data/gezel-templates/index.json +1 -1
  21. package/data/gezel-templates/pu/puzzelaar/manifest.json +19 -0
  22. package/data/gezel-templates/pu/puzzelaar/versions/1.0.0/about.md +18 -0
  23. package/data/gezel-templates/pu/puzzelaar/versions/1.0.0/manifest.json +16 -0
  24. package/data/gezel-templates/raw-index.json +1 -1
  25. package/data/gezel-templates/wo/woordsmid/manifest.json +19 -0
  26. package/data/gezel-templates/wo/woordsmid/versions/1.0.0/about.md +18 -0
  27. package/data/gezel-templates/wo/woordsmid/versions/1.0.0/manifest.json +16 -0
  28. package/data/knowledge-catalogs/be/bendyline-how-to-books/manifest.json +26 -0
  29. package/data/knowledge-catalogs/be/bendyline-how-to-books/versions/2026.10.8/manifest.json +8950 -0
  30. package/data/knowledge-catalogs/ca/caselaw-washington-part-0001/manifest.json +24 -0
  31. package/data/knowledge-catalogs/ca/caselaw-washington-part-0001/versions/2026.10.1/manifest.json +318 -0
  32. package/data/knowledge-catalogs/ca/caselaw-washington-part-0002/manifest.json +24 -0
  33. package/data/knowledge-catalogs/ca/caselaw-washington-part-0002/versions/2026.10.1/manifest.json +266 -0
  34. package/data/knowledge-catalogs/ca/caselaw-washington-part-0003/manifest.json +24 -0
  35. package/data/knowledge-catalogs/ca/caselaw-washington-part-0003/versions/2026.10.1/manifest.json +186 -0
  36. package/data/knowledge-catalogs/ca/caselaw-washington-part-0004/manifest.json +24 -0
  37. package/data/knowledge-catalogs/ca/caselaw-washington-part-0004/versions/2026.10.1/manifest.json +138 -0
  38. package/data/knowledge-catalogs/ca/caselaw-washington-part-0005/manifest.json +24 -0
  39. package/data/knowledge-catalogs/ca/caselaw-washington-part-0005/versions/2026.10.1/manifest.json +138 -0
  40. package/data/knowledge-catalogs/index.json +1 -1
  41. package/data/knowledge-catalogs/mi/microsoftdocs-aspnet-core-en/manifest.json +22 -0
  42. package/data/knowledge-catalogs/mi/microsoftdocs-aspnet-core-en/versions/2026.10.16/manifest.json +18270 -0
  43. package/data/knowledge-catalogs/mi/microsoftdocs-azure-ai-platform-en/manifest.json +23 -0
  44. package/data/knowledge-catalogs/mi/microsoftdocs-azure-ai-platform-en/versions/2026.10.16/manifest.json +1098 -0
  45. package/data/knowledge-catalogs/mi/microsoftdocs-azure-apps-compute-en/manifest.json +23 -0
  46. package/data/knowledge-catalogs/mi/microsoftdocs-azure-apps-compute-en/versions/2026.10.16/manifest.json +722 -0
  47. package/data/knowledge-catalogs/mi/microsoftdocs-azure-data-aiot-en/manifest.json +23 -0
  48. package/data/knowledge-catalogs/mi/microsoftdocs-azure-data-aiot-en/versions/2026.10.16/manifest.json +530 -0
  49. package/data/knowledge-catalogs/mi/microsoftdocs-azure-management-hybrid-en/manifest.json +23 -0
  50. package/data/knowledge-catalogs/mi/microsoftdocs-azure-management-hybrid-en/versions/2026.10.16/manifest.json +602 -0
  51. package/data/knowledge-catalogs/mi/microsoftdocs-azure-network-security-en/manifest.json +23 -0
  52. package/data/knowledge-catalogs/mi/microsoftdocs-azure-network-security-en/versions/2026.10.16/manifest.json +250 -0
  53. package/data/knowledge-catalogs/mi/microsoftdocs-dotnet-en/manifest.json +22 -0
  54. package/data/knowledge-catalogs/mi/microsoftdocs-dotnet-en/versions/2026.10.16/manifest.json +16794 -0
  55. package/data/knowledge-catalogs/mi/microsoftdocs-entity-framework-en/manifest.json +22 -0
  56. package/data/knowledge-catalogs/mi/microsoftdocs-entity-framework-en/versions/2026.10.16/manifest.json +1106 -0
  57. package/data/knowledge-catalogs/mi/microsoftdocs-microsoft-365-en/manifest.json +22 -0
  58. package/data/knowledge-catalogs/mi/microsoftdocs-microsoft-365-en/versions/2026.10.16/manifest.json +234 -0
  59. package/data/knowledge-catalogs/mi/microsoftdocs-powershell-en/manifest.json +22 -0
  60. package/data/knowledge-catalogs/mi/microsoftdocs-powershell-en/versions/2026.10.16/manifest.json +522 -0
  61. package/data/knowledge-catalogs/mi/microsoftdocs-sql-server-en/manifest.json +22 -0
  62. package/data/knowledge-catalogs/mi/microsoftdocs-sql-server-en/versions/2026.10.16/manifest.json +1850 -0
  63. package/data/knowledge-catalogs/mi/microsoftdocs-windows-apps-en/manifest.json +22 -0
  64. package/data/knowledge-catalogs/mi/microsoftdocs-windows-apps-en/versions/2026.10.16/manifest.json +1734 -0
  65. package/data/knowledge-catalogs/mi/microsoftdocs-windows-server-en/manifest.json +22 -0
  66. package/data/knowledge-catalogs/mi/microsoftdocs-windows-server-en/versions/2026.10.16/manifest.json +774 -0
  67. package/data/knowledge-catalogs/raw-index.json +1 -1
  68. package/data/project-types/ch/checkers/versions/1.2.3/about.md +9 -0
  69. package/data/project-types/ch/checkers/versions/1.2.3/game.json +77 -0
  70. package/data/project-types/ch/checkers/versions/1.2.3/manifest.json +182 -0
  71. package/data/project-types/ch/checkers/versions/1.2.3/mission.md +9 -0
  72. package/data/project-types/ch/checkers/versions/1.2.3/pages/board/index.html +1158 -0
  73. package/data/project-types/ch/chess/versions/1.0.6/about.md +9 -0
  74. package/data/project-types/ch/chess/versions/1.0.6/game.json +127 -0
  75. package/data/project-types/ch/chess/versions/1.0.6/manifest.json +196 -0
  76. package/data/project-types/ch/chess/versions/1.0.6/mission.md +8 -0
  77. package/data/project-types/ch/chess/versions/1.0.6/pages/board/index.html +851 -0
  78. package/data/project-types/fi/fitness-coach/versions/1.1.0/about.md +8 -0
  79. package/data/project-types/fi/fitness-coach/versions/1.1.0/craftbooks/training-recap.json +34 -0
  80. package/data/project-types/fi/fitness-coach/versions/1.1.0/manifest.json +229 -0
  81. package/data/project-types/fi/fitness-coach/versions/1.1.0/mission.md +7 -0
  82. package/data/project-types/fi/fitness-coach/versions/1.1.0/pages/dashboard/index.html +383 -0
  83. package/data/project-types/fi/fitness-coach/versions/1.1.0/training.json +6 -0
  84. package/data/project-types/fl/flashcards/versions/1.1.2/about.md +8 -0
  85. package/data/project-types/fl/flashcards/versions/1.1.2/deck.json +7 -0
  86. package/data/project-types/fl/flashcards/versions/1.1.2/manifest.json +169 -0
  87. package/data/project-types/fl/flashcards/versions/1.1.2/mission.md +6 -0
  88. package/data/project-types/fl/flashcards/versions/1.1.2/pages/review/index.html +477 -0
  89. package/data/project-types/fl/flashcards/versions/1.2.0/about.md +8 -0
  90. package/data/project-types/fl/flashcards/versions/1.2.0/deck.json +7 -0
  91. package/data/project-types/fl/flashcards/versions/1.2.0/manifest.json +207 -0
  92. package/data/project-types/fl/flashcards/versions/1.2.0/mission.md +6 -0
  93. package/data/project-types/fl/flashcards/versions/1.2.0/pages/review/index.html +549 -0
  94. package/data/project-types/go/go/versions/1.0.4/about.md +9 -0
  95. package/data/project-types/go/go/versions/1.0.4/game.json +103 -0
  96. package/data/project-types/go/go/versions/1.0.4/manifest.json +182 -0
  97. package/data/project-types/go/go/versions/1.0.4/mission.md +8 -0
  98. package/data/project-types/go/go/versions/1.0.4/pages/board/index.html +855 -0
  99. package/data/project-types/index.json +1 -1
  100. package/data/project-types/la/language-trainer/versions/1.1.0/about.md +8 -0
  101. package/data/project-types/la/language-trainer/versions/1.1.0/manifest.json +245 -0
  102. package/data/project-types/la/language-trainer/versions/1.1.0/mission.md +4 -0
  103. package/data/project-types/la/language-trainer/versions/1.1.0/pages/dashboard/index.html +468 -0
  104. package/data/project-types/la/language-trainer/versions/1.1.0/progress.json +10 -0
  105. package/data/project-types/la/language-trainer/versions/1.1.0/reviews.json +3 -0
  106. package/data/project-types/la/language-trainer/versions/1.1.0/scenarios.json +84 -0
  107. package/data/project-types/raw-index.json +1 -1
  108. package/data/project-types/wo/word-clues/manifest.json +20 -0
  109. package/data/project-types/wo/word-clues/versions/1.0.0/about.md +7 -0
  110. package/data/project-types/wo/word-clues/versions/1.0.0/game.json +14 -0
  111. package/data/project-types/wo/word-clues/versions/1.0.0/manifest.json +205 -0
  112. package/data/project-types/wo/word-clues/versions/1.0.0/mission.md +3 -0
  113. package/data/project-types/wo/word-clues/versions/1.0.0/pages/board/index.html +399 -0
  114. package/data/project-types/wo/word-clues/versions/1.0.0/words.json +216 -0
  115. package/package.json +5 -3
  116. package/schemas/craftbook-doc.schema.json +14 -0
  117. package/schemas/craftbook-template-version.schema.json +14 -0
  118. package/schemas/craftbook-test.schema.json +2 -0
  119. package/schemas/project-type-version.schema.json +131 -0
@@ -0,0 +1,383 @@
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>Fitness Coach</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #faf7f2; --card: #fff; --ink: #2b2620; --muted: #7a7166;
12
+ --accent: #b5651d; --track: #ece5da; --good: #4a7c59;
13
+ }
14
+ :root[data-theme='dark'] {
15
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
16
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d;
17
+ }
18
+ @media (prefers-color-scheme: dark) {
19
+ :root:not([data-theme='light']) {
20
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
21
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d;
22
+ }
23
+ }
24
+ * { box-sizing: border-box; }
25
+ body { margin: 0; font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--ink); padding: 1.5rem 1.25rem; }
26
+ main { max-width: 620px; margin: 0 auto; }
27
+ h1 { font-family: Georgia, 'PT Serif', 'Times New Roman', serif; font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
28
+ .sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 0.9rem; }
29
+ .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; margin-bottom: 0.75rem; }
30
+ .stat { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; }
31
+ .stat .num { font-size: 1.9rem; font-weight: 700; font-variant-numeric: tabular-nums; }
32
+ .stat .num.good { color: var(--good); }
33
+ .stat .lbl { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; }
34
+ .week { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; margin-bottom: 0.75rem; }
35
+ .week h2, .recent h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
36
+ .dots { display: flex; gap: 0.4rem; }
37
+ .dot { width: 22px; height: 22px; border-radius: 6px; background: var(--track); }
38
+ .dot.hit { background: var(--good); }
39
+ .week .note { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
40
+ .recent { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.9rem; }
41
+ .recent ul { margin: 0; padding-left: 1.05rem; }
42
+ .recent li { margin: 0.25rem 0; }
43
+ .recent .when { color: var(--muted); font-variant-numeric: tabular-nums; }
44
+ .empty { color: var(--muted); font-style: italic; }
45
+ .panel { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; margin-bottom: 0.75rem; }
46
+ .panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
47
+ .log-row { display: grid; grid-template-columns: minmax(0, 1fr) 6.5rem; gap: 0.5rem; }
48
+ .log-row input {
49
+ font: inherit; color: var(--ink); background: var(--bg);
50
+ border: 1px solid var(--track); border-radius: 6px; padding: 0.5rem; min-height: 44px; width: 100%;
51
+ }
52
+ .quick { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.5rem 0; }
53
+ button {
54
+ font: inherit; color: var(--ink); background: var(--card);
55
+ border: 1px solid var(--track); border-radius: 6px; min-height: 44px; padding: 0.35rem 0.8rem; cursor: pointer;
56
+ }
57
+ button:hover:not(:disabled) { border-color: var(--accent); }
58
+ button:disabled { opacity: 0.5; cursor: default; }
59
+ button.primary { border-color: var(--good); color: var(--good); }
60
+ .status { color: var(--muted); font-size: 0.85rem; margin: 0.4rem 0 0; }
61
+ .status.best { color: var(--good); font-weight: 600; }
62
+ .recap p { margin: 0.2rem 0; }
63
+ .demo-banner {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ gap: 0.75rem;
68
+ margin: 0 0 0.85rem;
69
+ padding: 0.5rem 0.65rem;
70
+ border: 1px solid var(--accent);
71
+ border-radius: 6px;
72
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
73
+ color: var(--ink);
74
+ font-size: 0.8rem;
75
+ }
76
+ .demo-banner[hidden] { display: none; }
77
+ .demo-banner strong { font-weight: 700; }
78
+ .demo-reset {
79
+ flex: none;
80
+ padding: 0.25rem 0.5rem;
81
+ border: 1px solid var(--accent);
82
+ border-radius: 6px;
83
+ background: var(--card);
84
+ color: var(--ink);
85
+ cursor: pointer;
86
+ font: inherit;
87
+ }
88
+ .demo-reset:hover { border-color: var(--ink); }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <main>
93
+ <div class="demo-banner" data-gezel-demo-banner hidden>
94
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
95
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
96
+ </div>
97
+ <h1 id="title">Fitness Coach</h1>
98
+ <p class="sub" id="sub">Loading the training log…</p>
99
+ <div class="panel">
100
+ <h2>Log a session</h2>
101
+ <div class="log-row">
102
+ <input id="activity" type="text" maxlength="60" placeholder="What did you do?" aria-label="Activity" />
103
+ <input id="minutes" type="number" min="1" max="600" inputmode="numeric" placeholder="Minutes" aria-label="Minutes" />
104
+ </div>
105
+ <div class="quick" id="quick"></div>
106
+ <button type="button" class="primary" id="log">Log it</button>
107
+ <p class="status" id="log-status" role="status"></p>
108
+ </div>
109
+ <div class="stats">
110
+ <div class="stat"><div class="num good" id="streak">—</div><div class="lbl">Day streak</div></div>
111
+ <div class="stat"><div class="num" id="total">—</div><div class="lbl">Sessions all time</div></div>
112
+ </div>
113
+ <div class="week">
114
+ <h2>This week</h2>
115
+ <div class="dots" id="dots"></div>
116
+ <div class="note" id="weekNote"></div>
117
+ </div>
118
+ <div class="panel recap">
119
+ <h2>Weekly recap</h2>
120
+ <div id="recap"><p class="empty">No recap yet.</p></div>
121
+ <button type="button" id="ask-recap">Write this week's recap</button>
122
+ <p class="status" id="recap-status" role="status"></p>
123
+ </div>
124
+ <div class="recent">
125
+ <h2>Recent sessions</h2>
126
+ <ul id="recent"><li class="empty">Nothing logged yet — tell your Coach about your first session.</li></ul>
127
+ </div>
128
+ </main>
129
+ <script>
130
+ /* gezel-page-demo-stub v1 */
131
+ /*
132
+ * The canonical demo stub for Gezel type pages. MIT License.
133
+ *
134
+ * Paste this file's bytes verbatim into a <script> block ahead of the
135
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
136
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
137
+ * injected and the stub steps aside; opened as a raw file (the live demos
138
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
139
+ * the page-supplied handler maps — one code path in every mode. `tools`
140
+ * and `data` map a tool name / declared read path to a function; data
141
+ * handlers back read() and list(), and url() returns the handler's value
142
+ * or passes a data: URI through. See docs/page-authoring.md.
143
+ */
144
+ (function () {
145
+ 'use strict';
146
+ window.makeDemoGezel = function (opts) {
147
+ if (window.gezel) return window.gezel;
148
+ var page = (opts && opts.page) || {};
149
+ var tools = (opts && opts.tools) || {};
150
+ var data = (opts && opts.data) || {};
151
+ var themeListeners = [];
152
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
153
+ var ui = {
154
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
155
+ onTheme: function (cb) {
156
+ themeListeners.push(cb);
157
+ return function () {
158
+ var at = themeListeners.indexOf(cb);
159
+ if (at >= 0) themeListeners.splice(at, 1);
160
+ };
161
+ },
162
+ };
163
+ if (media && media.addEventListener) {
164
+ media.addEventListener('change', function (ev) {
165
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
166
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
167
+ });
168
+ }
169
+ function fail(code, message) {
170
+ var err = new Error(message);
171
+ err.code = code;
172
+ return Promise.reject(err);
173
+ }
174
+ function run(map, key, arg) {
175
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
176
+ return Promise.resolve().then(function () { return map[key](arg); });
177
+ }
178
+ window.gezel = {
179
+ page: {
180
+ api: 1,
181
+ projectId: 'demo',
182
+ source: 'type',
183
+ entry: page.entry || 'index.html',
184
+ typeName: page.typeName || 'demo',
185
+ params: page.params || {},
186
+ mode: 'demo',
187
+ },
188
+ tools: {
189
+ list: function () { return Promise.resolve(Object.keys(tools)); },
190
+ invoke: function (name, input) {
191
+ var call = run(tools, name, input || {});
192
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
193
+ return call.then(function (output) { return { output: output }; });
194
+ },
195
+ },
196
+ data: {
197
+ read: function (path, o) {
198
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
199
+ },
200
+ list: function (path, o) {
201
+ return run(data, path, o || {}) || Promise.resolve([]);
202
+ },
203
+ watch: function () { return function () {}; },
204
+ url: function (path) {
205
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
206
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
207
+ },
208
+ },
209
+ ui: ui,
210
+ refresh: function () {},
211
+ };
212
+ return window.gezel;
213
+ };
214
+ })();
215
+ </script>
216
+ <script>
217
+ (function () {
218
+ 'use strict';
219
+ var DAY = 86400000;
220
+
221
+ // ── demo log (used only when the page opens as a raw file) ──
222
+ var demoTraining = {
223
+ goal: 'a comfortable 10K',
224
+ weeklyTarget: 4,
225
+ events: [
226
+ { at: new Date(Date.now() - 6 * DAY).toISOString(), data: { activity: 'Easy run', minutes: 32 } },
227
+ { at: new Date(Date.now() - 4 * DAY).toISOString(), data: { activity: 'Mobility and strength', minutes: 25 } },
228
+ { at: new Date(Date.now() - 2 * DAY).toISOString(), data: { activity: 'Tempo intervals', minutes: 38 } },
229
+ { at: new Date().toISOString(), data: { activity: 'Recovery walk', minutes: 28 } },
230
+ ],
231
+ };
232
+
233
+ var demoRecap = {
234
+ weekOf: new Date(Date.now() - 7 * DAY).toISOString().slice(0, 10),
235
+ sessions: 3, target: 4, minutes: 95, sessionsChange: 1, minutesChange: 20,
236
+ suggestion: 'Add one easy 20-minute run midweek to reach four sessions.',
237
+ };
238
+
239
+ makeDemoGezel({
240
+ page: { entry: 'dashboard/index.html', typeName: 'Fitness Coach', params: { goal: 'feeling strong and consistent', weeklyTarget: 3 } },
241
+ tools: {
242
+ page_log_workout: function (input) {
243
+ var minutes = Number(input.minutes) || 0;
244
+ var best = demoTraining.events.every(function (e) { return (e.data.minutes || 0) < minutes; });
245
+ demoTraining.events.push({ at: new Date().toISOString(), data: { activity: input.activity, minutes: minutes } });
246
+ return { personalBest: best && minutes > 0 };
247
+ },
248
+ request_recap: function () { return { status: 'recap_wanted' }; },
249
+ },
250
+ data: {
251
+ 'training.json': function () { return demoTraining; },
252
+ 'recaps/latest.json': function () { return demoRecap; },
253
+ },
254
+ });
255
+
256
+ // ── one code path from here: window.gezel exists in every mode ──
257
+ var demoMode = gezel.page.mode === 'demo';
258
+
259
+ function applyTheme(mode) {
260
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
261
+ }
262
+ applyTheme(gezel.ui.theme.mode);
263
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
264
+
265
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
266
+
267
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
268
+ function utcDay(iso) { return iso.slice(0, 10); }
269
+ function weekStartMs(nowIso) {
270
+ var d = new Date(nowIso.slice(0, 10) + 'T00:00:00Z');
271
+ return d.getTime() - ((d.getUTCDay() + 6) % 7) * DAY;
272
+ }
273
+ function streakDays(events) {
274
+ if (!events.length) return 0;
275
+ var days = {};
276
+ var last = null;
277
+ events.forEach(function (e) { days[utcDay(e.at)] = true; if (last === null || e.at > last) last = e.at; });
278
+ var streak = 0;
279
+ var cursor = Date.parse(utcDay(last) + 'T00:00:00Z');
280
+ while (days[new Date(cursor).toISOString().slice(0, 10)]) { streak += 1; cursor -= DAY; }
281
+ return streak;
282
+ }
283
+
284
+ function render(doc) {
285
+ var events = Array.isArray(doc.events) ? doc.events : [];
286
+ var target = Number(doc.weeklyTarget);
287
+ if (!isFinite(target) || target <= 0) target = 0;
288
+ if (doc.goal) document.getElementById('title').textContent = 'Training toward: ' + doc.goal;
289
+ document.getElementById('sub').textContent = events.length ? events.length + ' session(s) on record' : 'The record starts with the first session.';
290
+ document.getElementById('streak').textContent = String(streakDays(events));
291
+ document.getElementById('total').textContent = String(events.length);
292
+
293
+ var start = weekStartMs(new Date().toISOString());
294
+ var week = events.filter(function (e) { return Date.parse(e.at) >= start; }).length;
295
+ var slots = Math.max(target, week, 1);
296
+ var dots = '';
297
+ for (var i = 0; i < slots; i++) dots += '<span class="dot' + (i < week ? ' hit' : '') + '"></span>';
298
+ document.getElementById('dots').innerHTML = dots;
299
+ document.getElementById('weekNote').textContent = target > 0
300
+ ? week + ' of ' + target + ' session(s)' + (week >= target ? ' — target met.' : '.')
301
+ : week + ' session(s) this week.';
302
+
303
+ if (events.length) {
304
+ document.getElementById('recent').innerHTML = events.slice(-8).reverse().map(function (e) {
305
+ var mins = e.data && isFinite(Number(e.data.minutes)) && Number(e.data.minutes) > 0 ? ' (' + Number(e.data.minutes) + ' min)' : '';
306
+ return '<li><span class="when">' + esc(utcDay(e.at)) + '</span> ' + esc((e.data && e.data.activity) || 'session') + mins + '</li>';
307
+ }).join('');
308
+ }
309
+ }
310
+
311
+ function loadTraining() {
312
+ return gezel.data
313
+ .read('training.json')
314
+ .then(function (next) { if (next) render(next); })
315
+ .catch(function () {});
316
+ }
317
+
318
+ ['Run', 'Walk', 'Gym', 'Cycle', 'Swim', 'Yoga'].forEach(function (name) {
319
+ var b = document.createElement('button');
320
+ b.type = 'button';
321
+ b.textContent = name;
322
+ b.onclick = function () { document.getElementById('activity').value = name; };
323
+ document.getElementById('quick').appendChild(b);
324
+ });
325
+
326
+ function unavailable(err, where) {
327
+ return err && err.code === 'unavailable'
328
+ ? where + ' happens in the app’s Output pane.'
329
+ : (err && err.message) || 'That did not work. Try again.';
330
+ }
331
+
332
+ document.getElementById('log').onclick = function () {
333
+ var activity = document.getElementById('activity').value.trim();
334
+ var minutes = Number(document.getElementById('minutes').value);
335
+ var status = document.getElementById('log-status');
336
+ status.className = 'status';
337
+ if (!activity) { status.textContent = 'Say what you did first.'; return; }
338
+ var input = { activity: activity };
339
+ if (minutes > 0) input.minutes = minutes;
340
+ gezel.tools
341
+ .invoke('page_log_workout', input)
342
+ .then(function (res) {
343
+ var best = res && res.output && res.output.personalBest;
344
+ status.className = best ? 'status best' : 'status';
345
+ status.textContent = best ? 'Logged. That is your longest session yet.' : 'Logged.';
346
+ document.getElementById('activity').value = '';
347
+ document.getElementById('minutes').value = '';
348
+ return loadTraining();
349
+ })
350
+ .catch(function (err) { status.textContent = unavailable(err, 'Logging'); });
351
+ };
352
+
353
+ function renderRecap(r) {
354
+ if (!r) return;
355
+ var change = r.minutesChange === 0 ? 'same as last week' : (r.minutesChange > 0 ? '+' : '') + r.minutesChange + ' min vs last week';
356
+ document.getElementById('recap').innerHTML =
357
+ '<p><strong>Week of ' + esc(r.weekOf) + '</strong></p>' +
358
+ '<p>' + r.sessions + (r.target ? ' of ' + r.target : '') + ' session(s), ' + r.minutes + ' min (' + esc(change) + ').</p>' +
359
+ (r.suggestion ? '<p>Next week: ' + esc(r.suggestion) + '</p>' : '');
360
+ }
361
+ function loadRecap() {
362
+ return gezel.data.read('recaps/latest.json').then(renderRecap).catch(function () {});
363
+ }
364
+
365
+ document.getElementById('ask-recap').onclick = function () {
366
+ var status = document.getElementById('recap-status');
367
+ status.textContent = '';
368
+ gezel.tools
369
+ .invoke('request_recap', {})
370
+ .then(function () {
371
+ status.textContent = demoMode ? 'In the app, your coach writes it here.' : 'Your coach is writing it. It appears here when ready.';
372
+ })
373
+ .catch(function (err) { status.textContent = unavailable(err, 'Asking for a recap'); });
374
+ };
375
+
376
+ loadTraining();
377
+ loadRecap();
378
+ gezel.data.watch('training.json', function () { loadTraining(); });
379
+ gezel.data.watch('recaps/latest.json', function () { loadRecap(); });
380
+ })();
381
+ </script>
382
+ </body>
383
+ </html>
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 1,
3
+ "goal": "{{goal}}",
4
+ "weeklyTarget": {{weeklyTarget}},
5
+ "events": []
6
+ }
@@ -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,169 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.1.2",
4
+ "releasedAt": "2026-10-07T00: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', 'reminders'],\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\n// ── reminder-start ──\n// The next time a reviewed card comes due, worked out from the deck itself:\n// the one moment a reminder is earned. Nothing left to come due clears it.\n// Kept self-contained between these sentinels so unit tests execute the\n// shipped bytes.\nasync function remind(deck, nowMs) {\n var next = Infinity;\n for (var i = 0; i < deck.cards.length; i += 1) {\n var card = deck.cards[i];\n if (!card.lastReviewedAt) continue;\n var dueAt = Date.parse(card.lastReviewedAt) + (BOX_INTERVAL_DAYS[card.box] || 1) * DAY_MS;\n if (dueAt > nowMs && dueAt < next) next = dueAt;\n }\n try {\n if (!isFinite(next)) {\n await gezel.reminder.clear();\n return;\n }\n var count = computeDueIds(deck.cards, next).length;\n await gezel.reminder.set({\n at: new Date(next).toISOString(),\n title: 'Flashcards are due',\n body: count + (count === 1 ? ' card' : ' cards') + ' on \"' + deck.subject + '\" ready to review.',\n });\n } catch (err) {\n // A host without reminders still keeps the deck.\n }\n}\n// ── reminder-end ──\n\nasync function persist(deck: Deck): Promise<Deck> {\n const nowMs = Date.now();\n deck.dueCardIds = computeDueIds(deck.cards, nowMs);\n deck.updatedAt = new Date(nowMs).toISOString();\n await gezel.fs.write(DECK_FILE, `${JSON.stringify(deck, null, 2)}\\n`);\n await remind(deck, nowMs);\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
+ "minGezelVersion": "1.26280"
169
+ }
@@ -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.