@jtakeit/kit 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jtakeit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -11,8 +11,8 @@ takes the same pieces instead of reproducing them. The scaffold itself
11
11
  imports this package; there is one implementation of each piece and two
12
12
  homes for it.
13
13
 
14
- What the platform holds a repository to is in
15
- [connecting a site](https://jtakeit.com/docs/guides/connecting-a-site), under
14
+ What the platform holds a repository to is in its guide *connecting a site*
15
+ (`jtakeit:///guides/connecting-a-site.md`, through the MCP server), under
16
16
  *On another generator*. This package is the pieces of that list that are
17
17
  worth not writing twice.
18
18
 
@@ -21,7 +21,7 @@ worth not writing twice.
21
21
  The platform's diary, on the page: the free times fetched, a stay of several
22
22
  nights, a class of cars, a party, the words in the business's kind, the
23
23
  payment step, Turnstile when the host has it. The contract it keeps is
24
- [bookings on the site](https://jtakeit.com/docs/guides/bookings-on-the-site);
24
+ the platform's *bookings on the site* (`jtakeit:///guides/bookings-on-the-site.md`);
25
25
  `preflight` reads `data-sends` off the built page and books in exactly that
26
26
  shape, so the form says what it sends and the element sends nothing else.
27
27
 
@@ -96,7 +96,7 @@ platform answers with a page — and the element only spares a working browser
96
96
  the navigation. What the build's lint holds it to: a `name` control, one of
97
97
  `contact`, `phone` or `email`, the honeypot `website`, and nothing else that
98
98
  the collecting block does not declare in `asks`
99
- ([the contract](https://jtakeit.com/docs/reference/contract.md)).
99
+ (the platform's contract, `jtakeit:///reference/contract.md`).
100
100
 
101
101
  ```js
102
102
  import { renderLeadForm } from '@jtakeit/kit/lead';
package/bin/jtkit.mjs CHANGED
@@ -84,7 +84,7 @@ switch (command) {
84
84
  case 'progress': {
85
85
  // The same reading the scaffold's dev server serves the panel as
86
86
  // /_jtk/progress.json — what the owner sees as missing is what this
87
- // prints (jtakeit-core, wiki/73).
87
+ // prints.
88
88
  const { readProgress, sayProgress } = await import(pathToFileURL(join(HERE, '..', 'lib', 'progress.mjs')).href);
89
89
  const progress = await readProgress(resolve(flag('root', '.')));
90
90
  console.log(has('json') ? JSON.stringify(progress, null, 2) : sayProgress(progress));
@@ -93,7 +93,7 @@ switch (command) {
93
93
  case 'lint': {
94
94
  // The annotation lint the platform's build runs, on this working tree:
95
95
  // `dist/` against `jtk/` and the catalogue, every finding at once,
96
- // before any push (core wiki/71 · §3.2).
96
+ // before any push.
97
97
  const { lintCommand } = await import(pathToFileURL(join(HERE, '..', 'lib', 'lint.mjs')).href);
98
98
  const root = resolve(flag('root', process.cwd()));
99
99
  const dist = resolve(root, flag('dist', 'dist'));
@@ -117,7 +117,8 @@ switch (command) {
117
117
  jtkit apply --kit <kind>|<file> [--locale uk] [--api <url>] [--root <dir>]
118
118
  jtkit catalogue [--declaration src/content/blocks.ts] [--emit-only] [--dist ./dist] [--judge]
119
119
 
120
- The contract the platform holds a repository to: https://jtakeit.com/docs/guides/connecting-a-site`);
120
+ The contract the platform holds a repository to: jtakeit:///guides/connecting-a-site.md
121
+ (the platform's MCP server, https://mcp.jtakeit.com/mcp, hands it to your agent)`);
121
122
  process.exit(command === undefined || command === '--help' || command === '-h' ? 0 : 1);
122
123
  }
123
124
  }
@@ -132,8 +132,8 @@ export function lasting(minutes, words) {
132
132
  * @param {HTMLFormElement} form
133
133
  */
134
134
  /**
135
- * The booking protocol's version this element speaks, named on every request
136
- * (the platform's wiki/79). A page is a file in a build: when the platform
135
+ * The booking protocol's version this element speaks, named on every request.
136
+ * A page is a file in a build: when the platform
137
137
  * changes, a site keeps the kit it was built with, and the platform answers it
138
138
  * by this number for as long as a live site speaks it — so it moves only with
139
139
  * a change the platform cannot make by adding, and the platform says which
@@ -181,7 +181,7 @@ export function wireBooking(form) {
181
181
  const chosenBoxes = boxes.filter((b) => b.checked);
182
182
  if (chosenBoxes.length < 2) { total.hidden = true; return; }
183
183
  const minutes = chosenBoxes.reduce((sum, b) => sum + Number(b.dataset.takes ?? 0), 0);
184
- // A price per person is times the party (wiki/70); the slot's price is
184
+ // A price per person is times the party; the slot's price is
185
185
  // the platform's, this is the list's own running sum.
186
186
  const people = partyField.hidden ? 1 : Number(party.value) || 1;
187
187
  const minor = chosenBoxes.reduce((sum, b) => sum + Number(b.dataset.costs ?? 0) * (b.dataset.per === 'person' ? people : 1), 0);
@@ -371,7 +371,7 @@ export function wireBooking(form) {
371
371
  }
372
372
 
373
373
  /**
374
- * How many of the class one request may take (wiki/69): the settings'
374
+ * How many of the class one request may take: the settings'
375
375
  * most, and no more than are free at the chosen time. Hidden at one, and
376
376
  * when a resource is named — a name is one thing.
377
377
  * @param {number} most
@@ -757,7 +757,7 @@ export function wireBooking(form) {
757
757
  }
758
758
  hint.textContent = '';
759
759
  // A price beside each time only where the times differ in price — a
760
- // rate by the hour (wiki/68). Where every time costs the same, the
760
+ // rate by the hour. Where every time costs the same, the
761
761
  // price is the services' and is said beside them: «09:00 · CHF 95» on
762
762
  // thirty pills is one number read thirty times (11 October 2026).
763
763
  const slotPrices = new Set(data.slots.map((one) => one.costs_minor ?? 0));
@@ -773,7 +773,7 @@ export function wireBooking(form) {
773
773
  ? slot.ends ? `${dayAndHour(slot.start, false)} — ${dayAndHour(slot.ends, false)}` : dayAndHour(slot.start, false)
774
774
  : clock(slot.start);
775
775
  // What the booking starting here costs — the platform's number, every
776
- // hour by its own row (wiki/68) — beside the time, where it varies.
776
+ // hour by its own row — beside the time, where it varies.
777
777
  if (pricedByTime && slot.costs_minor > 0) button.textContent += ` · ${money(slot.costs_minor, currency, pageLang())}`;
778
778
  button.dataset.start = slot.start;
779
779
  button.addEventListener('click', () => {
@@ -934,7 +934,7 @@ export function wireBooking(form) {
934
934
  // one by name and marks, where it was chosen — «VW Combi · білий ·
935
935
  // AA1234BB». Nothing where the business has one of everything.
936
936
  let what = made.class ? words.anyOf.replace('{class}', made.class) : resourceChosen() ? made.resource ?? '' : '';
937
- // A group: the resources it took, and how many together (wiki/69).
937
+ // A group: the resources it took, and how many together.
938
938
  if (made.group && made.group.quantity > 1) {
939
939
  what = `${made.group.bookings.map((/** @type {{resource_name?: string, resource: string}} */ b) => b.resource_name || b.resource).join(', ')} · ${words.together.replace('{n}', String(made.group.quantity))}`;
940
940
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The page's own card: where the site is, bottom right, on the dev server
3
- * only (jtakeit-core, wiki/73 · §7).
3
+ * only.
4
4
  *
5
5
  * A scaffold just made is an empty page, and a developer looking at
6
6
  * localhost while the agent writes sees nothing of what is happening. This
@@ -14,15 +14,27 @@
14
14
  *
15
15
  * Collapsed, it is a pill with the phase; a press opens it; the choice is
16
16
  * remembered in this browser.
17
+ *
18
+ * ── the stage, while the page has no words ──────────────────────────────────
19
+ *
20
+ * A scaffold just laid out is a page of empty fields and asterisks, and a
21
+ * developer who opens localhost to watch the agent saw that and nothing else
22
+ * (13 October 2026). So while the page being looked at has not one word in it
23
+ * yet, the element covers it with a stage: a page assembling itself, what the
24
+ * agent is on, what is left, the panel's steps meanwhile, and the promise that
25
+ * the page will appear here by itself. The moment the page has its first
26
+ * words, the stage goes and the page is loaded again — nobody presses
27
+ * anything. A page the files do not know (a listing, an entry) is never
28
+ * covered, nor is any page when the dev server cannot be read.
17
29
  */
18
- import { PHASES, PHASE_NAMES, phasesOf, sayPhase, meanwhile } from '../lib/phases.mjs';
30
+ import { PHASES, PHASE_NAMES, phasesOf, sayPhase, meanwhile, blankHere } from '../lib/phases.mjs';
19
31
 
20
32
  const STORE = 'jtk-writing:collapsed';
21
33
 
22
34
  const CSS = `
23
35
  :host{position:fixed;right:16px;bottom:16px;z-index:2147483000;font:13px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased;
24
- --bg:#fbfaf7;--ink:#1d1b17;--ink2:#3f3b33;--muted:#6b665c;--faint:#9b958a;--line:#e6e1d6;--line2:#d8d2c4;--ok:#2f7d5a;--attn:#9a6b1c;--shadow:0 12px 36px rgba(30,25,15,.16),0 2px 6px rgba(30,25,15,.08)}
25
- @media (prefers-color-scheme:dark){:host{--bg:#1c1b18;--ink:#f1ede5;--ink2:#d9d3c7;--muted:#a8a295;--faint:#716c62;--line:#33302a;--line2:#443f36;--ok:#5bb38a;--attn:#d4a24a;--shadow:0 12px 36px rgba(0,0,0,.5)}}
36
+ --paper:#f4f1ea;--glow:rgba(212,162,74,.16);--bg:#fbfaf7;--ink:#1d1b17;--ink2:#3f3b33;--muted:#6b665c;--faint:#9b958a;--line:#e6e1d6;--line2:#d8d2c4;--ok:#2f7d5a;--attn:#9a6b1c;--shadow:0 12px 36px rgba(30,25,15,.16),0 2px 6px rgba(30,25,15,.08)}
37
+ @media (prefers-color-scheme:dark){:host{--paper:#141311;--glow:rgba(212,162,74,.12);--bg:#1c1b18;--ink:#f1ede5;--ink2:#d9d3c7;--muted:#a8a295;--faint:#716c62;--line:#33302a;--line2:#443f36;--ok:#5bb38a;--attn:#d4a24a;--shadow:0 12px 36px rgba(0,0,0,.5)}}
26
38
  *{box-sizing:border-box;margin:0;padding:0}
27
39
  button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
28
40
  a{color:var(--ink);text-decoration:underline;text-decoration-color:var(--line2);text-underline-offset:3px}
@@ -41,7 +53,9 @@ a:hover{text-decoration-color:var(--ink)}
41
53
  .seg.done,.mini i.done{background:var(--ok)}.seg.current,.mini i.current{background:var(--attn)}.seg.current{animation:pulse 2.4s ease-in-out infinite}
42
54
  .seg.unseen{background:transparent;border:1px solid var(--line2)}
43
55
  @keyframes pulse{50%{opacity:.45}}@media (prefers-reduced-motion:reduce){.seg.current,.card.enter{animation:none}}
44
- .names{display:flex;gap:4px;margin-top:6px}.names span{flex:1;min-width:0;font-size:10px;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.names .current{color:var(--ink);font-weight:600}.names .done{color:var(--muted)}
56
+ /* One line under the strip: the phase it is in, whole, and the one after —
57
+ seven names in 340 px were seven ellipses (13 October 2026). */
58
+ .names{display:flex;gap:8px;align-items:baseline;margin-top:7px;font-size:12px}.names .current{color:var(--ink);font-weight:600}.names .next{color:var(--faint)}
45
59
  .say{display:flex;gap:9px;align-items:flex-start;margin-top:12px;font-weight:600;font-size:13.5px;line-height:1.35}.say i{flex:none;width:8px;height:8px;border-radius:999px;background:var(--attn);margin-top:5px}
46
60
  .todo{list-style:none;margin:6px 0 0 17px;color:var(--muted);font-size:12.5px}.todo li+li{margin-top:2px}
47
61
  .rule{height:1px;background:var(--line);margin:12px 0 10px}
@@ -54,6 +68,35 @@ a:hover{text-decoration-color:var(--ink)}
54
68
  .foot{margin-top:10px;color:var(--faint);font-size:11px}
55
69
  `;
56
70
 
71
+ const STAGE_CSS = `
72
+ .stage{position:fixed;inset:0;z-index:2147482999;display:grid;place-items:center;overflow:auto;padding:32px 20px;
73
+ background:radial-gradient(1200px 600px at 50% -10%,var(--glow),transparent 60%),var(--paper);color:var(--ink);
74
+ font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;animation:fade .4s ease both}
75
+ .stage.leaving{animation:out .35s ease both}
76
+ @keyframes fade{from{opacity:0}to{opacity:1}}@keyframes out{to{opacity:0}}
77
+ .wrap{width:min(560px,100%);display:grid;gap:22px}
78
+ .sheet{position:relative;height:210px;border-radius:18px;background:var(--bg);border:1px solid var(--line);box-shadow:var(--shadow);overflow:hidden;padding:18px}
79
+ .bar{height:10px;width:34%;border-radius:999px}
80
+ .row{display:flex;gap:8px;margin-top:22px}.row b{display:block;height:9px;border-radius:999px}
81
+ .hero{height:46px;width:72%;border-radius:12px;margin-top:18px}
82
+ .cards{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:18px}.cards b{display:block;height:46px;border-radius:10px}
83
+ .sk{background:linear-gradient(90deg,var(--line) 0%,var(--line2) 50%,var(--line) 100%);background-size:200% 100%;animation:shine 1.8s linear infinite;opacity:0;transform:translateY(6px)}
84
+ .sk{animation:shine 1.8s linear infinite,rise .6s cubic-bezier(.22,.61,.36,1) forwards}
85
+ .d1{animation-delay:0s,.1s}.d2{animation-delay:.1s,.35s}.d3{animation-delay:.2s,.6s}.d4{animation-delay:.3s,.85s}.d5{animation-delay:.4s,1.1s}.d6{animation-delay:.5s,1.35s}
86
+ @keyframes shine{to{background-position:-200% 0}}@keyframes rise{to{opacity:1;transform:none}}
87
+ .caret{position:absolute;right:18px;bottom:16px;display:flex;gap:5px}.caret i{width:6px;height:6px;border-radius:999px;background:var(--attn);animation:dot 1.2s ease-in-out infinite}
88
+ .caret i:nth-child(2){animation-delay:.15s}.caret i:nth-child(3){animation-delay:.3s}@keyframes dot{50%{opacity:.25;transform:translateY(-3px)}}
89
+ .kicker{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
90
+ .big{font-size:clamp(24px,4vw,32px);line-height:1.15;font-weight:650;letter-spacing:-.01em;margin-top:6px}
91
+ .now{display:flex;gap:10px;align-items:flex-start;margin-top:10px;color:var(--ink2)}.now i{flex:none;width:9px;height:9px;border-radius:999px;background:var(--attn);margin-top:7px;animation:pulse 2.4s ease-in-out infinite}
92
+ .promise{color:var(--muted);font-size:14px}
93
+ .right{margin:6px 0 0 19px;color:var(--muted);font-size:13px}
94
+ .stage .strip{margin-top:0}
95
+ .cols{display:grid;grid-template-columns:1fr 1fr;gap:18px}@media (max-width:560px){.cols{grid-template-columns:1fr}}
96
+ .stage .todo{margin:6px 0 0;padding-left:16px;list-style:disc}
97
+ @media (prefers-reduced-motion:reduce){.stage,.stage.leaving,.sk,.caret i,.now i{animation:none!important}.sk{opacity:1;transform:none}}
98
+ `;
99
+
57
100
  const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
58
101
 
59
102
  function ago(iso) {
@@ -90,7 +133,7 @@ class JtkWriting extends HTMLElement {
90
133
  connectedCallback() {
91
134
  const root = this.attachShadow({ mode: 'open' });
92
135
  const style = document.createElement('style');
93
- style.textContent = CSS;
136
+ style.textContent = CSS + STAGE_CSS;
94
137
  this.box = document.createElement('div');
95
138
  root.append(style, this.box);
96
139
  root.addEventListener('click', (event) => {
@@ -128,6 +171,19 @@ class JtkWriting extends HTMLElement {
128
171
  const where = phasesOf(progress, half);
129
172
  const n = where.index + 1;
130
173
 
174
+ // The stage, while this page has not one word in it (see the top).
175
+ const blank = !this.failed && blankHere(progress);
176
+ if (this.wasBlank && !blank && progress !== null) {
177
+ // Its first words: let the page be the page.
178
+ this.leave();
179
+ return;
180
+ }
181
+ this.wasBlank = blank;
182
+ if (blank) {
183
+ this.stage(where, progress, half);
184
+ return;
185
+ }
186
+
131
187
  if (this.collapsed) {
132
188
  this.show(`<button class="pill" data-toggle aria-expanded="false" aria-label="Where the site is: phase ${n} of 7">
133
189
  <span class="mini">${where.phases.map((p) => `<i class="${p.state}"></i>`).join('')}</span>
@@ -148,15 +204,18 @@ class JtkWriting extends HTMLElement {
148
204
  ? `<p class="note">The platform could not be read: ${esc(platform.error)}</p>`
149
205
  : `<p class="note">Not connected to the platform yet — in the working tree: <code>npx jtkit session &lt;session_id&gt; &lt;watch_key&gt;</code>, both from the session's answer.</p>`;
150
206
 
207
+ const last = this.state?.lastChange ?? null;
151
208
  const foot = [
152
- this.failed ? 'the dev server did not answer' : progress?.changedAt ? `read from the files, changed ${ago(progress.changedAt)}` : 'read from the files',
209
+ this.failed ? 'the dev server did not answer'
210
+ : last ? `last change: ${last.path.split('/').slice(-2).join('/')} · ${ago(last.at)}`
211
+ : progress?.changedAt ? `read from the files, changed ${ago(progress.changedAt)}` : 'read from the files',
153
212
  half?.until ? `platform through the session until ${until(half.until)}` : '',
154
213
  ].filter(Boolean).join(' · ');
155
214
 
156
215
  this.show(`<section class="card${this.enter ? ' enter' : ''}" aria-label="Where the site is">
157
216
  <div class="head"><span class="title">What the agent is doing</span><span class="count">phase ${n} of 7</span><button class="hide" data-toggle aria-label="Collapse">–</button></div>
158
217
  <ol class="strip" aria-label="The seven phases">${where.phases.map((p, i) => `<li class="seg ${p.state}" title="${i + 1}. ${PHASE_NAMES[p.key]}"></li>`).join('')}</ol>
159
- <div class="names" aria-hidden="true">${where.phases.map((p, i) => `<span class="${p.state}">${i + 1}. ${PHASE_NAMES[p.key]}</span>`).join('')}</div>
218
+ <p class="names"><span class="current">${n}. ${esc(PHASE_NAMES[where.current] ?? '')}</span>${where.index < 6 ? `<span class="next">next: ${esc(PHASE_NAMES[where.phases[where.index + 1].key])}</span>` : ''}</p>
160
219
  <p class="say"><i></i><span>${esc(say)}</span></p>
161
220
  ${todo.length > 0 && where.current !== 'preview'
162
221
  ? `<ul class="todo">${todo.slice(0, 4).map((line) => `<li>${esc(line)}</li>`).join('')}${todo.length > 4 ? `<li>+ ${todo.length - 4} more — <code>jtkit progress</code></li>` : ''}</ul>`
@@ -169,6 +228,48 @@ class JtkWriting extends HTMLElement {
169
228
  </section>`);
170
229
  }
171
230
 
231
+ /** The stage over a page with no words yet: what is happening, and that it will appear. */
232
+ stage(where, progress, half) {
233
+ const say = sayPhase(where, progress, half);
234
+ const todo = (progress?.missing ?? []).slice(0, 4);
235
+ const steps = meanwhile(half);
236
+ const open = half ? steps.open : [];
237
+ document.documentElement.style.overflow = 'hidden';
238
+ this.show(`<section class="stage" role="status" aria-live="polite" aria-label="The site is being written">
239
+ <div class="wrap">
240
+ <div class="sheet" aria-hidden="true">
241
+ <div class="bar sk d1"></div>
242
+ <div class="hero sk d2"></div>
243
+ <div class="row"><b class="sk d3" style="width:58%"></b><b class="sk d3" style="width:24%"></b></div>
244
+ <div class="cards"><b class="sk d4"></b><b class="sk d5"></b><b class="sk d6"></b></div>
245
+ <div class="caret"><i></i><i></i><i></i></div>
246
+ </div>
247
+ <div>
248
+ <p class="kicker">Phase ${where.index + 1} of 7 · ${esc(PHASE_NAMES[where.current] ?? '')}</p>
249
+ <h1 class="big">The site is being written</h1>
250
+ <p class="now"><i></i><span>${esc(say)}</span></p>
251
+ ${this.state?.lastChange ? `<p class="right">right now: <code>${esc(this.state.lastChange.path)}</code> · ${esc(ago(this.state.lastChange.at))}</p>` : ''}
252
+ </div>
253
+ <ol class="strip" aria-hidden="true">${where.phases.map((p) => `<li class="seg ${p.state}"></li>`).join('')}</ol>
254
+ <div class="cols">
255
+ ${todo.length > 0 ? `<div><p class="label">Still to do</p><ul class="todo">${todo.map((line) => `<li>${esc(line)}</li>`).join('')}</ul></div>` : ''}
256
+ ${open.length > 0
257
+ ? `<div><p class="label">Meanwhile, in the panel</p><ul class="steps">${open.map((s) => `<li><i class="o"></i><span>${esc(s.name)}</span></li>`).join('')}</ul>${half?.panel ? `<a class="panel" href="${esc(half.panel)}" target="_blank" rel="noopener">Open the panel ↗</a>` : ''}</div>`
258
+ : ''}
259
+ </div>
260
+ <p class="promise">This page appears here by itself the moment it has its first words — leave the tab open.</p>
261
+ </div>
262
+ </section>`);
263
+ }
264
+
265
+ /** The page has its words: the stage fades and the page is read again. */
266
+ leave() {
267
+ this.wasBlank = false;
268
+ const stage = this.box.querySelector('.stage');
269
+ stage?.classList.add('leaving');
270
+ setTimeout(() => location.reload(), stage ? 350 : 0);
271
+ }
272
+
172
273
  /** Put this on the page, unless it is what is there already. */
173
274
  show(html) {
174
275
  const key = html.replace(' enter', '');
@@ -176,6 +277,7 @@ class JtkWriting extends HTMLElement {
176
277
  this.shown = key;
177
278
  this.box.innerHTML = html;
178
279
  this.enter = false;
280
+ if (!html.includes('class="stage"')) document.documentElement.style.removeProperty('overflow');
179
281
  }
180
282
  }
181
283
 
package/index.mjs CHANGED
@@ -4,8 +4,7 @@
4
4
  *
5
5
  * `@jtakeit/astro` is the scaffold: an Astro project with the platform's
6
6
  * contract already on it. Everything in it that is not Astro's lives here,
7
- * so a site on any other generator can take the same pieces (wiki/66,
8
- * layer B): the booking form, as markup to put on a page at build time and
7
+ * so a site on any other generator can take the same pieces: the booking form, as markup to put on a page at build time and
9
8
  * as an element that makes it work in the browser.
10
9
  *
11
10
  * import { renderBookingForm } from '@jtakeit/kit/booking'; // build time
@@ -90,7 +90,7 @@ function esc(value) {
90
90
  /**
91
91
  * A service's price as the list says it: the price, or «from …» for one
92
92
  * priced by the hour of the week — whose price is the slot's and is printed
93
- * beside the time once one is chosen (wiki/68). Plain text, not escaped.
93
+ * beside the time once one is chosen. Plain text, not escaped.
94
94
  * @param {Named} one
95
95
  * @param {string} currency
96
96
  * @param {string} lang
@@ -153,7 +153,7 @@ export function bookingForm(props) {
153
153
  const last = new Date(now.getTime() + daysAhead * 86_400_000).toISOString().slice(0, 10);
154
154
 
155
155
  // An add-on — a price and no length: shoes, a racket — is listed under
156
- // its own heading, after the services, and rides on the visit (wiki/70).
156
+ // its own heading, after the services, and rides on the visit.
157
157
  const addOns = runs.flatMap((run) => run.items.filter((one) => !one.takes && (one.costs || priceFrom(one))));
158
158
  const timedRuns = runs.map((run) => ({ ...run, items: run.items.filter((one) => !addOns.includes(one)) })).filter((run) => run.items.length > 0);
159
159
  /** @param {Named} one */
package/lib/catalogue.mjs CHANGED
@@ -39,7 +39,7 @@
39
39
  * writes it, with a line number, rather than in the admin days later with none.
40
40
  *
41
41
  * Two hand-written validators of one contract disagree silently, so this one
42
- * sits the other's exam (wiki/35 §5): every finding carries the same `JTK_E_*`
42
+ * sits the other's exam: every finding carries the same `JTK_E_*`
43
43
  * code the server would give it, and `--judge` posts the emitted catalogue to
44
44
  * the real validator and reports every place the two answers differ. Every
45
45
  * client project the studio runs through it is one sitting.
@@ -108,8 +108,8 @@ const catalogue = {
108
108
  * The version of the catalogue FORMAT, and the tool that wrote this file.
109
109
  *
110
110
  * A whole number because the question the service asks is binary: can this
111
- * parser read this file. Additive changes are compatible by the rule in
112
- * wiki/05, so a minor could never change what the validator does — it would
111
+ * parser read this file. Additive changes are compatible by the platform's
112
+ * rule, so a minor could never change what the validator does — it would
113
113
  * be documentation living in a load-bearing field. Semver belongs to this
114
114
  * toolkit instead, and its major names the contract it speaks.
115
115
  *
@@ -591,7 +591,7 @@ for (const collection of collections) {
591
591
  /*
592
592
  * What the entries are grouped by: a select, or a ref to another
593
593
  * collection, and nothing else — the platform's validateEntryGroup, mirrored
594
- * (12 October 2026, wiki/78). Free text would be a list of one-entry groups
594
+ * (12 October 2026). Free text would be a list of one-entry groups
595
595
  * with three spellings of "manicure". A ref into the collection itself is a
596
596
  * tree, which is a `parent` field and takes no part in grouping: an entry
597
597
  * heading its own siblings is a listing nobody can order.
@@ -662,7 +662,7 @@ for (const collection of collections) {
662
662
  * Every `ref`, wherever it is — a block's field, a row of a list, the page's
663
663
  * SEO, the business facts — names a collection this catalogue declares. The
664
664
  * platform's validateRefs, mirrored on 12 October 2026, when a ref became
665
- * the way a site has categories and tags (wiki/78): `tags: list of { tag:
665
+ * the way a site has categories and tags: `tags: list of { tag:
666
666
  * ref tags }` with no `tags` collection is a field whose every value points
667
667
  * nowhere, and the judge refused it while this said nothing.
668
668
  */
@@ -782,7 +782,7 @@ for (const file of sourceFiles(join(root, 'src'))) {
782
782
  /*
783
783
  * ── the exam ────────────────────────────────────────────────────────────────
784
784
  *
785
- * wiki/35 §5 settles which of the two validators is right: the Go one is the
785
+ * Of the two validators the platform's is right: the Go one is the
786
786
  * judge, and this one sits its exam. Everything below is that sitting — post the
787
787
  * catalogue we just built to `POST /v1/registry/validate`, and compare the two
788
788
  * verdicts.
@@ -801,8 +801,7 @@ for (const file of sourceFiles(join(root, 'src'))) {
801
801
  /**
802
802
  * The working tree's other jtk/ files, sent beside the catalogue so the
803
803
  * second judge — the settings, the entries, a price table's gaps, an add-on
804
- * nobody can take — answers in the same call, before any push (core
805
- * wiki/71 · §3.2). Everything under jtk/ but the catalogue itself, as
804
+ * nobody can take — answers in the same call, before any push. Everything under jtk/ but the catalogue itself, as
806
805
  * `{path, text}`; a site with no jtk/ yet sends none.
807
806
  * @param {string} root
808
807
  * @returns {{ path: string, text: string }[]}
@@ -838,7 +837,7 @@ function judgeRequest(text, files) {
838
837
  // (auth/authHandler.go): an agent's `jtk_…` token as a bearer, a
839
838
  // person's signed-in session as the `sid` cookie. This said once that
840
839
  // there were no bearer tokens in the app, which stopped being true
841
- // when agents got theirs (wiki/40); a `jtk_…` token sent as a cookie
840
+ // when agents got theirs; a `jtk_…` token sent as a cookie
842
841
  // is not read at all.
843
842
  ...(token.startsWith('jtk_') ? { authorization: `Bearer ${token}` } : { cookie: `sid=${token}` }),
844
843
  accept: 'application/json',
@@ -970,7 +969,7 @@ catalogue, the settings, the content — before any push, every finding at once.
970
969
  }
971
970
 
972
971
  console.error(`
973
- The Go validator in the admin's registry package is the judge (wiki/35 §5); the
972
+ The Go validator in the admin's registry package is the judge; the
974
973
  checks in this file are a copy that has to work offline. A divergence is this
975
974
  file's bug, not the server's — bring the check here into line, or add the one it
976
975
  is missing, and note it in the platform's 12 · debt if the contract is what
@@ -1178,8 +1177,7 @@ function hasBodyPicture(document) {
1178
1177
  * Whether this site has any entries at all, for the one message that mentions
1179
1178
  * them.
1180
1179
  *
1181
- * An entry is a page now — a `.json` document of blocks (the platform's
1182
- * wiki/30) — so what makes one an entry is the `collection` inside it, not the
1180
+ * An entry is a page now — a `.json` document of blocks — so what makes one an entry is the `collection` inside it, not the
1183
1181
  * extension. Which also means this check no longer needs the site's own Astro
1184
1182
  * installed to read a body: it used to parse frontmatter with it, and that is
1185
1183
  * gone with the format.
@@ -1414,7 +1412,7 @@ const disagreements = [];
1414
1412
  * `tappable()` in its sitebuild/annotation-lint.mjs, copied (12 October 2026).
1415
1413
  *
1416
1414
  * This used to ask it of every field but a bool or a `no_tap_target`, which
1417
- * is stricter than the judge in exactly the shapes wiki/78 made the way a
1415
+ * is stricter than the judge in exactly the shapes that are the way a
1418
1416
  * site has categories and tags: a service's `category: ref categories`, a
1419
1417
  * dish's `course` select, a work's `tags: [{ tag }]` were each reported as a
1420
1418
  * control that edits nothing — they are edited in the panel beside the page,
@@ -1440,9 +1438,7 @@ for (const page of pages) {
1440
1438
  // are read by the page that lists them and by the diary.
1441
1439
  // Said by the entry's own collection, or by its address — with the
1442
1440
  // language in front of it for an entry in another one: `/de/services/x`
1443
- // is the `services` collection's as much as `/services/x` is (wiki/75 of
1444
- // the platform; a German salon's eight services were each reported as a
1445
- // page the build did not produce, 10 October 2026).
1441
+ // is the `services` collection's as much as `/services/x` is.
1446
1442
  const bare = basePathOf(page.path, catalogue.locales ?? []);
1447
1443
  const listedOnly = collections.some(
1448
1444
  (c) => c.pages === false && (page.collection === c.name || bare.startsWith(`${c.prefix}/`)),
@@ -1963,7 +1959,7 @@ them data-jtk-fixed, which silences everything inside the element it is on.
1963
1959
 
1964
1960
  Undeclared is not a third answer. It is what "I cannot edit the header" is made
1965
1961
  of, and it arrives as a message and a deploy weeks later. See
1966
- references/catalogue.md.`);
1962
+ docs/catalogue.md in @jtakeit/astro.`);
1967
1963
  process.exit(Math.min(loose.length, 100));
1968
1964
  }
1969
1965
 
package/lib/codes.mjs CHANGED
@@ -23,7 +23,7 @@
23
23
  * That flag posts the emitted catalogue to the real validator and reports every
24
24
  * place the two answers differ — a code the server raised and we did not, or one
25
25
  * we raised and it did not. Every client the studio runs through it is one
26
- * sitting of that exam (wiki/35 §5, §7), so a code that has been renamed or a
26
+ * sitting of that exam, so a code that has been renamed or a
27
27
  * check that has drifted is found by the next real project rather than by a
28
28
  * reviewer noticing. A copy nobody checks rots in a month; a copy that is
29
29
  * examined weekly is a cache.
package/lib/kits.mjs CHANGED
@@ -34,7 +34,7 @@ export function readKit(file) {
34
34
  }
35
35
 
36
36
  /**
37
- * A kit asked of the platform by its kind (wiki/64 · §4.1): the kit whole,
37
+ * A kit asked of the platform by its kind: the kit whole,
38
38
  * every part written out, and its version — which the site's settings record
39
39
  * as `kit_version`, so the platform's judge can say when a fix to the kit has
40
40
  * not reached the site (JTK_W_KIT_BEHIND).
package/lib/lint.mjs CHANGED
@@ -9,8 +9,7 @@
9
9
  * so it works with any template, including one an agent wrote this afternoon.
10
10
  *
11
11
  * The platform's build runs this lint on every demo build. It is here so that
12
- * a site can run it on its own `dist/` before any push (`jtkit lint`, core
13
- * wiki/71 · §3.2): the same checks, the same codes, the same sentences, and
12
+ * a site can run it on its own `dist/` before any push (`jtkit lint`): the same checks, the same codes, the same sentences, and
14
13
  * every finding at once rather than one build round each.
15
14
  *
16
15
  * `lint()` returns findings rather than printing and exiting, so a script, a
@@ -366,7 +365,7 @@ export async function lint(options = {}) {
366
365
  /**
367
366
  * The files under a built directory the platform never publishes: a path with
368
367
  * a segment that starts with a dot, `.well-known/` at the root aside — the
369
- * rule the platform's upload and edge keep (its wiki/81). A directory is said
368
+ * rule the platform's upload and edge keep. A directory is said
370
369
  * once, with a slash.
371
370
  * @param {string} dir
372
371
  * @returns {Promise<string[]>}
package/lib/phases.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
- * The seven phases of a site being written, from two halves (jtakeit-core,
3
- * wiki/73 · §4 and §7).
2
+ * The seven phases of a site being written, from two halves.
4
3
  *
5
4
  * The kit's half is `readProgress` — the brief, the layout, the writing,
6
5
  * read off the files. The platform's half — the judge's word, the
@@ -103,9 +102,15 @@ export function sayPhase(where, progress, platform) {
103
102
  case 'laidOut':
104
103
  return 'Laying the site out';
105
104
  case 'writing': {
105
+ // In the order the work goes, not always «the pages»: an agent declaring
106
+ // the blocks and setting the diary up for five minutes read as writing
107
+ // a page it had not reached (13 October 2026).
108
+ if (progress && progress.catalogue !== true) return 'Declaring the blocks — what the site is made of';
109
+ if (progress && progress.kit !== null && progress.settings === null) return 'Setting up the bookings — hours, prices, what is booked';
106
110
  const pages = progress?.pages ?? [];
107
111
  const written = pages.filter((one) => one.written).length;
108
- return `Writing the pages — ${written} of ${pages.length} with their text · still to do: ${progress?.missing.length ?? 0}`;
112
+ if (pages.length > 0 && written < pages.length) return `Writing the pages — ${written} of ${pages.length} with their text`;
113
+ return `Finishing — still to do: ${progress?.missing.length ?? 0}`;
109
114
  }
110
115
  case 'judged':
111
116
  if (!known || !platform.judged) return 'To be checked before the push';
@@ -137,3 +142,18 @@ export function meanwhile(platform) {
137
142
  done: theirs.filter((step) => step.done).map(named),
138
143
  };
139
144
  }
145
+
146
+ /**
147
+ * Whether the page being looked at has no words yet: a page the files know,
148
+ * with not one text field filled. A page they do not know — a listing, an
149
+ * entry, a 404 — is never blank by this; nor is any page before the files
150
+ * could be read.
151
+ * @param {any} progress
152
+ */
153
+ export function blankHere(progress, path = typeof location === 'undefined' ? '/' : location.pathname) {
154
+ if (!progress || !Array.isArray(progress.pages)) return false;
155
+ const norm = (p) => (p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p);
156
+ const here = norm(path);
157
+ const page = progress.pages.find((one) => norm(one.path) === here);
158
+ return page !== undefined && page.filled === 0;
159
+ }
package/lib/progress.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Progress is a function of the files (jtakeit-core, wiki/73).
2
+ * Progress is a function of the files.
3
3
  *
4
4
  * Between «connect an agent» and the first preview a person sees nothing for
5
5
  * an hour while the site is written. What they should see is not what the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtakeit/kit",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "The framework-free half of a site on jtakeit: the booking form as an element and as markup, for Astro through @jtakeit/astro and for any other generator directly",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -92,7 +92,8 @@
92
92
  "types",
93
93
  "booking.css",
94
94
  "lead.css",
95
- "README.md"
95
+ "README.md",
96
+ "LICENSE"
96
97
  ],
97
98
  "engines": {
98
99
  "node": ">=22.12.0"
@@ -110,5 +111,9 @@
110
111
  ],
111
112
  "scripts": {
112
113
  "test": "node --test"
113
- }
114
+ },
115
+ "bugs": {
116
+ "url": "https://github.com/jtakeit/jtakeit-kit/issues"
117
+ },
118
+ "homepage": "https://github.com/jtakeit/jtakeit-kit/tree/main/kit#readme"
114
119
  }
package/types/phases.d.ts CHANGED
@@ -39,3 +39,4 @@ export function meanwhile(platform: PlatformHalf | null | undefined): {
39
39
  open: { key: string; name: string; done: boolean }[];
40
40
  done: { key: string; name: string; done: boolean }[];
41
41
  };
42
+ export function blankHere(progress: unknown, path?: string): boolean;