@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 +174 -2
- package/bin/jtkit.mjs +62 -0
- package/booking.css +36 -0
- package/elements/alive.js +52 -0
- package/elements/booking.js +696 -0
- package/elements/lead.js +134 -0
- package/index.mjs +21 -0
- package/lead.css +23 -0
- package/lib/booking/render.mjs +210 -0
- package/lib/booking/words.mjs +88 -0
- package/lib/catalogue.mjs +1819 -0
- package/lib/codes.mjs +171 -0
- package/lib/kits.mjs +208 -0
- package/lib/lead/render.mjs +166 -0
- package/lib/meta.mjs +150 -0
- package/lib/money.mjs +30 -0
- package/package.json +51 -4
- package/types/alive.d.ts +2 -0
- package/types/booking.d.ts +28 -0
- package/types/elements-booking.d.ts +9 -0
- package/types/elements-lead.d.ts +6 -0
- package/types/index.d.ts +6 -0
- package/types/kits.d.ts +24 -0
- package/types/lead.d.ts +49 -0
- package/types/meta.d.ts +26 -0
- package/types/money.d.ts +2 -0
- package/types/words.d.ts +3 -0
package/README.md
CHANGED
|
@@ -1,3 +1,175 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jtakeit/kit
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|