@jtakeit/kit 0.3.0 → 0.5.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.
- package/README.md +36 -0
- package/bin/jtkit.mjs +44 -0
- package/booking.css +55 -1
- package/elements/booking-ui.js +371 -0
- package/elements/booking.js +173 -8
- package/elements/writing.js +193 -0
- package/lib/booking/parts.mjs +63 -0
- package/lib/booking/render.mjs +16 -4
- package/lib/booking/words.mjs +200 -0
- package/lib/catalogue.mjs +18 -2
- package/lib/money.mjs +24 -4
- package/lib/phases.mjs +139 -0
- package/lib/progress.mjs +317 -0
- package/package.json +21 -2
- package/types/booking-ui.d.ts +6 -0
- package/types/booking.d.ts +6 -0
- package/types/parts.d.ts +7 -0
- package/types/phases.d.ts +41 -0
- package/types/progress.d.ts +50 -0
package/lib/progress.mjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress is a function of the files (jtakeit-core, wiki/73).
|
|
3
|
+
*
|
|
4
|
+
* Between «connect an agent» and the first preview a person sees nothing for
|
|
5
|
+
* an hour while the site is written. What they should see is not what the
|
|
6
|
+
* agent says it is doing but what the site *is*: this module reads the
|
|
7
|
+
* working tree and says, for the person and the agent alike, what is written
|
|
8
|
+
* and what is still missing. It declares nothing and believes nothing — the
|
|
9
|
+
* brief's own checklist is read out as the agent's words, apart from the
|
|
10
|
+
* files', and the rest is counted.
|
|
11
|
+
*
|
|
12
|
+
* One implementation, two readers: the scaffold's dev server serves this as
|
|
13
|
+
* `/_jtk/progress.json` (jtakeit-meta.mjs) for the panel, and `jtkit
|
|
14
|
+
* progress` prints it for the agent. A rule here is a rule in both.
|
|
15
|
+
*
|
|
16
|
+
* ── what «written» means ───────────────────────────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* The scaffold's content leaves every text empty (`jtk/content/index.json`),
|
|
19
|
+
* so a page is being written when any of its text fields is filled, and is
|
|
20
|
+
* written when every field the catalogue marks `required` is — the exact
|
|
21
|
+
* rule where there is a catalogue, and the weaker one («has text») where a
|
|
22
|
+
* site on another generator has none yet, said as such in `rule`.
|
|
23
|
+
*/
|
|
24
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { collectContent } from './content.mjs';
|
|
27
|
+
|
|
28
|
+
/** Under the weaker rule, a key named like one of these is not words. */
|
|
29
|
+
const NOT_WORDS = /(^|_)(image|picture|photo|media|video|url|href|link|icon|src)(_|$)|_href$|_url$/;
|
|
30
|
+
/** The field kinds a person writes words into. */
|
|
31
|
+
const TEXT = new Set(['text', 'textarea', 'richtext_lite', 'markdown']);
|
|
32
|
+
/** The brief's headings, in the order the guide writes them. */
|
|
33
|
+
export const BRIEF_SECTIONS = ['In one sentence', 'The one action', 'Facts', 'Bookings', 'Decisions', 'Open — NEEDS CLIENT', 'Done when'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the working tree and say where the site is.
|
|
37
|
+
* @param {string} root the site's root, holding `BRIEF.md`, `jtk/` and `src/`
|
|
38
|
+
* @param {{ dir?: string }} [options] `dir`: where `jtk/` is, relative to the root
|
|
39
|
+
*/
|
|
40
|
+
export async function readProgress(root, options = {}) {
|
|
41
|
+
const dir = options.dir ?? 'jtk';
|
|
42
|
+
const seen = [];
|
|
43
|
+
const note = (file) => stat(file).then((s) => seen.push(s.mtime), () => {});
|
|
44
|
+
|
|
45
|
+
const brief = await readBrief(join(root, 'BRIEF.md'));
|
|
46
|
+
await note(join(root, 'BRIEF.md'));
|
|
47
|
+
const catalogue = await readJSON(join(root, dir, 'catalogue.json'));
|
|
48
|
+
await note(join(root, dir, 'catalogue.json'));
|
|
49
|
+
const settings = await readJSON(join(root, dir, 'bookings.json'));
|
|
50
|
+
await note(join(root, dir, 'bookings.json'));
|
|
51
|
+
const facts = await readFacts(join(root, 'src', 'data', 'site.ts'));
|
|
52
|
+
await note(join(root, 'src', 'data', 'site.ts'));
|
|
53
|
+
let content = { pages: [] };
|
|
54
|
+
try {
|
|
55
|
+
content = await collectContent(root, { dir });
|
|
56
|
+
} catch {
|
|
57
|
+
// No jtk/ yet, or a document that is not a page: nothing is written.
|
|
58
|
+
}
|
|
59
|
+
for (const file of await contentFiles(join(root, dir, 'content'))) await note(file);
|
|
60
|
+
|
|
61
|
+
const fields = fieldsOf(catalogue);
|
|
62
|
+
const rule = catalogue ? 'exact' : 'has-text';
|
|
63
|
+
const pages = [];
|
|
64
|
+
const entries = {};
|
|
65
|
+
let media = 0;
|
|
66
|
+
for (const page of content.pages) {
|
|
67
|
+
const counted = countFields(page, fields, rule);
|
|
68
|
+
media += counted.media;
|
|
69
|
+
if (page.collection !== undefined) {
|
|
70
|
+
entries[page.collection] = (entries[page.collection] ?? 0) + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
pages.push({
|
|
74
|
+
path: page.path,
|
|
75
|
+
written: counted.filled > 0 && counted.requiredMissing === 0,
|
|
76
|
+
filled: counted.filled,
|
|
77
|
+
of: counted.of,
|
|
78
|
+
...(counted.requiredMissing > 0 ? { missing: counted.requiredMissing } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
pages.sort((a, b) => (a.path === '/' ? -1 : b.path === '/' ? 1 : a.path.localeCompare(b.path)));
|
|
82
|
+
|
|
83
|
+
const collections = {};
|
|
84
|
+
const bookings = catalogue?.modules?.bookings ?? null;
|
|
85
|
+
for (const one of catalogue?.collections ?? []) {
|
|
86
|
+
const expected = bookings !== null && (bookings.services === one.name || bookings.resources === one.name) ? 1 : 0;
|
|
87
|
+
collections[one.name] = { entries: entries[one.name] ?? 0, expected };
|
|
88
|
+
}
|
|
89
|
+
for (const [name, count] of Object.entries(entries)) {
|
|
90
|
+
if (collections[name] === undefined) collections[name] = { entries: count, expected: 0 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const block = settings?.blocks?.[0] ?? null;
|
|
94
|
+
const settingsRead =
|
|
95
|
+
block === null
|
|
96
|
+
? null
|
|
97
|
+
: {
|
|
98
|
+
kit: typeof block.kit === 'string' ? block.kit : '',
|
|
99
|
+
kit_version: typeof block.kit_version === 'string' ? block.kit_version : '',
|
|
100
|
+
hours: Array.isArray(block.hours) && block.hours.length > 0,
|
|
101
|
+
zone: typeof block.zone === 'string' && block.zone !== '',
|
|
102
|
+
payment: typeof block.payment === 'string' ? block.payment : '',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const missing = [];
|
|
106
|
+
if (brief === null) missing.push('BRIEF.md is not written yet');
|
|
107
|
+
else {
|
|
108
|
+
if (brief.open > 0) missing.push(`${brief.open} question${brief.open === 1 ? '' : 's'} for the client (Open — NEEDS CLIENT)`);
|
|
109
|
+
if (!brief.sections['Decisions']) missing.push('the brief has no Decisions yet');
|
|
110
|
+
}
|
|
111
|
+
if (!catalogue) missing.push('jtk/catalogue.json is not written yet (npx @jtakeit/astro catalogue)');
|
|
112
|
+
if (pages.length === 0) missing.push('no page has content yet');
|
|
113
|
+
for (const page of pages) {
|
|
114
|
+
if (page.written) continue;
|
|
115
|
+
missing.push(page.filled === 0 ? `${page.path} is not written yet` : `${page.path}: ${page.filled} of ${page.of} fields, ${page.missing ?? 0} required missing`);
|
|
116
|
+
}
|
|
117
|
+
for (const [name, one] of Object.entries(collections)) {
|
|
118
|
+
if (one.entries < one.expected) missing.push(`${name}: no entry yet`);
|
|
119
|
+
}
|
|
120
|
+
if (bookings !== null) {
|
|
121
|
+
if (settingsRead === null) missing.push('jtk/bookings.json is not written yet');
|
|
122
|
+
else {
|
|
123
|
+
if (!settingsRead.hours) missing.push('the opening hours are not set (jtk/bookings.json)');
|
|
124
|
+
if (!settingsRead.zone) missing.push('the time zone is not set (jtk/bookings.json)');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (facts !== null && facts.placeholders > 0) missing.push(`src/data/site.ts still carries ${facts.placeholders} placeholder${facts.placeholders === 1 ? '' : 's'}`);
|
|
128
|
+
if (pages.length > 0 && media === 0) missing.push('no picture yet');
|
|
129
|
+
|
|
130
|
+
const changedAt = seen.length > 0 ? new Date(Math.max(...seen.map((d) => d.getTime()))).toISOString() : null;
|
|
131
|
+
return {
|
|
132
|
+
version: 1,
|
|
133
|
+
rule,
|
|
134
|
+
kit: settingsRead === null ? null : { kind: settingsRead.kit, version: settingsRead.kit_version },
|
|
135
|
+
brief,
|
|
136
|
+
catalogue: catalogue !== null,
|
|
137
|
+
pages,
|
|
138
|
+
collections,
|
|
139
|
+
settings: settingsRead === null ? null : { hours: settingsRead.hours, zone: settingsRead.zone, payment: settingsRead.payment },
|
|
140
|
+
facts,
|
|
141
|
+
media,
|
|
142
|
+
missing,
|
|
143
|
+
changedAt,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The brief as a document: which sections have words, how many facts carry a
|
|
149
|
+
* source, how many questions are open, and the agent's own checklist under
|
|
150
|
+
* «Done when» — read out, never believed.
|
|
151
|
+
*/
|
|
152
|
+
export async function readBrief(file) {
|
|
153
|
+
let text;
|
|
154
|
+
try {
|
|
155
|
+
text = await readFile(file, 'utf8');
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const sections = {};
|
|
160
|
+
const bodies = {};
|
|
161
|
+
let current = null;
|
|
162
|
+
for (const line of text.split('\n')) {
|
|
163
|
+
const heading = /^##\s+(.+?)\s*$/.exec(line);
|
|
164
|
+
if (heading) {
|
|
165
|
+
current = heading[1];
|
|
166
|
+
bodies[current] = [];
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (current !== null) bodies[current].push(line);
|
|
170
|
+
}
|
|
171
|
+
// A section has words when a line of it is neither empty, nor a table's
|
|
172
|
+
// header or separator, nor the skeleton's own sentence (the kit's line
|
|
173
|
+
// under Bookings is written by `jtk create`, not by the agent).
|
|
174
|
+
for (const name of BRIEF_SECTIONS) {
|
|
175
|
+
sections[name] = rowsOf(bodies[name] ?? []).some((l) => !/^\|/.test(l) ? !/^The kit: .*, whole/.test(l) : true);
|
|
176
|
+
}
|
|
177
|
+
const factRows = rowsOf(bodies['Facts'] ?? []).filter((l) => /^\|/.test(l));
|
|
178
|
+
const open = (bodies['Open — NEEDS CLIENT'] ?? []).filter((l) => /^\s*[-*]\s+\S/.test(l)).length;
|
|
179
|
+
const items = (bodies['Done when'] ?? []).filter((l) => /^\s*[-*]\s+\[[ xX]\]/.test(l));
|
|
180
|
+
const done = items.filter((l) => /\[[xX]\]/.test(l)).length;
|
|
181
|
+
// Gaps are marked in the tables: a fact or a default whose value is still
|
|
182
|
+
// the client's to give. The heading and the checklist's own words are not
|
|
183
|
+
// gaps.
|
|
184
|
+
const needsClient = [...rowsOf(bodies['Facts'] ?? []), ...rowsOf(bodies['Bookings'] ?? [])]
|
|
185
|
+
.filter((l) => /^\|/.test(l) && /NEEDS CLIENT/.test(l)).length;
|
|
186
|
+
return {
|
|
187
|
+
sections,
|
|
188
|
+
facts: factRows.length,
|
|
189
|
+
open,
|
|
190
|
+
needsClient: Math.max(needsClient, 0),
|
|
191
|
+
doneWhen: { done, of: items.length, items: items.map((l) => ({ done: /\[[xX]\]/.test(l), says: l.replace(/^\s*[-*]\s+\[[ xX]\]\s*/, '').trim() })) },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The lines of a section that carry words: not blank, not a table's header or its separator. */
|
|
196
|
+
function rowsOf(body) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (let i = 0; i < body.length; i++) {
|
|
199
|
+
const line = body[i];
|
|
200
|
+
if (line.trim() === '') continue;
|
|
201
|
+
if (/^\|?\s*:?-+:?\s*\|/.test(line)) continue;
|
|
202
|
+
if (/^\|/.test(line) && /^\|?\s*:?-+:?\s*\|/.test(body[i + 1] ?? '')) continue;
|
|
203
|
+
out.push(line);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function readFacts(file) {
|
|
209
|
+
let text;
|
|
210
|
+
try {
|
|
211
|
+
text = await readFile(file, 'utf8');
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return { placeholders: (text.match(/\{\{[A-Z_]+\}\}/g) ?? []).length };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fieldsOf(catalogue) {
|
|
219
|
+
const byType = new Map();
|
|
220
|
+
for (const block of catalogue?.blocks ?? []) {
|
|
221
|
+
byType.set(block.type, block.fields ?? []);
|
|
222
|
+
}
|
|
223
|
+
return byType;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** How much of a document is written: the text fields filled, the required ones missing, the pictures in it. */
|
|
227
|
+
function countFields(page, fields, rule) {
|
|
228
|
+
let filled = 0;
|
|
229
|
+
let of = 0;
|
|
230
|
+
let requiredMissing = 0;
|
|
231
|
+
let media = 0;
|
|
232
|
+
for (const block of page.blocks ?? []) {
|
|
233
|
+
const defs = fields.get(block.type);
|
|
234
|
+
if (rule === 'exact' && defs !== undefined) {
|
|
235
|
+
for (const def of defs) {
|
|
236
|
+
if (def.kind === 'media') {
|
|
237
|
+
if (hasText(block[def.key]) || (Array.isArray(block[def.key]) && block[def.key].length > 0)) media++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!TEXT.has(def.kind)) continue;
|
|
241
|
+
of++;
|
|
242
|
+
if (hasText(block[def.key])) filled++;
|
|
243
|
+
else if (def.required) requiredMissing++;
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// The weaker rule, with no catalogue to say what a field is: every
|
|
248
|
+
// string is words unless its name or its value says it is an address
|
|
249
|
+
// or a picture.
|
|
250
|
+
for (const [key, value] of Object.entries(block)) {
|
|
251
|
+
if (key === '_key' || key === 'type' || key === 'v') continue;
|
|
252
|
+
if (typeof value !== 'string') continue;
|
|
253
|
+
if (NOT_WORDS.test(key)) {
|
|
254
|
+
if (hasText(value) && /image|picture|photo|media|video/.test(key)) media++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (/^(https?:|#|\/)/.test(value)) continue;
|
|
258
|
+
of++;
|
|
259
|
+
if (hasText(value)) filled++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { filled, of, requiredMissing, media };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function hasText(value) {
|
|
266
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function readJSON(file) {
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function contentFiles(dir) {
|
|
278
|
+
const out = [];
|
|
279
|
+
let names;
|
|
280
|
+
try {
|
|
281
|
+
names = await readdir(dir, { withFileTypes: true });
|
|
282
|
+
} catch {
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
for (const one of names) {
|
|
286
|
+
const path = join(dir, one.name);
|
|
287
|
+
if (one.isDirectory()) out.push(...(await contentFiles(path)));
|
|
288
|
+
else if (one.name.endsWith('.json')) out.push(path);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The list as a person reads it in a terminal. */
|
|
294
|
+
export function sayProgress(progress) {
|
|
295
|
+
const lines = [];
|
|
296
|
+
if (progress.brief === null) lines.push('brief: not written');
|
|
297
|
+
else {
|
|
298
|
+
const b = progress.brief;
|
|
299
|
+
lines.push(`brief: ${b.facts} fact(s), ${b.open} open for the client, done when ${b.doneWhen.done}/${b.doneWhen.of}`);
|
|
300
|
+
}
|
|
301
|
+
lines.push(`catalogue: ${progress.catalogue ? 'written' : 'not yet'}`);
|
|
302
|
+
for (const page of progress.pages) {
|
|
303
|
+
lines.push(`${page.written ? 'ok ' : 'todo '}${page.path} ${page.filled}/${page.of} fields${page.missing ? `, ${page.missing} required missing` : ''}`);
|
|
304
|
+
}
|
|
305
|
+
for (const [name, one] of Object.entries(progress.collections)) {
|
|
306
|
+
lines.push(`${one.entries >= one.expected ? 'ok ' : 'todo '}${name}: ${one.entries} entr${one.entries === 1 ? 'y' : 'ies'}${one.expected ? ` (≥ ${one.expected})` : ''}`);
|
|
307
|
+
}
|
|
308
|
+
if (progress.settings) lines.push(`settings: hours ${progress.settings.hours ? 'set' : 'not set'}, zone ${progress.settings.zone ? 'set' : 'not set'}`);
|
|
309
|
+
lines.push(`pictures: ${progress.media}`);
|
|
310
|
+
lines.push('');
|
|
311
|
+
if (progress.missing.length === 0) lines.push('nothing missing — judge it, push, attach, build');
|
|
312
|
+
else {
|
|
313
|
+
lines.push('missing:');
|
|
314
|
+
for (const one of progress.missing) lines.push(` - ${one}`);
|
|
315
|
+
}
|
|
316
|
+
return lines.join('\n');
|
|
317
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtakeit/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./types/words.d.ts",
|
|
21
21
|
"default": "./lib/booking/words.mjs"
|
|
22
22
|
},
|
|
23
|
+
"./booking/parts": {
|
|
24
|
+
"types": "./types/parts.d.ts",
|
|
25
|
+
"default": "./lib/booking/parts.mjs"
|
|
26
|
+
},
|
|
23
27
|
"./lead": {
|
|
24
28
|
"types": "./types/lead.d.ts",
|
|
25
29
|
"default": "./lib/lead/render.mjs"
|
|
@@ -42,6 +46,10 @@
|
|
|
42
46
|
"types": "./types/elements-booking.d.ts",
|
|
43
47
|
"default": "./elements/booking.js"
|
|
44
48
|
},
|
|
49
|
+
"./elements/booking-ui": {
|
|
50
|
+
"types": "./types/booking-ui.d.ts",
|
|
51
|
+
"default": "./elements/booking-ui.js"
|
|
52
|
+
},
|
|
45
53
|
"./elements/lead": {
|
|
46
54
|
"types": "./types/elements-lead.d.ts",
|
|
47
55
|
"default": "./elements/lead.js"
|
|
@@ -56,6 +64,17 @@
|
|
|
56
64
|
"types": "./types/lint.d.ts",
|
|
57
65
|
"default": "./lib/lint.mjs"
|
|
58
66
|
},
|
|
67
|
+
"./progress": {
|
|
68
|
+
"types": "./types/progress.d.ts",
|
|
69
|
+
"default": "./lib/progress.mjs"
|
|
70
|
+
},
|
|
71
|
+
"./phases": {
|
|
72
|
+
"types": "./types/phases.d.ts",
|
|
73
|
+
"default": "./lib/phases.mjs"
|
|
74
|
+
},
|
|
75
|
+
"./writing": {
|
|
76
|
+
"default": "./elements/writing.js"
|
|
77
|
+
},
|
|
59
78
|
"./content": {
|
|
60
79
|
"types": "./types/content.d.ts",
|
|
61
80
|
"default": "./lib/content.mjs"
|
|
@@ -76,7 +95,7 @@
|
|
|
76
95
|
},
|
|
77
96
|
"repository": {
|
|
78
97
|
"type": "git",
|
|
79
|
-
"url": "https://github.com/jtakeit/jtakeit-kit.git",
|
|
98
|
+
"url": "git+https://github.com/jtakeit/jtakeit-kit.git",
|
|
80
99
|
"directory": "kit"
|
|
81
100
|
},
|
|
82
101
|
"keywords": [
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The booking form's look, by default: the steps, the week of days, the parts of the day, the drawn choice. */
|
|
2
|
+
export function dressBooking(form: HTMLFormElement, words: Record<string, string>): void;
|
|
3
|
+
export function weekStrip(input: HTMLInputElement, options: { open: string[]; lang: string; words: Record<string, string> }): void;
|
|
4
|
+
export function partsOfDay(form: HTMLFormElement, words: Record<string, string>): void;
|
|
5
|
+
export function twoSteps(form: HTMLFormElement, words: Record<string, string>, lang: string): void;
|
|
6
|
+
export function drawnSelect(select: HTMLSelectElement): void;
|
package/types/booking.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface Named {
|
|
|
8
8
|
/** `'person'` for a price times the party — a class per place, shoes; the list says so and the running total multiplies. */
|
|
9
9
|
per?: 'booking' | 'person';
|
|
10
10
|
group?: string;
|
|
11
|
+
/** For a resource: the services they perform, by slug. The element greys the others out once they are chosen. */
|
|
12
|
+
does?: string[];
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface BookingFormProps {
|
|
@@ -24,6 +26,10 @@ export interface BookingFormProps {
|
|
|
24
26
|
/** The element around the form, `jtk-booking`; `''` for none. */
|
|
25
27
|
tag?: string;
|
|
26
28
|
now?: Date;
|
|
29
|
+
/** The days the business opens, by name (`monday`…); the week of days strikes the others out. */
|
|
30
|
+
open?: string[];
|
|
31
|
+
/** The form's look — two steps, the week, the parts of the day, the drawn choice — on by default; `false` for the plain form, or the ones wanted: `'week parts'`. */
|
|
32
|
+
ui?: boolean | string;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/** What the form sends to /api/book, as `data-sends` says it. */
|
package/types/parts.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const PARTS: readonly ['morning', 'afternoon', 'evening'];
|
|
2
|
+
export const DAY_NAMES: readonly string[];
|
|
3
|
+
export function to24(text: string): string;
|
|
4
|
+
export function partOfHour(hour: number): 'morning' | 'afternoon' | 'evening';
|
|
5
|
+
export function partsOf(hours: number[], least?: number): ('morning' | 'afternoon' | 'evening')[];
|
|
6
|
+
export function ymd(d: Date): string;
|
|
7
|
+
export function parseDay(text: string): Date | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Progress } from './progress';
|
|
2
|
+
|
|
3
|
+
export type PhaseKey = 'brief' | 'laidOut' | 'writing' | 'judged' | 'attached' | 'building' | 'preview';
|
|
4
|
+
export type PhaseState = 'done' | 'current' | 'left' | 'unseen';
|
|
5
|
+
|
|
6
|
+
export const PHASES: readonly PhaseKey[];
|
|
7
|
+
export const PHASE_NAMES: Record<PhaseKey, string>;
|
|
8
|
+
export const STEP_NAMES: Record<string, string>;
|
|
9
|
+
|
|
10
|
+
/** The platform's half, as `/v1/dev-sessions/{id}/progress` answers it. */
|
|
11
|
+
export interface PlatformHalf {
|
|
12
|
+
site_id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
until: string;
|
|
15
|
+
judged?: { at: string; ok: boolean; findings: number };
|
|
16
|
+
attached: boolean;
|
|
17
|
+
served: boolean;
|
|
18
|
+
build?: { status: string; stage_num?: number; stage_of?: number; finished_at?: string; error?: string };
|
|
19
|
+
steps: { key: string; done: boolean; whose: string }[];
|
|
20
|
+
left: number;
|
|
21
|
+
panel: string;
|
|
22
|
+
/** Set by the dev server when the platform could not be read. */
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Where {
|
|
27
|
+
phases: { key: PhaseKey; state: PhaseState }[];
|
|
28
|
+
current: PhaseKey;
|
|
29
|
+
index: number;
|
|
30
|
+
/** Whether the platform's half was read. */
|
|
31
|
+
known: boolean;
|
|
32
|
+
/** Whether the kit's half was read. */
|
|
33
|
+
seen: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function phasesOf(progress: Progress | null | undefined, platform: PlatformHalf | null | undefined): Where;
|
|
37
|
+
export function sayPhase(where: Where, progress: Progress | null | undefined, platform: PlatformHalf | null | undefined): string;
|
|
38
|
+
export function meanwhile(platform: PlatformHalf | null | undefined): {
|
|
39
|
+
open: { key: string; name: string; done: boolean }[];
|
|
40
|
+
done: { key: string; name: string; done: boolean }[];
|
|
41
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** The brief's headings, in the order the guide writes them. */
|
|
2
|
+
export const BRIEF_SECTIONS: readonly string[];
|
|
3
|
+
|
|
4
|
+
export interface BriefRead {
|
|
5
|
+
/** Which sections have words in them. */
|
|
6
|
+
sections: Record<string, boolean>;
|
|
7
|
+
/** Rows in the Facts table. */
|
|
8
|
+
facts: number;
|
|
9
|
+
/** Items under «Open — NEEDS CLIENT». */
|
|
10
|
+
open: number;
|
|
11
|
+
/** How often `NEEDS CLIENT` appears outside its heading. */
|
|
12
|
+
needsClient: number;
|
|
13
|
+
/** The agent's own checklist under «Done when» — read out, never believed. */
|
|
14
|
+
doneWhen: { done: number; of: number; items: { done: boolean; says: string }[] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PageProgress {
|
|
18
|
+
path: string;
|
|
19
|
+
/** Written: some text, and every required text field filled (the exact rule); or any text (has-text). */
|
|
20
|
+
written: boolean;
|
|
21
|
+
filled: number;
|
|
22
|
+
of: number;
|
|
23
|
+
/** Required text fields still empty, under the exact rule. */
|
|
24
|
+
missing?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Progress {
|
|
28
|
+
version: 1;
|
|
29
|
+
/** `exact` with a catalogue, `has-text` without one — a weaker rule, said as such. */
|
|
30
|
+
rule: 'exact' | 'has-text';
|
|
31
|
+
kit: { kind: string; version: string } | null;
|
|
32
|
+
brief: BriefRead | null;
|
|
33
|
+
catalogue: boolean;
|
|
34
|
+
pages: PageProgress[];
|
|
35
|
+
collections: Record<string, { entries: number; expected: number }>;
|
|
36
|
+
settings: { hours: boolean; zone: boolean; payment: string } | null;
|
|
37
|
+
facts: { placeholders: number } | null;
|
|
38
|
+
media: number;
|
|
39
|
+
/** What is still to do, as sentences — the same list the panel shows. */
|
|
40
|
+
missing: string[];
|
|
41
|
+
/** The newest change among the files read, ISO 8601; null when none exist. */
|
|
42
|
+
changedAt: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read the working tree and say where the site is. */
|
|
46
|
+
export function readProgress(root: string, options?: { dir?: string }): Promise<Progress>;
|
|
47
|
+
/** The brief as a document, or null where there is none. */
|
|
48
|
+
export function readBrief(file: string): Promise<BriefRead | null>;
|
|
49
|
+
/** The list as a person reads it in a terminal. */
|
|
50
|
+
export function sayProgress(progress: Progress): string;
|