@bendyline/gilde 0.1.7 → 0.1.8
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.
- package/README.md +17 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/about.md +8 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/care.json +4 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/manifest.json +177 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/meds.json +8 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/mission.md +8 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/pages/dashboard/index.html +167 -0
- package/data/project-types/ch/checkers/versions/1.1.2/about.md +9 -0
- package/data/project-types/ch/checkers/versions/1.1.2/game.json +77 -0
- package/data/project-types/ch/checkers/versions/1.1.2/manifest.json +166 -0
- package/data/project-types/ch/checkers/versions/1.1.2/mission.md +9 -0
- package/data/project-types/ch/checkers/versions/1.1.2/pages/board/index.html +1091 -0
- package/data/project-types/ch/chess/versions/1.0.2/about.md +9 -0
- package/data/project-types/ch/chess/versions/1.0.2/game.json +127 -0
- package/data/project-types/ch/chess/versions/1.0.2/manifest.json +180 -0
- package/data/project-types/ch/chess/versions/1.0.2/mission.md +8 -0
- package/data/project-types/ch/chess/versions/1.0.2/pages/board/index.html +627 -0
- package/data/project-types/ch/chess/versions/1.0.3/about.md +9 -0
- package/data/project-types/ch/chess/versions/1.0.3/game.json +127 -0
- package/data/project-types/ch/chess/versions/1.0.3/manifest.json +180 -0
- package/data/project-types/ch/chess/versions/1.0.3/mission.md +8 -0
- package/data/project-types/ch/chess/versions/1.0.3/pages/board/index.html +769 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/about.md +6 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/manifest.json +69 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/mission.md +3 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/pages/gallery/index.html +105 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/schemes/index.json +9 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/schemes/warm-dusk.json +4 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/about.md +7 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/budget.json +5 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/guests.json +10 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/manifest.json +170 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/mission.md +6 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/pages/dashboard/index.html +186 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/about.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/manifest.json +125 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/mission.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/pages/dashboard/index.html +172 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/training.json +6 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/about.md +8 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/deck.json +7 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/manifest.json +167 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/mission.md +6 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/pages/review/index.html +396 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/about.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/invoices.json +9 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/manifest.json +142 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/mission.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/pages/dashboard/index.html +157 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/about.md +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/donations.json +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/manifest.json +126 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/mission.md +6 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/pages/dashboard/index.html +160 -0
- package/data/project-types/go/go/versions/1.0.1/about.md +9 -0
- package/data/project-types/go/go/versions/1.0.1/game.json +103 -0
- package/data/project-types/go/go/versions/1.0.1/manifest.json +166 -0
- package/data/project-types/go/go/versions/1.0.1/mission.md +8 -0
- package/data/project-types/go/go/versions/1.0.1/pages/board/index.html +778 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/about.md +6 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/ledger.json +5 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/manifest.json +163 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/mission.md +6 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/pages/dashboard/index.html +233 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/about.md +7 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/manifest.json +152 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/mission.md +8 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/pages/dashboard/index.html +157 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/systems.json +8 -0
- package/data/project-types/index.json +1 -1
- package/data/project-types/jo/job-hunt/versions/1.0.1/about.md +10 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/activity.json +4 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/manifest.json +212 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/mission.md +9 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/pages/dashboard/index.html +327 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/pipeline.json +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/about.md +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/manifest.json +79 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/mission.md +4 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/pages/dashboard/index.html +153 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/progress.json +7 -0
- package/data/project-types/li/life-binder/versions/1.0.1/about.md +7 -0
- package/data/project-types/li/life-binder/versions/1.0.1/documents.json +145 -0
- package/data/project-types/li/life-binder/versions/1.0.1/manifest.json +137 -0
- package/data/project-types/li/life-binder/versions/1.0.1/mission.md +7 -0
- package/data/project-types/li/life-binder/versions/1.0.1/pages/dashboard/index.html +147 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/about.md +6 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/manifest.json +159 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/menu.json +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/mission.md +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/pages/dashboard/index.html +152 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/pantry.json +9 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/about.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/chapters.json +10 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/manifest.json +157 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/mission.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/pages/dashboard/index.html +165 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/sessions.json +6 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/about.md +7 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/journal.json +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/manifest.json +193 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/mission.md +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/packing.json +8 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/pages/dashboard/index.html +215 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/trip.json +7 -0
- package/package.json +4 -2
|
@@ -0,0 +1,396 @@
|
|
|
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
|
+
@media (prefers-color-scheme: dark) {
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #1c1a17;
|
|
23
|
+
--card: #262320;
|
|
24
|
+
--ink: #efe9e0;
|
|
25
|
+
--muted: #a59a8c;
|
|
26
|
+
--accent: #e0894a;
|
|
27
|
+
--track: #3a352f;
|
|
28
|
+
--good: #7fae8d;
|
|
29
|
+
--warn: #d98a63;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
* { box-sizing: border-box; }
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
36
|
+
background: var(--bg);
|
|
37
|
+
color: var(--ink);
|
|
38
|
+
padding: 1.5rem 1.25rem;
|
|
39
|
+
}
|
|
40
|
+
main { max-width: 560px; margin: 0 auto; }
|
|
41
|
+
h1 {
|
|
42
|
+
font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
|
|
43
|
+
font-size: 1.35rem;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
margin: 0 0 0.15rem;
|
|
46
|
+
}
|
|
47
|
+
.sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
|
|
48
|
+
.dots { display: flex; gap: 0.3rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
|
|
49
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--track); }
|
|
50
|
+
.dot.done-good { background: var(--good); }
|
|
51
|
+
.dot.done-miss { background: var(--warn); }
|
|
52
|
+
.dot.current { background: var(--accent); }
|
|
53
|
+
.flashcard {
|
|
54
|
+
background: var(--card);
|
|
55
|
+
border: 1px solid var(--track);
|
|
56
|
+
border-radius: 10px;
|
|
57
|
+
min-height: 200px;
|
|
58
|
+
display: grid;
|
|
59
|
+
place-items: center;
|
|
60
|
+
text-align: center;
|
|
61
|
+
padding: 1.5rem;
|
|
62
|
+
font-size: 1.15rem;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
user-select: none;
|
|
65
|
+
}
|
|
66
|
+
.flashcard .face-label {
|
|
67
|
+
display: block;
|
|
68
|
+
font-size: 0.7rem;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
letter-spacing: 0.05em;
|
|
71
|
+
color: var(--muted);
|
|
72
|
+
margin-bottom: 0.6rem;
|
|
73
|
+
}
|
|
74
|
+
.actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; justify-content: center; }
|
|
75
|
+
.actions button {
|
|
76
|
+
border: 1px solid var(--track);
|
|
77
|
+
background: var(--card);
|
|
78
|
+
color: var(--ink);
|
|
79
|
+
border-radius: 6px;
|
|
80
|
+
padding: 0.45rem 1.1rem;
|
|
81
|
+
font: inherit;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.actions button.got { border-color: var(--good); color: var(--good); }
|
|
85
|
+
.actions button.miss { border-color: var(--warn); color: var(--warn); }
|
|
86
|
+
.actions button:disabled { opacity: 0.5; cursor: default; }
|
|
87
|
+
.empty, .done {
|
|
88
|
+
background: var(--card);
|
|
89
|
+
border: 1px dashed var(--track);
|
|
90
|
+
border-radius: 10px;
|
|
91
|
+
padding: 1.5rem;
|
|
92
|
+
text-align: center;
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
}
|
|
95
|
+
.banner {
|
|
96
|
+
border: 1px solid var(--accent);
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
padding: 0.45rem 0.7rem;
|
|
99
|
+
font-size: 0.85rem;
|
|
100
|
+
margin-bottom: 0.75rem;
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
.banner.show { display: block; }
|
|
104
|
+
.hint { font-size: 0.78rem; color: var(--muted); text-align: center; margin-top: 0.5rem; }
|
|
105
|
+
+ .demo-banner {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
gap: 0.75rem;
|
|
110
|
+
margin: 0 0 0.85rem;
|
|
111
|
+
padding: 0.5rem 0.65rem;
|
|
112
|
+
border: 1px solid var(--accent, #8a5a2b);
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
background: color-mix(in srgb, var(--card, var(--surface, #fffdf8)) 88%, var(--accent, #8a5a2b));
|
|
115
|
+
color: var(--ink, #2b2620);
|
|
116
|
+
font-size: 0.8rem;
|
|
117
|
+
}
|
|
118
|
+
.demo-banner[hidden] { display: none; }
|
|
119
|
+
.demo-banner strong { font-weight: 700; }
|
|
120
|
+
.demo-reset {
|
|
121
|
+
flex: none;
|
|
122
|
+
padding: 0.25rem 0.5rem;
|
|
123
|
+
border: 1px solid var(--accent, #8a5a2b);
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
background: var(--card, var(--surface, #fffdf8));
|
|
126
|
+
color: var(--ink, #2b2620);
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
font: inherit;
|
|
129
|
+
}
|
|
130
|
+
.demo-reset:hover { border-color: var(--ink, #2b2620); }
|
|
131
|
+
</style>
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<main>
|
|
135
|
+
+ <div class="demo-banner" data-gezel-demo-banner hidden>
|
|
136
|
+
<span><strong>Demo mode</strong> — sample data stays in this page.</span>
|
|
137
|
+
<button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="banner" id="banner"></div>
|
|
140
|
+
<h1 id="title">Flashcards</h1>
|
|
141
|
+
<p class="sub" id="sub">Loading the deck…</p>
|
|
142
|
+
<div class="dots" id="dots"></div>
|
|
143
|
+
<div id="stage"></div>
|
|
144
|
+
<p class="hint" id="hint"></p>
|
|
145
|
+
</main>
|
|
146
|
+
<script>
|
|
147
|
+
(function () {
|
|
148
|
+
'use strict';
|
|
149
|
+
|
|
150
|
+
var gezelPage = (function () {
|
|
151
|
+
var seq = 0;
|
|
152
|
+
var pending = {};
|
|
153
|
+
window.addEventListener('message', function (event) {
|
|
154
|
+
var d = event.data;
|
|
155
|
+
if (!d || d.__gezelPageResult !== true || typeof d.id !== 'string') return;
|
|
156
|
+
var entry = pending[d.id];
|
|
157
|
+
if (!entry) return;
|
|
158
|
+
delete pending[d.id];
|
|
159
|
+
clearTimeout(entry.timer);
|
|
160
|
+
if (d.ok) entry.resolve(d);
|
|
161
|
+
else entry.reject(new Error(d.error || 'invoke failed'));
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
embedded: function () {
|
|
165
|
+
return demoMode || Boolean(capability && projectId);
|
|
166
|
+
},
|
|
167
|
+
invoke: function (tool, input) {
|
|
168
|
+
return new Promise(function (resolve, reject) {
|
|
169
|
+
if (demoMode) {
|
|
170
|
+
resolve({ ok: true, tool: tool, input: input });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (window.parent === window) {
|
|
174
|
+
reject(new Error('not embedded in Gezel'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
seq += 1;
|
|
178
|
+
var id = 'pi-' + Date.now() + '-' + seq;
|
|
179
|
+
pending[id] = {
|
|
180
|
+
resolve: resolve,
|
|
181
|
+
reject: reject,
|
|
182
|
+
timer: setTimeout(function () {
|
|
183
|
+
delete pending[id];
|
|
184
|
+
reject(new Error('invoke timed out'));
|
|
185
|
+
}, 45000),
|
|
186
|
+
};
|
|
187
|
+
window.parent.postMessage(
|
|
188
|
+
{ __gezelPageInvoke: true, id: id, tool: tool, input: input || {} },
|
|
189
|
+
'*',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
refresh: function () {
|
|
194
|
+
if (!demoMode && window.parent !== window) window.parent.postMessage({ __gezelPageRefresh: true }, '*');
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
var parts = location.pathname.split('/').filter(Boolean);
|
|
200
|
+
var typeIdx = parts.indexOf('type');
|
|
201
|
+
var capability = typeIdx > 0 ? parts[typeIdx - 1] : null;
|
|
202
|
+
var projectId = typeIdx >= 0 ? parts[typeIdx + 1] : null;
|
|
203
|
+
var demoMode = !capability || !projectId;
|
|
204
|
+
var demoDeck = {
|
|
205
|
+
subject: 'Dutch for a market morning',
|
|
206
|
+
cards: [
|
|
207
|
+
{ id: 'card-1', front: 'Hoeveel kost dit?', back: 'How much does this cost?' },
|
|
208
|
+
{ id: 'card-2', front: 'een bos bloemen', back: 'a bunch of flowers' },
|
|
209
|
+
{ id: 'card-3', front: 'Mag ik pinnen?', back: 'Can I pay by card?' },
|
|
210
|
+
{ id: 'card-4', front: 'Dat is alles, dank u.', back: 'That is all, thank you.' },
|
|
211
|
+
],
|
|
212
|
+
dueCardIds: ['card-1', 'card-2', 'card-3', 'card-4'],
|
|
213
|
+
};
|
|
214
|
+
if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
|
|
215
|
+
|
|
216
|
+
var deck = null;
|
|
217
|
+
// The session queue is snapshotted from dueCardIds at start so a
|
|
218
|
+
// background poll never reshuffles cards mid-session.
|
|
219
|
+
var queue = null;
|
|
220
|
+
var idx = 0;
|
|
221
|
+
var flipped = false;
|
|
222
|
+
var results = [];
|
|
223
|
+
var busy = false;
|
|
224
|
+
var failedPolls = 0;
|
|
225
|
+
|
|
226
|
+
function esc(s) {
|
|
227
|
+
return String(s).replace(/[&<>"]/g, function (c) {
|
|
228
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function setBanner(text) {
|
|
232
|
+
var el = document.getElementById('banner');
|
|
233
|
+
el.textContent = text || '';
|
|
234
|
+
el.className = text ? 'banner show' : 'banner';
|
|
235
|
+
}
|
|
236
|
+
function cardById(id) {
|
|
237
|
+
return (deck.cards || []).find(function (c) { return c.id === id; }) || null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function render() {
|
|
241
|
+
var stage = document.getElementById('stage');
|
|
242
|
+
var dots = document.getElementById('dots');
|
|
243
|
+
var hint = document.getElementById('hint');
|
|
244
|
+
if (!deck) return;
|
|
245
|
+
if (deck.subject) document.getElementById('title').textContent = 'Flashcards: ' + deck.subject;
|
|
246
|
+
|
|
247
|
+
if (!deck.cards || deck.cards.length === 0) {
|
|
248
|
+
document.getElementById('sub').textContent = 'The deck is empty.';
|
|
249
|
+
dots.innerHTML = '';
|
|
250
|
+
hint.textContent = '';
|
|
251
|
+
stage.innerHTML =
|
|
252
|
+
'<div class="empty">No cards yet — ask your Studiemaat to write the first ones.</div>';
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (queue === null || (idx >= queue.length && queue.length === 0)) {
|
|
257
|
+
document.getElementById('sub').textContent =
|
|
258
|
+
deck.dueCardIds.length > 0
|
|
259
|
+
? deck.dueCardIds.length + ' card(s) due.'
|
|
260
|
+
: 'Nothing due right now.';
|
|
261
|
+
dots.innerHTML = '';
|
|
262
|
+
hint.textContent = '';
|
|
263
|
+
stage.innerHTML =
|
|
264
|
+
deck.dueCardIds.length > 0
|
|
265
|
+
? '<div class="actions"><button type="button" id="start">Start review (' + deck.dueCardIds.length + ')</button></div>'
|
|
266
|
+
: '<div class="done">All caught up — nothing due. Your Studiemaat can always add more.</div>';
|
|
267
|
+
var startBtn = document.getElementById('start');
|
|
268
|
+
if (startBtn) startBtn.addEventListener('click', startSession);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (idx >= queue.length) {
|
|
273
|
+
var missed = results.filter(function (r) { return r.verdict === 'missed'; }).length;
|
|
274
|
+
document.getElementById('sub').textContent =
|
|
275
|
+
'Session done: ' + (results.length - missed) + ' got it, ' + missed + ' missed.';
|
|
276
|
+
hint.textContent = '';
|
|
277
|
+
stage.innerHTML = '<div class="done">Nice work — your Studiemaat has the recap.</div>';
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
var card = cardById(queue[idx]);
|
|
282
|
+
if (!card) {
|
|
283
|
+
idx += 1;
|
|
284
|
+
render();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
document.getElementById('sub').textContent = 'Card ' + (idx + 1) + ' of ' + queue.length;
|
|
288
|
+
dots.innerHTML = queue
|
|
289
|
+
.map(function (id, i) {
|
|
290
|
+
if (i === idx) return '<span class="dot current"></span>';
|
|
291
|
+
var r = results[i];
|
|
292
|
+
if (!r) return '<span class="dot"></span>';
|
|
293
|
+
return '<span class="dot ' + (r.verdict === 'missed' ? 'done-miss' : 'done-good') + '"></span>';
|
|
294
|
+
})
|
|
295
|
+
.join('');
|
|
296
|
+
stage.innerHTML =
|
|
297
|
+
'<div class="flashcard" id="card"><div><span class="face-label">' +
|
|
298
|
+
(flipped ? 'Back' : 'Front') + '</span>' + esc(flipped ? card.back : card.front) +
|
|
299
|
+
'</div></div>' +
|
|
300
|
+
(flipped
|
|
301
|
+
? '<div class="actions">' +
|
|
302
|
+
'<button type="button" class="got" id="got"' + (busy ? ' disabled' : '') + '>Got it</button>' +
|
|
303
|
+
'<button type="button" class="miss" id="miss"' + (busy ? ' disabled' : '') + '>Missed</button>' +
|
|
304
|
+
'</div>'
|
|
305
|
+
: '');
|
|
306
|
+
hint.textContent = flipped ? '' : 'Tap the card to reveal the answer.';
|
|
307
|
+
document.getElementById('card').addEventListener('click', function () {
|
|
308
|
+
if (!flipped) {
|
|
309
|
+
flipped = true;
|
|
310
|
+
render();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
if (flipped) {
|
|
314
|
+
document.getElementById('got').addEventListener('click', function () { grade('got-it'); });
|
|
315
|
+
document.getElementById('miss').addEventListener('click', function () { grade('missed'); });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function startSession() {
|
|
320
|
+
queue = deck.dueCardIds.slice();
|
|
321
|
+
idx = 0;
|
|
322
|
+
flipped = false;
|
|
323
|
+
results = [];
|
|
324
|
+
render();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function grade(verdict) {
|
|
328
|
+
if (busy) return;
|
|
329
|
+
busy = true;
|
|
330
|
+
render();
|
|
331
|
+
var cardId = queue[idx];
|
|
332
|
+
gezelPage
|
|
333
|
+
.invoke('record_review', { cardId: cardId, verdict: verdict })
|
|
334
|
+
.then(function () {
|
|
335
|
+
busy = false;
|
|
336
|
+
results[idx] = { cardId: cardId, verdict: verdict };
|
|
337
|
+
idx += 1;
|
|
338
|
+
flipped = false;
|
|
339
|
+
if (idx >= queue.length) return finishSession();
|
|
340
|
+
render();
|
|
341
|
+
})
|
|
342
|
+
.catch(function (err) {
|
|
343
|
+
busy = false;
|
|
344
|
+
setBanner(String(err && err.message ? err.message : err));
|
|
345
|
+
render();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function finishSession() {
|
|
350
|
+
var missedIds = results
|
|
351
|
+
.filter(function (r) { return r.verdict === 'missed'; })
|
|
352
|
+
.map(function (r) { return r.cardId; });
|
|
353
|
+
return gezelPage
|
|
354
|
+
.invoke('finish_session', { reviewed: results.length, missedIds: missedIds })
|
|
355
|
+
.catch(function () {})
|
|
356
|
+
.then(function () {
|
|
357
|
+
render();
|
|
358
|
+
return poll();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function poll() {
|
|
363
|
+
if (demoMode) {
|
|
364
|
+
deck = demoDeck;
|
|
365
|
+
if (queue === null || idx >= queue.length) render();
|
|
366
|
+
return Promise.resolve();
|
|
367
|
+
}
|
|
368
|
+
if (!capability || !projectId) return Promise.resolve();
|
|
369
|
+
return fetch(
|
|
370
|
+
'/preview/' + encodeURIComponent(capability) + '/workspace/' + encodeURIComponent(projectId) + '/deck.json',
|
|
371
|
+
{ cache: 'no-store' },
|
|
372
|
+
)
|
|
373
|
+
.then(function (r) {
|
|
374
|
+
if (!r.ok) throw new Error('poll ' + r.status);
|
|
375
|
+
return r.json();
|
|
376
|
+
})
|
|
377
|
+
.then(function (next) {
|
|
378
|
+
failedPolls = 0;
|
|
379
|
+
deck = next;
|
|
380
|
+
// Only re-render outside an active session — mid-session the
|
|
381
|
+
// snapshot queue drives the UI.
|
|
382
|
+
if (queue === null || idx >= queue.length) render();
|
|
383
|
+
})
|
|
384
|
+
.catch(function () {
|
|
385
|
+
failedPolls += 1;
|
|
386
|
+
if (failedPolls === 3) gezelPage.refresh();
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
poll().then(function () {
|
|
391
|
+
setInterval(function () { void poll(); }, 10000);
|
|
392
|
+
});
|
|
393
|
+
})();
|
|
394
|
+
</script>
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
The back office for {{business}}. Tell your Kantoormeester when work is billed,
|
|
2
|
+
sent, or paid, and the invoice book stays honest — with warm, firm payment
|
|
3
|
+
chases drafted the day something goes overdue.
|
|
4
|
+
|
|
5
|
+
The **Output** tab shows the money picture: what's outstanding, what's overdue,
|
|
6
|
+
what got paid this month. All of it lives in plain files on this machine.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"releasedAt": "2026-08-01T00:00:00Z",
|
|
5
|
+
"meesterManaged": false,
|
|
6
|
+
"params": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"business": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"title": "What do you do?",
|
|
12
|
+
"description": "One line about the business — e.g. 'freelance brand design'.",
|
|
13
|
+
"default": "freelance work"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"required": [
|
|
17
|
+
"business"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"nameTemplate": "Freelance Office",
|
|
21
|
+
"aboutTemplate": "about.md",
|
|
22
|
+
"missionTemplate": "mission.md",
|
|
23
|
+
"gezels": [
|
|
24
|
+
{
|
|
25
|
+
"templateId": "kantoormeester",
|
|
26
|
+
"voorman": true
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"tools": [
|
|
30
|
+
{
|
|
31
|
+
"name": "record_invoice",
|
|
32
|
+
"description": "Add an invoice to the book (amount in ordinary units).",
|
|
33
|
+
"script": "office-store",
|
|
34
|
+
"inputs": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"client": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"amount": {
|
|
41
|
+
"type": "number"
|
|
42
|
+
},
|
|
43
|
+
"due": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"note": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": [
|
|
51
|
+
"client",
|
|
52
|
+
"amount"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"bind": {
|
|
56
|
+
"action": "record_invoice"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "invoice_sent",
|
|
61
|
+
"description": "Mark an invoice sent to the client.",
|
|
62
|
+
"script": "office-store",
|
|
63
|
+
"inputs": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"id": {
|
|
67
|
+
"type": "string"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"required": [
|
|
71
|
+
"id"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"bind": {
|
|
75
|
+
"action": "invoice_sent"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "invoice_paid",
|
|
80
|
+
"description": "Mark an invoice paid.",
|
|
81
|
+
"script": "office-store",
|
|
82
|
+
"inputs": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {
|
|
85
|
+
"id": {
|
|
86
|
+
"type": "string"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"required": [
|
|
90
|
+
"id"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"bind": {
|
|
94
|
+
"action": "invoice_paid"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "office_status",
|
|
99
|
+
"description": "The money picture: outstanding, overdue, paid this month.",
|
|
100
|
+
"script": "office-store",
|
|
101
|
+
"inputs": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {}
|
|
104
|
+
},
|
|
105
|
+
"bind": {
|
|
106
|
+
"action": "status"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "list_invoices",
|
|
111
|
+
"description": "Read the whole invoice book.",
|
|
112
|
+
"script": "office-store",
|
|
113
|
+
"inputs": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": {}
|
|
116
|
+
},
|
|
117
|
+
"bind": {
|
|
118
|
+
"action": "list_invoices"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"craftbooks": [
|
|
123
|
+
"proposal-sow",
|
|
124
|
+
"weekly-review"
|
|
125
|
+
],
|
|
126
|
+
"schedules": [],
|
|
127
|
+
"pages": {
|
|
128
|
+
"entry": "dashboard/index.html",
|
|
129
|
+
"reads": [
|
|
130
|
+
{
|
|
131
|
+
"source": "workspace",
|
|
132
|
+
"path": "invoices.json"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"workspaceSeed": [
|
|
137
|
+
"invoices.json"
|
|
138
|
+
],
|
|
139
|
+
"scripts": {
|
|
140
|
+
"office-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { rosterStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'office-store',\n description:\n \"Read and update the invoice book (invoices.json). Call action:'status' for outstanding/overdue/paid-this-month; 'record_invoice' when work is billed (amount in ordinary units, e.g. 1200.00); 'invoice_sent' / 'invoice_paid' as reality moves; 'list_invoices' to read the book.\",\n inputs: {\n action: {\n type: 'string',\n description:\n \"One of 'status', 'record_invoice', 'invoice_sent', 'invoice_paid', or 'list_invoices'.\",\n required: true,\n },\n client: { type: 'string', description: 'Client name.' },\n amount: { type: 'number', description: 'Invoice amount in ordinary units, e.g. 1200.00.' },\n due: { type: 'string', description: 'Due date (ISO), e.g. 2026-08-15.' },\n note: { type: 'string', description: 'Short note (what the invoice covers).' },\n id: { type: 'string', description: \"Invoice id, e.g. 'inv-3'.\" },\n },\n outputs: {\n invoices: { type: 'number', description: 'Invoices in the book.' },\n outstandingCents: { type: 'number', description: 'Total sent-but-unpaid, in cents.' },\n overdue: { type: 'number', description: 'Count of overdue invoices.' },\n summary: { type: 'string', description: 'Human-readable office summary.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── office-rules-start ──\n// Pure derivations over rosterStore-shaped invoice records: statuses are\n// draft -> sent -> paid; fields carry {client, amountCents, dueAt, note}.\n\nfunction toCents(amount) {\n if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) return null;\n return Math.round(amount * 100);\n}\n\nfunction fmtCents(cents) {\n var sign = cents < 0 ? '-' : '';\n var abs = Math.abs(cents);\n return sign + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0');\n}\n\nfunction officeShape(records, nowMs) {\n var outstanding = 0;\n var overdue = [];\n var paidMonth = 0;\n var month = new Date(nowMs).toISOString().slice(0, 7);\n records.forEach(function (r) {\n var cents = typeof r.fields.amountCents === 'number' ? r.fields.amountCents : 0;\n if (r.status === 'sent') {\n outstanding += cents;\n var due = typeof r.fields.dueAt === 'string' ? Date.parse(r.fields.dueAt) : NaN;\n if (isFinite(due) && due < nowMs) overdue.push(r);\n }\n if (r.status === 'paid' && r.updatedAt.slice(0, 7) === month) paidMonth += cents;\n });\n return { outstanding: outstanding, overdue: overdue, paidMonth: paidMonth, month: month };\n}\n\nfunction officeLine(records, nowMs) {\n var shape = officeShape(records, nowMs);\n var chase = shape.overdue\n .map(function (r) {\n return r.fields.client + ' (' + r.id + ', ' + fmtCents(r.fields.amountCents || 0) + ')';\n })\n .join(', ');\n return (\n 'Outstanding ' +\n fmtCents(shape.outstanding) +\n ' across ' +\n records.filter(function (r) {\n return r.status === 'sent';\n }).length +\n ' sent invoice(s); ' +\n shape.overdue.length +\n ' overdue' +\n (chase ? ' (' + chase + ')' : '') +\n '. Paid this month: ' +\n fmtCents(shape.paidMonth) +\n '.'\n );\n}\n// ── office-rules-end ──\n\nconst invoices = rosterStore(gezel.fs, 'invoices.json', {\n stages: ['draft', 'sent', 'paid'],\n idPrefix: 'inv',\n});\n\nif (input.action === 'record_invoice') {\n const cents = toCents(input.amount);\n if (!input.client || cents === null) {\n throw new Error(\"record_invoice needs 'client' and a positive 'amount' (ordinary units).\");\n }\n await invoices.add({\n fields: {\n client: input.client,\n amountCents: cents,\n ...(input.due ? { dueAt: input.due } : {}),\n ...(input.note ? { note: input.note } : {}),\n },\n });\n} else if (input.action === 'invoice_sent' || input.action === 'invoice_paid') {\n if (!input.id) throw new Error(\"Pass the invoice 'id', e.g. 'inv-3'.\");\n await invoices.transition(input.id, input.action === 'invoice_sent' ? 'sent' : 'paid');\n} else if (input.action !== 'status' && input.action !== 'list_invoices') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, record_invoice, invoice_sent, invoice_paid, or list_invoices.`,\n );\n}\n\nconst records = await invoices.list();\nconst shape = officeShape(records, Date.now());\nconst listing =\n input.action === 'list_invoices'\n ? records\n .map(\n (r) =>\n `${r.id} [${r.status}] ${String(r.fields.client ?? '?')} ${fmtCents(Number(r.fields.amountCents ?? 0))}${r.fields.dueAt ? ` due ${String(r.fields.dueAt).slice(0, 10)}` : ''}`,\n )\n .join('\\n') || 'The invoice book is empty.'\n : '';\n\ngezel.output({\n invoices: records.length,\n outstandingCents: shape.outstanding,\n overdue: shape.overdue.length,\n summary: listing || officeLine(records, Date.now()),\n});\n"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- Every invoice is tracked from draft to paid; nothing falls through.
|
|
2
|
+
- Overdue invoices get a drafted chase the day they slip — friendly, firm,
|
|
3
|
+
escalating gently.
|
|
4
|
+
- Proposals and quiet clients get follow-ups after a week, not a month.
|
|
5
|
+
- The income picture (outstanding, overdue, paid this month) is always one
|
|
6
|
+
question away.
|