@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.
Files changed (106) hide show
  1. package/README.md +17 -0
  2. package/data/project-types/ca/caregiving-binder/versions/1.0.1/about.md +8 -0
  3. package/data/project-types/ca/caregiving-binder/versions/1.0.1/care.json +4 -0
  4. package/data/project-types/ca/caregiving-binder/versions/1.0.1/manifest.json +177 -0
  5. package/data/project-types/ca/caregiving-binder/versions/1.0.1/meds.json +8 -0
  6. package/data/project-types/ca/caregiving-binder/versions/1.0.1/mission.md +8 -0
  7. package/data/project-types/ca/caregiving-binder/versions/1.0.1/pages/dashboard/index.html +167 -0
  8. package/data/project-types/ch/checkers/versions/1.1.2/about.md +9 -0
  9. package/data/project-types/ch/checkers/versions/1.1.2/game.json +77 -0
  10. package/data/project-types/ch/checkers/versions/1.1.2/manifest.json +166 -0
  11. package/data/project-types/ch/checkers/versions/1.1.2/mission.md +9 -0
  12. package/data/project-types/ch/checkers/versions/1.1.2/pages/board/index.html +1091 -0
  13. package/data/project-types/ch/chess/versions/1.0.2/about.md +9 -0
  14. package/data/project-types/ch/chess/versions/1.0.2/game.json +127 -0
  15. package/data/project-types/ch/chess/versions/1.0.2/manifest.json +180 -0
  16. package/data/project-types/ch/chess/versions/1.0.2/mission.md +8 -0
  17. package/data/project-types/ch/chess/versions/1.0.2/pages/board/index.html +627 -0
  18. package/data/project-types/ch/chess/versions/1.0.3/about.md +9 -0
  19. package/data/project-types/ch/chess/versions/1.0.3/game.json +127 -0
  20. package/data/project-types/ch/chess/versions/1.0.3/manifest.json +180 -0
  21. package/data/project-types/ch/chess/versions/1.0.3/mission.md +8 -0
  22. package/data/project-types/ch/chess/versions/1.0.3/pages/board/index.html +769 -0
  23. package/data/project-types/de/design-scheme/versions/1.0.1/about.md +6 -0
  24. package/data/project-types/de/design-scheme/versions/1.0.1/manifest.json +69 -0
  25. package/data/project-types/de/design-scheme/versions/1.0.1/mission.md +3 -0
  26. package/data/project-types/de/design-scheme/versions/1.0.1/pages/gallery/index.html +105 -0
  27. package/data/project-types/de/design-scheme/versions/1.0.1/schemes/index.json +9 -0
  28. package/data/project-types/de/design-scheme/versions/1.0.1/schemes/warm-dusk.json +4 -0
  29. package/data/project-types/ev/event-planner/versions/1.0.1/about.md +7 -0
  30. package/data/project-types/ev/event-planner/versions/1.0.1/budget.json +5 -0
  31. package/data/project-types/ev/event-planner/versions/1.0.1/guests.json +10 -0
  32. package/data/project-types/ev/event-planner/versions/1.0.1/manifest.json +170 -0
  33. package/data/project-types/ev/event-planner/versions/1.0.1/mission.md +6 -0
  34. package/data/project-types/ev/event-planner/versions/1.0.1/pages/dashboard/index.html +186 -0
  35. package/data/project-types/fi/fitness-coach/versions/1.0.1/about.md +7 -0
  36. package/data/project-types/fi/fitness-coach/versions/1.0.1/manifest.json +125 -0
  37. package/data/project-types/fi/fitness-coach/versions/1.0.1/mission.md +7 -0
  38. package/data/project-types/fi/fitness-coach/versions/1.0.1/pages/dashboard/index.html +172 -0
  39. package/data/project-types/fi/fitness-coach/versions/1.0.1/training.json +6 -0
  40. package/data/project-types/fl/flashcards/versions/1.0.1/about.md +8 -0
  41. package/data/project-types/fl/flashcards/versions/1.0.1/deck.json +7 -0
  42. package/data/project-types/fl/flashcards/versions/1.0.1/manifest.json +167 -0
  43. package/data/project-types/fl/flashcards/versions/1.0.1/mission.md +6 -0
  44. package/data/project-types/fl/flashcards/versions/1.0.1/pages/review/index.html +396 -0
  45. package/data/project-types/fr/freelance-office/versions/1.0.1/about.md +6 -0
  46. package/data/project-types/fr/freelance-office/versions/1.0.1/invoices.json +9 -0
  47. package/data/project-types/fr/freelance-office/versions/1.0.1/manifest.json +142 -0
  48. package/data/project-types/fr/freelance-office/versions/1.0.1/mission.md +6 -0
  49. package/data/project-types/fr/freelance-office/versions/1.0.1/pages/dashboard/index.html +157 -0
  50. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/about.md +7 -0
  51. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/donations.json +7 -0
  52. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/manifest.json +126 -0
  53. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/mission.md +6 -0
  54. package/data/project-types/fu/fundraiser-hq/versions/1.0.1/pages/dashboard/index.html +160 -0
  55. package/data/project-types/go/go/versions/1.0.1/about.md +9 -0
  56. package/data/project-types/go/go/versions/1.0.1/game.json +103 -0
  57. package/data/project-types/go/go/versions/1.0.1/manifest.json +166 -0
  58. package/data/project-types/go/go/versions/1.0.1/mission.md +8 -0
  59. package/data/project-types/go/go/versions/1.0.1/pages/board/index.html +778 -0
  60. package/data/project-types/ho/household-budget/versions/1.0.1/about.md +6 -0
  61. package/data/project-types/ho/household-budget/versions/1.0.1/ledger.json +5 -0
  62. package/data/project-types/ho/household-budget/versions/1.0.1/manifest.json +163 -0
  63. package/data/project-types/ho/household-budget/versions/1.0.1/mission.md +6 -0
  64. package/data/project-types/ho/household-budget/versions/1.0.1/pages/dashboard/index.html +233 -0
  65. package/data/project-types/ho/household-manual/versions/1.0.1/about.md +7 -0
  66. package/data/project-types/ho/household-manual/versions/1.0.1/manifest.json +152 -0
  67. package/data/project-types/ho/household-manual/versions/1.0.1/mission.md +8 -0
  68. package/data/project-types/ho/household-manual/versions/1.0.1/pages/dashboard/index.html +157 -0
  69. package/data/project-types/ho/household-manual/versions/1.0.1/systems.json +8 -0
  70. package/data/project-types/index.json +1 -1
  71. package/data/project-types/jo/job-hunt/versions/1.0.1/about.md +10 -0
  72. package/data/project-types/jo/job-hunt/versions/1.0.1/activity.json +4 -0
  73. package/data/project-types/jo/job-hunt/versions/1.0.1/manifest.json +212 -0
  74. package/data/project-types/jo/job-hunt/versions/1.0.1/mission.md +9 -0
  75. package/data/project-types/jo/job-hunt/versions/1.0.1/pages/dashboard/index.html +327 -0
  76. package/data/project-types/jo/job-hunt/versions/1.0.1/pipeline.json +6 -0
  77. package/data/project-types/la/language-trainer/versions/1.0.1/about.md +6 -0
  78. package/data/project-types/la/language-trainer/versions/1.0.1/manifest.json +79 -0
  79. package/data/project-types/la/language-trainer/versions/1.0.1/mission.md +4 -0
  80. package/data/project-types/la/language-trainer/versions/1.0.1/pages/dashboard/index.html +153 -0
  81. package/data/project-types/la/language-trainer/versions/1.0.1/progress.json +7 -0
  82. package/data/project-types/li/life-binder/versions/1.0.1/about.md +7 -0
  83. package/data/project-types/li/life-binder/versions/1.0.1/documents.json +145 -0
  84. package/data/project-types/li/life-binder/versions/1.0.1/manifest.json +137 -0
  85. package/data/project-types/li/life-binder/versions/1.0.1/mission.md +7 -0
  86. package/data/project-types/li/life-binder/versions/1.0.1/pages/dashboard/index.html +147 -0
  87. package/data/project-types/me/meal-planner/versions/1.0.1/about.md +6 -0
  88. package/data/project-types/me/meal-planner/versions/1.0.1/manifest.json +159 -0
  89. package/data/project-types/me/meal-planner/versions/1.0.1/menu.json +5 -0
  90. package/data/project-types/me/meal-planner/versions/1.0.1/mission.md +5 -0
  91. package/data/project-types/me/meal-planner/versions/1.0.1/pages/dashboard/index.html +152 -0
  92. package/data/project-types/me/meal-planner/versions/1.0.1/pantry.json +9 -0
  93. package/data/project-types/no/novel-writing-room/versions/1.0.1/about.md +7 -0
  94. package/data/project-types/no/novel-writing-room/versions/1.0.1/chapters.json +10 -0
  95. package/data/project-types/no/novel-writing-room/versions/1.0.1/manifest.json +157 -0
  96. package/data/project-types/no/novel-writing-room/versions/1.0.1/mission.md +7 -0
  97. package/data/project-types/no/novel-writing-room/versions/1.0.1/pages/dashboard/index.html +165 -0
  98. package/data/project-types/no/novel-writing-room/versions/1.0.1/sessions.json +6 -0
  99. package/data/project-types/tr/trip-planner/versions/1.0.1/about.md +7 -0
  100. package/data/project-types/tr/trip-planner/versions/1.0.1/journal.json +4 -0
  101. package/data/project-types/tr/trip-planner/versions/1.0.1/manifest.json +193 -0
  102. package/data/project-types/tr/trip-planner/versions/1.0.1/mission.md +4 -0
  103. package/data/project-types/tr/trip-planner/versions/1.0.1/packing.json +8 -0
  104. package/data/project-types/tr/trip-planner/versions/1.0.1/pages/dashboard/index.html +215 -0
  105. package/data/project-types/tr/trip-planner/versions/1.0.1/trip.json +7 -0
  106. package/package.json +4 -2
@@ -0,0 +1,6 @@
1
+ A project for exploring the visual direction of **{{brief}}**. Work with a designer to
2
+ develop color schemes and design directions, iterate on them in conversation, and keep the
3
+ ones that feel right.
4
+
5
+ Open the **Output** tab to see the scheme gallery — every palette the designer saves shows
6
+ up there as a set of swatches you can compare at a glance.
@@ -0,0 +1,69 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "1.0.1",
4
+ "releasedAt": "2026-08-01T00:00:00Z",
5
+ "extends": "design-prototype",
6
+ "params": {
7
+ "type": "object",
8
+ "properties": {
9
+ "brief": {
10
+ "type": "string",
11
+ "title": "What are you designing?",
12
+ "description": "A short brief — e.g. \"a calm meditation app\", \"a bold sports brand\".",
13
+ "default": "a new brand"
14
+ }
15
+ },
16
+ "required": [
17
+ "brief"
18
+ ]
19
+ },
20
+ "nameTemplate": "Design Scheme: {{brief}}",
21
+ "aboutTemplate": "about.md",
22
+ "missionTemplate": "mission.md",
23
+ "gezels": [
24
+ {
25
+ "templateId": "designer",
26
+ "voorman": true
27
+ }
28
+ ],
29
+ "scripts": {
30
+ "scheme-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\n\nexport const meta = defineScript({\n name: 'scheme-store',\n description:\n \"Save and list the project's design color schemes. Call action:'save' with a name and an array of hex colors to add a scheme (it lands in artifacts/schemes/ and shows on the project's gallery). Call action:'list' to see what's saved.\",\n inputs: {\n action: { type: 'string', description: \"'save' or 'list'.\", required: true },\n name: { type: 'string', description: \"Scheme name (required for 'save'), e.g. 'Dusk'.\" },\n colors: {\n type: 'json',\n description: \"Array of hex colors for 'save', e.g. [\\\"#1b1a2e\\\", \\\"#e0894a\\\", \\\"#f5f0e8\\\"].\",\n },\n },\n outputs: {\n count: { type: 'number', description: 'How many schemes are saved.' },\n message: { type: 'string', description: 'Human-readable summary.' },\n },\n requires: ['artifacts.read', 'artifacts.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\ninterface Scheme {\n name: string;\n file: string;\n colors: string[];\n}\n\nconst INDEX = 'schemes/index.json';\n\nasync function loadIndex(): Promise<Scheme[]> {\n try {\n const raw = await gezel.artifacts.read(INDEX);\n const parsed = JSON.parse(raw) as { schemes?: Scheme[] };\n return Array.isArray(parsed.schemes) ? parsed.schemes : [];\n } catch {\n return [];\n }\n}\n\nfunction slugify(s: string): string {\n return (\n s\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '') || 'scheme'\n );\n}\n\nconst schemes = await loadIndex();\n\nif (input.action === 'save') {\n const name = (input.name ?? '').trim();\n const colors = Array.isArray(input.colors) ? input.colors.map(String) : [];\n if (!name || colors.length === 0) {\n gezel.output({ count: schemes.length, message: 'save needs a name and a non-empty colors array.' });\n } else {\n const file = `schemes/${slugify(name)}.json`;\n await gezel.artifacts.write(file, `${JSON.stringify({ name, colors }, null, 2)}\\n`);\n const next = [...schemes.filter((s) => s.file !== file), { name, file, colors }];\n await gezel.artifacts.write(INDEX, `${JSON.stringify({ schemes: next }, null, 2)}\\n`);\n gezel.output({ count: next.length, message: `Saved \"${name}\" (${colors.length} colors). ${next.length} scheme(s) total.` });\n }\n} else {\n gezel.output({\n count: schemes.length,\n message: schemes.length\n ? `${schemes.length} scheme(s): ${schemes.map((s) => s.name).join(', ')}.`\n : 'No schemes saved yet.',\n });\n}\n"
31
+ },
32
+ "tools": [
33
+ {
34
+ "name": "save_scheme",
35
+ "description": "Save a named color scheme so it shows on the gallery.",
36
+ "script": "scheme-store",
37
+ "inputs": {
38
+ "type": "object",
39
+ "properties": {
40
+ "name": {
41
+ "type": "string"
42
+ },
43
+ "colors": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "string"
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "bind": {
52
+ "action": "save"
53
+ }
54
+ }
55
+ ],
56
+ "pages": {
57
+ "entry": "gallery/index.html",
58
+ "reads": [
59
+ {
60
+ "source": "artifacts",
61
+ "path": "schemes/index.json"
62
+ }
63
+ ]
64
+ },
65
+ "artifactsSeed": [
66
+ "schemes/index.json",
67
+ "schemes/warm-dusk.json"
68
+ ]
69
+ }
@@ -0,0 +1,3 @@
1
+ - Develop color schemes and design directions for {{brief}}.
2
+ - Save each promising palette so it shows on the gallery for comparison.
3
+ - Converge on a small set of strong directions worth taking forward.
@@ -0,0 +1,105 @@
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>Scheme gallery</title>
8
+ <style>
9
+ :root { color-scheme: light dark; --bg: #faf7f2; --card: #fff; --ink: #2b2620; --muted: #7a7166; --line: #ece5da; }
10
+ @media (prefers-color-scheme: dark) {
11
+ :root { --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c; --line: #3a352f; }
12
+ }
13
+ * { box-sizing: border-box; }
14
+ body { margin: 0; font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: var(--bg); color: var(--ink); padding: 2rem 1.25rem; }
15
+ main { max-width: 720px; margin: 0 auto; }
16
+ h1 { font-size: 1.5rem; margin: 0 0 0.25rem; }
17
+ .sub { color: var(--muted); margin: 0 0 1.5rem; }
18
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
19
+ .card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
20
+ .swatches { display: flex; height: 84px; }
21
+ .swatches > span { flex: 1; }
22
+ .card .name { padding: 0.6rem 0.85rem; font-weight: 600; }
23
+ .card .hexes { padding: 0 0.85rem 0.75rem; color: var(--muted); font-size: 0.78rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
24
+ .empty { color: var(--muted); font-style: italic; }
25
+ + .demo-banner {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ gap: 0.75rem;
30
+ margin: 0 0 0.85rem;
31
+ padding: 0.5rem 0.65rem;
32
+ border: 1px solid var(--accent, #8a5a2b);
33
+ border-radius: 6px;
34
+ background: color-mix(in srgb, var(--card, var(--surface, #fffdf8)) 88%, var(--accent, #8a5a2b));
35
+ color: var(--ink, #2b2620);
36
+ font-size: 0.8rem;
37
+ }
38
+ .demo-banner[hidden] { display: none; }
39
+ .demo-banner strong { font-weight: 700; }
40
+ .demo-reset {
41
+ flex: none;
42
+ padding: 0.25rem 0.5rem;
43
+ border: 1px solid var(--accent, #8a5a2b);
44
+ border-radius: 6px;
45
+ background: var(--card, var(--surface, #fffdf8));
46
+ color: var(--ink, #2b2620);
47
+ cursor: pointer;
48
+ font: inherit;
49
+ }
50
+ .demo-reset:hover { border-color: var(--ink, #2b2620); }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <main>
55
+ + <div class="demo-banner" data-gezel-demo-banner hidden>
56
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
57
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
58
+ </div>
59
+ <h1>Scheme gallery</h1>
60
+ <p class="sub" id="sub">Loading schemes…</p>
61
+ <div class="grid" id="grid"></div>
62
+ </main>
63
+ <script>
64
+ (function () {
65
+ var parts = location.pathname.split('/').filter(Boolean);
66
+ var i = parts.indexOf('type');
67
+ var capability = i > 0 ? parts[i - 1] : null;
68
+ var projectId = i >= 0 ? parts[i + 1] : null;
69
+ var demoMode = !capability || !projectId;
70
+ var demoSchemes = [
71
+ { name: 'Warm Dusk', colors: ['#2B2620', '#8D694D', '#D5B58C', '#EFE4D1', '#FFF8E8'] },
72
+ { name: 'Canal Morning', colors: ['#19323C', '#4F6D7A', '#9BB8B3', '#E7E3D4', '#F7F4EC'] },
73
+ { name: 'Workshop Sage', colors: ['#26352D', '#557A5B', '#A7B89A', '#DDD5BF', '#FAF7F2'] },
74
+ ];
75
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
76
+ function isHex(c) { return typeof c === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(c); }
77
+ function render(schemes) {
78
+ var sub = document.getElementById('sub');
79
+ var grid = document.getElementById('grid');
80
+ if (!schemes.length) {
81
+ sub.textContent = 'No schemes yet — ask the designer to save one.';
82
+ return;
83
+ }
84
+ sub.textContent = schemes.length + ' scheme' + (schemes.length === 1 ? '' : 's');
85
+ grid.innerHTML = schemes.map(function (s) {
86
+ var colors = Array.isArray(s.colors) ? s.colors.filter(isHex) : [];
87
+ var bars = colors.map(function (c) { return '<span style="background:' + c + '"></span>'; }).join('');
88
+ return '<div class="card"><div class="swatches">' + bars + '</div>' +
89
+ '<div class="name">' + esc(s.name || 'Untitled') + '</div>' +
90
+ '<div class="hexes">' + colors.map(esc).join(' · ') + '</div></div>';
91
+ }).join('');
92
+ }
93
+ if (demoMode) {
94
+ document.querySelector('[data-gezel-demo-banner]').hidden = false;
95
+ render(demoSchemes);
96
+ return;
97
+ }
98
+ fetch('/preview/' + encodeURIComponent(capability) + '/artifacts/' + encodeURIComponent(projectId) + '/schemes/index.json', { cache: 'no-store' })
99
+ .then(function (r) { if (!r.ok) throw new Error('no schemes'); return r.json(); })
100
+ .then(function (data) { render(Array.isArray(data.schemes) ? data.schemes : []); })
101
+ .catch(function () { render([]); });
102
+ })();
103
+ </script>
104
+ </body>
105
+ </html>
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemes": [
3
+ {
4
+ "name": "Warm Dusk",
5
+ "file": "schemes/warm-dusk.json",
6
+ "colors": ["#1b1a2e", "#3a2e4d", "#b5651d", "#e0894a", "#f5f0e8"]
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "Warm Dusk",
3
+ "colors": ["#1b1a2e", "#3a2e4d", "#b5651d", "#e0894a", "#f5f0e8"]
4
+ }
@@ -0,0 +1,7 @@
1
+ The planning table for {{occasion}} — {{date}}. Your Ceremoniemeester keeps the
2
+ three things that make or break an event: a guest list that's always current, a
3
+ budget recorded when money is committed, and a day-of runbook built early.
4
+
5
+ The **Output** tab shows the countdown, the headcount, and where the budget
6
+ stands. Ask "what does this week need?" and get the two or three things that
7
+ matter, not the whole list.
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "currency": "USD",
4
+ "entries": []
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "eventDate": "{{date}}",
4
+ "stages": [
5
+ "invited",
6
+ "yes",
7
+ "no"
8
+ ],
9
+ "records": []
10
+ }
@@ -0,0 +1,170 @@
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
+ "occasion": {
10
+ "type": "string",
11
+ "title": "What are we celebrating?",
12
+ "description": "e.g. 'our wedding', 'Dad's 70th', 'the class reunion'.",
13
+ "default": "a big celebration"
14
+ },
15
+ "date": {
16
+ "type": "string",
17
+ "title": "When is it?",
18
+ "description": "The date, e.g. 2026-09-12. A rough date is fine — change it later.",
19
+ "default": "2026-12-31"
20
+ }
21
+ },
22
+ "required": [
23
+ "occasion",
24
+ "date"
25
+ ]
26
+ },
27
+ "nameTemplate": "Event: {{occasion}}",
28
+ "aboutTemplate": "about.md",
29
+ "missionTemplate": "mission.md",
30
+ "gezels": [
31
+ {
32
+ "templateId": "ceremoniemeester",
33
+ "voorman": true
34
+ }
35
+ ],
36
+ "tools": [
37
+ {
38
+ "name": "add_guest",
39
+ "description": "Add a guest or party to the list (status starts at invited).",
40
+ "script": "event-store",
41
+ "inputs": {
42
+ "type": "object",
43
+ "properties": {
44
+ "name": {
45
+ "type": "string"
46
+ },
47
+ "party": {
48
+ "type": "number"
49
+ },
50
+ "note": {
51
+ "type": "string"
52
+ }
53
+ },
54
+ "required": [
55
+ "name"
56
+ ]
57
+ },
58
+ "bind": {
59
+ "action": "add_guest"
60
+ }
61
+ },
62
+ {
63
+ "name": "guest_status",
64
+ "description": "Record an RSVP: 'yes', 'no', or back to 'invited'.",
65
+ "script": "event-store",
66
+ "inputs": {
67
+ "type": "object",
68
+ "properties": {
69
+ "id": {
70
+ "type": "string"
71
+ },
72
+ "name": {
73
+ "type": "string"
74
+ },
75
+ "status": {
76
+ "type": "string",
77
+ "enum": [
78
+ "invited",
79
+ "yes",
80
+ "no"
81
+ ]
82
+ },
83
+ "note": {
84
+ "type": "string"
85
+ }
86
+ },
87
+ "required": [
88
+ "status"
89
+ ]
90
+ },
91
+ "bind": {
92
+ "action": "guest_status"
93
+ }
94
+ },
95
+ {
96
+ "name": "record_spend",
97
+ "description": "Record committed spending (deposit, balance, purchase) in ordinary units.",
98
+ "script": "event-store",
99
+ "inputs": {
100
+ "type": "object",
101
+ "properties": {
102
+ "amount": {
103
+ "type": "number"
104
+ },
105
+ "category": {
106
+ "type": "string"
107
+ },
108
+ "note": {
109
+ "type": "string"
110
+ }
111
+ },
112
+ "required": [
113
+ "amount",
114
+ "category"
115
+ ]
116
+ },
117
+ "bind": {
118
+ "action": "record_spend"
119
+ }
120
+ },
121
+ {
122
+ "name": "event_status",
123
+ "description": "Countdown, headcount, and budget in one line.",
124
+ "script": "event-store",
125
+ "inputs": {
126
+ "type": "object",
127
+ "properties": {}
128
+ },
129
+ "bind": {
130
+ "action": "status"
131
+ }
132
+ },
133
+ {
134
+ "name": "guest_list",
135
+ "description": "Read the whole guest list with statuses.",
136
+ "script": "event-store",
137
+ "inputs": {
138
+ "type": "object",
139
+ "properties": {}
140
+ },
141
+ "bind": {
142
+ "action": "guest_list"
143
+ }
144
+ }
145
+ ],
146
+ "craftbooks": [
147
+ "runbook"
148
+ ],
149
+ "schedules": [],
150
+ "pages": {
151
+ "entry": "dashboard/index.html",
152
+ "reads": [
153
+ {
154
+ "source": "workspace",
155
+ "path": "guests.json"
156
+ },
157
+ {
158
+ "source": "workspace",
159
+ "path": "budget.json"
160
+ }
161
+ ]
162
+ },
163
+ "workspaceSeed": [
164
+ "guests.json",
165
+ "budget.json"
166
+ ],
167
+ "scripts": {
168
+ "event-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { ledgerStore, rosterStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'event-store',\n description:\n \"Read and update the event: guests (guests.json) and spending (budget.json). Call action:'status' for headcount + budget + countdown; 'add_guest' as invitations go out; 'guest_status' as RSVPs land (invited/yes/no); 'record_spend' when money is committed (ordinary units); 'guest_list' to read everyone.\",\n inputs: {\n action: {\n type: 'string',\n description:\n \"One of 'status', 'add_guest', 'guest_status', 'record_spend', or 'guest_list'.\",\n required: true,\n },\n name: { type: 'string', description: 'Guest name (party name is fine).' },\n party: { type: 'number', description: 'How many people this entry covers (default 1).' },\n status: { type: 'string', description: \"RSVP status: 'invited', 'yes', or 'no'.\" },\n id: { type: 'string', description: \"Guest id, e.g. 'guest-4'.\" },\n amount: { type: 'number', description: 'Spend amount in ordinary units.' },\n category: { type: 'string', description: \"Spend category, e.g. 'venue', 'catering'.\" },\n note: { type: 'string', description: 'Short note.' },\n },\n outputs: {\n invited: { type: 'number', description: 'People invited (sum of parties).' },\n coming: { type: 'number', description: \"People who said yes.\" },\n spentCents: { type: 'number', description: 'Total committed spend, in cents.' },\n summary: { type: 'string', description: 'The event picture.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── event-rules-start ──\n// Pure derivations: headcounts weighted by party size, and the countdown.\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 abs = Math.abs(cents);\n return (cents < 0 ? '-' : '') + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0');\n}\n\nfunction partyOf(record) {\n var p = record.fields && record.fields.party;\n return typeof p === 'number' && p >= 1 ? Math.floor(p) : 1;\n}\n\nfunction headcounts(records) {\n var invited = 0;\n var coming = 0;\n var awaiting = 0;\n records.forEach(function (r) {\n var size = partyOf(r);\n invited += size;\n if (r.status === 'yes') coming += size;\n if (r.status === 'invited') awaiting += size;\n });\n return { invited: invited, coming: coming, awaiting: awaiting };\n}\n\nfunction daysUntil(dateStr, nowMs) {\n var when = Date.parse(dateStr);\n if (!isFinite(when)) return null;\n return Math.ceil((when - nowMs) / (24 * 60 * 60 * 1000));\n}\n\nfunction eventLine(records, spentCents, eventDate, nowMs) {\n var heads = headcounts(records);\n var days = daysUntil(eventDate, nowMs);\n return (\n (days === null ? '' : days >= 0 ? days + ' day(s) to go. ' : 'The day has passed. ') +\n heads.coming +\n ' coming, ' +\n heads.awaiting +\n ' awaiting reply (' +\n heads.invited +\n ' invited). Spent ' +\n fmtCents(spentCents) +\n '.'\n );\n}\n// ── event-rules-end ──\n\nconst guests = rosterStore(gezel.fs, 'guests.json', {\n stages: ['invited', 'yes', 'no'],\n idPrefix: 'guest',\n});\nconst budget = ledgerStore(gezel.fs, 'budget.json');\n\nasync function readEventDate(): Promise<string> {\n try {\n const parsed = JSON.parse(await gezel.fs.read('guests.json')) as { eventDate?: string };\n return typeof parsed.eventDate === 'string' ? parsed.eventDate : '';\n } catch {\n return '';\n }\n}\n\nasync function resolveGuest(): Promise<string> {\n if (input.id) return input.id;\n if (input.name) {\n const wanted = input.name.toLowerCase();\n const matches = (await guests.list()).filter(\n (r) => String(r.fields.name ?? '').toLowerCase() === wanted,\n );\n if (matches.length === 1) return matches[0]!.id;\n }\n throw new Error(\"Pass 'id' (e.g. 'guest-4') or an exact guest 'name'.\");\n}\n\nif (input.action === 'add_guest') {\n if (!input.name) throw new Error(\"add_guest needs 'name'.\");\n await guests.add({\n status: 'invited',\n fields: {\n name: input.name,\n ...(typeof input.party === 'number' && input.party >= 1 ? { party: Math.floor(input.party) } : {}),\n ...(input.note ? { note: input.note } : {}),\n },\n });\n} else if (input.action === 'guest_status') {\n if (!input.status || !['invited', 'yes', 'no'].includes(input.status)) {\n throw new Error(\"guest_status needs status 'invited', 'yes', or 'no'.\");\n }\n await guests.transition(await resolveGuest(), input.status, input.note ? { note: input.note } : {});\n} else if (input.action === 'record_spend') {\n const cents = toCents(input.amount);\n if (cents === null || !input.category) {\n throw new Error(\"record_spend needs a positive 'amount' and a 'category' (e.g. 'venue').\");\n }\n await budget.add({\n amountCents: cents,\n kind: 'out',\n category: input.category.toLowerCase(),\n ...(input.note ? { note: input.note } : {}),\n });\n} else if (input.action !== 'status' && input.action !== 'guest_list') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, add_guest, guest_status, record_spend, or guest_list.`,\n );\n}\n\nconst records = await guests.list();\nconst totals = await budget.totals();\nconst heads = headcounts(records);\nconst eventDate = await readEventDate();\nconst listing =\n input.action === 'guest_list'\n ? records\n .map(\n (r) =>\n `${r.id} [${r.status}] ${String(r.fields.name ?? '?')}${partyOf(r) > 1 ? ` (party of ${partyOf(r)})` : ''}`,\n )\n .join('\\n') || 'The guest list is empty.'\n : '';\n\ngezel.output({\n invited: heads.invited,\n coming: heads.coming,\n spentCents: totals.outCents,\n summary: listing || eventLine(records, totals.outCents, eventDate, Date.now()),\n});\n"
169
+ }
170
+ }
@@ -0,0 +1,6 @@
1
+ - The day goes beautifully — planned backward from {{date}}, with a day-of
2
+ runbook so complete nobody has to think.
3
+ - The guest list is always current: every RSVP recorded the day it lands,
4
+ headcounts answered from the list.
5
+ - The budget is honest: money recorded when committed, not when remembered.
6
+ - After the event, every thank-you goes out — tracked to done.
@@ -0,0 +1,186 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="gezel-demo" content="standalone" />
7
+ <title>Event planner</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ --bg: #faf7f2; --card: #fff; --ink: #2b2620; --muted: #7a7166;
12
+ --accent: #b5651d; --track: #ece5da; --good: #4a7c59; --warn: #a4552e;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
17
+ --accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
18
+ }
19
+ }
20
+ * { box-sizing: border-box; }
21
+ 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; }
22
+ main { max-width: 680px; margin: 0 auto; }
23
+ h1 { font-family: Georgia, 'PT Serif', 'Times New Roman', serif; font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
24
+ .sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 0.9rem; }
25
+ .tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6rem; margin-bottom: 0.9rem; }
26
+ .tile { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.7rem 0.85rem; }
27
+ .tile .n { font-size: 1.35rem; font-weight: 700; color: var(--accent); }
28
+ .tile .n.good { color: var(--good); }
29
+ .tile .l { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
30
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
31
+ .panel { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.9rem; }
32
+ .panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
33
+ .panel ul { margin: 0; padding-left: 1.05rem; }
34
+ .panel li { margin: 0.2rem 0; }
35
+ .rsvp { color: var(--muted); font-size: 0.78rem; }
36
+ .rsvp.yes { color: var(--good); }
37
+ .rsvp.no { color: var(--warn); }
38
+ .bar-row { display: grid; grid-template-columns: 6.5rem 1fr 4.5rem; gap: 0.5rem; align-items: center; margin: 0.3rem 0; }
39
+ .bar { background: var(--track); border-radius: 4px; height: 10px; overflow: hidden; }
40
+ .bar > span { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
41
+ .amt { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
42
+ .empty { color: var(--muted); font-style: italic; }
43
+ @media (max-width: 640px) { .cols { grid-template-columns: 1fr; } .tiles { grid-template-columns: 1fr 1fr; } }
44
+ + .demo-banner {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ gap: 0.75rem;
49
+ margin: 0 0 0.85rem;
50
+ padding: 0.5rem 0.65rem;
51
+ border: 1px solid var(--accent, #8a5a2b);
52
+ border-radius: 6px;
53
+ background: color-mix(in srgb, var(--card, var(--surface, #fffdf8)) 88%, var(--accent, #8a5a2b));
54
+ color: var(--ink, #2b2620);
55
+ font-size: 0.8rem;
56
+ }
57
+ .demo-banner[hidden] { display: none; }
58
+ .demo-banner strong { font-weight: 700; }
59
+ .demo-reset {
60
+ flex: none;
61
+ padding: 0.25rem 0.5rem;
62
+ border: 1px solid var(--accent, #8a5a2b);
63
+ border-radius: 6px;
64
+ background: var(--card, var(--surface, #fffdf8));
65
+ color: var(--ink, #2b2620);
66
+ cursor: pointer;
67
+ font: inherit;
68
+ }
69
+ .demo-reset:hover { border-color: var(--ink, #2b2620); }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <main>
74
+ + <div class="demo-banner" data-gezel-demo-banner hidden>
75
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
76
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
77
+ </div>
78
+ <h1 id="title">Event planner</h1>
79
+ <p class="sub" id="sub">Loading the plan…</p>
80
+ <div class="tiles">
81
+ <div class="tile"><div class="n" id="t-days">—</div><div class="l">Days to go</div></div>
82
+ <div class="tile"><div class="n good" id="t-coming">—</div><div class="l">Coming</div></div>
83
+ <div class="tile"><div class="n" id="t-await">—</div><div class="l">Awaiting reply</div></div>
84
+ <div class="tile"><div class="n" id="t-spent">—</div><div class="l">Spent</div></div>
85
+ </div>
86
+ <div class="cols">
87
+ <div class="panel">
88
+ <h2>Guest list</h2>
89
+ <ul id="guests"><li class="empty">No guests yet — start the list with your Ceremoniemeester.</li></ul>
90
+ </div>
91
+ <div class="panel">
92
+ <h2>Where the budget went</h2>
93
+ <div id="budget"><p class="empty">Nothing committed yet.</p></div>
94
+ </div>
95
+ </div>
96
+ </main>
97
+ <script>
98
+ (function () {
99
+ 'use strict';
100
+ var parts = location.pathname.split('/').filter(Boolean);
101
+ var typeIdx = parts.indexOf('type');
102
+ var capability = typeIdx > 0 ? parts[typeIdx - 1] : null;
103
+ var projectId = typeIdx >= 0 ? parts[typeIdx + 1] : null;
104
+ var failedPolls = 0;
105
+ var demoMode = !capability || !projectId;
106
+ var demoGuests = {
107
+ eventDate: new Date(Date.now() + 28 * 86400000).toISOString(),
108
+ records: [
109
+ { id: 'guest-1', status: 'yes', fields: { name: 'Mara & Jo', party: 2 } },
110
+ { id: 'guest-2', status: 'yes', fields: { name: 'The De Vries family', party: 4 } },
111
+ { id: 'guest-3', status: 'invited', fields: { name: 'Samir Patel', party: 1 } },
112
+ { id: 'guest-4', status: 'invited', fields: { name: 'Anika Bose', party: 2 } },
113
+ { id: 'guest-5', status: 'no', fields: { name: 'Leo Martins', party: 1 } },
114
+ ],
115
+ };
116
+ var demoBudget = { entries: [
117
+ { kind: 'out', category: 'Venue', amountCents: 185000 },
118
+ { kind: 'out', category: 'Food', amountCents: 92000 },
119
+ { kind: 'out', category: 'Flowers', amountCents: 36000 },
120
+ { kind: 'out', category: 'Music', amountCents: 54000 },
121
+ ] };
122
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
123
+
124
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
125
+ function fmt(cents) { var abs = Math.abs(cents); return (cents < 0 ? '-' : '') + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0'); }
126
+ function fetchJson(file) {
127
+ return fetch('/preview/' + encodeURIComponent(capability) + '/workspace/' + encodeURIComponent(projectId) + '/' + file, { cache: 'no-store' })
128
+ .then(function (r) { if (!r.ok) throw new Error('poll ' + r.status); return r.json(); });
129
+ }
130
+ function partyOf(r) { var p = r.fields && r.fields.party; return typeof p === 'number' && p >= 1 ? Math.floor(p) : 1; }
131
+
132
+ function render(guestsDoc, budgetDoc) {
133
+ var records = (guestsDoc && guestsDoc.records) || [];
134
+ var entries = (budgetDoc && budgetDoc.entries) || [];
135
+ var coming = 0, awaiting = 0;
136
+ records.forEach(function (r) {
137
+ if (r.status === 'yes') coming += partyOf(r);
138
+ if (r.status === 'invited') awaiting += partyOf(r);
139
+ });
140
+ var spent = 0; var byCat = {};
141
+ entries.forEach(function (e) {
142
+ if (e.kind === 'out') { spent += e.amountCents; byCat[e.category] = (byCat[e.category] || 0) + e.amountCents; }
143
+ });
144
+ var days = guestsDoc && guestsDoc.eventDate ? Math.ceil((Date.parse(guestsDoc.eventDate) - Date.now()) / 86400000) : null;
145
+
146
+ document.getElementById('sub').textContent =
147
+ (guestsDoc && guestsDoc.eventDate ? 'The day: ' + String(guestsDoc.eventDate).slice(0, 10) + ' · ' : '') +
148
+ records.length + ' invitation(s) on the list';
149
+ document.getElementById('t-days').textContent = days === null || !isFinite(days) ? '—' : Math.max(0, days);
150
+ document.getElementById('t-coming').textContent = coming;
151
+ document.getElementById('t-await').textContent = awaiting;
152
+ document.getElementById('t-spent').textContent = fmt(spent);
153
+
154
+ if (records.length) {
155
+ document.getElementById('guests').innerHTML = records.slice(0, 12).map(function (r) {
156
+ return '<li>' + esc((r.fields && r.fields.name) || r.id) + (partyOf(r) > 1 ? ' (+' + (partyOf(r) - 1) + ')' : '') +
157
+ ' <span class="rsvp ' + r.status + '">' + (r.status === 'invited' ? 'awaiting' : r.status) + '</span></li>';
158
+ }).join('') + (records.length > 12 ? '<li class="empty">and ' + (records.length - 12) + ' more…</li>' : '');
159
+ }
160
+ var cats = Object.entries(byCat).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 6);
161
+ if (cats.length) {
162
+ var max = cats[0][1];
163
+ document.getElementById('budget').innerHTML = cats.map(function (pair) {
164
+ return '<div class="bar-row"><span>' + esc(pair[0]) + '</span><span class="bar"><span style="width:' + Math.max(4, Math.round((pair[1] / max) * 100)) + '%"></span></span><span class="amt">' + fmt(pair[1]) + '</span></div>';
165
+ }).join('');
166
+ }
167
+ }
168
+
169
+ function poll() {
170
+ if (demoMode) { render(demoGuests, demoBudget); return Promise.resolve(); }
171
+ if (!capability || !projectId) return Promise.resolve();
172
+ return Promise.all([
173
+ fetchJson('guests.json').catch(function () { return { records: [] }; }),
174
+ fetchJson('budget.json').catch(function () { return { entries: [] }; }),
175
+ ])
176
+ .then(function (results) { failedPolls = 0; render(results[0], results[1]); })
177
+ .catch(function () {
178
+ failedPolls += 1;
179
+ if (failedPolls === 3 && window.parent !== window) window.parent.postMessage({ __gezelPageRefresh: true }, '*');
180
+ });
181
+ }
182
+ poll().then(function () { setInterval(function () { void poll(); }, 6000); });
183
+ })();
184
+ </script>
185
+ </body>
186
+ </html>
@@ -0,0 +1,7 @@
1
+ Training toward **{{goal}}**, {{weeklyTarget}} session(s) a week. Your Coach
2
+ logs every workout the moment you report it, celebrates the streak, and keeps
3
+ the plan sized to the life you actually have — smaller when the week is hard,
4
+ growing again when it isn't.
5
+
6
+ The **Output** tab is the training board: the streak, this week against the
7
+ target, and the recent sessions.