@bendyline/gilde 0.1.21 → 0.1.22

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 (47) hide show
  1. package/authoring/chat-models/nemotron3.5-lightning-30b-q4.json +3 -2
  2. package/authoring/chat-models/nemotron3.5-lightning-30b-q8.json +3 -2
  3. package/authoring/page-demo-stub.js +85 -0
  4. package/data/chat-models/index.json +1 -1
  5. package/data/chat-models/ne/nemotron3.5-lightning-30b-q4/manifest.json +3 -2
  6. package/data/chat-models/ne/nemotron3.5-lightning-30b-q8/manifest.json +3 -2
  7. package/data/connector-types/bl/bluesky-posts/manifest.json +16 -0
  8. package/data/connector-types/bl/bluesky-posts/versions/1.0.0/manifest.json +70 -0
  9. package/data/connector-types/gi/github-issues/versions/1.0.1/manifest.json +67 -0
  10. package/data/connector-types/in/instagram-media/manifest.json +17 -0
  11. package/data/connector-types/in/instagram-media/versions/1.0.0/manifest.json +70 -0
  12. package/data/connector-types/index.json +1 -1
  13. package/data/connector-types/x-/x-posts/manifest.json +17 -0
  14. package/data/connector-types/x-/x-posts/versions/1.0.0/manifest.json +66 -0
  15. package/data/craftbook-templates/dr/draft-social-post/manifest.json +23 -0
  16. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/craftbook.json +176 -0
  17. package/data/craftbook-templates/dr/draft-social-post/versions/1.0.0/test.json +161 -0
  18. package/data/craftbook-templates/index.json +1 -1
  19. package/data/craftbook-templates/re/reception-report/manifest.json +21 -0
  20. package/data/craftbook-templates/re/reception-report/versions/1.0.0/craftbook.json +170 -0
  21. package/data/craftbook-templates/re/reception-report/versions/1.0.0/test.json +168 -0
  22. package/data/craftbook-templates/so/social-digest/manifest.json +21 -0
  23. package/data/craftbook-templates/so/social-digest/versions/1.0.0/craftbook.json +133 -0
  24. package/data/craftbook-templates/so/social-digest/versions/1.0.0/test.json +168 -0
  25. package/data/gezel-templates/index.json +1 -1
  26. package/data/gezel-templates/om/omroeper/manifest.json +20 -0
  27. package/data/gezel-templates/om/omroeper/versions/1.0.0/about.md +33 -0
  28. package/data/gezel-templates/om/omroeper/versions/1.0.0/manifest.json +23 -0
  29. package/data/project-types/im/image-feed/manifest.json +20 -0
  30. package/data/project-types/im/image-feed/versions/1.0.0/about.md +21 -0
  31. package/data/project-types/im/image-feed/versions/1.0.0/manifest.json +330 -0
  32. package/data/project-types/im/image-feed/versions/1.0.0/mission.md +10 -0
  33. package/data/project-types/im/image-feed/versions/1.0.0/pages/gallery/index.html +831 -0
  34. package/data/project-types/im/image-feed/versions/1.0.0/posts/index.json +4 -0
  35. package/data/project-types/im/image-feed/versions/1.0.0/style-guide.md +24 -0
  36. package/data/project-types/index.json +1 -1
  37. package/data/project-types/so/social-feed/manifest.json +20 -0
  38. package/data/project-types/so/social-feed/versions/1.0.0/about.md +19 -0
  39. package/data/project-types/so/social-feed/versions/1.0.0/manifest.json +330 -0
  40. package/data/project-types/so/social-feed/versions/1.0.0/mission.md +10 -0
  41. package/data/project-types/so/social-feed/versions/1.0.0/pages/dashboard/index.html +754 -0
  42. package/data/project-types/so/social-feed/versions/1.0.0/posts/index.json +4 -0
  43. package/data/project-types/so/social-feed/versions/1.0.0/voice-guide.md +22 -0
  44. package/package.json +1 -1
  45. package/schemas/connector-type-version.schema.json +6 -0
  46. package/schemas/craftbook-test.schema.json +1 -0
  47. package/schemas/project-type-version.schema.json +4 -0
@@ -0,0 +1,754 @@
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>Social feed pipeline</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 2rem;
49
+ }
50
+ main { max-width: 1080px; margin: 0 auto; }
51
+ h1 {
52
+ font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
53
+ font-size: 1.4rem;
54
+ font-weight: 600;
55
+ margin: 0 0 0.15rem;
56
+ overflow-wrap: anywhere;
57
+ }
58
+ .head { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; }
59
+ .sub { color: var(--muted); margin: 0 0 1rem; font-size: 0.9rem; }
60
+ .hint { color: var(--muted); font-size: 0.8rem; margin: 0 0 0.9rem; }
61
+ .key {
62
+ font: inherit;
63
+ font-size: 0.8rem;
64
+ padding: 0.25rem 0.55rem;
65
+ border: 1px solid var(--track);
66
+ border-radius: 6px;
67
+ background: var(--card);
68
+ color: var(--ink);
69
+ cursor: pointer;
70
+ }
71
+ .key:hover { border-color: var(--muted); }
72
+ .key[aria-pressed='true'] {
73
+ border-color: var(--accent);
74
+ color: var(--accent);
75
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12);
76
+ }
77
+ .note {
78
+ margin: 0 0 0.85rem;
79
+ padding: 0.5rem 0.65rem;
80
+ border: 1px solid var(--track);
81
+ border-radius: 6px;
82
+ background: var(--card);
83
+ color: var(--muted);
84
+ font-size: 0.8rem;
85
+ }
86
+ .board-wrap { overflow-x: auto; padding-bottom: 0.25rem; }
87
+ .board { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(168px, 1fr); gap: 0.6rem; min-width: 900px; }
88
+ .col { min-width: 0; }
89
+ .col h2 {
90
+ font-size: 0.72rem;
91
+ text-transform: uppercase;
92
+ letter-spacing: 0.05em;
93
+ color: var(--muted);
94
+ margin: 0 0 0.4rem;
95
+ font-weight: 600;
96
+ display: flex;
97
+ justify-content: space-between;
98
+ align-items: baseline;
99
+ }
100
+ .col h2 .count { color: var(--ink); }
101
+ .card {
102
+ display: block;
103
+ width: 100%;
104
+ text-align: left;
105
+ font: inherit;
106
+ color: var(--ink);
107
+ background: var(--card);
108
+ border: 1px solid var(--track);
109
+ border-radius: 6px;
110
+ padding: 0.5rem 0.6rem;
111
+ margin-bottom: 0.45rem;
112
+ font-size: 0.85rem;
113
+ cursor: pointer;
114
+ }
115
+ .card:hover { border-color: var(--muted); }
116
+ .card .ti { display: block; font-weight: 600; overflow-wrap: anywhere; }
117
+ .card .ft { margin-top: 0.3rem; display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center; color: var(--muted); font-size: 0.72rem; }
118
+ .chip {
119
+ display: inline-block;
120
+ border: 1px solid var(--track);
121
+ border-radius: 4px;
122
+ padding: 0 0.3rem;
123
+ font-size: 0.7rem;
124
+ line-height: 1.4;
125
+ color: var(--muted);
126
+ }
127
+ .chip.tag { border-style: dashed; }
128
+ .chip.st-draft, .chip.st-scheduled { color: var(--accent); border-color: var(--accent); }
129
+ .chip.st-in-review { color: var(--warn); border-color: var(--warn); }
130
+ .chip.st-approved, .chip.st-published { color: var(--good); border-color: var(--good); }
131
+ .board-empty {
132
+ background: var(--card);
133
+ border: 1px dashed var(--track);
134
+ border-radius: 10px;
135
+ padding: 0.85rem 1.25rem;
136
+ text-align: center;
137
+ color: var(--muted);
138
+ margin-bottom: 0.75rem;
139
+ }
140
+ .foot { color: var(--muted); font-size: 0.72rem; margin-top: 1rem; }
141
+ .drawer {
142
+ position: fixed;
143
+ top: 0;
144
+ right: 0;
145
+ bottom: 0;
146
+ width: min(430px, 94vw);
147
+ background: var(--card);
148
+ border-left: 1px solid var(--track);
149
+ box-shadow: -6px 0 18px rgba(0, 0, 0, 0.08);
150
+ padding: 1rem 1.1rem 1.5rem;
151
+ overflow-y: auto;
152
+ z-index: 10;
153
+ }
154
+ .drawer[hidden] { display: none; }
155
+ .drawer .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.5rem; }
156
+ .drawer h2 {
157
+ font-family: Georgia, 'PT Serif', 'Times New Roman', serif;
158
+ font-size: 1.1rem;
159
+ font-weight: 600;
160
+ margin: 0 0 0.2rem;
161
+ overflow-wrap: anywhere;
162
+ }
163
+ .drawer h3 {
164
+ font-size: 0.72rem;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.05em;
167
+ color: var(--muted);
168
+ margin: 1rem 0 0.35rem;
169
+ font-weight: 600;
170
+ }
171
+ .drawer .meta { color: var(--muted); font-size: 0.8rem; }
172
+ .drawer .body-text { white-space: pre-wrap; font-size: 0.88rem; overflow-wrap: anywhere; }
173
+ .drawer .moves { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-top: 0.35rem; }
174
+ .drawer .error { color: var(--warn); font-size: 0.8rem; margin: 0.4rem 0 0; }
175
+ .variant {
176
+ border: 1px solid var(--track);
177
+ border-radius: 6px;
178
+ padding: 0.45rem 0.6rem;
179
+ margin-bottom: 0.45rem;
180
+ font-size: 0.83rem;
181
+ }
182
+ .variant .vh { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.25rem; }
183
+ .variant .vh .name { font-weight: 600; }
184
+ .variant .vb { white-space: pre-wrap; color: var(--ink); overflow-wrap: anywhere; }
185
+ .variant a { color: var(--accent); }
186
+ .empty { color: var(--muted); font-style: italic; }
187
+ .demo-banner {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ gap: 0.75rem;
192
+ margin: 0 0 0.85rem;
193
+ padding: 0.5rem 0.65rem;
194
+ border: 1px solid var(--accent);
195
+ border-radius: 6px;
196
+ background: color-mix(in srgb, var(--card) 88%, var(--accent));
197
+ color: var(--ink);
198
+ font-size: 0.8rem;
199
+ }
200
+ .demo-banner[hidden] { display: none; }
201
+ .demo-banner strong { font-weight: 700; }
202
+ .demo-reset {
203
+ flex: none;
204
+ padding: 0.25rem 0.5rem;
205
+ border: 1px solid var(--accent);
206
+ border-radius: 6px;
207
+ background: var(--card);
208
+ color: var(--ink);
209
+ cursor: pointer;
210
+ font: inherit;
211
+ }
212
+ .demo-reset:hover { border-color: var(--ink); }
213
+ @media (max-width: 860px) {
214
+ .board { grid-auto-flow: row; grid-auto-columns: unset; grid-template-columns: 1fr; min-width: 0; }
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <main>
220
+ <div class="demo-banner" data-gezel-demo-banner hidden>
221
+ <span><strong>Demo mode</strong> — sample data stays in this page.</span>
222
+ <button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
223
+ </div>
224
+ <p class="note" id="browser-note" hidden>
225
+ Viewing from a browser tab — the board stays live, but moving posts happens in the app's Output pane.
226
+ </p>
227
+ <div class="head">
228
+ <div>
229
+ <h1 id="title">Social feed</h1>
230
+ <p class="sub" id="sub">Loading the pipeline…</p>
231
+ </div>
232
+ <button type="button" class="key" id="toggle-archived" aria-pressed="false">Show archived</button>
233
+ </div>
234
+ <p class="hint" id="corpus-hint" hidden>
235
+ Connected feeds are syncing — reception reports read from the project's artifacts.
236
+ </p>
237
+ <div class="board-empty" id="board-empty" hidden>
238
+ No posts yet — ask your Omroeper to draft the first one.
239
+ </div>
240
+ <div class="board-wrap"><div class="board" id="board"></div></div>
241
+ <p class="foot" id="mode-line"></p>
242
+ </main>
243
+ <aside class="drawer" id="drawer" hidden>
244
+ <div class="top">
245
+ <div>
246
+ <h2 id="d-title"></h2>
247
+ <p class="meta" id="d-meta"></p>
248
+ </div>
249
+ <button type="button" class="key" id="d-close">Close</button>
250
+ </div>
251
+ <h3>Move to</h3>
252
+ <div class="moves" id="d-moves"></div>
253
+ <p class="error" id="d-error" hidden></p>
254
+ <h3>Post</h3>
255
+ <div class="body-text" id="d-body"></div>
256
+ <h3>Variants</h3>
257
+ <div id="d-variants"></div>
258
+ <h3>Media</h3>
259
+ <p class="meta" id="d-media"></p>
260
+ </aside>
261
+ <script>
262
+ /* gezel-page-demo-stub v1 */
263
+ /*
264
+ * The canonical demo stub for Gezel type pages. MIT License.
265
+ *
266
+ * Paste this file's bytes verbatim into a <script> block ahead of the
267
+ * page's own code, then call makeDemoGezel({ page, tools, data }) with the
268
+ * page's demo handlers. Served by Gezel, the real window.gezel is already
269
+ * injected and the stub steps aside; opened as a raw file (the live demos
270
+ * on gezelgilde.com), it installs a window.gezel whose tools and data are
271
+ * the page-supplied handler maps — one code path in every mode. `tools`
272
+ * and `data` map a tool name / declared read path to a function; data
273
+ * handlers back read() and list(), and url() returns the handler's value
274
+ * or passes a data: URI through. See docs/page-authoring.md.
275
+ */
276
+ (function () {
277
+ 'use strict';
278
+ window.makeDemoGezel = function (opts) {
279
+ if (window.gezel) return window.gezel;
280
+ var page = (opts && opts.page) || {};
281
+ var tools = (opts && opts.tools) || {};
282
+ var data = (opts && opts.data) || {};
283
+ var themeListeners = [];
284
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
285
+ var ui = {
286
+ theme: { mode: media && media.matches ? 'dark' : 'light' },
287
+ onTheme: function (cb) {
288
+ themeListeners.push(cb);
289
+ return function () {
290
+ var at = themeListeners.indexOf(cb);
291
+ if (at >= 0) themeListeners.splice(at, 1);
292
+ };
293
+ },
294
+ };
295
+ if (media && media.addEventListener) {
296
+ media.addEventListener('change', function (ev) {
297
+ ui.theme = { mode: ev.matches ? 'dark' : 'light' };
298
+ themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
299
+ });
300
+ }
301
+ function fail(code, message) {
302
+ var err = new Error(message);
303
+ err.code = code;
304
+ return Promise.reject(err);
305
+ }
306
+ function run(map, key, arg) {
307
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
308
+ return Promise.resolve().then(function () { return map[key](arg); });
309
+ }
310
+ window.gezel = {
311
+ page: {
312
+ api: 1,
313
+ projectId: 'demo',
314
+ source: 'type',
315
+ entry: page.entry || 'index.html',
316
+ typeName: page.typeName || 'demo',
317
+ params: page.params || {},
318
+ mode: 'demo',
319
+ },
320
+ tools: {
321
+ list: function () { return Promise.resolve(Object.keys(tools)); },
322
+ invoke: function (name, input) {
323
+ var call = run(tools, name, input || {});
324
+ if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
325
+ return call.then(function (output) { return { output: output }; });
326
+ },
327
+ },
328
+ data: {
329
+ read: function (path, o) {
330
+ return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
331
+ },
332
+ list: function (path, o) {
333
+ return run(data, path, o || {}) || Promise.resolve([]);
334
+ },
335
+ watch: function () { return function () {}; },
336
+ url: function (path) {
337
+ if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
338
+ return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
339
+ },
340
+ },
341
+ ui: ui,
342
+ refresh: function () {},
343
+ };
344
+ return window.gezel;
345
+ };
346
+ })();
347
+ </script>
348
+ <script>
349
+ (function () {
350
+ 'use strict';
351
+ var STAGES = ['idea', 'draft', 'in-review', 'approved', 'scheduled', 'published'];
352
+ var DAY = 24 * 60 * 60 * 1000;
353
+
354
+ function legalNext(from) {
355
+ if (from === 'archived') return [];
356
+ var at = STAGES.indexOf(from);
357
+ var next = at >= 0 && at < STAGES.length - 1 ? [STAGES[at + 1]] : [];
358
+ return next.concat(['archived']);
359
+ }
360
+
361
+ // ── demo dataset (used only when the page opens as a raw file) ──
362
+ function demoPost(daysAgo, slug, title, status, platforms, extra) {
363
+ var created = new Date(Date.now() - daysAgo * DAY).toISOString().slice(0, 10);
364
+ var post = {
365
+ id: created + '-' + slug,
366
+ title: title,
367
+ status: status,
368
+ platforms: platforms,
369
+ created: created,
370
+ body: extra.body || 'A note from the workshop.',
371
+ variants: extra.variants || {},
372
+ media: extra.media || [],
373
+ };
374
+ if (extra.scheduledFor) post.scheduledFor = extra.scheduledFor;
375
+ if (extra.campaign) post.campaign = extra.campaign;
376
+ if (extra.tags) post.tags = extra.tags;
377
+ return post;
378
+ }
379
+ var demoPosts = [
380
+ demoPost(0, 'field-notes-from-the-bindery', 'Field notes from the bindery', 'idea', ['bluesky', 'x'], {
381
+ body: 'Rough notion: a short thread on what the bindery bench looks like on a Tuesday. Honest tools, honest mess.',
382
+ }),
383
+ demoPost(1, 'introducing-the-autumn-catalog', 'Introducing the autumn catalog', 'draft', ['bluesky', 'x'], {
384
+ campaign: 'autumn-launch',
385
+ body: 'The autumn catalog is nearly sewn. This post introduces the three new editions and the paper they live on.',
386
+ variants: {
387
+ bluesky: { charCount: 182, body: 'The autumn catalog is at the bench: three new editions, one very stubborn marbled endpaper. First look on Thursday.' },
388
+ },
389
+ }),
390
+ demoPost(2, 'how-we-choose-paper', 'How we choose paper', 'in-review', ['bluesky'], {
391
+ tags: ['craft', 'paper'],
392
+ body: 'A short piece on choosing paper by ear as much as by hand — what a good sheet sounds like when it turns.',
393
+ variants: {
394
+ bluesky: { charCount: 236, body: 'You can hear a good sheet before you feel it. A note on choosing paper by ear, from the person who turns every page twice.' },
395
+ },
396
+ }),
397
+ demoPost(4, 'meet-the-press-vandercook-4', 'Meet the press: Vandercook No. 4', 'approved', ['bluesky', 'x'], {
398
+ tags: ['workshop'],
399
+ body: 'Portrait of the shop’s Vandercook No. 4 — sixty years old, still square, still the fussiest colleague we have.',
400
+ variants: {
401
+ bluesky: { charCount: 204, body: 'Meet the Vandercook No. 4. Sixty years old, perfectly square, entirely unimpressed by deadlines. The shop runs on her mood.' },
402
+ x: { charCount: 168, body: 'Sixty years old, perfectly square, unimpressed by deadlines. Meet the press the whole shop answers to.' },
403
+ },
404
+ }),
405
+ demoPost(6, 'autumn-catalog-first-look', 'Autumn catalog — first look', 'scheduled', ['bluesky', 'x'], {
406
+ campaign: 'autumn-launch',
407
+ scheduledFor: new Date(Date.now() + 2 * DAY).toISOString().slice(0, 10),
408
+ body: 'The first spread from the autumn catalog, photographed on the bench it was made at.',
409
+ variants: {
410
+ bluesky: { charCount: 148, body: 'First look at the autumn catalog — three editions, sewn by hand, photographed where they were made.' },
411
+ x: { charCount: 121, body: 'Autumn catalog, first look. Three editions, sewn by hand. Thursday.' },
412
+ },
413
+ }),
414
+ demoPost(9, 'a-morning-at-the-bench', 'A morning at the bench', 'published', ['bluesky', 'x'], {
415
+ body: 'What 7am looks like in the shop: coffee, bone folders, and the day’s first imposition.',
416
+ variants: {
417
+ bluesky: { charCount: 158, publishedAt: new Date(Date.now() - 8 * DAY).toISOString(), permalink: 'https://bsky.app/profile/demo/post/3kexample', body: '7am in the shop: coffee going cold, bone folders warming up. The day’s first imposition, in pictures.' },
418
+ x: { charCount: 132, publishedAt: new Date(Date.now() - 8 * DAY).toISOString(), permalink: 'https://x.com/demo/status/1234567890', body: '7am: coffee cold, bone folders warm, first imposition on the stone.' },
419
+ },
420
+ }),
421
+ demoPost(20, 'old-workshop-hours', 'Workshop hours update (superseded)', 'archived', ['bluesky'], {
422
+ body: 'Superseded hours announcement, kept for the record.',
423
+ }),
424
+ ];
425
+ function demoFind(id) {
426
+ for (var i = 0; i < demoPosts.length; i += 1) {
427
+ if (demoPosts[i].id === id) return demoPosts[i];
428
+ }
429
+ return null;
430
+ }
431
+ function demoIndex() {
432
+ return {
433
+ version: 1,
434
+ posts: demoPosts.map(function (post) {
435
+ var entry = {
436
+ id: post.id,
437
+ title: post.title,
438
+ status: post.status,
439
+ platforms: post.platforms,
440
+ created: post.created,
441
+ };
442
+ if (post.scheduledFor) entry.scheduledFor = post.scheduledFor;
443
+ if (post.campaign) entry.campaign = post.campaign;
444
+ return entry;
445
+ }),
446
+ };
447
+ }
448
+
449
+ makeDemoGezel({
450
+ page: {
451
+ entry: 'dashboard/index.html',
452
+ typeName: 'Social Feed',
453
+ params: {
454
+ brand: 'Juniper Press',
455
+ cadence: '3 posts a week',
456
+ voice: 'Warm, plain, and a little inky.',
457
+ },
458
+ },
459
+ tools: {
460
+ page_set_status: function (input) {
461
+ var post = demoFind(input && input.id);
462
+ if (!post) throw new Error("No post '" + (input && input.id) + "' in the demo board.");
463
+ var to = input.status;
464
+ if (post.status !== to && legalNext(post.status).indexOf(to) < 0) {
465
+ var next = legalNext(post.status);
466
+ throw new Error(
467
+ "Cannot move '" + post.id + "' from " + post.status + ' to ' + to +
468
+ ' — posts move one stage at a time.' +
469
+ (next.length ? ' Legal next: ' + next.join(', ') + '.' : " '" + post.status + "' is final.")
470
+ );
471
+ }
472
+ post.status = to;
473
+ return { id: post.id, status: post.status, summary: 'Moved.' };
474
+ },
475
+ page_get_post: function (input) {
476
+ var post = demoFind(input && input.id);
477
+ if (!post) throw new Error("No post '" + (input && input.id) + "' in the demo board.");
478
+ return { id: post.id, status: post.status, post: post, summary: post.title };
479
+ },
480
+ },
481
+ data: {
482
+ 'posts/index.json': function () { return demoIndex(); },
483
+ data: function () {
484
+ return [{ name: 'bluesky-posts', kind: 'dir', size: 0, mtime: new Date().toISOString() }];
485
+ },
486
+ },
487
+ });
488
+
489
+ // ── one code path from here: window.gezel exists in every mode ──
490
+ var demoMode = gezel.page.mode === 'demo';
491
+ var browserMode = gezel.page.mode === 'browser';
492
+ var params = gezel.page.params || {};
493
+ var showArchived = false;
494
+ var openPostId = null;
495
+ var lastIndex = { posts: [] };
496
+
497
+ function applyTheme(mode) {
498
+ document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
499
+ }
500
+ applyTheme(gezel.ui.theme.mode);
501
+ gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
502
+
503
+ if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
504
+ if (browserMode) document.getElementById('browser-note').hidden = false;
505
+
506
+ function esc(value) {
507
+ return String(value).replace(/[&<>"]/g, function (ch) {
508
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ch];
509
+ });
510
+ }
511
+ function el(id) { return document.getElementById(id); }
512
+
513
+ var brand = typeof params.brand === 'string' && params.brand ? params.brand : gezel.page.typeName;
514
+ el('title').textContent = brand;
515
+ el('mode-line').textContent = 'mode: ' + gezel.page.mode;
516
+ document.title = brand + ' — social feed pipeline';
517
+
518
+ function parseFront(text) {
519
+ var lines = String(text).split(/\r?\n/);
520
+ if ((lines[0] || '').trim() !== '---') return { fields: {}, body: String(text) };
521
+ var fields = {};
522
+ for (var i = 1; i < lines.length; i += 1) {
523
+ if (lines[i].trim() === '---') {
524
+ return { fields: fields, body: lines.slice(i + 1).join('\n').replace(/^\n/, '') };
525
+ }
526
+ var sep = lines[i].indexOf(':');
527
+ if (sep <= 0) continue;
528
+ var key = lines[i].slice(0, sep).trim();
529
+ var raw = lines[i].slice(sep + 1).trim();
530
+ if (raw.charAt(0) === '[' && raw.charAt(raw.length - 1) === ']') {
531
+ fields[key] = raw.slice(1, -1).split(',').map(function (part) { return part.trim(); }).filter(Boolean);
532
+ } else {
533
+ fields[key] = raw;
534
+ }
535
+ }
536
+ return { fields: {}, body: String(text) };
537
+ }
538
+
539
+ function renderBoard(index) {
540
+ lastIndex = index && Array.isArray(index.posts) ? index : { posts: [] };
541
+ var posts = lastIndex.posts;
542
+ var live = posts.filter(function (post) { return post.status !== 'archived'; });
543
+ var published = posts.filter(function (post) { return post.status === 'published'; }).length;
544
+ var cadence = typeof params.cadence === 'string' && params.cadence ? params.cadence : '';
545
+ el('sub').textContent =
546
+ (cadence ? 'Aiming for ' + cadence + ' · ' : '') +
547
+ live.length + ' post' + (live.length === 1 ? '' : 's') + ' in flight · ' + published + ' published';
548
+ el('board-empty').hidden = posts.length > 0;
549
+
550
+ var columns = showArchived ? STAGES.concat(['archived']) : STAGES;
551
+ el('board').innerHTML = columns
552
+ .map(function (stage) {
553
+ var here = posts.filter(function (post) { return post.status === stage; });
554
+ var cards = here
555
+ .map(function (post) {
556
+ var chips = (post.platforms || [])
557
+ .map(function (platform) { return '<span class="chip">' + esc(platform) + '</span>'; })
558
+ .join('');
559
+ if (post.campaign) chips += '<span class="chip tag">' + esc(post.campaign) + '</span>';
560
+ var when = post.status === 'scheduled' && post.scheduledFor ? 'for ' + esc(String(post.scheduledFor).slice(0, 10)) : esc(String(post.created || '').slice(0, 10));
561
+ return (
562
+ '<button type="button" class="card" data-post="' + esc(post.id) + '">' +
563
+ '<span class="ti">' + esc(post.title || post.id) + '</span>' +
564
+ '<span class="ft">' + chips + '<span>' + when + '</span></span>' +
565
+ '</button>'
566
+ );
567
+ })
568
+ .join('');
569
+ return (
570
+ '<div class="col"><h2><span>' + esc(stage) + '</span><span class="count">' + here.length + '</span></h2>' +
571
+ cards +
572
+ '</div>'
573
+ );
574
+ })
575
+ .join('');
576
+ }
577
+
578
+ function loadBoard() {
579
+ return gezel.data
580
+ .read('posts/index.json')
581
+ .then(function (index) { renderBoard(index); })
582
+ .catch(function () { renderBoard({ posts: [] }); });
583
+ }
584
+
585
+ function setDrawerError(message) {
586
+ var node = el('d-error');
587
+ node.textContent = message || '';
588
+ node.hidden = !message;
589
+ }
590
+
591
+ function renderDrawer(post) {
592
+ el('drawer').hidden = false;
593
+ el('d-title').textContent = post.title || post.id;
594
+ var meta = [post.status, 'created ' + String(post.created || '').slice(0, 10)];
595
+ if (post.scheduledFor) meta.push('scheduled for ' + String(post.scheduledFor).slice(0, 10));
596
+ if (post.campaign) meta.push('campaign: ' + post.campaign);
597
+ el('d-meta').textContent = meta.join(' · ');
598
+ el('d-body').textContent = post.body || '(no body yet)';
599
+
600
+ var moves = legalNext(post.status);
601
+ el('d-moves').innerHTML =
602
+ moves.length === 0
603
+ ? '<span class="empty">' + esc(post.status) + ' is the end of the line.</span>'
604
+ : moves
605
+ .map(function (stage) {
606
+ return '<button type="button" class="key" data-move="' + esc(stage) + '">' + esc(stage) + '</button>';
607
+ })
608
+ .join('');
609
+ if (browserMode && moves.length > 0) {
610
+ el('d-moves').innerHTML = '<span class="empty">Open the Output pane in the app to move this post.</span>';
611
+ }
612
+
613
+ var variants = post.variants || {};
614
+ var names = Object.keys(variants);
615
+ el('d-variants').innerHTML =
616
+ names.length === 0
617
+ ? '<p class="meta empty">No platform variants drafted yet.</p>'
618
+ : names
619
+ .map(function (name) {
620
+ var variant = variants[name] || {};
621
+ var head = '<span class="name">' + esc(name) + '</span>' +
622
+ '<span class="chip">' + esc(String(variant.charCount || 0)) + ' chars</span>';
623
+ if (variant.publishedAt) head += '<span class="chip st-published">live</span>';
624
+ if (variant.permalink) {
625
+ head += '<a href="' + esc(variant.permalink) + '" rel="noreferrer">permalink</a>';
626
+ }
627
+ return '<div class="variant"><div class="vh">' + head + '</div>' +
628
+ '<div class="vb">' + esc(variant.body || '') + '</div></div>';
629
+ })
630
+ .join('');
631
+
632
+ var media = post.media || [];
633
+ el('d-media').textContent = media.length === 0 ? 'No media attached.' : media.join(', ');
634
+ }
635
+
636
+ function fallbackDetail(id) {
637
+ return gezel.data.read('posts/' + id + '/post.md').then(function (raw) {
638
+ var parsed = parseFront(raw);
639
+ var post = { id: id };
640
+ Object.keys(parsed.fields).forEach(function (key) { post[key] = parsed.fields[key]; });
641
+ post.status = String(parsed.fields.status || 'idea');
642
+ post.platforms = [].concat(parsed.fields.platforms || []);
643
+ post.body = parsed.body.trim();
644
+ post.variants = {};
645
+ post.media = [];
646
+ return gezel.data
647
+ .list('posts/' + id + '/variants')
648
+ .catch(function () { return []; })
649
+ .then(function (entries) {
650
+ var files = entries.filter(function (entry) { return entry.kind !== 'dir' && /\.md$/.test(entry.name); });
651
+ return Promise.all(
652
+ files.map(function (entry) {
653
+ return gezel.data
654
+ .read('posts/' + id + '/variants/' + entry.name)
655
+ .then(function (variantRaw) {
656
+ var variantDoc = parseFront(variantRaw);
657
+ var body = variantDoc.body.trim();
658
+ var variant = { charCount: Number(variantDoc.fields.charCount) || body.length, body: body };
659
+ ['publishedAt', 'permalink', 'postId', 'corpusRef'].forEach(function (key) {
660
+ if (variantDoc.fields[key]) variant[key] = String(variantDoc.fields[key]);
661
+ });
662
+ post.variants[entry.name.replace(/\.md$/, '')] = variant;
663
+ })
664
+ .catch(function () {});
665
+ }),
666
+ );
667
+ })
668
+ .then(function () {
669
+ return gezel.data.list('posts/' + id + '/media').catch(function () { return []; });
670
+ })
671
+ .then(function (entries) {
672
+ post.media = entries
673
+ .filter(function (entry) { return entry.kind !== 'dir'; })
674
+ .map(function (entry) { return entry.name; });
675
+ return post;
676
+ });
677
+ });
678
+ }
679
+
680
+ function openPost(id) {
681
+ openPostId = id;
682
+ setDrawerError('');
683
+ gezel.tools
684
+ .invoke('page_get_post', { id: id })
685
+ .then(function (result) {
686
+ if (openPostId === id) renderDrawer(result.output.post || result.output);
687
+ })
688
+ .catch(function (err) {
689
+ if (err && err.code === 'unavailable') {
690
+ fallbackDetail(id)
691
+ .then(function (post) { if (openPostId === id) renderDrawer(post); })
692
+ .catch(function () {
693
+ if (openPostId !== id) return;
694
+ el('drawer').hidden = false;
695
+ el('d-title').textContent = id;
696
+ setDrawerError('Could not load this post right now.');
697
+ });
698
+ return;
699
+ }
700
+ el('drawer').hidden = false;
701
+ el('d-title').textContent = id;
702
+ setDrawerError(err && err.message ? err.message : 'Could not load this post right now.');
703
+ });
704
+ }
705
+
706
+ function closeDrawer() {
707
+ openPostId = null;
708
+ el('drawer').hidden = true;
709
+ }
710
+
711
+ function movePost(id, status) {
712
+ setDrawerError('');
713
+ gezel.tools
714
+ .invoke('page_set_status', { id: id, status: status })
715
+ .then(function () {
716
+ loadBoard();
717
+ if (openPostId === id) openPost(id);
718
+ })
719
+ .catch(function (err) {
720
+ setDrawerError(err && err.message ? err.message : 'The move was refused.');
721
+ });
722
+ }
723
+
724
+ el('board').addEventListener('click', function (event) {
725
+ var card = event.target.closest('[data-post]');
726
+ if (card) openPost(card.getAttribute('data-post'));
727
+ });
728
+ el('d-moves').addEventListener('click', function (event) {
729
+ var key = event.target.closest('[data-move]');
730
+ if (key && openPostId) movePost(openPostId, key.getAttribute('data-move'));
731
+ });
732
+ el('d-close').addEventListener('click', closeDrawer);
733
+ document.addEventListener('keydown', function (event) {
734
+ if (event.key === 'Escape') closeDrawer();
735
+ });
736
+ el('toggle-archived').addEventListener('click', function () {
737
+ showArchived = !showArchived;
738
+ this.setAttribute('aria-pressed', showArchived ? 'true' : 'false');
739
+ renderBoard(lastIndex);
740
+ });
741
+
742
+ gezel.data
743
+ .list('data', { source: 'artifacts' })
744
+ .then(function (entries) {
745
+ if (entries && entries.length > 0) el('corpus-hint').hidden = false;
746
+ })
747
+ .catch(function () {});
748
+
749
+ loadBoard();
750
+ gezel.data.watch('posts/index.json', function () { loadBoard(); });
751
+ })();
752
+ </script>
753
+ </body>
754
+ </html>