@bendyline/gilde 0.1.85 → 0.1.87

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 (106) hide show
  1. package/data/craftbook-templates/index.json +1 -1
  2. package/data/craftbook-templates/raw-index.json +1 -1
  3. package/data/craftbook-templates/ta/tailor-resume/versions/1.0.5/craftbook.json +226 -0
  4. package/data/craftbook-templates/ta/tailor-resume/versions/1.0.5/test.json +79 -0
  5. package/data/knowledge-catalogs/ca/caselaw-alaska-part-0001/manifest.json +24 -0
  6. package/data/knowledge-catalogs/ca/caselaw-alaska-part-0001/versions/2026.10.1/manifest.json +658 -0
  7. package/data/knowledge-catalogs/ca/caselaw-us-347-349/manifest.json +25 -0
  8. package/data/knowledge-catalogs/ca/caselaw-us-347-349/versions/2026.10.5/manifest.json +38 -0
  9. package/data/knowledge-catalogs/index.json +1 -1
  10. package/data/knowledge-catalogs/raw-index.json +1 -1
  11. package/data/project-types/ch/checkers/versions/1.2.1/about.md +9 -0
  12. package/data/project-types/ch/checkers/versions/1.2.1/game.json +77 -0
  13. package/data/project-types/ch/checkers/versions/1.2.1/manifest.json +168 -0
  14. package/data/project-types/ch/checkers/versions/1.2.1/mission.md +9 -0
  15. package/data/project-types/ch/checkers/versions/1.2.1/pages/board/index.html +1158 -0
  16. package/data/project-types/ch/chess/versions/1.0.4/about.md +9 -0
  17. package/data/project-types/ch/chess/versions/1.0.4/game.json +127 -0
  18. package/data/project-types/ch/chess/versions/1.0.4/manifest.json +182 -0
  19. package/data/project-types/ch/chess/versions/1.0.4/mission.md +8 -0
  20. package/data/project-types/ch/chess/versions/1.0.4/pages/board/index.html +851 -0
  21. package/data/project-types/de/design-scheme/versions/1.0.2/about.md +6 -0
  22. package/data/project-types/de/design-scheme/versions/1.0.2/manifest.json +70 -0
  23. package/data/project-types/de/design-scheme/versions/1.0.2/mission.md +3 -0
  24. package/data/project-types/de/design-scheme/versions/1.0.2/pages/gallery/index.html +212 -0
  25. package/data/project-types/de/design-scheme/versions/1.0.2/schemes/index.json +9 -0
  26. package/data/project-types/de/design-scheme/versions/1.0.2/schemes/warm-dusk.json +4 -0
  27. package/data/project-types/ev/event-planner/versions/1.0.2/about.md +7 -0
  28. package/data/project-types/ev/event-planner/versions/1.0.2/budget.json +5 -0
  29. package/data/project-types/ev/event-planner/versions/1.0.2/guests.json +10 -0
  30. package/data/project-types/ev/event-planner/versions/1.0.2/manifest.json +171 -0
  31. package/data/project-types/ev/event-planner/versions/1.0.2/mission.md +6 -0
  32. package/data/project-types/ev/event-planner/versions/1.0.2/pages/dashboard/index.html +283 -0
  33. package/data/project-types/fi/fitness-coach/versions/1.0.2/about.md +7 -0
  34. package/data/project-types/fi/fitness-coach/versions/1.0.2/manifest.json +126 -0
  35. package/data/project-types/fi/fitness-coach/versions/1.0.2/mission.md +7 -0
  36. package/data/project-types/fi/fitness-coach/versions/1.0.2/pages/dashboard/index.html +273 -0
  37. package/data/project-types/fi/fitness-coach/versions/1.0.2/training.json +6 -0
  38. package/data/project-types/fl/flashcards/versions/1.1.1/about.md +8 -0
  39. package/data/project-types/fl/flashcards/versions/1.1.1/deck.json +7 -0
  40. package/data/project-types/fl/flashcards/versions/1.1.1/manifest.json +168 -0
  41. package/data/project-types/fl/flashcards/versions/1.1.1/mission.md +6 -0
  42. package/data/project-types/fl/flashcards/versions/1.1.1/pages/review/index.html +477 -0
  43. package/data/project-types/fr/freelance-office/versions/1.0.2/about.md +6 -0
  44. package/data/project-types/fr/freelance-office/versions/1.0.2/invoices.json +9 -0
  45. package/data/project-types/fr/freelance-office/versions/1.0.2/manifest.json +143 -0
  46. package/data/project-types/fr/freelance-office/versions/1.0.2/mission.md +6 -0
  47. package/data/project-types/fr/freelance-office/versions/1.0.2/pages/dashboard/index.html +258 -0
  48. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/about.md +7 -0
  49. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/donations.json +7 -0
  50. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/manifest.json +127 -0
  51. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/mission.md +6 -0
  52. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/pages/dashboard/index.html +261 -0
  53. package/data/project-types/go/go/versions/1.0.2/about.md +9 -0
  54. package/data/project-types/go/go/versions/1.0.2/game.json +103 -0
  55. package/data/project-types/go/go/versions/1.0.2/manifest.json +168 -0
  56. package/data/project-types/go/go/versions/1.0.2/mission.md +8 -0
  57. package/data/project-types/go/go/versions/1.0.2/pages/board/index.html +855 -0
  58. package/data/project-types/ho/household-budget/versions/1.0.3/about.md +6 -0
  59. package/data/project-types/ho/household-budget/versions/1.0.3/ledger.json +5 -0
  60. package/data/project-types/ho/household-budget/versions/1.0.3/manifest.json +164 -0
  61. package/data/project-types/ho/household-budget/versions/1.0.3/mission.md +5 -0
  62. package/data/project-types/ho/household-budget/versions/1.0.3/pages/dashboard/index.html +340 -0
  63. package/data/project-types/ho/household-manual/versions/1.0.2/about.md +7 -0
  64. package/data/project-types/ho/household-manual/versions/1.0.2/manifest.json +153 -0
  65. package/data/project-types/ho/household-manual/versions/1.0.2/mission.md +8 -0
  66. package/data/project-types/ho/household-manual/versions/1.0.2/pages/dashboard/index.html +258 -0
  67. package/data/project-types/ho/household-manual/versions/1.0.2/systems.json +8 -0
  68. package/data/project-types/index.json +1 -1
  69. package/data/project-types/jo/job-hunt/versions/1.0.2/about.md +10 -0
  70. package/data/project-types/jo/job-hunt/versions/1.0.2/activity.json +4 -0
  71. package/data/project-types/jo/job-hunt/versions/1.0.2/manifest.json +213 -0
  72. package/data/project-types/jo/job-hunt/versions/1.0.2/mission.md +9 -0
  73. package/data/project-types/jo/job-hunt/versions/1.0.2/pages/dashboard/index.html +430 -0
  74. package/data/project-types/jo/job-hunt/versions/1.0.2/pipeline.json +6 -0
  75. package/data/project-types/la/language-trainer/versions/1.0.2/about.md +6 -0
  76. package/data/project-types/la/language-trainer/versions/1.0.2/manifest.json +80 -0
  77. package/data/project-types/la/language-trainer/versions/1.0.2/mission.md +4 -0
  78. package/data/project-types/la/language-trainer/versions/1.0.2/pages/dashboard/index.html +268 -0
  79. package/data/project-types/la/language-trainer/versions/1.0.2/progress.json +7 -0
  80. package/data/project-types/li/life-binder/versions/1.0.3/about.md +7 -0
  81. package/data/project-types/li/life-binder/versions/1.0.3/documents.json +145 -0
  82. package/data/project-types/li/life-binder/versions/1.0.3/manifest.json +138 -0
  83. package/data/project-types/li/life-binder/versions/1.0.3/mission.md +6 -0
  84. package/data/project-types/li/life-binder/versions/1.0.3/pages/dashboard/index.html +248 -0
  85. package/data/project-types/me/meal-planner/versions/1.0.2/about.md +6 -0
  86. package/data/project-types/me/meal-planner/versions/1.0.2/manifest.json +160 -0
  87. package/data/project-types/me/meal-planner/versions/1.0.2/menu.json +5 -0
  88. package/data/project-types/me/meal-planner/versions/1.0.2/mission.md +5 -0
  89. package/data/project-types/me/meal-planner/versions/1.0.2/pages/dashboard/index.html +248 -0
  90. package/data/project-types/me/meal-planner/versions/1.0.2/pantry.json +9 -0
  91. package/data/project-types/no/novel-writing-room/versions/1.0.2/about.md +7 -0
  92. package/data/project-types/no/novel-writing-room/versions/1.0.2/chapters.json +10 -0
  93. package/data/project-types/no/novel-writing-room/versions/1.0.2/manifest.json +158 -0
  94. package/data/project-types/no/novel-writing-room/versions/1.0.2/mission.md +7 -0
  95. package/data/project-types/no/novel-writing-room/versions/1.0.2/pages/dashboard/index.html +264 -0
  96. package/data/project-types/no/novel-writing-room/versions/1.0.2/sessions.json +6 -0
  97. package/data/project-types/raw-index.json +1 -1
  98. package/data/project-types/tr/trip-planner/versions/1.0.2/about.md +7 -0
  99. package/data/project-types/tr/trip-planner/versions/1.0.2/journal.json +4 -0
  100. package/data/project-types/tr/trip-planner/versions/1.0.2/manifest.json +194 -0
  101. package/data/project-types/tr/trip-planner/versions/1.0.2/mission.md +4 -0
  102. package/data/project-types/tr/trip-planner/versions/1.0.2/packing.json +8 -0
  103. package/data/project-types/tr/trip-planner/versions/1.0.2/pages/dashboard/index.html +308 -0
  104. package/data/project-types/tr/trip-planner/versions/1.0.2/trip.json +7 -0
  105. package/package.json +1 -1
  106. package/schemas/project-type-version.schema.json +13 -0
@@ -0,0 +1,283 @@
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>Event planner</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; --warn: #a4552e;
13
+ }
14
+ :root[data-theme='dark'] {
15
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
16
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
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; --warn: #d98a63;
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: 680px; 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
+ .tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6rem; margin-bottom: 0.9rem; }
30
+ .tile { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.7rem 0.85rem; }
31
+ .tile .n { font-size: 1.35rem; font-weight: 700; color: var(--accent); }
32
+ .tile .n.good { color: var(--good); }
33
+ .tile .l { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
34
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
35
+ .panel { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.9rem; }
36
+ .panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
37
+ .panel ul { margin: 0; padding-left: 1.05rem; }
38
+ .panel li { margin: 0.2rem 0; }
39
+ .rsvp { color: var(--muted); font-size: 0.78rem; }
40
+ .rsvp.yes { color: var(--good); }
41
+ .rsvp.no { color: var(--warn); }
42
+ .bar-row { display: grid; grid-template-columns: 6.5rem 1fr 4.5rem; gap: 0.5rem; align-items: center; margin: 0.3rem 0; }
43
+ .bar { background: var(--track); border-radius: 4px; height: 10px; overflow: hidden; }
44
+ .bar > span { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
45
+ .amt { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
46
+ .empty { color: var(--muted); font-style: italic; }
47
+ @media (max-width: 640px) { .cols { grid-template-columns: 1fr; } .tiles { grid-template-columns: 1fr 1fr; } }
48
+ .demo-banner {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ gap: 0.75rem;
53
+ margin: 0 0 0.85rem;
54
+ padding: 0.5rem 0.65rem;
55
+ border: 1px solid var(--accent);
56
+ border-radius: 6px;
57
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
58
+ color: var(--ink);
59
+ font-size: 0.8rem;
60
+ }
61
+ .demo-banner[hidden] { display: none; }
62
+ .demo-banner strong { font-weight: 700; }
63
+ .demo-reset {
64
+ flex: none;
65
+ padding: 0.25rem 0.5rem;
66
+ border: 1px solid var(--accent);
67
+ border-radius: 6px;
68
+ background: var(--card);
69
+ color: var(--ink);
70
+ cursor: pointer;
71
+ font: inherit;
72
+ }
73
+ .demo-reset:hover { border-color: var(--ink); }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <main>
78
+ <div class="demo-banner" data-gezel-demo-banner hidden>
79
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
80
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
81
+ </div>
82
+ <h1 id="title">Event planner</h1>
83
+ <p class="sub" id="sub">Loading the plan…</p>
84
+ <div class="tiles">
85
+ <div class="tile"><div class="n" id="t-days">—</div><div class="l">Days to go</div></div>
86
+ <div class="tile"><div class="n good" id="t-coming">—</div><div class="l">Coming</div></div>
87
+ <div class="tile"><div class="n" id="t-await">—</div><div class="l">Awaiting reply</div></div>
88
+ <div class="tile"><div class="n" id="t-spent">—</div><div class="l">Spent</div></div>
89
+ </div>
90
+ <div class="cols">
91
+ <div class="panel">
92
+ <h2>Guest list</h2>
93
+ <ul id="guests"><li class="empty">No guests yet — start the list with your Ceremoniemeester.</li></ul>
94
+ </div>
95
+ <div class="panel">
96
+ <h2>Where the budget went</h2>
97
+ <div id="budget"><p class="empty">Nothing committed yet.</p></div>
98
+ </div>
99
+ </div>
100
+ </main>
101
+ <script>
102
+ /* gezel-page-demo-stub v1 */
103
+ /*
104
+ * The canonical demo stub for Gezel type pages. MIT License.
105
+ *
106
+ * Paste this file's bytes verbatim into a <script> block ahead of the
107
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
108
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
109
+ * injected and the stub steps aside; opened as a raw file (the live demos
110
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
111
+ * the page-supplied handler maps — one code path in every mode. `tools`
112
+ * and `data` map a tool name / declared read path to a function; data
113
+ * handlers back read() and list(), and url() returns the handler's value
114
+ * or passes a data: URI through. See docs/page-authoring.md.
115
+ */
116
+ (function () {
117
+ 'use strict';
118
+ window.makeDemoGezel = function (opts) {
119
+ if (window.gezel) return window.gezel;
120
+ var page = (opts && opts.page) || {};
121
+ var tools = (opts && opts.tools) || {};
122
+ var data = (opts && opts.data) || {};
123
+ var themeListeners = [];
124
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
125
+ var ui = {
126
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
127
+ onTheme: function (cb) {
128
+ themeListeners.push(cb);
129
+ return function () {
130
+ var at = themeListeners.indexOf(cb);
131
+ if (at >= 0) themeListeners.splice(at, 1);
132
+ };
133
+ },
134
+ };
135
+ if (media && media.addEventListener) {
136
+ media.addEventListener('change', function (ev) {
137
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
138
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
139
+ });
140
+ }
141
+ function fail(code, message) {
142
+ var err = new Error(message);
143
+ err.code = code;
144
+ return Promise.reject(err);
145
+ }
146
+ function run(map, key, arg) {
147
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
148
+ return Promise.resolve().then(function () { return map[key](arg); });
149
+ }
150
+ window.gezel = {
151
+ page: {
152
+ api: 1,
153
+ projectId: 'demo',
154
+ source: 'type',
155
+ entry: page.entry || 'index.html',
156
+ typeName: page.typeName || 'demo',
157
+ params: page.params || {},
158
+ mode: 'demo',
159
+ },
160
+ tools: {
161
+ list: function () { return Promise.resolve(Object.keys(tools)); },
162
+ invoke: function (name, input) {
163
+ var call = run(tools, name, input || {});
164
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
165
+ return call.then(function (output) { return { output: output }; });
166
+ },
167
+ },
168
+ data: {
169
+ read: function (path, o) {
170
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
171
+ },
172
+ list: function (path, o) {
173
+ return run(data, path, o || {}) || Promise.resolve([]);
174
+ },
175
+ watch: function () { return function () {}; },
176
+ url: function (path) {
177
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
178
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
179
+ },
180
+ },
181
+ ui: ui,
182
+ refresh: function () {},
183
+ };
184
+ return window.gezel;
185
+ };
186
+ })();
187
+ </script>
188
+ <script>
189
+ (function () {
190
+ 'use strict';
191
+
192
+ // ── demo plan (used only when the page opens as a raw file) ──
193
+ var demoGuests = {
194
+ eventDate: new Date(Date.now() + 28 * 86400000).toISOString(),
195
+ records: [
196
+ { id: 'guest-1', status: 'yes', fields: { name: 'Mara & Jo', party: 2 } },
197
+ { id: 'guest-2', status: 'yes', fields: { name: 'The De Vries family', party: 4 } },
198
+ { id: 'guest-3', status: 'invited', fields: { name: 'Samir Patel', party: 1 } },
199
+ { id: 'guest-4', status: 'invited', fields: { name: 'Anika Bose', party: 2 } },
200
+ { id: 'guest-5', status: 'no', fields: { name: 'Leo Martins', party: 1 } },
201
+ ],
202
+ };
203
+ var demoBudget = { entries: [
204
+ { kind: 'out', category: 'Venue', amountCents: 185000 },
205
+ { kind: 'out', category: 'Food', amountCents: 92000 },
206
+ { kind: 'out', category: 'Flowers', amountCents: 36000 },
207
+ { kind: 'out', category: 'Music', amountCents: 54000 },
208
+ ] };
209
+
210
+ makeDemoGezel({
211
+ page: { entry: 'dashboard/index.html', typeName: 'Event Planner', params: { occasion: 'a big celebration', date: '2026-12-31' } },
212
+ data: {
213
+ 'guests.json': function () { return demoGuests; },
214
+ 'budget.json': function () { return demoBudget; },
215
+ },
216
+ });
217
+
218
+ // ── one code path from here: window.gezel exists in every mode ──
219
+ var demoMode = gezel.page.mode === 'demo';
220
+
221
+ function applyTheme(mode) {
222
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
223
+ }
224
+ applyTheme(gezel.ui.theme.mode);
225
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
226
+
227
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
228
+
229
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
230
+ function fmt(cents) { var abs = Math.abs(cents); return (cents < 0 ? '-' : '') + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0'); }
231
+ function partyOf(r) { var p = r.fields && r.fields.party; return typeof p === 'number' && p >= 1 ? Math.floor(p) : 1; }
232
+
233
+ function render(guestsDoc, budgetDoc) {
234
+ var records = (guestsDoc && guestsDoc.records) || [];
235
+ var entries = (budgetDoc && budgetDoc.entries) || [];
236
+ var coming = 0, awaiting = 0;
237
+ records.forEach(function (r) {
238
+ if (r.status === 'yes') coming += partyOf(r);
239
+ if (r.status === 'invited') awaiting += partyOf(r);
240
+ });
241
+ var spent = 0; var byCat = {};
242
+ entries.forEach(function (e) {
243
+ if (e.kind === 'out') { spent += e.amountCents; byCat[e.category] = (byCat[e.category] || 0) + e.amountCents; }
244
+ });
245
+ var days = guestsDoc && guestsDoc.eventDate ? Math.ceil((Date.parse(guestsDoc.eventDate) - Date.now()) / 86400000) : null;
246
+
247
+ document.getElementById('sub').textContent =
248
+ (guestsDoc && guestsDoc.eventDate ? 'The day: ' + String(guestsDoc.eventDate).slice(0, 10) + ' · ' : '') +
249
+ records.length + ' invitation(s) on the list';
250
+ document.getElementById('t-days').textContent = days === null || !isFinite(days) ? '—' : Math.max(0, days);
251
+ document.getElementById('t-coming').textContent = coming;
252
+ document.getElementById('t-await').textContent = awaiting;
253
+ document.getElementById('t-spent').textContent = fmt(spent);
254
+
255
+ if (records.length) {
256
+ document.getElementById('guests').innerHTML = records.slice(0, 12).map(function (r) {
257
+ return '<li>' + esc((r.fields && r.fields.name) || r.id) + (partyOf(r) > 1 ? ' (+' + (partyOf(r) - 1) + ')' : '') +
258
+ ' <span class="rsvp ' + r.status + '">' + (r.status === 'invited' ? 'awaiting' : r.status) + '</span></li>';
259
+ }).join('') + (records.length > 12 ? '<li class="empty">and ' + (records.length - 12) + ' more…</li>' : '');
260
+ }
261
+ var cats = Object.entries(byCat).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 6);
262
+ if (cats.length) {
263
+ var max = cats[0][1];
264
+ document.getElementById('budget').innerHTML = cats.map(function (pair) {
265
+ return '<div class="bar-row"><span>' + esc(pair[0]) + '</span><span class="bar"><span style="width:' + Math.max(4, Math.round((pair[1] / max) * 100)) + '%"></span></span><span class="amt">' + fmt(pair[1]) + '</span></div>';
266
+ }).join('');
267
+ }
268
+ }
269
+
270
+ function loadPlan() {
271
+ return Promise.all([
272
+ gezel.data.read('guests.json').catch(function () { return { records: [] }; }),
273
+ gezel.data.read('budget.json').catch(function () { return { entries: [] }; }),
274
+ ]).then(function (results) { render(results[0], results[1]); });
275
+ }
276
+
277
+ loadPlan();
278
+ gezel.data.watch('guests.json', function () { loadPlan(); });
279
+ gezel.data.watch('budget.json', function () { loadPlan(); });
280
+ })();
281
+ </script>
282
+ </body>
283
+ </html>
@@ -0,0 +1,7 @@
1
+ Training toward **{{goal}}**, {{weeklyTarget}} session(s) a week. Your Coach
2
+ logs every workout the moment you report it, celebrates the streak, and keeps
3
+ the plan sized to the life you actually have — smaller when the week is hard,
4
+ growing again when it isn't.
5
+
6
+ The **Output** tab is the training board: the streak, this week against the
7
+ target, and the recent sessions.
@@ -0,0 +1,126 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.2",
4
+ "releasedAt": "2026-10-06T00:00:00Z",
5
+ "meesterManaged": false,
6
+ "params": {
7
+ "type": "object",
8
+ "properties": {
9
+ "goal": {
10
+ "type": "string",
11
+ "title": "What are you training toward?",
12
+ "description": "e.g. 'feel strong again', 'a 10k in October'.",
13
+ "default": "feeling strong and consistent"
14
+ },
15
+ "weeklyTarget": {
16
+ "type": "number",
17
+ "title": "Sessions per week",
18
+ "description": "How many training sessions a week to aim for.",
19
+ "default": 3
20
+ }
21
+ },
22
+ "required": [
23
+ "goal",
24
+ "weeklyTarget"
25
+ ]
26
+ },
27
+ "nameTemplate": "Fitness: {{goal}}",
28
+ "aboutTemplate": "about.md",
29
+ "missionTemplate": "mission.md",
30
+ "gezels": [
31
+ {
32
+ "templateId": "coach",
33
+ "voorman": true
34
+ }
35
+ ],
36
+ "tools": [
37
+ {
38
+ "name": "log_workout",
39
+ "description": "Log a training session the moment it is reported (activity + minutes).",
40
+ "script": "training-store",
41
+ "inputs": {
42
+ "type": "object",
43
+ "properties": {
44
+ "activity": {
45
+ "type": "string"
46
+ },
47
+ "minutes": {
48
+ "type": "number"
49
+ },
50
+ "note": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "required": [
55
+ "activity"
56
+ ]
57
+ },
58
+ "bind": {
59
+ "action": "log_workout"
60
+ }
61
+ },
62
+ {
63
+ "name": "set_target",
64
+ "description": "Change the weekly session target.",
65
+ "script": "training-store",
66
+ "inputs": {
67
+ "type": "object",
68
+ "properties": {
69
+ "weeklyTarget": {
70
+ "type": "number"
71
+ }
72
+ },
73
+ "required": [
74
+ "weeklyTarget"
75
+ ]
76
+ },
77
+ "bind": {
78
+ "action": "set_target"
79
+ }
80
+ },
81
+ {
82
+ "name": "training_status",
83
+ "description": "Streak, this week against the target, all-time count.",
84
+ "script": "training-store",
85
+ "inputs": {
86
+ "type": "object",
87
+ "properties": {}
88
+ },
89
+ "bind": {
90
+ "action": "status"
91
+ }
92
+ },
93
+ {
94
+ "name": "workout_history",
95
+ "description": "The recent sessions, most recent last.",
96
+ "script": "training-store",
97
+ "inputs": {
98
+ "type": "object",
99
+ "properties": {}
100
+ },
101
+ "bind": {
102
+ "action": "history"
103
+ }
104
+ }
105
+ ],
106
+ "craftbooks": [
107
+ "weekly-review"
108
+ ],
109
+ "schedules": [],
110
+ "pages": {
111
+ "entry": "dashboard/index.html",
112
+ "api": 1,
113
+ "reads": [
114
+ {
115
+ "source": "workspace",
116
+ "path": "training.json"
117
+ }
118
+ ]
119
+ },
120
+ "workspaceSeed": [
121
+ "training.json"
122
+ ],
123
+ "scripts": {
124
+ "training-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { logStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'training-store',\n description:\n \"Read and update the training log (training.json). Call action:'status' for streak + this week vs the target; 'log_workout' the moment a session is reported (activity + minutes); 'set_target' to change the weekly session target; 'history' for the recent sessions.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'status', 'log_workout', 'set_target', or 'history'.\",\n required: true,\n },\n activity: { type: 'string', description: \"What they did, e.g. 'run', 'gym', 'long walk'.\" },\n minutes: { type: 'number', description: 'How long the session ran, in minutes.' },\n note: { type: 'string', description: 'Anything worth remembering about the session.' },\n weeklyTarget: { type: 'number', description: 'Sessions per week to aim for (set_target).' },\n },\n outputs: {\n streakDays: { type: 'number', description: 'Consecutive days with a session, ending at the latest.' },\n thisWeek: { type: 'number', description: 'Sessions logged this week (Monday UTC start).' },\n weeklyTarget: { type: 'number', description: 'The current weekly session target.' },\n summary: { type: 'string', description: 'The training picture.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── fitness-rules-start ──\n// Pure derivations over logStore-shaped workout events: kind 'workout',\n// data {activity, minutes}. Weeks start Monday 00:00 UTC; nowIso is a\n// parameter so the rules stay clock-independent and testable.\n\nfunction weekStartMs(nowIso) {\n var day = new Date(nowIso.slice(0, 10) + 'T00:00:00Z');\n var sinceMonday = (day.getUTCDay() + 6) % 7;\n return day.getTime() - sinceMonday * 86400000;\n}\n\nfunction thisWeekCount(events, nowIso) {\n var start = weekStartMs(nowIso);\n return events.filter(function (e) {\n return Date.parse(e.at) >= start;\n }).length;\n}\n\nfunction trainingLine(stats, weekCount, weeklyTarget) {\n var target = weeklyTarget > 0 ? weeklyTarget : 0;\n var weekPart =\n 'This week: ' +\n weekCount +\n (target > 0 ? ' of ' + target + ' session(s)' : ' session(s)') +\n (target > 0 && weekCount >= target ? ' — target met.' : '.');\n var streakPart =\n stats.total === 0\n ? 'No sessions logged yet.'\n : 'Streak: ' + stats.streakDays + ' day(s); ' + stats.total + ' session(s) all time.';\n return weekPart + ' ' + streakPart;\n}\n// ── fitness-rules-end ──\n\nconst training = logStore(gezel.fs, 'training.json');\n\nasync function readWeeklyTarget(): Promise<number> {\n try {\n const parsed = JSON.parse(await gezel.fs.read('training.json')) as { weeklyTarget?: unknown };\n const target = Number(parsed.weeklyTarget);\n return Number.isFinite(target) && target > 0 ? Math.round(target) : 0;\n } catch {\n return 0;\n }\n}\n\nif (input.action === 'log_workout') {\n if (!input.activity) throw new Error(\"log_workout needs 'activity' (what they did).\");\n await training.append({\n kind: 'workout',\n ...(input.note ? { note: input.note } : {}),\n data: {\n activity: input.activity,\n ...(typeof input.minutes === 'number' && input.minutes > 0\n ? { minutes: Math.round(input.minutes) }\n : {}),\n },\n });\n} else if (input.action === 'set_target') {\n const target = Number(input.weeklyTarget);\n if (!Number.isFinite(target) || target <= 0) {\n throw new Error(\"set_target needs a positive 'weeklyTarget' (sessions per week).\");\n }\n // logStore has no field API; the passenger key flips via a targeted\n // read-modify-write that preserves the rest of the file.\n const raw = JSON.parse(await gezel.fs.read('training.json')) as Record<string, unknown>;\n raw.weeklyTarget = Math.round(target);\n await gezel.fs.write('training.json', `${JSON.stringify(raw, null, 2)}\\n`);\n} else if (input.action !== 'status' && input.action !== 'history') {\n throw new Error(`Unknown action '${input.action}'. Use status, log_workout, set_target, or history.`);\n}\n\nconst events = await training.list();\nconst stats = await training.stats();\nconst weeklyTarget = await readWeeklyTarget();\nconst nowIso = new Date().toISOString();\nconst weekCount = thisWeekCount(events, nowIso);\nconst listing =\n input.action === 'history'\n ? events\n .slice(-12)\n .map(\n (e) =>\n `${e.at.slice(0, 10)} ${String(e.data?.activity ?? 'session')}${typeof e.data?.minutes === 'number' ? ` (${e.data.minutes} min)` : ''}${e.note ? ` — ${e.note}` : ''}`,\n )\n .join('\\n') || 'No sessions yet.'\n : '';\n\ngezel.output({\n streakDays: stats.streakDays,\n thisWeek: weekCount,\n weeklyTarget,\n summary: listing || trainingLine(stats, weekCount, weeklyTarget),\n});\n"
125
+ }
126
+ }
@@ -0,0 +1,7 @@
1
+ - Build a training rhythm that holds: {{weeklyTarget}} session(s) a week toward
2
+ {{goal}}.
3
+ - Every session gets logged the day it happens — the streak and the weekly
4
+ count come from the record, never from memory.
5
+ - Consistency over intensity: a shrunk plan that survives a hard week beats an
6
+ ambitious one that dies in it.
7
+ - Encouragement always, guilt never.