@jtakeit/kit 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,175 @@
1
- # Temporary Holding Version
1
+ # @jtakeit/kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The half of a site on [jtakeit](https://jtakeit.com) that does not depend on
4
+ how the site is built.
5
+
6
+ [`@jtakeit/astro`](https://www.npmjs.com/package/@jtakeit/astro) is the
7
+ scaffold the platform ships: an Astro project with the contract already on
8
+ it. Everything in it that is not Astro's lives here, so a site on any other
9
+ static generator — Eleventy, a Next export, Nuxt, SvelteKit, a Node script —
10
+ takes the same pieces instead of reproducing them. The scaffold itself
11
+ imports this package; there is one implementation of each piece and two
12
+ homes for it.
13
+
14
+ What the platform holds a repository to is in
15
+ [connecting a site](https://jtakeit.com/docs/guides/connecting-a-site), under
16
+ *On another generator*. This package is the pieces of that list that are
17
+ worth not writing twice.
18
+
19
+ ## The booking form
20
+
21
+ The platform's diary, on the page: the free times fetched, a stay of several
22
+ nights, a class of cars, a party, the words in the business's kind, the
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);
25
+ `preflight` reads `data-sends` off the built page and books in exactly that
26
+ shape, so the form says what it sends and the element sends nothing else.
27
+
28
+ **At build time**, the markup:
29
+
30
+ ```js
31
+ import { renderBookingForm } from '@jtakeit/kit/booking';
32
+
33
+ const html = renderBookingForm({
34
+ services, // [{ slug, title, takes, costs, group }], the entries with `takes`
35
+ resources, // [{ slug, title }] — leave empty for a solo business
36
+ locale: 'uk', // uk, de or en: the words before the platform has spoken
37
+ combine: settings.combine === true, // jtk/bookings.json
38
+ scale: settings.scale ?? '', // 'daily' for a business that lets by the day
39
+ kind: settings.kind ?? '', // whose words to wait for
40
+ currency: site.business.currency,
41
+ api: `${base}api/`, // where /api/ is from this page — under the preview the site is served at /p/<slug>/
42
+ });
43
+ ```
44
+
45
+ Put the string on the page where the site sends people to book — the one
46
+ `page` in `jtk/bookings.json` names. In Eleventy that is a shortcode; in a
47
+ Next export `dangerouslySetInnerHTML`; in a script, a template literal.
48
+
49
+ **On the page**, the behaviour, once:
50
+
51
+ ```html
52
+ <script type="module">import '@jtakeit/kit/elements/booking';</script>
53
+ ```
54
+
55
+ The markup comes wrapped in `<jtk-booking>`, and the element wires the form
56
+ when it reaches the document — on load, and when the panel places it later.
57
+ A form written by hand is wired too, as long as it keeps the `data-*` hooks
58
+ and the input `name`s of the rendered one: `wireAll()` walks the document at
59
+ load and again on the panel's `fl:placed`.
60
+
61
+ **The look** is the site's to give. `@jtakeit/kit/booking.css` makes the form
62
+ legible and nothing more; `field`, `field__input`, `booking__slot`,
63
+ `booking__submit` are names to restyle with the site's own fields and
64
+ buttons. Keep the `[hidden]` rule.
65
+
66
+ Under `base` — a site served from a path, which every preview is — pass
67
+ `api` as your generator writes a root-relative address, so the form posts to
68
+ `/p/<slug>/api/book` on the preview and to `/api/book` on the site's own host.
69
+
70
+ ## The enquiry form
71
+
72
+ `/api/lead`, which the platform serves beside every site. The form is a real
73
+ `<form method="post">` first — with scripts off the browser posts it and the
74
+ platform answers with a page — and the element only spares a working browser
75
+ the navigation. What the build's lint holds it to: a `name` control, one of
76
+ `contact`, `phone` or `email`, the honeypot `website`, and nothing else that
77
+ the collecting block does not declare in `asks`
78
+ ([the contract](https://jtakeit.com/docs/reference/contract.md)).
79
+
80
+ ```js
81
+ import { renderLeadForm } from '@jtakeit/kit/lead';
82
+
83
+ const html = renderLeadForm({
84
+ action: `${base}api/lead`, // under the preview the site is served at /p/<slug>/
85
+ copy: { title, lead, name, contact, message, requiredNote, submit, sending, success, error, invalidName, invalidContact },
86
+ asks: [{ key: 'topic', label: 'What it is about', kind: 'text', max: 120 }], // the block's `asks`, labels resolved
87
+ paths: { title: 'blocks[3].title', lead: 'blocks[3].lead', name_label: 'blocks[3].name_label', /* … */ }, // the block's fields' data-jtk-path, when annotating
88
+ });
89
+ ```
90
+
91
+ ```html
92
+ <script type="module">import '@jtakeit/kit/elements/lead';</script>
93
+ ```
94
+
95
+ `copy` is the collecting block's own words — the owner edits them by tapping,
96
+ which is what `paths` annotates — and `asks` are the catalogue's, drawn fixed.
97
+ `@jtakeit/kit/lead.css` is the legible minimum; keep `.trap` off-screen
98
+ whatever you restyle.
99
+
100
+ ## `_meta.json`, and the specimen pages
101
+
102
+ What the edge needs to know that the HTML cannot say: the redirects, the
103
+ unfinished entries (built, and served only to an editing session), and the
104
+ directory whose file names carry a content hash, which the edge caches for a
105
+ year. The scaffold writes it from an Astro hook; any build step can:
106
+
107
+ ```js
108
+ import { writeMeta } from '@jtakeit/kit/meta';
109
+
110
+ await writeMeta('dist', {
111
+ collections, blocks, locales, // the declaration: the specimens are derived from it
112
+ immutable: ['/_next/static/'], // where this generator keeps its hashed files; absent, the edge assumes Astro's
113
+ redirects: [{ from: '/old', to: '/new', status: 301 }],
114
+ });
115
+ ```
116
+
117
+ The **specimen pages** are the one thing in that list a site has to build as
118
+ well as declare: one page per block a post may hold, at
119
+ `<collection prefix>/_fl-<type>[-<view>]/` and per extra language — the panel
120
+ lifts the markup for a block the owner has just put into a post from there,
121
+ in the site's own stylesheet. `specimenAddresses(collections, blocks,
122
+ locales)` is the rule; build a page at each address that renders the block
123
+ once, and `writeMeta` lists them as drafts so a stranger never sees one.
124
+
125
+ A script that attaches to a selector attaches again on the markup the panel
126
+ places — `onAlive(selector, start)` from `@jtakeit/kit/elements/alive` is the
127
+ shape, and it listens for the panel's `fl:placed`.
128
+
129
+ ## A kit, laid out
130
+
131
+ A business of a kind the platform knows starts from its kit — the
132
+ collections, the rates and the settings, judged on the platform's side:
133
+
134
+ ```
135
+ npx @jtakeit/kit apply --kit car_rental --locale uk # asked of the platform by its kind
136
+ npx @jtakeit/kit apply --kit ./car_rental.json # or a file of it
137
+ ```
138
+
139
+ writes `jtk/bookings.json` with `page` at the services' listing and `kit` /
140
+ `kit_version` recorded, the rates as entries under `jtk/content`, and the
141
+ kit's block types, collections and module into `jtk/catalogue.json` (a type
142
+ or a collection the site already has by that name is left alone). The
143
+ scaffold does the same through `jtk create --kit`, writing the declaration
144
+ into `src/content/blocks.ts` instead of the catalogue file. `@jtakeit/kit/kits`
145
+ has the pieces — `loadKit`, `kitSettings`, `kitEntries`,
146
+ `mergeKitIntoCatalogue`, `layKitFiles` — for a tool of your own.
147
+
148
+ ## The catalogue tool
149
+
150
+ ```
151
+ npx @jtakeit/kit catalogue --declaration src/content/blocks.ts --emit-only
152
+ npx @jtakeit/kit catalogue --declaration src/content/blocks.ts --dist ./dist
153
+ npx @jtakeit/kit catalogue … --judge # and ask the platform's validator whether we agree
154
+ ```
155
+
156
+ Derives `jtk/catalogue.json` from a declaration module — the scaffold's
157
+ `src/content/blocks.ts` shape: `BLOCKS`, `PAGE_SEO`, `BUSINESS_FACTS`,
158
+ `COLLECTIONS`, `MODULES`, `LOCALES` — and checks it against a built `dist/`
159
+ both ways: every declared field rendered with its `data-jtk-path`, every
160
+ annotated path declared. On an Astro project it also builds once more under
161
+ a path to prove every address stays inside the preview; on another generator
162
+ build that way yourself (`SITE_URL=https://x.invalid/p/check/`) and pass
163
+ `--dist`. `@jtakeit/astro catalogue` is this tool, signing the file as the
164
+ scaffold.
165
+
166
+ ## Also here
167
+
168
+ - `@jtakeit/kit/money` — a price as the page says it: «2 300 ₴», no kopecks
169
+ on a whole price, no sign where the site has not said its currency.
170
+ - `@jtakeit/kit/booking/words` — the form's words per language, the salon's
171
+ and the letting business's by the day, before the platform corrects them.
172
+ - `@jtakeit/kit/codes` — the catalogue's error codes, a copy of the platform's
173
+ table that `--judge` keeps honest.
174
+
175
+ `@jtakeit/astro` stays the scaffold and is a thin layer over all of this.
package/bin/jtkit.mjs ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * jtkit — the command line of @jtakeit/kit, for a site on any generator.
4
+ *
5
+ * jtkit apply --kit <kind>|<file> [--locale uk] [--api <url>] [--root <dir>]
6
+ * lay a booking kit out: jtk/bookings.json, the rates as entries, and the
7
+ * kit's types, collections and module into jtk/catalogue.json
8
+ * jtkit catalogue [--declaration src/content/blocks.ts] [--emit-only] [--dist ./dist] [--judge]
9
+ * write jtk/catalogue.json from a declaration and check it against the built pages
10
+ */
11
+ import { pathToFileURL } from 'node:url';
12
+ import { join, dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { relative } from 'node:path';
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const [command, ...rest] = process.argv.slice(2);
18
+ const has = (name) => rest.includes(`--${name}`);
19
+ const flag = (name, fallback) => {
20
+ const at = rest.indexOf(`--${name}`);
21
+ return at === -1 ? fallback : rest[at + 1];
22
+ };
23
+
24
+ switch (command) {
25
+ case 'apply': {
26
+ const { loadKit, layKitFiles } = await import(pathToFileURL(join(HERE, '..', 'lib', 'kits.mjs')).href);
27
+ const asked = has('kit') ? flag('kit', '') : has('kind') ? flag('kind', '') : '';
28
+ if (asked === '') {
29
+ console.error('jtkit apply: --kit <kind> or --kit <file> says which kit');
30
+ process.exit(1);
31
+ }
32
+ const root = resolve(flag('root', process.cwd()));
33
+ try {
34
+ const kit = await loadKit(asked, flag('api', undefined));
35
+ const written = layKitFiles(root, kit, { locale: flag('locale', 'en'), catalogue: true });
36
+ console.log(`${written.length} files written under ${relative(process.cwd(), root) || '.'}:`);
37
+ for (const file of written) console.log(` ${relative(root, file)}`);
38
+ console.log(`\nThe «${kit.kind}» kit is laid out. Ask, never make up:`);
39
+ for (const line of kit.ask ?? []) console.log(` - ${line}`);
40
+ console.log(`What the kit chose for the business — say each back and get a yes: jtakeit:///kits/${kit.kind}.md, «Taken by default».`);
41
+ } catch (why) {
42
+ console.error(`jtkit apply: ${why.message}`);
43
+ process.exit(1);
44
+ }
45
+ break;
46
+ }
47
+ case 'catalogue':
48
+ case 'catalog': {
49
+ process.argv = [process.argv[0], process.argv[1], ...rest];
50
+ await import(pathToFileURL(join(HERE, '..', 'lib', 'catalogue.mjs')).href);
51
+ break;
52
+ }
53
+ default: {
54
+ console.error(`@jtakeit/kit — the framework-free half of a site on jtakeit
55
+
56
+ jtkit apply --kit <kind>|<file> [--locale uk] [--api <url>] [--root <dir>]
57
+ jtkit catalogue [--declaration src/content/blocks.ts] [--emit-only] [--dist ./dist] [--judge]
58
+
59
+ The contract the platform holds a repository to: https://jtakeit.com/docs/guides/connecting-a-site`);
60
+ process.exit(command === undefined || command === '--help' || command === '-h' ? 0 : 1);
61
+ }
62
+ }
package/booking.css ADDED
@@ -0,0 +1,36 @@
1
+ /*
2
+ * The booking form, legible and nothing more.
3
+ *
4
+ * A reference, not a design: `field`, `field__input`, `booking__slot` and
5
+ * `booking__submit` are names for the site to restyle with its own fields and
6
+ * buttons, the way the scaffold's booking.md asks. Keep the `data-*` hooks and
7
+ * the `[hidden]` rule below whatever you do to the rest.
8
+ */
9
+ .booking { display: grid; gap: var(--space-4, 1rem); max-width: 34rem; }
10
+
11
+ /* The kind's words, until the platform has said them: the space kept, the
12
+ salon's word not shown at a car rental. */
13
+ .booking--unsaid [data-say], .booking--unsaid [data-say-wait] { visibility: hidden; }
14
+
15
+ .booking__times { border: 0; padding: 0; margin: 0; }
16
+ .booking__services { border: 0; padding: 0; margin: 0; display: grid; gap: 0.75rem; }
17
+ .booking__run { display: grid; gap: 0.35rem; }
18
+ .booking__run-title { margin: 0.5rem 0 0.15rem; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; opacity: 0.7; }
19
+ .booking__service { display: grid; grid-template-columns: auto 1fr auto; gap: 0.6rem; align-items: baseline; padding: 0.45rem 0; border-block-end: 1px solid var(--line, #e5e1d8); cursor: pointer; }
20
+ .booking__service-meta { font-size: 0.85em; opacity: 0.7; white-space: nowrap; }
21
+ .booking__total { margin: 0.25rem 0 0; font-weight: 600; }
22
+ .booking__slots { display: flex; flex-wrap: wrap; gap: 0.4rem; }
23
+ .booking__slot {
24
+ font: inherit; padding: 0.45rem 0.75rem; border-radius: var(--radius-sm, 0.5rem);
25
+ border: 1px solid var(--color-line, currentColor); background: transparent; color: inherit; cursor: pointer;
26
+ }
27
+ .booking__slot.is-chosen { background: var(--color-ink, #111); color: var(--color-bg, #fff); border-color: var(--color-ink, #111); }
28
+ .booking__hint, .booking__note { margin: 0; opacity: 0.75; }
29
+ .booking__links { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; margin: 0; }
30
+ .booking__done-line { font-weight: 600; margin: 0; }
31
+ .booking__done-when { margin: 0; }
32
+
33
+ /* `hidden` means hidden, whatever the element's class says: every state here
34
+ is shown and hidden by the attribute, and a class that sets `display` would
35
+ otherwise win (an agent's rehearsal, 30 September 2026). */
36
+ .booking [hidden] { display: none !important; }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Initialising something on the page, in the two places it can arrive.
3
+ *
4
+ * ── why there are two ───────────────────────────────────────────────────────
5
+ *
6
+ * Everything on a page arrives at load, and a script that queries the document
7
+ * once and attaches what it needs is correct — until something is put on the
8
+ * page afterwards. Then it is not: the new node was never seen, its listeners
9
+ * were never attached, and it sits there inert.
10
+ *
11
+ * That happens here for one reason and it is worth knowing. The platform's
12
+ * panel shows the owner a *preview of this site* while they write, and when
13
+ * they put a gallery into a post the built page has no element for it. Rather
14
+ * than drawing its own — it would be guessing at this repository's markup and
15
+ * CSS — it takes the arrangement from a specimen page this build made and puts
16
+ * it where the block goes. What arrives is this site's own markup, in this
17
+ * site's own stylesheet, and everything declarative about it is already right.
18
+ *
19
+ * What is not right is anything a script was supposed to do to it. So the
20
+ * panel fires `fl:placed` on the new node, and this is how a site listens.
21
+ *
22
+ * ── the shape it forces, which is the shape you wanted anyway ───────────────
23
+ *
24
+ * An initialiser registered here is called with a root and must work on it:
25
+ * once with the document at load, and again with a subtree when one appears. A
26
+ * function written that way is also the function that survives view
27
+ * transitions and anything else that swaps part of a page — the browser's own
28
+ * answer, `connectedCallback`, has the same shape for the same reason.
29
+ *
30
+ * Idempotent by scope rather than by flag: it is never called twice with the
31
+ * same node, because the node it is called with is the new one.
32
+ *
33
+ * @param {string} selector
34
+ * @param {(node: Element) => void} start
35
+ */
36
+ export function onAlive(selector, start) {
37
+ const wake = (root) => {
38
+ if (root instanceof Element && root.matches(selector)) start(root);
39
+ for (const node of root.querySelectorAll(selector)) start(node);
40
+ };
41
+
42
+ if (document.readyState === 'loading') {
43
+ document.addEventListener('DOMContentLoaded', () => wake(document), { once: true });
44
+ } else {
45
+ wake(document);
46
+ }
47
+
48
+ document.addEventListener('fl:placed', (event) => {
49
+ const at = event.target;
50
+ if (at instanceof Element) wake(at);
51
+ });
52
+ }