@jtakeit/kit 0.7.0 → 0.7.1

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.
@@ -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/lib/phases.mjs CHANGED
@@ -103,9 +103,15 @@ export function sayPhase(where, progress, platform) {
103
103
  case 'laidOut':
104
104
  return 'Laying the site out';
105
105
  case 'writing': {
106
+ // In the order the work goes, not always «the pages»: an agent declaring
107
+ // the blocks and setting the diary up for five minutes read as writing
108
+ // a page it had not reached (13 October 2026).
109
+ if (progress && progress.catalogue !== true) return 'Declaring the blocks — what the site is made of';
110
+ if (progress && progress.kit !== null && progress.settings === null) return 'Setting up the bookings — hours, prices, what is booked';
106
111
  const pages = progress?.pages ?? [];
107
112
  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}`;
113
+ if (pages.length > 0 && written < pages.length) return `Writing the pages — ${written} of ${pages.length} with their text`;
114
+ return `Finishing — still to do: ${progress?.missing.length ?? 0}`;
109
115
  }
110
116
  case 'judged':
111
117
  if (!known || !platform.judged) return 'To be checked before the push';
@@ -137,3 +143,18 @@ export function meanwhile(platform) {
137
143
  done: theirs.filter((step) => step.done).map(named),
138
144
  };
139
145
  }
146
+
147
+ /**
148
+ * Whether the page being looked at has no words yet: a page the files know,
149
+ * with not one text field filled. A page they do not know — a listing, an
150
+ * entry, a 404 — is never blank by this; nor is any page before the files
151
+ * could be read.
152
+ * @param {any} progress
153
+ */
154
+ export function blankHere(progress, path = typeof location === 'undefined' ? '/' : location.pathname) {
155
+ if (!progress || !Array.isArray(progress.pages)) return false;
156
+ const norm = (p) => (p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p);
157
+ const here = norm(path);
158
+ const page = progress.pages.find((one) => norm(one.path) === here);
159
+ return page !== undefined && page.filled === 0;
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtakeit/kit",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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",
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;