@jtakeit/kit 0.5.1 → 0.6.0
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 +1 -1
- package/elements/booking.js +8 -2
- package/lib/booking/render.mjs +9 -1
- package/lib/catalogue.mjs +96 -1
- package/lib/content.mjs +7 -2
- package/lib/kits.mjs +5 -1
- package/lib/lint.mjs +2 -0
- package/lib/meta.mjs +11 -3
- package/lib/schedule.mjs +46 -0
- package/package.json +1 -1
- package/types/booking.d.ts +1 -0
- package/types/content.d.ts +1 -1
- package/types/meta.d.ts +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ shape, so the form says what it sends and the element sends nothing else.
|
|
|
31
31
|
import { renderBookingForm } from '@jtakeit/kit/booking';
|
|
32
32
|
|
|
33
33
|
const html = renderBookingForm({
|
|
34
|
-
services, // [{ slug, title, takes, costs, rates, group }], the entries with `takes`; `rates` instead of `costs` says «from
|
|
34
|
+
services, // [{ slug, title, takes, costs, rates, group }], the entries with `takes`; `rates` instead of `costs` says «from …»; `group` is a heading's words (a select's label, a category's title), and the services with none come after every heading
|
|
35
35
|
resources, // [{ slug, title }] — leave empty for a solo business
|
|
36
36
|
locale: 'uk', // uk, de or en: the words before the platform has spoken
|
|
37
37
|
combine: settings.combine === true, // jtk/bookings.json
|
package/elements/booking.js
CHANGED
|
@@ -636,6 +636,12 @@ export function wireBooking(form) {
|
|
|
636
636
|
return;
|
|
637
637
|
}
|
|
638
638
|
hint.textContent = '';
|
|
639
|
+
// A price beside each time only where the times differ in price — a
|
|
640
|
+
// rate by the hour (wiki/68). Where every time costs the same, the
|
|
641
|
+
// price is the services' and is said beside them: «09:00 · CHF 95» on
|
|
642
|
+
// thirty pills is one number read thirty times (11 October 2026).
|
|
643
|
+
const slotPrices = new Set(data.slots.map((one) => one.costs_minor ?? 0));
|
|
644
|
+
const pricedByTime = slotPrices.size > 1;
|
|
639
645
|
for (const slot of data.slots) {
|
|
640
646
|
const button = document.createElement('button');
|
|
641
647
|
button.type = 'button';
|
|
@@ -646,8 +652,8 @@ export function wireBooking(form) {
|
|
|
646
652
|
? slot.ends ? `${dayAndHour(slot.start, false)} — ${dayAndHour(slot.ends, false)}` : dayAndHour(slot.start, false)
|
|
647
653
|
: clock(slot.start);
|
|
648
654
|
// What the booking starting here costs — the platform's number, every
|
|
649
|
-
// hour by its own row (wiki/68) — beside the time,
|
|
650
|
-
if (slot.costs_minor > 0) button.textContent += ` · ${money(slot.costs_minor, currency, pageLang())}`;
|
|
655
|
+
// hour by its own row (wiki/68) — beside the time, where it varies.
|
|
656
|
+
if (pricedByTime && slot.costs_minor > 0) button.textContent += ` · ${money(slot.costs_minor, currency, pageLang())}`;
|
|
651
657
|
button.dataset.start = slot.start;
|
|
652
658
|
button.addEventListener('click', () => {
|
|
653
659
|
chosen = slot.start;
|
package/lib/booking/render.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export const SENDS = Object.freeze(['service', 'services', 'resource', 'class',
|
|
|
48
48
|
* @property {number} [costs] minor units, for the same
|
|
49
49
|
* @property {{ day: string, from: number, until: number, costs: number }[]} [rates] a price by the hour of the week instead of `costs`: the list says «from …», and the price is the slot's, printed beside the time
|
|
50
50
|
* @property {string} [per] `'person'` for a price times the party — a class per place, shoes; the list says so and the running total multiplies
|
|
51
|
-
* @property {string} [group] a heading the services are listed under
|
|
51
|
+
* @property {string} [group] a heading the services are listed under — the words (an option's label, a category's title), not the stored value; services with none are listed after every heading
|
|
52
52
|
* @property {string[]} [does] for a resource: the services they perform, by slug — the element greys the others out once they are chosen; empty or absent is everything
|
|
53
53
|
*/
|
|
54
54
|
|
|
@@ -123,6 +123,14 @@ export function renderBookingForm(props) {
|
|
|
123
123
|
if (run) run.items.push(one);
|
|
124
124
|
else runs.push({ group, items: [one] });
|
|
125
125
|
}
|
|
126
|
+
// The services with no heading go after every run that has one, wherever
|
|
127
|
+
// the first of them came: drawn first, they read as the run above the first
|
|
128
|
+
// heading, and drawn between two they read as the one before. A group is
|
|
129
|
+
// the heading's words — an option's label, a category's title — never the
|
|
130
|
+
// stored value; the page that reads the catalogue resolves it (12 October
|
|
131
|
+
// 2026: the scaffold passed `entry.data.group` itself).
|
|
132
|
+
const loose = runs.findIndex((r) => r.group === '');
|
|
133
|
+
if (loose >= 0 && runs.length > 1) runs.push(...runs.splice(loose, 1));
|
|
126
134
|
|
|
127
135
|
const today = now.toISOString().slice(0, 10);
|
|
128
136
|
const last = new Date(now.getTime() + daysAhead * 86_400_000).toISOString().slice(0, 10);
|
package/lib/catalogue.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
51
51
|
import { pathToFileURL } from 'node:url';
|
|
52
52
|
|
|
53
53
|
import { E, isAdvice, isPlatformCode, LOCAL } from './codes.mjs';
|
|
54
|
+
import { shownAt } from './schedule.mjs';
|
|
54
55
|
|
|
55
56
|
// What this tool calls itself in a catalogue it writes. Bumped when the shape
|
|
56
57
|
// it emits changes; the `contract` beside it moves only when the format does.
|
|
@@ -587,11 +588,49 @@ for (const collection of collections) {
|
|
|
587
588
|
}
|
|
588
589
|
}
|
|
589
590
|
|
|
591
|
+
/*
|
|
592
|
+
* What the entries are grouped by: a select, or a ref to another
|
|
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
|
|
595
|
+
* with three spellings of "manicure". A ref into the collection itself is a
|
|
596
|
+
* tree, which is a `parent` field and takes no part in grouping: an entry
|
|
597
|
+
* heading its own siblings is a listing nobody can order.
|
|
598
|
+
*/
|
|
599
|
+
if (collection.group !== undefined && collection.group !== '') {
|
|
600
|
+
const groupWhere = `${where}.group`;
|
|
601
|
+
const by = field(collection.group);
|
|
602
|
+
if (by === undefined) {
|
|
603
|
+
fault(E.COLLECTION, groupWhere, `grouped by "${collection.group}", which ${collection.type} does not declare`);
|
|
604
|
+
} else if (by.kind === 'ref' && by.ref === collection.name) {
|
|
605
|
+
fault(E.COLLECTION, groupWhere, `grouped by ${collection.type}.${collection.group}, which points into "${collection.name}" itself — group by another collection, and make a tree with a parent field`);
|
|
606
|
+
} else if (by.kind !== 'select' && by.kind !== 'ref') {
|
|
607
|
+
fault(E.COLLECTION, groupWhere, `grouped by ${collection.type}.${collection.group}, which is "${by.kind}" — group by a select, or by a ref to another collection`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
590
611
|
const perPage = collection.per_page ?? 0;
|
|
591
612
|
if (!Number.isInteger(perPage) || perPage < 0 || perPage > 200) {
|
|
592
613
|
fault(E.COLLECTION, where, 'per_page must be between 1 and 200, or absent for one page');
|
|
593
614
|
}
|
|
594
615
|
|
|
616
|
+
/*
|
|
617
|
+
* A listing at the prefix, asked for (the platform's CollectionDef.Listing,
|
|
618
|
+
* 12 October 2026): the scaffold draws one only where a collection says
|
|
619
|
+
* `listing: true`, because a page nobody asked for is an address on a
|
|
620
|
+
* client's site nobody designed. Only for a collection whose entries have
|
|
621
|
+
* pages — one without is listed on its prefix's page already, which the
|
|
622
|
+
* repository writes.
|
|
623
|
+
*/
|
|
624
|
+
if (collection.listing !== undefined && typeof collection.listing !== 'boolean') {
|
|
625
|
+
fault(E.COLLECTION, `${where}.listing`, `listing is ${JSON.stringify(collection.listing)} — true, or absent for no listing at the prefix`);
|
|
626
|
+
} else if (collection.listing === true && collection.pages === false) {
|
|
627
|
+
fault(
|
|
628
|
+
E.COLLECTION,
|
|
629
|
+
`${where}.listing`,
|
|
630
|
+
`collection "${collection.name}" has no pages of its own, so its prefix's page is the listing already — the repository writes it; \`listing\` is for a collection whose entries have pages`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
595
634
|
/*
|
|
596
635
|
* What a post may hold, and the one type it must.
|
|
597
636
|
*
|
|
@@ -619,6 +658,30 @@ for (const collection of collections) {
|
|
|
619
658
|
}
|
|
620
659
|
}
|
|
621
660
|
|
|
661
|
+
/*
|
|
662
|
+
* Every `ref`, wherever it is — a block's field, a row of a list, the page's
|
|
663
|
+
* SEO, the business facts — names a collection this catalogue declares. The
|
|
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:
|
|
666
|
+
* ref tags }` with no `tags` collection is a field whose every value points
|
|
667
|
+
* nowhere, and the judge refused it while this said nothing.
|
|
668
|
+
*/
|
|
669
|
+
{
|
|
670
|
+
const names = new Set(collections.map((one) => one.name));
|
|
671
|
+
const walk = (where, fields) => {
|
|
672
|
+
for (const one of fields ?? []) {
|
|
673
|
+
const at = `${where}.${one.key}`;
|
|
674
|
+
if (one.kind === 'ref' && !names.has(one.ref)) {
|
|
675
|
+
fault(E.COLLECTION, `${at}.ref`, `${at} refers to collection ${JSON.stringify(one.ref ?? '')}, which this catalogue does not declare`);
|
|
676
|
+
}
|
|
677
|
+
walk(at, one.of);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
catalogue.blocks.forEach((block, at) => walk(`blocks[${at}]`, block.fields));
|
|
681
|
+
walk('page_seo', catalogue.page_seo);
|
|
682
|
+
walk('business_facts', catalogue.business_facts);
|
|
683
|
+
}
|
|
684
|
+
|
|
622
685
|
/*
|
|
623
686
|
* ── can the platform serve this at all ─────────────────────────────────────
|
|
624
687
|
*
|
|
@@ -1346,6 +1409,21 @@ const sharedBlocks = existsSync(sharedFile)
|
|
|
1346
1409
|
const byType = new Map(catalogue.blocks.map((block) => [block.type, block]));
|
|
1347
1410
|
const disagreements = [];
|
|
1348
1411
|
|
|
1412
|
+
/*
|
|
1413
|
+
* Which fields a page must carry a tap target for: the platform's own rule,
|
|
1414
|
+
* `tappable()` in its sitebuild/annotation-lint.mjs, copied (12 October 2026).
|
|
1415
|
+
*
|
|
1416
|
+
* 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
|
|
1418
|
+
* site has categories and tags: a service's `category: ref categories`, a
|
|
1419
|
+
* dish's `course` select, a work's `tags: [{ tag }]` were each reported as a
|
|
1420
|
+
* control that edits nothing — they are edited in the panel beside the page,
|
|
1421
|
+
* and the platform's build asks no element of them. A field outside the rule
|
|
1422
|
+
* may still be annotated; it is accepted, never required.
|
|
1423
|
+
*/
|
|
1424
|
+
const TAPPABLE = new Set(['text', 'textarea', 'richtext_lite', 'markdown', 'media', 'money', 'duration']);
|
|
1425
|
+
const tappable = (field) => TAPPABLE.has(field.kind) && field.client_editable !== false && field.no_tap_target !== true;
|
|
1426
|
+
|
|
1349
1427
|
/** `/de/services/x` without its language: `/services/x`. The site's own language has no prefix. */
|
|
1350
1428
|
function basePathOf(path, locales) {
|
|
1351
1429
|
for (const locale of locales) {
|
|
@@ -1370,6 +1448,13 @@ for (const page of pages) {
|
|
|
1370
1448
|
(c) => c.pages === false && (page.collection === c.name || bare.startsWith(`${c.prefix}/`)),
|
|
1371
1449
|
);
|
|
1372
1450
|
if (listedOnly) {
|
|
1451
|
+
/*
|
|
1452
|
+
* A hidden one is on no listing — `visible: false`, or a schedule not
|
|
1453
|
+
* come or gone (schedule.mjs), which the platform writes `visible:
|
|
1454
|
+
* false` before its build — and is owed nowhere, as the platform's
|
|
1455
|
+
* lint says (`listable`). This asked it of a hidden one too.
|
|
1456
|
+
*/
|
|
1457
|
+
if (!shownAt(page)) continue;
|
|
1373
1458
|
/*
|
|
1374
1459
|
* No page of its own, so its fields are annotated on the listing as
|
|
1375
1460
|
* `page:<address>:<field>` — and the platform's lint fails a build
|
|
@@ -1389,7 +1474,7 @@ for (const page of pages) {
|
|
|
1389
1474
|
const value = block[field.key];
|
|
1390
1475
|
const empty = value === undefined || value === null || value === '' ||
|
|
1391
1476
|
(Array.isArray(value) && value.length === 0);
|
|
1392
|
-
if (empty || field
|
|
1477
|
+
if (empty || !tappable(field) || (field.kind === 'media' && field.multiple)) continue;
|
|
1393
1478
|
if (!everywhere.has(`page:${page.path}:blocks[${at}].${field.key}`)) {
|
|
1394
1479
|
disagreements.push(
|
|
1395
1480
|
`${page.path}: blocks[${at}].${field.key} is on no page — an entry without a page of its own is annotated on its listing as page:${page.path}:blocks[${at}].${field.key}`,
|
|
@@ -1419,6 +1504,12 @@ for (const page of pages) {
|
|
|
1419
1504
|
if (empty || field.no_tap_target || field.kind === 'bool') continue;
|
|
1420
1505
|
|
|
1421
1506
|
const path = `blocks[${at}].${field.key}`;
|
|
1507
|
+
// Outside the platform's rule and not a list (whose rows are asked one
|
|
1508
|
+
// by one below): accepted where it is annotated, asked for nowhere.
|
|
1509
|
+
if (field.kind !== 'list' && !tappable(field)) {
|
|
1510
|
+
onThePage.delete(path);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1422
1513
|
|
|
1423
1514
|
/*
|
|
1424
1515
|
* A gallery: one field, many pictures, and each picture is its own tap
|
|
@@ -1455,6 +1546,10 @@ for (const page of pages) {
|
|
|
1455
1546
|
for (const sub of field.of ?? []) {
|
|
1456
1547
|
if (sub.no_tap_target || !row[sub.key]) continue;
|
|
1457
1548
|
const rowPath = `blocks[${at}].${field.key}[${i}].${sub.key}`;
|
|
1549
|
+
if (!tappable(sub)) {
|
|
1550
|
+
onThePage.delete(rowPath);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1458
1553
|
if (!onThePage.has(rowPath)) {
|
|
1459
1554
|
disagreements.push(`${rowPath} is declared and the page does not render it`);
|
|
1460
1555
|
}
|
package/lib/content.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readFile, readdir } from 'node:fs/promises';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { shownAt } from './schedule.mjs';
|
|
13
14
|
|
|
14
15
|
/** @param {string} path */
|
|
15
16
|
async function readJSON(path) {
|
|
@@ -59,9 +60,10 @@ export function sharedLocaleOf(name) {
|
|
|
59
60
|
* anyway with the site's facts left empty — the annotations do not depend on
|
|
60
61
|
* them.
|
|
61
62
|
* @param {string} root the site's root, holding `jtk/`
|
|
62
|
-
* @param {{ dir?: string }} [options] `dir`: where `jtk/` is, relative to the root
|
|
63
|
+
* @param {{ dir?: string; now?: number }} [options] `dir`: where `jtk/` is, relative to the root; `now`: the clock a `show_at`/`hide_at` is read against
|
|
63
64
|
*/
|
|
64
65
|
export async function collectContent(root, options = {}) {
|
|
66
|
+
const now = options.now ?? Date.now();
|
|
65
67
|
const dir = join(root, options.dir ?? 'jtk');
|
|
66
68
|
const site = (await readJSON(join(dir, 'site.json'))) ?? {};
|
|
67
69
|
const pages = [];
|
|
@@ -72,7 +74,10 @@ export async function collectContent(root, options = {}) {
|
|
|
72
74
|
const collected = { path: page.path, seo: page.seo ?? {}, blocks: page.blocks ?? [] };
|
|
73
75
|
if (typeof page.collection === 'string' && page.collection !== '') {
|
|
74
76
|
collected.collection = page.collection;
|
|
75
|
-
|
|
77
|
+
// Hidden by `visible: false`, or by a schedule not come or gone
|
|
78
|
+
// (schedule.mjs) — read by the clock of this check, as the platform's
|
|
79
|
+
// build reads it by its own.
|
|
80
|
+
collected.visible = shownAt(page, now);
|
|
76
81
|
}
|
|
77
82
|
pages.push(collected);
|
|
78
83
|
}
|
package/lib/kits.mjs
CHANGED
|
@@ -137,7 +137,11 @@ export function kitEntries(kit, locale) {
|
|
|
137
137
|
const collection = kit.catalogue.collections.find((one) => one.name === entry.collection);
|
|
138
138
|
const block = { _key: `kit${String(i + 1).padStart(5, '0')}`, type: collection.type, v: 1 };
|
|
139
139
|
for (const [key, value] of Object.entries(entry.fields)) {
|
|
140
|
-
|
|
140
|
+
// An object is the words per language; a list — a master's `does`, a
|
|
141
|
+
// post's `tags: [{ tag }]` — is the value itself. It was read as a map
|
|
142
|
+
// of languages and laid out as '' (12 October 2026), which broke the
|
|
143
|
+
// listing that maps a resource's `does`.
|
|
144
|
+
block[key] = Array.isArray(value) ? value : value !== null && typeof value === 'object' ? (value[lang] ?? '') : value;
|
|
141
145
|
}
|
|
142
146
|
const path = `${collection.prefix}/${entry.slug}`;
|
|
143
147
|
const doc = { path, collection: collection.name, visible: true, seo: { title: '', description: '' }, blocks: [block] };
|
package/lib/lint.mjs
CHANGED
|
@@ -301,6 +301,8 @@ export async function lint(options = {}) {
|
|
|
301
301
|
// Entries of a collection without pages: on their listing, annotated there.
|
|
302
302
|
const pageless = new Set((registry?.collections ?? []).filter((one) => one.pages === false).map((one) => one.name));
|
|
303
303
|
const everywhere = [...htmlByPath.values()].join('\n');
|
|
304
|
+
// `visible` as collectContent read it: false for `visible: false` and for a
|
|
305
|
+
// schedule not come or gone (schedule.mjs) — a listing leaves both out.
|
|
304
306
|
const listable = (page) => page.visible !== false;
|
|
305
307
|
for (const one of registry?.collections ?? []) {
|
|
306
308
|
if (one.pages !== false) continue;
|
package/lib/meta.mjs
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
36
36
|
import { join, relative, sep } from 'node:path';
|
|
37
|
+
import { shownAt } from './schedule.mjs';
|
|
37
38
|
|
|
38
39
|
/** The id of one specimen, which is also its address under the collection. */
|
|
39
40
|
export function specimenId(type, view) {
|
|
@@ -76,12 +77,16 @@ export function specimenAddresses(collections, blocks, locales = []) {
|
|
|
76
77
|
*
|
|
77
78
|
* A document, not frontmatter: an entry is a page of blocks in a `.json`
|
|
78
79
|
* file. Only an entry carries `visible` at all; a page has none and is not a
|
|
79
|
-
* draft. `=== false` and not falsy: absent means listed
|
|
80
|
+
* draft. `=== false` and not falsy: absent means listed — and so does a
|
|
81
|
+
* schedule that has come and not gone (`shownAt`, schedule.mjs): a `show_at`
|
|
82
|
+
* still ahead or a `hide_at` behind is a draft at the moment of the build,
|
|
83
|
+
* as it is on the platform, which writes it `visible: false` before building.
|
|
80
84
|
*
|
|
81
85
|
* @param {string} [root] where the content is, `jtk/content` under the working directory
|
|
86
|
+
* @param {number} [now] the clock a schedule is read against, the build's by default
|
|
82
87
|
* @returns {Promise<string[]>}
|
|
83
88
|
*/
|
|
84
|
-
export async function hiddenEntries(root = join(process.cwd(), 'jtk', 'content')) {
|
|
89
|
+
export async function hiddenEntries(root = join(process.cwd(), 'jtk', 'content'), now = Date.now()) {
|
|
85
90
|
const out = [];
|
|
86
91
|
const walk = async (at) => {
|
|
87
92
|
let entries;
|
|
@@ -104,7 +109,10 @@ export async function hiddenEntries(root = join(process.cwd(), 'jtk', 'content')
|
|
|
104
109
|
} catch {
|
|
105
110
|
continue; // unreadable JSON is the build's error to report, not this one
|
|
106
111
|
}
|
|
107
|
-
|
|
112
|
+
// A schedule is an entry's (it names its collection); a page carries
|
|
113
|
+
// none, and `visible: false` is read wherever it is written, as before.
|
|
114
|
+
const isEntry = typeof document?.collection === 'string' && document.collection !== '';
|
|
115
|
+
if (document?.visible === false || (isEntry && !shownAt(document, now))) {
|
|
108
116
|
const name = relative(root, full).split(sep).join('/').replace(/\.json$/, '');
|
|
109
117
|
out.push(`/${name}/`);
|
|
110
118
|
}
|
package/lib/schedule.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an entry is out yet, and still out — the platform's scheduled
|
|
3
|
+
* publishing, read the way the platform reads it.
|
|
4
|
+
*
|
|
5
|
+
* An entry's file may carry `"show_at"` (hidden until then) and `"hide_at"`
|
|
6
|
+
* (hidden from then on), both RFC 3339 in UTC. A build the platform makes
|
|
7
|
+
* rewrites such an entry to `visible: false` in its checkout before the
|
|
8
|
+
* generator runs, so on the platform this changes nothing. On a laptop the
|
|
9
|
+
* files are read as they are, and every reader here that asks `visible` — the
|
|
10
|
+
* drafts in `_meta.json` (meta.mjs), the content the check reads (content.mjs,
|
|
11
|
+
* and so lint.mjs) — asks this instead, so a post scheduled for next week is a
|
|
12
|
+
* draft and not a listing's missing row.
|
|
13
|
+
*
|
|
14
|
+
* `visible: false` hides an entry whatever its dates say. A value that is not
|
|
15
|
+
* a time is no schedule: the platform refuses one where it is written, and a
|
|
16
|
+
* file edited by hand that says `"next week"` is shown rather than hidden for
|
|
17
|
+
* a reason nobody can see.
|
|
18
|
+
*
|
|
19
|
+
* The scaffold's entry loader asks the same question in
|
|
20
|
+
* `template/src/lib/schedule.ts` — a copy, not an import: a scaffold is built
|
|
21
|
+
* against the published kit, which may not have this file yet. The two are
|
|
22
|
+
* held to one answer by `test/schedule.test.mjs` at the repository's root.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the site lists the entry at `now` (milliseconds since the epoch).
|
|
27
|
+
* @param {{ visible?: boolean; show_at?: unknown; hide_at?: unknown } | null | undefined} document
|
|
28
|
+
* @param {number} [now]
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function shownAt(document, now = Date.now()) {
|
|
32
|
+
if (document == null) return true;
|
|
33
|
+
if (document.visible === false) return false;
|
|
34
|
+
const show = instant(document.show_at);
|
|
35
|
+
if (show !== undefined && now < show) return false;
|
|
36
|
+
const hide = instant(document.hide_at);
|
|
37
|
+
if (hide !== undefined && now >= hide) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A time written down, or `undefined` for none and for one that is not a time. */
|
|
42
|
+
function instant(value) {
|
|
43
|
+
if (typeof value !== 'string' || value.trim() === '') return undefined;
|
|
44
|
+
const at = Date.parse(value);
|
|
45
|
+
return Number.isNaN(at) ? undefined : at;
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtakeit/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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/booking.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Named {
|
|
|
7
7
|
rates?: { day: string; from: number; until: number; costs: number }[];
|
|
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
|
+
/** The heading a service is listed under: the words — an option's label, a category's title — not the stored value. Services with none come after every heading. */
|
|
10
11
|
group?: string;
|
|
11
12
|
/** For a resource: the services they perform, by slug. The element greys the others out once they are chosen. */
|
|
12
13
|
does?: string[];
|
package/types/content.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The published content as one document, from the files in `jtk/` — what the build renders from. */
|
|
2
|
-
export function collectContent(root: string, options?: { dir?: string }): Promise<{
|
|
2
|
+
export function collectContent(root: string, options?: { dir?: string; now?: number }): Promise<{
|
|
3
3
|
schema_version: number;
|
|
4
4
|
content_rev: number;
|
|
5
5
|
site: { slug: string; locale: string; media_base: string; business: Record<string, unknown> };
|
package/types/meta.d.ts
CHANGED
|
@@ -21,6 +21,6 @@ export function specimenAddresses(
|
|
|
21
21
|
blocks: { type: string; views?: { key: string }[] }[],
|
|
22
22
|
locales?: string[],
|
|
23
23
|
): string[];
|
|
24
|
-
export function hiddenEntries(root?: string): Promise<string[]>;
|
|
24
|
+
export function hiddenEntries(root?: string, now?: number): Promise<string[]>;
|
|
25
25
|
export function buildMeta(what?: MetaInput): Promise<Meta>;
|
|
26
26
|
export function writeMeta(dir: string, what?: MetaInput): Promise<Meta>;
|