@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.
Files changed (108) 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-nevada-part-0001/manifest.json +24 -0
  8. package/data/knowledge-catalogs/ca/caselaw-nevada-part-0001/versions/2026.10.1/manifest.json +666 -0
  9. package/data/knowledge-catalogs/ca/caselaw-wyoming-part-0001/manifest.json +24 -0
  10. package/data/knowledge-catalogs/ca/caselaw-wyoming-part-0001/versions/2026.10.3/manifest.json +626 -0
  11. package/data/knowledge-catalogs/index.json +1 -1
  12. package/data/knowledge-catalogs/raw-index.json +1 -1
  13. package/data/project-types/ch/checkers/versions/1.2.1/about.md +9 -0
  14. package/data/project-types/ch/checkers/versions/1.2.1/game.json +77 -0
  15. package/data/project-types/ch/checkers/versions/1.2.1/manifest.json +168 -0
  16. package/data/project-types/ch/checkers/versions/1.2.1/mission.md +9 -0
  17. package/data/project-types/ch/checkers/versions/1.2.1/pages/board/index.html +1158 -0
  18. package/data/project-types/ch/chess/versions/1.0.4/about.md +9 -0
  19. package/data/project-types/ch/chess/versions/1.0.4/game.json +127 -0
  20. package/data/project-types/ch/chess/versions/1.0.4/manifest.json +182 -0
  21. package/data/project-types/ch/chess/versions/1.0.4/mission.md +8 -0
  22. package/data/project-types/ch/chess/versions/1.0.4/pages/board/index.html +851 -0
  23. package/data/project-types/de/design-scheme/versions/1.0.2/about.md +6 -0
  24. package/data/project-types/de/design-scheme/versions/1.0.2/manifest.json +70 -0
  25. package/data/project-types/de/design-scheme/versions/1.0.2/mission.md +3 -0
  26. package/data/project-types/de/design-scheme/versions/1.0.2/pages/gallery/index.html +212 -0
  27. package/data/project-types/de/design-scheme/versions/1.0.2/schemes/index.json +9 -0
  28. package/data/project-types/de/design-scheme/versions/1.0.2/schemes/warm-dusk.json +4 -0
  29. package/data/project-types/ev/event-planner/versions/1.0.2/about.md +7 -0
  30. package/data/project-types/ev/event-planner/versions/1.0.2/budget.json +5 -0
  31. package/data/project-types/ev/event-planner/versions/1.0.2/guests.json +10 -0
  32. package/data/project-types/ev/event-planner/versions/1.0.2/manifest.json +171 -0
  33. package/data/project-types/ev/event-planner/versions/1.0.2/mission.md +6 -0
  34. package/data/project-types/ev/event-planner/versions/1.0.2/pages/dashboard/index.html +283 -0
  35. package/data/project-types/fi/fitness-coach/versions/1.0.2/about.md +7 -0
  36. package/data/project-types/fi/fitness-coach/versions/1.0.2/manifest.json +126 -0
  37. package/data/project-types/fi/fitness-coach/versions/1.0.2/mission.md +7 -0
  38. package/data/project-types/fi/fitness-coach/versions/1.0.2/pages/dashboard/index.html +273 -0
  39. package/data/project-types/fi/fitness-coach/versions/1.0.2/training.json +6 -0
  40. package/data/project-types/fl/flashcards/versions/1.1.1/about.md +8 -0
  41. package/data/project-types/fl/flashcards/versions/1.1.1/deck.json +7 -0
  42. package/data/project-types/fl/flashcards/versions/1.1.1/manifest.json +168 -0
  43. package/data/project-types/fl/flashcards/versions/1.1.1/mission.md +6 -0
  44. package/data/project-types/fl/flashcards/versions/1.1.1/pages/review/index.html +477 -0
  45. package/data/project-types/fr/freelance-office/versions/1.0.2/about.md +6 -0
  46. package/data/project-types/fr/freelance-office/versions/1.0.2/invoices.json +9 -0
  47. package/data/project-types/fr/freelance-office/versions/1.0.2/manifest.json +143 -0
  48. package/data/project-types/fr/freelance-office/versions/1.0.2/mission.md +6 -0
  49. package/data/project-types/fr/freelance-office/versions/1.0.2/pages/dashboard/index.html +258 -0
  50. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/about.md +7 -0
  51. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/donations.json +7 -0
  52. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/manifest.json +127 -0
  53. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/mission.md +6 -0
  54. package/data/project-types/fu/fundraiser-hq/versions/1.0.2/pages/dashboard/index.html +261 -0
  55. package/data/project-types/go/go/versions/1.0.2/about.md +9 -0
  56. package/data/project-types/go/go/versions/1.0.2/game.json +103 -0
  57. package/data/project-types/go/go/versions/1.0.2/manifest.json +168 -0
  58. package/data/project-types/go/go/versions/1.0.2/mission.md +8 -0
  59. package/data/project-types/go/go/versions/1.0.2/pages/board/index.html +855 -0
  60. package/data/project-types/ho/household-budget/versions/1.0.3/about.md +6 -0
  61. package/data/project-types/ho/household-budget/versions/1.0.3/ledger.json +5 -0
  62. package/data/project-types/ho/household-budget/versions/1.0.3/manifest.json +164 -0
  63. package/data/project-types/ho/household-budget/versions/1.0.3/mission.md +5 -0
  64. package/data/project-types/ho/household-budget/versions/1.0.3/pages/dashboard/index.html +340 -0
  65. package/data/project-types/ho/household-manual/versions/1.0.2/about.md +7 -0
  66. package/data/project-types/ho/household-manual/versions/1.0.2/manifest.json +153 -0
  67. package/data/project-types/ho/household-manual/versions/1.0.2/mission.md +8 -0
  68. package/data/project-types/ho/household-manual/versions/1.0.2/pages/dashboard/index.html +258 -0
  69. package/data/project-types/ho/household-manual/versions/1.0.2/systems.json +8 -0
  70. package/data/project-types/index.json +1 -1
  71. package/data/project-types/jo/job-hunt/versions/1.0.2/about.md +10 -0
  72. package/data/project-types/jo/job-hunt/versions/1.0.2/activity.json +4 -0
  73. package/data/project-types/jo/job-hunt/versions/1.0.2/manifest.json +213 -0
  74. package/data/project-types/jo/job-hunt/versions/1.0.2/mission.md +9 -0
  75. package/data/project-types/jo/job-hunt/versions/1.0.2/pages/dashboard/index.html +430 -0
  76. package/data/project-types/jo/job-hunt/versions/1.0.2/pipeline.json +6 -0
  77. package/data/project-types/la/language-trainer/versions/1.0.2/about.md +6 -0
  78. package/data/project-types/la/language-trainer/versions/1.0.2/manifest.json +80 -0
  79. package/data/project-types/la/language-trainer/versions/1.0.2/mission.md +4 -0
  80. package/data/project-types/la/language-trainer/versions/1.0.2/pages/dashboard/index.html +268 -0
  81. package/data/project-types/la/language-trainer/versions/1.0.2/progress.json +7 -0
  82. package/data/project-types/li/life-binder/versions/1.0.3/about.md +7 -0
  83. package/data/project-types/li/life-binder/versions/1.0.3/documents.json +145 -0
  84. package/data/project-types/li/life-binder/versions/1.0.3/manifest.json +138 -0
  85. package/data/project-types/li/life-binder/versions/1.0.3/mission.md +6 -0
  86. package/data/project-types/li/life-binder/versions/1.0.3/pages/dashboard/index.html +248 -0
  87. package/data/project-types/me/meal-planner/versions/1.0.2/about.md +6 -0
  88. package/data/project-types/me/meal-planner/versions/1.0.2/manifest.json +160 -0
  89. package/data/project-types/me/meal-planner/versions/1.0.2/menu.json +5 -0
  90. package/data/project-types/me/meal-planner/versions/1.0.2/mission.md +5 -0
  91. package/data/project-types/me/meal-planner/versions/1.0.2/pages/dashboard/index.html +248 -0
  92. package/data/project-types/me/meal-planner/versions/1.0.2/pantry.json +9 -0
  93. package/data/project-types/no/novel-writing-room/versions/1.0.2/about.md +7 -0
  94. package/data/project-types/no/novel-writing-room/versions/1.0.2/chapters.json +10 -0
  95. package/data/project-types/no/novel-writing-room/versions/1.0.2/manifest.json +158 -0
  96. package/data/project-types/no/novel-writing-room/versions/1.0.2/mission.md +7 -0
  97. package/data/project-types/no/novel-writing-room/versions/1.0.2/pages/dashboard/index.html +264 -0
  98. package/data/project-types/no/novel-writing-room/versions/1.0.2/sessions.json +6 -0
  99. package/data/project-types/raw-index.json +1 -1
  100. package/data/project-types/tr/trip-planner/versions/1.0.2/about.md +7 -0
  101. package/data/project-types/tr/trip-planner/versions/1.0.2/journal.json +4 -0
  102. package/data/project-types/tr/trip-planner/versions/1.0.2/manifest.json +194 -0
  103. package/data/project-types/tr/trip-planner/versions/1.0.2/mission.md +4 -0
  104. package/data/project-types/tr/trip-planner/versions/1.0.2/packing.json +8 -0
  105. package/data/project-types/tr/trip-planner/versions/1.0.2/pages/dashboard/index.html +308 -0
  106. package/data/project-types/tr/trip-planner/versions/1.0.2/trip.json +7 -0
  107. package/package.json +1 -1
  108. package/schemas/project-type-version.schema.json +13 -0
@@ -0,0 +1,6 @@
1
+ The household ledger, kept honest. Tell your Penningmeester when money moves —
2
+ "14.20 on groceries", "salary landed" — and it goes straight into the ledger on
3
+ this machine.
4
+
5
+ The **Output** tab shows the month at a glance: in, out, net ({{currency}}), the
6
+ biggest categories, and a quick-add box for the receipt in your hand.
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "currency": "{{currency}}",
4
+ "entries": []
5
+ }
@@ -0,0 +1,164 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.3",
4
+ "releasedAt": "2026-10-06T00:00:00Z",
5
+ "meesterManaged": false,
6
+ "params": {
7
+ "type": "object",
8
+ "properties": {
9
+ "currency": {
10
+ "type": "string",
11
+ "title": "Currency",
12
+ "description": "Shown on the dashboard; entries are unit-agnostic.",
13
+ "default": "USD"
14
+ }
15
+ },
16
+ "required": [
17
+ "currency"
18
+ ]
19
+ },
20
+ "nameTemplate": "Household Budget",
21
+ "aboutTemplate": "about.md",
22
+ "missionTemplate": "mission.md",
23
+ "gezels": [
24
+ {
25
+ "templateId": "penningmeester",
26
+ "voorman": true
27
+ }
28
+ ],
29
+ "tools": [
30
+ {
31
+ "name": "add_expense",
32
+ "description": "Record spending the moment it happens (amount in ordinary units).",
33
+ "script": "budget-store",
34
+ "inputs": {
35
+ "type": "object",
36
+ "properties": {
37
+ "amount": {
38
+ "type": "number"
39
+ },
40
+ "category": {
41
+ "type": "string"
42
+ },
43
+ "note": {
44
+ "type": "string"
45
+ }
46
+ },
47
+ "required": [
48
+ "amount",
49
+ "category"
50
+ ]
51
+ },
52
+ "bind": {
53
+ "action": "add_expense"
54
+ }
55
+ },
56
+ {
57
+ "name": "add_income",
58
+ "description": "Record money coming in.",
59
+ "script": "budget-store",
60
+ "inputs": {
61
+ "type": "object",
62
+ "properties": {
63
+ "amount": {
64
+ "type": "number"
65
+ },
66
+ "category": {
67
+ "type": "string"
68
+ },
69
+ "note": {
70
+ "type": "string"
71
+ }
72
+ },
73
+ "required": [
74
+ "amount",
75
+ "category"
76
+ ]
77
+ },
78
+ "bind": {
79
+ "action": "add_income"
80
+ }
81
+ },
82
+ {
83
+ "name": "budget_status",
84
+ "description": "This month's shape: in, out, net, and the biggest categories.",
85
+ "script": "budget-store",
86
+ "inputs": {
87
+ "type": "object",
88
+ "properties": {}
89
+ },
90
+ "bind": {
91
+ "action": "status"
92
+ }
93
+ },
94
+ {
95
+ "name": "month_summary",
96
+ "description": "Summarize any month ('YYYY-MM').",
97
+ "script": "budget-store",
98
+ "inputs": {
99
+ "type": "object",
100
+ "properties": {
101
+ "month": {
102
+ "type": "string"
103
+ }
104
+ },
105
+ "required": [
106
+ "month"
107
+ ]
108
+ },
109
+ "bind": {
110
+ "action": "month_summary"
111
+ }
112
+ },
113
+ {
114
+ "name": "record_expense",
115
+ "description": "The dashboard's quick-add expense entry.",
116
+ "script": "budget-store",
117
+ "inputs": {
118
+ "type": "object",
119
+ "properties": {
120
+ "amount": {
121
+ "type": "number"
122
+ },
123
+ "category": {
124
+ "type": "string"
125
+ },
126
+ "note": {
127
+ "type": "string"
128
+ }
129
+ },
130
+ "required": [
131
+ "amount",
132
+ "category"
133
+ ]
134
+ },
135
+ "bind": {
136
+ "action": "add_expense"
137
+ }
138
+ }
139
+ ],
140
+ "craftbooks": [
141
+ "subscription-audit",
142
+ "month-close"
143
+ ],
144
+ "schedules": [],
145
+ "pages": {
146
+ "entry": "dashboard/index.html",
147
+ "api": 1,
148
+ "reads": [
149
+ {
150
+ "source": "workspace",
151
+ "path": "ledger.json"
152
+ }
153
+ ],
154
+ "tools": [
155
+ "record_expense"
156
+ ]
157
+ },
158
+ "workspaceSeed": [
159
+ "ledger.json"
160
+ ],
161
+ "scripts": {
162
+ "budget-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { ledgerStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'budget-store',\n description:\n \"Read and update the household ledger (ledger.json). Call action:'status' for this month's shape (in/out/net, top categories); 'add_expense' or 'add_income' the moment money moves (amount in ordinary units, e.g. 12.50); 'month_summary' with month 'YYYY-MM' for any past month.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'status', 'add_expense', 'add_income', or 'month_summary'.\",\n required: true,\n },\n amount: { type: 'number', description: 'Amount in ordinary currency units, e.g. 12.50.' },\n category: { type: 'string', description: \"Category, e.g. 'groceries', 'rent', 'salary'.\" },\n note: { type: 'string', description: 'Short note on the entry.' },\n month: { type: 'string', description: \"Month for month_summary, e.g. '2026-06'.\" },\n },\n outputs: {\n entries: { type: 'number', description: 'Total ledger entries.' },\n summary: { type: 'string', description: 'Human-readable money summary.' },\n netCents: { type: 'number', description: 'Net for the summarized month, in cents.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── budget-rules-start ──\n// Pure derivations shared with tests: cents conversion and the month\n// summary line, kept engine-side so the dashboard, the tools, and the\n// tests agree on one rendering.\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 topCategories(byCategory, kind, limit) {\n return Object.entries(byCategory)\n .map(function (pair) {\n return { category: pair[0], cents: kind === 'out' ? pair[1].outCents : pair[1].inCents };\n })\n .filter(function (row) {\n return row.cents > 0;\n })\n .sort(function (a, b) {\n return b.cents - a.cents;\n })\n .slice(0, limit);\n}\n\nfunction monthLine(month, totals) {\n var top = topCategories(totals.byCategory, 'out', 3)\n .map(function (row) {\n return row.category + ' ' + fmtCents(row.cents);\n })\n .join(', ');\n return (\n month +\n ': in ' +\n fmtCents(totals.inCents) +\n ', out ' +\n fmtCents(totals.outCents) +\n ', net ' +\n fmtCents(totals.netCents) +\n (top ? '. Biggest: ' + top + '.' : '. No spending recorded.')\n );\n}\n// ── budget-rules-end ──\n\nconst ledger = ledgerStore(gezel.fs, 'ledger.json');\nconst currentMonth = new Date().toISOString().slice(0, 7);\n\nif (input.action === 'add_expense' || input.action === 'add_income') {\n const cents = toCents(input.amount);\n if (cents === null) throw new Error(\"Pass a positive 'amount' in ordinary units, e.g. 12.50.\");\n if (!input.category) throw new Error(\"Pass a 'category', e.g. 'groceries' or 'salary'.\");\n await ledger.add({\n amountCents: cents,\n kind: input.action === 'add_expense' ? 'out' : 'in',\n category: input.category.toLowerCase(),\n ...(input.note ? { note: input.note } : {}),\n });\n} else if (input.action !== 'status' && input.action !== 'month_summary') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, add_expense, add_income, or month_summary.`,\n );\n}\n\nconst month =\n input.action === 'month_summary' && input.month ? input.month : currentMonth;\nconst totals = await ledger.totals({ month });\nconst all = await ledger.list();\n\ngezel.output({\n entries: all.length,\n summary: monthLine(month, totals),\n netCents: totals.netCents,\n});\n"
163
+ }
164
+ }
@@ -0,0 +1,5 @@
1
+ - Every expense and income lands in the ledger the day it happens.
2
+ - The month gets closed with a twenty-minute review: in, out, net, biggest
3
+ categories, one observation.
4
+ - Quiet leaks get surfaced — recurring charges and drifting categories, with
5
+ figures attached, never a lecture.
@@ -0,0 +1,340 @@
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>Household budget</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: 760px; 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 1rem; }
29
+ .tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; margin-bottom: 1rem; }
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); line-height: 1.2; }
32
+ .tile .n.good { color: var(--good); }
33
+ .tile .n.warn { color: var(--warn); }
34
+ .tile .l { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
35
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
36
+ .panel { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.85rem; }
37
+ .panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
38
+ .bar-row { display: grid; grid-template-columns: 7.5rem 1fr 4.5rem; gap: 0.5rem; align-items: center; margin: 0.3rem 0; }
39
+ .bar-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
40
+ .bar { background: var(--track); border-radius: 4px; height: 10px; overflow: hidden; }
41
+ .bar > span { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
42
+ .bar-row .amt { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
43
+ .entries { margin: 0; padding-left: 1.05rem; }
44
+ .entries li { margin: 0.25rem 0; color: var(--muted); }
45
+ .entries li strong { color: var(--ink); font-weight: 600; }
46
+ form { display: grid; grid-template-columns: 5.5rem 1fr auto; gap: 0.4rem; margin-top: 0.5rem; }
47
+ input { font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--track); border-radius: 6px; padding: 0.35rem 0.5rem; min-width: 0; min-height: 44px; }
48
+ button { font: inherit; border: 1px solid var(--track); background: var(--card); color: var(--ink); border-radius: 6px; padding: 0.35rem 0.8rem; cursor: pointer; min-height: 44px; }
49
+ button:hover { border-color: var(--accent); }
50
+ button:disabled { opacity: 0.5; cursor: default; }
51
+ .banner { border: 1px solid var(--accent); border-radius: 6px; padding: 0.45rem 0.7rem; font-size: 0.85rem; margin-bottom: 0.75rem; display: none; }
52
+ .banner.show { display: block; }
53
+ .note { margin: 0 0 0.85rem; padding: 0.5rem 0.65rem; border: 1px solid var(--track); border-radius: 6px; background: var(--card); color: var(--muted); font-size: 0.8rem; }
54
+ .empty { color: var(--muted); font-style: italic; }
55
+ @media (max-width: 640px) { .cols { grid-template-columns: 1fr; } .tiles { grid-template-columns: 1fr 1fr; } }
56
+ .demo-banner {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: space-between;
60
+ gap: 0.75rem;
61
+ margin: 0 0 0.85rem;
62
+ padding: 0.5rem 0.65rem;
63
+ border: 1px solid var(--accent);
64
+ border-radius: 6px;
65
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
66
+ color: var(--ink);
67
+ font-size: 0.8rem;
68
+ }
69
+ .demo-banner[hidden] { display: none; }
70
+ .demo-banner strong { font-weight: 700; }
71
+ .demo-reset {
72
+ flex: none;
73
+ padding: 0.25rem 0.5rem;
74
+ border: 1px solid var(--accent);
75
+ border-radius: 6px;
76
+ background: var(--card);
77
+ color: var(--ink);
78
+ cursor: pointer;
79
+ font: inherit;
80
+ }
81
+ .demo-reset:hover { border-color: var(--ink); }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <main>
86
+ <div class="demo-banner" data-gezel-demo-banner hidden>
87
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
88
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
89
+ </div>
90
+ <p class="note" id="browser-note" hidden>
91
+ Viewing from a browser tab — the ledger stays live, but quick add happens in the app's Output pane.
92
+ </p>
93
+ <div class="banner" id="banner"></div>
94
+ <h1>Household budget</h1>
95
+ <p class="sub" id="sub">Loading the ledger…</p>
96
+ <div class="tiles">
97
+ <div class="tile"><div class="n good" id="t-in">—</div><div class="l">In this month</div></div>
98
+ <div class="tile"><div class="n warn" id="t-out">—</div><div class="l">Out this month</div></div>
99
+ <div class="tile"><div class="n" id="t-net">—</div><div class="l">Net</div></div>
100
+ </div>
101
+ <div class="cols">
102
+ <div class="panel">
103
+ <h2>Where it goes</h2>
104
+ <div id="cats"><p class="empty">No spending recorded this month.</p></div>
105
+ </div>
106
+ <div class="panel">
107
+ <h2>Recent entries</h2>
108
+ <ul class="entries" id="entries"><li class="empty">Nothing yet — tell your Penningmeester, or use quick add.</li></ul>
109
+ <h2 style="margin-top: 0.9rem">Quick add expense</h2>
110
+ <form id="quick">
111
+ <input id="q-amount" type="number" step="0.01" min="0.01" placeholder="12.50" required />
112
+ <input id="q-category" type="text" placeholder="groceries" required />
113
+ <button type="button" id="q-go">Add</button>
114
+ </form>
115
+ </div>
116
+ </div>
117
+ </main>
118
+ <script>
119
+ /* gezel-page-demo-stub v1 */
120
+ /*
121
+ * The canonical demo stub for Gezel type pages. MIT License.
122
+ *
123
+ * Paste this file's bytes verbatim into a <script> block ahead of the
124
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
125
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
126
+ * injected and the stub steps aside; opened as a raw file (the live demos
127
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
128
+ * the page-supplied handler maps — one code path in every mode. `tools`
129
+ * and `data` map a tool name / declared read path to a function; data
130
+ * handlers back read() and list(), and url() returns the handler's value
131
+ * or passes a data: URI through. See docs/page-authoring.md.
132
+ */
133
+ (function () {
134
+ 'use strict';
135
+ window.makeDemoGezel = function (opts) {
136
+ if (window.gezel) return window.gezel;
137
+ var page = (opts && opts.page) || {};
138
+ var tools = (opts && opts.tools) || {};
139
+ var data = (opts && opts.data) || {};
140
+ var themeListeners = [];
141
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
142
+ var ui = {
143
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
144
+ onTheme: function (cb) {
145
+ themeListeners.push(cb);
146
+ return function () {
147
+ var at = themeListeners.indexOf(cb);
148
+ if (at >= 0) themeListeners.splice(at, 1);
149
+ };
150
+ },
151
+ };
152
+ if (media && media.addEventListener) {
153
+ media.addEventListener('change', function (ev) {
154
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
155
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
156
+ });
157
+ }
158
+ function fail(code, message) {
159
+ var err = new Error(message);
160
+ err.code = code;
161
+ return Promise.reject(err);
162
+ }
163
+ function run(map, key, arg) {
164
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
165
+ return Promise.resolve().then(function () { return map[key](arg); });
166
+ }
167
+ window.gezel = {
168
+ page: {
169
+ api: 1,
170
+ projectId: 'demo',
171
+ source: 'type',
172
+ entry: page.entry || 'index.html',
173
+ typeName: page.typeName || 'demo',
174
+ params: page.params || {},
175
+ mode: 'demo',
176
+ },
177
+ tools: {
178
+ list: function () { return Promise.resolve(Object.keys(tools)); },
179
+ invoke: function (name, input) {
180
+ var call = run(tools, name, input || {});
181
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
182
+ return call.then(function (output) { return { output: output }; });
183
+ },
184
+ },
185
+ data: {
186
+ read: function (path, o) {
187
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
188
+ },
189
+ list: function (path, o) {
190
+ return run(data, path, o || {}) || Promise.resolve([]);
191
+ },
192
+ watch: function () { return function () {}; },
193
+ url: function (path) {
194
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
195
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
196
+ },
197
+ },
198
+ ui: ui,
199
+ refresh: function () {},
200
+ };
201
+ return window.gezel;
202
+ };
203
+ })();
204
+ </script>
205
+ <script>
206
+ (function () {
207
+ 'use strict';
208
+
209
+ // ── demo ledger (used only when the page opens as a raw file) ──
210
+ var demoLedger = { currency: 'USD', entries: [
211
+ { kind: 'in', amountCents: 520000, category: 'Income', note: 'Monthly pay', at: new Date().toISOString() },
212
+ { kind: 'out', amountCents: 146000, category: 'Housing', note: 'Rent', at: new Date().toISOString() },
213
+ { kind: 'out', amountCents: 58200, category: 'Groceries', note: 'Weekly shops', at: new Date().toISOString() },
214
+ { kind: 'out', amountCents: 22400, category: 'Transport', note: 'Train and fuel', at: new Date().toISOString() },
215
+ { kind: 'out', amountCents: 18900, category: 'Eating out', note: 'Dinner with friends', at: new Date().toISOString() },
216
+ ] };
217
+
218
+ makeDemoGezel({
219
+ page: { entry: 'dashboard/index.html', typeName: 'Household Budget', params: { currency: 'USD' } },
220
+ tools: {
221
+ record_expense: function (input) {
222
+ var amount = Number(input && input.amount);
223
+ if (!(amount > 0) || !input.category) throw new Error('Give an amount above zero and a category.');
224
+ demoLedger.entries.push({
225
+ kind: 'out',
226
+ amountCents: Math.round(amount * 100),
227
+ category: input.category,
228
+ at: new Date().toISOString(),
229
+ });
230
+ return { summary: 'Recorded ' + amount.toFixed(2) + ' for ' + input.category + '.' };
231
+ },
232
+ },
233
+ data: {
234
+ 'ledger.json': function () { return demoLedger; },
235
+ },
236
+ });
237
+
238
+ // ── one code path from here: window.gezel exists in every mode ──
239
+ var demoMode = gezel.page.mode === 'demo';
240
+ var browserMode = gezel.page.mode === 'browser';
241
+
242
+ function applyTheme(mode) {
243
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
244
+ }
245
+ applyTheme(gezel.ui.theme.mode);
246
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
247
+
248
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
249
+ if (browserMode) document.getElementById('browser-note').hidden = false;
250
+
251
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
252
+ function fmt(cents) {
253
+ var sign = cents < 0 ? '-' : '';
254
+ var abs = Math.abs(cents);
255
+ return sign + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0');
256
+ }
257
+ function setBanner(text) { var el = document.getElementById('banner'); el.textContent = text || ''; el.className = text ? 'banner show' : 'banner'; }
258
+ function invokeErrorText(err) {
259
+ if (err && err.code === 'unavailable') {
260
+ return "Quick add happens in the app's Output pane — this browser tab stays view-only.";
261
+ }
262
+ return String(err && err.message ? err.message : err);
263
+ }
264
+
265
+ function render(ledger) {
266
+ var month = new Date().toISOString().slice(0, 7);
267
+ var entries = (ledger.entries || []).filter(function (e) { return typeof e.at === 'string'; });
268
+ var monthEntries = entries.filter(function (e) { return e.at.slice(0, 7) === month; });
269
+ var inC = 0, outC = 0; var byCat = {};
270
+ monthEntries.forEach(function (e) {
271
+ if (e.kind === 'in') inC += e.amountCents;
272
+ else { outC += e.amountCents; byCat[e.category] = (byCat[e.category] || 0) + e.amountCents; }
273
+ });
274
+ document.getElementById('sub').textContent = month + ' · ' + (ledger.currency || '') + ' · ' + entries.length + ' entr' + (entries.length === 1 ? 'y' : 'ies') + ' all time';
275
+ document.getElementById('t-in').textContent = fmt(inC);
276
+ document.getElementById('t-out').textContent = fmt(outC);
277
+ var net = document.getElementById('t-net');
278
+ net.textContent = fmt(inC - outC);
279
+ net.className = 'n ' + (inC - outC >= 0 ? 'good' : 'warn');
280
+
281
+ var cats = Object.entries(byCat).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 7);
282
+ var max = cats.length ? cats[0][1] : 1;
283
+ document.getElementById('cats').innerHTML = cats.length
284
+ ? cats.map(function (pair) {
285
+ return '<div class="bar-row"><span class="name">' + 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>';
286
+ }).join('')
287
+ : '<p class="empty">No spending recorded this month.</p>';
288
+
289
+ var recent = entries.slice(-7).reverse();
290
+ if (recent.length) {
291
+ document.getElementById('entries').innerHTML = recent.map(function (e) {
292
+ return '<li><strong>' + (e.kind === 'in' ? '+' : '-') + fmt(e.amountCents) + '</strong> ' + esc(e.category) + (e.note ? ' — ' + esc(e.note) : '') + '</li>';
293
+ }).join('');
294
+ }
295
+ }
296
+
297
+ function loadLedger() {
298
+ return gezel.data
299
+ .read('ledger.json')
300
+ .then(function (next) { if (next) render(next); })
301
+ .catch(function () {});
302
+ }
303
+
304
+ // The Output pane frame is sandboxed without allow-forms, which blocks
305
+ // form submission before any submit event fires — so the quick add
306
+ // listens for the button and Enter itself.
307
+ function quickAdd() {
308
+ var amount = parseFloat(document.getElementById('q-amount').value);
309
+ var category = document.getElementById('q-category').value.trim();
310
+ if (!(amount > 0) || !category) return;
311
+ var btn = document.getElementById('q-go');
312
+ btn.disabled = true;
313
+ gezel.tools
314
+ .invoke('record_expense', { amount: amount, category: category })
315
+ .then(function () {
316
+ setBanner('');
317
+ document.getElementById('q-amount').value = '';
318
+ document.getElementById('q-category').value = '';
319
+ return loadLedger();
320
+ })
321
+ .catch(function (err) { setBanner(invokeErrorText(err)); })
322
+ .then(function () { btn.disabled = false; });
323
+ }
324
+ document.getElementById('quick').addEventListener('submit', function (ev) { ev.preventDefault(); });
325
+ document.getElementById('q-go').addEventListener('click', quickAdd);
326
+ ['q-amount', 'q-category'].forEach(function (id) {
327
+ document.getElementById(id).addEventListener('keydown', function (ev) {
328
+ if (ev.key === 'Enter') {
329
+ ev.preventDefault();
330
+ quickAdd();
331
+ }
332
+ });
333
+ });
334
+
335
+ loadLedger();
336
+ gezel.data.watch('ledger.json', function () { loadLedger(); });
337
+ })();
338
+ </script>
339
+ </body>
340
+ </html>
@@ -0,0 +1,7 @@
1
+ The operations book for {{home}}. Tell your Huismeester about the systems that
2
+ need regular care — the furnace, the gutters, the smoke alarms — and each gets a
3
+ card: what it is, where it lives, what care it needs, how often. Maintenance
4
+ gets logged; the schedule takes care of itself.
5
+
6
+ The **Output** tab shows what the house needs right now: care due and anything
7
+ flagged for attention.