@bendyline/gilde 0.1.86 → 0.1.88
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/data/craftbook-templates/index.json +1 -1
- package/data/craftbook-templates/raw-index.json +1 -1
- package/data/craftbook-templates/ta/tailor-resume/versions/1.0.5/craftbook.json +226 -0
- package/data/craftbook-templates/ta/tailor-resume/versions/1.0.5/test.json +79 -0
- package/data/knowledge-catalogs/ca/caselaw-alaska-part-0001/manifest.json +24 -0
- package/data/knowledge-catalogs/ca/caselaw-alaska-part-0001/versions/2026.10.1/manifest.json +658 -0
- package/data/knowledge-catalogs/ca/caselaw-nevada-part-0001/manifest.json +24 -0
- package/data/knowledge-catalogs/ca/caselaw-nevada-part-0001/versions/2026.10.1/manifest.json +666 -0
- package/data/knowledge-catalogs/ca/caselaw-wyoming-part-0001/manifest.json +24 -0
- package/data/knowledge-catalogs/ca/caselaw-wyoming-part-0001/versions/2026.10.3/manifest.json +626 -0
- package/data/knowledge-catalogs/index.json +1 -1
- package/data/knowledge-catalogs/raw-index.json +1 -1
- package/data/project-types/ch/checkers/versions/1.2.1/about.md +9 -0
- package/data/project-types/ch/checkers/versions/1.2.1/game.json +77 -0
- package/data/project-types/ch/checkers/versions/1.2.1/manifest.json +168 -0
- package/data/project-types/ch/checkers/versions/1.2.1/mission.md +9 -0
- package/data/project-types/ch/checkers/versions/1.2.1/pages/board/index.html +1158 -0
- package/data/project-types/ch/chess/versions/1.0.4/about.md +9 -0
- package/data/project-types/ch/chess/versions/1.0.4/game.json +127 -0
- package/data/project-types/ch/chess/versions/1.0.4/manifest.json +182 -0
- package/data/project-types/ch/chess/versions/1.0.4/mission.md +8 -0
- package/data/project-types/ch/chess/versions/1.0.4/pages/board/index.html +851 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/about.md +6 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/manifest.json +70 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/mission.md +3 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/pages/gallery/index.html +212 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/schemes/index.json +9 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/schemes/warm-dusk.json +4 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/about.md +7 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/budget.json +5 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/guests.json +10 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/manifest.json +171 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/mission.md +6 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/pages/dashboard/index.html +283 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/about.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/manifest.json +126 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/mission.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/pages/dashboard/index.html +273 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/training.json +6 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/about.md +8 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/deck.json +7 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/manifest.json +168 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/mission.md +6 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/pages/review/index.html +477 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/about.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/invoices.json +9 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/manifest.json +143 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/mission.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/pages/dashboard/index.html +258 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/about.md +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/donations.json +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/manifest.json +127 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/mission.md +6 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/pages/dashboard/index.html +261 -0
- package/data/project-types/go/go/versions/1.0.2/about.md +9 -0
- package/data/project-types/go/go/versions/1.0.2/game.json +103 -0
- package/data/project-types/go/go/versions/1.0.2/manifest.json +168 -0
- package/data/project-types/go/go/versions/1.0.2/mission.md +8 -0
- package/data/project-types/go/go/versions/1.0.2/pages/board/index.html +855 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/about.md +6 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/ledger.json +5 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/manifest.json +164 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/mission.md +5 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/pages/dashboard/index.html +340 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/about.md +7 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/manifest.json +153 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/mission.md +8 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/pages/dashboard/index.html +258 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/systems.json +8 -0
- package/data/project-types/index.json +1 -1
- package/data/project-types/jo/job-hunt/versions/1.0.2/about.md +10 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/activity.json +4 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/manifest.json +213 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/mission.md +9 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/pages/dashboard/index.html +430 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/pipeline.json +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/about.md +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/manifest.json +80 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/mission.md +4 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/pages/dashboard/index.html +268 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/progress.json +7 -0
- package/data/project-types/li/life-binder/versions/1.0.3/about.md +7 -0
- package/data/project-types/li/life-binder/versions/1.0.3/documents.json +145 -0
- package/data/project-types/li/life-binder/versions/1.0.3/manifest.json +138 -0
- package/data/project-types/li/life-binder/versions/1.0.3/mission.md +6 -0
- package/data/project-types/li/life-binder/versions/1.0.3/pages/dashboard/index.html +248 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/about.md +6 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/manifest.json +160 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/menu.json +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/mission.md +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/pages/dashboard/index.html +248 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/pantry.json +9 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/about.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/chapters.json +10 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/manifest.json +158 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/mission.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/pages/dashboard/index.html +264 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/sessions.json +6 -0
- package/data/project-types/raw-index.json +1 -1
- package/data/project-types/tr/trip-planner/versions/1.0.2/about.md +7 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/journal.json +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/manifest.json +194 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/mission.md +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/packing.json +8 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/pages/dashboard/index.html +308 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/trip.json +7 -0
- package/package.json +1 -1
- package/schemas/project-type-version.schema.json +13 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="gezel-demo" content="standalone" />
|
|
7
|
+
<title>Flashcards</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--bg: #faf7f2;
|
|
12
|
+
--card: #fff;
|
|
13
|
+
--ink: #2b2620;
|
|
14
|
+
--muted: #7a7166;
|
|
15
|
+
--accent: #b5651d;
|
|
16
|
+
--track: #ece5da;
|
|
17
|
+
--good: #4a7c59;
|
|
18
|
+
--warn: #a4552e;
|
|
19
|
+
}
|
|
20
|
+
:root[data-theme='dark'] {
|
|
21
|
+
--bg: #1c1a17;
|
|
22
|
+
--card: #262320;
|
|
23
|
+
--ink: #efe9e0;
|
|
24
|
+
--muted: #a59a8c;
|
|
25
|
+
--accent: #e0894a;
|
|
26
|
+
--track: #3a352f;
|
|
27
|
+
--good: #7fae8d;
|
|
28
|
+
--warn: #d98a63;
|
|
29
|
+
}
|
|
30
|
+
@media (prefers-color-scheme: dark) {
|
|
31
|
+
:root:not([data-theme='light']) {
|
|
32
|
+
--bg: #1c1a17;
|
|
33
|
+
--card: #262320;
|
|
34
|
+
--ink: #efe9e0;
|
|
35
|
+
--muted: #a59a8c;
|
|
36
|
+
--accent: #e0894a;
|
|
37
|
+
--track: #3a352f;
|
|
38
|
+
--good: #7fae8d;
|
|
39
|
+
--warn: #d98a63;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
* { box-sizing: border-box; }
|
|
43
|
+
body {
|
|
44
|
+
margin: 0;
|
|
45
|
+
font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
46
|
+
background: var(--bg);
|
|
47
|
+
color: var(--ink);
|
|
48
|
+
padding: 1.5rem 1.25rem;
|
|
49
|
+
}
|
|
50
|
+
main { max-width: 560px; margin: 0 auto; }
|
|
51
|
+
h1 {
|
|
52
|
+
font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
|
|
53
|
+
font-size: 1.35rem;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
margin: 0 0 0.15rem;
|
|
56
|
+
}
|
|
57
|
+
.sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
|
|
58
|
+
.dots { display: flex; gap: 0.3rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
|
|
59
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--track); }
|
|
60
|
+
.dot.done-good { background: var(--good); }
|
|
61
|
+
.dot.done-miss { background: var(--warn); }
|
|
62
|
+
.dot.current { background: var(--accent); }
|
|
63
|
+
.flashcard {
|
|
64
|
+
background: var(--card);
|
|
65
|
+
border: 1px solid var(--track);
|
|
66
|
+
border-radius: 10px;
|
|
67
|
+
min-height: 200px;
|
|
68
|
+
display: grid;
|
|
69
|
+
place-items: center;
|
|
70
|
+
text-align: center;
|
|
71
|
+
padding: 1.5rem;
|
|
72
|
+
font-size: 1.15rem;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
user-select: none;
|
|
75
|
+
}
|
|
76
|
+
.flashcard .face-label {
|
|
77
|
+
display: block;
|
|
78
|
+
font-size: 0.7rem;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
letter-spacing: 0.05em;
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
margin-bottom: 0.6rem;
|
|
83
|
+
}
|
|
84
|
+
.actions { display: flex; gap: 0.6rem; margin-top: 0.85rem; justify-content: center; }
|
|
85
|
+
.actions button {
|
|
86
|
+
border: 1px solid var(--track);
|
|
87
|
+
background: var(--card);
|
|
88
|
+
color: var(--ink);
|
|
89
|
+
border-radius: 6px;
|
|
90
|
+
padding: 0.45rem 1.1rem;
|
|
91
|
+
min-height: 44px;
|
|
92
|
+
font: inherit;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
.actions button.got { border-color: var(--good); color: var(--good); }
|
|
96
|
+
.actions button.miss { border-color: var(--warn); color: var(--warn); }
|
|
97
|
+
.actions button:disabled { opacity: 0.5; cursor: default; }
|
|
98
|
+
.empty, .done {
|
|
99
|
+
background: var(--card);
|
|
100
|
+
border: 1px dashed var(--track);
|
|
101
|
+
border-radius: 10px;
|
|
102
|
+
padding: 1.5rem;
|
|
103
|
+
text-align: center;
|
|
104
|
+
color: var(--muted);
|
|
105
|
+
}
|
|
106
|
+
.banner {
|
|
107
|
+
border: 1px solid var(--accent);
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
padding: 0.45rem 0.7rem;
|
|
110
|
+
font-size: 0.85rem;
|
|
111
|
+
margin-bottom: 0.75rem;
|
|
112
|
+
display: none;
|
|
113
|
+
}
|
|
114
|
+
.banner.show { display: block; }
|
|
115
|
+
.hint { font-size: 0.78rem; color: var(--muted); text-align: center; margin-top: 0.5rem; }
|
|
116
|
+
.note {
|
|
117
|
+
margin: 0 0 0.85rem;
|
|
118
|
+
padding: 0.5rem 0.65rem;
|
|
119
|
+
border: 1px solid var(--track);
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
background: var(--card);
|
|
122
|
+
color: var(--muted);
|
|
123
|
+
font-size: 0.8rem;
|
|
124
|
+
}
|
|
125
|
+
.demo-banner {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
gap: 0.75rem;
|
|
130
|
+
margin: 0 0 0.85rem;
|
|
131
|
+
padding: 0.5rem 0.65rem;
|
|
132
|
+
border: 1px solid var(--accent);
|
|
133
|
+
border-radius: 6px;
|
|
134
|
+
background: color-mix(in srgb, var(--card) 88%, var(--accent));
|
|
135
|
+
color: var(--ink);
|
|
136
|
+
font-size: 0.8rem;
|
|
137
|
+
}
|
|
138
|
+
.demo-banner[hidden] { display: none; }
|
|
139
|
+
.demo-banner strong { font-weight: 700; }
|
|
140
|
+
.demo-reset {
|
|
141
|
+
flex: none;
|
|
142
|
+
padding: 0.25rem 0.5rem;
|
|
143
|
+
border: 1px solid var(--accent);
|
|
144
|
+
border-radius: 6px;
|
|
145
|
+
background: var(--card);
|
|
146
|
+
color: var(--ink);
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
font: inherit;
|
|
149
|
+
}
|
|
150
|
+
.demo-reset:hover { border-color: var(--ink); }
|
|
151
|
+
</style>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
<main>
|
|
155
|
+
<div class="demo-banner" data-gezel-demo-banner hidden>
|
|
156
|
+
<span><strong>Demo mode</strong> — sample data stays in this page.</span>
|
|
157
|
+
<button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
|
|
158
|
+
</div>
|
|
159
|
+
<p class="note" id="browser-note" hidden>
|
|
160
|
+
Viewing from a browser tab — the deck stays live, but reviews happen in the app's Output pane.
|
|
161
|
+
</p>
|
|
162
|
+
<div class="banner" id="banner"></div>
|
|
163
|
+
<h1 id="title">Flashcards</h1>
|
|
164
|
+
<p class="sub" id="sub">Loading the deck…</p>
|
|
165
|
+
<div class="dots" id="dots"></div>
|
|
166
|
+
<div id="stage"></div>
|
|
167
|
+
<p class="hint" id="hint"></p>
|
|
168
|
+
</main>
|
|
169
|
+
<script>
|
|
170
|
+
/* gezel-page-demo-stub v1 */
|
|
171
|
+
/*
|
|
172
|
+
* The canonical demo stub for Gezel type pages. MIT License.
|
|
173
|
+
*
|
|
174
|
+
* Paste this file's bytes verbatim into a <script> block ahead of the
|
|
175
|
+
* page's own code, then call makeDemoGezel({ page, tools, data }) with the
|
|
176
|
+
* page's demo handlers. Served by Gezel, the real window.gezel is already
|
|
177
|
+
* injected and the stub steps aside; opened as a raw file (the live demos
|
|
178
|
+
* on gezelgilde.com), it installs a window.gezel whose tools and data are
|
|
179
|
+
* the page-supplied handler maps — one code path in every mode. `tools`
|
|
180
|
+
* and `data` map a tool name / declared read path to a function; data
|
|
181
|
+
* handlers back read() and list(), and url() returns the handler's value
|
|
182
|
+
* or passes a data: URI through. See docs/page-authoring.md.
|
|
183
|
+
*/
|
|
184
|
+
(function () {
|
|
185
|
+
'use strict';
|
|
186
|
+
window.makeDemoGezel = function (opts) {
|
|
187
|
+
if (window.gezel) return window.gezel;
|
|
188
|
+
var page = (opts && opts.page) || {};
|
|
189
|
+
var tools = (opts && opts.tools) || {};
|
|
190
|
+
var data = (opts && opts.data) || {};
|
|
191
|
+
var themeListeners = [];
|
|
192
|
+
var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
193
|
+
var ui = {
|
|
194
|
+
theme: { mode: media && media.matches ? 'dark' : 'light' },
|
|
195
|
+
onTheme: function (cb) {
|
|
196
|
+
themeListeners.push(cb);
|
|
197
|
+
return function () {
|
|
198
|
+
var at = themeListeners.indexOf(cb);
|
|
199
|
+
if (at >= 0) themeListeners.splice(at, 1);
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
if (media && media.addEventListener) {
|
|
204
|
+
media.addEventListener('change', function (ev) {
|
|
205
|
+
ui.theme = { mode: ev.matches ? 'dark' : 'light' };
|
|
206
|
+
themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function fail(code, message) {
|
|
210
|
+
var err = new Error(message);
|
|
211
|
+
err.code = code;
|
|
212
|
+
return Promise.reject(err);
|
|
213
|
+
}
|
|
214
|
+
function run(map, key, arg) {
|
|
215
|
+
if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
|
|
216
|
+
return Promise.resolve().then(function () { return map[key](arg); });
|
|
217
|
+
}
|
|
218
|
+
window.gezel = {
|
|
219
|
+
page: {
|
|
220
|
+
api: 1,
|
|
221
|
+
projectId: 'demo',
|
|
222
|
+
source: 'type',
|
|
223
|
+
entry: page.entry || 'index.html',
|
|
224
|
+
typeName: page.typeName || 'demo',
|
|
225
|
+
params: page.params || {},
|
|
226
|
+
mode: 'demo',
|
|
227
|
+
},
|
|
228
|
+
tools: {
|
|
229
|
+
list: function () { return Promise.resolve(Object.keys(tools)); },
|
|
230
|
+
invoke: function (name, input) {
|
|
231
|
+
var call = run(tools, name, input || {});
|
|
232
|
+
if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
|
|
233
|
+
return call.then(function (output) { return { output: output }; });
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
data: {
|
|
237
|
+
read: function (path, o) {
|
|
238
|
+
return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
|
|
239
|
+
},
|
|
240
|
+
list: function (path, o) {
|
|
241
|
+
return run(data, path, o || {}) || Promise.resolve([]);
|
|
242
|
+
},
|
|
243
|
+
watch: function () { return function () {}; },
|
|
244
|
+
url: function (path) {
|
|
245
|
+
if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
|
|
246
|
+
return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
ui: ui,
|
|
250
|
+
refresh: function () {},
|
|
251
|
+
};
|
|
252
|
+
return window.gezel;
|
|
253
|
+
};
|
|
254
|
+
})();
|
|
255
|
+
</script>
|
|
256
|
+
<script>
|
|
257
|
+
(function () {
|
|
258
|
+
'use strict';
|
|
259
|
+
|
|
260
|
+
// ── demo deck (used only when the page opens as a raw file) ──
|
|
261
|
+
var demoDeck = {
|
|
262
|
+
subject: 'Dutch for a market morning',
|
|
263
|
+
cards: [
|
|
264
|
+
{ id: 'card-1', front: 'Hoeveel kost dit?', back: 'How much does this cost?' },
|
|
265
|
+
{ id: 'card-2', front: 'een bos bloemen', back: 'a bunch of flowers' },
|
|
266
|
+
{ id: 'card-3', front: 'Mag ik pinnen?', back: 'Can I pay by card?' },
|
|
267
|
+
{ id: 'card-4', front: 'Dat is alles, dank u.', back: 'That is all, thank you.' },
|
|
268
|
+
],
|
|
269
|
+
dueCardIds: ['card-1', 'card-2', 'card-3', 'card-4'],
|
|
270
|
+
};
|
|
271
|
+
function demoCard(id) {
|
|
272
|
+
return demoDeck.cards.find(function (c) { return c.id === id; }) || null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
makeDemoGezel({
|
|
276
|
+
page: { entry: 'review/index.html', typeName: 'Flashcards' },
|
|
277
|
+
tools: {
|
|
278
|
+
record_review: function (input) {
|
|
279
|
+
var card = demoCard(input && input.cardId);
|
|
280
|
+
if (!card) throw new Error("No card '" + (input && input.cardId) + "' in the demo deck.");
|
|
281
|
+
// The demo deck stays due so the session can be replayed;
|
|
282
|
+
// Reset demo restores everything else.
|
|
283
|
+
return { summary: card.id + ' marked ' + input.verdict + '.' };
|
|
284
|
+
},
|
|
285
|
+
finish_session: function (input) {
|
|
286
|
+
var reviewed = (input && input.reviewed) || 0;
|
|
287
|
+
var missed = ((input && input.missedIds) || []).length;
|
|
288
|
+
return { summary: 'Reviewed ' + reviewed + ' card(s): ' + (reviewed - missed) + ' got it, ' + missed + ' missed.' };
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
data: {
|
|
292
|
+
'deck.json': function () { return demoDeck; },
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ── one code path from here: window.gezel exists in every mode ──
|
|
297
|
+
var demoMode = gezel.page.mode === 'demo';
|
|
298
|
+
var browserMode = gezel.page.mode === 'browser';
|
|
299
|
+
|
|
300
|
+
function applyTheme(mode) {
|
|
301
|
+
document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
|
|
302
|
+
}
|
|
303
|
+
applyTheme(gezel.ui.theme.mode);
|
|
304
|
+
gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
|
|
305
|
+
|
|
306
|
+
if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
|
|
307
|
+
if (browserMode) document.getElementById('browser-note').hidden = false;
|
|
308
|
+
|
|
309
|
+
var deck = null;
|
|
310
|
+
// The session queue is snapshotted from dueCardIds at start so a
|
|
311
|
+
// background change never reshuffles cards mid-session.
|
|
312
|
+
var queue = null;
|
|
313
|
+
var idx = 0;
|
|
314
|
+
var flipped = false;
|
|
315
|
+
var results = [];
|
|
316
|
+
var busy = false;
|
|
317
|
+
|
|
318
|
+
function esc(s) {
|
|
319
|
+
return String(s).replace(/[&<>"]/g, function (c) {
|
|
320
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function setBanner(text) {
|
|
324
|
+
var el = document.getElementById('banner');
|
|
325
|
+
el.textContent = text || '';
|
|
326
|
+
el.className = text ? 'banner show' : 'banner';
|
|
327
|
+
}
|
|
328
|
+
function invokeErrorText(err) {
|
|
329
|
+
if (err && err.code === 'unavailable') {
|
|
330
|
+
return "Reviews happen in the app's Output pane — this browser tab stays view-only.";
|
|
331
|
+
}
|
|
332
|
+
return String(err && err.message ? err.message : err);
|
|
333
|
+
}
|
|
334
|
+
function cardById(id) {
|
|
335
|
+
return (deck.cards || []).find(function (c) { return c.id === id; }) || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function render() {
|
|
339
|
+
var stage = document.getElementById('stage');
|
|
340
|
+
var dots = document.getElementById('dots');
|
|
341
|
+
var hint = document.getElementById('hint');
|
|
342
|
+
if (!deck) return;
|
|
343
|
+
if (deck.subject) document.getElementById('title').textContent = 'Flashcards: ' + deck.subject;
|
|
344
|
+
|
|
345
|
+
if (!deck.cards || deck.cards.length === 0) {
|
|
346
|
+
document.getElementById('sub').textContent = 'The deck is empty.';
|
|
347
|
+
dots.innerHTML = '';
|
|
348
|
+
hint.textContent = '';
|
|
349
|
+
stage.innerHTML =
|
|
350
|
+
'<div class="empty">No cards yet — ask your Studiemaat to write the first ones.</div>';
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (queue === null || (idx >= queue.length && queue.length === 0)) {
|
|
355
|
+
document.getElementById('sub').textContent =
|
|
356
|
+
deck.dueCardIds.length > 0
|
|
357
|
+
? deck.dueCardIds.length + ' card(s) due.'
|
|
358
|
+
: 'Nothing due right now.';
|
|
359
|
+
dots.innerHTML = '';
|
|
360
|
+
hint.textContent = '';
|
|
361
|
+
stage.innerHTML =
|
|
362
|
+
deck.dueCardIds.length > 0
|
|
363
|
+
? '<div class="actions"><button type="button" id="start">Start review (' + deck.dueCardIds.length + ')</button></div>'
|
|
364
|
+
: '<div class="done">All caught up — nothing due. Your Studiemaat can always add more.</div>';
|
|
365
|
+
var startBtn = document.getElementById('start');
|
|
366
|
+
if (startBtn) startBtn.addEventListener('click', startSession);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (idx >= queue.length) {
|
|
371
|
+
var missed = results.filter(function (r) { return r.verdict === 'missed'; }).length;
|
|
372
|
+
document.getElementById('sub').textContent =
|
|
373
|
+
'Session done: ' + (results.length - missed) + ' got it, ' + missed + ' missed.';
|
|
374
|
+
hint.textContent = '';
|
|
375
|
+
stage.innerHTML = '<div class="done">Nice work — your Studiemaat has the recap.</div>';
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
var card = cardById(queue[idx]);
|
|
380
|
+
if (!card) {
|
|
381
|
+
idx += 1;
|
|
382
|
+
render();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
document.getElementById('sub').textContent = 'Card ' + (idx + 1) + ' of ' + queue.length;
|
|
386
|
+
dots.innerHTML = queue
|
|
387
|
+
.map(function (id, i) {
|
|
388
|
+
if (i === idx) return '<span class="dot current"></span>';
|
|
389
|
+
var r = results[i];
|
|
390
|
+
if (!r) return '<span class="dot"></span>';
|
|
391
|
+
return '<span class="dot ' + (r.verdict === 'missed' ? 'done-miss' : 'done-good') + '"></span>';
|
|
392
|
+
})
|
|
393
|
+
.join('');
|
|
394
|
+
stage.innerHTML =
|
|
395
|
+
'<div class="flashcard" id="card"><div><span class="face-label">' +
|
|
396
|
+
(flipped ? 'Back' : 'Front') + '</span>' + esc(flipped ? card.back : card.front) +
|
|
397
|
+
'</div></div>' +
|
|
398
|
+
(flipped
|
|
399
|
+
? '<div class="actions">' +
|
|
400
|
+
'<button type="button" class="got" id="got"' + (busy ? ' disabled' : '') + '>Got it</button>' +
|
|
401
|
+
'<button type="button" class="miss" id="miss"' + (busy ? ' disabled' : '') + '>Missed</button>' +
|
|
402
|
+
'</div>'
|
|
403
|
+
: '');
|
|
404
|
+
hint.textContent = flipped ? '' : 'Tap the card to reveal the answer.';
|
|
405
|
+
document.getElementById('card').addEventListener('click', function () {
|
|
406
|
+
if (!flipped) {
|
|
407
|
+
flipped = true;
|
|
408
|
+
render();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
if (flipped) {
|
|
412
|
+
document.getElementById('got').addEventListener('click', function () { grade('got-it'); });
|
|
413
|
+
document.getElementById('miss').addEventListener('click', function () { grade('missed'); });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function startSession() {
|
|
418
|
+
queue = deck.dueCardIds.slice();
|
|
419
|
+
idx = 0;
|
|
420
|
+
flipped = false;
|
|
421
|
+
results = [];
|
|
422
|
+
render();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function grade(verdict) {
|
|
426
|
+
if (busy) return;
|
|
427
|
+
busy = true;
|
|
428
|
+
render();
|
|
429
|
+
var cardId = queue[idx];
|
|
430
|
+
gezel.tools
|
|
431
|
+
.invoke('record_review', { cardId: cardId, verdict: verdict })
|
|
432
|
+
.then(function () {
|
|
433
|
+
busy = false;
|
|
434
|
+
results[idx] = { cardId: cardId, verdict: verdict };
|
|
435
|
+
idx += 1;
|
|
436
|
+
flipped = false;
|
|
437
|
+
if (idx >= queue.length) return finishSession();
|
|
438
|
+
render();
|
|
439
|
+
})
|
|
440
|
+
.catch(function (err) {
|
|
441
|
+
busy = false;
|
|
442
|
+
setBanner(invokeErrorText(err));
|
|
443
|
+
render();
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function finishSession() {
|
|
448
|
+
var missedIds = results
|
|
449
|
+
.filter(function (r) { return r.verdict === 'missed'; })
|
|
450
|
+
.map(function (r) { return r.cardId; });
|
|
451
|
+
return gezel.tools
|
|
452
|
+
.invoke('finish_session', { reviewed: results.length, missedIds: missedIds })
|
|
453
|
+
.catch(function () {})
|
|
454
|
+
.then(function () {
|
|
455
|
+
render();
|
|
456
|
+
return loadDeck();
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function loadDeck() {
|
|
461
|
+
return gezel.data
|
|
462
|
+
.read('deck.json')
|
|
463
|
+
.then(function (next) {
|
|
464
|
+
deck = next;
|
|
465
|
+
// Only re-render outside an active session — mid-session the
|
|
466
|
+
// snapshot queue drives the UI.
|
|
467
|
+
if (queue === null || idx >= queue.length) render();
|
|
468
|
+
})
|
|
469
|
+
.catch(function () {});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
loadDeck();
|
|
473
|
+
gezel.data.watch('deck.json', function () { loadDeck(); });
|
|
474
|
+
})();
|
|
475
|
+
</script>
|
|
476
|
+
</body>
|
|
477
|
+
</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,143 @@
|
|
|
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
|
+
"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
|
+
"api": 1,
|
|
130
|
+
"reads": [
|
|
131
|
+
{
|
|
132
|
+
"source": "workspace",
|
|
133
|
+
"path": "invoices.json"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"workspaceSeed": [
|
|
138
|
+
"invoices.json"
|
|
139
|
+
],
|
|
140
|
+
"scripts": {
|
|
141
|
+
"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"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -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.
|